fix(lifecycle): guard async state updates
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -705,6 +705,7 @@ class GuideTabState extends State<GuideTab> with MountedSetStateMixin {
|
||||
}),
|
||||
],
|
||||
).then((value) {
|
||||
if (!mounted) return;
|
||||
if (value == null) {
|
||||
_guideFocusNode.requestFocus();
|
||||
return;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user