mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
refactor: generate playlist to shadcn
This commit is contained in:
parent
b8f2495acb
commit
dd0bb01af5
@ -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(
|
||||||
|
@ -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",
|
||||||
|
@ -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,52 +72,25 @@ 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(
|
|
||||||
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;
|
RecommendationAttribute newValues = zeroValues;
|
||||||
switch (index) {
|
switch (index) {
|
||||||
case 0:
|
case 0:
|
||||||
@ -137,21 +109,48 @@ class RecommendationAttributeDials extends HookWidget {
|
|||||||
} 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: [
|
children: [
|
||||||
Text(context.l10n.low),
|
Toggle(
|
||||||
Text(" ${context.l10n.moderate} "),
|
value: values == lowValues(base),
|
||||||
Text(context.l10n.high),
|
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),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onExpansionChanged: (value) {
|
),
|
||||||
if (value) {
|
),
|
||||||
animation.forward();
|
content: Column(
|
||||||
} else {
|
mainAxisSize: MainAxisSize.min,
|
||||||
animation.reverse();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
children: [
|
children: [
|
||||||
if (constrain.mdAndUp)
|
if (constrain.mdAndUp)
|
||||||
Row(
|
Row(
|
||||||
@ -176,6 +175,8 @@ class RecommendationAttributeDials extends HookWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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,73 +46,47 @@ class RecommendationAttributeFields extends HookWidget {
|
|||||||
};
|
};
|
||||||
}, [values]);
|
}, [values]);
|
||||||
|
|
||||||
final minField = TextField(
|
final minField = Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 5,
|
||||||
|
children: [
|
||||||
|
Text(context.l10n.min).semiBold(),
|
||||||
|
NumberInput(
|
||||||
controller: minController,
|
controller: minController,
|
||||||
decoration: InputDecoration(
|
allowDecimals: false,
|
||||||
labelText: context.l10n.min,
|
|
||||||
isDense: true,
|
|
||||||
),
|
|
||||||
keyboardType: const TextInputType.numberWithOptions(
|
|
||||||
decimal: false,
|
|
||||||
signed: true,
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
final targetField = TextField(
|
final targetField = Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 5,
|
||||||
|
children: [
|
||||||
|
Text(context.l10n.target).semiBold(),
|
||||||
|
NumberInput(
|
||||||
controller: targetController,
|
controller: targetController,
|
||||||
decoration: InputDecoration(
|
allowDecimals: false,
|
||||||
labelText: context.l10n.target,
|
|
||||||
isDense: true,
|
|
||||||
),
|
|
||||||
keyboardType: const TextInputType.numberWithOptions(
|
|
||||||
decimal: false,
|
|
||||||
signed: true,
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
final maxField = TextField(
|
final maxField = Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 5,
|
||||||
|
children: [
|
||||||
|
Text(context.l10n.max).semiBold(),
|
||||||
|
NumberInput(
|
||||||
controller: maxController,
|
controller: maxController,
|
||||||
decoration: InputDecoration(
|
allowDecimals: false,
|
||||||
labelText: context.l10n.max,
|
|
||||||
isDense: true,
|
|
||||||
),
|
|
||||||
keyboardType: const TextInputType.numberWithOptions(
|
|
||||||
decimal: false,
|
|
||||||
signed: true,
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return LayoutBuilder(builder: (context, constrain) {
|
void onSelected(int index) {
|
||||||
return Card(
|
RecommendationAttribute newValues = presets!.values.elementAt(index);
|
||||||
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) {
|
if (newValues == values) {
|
||||||
onChanged(zeroValues);
|
onChanged(zeroValues);
|
||||||
minController.text = zeroValues.min.toString();
|
minController.text = zeroValues.min.toString();
|
||||||
@ -131,20 +98,51 @@ class RecommendationAttributeFields extends HookWidget {
|
|||||||
targetController.text = newValues.target.toString();
|
targetController.text = newValues.target.toString();
|
||||||
maxController.text = newValues.max.toString();
|
maxController.text = newValues.max.toString();
|
||||||
}
|
}
|
||||||
},
|
|
||||||
children: presets!.keys.map((key) => Text(key)).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onExpansionChanged: (value) {
|
|
||||||
if (value) {
|
|
||||||
animation.forward();
|
|
||||||
} else {
|
|
||||||
animation.reverse();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() ??
|
||||||
|
<MapEntry<String, RecommendationAttribute>>[])
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
if (constrain.mdAndUp)
|
if (constraints.mdAndUp)
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
@ -173,6 +171,8 @@ class RecommendationAttributeFields extends HookWidget {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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,15 +133,19 @@ 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: seeds.value.isEmpty
|
||||||
|
? const SizedBox.shrink()
|
||||||
|
: Card(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
for (final seed in seeds.value) ...[
|
for (final seed in seeds.value) ...[
|
||||||
selectedSeedBuilder(seed),
|
selectedSeedBuilder(seed),
|
||||||
if (seeds.value.length > 1 && seed != seeds.value.last)
|
if (seeds.value.length > 1 &&
|
||||||
|
seed != seeds.value.last)
|
||||||
Divider(
|
Divider(
|
||||||
color: theme.colorScheme.primaryContainer,
|
color: theme.colorScheme.secondary,
|
||||||
height: 1,
|
height: 1,
|
||||||
indent: 12,
|
indent: 12,
|
||||||
endIndent: 12,
|
endIndent: 12,
|
||||||
@ -137,6 +154,7 @@ class SeedsMultiAutocomplete<T extends Object> extends HookWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
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/spotify_markets.dart';
|
import 'package:spotube/collections/spotify_markets.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.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_dials.dart';
|
||||||
import 'package:spotube/modules/library/playlist_generate/recommendation_attribute_fields.dart';
|
import 'package:spotube/modules/library/playlist_generate/recommendation_attribute_fields.dart';
|
||||||
import 'package:spotube/modules/library/playlist_generate/seeds_multi_autocomplete.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 spotify = ref.watch(spotifyProvider);
|
||||||
|
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final textTheme = theme.textTheme;
|
final typography = theme.typography;
|
||||||
final preferences = ref.watch(userPreferencesProvider);
|
final preferences = ref.watch(userPreferencesProvider);
|
||||||
|
|
||||||
final genresCollection = ref.watch(categoryGenresProvider);
|
final genresCollection = ref.watch(categoryGenresProvider);
|
||||||
@ -59,14 +62,11 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
final artistAutoComplete = SeedsMultiAutocomplete<Artist>(
|
final artistAutoComplete = SeedsMultiAutocomplete<Artist>(
|
||||||
seeds: artists,
|
seeds: artists,
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
inputDecoration: InputDecoration(
|
label: Text(context.l10n.artists),
|
||||||
labelText: context.l10n.artists,
|
placeholder: Text(context.l10n.select_up_to_count_type(
|
||||||
labelStyle: textTheme.titleMedium,
|
|
||||||
helperText: context.l10n.select_up_to_count_type(
|
|
||||||
leftSeedCount,
|
leftSeedCount,
|
||||||
context.l10n.artists,
|
context.l10n.artists,
|
||||||
),
|
)),
|
||||||
),
|
|
||||||
fetchSeeds: (textEditingValue) => spotify.search
|
fetchSeeds: (textEditingValue) => spotify.search
|
||||||
.get(
|
.get(
|
||||||
textEditingValue.text,
|
textEditingValue.text,
|
||||||
@ -83,15 +83,15 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
autocompleteOptionBuilder: (option, onSelected) => ListTile(
|
autocompleteOptionBuilder: (option, onSelected) => ButtonTile(
|
||||||
leading: CircleAvatar(
|
leading: Avatar(
|
||||||
backgroundImage: UniversalImage.imageProvider(
|
initials: "O",
|
||||||
|
provider: UniversalImage.imageProvider(
|
||||||
option.images.asUrlString(
|
option.images.asUrlString(
|
||||||
placeholder: ImagePlaceholder.artist,
|
placeholder: ImagePlaceholder.artist,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
horizontalTitleGap: 20,
|
|
||||||
title: Text(option.name!),
|
title: Text(option.name!),
|
||||||
subtitle: option.genres?.isNotEmpty != true
|
subtitle: option.genres?.isNotEmpty != true
|
||||||
? null
|
? null
|
||||||
@ -101,49 +101,48 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
children: option.genres!.mapIndexed(
|
children: option.genres!.mapIndexed(
|
||||||
(index, genre) {
|
(index, genre) {
|
||||||
return Chip(
|
return Chip(
|
||||||
label: Text(genre),
|
style: ButtonVariance.secondary,
|
||||||
labelStyle: textTheme.bodySmall?.copyWith(
|
child: Text(genre),
|
||||||
color: theme.colorScheme.secondary,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
side: BorderSide.none,
|
|
||||||
backgroundColor: theme.colorScheme.secondaryContainer,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
).toList(),
|
).toList(),
|
||||||
),
|
),
|
||||||
onTap: () => onSelected(option),
|
onPressed: () => onSelected(option),
|
||||||
|
style: ButtonVariance.ghost,
|
||||||
),
|
),
|
||||||
displayStringForOption: (option) => option.name!,
|
displayStringForOption: (option) => option.name!,
|
||||||
selectedSeedBuilder: (artist) => Chip(
|
selectedSeedBuilder: (artist) => OutlineBadge(
|
||||||
avatar: CircleAvatar(
|
leading: Avatar(
|
||||||
backgroundImage: UniversalImage.imageProvider(
|
initials: artist.name!.substring(0, 1),
|
||||||
|
size: 30,
|
||||||
|
provider: UniversalImage.imageProvider(
|
||||||
artist.images.asUrlString(
|
artist.images.asUrlString(
|
||||||
placeholder: ImagePlaceholder.artist,
|
placeholder: ImagePlaceholder.artist,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
label: Text(artist.name!),
|
trailing: IconButton.ghost(
|
||||||
onDeleted: () {
|
icon: const Icon(SpotubeIcons.close),
|
||||||
|
onPressed: () {
|
||||||
artists.value = [
|
artists.value = [
|
||||||
...artists.value..removeWhere((element) => element.id == artist.id)
|
...artists.value
|
||||||
|
..removeWhere((element) => element.id == artist.id)
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
child: Text(artist.name!),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final tracksAutocomplete = SeedsMultiAutocomplete<Track>(
|
final tracksAutocomplete = SeedsMultiAutocomplete<Track>(
|
||||||
seeds: tracks,
|
seeds: tracks,
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
selectedItemDisplayType: SelectedItemDisplayType.list,
|
selectedItemDisplayType: SelectedItemDisplayType.list,
|
||||||
inputDecoration: InputDecoration(
|
label: Text(context.l10n.tracks),
|
||||||
labelText: context.l10n.tracks,
|
placeholder: Text(context.l10n.select_up_to_count_type(
|
||||||
labelStyle: textTheme.titleMedium,
|
|
||||||
helperText: context.l10n.select_up_to_count_type(
|
|
||||||
leftSeedCount,
|
leftSeedCount,
|
||||||
context.l10n.tracks,
|
context.l10n.tracks,
|
||||||
),
|
)),
|
||||||
),
|
|
||||||
fetchSeeds: (textEditingValue) => spotify.search
|
fetchSeeds: (textEditingValue) => spotify.search
|
||||||
.get(
|
.get(
|
||||||
textEditingValue.text,
|
textEditingValue.text,
|
||||||
@ -160,22 +159,23 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
autocompleteOptionBuilder: (option, onSelected) => ListTile(
|
autocompleteOptionBuilder: (option, onSelected) => ButtonTile(
|
||||||
leading: CircleAvatar(
|
leading: Avatar(
|
||||||
backgroundImage: UniversalImage.imageProvider(
|
initials: option.name!.substring(0, 1),
|
||||||
|
provider: UniversalImage.imageProvider(
|
||||||
(option.album?.images).asUrlString(
|
(option.album?.images).asUrlString(
|
||||||
placeholder: ImagePlaceholder.artist,
|
placeholder: ImagePlaceholder.artist,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
horizontalTitleGap: 20,
|
|
||||||
title: Text(option.name!),
|
title: Text(option.name!),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
option.artists?.map((e) => e.name).join(", ") ??
|
option.artists?.map((e) => e.name).join(", ") ??
|
||||||
option.album?.name ??
|
option.album?.name ??
|
||||||
"",
|
"",
|
||||||
),
|
),
|
||||||
onTap: () => onSelected(option),
|
onPressed: () => onSelected(option),
|
||||||
|
style: ButtonVariance.ghost,
|
||||||
),
|
),
|
||||||
displayStringForOption: (option) => option.name!,
|
displayStringForOption: (option) => option.name!,
|
||||||
selectedSeedBuilder: (option) => SimpleTrackTile(
|
selectedSeedBuilder: (option) => SimpleTrackTile(
|
||||||
@ -188,42 +188,65 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final genreSelector = MultiSelectField<String>(
|
final genreSelector = MultiSelect<String>(
|
||||||
options: genresCollection.asData?.value ?? [],
|
value: genres.value,
|
||||||
selectedOptions: genres.value,
|
searchFilter: (item, query) {
|
||||||
getValueForOption: (option) => option,
|
return item.toLowerCase().contains(query.toLowerCase()) ? 1 : 0;
|
||||||
onSelected: (value) {
|
},
|
||||||
|
onChanged: (value) {
|
||||||
|
if (!enabled) return;
|
||||||
genres.value = value;
|
genres.value = value;
|
||||||
},
|
},
|
||||||
dialogTitle: Text(context.l10n.select_genres),
|
itemBuilder: (context, item) => Text(item),
|
||||||
label: Text(context.l10n.add_genres),
|
searchPlaceholder: Text(context.l10n.select_genres),
|
||||||
helperText: context.l10n.select_up_to_count_type(
|
orderSelectedFirst: false,
|
||||||
|
popoverAlignment: Alignment.bottomCenter,
|
||||||
|
popupConstraints: BoxConstraints(
|
||||||
|
maxHeight: MediaQuery.sizeOf(context).height * .8,
|
||||||
|
),
|
||||||
|
placeholder: Text(
|
||||||
|
context.l10n.select_up_to_count_type(
|
||||||
leftSeedCount,
|
leftSeedCount,
|
||||||
context.l10n.genre,
|
context.l10n.genre,
|
||||||
),
|
),
|
||||||
enabled: enabled,
|
),
|
||||||
|
children: [
|
||||||
|
for (final option in genresCollection.asData?.value ?? <String>[])
|
||||||
|
SelectItemButton(
|
||||||
|
value: option,
|
||||||
|
child: Text(option),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
final countrySelector = ValueListenableBuilder(
|
final countrySelector = ValueListenableBuilder(
|
||||||
valueListenable: market,
|
valueListenable: market,
|
||||||
builder: (context, value, _) {
|
builder: (context, value, _) {
|
||||||
return DropdownButtonFormField<Market>(
|
return Select<Market>(
|
||||||
decoration: InputDecoration(
|
placeholder: Text(context.l10n.country),
|
||||||
labelText: context.l10n.country,
|
value: market.value,
|
||||||
labelStyle: textTheme.titleMedium,
|
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,
|
popoverAlignment: Alignment.bottomCenter,
|
||||||
items: spotifyMarkets
|
itemBuilder: (context, value) => Text(value.name),
|
||||||
|
children: spotifyMarkets
|
||||||
.map(
|
.map(
|
||||||
(country) => DropdownMenuItem(
|
(country) => SelectItemButton(
|
||||||
value: country.$1,
|
value: country.$1,
|
||||||
child: Text(country.$2),
|
child: Text(country.$2),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
value: market.value,
|
|
||||||
onChanged: (value) {
|
|
||||||
market.value = value!;
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -231,19 +254,17 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
final controller = useScrollController();
|
final controller = useScrollController();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: TitleBar(
|
headers: [
|
||||||
|
TitleBar(
|
||||||
leading: const [BackButton()],
|
leading: const [BackButton()],
|
||||||
title: Text(context.l10n.generate_playlist),
|
title: Text(context.l10n.generate),
|
||||||
),
|
)
|
||||||
body: Scrollbar(
|
],
|
||||||
|
child: Scrollbar(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(maxWidth: Breakpoints.lg),
|
constraints: BoxConstraints(maxWidth: Breakpoints.lg),
|
||||||
child: SliderTheme(
|
|
||||||
data: const SliderThemeData(
|
|
||||||
overlayShape: RoundSliderOverlayShape(),
|
|
||||||
),
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: LayoutBuilder(builder: (context, constrains) {
|
child: LayoutBuilder(builder: (context, constrains) {
|
||||||
return ScrollConfiguration(
|
return ScrollConfiguration(
|
||||||
@ -261,35 +282,36 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
context.l10n.number_of_tracks_generate,
|
context.l10n.number_of_tracks_generate,
|
||||||
style: textTheme.titleMedium,
|
style: typography.semiBold,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
|
spacing: 5,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.primary,
|
color: theme.colorScheme.primary
|
||||||
|
.withAlpha(25),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
value.round().toString(),
|
value.round().toString(),
|
||||||
style: textTheme.bodyLarge?.copyWith(
|
style: typography.large.copyWith(
|
||||||
color: theme
|
color: theme.colorScheme.primary,
|
||||||
.colorScheme.primaryContainer,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Slider(
|
child: Slider(
|
||||||
value: value.toDouble(),
|
value:
|
||||||
|
SliderValue.single(value.toDouble()),
|
||||||
min: 10,
|
min: 10,
|
||||||
max: 100,
|
max: 100,
|
||||||
divisions: 9,
|
divisions: 9,
|
||||||
label: value.round().toString(),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
limit.value = value.round();
|
limit.value = value.value.round();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -616,10 +638,10 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const Gap(20),
|
||||||
FilledButton.icon(
|
Center(
|
||||||
icon: const Icon(SpotubeIcons.magic),
|
child: Button.primary(
|
||||||
label: Text(context.l10n.generate_playlist),
|
leading: const Icon(SpotubeIcons.magic),
|
||||||
onPressed: artists.value.isEmpty &&
|
onPressed: artists.value.isEmpty &&
|
||||||
tracks.value.isEmpty &&
|
tracks.value.isEmpty &&
|
||||||
genres.value.isEmpty
|
genres.value.isEmpty
|
||||||
@ -643,6 +665,8 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
extra: routeState,
|
extra: routeState,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
child: Text(context.l10n.generate),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -652,7 +676,6 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
builder: (context, overlay) {
|
||||||
|
return SurfaceCard(
|
||||||
|
child: Text(
|
||||||
context.l10n.add_count_to_queue(
|
context.l10n.add_count_to_queue(
|
||||||
selectedTracks.value.length,
|
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,
|
||||||
|
builder: (context, overlay) {
|
||||||
|
return SurfaceCard(
|
||||||
|
child: Text(
|
||||||
context.l10n.add_count_to_playlist(
|
context.l10n.add_count_to_playlist(
|
||||||
selectedTracks.value.length,
|
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,18 +218,21 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Card(
|
SafeArea(
|
||||||
margin: const EdgeInsets.all(0),
|
|
||||||
child: SafeArea(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
for (final track
|
for (final track
|
||||||
in generatedPlaylist.asData?.value ?? [])
|
in generatedPlaylist.asData?.value ?? [])
|
||||||
CheckboxListTile(
|
Row(
|
||||||
value: selectedTracks.value.contains(track.id),
|
spacing: 5,
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
state: selectedTracks.value.contains(track.id)
|
||||||
|
? CheckboxState.checked
|
||||||
|
: CheckboxState.unchecked,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value == true) {
|
if (value == CheckboxState.checked) {
|
||||||
selectedTracks.value.add(track.id!);
|
selectedTracks.value.add(track.id!);
|
||||||
} else {
|
} else {
|
||||||
selectedTracks.value.remove(track.id);
|
selectedTracks.value.remove(track.id);
|
||||||
@ -226,15 +240,24 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
|
|||||||
selectedTracks.value =
|
selectedTracks.value =
|
||||||
selectedTracks.value.toList();
|
selectedTracks.value.toList();
|
||||||
},
|
},
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
),
|
||||||
contentPadding: EdgeInsets.zero,
|
Expanded(
|
||||||
dense: true,
|
child: GestureDetector(
|
||||||
title: SimpleTrackTile(track: track),
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user