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 4216b0f..2741714 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -41,6 +41,8 @@ class DesktopControls extends ConsumerStatefulWidget { } class _DesktopControlsState extends ConsumerState { + final GlobalKey _bottomControlsKey = GlobalKey(); + late RestartableTimer timer = RestartableTimer( const Duration(seconds: 5), () => mounted ? toggleOverlay(value: false) : null, @@ -112,7 +114,7 @@ class _DesktopControlsState extends ConsumerState { 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, controlsKey: _bottomControlsKey); return InputHandler( autoFocus: false, onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored, @@ -293,6 +295,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..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) => null; + Widget? subtitles(bool showOverlay, {GlobalKey? controlsKey}) => null; @override Future setVolume(double volume) async {} diff --git a/lib/util/subtitle_position_calculator.dart b/lib/util/subtitle_position_calculator.dart new file mode 100644 index 0000000..7617ddf --- /dev/null +++ b/lib/util/subtitle_position_calculator.dart @@ -0,0 +1,35 @@ +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 _maxSubtitleOffset = 0.85; + + static double calculateOffset({ + required SubtitleSettingsModel settings, + required bool showOverlay, + required double screenHeight, + double? menuHeight, + }) { + if (!showOverlay) { + return settings.verticalOffset; + } + + double menuHeightPercentage; + + if (menuHeight != null && screenHeight > 0) { + menuHeightPercentage = menuHeight / screenHeight; + } else { + menuHeightPercentage = _fallbackMenuHeightPercentage; + } + + final minSafeOffset = menuHeightPercentage; + + if (settings.verticalOffset >= minSafeOffset) { + return math.min(settings.verticalOffset, _maxSubtitleOffset); + } + + return math.max(0.0, math.min(minSafeOffset, _maxSubtitleOffset)); + } +} diff --git a/lib/wrappers/media_control_wrapper.dart b/lib/wrappers/media_control_wrapper.dart index 5ad95c6..05f068a 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, {GlobalKey? 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 76240e4..bb975e7 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, { + GlobalKey? controlsKey, + }); 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..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) => 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 a0ac078..c18f4e5 100644 --- a/lib/wrappers/players/lib_mpv.dart +++ b/lib/wrappers/players/lib_mpv.dart @@ -13,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'; @@ -31,7 +32,7 @@ class LibMPV extends BasePlayer { dispose(); mpv.MediaKit.ensureInitialized(); - + _player = mpv.Player( configuration: mpv.PlayerConfiguration( title: "nl.jknaapen.fladder", @@ -166,12 +167,14 @@ class LibMPV extends BasePlayer { @override Widget? subtitles( - bool showOverlay, - ) => + bool showOverlay, { + GlobalKey? controlsKey, + }) => _controller != null ? _VideoSubtitles( controller: _controller!, showOverlay: showOverlay, + controlsKey: controlsKey, ) : null; @@ -195,27 +198,38 @@ class LibMPV extends BasePlayer { class _VideoSubtitles extends ConsumerStatefulWidget { final VideoController controller; final bool showOverlay; + final GlobalKey? controlsKey; + const _VideoSubtitles({ required this.controller, this.showOverlay = false, + this.controlsKey, }); @override - ConsumerState createState() => _VideoSubtitlesState(); + _VideoSubtitlesState createState() => _VideoSubtitlesState(); } class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { - late List subtitle = widget.controller.player.state.subtitle; + late List subtitle; + String _cachedSubtitleText = ''; + List? _lastSubtitleList; StreamSubscription>? subscription; + double? _cachedMenuHeight; + @override void initState() { - subscription = widget.controller.player.stream.subtitle.listen((value) { - setState(() { - subtitle = value; - }); - }); super.initState(); + subtitle = widget.controller.player.state.subtitle; + subscription = widget.controller.player.stream.subtitle.listen((value) { + if (mounted) { + setState(() { + subtitle = value; + _lastSubtitleList = null; + }); + } + }); } @override @@ -226,22 +240,51 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { @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'); + _measureMenuHeight(); - if (widget.controller.player.platform?.configuration.libass ?? false) { - return const IgnorePointer(child: SizedBox.shrink()); - } else { - return SubtitleText( - subModel: settings, - padding: padding, - offset: (widget.showOverlay ? 0.5 : settings.verticalOffset), - text: text, - ); + final settings = ref.watch(subtitleSettingsProvider); + final padding = MediaQuery.paddingOf(context); + + if (!const ListEquality().equals(subtitle, _lastSubtitleList)) { + _lastSubtitleList = List.from(subtitle); + _cachedSubtitleText = subtitle.where((line) => line.trim().isNotEmpty).map((line) => line.trim()).join('\n'); } + + final text = _cachedSubtitleText; + + final bool isLibassEnabled = widget.controller.player.platform?.configuration.libass ?? false; + + if (isLibassEnabled || text.isEmpty) { + return const SizedBox.shrink(); + } + + final offset = SubtitlePositionCalculator.calculateOffset( + settings: settings, + showOverlay: widget.showOverlay, + screenHeight: MediaQuery.sizeOf(context).height, + menuHeight: _cachedMenuHeight, + ); + + return SubtitleText( + subModel: settings, + padding: padding, + offset: offset, + text: text, + ); + } + + void _measureMenuHeight() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || widget.controlsKey == null) return; + + final RenderBox? renderBox = widget.controlsKey?.currentContext?.findRenderObject() as RenderBox?; + final newHeight = renderBox?.size.height; + + if (newHeight != _cachedMenuHeight && newHeight != null) { + setState(() { + _cachedMenuHeight = newHeight; + }); + } + }); } }