Required by F-Droid for Flutter apps using --split-per-abi: each
per-ABI APK gets a unique versionCode = base * 10 + abiCode
(armeabi-v7a=1, arm64-v8a=2, x86_64=3).
Co-Authored-By: Claude Opus 4.7 <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>
2026-01-02 08:48:11 +00:00
198 changed files with 54026 additions and 10068 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"
You are tasked with conducting an intensive, comprehensive audit of the **Ensemble** Flutter application - a Music Assistant client for Android with multi-device synchronization, local playback, and Sendspin raw audio streaming capabilities.
**Codebase Statistics:**
- 90 Dart files, ~40,583 lines of code
- 18 screens, 27 services, 6 providers
- Architecture: Provider pattern with ChangeNotifier
The Ensemble codebase demonstrates **strong architectural foundations** with a well-organized service layer, effective caching strategies, and sophisticated audio handling. However, the audit revealed **critical security vulnerabilities**, **zero test coverage**, and significant **technical debt** that must be addressed before production release.
The Ensemble app has a solid foundation with well-designed patterns for caching, audio handling, and multi-device synchronization. However, **critical security vulnerabilities** (plaintext credentials), **zero test coverage**, and a **4,469-line god class** represent significant risks that must be addressed before any production deployment.
# Music Assistant 2.8.0 Beta 14 — Impact on Ensemble
**Date:** 2026-03-03
**Current server:** 2.8.0 beta 12
**Target server:** 2.8.0 beta 14 (released March 2, 2026)
---
## Summary
Beta 14 is a large release (50+ PRs). The headline change — merging multi-protocol players into a single entity — directly overlaps with Ensemble's client-side Cast↔Sendspin consolidation logic. Sendspin also received a major v4.0 overhaul. Most changes should degrade gracefully but player ID persistence will break on upgrade.
---
## Breaking: Player Merging (PR #3150)
MA now merges players that support multiple protocols (e.g. a Chromecast with Cast + Sendspin) into a **single player entity** server-side. Previously these appeared as two separate players.
> *"After this change, all your players will be reset. If you previously had multiple players in Music Assistant per protocol, this will now show up as one single player."*
### What Ensemble does today
Ensemble handles this client-side in `music_assistant_provider.dart:4271-4406`:
- When Sendspin is grouped: hides Cast player, shows Sendspin (renamed to base name)
- When ungrouped: hides Sendspin, shows original Cast
- Mappings persisted to database for cold start restoration
### Impact on Ensemble
| Component | Location | Impact | Severity |
|---|---|---|---|
| `_castToSendspinIdMap` + consolidation logic | `provider:4271-4406` | Dead code — server no longer sends separate players | Medium |
| `isManuallySyncedPlayer()` ID translation | `provider:533-595` | Lookups against stale map fail silently (returns `false`) | Low |
| Player selection persistence | Settings + DB | **Stored player IDs invalid after upgrade** — all players reset | **High** |
| `_handlePlayerUpdatedEvent()` ID matching | `provider:2956-3358` | Events arrive with new merged IDs, old translation won't match | Medium |
| Database-persisted Cast↔Sendspin mappings | `DatabaseService` | Stale data, won't match anything | Low |
### Assessment
The consolidation logic should degrade gracefully — it won't find Sendspin suffixes to strip or Cast pairs to merge, so the server's already-merged players pass through `filterPlayers()` as-is. The main risk is **player selection breaking** because saved player IDs no longer exist.
### Action required
1. Verify the existing fallback logic (`provider:4427-4465`) gracefully re-selects when the stored `selectedPlayerId` doesn't exist in the player list
2. Remove or gate the Cast↔Sendspin consolidation code behind a server version check (on beta 14+ it's unnecessary and could cause confusion)
3. Clear stale database mappings on first connection to a beta 14+ server
---
## Sendspin v4.0 (PR #3158)
Major rewrite of the Sendspin server.
### Changes
- New `SendspinPlaybackSession` coordinator architecture
- Per-member DSP pipelines, dynamic group membership, late-join audio backfill
- Max buffer capacity 5 → 30 seconds
- Multiple Sendspin servers with automatic failover
| Per-client codec options | `stream/start` may contain new fields — safe if Ensemble ignores unknown fields |
| Multiple servers with failover | Ensemble's 3-tier connection strategy may need awareness |
| Buffer increase | No client impact |
### Action required
1. Test Sendspin connection against beta 14 — verify `client/hello`, `stream/start`, state reporting all still work
2. Confirm unknown fields in protocol messages are ignored gracefully
---
## Sendspin-Chromecast Bridge (PR #3255)
Replaces the inline experimental Sendspin mode in `ChromecastPlayer` with a dedicated bridge. Cast devices now use a Cast receiver app with an embedded JS Sendspin client.
### Impact on Ensemble
- **Direct impact: None** — Ensemble is a Sendspin client itself, not going through Cast
- **Indirect:** Changes how grouped Cast players behave, which could affect `GroupVolumeManager`
---
## Group Volume / Grouping Fixes
| PR | Change | Ensemble Impact |
|---|---|---|
| #3192 | Fix grouping for players whose native protocol is also a protocol of other players | Bidirectional grouping translation — may affect group detection logic |
| #3205 | Fix group mute for protocol-synced players (`player.state.group_members` vs `player.group_members`) | `GroupVolumeManager` may see different/correct member lists |
| #3277 | Fix `volume_up`/`volume_down` for group players | Server-side fix, should improve behavior |
| #3118 | Fix sync groups losing members on power off | Affects dynamic group management |
### Action required
1. Test group volume behavior — may "just work better" with server-side fixes
2. Verify `GroupVolumeManager` handles any changes to `group_members` field population
---
## Sendspin Bug Fixes
| PR | Fix | Ensemble Relevance |
|---|---|---|
| #3191 | DSP not applying for Sendspin players | DSP config stored on parent player, looked up by protocol player ID — now fixed |
<p><strong>An unofficial mobile client for Music Assistant</strong></p>
@@ -17,47 +23,82 @@ This application was built with AI-assisted development using **Claude Code** an
---
## 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 beta 20 or later)
- 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)
- **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()
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
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.