fix: Subtitle positioning issue bottom controls display (#420)

This commit is contained in:
PartyDonut 2025-07-29 15:19:41 +02:00 committed by GitHub
commit a983356cd2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 118 additions and 33 deletions

View file

@ -216,7 +216,8 @@ class SubtitleText extends ConsumerWidget {
child: Stack( child: Stack(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
children: [ children: [
Positioned( AnimatedPositioned(
duration: const Duration(milliseconds: 125),
bottom: position, bottom: position,
child: Container( child: Container(
constraints: BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight), 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, bottom: position,
child: Container( child: Container(
constraints: BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight), constraints: BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight),

View file

@ -41,6 +41,8 @@ class DesktopControls extends ConsumerStatefulWidget {
} }
class _DesktopControlsState extends ConsumerState<DesktopControls> { class _DesktopControlsState extends ConsumerState<DesktopControls> {
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,
@ -112,7 +114,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
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, controlsKey: _bottomControlsKey);
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 +295,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, {GlobalKey? controlsKey}) => null;
@override @override
Future<void> setVolume(double volume) async {} Future<void> setVolume(double volume) async {}

View file

@ -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));
}
}

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, {GlobalKey? controlsKey}) =>
_player?.subtitles(showOverlay, controlsKey: controlsKey);
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, {
); GlobalKey? controlsKey,
});
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, {GlobalKey? controlsKey}) => 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

@ -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/subtitle_settings_model.dart';
import 'package:fladder/models/settings/video_player_settings.dart'; import 'package:fladder/models/settings/video_player_settings.dart';
import 'package:fladder/providers/settings/subtitle_settings_provider.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/base_player.dart';
import 'package:fladder/wrappers/players/player_states.dart'; import 'package:fladder/wrappers/players/player_states.dart';
@ -31,7 +32,7 @@ class LibMPV extends BasePlayer {
dispose(); dispose();
mpv.MediaKit.ensureInitialized(); mpv.MediaKit.ensureInitialized();
_player = mpv.Player( _player = mpv.Player(
configuration: mpv.PlayerConfiguration( configuration: mpv.PlayerConfiguration(
title: "nl.jknaapen.fladder", title: "nl.jknaapen.fladder",
@ -166,12 +167,14 @@ class LibMPV extends BasePlayer {
@override @override
Widget? subtitles( Widget? subtitles(
bool showOverlay, bool showOverlay, {
) => GlobalKey? controlsKey,
}) =>
_controller != null _controller != null
? _VideoSubtitles( ? _VideoSubtitles(
controller: _controller!, controller: _controller!,
showOverlay: showOverlay, showOverlay: showOverlay,
controlsKey: controlsKey,
) )
: null; : null;
@ -195,27 +198,38 @@ 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 GlobalKey? controlsKey;
const _VideoSubtitles({ const _VideoSubtitles({
required this.controller, required this.controller,
this.showOverlay = false, this.showOverlay = false,
this.controlsKey,
}); });
@override @override
ConsumerState<ConsumerStatefulWidget> createState() => _VideoSubtitlesState(); _VideoSubtitlesState createState() => _VideoSubtitlesState();
} }
class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> {
late List<String> subtitle = widget.controller.player.state.subtitle; late List<String> subtitle;
String _cachedSubtitleText = '';
List<String>? _lastSubtitleList;
StreamSubscription<List<String>>? subscription; StreamSubscription<List<String>>? subscription;
double? _cachedMenuHeight;
@override @override
void initState() { void initState() {
subscription = widget.controller.player.stream.subtitle.listen((value) {
setState(() {
subtitle = value;
});
});
super.initState(); super.initState();
subtitle = widget.controller.player.state.subtitle;
subscription = widget.controller.player.stream.subtitle.listen((value) {
if (mounted) {
setState(() {
subtitle = value;
_lastSubtitleList = null;
});
}
});
} }
@override @override
@ -226,22 +240,51 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final settings = ref.watch(subtitleSettingsProvider); _measureMenuHeight();
final padding = MediaQuery.of(context).padding;
final text = [
for (final line in subtitle)
if (line.trim().isNotEmpty) line.trim(),
].join('\n');
if (widget.controller.player.platform?.configuration.libass ?? false) { final settings = ref.watch(subtitleSettingsProvider);
return const IgnorePointer(child: SizedBox.shrink()); final padding = MediaQuery.paddingOf(context);
} else {
return SubtitleText( if (!const ListEquality().equals(subtitle, _lastSubtitleList)) {
subModel: settings, _lastSubtitleList = List<String>.from(subtitle);
padding: padding, _cachedSubtitleText = subtitle.where((line) => line.trim().isNotEmpty).map((line) => line.trim()).join('\n');
offset: (widget.showOverlay ? 0.5 : settings.verticalOffset),
text: text,
);
} }
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;
});
}
});
} }
} }