/* ══════════════════════════════════════ hyprcrd — app.js (orchestrator) ══════════════════════════════════════ */ import { api, setCsrf, getCsrf } from './modules/util.js'; import { state, getGroupCards } from './modules/state.js'; import { showDialog, isDialogOpen } from './modules/dialog.js'; import './modules/theme.js'; import { loadBuckets } from './modules/buckets.js'; import { renderBoard, updateSortUI } from './modules/board.js'; import { openDetail, closeDetail, showDetailCard, ungroupCard } from './modules/detail.js'; import { openModal, closeModal, saveCard } from './modules/editor.js'; import { openSearch, closeSearch } from './modules/search.js'; import { showContextMenu } from './modules/context-menu.js'; import { undo, canUndo } from './modules/undo.js'; import { closeGallery } from './modules/gallery.js'; import { refreshTasksView, openTasks, closeTasks } from './modules/tasks.js'; // ── Global API (for inline onclick handlers and cross-module calls) ── window._openDetail = openDetail; window._openModal = openModal; window._closeSearch = closeSearch; window._closeTasks = closeTasks; window._renderBoard = renderBoard; window._showContextMenu = showContextMenu; window._ungroupCard = ungroupCard; window._refreshTasks = refreshTasksView; window._reloadAll = async () => { state.cards = await api('api/cards'); renderBoard(); }; // ── Data loading ── async function loadCards() { state.cards = await api('api/cards'); updateSortUI(); renderBoard(); } // ── Export / Import ── document.getElementById('export-btn')?.addEventListener('click', () => { window.location.href = 'api/export'; }); const importFile = document.getElementById('import-file'); document.getElementById('import-btn')?.addEventListener('click', () => importFile?.click()); importFile?.addEventListener('change', async e => { const file = e.target.files?.[0]; if (!file) return; const ok = await showDialog({ title: 'import_data', message: 'Alle bestehenden Daten werden ersetzt. Fortfahren?', danger: true, okLabel: 'import' }); if (!ok) { e.target.value = ''; return; } const form = new FormData(); form.append('file', file); try { const res = await fetch('api/import', { method: 'POST', body: form, headers: { 'X-CSRF-Token': getCsrf() } }); const data = JSON.parse(await res.text()); if (data.error) throw new Error(data.error); await loadBuckets(); state.cards = await api('api/cards'); renderBoard(); } catch (err) { console.error('Import failed:', err); } e.target.value = ''; }); // ── Auth ── document.getElementById('logout-btn')?.addEventListener('click', async () => { const ok = await showDialog({ title: 'logout', message: 'Abmelden?', okLabel: 'logout', danger: true }); if (!ok) return; await fetch('api/auth/logout', { method: 'POST' }); location.reload(); }); async function checkAuth() { const res = await fetch('api/auth/status'); const data = await res.json(); if (data.csrf) setCsrf(data.csrf); if (data.setup) { let pw = null; while (!pw || pw.length < 4) { pw = await showDialog({ title: 'setup', message: 'Willkommen bei hyprcrd. Setze ein Passwort:', input: 'password', placeholder: '> passwort (min. 4 zeichen)...', okLabel: 'set', cancelLabel: '' }); if (pw && pw.length < 4) pw = null; } const r = await fetch('api/auth/setup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: pw }) }); const d = await r.json(); if (d.error) { location.reload(); return; } if (d.csrf) setCsrf(d.csrf); } else if (!data.logged_in) { let ok = false; while (!ok) { const pw = await showDialog({ title: 'login', message: '', input: 'password', placeholder: '> passwort...', okLabel: 'login', cancelLabel: '' }); if (!pw) continue; const r = await fetch('api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: pw }) }); const d = await r.json(); if (d.ok) { ok = true; if (d.csrf) setCsrf(d.csrf); } } } loadBuckets(); loadCards(); } // ── Global keyboard ── document.addEventListener('keydown', e => { const modal = document.getElementById('modal'); const detail = document.getElementById('detail'); const search = document.getElementById('search'); const tasks = document.getElementById('tasks'); if (isDialogOpen()) return; if (e.key === 'k' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); search.classList.contains('open') ? closeSearch() : openSearch(); return; } if (e.key === 'Escape') { if (detail.classList.contains('open')) closeDetail(); else if (modal.classList.contains('open')) closeModal(); else if (search.classList.contains('open')) closeSearch(); else if (tasks?.classList.contains('open')) closeTasks(); else if (document.getElementById('gallery')?.classList.contains('open')) closeGallery(); return; } if (e.key === 's' && (e.ctrlKey || e.metaKey) && modal.classList.contains('open')) { e.preventDefault(); saveCard(); return; } if (e.key === 'z' && (e.ctrlKey || e.metaKey) && !modal.classList.contains('open') && canUndo()) { e.preventDefault(); undo(); return; } if (e.key === '/' && ![modal, detail, search, tasks].some(el => el?.classList.contains('open')) && document.activeElement === document.body) { e.preventDefault(); openSearch(); } // Arrow keys navigate group in detail view if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && detail.classList.contains('open') && state.viewingCard) { const gc = getGroupCards(state.viewingCard); if (!gc || gc.length < 2) return; const idx = gc.findIndex(c => c.id == state.viewingCard.id); if (idx === -1) return; e.preventDefault(); showDetailCard((e.key === 'ArrowRight' ? gc[(idx + 1) % gc.length] : gc[(idx - 1 + gc.length) % gc.length]).id); } }); // ── Boot ── checkAuth();