diff --git a/lib/mixins/item_updatable.dart b/lib/mixins/item_updatable.dart index eb9fa298..529992cb 100644 --- a/lib/mixins/item_updatable.dart +++ b/lib/mixins/item_updatable.dart @@ -26,6 +26,7 @@ mixin ItemUpdatable on State { if (serverId == null) return; final updatedItem = await context.tryGetMediaClientForServer(serverId)?.fetchItem(itemId); if (updatedItem != null) { + if (!mounted) return; setState(() { updateItemInLists(itemId, updatedItem); }); diff --git a/lib/screens/discover_screen.dart b/lib/screens/discover_screen.dart index 7fdb8b66..d32de005 100644 --- a/lib/screens/discover_screen.dart +++ b/lib/screens/discover_screen.dart @@ -394,7 +394,7 @@ class _DiscoverScreenState extends State _heroController.animateToPage(nextPage, duration: const Duration(milliseconds: 500), curve: Curves.easeInOut); // Wait for page transition to complete before resetting progress Future.delayed(const Duration(milliseconds: 500), () { - if (!_isAutoScrollPaused) { + if (mounted && !_isAutoScrollPaused) { _startIndicatorProgress(); } }); @@ -402,11 +402,16 @@ class _DiscoverScreenState extends State } void _startIndicatorProgress() { + if (!mounted) return; _indicatorTimer?.cancel(); _indicatorProgress.value = 0.0; final totalSteps = _heroAutoScrollDuration.inMilliseconds ~/ _indicatorUpdateInterval.inMilliseconds; int step = 0; _indicatorTimer = Timer.periodic(_indicatorUpdateInterval, (timer) { + if (!mounted) { + timer.cancel(); + return; + } step++; _indicatorProgress.value = (step / totalSteps).clamp(0.0, 1.0); if (step >= totalSteps) { diff --git a/lib/screens/livetv/tabs/guide_tab.dart b/lib/screens/livetv/tabs/guide_tab.dart index 903c2e59..05f832b4 100644 --- a/lib/screens/livetv/tabs/guide_tab.dart +++ b/lib/screens/livetv/tabs/guide_tab.dart @@ -705,6 +705,7 @@ class GuideTabState extends State with MountedSetStateMixin { }), ], ).then((value) { + if (!mounted) return; if (value == null) { _guideFocusNode.requestFocus(); return; diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 0b9d8e62..f99d3a18 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -29,6 +29,7 @@ import '../models/livetv_capture_buffer.dart'; import '../models/livetv_channel.dart'; import '../models/transcode_quality_preset.dart'; import '../media/media_source_info.dart'; +import '../mixins/mounted_set_state_mixin.dart'; import '../providers/download_provider.dart'; import '../providers/multi_server_provider.dart'; import '../providers/playback_state_provider.dart'; @@ -213,7 +214,7 @@ class VideoPlayerScreen extends StatefulWidget { State createState() => VideoPlayerScreenState(); } -class VideoPlayerScreenState extends State with WidgetsBindingObserver { +class VideoPlayerScreenState extends State with WidgetsBindingObserver, MountedSetStateMixin { static const int _liveEdgeThresholdSeconds = 5; // Track the currently active video to guard against duplicate navigation @@ -1131,7 +1132,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin await _navigateToEpisode(metadata); } - void _setPlayerState(VoidCallback fn) => setState(fn); + void _setPlayerState(VoidCallback fn) => setStateIfMounted(fn); bool _isSwitchingChannel = false; diff --git a/lib/watch_together/screens/watch_together_screen.dart b/lib/watch_together/screens/watch_together_screen.dart index a339f968..8f643f02 100644 --- a/lib/watch_together/screens/watch_together_screen.dart +++ b/lib/watch_together/screens/watch_together_screen.dart @@ -347,12 +347,12 @@ class _NotInSessionViewState extends State<_NotInSessionView> with MountedSetSta if (name == null || !mounted) return; await RecentRoomsService.renameRoom(room.code, name.isEmpty ? null : name); - setState(() => _recentRooms = RecentRoomsService.getRecentRooms()); + setStateIfMounted(() => _recentRooms = RecentRoomsService.getRecentRooms()); } Future _removeRoom(RecentRoom room) async { await RecentRoomsService.removeRoom(room.code); - setState(() => _recentRooms = RecentRoomsService.getRecentRooms()); + setStateIfMounted(() => _recentRooms = RecentRoomsService.getRecentRooms()); } } diff --git a/lib/widgets/video_controls/desktop_video_controls.dart b/lib/widgets/video_controls/desktop_video_controls.dart index d4a0e78f..63ee3516 100644 --- a/lib/widgets/video_controls/desktop_video_controls.dart +++ b/lib/widgets/video_controls/desktop_video_controls.dart @@ -336,6 +336,7 @@ class DesktopVideoControlsState extends State { widget.onContentStripVisibilityChanged?.call(true); WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; _contentStripKey.currentState?.requestInitialFocus(); }); } @@ -349,6 +350,7 @@ class DesktopVideoControlsState extends State { // Return focus to the last focused button (or play/pause as fallback) WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; final target = _lastFocusedButtonNode; if (target != null && target.context != null) { target.requestFocus(); diff --git a/lib/widgets/video_controls/parts/playback_input.dart b/lib/widgets/video_controls/parts/playback_input.dart index 021eae9a..850012f4 100644 --- a/lib/widgets/video_controls/parts/playback_input.dart +++ b/lib/widgets/video_controls/parts/playback_input.dart @@ -167,6 +167,7 @@ extension _PlexVideoControlsPlaybackInputMethods on _PlexVideoControlsState { final delta = Duration(seconds: isForward ? _seekTimeSmall : -_seekTimeSmall); await _seekByOffset(delta); + if (!mounted) return; // Refresh feedback (extends timer, updates display) _showSkipFeedback(isForward: isForward); @@ -181,6 +182,7 @@ extension _PlexVideoControlsPlaybackInputMethods on _PlexVideoControlsState { final delta = Duration(seconds: isForward ? _seekTimeSmall : -_seekTimeSmall); await _seekByOffset(delta); + if (!mounted) return; _showSkipFeedback(isForward: isForward); diff --git a/lib/widgets/video_controls/parts/track_controls.dart b/lib/widgets/video_controls/parts/track_controls.dart index f87b2029..61bcd161 100644 --- a/lib/widgets/video_controls/parts/track_controls.dart +++ b/lib/widgets/video_controls/parts/track_controls.dart @@ -34,8 +34,9 @@ extension _PlexVideoControlsTrackMethods on _PlexVideoControlsState { shaderService .applyPreset(ShaderPreset.none) .then((_) { + if (!mounted) return; // ignore: no-empty-block - setState triggers rebuild to reflect disabled shader - if (mounted) _setControlsState(() {}); + _setControlsState(() {}); widget.onShaderChanged?.call(); }) .catchError((Object e, StackTrace st) { @@ -54,9 +55,10 @@ extension _PlexVideoControlsTrackMethods on _PlexVideoControlsState { shaderService .applyPreset(targetPreset) .then((_) { + if (!mounted) return; shaderProvider.setCurrentPreset(targetPreset); // ignore: no-empty-block - setState triggers rebuild to reflect restored shader - if (mounted) _setControlsState(() {}); + _setControlsState(() {}); widget.onShaderChanged?.call(); }) .catchError((Object e, StackTrace st) { diff --git a/lib/widgets/video_controls/parts/visibility.dart b/lib/widgets/video_controls/parts/visibility.dart index 53827a7a..662959d4 100644 --- a/lib/widgets/video_controls/parts/visibility.dart +++ b/lib/widgets/video_controls/parts/visibility.dart @@ -344,6 +344,7 @@ extension _PlexVideoControlsVisibilityMethods on _PlexVideoControlsState { if (requestFocus) { WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; _desktopControlsKey.currentState?.requestPlayPauseFocus(); }); } else { @@ -372,6 +373,7 @@ extension _PlexVideoControlsVisibilityMethods on _PlexVideoControlsState { _startHideTimer(); WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; _desktopControlsKey.currentState?.requestTimelineFocus(); }); } diff --git a/lib/widgets/video_controls/sheets/video_settings_sheet.dart b/lib/widgets/video_controls/sheets/video_settings_sheet.dart index 7b3fae3e..90d53547 100644 --- a/lib/widgets/video_controls/sheets/video_settings_sheet.dart +++ b/lib/widgets/video_controls/sheets/video_settings_sheet.dart @@ -798,8 +798,9 @@ class _VideoSettingsSheetState extends State { } await widget.shaderService!.applyPreset(preset); await shaderProvider.setPreset(preset); + if (!context.mounted) return; widget.onShaderChanged?.call(); - if (context.mounted) OverlaySheetController.of(context).close(); + OverlaySheetController.of(context).close(); }, ); }, @@ -826,6 +827,7 @@ class _VideoSettingsSheetState extends State { } await widget.shaderService!.applyPreset(preset); await shaderProvider.setPreset(preset); + if (!mounted) return; widget.onShaderChanged?.call(); } @@ -846,7 +848,7 @@ class _VideoSettingsSheetState extends State { // If the deleted shader is active, clear it from the player first if (widget.shaderService!.currentPreset.id == preset.id) { await widget.shaderService!.applyPreset(ShaderPreset.none); - widget.onShaderChanged?.call(); + if (mounted) widget.onShaderChanged?.call(); } await shaderProvider.deleteCustomShader(preset); diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index bf25acf8..adba3f07 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -22,6 +22,7 @@ import '../../services/pip_service.dart'; import 'package:window_manager/window_manager.dart'; import '../../mixins/settings_effect_mixin.dart'; +import '../../mixins/mounted_set_state_mixin.dart'; import '../../mpv/mpv.dart'; import '../overlay_sheet.dart'; import '../../focus/dpad_navigator.dart'; @@ -337,7 +338,8 @@ class PlexVideoControls extends StatefulWidget { State createState() => _PlexVideoControlsState(); } -class _PlexVideoControlsState extends State with WindowListener, SettingsEffectMixin { +class _PlexVideoControlsState extends State + with WindowListener, SettingsEffectMixin, MountedSetStateMixin { bool _showControls = true; bool _forceShowControls = false; bool _isLoadingExtras = false; @@ -501,7 +503,7 @@ class _PlexVideoControlsState extends State with WindowListen }); } - void _setControlsState(VoidCallback fn) => setState(fn); + void _setControlsState(VoidCallback fn) => setStateIfMounted(fn); @override void dispose() {