SyncedLyrics autoscrolling active lyric implementation

improved title cleaning for youtube search
This commit is contained in:
Kingkor Roy Tirtho 2022-04-20 21:29:18 +06:00
parent e92f107e55
commit 8af0281b23
8 changed files with 201 additions and 28 deletions

View File

@ -1,10 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/helpers/artist-to-string.dart';
import 'package:spotube/helpers/timed-lyrics.dart';
import 'package:spotube/provider/AudioPlayer.dart';
import 'package:spotube/hooks/useAutoScrollController.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/hooks/useSyncedLyrics.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
class SyncedLyrics extends HookConsumerWidget {
const SyncedLyrics({Key? key}) : super(key: key);
@ -12,18 +16,13 @@ class SyncedLyrics extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider);
AudioPlayer player = ref.watch(audioPlayerProvider);
final breakpoint = useBreakpoints();
final controller = useAutoScrollController();
final timedLyrics = useMemoized(() {
if (playback.currentTrack == null) return null;
return getTimedLyrics(playback.currentTrack!);
}, [playback.currentTrack]);
final lyricsSnapshot = useFuture(timedLyrics);
final stream = useStream(
player.positionStream.isBroadcast
? player.positionStream
: player.positionStream.asBroadcastStream(),
);
final lyricsMap = useMemoized(
() =>
lyricsSnapshot.data?.lyrics
@ -33,22 +32,65 @@ class SyncedLyrics extends HookConsumerWidget {
[lyricsSnapshot.data],
);
print(lyricsSnapshot.data?.name);
final currentTime = useSyncedLyrics(ref, lyricsMap);
final currentLyric = useState("");
useEffect(() {
if (stream.hasData && lyricsMap.containsKey(stream.data!.inSeconds)) {
currentLyric.value = lyricsMap[stream.data!.inSeconds]!;
}
return null;
}, [stream.data, stream.hasData]);
final textTheme = Theme.of(context).textTheme;
return Expanded(
child: Column(
children: [
const Text("Lyrics"),
Text(currentLyric.value),
Center(
child: Text(
playback.currentTrack?.name ?? "",
style: breakpoint >= Breakpoints.md
? textTheme.headline3
: textTheme.headline4?.copyWith(fontSize: 25),
),
),
Center(
child: Text(
artistsToString<Artist>(playback.currentTrack?.artists ?? []),
style: breakpoint >= Breakpoints.md
? textTheme.headline5
: textTheme.headline6,
),
),
if (lyricsSnapshot.hasData)
Expanded(
child: ListView.builder(
controller: controller,
itemBuilder: (context, index) {
final lyricSlice = lyricsSnapshot.data!.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: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
lyricSlice.text,
style: TextStyle(
// indicating the active state of that lyric slice
color: isActive ? Colors.green : null,
fontWeight: isActive ? FontWeight.bold : null,
fontSize: 30,
),
),
),
),
);
},
itemCount: lyricsSnapshot.data!.lyrics.length,
),
),
],
),
);

View File

