Compare commits

..
4 Commits
Author SHA1 Message Date
Chris 6fc24539b1 On audit: Discarded multi-provider fixes - reverting to master 2026-02-07 20:36:50 +00:00
Chris 32f62a4731 index on audit: 9b8f6d3 Fix: Show all available home screen rows in settings 2026-02-07 20:36:50 +00:00
ChrisandClaude Opus 4.6 9b8f6d36c6 Fix: Show all available home screen rows in settings
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>
2026-02-07 17:15:29 +00:00
ChrisandClaude Opus 4.6 a3e414dc4b Fix: Add Overlay wrapper and checks to prevent red screen error
- 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>
2026-02-07 17:15:20 +00:00
31 changed files with 1025 additions and 784 deletions
+2 -6
View File
@@ -17,14 +17,10 @@
### Integration Status ### Integration Status
✅ **Phase 1 Complete**: All 5 providers created ✅ **Phase 1 Complete**: All 5 providers created
✅ **Phase 2 Complete**: Integration into main.dart completed ✅ **Phase 2 Started**: Integration into main.dart completed
- All providers added to MultiProvider tree - All providers added to MultiProvider tree
- Shared services (CacheService, PositionTracker, ImageHelperService) properly initialized - Shared services (CacheService, PositionTracker, ImageHelperService) properly initialized
- QueueManagerService circular dependency resolved - Build successful (52.2MB APK)
- Build successful (debug APK tested)
✅ **Critical Bug Fixed**: Red screen after login resolved (commit: ebf4e53)
- Fixed setState during build error in LibraryStatusMixin (using Future.microtask)
- Fixed context.select inside PageView.builder error (replaced with context.watch)
### Remaining Work ### Remaining Work
+14 -5
View File
@@ -236,9 +236,9 @@ class _MusicAssistantAppState extends State<MusicAssistantApp> with WidgetsBindi
// Set the queue manager after creation (breaks circular dependency) // Set the queue manager after creation (breaks circular dependency)
_playerProvider.queueManager = queueManager; _playerProvider.queueManager = queueManager;
// Initialize main provider (will act as facade) // Initialize main provider (facade mode - sub-providers not yet wired)
// For now, don't pass providers - MusicAssistantProvider doesn't accept them yet // Sub-providers have null API at construction time and no update mechanism,
// It will delegate to the new providers gradually // so keep facade using its own internal logic until wiring is complete.
_musicProvider = MusicAssistantProvider(); _musicProvider = MusicAssistantProvider();
// Wire up player provider callbacks // Wire up player provider callbacks
@@ -450,8 +450,17 @@ class _MusicAssistantAppState extends State<MusicAssistantApp> with WidgetsBindi
theme: AppTheme.lightTheme(colorScheme: lightColorScheme), theme: AppTheme.lightTheme(colorScheme: lightColorScheme),
darkTheme: AppTheme.darkTheme(colorScheme: darkColorScheme), darkTheme: AppTheme.darkTheme(colorScheme: darkColorScheme),
builder: (context, child) { builder: (context, child) {
// Wrap entire app with global player overlay // Wrap entire app with global player overlay.
return GlobalPlayerOverlay(child: child ?? const SizedBox.shrink()); // Provide an Overlay ancestor so BottomNavigationBar/Tooltip can render safely.
return Overlay(
initialEntries: [
OverlayEntry(
builder: (context) => GlobalPlayerOverlay(
child: child ?? const SizedBox.shrink(),
),
),
],
);
}, },
home: const AppStartup(), home: const AppStartup(),
), ),
+138 -55
View File
@@ -36,6 +36,9 @@ import '../utils/player_sort_utility.dart';
import '../utils/queue_controls.dart'; import '../utils/queue_controls.dart';
import '../utils/player_sync_state.dart'; import '../utils/player_sync_state.dart';
import '../main.dart' show audioHandler; import '../main.dart' show audioHandler;
import 'connection_provider.dart';
import 'library_provider.dart';
import 'player_provider.dart';
/// Main provider that coordinates connection, player, and library state. /// Main provider that coordinates connection, player, and library state.
/// ///
@@ -48,13 +51,19 @@ import '../main.dart' show audioHandler;
/// - Player logic: Player selection, controls, local player /// - Player logic: Player selection, controls, local player
/// - Library logic: Artists, albums, tracks, search /// - Library logic: Artists, albums, tracks, search
class MusicAssistantProvider with ChangeNotifier { class MusicAssistantProvider with ChangeNotifier {
// Sub-providers (injected for delegation)
final ConnectionProvider? _connectionProvider;
final LibraryProvider? _libraryProvider;
final PlayerProvider? _playerProvider;
MusicAssistantAPI? _api; MusicAssistantAPI? _api;
final AuthManager _authManager = AuthManager(); final AuthManager _authManager = AuthManager();
final DebugLogger _logger = DebugLogger(); final DebugLogger _logger = DebugLogger();
final CacheService _cacheService = CacheService(); final CacheService _cacheService = CacheService();
final PositionTracker _positionTracker = PositionTracker(); final PositionTracker _positionTracker = PositionTracker();
MAConnectionState _connectionState = MAConnectionState.disconnected; // Local state (used when _connectionProvider is null, for backward compatibility)
MAConnectionState? _connectionState;
String? _serverUrl; String? _serverUrl;
String? _error; String? _error;
@@ -183,13 +192,23 @@ class MusicAssistantProvider with ChangeNotifier {
// GETTERS // GETTERS
// ============================================================================ // ============================================================================
MAConnectionState get connectionState => _connectionState; // Connection getters - use facade's own state (sub-providers not yet wired)
String? get serverUrl => _serverUrl; MAConnectionState get connectionState => _connectionState ?? MAConnectionState.disconnected;
String? get error => _error;
bool get isConnected => _connectionState == MAConnectionState.connected ||
_connectionState == MAConnectionState.authenticated;
// Library getters with provider filtering applied String? get serverUrl => _serverUrl;
String? get error => _error;
bool get isConnected => _connectionState == MAConnectionState.connected;
MusicAssistantAPI? get api => _api;
List<String> get providerFilter => _providerFilter;
List<String> get playerFilter => _playerFilter;
// Library getters - delegate to LibraryProvider when available
// TEMP: Disable delegation to debug context.select error
List<Artist> get artists => filterByProvider(_artists); List<Artist> get artists => filterByProvider(_artists);
List<Album> get albums => filterByProvider(_albums); List<Album> get albums => filterByProvider(_albums);
List<Track> get tracks => filterByProvider(_tracks); List<Track> get tracks => filterByProvider(_tracks);
@@ -201,13 +220,12 @@ class MusicAssistantProvider with ChangeNotifier {
List<Album> get albumsUnfiltered => _albums; List<Album> get albumsUnfiltered => _albums;
List<MediaItem> get podcastsUnfiltered => _podcasts; List<MediaItem> get podcastsUnfiltered => _podcasts;
List<MediaItem> get radioStationsUnfiltered => _radioStations; List<MediaItem> get radioStationsUnfiltered => _radioStations;
// Library loading states - for now keep local state as LibraryProvider doesn't expose these
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
bool get isLoadingRadio => _isLoadingRadio; bool get isLoadingRadio => _isLoadingRadio;
bool get isLoadingPodcasts => _isLoadingPodcasts; bool get isLoadingPodcasts => _isLoadingPodcasts;
/// Provider filter from MA user settings (empty = all providers allowed)
List<String> get providerFilter => _providerFilter;
/// Whether provider filtering is active /// Whether provider filtering is active
bool get hasProviderFilter => _providerFilterService.hasProviderFilter; bool get hasProviderFilter => _providerFilterService.hasProviderFilter;
@@ -230,9 +248,6 @@ class MusicAssistantProvider with ChangeNotifier {
return _providerFilterService.filterSearchResults(results); return _providerFilterService.filterSearchResults(results);
} }
/// Player filter from MA user settings (empty = all players allowed)
List<String> get playerFilter => _playerFilter;
/// Whether player filtering is active /// Whether player filtering is active
bool get hasPlayerFilter => _providerFilterService.hasPlayerFilter; bool get hasPlayerFilter => _providerFilterService.hasPlayerFilter;
@@ -496,12 +511,7 @@ class MusicAssistantProvider with ChangeNotifier {
SyncStatus get syncStatus => SyncService.instance.status; SyncStatus get syncStatus => SyncService.instance.status;
/// Selected player - loads from cache if not yet set /// Selected player - loads from cache if not yet set
Player? get selectedPlayer { Player? get selectedPlayer => _selectedPlayer;
if (_selectedPlayer == null && _cacheService.getCachedSelectedPlayer() != null) {
_selectedPlayer = _cacheService.getCachedSelectedPlayer();
}
return _selectedPlayer;
}
/// Available players - loads from cache for instant UI display /// Available players - loads from cache for instant UI display
/// Filtered by player_filter if active /// Filtered by player_filter if active
@@ -525,6 +535,8 @@ class MusicAssistantProvider with ChangeNotifier {
} }
Track? get currentTrack => _currentTrack; Track? get currentTrack => _currentTrack;
Audiobook? get currentAudiobook => _currentAudiobook;
Map<String, String> get castToSendspinIdMap => _castToSendspinIdMap;
// Sleep timer getters // Sleep timer getters
bool get sleepTimerActive => _sleepTimerMinutes != null; bool get sleepTimerActive => _sleepTimerMinutes != null;
@@ -541,11 +553,8 @@ class MusicAssistantProvider with ChangeNotifier {
/// Home refresh counter - increments when home rows should refresh their data /// Home refresh counter - increments when home rows should refresh their data
int get homeRefreshCounter => _homeRefreshCounter; int get homeRefreshCounter => _homeRefreshCounter;
/// Currently playing audiobook context (with chapters) - set when playing an audiobook
Audiobook? get currentAudiobook => _currentAudiobook;
/// Whether we're currently playing an audiobook /// Whether we're currently playing an audiobook
bool get isPlayingAudiobook => _currentAudiobook != null; bool get isPlayingAudiobook => currentAudiobook != null;
/// Whether we're currently playing a podcast episode /// Whether we're currently playing a podcast episode
/// Detected by checking if the current track's URI contains podcast_episode /// Detected by checking if the current track's URI contains podcast_episode
@@ -621,7 +630,6 @@ class MusicAssistantProvider with ChangeNotifier {
String get lastSearchQuery => _lastSearchQuery; String get lastSearchQuery => _lastSearchQuery;
Map<String, List<MediaItem>> get lastSearchResults => _lastSearchResults; Map<String, List<MediaItem>> get lastSearchResults => _lastSearchResults;
MusicAssistantAPI? get api => _api;
AuthManager get authManager => _authManager; AuthManager get authManager => _authManager;
/// Position tracker for playback progress - single source of truth /// Position tracker for playback progress - single source of truth
@@ -757,12 +765,25 @@ class MusicAssistantProvider with ChangeNotifier {
// INITIALIZATION // INITIALIZATION
// ============================================================================ // ============================================================================
MusicAssistantProvider() { MusicAssistantProvider({
ConnectionProvider? connectionProvider,
LibraryProvider? libraryProvider,
PlayerProvider? playerProvider,
}) : _connectionProvider = connectionProvider,
_libraryProvider = libraryProvider,
_playerProvider = playerProvider {
_localPlayer = LocalPlayerService(_authManager); _localPlayer = LocalPlayerService(_authManager);
_initialize(); _initialize();
} }
Future<void> _initialize() async { Future<void> _initialize() async {
// Set up ConnectionProvider callbacks before connecting
if (_connectionProvider != null) {
_connectionProvider!.onConnected = _onConnectionProviderConnected;
_connectionProvider!.onAuthenticated = _onConnectionProviderAuthenticated;
_connectionProvider!.onDisconnected = _onConnectionProviderDisconnected;
}
_serverUrl = await SettingsService.getServerUrl(); _serverUrl = await SettingsService.getServerUrl();
// Load cached players from database for instant display (before connecting) // Load cached players from database for instant display (before connecting)
@@ -991,11 +1012,89 @@ class MusicAssistantProvider with ChangeNotifier {
} }
} }
// ============================================================================
// CONNECTION PROVIDER CALLBACKS
// ============================================================================
/// Called by ConnectionProvider when WebSocket connects (before auth)
Future<void> _onConnectionProviderConnected() async {
_logger.log('🔗 ConnectionProvider: WebSocket connected');
// No-op for now - initialization happens after authentication
}
/// Called by ConnectionProvider when authentication succeeds
Future<void> _onConnectionProviderAuthenticated() async {
_logger.log('✅ ConnectionProvider: Authenticated');
await _initializeAfterConnection();
}
/// Called by ConnectionProvider when disconnected
void _onConnectionProviderDisconnected() {
_logger.log('📡 ConnectionProvider: Disconnected - keeping cached data');
}
/// Set up event subscriptions that are specific to the facade
void _setupEventSubscriptions() {
if (_api == null) return;
// Subscribe to player events
_localPlayerEventSubscription?.cancel();
_localPlayerEventSubscription = _api!.builtinPlayerEvents.listen(
_handleLocalPlayerEvent,
onError: (error) => _logger.log('Builtin player event stream error: $error'),
);
_playerUpdatedEventSubscription?.cancel();
_playerUpdatedEventSubscription = _api!.playerUpdatedEvents.listen(
_handlePlayerUpdatedEvent,
onError: (error) => _logger.log('Player updated event stream error: $error'),
);
_playerAddedEventSubscription?.cancel();
_playerAddedEventSubscription = _api!.playerAddedEvents.listen(
_handlePlayerAddedEvent,
onError: (error) => _logger.log('Player added event stream error: $error'),
);
_mediaItemAddedEventSubscription?.cancel();
_mediaItemAddedEventSubscription = _api!.mediaItemAddedEvents.listen(
_handleMediaItemAddedEvent,
onError: (error) => _logger.log('Media item added event stream error: $error'),
);
_mediaItemDeletedEventSubscription?.cancel();
_mediaItemDeletedEventSubscription = _api!.mediaItemDeletedEvents.listen(
_handleMediaItemDeletedEvent,
onError: (error) => _logger.log('Media item deleted event stream error: $error'),
);
}
// ============================================================================ // ============================================================================
// CONNECTION // CONNECTION
// ============================================================================ // ============================================================================
Future<void> connectToServer(String serverUrl) async { Future<void> connectToServer(String serverUrl) async {
if (_connectionProvider != null) {
// Delegate to ConnectionProvider
await _connectionProvider!.connect(serverUrl);
// After ConnectionProvider connects, get the API reference and initialize facade-specific stuff
_api = _connectionProvider!.api;
_imageHelper = ImageHelperService(
api: _api,
getCachedTrackForPlayer: (playerId) => _cacheService.getCachedTrackForPlayer(playerId),
);
// Set up facade-specific event subscriptions
_setupEventSubscriptions();
} else {
// Fallback to old behavior when ConnectionProvider is not available
await _connectToServerLegacy(serverUrl);
}
}
/// Legacy connection method (used when ConnectionProvider is not provided)
Future<void> _connectToServerLegacy(String serverUrl) async {
try { try {
_error = null; _error = null;
_serverUrl = serverUrl; _serverUrl = serverUrl;
@@ -1054,36 +1153,8 @@ class MusicAssistantProvider with ChangeNotifier {
}, },
); );
_localPlayerEventSubscription?.cancel(); // Set up event subscriptions using shared method
_localPlayerEventSubscription = _api!.builtinPlayerEvents.listen( _setupEventSubscriptions();
_handleLocalPlayerEvent,
onError: (error) => _logger.log('Builtin player event stream error: $error'),
);
_playerUpdatedEventSubscription?.cancel();
_playerUpdatedEventSubscription = _api!.playerUpdatedEvents.listen(
_handlePlayerUpdatedEvent,
onError: (error) => _logger.log('Player updated event stream error: $error'),
);
_playerAddedEventSubscription?.cancel();
_playerAddedEventSubscription = _api!.playerAddedEvents.listen(
_handlePlayerAddedEvent,
onError: (error) => _logger.log('Player added event stream error: $error'),
);
// Subscribe to library change events for instant UI updates
_mediaItemAddedEventSubscription?.cancel();
_mediaItemAddedEventSubscription = _api!.mediaItemAddedEvents.listen(
_handleMediaItemAddedEvent,
onError: (error) => _logger.log('Media item added event stream error: $error'),
);
_mediaItemDeletedEventSubscription?.cancel();
_mediaItemDeletedEventSubscription = _api!.mediaItemDeletedEvents.listen(
_handleMediaItemDeletedEvent,
onError: (error) => _logger.log('Media item deleted event stream error: $error'),
);
await _api!.connect(); await _api!.connect();
notifyListeners(); notifyListeners();
@@ -4906,6 +4977,18 @@ class MusicAssistantProvider with ChangeNotifier {
// ============================================================================ // ============================================================================
Future<void> loadLibrary() async { Future<void> loadLibrary() async {
if (_libraryProvider != null) {
// Delegate to LibraryProvider
await _libraryProvider!.loadLibrary();
notifyListeners();
} else {
// Fallback to legacy behavior
await _loadLibraryLegacy();
}
}
/// Legacy library loading method (used when LibraryProvider is not provided)
Future<void> _loadLibraryLegacy() async {
if (!isConnected) return; if (!isConnected) return;
try { try {
+2 -2
View File
@@ -67,10 +67,10 @@ class _AlbumDetailsScreenState extends State<AlbumDetailsScreen> with SingleTick
final service = LibraryStatusService.instance; final service = LibraryStatusService.instance;
final key = libraryItemKey; final key = libraryItemKey;
if (!service.isInLibrary(key) && widget.album.inLibrary) { if (!service.isInLibrary(key) && widget.album.inLibrary) {
service.setLibraryStatus(key, true); service.initLibraryStatus(key, true);
} }
if (!service.isFavorite(key) && (widget.album.favorite ?? false)) { if (!service.isFavorite(key) && (widget.album.favorite ?? false)) {
service.setFavoriteStatus(key, true); service.initFavoriteStatus(key, true);
} }
_loadTracks(); _loadTracks();
_loadAlbumDescription(); _loadAlbumDescription();
+2 -2
View File
@@ -69,10 +69,10 @@ class _ArtistDetailsScreenState extends State<ArtistDetailsScreen> with LibraryS
final service = LibraryStatusService.instance; final service = LibraryStatusService.instance;
final key = libraryItemKey; final key = libraryItemKey;
if (!service.isInLibrary(key) && _checkIfInLibrary(widget.artist)) { if (!service.isInLibrary(key) && _checkIfInLibrary(widget.artist)) {
service.setLibraryStatus(key, true); service.initLibraryStatus(key, true);
} }
if (!service.isFavorite(key) && (widget.artist.favorite ?? false)) { if (!service.isFavorite(key) && (widget.artist.favorite ?? false)) {
service.setFavoriteStatus(key, true); service.initFavoriteStatus(key, true);
} }
// Use initial image URL immediately for smooth hero animation // Use initial image URL immediately for smooth hero animation
_artistImageUrl = widget.initialImageUrl; _artistImageUrl = widget.initialImageUrl;
+43 -10
View File
@@ -6,12 +6,52 @@ import '../widgets/album_card.dart';
import '../widgets/common/empty_state.dart'; import '../widgets/common/empty_state.dart';
import '../l10n/app_localizations.dart'; import '../l10n/app_localizations.dart';
class LibraryAlbumsScreen extends StatelessWidget { class LibraryAlbumsScreen extends StatefulWidget {
const LibraryAlbumsScreen({super.key}); const LibraryAlbumsScreen({super.key});
@override
State<LibraryAlbumsScreen> createState() => _LibraryAlbumsScreenState();
}
class _LibraryAlbumsScreenState extends State<LibraryAlbumsScreen> {
List<Album> _albums = [];
bool _isLoading = false;
@override
void initState() {
super.initState();
final provider = context.read<MusicAssistantProvider>();
_albums = provider.albums;
_isLoading = provider.isLoading;
provider.addListener(_onProviderChanged);
// Auto-load library if empty
if (_albums.isEmpty && !_isLoading) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
provider.loadLibrary();
}
});
}
}
@override
void dispose() {
context.read<MusicAssistantProvider>().removeListener(_onProviderChanged);
super.dispose();
}
void _onProviderChanged() {
if (!mounted) return;
final provider = context.read<MusicAssistantProvider>();
setState(() {
_albums = provider.albums;
_isLoading = provider.isLoading;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Use Selector for targeted rebuilds - only rebuild when albums or loading state changes
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme; final textTheme = Theme.of(context).textTheme;
@@ -34,13 +74,7 @@ class LibraryAlbumsScreen extends StatelessWidget {
), ),
centerTitle: true, centerTitle: true,
), ),
body: Selector<MusicAssistantProvider, (List<Album>, bool)>( body: _buildAlbumsList(context, _albums, _isLoading),
selector: (_, provider) => (provider.albums, provider.isLoading),
builder: (context, data, _) {
final (albums, isLoading) = data;
return _buildAlbumsList(context, albums, isLoading);
},
),
); );
} }
@@ -93,4 +127,3 @@ class LibraryAlbumsScreen extends StatelessWidget {
); );
} }
} }
+43 -9
View File
@@ -8,12 +8,52 @@ import '../widgets/artist_avatar.dart';
import 'artist_details_screen.dart'; import 'artist_details_screen.dart';
import '../l10n/app_localizations.dart'; import '../l10n/app_localizations.dart';
class LibraryArtistsScreen extends StatelessWidget { class LibraryArtistsScreen extends StatefulWidget {
const LibraryArtistsScreen({super.key}); const LibraryArtistsScreen({super.key});
@override
State<LibraryArtistsScreen> createState() => _LibraryArtistsScreenState();
}
class _LibraryArtistsScreenState extends State<LibraryArtistsScreen> {
List<Artist> _artists = [];
bool _isLoading = false;
@override
void initState() {
super.initState();
final provider = context.read<MusicAssistantProvider>();
_artists = provider.artists;
_isLoading = provider.isLoading;
provider.addListener(_onProviderChanged);
// Auto-load library if empty
if (_artists.isEmpty && !_isLoading) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
provider.loadLibrary();
}
});
}
}
@override
void dispose() {
context.read<MusicAssistantProvider>().removeListener(_onProviderChanged);
super.dispose();
}
void _onProviderChanged() {
if (!mounted) return;
final provider = context.read<MusicAssistantProvider>();
setState(() {
_artists = provider.artists;
_isLoading = provider.isLoading;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Use Selector for targeted rebuilds - only rebuild when artists or loading state changes
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme; final textTheme = Theme.of(context).textTheme;
@@ -36,13 +76,7 @@ class LibraryArtistsScreen extends StatelessWidget {
), ),
centerTitle: true, centerTitle: true,
), ),
body: Selector<MusicAssistantProvider, (List<Artist>, bool)>( body: _buildArtistsList(context, _artists, _isLoading),
selector: (_, provider) => (provider.artists, provider.isLoading),
builder: (context, data, _) {
final (artists, isLoading) = data;
return _buildArtistsList(context, artists, isLoading);
},
),
); );
} }
File diff suppressed because it is too large Load Diff
+39 -27
View File
@@ -598,35 +598,47 @@ class SearchScreenState extends State<SearchScreen> {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
// Library-only toggle with circle background // Library-only toggle with circle background
Tooltip( LayoutBuilder(
message: S.of(context)!.libraryOnly, builder: (context, constraints) {
child: SizedBox( final control = SizedBox(
width: 44, width: 44,
height: 44, height: 44,
child: Material( child: Material(
color: colorScheme.surfaceContainerHighest.withOpacity(0.6), color: colorScheme.surfaceContainerHighest.withOpacity(0.6),
shape: const CircleBorder(), shape: const CircleBorder(),
child: InkWell( child: InkWell(
onTap: () { onTap: () {
setState(() { setState(() {
_libraryOnly = !_libraryOnly; _libraryOnly = !_libraryOnly;
}); });
if (_searchController.text.isNotEmpty) { if (_searchController.text.isNotEmpty) {
_performSearch(_searchController.text); _performSearch(_searchController.text);
} }
}, },
customBorder: const CircleBorder(), customBorder: const CircleBorder(),
child: Icon( child: Icon(
Symbols.book_2, Symbols.book_2,
fill: _libraryOnly ? 1 : 0, fill: _libraryOnly ? 1 : 0,
size: 22, size: 22,
color: _libraryOnly color: _libraryOnly
? colorScheme.primary ? colorScheme.primary
: colorScheme.onSurface, : colorScheme.onSurface,
),
), ),
), ),
), );
),
// Only use Tooltip if an Overlay ancestor exists
final hasOverlay = Overlay.maybeOf(context) != null;
if (!hasOverlay) {
return control;
}
return Tooltip(
message: S.of(context)!.libraryOnly,
child: control,
);
},
), ),
], ],
), ),
+28 -4
View File
@@ -56,18 +56,42 @@ class _SettingsScreenState extends State<SettingsScreen> {
Map<String, bool> _discoveryRowEnabled = {}; Map<String, bool> _discoveryRowEnabled = {};
bool _isLoadingDiscoveryFolders = false; bool _isLoadingDiscoveryFolders = false;
/// Filtered row order for the draggable list (excludes discovery-mixes) /// All available static home screen rows (that can be toggled on/off)
/// Shows rows in home order, plus any discovery rows from provider that aren't in home order yet static const List<String> _allAvailableRows = [
'recent-albums',
'discover-artists',
'discover-albums',
'discovery-mixes',
'continue-listening',
'discover-audiobooks',
'discover-series',
'favorite-albums',
'favorite-artists',
'favorite-tracks',
'favorite-playlists',
'favorite-radio-stations',
'favorite-podcasts',
];
/// Filtered row order for the draggable list
/// Shows all available rows (in home order + discovery rows), regardless of enabled state
List<String> get _filteredRowOrder { List<String> get _filteredRowOrder {
final rows = <String>[]; final rows = <String>[];
// First, add all rows from home order (excluding discovery-mixes) // First, add all static rows in their current home order
for (final rowId in _homeRowOrder) { for (final rowId in _homeRowOrder) {
if (rowId == 'discovery-mixes') continue; if (rowId == 'discovery-mixes') continue;
rows.add(rowId); rows.add(rowId);
} }
// Then, add any discovery rows from provider that aren't in home order yet // Then, add any static rows that aren't in home order yet (so users can enable them)
for (final rowId in _allAvailableRows) {
if (!_homeRowOrder.contains(rowId) && rowId != 'discovery-mixes') {
rows.add(rowId);
}
}
// Finally, add any discovery rows from provider that aren't in home order yet
for (final folder in _discoveryFolders) { for (final folder in _discoveryFolders) {
final discoveryRowId = 'discovery:${folder.itemId}'; final discoveryRowId = 'discovery:${folder.itemId}';
if (!_homeRowOrder.contains(discoveryRowId)) { if (!_homeRowOrder.contains(discoveryRowId)) {
+13 -2
View File
@@ -62,6 +62,18 @@ class LibraryStatusService extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
/// Initialize library status without notifying listeners.
/// Use during initState to sync service from widget data without triggering rebuilds.
void initLibraryStatus(String key, bool inLibrary) {
_libraryStatus[key] ??= inLibrary;
}
/// Initialize favorite status without notifying listeners.
/// Use during initState to sync service from widget data without triggering rebuilds.
void initFavoriteStatus(String key, bool favorite) {
_favoriteStatus[key] ??= favorite;
}
/// Mark operation as pending (for rollback tracking) /// Mark operation as pending (for rollback tracking)
void markLibraryPending(String key, bool previousValue) { void markLibraryPending(String key, bool previousValue) {
_pendingLibraryOps[key] = previousValue; _pendingLibraryOps[key] = previousValue;
@@ -106,7 +118,6 @@ class LibraryStatusService extends ChangeNotifier {
required bool Function(dynamic item) getFavorite, required bool Function(dynamic item) getFavorite,
required String Function(dynamic item) getProvider, required String Function(dynamic item) getProvider,
required String Function(dynamic item) getItemId, required String Function(dynamic item) getItemId,
bool notify = true,
}) { }) {
bool changed = false; bool changed = false;
@@ -131,7 +142,7 @@ class LibraryStatusService extends ChangeNotifier {
} }
} }
if (changed && notify) { if (changed) {
notifyListeners(); notifyListeners();
} }
} }
+19 -15
View File
@@ -63,10 +63,10 @@ class _AlbumCardState extends State<AlbumCard> with LibraryStatusMixin {
final service = LibraryStatusService.instance; final service = LibraryStatusService.instance;
final key = libraryItemKey; final key = libraryItemKey;
if (!service.isInLibrary(key) && widget.album.inLibrary) { if (!service.isInLibrary(key) && widget.album.inLibrary) {
service.setLibraryStatus(key, true); service.initLibraryStatus(key, true);
} }
if (!service.isFavorite(key) && (widget.album.favorite ?? false)) { if (!service.isFavorite(key) && (widget.album.favorite ?? false)) {
service.setFavoriteStatus(key, true); service.initFavoriteStatus(key, true);
} }
_initFallbackImage(); _initFallbackImage();
} }
@@ -410,22 +410,26 @@ class _AlbumCardState extends State<AlbumCard> with LibraryStatusMixin {
const SizedBox(height: 8), const SizedBox(height: 8),
// Album title with year // Album title with year
// PERF: Removed Hero - text animations provide minimal benefit but add overhead // PERF: Removed Hero - text animations provide minimal benefit but add overhead
Text( Flexible(
widget.album.nameWithYear, child: Text(
maxLines: 1, widget.album.nameWithYear,
overflow: TextOverflow.ellipsis, maxLines: 1,
style: textTheme.titleSmall?.copyWith( overflow: TextOverflow.ellipsis,
color: colorScheme.onSurface, style: textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500, color: colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
), ),
), ),
// Artist name // Artist name
Text( Flexible(
widget.album.artistsString, child: Text(
maxLines: 1, widget.album.artistsString,
overflow: TextOverflow.ellipsis, maxLines: 1,
style: textTheme.bodySmall?.copyWith( overflow: TextOverflow.ellipsis,
color: colorScheme.onSurface.withOpacity(0.7), style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurface.withOpacity(0.7),
),
), ),
), ),
], ],
+2 -2
View File
@@ -113,8 +113,8 @@ class _AlbumRowState extends State<AlbumRow> with AutomaticKeepAliveClientMixin
} }
// Card layout: square artwork + text below // Card layout: square artwork + text below
// Text area: 8px gap + ~18px title + ~18px artist = ~44px // Text area: 8px gap + ~20px title + ~20px artist = ~48px
const textAreaHeight = 44.0; const textAreaHeight = 48.0;
final artworkSize = contentHeight - textAreaHeight; final artworkSize = contentHeight - textAreaHeight;
final cardWidth = artworkSize; // Card width = artwork width (square) final cardWidth = artworkSize; // Card width = artwork width (square)
final itemExtent = cardWidth + 12; // width + horizontal margins final itemExtent = cardWidth + 12; // width + horizontal margins
+2 -2
View File
@@ -63,10 +63,10 @@ class _ArtistCardState extends State<ArtistCard> with LibraryStatusMixin {
final service = LibraryStatusService.instance; final service = LibraryStatusService.instance;
final key = libraryItemKey; final key = libraryItemKey;
if (!service.isInLibrary(key) && widget.artist.inLibrary) { if (!service.isInLibrary(key) && widget.artist.inLibrary) {
service.setLibraryStatus(key, true); service.initLibraryStatus(key, true);
} }
if (!service.isFavorite(key) && (widget.artist.favorite ?? false)) { if (!service.isFavorite(key) && (widget.artist.favorite ?? false)) {
service.setFavoriteStatus(key, true); service.initFavoriteStatus(key, true);
} }
// Fetch fallback image once in initState, not during build // Fetch fallback image once in initState, not during build
_initFallbackImage(); _initFallbackImage();
+2 -2
View File
@@ -113,8 +113,8 @@ class _ArtistRowState extends State<ArtistRow> with AutomaticKeepAliveClientMixi
} }
// Card layout: circle image + name below // Card layout: circle image + name below
// Text area: 8px gap + ~36px for 2-line name = ~44px // Text area: 8px gap + ~40px for 2-line name = ~48px
const textAreaHeight = 44.0; const textAreaHeight = 48.0;
final imageSize = contentHeight - textAreaHeight; final imageSize = contentHeight - textAreaHeight;
final cardWidth = imageSize; // Card width = image width (circle) final cardWidth = imageSize; // Card width = image width (circle)
final itemExtent = cardWidth + 16; // width + horizontal margins final itemExtent = cardWidth + 16; // width + horizontal margins
+3 -3
View File
@@ -121,7 +121,7 @@ class _AudiobookRowState extends State<AudiobookRow> with AutomaticKeepAliveClie
); );
} }
const textAreaHeight = 44.0; const textAreaHeight = 48.0;
final artworkSize = contentHeight - textAreaHeight; final artworkSize = contentHeight - textAreaHeight;
final cardWidth = artworkSize; final cardWidth = artworkSize;
final itemExtent = cardWidth + 12; final itemExtent = cardWidth + 12;
@@ -223,10 +223,10 @@ class _AudiobookCardState extends State<_AudiobookCard> with LibraryStatusMixin
final service = LibraryStatusService.instance; final service = LibraryStatusService.instance;
final key = libraryItemKey; final key = libraryItemKey;
if (!service.isInLibrary(key) && widget.audiobook.inLibrary) { if (!service.isInLibrary(key) && widget.audiobook.inLibrary) {
service.setLibraryStatus(key, true); service.initLibraryStatus(key, true);
} }
if (!service.isFavorite(key) && (widget.audiobook.favorite ?? false)) { if (!service.isFavorite(key) && (widget.audiobook.favorite ?? false)) {
service.setFavoriteStatus(key, true); service.initFavoriteStatus(key, true);
} }
} }
+10 -8
View File
@@ -114,8 +114,8 @@ class _DiscoveryRowState extends State<DiscoveryRow> with AutomaticKeepAliveClie
} }
// Card layout: square artwork + text below // Card layout: square artwork + text below
// Text area: 8px gap + ~18px title + ~18px subtitle = ~44px // Text area: 8px gap + ~20px title + ~20px subtitle = ~48px
const textAreaHeight = 44.0; const textAreaHeight = 48.0;
final artworkSize = contentHeight - textAreaHeight; final artworkSize = contentHeight - textAreaHeight;
final cardWidth = artworkSize; // Card width = artwork width (square) final cardWidth = artworkSize; // Card width = artwork width (square)
final itemExtent = cardWidth + 12; // width + horizontal margins final itemExtent = cardWidth + 12; // width + horizontal margins
@@ -191,13 +191,15 @@ class _DiscoveryRowState extends State<DiscoveryRow> with AutomaticKeepAliveClie
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Flexible(
item.name, child: Text(
style: textTheme.bodySmall?.copyWith( item.name,
color: colorScheme.onSurface, style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurface,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
), ),
maxLines: 2,
overflow: TextOverflow.ellipsis,
), ),
], ],
), ),
+10 -21
View File
@@ -244,7 +244,11 @@ class ExpandablePlayerState extends State<ExpandablePlayer>
// Reset expansion notifier to match initial controller state (0.0) // Reset expansion notifier to match initial controller state (0.0)
// This fixes a bug where stale progress values from a previous widget instance // This fixes a bug where stale progress values from a previous widget instance
// caused the nav bar to be invisible on app restart/widget recreation // caused the nav bar to be invisible on app restart/widget recreation
playerExpansionNotifier.value = PlayerExpansionState(0.0, null, null); WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
playerExpansionNotifier.value = PlayerExpansionState(0.0, null, null);
}
});
_controller = AnimationController( _controller = AnimationController(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
@@ -417,7 +421,6 @@ class ExpandablePlayerState extends State<ExpandablePlayer>
_controller.removeListener(_recordAnimationFrame); _controller.removeListener(_recordAnimationFrame);
_lyricsScrollController.removeListener(_onLyricsScroll); _lyricsScrollController.removeListener(_onLyricsScroll);
_expansionNotifyTimer?.cancel();
_controller.dispose(); _controller.dispose();
_queuePanelController.dispose(); _queuePanelController.dispose();
_sleepTimerPanelController.dispose(); _sleepTimerPanelController.dispose();
@@ -616,8 +619,6 @@ class ExpandablePlayerState extends State<ExpandablePlayer>
// PERF Phase 4: Track last notified value to avoid unnecessary object creation // PERF Phase 4: Track last notified value to avoid unnecessary object creation
double _lastNotifiedProgress = -1; double _lastNotifiedProgress = -1;
Timer? _expansionNotifyTimer;
double? _pendingProgress;
void _notifyExpansionProgress() { void _notifyExpansionProgress() {
final progress = _controller.value; final progress = _controller.value;
@@ -626,23 +627,11 @@ class ExpandablePlayerState extends State<ExpandablePlayer>
if ((progress - _lastNotifiedProgress).abs() >= 0.01 || if ((progress - _lastNotifiedProgress).abs() >= 0.01 ||
progress == 0.0 || progress == 1.0) { progress == 0.0 || progress == 1.0) {
_lastNotifiedProgress = progress; _lastNotifiedProgress = progress;
_pendingProgress = progress; playerExpansionNotifier.value = PlayerExpansionState(
progress,
// Cancel any pending timer to avoid queue buildup _currentExpandedBgColor,
_expansionNotifyTimer?.cancel(); _currentExpandedPrimaryColor,
);
// Debounce with a short delay to ensure we're outside build phase
_expansionNotifyTimer = Timer(const Duration(milliseconds: 1), () {
if (mounted && _pendingProgress != null) {
playerExpansionNotifier.value = PlayerExpansionState(
_pendingProgress!,
_currentExpandedBgColor,
_currentExpandedPrimaryColor,
);
}
_expansionNotifyTimer = null;
_pendingProgress = null;
});
} }
} }
+6 -4
View File
@@ -478,10 +478,10 @@ class _GlobalPlayerOverlayState extends State<GlobalPlayerOverlay>
final theme = Theme.of(context); final theme = Theme.of(context);
final colorScheme = theme.colorScheme; final colorScheme = theme.colorScheme;
// PERF: Use select() to only rebuild when connection state or player changes return Consumer<MusicAssistantProvider>(
final (isConnected, hasSelectedPlayer) = context.select<MusicAssistantProvider, (bool, bool)>( builder: (context, maProvider, _) {
(p) => (p.isConnected, p.selectedPlayer != null), final isConnected = maProvider.isConnected;
); final hasSelectedPlayer = maProvider.selectedPlayer != null;
// Compute whether this is a first-time user ready for welcome screen. // Compute whether this is a first-time user ready for welcome screen.
// This is SYNCHRONOUS - no setState needed - so it works in the same frame. // This is SYNCHRONOUS - no setState needed - so it works in the same frame.
@@ -849,5 +849,7 @@ class _GlobalPlayerOverlayState extends State<GlobalPlayerOverlay>
], ],
), ),
); );
},
);
} }
} }
+43 -35
View File
@@ -69,46 +69,54 @@ class HiResBadge extends StatelessWidget {
final baseColor = primaryColor ?? colorScheme.primary; final baseColor = primaryColor ?? colorScheme.primary;
final color = Color.lerp(baseColor, Colors.white, 0.5)!; final color = Color.lerp(baseColor, Colors.white, 0.5)!;
return Tooltip( final content = Container(
message: tooltip ?? 'Hi-Res Audio', padding: const EdgeInsets.all(2),
child: Container( decoration: BoxDecoration(
padding: const EdgeInsets.all(2), border: Border.all(color: color, width: 1.5),
decoration: BoxDecoration( borderRadius: BorderRadius.circular(4),
border: Border.all(color: color, width: 1.5), ),
borderRadius: BorderRadius.circular(4), child: Column(
), mainAxisSize: MainAxisSize.min,
child: Column( children: [
mainAxisSize: MainAxisSize.min, Text(
children: [ 'Hi-Res',
Text( style: TextStyle(
'Hi-Res', color: color,
fontSize: 9,
fontWeight: FontWeight.w500,
height: 1.1,
),
),
Container(
margin: const EdgeInsets.only(top: 1),
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 1),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(2),
),
child: Text(
'AUDIO',
style: TextStyle( style: TextStyle(
color: color, color: colorScheme.surface,
fontSize: 9, fontSize: 8,
fontWeight: FontWeight.w500, fontWeight: FontWeight.bold,
height: 1.1, height: 1.1,
), ),
), ),
Container( ),
margin: const EdgeInsets.only(top: 1), ],
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 1),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(2),
),
child: Text(
'AUDIO',
style: TextStyle(
color: colorScheme.surface,
fontSize: 8,
fontWeight: FontWeight.bold,
height: 1.1,
),
),
),
],
),
), ),
); );
// Only use Tooltip if an Overlay ancestor exists
final hasOverlay = Overlay.maybeOf(context) != null;
if (!hasOverlay) {
return content;
}
return Tooltip(
message: tooltip ?? 'Hi-Res Audio',
child: content,
);
} }
} }
+3 -11
View File
@@ -109,8 +109,6 @@ mixin LibraryStatusMixin<T extends StatefulWidget> on State<T> {
/// Whether the item is currently a favorite /// Whether the item is currently a favorite
bool get isFavorite => LibraryStatusService.instance.isFavorite(libraryItemKey); bool get isFavorite => LibraryStatusService.instance.isFavorite(libraryItemKey);
bool _isStatusUpdatePending = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -124,16 +122,10 @@ mixin LibraryStatusMixin<T extends StatefulWidget> on State<T> {
} }
void _onLibraryStatusChanged() { void _onLibraryStatusChanged() {
// Use addPostFrameCallback to ensure setState is called AFTER the build phase if (!mounted) return;
// This prevents "setState during build" errors
if (_isStatusUpdatePending) return;
_isStatusUpdatePending = true;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) { if (!mounted) return;
setState(() {}); setState(() {});
}
_isStatusUpdatePending = false;
}); });
} }
+12 -3
View File
@@ -68,6 +68,9 @@ class PlayerRevealOverlayState extends State<PlayerRevealOverlay>
// Hint system // Hint system
bool _showHints = true; bool _showHints = true;
// Guard to run color preload only once from didChangeDependencies
bool _hasPreloaded = false;
// PERF: Pre-cached static BoxShadow to avoid allocation per frame // PERF: Pre-cached static BoxShadow to avoid allocation per frame
static const BoxShadow _cardShadow = BoxShadow( static const BoxShadow _cardShadow = BoxShadow(
color: Color(0x33000000), // 20% black color: Color(0x33000000), // 20% black
@@ -98,9 +101,6 @@ class PlayerRevealOverlayState extends State<PlayerRevealOverlay>
curve: const Interval(0.0, 0.4, curve: Curves.easeOut), curve: const Interval(0.0, 0.4, curve: Curves.easeOut),
); );
// Preload all player track info and extract colors
_preloadColorsForPlayers();
// Start reveal animation // Start reveal animation
_revealController.duration = const Duration(milliseconds: 200); _revealController.duration = const Duration(milliseconds: 200);
_revealController.forward(); _revealController.forward();
@@ -116,6 +116,15 @@ class PlayerRevealOverlayState extends State<PlayerRevealOverlay>
}); });
} }
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_hasPreloaded) {
_hasPreloaded = true;
_preloadColorsForPlayers();
}
}
Future<void> _loadHintSettings() async { Future<void> _loadHintSettings() async {
final value = await SettingsService.getShowHints(); final value = await SettingsService.getShowHints();
if (mounted) setState(() => _showHints = value); if (mounted) setState(() => _showHints = value);
+7 -4
View File
@@ -14,12 +14,13 @@ class PlayerSelector extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Use select to only rebuild when player data changes (not on every provider update)
final (selectedPlayer, availablePlayers) = context.select<MusicAssistantProvider, (dynamic, List)>(
(p) => (p.selectedPlayer, p.availablePlayers),
);
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
return Consumer<MusicAssistantProvider>(
builder: (context, maProvider, _) {
final selectedPlayer = maProvider.selectedPlayer;
final availablePlayers = maProvider.availablePlayers;
// Count players that are currently playing MA content (excluding selected player) // Count players that are currently playing MA content (excluding selected player)
// User already sees if selected player is playing, badge shows "other" activity // User already sees if selected player is playing, badge shows "other" activity
// Exclude external sources (Spotify Connect, TV optical, etc.) - they're not playing MA content // Exclude external sources (Spotify Connect, TV optical, etc.) - they're not playing MA content
@@ -80,6 +81,8 @@ class PlayerSelector extends StatelessWidget {
), ),
), ),
); );
},
);
} }
void _showPlayerSelector(BuildContext context) { void _showPlayerSelector(BuildContext context) {
+19 -15
View File
@@ -50,10 +50,10 @@ class _PlaylistCardState extends State<PlaylistCard> with LibraryStatusMixin {
final service = LibraryStatusService.instance; final service = LibraryStatusService.instance;
final key = libraryItemKey; final key = libraryItemKey;
if (!service.isInLibrary(key) && widget.playlist.inLibrary) { if (!service.isInLibrary(key) && widget.playlist.inLibrary) {
service.setLibraryStatus(key, true); service.initLibraryStatus(key, true);
} }
if (!service.isFavorite(key) && (widget.playlist.favorite ?? false)) { if (!service.isFavorite(key) && (widget.playlist.favorite ?? false)) {
service.setFavoriteStatus(key, true); service.initFavoriteStatus(key, true);
} }
} }
@@ -327,22 +327,26 @@ class _PlaylistCardState extends State<PlaylistCard> with LibraryStatusMixin {
const SizedBox(height: 8), const SizedBox(height: 8),
// Playlist title // Playlist title
// PERF: Removed Hero - text animations provide minimal benefit but add overhead // PERF: Removed Hero - text animations provide minimal benefit but add overhead
Text( Flexible(
widget.playlist.name, child: Text(
maxLines: 1, widget.playlist.name,
overflow: TextOverflow.ellipsis, maxLines: 1,
style: textTheme.titleSmall?.copyWith( overflow: TextOverflow.ellipsis,
color: colorScheme.onSurface, style: textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500, color: colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
), ),
), ),
// Owner/track count // Owner/track count
Text( Flexible(
widget.playlist.owner ?? (widget.playlist.trackCount != null ? '${widget.playlist.trackCount} tracks' : ''), child: Text(
maxLines: 1, widget.playlist.owner ?? (widget.playlist.trackCount != null ? '${widget.playlist.trackCount} tracks' : ''),
overflow: TextOverflow.ellipsis, maxLines: 1,
style: textTheme.bodySmall?.copyWith( overflow: TextOverflow.ellipsis,
color: colorScheme.onSurface.withOpacity(0.7), style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurface.withOpacity(0.7),
),
), ),
), ),
], ],
+2 -2
View File
@@ -106,8 +106,8 @@ class _PlaylistRowState extends State<PlaylistRow> with AutomaticKeepAliveClient
} }
// Card layout: square artwork + text below (same as AlbumRow) // Card layout: square artwork + text below (same as AlbumRow)
// Text area: 8px gap + ~18px title + ~18px owner = ~44px // Text area: 8px gap + ~20px title + ~20px owner = ~48px
const textAreaHeight = 44.0; const textAreaHeight = 48.0;
final artworkSize = contentHeight - textAreaHeight; final artworkSize = contentHeight - textAreaHeight;
final cardWidth = artworkSize; // Card width = artwork width (square) final cardWidth = artworkSize; // Card width = artwork width (square)
final itemExtent = cardWidth + 12; // width + horizontal margins final itemExtent = cardWidth + 12; // width + horizontal margins
+19 -15
View File
@@ -53,10 +53,10 @@ class _PodcastCardState extends State<PodcastCard> with LibraryStatusMixin {
final service = LibraryStatusService.instance; final service = LibraryStatusService.instance;
final key = libraryItemKey; final key = libraryItemKey;
if (!service.isInLibrary(key) && widget.podcast.inLibrary) { if (!service.isInLibrary(key) && widget.podcast.inLibrary) {
service.setLibraryStatus(key, true); service.initLibraryStatus(key, true);
} }
if (!service.isFavorite(key) && (widget.podcast.favorite ?? false)) { if (!service.isFavorite(key) && (widget.podcast.favorite ?? false)) {
service.setFavoriteStatus(key, true); service.initFavoriteStatus(key, true);
} }
} }
@@ -333,22 +333,26 @@ class _PodcastCardState extends State<PodcastCard> with LibraryStatusMixin {
const SizedBox(height: 8), const SizedBox(height: 8),
// Podcast title // Podcast title
// PERF: Removed Hero - text animations provide minimal benefit but add overhead // PERF: Removed Hero - text animations provide minimal benefit but add overhead
Text( Flexible(
widget.podcast.name, child: Text(
maxLines: 1, widget.podcast.name,
overflow: TextOverflow.ellipsis, maxLines: 1,
style: textTheme.titleSmall?.copyWith( overflow: TextOverflow.ellipsis,
color: colorScheme.onSurface, style: textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500, color: colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
), ),
), ),
// Podcast author (if available) // Podcast author (if available)
Text( Flexible(
_getAuthor(widget.podcast), child: Text(
maxLines: 1, _getAuthor(widget.podcast),
overflow: TextOverflow.ellipsis, maxLines: 1,
style: textTheme.bodySmall?.copyWith( overflow: TextOverflow.ellipsis,
color: colorScheme.onSurface.withOpacity(0.7), style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurface.withOpacity(0.7),
),
), ),
), ),
], ],
+2 -2
View File
@@ -114,8 +114,8 @@ class _PodcastRowState extends State<PodcastRow> with AutomaticKeepAliveClientMi
} }
// Card layout: square artwork + text below (same as AlbumRow) // Card layout: square artwork + text below (same as AlbumRow)
// Text area: 8px gap + ~18px title + ~18px author = ~44px // Text area: 8px gap + ~20px title + ~20px author = ~48px
const textAreaHeight = 44.0; const textAreaHeight = 48.0;
final artworkSize = contentHeight - textAreaHeight; final artworkSize = contentHeight - textAreaHeight;
final cardWidth = artworkSize; // Card width = artwork width (square) final cardWidth = artworkSize; // Card width = artwork width (square)
final itemExtent = cardWidth + 12; // width + horizontal margins final itemExtent = cardWidth + 12; // width + horizontal margins
+2 -2
View File
@@ -47,10 +47,10 @@ class _RadioStationCardState extends State<RadioStationCard> with LibraryStatusM
final service = LibraryStatusService.instance; final service = LibraryStatusService.instance;
final key = libraryItemKey; final key = libraryItemKey;
if (!service.isInLibrary(key) && widget.radioStation.inLibrary) { if (!service.isInLibrary(key) && widget.radioStation.inLibrary) {
service.setLibraryStatus(key, true); service.initLibraryStatus(key, true);
} }
if (!service.isFavorite(key) && (widget.radioStation.favorite ?? false)) { if (!service.isFavorite(key) && (widget.radioStation.favorite ?? false)) {
service.setFavoriteStatus(key, true); service.initFavoriteStatus(key, true);
} }
} }
+2 -2
View File
@@ -106,8 +106,8 @@ class _RadioStationRowState extends State<RadioStationRow> with AutomaticKeepAli
} }
// Card layout: circle image + name below (same as ArtistRow) // Card layout: circle image + name below (same as ArtistRow)
// Text area: 8px gap + ~36px for 2-line name = ~44px // Text area: 8px gap + ~40px for 2-line name = ~48px
const textAreaHeight = 44.0; const textAreaHeight = 48.0;
final imageSize = contentHeight - textAreaHeight; final imageSize = contentHeight - textAreaHeight;
final cardWidth = imageSize; // Card width = image width (circle) final cardWidth = imageSize; // Card width = image width (circle)
final itemExtent = cardWidth + 16; // width + horizontal margins final itemExtent = cardWidth + 16; // width + horizontal margins
+1 -1
View File
@@ -179,7 +179,7 @@ class _SeriesRowState extends State<SeriesRow> with AutomaticKeepAliveClientMixi
); );
} }
const textAreaHeight = 44.0; const textAreaHeight = 48.0;
final artworkSize = contentHeight - textAreaHeight; final artworkSize = contentHeight - textAreaHeight;
final cardWidth = artworkSize; final cardWidth = artworkSize;
final itemExtent = cardWidth + 12; final itemExtent = cardWidth + 12;
+21 -17
View File
@@ -108,8 +108,8 @@ class _TrackRowState extends State<TrackRow> with AutomaticKeepAliveClientMixin
} }
// Card layout: square artwork + text below // Card layout: square artwork + text below
// Text area: 8px gap + ~18px title + ~18px artist = ~44px // Text area: 8px gap + ~20px title + ~20px artist = ~48px
const textAreaHeight = 44.0; const textAreaHeight = 48.0;
final artworkSize = contentHeight - textAreaHeight; final artworkSize = contentHeight - textAreaHeight;
final cardWidth = artworkSize; // Card width = artwork width (square) final cardWidth = artworkSize; // Card width = artwork width (square)
final itemExtent = cardWidth + 12; final itemExtent = cardWidth + 12;
@@ -211,10 +211,10 @@ class _TrackCardState extends State<_TrackCard> with LibraryStatusMixin {
final service = LibraryStatusService.instance; final service = LibraryStatusService.instance;
final key = libraryItemKey; final key = libraryItemKey;
if (!service.isInLibrary(key) && widget.track.inLibrary) { if (!service.isInLibrary(key) && widget.track.inLibrary) {
service.setLibraryStatus(key, true); service.initLibraryStatus(key, true);
} }
if (!service.isFavorite(key) && (widget.track.favorite ?? false)) { if (!service.isFavorite(key) && (widget.track.favorite ?? false)) {
service.setFavoriteStatus(key, true); service.initFavoriteStatus(key, true);
} }
} }
@@ -453,22 +453,26 @@ class _TrackCardState extends State<_TrackCard> with LibraryStatusMixin {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// Track title // Track title
Text( Flexible(
widget.track.name, child: Text(
maxLines: 1, widget.track.name,
overflow: TextOverflow.ellipsis, maxLines: 1,
style: textTheme.titleSmall?.copyWith( overflow: TextOverflow.ellipsis,
color: colorScheme.onSurface, style: textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500, color: colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
), ),
), ),
// Artist name // Artist name
Text( Flexible(
widget.track.artistsString, child: Text(
maxLines: 1, widget.track.artistsString,
overflow: TextOverflow.ellipsis, maxLines: 1,
style: textTheme.bodySmall?.copyWith( overflow: TextOverflow.ellipsis,
color: colorScheme.onSurface.withOpacity(0.7), style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurface.withOpacity(0.7),
),
), ),
), ),
], ],