fix: startup speed, image retry, shuffle random start, genres, search badge, playlist refresh

- Remove await on fetchRomajiIndex() at startup so navigation is not blocked
  by the romaji index network call (was the main cause of 30s Android startup)
- Add window._imgRetry() with 3 retries (1.5s/3s/4.5s backoff) for all card/
  song/cover images that fail to load; falls back to emoji only after exhausting retries
- playQueue with HAS_BRIDGE + shuffle: pick a random startIdx so album/playlist
  playback starts at a random track, not always index 0
- Reduce mobile genre grid limit from 12 to 6
- Fix "Global Suche" badge text: was "iTunes Store + Local", now "iTunes Store"
- deleteSong: always refresh loadPlaylists() unconditionally (not just when
  page is active) with a 3s delay so song counts are current when navigating back

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-06-04 10:38:15 +02:00
1 parent 6dc47a2c77
commit 1f1a5cd3a6
6 files changed
+25 -12

No files matched your search

+1 -1
View File
@@ -315,7 +315,7 @@
<div class="page" id="page-discover">
<div class="page-header">
<div class="page-title"><span class="blue">Global</span> Suche</div>
<span class="badge badge-itunes" style="font-size:11px;padding:4px 10px">iTunes Store + Local</span>
<span class="badge badge-itunes" style="font-size:11px;padding:4px 10px">iTunes Store</span>
</div>
<div class="search-wrap" style="padding:0 0 20px">
<div class="search-box" style="background:var(--surface);border-color:var(--border2)">
+10 -2
View File
@@ -1,5 +1,13 @@
import { S } from './state.js';
import { esc, formatDur } from './utils.js';
// Exposed on window so inline onerror= handlers can call it.
// Retries the same src up to 3 times with increasing delays, then shows fallback.
window._imgRetry = function(img, fallback) {
const n = (img._r = (img._r || 0) + 1);
if (n > 3 || !img.src) { img.parentNode.innerHTML = `<div class="no-art">${fallback}</div>`; return; }
setTimeout(() => { const s = img.src; img.src = ''; img.src = s; }, 1500 * n);
};
import { coverUrl, dispTitle, dispArtistParts, dispAlbum } from './api.js';
// Circular imports — safe (only used inside event handlers after module init)
import { playQueue, toggleStar, playPreview, parseArtists } from './player.js';
@@ -11,7 +19,7 @@ export function makeCard(title, sub, imgUrl, fallback='💿') {
const c = document.createElement('div');
c.className = 'card';
c.innerHTML = `<div class="card-art">
${imgUrl ? `<img src="${imgUrl}" loading="lazy" onerror="this.parentNode.innerHTML='<div class=\\'no-art\\'>${fallback}</div>'">` : `<div class="no-art">${fallback}</div>`}
${imgUrl ? `<img src="${imgUrl}" loading="lazy" onerror="_imgRetry(this,'${fallback}')">` : `<div class="no-art">${fallback}</div>`}
<div class="play-overlay"><button class="play-btn"><svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg></button></div>
</div>
<div class="card-info"><div class="card-title">${esc(title)}</div><div class="card-sub">${esc(sub||'')}</div></div>`;
@@ -45,7 +53,7 @@ export function makeSongItem(s, idx, list, { noArtistLink = false } = {}) {
? `<span class="song-plays">${s.playCount ? s.playCount.toLocaleString() + '<span class="song-plays-x">×</span>' : '—'}</span>`
: '';
el.innerHTML = `<div class="song-num">${idx+1}</div>
<div class="song-thumb">${cv ? `<img src="${cv}" loading="lazy" onerror="this.parentNode.innerHTML='<div class=\\'no-art\\'>♪</div>'">` : '<div class="no-art">♪</div>'}</div>
<div class="song-thumb">${cv ? `<img src="${cv}" loading="lazy" onerror="_imgRetry(this,'♪')">` : '<div class="no-art">♪</div>'}</div>
<div class="song-info">
<div class="song-title">${esc(dispTitle(s))}</div>
<div class="song-artist">${artistHtml}</div>
+1 -1
View File
@@ -94,7 +94,7 @@ async function startApp() {
if (bnav) bnav.style.display = '';
absLoadSettingsLibraries();
}
if (S.cfg.showRomaji) await fetchRomajiIndex().catch(() => {});
if (S.cfg.showRomaji) fetchRomajiIndex().catch(() => {}); // fire-and-forget — don't block navigation
if(_navLoad() && S._navCursor >= 0 && S._navStack[S._navCursor]) {
const last = S._navStack[S._navCursor];
S._navJumping = true;
+5
View File
@@ -359,6 +359,11 @@ export function playQueue(songs, startIdx) {
_radioFilling = false; // cancel any in-flight radio fill for the old queue
S._origQueue = null;
_histClear();
// On Android with shuffle on, start at a random position so we don't always
// begin at index 0 (the first album/playlist track).
if (HAS_BRIDGE && S.shuffle && songs.length > 1) {
startIdx = Math.floor(Math.random() * songs.length);
}
if (!HAS_BRIDGE && S.shuffle && songs.length > 1) {
const start = songs[startIdx];
const rest = songs.filter((_, i) => i !== startIdx).sort(() => Math.random() - 0.5);
+4 -4
View File
@@ -536,13 +536,13 @@ export async function deleteSong() {
if (!r.ok) throw new Error(d.error || `HTTP ${r.status}`);
toast(`🗑 "${s.title}" gelöscht`);
closeInfoModal();
// Refresh playlist views after Navidrome's background scan has a chance to finish
// Refresh playlist data after Navidrome's background scan has a chance to finish.
// Always reload playlists (even if not active) so counts are fresh when navigating back.
setTimeout(() => {
if (document.getElementById('page-playlists')?.classList.contains('active'))
loadPlaylists();
else if (document.getElementById('page-playlist-detail')?.classList.contains('active') && S._currentPlaylist)
if (document.getElementById('page-playlist-detail')?.classList.contains('active') && S._currentPlaylist)
loadPlaylistDetail(S._currentPlaylist);
}, 2000);
}, 3000);
} catch (e) {
status.textContent = '❌ Fehler: ' + e.message;
}
+4 -4
View File
@@ -180,7 +180,7 @@ function _genreColor(name) {
let _allGenres = [];
let _genresExpanded = false;
const GENRE_LIMIT = () => window.innerWidth >= 700 ? 24 : 12;
const GENRE_LIMIT = () => window.innerWidth >= 700 ? 24 : 6;
async function loadGenreSection() {
const el = document.getElementById('home-genres');
@@ -631,7 +631,7 @@ export async function loadItunesAlbumDetail(album) {
document.getElementById('itunes-album-detail-artist').textContent = album.artistName || '';
const cw = document.getElementById('itunes-album-cover-wrap');
const art = (album.artworkUrl100 || '').replace('100x100bb','600x600bb').replace('100x100','600x600');
cw.innerHTML = art ? `<img src="${art}" onerror="this.parentNode.innerHTML='<div class=\\'no-art\\'>💿</div>'">` : '<div class="no-art">💿</div>';
cw.innerHTML = art ? `<img src="${art}" onerror="_imgRetry(this,'💿')">` : '<div class="no-art">💿</div>';
const dlBtn = document.getElementById('itunes-album-dl-btn');
dlBtn.onclick = () => openDlModal({ artistName: album.artistName, trackName: album.collectionName, collectionName: album.collectionName, artworkUrl100: album.artworkUrl100||'', isAlbum: true, itunesData: album, collectionId: album.collectionId });
const sl = document.getElementById('itunes-album-songs');
@@ -657,7 +657,7 @@ export async function loadAlbumDetail(album) {
.join('<span style="color:var(--text3)"> • </span>') || '';
const cw = document.getElementById('album-cover-wrap');
const cv = coverUrl(album.coverArt, 512);
cw.innerHTML = cv ? `<img src="${cv}" onerror="this.parentNode.innerHTML='<div class=\\'no-art\\'>💿</div>'">` : '<div class="no-art">💿</div>';
cw.innerHTML = cv ? `<img src="${cv}" onerror="_imgRetry(this,'💿')">` : '<div class="no-art">💿</div>';
const sl = document.getElementById('album-songs');
sl.innerHTML = '<div class="spinner"></div>';
try {
@@ -717,7 +717,7 @@ export async function loadPlaylistDetail(pl) {
document.getElementById('playlist-detail-name').textContent = pl.name;
const cw = document.getElementById('playlist-cover');
const cv = coverUrl(pl.coverArt, 512);
cw.innerHTML = cv ? `<img src="${cv}" onerror="this.parentNode.innerHTML='<div class=\\'no-art\\'>🎶</div>'">` : '<div class="no-art">🎶</div>';
cw.innerHTML = cv ? `<img src="${cv}" onerror="_imgRetry(this,'🎶')">` : '<div class="no-art">🎶</div>';
document.getElementById('btn-edit-playlist').onclick = () => openEditPlModal(S._currentPlaylist);
const sl = document.getElementById('playlist-songs');
sl.innerHTML = '<div class="spinner"></div>';