spotube/lib/components/Lyrics/SyncedLyrics.dart

262 lines
11 KiB
Dart

import 'dart:ui';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart';
import 'package:spotube/components/Lyrics/LyricDelayAdjustDialog.dart';
import 'package:spotube/components/Lyrics/Lyrics.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Shared/SpotubeMarqueeText.dart';
import 'package:spotube/components/Shared/UniversalImage.dart';
import 'package:spotube/hooks/useAutoScrollController.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/hooks/useCustomStatusBarColor.dart';
import 'package:spotube/hooks/usePaletteColor.dart';
import 'package:spotube/hooks/useSyncedLyrics.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
final lyricDelayState = StateProvider<Duration>(
(ref) {
return Duration.zero;
},
);
class SyncedLyrics extends HookConsumerWidget {
const SyncedLyrics({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
final timedLyricsSnapshot = ref.watch(rentanadviserLyricsQuery);
final lyricDelay = ref.watch(lyricDelayState);
Playback playback = ref.watch(playbackProvider);
final breakpoint = useBreakpoints();
final controller = useAutoScrollController();
final failed = useState(false);
final lyricValue = timedLyricsSnapshot.asData?.value;
final lyricsMap = useMemoized(
() =>
lyricValue?.lyrics
.map((lyric) => {lyric.time.inSeconds: lyric.text})
.reduce((accumulator, lyricSlice) =>
{...accumulator, ...lyricSlice}) ??
{},
[lyricValue],
);
final currentTime = useSyncedLyrics(ref, lyricsMap, lyricDelay);
final textTheme = Theme.of(context).textTheme;
useEffect(() {
controller.scrollToIndex(0);
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(lyricDelayState.notifier).state = Duration.zero;
});
failed.value = false;
return null;
}, [playback.track]);
useEffect(() {
if (lyricValue != null && lyricValue.rating <= 2) {
Future.delayed(const Duration(seconds: 5), () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
actions: [
TextButton(
child: const Text("No"),
onPressed: () {
Navigator.pop(context);
},
),
TextButton(
child: const Text("Yes"),
onPressed: () {
failed.value = true;
Navigator.pop(context);
},
),
],
content: Column(
mainAxisSize: MainAxisSize.min,
children: const [
Text(
"The found lyrics might not be properly synced. Do you want to default to static (genius.com) lyrics?",
),
SizedBox(height: 10),
Text(
"Hint: Wait for a moment to see if the lyric actually sync. Sometimes it may sync.",
),
],
),
);
},
);
});
}
return null;
}, [lyricValue]);
// when synced lyrics not found, fallback to GeniusLyrics
String albumArt = useMemoized(
() => TypeConversionUtils.image_X_UrlString(
playback.track?.album?.images,
index: (playback.track?.album?.images?.length ?? 1) - 1,
placeholder: ImagePlaceholder.albumArt,
),
[playback.track?.album?.images],
);
final palette = usePaletteColor(albumArt, ref);
final headlineTextStyle = (breakpoint >= Breakpoints.md
? textTheme.headline3
: textTheme.headline4?.copyWith(fontSize: 25))
?.copyWith(color: palette.titleTextColor);
useCustomStatusBarColor(
palette.color,
true,
noSetBGColor: true,
);
return Expanded(
child: Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
image: DecorationImage(
image: UniversalImage.imageProvider(albumArt),
fit: BoxFit.cover,
),
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(
color: palette.color.withOpacity(.7),
child: SafeArea(
child: failed.value
? Lyrics(titleBarForegroundColor: palette.bodyTextColor)
: Column(
children: [
PageWindowTitleBar(
foregroundColor: palette.bodyTextColor,
),
SizedBox(
height: breakpoint >= Breakpoints.md ? 50 : 30,
child: Material(
type: MaterialType.transparency,
child: Stack(
children: [
Center(
child: SpotubeMarqueeText(
text: playback.track?.name ?? "Not Playing",
style: headlineTextStyle,
isHovering: true,
),
),
Positioned.fill(
child: Align(
alignment: Alignment.centerRight,
child: IconButton(
tooltip: "Lyrics Delay",
icon: const Icon(Icons.av_timer_rounded),
onPressed: () async {
final delay = await showDialog(
context: context,
builder: (context) =>
const LyricDelayAdjustDialog(),
);
if (delay != null) {
ref
.read(lyricDelayState.notifier)
.state = delay;
}
},
),
),
),
],
),
),
),
Center(
child: Text(
TypeConversionUtils.artists_X_String<Artist>(
playback.track?.artists ?? []),
style: breakpoint >= Breakpoints.md
? textTheme.headline5
: textTheme.headline6,
),
),
if (lyricValue != null && lyricValue.lyrics.isNotEmpty)
Expanded(
child: ListView.builder(
controller: controller,
itemCount: lyricValue.lyrics.length,
itemBuilder: (context, index) {
final lyricSlice = lyricValue.lyrics[index];
final isActive =
lyricSlice.time.inSeconds == currentTime;
if (isActive) {
controller.scrollToIndex(
index,
preferPosition: AutoScrollPosition.middle,
);
}
return AutoScrollTag(
key: ValueKey(index),
index: index,
controller: controller,
child: lyricSlice.text.isEmpty
? Container()
: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: AnimatedDefaultTextStyle(
duration: const Duration(
milliseconds: 250),
style: TextStyle(
color: isActive
? Colors.white
: palette.bodyTextColor,
fontWeight: isActive
? FontWeight.bold
: FontWeight.normal,
fontSize: isActive ? 30 : 26,
),
child: Text(
lyricSlice.text,
maxLines: 2,
textAlign: TextAlign.center,
),
),
),
),
);
},
),
),
if (playback.track != null &&
(lyricValue == null ||
lyricValue.lyrics.isEmpty == true))
const Expanded(child: ShimmerLyrics()),
],
),
),
),
),
),
);
}
}