FileBrowser
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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,25 +86,28 @@
|
|||||||
|
|
||||||
// ─── 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');
|
||||||
const elProgList = document.getElementById('program-list');
|
const elProgList = document.getElementById('program-list');
|
||||||
const elListStatus = document.getElementById('list-status');
|
const elListStatus = document.getElementById('list-status');
|
||||||
const elActiveName = document.getElementById('active-name');
|
const elActiveName = document.getElementById('active-name');
|
||||||
const elActiveBar = document.getElementById('active-bar');
|
const elActiveBar = document.getElementById('active-bar');
|
||||||
const elAiId = document.getElementById('ai-id');
|
const elAiId = document.getElementById('ai-id');
|
||||||
const elAiCount = document.getElementById('ai-count');
|
const elAiCount = document.getElementById('ai-count');
|
||||||
const elAiCursor = document.getElementById('ai-cursor');
|
const elAiCursor = document.getElementById('ai-cursor');
|
||||||
const elAiVersion = document.getElementById('ai-version');
|
const elAiVersion = document.getElementById('ai-version');
|
||||||
const elLinesBody = document.getElementById('lines-body');
|
const elLinesBody = document.getElementById('lines-body');
|
||||||
const elActStatus = document.getElementById('active-status');
|
const elActStatus = document.getElementById('active-status');
|
||||||
const elBtnFirst = document.getElementById('btn-first');
|
const elBtnFirst = document.getElementById('btn-first');
|
||||||
const elBtnPrev = document.getElementById('btn-prev');
|
const elBtnPrev = document.getElementById('btn-prev');
|
||||||
const elBtnNext = document.getElementById('btn-next');
|
const elBtnNext = document.getElementById('btn-next');
|
||||||
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 => {
|
|
||||||
const isActive = p.id === activeId;
|
|
||||||
return `<div class="prog-item${isActive ? ' is-active' : ''}" data-id="${esc(p.id)}">
|
|
||||||
<span class="prog-name" title="${esc(p.name)}">${esc(p.name)}</span>
|
|
||||||
<span class="prog-id">${esc(p.id)}</span>
|
|
||||||
<span class="prog-count">${p.lineCount} Z.</span>
|
|
||||||
<button class="btn-del" data-id="${esc(p.id)}" title="Löschen">✕</button>
|
|
||||||
</div>`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
// Klick auf Zeile → FLoad (Programm aktivieren)
|
// 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 isSel = p.id === selectedId;
|
||||||
|
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-count">${p.lineCount} Z.</span>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
elProgList.innerHTML = folderHtml + fileHtml;
|
||||||
|
|
||||||
|
// Einfachklick → auswählen; Doppelklick → Programm laden
|
||||||
elProgList.querySelectorAll('.prog-item').forEach(el => {
|
elProgList.querySelectorAll('.prog-item').forEach(el => {
|
||||||
el.addEventListener('click', e => {
|
el.addEventListener('click', () => setSelected(el.dataset.id, el.dataset.type));
|
||||||
if (e.target.classList.contains('btn-del')) return;
|
el.addEventListener('dblclick', () => {
|
||||||
loadProgram(el.dataset.id);
|
if (el.dataset.type === 'file') loadProgram(el.dataset.id);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Klick auf Löschen-Button
|
|
||||||
elProgList.querySelectorAll('.btn-del').forEach(btn => {
|
|
||||||
btn.addEventListener('click', e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
deleteProgram(btn.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'));
|
||||||
|
|||||||
Reference in New Issue
Block a user