feat: Enhance subtitle handling with dynamic menu height adjustment

This commit is contained in:
Kirill Boychenko 2025-07-28 02:13:00 +02:00
parent d60522b021
commit 1fdab92f1f
6 changed files with 47 additions and 20 deletions

View file

@ -41,6 +41,9 @@ class DesktopControls extends ConsumerStatefulWidget {
} }
class _DesktopControlsState extends ConsumerState<DesktopControls> { class _DesktopControlsState extends ConsumerState<DesktopControls> {
// Add GlobalKey to measure bottom controls height
final GlobalKey _bottomControlsKey = GlobalKey();
late RestartableTimer timer = RestartableTimer( late RestartableTimer timer = RestartableTimer(
const Duration(seconds: 5), const Duration(seconds: 5),
() => mounted ? toggleOverlay(value: false) : null, () => mounted ? toggleOverlay(value: false) : null,
@ -108,11 +111,17 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
timer.reset(); timer.reset();
} }
// Method to get actual menu height
double? getBottomControlsHeight() {
final RenderBox? renderBox = _bottomControlsKey.currentContext?.findRenderObject() as RenderBox?;
return renderBox?.size.height;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final mediaSegments = ref.watch(playBackModel.select((value) => value?.mediaSegments)); final mediaSegments = ref.watch(playBackModel.select((value) => value?.mediaSegments));
final player = ref.watch(videoPlayerProvider); final player = ref.watch(videoPlayerProvider);
final subtitleWidget = player.subtitleWidget(showOverlay); final subtitleWidget = player.subtitleWidget(showOverlay, menuHeight: getBottomControlsHeight());
return InputHandler( return InputHandler(
autoFocus: false, autoFocus: false,
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored, onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
@ -293,6 +302,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
final mediaPlayback = ref.watch(mediaPlaybackProvider); final mediaPlayback = ref.watch(mediaPlaybackProvider);
final bitRateOptions = ref.watch(playBackModel.select((value) => value?.bitRateOptions)); final bitRateOptions = ref.watch(playBackModel.select((value) => value?.bitRateOptions));
return Container( return Container(
key: _bottomControlsKey, // Add key to measure height
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.bottomCenter, begin: Alignment.bottomCenter,

View file

@ -64,7 +64,7 @@ class LibMDK extends BasePlayer {
null; null;
@override @override
Widget? subtitles(bool showOverlay) => null; Widget? subtitles(bool showOverlay, {double? menuHeight}) => null;
@override @override
Future<void> setVolume(double volume) async {} Future<void> setVolume(double volume) async {}

View file

@ -39,7 +39,8 @@ class MediaControlsWrapper extends BaseAudioHandler {
Stream<PlayerState>? get stateStream => _player?.stateStream; Stream<PlayerState>? get stateStream => _player?.stateStream;
PlayerState? get lastState => _player?.lastState; 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); Widget? videoWidget(Key key, BoxFit fit) => _player?.videoWidget(key, fit);
final Ref ref; final Ref ref;

View file

@ -19,8 +19,9 @@ abstract class BasePlayer {
BoxFit fit, BoxFit fit,
); );
Widget? subtitles( Widget? subtitles(
bool showOverlay, bool showOverlay, {
); double? menuHeight,
});
Future<void> dispose(); Future<void> dispose();
Future<void> open(String url, bool play); Future<void> open(String url, bool play);
Future<void> seek(Duration position); Future<void> seek(Duration position);

View file

@ -191,7 +191,7 @@ class LibMDK extends BasePlayer {
); );
@override @override
Widget? subtitles(bool showOverlay) => null; Widget? subtitles(bool showOverlay, {double? menuHeight}) => null;
@override @override
Future<void> setVolume(double volume) async => _controller?.setVolume(volume / 100); Future<void> setVolume(double volume) async => _controller?.setVolume(volume / 100);

View file

@ -167,12 +167,14 @@ class LibMPV extends BasePlayer {
@override @override
Widget? subtitles( Widget? subtitles(
bool showOverlay, bool showOverlay, {
) => double? menuHeight,
}) =>
_controller != null _controller != null
? _VideoSubtitles( ? _VideoSubtitles(
controller: _controller!, controller: _controller!,
showOverlay: showOverlay, showOverlay: showOverlay,
menuHeight: menuHeight,
) )
: null; : null;
@ -196,10 +198,12 @@ class LibMPV extends BasePlayer {
class _VideoSubtitles extends ConsumerStatefulWidget { class _VideoSubtitles extends ConsumerStatefulWidget {
final VideoController controller; final VideoController controller;
final bool showOverlay; final bool showOverlay;
final double? menuHeight;
const _VideoSubtitles({ const _VideoSubtitles({
required this.controller, required this.controller,
this.showOverlay = false, this.showOverlay = false,
this.menuHeight,
}); });
@override @override
@ -207,9 +211,9 @@ class _VideoSubtitles extends ConsumerStatefulWidget {
} }
class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> {
// Promote constants to static for better readability and flexibility // Keep fallback constants for when dynamic height isn't available
static const double _menuAreaThreshold = 0.15; // Bottom 15% typically contains controls static const double _fallbackMenuHeightPercentage = 0.15; // 15% fallback
static const double _menuAvoidanceOffset = 0.1; // Move up by 10% when needed static const double _subtitlePadding = 0.005; // 0.5% padding above menu
static const double _maxSubtitleOffset = 0.85; // Max 85% up from bottom static const double _maxSubtitleOffset = 0.85; // Max 85% up from bottom
late List<String> subtitle = widget.controller.player.state.subtitle; late List<String> subtitle = widget.controller.player.state.subtitle;
@ -233,23 +237,34 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> {
super.dispose(); super.dispose();
} }
/// Calculate subtitle offset based on menu visibility /// Calculate subtitle offset using actual menu height when available
double _calculateSubtitleOffset(SubtitleSettingsModel settings) { double _calculateSubtitleOffset(SubtitleSettingsModel settings) {
if (!widget.showOverlay) { if (!widget.showOverlay) {
return settings.verticalOffset; return settings.verticalOffset;
} }
// If subtitles are already positioned above the menu area, leave them alone final screenHeight = MediaQuery.of(context).size.height;
if (settings.verticalOffset >= _menuAreaThreshold) { 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; return settings.verticalOffset;
} }
// When menu is visible and subtitles are in the menu area, // Instead of replacing user offset, use the minimum safe position
// move them up slightly to avoid overlap // This ensures subtitles are just above the menu, not way up high
final adjustedOffset = settings.verticalOffset + _menuAvoidanceOffset; return math.max(minSafeOffset, math.min(settings.verticalOffset, _maxSubtitleOffset));
// Clamp to reasonable bounds (don't go too high or too low)
return math.max(0.0, math.min(adjustedOffset, _maxSubtitleOffset));
} }
@override @override