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

View File

@ -1,7 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spotube/extensions/constrains.dart';
enum SelectedItemDisplayType { enum SelectedItemDisplayType {
wrap, wrap,
@ -39,14 +41,30 @@ class SeedsMultiAutocomplete<T extends Object> extends HookWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
useValueListenable(seeds); useValueListenable(seeds);
final theme = Theme.of(context); final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
final seedController = useTextEditingController(); 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( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
LayoutBuilder(builder: (context, constrains) { LayoutBuilder(builder: (context, constrains) {
return Autocomplete<T>( return Container(
key: containerKey.value,
child: Autocomplete<T>(
optionsBuilder: (textEditingValue) async { optionsBuilder: (textEditingValue) async {
if (textEditingValue.text.isEmpty) return []; if (textEditingValue.text.isEmpty) return [];
return fetchSeeds(textEditingValue); return fetchSeeds(textEditingValue);
@ -58,10 +76,11 @@ class SeedsMultiAutocomplete<T extends Object> extends HookWidget {
optionsViewBuilder: (context, onSelected, options) { optionsViewBuilder: (context, onSelected, options) {
return Align( return Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: ConstrainedBox( child: Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: constrains.maxWidth, maxWidth: constrains.maxWidth,
), ),
height: max(listHeight, 0),
child: Card( child: Card(
child: ListView.builder( child: ListView.builder(
shrinkWrap: true, shrinkWrap: true,
@ -91,6 +110,7 @@ class SeedsMultiAutocomplete<T extends Object> extends HookWidget {
decoration: inputDecoration, decoration: inputDecoration,
); );
}, },
),
); );
}), }),
const SizedBox(height: 8), 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/library/playlist_generate/simple_track_tile.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/components/shared/page_window_title_bar.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/extensions/context.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart'; import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify_provider.dart';
@ -44,6 +45,172 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
final leftSeedCount = final leftSeedCount =
5 - genres.value.length - artists.value.length - tracks.value.length; 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( return Scaffold(
appBar: PageWindowTitleBar( appBar: PageWindowTitleBar(
leading: const BackButton(), leading: const BackButton(),
@ -51,7 +218,8 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
centerTitle: true, centerTitle: true,
), ),
body: SafeArea( body: SafeArea(
child: ListView( child: LayoutBuilder(builder: (context, constrains) {
return ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
children: [ children: [
ValueListenableBuilder( ValueListenableBuilder(
@ -100,171 +268,43 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
ValueListenableBuilder( if (constrains.mdAndUp)
valueListenable: market, Row(
builder: (context, value, _) { crossAxisAlignment: CrossAxisAlignment.start,
return DropdownMenu<String>( children: [
hintText: "Select a country", Expanded(
dropdownMenuEntries: spotifyMarkets child: countrySelector,
.map(
(country) => DropdownMenuEntry(
value: country.first,
label: country.last,
), ),
const SizedBox(width: 16),
Expanded(
child: genreSelector,
),
],
) )
.toList(), else ...[
initialSelection: market.value, countrySelector,
onSelected: (value) {
market.value = value!;
},
);
},
),
const SizedBox(height: 16), const SizedBox(height: 16),
MultiSelectField<String>( genreSelector,
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), const SizedBox(height: 16),
SeedsMultiAutocomplete<Artist>( if (constrains.mdAndUp)
seeds: artists, Row(
enabled: enabled, crossAxisAlignment: CrossAxisAlignment.start,
inputDecoration: InputDecoration( children: [
labelText: "Artists", Expanded(
labelStyle: textTheme.titleMedium, child: artistAutoComplete,
helperText: "Select up to $leftSeedCount artists",
), ),
fetchSeeds: (textEditingValue) => spotify.search const SizedBox(width: 16),
.get( Expanded(
textEditingValue.text, child: tracksAutocomplete,
types: [SearchType.artist], ),
],
) )
.first(6) else ...[
.then( artistAutoComplete,
(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), const SizedBox(height: 16),
SeedsMultiAutocomplete<Track>( tracksAutocomplete,
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)
];
},
),
),
const SizedBox(height: 20), const SizedBox(height: 20),
FilledButton.icon( FilledButton.icon(
icon: const Icon(SpotubeIcons.magic), icon: const Icon(SpotubeIcons.magic),
@ -289,7 +329,8 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
}, },
), ),
], ],
), );
}),
), ),
); );
} }

View File

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