From 480766f75ffd7b10b35e6e22176e4538279b0991 Mon Sep 17 00:00:00 2001 From: Kirill Boychenko Date: Mon, 28 Jul 2025 00:50:16 +0200 Subject: [PATCH] refactor: Promote constants for subtitle positioning to improve readability and flexibility --- .../video_player/video_player_controls.dart | 279 +++++++++--------- lib/wrappers/players/lib_mpv.dart | 21 +- 2 files changed, 145 insertions(+), 155 deletions(-) diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 9f2b2df..147dea8 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -288,160 +288,149 @@ class _DesktopControlsState extends ConsumerState { ); } - final GlobalKey _bottomControlsKey = GlobalKey(); - Widget bottomButtons(BuildContext context) { - return Container( - key: _bottomControlsKey, - child: Consumer(builder: (context, ref, child) { - final mediaPlayback = ref.watch(mediaPlaybackProvider); - final bitRateOptions = ref.watch(playBackModel.select((value) => value?.bitRateOptions)); - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - 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), + return Container(child: Consumer(builder: (context, ref, child) { + final mediaPlayback = ref.watch(mediaPlaybackProvider); + final bitRateOptions = ref.watch(playBackModel.select((value) => value?.bitRateOptions)); + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + 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: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: progressBar(mediaPlayback), ), - child: Column( + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: progressBar(mediaPlayback), + Flexible( + flex: 2, + child: Row( + children: [ + 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), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - flex: 2, - child: Row( - children: [ - 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), + previousButton, + seekBackwardButton(ref), + IconButton.filledTonal( + iconSize: 38, + onPressed: () { + ref.read(videoPlayerProvider).playOrPause(); + }, + icon: Icon( + mediaPlayback.playing ? IconsaxPlusBold.pause : IconsaxPlusBold.play, + ), + ), + seekForwardButton(ref), + nextVideoButton, + Flexible( + 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( - mediaPlayback.playing ? IconsaxPlusBold.pause : IconsaxPlusBold.play, - ), - ), - seekForwardButton(ref), - nextVideoButton, - Flexible( - 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), - ), - ), + if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer && + AdaptiveLayout.viewSizeOf(context) > ViewSize.phone) ...[ + Listener( + onPointerSignal: (event) { + if (event is PointerScrollEvent) { + if (event.scrollDelta.dy > 0) { + ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5); + } else { + ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(5); + } + } }, - if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer && - AdaptiveLayout.viewSizeOf(context) > ViewSize.phone) ...[ - Listener( - onPointerSignal: (event) { - if (event is PointerScrollEvent) { - if (event.scrollDelta.dy > 0) { - 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)), + child: VideoVolumeSlider( + onChanged: () => resetTimer(), + ), + ), + const FullScreenButton(), + ] + ].addInBetween(const SizedBox(width: 8)), + ), ), - ], + ].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) { diff --git a/lib/wrappers/players/lib_mpv.dart b/lib/wrappers/players/lib_mpv.dart index 8235988..f306359 100644 --- a/lib/wrappers/players/lib_mpv.dart +++ b/lib/wrappers/players/lib_mpv.dart @@ -207,12 +207,17 @@ class _VideoSubtitles extends ConsumerStatefulWidget { } 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 subtitle = widget.controller.player.state.subtitle; StreamSubscription>? subscription; @override void initState() { - super.initState(); + super.initState(); // Move to very start as per best practices subscription = widget.controller.player.stream.subtitle.listen((value) { if (mounted) { setState(() { @@ -234,21 +239,17 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { 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) { + 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; + 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 + return math.max(0.0, math.min(adjustedOffset, _maxSubtitleOffset)); } @override @@ -261,12 +262,12 @@ class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> { // Return empty widget if libass is enabled (native subtitle rendering) 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 if (text.isEmpty) { - return const IgnorePointer(child: SizedBox.shrink()); + return const SizedBox.shrink(); } return SubtitleText(