refactor: Promote constants for subtitle positioning to improve readability and flexibility

This commit is contained in:
Kirill Boychenko 2025-07-28 00:50:16 +02:00
parent b9f87bbc5e
commit 480766f75f
2 changed files with 145 additions and 155 deletions

View file

@ -288,160 +288,149 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
); );
} }
final GlobalKey _bottomControlsKey = GlobalKey();
Widget bottomButtons(BuildContext context) { Widget bottomButtons(BuildContext context) {
return Container( return Container(child: Consumer(builder: (context, ref, child) {
key: _bottomControlsKey, final mediaPlayback = ref.watch(mediaPlaybackProvider);
child: Consumer(builder: (context, ref, child) { final bitRateOptions = ref.watch(playBackModel.select((value) => value?.bitRateOptions));
final mediaPlayback = ref.watch(mediaPlaybackProvider); return Container(
final bitRateOptions = ref.watch(playBackModel.select((value) => value?.bitRateOptions)); decoration: BoxDecoration(
return Container( gradient: LinearGradient(
decoration: BoxDecoration( begin: Alignment.bottomCenter,
gradient: LinearGradient( end: Alignment.topCenter,
begin: Alignment.bottomCenter, colors: [
end: Alignment.topCenter, Colors.black.withValues(alpha: 0.8),
colors: [ Colors.black.withValues(alpha: 0),
Colors.black.withValues(alpha: 0.8), ],
Colors.black.withValues(alpha: 0), )),
], child: Padding(
)), padding: MediaQuery.paddingOf(context).add(
child: Padding( const EdgeInsets.symmetric(horizontal: 16).copyWith(bottom: 12),
padding: MediaQuery.paddingOf(context).add( ),
const EdgeInsets.symmetric(horizontal: 16).copyWith(bottom: 12), child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: progressBar(mediaPlayback),
), ),
child: Column( const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Padding( Flexible(
padding: const EdgeInsets.symmetric(horizontal: 12), flex: 2,
child: progressBar(mediaPlayback), child: Row(
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,
)),
),
), ),
const SizedBox(height: 8), previousButton,
Row( seekBackwardButton(ref),
mainAxisAlignment: MainAxisAlignment.center, IconButton.filledTonal(
children: [ iconSize: 38,
Flexible( onPressed: () {
flex: 2, ref.read(videoPlayerProvider).playOrPause();
child: Row( },
children: <Widget>[ icon: Icon(
IconButton( mediaPlayback.playing ? IconsaxPlusBold.pause : IconsaxPlusBold.play,
onPressed: () => showVideoPlayerOptions(context, () => minimizePlayer(context)), ),
icon: const Icon(IconsaxPlusLinear.more)), ),
if (AdaptiveLayout.layoutOf(context) == ViewSize.tablet) ...[ seekForwardButton(ref),
IconButton( nextVideoButton,
onPressed: () => showSubSelection(context), Flexible(
icon: const Icon(IconsaxPlusLinear.subtitle), flex: 2,
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();
}, },
icon: Icon( if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer &&
mediaPlayback.playing ? IconsaxPlusBold.pause : IconsaxPlusBold.play, AdaptiveLayout.viewSizeOf(context) > ViewSize.phone) ...[
), Listener(
), onPointerSignal: (event) {
seekForwardButton(ref), if (event is PointerScrollEvent) {
nextVideoButton, if (event.scrollDelta.dy > 0) {
Flexible( ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5);
flex: 2, } else {
child: Row( ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(5);
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),
),
),
}, },
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer && child: VideoVolumeSlider(
AdaptiveLayout.viewSizeOf(context) > ViewSize.phone) ...[ onChanged: () => resetTimer(),
Listener( ),
onPointerSignal: (event) { ),
if (event is PointerScrollEvent) { const FullScreenButton(),
if (event.scrollDelta.dy > 0) { ]
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5); ].addInBetween(const SizedBox(width: 8)),
} 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) {

View file

@ -207,12 +207,17 @@ class _VideoSubtitles extends ConsumerStatefulWidget {
} }
class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> {
// Promote constants to static for better readability and flexibility
static const double _menuAreaThreshold = 0.15; // Bottom 15% typically contains controls
static const double _menuAvoidanceOffset = 0.1; // Move up by 10% when needed
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;
StreamSubscription<List<String>>? subscription; StreamSubscription<List<String>>? subscription;
@override @override
void initState() { void initState() {
super.initState(); super.initState(); // Move to very start as per best practices
subscription = widget.controller.player.stream.subtitle.listen((value) { subscription = widget.controller.player.stream.subtitle.listen((value) {
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -234,21 +239,17 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> {
return settings.verticalOffset; 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 subtitles are already positioned above the menu area, leave them alone
if (settings.verticalOffset >= menuAreaThreshold) { if (settings.verticalOffset >= _menuAreaThreshold) {
return settings.verticalOffset; return settings.verticalOffset;
} }
// When menu is visible and subtitles are in the menu area, // When menu is visible and subtitles are in the menu area,
// move them up slightly to avoid overlap // move them up slightly to avoid overlap
const menuAvoidanceOffset = 0.1; final adjustedOffset = settings.verticalOffset + _menuAvoidanceOffset;
final adjustedOffset = settings.verticalOffset + menuAvoidanceOffset;
// Clamp to reasonable bounds (don't go too high or too low) // Clamp to reasonable bounds (don't go too high or too low)
return math.min(adjustedOffset, 0.85); // Max 85% up from bottom return math.max(0.0, math.min(adjustedOffset, _maxSubtitleOffset));
} }
@override @override
@ -261,12 +262,12 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> {
// Return empty widget if libass is enabled (native subtitle rendering) // 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 SizedBox.shrink();
} }
// Return empty widget if no subtitle text // Return empty widget if no subtitle text
if (text.isEmpty) { if (text.isEmpty) {
return const IgnorePointer(child: SizedBox.shrink()); return const SizedBox.shrink();
} }
return SubtitleText( return SubtitleText(