From b9f87bbc5efaafb08b44d2e1a2bee29c3c177c9e Mon Sep 17 00:00:00 2001 From: Kirill Boychenko Date: Mon, 28 Jul 2025 00:17:03 +0200 Subject: [PATCH] fix: Adjust subtitle offset to avoid overlap with visible menu --- .../video_player/video_player_controls.dart | 279 +++++++++--------- lib/wrappers/players/lib_mpv.dart | 63 +++- 2 files changed, 192 insertions(+), 150 deletions(-) diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 4216b0f..9f2b2df 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -288,149 +288,160 @@ class _DesktopControlsState extends ConsumerState { ); } + final GlobalKey _bottomControlsKey = GlobalKey(); + Widget bottomButtons(BuildContext context) { - return Consumer(builder: (context, ref, child) { - final mediaPlayback = ref.watch(mediaPlaybackProvider); - final bitRateOptions = ref.watch(playBackModel.select((value) => value?.bitRateOptions)); - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [ - Colors.black.withValues(alpha: 0.8), - Colors.black.withValues(alpha: 0), - ], - )), - child: Padding( - padding: MediaQuery.paddingOf(context).add( - const EdgeInsets.symmetric(horizontal: 16).copyWith(bottom: 12), - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: progressBar(mediaPlayback), + return Container( + key: _bottomControlsKey, + child: Consumer(builder: (context, ref, child) { + final mediaPlayback = ref.watch(mediaPlaybackProvider); + final bitRateOptions = ref.watch(playBackModel.select((value) => value?.bitRateOptions)); + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black.withValues(alpha: 0.8), + Colors.black.withValues(alpha: 0), + ], + )), + child: Padding( + padding: MediaQuery.paddingOf(context).add( + const EdgeInsets.symmetric(horizontal: 16).copyWith(bottom: 12), ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.center, + child: Column( children: [ - Flexible( - flex: 2, - child: Row( - children: [ - IconButton( - onPressed: () => showVideoPlayerOptions(context, () => minimizePlayer(context)), - icon: const Icon(IconsaxPlusLinear.more)), - if (AdaptiveLayout.layoutOf(context) == ViewSize.tablet) ...[ - IconButton( - onPressed: () => showSubSelection(context), - icon: const Icon(IconsaxPlusLinear.subtitle), - ), - IconButton( - onPressed: () => showAudioSelection(context), - icon: const Icon(IconsaxPlusLinear.audio_square), - ), - ], - if (AdaptiveLayout.layoutOf(context) == ViewSize.desktop) ...[ - Flexible( - child: ElevatedButton.icon( - onPressed: () => showSubSelection(context), - icon: const Icon(IconsaxPlusLinear.subtitle), - label: Text( - ref.watch(playBackModel.select((value) { - final language = value?.mediaStreams?.currentSubStream?.language; - return language?.isEmpty == true ? context.localized.off : language; - }))?.capitalize() ?? - "", - maxLines: 1, - ), - ), - ), - Flexible( - child: ElevatedButton.icon( - onPressed: () => showAudioSelection(context), - icon: const Icon(IconsaxPlusLinear.audio_square), - label: Text( - ref.watch(playBackModel.select((value) { - final language = value?.mediaStreams?.currentAudioStream?.language; - return language?.isEmpty == true ? context.localized.off : language; - }))?.capitalize() ?? - "", - maxLines: 1, - ), - ), - ) - ], - ].addInBetween(const SizedBox( - width: 4, - )), - ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: progressBar(mediaPlayback), ), - previousButton, - seekBackwardButton(ref), - IconButton.filledTonal( - iconSize: 38, - onPressed: () { - ref.read(videoPlayerProvider).playOrPause(); - }, - icon: Icon( - mediaPlayback.playing ? IconsaxPlusBold.pause : IconsaxPlusBold.play, - ), - ), - seekForwardButton(ref), - nextVideoButton, - Flexible( - flex: 2, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) - Tooltip( - message: context.localized.stop, - child: IconButton( - onPressed: () => closePlayer(), icon: const Icon(IconsaxPlusLinear.close_square))), - const Spacer(), - if (AdaptiveLayout.viewSizeOf(context) >= ViewSize.tablet && - ref.read(videoPlayerProvider).hasPlayer) ...{ - if (bitRateOptions?.isNotEmpty == true) - Tooltip( - message: context.localized.qualityOptionsTitle, - child: IconButton( - onPressed: () => openQualityOptions(context), - icon: const Icon(IconsaxPlusLinear.speedometer), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + flex: 2, + child: Row( + children: [ + IconButton( + onPressed: () => showVideoPlayerOptions(context, () => minimizePlayer(context)), + icon: const Icon(IconsaxPlusLinear.more)), + if (AdaptiveLayout.layoutOf(context) == ViewSize.tablet) ...[ + IconButton( + onPressed: () => showSubSelection(context), + icon: const Icon(IconsaxPlusLinear.subtitle), ), - ), + IconButton( + onPressed: () => showAudioSelection(context), + icon: const Icon(IconsaxPlusLinear.audio_square), + ), + ], + if (AdaptiveLayout.layoutOf(context) == ViewSize.desktop) ...[ + Flexible( + child: ElevatedButton.icon( + onPressed: () => showSubSelection(context), + icon: const Icon(IconsaxPlusLinear.subtitle), + label: Text( + ref.watch(playBackModel.select((value) { + final language = value?.mediaStreams?.currentSubStream?.language; + return language?.isEmpty == true ? context.localized.off : language; + }))?.capitalize() ?? + "", + maxLines: 1, + ), + ), + ), + Flexible( + child: ElevatedButton.icon( + onPressed: () => showAudioSelection(context), + icon: const Icon(IconsaxPlusLinear.audio_square), + label: Text( + ref.watch(playBackModel.select((value) { + final language = value?.mediaStreams?.currentAudioStream?.language; + return language?.isEmpty == true ? context.localized.off : language; + }))?.capitalize() ?? + "", + maxLines: 1, + ), + ), + ) + ], + ].addInBetween(const SizedBox( + width: 4, + )), + ), + ), + previousButton, + seekBackwardButton(ref), + IconButton.filledTonal( + iconSize: 38, + onPressed: () { + ref.read(videoPlayerProvider).playOrPause(); }, - if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer && - AdaptiveLayout.viewSizeOf(context) > ViewSize.phone) ...[ - Listener( - onPointerSignal: (event) { - if (event is PointerScrollEvent) { - if (event.scrollDelta.dy > 0) { - ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5); - } else { - ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(5); - } - } + icon: Icon( + mediaPlayback.playing ? IconsaxPlusBold.pause : IconsaxPlusBold.play, + ), + ), + seekForwardButton(ref), + nextVideoButton, + Flexible( + flex: 2, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) + Tooltip( + message: context.localized.stop, + child: IconButton( + onPressed: () => closePlayer(), + icon: const Icon(IconsaxPlusLinear.close_square))), + const Spacer(), + if (AdaptiveLayout.viewSizeOf(context) >= ViewSize.tablet && + ref.read(videoPlayerProvider).hasPlayer) ...{ + if (bitRateOptions?.isNotEmpty == true) + Tooltip( + message: context.localized.qualityOptionsTitle, + child: IconButton( + onPressed: () => openQualityOptions(context), + icon: const Icon(IconsaxPlusLinear.speedometer), + ), + ), }, - child: VideoVolumeSlider( - onChanged: () => resetTimer(), - ), - ), - const FullScreenButton(), - ] - ].addInBetween(const SizedBox(width: 8)), - ), + if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer && + AdaptiveLayout.viewSizeOf(context) > ViewSize.phone) ...[ + Listener( + onPointerSignal: (event) { + if (event is PointerScrollEvent) { + if (event.scrollDelta.dy > 0) { + ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5); + } else { + ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(5); + } + } + }, + child: VideoVolumeSlider( + onChanged: () => resetTimer(), + ), + ), + const FullScreenButton(), + ] + ].addInBetween(const SizedBox(width: 8)), + ), + ), + ].addInBetween(const SizedBox(width: 6)), ), - ].addInBetween(const SizedBox(width: 6)), + ], ), - ], - ), - ), - ); - }); + ), + ); + })); + } + + // Method to get height + double? getMenuHeight() { + final RenderBox? renderBox = _bottomControlsKey.currentContext?.findRenderObject() as RenderBox?; + return renderBox?.size.height; } Widget progressBar(MediaPlaybackModel mediaPlayback) { diff --git a/lib/wrappers/players/lib_mpv.dart b/lib/wrappers/players/lib_mpv.dart index 9d6237f..8235988 100644 --- a/lib/wrappers/players/lib_mpv.dart +++ b/lib/wrappers/players/lib_mpv.dart @@ -196,6 +196,7 @@ class LibMPV extends BasePlayer { class _VideoSubtitles extends ConsumerStatefulWidget { final VideoController controller; final bool showOverlay; + const _VideoSubtitles({ required this.controller, this.showOverlay = false, @@ -211,12 +212,14 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { @override void initState() { - subscription = widget.controller.player.stream.subtitle.listen((value) { - setState(() { - subtitle = value; - }); - }); super.initState(); + subscription = widget.controller.player.stream.subtitle.listen((value) { + if (mounted) { + setState(() { + subtitle = value; + }); + } + }); } @override @@ -225,24 +228,52 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { super.dispose(); } + /// Calculate subtitle offset based on menu visibility + double _calculateSubtitleOffset(SubtitleSettingsModel settings) { + if (!widget.showOverlay) { + return settings.verticalOffset; + } + + // Estimate the menu area (bottom ~15% of screen typically contains controls) + const menuAreaThreshold = 0.15; + + // If subtitles are already positioned above the menu area, leave them alone + if (settings.verticalOffset >= menuAreaThreshold) { + return settings.verticalOffset; + } + + // When menu is visible and subtitles are in the menu area, + // move them up slightly to avoid overlap + const menuAvoidanceOffset = 0.1; + final adjustedOffset = settings.verticalOffset + menuAvoidanceOffset; + + // Clamp to reasonable bounds (don't go too high or too low) + return math.min(adjustedOffset, 0.85); // Max 85% up from bottom + } + @override Widget build(BuildContext context) { final settings = ref.watch(subtitleSettingsProvider); final padding = MediaQuery.of(context).padding; - final text = [ - for (final line in subtitle) - if (line.trim().isNotEmpty) line.trim(), - ].join('\n'); + // Process subtitle text + final text = subtitle.where((line) => line.trim().isNotEmpty).map((line) => line.trim()).join('\n'); + + // Return empty widget if libass is enabled (native subtitle rendering) if (widget.controller.player.platform?.configuration.libass ?? false) { return const IgnorePointer(child: SizedBox.shrink()); - } else { - return SubtitleText( - subModel: settings, - padding: padding, - offset: settings.verticalOffset, // Always use user's preferred position - text: text, - ); } + + // Return empty widget if no subtitle text + if (text.isEmpty) { + return const IgnorePointer(child: SizedBox.shrink()); + } + + return SubtitleText( + subModel: settings, + padding: padding, + offset: _calculateSubtitleOffset(settings), + text: text, + ); } }