chore: gitignore chat-summaries and remove from tracking

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-10 20:45:20 -04:00
parent fdecf053db
commit c9b1673e95
13 changed files with 3 additions and 476 deletions

View File

@@ -1,20 +0,0 @@
# Live Listener Polling and Station Hiding
## Task
Implement live SomaFM listener count polling (triggered on "Listeners" chip tap with starred-first progressive sort) and a hide/unhide mechanism for stations.
## Changes Made
### New Files
- **`data/api/SomaFmApi.kt`** — Fetches `channels.json` from SomaFM, parses channel ID to listener count map. Includes `extractStreamId()` to derive the SomaFM channel ID from a station URL.
### Modified Files
- **`Station.kt`** — Added `isHidden: Boolean = false` field.
- **`StationDao.kt`** — Added `getHiddenStationsByPlaylist`, `getHiddenUnsortedStations`, `getHiddenCountByPlaylist`, `getHiddenUnsortedCount`, `toggleHidden`, and `updateListenerCount` queries. Existing `getStationsByPlaylist` and `getUnsortedStations` now filter out hidden stations (`AND isHidden = 0`).
- **`RadioDatabase.kt`** — Bumped to version 3. Added `MIGRATION_2_3` that adds the `isHidden` column.
- **`RadioApplication.kt`** — Registered `MIGRATION_2_3`.
- **`StationListViewModel.kt`** — Added live polling logic (`pollListenerCounts()` coroutine that fetches from SomaFM API, updates starred stations first, then the rest). Added `_showHidden`, `_isPollingListeners` state flows, `hiddenCountFlow`, `toggleHidden()`, `toggleShowHidden()`. `TabInfo.hasListenerData` replaced with `isBuiltIn` flag. Sort mode `LISTENERS_DESC` now groups starred first, then unstarred, each sorted by listener count desc.
- **`StationListScreen.kt`** — "Listeners" chip now shown on all built-in tabs with a `CircularProgressIndicator` while polling. "Show N hidden" / "Show visible" toggle appears when hidden stations exist. Long-press context menu on all stations now includes "Hide" (or "Unhide" in hidden view). Hidden stations render at 50% opacity.
## Follow-up Items
- None identified.

View File

