mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00

Downloaded tracks are saved with metadata. Only MP3 file metadata support is available in local track player for now
260 lines
11 KiB
Dart
260 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,
|
|
),
|
|
[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: AutoSizeText(
|
|
lyricSlice.text,
|
|
maxLines: 2,
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.headline4
|
|
?.copyWith(
|
|
color: isActive
|
|
? Colors.white
|
|
: palette.bodyTextColor,
|
|
// indicating the active state of that lyric slice
|
|
fontWeight: isActive
|
|
? FontWeight.bold
|
|
: null,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
if (playback.track != null &&
|
|
(lyricValue == null ||
|
|
lyricValue.lyrics.isEmpty == true))
|
|
const Expanded(child: ShimmerLyrics()),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|