refactor(providers): apply DisposableChangeNotifierMixin to 11 providers

This commit is contained in:
edde746
2026-04-25 12:35:52 +02:00
parent eeed34e2df
commit c36fee17dd
11 changed files with 88 additions and 77 deletions
+25 -24
View File
@@ -12,12 +12,13 @@ import '../services/companion_remote/lan_discovery_service.dart';
import '../services/companion_remote/remote_auth_service.dart'; import '../services/companion_remote/remote_auth_service.dart';
import '../services/storage_service.dart'; import '../services/storage_service.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../mixins/disposable_change_notifier_mixin.dart';
export '../services/companion_remote/lan_discovery_service.dart' show DiscoveredHost; export '../services/companion_remote/lan_discovery_service.dart' show DiscoveredHost;
typedef CommandReceivedCallback = void Function(RemoteCommand command); typedef CommandReceivedCallback = void Function(RemoteCommand command);
class CompanionRemoteProvider with ChangeNotifier { class CompanionRemoteProvider with ChangeNotifier, DisposableChangeNotifierMixin {
RemoteSession? _session; RemoteSession? _session;
CompanionRemotePeerService? _peerService; CompanionRemotePeerService? _peerService;
LanDiscoveryService? _discoveryService; LanDiscoveryService? _discoveryService;
@@ -97,7 +98,7 @@ class CompanionRemoteProvider with ChangeNotifier {
_platform = Platform.operatingSystem; _platform = Platform.operatingSystem;
} }
notifyListeners(); safeNotifyListeners();
} }
/// Initialize crypto context from Plex home data. /// Initialize crypto context from Plex home data.
@@ -159,7 +160,7 @@ class CompanionRemoteProvider with ChangeNotifier {
); );
_session = RemoteSession(role: RemoteSessionRole.host, status: RemoteSessionStatus.connected); _session = RemoteSession(role: RemoteSessionRole.host, status: RemoteSessionStatus.connected);
notifyListeners(); safeNotifyListeners();
// Start LAN discovery broadcasting // Start LAN discovery broadcasting
_discoveryService ??= LanDiscoveryService(); _discoveryService ??= LanDiscoveryService();
@@ -181,7 +182,7 @@ class CompanionRemoteProvider with ChangeNotifier {
status: RemoteSessionStatus.error, status: RemoteSessionStatus.error,
errorMessage: e.toString(), errorMessage: e.toString(),
); );
notifyListeners(); safeNotifyListeners();
} }
} }
@@ -199,7 +200,7 @@ class CompanionRemoteProvider with ChangeNotifier {
_session = null; _session = null;
_isPlayerActive = false; _isPlayerActive = false;
_intentionalDisconnect = false; _intentionalDisconnect = false;
notifyListeners(); safeNotifyListeners();
} }
// ── Client: Discovery ── // ── Client: Discovery ──
@@ -237,7 +238,7 @@ class CompanionRemoteProvider with ChangeNotifier {
_setupPeerServiceListeners(); _setupPeerServiceListeners();
_session = RemoteSession(role: RemoteSessionRole.remote, status: RemoteSessionStatus.connecting); _session = RemoteSession(role: RemoteSessionRole.remote, status: RemoteSessionStatus.connecting);
notifyListeners(); safeNotifyListeners();
try { try {
final winner = await _peerService!.joinSessionRacing( final winner = await _peerService!.joinSessionRacing(
@@ -252,12 +253,12 @@ class CompanionRemoteProvider with ChangeNotifier {
_lastHostAddresses = [winner]; _lastHostAddresses = [winner];
_session = _session?.copyWith(status: RemoteSessionStatus.connected); _session = _session?.copyWith(status: RemoteSessionStatus.connected);
notifyListeners(); safeNotifyListeners();
appLogger.d('CompanionRemote: Connected to ${host.name} via $winner'); appLogger.d('CompanionRemote: Connected to ${host.name} via $winner');
} catch (e) { } catch (e) {
appLogger.e('CompanionRemote: Failed to connect to host', error: e); appLogger.e('CompanionRemote: Failed to connect to host', error: e);
_session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: e.toString()); _session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: e.toString());
notifyListeners(); safeNotifyListeners();
rethrow; rethrow;
} }
} }
@@ -279,7 +280,7 @@ class CompanionRemoteProvider with ChangeNotifier {
_setupPeerServiceListeners(); _setupPeerServiceListeners();
_session = RemoteSession(role: RemoteSessionRole.remote, status: RemoteSessionStatus.connecting); _session = RemoteSession(role: RemoteSessionRole.remote, status: RemoteSessionStatus.connecting);
notifyListeners(); safeNotifyListeners();
try { try {
await _peerService!.joinSession( await _peerService!.joinSession(
@@ -293,11 +294,11 @@ class CompanionRemoteProvider with ChangeNotifier {
); );
_session = _session?.copyWith(status: RemoteSessionStatus.connected); _session = _session?.copyWith(status: RemoteSessionStatus.connected);
notifyListeners(); safeNotifyListeners();
} catch (e) { } catch (e) {
appLogger.e('CompanionRemote: Failed to connect to manual host', error: e); appLogger.e('CompanionRemote: Failed to connect to manual host', error: e);
_session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: e.toString()); _session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: e.toString());
notifyListeners(); safeNotifyListeners();
rethrow; rethrow;
} }
} }
@@ -327,25 +328,25 @@ class CompanionRemoteProvider with ChangeNotifier {
_deviceConnectedSubscription = _peerService!.onDeviceConnected.listen((device) { _deviceConnectedSubscription = _peerService!.onDeviceConnected.listen((device) {
appLogger.d('CompanionRemote: Device connected: ${device.name}'); appLogger.d('CompanionRemote: Device connected: ${device.name}');
_session = _session?.copyWith(status: RemoteSessionStatus.connected, connectedDevice: device); _session = _session?.copyWith(status: RemoteSessionStatus.connected, connectedDevice: device);
notifyListeners(); safeNotifyListeners();
}); });
_deviceDisconnectedSubscription = _peerService!.onDeviceDisconnected.listen((_) { _deviceDisconnectedSubscription = _peerService!.onDeviceDisconnected.listen((_) {
appLogger.d('CompanionRemote: Device disconnected (intentional: $_intentionalDisconnect)'); appLogger.d('CompanionRemote: Device disconnected (intentional: $_intentionalDisconnect)');
if (_intentionalDisconnect) { if (_intentionalDisconnect) {
_session = _session?.copyWith(status: RemoteSessionStatus.disconnected, clearConnectedDevice: true); _session = _session?.copyWith(status: RemoteSessionStatus.disconnected, clearConnectedDevice: true);
notifyListeners(); safeNotifyListeners();
} else if (isHost) { } else if (isHost) {
_session = _session?.copyWith( _session = _session?.copyWith(
status: RemoteSessionStatus.reconnecting, status: RemoteSessionStatus.reconnecting,
clearConnectedDevice: true, clearConnectedDevice: true,
clearErrorMessage: true, clearErrorMessage: true,
); );
notifyListeners(); safeNotifyListeners();
appLogger.d('CompanionRemote: Host waiting for client to reconnect'); appLogger.d('CompanionRemote: Host waiting for client to reconnect');
} else { } else {
_session = _session?.copyWith(status: RemoteSessionStatus.reconnecting); _session = _session?.copyWith(status: RemoteSessionStatus.reconnecting);
notifyListeners(); safeNotifyListeners();
_scheduleReconnect(); _scheduleReconnect();
} }
}); });
@@ -353,13 +354,13 @@ class CompanionRemoteProvider with ChangeNotifier {
_errorSubscription = _peerService!.onError.listen((error) { _errorSubscription = _peerService!.onError.listen((error) {
appLogger.e('CompanionRemote: Error: ${error.message}'); appLogger.e('CompanionRemote: Error: ${error.message}');
_session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: error.message); _session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: error.message);
notifyListeners(); safeNotifyListeners();
}); });
_statusSubscription = _peerService!.onConnectionStateChanged.listen((status) { _statusSubscription = _peerService!.onConnectionStateChanged.listen((status) {
appLogger.d('CompanionRemote: Status changed: $status'); appLogger.d('CompanionRemote: Status changed: $status');
_session = _session?.copyWith(status: status); _session = _session?.copyWith(status: status);
notifyListeners(); safeNotifyListeners();
}); });
} }
@@ -375,7 +376,7 @@ class CompanionRemoteProvider with ChangeNotifier {
final device = RemoteDevice(id: id, name: name, platform: platform); final device = RemoteDevice(id: id, name: name, platform: platform);
_session = _session?.copyWith(connectedDevice: device); _session = _session?.copyWith(connectedDevice: device);
notifyListeners(); safeNotifyListeners();
} }
} }
@@ -383,7 +384,7 @@ class CompanionRemoteProvider with ChangeNotifier {
final playerActive = command.data?['playerActive'] as bool? ?? false; final playerActive = command.data?['playerActive'] as bool? ?? false;
if (_isPlayerActive != playerActive) { if (_isPlayerActive != playerActive) {
_isPlayerActive = playerActive; _isPlayerActive = playerActive;
notifyListeners(); safeNotifyListeners();
} }
} }
@@ -422,7 +423,7 @@ class CompanionRemoteProvider with ChangeNotifier {
errorMessage: 'Connection lost after $_maxReconnectAttempts attempts', errorMessage: 'Connection lost after $_maxReconnectAttempts attempts',
); );
_reconnectAttempts = 0; _reconnectAttempts = 0;
notifyListeners(); safeNotifyListeners();
return; return;
} }
@@ -438,7 +439,7 @@ class CompanionRemoteProvider with ChangeNotifier {
if (_lastHostAddresses == null || !isCryptoReady) { if (_lastHostAddresses == null || !isCryptoReady) {
appLogger.w('CompanionRemote: No stored context for reconnect'); appLogger.w('CompanionRemote: No stored context for reconnect');
_session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: 'Connection lost'); _session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: 'Connection lost');
notifyListeners(); safeNotifyListeners();
return; return;
} }
@@ -464,7 +465,7 @@ class CompanionRemoteProvider with ChangeNotifier {
_session = _session?.copyWith(status: RemoteSessionStatus.connected, clearErrorMessage: true); _session = _session?.copyWith(status: RemoteSessionStatus.connected, clearErrorMessage: true);
_reconnectAttempts = 0; _reconnectAttempts = 0;
notifyListeners(); safeNotifyListeners();
appLogger.d('CompanionRemote: Reconnected successfully'); appLogger.d('CompanionRemote: Reconnected successfully');
} catch (e) { } catch (e) {
appLogger.e('CompanionRemote: Reconnect failed', error: e); appLogger.e('CompanionRemote: Reconnect failed', error: e);
@@ -484,7 +485,7 @@ class CompanionRemoteProvider with ChangeNotifier {
_reconnectTimer?.cancel(); _reconnectTimer?.cancel();
_reconnectAttempts = 0; _reconnectAttempts = 0;
_session = _session?.copyWith(status: RemoteSessionStatus.disconnected, clearConnectedDevice: true); _session = _session?.copyWith(status: RemoteSessionStatus.disconnected, clearConnectedDevice: true);
notifyListeners(); safeNotifyListeners();
} }
Future<void> leaveSession() async { Future<void> leaveSession() async {
@@ -506,7 +507,7 @@ class CompanionRemoteProvider with ChangeNotifier {
} }
_isPlayerActive = false; _isPlayerActive = false;
_intentionalDisconnect = false; _intentionalDisconnect = false;
notifyListeners(); safeNotifyListeners();
} }
@override @override
+6 -5
View File
@@ -1,10 +1,11 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../mixins/disposable_change_notifier_mixin.dart';
import '../services/storage_service.dart'; import '../services/storage_service.dart';
/// Provider for managing hidden library state across the app. /// Provider for managing hidden library state across the app.
/// This ensures that when a library is hidden/unhidden in one screen, /// This ensures that when a library is hidden/unhidden in one screen,
/// all other screens are automatically updated. /// all other screens are automatically updated.
class HiddenLibrariesProvider extends ChangeNotifier { class HiddenLibrariesProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
late StorageService _storageService; late StorageService _storageService;
Set<String> _hiddenLibraryKeys = {}; Set<String> _hiddenLibraryKeys = {};
bool _isInitialized = false; bool _isInitialized = false;
@@ -31,7 +32,7 @@ class HiddenLibrariesProvider extends ChangeNotifier {
_storageService = await StorageService.getInstance(); _storageService = await StorageService.getInstance();
_hiddenLibraryKeys = _storageService.getHiddenLibraries(); _hiddenLibraryKeys = _storageService.getHiddenLibraries();
_isInitialized = true; _isInitialized = true;
notifyListeners(); safeNotifyListeners();
} }
/// Hide a library by its key /// Hide a library by its key
@@ -41,7 +42,7 @@ class HiddenLibrariesProvider extends ChangeNotifier {
if (!_hiddenLibraryKeys.contains(libraryKey)) { if (!_hiddenLibraryKeys.contains(libraryKey)) {
_hiddenLibraryKeys = Set.from(_hiddenLibraryKeys)..add(libraryKey); _hiddenLibraryKeys = Set.from(_hiddenLibraryKeys)..add(libraryKey);
await _storageService.saveHiddenLibraries(_hiddenLibraryKeys); await _storageService.saveHiddenLibraries(_hiddenLibraryKeys);
notifyListeners(); safeNotifyListeners();
} }
} }
@@ -52,7 +53,7 @@ class HiddenLibrariesProvider extends ChangeNotifier {
if (_hiddenLibraryKeys.contains(libraryKey)) { if (_hiddenLibraryKeys.contains(libraryKey)) {
_hiddenLibraryKeys = Set.from(_hiddenLibraryKeys)..remove(libraryKey); _hiddenLibraryKeys = Set.from(_hiddenLibraryKeys)..remove(libraryKey);
await _storageService.saveHiddenLibraries(_hiddenLibraryKeys); await _storageService.saveHiddenLibraries(_hiddenLibraryKeys);
notifyListeners(); safeNotifyListeners();
} }
} }
@@ -63,6 +64,6 @@ class HiddenLibrariesProvider extends ChangeNotifier {
/// Useful if storage was modified outside the provider /// Useful if storage was modified outside the provider
Future<void> refresh() async { Future<void> refresh() async {
_hiddenLibraryKeys = _storageService.getHiddenLibraries(); _hiddenLibraryKeys = _storageService.getHiddenLibraries();
notifyListeners(); safeNotifyListeners();
} }
} }
+7 -6
View File
@@ -1,5 +1,6 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../mixins/disposable_change_notifier_mixin.dart';
import '../models/plex_library.dart'; import '../models/plex_library.dart';
import '../services/data_aggregation_service.dart'; import '../services/data_aggregation_service.dart';
import '../services/storage_service.dart'; import '../services/storage_service.dart';
@@ -12,7 +13,7 @@ enum LibrariesLoadState { initial, loading, loaded, error }
/// Provider that serves as the single source of truth for library data. /// Provider that serves as the single source of truth for library data.
/// Both SideNavigationRail and LibrariesScreen consume this provider /// Both SideNavigationRail and LibrariesScreen consume this provider
/// instead of independently fetching library data. /// instead of independently fetching library data.
class LibrariesProvider extends ChangeNotifier { class LibrariesProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
DataAggregationService? _aggregationService; DataAggregationService? _aggregationService;
List<PlexLibrary> _libraries = []; List<PlexLibrary> _libraries = [];
LibrariesLoadState _loadState = LibrariesLoadState.initial; LibrariesLoadState _loadState = LibrariesLoadState.initial;
@@ -60,7 +61,7 @@ class LibrariesProvider extends ChangeNotifier {
_loadState = LibrariesLoadState.loading; _loadState = LibrariesLoadState.loading;
_errorMessage = null; _errorMessage = null;
notifyListeners(); safeNotifyListeners();
try { try {
// Fetch libraries from all servers // Fetch libraries from all servers
@@ -79,12 +80,12 @@ class LibrariesProvider extends ChangeNotifier {
_errorMessage = null; _errorMessage = null;
appLogger.i('LibrariesProvider: Loaded ${_libraries.length} libraries'); appLogger.i('LibrariesProvider: Loaded ${_libraries.length} libraries');
notifyListeners(); safeNotifyListeners();
} catch (e, stackTrace) { } catch (e, stackTrace) {
appLogger.e('LibrariesProvider: Failed to load libraries', error: e, stackTrace: stackTrace); appLogger.e('LibrariesProvider: Failed to load libraries', error: e, stackTrace: stackTrace);
_loadState = LibrariesLoadState.error; _loadState = LibrariesLoadState.error;
_errorMessage = e.toString(); _errorMessage = e.toString();
notifyListeners(); safeNotifyListeners();
} }
} }
@@ -100,7 +101,7 @@ class LibrariesProvider extends ChangeNotifier {
/// Update the library order and persist it. /// Update the library order and persist it.
Future<void> updateLibraryOrder(List<PlexLibrary> orderedLibraries) async { Future<void> updateLibraryOrder(List<PlexLibrary> orderedLibraries) async {
_libraries = List.from(orderedLibraries); _libraries = List.from(orderedLibraries);
notifyListeners(); safeNotifyListeners();
// Save the new order // Save the new order
final storage = await StorageService.getInstance(); final storage = await StorageService.getInstance();
@@ -115,7 +116,7 @@ class LibrariesProvider extends ChangeNotifier {
_libraries = []; _libraries = [];
_loadState = LibrariesLoadState.initial; _loadState = LibrariesLoadState.initial;
_errorMessage = null; _errorMessage = null;
notifyListeners(); safeNotifyListeners();
appLogger.d('LibrariesProvider: Cleared library data'); appLogger.d('LibrariesProvider: Cleared library data');
} }
+5 -4
View File
@@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../i18n/strings.g.dart'; import '../i18n/strings.g.dart';
import '../mixins/disposable_change_notifier_mixin.dart';
import '../models/download_models.dart'; import '../models/download_models.dart';
import '../models/plex_metadata.dart'; import '../models/plex_metadata.dart';
import '../services/offline_watch_sync_service.dart'; import '../services/offline_watch_sync_service.dart';
@@ -18,7 +19,7 @@ import '../utils/global_key_utils.dart';
/// - Effective watch status (local changes + cached server data) /// - Effective watch status (local changes + cached server data)
/// - Offline "OnDeck" calculation for shows /// - Offline "OnDeck" calculation for shows
/// - Manual mark watched/unwatched while offline /// - Manual mark watched/unwatched while offline
class OfflineWatchProvider extends ChangeNotifier { class OfflineWatchProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
final OfflineWatchSyncService _syncService; final OfflineWatchSyncService _syncService;
final DownloadProvider _downloadProvider; final DownloadProvider _downloadProvider;
@@ -30,7 +31,7 @@ class OfflineWatchProvider extends ChangeNotifier {
} }
void _onSyncServiceChanged() { void _onSyncServiceChanged() {
notifyListeners(); safeNotifyListeners();
} }
/// Whether a sync is in progress /// Whether a sync is in progress
@@ -178,7 +179,7 @@ class OfflineWatchProvider extends ChangeNotifier {
isNowWatched: true, isNowWatched: true,
changeType: WatchStateChangeType.watched, changeType: WatchStateChangeType.watched,
); );
notifyListeners(); safeNotifyListeners();
_autoDeleteIfWatched(serverId, ratingKey); _autoDeleteIfWatched(serverId, ratingKey);
} }
@@ -219,7 +220,7 @@ class OfflineWatchProvider extends ChangeNotifier {
isNowWatched: false, isNowWatched: false,
changeType: WatchStateChangeType.unwatched, changeType: WatchStateChangeType.unwatched,
); );
notifyListeners(); safeNotifyListeners();
} }
/// Get downloaded episodes for a show with their watch status. /// Get downloaded episodes for a show with their watch status.
+6 -5
View File
@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
import '../models/plex_metadata.dart'; import '../models/plex_metadata.dart';
import '../models/play_queue_response.dart'; import '../models/play_queue_response.dart';
import '../services/plex_client.dart'; import '../services/plex_client.dart';
import '../mixins/disposable_change_notifier_mixin.dart';
/// Result of trying to locate the current queue index. /// Result of trying to locate the current queue index.
class _IndexLookupResult { class _IndexLookupResult {
@@ -14,7 +15,7 @@ class _IndexLookupResult {
/// Manages playback state using Plex's play queue API. /// Manages playback state using Plex's play queue API.
/// This provider is session-only and does not persist across app restarts. /// This provider is session-only and does not persist across app restarts.
class PlaybackStateProvider with ChangeNotifier { class PlaybackStateProvider with ChangeNotifier, DisposableChangeNotifierMixin {
// Play queue state // Play queue state
int? _playQueueId; int? _playQueueId;
int _playQueueTotalCount = 0; int _playQueueTotalCount = 0;
@@ -61,7 +62,7 @@ class PlaybackStateProvider with ChangeNotifier {
void setCurrentItem(PlexMetadata metadata) { void setCurrentItem(PlexMetadata metadata) {
if (_isQueueMode && metadata.playQueueItemID != null) { if (_isQueueMode && metadata.playQueueItemID != null) {
_currentPlayQueueItemID = metadata.playQueueItemID; _currentPlayQueueItemID = metadata.playQueueItemID;
notifyListeners(); safeNotifyListeners();
} }
} }
@@ -79,7 +80,7 @@ class PlaybackStateProvider with ChangeNotifier {
_contextKey = contextKey; _contextKey = contextKey;
_isQueueMode = true; _isQueueMode = true;
notifyListeners(); safeNotifyListeners();
} }
/// Load more items from the play queue if needed /// Load more items from the play queue if needed
@@ -106,7 +107,7 @@ class PlaybackStateProvider with ChangeNotifier {
// Use size or items length as fallback if totalCount is null // Use size or items length as fallback if totalCount is null
_playQueueTotalCount = response.playQueueTotalCount ?? response.size ?? response.items!.length; _playQueueTotalCount = response.playQueueTotalCount ?? response.size ?? response.items!.length;
_playQueueShuffled = response.playQueueShuffled; _playQueueShuffled = response.playQueueShuffled;
notifyListeners(); safeNotifyListeners();
return true; return true;
} }
} catch (e) { } catch (e) {
@@ -249,6 +250,6 @@ class PlaybackStateProvider with ChangeNotifier {
_loadedItems = []; _loadedItems = [];
_contextKey = null; _contextKey = null;
_isQueueMode = false; _isQueueMode = false;
notifyListeners(); safeNotifyListeners();
} }
} }
+5 -4
View File
@@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../mixins/disposable_change_notifier_mixin.dart';
import '../models/transcode_quality_preset.dart'; import '../models/transcode_quality_preset.dart';
import '../services/settings_service.dart'; import '../services/settings_service.dart';
class SettingsProvider extends ChangeNotifier { class SettingsProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
SettingsService? _settingsService; SettingsService? _settingsService;
bool _isInitialized = false; bool _isInitialized = false;
Future<void>? _initFuture; Future<void>? _initFuture;
@@ -21,7 +22,7 @@ class SettingsProvider extends ChangeNotifier {
if (_isInitialized) return; if (_isInitialized) return;
_settingsService = await SettingsService.getInstance(); _settingsService = await SettingsService.getInstance();
_isInitialized = true; _isInitialized = true;
notifyListeners(); safeNotifyListeners();
} }
/// Re-read settings after an external mutation (import, reset). The provider /// Re-read settings after an external mutation (import, reset). The provider
@@ -30,7 +31,7 @@ class SettingsProvider extends ChangeNotifier {
Future<void> reload() async { Future<void> reload() async {
_settingsService = await SettingsService.getInstance(); _settingsService = await SettingsService.getInstance();
_isInitialized = true; _isInitialized = true;
notifyListeners(); safeNotifyListeners();
} }
T _read<T>(Pref<T> pref, T fallback) => _isInitialized ? _settingsService!.read(pref) : fallback; T _read<T>(Pref<T> pref, T fallback) => _isInitialized ? _settingsService!.read(pref) : fallback;
@@ -39,7 +40,7 @@ class SettingsProvider extends ChangeNotifier {
if (!_isInitialized) await _initializeSettings(); if (!_isInitialized) await _initializeSettings();
if (_settingsService!.read(pref) == value) return; if (_settingsService!.read(pref) == value) return;
await _settingsService!.write(pref, value); await _settingsService!.write(pref, value);
notifyListeners(); safeNotifyListeners();
} }
int get libraryDensity => _read(SettingsService.libraryDensity, LibraryDensity.defaultValue); int get libraryDensity => _read(SettingsService.libraryDensity, LibraryDensity.defaultValue);
+6 -5
View File
@@ -1,5 +1,6 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../mixins/disposable_change_notifier_mixin.dart';
import '../models/shader_preset.dart'; import '../models/shader_preset.dart';
import '../services/settings_service.dart'; import '../services/settings_service.dart';
import '../services/shader_asset_loader.dart'; import '../services/shader_asset_loader.dart';
@@ -7,7 +8,7 @@ import '../services/shader_asset_loader.dart';
/// Provider for managing shader preset state. /// Provider for managing shader preset state.
/// ///
/// Persists the selected shader preset so it is restored across sessions. /// Persists the selected shader preset so it is restored across sessions.
class ShaderProvider extends ChangeNotifier { class ShaderProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
late SettingsService _settingsService; late SettingsService _settingsService;
ShaderPreset _savedPreset = ShaderPreset.none; ShaderPreset _savedPreset = ShaderPreset.none;
@@ -31,7 +32,7 @@ class ShaderProvider extends ChangeNotifier {
_currentPreset = _savedPreset; _currentPreset = _savedPreset;
_initialized = true; _initialized = true;
notifyListeners(); safeNotifyListeners();
} }
/// Whether the provider has finished initializing /// Whether the provider has finished initializing
@@ -63,7 +64,7 @@ class ShaderProvider extends ChangeNotifier {
_savedPreset = preset; _savedPreset = preset;
_currentPreset = preset; _currentPreset = preset;
await _settingsService.write(SettingsService.globalShaderPreset, preset.id); await _settingsService.write(SettingsService.globalShaderPreset, preset.id);
notifyListeners(); safeNotifyListeners();
} }
/// Update the current preset without persisting (e.g. toggling off temporarily) /// Update the current preset without persisting (e.g. toggling off temporarily)
@@ -84,7 +85,7 @@ class ShaderProvider extends ChangeNotifier {
_customPresets.add(preset); _customPresets.add(preset);
await _saveCustomPresets(); await _saveCustomPresets();
notifyListeners(); safeNotifyListeners();
return preset; return preset;
} }
@@ -103,7 +104,7 @@ class ShaderProvider extends ChangeNotifier {
await _settingsService.write(SettingsService.globalShaderPreset, ShaderPreset.none.id); await _settingsService.write(SettingsService.globalShaderPreset, ShaderPreset.none.id);
} }
notifyListeners(); safeNotifyListeners();
} }
Future<void> _saveCustomPresets() async { Future<void> _saveCustomPresets() async {
+6 -5
View File
@@ -2,10 +2,11 @@ import 'dart:io' show Platform;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import '../mixins/disposable_change_notifier_mixin.dart';
import '../services/settings_service.dart' as settings; import '../services/settings_service.dart' as settings;
import '../theme/mono_theme.dart'; import '../theme/mono_theme.dart';
class ThemeProvider extends ChangeNotifier { class ThemeProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
late settings.SettingsService _settingsService; late settings.SettingsService _settingsService;
settings.ThemeMode _themeMode = settings.ThemeMode.system; settings.ThemeMode _themeMode = settings.ThemeMode.system;
late Brightness _systemBrightness; late Brightness _systemBrightness;
@@ -19,7 +20,7 @@ class ThemeProvider extends ChangeNotifier {
void _onBrightnessChanged() { void _onBrightnessChanged() {
_systemBrightness = WidgetsBinding.instance.platformDispatcher.platformBrightness; _systemBrightness = WidgetsBinding.instance.platformDispatcher.platformBrightness;
if (_themeMode == settings.ThemeMode.system) { if (_themeMode == settings.ThemeMode.system) {
notifyListeners(); safeNotifyListeners();
} }
} }
@@ -35,7 +36,7 @@ class ThemeProvider extends ChangeNotifier {
_settingsService = await settings.SettingsService.getInstance(); _settingsService = await settings.SettingsService.getInstance();
_themeMode = _settingsService.read(settings.SettingsService.themeMode); _themeMode = _settingsService.read(settings.SettingsService.themeMode);
_updateSplashTheme(_themeMode); _updateSplashTheme(_themeMode);
notifyListeners(); safeNotifyListeners();
} }
settings.ThemeMode get themeMode => _themeMode; settings.ThemeMode get themeMode => _themeMode;
@@ -81,7 +82,7 @@ class ThemeProvider extends ChangeNotifier {
_themeMode = mode; _themeMode = mode;
await _settingsService.write(settings.SettingsService.themeMode, mode); await _settingsService.write(settings.SettingsService.themeMode, mode);
_updateSplashTheme(mode); _updateSplashTheme(mode);
notifyListeners(); safeNotifyListeners();
} }
} }
@@ -92,7 +93,7 @@ class ThemeProvider extends ChangeNotifier {
final mode = _settingsService.read(settings.SettingsService.themeMode); final mode = _settingsService.read(settings.SettingsService.themeMode);
_themeMode = mode; _themeMode = mode;
_updateSplashTheme(mode); _updateSplashTheme(mode);
notifyListeners(); safeNotifyListeners();
} }
void _updateSplashTheme(settings.ThemeMode mode) { void _updateSplashTheme(settings.ThemeMode mode) {
+8 -7
View File
@@ -24,11 +24,12 @@ import '../services/trackers/tracker_connect_runner.dart';
import '../services/trackers/tracker_constants.dart'; import '../services/trackers/tracker_constants.dart';
import '../services/trackers/tracker_coordinator.dart'; import '../services/trackers/tracker_coordinator.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../mixins/disposable_change_notifier_mixin.dart';
/// Owns the active MAL / AniList / Simkl sessions for the currently-selected /// Owns the active MAL / AniList / Simkl sessions for the currently-selected
/// Plex profile. Single rebind seam: [onActiveProfileChanged] loads all three /// Plex profile. Single rebind seam: [onActiveProfileChanged] loads all three
/// sessions from their stores and pushes them to their trackers. /// sessions from their stores and pushes them to their trackers.
class TrackersProvider extends ChangeNotifier { class TrackersProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
final MalAuthService _malAuth = MalAuthService(); final MalAuthService _malAuth = MalAuthService();
final AnilistAuthService _anilistAuth = AnilistAuthService(); final AnilistAuthService _anilistAuth = AnilistAuthService();
final SimklAuthService _simklAuth = SimklAuthService(); final SimklAuthService _simklAuth = SimklAuthService();
@@ -77,7 +78,7 @@ class TrackersProvider extends ChangeNotifier {
_anilist = results[1] as AnilistSession?; _anilist = results[1] as AnilistSession?;
_simkl = results[2] as SimklSession?; _simkl = results[2] as SimklSession?;
_rebindAll(); _rebindAll();
notifyListeners(); safeNotifyListeners();
} }
// ───── Connect / disconnect ───── // ───── Connect / disconnect ─────
@@ -159,7 +160,7 @@ class TrackersProvider extends ChangeNotifier {
if (_connecting != null || alreadyConnected) return false; if (_connecting != null || alreadyConnected) return false;
_connecting = service; _connecting = service;
_cancelCompleter = Completer<void>(); _cancelCompleter = Completer<void>();
notifyListeners(); safeNotifyListeners();
try { try {
return await runConnectPipeline<T>( return await runConnectPipeline<T>(
logLabel: service.name, logLabel: service.name,
@@ -173,14 +174,14 @@ class TrackersProvider extends ChangeNotifier {
if (c != null && !c.isCompleted) c.complete(); if (c != null && !c.isCompleted) c.complete();
_cancelCompleter = null; _cancelCompleter = null;
_connecting = null; _connecting = null;
notifyListeners(); safeNotifyListeners();
} }
} }
Future<void> _clearAndRebind<T>(TrackerAccountStore<T> store, void Function() clearAndRebind) async { Future<void> _clearAndRebind<T>(TrackerAccountStore<T> store, void Function() clearAndRebind) async {
await store.clear(_activeUserUuid); await store.clear(_activeUserUuid);
clearAndRebind(); clearAndRebind();
notifyListeners(); safeNotifyListeners();
} }
Future<MalSession> _enrichMal(MalSession raw) async { Future<MalSession> _enrichMal(MalSession raw) async {
@@ -240,7 +241,7 @@ class TrackersProvider extends ChangeNotifier {
onSessionUpdated: (next) { onSessionUpdated: (next) {
_mal = next; _mal = next;
malAccountStore.save(_activeUserUuid, next); malAccountStore.save(_activeUserUuid, next);
notifyListeners(); safeNotifyListeners();
}, },
); );
} }
@@ -263,7 +264,7 @@ class TrackersProvider extends ChangeNotifier {
store.clear(_activeUserUuid); store.clear(_activeUserUuid);
clearSession(); clearSession();
rebind(); rebind();
notifyListeners(); safeNotifyListeners();
} }
@override @override
+4 -3
View File
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../mixins/disposable_change_notifier_mixin.dart';
import '../models/trackers/device_code.dart'; import '../models/trackers/device_code.dart';
import '../services/trackers/tracker_connect_runner.dart'; import '../services/trackers/tracker_connect_runner.dart';
import '../services/trakt/trakt_account_store.dart'; import '../services/trakt/trakt_account_store.dart';
@@ -16,7 +17,7 @@ import '../utils/app_logger.dart';
/// ///
/// Single rebind seam: `onActiveProfileChanged` loads the new profile's /// Single rebind seam: `onActiveProfileChanged` loads the new profile's
/// session and pushes it to both `TraktScrobbleService` and `TraktSyncService`. /// session and pushes it to both `TraktScrobbleService` and `TraktSyncService`.
class TraktAccountProvider extends ChangeNotifier { class TraktAccountProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
final TraktAuthService _auth = TraktAuthService(); final TraktAuthService _auth = TraktAuthService();
final _store = traktAccountStore; final _store = traktAccountStore;
@@ -71,7 +72,7 @@ class TraktAccountProvider extends ChangeNotifier {
if (c != null && !c.isCompleted) c.complete(); if (c != null && !c.isCompleted) c.complete();
_cancelCompleter = null; _cancelCompleter = null;
_isConnecting = false; _isConnecting = false;
notifyListeners(); safeNotifyListeners();
} }
} }
@@ -107,7 +108,7 @@ class TraktAccountProvider extends ChangeNotifier {
session, session,
onSessionInvalidated: _handleSessionInvalidated, onSessionInvalidated: _handleSessionInvalidated,
); );
notifyListeners(); safeNotifyListeners();
} }
/// Called by [TraktClient] when refresh fails permanently. Clears local state /// Called by [TraktClient] when refresh fails permanently. Clears local state
+10 -9
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../mixins/disposable_change_notifier_mixin.dart';
import '../utils/plex_http_exception.dart'; import '../utils/plex_http_exception.dart';
import '../models/plex_home.dart'; import '../models/plex_home.dart';
import '../models/plex_home_user.dart'; import '../models/plex_home_user.dart';
@@ -8,7 +9,7 @@ import '../services/storage_service.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../screens/profile/pin_entry_dialog.dart'; import '../screens/profile/pin_entry_dialog.dart';
class UserProfileProvider extends ChangeNotifier { class UserProfileProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
PlexHome? _home; PlexHome? _home;
PlexHomeUser? _currentUser; PlexHomeUser? _currentUser;
PlexUserProfile? _profileSettings; PlexUserProfile? _profileSettings;
@@ -117,7 +118,7 @@ class UserProfileProvider extends ChangeNotifier {
// Profile settings are NOT cached - they will be fetched fresh from API // Profile settings are NOT cached - they will be fetched fresh from API
// in refreshProfileSettings() // in refreshProfileSettings()
notifyListeners(); safeNotifyListeners();
} }
/// Fetch the user's profile settings from the API /// Fetch the user's profile settings from the API
@@ -140,7 +141,7 @@ class UserProfileProvider extends ChangeNotifier {
appLogger.i('Successfully fetched user profile settings from API'); appLogger.i('Successfully fetched user profile settings from API');
notifyListeners(); safeNotifyListeners();
} catch (e) { } catch (e) {
appLogger.w('Failed to fetch user profile settings from API', error: e); appLogger.w('Failed to fetch user profile settings from API', error: e);
// Don't set error state, profile will remain null or keep existing value // Don't set error state, profile will remain null or keep existing value
@@ -210,7 +211,7 @@ class UserProfileProvider extends ChangeNotifier {
} }
} }
notifyListeners(); safeNotifyListeners();
} catch (e) { } catch (e) {
_setError('Failed to load home users: $e'); _setError('Failed to load home users: $e');
appLogger.e('Failed to load home users', error: e); appLogger.e('Failed to load home users', error: e);
@@ -286,7 +287,7 @@ class UserProfileProvider extends ChangeNotifier {
}, },
); );
notifyListeners(); safeNotifyListeners();
// Invalidate all cached data and reconnect to all servers with new tokens // Invalidate all cached data and reconnect to all servers with new tokens
// The callback will handle server reconnection using the servers list // The callback will handle server reconnection using the servers list
@@ -337,7 +338,7 @@ class UserProfileProvider extends ChangeNotifier {
// Update current user from refreshed data // Update current user from refreshed data
if (_home != null) { if (_home != null) {
_currentUser = _home!.getUserByUUID(_currentUser!.uuid); _currentUser = _home!.getUserByUUID(_currentUser!.uuid);
notifyListeners(); safeNotifyListeners();
} }
} }
} }
@@ -361,7 +362,7 @@ class UserProfileProvider extends ChangeNotifier {
_isInitialized = false; _isInitialized = false;
_clearError(); _clearError();
notifyListeners(); safeNotifyListeners();
appLogger.i('User logged out successfully'); appLogger.i('User logged out successfully');
} catch (e) { } catch (e) {
@@ -373,12 +374,12 @@ class UserProfileProvider extends ChangeNotifier {
void _setLoading(bool loading) { void _setLoading(bool loading) {
_isLoading = loading; _isLoading = loading;
notifyListeners(); safeNotifyListeners();
} }
void _setError(String error) { void _setError(String error) {
_error = error; _error = error;
notifyListeners(); safeNotifyListeners();
} }
void _clearError() { void _clearError() {