diff --git a/index.html b/index.html index 6bb9403..31ffdb5 100644 --- a/index.html +++ b/index.html @@ -48,6 +48,10 @@ NovaSonic
+ @@ -121,6 +125,10 @@ NovaSonic
+ @@ -739,5 +747,15 @@
+ +
+
+
+ Benachrichtigungen + +
+
+
+ \ No newline at end of file diff --git a/src/main.js b/src/main.js index d8582f9..df4ea43 100644 --- a/src/main.js +++ b/src/main.js @@ -29,6 +29,7 @@ import { selectDlType, executeDownload, executeSpotifyDownload, openEditPlModal, closeEditPlModal, saveEditPlaylist, editPlUploadCover, editPlCoverSelected, editPlRemoveCover, toggleMobileSearch, closeMobileSearch, switchSetupTab, + toggleNotifPanel, deleteNotif, clearNotifs, initNotifBell, } from './ui.js'; // ─── CONNECT ─── @@ -80,6 +81,7 @@ async function startApp() { navigate('home'); } loadHomeData(); + initNotifBell(); _restoreLastPlayed(); if(localStorage.getItem('ns_radio')==='1') { S.radioMode=true; _applyRadioBtn(); } } @@ -235,6 +237,7 @@ Object.assign(window, { openEditPlModal, closeEditPlModal, saveEditPlaylist, editPlUploadCover, editPlCoverSelected, editPlRemoveCover, toggleMobileSearch, closeMobileSearch, switchSetupTab, + toggleNotifPanel, deleteNotif, clearNotifs, // App doConnect, disconnect, }); diff --git a/src/ui.js b/src/ui.js index aac218b..bfdaa13 100644 --- a/src/ui.js +++ b/src/ui.js @@ -339,6 +339,95 @@ async function _createNdPlaylist(tracks, name) { } } +// ─── NOTIFICATIONS ─── +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))); } + +function _addNotif(notif) { + const list = _loadNotifs(); + list.unshift(notif); + _saveNotifs(list); + _renderNotifBell(); +} + +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 = '
Keine Benachrichtigungen
'; + 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 + ? `
${n.failed.length} Track${n.failed.length > 1 ? 's' : ''} fehlgeschlagen
` + : ''; + return `
+
+ ${icon} +
+
${esc(n.title)}
+
${n.artist ? esc(n.artist) + ' · ' : ''}${time}
+
+ +
+ ${n.message ? `
${esc(n.message)}
` : ''} + ${failedHtml} +
`; + }).join(''); +} + +function _parseFailedTracks(log) { + const re = /Fehler bei "([^"]+)"/; + return (log || []).map(line => re.exec(line)?.[1]).filter(Boolean); +} + // ─── JOBS ─── function addJob(id, title, artist) { S.jobs[id] = {title, artist, progress:0, status:'pending'}; @@ -373,10 +462,24 @@ async function pollJob(jobId) { const res = await fetch(`${S.cfg.backendUrl}/api/status/${jobId}`); const d = await res.json(); updateJob(jobId, d.status, d.message, d.progress); - if(d.status!=='done' && d.status!=='error') setTimeout(() => pollJob(jobId), 1500); - else if(d.status==='done') { - const title = S.jobs[jobId]?.title || 'Track'; - toast(d.skipped ? `⏭ "${title}" bereits auf dem Server` : `✅ ${title} gespeichert!`); + if(d.status !== 'done' && d.status !== 'error') { + setTimeout(() => pollJob(jobId), 1500); + } else { + const job = S.jobs[jobId]; + const title = job?.title || 'Download'; + _addNotif({ + id: jobId, + title, + artist: job?.artist || '', + status: d.status, + message: d.message || (d.status === 'done' ? 'Fertig!' : 'Fehler'), + failed: _parseFailedTracks(d.log), + timestamp: Date.now(), + read: false, + }); + if (d.status === 'done') { + toast(d.skipped ? `⏭ "${title}" bereits auf dem Server` : `✅ ${title} gespeichert!`); + } } } catch { updateJob(jobId, 'error', 'Status-Abfrage fehlgeschlagen'); } } diff --git a/styles.css b/styles.css index a28baa8..4b0ee8e 100644 --- a/styles.css +++ b/styles.css @@ -79,6 +79,37 @@ nav{flex:1;overflow-y:auto;padding:4px 10px} .page-title em{color:var(--accent);font-style:normal} .page-title .blue{color:var(--blue)} +/* ── NOTIFICATION BELL ── */ +.notif-bell-btn{position:relative} +.notif-badge{position:absolute;top:1px;right:1px;min-width:15px;height:15px;background:var(--accent);color:#000;border-radius:8px;font-size:9px;font-weight:700;display:flex;align-items:center;justify-content:center;padding:0 3px;pointer-events:none;line-height:1} + +/* ── NOTIFICATION PANEL ── */ +.notif-overlay{display:none;position:fixed;inset:0;z-index:599} +.notif-overlay.active{display:block} +.notif-panel{position:fixed;top:12px;right:12px;width:320px;max-height:72vh;background:var(--surface);border:1px solid var(--border2);border-radius:14px;z-index:600;display:flex;flex-direction:column;box-shadow:0 8px 32px rgba(0,0,0,.5);transform:translateY(-8px) scale(.97);opacity:0;pointer-events:none;transition:opacity .18s,transform .18s} +.notif-panel.open{transform:translateY(0) scale(1);opacity:1;pointer-events:all} +.notif-panel-hdr{display:flex;align-items:center;justify-content:space-between;padding:14px 16px 10px;border-bottom:1px solid var(--border);flex-shrink:0} +.notif-panel-title{font-size:14px;font-weight:700} +.notif-clear-btn{font-size:11px;color:var(--text3);background:none;border:none;cursor:pointer;padding:2px 6px;border-radius:4px;transition:color .15s} +.notif-clear-btn:hover{color:var(--text)} +.notif-list{overflow-y:auto;flex:1;padding:8px;display:flex;flex-direction:column;gap:6px} +.notif-empty{font-size:13px;color:var(--text3);text-align:center;padding:28px 0} +.notif-card{background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:10px 12px;font-size:13px} +.notif-card.unread{border-color:var(--accent-dim)} +.notif-card-top{display:flex;align-items:flex-start;gap:8px} +.notif-icon{font-size:15px;flex-shrink:0;margin-top:1px} +.notif-info{flex:1;min-width:0} +.notif-title{font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.notif-sub{font-size:11px;color:var(--text3);margin-top:1px} +.notif-x{background:none;border:none;cursor:pointer;color:var(--text3);font-size:13px;padding:0;line-height:1;flex-shrink:0;transition:color .15s} +.notif-x:hover{color:var(--text)} +.notif-msg{font-size:12px;color:var(--text2);margin-top:6px} +.notif-failed{margin-top:6px} +.notif-failed summary{font-size:11px;color:var(--accent);cursor:pointer;user-select:none} +.notif-failed ul{margin:4px 0 0 14px;padding:0;font-size:11px;color:var(--text3)} +.notif-failed li{margin-bottom:2px} +@media(max-width:600px){.notif-panel{right:8px;left:8px;width:auto;top:60px}} + /* ── HOME GREETING ── */ .home-greeting{font-size:22px;font-weight:700;color:var(--text);padding:18px 0 6px;letter-spacing:-.3px}