// ── Notification banner system ── // Providers register via addProvider(fn). Each fn(settings) returns an array of items. import { $, esc } from './util.js'; const banner = $('upcoming-banner'); const providers = []; const DISMISS_KEY = 'hyprcrd-dismissed-notifs'; const DISMISS_DATE_KEY = 'hyprcrd-dismissed-date'; const SETTINGS_KEY = 'hyprcrd-notif-settings'; // Clear dismissed list daily if (localStorage.getItem(DISMISS_DATE_KEY) !== new Date().toISOString().substring(0, 10)) { localStorage.removeItem(DISMISS_KEY); localStorage.setItem(DISMISS_DATE_KEY, new Date().toISOString().substring(0, 10)); } function getDismissed() { try { return JSON.parse(localStorage.getItem(DISMISS_KEY) || '[]'); } catch { return []; } } function dismiss(id) { const d = getDismissed(); if (!d.includes(id)) d.push(id); localStorage.setItem(DISMISS_KEY, JSON.stringify(d)); render(); } export function getSettings() { const defaults = { lookahead: 3, time: 0, birthdays: true, termine: true, overdue: true }; try { return { ...defaults, ...JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}') }; } catch { return defaults; } } export function saveSettings(s) { localStorage.setItem(SETTINGS_KEY, JSON.stringify(s)); render(); } /** * Register a notification provider. * @param {function} fn - Receives settings, returns array of { id, icon, iconColor, label, when, sort, onClick?, dismissable?, type? } */ export function addProvider(fn) { providers.push(fn); } function getAllItems() { const settings = getSettings(); const items = []; for (const fn of providers) { try { const result = fn(settings); if (Array.isArray(result)) items.push(...result); } catch {} } items.sort((a, b) => (a.sort || '').localeCompare(b.sort || '')); return items; } export function render() { if (!banner) return; const dismissed = getDismissed(); const visible = getAllItems().filter(i => !dismissed.includes(String(i.id))); if (!visible.length) { banner.classList.add('hidden'); banner.innerHTML = ''; return; } banner.classList.remove('hidden'); banner.innerHTML = `
${visible.map(i => `
${i.when ? `${esc(i.when)}` : ''} ${esc(i.label)} ${i.dismissable !== false ? `` : ''}
`).join('')}
`; banner.querySelectorAll('[data-notif-id]').forEach(el => { el.addEventListener('click', e => { if (e.target.closest('.upcoming-dismiss')) return; const item = visible.find(i => String(i.id) === el.dataset.notifId); if (item?.onClick) item.onClick(); }); }); banner.querySelectorAll('.upcoming-dismiss').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); dismiss(btn.dataset.dismiss); }); }); banner.querySelector('#dismiss-all')?.addEventListener('click', e => { e.stopPropagation(); for (const i of visible) dismiss(String(i.id)); }); } // ── Settings panel integration ── export function renderSettingsPanel() { const s = getSettings(); // Populate controls const lookahead = $('notif-lookahead'); if (lookahead) lookahead.value = String(s.lookahead); const time = $('notif-time'); if (time) time.value = String(s.time); const cbBday = $('notif-birthdays'); if (cbBday) cbBday.checked = s.birthdays; const cbTermin = $('notif-termine'); if (cbTermin) cbTermin.checked = s.termine; const cbOverdue = $('notif-overdue'); if (cbOverdue) cbOverdue.checked = s.overdue; // Auto-save on change (use onchange to avoid stacking listeners) const save = () => saveSettings({ lookahead: parseInt(lookahead?.value || '3'), time: parseInt(time?.value || '0'), birthdays: cbBday?.checked ?? true, termine: cbTermin?.checked ?? true, overdue: cbOverdue?.checked ?? true, }); if (lookahead) lookahead.onchange = save; if (time) time.onchange = save; if (cbBday) cbBday.onchange = save; if (cbTermin) cbTermin.onchange = save; if (cbOverdue) cbOverdue.onchange = save; // Restore dismissed — replace element to avoid duplicate listeners const restoreBtn = $('notif-clear-dismissed'); if (restoreBtn) { const fresh = restoreBtn.cloneNode(true); restoreBtn.parentNode.replaceChild(fresh, restoreBtn); fresh.addEventListener('click', () => { localStorage.removeItem(DISMISS_KEY); render(); renderStream(); }); } renderStream(); } function renderStream() { const stream = $('notif-stream'); if (!stream) return; const dismissed = getDismissed(); const all = getAllItems(); if (!all.length) { stream.innerHTML = '
no notifications
'; return; } stream.innerHTML = all.map(i => { const isDismissed = dismissed.includes(String(i.id)); return `
${i.when ? `${esc(i.when)}` : ''} ${esc(i.label)} ${isDismissed ? 'dismissed' : ''}
`; }).join(''); }