refactor: generate playlist to shadcn

This commit is contained in:
Kingkor Roy Tirtho 2025-01-05 16:16:57 +06:00
parent b8f2495acb
commit dd0bb01af5
11 changed files with 868 additions and 761 deletions

View File

@ -31,8 +31,7 @@ class ButtonTile extends StatelessWidget {
onPressed: onPressed, onPressed: onPressed,
style: style.copyWith( style: style.copyWith(
decoration: (context, states, value) { decoration: (context, states, value) {
final decoration = ButtonVariance.outline.decoration(context, states) final decoration = style.decoration(context, states) as BoxDecoration;
as BoxDecoration;
if (selected && style == ButtonVariance.outline) { if (selected && style == ButtonVariance.outline) {
return decoration.copyWith( return decoration.copyWith(
@ -47,7 +46,7 @@ class ButtonTile extends StatelessWidget {
return decoration; return decoration;
}, },
iconTheme: (context, states, value) { iconTheme: (context, states, value) {
final iconTheme = ButtonVariance.outline.iconTheme(context, states); final iconTheme = style.iconTheme(context, states);
if (selected && style == ButtonVariance.outline) { if (selected && style == ButtonVariance.outline) {
return iconTheme.copyWith( return iconTheme.copyWith(
@ -58,7 +57,7 @@ class ButtonTile extends StatelessWidget {
return iconTheme; return iconTheme;
}, },
textStyle: (context, states, value) { textStyle: (context, states, value) {
final textStyle = ButtonVariance.outline.textStyle(context, states); final textStyle = style.textStyle(context, states);
if (selected && style == ButtonVariance.outline) { if (selected && style == ButtonVariance.outline) {
return textStyle.copyWith( return textStyle.copyWith(

View File

@ -22,7 +22,7 @@
"filter_playlists": "Filter your playlists...", "filter_playlists": "Filter your playlists...",
"liked_tracks": "Liked Tracks", "liked_tracks": "Liked Tracks",
"liked_tracks_description": "All your liked tracks", "liked_tracks_description": "All your liked tracks",
"create_playlist": "Create Playlist", "playlist": "Playlist",
"create_a_playlist": "Create a playlist", "create_a_playlist": "Create a playlist",
"update_playlist": "Update playlist", "update_playlist": "Update playlist",
"create": "Create", "create": "Create",
@ -194,7 +194,7 @@
"invidious_instance": "Invidious Server Instance", "invidious_instance": "Invidious Server Instance",
"invidious_description": "The Invidious server instance to use for track matching", "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", "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", "track_exists": "Track {track} already exists",
"replace_downloaded_tracks": "Replace all downloaded tracks", "replace_downloaded_tracks": "Replace all downloaded tracks",
"skip_download_tracks": "Skip downloading all downloaded tracks", "skip_download_tracks": "Skip downloading all downloaded tracks",

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart'; import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
@ -29,23 +29,21 @@ class RecommendationAttributeDials extends HookWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final animation = useAnimationController( final labelStyle = Theme.of(context).typography.small.copyWith(
duration: const Duration(milliseconds: 300),
);
final labelStyle = Theme.of(context).textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
); );
final minSlider = Row( final minSlider = Row(
spacing: 5,
children: [ children: [
Text(context.l10n.min, style: labelStyle), Text(context.l10n.min, style: labelStyle),
Expanded( Expanded(
child: Slider( child: Slider(
value: values.min / base, value: SliderValue.single(values.min / base),
min: 0, min: 0,
max: 1, max: 1,
onChanged: (value) => onChanged(( onChanged: (value) => onChanged((
min: value * base, min: value.value * base,
target: values.target, target: values.target,
max: values.max, max: values.max,
)), )),
@ -55,16 +53,17 @@ class RecommendationAttributeDials extends HookWidget {
); );
final targetSlider = Row( final targetSlider = Row(
spacing: 5,
children: [ children: [
Text(context.l10n.target, style: labelStyle), Text(context.l10n.target, style: labelStyle),
Expanded( Expanded(
child: Slider( child: Slider(
value: values.target / base, value: SliderValue.single(values.target / base),
min: 0, min: 0,
max: 1, max: 1,
onChanged: (value) => onChanged(( onChanged: (value) => onChanged((
min: values.min, min: values.min,
target: value * base, target: value.value * base,
max: values.max, max: values.max,
)), )),
), ),
@ -73,109 +72,111 @@ class RecommendationAttributeDials extends HookWidget {
); );
final maxSlider = Row( final maxSlider = Row(
spacing: 5,
children: [ children: [
Text(context.l10n.max, style: labelStyle), Text(context.l10n.max, style: labelStyle),
Expanded( Expanded(
child: Slider( child: Slider(
value: values.max / base, value: SliderValue.single(values.max / base),
min: 0, min: 0,
max: 1, max: 1,
onChanged: (value) => onChanged(( onChanged: (value) => onChanged((
min: values.min, min: values.min,
target: values.target, target: values.target,
max: value * base, max: value.value * base,
)), )),
), ),
), ),
], ],
); );
return LayoutBuilder(builder: (context, constrain) { void onSelected(int index) {
return Card( RecommendationAttribute newValues = zeroValues;
child: ExpansionTile( switch (index) {
title: DefaultTextStyle( case 0:
style: Theme.of(context).textTheme.titleSmall!, newValues = lowValues(base);
child: title, break;
), case 1:
shape: const Border(), newValues = moderateValues(base);
leading: AnimatedBuilder( break;
animation: animation, case 2:
builder: (context, child) { newValues = highValues(base);
return Transform.rotate( break;
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;
}
if (newValues == values) { if (newValues == values) {
onChanged(zeroValues); onChanged(zeroValues);
} else { } else {
onChanged(newValues); 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: [ children: [
Text(context.l10n.low), if (constrain.mdAndUp)
Text(" ${context.l10n.moderate} "), Row(
Text(context.l10n.high), 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,
],
),
),
],
),
); );
}); });
} }

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/modules/library/playlist_generate/recommendation_attribute_dials.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
@ -21,13 +21,6 @@ class RecommendationAttributeFields extends HookWidget {
@override @override
Widget build(BuildContext context) { 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 minController = useTextEditingController(text: values.min.toString());
final targetController = final targetController =
useTextEditingController(text: values.target.toString()); useTextEditingController(text: values.target.toString());
@ -53,126 +46,133 @@ class RecommendationAttributeFields extends HookWidget {
}; };
}, [values]); }, [values]);
final minField = TextField( final minField = Column(
controller: minController, mainAxisSize: MainAxisSize.min,
decoration: InputDecoration( crossAxisAlignment: CrossAxisAlignment.start,
labelText: context.l10n.min, spacing: 5,
isDense: true, children: [
), Text(context.l10n.min).semiBold(),
keyboardType: const TextInputType.numberWithOptions( NumberInput(
decimal: false, controller: minController,
signed: true, allowDecimals: false,
), ),
],
); );
final targetField = TextField( final targetField = Column(
controller: targetController, mainAxisSize: MainAxisSize.min,
decoration: InputDecoration( crossAxisAlignment: CrossAxisAlignment.start,
labelText: context.l10n.target, spacing: 5,
isDense: true, children: [
), Text(context.l10n.target).semiBold(),
keyboardType: const TextInputType.numberWithOptions( NumberInput(
decimal: false, controller: targetController,
signed: true, allowDecimals: false,
), ),
],
); );
final maxField = TextField( final maxField = Column(
controller: maxController, mainAxisSize: MainAxisSize.min,
decoration: InputDecoration( crossAxisAlignment: CrossAxisAlignment.start,
labelText: context.l10n.max, spacing: 5,
isDense: true, children: [
), Text(context.l10n.max).semiBold(),
keyboardType: const TextInputType.numberWithOptions( NumberInput(
decimal: false, controller: maxController,
signed: true, allowDecimals: false,
), ),
],
); );
return LayoutBuilder(builder: (context, constrain) { void onSelected(int index) {
return Card( RecommendationAttribute newValues = presets!.values.elementAt(index);
child: ExpansionTile( if (newValues == values) {
title: DefaultTextStyle( onChanged(zeroValues);
style: Theme.of(context).textTheme.titleSmall!, minController.text = zeroValues.min.toString();
child: title, targetController.text = zeroValues.target.toString();
), maxController.text = zeroValues.max.toString();
shape: const Border(), } else {
leading: AnimatedBuilder( onChanged(newValues);
animation: animation, minController.text = newValues.min.toString();
builder: (context, child) { targetController.text = newValues.target.toString();
return Transform.rotate( maxController.text = newValues.max.toString();
angle: (animation.value * 3.14) / 2, }
child: child, }
);
}, return LayoutBuilder(builder: (context, constraints) {
child: const Icon(Icons.chevron_right), return Accordion(
), items: [
trailing: presets == null AccordionItem(
? const SizedBox.shrink() trigger: AccordionTrigger(
: Padding( child: SizedBox(
padding: const EdgeInsets.symmetric(vertical: 8.0), width: double.infinity,
child: ToggleButtons( child: Basic(
borderRadius: BorderRadius.circular(8), title: title.semiBold(),
textStyle: labelStyle, trailing: presets == null
isSelected: presets!.values ? const SizedBox.shrink()
.map((value) => value == values) : Padding(
.toList(), padding: const EdgeInsets.symmetric(vertical: 8.0),
onPressed: (index) { child: Row(
RecommendationAttribute newValues = spacing: 5,
presets!.values.elementAt(index); children: [
if (newValues == values) { for (final presetEntry in presets?.entries
onChanged(zeroValues); .toList() ??
minController.text = zeroValues.min.toString(); <MapEntry<String, RecommendationAttribute>>[])
targetController.text = zeroValues.target.toString(); Toggle(
maxController.text = zeroValues.max.toString(); value: presetEntry.value == values,
} else { style: const ButtonStyle.outline(
onChanged(newValues); size: ButtonSize.small,
minController.text = newValues.min.toString(); ),
targetController.text = newValues.target.toString(); onChanged: (value) {
maxController.text = newValues.max.toString(); onSelected(
} presets!.entries.toList().indexWhere(
}, (s) => s.key == presetEntry.key),
children: presets!.keys.map((key) => Text(key)).toList(), );
), },
), child: Text(presetEntry.key),
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,
],
), ),
), ),
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),
],
),
),
],
); );
}); });
} }

View File

@ -1,8 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart' show Autocomplete;
import 'package:flutter_hooks/flutter_hooks.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/constrains.dart';
enum SelectedItemDisplayType { enum SelectedItemDisplayType {
@ -20,10 +21,13 @@ class SeedsMultiAutocomplete<T extends Object> extends HookWidget {
final Widget Function(T option) selectedSeedBuilder; final Widget Function(T option) selectedSeedBuilder;
final String Function(T option) displayStringForOption; final String Function(T option) displayStringForOption;
final InputDecoration? inputDecoration;
final bool enabled; final bool enabled;
final SelectedItemDisplayType selectedItemDisplayType; final SelectedItemDisplayType selectedItemDisplayType;
final Widget? placeholder;
final Widget? leading;
final Widget? trailing;
final Widget? label;
const SeedsMultiAutocomplete({ const SeedsMultiAutocomplete({
super.key, super.key,
@ -32,9 +36,12 @@ class SeedsMultiAutocomplete<T extends Object> extends HookWidget {
required this.autocompleteOptionBuilder, required this.autocompleteOptionBuilder,
required this.displayStringForOption, required this.displayStringForOption,
required this.selectedSeedBuilder, required this.selectedSeedBuilder,
this.inputDecoration,
this.enabled = true, this.enabled = true,
this.selectedItemDisplayType = SelectedItemDisplayType.wrap, this.selectedItemDisplayType = SelectedItemDisplayType.wrap,
this.placeholder,
this.leading,
this.trailing,
this.label,
}); });
@override @override
@ -61,6 +68,10 @@ class SeedsMultiAutocomplete<T extends Object> extends HookWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (label != null) ...[
label!.semiBold(),
const Gap(8),
],
LayoutBuilder(builder: (context, constrains) { LayoutBuilder(builder: (context, constrains) {
return Container( return Container(
key: containerKey.value, key: containerKey.value,
@ -101,13 +112,15 @@ class SeedsMultiAutocomplete<T extends Object> extends HookWidget {
focusNode, focusNode,
onFieldSubmitted, onFieldSubmitted,
) { ) {
return TextFormField( return TextField(
controller: seedController, controller: seedController,
onChanged: (value) => textEditingController.text = value, onChanged: (value) => textEditingController.text = value,
focusNode: focusNode, focusNode: focusNode,
onFieldSubmitted: (_) => onFieldSubmitted(), onSubmitted: (_) => onFieldSubmitted(),
enabled: enabled, enabled: enabled,
decoration: inputDecoration, leading: leading,
trailing: trailing,
placeholder: placeholder,
); );
}, },
), ),
@ -120,22 +133,27 @@ class SeedsMultiAutocomplete<T extends Object> extends HookWidget {
runSpacing: 4, runSpacing: 4,
children: seeds.value.map(selectedSeedBuilder).toList(), children: seeds.value.map(selectedSeedBuilder).toList(),
), ),
SelectedItemDisplayType.list => Card( SelectedItemDisplayType.list => AnimatedSwitcher(
margin: EdgeInsets.zero, duration: const Duration(milliseconds: 300),
child: Column( child: seeds.value.isEmpty
children: [ ? const SizedBox.shrink()
for (final seed in seeds.value) ...[ : Card(
selectedSeedBuilder(seed), child: Column(
if (seeds.value.length > 1 && seed != seeds.value.last) children: [
Divider( for (final seed in seeds.value) ...[
color: theme.colorScheme.primaryContainer, selectedSeedBuilder(seed),
height: 1, if (seeds.value.length > 1 &&
indent: 12, seed != seeds.value.last)
endIndent: 12, Divider(
color: theme.colorScheme.secondary,
height: 1,
indent: 12,
endIndent: 12,
),
],
],
), ),
], ),
],
),
), ),
}, },
], ],

View File

@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
class SimpleTrackTile extends HookWidget { class SimpleTrackTile extends HookWidget {
@ -17,7 +18,7 @@ class SimpleTrackTile extends HookWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ButtonTile(
leading: ClipRRect( leading: ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: UniversalImage( child: UniversalImage(
@ -28,18 +29,17 @@ class SimpleTrackTile extends HookWidget {
width: 40, width: 40,
), ),
), ),
horizontalTitleGap: 10,
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
title: Text(track.name!), title: Text(track.name!),
trailing: onDelete == null trailing: onDelete == null
? null ? null
: IconButton( : IconButton.ghost(
icon: const Icon(SpotubeIcons.close), icon: const Icon(SpotubeIcons.close),
onPressed: onDelete, onPressed: onDelete,
), ),
subtitle: Text( subtitle: Text(
track.artists?.map((e) => e.name).join(", ") ?? track.album?.name ?? "", track.artists?.map((e) => e.name).join(", ") ?? track.album?.name ?? "",
), ),
style: ButtonVariance.ghost,
); );
} }
} }

View File

@ -110,7 +110,7 @@ class UserPlaylists extends HookConsumerWidget {
const Gap(10), const Gap(10),
Button.primary( Button.primary(
leading: const Icon(SpotubeIcons.magic), leading: const Icon(SpotubeIcons.magic),
child: Text(context.l10n.generate_playlist), child: Text(context.l10n.generate),
onPressed: () { onPressed: () {
ServiceUtils.pushNamed( ServiceUtils.pushNamed(
context, context,

View File

@ -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/checkbox_form_field.dart';
import 'package:spotube/components/form/text_form_field.dart'; import 'package:spotube/components/form/text_form_field.dart';
import 'package:spotube/components/image/universal_image.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/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
@ -267,19 +266,11 @@ class PlaylistCreateDialogButton extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final mediaQuery = MediaQuery.of(context);
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
if (mediaQuery.smAndDown) {
return IconButton.secondary(
icon: const Icon(SpotubeIcons.addFilled),
onPressed: () => showPlaylistDialog(context, spotify),
);
}
return Button.secondary( return Button.secondary(
leading: const Icon(SpotubeIcons.addFilled), leading: const Icon(SpotubeIcons.addFilled),
child: Text(context.l10n.create_playlist), child: Text(context.l10n.playlist),
onPressed: () => showPlaylistDialog(context, spotify), onPressed: () => showPlaylistDialog(context, spotify),
); );
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.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/library/playlist_generate/simple_track_tile.dart';
import 'package:spotube/modules/playlist/playlist_create_dialog.dart'; import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart'; import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart';
@ -27,7 +28,7 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final router = GoRouter.of(context); final router = GoRouter.of(context);
final scaffoldMessenger = ScaffoldMessenger.of(context);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final generatedPlaylist = ref.watch(generatePlaylistProvider(state)); final generatedPlaylist = ref.watch(generatePlaylistProvider(state));
@ -48,8 +49,10 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
(generatedPlaylist.asData?.value.length ?? 0); (generatedPlaylist.asData?.value.length ?? 0);
return Scaffold( return Scaffold(
appBar: const TitleBar(leading: [BackButton()]), headers: const [
body: generatedPlaylist.isLoading TitleBar(leading: [BackButton()])
],
child: generatedPlaylist.isLoading
? Center( ? Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -74,9 +77,8 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
), ),
shrinkWrap: true, shrinkWrap: true,
children: [ children: [
FilledButton.tonalIcon( Button.primary(
icon: const Icon(SpotubeIcons.play), leading: const Icon(SpotubeIcons.play),
label: Text(context.l10n.play),
onPressed: selectedTracks.value.isEmpty onPressed: selectedTracks.value.isEmpty
? null ? null
: () async { : () async {
@ -90,10 +92,10 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
autoPlay: true, autoPlay: true,
); );
}, },
child: Text(context.l10n.play),
), ),
FilledButton.tonalIcon( Button.primary(
icon: const Icon(SpotubeIcons.queueAdd), leading: const Icon(SpotubeIcons.queueAdd),
label: Text(context.l10n.add_to_queue),
onPressed: selectedTracks.value.isEmpty onPressed: selectedTracks.value.isEmpty
? null ? null
: () async { : () async {
@ -103,21 +105,25 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
), ),
); );
if (context.mounted) { if (context.mounted) {
scaffoldMessenger.showSnackBar( showToast(
SnackBar( context: context,
content: Text( location: ToastLocation.topRight,
context.l10n.add_count_to_queue( builder: (context, overlay) {
selectedTracks.value.length, return SurfaceCard(
child: Text(
context.l10n.add_count_to_queue(
selectedTracks.value.length,
),
), ),
), );
), },
); );
} }
}, },
child: Text(context.l10n.add_to_queue),
), ),
FilledButton.tonalIcon( Button.primary(
icon: const Icon(SpotubeIcons.addFilled), leading: const Icon(SpotubeIcons.addFilled),
label: Text(context.l10n.create_a_playlist),
onPressed: selectedTracks.value.isEmpty onPressed: selectedTracks.value.isEmpty
? null ? null
: () async { : () async {
@ -138,10 +144,10 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
); );
} }
}, },
child: Text(context.l10n.create_a_playlist),
), ),
FilledButton.tonalIcon( Button.primary(
icon: const Icon(SpotubeIcons.playlistAdd), leading: const Icon(SpotubeIcons.playlistAdd),
label: Text(context.l10n.add_to_playlist),
onPressed: selectedTracks.value.isEmpty onPressed: selectedTracks.value.isEmpty
? null ? null
: () async { : () async {
@ -161,17 +167,22 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
); );
if (context.mounted && hasAdded == true) { if (context.mounted && hasAdded == true) {
scaffoldMessenger.showSnackBar( showToast(
SnackBar( context: context,
content: Text( location: ToastLocation.topRight,
context.l10n.add_count_to_playlist( builder: (context, overlay) {
selectedTracks.value.length, 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, selectedTracks.value.length,
), ),
), ),
ElevatedButton.icon( Button.secondary(
onPressed: () { onPressed: () {
if (isAllTrackSelected) { if (isAllTrackSelected) {
selectedTracks.value = []; selectedTracks.value = [];
@ -197,8 +208,8 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
[]; [];
} }
}, },
icon: const Icon(SpotubeIcons.selectionCheck), leading: const Icon(SpotubeIcons.selectionCheck),
label: Text( child: Text(
isAllTrackSelected isAllTrackSelected
? context.l10n.deselect_all ? context.l10n.deselect_all
: context.l10n.select_all, : context.l10n.select_all,
@ -207,32 +218,44 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Card( SafeArea(
margin: const EdgeInsets.all(0), child: Column(
child: SafeArea( mainAxisSize: MainAxisSize.min,
child: Column( children: [
mainAxisSize: MainAxisSize.min, for (final track
children: [ in generatedPlaylist.asData?.value ?? [])
for (final track Row(
in generatedPlaylist.asData?.value ?? []) spacing: 5,
CheckboxListTile( children: [
value: selectedTracks.value.contains(track.id), Checkbox(
onChanged: (value) { state: selectedTracks.value.contains(track.id)
if (value == true) { ? CheckboxState.checked
selectedTracks.value.add(track.id!); : CheckboxState.unchecked,
} else { onChanged: (value) {
selectedTracks.value.remove(track.id); if (value == CheckboxState.checked) {
} selectedTracks.value.add(track.id!);
selectedTracks.value = } else {
selectedTracks.value.toList(); selectedTracks.value.remove(track.id);
}, }
controlAffinity: ListTileControlAffinity.leading, selectedTracks.value =
contentPadding: EdgeInsets.zero, selectedTracks.value.toList();
dense: true, },
title: SimpleTrackTile(track: track), ),
) 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),
),
),
],
)
],
), ),
), ),
], ],

View File

@ -1,6 +1,8 @@
{ {
"ar": [ "ar": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -10,7 +12,9 @@
], ],
"bn": [ "bn": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -20,7 +24,9 @@
], ],
"ca": [ "ca": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -30,7 +36,9 @@
], ],
"cs": [ "cs": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -40,7 +48,9 @@
], ],
"de": [ "de": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -50,7 +60,9 @@
], ],
"es": [ "es": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -60,7 +72,9 @@
], ],
"eu": [ "eu": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -70,7 +84,9 @@
], ],
"fa": [ "fa": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -80,7 +96,9 @@
], ],
"fi": [ "fi": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -90,7 +108,9 @@
], ],
"fr": [ "fr": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -100,7 +120,9 @@
], ],
"hi": [ "hi": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -110,7 +132,9 @@
], ],
"id": [ "id": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -120,7 +144,9 @@
], ],
"it": [ "it": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -130,7 +156,9 @@
], ],
"ja": [ "ja": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -140,7 +168,9 @@
], ],
"ka": [ "ka": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -150,7 +180,9 @@
], ],
"ko": [ "ko": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -160,7 +192,9 @@
], ],
"ne": [ "ne": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -170,7 +204,9 @@
], ],
"nl": [ "nl": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -180,7 +216,9 @@
], ],
"pl": [ "pl": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -190,7 +228,9 @@
], ],
"pt": [ "pt": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -200,7 +240,9 @@
], ],
"ru": [ "ru": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -210,7 +252,9 @@
], ],
"th": [ "th": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -220,7 +264,9 @@
], ],
"tr": [ "tr": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -230,7 +276,9 @@
], ],
"uk": [ "uk": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -240,7 +288,9 @@
], ],
"vi": [ "vi": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",
@ -250,7 +300,9 @@
], ],
"zh": [ "zh": [
"playlist",
"no_loop", "no_loop",
"generate",
"undo", "undo",
"download_all", "download_all",
"add_all_to_playlist", "add_all_to_playlist",