From 988a975bf1a675df0cfc7b17776bcec74c67f1f2 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 5 Jan 2024 14:14:15 +0600 Subject: [PATCH] feat(deep-link): add track opening page --- lib/collections/routes.dart | 10 + .../player/player_track_details.dart | 11 +- lib/components/shared/links/link_text.dart | 3 + .../shared/track_tile/track_options.dart | 4 +- .../shared/track_tile/track_tile.dart | 4 +- lib/hooks/configurators/use_deep_linking.dart | 8 + lib/pages/artist/artist.dart | 2 +- lib/pages/track/track.dart | 227 ++++++++++++++++++ lib/services/queries/queries.dart | 2 + lib/services/queries/tracks.dart | 16 ++ pubspec.lock | 56 +++-- 11 files changed, 317 insertions(+), 26 deletions(-) create mode 100644 lib/pages/track/track.dart create mode 100644 lib/services/queries/tracks.dart diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index 7816f204..3e2c42e0 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -17,6 +17,7 @@ import 'package:spotube/pages/search/search.dart'; import 'package:spotube/pages/settings/blacklist.dart'; import 'package:spotube/pages/settings/about.dart'; import 'package:spotube/pages/settings/logs.dart'; +import 'package:spotube/pages/track/track.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/components/shared/spotube_page_route.dart'; import 'package:spotube/pages/artist/artist.dart'; @@ -144,6 +145,15 @@ final router = GoRouter( ); }, ), + GoRoute( + path: "/track/:id", + pageBuilder: (context, state) { + final id = state.pathParameters["id"]!; + return SpotubePage( + child: TrackPage(trackId: id), + ); + }, + ), ], ), GoRoute( diff --git a/lib/components/player/player_track_details.dart b/lib/components/player/player_track_details.dart index d6f275fa..66cb9ef5 100644 --- a/lib/components/player/player_track_details.dart +++ b/lib/components/player/player_track_details.dart @@ -4,6 +4,7 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/shared/links/link_text.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -44,10 +45,12 @@ class PlayerTrackDetails extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), - Text( + LinkText( playback.activeTrack?.name ?? "", + "/track/${playback.activeTrack?.id}", + push: true, overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodyMedium?.copyWith( + style: theme.textTheme.bodyMedium!.copyWith( color: color, ), ), @@ -66,8 +69,10 @@ class PlayerTrackDetails extends HookConsumerWidget { flex: 1, child: Column( children: [ - Text( + LinkText( playback.activeTrack?.name ?? "", + "/track/${playback.activeTrack?.id}", + push: true, overflow: TextOverflow.ellipsis, style: TextStyle(fontWeight: FontWeight.bold, color: color), ), diff --git a/lib/components/shared/links/link_text.dart b/lib/components/shared/links/link_text.dart index 217b247d..d7b00b72 100644 --- a/lib/components/shared/links/link_text.dart +++ b/lib/components/shared/links/link_text.dart @@ -8,6 +8,7 @@ class LinkText extends StatelessWidget { final TextAlign? textAlign; final TextOverflow? overflow; final String route; + final int? maxLines; final T? extra; final bool push; @@ -19,6 +20,7 @@ class LinkText extends StatelessWidget { this.extra, this.overflow, this.style = const TextStyle(), + this.maxLines, this.push = false, }) : super(key: key); @@ -37,6 +39,7 @@ class LinkText extends StatelessWidget { overflow: overflow, style: style, textAlign: textAlign, + maxLines: maxLines, ); } } diff --git a/lib/components/shared/track_tile/track_options.dart b/lib/components/shared/track_tile/track_options.dart index 8405d6ea..724bc029 100644 --- a/lib/components/shared/track_tile/track_options.dart +++ b/lib/components/shared/track_tile/track_options.dart @@ -43,12 +43,14 @@ class TrackOptions extends HookConsumerWidget { final bool userPlaylist; final String? playlistId; final ObjectRef?>? showMenuCbRef; + final Widget? icon; const TrackOptions({ Key? key, required this.track, this.showMenuCbRef, this.userPlaylist = false, this.playlistId, + this.icon, }) : super(key: key); void actionShare(BuildContext context, Track track) { @@ -207,7 +209,7 @@ class TrackOptions extends HookConsumerWidget { break; } }, - icon: const Icon(SpotubeIcons.moreHorizontal), + icon: icon ?? const Icon(SpotubeIcons.moreHorizontal), headings: [ ListTile( dense: true, diff --git a/lib/components/shared/track_tile/track_tile.dart b/lib/components/shared/track_tile/track_tile.dart index 961f29c9..c3b03f3c 100644 --- a/lib/components/shared/track_tile/track_tile.dart +++ b/lib/components/shared/track_tile/track_tile.dart @@ -193,8 +193,10 @@ class TrackTile extends HookConsumerWidget { children: [ Expanded( flex: 6, - child: Text( + child: LinkText( track.name!, + "/track/${track.id}", + push: true, maxLines: 1, overflow: TextOverflow.ellipsis, ), diff --git a/lib/hooks/configurators/use_deep_linking.dart b/lib/hooks/configurators/use_deep_linking.dart index be6facf9..3b7ec3f3 100644 --- a/lib/hooks/configurators/use_deep_linking.dart +++ b/lib/hooks/configurators/use_deep_linking.dart @@ -48,6 +48,11 @@ void useDeepLinking(WidgetRef ref) { ), ); break; + case "track": + router.push( + "/track/${url.pathSegments.last}", + ); + break; default: break; } @@ -80,6 +85,9 @@ void useDeepLinking(WidgetRef ref) { case "spotify:artist": await router.push("/artist/$endSegment"); break; + case "spotify:track": + await router.push("/track/$endSegment"); + break; case "spotify:playlist": await router.push( "/playlist/$endSegment", diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index 92470397..d511cb97 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -35,7 +35,7 @@ class ArtistPage extends HookConsumerWidget { ), extendBodyBehindAppBar: true, body: Builder(builder: (context) { - if (artistQuery.hasError) { + if (artistQuery.hasError && artistQuery.data == null) { return Center(child: Text(artistQuery.error.toString())); } return Skeletonizer( diff --git a/lib/pages/track/track.dart b/lib/pages/track/track.dart new file mode 100644 index 00000000..14052c10 --- /dev/null +++ b/lib/pages/track/track.dart @@ -0,0 +1,227 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:spotube/collections/fake.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/shared/heart_button.dart'; +import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/shared/links/link_text.dart'; +import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/shared/track_tile/track_options.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/queries/queries.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; +import 'package:spotube/extensions/constrains.dart'; + +class TrackPage extends HookConsumerWidget { + final String trackId; + const TrackPage({ + Key? key, + required this.trackId, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final ThemeData(:textTheme, :colorScheme) = Theme.of(context); + final mediaQuery = MediaQuery.of(context); + + final playlist = ref.watch(ProxyPlaylistNotifier.provider); + final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); + + final isActive = playlist.activeTrack?.id == trackId; + + final trackQuery = useQueries.tracks.track(ref, trackId); + + final track = trackQuery.data ?? FakeData.track; + + void onPlay() async { + if (isActive) { + audioPlayer.pause(); + } else { + await playlistNotifier.load([track], autoPlay: true); + } + } + + return Scaffold( + appBar: const PageWindowTitleBar( + automaticallyImplyLeading: true, + backgroundColor: Colors.transparent, + ), + extendBodyBehindAppBar: true, + body: Stack( + children: [ + Positioned.fill( + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: UniversalImage.imageProvider( + TypeConversionUtils.image_X_UrlString( + track.album!.images, + placeholder: ImagePlaceholder.albumArt, + ), + ), + fit: BoxFit.cover, + colorFilter: ColorFilter.mode( + colorScheme.surface.withOpacity(0.5), + BlendMode.srcOver, + ), + alignment: Alignment.topCenter, + ), + ), + ), + ), + Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Skeletonizer( + enabled: trackQuery.isLoading, + child: Container( + alignment: Alignment.topCenter, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + colorScheme.surface, + Colors.transparent, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + stops: const [0.2, 1], + ), + ), + child: SafeArea( + child: Wrap( + spacing: 20, + runSpacing: 20, + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + runAlignment: WrapAlignment.center, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: UniversalImage( + path: TypeConversionUtils.image_X_UrlString( + track.album!.images, + placeholder: ImagePlaceholder.albumArt, + ), + height: 200, + width: 200, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: mediaQuery.smAndDown + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + track.name!, + style: textTheme.titleLarge, + ), + const Gap(10), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(SpotubeIcons.album), + const Gap(5), + Flexible( + child: LinkText( + track.album!.name!, + '/album/${track.album!.id}', + push: true, + extra: track.album, + ), + ), + ], + ), + const Gap(10), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(SpotubeIcons.artist), + const Gap(5), + TypeConversionUtils + .artists_X_ClickableArtists( + track.artists!, + ), + ], + ), + const Gap(10), + ConstrainedBox( + constraints: + const BoxConstraints(maxWidth: 350), + child: Row( + mainAxisSize: mediaQuery.smAndDown + ? MainAxisSize.max + : MainAxisSize.min, + children: [ + const Gap(5), + if (!isActive && + !playlist.tracks.contains(track)) + OutlinedButton.icon( + icon: const Icon(SpotubeIcons.queueAdd), + label: Text(context.l10n.queue), + onPressed: () { + playlistNotifier.addTrack(track); + }, + ), + const Gap(5), + if (!isActive && + !playlist.tracks.contains(track)) + IconButton.outlined( + icon: + const Icon(SpotubeIcons.lightning), + tooltip: context.l10n.play_next, + onPressed: () { + playlistNotifier + .addTracksAtFirst([track]); + }, + ), + const Gap(5), + IconButton.filled( + tooltip: isActive + ? context.l10n.pause_playback + : context.l10n.play, + icon: Icon( + isActive + ? SpotubeIcons.pause + : SpotubeIcons.play, + color: colorScheme.onPrimary, + ), + onPressed: onPlay, + ), + const Gap(5), + if (mediaQuery.smAndDown) + const Spacer() + else + const Gap(20), + TrackHeartButton(track: track), + TrackOptions( + track: track, + userPlaylist: false, + ), + const Gap(5), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/services/queries/queries.dart b/lib/services/queries/queries.dart index cc3ce132..30c23268 100644 --- a/lib/services/queries/queries.dart +++ b/lib/services/queries/queries.dart @@ -4,6 +4,7 @@ import 'package:spotube/services/queries/category.dart'; import 'package:spotube/services/queries/lyrics.dart'; import 'package:spotube/services/queries/playlist.dart'; import 'package:spotube/services/queries/search.dart'; +import 'package:spotube/services/queries/tracks.dart'; import 'package:spotube/services/queries/user.dart'; import 'package:spotube/services/queries/views.dart'; @@ -17,6 +18,7 @@ class Queries { final search = const SearchQueries(); final user = const UserQueries(); final views = const ViewsQueries(); + final tracks = const TracksQueries(); } const useQueries = Queries._(); diff --git a/lib/services/queries/tracks.dart b/lib/services/queries/tracks.dart new file mode 100644 index 00000000..52bab984 --- /dev/null +++ b/lib/services/queries/tracks.dart @@ -0,0 +1,16 @@ +import 'package:fl_query/fl_query.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/hooks/spotify/use_spotify_query.dart'; + +class TracksQueries { + const TracksQueries(); + + Query track(WidgetRef ref, String id) { + return useSpotifyQuery( + "track/$id", + (spotify) => spotify.tracks.get(id), + ref: ref, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 526898d5..b4182d12 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -543,10 +543,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" file_picker: dependency: "direct main" description: @@ -1255,6 +1255,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "04be76c4a4bb50f14904e64749237e541e7c7bcf7ec0b196907322ab5d2fc739" + url: "https://pub.dev" + source: hosted + version: "9.0.16" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: b06739349ec2477e943055aea30172c5c7000225f79dad4702e2ec0eda79a6ff + url: "https://pub.dev" + source: hosted + version: "1.0.5" lints: dependency: transitive description: @@ -1307,10 +1323,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" media_kit: dependency: "direct main" description: @@ -1379,10 +1395,10 @@ packages: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" metadata_god: dependency: "direct main" description: @@ -1595,10 +1611,10 @@ packages: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" plugin_platform_interface: dependency: transitive description: @@ -1635,10 +1651,10 @@ packages: dependency: transitive description: name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + sha256: "266ca5be5820feefc777793d0a583acfc8c40834893c87c00c6c09e2cf58ea42" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "5.0.1" provider: dependency: transitive description: @@ -1780,10 +1796,10 @@ packages: dependency: transitive description: name: shared_preferences_linux - sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shared_preferences_platform_interface: dependency: transitive description: @@ -1804,10 +1820,10 @@ packages: dependency: transitive description: name: shared_preferences_windows - sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shelf: dependency: transitive description: @@ -2217,10 +2233,10 @@ packages: dependency: transitive description: name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "11.10.0" + version: "13.0.0" watcher: dependency: transitive description: @@ -2233,10 +2249,10 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: edc8a9573dd8c5a83a183dae1af2b6fd4131377404706ca4e5420474784906fa url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.4.0" web_socket_channel: dependency: transitive description: @@ -2249,10 +2265,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" wikipedia_api: dependency: "direct main" description: