Sync groups, volume management and UI bug fixes.#69
Merged
CollotsSpot merged 1 commit intoCollotsSpot:masterfrom Feb 9, 2026
Merged
Sync groups, volume management and UI bug fixes.#69CollotsSpot merged 1 commit intoCollotsSpot:masterfrom
CollotsSpot merged 1 commit intoCollotsSpot:masterfrom
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
All changes have been tested and verified with
dart analyzeshowing no new issues.Issue: Volume control for Music Assistant sync groups had two distinct problems depending on how the group was created:
Root Cause: The Music Assistant API returns
volume_level: nullfor certain group players (pre-configured speaker groups withprovider == '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
GroupVolumeManagerservice with dual-tier volume protection: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:memberNewVol = (memberCurrentVol * newVol / leaderCurrentVol).round()(handle zero case with absolute).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:
MainActivity.kt(~120 lines) - VolumeContentObserver, MethodChannel commands, lifecycle.hardware_volume_service.dart(~65 lines) - streams, observer methods, sync.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.dartIssue: 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.dartIssue: 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.dartIssue: 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.dartIssue: Crash on library tabs.
Root Cause: context.select in sliver context.
Fix: Use Consumer wrappers for tabs.
File:
lib/widgets/player/queue_panel.dartIssue: Crash on long-press buttons.
Root Cause: Tooltips require Overlay, but in Stack.
Fix: Remove tooltip properties.
File:
lib/screens/settings_screen.dartIssue: 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
GroupVolumeManageris a stateful service that handles server-side groups with null volume: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; }Computes effective volume:
groupMembersnullif no members have valid volumesCaches pending volumes:
Integrates with provider:
_applyVolumeProtection()in providerallPlayerslist for accurate member volumes_selectedPlayerviacopyWith()For in-app created groups with non-null leader volume, proportional propagation works as follows:
Detection:
setVolume()checks ifplayer.isGroupLeader && !isGroupPlayer(player)groupMemberswith 2+ entriesvolumeLevelCalculation: 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 }Execution:
_pendingVolumes[memberId]and_pendingVolumeTimestamps[memberId]_availablePlayers[idx]optimistically_api?.setVolume(memberId, memberNewVol)without awaitingvolume_setcommandResult: All member volumes scale proportionally with the leader, preserving relative balance.
Key: Listeners only see
isConnected = trueafter 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.