feat(web): Filter "❤ Gefolgt" plus Suche auf der Künstlerseite

Die Liste, wem man folgt, gehört dorthin, wo man Künstler ohnehin sucht —
bisher gab es sie nur als waagerechte Reihe unter den Favoriten.
getArtists liefert kein starred mit, die Markierungen kommen deshalb aus
getStarred2 und werden 30 s gehalten, damit das Umschalten flott bleibt.

Ein Herz-Klick verwirft diesen Zwischenspeicher sofort — sonst zeigte der
Filter nach dem Entfolgen noch eine halbe Minute den alten Stand. Beim
Testen aufgefallen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-09-18 13:58:09 +02:00
1 parent 908488a337
commit f3d54158d3
5 files changed
+64 -4

No files matched your search

+6
View File
@@ -101,6 +101,12 @@ Die Android-App zählt eigenständig weiter (mehrere APKs pro Release). Welche d
- Wer ohne Künstler-Pin folgt, bekommt einen Hinweis: ohne Pinnung läuft die Deezer-Zuordnung über - Wer ohne Künstler-Pin folgt, bekommt einen Hinweis: ohne Pinnung läuft die Deezer-Zuordnung über
den Namen, und bei gleichnamigen Künstlern kämen dauerhaft die Neuheiten des Falschen. den Namen, und bei gleichnamigen Künstlern kämen dauerhaft die Neuheiten des Falschen.
- **Filter „❤ Gefolgt" auf der Künstlerseite (Web)** — die Künstlerseite hat jetzt zwei Chips
(*Alle* / *❤ Gefolgt (n)*) und ein Suchfeld. Damit ist die Liste, wem man folgt, dort, wo man
Künstler ohnehin sucht — statt nur als waagerechte Reihe unter den Favoriten. `getArtists` liefert
kein `starred` mit, die Markierungen kommen deshalb aus `getStarred2` und werden 30 s gehalten,
damit das Umschalten der Chips nicht jedes Mal nachfragt. Ein Herz-Klick verwirft diesen Zwischen-
speicher sofort — sonst zeigte der Filter nach dem Entfolgen noch eine halbe Minute den alten Stand.
- **Zeitleiste „Neu von deinen Künstlern" (Web)** — eigener Punkt in der Seitenleiste. Zeigt die - **Zeitleiste „Neu von deinen Künstlern" (Web)** — eigener Punkt in der Seitenleiste. Zeigt die
jüngsten Veröffentlichungen aller Künstler, denen du folgst: nach Monat gruppiert, neueste zuerst, jüngsten Veröffentlichungen aller Künstler, denen du folgst: nach Monat gruppiert, neueste zuerst,
pro Eintrag Cover, Künstler, Art (Album/Single/EP), Titelzahl und Datum. Umschaltbar auf 30 Tage, pro Eintrag Cover, Künstler, Art (Album/Single/EP), Titelzahl und Datum. Umschaltbar auf 30 Tage,
+8
View File
@@ -147,6 +147,14 @@
<!-- ARTISTS --> <!-- ARTISTS -->
<div class="page" id="page-artists"> <div class="page" id="page-artists">
<div class="page-header"><div class="page-title">Künstler</div></div> <div class="page-header"><div class="page-title">Künstler</div></div>
<div id="artists-filter" class="sort-chips">
<button class="sort-chip active" data-filter="all" onclick="loadArtists('all')">Alle</button>
<button class="sort-chip" data-filter="followed" onclick="loadArtists('followed')">❤ Gefolgt <span id="artists-followed-count"></span></button>
</div>
<div class="list-search-wrap">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input id="artists-search" class="list-search" type="text" placeholder="Künstler suchen…" oninput="filterArtists(this.value)">
</div>
<div id="artists-grid" class="grid grid-4"></div> <div id="artists-grid" class="grid grid-4"></div>
</div> </div>
+2 -2
View File
@@ -20,7 +20,7 @@ import {
navigate, _navGoBack, _navOnPopState, _navLoad, onHeaderSearch, closeHeaderSearch, navigate, _navGoBack, _navOnPopState, _navLoad, onHeaderSearch, closeHeaderSearch,
onGlobalSearch, s_goArtist, s_goAlbum, playerBarGoArtist, playerBarGoAlbum, onGlobalSearch, s_goArtist, s_goAlbum, playerBarGoArtist, playerBarGoAlbum,
searchSimilarArtist, searchAndPreview, loadDeezerAlbum, closeEventsModal, searchSimilarArtist, searchAndPreview, loadDeezerAlbum, closeEventsModal,
loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist, loadAlbums, loadArtists, filterArtists, loadStarred, loadRandom, filterStarred, filterPlaylist,
plReorderEnter, plReorderSave, plReorderCancel, toggleAllGenres, plReorderEnter, plReorderSave, plReorderCancel, toggleAllGenres,
} from './views/views.js'; } from './views/views.js';
import { loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll, import { loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll,
@@ -367,7 +367,7 @@ Object.assign(window, {
openCompare, closeCompare, openCompare, closeCompare,
closeArtistPin, clearArtistPin, closeArtistPin, clearArtistPin,
// Views // Views
loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist, loadAlbums, loadArtists, filterArtists, loadStarred, loadRandom, filterStarred, filterPlaylist,
plReorderEnter, plReorderSave, plReorderCancel, toggleAllGenres, plReorderEnter, plReorderSave, plReorderCancel, toggleAllGenres,
loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll, loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll,
onImporterInput, resolveImporterLink, onGlobalInput, tryImporterResolve, onImporterInput, resolveImporterLink, onGlobalInput, tryImporterResolve,
+3
View File
@@ -537,6 +537,9 @@ function _verdrahteMerken(btn, id, markiert, textAn, textAus, meldungAn, meldung
try { try {
await api(vorher ? 'unstar' : 'star', `&id=${encodeURIComponent(id)}`); await api(vorher ? 'unstar' : 'star', `&id=${encodeURIComponent(id)}`);
invalidateApiCache('getStarred2'); invalidateApiCache('getStarred2');
// Der "Gefolgt"-Filter auf der Künstlerseite hält eine eigene kurze Liste — die muss mit weg,
// sonst zeigt er nach dem Entfolgen noch eine halbe Minute den alten Stand.
import('./home.js').then(m => m.invalidateGefolgt?.()).catch(() => {});
toast(an ? meldungAn : meldungAus); toast(an ? meldungAn : meldungAus);
} catch (e) { } catch (e) {
an = vorher; malen(); an = vorher; malen();
+45 -2
View File
@@ -210,7 +210,12 @@ export async function loadAlbumRow(id, method, extra, force = false) {
} }
// ─── ARTISTS ─── // ─── ARTISTS ───
export async function loadArtists() { let _artistsFilter = 'all';
export async function loadArtists(filter = _artistsFilter) {
_artistsFilter = filter;
document.querySelectorAll('#artists-filter .sort-chip').forEach(c =>
c.classList.toggle('active', c.dataset.filter === filter));
const g = document.getElementById('artists-grid'); const g = document.getElementById('artists-grid');
if (!apiIsCached('getArtists')) g.innerHTML = '<div class="spinner"></div>'; if (!apiIsCached('getArtists')) g.innerHTML = '<div class="spinner"></div>';
try { try {
@@ -218,8 +223,24 @@ export async function loadArtists() {
const all = (r.artists?.index || []).flatMap(i => i.artist || []); const all = (r.artists?.index || []).flatMap(i => i.artist || []);
cacheArtistSortNames(all); cacheArtistSortNames(all);
if (S.cfg.showRomaji) await fetchRomajiIndex().catch(() => {}); if (S.cfg.showRomaji) await fetchRomajiIndex().catch(() => {});
// "Gefolgt" = Navidromes Stern. getArtists liefert kein `starred` mit, die Liste kommt also
// aus getStarred2 — zwischengespeichert, damit das Umschalten nicht jedes Mal nachfragt.
const gefolgt = await _gefolgteIds();
const zaehler = document.getElementById('artists-followed-count');
if (zaehler) zaehler.textContent = gefolgt.size ? `(${gefolgt.size})` : '';
const liste = filter === 'followed' ? all.filter(a => gefolgt.has(a.id)) : all;
g.innerHTML = ''; g.innerHTML = '';
all.forEach(a => { const suche = document.getElementById('artists-search');
if (suche) suche.value = '';
if (!liste.length) {
g.innerHTML = filter === 'followed'
? '<div class="empty-state"><div class="icon">❤</div><p>Du folgst noch niemandem</p><p style="font-size:12px;color:var(--text3);margin-top:6px">Auf einer Künstlerseite oben rechts aufs Herz tippen.</p></div>'
: '<div class="empty-state"><div class="icon">🎤</div><p>Keine Künstler</p></div>';
return;
}
liste.forEach(a => {
const c = makeCard(dispArtistById(a.id, a.name), a.albumCount+' Alben', null, '🎤'); const c = makeCard(dispArtistById(a.id, a.name), a.albumCount+' Alben', null, '🎤');
c.classList.add('artist-card'); c.classList.add('artist-card');
c.onclick = () => navigate('artist-detail', a); c.onclick = () => navigate('artist-detail', a);
@@ -239,6 +260,28 @@ export async function loadArtists() {
} catch(e) { g.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; } } catch(e) { g.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; }
} }
/** IDs der markierten Künstler; kurz gecacht, damit das Umschalten der Chips flott bleibt. */
let _gefolgtCache = { ts: 0, ids: new Set() };
/** Nach einem Herz-Klick verwerfen — sonst zeigte der Filter bis zu 30 s den alten Stand. */
export function invalidateGefolgt() { _gefolgtCache = { ts: 0, ids: _gefolgtCache.ids }; }
async function _gefolgteIds() {
if (Date.now() - _gefolgtCache.ts < 30_000) return _gefolgtCache.ids;
try {
const r = await api('getStarred2');
_gefolgtCache = { ts: Date.now(), ids: new Set((r.starred2?.artist || []).map(a => a.id)) };
} catch { /* Filter bleibt beim letzten bekannten Stand */ }
return _gefolgtCache.ids;
}
export function filterArtists(val) {
const q = val.trim().toLowerCase();
document.querySelectorAll('#artists-grid .card').forEach(c => {
const name = (c.querySelector('.card-title')?.textContent || '').toLowerCase();
c.style.display = !q || name.includes(q) ? '' : 'none';
});
}
// ─── ALBUMS ─── // ─── ALBUMS ───
let _albumSort = 'alphabeticalByName'; let _albumSort = 'alphabeticalByName';