Files
hyprcrd/public/js/modules/notifications.js
T
thandClaude Opus 4.6 f465306c1d global tooltip system, migrate all title attrs to data-tip/data-help
New tooltip.js module with styled tooltips (data-tip) and multi-line
inline help (data-help). Migrates all ~70 native title attributes
across index.php, JS modules, and all plugins. Adds inline help on
9 strategic UI elements (split, superview, zen, extract, assets, etc).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:55:17 +02:00

165 lines
6.2 KiB
JavaScript

// ── 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 = `<div class="flex items-center gap-2 px-6 py-2 bg-t-raised border-b border-t-border overflow-x-auto">
<i class="ph ph-bell-ringing text-t-accent text-sm shrink-0"></i>
${visible.map(i => `<div class="upcoming-item" data-notif-id="${i.id}" style="color:${i.iconColor || 'var(--t-text)'}">
<i class="ph ph-${i.icon || 'info'}" style="font-size:11px;"></i>
${i.when ? `<span class="font-pixel text-[10px]">${esc(i.when)}</span>` : ''}
<span>${esc(i.label)}</span>
${i.dismissable !== false ? `<button class="upcoming-dismiss" data-dismiss="${i.id}"><i class="ph ph-x text-xs"></i></button>` : ''}
</div>`).join('')}
<button id="dismiss-all" class="shrink-0 ml-auto text-t-dim hover:text-t-danger transition-colors" data-tip="dismiss all"><i class="ph ph-x-circle text-sm"></i></button>
</div>`;
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 = '<div class="text-xs text-t-dim font-pixel text-center py-4 cursor-blink">no notifications</div>';
return;
}
stream.innerHTML = all.map(i => {
const isDismissed = dismissed.includes(String(i.id));
return `<div class="flex items-center gap-2 px-2 py-1.5 rounded text-xs ${isDismissed ? 'opacity-40' : ''}" style="color:${i.iconColor || 'var(--t-text)'}">
<i class="ph ph-${i.icon || 'info'}" style="font-size:11px;flex-shrink:0;"></i>
${i.when ? `<span class="font-pixel text-[9px] text-t-dim" style="min-width:55px;">${esc(i.when)}</span>` : ''}
<span class="flex-1 truncate">${esc(i.label)}</span>
${isDismissed ? '<span class="font-pixel text-[8px] text-t-dim">dismissed</span>' : ''}
</div>`;
}).join('');
}