From ce38233de8f4775018a1d01e951b1635776fe743 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 15 Jun 2023 12:51:33 +0600 Subject: [PATCH 01/28] feat: player details dialog and separate location of lyrics button in player page --- .../player/player_track_details.dart | 5 + lib/components/root/bottom_player.dart | 1 - .../shared/dialogs/track_details_dialog.dart | 166 ++++++++++++++++++ .../shared/links/anchor_button.dart | 3 + lib/components/shared/links/hyper_link.dart | 4 + lib/components/shared/links/link_text.dart | 9 +- .../shared/track_table/track_options.dart | 17 +- .../shared/track_table/track_tile.dart | 1 + lib/l10n/app_en.arb | 9 +- lib/pages/lyrics/synced_lyrics.dart | 16 +- lib/pages/player/player.dart | 138 +++++++++------ lib/utils/service_utils.dart | 4 + 12 files changed, 314 insertions(+), 59 deletions(-) create mode 100644 lib/components/shared/dialogs/track_details_dialog.dart diff --git a/lib/components/player/player_track_details.dart b/lib/components/player/player_track_details.dart index 5ffaae90..8b66b8b7 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 073086c3..891da2c1 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 00000000..09e71e56 --- /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 ede984e9..b1b1cfea 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 88d4b2b9..fd31298e 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 710cfa81..217b247d 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 6e31f840..8dc094ad 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 c3be9d65..62d33514 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 c4b24eab..c820e588 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 51464fd3..eb49270f 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 2831b71a..9234d82c 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 c1ad94c7..36b361a1 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); } From 2b35c044adb15a97a58692e7880694a251899732 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 15 Jun 2023 13:00:49 +0600 Subject: [PATCH 02/28] fix: playlist generate slider shape --- .../recommendation_attribute_dials.dart | 2 +- .../recommendation_attribute_fields.dart | 2 +- .../playlist_generate/playlist_generate.dart | 489 +++++++++--------- 3 files changed, 249 insertions(+), 244 deletions(-) diff --git a/lib/components/library/playlist_generate/recommendation_attribute_dials.dart b/lib/components/library/playlist_generate/recommendation_attribute_dials.dart index 8f962da4..5af0b870 100644 --- a/lib/components/library/playlist_generate/recommendation_attribute_dials.dart +++ b/lib/components/library/playlist_generate/recommendation_attribute_dials.dart @@ -94,7 +94,7 @@ class RecommendationAttributeDials extends HookWidget { return Card( child: ExpansionTile( title: DefaultTextStyle( - style: Theme.of(context).textTheme.titleMedium!, + style: Theme.of(context).textTheme.titleSmall!, child: title, ), shape: const Border(), diff --git a/lib/components/library/playlist_generate/recommendation_attribute_fields.dart b/lib/components/library/playlist_generate/recommendation_attribute_fields.dart index 78bffbf2..de169147 100644 --- a/lib/components/library/playlist_generate/recommendation_attribute_fields.dart +++ b/lib/components/library/playlist_generate/recommendation_attribute_fields.dart @@ -93,7 +93,7 @@ class RecommendationAttributeFields extends HookWidget { return Card( child: ExpansionTile( title: DefaultTextStyle( - style: Theme.of(context).textTheme.titleMedium!, + style: Theme.of(context).textTheme.titleSmall!, child: title, ), shape: const Border(), diff --git a/lib/pages/library/playlist_generate/playlist_generate.dart b/lib/pages/library/playlist_generate/playlist_generate.dart index c0c719d3..c6f65fb6 100644 --- a/lib/pages/library/playlist_generate/playlist_generate.dart +++ b/lib/pages/library/playlist_generate/playlist_generate.dart @@ -248,251 +248,256 @@ class PlaylistGeneratorPage extends HookConsumerWidget { title: Text(context.l10n.generate_playlist), centerTitle: true, ), - body: SafeArea( - child: LayoutBuilder(builder: (context, constrains) { - return ListView( - padding: const EdgeInsets.all(16), - children: [ - ValueListenableBuilder( - valueListenable: limit, - builder: (context, value, child) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.number_of_tracks_generate, - style: textTheme.titleMedium, - ), - Row( - children: [ - Container( - width: 40, - height: 40, - alignment: Alignment.center, - decoration: BoxDecoration( - color: theme.colorScheme.primary, - shape: BoxShape.circle, - ), - child: Text( - value.round().toString(), - style: textTheme.bodyLarge?.copyWith( - color: theme.colorScheme.primaryContainer, + body: SliderTheme( + data: const SliderThemeData( + overlayShape: RoundSliderOverlayShape(), + ), + child: SafeArea( + child: LayoutBuilder(builder: (context, constrains) { + return ListView( + padding: const EdgeInsets.all(16), + children: [ + ValueListenableBuilder( + valueListenable: limit, + builder: (context, value, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.number_of_tracks_generate, + style: textTheme.titleMedium, + ), + Row( + children: [ + Container( + width: 40, + height: 40, + alignment: Alignment.center, + decoration: BoxDecoration( + color: theme.colorScheme.primary, + shape: BoxShape.circle, + ), + child: Text( + value.round().toString(), + style: textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.primaryContainer, + ), ), ), - ), - Expanded( - child: Slider.adaptive( - value: value.toDouble(), - min: 10, - max: 100, - divisions: 9, - label: value.round().toString(), - onChanged: (value) { - limit.value = value.round(); - }, - ), - ) - ], - ) - ], - ); - }, - ), - const SizedBox(height: 16), - if (constrains.mdAndUp) - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: countrySelector, - ), - const SizedBox(width: 16), - Expanded( - child: genreSelector, - ), - ], - ) - else ...[ - countrySelector, - const SizedBox(height: 16), - genreSelector, - ], - const SizedBox(height: 16), - if (constrains.mdAndUp) - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: artistAutoComplete, - ), - const SizedBox(width: 16), - Expanded( - child: tracksAutocomplete, - ), - ], - ) - else ...[ - artistAutoComplete, - const SizedBox(height: 16), - tracksAutocomplete, - ], - const SizedBox(height: 16), - RecommendationAttributeDials( - title: Text(context.l10n.acousticness), - values: acousticness.value, - onChanged: (value) { - acousticness.value = value; - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.danceability), - values: danceability.value, - onChanged: (value) { - danceability.value = value; - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.energy), - values: energy.value, - onChanged: (value) { - energy.value = value; - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.instrumentalness), - values: instrumentalness.value, - onChanged: (value) { - instrumentalness.value = value; - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.liveness), - values: liveness.value, - onChanged: (value) { - liveness.value = value; - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.loudness), - values: loudness.value, - onChanged: (value) { - loudness.value = value; - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.speechiness), - values: speechiness.value, - onChanged: (value) { - speechiness.value = value; - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.valence), - values: valence.value, - onChanged: (value) { - valence.value = value; - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.popularity), - values: popularity.value, - base: 100, - onChanged: (value) { - popularity.value = value; - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.key), - values: key.value, - base: 11, - onChanged: (value) { - key.value = value; - }, - ), - RecommendationAttributeFields( - title: Text(context.l10n.duration), - values: ( - max: durationMs.value.max / 1000, - target: durationMs.value.target / 1000, - min: durationMs.value.min / 1000, + Expanded( + child: Slider.adaptive( + value: value.toDouble(), + min: 10, + max: 100, + divisions: 9, + label: value.round().toString(), + onChanged: (value) { + limit.value = value.round(); + }, + ), + ) + ], + ) + ], + ); + }, ), - onChanged: (value) { - durationMs.value = ( - max: value.max * 1000, - target: value.target * 1000, - min: value.min * 1000, - ); - }, - presets: { - context.l10n.short: (min: 50, target: 90, max: 120), - context.l10n.medium: (min: 120, target: 180, max: 200), - context.l10n.long: (min: 480, target: 560, max: 640) - }, - ), - RecommendationAttributeFields( - title: Text(context.l10n.tempo), - values: tempo.value, - onChanged: (value) { - tempo.value = value; - }, - ), - RecommendationAttributeFields( - title: Text(context.l10n.mode), - values: mode.value, - onChanged: (value) { - mode.value = value; - }, - ), - RecommendationAttributeFields( - title: Text(context.l10n.time_signature), - values: timeSignature.value, - onChanged: (value) { - timeSignature.value = value; - }, - ), - const SizedBox(height: 20), - FilledButton.icon( - icon: const Icon(SpotubeIcons.magic), - label: Text(context.l10n.generate_playlist), - onPressed: artists.value.isEmpty && - tracks.value.isEmpty && - genres.value.isEmpty - ? null - : () { - final PlaylistGenerateResultRouteState routeState = ( - seeds: ( - artists: artists.value.map((a) => a.id!).toList(), - tracks: tracks.value.map((t) => t.id!).toList(), - genres: genres.value - ), - market: market.value, - limit: limit.value, - parameters: ( - acousticness: acousticness.value, - danceability: danceability.value, - energy: energy.value, - instrumentalness: instrumentalness.value, - liveness: liveness.value, - loudness: loudness.value, - speechiness: speechiness.value, - valence: valence.value, - popularity: popularity.value, - key: key.value, - duration_ms: durationMs.value, - tempo: tempo.value, - mode: mode.value, - time_signature: timeSignature.value, - ) - ); - GoRouter.of(context).push( - "/library/generate/result", - extra: routeState, - ); - }, - ), - ], - ); - }), + const SizedBox(height: 16), + if (constrains.mdAndUp) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: countrySelector, + ), + const SizedBox(width: 16), + Expanded( + child: genreSelector, + ), + ], + ) + else ...[ + countrySelector, + const SizedBox(height: 16), + genreSelector, + ], + const SizedBox(height: 16), + if (constrains.mdAndUp) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: artistAutoComplete, + ), + const SizedBox(width: 16), + Expanded( + child: tracksAutocomplete, + ), + ], + ) + else ...[ + artistAutoComplete, + const SizedBox(height: 16), + tracksAutocomplete, + ], + const SizedBox(height: 16), + RecommendationAttributeDials( + title: Text(context.l10n.acousticness), + values: acousticness.value, + onChanged: (value) { + acousticness.value = value; + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.danceability), + values: danceability.value, + onChanged: (value) { + danceability.value = value; + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.energy), + values: energy.value, + onChanged: (value) { + energy.value = value; + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.instrumentalness), + values: instrumentalness.value, + onChanged: (value) { + instrumentalness.value = value; + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.liveness), + values: liveness.value, + onChanged: (value) { + liveness.value = value; + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.loudness), + values: loudness.value, + onChanged: (value) { + loudness.value = value; + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.speechiness), + values: speechiness.value, + onChanged: (value) { + speechiness.value = value; + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.valence), + values: valence.value, + onChanged: (value) { + valence.value = value; + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.popularity), + values: popularity.value, + base: 100, + onChanged: (value) { + popularity.value = value; + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.key), + values: key.value, + base: 11, + onChanged: (value) { + key.value = value; + }, + ), + RecommendationAttributeFields( + title: Text(context.l10n.duration), + values: ( + max: durationMs.value.max / 1000, + target: durationMs.value.target / 1000, + min: durationMs.value.min / 1000, + ), + onChanged: (value) { + durationMs.value = ( + max: value.max * 1000, + target: value.target * 1000, + min: value.min * 1000, + ); + }, + presets: { + context.l10n.short: (min: 50, target: 90, max: 120), + context.l10n.medium: (min: 120, target: 180, max: 200), + context.l10n.long: (min: 480, target: 560, max: 640) + }, + ), + RecommendationAttributeFields( + title: Text(context.l10n.tempo), + values: tempo.value, + onChanged: (value) { + tempo.value = value; + }, + ), + RecommendationAttributeFields( + title: Text(context.l10n.mode), + values: mode.value, + onChanged: (value) { + mode.value = value; + }, + ), + RecommendationAttributeFields( + title: Text(context.l10n.time_signature), + values: timeSignature.value, + onChanged: (value) { + timeSignature.value = value; + }, + ), + const SizedBox(height: 20), + FilledButton.icon( + icon: const Icon(SpotubeIcons.magic), + label: Text(context.l10n.generate_playlist), + onPressed: artists.value.isEmpty && + tracks.value.isEmpty && + genres.value.isEmpty + ? null + : () { + final PlaylistGenerateResultRouteState routeState = ( + seeds: ( + artists: artists.value.map((a) => a.id!).toList(), + tracks: tracks.value.map((t) => t.id!).toList(), + genres: genres.value + ), + market: market.value, + limit: limit.value, + parameters: ( + acousticness: acousticness.value, + danceability: danceability.value, + energy: energy.value, + instrumentalness: instrumentalness.value, + liveness: liveness.value, + loudness: loudness.value, + speechiness: speechiness.value, + valence: valence.value, + popularity: popularity.value, + key: key.value, + duration_ms: durationMs.value, + tempo: tempo.value, + mode: mode.value, + time_signature: timeSignature.value, + ) + ); + GoRouter.of(context).push( + "/library/generate/result", + extra: routeState, + ); + }, + ), + ], + ); + }), + ), ), ); } From 7abe2c10735bc38c644487139557a731d25e80e6 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 15 Jun 2023 13:10:47 +0600 Subject: [PATCH 03/28] feat: volume slider in player page --- lib/components/player/volume_slider.dart | 66 ++++++++++++++++++++++++ lib/components/root/bottom_player.dart | 53 +------------------ lib/pages/player/player.dart | 20 +++++++ 3 files changed, 88 insertions(+), 51 deletions(-) create mode 100644 lib/components/player/volume_slider.dart diff --git a/lib/components/player/volume_slider.dart b/lib/components/player/volume_slider.dart new file mode 100644 index 00000000..55f8520d --- /dev/null +++ b/lib/components/player/volume_slider.dart @@ -0,0 +1,66 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/provider/volume_provider.dart'; + +class VolumeSlider extends HookConsumerWidget { + final bool fullWidth; + const VolumeSlider({ + Key? key, + this.fullWidth = false, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final volume = ref.watch(volumeProvider); + final volumeNotifier = ref.watch(volumeProvider.notifier); + + var slider = Listener( + onPointerSignal: (event) async { + if (event is PointerScrollEvent) { + if (event.scrollDelta.dy > 0) { + final value = volume - .2; + volumeNotifier.setVolume(value < 0 ? 0 : value); + } else { + final value = volume + .2; + volumeNotifier.setVolume(value > 1 ? 1 : value); + } + } + }, + child: Slider.adaptive( + min: 0, + max: 1, + value: volume, + onChanged: volumeNotifier.setVolume, + ), + ); + return Row( + mainAxisAlignment: + !fullWidth ? MainAxisAlignment.center : MainAxisAlignment.start, + children: [ + IconButton( + icon: Icon( + volume == 0 + ? SpotubeIcons.volumeMute + : volume <= 0.2 + ? SpotubeIcons.volumeLow + : volume <= 0.6 + ? SpotubeIcons.volumeMedium + : SpotubeIcons.volumeHigh, + size: 16, + ), + onPressed: () { + if (volume == 0) { + volumeNotifier.setVolume(1); + } else { + volumeNotifier.setVolume(0); + } + }, + ), + if (fullWidth) Expanded(child: slider) else slider, + ], + ); + } +} diff --git a/lib/components/root/bottom_player.dart b/lib/components/root/bottom_player.dart index 891da2c1..2b0bc1ff 100644 --- a/lib/components/root/bottom_player.dart +++ b/lib/components/root/bottom_player.dart @@ -12,6 +12,7 @@ import 'package:spotube/components/player/player_actions.dart'; import 'package:spotube/components/player/player_overlay.dart'; import 'package:spotube/components/player/player_track_details.dart'; import 'package:spotube/components/player/player_controls.dart'; +import 'package:spotube/components/player/volume_slider.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/use_brightness_value.dart'; @@ -115,57 +116,7 @@ class BottomPlayer extends HookConsumerWidget { Container( height: 40, constraints: const BoxConstraints(maxWidth: 250), - child: HookBuilder(builder: (context) { - final volume = ref.watch(volumeProvider); - final volumeNotifier = - ref.watch(volumeProvider.notifier); - - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon( - volume == 0 - ? SpotubeIcons.volumeMute - : volume <= 0.2 - ? SpotubeIcons.volumeLow - : volume <= 0.6 - ? SpotubeIcons.volumeMedium - : SpotubeIcons.volumeHigh, - size: 16, - ), - onPressed: () { - if (volume == 0) { - volumeNotifier.setVolume(1); - } else { - volumeNotifier.setVolume(0); - } - }, - ), - Listener( - onPointerSignal: (event) async { - if (event is PointerScrollEvent) { - if (event.scrollDelta.dy > 0) { - final value = volume - .2; - volumeNotifier - .setVolume(value < 0 ? 0 : value); - } else { - final value = volume + .2; - volumeNotifier - .setVolume(value > 1 ? 1 : value); - } - } - }, - child: Slider.adaptive( - min: 0, - max: 1, - value: volume, - onChanged: volumeNotifier.setVolume, - ), - ), - ], - ); - }), + child: const VolumeSlider(), ) ], ) diff --git a/lib/pages/player/player.dart b/lib/pages/player/player.dart index 9234d82c..61b54d72 100644 --- a/lib/pages/player/player.dart +++ b/lib/pages/player/player.dart @@ -9,6 +9,7 @@ import 'package:spotube/collections/assets.gen.dart'; 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/player/volume_slider.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'; @@ -247,6 +248,25 @@ class PlayerView extends HookConsumerWidget { const SizedBox(width: 10), ], ), + const SizedBox(height: 25), + SliderTheme( + data: theme.sliderTheme.copyWith( + activeTrackColor: titleTextColor, + inactiveTrackColor: bodyTextColor, + thumbColor: titleTextColor, + overlayColor: titleTextColor?.withOpacity(0.2), + trackHeight: 2, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 6, + ), + ), + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: VolumeSlider( + fullWidth: true, + ), + ), + ), ], ), ), From ea45c4f42ae89b8991e470e84a5290b3be3b0f36 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 17 Jun 2023 09:32:37 +0600 Subject: [PATCH 04/28] fix: re-enable download manager --- android/app/build.gradle | 13 +- android/build.gradle | 2 +- lib/components/library/user_downloads.dart | 10 +- lib/provider/download_manager_provider.dart | 163 ++++++++++---------- pubspec.lock | 8 + pubspec.yaml | 1 + 6 files changed, 107 insertions(+), 90 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 094a1a13..d05a90a1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -79,6 +79,17 @@ flutter { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + constraints { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version") { + because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib") + } + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") { + because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib") + } + } + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' + + // other deps so just ignore implementation 'com.android.support:multidex:2.0.1' } diff --git a/android/build.gradle b/android/build.gradle index caa67209..0801de62 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.7.21' + ext.kotlin_version = '1.8.22' repositories { google() mavenCentral() diff --git a/lib/components/library/user_downloads.dart b/lib/components/library/user_downloads.dart index 54432086..1a030377 100644 --- a/lib/components/library/user_downloads.dart +++ b/lib/components/library/user_downloads.dart @@ -1,4 +1,5 @@ import 'package:auto_size_text/auto_size_text.dart'; +import 'package:background_downloader/background_downloader.dart'; // import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -65,14 +66,11 @@ class UserDownloads extends HookConsumerWidget { .where((element) => element.taskId == track.id), ); final taskItSelf = useFuture( - Future.value(null), - // FileDownloader().database.recordForId(track.id!), + FileDownloader().database.recordForId(track.id!), ); - final hasFailed = failedTaskStream - .hasData /* || - taskItSelf.data?.status == TaskStatus.failed */ - ; + final hasFailed = failedTaskStream.hasData || + taskItSelf.data?.status == TaskStatus.failed; return ListTile( title: Text(track.name ?? ''), diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index 2ad95ba3..4ad2ed94 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:io'; -// import 'package:background_downloader/background_downloader.dart'; +import 'package:background_downloader/background_downloader.dart'; import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http/http.dart'; @@ -19,8 +19,8 @@ import 'package:spotube/utils/type_conversion_utils.dart'; class DownloadManagerProvider extends StateNotifier> { final Ref ref; - final StreamController /* */ activeDownloadProgress; - final StreamController /* */ failedDownloads; + final StreamController activeDownloadProgress; + final StreamController failedDownloads; Track? _activeItem; FutureOr Function(Track)? onFileExists; @@ -29,78 +29,78 @@ class DownloadManagerProvider extends StateNotifier> { : activeDownloadProgress = StreamController.broadcast(), failedDownloads = StreamController.broadcast(), super([]) { - // FileDownloader().registerCallbacks( - // group: FileDownloader.defaultGroup, - // taskNotificationTapCallback: (task, notificationType) { - // router.go("/library"); - // }, - // taskStatusCallback: (update) async { - // if (update.status == TaskStatus.running) { - // _activeItem = - // state.firstWhereOrNull((track) => track.id == update.task.taskId); - // state = state.toList(); - // } + FileDownloader().registerCallbacks( + group: FileDownloader.defaultGroup, + taskNotificationTapCallback: (task, notificationType) { + router.go("/library"); + }, + taskStatusCallback: (update) async { + if (update.status == TaskStatus.running) { + _activeItem = + state.firstWhereOrNull((track) => track.id == update.task.taskId); + state = state.toList(); + } - // if (update.status == TaskStatus.failed || - // update.status == TaskStatus.notFound) { - // failedDownloads.add(update.task); - // } + if (update.status == TaskStatus.failed || + update.status == TaskStatus.notFound) { + failedDownloads.add(update.task); + } - // if (update.status == TaskStatus.complete) { - // final track = - // state.firstWhere((element) => element.id == update.task.taskId); + if (update.status == TaskStatus.complete) { + final track = + state.firstWhere((element) => element.id == update.task.taskId); - // // resetting the replace downloaded file state on queue completion - // if (state.last == track) { - // ref.read(replaceDownloadedFileState.notifier).state = null; - // } + // resetting the replace downloaded file state on queue completion + if (state.last == track) { + ref.read(replaceDownloadedFileState.notifier).state = null; + } - // state = state - // .where((element) => element.id != update.task.taskId) - // .toList(); + state = state + .where((element) => element.id != update.task.taskId) + .toList(); - // final imageUri = TypeConversionUtils.image_X_UrlString( - // track.album?.images ?? [], - // placeholder: ImagePlaceholder.online, - // ); - // final response = await get(Uri.parse(imageUri)); + final imageUri = TypeConversionUtils.image_X_UrlString( + track.album?.images ?? [], + placeholder: ImagePlaceholder.online, + ); + final response = await get(Uri.parse(imageUri)); - // final tempFile = File(await update.task.filePath()); + final tempFile = File(await update.task.filePath()); - // final file = tempFile.copySync(_getPathForTrack(track)); + final file = tempFile.copySync(_getPathForTrack(track)); - // await tempFile.delete(); + await tempFile.delete(); - // await MetadataGod.writeMetadata( - // file: file.path, - // metadata: Metadata( - // title: track.name, - // artist: track.artists?.map((a) => a.name).join(", "), - // album: track.album?.name, - // albumArtist: track.artists?.map((a) => a.name).join(", "), - // year: track.album?.releaseDate != null - // ? int.tryParse(track.album!.releaseDate!) - // : null, - // trackNumber: track.trackNumber, - // discNumber: track.discNumber, - // durationMs: track.durationMs?.toDouble(), - // fileSize: file.lengthSync(), - // trackTotal: track.album?.tracks?.length, - // picture: response.headers['content-type'] != null - // ? Picture( - // data: response.bodyBytes, - // mimeType: response.headers['content-type']!, - // ) - // : null, - // ), - // ); - // } - // }, - // taskProgressCallback: (update) { - // activeDownloadProgress.add(update); - // }, - // ); - // FileDownloader().trackTasks(markDownloadedComplete: true); + await MetadataGod.writeMetadata( + file: file.path, + metadata: Metadata( + title: track.name, + artist: track.artists?.map((a) => a.name).join(", "), + album: track.album?.name, + albumArtist: track.artists?.map((a) => a.name).join(", "), + year: track.album?.releaseDate != null + ? int.tryParse(track.album!.releaseDate!) + : null, + trackNumber: track.trackNumber, + discNumber: track.discNumber, + durationMs: track.durationMs?.toDouble(), + fileSize: file.lengthSync(), + trackTotal: track.album?.tracks?.length, + picture: response.headers['content-type'] != null + ? Picture( + data: response.bodyBytes, + mimeType: response.headers['content-type']!, + ) + : null, + ), + ); + } + }, + taskProgressCallback: (update) { + activeDownloadProgress.add(update); + }, + ); + FileDownloader().trackTasks(markDownloadedComplete: true); } UserPreferences get preferences => ref.read(userPreferencesProvider); @@ -115,9 +115,9 @@ class DownloadManagerProvider extends StateNotifier> { "${track.name} - ${track.artists?.map((a) => a.name).join(", ")}.m4a", ); - Future /* */ _ensureSpotubeTrack(Track track) async { + Future _ensureSpotubeTrack(Track track) async { if (state.any((element) => element.id == track.id)) { - final task = null /* await FileDownloader().taskForId(track.id!) */; + final task = await FileDownloader().taskForId(track.id!); if (task != null) { return task; } @@ -133,17 +133,16 @@ class DownloadManagerProvider extends StateNotifier> { pipedClient, ); state = [...state, spotubeTrack]; - // final task = DownloadTask( - // url: spotubeTrack.ytUri, - // baseDirectory: BaseDirectory.applicationSupport, - // taskId: spotubeTrack.id!, - // updates: Updates.statusAndProgress, - // ); - // return task; - return null; + final task = DownloadTask( + url: spotubeTrack.ytUri, + baseDirectory: BaseDirectory.applicationSupport, + taskId: spotubeTrack.id!, + updates: Updates.statusAndProgress, + ); + return task; } - Future /* */ enqueue(Track track) async { + Future enqueue(Track track) async { final replaceFileGlobal = ref.read(replaceDownloadedFileState); final file = File(_getPathForTrack(track)); if (file.existsSync() && @@ -156,11 +155,11 @@ class DownloadManagerProvider extends StateNotifier> { final task = await _ensureSpotubeTrack(track); - // await FileDownloader().enqueue(task); + await FileDownloader().enqueue(task); return task; } - Future */ > enqueueAll(List tracks) async { + Future> enqueueAll(List tracks) async { final tasks = await Future.wait(tracks.mapIndexed((i, e) { if (i != 0) { /// One second delay between each download to avoid @@ -174,16 +173,16 @@ class DownloadManagerProvider extends StateNotifier> { ref.read(replaceDownloadedFileState.notifier).state = null; } - return tasks. /* whereType(). */ toList(); + return tasks.whereType().toList(); } Future cancel(Track track) async { - // await FileDownloader().cancelTaskWithId(track.id!); + await FileDownloader().cancelTaskWithId(track.id!); state = state.where((element) => element.id != track.id).toList(); } Future cancelAll() async { - // (await FileDownloader().reset()); + (await FileDownloader().reset()); state = []; } } diff --git a/pubspec.lock b/pubspec.lock index a913095d..6cbba78f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -153,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + background_downloader: + dependency: "direct main" + description: + name: background_downloader + sha256: "5e38a1d5d88a5cfea35c44cb376b89427688070518471ee52f6b04d07d85668e" + url: "https://pub.dev" + source: hosted + version: "7.4.0" badges: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 07db73fd..66fb8eac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -103,6 +103,7 @@ dependencies: media_kit_native_event_loop: ^1.0.4 dbus: ^0.7.8 motion_toast: ^2.6.8 + background_downloader: ^7.4.0 dev_dependencies: build_runner: ^2.3.2 From 73c012c71ab5050636f79e010d654b4390978ee7 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 17 Jun 2023 10:17:54 +0600 Subject: [PATCH 05/28] fix: local tracks getting fetched on first load --- lib/components/library/user_local_tracks.dart | 2 +- .../proxy_playlist_provider.dart | 40 +++++++++++-------- .../audio_services/linux_audio_service.dart | 2 +- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index 05e8a921..2431956e 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -95,7 +95,7 @@ final localTracksProvider = FutureProvider>((ref) async { return {"metadata": metadata, "file": f, "art": imageFile.path}; } catch (e, stack) { if (e is FfiException) { - return {}; + return {"file": f}; } Catcher.reportCheckedError(e, stack); return {}; diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index 2ed3567c..166b40e9 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -22,7 +22,7 @@ import 'package:spotube/utils/type_conversion_utils.dart'; /// Things to implement: /// * [x] Sponsor-Block skip /// * [x] Prefetch next track as [SpotubeTrack] on 80% of current track -/// * [ ] Mixed Queue containing both [SpotubeTrack] and [LocalTrack] +/// * [x] Mixed Queue containing both [SpotubeTrack] and [LocalTrack] /// * [ ] Modification of the Queue /// * [x] Add track at the end /// * [x] Add track at the beginning @@ -218,29 +218,37 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier bool autoPlay = false, }) async { tracks = blacklist.filter(tracks).toList() as List; - final addableTrack = await SpotubeTrack.fetchFromTrack( - tracks.elementAtOrNull(initialIndex) ?? tracks.first, - preferences, - pipedClient, - ); + final indexTrack = tracks.elementAtOrNull(initialIndex) ?? tracks.first; - state = state.copyWith( - tracks: mergeTracks([addableTrack], tracks), - active: initialIndex, - ); + if (indexTrack is LocalTrack) { + state = state.copyWith( + tracks: tracks.toSet(), + active: initialIndex, + ); + await notificationService.addTrack(indexTrack); + } else { + final addableTrack = await SpotubeTrack.fetchFromTrack( + tracks.elementAtOrNull(initialIndex) ?? tracks.first, + preferences, + pipedClient, + ); - await notificationService.addTrack(addableTrack); + state = state.copyWith( + tracks: mergeTracks([addableTrack], tracks), + active: initialIndex, + ); + await notificationService.addTrack(addableTrack); + await storeTrack( + tracks.elementAt(initialIndex), + addableTrack, + ); + } await audioPlayer.openPlaylist( state.tracks.map(makeAppropriateSource).toList(), initialIndex: initialIndex, autoPlay: autoPlay, ); - - await storeTrack( - tracks.elementAt(initialIndex), - addableTrack, - ); } Future jumpTo(int index) async { diff --git a/lib/services/audio_services/linux_audio_service.dart b/lib/services/audio_services/linux_audio_service.dart index b50e4ee4..99d840a4 100644 --- a/lib/services/audio_services/linux_audio_service.dart +++ b/lib/services/audio_services/linux_audio_service.dart @@ -323,7 +323,7 @@ class _MprisMediaPlayer2Player extends DBusObject { "xesam:url": DBusString( playlist.activeTrack is SpotubeTrack ? (playlist.activeTrack as SpotubeTrack).ytUri - : playlist.activeTrack!.previewUrl!, + : playlist.activeTrack!.previewUrl ?? "", ), "xesam:genre": const DBusString("Unknown"), }), From 1266a3f1607de11e793a294071850996527d494a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 17 Jun 2023 10:30:36 +0600 Subject: [PATCH 06/28] fix: failed download no error icon --- lib/components/library/user_downloads.dart | 27 ++++++++++------------ 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/lib/components/library/user_downloads.dart b/lib/components/library/user_downloads.dart index 1a030377..eedd7840 100644 --- a/lib/components/library/user_downloads.dart +++ b/lib/components/library/user_downloads.dart @@ -89,21 +89,18 @@ class UserDownloads extends HookConsumerWidget { ), ), horizontalTitleGap: 10, - trailing: SizedBox( - width: 30, - height: 30, - child: downloadManager.activeItem?.id == track.id - ? CircularProgressIndicator( - value: task.data?.progress ?? 0, - ) - : hasFailed - ? Icon(SpotubeIcons.error, color: Colors.red[400]) - : IconButton( - icon: const Icon(SpotubeIcons.close), - onPressed: () { - downloadManager.cancel(track); - }), - ), + trailing: downloadManager.activeItem?.id == track.id && + !hasFailed + ? CircularProgressIndicator( + value: task.data?.progress ?? 0, + ) + : hasFailed + ? Icon(SpotubeIcons.error, color: Colors.red[400]) + : IconButton( + icon: const Icon(SpotubeIcons.close), + onPressed: () { + downloadManager.cancel(track); + }), subtitle: TypeConversionUtils.artists_X_ClickableArtists( track.artists ?? [], mainAxisAlignment: WrapAlignment.start, From a0767f46649f1b9edf06b11a19248738e4b26951 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 17 Jun 2023 10:53:36 +0600 Subject: [PATCH 07/28] chore: navigation fix --- lib/components/album/album_card.dart | 2 +- lib/components/artist/artist_card.dart | 2 +- lib/components/player/player_overlay.dart | 2 +- lib/components/playlist/playlist_card.dart | 2 +- lib/components/shared/fallbacks/anonymous_fallback.dart | 2 +- lib/pages/desktop_login/login_tutorial.dart | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/components/album/album_card.dart b/lib/components/album/album_card.dart index 821194aa..5c94caf0 100644 --- a/lib/components/album/album_card.dart +++ b/lib/components/album/album_card.dart @@ -70,7 +70,7 @@ class AlbumCard extends HookConsumerWidget { description: "${AlbumType.from(album.albumType!).formatted} • ${TypeConversionUtils.artists_X_String(album.artists ?? [])}", onTap: () { - ServiceUtils.navigate(context, "/album/${album.id}", extra: album); + ServiceUtils.push(context, "/album/${album.id}", extra: album); }, onPlaybuttonPressed: () async { updating.value = true; diff --git a/lib/components/artist/artist_card.dart b/lib/components/artist/artist_card.dart index 77e22c22..cb695f6f 100644 --- a/lib/components/artist/artist_card.dart +++ b/lib/components/artist/artist_card.dart @@ -62,7 +62,7 @@ class ArtistCard extends HookConsumerWidget { ), child: InkWell( onTap: () { - ServiceUtils.navigate(context, "/artist/${artist.id}"); + ServiceUtils.push(context, "/artist/${artist.id}"); }, borderRadius: radius, child: Padding( diff --git a/lib/components/player/player_overlay.dart b/lib/components/player/player_overlay.dart index 88ca5488..e51d11ef 100644 --- a/lib/components/player/player_overlay.dart +++ b/lib/components/player/player_overlay.dart @@ -44,7 +44,7 @@ class PlayerOverlay extends HookConsumerWidget { int sensitivity = 8; if (details.primaryVelocity != null && details.primaryVelocity! < -sensitivity) { - ServiceUtils.navigate(context, "/player"); + ServiceUtils.push(context, "/player"); } }, child: ClipRRect( diff --git a/lib/components/playlist/playlist_card.dart b/lib/components/playlist/playlist_card.dart index ff3540cf..5c801ee5 100644 --- a/lib/components/playlist/playlist_card.dart +++ b/lib/components/playlist/playlist_card.dart @@ -48,7 +48,7 @@ class PlaylistCard extends HookConsumerWidget { isLoading: (isPlaylistPlaying && playlistQueue.isFetching) || updating.value, onTap: () { - ServiceUtils.navigate( + ServiceUtils.push( context, "/playlist/${playlist.id}", extra: playlist, diff --git a/lib/components/shared/fallbacks/anonymous_fallback.dart b/lib/components/shared/fallbacks/anonymous_fallback.dart index fce80a08..7a6eb046 100644 --- a/lib/components/shared/fallbacks/anonymous_fallback.dart +++ b/lib/components/shared/fallbacks/anonymous_fallback.dart @@ -24,7 +24,7 @@ class AnonymousFallback extends ConsumerWidget { const SizedBox(height: 10), FilledButton( child: const Text("Login with Spotify"), - onPressed: () => ServiceUtils.navigate(context, "/settings"), + onPressed: () => ServiceUtils.push(context, "/settings"), ) ], ), diff --git a/lib/pages/desktop_login/login_tutorial.dart b/lib/pages/desktop_login/login_tutorial.dart index 355df838..24373e75 100644 --- a/lib/pages/desktop_login/login_tutorial.dart +++ b/lib/pages/desktop_login/login_tutorial.dart @@ -54,7 +54,7 @@ class LoginTutorial extends ConsumerWidget { overrideDone: FilledButton( onPressed: authenticationNotifier.isLoggedIn ? () { - ServiceUtils.navigate(context, "/"); + ServiceUtils.push(context, "/"); } : null, child: Center(child: Text(context.l10n.done)), From 0cedc7a4187771efce8152003f890e242116c78c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 17 Jun 2023 13:08:33 +0600 Subject: [PATCH 08/28] feat: re-designed playlist/album page --- lib/collections/routes.dart | 4 +- lib/components/shared/spotube_page_route.dart | 20 ++ .../track_table/track_collection_view.dart | 338 ++++++++++-------- .../shared/track_table/track_tile.dart | 12 +- .../shared/track_table/tracks_table_view.dart | 88 ++++- lib/l10n/app_en.arb | 3 +- 6 files changed, 295 insertions(+), 170 deletions(-) diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index fc0fb838..44f57def 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -79,8 +79,8 @@ final router = GoRouter( routes: [ GoRoute( path: "blacklist", - pageBuilder: (context, state) => const SpotubePage( - child: BlackListPage(), + pageBuilder: (context, state) => SpotubeSlidePage( + child: const BlackListPage(), ), ), GoRoute( diff --git a/lib/components/shared/spotube_page_route.dart b/lib/components/shared/spotube_page_route.dart index 92049fb1..22e4d2f1 100644 --- a/lib/components/shared/spotube_page_route.dart +++ b/lib/components/shared/spotube_page_route.dart @@ -1,5 +1,25 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; class SpotubePage extends MaterialPage { const SpotubePage({required super.child}); } + +class SpotubeSlidePage extends CustomTransitionPage { + SpotubeSlidePage({ + required super.child, + super.key, + }) : super( + reverseTransitionDuration: const Duration(milliseconds: 150), + transitionDuration: const Duration(milliseconds: 150), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return SlideTransition( + position: Tween( + begin: const Offset(1, 0), + end: Offset.zero, + ).animate(animation), + child: child, + ); + }, + ); +} diff --git a/lib/components/shared/track_table/track_collection_view.dart b/lib/components/shared/track_table/track_collection_view.dart index da657f42..d6cb29cf 100644 --- a/lib/components/shared/track_table/track_collection_view.dart +++ b/lib/components/shared/track_table/track_collection_view.dart @@ -1,14 +1,13 @@ +import 'dart:ui'; + import 'package:fl_query/fl_query.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:collection/collection.dart'; -import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/album/album_card.dart'; -import 'package:spotube/components/shared/compact_search.dart'; import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; @@ -66,68 +65,33 @@ class TrackCollectionView extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final theme = Theme.of(context); + final mediaQuery = MediaQuery.of(context); final auth = ref.watch(AuthenticationNotifier.provider); final color = usePaletteGenerator(titleImage).dominantColor; final List buttons = [ if (showShare) IconButton( - icon: Icon( - SpotubeIcons.share, - color: color?.titleTextColor, - ), + icon: const Icon(SpotubeIcons.share), onPressed: onShare, ), if (heartBtn != null && auth != null) heartBtn!, IconButton( - tooltip: context.l10n.shuffle, - icon: Icon( - SpotubeIcons.shuffle, - color: color?.titleTextColor, + onPressed: isPlaying + ? null + : tracksSnapshot.data != null + ? onAddToQueue + : null, + icon: const Icon( + SpotubeIcons.queueAdd, ), - onPressed: onShuffledPlay, ), - const SizedBox(width: 5), - // add to queue playlist - if (!isPlaying) - IconButton( - onPressed: tracksSnapshot.data != null ? onAddToQueue : null, - icon: Icon( - SpotubeIcons.queueAdd, - color: color?.titleTextColor, - ), - ), - // play playlist - ElevatedButton( - style: ElevatedButton.styleFrom( - shape: const CircleBorder(), - backgroundColor: theme.colorScheme.inversePrimary, - ), - onPressed: tracksSnapshot.data != null ? onPlay : null, - child: Icon(isPlaying ? SpotubeIcons.stop : SpotubeIcons.play), - ), - const SizedBox(width: 10), ]; final controller = useScrollController(); final collapsed = useState(false); - final searchText = useState(""); - final searchController = useTextEditingController(); - - final filteredTracks = useMemoized(() { - if (searchText.value.isEmpty) { - return tracksSnapshot.data; - } - return tracksSnapshot.data - ?.map((e) => (weightedRatio(e.name!, searchText.value), e)) - .sorted((a, b) => b.$1.compareTo(a.$1)) - .where((e) => e.$1 > 50) - .map((e) => e.$2) - .toList(); - }, [tracksSnapshot.data, searchText.value]); - useCustomStatusBarColor( color?.color ?? theme.scaffoldBackgroundColor, GoRouter.of(context).location == routePath, @@ -147,48 +111,21 @@ class TrackCollectionView extends HookConsumerWidget { return () => controller.removeListener(listener); }, [collapsed.value]); - final searchbar = ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 300, - maxHeight: 50, - ), - child: TextField( - controller: searchController, - onChanged: (value) => searchText.value = value, - style: TextStyle(color: color?.titleTextColor), - decoration: InputDecoration( - hintText: context.l10n.search_tracks, - hintStyle: TextStyle(color: color?.titleTextColor), - border: theme.inputDecorationTheme.border?.copyWith( - borderSide: BorderSide( - color: color?.titleTextColor ?? Colors.white, - ), - ), - isDense: true, - prefixIconColor: color?.titleTextColor, - prefixIcon: const Icon(SpotubeIcons.search), - ), - ), - ); - return SafeArea( bottom: false, child: Scaffold( appBar: kIsDesktop - ? PageWindowTitleBar( - backgroundColor: color?.color, - foregroundColor: color?.titleTextColor, + ? const PageWindowTitleBar( + backgroundColor: Colors.transparent, + foregroundColor: Colors.white, leadingWidth: 400, - leading: Row( - mainAxisSize: MainAxisSize.min, - children: [ - BackButton(color: color?.titleTextColor), - const SizedBox(width: 10), - searchbar, - ], + leading: Align( + alignment: Alignment.centerLeft, + child: BackButton(color: Colors.white), ), ) : null, + extendBodyBehindAppBar: kIsDesktop, body: RefreshIndicator( onRefresh: () async { await tracksSnapshot.refresh(); @@ -199,13 +136,36 @@ class TrackCollectionView extends HookConsumerWidget { slivers: [ SliverAppBar( actions: [ - if (kIsMobile) - CompactSearch( - onChanged: (value) => searchText.value = value, - placeholder: context.l10n.search_tracks, - iconColor: color?.titleTextColor, + AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: collapsed.value ? 1 : 0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: buttons, ), - if (collapsed.value) ...buttons, + ), + AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: collapsed.value ? 1 : 0, + child: IconButton( + tooltip: context.l10n.shuffle, + icon: const Icon(SpotubeIcons.shuffle), + onPressed: isPlaying ? null : onShuffledPlay, + ), + ), + AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: collapsed.value ? 1 : 0, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + backgroundColor: theme.colorScheme.inversePrimary, + ), + onPressed: tracksSnapshot.data != null ? onPlay : null, + child: Icon( + isPlaying ? SpotubeIcons.stop : SpotubeIcons.play), + ), + ), ], floating: false, pinned: true, @@ -220,7 +180,7 @@ class TrackCollectionView extends HookConsumerWidget { title: collapsed.value ? Text( title, - style: theme.textTheme.titleLarge!.copyWith( + style: theme.textTheme.titleMedium!.copyWith( color: color?.titleTextColor, fontWeight: FontWeight.w600, ), @@ -230,80 +190,140 @@ class TrackCollectionView extends HookConsumerWidget { flexibleSpace: FlexibleSpaceBar( background: DecoratedBox( decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - color?.color ?? Colors.transparent, - theme.canvasColor, - ], - begin: const FractionalOffset(0, 0), - end: const FractionalOffset(0, 1), - tileMode: TileMode.clamp, + image: DecorationImage( + image: UniversalImage.imageProvider(titleImage), + fit: BoxFit.cover, ), ), - child: Material( - type: MaterialType.transparency, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.black45, + theme.colorScheme.surface, + ], + begin: const FractionalOffset(0, 0), + end: const FractionalOffset(0, 1), + tileMode: TileMode.clamp, + ), ), - child: Wrap( - spacing: 20, - runSpacing: 20, - crossAxisAlignment: WrapCrossAlignment.center, - alignment: WrapAlignment.center, - runAlignment: WrapAlignment.center, - children: [ - Container( - constraints: - const BoxConstraints(maxHeight: 200), - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: UniversalImage( - path: titleImage, - placeholder: Assets.albumPlaceholder.path, - ), - ), + child: Material( + type: MaterialType.transparency, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + child: Wrap( + spacing: 20, + runSpacing: 20, + crossAxisAlignment: WrapCrossAlignment.center, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, children: [ - Text( - title, - style: theme.textTheme.titleLarge!.copyWith( - color: color?.titleTextColor, - fontWeight: FontWeight.w600, - ), - ), - if (album != null) - Text( - "${AlbumType.from(album?.albumType).formatted} • ${context.l10n.released} • ${DateTime.tryParse( - album?.releaseDate ?? "", - )?.year}", - style: - theme.textTheme.titleMedium!.copyWith( - color: color?.titleTextColor, - fontWeight: FontWeight.normal, + Container( + constraints: + const BoxConstraints(maxHeight: 200), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: UniversalImage( + path: titleImage, + placeholder: + Assets.albumPlaceholder.path, ), ), - if (description != null) - Text( - description!, - style: TextStyle( - color: color?.bodyTextColor, - ), - maxLines: 2, - overflow: TextOverflow.fade, - ), - const SizedBox(height: 10), - Row( - mainAxisSize: MainAxisSize.min, - children: buttons, ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: theme.textTheme.titleLarge! + .copyWith( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + if (album != null) + Text( + "${AlbumType.from(album?.albumType).formatted} • ${context.l10n.released} • ${DateTime.tryParse( + album?.releaseDate ?? "", + )?.year}", + style: theme.textTheme.titleMedium! + .copyWith( + color: Colors.white, + fontWeight: FontWeight.normal, + ), + ), + if (description != null) + Text( + description!, + style: const TextStyle( + color: Colors.white), + maxLines: 2, + overflow: TextOverflow.fade, + ), + const SizedBox(height: 10), + IconTheme( + data: theme.iconTheme.copyWith( + color: Colors.white, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: buttons, + ), + ), + const SizedBox(height: 10), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + FilledButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: color?.color, + ), + label: Text(context.l10n.shuffle), + icon: const Icon( + SpotubeIcons.shuffle), + onPressed: + tracksSnapshot.data == null || + isPlaying + ? null + : onShuffledPlay, + ), + const SizedBox(width: 10), + FilledButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: color?.color, + foregroundColor: + color?.bodyTextColor, + ), + onPressed: + tracksSnapshot.data != null + ? onPlay + : null, + icon: Icon( + isPlaying + ? SpotubeIcons.stop + : SpotubeIcons.play, + ), + label: Text( + isPlaying + ? context.l10n.stop + : context.l10n.play, + ), + ), + ], + ), + ], + ) ], - ) - ], + ), + ), ), ), ), @@ -324,13 +344,15 @@ class TrackCollectionView extends HookConsumerWidget { return TracksTableView( List.from( - (filteredTracks ?? []).map( - (e) { - if (e is Track) { - return e; + (tracksSnapshot.data ?? []).map( + (track) { + if (track is Track) { + return track; } else { return TypeConversionUtils.simpleTrack_X_Track( - e, album!); + track, + album!, + ); } }, ), diff --git a/lib/components/shared/track_table/track_tile.dart b/lib/components/shared/track_table/track_tile.dart index 62d33514..5f8b9753 100644 --- a/lib/components/shared/track_table/track_tile.dart +++ b/lib/components/shared/track_table/track_tile.dart @@ -100,10 +100,14 @@ class TrackTile extends HookConsumerWidget { children: [ ClipRRect( borderRadius: BorderRadius.circular(4), - child: UniversalImage( - path: TypeConversionUtils.image_X_UrlString( - track.album?.images, - placeholder: ImagePlaceholder.albumArt, + child: AspectRatio( + aspectRatio: 1, + child: UniversalImage( + path: TypeConversionUtils.image_X_UrlString( + track.album?.images, + placeholder: ImagePlaceholder.albumArt, + ), + fit: BoxFit.cover, ), ), ), diff --git a/lib/components/shared/track_table/tracks_table_view.dart b/lib/components/shared/track_table/tracks_table_view.dart index 9ac8034c..e6835129 100644 --- a/lib/components/shared/track_table/tracks_table_view.dart +++ b/lib/components/shared/track_table/tracks_table_view.dart @@ -1,6 +1,8 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; @@ -43,7 +45,9 @@ class TracksTableView extends HookConsumerWidget { @override Widget build(context, ref) { - final playlist = ref.watch(ProxyPlaylistNotifier.provider); + final theme = Theme.of(context); + + ref.watch(ProxyPlaylistNotifier.provider); final playback = ref.watch(ProxyPlaylistNotifier.notifier); ref.watch(downloadManagerProvider); final downloader = ref.watch(downloadManagerProvider.notifier); @@ -54,11 +58,31 @@ class TracksTableView extends HookConsumerWidget { final showCheck = useState(false); final sortBy = ref.watch(trackCollectionSortState(playlistId ?? '')); + final isFiltering = useState(false); + + final searchController = useTextEditingController(); + final searchFocus = useFocusNode(); + + // this will trigger update on each change in searchController + useValueListenable(searchController); + + final filteredTracks = useMemoized(() { + if (searchController.text.isEmpty) { + return tracks; + } + return tracks + .map((e) => (weightedRatio(e.name!, searchController.text), e)) + .sorted((a, b) => b.$1.compareTo(a.$1)) + .where((e) => e.$1 > 50) + .map((e) => e.$2) + .toList(); + }, [tracks, searchController.text]); + final sortedTracks = useMemoized( () { - return ServiceUtils.sortTracks(tracks, sortBy); + return ServiceUtils.sortTracks(filteredTracks, sortBy); }, - [tracks, sortBy], + [filteredTracks, sortBy], ); final selectedTracks = useMemoized( @@ -68,7 +92,7 @@ class TracksTableView extends HookConsumerWidget { [sortedTracks], ); - final children = sortedTracks.isEmpty + final children = tracks.isEmpty ? [const NotFound(vertical: true)] : [ if (heading != null) heading!, @@ -105,7 +129,7 @@ class TracksTableView extends HookConsumerWidget { : const SizedBox(width: 16), ), Expanded( - flex: 5, + flex: 7, child: Row( children: [ Text( @@ -139,6 +163,28 @@ class TracksTableView extends HookConsumerWidget { .state = value; }, ), + IconButton( + tooltip: context.l10n.filter_playlists, + icon: const Icon(SpotubeIcons.filter), + style: IconButton.styleFrom( + foregroundColor: isFiltering.value + ? theme.colorScheme.secondary + : null, + backgroundColor: isFiltering.value + ? theme.colorScheme.secondaryContainer + : null, + minimumSize: const Size(22, 22), + ), + onPressed: () { + isFiltering.value = !isFiltering.value; + if (isFiltering.value) { + searchFocus.requestFocus(); + } else { + searchController.clear(); + searchFocus.unfocus(); + } + }, + ), AdaptivePopSheetList( tooltip: context.l10n.more_actions, headings: [ @@ -250,6 +296,38 @@ class TracksTableView extends HookConsumerWidget { ], ); }), + AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: isFiltering.value ? 1 : 0, + child: AnimatedSize( + duration: const Duration(milliseconds: 200), + child: SizedBox( + height: isFiltering.value ? 50 : 0, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: CallbackShortcuts( + bindings: { + LogicalKeySet(LogicalKeyboardKey.escape): () { + isFiltering.value = false; + searchController.clear(); + searchFocus.unfocus(); + } + }, + child: TextField( + autofocus: true, + focusNode: searchFocus, + controller: searchController, + decoration: InputDecoration( + hintText: context.l10n.search_tracks, + isDense: true, + prefixIcon: const Icon(SpotubeIcons.search), + ), + ), + ), + ), + ), + ), + ), ...sortedTracks.mapIndexed((i, track) { return TrackTile( index: i, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c820e588..f29f29fc 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -237,5 +237,6 @@ "likes": "Likes", "dislikes": "Dislikes", "views": "Views", - "streamUrl": "Stream URL" + "streamUrl": "Stream URL", + "stop": "Stop" } \ No newline at end of file From cca5625df7e432da8581a4504306baad154deb48 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 17 Jun 2023 13:17:38 +0600 Subject: [PATCH 09/28] fix: pop sheet list not scrollable --- .../adaptive/adaptive_pop_sheet_list.dart | 52 +++++++++---------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart b/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart index 9a9e9630..08cc5551 100644 --- a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart +++ b/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart @@ -74,40 +74,38 @@ class AdaptivePopSheetList extends StatelessWidget { showModalBottomSheet( context: context, useRootNavigator: useRootNavigator, + isScrollControlled: true, + showDragHandle: true, + constraints: BoxConstraints( + maxHeight: mediaQuery.size.height * 0.6, + ), builder: (context) { return Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8.0).copyWith(top: 0), child: DefaultTextStyle( style: theme.textTheme.titleMedium!, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (headings != null) ...[ - Container( - width: 180, - height: 6, - decoration: BoxDecoration( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (headings != null) ...[ + ...headings!, + const SizedBox(height: 8), + Divider( color: theme.colorScheme.primary, - borderRadius: BorderRadius.circular(999), + thickness: 0.3, + endIndent: 16, + indent: 16, ), - ), - const SizedBox(height: 8), - ...headings!, - const SizedBox(height: 8), - Divider( - color: theme.colorScheme.primary, - thickness: 0.3, - endIndent: 16, - indent: 16, - ), + ], + ...children.map( + (item) => _AdaptivePopSheetListItem( + item: item, + onSelected: onSelected, + ), + ) ], - ...children.map( - (item) => _AdaptivePopSheetListItem( - item: item, - onSelected: onSelected, - ), - ) - ], + ), ), ), ); From 7a8bd921047e3766dbbf24449e2873afe3dbecf8 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 18 Jun 2023 10:22:47 +0600 Subject: [PATCH 10/28] fix(track_collection_view): keyboard focus on scroll and no space for search results in playlist/album --- lib/components/shared/heart_button.dart | 15 ++------ .../track_table/track_collection_view.dart | 36 +++++++++++-------- .../shared/track_table/tracks_table_view.dart | 14 ++++++-- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/lib/components/shared/heart_button.dart b/lib/components/shared/heart_button.dart index b64ea6a6..3d68e1fc 100644 --- a/lib/components/shared/heart_button.dart +++ b/lib/components/shared/heart_button.dart @@ -5,13 +5,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/hooks/use_palette_color.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/services/mutations/mutations.dart'; import 'package:spotube/services/queries/queries.dart'; -import 'package:spotube/utils/type_conversion_utils.dart'; - class HeartButton extends HookConsumerWidget { final bool isLiked; final void Function()? onPressed; @@ -163,15 +160,6 @@ class PlaylistHeartButton extends HookConsumerWidget { ], ); - final titleImage = useMemoized( - () => TypeConversionUtils.image_X_UrlString( - playlist.images, - placeholder: ImagePlaceholder.collection, - ), - [playlist.images]); - - final color = usePaletteGenerator(titleImage).dominantColor; - if (me.isLoading || !me.hasData) { return const CircularProgressIndicator(); } @@ -181,7 +169,7 @@ class PlaylistHeartButton extends HookConsumerWidget { tooltip: isLikedQuery.data ?? false ? context.l10n.remove_from_favorites : context.l10n.save_as_favorite, - color: color?.titleTextColor, + color: Colors.white, onPressed: isLikedQuery.hasData ? () { togglePlaylistLike.mutate(isLikedQuery.data!); @@ -224,6 +212,7 @@ class AlbumHeartButton extends HookConsumerWidget { tooltip: isLiked ? context.l10n.remove_from_favorites : context.l10n.save_as_favorite, + color: Colors.white, onPressed: albumIsSaved.hasData ? () { toggleAlbumLike.mutate(isLiked); diff --git a/lib/components/shared/track_table/track_collection_view.dart b/lib/components/shared/track_table/track_collection_view.dart index d6cb29cf..b569d137 100644 --- a/lib/components/shared/track_table/track_collection_view.dart +++ b/lib/components/shared/track_table/track_collection_view.dart @@ -65,7 +65,6 @@ class TrackCollectionView extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final theme = Theme.of(context); - final mediaQuery = MediaQuery.of(context); final auth = ref.watch(AuthenticationNotifier.provider); final color = usePaletteGenerator(titleImage).dominantColor; @@ -343,23 +342,30 @@ class TrackCollectionView extends HookConsumerWidget { } return TracksTableView( - List.from( - (tracksSnapshot.data ?? []).map( - (track) { - if (track is Track) { - return track; - } else { - return TypeConversionUtils.simpleTrack_X_Track( - track, - album!, - ); - } - }, - ), - ), + (tracksSnapshot.data ?? []).map( + (track) { + if (track is Track) { + return track; + } else { + return TypeConversionUtils.simpleTrack_X_Track( + track, + album!, + ); + } + }, + ).toList(), onTrackPlayButtonPressed: onPlay, playlistId: id, userPlaylist: isOwned, + onFiltering: () { + // scroll the flexible space + // to allow more space for search results + controller.animateTo( + 390, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + ); + }, ); }, ) diff --git a/lib/components/shared/track_table/tracks_table_view.dart b/lib/components/shared/track_table/tracks_table_view.dart index e6835129..df3ee164 100644 --- a/lib/components/shared/track_table/tracks_table_view.dart +++ b/lib/components/shared/track_table/tracks_table_view.dart @@ -33,10 +33,14 @@ class TracksTableView extends HookConsumerWidget { final bool isSliver; final Widget? heading; + + final VoidCallback? onFiltering; + const TracksTableView( this.tracks, { Key? key, this.onTrackPlayButtonPressed, + this.onFiltering, this.userPlaylist = false, this.playlistId, this.heading, @@ -46,6 +50,7 @@ class TracksTableView extends HookConsumerWidget { @override Widget build(context, ref) { final theme = Theme.of(context); + final mediaQuery = MediaQuery.of(context); ref.watch(ProxyPlaylistNotifier.provider); final playback = ref.watch(ProxyPlaylistNotifier.notifier); @@ -178,6 +183,7 @@ class TracksTableView extends HookConsumerWidget { onPressed: () { isFiltering.value = !isFiltering.value; if (isFiltering.value) { + onFiltering?.call(); searchFocus.requestFocus(); } else { searchController.clear(); @@ -314,7 +320,6 @@ class TracksTableView extends HookConsumerWidget { } }, child: TextField( - autofocus: true, focusNode: searchFocus, controller: searchController, decoration: InputDecoration( @@ -375,7 +380,12 @@ class TracksTableView extends HookConsumerWidget { } }, ); - }).toList(), + }), + // extra space for mobile devices where keyboard takes half of the screen + if (isFiltering.value) + SizedBox( + height: mediaQuery.size.height * .75, //75% of the screen + ), ]; if (isSliver) { From dce1b88694cfcb6b7e63d6ee614ac1dbbd017f6e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 18 Jun 2023 12:07:26 +0600 Subject: [PATCH 11/28] feat(playlist,album page): play and shuffle take full width on smaller screens, add new xs breakpoint --- lib/components/album/album_card.dart | 2 +- lib/components/artist/artist_card.dart | 1 + .../playlist_generate/multi_select_field.dart | 2 +- lib/components/library/user_albums.dart | 1 + .../player/player_track_details.dart | 2 +- .../playlist/playlist_create_dialog.dart | 2 +- lib/components/root/bottom_player.dart | 3 +- lib/components/root/sidebar.dart | 9 +- .../shared/adaptive/adaptive_list_tile.dart | 9 +- lib/components/shared/playbutton_card.dart | 2 + .../shimmers/shimmer_artist_profile.dart | 1 + .../shared/shimmers/shimmer_categories.dart | 1 + .../shared/shimmers/shimmer_lyrics.dart | 2 +- .../shimmers/shimmer_playbutton_card.dart | 1 + .../shared/themed_button_tab_bar.dart | 1 + .../track_collection_heading.dart | 195 ++++++++++++++++++ .../track_collection_view.dart | 160 ++------------ .../shared/track_table/track_tile.dart | 4 +- .../shared/track_table/tracks_table_view.dart | 1 + lib/extensions/constrains.dart | 18 +- lib/hooks/use_breakpoint_value.dart | 17 +- lib/pages/album/album.dart | 4 +- lib/pages/artist/artist.dart | 9 +- lib/pages/desktop_login/desktop_login.dart | 2 +- lib/pages/playlist/playlist.dart | 4 +- 25 files changed, 276 insertions(+), 177 deletions(-) create mode 100644 lib/components/shared/track_table/track_collection_view/track_collection_heading.dart rename lib/components/shared/track_table/{ => track_collection_view}/track_collection_view.dart (50%) diff --git a/lib/components/album/album_card.dart b/lib/components/album/album_card.dart index 5c94caf0..da66f276 100644 --- a/lib/components/album/album_card.dart +++ b/lib/components/album/album_card.dart @@ -53,7 +53,7 @@ class AlbumCard extends HookConsumerWidget { [playlistNotifier, query?.data, album.tracks], ); final int marginH = - useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20); + useBreakpointValue(xs: 10, sm: 10, md: 15, lg: 20, xl: 20, xxl: 20); final updating = useState(false); final spotify = ref.watch(spotifyProvider); diff --git a/lib/components/artist/artist_card.dart b/lib/components/artist/artist_card.dart index cb695f6f..993e9f6a 100644 --- a/lib/components/artist/artist_card.dart +++ b/lib/components/artist/artist_card.dart @@ -35,6 +35,7 @@ class ArtistCard extends HookConsumerWidget { final radius = BorderRadius.circular(15); final double size = useBreakpointValue( + xs: 130, sm: 130, md: 150, others: 170, diff --git a/lib/components/library/playlist_generate/multi_select_field.dart b/lib/components/library/playlist_generate/multi_select_field.dart index d8de3da5..ed5eb38f 100644 --- a/lib/components/library/playlist_generate/multi_select_field.dart +++ b/lib/components/library/playlist_generate/multi_select_field.dart @@ -188,7 +188,7 @@ class _MultiSelectDialog extends HookWidget { return AlertDialog( scrollable: true, title: dialogTitle ?? const Text('Select'), - contentPadding: mediaQuery.isSm ? const EdgeInsets.all(16) : null, + contentPadding: mediaQuery.mdAndUp ? null : const EdgeInsets.all(16), insetPadding: const EdgeInsets.all(16), actions: [ OutlinedButton( diff --git a/lib/components/library/user_albums.dart b/lib/components/library/user_albums.dart index 8553a8c3..014a84f6 100644 --- a/lib/components/library/user_albums.dart +++ b/lib/components/library/user_albums.dart @@ -24,6 +24,7 @@ class UserAlbums extends HookConsumerWidget { final albumsQuery = useQueries.album.ofMine(ref); final spacing = useBreakpointValue( + xs: 0, sm: 0, others: 20, ); diff --git a/lib/components/player/player_track_details.dart b/lib/components/player/player_track_details.dart index 8b66b8b7..36c6ab25 100644 --- a/lib/components/player/player_track_details.dart +++ b/lib/components/player/player_track_details.dart @@ -39,7 +39,7 @@ class PlayerTrackDetails extends HookConsumerWidget { ), ), ), - if (mediaQuery.isSm || mediaQuery.isMd) + if (mediaQuery.mdAndDown) Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/components/playlist/playlist_create_dialog.dart b/lib/components/playlist/playlist_create_dialog.dart index f5d32186..b7cee79d 100644 --- a/lib/components/playlist/playlist_create_dialog.dart +++ b/lib/components/playlist/playlist_create_dialog.dart @@ -121,7 +121,7 @@ class PlaylistCreateDialogButton extends HookConsumerWidget { final mediaQuery = MediaQuery.of(context); final spotify = ref.watch(spotifyProvider); - if (mediaQuery.isSm) { + if (mediaQuery.smAndDown) { return ElevatedButton( style: FilledButton.styleFrom( foregroundColor: Theme.of(context).colorScheme.primary, diff --git a/lib/components/root/bottom_player.dart b/lib/components/root/bottom_player.dart index 2b0bc1ff..70d14f8a 100644 --- a/lib/components/root/bottom_player.dart +++ b/lib/components/root/bottom_player.dart @@ -58,8 +58,7 @@ class BottomPlayer extends HookConsumerWidget { // returning an empty non spacious Container as the overlay will take // place in the global overlay stack aka [_entries] if (layoutMode == LayoutMode.compact || - ((mediaQuery.isSm || mediaQuery.isMd) && - layoutMode == LayoutMode.adaptive)) { + ((mediaQuery.mdAndDown) && layoutMode == LayoutMode.adaptive)) { return PlayerOverlay(albumArt: albumArt); } diff --git a/lib/components/root/sidebar.dart b/lib/components/root/sidebar.dart index b6c90e77..64525823 100644 --- a/lib/components/root/sidebar.dart +++ b/lib/components/root/sidebar.dart @@ -82,16 +82,17 @@ class Sidebar extends HookConsumerWidget { }, [controller]); useEffect(() { + if (!context.mounted) return; if (mediaQuery.lgAndUp && !controller.extended) { controller.setExtended(true); - } else if ((mediaQuery.isSm || mediaQuery.isMd) && controller.extended) { + } else if (mediaQuery.mdAndDown && controller.extended) { controller.setExtended(false); } return null; }, [mediaQuery, controller]); if (layoutMode == LayoutMode.compact || - (mediaQuery.isSm && layoutMode == LayoutMode.adaptive)) { + (mediaQuery.smAndDown && layoutMode == LayoutMode.adaptive)) { return Scaffold(body: child); } @@ -186,7 +187,7 @@ class SidebarHeader extends HookWidget { final mediaQuery = MediaQuery.of(context); final theme = Theme.of(context); - if (mediaQuery.isSm || mediaQuery.isMd) { + if (mediaQuery.mdAndDown) { return Container( height: 40, width: 40, @@ -236,7 +237,7 @@ class SidebarFooter extends HookConsumerWidget { final auth = ref.watch(AuthenticationNotifier.provider); - if (mediaQuery.isSm || mediaQuery.isMd) { + if (mediaQuery.mdAndDown) { return IconButton( icon: const Icon(SpotubeIcons.settings), onPressed: () => Sidebar.goToSettings(context), diff --git a/lib/components/shared/adaptive/adaptive_list_tile.dart b/lib/components/shared/adaptive/adaptive_list_tile.dart index 965250a4..33df44c1 100644 --- a/lib/components/shared/adaptive/adaptive_list_tile.dart +++ b/lib/components/shared/adaptive/adaptive_list_tile.dart @@ -17,7 +17,7 @@ class AdaptiveListTile extends HookWidget { this.title, this.subtitle, this.leading, - this.breakOn , + this.breakOn, }); @override @@ -27,10 +27,11 @@ class AdaptiveListTile extends HookWidget { return ListTile( title: title, subtitle: subtitle, - trailing: - breakOn ?? mediaQuery.isSm ? null : trailing?.call(context, null), + trailing: breakOn ?? mediaQuery.smAndDown + ? null + : trailing?.call(context, null), leading: leading, - onTap: breakOn ?? mediaQuery.isSm + onTap: breakOn ?? mediaQuery.smAndDown ? () { onTap?.call(); showDialog( diff --git a/lib/components/shared/playbutton_card.dart b/lib/components/shared/playbutton_card.dart index 517a7e2d..f381e7ce 100644 --- a/lib/components/shared/playbutton_card.dart +++ b/lib/components/shared/playbutton_card.dart @@ -40,6 +40,7 @@ class PlaybuttonCard extends HookWidget { final radius = BorderRadius.circular(15); final double size = useBreakpointValue( + xs: 130, sm: 130, md: 150, others: 170, @@ -47,6 +48,7 @@ class PlaybuttonCard extends HookWidget { 170; final end = useBreakpointValue( + xs: 15, sm: 15, others: 20, ) ?? diff --git a/lib/components/shared/shimmers/shimmer_artist_profile.dart b/lib/components/shared/shimmers/shimmer_artist_profile.dart index 077e24e3..940c4e81 100644 --- a/lib/components/shared/shimmers/shimmer_artist_profile.dart +++ b/lib/components/shared/shimmers/shimmer_artist_profile.dart @@ -21,6 +21,7 @@ class ShimmerArtistProfile extends HookWidget { shimmerTheme.shimmerBackgroundColor ?? Colors.grey; final avatarWidth = useBreakpointValue( + xs: MediaQuery.of(context).size.width * 0.80, sm: MediaQuery.of(context).size.width * 0.80, md: MediaQuery.of(context).size.width * 0.50, lg: MediaQuery.of(context).size.width * 0.30, diff --git a/lib/components/shared/shimmers/shimmer_categories.dart b/lib/components/shared/shimmers/shimmer_categories.dart index d93b70a4..e9f442d4 100644 --- a/lib/components/shared/shimmers/shimmer_categories.dart +++ b/lib/components/shared/shimmers/shimmer_categories.dart @@ -18,6 +18,7 @@ class ShimmerCategories extends HookWidget { shimmerTheme.shimmerBackgroundColor ?? Colors.grey; final shimmerCount = useBreakpointValue( + xs: 2, sm: 2, md: 3, lg: 3, diff --git a/lib/components/shared/shimmers/shimmer_lyrics.dart b/lib/components/shared/shimmers/shimmer_lyrics.dart index fd88ac6b..b0fba340 100644 --- a/lib/components/shared/shimmers/shimmer_lyrics.dart +++ b/lib/components/shared/shimmers/shimmer_lyrics.dart @@ -32,7 +32,7 @@ class ShimmerLyrics extends HookWidget { if (mediaQuery.isMd) { widthsCp.removeLast(); } - if (mediaQuery.isSm) { + if (mediaQuery.smAndDown) { widthsCp.removeLast(); widthsCp.removeLast(); } diff --git a/lib/components/shared/shimmers/shimmer_playbutton_card.dart b/lib/components/shared/shimmers/shimmer_playbutton_card.dart index 48671aa6..82da5bd9 100644 --- a/lib/components/shared/shimmers/shimmer_playbutton_card.dart +++ b/lib/components/shared/shimmers/shimmer_playbutton_card.dart @@ -86,6 +86,7 @@ class ShimmerPlaybuttonCard extends HookWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final Size size = useBreakpointValue( + xs: const Size(130, 200), sm: const Size(130, 200), md: const Size(150, 220), others: const Size(170, 240), diff --git a/lib/components/shared/themed_button_tab_bar.dart b/lib/components/shared/themed_button_tab_bar.dart index ecf64134..ee107088 100644 --- a/lib/components/shared/themed_button_tab_bar.dart +++ b/lib/components/shared/themed_button_tab_bar.dart @@ -18,6 +18,7 @@ class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget { ); final breakpoint = useBreakpointValue( + xs: 85.0, sm: 85.0, md: 35.0, others: 0.0, diff --git a/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart b/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart new file mode 100644 index 00000000..45104eee --- /dev/null +++ b/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart @@ -0,0 +1,195 @@ +import 'dart:ui'; + +import 'package:fl_query/fl_query.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:palette_generator/palette_generator.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/assets.gen.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/album/album_card.dart'; +import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/extensions/context.dart'; + +class TrackCollectionHeading extends HookConsumerWidget { + final String title; + final String? description; + final String titleImage; + final List buttons; + final AlbumSimple? album; + final Query, T> tracksSnapshot; + final bool isPlaying; + final void Function([Track? currentTrack]) onPlay; + final void Function([Track? currentTrack]) onShuffledPlay; + final PaletteColor? color; + + const TrackCollectionHeading({ + Key? key, + required this.title, + required this.titleImage, + required this.buttons, + required this.tracksSnapshot, + required this.isPlaying, + required this.onPlay, + required this.onShuffledPlay, + required this.color, + this.description, + this.album, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final theme = Theme.of(context); + + return LayoutBuilder( + builder: (context, constrains) { + return DecoratedBox( + decoration: BoxDecoration( + image: DecorationImage( + image: UniversalImage.imageProvider(titleImage), + fit: BoxFit.cover, + ), + ), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.black45, + theme.colorScheme.surface, + ], + begin: const FractionalOffset(0, 0), + end: const FractionalOffset(0, 1), + tileMode: TileMode.clamp, + ), + ), + child: Material( + type: MaterialType.transparency, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + ), + child: Flex( + direction: + constrains.mdAndDown ? Axis.vertical : Axis.horizontal, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 200), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: UniversalImage( + path: titleImage, + placeholder: Assets.albumPlaceholder.path, + ), + ), + ), + const SizedBox(width: 10, height: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Text( + title, + style: theme.textTheme.titleLarge!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + if (album != null) + Text( + "${AlbumType.from(album?.albumType).formatted} • ${context.l10n.released} • ${DateTime.tryParse( + album?.releaseDate ?? "", + )?.year}", + style: theme.textTheme.titleMedium!.copyWith( + color: Colors.white, + fontWeight: FontWeight.normal, + ), + ), + if (description != null) + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: constrains.mdAndDown ? 400 : 300, + ), + child: Text( + description!, + style: const TextStyle(color: Colors.white), + maxLines: 2, + overflow: TextOverflow.fade, + ), + ), + const SizedBox(height: 10), + IconTheme( + data: theme.iconTheme.copyWith( + color: Colors.white, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: buttons, + ), + ), + const SizedBox(height: 10), + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: constrains.mdAndDown ? 400 : 300, + ), + child: Row( + mainAxisSize: constrains.smAndUp + ? MainAxisSize.min + : MainAxisSize.min, + children: [ + Expanded( + child: FilledButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: color?.color, + ), + label: Text(context.l10n.shuffle), + icon: const Icon(SpotubeIcons.shuffle), + onPressed: + tracksSnapshot.data == null || isPlaying + ? null + : onShuffledPlay, + ), + ), + const SizedBox(width: 10), + Expanded( + child: FilledButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: color?.color, + foregroundColor: color?.bodyTextColor, + ), + onPressed: tracksSnapshot.data != null + ? onPlay + : null, + icon: Icon( + isPlaying + ? SpotubeIcons.stop + : SpotubeIcons.play, + ), + label: Text( + isPlaying + ? context.l10n.stop + : context.l10n.play, + ), + ), + ), + ], + ), + ), + ], + ) + ], + ), + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/components/shared/track_table/track_collection_view.dart b/lib/components/shared/track_table/track_collection_view/track_collection_view.dart similarity index 50% rename from lib/components/shared/track_table/track_collection_view.dart rename to lib/components/shared/track_table/track_collection_view/track_collection_view.dart index b569d137..b2e639b5 100644 --- a/lib/components/shared/track_table/track_collection_view.dart +++ b/lib/components/shared/track_table/track_collection_view/track_collection_view.dart @@ -1,16 +1,12 @@ -import 'dart:ui'; - import 'package:fl_query/fl_query.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/album/album_card.dart'; import 'package:spotube/components/shared/shimmers/shimmer_track_tile.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/track_table/track_collection_view/track_collection_heading.dart'; import 'package:spotube/components/shared/track_table/tracks_table_view.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/use_custom_status_bar_color.dart'; @@ -31,8 +27,8 @@ class TrackCollectionView extends HookConsumerWidget { final String titleImage; final bool isPlaying; final void Function([Track? currentTrack]) onPlay; - final void Function() onAddToQueue; final void Function([Track? currentTrack]) onShuffledPlay; + final void Function() onAddToQueue; final void Function() onShare; final Widget? heartBtn; final AlbumSimple? album; @@ -187,145 +183,17 @@ class TrackCollectionView extends HookConsumerWidget { : null, centerTitle: true, flexibleSpace: FlexibleSpaceBar( - background: DecoratedBox( - decoration: BoxDecoration( - image: DecorationImage( - image: UniversalImage.imageProvider(titleImage), - fit: BoxFit.cover, - ), - ), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Colors.black45, - theme.colorScheme.surface, - ], - begin: const FractionalOffset(0, 0), - end: const FractionalOffset(0, 1), - tileMode: TileMode.clamp, - ), - ), - child: Material( - type: MaterialType.transparency, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - ), - child: Wrap( - spacing: 20, - runSpacing: 20, - crossAxisAlignment: WrapCrossAlignment.center, - alignment: WrapAlignment.center, - runAlignment: WrapAlignment.center, - children: [ - Container( - constraints: - const BoxConstraints(maxHeight: 200), - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: UniversalImage( - path: titleImage, - placeholder: - Assets.albumPlaceholder.path, - ), - ), - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: theme.textTheme.titleLarge! - .copyWith( - color: Colors.white, - fontWeight: FontWeight.w600, - ), - ), - if (album != null) - Text( - "${AlbumType.from(album?.albumType).formatted} • ${context.l10n.released} • ${DateTime.tryParse( - album?.releaseDate ?? "", - )?.year}", - style: theme.textTheme.titleMedium! - .copyWith( - color: Colors.white, - fontWeight: FontWeight.normal, - ), - ), - if (description != null) - Text( - description!, - style: const TextStyle( - color: Colors.white), - maxLines: 2, - overflow: TextOverflow.fade, - ), - const SizedBox(height: 10), - IconTheme( - data: theme.iconTheme.copyWith( - color: Colors.white, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: buttons, - ), - ), - const SizedBox(height: 10), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - FilledButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: color?.color, - ), - label: Text(context.l10n.shuffle), - icon: const Icon( - SpotubeIcons.shuffle), - onPressed: - tracksSnapshot.data == null || - isPlaying - ? null - : onShuffledPlay, - ), - const SizedBox(width: 10), - FilledButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: color?.color, - foregroundColor: - color?.bodyTextColor, - ), - onPressed: - tracksSnapshot.data != null - ? onPlay - : null, - icon: Icon( - isPlaying - ? SpotubeIcons.stop - : SpotubeIcons.play, - ), - label: Text( - isPlaying - ? context.l10n.stop - : context.l10n.play, - ), - ), - ], - ), - ], - ) - ], - ), - ), - ), - ), - ), + background: TrackCollectionHeading( + color: color, + title: title, + description: description, + titleImage: titleImage, + isPlaying: isPlaying, + onPlay: onPlay, + onShuffledPlay: onShuffledPlay, + tracksSnapshot: tracksSnapshot, + buttons: buttons, + album: album, ), ), ), @@ -361,7 +229,7 @@ class TrackCollectionView extends HookConsumerWidget { // scroll the flexible space // to allow more space for search results controller.animateTo( - 390, + 330, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, ); diff --git a/lib/components/shared/track_table/track_tile.dart b/lib/components/shared/track_table/track_tile.dart index 5f8b9753..5a9f33e7 100644 --- a/lib/components/shared/track_table/track_tile.dart +++ b/lib/components/shared/track_table/track_tile.dart @@ -61,7 +61,7 @@ class TrackTile extends HookConsumerWidget { return LayoutBuilder(builder: (context, constrains) { return HoverBuilder( - permanentState: isPlaying || constrains.isSm ? true : null, + permanentState: isPlaying || constrains.smAndDown ? true : null, builder: (context, isHovering) { return ListTile( selected: isPlaying, @@ -89,7 +89,7 @@ class TrackTile extends HookConsumerWidget { ), ), ) - else if (constrains.isSm) + else if (constrains.smAndDown) const SizedBox(width: 16), if (onChanged != null) Checkbox.adaptive( diff --git a/lib/components/shared/track_table/tracks_table_view.dart b/lib/components/shared/track_table/tracks_table_view.dart index df3ee164..d9589552 100644 --- a/lib/components/shared/track_table/tracks_table_view.dart +++ b/lib/components/shared/track_table/tracks_table_view.dart @@ -390,6 +390,7 @@ class TracksTableView extends HookConsumerWidget { if (isSliver) { return SliverSafeArea( + top: false, sliver: SliverList(delegate: SliverChildListDelegate(children)), ); } diff --git a/lib/extensions/constrains.dart b/lib/extensions/constrains.dart index b36087c4..8ee22320 100644 --- a/lib/extensions/constrains.dart +++ b/lib/extensions/constrains.dart @@ -1,25 +1,39 @@ import 'package:flutter/widgets.dart'; extension ContainerBreakpoints on BoxConstraints { - bool get isSm => biggest.width <= 640; + bool get isXs => biggest.width <= 480; + bool get isSm => biggest.width > 480 && biggest.width <= 640; bool get isMd => biggest.width > 640 && biggest.width <= 768; bool get isLg => biggest.width > 768 && biggest.width <= 1024; bool get isXl => biggest.width > 1024 && biggest.width <= 1280; bool get is2Xl => biggest.width > 1280; + bool get smAndUp => isSm || isMd || isLg || isXl || is2Xl; bool get mdAndUp => isMd || isLg || isXl || is2Xl; bool get lgAndUp => isLg || isXl || is2Xl; bool get xlAndUp => isXl || is2Xl; + + bool get smAndDown => isXs || isSm; + bool get mdAndDown => isXs || isSm || isMd; + bool get lgAndDown => isXs || isSm || isMd || isLg; + bool get xlAndDown => isXs || isSm || isMd || isLg || isXl; } extension ScreenBreakpoints on MediaQueryData { - bool get isSm => size.width <= 640; + bool get isXs => size.width <= 480; + bool get isSm => size.width > 480 && size.width <= 640; bool get isMd => size.width > 640 && size.width <= 768; bool get isLg => size.width > 768 && size.width <= 1024; bool get isXl => size.width > 1024 && size.width <= 1280; bool get is2Xl => size.width > 1280; + bool get smAndUp => isSm || isMd || isLg || isXl || is2Xl; bool get mdAndUp => isMd || isLg || isXl || is2Xl; bool get lgAndUp => isLg || isXl || is2Xl; bool get xlAndUp => isXl || is2Xl; + + bool get smAndDown => isXs || isSm; + bool get mdAndDown => isXs || isSm || isMd; + bool get lgAndDown => isXs || isSm || isMd || isLg; + bool get xlAndDown => isXs || isSm || isMd || isLg || isXl; } diff --git a/lib/hooks/use_breakpoint_value.dart b/lib/hooks/use_breakpoint_value.dart index 854af39a..b2592124 100644 --- a/lib/hooks/use_breakpoint_value.dart +++ b/lib/hooks/use_breakpoint_value.dart @@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:spotube/extensions/constrains.dart'; T useBreakpointValue({ + T? xs, T? sm, T? md, T? lg, @@ -10,8 +11,12 @@ T useBreakpointValue({ T? xxl, T? others, }) { - final isSomeNull = - sm == null || md == null || lg == null || xl == null || xxl == null; + final isSomeNull = xs == null || + sm == null || + md == null || + lg == null || + xl == null || + xxl == null; assert( (isSomeNull && others != null) || (!isSomeNull && others == null), 'You must provide a value for all breakpoints or a default value for others', @@ -20,7 +25,9 @@ T useBreakpointValue({ final mediaQuery = MediaQuery.of(context); if (isSomeNull) { - if (mediaQuery.isSm) { + if (mediaQuery.isXs) { + return xs ?? others!; + } else if (mediaQuery.isSm) { return sm ?? others!; } else if (mediaQuery.isMd) { return md ?? others!; @@ -32,7 +39,9 @@ T useBreakpointValue({ return lg ?? others!; } } else { - if (mediaQuery.isSm) { + if (mediaQuery.isXs) { + return xs; + } else if (mediaQuery.isSm) { return sm; } else if (mediaQuery.isMd) { return md; diff --git a/lib/pages/album/album.dart b/lib/pages/album/album.dart index ca04d726..69b542ee 100644 --- a/lib/pages/album/album.dart +++ b/lib/pages/album/album.dart @@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/shared/heart_button.dart'; -import 'package:spotube/components/shared/track_table/track_collection_view.dart'; +import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_view.dart'; import 'package:spotube/components/shared/track_table/tracks_table_view.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; @@ -67,7 +67,7 @@ class AlbumPage extends HookConsumerWidget { tracksSnapshot: tracksSnapshot, album: album, routePath: "/album/${album.id}", - bottomSpace: mediaQuery.isSm || mediaQuery.isMd, + bottomSpace: mediaQuery.mdAndDown, onPlay: ([track]) { if (tracksSnapshot.hasData) { if (!isAlbumPlaying) { diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index 865c43bc..44e40423 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -39,6 +39,7 @@ class ArtistPage extends HookConsumerWidget { final scaffoldMessenger = ScaffoldMessenger.of(context); final textTheme = theme.textTheme; final chipTextVariant = useBreakpointValue( + xs: textTheme.bodySmall, sm: textTheme.bodySmall, md: textTheme.bodyMedium, lg: textTheme.bodyLarge, @@ -49,6 +50,7 @@ class ArtistPage extends HookConsumerWidget { final mediaQuery = MediaQuery.of(context); final avatarWidth = useBreakpointValue( + xs: mediaQuery.size.width * 0.50, sm: mediaQuery.size.width * 0.50, md: mediaQuery.size.width * 0.40, lg: mediaQuery.size.width * 0.18, @@ -155,7 +157,7 @@ class ArtistPage extends HookConsumerWidget { ), Text( data.name!, - style: mediaQuery.isSm + style: mediaQuery.smAndDown ? textTheme.headlineSmall : textTheme.headlineMedium, ), @@ -166,8 +168,9 @@ class ArtistPage extends HookConsumerWidget { ), ), style: textTheme.bodyMedium?.copyWith( - fontWeight: - mediaQuery.isSm ? null : FontWeight.bold, + fontWeight: mediaQuery.mdAndUp + ? FontWeight.bold + : null, ), ), const SizedBox(height: 20), diff --git a/lib/pages/desktop_login/desktop_login.dart b/lib/pages/desktop_login/desktop_login.dart index 02f580f0..c2cc3695 100644 --- a/lib/pages/desktop_login/desktop_login.dart +++ b/lib/pages/desktop_login/desktop_login.dart @@ -35,7 +35,7 @@ class DesktopLoginPage extends HookConsumerWidget { children: [ Assets.spotubeLogoPng.image( width: MediaQuery.of(context).size.width * - (mediaQuery.isSm || mediaQuery.isMd ? .5 : .3), + (mediaQuery.mdAndDown ? .5 : .3), ), Text( context.l10n.add_spotify_credentials, diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index 15cc246b..170d9693 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -2,7 +2,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/components/shared/heart_button.dart'; -import 'package:spotube/components/shared/track_table/track_collection_view.dart'; +import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_view.dart'; import 'package:spotube/components/shared/track_table/tracks_table_view.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/models/logger.dart'; @@ -99,7 +99,7 @@ class PlaylistView extends HookConsumerWidget { playlistNotifier.addTracks(tracksSnapshot.data!); } }, - bottomSpace: mediaQuery.isSm || mediaQuery.isMd, + bottomSpace: mediaQuery.mdAndDown, showShare: playlist.id != "user-liked-tracks", routePath: "/playlist/${playlist.id}", onShare: () { From 20274b1c653e6e4b2e6564d702676b2a6cd6cf56 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 18 Jun 2023 12:49:38 +0600 Subject: [PATCH 12/28] refactor: expandable filter field on genre and user local tracks page --- lib/components/library/user_local_tracks.dart | 28 +++--- .../expandable_search/expandable_search.dart | 91 +++++++++++++++++++ .../shared/track_table/tracks_table_view.dart | 57 ++---------- lib/pages/home/genres.dart | 81 +++++++++++------ 4 files changed, 170 insertions(+), 87 deletions(-) create mode 100644 lib/components/shared/expandable_search/expandable_search.dart diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index 2431956e..217e2d7f 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -15,7 +15,7 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/shared/compact_search.dart'; +import 'package:spotube/components/shared/expandable_search/expandable_search.dart'; import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart'; import 'package:spotube/components/shared/sort_tracks_dropdown.dart'; import 'package:spotube/components/shared/track_table/track_tile.dart'; @@ -160,7 +160,10 @@ class UserLocalTracks extends HookConsumerWidget { playlist.containsTracks(trackSnapshot.value ?? []); final isMounted = useIsMounted(); - final searchText = useState(""); + final searchController = useTextEditingController(); + useValueListenable(searchController); + final searchFocus = useFocusNode(); + final isFiltering = useState(false); useAsyncEffect( () async { @@ -175,11 +178,6 @@ class UserLocalTracks extends HookConsumerWidget { [], ); - final searchbar = CompactSearch( - onChanged: (value) => searchText.value = value, - placeholder: context.l10n.search_local_tracks, - ); - return Column( children: [ Padding( @@ -213,7 +211,10 @@ class UserLocalTracks extends HookConsumerWidget { ), ), const Spacer(), - searchbar, + ExpandableSearchButton( + isFiltering: isFiltering, + searchFocus: searchFocus, + ), const SizedBox(width: 10), SortTracksDropdown( value: sortBy.value, @@ -231,6 +232,11 @@ class UserLocalTracks extends HookConsumerWidget { ], ), ), + ExpandableSearchField( + searchController: searchController, + searchFocus: searchFocus, + isFiltering: isFiltering, + ), trackSnapshot.when( data: (tracks) { final sortedTracks = useMemoized(() { @@ -238,14 +244,14 @@ class UserLocalTracks extends HookConsumerWidget { }, [sortBy.value, tracks]); final filteredTracks = useMemoized(() { - if (searchText.value.isEmpty) { + if (searchController.text.isEmpty) { return sortedTracks; } return sortedTracks .map((e) => ( weightedRatio( "${e.name} - ${TypeConversionUtils.artists_X_String(e.artists ?? [])}", - searchText.value, + searchController.text, ), e, )) @@ -257,7 +263,7 @@ class UserLocalTracks extends HookConsumerWidget { .map((e) => e.$2) .toList() .toList(); - }, [searchText.value, sortedTracks]); + }, [searchController.text, sortedTracks]); return Expanded( child: RefreshIndicator( diff --git a/lib/components/shared/expandable_search/expandable_search.dart b/lib/components/shared/expandable_search/expandable_search.dart new file mode 100644 index 00000000..684e373e --- /dev/null +++ b/lib/components/shared/expandable_search/expandable_search.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/extensions/context.dart'; + +class ExpandableSearchField extends StatelessWidget { + final ValueNotifier isFiltering; + final TextEditingController searchController; + final FocusNode searchFocus; + + const ExpandableSearchField({ + Key? key, + required this.isFiltering, + required this.searchController, + required this.searchFocus, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: isFiltering.value ? 1 : 0, + child: AnimatedSize( + duration: const Duration(milliseconds: 200), + child: SizedBox( + height: isFiltering.value ? 50 : 0, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: CallbackShortcuts( + bindings: { + LogicalKeySet(LogicalKeyboardKey.escape): () { + isFiltering.value = false; + searchController.clear(); + searchFocus.unfocus(); + } + }, + child: TextField( + focusNode: searchFocus, + controller: searchController, + decoration: InputDecoration( + hintText: context.l10n.search_tracks, + isDense: true, + prefixIcon: const Icon(SpotubeIcons.search), + ), + ), + ), + ), + ), + ), + ); + } +} + +class ExpandableSearchButton extends StatelessWidget { + final ValueNotifier isFiltering; + final FocusNode searchFocus; + final Widget icon; + final ValueChanged? onPressed; + + const ExpandableSearchButton({ + Key? key, + required this.isFiltering, + required this.searchFocus, + this.icon = const Icon(SpotubeIcons.filter), + this.onPressed, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return IconButton( + icon: icon, + style: IconButton.styleFrom( + backgroundColor: + isFiltering.value ? theme.colorScheme.secondaryContainer : null, + foregroundColor: isFiltering.value ? theme.colorScheme.secondary : null, + minimumSize: const Size(25, 25), + ), + onPressed: () { + isFiltering.value = !isFiltering.value; + if (isFiltering.value) { + searchFocus.requestFocus(); + } else { + searchFocus.unfocus(); + } + onPressed?.call(isFiltering.value); + }, + ); + } +} diff --git a/lib/components/shared/track_table/tracks_table_view.dart b/lib/components/shared/track_table/tracks_table_view.dart index d9589552..f6340b33 100644 --- a/lib/components/shared/track_table/tracks_table_view.dart +++ b/lib/components/shared/track_table/tracks_table_view.dart @@ -10,6 +10,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/shared/dialogs/confirm_download_dialog.dart'; import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart'; +import 'package:spotube/components/shared/expandable_search/expandable_search.dart'; import 'package:spotube/components/shared/fallbacks/not_found.dart'; import 'package:spotube/components/shared/sort_tracks_dropdown.dart'; import 'package:spotube/components/shared/track_table/track_tile.dart'; @@ -168,26 +169,12 @@ class TracksTableView extends HookConsumerWidget { .state = value; }, ), - IconButton( - tooltip: context.l10n.filter_playlists, - icon: const Icon(SpotubeIcons.filter), - style: IconButton.styleFrom( - foregroundColor: isFiltering.value - ? theme.colorScheme.secondary - : null, - backgroundColor: isFiltering.value - ? theme.colorScheme.secondaryContainer - : null, - minimumSize: const Size(22, 22), - ), - onPressed: () { - isFiltering.value = !isFiltering.value; + ExpandableSearchButton( + isFiltering: isFiltering, + searchFocus: searchFocus, + onPressed: (value) { if (isFiltering.value) { onFiltering?.call(); - searchFocus.requestFocus(); - } else { - searchController.clear(); - searchFocus.unfocus(); } }, ), @@ -302,36 +289,10 @@ class TracksTableView extends HookConsumerWidget { ], ); }), - AnimatedOpacity( - duration: const Duration(milliseconds: 200), - opacity: isFiltering.value ? 1 : 0, - child: AnimatedSize( - duration: const Duration(milliseconds: 200), - child: SizedBox( - height: isFiltering.value ? 50 : 0, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: CallbackShortcuts( - bindings: { - LogicalKeySet(LogicalKeyboardKey.escape): () { - isFiltering.value = false; - searchController.clear(); - searchFocus.unfocus(); - } - }, - child: TextField( - focusNode: searchFocus, - controller: searchController, - decoration: InputDecoration( - hintText: context.l10n.search_tracks, - isDense: true, - prefixIcon: const Icon(SpotubeIcons.search), - ), - ), - ), - ), - ), - ), + ExpandableSearchField( + isFiltering: isFiltering, + searchController: searchController, + searchFocus: searchFocus, ), ...sortedTracks.mapIndexed((i, track) { return TrackTile( diff --git a/lib/pages/home/genres.dart b/lib/pages/home/genres.dart index 8bc4fd36..2aad00f7 100644 --- a/lib/pages/home/genres.dart +++ b/lib/pages/home/genres.dart @@ -1,11 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:collection/collection.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/genre/category_card.dart'; import 'package:spotube/components/shared/compact_search.dart'; +import 'package:spotube/components/shared/expandable_search/expandable_search.dart'; import 'package:spotube/components/shared/shimmers/shimmer_categories.dart'; import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/extensions/context.dart'; @@ -18,15 +21,21 @@ class GenrePage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final theme = Theme.of(context); final scrollController = useScrollController(); final recommendationMarket = ref.watch( userPreferencesProvider.select((s) => s.recommendationMarket), ); final categoriesQuery = useQueries.category.list(ref, recommendationMarket); + final isFiltering = useState(false); final isMounted = useIsMounted(); - final searchText = useState(""); + final searchController = useTextEditingController(); + final searchFocus = useFocusNode(); + + useValueListenable(searchController); + final categories = useMemoized( () { final categories = categoriesQuery.pages @@ -34,12 +43,12 @@ class GenrePage extends HookConsumerWidget { (page) => page.items ?? const Iterable.empty(), ) .toList(); - if (searchText.value.isEmpty) { + if (searchController.text.isEmpty) { return categories; } return categories .map((e) => ( - weightedRatio(e.name!, searchText.value), + weightedRatio(e.name!, searchController.text), e, )) .sorted((a, b) => b.$1.compareTo(a.$1)) @@ -47,14 +56,7 @@ class GenrePage extends HookConsumerWidget { .map((e) => e.$2) .toList(); }, - [categoriesQuery.pages, searchText.value], - ); - - final searchbar = CompactSearch( - onChanged: (value) { - searchText.value = value; - }, - placeholder: context.l10n.genre_categories_filter, + [categoriesQuery.pages, searchController.text], ); final list = RefreshIndicator( @@ -68,22 +70,32 @@ class GenrePage extends HookConsumerWidget { } }, controller: scrollController, - child: ListView.builder( - controller: scrollController, - itemCount: categories.length, - shrinkWrap: true, - itemBuilder: (context, index) { - return AnimatedCrossFade( - crossFadeState: searchText.value.isEmpty && - index == categories.length - 1 && - categoriesQuery.hasNextPage - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 300), - firstChild: const ShimmerCategories(), - secondChild: CategoryCard(categories[index]), - ); - }, + child: Column( + children: [ + ExpandableSearchField( + isFiltering: isFiltering, + searchController: searchController, + searchFocus: searchFocus, + ), + Expanded( + child: ListView.builder( + controller: scrollController, + itemCount: categories.length, + itemBuilder: (context, index) { + return AnimatedCrossFade( + crossFadeState: searchController.text.isEmpty && + index == categories.length - 1 && + categoriesQuery.hasNextPage + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 300), + firstChild: const ShimmerCategories(), + secondChild: CategoryCard(categories[index]), + ); + }, + ), + ), + ], ), ), ); @@ -94,7 +106,20 @@ class GenrePage extends HookConsumerWidget { Positioned( top: 0, right: 10, - child: searchbar, + child: ExpandableSearchButton( + isFiltering: isFiltering, + searchFocus: searchFocus, + icon: const Icon(SpotubeIcons.search), + onPressed: (value) { + if (isFiltering.value) { + scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + }, + ), ), ], ); From 65fa3cb624c240360de5a06778a1f72ad10bbe2d Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 18 Jun 2023 13:25:59 +0600 Subject: [PATCH 13/28] fix: status bar color of playlist/album page --- .../track_collection_view/track_collection_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/shared/track_table/track_collection_view/track_collection_view.dart b/lib/components/shared/track_table/track_collection_view/track_collection_view.dart index b2e639b5..78d74fcd 100644 --- a/lib/components/shared/track_table/track_collection_view/track_collection_view.dart +++ b/lib/components/shared/track_table/track_collection_view/track_collection_view.dart @@ -88,7 +88,7 @@ class TrackCollectionView extends HookConsumerWidget { final collapsed = useState(false); useCustomStatusBarColor( - color?.color ?? theme.scaffoldBackgroundColor, + Colors.transparent, GoRouter.of(context).location == routePath, ); From 93bd4dc3aaace63e9c3f7c513335d2f6a453984f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 18 Jun 2023 15:01:11 +0600 Subject: [PATCH 14/28] chore: fix colors --- .../track_collection_view/track_collection_view.dart | 5 ++--- lib/pages/search/search.dart | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/components/shared/track_table/track_collection_view/track_collection_view.dart b/lib/components/shared/track_table/track_collection_view/track_collection_view.dart index 78d74fcd..c0caad96 100644 --- a/lib/components/shared/track_table/track_collection_view/track_collection_view.dart +++ b/lib/components/shared/track_table/track_collection_view/track_collection_view.dart @@ -166,9 +166,8 @@ class TrackCollectionView extends HookConsumerWidget { pinned: true, expandedHeight: 400, automaticallyImplyLeading: kIsMobile, - leading: kIsMobile - ? BackButton(color: color?.titleTextColor) - : null, + leading: + kIsMobile ? const BackButton(color: Colors.white) : null, iconTheme: IconThemeData(color: color?.titleTextColor), primary: true, backgroundColor: color?.color, diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index de118866..9d5e7eed 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -77,6 +77,7 @@ class SearchPage extends HookConsumerWidget { ), color: theme.scaffoldBackgroundColor, child: TextField( + autofocus: true, decoration: InputDecoration( prefixIcon: const Icon(SpotubeIcons.search), hintText: "${context.l10n.search}...", From af6ab5fc2cf9dc2a636b46d06aed5da974c36488 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 18 Jun 2023 15:13:44 +0600 Subject: [PATCH 15/28] refactor: use table in about --- lib/pages/settings/about.dart | 119 +++++++++++++++--------------- lib/pages/settings/blacklist.dart | 9 ++- 2 files changed, 65 insertions(+), 63 deletions(-) diff --git a/lib/pages/settings/about.dart b/lib/pages/settings/about.dart index 67daef45..668a4d8f 100644 --- a/lib/pages/settings/about.dart +++ b/lib/pages/settings/about.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/shared/links/hyper_link.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/use_package_info.dart'; @@ -24,6 +25,8 @@ class AboutSpotube extends HookConsumerWidget { final license = ref.watch(_licenseProvider); final theme = Theme.of(context); + final colon = Text(":"); + return Scaffold( appBar: PageWindowTitleBar( leading: const BackButton(), @@ -40,77 +43,75 @@ class AboutSpotube extends HookConsumerWidget { ), Center( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( context.l10n.spotube_description, style: theme.textTheme.titleLarge, ), const SizedBox(height: 20), - Row( - mainAxisSize: MainAxisSize.min, + Table( + columnWidths: const { + 0: FixedColumnWidth(95), + 1: FixedColumnWidth(10), + 2: IntrinsicColumnWidth(), + }, children: [ - Text( - "${context.l10n.founder}: ${context.l10n.kingkor_roy_tirtho}", - style: const TextStyle( - fontWeight: FontWeight.bold, - ), + TableRow( + children: [ + Text(context.l10n.founder), + colon, + Hyperlink( + context.l10n.kingkor_roy_tirtho, + "https://github.com/KRTirtho", + ) + ], ), - const SizedBox(width: 5), - CircleAvatar( - radius: 20, - child: ClipOval( - child: Image.network( - "https://avatars.githubusercontent.com/u/61944859?v=4", + TableRow( + children: [ + Text(context.l10n.version), + colon, + Text("v${packageInfo.version}") + ], + ), + TableRow( + children: [ + Text(context.l10n.build_number), + colon, + Text(packageInfo.buildNumber.replaceAll(".", " ")) + ], + ), + TableRow( + children: [ + Text(context.l10n.repository), + colon, + const Hyperlink( + "github.com/KRTirtho/spotube", + "https://github.com/KRTirtho/spotube", ), - ), + ], + ), + TableRow( + children: [ + Text(context.l10n.license), + colon, + const Hyperlink( + "BSD-4-Clause", + "https://raw.githubusercontent.com/KRTirtho/spotube/master/LICENSE", + ), + ], + ), + TableRow( + children: [ + Text(context.l10n.bug_issues), + colon, + const Hyperlink( + "github.com/KRTirtho/spotube/issues", + "https://github.com/KRTirtho/spotube/issues", + ), + ], ), ], ), - const SizedBox(height: 5), - Text( - "${context.l10n.version}: v${packageInfo.version}", - ), - const SizedBox(height: 5), - Text( - "${context.l10n.build_number}: ${packageInfo.buildNumber.replaceAll(".", " ")}", - ), - const SizedBox(height: 5), - InkWell( - onTap: () { - launchUrlString( - "https://github.com/KRTirtho/spotube", - mode: LaunchMode.externalApplication, - ); - }, - child: Text( - "${context.l10n.repository}: https://github.com/KRTirtho/spotube", - ), - ), - const SizedBox(height: 5), - InkWell( - onTap: () { - launchUrlString( - "https://raw.githubusercontent.com/KRTirtho/spotube/master/LICENSE", - mode: LaunchMode.externalApplication, - ); - }, - child: Text( - "${context.l10n.license}: BSD-4-Clause", - ), - ), - const SizedBox(height: 5), - InkWell( - onTap: () { - launchUrlString( - "https://github.com/KRTirtho/spotube/issues", - mode: LaunchMode.externalApplication, - ); - }, - child: Text( - "${context.l10n.bug_issues}: https://github.com/KRTirtho/spotube/issues", - ), - ), ], ), ), diff --git a/lib/pages/settings/blacklist.dart b/lib/pages/settings/blacklist.dart index 97947150..a41a38eb 100644 --- a/lib/pages/settings/blacklist.dart +++ b/lib/pages/settings/blacklist.dart @@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/blacklist_provider.dart'; class BlackListPage extends HookConsumerWidget { @@ -38,7 +39,7 @@ class BlackListPage extends HookConsumerWidget { return Scaffold( appBar: PageWindowTitleBar( - title: const Text("Blacklist"), + title: Text(context.l10n.blacklist), centerTitle: true, leading: const BackButton(), ), @@ -49,9 +50,9 @@ class BlackListPage extends HookConsumerWidget { padding: const EdgeInsets.all(8.0), child: TextField( onChanged: (value) => searchText.value = value, - decoration: const InputDecoration( - hintText: "Search", - prefixIcon: Icon(SpotubeIcons.search), + decoration: InputDecoration( + hintText: context.l10n.search, + prefixIcon: const Icon(SpotubeIcons.search), ), ), ), From b4713e377a938cbebe70089874216f86fe550c34 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 18 Jun 2023 15:28:44 +0600 Subject: [PATCH 16/28] feat: sort tracks by newest and oldest dates --- lib/components/library/user_local_tracks.dart | 3 ++- .../shared/sort_tracks_dropdown.dart | 19 ++++++++++++++----- .../shared/track_table/tracks_table_view.dart | 1 - lib/l10n/app_bn.arb | 1 - lib/l10n/app_en.arb | 7 ++++--- lib/l10n/app_fr.arb | 1 - lib/l10n/app_hi.arb | 1 - lib/utils/service_utils.dart | 12 +++++++----- 8 files changed, 27 insertions(+), 18 deletions(-) diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index 217e2d7f..86880820 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -52,7 +52,8 @@ enum SortBy { descending, artist, album, - dateAdded, + newest, + oldest, } final localTracksProvider = FutureProvider>((ref) async { diff --git a/lib/components/shared/sort_tracks_dropdown.dart b/lib/components/shared/sort_tracks_dropdown.dart index be43f27e..7695f89a 100644 --- a/lib/components/shared/sort_tracks_dropdown.dart +++ b/lib/components/shared/sort_tracks_dropdown.dart @@ -16,6 +16,7 @@ class SortTracksDropdown extends StatelessWidget { @override Widget build(BuildContext context) { + var theme = Theme.of(context); return ListTileTheme( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), @@ -47,11 +48,19 @@ class SortTracksDropdown extends StatelessWidget { ), ), PopSheetEntry( - value: SortBy.dateAdded, - enabled: value != SortBy.dateAdded, + value: SortBy.newest, + enabled: value != SortBy.newest, child: ListTile( - enabled: value != SortBy.dateAdded, - title: Text(context.l10n.sort_date), + enabled: value != SortBy.newest, + title: Text(context.l10n.sort_newest), + ), + ), + PopSheetEntry( + value: SortBy.oldest, + enabled: value != SortBy.oldest, + child: ListTile( + enabled: value != SortBy.oldest, + title: Text(context.l10n.sort_oldest), ), ), PopSheetEntry( @@ -79,7 +88,7 @@ class SortTracksDropdown extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), child: DefaultTextStyle( - style: Theme.of(context).textTheme.titleSmall!, + style: theme.textTheme.titleSmall!, child: Row( children: [ const Icon(SpotubeIcons.sort), diff --git a/lib/components/shared/track_table/tracks_table_view.dart b/lib/components/shared/track_table/tracks_table_view.dart index f6340b33..8f20b855 100644 --- a/lib/components/shared/track_table/tracks_table_view.dart +++ b/lib/components/shared/track_table/tracks_table_view.dart @@ -50,7 +50,6 @@ class TracksTableView extends HookConsumerWidget { @override Widget build(context, ref) { - final theme = Theme.of(context); final mediaQuery = MediaQuery.of(context); ref.watch(ProxyPlaylistNotifier.provider); diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb index 0b5f3c14..277a7997 100644 --- a/lib/l10n/app_bn.arb +++ b/lib/l10n/app_bn.arb @@ -37,7 +37,6 @@ "none": "কোনটিই না", "sort_a_z": "A-Z ক্রমে সাজান", "sort_z_a": "Z-A ক্রমে সাজান", - "sort_date": "তারিখের ক্রমে সাজান", "sort_artist": "শিল্পীর ক্রমে সাজান", "sort_album": "অ্যালবামের ক্রমে সাজান", "sort_tracks": "গানের ক্রম", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f29f29fc..3813a389 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -37,7 +37,6 @@ "none": "None", "sort_a_z": "Sort by A-Z", "sort_z_a": "Sort by Z-A", - "sort_date": "Sort by date", "sort_artist": "Sort by Artist", "sort_album": "Sort by Album", "sort_tracks": "Sort Tracks", @@ -235,8 +234,10 @@ "youtube": "YouTube", "channel": "Channel", "likes": "Likes", - "dislikes": "Dislikes", + "dislikes": "Dislikes", "views": "Views", "streamUrl": "Stream URL", - "stop": "Stop" + "stop": "Stop", + "sort_newest": "Sort by newest added", + "sort_oldest": "Sort by oldest added" } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 5a3c7385..724822a8 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -37,7 +37,6 @@ "none": "Aucun", "sort_a_z": "Trier par ordre alphabétique", "sort_z_a": "Trier par ordre alphabétique inverse", - "sort_date": "Trier par date", "sort_artist": "Trier par artiste", "sort_album": "Trier par album", "sort_tracks": "Trier les pistes", diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index 51e85cec..de2ed6e2 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -37,7 +37,6 @@ "none": "कोई नहीं", "sort_a_z": "A-Z सॉर्ट करें", "sort_z_a": "Z-A सॉर्ट करें", - "sort_date": "तिथि के अनुसार सॉर्ट करें", "sort_artist": "कलाकार के अनुसार सॉर्ट करें", "sort_album": "एल्बम के अनुसार सॉर्ट करें", "sort_tracks": "ट्रैक को सॉर्ट करें", diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 36b361a1..c9c2943c 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -271,12 +271,14 @@ abstract class ServiceUtils { 0; case SortBy.ascending: return a.name?.compareTo(b.name ?? "") ?? 0; - case SortBy.dateAdded: - final aDate = - double.parse(a.album?.releaseDate?.split("-").first ?? "2069"); - final bDate = - double.parse(b.album?.releaseDate?.split("-").first ?? "2069"); + case SortBy.oldest: + final aDate = DateTime.parse(a.album?.releaseDate ?? "2069-01-01"); + final bDate = DateTime.parse(b.album?.releaseDate ?? "2069-01-01"); return aDate.compareTo(bDate); + case SortBy.newest: + final aDate = DateTime.parse(a.album?.releaseDate ?? "2069-01-01"); + final bDate = DateTime.parse(b.album?.releaseDate ?? "2069-01-01"); + return bDate.compareTo(aDate); case SortBy.descending: return b.name?.compareTo(a.name ?? "") ?? 0; default: From 0620b62023fe9a3bb2bb5657d97b9cbf909e7e4c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 18 Jun 2023 22:13:06 +0600 Subject: [PATCH 17/28] refactor: extend list item for PopSheetEntry for better interactivity --- lib/collections/spotube_icons.dart | 1 + lib/components/player/player_actions.dart | 32 +++++- .../adaptive/adaptive_pop_sheet_list.dart | 68 +++++++++---- .../shared/sort_tracks_dropdown.dart | 35 ++----- .../shared/track_table/track_options.dart | 97 ++++++++----------- .../shared/track_table/tracks_table_view.dart | 86 +++++++--------- lib/l10n/app_en.arb | 6 +- lib/pages/player/player.dart | 8 +- 8 files changed, 175 insertions(+), 158 deletions(-) diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index 612f23a1..f5caac09 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -86,4 +86,5 @@ abstract class SpotubeIcons { static const volumeMedium = FeatherIcons.volume1; static const volumeLow = FeatherIcons.volume; static const volumeMute = FeatherIcons.volumeX; + static const timer = FeatherIcons.clock; } diff --git a/lib/components/player/player_actions.dart b/lib/components/player/player_actions.dart index 4026ef4d..59deeda3 100644 --- a/lib/components/player/player_actions.dart +++ b/lib/components/player/player_actions.dart @@ -3,10 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotify/spotify.dart'; +import 'package:spotify/spotify.dart' hide Offset; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/player/player_queue.dart'; import 'package:spotube/components/player/sibling_tracks_sheet.dart'; +import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/shared/heart_button.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/local_track.dart'; @@ -127,6 +128,35 @@ class PlayerActions extends HookConsumerWidget { ), if (playlist.activeTrack != null && !isLocalTrack && auth != null) TrackHeartButton(track: playlist.activeTrack!), + AdaptivePopSheetList( + offset: const Offset(0, -50 * 5), + headings: [ + Text(context.l10n.sleep_timer), + ], + icon: const Icon(SpotubeIcons.timer), + children: [ + PopSheetEntry( + value: const Duration(minutes: 15), + title: Text(context.l10n.mins(15)), + ), + PopSheetEntry( + value: const Duration(minutes: 30), + title: Text(context.l10n.mins(30)), + ), + PopSheetEntry( + value: const Duration(hours: 1), + title: Text(context.l10n.hour(1)), + ), + PopSheetEntry( + value: const Duration(hours: 2), + title: Text(context.l10n.hours(2)), + ), + PopSheetEntry( + value: Duration.zero, + title: Text(context.l10n.cancel), + ), + ], + ), ...(extraActions ?? []) ], ); diff --git a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart b/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart index 08cc5551..11fe042b 100644 --- a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart +++ b/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart @@ -2,17 +2,47 @@ import 'package:flutter/material.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/extensions/constrains.dart'; -class PopSheetEntry { - final T? value; - final VoidCallback? onTap; - final Widget child; - final bool enabled; +_emptyCB() {} +class PopSheetEntry extends ListTile { + final T? value; const PopSheetEntry({ - required this.child, this.value, - this.onTap, - this.enabled = true, + super.key, + super.leading, + super.title, + super.subtitle, + super.trailing, + super.isThreeLine = false, + super.dense, + super.visualDensity, + super.shape, + super.style, + super.selectedColor, + super.iconColor, + super.textColor, + super.titleTextStyle, + super.subtitleTextStyle, + super.leadingAndTrailingTextStyle, + super.contentPadding, + super.enabled = true, + super.onTap = _emptyCB, + super.onLongPress, + super.onFocusChange, + super.mouseCursor, + super.selected = false, + super.focusColor, + super.hoverColor, + super.splashColor, + super.focusNode, + super.autofocus = false, + super.tileColor, + super.selectedTileColor, + super.enableFeedback, + super.horizontalTitleGap, + super.minVerticalPadding, + super.minLeadingWidth, + super.titleAlignment, }); } @@ -30,6 +60,7 @@ class AdaptivePopSheetList extends StatelessWidget { final ValueChanged? onSelected; final BorderRadius borderRadius; + final Offset offset; const AdaptivePopSheetList({ super.key, @@ -41,6 +72,7 @@ class AdaptivePopSheetList extends StatelessWidget { this.onSelected, this.borderRadius = const BorderRadius.all(Radius.circular(999)), this.tooltip, + this.offset = Offset.zero, }) : assert( !(icon != null && child != null), 'Either icon or child must be provided', @@ -55,11 +87,13 @@ class AdaptivePopSheetList extends StatelessWidget { return PopupMenuButton( icon: icon, tooltip: tooltip, + offset: offset, child: child == null ? null : IgnorePointer(child: child), itemBuilder: (context) => children .map( (item) => PopupMenuItem( padding: EdgeInsets.zero, + enabled: false, child: _AdaptivePopSheetListItem( item: item, onSelected: onSelected, @@ -151,8 +185,11 @@ class _AdaptivePopSheetListItem extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + return InkWell( - borderRadius: BorderRadius.circular(10), + borderRadius: (theme.listTileTheme.shape as RoundedRectangleBorder?) + ?.borderRadius as BorderRadius? ?? + const BorderRadius.all(Radius.circular(10)), onTap: !item.enabled ? null : () { @@ -162,16 +199,9 @@ class _AdaptivePopSheetListItem extends StatelessWidget { onSelected?.call(item.value as T); } }, - child: DefaultTextStyle( - style: TextStyle( - color: item.enabled - ? theme.textTheme.bodyMedium!.color - : theme.textTheme.bodyMedium!.color!.withOpacity(0.5), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: item.child, - ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: IgnorePointer(child: item), ), ); } diff --git a/lib/components/shared/sort_tracks_dropdown.dart b/lib/components/shared/sort_tracks_dropdown.dart index 7695f89a..0547beb0 100644 --- a/lib/components/shared/sort_tracks_dropdown.dart +++ b/lib/components/shared/sort_tracks_dropdown.dart @@ -26,58 +26,37 @@ class SortTracksDropdown extends StatelessWidget { PopSheetEntry( value: SortBy.none, enabled: value != SortBy.none, - child: ListTile( - enabled: value != SortBy.none, - title: Text(context.l10n.none), - ), + title: Text(context.l10n.none), ), PopSheetEntry( value: SortBy.ascending, enabled: value != SortBy.ascending, - child: ListTile( - enabled: value != SortBy.ascending, - title: Text(context.l10n.sort_a_z), - ), + title: Text(context.l10n.sort_a_z), ), PopSheetEntry( value: SortBy.descending, enabled: value != SortBy.descending, - child: ListTile( - enabled: value != SortBy.descending, - title: Text(context.l10n.sort_z_a), - ), + title: Text(context.l10n.sort_z_a), ), PopSheetEntry( value: SortBy.newest, enabled: value != SortBy.newest, - child: ListTile( - enabled: value != SortBy.newest, - title: Text(context.l10n.sort_newest), - ), + title: Text(context.l10n.sort_newest), ), PopSheetEntry( value: SortBy.oldest, enabled: value != SortBy.oldest, - child: ListTile( - enabled: value != SortBy.oldest, - title: Text(context.l10n.sort_oldest), - ), + title: Text(context.l10n.sort_oldest), ), PopSheetEntry( value: SortBy.artist, enabled: value != SortBy.artist, - child: ListTile( - enabled: value != SortBy.artist, - title: Text(context.l10n.sort_artist), - ), + title: Text(context.l10n.sort_artist), ), PopSheetEntry( value: SortBy.album, enabled: value != SortBy.album, - child: ListTile( - enabled: value != SortBy.album, - title: Text(context.l10n.sort_album), - ), + title: Text(context.l10n.sort_album), ), ], headings: [ diff --git a/lib/components/shared/track_table/track_options.dart b/lib/components/shared/track_table/track_options.dart index 8dc094ad..e3b5cee2 100644 --- a/lib/components/shared/track_table/track_options.dart +++ b/lib/components/shared/track_table/track_options.dart @@ -207,102 +207,81 @@ class TrackOptions extends HookConsumerWidget { LocalTrack => [ PopSheetEntry( value: TrackOptionValue.delete, - child: ListTile( - leading: const Icon(SpotubeIcons.trash), - title: Text(context.l10n.delete), - ), + leading: const Icon(SpotubeIcons.trash), + title: Text(context.l10n.delete), ) ], _ => [ if (!playlist.containsTrack(track)) ...[ PopSheetEntry( value: TrackOptionValue.addToQueue, - child: ListTile( - leading: const Icon(SpotubeIcons.queueAdd), - title: Text(context.l10n.add_to_queue), - ), + leading: const Icon(SpotubeIcons.queueAdd), + title: Text(context.l10n.add_to_queue), ), PopSheetEntry( value: TrackOptionValue.playNext, - child: ListTile( - leading: const Icon(SpotubeIcons.lightning), - title: Text(context.l10n.play_next), - ), + leading: const Icon(SpotubeIcons.lightning), + title: Text(context.l10n.play_next), ), ] else PopSheetEntry( value: TrackOptionValue.removeFromQueue, enabled: playlist.activeTrack?.id != track.id, - child: ListTile( - enabled: playlist.activeTrack?.id != track.id, - leading: const Icon(SpotubeIcons.queueRemove), - title: Text(context.l10n.remove_from_queue), - ), + leading: const Icon(SpotubeIcons.queueRemove), + title: Text(context.l10n.remove_from_queue), ), if (favorites.me.hasData) PopSheetEntry( value: TrackOptionValue.favorite, - child: ListTile( - leading: favorites.isLiked - ? const Icon( - SpotubeIcons.heartFilled, - color: Colors.pink, - ) - : const Icon(SpotubeIcons.heart), - title: Text( - favorites.isLiked - ? context.l10n.remove_from_favorites - : context.l10n.save_as_favorite, - ), + leading: favorites.isLiked + ? const Icon( + SpotubeIcons.heartFilled, + color: Colors.pink, + ) + : const Icon(SpotubeIcons.heart), + title: Text( + favorites.isLiked + ? context.l10n.remove_from_favorites + : context.l10n.save_as_favorite, ), ), if (auth != null) PopSheetEntry( value: TrackOptionValue.addToPlaylist, - child: ListTile( - leading: const Icon(SpotubeIcons.playlistAdd), - title: Text(context.l10n.add_to_playlist), - ), + leading: const Icon(SpotubeIcons.playlistAdd), + title: Text(context.l10n.add_to_playlist), ), if (userPlaylist && auth != null) PopSheetEntry( value: TrackOptionValue.removeFromPlaylist, - child: ListTile( - leading: (removeTrack.isMutating || !removeTrack.hasData) && - removingTrack.value == track.uri - ? const Center( - child: CircularProgressIndicator(), - ) - : const Icon(SpotubeIcons.removeFilled), - title: Text(context.l10n.remove_from_playlist), - ), + leading: (removeTrack.isMutating || !removeTrack.hasData) && + removingTrack.value == track.uri + ? const Center( + child: CircularProgressIndicator(), + ) + : const Icon(SpotubeIcons.removeFilled), + title: Text(context.l10n.remove_from_playlist), ), PopSheetEntry( value: TrackOptionValue.blacklist, - child: ListTile( - leading: const Icon(SpotubeIcons.playlistRemove), - iconColor: !isBlackListed ? Colors.red[400] : null, - textColor: !isBlackListed ? Colors.red[400] : null, - title: Text( - isBlackListed - ? context.l10n.remove_from_blacklist - : context.l10n.add_to_blacklist, - ), + leading: const Icon(SpotubeIcons.playlistRemove), + iconColor: !isBlackListed ? Colors.red[400] : null, + textColor: !isBlackListed ? Colors.red[400] : null, + title: Text( + isBlackListed + ? context.l10n.remove_from_blacklist + : context.l10n.add_to_blacklist, ), ), PopSheetEntry( value: TrackOptionValue.share, - child: ListTile( - leading: const Icon(SpotubeIcons.share), - title: Text(context.l10n.share), - ), + 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), - ), + leading: const Icon(SpotubeIcons.info), + title: Text(context.l10n.details), ), ] }, diff --git a/lib/components/shared/track_table/tracks_table_view.dart b/lib/components/shared/track_table/tracks_table_view.dart index 8f20b855..5c4333e7 100644 --- a/lib/components/shared/track_table/tracks_table_view.dart +++ b/lib/components/shared/track_table/tracks_table_view.dart @@ -185,55 +185,6 @@ class TracksTableView extends HookConsumerWidget { style: tableHeadStyle, ), ], - children: [ - PopSheetEntry( - enabled: selectedTracks.isNotEmpty, - value: "download", - child: ListTile( - leading: const Icon(SpotubeIcons.download), - enabled: selectedTracks.isNotEmpty, - title: Text( - context.l10n.download_count(selectedTracks.length), - ), - ), - ), - if (!userPlaylist) - PopSheetEntry( - enabled: selectedTracks.isNotEmpty, - value: "add-to-playlist", - child: ListTile( - leading: const Icon(SpotubeIcons.playlistAdd), - enabled: selectedTracks.isNotEmpty, - title: Text( - context.l10n - .add_count_to_playlist(selectedTracks.length), - ), - ), - ), - PopSheetEntry( - enabled: selectedTracks.isNotEmpty, - value: "add-to-queue", - child: ListTile( - leading: const Icon(SpotubeIcons.queueAdd), - enabled: selectedTracks.isNotEmpty, - title: Text( - context.l10n - .add_count_to_queue(selectedTracks.length), - ), - ), - ), - PopSheetEntry( - enabled: selectedTracks.isNotEmpty, - value: "play-next", - child: ListTile( - leading: const Icon(SpotubeIcons.lightning), - enabled: selectedTracks.isNotEmpty, - title: Text( - context.l10n.play_count_next(selectedTracks.length), - ), - ), - ), - ], onSelected: (action) async { switch (action) { case "download": @@ -283,6 +234,43 @@ class TracksTableView extends HookConsumerWidget { } }, icon: const Icon(SpotubeIcons.moreVertical), + children: [ + PopSheetEntry( + value: "download", + leading: const Icon(SpotubeIcons.download), + enabled: selectedTracks.isNotEmpty, + title: Text( + context.l10n.download_count(selectedTracks.length), + ), + ), + if (!userPlaylist) + PopSheetEntry( + value: "add-to-playlist", + leading: const Icon(SpotubeIcons.playlistAdd), + enabled: selectedTracks.isNotEmpty, + title: Text( + context.l10n + .add_count_to_playlist(selectedTracks.length), + ), + ), + PopSheetEntry( + enabled: selectedTracks.isNotEmpty, + value: "add-to-queue", + leading: const Icon(SpotubeIcons.queueAdd), + title: Text( + context.l10n + .add_count_to_queue(selectedTracks.length), + ), + ), + PopSheetEntry( + enabled: selectedTracks.isNotEmpty, + value: "play-next", + leading: const Icon(SpotubeIcons.lightning), + title: Text( + context.l10n.play_count_next(selectedTracks.length), + ), + ), + ], ), const SizedBox(width: 10), ], diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3813a389..f9244468 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -239,5 +239,9 @@ "streamUrl": "Stream URL", "stop": "Stop", "sort_newest": "Sort by newest added", - "sort_oldest": "Sort by oldest added" + "sort_oldest": "Sort by oldest added", + "sleep_timer": "Sleep Timer", + "mins": "{minutes} Minutes", + "hours": "{hours} Hours", + "hour": "{hours} Hour" } \ No newline at end of file diff --git a/lib/pages/player/player.dart b/lib/pages/player/player.dart index 61b54d72..8e7750f4 100644 --- a/lib/pages/player/player.dart +++ b/lib/pages/player/player.dart @@ -197,6 +197,9 @@ class PlayerView extends HookConsumerWidget { label: Text(context.l10n.details), style: OutlinedButton.styleFrom( foregroundColor: bodyTextColor, + side: BorderSide( + color: bodyTextColor ?? Colors.white, + ), ), onPressed: currentTrack == null ? null @@ -218,6 +221,9 @@ class PlayerView extends HookConsumerWidget { icon: const Icon(SpotubeIcons.music), style: OutlinedButton.styleFrom( foregroundColor: bodyTextColor, + side: BorderSide( + color: bodyTextColor ?? Colors.white, + ), ), onPressed: () { showModalBottomSheet( @@ -257,7 +263,7 @@ class PlayerView extends HookConsumerWidget { overlayColor: titleTextColor?.withOpacity(0.2), trackHeight: 2, thumbShape: const RoundSliderThumbShape( - enabledThumbRadius: 6, + enabledThumbRadius: 8, ), ), child: const Padding( From 4a75f3dbd1e7e6f68899de001df70e809533f142 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 19 Jun 2023 21:35:58 +0600 Subject: [PATCH 18/28] feat: add sleep timer support --- lib/components/player/player_actions.dart | 75 +++++++++++++++---- .../adaptive/adaptive_pop_sheet_list.dart | 2 +- .../shared/dialogs/track_details_dialog.dart | 4 +- lib/extensions/duration.dart | 2 +- lib/l10n/app_en.arb | 3 +- lib/provider/sleep_timer_provider.dart | 31 ++++++++ 6 files changed, 96 insertions(+), 21 deletions(-) create mode 100644 lib/provider/sleep_timer_provider.dart diff --git a/lib/components/player/player_actions.dart b/lib/components/player/player_actions.dart index 59deeda3..d7cf6bfb 100644 --- a/lib/components/player/player_actions.dart +++ b/lib/components/player/player_actions.dart @@ -10,11 +10,13 @@ import 'package:spotube/components/player/sibling_tracks_sheet.dart'; import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/shared/heart_button.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/extensions/duration.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/sleep_timer_provider.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class PlayerActions extends HookConsumerWidget { @@ -39,6 +41,8 @@ class PlayerActions extends HookConsumerWidget { downloader.activeItem!.id == playlist.activeTrack?.id; final localTracks = [] /* ref.watch(localTracksProvider).value */; final auth = ref.watch(AuthenticationNotifier.provider); + final sleepTimer = ref.watch(SleepTimerNotifier.provider); + final sleepTimerNotifier = ref.watch(SleepTimerNotifier.notifier); final isDownloaded = useMemoized(() { return localTracks.any( @@ -53,6 +57,18 @@ class PlayerActions extends HookConsumerWidget { true; }, [localTracks, playlist.activeTrack]); + final sleepTimerEntries = useMemoized( + () => { + context.l10n.mins(15): const Duration(minutes: 15), + context.l10n.mins(30): const Duration(minutes: 30), + context.l10n.hour(1): const Duration(hours: 1), + context.l10n.hour(2): const Duration(hours: 2), + }, + [context.l10n], + ); + + var customHoursEnabled = + sleepTimer == null || sleepTimerEntries.values.contains(sleepTimer); return Row( mainAxisAlignment: mainAxisAlignment, children: [ @@ -129,30 +145,57 @@ class PlayerActions extends HookConsumerWidget { if (playlist.activeTrack != null && !isLocalTrack && auth != null) TrackHeartButton(track: playlist.activeTrack!), AdaptivePopSheetList( - offset: const Offset(0, -50 * 5), + offset: Offset(0, -50 * (sleepTimerEntries.values.length + 2)), headings: [ Text(context.l10n.sleep_timer), ], - icon: const Icon(SpotubeIcons.timer), + icon: Icon( + SpotubeIcons.timer, + color: sleepTimer != null ? Colors.red : null, + ), + onSelected: (value) { + if (value == Duration.zero) { + sleepTimerNotifier.cancelSleepTimer(); + } else { + sleepTimerNotifier.setSleepTimer(value); + } + }, children: [ + for (final entry in sleepTimerEntries.entries) + PopSheetEntry( + value: entry.value, + enabled: sleepTimer != entry.value, + title: Text(entry.key), + ), PopSheetEntry( - value: const Duration(minutes: 15), - title: Text(context.l10n.mins(15)), - ), - PopSheetEntry( - value: const Duration(minutes: 30), - title: Text(context.l10n.mins(30)), - ), - PopSheetEntry( - value: const Duration(hours: 1), - title: Text(context.l10n.hour(1)), - ), - PopSheetEntry( - value: const Duration(hours: 2), - title: Text(context.l10n.hours(2)), + title: Text( + customHoursEnabled + ? context.l10n.custom_hours + : sleepTimer.toHumanReadableString(), + ), + // only enabled when there's no preset timers selected + enabled: customHoursEnabled, + onTap: () async { + final currentTime = TimeOfDay.now(); + final time = await showTimePicker( + context: context, + initialTime: currentTime, + ); + + if (time != null) { + sleepTimerNotifier.setSleepTimer( + Duration( + hours: (time.hour - currentTime.hour).abs(), + minutes: (time.minute - currentTime.minute).abs(), + ), + ); + } + }, ), PopSheetEntry( value: Duration.zero, + enabled: sleepTimer != Duration.zero && sleepTimer != null, + textColor: Colors.green, title: Text(context.l10n.cancel), ), ], diff --git a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart b/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart index 11fe042b..41534cb3 100644 --- a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart +++ b/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart @@ -194,8 +194,8 @@ class _AdaptivePopSheetListItem extends StatelessWidget { ? null : () { item.onTap?.call(); - Navigator.pop(context); if (item.value != null) { + Navigator.pop(context); onSelected?.call(item.value as T); } }, diff --git a/lib/components/shared/dialogs/track_details_dialog.dart b/lib/components/shared/dialogs/track_details_dialog.dart index 09e71e56..a0da2f57 100644 --- a/lib/components/shared/dialogs/track_details_dialog.dart +++ b/lib/components/shared/dialogs/track_details_dialog.dart @@ -119,9 +119,9 @@ class TrackDetailsDialog extends HookWidget { ), if (entry.value is Widget) entry.value as Widget - else + else if (entry.value is String) Text( - entry.value, + entry.value as String, style: theme.textTheme.bodyMedium, ), ], diff --git a/lib/extensions/duration.dart b/lib/extensions/duration.dart index 250b47af..a989f589 100644 --- a/lib/extensions/duration.dart +++ b/lib/extensions/duration.dart @@ -1,6 +1,6 @@ import 'package:spotube/utils/primitive_utils.dart'; extension DurationToHumanReadableString on Duration { - toHumanReadableString() => + String toHumanReadableString() => "${inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(inSeconds.remainder(60))}"; } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f9244468..16806c5e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -243,5 +243,6 @@ "sleep_timer": "Sleep Timer", "mins": "{minutes} Minutes", "hours": "{hours} Hours", - "hour": "{hours} Hour" + "hour": "{hours} Hour", + "custom_hours": "Custom Hours" } \ No newline at end of file diff --git a/lib/provider/sleep_timer_provider.dart b/lib/provider/sleep_timer_provider.dart new file mode 100644 index 00000000..32678ac7 --- /dev/null +++ b/lib/provider/sleep_timer_provider.dart @@ -0,0 +1,31 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class SleepTimerNotifier extends StateNotifier { + SleepTimerNotifier() : super(null); + + Timer? _timer; + + static final provider = StateNotifierProvider( + (ref) => SleepTimerNotifier(), + ); + + static AlwaysAliveRefreshable get notifier => + provider.notifier; + + void setSleepTimer(Duration duration) { + state = duration; + + _timer = Timer(duration, () { + //! This can be a reason for app termination in iOS AppStore + exit(0); + }); + } + + void cancelSleepTimer() { + state = null; + _timer?.cancel(); + } +} From b78e7f57a05db344aae59206cbb0f43b3ee199a9 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 19 Jun 2023 22:36:05 +0600 Subject: [PATCH 19/28] feat: logs page in settings --- lib/collections/routes.dart | 11 ++- lib/collections/spotube_icons.dart | 2 + lib/l10n/app_en.arb | 4 +- lib/main.dart | 8 +- lib/pages/settings/logs.dart | 139 ++++++++++++++++++++++++++++ lib/pages/settings/settings.dart | 13 +++ lib/utils/custom_toast_handler.dart | 58 ------------ pubspec.lock | 8 -- pubspec.yaml | 1 - 9 files changed, 167 insertions(+), 77 deletions(-) create mode 100644 lib/pages/settings/logs.dart delete mode 100644 lib/utils/custom_toast_handler.dart diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index 44f57def..9662419d 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -8,6 +8,7 @@ import 'package:spotube/pages/lyrics/mini_lyrics.dart'; 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/utils/platform.dart'; import 'package:spotube/components/shared/spotube_page_route.dart'; import 'package:spotube/pages/album/album.dart'; @@ -83,10 +84,16 @@ final router = GoRouter( child: const BlackListPage(), ), ), + GoRoute( + path: "logs", + pageBuilder: (context, state) => SpotubeSlidePage( + child: const LogsPage(), + ), + ), GoRoute( path: "about", - pageBuilder: (context, state) => const SpotubePage( - child: AboutSpotube(), + pageBuilder: (context, state) => SpotubeSlidePage( + child: const AboutSpotube(), ), ), ], diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index f5caac09..5db6e172 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -87,4 +87,6 @@ abstract class SpotubeIcons { static const volumeLow = FeatherIcons.volume; static const volumeMute = FeatherIcons.volumeX; static const timer = FeatherIcons.clock; + static const logs = FeatherIcons.fileText; + static const clipboard = FeatherIcons.clipboard; } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 16806c5e..85263046 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -244,5 +244,7 @@ "mins": "{minutes} Minutes", "hours": "{hours} Hours", "hour": "{hours} Hour", - "custom_hours": "Custom Hours" + "custom_hours": "Custom Hours", + "logs": "Logs", + "developers": "Developers" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 66a21772..f9037e35 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,7 +26,6 @@ import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/themes/theme.dart'; -import 'package:spotube/utils/custom_toast_handler.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:system_theme/system_theme.dart'; @@ -122,16 +121,11 @@ Future main(List rawArgs) async { releaseConfig: CatcherOptions( SilentReportMode(), [ - if (arguments["verbose"] ?? false) - ConsoleHandler( - enableDeviceParameters: false, - enableApplicationParameters: false, - ), + if (arguments["verbose"] ?? false) ConsoleHandler(), FileHandler( await getLogsPath(), printLogs: false, ), - CustomToastHandler(), ], ), runAppFunction: () { diff --git a/lib/pages/settings/logs.dart b/lib/pages/settings/logs.dart new file mode 100644 index 00000000..3bc1319f --- /dev/null +++ b/lib/pages/settings/logs.dart @@ -0,0 +1,139 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/settings/section_card_with_heading.dart'; +import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/models/logger.dart'; + +class LogsPage extends HookWidget { + const LogsPage({Key? key}) : super(key: key); + + List<({DateTime? date, String body})> parseLogs(String raw) { + return raw + .split( + "======================================================================", + ) + .map( + (line) { + DateTime? date; + line = line + .replaceAll( + "============================== CATCHER LOG ==============================", + "", + ) + .split("\n") + .map((l) { + if (l.startsWith("Crash occurred on")) { + date = DateTime.parse( + l.split("Crash occurred on")[1].trim(), + ); + return ""; + } + return l; + }) + .where((l) => l.replaceAll("\n", "").trim().isNotEmpty) + .join("\n"); + + return ( + date: date, + body: line, + ); + }, + ) + .where((e) => e.date != null && e.body.isNotEmpty) + .toList() + ..sort((a, b) => b.date!.compareTo(a.date!)); + } + + @override + Widget build(BuildContext context) { + final logs = useState>([]); + final rawLogs = useRef(""); + final path = useRef(null); + + useEffect(() { + final timer = Timer.periodic(const Duration(seconds: 5), (t) async { + path.value ??= await getLogsPath(); + final raw = await path.value!.readAsString(); + final hasChanged = rawLogs.value != raw; + rawLogs.value = raw; + if (hasChanged) logs.value = parseLogs(rawLogs.value); + }); + + return () { + timer.cancel(); + }; + }, []); + + return Scaffold( + appBar: PageWindowTitleBar( + title: Text(context.l10n.logs), + leading: const BackButton(), + actions: [ + IconButton( + icon: const Icon(SpotubeIcons.clipboard), + iconSize: 16, + onPressed: () async { + await Clipboard.setData(ClipboardData(text: rawLogs.value)); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.copied_to_clipboard("")), + ), + ); + } + }, + ), + ], + ), + body: SafeArea( + child: ListView.builder( + itemCount: logs.value.length, + itemBuilder: (context, index) { + final log = logs.value[index]; + return Stack( + children: [ + SectionCardWithHeading( + heading: log.date.toString(), + children: [ + Padding( + padding: const EdgeInsets.all(12.0), + child: SelectableText(log.body), + ), + ], + ), + Positioned( + right: 10, + top: 0, + child: IconButton( + icon: const Icon(SpotubeIcons.clipboard), + onPressed: () async { + await Clipboard.setData( + ClipboardData(text: log.body), + ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.copied_to_clipboard( + log.date.toString(), + ), + ), + ), + ); + } + }, + ), + ), + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 39e7d2bb..ebcfb58d 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -406,6 +406,19 @@ class SettingsPage extends HookConsumerWidget { ), ], ), + SectionCardWithHeading( + heading: context.l10n.developers, + children: [ + ListTile( + leading: const Icon(SpotubeIcons.logs), + title: Text(context.l10n.logs), + trailing: const Icon(SpotubeIcons.angleRight), + onTap: () { + GoRouter.of(context).push("/settings/logs"); + }, + ) + ], + ), SectionCardWithHeading( heading: context.l10n.about, children: [ diff --git a/lib/utils/custom_toast_handler.dart b/lib/utils/custom_toast_handler.dart deleted file mode 100644 index fa725962..00000000 --- a/lib/utils/custom_toast_handler.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:catcher/model/platform_type.dart'; -import 'package:catcher/model/report.dart'; -import 'package:catcher/model/report_handler.dart'; -import 'package:flutter/material.dart'; -import 'package:motion_toast/motion_toast.dart'; -import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/extensions/context.dart'; - -class CustomToastHandler extends ReportHandler { - CustomToastHandler(); - - @override - Future handle(Report error, BuildContext? context) async { - final theme = Theme.of(context!); - - MotionToast( - primaryColor: theme.colorScheme.errorContainer, - icon: SpotubeIcons.error, - title: Text( - context.l10n.something_went_wrong, - style: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.bold, - color: theme.colorScheme.onError, - ), - ), - description: Text( - error.error.toString(), - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onError, - ), - ), - dismissable: true, - toastDuration: const Duration(seconds: 5), - borderRadius: 10, - ).show(context); - return true; - } - - @override - List getSupportedPlatforms() => [ - PlatformType.android, - PlatformType.iOS, - PlatformType.web, - PlatformType.linux, - PlatformType.macOS, - PlatformType.windows, - ]; - - @override - bool isContextRequired() { - return true; - } - - @override - bool shouldHandleWhenRejected() { - return false; - } -} diff --git a/pubspec.lock b/pubspec.lock index 6cbba78f..14f36478 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1134,14 +1134,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - motion_toast: - dependency: "direct main" - description: - name: motion_toast - sha256: f33fad8264d6d5359e41f2027d2d833614401c3983102e8f0aa13ccbbdcdeecd - url: "https://pub.dev" - source: hosted - version: "2.6.8" mutex: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 66fb8eac..880f68d6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -102,7 +102,6 @@ dependencies: device_preview: ^1.1.0 media_kit_native_event_loop: ^1.0.4 dbus: ^0.7.8 - motion_toast: ^2.6.8 background_downloader: ^7.4.0 dev_dependencies: From c93c229b0fcc0692d3ef344a68b84b64b4726364 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 20 Jun 2023 09:39:27 +0600 Subject: [PATCH 20/28] chore: sleep timer custom hour format --- lib/components/player/player_actions.dart | 2 +- lib/extensions/duration.dart | 22 ++++++++++++++++++++++ pubspec.lock | 8 ++++++++ pubspec.yaml | 1 + 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/components/player/player_actions.dart b/lib/components/player/player_actions.dart index d7cf6bfb..9a01b531 100644 --- a/lib/components/player/player_actions.dart +++ b/lib/components/player/player_actions.dart @@ -171,7 +171,7 @@ class PlayerActions extends HookConsumerWidget { title: Text( customHoursEnabled ? context.l10n.custom_hours - : sleepTimer.toHumanReadableString(), + : sleepTimer.format(abbreviated: true), ), // only enabled when there's no preset timers selected enabled: customHoursEnabled, diff --git a/lib/extensions/duration.dart b/lib/extensions/duration.dart index a989f589..183fce5f 100644 --- a/lib/extensions/duration.dart +++ b/lib/extensions/duration.dart @@ -1,6 +1,28 @@ +import 'package:duration/locale.dart'; import 'package:spotube/utils/primitive_utils.dart'; +import 'package:duration/duration.dart'; extension DurationToHumanReadableString on Duration { String toHumanReadableString() => "${inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(inSeconds.remainder(60))}"; + + String format({ + DurationTersity tersity = DurationTersity.second, + DurationTersity upperTersity = DurationTersity.week, + DurationLocale locale = const EnglishDurationLocale(), + String? spacer, + String? delimiter, + String? conjugation, + bool abbreviated = false, + }) => + printDuration( + this, + tersity: tersity, + upperTersity: upperTersity, + locale: locale, + spacer: spacer, + delimiter: delimiter, + conjugation: conjugation, + abbreviated: abbreviated, + ); } diff --git a/pubspec.lock b/pubspec.lock index 14f36478..917028f6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -474,6 +474,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + duration: + dependency: "direct main" + description: + name: duration + sha256: d0b29d0a345429e3986ac56d60e4aef65b37d11e653022b2b9a4b361332b777f + url: "https://pub.dev" + source: hosted + version: "3.0.12" envied: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 880f68d6..69803ae1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -103,6 +103,7 @@ dependencies: media_kit_native_event_loop: ^1.0.4 dbus: ^0.7.8 background_downloader: ^7.4.0 + duration: ^3.0.12 dev_dependencies: build_runner: ^2.3.2 From 9251121ba0154599975e33819a43719477c644f8 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 20 Jun 2023 10:25:57 +0600 Subject: [PATCH 21/28] fix: track collection view status bar not transparent --- .../track_collection_heading.dart | 203 +++++++------- .../track_collection_view.dart | 249 +++++++++--------- lib/main.dart | 2 +- 3 files changed, 227 insertions(+), 227 deletions(-) diff --git a/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart b/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart index 45104eee..49bcd99c 100644 --- a/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart +++ b/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart @@ -71,118 +71,121 @@ class TrackCollectionHeading extends HookConsumerWidget { padding: const EdgeInsets.symmetric( horizontal: 20, ), - child: Flex( - direction: - constrains.mdAndDown ? Axis.vertical : Axis.horizontal, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 200), - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: UniversalImage( - path: titleImage, - placeholder: Assets.albumPlaceholder.path, + child: SafeArea( + child: Flex( + direction: constrains.mdAndDown + ? Axis.vertical + : Axis.horizontal, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 200), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: UniversalImage( + path: titleImage, + placeholder: Assets.albumPlaceholder.path, + ), ), ), - ), - const SizedBox(width: 10, height: 10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Text( - title, - style: theme.textTheme.titleLarge!.copyWith( - color: Colors.white, - fontWeight: FontWeight.w600, - ), - ), - if (album != null) + const SizedBox(width: 10, height: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ Text( - "${AlbumType.from(album?.albumType).formatted} • ${context.l10n.released} • ${DateTime.tryParse( - album?.releaseDate ?? "", - )?.year}", - style: theme.textTheme.titleMedium!.copyWith( + title, + style: theme.textTheme.titleLarge!.copyWith( color: Colors.white, - fontWeight: FontWeight.normal, + fontWeight: FontWeight.w600, ), ), - if (description != null) + if (album != null) + Text( + "${AlbumType.from(album?.albumType).formatted} • ${context.l10n.released} • ${DateTime.tryParse( + album?.releaseDate ?? "", + )?.year}", + style: theme.textTheme.titleMedium!.copyWith( + color: Colors.white, + fontWeight: FontWeight.normal, + ), + ), + if (description != null) + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: constrains.mdAndDown ? 400 : 300, + ), + child: Text( + description!, + style: const TextStyle(color: Colors.white), + maxLines: 2, + overflow: TextOverflow.fade, + ), + ), + const SizedBox(height: 10), + IconTheme( + data: theme.iconTheme.copyWith( + color: Colors.white, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: buttons, + ), + ), + const SizedBox(height: 10), ConstrainedBox( constraints: BoxConstraints( maxWidth: constrains.mdAndDown ? 400 : 300, ), - child: Text( - description!, - style: const TextStyle(color: Colors.white), - maxLines: 2, - overflow: TextOverflow.fade, + child: Row( + mainAxisSize: constrains.smAndUp + ? MainAxisSize.min + : MainAxisSize.min, + children: [ + Expanded( + child: FilledButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: color?.color, + ), + label: Text(context.l10n.shuffle), + icon: const Icon(SpotubeIcons.shuffle), + onPressed: tracksSnapshot.data == null || + isPlaying + ? null + : onShuffledPlay, + ), + ), + const SizedBox(width: 10), + Expanded( + child: FilledButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: color?.color, + foregroundColor: color?.bodyTextColor, + ), + onPressed: tracksSnapshot.data != null + ? onPlay + : null, + icon: Icon( + isPlaying + ? SpotubeIcons.stop + : SpotubeIcons.play, + ), + label: Text( + isPlaying + ? context.l10n.stop + : context.l10n.play, + ), + ), + ), + ], ), ), - const SizedBox(height: 10), - IconTheme( - data: theme.iconTheme.copyWith( - color: Colors.white, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: buttons, - ), - ), - const SizedBox(height: 10), - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: constrains.mdAndDown ? 400 : 300, - ), - child: Row( - mainAxisSize: constrains.smAndUp - ? MainAxisSize.min - : MainAxisSize.min, - children: [ - Expanded( - child: FilledButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: color?.color, - ), - label: Text(context.l10n.shuffle), - icon: const Icon(SpotubeIcons.shuffle), - onPressed: - tracksSnapshot.data == null || isPlaying - ? null - : onShuffledPlay, - ), - ), - const SizedBox(width: 10), - Expanded( - child: FilledButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: color?.color, - foregroundColor: color?.bodyTextColor, - ), - onPressed: tracksSnapshot.data != null - ? onPlay - : null, - icon: Icon( - isPlaying - ? SpotubeIcons.stop - : SpotubeIcons.play, - ), - label: Text( - isPlaying - ? context.l10n.stop - : context.l10n.play, - ), - ), - ), - ], - ), - ), - ], - ) - ], + ], + ) + ], + ), ), ), ), diff --git a/lib/components/shared/track_table/track_collection_view/track_collection_view.dart b/lib/components/shared/track_table/track_collection_view/track_collection_view.dart index c0caad96..419fb2d2 100644 --- a/lib/components/shared/track_table/track_collection_view/track_collection_view.dart +++ b/lib/components/shared/track_table/track_collection_view/track_collection_view.dart @@ -106,139 +106,136 @@ class TrackCollectionView extends HookConsumerWidget { return () => controller.removeListener(listener); }, [collapsed.value]); - return SafeArea( - bottom: false, - child: Scaffold( - appBar: kIsDesktop - ? const PageWindowTitleBar( - backgroundColor: Colors.transparent, - foregroundColor: Colors.white, - leadingWidth: 400, - leading: Align( - alignment: Alignment.centerLeft, - child: BackButton(color: Colors.white), + return Scaffold( + appBar: kIsDesktop + ? const PageWindowTitleBar( + backgroundColor: Colors.transparent, + foregroundColor: Colors.white, + leadingWidth: 400, + leading: Align( + alignment: Alignment.centerLeft, + child: BackButton(color: Colors.white), + ), + ) + : null, + extendBodyBehindAppBar: kIsDesktop, + body: RefreshIndicator( + onRefresh: () async { + await tracksSnapshot.refresh(); + }, + child: CustomScrollView( + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + SliverAppBar( + actions: [ + AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: collapsed.value ? 1 : 0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: buttons, + ), ), - ) - : null, - extendBodyBehindAppBar: kIsDesktop, - body: RefreshIndicator( - onRefresh: () async { - await tracksSnapshot.refresh(); - }, - child: CustomScrollView( - controller: controller, - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverAppBar( - actions: [ - AnimatedScale( - duration: const Duration(milliseconds: 200), - scale: collapsed.value ? 1 : 0, - child: Row( - mainAxisSize: MainAxisSize.min, - children: buttons, - ), + AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: collapsed.value ? 1 : 0, + child: IconButton( + tooltip: context.l10n.shuffle, + icon: const Icon(SpotubeIcons.shuffle), + onPressed: isPlaying ? null : onShuffledPlay, ), - AnimatedScale( - duration: const Duration(milliseconds: 200), - scale: collapsed.value ? 1 : 0, - child: IconButton( - tooltip: context.l10n.shuffle, - icon: const Icon(SpotubeIcons.shuffle), - onPressed: isPlaying ? null : onShuffledPlay, + ), + AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: collapsed.value ? 1 : 0, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + backgroundColor: theme.colorScheme.inversePrimary, ), + onPressed: tracksSnapshot.data != null ? onPlay : null, + child: Icon( + isPlaying ? SpotubeIcons.stop : SpotubeIcons.play), ), - AnimatedScale( - duration: const Duration(milliseconds: 200), - scale: collapsed.value ? 1 : 0, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - shape: const CircleBorder(), - backgroundColor: theme.colorScheme.inversePrimary, + ), + ], + floating: false, + pinned: true, + expandedHeight: 400, + automaticallyImplyLeading: kIsMobile, + leading: + kIsMobile ? const BackButton(color: Colors.white) : null, + iconTheme: IconThemeData(color: color?.titleTextColor), + primary: true, + backgroundColor: color?.color, + title: collapsed.value + ? Text( + title, + style: theme.textTheme.titleMedium!.copyWith( + color: color?.titleTextColor, + fontWeight: FontWeight.w600, ), - onPressed: tracksSnapshot.data != null ? onPlay : null, - child: Icon( - isPlaying ? SpotubeIcons.stop : SpotubeIcons.play), - ), - ), - ], - floating: false, - pinned: true, - expandedHeight: 400, - automaticallyImplyLeading: kIsMobile, - leading: - kIsMobile ? const BackButton(color: Colors.white) : null, - iconTheme: IconThemeData(color: color?.titleTextColor), - primary: true, - backgroundColor: color?.color, - title: collapsed.value - ? Text( - title, - style: theme.textTheme.titleMedium!.copyWith( - color: color?.titleTextColor, - fontWeight: FontWeight.w600, - ), - ) - : null, - centerTitle: true, - flexibleSpace: FlexibleSpaceBar( - background: TrackCollectionHeading( - color: color, - title: title, - description: description, - titleImage: titleImage, - isPlaying: isPlaying, - onPlay: onPlay, - onShuffledPlay: onShuffledPlay, - tracksSnapshot: tracksSnapshot, - buttons: buttons, - album: album, - ), + ) + : null, + centerTitle: true, + flexibleSpace: FlexibleSpaceBar( + background: TrackCollectionHeading( + color: color, + title: title, + description: description, + titleImage: titleImage, + isPlaying: isPlaying, + onPlay: onPlay, + onShuffledPlay: onShuffledPlay, + tracksSnapshot: tracksSnapshot, + buttons: buttons, + album: album, ), ), - HookBuilder( - builder: (context) { - if (tracksSnapshot.isLoading || !tracksSnapshot.hasData) { - return const ShimmerTrackTile(); - } else if (tracksSnapshot.hasError) { - return SliverToBoxAdapter( - child: Text( - context.l10n.error(tracksSnapshot.error ?? ""), - ), - ); - } - - return TracksTableView( - (tracksSnapshot.data ?? []).map( - (track) { - if (track is Track) { - return track; - } else { - return TypeConversionUtils.simpleTrack_X_Track( - track, - album!, - ); - } - }, - ).toList(), - onTrackPlayButtonPressed: onPlay, - playlistId: id, - userPlaylist: isOwned, - onFiltering: () { - // scroll the flexible space - // to allow more space for search results - controller.animateTo( - 330, - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - ); - }, + ), + HookBuilder( + builder: (context) { + if (tracksSnapshot.isLoading || !tracksSnapshot.hasData) { + return const ShimmerTrackTile(); + } else if (tracksSnapshot.hasError) { + return SliverToBoxAdapter( + child: Text( + context.l10n.error(tracksSnapshot.error ?? ""), + ), ); - }, - ) - ], - ), - )), - ); + } + + return TracksTableView( + (tracksSnapshot.data ?? []).map( + (track) { + if (track is Track) { + return track; + } else { + return TypeConversionUtils.simpleTrack_X_Track( + track, + album!, + ); + } + }, + ).toList(), + onTrackPlayButtonPressed: onPlay, + playlistId: id, + userPlaylist: isOwned, + onFiltering: () { + // scroll the flexible space + // to allow more space for search results + controller.animateTo( + 330, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + ); + }, + ); + }, + ) + ], + ), + )); } } diff --git a/lib/main.dart b/lib/main.dart index f9037e35..5541673d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -132,7 +132,7 @@ Future main(List rawArgs) async { runApp( DevicePreview( availableLocales: L10n.all, - enabled: !kReleaseMode, + enabled: !kReleaseMode && DesktopTools.platform.isDesktop, builder: (context) { return ProviderScope( child: QueryClientProvider( From 1c89e3efb0f05c648fc1c8e09039e62333de18d1 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 20 Jun 2023 11:55:23 +0600 Subject: [PATCH 22/28] fix: collection currently playing state persist on restart --- lib/components/album/album_card.dart | 8 ++++---- lib/components/playlist/playlist_card.dart | 9 ++++----- .../shared/track_table/tracks_table_view.dart | 7 ++++++- lib/pages/album/album.dart | 2 ++ lib/pages/playlist/playlist.dart | 2 ++ lib/provider/proxy_playlist/proxy_playlist.dart | 14 +++++++++++++- .../proxy_playlist/proxy_playlist_provider.dart | 15 +++++++++++++++ 7 files changed, 46 insertions(+), 11 deletions(-) diff --git a/lib/components/album/album_card.dart b/lib/components/album/album_card.dart index da66f276..afb637a0 100644 --- a/lib/components/album/album_card.dart +++ b/lib/components/album/album_card.dart @@ -46,11 +46,9 @@ class AlbumCard extends HookConsumerWidget { useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); final queryClient = useQueryClient(); - final query = queryClient - .getQuery, dynamic>("album-tracks/${album.id}"); bool isPlaylistPlaying = useMemoized( - () => playlist.containsTracks(query?.data ?? album.tracks ?? []), - [playlistNotifier, query?.data, album.tracks], + () => playlist.containsCollection(album.id!), + [playlist, album.id], ); final int marginH = useBreakpointValue(xs: 10, sm: 10, md: 15, lg: 20, xl: 20, xxl: 20); @@ -89,6 +87,7 @@ class AlbumCard extends HookConsumerWidget { [], autoPlay: true, ); + playlistNotifier.addCollection(album.id!); } finally { updating.value = false; } @@ -118,6 +117,7 @@ class AlbumCard extends HookConsumerWidget { if (fetchedTracks == null || fetchedTracks.isEmpty) return; playlistNotifier.addTracks(fetchedTracks); + playlistNotifier.addCollection(album.id!); if (context.mounted) { final snackbar = SnackBar( content: Text("Added ${album.tracks?.length} tracks to queue"), diff --git a/lib/components/playlist/playlist_card.dart b/lib/components/playlist/playlist_card.dart index 5c801ee5..579fbf93 100644 --- a/lib/components/playlist/playlist_card.dart +++ b/lib/components/playlist/playlist_card.dart @@ -24,13 +24,10 @@ class PlaylistCard extends HookConsumerWidget { final playing = useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; final queryBowl = QueryClient.of(context); - final query = queryBowl.getQuery, dynamic>( - "playlist-tracks/${playlist.id}", - ); final tracks = useState?>(null); bool isPlaylistPlaying = useMemoized( - () => playlistQueue.containsTracks(tracks.value ?? query?.data ?? []), - [playlistNotifier, tracks.value, query?.data], + () => playlistQueue.containsCollection(playlist.id!), + [playlistQueue, playlist.id], ); final updating = useState(false); @@ -72,6 +69,7 @@ class PlaylistCard extends HookConsumerWidget { if (fetchedTracks.isEmpty) return; await playlistNotifier.load(fetchedTracks, autoPlay: true); + playlistNotifier.addCollection(playlist.id!); tracks.value = fetchedTracks; } finally { updating.value = false; @@ -90,6 +88,7 @@ class PlaylistCard extends HookConsumerWidget { if (fetchedTracks.isEmpty) return; playlistNotifier.addTracks(fetchedTracks); + playlistNotifier.addCollection(playlist.id!); tracks.value = fetchedTracks; if (context.mounted) { final snackbar = SnackBar( diff --git a/lib/components/shared/track_table/tracks_table_view.dart b/lib/components/shared/track_table/tracks_table_view.dart index 5c4333e7..d41d4738 100644 --- a/lib/components/shared/track_table/tracks_table_view.dart +++ b/lib/components/shared/track_table/tracks_table_view.dart @@ -1,6 +1,5 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -219,6 +218,9 @@ class TracksTableView extends HookConsumerWidget { case "play-next": { playback.addTracksAtFirst(selectedTracks); + if (playlistId != null) { + playback.addCollection(playlistId!); + } selected.value = []; showCheck.value = false; break; @@ -226,6 +228,9 @@ class TracksTableView extends HookConsumerWidget { case "add-to-queue": { playback.addTracks(selectedTracks); + if (playlistId != null) { + playback.addCollection(playlistId!); + } selected.value = []; showCheck.value = false; break; diff --git a/lib/pages/album/album.dart b/lib/pages/album/album.dart index 69b542ee..0e9609d5 100644 --- a/lib/pages/album/album.dart +++ b/lib/pages/album/album.dart @@ -32,6 +32,7 @@ class AlbumPage extends HookConsumerWidget { sortedTracks, initialIndex: sortedTracks.indexWhere((s) => s.id == currentTrack?.id), ); + playback.addCollection(album.id!); } else if (isPlaylistPlaying && currentTrack.id != null && currentTrack.id != playlist.activeTrack?.id) { @@ -101,6 +102,7 @@ class AlbumPage extends HookConsumerWidget { TypeConversionUtils.simpleTrack_X_Track(track, album)) .toList(), ); + playback.addCollection(album.id!); } }, onShare: () { diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index 170d9693..de89429a 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -36,6 +36,7 @@ class PlaylistView extends HookConsumerWidget { initialIndex: sortedTracks.indexWhere((s) => s.id == currentTrack?.id), autoPlay: true, ); + playback.addCollection(playlist.id!); } else if (isPlaylistPlaying && currentTrack.id != null && currentTrack.id != proxyPlaylist.activeTrack?.id) { @@ -97,6 +98,7 @@ class PlaylistView extends HookConsumerWidget { onAddToQueue: () { if (tracksSnapshot.hasData && !isPlaylistPlaying) { playlistNotifier.addTracks(tracksSnapshot.data!); + playlistNotifier.addCollection(playlist.id!); } }, bottomSpace: mediaQuery.mdAndDown, diff --git a/lib/provider/proxy_playlist/proxy_playlist.dart b/lib/provider/proxy_playlist/proxy_playlist.dart index 2461f4b6..c0563f21 100644 --- a/lib/provider/proxy_playlist/proxy_playlist.dart +++ b/lib/provider/proxy_playlist/proxy_playlist.dart @@ -6,15 +6,20 @@ import 'package:spotube/models/spotube_track.dart'; class ProxyPlaylist { final Set tracks; + final Set collections; final int? active; - ProxyPlaylist(this.tracks, [this.active]); + ProxyPlaylist(this.tracks, [this.active, this.collections = const {}]); + factory ProxyPlaylist.fromJson(Map json) { return ProxyPlaylist( List.castFrom>( json['tracks'] ?? >[], ).map(_makeAppropriateTrack).toSet(), json['active'] as int?, + json['collections'] == null + ? {} + : (json['collections'] as List).toSet().cast(), ); } @@ -26,6 +31,10 @@ class ProxyPlaylist { activeTrack is! SpotubeTrack && activeTrack is! LocalTrack; + bool containsCollection(String collection) { + return collections.contains(collection); + } + bool containsTrack(TrackSimple track) { return tracks.firstWhereOrNull((element) => element.id == track.id) != null; } @@ -57,16 +66,19 @@ class ProxyPlaylist { return { 'tracks': tracks.map(_makeAppropriateTrackJson).toList(), 'active': active, + 'collections': collections.toList(), }; } ProxyPlaylist copyWith({ Set? tracks, int? active, + Set? collections, }) { return ProxyPlaylist( tracks ?? this.tracks, active ?? this.active, + collections ?? this.collections, ); } } diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index 166b40e9..7c59af62 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -185,6 +185,19 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier } } + void addCollection(String collectionId) { + state = state.copyWith(collections: { + ...state.collections, + collectionId, + }); + } + + void removeCollection(String collectionId) { + state = state.copyWith(collections: { + ...state.collections..remove(collectionId), + }); + } + // TODO: Safely Remove playing tracks Future removeTrack(String trackId) async { @@ -224,6 +237,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier state = state.copyWith( tracks: tracks.toSet(), active: initialIndex, + collections: {}, ); await notificationService.addTrack(indexTrack); } else { @@ -236,6 +250,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier state = state.copyWith( tracks: mergeTracks([addableTrack], tracks), active: initialIndex, + collections: {}, ); await notificationService.addTrack(addableTrack); await storeTrack( From 546fafbf37693acdc4633b5e21cafb7266d4210e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 20 Jun 2023 12:07:54 +0600 Subject: [PATCH 23/28] chore: --- lib/provider/proxy_playlist/proxy_playlist_provider.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index 7c59af62..8de5c7d2 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -462,12 +462,13 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier @override onInit() async { if (state.tracks.isEmpty) return null; - + final oldCollections = state.collections; await load( state.tracks, initialIndex: state.active ?? 0, autoPlay: false, ); + state = state.copyWith(collections: oldCollections); } @override From bcfbecc40aaf6a99c26a1a0d61282ad3fcc8d7be Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 25 Jun 2023 09:46:07 +0600 Subject: [PATCH 24/28] chore: update audio_service manifest config --- android/app/src/main/AndroidManifest.xml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 523f66b9..f47bab2e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -24,7 +24,7 @@ android:requestLegacyExternalStorage="true" > - + + - - + + From 6c2d65587b0e6e167be1d0b086df103c7e72d4b2 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 25 Jun 2023 10:00:44 +0600 Subject: [PATCH 25/28] fix(player): queue button not showing when not logged in --- lib/components/player/player_actions.dart | 53 +++++++------- lib/pages/home/personalized.dart | 15 ++-- lib/pages/player/player.dart | 88 ++++++++++++++++------- 3 files changed, 98 insertions(+), 58 deletions(-) diff --git a/lib/components/player/player_actions.dart b/lib/components/player/player_actions.dart index 9a01b531..577d5a83 100644 --- a/lib/components/player/player_actions.dart +++ b/lib/components/player/player_actions.dart @@ -22,10 +22,12 @@ import 'package:spotube/utils/type_conversion_utils.dart'; class PlayerActions extends HookConsumerWidget { final MainAxisAlignment mainAxisAlignment; final bool floatingQueue; + final bool showQueue; final List? extraActions; PlayerActions({ this.mainAxisAlignment = MainAxisAlignment.center, this.floatingQueue = true, + this.showQueue = true, this.extraActions, Key? key, }) : super(key: key); @@ -72,31 +74,32 @@ class PlayerActions extends HookConsumerWidget { return Row( mainAxisAlignment: mainAxisAlignment, children: [ - IconButton( - icon: const Icon(SpotubeIcons.queue), - tooltip: context.l10n.queue, - onPressed: playlist.activeTrack != null - ? () { - showModalBottomSheet( - context: context, - isDismissible: true, - enableDrag: true, - isScrollControlled: true, - backgroundColor: Colors.black12, - barrierColor: Colors.black12, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * .7, - ), - builder: (context) { - return PlayerQueue(floating: floatingQueue); - }, - ); - } - : null, - ), + if (showQueue) + IconButton( + icon: const Icon(SpotubeIcons.queue), + tooltip: context.l10n.queue, + onPressed: playlist.activeTrack != null + ? () { + showModalBottomSheet( + context: context, + isDismissible: true, + enableDrag: true, + isScrollControlled: true, + backgroundColor: Colors.black12, + barrierColor: Colors.black12, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * .7, + ), + builder: (context) { + return PlayerQueue(floating: floatingQueue); + }, + ); + } + : null, + ), if (!isLocalTrack) IconButton( icon: const Icon(SpotubeIcons.alternativeRoute), diff --git a/lib/pages/home/personalized.dart b/lib/pages/home/personalized.dart index 6c4ea851..29f6ecb5 100644 --- a/lib/pages/home/personalized.dart +++ b/lib/pages/home/personalized.dart @@ -10,6 +10,7 @@ import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart' import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/logger.dart'; +import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; @@ -94,6 +95,7 @@ class PersonalizedPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final auth = ref.watch(AuthenticationNotifier.provider); final featuredPlaylistsQuery = useQueries.playlist.featured(ref); final playlists = useMemoized( () => featuredPlaylistsQuery.pages @@ -132,12 +134,13 @@ class PersonalizedPage extends HookConsumerWidget { hasNextPage: featuredPlaylistsQuery.hasNextPage, onFetchMore: featuredPlaylistsQuery.fetchNext, ), - PersonalizedItemCard( - albums: albums, - title: context.l10n.new_releases, - hasNextPage: newReleases.hasNextPage, - onFetchMore: newReleases.fetchNext, - ), + if (auth != null) + PersonalizedItemCard( + albums: albums, + title: context.l10n.new_releases, + hasNextPage: newReleases.hasNextPage, + onFetchMore: newReleases.fetchNext, + ), ...?madeForUser.data?["content"]?["items"]?.map((item) { final playlists = item["content"]?["items"] ?.where((itemL2) => itemL2["type"] == "playlist") diff --git a/lib/pages/player/player.dart b/lib/pages/player/player.dart index 8e7750f4..9bd6bc8b 100644 --- a/lib/pages/player/player.dart +++ b/lib/pages/player/player.dart @@ -9,6 +9,7 @@ import 'package:spotube/collections/assets.gen.dart'; 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/player/player_queue.dart'; import 'package:spotube/components/player/volume_slider.dart'; import 'package:spotube/components/shared/animated_gradient.dart'; import 'package:spotube/components/shared/dialogs/track_details_dialog.dart'; @@ -77,6 +78,24 @@ class PlayerView extends HookConsumerWidget { foregroundColor: titleTextColor, toolbarOpacity: 1, leading: const BackButton(), + actions: [ + IconButton( + icon: const Icon(SpotubeIcons.info, size: 18), + tooltip: context.l10n.details, + style: IconButton.styleFrom(foregroundColor: bodyTextColor), + onPressed: currentTrack == null + ? null + : () { + showDialog( + context: context, + builder: (context) { + return TrackDetailsDialog( + track: currentTrack, + ); + }); + }, + ) + ], ), extendBodyBehindAppBar: true, body: SizedBox( @@ -183,38 +202,53 @@ class PlayerView extends HookConsumerWidget { const SizedBox(height: 25), PlayerActions( mainAxisAlignment: MainAxisAlignment.spaceEvenly, - floatingQueue: false, + showQueue: false, ), 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), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const SizedBox(width: 10), + Expanded( + child: OutlinedButton.icon( + icon: const Icon(SpotubeIcons.queue), + label: Text(context.l10n.queue), style: OutlinedButton.styleFrom( foregroundColor: bodyTextColor, side: BorderSide( color: bodyTextColor ?? Colors.white, ), ), - onPressed: currentTrack == null - ? null - : () { - showDialog( - context: context, - builder: (context) { - return TrackDetailsDialog( - track: currentTrack, - ); - }); - }, - ), - ), - const SizedBox(width: 10), + onPressed: currentTrack != null + ? () { + showModalBottomSheet( + context: context, + isDismissible: true, + enableDrag: true, + isScrollControlled: true, + backgroundColor: Colors.black12, + barrierColor: Colors.black12, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(10), + ), + constraints: BoxConstraints( + maxHeight: + MediaQuery.of(context) + .size + .height * + .7, + ), + builder: (context) { + return PlayerQueue( + floating: false); + }, + ); + } + : null), + ), + if (auth != null) const SizedBox(width: 10), + if (auth != null) Expanded( child: OutlinedButton.icon( label: Text(context.l10n.lyrics), @@ -251,9 +285,9 @@ class PlayerView extends HookConsumerWidget { }, ), ), - const SizedBox(width: 10), - ], - ), + const SizedBox(width: 10), + ], + ), const SizedBox(height: 25), SliderTheme( data: theme.sliderTheme.copyWith( From 72833a9bcd5ae8d5ec09d8a7545c4199eb10dbee Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 25 Jun 2023 10:09:16 +0600 Subject: [PATCH 26/28] chore: track collection view shuffle button color correction --- .../track_collection_view/track_collection_heading.dart | 2 +- .../track_collection_view/track_collection_view.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart b/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart index 49bcd99c..1aeb9107 100644 --- a/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart +++ b/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart @@ -147,7 +147,7 @@ class TrackCollectionHeading extends HookConsumerWidget { child: FilledButton.icon( style: ElevatedButton.styleFrom( backgroundColor: Colors.white, - foregroundColor: color?.color, + foregroundColor: Colors.black, ), label: Text(context.l10n.shuffle), icon: const Icon(SpotubeIcons.shuffle), diff --git a/lib/components/shared/track_table/track_collection_view/track_collection_view.dart b/lib/components/shared/track_table/track_collection_view/track_collection_view.dart index 419fb2d2..178b2aca 100644 --- a/lib/components/shared/track_table/track_collection_view/track_collection_view.dart +++ b/lib/components/shared/track_table/track_collection_view/track_collection_view.dart @@ -168,7 +168,7 @@ class TrackCollectionView extends HookConsumerWidget { kIsMobile ? const BackButton(color: Colors.white) : null, iconTheme: IconThemeData(color: color?.titleTextColor), primary: true, - backgroundColor: color?.color, + backgroundColor: color?.color.withOpacity(.5), title: collapsed.value ? Text( title, From 54c1ba777c205be61f4accdfca10068e47a9cfa1 Mon Sep 17 00:00:00 2001 From: maboroshin <41102508+maboroshin@users.noreply.github.com> Date: Sun, 25 Jun 2023 13:15:27 +0900 Subject: [PATCH 27/28] Add app_ja.arb Japanese translation (#531) --- lib/l10n/app_ja.arb | 250 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 lib/l10n/app_ja.arb diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb new file mode 100644 index 00000000..380bf484 --- /dev/null +++ b/lib/l10n/app_ja.arb @@ -0,0 +1,250 @@ +{ + "guest": "ゲスト", + "browse": "閲覧", + "search": "検索", + "library": "ライブラリ", + "lyrics": "歌詞", + "settings": "設定", + "genre_categories_filter": "カテゴリーやジャンルを絞り込み...", + "genre": "ジャンル", + "personalized": "あなたにおすすめ", + "featured": "注目", + "new_releases": "新着", + "songs": "曲", + "playing_track": "{track} を再生", + "queue_clear_alert": "現在のキューを消去します。{track_length} 曲を消去します。\n続行しますか?", + "load_more": "もっと読み込む", + "playlists": "再生リスト", + "artists": "アーティスト", + "albums": "アルバム", + "tracks": "曲", + "downloads": "ダウンロード", + "filter_playlists": "あなたの再生リストを絞り込み...", + "liked_tracks": "いいねした曲", + "liked_tracks_description": "いいねしたすべての曲", + "create_playlist": "再生リストの作成", + "create_a_playlist": "再生リストの作成, + "create": "作成", + "cancel": "キャンセル", + "playlist_name": "再生リスト名", + "name_of_playlist": "再生リストの名前", + "description": "説明", + "public": "公開", + "collaborative": "コラボ", + "search_local_tracks": "端末内の曲を検索...", + "play": "再生", + "delete": "削除", + "none": "なし", + "sort_a_z": "A-Z 順に並び替え", + "sort_z_a": "Z-A 順に並び替え", + "sort_artist": "アーティスト順に並び替え", + "sort_album": "アルバム順に並び替え", + "sort_tracks": "曲の並び替え", + "currently_downloading": "いまダウンロード中 ({tracks_length}) 曲", + "cancel_all": "すべてキャンセル", + "filter_artist": "アーティストを絞り込み...", + "followers": "{followers} フォロワー", + "add_artist_to_blacklist": "このアーティストをブラックリストに追加", + "top_tracks": "人気の曲", + "fans_also_like": "ファンの間で人気", + "loading": "読み込み中...", + "artist": "アーティスト", + "blacklisted": "ブラックリスト", + "following": "フォロー中", + "follow": "フォローする", + "artist_url_copied": "アーティストの URL をクリップボードにコピーしました", + "added_to_queue": "{tracks} をキューに追加しました", + "filter_albums": "アルバムを絞り込み...", + "synced": "同期する", + "plain": "そのまま", + "shuffle": "シャッフル", + "search_tracks": "曲を検索...", + "released": "リリース日", + "error": "エラー {error}", + "title": "タイトル", + "time": "長さ", + "more_actions": "ほかの操作", + "download_count": "ダウンロード ({count}) 曲", + "add_count_to_playlist": "再生リストに ({count}) 曲を追加", + "add_count_to_queue": "キューに ({count}) 曲を追加", + "play_count_next": "次に ({count}) 曲を再生", + "album": "アルバム", + "copied_to_clipboard": "{data} をクリップボードにコピーしました", + "add_to_following_playlists": "{track} をこの再生リストに追加", + "add": "追加", + "added_track_to_queue": "キューに {track} を追加しました", + "add_to_queue": "キューに追加", + "track_will_play_next": "{track} を次に再生", + "play_next": "次に再生", + "removed_track_from_queue": "キューから {track} を除去しました", + "remove_from_queue": "キューから除去", + "remove_from_favorites": "お気に入りから除去", + "save_as_favorite": "お気に入りに保存", + "add_to_playlist": "再生リストに追加", + "remove_from_playlist": "再生リストから除去", + "add_to_blacklist": "ブラックリストに追加", + "remove_from_blacklist": "ブラックリストから除去", + "share": "共有", + "mini_player": "ミニプレイヤー", + "slide_to_seek": "前後にスライドしてシーク", + "shuffle_playlist": "再生リストをシャッフル", + "unshuffle_playlist": "再生リストのシャッフル解除", + "previous_track": "前の曲", + "next_track": "次の曲", + "pause_playback": "再生を停止", + "resume_playback": "再生を再開", + "loop_track": "曲をループ", + "repeat_playlist": "再生リストをリピート", + "queue": "再生キュー", + "alternative_track_sources": "この曲の別の提供元を選ぶ", + "download_track": "曲のダウンロード", + "tracks_in_queue": "{tracks}曲の再生キュー", + "clear_all": "すべて消去l", + "show_hide_ui_on_hover": "マウスを乗せてUIを表示/隠す", + "always_on_top": "常に手前に表示", + "exit_mini_player": "ミニプレイヤーを終了", + "download_location": "ダウンロード先", + "account": "アカウント", + "login_with_spotify": "Spotify アカウントでログイン", + "connect_with_spotify": "Spotify に接続", + "logout": "ログアウト", + "logout_of_this_account": "このアカウントからログアウト", + "language_region": "言語 & 地域", + "language": "言語", + "system_default": "システムの既定値", + "market_place_region": "市場の地域", + "recommendation_country": "推薦先の国", + "appearance": "外観", + "layout_mode": "レイアウトの種類", + "override_layout_settings": "レスポンシブなレイアウトの種類の設定を上書きする", + "adaptive": "適応的", + "compact": "コンパクト", + "extended": "幅広", + "theme": "テーマ", + "dark": "ダーク", + "light": "ライト", + "system": "システムに従う", + "accent_color": "アクセントカラー", + "sync_album_color": "アルバムの色に合わせる", + "sync_album_color_description": "アルバムアートの主張色をアクセントカラーとして使用", + "playback": "再生", + "audio_quality": "音声品質", + "high": "高", + "low": "低", + "pre_download_play": "事前ダウンロードと再生", + "pre_download_play_description": "音声をストリーミングする代わりに、データをバイト単位でダウンロードして再生 (回線速度が早いユーザーにおすすめ)", + "skip_non_music": "音楽でない部分をスキップ (SponsorBlock)", + "blacklist_description": "曲とアーティストのブラックリスト", + "wait_for_download_to_finish": "現在のダウンロードが完了するまでお待ちください", + "download_lyrics": "曲と共に歌詞もダウンロード", + "desktop": "デスクトップ", + "close_behavior": "閉じた時の動作", + "close": "閉じる", + "minimize_to_tray": "トレイに最小化", + "show_tray_icon": "システムトレイにアイコンを表示", + "about": "このアプリについて", + "u_love_spotube": "Spotube が好きだと知っていますよ", + "check_for_updates": "アップデートの確認", + "about_spotube": "Spotube について", + "blacklist": "ブラックリスト", + "please_sponsor": "出資/寄付もお待ちします", + "spotube_description": "Spotube は、軽量でクロスプラットフォームな、すべて無料の spotify クライアント", + "version": "バージョン", + "build_number": "ビルド番号", + "founder": "創始者", + "repository": "リポジトリ", + "bug_issues": "バグや問題", + "made_with": "❤️ を込めてバングラディシュ🇧🇩で開発", + "kingkor_roy_tirtho": "Kingkor Roy Tirtho", + "copyright": "© 2021-{current_year} Kingkor Roy Tirtho", + "license": "ライセンス", + "add_spotify_credentials": "Spotify のログイン情報を追加してはじめましょう", + "credentials_will_not_be_shared_disclaimer": "心配ありません。個人情報を収集したり、共有されることはありません", + "know_how_to_login": "やり方が分からないですか?", + "follow_step_by_step_guide": "やり方の説明を見る", + "spotify_cookie": "Spotify {name} Cookies", + "cookie_name_cookie": "{name} Cookies", + "fill_in_all_fields": "すべての欄に入力してください", + "submit": "送信", + "exit": "終了", + "previous": "前へ", + "next": "次へ", + "done": "完了", + "step_1": "ステップ 1", + "first_go_to": "最初にここを開き", + "login_if_not_logged_in": "、ログインしてないならログインまたは登録します", + "step_2": "ステップ 2", + "step_2_steps": "1. ログインしたら、F12を押すか、マウス右クリック > 調査(検証)でブラウザの開発者ツール (devtools) を開きます。\n2. アプリケーション (Application) タブ (Chrome, Edge, Brave など) またはストレージタブ (Firefox, Palemoon など)\n3. Cookies 欄を選択し、https://accounts.spotify.com の枝を選びます", + "step_3": "ステップ 3", + "step_3_steps": "sp_dc と sp_key の値 (Value) をコピーします", + "success_emoji": "成功🥳", + "success_message": "アカウントへのログインに成功しました。よくできました!", + "step_4": "ステップ 4", + "step_4_steps": "コピーした sp_dc と sp_keyの値をそれぞれの入力欄に貼り付けます", + "something_went_wrong": "何か誤りがあります", + "piped_instance": "Piped サーバーのインスタンス", + "piped_description": "曲の matching (未訳)に使う Piped サーバーのインスタンス\nそれらの一部ではうまく動作しないこともあります。自己責任で使用してください", + "generate_playlist": "再生リストの生成", + "track_exists": "曲 {track} は既に存在します", + "replace_downloaded_tracks": "すべてのダウンロード済みの曲を置換", + "skip_download_tracks": "すべてのダウンロード済みの曲をスキップ", + "do_you_want_to_replace": "既存の曲と置換しますか?", + "replace": "置換する", + "skip": "スキップ", + "select_up_to_count_type": "{type}を最大{count} 個まで選択", + "select_genres": "ジャンルを選択", + "add_genres": "ジャンルを追加", + "country": "国", + "number_of_tracks_generate": "生成する曲数", + "acousticness": "アコースティック性", + "danceability": "ダンス性", + "energy": "エネルギー", + "instrumentalness": "楽器性", + "liveness": "ライブ性", + "loudness": "ラウドネス", + "speechiness": "会話性", + "valence": "多幸性", + "popularity": "人気度", + "key": "キー", + "duration": "長さ (秒)", + "tempo": "テンポ (BPM)", + "mode": "種類", + "time_signature": "拍子記号", + "short": "短", + "medium": "中", + "long": "長", + "min": "最小", + "max": "最大", + "target": "対象", + "moderate": "中", + "deselect_all": "すべて選択解除", + "select_all": "すべて選択", + "are_you_sure": "よろしいですか?", + "generating_playlist": "カスタム再生リストを生成中...", + "selected_count_tracks": "{count} 曲が選ばれました", + "download_warning": "全曲の一括ダウンロードは、明らかに音楽への海賊行為であり、音楽を生み出す共同体に損害を与えるでしょう。気づいてほしい。アーティストの多大な努力に敬意を払い、支援するようにしてください", + "download_ip_ban_warning": "また、通常よりも過剰なダウンロード要求があれば、YouTubeはあなたのIPをブロックします。つまり、そのIPの端末からは、少なくとも2-3か月の間、(ログインしても)YouTubeを利用できないということです。そうなっても Spotube は一切の責任を負いません", + "by_clicking_accept_terms": "「同意する」のクリックにより、以下の条件への同意となります:", + "download_agreement_1": "ええ、音楽への海賊行為だ。私は悪い", + "download_agreement_2": "芸術作品を買うお金がないのでそうするしかないが、アーティストをできる限り支援する", + "download_agreement_3": "私のIPがYouTubeにブロックされることがあると完全に把握した。私のこの行動により起きたどんな事故も、Spotube やその所有者/貢献者に責任はありません。", + "decline": "同意しない", + "accept": "同意する", + "details": "詳細", + "youtube": "YouTube", + "channel": "チャンネル", + "likes": "高評価", + "dislikes": "低評価", + "views": "視聴回数", + "streamUrl": "動画の URL", + "stop": "中止", + "sort_newest": "追加日の新しい順に並び替え", + "sort_oldest": "追加日の古い順に並び替え", + "sleep_timer": "スリープタイマー", + "mins": "{minutes} 分", + "hours": "{hours} 時間", + "hour": "{hours} 時間", + "custom_hours": "時間指定", + "logs": "ログ", + "developers": "開発" +} \ No newline at end of file From 4b52a71c0914bda6c831d8f637a5934f7bcf8fcb Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 25 Jun 2023 10:29:49 +0600 Subject: [PATCH 28/28] feat(translation): add Japanase locale --- lib/collections/language_codes.dart | 8 ++++---- lib/l10n/app_ja.arb | 2 +- lib/l10n/l10n.dart | 2 ++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/collections/language_codes.dart b/lib/collections/language_codes.dart index 89b7df77..e06d4209 100644 --- a/lib/collections/language_codes.dart +++ b/lib/collections/language_codes.dart @@ -296,10 +296,10 @@ abstract class LanguageLocals { // name: "Inuktitut", // nativeName: "ᐃᓄᒃᑎᑐᑦ", // ), - // "ja": const ISOLanguageName( - // name: "Japanese", - // nativeName: "日本語 (にほんご/にっぽんご)", - // ), + "ja": const ISOLanguageName( + name: "Japanese", + nativeName: "日本語 (にほんご/にっぽんご)", + ), // "jv": const ISOLanguageName( // name: "Javanese", // nativeName: "basa Jawa", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 380bf484..c5342bba 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -23,7 +23,7 @@ "liked_tracks": "いいねした曲", "liked_tracks_description": "いいねしたすべての曲", "create_playlist": "再生リストの作成", - "create_a_playlist": "再生リストの作成, + "create_a_playlist": "再生リストの作成", "create": "作成", "cancel": "キャンセル", "playlist_name": "再生リスト名", diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 5635013f..d4368b0d 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -2,6 +2,7 @@ /// /// Kingkor Roy Tirtho => English, Bengali /// ChatGPT (GPT 3.5) XD => Hindi, French +/// maboroshin@github => Japanese import 'package:flutter/material.dart'; class L10n { @@ -10,5 +11,6 @@ class L10n { const Locale('bn', 'BD'), const Locale('fr', 'FR'), const Locale('hi', 'IN'), + const Locale('ja', 'JA'), ]; }