// ── Theme + Settings ── import { $, api, getCsrf } from './util.js'; import { state } from './state.js'; import { renderSettingsPanel as renderNotifSettings } from './notifications.js'; const overlay = $('settings-overlay'); const themeList = $('settings-theme-list'); // Render theme list in settings function renderThemes() { if (!themeList) return; const current = localStorage.getItem('hyprcrd-theme') || 'slate'; themeList.innerHTML = ''; for (const [key, t] of Object.entries(window.themes)) { const btn = document.createElement('button'); const active = key === current; btn.className = `flex items-center gap-3 px-3 py-2.5 text-left text-xs border transition-colors rounded ${active ? 'border-t-accent text-t-bright' : 'border-t-border text-t-muted hover:border-t-accent hover:text-t-bright'}`; btn.innerHTML = ` ${t.label} ${active ? '' : ''}`; btn.onclick = () => { window.applyTheme(key); renderThemes(); window._renderBoard?.(); }; themeList.appendChild(btn); } } // Tab switching overlay?.querySelectorAll('.settings-tab').forEach(tab => { tab.addEventListener('click', () => { overlay.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active')); tab.classList.add('active'); const panel = tab.dataset.tab; overlay.querySelectorAll('[id^="settings-panel-"]').forEach(p => p.classList.add('hidden')); $(`settings-panel-${panel}`)?.classList.remove('hidden'); if (panel === 'caldav') { window._loadCaldavSettings?.(); } if (panel === 'relay') { window._loadRelaySettings?.(); } if (panel === 'notifications') renderNotifSettings(); if (panel === 'behavior') initBehaviorSettings(); if (panel === 'plugins') window._openPluginSettings?.(); }); }); // Open/Close function updateStats() { const sc = $('stats-cards'); if (sc) sc.textContent = state.cards?.length || 0; const sb = $('stats-buckets'); if (sb) sb.textContent = state.buckets?.length || 0; const tags = new Set(); (state.cards || []).forEach(c => (c.tags || '').split(',').forEach(t => { if (t) tags.add(t); })); const st = $('stats-tags'); if (st) st.textContent = tags.size; } // Vault key $('settings-vault-save')?.addEventListener('click', async () => { const key = $('settings-vault-key')?.value; const status = $('settings-vault-status'); if (!key || key.length < 8) { if (status) { status.textContent = 'min 8 characters'; status.style.color = 'var(--t-danger)'; } return; } const res = await fetch('api/auth/vault-key', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrf() }, body: JSON.stringify({ key }), }).then(r => r.json()).catch(() => ({ error: 'failed' })); if (res.ok) { if (status) { status.textContent = res.rekey ? 'saved — notes will re-encrypt on next sync' : 'saved'; status.style.color = 'var(--t-accent)'; } $('settings-vault-key').value = ''; if (res.rekey) window._triggerSync?.(); } else { if (status) { status.textContent = res.error || 'failed'; status.style.color = 'var(--t-danger)'; } } }); async function loadVaultStatus() { const res = await api('api/auth/vault-key'); if (!res) return; const status = $('settings-vault-status'); const input = $('settings-vault-key'); if (input) input.placeholder = res.has_key ? '(set — enter new to change)' : 'enter vault key'; if (status && res.has_key) { status.textContent = 'vault key is set'; status.style.color = 'var(--t-dim)'; } } $('settings-btn')?.addEventListener('click', () => { renderThemes(); updateStats(); loadVaultStatus(); overlay?.classList.add('open'); }); $('settings-close')?.addEventListener('click', () => overlay?.classList.remove('open')); overlay?.addEventListener('click', e => { if (e.target === overlay) overlay?.classList.remove('open'); }); // Password change $('settings-pw-save')?.addEventListener('click', async () => { const current = $('settings-pw-current')?.value; const newPw = $('settings-pw-new')?.value; const confirm = $('settings-pw-confirm')?.value; const status = $('settings-pw-status'); if (!current || !newPw) { if (status) { status.textContent = 'fill all fields'; status.style.color = 'var(--t-danger)'; } return; } if (newPw !== confirm) { if (status) { status.textContent = 'passwords do not match'; status.style.color = 'var(--t-danger)'; } return; } if (newPw.length < 4) { if (status) { status.textContent = 'min 4 characters'; status.style.color = 'var(--t-danger)'; } return; } const res = await fetch('api/auth/password', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrf() }, body: JSON.stringify({ current, new: newPw }), }).then(r => r.json()).catch(() => ({ error: 'network error' })); if (res.ok) { if (status) { status.textContent = res.rekey ? 'password changed — re-encrypting notes...' : 'password changed'; status.style.color = 'var(--t-accent)'; } $('settings-pw-current').value = ''; $('settings-pw-new').value = ''; $('settings-pw-confirm').value = ''; // Trigger sync to re-push notes with new encryption key if (res.rekey) { window._triggerSync?.(); setTimeout(() => { if (status) status.textContent = 'password changed, notes re-encrypted'; }, 3000); } } else { if (status) { status.textContent = res.error || 'failed'; status.style.color = 'var(--t-danger)'; } } }); function initBehaviorSettings() { const selects = { 'setting-current-days': { key: 'hyprcrd-current-days', def: '2' }, 'setting-card-height': { key: 'hyprcrd-card-height', def: '180' }, 'setting-card-width': { key: 'hyprcrd-card-width', def: '280' }, 'setting-body-lines': { key: 'hyprcrd-body-lines', def: '5' }, 'setting-click-action': { key: 'hyprcrd-click-action', def: 'preview' }, }; const toggles = { 'setting-current-include-synced': { key: 'hyprcrd-current-include-synced', def: '0' }, 'setting-show-tags': { key: 'hyprcrd-show-tags', def: '1' }, 'setting-show-thumbnails': { key: 'hyprcrd-show-thumbnails', def: '1' }, 'setting-show-done-badge': { key: 'hyprcrd-show-done-badge', def: '1' }, 'setting-confirm-delete': { key: 'hyprcrd-confirm-delete', def: '1' }, 'setting-auto-save': { key: 'hyprcrd-auto-save', def: '0' }, }; for (const [id, cfg] of Object.entries(selects)) { const el = $(id); if (!el) continue; el.value = localStorage.getItem(cfg.key) || cfg.def; el.onchange = () => { localStorage.setItem(cfg.key, el.value); applyBehavior(); }; } for (const [id, cfg] of Object.entries(toggles)) { const el = $(id); if (!el) continue; const isOn = (localStorage.getItem(cfg.key) ?? cfg.def) === '1'; el.classList.toggle('on', isOn); const row = el.closest('.toggle-row'); if (row) { row.onclick = () => { const nowOn = !el.classList.contains('on'); el.classList.toggle('on', nowOn); localStorage.setItem(cfg.key, nowOn ? '1' : '0'); applyBehavior(); }; } } } function applyBehavior() { const board = $('board'); if (board) { const w = localStorage.getItem('hyprcrd-card-width') || '280'; board.style.gridTemplateColumns = `repeat(auto-fill, minmax(${w}px, 1fr))`; } document.documentElement.style.setProperty('--card-min-h', (localStorage.getItem('hyprcrd-card-height') || '180') + 'px'); document.documentElement.style.setProperty('--card-body-lines', localStorage.getItem('hyprcrd-body-lines') || '5'); window._renderBoard?.(); } // Apply on load applyBehavior(); // Apply saved theme on load window.applyTheme(localStorage.getItem('hyprcrd-theme') || 'slate');