diff --git a/lib/collections/spotify_markets.dart b/lib/collections/spotify_markets.dart index 7bde6bbb3..a24fd7682 100644 --- a/lib/collections/spotify_markets.dart +++ b/lib/collections/spotify_markets.dart @@ -1,188 +1,187 @@ // Country Codes contributed by 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)"), ]; diff --git a/lib/components/library/playlist_generate/seeds_multi_autocomplete.dart b/lib/components/library/playlist_generate/seeds_multi_autocomplete.dart index cb6b0cf75..b1665d326 100644 --- a/lib/components/library/playlist_generate/seeds_multi_autocomplete.dart +++ b/lib/components/library/playlist_generate/seeds_multi_autocomplete.dart @@ -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 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( - 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( + 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), diff --git a/lib/pages/library/playlist_generate/playlist_generate.dart b/lib/pages/library/playlist_generate/playlist_generate.dart index f3a4473dd..1bc8b23de 100644 --- a/lib/pages/library/playlist_generate/playlist_generate.dart +++ b/lib/pages/library/playlist_generate/playlist_generate.dart @@ -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( + 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( + 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( + 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( + 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( + 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( + 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( - 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( - 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( - 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( - 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, + const SizedBox(height: 16), + if (constrains.mdAndUp) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: countrySelector, ), - ), - ), - 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, + const SizedBox(width: 16), + Expanded( + child: genreSelector, ), - ), - ), - label: Text(artist.name!), - onDeleted: () { - artists.value = [ - ...artists.value - ..removeWhere((element) => element.id == artist.id) - ]; - }, - ), - ), - const SizedBox(height: 16), - SeedsMultiAutocomplete( - 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( - 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, + ], + ) + else ...[ + countrySelector, + const SizedBox(height: 16), + genreSelector, + ], + const SizedBox(height: 16), + if (constrains.mdAndUp) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: artistAutoComplete, ), - ), - ), - 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: 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, - ); - }, - ), - ], - ), + ], + ); + }), ), ); } diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index de0454c93..0ba031ee6 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -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(),