Commit Graph
155 Commits
Author SHA1 Message Date
HoneyandClaude Opus 4.8 63a7260c94 fix(lyrics): stop hammering lrclib (caused IP throttling → no lyrics)
Rapid skips + lyrics prefetch fired the full lrclib cascade many times over,
so lrclib throttled the IP and requests timed out (canceled) on both web and
Android. Reduce the request volume:

- Web: de-dupe concurrent fetches per song (prefetch + tab-open now share one
  cascade); bump the per-request timeout 6s → 8s.
- Android: per-song Mutex so prefetch + on-open load don't both run the cascade;
  send a proper User-Agent (lrclib asks for one and treats it better); debounce
  the prefetch (1.5s, cancelled on the next skip) instead of firing immediately
  on every track change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 21:12:01 +02:00
HoneyandClaude Opus 4.8 211b8ec6c2 feat(lyrics): prefetch on play + smooth measured-position autoscroll
Warm the lrclib cache the moment a track starts (Android PlayerController
onMediaItemTransition; web onNpSongChange) so opening the Lyrics tab is
instant. Extract a DOM-free fetch helper on web for reuse.

Rewrite Android lyrics autoscroll to center on the active line's measured
position in a LazyColumn instead of a fixed line-height estimate — removes
stutter and the downward drift on wrapped lines; active line sits at a
~40%-from-top reading anchor.

CHANGELOG: fold into 1.0; drop the now-fixed lyrics known-issue note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 00:59:51 +02:00
HoneyandClaude Opus 4.8 73fcfae30f feat(android): offline downloads — albums, playlists, detail screen & playback
Download albums/playlists to private device storage (Room + WorkManager,
HttpURLConnection streaming). New Offline tab lists groups with progress;
tapping a group opens a detail screen (cover-art rows, play/shuffle, per-song
delete from device). Downloaded tracks play from disk on every screen.
Album/playlist cards show a downloaded badge; download button resumes missing
songs. Song storage is dedup'd by ID and reference-counted on delete.

Fixes: Android 15 download crash (dropped banned DATA_SYNC foreground service);
offline detail flash-back (initial empty state no longer mistaken for deleted).

Docs: CHANGELOG 0.10 + parity matrix; condensed README Features.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 10:06:43 +02:00
HoneyandClaude Sonnet 4.6 e326ee6350 feat(ui): new equalizer logo, favicon link, fix favicon path
- icon.svg / icon-192/512.png: blue→purple equalizer gradient design
- nginx.conf: add /app/ alias so reverse-proxy prefix resolves assets
- vite.config.js: base /app/ + %BASE_URL% in favicon links so path
  resolves correctly whether app is served at / or /app/
- ic_launcher.xml: same equalizer design for Android app icon

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 19:50:30 +02:00
HoneyandClaude Sonnet 4.6 925c173c15 chore: stage previously uncommitted screen changes (ArtistDetail, ItunesArtist, NowPlaying)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 20:52:42 +02:00
Honey 85252e24df fix: unresolved reference 'albums' after rename to albumsByNewest/albumsByName 2026-06-18 20:21:36 +02:00
HoneyandClaude Sonnet 4.6 93c5fb481d feat(android): add description field to playlist edit dialog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 20:13:19 +02:00
HoneyandClaude Sonnet 4.6 43cf11465e fix: playlist edit — cover upload in pencil dialog, remove cover-remove button
Android: camera hero button removed; cover upload moved into edit dialog.
Web: trash icon removed from cover actions in edit modal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 20:12:27 +02:00
HoneyandClaude Sonnet 4.6 6a1bf33f31 fix(android): album Neueste now uses Navidrome type=newest (add-date), not year sort
Loads albumsByNewest (type=newest) and albumsByName (alphabetical) in parallel.
Neueste → Navidrome add-date order; A-Z → alphabetical; Älteste → client-side by year asc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 20:09:32 +02:00
HoneyandClaude Sonnet 4.6 9ffd071c58 feat(android): artist fast-scroll index bar with letter bubble
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 20:06:44 +02:00
HoneyandClaude Sonnet 4.6 dd484df636 fix(android): album sort — scroll to top on change, remove Künstler, default Neueste
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 20:05:00 +02:00
HoneyandClaude Sonnet 4.6 6be7981b8a feat(android): playlist mgmt, album sort, home hero parity, notification fix
- Playlist: create (FAB + dialog), rename, delete, remove songs, cover upload/remove
- Library Albums tab: sort chips (Neueste/Älteste/A-Z/Künstler) matching StarredScreen
- ABS notification: shows current chapter title; updates live without re-buffering
- Home hero: pool now matches web (frequent(5) ∪ recentlyPlayed(5)); label MEISTGESPIELT / ZULETZT GEHÖRT
- Home: added Zuletzt gehört section (type=recent) matching web layout
- Notifications: fixed delivery bug — polling moved to NotificationStore app-lifetime scope so notifications arrive even after user navigates away from ImporterScreen
- NotificationStore.startPoll() extended with navType/navAlbum/navArtist/navId params
- Removed defunct AbsPlayerService.kt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 19:04:36 +02:00
HoneyandClaude Sonnet 4.6 eacce487df fix: lyrics scroll — center active line only, remove drift phase
The 'drift to next line' animation moved the text while the user was
still reading/singing the current line, making it feel out of sync.
Now matches the web behavior: smooth-scroll to center the active line
when it changes, nothing more.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 01:39:51 +02:00
HoneyandClaude Sonnet 4.6 ec9c0add9e feat: notification nav + ABS search + HTML desc fix + brighter NowPlaying bg
- Notification sheet: add 'Zum Album →' / 'Öffnen →' button; tapping searches
  Navidrome and navigates to the matching album/playlist
- AppNotification: add navType/navAlbum/navArtist/navId fields (mirrors web notify.js)
- ImporterViewModel: store resolved album/artist/type in notification after download
- ABS Android: search bar with 400ms debounce, server-side search via /search endpoint
- AbsClient: add searchLibraryItems() for library-specific search
- AbsScreen/abs.js: strip HTML tags from book descriptions (<p> tags no longer visible)
- NowPlayingScreen: vibrant palette swatch + lighter scrim (0.15) + wider gradient
  for more visible dominant color background

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 01:38:58 +02:00
HoneyandClaude Sonnet 4.6 72feac41bb fix: ABS skip library list + nav label overflow + NowPlaying top bar + dominant color bg
- ABS: new AbsBooksScreen loads books directly on tab open; library pills shown only when >1 library — no more two-step library→books navigation
- Nav: shorten "Hörbücher"→"Bücher", "Einstellungen"→"Einst.", reduce label font to 9sp with maxLines=1 to prevent label wrapping
- NowPlaying: increase top padding so the tab bar sits lower on screen (statusBarTop + 16dp, min 52dp)
- NowPlaying: replace blurred-cover background with dominant-color gradient extracted via Palette from cover art; animates on track change; falls back to dark color

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 00:23:43 +02:00
HoneyandClaude Sonnet 4.6 820a75f40b feat: ABS full playback on Android — chapters, speed, sleep timer, progress sync
- AbsClient: add session API (startSession, closeSession, getProgress, saveProgress, getItemChapters) and data classes (AbsChapter, AbsAudioTrack, AbsPlaySession, AbsProgress)
- AbsPlayerController (new): standalone ExoPlayer controller for ABS audio; tracks global time across multi-track audiobooks, syncs progress to ABS every 30 s, chapter navigation, speed control, sleep timer
- AbsScreen: book detail screen with cover/meta/progress/chapter list/play button; AbsNowPlayingScreen overlay (progress bar, prev/+30s/play/-30s/next controls, speed picker, sleep timer, chapter list with auto-scroll)
- AppNavigation: create AbsPlayerController, wire up book detail and player overlay, hardware back button handling for all ABS sub-screens
- PrefsStore + SettingsViewModel + SettingsScreen: absChapterTrackMode toggle — progress bar shows current chapter length instead of full book
- README: update ABS section to cover both web and Android features

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 00:15:30 +02:00
HoneyandClaude Sonnet 4.6 3280ffee37 docs: trim README to core features only; fix: remove duplicate save button in Android settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 23:58:15 +02:00
HoneyandClaude Sonnet 4.6 613d5156fa fix: settings order now matches web — Theme → Bibliothek → ABS → Discover APIs → Navidrome
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 23:54:11 +02:00
HoneyandClaude Sonnet 4.6 a0d3c7a592 feat: ABS integration, last played song, settings reorder + conn dots
Settings:
- Reordered to match web: Navidrome → Bibliothek → Theme → Audiobookshelf → Discover APIs
- Save button now appears at top AND bottom for easy access
- Audiobookshelf section: Server URL + API Key (masked) + green/red status dot with Test button
- NovaSonic Backend section: same green/red conn dot + Test button
- Connection status uses ConnStatus sealed class (Idle/Checking/Ok/Err)

Audiobookshelf:
- New AbsClient.kt: Ktor HTTP client, proxies through NovaSonic backend or direct with Bearer token
- New AbsScreen.kt: library list → book list with covers, author, duration
- Hörbücher tab appears in bottom nav only when ABS URL + API key are both set
- absUrl + absApiKey persisted in DataStore

Last played song:
- PlayerController saves song to DataStore on every media item transition
- MiniPlayer shows last song (dimmed, single play button) when no active queue
- Tapping play fetches the song from Navidrome and resumes
- LastPlayedSong stored in DataStore (id, title, artist, coverArt)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 23:49:36 +02:00
HoneyandClaude Sonnet 4.6 f7012dab49 fix: login flash, album name casing, lrclib duration mismatch
- Android: show nothing while DataStore loads credentials (null initial
  value) to prevent login screen flash on startup when already logged in
- Web: dispAlbumName now only uses sortName as romaji for Japanese titles;
  English albums with lowercase ALBUMSORT tags now display correct casing
- lrclib: add no-duration fallback exact match to find songs where local
  file duration differs from lrclib entry (e.g. Sanctuary Opening 5:58 vs 4:24)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 22:50:11 +02:00
HoneyandClaude Sonnet 4.6 0272b9e99f chore: remove NetEase lyrics fallback from Android and web
NetEase returned wrong/unsynced results. lrclib with exact + fuzzy
search is sufficient; no third-party Chinese platform needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 21:48:46 +02:00
HoneyandClaude Sonnet 4.6 eb5b0ea360 fix: lrclib rate limit — exact first, then 2 parallel searches; check HTTP 200
3 simultaneous requests were hitting lrclib rate limiter.
Now: exact match alone → if no content, 2 parallel searches.
Also check response.status === 200 before parsing to avoid treating
404/429 error bodies as valid empty lyrics responses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 21:46:09 +02:00
HoneyandClaude Sonnet 4.6 d7f8b301ab perf: parallel lrclib requests + in-memory cache for lyrics
- Android + web: fire exact/artist-search/title-search in parallel so
  total wait = slowest single request instead of sum of all three
- Android: HashMap cache keyed by artist|title, cleared on app restart
- Web: Map cache (session lifetime), cache hit is instant re-open
- Web: extract _showLyricsResult helper, safeJson wrapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 21:26:07 +02:00
HoneyandClaude Sonnet 4.6 d0df01a76d fix: lyrics faster + title-only search fallback for metadata mismatches
- Android: add 6s HttpTimeout to LrcLibClient, wrap each step in withTimeoutOrNull
- Both: add title-only lrclib search (step 3) so "Sanctuary (Opening)" is found
  even when artist tag differs ("Utada" vs "Hikaru Utada")
