mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat(deep-link): add track opening page
This commit is contained in:
parent
d1ed56926d
commit
988a975bf1
@ -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(
|
||||
|
@ -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),
|
||||
),
|
||||
|
@ -8,6 +8,7 @@ class LinkText<T> 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<T> extends StatelessWidget {
|
||||
this.extra,
|
||||
this.overflow,
|
||||
this.style = const TextStyle(),
|
||||
this.maxLines,
|
||||
this.push = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -37,6 +39,7 @@ class LinkText<T> extends StatelessWidget {
|
||||
overflow: overflow,
|
||||
style: style,
|
||||
textAlign: textAlign,
|
||||
maxLines: maxLines,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -43,12 +43,14 @@ class TrackOptions extends HookConsumerWidget {
|
||||
final bool userPlaylist;
|
||||
final String? playlistId;
|
||||
final ObjectRef<ValueChanged<RelativeRect>?>? 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,
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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",
|
||||
|
@ -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(
|
||||
|
227
lib/pages/track/track.dart
Normal file
227
lib/pages/track/track.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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._();
|
||||
|
16
lib/services/queries/tracks.dart
Normal file
16
lib/services/queries/tracks.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
56
pubspec.lock
56
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:
|
||||
|
Loading…
Reference in New Issue
Block a user