feat: player details dialog and separate location of lyrics button in player page

This commit is contained in:
Kingkor Roy Tirtho 2023-06-15 12:51:33 +06:00
parent 3b56c78d5c
commit ce38233de8
12 changed files with 314 additions and 59 deletions

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.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/components/shared/image/universal_image.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/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
class PlayerTrackDetails extends HookConsumerWidget { class PlayerTrackDetails extends HookConsumerWidget {
@ -72,6 +74,9 @@ class PlayerTrackDetails extends HookConsumerWidget {
), ),
TypeConversionUtils.artists_X_ClickableArtists( TypeConversionUtils.artists_X_ClickableArtists(
playback.activeTrack?.artists ?? [], playback.activeTrack?.artists ?? [],
onRouteChange: (route) {
ServiceUtils.push(context, route);
},
) )
], ],
), ),

View File

@ -20,7 +20,6 @@ import 'package:flutter/material.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart';
import 'package:spotube/provider/volume_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/platform.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';

View File

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

View File

@ -7,6 +7,7 @@ class AnchorButton<T> extends HookWidget {
final TextAlign? textAlign; final TextAlign? textAlign;
final TextOverflow? overflow; final TextOverflow? overflow;
final void Function()? onTap; final void Function()? onTap;
final int? maxLines;
const AnchorButton( const AnchorButton(
this.text, { this.text, {
@ -14,6 +15,7 @@ class AnchorButton<T> extends HookWidget {
this.onTap, this.onTap,
this.textAlign, this.textAlign,
this.overflow, this.overflow,
this.maxLines,
this.style = const TextStyle(), this.style = const TextStyle(),
}) : super(key: key); }) : super(key: key);
@ -34,6 +36,7 @@ class AnchorButton<T> extends HookWidget {
decoration: decoration:
hover.value || tap.value ? TextDecoration.underline : null, hover.value || tap.value ? TextDecoration.underline : null,
), ),
maxLines: maxLines,
textAlign: textAlign, textAlign: textAlign,
overflow: overflow, overflow: overflow,
), ),

View File

@ -8,6 +8,8 @@ class Hyperlink extends StatelessWidget {
final TextAlign? textAlign; final TextAlign? textAlign;
final TextOverflow? overflow; final TextOverflow? overflow;
final String url; final String url;
final int? maxLines;
const Hyperlink( const Hyperlink(
this.text, this.text,
this.url, { this.url, {
@ -15,6 +17,7 @@ class Hyperlink extends StatelessWidget {
this.textAlign, this.textAlign,
this.overflow, this.overflow,
this.style = const TextStyle(), this.style = const TextStyle(),
this.maxLines,
}) : super(key: key); }) : super(key: key);
@override @override
@ -29,6 +32,7 @@ class Hyperlink extends StatelessWidget {
}, },
key: key, key: key,
overflow: overflow, overflow: overflow,
maxLines: maxLines,
style: style.copyWith(color: Colors.blue), style: style.copyWith(color: Colors.blue),
textAlign: textAlign, textAlign: textAlign,
); );

View File

@ -9,6 +9,8 @@ class LinkText<T> extends StatelessWidget {
final TextOverflow? overflow; final TextOverflow? overflow;
final String route; final String route;
final T? extra; final T? extra;
final bool push;
const LinkText( const LinkText(
this.text, this.text,
this.route, { this.route, {
@ -17,6 +19,7 @@ class LinkText<T> extends StatelessWidget {
this.extra, this.extra,
this.overflow, this.overflow,
this.style = const TextStyle(), this.style = const TextStyle(),
this.push = false,
}) : super(key: key); }) : super(key: key);
@override @override
@ -24,7 +27,11 @@ class LinkText<T> extends StatelessWidget {
return AnchorButton( return AnchorButton(
text, text,
onTap: () { onTap: () {
if (push) {
ServiceUtils.push(context, route, extra: extra);
} else {
ServiceUtils.navigate(context, route, extra: extra); ServiceUtils.navigate(context, route, extra: extra);
}
}, },
key: key, key: key,
overflow: overflow, overflow: overflow,

View File

@ -9,6 +9,7 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/library/user_local_tracks.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/adaptive/adaptive_pop_sheet_list.dart';
import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.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/heart_button.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
@ -29,6 +30,7 @@ enum TrackOptionValue {
delete, delete,
playNext, playNext,
favorite, favorite,
details,
} }
class TrackOptions extends HookConsumerWidget { class TrackOptions extends HookConsumerWidget {
@ -163,6 +165,12 @@ class TrackOptions extends HookConsumerWidget {
case TrackOptionValue.share: case TrackOptionValue.share:
actionShare(context, track); actionShare(context, track);
break; break;
case TrackOptionValue.details:
showDialog(
context: context,
builder: (context) => TrackDetailsDialog(track: track),
);
break;
} }
}, },
icon: const Icon(SpotubeIcons.moreHorizontal), icon: const Icon(SpotubeIcons.moreHorizontal),
@ -288,7 +296,14 @@ class TrackOptions extends HookConsumerWidget {
leading: const Icon(SpotubeIcons.share), leading: const Icon(SpotubeIcons.share),
title: Text(context.l10n.share), title: Text(context.l10n.share),
), ),
) ),
PopSheetEntry(
value: TrackOptionValue.details,
child: ListTile(
leading: const Icon(SpotubeIcons.info),
title: Text(context.l10n.details),
),
),
] ]
}, },
), ),