- Web: reduce all timeouts 8s → 5s, extract bestMatch helper

Search order: lrclib exact → lrclib artist+title → lrclib title-only → NetEase

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 21:20:47 +02:00
HoneyandClaude Sonnet 4.6 71667d1a03 feat: add NetEase as lyrics fallback after lrclib (Android + web)
Search order: lrclib exact → lrclib fuzzy search → NetEase.
NetEase has strong coverage for Asian/Japanese music that lrclib misses.
Duration-matched candidate selection on all three steps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 20:50:16 +02:00
HoneyandClaude Sonnet 4.6 8b9bfd3098 fix: restore direct lrclib lyrics search with fuzzy fallback (both platforms)
Navidrome lyrics plugin does not implement getLyricsBySongId for synced
lyrics — removed all plugin calls. Both Android and web now query
lrclib.net/api/get (exact match) with /api/search fallback for metadata
mismatches (e.g. artist tagged differently in Navidrome vs lrclib).
Also fixes Android lyrics scroll tween<Int> type inference error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 20:45:32 +02:00
HoneyandClaude Sonnet 4.6 7e20062995 feat: Android Auto, radio mode, lyrics overhaul + web lyrics cleanup
Android:
- fix: Android Auto credentials never set on MusicService → artwork/stream null
- feat: Android Auto tabs reduced to Zuletzt gespielt, Favoriten, Playlists
- feat: radio mode (getSimilarSongs2 → getRandomSongs fallback, <5 songs refill)
- fix: lyrics now use Navidrome getLyricsBySongId plugin exclusively
- fix: lyrics scroll rewritten with Column+ScrollState absolute animateScrollTo
- fix: lyrics recomposition lag — extracted LyricsLineItem + key(idx) per line
- chore: remove dead LrcLibClient (direct lrclib bypassed Navidrome plugin)

Web:
- fix: lyrics now use Navidrome getLyricsBySongId only, removed direct lrclib fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 20:30:20 +02:00
HoneyandClaude Sonnet 4.6 8c8f03503b fix: multi-artist click in NowPlaying — individual composables per artist
Replace AnnotatedString/ClickableText (offset-based click was unreliable)
with a horizontally-scrollable Row of individual Text composables. Each
artist has its own explicit clickable modifier bound directly to its ID.

ID de-duplication: if Navidrome copies the primary artistId to all entries
in the artists array, only the first keeps the ID; others render as
non-navigable text (dimmed NsAccent) instead of silently going to wrong page.

Fallback: split song.artist by ';' when artists array is absent/single,
matching the web app's parseArtists() logic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 00:44:59 +02:00
HoneyandClaude Sonnet 4.6 f15c005a3c feat: multi-artist support in NowPlaying (OpenSubsonic artists array)
- Song model: add SongArtist{id,name} + artists: List<SongArtist>
  Navidrome sends this via OpenSubsonic extension — previously ignored
- NowPlaying: if song.artists has >1 entry, render each name as an
  inline AnnotatedString with ClickableText, separated by ' · ';
  every artist navigates to their own page with their real ID
  Falls back to single artist/artistId entry when array is absent
- AppNavigation: onArtistClick now receives artistId: String instead
  of a fixed closure, enabling per-artist navigation from the sheet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 00:37:08 +02:00
HoneyandClaude Sonnet 4.6 0e651e8be3 feat: randomized home greetings (music-themed, EN)
Replace static time-of-day greetings (Guten Morgen/Tag/Abend) with a random
pick from a pool of 10-12 music-themed phrases each time HomeScreen is composed.
With username: "Ready for some beats, Marc" — without: "What's on the playlist today?"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 00:28:44 +02:00
HoneyandClaude Sonnet 4.6 3855207530 feat: artist cards with image in search + fix action bar overflow
Search: replace SmallArtistChip (text pill) with ArtistCard showing
circular profile image (72dp) + name below; works for both Navidrome
artists (uses coverArt) and iTunes artists (uses artworkUrl100 if present)

