mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
feat(keyboard shortcuts): play/pause on space, seek position on left/right
This commit is contained in:
parent
dbb81de763
commit
2734454717
@ -79,11 +79,11 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
if (!kIsWeb)
|
if (!kIsWeb)
|
||||||
if (isInQueue)
|
if (isInQueue)
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
child: CircularProgressIndicator.adaptive(
|
child: CircularProgressIndicator.adaptive(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
),
|
),
|
||||||
height: 20,
|
|
||||||
width: 20,
|
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
IconButton(
|
IconButton(
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/hooks/playback.dart';
|
import 'package:spotube/hooks/playback.dart';
|
||||||
|
import 'package:spotube/models/Intents.dart';
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
@ -16,152 +18,185 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
|
|
||||||
final logger = getLogger(PlayerControls);
|
final logger = getLogger(PlayerControls);
|
||||||
|
|
||||||
|
static FocusNode focusNode = FocusNode();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
|
final shortcuts = useMemoized(
|
||||||
|
() => {
|
||||||
|
const SingleActivator(LogicalKeyboardKey.arrowRight):
|
||||||
|
SeekIntent(ref, true),
|
||||||
|
const SingleActivator(LogicalKeyboardKey.arrowLeft):
|
||||||
|
SeekIntent(ref, false),
|
||||||
|
},
|
||||||
|
[ref]);
|
||||||
|
final actions = useMemoized(
|
||||||
|
() => {
|
||||||
|
SeekIntent: SeekAction(),
|
||||||
|
},
|
||||||
|
[]);
|
||||||
final Playback playback = ref.watch(playbackProvider);
|
final Playback playback = ref.watch(playbackProvider);
|
||||||
|
|
||||||
final onNext = useNextTrack(ref);
|
final onNext = useNextTrack(ref);
|
||||||
final onPrevious = usePreviousTrack(ref);
|
final onPrevious = usePreviousTrack(ref);
|
||||||
final _playOrPause = useTogglePlayPause(ref);
|
|
||||||
|
|
||||||
final duration = playback.currentDuration;
|
final duration = playback.currentDuration;
|
||||||
|
|
||||||
return Container(
|
return GestureDetector(
|
||||||
constraints: const BoxConstraints(maxWidth: 600),
|
behavior: HitTestBehavior.translucent,
|
||||||
child: Column(
|
onTap: () {
|
||||||
children: [
|
if (focusNode.canRequestFocus) {
|
||||||
StreamBuilder<Duration>(
|
focusNode.requestFocus();
|
||||||
stream: playback.player.onPositionChanged,
|
}
|
||||||
builder: (context, snapshot) {
|
},
|
||||||
final totalMinutes = PrimitiveUtils.zeroPadNumStr(
|
child: FocusableActionDetector(
|
||||||
duration.inMinutes.remainder(60));
|
focusNode: focusNode,
|
||||||
final totalSeconds = PrimitiveUtils.zeroPadNumStr(
|
shortcuts: shortcuts,
|
||||||
duration.inSeconds.remainder(60));
|
actions: actions,
|
||||||
final currentMinutes = snapshot.hasData
|
child: Container(
|
||||||
? PrimitiveUtils.zeroPadNumStr(
|
constraints: const BoxConstraints(maxWidth: 600),
|
||||||
snapshot.data!.inMinutes.remainder(60))
|
child: Column(
|
||||||
: "00";
|
children: [
|
||||||
final currentSeconds = snapshot.hasData
|
StreamBuilder<Duration>(
|
||||||
? PrimitiveUtils.zeroPadNumStr(
|
stream: playback.player.onPositionChanged,
|
||||||
snapshot.data!.inSeconds.remainder(60))
|
builder: (context, snapshot) {
|
||||||
: "00";
|
final totalMinutes = PrimitiveUtils.zeroPadNumStr(
|
||||||
|
duration.inMinutes.remainder(60));
|
||||||
|
final totalSeconds = PrimitiveUtils.zeroPadNumStr(
|
||||||
|
duration.inSeconds.remainder(60));
|
||||||
|
final currentMinutes = snapshot.hasData
|
||||||
|
? PrimitiveUtils.zeroPadNumStr(
|
||||||
|
snapshot.data!.inMinutes.remainder(60))
|
||||||
|
: "00";
|
||||||
|
final currentSeconds = snapshot.hasData
|
||||||
|
? PrimitiveUtils.zeroPadNumStr(
|
||||||
|
snapshot.data!.inSeconds.remainder(60))
|
||||||
|
: "00";
|
||||||
|
|
||||||
final sliderMax = duration.inSeconds;
|
final sliderMax = duration.inSeconds;
|
||||||
final sliderValue = snapshot.data?.inSeconds ?? 0;
|
final sliderValue = snapshot.data?.inSeconds ?? 0;
|
||||||
|
|
||||||
return HookBuilder(
|
return HookBuilder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final progressStatic =
|
final progressStatic =
|
||||||
(sliderMax == 0 || sliderValue > sliderMax)
|
(sliderMax == 0 || sliderValue > sliderMax)
|
||||||
? 0
|
? 0
|
||||||
: sliderValue / sliderMax;
|
: sliderValue / sliderMax;
|
||||||
|
|
||||||
final progress = useState<num>(
|
final progress = useState<num>(
|
||||||
useMemoized(() => progressStatic, []),
|
useMemoized(() => progressStatic, []),
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
progress.value = progressStatic;
|
progress.value = progressStatic;
|
||||||
return null;
|
return null;
|
||||||
}, [progressStatic]);
|
}, [progressStatic]);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Slider.adaptive(
|
Slider.adaptive(
|
||||||
// cannot divide by zero
|
focusNode: FocusNode(),
|
||||||
// there's an edge case for value being bigger
|
// cannot divide by zero
|
||||||
// than total duration. Keeping it resolved
|
// there's an edge case for value being bigger
|
||||||
value: progress.value.toDouble(),
|
// than total duration. Keeping it resolved
|
||||||
onChanged: (v) {
|
value: progress.value.toDouble(),
|
||||||
progress.value = v;
|
onChanged: (v) {
|
||||||
},
|
progress.value = v;
|
||||||
onChangeEnd: (value) async {
|
},
|
||||||
await playback.seekPosition(
|
onChangeEnd: (value) async {
|
||||||
Duration(
|
await playback.seekPosition(
|
||||||
seconds: (value * sliderMax).toInt(),
|
Duration(
|
||||||
),
|
seconds: (value * sliderMax).toInt(),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
activeColor: iconColor,
|
},
|
||||||
),
|
activeColor: iconColor,
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8.0,
|
|
||||||
),
|
),
|
||||||
child: Row(
|
Padding(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
padding: const EdgeInsets.symmetric(
|
||||||
children: [
|
horizontal: 8.0,
|
||||||
Text(
|
),
|
||||||
"$currentMinutes:$currentSeconds",
|
child: Row(
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
Text("$totalMinutes:$totalSeconds"),
|
children: [
|
||||||
],
|
Text(
|
||||||
|
"$currentMinutes:$currentSeconds",
|
||||||
|
),
|
||||||
|
Text("$totalMinutes:$totalSeconds"),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
);
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
Row(
|
||||||
Row(
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
children: [
|
||||||
children: [
|
IconButton(
|
||||||
IconButton(
|
icon: Icon(
|
||||||
icon: Icon(
|
playback.isLoop
|
||||||
playback.isLoop
|
? Icons.repeat_one_rounded
|
||||||
? Icons.repeat_one_rounded
|
: playback.isShuffled
|
||||||
: playback.isShuffled
|
? Icons.shuffle_rounded
|
||||||
? Icons.shuffle_rounded
|
: Icons.repeat_rounded,
|
||||||
: Icons.repeat_rounded,
|
),
|
||||||
|
onPressed:
|
||||||
|
playback.track == null || playback.playlist == null
|
||||||
|
? null
|
||||||
|
: playback.cyclePlaybackMode,
|
||||||
),
|
),
|
||||||
onPressed: playback.track == null || playback.playlist == null
|
IconButton(
|
||||||
? null
|
icon: const Icon(Icons.skip_previous_rounded),
|
||||||
: playback.cyclePlaybackMode,
|
color: iconColor,
|
||||||
),
|
onPressed: () {
|
||||||
IconButton(
|
onPrevious();
|
||||||
icon: const Icon(Icons.skip_previous_rounded),
|
}),
|
||||||
|
IconButton(
|
||||||
|
icon: playback.status == PlaybackStatus.loading
|
||||||
|
? const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
playback.isPlaying
|
||||||
|
? Icons.pause_rounded
|
||||||
|
: Icons.play_arrow_rounded,
|
||||||
|
),
|
||||||
color: iconColor,
|
color: iconColor,
|
||||||
onPressed: () {
|
onPressed: Actions.handler<PlayPauseIntent>(
|
||||||
onPrevious();
|
context,
|
||||||
}),
|
PlayPauseIntent(ref),
|
||||||
IconButton(
|
),
|
||||||
icon: playback.status == PlaybackStatus.loading
|
),
|
||||||
? const SizedBox(
|
IconButton(
|
||||||
height: 20,
|
icon: const Icon(Icons.skip_next_rounded),
|
||||||
width: 20,
|
onPressed: () => onNext(),
|
||||||
child: CircularProgressIndicator(),
|
color: iconColor,
|
||||||
)
|
),
|
||||||
: Icon(
|
IconButton(
|
||||||
playback.isPlaying
|
icon: const Icon(Icons.stop_rounded),
|
||||||
? Icons.pause_rounded
|
color: iconColor,
|
||||||
: Icons.play_arrow_rounded,
|
onPressed: playback.track != null
|
||||||
),
|
? () async {
|
||||||
color: iconColor,
|
try {
|
||||||
onPressed: _playOrPause,
|
await playback.stop();
|
||||||
),
|
} catch (e, stack) {
|
||||||
IconButton(
|
logger.e("onStop", e, stack);
|
||||||
icon: const Icon(Icons.skip_next_rounded),
|
}
|
||||||
onPressed: () => onNext(),
|
|
||||||
color: iconColor,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.stop_rounded),
|
|
||||||
color: iconColor,
|
|
||||||
onPressed: playback.track != null
|
|
||||||
? () async {
|
|
||||||
try {
|
|
||||||
await playback.stop();
|
|
||||||
} catch (e, stack) {
|
|
||||||
logger.e("onStop", e, stack);
|
|
||||||
}
|
}
|
||||||
}
|
: null,
|
||||||
: null,
|
)
|
||||||
)
|
],
|
||||||
],
|
),
|
||||||
),
|
const SizedBox(height: 5)
|
||||||
const SizedBox(height: 5)
|
],
|
||||||
],
|
),
|
||||||
));
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import 'package:spotube/components/Player/PlayerTrackDetails.dart';
|
|||||||
import 'package:spotube/hooks/playback.dart';
|
import 'package:spotube/hooks/playback.dart';
|
||||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||||
import 'package:spotube/hooks/usePaletteColor.dart';
|
import 'package:spotube/hooks/usePaletteColor.dart';
|
||||||
|
import 'package:spotube/models/Intents.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
import 'package:spotube/provider/UserPreferences.dart';
|
import 'package:spotube/provider/UserPreferences.dart';
|
||||||
|
|
||||||
@ -33,7 +34,6 @@ class PlayerOverlay extends HookConsumerWidget {
|
|||||||
|
|
||||||
final onNext = useNextTrack(ref);
|
final onNext = useNextTrack(ref);
|
||||||
final onPrevious = usePreviousTrack(ref);
|
final onPrevious = usePreviousTrack(ref);
|
||||||
final _playOrPause = useTogglePlayPause(ref);
|
|
||||||
|
|
||||||
if (!isHome && !isAllowedPage) return Container();
|
if (!isHome && !isAllowedPage) return Container();
|
||||||
|
|
||||||
@ -109,7 +109,10 @@ class PlayerOverlay extends HookConsumerWidget {
|
|||||||
: Icons.play_arrow_rounded,
|
: Icons.play_arrow_rounded,
|
||||||
),
|
),
|
||||||
color: paletteColor.bodyTextColor,
|
color: paletteColor.bodyTextColor,
|
||||||
onPressed: _playOrPause,
|
onPressed: Actions.handler<PlayPauseIntent>(
|
||||||
|
context,
|
||||||
|
PlayPauseIntent(ref),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
|
|
||||||
@ -30,24 +29,3 @@ Future<void> Function() usePreviousTrack(WidgetRef ref) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> Function([dynamic]) useTogglePlayPause(WidgetRef ref) {
|
|
||||||
return ([key]) async {
|
|
||||||
try {
|
|
||||||
final playback = ref.read(playbackProvider);
|
|
||||||
if (playback.track == null) {
|
|
||||||
return;
|
|
||||||
} else if (playback.track != null &&
|
|
||||||
playback.currentDuration == Duration.zero &&
|
|
||||||
await playback.player.getCurrentPosition() == Duration.zero) {
|
|
||||||
final track = Track.fromJson(playback.track!.toJson());
|
|
||||||
playback.track = null;
|
|
||||||
await playback.play(track);
|
|
||||||
} else {
|
|
||||||
await playback.togglePlayPause();
|
|
||||||
}
|
|
||||||
} catch (e, stack) {
|
|
||||||
logger.e("useTogglePlayPause", e, stack);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -3,6 +3,7 @@ import 'dart:convert';
|
|||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
@ -11,6 +12,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
import 'package:spotube/components/Shared/ReplaceDownloadedFileDialog.dart';
|
import 'package:spotube/components/Shared/ReplaceDownloadedFileDialog.dart';
|
||||||
import 'package:spotube/entities/CacheTrack.dart';
|
import 'package:spotube/entities/CacheTrack.dart';
|
||||||
import 'package:spotube/models/GoRouteDeclarations.dart';
|
import 'package:spotube/models/GoRouteDeclarations.dart';
|
||||||
|
import 'package:spotube/models/Intents.dart';
|
||||||
import 'package:spotube/models/LocalStorageKeys.dart';
|
import 'package:spotube/models/LocalStorageKeys.dart';
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:spotube/provider/AudioPlayer.dart';
|
import 'package:spotube/provider/AudioPlayer.dart';
|
||||||
@ -52,7 +54,6 @@ void main() async {
|
|||||||
Builder(
|
Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return ProviderScope(
|
return ProviderScope(
|
||||||
child: const Spotube(),
|
|
||||||
overrides: [
|
overrides: [
|
||||||
playbackProvider.overrideWithProvider(
|
playbackProvider.overrideWithProvider(
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
@ -123,6 +124,7 @@ void main() async {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
child: const Spotube(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -205,6 +207,17 @@ class _SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver {
|
|||||||
backgroundMaterialColor: backgroundMaterialColor,
|
backgroundMaterialColor: backgroundMaterialColor,
|
||||||
),
|
),
|
||||||
themeMode: themeMode,
|
themeMode: themeMode,
|
||||||
|
shortcuts: {
|
||||||
|
...WidgetsApp.defaultShortcuts,
|
||||||
|
const SingleActivator(LogicalKeyboardKey.space): PlayPauseIntent(ref),
|
||||||
|
const SingleActivator(LogicalKeyboardKey.comma, control: true):
|
||||||
|
OpenSettingsIntent(_router),
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
...WidgetsApp.defaultActions,
|
||||||
|
PlayPauseIntent: PlayPauseAction(),
|
||||||
|
OpenSettingsIntent: OpenSettingsAction(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
85
lib/models/Intents.dart
Normal file
85
lib/models/Intents.dart
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/components/Player/PlayerControls.dart';
|
||||||
|
import 'package:spotube/models/Logger.dart';
|
||||||
|
import 'package:spotube/provider/Playback.dart';
|
||||||
|
|
||||||
|
class PlayPauseIntent extends Intent {
|
||||||
|
final WidgetRef ref;
|
||||||
|
const PlayPauseIntent(this.ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlayPauseAction extends Action<PlayPauseIntent> {
|
||||||
|
final logger = getLogger(PlayPauseAction);
|
||||||
|
|
||||||
|
@override
|
||||||
|
invoke(intent) async {
|
||||||
|
try {
|
||||||
|
if (PlayerControls.focusNode.canRequestFocus) {
|
||||||
|
PlayerControls.focusNode.requestFocus();
|
||||||
|
}
|
||||||
|
final playback = intent.ref.read(playbackProvider);
|
||||||
|
if (playback.track == null) {
|
||||||
|
return null;
|
||||||
|
} else if (playback.track != null &&
|
||||||
|
playback.currentDuration == Duration.zero &&
|
||||||
|
await playback.player.getCurrentPosition() == Duration.zero) {
|
||||||
|
final track = Track.fromJson(playback.track!.toJson());
|
||||||
|
playback.track = null;
|
||||||
|
await playback.play(track);
|
||||||
|
} else {
|
||||||
|
await playback.togglePlayPause();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e, stack) {
|
||||||
|
logger.e("useTogglePlayPause", e, stack);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OpenSettingsIntent extends Intent {
|
||||||
|
final GoRouter router;
|
||||||
|
const OpenSettingsIntent(this.router);
|
||||||
|
}
|
||||||
|
|
||||||
|
class OpenSettingsAction extends Action<OpenSettingsIntent> {
|
||||||
|
@override
|
||||||
|
invoke(intent) async {
|
||||||
|
intent.router.push("/settings");
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SeekIntent extends Intent {
|
||||||
|
final WidgetRef ref;
|
||||||
|
final bool forward;
|
||||||
|
const SeekIntent(this.ref, this.forward);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SeekAction extends Action<SeekIntent> {
|
||||||
|
@override
|
||||||
|
invoke(intent) async {
|
||||||
|
final playback = intent.ref.read(playbackProvider);
|
||||||
|
if ((playback.playlist == null && playback.track == null) ||
|
||||||
|
playback.status == PlaybackStatus.loading) {
|
||||||
|
DirectionalFocusAction().invoke(
|
||||||
|
DirectionalFocusIntent(
|
||||||
|
intent.forward ? TraversalDirection.right : TraversalDirection.left,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final position =
|
||||||
|
(await playback.player.getCurrentPosition() ?? Duration.zero).inSeconds;
|
||||||
|
await playback.seekPosition(
|
||||||
|
Duration(
|
||||||
|
seconds: intent.forward ? position + 5 : position - 5,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user