From 22203d25a7f3e25524b5af88e38e227a5071cb6c Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Sun, 14 Jun 2026 09:59:44 +0200 Subject: [PATCH] v2.3 Papierkorb: renderTrash, Wiederherstellen, endgueltig loeschen, leeren --- src/js/settings.js | 162 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/src/js/settings.js b/src/js/settings.js index c8cc8dc..86c60ac 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -49,6 +49,7 @@ function openSettings() { document.getElementById('settingsOverlay').classList.add('active'); }); panel.setAttribute('aria-hidden', 'false'); + renderTrash(); _settingsTrap = _makeTrap(panel, closeSettings); document.addEventListener('keydown', _settingsTrap); const first = _focusable(panel)[0]; @@ -141,6 +142,161 @@ function initAccordion() { }); } +// ---- PAPIERKORB ---- +/** + * Formatiert einen deletedAt-Timestamp lokalisiert (folgt der aktiven UI-Sprache). + * @param {number} ts - Millisekunden-Timestamp + * @returns {string} + */ +function formatTrashDate(ts) { + const locale = I18n.currentLang === 'de' ? 'de-DE' : 'en-US'; + return new Date(ts).toLocaleDateString(locale, { year: 'numeric', month: '2-digit', day: '2-digit' }); +} + +/** + * Rendert den Papierkorb in die Settings-Section. Wird bei jedem openSettings() + * sowie nach jeder Trash-Mutation aufgerufen. Baut DOM ohne innerHTML (XSS-frei, + * Titel kommen aus User-/Importdaten). + */ +function renderTrash() { + const listEl = document.getElementById('trashList'); + const actionsRow = document.getElementById('trashActionsRow'); + if (!listEl) return; + listEl.replaceChildren(); + + if (trash.length === 0) { + const empty = document.createElement('div'); + empty.className = 'trash-empty'; + empty.textContent = t('trash.empty'); + listEl.appendChild(empty); + if (actionsRow) actionsRow.classList.add('hidden'); + return; + } + if (actionsRow) actionsRow.classList.remove('hidden'); + + // Neueste zuerst. + const sorted = [...trash].sort((a, b) => b.deletedAt - a.deletedAt); + sorted.forEach(entry => listEl.appendChild(createTrashItemEl(entry))); +} + +/** + * Baut eine einzelne Papierkorb-Zeile. + * @param {Object} entry - trash-Eintrag { item, type, originBoardId, deletedAt } + * @returns {HTMLElement} + */ +function createTrashItemEl(entry) { + const row = document.createElement('div'); + row.className = 'trash-item'; + + const info = document.createElement('div'); + info.className = 'trash-item-info'; + + const titleLine = document.createElement('span'); + titleLine.className = 'trash-item-title'; + const badge = document.createElement('span'); + badge.className = 'trash-item-badge'; + badge.textContent = entry.type === 'board' ? t('trash.type.board') : t('trash.type.bookmark'); + const titleText = document.createTextNode(entry.item && entry.item.title ? entry.item.title : ''); + titleLine.append(badge, titleText); + + const meta = document.createElement('span'); + meta.className = 'trash-item-meta'; + let metaText = t('trash.deleted_at', { date: formatTrashDate(entry.deletedAt) }); + if (entry.type === 'bookmark') { + const origin = entry.originBoardId ? boards.find(b => b.id === entry.originBoardId) : null; + metaText += origin + ? ' · ' + t('trash.from_board', { board: origin.title }) + : ' · ' + t('trash.from_board_unknown'); + } + meta.textContent = metaText; + + info.append(titleLine, meta); + + const actions = document.createElement('div'); + actions.className = 'trash-item-actions'; + + const btnRestore = document.createElement('button'); + btnRestore.className = 'btn-small'; + btnRestore.textContent = t('trash.restore'); + btnRestore.title = t('trash.restore_title'); + btnRestore.addEventListener('click', () => restoreTrashEntry(entry)); + + const btnForever = document.createElement('button'); + btnForever.className = 'btn-danger'; + btnForever.textContent = t('trash.delete_forever'); + btnForever.title = t('trash.delete_forever_title'); + btnForever.addEventListener('click', () => deleteTrashEntryForever(entry)); + + actions.append(btnRestore, btnForever); + row.append(info, actions); + return row; +} + +/** + * Stellt einen Papierkorb-Eintrag wieder her. + * Bookmark -> in originBoardId (falls noch vorhanden), sonst in die Inbox (ensureInboxBoard). + * Board -> zurueck in boards[]. + * @param {Object} entry + */ +async function restoreTrashEntry(entry) { + if (entry.type === 'board') { + // Ganzes Board zurueck (inkl. blurred). Falls die id schon existiert (Edge-Case), + // neue uid vergeben, damit nichts ueberschrieben wird. + const restored = structuredClone(entry.item); + if (boards.some(b => b.id === restored.id)) restored.id = uid(); + boards.push(restored); + await saveBoards(); + } else { + const restored = structuredClone(entry.item); + let target = entry.originBoardId ? boards.find(b => b.id === entry.originBoardId) : null; + let toInbox = false; + if (!target) { + // Ursprungs-Board weg -> in die Inbox (Page-Wrapper ensureInboxBoard aus Phase 1). + target = await ensureInboxBoard(); + toInbox = true; + } + target.bookmarks.push(restored); + await saveBoards(); + if (toInbox) { + await HellionDialog.alert(t('trash.restored_to_inbox'), { type: 'info', title: t('trash.restored_to_inbox.title') }); + } + } + trash = trash.filter(e => e !== entry); + await saveTrash(); + renderTrash(); + renderBoards(); +} + +/** + * Loescht einen einzelnen Papierkorb-Eintrag endgueltig (mit Confirm). + * @param {Object} entry + */ +async function deleteTrashEntryForever(entry) { + const ok = await HellionDialog.confirm( + t('trash.delete_forever_confirm'), + { type: 'danger', title: t('trash.delete_forever_confirm.title'), confirmText: t('trash.delete_forever') } + ); + if (!ok) return; + trash = trash.filter(e => e !== entry); + await saveTrash(); + renderTrash(); +} + +/** + * Leert den gesamten Papierkorb (mit Confirm). + */ +async function emptyTrash() { + if (trash.length === 0) return; + const ok = await HellionDialog.confirm( + t('trash.empty_confirm', { count: trash.length }), + { type: 'danger', title: t('trash.empty_confirm.title'), confirmText: t('trash.empty_btn') } + ); + if (!ok) return; + trash = []; + await saveTrash(); + renderTrash(); +} + // ---- APPLY SETTINGS ---- function applySettings() { const body = document.body; @@ -336,6 +492,10 @@ function bindSettingsEvents() { Onboarding.start(); }); + // Papierkorb leeren + const btnEmptyTrash = document.getElementById('btnEmptyTrash'); + if (btnEmptyTrash) btnEmptyTrash.addEventListener('click', emptyTrash); + // Reset All document.getElementById('btnResetAll').addEventListener('click', async () => { const ok = await HellionDialog.confirm( @@ -344,11 +504,13 @@ function bindSettingsEvents() { ); if (!ok) return; boards = []; + trash = []; settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false, hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'nebula', showSearch: true, searchEngine: 'google', toolbarPos: 'right', imageRefEnabled: false, language: 'auto' }; await saveBoards(); + await saveTrash(); await saveSettings(); setLanguage('auto'); applySettings();