Wavlake rankings 400 on sub-genres (Trance, Classic Rock, Metal...). Discover
chips now include 15 sub-genres; tapping one routes through refreshRankings ->
browseRadioGenre, filling a new browseStations StateFlow from radio-browser
tag search (most-listened first) rendered by RadioGenreList. Wavlake-indexed
genres keep the rankings path.
Genre matching is now space-insensitive (user types 'oldschool' for 'Old
School Hip Hop'). radio-browser tag match is case-SENSITIVE against lowercase
stored tags — byTag() lowercases or every sub-genre browse returns zero hits.
The launcher icon was a flat square PNG with opaque black corners, so it
rendered with sharp edges. Extracted the artwork onto a proper adaptive
icon (solid #111 background layer, artwork centered in the safe zone,
monochrome layer for themed icons) and re-cut the legacy mipmaps with
rounded corners for pre-26 launchers.
- IcyStreamReader: requestedStop flag — intentional stops never report as errors
- PlaybackManager.startIcyReader: always stop the previous reader first; all four listener callbacks ignore events from a replaced reader (ghost audio was also poisoning the rolling buffer)
Directory search now merges name and tag(genre) hits so genre queries
find tagged stations. Tabs jump instantly (adjacent glide, swipe kept);
startup pre-builds disk-backed singletons off main thread; equalizer
animation runs only on the playing indicator; row placement animations;
missing list key. Debug-only StrictMode guard for main-thread I/O.
Release signed with debug key for direct installs.
- Equalizer chip dims while BUFFERING/INITIALIZING (no false 'playing'
signal during reconnect)
- On ERROR the bar's subtitle becomes 'Couldn't reach · Retry' —
tapping replays the current station without leaving the tab
- NowPlayingViewModel.retry() re-plays current URL
- verticalDragToDismiss: bar follows the finger downward
(translationY, capped 120dp) and dismisses past 40% height or on a
downward fling; springs back otherwise; static under Reduce Motion
- Dismissal is per-source: playback continues, and the bar returns
automatically when a different station/song starts
- Tabs route now owns its mini-player (root no longer stacks one)
- On-device: swipe dismissed bar while PLAYING; station change
re-showed it
- pressDip modifier (scale 0.97 press / spring-back, transform-only,
reduce-motion aware) sharing the row's interaction source with the
Material ripple
- Library song rows: ripple + dip on tap; verified tap still plays
- Page-settle haptic tick in the tab pager (skipped on first frame)
- New TabsScreen: HorizontalPager (Stations · Library · Streams ·
Playlists) synced with a shared bottom NavigationBar; swipes follow
the finger, tab taps animate to page, neighbors pre-composed so
pages and their ViewModels survive page changes
- Light haptic tick when a new page settles (skipped on first frame)
- Mini-player sits above the nav bar inside the pager surface:
playback controls reachable from every tab
- Hub tab buttons navigate to tabs?index=N; playlist selection from
the Playlists page animates to Library and deep-links the playlist
- All previous routes preserved (stations/library/streams/playlists
still resolve); detail transitions now direction-aware via
Motion.kt forward/backward params
- EqualizerBars: animated 3-bar 'now playing' indicator (out-of-phase
pulses; frozen mid-height when paused; static under Reduce Motion)
- Now-playing bar: two-line layout with live metadata (title + artist
for streams and local songs) and rise transitions; equalizer glyph
replaces the icon chip while playing
- Playing state in every list: Stations card, Streams card, and
Library song rows show the animated equalizer instead of a static
icon on the active source
- NowPlayingViewModel exposes stream/local metadata flows
Root cause: import deduped by artist+title, but save-from-air did not,
so saving the same broadcast again wrote 'Song (1).mp3' + a new row.
- SaveSongUseCase now refuses a save when artist+title already exists
(case-insensitive), reporting 'Already in your library'
- DedupeLibrary collapses existing duplicates: keeps the largest file
(ties → oldest), re-points playlist memberships to the keeper, then
deletes duplicate rows + files
- Runs automatically when the Library opens; a snackbar reports the
removed count
- On-device: user library 89 → 54 songs, 0 remaining dup groups,
playlist links intact (Zaza All 32, Apollo 19), zero orphans, all
kept files present on disk (pre-run backup taken)
- Sleep timer: 15/30/45/60/90 min, live countdown chip, pauses
playback on expiry (station stays loaded); cancel from dialog
- Playback speed 0.5x-2x for stations and saved songs; persists
across track changes; chip shown when != 1x
- Local playback now uses an ExoPlayer media queue (library list from
tapped song, shuffle option): native advance at track end, transport
next/prev walk the queue, repeat off/all/one
- Player transport routes queue-first: local playback advances the
queue, radio cycles stations; repeat/shuffle dimmed for radio
- Library 'now playing' highlight follows queue transitions via
currentLocalTrackIdFlow; dead manual shuffle-index code removed
- Verified on-device: speed 1.25x confirmed in media_session, sleep
dialog + countdown, save-from-air picker, radio option states
- radio-browser.info directory search (debounced, ~58k stations) with
codec/bitrate/country metadata and save-to-stations action
- station favorites (star) persisted in StationRepository; Starred
section pinned in Stations; active-station row highlight
- motion system (Motion.kt): shared-axis nav transitions, hub hop,
metadata rise, play/pause pop; LocalReduceMotion honors system
'remove animations'
- AppHomeButton (launcher icon) top-left on every surface + SatsAmp
wordmark on the hub; home pops the stack back to the player
- transport row: add missing Next button, conventional order
stop · prev · play · next
- WaveGraphic center glow: wider, fainter, no perceptible disc edge
- StreamMetadata: strip unsubstituted {{template}} residue
- PRODUCT.md + DESIGN.md (impeccable design context); .gitignore for
re-downloadable skill engine binaries