Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fc24539b1 | ||
|
|
32f62a4731 | ||
|
|
9b8f6d36c6 | ||
|
|
a3e414dc4b |
+14
-5
@@ -236,9 +236,9 @@ class _MusicAssistantAppState extends State<MusicAssistantApp> with WidgetsBindi
|
||||
// Set the queue manager after creation (breaks circular dependency)
|
||||
_playerProvider.queueManager = queueManager;
|
||||
|
||||
// Initialize main provider (will act as facade)
|
||||
// For now, don't pass providers - MusicAssistantProvider doesn't accept them yet
|
||||
// It will delegate to the new providers gradually
|
||||
// Initialize main provider (facade mode - sub-providers not yet wired)
|
||||
// Sub-providers have null API at construction time and no update mechanism,
|
||||
// so keep facade using its own internal logic until wiring is complete.
|
||||
_musicProvider = MusicAssistantProvider();
|
||||
|
||||
// Wire up player provider callbacks
|
||||
@@ -450,8 +450,17 @@ class _MusicAssistantAppState extends State<MusicAssistantApp> with WidgetsBindi
|
||||
theme: AppTheme.lightTheme(colorScheme: lightColorScheme),
|
||||
darkTheme: AppTheme.darkTheme(colorScheme: darkColorScheme),
|
||||
builder: (context, child) {
|
||||
// Wrap entire app with global player overlay
|
||||
return GlobalPlayerOverlay(child: child ?? const SizedBox.shrink());
|
||||
// Wrap entire app with global player overlay.
|
||||
// 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(),
|
||||
),
|
||||
|
||||
@@ -36,6 +36,9 @@ import '../utils/player_sort_utility.dart';
|
||||
import '../utils/queue_controls.dart';
|
||||
import '../utils/player_sync_state.dart';
|
||||
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.
|
||||
///
|
||||
@@ -48,13 +51,19 @@ import '../main.dart' show audioHandler;
|
||||
/// - Player logic: Player selection, controls, local player
|
||||
/// - Library logic: Artists, albums, tracks, search
|
||||
class MusicAssistantProvider with ChangeNotifier {
|
||||
// Sub-providers (injected for delegation)
|
||||
final ConnectionProvider? _connectionProvider;
|
||||
final LibraryProvider? _libraryProvider;
|
||||
final PlayerProvider? _playerProvider;
|
||||
|
||||
MusicAssistantAPI? _api;
|
||||
final AuthManager _authManager = AuthManager();
|
||||
final DebugLogger _logger = DebugLogger();
|
||||
final CacheService _cacheService = CacheService();
|
||||
final PositionTracker _positionTracker = PositionTracker();
|
||||
|
||||
MAConnectionState _connectionState = MAConnectionState.disconnected;
|
||||
// Local state (used when _connectionProvider is null, for backward compatibility)
|
||||
MAConnectionState? _connectionState;
|
||||
String? _serverUrl;
|
||||
String? _error;
|
||||
|
||||
@@ -183,13 +192,23 @@ class MusicAssistantProvider with ChangeNotifier {
|
||||
// GETTERS
|
||||
// ============================================================================
|
||||
|
||||
MAConnectionState get connectionState => _connectionState;
|
||||
String? get serverUrl => _serverUrl;
|
||||
String? get error => _error;
|
||||
bool get isConnected => _connectionState == MAConnectionState.connected ||
|
||||
_connectionState == MAConnectionState.authenticated;
|
||||
// Connection getters - use facade's own state (sub-providers not yet wired)
|
||||
MAConnectionState get connectionState => _connectionState ?? MAConnectionState.disconnected;
|
||||
|
||||
// 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<Album> get albums => filterByProvider(_albums);
|
||||
List<Track> get tracks => filterByProvider(_tracks);
|
||||
@@ -201,13 +220,12 @@ class MusicAssistantProvider with ChangeNotifier {
|
||||
List<Album> get albumsUnfiltered => _albums;
|
||||
List<MediaItem> get podcastsUnfiltered => _podcasts;
|
||||
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 isLoadingRadio => _isLoadingRadio;
|
||||
bool get isLoadingPodcasts => _isLoadingPodcasts;
|
||||
|
||||
/// Provider filter from MA user settings (empty = all providers allowed)
|
||||
List<String> get providerFilter => _providerFilter;
|
||||
|
||||
/// Whether provider filtering is active
|
||||
bool get hasProviderFilter => _providerFilterService.hasProviderFilter;
|
||||
|
||||
@@ -230,9 +248,6 @@ class MusicAssistantProvider with ChangeNotifier {
|
||||
return _providerFilterService.filterSearchResults(results);
|
||||
}
|
||||
|
||||
/// Player filter from MA user settings (empty = all players allowed)
|
||||
List<String> get playerFilter => _playerFilter;
|
||||
|
||||
/// Whether player filtering is active
|
||||
bool get hasPlayerFilter => _providerFilterService.hasPlayerFilter;
|
||||
|
||||
@@ -496,12 +511,7 @@ class MusicAssistantProvider with ChangeNotifier {
|
||||
SyncStatus get syncStatus => SyncService.instance.status;
|
||||
|
||||
/// Selected player - loads from cache if not yet set
|
||||
Player? get selectedPlayer {
|
||||
if (_selectedPlayer == null && _cacheService.getCachedSelectedPlayer() != null) {
|
||||
_selectedPlayer = _cacheService.getCachedSelectedPlayer();
|
||||
}
|
||||
return _selectedPlayer;
|
||||
}
|
||||
Player? get selectedPlayer => _selectedPlayer;
|
||||
|
||||
/// Available players - loads from cache for instant UI display
|
||||
/// Filtered by player_filter if active
|
||||
@@ -525,6 +535,8 @@ class MusicAssistantProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
Track? get currentTrack => _currentTrack;
|
||||
Audiobook? get currentAudiobook => _currentAudiobook;
|
||||
Map<String, String> get castToSendspinIdMap => _castToSendspinIdMap;
|
||||
|
||||
// Sleep timer getters
|
||||
bool get sleepTimerActive => _sleepTimerMinutes != null;
|
||||
@@ -541,11 +553,8 @@ class MusicAssistantProvider with ChangeNotifier {
|
||||
/// Home refresh counter - increments when home rows should refresh their data
|
||||
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
|
||||
bool get isPlayingAudiobook => _currentAudiobook != null;
|
||||
bool get isPlayingAudiobook => currentAudiobook != null;
|
||||
|
||||
/// Whether we're currently playing a 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;
|
||||
Map<String, List<MediaItem>> get lastSearchResults => _lastSearchResults;
|
||||
|
||||
MusicAssistantAPI? get api => _api;
|
||||
AuthManager get authManager => _authManager;
|
||||
|
||||
/// Position tracker for playback progress - single source of truth
|
||||
@@ -757,12 +765,25 @@ class MusicAssistantProvider with ChangeNotifier {
|
||||
// INITIALIZATION
|
||||
// ============================================================================
|
||||
|
||||
MusicAssistantProvider() {
|
||||
MusicAssistantProvider({
|
||||
ConnectionProvider? connectionProvider,
|
||||
LibraryProvider? libraryProvider,
|
||||
PlayerProvider? playerProvider,
|
||||
}) : _connectionProvider = connectionProvider,
|
||||
_libraryProvider = libraryProvider,
|
||||
_playerProvider = playerProvider {
|
||||
_localPlayer = LocalPlayerService(_authManager);
|
||||
_initialize();
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// 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
|
||||
// ============================================================================
|
||||
|
||||
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 {
|
||||
_error = null;
|
||||
_serverUrl = serverUrl;
|
||||
@@ -1054,36 +1153,8 @@ class MusicAssistantProvider with ChangeNotifier {
|
||||
},
|
||||
);
|
||||
|
||||
_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'),
|
||||
);
|
||||
|
||||
// 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'),
|
||||
);
|
||||
// Set up event subscriptions using shared method
|
||||
_setupEventSubscriptions();
|
||||
|
||||
await _api!.connect();
|
||||
notifyListeners();
|
||||
@@ -4906,6 +4977,18 @@ class MusicAssistantProvider with ChangeNotifier {
|
||||
// ============================================================================
|
||||
|
||||
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;
|
||||
|
||||
try {
|
||||
|
||||
@@ -67,10 +67,10 @@ class _AlbumDetailsScreenState extends State<AlbumDetailsScreen> with SingleTick
|
||||
final service = LibraryStatusService.instance;
|
||||
final key = libraryItemKey;
|
||||
if (!service.isInLibrary(key) && widget.album.inLibrary) {
|
||||
service.setLibraryStatus(key, true);
|
||||
service.initLibraryStatus(key, true);
|
||||
}
|
||||
if (!service.isFavorite(key) && (widget.album.favorite ?? false)) {
|
||||
service.setFavoriteStatus(key, true);
|
||||
service.initFavoriteStatus(key, true);
|
||||
}
|
||||
_loadTracks();
|
||||
_loadAlbumDescription();
|
||||
|
||||
@@ -69,10 +69,10 @@ class _ArtistDetailsScreenState extends State<ArtistDetailsScreen> with LibraryS
|
||||
final service = LibraryStatusService.instance;
|
||||
final key = libraryItemKey;
|
||||
if (!service.isInLibrary(key) && _checkIfInLibrary(widget.artist)) {
|
||||
service.setLibraryStatus(key, true);
|
||||
service.initLibraryStatus(key, true);
|
||||
}
|
||||
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
|
||||
_artistImageUrl = widget.initialImageUrl;
|
||||
|
||||
@@ -6,12 +6,52 @@ import '../widgets/album_card.dart';
|
||||
import '../widgets/common/empty_state.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class LibraryAlbumsScreen extends StatelessWidget {
|
||||
class LibraryAlbumsScreen extends StatefulWidget {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
// Use Selector for targeted rebuilds - only rebuild when albums or loading state changes
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
@@ -34,13 +74,7 @@ class LibraryAlbumsScreen extends StatelessWidget {
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Selector<MusicAssistantProvider, (List<Album>, bool)>(
|
||||
selector: (_, provider) => (provider.albums, provider.isLoading),
|
||||
builder: (context, data, _) {
|
||||
final (albums, isLoading) = data;
|
||||
return _buildAlbumsList(context, albums, isLoading);
|
||||
},
|
||||
),
|
||||
body: _buildAlbumsList(context, _albums, _isLoading),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,4 +127,3 @@ class LibraryAlbumsScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,52 @@ import '../widgets/artist_avatar.dart';
|
||||
import 'artist_details_screen.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class LibraryArtistsScreen extends StatelessWidget {
|
||||
class LibraryArtistsScreen extends StatefulWidget {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
// Use Selector for targeted rebuilds - only rebuild when artists or loading state changes
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
@@ -36,13 +76,7 @@ class LibraryArtistsScreen extends StatelessWidget {
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Selector<MusicAssistantProvider, (List<Artist>, bool)>(
|
||||
selector: (_, provider) => (provider.artists, provider.isLoading),
|
||||
builder: (context, data, _) {
|
||||
final (artists, isLoading) = data;
|
||||
return _buildArtistsList(context, artists, isLoading);
|
||||
},
|
||||
),
|
||||
body: _buildArtistsList(context, _artists, _isLoading),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+514
-498
File diff suppressed because it is too large
Load Diff
@@ -598,35 +598,47 @@ class SearchScreenState extends State<SearchScreen> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Library-only toggle with circle background
|
||||
Tooltip(
|
||||
message: S.of(context)!.libraryOnly,
|
||||
child: SizedBox(
|
||||
width: 44,
|
||||
height: 44,
|
||||
child: Material(
|
||||
color: colorScheme.surfaceContainerHighest.withOpacity(0.6),
|
||||
shape: const CircleBorder(),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_libraryOnly = !_libraryOnly;
|
||||
});
|
||||
if (_searchController.text.isNotEmpty) {
|
||||
_performSearch(_searchController.text);
|
||||
}
|
||||
},
|
||||
customBorder: const CircleBorder(),
|
||||
child: Icon(
|
||||
Symbols.book_2,
|
||||
fill: _libraryOnly ? 1 : 0,
|
||||
size: 22,
|
||||
color: _libraryOnly
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurface,
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final control = SizedBox(
|
||||
width: 44,
|
||||
height: 44,
|
||||
child: Material(
|
||||
color: colorScheme.surfaceContainerHighest.withOpacity(0.6),
|
||||
shape: const CircleBorder(),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_libraryOnly = !_libraryOnly;
|
||||
});
|
||||
if (_searchController.text.isNotEmpty) {
|
||||
_performSearch(_searchController.text);
|
||||
}
|
||||
},
|
||||
customBorder: const CircleBorder(),
|
||||
child: Icon(
|
||||
Symbols.book_2,
|
||||
fill: _libraryOnly ? 1 : 0,
|
||||
size: 22,
|
||||
color: _libraryOnly
|
||||
? colorScheme.primary
|
||||
: 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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -56,18 +56,42 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Map<String, bool> _discoveryRowEnabled = {};
|
||||
bool _isLoadingDiscoveryFolders = false;
|
||||
|
||||
/// Filtered row order for the draggable list (excludes discovery-mixes)
|
||||
/// Shows rows in home order, plus any discovery rows from provider that aren't in home order yet
|
||||
/// All available static home screen rows (that can be toggled on/off)
|
||||
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 {
|
||||
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) {
|
||||
if (rowId == 'discovery-mixes') continue;
|
||||
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) {
|
||||
final discoveryRowId = 'discovery:${folder.itemId}';
|
||||
if (!_homeRowOrder.contains(discoveryRowId)) {
|
||||
|
||||
@@ -62,6 +62,18 @@ class LibraryStatusService extends ChangeNotifier {
|
||||
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)
|
||||
void markLibraryPending(String key, bool previousValue) {
|
||||
_pendingLibraryOps[key] = previousValue;
|
||||
|
||||
+19
-15
@@ -63,10 +63,10 @@ class _AlbumCardState extends State<AlbumCard> with LibraryStatusMixin {
|
||||
final service = LibraryStatusService.instance;
|
||||
final key = libraryItemKey;
|
||||
if (!service.isInLibrary(key) && widget.album.inLibrary) {
|
||||
service.setLibraryStatus(key, true);
|
||||
service.initLibraryStatus(key, true);
|
||||
}
|
||||
if (!service.isFavorite(key) && (widget.album.favorite ?? false)) {
|
||||
service.setFavoriteStatus(key, true);
|
||||
service.initFavoriteStatus(key, true);
|
||||
}
|
||||
_initFallbackImage();
|
||||
}
|
||||
@@ -410,22 +410,26 @@ class _AlbumCardState extends State<AlbumCard> with LibraryStatusMixin {
|
||||
const SizedBox(height: 8),
|
||||
// Album title with year
|
||||
// PERF: Removed Hero - text animations provide minimal benefit but add overhead
|
||||
Text(
|
||||
widget.album.nameWithYear,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.titleSmall?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.album.nameWithYear,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.titleSmall?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Artist name
|
||||
Text(
|
||||
widget.album.artistsString,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.album.artistsString,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -113,8 +113,8 @@ class _AlbumRowState extends State<AlbumRow> with AutomaticKeepAliveClientMixin
|
||||
}
|
||||
|
||||
// Card layout: square artwork + text below
|
||||
// Text area: 8px gap + ~18px title + ~18px artist = ~44px
|
||||
const textAreaHeight = 44.0;
|
||||
// Text area: 8px gap + ~20px title + ~20px artist = ~48px
|
||||
const textAreaHeight = 48.0;
|
||||
final artworkSize = contentHeight - textAreaHeight;
|
||||
final cardWidth = artworkSize; // Card width = artwork width (square)
|
||||
final itemExtent = cardWidth + 12; // width + horizontal margins
|
||||
|
||||
@@ -63,10 +63,10 @@ class _ArtistCardState extends State<ArtistCard> with LibraryStatusMixin {
|
||||
final service = LibraryStatusService.instance;
|
||||
final key = libraryItemKey;
|
||||
if (!service.isInLibrary(key) && widget.artist.inLibrary) {
|
||||
service.setLibraryStatus(key, true);
|
||||
service.initLibraryStatus(key, true);
|
||||
}
|
||||
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
|
||||
_initFallbackImage();
|
||||
|
||||
@@ -113,8 +113,8 @@ class _ArtistRowState extends State<ArtistRow> with AutomaticKeepAliveClientMixi
|
||||
}
|
||||
|
||||
// Card layout: circle image + name below
|
||||
// Text area: 8px gap + ~36px for 2-line name = ~44px
|
||||
const textAreaHeight = 44.0;
|
||||
// Text area: 8px gap + ~40px for 2-line name = ~48px
|
||||
const textAreaHeight = 48.0;
|
||||
final imageSize = contentHeight - textAreaHeight;
|
||||
final cardWidth = imageSize; // Card width = image width (circle)
|
||||
final itemExtent = cardWidth + 16; // width + horizontal margins
|
||||
|
||||
@@ -121,7 +121,7 @@ class _AudiobookRowState extends State<AudiobookRow> with AutomaticKeepAliveClie
|
||||
);
|
||||
}
|
||||
|
||||
const textAreaHeight = 44.0;
|
||||
const textAreaHeight = 48.0;
|
||||
final artworkSize = contentHeight - textAreaHeight;
|
||||
final cardWidth = artworkSize;
|
||||
final itemExtent = cardWidth + 12;
|
||||
@@ -223,10 +223,10 @@ class _AudiobookCardState extends State<_AudiobookCard> with LibraryStatusMixin
|
||||
final service = LibraryStatusService.instance;
|
||||
final key = libraryItemKey;
|
||||
if (!service.isInLibrary(key) && widget.audiobook.inLibrary) {
|
||||
service.setLibraryStatus(key, true);
|
||||
service.initLibraryStatus(key, true);
|
||||
}
|
||||
if (!service.isFavorite(key) && (widget.audiobook.favorite ?? false)) {
|
||||
service.setFavoriteStatus(key, true);
|
||||
service.initFavoriteStatus(key, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,8 +114,8 @@ class _DiscoveryRowState extends State<DiscoveryRow> with AutomaticKeepAliveClie
|
||||
}
|
||||
|
||||
// Card layout: square artwork + text below
|
||||
// Text area: 8px gap + ~18px title + ~18px subtitle = ~44px
|
||||
const textAreaHeight = 44.0;
|
||||
// Text area: 8px gap + ~20px title + ~20px subtitle = ~48px
|
||||
const textAreaHeight = 48.0;
|
||||
final artworkSize = contentHeight - textAreaHeight;
|
||||
final cardWidth = artworkSize; // Card width = artwork width (square)
|
||||
final itemExtent = cardWidth + 12; // width + horizontal margins
|
||||
@@ -191,13 +191,15 @@ class _DiscoveryRowState extends State<DiscoveryRow> with AutomaticKeepAliveClie
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
item.name,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
Flexible(
|
||||
child: Text(
|
||||
item.name,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -244,7 +244,11 @@ class ExpandablePlayerState extends State<ExpandablePlayer>
|
||||
// Reset expansion notifier to match initial controller state (0.0)
|
||||
// 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
|
||||
playerExpansionNotifier.value = PlayerExpansionState(0.0, null, null);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
playerExpansionNotifier.value = PlayerExpansionState(0.0, null, null);
|
||||
}
|
||||
});
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
|
||||
@@ -478,10 +478,10 @@ class _GlobalPlayerOverlayState extends State<GlobalPlayerOverlay>
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
// PERF: Use select() to only rebuild when connection state or player changes
|
||||
final (isConnected, hasSelectedPlayer) = context.select<MusicAssistantProvider, (bool, bool)>(
|
||||
(p) => (p.isConnected, p.selectedPlayer != null),
|
||||
);
|
||||
return Consumer<MusicAssistantProvider>(
|
||||
builder: (context, maProvider, _) {
|
||||
final isConnected = maProvider.isConnected;
|
||||
final hasSelectedPlayer = maProvider.selectedPlayer != null;
|
||||
|
||||
// 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.
|
||||
@@ -849,5 +849,7 @@ class _GlobalPlayerOverlayState extends State<GlobalPlayerOverlay>
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,46 +69,54 @@ class HiResBadge extends StatelessWidget {
|
||||
final baseColor = primaryColor ?? colorScheme.primary;
|
||||
final color = Color.lerp(baseColor, Colors.white, 0.5)!;
|
||||
|
||||
return Tooltip(
|
||||
message: tooltip ?? 'Hi-Res Audio',
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: color, width: 1.5),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Hi-Res',
|
||||
final content = Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: color, width: 1.5),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Hi-Res',
|
||||
style: TextStyle(
|
||||
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(
|
||||
color: color,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colorScheme.surface,
|
||||
fontSize: 8,
|
||||
fontWeight: FontWeight.bold,
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,10 +122,11 @@ mixin LibraryStatusMixin<T extends StatefulWidget> on State<T> {
|
||||
}
|
||||
|
||||
void _onLibraryStatusChanged() {
|
||||
// Trigger rebuild to pick up new status values
|
||||
if (mounted) {
|
||||
if (!mounted) return;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Set library status optimistically and return previous value for rollback
|
||||
|
||||
@@ -68,6 +68,9 @@ class PlayerRevealOverlayState extends State<PlayerRevealOverlay>
|
||||
// Hint system
|
||||
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
|
||||
static const BoxShadow _cardShadow = BoxShadow(
|
||||
color: Color(0x33000000), // 20% black
|
||||
@@ -98,9 +101,6 @@ class PlayerRevealOverlayState extends State<PlayerRevealOverlay>
|
||||
curve: const Interval(0.0, 0.4, curve: Curves.easeOut),
|
||||
);
|
||||
|
||||
// Preload all player track info and extract colors
|
||||
_preloadColorsForPlayers();
|
||||
|
||||
// Start reveal animation
|
||||
_revealController.duration = const Duration(milliseconds: 200);
|
||||
_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 {
|
||||
final value = await SettingsService.getShowHints();
|
||||
if (mounted) setState(() => _showHints = value);
|
||||
|
||||
@@ -14,12 +14,13 @@ class PlayerSelector extends StatelessWidget {
|
||||
|
||||
@override
|
||||
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;
|
||||
|
||||
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)
|
||||
// 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
|
||||
@@ -80,6 +81,8 @@ class PlayerSelector extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showPlayerSelector(BuildContext context) {
|
||||
|
||||
@@ -50,10 +50,10 @@ class _PlaylistCardState extends State<PlaylistCard> with LibraryStatusMixin {
|
||||
final service = LibraryStatusService.instance;
|
||||
final key = libraryItemKey;
|
||||
if (!service.isInLibrary(key) && widget.playlist.inLibrary) {
|
||||
service.setLibraryStatus(key, true);
|
||||
service.initLibraryStatus(key, true);
|
||||
}
|
||||
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),
|
||||
// Playlist title
|
||||
// PERF: Removed Hero - text animations provide minimal benefit but add overhead
|
||||
Text(
|
||||
widget.playlist.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.titleSmall?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.playlist.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.titleSmall?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Owner/track count
|
||||
Text(
|
||||
widget.playlist.owner ?? (widget.playlist.trackCount != null ? '${widget.playlist.trackCount} tracks' : ''),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.playlist.owner ?? (widget.playlist.trackCount != null ? '${widget.playlist.trackCount} tracks' : ''),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -106,8 +106,8 @@ class _PlaylistRowState extends State<PlaylistRow> with AutomaticKeepAliveClient
|
||||
}
|
||||
|
||||
// Card layout: square artwork + text below (same as AlbumRow)
|
||||
// Text area: 8px gap + ~18px title + ~18px owner = ~44px
|
||||
const textAreaHeight = 44.0;
|
||||
// Text area: 8px gap + ~20px title + ~20px owner = ~48px
|
||||
const textAreaHeight = 48.0;
|
||||
final artworkSize = contentHeight - textAreaHeight;
|
||||
final cardWidth = artworkSize; // Card width = artwork width (square)
|
||||
final itemExtent = cardWidth + 12; // width + horizontal margins
|
||||
|
||||
@@ -53,10 +53,10 @@ class _PodcastCardState extends State<PodcastCard> with LibraryStatusMixin {
|
||||
final service = LibraryStatusService.instance;
|
||||
final key = libraryItemKey;
|
||||
if (!service.isInLibrary(key) && widget.podcast.inLibrary) {
|
||||
service.setLibraryStatus(key, true);
|
||||
service.initLibraryStatus(key, true);
|
||||
}
|
||||
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),
|
||||
// Podcast title
|
||||
// PERF: Removed Hero - text animations provide minimal benefit but add overhead
|
||||
Text(
|
||||
widget.podcast.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.titleSmall?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.podcast.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.titleSmall?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Podcast author (if available)
|
||||
Text(
|
||||
_getAuthor(widget.podcast),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
Flexible(
|
||||
child: Text(
|
||||
_getAuthor(widget.podcast),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -114,8 +114,8 @@ class _PodcastRowState extends State<PodcastRow> with AutomaticKeepAliveClientMi
|
||||
}
|
||||
|
||||
// Card layout: square artwork + text below (same as AlbumRow)
|
||||
// Text area: 8px gap + ~18px title + ~18px author = ~44px
|
||||
const textAreaHeight = 44.0;
|
||||
// Text area: 8px gap + ~20px title + ~20px author = ~48px
|
||||
const textAreaHeight = 48.0;
|
||||
final artworkSize = contentHeight - textAreaHeight;
|
||||
final cardWidth = artworkSize; // Card width = artwork width (square)
|
||||
final itemExtent = cardWidth + 12; // width + horizontal margins
|
||||
|
||||
@@ -47,10 +47,10 @@ class _RadioStationCardState extends State<RadioStationCard> with LibraryStatusM
|
||||
final service = LibraryStatusService.instance;
|
||||
final key = libraryItemKey;
|
||||
if (!service.isInLibrary(key) && widget.radioStation.inLibrary) {
|
||||
service.setLibraryStatus(key, true);
|
||||
service.initLibraryStatus(key, true);
|
||||
}
|
||||
if (!service.isFavorite(key) && (widget.radioStation.favorite ?? false)) {
|
||||
service.setFavoriteStatus(key, true);
|
||||
service.initFavoriteStatus(key, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -106,8 +106,8 @@ class _RadioStationRowState extends State<RadioStationRow> with AutomaticKeepAli
|
||||
}
|
||||
|
||||
// Card layout: circle image + name below (same as ArtistRow)
|
||||
// Text area: 8px gap + ~36px for 2-line name = ~44px
|
||||
const textAreaHeight = 44.0;
|
||||
// Text area: 8px gap + ~40px for 2-line name = ~48px
|
||||
const textAreaHeight = 48.0;
|
||||
final imageSize = contentHeight - textAreaHeight;
|
||||
final cardWidth = imageSize; // Card width = image width (circle)
|
||||
final itemExtent = cardWidth + 16; // width + horizontal margins
|
||||
|
||||
@@ -179,7 +179,7 @@ class _SeriesRowState extends State<SeriesRow> with AutomaticKeepAliveClientMixi
|
||||
);
|
||||
}
|
||||
|
||||
const textAreaHeight = 44.0;
|
||||
const textAreaHeight = 48.0;
|
||||
final artworkSize = contentHeight - textAreaHeight;
|
||||
final cardWidth = artworkSize;
|
||||
final itemExtent = cardWidth + 12;
|
||||
|
||||
+21
-17
@@ -108,8 +108,8 @@ class _TrackRowState extends State<TrackRow> with AutomaticKeepAliveClientMixin
|
||||
}
|
||||
|
||||
// Card layout: square artwork + text below
|
||||
// Text area: 8px gap + ~18px title + ~18px artist = ~44px
|
||||
const textAreaHeight = 44.0;
|
||||
// Text area: 8px gap + ~20px title + ~20px artist = ~48px
|
||||
const textAreaHeight = 48.0;
|
||||
final artworkSize = contentHeight - textAreaHeight;
|
||||
final cardWidth = artworkSize; // Card width = artwork width (square)
|
||||
final itemExtent = cardWidth + 12;
|
||||
@@ -211,10 +211,10 @@ class _TrackCardState extends State<_TrackCard> with LibraryStatusMixin {
|
||||
final service = LibraryStatusService.instance;
|
||||
final key = libraryItemKey;
|
||||
if (!service.isInLibrary(key) && widget.track.inLibrary) {
|
||||
service.setLibraryStatus(key, true);
|
||||
service.initLibraryStatus(key, true);
|
||||
}
|
||||
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),
|
||||
// Track title
|
||||
Text(
|
||||
widget.track.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.titleSmall?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.track.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.titleSmall?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Artist name
|
||||
Text(
|
||||
widget.track.artistsString,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.track.artistsString,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user