ArtistDetailScreen: convert action bar Row → LazyRow so Play / Shuffle /
Konzerte pills sit comfortably with horizontal scroll instead of cramming

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 00:24:18 +02:00
HoneyandClaude Sonnet 4.6 13e9ec9d9a feat: Konzerte button on artist page (Android)
- EventModels: ConcertEvent / ConcertVenue / ConcertOffer (Kotlinx Serializable)
- EventsClient: Ktor client fetching backendUrl/api/events/{artist}
- ArtistViewModel: fetches events in parallel with iTunes data on load;
  openEventsSheet / dismissEventsSheet
- ConcertsSheet: ModalBottomSheet with event rows (date, venue, city,
  Tickets → button that opens browser, Ausverkauft state)
- ArtistDetailScreen: Konzerte pill in action bar showing event count,
  tapping opens ConcertsSheet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 00:17:55 +02:00
HoneyandClaude Sonnet 4.6 b5aa484c20 fix: push NowPlaying cover and controls down by 28dp
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 23:35:59 +02:00
HoneyandClaude Sonnet 4.6 42c12cbd3a fix: genre loading spinner + NowPlaying back button priority
GenreViewModel: GenreUiState(genres = it) left isLoading=true (default),
so genres loaded but spinner never cleared. Added isLoading=false explicitly.

AppNavigation: replace BackHandler + navController.enableOnBackPressed with a
DisposableEffect that re-adds an OnBackPressedCallback each time NowPlaying
opens. Since callbacks are LIFO, a freshly-added callback always beats the
BackHandler NavHost registers internally, regardless of composition order.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 22:49:40 +02:00
HoneyandClaude Sonnet 4.6 f01fc71f54 feat: back navigation, NowPlaying fix, home refresh, genre top-6
- ExploreScreen: add BackHandlers for state-based overlays (partialAlbum →
  albumTracks → artistDetail hierarchy) so system back navigates correctly
- AppNavigation: disable NavController's OnBackPressedCallback while NowPlaying
  is open so our BackHandler always intercepts first (fixes tab-switch-then-back bug)
- Home tab: tapping Home while already on Home triggers vm.load() refresh via refreshKey
- HomeViewModel: load all genres sorted by songCount desc (removed take(12) cap)
- HomeScreen: show only top-6 genres + 'Alle anzeigen' button with total count
- New GenreListScreen + GenreViewModel: full scrollable genre list at 'genres' route

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 22:40:48 +02:00
HoneyandClaude Sonnet 4.6 afae5d94df feat: partial album split view + artist page full-screen + Bibliothek iTunes integration
- Partial album detail: tapping incomplete albums now shows downloaded songs
  (normal playback) above and missing iTunes tracks (30s preview + download)
  below — on both Android and web PWA
- Android: ItunesArtistScreen now renders full-screen above the tab/search bar
- Android: ArtistDetailScreen (Bibliothek) now loads iTunes data in parallel —
  adds Weltweit populär section and In Library/Unvollständig/Nicht heruntergeladen
  album sections identical to the Erkunden artist page
- Fix: artist no longer appears in its own Similar Artists row
- Fix: duplicate album entries deduplicated by collectionId in categorizeAlbums

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 20:54:32 +02:00
HoneyandClaude Sonnet 4.6 60e89fe3b6 fix(android): instant cold-start resume + play button icon flicker
WebBridge: skip setMediaItems/prepare when the track is already buffered
from the cold-start pre-warm, so resuming the last song is instant (~0s)
instead of re-buffering from scratch (~10s).

