FileBrowser

This commit is contained in:
chk
2026-06-14 22:08:02 +02:00
parent 47db676b51
commit fb6453e2e4
2 changed files with 110 additions and 63 deletions

View File

@@ -12,15 +12,20 @@
* { box-sizing: border-box; margin: 0; padding: 0; } * { box-sizing: border-box; margin: 0; padding: 0; }
html, body { html {
min-height: 100%; min-height: 100%;
font-family: Arial, sans-serif;
font-size: 14px;
background: linear-gradient(to bottom, #dddddd -20%, var(--bg) 130%);
color: var(--text);
} }
body { padding: 16px; } body {
min-height: 100vh;
font-family: Arial, sans-serif;
font-size: 14px;
/* background-attachment: fixed hält den Verlauf relativ zum Viewport —
verhindert das Wiederholen bei langen Seiten */
background: linear-gradient(to bottom, #dddddd -20%, var(--bg) 130%) fixed;
color: var(--text);
padding: 16px;
}
/* ===== HEADER ===== */ /* ===== HEADER ===== */
.app-header { .app-header {
@@ -28,6 +33,10 @@ body { padding: 16px; }
align-items: baseline; align-items: baseline;
gap: 12px; gap: 12px;
margin-bottom: 16px; margin-bottom: 16px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 16px;
} }
.app-header h1 { .app-header h1 {
font-size: 16px; font-size: 16px;
@@ -100,6 +109,11 @@ body { padding: 16px; }
background: var(--active); background: var(--active);
border-color: var(--accent); border-color: var(--accent);
} }
.prog-item.is-selected {
background: #1e293b;
border-color: #475569;
}
.prog-item.is-folder { font-style: italic; }
.prog-item .prog-name { .prog-item .prog-name {
flex: 1; flex: 1;
font-size: 13px; font-size: 13px;

View File

@@ -23,7 +23,9 @@
</h2> </h2>
<div class="controls"> <div class="controls">
<button id="btn-refresh" title="Liste neu laden">Aktualisieren</button> <button id="btn-refresh" title="Aktualisieren"></button>
<button id="btn-new-folder" title="Neuen Ordner erstellen">📁+</button>
<button id="btn-delete-selected" title="Ausgewähltes löschen" disabled>🗑</button>
</div> </div>
<div class="status-line" id="list-status"></div> <div class="status-line" id="list-status"></div>
@@ -84,6 +86,8 @@
// ─── Zustand ───────────────────────────────────────────────────────────────── // ─── Zustand ─────────────────────────────────────────────────────────────────
let currentActiveId = null; let currentActiveId = null;
let selectedId = null; // in der Liste ausgewähltes Item (unabhängig vom aktiven Programm)
let selectedType = null; // 'file' | 'folder'
// ─── DOM-Referenzen ─────────────────────────────────────────────────────────── // ─── DOM-Referenzen ───────────────────────────────────────────────────────────
const elProgCount = document.getElementById('prog-count'); const elProgCount = document.getElementById('prog-count');
@@ -103,6 +107,7 @@
const elBtnLast = document.getElementById('btn-last'); const elBtnLast = document.getElementById('btn-last');
const elBtnClear = document.getElementById('btn-clear'); const elBtnClear = document.getElementById('btn-clear');
const elBtnDownload = document.getElementById('btn-download'); const elBtnDownload = document.getElementById('btn-download');
const elBtnDeleteSelected = document.getElementById('btn-delete-selected');
// ─── API-Helpers ───────────────────────────────────────────────────────────── // ─── API-Helpers ─────────────────────────────────────────────────────────────
async function apiFetch(method, path, body) { async function apiFetch(method, path, body) {
@@ -137,37 +142,50 @@
return { code: line.slice(0, idx).trim(), ts: fmtTs(line.slice(idx + 1).trim()) }; return { code: line.slice(0, idx).trim(), ts: fmtTs(line.slice(idx + 1).trim()) };
} }
// ─── Selection-State aktualisieren ──────────────────────────────────────────
function setSelected(id, type) {
selectedId = id;
selectedType = type;
// Alle Items neu markieren
elProgList.querySelectorAll('.prog-item').forEach(el => {
el.classList.toggle('is-selected', el.dataset.id === id);
});
elBtnDeleteSelected.disabled = !id;
}
// ─── Programmliste rendern ─────────────────────────────────────────────────── // ─── Programmliste rendern ───────────────────────────────────────────────────
function renderProgList(programs, activeId) { function renderProgList(programs, folders, activeId) {
elProgCount.textContent = programs.length; const total = folders.length + programs.length;
if (!programs.length) { elProgCount.textContent = total;
if (!total) {
elProgList.innerHTML = '<span class="empty-hint">Keine Programme gefunden</span>'; elProgList.innerHTML = '<span class="empty-hint">Keine Programme gefunden</span>';
elBtnDeleteSelected.disabled = true;
return; return;
} }
elProgList.innerHTML = programs
.map(p => { // Ordner zuerst, dann Dateien
const folderHtml = folders.map(f => `
<div class="prog-item is-folder" data-id="${esc(f.id)}" data-type="folder">
<span class="prog-name" title="${esc(f.name)}">📁 ${esc(f.name)}</span>
</div>`).join('');
const fileHtml = programs.map(p => {
const isActive = p.id === activeId; const isActive = p.id === activeId;
return `<div class="prog-item${isActive ? ' is-active' : ''}" data-id="${esc(p.id)}"> const isSel = p.id === selectedId;
<span class="prog-name" title="${esc(p.name)}">${esc(p.name)}</span> return `<div class="prog-item${isActive ? ' is-active' : ''}${isSel ? ' is-selected' : ''}" data-id="${esc(p.id)}" data-type="file">
<span class="prog-name" title="${esc(p.name)}">📄 ${esc(p.name)}</span>
<span class="prog-id">${esc(p.id)}</span> <span class="prog-id">${esc(p.id)}</span>
<span class="prog-count">${p.lineCount} Z.</span> <span class="prog-count">${p.lineCount} Z.</span>
<button class="btn-del" data-id="${esc(p.id)}" title="Löschen">✕</button>
</div>`; </div>`;
}).join(''); }).join('');
// Klick auf Zeile → FLoad (Programm aktivieren) elProgList.innerHTML = folderHtml + fileHtml;
elProgList.querySelectorAll('.prog-item').forEach(el => {
el.addEventListener('click', e => {
if (e.target.classList.contains('btn-del')) return;
loadProgram(el.dataset.id);
});
});
// Klick auf Löschen-Button // Einfachklick → auswählen; Doppelklick → Programm laden
elProgList.querySelectorAll('.btn-del').forEach(btn => { elProgList.querySelectorAll('.prog-item').forEach(el => {
btn.addEventListener('click', e => { el.addEventListener('click', () => setSelected(el.dataset.id, el.dataset.type));
e.stopPropagation(); el.addEventListener('dblclick', () => {
deleteProgram(btn.dataset.id); if (el.dataset.type === 'file') loadProgram(el.dataset.id);
}); });
}); });
} }
@@ -238,7 +256,7 @@
apiFetch('GET', '/api/programs'), apiFetch('GET', '/api/programs'),
apiFetch('GET', '/api/active'), apiFetch('GET', '/api/active'),
]); ]);
renderProgList(programs.programs || [], active.programId); renderProgList(programs.programs || [], [], active.programId);
renderLines(active); renderLines(active);
setStatus(elListStatus, ''); setStatus(elListStatus, '');
setStatus(elActStatus, ''); setStatus(elActStatus, '');
@@ -260,19 +278,6 @@
} }
} }
// ─── Programm löschen ────────────────────────────────────────────────────────
async function deleteProgram(id) {
if (!confirm(`Programm '${id}' wirklich löschen?`)) return;
setStatus(elListStatus, `Lösche '${id}'…`);
try {
await apiFetch('DELETE', `/api/programs/${encodeURIComponent(id)}`);
setStatus(elListStatus, `'${id}' gelöscht`);
await refresh();
} catch (err) {
setStatus(elListStatus, `Fehler: ${err.message}`, true);
}
}
// ─── Stepping ──────────────────────────────────────────────────────────────── // ─── Stepping ────────────────────────────────────────────────────────────────
async function step(endpoint) { async function step(endpoint) {
setStatus(elActStatus, '…'); setStatus(elActStatus, '…');
@@ -298,8 +303,36 @@
} }
} }
// ─── Programm löschen (aus Toolbar) ─────────────────────────────────────────
async function deleteSelected() {
if (!selectedId) return;
if (selectedType === 'folder') {
alert(`Ordner-Löschung ist noch nicht implementiert.\n('${selectedId}')`);
return;
}
if (!confirm(`Programm '${selectedId}' wirklich löschen?`)) return;
setStatus(elListStatus, `Lösche '${selectedId}'…`);
try {
await apiFetch('DELETE', `/api/programs/${encodeURIComponent(selectedId)}`);
setStatus(elListStatus, `'${selectedId}' gelöscht`);
selectedId = null;
selectedType = null;
elBtnDeleteSelected.disabled = true;
await refresh();
} catch (err) {
setStatus(elListStatus, `Fehler: ${err.message}`, true);
}
}
// ─── Neuen Ordner anlegen ────────────────────────────────────────────────────
async function createFolder() {
alert('Ordner-Verwaltung ist noch nicht implementiert.\n(kommt in Phase 3 des Roadmaps)');
}
// ─── Event-Listener ────────────────────────────────────────────────────────── // ─── Event-Listener ──────────────────────────────────────────────────────────
document.getElementById('btn-refresh').addEventListener('click', refresh); document.getElementById('btn-refresh').addEventListener('click', refresh);
document.getElementById('btn-new-folder').addEventListener('click', createFolder);
elBtnDeleteSelected.addEventListener('click', deleteSelected);
elBtnFirst.addEventListener('click', () => step('first')); elBtnFirst.addEventListener('click', () => step('first'));
elBtnPrev .addEventListener('click', () => step('prev')); elBtnPrev .addEventListener('click', () => step('prev'));
elBtnNext .addEventListener('click', () => step('next')); elBtnNext .addEventListener('click', () => step('next'));