fix(player): dismiss prompts on back

close #1373
This commit is contained in:
edde746
2026-06-18 03:25:30 +02:00
parent 6f39a286d3
commit a75cf5fd35
6 changed files with 74 additions and 0 deletions
@@ -291,6 +291,9 @@ extension _VideoPlayerBuildMethods on VideoPlayerScreenState {
onPlayPauseRequested: () => _playOrPauseWithPlaybackIntent(player!),
onSeekCompleted: _notifyWatchTogetherSeek,
onBack: _handleBackButton,
onDismissPrompt: (_showPlayNextDialog || _showStillWatchingPrompt)
? _dismissPlaybackPromptForBack
: null,
onReachedEnd: ({skipAutoPlayCountdown = false}) =>
_onVideoCompleted(true, skipAutoPlayCountdown: skipAutoPlayCountdown),
canControl: canControl,
@@ -109,6 +109,16 @@ extension _VideoPlayerPlaybackPromptMethods on VideoPlayerScreenState {
});
}
void _dismissPlaybackPromptForBack() {
if (_showPlayNextDialog) {
_cancelAutoPlay();
return;
}
if (_showStillWatchingPrompt) {
_dismissStillWatching();
}
}
/// Re-arm the end-of-video latch so Play Next can fire again. Callers
/// decide *when* it is safe to re-arm (media reloaded, or playback moved
/// back out of the end region); the latch itself refuses while a prompt
+4
View File
@@ -982,6 +982,10 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
/// Handle back button press
/// For non-host participants in Watch Together, shows leave session confirmation
Future<void> _handleBackButton() async {
if (_showPlayNextDialog || _showStillWatchingPrompt) {
_dismissPlaybackPromptForBack();
return;
}
if (_isHandlingBack) return;
_isHandlingBack = true;
try {
@@ -91,6 +91,8 @@ extension _PlexVideoControlsKeyEventMethods on _PlexVideoControlsState {
if (PlatformDetector.isTV() && event is KeyDownEvent) {
BackKeyCoordinator.markHandled();
}
final promptBackResult = handlePromptDismissBackKey(event, widget.onDismissPrompt);
if (promptBackResult != KeyEventResult.ignored) return true;
final backResult = handleBackKeyAction(event, () {
if (PlatformDetector.isTV()) {
if (_showControls) {
@@ -193,6 +195,10 @@ extension _PlexVideoControlsKeyEventMethods on _PlexVideoControlsState {
if (PlatformDetector.isTV() && event.logicalKey.isBackKey && event is KeyDownEvent) {
BackKeyCoordinator.markHandled();
}
final promptBackResult = handlePromptDismissBackKey(event, widget.onDismissPrompt);
if (promptBackResult != KeyEventResult.ignored) {
return promptBackResult;
}
final backResult = handleBackKeyAction(event, () {
if (PlatformDetector.isTV()) {
if (_showControls) {
@@ -159,6 +159,12 @@ bool shouldShowSkipMarkerButton({
return hasFirstFrame && hasMarker && !hasPlayNextPrompt && (!skipButtonDismissed || controlsVisible);
}
@visibleForTesting
KeyEventResult handlePromptDismissBackKey(KeyEvent event, VoidCallback? onDismissPrompt) {
if (onDismissPrompt == null || !event.logicalKey.isBackKey) return KeyEventResult.ignored;
return handleBackKeyAction(event, onDismissPrompt);
}
typedef PlaybackSourceChangeCallback =
Future<void> Function({
int? newMediaIndex,
@@ -214,6 +220,10 @@ class PlexVideoControls extends StatefulWidget {
/// Called when back button is pressed (for Watch Together session leave confirmation)
final VoidCallback? onBack;
/// Called when Back should dismiss a visible playback prompt before normal
/// player back handling.
final VoidCallback? onDismissPrompt;
/// Called when the video has effectively reached the end (e.g. credits extend
/// to EOF and can't be seeked past). Parent should route this into its normal
/// completion flow so the auto-play-next setting is honored.
@@ -314,6 +324,7 @@ class PlexVideoControls extends StatefulWidget {
this.onPlayPauseRequested,
this.onSeekCompleted,
this.onBack,
this.onDismissPrompt,
this.onReachedEnd,
this.canControl = true,
this.hasFirstFrame,
+40
View File
@@ -186,6 +186,38 @@ void main() {
});
});
group('handlePromptDismissBackKey', () {
test('ignores back keys when no prompt is visible', () {
var dismissCount = 0;
final result = handlePromptDismissBackKey(_keyUp(LogicalKeyboardKey.goBack), null);
expect(result, KeyEventResult.ignored);
expect(dismissCount, 0);
});
test('consumes key down and dismisses on key up', () {
var dismissCount = 0;
void dismissPrompt() => dismissCount++;
final downResult = handlePromptDismissBackKey(_keyDown(LogicalKeyboardKey.goBack), dismissPrompt);
final upResult = handlePromptDismissBackKey(_keyUp(LogicalKeyboardKey.goBack), dismissPrompt);
expect(downResult, KeyEventResult.handled);
expect(upResult, KeyEventResult.handled);
expect(dismissCount, 1);
});
test('ignores non-back keys', () {
var dismissCount = 0;
final result = handlePromptDismissBackKey(_keyDown(LogicalKeyboardKey.arrowLeft), () => dismissCount++);
expect(result, KeyEventResult.ignored);
expect(dismissCount, 0);
});
});
group('SkipMarkerButton', () {
testWidgets('tap cancels active auto-skip and performs skip', (tester) async {
final focusNode = FocusNode();
@@ -723,6 +755,14 @@ void main() {
});
}
KeyDownEvent _keyDown(LogicalKeyboardKey key) {
return KeyDownEvent(physicalKey: PhysicalKeyboardKey.escape, logicalKey: key, timeStamp: Duration.zero);
}
KeyUpEvent _keyUp(LogicalKeyboardKey key) {
return KeyUpEvent(physicalKey: PhysicalKeyboardKey.escape, logicalKey: key, timeStamp: Duration.zero);
}
Future<void> _pumpSkipMarkerButton(
WidgetTester tester, {
required FocusNode focusNode,