@ -14,10 +14,29 @@ String clearArtistsOfTitle(String title, List<String> artists) {
.trim();
}
String getTitle(String title, [List<String> artists = const []]) {
String getTitle(
String title, {
List<String> artists = const [],
bool onlyCleanArtist = false,
}) {
final match = RegExp(r"(?<=\().+?(?=\))").firstMatch(title)?.group(0);
final artistInBracket =
artists.any((artist) => match?.contains(artist) ?? false);
if (artistInBracket) {
title = title.replaceAll(
RegExp(" *\\([^)]*\\) *"),
'',
);
}
title = clearArtistsOfTitle(title, artists);
if (onlyCleanArtist) {
artists = [];
}
return "$title ${artists.map((e) => e.replaceAll(",", " ")).join(", ")}"
.toLowerCase()
.replaceAll(RegExp(" *\\([^)]*\\) *"), '')
.replaceAll(RegExp(" *\\[[^\\]]*]"), '')
.replaceAll(RegExp("feat.|ft."), '')
.replaceAll(RegExp("\\s+"), ' ')
@ -66,9 +85,8 @@ Future<List?> searchSong(
apiKey = getRandomElement(lyricsSecrets);
}
const searchUrl = 'https://api.genius.com/search?q=';
String song = optimizeQuery
? getTitle(clearArtistsOfTitle(title, artist), artist)
: "$title $artist";
String song =
optimizeQuery ? getTitle(title, artists: artist) : "$title $artist";
String reqUrl = "$searchUrl${Uri.encodeComponent(song)}";
Map<String, String> headers = {"Authorization": 'Bearer $apiKey'};

View File

@ -16,7 +16,11 @@ Future<Track> toYoutubeTrack(
final mainArtist = artistsName.first;
final featuredArtists =
artistsName.length > 1 ? "feat. " + artistsName.sublist(1).join(" ") : "";
final title = getTitle(clearArtistsOfTitle(track.name!, artistsName)).trim();
final title = getTitle(
track.name!,
artists: artistsName,
onlyCleanArtist: true,
).trim();
logger.v("[Track Search Title] $title");
final queryString = format
.replaceAll("\$MAIN_ARTIST", mainArtist)

View File

@ -32,8 +32,8 @@ Future<SubtitleSimple?> getTimedLyrics(Track track) async {
final artistNames =
track.artists?.map((artist) => artist.name!).toList() ?? [];
final query = getTitle(
clearArtistsOfTitle(track.name!, artistNames),
artistNames,
track.name!,
artists: artistNames,
);
final searchUri = Uri.parse("$baseUri/subtitles4songs.aspx").replace(
queryParameters: {"q": query},

View File

@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
/// Creates [AutoScrollController] that will be disposed automatically.
///
/// See also:
/// - [AutoScrollController]
AutoScrollController useAutoScrollController({
double initialScrollOffset = 0.0,
bool keepScrollOffset = true,
String? debugLabel,
Axis? axis,
AutoScrollController? copyTagsFrom,
double? suggestedRowHeight,
Rect Function() viewportBoundaryGetter = defaultViewportBoundaryGetter,
List<Object?>? keys,
}) {
return use(
_AutoScrollControllerHook(
initialScrollOffset: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
debugLabel: debugLabel,
axis: axis,
copyTagsFrom: copyTagsFrom,
suggestedRowHeight: suggestedRowHeight,
viewportBoundaryGetter: viewportBoundaryGetter,
keys: keys,
),
);
}
class _AutoScrollControllerHook extends Hook<AutoScrollController> {
const _AutoScrollControllerHook({
required this.initialScrollOffset,
required this.keepScrollOffset,
required this.viewportBoundaryGetter,
this.axis,
this.copyTagsFrom,
this.suggestedRowHeight,
this.debugLabel,
List<Object?>? keys,
}) : super(keys: keys);
final double initialScrollOffset;
final bool keepScrollOffset;
final String? debugLabel;
final Axis? axis;
final AutoScrollController? copyTagsFrom;
final double? suggestedRowHeight;
final Rect Function() viewportBoundaryGetter;
@override
HookState<AutoScrollController, Hook<AutoScrollController>> createState() =>
_AutoScrollControllerHookState();
}
class _AutoScrollControllerHookState
extends HookState<AutoScrollController, _AutoScrollControllerHook> {
late final controller = AutoScrollController(
initialScrollOffset: hook.initialScrollOffset,
keepScrollOffset: hook.keepScrollOffset,
debugLabel: hook.debugLabel,
axis: hook.axis,
copyTagsFrom: hook.copyTagsFrom,
suggestedRowHeight: hook.suggestedRowHeight,
viewportBoundaryGetter: hook.viewportBoundaryGetter,
);
@override
AutoScrollController build(BuildContext context) => controller;
@override
void dispose() => controller.dispose();
@override
String get debugLabel => 'useAutoScrollController';
}

View File

@ -0,0 +1,23 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/provider/AudioPlayer.dart';
useSyncedLyrics(WidgetRef ref, Map<int, String> lyricsMap) {
final player = ref.watch(audioPlayerProvider);
final stream = player.positionStream.isBroadcast
? player.positionStream
: player.positionStream.asBroadcastStream();
final currentTime = useState(0);
useEffect(() {
final lol = stream.listen((pos) {
if (lyricsMap.containsKey(pos.inSeconds)) {
currentTime.value = pos.inSeconds;
}
});
return () => lol.cancel();
}, [lyricsMap]);
return currentTime.value;
}

View File

@ -597,6 +597,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.27.3"
scroll_to_index:
dependency: "direct main"
description:
name: scroll_to_index
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
shared_preferences:
dependency: "direct main"
description:

View File

@ -59,6 +59,7 @@ dependencies:
logger: ^1.1.0
permission_handler: ^9.2.0
marquee: ^2.2.1
scroll_to_index: ^2.1.1
dev_dependencies:
flutter_test: