feat: responsive playlist generate page and scrollable multi autocomplete

This commit is contained in:
Kingkor Roy Tirtho 2023-06-08 10:01:01 +06:00
parent 4a21249ee3
commit d57aad5612
4 changed files with 520 additions and 460 deletions

View File

@ -1,188 +1,187 @@
// Country Codes contributed by momobobe <https://github.com/momobobe>
final spotifyMarkets = [
["AL", "Albania (AL)"],
["DZ", "Algeria (DZ)"],
["AD", "Andorra (AD)"],
["AO", "Angola (AO)"],
["AG", "Antigua and Barbuda (AG)"],
["AR", "Argentina (AR)"],
["AM", "Armenia (AM)"],
["AU", "Australia (AU)"],
["AT", "Austria (AT)"],
["AZ", "Azerbaijan (AZ)"],
["BH", "Bahrain (BH)"],
["BD", "Bangladesh (BD)"],
["BB", "Barbados (BB)"],
["BY", "Belarus (BY)"],
["BE", "Belgium (BE)"],
["BZ", "Belize (BZ)"],
["BJ", "Benin (BJ)"],
["BT", "Bhutan (BT)"],
["BO", "Bolivia (BO)"],
["BA", "Bosnia and Herzegovina (BA)"],
["BW", "Botswana (BW)"],
["BR", "Brazil (BR)"],
["BN", "Brunei Darussalam (BN)"],
["BG", "Bulgaria (BG)"],
["BF", "Burkina Faso (BF)"],
["BI", "Burundi (BI)"],
["CV", "Cabo Verde / Cape Verde (CV)"],
["KH", "Cambodia (KH)"],
["CM", "Cameroon (CM)"],
["CA", "Canada (CA)"],
["TD", "Chad (TD)"],
["CL", "Chile (CL)"],
["CO", "Colombia (CO)"],
["KM", "Comoros (KM)"],
["CR", "Costa Rica (CR)"],
["HR", "Croatia (HR)"],
["CW", "Curaçao (CW)"],
["CY", "Cyprus (CY)"],
["CZ", "Czech Republic (CZ)"],
["CI", "Côte d'Ivoire / Ivory Coast (CI)"],
["CD", "Democratic Republic of the Congo (CD)"],
["DK", "Denmark (DK)"],
["DJ", "Djibouti (DJ)"],
["DM", "Dominica (DM)"],
["DO", "Dominican Republic (DO)"],
["EC", "Ecuador (EC)"],
["EG", "Egypt (EG)"],
["SV", "El Salvador (SV)"],
["GQ", "Equatorial Guinea (GQ)"],
["EE", "Estonia (EE)"],
["SZ", "Eswatini (SZ)"],
["FJ", "Fiji (FJ)"],
["FI", "Finland (FI)"],
["FR", "France (FR)"],
["GA", "Gabon (GA)"],
["GE", "Georgia (GE)"],
["DE", "Germany (DE)"],
["GH", "Ghana (GH)"],
["GR", "Greece (GR)"],
["GD", "Grenada (GD)"],
["GT", "Guatemala (GT)"],
["GN", "Guinea (GN)"],
["GW", "Guinea-Bissau (GW)"],
["GY", "Guyana (GY)"],
["HT", "Haiti (HT)"],
["HN", "Honduras (HN)"],
["HK", "Hong Kong (HK)"],
["HU", "Hungary (HU)"],
["IS", "Iceland (IS)"],
["IN", "India (IN)"],
["ID", "Indonesia (ID)"],
["IQ", "Iraq (IQ)"],
["IE", "Ireland (IE)"],
["IL", "Israel (IL)"],
["IT", "Italy (IT)"],
["JM", "Jamaica (JM)"],
["JP", "Japan (JP)"],
["JO", "Jordan (JO)"],
["KZ", "Kazakhstan (KZ)"],
["KE", "Kenya (KE)"],
["KI", "Kiribati (KI)"],
["XK", "Kosovo (XK)"],
["KW", "Kuwait (KW)"],
["KG", "Kyrgyzstan (KG)"],
["LA", "Laos (LA)"],
["LV", "Latvia (LV)"],
["LB", "Lebanon (LB)"],
["LS", "Lesotho (LS)"],
["LR", "Liberia (LR)"],
["LY", "Libya (LY)"],
["LI", "Liechtenstein (LI)"],
["LT", "Lithuania (LT)"],
["LU", "Luxembourg (LU)"],
["MO", "Macao / Macau (MO)"],
["MG", "Madagascar (MG)"],
["MW", "Malawi (MW)"],
["MY", "Malaysia (MY)"],
["MV", "Maldives (MV)"],
["ML", "Mali (ML)"],
["MT", "Malta (MT)"],
["MH", "Marshall Islands (MH)"],
["MR", "Mauritania (MR)"],
["MU", "Mauritius (MU)"],
["MX", "Mexico (MX)"],
["FM", "Micronesia (FM)"],
["MD", "Moldova (MD)"],
["MC", "Monaco (MC)"],
["MN", "Mongolia (MN)"],
["ME", "Montenegro (ME)"],
["MA", "Morocco (MA)"],
["MZ", "Mozambique (MZ)"],
["NA", "Namibia (NA)"],
["NR", "Nauru (NR)"],
["NP", "Nepal (NP)"],
["NL", "Netherlands (NL)"],
["NZ", "New Zealand (NZ)"],
["NI", "Nicaragua (NI)"],
["NE", "Niger (NE)"],
["NG", "Nigeria (NG)"],
["MK", "North Macedonia (MK)"],
["NO", "Norway (NO)"],
["OM", "Oman (OM)"],
["PK", "Pakistan (PK)"],
["PW", "Palau (PW)"],
["PS", "Palestine (PS)"],
["PA", "Panama (PA)"],
["PG", "Papua New Guinea (PG)"],
["PY", "Paraguay (PY)"],
["PE", "Peru (PE)"],
["PH", "Philippines (PH)"],
["PL", "Poland (PL)"],
["PT", "Portugal (PT)"],
["QA", "Qatar (QA)"],
["CG", "Republic of the Congo (CG)"],
["RO", "Romania (RO)"],
["RU", "Russia (RU)"],
["RW", "Rwanda (RW)"],
["WS", "Samoa (WS)"],
["SM", "San Marino (SM)"],
["SA", "Saudi Arabia (SA)"],
["SN", "Senegal (SN)"],
["RS", "Serbia (RS)"],
["SC", "Seychelles (SC)"],
["SL", "Sierra Leone (SL)"],
["SG", "Singapore (SG)"],
["SK", "Slovakia (SK)"],
["SI", "Slovenia (SI)"],
["SB", "Solomon Islands (SB)"],
["ZA", "South Africa (ZA)"],
["KR", "South Korea (KR)"],
["ES", "Spain (ES)"],
["LK", "Sri Lanka (LK)"],
["VC", "St Vincent and the Grenadines (VC)"],
["KN", "St. Kitts and Nevis (KN)"],
["LC", "St. Lucia (LC)"],
["SR", "Suriname (SR)"],
["SE", "Sweden (SE)"],
["CH", "Switzerland (CH)"],
["ST", "São Tomé and Príncipe (ST)"],
["TW", "Taiwan (TW)"],
["TJ", "Tajikistan (TJ)"],
["TZ", "Tanzania (TZ)"],
["TH", "Thailand (TH)"],
["BS", "The Bahamas (BS)"],
["GM", "The Gambia (GM)"],
["TL", "Timor-Leste / East Timor (TL)"],
["TG", "Togo (TG)"],
["TO", "Tonga (TO)"],
["TT", "Trinidad and Tobago (TT)"],
["TN", "Tunisia (TN)"],
["TR", "Turkey (TR)"],
["TV", "Tuvalu (TV)"],
["UG", "Uganda (UG)"],
["UA", "Ukraine (UA)"],
["AE", "United Arab Emirates (AE)"],
["GB", "United Kingdom (GB)"],
["US", "United States (US)"],
["UY", "Uruguay (UY)"],
["UZ", "Uzbekistan (UZ)"],
["VU", "Vanuatu (VU)"],
["VE", "Venezuela (VE)"],
["VN", "Vietnam (VN)"],
["ZM", "Zambia (ZM)"],
["Z", "Zimbabwe (ZW)"],
("AL", "Albania (AL)"),
("DZ", "Algeria (DZ)"),
("AD", "Andorra (AD)"),
("AO", "Angola (AO)"),
("AG", "Antigua and Barbuda (AG)"),
("AR", "Argentina (AR)"),
("AM", "Armenia (AM)"),
("AU", "Australia (AU)"),
("AT", "Austria (AT)"),
("AZ", "Azerbaijan (AZ)"),
("BH", "Bahrain (BH)"),
("BD", "Bangladesh (BD)"),
("BB", "Barbados (BB)"),
("BY", "Belarus (BY)"),
("BE", "Belgium (BE)"),
("BZ", "Belize (BZ)"),
("BJ", "Benin (BJ)"),
("BT", "Bhutan (BT)"),
("BO", "Bolivia (BO)"),
("BA", "Bosnia and Herzegovina (BA)"),
("BW", "Botswana (BW)"),
("BR", "Brazil (BR)"),
("BN", "Brunei Darussalam (BN)"),
("BG", "Bulgaria (BG)"),
("BF", "Burkina Faso (BF)"),
("BI", "Burundi (BI)"),
("CV", "Cabo Verde / Cape Verde (CV)"),
("KH", "Cambodia (KH)"),
("CM", "Cameroon (CM)"),
("CA", "Canada (CA)"),
("TD", "Chad (TD)"),
("CL", "Chile (CL)"),
("CO", "Colombia (CO)"),
("KM", "Comoros (KM)"),
("CR", "Costa Rica (CR)"),
("HR", "Croatia (HR)"),
("CW", "Curaçao (CW)"),
("CY", "Cyprus (CY)"),
("CZ", "Czech Republic (CZ)"),
("CI", "Ivory Coast (CI)"),
("CD", "Congo (CD)"),
("DK", "Denmark (DK)"),
("DJ", "Djibouti (DJ)"),
("DM", "Dominica (DM)"),
("DO", "Dominican Republic (DO)"),
("EC", "Ecuador (EC)"),
("EG", "Egypt (EG)"),
("SV", "El Salvador (SV)"),
("GQ", "Equatorial Guinea (GQ)"),
("EE", "Estonia (EE)"),
("SZ", "Eswatini (SZ)"),
("FJ", "Fiji (FJ)"),
("FI", "Finland (FI)"),
("FR", "France (FR)"),
("GA", "Gabon (GA)"),
("GE", "Georgia (GE)"),
("DE", "Germany (DE)"),
("GH", "Ghana (GH)"),
("GR", "Greece (GR)"),
("GD", "Grenada (GD)"),
("GT", "Guatemala (GT)"),
("GN", "Guinea (GN)"),
("GW", "Guinea-Bissau (GW)"),
("GY", "Guyana (GY)"),
("HT", "Haiti (HT)"),
("HN", "Honduras (HN)"),
("HK", "Hong Kong (HK)"),
("HU", "Hungary (HU)"),
("IS", "Iceland (IS)"),
("IN", "India (IN)"),
("ID", "Indonesia (ID)"),
("IQ", "Iraq (IQ)"),
("IE", "Ireland (IE)"),
("IL", "Israel (IL)"),
("IT", "Italy (IT)"),
("JM", "Jamaica (JM)"),
("JP", "Japan (JP)"),
("JO", "Jordan (JO)"),
("KZ", "Kazakhstan (KZ)"),
("KE", "Kenya (KE)"),
("KI", "Kiribati (KI)"),
("XK", "Kosovo (XK)"),
("KW", "Kuwait (KW)"),
("KG", "Kyrgyzstan (KG)"),
("LA", "Laos (LA)"),
("LV", "Latvia (LV)"),
("LB", "Lebanon (LB)"),
("LS", "Lesotho (LS)"),
("LR", "Liberia (LR)"),
("LY", "Libya (LY)"),
("LI", "Liechtenstein (LI)"),
("LT", "Lithuania (LT)"),
("LU", "Luxembourg (LU)"),
("MO", "Macao / Macau (MO)"),
("MG", "Madagascar (MG)"),
("MW", "Malawi (MW)"),
("MY", "Malaysia (MY)"),
("MV", "Maldives (MV)"),
("ML", "Mali (ML)"),
("MT", "Malta (MT)"),
("MH", "Marshall Islands (MH)"),
("MR", "Mauritania (MR)"),
("MU", "Mauritius (MU)"),
("MX", "Mexico (MX)"),
("FM", "Micronesia (FM)"),
("MD", "Moldova (MD)"),
("MC", "Monaco (MC)"),
("MN", "Mongolia (MN)"),
("ME", "Montenegro (ME)"),
("MA", "Morocco (MA)"),
("MZ", "Mozambique (MZ)"),
("NA", "Namibia (NA)"),
("NR", "Nauru (NR)"),
("NP", "Nepal (NP)"),
("NL", "Netherlands (NL)"),
("NZ", "New Zealand (NZ)"),
("NI", "Nicaragua (NI)"),
("NE", "Niger (NE)"),
("NG", "Nigeria (NG)"),
("MK", "North Macedonia (MK)"),
("NO", "Norway (NO)"),
("OM", "Oman (OM)"),
("PK", "Pakistan (PK)"),
("PW", "Palau (PW)"),
("PS", "Palestine (PS)"),
("PA", "Panama (PA)"),
("PG", "Papua New Guinea (PG)"),
("PY", "Paraguay (PY)"),
("PE", "Peru (PE)"),
("PH", "Philippines (PH)"),
("PL", "Poland (PL)"),
("PT", "Portugal (PT)"),
("QA", "Qatar (QA)"),
("CG", "Congo (CG)"),
("RO", "Romania (RO)"),
("RU", "Russia (RU)"),
("RW", "Rwanda (RW)"),
("WS", "Samoa (WS)"),
("SM", "San Marino (SM)"),
("SA", "Saudi Arabia (SA)"),
("SN", "Senegal (SN)"),
("RS", "Serbia (RS)"),
("SC", "Seychelles (SC)"),
("SL", "Sierra Leone (SL)"),
("SG", "Singapore (SG)"),
("SK", "Slovakia (SK)"),
("SI", "Slovenia (SI)"),
("SB", "Solomon Islands (SB)"),
("ZA", "South Africa (ZA)"),
("KR", "South Korea (KR)"),
("ES", "Spain (ES)"),
("LK", "Sri Lanka (LK)"),
("KN", "St. Kitts and Nevis (KN)"),
("LC", "St. Lucia (LC)"),
("SR", "Suriname (SR)"),
("SE", "Sweden (SE)"),
("CH", "Switzerland (CH)"),
("ST", "São Tomé and Príncipe (ST)"),
("TW", "Taiwan (TW)"),
("TJ", "Tajikistan (TJ)"),
("TZ", "Tanzania (TZ)"),
("TH", "Thailand (TH)"),
("BS", "The Bahamas (BS)"),
("GM", "The Gambia (GM)"),
("TL", "East Timor (TL)"),
("TG", "Togo (TG)"),
("TO", "Tonga (TO)"),
("TT", "Trinidad and Tobago (TT)"),
("TN", "Tunisia (TN)"),
("TR", "Turkey (TR)"),
("TV", "Tuvalu (TV)"),
("UG", "Uganda (UG)"),
("UA", "Ukraine (UA)"),
("AE", "United Arab Emirates (AE)"),
("GB", "United Kingdom (GB)"),
("US", "United States (US)"),
("UY", "Uruguay (UY)"),
("UZ", "Uzbekistan (UZ)"),
("VU", "Vanuatu (VU)"),
("VE", "Venezuela (VE)"),
("VN", "Vietnam (VN)"),
("ZM", "Zambia (ZM)"),
("ZW", "Zimbabwe (ZW)"),
];