@@ -1,29 +0,0 @@
# Brainstorm & Implementation Plan — Android 24/7 Radio
**Date:** 2026-03-09
## Task Description
Brainstormed and designed a personal-use Android radio streaming app from an IDEA.md spec. Produced a full design document and a 15-task implementation plan.
## Key Decisions
- **Custom raw audio pipeline** (Approach B) over ExoPlayer, for absolute minimum latency (~26ms per MP3 frame vs ExoPlayer's ~1-2s floor)
- **Pipeline:** OkHttp → IcyParser → Mp3FrameSync → MediaCodec → AudioTrack, single-threaded
- **Kotlin + Jetpack Compose + Material 3**, targeting API 28 (Android 9) minimum
- **Room DB** with future-proofed schema (MetadataSnapshot, ListeningSession, ConnectionSpan tables for future recording/clips)
- **PLS/M3U import/export** with `#EXTIMG` support for station default artwork
- **Album art fallback chain:** MusicBrainz → ICY StreamUrl → EXTIMG → Station favicon → Placeholder
- **Dual timers** on Now Playing: session time (never resets) + connection time (resets on reconnect)
- **Latency indicator** estimated from ring buffer + AudioTrack write/play head delta
## Changes Made
- `docs/plans/2026-03-09-android-247-radio-design.md` — Full design document (6 sections)
- `docs/plans/2026-03-09-android-247-radio-implementation.md` — 15-task implementation plan with TDD steps
- Initialized git repository with 2 commits
## Follow-Up Items
- Execute the implementation plan (15 tasks, starting with project scaffolding)
- Execution options: subagent-driven (this session) or parallel session with executing-plans skill

View File

@@ -1,34 +0,0 @@
# Bugfix: Star icons, station switching, and seek-to-live
**Date:** 2026-03-09
## Task description
Fixed three issues found during manual testing of the app.
## Changes made
### 1. Star icon visual state (StationListScreen.kt)
- Added explicit tint colors to star icons: `primary` color when starred, faded `onSurfaceVariant` (40% alpha) when unstarred.
- Applies to both station rows and playlist section headers.
### 2. Station switching race condition (RadioPlaybackService.kt)
- Added `playJob` tracking — each new play request cancels the previous playback coroutine before starting a new one.
- The old job's `finally` block now checks `playJob == coroutineContext[Job]` to avoid calling `cleanup()`/`stopSelf()` when being replaced by a new station.
- Tapping the same station now restarts it (re-fires `ACTION_PLAY`).
- Fixed collector coroutine leak: `return@collect` on terminal events (`Error`, `Stopped`) so the SharedFlow collection terminates.
### 3. Seek-to-live feature
- Added `ACTION_SEEK_LIVE` to `RadioPlaybackService` — ends current connection span, stops/restarts the engine for the current station without creating a new `ListeningSession`.
- Added `seekToLive()` to `RadioController`.
- Added `seekToLive()` to `NowPlayingViewModel`.
- Added "SKIP TO LIVE" `FilledTonalButton` to `NowPlayingScreen`, positioned between latency indicator and Stay Connected toggle. Disabled during reconnection.
## Files changed
- `app/src/main/java/.../ui/screens/stationlist/StationListScreen.kt`
- `app/src/main/java/.../service/RadioPlaybackService.kt`
- `app/src/main/java/.../service/RadioController.kt`
- `app/src/main/java/.../ui/screens/nowplaying/NowPlayingScreen.kt`
- `app/src/main/java/.../ui/screens/nowplaying/NowPlayingViewModel.kt`
## Follow-up items
- None identified.

View File

@@ -1,75 +0,0 @@
# Full Implementation — Android 24/7 Radio
**Date:** 2026-03-09
## Task Description
Brainstormed, designed, planned, and implemented a complete Android 24/7 internet radio streaming app from scratch using subagent-driven development.
## What Was Built
### Audio Engine (custom raw pipeline)
- **StreamConnection** — OkHttp HTTP client with ICY metadata header support
- **IcyParser** — Separates audio bytes from ICY metadata, parses StreamTitle into artist/title
- **Mp3FrameSync** — Finds MP3 frame boundaries with two-frame validation and re-sync
- **AudioEngine** — Wires pipeline: StreamConnection → IcyParser → RingBuffer → Mp3FrameSync → MediaCodec → AudioTrack
### Android Service Layer
- **RadioPlaybackService** — Foreground service with wake lock, wifi lock, MediaSession
- **Stay Connected** — Exponential backoff reconnection (1s→30s cap), ConnectivityManager callback for instant retry
- **NotificationHelper** — Media-style notification with stop action
- **RadioController** — Shared state between service and UI via StateFlow
### Data Layer
- **Room Database** — Station, Playlist, MetadataSnapshot, ListeningSession, ConnectionSpan entities with full DAOs
- **DataStore Preferences** — stayConnected, bufferMs, lastStationId
- **PLS/M3U Import/Export** — Full parsers with #EXTIMG support, round-trip tested
### UI (Jetpack Compose + Material 3)
- **Station List** — Playlists as expandable groups, starring, tap-to-play, long-press menu, import
- **Now Playing** — Album art, dual timers (session + connection), latency indicator, stay connected toggle, buffer slider
- **Settings** — Playback prefs, playlist export, recently played, track history with search
- **MiniPlayer** — Bottom bar on station list when playing
### Metadata
- **AlbumArtResolver** — MusicBrainz/Cover Art Archive → ICY StreamUrl → #EXTIMG → placeholder
- **ArtCache** — In-memory LRU cache (500 entries)
- **Coil 3** — Image loading in Compose
## Commit History (20 commits)
1. Design document and implementation plan
2. Project scaffolding (Gradle, manifest, dependencies)
3. Room entities, DAOs, database, DataStore
4. M3U/PLS import/export with tests
5. ICY metadata parser with tests
6. MP3 frame synchronizer with tests
7. HTTP stream connection with tests
8. Audio engine integration
9. Foreground service with Stay Connected
10. Material 3 theme and navigation
11. Station List screen
12. Now Playing screen with dual timers
13. Settings screen with history
14. Album art resolution with MusicBrainz
15. Final integration and README
## Test Coverage
- IcyParser: 10 tests
- Mp3FrameSync: 9 tests
- StreamConnection: 6 tests
- M3uParser: 6 tests
- PlsParser: 5 tests
- PlaylistExporter: 4 tests
- RingBuffer: 4 tests
- AlbumArtResolver: 9 tests (MockWebServer)
## Follow-Up Items
- Test on actual Android 9 device with real Icecast/Shoutcast streams
- Add drag-to-reorder for stations and playlists
- Implement latency estimation from AudioTrack write/play head positions
- Add Bluetooth headset AUDIO_BECOMING_NOISY handling
- Add audio focus management
- Future: recording, clips, analytics (schema already supports it)

View File

@@ -1,46 +0,0 @@
# Player State Machine Refactor
## Task
Refactored the player state management to eliminate race conditions during rapid station tapping, stop/play transitions, and the NowPlayingScreen bounce-back bug.
## Changes Made
### PlaybackState.kt
- Added `Connecting` state variant to represent the period between user tap and service processing the intent.
### RadioController.kt
- `play()` now sets state to `Connecting` synchronously before sending the intent, eliminating the async gap.
- `stop()` now sets state to `Idle` synchronously before sending the intent.
- `pause()` now sets state to `Paused` synchronously before sending the intent.
- Added redundancy guard: double-tapping a station that's already `Connecting` is a no-op.
### RadioPlaybackService.kt
- Removed the `pauseRequested` volatile flag entirely.
- The `handlePlay()` finally block now checks `controller.state.value` (the single source of truth) instead of the volatile flag to determine cleanup behavior.
- Renamed `cleanup()` to `cleanupResources()` since it no longer sets `Idle` state (that's now the controller's responsibility).
- `handlePause()` and `handleStop()` no longer set `pauseRequested`.
### MainActivity.kt
- Centralized navigation logic via a `LaunchedEffect` that observes `RadioController.state`.
- Navigates to NowPlaying on `Idle -> Active` transitions.
- Navigates back to StationList on `Active -> Idle` transitions while on NowPlaying.
- Station switching (`Playing(A) -> Connecting(B)`) stays on NowPlaying.
### NowPlayingScreen.kt
- Removed the `LaunchedEffect(playbackState)` that called `onBack()` on `Idle` (was the source of the bounce-back bug).
- Added `Connecting` to the `when` branches, showing a loading spinner overlay.
- Disabled pause/skip-ahead buttons while connecting.
### NowPlayingViewModel.kt
- Added `Connecting` handling in session timer (uses `sessionStartedAt`), connection timer (shows 0), and artwork resolver (uses station default artwork).
### StationListScreen.kt
- Removed `onNavigateToNowPlaying()` from `onPlay` callbacks (navigation is now centralized in MainActivity).
- Added `Connecting` to mini player visibility and "now playing" station highlight.
### MiniPlayer.kt
- Added `Connecting` to the destructuring `when` block.
- Shows "Connecting..." subtitle for the `Connecting` state.
## Follow-up Items
- The `stayConnected` and `retryImmediatelyOnNetwork` volatile flags in RadioPlaybackService could be further consolidated into the state machine in a future pass.

View File

@@ -1,26 +0,0 @@
# Tabbed Station Libraries
## Task
Replace the single station list with a tabbed interface where every playlist gets its own tab, SomaFM is a built-in tab with listener-count sorting, and imported playlists become their own tabs automatically.
## Changes Made
### Data Model
- **Station.kt** -- Added `listenerCount: Int = 0` field for sort-by-listeners feature
- **Playlist.kt** -- Added `isBuiltIn: Boolean = false` to protect built-in playlists (SomaFM) from deletion
### Database
- **RadioDatabase.kt** -- Bumped to version 2, added `MIGRATION_1_2` that adds the new columns and seeds SomaFM data for existing installs
- **SomaFmSeedData.kt** -- Added listener counts to all 44 stations, set `isBuiltIn = true` on SomaFM playlist, extracted shared `seedStations()` function used by both `onCreate` and the migration
- **RadioApplication.kt** -- Registered `MIGRATION_1_2`
### UI
- **StationListScreen.kt** -- Complete restructure: `ScrollableTabRow` for tabs, `SortChipRow` with FilterChips (Default, A-Z, Z-A, Listeners), per-tab station list. Built-in tabs hide add/import/delete actions. Listener count shown inline on SomaFM stations. Long-press edit/delete disabled on built-in tab stations.
- **StationListViewModel.kt** -- New state model with `TabInfo`, `SortMode`, tab selection, and in-memory sorting. Import now creates a new playlist (tab) from the file name and assigns all parsed stations to it, then switches to the new tab.
- **AddStationDialog.kt** -- Simplified: removed playlist picker (stations go to current tab's playlist automatically)
- **EditStationDialog.kt** -- Simplified: removed playlist picker
## Follow-up Items
- Drag-and-drop reordering within tabs
- Tab reordering
- Renaming tabs/playlists

View File

@@ -1,38 +0,0 @@
# UI polish: navigation, pause, artwork, scrollability
**Date:** 2026-03-09
## Task description
Five UI/UX improvements after manual testing.
## Changes made
### 1. Auto-navigate to Now Playing on station tap (StationListScreen.kt)
- `onPlay` callback now calls `onNavigateToNowPlaying()` immediately after `viewModel.playStation()`.
### 2. Auto-navigate back to station list on stop (NowPlayingScreen.kt)
- `LaunchedEffect(playbackState)` watches for `PlaybackState.Idle` and calls `onBack()`.
- Removed the "Nothing playing" dead-end screen.
### 3. Pause/resume support
- **PlaybackState.kt**: Added `Paused(station, metadata, sessionStartedAt)` state.
- **RadioController.kt**: Added `pause()` method sending `ACTION_PAUSE`.
- **RadioPlaybackService.kt**: `handlePause()` sets `pauseRequested = true` and stops engine. In `handlePlay`'s finally block, if paused: updates state to `Paused`, keeps listening session alive, doesn't cleanup service. On resume (ACTION_PLAY with same station while paused), reuses the session.
- **NowPlayingViewModel.kt**: Added `pause()` and `resume()`. Session timer keeps ticking during pause.
- **NowPlayingScreen.kt**: Transport controls adapt to state — paused shows large play/resume button + stop; playing shows skip-ahead + pause + stop.
### 4. Artwork in mini player and station list
- **MiniPlayer.kt**: Shows 40dp rounded artwork thumbnail from ICY StreamUrl or station's `defaultArtworkUrl`. Shows "Paused"/"Reconnecting..." subtitle as appropriate.
- **StationListScreen.kt**: Station rows show a 36dp circular artwork thumbnail from `defaultArtworkUrl` (EXTIMG) when available.
### 5. Scrollability / drag-n-drop readiness
- LazyColumn was already scrollable. Added `padding(vertical = 4.dp)` to station rows for better touch targets. All items have stable keys (`station.id`, `"playlist_header_${playlist.id}"`) ready for reorderable integration.
## Files changed
- `app/src/main/java/.../service/PlaybackState.kt`
- `app/src/main/java/.../service/RadioController.kt`
- `app/src/main/java/.../service/RadioPlaybackService.kt`
- `app/src/main/java/.../ui/screens/nowplaying/NowPlayingScreen.kt`
- `app/src/main/java/.../ui/screens/nowplaying/NowPlayingViewModel.kt`
- `app/src/main/java/.../ui/screens/stationlist/StationListScreen.kt`
- `app/src/main/java/.../ui/components/MiniPlayer.kt`

View File

@@ -1,26 +0,0 @@
# Dominant Color Extraction for Now Playing Metadata Background
## Task
Detect the dominant color of the album artwork and use it as the background color for the metadata section of the Now Playing screen. Text colors are automatically inverted for contrast.
## Changes Made
### New dependency
- **`gradle/libs.versions.toml`**: Added `androidx.palette:palette-ktx:1.0.0`
- **`app/build.gradle.kts`**: Added `implementation(libs.palette)`
### `RadioApplication.kt`
- Added a shared `ImageLoader` instance (Coil 3) for use in palette extraction.
### `NowPlayingScreen.kt`
- **Palette extraction**: Uses `LaunchedEffect(artworkUrl)` to load a 64x64 thumbnail via Coil's `ImageLoader`, then runs `Palette.from(bitmap).generate()` on `Dispatchers.IO`.
- **`DominantColors` data class**: Holds `background`, `onBackground`, and `accent` colors extracted from the artwork.
- **Contrast logic**: `contrastingTextColor()` checks luminance and picks black or white text. Accent color is picked from light/dark vibrant or muted swatches depending on background brightness.
- **All child composables** (`TrackInfoSection`, `TimerSection`, `TransportControls`, `SettingsSection`) now accept explicit color parameters instead of reading from `MaterialTheme.colorScheme`.
- **Metadata panel backgrounds**: Both `PortraitContent` (bottom half) and `LandscapeContent` (right half) apply `Modifier.background(bgColor)` using the extracted dominant color.
- **Slider and Switch colors**: Themed to match the extracted accent color.
- **Landscape TopAppBar**: Also uses the dominant background color for consistency.
## Follow-up Items
- Consider animating the color transition when artwork changes (e.g., `animateColorAsState`).
- May want to add a subtle gradient at the seam between artwork panel and metadata panel.

View File

@@ -1,44 +0,0 @@
# Now Playing UI Overhaul — Dominant Color, Typography, Modern Controls
## Task
Address four issues: (1) swap station name / artist-title text hierarchy, (2) fix dominant color extraction not applying, (3) invert back button in portrait mode, (4) make the UI feel cohesive and modern to match the blur aesthetic.
## Changes Made
### `NowPlayingScreen.kt` — full rewrite
**Typography swap**
- Station name → `labelLarge`, uppercase, 3sp letter-spacing, 50% alpha (diminished label)
- Artist/title → `headlineLarge`, bold (hero text)
- "No track info" replaced with `♪` music note when no metadata
- Artist-title separator changed from `-` to `—` (em dash)
**Palette extraction fix**
- Replaced Coil 3 `ImageLoader.execute()` + `toBitmap()` (which was silently failing) with direct `OkHttp` + `BitmapFactory.decodeStream()` for reliable bitmap creation
- Palette swatch fallback chain: dominant → vibrant → muted
- Accent color picks light variants on dark backgrounds, dark variants on light
- Added `animateColorAsState` with 600ms tween for smooth color transitions when artwork/tracks change
- Bitmap properly recycled after palette extraction
**Portrait back button**
- Added gradient scrim (45% black → transparent, 100dp tall) at top of artwork panel
- Back icon uses `Color.White` for visibility over any artwork
**Modern UI refresh**
- Transport controls: hero button (play/pause) is a filled accent circle (80dp) with contrasting icon; secondary buttons (skip, stop) are smaller plain icons
- Settings section wrapped in a translucent `Surface` card with 16dp rounded corners
- Timer section condensed to a single compact line with `·` separators, `bodyMedium` weight `Light`
- Settings labels use `labelLarge`/`labelMedium` for clean hierarchy
- Switch and slider colors fully themed to accent/text palette
- Increased breathing room between sections (2028dp spacers)
- Stop button toned down to 50% alpha (not a bright red error color)
### `RadioApplication.kt`
- Removed unused `ImageLoader` (palette now uses OkHttp directly)
### `gradle/libs.versions.toml` / `app/build.gradle.kts`
- `palette-ktx` dependency added (previous commit, retained)
## Follow-up Items
- Consider adding a gradient seam between artwork panel and metadata panel for smoother visual transition
- May want to add haptic feedback on transport button taps

View File

@@ -1,38 +0,0 @@
# Full-Screen Blurred Background, Larger Controls, Dynamic System Bars
## Task
1. Larger transport buttons with more spacing
2. Narrower settings widget with larger text
3. Full-screen blurred album art background (replacing split artwork/metadata layout)
4. Dynamic status bar and navigation bar coloring
## Changes Made
### `NowPlayingScreen.kt`
**Full-screen blurred background (#3)**
- New `BlurredBackground` composable renders behind all content — loads artwork at 10x10 resolution via Coil, applies `cloudy(radius = 25)`, then overlays a 15% black dim
- Falls back to `bgColor` (palette-extracted dominant color) when no artwork
- Removed `ArtworkPanel` (which had its own blur + sharp art). Replaced with `ArtworkImage` (sharp art only) layered on top of the shared blurred background
- Portrait: top half shows sharp artwork, bottom half shows metadata/controls — both over the shared blur
- Landscape: left half shows sharp artwork, right half shows metadata/controls — both over the shared blur
- Metadata section no longer has its own `background(bgColor)` since the blur is now the background
**Larger transport buttons + more spacing (#1)**
- Hero button (play/pause): 80dp → 96dp, icon 48dp → 56dp
- Secondary buttons (skip, stop): 56dp → 64dp, icon 30dp → 36dp
- Spacing between buttons: 16dp → 28dp
**Narrower settings widget + larger text (#2)**
- Settings card width capped at `widthIn(max = 320.dp)` instead of `fillMaxWidth()`
- Label text style: `labelLarge``bodyLarge` (portrait), `labelMedium``bodyMedium` (landscape)
**Dynamic system bars (#4)**
- `DisposableEffect` saves original status/nav bar colors and restores them on exit
- `SideEffect` applies the animated `bgColor` (at 85% opacity) to both `window.statusBarColor` and `window.navigationBarColor`
- `WindowInsetsControllerCompat` flips light/dark icon appearance based on background luminance
- Works on API 28+ via the deprecated-but-functional Window color APIs
## Follow-up Items
- The 15% dim on the blurred background is configurable — user may want to adjust
- Could add edge-to-edge mode if targeting API 35+

View File

@@ -1,33 +0,0 @@
# Settings Panel Implementation
## Task
Added four new feature sections to the Settings screen: SomaFM quality preference, now-playing history file logging, reset customizations, and restart app. Also fixed a crash-on-launch bug on physical devices caused by missing `isHidden` column in seed data INSERT.
## Changes Made
### Bug Fix
- `SomaFmSeedData.kt`: Added `isHidden` column to the station INSERT for fresh installs (v4+ schema) to fix `SQLiteConstraintException` crash on real devices.
### New Files
- `data/model/StationStream.kt` — Room entity for multi-stream quality options per station
- `data/db/StationStreamDao.kt` — DAO for station stream queries
- `service/StreamResolver.kt` — Resolves ordered list of stream URLs based on quality preference (per-station override or global)
- `data/logging/NowPlayingHistoryWriter.kt` — Appends track changes to a file in CSV, JSON lines, or plain text format
### Modified Files
- `data/model/Station.kt` — Added `qualityOverride: String?` column
- `data/db/RadioDatabase.kt` — Added `StationStream` entity, `StationStreamDao`, bumped to v4, added `MIGRATION_3_4`
- `data/db/SomaFmSeedData.kt` — Seeds `station_streams` table (2-4 rows per station: SSL/non-SSL x 128/256kbps), added `includeStreams` parameter, `seedStreamsForExistingStations()` for migration
- `data/prefs/RadioPreferences.kt` — Added `qualityPreference`, `historyEnabled`, `historyFormat`, `historyDirUri` keys + setters
- `RadioApplication.kt` — Exposed `streamResolver` and `historyWriter`, registered `MIGRATION_3_4`
- `service/RadioPlaybackService.kt` — Uses `StreamResolver` for URL resolution, calls `historyWriter.append()` after metadata persist
- `ui/screens/settings/SettingsScreen.kt` — Added SomaFM Quality (reorderable list), Now Playing History (toggle/format/dir picker), Reset (with optional station deletion), Restart App sections
- `ui/screens/settings/SettingsViewModel.kt` — Added quality/history preference management, `resetCustomizations()` (DB transaction), `restartApp()` (graceful then forced)
### Design Doc
- `docs/plans/2026-03-10-settings-panel-design.md`
## Follow-up Items
- Per-station quality override UI (accessible from station edit dialog or long-press menu)
- Stream fallback chain retry (currently uses first resolved URL; could try subsequent URLs on connection failure)
- History file rotation/cleanup for large files

View File

@@ -1,67 +0,0 @@
# Now Playing UX Improvements
**Date:** 2026-03-10
## Task Description
Five UX enhancements to the Now Playing screen in the Android 247 Radio app.
## Changes Made
### 1. Bouncing Marquee Text (`NowPlayingScreen.kt`)
- Created `BounceMarqueeText` composable that measures text width vs container
- If text fits: displays centered, stationary (single line)
- If text overflows: animates horizontal translation with a bounce pattern (scroll to end, pause 1.5s, scroll back, pause 1.5s, repeat)
- Scroll speed scales proportionally with overflow distance (18ms per dp)
- Uses `rememberTextMeasurer` for unconstrained width measurement, `rememberInfiniteTransition` with `keyframes` for bounce animation
- Both stroke and fill text layers share the same animated offset
- Replaced multi-line wrapping in `TrackInfoSection` with single-line marquee
### 2. Timer/Latency Icons (`NowPlayingScreen.kt`)
- Replaced the concatenated text string in `TimerSection` with a `Row` of icon+value pairs
- Session elapsed: `Icons.Outlined.Schedule` icon
- Connection elapsed: `Icons.Outlined.Wifi` icon
- Latency: `Icons.Outlined.Speed` icon
- Icons sized at 14dp, tinted with same dimmed color as text
### 3. Cross-Fade Metadata and Artwork (`NowPlayingViewModel.kt`, `NowPlayingScreen.kt`)
- Added `displayMetadata` and `displayArtworkUrl` StateFlows to ViewModel
- These are "committed" values that only update after artwork resolution completes
- When metadata changes, artwork resolve runs in background while old metadata+art remains displayed
- Once resolve completes, both metadata and artwork URL update atomically
- Station changes trigger immediate display reset
- Added `AnimatedContent` with 600ms fade transition around `ArtworkImage`
- Screen now reads `displayMetadata` for track info instead of raw `playbackState.metadata`
### 4. Stream Quality Indicator (5 files)
- `StreamConnection.kt`: Added `StreamInfo` data class (bitrate, ssl, contentType); parsed from ICY response headers (`icy-br`, `Content-Type`, URL scheme)
- `AudioEngineEvent.kt`: Added `StreamInfoReceived` event
- `AudioEngine.kt`: Emits `StreamInfoReceived` after connection opens
- `PlaybackState.kt`: Added `streamInfo: StreamInfo?` to `Playing` and `Paused` states
- `RadioPlaybackService.kt`: Handles `StreamInfoReceived` event, updates `Playing` state
- `RadioController.kt`: Carries `streamInfo` to `Paused` state on pause
- `NowPlayingScreen.kt`: Added `QualityBadge` composable showing bitrate, codec (MP3/AAC/OGG/FLAC), and SSL lock icon
### 5. Dominant Color White Handling (`NowPlayingScreen.kt`)
- Added `.clearFilters()` to `Palette.Builder` chain before `.generate()`
- Removes the default Palette filter that excludes near-white and near-black colors
- White-dominant album art now correctly produces a white background with dark text
## Files Modified
- `app/src/main/java/xyz/cottongin/radio247/ui/screens/nowplaying/NowPlayingScreen.kt`
- `app/src/main/java/xyz/cottongin/radio247/ui/screens/nowplaying/NowPlayingViewModel.kt`
- `app/src/main/java/xyz/cottongin/radio247/audio/StreamConnection.kt`
- `app/src/main/java/xyz/cottongin/radio247/audio/AudioEngine.kt`
- `app/src/main/java/xyz/cottongin/radio247/audio/AudioEngineEvent.kt`
- `app/src/main/java/xyz/cottongin/radio247/service/PlaybackState.kt`
- `app/src/main/java/xyz/cottongin/radio247/service/RadioPlaybackService.kt`
- `app/src/main/java/xyz/cottongin/radio247/service/RadioController.kt`
## Verification
- Build: `./gradlew compileDebugKotlin` -- exit 0 (only pre-existing deprecation warnings)
- Tests: `./gradlew testDebugUnitTest` -- 56/56 pass, 0 failures
## Follow-up Items
- Test on device: marquee animation with various track name lengths
- Test on device: cross-fade timing with slow MusicBrainz lookups
- Test on device: verify ICY headers (`icy-br`) are present for non-SomaFM stations