Files
Ensemble/lib/utils/player_sync_state.dart
ChrisandClaude Opus 4.6 c4b80b9d33 Extract player sync state utility
Third incremental extraction to reduce monolithic provider size:
- Created PlayerSyncState utility (135 lines) for sync state determination
- Extracted isPlayerManuallySynced method with complex business logic
- Added helper methods: findPlayer, findSyncTarget, isGroupPlayer, isManualSyncLeader
- Reduced main provider from 6158 to 6110 lines

PlayerSyncState provides:
- Clean separation of sync state logic from UI provider
- Testable business rules for manual vs pre-configured groups
- Cast-to-Sendspin ID translation support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 11:25:28 +00:00

136 lines
5.1 KiB
Dart

import '../models/player.dart';
/// Utility for determining player sync state.
///
/// Extracted from MusicAssistantProvider to isolate complex sync logic
/// and make it testable independently.
class PlayerSyncState {
final List<Player> availablePlayers;
final Map<String, String> castToSendspinIdMap;
PlayerSyncState({
required this.availablePlayers,
required this.castToSendspinIdMap,
});
/// Check if a player should show the "manually synced" indicator (yellow border).
///
/// Returns true for BOTH the leader AND children of a manually created sync group.
/// Excludes pre-configured MA speaker groups (provider = 'player_group').
bool isPlayerManuallySynced(String playerId) {
final player = availablePlayers.where((p) => p.playerId == playerId).firstOrNull;
if (player == null) return false;
// Group players (like "All Speakers") should NEVER have yellow border
// They are pre-configured containers, not manually synced players
// Check this FIRST before any other logic to prevent edge cases
if (player.provider == 'player_group') return false;
// Case 1: Player is a child synced to another player
if (player.syncedTo != null) {
// Look up sync target - also check translated IDs for Cast+Sendspin players
// The syncedTo might contain a Cast ID but the player list has the Sendspin version
Player? syncTarget = availablePlayers.where((p) => p.playerId == player.syncedTo).firstOrNull;
// If not found, try looking up by translated Sendspin ID
if (syncTarget == null) {
final translatedId = castToSendspinIdMap[player.syncedTo];
if (translatedId != null) {
syncTarget = availablePlayers.where((p) => p.playerId == translatedId).firstOrNull;
}
}
// Also check reverse: syncedTo might be Sendspin ID, look for Cast player
if (syncTarget == null) {
// Build reverse map on demand
for (final entry in castToSendspinIdMap.entries) {
if (entry.value == player.syncedTo) {
syncTarget = availablePlayers.where((p) => p.playerId == entry.key).firstOrNull;
if (syncTarget != null) break;
}
}
}
if (syncTarget == null) return false;
// If synced to a group player, it's part of a pre-configured group
if (syncTarget.provider == 'player_group') return false;
// Synced to a regular player - this is a manual sync child
return true;
}
// Case 2: Player is a leader with group members
if (player.groupMembers != null && player.groupMembers!.length > 1) {
// Key distinction: In a MANUAL sync, the leader's own ID is in groupMembers
// In a PRE-CONFIGURED group (UGP), the group player's ID is NOT in groupMembers
// (the members are the child players, not including the group itself)
final isInOwnGroup = player.groupMembers!.contains(player.playerId);
if (!isInOwnGroup) {
// This is a pre-configured group player (like "All Speakers")
return false;
}
// Leader's ID is in groupMembers = manual sync
return true;
}
return false;
}
/// Find a player by ID, optionally translating Cast IDs to Sendspin IDs.
Player? findPlayer(String playerId) {
// Try direct lookup first
var player = availablePlayers.where((p) => p.playerId == playerId).firstOrNull;
// If not found, try Cast-to-Sendspin translation
if (player == null && castToSendspinIdMap.containsKey(playerId)) {
final sendspinId = castToSendspinIdMap[playerId];
player = availablePlayers.where((p) => p.playerId == sendspinId).firstOrNull;
}
return player;
}
/// Find the sync target for a player, handling Cast/Sendspin ID translation.
Player? findSyncTarget(Player player) {
if (player.syncedTo == null) return null;
// Try direct lookup
var syncTarget = availablePlayers.where((p) => p.playerId == player.syncedTo).firstOrNull;
// If not found, try translated Sendspin ID
if (syncTarget == null) {
final translatedId = castToSendspinIdMap[player.syncedTo];
if (translatedId != null) {
syncTarget = availablePlayers.where((p) => p.playerId == translatedId).firstOrNull;
}
}
// Also check reverse: syncedTo might be Sendspin ID, look for Cast player
if (syncTarget == null) {
for (final entry in castToSendspinIdMap.entries) {
if (entry.value == player.syncedTo) {
syncTarget = availablePlayers.where((p) => p.playerId == entry.key).firstOrNull;
if (syncTarget != null) break;
}
}
}
return syncTarget;
}
/// Check if a player is a group player (pre-configured MA speaker group).
bool isGroupPlayer(Player player) {
return player.provider == 'player_group';
}
/// Check if a player is a leader in a manual sync group.
bool isManualSyncLeader(Player player) {
if (player.groupMembers == null || player.groupMembers!.length <= 1) {
return false;
}
// In a MANUAL sync, the leader's own ID is in groupMembers
return player.groupMembers!.contains(player.playerId);
}
}