mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-09 07:28:14 -07:00
fix: Subtitle positioning issue bottom controls display (#420)
This commit is contained in:
commit
a983356cd2
8 changed files with 118 additions and 33 deletions
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
35
lib/util/subtitle_position_calculator.dart
Normal file
35
lib/util/subtitle_position_calculator.dart
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue