diff --git a/lib/components/lyrics/zoom_controls.dart b/lib/components/lyrics/zoom_controls.dart index 25479c159..6dbaabf1d 100644 --- a/lib/components/lyrics/zoom_controls.dart +++ b/lib/components/lyrics/zoom_controls.dart @@ -6,19 +6,49 @@ import 'package:spotube/collections/spotube_icons.dart'; class ZoomControls extends HookWidget { final int value; final ValueChanged onChanged; - final int min; - final int max; + final int? min; + final int? max; + + final int interval; + final Icon increaseIcon; + final Icon decreaseIcon; + + final Axis direction; + final String unit; const ZoomControls({ Key? key, required this.value, required this.onChanged, - this.min = 50, - this.max = 200, + this.min, + this.max, + this.interval = 10, + this.increaseIcon = const Icon(SpotubeIcons.zoomIn), + this.decreaseIcon = const Icon(SpotubeIcons.zoomOut), + this.direction = Axis.horizontal, + this.unit = "%", }) : super(key: key); @override Widget build(BuildContext context) { + final actions = [ + PlatformIconButton( + icon: decreaseIcon, + onPressed: () { + if (value == min) return; + onChanged(value - interval); + }, + ), + PlatformText("$value$unit"), + PlatformIconButton( + icon: increaseIcon, + onPressed: () { + if (value == max) return; + onChanged(value + interval); + }, + ), + ]; + return Container( decoration: BoxDecoration( color: PlatformTheme.of(context) @@ -26,29 +56,21 @@ class ZoomControls extends HookWidget { ?.withOpacity(0.7), borderRadius: BorderRadius.circular(10), ), - constraints: const BoxConstraints(maxHeight: 50), + constraints: + BoxConstraints(maxHeight: direction == Axis.horizontal ? 50 : 200), margin: const EdgeInsets.all(8), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - PlatformIconButton( - icon: const Icon(SpotubeIcons.zoomOut), - onPressed: () { - if (value == min) return; - onChanged(value - 10); - }, - ), - PlatformText("$value%"), - PlatformIconButton( - icon: const Icon(SpotubeIcons.zoomIn), - onPressed: () { - if (value == max) return; - onChanged(value + 10); - }, - ), - ], - ), + child: direction == Axis.horizontal + ? Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: actions, + ) + : Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + verticalDirection: VerticalDirection.up, + children: actions, + ), ); } } diff --git a/lib/hooks/use_synced_lyrics.dart b/lib/hooks/use_synced_lyrics.dart index 21f0f7345..99c4a0f1c 100644 --- a/lib/hooks/use_synced_lyrics.dart +++ b/lib/hooks/use_synced_lyrics.dart @@ -5,6 +5,7 @@ import 'package:spotube/provider/playlist_queue_provider.dart'; int useSyncedLyrics( WidgetRef ref, Map lyricsMap, + int delay, ) { final stream = PlaylistQueueNotifier.position; @@ -12,11 +13,11 @@ int useSyncedLyrics( useEffect(() { return stream.listen((pos) { - if (lyricsMap.containsKey(pos.inSeconds)) { - currentTime.value = pos.inSeconds; + if (lyricsMap.containsKey(pos.inSeconds + delay)) { + currentTime.value = pos.inSeconds + delay; } }).cancel; - }, [lyricsMap]); + }, [lyricsMap, delay]); return (Duration(seconds: currentTime.value)).inSeconds; } diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 4d0ad0323..b19b83ab4 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/lyrics/zoom_controls.dart'; import 'package:spotube/components/shared/shimmers/shimmer_lyrics.dart'; import 'package:spotube/components/shared/spotube_marquee_text.dart'; @@ -15,6 +16,8 @@ import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; +final _delay = StateProvider((ref) => 0); + class SyncedLyrics extends HookConsumerWidget { final PaletteColor palette; final bool? isModal; @@ -32,9 +35,13 @@ class SyncedLyrics extends HookConsumerWidget { final breakpoint = useBreakpoints(); final controller = useAutoScrollController(); + final delay = ref.watch(_delay); + final timedLyricsQuery = useQueries.lyrics.spotifySynced(ref, playlist?.activeTrack); + final lyricValue = timedLyricsQuery.data; + final lyricsMap = useMemoized( () => lyricValue?.lyrics @@ -44,13 +51,16 @@ class SyncedLyrics extends HookConsumerWidget { {}, [lyricValue], ); - final currentTime = useSyncedLyrics(ref, lyricsMap); + final currentTime = useSyncedLyrics(ref, lyricsMap, delay); final textZoomLevel = useState(100); final textTheme = Theme.of(context).textTheme; useEffect(() { controller.scrollToIndex(0); + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(_delay.notifier).state = 0; + }); return null; }, [playlist?.activeTrack]); @@ -137,12 +147,37 @@ class SyncedLyrics extends HookConsumerWidget { ), Align( alignment: Alignment.bottomRight, - child: ZoomControls( - value: textZoomLevel.value, - onChanged: (value) => textZoomLevel.value = value, - min: 50, - max: 200, - ), + child: Builder(builder: (context) { + final actions = [ + ZoomControls( + value: delay, + onChanged: (value) => ref.read(_delay.notifier).state = value, + interval: 1, + unit: "s", + increaseIcon: const Icon(SpotubeIcons.add), + decreaseIcon: const Icon(SpotubeIcons.remove), + direction: isModal == true ? Axis.horizontal : Axis.vertical, + ), + ZoomControls( + value: textZoomLevel.value, + onChanged: (value) => textZoomLevel.value = value, + min: 50, + max: 200, + ), + ]; + + return isModal == true + ? Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: actions, + ) + : Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: actions, + ); + }), ), ], );