feat(deep-link): add track opening page

This commit is contained in:
Kingkor Roy Tirtho 2024-01-05 14:14:15 +06:00
parent d1ed56926d
commit 988a975bf1
11 changed files with 317 additions and 26 deletions

View File

@ -17,6 +17,7 @@ import 'package:spotube/pages/search/search.dart';
import 'package:spotube/pages/settings/blacklist.dart'; import 'package:spotube/pages/settings/blacklist.dart';
import 'package:spotube/pages/settings/about.dart'; import 'package:spotube/pages/settings/about.dart';
import 'package:spotube/pages/settings/logs.dart'; import 'package:spotube/pages/settings/logs.dart';
import 'package:spotube/pages/track/track.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/components/shared/spotube_page_route.dart'; import 'package:spotube/components/shared/spotube_page_route.dart';
import 'package:spotube/pages/artist/artist.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( GoRoute(

View File

@ -4,6 +4,7 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/components/shared/image/universal_image.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/extensions/constrains.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
@ -44,10 +45,12 @@ class PlayerTrackDetails extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 4), const SizedBox(height: 4),
Text( LinkText(
playback.activeTrack?.name ?? "", playback.activeTrack?.name ?? "",
"/track/${playback.activeTrack?.id}",
push: true,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium!.copyWith(
color: color, color: color,
), ),
), ),
@ -66,8 +69,10 @@ class PlayerTrackDetails extends HookConsumerWidget {
flex: 1, flex: 1,
child: Column( child: Column(
children: [ children: [
Text( LinkText(
playback.activeTrack?.name ?? "", playback.activeTrack?.name ?? "",
"/track/${playback.activeTrack?.id}",
push: true,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.bold, color: color), style: TextStyle(fontWeight: FontWeight.bold, color: color),
), ),

View File

@ -8,6 +8,7 @@ class LinkText<T> extends StatelessWidget {
final TextAlign? textAlign; final TextAlign? textAlign;
final TextOverflow? overflow; final TextOverflow? overflow;
final String route; final String route;
final int? maxLines;
final T? extra; final T? extra;
final bool push; final bool push;
@ -19,6 +20,7 @@ class LinkText<T> extends StatelessWidget {
this.extra, this.extra,
this.overflow, this.overflow,
this.style = const TextStyle(), this.style = const TextStyle(),
this.maxLines,
this.push = false, this.push = false,
}) : super(key: key); }) : super(key: key);
@ -37,6 +39,7 @@ class LinkText<T> extends StatelessWidget {
overflow: overflow, overflow: overflow,
style: style, style: style,
textAlign: textAlign, textAlign: textAlign,
maxLines: maxLines,
); );
} }
} }

View File

