diff --git a/public/index.css b/public/index.css index d9bbe48..82358c7 100644 --- a/public/index.css +++ b/public/index.css @@ -246,6 +246,67 @@ table td.ts { color: var(--muted); font-size: 11px; } } .status-line.err { color: #fca5a5; } +/* ===== ZEILEN-LÖSCHEN ===== */ +table td.del-col, +table th.del-col { + width: 32px; + text-align: center; + padding: 2px 4px; +} +.btn-row-del { + background: none; + border: 1px solid transparent; + color: var(--muted); + border-radius: 4px; + padding: 1px 5px; + cursor: pointer; + font-size: 12px; + opacity: 0; + transition: opacity 0.1s; +} +tr:hover .btn-row-del, +tr.pending-delete .btn-row-del { opacity: 1; } +.btn-row-del:hover { + color: #fca5a5; + border-color: var(--danger-border); + background: var(--danger); +} +tr.pending-delete td { + text-decoration: line-through; + color: #fca5a5; + opacity: 0.6; +} +tr.pending-delete .btn-row-del { + color: #fca5a5; + border-color: var(--danger-border); + background: var(--danger); +} + +/* ===== EDIT-BAR (Abbrechen / Speichern) ===== */ +.edit-bar { + display: flex; + align-items: center; + gap: 8px; + margin-top: 10px; + padding: 8px 12px; + background: #1a0a0a; + border: 1px solid var(--danger-border); + border-radius: 6px; +} +.edit-bar-info { + font-size: 12px; + color: #fca5a5; +} +button.danger-confirm { + background: var(--danger); + border-color: var(--danger-border); + color: #fca5a5; +} +button.danger-confirm:hover:not(:disabled) { + border-color: #fca5a5; + color: #fff; +} + /* ===== DOWNLOAD LINK ===== */ .dl-link { display: inline-block; diff --git a/public/index.html b/public/index.html index 25b30cd..3e03f50 100644 --- a/public/index.html +++ b/public/index.html @@ -68,13 +68,20 @@ # G-Code (gespeichert in Grad) Zeitstempel + - Kein aktives Programm + Kein aktives Programm + + @@ -85,9 +92,11 @@ const REFRESH_MS = 5000; // Auto-Refresh-Intervall // ─── Zustand ───────────────────────────────────────────────────────────────── - let currentActiveId = null; - let selectedId = null; // in der Liste ausgewähltes Item (unabhängig vom aktiven Programm) - let selectedType = null; // 'file' | 'folder' + let currentActiveId = null; + let selectedId = null; // in der Liste ausgewähltes Item (unabhängig vom aktiven Programm) + let selectedType = null; // 'file' | 'folder' + let pendingDeletes = new Set(); // Indices markierter Zeilen (noch nicht gespeichert) + let cachedState = null; // letzter bekannter Serverzustand (für Abbrechen) // ─── DOM-Referenzen ─────────────────────────────────────────────────────────── const elProgCount = document.getElementById('prog-count'); @@ -190,8 +199,35 @@ }); } + // ─── Edit-Bar (Abbrechen / Speichern) ─────────────────────────────────────── + const elEditBar = document.getElementById('edit-bar'); + const elEditBarInfo = document.getElementById('edit-bar-info'); + + function updateEditBar() { + const n = pendingDeletes.size; + if (n === 0) { + elEditBar.style.display = 'none'; + } else { + elEditBar.style.display = 'flex'; + elEditBarInfo.textContent = `${n} Zeile${n === 1 ? '' : 'n'} zum Löschen markiert`; + } + } + + function markDelete(index) { + if (pendingDeletes.has(index)) { + pendingDeletes.delete(index); + } else { + pendingDeletes.add(index); + } + // Nur die betroffene Zeile neu stylen statt ganzer Tabelle + const row = elLinesBody.querySelector(`tr[data-index="${index}"]`); + if (row) row.classList.toggle('pending-delete', pendingDeletes.has(index)); + updateEditBar(); + } + // ─── Zeilen-Tabelle rendern ────────────────────────────────────────────────── function renderLines(state) { + cachedState = state; const { programId, cursor, lineCount, lines = [], version } = state; currentActiveId = programId; @@ -228,29 +264,45 @@ // Tabelle if (!lines.length) { - elLinesBody.innerHTML = `${ + elLinesBody.innerHTML = `${ hasProgram ? 'Programm ist leer' : 'Kein aktives Programm — links ein Programm anklicken' }`; + updateEditBar(); return; } elLinesBody.innerHTML = lines.map((line, i) => { const { code, ts } = parseLine(line); - const isCursor = i === cursor; - return ` + const isCursor = i === cursor; + const isPending = pendingDeletes.has(i); + return ` ${i} ${isCursor ? '▶ ' : ''}${esc(code)} ${esc(ts)} + `; }).join(''); - // Zur Cursor-Zeile scrollen - const cursorRow = elLinesBody.querySelector('.cursor-row'); - if (cursorRow) cursorRow.scrollIntoView({ block: 'nearest' }); + // Delete-Buttons verdrahten + elLinesBody.querySelectorAll('.btn-row-del').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + markDelete(Number(btn.dataset.index)); + }); + }); + + // Zur Cursor-Zeile scrollen (nur wenn kein edit-Modus aktiv) + if (pendingDeletes.size === 0) { + const cursorRow = elLinesBody.querySelector('.cursor-row'); + if (cursorRow) cursorRow.scrollIntoView({ block: 'nearest' }); + } + + updateEditBar(); } // ─── Daten laden ───────────────────────────────────────────────────────────── async function refresh() { + if (pendingDeletes.size > 0) return; // edit-Modus schützen try { const [programs, active] = await Promise.all([ apiFetch('GET', '/api/programs'), @@ -303,6 +355,30 @@ } } + // ─── Zeilen-Löschung: Abbrechen / Speichern ───────────────────────────────── + function cancelDeletes() { + pendingDeletes.clear(); + if (cachedState) renderLines(cachedState); + else updateEditBar(); + } + + async function saveDeletes() { + const indices = [...pendingDeletes].sort((a, b) => b - a); // höchste zuerst → kein Index-Shift + setStatus(elActStatus, `Lösche ${indices.length} Zeile(n)…`); + document.getElementById('btn-save-delete').disabled = true; + try { + for (const i of indices) { + await apiFetch('DELETE', `/api/active/lines/${i}`); + } + pendingDeletes.clear(); + setStatus(elActStatus, `${indices.length} Zeile(n) gelöscht`); + await refresh(); + } catch (err) { + setStatus(elActStatus, `Fehler: ${err.message}`, true); + document.getElementById('btn-save-delete').disabled = false; + } + } + // ─── Programm löschen (aus Toolbar) ───────────────────────────────────────── async function deleteSelected() { if (!selectedId) return; @@ -332,6 +408,8 @@ // ─── Event-Listener ────────────────────────────────────────────────────────── document.getElementById('btn-refresh').addEventListener('click', refresh); document.getElementById('btn-new-folder').addEventListener('click', createFolder); + document.getElementById('btn-cancel-delete').addEventListener('click', cancelDeletes); + document.getElementById('btn-save-delete').addEventListener('click', saveDeletes); elBtnDeleteSelected.addEventListener('click', deleteSelected); elBtnFirst.addEventListener('click', () => step('first')); elBtnPrev .addEventListener('click', () => step('prev'));