feat(web): Playlists und Favoriten als Liste in der Seitenleiste

Statt zweier Knoepfe, die erst eine Uebersichtsseite oeffnen, steht unter den
Navigationsrubriken der Abschnitt "Deine Musik": angepinnt Favoriten und "Neu von
deinen Kuenstlern", darunter die Playlists mit Cover und Titelzahl.

Links und nicht rechts, weil rechts belegt ist: dort faehrt die Warteschlange als
290-px-Panel ein und am Rand sitzt der Rollbalken aus 3.1.

Der Listenteil rollt eigenstaendig (nav ist jetzt eine Flex-Spalte), auf niedrigen
Fenstern rollt als Rueckfall die ganze Leiste. Dieselbe gecachte Abfrage wie die
Uebersichtsseite; nach Anlegen/Umbenennen/Loeschen wird der Cache verworfen und neu
gezeichnet, ein Coverwechsel tauscht nur die betroffene Zeile (gleiche Adresse pl-<id>,
sonst kaeme dasselbe Bild aus dem Browser-Cache).

Die Kachel-Uebersicht bleibt per "Alle" erreichbar, die untere Leiste auf dem Handy
bleibt unveraendert.

Ausserdem: in 3.2 uebersehene Mischstelle nachgezogen — der Shuffle-Knopf der
Playlist-Seite benutzte weiter sort(() => Math.random()-.5). Mein Suchmuster hatte nur
die Schreibweise mit 0.5 erfasst, nicht die mit .5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-10-13 19:40:56 +02:00
1 parent 7cdcc352d6
commit 9afd73ae7f
7 files changed
+139 -10

No files matched your search

+58 -1
View File
@@ -3,6 +3,7 @@ import { esc, toast, formatGesamtdauer } from '../utils.js';
import { api, apiCached, apiIsCached, coverUrl, romajiLookup, dispArtistById, getArtistOverride } from '../api.js';
import { makeCard, makeAlbumCard, makeSongItem, mkSection } from '../ui/components.js';
import { playQueue } from '../player/player.js';
import { mischeListe } from '../player/queueMath.js';
import { openEditPlModal } from '../ui/ui.js';
// Circular — safe (called inside event handlers)
import { navigate } from './navigation.js';
@@ -48,6 +49,62 @@ export async function loadPlaylists() {
} catch(e) { g.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; }
}
// ─── DEINE MUSIK (Seitenleiste) ───────────────────────────────────────────────
// Die Playlists stehen links als Liste, nicht hinter einem Knopf. Dieselbe gecachte Abfrage
// wie die Uebersichtsseite (3 Min.), es entsteht also kein zusaetzlicher Netzverkehr: wer die
// Seite oeffnet, hat den Stand schon, und umgekehrt.
export async function renderSidebarPlaylists() {
const el = document.getElementById('sidebar-playlists');
if (!el) return;
try {
const r = await apiCached('getPlaylists', '', 3 * 60 * 1000);
const lists = r.playlists?.playlist || [];
el.innerHTML = '';
if (!lists.length) {
el.innerHTML = '<div class="lib-empty">Noch keine Playlists — mit + anlegen.</div>';
return;
}
for (const pl of lists) {
const von = _besitzer(pl);
const cv = coverUrl(pl.coverArt, 128);
const row = document.createElement('div');
row.className = 'lib-item';
row.dataset.plId = pl.id;
row.title = pl.name + (von ? ' · von ' + von : '');
row.innerHTML =
`<div class="lib-art">${cv ? `<img src="${cv}" loading="lazy" onerror="_imgRetry(this,'🎶')">` : '🎶'}</div>` +
`<div class="lib-text"><div class="lib-name">${esc(pl.name)}</div>` +
`<div class="lib-sub">${pl.songCount || 0} Titel${von ? ' · ' + esc(von) : ''}</div></div>`;
row.onclick = () => navigate('playlist-detail', pl);
el.appendChild(row);
}
markSidebarPlaylist(S._currentPlaylist?.id);
} catch {
// Kein Alarm in der Seitenleiste: die Uebersichtsseite meldet Fehler ohnehin.
el.innerHTML = '<div class="lib-empty">Playlists nicht geladen.</div>';
}
}
/**
* Tauscht das Cover EINER Zeile aus, nachdem es gerade hochgeladen oder entfernt wurde.
* Noetig, weil die Cover-Adresse dieselbe bleibt (`pl-<id>`): ein Neuzeichnen holte dasselbe
* Bild aus dem Browser-Cache, und die Zeile zeigte weiter das alte Cover.
*/
export function bustSidebarCover(plId, coverArt) {
const row = document.querySelector(`#sidebar-playlists .lib-item[data-pl-id="${plId}"] .lib-art`);
if (!row) return;
const cv = coverArt ? coverUrl(coverArt, 128) + '&_cb=' + Date.now() : '';
row.innerHTML = cv ? `<img src="${cv}" onerror="_imgRetry(this,'🎶')">` : '🎶';
}
/** Hebt die laufend geoeffnete Playlist hervor. Aufgerufen aus navigate(). */
export function markSidebarPlaylist(plId) {
const id = plId == null ? '' : String(plId);
document.querySelectorAll('#sidebar-playlists .lib-item').forEach(el => {
el.classList.toggle('active', !!id && el.dataset.plId === id);
});
}
export async function loadPlaylistDetail(pl) {
S._currentPlaylist = {...pl};
document.getElementById('playlist-detail-name').textContent = pl.name;
@@ -73,7 +130,7 @@ export async function loadPlaylistDetail(pl) {
if(descEl) { descEl.textContent = full.comment||''; descEl.style.display = full.comment ? '' : 'none'; }
document.getElementById('btn-play-playlist').onclick = () => playQueue(songs, 0);
document.getElementById('btn-shuffle-playlist').onclick = () => {
playQueue([...songs].sort(() => Math.random()-.5), 0);
playQueue(mischeListe(songs), 0);
};
sl.innerHTML = '';
const plSearch = document.getElementById('playlist-search');