Files
NovaSonic/src/ui/notify.js
T
HoneyandClaude Sonnet 4.6 920e385a8f fix: break static circular dep in notify.js that caused 'Failed to fetch' after ui split
notify.js statically imported navigate from views/views.js, creating a new
circular chain: navigation.js → ui/ui.js → settings.js → downloads.js →
notify.js → views/views.js → navigation.js. This chain did not exist in the
old monolithic ui.js and caused module initialisation failures in some browsers.

Fix: make navigate a lazy dynamic import inside _notifNavigate() body, which
is the only call site. Also replace unnecessary dynamic import('../utils.js')
calls with the already-statically-imported toast function.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 19:49:29 +02:00

147 lines
5.5 KiB
JavaScript

import { esc, toast } from '../utils.js';
import { api } from '../api.js';
const _NOTIF_KEY = 'ns_notifications';
function _loadNotifs() {
try { return JSON.parse(localStorage.getItem(_NOTIF_KEY) || '[]'); } catch { return []; }
}
function _saveNotifs(n) { localStorage.setItem(_NOTIF_KEY, JSON.stringify(n.slice(0, 100))); }
export function _addNotif(notif) {
const list = _loadNotifs();
list.unshift(notif);
_saveNotifs(list);
_renderNotifBell();
_renderNotifList();
}
export function deleteNotif(id) {
_saveNotifs(_loadNotifs().filter(n => n.id !== id));
_renderNotifBell();
_renderNotifList();
}
export function clearNotifs() {
_saveNotifs([]);
_renderNotifBell();
_renderNotifList();
}
export function toggleNotifPanel() {
const panel = document.getElementById('notif-panel');
const overlay = document.getElementById('notif-overlay');
if (!panel) return;
const isOpen = panel.classList.contains('open');
if (isOpen) {
panel.classList.remove('open');
overlay?.classList.remove('active');
} else {
_saveNotifs(_loadNotifs().map(n => ({...n, read: true})));
_renderNotifList();
_renderNotifBell();
panel.classList.add('open');
overlay?.classList.add('active');
}
}
export function initNotifBell() { _renderNotifBell(); }
function _renderNotifBell() {
const unread = _loadNotifs().filter(n => !n.read).length;
document.querySelectorAll('.notif-badge').forEach(b => {
b.textContent = unread > 9 ? '9+' : unread;
b.style.display = unread > 0 ? '' : 'none';
});
}
function _renderNotifList() {
const list = document.getElementById('notif-list');
if (!list) return;
const notifs = _loadNotifs();
if (!notifs.length) {
list.innerHTML = '<div class="notif-empty">Keine Benachrichtigungen</div>';
return;
}
list.innerHTML = notifs.map(n => {
const d = new Date(n.timestamp);
const time = d.toLocaleDateString('de-DE', {day:'2-digit', month:'2-digit'}) + ' ' + d.toLocaleTimeString('de-DE', {hour:'2-digit', minute:'2-digit'});
const icon = n.status === 'done' ? '✅' : '❌';
const failedHtml = n.failed?.length
? `<details class="notif-failed"><summary>${n.failed.length} Track${n.failed.length > 1 ? 's' : ''} fehlgeschlagen</summary><ul>${n.failed.map(f => `<li>${esc(f)}</li>`).join('')}</ul></details>`
: '';
const missingHtml = n.missing?.length
? `<details class="notif-failed"><summary>${n.missing.length} Track${n.missing.length > 1 ? 's' : ''} nicht in Navidrome gefunden</summary><ul>${n.missing.map(f => `<li>${esc(f)}</li>`).join('')}</ul></details>`
: '';
const hasNav = n.status === 'done' && ((n.navType === 'playlist' && n.navId) || n.navArtist || n.navAlbum);
const navLabel = n.navType === 'artist' ? 'Zum Künstler →'
: (n.navType === 'track' || n.navType === 'album') ? 'Zum Album →'
: 'Öffnen →';
const navBtn = hasNav
? `<button class="notif-nav-btn" onclick="_notifNavigate('${esc(n.id)}')">${navLabel}</button>`
: '';
return `<div class="notif-card${n.read ? '' : ' unread'}">
<div class="notif-card-top">
<span class="notif-icon">${icon}</span>
<div class="notif-info">
<div class="notif-title">${esc(n.title)}</div>
<div class="notif-sub">${n.artist ? esc(n.artist) + ' · ' : ''}${time}</div>
</div>
<button class="notif-x" onclick="deleteNotif('${esc(n.id)}')">✕</button>
</div>
${n.message ? `<div class="notif-msg">${esc(n.message)}</div>` : ''}
${failedHtml}
${missingHtml}
${navBtn}
</div>`;
}).join('');
}
export async function _notifNavigate(notifId) {
const notifs = _loadNotifs();
const n = notifs.find(x => x.id === notifId);
if (!n) return;
const panel = document.getElementById('notif-panel');
const overlay = document.getElementById('notif-overlay');
panel?.classList.remove('open');
overlay?.classList.remove('active');
const { navigate } = await import('../views/views.js');
if (n.navType === 'playlist' && n.navId) {
navigate('playlist-detail', { id: n.navId, name: n.navName || n.title.replace(/^Playlist:\s*/, '') });
return;
}
const norm = s => s?.toLowerCase().trim() || '';
if (n.navType === 'track' || n.navType === 'album') {
const albumQuery = n.navAlbum || n.navTitle;
if (albumQuery) {
try {
const r = await api('search3', `&query=${encodeURIComponent(albumQuery)}&artistCount=0&albumCount=10&songCount=0`);
const albums = r.searchResult3?.album || [];
const match = albums.find(a => norm(a.name) === norm(albumQuery) && norm(a.artist) === norm(n.navArtist))
|| albums.find(a => norm(a.name) === norm(albumQuery))
|| albums[0];
if (match) {
navigate('album-detail', { id: match.id, name: match.name, artist: match.artist || '', artistId: match.artistId || '', coverArt: match.coverArt || '' });
return;
}
} catch {}
}
toast('Album nicht gefunden — Library-Scan nötig?');
return;
}
if (n.navArtist) {
try {
const r = await api('search3', `&query=${encodeURIComponent(n.navArtist)}&artistCount=5&albumCount=0&songCount=0`);
const artists = r.searchResult3?.artist || [];
const match = artists.find(a => norm(a.name) === norm(n.navArtist)) || artists[0];
if (match) { navigate('artist-detail', { id: match.id, name: match.name }); return; }
} catch {}
toast('Künstler nicht gefunden — Library-Scan nötig?');
}
}