Shuffle/repeat buttons showed disabled after starting playback because
_refreshQueueAfterDelay only updated queue items without syncing the
shuffle/repeat state. Now syncs both flags and refreshes playback state.
Artist images failed in AA because _autoBuildArtistList used the basic
getImageUrl which returns null for artists without MA metadata images.
Now uses getArtistImageUrlWithFallback which tries external sources.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace boolean toggle with continuous ValueNotifier<double> so bars
slide 1:1 with finger movement instead of snapping. Use
ValueListenableBuilder to avoid full-screen rebuilds on every scroll
frame, and Align(heightFactor) for uniform top-clip instead of
SizedBox squishing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The local _shuffleOn and _repeatMode flags were initialized to off,
so AA buttons always showed disabled on connect even when shuffle/repeat
were actually enabled.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Make onPlay/onPause callbacks non-toggle (resumePlayer/pausePlayer
instead of playPauseSelectedPlayer) so AA buttons work correctly
- Fix interruption guard to treat builtin-active as "was playing" for
PCM/Sendspin mode where playbackState can be stale
- Keep media session alive during transient idle when PCM is playing
- Only cache non-empty artist album results to prevent stale 0 counts
- Only pause builtin player on AA disconnect (not remote speakers)
- Auto-select builtin player on AA connect broadcast
Fixes#25
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Position tracker, queue persistence, notification timer, and message
receipt logs fired every few seconds, pushing out meaningful events
(AA lifecycle, playback state changes) within minutes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Without HitTestBehavior.opaque, the GestureDetector only registered
hits on the 20px painted track area instead of the full 48px touch target.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
List builders for artists, albums, and playlists now await
SyncService.loadFromCache() when data is empty (matching the existing
audiobook pattern). getArtistAlbumsWithCache also falls back to
SyncService when _albums is empty. After library loads (both from cache
and background sync), AA browse children are invalidated so stale
empty results get replaced.
Fixes#25
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Don't cache empty artist album results when the library hasn't loaded
yet. On cold start, SyncService may not have finished populating the
album list, causing getArtistAlbumsWithCache to return and cache 0
results permanently until the cache expires.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AA caches the browse tree after first session, so getChildren for the
browsable root is never called on subsequent connects. This meant
_isAndroidAutoConnected was never set, breaking both disconnect detection
(display listener ignored) and playback button refresh.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Android Auto on projection-mode devices (com.google.android.projection.gearhead)
doesn't trigger ACTION_EXIT_CAR_MODE, so the disconnect was never detected.
- Add DisplayManager.DisplayListener to detect AA disconnect when virtual
displays are removed (projection mode)
- Add bidirectional method channel so Dart notifies native of AA connection
- Add onAADisconnected callback that force-pauses (not toggles) the player
- Suppress Sendspin stream/start for 2s after disconnect to handle the race
where the server sends audio before processing our pause command
- Guard interruption begin handler to only pause/toggle when actually playing,
preventing the interrupted-while-paused -> accidental resume bug
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Detect Android Auto via car mode broadcasts and getChildren; hide
switch player button from controls while AA is connected
- Suppress audio auto-resume after becoming noisy / AA disconnect to
prevent music playing from phone speaker when car turns off
- Rename "Smart Shuffle" to "Start Radio" with radio icon
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Radio calls now fall back gracefully when the provider doesn't support radio:
playRadio falls back to playing the single track, playArtistRadio falls back
to shuffled artist tracks via search. Direct API calls in context menu and
library screen routed through provider to pick up the fallback. Android Auto
gets a "Smart Shuffle" item at the top of track lists (albums, playlists,
favorites) with a custom sparkle-shuffle icon that attempts radio then falls
back to shuffled playback. Also fixes builtin player selection to avoid
premature fallback before Sendspin registration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add _trackDisplayArtist() helper to show podcast name, audiobook author,
or radio station instead of "Unknown Artist" in all notification/AA contexts
- Auto-restore audiobook context from cached data when URI matches
- Set podcast name context when playing episodes from AA browse tree
- Encode podcast ID in episode media IDs for context lookup
- Fix Sendspin pause position using server-synced position tracker instead
of PCM elapsed time (which resets to 0 each stream)
- Fix Sendspin resume position to use position tracker instead of Duration.zero
- Add 10s seek-back on audiobook resume to compensate for Sendspin buffering
- Populate AA queue for podcast and radio playback via _refreshQueueAfterDelay
- Show podcast name as artist in AA episode browse list
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use updateLocalModeNotification instead of setRemotePlaybackState for
Sendspin/builtin players to avoid conflicting mediaItem.add() calls that
cause artwork flickering. Add fallback to builtin player when last
selected player is unavailable. Extract shared _controls getter, use
MediaControl.custom for switch player, and only claim audio focus on
play transitions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add ArtworkContentProvider to serve artwork via content:// URIs
(fixes broken images on many AA head units that reject HTTP URLs)
- Wrap all artUri sites in handler and provider with content:// URIs
- Populate AA play queue after playTracks for queue view support
- Wire updateQueueIndex from provider's _updatePlayerState
- Add error state communication to AA on playback failures
- Rewrite search() to return grouped results (artists, albums, tracks)
- Add playFromSearch() for voice command support ("Play X on Ensemble")
- Add search race condition guard
- Declare SEARCH_SUPPORTED in browsable root extras for search button
- Add invalidateAutoChildren for library add/delete events
- Add cache size limit (50 entries) with LRU eviction for track cache
- Clean up track cache and children subjects in dispose()
- Add audiobook completion status extras (progress bars in AA)
- Add subtitle link extras on track items (tappable artist)
- Reset idle timer on AA browse activity
- Fix PCM player auto-recovery triggering on user-initiated pause
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Make checkAndReconnect idempotent: concurrent callers now join the
existing reconnect attempt instead of each creating a new WebSocket
connection. This prevents the cascading "API disposed" errors seen
when Android Auto browses multiple categories simultaneously.
Activate audio session when Sendspin/remote playback starts so Android
delivers audio focus interruption events. Ensemble now pauses when
another app (Spotify, YouTube, etc.) takes audio focus, and resumes
when focus returns.
Remove switch-player button from Android Auto controls (not useful
in car context).
Fixes#25
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Only show the bottom fade gradient when 4+ rows are enabled, since
with ≤3 rows the content doesn't scroll behind the mini player. Reduce
gradient height to mini player height only.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix imageproxy URLs using wrong port: Dart's Uri.hasPort returns false
for default ports (80/443) even when explicitly specified, causing
fallback to port 8095. Use raw string check instead.
Fix player background flash: use theme surface color as fallback instead
of hardcoded black, and add race guard to color extraction to prevent
stale results on rapid track changes.
Fixes#25, fixes#73
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Downgrade high-frequency polling logs to debug level (PositionTracker,
Sendspin state, queue metadata, received messages) to prevent the
1000-entry log cap from rotating out useful entries. Add logging when
AA provider is not set or when it becomes available.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace pcmAudioPlayer.setVolumeGain() with FlutterVolumeController.setVolume() for system volume control
- Add notifyListeners() after local volume updates so volume slider stays in sync
- Sync localPlayerVolume from player_updated events to keep UI accurate
- Fix volume_control.dart to read maLocalVolume from provider instead of _systemVolume
- Change supported_roles to 'player@v1' in Sendspin handshake
- Remove track background sync from SyncService (tracks load on-demand)
- Simplify getAlbumTracks/getPlaylistTracks error handling (rethrow instead of swallow)
- Pass album to getAlbumTracks in AlbumDetailsScreen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MA→Ensemble: supported_roles must be ['player'] not ['player@v1'], and the
key must be 'player@v1_support' not 'player_support'. The server checks for
the literal string "player" in supported_roles to initialise PlayerClient -
without it, self.api.player is always None and volume commands are silently
dropped.
Hardware buttons: FlutterVolumeController listener was updating the UI but
never calling updateLocalPlayerVolume, so hardware volume presses never
reached MA.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MA sends volume via {"type":"server/command","payload":{"player":{"command":"volume","volume":N}}}
but we were handling a dead "volume" case with a different format that MA never sends.
Also: updateLocalPlayerVolume now calls reportState immediately instead of waiting for
the 5-second periodic timer, so volume changes in Ensemble reach MA without delay.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs fixed:
1. MA→Ensemble: Volume commands from MA (HA automations, MA web UI) were
applied as system media volume via FlutterVolumeController. This is
unreliable on some devices and incorrect — all other Sendspin clients
(Snapcast, MassDroid, HA Voice PE) apply volume as software gain on the
PCM audio stream. Add setVolumeGain() to PcmAudioPlayer and scale Int16
samples before feeding to flutter_pcm_sound instead.
2. Ensemble→MA: When the user moved the volume slider for the local player,
_localPlayerVolume was never updated. Every 5 seconds the periodic state
report would send the stale value to MA, keeping MA's Sendspin player
volume permanently out of sync. Now VolumeControl calls
updateLocalPlayerVolume() on all local-player volume changes so the next
report reflects the actual level.
Fixes: CollotsSpot/Ensemble#70
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author images:
- Fetch from all 4 sources in parallel (was sequential) — ~5s vs 30s worst case
- Validate all image URLs with HEAD request before accepting
- Deduplicate concurrent requests for same author via Completer map
- Persist URLs to SharedPreferences for instant cold start
- Increase LRU cache from 100 to 500 entries
Series cover mosaic:
- Cache built cover widget to prevent rebuilds from unrelated setState
- Match hero widget tree structure (RepaintBoundary > AspectRatio > ClipRRect)
- Keep initialCovers stable — don't overwrite from loaded books
- Add ValueKey and memCacheWidth/Height to cover CachedNetworkImages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add _audiobooks in-memory list to provider (bulk loaded like albums)
- Add series audiobook caching for instant revisit
- Fix audiobook cover not loading in detail screen (use _audiobook getter)
- Defer data loading 350ms on audiobook/podcast/playlist detail screens
- Fix artist detail not showing provider albums after background fetch
- Add audiobook case to _removeFromLocalLibrary and _scheduleLibraryRefresh
- Delay color extraction and add memCacheHeight on series hero images
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Guard _updatePlayerState() polling against disconnected state to prevent
"Not authenticated" log spam. Cache last-known track per player so it
survives idle-out and can be re-queued when user taps play.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cache album/playlist tracks in a new DetailTrackCache DB table (schema v7)
so detail screens load instantly on revisit. Prefetch album artwork after
sync via ImagePrefetchService with batched downloads and LRU eviction.
Expand sync_service and cache_service to support the new caching layers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keep bottom nav and mini player visible when cached players exist so
the UI doesn't flash during reconnection. Also refine the home screen
bottom fade gradient with intermediate stops to avoid a hard dark line.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a gradient overlay above the mini player to smoothly mask content
scrolling behind it, and increases each row height by 2px to push the
4th row title fully behind the mini player.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Re-tapping the search nav tab now scrolls to top and focuses the search
field (#67). Keyboard dismisses on scroll (onDrag), tap, or long-press.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Context menu and options menu now account for mini player/nav bar insets
so they don't get clipped behind the player overlay. Detail screens use
different bottom spacing since the nav bar is hidden. Home screen row
height calculation simplified by removing the caching/ratchet mechanism.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Simplify blur backdrop to static filter, defer color extraction until
after reveal animation, and use Selector to prevent unnecessary rebuilds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace UniqueKey-based row refresh with ValueNotifier signal so rows
reload data in-place without being destroyed/recreated. Only invalidate
home cache when connecting to a different server (not on routine
reconnects after backgrounding). Cache row height to prevent size jumps
caused by the bottom nav bar toggling on connect/disconnect.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Discovery rows now behave like regular rows - always kept in _homeRowOrder
with visibility controlled by _isRowEnabled(), eliminating flash for non-Tidal
users. Tab-switch from settings only reloads local preferences (no API calls).
Settings scroll position preserved via ScrollController.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Stop volume observer and key interception on app pause so hardware
buttons only control the foreground app (fixes volume affecting MA
player while in YouTube etc.)
- Restore volume interception on app resume
- Animate blur backdrop fade in (250ms) and out (120ms) for player
reveal instead of instant toggle
- Start blur fade-out immediately on dismiss, in parallel with card
slide animation
- Extend mini player gesture hit area 20px above for easier swipe-down,
especially on detail screens where player sits closer to screen edge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add gradient fade overlay to all 8 detail screens so content fades
smoothly behind the mini player instead of cutting off abruptly
- Fix player reveal gap on detail screens by accounting for hidden nav bar
in miniPlayerBottom calculation
- Stabilize home row height by using constant margin calculation (prevents
subtle row size jitter when app resumes and row count fluctuates)
- Remove settings reload on app resume; only reload when switching from
Settings tab back to Home tab
- Skip discovery folders API call if already loaded (just re-sync row order)
- Only invalidate home cache after real disconnects, not routine reconnects
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The discovery rows feature (commits efb84a4..0263c75) was inspired by
and based on work from PR #64 by relic664.
Co-Authored-By: relic664 <relic664@pm.me>
- Fix home screen row height formula for extendBody:true (use 106px
mini player space instead of 162px which double-counts nav bar)
- Fix 4+ row scroll padding to use mini player space
- Wrap Artists/Albums tabs with Consumer to fix context.select assertion
- Force home cache refresh after auth to fix race condition where
AlbumRow fires before credentials are ready
- Add .catchError() to individual futures in getRecentAlbums batch
- Fix settings toggle to add enabled rows to homeRowOrder
- Animate mini player position on detail screen nav bar transitions
- Prevent discovery loading placeholder for users without Tidal
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reduce gaps in album details and fix playlist track list padding for better alignment of track numbers, artwork, and durations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This PR represents a testing and debugging cycle following v3.0.2. Initial work on sync group volume management uncovered multiple UI bugs that required fixes. The final result addresses:
1. **Fix: Group Player Volume Management** - Two distinct volume control issues for Music Assistant sync groups
2. **Feature: Hardware Volume Button Integration** - Phone volume buttons now control Music Assistant player volume from lockscreen / shade widget / app minimised
3. **UI Bug Fixes** - Five UI issues discovered during sync group testing, which had to be fixed to get the sync groups to work without crashes
4. **Fix: Cold Start Homepage Race Condition** - Intermittent empty homepage on app cold start - surfaced once sync group volume calculations were introduced.
5. **Code Quality: Cleanup and Optimization** - Removes dead code and eliminates duplicate logic - mine, not yours, I tried to not to touch your code as much as possible
All changes have been tested and verified with `dart analyze` showing no new issues.
---
**Issue:** Volume control for Music Assistant sync groups had two distinct problems depending on how the group was created:
1. **Server-side groups**: Volume slider would snap back to 0 immediately after release
2. **In-app created groups**: Adjusting the leader's volume only changed that one player, not all members proportionally
**Root Cause:** The Music Assistant API returns `volume_level: null` for certain group players (pre-configured speaker groups with `provider == 'player_group'` and some server-side sync groups). The app had no logic to handle this case, so the null volume was interpreted as 0.
**Solution:** Implemented `GroupVolumeManager` service with dual-tier volume protection:
- **Tier 1 (GroupVolumeManager):** Computes effective volume from group members (detects groups, resolves nested, averages volumes, caches pending for 30s, confirms with ±3 tolerance).
- **Tier 2 (Provider-level):** Optimistic update protection (caches for 8s/30s, prevents stale overwrites).
**Root Cause:** For in-app groups, leader has non-null volume, so `setVolume()` only affects leader, not members.
**Expected behavior:** Leader adjustment changes all members proportionally; individual adjustments affect only that member.
**Solution:** In `setVolume()`, for group leaders with non-null volume:
1. Calculate proportional: `memberNewVol = (memberCurrentVol * newVol / leaderCurrentVol).round()` (handle zero case with absolute).
2. Cache pending, update optimistically, fire parallel API calls.
**Files Changed:**
- `lib/services/group_volume_manager.dart` (NEW - 162 lines)
- `lib/providers/music_assistant_provider.dart` (added manager, helper, updated events/state/volume methods)
---
**Feature:** Phone hardware volume buttons (including lockscreen) control MA player volume, with system HUD mirroring.
**Implementation:**
- **Android Native:** `MainActivity.kt` (~120 lines) - VolumeContentObserver, MethodChannel commands, lifecycle.
- **Flutter Service:** `hardware_volume_service.dart` (~65 lines) - streams, observer methods, sync.
- **Integration:** `main.dart` (~35 lines) - adjust/set/update methods, subscription.
**Why This Matters:** Seamless remote control from lockscreen/tray/buttons.
---
During sync group testing, fixed these UI bugs:
**File:** `lib/main.dart`
**Issue:** Console warnings during init.
**Root Cause:** Init code outside `runZonedGuarded()`.
**Fix:** Move init inside `runZonedGuarded()`.
---
**Files:** `lib/screens/home_screen.dart`, `lib/widgets/global_player_overlay.dart`
**Issue:** Crash on nav interaction.
**Root Cause:** Nav in Stack without Overlay.
**Fix:** Move nav to Scaffold.bottomNavigationBar, add extendBody: true.
---
**Files:** `lib/widgets/artist_card.dart`, `lib/widgets/album_card.dart`
**Issue:** Warnings causing corruption.
**Root Cause:** Service calls in initState trigger setState during build.
**Fix:** Defer with addPostFrameCallback, add mounted guard.
---
**File:** `lib/screens/new_library_screen.dart`
**Issue:** Crash on library tabs.
**Root Cause:** context.select in sliver context.
**Fix:** Use Consumer wrappers for tabs.
---
**File:** `lib/widgets/player/queue_panel.dart`
**Issue:** Crash on long-press buttons.
**Root Cause:** Tooltips require Overlay, but in Stack.
**Fix:** Remove tooltip properties.
---
**File:** `lib/screens/settings_screen.dart`
**Issue:** Back button crashes to black screen.
**Root Cause:** Navigator.pop pops home screen.
**Fix:** Use showPlayer + setSelectedIndex(0).
---
**Issue:** Intermittent empty homepage on cold start.
**Root Cause:** isConnected true before auth complete, leading to failed fetches.
**Solution:** Delay broadcasting 'connected' state until auth if required (`music_assistant_provider.dart:1058-1101`).
---
The `GroupVolumeManager` is a stateful service that handles **server-side groups with null volume**:
1. **Detects group players:**
```dart
bool isGroupPlayer(Player player) {
// Pre-configured MA speaker groups
if (player.provider == 'player_group') return true;
// Dynamic sync groups: have multiple members but MA returns null volume
if (player.groupMembers != null &&
player.groupMembers!.length > 1 &&
player.volumeLevel == null) {
return true;
}
return false;
}
```
2. **Computes effective volume:**
- Recursively resolves nested groups (max depth 3)
- Averages volumes across all leaf members
- Skips self-references in `groupMembers`
- Returns `null` if no members have valid volumes
3. **Caches pending volumes:**
- When user sets volume: stored with timestamp
- Returns cached value if within 30s timeout
- Clears cache when member average converges (±3 tolerance)
- Falls back to computed average after timeout
4. **Integrates with provider:**
- Called from `_applyVolumeProtection()` in provider
- Receives fresh `allPlayers` list for accurate member volumes
- Injects effective volume into `_selectedPlayer` via `copyWith()`
For **in-app created groups with non-null leader volume**, proportional propagation works as follows:
1. **Detection:** `setVolume()` checks if `player.isGroupLeader && !isGroupPlayer(player)`
- Leader has `groupMembers` with 2+ entries
- Leader has its own non-null `volumeLevel`
- NOT detected as a group player (GroupVolumeManager handles those)
2. **Calculation:** For each member in `groupMembers` (excluding leader itself):
```dart
if (leaderCurrentVol > 0) {
memberNewVol = (memberVol * newVol / leaderCurrentVol).round().clamp(0, 100)
} else {
memberNewVol = newVol // Can't ratio from zero
}
```
3. **Execution:**
- Cache `_pendingVolumes[memberId]` and `_pendingVolumeTimestamps[memberId]`
- Update `_availablePlayers[idx]` optimistically
- Fire `_api?.setVolume(memberId, memberNewVol)` without awaiting
- Continue to send leader's own `volume_set` command
4. **Result:** All member volumes scale proportionally with the leader, preserving relative balance.
```
WebSocket connects → MAConnectionState.connected
↓
Auth required?
↙ ↘
YES NO
↓ ↓
Don't notify Set state + notify
Try auth ↓
↓ Initialize
Success?
↙ ↘
YES NO
↓ ↓
auth error state
event + notify
↓
Set state
+ notify
↓
Initialize
```
**Key:** Listeners only see `isConnected = true` after auth completes (or immediately if no auth required).
android/.../MainActivity.kt | 120 + (NEW - ContentObserver)
lib/main.dart | 207 ±
lib/providers/music_assistant_provider.dart | 165 ±
lib/screens/home_screen.dart | 172 ± (BottomNav moved here)
lib/screens/new_library_screen.dart | 70 ± (Consumer wrappers)
lib/screens/settings_screen.dart | 9 ± (back button fix)
lib/services/group_volume_manager.dart | 162 + (NEW)
lib/services/hardware_volume_service.dart | 65 + (NEW)
lib/widgets/album_card.dart | 23 ± (postFrameCallback)
lib/widgets/artist_card.dart | 23 ± (postFrameCallback)
lib/widgets/global_player_overlay.dart | 171 - (BottomNav moved out)
lib/widgets/player/queue_panel.dart | 2 - (tooltip removed)
**Total:** ~650 added, ~300 removed/refactored.
Tightens the gap between the cover image and the content below so album/audiobook
titles aren't obscured by the mini player. Also centers the context menu button
vertically with the author name and book count.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SyncService was skipping sync when the database had a recent timestamp
but in-memory lists were empty (e.g., after app restart). This caused
library screens to show no data until a manual refresh.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wrap GlobalPlayerOverlay with Overlay in main.dart to provide Overlay ancestor
- Add Overlay.maybeOf check in HiResBadge before using Tooltip
- Add Overlay.maybeOf check in search_screen library-only toggle before using Tooltip
This fixes "No Overlay widget found" error that caused red screen in debug builds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace context.select with context.watch in PageView.builder tabs
(context.select cannot be used inside PageView.builder/SliverList)
- Fix LibraryStatusMixin setState during build error using Future.microtask
- Both fixes resolve the red screen issue after login
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Commit 0263c75 reduced defaultHomeRowOrder to 3 rows but also
introduced a bug where _filteredRowOrder only showed rows currently
in _homeRowOrder. This made most row options invisible in settings.
Added _allAvailableRows constant (13 rows) and modified _filteredRowOrder
to show all available static rows regardless of their current enabled state.
Users can now see and toggle all 13 home screen rows:
- 3 enabled by default (recent albums, discover artists, discover albums)
- 10 disabled by default (all other rows)
Fixes regression from commit 0263c75 (Feb 5, 2025)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Reduce artist and audiobook author cover size from 70%/280px to 50%/200px
- Reduce expanded height padding for better space utilization
- Reduce top padding across all detail screens from 24px to 8px
- Move audiobook author three-dot menu from SliverAppBar to inline with author info
This fixes issue #60 where album art/titles were cut off by bottom menu bar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement "latest request wins" pattern to prevent stale search results from overwriting current results when multiple searches are in flight. This was causing results to suddenly switch to only show "All", "Radio", "Podcasts" categories when typing quickly.
- Add _currentSearchId to track the active search request
- Add mounted check to Timer callback in _onSearchChanged
- Add search ID validation after each async operation (radio/podcast search)
- Only update state if the search is still current when results arrive
- Reset _hasPendingSearch flag when search completes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add home refresh counter to signal UI refresh after authentication
- Smart cache invalidation: only refresh home cache if it was empty
- Prevent caching empty results to avoid persisting auth errors
- Add ensureHomeRowsLoaded() to populate cache on first access
- Prevent duplicate home row loads with shared future
- Fix SendspinService emitting after disposal
- Reset auth state on WebSocket disconnect
- Change volume precision mode default to false
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove Discovery Mixes master toggle entirely
- Discovery rows now auto-appear based on provider availability (e.g., Tidal)
- Discovery rows are off by default on first launch
- All rows (discovery and static) now behave consistently:
* Enabled rows are added to end of list, not inserted at fixed positions
* Users can drag rows to any position and they stay there
- Simplified default row order to only include: recently played,
discover artists, and discover albums
- Fixed critical bug: discovery rows now correctly default to false
- Fixed SharedPreferences variable shadowing bug in setDiscoveryRowPreferences
- Fixed grey section in settings draggable list by filtering discovery-mixes
- Added localization strings for discovery rows UI
- Removed debug print statements
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show a loading indicator when discovery folders are being fetched,
ensuring pull-to-refresh works even on empty screens while discovery
is enabled.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move discovery mixes toggle from standalone to draggable row list in settings
- Add empty state widget when no home screen rows are enabled
- Fix pull-to-refresh not working when screen is empty by using SizedBox.expand
- Add localization strings for empty state hint
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
DiscoveryRow was using 52px for textAreaHeight vs AlbumRow's 44px,
causing smaller artwork and different row appearance.
Changed to 44px to match other rows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add import for RecommendationFolder model
- Wrap firstWhere in try-catch to handle missing folders gracefully
- Fixes the grey screen issue when discovery toggle is enabled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove references to non-existent RadioStation, Podcast, PodcastEpisode classes
- Fix MediaType.podcastEpisode enum value (camelCase, not snake_case)
- Use MediaItem base class for radio, podcast, podcast_episode types
Instead of rendering multiple discovery shelves in a single MixesRow,
each discovery folder now becomes its own row in the home screen,
following the standard 237px height pattern.
Changes:
- Add RecommendationFolder model with polymorphic JSON parsing
- Add DiscoveryRow widget for displaying a single discovery folder
- Add discovery folders to MusicAssistantProvider (getDiscoveryFoldersWithCache)
- Add discovery folders to CacheService and MusicAssistantAPI
- Add "Discovery Mixes" toggle to settings (default off)
- Update home screen to dynamically register discovery rows
This fixes the architectural issue where MixesRow broke the home
screen's assumption that each row entry renders exactly one row
with a fixed height.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add GitHub Sponsors and Ko-fi donation buttons in settings screen,
along with an About section showing app version and links to
GitHub repository and issue tracker.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
BREAKING CHANGE: Users must uninstall and reinstall the app.
The previous package ID (com.musicassistant.music_assistant) could be
confused with an official Music Assistant app.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When selecting the builtin/local player from the player selector overlay,
the track info would disappear because the queue API returns null for
local players. The code was clearing _currentTrack unconditionally.
Now for the builtin player:
- Preserve cached track data instead of clearing it
- Restore from cache if _currentTrack is null
- Update notification with actual track info when available
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create LibraryStatusService singleton for single source of truth
- Add LibraryStatusMixin for reactive status in StatefulWidgets
- Convert all card widgets to use centralized service:
- album_card, artist_card, playlist_card, podcast_card
- radio_station_card, track_row, audiobook_row
- Convert detail screens: album_details_screen, artist_details_screen
- Fix album stuck in library after removal:
- Add removeFromCacheByLibraryId() to SyncService
- Add markCachedItemDeletedByLibraryId() to DatabaseService
- Update removeFromLibrary() to sync all caches
This ensures library/favorite status changes are immediately reflected
across all instances of the same item throughout the app.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove confusing auto-switch behavior that could change the selected
player unexpectedly right before the user taps play.
Removed settings:
- "Prefer Local Player"
- "Disable Auto-Switch"
New simplified logic:
1. User's manual selection is always respected
2. On app reopen: restore last selected player
3. If selected player goes offline: switch to local player
4. First launch (no history): select local player
5. Local player unavailable: select first available
Kept "Smart Sort" setting for display order only.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace HSL-based color adjustments with HCT (Material Design 3 standard)
- Use proper WCAG contrast ratio calculations instead of luminance threshold
- Implement binary search to find minimum tone adjustment for contrast
- Ensure minimum chroma of 36 for primary colors to prevent washed-out highlights
- Add color scheme caching for instant loading on revisits
- Pre-extract colors on album tap for faster initial load
Fixes issue where highlighted library button was darker than unhighlighted
state on albums with dark/muted covers (e.g., Portishead, NIHT).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add material_symbols_icons package for access to book_2 icon
- Update library icon across all detail screens, context menus, bottom
nav, and search screen to use Symbols.book_2 with fill parameter
- Add podcast episode detail screen with hero animation and full
scrollable description
- Add episode context menu support with Play, Play On, Add to Queue,
and View Details options
- Add sort controls for podcast episodes (newest, oldest, alpha, duration)
- Improve context menu: slightly larger buttons (46px), wider menu (185px),
neutral text color for labels
- Search library filter now uses filled icon without background highlight
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix menu width rendering inconsistently on dark album covers by:
- Adding explicit secondaryContainer/onSecondaryContainer to ColorScheme
- Wrapping menu content in Theme to ensure FilledButton.tonal uses
correct colorScheme
- Constraining text labels to 44px width to prevent row overflow
- Shorten "Add to library" label to "Add" for better fit
- Standardize button styling across all detail screens:
- Use FilledButton.tonal for three-dot menu buttons
- Use filled heart icons with Colors.white70 inactive state
- Standardize icon size to 25
- Add sort/view controls to artist and audiobook author context menus
- Remove duplicate haptic feedback (keep single haptic in
MediaContextMenu.show)
- Make context menu dividers more subtle (15% opacity)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add inLibrary getter to base MediaItem class for podcasts/radio
- Add library toggle support to all card widgets (podcast, track, radio, audiobook)
- Add library toggle methods to detail screens (album, playlist, podcast, audiobook)
- Add library toggle for tracks within album and playlist detail screens
- Add library toggle methods to new_library_screen for audiobooks, podcasts, radio
- Update all context menu calls to use proper library toggle callbacks
- Make context menu background 25% lighter when adaptive colors are used
for better contrast against detail screen backgrounds
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 1 audit fixes:
- Use FadeSlidePageRoute for DebugLogScreen navigation (consistent transitions)
- Add volumePrecisionMode and volumePrecisionModeDescription to localizations
- Replace hardcoded empty state strings in all row widgets with localized versions:
- album_row, artist_row, track_row, playlist_row
- podcast_row, radio_station_row, audiobook_row, series_row
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Display a hi-res badge on tracks with lossless codec and either
sample rate > 48kHz or bit depth > 16. Badge uses adaptive primary
color muted with white to match album theme.
- Add HiResBadge widget with vector-drawn badge (no external assets)
- Show badge in album, playlist, and library track listings
- Fixed-width duration column to keep badges aligned
- Tooltip shows exact sample rate and bit depth
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Set itemExtent to 67px for consistent 10-item display across all library lists
- Remove vertical padding from ListTile contentPadding for better fit
- Add dedicated addTracksToQueue method to properly add tracks without interrupting playback
- Update album and playlist detail screens to use new queue method
- Add memCacheWidth/Height to series row for consistent image caching
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use centralized GlobalPlayerOverlay.showPlayerSelectorForAction() for queue
actions, reducing code duplication. Add itemExtent to library lists and reduce
image sizes for smoother scrolling.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The external source detection was incorrectly flagging Spotify tracks
played through Music Assistant as "external" because it checked for
spotify:// URI patterns without considering whether MA had an active queue.
Changes:
- Add activeQueue field to Player model to track MA queue state
- Fix detection priority: active_queue presence now indicates MA control
- Remove spotify:// from fallback URI checks (needs app_id context)
- Only flag physical inputs (optical, hdmi, etc.) in URI fallback
- Update provider event handling to use same detection logic
- Improve debug logging to include active_queue state
Detection logic priority:
1. app_id == 'music_assistant' → NOT external
2. active_queue has value → NOT external (key fix for Spotify provider)
3. app_id set and != 'music_assistant' → external
4. Fallback: Check physical input URIs only
Fixes#27
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Move palette extraction to isolate using compute() to prevent main thread blocking
- Add image package for pure-Dart isolate-compatible image decoding
- Add extractColorSchemesFromUrl() with LRU cache for downloaded images
- Replace context.watch() with context.select() to reduce unnecessary rebuilds
- Update all detail screens to use isolate-based color extraction
- Optimize library screens and widgets to only rebuild on specific field changes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Documents WebRTC remote connection investigation and findings.
Feature on hold due to one-way channel bug. See docs for details.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace Material Slider with custom drawn sliders in:
- expandable_player.dart (progress bar)
- volume_control.dart (volume slider)
- Add seek debounce to prevent rapid-fire seeks
- Use standard PageScrollPhysics in library to fix multi-tab scroll
Flutter 3.38's Material 3 Slider uses OverlayPortal which causes
grey rectangles in our overlay context. Custom sliders avoid this.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Two issues fixed:
1. Version check was looking for MA 2.8.0+ but proxy was added in 2.7.1
2. Local proxy fallback wasn't sending auth token, causing connection rejection
Now local IP connections properly authenticate with the MA token when
falling back to the /sendspin proxy endpoint.
Fixes#52
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Show the bottom navigation bar when connected even without a player
selected. This allows users to access Settings and Library to
troubleshoot connection issues or browse content.
Fixes#52
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix audiobook year sorting (ascending/descending support)
- Fix server-handled sort orders (play_count, last_played, etc.) to preserve server order
- Fix letter scrollbar showing irrelevant letters for non-alphabetical sorts
- Add ScrollbarDisplayMode enum for contextual scrollbar popups
- Fix playlists sorting by requesting sorted data from server
- Fix multiple accounts of same service not showing in provider toggles
- Fix stale provider IDs persisting in settings
- Use context.watch instead of Selector for albums/artists tabs
- Use Consumer for menu provider toggles
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add idle service timeout to stop the Android foreground service when no
playback occurs for 30 minutes. This saves battery by allowing the
service to be fully stopped rather than persisting indefinitely.
- Add stopService() method to MassivAudioHandler
- Add 30-minute idle timer in MusicAssistantProvider
- Timer starts when playback stops, cancels when playback resumes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Provider Icons:
- Add provider icon overlays to series detail screen (grid/list)
- Add provider icon overlays to library playlists (grid/list)
- Add ProviderIconOverlay widget and provider manifest model
Performance Optimizations:
- Fix empty setState in SyncService listener - only rebuild on sync complete
- Add cached sorting for albums, artists, authors (avoid O(n log n) per rebuild)
- Add cached track sort keys for LetterScrollbar
- Fix image cache size mismatches (author 128px, audiobook list 128px, series 150px)
- Simplify provider toggle setState to avoid double rebuild
- Remove redundant text Hero animations from card widgets
- Defer color extraction from card tap to detail screen initState
- Increase fallback image debounce from 200ms to 400ms
Other Changes:
- Update screenshots
- Add new localizations
- Various detail screen improvements
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The ExpandablePlayer was loading the volume precision setting only once
in initState(), so changes made in settings weren't reflected until app
restart. PlayerCards in the overlay were loading fresh values on each
creation.
Fix: Add didUpdateWidget to reload the precision mode setting when the
device reveal overlay becomes visible. This ensures the setting is fresh
after the user may have changed it in settings.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Memory Leak Fixes:
- Add max pending requests limit (100) to prevent unbounded growth
- Cancel pending requests in _reconnect() before reconnecting
- Track ImageStream listeners in expandable_player for proper cleanup
- Add cleanup in dispose() for active image stream listeners
Database Performance:
- Add batchCacheItems() method using drift batch API
- Chunk batches at 500 items to avoid SQLite limits
- Refactor sync_service to use batch inserts (~10x faster)
- Add BatchCacheItem data class for type-safe batch operations
Race Condition Fix:
- Make connection guard check-and-set atomic
- Create completer before check to close race window
WCAG Accessibility:
- Mark ButtonSizes.sm/md as @Deprecated (below 44x44 minimum)
- Add ButtonSizes.minAccessible constant (48x48)
- Add clear documentation on WCAG Level AA/AAA requirements
Provider Decomposition (partial):
- Extract SleepTimerProvider with callback pattern
- Extract ConnectionProvider skeleton for future integration
- Prepare facade pattern for backward compatibility
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use the existing PlayerRevealOverlay for "Play On" actions instead of
the PlayerPickerSheet popup modal. This provides a more consistent UX
by reusing the same player selection interface throughout the app.
- Add onPlayerSelected callback and contextHint to PlayerRevealOverlay
- Add showPlayerSelectorForAction helper to GlobalPlayerOverlay
- Refactor album, artist, playlist, podcast, audiobook, and search screens
- Add localization strings for context-specific hints
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Clear notification when switching to idle player (fixes issue #42)
- Fix notification progress bar desync after seek and with Sendspin
- Always use position tracker instead of PCM elapsed time
- PCM elapsed resets to 0 on stream start, causing wrong position
- Fix invisible nav bar due to stale playerExpansionNotifier
- Reset notifier when player hidden (disconnect/no player)
- Reset notifier after collapse() completes even if already at 0
- Fix welcome backdrop showing dark color in light theme
- Now uses theme-appropriate surface color
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sleep Timer:
- Add sleep timer panel with preset durations (15/30/45/60 min)
- Support finish current track option
- Show countdown in queue panel header
- Auto-stop playback when timer expires
Progress Bar Fix:
- Fix anchor refresh logic when server sends stale position data
- Allow anchor refresh even with suspicious reset when anchor is stale
- Use interpolated position to prevent backward jumps from bad server data
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove "Show only artists with albums" option from settings (now in library menu)
- Remove "Audiobook Libraries" section from settings screen
- Add ABS library filter to Books tab options menu
- Show spinner on menu button while syncing libraries
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Tap artist name in expanded player to navigate to artist details
- Tap album name in expanded player to navigate to album details
- Use global navigator key for overlay context navigation
- Fix album details to use fresh album data for artist display
- Fix EmptyState widgets to be scrollable for PageView swipe detection
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Provider counts now only count items where inLibrary=true
(the provider that actually added the item to library)
- Providers with 0 items are hidden from the filter list
- Fixes issue where playback-only providers appeared with
duplicate counts matching other accounts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add inLibrary field to ProviderMapping for accurate ownership tracking
- Fix podcast provider filtering to use inLibrary field
- Add itunes_podcasts to provider capabilities
- Convert album/artist/podcast detail screens to use optimistic updates
(button updates immediately, reverts on API failure)
- Remove server-side provider filtering from search
- Add client-side provider filtering for search in-library mode
- Fix episode loading fallback for non-library podcasts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add sourceProviders column to database schema (v6) for tracking which
provider instance each item came from
- Update SyncService to sync each enabled provider separately and tag
items with their source provider
- Add client-side filtering methods that use source tracking to properly
differentiate between multiple accounts of the same provider type
(e.g., two Spotify accounts)
- Update library UI to use instant client-side filtering with background
sync for data freshness
This enables instant UI updates when toggling provider filters, with
accurate filtering even for same-provider-type accounts.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Change transfer dropdown background to lighter shade of queue panel
background instead of dark album highlight color
- Add forceCollapse() to cleanly collapse player after transfer
(closes queue panel instantly + collapses in one motion)
- Track queue's player ID to detect player changes and reload correct queue
- Fix transfer dropdown to use selected player ID for correct filtering
- Clear source player's track cache after transfer to prevent stale data
in player selector
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add transferQueue API method to move playback queue between players
- Add transfer queue button to queue screen with player picker
- Refactor queue panel with improved controls
- Minor library screen cleanup
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove separate _dragOffset variable that caused cards to "shoot out"
below mini player during swipe dismiss
- Directly control animation controller during drag for seamless handoff
- Fix bounce timing when user drags past bounce threshold before release
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add decryption error handling in SecureStorageService
- Clear corrupted credentials when BadPaddingException occurs
- Add android:appCategory="audio" to manifest for proper app categorization
- Fix letter scrollbar proportional scrolling and bottom padding
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Two bugs were causing author photos to fail for well-known authors like
J.R.R. Tolkien and J.K. Rowling:
1. Audnexus API requires two-step fetch: The search endpoint only returns
ASIN and name, not the image. Now correctly fetches author details by
ASIN after search to get the image URL.
2. Open Library returns 302 redirects: The HEAD request check was only
accepting 200 status codes, but Open Library redirects to archive.org.
Now accepts 301/302/307/308 redirect responses as valid.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use consistent 256px image cache size for album grid cards to match
the detail screen cache size. This ensures smooth Hero animations
when navigating from grid view (both 2-column and 3-column) to the
album detail screen.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Make track name the primary line (title) instead of artist
- Move artist to subtitle line
- Add track duration on the right side
- Keep favorite icon (now before duration) for favorited tracks
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add magnifying glass icon inside search bar
- Move library toggle outside search bar with circle background
- Style library toggle like library screen buttons (44x44, CircleBorder)
- Toggle highlights with primary color when active
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add updateAdaptiveColorsFromImage calls before navigation to detail
screens in library tab. This ensures bottom nav colors update
immediately when navigating from:
- Artist tiles (list and grid views)
- Author navigation
- Audiobook/book navigation
- Series navigation (list and grid views)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1. Fix missing icon in author detail screen:
- Use Stack pattern (icon underneath + image overlay) like library list
- Ensures icon always visible when no image
- Consistent Hero animation between screens
2. Add Wikidata as author image source:
- Structured data from Wikipedia, better for exact person matching
- Uses P18 (image) property to get Wikimedia Commons photos
- Searches top 3 results to find one with an image
- Added between Open Library and Wikipedia in priority
Source priority:
1. Audnexus (audiobook-focused)
2. Open Library (book database)
3. Wikidata (structured Wikipedia data) - NEW
4. Wikipedia (search-based fallback)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Check for common suffixes (Jr., Sr., III, PhD, etc.)
- Check if second part looks like initials (J.R.R., etc.)
- Require BOTH parts to have multiple words for multi-author split
- Don't cache null on failure - fall through to try full name
This prevents incorrectly splitting "Name, Jr." or "Lastname, First"
while still handling "Neil Gaiman, Dirk Maggs".
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The multiple search queries (exact name, then with suffixes) was
finding wrong pages. Reverting to original "name writer author"
search which was tuned to find author pages.
Keep the comma-separated author handling.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1. Handle comma-separated author names (e.g., "Neil Gaiman, Dirk Maggs")
- Detects multi-author format vs "Lastname, Firstname" format
- Tries to get image for each author until one succeeds
2. Improve Wikipedia search strategy
- Try exact name first (works best for famous authors)
- Then try with "author", "writer", "novelist" suffixes
- This helps find authors whose pages might not rank for generic search
Should help find: Anne Rice, Philip Pullman, Paulo Coelho, P.G. Wodehouse
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
After normalizing "J. R.R Tolkien" to "J. R. R Tolkien", the final
"R" lacks a period and won't be detected as an initial.
Now adds periods to standalone uppercase letters between spaces:
"J. R. R Tolkien" -> "J. R. R. Tolkien"
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The metadata sometimes has messy formats with inconsistent spacing
around initials. Now handles:
- "J. R.R Tolkien" -> "J. R. R. Tolkien"
- "J.R.R. Tolkien" -> "J. R. R. Tolkien"
- "J.K. Rowling" -> "J. K. Rowling"
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Problems:
- "J.R.R. Tolkien" wasn't being parsed correctly - the condensed
initials "J.R.R." weren't detected as initials
- Wikipedia only tried the first name variation
Fixes:
1. Expand condensed initials: "J.R.R." -> "J. R. R." for better splitting
2. Better initial detection that handles multiple initials
3. Generate compact variations: "JRR Tolkien", "JK Rowling"
4. Wikipedia now tries top 3 variations (best source for famous authors)
Example variations for "J.R.R. Tolkien":
- J.R.R. Tolkien (original)
- J. R. R. Tolkien (expanded)
- Tolkien (last name only)
- JRR Tolkien (compact initials)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove memCacheHeight from author images - setting both memCacheWidth
and memCacheHeight to equal values forces the image to be cached as
a square in memory, which distorts non-square source images BEFORE
BoxFit.cover can properly scale and crop.
By only setting memCacheWidth, height scales proportionally to
preserve the original aspect ratio. BoxFit.cover then handles
filling the circular container correctly.
Fixed in:
- Library grid author cards
- Library list author tiles
- Author detail screen header
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
These providers were early attempts at splitting MusicAssistantProvider
that were never completed or integrated. Like PlayerProvider (removed in
previous commit), the code has evolved significantly and these files
would need substantial updates to be useful.
Also fixes stale comment reference to library_provider.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The standalone PlayerProvider was created early but never integrated.
MusicAssistantProvider has since evolved with:
- Sendspin support (MA 2.7.0b20+)
- Cast-to-Sendspin ID mapping with DB persistence
- Position tracker integration
- Audio handler mode switching
- Complex player filtering
Keeping player code in MusicAssistantProvider as it's working
and would require ~1600 lines of sync work to update PlayerProvider.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Updated favorites heart icons to use semantic StatusColors.favorite:
- new_library_screen.dart: playlist and track favorites
- library_playlists_screen.dart: playlist favorites
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- global_player_overlay.dart: Use isConnected/hasSelectedPlayer from
context.select() instead of removed provider variable
- expandable_player.dart: Add themeProvider via context.read() for
methods that need full ThemeProvider object
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Reverts the oversized 44x44 buttons from Phase 1 audit fix to a more
balanced 36x36 size that fits better visually while still being
easily tappable.
- _filterRowHeight: 48 → 36
- buttonSize: 44 → 36
- iconSize: 20 → 18
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- global_player_overlay: Use select() for isConnected/selectedPlayer check
- new_library_screen: Use select() for podcasts and radio tabs
- expandable_player: Use select() for adaptiveTheme boolean
- player_reveal_overlay: Remove unused ThemeProvider watch
Performance impact: Reduces unnecessary rebuilds when unrelated
provider state changes, improving scroll performance in library tabs.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add SettingsService.initialize() to cache SharedPreferences at startup
- Replace 130+ getInstance() calls with cached _getPrefs() helper
- Add migration completed flag to SecureStorageService
- Skip SecureStorage migration on subsequent app launches
Performance impact: Eliminates redundant SharedPreferences lookups
and saves ~100-300ms on startup after first run.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Icon is always rendered underneath in a Stack
- Image covers icon when loaded successfully
- No widget tree switching = no layout glitches
- Placeholder/error show SizedBox.shrink() to reveal icon beneath
Use a builder function to create fresh Icon widgets for each use
instead of reusing a single widget instance. Also wrap placeholder
and error widgets in properly sized Containers.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Name variations tried in order:
- Original name
- "Lastname, Firstname" -> "Firstname Lastname" conversion
- Remove titles (Dr., Mr., Mrs., Prof., Sir, Dame, etc.)
- Remove suffixes (Jr., Sr., III, PhD, etc.)
- First + Last name only (skip middle names/initials)
- Last name only (when first part is an initial)
- Handle double initials like "J.K. Rowling" -> try "Rowling"
Also refactored API calls into separate helper methods for cleaner code.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use Icons.person_rounded for fallback instead of text initials
- Properly center the icon in both list tile and card views
- Fix sizing to prevent visual jumps when images load
- Simplify widget structure for consistent rendering
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix favorites toggle not showing tracks by using _sortedFavoriteTracks
instead of filtering _sortedAllTracks (which has stale favorite flags)
- Fix tracks list font size to use titleMedium like other tabs
- Fix all tabs to use BottomSpacing.withMiniPlayer to account for
mini player height, preventing bottom items from being cut off
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The browse API returns abbreviated paths like /s for series instead of
/series. Update detection to check for path.endsWith('/s') in addition
to checking for 'series' in the name or path.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix audiobook favorites toggle not reloading books when toggled
- Fix series loading by adding 'label' fallback for 'name' field
- Add consistent padding to View Mode button
- Add sort direction indicator with rotated icon for descending sorts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add flag to ignore onPageChanged during media type transitions
- Handle page jump directly in _changeMediaType instead of _resetCategoryIndex
- Clear flag after short delay to ensure all page change events have fired
- Make Tracks tab always visible in Music (not just when favorites on)
- Add lazy loading for tracks (initial 200, +100 on scroll)
- Add sort button with popup menu next to favorites toggle
- Sort icon changes to reflect current sort order
- Extend favorites filtering to Radio and Podcasts tabs
- Add sort options for all categories:
- Artists: A-Z, Z-A
- Albums: A-Z, Z-A, Year (Newest/Oldest), By Artist
- Tracks: A-Z, By Artist, By Album, Duration
- Playlists: A-Z, Z-A, Track Count
- Radio/Podcasts: A-Z, Z-A
- Persist sort preferences per category in settings
- Add localization strings for new empty states
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Subscribe to queue_items_updated events from Music Assistant
- Add error handling with rollback for queue delete operations
- Add error handling with rollback for queue move operations
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add AnimatedPositioned highlight that slides smoothly between filters
- Match the library screen's animated pill-style highlight effect
- Use GlobalKeys to measure actual filter chip positions for accurate animation
- Fix scroll-back behavior by using measured positions instead of estimates
- The highlight animates with 300ms easeOutCubic curve matching library screen
Also fixes the scroll issue where swiping back to the beginning would leave
the last filter hidden. The scroll logic now uses actual measured positions
with proper padding instead of approximate 80px widths.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add listeners to SyncService and MusicAssistantProvider to detect when
artists become available after initial load. When the home screen builds
with empty favorites (before cache/sync completes), it now:
- Listens for SyncService status change from syncing to completed
- Listens for MusicAssistantProvider changes when artists load from cache
- Triggers a full row refresh when artists become available
This fixes the race condition where favorite rows would load before
the library cache was populated, showing empty results until manual refresh.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Quick enhancement from Issue #43:
- Convert PlaylistCard to StatefulWidget
- Add onLongPress handler to toggle favorite
- Show animated heart overlay with elastic animation
- Display persistent favorite indicator (heart badge) when favorited
- Haptic feedback on long-press
- Snackbar confirmation with offline sync support
- Invalidate home cache after favorite change
Matches the favorite functionality in PlaylistDetailsScreen.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Issue #44: Language now correctly falls back to English instead of German
for unsupported locales (e.g., French system language before this update).
Added localeListResolutionCallback to MaterialApp that:
- Respects user's explicit locale choice
- Falls back to matching system locale if available
- Defaults to English when no match found
Added complete French (fr) translation:
- All 200+ strings translated
- Proper French accents (é, è, ê, à, etc.)
- Added to settings language picker
Updated:
- lib/main.dart: localeListResolutionCallback for fallback logic
- lib/l10n/app_fr.arb: New French translation file
- lib/l10n/app_en.arb: Added "french" string
- lib/l10n/app_de.arb: Added "french" string (Französisch)
- lib/l10n/app_es.arb: Added "french" string (Francés)
- lib/providers/locale_provider.dart: Added French display name
- lib/screens/settings_screen.dart: Added French option
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Major improvements to the playlist detail screen:
- Add hero animations for playlist cover and title
- Add adaptive color extraction from cover art
- Add responsive layout with proper sizing (LayoutBuilder)
- Add action button row: Play, Play On, Add to Queue, Favorite
- Add expandable track actions on long-press (radio, queue, favorite)
- Replace hardcoded colors with theme-aware colors
- Add track duration display in track list
- Add fullscreen art viewer on cover tap
- Add proper bottom padding for mini player
- Update all playlist navigation to use FadeSlidePageRoute
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Debug colors now show every pixel:
- Green = title padding (12px top, 8px bottom)
- Yellow = 2px margin between rows
- Blue = content area (horizontal list)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Shows availableHeight, rowHeight, enabledRows, and margins to help
identify what's causing scroll overflow.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Only subtract margin space when there are 3 or fewer rows.
For 4+ rows, use original calculation so 4th row title stays hidden.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The row height calculation now subtracts margin space before dividing
by 3, ensuring that 3 rows + 2 margins fit exactly without scroll.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Green: title padding area (12px top, 8px bottom)
- Yellow: 1px margin between rows
Temporary debug visualization to help understand spacing.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add PodcastCard and PodcastRow widgets for favorite podcasts
- Fix RadioStationRow to conform to established row patterns:
- Use titleLarge font instead of titleMedium
- Use fromLTRB padding (16, 12, 16, 8) instead of symmetric
- Add SizedBox height wrapper, itemExtent, StretchScrollBehavior
- Add DebugLogger for performance tracking
- Fix RadioStationCard text clipping (height: 1.15, SizedBox wrapper)
- Fix PlaylistRow with same conformity updates as RadioStationRow
- Add getFavoritePodcasts() to provider
- Add settings support for favorite podcasts row visibility
- Add localization strings for favorite podcasts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Two issues fixed:
1. Podcast name now displays correctly (not episode name twice)
- Added _currentPodcastName context field to provider
- Set podcast name when playing from podcast detail screen
- Clear context on media type change, external source, or playback stop
- Removed fallback to album/artist (which contained episode name)
2. Text overlap in mini player for long podcast titles
- Changed maxLines from 2 to conditional (1 when collapsed, 2 when expanded)
- Fixed trackInfoBlockHeight to account for podcasts hiding album line
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
First and last tabs are now flush with the container edges.
Middle tabs maintain 2px gap between adjacent highlights.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Moved color function to class level (_getMediaTypeColor)
- Category filter bar now uses the same color as the selected
media type (e.g., purple for Music, orange for Audiobooks)
- Both bars share the same color logic
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Same treatment as media type bar:
- Sliding highlight with 300ms easeOutCubic animation
- Full border radius pill shape
- 2px horizontal inset
- Proportional widths based on label length
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Highlight now fills full bar height (top: 0, bottom: 0)
- 2px horizontal inset for small gap between segments
- Border radius 8px to match container
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Highlight now slides smoothly between tabs (300ms, easeOutCubic)
- Pill-shaped highlight with full border radius on all sides
- 3px inset from container edges for visual separation
- Color also animates when switching types
- Uses LayoutBuilder to calculate proportional positions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
PageView with only 1 item doesn't generate overscroll notifications.
For single-category types, use GestureDetector to detect horizontal
swipes and switch media types accordingly.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Swapped the overscroll logic:
- Positive overscroll (swipe left at end) = go to NEXT type
- Negative overscroll (swipe right at start) = go to PREVIOUS type
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When at the last category (e.g., Playlists in Music), swiping left
switches to the next media type (Audiobooks). Similarly, swiping
right at the first category goes to the previous media type.
Uses overscroll detection with 80px threshold to trigger the switch.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Colors derived from primaryContainer using HSL hue shift
- Same saturation/lightness as filter bar, just different hues
- Text uses onPrimaryContainer for proper theme contrast
- Fixed vertical centering with alignment: Alignment.center
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use Expanded (not Flexible) so bar fills full width,
while flex values still give longer labels more space.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Music: soft purple
- Audiobooks: soft orange/amber
- Podcasts: soft teal
- Radio: soft pink
Colors appear when type is selected, dark text for contrast.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Bar fills full width with no gaps between segments
- Each segment gets flex = 10 (base) + label.length
- Longer labels like "Audiobooks" get more space than "Music"
- Works with any language - translations auto-adjust
- Icons always visible for all types
- Border radius matches filter bar (8px)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- All buttons now on one continuous bar with no gaps
- Icons always visible for each type (not just when selected)
- Smooth animation on selection change
- Better appearance on larger screens
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The nav bar now correctly uses default surface color (grey) on the home screen
when the player is collapsed. Adaptive colors are only applied when:
1. The player is expanding/expanded (progress > 0)
2. The user is on a detail screen (album, artist, playlist)
This prevents favorites and radio stations from incorrectly tinting the nav bar
when played directly from the home screen.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Only use adaptive colors for nav bar when:
1. Player is expanding/expanded (progress > 0), OR
2. On a detail screen (isOnDetailScreen flag)
- On home screen with collapsed player, always use default surface color
- Added isOnDetailScreen flag to ThemeProvider to distinguish between
colors from detail screen navigation vs player color extraction
- Also applies to icon highlight color, not just background
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adjust 3-line layout (track, artist, player) to be properly centered
within the 72px mini player height.
Before: Text block center at 32.5px (3.5px above true center)
After: Text block center at 36px (true center)
Values changed:
- primaryTop: 5 → 9
- secondaryTop: 26 → 30
- tertiaryTop: 46 → 50
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove flightShuttleBuilder that was showing only the first cover
during hero animation. The mosaic grid is simple enough (max 3x3)
to animate directly without performance issues.
Before: Single cover during flight -> mosaic snaps in at end
After: Mosaic animates smoothly throughout the transition
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move cached data loading from async method to initState so the first
build already has data. This prevents the brief spinner flash when
returning to the app after backgrounding.
Before: initState -> build (spinner) -> async cache load -> setState
After: initState (sync cache) -> build (data) -> async fresh load
Applied to all row widgets:
- album_row.dart
- artist_row.dart
- playlist_row.dart
- radio_station_row.dart
- audiobook_row.dart
- series_row.dart
- track_row.dart
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Convert TrackRow from FutureBuilder to setState-based dual-loading
pattern (matching other row widgets). Add getCachedTracks callback
support for instant display and offline fallback.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Convert AudiobookRow and SeriesRow from FutureBuilder to setState-based
dual-loading pattern (matching AlbumRow). Add caching support for
audiobooks and series to enable instant display and offline fallback.
Root cause: FutureBuilder pattern created Future once in initState.
When connection restored, widget State was preserved via KeepAlive,
_hasLoaded remained true, preventing data re-fetch.
Changes:
- AudiobookRow: setState pattern + getCachedAudiobooks callback
- SeriesRow: setState pattern + getCachedSeries callback
- CacheService: Add audiobook/series caching methods
- Provider: Add *WithCache() and getCached*() methods
- NewHomeScreen: Pass cache callbacks to audiobook/series rows
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Major performance fixes for animation stutter:
1. Cache FadeTransition opacity animation
- Created _fadeIn50to100 once in initState
- Previously created new Tween/CurveTween/Animation objects every frame
- Used by 3 FadeTransition widgets (audio format, progress bar, volume)
2. Pre-compute Alignment and FontWeight
- textAlignment: computed once per frame, reused by 3 text elements
- titleFontWeight: computed once per frame for title text
- Removed unnecessary FontWeight.lerp from artist text
These changes eliminate ~6-9 object allocations per animation frame
that were happening in hot paths.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 4 performance optimizations:
ExpandablePlayer:
- Cache MaterialRectCenterArcTween (recreate only on dimension change)
- Pre-cached static BoxShadow objects for mini player and art shadows
- Use Transform.translate for GPU-accelerated slide animations
- Pre-compute color opacities (textColor60, textColor70, textColor45)
- Use Color.lerp between pre-computed colors for artist text
- Throttle expansion progress notifications (notify on ≥1% change)
- Simplified shadow rendering with threshold-based visibility
PlayerRevealOverlay:
- Pre-compute player card data outside AnimatedBuilder
- Cache hint colors to avoid withOpacity() per frame
- Add _PlayerCardData helper class for pre-computed data
PlayerCard:
- Static const BoxShadow for card and volume overlay
- Already has RepaintBoundary (verified)
These changes reduce per-frame object allocations and move
slide animations to GPU compositing layer.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously shuffle and repeat buttons used conditional rendering
(if t > 0.5) which caused them to suddenly appear in the Row,
jerking the other playback controls.
Now the buttons:
- Always render with animated width (0 to 44px)
- Smoothly grow into place as animation progresses
- Fade in with expandedElementsOpacity after t > 0.3
This eliminates the visual jerk when expanding the player.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Pre-compute textColor50, primaryColor20, primaryColor70 at method start
- Replace 8+ withOpacity() calls with cached color references
- Reduces Color object allocations during animation frames
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Major animation improvements:
1. Album art: Uses MaterialRectCenterArcTween for true curved arc path
- Creates natural Hero-like trajectory instead of straight diagonal
2. Text alignment: Replaced discrete textAlign switches with Align.lerp
- Title, artist, and player name now smoothly transition from left to center
- No more jump at t=0.5 threshold
3. Text styles: Replaced discrete style switches with smooth lerping
- FontWeight.lerp for smooth weight transitions
- _lerpDouble for letterSpacing and lineHeight
- Opacity lerping for color transitions
4. Player name: Staggered opacity fade (0 to 0.4 range)
- Position animates smoothly throughout
- Wraps in Align for smooth centering
5. Controls: Proper Align.lerp for centering
- Removed hacky Padding approach
- Smooth transition from right-aligned to centered
6. Removed broken _lerpX/_lerpY helpers
- These were double-curving an already-curved t value
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement Hero-like curved animations for mini player to expanded player:
- Add _lerpX (easeOutQuad) for faster horizontal centering
- Add _lerpY (easeInOutCubic) for gradual vertical movement
- Album art now follows curved arc path instead of straight diagonal
- Title and artist text use curved paths for smoother transitions
- Controls use Align widget for smooth centering transition
- Player name now animates position (was previously static)
This creates more natural, hero-like transitions where elements
curve into their final positions rather than moving linearly.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Flutter's Slider uses overlayRadius (20px) as padding on each side,
so the thumb moves within [overlayRadius, width - overlayRadius].
The indicator position calculation now accounts for this to stay
perfectly aligned with the thumb at all positions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously the indicator was clamped to stay 16px from edges, causing
it to animate separately from the thumb at 0% and 100%. Now it follows
the thumb exactly at all positions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
ScaffoldMessenger couldn't find Scaffold from within expanded player overlay.
Using custom hint positioned below slider with snackbar-like styling:
- Dark background (#323232)
- White text
- Positioned below the volume slider
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix consecutive button taps using stale volume (now uses pending volume)
- Both +/- buttons correctly step by 1
- Change precision mode hint to standard SnackBar for consistency
- SnackBar appears at bottom like other app messages
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Display teardrop indicator for 800ms when +/- buttons are tapped
- Both buttons already step by 1 (delta of -1 and +1)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Clamp teardrop indicator position to stay within slider bounds
- Use adaptive primary color instead of amber for precision mode
- Show "Precision mode" hint when show hints setting is enabled
- Auto-hide hint after 2 seconds
- Text color adapts based on background luminance
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Live volume: throttled updates during drag (150ms normal, 50ms precision)
- Precision mode: hold still 800ms for fine-grained control (10% range)
- Teardrop indicator: upside-down teardrop above thumb showing volume number
- Slider and track turn amber in precision mode
- Haptic feedback when entering precision mode
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace mute button with volume decrease (-1%)
- Replace percentage text with volume increase (+1%)
- Add floating percentage indicator above thumb (visible only when dragging)
- Buttons adjust volume in steps of 1%
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix Android back gesture for queue panel (double haptic, reliability)
- Isolate queue animation to prevent full player rebuilds (perf)
- Increase spring stiffness for snappier queue animation
- Fix home screen row height calculation (no more scroll with 3 rows)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Row height now correctly divides available space by 3, removing
the erroneous +8 that caused unnecessary scroll with 3 rows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Critical optimizations:
1. Remove _queuePanelAnimation from main AnimatedBuilder merge
- Queue animation no longer rebuilds entire player (~150 widgets)
- Only queue panel section rebuilds during slide animation
2. Add cached _queueSlideAnimation
- Avoids recreating Tween.animate() on every frame
- Animation object created once in initState
3. Wrap queue panel in own AnimatedBuilder
- Uses child parameter to avoid rebuilding QueuePanel itself
- Only Offstage wrapper updates during animation
4. Wrap favorite/queue buttons in own AnimatedBuilder
- Button fade animation isolated from main player rebuilds
- Returns SizedBox.shrink() when fully hidden
5. Increase spring stiffness for snappier animation
- Open: 400→550 stiffness, 42→50 damping
- Close: 450→600 stiffness, 45→52 damping
Expected improvement: ~60% reduction in frame time during queue animation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Root cause: During rapid open-close, the animation value might not
have reached 0.5 yet when back is pressed, causing isQueuePanelOpen
to return false. Also, closeQueuePanel() rejected calls during animation.
Fix:
- Add isQueuePanelTargetOpen getter (intent, not animation value)
- Use target state in didPopRoute for reliable detection
- Allow closeQueuePanel to work during animation by stopping it first
- This handles rapid gestures without needing 3-second gaps
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Root cause: didPopRoute in main.dart intercepts back gesture at app
level but only checked isPlayerExpanded, not isQueuePanelOpen. This
caused back to call collapsePlayer() which then called closeQueuePanel()
with default haptic enabled, resulting in double haptic (system + app).
Fix:
- Add isQueuePanelOpen check in didPopRoute BEFORE isPlayerExpanded
- Close queue panel with withHaptic: false (system provides haptic)
- Add withHaptic parameter to closeQueuePanel() and propagate through
GlobalPlayerOverlay
This handles Android back at the app level, bypassing PopScope entirely
for reliable 100% interception.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Root cause: PopScope was outside AnimatedBuilder, so it never
rebuilt when _queuePanelTargetOpen changed. This caused canPop
to retain stale value (true) even when panel was open, allowing
Android back gesture to pass through instead of being intercepted.
Fix: Wrap _queuePanelTargetOpen changes in setState() to force
widget rebuild including PopScope with correct canPop value.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add _queuePanelTargetOpen boolean to track intended panel state
separate from animation value, preventing race conditions where
animation crosses 0.5 threshold during back gesture
- Set target state immediately when open/close is triggered
- Use target state for PopScope canPop check instead of animation value
- Add withHaptic parameter to _closeQueuePanelWithSpring() to avoid
double haptic (system + app) on Android back gesture
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previous approaches using state tracking in confirmDismiss didn't
reliably block edge gestures from reaching Dismissible.
New approach: Add invisible GestureDetector overlays on left and
right edges of the queue list Stack. These physically absorb all
horizontal drag gestures from the edge zones before they can reach
the Dismissible widgets underneath.
The absorbers use HitTestBehavior.translucent so they don't block
taps, only horizontal drags.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The Dismissible gesture system operates independently of the Listener.
Using static variables ensures the edge zone state persists correctly:
- Store last pointer down X position as static variable
- Add static getter lastTouchWasInEdgeZone for confirmDismiss
- Increased edge dead zone to 48px for better Android gesture coverage
- Edge touches now clear swipe state immediately
This ensures Dismissible's confirmDismiss can reliably check if the
gesture started from a screen edge, blocking accidental deletes
during Android back gesture.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1. Prevent Dismissible from triggering on edge swipes:
- Keep _startedInEdgeZone flag until next touch (not reset on pointer up)
- Check flag in confirmDismiss to reject edge-initiated swipes
- This prevents Android back from right edge triggering swipe-to-delete
2. Fix PopScope sometimes failing to close:
- Remove strict isAnimating guard that could block close
- Stop any existing animation and start close immediately
- Always respond to back gesture when queue panel is open
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Bug: Was checking local X within queue panel widget, but queue panel
slides in from right side of screen. Local x=0 is middle of screen,
not the screen edge where Android back gesture starts.
Fix: Use global event.position.dx (screen coordinates) to detect
if touch started near screen edges, not local widget coordinates.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Root cause: QueuePanel's Listener was detecting Android back gesture
(edge swipe) as a swipe-to-close, triggering haptic. PopScope also
handled it, causing double haptic.
Fix: Add edge dead zone (40px) to QueuePanel's swipe detection.
Swipes starting within the edge zone are ignored, letting only
PopScope handle the Android back gesture.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The direct value manipulation during swipe was causing jank from the
transition between finger tracking and spring animation. Simplified to:
- Swipe gesture just detects direction and velocity
- On release, decides to close or stay open based on velocity/displacement
- No intermediate finger tracking - cleaner animation trigger
- Panel either closes with spring or stays fully open
This eliminates the jank from mixed animation modes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Root cause: CurvedAnimation was transforming spring physics output,
causing discontinuity between finger tracking and spring animation.
During swipe:
- Controller value set to 0.6 (finger position)
- CurvedAnimation transforms: fastOutSlowIn(0.6) ≈ 0.78
- Panel shows at wrong position (not following finger 1:1)
- When spring starts, there's a jump to correct position → jerk
Fix: Use raw AnimationController directly instead of CurvedAnimation.
Spring physics already provide natural easing - applying another
curve on top distorts the motion and breaks finger tracking.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Root cause: Two gesture handlers both triggered close on swipe right:
1. GestureDetector.onHorizontalDragEnd called _toggleQueuePanel()
2. QueuePanel's Listener called _closeQueuePanelWithSpring() via callback
Both fired on the same gesture, causing double close animation and
double haptic feedback.
Fix: Remove the redundant close trigger from GestureDetector since
QueuePanel's Listener already handles its own swipe-to-close.
Swipe left to OPEN queue is still handled by GestureDetector.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Root cause: The .then() callbacks that snapped controller value to
exactly 0/1 after spring animation caused a visible visual jump.
Changes:
- Remove all .then() snap callbacks from spring animations
- Use overdamped springs (damping > critical) to ensure clean settle
without oscillation or need for manual snapping
- Open spring: stiffness=400, damping=42 (critical damping = 40)
- Close spring: stiffness=450, damping=45 for snappier close
- Snap-back spring uses same overdamped settings
The overdamped spring naturally settles at exactly 0/1 without
bouncing, eliminating the need for post-animation value correction.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Spring simulations don't always land exactly at 0/1, causing the toggle
check to fail. Fixed by:
- Using threshold check (< 0.1) instead of exact equality
- Snapping controller value to exact 0/1 when spring settles
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use spring physics for smoother open/close animations
- Add haptic feedback on queue open, close, and swipe start
- Fix swipe-to-close gesture requiring 2 attempts:
- Reduced horizontal detection tolerance (1.5x → 1.2x)
- Added direction locking to prevent accidental cancellation
- Use weighted velocity averaging for smoother detection
- Consider total displacement for slow swipes
- Lowered velocity threshold (300 → 200) for easier close
- Increased position threshold (0.5 → 0.65) for more forgiving close
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Medium impact on drag start (confirms grab)
- Selection click on each reorder (subtle feedback when items swap)
- Light impact on drop (confirms release)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Current item has 8px margin for highlight effect.
Now compensate on BOTH left and right contentPadding:
- Left: Non-current 8+0+16=24px, Current 8+8+8=24px
- Right: Non-current 8+0+8+14=30px, Current 8+8+0+14=30px
Album art and icons now align consistently across all items.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Current item has 8px margin for highlight effect
- Non-current items have no margin
- Use contentPadding right=0 for current, right=8 for others
- This compensates so both have equal 30px gap from icon to edge
- Reduced from original 38px (non-current) and 46px (current)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Keep 48x48 touch target (Material Design guideline)
- Wrap play arrow in matching 48x48 SizedBox for visual alignment
- Remove duration right padding (48px box provides natural spacing)
- Both current track and other tracks now have consistent icon positioning
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Reverted contentPadding and alignment changes
- Reduced SizedBox width from 48 to 32 (kept height 48 for vertical touch area)
- This brings drag handle closer to duration while maintaining touch target
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changed Center to Align with centerRight alignment so the drag handle
icon aligns with the play arrow position on the current track.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Set custom contentPadding on ListTile to reduce right padding from
default 16px to 8px, bringing drag handles closer to the edge.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add _formatDuration helper method for displaying track duration
- Move drag handle from leading to trailing position
- Display duration text next to drag handle
- Show play arrow icon for current item instead of drag handle
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
GestureDetector was losing to Dismissible in the gesture arena.
Listener uses raw pointer events and doesn't compete.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add primaryColor to PlayerExpansionState
- Pass adaptive primary color from expanded player to nav bar
- Blend nav bar icon color during expansion animation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix nav bar icon color to use Material You when enabled (was falling back to lilac)
- Fix stale adaptive colors hanging on when returning to home
- Only use adaptive colors when they actually exist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Wrap drag handle in GestureDetector with larger padding (12h x 16v)
- Use HitTestBehavior.opaque for reliable touch detection
- Handle all pan events in GestureDetector for smoother dragging
- Remove redundant refresh button from header (queue syncs via WebSocket)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
**Drag position fix:**
- Added GlobalKey to Stack for accurate position tracking
- Calculate overlay position relative to Stack, not screen
- Track touch offset within item for smooth finger following
- Works correctly when list is scrolled down
**Clear queue feature:**
- Added delete_sweep button in queue header
- Added queueCommandClear API method (player_queues/clear)
- Optimistic update clears list immediately
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Based on detailed audit, fixed dual gesture system conflict:
**Root cause:** Two gesture systems (GestureDetector + Listener) were
fighting to control the same animation controller, causing jitter and
stuck states.
**Solution:** Use ONLY the Listener approach (bypasses gesture arena):
- Removed GestureDetector horizontal drag handling for queue panel
- Removed unused state variables and methods
- Fixed Listener to handle direction changes (snap back when vertical)
- Added proper instantaneous velocity calculation
- Simplified animation callbacks in expandable_player
**Other cleanup:**
- Removed dead code (_listKey, _itemListsEqual, _buildAnimatedItem)
- Fixed _pendingReorder timer stacking with cancellable Timer
- Proper state cleanup in _startDrag and _resetSwipeState
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add swipe callbacks to QueuePanel (onSwipeStart, onSwipeUpdate, onSwipeEnd)
- Use Listener for raw pointer events to bypass gesture arena
- Wire callbacks to expandable_player's queue panel animation controller
- Swipe-to-close now works on tracks, not just header
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add _optimisticCurrentIndex state to show tapped track as current immediately
- Update _buildQueueList to use optimistic index when set
- Clear optimistic state when server catches up or after timeout
- Provides instant visual feedback when user taps a track
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add state variables for queue panel horizontal drag tracking
- Create _handleQueuePanelDragStart/Update/End/Cancel methods
- Wire up horizontal drag handlers when queue panel is open
- Remove redundant swipe-to-close from QueuePanel itself
- Queue panel now follows finger when swiping right to close
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Block onVerticalDragStart/Update/End when _isQueueDragging
- Use Offstage instead of conditional rendering to preserve QueuePanel state
- QueuePanel stays mounted when hidden, keeping _pendingReorder and _items
- Add onDraggingChanged callback to QueuePanel
- Block swipe-to-close in expandable_player while queue is dragging
- Add _pendingReorder to prevent state revert after API call
- Listener on drag handle only starts drag
- Listener wrapping Stack captures move/up/cancel events
- This prevents gesture loss when finger moves off the small drag handle
The ListView was stealing the drag gesture after a brief moment,
causing the dragged item to freeze in place.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace OverlayEntry with Stack+Positioned within the widget tree.
This should fix the drag overlay not following the finger properly.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Track absolute finger position instead of deltas
- Calculate target index from total offset
- Prevent didUpdateWidget from reverting during drag
- Fix cancel to restore original position
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove great_list_view package (alpha, abandoned, drag bug) and implement
custom drag-to-reorder using Overlay for dragged item. Uses ListView.builder
with manual drag state management.
Avoids:
- ReorderableListView (grey screen bug)
- great_list_view (drag duplication bug)
Features preserved:
- Swipe left to delete (Dismissible)
- Drag handle for reorder
- Right swipe to close panel
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add documentation for all features since v2.0.0:
- Podcasts and Radio Stations
- Multi-room grouping
- Volume precision mode
- Queue improvements (drag/swipe)
- Search enhancements (fuzzy, history, quick actions)
- Letter scrollbar
- HA add-on setup instructions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Clarify that users need the Music Assistant URL (port 8095), not the
Home Assistant URL. Add instructions for finding Base URL and notes
about remote access setup.
Closes#31🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove darkening and desaturation visual effects from precision volume control,
replacing with simple haptic feedback when entering precision mode. Cleaner UX
that doesn't obscure the UI while still providing clear tactile confirmation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Added PlaylistCard and PlaylistRow widgets with hero animations
- Added RadioStationCard and RadioStationRow widgets with hero animations
- Added hero tags for playlists and radio stations
- Added settings to show/hide the new rows on home screen
- Added provider methods getFavoritePlaylists() and getFavoriteRadioStations()
- Added localization strings for the new rows
- Radio stations use circular images like artists
- Playlists use square images like albums
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Mini player now darkens/desaturates when player list card is in precision mode
- Player list cards darken/desaturate when another card or mini player is in precision mode
- Content behind mini player darkens when in precision mode (during reveal)
- Uses animated saturation matrix for smooth grayscale transition
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previous implementation mapped absolute finger position to zoomed range,
causing a jump when entering precision mode. Now uses offset from entry
point:
- Records finger X position when entering precision mode
- Movement FROM that position maps to the zoomed range
- Full card width of movement = 10% volume change
- No jump when entering precision mode
Example at 40%:
- Hold still → enter precision mode, finger at position X
- Move full card width RIGHT → volume goes to 50%
- Move full card width LEFT → volume goes to 30%
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Issues fixed:
1. Precision mode now uses zoomed range mapping instead of sensitivity
- At 40% volume, left edge = 35%, right edge = 45% (10% range)
- Full card width control maintained, just zoomed into smaller range
2. Visual feedback now darkens instead of reducing opacity
- Black overlay (60% opacity) instead of reducing card opacity to 30%
- Proper darkening effect as requested
3. Mini player precision mode now darkens all player cards
- Added callback from ExpandablePlayer to GlobalPlayerOverlay
- GlobalPlayerOverlay passes state to PlayerRevealOverlay
- All cards darken when mini player enters precision mode
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Hold still while adjusting volume to enter precision mode (10x more
precise). This helps with low-volume fine-tuning like audiobooks at 3%.
Features:
- Hold still for 800ms during volume drag to enter precision mode
- 10x sensitivity reduction (full swipe = 10% change instead of 100%)
- Other player cards dim when one is in precision mode (spotlight effect)
- New setting to enable/disable precision mode (Settings > Players)
- Works on both player cards and mini player volume swipe
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Status bar icons now use dark icons in light theme (main.dart)
- Expanded player text/primary color fallbacks use colorScheme instead of white
- Placeholder art uses theme-aware colors
- Settings logo uses dark color filter in light theme
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Expanded player uses adaptiveScheme.surface for its background.
Nav bar was incorrectly using primaryContainer or HSL manipulation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Dark mode: lightness 0.10, saturation 0.12 (very dark)
- Light mode: lightness 0.98, saturation 0.04 (nearly white)
Nav bar should be more neutral than player card.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Logs drag start/end with key state values:
- useLocalVolume decision
- hasOverride flag
- time since last drag
- local vs player volume
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use primaryContainer from appropriate light/dark scheme instead
of custom HSL manipulation. This matches how the expanded player
gets its background color, ensuring consistency.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Comprehensive audit prompt for polishing the player system animations
including mini player, expanded player, queue panel, and player selector.
Uses ultrathink and sub-agents for thorough analysis.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Belt-and-suspenders fix: collapse() now checks if queue panel is open
and closes it instead of collapsing the player. This ensures queue
always closes first regardless of how collapse() is called.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The back button handling in home_screen.dart was checking for expanded
player but not for queue panel. Since home_screen's PopScope intercepts
all back presses (canPop: false), the expandable_player's PopScope was
never reached.
Added:
- closeQueuePanel() method to ExpandablePlayerState
- isQueuePanelOpen and closeQueuePanel() static accessors to GlobalPlayerOverlay
- Queue panel check in home_screen.dart back button handler (before player check)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use raw Listener instead of GestureDetector for right-swipe-to-close
detection on the entire queue panel. This bypasses the gesture arena
so Dismissible widgets don't block the gesture.
- Update PopScope to handle both queue panel and expanded player:
back button now closes queue first, then collapses player, then exits.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- PopScope in expandable_player: back button closes queue first
- GestureDetector in queue_panel: swipe right closes queue
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Removing from _items causes AutomaticAnimatedListView to also animate
the removal, conflicting with Dismissible's animation. Let Dismissible
handle the visual removal alone, API updates server.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Always wrap in Dismissible even during measuring phase to maintain
consistent widget tree structure. Only vary image loading based on
measuring flag.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, didUpdateWidget reset _items on every parent rebuild
(reference comparison always true). Now only syncs when switching
to a different player's queue, preserving local state for animations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove onRefresh() after delete to prevent stale data from
bringing back the deleted item (ghost item bug). Local removal
is sufficient - next natural refresh will confirm deletion.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use Builder to get itemContext for notifyStartReorder
- Pass localPosition instead of delta for updates
- Remove unused _draggingIndex state
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use GestureDetector on drag handle icon
- Call controller.notifyStartReorder/Update/Stop on drag events
- Set addLongPressReorderable: false
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use removeWhere with queueItemId instead of removeAt(index)
- Prevents wrong track being removed due to index mismatch
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add init.gradle that removes Maven Central and forces Alibaba mirrors
- Add Gradle cache step to workflow
- Copy init.gradle to ~/.gradle before build
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Set card background to match expanded player background
- Use zero border radius to cover grey from list package
- Remove item from local list immediately on dismiss to prevent
double animation (Dismissible + AutomaticAnimatedListView)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use playerId instead of queueId (PlayerQueue property)
- Fix _formatDuration to accept Duration? type
- Remove dispose() call (AnimatedListController doesn't have it)
- Remove unsupported reorderableOptions parameter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Convert QueuePanel to StatefulWidget with AutomaticAnimatedListView
- Add long-press drag-to-reorder functionality
- Add swipe-left-to-delete (endToStart only)
- Add queueCommandDeleteItem and queueCommandMoveItem API methods
- Show drag handle icon and track durations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use mediaType enum check instead of type check since Podcast
is not a distinct class in the model hierarchy.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove faulty existingTrack.name.contains(' - ') check that
incorrectly marked good metadata as not-proper
- Add protection for malformed titles starting with '- '
- Add debug logging for radio metadata quality comparison
The old check was meant to detect unparsed "Artist - Title" format
but it also blocked legitimate titles like "Jazzy French - La musique
revient vite..." from being recognized as proper metadata.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The cache was being updated correctly with stream metadata, but
_currentTrack (which the UI displays) was only updated for podcasts.
Now also updates _currentTrack for radio when stream metadata arrives
(detected by artist changing from Unknown to actual value).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
For radio streams, MA sends now-playing info in `stream_title` field
(e.g., "Jazzy French - La musique revient vite...") while `title` field
contains the station name (e.g., "Jazz Radio Jazzy French").
Previously, stream_title was only used if title was "Unknown Track".
Now we always prefer stream_title for radio streams, parsing the
"Artist - Title" format properly.
Bump version to 2.8.4-beta+41
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Override Material You ColorScheme backgrounds with app's preferred colors.
Material You generates unpredictable background values that could cause
black screen or inconsistent navigation bar colors.
- Force dark background to 0xFF1a1a1a
- Force dark surface to 0xFF2a2a2a
- Force light background to 0xFFFAFAFA
Bump version to 2.8.3-beta+40
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add .ts.net to _isLocalIpAddress() check so Tailscale URLs get the same
redirect-safe HTTP handling as local IPs. This fixes auth detection
failing to show username/password fields for Tailscale connections.
Fixes#38🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Syncs provider_filter and player_filter from Music Assistant user settings.
Filters library content, search results, and available players based on
the user's MA profile configuration.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Sync player_filter from MA user settings (like provider_filter)
- Apply provider filtering to recent albums
- Add debug logging to diagnose recently_played_items API response
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Syncs player_filter from Music Assistant auth/me endpoint alongside
the existing provider_filter. Filters which players are visible to
the user based on their MA profile settings.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Read provider_filter from auth/me response
- Store and apply filter in MusicAssistantProvider
- Filter library items (artists, albums, tracks, radio, podcasts)
- Filter search results
- Add wrapper methods for playlists and audiobooks with filtering
- Update screens to use filtered wrapper methods
Empty filter = all providers visible (no filtering)
Non-empty filter = only show items from allowed providers
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When adding/removing items from library, invalidate relevant caches
to prevent stale inLibrary status in detail screens:
- Albums: invalidate artist albums, home album caches
- Tracks: invalidate all album/playlist tracks caches
- Artists: invalidate home artist caches
Fix stale database cache issue:
- Add clearCacheForType() - clears cache type before re-syncing
- Add markCachedItemDeleted() - marks specific item as deleted
- Call markCachedItemDeleted() immediately when removing from library
- This ensures removed items don't reappear from database cache
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
loadLibrary() loads from cache first then syncs in background,
causing the UI to show stale data until manual refresh.
forceLibrarySync() forces an immediate sync, bypassing the 5-minute
cache freshness check and ensuring the new filter is applied immediately.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changed the artist filter setting to use the Music Assistant API's
built-in albumArtistsOnly parameter instead of filtering in the UI.
When "Show only artists with albums" is enabled:
- sync_service.dart passes albumArtistsOnly: true to getArtists()
- library_provider.dart passes albumArtistsOnly: true to getArtists()
- Settings screen triggers loadLibrary() when setting changes
Removed the unreliable UI-level filtering that was causing the Selector
widget to not properly rebuild when the setting changed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The Selector widget was blocking rebuilds because it only watched
provider data, but _showOnlyArtistsWithAlbums is a local state
variable. Added a ValueKey that includes the setting value to force
Selector to rebuild when the setting changes.
Also made the settings save async and added verification logging.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Podcast Detail Screen:
- Added action buttons row matching album/artist detail screens
- Play button plays most recent episode
- Play on... button shows device selector for most recent episode
- Library button remains for add/remove from library
Settings:
- Added new "Library" section in settings
- Added "Show only artists with albums" toggle
- When enabled, hides artists that have no albums in library
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changed maProvider.getImageUrl to context.read<MusicAssistantProvider>().getImageUrl
at line 620 where maProvider was no longer defined after switching from context.watch()
to context.select() for Hero animation performance.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Reverted colorScheme.surface back to colorScheme.background in 11 files
(user reported the new color was too light/grey)
- Fixed search screen podcast Hero structure to match library/detail pattern:
- Changed widget order from Container → ClipRRect to ClipRRect → Container
- Changed borderRadius from 8 to 16 (matching library/detail)
- Changed color from surfaceVariant to surfaceContainerHighest
- Removed memCacheWidth constraints to match library behavior
- This ensures smooth Hero animation when navigating from search
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, library updates relied only on optimistic UI updates and
API response timing. This caused sync issues when MA operations timed
out (operations often succeed but take >30 seconds).
Now subscribing to MA server events for real-time library sync:
- media_item_added: Triggers library refresh when items added
- media_item_deleted: Triggers library refresh when items removed
Also added:
- Provider listener in artist details screen for reactive updates
- Debug logging for media events in API layer
- Event handlers that invalidate caches and refresh library data
This ensures the UI stays in sync even when API calls timeout,
since MA server sends events when operations complete.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, library buttons waited for API response before updating.
This caused issues because:
1. MA operations can take >5 seconds and timeout
2. Button appeared stuck even when operation succeeded
3. No snackbar feedback until API completed
Now using optimistic updates:
- UI updates immediately when button is tapped
- Snackbar shows instantly
- API call runs in background
- Only revert on actual errors, not timeouts
(timeouts usually mean MA is still processing successfully)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Two issues fixed:
1. "Not found in library" error handling:
- When trying to remove an item that's already removed, MA returns
"not found in library" error
- Now we treat this as success (item is already not in library)
- Button correctly updates to "add to library" state
2. Search cache invalidation:
- Added invalidateSearchCache() method to CacheService
- Called when items are added or removed from library
- Ensures next search shows correct library status
- Fixes issue where button state was wrong after navigating back
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Track class doesn't have discNumber or trackNumber constructor parameters.
These properties are accessed through metadata, not as direct params.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, toggling favorites or adding/removing from library required
a manual refresh to see changes. Now updates are instant:
Favorites:
- After toggle, local cache is updated with new favorite status
- notifyListeners() triggers immediate UI rebuild
Library:
- Remove: Item is removed from local cache instantly
- Add: Triggers background refresh (500ms delay) to fetch new item
This provides a responsive UX without requiring manual pull-to-refresh.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The getArtists() API defaulted to albumArtistsOnly=true, which filtered
out artists that were added to library but don't have albums there.
Changed to albumArtistsOnly=false in:
- music_assistant_provider.dart loadArtists()
- library_provider.dart loadLibrary() and loadArtists()
- sync_service.dart background sync
Now artists like Ramones will appear in Library even if you only
added the artist (not their albums).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The API calls were working (MA confirming with events), but the UI
wasn't updating because _isInLibrary() still checked the unchanged
item data.
Added local state tracking:
- _addedToLibrary and _removedFromLibrary Sets track changes in session
- _isInLibrary() checks local state first, then falls back to server data
- Add/remove handlers update tracking sets on success
Now buttons visually update immediately when library status changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The library add button was failing because when items already had a
library mapping, the code was falling back to the library provider
instead of using external providers like Spotify.
Changes:
- Fixed _addToLibrary in search_screen.dart to ALWAYS prefer non-library
providers (ignoring availability flag which only affects playback)
- Applied same fix to artist_details_screen.dart, album_details_screen.dart,
and podcast_detail_screen.dart
- Added proper handling for library-only items (show message instead of
sending invalid request)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix URI bug: use providerDomain instead of providerInstance for library operations
- Add expandable dropdown menus for all search result types (long-press to reveal)
- Implement action buttons per media type:
- Artist: Radio, Queue, Favorite, Library
- Album: Play, Play On, Queue, Favorite, Library
- Playlist: Play, Play On, Library
- Audiobook: Play, Play On, Favorite, Library
- Radio/Podcast: Play, Play On, Library
- Add library toggle button to Artist, Album, and Podcast detail screens
- Add API methods: playAlbum, addAlbumToQueue, addArtistToQueue, playPlaylist
- Add localization strings for playingAlbum and playingPlaylist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add music/library/add_item and music/library/remove_item API endpoints
- Add addToLibrary/removeFromLibrary methods in provider
- Add library_add icon button to search tiles (artist, album, playlist, audiobook, radio, podcast)
- Button only shows for items not already in library
- Add localization strings for library actions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Mini player buttons: Show play/skip 30s/power for podcasts/audiobooks when player list is expanded
- Fixed gap between expanded player and nav bar:
- Use viewPadding.bottom to match BottomNavigationBar's calculation
- Use Clip.hardEdge when expanded to avoid anti-alias edge artifacts
- Add 2px overlap to eliminate any remaining subpixel rendering gaps
- Fixed stream quality overlapping chapter info in audiobook expanded player
- Added skip 10 seconds buttons (±10s) to expanded audiobook and podcast controls only
- Skip 10s and 30s buttons now use the same size for visual balance
- Removed verbose podcast name logging causing white flash behind mini player
- Fixed radio streams showing "Unknown Artist" - now extracts artist from stream metadata
- Added letter scrollbar to Podcasts and Radio library views
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add missing heroTag to ArtistAvatar in grid view - list view had it but grid view was missing it entirely, causing fade instead of hero animation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add SharedPreferences persistence for iTunes podcast cover URLs
- Load cached URLs on app start (before UI renders) for instant high-res
- Save new iTunes URLs as they're fetched for future launches
- Change iTunes artwork from 1400x1400 to 800x800 (still crisp, smaller payload)
- Remove useStableUrl workaround (no longer needed with persistence)
This ensures:
- Smooth hero animations (same URL in library and detail)
- High-res podcast covers (iTunes 800x800)
- Instant display on repeat launches (URLs loaded from storage)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
ROOT CAUSE IDENTIFIED:
Albums/artists/audiobooks use getImageUrl() which ALWAYS returns the same URL.
Podcasts use getPodcastImageUrl() which can return DIFFERENT URLs:
- Before iTunes lookup: imageproxy URL
- After iTunes lookup: iTunes 1400px URL
This causes hero animation failures because:
1. Library displays imageproxy URL image
2. iTunes lookup completes asynchronously
3. User taps - getPodcastImageUrl now returns iTunes URL
4. Detail screen gets different URL than library is showing
5. Cache miss during hero animation = choppy
FIX:
- Added useStableUrl parameter to getPodcastImageUrl()
- When true, bypasses iTunes cache and always returns imageproxy URL
- Library now uses useStableUrl: true for all podcast images
- Precaching also uses useStableUrl: true
- This ensures SAME URL in library, precache, and detail screen
Also removed memCacheWidth from library (decode at native resolution).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Critical fix: List view had different structure from grid/detail:
- List: ClipRRect(8) → CachedNetworkImage (no Container wrapper)
- Grid: ClipRRect(16) → Container → CachedNetworkImage
- Detail: ClipRRect(16) → Container → CachedNetworkImage
Hero animations require IDENTICAL widget structures to morph smoothly.
Now all three match:
- borderRadius: 16 (was 8 in list view)
- Container wrapper with surfaceContainerHighest color
- CachedNetworkImage with fadeIn/fadeOut Duration.zero
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Hero Animation Fixes (matching album pattern exactly):
- Increase podcast detail cover size from 0.6/280px to 0.7/320px (matches albums)
- Use DecorationImage with CachedNetworkImageProvider instead of CachedNetworkImage
widget (matches album detail screen exactly)
- Remove memCacheWidth/Height constraints for full resolution display
- Add box shadow to match album detail screen aesthetic
- Update border radius to 16px in both library grid and detail screen
Search Ranking Improvements:
- Calculate "prominence" of query in podcast name (query length / name length)
- Higher boost (+15) when query is >50% of podcast name
- Medium boost (+12) when query is 30-50% of name
- Base boost (+8) for other matches
Example: "Louis Theroux Interviews" with query "Louis Theroux"
- Query is 14/23 = 60% of name -> +15 boost
- Combined with word boundary match (60) = 75 points total
- Should rank above tracks with artist match (55 points max)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Hero Animation Fixes:
- Change getPodcastImageUrl default size from 1024px to 256px to match
albums/artists and ensure cache consistency for smooth hero transitions
- Add podcast image pre-caching in library screen (matches album pattern)
Search Relevance Fixes:
- Fix _matchesWordBoundary() to handle multi-word queries like "Louis Theroux"
- Add podcast metadata checks for owner/creator fields (not just author/publisher)
- Increase podcast description bonus from +3 to +5 (matches other media types)
- Add fallback boost (+10) for podcasts when multi-word query matches in name
but no creator metadata exists (podcast names often include host name)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Hero Animation Fixes:
- Align memCacheWidth/Height to 256 across library, search, and detail screens
- Ensures same cached image is used for hero source and destination
Search Relevance Improvements:
- Add author/narrator scoring for Audiobooks (+15 exact, +8 contains)
- Add author/publisher/description scoring for Podcasts
- Podcasts matching creator name now rank higher (fixes Louis Theroux issue)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add podcastSingular localization key
- Use singular labels in type pills (Podcast not Podcasts)
- Fix pill stretching for artist/radio/podcast with mainAxisSize.min
- Increase pill size (8x3 padding, 11px font)
- Align memCacheWidth/Height to 400 across library and detail screens
for smoother hero animations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add searchPodcasts API method and include podcasts in search results
- Add podcast tile with navigation to podcast detail screen
- Add colored type pills to distinguish media types in "All" view:
- Track (blue), Album (purple), Artist (pink), Playlist (teal)
- Audiobook (orange), Radio (red), Podcast (green)
- Fix choppy hero animations for podcast covers by adding memCacheWidth/memCacheHeight constraints
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Episode list redesign:
- 72x72 episode covers (each can have unique artwork)
- Duration + release date on same line
- Long-press to expand: full description + action buttons
- Action buttons match album track style (44x44 icon-only)
Podcast cover quality fix:
- Fetch high-res artwork from iTunes API (1400x1400)
- Works for any podcast listed in iTunes (most podcasts)
- Correct podcast artwork, not episode covers
- Cached in background after podcast list loads
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Compare podcast and episode image URLs to determine if they're the
same source image. If same source, use episode version for higher
quality. If different sources (unique episode covers), keep the
podcast's own cover to avoid incorrect artwork.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously we always overwrote podcast covers with episode covers,
which was wrong for podcasts where each episode has a unique cover.
Now we check if the podcast has its own image first and only fall
back to episode covers when the podcast itself has no cover image.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace play icon with actual episode covers (72x72, rounded)
- Each episode can have its own unique cover art
- Add release date display alongside episode duration
- Long-press to expand episode showing full description
- Expanded state reveals action buttons: Play, Add to Queue, Play On...
- Add debug logging for episode metadata to discover date field names
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Updated podcast_detail_screen.dart to use getPodcastImageUrl()
for both the cover display and color extraction.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Music Assistant sometimes returns low-res images for podcasts. Added:
- Podcast cover cache populated with episode covers in background
- getPodcastImageUrl() method that checks cache first
- Library screen now uses getPodcastImageUrl for podcasts
Covers are loaded progressively after initial podcast list loads,
so the UI is not blocked while fetching episode data.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When podcast episode titles contain " - " (like "Show 73 - Mania for
Subjugation III"), the title was being split and "Show 73" became the
artist. The caching logic then kept the "existing" track thinking it
had better data, but this caused the album (podcast name) from the
player_updated event to be lost.
Fix: For podcast URIs, if the new track has album info but existing
doesn't, either prefer the new track or merge the album data into
the existing track. Also update _currentTrack when we have better
album data for podcasts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Log all currentMedia fields for podcast episodes
- Extract and store podcast field in metadata
- Add helper method for episode cover fallback
- Helps diagnose podcast name detection issues
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Hide album line for podcasts (was duplicating podcast name)
- Add debug logging to understand podcast data structure
- Helps diagnose why some podcasts show "Podcasts" fallback
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add isPlayingPodcast getter to detect podcast episodes by URI pattern
- Add currentPodcastName getter to show podcast name instead of artist
- Use audiobook-style skip 30s controls for podcasts in expanded player
- Display podcast name in artist line instead of "Unknown Artist"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Found the actual API endpoint from Music Assistant documentation.
Simplified the code to use the proper endpoint.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Check metadata for episodes array
- Use provider_mappings to fetch from source provider (Spotify)
- Log total_episodes and metadata keys for debugging
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use the podcast's uri from API response instead of constructed path
- Filter out folder navigation items (like "..")
- Add _parseBrowseEpisodes helper method
- Include duration parsing for episodes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Parse media_type string to MediaType enum properly
- Default to MediaType.track if parsing fails
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Log browse item structure (keys, name, label, uri, path, media_type)
- Parse browse items with fallback for name/label fields
- Add URI logging to play method for debugging
- Better handling of browse response format
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use CachedNetworkImageProvider for color extraction
- Change _formatDuration to accept Duration instead of int
- Replace non-existent podcastOff icon with microphoneOff
- Fix duration comparison to use Duration.zero
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create PodcastDetailScreen with podcast cover, title, author
- Load and display episode list with play functionality
- Add hero animations between library and details screens
- Add expandable description section
- Add podcastCover hero tag constant
- Pass imageUrl parameter to navigation for adaptive colors
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add getPodcasts, getPodcastEpisodes, and playPodcastEpisode API methods
- Add podcast state (podcasts list, isLoadingPodcasts) to provider
- Replace "Coming soon" placeholder with functional podcasts tab
- Add list and grid view modes for podcasts (2-column and 3-column)
- Add view mode toggle support for podcasts
- Persist podcast view mode preference in settings
- Add localization strings for podcasts (noPodcasts, addPodcastsHint, episodes)
- Tapping a podcast shows placeholder message (details screen coming later)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add proper spacing between radio station items in list view
- Add grid view support (2-column and 3-column layouts)
- Add view mode toggle button functionality for radio tab
- Persist radio view mode preference in settings
- Add pull-to-refresh support
- Optimize image caching based on view mode
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add searchRadioStations() method that searches with media_types=['radio']
- Combine library radio filtering with global search results
- Deduplicate results by name, preferring library items
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The Music Assistant search API doesn't return radios, so filter
from the provider's pre-loaded radioStations list instead.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Parse radios from search API response
- Add radio filter tab to search screen
- Add radio tile with play functionality
- Add playingRadioStation localization
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add 200ms delay before fetching fallback images to avoid
network requests during fast scrolling
- Use dynamic image cache sizes based on view mode:
- List view: 128px (smaller thumbnails)
- 3-col grid: 200px
- 2-col grid: 300px
- Increase ListView cacheExtent from 500px to 1000px to
pre-build more offscreen items
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds _FastPageScrollPhysics to both library and search screens
to reduce the time PageView animations take to settle. This makes
vertical scrolling within tabs more responsive after swiping
horizontally between pages.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously all 3-4 tabs were built upfront with PageView(children:).
Now only the current tab is built, with adjacent tabs for smooth swiping.
This should significantly improve tab swipe performance.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
RestorableInt doesn't implement ValueListenable, so add separate
ValueNotifier for UI updates while keeping RestorableInt for state
restoration.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Queue color extraction requests with 300ms debounce
- Process extractions in batch after scroll settles
- Add 50ms yield between extractions for UI responsiveness
- Add mounted checks to prevent work after disposal
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Only update scroll fraction when it changes by more than 1%, reducing
the number of setState calls and widget rebuilds during scrolling.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add cached sorted lists for playlists, tracks, audiobooks, authors, series
- Sort once when data is loaded, not on every widget rebuild
- Pre-group audiobooks by author when loaded
- Re-sort audiobooks when sort order changes (alpha/year)
This eliminates O(n log n) sorting operations during scroll/swipe animations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove setState from _onPageChanged in both library and search screens
- Use ValueNotifier + ValueListenableBuilder for filter chip updates
- Only filter chips rebuild during swipe, not entire screen
- Remove redundant _selectedCategoryIndex in library screen
This significantly reduces widget rebuilds during swipe animations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Content now fades out as it scrolls under the filter bar instead of
abruptly meeting it. Uses a 24px gradient from background color to
transparent on both library and search screens.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use AnimatedPositioned with 50ms duration to smooth out the thumb
tracking during scroll, eliminating jerky/stuttery motion.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Increased top padding on list/grid views in library and search screens
so there's a visible gap when content scrolls under the tab bars.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Thumb now tracks scroll position instead of staying centered
- Added onDragStateChanged callback to prevent tab bar hiding during drag
- Made grab area wider (32px) for easier interaction
- Adjusted letter popup position for wider grab area
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix vertical alignment of text in filter tab row (center aligned)
- Only hide library type bar when scrolling, keep filter tab always visible
- Use Container with Alignment.center for proper text centering
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create LetterScrollbar widget that shows a letter popup when dragging
- Apply to library screen lists: Artists, Albums, Playlists, Authors, Audiobooks, Series
- Dragging the scroll thumb shows the current letter in a popup bubble
- Jump to items starting with that letter for quick navigation
- Closes#36🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Library screen:
- Reduce filter row height from 44px to 36px
- Add 8px spacing between library type and filter rows
- Remove bottom border line from filter bar
- Add scroll-to-hide behavior (like search screen)
Search screen:
- Reduce filter bar height from 44px to 36px
- Adjust padding to match smaller height
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Library screen:
- Justify media type chips across full width (spaceBetween)
- Reduce vertical spacing between rows (fixed 44px height)
- Removed redundant padding
Search screen:
- Filter bar now matches search bar width with rounded corners (12px)
- Content clips and scrolls within the rounded container
- Same 44px height as library screen for consistency
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove full-width background from media type row
- Show icons only when media type is selected (saves horizontal space)
- Use tertiaryContainer for selected state (complementary color)
- Move action buttons (favorites, view mode) inline with sub-category row
- Revert search screen to inline library toggle in search bar
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Library screen: replace dropdown with two-row chip layout
- Row 1: Media type chips (Music, Audiobooks, Podcasts, Radio) with secondaryContainer background and icons
- Row 2: Sub-category chips (Artists, Albums, Playlists, etc.)
- Move favorites and view mode toggles to floating FABs in bottom-right above mini player
- Search screen: move library-only toggle from search bar to floating FAB
- Remove old dropdown-based media type selector
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace _tabController.index with _selectedCategoryIndex
- Fix metadata access to use Map syntax (metadata?['description'])
- Add playRadioStation method to API
- Use maProvider.api?.playRadioStation instead of undefined playMedia
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace media type icon buttons with dropdown selector
- Replace TabBar with horizontal scrollable chips (matching search screen)
- Remove PlayerSelector from library AppBar (redundant with mini player)
- Add Radio as a new library media type
- Add getRadioStations API endpoint
- Add loadRadioStations to provider
- Add radio stations tab with list view
- Add localization strings for radio, stations, selectLibrary
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Power button now matches PlayerCard style (IconButton, no background)
- Album details: reduce bottom padding from 24px to 12px
- Mini player: raise secondary line from y=42 to y=40
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add power toggle button on right side (2-line off state only)
- Increase primary font size from 16px to 18px in 2-line mode
- Evenly space the two text lines vertically (14px and 42px)
- Power button shows powered state with opacity change
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add 8px total to row height formula for 4th row behind mini player
- Add clipBehavior: Clip.none to album row for descender overflow
- Add 4px bottom padding to album cards for descender room (y, g, j, q)
- Tighten artist card line height to 1.15 for 2-line names
- Increase detail screen artwork from 40-50% to 70% on all screens
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Accessibility:
- Add Semantics to player control buttons (previous, play/pause, next)
- Include semantic labels and hints for screen readers
- Play/pause button includes "long press to stop" hint
Touch Targets:
- Increase 28dp touch targets to 44dp in player_card.dart
- Increase 28dp touch targets to 44dp in expandable_player.dart
- Comments note icons remain 28dp for visual balance
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Memory Management:
- Add LRU eviction to all caches with configurable max sizes
- Fix static memory leak in Player model with proper LRU cleanup
- Add PCM buffer overflow protection (max 500 chunks / ~10s)
Database Performance:
- Add indexes for RecentlyPlayed (profile, played_at)
- Add indexes for LibraryCache (itemType, isDeleted)
- Add indexes for CachedQueue (playerId, position)
- Bump schema version to 5 with migration
Race Conditions:
- Add connection deduplication guard in SendspinService
- Complete pending API requests on disconnect instead of abandoning
- Add position tracker anchor timeout (30s max drift)
Error Handling:
- Add catchError to fire-and-forget pause/skip commands
- Log unawaited errors instead of silently dropping them
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Device selector now shows two hints:
- "Long-press to sync" (shortened from "Tap and hold a player to sync")
- "Swipe to adjust volume" (new)
Updated all three language files (en, de, es).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Cast players (chromecast provider) don't support group commands - only
their Sendspin counterparts do. The previous fix incorrectly tried to
use Cast UUIDs as leaders, which caused "Player X does not support
group commands" errors.
Now we translate BOTH the target AND leader to Sendspin IDs when syncing,
since only Sendspin players support the SET_MEMBERS feature required for
grouping.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This debug log was firing on every UI build cycle for every player card,
creating excessive noise in the logs.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Pure Sendspin CLI players (like sendspin-cli-dining-room-hifi) don't
implement the SET_MEMBERS feature required for ungrouping. When unsyncing
such a player fails with "set_members needs to be implemented", fall back
to unsyncing via the leader instead. This dissolves the entire group but
at least provides a working path.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When syncing a player to the leader, use the Cast player UUID instead of
the Sendspin ID. This may help prevent MA from switching to the Sendspin
player's queue (which can have stale content like old audiobooks).
The root cause is a Music Assistant server bug where the Sendspin player
has a separate queue from the Cast player, and syncing with the Sendspin
ID as leader causes MA to start streaming from the Sendspin queue.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When long-pressing a child player to unsync it, the code was incorrectly
calling unsync on the leader instead, which caused the leader to leave
the group while the child remained synced to another player.
The flawed logic tried to "unsync via leader first" for child players,
but calling ungroup on a leader dissolves the entire group rather than
removing a specific child.
Now unsyncs the child directly, which correctly removes just that player
from the sync group.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove redundant "Tracks" heading from album detail view
- Update album_details_screen to use shared showPlayerPickerSheet
- Update audiobook_detail_screen to use shared showPlayerPickerSheet
- All Play On menus now have status indicators, drag handle, and theming
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add centered 2-line layout for non-playing state (player off)
- Add tertiaryText parameter to MiniPlayerContent for 3-line support
- Peek content now shows 3 lines when player has track, 2 centered when not
- DeviceSelectorBar uses centered 2-line layout (no empty third line)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Volume swipe already uses incremental deltas - drag right increases,
drag left decreases. Multiple swipes accumulate as each starts from
current volume. Removed unused _dragStartX variable and added comments
to clarify this behavior.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move volume overlay to end of Stack children so it renders on top of
all content (album art, track info, buttons) when adjusting volume.
Matches PlayerCard behavior where overlay completely covers the card.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Only send volume updates every 150ms during drag
- Send final volume value on release
- Visual feedback still updates immediately
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When device reveal overlay is visible:
- Mini player horizontal swipe = volume control (two-tone overlay)
- Live volume changes as you drag
When device reveal is closed:
- Mini player horizontal swipe = change player (existing behavior)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Mini player refinements:
- Reduce height from 76px to 72px
- Adjust line spacing (secondary at 27px, tertiary at 46px)
- Remove player name swap on primary line when reveal visible
(no longer needed since player name is now on line 3)
Volume swipe on PlayerCards:
- Horizontal swipe adjusts volume with live feedback
- Two-tone overlay covers card while dragging (same colors as progress bar)
- Volume changes apply immediately as you drag
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Increase mini player height from 64px to 76px
- Increase album art to 76x76 to fill vertical space
- Add player name as third line (same size as artist, same color as track)
- Refactor BottomSpacing to reference MiniPlayerLayout.height
- Refactor _collapsedHeight and _collapsedArtSize to reference MiniPlayerLayout
Single source of truth for mini player dimensions now.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Key changes:
- Cast-to-Sendspin ID mapping and persistence
- Sync fixes for unavailable/mixed player types
- Manual sync yellow border improvements
- Player name pill removed (didn't work out)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The pill experiment didn't work out - removing it entirely.
Keeps all the Sendspin sync fixes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When we discover a Cast+Sendspin player pair, save the mapping to
the database. On startup, load persisted mappings into memory.
This allows syncing to work even when the Sendspin player is
unavailable (device off) - we remember that it has Sendspin enabled.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a Cast device with Sendspin is powered off, its Sendspin player
becomes unavailable and won't be in _castToSendspinIdMap. Now we also
check if the computed Sendspin ID exists in any sync group's members
(like "All Speakers") to identify players with Sendspin enabled.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Only translate Cast IDs to Sendspin IDs when the player is
actually in _castToSendspinIdMap. Cast players without Sendspin
enabled should use their original UUID for group commands.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When unsyncing a child player (like Sendspin CLI), the direct unsync call
fails with "set_members needs to be implemented" because the player doesn't
support that feature.
Fix: When unsyncing a child player, first try to unsync via the leader
(which does support set_members). This translates the syncedTo Cast UUID
to the Sendspin leader ID and calls unsync on that instead.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When fetching the queue for a Sendspin player (e.g., cast-7df484e3),
we need to translate it to the corresponding Cast UUID (e.g.,
7df484e3-d2ee-c897-f746-2dffc29595ff) because MA stores queues under
the Cast UUID, not the Sendspin ID.
This was causing the mini player to show "off state" for Shield after
syncing another player to it - the queue fetch was returning empty
because we were querying with the wrong player ID.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a Sendspin player (e.g., Shield) is selected and synced to by another
player, the mini player was showing "off state" because track cache lookups
were failing. The issue was that track events come from MA with Cast UUIDs
but the selected player uses Sendspin IDs (cast-xxxxxxxx format).
Changes:
- Add getAllCachedPlayerIds() to cache service for dynamic ID lookup
- Add dual-caching: when caching a track for a Cast player, also cache
under its computed Sendspin ID (cast-{first 8 chars of UUID})
- This applies to _handlePlayerUpdatedEvent, _preloadPlayerTrack, and
_updatePlayerState where tracks are cached
- Add dynamic Sendspin ID computation when looking up tracks, trying
both the map and computed IDs as fallbacks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Changed right: 1 to right: 0 (moves pill right 1px)
- Use tertiaryContainer for background (darker complementary color)
- Use onTertiaryContainer for text color
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1. Track cache lookup now checks translated Cast<->Sendspin IDs
2. Player state updates preserve the renamed name (without "(Sendspin)")
by using processed player from _availablePlayers
3. Player update events now trigger state refresh for translated IDs
(events may come with Cast ID but selected player is Sendspin)
This fixes:
- Mini player showing "Shield (Sendspin)" instead of "Shield"
- Mini player showing off state when Shield is selected and synced
- Track info not loading for Sendspin players
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1. Yellow border now shows on synced children even when leader is
Sendspin version (translated ID lookup in isPlayerManuallySynced)
2. Selected player no longer jumps after sync when Cast player gets
replaced by Sendspin version (ID translation in player selection)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use primaryContainer/onPrimaryContainer instead of tertiary colors
for a more subtle, unified appearance with the mini player.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The previous fix only worked when the Sendspin player was already
visible in the player list. But Cast players need their Sendspin
counterpart for group commands, even before Sendspin playback starts.
This fix:
- Computes Sendspin ID dynamically for chromecast players: cast-{first 8 chars of UUID}
- No longer relies on _castToSendspinIdMap having a pre-built mapping
- Works for both Kitchen speaker and Shield (any Cast+Sendspin device)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Some Cast devices (like Shield) have their Sendspin player registered
with a raw ID as the name (e.g., "cast-7df484e3") instead of the
standard "DeviceName (Sendspin)" format.
This fix:
- Detects Sendspin players by ID pattern (cast-{uuid-prefix})
- Maps Cast player IDs to Sendspin IDs for these raw-named players
- Enables sync TO these Cast devices by translating the leader ID
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Move pill 1px left to hide sharp corner behind mini player curve
- Match pill border radius to mini player (16px) for seamless look
- Increase left padding to accommodate larger radius
- Raise text up (top padding 5→3) for better vertical balance
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When syncing to a Chromecast player that has Sendspin enabled,
the leader player ID also needs to be translated from the Cast ID
to the Sendspin ID. Previously only the target was translated.
This fixes "Player Shield does not support group commands" error
when trying to sync other players to a Cast+Sendspin player.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When syncing to a Chromecast player that has Sendspin enabled,
the leader player ID also needs to be translated from the Cast ID
to the Sendspin ID. Previously only the target was translated.
This fixes "Player Shield does not support group commands" error
when trying to sync other players to a Cast+Sendspin player.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Pill renders BEHIND mini player (first in Stack)
- Tertiary color for contrast
- Extends down behind player with bottom padding
- Mini player's curved corner reveals the "tail" naturally tapering
- Top corners rounded, bottom straight for seamless extension
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Top right position, flush with right edge
- Same background color as mini player (collapsedBgUnplayed)
- Progress bar flows through the pill matching main player
- Only top-left corner rounded, right side matches player border radius
- Creates seamless integrated appearance
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Position at top right instead of centered below
- Right edge flush with mini player edge
- Slightly larger (font 12, padding 12x5)
- Reduced border radius (8) for smoother transition
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Reverted workflow to not use --build-number flag
- Added +35 build number to pubspec.yaml (higher than previous 31)
- Build numbers should be manually incremented in pubspec.yaml for each release
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use GitHub run_number as the build number to ensure each build has an
incrementing versionCode. This fixes the "newer version already installed"
error when upgrading.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The player name pill was inheriting some weird text style from parent widgets.
Added explicit fontFamily: 'Roboto' to match the app's design.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Wrap Container in outer Stack with Clip.none, place pill in outer Stack
so it's not clipped by Material's Clip.antiAlias
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Move pill to end of Stack children so it renders on top
- Add explicit TextStyle properties to prevent inherited styling:
- decoration: TextDecoration.none (removes underline)
- fontFamily: null (uses default font, not monospace)
- letterSpacing: 0
- Slightly larger: 11px font, 12px radius, more padding
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Pill shows selected player name below the mini player
- Uses adaptive tertiary color for complementary look
- Fades out smoothly during player expansion
- Only visible when track is playing (DeviceSelectorBar already shows player name when idle)
- Positioned at bottom: -10px, right: 8px to attach beneath mini player
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Pre-configured group players (like "All Speakers") have groupMembers that
list the child players, but the group's own ID is NOT in that list.
Manual sync groups have the leader's ID IN the groupMembers list.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Move player_group check to top of isPlayerManuallySynced() to prevent
edge cases where group players could incorrectly show yellow border
- Add onTap handler to _buildDeviceSelectorBar to dismiss player reveal
when tapping selected player that has no track (matching morphing player)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Updated isPlayerManuallySynced to detect:
- Case 1: Player is a child synced to a regular player (not a group)
- Case 2: Player is a leader with groupMembers (but not a player_group provider)
This ensures both players in a manual sync show the yellow border,
while pre-configured groups like "All Speakers" show no border.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The second PlayerCard in player_reveal_overlay.dart was using
player.isGrouped directly instead of the provider's isPlayerManuallySynced
method. This caused group players like "All Speakers" to incorrectly show
the yellow synced border. Now both PlayerCard instances use the same
correct logic that only highlights manually synced players.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add isPlayerManuallySynced() helper in provider to check if syncedTo target is a regular player
- Pre-configured MA speaker groups (provider='player_group') no longer show yellow border
- Fix PlayerCard border clipping by using Stack with border overlay on top
- Update all widgets to use provider method for sync detection
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Tapping active player when list expanded now collapses the list
- Scribe player shows Play/Next/Power buttons when list visible (matching other players)
- Yellow synced border renders on top using foregroundDecoration (fixes corner clipping)
- Yellow border only shows for manually synced players, not pre-configured MA groups
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Root cause: After toggling shuffle, the queue comparison only checked
metadata (length, currentIndex, shuffle state). When the optimistic
update set shuffleEnabled=true with old items, and the server returned
shuffleEnabled=true with reordered items, the comparison showed "no change"
because both had the same shuffle state.
Changes:
- Add item reordering detection by comparing first 3 queue item IDs
- Add 150ms delay after toggle API call to ensure server finishes reordering
- Add debug logging to track reordering detection
Fixes queue display not updating when shuffle is toggled on/off.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Apply optimistic UI updates for shuffle and repeat toggles:
- Immediately update local queue state before API call
- Prevents cached queue from overwriting optimistic state
- Ensures button color changes instantly on tap
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
fix(#21): prevent early return on coldStart for auto-selection priority
The shuffle button was failing because the Music Assistant API requires
the shuffle_enabled boolean parameter. Added the parameter through all
layers (API → provider → widget).
The auto-selection wasn't applying Priority 2 (local player) when
"Prefer Local Player" was disabled because an early cache-valid return
was bypassing the cold start priority logic.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Expose app_id on Player model and log it for playing players
to help diagnose why Spotify Connect isn't detected as external source.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a player is using an external source (Spotify Connect, TV optical,
etc.), Ensemble was showing stale MA track data instead of clearing it.
Changes:
- selectPlayer(): Clear _currentTrack for external source players
- selectPlayer(): Skip position tracker updates for external sources
- _preloadPlayerTrack(): Skip preloading for external source players
- _updatePlayerState(): Early return for external sources, clearing
track, audiobook context, and notification
- player_selector.dart: Exclude external sources from badge count
- Auto-selection logic: Exclude external sources from "playing player"
priority in both music_assistant_provider and player_provider
The isExternalSource flag (detecting app_id != 'music_assistant') was
already being set correctly in the Player model, but wasn't being used
throughout the app to prevent showing stale data.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace random UUID with ANDROID_ID (survives storage wipes)
- Include MA username in player ID for multi-device support
- Auto-detect Phone vs Tablet based on screen size (600dp threshold)
- Format: ensemble_{username}_{deviceId8}
- Prevents duplicate players when clearing app storage
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Only rebuild when clear button visibility changes (not every keystroke)
- Use context.select for connection status to avoid unnecessary rebuilds
- Cache available filters list to avoid recalculating per build
- Use theme-aware colors for filter chips (light/dark mode support)
- Remove dead code (_buildListItems method - 88 lines)
- Use safe .whereType<T>() instead of unsafe .cast<T>()
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Filter bar only scrolls when active filter would be off-screen
- Changed from Stack overlay to Column layout
- Results no longer scroll behind filter bar
- Filter bar hides on scroll along with search bar
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Only builds visible player cards (lazy loading)
- Uses itemExtent for optimal scroll performance
- Reduces animation work from O(n) to O(visible) per frame
- Should fix choppy animation reported by shpongledsummer
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Swipe up was incorrectly moving cards down because delta negation
was inverting the direction. Now:
- Swipe up (finger up): ignored when not scrollable
- Swipe down (finger down): triggers dismiss gesture
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add scroll when player list exceeds 80% of available screen height
- Swipe down dismisses only when scrolled to top
- When scrolled down, swipe gestures scroll the list instead
- No behavior change for small player counts (5-6 players)
This matches the common pattern used by Instagram, Apple Maps, etc.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The scrollability changes were interfering with the drag-to-dismiss
gesture. For normal player counts (5-6), scrolling isn't needed.
Issue #23 (many players overflow) needs a different approach.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
## Issue Fixes
### #27 - DLNA external source detection
- Add app_id check as primary indicator for external sources
- When app_id != 'music_assistant', mark as external source
- Keeps existing URI-based detection as fallback
### #23 - Speaker selection scrollability
- Add top constraint to player overlay for max height
- Wrap player list in SingleChildScrollView with Flexible
- Enable scrolling when more than 5 players (BouncingScrollPhysics)
- Prevents player cards from overflowing on devices with many speakers
### #21 - Auto-selection debugging
- Add detailed logging when Priority 2 (local player) fails
- Log whether local player is missing from list or not available
- Log when builtinPlayerId is null
## Enhancements
### Hero animations for search results
- Add hero animations to album, artist, and audiobook tiles
- Use FadeSlidePageRoute for smooth transitions
- Use 'search' suffix for hero tags to avoid conflicts
- Update adaptive colors on navigation tap
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Radio button now shows player selection popup (consistent with search/album)
- Add to Queue button now shows player selection popup (was broken - did nothing without selected player)
- Removed redundant "Play On" button (Radio now provides the same functionality)
- Renamed _showPlayOnMenu to _showRadioMenu for clarity
- Removed unused _startArtistRadio method
Artist details header now has: [Radio] [Add to Queue] [Favorite]
All buttons work consistently with popup selection, matching search/album behavior.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- All sheets now use consistent pattern: transparent background with
Container decoration, mainAxisSize.min, Flexible with shrinkWrap
- All sheets hide mini player before showing and restore on dismiss
- All sheets include safe area bottom padding
- Fixed album_details_screen _showPlayOnSheet (was using fixed 45% height)
- Fixed artist_details_screen _showPlayOnMenu (was missing hidePlayer/showPlayer)
- Fixed audiobook_detail_screen _showPlayOnMenu (was using fixed 45% height)
- Added safe area padding to search_screen sheets
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use expandable inline row instead of bottom sheet
- Long-press track to expand action buttons
- Actions: Radio (player picker), Add to Queue (player picker), Favorite (toggle)
- Matches existing album details screen behavior
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add addTrackToQueue method to API and provider
- Add quick action bottom sheet with Play Now, Add to Queue, Play Radio
- Add onLongPress handler to track tiles in search
- Add localization strings for quick actions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use item is Album check before accessing inLibrary
- Compare favorite with == true (nullable bool)
- Use item.album?.name instead of albumName
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add SearchHistory table to database (schema v4)
- Save successful searches to history (deduplicated, max 10)
- Display recent searches as chips in empty state
- Clicking a chip performs that search
- Add localization strings for search history
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When searching for a track or album title along with an artist name
(e.g., "Yesterday Beatles"), the search now includes related artists
from the matched tracks/albums even if they weren't in the direct
artist search results.
- Extract unique artists from matched tracks and albums
- Filter to artists whose name contains query words (min 3 chars)
- Add cross-referenced artists with lower relevance score (25)
- Avoids duplicates with direct search results
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Parse playlists and audiobooks from MA search API response
- Add playlist and audiobook filter chips to search screen
- Add playlist and audiobook tiles with navigation to detail screens
- Update cache service and provider to include new media types
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add activeSource and isExternalSource fields to Player model
- Detect external sources (optical, Spotify, AirPlay, etc.) from URI patterns
- Show "External Source" label instead of stale metadata
- Use cyan status indicator dot for external sources
- Hide play/pause button when external source is active (controls don't work)
- Clear cached track data when external source detected
- Add localization strings for "External Source" in EN/DE/ES
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Issue #26.2: Add to Queue on artist pages
- Added null check for API before calling playArtistRadioToQueue()
- Shows "Not connected" error instead of silent failure
Issue #21: Player selection timing and auto-selection
- Added _sortPlayersSync() helper for reusable smart sorting
- Sort cached players immediately on app resume (no 5s delay)
- Added coldStart parameter to apply full priority logic on launch
- Priority: Playing player > Local player > Last selected > First
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add selectedPlayer check to nav bar visibility condition to prevent
it from showing during the ~1.2s gap between connection and player
selection for first-time users.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1. Removed Selector for nav bar visibility - Selector only rebuilds when
isConnected changes, but shouldShowWelcomeBackdrop is local state that
can change independently. Now using simple if condition.
2. Changed backdrop color from colorScheme.surface to Color(0xFF1a1a1a)
to match the connection screen exactly, ensuring seamless transition.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The nav bar was appearing briefly before the welcome backdrop because
it only checked isConnected, not the welcome screen state. Now it also
hides when shouldShowWelcomeBackdrop is true.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a user logs in via LoginScreen (fresh install), the navigation to
HomeScreen was happening immediately on connection - before the player
was selected. This caused the home screen to flash before the welcome
overlay could appear.
For first-time users (hasCompletedOnboarding=false), LoginScreen now
waits for provider.selectedPlayer to be set before navigating, matching
the logic in AppStartup.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The backdrop was only showing when _isHintModeActive was true, but that
required setState which happens in a post-frame callback - causing a
1-frame gap where the home screen was visible.
Fix: Compute shouldShowWelcomeBackdrop synchronously in build():
- True when first-time user is ready (settings loaded + connected + player)
- True when already in hint mode
- This shows the backdrop IMMEDIATELY in the same frame as HomeScreen
- Welcome content (logo, text) still fades in via animation after
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The welcome screen check in build() was missing - when the provider state
changes (connection happens), build() runs via context.watch(), but we
weren't checking if conditions were met for the welcome screen.
Added the check in build() with a post-frame callback to trigger the
welcome screen when:
- Settings are loaded
- First-time user (not completed onboarding)
- Connected with player selected
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The home screen was briefly visible between connection and welcome screens
because AppStartup and GlobalPlayerOverlay were racing - they loaded settings
and detected connection independently with no synchronization.
Solution: Make AppStartup the single source of truth for the transition.
- Load onboarding settings FIRST (before connection check)
- For first-time users: wait for connected + player selected
- For returning users: wait for connected only
- Remove preemptive backdrop from GlobalPlayerOverlay (no longer needed)
This eliminates the race by design - the dark "Connecting..." screen stays
visible until the welcome overlay is ready to appear immediately.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add _settingsLoaded flag to track async settings initialization.
Show solid backdrop when connected but settings not yet loaded,
ensuring the grey "Connecting..." background stays visible until
welcome screen is ready.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use context.watch() directly in build method instead of nested Selector
to ensure the preemptive backdrop renders in the same frame as home screen.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The _waitingForConnection flag was set without setState(), so the
preemptive backdrop condition was never re-evaluated. Now it properly
triggers a rebuild so the backdrop shows immediately when connected.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When connection is established, a solid backdrop now appears instantly
in the same render frame, covering the home screen before the animated
welcome screen starts. This prevents any flash of the home screen.
Flow: login → solid backdrop (instant) → blur + welcome content (animated)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The welcome screen was incorrectly starting immediately on first use,
even before the user had logged in. Now it correctly:
1. Shows login screen first (with hidden nav bar)
2. Waits for successful connection
3. Then shows welcome screen (covering home screen)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1. Hide bottom nav bar when not connected (fixes login screen issue)
2. Start welcome screen immediately on first use (blur + logo)
3. Show mini player hints only after connection is established
This ensures a smooth transition from login to home - the welcome
blur covers the home screen immediately, preventing any flash.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
On fresh install, welcome screen was starting immediately even when
user hadn't configured server address/login yet. This meant the hints
were pointing to a mini player that wasn't visible.
Now the welcome screen waits until the user is connected with a
selected player before showing the onboarding experience.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Welcome screen now only shows on first app launch
- Added hasCompletedOnboarding setting (separate from showHints)
- Onboarding marked complete when user skips or swipes to reveal
- Version bumped to 2.6.0-beta+28
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove "Swipe down, tap outside..." dismissal hint
- Separate backdrop into two widgets:
- Reveal mode: static backdrop (no TweenAnimationBuilder overhead)
- Hint mode: animated fade backdrop
- Fixes choppy expand/collapse by avoiding key changes and rebuilds
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Welcome screen: only skip button dismisses (not tapping anywhere)
- Player selector onboarding: show "Tap a player to start listening"
- Player selector regular use: show "Tap and hold a player to sync"
- Different icons for each context (touch_app vs lightbulb)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Mini player secondary line now always shows artist/author
- Removed "Now Playing" text when device reveal is visible
- Consistent with showing actual track info
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Welcome backdrop holds at 100% black for 2 seconds
- Then fades to 50% opacity over 1 second
- Uses Interval curve (0.67-1.0) for the delayed fade effect
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Backdrop starts at 100% black opacity on first launch
- Fades to 50% opacity over 2 seconds (reveals blurred home screen)
- Hides any loading/rendering happening behind the scenes
- Reveal mode (device selector) uses instant 0.5 opacity (no fade)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Start welcome overlay immediately on first launch (no delay)
- Add 800ms fade-in animation for welcome content
- Blur backdrop visible from start, hiding home screen load
- Hint text reduced to 16px (was 18px)
- Bounce animation starts after fade-in completes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Split welcome screen into two positioned sections:
- Top: Logo and "Welcome to Ensemble" (anchored from top)
- Bottom: Hint text and Skip button (anchored from bottom)
- Fix hint text using theme.textTheme.bodyLarge instead of raw TextStyle
- Create big visual gap between welcome title and hint text
- Equal 32px spacing between hint text <-> skip <-> mini player
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Hint text font size: 18px (was 14px)
- Move content up by 224px (1.5 album covers height)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Logo first, then "Welcome to Ensemble", then hint text
- Uses same logo as settings screen (50% width)
- Nicely spaced: 24px after logo, 16px after title, 32px before skip
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- GlobalPlayerOverlay now uses consistent 0.5 opacity for both states
- Removed PlayerRevealOverlay's own backdrop (was causing layered darkness)
- Transition from welcome screen to player select is now seamless
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Stage 1 - Welcome screen:
- Darker blur backdrop (0.4 opacity) for better focus
- Welcome message above mini player explaining phone as default player
- Skip button to bypass onboarding
- Bouncing mini player continues until user acts
Stage 2 - Player selector hints:
- Shows dismissal hint when coming from onboarding
- "Swipe down, tap outside, or press back to return"
All new text strings are localized.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The hint now triggers when a player is available, regardless of whether
there's a track playing. The 1500ms delay still gives time for any track
to load for a stable state.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Favorite tracks:
- Cache favorite tracks to database for instant display on startup
- Load cached favorites when _tracks list is empty
Interactive hint mode:
- Blur/darken backdrop when hint triggers (like player selector)
- Mini player bounces every 2 seconds until user acts
- User can swipe down (reveals player selector) or tap/back to dismiss
- Single bounce animation (600ms) with text overlay
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Pre-load library from SyncService for instant favorites display
- Favorites now filter from cached albums/artists immediately
- No need to wait for server connection
- Add HomeRowCache table for discover rows persistence
- Schema version 3 with migration
- Stores recent albums, discover artists, discover albums
- Update CacheService to persist and restore home rows
- loadHomeRowsFromDatabase() loads cached data on startup
- setCached* methods now persist to database in background
- Update provider initialization order:
1. Load players from database
2. Load library from cache (for favorites)
3. Load home rows from database (for discover)
4. Then connect to server
This ensures all home screen rows load instantly on app launch,
including favorites and discover rows, without waiting for server.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix hint timing to only fire when track is loaded (stable state)
- Previously hint could fire before track loaded, getting lost in the UI
- Now waits for currentTrack to be available before showing hint
- Reduced delay from 3500ms to 500ms since we're waiting for track
- Wire up PlaybackState persistence for currentTrack
- Load saved track from database on startup (_loadPlaybackStateFromDatabase)
- Track displays immediately without waiting for server response
- Persist track changes to database for next app launch
- Clear persisted state when playback stops
This ensures the mini player shows the correct track info instantly when
the app opens, instead of going through multiple state transitions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Both bounces now 20px (was 20px + 12px)
- Hint text vertically centered in mini player
- Separate Positioned widget for hint vs track title
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- First bounce: 10px -> 20px
- Second bounce: 6px -> 12px
- Duration: 900ms -> 1000ms
- Single bounce for device selector unchanged at 10px
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add isHintVisible prop to ExpandablePlayer
- When hint visible: show lightbulb icon + "Pull to select players" as primary text
- When hint visible: hide secondary text line (blank)
- Remove separate toast pill widget
- Hint text now appears directly in mini player, bouncing with it
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Single bounce on device selector expand (when pulling down)
- Single bounce on device selector collapse (cards landing)
- Double bounce on mini player when hint appears
- Toast pill: no bounce, positioned over nav bar, proper font
- Added decoration: TextDecoration.none to prevent underlines
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove double bounce from device selector expand/collapse
- Create separate hint-only bounce animation (doesn't affect player)
- Change pull-to-select hint to toast pill box at bottom of screen
- Update "hold to sync" text to "Tap and hold a player to sync"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add isHint parameter to MiniPlayerContent
- Show lightbulb icon when secondary text is a hint
- Apply to both main and peek player content in DeviceSelectorBar
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Reposition hint to half-overlap mini player top edge
- Add double bounce animation (first 10px, second 6px)
- Add lightbulb icon before hint text
- Extend animation duration to 900ms for better visibility
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Wrap hint text in Material widget to fix typewriter font/yellow underline
- Increase delay before showing hint: 500ms → 1500ms
- Increase linger time: 800ms → 2500ms
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Match hold-to-sync hint font size (16) with player card primary text
- Change text to "Hold a player to sync"
- Remove pill container from pull-to-select hint
- Make pull hint bounce with the mini player
- Simplify swipe text to just "Swipe to change player"
- Update German and Spanish translations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When long-pressing a powered-off Cast player that has a Sendspin
counterpart, automatically power it on first and wait for it to
become available before syncing. This provides a seamless experience
for grouping Cast devices that are currently off.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Don't clear the _castToSendspinIdMap on each player refresh. This allows
syncing to work with powered-off Cast devices by remembering their
Sendspin IDs from when they were previously online.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix hint text decoration to prevent underlines
- Make hold-to-sync hint slide with player cards instead of fading
- Show pull-down hint on every app launch (not just first time)
- Replace arrow symbols (← ↓ →) with text (left, down, or right)
- Allow long-press to sync players even when powered off
- Update translations for German and Spanish
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add show hints toggle in new Hints & Tips settings section
- Add pull-to-select-players hint with bounce animation on first connection
- Add hold-to-sync hint in player reveal overlay
- Track when user has discovered player reveal feature
- Update swipe hint to show directional arrows: ← ↓ →
- Support English, German, and Spanish translations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When the device reveal overlay is visible:
- Mini player shows player name instead of track name
- Secondary line shows "Now Playing" instead of artist
- Adds blur backdrop behind device selector for focus
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When showing the regular Cast player (ungrouped state), sync operations
need to use the corresponding Sendspin player ID since only Sendspin
players can group with other Sendspin players.
Added _castToSendspinIdMap to track the mapping between regular Cast
and Sendspin player IDs, populated during player filtering.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- When grouped: show Sendspin version (for sync), hide original Cast
- When ungrouped: show original Cast (for power control + queue)
This preserves power control and proper queue behavior when not syncing,
while still providing seamless Sendspin grouping when needed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a Cast device has Sendspin mode enabled, both the regular Cast
player and the "(Sendspin)" version appear in Music Assistant. This
change:
- Automatically prefers the Sendspin version when both exist
- Removes the "(Sendspin)" suffix for a cleaner display
- Provides seamless transitions when Sendspin becomes available
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Change playerState from conditional 'synchronized'/'idle' to always 'synchronized'
(MA only accepts 'synchronized', 'error', 'external_source')
- Rename 'timestamp' to 'client_transmitted' in time messages per protocol spec
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Thinner border for a more subtle grouped indicator on:
- Mini player
- Player cards in reveal overlay
- Player selector sheet
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a player is synced/grouped as a child:
- getQueue() now returns the leader's queue
- getCachedTrackForPlayer() returns the leader's track
- selectPlayer() gets the leader's track from cache
This ensures grouped players show the same queue and track info
as their leader, reflecting the actual audio being played.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Shows the same pastel yellow border (#FFF59D) on the mini player
that is shown on grouped player cards in the reveal overlay.
Only visible when collapsed (t < 0.5) to indicate group membership.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Child players (like Dining Room) have empty group_members but have
syncedTo set to the leader. Updated isGrouped to check for both:
- Leader: group_members.length > 1
- Child: syncedTo != null
Both should show the yellow border now.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The device list uses PlayerCard widget (not _PlayerSelectorSheet).
Added:
- onLongPress callback to PlayerCard
- isGrouped property for pastel yellow border
- Long-press handler in player_reveal_overlay.dart
- SnackBar feedback when long-press detected
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Show visual snackbar "Syncing {player}..." when long-press detected
- Remove !isSelected condition to allow sync on any available player
- Helps debug if long-press gesture is being recognized
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Logs added at:
- UI: Long-press trigger with player details
- Provider: togglePlayerSync, syncPlayerToSelected
- API: syncPlayerToLeader with request/response
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The correct MA API command is:
- players/cmd/group (not group_many)
- player_id: the player joining the group
- target_player: the leader player to sync to
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Feature: Long-press on a player in the device list to sync it with the
currently selected/playing player, creating a temporary group.
Changes:
- API: Add syncPlayers() and unsyncPlayer() methods using MA commands
players/cmd/group_many and players/cmd/ungroup
- Model: Add groupMembers and syncedTo fields to Player, with derived
properties isGrouped, isGroupLeader, isGroupChild
- Provider: Add syncPlayerToSelected(), unsyncPlayer(), togglePlayerSync()
- UI: Long-press gesture on player cards (with haptic feedback)
- UI: Pastel yellow border (#FFF59D) around grouped players
- UI: "Synced" badge with link icon for grouped players
UX Flow:
1. Play music on a speaker (e.g., Kitchen HiFi)
2. Open device selector
3. Long-press another speaker (e.g., Bedroom)
4. Bedroom syncs to Kitchen and both show yellow border
5. Long-press again to unsync
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Badge now excludes selected player from count (shows "other" activity)
- Auto-selection now switches to playing player when preferLocalPlayer is OFF
and current selection is not playing
Addresses feedback from issue #21🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The root cause: second tap is smooth because image is cached,
but first tap shows grey flash because image isn't cached yet.
Recent Albums works perfectly because those images are already
cached from when the user played those albums.
Fix: Pre-cache the first 10 album/artist images when the row
loads, so they're ready in cache before the user taps.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Artist card layout:
- Replace Expanded > Center > LayoutBuilder > Hero with AspectRatio > Hero
- LayoutBuilder inside Hero caused dynamic sizing during animation
- AspectRatio provides fixed geometry BEFORE hero animation starts
Album details screen:
- Defer _loadFreshAlbumData until 350ms after transition (same as colors)
- Previously called immediately in initState, causing setState during animation
- If fresh album had different image URL, caused grey icon flash
These are the actual root causes identified by comparing "Recent Albums"
(perfect animation) with other album/artist taps (janky animation).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Critical fixes:
- Replace Opacity widgets with color alpha to avoid saveLayer in player_controls
- Fix AnimatedBuilder not using child parameter in global_player_overlay
- Replace setState with ValueNotifier for bounce animation to prevent rebuilds
- Add flightShuttleBuilder to series Hero to simplify animation flight
- Remove Opacity+ClipRRect combo in expandable_player progress bar
Image loading optimizations:
- Add fadeInDuration: Duration.zero to audiobook_row, track_row, queue_panel
- Add memCacheWidth/Height to queue_panel (176x176 for 44x44 display)
- Move _loadQueue() from AnimationStatus.forward to .completed
Animation polish:
- Add curve: Curves.easeInOut to AnimatedContainer in settings_screen
- Use FadeSlidePageRoute for series navigation for consistent hero timing
- Add clipBehavior: Clip.hardEdge to progress bar ClipRRect
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Set fadeInDuration: Duration.zero on source card widgets to prevent
end-of-animation flicker:
- album_card.dart: was 150ms, now zero
- artist_card.dart: was 150ms, now zero
- artist_avatar.dart: added zero fade
- series_row.dart: added zero fade
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
ArtistDetailsScreen was using Image.network which re-fetches images
from the network, ignoring the cache. This caused jank during hero
animations because the image wasn't ready instantly.
Changed to CachedNetworkImage with zero fade duration to match
AlbumDetailsScreen behavior - image loads instantly from cache
for smooth hero transitions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Apply the same pattern that fixed album card hero animations to all
navigation paths:
- AudiobookDetailScreen now accepts initialImageUrl parameter
- Library screen passes imageUrl when navigating to audiobooks/albums
- Audiobook series and author screens pass imageUrl
- Artist details screen passes imageUrl to album navigations
- Search screen passes imageUrl to album details
This ensures hero animations show the correct image throughout the
transition instead of showing a placeholder icon.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Two issues prevented queue restoration from working:
1. MediaItem.toJson() was passing providerMappings directly instead of
converting each ProviderMapping to JSON. This meant jsonEncode would
fail to serialize them, losing the provider mapping data.
2. _restoreQueueFromCache was filtering for track.uri, but queue items
from MA API don't include URIs. playTracks uses providerMappings to
build URIs via _buildTrackUri(), so we should check for those instead.
Now providerMappings are properly serialized/deserialized, and queue
restore checks for available providerMappings (or provider+itemId as
fallback) to match what playTracks expects.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When navigating from AlbumCard to AlbumDetailsScreen, the detail screen's
first build may not have access to image metadata immediately. This caused
the hero animation to flash to a record icon even though both screens
eventually show the artwork.
Fix: AlbumCard now passes its imageUrl to AlbumDetailsScreen via a new
initialImageUrl parameter. The detail screen uses this as a fallback,
ensuring the hero animation has the same image on both ends.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
getRecentAlbumsWithCache was short-circuiting to local database data
which has OLD entries without image metadata. This prevented fresh
albums with images from ever being fetched.
Now the function always fetches from API when connected. Local/cached
data is only used:
- By getCachedRecentAlbums() for instant display in stale-while-revalidate
- As fallback when API is unavailable or errors
This ensures albums passed to detail screen have complete metadata
including images for proper hero animation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The stale-while-revalidate pattern had a flawed hasChanged check that
only compared item IDs and counts. If cached albums had the same items
but without image metadata, fresh albums (with images) would never
replace them.
This caused the hero animation to show a record icon because the user
would tap on a cached album without images before fresh data loaded.
Now AlbumRow and ArtistRow always update with fresh data when it arrives,
ensuring albums have complete metadata including images for proper
artwork display.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The getRecentAlbums() was extracting album data from track responses,
which only contains simplified/nested album objects without the full
metadata (including images array). This caused missing artwork in the
recent albums row and album details screen.
Now the function:
1. First fetches tracks to collect unique album URIs
2. Then fetches full album details for each URI via music/item_by_uri
This ensures albums have complete metadata with images for proper
artwork display and hero animations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When recording album plays, now stores the images array from album
metadata. When reconstructing albums from recently played data, this
image metadata is included, enabling getImageUrl() to work without
needing to fetch fresh album data from the API.
This fixes the artwork "pop-in" issue where album art would flash into
place at the end of the hero animation when navigating from the recent
albums row to album details screen.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When tapping an album from recently played, the detail screen now
fetches fresh album data from the API to get full metadata including
the images array needed for artwork display.
- Added _freshAlbum state variable to store full album data
- Added _displayAlbum getter that returns fresh data if available
- _loadFreshAlbumData() fetches album by URI and triggers color extraction
- Build method now uses _displayAlbum for image URL
This fixes the placeholder icon showing in album details when navigating
from the recently played row.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The recently played albums were being stored with minimal metadata,
missing the 'images' array needed for getImageUrl().
Now recordAlbumPlayed stores the full album JSON including metadata,
and getRecentAlbums reconstructs full Album objects from that JSON.
Note: This only fixes newly played albums. Existing entries in the
database will still show placeholder icons until played again.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When the app returns after a while, Music Assistant server may have
cleared its queue. The app would fail with "No playable item found".
Now when this error occurs, the app automatically:
1. Detects the "No playable item" error
2. Loads the cached queue from the database
3. Extracts tracks and re-queues them on the server
4. Starts playback from the beginning
This fixes the issue where Chris' phone couldn't play after
returning to the app - the 35 cached tracks are now restored.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- AlbumRow and ArtistRow now show cached data instantly (no loading spinner)
- Fresh data is fetched in background and UI updates silently if changed
- Added getCachedAlbums/getCachedArtists optional params for sync cache access
- Added cache getter methods to MusicAssistantProvider
This fixes the issue where home screen rows showed loading spinners
when returning to the app after the 10-minute cache expired.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Created OfflineActionQueue service to persist actions when offline
- Queue is stored in SharedPreferences and processed on reconnect
- Updated all favorite toggle methods in screens to use new provider methods
- Provider methods check connection state and queue if offline
- Optimistic UI updates with "Will sync when online" feedback
- Added localization strings for offline sync messages
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Queue is now persisted to database when fetched from API
- Expandable player shows cached queue immediately on load
- Added getCachedQueue() method to MusicAssistantProvider
- Background refresh updates queue silently
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Album, playlist, and artist detail screens now:
- Show cached data immediately (if available)
- Fetch fresh data in background (silent refresh)
- Update UI only if data actually changed
Added cache accessor methods to MusicAssistantProvider:
- getCachedAlbumTracks()
- getCachedPlaylistTracks()
- getCachedArtistAlbums()
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Renamed hasResults to hasCachedResults to avoid conflict with
existing hasResults variable later in the same function.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Type name fixes:
- CachedPlayers table generates CachedPlayer data class (singular)
- Fixed type references in database.dart and database_service.dart
Loading spinner elimination:
- Show cached data immediately if available, even while loading
- Only show spinner if no cached data AND loading
- Applied to library_albums, library_artists, library_tracks, library_playlists
- Updated search_screen to show cached results while fetching
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Cache clearing changes:
- Remove cache clearing on disconnect events in both providers
- Keep cached players and data for instant UI display on reconnect
- Add clearAllOnLogout() method for explicit logout clearing
- Add clearCachesOnLogout() to ConnectionProvider
Player persistence:
- Add player caching convenience methods to DatabaseService
- Load cached players from database on app startup (instant display)
- Persist players to database when loaded from API
- Restore selected player from database on startup
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add PlaybackState table: persists current track, position, playing state
- Add CachedPlayers table: stores player list with current track info
- Add CachedQueue table: persists queue items per player
- Increment schema version to 2 with migration logic
- Add CRUD operations for all new tables
Part of aggressive caching overhaul for instant app resume.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Create a unified MiniPlayerContent widget used by all mini player
components to ensure pixel-perfect consistency:
- New MiniPlayerLayout class with shared constants:
- height: 64px, artSize: 64px
- textLeft: 74px, primaryTop: 13px, secondaryTop: 33px
- iconSize: 28px, iconOpacity: 0.4
- primaryFontSize: 16px, secondaryFontSize: 14px
- DeviceSelectorBar now uses MiniPlayerContent
- ExpandablePlayer uses MiniPlayerLayout constants for collapsed state
- Peek content uses MiniPlayerContent via Transform.translate
This eliminates the drift between components that was causing visual
jumps when swiping between playing and non-playing players.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add explicit fontSize (11) and color to badge text for visibility
- Fix peek icon color: use textColor.withOpacity(0.4) to match
DeviceSelectorBar (was using full opacity onSurfaceVariant)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove height: 1.2 from peek content text (DeviceSelectorBar doesn't have it)
- Make height: 1.2 conditional on expanded state for main title
- Fix icon size in peek content: 28px to match DeviceSelectorBar
These mismatches caused visible text movement and icon size
changes when swiping from playing to non-playing player.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace manual badge implementation with Flutter's Badge widget
for proper centering and styling
- Standardize mini player text positioning between playing and
non-playing states:
- Title: left=74px (art+10), top=13px
- Artist: left=74px, top=33px (gap of 20)
- Convert DeviceSelectorBar to absolute positioning to match
expandable player structure
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Adjust padding to shift number up (less top padding)
- Increase font weight to w800 (extra bold)
- Set line height to 1.0 for tighter baseline
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When "Prefer Local Player" is enabled, the local player is now selected
even if another player was already selected. This ensures that when the
local player registers (via Sendspin), it becomes selected automatically.
Previously, the "keep current selection" logic ran before checking the
prefer local setting, which meant the local player was never selected
when it registered after initial connection.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1. Smart Player Auto-Selection:
- Priority 1: Single playing player (skip if multiple playing)
- Priority 2: Local player (this device)
- Priority 3: Last manually selected player
- Priority 4: First available player
- New "Prefer Local Player" toggle skips to local player first
2. Active Player Count Badge:
- Shows number of currently playing devices
- Round badge with tertiary color in top-right of device selector
3. Dynamic Player List Sorting:
- New "Smart Sort" toggle in settings
- When enabled: Local player → Playing → On → Off
- Applies to device list and mini player swipe
- Default: alphabetical (unchanged)
4. Settings reorganization:
- Moved Language selector to its own section
- Added new Players section with toggles
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add 6 predefined accent colors (purple, blue, teal, deep orange, pink,
green) that users can choose when Material You is disabled. The colors
are displayed as circular swatches above the Material You toggle and
are greyed out when Material You is enabled.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Reverted MDI icons back to Material Icons
- Used Transform.translate(-6, 0) to nudge power button left
- This closes the gap between skip and power without changing icon pack
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace Material Icons with Material Design Icons (MDI) in:
- Device list: play/pause, skip next, power
- Mini player: skip previous, play/pause, skip next
- Expanded player: shuffle, repeat, skip controls, audiobook controls
MDI icons have less built-in whitespace for more compact layouts
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Play/Pause: 34→28 icon size, smaller container
- Skip next: 28→24 icon, reduced horizontal padding
- Power button: 28→20 (smallest of the three)
- Reduced gaps between buttons for more compact layout
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use SizedBox wrapper with zero padding for play button, matching
the mini player's Container approach. Also restore right margin to 4px.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Change button order from prev/play/next to play/pause/next/power
- Match mini player button sizes (play=34, skip/power=28)
- Power button always visible when player available (dimmed when off)
- Play/pause and next only shown when powered with content
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add device list (player reveal) to didPopRoute() priority chain.
Previously back gesture would pop navigation routes before dismissing
the device list when on screens other than home.
Priority order is now: device list > expanded player > navigation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
TweenSequence with chained curves was causing low FPS on the mini player
to expanded player animation. Reverted to simple CurvedAnimation which
is more performant:
- Replace 3-phase TweenSequence with single CurvedAnimation (easeOutCubic)
- Reduce expand duration from 350ms to 280ms
- Simplify bounce animation back to sine-wave calculation
- Keep gesture-driven expand/collapse functionality
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix swipe-down gesture on active mini player to reveal device list
(was blocked by gesture tracking starting immediately)
- Fix back gesture priority: device list now closes before underlying
album/artist screens by handling PopScope at GlobalPlayerOverlay level
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix inverted drag: swipe down now moves cards down (toward dismiss)
- Faster collapse animations:
- Mini player: 250ms → 200ms
- Player list: 200ms → 150ms
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Faster collapse (250ms vs 350ms expand) for snappier feel
- Simplified bounce to single clean motion (down 10px → back)
- Removed overshoot phases that looked like multiple bounces
- Duration reduced to 200ms
- Fixed swipe dismiss on player list vanishing halfway
- Reset dragOffset before dismiss() so animation starts correctly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace threshold-based expand/collapse with finger-tracking
- Mini player now follows finger during vertical swipe
- Snap decision based on velocity (800px/s) and position (30%)
- Add TweenSequence for phased animations
- Phase 1 (0-40%): Quick acceleration with easeOut
- Phase 2 (40-75%): Consistent cruise with linear
- Phase 3 (75-100%): Settle with slight overshoot (easeOutBack)
- Fix position calculation discontinuity
- Removed abrupt switch at t=0.1
- Now smoothly fades out slide/bounce offsets over first 15%
- Add settle bounce when cards collapse into mini player
- TweenSequence bounce: push down 12px → overshoot -4px → settle
- Triggered when cards are 70% collapsed for natural "landing" feel
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add _parseServerVersion() helper for version comparisons
- Add _serverHasSendspinProxy() to detect MA 2.8.0+ (has /sendspin proxy)
- Add _logSendspinTroubleshooting() with version-specific guidance
- Update connection logic to try local proxy fallback for local IPs
- Improve logging to show server version and proxy availability
- MA 2.7.x users now get clear guidance: upgrade to 2.8.0+ or configure proxy
Fixes#17🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes issue #17 - users unable to connect via local IP or see local player
Changes:
1. **Local IP connection handling**:
- Don't follow HTTP->HTTPS redirects for local IPs
- Detect 308/301/302 redirects and handle gracefully
- Prevents certificate verification errors when server redirects to HTTPS
2. **Sendspin connection logic**:
- Remove non-existent API calls (sendspin/connection_info, webrtc_offer, etc.)
- For local IPs: Connect directly to ws://ip:8927/sendspin
- For external/HTTPS: Use proxy at wss://server/sendspin with auth
- Better error messages explaining what to configure
3. **Utility improvements**:
- Add _isLocalNetworkHost() helper for IP detection
- Include .ts.net (Tailscale) as local network
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add complete Spanish translations (app_es.arb)
- Replace cramped SegmentedButton with ListTile + Dialog picker
- Localize player states (Off, Idle, Unavailable, Selected)
- Localize Material You description
- Scales better for multiple languages
- Bump version to 2.3.2-beta+24
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Updated theme/language pickers to match library picker style (border radius 8, opacity 0.5)
- Added 35+ new translation keys for tooltips, settings, and UI elements
- Replaced hardcoded strings in settings, artist, album, audiobook screens
- Fixed sort/view mode tooltips across all detail screens
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixed DisconnectedState.full and DisconnectedState.withSettingsAction
calls in new_home_screen.dart and new_library_screen.dart.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixed all call sites that were missing the required context parameter
after the localization refactor.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add LocaleProvider for managing app locale
- Add language picker in settings (System/EN/DE) matching library picker style
- Update theme picker to use same compact icon-bar style
- Replace hardcoded strings in login, settings, home, and search screens
- Persist language preference in SharedPreferences
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add flutter_localizations and intl dependencies
- Create l10n.yaml configuration for code generation
- Add English (app_en.arb) and German (app_de.arb) translation files
- Configure MaterialApp with localization delegates
- Support 150+ translatable strings across the app
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Make availablePlayers getter load from cache when empty
- Make selectedPlayer getter load from cache when null
- Update connecting banner to check hasCachedPlayers (cache service)
instead of availablePlayers.isEmpty (timing-dependent)
This ensures mini player and device selector appear instantly when
resuming the app, without waiting for checkAndReconnect to run.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Cache players in memory so mini player and device selector appear
instantly when resuming the app, without waiting for reconnection.
- Add player caching to CacheService (players list + selected player)
- Load cached players immediately in checkAndReconnect before connecting
- Hide "Connecting..." banner when cached players are available
- Background reconnection happens silently while UI is already usable
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Instead of attempting 3 retries of builtin_player/register (which fail
on MA 2.7.0b20+) before falling back to Sendspin, detect the server
version upfront and go directly to Sendspin.
This saves ~2 seconds on app resume by avoiding:
- 3 failed builtin_player/register attempts
- Exponential backoff delays (500ms, 1s)
- Error handling overhead
The version check parses server_version (e.g., "2.8.0b2") and compares
against the 2.7.0b20 threshold where Sendspin was introduced.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove duplicate _preloadAdjacentPlayers call in checkAndReconnect
(already called via refreshPlayers -> _loadAndSelectPlayers)
- Don't await preload in _loadAndSelectPlayers - UI should show
immediately with cached data, preload happens in background
This fixes slow device button and mini player loading when returning
to the app, especially noticeable on slower network connections.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add auth token field and setAuthToken() method to SendspinService
- Send auth message before client/hello when connecting via HTTPS proxy
- Wait for auth_ok response before proceeding with handshake
- Provider now sets auth token from SettingsService before connecting
- Allows Sendspin to work through reverse proxies without exposing port 8927
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Preload all player tracks in checkAndReconnect() after app resumes
- Preserve cached tracks that have images even if new event lacks image
- This ensures album art is available when swiping between players
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Text overlap fix:
- Use collapsedWidth (with margins) instead of screenSize.width
- collapsedWidth is 24px narrower, fixing the overlap
Stale track data fix:
- Parse artist from title when title contains " - " and artist is unknown
- Don't overwrite cache if existing track has proper artist info
- Only set _currentTrack from event if not already set
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix width calculation to use titleLeft position for accurate constraint
- Add softWrap: false in collapsed state to ensure ellipsis truncation
- Controls: 36 (prev) + 34 (play) + 36 (next) + 8 (margin) = 114px
- Reserve 122px from right edge for 8px gap to controls
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When resuming app after background, cached track data from player_updated
events may have incomplete artist info (e.g., "Artist - Title" in name field
with null artist). Now awaits _updatePlayerState() before notifying UI to
ensure fresh queue data is displayed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Set secondary text to 14px in both mini player and device selector bar
- Adjust text width for consistent 8px gap before player controls
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Change artist font size from 14px to 12px in collapsed mini player to match
the DeviceSelectorBar styling for visual consistency.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When switching from audiobook to music track playback, the audiobook
context (_currentAudiobook) was not being cleared. This caused the
player UI to remain in audiobook mode (30-second skip buttons, author
instead of artist, chapters in queue) even when playing regular music.
The fix detects when media_type changes to 'track' in player_updated
events and clears the audiobook context, allowing the UI to correctly
switch to music mode.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When audio frames arrive before the stream/start message (or after
pause/release), the native PCM player isn't setup yet, causing
"must call setup first" errors and silent playback.
This fix adds auto-recovery in two places:
1. In _onAudioData: If audio arrives while paused, automatically
re-initialize and resume playback
2. In _feedNextChunk: If feed() fails with setup error, re-initialize
the native player and retry
This ensures audio playback starts even when binary frames race
ahead of JSON control messages on the WebSocket.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changed from Column to Stack so the "Connecting..." banner overlays
content instead of pushing rows down when it appears/disappears.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use AspectRatio instead of Expanded to guarantee square covers
- Add color extraction from book covers for empty cell backgrounds
- Add nested grid patterns (solid, 2x2, 3x3) in empty cells
- Pass initialCovers to series detail for smooth Hero animation
- Removes grey bars that appeared due to non-square container
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove margins between grid cells in series detail cover for seamless appearance
- Add book count caching when loading series covers
- Display book count under series titles in all layouts (list, grid, home row)
- Book count now shows when covers load, even if not provided by API
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add homeRowOrder setting to persist row display order
- Replace separate toggle sections with ReorderableListView
- Add drag handles to each row for reordering
- Home screen now respects the saved row order
- Remove legacy random favorites order logic
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Wrap cover grid in AspectRatio(1) to force square
- Use Column/Row instead of GridView for consistent sizing
- Eliminates grey bar at bottom of covers
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Hero wrapper to series list tile leading image
- Pass heroTag and initialCovers to series detail screen
- First cover in list becomes first cover in detail grid
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Pass pre-loaded cover URLs from library to series detail screen
- Use initial covers immediately instead of showing loading placeholder
- Extract colors from initial covers for consistent empty cell appearance
- This ensures the Hero animation works smoothly in both directions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove grid lines between nested cells (seamless appearance)
- Limit nested grids to 1x1, 2x2, 3x3 only (no 4x4)
- Adjust distribution: 50% solid, 30% 2x2, 20% 3x3
- Reduce color saturation (50%) and darken (70%) for subtler look
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Extract dark muted colors from first 4 book covers per series
- Use extracted colors for empty cells instead of static palette
- Colors extracted asynchronously to avoid blocking UI
- Falls back to static dark pastel palette when colors not yet loaded
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add dark pastel color palette to series detail screen header
- Implement random nested grids (2x2, 3x3, 4x4) inside empty squares
- Use deterministic pattern based on series ID for consistency
- Distribution: ~40% solid, ~25% 2x2, ~20% 3x3, ~15% 4x4
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When checking for MA auth on HTTP URLs without a port, the code was
defaulting to port 8095 (MA's direct port). This caused auth detection
to fail for Tailscale URLs behind a reverse proxy on port 80.
Now uses default HTTP/HTTPS ports (80/443) when no port is specified.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Updated screenshots 3, 4, 5 with new captures
- Added new screenshots 7 (Audiobooks) and 8 (Audiobook Player)
- Reduced screenshot width from 250px to 150px for compact display
- Arranged all 8 screenshots in a single row that wraps naturally
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Tailscale URL detection (*.ts.net) to default to HTTP instead of HTTPS,
since the VPN tunnel already provides encryption
- Implement HTTPS to HTTP automatic fallback when HTTPS connection fails
- Add HTTP connection info banner on login screen:
- Shows "Tailscale VPN Connection" with VPN lock icon for *.ts.net URLs
- Shows "Unencrypted Connection" warning for other HTTP connections
- Track resolved server URL from fallback and use it for actual connection
Fixes user-reported issue where connecting via Tailscale URL required
manually adding "http://" prefix and specifying port 80.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Audiobooks section with full feature list
- Add Instant App Restore and Offline Browsing to Smart Features
- Update requirements for MA 2.7.0 beta 20+
- Add Audiobookshelf requirement for audiobook features
- Remove outdated Sendspin compatibility note
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add browseOrder field to Audiobook class
- Inject browse order from API response when no position is set
- Use browse order as last-resort fallback in seriesSequence
- This preserves the order returned by MA/Audiobookshelf browse API
when no explicit series sequence metadata is available
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Logs raw audiobook data and sequence values to diagnose
sorting issues with different series (Dark Tower, Dan Carlin).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Move _sortAudiobooks() call inside setState so UI updates correctly
- Default sort order is now 'series' (by series number ascending)
- Sort button toggles between series number and alphabetical order
- Enhanced seriesSequence getter to extract from multiple sources:
- Parent's position field
- sortName patterns (leading numbers, "Book N")
- name patterns (leading numbers, "Book N")
- Metadata fields (sequence, series_sequence, series_number, etc.)
- Series object with sequence/number/order
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace GridView.builder with Column/Row layout
- Always use 3x3 grid regardless of cover count
- Empty cells show placeholder color
- Fixes cropping issue where grid wasn't filling container
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix: Use Theme.of(context).textTheme instead of undefined textTheme
- Increase series detail cover to 280x280 (matches audiobook detail)
- Increase expandedHeight to 350 (matches audiobook detail)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Show cached library data immediately while connecting
- Add audiobook and playlist caching to SyncService
- Add connecting banner when showing cached data
- Fix series book ordering: default to series number, toggle series/alpha
- Add seriesSequence getter to Audiobook model for sorting
This enables the app to show the last known state instantly on startup,
with a subtle "Connecting..." indicator while reconnecting in background.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The import was incorrectly removed but ImageConfiguration,
ImageStreamListener, and ImageInfo are used by _precacheImage().
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Play On button (speaker icon) to audiobook detail action buttons
- Remove play button row from series screen (was added in error)
- Keep Hero animation for series cards
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Hero animation to series cards in library grid view
- Add play button row to series detail screen (Play, Play On, Add to Queue)
- Fix audiobook detail expandedHeight from 320 to 350
- Remove unused flutter/painting.dart imports
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 8 muted book-like colors: slate, blue-grey, brown, midnight blue, etc.
- Series ID used as seed for consistent color selection per series
- No grid lines between cells for seamless appearance
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove 1px margins between cover cells for seamless appearance
- Empty cells now transparent (show parent background)
- Simplify loading state to solid color container
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use first author name for image lookup instead of concatenated string
(searching "Author A, Author B" never matches)
- Add Wikipedia API as third fallback source for author photos
- Image sources now: Audnexus → Open Library → Wikipedia
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
GridView.builder inside AspectRatio was causing layout animations
when scrolling. Using simple Column/Row layout eliminates this.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove fade animation from series cover grid images in 2-column view
- Change chapter time display from duration to cumulative time from start
- Clean up redundant position time in audiobook detail chapter subtitle
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create AudiobookRow widget for displaying audiobooks in horizontal row
- Create SeriesRow widget for displaying series in horizontal row
- Add provider methods: getInProgressAudiobooks, getDiscoverAudiobooks, getDiscoverSeries
- Integrate audiobook settings into home screen (Continue Listening, Discover Audiobooks, Discover Series)
- Home screen now reads audiobook row settings from SettingsService
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add list view option to series tab (cycling: grid2 → grid3 → list)
- Remove Hero animations from series grid cards to fix cover loading issues
- Replace animated loading spinners with static placeholders in series grid
- Redesign series detail screen to match author/album detail screens:
- Centered cover collage in header
- Title and book count below header
- Section header with sort/view mode controls
- List/grid view with progress indicators
- Add series audiobook view preferences to SettingsService
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use Chapter class instead of AudiobookChapter
- Use positionMs (milliseconds) instead of position for seeking
- Convert milliseconds to seconds when seeking
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Show author name instead of 'unknown artist' for audiobooks in player
- Add ChaptersPanel widget for audiobook chapter navigation
- Replace queue panel with chapters panel when playing audiobooks
- Chapters panel shows chapter numbers, titles, durations
- Tap chapter to seek directly to that position
- Current chapter highlighted with accent color
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Authors tab: Use LayoutBuilder for proper circular images (not ovals)
- All Books tab: Remove header row with extra sort/view buttons
- All Books tab: Change to direct GridView.builder like music albums
- Series tab: Update aspect ratio from 0.68 to 0.75 to match music albums
- Match spacing and padding with music tabs across all audiobook tabs
- Remove unused _buildAudiobooksSliver method
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Books tab:
- Use AspectRatio(1.0) for square artwork, consistent in 2 and 3 column
- Progress bar always inside artwork
Authors tab:
- Reduce circle size with padding to leave room for text
- Adjust aspect ratio for better text visibility
Series tab:
- Remove Card wrapper and shadow for cleaner look
- Match books tab style: square cover grid with text below
- Use consistent spacing and aspect ratios
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Mark as Finished and Mark as Unplayed options in audiobook detail menu
- Fix duration format for long audiobooks (20+ hours) to show hh:mm:ss
- Clear audiobook context when switching to non-audiobook media
- Clear audiobook context when playback stops
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add recordAudiobookPlayed method to RecentlyPlayedService
- Add getRecentAudiobooks method to retrieve recent audiobooks
- Record audiobook plays in audiobook detail screen
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add 30-second skip forward/back buttons for audiobooks
- Add chapter navigation (previous/next chapter) when chapters available
- Show current chapter name and progress in expanded player
- Add audiobook context tracking in provider with chapter methods
- Store audiobook context when playback starts for player controls
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When specific ABS libraries are enabled in settings, audiobooks will now
be fetched using the browse API (same approach as series) to filter by
library.
If no specific libraries are enabled (all libraries shown), uses the
faster MA API endpoint.
This allows users to select which ABS libraries show audiobooks in the
Library > Books tab.
The player filter was using 'player.playerId.startsWith("ma_")' which
incorrectly matched snapcast players that have IDs starting with 'ma_'.
Fix: Now requires BOTH 'provider == "builtin_player"' AND the 'ma_' prefix
to filter out MA Web UI's built-in player. This prevents legitimate players
like snapcast from being filtered.
- Series card cover grid animates to series screen header
- Book list items animate to audiobook detail screen cover
- Uses unique hero tags based on series/book IDs
Series cards now show a grid of book covers instead of a placeholder:
- Fetches first 9 book covers from each series
- Caches covers for performance
- Shows loading indicator while fetching
- Dynamically adjusts grid size (1x1, 2x2, or 3x3) based on available covers
- Falls back to placeholder icon if no covers available
The freeze was caused by nesting GlobalPlayerOverlay widgets, which use
a GlobalKey. Having two widgets with the same GlobalKey causes Flutter
to crash/freeze.
Fix: Remove the GlobalPlayerOverlay wrapper from the series screen since
it's already applied at the app level in MaterialApp.builder.
Also added bottom padding for mini player.
Just a Scaffold with AppBar and text - no:
- GlobalPlayerOverlay
- CustomScrollView
- Images
- Provider watching
- Data loading
If this still freezes, the issue is in navigation or widget creation itself.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previous test still froze with no data loading, so the issue
is in the UI. Removed CachedNetworkImage from header - now
just shows a plain colored container with icon.
If this works: CachedNetworkImage is the problem
If still freezes: Issue is elsewhere in the screen
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Test build - the series screen will show "Loading..." forever
but should NOT freeze. If this doesn't freeze, the issue is
confirmed to be in the data loading code.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The series tap freeze might be caused by something blocking during
initState. By deferring the load to addPostFrameCallback, we allow
the UI to render first (showing the loading spinner), then start
the data fetch.
This helps isolate whether the freeze is in:
1. The navigation/initial build (screen would show briefly)
2. The data loading (screen shows then freezes)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Detailed logging added to:
1. AudiobookSeriesScreen - track initState, provider access, API call
2. getSeriesAudiobooks - track browse call, item parsing
This will help identify where the freeze occurs when tapping on a series.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changes:
1. Created AudiobookSeriesScreen - shows books in a series with navigation
to audiobook detail screen
2. Hooked up series card tap to navigate to series screen
3. Removed audiobook library filtering code - it can't work because
the audiobook URI (library://audiobook/123) doesn't contain library info
Note: Series filtering DOES work via the browse API since series are
loaded per-library. Library toggle affects series when user pulls to
refresh on the Series tab.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The providerMappings.itemId is just a UUID without library info.
The URI field contains the full path with library ID (lb xxx format).
If URI doesn't contain library info, default to showing the audiobook.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added logging to debug why all audiobooks are being filtered out:
- Log enabled libraries list
- Log first audiobook's providerMappings to see actual item_id format
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fixed series filtering that was incorrectly skipping all series items
(was checking for any media_type, now only skips audiobook/track)
- Added library filtering for audiobooks based on enabled libraries
- Added snackbar notification to remind users to refresh after toggling
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add settings to store discovered and enabled ABS libraries
- API saves discovered libraries when browsing for series
- API respects enabled libraries when searching for series
- Settings screen shows library toggles when libraries discovered
- Libraries all enabled by default, can be disabled individually
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The audiobookshelf provider shows libraries first, not categories.
Now we browse:
Root -> Audiobookshelf -> Each Library -> Look for Series folder
This discovers series across all ABS libraries.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add _stripHtml helper to clean HTML tags from descriptions
- Update series fetching to browse root first to discover structure
- Add detailed logging for browse path discovery
- Log all items at each level to debug series path finding
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add BrowseFolder and AudiobookSeries models
- Implement browse() method to navigate MA content
- Add getAudiobookSeries() to fetch series from provider
- Add getSeriesAudiobooks() to fetch books in a series
- Update library screen Series tab to display actual series
- Replace discovery-only approach with real data display
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Chapter.fromJson now handles MA API format (position, name, start, end)
- Audiobook.fromJson now checks metadata['chapters'] if top-level missing
- Also parse description from metadata as fallback
- Properly convert start/end seconds to milliseconds
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Try multiple API patterns for getAudiobookDetails since
'music/audiobooks/get_item' is invalid
- Fall back to list data if all API patterns fail
- Add detailed metadata logging in getAudiobooks to discover
where chapters and series info is stored
- Chapters/series are likely in the metadata field
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove audiobookshelf_service.dart and all ABS integration code
- Remove ABS settings from settings screen
- Add verbose logging to discover MA audiobook/chapter/series fields
- Add exploreSeries() method for API discovery
- Update Series tab to show discovery UI while we learn MA's API
- Make author/audiobook art sizes consistent with artist/album screens:
- Author: 200x200 circular (matches Artist)
- Audiobook: 280x280 square (matches Album)
Chapters and series are available in MA web UI, so we should be able
to get them from MA's API without needing direct ABS calls. The added
logging will help discover the correct API endpoints and field names.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Series tab that fetches and displays series from Audiobookshelf
- Fix layout buttons for books tabs (Authors, All Books, Series)
- Add chapter start time display in audiobook detail screen
- Improve chapter duration formatting and display
Series tab features:
- Shows loading state while fetching
- Prompts to configure ABS if not connected
- Displays series in a 2-column grid with name and book count
- Supports pull-to-refresh
- Sorts series alphabetically
Chapter display improvements:
- Shows start position time (h:mm:ss format) for each chapter
- Better duration formatting in trailing position
- "In progress" indicator with position time
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Audiobookshelf service for direct API communication
- Add ABS settings UI in settings screen (server URL, username, password)
- Implement chapter fetching from Audiobookshelf when MA doesn't provide chapters
- Add author image fetching priority: Audiobookshelf > Audnexus > Open Library
- Add book search by title in ABS libraries
- Add settings persistence for ABS configuration
This enables enhanced audiobook features:
- Author images from ABS (more reliable than external APIs)
- Chapter information for audiobooks
- Series data support (foundation for series tab)
- Better metadata from ABS
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add getAudiobookDetails API method to fetch full audiobook with chapters
- Load chapters when opening audiobook detail screen
- Show loading state while fetching chapters
- Display chapters in album-track style with:
- Square rounded number badges (checkmark when completed)
- Duration in trailing position
- "In progress" subtitle for current chapter
- Long-press to expand action button
- Show "No chapter information available" if none exist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Increase audiobook fetch limit from 500 to 10000 to show all books
- Add Audnexus API as primary source for author images (better coverage for audiobook authors)
- Keep Open Library as fallback with image existence verification
- Audnexus uses Audible data which has more audiobook author photos
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add hero tag constants for audiobooks and authors in hero_tags.dart
- Add Hero widgets to AudiobookDetailScreen (cover image and title)
- Add Hero widgets to AudiobookAuthorScreen (author image)
- Add Hero widgets to audiobook cards/list tiles in library screen
- Add Hero widgets to author cards/list tiles in library screen
- Add Hero widgets to audiobook cards/list tiles in author screen
- Pass heroTagSuffix through navigation to match animation tags
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add getAuthorImageUrl() to MetadataService using Open Library API
- Update AudiobookAuthorScreen to fetch and display author photo
- Update library screen Authors tab with author images in list/grid
- Images fetched in background, cached locally
- Falls back to initials if no image found
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add playAudiobook() method to MusicAssistantAPI
- Fix audiobook_detail_screen to use correct API methods:
- playAudiobook() instead of non-existent playMedia()
- seek() instead of non-existent queueCommandSeek()
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
MA returns authors and narrators as string arrays like ["Author Name"]
instead of Artist objects. Added parseArtistList helper that handles
both formats - strings get converted to Artist objects with just the name.
Fixes: type 'String' is not a subtype of type 'Map<String, dynamic>'
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove full response dump that was overwhelming logs
- Only log first item's keys and name/media_type
- Summarize parse errors instead of logging each one
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add verbose logging to getAudiobooks API call
- Log response structure, item count, and parsing results
- Add logging to _loadAudiobooks and _changeMediaType in library screen
- Track when Books tab is selected and API calls are made
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The EmptyState.custom factory constructor uses onRefresh parameter,
not actionLabel/onAction. Fixed both occurrences in the Books tab.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Audiobook and Chapter models with progress tracking
- Add MediaType enum values for audiobook, chapter, podcast, podcastEpisode
- Add getAudiobooks() API method using music/audiobooks/library_items endpoint
- Implement Authors tab with expandable sections grouped by author
- Implement All Books tab with grid/list views and progress indicators
- Keep Series tab as placeholder for future implementation
- Support favorites filtering for audiobooks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Top left: Music/Books/Podcasts icons for media type selection
- Tab row: Scrollable tabs on left, favorites+layout buttons on right
- Remove centered pill tabs for cleaner layout
- Reduce vertical space usage
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Long-press or swipe-up on Library icon shows Music/Books/Podcasts menu
- Remove pill tabs from library screen for cleaner layout
- Move LibraryMediaType to NavigationProvider for global state
- Custom bottom nav bar with gesture detection on Library item
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Favorites toggle: filled heart when active, outline when inactive
- Layout button: cycles through grid/list views
- Both buttons only show for Music media type
- PlayerSelector restored to top right position
- Add pill selector at top of Library for media type
- Music: Artists, Albums, (Tracks), Playlists tabs
- Books: Authors, All Books, Series tabs (placeholders)
- Podcasts: Coming soon placeholder
- Pills use accent color (filled when selected, muted when not)
- Favorites toggle only shows for Music type
- View mode toggle only shows for Music type
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add SyncService for background library synchronization
- Load library from database cache on startup for instant UI
- Sync from MA API in background without blocking
- Add toJson methods to Album, Artist, Track for serialization
- Update MusicAssistantProvider to use cache-first approach
- Add sync indicator to home screen app bar
- Pull-to-refresh triggers full library sync
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Images are fetched via MusicAssistantProvider.getImageUrl(), not stored
on the model objects. The Album/Artist/Track/Playlist models don't have
an imageUrl property.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 2 of audiobooks integration - local recently played:
- Add RecentlyPlayedService for tracking plays in local database
- Hook album plays into recording (album_details_screen.dart)
- Hook playlist plays into recording (playlist_details_screen.dart)
- Update getRecentAlbumsWithCache to prioritize local data
- Local data provides instant results on app startup
- Falls back to MA data if no local history yet
Benefits:
- Instant recently played display (no API wait)
- Per-profile isolation (each user sees their own history)
- Foundation for audiobook "Continue Listening"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
share_plus ^9.0.0 depends on web ^0.5.0 which conflicts with
drift ^2.22.1 which depends on web ^1.0.0
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 1 of audiobooks integration - establishes local storage foundation:
- Add Drift (SQLite) database with tables for profiles, recently played,
library cache, and sync metadata
- Add DatabaseService singleton for database access
- Add ProfileService for automatic user profile management
- Profiles auto-created from MA authentication (username) or manual entry
- Migration support for existing ownerName to profile
- Hook profile creation into MA login flow and login screen
- Update CI workflow to generate Drift code with build_runner
- Add comprehensive implementation plan document
This enables:
- Per-user recently played isolation (no cross-contamination)
- Fast app startup with cached data
- Foundation for audiobook progress tracking
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove isScrollable to distribute tabs evenly across width
- Add full-width bottom line that extends under view button
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Artist view: Move sort/view buttons inline with section headers
- Library: Move view toggle to tab row (right side of tabs)
- Artist grid: Use ArtistAvatar widget for proper fallback image loading
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Artist view: Right-align sort/view buttons, reduce gap from play buttons
- Artist view: Change year sort to ascending (oldest album first)
- Library: Move view toggle to app bar (next to player selector)
- Library: Fix artist grid - use AspectRatio for proper circles
- Library: Adjust grid aspect ratios for better spacing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
MA API returns year as a top-level field on Album, not inside metadata.
Now correctly parsing json['year'] in Album.fromJson.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix album aspect ratio in artist view to be square (using AspectRatio)
- Restore "In Library" vs "From Providers" album sections
- Add year to album names throughout the app (Album Name (2023))
- Add year, nameWithYear, and inLibrary getters to Album model
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When playing on SendSpin or another player, the builtin/local player
was filtered out of the switch list because it wasn't in playing/paused
state. Now the builtin player (ensemble_*) is always included so users
can switch back to local device from the notification.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Key improvements:
- Add proper state machine with transitional states (pausing, resuming, stopping)
- Reduce feed threshold from 8000 to 5000 frames (~104ms vs ~166ms)
- Reduce max chunks per feed from 12 to 8 (~200ms vs ~300ms)
- Add error callbacks (PcmErrorCallback) for operation failures
- Fix silent stream errors - now pause and emit error on stream failure
- Add release() timeout to prevent hangs
- play()/pause()/stop() now return bool for success/failure
Bug fixes:
- Fix sendspin_service.dart ternary always returning 'synchronized'
- Replace fragile _isPausePending flag with proper state machine
UI enhancements:
- Add public getters for Sendspin status in provider
- Add audio format info section in expanded player (48kHz • Stereo • 16-bit PCM)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Key changes:
- Limit feeding to 12 chunks (~300ms) per feed cycle instead of entire buffer
- Re-enable release() on pause with Future.delayed to avoid UI block
- Re-initialize PCM player on resume since we release on pause
- Make next/previous track non-blocking with unawaited pause
This should fix the 5-8 second delay when pressing pause/skip.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The pause fix (keeping _isPausePending=true) worked but broke resume.
When MA sends stream/start after resume, _onAudioData was ignoring
all audio because _isPausePending was still true.
Fix: Call _pcmAudioPlayer.play() in _handleSendspinStreamStart to:
- Clear _isPausePending flag
- Set state from paused to playing
- Allow _onAudioData to process incoming audio
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Attempt #8: The feed loop while() was starving the event loop.
Even with await, if native calls return quickly, the loop runs
continuously without letting UI events (like pause) be processed.
Added: await Future.delayed(Duration.zero) every 5 chunks to force
the event loop to process pending events like button taps.
Previous attempts focused on pause() but the real issue was the
feed loop monopolizing the isolate during playback.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previous fix didn't work - the issue was multiple blocking await calls
in the pause chain, not just the PCM player.
Changes:
- pausePlayer(): Use unawaited() for PCM pause and API calls
- pausePlayer(): Don't pause just_audio when in Sendspin mode
- pausePlayer(): Optimistically update _selectedPlayer state and notify
- playPauseSelectedPlayer(): Don't call refreshPlayers() after pause
- Player model: Add copyWith() method for optimistic updates
The key insight is that we don't need to wait for pause operations
to complete before updating the UI. The UI should update immediately
(optimistic) and the actual operations run in the background.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Root cause analysis (deep dive with 4 agents) revealed:
1. _isPausePending was cleared immediately in pause(), allowing
new feeds to start before pause was complete
2. Auto-resume in _onAudioData could trigger right after pause
3. _onFeedRequested could start new feeds during pause transition
4. No waiting for in-flight feed operations
Changes:
- Keep _isPausePending = true until play() is called
- Wait for in-flight feed operations with 100ms timeout
- Remove auto-resume from _onAudioData (provider calls play())
- Add explicit state check in _onFeedRequested callback
- Ignore all audio data when paused (don't even buffer)
This prevents the race condition where the feed loop could be
active during pause, causing FFI calls to block the UI thread.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The native flutter_pcm_sound release() was causing deadlocks.
New approach:
- pause() only sets Dart flags and clears buffer (no native calls)
- Native buffer is small (~170ms at 8000 frame threshold)
- Audio stops naturally as buffer drains
- Player stays initialized for instant resume
- play() just changes state and resumes feeding
This eliminates all native call race conditions on pause.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
pause() now sets only Dart state and returns immediately.
All native calls (setFeedCallback, release) run in a microtask
that executes after pause() returns, ensuring UI never blocks.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Instead of awaiting release() which can deadlock at native level,
now pause() returns immediately and schedules release asynchronously:
- pause() sets flags and clears buffer, returns instantly
- _releasePlayerAsync() handles cleanup in background
- 50ms delay before release to let feed loop exit
- UI thread is never blocked by native code
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The issue was that pause() used a polling loop that could time out
while feed() was still executing, causing a freeze/deadlock. Now:
- Added Completer to track when feed operations complete
- pause/stop/dispose await the Completer with 500ms timeout
- Clear feed callback before release() to prevent native triggers
- Added more _isPausePending checks in feed loop
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Added _isPausePending flag to stop feed operations during pause/stop
- Feed loop now checks pause flag and exits early when set
- Pause/stop wait for in-progress feed operations before releasing player
- Prevents FlutterPcmSound.release() being called while feed() is executing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- PCM pause now releases flutter_pcm_sound to stop audio immediately
instead of letting buffered audio drain (was causing 5-8 second delays)
- Play/resume re-initializes PCM player after release
- Added optimistic local pause/stop for builtin player - don't wait for
MA WebSocket round-trip
- Next/Previous also do optimistic local stop for immediate feedback
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When playing in radio mode, after the first track ends the PCM player
would be in paused state. When the next track started streaming,
audio data was received but never played because:
- _isStarted was true (from previous track)
- _state was paused (not ready or playing)
The fix automatically resumes playback when new audio data arrives
while in paused state, which handles track transitions in radio mode.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add pre-flight check for tracks with available provider mappings
- Skip tracks that lack available providers instead of sending them to MA
- Log detailed info about which tracks are playable vs unavailable
- Add smart retry logic that doesn't retry non-transient errors
- Improve user-facing error messages for unavailable content
- Reduce unnecessary retry delays for data-related failures
Fixes "No playable items found" errors when albums contain tracks
that don't have available streaming providers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add elapsed time tracking to PcmAudioPlayer based on bytes played
- Fix notification timer to detect Sendspin PCM players (not just ensemble_ prefix)
- Update stream handlers to manage position correctly:
- Reset position on stream/start (new track)
- Preserve position on stream/end (pause)
- Wire up 500ms notification updates using PCM elapsed time
This fixes the issue where notification showed 0:00 for entire track duration
during Sendspin PCM playback.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add onStreamStart and onStreamEnd callbacks to SendspinService
- Activate audioHandler foreground service when PCM streaming starts
to prevent Android from throttling background audio playback
- Stop PCM player and update notification when stream ends
- Reinitialize PCM player on stream start if needed for reliability
This fixes the "robotic slowed down" audio when screen turns off
by keeping the app in foreground service mode during PCM playback.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sendspin binary frames contain a 9-byte header before the actual PCM data:
- Byte 0: message type (4 = audio data)
- Bytes 1-8: timestamp (int64 microseconds for sync)
- Bytes 9+: actual PCM audio samples
Previously we were feeding the entire frame including the header to the
audio player, causing hissing/static instead of music.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The raw_sound package has an outdated Android build configuration
missing the namespace declaration required by newer AGP versions.
Switched to flutter_pcm_sound (^3.3.3) which is actively maintained
(last updated Oct 2025) and properly configured for modern Android.
Changes:
- Update pubspec.yaml to use flutter_pcm_sound
- Rewrite PcmAudioPlayer to use flutter_pcm_sound API:
- FlutterPcmSound.setup() for initialization
- FlutterPcmSound.feed(PcmArrayInt16) for audio data
- Callback-based feeding with setFeedCallback
- Convert Uint8List bytes to Int16 samples
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements raw PCM audio playback using the raw_sound package to
handle binary audio frames sent by Sendspin over WebSocket.
Changes:
- Add raw_sound dependency (^0.3.0+1) to pubspec.yaml
- Create PcmAudioPlayer service wrapper (lib/services/pcm_audio_player.dart)
- Configurable audio format (defaults to Sendspin: 48kHz, stereo, 16-bit)
- Stream-based audio feeding with buffering
- Play/pause/stop controls
- Periodic logging of playback stats
- Integrate PcmAudioPlayer into MusicAssistantProvider
- Initialize PCM player when Sendspin connects
- Connect audioDataStream to PCM player
- Control PCM player on pause/stop commands
- Clean up in disconnect/dispose
This completes the Sendspin audio streaming pipeline:
1. SendspinService receives binary WebSocket frames
2. Frames are emitted via audioDataStream
3. PcmAudioPlayer consumes and plays the raw PCM data
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The server sends binary WebSocket frames for PCM audio data, but
_handleMessage was trying to cast all messages to String for JSON
parsing. This caused hundreds of type cast errors:
"type '_Uint8ArrayView' is not a subtype of type 'String' in type cast"
Changes:
- Add dart:typed_data import for Uint8List
- Add SendspinAudioDataCallback typedef and onAudioData callback
- Add _audioDataController stream for raw PCM frames
- Add _isStreamingAudio and _audioFramesReceived state tracking
- Update _handleMessage to detect binary (List<int>) vs text (String)
- Add _handleBinaryAudioData method with periodic logging
- Add stream/start and stream/end message handlers
- Close _audioDataController in dispose()
This fixes the type cast errors and sets up infrastructure for PCM
audio playback (actual audio player integration is a separate task).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added player_added event listener that refreshes the player list when
the Sendspin player gets registered. This fixes the issue where the
local phone player wasn't appearing in the device list.
The problem was timing: the player list was fetched BEFORE the Sendspin
player finished registering in Music Assistant. Now when the player_added
event is received, the list is automatically refreshed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The initial client/state and client/time messages were being silently
dropped because they were sent while the connection state was still
'connecting' (before _waitForAck completed).
The _sendMessage method has a guard that blocks messages unless
state == connected OR allowDuringHandshake is true. The initial
messages need allowDuringHandshake since they must be sent immediately
after receiving server/hello, before the handshake completes.
Without this fix, the server times out after 5 seconds waiting for
the required initial state message.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Root cause: Server logs showed "ClientMessage has no subtype with
attribute 'type' equal to 'state'" - the client was using incorrect
message types that the aiosendspin library couldn't parse.
Changes:
- Fix state reporting: 'state' → 'client/state' with proper payload
- Fix heartbeat: 'ping' → 'client/time' with microsecond timestamp
- Fix disconnect: 'disconnect' → 'client/goodbye' with reason
- Send initial client/state immediately after server/hello (required)
- Handle server/time and group/update message types
- Reduce state report interval from 1s to 5s (was flooding server)
The server was disconnecting after 5 seconds because it couldn't parse
the malformed 'state' messages, triggering a "spec violation" timeout.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The _sendMessage method was checking for connected state before sending,
but during handshake the state is still 'connecting'. This caused the
hello message to be silently dropped, preventing registration.
Added allowDuringHandshake flag to bypass the state check for the
initial hello message.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The Sendspin protocol requires:
- Nested 'payload' object with client_id, name, version
- version as integer (not string)
- supported_roles array with versioned roles like 'player@v1'
- player_support object with supported_formats, buffer_capacity, supported_commands
- Handle 'server/hello' response type
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The Sendspin protocol requires the client to send a client/hello message
immediately after connecting. The server waits for this message before
registering the player. Previously we were waiting for the server to
speak first, causing a deadlock and connection timeout.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements Sendspin protocol support as a replacement for the deprecated
builtin_player API. This allows local playback on devices running Music
Assistant 2.7.0b20 or newer.
Key features:
- SendspinService for WebSocket connection management
- Smart connection fallback: external wss:// → local ws:// → WebRTC
- API methods for Sendspin connection info and WebRTC signaling
- Automatic detection when builtin_player API is unavailable
- Username-based player naming (same as builtin player)
- Persistent device ID to prevent ghost players
Connection strategy for external access:
1. If server is HTTPS, try wss://{server}/sendspin first
2. Fall back to local_ws_url from API
3. WebRTC fallback noted but not yet implemented
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Reverted Stack clipBehavior back to Clip.none
- Removed peek progress bar (was causing app to freeze/go white)
- Main progress bar still slides with content
The peek progress bar and Clip.hardEdge were causing the app to become
unresponsive with a white overlay after swiping.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Refactored peek progress bar into helper method _buildPeekProgressBar
- Added null safety checks for peek player elapsed time
- Changed Stack clipBehavior to hardEdge to prevent overflow issues
- This should fix the white rendering issue during swipe
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Progress bar now slides with album art, title, and artist during swipe
- Added peek player progress bar that slides in from opposite direction
- Peek progress shows the incoming player's playback position
- Non-playing peek players show no progress bar (as expected)
- Progress bar hidden during transition when peek content is available
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Removed direct links to music-assistant.io
- Added clear disclaimer that Ensemble is unofficial and not affiliated
with, endorsed by, or supported by the Music Assistant project
- Removed the "About Music Assistant" promotion section
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Bug: When swiping between players (especially between playing and
non-playing states), the album art, track title, and artist name
would disappear, leaving only the two-tone progress bar visible.
Root cause:
1. `_inTransition` flag was set without `setState`, causing rebuild
timing issues with provider state changes
2. Main content visibility condition `!(_inTransition && t < 0.1)`
would hide content during transitions, but if `_peekPlayer` was
null or cleared prematurely, peek content wouldn't render either
Fix:
1. Wrap `_inTransition = true` in `setState` for proper rebuild timing
2. Add `_peekPlayer != null` to main content visibility condition as
fallback - if peek content is unavailable during transition, main
content will show instead of showing nothing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove provider icons feature from expanded player (caused overlay issues)
- Remove provider icons setting from settings screen
- Remove provider icon methods from settings service
- Fix fullscreen album art tap in expanded player by using onTapUp
instead of onTap (resolves gesture arena conflict with vertical drag)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix expanded player parent GestureDetector to not intercept taps when expanded
- Add fullscreen album art to album details screen
- Both support pinch-to-zoom and tap/swipe to dismiss
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Mini player progress bar now starts after album art (not hidden)
- Fullscreen album art tap detection fixed with opaque hit behavior
- Provider icons positioning simplified and error handling improved
- Album view padding increased to 164px for better clearance
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Album view: Add 8px more bottom padding for mini player clearance
- Expanded player: Fix favourite button to properly handle provider mappings
- Expanded player: Add fullscreen album art on tap (pinch to zoom supported)
- Expanded player: Add optional provider icons on album art (Spotify, Subsonic, etc.)
- Settings: Add "Provider Icons" toggle under new Player section
- Mini player: Add two-tone progress bar background showing playback position
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 21:13:33 +00:00
215 changed files with 73823 additions and 7529 deletions
This is a Music Assistant client app (Ensemble) built with Flutter. The player system has undergone extensive development and now needs a comprehensive polish audit. The goal is buttery smooth 60fps animations and a premium user experience.
## Architecture Overview
The player system consists of three interconnected components:
1.**Mini Player** (collapsed state) - Floating card at bottom, shows current track
2.**Expanded Player** - Full-screen player with album art, controls, progress
3.**Queue Panel** - Slides in from right of expanded player, shows playback queue
│ ├── queue_panel.dart # Queue list with great_list_view
│ ├── chapters_panel.dart # Audiobook chapters
│ └── mini_player_content.dart
```
## CRITICAL CONSTRAINTS
1.**DO NOT MODIFY the queue list implementation** - It uses `great_list_view` package with `AutomaticAnimatedListView`. This was chosen specifically to avoid Flutter's grey screen overlay bug (#103804). The current implementation works correctly.
2.**Preserve all existing functionality** - Focus on polish, not rewrites
3.**Test changes incrementally** - This system is complex with many interdependencies
## Audit Areas
### 1. Animation Performance Audit
Use sub-agents to analyze each animation system:
**Agent 1: Expand/Collapse Animation**
- Analyze `_controller` and `_expandAnimation`
- Check for jank during morphing transition
- Verify lerp functions are efficient
- Look for unnecessary rebuilds during animation
- Check `RepaintBoundary` placement
**Agent 2: Queue Panel Animation**
- Analyze `_queuePanelController` and `_queuePanelAnimation`
- Verify slide-in/out is smooth
- Check interaction with expanded player state
- Verify the Listener-based swipe detection doesn't cause issues
**Agent 3: Player Selector Animation**
- Analyze horizontal swipe between players
- Check `_slideOffset` and peek player animations
- Verify finger-following feels natural
- Look for edge cases with rapid swipes
**Agent 4: Micro-interactions**
- Play/pause button animations
- Progress bar interactions
- Volume swipe overlay
- Favorite button feedback
- Skip button responsiveness
### 2. State Management Audit
**Check for:**
- Unnecessary `setState` calls during animations
- ValueNotifier usage efficiency
- Animation listener cleanup
- Memory leaks from subscriptions
- State synchronization between components
### 3. Visual Polish Audit
**Check for:**
- Color transitions during expand/collapse
- Text fade/scale animations
- Image loading placeholders
- Shadow/elevation consistency
- Border radius morphing smoothness
- Safe area handling
### 4. Gesture Conflict Resolution
**Verify:**
- Vertical drag (expand/collapse) vs horizontal drag (player switch)
- Queue panel swipe-right-to-close vs Dismissible swipe-left-to-delete
- Edge dead zones for Android back gesture
- Tap vs drag disambiguation
### 5. Edge Cases
**Test scenarios:**
- Rapid expand/collapse
- Swipe during animation
- Back button during animations
- Track change during queue scroll
- Network image loading failures
- Very long track/artist names
## Execution Strategy
```
ultrathink: true
```
### Phase 1: Discovery (Read-Only)
Launch parallel sub-agents to analyze each file:
- Read and understand the animation architecture
- Map all animation controllers and their relationships
This document describes adding Android Auto support to Ensemble. When a user connects their Android phone to a car head unit, Android Auto will browse and play music through Ensemble without touching the phone.
### What we're building
- A browse hierarchy (Favorites, Playlists, Albums, Radio) accessible from the car screen
- Playback from Android Auto routes through the builtin (local) player, which streams through MA as normal
- Voice search support
### Why builtin player only
Android Auto requires local audio output. The builtin player plays audio through `just_audio` on the device, which the car receives over Bluetooth. Remote MA players are irrelevant in a car context.
---
## 2. Architecture
### Provider reference
`MassivAudioHandler` has no reference to `MusicAssistantProvider` today. The handler is created before the provider (in `main()` before `runApp`). Solution: add a `setProvider()` method and call it from `_MusicAssistantAppState.initState()`.
```
main.dart
audioHandler = await AudioService.init(...) // global, created first
runApp(MusicAssistantApp())
_MusicAssistantAppState.initState()
_musicProvider = MusicAssistantProvider()
audioHandler.setProvider(_musicProvider) // NEW
```
### Browse → play call flow
```
Android Auto MassivAudioHandler MusicAssistantProvider
| `cat\|radio` | Radio stations category (browsable) |
| `playlist\|{provider}\|{itemId}` | A playlist (browsable) |
| `album\|{provider}\|{itemId}` | An album (browsable) |
| `track\|{tProvider}\|{tItemId}\|{ctxType}\|{ctxProvider}\|{ctxId}` | A track in context (playable) |
| `radio\|{provider}\|{itemId}` | A radio station (playable) |
The `ctxType|ctxProvider|ctxId` suffix in track IDs encodes the parent container. Context types: `plist`, `album`, `favs`, `search`.
**Track queue cache key** = `{ctxType}|{ctxProvider}|{ctxId}` (e.g. `plist|spotify|abc`). Stored when building children, looked up in `playFromMediaId`.
---
## 4. Browse Hierarchy
```
root
├── recent (playable, shown if something is/was playing)
├── cat|favorites (browsable → favorite tracks, all playable)
├── cat|playlists (browsable → list of playlists, each browsable)
├── cat|albums (browsable → list of albums, each browsable)
│ └── album|{provider}|{id} → tracks (playable)
└── cat|radio (browsable → radio stations, each playable)
```
Artists are **not included**. Navigating Artist → Albums → Album → Tracks is four taps — too many while driving. Can be added later.
---
## 5. Track Queue Cache
Tapping a track in Android Auto should play the full album/playlist from that track, not just the single track. The handler caches track lists when building children:
```dart
// Key: ctxKey e.g. "plist|spotify|abc" or "album|library|42" or "favs||"
// Value: ordered list of tracks in that container
Note: `audio_service` exports its own `MediaItem`. Use `ma.MediaItem`, `ma.Track`, `ma.Album`, `ma.Playlist` for app models. The bare `MediaItem` refers to the audio_service type throughout new methods.
### New fields
```dart
MusicAssistantProvider?_provider;
finalMap<String,ma.Track>_trackQueueCache={};// ctxKey -> tracks (see section 5)
Add ~200 lines. New imports, fields, `setProvider()`, `getChildren()`, `playFromMediaId()`, `search()`, `subscribeToChildren()`, and private builder helpers. All existing code is untouched.
---
## 8. Testing with DHU (No Car Required)
### Install DHU
In Android Studio: SDK Manager → SDK Tools → Android Auto Desktop Head Unit Emulator.
Or via command line:
```bash
sdkmanager "extras;google;auto"
```
### Enable Android Auto developer mode on phone
1. Settings → Apps → Android Auto → tap version number 10 times
2. In Android Auto developer settings: enable "Unknown sources"
<p>A minimalistic mobile client for <a href="https://music-assistant.io/">Music Assistant</a></p>
<p><strong>An unofficial mobile client for Music Assistant</strong></p>
<p>Stream your music library directly to your phone, or control playback on any connected speaker.</p>
</div>
@@ -11,41 +17,88 @@
## Disclaimer
**Ensemble is an unofficial, community-built mobile client for Music Assistant. It is not affiliated with, endorsed by, or supported by the Music Assistant project or its developers.**
This application was built with AI-assisted development using **Claude Code** and **Gemini CLI**.
---
## Also See
**[Ensemble TV](https://github.com/CollotsSpot/ensemble-tv)** - A simple Android TV client for Music Assistant that displays what's currently playing on your TV with remote control support.
---
## Features
### Local Playback
- **Stream to Your Phone** - Play music from your Music Assistant library directly on your mobile device
- **Stream to Your Phone** - Play music from your Music Assistant library directly on your mobile device via Sendspin protocol
- **Background Playback** - Music continues playing when the app is minimized
- **Media Notifications** - Control playback from your notification shade with album art display
- **Instant Response** - Pause/resume in ~300ms
### Remote Control
- **Multi-Player Support** - Control any speaker or device connected to Music Assistant
- **Device Selector** - Quickly switch between your phone and other players
- **Device Selector** - Swipe down on mini player to reveal all your devices
- **Multi-Room Grouping** - Long-press any player to sync it with the current player
**Important:** You need the **Music Assistant** URL, not your Home Assistant URL.
| Method | Status |
|--------|--------|
| Music Assistant native auth | Tested |
| No authentication | Tested |
| Authelia | Implemented, not recently tested |
| HTTP Basic Auth | Implemented, not recently tested |
To find the correct URL:
1. Open Music Assistant web UI
2. Go to **Settings** > **About**
3. Look for **Base URL** (e.g., `http://192.168.1.100:8095`)
**Note:** Development and testing is done against Music Assistant beta with native authentication enabled.
### Home Assistant Add-on Users
If you run Music Assistant as a Home Assistant add-on:
- Use the IP address of your Home Assistant server
- Do **not** use your Home Assistant URL or ingress URL
### Android Auto
Since Ensemble is not installed from the Play Store, you need to enable unknown sources in Android Auto:
1. Open **Android Auto** settings on your phone
2. Tap the **Version** number at the bottom 10 times to enable developer mode
3. Tap the three-dot menu and open **Developer settings**
4. Enable **Unknown sources**
5. Connect your phone to your car via USB or wirelessly
6. Ensemble will appear in Android Auto's app list
### Remote Access
For access outside your home network, you'll need to expose Music Assistant through a reverse proxy (e.g., Traefik, Nginx Proxy Manager, Cloudflare Tunnel).
## Requirements
- Music Assistant server (v2.7.0 or later recommended)
- Music Assistant server (v2.7.0 beta 20 or later recommended)
- Network connectivity to your Music Assistant server
- Android device (Android 5.0+)
- Audiobookshelf provider configured in Music Assistant (for audiobook features)
> **Note:** Local device playback (streaming to your phone) is not currently supported on Music Assistant beta 20+. This is due to MA replacing the built-in player with Sendspin. Support will be added once Sendspin is more stable. Remote control of other players works fine.
## Sponsors
## About Music Assistant
A huge thank you to my sponsors! 💖
Ensemble is a mobile client for [Music Assistant](https://music-assistant.io/), an open-source music library manager that integrates with various music sources and streaming providers.
With Ensemble, you can:
- **Stream locally** - Use your phone as a Music Assistant player
- **Control remotely** - Manage playback on smart speakers, Chromecast, Sonos, and more
- **Long-press context menus on all media items**: Albums, artists, tracks, playlists, podcasts, and radio stations now all support long-press to show a context menu with Play, Add to Library, and Favorite options.
#### Podcasts
- **Podcast episode detail screen**: Tap "View Details" from episode context menu to see full scrollable description, publish date, duration, with Play, Play On, and Add to Queue actions.
- **Episode sorting**: Sort podcast episodes by newest first, oldest first, alphabetical, or duration.
#### Audio Quality
- **Hi-res audio badge**: Track listings now show a hi-res badge when high quality audio is available.
### Improvements
#### UI/UX
- **New library icon**: Replaced confusing library icon with clearer book icon (book_2) across bottom nav, search filter, detail screens, and context menus. Filled when in library, outlined when not.
- **Player selector**: Replaced popup dialog with player select overlay for queue actions.
- **Search library filter**: Filled icon indicates active filter state without background highlight.
- **Context menu polish**: Slightly larger buttons (46px), wider menu (185px), consistent text colors that don't pick up adaptive theming.
- **Navigation transitions**: Smoother fade/slide animations between screens.
- **Library list optimization**: Standardized list item heights with itemExtent, reduced image cache sizes for smoother scrolling.
- **Palette extraction**: Moved to isolate for better UI responsiveness during color extraction.
- **Provider selects**: Optimized provider state watching to reduce unnecessary rebuilds.
### Bug Fixes
#### Audiobooks
- **Fixed audiobooks not loading after provider re-add**: When removing and re-adding an AudiobookShelf provider, the app would show 0 audiobooks because saved library paths referenced the old provider instance ID. Now automatically clears stale settings and falls back to the library_items API.
- **Fixed duplicate provider filter options**: After re-adding a provider, two entries for the same provider could appear in the library filter menu. Now deduplicates providers by name, keeping the one with more items.
- **Fixed multiple accounts of same service not showing**: When you have multiple accounts of the same service (e.g., two Spotify accounts for different family members), only one would appear in the library menu provider toggles. Now all accounts with distinct names are shown.
- **Fixed audiobook year sorting**: Sort by year now works correctly for audiobooks. Fixed two issues:
- Year data is now properly extracted from ABS's `metadata.release_date` field (handles both "2024" and "2024-01-15" formats)
- Sort handler now properly supports ascending and descending year sorts
#### Library Sorting
- **Fixed letter scrollbar showing irrelevant letters**: When sorting by date or play count, the scrollbar popup now hides instead of showing meaningless letters. Shows contextual info based on sort type (letters for alphabetical, years for year sort, hidden for date/count sorts).
- **Fixed sorting not working for date added, last played, play count**: These sort options now correctly preserve server-side sorting instead of re-sorting alphabetically on the client. Affected albums, artists, and tracks tabs.
- **Fixed playlists sorting**: Modified, last played, and play count sorting now works for playlists by properly requesting sorted data from the server.
- **Fixed ascending/descending not working**: Sort direction now properly applies for all sort types across albums, artists, playlists, and audiobooks.
- **Fixed list not updating when sort order changes**: Added sort order to ListView/GridView keys to force UI rebuilds when changing sort direction.
#### Provider Management
- **Fixed stale provider IDs persisting in settings**: When toggling providers, stale provider instance IDs (from removed providers) are now automatically cleaned up from saved settings.
#### Playback
- **Fixed Spotify external source detection** (issue #27): Properly detects when Spotify is the music provider.
- **Fixed foreground service not stopping** (issue #51): Auto-stops after 30 minutes idle.
- **Fixed Sendspin proxy auth**: Now works correctly for local network connections.
#### UI Fixes
- **Fixed nav bar hidden when no player selected**: Navigation bar now always visible.
- Added `updateCachedAlbums()`, `updateCachedArtists()`, `updateCachedPlaylists()` methods to SyncService for proper cache synchronization with sorted data
- Provider filter now validates against available providers when toggling, not just on initial load
- Removed Selector caching from albums and artists tabs to ensure proper rebuilds when source tracking updates
- Options menu provider toggles now use Consumer to watch Provider state directly
- Centralized player selector logic in GlobalPlayerOverlay.showPlayerSelectorForAction()
Add audiobook support to Ensemble with automatic user profile separation based on Music Assistant authentication. Includes local database for performance caching and per-user data isolation.
---
## Phase 1: Database Foundation
**Goal:** Add persistent local storage layer using Drift (SQLite)
This document details the implementation of per-provider sync with source tracking, enabling instant client-side filtering that properly differentiates between multiple accounts of the same provider type (e.g., two different Spotify accounts).
## Problem Statement
### The Core Issue
When a user has multiple accounts of the same provider type (e.g., two Spotify accounts: "Chris Laycock" and "Bababagoly"), the existing provider filter couldn't differentiate between them.
**Why?** Music Assistant's `provider_mappings` field on media items shows which providers **CAN PLAY** an item, not which provider **ADDED** it. When both Spotify accounts can play the same artist, both appear in `provider_mappings`, making client-side filtering impossible.
### Previous Approach (Failed)
The initial implementation tried to filter client-side using `provider_mappings`:
**Result:** Both Spotify accounts showed the same items because `provider_mappings` contained both account IDs for shared content.
### Race Condition Issue
Additionally, when rapidly toggling providers, a race condition occurred:
1. User toggles Provider A off
2. Sync starts for remaining providers
3. User toggles Provider B off before sync completes
4. First sync completes with wrong filter state
5. UI shows incorrect results
## Solution: Per-Provider Sync with Source Tracking
### Concept
Instead of relying on `provider_mappings`, we:
1.**Sync each provider separately** using the MA API's `provider` parameter
2.**Tag each item** with which provider instance(s) returned it
3.**Store source tracking** in the local database
4.**Filter client-side** using the source tracking data (instant)
5.**Background sync** keeps data fresh without blocking UI
### Why This Works
When we call the MA API with `provider: ["spotify--RKnr3jKx"]`, the server returns **only items from that specific account's library**. By syncing each provider separately and remembering which provider returned each item, we can accurately filter.
## Implementation Details
### 1. Database Schema Update
**File:**`lib/database/database.dart`
Added `sourceProviders` column to `LibraryCache` table:
```dart
classLibraryCacheextendsTable{
// ... existing columns ...
/// Provider instance IDs that provided this item (JSON array)
/// Used for client-side filtering by source provider
This document captures the context and learnings from an attempted implementation of provider filtering in the Ensemble app. The goal was to allow users to filter their library to show only items from a specific music provider (e.g., only one Spotify account when multiple are configured).
## The Problem
Users with multiple music provider accounts (e.g., two Spotify accounts: one personal, one family) want to filter their library view to show only items from one specific account. The Music Assistant (MA) web UI successfully implements this feature.
## What Works vs What Failed
### Works: Different Provider Types
Filtering between different provider types (e.g., Spotify vs Tidal vs Plex) works correctly. Each provider type has distinct `provider_mappings` entries.
### Failed: Multiple Accounts of Same Provider
Filtering between multiple Spotify accounts (e.g., `spotify--abc123` vs `spotify--xyz789`) does NOT work correctly. The same library items appear regardless of which Spotify account is selected.
## Root Cause Analysis
### The provider_mappings Problem
MA's `provider_mappings` field on media items does NOT indicate "which account ADDED this item to the library." Instead, it indicates "which providers CAN PLAY this item."
Example: An artist "Ari Mason" that exists ONLY in User A's Spotify library will show provider_mappings containing BOTH Spotify accounts:
1. MA's internal database stores that the artist exists
2. MA knows both Spotify accounts CAN play this artist (Spotify has the content)
3. MA doesn't track WHICH account originally added the item to the library
### MA Web UI Behavior
The MA web UI's provider filter works differently - it appears to use server-side filtering via the `provider` parameter on library_items API calls. When you select a single provider in MA settings, the API returns only items from that provider's library.
## API Investigation
### Server-Side Filtering Parameter
The MA server's `library_items` method accepts a `provider` parameter:
- Parameter name: `provider` (string or list of strings)
The core challenge is that MA's `provider_mappings` indicates playability, not ownership. Server-side filtering via the `provider` parameter should work but needs further investigation to understand why it doesn't differentiate between multiple accounts of the same provider type.
The implementation is preserved in the git stash for future reference. The sorting/dropdown improvements were extracted and committed separately.
This session focused on fixing provider filtering issues discovered after the initial provider filtering improvements:
1. Audiobooks were not responding to provider filter changes (browse API ignoring `providerInstanceIds`)
2. UI not updating instantly when toggling providers (missing `notifyListeners()` call)
3. Audiobook tabs not rebuilding on provider changes (using `context.read` instead of `Selector`)
---
## Problem Statement
After implementing capability-based provider filtering for the music library, two issues remained:
### Issue 1: Audiobooks Not Filtering During Sync
The bug report showed:
```
📊 Source tracking: 252 albums, 87 artists have provider info
📚 Total audiobooks from browse: 186
```
- Albums and artists had provider source tracking
- Audiobooks had NO provider source tracking (not listed)
- All 186 audiobooks were returned regardless of provider selection
### Issue 2: UI Not Updating Instantly
When toggling providers in the library menu:
- Music tabs weren't updating immediately
- Audiobooks required manual pull-to-refresh
- Changes only appeared after ~600ms (debounce timer)
---
## Root Cause Analysis
### Cause 1: Browse API Ignoring Provider Filter
In `lib/services/music_assistant_api.dart`, when ABS library filtering is enabled, the `getAudiobooks()` function uses the browse API but was **ignoring** the `providerInstanceIds` parameter:
```dart
// BEFORE: providerInstanceIds was ignored when using browse API
In `lib/providers/music_assistant_provider.dart`, the `toggleProviderEnabled()` function only called `notifyListeners()`**inside** the 600ms debounce timer:
```dart
// BEFORE: notifyListeners only called after 600ms debounce
1.**Series Tab:** The Series tab still doesn't filter by provider. Series are fetched as `AudiobookSeries` objects without per-provider tracking.
2.**Mixed Provider Scenario:** If user has both Spotify audiobooks and ABS audiobooks, and ABS library filtering is enabled, Spotify audiobooks may not be fetched because the browse API path is taken. This is a pre-existing architectural limitation.
3.**Enabling Providers:** When ENABLING a previously disabled provider, the UI updates immediately but shows empty until the background sync completes (because the cache doesn't have that provider's data yet). This is expected behavior.
This session focused on fixing provider filtering for podcasts, resolving iTunes podcast provider issues, and implementing optimistic updates for library buttons across detail screens.
**Problem**: Podcasts added from iTunes search weren't showing in library and iTunes provider filter showed 0 count.
**Investigation**:
- Added `itunes_podcasts` to provider capabilities and music provider domains
- Added synthetic provider instance creation for search-only providers
- Added debug logging to understand podcast loading
**Resolution**: The issue was with Music Assistant's iTunes podcast provider itself - podcasts added via iTunes weren't being properly stored in the MA library. The user removed the iTunes provider from MA and re-added podcasts via Spotify search as a workaround.
**Files Modified**:
-`lib/models/provider_instance.dart` - Added `itunes_podcasts` to capabilities
### 3. Episode Loading Failing for Non-Library Podcasts
**Problem**: When a podcast was removed from library, the old library ID became invalid and episodes wouldn't load.
**Solution**: Added fallback logic to try loading episodes via provider mappings when the primary ID fails.
| `lib/screens/album_details_screen.dart` | Optimistic updates for library button |
| `lib/screens/artist_details_screen.dart` | Optimistic updates for library button |
## Key Learnings
1.**inLibrary field**: Music Assistant's provider mappings include an `in_library` field that indicates whether the user added the item from that specific provider account. This is essential for accurate per-provider filtering.
2.**iTunes podcast provider**: The MA iTunes podcast provider is for search only - it doesn't properly store items in the library. Users should add podcasts from streaming providers like Spotify instead.
3.**Optimistic updates**: For better UX, library toggle buttons should update immediately and only revert if the API call fails, rather than waiting for API response.
4.**Search vs Library filtering**: Server-side provider filtering should only apply to library views, not search results (unless the "in library" toggle is enabled).
This session focused on improving the provider filtering system across the app, building on the per-provider sync implementation. Key improvements include a cleaner capability-based architecture, fixing filter visibility bugs, and extending filtering to the audiobooks library.
---
## Changes Made
### 1. Provider Capability Mapping
**File:**`lib/models/provider_instance.dart`
Replaced the clunky item-count-based filtering with a static capability map that defines what content types each provider domain supports.
- Updated PageStorageKey to include provider count for proper rebuild
---
## Bug Fixes
### Provider Filter Menu Disappearing
**Problem:** When providers were deselected, the background sync would clear the cache and only fetch data for enabled providers. Disabled providers then had 0 items, and the menu required `> 1 providers WITH items` to show. The entire Providers section would disappear.
**Solution:** Changed to capability-based filtering. The menu now shows all providers that support the category, regardless of item count.
### Audiobooks Not Filtering by Provider
**Problem:** Audiobooks tabs loaded data once into `_audiobooks` and displayed it without provider filtering. Toggling providers in the menu had no effect.
**Solution:** Added inline provider filtering during build (same pattern as music tabs).
---
## Files Changed
| File | Changes |
|------|---------|
| `lib/models/provider_instance.dart` | Added `providerCapabilities` map and helper methods |
| `lib/providers/music_assistant_provider.dart` | Updated `getRelevantProvidersForCategory()` to filter by capability |
1.**Series Tab:** The Series tab in audiobooks doesn't filter by provider yet. Series are fetched separately as `AudiobookSeries` objects and don't have the same source tracking as individual audiobooks.
2.**First Sync Required:** Source tracking only works after at least one sync with providers enabled.
---
## Testing Checklist
- [x] Music tabs filter by provider correctly
- [x] Provider menu shows only relevant providers per category
- [x] Radio providers don't appear in Artists/Albums tabs
- [x] Spotify appears in Audiobooks tab (has audiobooks)
- [x] Audiobooks Authors tab filters by provider
- [x] Audiobooks All Books tab filters by provider
- [ ] Audiobooks Series tab filters by provider (not implemented)
- [x] Disabled providers can be re-enabled from menu
- [x] Settings screen no longer has provider toggles
---
## Related Documentation
-`docs/PER_PROVIDER_SYNC_IMPLEMENTATION.md` - Original per-provider sync implementation
# Feature: Replace "Play On" Popups with Player Selector
## Overview
Replace the `PlayerPickerSheet` popup modal with the existing Player Selector (reveal overlay) when users tap "Play on" buttons throughout the app. This provides a consistent, richer player selection experience.
## Current State
### PlayerPickerSheet Popup
Used in 7+ places:
-`album_details_screen.dart` - Play Album, Play From Here, Play Radio
-`playlist_details_screen.dart` - Play Playlist, Play Radio
-`search_screen.dart` - Play album/playlist/audiobook/radio from search
-`podcast_detail_screen.dart` - Play Episode
-`audiobook_detail_screen.dart` - Play Audiobook
-`queue_screen.dart` - Transfer Queue (special case - keep popup)
This document provides guidance for implementing remote connection to Music Assistant servers via WebRTC. Remote connection allows the Ensemble app to connect to MA servers outside the local network using Nabu Casa's WebRTC signaling infrastructure.
## Current Status: On Hold
Remote connection was partially implemented but put on hold due to a critical bug where the `ma-api` data channel becomes one-way after 1-3 minutes. See `MA_WEBRTC_BUG_REPORT.md` for details.
### 1. One-Way Channel Death (CRITICAL - UNSOLVED)
The `ma-api` channel stops receiving after 1-3 minutes while `sendspin` continues working. This occurs in both flutter_webrtc and WebView implementations.
**Workaround options:**
- Detect via health check and force reconnection
- Try single-channel multiplexing (see `PLAN_B_SINGLE_CHANNEL.md`)
- Report to MA team and wait for server-side fix
### 2. flutter_webrtc Quirks
-`RTCDataChannel.state` may show "open" when actually dead
-`bufferedAmount` accuracy varies by platform
-`onBufferedAmountLow` callback support inconsistent
# WebRTC Data Channel Failure on Android - Bug Report
## Summary
When connecting to Music Assistant via WebRTC remote access (Nabu Casa), the `ma-api` data channel becomes **one-way** (can send, cannot receive) after 1-3 minutes of use, while the `sendspin` channel continues working normally.
## Environment
- **MA Server**: 2.8.0b7 (Home Assistant add-on)
- **Client**: Android app (Flutter) connecting via Nabu Casa remote access
- **Remote Access**: WebRTC via `wss://signaling.music-assistant.io/ws`
## Reproduction Steps
1. Connect to MA server via WebRTC remote access (not local network)
2. Use the app normally - browse library, view players
3. After 1-3 minutes, the connection degrades
4. Commands time out, but signaling ping/pong continues working
## Observed Behavior
### Timeline from logs:
```
07:14:20 - Working normally
Health: API sent:51 recv:54 (balanced)
07:15:57 - Receive stops
Health: API sent:97 recv:97, lastRecv:12s ago
07:16:02 - Timeouts begin
"TimeoutException: Command timeout: players/all"
07:16:17 - Sendspin still works!
"Sendspin: Received server/time response"
Health: API sent:114 recv:97, lastRecv:27s ago
07:17:30 - Forced reconnect
Health: API sent:137 recv:97, lastRecv:42s ago
"Health check detected stale connection"
```
### Key Observation
| Channel | Traffic Pattern | Status |
|---------|-----------------|--------|
| `ma-api` | High (10+ msg/s, large payloads) | **FAILS** - stops receiving |
| `sendspin` | Low (~1 msg/5s, small payloads) | **SURVIVES** - continues working |
| Signaling | Ping/pong every 15s | **SURVIVES** - continues working |
The WebRTC peer connection remains in "connected" state. ICE connection shows "completed". Only the ma-api data channel stops receiving.
## Testing Performed
### Test 1: flutter_webrtc (Native WebRTC)
- **Result**: ma-api channel dies after 1-3 minutes
- **Sendspin**: Continues working
### Test 2: WebView-based WebRTC (Browser engine)
- **Hypothesis**: Use Android WebView's browser-native WebRTC, since MA Web UI works
The MA Web UI runs in a **visible, active browser tab**. Android may throttle background apps differently, but this doesn't explain why sendspin survives while ma-api dies on the same connection.
## Logs
### Health Check Pattern (shows channel death)
```
07:14:25 API sent:51 recv:54, lastRecv:2s ago <- Working
07:15:35 API sent:87 recv:91, lastRecv:2s ago <- Working
07:16:05 API sent:108 recv:97, lastRecv:17s ago <- DYING (gap growing)
07:16:15 API sent:114 recv:97, lastRecv:27s ago <- DEAD
07:16:45 API sent:130 recv:97, lastRecv:27s ago <- Still sending, no receive
07:17:30 API sent:137 recv:97, lastRecv:42s ago <- Forced reconnect
```
### Sendspin Still Working During Failure
```
07:16:17 Sendspin: Received server/time response <- Still alive!
07:16:47 Sendspin: Received server/time response <- Still alive!
```
## Requested Information
1. Data channel creation code/settings for both channels
2. Any server-side differences in channel handling
3. TURN server configuration details
4. Whether this has been reported by other Android/mobile clients
## Workaround
Currently using local WebSocket connection when on same network. Remote access via WebRTC is unusable for sustained use.
If upgrading flutter_webrtc doesn't fix the one-way channel failure, the root cause is likely a bug in how flutter_webrtc (or underlying libwebrtc) handles **multiple data channels**.
Evidence:
- MA Web UI works fine with local playback (same server, browser WebRTC)
- Our app uses flutter_webrtc which has known issues:
- [Issue #1428](https://github.com/flutter-webrtc/flutter-webrtc/issues/1428): Threading issues causing data loss
Track all fixes attempted for the WebRTC data channel disconnection issues in Ensemble's remote connection feature. This document should be referenced when debugging future disconnection problems.
---
## Problem Summary
The WebRTC data channel between the Ensemble mobile app and Music Assistant server can fail in several ways:
1.**One-way failure**: Data channel appears "open" but only sends work, receives stop
2.**Silent disconnect**: No error events fire, channel just stops working
3.**ICE failure**: ICE connection fails, properly detected but causes disconnect
4.**Signaling disconnect**: Signaling WebSocket closes, causing ICE/WebRTC to fail
---
## Key Files
| File | Purpose |
|------|---------|
| `lib/services/remote/webrtc_connection.dart` | WebRTC peer connection, data channel management |
| `lib/services/remote/remote_bridge.dart` | WebSocket bridge, health monitoring, reconnection logic |
| `lib/services/remote/signaling_client.dart` | Signaling server WebSocket connection |
---
## Implemented Fixes (Chronological)
### Fix 1: Data Channel Keepalive (Original)
**Problem**: Data channel could silently die without any error events firing.
**Solution**: Added periodic keepalive ping/pong on the data channel itself.
**Code** (`webrtc_connection.dart`):
-`_keepaliveTimer`: Runs every 10 seconds
-`_checkKeepalive()`: Checks time since last data received
-`_sendKeepalivePing()`: Sends JSON ping message
-`_recordDataReceived()`: Called on every message receipt
The flutter_webrtc library has reliability issues causing data channel failures during remote connections via Nabu Casa. The ma-api data channel becomes one-way (can send, but receives stop) after some time, while the sendspin channel continues working.
## Approaches Tried
### 1. Original Implementation (flutter_webrtc)
- **Status**: Fails
- **Issue**: ma-api data channel goes one-way after some time
"theAudioDbApiKeyHint": "\"2\" für kostenlosen Zugang oder Premium-Schlüssel",
"audiobookLibraries": "Hörbuch-Bibliotheken",
"chooseAudiobookLibraries": "Wähle welche Audiobookshelf-Bibliotheken einbezogen werden",
"unknownLibrary": "Unbekannte Bibliothek",
"noSeriesFound": "Keine Serien gefunden",
"ensembleBugReport": "Ensemble Fehlerbericht",
"byOwner": "Von {owner}",
"noSeriesAvailable": "Keine Serien in deiner Hörbuch-Bibliothek verfügbar.\nZiehe zum Aktualisieren.",
"pullToLoadSeries": "Ziehe nach unten um Serien\nvon Music Assistant zu laden",
"search": "Suche",
"metadataApisDescription": "Künstlerbilder werden automatisch von Deezer abgerufen. Füge unten API-Schlüssel für Künstlerbiografien und Albenbeschreibungen hinzu.",
"lastFmApiKey": "Last.fm API-Schlüssel",
"lastFmApiKeyHint": "Kostenlosen Schlüssel auf last.fm/api holen",
"swipeToSwitchDevice": "Wischen um Gerät zu wechseln",
"chapterNumber": "Kapitel {number}",
"pcmAudio": "PCM-Audio",
"playOn": "Abspielen auf...",
"addAlbumToQueueOn": "Album zur Warteschlange hinzufügen auf...",
"showRandomArtists": "Mostrar artistas aleatorios para descubrir",
"showRandomAlbums": "Mostrar álbumes aleatorios para descubrir",
"showAudiobooksInProgress": "Mostrar audiolibros en progreso",
"showRandomAudiobooks": "Mostrar audiolibros aleatorios para descubrir",
"showRandomSeries": "Mostrar series de audiolibros aleatorias para descubrir",
"showFavoriteAlbums": "Mostrar una fila de tus álbumes favoritos",
"showFavoriteArtists": "Mostrar una fila de tus artistas favoritos",
"showFavoriteTracks": "Mostrar una fila de tus canciones favoritas",
"extractColorsFromArtwork": "Extraer colores de las portadas de álbumes y artistas",
"chooseHomeScreenRows": "Elige qué filas mostrar en la pantalla de inicio",
"addedToFavorites": "Añadido a favoritos",
"removedFromFavorites": "Eliminado de favoritos",
"unknown": "Desconocido",
"noUpcomingTracks": "No hay canciones próximas",
"showAll": "Mostrar todo",
"showFavoritesOnly": "Mostrar solo favoritos",
"changeView": "Cambiar vista",
"authors": "Autores",
"series": "Series",
"shows": "Programas",
"podcasts": "Podcasts",
"podcastSupportComingSoon": "Soporte de podcasts próximamente",
"playlist": "Lista de reproducción",
"connectionError": "Error de conexión",
"twoColumnGrid": "Cuadrícula de 2 columnas",
"threeColumnGrid": "Cuadrícula de 3 columnas",
"fromProviders": "De proveedores",
"resume": "Continuar",
"about": "Acerca de",
"inProgress": "En progreso",
"narratedBy": "Narrado por {narrators}",
"@narratedBy": {
"placeholders": {
"narrators": {
"type": "String"
}
}
},
"unknownNarrator": "Narrador desconocido",
"unknownAuthor": "Autor desconocido",
"loadingChapters": "Cargando capítulos...",
"noChapterInfoAvailable": "No hay información de capítulos disponible",
"percentComplete": "{percent}% completado",
"@percentComplete": {
"placeholders": {
"percent": {
"type": "int"
}
}
},
"theAudioDbApiKey": "Clave API de TheAudioDB",
"theAudioDbApiKeyHint": "Usa \"2\" para nivel gratuito o clave premium",
"audiobookLibraries": "Bibliotecas de audiolibros",
"chooseAudiobookLibraries": "Elige qué bibliotecas de Audiobookshelf incluir",
"unknownLibrary": "Biblioteca desconocida",
"noSeriesFound": "No se encontraron series",
"ensembleBugReport": "Informe de errores de Ensemble",
"byOwner": "Por {owner}",
"@byOwner": {
"placeholders": {
"owner": {
"type": "String"
}
}
},
"noSeriesAvailable": "No hay series disponibles en tu biblioteca de audiolibros.\nDesliza para actualizar.",
"pullToLoadSeries": "Desliza hacia abajo para cargar series\ndesde Music Assistant",
"search": "Buscar",
"metadataApisDescription": "Las imágenes de artistas se obtienen automáticamente de Deezer. Añade claves API a continuación para biografías de artistas y descripciones de álbumes.",
"lastFmApiKey": "Clave API de Last.fm",
"lastFmApiKeyHint": "Obtén una clave gratuita en last.fm/api",
"swipeToSwitchDevice": "Desliza para cambiar dispositivo",
"chapterNumber": "Capítulo {number}",
"@chapterNumber": {
"placeholders": {
"number": {
"type": "int"
}
}
},
"pcmAudio": "Audio PCM",
"playOn": "Reproducir en...",
"addAlbumToQueueOn": "Añadir álbum a la cola en...",
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.