Files
hyprcrd/public/js/modules/theme.js
T
2026-04-09 19:14:52 +02:00

173 lines
8.1 KiB
JavaScript

// ── 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 = `<span class="w-4 h-4 rounded-full shrink-0" style="background:${t.vars['--t-accent']}"></span>
<span class="font-pixel text-[10px] flex-1">${t.label}</span>
${active ? '<i class="ph ph-check text-t-accent text-sm"></i>' : ''}`;
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();
});
});
// 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');