From 60f249151a8fd4d49bca48d5fa8d50d328d1562c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Haren=C4=8D=C3=A1k?= Date: Wed, 23 Sep 2026 21:26:59 +0200 Subject: [PATCH 1/2] Keep subtitles visible during Next-up preview --- .../components/video_player_next_wrapper.dart | 13 +- lib/screens/video_player/video_player.dart | 2 +- .../video_player/video_player_controls.dart | 176 ++++++++++-------- 3 files changed, 100 insertions(+), 91 deletions(-) diff --git a/lib/screens/video_player/components/video_player_next_wrapper.dart b/lib/screens/video_player/components/video_player_next_wrapper.dart index 529a467e4..b6fc9595b 100644 --- a/lib/screens/video_player/components/video_player_next_wrapper.dart +++ b/lib/screens/video_player/components/video_player_next_wrapper.dart @@ -26,11 +26,11 @@ import 'package:fladder/widgets/shared/progress_floating_button.dart'; class VideoPlayerNextWrapper extends ConsumerStatefulWidget { final Widget video; - final Widget controls; + final Widget Function(bool nextUpVisible) controlsBuilder; final List overlays; const VideoPlayerNextWrapper({ required this.video, - required this.controls, + required this.controlsBuilder, this.overlays = const [], super.key, }); @@ -296,14 +296,7 @@ class _VideoPlayerNextWrapperState extends ConsumerState ), child: widget.video, ), - IgnorePointer( - ignoring: show, - child: AnimatedOpacity( - opacity: show ? 0 : 1, - duration: animSpeed, - child: widget.controls, - ), - ), + widget.controlsBuilder(show), ], ), ), diff --git a/lib/screens/video_player/video_player.dart b/lib/screens/video_player/video_player.dart index b6091af35..67c40c99c 100644 --- a/lib/screens/video_player/video_player.dart +++ b/lib/screens/video_player/video_player.dart @@ -138,7 +138,7 @@ class _VideoPlayerState extends ConsumerState with WidgetsBindingOb ), _ => VideoPlayerNextWrapper( video: player, - controls: const DesktopControls(), + controlsBuilder: (nextUpVisible) => DesktopControls(nextUpVisible: nextUpVisible), overlays: [ if (errorPlaying) const _VideoErrorWidget(), ], diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 6a556751f..75a64a19a 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -45,7 +45,9 @@ import 'package:fladder/widgets/full_screen_helpers/full_screen_wrapper.dart'; import 'package:fladder/wrappers/pip_manager.dart'; class DesktopControls extends ConsumerStatefulWidget { - const DesktopControls({super.key}); + final bool nextUpVisible; + + const DesktopControls({this.nextUpVisible = false, super.key}); @override ConsumerState createState() => _DesktopControlsState(); @@ -111,7 +113,7 @@ class _DesktopControlsState extends ConsumerState { ); } final mediaSegments = ref.watch(playBackModel.select((value) => value?.mediaSegments)); - final subtitleWidget = player.subtitleWidget(showOverlay, controlsKey: _bottomControlsKey); + final subtitleWidget = player.subtitleWidget(showOverlay && !widget.nextUpVisible, controlsKey: _bottomControlsKey); final isDesktop = AdaptiveLayout.of(context).isDesktop || kIsWeb; final speedBoostEnabled = ref.watch(videoPlayerSettingsProvider.select((value) => value.enableSpeedBoost)); @@ -144,87 +146,101 @@ class _DesktopControlsState extends ConsumerState { child: Stack( children: [ Positioned.fill( - child: GestureDetector( - onTap: initInputDevice == InputDevice.pointer ? null : () => toggleOverlay(), - onDoubleTapDown: initInputDevice == InputDevice.touch ? _handleDoubleTapDown : null, - onDoubleTap: initInputDevice == InputDevice.pointer - ? () => fullScreenHelper.toggleFullScreen(ref) - : _handleDoubleTapSeek, - onLongPressStart: initInputDevice == InputDevice.touch ? _handleLongPressStart : null, - onLongPressEnd: initInputDevice == InputDevice.touch ? _handleLongPressEnd : null, - onVerticalDragStart: initInputDevice == InputDevice.touch ? _handleVerticalDragStart : null, - onVerticalDragUpdate: initInputDevice == InputDevice.touch ? _handleVerticalDragUpdate : null, - onVerticalDragEnd: initInputDevice == InputDevice.touch ? _handleVerticalDragEnd : null, - //better play/pause handling on Desktop (works with dragging on click) - onHorizontalDragDown: - initInputDevice == InputDevice.pointer ? (details) => player.playOrPause() : null, - ), - ), - if (subtitleWidget != null) subtitleWidget, - if (AdaptiveLayout.of(context).isDesktop) - Consumer(builder: (context, ref, child) { - final playing = ref.watch(mediaPlaybackProvider.select((value) => value.playing)); - final buffering = ref.watch(mediaPlaybackProvider.select((value) => value.buffering)); - return playButton(playing, buffering); - }), - IgnorePointer( - ignoring: !showOverlay, - child: AnimatedOpacity( - duration: fadeDuration, - opacity: showOverlay ? 1 : 0, - child: Column( - children: [ - topButtons(context), - const Spacer(), - bottomButtons(context), - ], - ), - ), - ), - VideoPlayerSeekIndicator(controller: _seekController), - const VideoPlayerVolumeIndicator(), - const VideoPlayerBrightnessIndicator(), - const VideoPlayerSpeedIndicator(), - const VideoPlayerScreenshotIndicator(), - Consumer( - builder: (context, ref, child) { - final position = ref.watch(mediaPlaybackProvider.select((value) => value.position)); - final skippedSegments = ref.watch(mediaPlaybackProvider.select((value) => value.skippedSegments)); - MediaSegment? segment = mediaSegments?.atPosition(position); - SegmentVisibility forceShow = - segment?.visibility(position, force: showOverlay) ?? SegmentVisibility.hidden; - final segmentSkipType = ref - .watch(videoPlayerSettingsProvider.select((value) => value.segmentSkipSettings[segment?.type])); - - final segmentId = segment != null ? '${segment.type.name}_${segment.start.inMilliseconds}' : null; - final wasSkipped = segmentId != null && skippedSegments.contains(segmentId); - - final autoSkip = forceShow != SegmentVisibility.hidden && - (segmentSkipType == SegmentSkip.skip || - (segmentSkipType == SegmentSkip.skipOnce && !wasSkipped)) && - player.lastState?.buffering == false; - - if (autoSkip) { - skipToSegmentEnd(segment, segmentId); - } - return Stack( - children: [ - Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.all(32), - child: SkipSegmentButton( - segment: segment, - skipType: segmentSkipType, - visibility: forceShow, - pressedSkip: () => skipToSegmentEnd(segment, null), + child: IgnorePointer( + ignoring: widget.nextUpVisible, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 250), + opacity: widget.nextUpVisible ? 0 : 1, + child: Stack( + children: [ + Positioned.fill( + child: GestureDetector( + onTap: initInputDevice == InputDevice.pointer ? null : () => toggleOverlay(), + onDoubleTapDown: initInputDevice == InputDevice.touch ? _handleDoubleTapDown : null, + onDoubleTap: initInputDevice == InputDevice.pointer + ? () => fullScreenHelper.toggleFullScreen(ref) + : _handleDoubleTapSeek, + onLongPressStart: initInputDevice == InputDevice.touch ? _handleLongPressStart : null, + onLongPressEnd: initInputDevice == InputDevice.touch ? _handleLongPressEnd : null, + onVerticalDragStart: initInputDevice == InputDevice.touch ? _handleVerticalDragStart : null, + onVerticalDragUpdate: initInputDevice == InputDevice.touch ? _handleVerticalDragUpdate : null, + onVerticalDragEnd: initInputDevice == InputDevice.touch ? _handleVerticalDragEnd : null, + //better play/pause handling on Desktop (works with dragging on click) + onHorizontalDragDown: + initInputDevice == InputDevice.pointer ? (details) => player.playOrPause() : null, ), ), - ), - ], - ); - }, + if (AdaptiveLayout.of(context).isDesktop) + Consumer(builder: (context, ref, child) { + final playing = ref.watch(mediaPlaybackProvider.select((value) => value.playing)); + final buffering = ref.watch(mediaPlaybackProvider.select((value) => value.buffering)); + return playButton(playing, buffering); + }), + IgnorePointer( + ignoring: !showOverlay, + child: AnimatedOpacity( + duration: fadeDuration, + opacity: showOverlay ? 1 : 0, + child: Column( + children: [ + topButtons(context), + const Spacer(), + bottomButtons(context), + ], + ), + ), + ), + VideoPlayerSeekIndicator(controller: _seekController), + const VideoPlayerVolumeIndicator(), + const VideoPlayerBrightnessIndicator(), + const VideoPlayerSpeedIndicator(), + const VideoPlayerScreenshotIndicator(), + Consumer( + builder: (context, ref, child) { + final position = ref.watch(mediaPlaybackProvider.select((value) => value.position)); + final skippedSegments = ref.watch(mediaPlaybackProvider.select((value) => value.skippedSegments)); + MediaSegment? segment = mediaSegments?.atPosition(position); + SegmentVisibility forceShow = + segment?.visibility(position, force: showOverlay) ?? SegmentVisibility.hidden; + final segmentSkipType = ref + .watch(videoPlayerSettingsProvider.select((value) => value.segmentSkipSettings[segment?.type])); + + final segmentId = segment != null ? '${segment.type.name}_${segment.start.inMilliseconds}' : null; + final wasSkipped = segmentId != null && skippedSegments.contains(segmentId); + + final autoSkip = forceShow != SegmentVisibility.hidden && + (segmentSkipType == SegmentSkip.skip || + (segmentSkipType == SegmentSkip.skipOnce && !wasSkipped)) && + player.lastState?.buffering == false; + + if (autoSkip) { + skipToSegmentEnd(segment, segmentId); + } + return Stack( + children: [ + Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.all(32), + child: SkipSegmentButton( + segment: segment, + skipType: segmentSkipType, + visibility: forceShow, + pressedSkip: () => skipToSegmentEnd(segment, null), + ), + ), + ), + ], + ); + }, + ), + ], + ), + ), + ), ), + // Keep the existing subtitle widget visible while Next-up hides the controls. + if (subtitleWidget != null) Positioned.fill(child: subtitleWidget), ], ), ), From e934b94320ccfd93bd53a6bef989646b46bbfe64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Haren=C4=8D=C3=A1k?= Date: Wed, 23 Sep 2026 21:44:39 +0200 Subject: [PATCH 2/2] Format video player controls --- .../video_player/video_player_controls.dart | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 75a64a19a..67e36fa17 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -162,8 +162,10 @@ class _DesktopControlsState extends ConsumerState { : _handleDoubleTapSeek, onLongPressStart: initInputDevice == InputDevice.touch ? _handleLongPressStart : null, onLongPressEnd: initInputDevice == InputDevice.touch ? _handleLongPressEnd : null, - onVerticalDragStart: initInputDevice == InputDevice.touch ? _handleVerticalDragStart : null, - onVerticalDragUpdate: initInputDevice == InputDevice.touch ? _handleVerticalDragUpdate : null, + onVerticalDragStart: + initInputDevice == InputDevice.touch ? _handleVerticalDragStart : null, + onVerticalDragUpdate: + initInputDevice == InputDevice.touch ? _handleVerticalDragUpdate : null, onVerticalDragEnd: initInputDevice == InputDevice.touch ? _handleVerticalDragEnd : null, //better play/pause handling on Desktop (works with dragging on click) onHorizontalDragDown: @@ -198,14 +200,16 @@ class _DesktopControlsState extends ConsumerState { Consumer( builder: (context, ref, child) { final position = ref.watch(mediaPlaybackProvider.select((value) => value.position)); - final skippedSegments = ref.watch(mediaPlaybackProvider.select((value) => value.skippedSegments)); + final skippedSegments = + ref.watch(mediaPlaybackProvider.select((value) => value.skippedSegments)); MediaSegment? segment = mediaSegments?.atPosition(position); SegmentVisibility forceShow = segment?.visibility(position, force: showOverlay) ?? SegmentVisibility.hidden; - final segmentSkipType = ref - .watch(videoPlayerSettingsProvider.select((value) => value.segmentSkipSettings[segment?.type])); + final segmentSkipType = ref.watch(videoPlayerSettingsProvider + .select((value) => value.segmentSkipSettings[segment?.type])); - final segmentId = segment != null ? '${segment.type.name}_${segment.start.inMilliseconds}' : null; + final segmentId = + segment != null ? '${segment.type.name}_${segment.start.inMilliseconds}' : null; final wasSkipped = segmentId != null && skippedSegments.contains(segmentId); final autoSkip = forceShow != SegmentVisibility.hidden &&