@ -43,12 +43,14 @@ class TrackOptions extends HookConsumerWidget {
final bool userPlaylist; final bool userPlaylist;
final String? playlistId; final String? playlistId;
final ObjectRef<ValueChanged<RelativeRect>?>? showMenuCbRef; final ObjectRef<ValueChanged<RelativeRect>?>? showMenuCbRef;
final Widget? icon;
const TrackOptions({ const TrackOptions({
Key? key, Key? key,
required this.track, required this.track,
this.showMenuCbRef, this.showMenuCbRef,
this.userPlaylist = false, this.userPlaylist = false,
this.playlistId, this.playlistId,
this.icon,
}) : super(key: key); }) : super(key: key);
void actionShare(BuildContext context, Track track) { void actionShare(BuildContext context, Track track) {
@ -207,7 +209,7 @@ class TrackOptions extends HookConsumerWidget {
break; break;
} }
}, },
icon: const Icon(SpotubeIcons.moreHorizontal), icon: icon ?? const Icon(SpotubeIcons.moreHorizontal),
headings: [ headings: [
ListTile( ListTile(
dense: true, dense: true,

View File

@ -193,8 +193,10 @@ class TrackTile extends HookConsumerWidget {
children: [ children: [
Expanded( Expanded(
flex: 6, flex: 6,
child: Text( child: LinkText(
track.name!, track.name!,
"/track/${track.id}",
push: true,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),

View File

@ -48,6 +48,11 @@ void useDeepLinking(WidgetRef ref) {
), ),
); );
break; break;
case "track":
router.push(
"/track/${url.pathSegments.last}",
);
break;
default: default:
break; break;
} }
@ -80,6 +85,9 @@ void useDeepLinking(WidgetRef ref) {
case "spotify:artist": case "spotify:artist":
await router.push("/artist/$endSegment"); await router.push("/artist/$endSegment");
break; break;
case "spotify:track":
await router.push("/track/$endSegment");
break;
case "spotify:playlist": case "spotify:playlist":
await router.push( await router.push(
"/playlist/$endSegment", "/playlist/$endSegment",

View File

@ -35,7 +35,7 @@ class ArtistPage extends HookConsumerWidget {
), ),
extendBodyBehindAppBar: true, extendBodyBehindAppBar: true,
body: Builder(builder: (context) { body: Builder(builder: (context) {
if (artistQuery.hasError) { if (artistQuery.hasError && artistQuery.data == null) {
return Center(child: Text(artistQuery.error.toString())); return Center(child: Text(artistQuery.error.toString()));
} }
return Skeletonizer( return Skeletonizer(

227
lib/pages/track/track.dart Normal file
View File

@ -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),
],
),
),
],
),
),
],
),
),
),
),
),
),
],
),
);
}
}

View File

@ -4,6 +4,7 @@ import 'package:spotube/services/queries/category.dart';
import 'package:spotube/services/queries/lyrics.dart'; import 'package:spotube/services/queries/lyrics.dart';
import 'package:spotube/services/queries/playlist.dart'; import 'package:spotube/services/queries/playlist.dart';
import 'package:spotube/services/queries/search.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/user.dart';
import 'package:spotube/services/queries/views.dart'; import 'package:spotube/services/queries/views.dart';
@ -17,6 +18,7 @@ class Queries {
final search = const SearchQueries(); final search = const SearchQueries();
final user = const UserQueries(); final user = const UserQueries();
final views = const ViewsQueries(); final views = const ViewsQueries();
final tracks = const TracksQueries();
} }
const useQueries = Queries._(); const useQueries = Queries._();

View File

@ -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, dynamic> track(WidgetRef ref, String id) {
return useSpotifyQuery(
"track/$id",
(spotify) => spotify.tracks.get(id),
ref: ref,
);
}
}

View File

@ -543,10 +543,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file name: file
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.4" version: "7.0.0"
file_picker: file_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1255,6 +1255,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.2" 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: lints:
dependency: transitive dependency: transitive
description: description:
@ -1307,10 +1323,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.0" version: "0.8.0"
media_kit: media_kit:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1379,10 +1395,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.0" version: "1.11.0"
metadata_god: metadata_god:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1595,10 +1611,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: platform name: platform
sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.2" version: "3.1.3"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -1635,10 +1651,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: process name: process
sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" sha256: "266ca5be5820feefc777793d0a583acfc8c40834893c87c00c6c09e2cf58ea42"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.4" version: "5.0.1"
provider: provider:
dependency: transitive dependency: transitive
description: description:
@ -1780,10 +1796,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_linux name: shared_preferences_linux
sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.1" version: "2.3.2"
shared_preferences_platform_interface: shared_preferences_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -1804,10 +1820,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_windows name: shared_preferences_windows
sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.1" version: "2.3.2"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:
@ -2217,10 +2233,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "11.10.0" version: "13.0.0"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
@ -2233,10 +2249,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: web name: web
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 sha256: edc8a9573dd8c5a83a183dae1af2b6fd4131377404706ca4e5420474784906fa
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.0" version: "0.4.0"
web_socket_channel: web_socket_channel:
dependency: transitive dependency: transitive
description: description:
@ -2249,10 +2265,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: webdriver name: webdriver
sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.2" version: "3.0.3"
wikipedia_api: wikipedia_api:
dependency: "direct main" dependency: "direct main"
description: description: