mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-13 17:30:31 -07:00
feat: Add volume indicator and scroll handles on entire screen (#443)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
715e707bb6
commit
d0d6a2ffa6
5 changed files with 191 additions and 95 deletions
|
|
@ -1317,5 +1317,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"blurred": "Blurred"
|
"blurred": "Blurred",
|
||||||
|
"volumeIndicator": "Volume: {volume}",
|
||||||
|
"@volumeIndicator": {
|
||||||
|
"placeholders": {
|
||||||
|
"volume": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:async/async.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||||
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
|
|
||||||
|
class VideoPlayerVolumeIndicator extends ConsumerStatefulWidget {
|
||||||
|
const VideoPlayerVolumeIndicator({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ConsumerStatefulWidget> createState() => _VideoPlayerVolumeIndicatorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VideoPlayerVolumeIndicatorState extends ConsumerState<VideoPlayerVolumeIndicator> {
|
||||||
|
late double currentVolume = ref.read(videoPlayerSettingsProvider.select((value) => value.volume));
|
||||||
|
|
||||||
|
bool showIndicator = false;
|
||||||
|
late final timer = RestartableTimer(const Duration(seconds: 1), () {
|
||||||
|
setState(() {
|
||||||
|
showIndicator = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
showIndicator = false;
|
||||||
|
timer.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
ref.listen(
|
||||||
|
videoPlayerSettingsProvider.select((value) => value.volume),
|
||||||
|
(previous, next) {
|
||||||
|
setState(() {
|
||||||
|
showIndicator = true;
|
||||||
|
currentVolume = next;
|
||||||
|
});
|
||||||
|
timer.reset();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return IgnorePointer(
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
opacity: showIndicator ? 1 : 0,
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withValues(alpha: 0.85),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
volumeIcon(currentVolume),
|
||||||
|
),
|
||||||
|
Text(context.localized.volumeIndicator(currentVolume.round()))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData volumeIcon(double value) {
|
||||||
|
if (value <= 0) {
|
||||||
|
return IconsaxPlusLinear.volume_mute;
|
||||||
|
}
|
||||||
|
if (value < 50) {
|
||||||
|
return IconsaxPlusLinear.volume_low_1;
|
||||||
|
}
|
||||||
|
return IconsaxPlusLinear.volume_high;
|
||||||
|
}
|
||||||
|
|
@ -80,7 +80,7 @@ IconData volumeIcon(double value) {
|
||||||
return IconsaxPlusLinear.volume_mute;
|
return IconsaxPlusLinear.volume_mute;
|
||||||
}
|
}
|
||||||
if (value < 50) {
|
if (value < 50) {
|
||||||
return IconsaxPlusLinear.volume_low;
|
return IconsaxPlusLinear.volume_low_1;
|
||||||
}
|
}
|
||||||
return IconsaxPlusLinear.volume_high;
|
return IconsaxPlusLinear.volume_high;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import 'package:fladder/screens/video_player/components/video_player_controls_ex
|
||||||
import 'package:fladder/screens/video_player/components/video_player_options_sheet.dart';
|
import 'package:fladder/screens/video_player/components/video_player_options_sheet.dart';
|
||||||
import 'package:fladder/screens/video_player/components/video_player_quality_controls.dart';
|
import 'package:fladder/screens/video_player/components/video_player_quality_controls.dart';
|
||||||
import 'package:fladder/screens/video_player/components/video_player_seek_indicator.dart';
|
import 'package:fladder/screens/video_player/components/video_player_seek_indicator.dart';
|
||||||
|
import 'package:fladder/screens/video_player/components/video_player_volume_indicator.dart';
|
||||||
import 'package:fladder/screens/video_player/components/video_progress_bar.dart';
|
import 'package:fladder/screens/video_player/components/video_progress_bar.dart';
|
||||||
import 'package:fladder/screens/video_player/components/video_volume_slider.dart';
|
import 'package:fladder/screens/video_player/components/video_volume_slider.dart';
|
||||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||||
|
|
@ -71,89 +72,93 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
||||||
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, controlsKey: _bottomControlsKey);
|
final subtitleWidget = player.subtitleWidget(showOverlay, controlsKey: _bottomControlsKey);
|
||||||
return InputHandler(
|
return Listener(
|
||||||
autoFocus: true,
|
onPointerSignal: setVolume,
|
||||||
keyMap: ref.watch(videoPlayerSettingsProvider.select((value) => value.currentShortcuts)),
|
child: InputHandler(
|
||||||
keyMapResult: (result) => _onKey(result),
|
autoFocus: true,
|
||||||
child: PopScope(
|
keyMap: ref.watch(videoPlayerSettingsProvider.select((value) => value.currentShortcuts)),
|
||||||
canPop: false,
|
keyMapResult: (result) => _onKey(result),
|
||||||
onPopInvokedWithResult: (didPop, result) {
|
child: PopScope(
|
||||||
if (!didPop) {
|
canPop: false,
|
||||||
closePlayer();
|
onPopInvokedWithResult: (didPop, result) {
|
||||||
}
|
if (!didPop) {
|
||||||
},
|
closePlayer();
|
||||||
child: MouseRegion(
|
}
|
||||||
cursor: showOverlay ? SystemMouseCursors.basic : SystemMouseCursors.none,
|
},
|
||||||
onExit: (event) => toggleOverlay(value: false),
|
child: MouseRegion(
|
||||||
onEnter: (event) => toggleOverlay(value: true),
|
cursor: showOverlay ? SystemMouseCursors.basic : SystemMouseCursors.none,
|
||||||
onHover: AdaptiveLayout.of(context).isDesktop || kIsWeb ? (event) => toggleOverlay(value: true) : null,
|
onExit: (event) => toggleOverlay(value: false),
|
||||||
child: Stack(
|
onEnter: (event) => toggleOverlay(value: true),
|
||||||
children: [
|
onHover: AdaptiveLayout.of(context).isDesktop || kIsWeb ? (event) => toggleOverlay(value: true) : null,
|
||||||
Positioned.fill(
|
child: Stack(
|
||||||
child: GestureDetector(
|
children: [
|
||||||
onTap: AdaptiveLayout.of(context).inputDevice == InputDevice.pointer
|
Positioned.fill(
|
||||||
? () => player.playOrPause()
|
child: GestureDetector(
|
||||||
: () => toggleOverlay(),
|
onTap: AdaptiveLayout.of(context).inputDevice == InputDevice.pointer
|
||||||
onDoubleTap: AdaptiveLayout.of(context).inputDevice == InputDevice.pointer
|
? () => player.playOrPause()
|
||||||
? () => fullScreenHelper.toggleFullScreen(ref)
|
: () => toggleOverlay(),
|
||||||
: null,
|
onDoubleTap: AdaptiveLayout.of(context).inputDevice == InputDevice.pointer
|
||||||
),
|
? () => fullScreenHelper.toggleFullScreen(ref)
|
||||||
),
|
: null,
|
||||||
if (subtitleWidget != null) subtitleWidget,
|
|
||||||
if (AdaptiveLayout.of(context).isDesktop)
|
|
||||||
Consumer(builder: (context, ref, child) {
|
|
||||||
final playing = ref.watch(mediaPlaybackProvider.select((value) => value.playing));
|
|
||||||
final buffering = ref.watch(mediaPlaybackProvider.select((value) => value.buffering));
|
|
||||||
return playButton(playing, buffering);
|
|
||||||
}),
|
|
||||||
IgnorePointer(
|
|
||||||
ignoring: !showOverlay,
|
|
||||||
child: AnimatedOpacity(
|
|
||||||
duration: fadeDuration,
|
|
||||||
opacity: showOverlay ? 1 : 0,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
topButtons(context),
|
|
||||||
const Spacer(),
|
|
||||||
bottomButtons(context),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (subtitleWidget != null) subtitleWidget,
|
||||||
const VideoPlayerSeekIndicator(),
|
if (AdaptiveLayout.of(context).isDesktop)
|
||||||
Consumer(
|
Consumer(builder: (context, ref, child) {
|
||||||
builder: (context, ref, child) {
|
final playing = ref.watch(mediaPlaybackProvider.select((value) => value.playing));
|
||||||
final position = ref.watch(mediaPlaybackProvider.select((value) => value.position));
|
final buffering = ref.watch(mediaPlaybackProvider.select((value) => value.buffering));
|
||||||
MediaSegment? segment = mediaSegments?.atPosition(position);
|
return playButton(playing, buffering);
|
||||||
SegmentVisibility forceShow =
|
}),
|
||||||
segment?.visibility(position, force: showOverlay) ?? SegmentVisibility.hidden;
|
IgnorePointer(
|
||||||
final segmentSkipType = ref
|
ignoring: !showOverlay,
|
||||||
.watch(videoPlayerSettingsProvider.select((value) => value.segmentSkipSettings[segment?.type]));
|
child: AnimatedOpacity(
|
||||||
final autoSkip = forceShow != SegmentVisibility.hidden &&
|
duration: fadeDuration,
|
||||||
segmentSkipType == SegmentSkip.skip &&
|
opacity: showOverlay ? 1 : 0,
|
||||||
player.lastState?.buffering == false;
|
child: Column(
|
||||||
if (autoSkip) {
|
children: [
|
||||||
skipToSegmentEnd(segment);
|
topButtons(context),
|
||||||
}
|
const Spacer(),
|
||||||
return Stack(
|
bottomButtons(context),
|
||||||
children: [
|
],
|
||||||
Align(
|
),
|
||||||
alignment: Alignment.centerRight,
|
),
|
||||||
child: Padding(
|
),
|
||||||
padding: const EdgeInsets.all(32),
|
const VideoPlayerSeekIndicator(),
|
||||||
child: SkipSegmentButton(
|
const VideoPlayerVolumeIndicator(),
|
||||||
segment: segment,
|
Consumer(
|
||||||
skipType: segmentSkipType,
|
builder: (context, ref, child) {
|
||||||
visibility: forceShow,
|
final position = ref.watch(mediaPlaybackProvider.select((value) => value.position));
|
||||||
pressedSkip: () => skipToSegmentEnd(segment),
|
MediaSegment? segment = mediaSegments?.atPosition(position);
|
||||||
|
SegmentVisibility forceShow =
|
||||||
|
segment?.visibility(position, force: showOverlay) ?? SegmentVisibility.hidden;
|
||||||
|
final segmentSkipType = ref
|
||||||
|
.watch(videoPlayerSettingsProvider.select((value) => value.segmentSkipSettings[segment?.type]));
|
||||||
|
final autoSkip = forceShow != SegmentVisibility.hidden &&
|
||||||
|
segmentSkipType == SegmentSkip.skip &&
|
||||||
|
player.lastState?.buffering == false;
|
||||||
|
if (autoSkip) {
|
||||||
|
skipToSegmentEnd(segment);
|
||||||
|
}
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: SkipSegmentButton(
|
||||||
|
segment: segment,
|
||||||
|
skipType: segmentSkipType,
|
||||||
|
visibility: forceShow,
|
||||||
|
pressedSkip: () => skipToSegmentEnd(segment),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -374,19 +379,8 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
||||||
},
|
},
|
||||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer &&
|
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer &&
|
||||||
AdaptiveLayout.viewSizeOf(context) > ViewSize.phone) ...[
|
AdaptiveLayout.viewSizeOf(context) > ViewSize.phone) ...[
|
||||||
Listener(
|
VideoVolumeSlider(
|
||||||
onPointerSignal: (event) {
|
onChanged: () => resetTimer(),
|
||||||
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(),
|
const FullScreenButton(),
|
||||||
]
|
]
|
||||||
|
|
@ -673,6 +667,16 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
||||||
fullScreenHelper.closeFullScreen(ref);
|
fullScreenHelper.closeFullScreen(ref);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setVolume(PointerEvent event) {
|
||||||
|
if (event is PointerScrollEvent) {
|
||||||
|
if (event.scrollDelta.dy > 0) {
|
||||||
|
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5);
|
||||||
|
} else {
|
||||||
|
ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool _onKey(VideoHotKeys value) {
|
bool _onKey(VideoHotKeys value) {
|
||||||
final mediaSegments = ref.read(playBackModel.select((value) => value?.mediaSegments));
|
final mediaSegments = ref.read(playBackModel.select((value) => value?.mediaSegments));
|
||||||
final position = ref.read(mediaPlaybackProvider).position;
|
final position = ref.read(mediaPlaybackProvider).position;
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ class _InputHandlerState<T> extends State<InputHandler<T>> {
|
||||||
focusNode.requestFocus();
|
focusNode.requestFocus();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onKeyEvent: (node, event) => _onKey(event),
|
onKeyEvent: widget.onKeyEvent ?? (node, event) => _onKey(event),
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue