-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: playlist generation all parameters support
- Loading branch information
Showing
8 changed files
with
666 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
182 changes: 182 additions & 0 deletions
182
lib/components/library/playlist_generate/recommendation_attribute_dials.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
import 'package:flutter/material.dart'; | ||
import 'package:flutter_hooks/flutter_hooks.dart'; | ||
import 'package:spotube/extensions/constrains.dart'; | ||
import 'package:spotube/extensions/context.dart'; | ||
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart'; | ||
|
||
typedef RecommendationAttribute = ({double min, double target, double max}); | ||
|
||
RecommendationAttribute lowValues(double base) => | ||
(min: 1 * base, target: 0.3 * base, max: 0.3 * base); | ||
RecommendationAttribute moderateValues(double base) => | ||
(min: 0.5 * base, target: 1 * base, max: 0.5 * base); | ||
RecommendationAttribute highValues(double base) => | ||
(min: 0.3 * base, target: 0.3 * base, max: 1 * base); | ||
|
||
class RecommendationAttributeDials extends HookWidget { | ||
final Widget title; | ||
final RecommendationAttribute values; | ||
final ValueChanged<RecommendationAttribute> onChanged; | ||
final double base; | ||
|
||
const RecommendationAttributeDials({ | ||
Key? key, | ||
required this.values, | ||
required this.onChanged, | ||
required this.title, | ||
this.base = 1, | ||
}) : super(key: key); | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
final animation = useAnimationController( | ||
duration: const Duration(milliseconds: 300), | ||
); | ||
final labelStyle = Theme.of(context).textTheme.labelSmall?.copyWith( | ||
fontWeight: FontWeight.w500, | ||
); | ||
|
||
final minSlider = Row( | ||
children: [ | ||
Text(context.l10n.min, style: labelStyle), | ||
Expanded( | ||
child: Slider( | ||
value: values.min / base, | ||
min: 0, | ||
max: 1, | ||
onChanged: (value) => onChanged(( | ||
min: value * base, | ||
target: values.target, | ||
max: values.max, | ||
)), | ||
), | ||
), | ||
], | ||
); | ||
|
||
final targetSlider = Row( | ||
children: [ | ||
Text(context.l10n.target, style: labelStyle), | ||
Expanded( | ||
child: Slider( | ||
value: values.target / base, | ||
min: 0, | ||
max: 1, | ||
onChanged: (value) => onChanged(( | ||
min: values.min, | ||
target: value * base, | ||
max: values.max, | ||
)), | ||
), | ||
), | ||
], | ||
); | ||
|
||
final maxSlider = Row( | ||
children: [ | ||
Text(context.l10n.max, style: labelStyle), | ||
Expanded( | ||
child: Slider( | ||
value: values.max / base, | ||
min: 0, | ||
max: 1, | ||
onChanged: (value) => onChanged(( | ||
min: values.min, | ||
target: values.target, | ||
max: value * base, | ||
)), | ||
), | ||
), | ||
], | ||
); | ||
|
||
return LayoutBuilder(builder: (context, constrain) { | ||
return Card( | ||
child: ExpansionTile( | ||
title: DefaultTextStyle( | ||
style: Theme.of(context).textTheme.titleMedium!, | ||
child: title, | ||
), | ||
shape: const Border(), | ||
leading: AnimatedBuilder( | ||
animation: animation, | ||
builder: (context, child) { | ||
return Transform.rotate( | ||
angle: (animation.value * 3.14) / 2, | ||
child: child, | ||
); | ||
}, | ||
child: const Icon(Icons.chevron_right), | ||
), | ||
trailing: Padding( | ||
padding: const EdgeInsets.symmetric(vertical: 8.0), | ||
child: ToggleButtons( | ||
borderRadius: BorderRadius.circular(8), | ||
textStyle: labelStyle, | ||
isSelected: [ | ||
values == lowValues(base), | ||
values == moderateValues(base), | ||
values == highValues(base), | ||
], | ||
onPressed: (index) { | ||
RecommendationAttribute newValues = zeroValues; | ||
switch (index) { | ||
case 0: | ||
newValues = lowValues(base); | ||
break; | ||
case 1: | ||
newValues = moderateValues(base); | ||
break; | ||
case 2: | ||
newValues = highValues(base); | ||
break; | ||
} | ||
|
||
if (newValues == values) { | ||
onChanged(zeroValues); | ||
} else { | ||
onChanged(newValues); | ||
} | ||
}, | ||
children: [ | ||
Text(context.l10n.low), | ||
Text(" ${context.l10n.moderate} "), | ||
Text(context.l10n.high), | ||
], | ||
), | ||
), | ||
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, | ||
], | ||
), | ||
), | ||
], | ||
), | ||
); | ||
}); | ||
} | ||
} |
179 changes: 179 additions & 0 deletions
179
lib/components/library/playlist_generate/recommendation_attribute_fields.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
import 'package:flutter/material.dart'; | ||
import 'package:flutter_hooks/flutter_hooks.dart'; | ||
import 'package:spotube/components/library/playlist_generate/recommendation_attribute_dials.dart'; | ||
import 'package:spotube/extensions/constrains.dart'; | ||
import 'package:spotube/extensions/context.dart'; | ||
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart'; | ||
|
||
class RecommendationAttributeFields extends HookWidget { | ||
final Widget title; | ||
final RecommendationAttribute values; | ||
final ValueChanged<RecommendationAttribute> onChanged; | ||
final Map<String, RecommendationAttribute>? presets; | ||
|
||
const RecommendationAttributeFields({ | ||
Key? key, | ||
required this.values, | ||
required this.onChanged, | ||
required this.title, | ||
this.presets, | ||
}) : super(key: key); | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
final animation = useAnimationController( | ||
duration: const Duration(milliseconds: 300), | ||
); | ||
final labelStyle = Theme.of(context).textTheme.labelSmall?.copyWith( | ||
fontWeight: FontWeight.w500, | ||
); | ||
|
||
final minController = useTextEditingController(text: values.min.toString()); | ||
final targetController = | ||
useTextEditingController(text: values.target.toString()); | ||
final maxController = useTextEditingController(text: values.max.toString()); | ||
|
||
useEffect(() { | ||
listener() { | ||
onChanged(( | ||
min: double.tryParse(minController.text) ?? 0, | ||
target: double.tryParse(targetController.text) ?? 0, | ||
max: double.tryParse(maxController.text) ?? 0, | ||
)); | ||
} | ||
|
||
minController.addListener(listener); | ||
targetController.addListener(listener); | ||
maxController.addListener(listener); | ||
|
||
return () { | ||
minController.removeListener(listener); | ||
targetController.removeListener(listener); | ||
maxController.removeListener(listener); | ||
}; | ||
}, [values]); | ||
|
||
final minField = TextField( | ||
controller: minController, | ||
decoration: InputDecoration( | ||
labelText: context.l10n.min, | ||
isDense: true, | ||
), | ||
keyboardType: const TextInputType.numberWithOptions( | ||
decimal: false, | ||
signed: true, | ||
), | ||
); | ||
|
||
final targetField = TextField( | ||
controller: targetController, | ||
decoration: InputDecoration( | ||
labelText: context.l10n.target, | ||
isDense: true, | ||
), | ||
keyboardType: const TextInputType.numberWithOptions( | ||
decimal: false, | ||
signed: true, | ||
), | ||
); | ||
|
||
final maxField = TextField( | ||
controller: maxController, | ||
decoration: InputDecoration( | ||
labelText: context.l10n.max, | ||
isDense: true, | ||
), | ||
keyboardType: const TextInputType.numberWithOptions( | ||
decimal: false, | ||
signed: true, | ||
), | ||
); | ||
|
||
return LayoutBuilder(builder: (context, constrain) { | ||
return Card( | ||
child: ExpansionTile( | ||
title: DefaultTextStyle( | ||
style: Theme.of(context).textTheme.titleMedium!, | ||
child: title, | ||
), | ||
shape: const Border(), | ||
leading: AnimatedBuilder( | ||
animation: animation, | ||
builder: (context, child) { | ||
return Transform.rotate( | ||
angle: (animation.value * 3.14) / 2, | ||
child: child, | ||
); | ||
}, | ||
child: const Icon(Icons.chevron_right), | ||
), | ||
trailing: presets == null | ||
? const SizedBox.shrink() | ||
: Padding( | ||
padding: const EdgeInsets.symmetric(vertical: 8.0), | ||
child: ToggleButtons( | ||
borderRadius: BorderRadius.circular(8), | ||
textStyle: labelStyle, | ||
isSelected: presets!.values | ||
.map((value) => value == values) | ||
.toList(), | ||
onPressed: (index) { | ||
RecommendationAttribute newValues = | ||
presets!.values.elementAt(index); | ||
if (newValues == values) { | ||
onChanged(zeroValues); | ||
minController.text = zeroValues.min.toString(); | ||
targetController.text = zeroValues.target.toString(); | ||
maxController.text = zeroValues.max.toString(); | ||
} else { | ||
onChanged(newValues); | ||
minController.text = newValues.min.toString(); | ||
targetController.text = newValues.target.toString(); | ||
maxController.text = newValues.max.toString(); | ||
} | ||
}, | ||
children: presets!.keys.map((key) => Text(key)).toList(), | ||
), | ||
), | ||
onExpansionChanged: (value) { | ||
if (value) { | ||
animation.forward(); | ||
} else { | ||
animation.reverse(); | ||
} | ||
}, | ||
children: [ | ||
const SizedBox(height: 8), | ||
if (constrain.mdAndUp) | ||
Row( | ||
mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
children: [ | ||
const SizedBox(width: 16), | ||
Expanded(child: minField), | ||
const SizedBox(width: 16), | ||
Expanded(child: targetField), | ||
const SizedBox(width: 16), | ||
Expanded(child: maxField), | ||
const SizedBox(width: 16), | ||
], | ||
) | ||
else | ||
Padding( | ||
padding: const EdgeInsets.symmetric(horizontal: 16), | ||
child: Column( | ||
children: [ | ||
minField, | ||
const SizedBox(height: 16), | ||
targetField, | ||
const SizedBox(height: 16), | ||
maxField, | ||
], | ||
), | ||
), | ||
const SizedBox(height: 8), | ||
], | ||
), | ||
); | ||
}); | ||
} | ||
} |
Oops, something went wrong.