From dd0bb01af53c7d62bc7681e17d5e4de8438a9bda Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 5 Jan 2025 16:16:57 +0600 Subject: [PATCH] refactor: generate playlist to shadcn --- lib/components/ui/button_tile.dart | 7 +- lib/l10n/app_en.arb | 4 +- .../recommendation_attribute_dials.dart | 185 ++-- .../recommendation_attribute_fields.dart | 240 ++--- .../seeds_multi_autocomplete.dart | 60 +- .../playlist_generate/simple_track_tile.dart | 10 +- lib/modules/library/user_playlists.dart | 2 +- .../playlist/playlist_create_dialog.dart | 11 +- .../playlist_generate/playlist_generate.dart | 917 +++++++++--------- .../playlist_generate_result.dart | 141 +-- untranslated_messages.json | 52 + 11 files changed, 868 insertions(+), 761 deletions(-) diff --git a/lib/components/ui/button_tile.dart b/lib/components/ui/button_tile.dart index d865b583..099691d0 100644 --- a/lib/components/ui/button_tile.dart +++ b/lib/components/ui/button_tile.dart @@ -31,8 +31,7 @@ class ButtonTile extends StatelessWidget { onPressed: onPressed, style: style.copyWith( decoration: (context, states, value) { - final decoration = ButtonVariance.outline.decoration(context, states) - as BoxDecoration; + final decoration = style.decoration(context, states) as BoxDecoration; if (selected && style == ButtonVariance.outline) { return decoration.copyWith( @@ -47,7 +46,7 @@ class ButtonTile extends StatelessWidget { return decoration; }, iconTheme: (context, states, value) { - final iconTheme = ButtonVariance.outline.iconTheme(context, states); + final iconTheme = style.iconTheme(context, states); if (selected && style == ButtonVariance.outline) { return iconTheme.copyWith( @@ -58,7 +57,7 @@ class ButtonTile extends StatelessWidget { return iconTheme; }, textStyle: (context, states, value) { - final textStyle = ButtonVariance.outline.textStyle(context, states); + final textStyle = style.textStyle(context, states); if (selected && style == ButtonVariance.outline) { return textStyle.copyWith( diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0be79bda..45a8d78f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -22,7 +22,7 @@ "filter_playlists": "Filter your playlists...", "liked_tracks": "Liked Tracks", "liked_tracks_description": "All your liked tracks", - "create_playlist": "Create Playlist", + "playlist": "Playlist", "create_a_playlist": "Create a playlist", "update_playlist": "Update playlist", "create": "Create", @@ -194,7 +194,7 @@ "invidious_instance": "Invidious Server Instance", "invidious_description": "The Invidious server instance to use for track matching", "invidious_warning": "Some of them might not work well. So use at your own risk", - "generate_playlist": "Generate Playlist", + "generate": "Generate", "track_exists": "Track {track} already exists", "replace_downloaded_tracks": "Replace all downloaded tracks", "skip_download_tracks": "Skip downloading all downloaded tracks", diff --git a/lib/modules/library/playlist_generate/recommendation_attribute_dials.dart b/lib/modules/library/playlist_generate/recommendation_attribute_dials.dart index d7f51ffb..564bfb55 100644 --- a/lib/modules/library/playlist_generate/recommendation_attribute_dials.dart +++ b/lib/modules/library/playlist_generate/recommendation_attribute_dials.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart'; @@ -29,23 +29,21 @@ class RecommendationAttributeDials extends HookWidget { @override Widget build(BuildContext context) { - final animation = useAnimationController( - duration: const Duration(milliseconds: 300), - ); - final labelStyle = Theme.of(context).textTheme.labelSmall?.copyWith( + final labelStyle = Theme.of(context).typography.small.copyWith( fontWeight: FontWeight.w500, ); final minSlider = Row( + spacing: 5, children: [ Text(context.l10n.min, style: labelStyle), Expanded( child: Slider( - value: values.min / base, + value: SliderValue.single(values.min / base), min: 0, max: 1, onChanged: (value) => onChanged(( - min: value * base, + min: value.value * base, target: values.target, max: values.max, )), @@ -55,16 +53,17 @@ class RecommendationAttributeDials extends HookWidget { ); final targetSlider = Row( + spacing: 5, children: [ Text(context.l10n.target, style: labelStyle), Expanded( child: Slider( - value: values.target / base, + value: SliderValue.single(values.target / base), min: 0, max: 1, onChanged: (value) => onChanged(( min: values.min, - target: value * base, + target: value.value * base, max: values.max, )), ), @@ -73,109 +72,111 @@ class RecommendationAttributeDials extends HookWidget { ); final maxSlider = Row( + spacing: 5, children: [ Text(context.l10n.max, style: labelStyle), Expanded( child: Slider( - value: values.max / base, + value: SliderValue.single(values.max / base), min: 0, max: 1, onChanged: (value) => onChanged(( min: values.min, target: values.target, - max: value * base, + max: value.value * base, )), ), ), ], ); - return LayoutBuilder(builder: (context, constrain) { - return Card( - child: ExpansionTile( - title: DefaultTextStyle( - style: Theme.of(context).textTheme.titleSmall!, - child: title, - ), - shape: const Border(), - leading: AnimatedBuilder( - animation: animation, - builder: (context, child) { - return Transform.rotate( - angle: (animation.value * 3.14) / 2, - child: child, - ); - }, - child: const Icon(Icons.chevron_right), - ), - trailing: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: ToggleButtons( - borderRadius: BorderRadius.circular(8), - textStyle: labelStyle, - isSelected: [ - values == lowValues(base), - values == moderateValues(base), - values == highValues(base), - ], - onPressed: (index) { - RecommendationAttribute newValues = zeroValues; - switch (index) { - case 0: - newValues = lowValues(base); - break; - case 1: - newValues = moderateValues(base); - break; - case 2: - newValues = highValues(base); - break; - } + void onSelected(int index) { + RecommendationAttribute newValues = zeroValues; + switch (index) { + case 0: + newValues = lowValues(base); + break; + case 1: + newValues = moderateValues(base); + break; + case 2: + newValues = highValues(base); + break; + } - if (newValues == values) { - onChanged(zeroValues); - } else { - onChanged(newValues); - } - }, + if (newValues == values) { + onChanged(zeroValues); + } else { + onChanged(newValues); + } + } + + return LayoutBuilder(builder: (context, constrain) { + return Accordion( + items: [ + AccordionItem( + trigger: AccordionTrigger( + child: SizedBox( + width: double.infinity, + child: Basic( + title: title.semiBold(), + trailing: Row( + spacing: 5, + children: [ + Toggle( + value: values == lowValues(base), + onChanged: (value) => onSelected(0), + style: + const ButtonStyle.outline(size: ButtonSize.small), + child: Text(context.l10n.low), + ), + Toggle( + value: values == moderateValues(base), + onChanged: (value) => onSelected(1), + style: + const ButtonStyle.outline(size: ButtonSize.small), + child: Text(context.l10n.moderate), + ), + Toggle( + value: values == highValues(base), + onChanged: (value) => onSelected(2), + style: + const ButtonStyle.outline(size: ButtonSize.small), + child: Text(context.l10n.high), + ), + ], + ), + ), + ), + ), + content: Column( + mainAxisSize: MainAxisSize.min, children: [ - Text(context.l10n.low), - Text(" ${context.l10n.moderate} "), - Text(context.l10n.high), + if (constrain.mdAndUp) + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const SizedBox(width: 16), + Expanded(child: minSlider), + Expanded(child: targetSlider), + Expanded(child: maxSlider), + ], + ) + else + Padding( + padding: const EdgeInsets.only(left: 16), + child: Column( + children: [ + minSlider, + targetSlider, + maxSlider, + ], + ), + ), ], ), ), - onExpansionChanged: (value) { - if (value) { - animation.forward(); - } else { - animation.reverse(); - } - }, - children: [ - if (constrain.mdAndUp) - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - const SizedBox(width: 16), - Expanded(child: minSlider), - Expanded(child: targetSlider), - Expanded(child: maxSlider), - ], - ) - else - Padding( - padding: const EdgeInsets.only(left: 16), - child: Column( - children: [ - minSlider, - targetSlider, - maxSlider, - ], - ), - ), - ], - ), + ], ); }); } diff --git a/lib/modules/library/playlist_generate/recommendation_attribute_fields.dart b/lib/modules/library/playlist_generate/recommendation_attribute_fields.dart index 7feff03a..351fde1e 100644 --- a/lib/modules/library/playlist_generate/recommendation_attribute_fields.dart +++ b/lib/modules/library/playlist_generate/recommendation_attribute_fields.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotube/modules/library/playlist_generate/recommendation_attribute_dials.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; @@ -21,13 +21,6 @@ class RecommendationAttributeFields extends HookWidget { @override Widget build(BuildContext context) { - final animation = useAnimationController( - duration: const Duration(milliseconds: 300), - ); - final labelStyle = Theme.of(context).textTheme.labelSmall?.copyWith( - fontWeight: FontWeight.w500, - ); - final minController = useTextEditingController(text: values.min.toString()); final targetController = useTextEditingController(text: values.target.toString()); @@ -53,126 +46,133 @@ class RecommendationAttributeFields extends HookWidget { }; }, [values]); - final minField = TextField( - controller: minController, - decoration: InputDecoration( - labelText: context.l10n.min, - isDense: true, - ), - keyboardType: const TextInputType.numberWithOptions( - decimal: false, - signed: true, - ), + final minField = Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 5, + children: [ + Text(context.l10n.min).semiBold(), + NumberInput( + controller: minController, + allowDecimals: false, + ), + ], ); - final targetField = TextField( - controller: targetController, - decoration: InputDecoration( - labelText: context.l10n.target, - isDense: true, - ), - keyboardType: const TextInputType.numberWithOptions( - decimal: false, - signed: true, - ), + final targetField = Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 5, + children: [ + Text(context.l10n.target).semiBold(), + NumberInput( + controller: targetController, + allowDecimals: false, + ), + ], ); - final maxField = TextField( - controller: maxController, - decoration: InputDecoration( - labelText: context.l10n.max, - isDense: true, - ), - keyboardType: const TextInputType.numberWithOptions( - decimal: false, - signed: true, - ), + final maxField = Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 5, + children: [ + Text(context.l10n.max).semiBold(), + NumberInput( + controller: maxController, + allowDecimals: false, + ), + ], ); - return LayoutBuilder(builder: (context, constrain) { - return Card( - child: ExpansionTile( - title: DefaultTextStyle( - style: Theme.of(context).textTheme.titleSmall!, - child: title, - ), - shape: const Border(), - leading: AnimatedBuilder( - animation: animation, - builder: (context, child) { - return Transform.rotate( - angle: (animation.value * 3.14) / 2, - child: child, - ); - }, - child: const Icon(Icons.chevron_right), - ), - trailing: presets == null - ? const SizedBox.shrink() - : Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: ToggleButtons( - borderRadius: BorderRadius.circular(8), - textStyle: labelStyle, - isSelected: presets!.values - .map((value) => value == values) - .toList(), - onPressed: (index) { - RecommendationAttribute newValues = - presets!.values.elementAt(index); - if (newValues == values) { - onChanged(zeroValues); - minController.text = zeroValues.min.toString(); - targetController.text = zeroValues.target.toString(); - maxController.text = zeroValues.max.toString(); - } else { - onChanged(newValues); - minController.text = newValues.min.toString(); - targetController.text = newValues.target.toString(); - maxController.text = newValues.max.toString(); - } - }, - children: presets!.keys.map((key) => Text(key)).toList(), - ), - ), - onExpansionChanged: (value) { - if (value) { - animation.forward(); - } else { - animation.reverse(); - } - }, - children: [ - const SizedBox(height: 8), - if (constrain.mdAndUp) - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - const SizedBox(width: 16), - Expanded(child: minField), - const SizedBox(width: 16), - Expanded(child: targetField), - const SizedBox(width: 16), - Expanded(child: maxField), - const SizedBox(width: 16), - ], - ) - else - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - children: [ - minField, - const SizedBox(height: 16), - targetField, - const SizedBox(height: 16), - maxField, - ], + void onSelected(int index) { + RecommendationAttribute newValues = presets!.values.elementAt(index); + if (newValues == values) { + onChanged(zeroValues); + minController.text = zeroValues.min.toString(); + targetController.text = zeroValues.target.toString(); + maxController.text = zeroValues.max.toString(); + } else { + onChanged(newValues); + minController.text = newValues.min.toString(); + targetController.text = newValues.target.toString(); + maxController.text = newValues.max.toString(); + } + } + + return LayoutBuilder(builder: (context, constraints) { + return Accordion( + items: [ + AccordionItem( + trigger: AccordionTrigger( + child: SizedBox( + width: double.infinity, + child: Basic( + title: title.semiBold(), + trailing: presets == null + ? const SizedBox.shrink() + : Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + spacing: 5, + children: [ + for (final presetEntry in presets?.entries + .toList() ?? + >[]) + Toggle( + value: presetEntry.value == values, + style: const ButtonStyle.outline( + size: ButtonSize.small, + ), + onChanged: (value) { + onSelected( + presets!.entries.toList().indexWhere( + (s) => s.key == presetEntry.key), + ); + }, + child: Text(presetEntry.key), + ), + ], + ), + ), ), ), - const SizedBox(height: 8), - ], - ), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8), + if (constraints.mdAndUp) + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const SizedBox(width: 16), + Expanded(child: minField), + const SizedBox(width: 16), + Expanded(child: targetField), + const SizedBox(width: 16), + Expanded(child: maxField), + const SizedBox(width: 16), + ], + ) + else + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + minField, + const SizedBox(height: 16), + targetField, + const SizedBox(height: 16), + maxField, + ], + ), + ), + const SizedBox(height: 8), + ], + ), + ), + ], ); }); } diff --git a/lib/modules/library/playlist_generate/seeds_multi_autocomplete.dart b/lib/modules/library/playlist_generate/seeds_multi_autocomplete.dart index 73c58deb..8c19ca6c 100644 --- a/lib/modules/library/playlist_generate/seeds_multi_autocomplete.dart +++ b/lib/modules/library/playlist_generate/seeds_multi_autocomplete.dart @@ -1,8 +1,9 @@ import 'dart:async'; import 'dart:math'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' show Autocomplete; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotube/extensions/constrains.dart'; enum SelectedItemDisplayType { @@ -20,10 +21,13 @@ class SeedsMultiAutocomplete extends HookWidget { final Widget Function(T option) selectedSeedBuilder; final String Function(T option) displayStringForOption; - final InputDecoration? inputDecoration; final bool enabled; final SelectedItemDisplayType selectedItemDisplayType; + final Widget? placeholder; + final Widget? leading; + final Widget? trailing; + final Widget? label; const SeedsMultiAutocomplete({ super.key, @@ -32,9 +36,12 @@ class SeedsMultiAutocomplete extends HookWidget { required this.autocompleteOptionBuilder, required this.displayStringForOption, required this.selectedSeedBuilder, - this.inputDecoration, this.enabled = true, this.selectedItemDisplayType = SelectedItemDisplayType.wrap, + this.placeholder, + this.leading, + this.trailing, + this.label, }); @override @@ -61,6 +68,10 @@ class SeedsMultiAutocomplete extends HookWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + if (label != null) ...[ + label!.semiBold(), + const Gap(8), + ], LayoutBuilder(builder: (context, constrains) { return Container( key: containerKey.value, @@ -101,13 +112,15 @@ class SeedsMultiAutocomplete extends HookWidget { focusNode, onFieldSubmitted, ) { - return TextFormField( + return TextField( controller: seedController, onChanged: (value) => textEditingController.text = value, focusNode: focusNode, - onFieldSubmitted: (_) => onFieldSubmitted(), + onSubmitted: (_) => onFieldSubmitted(), enabled: enabled, - decoration: inputDecoration, + leading: leading, + trailing: trailing, + placeholder: placeholder, ); }, ), @@ -120,22 +133,27 @@ class SeedsMultiAutocomplete extends HookWidget { runSpacing: 4, children: seeds.value.map(selectedSeedBuilder).toList(), ), - SelectedItemDisplayType.list => Card( - margin: EdgeInsets.zero, - child: Column( - children: [ - for (final seed in seeds.value) ...[ - selectedSeedBuilder(seed), - if (seeds.value.length > 1 && seed != seeds.value.last) - Divider( - color: theme.colorScheme.primaryContainer, - height: 1, - indent: 12, - endIndent: 12, + SelectedItemDisplayType.list => AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: seeds.value.isEmpty + ? const SizedBox.shrink() + : Card( + child: Column( + children: [ + for (final seed in seeds.value) ...[ + selectedSeedBuilder(seed), + if (seeds.value.length > 1 && + seed != seeds.value.last) + Divider( + color: theme.colorScheme.secondary, + height: 1, + indent: 12, + endIndent: 12, + ), + ], + ], ), - ], - ], - ), + ), ), }, ], diff --git a/lib/modules/library/playlist_generate/simple_track_tile.dart b/lib/modules/library/playlist_generate/simple_track_tile.dart index e6cc281f..afa723f3 100644 --- a/lib/modules/library/playlist_generate/simple_track_tile.dart +++ b/lib/modules/library/playlist_generate/simple_track_tile.dart @@ -1,9 +1,10 @@ -import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/image/universal_image.dart'; +import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/extensions/image.dart'; class SimpleTrackTile extends HookWidget { @@ -17,7 +18,7 @@ class SimpleTrackTile extends HookWidget { @override Widget build(BuildContext context) { - return ListTile( + return ButtonTile( leading: ClipRRect( borderRadius: BorderRadius.circular(8), child: UniversalImage( @@ -28,18 +29,17 @@ class SimpleTrackTile extends HookWidget { width: 40, ), ), - horizontalTitleGap: 10, - contentPadding: const EdgeInsets.symmetric(horizontal: 8), title: Text(track.name!), trailing: onDelete == null ? null - : IconButton( + : IconButton.ghost( icon: const Icon(SpotubeIcons.close), onPressed: onDelete, ), subtitle: Text( track.artists?.map((e) => e.name).join(", ") ?? track.album?.name ?? "", ), + style: ButtonVariance.ghost, ); } } diff --git a/lib/modules/library/user_playlists.dart b/lib/modules/library/user_playlists.dart index 58462bf9..06f08ab6 100644 --- a/lib/modules/library/user_playlists.dart +++ b/lib/modules/library/user_playlists.dart @@ -110,7 +110,7 @@ class UserPlaylists extends HookConsumerWidget { const Gap(10), Button.primary( leading: const Icon(SpotubeIcons.magic), - child: Text(context.l10n.generate_playlist), + child: Text(context.l10n.generate), onPressed: () { ServiceUtils.pushNamed( context, diff --git a/lib/modules/playlist/playlist_create_dialog.dart b/lib/modules/playlist/playlist_create_dialog.dart index 8b231b84..55e7ce77 100644 --- a/lib/modules/playlist/playlist_create_dialog.dart +++ b/lib/modules/playlist/playlist_create_dialog.dart @@ -16,7 +16,6 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/form/checkbox_form_field.dart'; import 'package:spotube/components/form/text_form_field.dart'; import 'package:spotube/components/image/universal_image.dart'; -import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/provider/spotify/spotify.dart'; @@ -267,19 +266,11 @@ class PlaylistCreateDialogButton extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final mediaQuery = MediaQuery.of(context); final spotify = ref.watch(spotifyProvider); - if (mediaQuery.smAndDown) { - return IconButton.secondary( - icon: const Icon(SpotubeIcons.addFilled), - onPressed: () => showPlaylistDialog(context, spotify), - ); - } - return Button.secondary( leading: const Icon(SpotubeIcons.addFilled), - child: Text(context.l10n.create_playlist), + child: Text(context.l10n.playlist), onPressed: () => showPlaylistDialog(context, spotify), ); } diff --git a/lib/pages/library/playlist_generate/playlist_generate.dart b/lib/pages/library/playlist_generate/playlist_generate.dart index 63565362..2b1e7512 100644 --- a/lib/pages/library/playlist_generate/playlist_generate.dart +++ b/lib/pages/library/playlist_generate/playlist_generate.dart @@ -1,12 +1,15 @@ import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; + import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotify_markets.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/modules/library/playlist_generate/multi_select_field.dart'; +import 'package:spotube/components/button/back_button.dart'; +import 'package:spotube/components/ui/button_tile.dart'; + import 'package:spotube/modules/library/playlist_generate/recommendation_attribute_dials.dart'; import 'package:spotube/modules/library/playlist_generate/recommendation_attribute_fields.dart'; import 'package:spotube/modules/library/playlist_generate/seeds_multi_autocomplete.dart'; @@ -33,7 +36,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget { final spotify = ref.watch(spotifyProvider); final theme = Theme.of(context); - final textTheme = theme.textTheme; + final typography = theme.typography; final preferences = ref.watch(userPreferencesProvider); final genresCollection = ref.watch(categoryGenresProvider); @@ -59,14 +62,11 @@ class PlaylistGeneratorPage extends HookConsumerWidget { final artistAutoComplete = SeedsMultiAutocomplete( seeds: artists, enabled: enabled, - inputDecoration: InputDecoration( - labelText: context.l10n.artists, - labelStyle: textTheme.titleMedium, - helperText: context.l10n.select_up_to_count_type( - leftSeedCount, - context.l10n.artists, - ), - ), + label: Text(context.l10n.artists), + placeholder: Text(context.l10n.select_up_to_count_type( + leftSeedCount, + context.l10n.artists, + )), fetchSeeds: (textEditingValue) => spotify.search .get( textEditingValue.text, @@ -83,15 +83,15 @@ class PlaylistGeneratorPage extends HookConsumerWidget { ) .toList(), ), - autocompleteOptionBuilder: (option, onSelected) => ListTile( - leading: CircleAvatar( - backgroundImage: UniversalImage.imageProvider( + autocompleteOptionBuilder: (option, onSelected) => ButtonTile( + leading: Avatar( + initials: "O", + provider: UniversalImage.imageProvider( option.images.asUrlString( placeholder: ImagePlaceholder.artist, ), ), ), - horizontalTitleGap: 20, title: Text(option.name!), subtitle: option.genres?.isNotEmpty != true ? null @@ -101,34 +101,36 @@ class PlaylistGeneratorPage extends HookConsumerWidget { children: option.genres!.mapIndexed( (index, genre) { return Chip( - label: Text(genre), - labelStyle: textTheme.bodySmall?.copyWith( - color: theme.colorScheme.secondary, - fontWeight: FontWeight.w600, - ), - side: BorderSide.none, - backgroundColor: theme.colorScheme.secondaryContainer, + style: ButtonVariance.secondary, + child: Text(genre), ); }, ).toList(), ), - onTap: () => onSelected(option), + onPressed: () => onSelected(option), + style: ButtonVariance.ghost, ), displayStringForOption: (option) => option.name!, - selectedSeedBuilder: (artist) => Chip( - avatar: CircleAvatar( - backgroundImage: UniversalImage.imageProvider( + selectedSeedBuilder: (artist) => OutlineBadge( + leading: Avatar( + initials: artist.name!.substring(0, 1), + size: 30, + provider: UniversalImage.imageProvider( artist.images.asUrlString( placeholder: ImagePlaceholder.artist, ), ), ), - label: Text(artist.name!), - onDeleted: () { - artists.value = [ - ...artists.value..removeWhere((element) => element.id == artist.id) - ]; - }, + trailing: IconButton.ghost( + icon: const Icon(SpotubeIcons.close), + onPressed: () { + artists.value = [ + ...artists.value + ..removeWhere((element) => element.id == artist.id) + ]; + }, + ), + child: Text(artist.name!), ), ); @@ -136,14 +138,11 @@ class PlaylistGeneratorPage extends HookConsumerWidget { seeds: tracks, enabled: enabled, selectedItemDisplayType: SelectedItemDisplayType.list, - inputDecoration: InputDecoration( - labelText: context.l10n.tracks, - labelStyle: textTheme.titleMedium, - helperText: context.l10n.select_up_to_count_type( - leftSeedCount, - context.l10n.tracks, - ), - ), + label: Text(context.l10n.tracks), + placeholder: Text(context.l10n.select_up_to_count_type( + leftSeedCount, + context.l10n.tracks, + )), fetchSeeds: (textEditingValue) => spotify.search .get( textEditingValue.text, @@ -160,22 +159,23 @@ class PlaylistGeneratorPage extends HookConsumerWidget { ) .toList(), ), - autocompleteOptionBuilder: (option, onSelected) => ListTile( - leading: CircleAvatar( - backgroundImage: UniversalImage.imageProvider( + autocompleteOptionBuilder: (option, onSelected) => ButtonTile( + leading: Avatar( + initials: option.name!.substring(0, 1), + provider: UniversalImage.imageProvider( (option.album?.images).asUrlString( placeholder: ImagePlaceholder.artist, ), ), ), - horizontalTitleGap: 20, title: Text(option.name!), subtitle: Text( option.artists?.map((e) => e.name).join(", ") ?? option.album?.name ?? "", ), - onTap: () => onSelected(option), + onPressed: () => onSelected(option), + style: ButtonVariance.ghost, ), displayStringForOption: (option) => option.name!, selectedSeedBuilder: (option) => SimpleTrackTile( @@ -188,42 +188,65 @@ class PlaylistGeneratorPage extends HookConsumerWidget { ), ); - final genreSelector = MultiSelectField( - options: genresCollection.asData?.value ?? [], - selectedOptions: genres.value, - getValueForOption: (option) => option, - onSelected: (value) { + final genreSelector = MultiSelect( + value: genres.value, + searchFilter: (item, query) { + return item.toLowerCase().contains(query.toLowerCase()) ? 1 : 0; + }, + onChanged: (value) { + if (!enabled) return; genres.value = value; }, - dialogTitle: Text(context.l10n.select_genres), - label: Text(context.l10n.add_genres), - helperText: context.l10n.select_up_to_count_type( - leftSeedCount, - context.l10n.genre, + itemBuilder: (context, item) => Text(item), + searchPlaceholder: Text(context.l10n.select_genres), + orderSelectedFirst: false, + popoverAlignment: Alignment.bottomCenter, + popupConstraints: BoxConstraints( + maxHeight: MediaQuery.sizeOf(context).height * .8, ), - enabled: enabled, + placeholder: Text( + context.l10n.select_up_to_count_type( + leftSeedCount, + context.l10n.genre, + ), + ), + children: [ + for (final option in genresCollection.asData?.value ?? []) + SelectItemButton( + value: option, + child: Text(option), + ), + ], ); + final countrySelector = ValueListenableBuilder( valueListenable: market, builder: (context, value, _) { - return DropdownButtonFormField( - decoration: InputDecoration( - labelText: context.l10n.country, - labelStyle: textTheme.titleMedium, + return Select( + placeholder: Text(context.l10n.country), + value: market.value, + onChanged: (value) { + market.value = value!; + }, + searchFilter: (item, query) { + return item.name.toLowerCase().contains(query.toLowerCase()) + ? 1 + : 0; + }, + searchPlaceholder: Text(context.l10n.search), + popupConstraints: BoxConstraints( + maxHeight: MediaQuery.sizeOf(context).height * .8, ), - isExpanded: true, - items: spotifyMarkets + popoverAlignment: Alignment.bottomCenter, + itemBuilder: (context, value) => Text(value.name), + children: spotifyMarkets .map( - (country) => DropdownMenuItem( + (country) => SelectItemButton( value: country.$1, child: Text(country.$2), ), ) .toList(), - value: market.value, - onChanged: (value) { - market.value = value!; - }, ); }, ); @@ -231,395 +254,394 @@ class PlaylistGeneratorPage extends HookConsumerWidget { final controller = useScrollController(); return Scaffold( - appBar: TitleBar( - leading: const [BackButton()], - title: Text(context.l10n.generate_playlist), - ), - body: Scrollbar( + headers: [ + TitleBar( + leading: const [BackButton()], + title: Text(context.l10n.generate), + ) + ], + child: Scrollbar( controller: controller, child: Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: Breakpoints.lg), - child: SliderTheme( - data: const SliderThemeData( - overlayShape: RoundSliderOverlayShape(), - ), - child: SafeArea( - child: LayoutBuilder(builder: (context, constrains) { - return ScrollConfiguration( - behavior: ScrollConfiguration.of(context) - .copyWith(scrollbars: false), - child: ListView( - controller: controller, - 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( + child: SafeArea( + child: LayoutBuilder(builder: (context, constrains) { + return ScrollConfiguration( + behavior: ScrollConfiguration.of(context) + .copyWith(scrollbars: false), + child: ListView( + controller: controller, + 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: typography.semiBold, + ), + Row( + spacing: 5, + children: [ + Container( + width: 40, + height: 40, + alignment: Alignment.center, + decoration: BoxDecoration( + color: theme.colorScheme.primary + .withAlpha(25), + shape: BoxShape.circle, + ), + child: Text( + value.round().toString(), + style: typography.large.copyWith( color: theme.colorScheme.primary, - shape: BoxShape.circle, - ), - child: Text( - value.round().toString(), - style: textTheme.bodyLarge?.copyWith( - color: theme - .colorScheme.primaryContainer, - ), ), ), - Expanded( - child: Slider( - 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, - ), + ), + Expanded( + child: Slider( + value: + SliderValue.single(value.toDouble()), + min: 10, + max: 100, + divisions: 9, + onChanged: (value) { + limit.value = value.value.round(); + }, + ), + ) + ], + ) ], - ) - 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: ( - target: target.value.acousticness?.toDouble() ?? 0, - min: min.value.acousticness?.toDouble() ?? 0, - max: max.value.acousticness?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - acousticness: value.target, - ); - min.value = min.value.copyWith( - acousticness: value.min, - ); - max.value = max.value.copyWith( - acousticness: value.max, - ); - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.danceability), - values: ( - target: target.value.danceability?.toDouble() ?? 0, - min: min.value.danceability?.toDouble() ?? 0, - max: max.value.danceability?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - danceability: value.target, - ); - min.value = min.value.copyWith( - danceability: value.min, - ); - max.value = max.value.copyWith( - danceability: value.max, - ); - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.energy), - values: ( - target: target.value.energy?.toDouble() ?? 0, - min: min.value.energy?.toDouble() ?? 0, - max: max.value.energy?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - energy: value.target, - ); - min.value = min.value.copyWith( - energy: value.min, - ); - max.value = max.value.copyWith( - energy: value.max, - ); - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.instrumentalness), - values: ( - target: - target.value.instrumentalness?.toDouble() ?? 0, - min: min.value.instrumentalness?.toDouble() ?? 0, - max: max.value.instrumentalness?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - instrumentalness: value.target, - ); - min.value = min.value.copyWith( - instrumentalness: value.min, - ); - max.value = max.value.copyWith( - instrumentalness: value.max, - ); - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.liveness), - values: ( - target: target.value.liveness?.toDouble() ?? 0, - min: min.value.liveness?.toDouble() ?? 0, - max: max.value.liveness?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - liveness: value.target, - ); - min.value = min.value.copyWith( - liveness: value.min, - ); - max.value = max.value.copyWith( - liveness: value.max, - ); - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.loudness), - values: ( - target: target.value.loudness?.toDouble() ?? 0, - min: min.value.loudness?.toDouble() ?? 0, - max: max.value.loudness?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - loudness: value.target, - ); - min.value = min.value.copyWith( - loudness: value.min, - ); - max.value = max.value.copyWith( - loudness: value.max, - ); - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.speechiness), - values: ( - target: target.value.speechiness?.toDouble() ?? 0, - min: min.value.speechiness?.toDouble() ?? 0, - max: max.value.speechiness?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - speechiness: value.target, - ); - min.value = min.value.copyWith( - speechiness: value.min, - ); - max.value = max.value.copyWith( - speechiness: value.max, - ); - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.valence), - values: ( - target: target.value.valence?.toDouble() ?? 0, - min: min.value.valence?.toDouble() ?? 0, - max: max.value.valence?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - valence: value.target, - ); - min.value = min.value.copyWith( - valence: value.min, - ); - max.value = max.value.copyWith( - valence: value.max, - ); - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.popularity), - base: 100, - values: ( - target: target.value.popularity?.toDouble() ?? 0, - min: min.value.popularity?.toDouble() ?? 0, - max: max.value.popularity?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - popularity: value.target, - ); - min.value = min.value.copyWith( - popularity: value.min, - ); - max.value = max.value.copyWith( - popularity: value.max, - ); - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.key), - base: 11, - values: ( - target: target.value.key?.toDouble() ?? 0, - min: min.value.key?.toDouble() ?? 0, - max: max.value.key?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - key: value.target, - ); - min.value = min.value.copyWith( - key: value.min, - ); - max.value = max.value.copyWith( - key: value.max, - ); - }, - ), - RecommendationAttributeFields( - title: Text(context.l10n.duration), - values: ( - max: (max.value.durationMs ?? 0) / 1000, - target: (target.value.durationMs ?? 0) / 1000, - min: (min.value.durationMs ?? 0) / 1000, - ), - onChanged: (value) { - target.value = target.value.copyWith( - durationMs: (value.target * 1000).toInt(), - ); - min.value = min.value.copyWith( - durationMs: (value.min * 1000).toInt(), - ); - max.value = max.value.copyWith( - durationMs: (value.max * 1000).toInt(), - ); - }, - presets: { - context.l10n.short: (min: 50, target: 90, max: 120), - context.l10n.medium: ( - min: 120, - target: 180, - max: 200 + ); + }, + ), + const SizedBox(height: 16), + if (constrains.mdAndUp) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: countrySelector, ), - context.l10n.long: (min: 480, target: 560, max: 640) - }, + 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: ( + target: target.value.acousticness?.toDouble() ?? 0, + min: min.value.acousticness?.toDouble() ?? 0, + max: max.value.acousticness?.toDouble() ?? 0, ), - RecommendationAttributeFields( - title: Text(context.l10n.tempo), - values: ( - max: max.value.tempo?.toDouble() ?? 0, - target: target.value.tempo?.toDouble() ?? 0, - min: min.value.tempo?.toDouble() ?? 0, + onChanged: (value) { + target.value = target.value.copyWith( + acousticness: value.target, + ); + min.value = min.value.copyWith( + acousticness: value.min, + ); + max.value = max.value.copyWith( + acousticness: value.max, + ); + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.danceability), + values: ( + target: target.value.danceability?.toDouble() ?? 0, + min: min.value.danceability?.toDouble() ?? 0, + max: max.value.danceability?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + danceability: value.target, + ); + min.value = min.value.copyWith( + danceability: value.min, + ); + max.value = max.value.copyWith( + danceability: value.max, + ); + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.energy), + values: ( + target: target.value.energy?.toDouble() ?? 0, + min: min.value.energy?.toDouble() ?? 0, + max: max.value.energy?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + energy: value.target, + ); + min.value = min.value.copyWith( + energy: value.min, + ); + max.value = max.value.copyWith( + energy: value.max, + ); + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.instrumentalness), + values: ( + target: + target.value.instrumentalness?.toDouble() ?? 0, + min: min.value.instrumentalness?.toDouble() ?? 0, + max: max.value.instrumentalness?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + instrumentalness: value.target, + ); + min.value = min.value.copyWith( + instrumentalness: value.min, + ); + max.value = max.value.copyWith( + instrumentalness: value.max, + ); + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.liveness), + values: ( + target: target.value.liveness?.toDouble() ?? 0, + min: min.value.liveness?.toDouble() ?? 0, + max: max.value.liveness?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + liveness: value.target, + ); + min.value = min.value.copyWith( + liveness: value.min, + ); + max.value = max.value.copyWith( + liveness: value.max, + ); + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.loudness), + values: ( + target: target.value.loudness?.toDouble() ?? 0, + min: min.value.loudness?.toDouble() ?? 0, + max: max.value.loudness?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + loudness: value.target, + ); + min.value = min.value.copyWith( + loudness: value.min, + ); + max.value = max.value.copyWith( + loudness: value.max, + ); + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.speechiness), + values: ( + target: target.value.speechiness?.toDouble() ?? 0, + min: min.value.speechiness?.toDouble() ?? 0, + max: max.value.speechiness?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + speechiness: value.target, + ); + min.value = min.value.copyWith( + speechiness: value.min, + ); + max.value = max.value.copyWith( + speechiness: value.max, + ); + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.valence), + values: ( + target: target.value.valence?.toDouble() ?? 0, + min: min.value.valence?.toDouble() ?? 0, + max: max.value.valence?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + valence: value.target, + ); + min.value = min.value.copyWith( + valence: value.min, + ); + max.value = max.value.copyWith( + valence: value.max, + ); + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.popularity), + base: 100, + values: ( + target: target.value.popularity?.toDouble() ?? 0, + min: min.value.popularity?.toDouble() ?? 0, + max: max.value.popularity?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + popularity: value.target, + ); + min.value = min.value.copyWith( + popularity: value.min, + ); + max.value = max.value.copyWith( + popularity: value.max, + ); + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.key), + base: 11, + values: ( + target: target.value.key?.toDouble() ?? 0, + min: min.value.key?.toDouble() ?? 0, + max: max.value.key?.toDouble() ?? 0, + ), + onChanged: (value) { + target.value = target.value.copyWith( + key: value.target, + ); + min.value = min.value.copyWith( + key: value.min, + ); + max.value = max.value.copyWith( + key: value.max, + ); + }, + ), + RecommendationAttributeFields( + title: Text(context.l10n.duration), + values: ( + max: (max.value.durationMs ?? 0) / 1000, + target: (target.value.durationMs ?? 0) / 1000, + min: (min.value.durationMs ?? 0) / 1000, + ), + onChanged: (value) { + target.value = target.value.copyWith( + durationMs: (value.target * 1000).toInt(), + ); + min.value = min.value.copyWith( + durationMs: (value.min * 1000).toInt(), + ); + max.value = max.value.copyWith( + durationMs: (value.max * 1000).toInt(), + ); + }, + presets: { + context.l10n.short: (min: 50, target: 90, max: 120), + context.l10n.medium: ( + min: 120, + target: 180, + max: 200 ), - onChanged: (value) { - target.value = target.value.copyWith( - tempo: value.target, - ); - min.value = min.value.copyWith( - tempo: value.min, - ); - max.value = max.value.copyWith( - tempo: value.max, - ); - }, + context.l10n.long: (min: 480, target: 560, max: 640) + }, + ), + RecommendationAttributeFields( + title: Text(context.l10n.tempo), + values: ( + max: max.value.tempo?.toDouble() ?? 0, + target: target.value.tempo?.toDouble() ?? 0, + min: min.value.tempo?.toDouble() ?? 0, ), - RecommendationAttributeFields( - title: Text(context.l10n.mode), - values: ( - max: max.value.mode?.toDouble() ?? 0, - target: target.value.mode?.toDouble() ?? 0, - min: min.value.mode?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - mode: value.target, - ); - min.value = min.value.copyWith( - mode: value.min, - ); - max.value = max.value.copyWith( - mode: value.max, - ); - }, + onChanged: (value) { + target.value = target.value.copyWith( + tempo: value.target, + ); + min.value = min.value.copyWith( + tempo: value.min, + ); + max.value = max.value.copyWith( + tempo: value.max, + ); + }, + ), + RecommendationAttributeFields( + title: Text(context.l10n.mode), + values: ( + max: max.value.mode?.toDouble() ?? 0, + target: target.value.mode?.toDouble() ?? 0, + min: min.value.mode?.toDouble() ?? 0, ), - RecommendationAttributeFields( - title: Text(context.l10n.time_signature), - values: ( - max: max.value.timeSignature?.toDouble() ?? 0, - target: target.value.timeSignature?.toDouble() ?? 0, - min: min.value.timeSignature?.toDouble() ?? 0, - ), - onChanged: (value) { - target.value = target.value.copyWith( - timeSignature: value.target, - ); - min.value = min.value.copyWith( - timeSignature: value.min, - ); - max.value = max.value.copyWith( - timeSignature: value.max, - ); - }, + onChanged: (value) { + target.value = target.value.copyWith( + mode: value.target, + ); + min.value = min.value.copyWith( + mode: value.min, + ); + max.value = max.value.copyWith( + mode: value.max, + ); + }, + ), + RecommendationAttributeFields( + title: Text(context.l10n.time_signature), + values: ( + max: max.value.timeSignature?.toDouble() ?? 0, + target: target.value.timeSignature?.toDouble() ?? 0, + min: min.value.timeSignature?.toDouble() ?? 0, ), - const SizedBox(height: 20), - FilledButton.icon( - icon: const Icon(SpotubeIcons.magic), - label: Text(context.l10n.generate_playlist), + onChanged: (value) { + target.value = target.value.copyWith( + timeSignature: value.target, + ); + min.value = min.value.copyWith( + timeSignature: value.min, + ); + max.value = max.value.copyWith( + timeSignature: value.max, + ); + }, + ), + const Gap(20), + Center( + child: Button.primary( + leading: const Icon(SpotubeIcons.magic), onPressed: artists.value.isEmpty && tracks.value.isEmpty && genres.value.isEmpty @@ -643,12 +665,13 @@ class PlaylistGeneratorPage extends HookConsumerWidget { extra: routeState, ); }, + child: Text(context.l10n.generate), ), - ], - ), - ); - }), - ), + ), + ], + ), + ); + }), ), ), ), diff --git a/lib/pages/library/playlist_generate/playlist_generate_result.dart b/lib/pages/library/playlist_generate/playlist_generate_result.dart index f44cbee7..87d6fdc9 100644 --- a/lib/pages/library/playlist_generate/playlist_generate_result.dart +++ b/lib/pages/library/playlist_generate/playlist_generate_result.dart @@ -1,9 +1,10 @@ -import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/button/back_button.dart'; import 'package:spotube/modules/library/playlist_generate/simple_track_tile.dart'; import 'package:spotube/modules/playlist/playlist_create_dialog.dart'; import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart'; @@ -27,7 +28,7 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final router = GoRouter.of(context); - final scaffoldMessenger = ScaffoldMessenger.of(context); + final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final generatedPlaylist = ref.watch(generatePlaylistProvider(state)); @@ -48,8 +49,10 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { (generatedPlaylist.asData?.value.length ?? 0); return Scaffold( - appBar: const TitleBar(leading: [BackButton()]), - body: generatedPlaylist.isLoading + headers: const [ + TitleBar(leading: [BackButton()]) + ], + child: generatedPlaylist.isLoading ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -74,9 +77,8 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { ), shrinkWrap: true, children: [ - FilledButton.tonalIcon( - icon: const Icon(SpotubeIcons.play), - label: Text(context.l10n.play), + Button.primary( + leading: const Icon(SpotubeIcons.play), onPressed: selectedTracks.value.isEmpty ? null : () async { @@ -90,10 +92,10 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { autoPlay: true, ); }, + child: Text(context.l10n.play), ), - FilledButton.tonalIcon( - icon: const Icon(SpotubeIcons.queueAdd), - label: Text(context.l10n.add_to_queue), + Button.primary( + leading: const Icon(SpotubeIcons.queueAdd), onPressed: selectedTracks.value.isEmpty ? null : () async { @@ -103,21 +105,25 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { ), ); if (context.mounted) { - scaffoldMessenger.showSnackBar( - SnackBar( - content: Text( - context.l10n.add_count_to_queue( - selectedTracks.value.length, + showToast( + context: context, + location: ToastLocation.topRight, + builder: (context, overlay) { + return SurfaceCard( + child: Text( + context.l10n.add_count_to_queue( + selectedTracks.value.length, + ), ), - ), - ), + ); + }, ); } }, + child: Text(context.l10n.add_to_queue), ), - FilledButton.tonalIcon( - icon: const Icon(SpotubeIcons.addFilled), - label: Text(context.l10n.create_a_playlist), + Button.primary( + leading: const Icon(SpotubeIcons.addFilled), onPressed: selectedTracks.value.isEmpty ? null : () async { @@ -138,10 +144,10 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { ); } }, + child: Text(context.l10n.create_a_playlist), ), - FilledButton.tonalIcon( - icon: const Icon(SpotubeIcons.playlistAdd), - label: Text(context.l10n.add_to_playlist), + Button.primary( + leading: const Icon(SpotubeIcons.playlistAdd), onPressed: selectedTracks.value.isEmpty ? null : () async { @@ -161,17 +167,22 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { ); if (context.mounted && hasAdded == true) { - scaffoldMessenger.showSnackBar( - SnackBar( - content: Text( - context.l10n.add_count_to_playlist( - selectedTracks.value.length, + showToast( + context: context, + location: ToastLocation.topRight, + builder: (context, overlay) { + return SurfaceCard( + child: Text( + context.l10n.add_count_to_playlist( + selectedTracks.value.length, + ), ), - ), - ), + ); + }, ); } }, + child: Text(context.l10n.add_to_playlist), ) ], ), @@ -185,7 +196,7 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { selectedTracks.value.length, ), ), - ElevatedButton.icon( + Button.secondary( onPressed: () { if (isAllTrackSelected) { selectedTracks.value = []; @@ -197,8 +208,8 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { []; } }, - icon: const Icon(SpotubeIcons.selectionCheck), - label: Text( + leading: const Icon(SpotubeIcons.selectionCheck), + child: Text( isAllTrackSelected ? context.l10n.deselect_all : context.l10n.select_all, @@ -207,32 +218,44 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { ], ), const SizedBox(height: 8), - Card( - margin: const EdgeInsets.all(0), - child: SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - for (final track - in generatedPlaylist.asData?.value ?? []) - CheckboxListTile( - value: selectedTracks.value.contains(track.id), - onChanged: (value) { - if (value == true) { - selectedTracks.value.add(track.id!); - } else { - selectedTracks.value.remove(track.id); - } - selectedTracks.value = - selectedTracks.value.toList(); - }, - controlAffinity: ListTileControlAffinity.leading, - contentPadding: EdgeInsets.zero, - dense: true, - title: SimpleTrackTile(track: track), - ) - ], - ), + SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (final track + in generatedPlaylist.asData?.value ?? []) + Row( + spacing: 5, + children: [ + Checkbox( + state: selectedTracks.value.contains(track.id) + ? CheckboxState.checked + : CheckboxState.unchecked, + onChanged: (value) { + if (value == CheckboxState.checked) { + selectedTracks.value.add(track.id!); + } else { + selectedTracks.value.remove(track.id); + } + selectedTracks.value = + selectedTracks.value.toList(); + }, + ), + Expanded( + child: GestureDetector( + onTap: () { + selectedTracks.value.contains(track.id) + ? selectedTracks.value.remove(track.id) + : selectedTracks.value.add(track.id!); + selectedTracks.value = + selectedTracks.value.toList(); + }, + child: SimpleTrackTile(track: track), + ), + ), + ], + ) + ], ), ), ], diff --git a/untranslated_messages.json b/untranslated_messages.json index fae95f00..a31de6a2 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,6 +1,8 @@ { "ar": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -10,7 +12,9 @@ ], "bn": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -20,7 +24,9 @@ ], "ca": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -30,7 +36,9 @@ ], "cs": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -40,7 +48,9 @@ ], "de": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -50,7 +60,9 @@ ], "es": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -60,7 +72,9 @@ ], "eu": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -70,7 +84,9 @@ ], "fa": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -80,7 +96,9 @@ ], "fi": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -90,7 +108,9 @@ ], "fr": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -100,7 +120,9 @@ ], "hi": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -110,7 +132,9 @@ ], "id": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -120,7 +144,9 @@ ], "it": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -130,7 +156,9 @@ ], "ja": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -140,7 +168,9 @@ ], "ka": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -150,7 +180,9 @@ ], "ko": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -160,7 +192,9 @@ ], "ne": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -170,7 +204,9 @@ ], "nl": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -180,7 +216,9 @@ ], "pl": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -190,7 +228,9 @@ ], "pt": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -200,7 +240,9 @@ ], "ru": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -210,7 +252,9 @@ ], "th": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -220,7 +264,9 @@ ], "tr": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -230,7 +276,9 @@ ], "uk": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -240,7 +288,9 @@ ], "vi": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist", @@ -250,7 +300,9 @@ ], "zh": [ + "playlist", "no_loop", + "generate", "undo", "download_all", "add_all_to_playlist",