View File

@ -1,7 +1,9 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spotube/extensions/constrains.dart';
enum SelectedItemDisplayType {
wrap,
@ -39,58 +41,76 @@ class SeedsMultiAutocomplete<T extends Object> extends HookWidget {
Widget build(BuildContext context) {
useValueListenable(seeds);
final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
final seedController = useTextEditingController();
final containerKey = useRef(GlobalKey());
final box =
containerKey.value.currentContext?.findRenderObject() as RenderBox?;
final position = box?.localToGlobal(Offset.zero); //this is global position
final containerYPos = position?.dy ?? 0; //th
final containerHeight = box?.size.height ?? 0;
final listHeight = mediaQuery.size.height -
(containerYPos + containerHeight) -
// bottom player bar height
(mediaQuery.mdAndUp ? 80 : 0);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
LayoutBuilder(builder: (context, constrains) {
return Autocomplete<T>(
optionsBuilder: (textEditingValue) async {
if (textEditingValue.text.isEmpty) return [];
return fetchSeeds(textEditingValue);
},
onSelected: (value) {
seeds.value = [...seeds.value, value];
seedController.clear();
},
optionsViewBuilder: (context, onSelected, options) {
return Align(
alignment: Alignment.topLeft,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constrains.maxWidth,
),
child: Card(
child: ListView.builder(
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (context, index) {
final option = options.elementAt(index);
return autocompleteOptionBuilder(option, onSelected);
},
return Container(
key: containerKey.value,
child: Autocomplete<T>(
optionsBuilder: (textEditingValue) async {
if (textEditingValue.text.isEmpty) return [];
return fetchSeeds(textEditingValue);
},
onSelected: (value) {
seeds.value = [...seeds.value, value];
seedController.clear();
},
optionsViewBuilder: (context, onSelected, options) {
return Align(
alignment: Alignment.topLeft,
child: Container(
constraints: BoxConstraints(
maxWidth: constrains.maxWidth,
),
height: max(listHeight, 0),
child: Card(
child: ListView.builder(
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (context, index) {
final option = options.elementAt(index);
return autocompleteOptionBuilder(option, onSelected);
},
),
),
),
),
);
},
displayStringForOption: displayStringForOption,
fieldViewBuilder: (
context,
textEditingController,
focusNode,
onFieldSubmitted,
) {
return TextFormField(
controller: seedController,
onChanged: (value) => textEditingController.text = value,
focusNode: focusNode,
onFieldSubmitted: (_) => onFieldSubmitted(),
enabled: enabled,
decoration: inputDecoration,
);
},
);
},
displayStringForOption: displayStringForOption,
fieldViewBuilder: (
context,
textEditingController,
focusNode,
onFieldSubmitted,
) {
return TextFormField(
controller: seedController,
onChanged: (value) => textEditingController.text = value,
focusNode: focusNode,
onFieldSubmitted: (_) => onFieldSubmitted(),
enabled: enabled,
decoration: inputDecoration,
);
},
),
);
}),
const SizedBox(height: 8),

View File

@ -11,6 +11,7 @@ import 'package:spotube/components/library/playlist_generate/seeds_multi_autocom
import 'package:spotube/components/library/playlist_generate/simple_track_tile.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart';
import 'package:spotube/provider/spotify_provider.dart';
@ -44,6 +45,172 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
final leftSeedCount =
5 - genres.value.length - artists.value.length - tracks.value.length;
final artistAutoComplete = SeedsMultiAutocomplete<Artist>(
seeds: artists,
enabled: enabled,
inputDecoration: InputDecoration(
labelText: "Artists",
labelStyle: textTheme.titleMedium,
helperText: "Select up to $leftSeedCount artists",
),
fetchSeeds: (textEditingValue) => spotify.search
.get(
textEditingValue.text,
types: [SearchType.artist],
)
.first(6)
.then(
(v) => List.castFrom<dynamic, Artist>(
v.expand((e) => e.items ?? []).toList(),
)
.where(
(element) =>
artists.value.none((artist) => element.id == artist.id),
)
.toList(),
),
autocompleteOptionBuilder: (option, onSelected) => ListTile(
leading: CircleAvatar(
backgroundImage: UniversalImage.imageProvider(
TypeConversionUtils.image_X_UrlString(
option.images,
placeholder: ImagePlaceholder.artist,
),
),
),
horizontalTitleGap: 20,
title: Text(option.name!),
subtitle: option.genres?.isNotEmpty != true
? null
: Wrap(
spacing: 4,
runSpacing: 4,
children: option.genres!.mapIndexed(
(index, genre) {
return Chip(
label: Text(genre),
labelStyle: textTheme.bodySmall?.copyWith(
color: theme.colorScheme.secondary,
fontWeight: FontWeight.w600,
),
side: BorderSide.none,
backgroundColor: theme.colorScheme.secondaryContainer,
);
},
).toList(),
),
onTap: () => onSelected(option),
),
displayStringForOption: (option) => option.name!,
selectedSeedBuilder: (artist) => Chip(
avatar: CircleAvatar(
backgroundImage: UniversalImage.imageProvider(
TypeConversionUtils.image_X_UrlString(
artist.images,
placeholder: ImagePlaceholder.artist,
),
),
),
label: Text(artist.name!),
onDeleted: () {
artists.value = [
...artists.value..removeWhere((element) => element.id == artist.id)
];
},
),
);
final tracksAutocomplete = SeedsMultiAutocomplete<Track>(
seeds: tracks,
enabled: enabled,
selectedItemDisplayType: SelectedItemDisplayType.list,
inputDecoration: InputDecoration(
labelText: "Tracks",
labelStyle: textTheme.titleMedium,
helperText: "Select up to $leftSeedCount tracks",
),
fetchSeeds: (textEditingValue) => spotify.search
.get(
textEditingValue.text,
types: [SearchType.track],
)
.first(6)
.then(
(v) => List.castFrom<dynamic, Track>(
v.expand((e) => e.items ?? []).toList(),
)
.where(
(element) =>
tracks.value.none((track) => element.id == track.id),
)
.toList(),
),
autocompleteOptionBuilder: (option, onSelected) => ListTile(
leading: CircleAvatar(
backgroundImage: UniversalImage.imageProvider(
TypeConversionUtils.image_X_UrlString(
option.album?.images,
placeholder: ImagePlaceholder.artist,
),
),
),
horizontalTitleGap: 20,
title: Text(option.name!),
subtitle: Text(
option.artists?.map((e) => e.name).join(", ") ??
option.album?.name ??
"",
),
onTap: () => onSelected(option),
),
displayStringForOption: (option) => option.name!,
selectedSeedBuilder: (option) => SimpleTrackTile(
track: option,
onDelete: () {
tracks.value = [
...tracks.value..removeWhere((element) => element.id == option.id)
];
},
),
);
final genreSelector = MultiSelectField<String>(
options: genresCollection.data ?? [],
selectedOptions: genres.value,
getValueForOption: (option) => option,
onSelected: (value) {
genres.value = value;
},
dialogTitle: const Text("Select genres"),
label: const Text("Add genres"),
helperText: "Select up to $leftSeedCount genres",
enabled: enabled,
);
final countrySelector = ValueListenableBuilder(
valueListenable: market,
builder: (context, value, _) {
return DropdownButtonFormField<String>(
decoration: InputDecoration(
labelText: "Country",
labelStyle: textTheme.titleMedium,
),
isExpanded: true,
items: spotifyMarkets
.map(
(country) => DropdownMenuItem(
value: country.$1,
child: Text(country.$2),
),
)
.toList(),
value: market.value,
onChanged: (value) {
market.value = value!;
},
);
},
);
return Scaffold(
appBar: PageWindowTitleBar(
leading: const BackButton(),
@ -51,245 +218,119 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
centerTitle: true,
),
body: SafeArea(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
ValueListenableBuilder(
valueListenable: limit,
builder: (context, value, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Number of tracks to generate",
style: textTheme.titleMedium,
),
Row(
children: [
Container(
width: 40,
height: 40,
alignment: Alignment.center,
decoration: BoxDecoration(
color: theme.colorScheme.primary,
shape: BoxShape.circle,
),
child: Text(
value.round().toString(),
style: textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.primaryContainer,
child: LayoutBuilder(builder: (context, constrains) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
ValueListenableBuilder(
valueListenable: limit,
builder: (context, value, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Number of tracks to generate",
style: textTheme.titleMedium,
),
Row(
children: [
Container(
width: 40,
height: 40,
alignment: Alignment.center,
decoration: BoxDecoration(
color: theme.colorScheme.primary,
shape: BoxShape.circle,
),
child: Text(
value.round().toString(),
style: textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.primaryContainer,
),
),
),
),
Expanded(
child: Slider(
value: value.toDouble(),
min: 10,
max: 100,
divisions: 9,
label: value.round().toString(),
onChanged: (value) {
limit.value = value.round();
},
),
)
],
)
],
);
},
),
const SizedBox(height: 16),
ValueListenableBuilder(
valueListenable: market,
builder: (context, value, _) {
return DropdownMenu<String>(
hintText: "Select a country",
dropdownMenuEntries: spotifyMarkets
.map(
(country) => DropdownMenuEntry(
value: country.first,
label: country.last,
),
Expanded(
child: Slider(
value: value.toDouble(),
min: 10,
max: 100,
divisions: 9,
label: value.round().toString(),
onChanged: (value) {
limit.value = value.round();
},
),
)
],
)
.toList(),
initialSelection: market.value,
onSelected: (value) {
market.value = value!;
},
);
},
),
const SizedBox(height: 16),
MultiSelectField<String>(
options: genresCollection.data ?? [],
selectedOptions: genres.value,
getValueForOption: (option) => option,
onSelected: (value) {
genres.value = value;
},
dialogTitle: const Text("Select genres"),
label: const Text("Add genres"),
helperText: "Select up to $leftSeedCount genres",
enabled: enabled,
),
const SizedBox(height: 16),
SeedsMultiAutocomplete<Artist>(
seeds: artists,
enabled: enabled,
inputDecoration: InputDecoration(
labelText: "Artists",
labelStyle: textTheme.titleMedium,
helperText: "Select up to $leftSeedCount artists",
),
fetchSeeds: (textEditingValue) => spotify.search
.get(
textEditingValue.text,
types: [SearchType.artist],
)
.first(6)
.then(
(v) => List.castFrom<dynamic, Artist>(
v.expand((e) => e.items ?? []).toList(),
)
.where(
(element) => artists.value
.none((artist) => element.id == artist.id),
)
.toList(),
),
autocompleteOptionBuilder: (option, onSelected) => ListTile(
leading: CircleAvatar(
backgroundImage: UniversalImage.imageProvider(
TypeConversionUtils.image_X_UrlString(
option.images,
placeholder: ImagePlaceholder.artist,
),
),
),
horizontalTitleGap: 20,
title: Text(option.name!),
subtitle: option.genres?.isNotEmpty != true
? null
: Wrap(
spacing: 4,
runSpacing: 4,
children: option.genres!.mapIndexed(
(index, genre) {
return Chip(
label: Text(genre),
labelStyle: textTheme.bodySmall?.copyWith(
color: theme.colorScheme.secondary,
fontWeight: FontWeight.w600,
),
side: BorderSide.none,
backgroundColor:
theme.colorScheme.secondaryContainer,
);
},
).toList(),
),
onTap: () => onSelected(option),
),
displayStringForOption: (option) => option.name!,
selectedSeedBuilder: (artist) => Chip(
avatar: CircleAvatar(
backgroundImage: UniversalImage.imageProvider(
TypeConversionUtils.image_X_UrlString(
artist.images,
placeholder: ImagePlaceholder.artist,
),
),
),
label: Text(artist.name!),
onDeleted: () {
artists.value = [
...artists.value
..removeWhere((element) => element.id == artist.id)
];
],
);
},
),
),
const SizedBox(height: 16),
SeedsMultiAutocomplete<Track>(
seeds: tracks,
enabled: enabled,
selectedItemDisplayType: SelectedItemDisplayType.list,
inputDecoration: InputDecoration(
labelText: "Tracks",
labelStyle: textTheme.titleMedium,
helperText: "Select up to $leftSeedCount tracks",
),
fetchSeeds: (textEditingValue) => spotify.search
.get(
textEditingValue.text,
types: [SearchType.track],
)
.first(6)
.then(
(v) => List.castFrom<dynamic, Track>(
v.expand((e) => e.items ?? []).toList(),
)
.where(
(element) => tracks.value
.none((track) => element.id == track.id),
)
.toList(),
),
autocompleteOptionBuilder: (option, onSelected) => ListTile(
leading: CircleAvatar(
backgroundImage: UniversalImage.imageProvider(
TypeConversionUtils.image_X_UrlString(
option.album?.images,
placeholder: ImagePlaceholder.artist,
const SizedBox(height: 16),
if (constrains.mdAndUp)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: countrySelector,
),
),
),
horizontalTitleGap: 20,
title: Text(option.name!),
subtitle: Text(
option.artists?.map((e) => e.name).join(", ") ??
option.album?.name ??
"",
),
onTap: () => onSelected(option),
),
displayStringForOption: (option) => option.name!,
selectedSeedBuilder: (option) => SimpleTrackTile(
track: option,
onDelete: () {
tracks.value = [
...tracks.value
..removeWhere((element) => element.id == option.id)
];
const SizedBox(width: 16),
Expanded(
child: genreSelector,
),
],
)
else ...[
countrySelector,
const SizedBox(height: 16),
genreSelector,
],
const SizedBox(height: 16),
if (constrains.mdAndUp)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: artistAutoComplete,
),
const SizedBox(width: 16),
Expanded(
child: tracksAutocomplete,
),
],
)
else ...[
artistAutoComplete,
const SizedBox(height: 16),
tracksAutocomplete,
],
const SizedBox(height: 20),
FilledButton.icon(
icon: const Icon(SpotubeIcons.magic),
label: Text("Generate"),
onPressed: () {
final PlaylistGenerateResultRouteState routeState = (
seeds: (
artists: artists.value.map((a) => a.id!).toList(),
tracks: tracks.value.map((t) => t.id!).toList(),
genres: genres.value
),
market: market.value,
limit: limit.value,
max: null,
min: null,
target: null,
);
GoRouter.of(context).push(
"/library/generate/result",
extra: routeState,
);
},
),
),
const SizedBox(height: 20),
FilledButton.icon(
icon: const Icon(SpotubeIcons.magic),
label: Text("Generate"),
onPressed: () {
final PlaylistGenerateResultRouteState routeState = (
seeds: (
artists: artists.value.map((a) => a.id!).toList(),
tracks: tracks.value.map((t) => t.id!).toList(),
genres: genres.value
),
market: market.value,
limit: limit.value,
max: null,
min: null,
target: null,
);
GoRouter.of(context).push(
"/library/generate/result",
extra: routeState,
);
},
),
],
),
],
);
}),
),
);
}

View File

@ -181,8 +181,8 @@ class SettingsPage extends HookConsumerWidget {
options: spotifyMarkets
.map(
(country) => DropdownMenuItem(
value: country.first,
child: Text(country.last),
value: country.$1,
child: Text(country.$2),
),
)
.toList(),