standalone
This commit is contained in:
@@ -17,42 +17,54 @@ import (
|
||||
|
||||
func PlecPage(w http.ResponseWriter, r *http.Request) {
|
||||
body := `
|
||||
<h2>PLEC Upload</h2>
|
||||
<p class="hint" style="margin-top:0;">Pick HTML files, give each an iteration name, then upload. The preview link is returned by the server.</p>
|
||||
<header class="tool-header">
|
||||
<h2 class="tool-title">PLEC Upload</h2>
|
||||
<p class="tool-description">Pick HTML files, give each an iteration name, then upload them to PLEC.</p>
|
||||
</header>
|
||||
|
||||
<table id="rows" style="width:100%;border-collapse:collapse;margin-bottom:12px;">
|
||||
<thead><tr>
|
||||
<th style="width:45%;text-align:left;padding:6px 8px;font-size:12px;text-transform:uppercase;opacity:0.7;">HTML File</th>
|
||||
<th style="width:45%;text-align:left;padding:6px 8px;font-size:12px;text-transform:uppercase;opacity:0.7;">Iteration Name</th>
|
||||
<th></th>
|
||||
</tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<div style="display:flex;gap:8px;">
|
||||
<button id="addRow" class="secondary">+ Add Row</button>
|
||||
<button id="upload">Upload</button>
|
||||
</div>
|
||||
|
||||
<div id="status" style="margin-top:12px;min-height:20px;"></div>
|
||||
<div id="results"></div>
|
||||
<section class="tool-panel">
|
||||
<div class="panel-header"><h3 class="panel-title">Inputs</h3></div>
|
||||
<div class="panel-body">
|
||||
<div class="control-group">
|
||||
<div class="file-row">
|
||||
<button id="pickFolder" class="secondary">Select Folder</button>
|
||||
<button id="pickFiles" class="secondary">Select File(s)</button>
|
||||
<button id="clear" class="secondary">Clear</button>
|
||||
</div>
|
||||
<div id="emptySelection" class="file-name" style="margin-top:8px;">(no files selected)</div>
|
||||
<div id="rowsPanel" class="results-panel is-hidden" style="margin-top:8px;">
|
||||
<table id="rows" class="data-table">
|
||||
<thead><tr><th>Filename</th><th style="width:240px;">Iteration Name</th><th style="width:44px;"></th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<button id="upload">Upload</button>
|
||||
<button id="openResult" class="secondary" disabled>Open</button>
|
||||
<button id="copyResult" class="secondary" disabled>Copy Link</button>
|
||||
</div>
|
||||
<div id="status" class="status-panel"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
#rows td { padding:6px 8px; vertical-align:middle; border-bottom:1px solid #333; }
|
||||
.file-cell { display:flex; align-items:center; gap:8px; }
|
||||
.file-name { opacity:0.85; font-size:12px; word-break:break-all; }
|
||||
.row-error { color:#f48771; font-size:12px; margin-top:4px; }
|
||||
.result { margin-top:12px; padding:10px; background:#252526; border-left:3px solid #007fd4; word-break:break-all; }
|
||||
.result a { color:#9cdcfe; }
|
||||
.result-actions { margin-top:8px; display:flex; gap:8px; }
|
||||
.remove-btn { background:transparent; color:#ddd; padding:4px 8px; }
|
||||
#rows input[type=text] { width: 100%; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const PAGE_SIZE = 5;
|
||||
let nextId = 1;
|
||||
let rows = [];
|
||||
let page = 0;
|
||||
let previewUrl = '';
|
||||
const tbody = document.querySelector('#rows tbody');
|
||||
const statusEl = document.getElementById('status');
|
||||
const resultsEl = document.getElementById('results');
|
||||
const rowsPanel = document.getElementById('rowsPanel');
|
||||
const emptySelection = document.getElementById('emptySelection');
|
||||
const openResultBtn = document.getElementById('openResult');
|
||||
const copyResultBtn = document.getElementById('copyResult');
|
||||
|
||||
function parseIterationName(filename) {
|
||||
const base = filename.replace(/\.html?$/i, '');
|
||||
@@ -63,65 +75,101 @@ function parseIterationName(filename) {
|
||||
return base;
|
||||
}
|
||||
|
||||
function addRow() {
|
||||
const id = 'r' + (nextId++);
|
||||
const tr = document.createElement('tr');
|
||||
tr.dataset.id = id;
|
||||
tr.innerHTML = ` + "`" + `
|
||||
<td>
|
||||
<div class="file-cell">
|
||||
<button class="pick secondary">Choose...</button>
|
||||
<span class="file-name" data-name>(no file)</span>
|
||||
</div>
|
||||
<input type="hidden" data-path />
|
||||
<div class="row-error" data-error></div>
|
||||
</td>
|
||||
<td><input type="text" data-iter placeholder="iteration name" style="width:100%;" /></td>
|
||||
<td><button class="remove-btn" title="Remove">×</button></td>
|
||||
` + "`" + `;
|
||||
tr.querySelector('.pick').addEventListener('click', async () => {
|
||||
const res = await fetch('/api/plec/pick', { method: 'POST' });
|
||||
const j = await res.json();
|
||||
if (j.files && j.files.length) {
|
||||
const affected = [];
|
||||
j.files.forEach(function(f, i) {
|
||||
const target = i === 0 ? tr : (addRow(), tbody.lastElementChild);
|
||||
target.querySelector('[data-path]').value = f.path;
|
||||
target.querySelector('[data-name]').textContent = f.name;
|
||||
const iter = target.querySelector('[data-iter]');
|
||||
if (!iter.value) iter.value = parseIterationName(f.name);
|
||||
affected.push(target);
|
||||
});
|
||||
if (affected.length > 1) {
|
||||
const iters = affected.map(r => r.querySelector('[data-iter]').value);
|
||||
if (iters[0] !== '' && iters.every(n => n === iters[0])) {
|
||||
affected.forEach((r, i) => {
|
||||
r.querySelector('[data-iter]').value = String(i + 1).padStart(2, '0') + '_' + iters[0];
|
||||
});
|
||||
}
|
||||
}
|
||||
function escapeHtml(s){
|
||||
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
}
|
||||
function addFiles(files) {
|
||||
const existing = new Set(rows.map(r => r.path));
|
||||
const before = rows.length;
|
||||
(files || []).forEach(f => {
|
||||
if (existing.has(f.path)) return;
|
||||
rows.push({ id:'r' + (nextId++), path:f.path, name:f.name, iteration:parseIterationName(f.name), error:'' });
|
||||
existing.add(f.path);
|
||||
});
|
||||
const added = rows.length - before;
|
||||
if (added > 1) {
|
||||
const batch = rows.slice(rows.length - added);
|
||||
const first = batch[0].iteration;
|
||||
if (first && batch.every(r => r.iteration === first)) {
|
||||
batch.forEach((r, i) => r.iteration = String(i + 1).padStart(2, '0') + '_' + first);
|
||||
}
|
||||
}
|
||||
page = Math.max(0, Math.ceil(rows.length / PAGE_SIZE) - 1);
|
||||
renderRows();
|
||||
}
|
||||
function renderRows() {
|
||||
const oldPager = document.getElementById('selectedPager');
|
||||
if (oldPager) oldPager.remove();
|
||||
rowsPanel.classList.toggle('is-hidden', rows.length === 0);
|
||||
emptySelection.classList.toggle('is-hidden', rows.length !== 0);
|
||||
if (!rows.length) { tbody.innerHTML = ''; return; }
|
||||
const maxPage = Math.max(0, Math.ceil(rows.length / PAGE_SIZE) - 1);
|
||||
page = Math.min(page, maxPage);
|
||||
const start = page * PAGE_SIZE;
|
||||
const pageRows = rows.slice(start, start + PAGE_SIZE);
|
||||
tbody.innerHTML = pageRows.map((row, offset) => {
|
||||
const i = start + offset;
|
||||
return '<tr data-id="' + row.id + '">' +
|
||||
'<td><span class="mono wrap">' + escapeHtml(row.name) + '</span><div class="row-error">' + escapeHtml(row.error || '') + '</div></td>' +
|
||||
'<td><input type="text" data-iter data-index="' + i + '" value="' + escapeHtml(row.iteration) + '" /></td>' +
|
||||
'<td><button class="remove-btn danger" data-remove="' + i + '" title="Remove">×</button></td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
tbody.querySelectorAll('[data-iter]').forEach(input => {
|
||||
input.addEventListener('input', () => rows[Number(input.dataset.index)].iteration = input.value);
|
||||
});
|
||||
tr.querySelector('.remove-btn').addEventListener('click', () => {
|
||||
tr.remove();
|
||||
if (!tbody.children.length) addRow();
|
||||
tbody.querySelectorAll('[data-remove]').forEach(btn => {
|
||||
btn.addEventListener('click', () => { rows.splice(Number(btn.dataset.remove), 1); renderRows(); });
|
||||
});
|
||||
tbody.appendChild(tr);
|
||||
if (rows.length > PAGE_SIZE) {
|
||||
const pager = document.createElement('div');
|
||||
pager.id = 'selectedPager';
|
||||
pager.className = 'pager';
|
||||
pager.innerHTML = '<button class="secondary" id="prevPage"' + (page === 0 ? ' disabled' : '') + '>Previous</button>' +
|
||||
'<span class="file-name">Page ' + (page + 1) + ' of ' + (maxPage + 1) + '</span>' +
|
||||
'<button class="secondary" id="nextPage"' + (page === maxPage ? ' disabled' : '') + '>Next</button>';
|
||||
rowsPanel.after(pager);
|
||||
pager.querySelector('#prevPage').addEventListener('click', () => { page--; renderRows(); });
|
||||
pager.querySelector('#nextPage').addEventListener('click', () => { page++; renderRows(); });
|
||||
}
|
||||
}
|
||||
function setPreview(url) {
|
||||
previewUrl = url || '';
|
||||
openResultBtn.disabled = !previewUrl;
|
||||
copyResultBtn.disabled = !previewUrl;
|
||||
}
|
||||
|
||||
document.getElementById('addRow').addEventListener('click', addRow);
|
||||
document.getElementById('pickFolder').addEventListener('click', async () => {
|
||||
const res = await fetch('/api/plec/pickFolder', { method: 'POST' });
|
||||
const j = await res.json();
|
||||
addFiles(j.files || []);
|
||||
});
|
||||
document.getElementById('pickFiles').addEventListener('click', async () => {
|
||||
const res = await fetch('/api/plec/pick', { method: 'POST' });
|
||||
const j = await res.json();
|
||||
addFiles(j.files || []);
|
||||
});
|
||||
document.getElementById('clear').addEventListener('click', () => {
|
||||
rows = [];
|
||||
page = 0;
|
||||
setPreview('');
|
||||
statusEl.textContent = '';
|
||||
renderRows();
|
||||
});
|
||||
openResultBtn.addEventListener('click', () => {
|
||||
if (previewUrl) fetch('/api/open', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ url: previewUrl }) });
|
||||
});
|
||||
copyResultBtn.addEventListener('click', () => {
|
||||
if (previewUrl) fetch('/api/clipboard', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ text: previewUrl }) });
|
||||
});
|
||||
|
||||
document.getElementById('upload').addEventListener('click', async () => {
|
||||
document.querySelectorAll('[data-error]').forEach(el => el.textContent = '');
|
||||
rows.forEach(row => row.error = '');
|
||||
renderRows();
|
||||
statusEl.textContent = '';
|
||||
resultsEl.innerHTML = '';
|
||||
const rows = [...tbody.querySelectorAll('tr')].map(tr => ({
|
||||
id: tr.dataset.id,
|
||||
path: tr.querySelector('[data-path]').value,
|
||||
iteration: tr.querySelector('[data-iter]').value,
|
||||
})).filter(r => r.path || r.iteration);
|
||||
setPreview('');
|
||||
if (rows.length === 0) {
|
||||
statusEl.innerHTML = '<span class="err">Add at least one row with a file and iteration name.</span>';
|
||||
statusEl.innerHTML = '<span class="err">Select at least one file.</span>';
|
||||
return;
|
||||
}
|
||||
statusEl.textContent = 'Uploading...';
|
||||
@@ -133,9 +181,10 @@ document.getElementById('upload').addEventListener('click', async () => {
|
||||
const j = await res.json();
|
||||
if (j.rowErrors) {
|
||||
for (const re of j.rowErrors) {
|
||||
const tr = tbody.querySelector('tr[data-id="' + re.id + '"]');
|
||||
if (tr) tr.querySelector('[data-error]').textContent = re.message;
|
||||
const row = rows.find(r => r.id === re.id);
|
||||
if (row) row.error = re.message;
|
||||
}
|
||||
renderRows();
|
||||
statusEl.innerHTML = '<span class="err">Fix row errors and retry.</span>';
|
||||
return;
|
||||
}
|
||||
@@ -144,40 +193,10 @@ document.getElementById('upload').addEventListener('click', async () => {
|
||||
return;
|
||||
}
|
||||
statusEl.innerHTML = '<span class="ok">Upload complete.</span>';
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'result';
|
||||
if (j.preview) {
|
||||
wrap.innerHTML = '<div><strong>Preview:</strong> <a href="' + j.preview + '" target="_blank">' + j.preview + '</a></div>';
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'result-actions';
|
||||
const copy = document.createElement('button');
|
||||
copy.textContent = 'Copy';
|
||||
copy.onclick = () => fetch('/api/clipboard', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ text: j.preview }) });
|
||||
const open = document.createElement('button');
|
||||
open.textContent = 'Open';
|
||||
open.className = 'secondary';
|
||||
open.onclick = () => fetch('/api/open', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ url: j.preview }) });
|
||||
const showRaw = document.createElement('button');
|
||||
showRaw.textContent = 'Show server response';
|
||||
showRaw.className = 'secondary';
|
||||
showRaw.onclick = () => {
|
||||
const pre = document.createElement('pre');
|
||||
pre.style.marginTop = '8px';
|
||||
pre.style.whiteSpace = 'pre-wrap';
|
||||
pre.textContent = j.rawText;
|
||||
wrap.appendChild(pre);
|
||||
showRaw.disabled = true;
|
||||
};
|
||||
actions.appendChild(copy); actions.appendChild(open); actions.appendChild(showRaw);
|
||||
wrap.appendChild(actions);
|
||||
} else {
|
||||
wrap.innerHTML = '<div>No preview field in response. Raw:</div><pre>' + (j.rawText || '').replace(/</g,'<') + '</pre>';
|
||||
}
|
||||
resultsEl.innerHTML = '';
|
||||
resultsEl.appendChild(wrap);
|
||||
setPreview(j.preview);
|
||||
});
|
||||
|
||||
addRow();
|
||||
renderRows();
|
||||
</script>
|
||||
`
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
@@ -204,6 +223,23 @@ func PlecPick(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, map[string]any{"files": files})
|
||||
}
|
||||
|
||||
func PlecPickFolder(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := LoadConfig()
|
||||
dir := PickFolder("Select folder", cfg.LastPickDir)
|
||||
if dir == "" {
|
||||
writeJSON(w, map[string]any{"files": []any{}})
|
||||
return
|
||||
}
|
||||
cfg.LastPickDir = dir
|
||||
_ = SaveConfig(cfg)
|
||||
paths := collectHTMLFiles(dir)
|
||||
files := make([]map[string]string, 0, len(paths))
|
||||
for _, p := range paths {
|
||||
files = append(files, map[string]string{"path": p, "name": filepath.Base(p)})
|
||||
}
|
||||
writeJSON(w, map[string]any{"files": files})
|
||||
}
|
||||
|
||||
type plecRow struct {
|
||||
ID string `json:"id"`
|
||||
Path string `json:"path"`
|
||||
|
||||
Reference in New Issue
Block a user