diff --git a/src/main.js b/src/main.js index 2865672..4901040 100644 --- a/src/main.js +++ b/src/main.js @@ -30,7 +30,7 @@ import { saveEditPlaylist, editPlUploadCover, editPlCoverSelected, editPlRemoveCover, deletePlaylist, _confirmOk, _confirmCancel, toggleMobileSearch, closeMobileSearch, switchSetupTab, - toggleNotifPanel, deleteNotif, clearNotifs, initNotifBell, + toggleNotifPanel, deleteNotif, clearNotifs, initNotifBell, _notifNavigate, } from './ui.js'; // ─── CONNECT ─── @@ -241,7 +241,7 @@ Object.assign(window, { openEditPlModal, closeEditPlModal, saveEditPlaylist, editPlUploadCover, editPlCoverSelected, editPlRemoveCover, deletePlaylist, _confirmOk, _confirmCancel, toggleMobileSearch, closeMobileSearch, switchSetupTab, - toggleNotifPanel, deleteNotif, clearNotifs, + toggleNotifPanel, deleteNotif, clearNotifs, _notifNavigate, // App doConnect, disconnect, }); diff --git a/src/ui.js b/src/ui.js index 350886d..bc2b8e2 100644 --- a/src/ui.js +++ b/src/ui.js @@ -273,7 +273,7 @@ export async function executeDownload() { collectionId: S.downloadTarget.collectionId || null, explicit: S.downloadTarget.trackExplicitness === 'explicit', }; - addJob(jobId, payload.title, payload.artist); + addJob(jobId, payload.title, payload.artist, payload.type); try { const res = await fetch(`${S.cfg.backendUrl}/api/download`, { method: 'POST', headers: {'Content-Type':'application/json'}, @@ -291,7 +291,7 @@ export async function executeDownload() { export async function executeSpotifyDownload({ type, title, artist, artworkUrl, tracks, createPlaylist, playlistName }) { if (!S.cfg.backendUrl) { toast('⚠ Kein Backend konfiguriert'); return; } const jobId = 'job_' + Date.now(); - addJob(jobId, title, artist); + addJob(jobId, title, artist, type); try { const res = await fetch(`${S.cfg.backendUrl}/api/download`, { method: 'POST', @@ -344,10 +344,11 @@ async function _createNdPlaylist(tracks, name) { return; } const songParams = songIds.map(id => `&songId=${encodeURIComponent(id)}`).join(''); - await api('createPlaylist', `&name=${encodeURIComponent(name)}${songParams}`); + const plRes = await api('createPlaylist', `&name=${encodeURIComponent(name)}${songParams}`); + const plId = plRes.playlist?.id || null; const msg = `${songIds.length}/${tracks.length} Tracks hinzugefügt${missing.length ? `, ${missing.length} nicht gefunden` : ''}`; toast(`✅ Playlist "${name}" mit ${songIds.length}/${tracks.length} Tracks erstellt`); - _addNotif({ id: 'pl_' + Date.now(), title: `Playlist: ${name}`, artist: '', status: 'done', message: msg, missing, timestamp: Date.now(), read: false }); + _addNotif({ id: 'pl_' + Date.now(), title: `Playlist: ${name}`, artist: '', status: 'done', message: msg, missing, timestamp: Date.now(), read: false, navType: 'playlist', navId: plId, navName: name }); } catch(e) { toast('Playlist-Erstellung fehlgeschlagen: ' + e.message); } @@ -425,6 +426,10 @@ function _renderNotifList() { const missingHtml = n.missing?.length ? `
${n.missing.length} Track${n.missing.length > 1 ? 's' : ''} nicht in Navidrome gefunden
` : ''; + const hasNav = n.status === 'done' && ((n.navType === 'playlist' && n.navId) || n.navArtist); + const navBtn = hasNav + ? `` + : ''; return `
${icon} @@ -437,18 +442,46 @@ function _renderNotifList() { ${n.message ? `
${esc(n.message)}
` : ''} ${failedHtml} ${missingHtml} + ${navBtn}
`; }).join(''); } +export async function _notifNavigate(notifId) { + const notifs = _loadNotifs(); + const n = notifs.find(x => x.id === notifId); + if (!n) return; + // close panel first + const panel = document.getElementById('notif-panel'); + const overlay = document.getElementById('notif-overlay'); + panel?.classList.remove('open'); + overlay?.classList.remove('active'); + + if (n.navType === 'playlist' && n.navId) { + navigate('playlist-detail', { id: n.navId, name: n.navName || n.title.replace(/^Playlist:\s*/, '') }); + return; + } + + if (n.navArtist) { + try { + const norm = s => s?.toLowerCase().trim() || ''; + 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?'); + } +} + 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'}; +function addJob(id, title, artist, type) { + S.jobs[id] = {title, artist, type: type || '', progress:0, status:'pending'}; renderJobs(); } function updateJob(id, status, msg, progress) { @@ -494,6 +527,9 @@ async function pollJob(jobId) { failed: _parseFailedTracks(d.log), timestamp: Date.now(), read: false, + navType: job?.type || '', + navArtist: job?.artist || '', + navTitle: title, }); if (d.status === 'done') { toast(d.skipped ? `⏭ "${title}" bereits auf dem Server` : `✅ ${title} gespeichert!`); diff --git a/styles.css b/styles.css index 79f89f3..5d1be12 100644 --- a/styles.css +++ b/styles.css @@ -104,6 +104,8 @@ nav{flex:1;overflow-y:auto;padding:4px 10px} .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-nav-btn{margin-top:8px;background:none;border:1px solid var(--accent-dim);border-radius:6px;color:var(--accent);font-size:12px;padding:4px 10px;cursor:pointer;transition:background .15s,color .15s} +.notif-nav-btn:hover{background:var(--accent);color:#fff} .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)}