diff --git a/lib/components/Home/Home.dart b/lib/components/Home/Home.dart index 50be0c6d..c99d3c38 100644 --- a/lib/components/Home/Home.dart +++ b/lib/components/Home/Home.dart @@ -11,6 +11,7 @@ import 'package:spotify/spotify.dart' hide Image, Player, Search; import 'package:spotube/components/Category/CategoryCard.dart'; import 'package:spotube/components/Home/Sidebar.dart'; import 'package:spotube/components/Home/SpotubeNavigationBar.dart'; +import 'package:spotube/components/Lyrics/SyncedLyrics.dart'; import 'package:spotube/components/Lyrics.dart'; import 'package:spotube/components/Search/Search.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; @@ -170,7 +171,7 @@ class Home extends HookConsumerWidget { }, [localStorage]); final titleBarContents = Container( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).scaffoldBackgroundColor, child: Row( children: [ Expanded( @@ -222,7 +223,7 @@ class Home extends HookConsumerWidget { ), if (_selectedIndex.value == 1) const Search(), if (_selectedIndex.value == 2) const UserLibrary(), - if (_selectedIndex.value == 3) const Lyrics(), + if (_selectedIndex.value == 3) const SyncedLyrics(), ], ), ), diff --git a/lib/components/Lyrics.dart b/lib/components/Lyrics.dart index 71d155c8..8582ff5c 100644 --- a/lib/components/Lyrics.dart +++ b/lib/components/Lyrics.dart @@ -7,6 +7,7 @@ import 'package:spotube/helpers/getLyrics.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/UserPreferences.dart'; +import 'package:collection/collection.dart'; class Lyrics extends HookConsumerWidget { const Lyrics({Key? key}) : super(key: key); @@ -26,7 +27,11 @@ class Lyrics extends HookConsumerWidget { } return getLyrics( playback.currentTrack!.name!, - artistsToString(playback.currentTrack!.artists ?? []), + playback.currentTrack!.artists + ?.map((s) => s.name) + .whereNotNull() + .toList() ?? + [], apiKey: userPreferences.geniusAccessToken, optimizeQuery: true, ); diff --git a/lib/components/Lyrics/SyncedLyrics.dart b/lib/components/Lyrics/SyncedLyrics.dart new file mode 100644 index 00000000..7c7978f4 --- /dev/null +++ b/lib/components/Lyrics/SyncedLyrics.dart @@ -0,0 +1,56 @@ +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:spotube/helpers/timed-lyrics.dart'; +import 'package:spotube/provider/AudioPlayer.dart'; +import 'package:spotube/provider/Playback.dart'; + +class SyncedLyrics extends HookConsumerWidget { + const SyncedLyrics({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + Playback playback = ref.watch(playbackProvider); + AudioPlayer player = ref.watch(audioPlayerProvider); + 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 + .map((lyric) => {lyric.time.inSeconds: lyric.text}) + .reduce((a, b) => {...a, ...b}) ?? + {}, + [lyricsSnapshot.data], + ); + + print(lyricsSnapshot.data?.name); + + final currentLyric = useState(""); + + useEffect(() { + if (stream.hasData && lyricsMap.containsKey(stream.data!.inSeconds)) { + currentLyric.value = lyricsMap[stream.data!.inSeconds]!; + } + return null; + }, [stream.data, stream.hasData]); + + return Expanded( + child: Column( + children: [ + const Text("Lyrics"), + Text(currentLyric.value), + ], + ), + ); + } +} diff --git a/lib/components/Shared/DownloadTrackButton.dart b/lib/components/Shared/DownloadTrackButton.dart index 5bee714f..22e8874e 100644 --- a/lib/components/Shared/DownloadTrackButton.dart +++ b/lib/components/Shared/DownloadTrackButton.dart @@ -12,6 +12,7 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:path_provider/path_provider.dart' as path_provider; import 'package:path/path.dart' as path; import 'package:permission_handler/permission_handler.dart'; +import 'package:collection/collection.dart'; enum TrackStatus { downloading, idle, done } @@ -118,7 +119,11 @@ class DownloadTrackButton extends HookConsumerWidget { } final lyrics = await getLyrics( playback.currentTrack!.name!, - artistsToString(playback.currentTrack!.artists ?? []), + playback.currentTrack!.artists + ?.map((s) => s.name) + .whereNotNull() + .toList() ?? + [], apiKey: preferences.geniusAccessToken, optimizeQuery: true, ); diff --git a/lib/components/Shared/PageWindowTitleBar.dart b/lib/components/Shared/PageWindowTitleBar.dart index b8fa26f3..f773309e 100644 --- a/lib/components/Shared/PageWindowTitleBar.dart +++ b/lib/components/Shared/PageWindowTitleBar.dart @@ -71,7 +71,7 @@ class PageWindowTitleBar extends StatelessWidget } return WindowTitleBarBox( child: Container( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).scaffoldBackgroundColor, child: Row( children: [ if (Platform.isMacOS) diff --git a/lib/extensions/list-sort-multiple.dart b/lib/extensions/list-sort-multiple.dart new file mode 100644 index 00000000..4b364e71 --- /dev/null +++ b/lib/extensions/list-sort-multiple.dart @@ -0,0 +1,48 @@ +import 'package:collection/collection.dart'; + +extension MultiSortListMap on List { + /// [preference] - List of properties in which you want to sort the list + /// i.e. + /// ``` + /// List preference = ['property1','property2']; + /// ``` + /// This will first sort the list by property1 then by property2 + /// + /// [criteria] - List of booleans that specifies the criteria of sort + /// i.e., For ascending order `true` and for descending order `false`. + /// ``` + /// List criteria = [true. false]; + /// ``` + List sortByProperties(List criteria, List preference) { + if (preference.isEmpty || criteria.isEmpty || isEmpty) { + return this; + } + if (preference.length != criteria.length) { + print('Criteria length is not equal to preference'); + return this; + } + + int compare(int i, Map a, Map b) { + if (a[preference[i]] == b[preference[i]]) { + return 0; + } else if (a[preference[i]] > b[preference[i]]) { + return criteria[i] ? 1 : -1; + } else { + return criteria[i] ? -1 : 1; + } + } + + int sortAll(Map a, Map b) { + int i = 0; + int result = 0; + while (i < preference.length) { + result = compare(i, a, b); + if (result != 0) break; + i++; + } + return result; + } + + return sorted((a, b) => sortAll(a, b)); + } +} diff --git a/lib/helpers/getLyrics.dart b/lib/helpers/getLyrics.dart index 197cc862..15a2fc35 100644 --- a/lib/helpers/getLyrics.dart +++ b/lib/helpers/getLyrics.dart @@ -8,8 +8,14 @@ import 'package:spotube/models/generated_secrets.dart'; final logger = getLogger("GetLyrics"); -String getTitle(String title, String artist) { - return "$title $artist" +String clearArtistsOfTitle(String title, List artists) { + return title + .replaceAll(RegExp(artists.join("|"), caseSensitive: false), "") + .trim(); +} + +String getTitle(String title, [List artists = const []]) { + return "$title ${artists.map((e) => e.replaceAll(",", " ")).join(", ")}" .toLowerCase() .replaceAll(RegExp(" *\\([^)]*\\) *"), '') .replaceAll(RegExp(" *\\[[^\\]]*]"), '') @@ -50,7 +56,7 @@ Future extractLyrics(Uri url) async { Future searchSong( String title, - String artist, { + List artist, { String? apiKey, bool optimizeQuery = false, bool authHeader = false, @@ -60,7 +66,9 @@ Future searchSong( apiKey = getRandomElement(lyricsSecrets); } const searchUrl = 'https://api.genius.com/search?q='; - String song = optimizeQuery ? getTitle(title, artist) : "$title $artist"; + String song = optimizeQuery + ? getTitle(clearArtistsOfTitle(title, artist), artist) + : "$title $artist"; String reqUrl = "$searchUrl${Uri.encodeComponent(song)}"; Map headers = {"Authorization": 'Bearer $apiKey'}; @@ -87,13 +95,13 @@ Future searchSong( Future getLyrics( String title, - String artist, { + List artist, { required String apiKey, bool optimizeQuery = false, bool authHeader = false, }) async { try { - var results = await searchSong( + final results = await searchSong( title, artist, apiKey: apiKey, diff --git a/lib/helpers/search-youtube.dart b/lib/helpers/search-youtube.dart index 73eea476..e3df13d5 100644 --- a/lib/helpers/search-youtube.dart +++ b/lib/helpers/search-youtube.dart @@ -3,16 +3,20 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/helpers/getLyrics.dart'; import 'package:spotube/models/Logger.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; +import 'package:collection/collection.dart'; +import 'package:spotube/extensions/list-sort-multiple.dart'; final logger = getLogger("toYoutubeTrack"); Future toYoutubeTrack( YoutubeExplode youtube, Track track, String format) async { - final artistsName = track.artists?.map((ar) => ar.name).toList() ?? []; + final artistsName = + track.artists?.map((ar) => ar.name).toList().whereNotNull().toList() ?? + []; logger.v("[Track Search Artists] $artistsName"); - final mainArtist = artistsName.first ?? ""; + final mainArtist = artistsName.first; final featuredArtists = artistsName.length > 1 ? "feat. " + artistsName.sublist(1).join(" ") : ""; - final title = getTitle(track.name!, "").trim(); + final title = getTitle(clearArtistsOfTitle(track.name!, artistsName)).trim(); logger.v("[Track Search Title] $title"); final queryString = format .replaceAll("\$MAIN_ARTIST", mainArtist) @@ -22,18 +26,37 @@ Future toYoutubeTrack( SearchList videos = await youtube.search.getVideos(queryString); - List