mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
fix: Adjust subtitle offset to avoid overlap with visible menu
This commit is contained in:
parent
c7afade615
commit
b9f87bbc5e
2 changed files with 192 additions and 150 deletions
|
|
@ -288,149 +288,160 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final GlobalKey _bottomControlsKey = GlobalKey();
|
||||||
|
|
||||||
Widget bottomButtons(BuildContext context) {
|
Widget bottomButtons(BuildContext context) {
|
||||||
return Consumer(builder: (context, ref, child) {
|
return Container(
|
||||||
final mediaPlayback = ref.watch(mediaPlaybackProvider);
|
key: _bottomControlsKey,
|
||||||
final bitRateOptions = ref.watch(playBackModel.select((value) => value?.bitRateOptions));
|
child: Consumer(builder: (context, ref, child) {
|
||||||
return Container(
|
final mediaPlayback = ref.watch(mediaPlaybackProvider);
|
||||||
decoration: BoxDecoration(
|
final bitRateOptions = ref.watch(playBackModel.select((value) => value?.bitRateOptions));
|
||||||
gradient: LinearGradient(
|
return Container(
|
||||||
begin: Alignment.bottomCenter,
|
decoration: BoxDecoration(
|
||||||
end: Alignment.topCenter,
|
gradient: LinearGradient(
|
||||||
colors: [
|
begin: Alignment.bottomCenter,
|
||||||
Colors.black.withValues(alpha: 0.8),
|
end: Alignment.topCenter,
|
||||||
Colors.black.withValues(alpha: 0),
|
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: Padding(
|
||||||
),
|
padding: MediaQuery.paddingOf(context).add(
|
||||||
child: Column(
|
const EdgeInsets.symmetric(horizontal: 16).copyWith(bottom: 12),
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
||||||
child: progressBar(mediaPlayback),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
child: Column(
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Padding(
|
||||||
flex: 2,
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
child: Row(
|
child: progressBar(mediaPlayback),
|
||||||
children: <Widget>[
|
|
||||||
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,
|
const SizedBox(height: 8),
|
||||||
seekBackwardButton(ref),
|
Row(
|
||||||
IconButton.filledTonal(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
iconSize: 38,
|
children: [
|
||||||
onPressed: () {
|
Flexible(
|
||||||
ref.read(videoPlayerProvider).playOrPause();
|
flex: 2,
|
||||||
},
|
child: Row(
|
||||||
icon: Icon(
|
children: <Widget>[
|
||||||
mediaPlayback.playing ? IconsaxPlusBold.pause : IconsaxPlusBold.play,
|
IconButton(
|
||||||
),
|
onPressed: () => showVideoPlayerOptions(context, () => minimizePlayer(context)),
|
||||||
),
|
icon: const Icon(IconsaxPlusLinear.more)),
|
||||||
seekForwardButton(ref),
|
if (AdaptiveLayout.layoutOf(context) == ViewSize.tablet) ...[
|
||||||
nextVideoButton,
|
IconButton(
|
||||||
Flexible(
|
onPressed: () => showSubSelection(context),
|
||||||
flex: 2,
|
icon: const Icon(IconsaxPlusLinear.subtitle),
|
||||||
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();
|
||||||
},
|
},
|
||||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer &&
|
icon: Icon(
|
||||||
AdaptiveLayout.viewSizeOf(context) > ViewSize.phone) ...[
|
mediaPlayback.playing ? IconsaxPlusBold.pause : IconsaxPlusBold.play,
|
||||||
Listener(
|
),
|
||||||
onPointerSignal: (event) {
|
),
|
||||||
if (event is PointerScrollEvent) {
|
seekForwardButton(ref),
|
||||||
if (event.scrollDelta.dy > 0) {
|
nextVideoButton,
|
||||||
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5);
|
Flexible(
|
||||||
} else {
|
flex: 2,
|
||||||
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(5);
|
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(
|
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer &&
|
||||||
onChanged: () => resetTimer(),
|
AdaptiveLayout.viewSizeOf(context) > ViewSize.phone) ...[
|
||||||
),
|
Listener(
|
||||||
),
|
onPointerSignal: (event) {
|
||||||
const FullScreenButton(),
|
if (event is PointerScrollEvent) {
|
||||||
]
|
if (event.scrollDelta.dy > 0) {
|
||||||
].addInBetween(const SizedBox(width: 8)),
|
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) {
|
Widget progressBar(MediaPlaybackModel mediaPlayback) {
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,7 @@ class LibMPV extends BasePlayer {
|
||||||
class _VideoSubtitles extends ConsumerStatefulWidget {
|
class _VideoSubtitles extends ConsumerStatefulWidget {
|
||||||
final VideoController controller;
|
final VideoController controller;
|
||||||
final bool showOverlay;
|
final bool showOverlay;
|
||||||
|
|
||||||
const _VideoSubtitles({
|
const _VideoSubtitles({
|
||||||
required this.controller,
|
required this.controller,
|
||||||
this.showOverlay = false,
|
this.showOverlay = false,
|
||||||
|
|
@ -211,12 +212,14 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
subscription = widget.controller.player.stream.subtitle.listen((value) {
|
|
||||||
setState(() {
|
|
||||||
subtitle = value;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
super.initState();
|
super.initState();
|
||||||
|
subscription = widget.controller.player.stream.subtitle.listen((value) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
subtitle = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -225,24 +228,52 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> {
|
||||||
super.dispose();
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final settings = ref.watch(subtitleSettingsProvider);
|
final settings = ref.watch(subtitleSettingsProvider);
|
||||||
final padding = MediaQuery.of(context).padding;
|
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) {
|
if (widget.controller.player.platform?.configuration.libass ?? false) {
|
||||||
return const IgnorePointer(child: SizedBox.shrink());
|
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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue