Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e27e6a4c2 | ||
|
|
b136b26b7e | ||
|
|
404828c47c | ||
|
|
48afbf9d50 | ||
|
|
ebf4e53328 |
@@ -17,10 +17,14 @@
|
||||
### Integration Status
|
||||
|
||||
✅ **Phase 1 Complete**: All 5 providers created
|
||||
✅ **Phase 2 Started**: Integration into main.dart completed
|
||||
✅ **Phase 2 Complete**: Integration into main.dart completed
|
||||
- All providers added to MultiProvider tree
|
||||
- Shared services (CacheService, PositionTracker, ImageHelperService) properly initialized
|
||||
- Build successful (52.2MB APK)
|
||||
- QueueManagerService circular dependency resolved
|
||||
- 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
|
||||
|
||||
|
||||
@@ -3420,13 +3420,12 @@ class _NewLibraryScreenState extends State<NewLibraryScreen>
|
||||
Widget _buildPodcastsTab(BuildContext context, S l10n) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
// PERF: Use select() to only rebuild when podcasts, loading state, or enabled providers changes
|
||||
// PERF: Use watch() - cannot use select() inside PageView.builder
|
||||
// Use podcastsUnfiltered to avoid double-filtering with MA's provider filter
|
||||
final (allPodcasts, isLoading, enabledProviders) = context.select<MusicAssistantProvider, (List<MediaItem>, bool, Set<String>)>(
|
||||
(p) => (p.podcastsUnfiltered, p.isLoadingPodcasts, p.enabledProviderIds.toSet()),
|
||||
);
|
||||
// Use read() for methods that don't need reactive updates
|
||||
final maProvider = context.read<MusicAssistantProvider>();
|
||||
final maProvider = context.watch<MusicAssistantProvider>();
|
||||
final allPodcasts = maProvider.podcastsUnfiltered;
|
||||
final isLoading = maProvider.isLoadingPodcasts;
|
||||
final enabledProviders = maProvider.enabledProviderIds.toSet();
|
||||
|
||||
if (isLoading) {
|
||||
return Center(child: CircularProgressIndicator(color: colorScheme.primary));
|
||||
@@ -3724,13 +3723,12 @@ class _NewLibraryScreenState extends State<NewLibraryScreen>
|
||||
Widget _buildRadioStationsTab(BuildContext context, S l10n) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
// PERF: Use select() to only rebuild when radio stations, loading state, or enabled providers changes
|
||||
// PERF: Use watch() - cannot use select() inside PageView.builder
|
||||
// Use radioStationsUnfiltered to avoid double-filtering with MA's provider filter
|
||||
final (allRadioStations, isLoading, enabledProviders) = context.select<MusicAssistantProvider, (List<MediaItem>, bool, Set<String>)>(
|
||||
(p) => (p.radioStationsUnfiltered, p.isLoadingRadio, p.enabledProviderIds.toSet()),
|
||||
);
|
||||
// Use read() for methods that don't need reactive updates
|
||||
final maProvider = context.read<MusicAssistantProvider>();
|
||||
final maProvider = context.watch<MusicAssistantProvider>();
|
||||
final allRadioStations = maProvider.radioStationsUnfiltered;
|
||||
final isLoading = maProvider.isLoadingRadio;
|
||||
final enabledProviders = maProvider.enabledProviderIds.toSet();
|
||||
|
||||
if (isLoading) {
|
||||
return Center(child: CircularProgressIndicator(color: colorScheme.primary));
|
||||
@@ -3972,11 +3970,11 @@ class _NewLibraryScreenState extends State<NewLibraryScreen>
|
||||
|
||||
// ============ ARTISTS TAB ============
|
||||
Widget _buildArtistsTab(BuildContext context, S l10n) {
|
||||
// Use select to only rebuild when specific fields change
|
||||
// Use watch instead of select - cannot use select inside PageView.builder
|
||||
// SyncService changes are handled by _onSyncServiceChanged listener
|
||||
final (isLoading, enabledProviders) = context.select<MusicAssistantProvider, (bool, Set<String>)>(
|
||||
(p) => (p.isLoading, p.enabledProviderIds.toSet()),
|
||||
);
|
||||
final provider = context.watch<MusicAssistantProvider>();
|
||||
final isLoading = provider.isLoading;
|
||||
final enabledProviders = provider.enabledProviderIds.toSet();
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final syncService = SyncService.instance;
|
||||
|
||||
@@ -4209,11 +4207,11 @@ class _NewLibraryScreenState extends State<NewLibraryScreen>
|
||||
|
||||
// ============ ALBUMS TAB ============
|
||||
Widget _buildAlbumsTab(BuildContext context, S l10n) {
|
||||
// Use select to only rebuild when specific fields change
|
||||
// Use watch instead of select - cannot use select inside PageView.builder
|
||||
// SyncService changes are handled by _onSyncServiceChanged listener
|
||||
final (isLoading, enabledProviders) = context.select<MusicAssistantProvider, (bool, Set<String>)>(
|
||||
(p) => (p.isLoading, p.enabledProviderIds.toSet()),
|
||||
);
|
||||
final provider = context.watch<MusicAssistantProvider>();
|
||||
final isLoading = provider.isLoading;
|
||||
final enabledProviders = provider.enabledProviderIds.toSet();
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final syncService = SyncService.instance;
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ class LibraryStatusService extends ChangeNotifier {
|
||||
required bool Function(dynamic item) getFavorite,
|
||||
required String Function(dynamic item) getProvider,
|
||||
required String Function(dynamic item) getItemId,
|
||||
bool notify = true,
|
||||
}) {
|
||||
bool changed = false;
|
||||
|
||||
@@ -130,7 +131,7 @@ class LibraryStatusService extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
if (changed && notify) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,6 +417,7 @@ class ExpandablePlayerState extends State<ExpandablePlayer>
|
||||
_controller.removeListener(_recordAnimationFrame);
|
||||
_lyricsScrollController.removeListener(_onLyricsScroll);
|
||||
|
||||
_expansionNotifyTimer?.cancel();
|
||||
_controller.dispose();
|
||||
_queuePanelController.dispose();
|
||||
_sleepTimerPanelController.dispose();
|
||||
@@ -615,6 +616,8 @@ class ExpandablePlayerState extends State<ExpandablePlayer>
|
||||
|
||||
// PERF Phase 4: Track last notified value to avoid unnecessary object creation
|
||||
double _lastNotifiedProgress = -1;
|
||||
Timer? _expansionNotifyTimer;
|
||||
double? _pendingProgress;
|
||||
|
||||
void _notifyExpansionProgress() {
|
||||
final progress = _controller.value;
|
||||
@@ -623,12 +626,24 @@ class ExpandablePlayerState extends State<ExpandablePlayer>
|
||||
if ((progress - _lastNotifiedProgress).abs() >= 0.01 ||
|
||||
progress == 0.0 || progress == 1.0) {
|
||||
_lastNotifiedProgress = progress;
|
||||
_pendingProgress = progress;
|
||||
|
||||
// Cancel any pending timer to avoid queue buildup
|
||||
_expansionNotifyTimer?.cancel();
|
||||
|
||||
// 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(
|
||||
progress,
|
||||
_pendingProgress!,
|
||||
_currentExpandedBgColor,
|
||||
_currentExpandedPrimaryColor,
|
||||
);
|
||||
}
|
||||
_expansionNotifyTimer = null;
|
||||
_pendingProgress = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _subscribeToPositionTracker() {
|
||||
|
||||
@@ -109,6 +109,8 @@ mixin LibraryStatusMixin<T extends StatefulWidget> on State<T> {
|
||||
/// Whether the item is currently a favorite
|
||||
bool get isFavorite => LibraryStatusService.instance.isFavorite(libraryItemKey);
|
||||
|
||||
bool _isStatusUpdatePending = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -122,10 +124,17 @@ mixin LibraryStatusMixin<T extends StatefulWidget> on State<T> {
|
||||
}
|
||||
|
||||
void _onLibraryStatusChanged() {
|
||||
// Trigger rebuild to pick up new status values
|
||||
// Use addPostFrameCallback to ensure setState is called AFTER the build phase
|
||||
// This prevents "setState during build" errors
|
||||
if (_isStatusUpdatePending) return;
|
||||
|
||||
_isStatusUpdatePending = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
_isStatusUpdatePending = false;
|
||||
});
|
||||
}
|
||||
|
||||
/// Set library status optimistically and return previous value for rollback
|
||||
|
||||
Reference in New Issue
Block a user