From 6b6907af3fdb327312ad4dd9e16b3e2a850ed896 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 24 Oct 2022 17:59:58 +0600 Subject: [PATCH] feat(lyrics): tabs for both synced and static lyrics #182 refactor: remove code-style warnings --- lib/components/Album/AlbumView.dart | 7 - lib/components/Home/Home.dart | 197 ------------ lib/components/Library/UserLocalTracks.dart | 3 +- lib/components/Login/TokenLogin.dart | 1 - lib/components/Lyrics/GeniusLyrics.dart | 114 +++++++ lib/components/Lyrics/Lyrics.dart | 120 +++----- lib/components/Lyrics/SyncedLyrics.dart | 288 ++++++------------ .../Playlist/PlaylistCreateDialog.dart | 16 +- lib/components/Settings/About.dart | 2 +- lib/components/Settings/Settings.dart | 4 +- lib/components/Shared/AnchorButton.dart | 6 +- lib/components/Shared/ColoredTabBar.dart | 2 + .../Shared/DownloadConfirmationDialog.dart | 6 +- lib/components/Shared/Hyperlink.dart | 1 - lib/components/Shared/PageWindowTitleBar.dart | 2 - lib/hooks/usePaletteColor.dart | 1 - lib/main.dart | 4 +- lib/models/GoRouteDeclarations.dart | 5 +- lib/models/Intents.dart | 11 +- lib/provider/DBus.dart | 2 - lib/provider/SpotifyRequests.dart | 2 - lib/services/LinuxAudioService.dart | 9 +- lib/utils/duration.dart | 17 +- lib/utils/service_utils.dart | 108 ------- pubspec.lock | 2 +- pubspec.yaml | 1 + test/widget_test.dart | 2 +- 27 files changed, 311 insertions(+), 622 deletions(-) delete mode 100644 lib/components/Home/Home.dart create mode 100644 lib/components/Lyrics/GeniusLyrics.dart diff --git a/lib/components/Album/AlbumView.dart b/lib/components/Album/AlbumView.dart index 4397e55e..e30aac71 100644 --- a/lib/components/Album/AlbumView.dart +++ b/lib/components/Album/AlbumView.dart @@ -1,4 +1,3 @@ -import 'package:fl_query/fl_query.dart'; import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -12,7 +11,6 @@ import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/models/CurrentPlaylist.dart'; -import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyRequests.dart'; @@ -56,16 +54,11 @@ class AlbumView extends HookConsumerWidget { Playback playback = ref.watch(playbackProvider); final SpotifyApi spotify = ref.watch(spotifyProvider); - final Auth auth = ref.watch(authProvider); final tracksSnapshot = useQuery( job: albumTracksQueryJob(album.id!), externalData: spotify, ); - final albumSavedSnapshot = useQuery( - job: albumIsSavedForCurrentUserQueryJob(album.id!), - externalData: spotify, - ); final albumArt = useMemoized( () => TypeConversionUtils.image_X_UrlString( diff --git a/lib/components/Home/Home.dart b/lib/components/Home/Home.dart deleted file mode 100644 index e2d54647..00000000 --- a/lib/components/Home/Home.dart +++ /dev/null @@ -1,197 +0,0 @@ -import 'package:bitsdojo_window/bitsdojo_window.dart'; -import 'package:fl_query_hooks/fl_query_hooks.dart'; -import 'package:flutter/material.dart' hide Page; -import 'package:flutter/services.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -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/LoaderShimmers/ShimmerCategories.dart'; -import 'package:spotube/components/Lyrics/SyncedLyrics.dart'; -import 'package:spotube/components/Search/Search.dart'; -import 'package:spotube/components/Shared/ReplaceDownloadedFileDialog.dart'; -import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; -import 'package:spotube/components/Player/Player.dart'; -import 'package:spotube/components/Library/UserLibrary.dart'; -import 'package:spotube/components/Shared/Waypoint.dart'; -import 'package:spotube/hooks/useBreakpointValue.dart'; -import 'package:spotube/hooks/useUpdateChecker.dart'; -import 'package:spotube/models/Logger.dart'; -import 'package:spotube/provider/Downloader.dart'; -import 'package:spotube/provider/SpotifyDI.dart'; -import 'package:spotube/provider/SpotifyRequests.dart'; -import 'package:spotube/provider/UserPreferences.dart'; -import 'package:spotube/utils/platform.dart'; - -final selectedIndexState = StateProvider((ref) => 0); - -class Home extends HookConsumerWidget { - Home({Key? key}) : super(key: key); - final logger = getLogger(Home); - - @override - Widget build(BuildContext context, ref) { - final double titleBarWidth = useBreakpointValue( - sm: 0.0, - md: 80.0, - lg: 256.0, - xl: 256.0, - xxl: 256.0, - ); - final extended = ref.watch(sidebarExtendedStateProvider); - final selectedIndex = ref.watch(selectedIndexState); - onSelectedIndexChanged(int index) => - ref.read(selectedIndexState.notifier).state = index; - - final downloader = ref.watch(downloaderProvider); - final isMounted = useIsMounted(); - - useEffect(() { - downloader.onFileExists = (track) async { - if (!isMounted()) return false; - return await showDialog( - context: context, - builder: (context) => ReplaceDownloadedFileDialog( - track: track, - ), - ) ?? - false; - }; - return null; - }, [downloader]); - - // checks for latest version of the application - useUpdateChecker(ref); - - final titleBarContents = Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: Row( - children: [ - Expanded( - child: Row( - children: [ - Container( - constraints: BoxConstraints( - maxWidth: extended == null - ? titleBarWidth - : (extended ? 256 : 80), - ), - color: Theme.of(context).navigationRailTheme.backgroundColor, - child: MoveWindow(), - ), - Expanded(child: MoveWindow()), - if (!kIsMacOS && !kIsMobile) const TitleBarActionButtons(), - ], - ), - ) - ], - ), - ); - - final backgroundColor = Theme.of(context).backgroundColor; - - useEffect(() { - SystemChrome.setSystemUIOverlayStyle( - SystemUiOverlayStyle( - statusBarColor: backgroundColor, // status bar color - statusBarIconBrightness: backgroundColor.computeLuminance() > 0.179 - ? Brightness.dark - : Brightness.light, - ), - ); - return null; - }, [backgroundColor]); - - return Scaffold( - bottomNavigationBar: SpotubeNavigationBar( - selectedIndex: selectedIndex, - onSelectedIndexChanged: onSelectedIndexChanged, - ), - body: Column( - children: [ - if (selectedIndex != 3) - kIsMobile - ? titleBarContents - : WindowTitleBarBox(child: titleBarContents), - Expanded( - child: Row( - children: [ - Sidebar( - selectedIndex: selectedIndex, - onSelectedIndexChanged: onSelectedIndexChanged, - ), - // contents of the spotify - if (selectedIndex == 0) - Expanded( - child: Padding( - padding: const EdgeInsets.only( - bottom: 8.0, - top: 8.0, - left: 8.0, - ), - child: HookBuilder(builder: (context) { - final spotify = ref.watch(spotifyProvider); - final recommendationMarket = ref.watch( - userPreferencesProvider - .select((s) => s.recommendationMarket), - ); - - final categoriesQuery = useInfiniteQuery( - job: categoriesQueryJob, - externalData: { - "spotify": spotify, - "recommendationMarket": recommendationMarket, - }, - ); - - final categories = [ - useMemoized( - () => Category() - ..id = "user-featured-playlists" - ..name = "Featured", - [], - ), - ...categoriesQuery.pages - .expand( - (page) => page?.items ?? const Iterable.empty(), - ) - .toList() - ]; - - return ListView.builder( - itemCount: categories.length, - itemBuilder: (context, index) { - final category = categories[index]; - if (category == null) return Container(); - if (index == categories.length - 1) { - return Waypoint( - onEnter: () { - if (categoriesQuery.hasNextPage) { - categoriesQuery.fetchNextPage(); - } - }, - child: const ShimmerCategories(), - ); - } - return CategoryCard(category); - }, - ); - }), - ), - ), - if (selectedIndex == 1) const Search(), - if (selectedIndex == 2) const UserLibrary(), - if (selectedIndex == 3) const SyncedLyrics(), - ], - ), - ), - // player itself - Player(), - ], - ), - ); - } -} diff --git a/lib/components/Library/UserLocalTracks.dart b/lib/components/Library/UserLocalTracks.dart index 71cfa155..221aa213 100644 --- a/lib/components/Library/UserLocalTracks.dart +++ b/lib/components/Library/UserLocalTracks.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -193,7 +192,7 @@ class UserLocalTracks extends HookConsumerWidget { SortTracksDropdown( value: sortBy.value, onChanged: (value) { - if (value != null) sortBy.value = value; + sortBy.value = value; }, ), const SizedBox(width: 10), diff --git a/lib/components/Login/TokenLogin.dart b/lib/components/Login/TokenLogin.dart index 9515d816..57b2be9a 100644 --- a/lib/components/Login/TokenLogin.dart +++ b/lib/components/Login/TokenLogin.dart @@ -4,7 +4,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/components/Login/TokenLoginForms.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; -import 'package:spotube/utils/service_utils.dart'; class TokenLogin extends HookConsumerWidget { const TokenLogin({Key? key}) : super(key: key); diff --git a/lib/components/Lyrics/GeniusLyrics.dart b/lib/components/Lyrics/GeniusLyrics.dart new file mode 100644 index 00000000..c7e39d92 --- /dev/null +++ b/lib/components/Lyrics/GeniusLyrics.dart @@ -0,0 +1,114 @@ +import 'package:fl_query_hooks/fl_query_hooks.dart'; +import 'package:flutter/material.dart'; +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/components/LoaderShimmers/ShimmerLyrics.dart'; +import 'package:spotube/hooks/useBreakpoints.dart'; +import 'package:spotube/provider/Playback.dart'; +import 'package:spotube/provider/SpotifyRequests.dart'; +import 'package:spotube/provider/UserPreferences.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; +import 'package:tuple/tuple.dart'; + +class GeniusLyrics extends HookConsumerWidget { + final PaletteColor palette; + const GeniusLyrics({ + required this.palette, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + Playback playback = ref.watch(playbackProvider); + final geniusLyricsQuery = useQuery( + job: geniusLyricsQueryJob, + externalData: Tuple2( + playback.track, + ref.watch(userPreferencesProvider).geniusAccessToken, + ), + ); + final breakpoint = useBreakpoints(); + final textTheme = Theme.of(context).textTheme; + + useEffect(() { + if (playback.track != null) { + geniusLyricsQuery.setExternalData(Tuple2( + playback.track, + ref.read(userPreferencesProvider).geniusAccessToken, + )); + geniusLyricsQuery.refetch(); + } + return null; + }, [playback.track]); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Text( + playback.track?.name ?? "", + style: breakpoint >= Breakpoints.md + ? textTheme.headline3 + : textTheme.headline4?.copyWith( + fontSize: 25, + color: palette.titleTextColor, + ), + ), + ), + Center( + child: Text( + TypeConversionUtils.artists_X_String( + playback.track?.artists ?? []), + style: (breakpoint >= Breakpoints.md + ? textTheme.headline5 + : textTheme.headline6) + ?.copyWith(color: palette.bodyTextColor), + ), + ), + Expanded( + child: SingleChildScrollView( + child: Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Builder( + builder: (context) { + if (geniusLyricsQuery.isLoading) { + return const ShimmerLyrics(); + } else if (geniusLyricsQuery.hasError) { + return Text( + "Sorry, no Lyrics were found for `${playback.track?.name}` :'(\n${geniusLyricsQuery.error.toString()}", + style: textTheme.bodyText1?.copyWith( + color: palette.bodyTextColor, + ), + ); + } + + final lyrics = geniusLyricsQuery.data; + + return Text( + lyrics == null && playback.track == null + ? "No Track being played currently" + : lyrics ?? "", + style: textTheme.headline6?.copyWith( + color: palette.bodyTextColor, + ), + ); + }, + ), + ), + ), + ), + ), + const Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: EdgeInsets.all(8.0), + child: Text("Powered by genius.com"), + ), + ) + ], + ); + } +} diff --git a/lib/components/Lyrics/Lyrics.dart b/lib/components/Lyrics/Lyrics.dart index aebe37ec..77573eea 100644 --- a/lib/components/Lyrics/Lyrics.dart +++ b/lib/components/Lyrics/Lyrics.dart @@ -1,93 +1,73 @@ -import 'package:fl_query_hooks/fl_query_hooks.dart'; +import 'dart:ui'; + 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/hooks/useBreakpoints.dart'; +import 'package:spotube/components/Lyrics/GeniusLyrics.dart'; +import 'package:spotube/components/Lyrics/SyncedLyrics.dart'; +import 'package:spotube/components/Shared/UniversalImage.dart'; +import 'package:spotube/hooks/useCustomStatusBarColor.dart'; +import 'package:spotube/hooks/usePaletteColor.dart'; import 'package:spotube/provider/Playback.dart'; -import 'package:spotube/provider/SpotifyRequests.dart'; -import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; -import 'package:tuple/tuple.dart'; class Lyrics extends HookConsumerWidget { - final Color? titleBarForegroundColor; - const Lyrics({ - required this.titleBarForegroundColor, - Key? key, - }) : super(key: key); + const Lyrics({Key? key}) : super(key: key); @override Widget build(BuildContext context, ref) { Playback playback = ref.watch(playbackProvider); - final geniusLyricsQuery = useQuery( - job: geniusLyricsQueryJob, - externalData: Tuple2( - playback.track, - ref.watch(userPreferencesProvider).geniusAccessToken, + 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 breakpoint = useBreakpoints(); - final textTheme = Theme.of(context).textTheme; + final palette = usePaletteColor(albumArt, ref); - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Text( - playback.track?.name ?? "", - style: breakpoint >= Breakpoints.md - ? textTheme.headline3 - : textTheme.headline4?.copyWith(fontSize: 25), - ), + useCustomStatusBarColor( + palette.color, + true, + noSetBGColor: true, + ); + + return DefaultTabController( + length: 2, + child: Scaffold( + extendBodyBehindAppBar: true, + appBar: const TabBar( + isScrollable: true, + tabs: [ + Tab(text: "Synced Lyrics"), + Tab(text: "Lyrics (genius.com)"), + ], ), - Center( - child: Text( - TypeConversionUtils.artists_X_String( - playback.track?.artists ?? []), - style: breakpoint >= Breakpoints.md - ? textTheme.headline5 - : textTheme.headline6, + body: Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + image: DecorationImage( + image: UniversalImage.imageProvider(albumArt), + fit: BoxFit.cover, + ), ), - ), - Expanded( - child: SingleChildScrollView( - child: Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Builder( - builder: (context) { - if (geniusLyricsQuery.isLoading) { - return const ShimmerLyrics(); - } else if (geniusLyricsQuery.hasError) { - return Text( - "Sorry, no Lyrics were found for `${playback.track?.name}` :'(\n${geniusLyricsQuery.error.toString()}", - ); - } - - final lyrics = geniusLyricsQuery.data; - - return Text( - lyrics == null && playback.track == null - ? "No Track being played currently" - : lyrics ?? "", - style: textTheme.headline6 - ?.copyWith(color: textTheme.headline1?.color), - ); - }, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: Container( + color: palette.color.withOpacity(.7), + child: SafeArea( + child: TabBarView( + children: [ + SyncedLyrics(palette: palette), + GeniusLyrics(palette: palette), + ], ), ), ), ), ), - const Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: EdgeInsets.all(8.0), - child: Text("Powered by genius.com"), - ), - ) - ], + ), ); } } diff --git a/lib/components/Lyrics/SyncedLyrics.dart b/lib/components/Lyrics/SyncedLyrics.dart index 707bc2f9..042102a8 100644 --- a/lib/components/Lyrics/SyncedLyrics.dart +++ b/lib/components/Lyrics/SyncedLyrics.dart @@ -1,19 +1,14 @@ -import 'dart:ui'; - import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart'; 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/components/LoaderShimmers/ShimmerLyrics.dart'; import 'package:spotube/components/Lyrics/LyricDelayAdjustDialog.dart'; -import 'package:spotube/components/Lyrics/Lyrics.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'; @@ -27,7 +22,11 @@ final lyricDelayState = StateProvider( ); class SyncedLyrics extends HookConsumerWidget { - const SyncedLyrics({Key? key}) : super(key: key); + final PaletteColor palette; + const SyncedLyrics({ + required this.palette, + Key? key, + }) : super(key: key); @override Widget build(BuildContext context, ref) { @@ -40,7 +39,6 @@ class SyncedLyrics extends HookConsumerWidget { final breakpoint = useBreakpoints(); final controller = useAutoScrollController(); - final failed = useState(false); final lyricValue = timedLyricsQuery.data; final lyricsMap = useMemoized( () => @@ -61,197 +59,111 @@ class SyncedLyrics extends HookConsumerWidget { 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 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: [ - 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( - 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()), - ], + return Column( + children: [ + 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( + 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()), + ], ); } } diff --git a/lib/components/Playlist/PlaylistCreateDialog.dart b/lib/components/Playlist/PlaylistCreateDialog.dart index 2451e58d..6cae933b 100644 --- a/lib/components/Playlist/PlaylistCreateDialog.dart +++ b/lib/components/Playlist/PlaylistCreateDialog.dart @@ -13,14 +13,6 @@ class PlaylistCreateDialog extends HookConsumerWidget { final spotify = ref.watch(spotifyProvider); return TextButton( - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Icon(Icons.add_box_rounded, size: 50), - Text("Create Playlist", style: TextStyle(fontSize: 22)), - ], - ), onPressed: () { showDialog( context: context, @@ -106,6 +98,14 @@ class PlaylistCreateDialog extends HookConsumerWidget { padding: MaterialStateProperty.all( const EdgeInsets.symmetric(horizontal: 15, vertical: 100)), ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon(Icons.add_box_rounded, size: 50), + Text("Create Playlist", style: TextStyle(fontSize: 22)), + ], + ), ); } } diff --git a/lib/components/Settings/About.dart b/lib/components/Settings/About.dart index 94dfe253..6a3eaeb1 100644 --- a/lib/components/Settings/About.dart +++ b/lib/components/Settings/About.dart @@ -30,7 +30,7 @@ class About extends HookWidget { ); return ListTile( - leading: Icon(Icons.info_outline_rounded), + leading: const Icon(Icons.info_outline_rounded), title: const Text("About Spotube"), onTap: () { showAboutDialog( diff --git a/lib/components/Settings/Settings.dart b/lib/components/Settings/Settings.dart index 51aadb16..b32307b6 100644 --- a/lib/components/Settings/Settings.dart +++ b/lib/components/Settings/Settings.dart @@ -475,8 +475,8 @@ class Settings extends HookConsumerWidget { icon: const Icon(Icons.favorite_outline_rounded), label: const Text("Please Sponsor/Donate"), style: ElevatedButton.styleFrom( - primary: Colors.red[100], - onPrimary: Colors.pinkAccent, + backgroundColor: Colors.red[100], + foregroundColor: Colors.pinkAccent, padding: const EdgeInsets.all(15), ), onPressed: () { diff --git a/lib/components/Shared/AnchorButton.dart b/lib/components/Shared/AnchorButton.dart index 5af2b20b..ede984e9 100644 --- a/lib/components/Shared/AnchorButton.dart +++ b/lib/components/Shared/AnchorButton.dart @@ -23,6 +23,9 @@ class AnchorButton extends HookWidget { var tap = useState(false); return GestureDetector( + onTapDown: (event) => tap.value = true, + onTapUp: (event) => tap.value = false, + onTap: onTap, child: MouseRegion( cursor: MaterialStateMouseCursor.clickable, child: Text( @@ -37,9 +40,6 @@ class AnchorButton extends HookWidget { onEnter: (event) => hover.value = true, onExit: (event) => hover.value = false, ), - onTapDown: (event) => tap.value = true, - onTapUp: (event) => tap.value = false, - onTap: onTap, ); } } diff --git a/lib/components/Shared/ColoredTabBar.dart b/lib/components/Shared/ColoredTabBar.dart index f528a7c5..a2a8b597 100644 --- a/lib/components/Shared/ColoredTabBar.dart +++ b/lib/components/Shared/ColoredTabBar.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; class ColoredTabBar extends ColoredBox implements PreferredSizeWidget { + @override + // ignore: overridden_fields final TabBar child; const ColoredTabBar({ diff --git a/lib/components/Shared/DownloadConfirmationDialog.dart b/lib/components/Shared/DownloadConfirmationDialog.dart index 0d2093eb..48c96a32 100644 --- a/lib/components/Shared/DownloadConfirmationDialog.dart +++ b/lib/components/Shared/DownloadConfirmationDialog.dart @@ -62,12 +62,12 @@ class DownloadConfirmationDialog extends StatelessWidget { onPressed: () => Navigator.of(context).pop(false), ), ElevatedButton( - child: const Text("Accept"), onPressed: () => Navigator.of(context).pop(true), style: ElevatedButton.styleFrom( - primary: Colors.red, - onPrimary: Colors.white, + foregroundColor: Colors.white, + backgroundColor: Colors.red, ), + child: const Text("Accept"), ) ], ); diff --git a/lib/components/Shared/Hyperlink.dart b/lib/components/Shared/Hyperlink.dart index 3ef61e74..5b3055f8 100644 --- a/lib/components/Shared/Hyperlink.dart +++ b/lib/components/Shared/Hyperlink.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:spotube/components/Shared/AnchorButton.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; class Hyperlink extends StatelessWidget { diff --git a/lib/components/Shared/PageWindowTitleBar.dart b/lib/components/Shared/PageWindowTitleBar.dart index f9a93334..cc39f499 100644 --- a/lib/components/Shared/PageWindowTitleBar.dart +++ b/lib/components/Shared/PageWindowTitleBar.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart'; import 'package:spotube/utils/platform.dart'; diff --git a/lib/hooks/usePaletteColor.dart b/lib/hooks/usePaletteColor.dart index 9809fb26..80fa801a 100644 --- a/lib/hooks/usePaletteColor.dart +++ b/lib/hooks/usePaletteColor.dart @@ -1,4 +1,3 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; diff --git a/lib/main.dart b/lib/main.dart index 35438022..ae6b3876 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -139,10 +139,10 @@ class Spotube extends StatefulHookConsumerWidget { const Spotube({Key? key}) : super(key: key); @override - _SpotubeState createState() => _SpotubeState(); + SpotubeState createState() => SpotubeState(); } -class _SpotubeState extends ConsumerState with WidgetsBindingObserver { +class SpotubeState extends ConsumerState with WidgetsBindingObserver { final logger = getLogger(Spotube); SharedPreferences? localStorage; diff --git a/lib/models/GoRouteDeclarations.dart b/lib/models/GoRouteDeclarations.dart index b368a627..c616c4b6 100644 --- a/lib/models/GoRouteDeclarations.dart +++ b/lib/models/GoRouteDeclarations.dart @@ -8,7 +8,7 @@ import 'package:spotube/components/Home/Shell.dart'; import 'package:spotube/components/Library/UserLibrary.dart'; import 'package:spotube/components/Login/LoginTutorial.dart'; import 'package:spotube/components/Login/TokenLogin.dart'; -import 'package:spotube/components/Lyrics/SyncedLyrics.dart'; +import 'package:spotube/components/Lyrics/Lyrics.dart'; import 'package:spotube/components/Player/PlayerView.dart'; import 'package:spotube/components/Playlist/PlaylistView.dart'; import 'package:spotube/components/Search/Search.dart'; @@ -44,8 +44,7 @@ final router = GoRouter( GoRoute( path: "/lyrics", name: "Lyrics", - pageBuilder: (context, state) => - const SpotubePage(child: SyncedLyrics()), + pageBuilder: (context, state) => const SpotubePage(child: Lyrics()), ), GoRoute( path: "/settings", diff --git a/lib/models/Intents.dart b/lib/models/Intents.dart index 67e4466e..3c0010c1 100644 --- a/lib/models/Intents.dart +++ b/lib/models/Intents.dart @@ -4,8 +4,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/Home/Home.dart'; import 'package:spotube/components/Player/PlayerControls.dart'; +import 'package:spotube/models/GoRouteDeclarations.dart'; import 'package:spotube/models/Logger.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/utils/platform.dart'; @@ -80,19 +80,18 @@ class HomeTabIntent extends Intent { class HomeTabAction extends Action { @override invoke(intent) { - final notifier = intent.ref.read(selectedIndexState.notifier); switch (intent.tab) { case HomeTabs.browse: - notifier.state = 0; + router.go("/"); break; case HomeTabs.search: - notifier.state = 1; + router.go("/search"); break; case HomeTabs.library: - notifier.state = 2; + router.go("/library"); break; case HomeTabs.lyrics: - notifier.state = 3; + router.go("/lyrics"); break; } return null; diff --git a/lib/provider/DBus.dart b/lib/provider/DBus.dart index bd725b73..938d3687 100644 --- a/lib/provider/DBus.dart +++ b/lib/provider/DBus.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:dbus/dbus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotube/utils/platform.dart'; diff --git a/lib/provider/SpotifyRequests.dart b/lib/provider/SpotifyRequests.dart index 1a90728a..f85a82b9 100644 --- a/lib/provider/SpotifyRequests.dart +++ b/lib/provider/SpotifyRequests.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:fl_query/fl_query.dart'; import 'package:spotube/models/LyricsModels.dart'; import 'package:spotube/models/SpotubeTrack.dart'; diff --git a/lib/services/LinuxAudioService.dart b/lib/services/LinuxAudioService.dart index 0b6b2d73..05adf0b9 100644 --- a/lib/services/LinuxAudioService.dart +++ b/lib/services/LinuxAudioService.dart @@ -260,7 +260,7 @@ class _MprisMediaPlayer2Player extends DBusObject { /// Gets value of property org.mpris.MediaPlayer2.Player.Rate Future getRate() async { - return DBusMethodSuccessResponse([DBusDouble(1)]); + return DBusMethodSuccessResponse([const DBusDouble(1)]); } /// Sets property org.mpris.MediaPlayer2.Player.Rate @@ -442,9 +442,12 @@ class _MprisMediaPlayer2Player extends DBusObject { } /// Emits signal org.mpris.MediaPlayer2.Player.Seeked - Future emitSeeked(int Position) async { + Future emitSeeked(int position) async { await emitSignal( - 'org.mpris.MediaPlayer2.Player', 'Seeked', [DBusInt64(Position)]); + 'org.mpris.MediaPlayer2.Player', + 'Seeked', + [DBusInt64(position)], + ); } Future updateProperties(Playback playback) async { diff --git a/lib/utils/duration.dart b/lib/utils/duration.dart index caa92dbe..7d3cd58e 100644 --- a/lib/utils/duration.dart +++ b/lib/utils/duration.dart @@ -6,7 +6,7 @@ Duration parseDuration(String input) { final parts = input.split(':'); - if (parts.length != 3) throw FormatException('Invalid time format'); + if (parts.length != 3) throw const FormatException('Invalid time format'); int days; int hours; @@ -18,7 +18,7 @@ Duration parseDuration(String input) { { final p = parts[2].split('.'); - if (p.length != 2) throw FormatException('Invalid time format'); + if (p.length != 2) throw const FormatException('Invalid time format'); final p2 = int.parse(p[1]); microseconds = p2 % 1000; @@ -38,12 +38,13 @@ Duration parseDuration(String input) { // TODO verify that there are no negative parts return Duration( - days: days, - hours: hours, - minutes: minutes, - seconds: seconds, - milliseconds: milliseconds, - microseconds: microseconds); + days: days, + hours: hours, + minutes: minutes, + seconds: seconds, + milliseconds: milliseconds, + microseconds: microseconds, + ); } Duration? tryParseDuration(String input) { diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 0b54925e..ac51ac5d 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -1,24 +1,19 @@ import 'dart:convert'; -import 'dart:io'; import 'package:flutter/widgets.dart' hide Element; import 'package:go_router/go_router.dart'; import 'package:html/dom.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Library/UserLocalTracks.dart'; -import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/models/Logger.dart'; import 'package:http/http.dart' as http; import 'package:spotube/models/LyricsModels.dart'; import 'package:spotube/models/SpotifySpotubeCredentials.dart'; import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/models/generated_secrets.dart'; -import 'package:spotube/provider/Auth.dart'; import 'package:spotube/utils/primitive_utils.dart'; import 'package:collection/collection.dart'; import 'package:html/parser.dart' as parser; -import 'package:url_launcher/url_launcher.dart'; abstract class ServiceUtils { static final logger = getLogger("ServiceUtils"); @@ -179,109 +174,6 @@ abstract class ServiceUtils { } } - @Deprecated("Use getAccessToken instead") - static Future connectIpc(String authUri, String redirectUri) async { - try { - logger.i("[connectIpc][Launching]: $authUri"); - await launchUrl( - Uri.parse(authUri), - mode: LaunchMode.externalApplication, - ); - - HttpServer server = await HttpServer.bind( - InternetAddress.loopbackIPv4, - 4304, - shared: true, - ); - - logger.i("[connectIpc] Server started"); - - await for (HttpRequest request in server) { - if (request.uri.path == "/auth/spotify/callback" && - request.method == "GET") { - String? code = request.uri.queryParameters["code"]; - if (code != null) { - request.response - ..statusCode = HttpStatus.ok - ..write("Authentication successful. Now Go back to Spotube") - ..close(); - return "$redirectUri?code=$code"; - } else { - request.response - ..statusCode = HttpStatus.forbidden - ..write("Authorization failed start over!") - ..close(); - throw Exception("No code provided"); - } - } - } - } catch (e, stack) { - logger.e("connectIpc", e, stack); - rethrow; - } - return null; - } - - static const authRedirectUri = "http://localhost:4304/auth/spotify/callback"; - - /// Use [getAccessToken] instead - /// This method will be removed in the next major release - @Deprecated("Use getAccessToken instead") - static Future oauthLogin(Auth auth, - {required String clientId, required String clientSecret}) async { - try { - String? accessToken; - String? refreshToken; - DateTime? expiration; - final credentials = SpotifyApiCredentials(clientId, clientSecret); - final grant = SpotifyApi.authorizationCodeGrant(credentials); - - final authUri = grant.getAuthorizationUrl( - Uri.parse(authRedirectUri), - ); - - final responseUri = await connectIpc(authUri.toString(), authRedirectUri); - SharedPreferences localStorage = await SharedPreferences.getInstance(); - if (responseUri != null) { - final SpotifyApi spotify = - SpotifyApi.fromAuthCodeGrant(grant, responseUri); - final credentials = await spotify.getCredentials(); - if (credentials.accessToken != null) { - accessToken = credentials.accessToken; - await localStorage.setString( - LocalStorageKeys.accessToken, credentials.accessToken!); - } - if (credentials.refreshToken != null) { - refreshToken = credentials.refreshToken; - await localStorage.setString( - LocalStorageKeys.refreshToken, credentials.refreshToken!); - } - if (credentials.expiration != null) { - expiration = credentials.expiration; - await localStorage.setString(LocalStorageKeys.expiration, - credentials.expiration?.toString() ?? ""); - } - } - - await localStorage.setString(LocalStorageKeys.clientId, clientId); - await localStorage.setString( - LocalStorageKeys.clientSecret, - clientSecret, - ); - - // auth.setAuthState( - // clientId: clientId, - // clientSecret: clientSecret, - // accessToken: accessToken, - // refreshToken: refreshToken, - // expiration: expiration, - // ); - } catch (e, stack) { - logger.e("oauthLogin", e, stack); - rethrow; - } - } - static const baseUri = "https://www.rentanadviser.com/subtitles"; static Future getTimedLyrics(SpotubeTrack track) async { diff --git a/pubspec.lock b/pubspec.lock index 4850ec60..65215c29 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1300,7 +1300,7 @@ packages: source: hosted version: "3.0.1" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index 95f8b281..6f39e7e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,7 @@ dependencies: fl_query_hooks: ^0.3.1 flutter_inappwebview: ^5.4.3+7 tuple: ^2.0.1 + uuid: ^3.0.6 dev_dependencies: flutter_test: diff --git a/test/widget_test.dart b/test/widget_test.dart index b18f4962..46ff535e 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -13,7 +13,7 @@ import 'package:spotube/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(Spotube()); + await tester.pumpWidget(const Spotube()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget);