feat: add navigation button to notifications for direct song/album/playlist access

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-04-28 10:01:41 +02:00
1 parent d9b9151f50
commit 1bb8494fdb
3 files changed
+46 -8

No files matched your search

+2 -2
View File
@@ -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,
});
+42 -6
View File
@@ -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
? `<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);
const navBtn = hasNav
? `<button class="notif-nav-btn" onclick="_notifNavigate('${esc(n.id)}')">Öffnen →</button>`
: '';
return `<div class="notif-card${n.read ? '' : ' unread'}">
<div class="notif-card-top">
<span class="notif-icon">${icon}</span>
@@ -437,18 +442,46 @@ function _renderNotifList() {
${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;
// 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!`);