mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat: player details dialog and separate location of lyrics button in player page
This commit is contained in:
parent
3b56c78d5c
commit
ce38233de8
@ -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);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
|
@ -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';
|
||||
|
||||
|
166
lib/components/shared/dialogs/track_details_dialog.dart
Normal file
166
lib/components/shared/dialogs/track_details_dialog.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ class AnchorButton<T> 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<T> extends HookWidget {
|
||||
this.onTap,
|
||||
this.textAlign,
|
||||
this.overflow,
|
||||
this.maxLines,
|
||||
this.style = const TextStyle(),
|
||||
}) : super(key: key);
|
||||
|
||||
@ -34,6 +36,7 @@ class AnchorButton<T> extends HookWidget {
|
||||
decoration:
|
||||
hover.value || tap.value ? TextDecoration.underline : null,
|
||||
),
|
||||
maxLines: maxLines,
|
||||
textAlign: textAlign,
|
||||
overflow: overflow,
|
||||
),
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -9,6 +9,8 @@ class LinkText<T> 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<T> extends StatelessWidget {
|
||||
this.extra,
|
||||
this.overflow,
|
||||
this.style = const TextStyle(),
|
||||
this.push = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -24,7 +27,11 @@ class LinkText<T> 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,
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
]
|
||||
},
|
||||
),
|
||||
|
@ -176,6 +176,7 @@ class TrackTile extends HookConsumerWidget {
|
||||
track.album!.name!,
|
||||
"/album/${track.album?.id}",
|
||||
extra: track.album,
|
||||
push: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
|
@ -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"
|
||||
}
|
@ -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(
|
||||
|
@ -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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user