View File

@ -176,6 +176,7 @@ class TrackTile extends HookConsumerWidget {
track.album!.name!, track.album!.name!,
"/album/${track.album?.id}", "/album/${track.album?.id}",
extra: track.album, extra: track.album,
push: true,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
) )

View File

@ -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_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", "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", "decline": "Decline",
"accept": "Accept" "accept": "Accept",
"details": "Details",
"youtube": "YouTube",
"channel": "Channel",
"likes": "Likes",
"dislikes": "Dislikes",
"views": "Views",
"streamUrl": "Stream URL"
} }

View File

@ -111,10 +111,22 @@ class SyncedLyrics extends HookConsumerWidget {
index: index, index: index,
controller: controller, controller: controller,
child: lyricSlice.text.isEmpty child: lyricSlice.text.isEmpty
? Container() ? Container(
padding: index == lyricValue.lyrics.length - 1
? EdgeInsets.only(
bottom:
MediaQuery.of(context).size.height /
2,
)
: null,
)
: Center( : Center(
child: Padding( 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( child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 250),
style: TextStyle( style: TextStyle(

View File

@ -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_actions.dart';
import 'package:spotube/components/player/player_controls.dart'; import 'package:spotube/components/player/player_controls.dart';
import 'package:spotube/components/shared/animated_gradient.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/page_window_title_bar.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/extensions/constrains.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_custom_status_bar_color.dart';
import 'package:spotube/hooks/use_palette_color.dart'; import 'package:spotube/hooks/use_palette_color.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
@ -106,9 +108,8 @@ class PlayerView extends HookConsumerWidget {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Column( child: Column(
children: [ children: [
Padding( Container(
padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(8),
child: Container(
constraints: const BoxConstraints( constraints: const BoxConstraints(
maxHeight: 300, maxWidth: 300), maxHeight: 300, maxWidth: 300),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -131,7 +132,6 @@ class PlayerView extends HookConsumerWidget {
), ),
), ),
), ),
),
const SizedBox(height: 60), const SizedBox(height: 60),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
@ -183,11 +183,41 @@ class PlayerView extends HookConsumerWidget {
PlayerActions( PlayerActions(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
floatingQueue: false, floatingQueue: false,
extraActions: [ ),
const SizedBox(height: 10),
if (auth != null) if (auth != null)
IconButton( Row(
tooltip: "Open Lyrics", 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), icon: const Icon(SpotubeIcons.music),
style: OutlinedButton.styleFrom(
foregroundColor: bodyTextColor,
),
onPressed: () { onPressed: () {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@ -203,18 +233,20 @@ class PlayerView extends HookConsumerWidget {
), ),
), ),
constraints: BoxConstraints( constraints: BoxConstraints(
maxHeight: maxHeight: MediaQuery.of(context)
MediaQuery.of(context).size.height * .size
.height *
0.8, 0.8,
), ),
builder: (context) => builder: (context) =>
const LyricsPage(isModal: true), const LyricsPage(isModal: true),
); );
}, },
) ),
),
const SizedBox(width: 10),
], ],
), ),
const SizedBox(height: 25)
], ],
), ),
), ),

View File

@ -251,6 +251,10 @@ abstract class ServiceUtils {
} }
static void navigate(BuildContext context, String location, {Object? extra}) { 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); GoRouter.of(context).push(location, extra: extra);
} }