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:
1 parent
908488a337
commit
f3d54158d3
5 files changed
+64
-4
No files matched your search
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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';
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user