From c7afade615412abffe6203403f8213a415587246 Mon Sep 17 00:00:00 2001 From: Kirill Boychenko Date: Sun, 27 Jul 2025 23:56:13 +0200 Subject: [PATCH 01/11] fix: Adjust subtitle offset to always use user's preferred position --- lib/wrappers/players/lib_mpv.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/wrappers/players/lib_mpv.dart b/lib/wrappers/players/lib_mpv.dart index a0ac078..9d6237f 100644 --- a/lib/wrappers/players/lib_mpv.dart +++ b/lib/wrappers/players/lib_mpv.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -31,7 +32,7 @@ class LibMPV extends BasePlayer { dispose(); mpv.MediaKit.ensureInitialized(); - + _player = mpv.Player( configuration: mpv.PlayerConfiguration( title: "nl.jknaapen.fladder", @@ -239,7 +240,7 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { return SubtitleText( subModel: settings, padding: padding, - offset: (widget.showOverlay ? 0.5 : settings.verticalOffset), + offset: settings.verticalOffset, // Always use user's preferred position text: text, ); } From b9f87bbc5efaafb08b44d2e1a2bee29c3c177c9e Mon Sep 17 00:00:00 2001 From: Kirill Boychenko Date: Mon, 28 Jul 2025 00:17:03 +0200 Subject: [PATCH 02/11] 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, + ); } } From 480766f75ffd7b10b35e6e22176e4538279b0991 Mon Sep 17 00:00:00 2001 From: Kirill Boychenko Date: Mon, 28 Jul 2025 00:50:16 +0200 Subject: [PATCH 03/11] refactor: Promote constants for subtitle positioning to improve readability and flexibility --- .../video_player/video_player_controls.dart | 279 +++++++++--------- lib/wrappers/players/lib_mpv.dart | 21 +- 2 files changed, 145 insertions(+), 155 deletions(-) diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 9f2b2df..147dea8 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -288,160 +288,149 @@ class _DesktopControlsState extends ConsumerState { ); } - final GlobalKey _bottomControlsKey = GlobalKey(); - Widget bottomButtons(BuildContext context) { - 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), + return Container(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), + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: progressBar(mediaPlayback), ), - child: Column( + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: progressBar(mediaPlayback), + 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, + )), + ), ), - 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), + 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), ), - 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(); + ), }, - 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), - ), - ), + 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); + } + } }, - 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)), + child: VideoVolumeSlider( + onChanged: () => resetTimer(), + ), + ), + const FullScreenButton(), + ] + ].addInBetween(const SizedBox(width: 8)), + ), ), - ], + ].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 8235988..f306359 100644 --- a/lib/wrappers/players/lib_mpv.dart +++ b/lib/wrappers/players/lib_mpv.dart @@ -207,12 +207,17 @@ class _VideoSubtitles extends ConsumerStatefulWidget { } class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { + // Promote constants to static for better readability and flexibility + static const double _menuAreaThreshold = 0.15; // Bottom 15% typically contains controls + static const double _menuAvoidanceOffset = 0.1; // Move up by 10% when needed + static const double _maxSubtitleOffset = 0.85; // Max 85% up from bottom + late List subtitle = widget.controller.player.state.subtitle; StreamSubscription>? subscription; @override void initState() { - super.initState(); + super.initState(); // Move to very start as per best practices subscription = widget.controller.player.stream.subtitle.listen((value) { if (mounted) { setState(() { @@ -234,21 +239,17 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { 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) { + 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; + 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 + return math.max(0.0, math.min(adjustedOffset, _maxSubtitleOffset)); } @override @@ -261,12 +262,12 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { // Return empty widget if libass is enabled (native subtitle rendering) if (widget.controller.player.platform?.configuration.libass ?? false) { - return const IgnorePointer(child: SizedBox.shrink()); + return const SizedBox.shrink(); } // Return empty widget if no subtitle text if (text.isEmpty) { - return const IgnorePointer(child: SizedBox.shrink()); + return const SizedBox.shrink(); } return SubtitleText( From 2b2cd40d4fab52cedae777eece17b9c0ffddec82 Mon Sep 17 00:00:00 2001 From: bk <106319367+bk-bf@users.noreply.github.com> Date: Mon, 28 Jul 2025 01:06:07 +0200 Subject: [PATCH 04/11] Update lib/wrappers/players/lib_mpv.dart Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- lib/wrappers/players/lib_mpv.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/wrappers/players/lib_mpv.dart b/lib/wrappers/players/lib_mpv.dart index f306359..a1f343b 100644 --- a/lib/wrappers/players/lib_mpv.dart +++ b/lib/wrappers/players/lib_mpv.dart @@ -219,6 +219,13 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { void initState() { super.initState(); // Move to very start as per best practices subscription = widget.controller.player.stream.subtitle.listen((value) { + + @override + void dispose() { + subscription?.cancel(); + subscription = null; + super.dispose(); + } if (mounted) { setState(() { subtitle = value; From ee824f5f16f73028522c120e4532c18a3a2be9d3 Mon Sep 17 00:00:00 2001 From: Kirill Boychenko Date: Mon, 28 Jul 2025 01:13:32 +0200 Subject: [PATCH 05/11] refactor: Move super.initState() to the start of initState for best practices --- lib/wrappers/players/lib_mpv.dart | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/wrappers/players/lib_mpv.dart b/lib/wrappers/players/lib_mpv.dart index a1f343b..f306359 100644 --- a/lib/wrappers/players/lib_mpv.dart +++ b/lib/wrappers/players/lib_mpv.dart @@ -219,13 +219,6 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { void initState() { super.initState(); // Move to very start as per best practices subscription = widget.controller.player.stream.subtitle.listen((value) { - - @override - void dispose() { - subscription?.cancel(); - subscription = null; - super.dispose(); - } if (mounted) { setState(() { subtitle = value; From d60522b021f56186acdb7f66a0dfccd87c2bcd0f Mon Sep 17 00:00:00 2001 From: Kirill Boychenko Date: Mon, 28 Jul 2025 01:20:30 +0200 Subject: [PATCH 06/11] refactor: Simplify subtitle rendering logic with early return --- lib/screens/video_player/video_player_controls.dart | 4 ++-- lib/wrappers/players/lib_mpv.dart | 9 ++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 147dea8..4216b0f 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -289,7 +289,7 @@ class _DesktopControlsState extends ConsumerState { } Widget bottomButtons(BuildContext context) { - return Container(child: Consumer(builder: (context, ref, child) { + return Consumer(builder: (context, ref, child) { final mediaPlayback = ref.watch(mediaPlaybackProvider); final bitRateOptions = ref.watch(playBackModel.select((value) => value?.bitRateOptions)); return Container( @@ -430,7 +430,7 @@ class _DesktopControlsState extends ConsumerState { ), ), ); - })); + }); } Widget progressBar(MediaPlaybackModel mediaPlayback) { diff --git a/lib/wrappers/players/lib_mpv.dart b/lib/wrappers/players/lib_mpv.dart index f306359..fa0e590 100644 --- a/lib/wrappers/players/lib_mpv.dart +++ b/lib/wrappers/players/lib_mpv.dart @@ -260,13 +260,8 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { // 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 SizedBox.shrink(); - } - - // Return empty widget if no subtitle text - if (text.isEmpty) { + // Early return for cases where subtitles shouldn't be rendered + if ((widget.controller.player.platform?.configuration.libass ?? false) || text.isEmpty) { return const SizedBox.shrink(); } From 1fdab92f1f72c53f71c172eeecf6c9ba863a30e2 Mon Sep 17 00:00:00 2001 From: Kirill Boychenko Date: Mon, 28 Jul 2025 02:13:00 +0200 Subject: [PATCH 07/11] feat: Enhance subtitle handling with dynamic menu height adjustment --- .../video_player/video_player_controls.dart | 12 +++++- lib/stubs/web/lib_mdk_web.dart | 2 +- lib/wrappers/media_control_wrapper.dart | 3 +- lib/wrappers/players/base_player.dart | 5 ++- lib/wrappers/players/lib_mdk.dart | 2 +- lib/wrappers/players/lib_mpv.dart | 43 +++++++++++++------ 6 files changed, 47 insertions(+), 20 deletions(-) diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 4216b0f..ed0593a 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -41,6 +41,9 @@ class DesktopControls extends ConsumerStatefulWidget { } class _DesktopControlsState extends ConsumerState { + // Add GlobalKey to measure bottom controls height + final GlobalKey _bottomControlsKey = GlobalKey(); + late RestartableTimer timer = RestartableTimer( const Duration(seconds: 5), () => mounted ? toggleOverlay(value: false) : null, @@ -108,11 +111,17 @@ class _DesktopControlsState extends ConsumerState { timer.reset(); } + // Method to get actual menu height + double? getBottomControlsHeight() { + final RenderBox? renderBox = _bottomControlsKey.currentContext?.findRenderObject() as RenderBox?; + return renderBox?.size.height; + } + @override Widget build(BuildContext context) { final mediaSegments = ref.watch(playBackModel.select((value) => value?.mediaSegments)); final player = ref.watch(videoPlayerProvider); - final subtitleWidget = player.subtitleWidget(showOverlay); + final subtitleWidget = player.subtitleWidget(showOverlay, menuHeight: getBottomControlsHeight()); return InputHandler( autoFocus: false, onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored, @@ -293,6 +302,7 @@ class _DesktopControlsState extends ConsumerState { final mediaPlayback = ref.watch(mediaPlaybackProvider); final bitRateOptions = ref.watch(playBackModel.select((value) => value?.bitRateOptions)); return Container( + key: _bottomControlsKey, // Add key to measure height decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.bottomCenter, diff --git a/lib/stubs/web/lib_mdk_web.dart b/lib/stubs/web/lib_mdk_web.dart index 3e3a790..abb2e17 100644 --- a/lib/stubs/web/lib_mdk_web.dart +++ b/lib/stubs/web/lib_mdk_web.dart @@ -64,7 +64,7 @@ class LibMDK extends BasePlayer { null; @override - Widget? subtitles(bool showOverlay) => null; + Widget? subtitles(bool showOverlay, {double? menuHeight}) => null; @override Future setVolume(double volume) async {} diff --git a/lib/wrappers/media_control_wrapper.dart b/lib/wrappers/media_control_wrapper.dart index 5ad95c6..37d48db 100644 --- a/lib/wrappers/media_control_wrapper.dart +++ b/lib/wrappers/media_control_wrapper.dart @@ -39,7 +39,8 @@ class MediaControlsWrapper extends BaseAudioHandler { Stream? get stateStream => _player?.stateStream; PlayerState? get lastState => _player?.lastState; - Widget? subtitleWidget(bool showOverlay) => _player?.subtitles(showOverlay); + Widget? subtitleWidget(bool showOverlay, {double? menuHeight}) => + _player?.subtitles(showOverlay, menuHeight: menuHeight); Widget? videoWidget(Key key, BoxFit fit) => _player?.videoWidget(key, fit); final Ref ref; diff --git a/lib/wrappers/players/base_player.dart b/lib/wrappers/players/base_player.dart index 76240e4..f999548 100644 --- a/lib/wrappers/players/base_player.dart +++ b/lib/wrappers/players/base_player.dart @@ -19,8 +19,9 @@ abstract class BasePlayer { BoxFit fit, ); Widget? subtitles( - bool showOverlay, - ); + bool showOverlay, { + double? menuHeight, + }); Future dispose(); Future open(String url, bool play); Future seek(Duration position); diff --git a/lib/wrappers/players/lib_mdk.dart b/lib/wrappers/players/lib_mdk.dart index 2ee7df8..056ab76 100644 --- a/lib/wrappers/players/lib_mdk.dart +++ b/lib/wrappers/players/lib_mdk.dart @@ -191,7 +191,7 @@ class LibMDK extends BasePlayer { ); @override - Widget? subtitles(bool showOverlay) => null; + Widget? subtitles(bool showOverlay, {double? menuHeight}) => null; @override Future setVolume(double volume) async => _controller?.setVolume(volume / 100); diff --git a/lib/wrappers/players/lib_mpv.dart b/lib/wrappers/players/lib_mpv.dart index fa0e590..759af30 100644 --- a/lib/wrappers/players/lib_mpv.dart +++ b/lib/wrappers/players/lib_mpv.dart @@ -167,12 +167,14 @@ class LibMPV extends BasePlayer { @override Widget? subtitles( - bool showOverlay, - ) => + bool showOverlay, { + double? menuHeight, + }) => _controller != null ? _VideoSubtitles( controller: _controller!, showOverlay: showOverlay, + menuHeight: menuHeight, ) : null; @@ -196,10 +198,12 @@ class LibMPV extends BasePlayer { class _VideoSubtitles extends ConsumerStatefulWidget { final VideoController controller; final bool showOverlay; + final double? menuHeight; const _VideoSubtitles({ required this.controller, this.showOverlay = false, + this.menuHeight, }); @override @@ -207,9 +211,9 @@ class _VideoSubtitles extends ConsumerStatefulWidget { } class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { - // Promote constants to static for better readability and flexibility - static const double _menuAreaThreshold = 0.15; // Bottom 15% typically contains controls - static const double _menuAvoidanceOffset = 0.1; // Move up by 10% when needed + // Keep fallback constants for when dynamic height isn't available + static const double _fallbackMenuHeightPercentage = 0.15; // 15% fallback + static const double _subtitlePadding = 0.005; // 0.5% padding above menu static const double _maxSubtitleOffset = 0.85; // Max 85% up from bottom late List subtitle = widget.controller.player.state.subtitle; @@ -233,23 +237,34 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { super.dispose(); } - /// Calculate subtitle offset based on menu visibility + /// Calculate subtitle offset using actual menu height when available double _calculateSubtitleOffset(SubtitleSettingsModel settings) { if (!widget.showOverlay) { return settings.verticalOffset; } - // If subtitles are already positioned above the menu area, leave them alone - if (settings.verticalOffset >= _menuAreaThreshold) { + final screenHeight = MediaQuery.of(context).size.height; + double menuHeightPercentage; + + if (widget.menuHeight != null && screenHeight > 0) { + // Convert menu height to percentage (without extra padding here) + menuHeightPercentage = widget.menuHeight! / screenHeight; + } else { + // Fallback to static percentage + menuHeightPercentage = _fallbackMenuHeightPercentage; + } + + // Calculate the minimum safe position (menu height + small padding) + final minSafeOffset = menuHeightPercentage + _subtitlePadding; + + // If subtitles are already positioned above the safe area, leave them alone + if (settings.verticalOffset >= minSafeOffset) { return settings.verticalOffset; } - // When menu is visible and subtitles are in the menu area, - // move them up slightly to avoid overlap - final adjustedOffset = settings.verticalOffset + _menuAvoidanceOffset; - - // Clamp to reasonable bounds (don't go too high or too low) - return math.max(0.0, math.min(adjustedOffset, _maxSubtitleOffset)); + // Instead of replacing user offset, use the minimum safe position + // This ensures subtitles are just above the menu, not way up high + return math.max(minSafeOffset, math.min(settings.verticalOffset, _maxSubtitleOffset)); } @override From 2884a5fadccfdad45cac4127bba7fcf3619b331f Mon Sep 17 00:00:00 2001 From: Kirill Boychenko Date: Mon, 28 Jul 2025 03:30:26 +0200 Subject: [PATCH 08/11] feat: Implement dynamic subtitle positioning based on menu height --- .../video_player/video_player_controls.dart | 23 +++- lib/util/subtitle_position_calculator.dart | 65 +++++++++++ lib/wrappers/players/lib_mpv.dart | 69 +++++------- .../subtitle_position_calculator_test.dart | 104 ++++++++++++++++++ 4 files changed, 217 insertions(+), 44 deletions(-) create mode 100644 lib/util/subtitle_position_calculator.dart create mode 100644 test/util/subtitle_position_calculator_test.dart diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index ed0593a..ed8f1ef 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -43,6 +43,7 @@ class DesktopControls extends ConsumerStatefulWidget { class _DesktopControlsState extends ConsumerState { // Add GlobalKey to measure bottom controls height final GlobalKey _bottomControlsKey = GlobalKey(); + double? _cachedMenuHeight; late RestartableTimer timer = RestartableTimer( const Duration(seconds: 5), @@ -111,14 +112,32 @@ class _DesktopControlsState extends ConsumerState { timer.reset(); } + // Use PostFrameCallback to measure height after layout + void _measureMenuHeight() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + + final RenderBox? renderBox = _bottomControlsKey.currentContext?.findRenderObject() as RenderBox?; + final newHeight = renderBox?.size.height; + + if (newHeight != _cachedMenuHeight && newHeight != null) { + setState(() { + _cachedMenuHeight = newHeight; + }); + } + }); + } + // Method to get actual menu height double? getBottomControlsHeight() { - final RenderBox? renderBox = _bottomControlsKey.currentContext?.findRenderObject() as RenderBox?; - return renderBox?.size.height; + return _cachedMenuHeight; } @override Widget build(BuildContext context) { + // Trigger measurement after each build to ensure accurate height + _measureMenuHeight(); + final mediaSegments = ref.watch(playBackModel.select((value) => value?.mediaSegments)); final player = ref.watch(videoPlayerProvider); final subtitleWidget = player.subtitleWidget(showOverlay, menuHeight: getBottomControlsHeight()); diff --git a/lib/util/subtitle_position_calculator.dart b/lib/util/subtitle_position_calculator.dart new file mode 100644 index 0000000..f6e054b --- /dev/null +++ b/lib/util/subtitle_position_calculator.dart @@ -0,0 +1,65 @@ +import 'dart:math' as math; +import 'package:fladder/models/settings/subtitle_settings_model.dart'; + +/// Utility class for calculating subtitle positioning based on menu overlay state +/// Provides utilities for calculating the optimal vertical position of subtitles +/// based on user settings and the visibility or size of the player menu overlay. +class SubtitlePositionCalculator { + // Configuration constants + static const double _fallbackMenuHeightPercentage = 0.15; // 15% fallback + static const double _dynamicSubtitlePadding = + -0.03; // -3% padding when we have accurate menu height so the subtitles are closer to the menu + static const double _fallbackSubtitlePadding = 0.01; // 1% padding for conservative fallback positioning + static const double _maxSubtitleOffset = 0.85; // Max 85% up from bottom + + /// Calculate subtitle offset using actual menu height when available + /// + /// Returns the optimal subtitle offset (0.0 to 1.0) where: + /// - 0.0 = bottom of screen + /// - 1.0 = top of screen + /// + /// Parameters: + /// - [settings]: User's subtitle settings containing preferred vertical offset + /// - [showOverlay]: Whether the player menu overlay is currently visible + /// - [screenHeight]: Height of the screen in pixels + /// - [menuHeight]: Optional actual height of the menu in pixels + static double calculateOffset({ + required SubtitleSettingsModel settings, + required bool showOverlay, + required double screenHeight, + double? menuHeight, + }) { + if (!showOverlay) { + return settings.verticalOffset; + } + + double menuHeightPercentage; + double subtitlePadding; + + if (menuHeight != null && screenHeight > 0) { + // Convert menu height from pixels to screen percentage + menuHeightPercentage = menuHeight / screenHeight; + // Use negative padding since we have accurate measurement - can position closer + subtitlePadding = _dynamicSubtitlePadding; + } else { + // Fallback to static percentage when measurement unavailable + menuHeightPercentage = _fallbackMenuHeightPercentage; + // Use positive padding for safety since we're estimating + subtitlePadding = _fallbackSubtitlePadding; + } + + // Calculate the minimum safe position (menu height + appropriate padding) + final minSafeOffset = menuHeightPercentage + subtitlePadding; + + // If subtitles are already positioned above the safe area, leave them alone + // but still apply maximum bounds checking + if (settings.verticalOffset >= minSafeOffset) { + return math.min(settings.verticalOffset, _maxSubtitleOffset); + } + + // Position subtitles just above the menu with bounds checking + // Defensive: math.max(0.0, ...) ensures the offset is never negative, + // which could happen if future changes allow negative menuHeight or padding. + return math.max(0.0, math.min(minSafeOffset, _maxSubtitleOffset)); + } +} diff --git a/lib/wrappers/players/lib_mpv.dart b/lib/wrappers/players/lib_mpv.dart index 759af30..4ed69c9 100644 --- a/lib/wrappers/players/lib_mpv.dart +++ b/lib/wrappers/players/lib_mpv.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -14,6 +13,7 @@ import 'package:fladder/models/playback/playback_model.dart'; import 'package:fladder/models/settings/subtitle_settings_model.dart'; import 'package:fladder/models/settings/video_player_settings.dart'; import 'package:fladder/providers/settings/subtitle_settings_provider.dart'; +import 'package:fladder/util/subtitle_position_calculator.dart'; import 'package:fladder/wrappers/players/base_player.dart'; import 'package:fladder/wrappers/players/player_states.dart'; @@ -207,25 +207,25 @@ class _VideoSubtitles extends ConsumerStatefulWidget { }); @override - ConsumerState createState() => _VideoSubtitlesState(); + _VideoSubtitlesState createState() => _VideoSubtitlesState(); } class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { - // Keep fallback constants for when dynamic height isn't available - static const double _fallbackMenuHeightPercentage = 0.15; // 15% fallback - static const double _subtitlePadding = 0.005; // 0.5% padding above menu - static const double _maxSubtitleOffset = 0.85; // Max 85% up from bottom - - late List subtitle = widget.controller.player.state.subtitle; + late List subtitle; + String _cachedSubtitleText = ''; + List? _lastSubtitleList; StreamSubscription>? subscription; @override void initState() { super.initState(); // Move to very start as per best practices + subtitle = widget.controller.player.state.subtitle; subscription = widget.controller.player.stream.subtitle.listen((value) { if (mounted) { setState(() { subtitle = value; + // Invalidate cache when subtitle changes + _lastSubtitleList = null; }); } }); @@ -237,53 +237,38 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { super.dispose(); } - /// Calculate subtitle offset using actual menu height when available - double _calculateSubtitleOffset(SubtitleSettingsModel settings) { - if (!widget.showOverlay) { - return settings.verticalOffset; - } - - final screenHeight = MediaQuery.of(context).size.height; - double menuHeightPercentage; - - if (widget.menuHeight != null && screenHeight > 0) { - // Convert menu height to percentage (without extra padding here) - menuHeightPercentage = widget.menuHeight! / screenHeight; - } else { - // Fallback to static percentage - menuHeightPercentage = _fallbackMenuHeightPercentage; - } - - // Calculate the minimum safe position (menu height + small padding) - final minSafeOffset = menuHeightPercentage + _subtitlePadding; - - // If subtitles are already positioned above the safe area, leave them alone - if (settings.verticalOffset >= minSafeOffset) { - return settings.verticalOffset; - } - - // Instead of replacing user offset, use the minimum safe position - // This ensures subtitles are just above the menu, not way up high - return math.max(minSafeOffset, math.min(settings.verticalOffset, _maxSubtitleOffset)); - } - @override Widget build(BuildContext context) { final settings = ref.watch(subtitleSettingsProvider); final padding = MediaQuery.of(context).padding; - // Process subtitle text - final text = subtitle.where((line) => line.trim().isNotEmpty).map((line) => line.trim()).join('\n'); + // Cache processed subtitle text to avoid unnecessary computation + if (!const ListEquality().equals(subtitle, _lastSubtitleList)) { + _cachedSubtitleText = subtitle.where((line) => line.trim().isNotEmpty).map((line) => line.trim()).join('\n'); + _lastSubtitleList = List.from(subtitle); + } + final text = _cachedSubtitleText; + + // Extract libass enabled check for clarity + final bool isLibassEnabled = widget.controller.player.platform?.configuration.libass ?? false; // Early return for cases where subtitles shouldn't be rendered - if ((widget.controller.player.platform?.configuration.libass ?? false) || text.isEmpty) { + if (isLibassEnabled || text.isEmpty) { return const SizedBox.shrink(); } + // Use the utility for offset calculation + final offset = SubtitlePositionCalculator.calculateOffset( + settings: settings, + showOverlay: widget.showOverlay, + screenHeight: MediaQuery.of(context).size.height, + menuHeight: widget.menuHeight, + ); + return SubtitleText( subModel: settings, padding: padding, - offset: _calculateSubtitleOffset(settings), + offset: offset, text: text, ); } diff --git a/test/util/subtitle_position_calculator_test.dart b/test/util/subtitle_position_calculator_test.dart new file mode 100644 index 0000000..8197d23 --- /dev/null +++ b/test/util/subtitle_position_calculator_test.dart @@ -0,0 +1,104 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:fladder/models/settings/subtitle_settings_model.dart'; +import 'package:fladder/util/subtitle_position_calculator.dart'; + +void main() { + group('SubtitlePositionCalculator', () { + test('returns original offset when overlay is hidden', () { + const settings = SubtitleSettingsModel(verticalOffset: 0.2); + + final result = SubtitlePositionCalculator.calculateOffset( + settings: settings, + showOverlay: false, + screenHeight: 800, + menuHeight: 120, + ); + + expect(result, equals(0.2)); + }); + + test('uses dynamic menu height when available', () { + const settings = SubtitleSettingsModel(verticalOffset: 0.1); + + final result = SubtitlePositionCalculator.calculateOffset( + settings: settings, + showOverlay: true, + screenHeight: 800, + menuHeight: 120, // 120/800 = 0.15 (15%) + ); + + // Should position at menu height + dynamic padding = (120/800) + (-0.03) = 0.12 + expect(result, closeTo(0.12, 0.001)); + }); + + test('uses fallback when menu height unavailable', () { + const settings = SubtitleSettingsModel(verticalOffset: 0.1); + + final result = SubtitlePositionCalculator.calculateOffset( + settings: settings, + showOverlay: true, + screenHeight: 800, + menuHeight: null, + ); + + // Should use fallback: 0.15 + 0.01 = 0.16 + expect(result, equals(0.16)); + }); + + test('preserves user offset when already above menu', () { + const settings = SubtitleSettingsModel(verticalOffset: 0.3); + + final result = SubtitlePositionCalculator.calculateOffset( + settings: settings, + showOverlay: true, + screenHeight: 800, + menuHeight: 120, + ); + + // Should keep original 0.3 since it's above menu area (0.12) + expect(result, equals(0.3)); + }); + + test('clamps to maximum offset', () { + const settings = SubtitleSettingsModel(verticalOffset: 0.95); + + final result = SubtitlePositionCalculator.calculateOffset( + settings: settings, + showOverlay: true, + screenHeight: 800, + menuHeight: 600, // Large menu that would push subtitles too high + ); + + // Should clamp to max 0.85 + expect(result, equals(0.85)); + }); + + test('handles zero screen height gracefully', () { + const settings = SubtitleSettingsModel(verticalOffset: 0.1); + + final result = SubtitlePositionCalculator.calculateOffset( + settings: settings, + showOverlay: true, + screenHeight: 0, + menuHeight: 120, + ); + + // Should use fallback when screen height is invalid + expect(result, equals(0.16)); + }); + + test('clamps to minimum offset', () { + const settings = SubtitleSettingsModel(verticalOffset: 0.05); + + final result = SubtitlePositionCalculator.calculateOffset( + settings: settings, + showOverlay: true, + screenHeight: 800, + menuHeight: 120, + ); + + // Should not go below 0.0 + expect(result, greaterThanOrEqualTo(0.0)); + }); + }); +} From c446210e6a98b25f4cf8fb14c69253f492086092 Mon Sep 17 00:00:00 2001 From: Kirill Boychenko Date: Mon, 28 Jul 2025 20:23:36 +0200 Subject: [PATCH 09/11] refactor: Improve subtitle positioning logic and remove outdated tests --- .../video_player/video_player_controls.dart | 6 + lib/util/subtitle_position_calculator.dart | 32 +----- lib/wrappers/players/lib_mpv.dart | 10 +- .../subtitle_position_calculator_test.dart | 104 ------------------ 4 files changed, 15 insertions(+), 137 deletions(-) delete mode 100644 test/util/subtitle_position_calculator_test.dart diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index ed8f1ef..592736a 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -112,6 +112,12 @@ class _DesktopControlsState extends ConsumerState { timer.reset(); } + // Height measurement logic remains here for architectural reasons: + // 1. The video controls widget owns and renders the bottom menu UI elements + // 2. Only this widget has direct access to the menu's RenderBox for accurate measurement + // 3. Subtitle widgets are separate components that shouldn't know about control UI structure + // 4. Different players (LibMPV, MDK) can receive the same measurement without duplicating logic + // 5. Clean separation: controls handle UI measurement, players handle subtitle positioning // Use PostFrameCallback to measure height after layout void _measureMenuHeight() { WidgetsBinding.instance.addPostFrameCallback((_) { diff --git a/lib/util/subtitle_position_calculator.dart b/lib/util/subtitle_position_calculator.dart index f6e054b..5cbecba 100644 --- a/lib/util/subtitle_position_calculator.dart +++ b/lib/util/subtitle_position_calculator.dart @@ -1,28 +1,14 @@ import 'dart:math' as math; import 'package:fladder/models/settings/subtitle_settings_model.dart'; -/// Utility class for calculating subtitle positioning based on menu overlay state -/// Provides utilities for calculating the optimal vertical position of subtitles -/// based on user settings and the visibility or size of the player menu overlay. class SubtitlePositionCalculator { - // Configuration constants - static const double _fallbackMenuHeightPercentage = 0.15; // 15% fallback + static const double _fallbackMenuHeightPercentage = 0.15; static const double _dynamicSubtitlePadding = - -0.03; // -3% padding when we have accurate menu height so the subtitles are closer to the menu + 0.00; // Currently unused (0%). Reserved for future implementation of a user-adjustable slider to control subtitle positioning + // relative to the player menu static const double _fallbackSubtitlePadding = 0.01; // 1% padding for conservative fallback positioning - static const double _maxSubtitleOffset = 0.85; // Max 85% up from bottom + static const double _maxSubtitleOffset = 0.85; - /// Calculate subtitle offset using actual menu height when available - /// - /// Returns the optimal subtitle offset (0.0 to 1.0) where: - /// - 0.0 = bottom of screen - /// - 1.0 = top of screen - /// - /// Parameters: - /// - [settings]: User's subtitle settings containing preferred vertical offset - /// - [showOverlay]: Whether the player menu overlay is currently visible - /// - [screenHeight]: Height of the screen in pixels - /// - [menuHeight]: Optional actual height of the menu in pixels static double calculateOffset({ required SubtitleSettingsModel settings, required bool showOverlay, @@ -37,29 +23,19 @@ class SubtitlePositionCalculator { double subtitlePadding; if (menuHeight != null && screenHeight > 0) { - // Convert menu height from pixels to screen percentage menuHeightPercentage = menuHeight / screenHeight; - // Use negative padding since we have accurate measurement - can position closer subtitlePadding = _dynamicSubtitlePadding; } else { - // Fallback to static percentage when measurement unavailable menuHeightPercentage = _fallbackMenuHeightPercentage; - // Use positive padding for safety since we're estimating subtitlePadding = _fallbackSubtitlePadding; } - // Calculate the minimum safe position (menu height + appropriate padding) final minSafeOffset = menuHeightPercentage + subtitlePadding; - // If subtitles are already positioned above the safe area, leave them alone - // but still apply maximum bounds checking if (settings.verticalOffset >= minSafeOffset) { return math.min(settings.verticalOffset, _maxSubtitleOffset); } - // Position subtitles just above the menu with bounds checking - // Defensive: math.max(0.0, ...) ensures the offset is never negative, - // which could happen if future changes allow negative menuHeight or padding. return math.max(0.0, math.min(minSafeOffset, _maxSubtitleOffset)); } } diff --git a/lib/wrappers/players/lib_mpv.dart b/lib/wrappers/players/lib_mpv.dart index 4ed69c9..4d17d8d 100644 --- a/lib/wrappers/players/lib_mpv.dart +++ b/lib/wrappers/players/lib_mpv.dart @@ -168,13 +168,13 @@ class LibMPV extends BasePlayer { @override Widget? subtitles( bool showOverlay, { - double? menuHeight, + double? menuHeight, // Passed from video_player_controls.dart which owns the menu UI }) => _controller != null ? _VideoSubtitles( controller: _controller!, showOverlay: showOverlay, - menuHeight: menuHeight, + menuHeight: menuHeight, // Forward the measured height for accurate positioning ) : null; @@ -198,12 +198,12 @@ class LibMPV extends BasePlayer { class _VideoSubtitles extends ConsumerStatefulWidget { final VideoController controller; final bool showOverlay; - final double? menuHeight; + final double? menuHeight; // Accurate measurement from controls, null triggers fallback positioning const _VideoSubtitles({ required this.controller, this.showOverlay = false, - this.menuHeight, + this.menuHeight, // Receives pre-measured height rather than measuring internally }); @override @@ -257,7 +257,7 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { return const SizedBox.shrink(); } - // Use the utility for offset calculation + // Use the utility for offset calculation with passed menuHeight final offset = SubtitlePositionCalculator.calculateOffset( settings: settings, showOverlay: widget.showOverlay, diff --git a/test/util/subtitle_position_calculator_test.dart b/test/util/subtitle_position_calculator_test.dart deleted file mode 100644 index 8197d23..0000000 --- a/test/util/subtitle_position_calculator_test.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:fladder/models/settings/subtitle_settings_model.dart'; -import 'package:fladder/util/subtitle_position_calculator.dart'; - -void main() { - group('SubtitlePositionCalculator', () { - test('returns original offset when overlay is hidden', () { - const settings = SubtitleSettingsModel(verticalOffset: 0.2); - - final result = SubtitlePositionCalculator.calculateOffset( - settings: settings, - showOverlay: false, - screenHeight: 800, - menuHeight: 120, - ); - - expect(result, equals(0.2)); - }); - - test('uses dynamic menu height when available', () { - const settings = SubtitleSettingsModel(verticalOffset: 0.1); - - final result = SubtitlePositionCalculator.calculateOffset( - settings: settings, - showOverlay: true, - screenHeight: 800, - menuHeight: 120, // 120/800 = 0.15 (15%) - ); - - // Should position at menu height + dynamic padding = (120/800) + (-0.03) = 0.12 - expect(result, closeTo(0.12, 0.001)); - }); - - test('uses fallback when menu height unavailable', () { - const settings = SubtitleSettingsModel(verticalOffset: 0.1); - - final result = SubtitlePositionCalculator.calculateOffset( - settings: settings, - showOverlay: true, - screenHeight: 800, - menuHeight: null, - ); - - // Should use fallback: 0.15 + 0.01 = 0.16 - expect(result, equals(0.16)); - }); - - test('preserves user offset when already above menu', () { - const settings = SubtitleSettingsModel(verticalOffset: 0.3); - - final result = SubtitlePositionCalculator.calculateOffset( - settings: settings, - showOverlay: true, - screenHeight: 800, - menuHeight: 120, - ); - - // Should keep original 0.3 since it's above menu area (0.12) - expect(result, equals(0.3)); - }); - - test('clamps to maximum offset', () { - const settings = SubtitleSettingsModel(verticalOffset: 0.95); - - final result = SubtitlePositionCalculator.calculateOffset( - settings: settings, - showOverlay: true, - screenHeight: 800, - menuHeight: 600, // Large menu that would push subtitles too high - ); - - // Should clamp to max 0.85 - expect(result, equals(0.85)); - }); - - test('handles zero screen height gracefully', () { - const settings = SubtitleSettingsModel(verticalOffset: 0.1); - - final result = SubtitlePositionCalculator.calculateOffset( - settings: settings, - showOverlay: true, - screenHeight: 0, - menuHeight: 120, - ); - - // Should use fallback when screen height is invalid - expect(result, equals(0.16)); - }); - - test('clamps to minimum offset', () { - const settings = SubtitleSettingsModel(verticalOffset: 0.05); - - final result = SubtitlePositionCalculator.calculateOffset( - settings: settings, - showOverlay: true, - screenHeight: 800, - menuHeight: 120, - ); - - // Should not go below 0.0 - expect(result, greaterThanOrEqualTo(0.0)); - }); - }); -} From 5fac088e2d1727cd206e1d9191eff022c933c61c Mon Sep 17 00:00:00 2001 From: PartyDonut Date: Mon, 28 Jul 2025 21:32:37 +0200 Subject: [PATCH 10/11] fix: Move calculation logic to lib_mpv subtitles --- .../settings/subtitle_settings_model.dart | 6 ++- .../video_player/video_player_controls.dart | 34 +------------- lib/stubs/web/lib_mdk_web.dart | 2 +- lib/util/subtitle_position_calculator.dart | 10 +---- lib/wrappers/media_control_wrapper.dart | 4 +- lib/wrappers/players/base_player.dart | 2 +- lib/wrappers/players/lib_mdk.dart | 2 +- lib/wrappers/players/lib_mpv.dart | 45 ++++++++++++------- 8 files changed, 42 insertions(+), 63 deletions(-) diff --git a/lib/models/settings/subtitle_settings_model.dart b/lib/models/settings/subtitle_settings_model.dart index 77e1ebf..2cff918 100644 --- a/lib/models/settings/subtitle_settings_model.dart +++ b/lib/models/settings/subtitle_settings_model.dart @@ -216,7 +216,8 @@ class SubtitleText extends ConsumerWidget { child: Stack( alignment: Alignment.bottomCenter, children: [ - Positioned( + AnimatedPositioned( + duration: const Duration(milliseconds: 125), bottom: position, child: Container( constraints: BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight), @@ -234,7 +235,8 @@ class SubtitleText extends ConsumerWidget { ), ), ), - Positioned( + AnimatedPositioned( + duration: const Duration(milliseconds: 125), bottom: position, child: Container( constraints: BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight), diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 592736a..2741714 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -41,9 +41,7 @@ class DesktopControls extends ConsumerStatefulWidget { } class _DesktopControlsState extends ConsumerState { - // Add GlobalKey to measure bottom controls height final GlobalKey _bottomControlsKey = GlobalKey(); - double? _cachedMenuHeight; late RestartableTimer timer = RestartableTimer( const Duration(seconds: 5), @@ -112,41 +110,11 @@ class _DesktopControlsState extends ConsumerState { timer.reset(); } - // Height measurement logic remains here for architectural reasons: - // 1. The video controls widget owns and renders the bottom menu UI elements - // 2. Only this widget has direct access to the menu's RenderBox for accurate measurement - // 3. Subtitle widgets are separate components that shouldn't know about control UI structure - // 4. Different players (LibMPV, MDK) can receive the same measurement without duplicating logic - // 5. Clean separation: controls handle UI measurement, players handle subtitle positioning - // Use PostFrameCallback to measure height after layout - void _measureMenuHeight() { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) return; - - final RenderBox? renderBox = _bottomControlsKey.currentContext?.findRenderObject() as RenderBox?; - final newHeight = renderBox?.size.height; - - if (newHeight != _cachedMenuHeight && newHeight != null) { - setState(() { - _cachedMenuHeight = newHeight; - }); - } - }); - } - - // Method to get actual menu height - double? getBottomControlsHeight() { - return _cachedMenuHeight; - } - @override Widget build(BuildContext context) { - // Trigger measurement after each build to ensure accurate height - _measureMenuHeight(); - final mediaSegments = ref.watch(playBackModel.select((value) => value?.mediaSegments)); final player = ref.watch(videoPlayerProvider); - final subtitleWidget = player.subtitleWidget(showOverlay, menuHeight: getBottomControlsHeight()); + final subtitleWidget = player.subtitleWidget(showOverlay, controlsKey: _bottomControlsKey); return InputHandler( autoFocus: false, onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored, diff --git a/lib/stubs/web/lib_mdk_web.dart b/lib/stubs/web/lib_mdk_web.dart index abb2e17..a756fbd 100644 --- a/lib/stubs/web/lib_mdk_web.dart +++ b/lib/stubs/web/lib_mdk_web.dart @@ -64,7 +64,7 @@ class LibMDK extends BasePlayer { null; @override - Widget? subtitles(bool showOverlay, {double? menuHeight}) => null; + Widget? subtitles(bool showOverlay, {GlobalKey? menuKey}) => null; @override Future setVolume(double volume) async {} diff --git a/lib/util/subtitle_position_calculator.dart b/lib/util/subtitle_position_calculator.dart index 5cbecba..7617ddf 100644 --- a/lib/util/subtitle_position_calculator.dart +++ b/lib/util/subtitle_position_calculator.dart @@ -1,12 +1,9 @@ import 'dart:math' as math; + import 'package:fladder/models/settings/subtitle_settings_model.dart'; class SubtitlePositionCalculator { static const double _fallbackMenuHeightPercentage = 0.15; - static const double _dynamicSubtitlePadding = - 0.00; // Currently unused (0%). Reserved for future implementation of a user-adjustable slider to control subtitle positioning - // relative to the player menu - static const double _fallbackSubtitlePadding = 0.01; // 1% padding for conservative fallback positioning static const double _maxSubtitleOffset = 0.85; static double calculateOffset({ @@ -20,17 +17,14 @@ class SubtitlePositionCalculator { } double menuHeightPercentage; - double subtitlePadding; if (menuHeight != null && screenHeight > 0) { menuHeightPercentage = menuHeight / screenHeight; - subtitlePadding = _dynamicSubtitlePadding; } else { menuHeightPercentage = _fallbackMenuHeightPercentage; - subtitlePadding = _fallbackSubtitlePadding; } - final minSafeOffset = menuHeightPercentage + subtitlePadding; + final minSafeOffset = menuHeightPercentage; if (settings.verticalOffset >= minSafeOffset) { return math.min(settings.verticalOffset, _maxSubtitleOffset); diff --git a/lib/wrappers/media_control_wrapper.dart b/lib/wrappers/media_control_wrapper.dart index 37d48db..645fb91 100644 --- a/lib/wrappers/media_control_wrapper.dart +++ b/lib/wrappers/media_control_wrapper.dart @@ -39,8 +39,8 @@ class MediaControlsWrapper extends BaseAudioHandler { Stream? get stateStream => _player?.stateStream; PlayerState? get lastState => _player?.lastState; - Widget? subtitleWidget(bool showOverlay, {double? menuHeight}) => - _player?.subtitles(showOverlay, menuHeight: menuHeight); + Widget? subtitleWidget(bool showOverlay, {GlobalKey? controlsKey}) => + _player?.subtitles(showOverlay, menuKey: controlsKey); Widget? videoWidget(Key key, BoxFit fit) => _player?.videoWidget(key, fit); final Ref ref; diff --git a/lib/wrappers/players/base_player.dart b/lib/wrappers/players/base_player.dart index f999548..686ecb5 100644 --- a/lib/wrappers/players/base_player.dart +++ b/lib/wrappers/players/base_player.dart @@ -20,7 +20,7 @@ abstract class BasePlayer { ); Widget? subtitles( bool showOverlay, { - double? menuHeight, + GlobalKey? menuKey, }); Future dispose(); Future open(String url, bool play); diff --git a/lib/wrappers/players/lib_mdk.dart b/lib/wrappers/players/lib_mdk.dart index 056ab76..90d2c76 100644 --- a/lib/wrappers/players/lib_mdk.dart +++ b/lib/wrappers/players/lib_mdk.dart @@ -191,7 +191,7 @@ class LibMDK extends BasePlayer { ); @override - Widget? subtitles(bool showOverlay, {double? menuHeight}) => null; + Widget? subtitles(bool showOverlay, {GlobalKey? menuKey}) => null; @override Future setVolume(double volume) async => _controller?.setVolume(volume / 100); diff --git a/lib/wrappers/players/lib_mpv.dart b/lib/wrappers/players/lib_mpv.dart index 4d17d8d..ebdf233 100644 --- a/lib/wrappers/players/lib_mpv.dart +++ b/lib/wrappers/players/lib_mpv.dart @@ -168,13 +168,13 @@ class LibMPV extends BasePlayer { @override Widget? subtitles( bool showOverlay, { - double? menuHeight, // Passed from video_player_controls.dart which owns the menu UI + GlobalKey? menuKey, }) => _controller != null ? _VideoSubtitles( controller: _controller!, showOverlay: showOverlay, - menuHeight: menuHeight, // Forward the measured height for accurate positioning + menuKey: menuKey, ) : null; @@ -198,12 +198,12 @@ class LibMPV extends BasePlayer { class _VideoSubtitles extends ConsumerStatefulWidget { final VideoController controller; final bool showOverlay; - final double? menuHeight; // Accurate measurement from controls, null triggers fallback positioning + final GlobalKey? menuKey; const _VideoSubtitles({ required this.controller, this.showOverlay = false, - this.menuHeight, // Receives pre-measured height rather than measuring internally + this.menuKey, }); @override @@ -216,15 +216,16 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { List? _lastSubtitleList; StreamSubscription>? subscription; + double? _cachedMenuHeight; + @override void initState() { - super.initState(); // Move to very start as per best practices + super.initState(); subtitle = widget.controller.player.state.subtitle; subscription = widget.controller.player.stream.subtitle.listen((value) { if (mounted) { setState(() { subtitle = value; - // Invalidate cache when subtitle changes _lastSubtitleList = null; }); } @@ -239,30 +240,29 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { @override Widget build(BuildContext context) { - final settings = ref.watch(subtitleSettingsProvider); - final padding = MediaQuery.of(context).padding; + _measureMenuHeight(); + + final settings = ref.watch(subtitleSettingsProvider); + final padding = MediaQuery.paddingOf(context); - // Cache processed subtitle text to avoid unnecessary computation if (!const ListEquality().equals(subtitle, _lastSubtitleList)) { - _cachedSubtitleText = subtitle.where((line) => line.trim().isNotEmpty).map((line) => line.trim()).join('\n'); _lastSubtitleList = List.from(subtitle); + _cachedSubtitleText = subtitle.where((line) => line.trim().isNotEmpty).map((line) => line.trim()).join('\n'); } + final text = _cachedSubtitleText; - // Extract libass enabled check for clarity final bool isLibassEnabled = widget.controller.player.platform?.configuration.libass ?? false; - // Early return for cases where subtitles shouldn't be rendered if (isLibassEnabled || text.isEmpty) { return const SizedBox.shrink(); } - // Use the utility for offset calculation with passed menuHeight final offset = SubtitlePositionCalculator.calculateOffset( settings: settings, showOverlay: widget.showOverlay, - screenHeight: MediaQuery.of(context).size.height, - menuHeight: widget.menuHeight, + screenHeight: MediaQuery.sizeOf(context).height, + menuHeight: _cachedMenuHeight, ); return SubtitleText( @@ -272,4 +272,19 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { text: text, ); } + + void _measureMenuHeight() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || widget.menuKey == null) return; + + final RenderBox? renderBox = widget.menuKey?.currentContext?.findRenderObject() as RenderBox?; + final newHeight = renderBox?.size.height; + + if (newHeight != _cachedMenuHeight && newHeight != null) { + setState(() { + _cachedMenuHeight = newHeight; + }); + } + }); + } } From 0bdf123d15f7ef08b93cee4aaccc42d600b06821 Mon Sep 17 00:00:00 2001 From: PartyDonut Date: Mon, 28 Jul 2025 22:00:21 +0200 Subject: [PATCH 11/11] Renamed variable --- lib/stubs/web/lib_mdk_web.dart | 2 +- lib/wrappers/media_control_wrapper.dart | 2 +- lib/wrappers/players/base_player.dart | 2 +- lib/wrappers/players/lib_mdk.dart | 2 +- lib/wrappers/players/lib_mpv.dart | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/stubs/web/lib_mdk_web.dart b/lib/stubs/web/lib_mdk_web.dart index a756fbd..e2d1143 100644 --- a/lib/stubs/web/lib_mdk_web.dart +++ b/lib/stubs/web/lib_mdk_web.dart @@ -64,7 +64,7 @@ class LibMDK extends BasePlayer { null; @override - Widget? subtitles(bool showOverlay, {GlobalKey? menuKey}) => null; + Widget? subtitles(bool showOverlay, {GlobalKey? controlsKey}) => null; @override Future setVolume(double volume) async {} diff --git a/lib/wrappers/media_control_wrapper.dart b/lib/wrappers/media_control_wrapper.dart index 645fb91..05f068a 100644 --- a/lib/wrappers/media_control_wrapper.dart +++ b/lib/wrappers/media_control_wrapper.dart @@ -40,7 +40,7 @@ class MediaControlsWrapper extends BaseAudioHandler { PlayerState? get lastState => _player?.lastState; Widget? subtitleWidget(bool showOverlay, {GlobalKey? controlsKey}) => - _player?.subtitles(showOverlay, menuKey: controlsKey); + _player?.subtitles(showOverlay, controlsKey: controlsKey); Widget? videoWidget(Key key, BoxFit fit) => _player?.videoWidget(key, fit); final Ref ref; diff --git a/lib/wrappers/players/base_player.dart b/lib/wrappers/players/base_player.dart index 686ecb5..bb975e7 100644 --- a/lib/wrappers/players/base_player.dart +++ b/lib/wrappers/players/base_player.dart @@ -20,7 +20,7 @@ abstract class BasePlayer { ); Widget? subtitles( bool showOverlay, { - GlobalKey? menuKey, + GlobalKey? controlsKey, }); Future dispose(); Future open(String url, bool play); diff --git a/lib/wrappers/players/lib_mdk.dart b/lib/wrappers/players/lib_mdk.dart index 90d2c76..920af32 100644 --- a/lib/wrappers/players/lib_mdk.dart +++ b/lib/wrappers/players/lib_mdk.dart @@ -191,7 +191,7 @@ class LibMDK extends BasePlayer { ); @override - Widget? subtitles(bool showOverlay, {GlobalKey? menuKey}) => null; + Widget? subtitles(bool showOverlay, {GlobalKey? controlsKey}) => null; @override Future setVolume(double volume) async => _controller?.setVolume(volume / 100); diff --git a/lib/wrappers/players/lib_mpv.dart b/lib/wrappers/players/lib_mpv.dart index ebdf233..c18f4e5 100644 --- a/lib/wrappers/players/lib_mpv.dart +++ b/lib/wrappers/players/lib_mpv.dart @@ -168,13 +168,13 @@ class LibMPV extends BasePlayer { @override Widget? subtitles( bool showOverlay, { - GlobalKey? menuKey, + GlobalKey? controlsKey, }) => _controller != null ? _VideoSubtitles( controller: _controller!, showOverlay: showOverlay, - menuKey: menuKey, + controlsKey: controlsKey, ) : null; @@ -198,12 +198,12 @@ class LibMPV extends BasePlayer { class _VideoSubtitles extends ConsumerStatefulWidget { final VideoController controller; final bool showOverlay; - final GlobalKey? menuKey; + final GlobalKey? controlsKey; const _VideoSubtitles({ required this.controller, this.showOverlay = false, - this.menuKey, + this.controlsKey, }); @override @@ -275,9 +275,9 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { void _measureMenuHeight() { WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted || widget.menuKey == null) return; + if (!mounted || widget.controlsKey == null) return; - final RenderBox? renderBox = widget.menuKey?.currentContext?.findRenderObject() as RenderBox?; + final RenderBox? renderBox = widget.controlsKey?.currentContext?.findRenderObject() as RenderBox?; final newHeight = renderBox?.size.height; if (newHeight != _cachedMenuHeight && newHeight != null) {