diff --git a/lib/components/player/player_track_details.dart b/lib/components/player/player_track_details.dart index 5ffaae902..8b66b8b71 100644 --- a/lib/components/player/player_track_details.dart +++ b/lib/components/player/player_track_details.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; @@ -6,6 +7,7 @@ import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class PlayerTrackDetails extends HookConsumerWidget { @@ -72,6 +74,9 @@ class PlayerTrackDetails extends HookConsumerWidget { ), TypeConversionUtils.artists_X_ClickableArtists( playback.activeTrack?.artists ?? [], + onRouteChange: (route) { + ServiceUtils.push(context, route); + }, ) ], ), diff --git a/lib/components/root/bottom_player.dart b/lib/components/root/bottom_player.dart index 073086c31..891da2c13 100644 --- a/lib/components/root/bottom_player.dart +++ b/lib/components/root/bottom_player.dart @@ -20,7 +20,6 @@ import 'package:flutter/material.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/volume_provider.dart'; -import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; diff --git a/lib/components/shared/dialogs/track_details_dialog.dart b/lib/components/shared/dialogs/track_details_dialog.dart new file mode 100644 index 000000000..09e71e567 --- /dev/null +++ b/lib/components/shared/dialogs/track_details_dialog.dart @@ -0,0 +1,166 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/shared/links/hyper_link.dart'; +import 'package:spotube/components/shared/links/link_text.dart'; +import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/models/spotube_track.dart'; +import 'package:spotube/utils/primitive_utils.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; +import 'package:spotube/extensions/duration.dart'; + +class TrackDetailsDialog extends HookWidget { + final Track track; + const TrackDetailsDialog({ + Key? key, + required this.track, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final mediaQuery = MediaQuery.of(context); + + final detailsMap = { + context.l10n.title: track.name!, + context.l10n.artist: TypeConversionUtils.artists_X_ClickableArtists( + track.artists ?? [], + mainAxisAlignment: WrapAlignment.start, + textStyle: const TextStyle(color: Colors.blue), + ), + context.l10n.album: LinkText( + track.album!.name!, + "/album/${track.album?.id}", + extra: track.album, + overflow: TextOverflow.ellipsis, + style: const TextStyle(color: Colors.blue), + ), + context.l10n.duration: (track is SpotubeTrack + ? (track as SpotubeTrack).ytTrack.duration + : track.duration!) + .toHumanReadableString(), + if (track.album!.releaseDate != null) + context.l10n.released: track.album!.releaseDate, + context.l10n.popularity: track.popularity?.toString() ?? "0", + }; + + final ytTrack = + track is SpotubeTrack ? (track as SpotubeTrack).ytTrack : null; + + final ytTracksDetailsMap = ytTrack == null + ? {} + : { + context.l10n.youtube: Hyperlink( + "https://piped.video/watch?v=${ytTrack.id}", + "https://piped.video/watch?v=${ytTrack.id}", + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + context.l10n.channel: Hyperlink( + ytTrack.uploader, + "https://youtube.com${ytTrack.uploaderUrl}", + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + context.l10n.likes: + PrimitiveUtils.toReadableNumber(ytTrack.likes.toDouble()), + context.l10n.dislikes: + PrimitiveUtils.toReadableNumber(ytTrack.dislikes.toDouble()), + context.l10n.views: + PrimitiveUtils.toReadableNumber(ytTrack.views.toDouble()), + context.l10n.streamUrl: Hyperlink( + (track as SpotubeTrack).ytUri, + (track as SpotubeTrack).ytUri, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + }; + + return AlertDialog( + contentPadding: const EdgeInsets.all(16), + insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 100), + scrollable: true, + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(SpotubeIcons.info), + const SizedBox(width: 8), + Text( + context.l10n.details, + style: theme.textTheme.titleMedium, + ), + ], + ), + content: SizedBox( + width: mediaQuery.mdAndUp ? double.infinity : 700, + child: Table( + columnWidths: const { + 0: FixedColumnWidth(95), + 1: FixedColumnWidth(10), + 2: FlexColumnWidth(1), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + for (final entry in detailsMap.entries) + TableRow( + children: [ + TableCell( + verticalAlignment: TableCellVerticalAlignment.top, + child: Text( + entry.key, + style: theme.textTheme.titleMedium, + ), + ), + const TableCell( + verticalAlignment: TableCellVerticalAlignment.top, + child: Text(":"), + ), + if (entry.value is Widget) + entry.value as Widget + else + Text( + entry.value, + style: theme.textTheme.bodyMedium, + ), + ], + ), + const TableRow( + children: [ + SizedBox(height: 16), + SizedBox(height: 16), + SizedBox(height: 16), + ], + ), + for (final entry in ytTracksDetailsMap.entries) + TableRow( + children: [ + TableCell( + verticalAlignment: TableCellVerticalAlignment.top, + child: Text( + entry.key, + style: theme.textTheme.titleMedium, + ), + ), + const TableCell( + verticalAlignment: TableCellVerticalAlignment.top, + child: Text(":"), + ), + if (entry.value is Widget) + entry.value as Widget + else + Text( + entry.value, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodyMedium, + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/components/shared/links/anchor_button.dart b/lib/components/shared/links/anchor_button.dart index ede984e9a..b1b1cfea0 100644 --- a/lib/components/shared/links/anchor_button.dart +++ b/lib/components/shared/links/anchor_button.dart @@ -7,6 +7,7 @@ class AnchorButton extends HookWidget { final TextAlign? textAlign; final TextOverflow? overflow; final void Function()? onTap; + final int? maxLines; const AnchorButton( this.text, { @@ -14,6 +15,7 @@ class AnchorButton extends HookWidget { this.onTap, this.textAlign, this.overflow, + this.maxLines, this.style = const TextStyle(), }) : super(key: key); @@ -34,6 +36,7 @@ class AnchorButton extends HookWidget { decoration: hover.value || tap.value ? TextDecoration.underline : null, ), + maxLines: maxLines, textAlign: textAlign, overflow: overflow, ), diff --git a/lib/components/shared/links/hyper_link.dart b/lib/components/shared/links/hyper_link.dart index 88d4b2b93..fd31298e6 100644 --- a/lib/components/shared/links/hyper_link.dart +++ b/lib/components/shared/links/hyper_link.dart @@ -8,6 +8,8 @@ class Hyperlink extends StatelessWidget { final TextAlign? textAlign; final TextOverflow? overflow; final String url; + final int? maxLines; + const Hyperlink( this.text, this.url, { @@ -15,6 +17,7 @@ class Hyperlink extends StatelessWidget { this.textAlign, this.overflow, this.style = const TextStyle(), + this.maxLines, }) : super(key: key); @override @@ -29,6 +32,7 @@ class Hyperlink extends StatelessWidget { }, key: key, overflow: overflow, + maxLines: maxLines, style: style.copyWith(color: Colors.blue), textAlign: textAlign, ); diff --git a/lib/components/shared/links/link_text.dart b/lib/components/shared/links/link_text.dart index 710cfa81a..217b247db 100644 --- a/lib/components/shared/links/link_text.dart +++ b/lib/components/shared/links/link_text.dart @@ -9,6 +9,8 @@ class LinkText extends StatelessWidget { final TextOverflow? overflow; final String route; final T? extra; + + final bool push; const LinkText( this.text, this.route, { @@ -17,6 +19,7 @@ class LinkText extends StatelessWidget { this.extra, this.overflow, this.style = const TextStyle(), + this.push = false, }) : super(key: key); @override @@ -24,7 +27,11 @@ class LinkText extends StatelessWidget { return AnchorButton( text, onTap: () { - ServiceUtils.navigate(context, route, extra: extra); + if (push) { + ServiceUtils.push(context, route, extra: extra); + } else { + ServiceUtils.navigate(context, route, extra: extra); + } }, key: key, overflow: overflow, diff --git a/lib/components/shared/track_table/track_options.dart b/lib/components/shared/track_table/track_options.dart index 6e31f840a..8dc094ad0 100644 --- a/lib/components/shared/track_table/track_options.dart +++ b/lib/components/shared/track_table/track_options.dart @@ -9,6 +9,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/library/user_local_tracks.dart'; import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart'; +import 'package:spotube/components/shared/dialogs/track_details_dialog.dart'; import 'package:spotube/components/shared/heart_button.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/extensions/context.dart'; @@ -29,6 +30,7 @@ enum TrackOptionValue { delete, playNext, favorite, + details, } class TrackOptions extends HookConsumerWidget { @@ -163,6 +165,12 @@ class TrackOptions extends HookConsumerWidget { case TrackOptionValue.share: actionShare(context, track); break; + case TrackOptionValue.details: + showDialog( + context: context, + builder: (context) => TrackDetailsDialog(track: track), + ); + break; } }, icon: const Icon(SpotubeIcons.moreHorizontal), @@ -288,7 +296,14 @@ class TrackOptions extends HookConsumerWidget { leading: const Icon(SpotubeIcons.share), title: Text(context.l10n.share), ), - ) + ), + PopSheetEntry( + value: TrackOptionValue.details, + child: ListTile( + leading: const Icon(SpotubeIcons.info), + title: Text(context.l10n.details), + ), + ), ] }, ), diff --git a/lib/components/shared/track_table/track_tile.dart b/lib/components/shared/track_table/track_tile.dart index c3be9d65d..62d335144 100644 --- a/lib/components/shared/track_table/track_tile.dart +++ b/lib/components/shared/track_table/track_tile.dart @@ -176,6 +176,7 @@ class TrackTile extends HookConsumerWidget { track.album!.name!, "/album/${track.album?.id}", extra: track.album, + push: true, overflow: TextOverflow.ellipsis, ), ) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c4b24eaba..c820e5889 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -230,5 +230,12 @@ "download_agreement_2": "I'll support the Artist wherever I can and I'm only doing this because I don't have money to buy their art", "download_agreement_3": "I'm completely aware that my IP can get blocked on YouTube & I don't hold Spotube or his owners/contributors responsible for any accidents caused by my current action", "decline": "Decline", - "accept": "Accept" + "accept": "Accept", + "details": "Details", + "youtube": "YouTube", + "channel": "Channel", + "likes": "Likes", + "dislikes": "Dislikes", + "views": "Views", + "streamUrl": "Stream URL" } \ No newline at end of file diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 51464fd30..eb49270f5 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -111,10 +111,22 @@ class SyncedLyrics extends HookConsumerWidget { index: index, controller: controller, child: lyricSlice.text.isEmpty - ? Container() + ? Container( + padding: index == lyricValue.lyrics.length - 1 + ? EdgeInsets.only( + bottom: + MediaQuery.of(context).size.height / + 2, + ) + : null, + ) : Center( child: Padding( - padding: const EdgeInsets.all(8.0), + padding: index == lyricValue.lyrics.length - 1 + ? const EdgeInsets.all(8.0).copyWith( + bottom: 100, + ) + : const EdgeInsets.all(8.0), child: AnimatedDefaultTextStyle( duration: const Duration(milliseconds: 250), style: TextStyle( diff --git a/lib/pages/player/player.dart b/lib/pages/player/player.dart index 2831b71a5..9234d82c2 100644 --- a/lib/pages/player/player.dart +++ b/lib/pages/player/player.dart @@ -10,9 +10,11 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/player/player_actions.dart'; import 'package:spotube/components/player/player_controls.dart'; import 'package:spotube/components/shared/animated_gradient.dart'; +import 'package:spotube/components/shared/dialogs/track_details_dialog.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/use_custom_status_bar_color.dart'; import 'package:spotube/hooks/use_palette_color.dart'; import 'package:spotube/models/local_track.dart'; @@ -106,29 +108,27 @@ class PlayerView extends HookConsumerWidget { padding: const EdgeInsets.all(8.0), child: Column( children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Container( - constraints: const BoxConstraints( - maxHeight: 300, maxWidth: 300), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - spreadRadius: 2, - blurRadius: 10, - offset: Offset(0, 0), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(20), - child: UniversalImage( - path: albumArt, - placeholder: Assets.albumPlaceholder.path, - fit: BoxFit.cover, + Container( + margin: const EdgeInsets.all(8), + constraints: const BoxConstraints( + maxHeight: 300, maxWidth: 300), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + spreadRadius: 2, + blurRadius: 10, + offset: Offset(0, 0), ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: UniversalImage( + path: albumArt, + placeholder: Assets.albumPlaceholder.path, + fit: BoxFit.cover, ), ), ), @@ -183,38 +183,70 @@ class PlayerView extends HookConsumerWidget { PlayerActions( mainAxisAlignment: MainAxisAlignment.spaceEvenly, floatingQueue: false, - extraActions: [ - if (auth != null) - IconButton( - tooltip: "Open Lyrics", - icon: const Icon(SpotubeIcons.music), - onPressed: () { - showModalBottomSheet( - context: context, - isDismissible: true, - enableDrag: true, - isScrollControlled: true, - backgroundColor: Colors.black38, - barrierColor: Colors.black12, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - constraints: BoxConstraints( - maxHeight: - MediaQuery.of(context).size.height * - 0.8, - ), - builder: (context) => - const LyricsPage(isModal: true), - ); - }, - ) - ], ), - const SizedBox(height: 25) + const SizedBox(height: 10), + if (auth != null) + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const SizedBox(width: 10), + Expanded( + child: OutlinedButton.icon( + icon: const Icon(SpotubeIcons.info), + label: Text(context.l10n.details), + style: OutlinedButton.styleFrom( + foregroundColor: bodyTextColor, + ), + onPressed: currentTrack == null + ? null + : () { + showDialog( + context: context, + builder: (context) { + return TrackDetailsDialog( + track: currentTrack, + ); + }); + }, + ), + ), + const SizedBox(width: 10), + Expanded( + child: OutlinedButton.icon( + label: Text(context.l10n.lyrics), + icon: const Icon(SpotubeIcons.music), + style: OutlinedButton.styleFrom( + foregroundColor: bodyTextColor, + ), + onPressed: () { + showModalBottomSheet( + context: context, + isDismissible: true, + enableDrag: true, + isScrollControlled: true, + backgroundColor: Colors.black38, + barrierColor: Colors.black12, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context) + .size + .height * + 0.8, + ), + builder: (context) => + const LyricsPage(isModal: true), + ); + }, + ), + ), + const SizedBox(width: 10), + ], + ), ], ), ), diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index c1ad94c7a..36b361a18 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -251,6 +251,10 @@ abstract class ServiceUtils { } static void navigate(BuildContext context, String location, {Object? extra}) { + GoRouter.of(context).go(location, extra: extra); + } + + static void push(BuildContext context, String location, {Object? extra}) { GoRouter.of(context).push(location, extra: extra); }