diff --git a/CHANGELOG.md b/CHANGELOG.md index 2698130..20d8b22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ Versioning: two-part `MAJOR.MINOR` — each release bumps the minor (features an - **Android** — Download crash on Android 15 (targetSdk 35): `FOREGROUND_SERVICE_TYPE_DATA_SYNC` is banned for apps targeting API 35; WorkManager's expedited path used it and the resulting exception killed the app ~1.5 s after tapping download. Fixed by dropping `setExpedited()` and all foreground-service use from `DownloadWorker` — downloads run as silent background work with progress shown in the Offline tab - **Android** — Offline detail screen flashed open then immediately returned: its initial empty state (before Room emitted) was mistaken for "deleted". Now shows a brief spinner until loaded, and only leaves when the last song is actually removed - **Android** — Lyrics autoscroll: rewrote centering to use the active line's *measured* position in a `LazyColumn` instead of a fixed line-height estimate. Eliminates the stutter and the downward drift on wrapped (multi-line) lyrics — the active line now tracks smoothly and sits at a natural ~40 %-from-top reading anchor +- **Web** — Home now refreshes on an explicit Home click (matching Android): the album rows re-fetch and the random row re-randomizes instead of serving the 5-minute cache. Back/forward navigation still keeps the cached state ### Internal - **Android** — Offline system: `Room` DB (`novasonic_offline.db`) with `downloaded_tracks`, `download_groups`, `group_track_cross_refs`; `OfflineRepository` holds hot `StateFlow` maps for O(1) local-path lookups; `DownloadWorker` streams audio + cover art via `HttpURLConnection` to `context.filesDir/offline/{audio|covers}/`. `MusicRepository.streamUrl()` checks local files first, so playback uses the `file://` URI on every entry point @@ -306,4 +307,4 @@ Primary causes identified: ### Known Bugs - **Backend (download)** — The duplicate check requires at least a `firstArtist` folder match. If the same song was previously stored under a very different artist name (e.g. before an iTunes-name fix), it will not be detected as a duplicate and will be re-downloaded. - **Backend (metadata/sorting)** — MusicBrainz `release['artist-credit']` is often absent in the standard recording search response (requires `inc=artist-credits` which can slow the endpoint). The albumArtist field from MusicBrainz autofill may fall back to the recording artist rather than the release artist. -- **Backend (metadata/sorting)** — MBIDs are not embedded in downloaded tracks; Navidrome cannot use them for authoritative album grouping (low priority — album splitting is mostly solved via consistent `album_artist`). +- **Backend (metadata/sorting)** — MBIDs are not embedded in downloaded tracks; Navidrome cannot use them for authoritative album grouping (low priority — album splitting is mostly solved via consistent `album_artist`). diff --git a/src/views/home.js b/src/views/home.js index a36f55c..2c14a7c 100644 --- a/src/views/home.js +++ b/src/views/home.js @@ -1,6 +1,6 @@ import { S } from '../state.js'; import { esc } from '../utils.js'; -import { api, apiCached, apiIsCached, coverUrl, fetchRomajiIndex, dispArtistById, dispAlbumName, cacheArtistSortNames } from '../api.js'; +import { api, apiCached, apiIsCached, invalidateApiCache, coverUrl, fetchRomajiIndex, dispArtistById, dispAlbumName, cacheArtistSortNames } from '../api.js'; import { makeCard, makeAlbumCard, makeSongItem } from '../ui/components.js'; import { playQueue } from '../player/player.js'; // Circular — safe (called inside event handlers) @@ -32,7 +32,7 @@ const _greetN = [ 'Time to escape into music', 'Pick something great', ]; -export async function loadHomeData() { +export async function loadHomeData(force = false) { const name = S.cfg.user || ''; const g = name ? `${_greetW[Math.floor(Math.random() * _greetW.length)]} ${name}` @@ -42,10 +42,10 @@ export async function loadHomeData() { loadHeroCard(); loadGenreSection(); - loadAlbumRow('home-newest', 'getAlbumList2', '&type=newest&size=20'); - loadAlbumRow('home-recent', 'getAlbumList2', '&type=recent&size=20'); - loadAlbumRow('home-random', 'getAlbumList2', '&type=random&size=20'); - loadAlbumRow('home-frequent', 'getAlbumList2', '&type=frequent&size=20'); + loadAlbumRow('home-newest', 'getAlbumList2', '&type=newest&size=20', force); + loadAlbumRow('home-recent', 'getAlbumList2', '&type=recent&size=20', force); + loadAlbumRow('home-random', 'getAlbumList2', '&type=random&size=20', force); + loadAlbumRow('home-frequent', 'getAlbumList2', '&type=frequent&size=20', force); } async function loadHeroCard() { @@ -194,9 +194,10 @@ export async function loadGenreDetail(g) { songs.forEach((s, i) => list.appendChild(makeSongItem(s, i, songs))); } -export async function loadAlbumRow(id, method, extra) { +export async function loadAlbumRow(id, method, extra, force = false) { const c = document.getElementById(id); if(!c) return; + if (force) invalidateApiCache(method, extra); // re-fetch (re-randomizes) on explicit Home click if (!apiIsCached(method, extra)) c.innerHTML = '
'; try { const [r] = await Promise.all([ diff --git a/src/views/navigation.js b/src/views/navigation.js index bda8ff9..a73ae3e 100644 --- a/src/views/navigation.js +++ b/src/views/navigation.js @@ -48,7 +48,7 @@ export function navigate(page, data=null) { if(['discover','new-releases','recommendations','random','spotify-importer'].includes(page)) switchNavMode('discover'); else if(['home','artists','albums','starred','playlists'].includes(page)) switchNavMode('library'); - if(page==='home') loadHomeData(); + if(page==='home') loadHomeData(!S._navJumping); // explicit Home click refreshes; back/forward keeps state if(page==='artists' && !document.getElementById('artists-grid').children.length) loadArtists(); if(page==='albums' && !document.getElementById('albums-grid').children.length) loadAlbums(); if(page==='starred') loadStarred();