Player: show pause icon during buffering (isPlaying=false, isBuffering=true)
so the button doesn't flash back to "play" between tracks. Also optimistically
set _nativePlaying=true and call setPlayIcon immediately on playCurrent so the
icon updates before the native callback round-trip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:08:56 +02:00
HoneyandClaude Sonnet 4.6 76efa393ea fix(android): keep skip buttons visible in notification
Returning RESULT_ERROR_NOT_SUPPORTED from onPlayerCommandRequest caused
Media3 to mark the skip commands as failed and hide the buttons. Now
returns RESULT_SUCCESS so the notification keeps them enabled, while
ForwardingPlayer no-ops seekToNext/Previous so ExoPlayer never tries to
advance its single-item queue — JS still owns all queue navigation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:06:33 +02:00
HoneyandClaude Sonnet 4.6 308a27d293 refactor: restructure src into player/views/ui subfolders; fix notification skip buttons
- Move player.js + nowplaying.js → src/player/
- Move views.js + spotify/soundcloud/youtube/abs → src/views/
- Move ui.js + components.js → src/ui/
- Update all import paths (static + lazy) across every file
- MusicService: wrap ExoPlayer in ForwardingPlayer that always advertises
  next/prev commands so the notification skip buttons stay visible in radio
  mode even when the JS queue has only one native track

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 13:12:27 +02:00
HoneyandClaude Sonnet 4.6 36a0b38eca feat: server-side romaji overrides (shared list); preload ExoPlayer from SharedPreferences on cold start
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 10:47:25 +02:00
HoneyandClaude Sonnet 4.6 21d55d8ab0 fix: suppress spurious onQueueEnded after reconnect; handle STATE_ENDED on reopen; add isEnded to snapshot
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 01:03:19 +02:00
HoneyandClaude Sonnet 4.6 3252706dc2 fix: replace non-existent MediaSession.Callback skip overrides with onPlayerCommandRequest
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 00:07:14 +02:00
HoneyandClaude Sonnet 4.6 d539a43307 refactor: single-track ExoPlayer model — JS owns queue entirely
Root cause of all Android player bugs: two competing queue managers.
ExoPlayer held all tracks and auto-advanced with its own index while JS
tracked S.qIdx separately. seekToIndex() is async via runOnMain, so
skips would interleave with ExoPlayer's own advances — causing desync,
wrong-song display, and skips that silently did nothing.

Fix: ExoPlayer always holds exactly ONE MediaItem. JS is the single
source of truth for queue order, shuffle, and repeat.

JS changes (player.js):
- playQueue/playCurrent/nextSong/prevSong/toggleShuffle: replace
  setQueue(allTracks, idx) + seekToIndex with setQueue([single], 0)
- _pushQueueIfBridge: send only current track
- nativeInsertAt: no-op (radio songs land in S.queue; JS picks them up)
- fillRadioQueue: remove addToQueue native call
- onTrackChanged: simplified to just schedule scrobble + radio top-up
- onPlaybackStateChanged: remove index-reconciliation block
- onPositionUpdate: remove reconciliation tick (no queue drift possible)
- onQueueEnded: call onEnded() — JS drives what plays next
- toggleRepeat: setRepeatMode('one') only for repeat=one, else 'off'
- Add onNotificationNext / onNotificationPrev to NSPlayer
- Remove _skipping/_skipTimer/_reconTick/_setSkipping (all dead code)

Android changes:
- MusicService: add onSkipToNext/onSkipToPrevious to StateListener;
  add MediaSession.Callback to intercept notification skip buttons and
  delegate them to JS instead of letting ExoPlayer handle them
- MainActivity: implement new StateListener methods → callJs()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 23:57:57 +02:00
HoneyandClaude Sonnet 4.6 20545f71be chore: add Android project files to version control
Adds build config, Gradle wrapper, resources, and gitignore.
Kotlin sources, manifests and app build.gradle were already tracked.
Excludes build output, .gradle cache, .idea, local.properties, and release APK.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 00:34:40 +02:00
HoneyandClaude Sonnet 4.6 d3a1bf6a82 feat: home hero+genres, fix Android queue desync via song ID verification
Home view:
- Add hero card (random pick from recent+frequent combined pool)
- Add genre browser chips (sorted by song count, tap = shuffle-play)

