fix(lifecycle): guard async state updates

This commit is contained in:
edde746
2026-05-12 06:11:14 +02:00
parent 37b1a26cde
commit 3a6012cc21
11 changed files with 31 additions and 11 deletions
+1
View File
@@ -26,6 +26,7 @@ mixin ItemUpdatable<T extends StatefulWidget> on State<T> {
if (serverId == null) return;
final updatedItem = await context.tryGetMediaClientForServer(serverId)?.fetchItem(itemId);
if (updatedItem != null) {
if (!mounted) return;
setState(() {
updateItemInLists(itemId, updatedItem);
});
+6 -1
View File
@@ -394,7 +394,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
_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<DiscoverScreen>
}
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) {
+1
View File
@@ -705,6 +705,7 @@ class GuideTabState extends State<GuideTab> with MountedSetStateMixin {
}),
],
).then((value) {
if (!mounted) return;
if (value == null) {
_guideFocusNode.requestFocus();
return;
+3 -2
View File
@@ -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<VideoPlayerScreen> createState() => VideoPlayerScreenState();
}
class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindingObserver {
class VideoPlayerScreenState extends State<VideoPlayerScreen> 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<VideoPlayerScreen> with WidgetsBindin
await _navigateToEpisode(metadata);
}
void _setPlayerState(VoidCallback fn) => setState(fn);
void _setPlayerState(VoidCallback fn) => setStateIfMounted(fn);
bool _isSwitchingChannel = false;
@@ -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<void> _removeRoom(RecentRoom room) async {
await RecentRoomsService.removeRoom(room.code);
setState(() => _recentRooms = RecentRoomsService.getRecentRooms());
setStateIfMounted(() => _recentRooms = RecentRoomsService.getRecentRooms());
}
}
@@ -336,6 +336,7 @@ class DesktopVideoControlsState extends State<DesktopVideoControls> {
widget.onContentStripVisibilityChanged?.call(true);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_contentStripKey.currentState?.requestInitialFocus();
});
}
@@ -349,6 +350,7 @@ class DesktopVideoControlsState extends State<DesktopVideoControls> {
// 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();
@@ -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);
@@ -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) {
@@ -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();
});
}
@@ -798,8 +798,9 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
}
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<VideoSettingsSheet> {
}
await widget.shaderService!.applyPreset(preset);
await shaderProvider.setPreset(preset);
if (!mounted) return;
widget.onShaderChanged?.call();
}
@@ -846,7 +848,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
// 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);
@@ -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<PlexVideoControls> createState() => _PlexVideoControlsState();
}
class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListener, SettingsEffectMixin {
class _PlexVideoControlsState extends State<PlexVideoControls>
with WindowListener, SettingsEffectMixin, MountedSetStateMixin {
bool _showControls = true;
bool _forceShowControls = false;
bool _isLoadingExtras = false;
@@ -501,7 +503,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
});
}
void _setControlsState(VoidCallback fn) => setState(fn);
void _setControlsState(VoidCallback fn) => setStateIfMounted(fn);
@override
void dispose() {