diff --git a/lib/screens/player/source_details.dart b/lib/screens/player/source_details.dart new file mode 100644 index 0000000..8ea9bae --- /dev/null +++ b/lib/screens/player/source_details.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:rhythm_box/services/server/active_sourced_track.dart'; +import 'package:rhythm_box/widgets/player/track_source_details.dart'; + +class SourceDetailsPopup extends StatelessWidget { + const SourceDetailsPopup({super.key}); + + @override + Widget build(BuildContext context) { + final ActiveSourcedTrackProvider activeTrack = Get.find(); + + return SizedBox( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Source Details', + style: Theme.of(context).textTheme.headlineSmall, + ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), + Expanded( + child: Obx( + () => TrackSourceDetails( + track: activeTrack.state.value!, + ).paddingSymmetric(horizontal: 24), + ), + ) + ], + ), + ); + } +} diff --git a/lib/screens/player/view.dart b/lib/screens/player/view.dart index 41fee5e..a6516fd 100644 --- a/lib/screens/player/view.dart +++ b/lib/screens/player/view.dart @@ -12,6 +12,7 @@ import 'package:rhythm_box/providers/audio_player.dart'; import 'package:rhythm_box/providers/auth.dart'; import 'package:rhythm_box/screens/player/queue.dart'; import 'package:rhythm_box/screens/player/siblings.dart'; +import 'package:rhythm_box/screens/player/source_details.dart'; import 'package:rhythm_box/services/artist.dart'; import 'package:rhythm_box/services/audio_player/audio_player.dart'; import 'package:rhythm_box/services/duration.dart'; @@ -308,12 +309,18 @@ class _PlayerScreenState extends State { ], ), const Gap(20), - Row( - children: [ - Expanded( - child: TextButton.icon( + SizedBox( + height: 40, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + TextButton.icon( icon: const Icon(Icons.queue_music), - label: const Text('Queue'), + label: const Text( + 'Queue', + maxLines: 1, + overflow: TextOverflow.fade, + ), onPressed: () { showModalBottomSheet( useRootNavigator: true, @@ -328,24 +335,28 @@ class _PlayerScreenState extends State { }); }, ), - ), - if (!isLargeScreen) const Gap(4), - if (!isLargeScreen) - Expanded( - child: TextButton.icon( + if (!isLargeScreen) const Gap(4), + if (!isLargeScreen) + TextButton.icon( icon: const Icon(Icons.lyrics), - label: const Text('Lyrics'), + label: const Text( + 'Lyrics', + maxLines: 1, + overflow: TextOverflow.fade, + ), onPressed: () { GoRouter.of(context) .pushNamed('playerLyrics'); }, ), - ), - const Gap(4), - Expanded( - child: TextButton.icon( + const Gap(4), + TextButton.icon( icon: const Icon(Icons.merge), - label: const Text('Sources'), + label: const Text( + 'Sources', + maxLines: 1, + overflow: TextOverflow.fade, + ), onPressed: () { showModalBottomSheet( useRootNavigator: true, @@ -360,8 +371,25 @@ class _PlayerScreenState extends State { }); }, ), - ), - ], + const Gap(4), + TextButton.icon( + label: const Text('Info'), + icon: const Icon(Icons.info), + onPressed: () { + showModalBottomSheet( + useRootNavigator: true, + context: context, + builder: (context) => + const SourceDetailsPopup(), + ).then((_) { + if (mounted) { + setState(() {}); + } + }); + }, + ), + ], + ), ), ], ), diff --git a/lib/widgets/player/track_source_details.dart b/lib/widgets/player/track_source_details.dart new file mode 100644 index 0000000..b7901fc --- /dev/null +++ b/lib/widgets/player/track_source_details.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:rhythm_box/services/duration.dart'; +import 'package:rhythm_box/services/sourced_track/sourced_track.dart'; +import 'package:rhythm_box/services/sourced_track/sources/netease.dart'; +import 'package:rhythm_box/services/sourced_track/sources/piped.dart'; +import 'package:rhythm_box/services/sourced_track/sources/youtube.dart'; +import 'package:spotify/spotify.dart'; + +class TrackSourceDetails extends StatelessWidget { + final Track track; + + const TrackSourceDetails({super.key, required this.track}); + + static final sourceInfoToLabelMap = { + YoutubeSourceInfo: 'YouTube', + PipedSourceInfo: 'Piped', + NeteaseSourceInfo: 'Netease', + }; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final detailsMap = { + 'Title': track.name!, + 'Artist': track.artists?.map((x) => x.name).join(', '), + 'Album': track.album!.name!, + 'Duration': (track is SourcedTrack + ? (track as SourcedTrack).sourceInfo.duration + : track.duration!) + .toHumanReadableString(), + if (track.album!.releaseDate != null) + 'Released': track.album!.releaseDate, + 'Popularity': track.popularity?.toString() ?? '0', + 'Provider': sourceInfoToLabelMap[track.runtimeType], + }; + + return 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 if (entry.value is String) + Text( + entry.value as String, + style: theme.textTheme.bodyMedium, + ) + else + const Text('Unknown'), + ], + ), + ], + ); + } +}