Android sync fix:
- Add currentId to getState() snapshot so JS can verify song identity
- onReady: reset stale localStorage queue when native song ID mismatches
- onTrackChanged: reject stale old-queue callbacks by ID verification
- onPlaybackStateChanged safety net: ID-guard prevents wrong-queue index corruption
- connectToService: call updateStateSnapshot() before onReady so getState() is never empty

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 09:06:47 +02:00
HoneyandClaude Sonnet 4.6 f97e31178c fix: revert MusicService debounce — it was causing the desync, not fixing it
The 120ms debounce on JS-initiated transitions (seek/setQueue) delayed
updateStateSnapshot(), leaving a stale window where getState() returned
the old index. If an AUTO callback fired during that window and the JS
onTrackChanged handler called getState(), it would read the stale index
and overwrite the correct player bar state with the wrong song.

Fix: always fire onTrackChanged immediately so updateStateSnapshot() runs
synchronously. The JS-side getState() reconciliation (from the previous
commit) then reliably gets the settled native index every time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:51:15 +02:00
HoneyandClaude Sonnet 4.6 52079536b6 fix: eliminate notification/playerbar song desync on Android
Two-pronged fix for the state drift between the system notification
(ground truth) and the WebView player bar:

JS (player.js): onTrackChanged now reconciles the incoming index with a
synchronous NSBridge.getState() call before applying it. Stale callbacks
from earlier rapid skips immediately correct themselves to the live
settled index instead of overwriting the correct state.

Kotlin (MusicService.kt): debounce JS-initiated transitions (seek /
setQueue) by 120 ms so rapid skip sequences produce one onTrackChanged
notification at the final index rather than one per skip step. Auto-advance
transitions are still fired immediately since JS doesn't initiate those.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:38:25 +02:00
HoneyandClaude Sonnet 4.6 2d0ee021e6 fix: sync track state before opening NP on notification tap; remove clickable title from player bar
- MainActivity: emit onTrackChanged + openNowPlaying in one JS tick on notification tap
  to prevent race where stale song appeared in NP overlay and bottom bar
- index.html: remove onclick/cursor from player-title so only album/artist are clickable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 22:58:49 +02:00
HoneyandClaude Sonnet 4.6 494361191a fix(android): debounce onReady; pre-warm artwork before playback starts
Song restart: onPageFinished can fire multiple times mid-session (hash/state
changes in WebView). Added 3s debounce so onReady is not re-delivered
spuriously while a song is playing.

Notification artwork: warmArtworkCache was called from onTimelineChanged
where player.currentMediaItemIndex hadn't updated yet (lags until prepare()),
so it warmed the wrong tracks. WebBridge.setQueue now calls
MusicService.publicWarmArtwork(startIndex) immediately after setMediaItems
and before prepare()/play(), giving the bitmap loader a head start with
the correct index.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:27:39 +02:00
HoneyandClaude Sonnet 4.6 ba382aa6d9 fix: cover art caching, skip desync, notification tap opens now playing
- api.js: cache coverUrl() per id+size so WebView HTTP cache can work;
  previously each call generated a new random salt/token → unique URL →
  no cache hits → images re-fetched on every render

- player.js: replace _nsBridge('next')/'prev' with explicit seekToIndex
  in nextSong/prevSong so native always jumps to the exact same index JS
  computed; 'next'/'prev' relied on native's internal pointer being in sync
  which caused 'UI shows new song but audio plays old one' when they drifted

- MusicService: set setSessionActivity PendingIntent on MediaSession so
  tapping the playback notification opens the app

- MainActivity: handle open_now_playing extra in onCreate (cold start)
  and onNewIntent (app already running) to show the Now Playing screen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 22:12:29 +02:00