feat(buscar): add discovery landing state, filter pills, counter, and sort
This commit is contained in:
@@ -458,6 +458,7 @@ class EstadoRadio extends ChangeNotifier {
|
||||
_ordenListas = switch (raw) {
|
||||
'nombre' => OrdenEmisoras.nombre,
|
||||
'calidad' => OrdenEmisoras.calidad,
|
||||
'popularidad' => OrdenEmisoras.popularidad,
|
||||
_ => OrdenEmisoras.calidad,
|
||||
};
|
||||
}
|
||||
@@ -874,6 +875,7 @@ class EstadoRadio extends ChangeNotifier {
|
||||
_ordenListas = switch (ordenRaw) {
|
||||
'nombre' => OrdenEmisoras.nombre,
|
||||
'calidad' => OrdenEmisoras.calidad,
|
||||
'popularidad' => OrdenEmisoras.popularidad,
|
||||
_ => OrdenEmisoras.calidad,
|
||||
};
|
||||
await prefs.setString(_keyOrdenListas, _ordenListas.name);
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import '../modelos/emisora.dart';
|
||||
|
||||
/// User-selectable ordering for every station list in the app.
|
||||
enum OrdenEmisoras { nombre, calidad }
|
||||
///
|
||||
/// WU6 adds [popularidad] to the Buscar "Ordenar" control (design ADR-4),
|
||||
/// backed by fields the model already carries (`votes`, `clickcount`) —
|
||||
/// no new API surface, no server-side `order` parameter.
|
||||
enum OrdenEmisoras { nombre, calidad, popularidad }
|
||||
|
||||
/// Returns a sorted COPY of [emisoras] according to [orden].
|
||||
List<Emisora> ordenarEmisoras(List<Emisora> emisoras, OrdenEmisoras orden) {
|
||||
@@ -17,6 +21,12 @@ List<Emisora> ordenarEmisoras(List<Emisora> emisoras, OrdenEmisoras orden) {
|
||||
if (porBitrate != 0) return porBitrate;
|
||||
return 0;
|
||||
});
|
||||
case OrdenEmisoras.popularidad:
|
||||
ordenadas.sort((a, b) {
|
||||
final porVotos = b.votes.compareTo(a.votes);
|
||||
if (porVotos != 0) return porVotos;
|
||||
return b.clickcount.compareTo(a.clickcount);
|
||||
});
|
||||
}
|
||||
return ordenadas;
|
||||
}
|
||||
|
||||
@@ -235,6 +235,7 @@
|
||||
"stationOrderTitle": "Station order",
|
||||
"stationOrderByName": "By name",
|
||||
"stationOrderByQuality": "By quality",
|
||||
"stationOrderByPopularity": "By popularity",
|
||||
"stationOrderScopeDescription": "Applies to favorites, searches, nearby stations and quick lists.",
|
||||
"favoriteGroupsTitle": "Favorite lists",
|
||||
"favoriteGroupsDescription": "Create short lists to organize your saved stations.",
|
||||
@@ -306,6 +307,8 @@
|
||||
"searchNoResultsTitle": "No results",
|
||||
"searchEmptySubtitle": "Use the top bar or chips to discover stations from around the world.",
|
||||
"searchNoResultsSubtitle": "Try removing filters or typing another name to find an active station.",
|
||||
"searchResultsCount": "{count, plural, =1{1 result} other{{count} results}}",
|
||||
"searchClearFiltersAction": "{count, plural, =1{Clear filter} other{Clear {count} filters}}",
|
||||
"countrySpain": "Spain",
|
||||
"countryUsa": "USA",
|
||||
"countryMexico": "Mexico",
|
||||
|
||||
@@ -235,6 +235,7 @@
|
||||
"stationOrderTitle": "Orden de emisoras",
|
||||
"stationOrderByName": "Por nombre",
|
||||
"stationOrderByQuality": "Por calidad",
|
||||
"stationOrderByPopularity": "Por popularidad",
|
||||
"stationOrderScopeDescription": "Se aplica a favoritos, búsquedas, emisoras cercanas y listados rápidos.",
|
||||
"favoriteGroupsTitle": "Listas de favoritos",
|
||||
"favoriteGroupsDescription": "Creá listas cortas para organizar tus emisoras guardadas.",
|
||||
@@ -306,6 +307,8 @@
|
||||
"searchNoResultsTitle": "Sin resultados",
|
||||
"searchEmptySubtitle": "Usá la barra superior o los chips para descubrir señales de todo el mundo.",
|
||||
"searchNoResultsSubtitle": "Probá quitar filtros o escribir otro nombre para encontrar una señal activa.",
|
||||
"searchResultsCount": "{count, plural, =1{1 resultado} other{{count} resultados}}",
|
||||
"searchClearFiltersAction": "{count, plural, =1{Quitar el filtro} other{Quitar los {count} filtros}}",
|
||||
"countrySpain": "España",
|
||||
"countryUsa": "EE. UU.",
|
||||
"countryMexico": "México",
|
||||
|
||||
@@ -892,6 +892,12 @@ abstract class AppLocalizations {
|
||||
/// **'Por calidad'**
|
||||
String get stationOrderByQuality;
|
||||
|
||||
/// No description provided for @stationOrderByPopularity.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Por popularidad'**
|
||||
String get stationOrderByPopularity;
|
||||
|
||||
/// No description provided for @stationOrderScopeDescription.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
@@ -1132,6 +1138,18 @@ abstract class AppLocalizations {
|
||||
/// **'Probá quitar filtros o escribir otro nombre para encontrar una señal activa.'**
|
||||
String get searchNoResultsSubtitle;
|
||||
|
||||
/// No description provided for @searchResultsCount.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'{count, plural, =1{1 resultado} other{{count} resultados}}'**
|
||||
String searchResultsCount(num count);
|
||||
|
||||
/// No description provided for @searchClearFiltersAction.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'{count, plural, =1{Quitar el filtro} other{Quitar los {count} filtros}}'**
|
||||
String searchClearFiltersAction(num count);
|
||||
|
||||
/// No description provided for @countrySpain.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
|
||||
@@ -447,6 +447,9 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
@override
|
||||
String get stationOrderByQuality => 'حسب الجودة';
|
||||
|
||||
@override
|
||||
String get stationOrderByPopularity => 'Por popularidad';
|
||||
|
||||
@override
|
||||
String get stationOrderScopeDescription =>
|
||||
'ينطبق على المفضلات وعمليات البحث والمحطات القريبة والقوائم السريعة.';
|
||||
@@ -586,6 +589,28 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
String get searchNoResultsSubtitle =>
|
||||
'جرّب إزالة الفلاتر أو كتابة اسم آخر للعثور على إشارة نشطة.';
|
||||
|
||||
@override
|
||||
String searchResultsCount(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count resultados',
|
||||
one: '1 resultado',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String searchClearFiltersAction(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Quitar los $count filtros',
|
||||
one: 'Quitar el filtro',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get countrySpain => 'إسبانيا';
|
||||
|
||||
|
||||
@@ -450,6 +450,9 @@ class AppLocalizationsBn extends AppLocalizations {
|
||||
@override
|
||||
String get stationOrderByQuality => 'গুণমান অনুযায়ী';
|
||||
|
||||
@override
|
||||
String get stationOrderByPopularity => 'Por popularidad';
|
||||
|
||||
@override
|
||||
String get stationOrderScopeDescription =>
|
||||
'প্রিয়, অনুসন্ধান, কাছাকাছি স্টেশন এবং দ্রুত তালিকায় প্রযোজ্য।';
|
||||
@@ -590,6 +593,28 @@ class AppLocalizationsBn extends AppLocalizations {
|
||||
String get searchNoResultsSubtitle =>
|
||||
'সক্রিয় সিগন্যাল পেতে ফিল্টার সরিয়ে বা অন্য নাম লিখে দেখুন।';
|
||||
|
||||
@override
|
||||
String searchResultsCount(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count resultados',
|
||||
one: '1 resultado',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String searchClearFiltersAction(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Quitar los $count filtros',
|
||||
one: 'Quitar el filtro',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get countrySpain => 'স্পেন';
|
||||
|
||||
|
||||
@@ -453,6 +453,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get stationOrderByQuality => 'Nach Qualität';
|
||||
|
||||
@override
|
||||
String get stationOrderByPopularity => 'Por popularidad';
|
||||
|
||||
@override
|
||||
String get stationOrderScopeDescription =>
|
||||
'Gilt für Favoriten, Suchen, nahegelegene Sender und Schnelllisten.';
|
||||
@@ -593,6 +596,28 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get searchNoResultsSubtitle =>
|
||||
'Versuche, Filter zu entfernen oder einen anderen Namen einzugeben, um einen aktiven Sender zu finden.';
|
||||
|
||||
@override
|
||||
String searchResultsCount(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count resultados',
|
||||
one: '1 resultado',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String searchClearFiltersAction(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Quitar los $count filtros',
|
||||
one: 'Quitar el filtro',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get countrySpain => 'Spanien';
|
||||
|
||||
|
||||
@@ -447,6 +447,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get stationOrderByQuality => 'By quality';
|
||||
|
||||
@override
|
||||
String get stationOrderByPopularity => 'By popularity';
|
||||
|
||||
@override
|
||||
String get stationOrderScopeDescription =>
|
||||
'Applies to favorites, searches, nearby stations and quick lists.';
|
||||
@@ -587,6 +590,28 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get searchNoResultsSubtitle =>
|
||||
'Try removing filters or typing another name to find an active station.';
|
||||
|
||||
@override
|
||||
String searchResultsCount(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count results',
|
||||
one: '1 result',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String searchClearFiltersAction(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Clear $count filters',
|
||||
one: 'Clear filter',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get countrySpain => 'Spain';
|
||||
|
||||
|
||||
@@ -451,6 +451,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get stationOrderByQuality => 'Por calidad';
|
||||
|
||||
@override
|
||||
String get stationOrderByPopularity => 'Por popularidad';
|
||||
|
||||
@override
|
||||
String get stationOrderScopeDescription =>
|
||||
'Se aplica a favoritos, búsquedas, emisoras cercanas y listados rápidos.';
|
||||
@@ -591,6 +594,28 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get searchNoResultsSubtitle =>
|
||||
'Probá quitar filtros o escribir otro nombre para encontrar una señal activa.';
|
||||
|
||||
@override
|
||||
String searchResultsCount(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count resultados',
|
||||
one: '1 resultado',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String searchClearFiltersAction(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Quitar los $count filtros',
|
||||
one: 'Quitar el filtro',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get countrySpain => 'España';
|
||||
|
||||
|
||||
@@ -455,6 +455,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get stationOrderByQuality => 'Par qualité';
|
||||
|
||||
@override
|
||||
String get stationOrderByPopularity => 'Por popularidad';
|
||||
|
||||
@override
|
||||
String get stationOrderScopeDescription =>
|
||||
'S’applique aux favoris, recherches, stations proches et listes rapides.';
|
||||
@@ -595,6 +598,28 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get searchNoResultsSubtitle =>
|
||||
'Essayez de retirer des filtres ou de saisir un autre nom pour trouver une station active.';
|
||||
|
||||
@override
|
||||
String searchResultsCount(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count resultados',
|
||||
one: '1 resultado',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String searchClearFiltersAction(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Quitar los $count filtros',
|
||||
one: 'Quitar el filtro',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get countrySpain => 'Espagne';
|
||||
|
||||
|
||||
@@ -448,6 +448,9 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
@override
|
||||
String get stationOrderByQuality => 'गुणवत्ता से';
|
||||
|
||||
@override
|
||||
String get stationOrderByPopularity => 'Por popularidad';
|
||||
|
||||
@override
|
||||
String get stationOrderScopeDescription =>
|
||||
'पसंदीदा, खोज, पास के स्टेशन और त्वरित सूचियों पर लागू होता है।';
|
||||
@@ -588,6 +591,28 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
String get searchNoResultsSubtitle =>
|
||||
'सक्रिय सिग्नल पाने के लिए फ़िल्टर हटाएँ या कोई दूसरा नाम लिखें।';
|
||||
|
||||
@override
|
||||
String searchResultsCount(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count resultados',
|
||||
one: '1 resultado',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String searchClearFiltersAction(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Quitar los $count filtros',
|
||||
one: 'Quitar el filtro',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get countrySpain => 'स्पेन';
|
||||
|
||||
|
||||
@@ -449,6 +449,9 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get stationOrderByQuality => 'Berdasarkan kualitas';
|
||||
|
||||
@override
|
||||
String get stationOrderByPopularity => 'Por popularidad';
|
||||
|
||||
@override
|
||||
String get stationOrderScopeDescription =>
|
||||
'Berlaku untuk favorit, pencarian, stasiun terdekat, dan daftar cepat.';
|
||||
@@ -589,6 +592,28 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
String get searchNoResultsSubtitle =>
|
||||
'Coba hapus filter atau tulis nama lain untuk menemukan sinyal aktif.';
|
||||
|
||||
@override
|
||||
String searchResultsCount(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count resultados',
|
||||
one: '1 resultado',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String searchClearFiltersAction(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Quitar los $count filtros',
|
||||
one: 'Quitar el filtro',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get countrySpain => 'Spanyol';
|
||||
|
||||
|
||||
@@ -451,6 +451,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get stationOrderByQuality => 'Per qualità';
|
||||
|
||||
@override
|
||||
String get stationOrderByPopularity => 'Por popularidad';
|
||||
|
||||
@override
|
||||
String get stationOrderScopeDescription =>
|
||||
'Si applica a preferiti, ricerche, emittenti vicine ed elenchi rapidi.';
|
||||
@@ -591,6 +594,28 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get searchNoResultsSubtitle =>
|
||||
'Prova a rimuovere i filtri o a digitare un altro nome per trovare un\'emittente attiva.';
|
||||
|
||||
@override
|
||||
String searchResultsCount(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count resultados',
|
||||
one: '1 resultado',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String searchClearFiltersAction(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Quitar los $count filtros',
|
||||
one: 'Quitar el filtro',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get countrySpain => 'Spagna';
|
||||
|
||||
|
||||
@@ -438,6 +438,9 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get stationOrderByQuality => '品質順';
|
||||
|
||||
@override
|
||||
String get stationOrderByPopularity => 'Por popularidad';
|
||||
|
||||
@override
|
||||
String get stationOrderScopeDescription => 'お気に入り、検索、近くの局、クイック一覧に適用されます。';
|
||||
|
||||
@@ -568,6 +571,28 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get searchNoResultsSubtitle => '有効な電波を見つけるには、フィルターを外すか別の名前を入力してください。';
|
||||
|
||||
@override
|
||||
String searchResultsCount(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count resultados',
|
||||
one: '1 resultado',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String searchClearFiltersAction(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Quitar los $count filtros',
|
||||
one: 'Quitar el filtro',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get countrySpain => 'スペイン';
|
||||
|
||||
|
||||
@@ -450,6 +450,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get stationOrderByQuality => 'Por qualidade';
|
||||
|
||||
@override
|
||||
String get stationOrderByPopularity => 'Por popularidad';
|
||||
|
||||
@override
|
||||
String get stationOrderScopeDescription =>
|
||||
'Aplica-se a favoritos, buscas, estações próximas e listas rápidas.';
|
||||
@@ -590,6 +593,28 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get searchNoResultsSubtitle =>
|
||||
'Tente remover filtros ou digitar outro nome para encontrar uma estação ativa.';
|
||||
|
||||
@override
|
||||
String searchResultsCount(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count resultados',
|
||||
one: '1 resultado',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String searchClearFiltersAction(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Quitar los $count filtros',
|
||||
one: 'Quitar el filtro',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get countrySpain => 'Espanha';
|
||||
|
||||
|
||||
@@ -451,6 +451,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get stationOrderByQuality => 'По качеству';
|
||||
|
||||
@override
|
||||
String get stationOrderByPopularity => 'Por popularidad';
|
||||
|
||||
@override
|
||||
String get stationOrderScopeDescription =>
|
||||
'Применяется к избранному, поиску, ближайшим станциям и быстрым спискам.';
|
||||
@@ -591,6 +594,28 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get searchNoResultsSubtitle =>
|
||||
'Попробуйте убрать фильтры или ввести другое название, чтобы найти активный сигнал.';
|
||||
|
||||
@override
|
||||
String searchResultsCount(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count resultados',
|
||||
one: '1 resultado',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String searchClearFiltersAction(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Quitar los $count filtros',
|
||||
one: 'Quitar el filtro',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get countrySpain => 'Испания';
|
||||
|
||||
|
||||
@@ -436,6 +436,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get stationOrderByQuality => '按质量';
|
||||
|
||||
@override
|
||||
String get stationOrderByPopularity => 'Por popularidad';
|
||||
|
||||
@override
|
||||
String get stationOrderScopeDescription => '适用于收藏、搜索、附近电台和快捷列表。';
|
||||
|
||||
@@ -566,6 +569,28 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get searchNoResultsSubtitle => '尝试减少筛选条件,或换个名称搜索,找到正在播出的电台。';
|
||||
|
||||
@override
|
||||
String searchResultsCount(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count resultados',
|
||||
one: '1 resultado',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String searchClearFiltersAction(num count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Quitar los $count filtros',
|
||||
one: 'Quitar el filtro',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get countrySpain => '西班牙';
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shimmer/shimmer.dart' as shimmer;
|
||||
|
||||
import '../estado/estado_busqueda.dart';
|
||||
import '../tema/pluri_animate.dart';
|
||||
import '../estado/estado_radio.dart';
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../modelos/emisora.dart';
|
||||
import '../tema/pluri_animate.dart';
|
||||
import '../widgets/pluri_glass_surface.dart';
|
||||
import '../widgets/pluri_icon.dart';
|
||||
import '../widgets/pluri_layout.dart';
|
||||
@@ -37,6 +40,28 @@ const _idiomas = [
|
||||
('russian', 'languageNameRussian'),
|
||||
];
|
||||
|
||||
const _calidades = [
|
||||
('64 kbps', 64),
|
||||
('96 kbps', 96),
|
||||
('128 kbps', 128),
|
||||
('192 kbps', 192),
|
||||
('320 kbps', 320),
|
||||
];
|
||||
|
||||
/// WU6, `station-discovery-browse` spec: Buscar now owns BOTH the discovery
|
||||
/// landing state (relocated from `PantallaInicio`, task 6.5 — `_seccionCercanas`,
|
||||
/// `_seccionTendencias`, `_chipGeneros`, `_errorBanner` and the browse grid
|
||||
/// all moved here verbatim and were deleted from `pantalla_inicio.dart`) and
|
||||
/// the free-text/filtered search results view. [_hayBusquedaActiva] is the
|
||||
/// single switch between the two: empty query AND no active filter shows
|
||||
/// discovery content; any of the three flips to the results view.
|
||||
///
|
||||
/// Design correction, documented like WU3a's own task 3a.1: the spec's
|
||||
/// landing-state scenario also lists a "Países entry point", but no such
|
||||
/// affordance existed anywhere in `pantalla_inicio.dart` to relocate —
|
||||
/// `PantallaPaises` itself doesn't exist until WU7. That entry point is
|
||||
/// added in WU7 alongside the screen it targets (design's own component
|
||||
/// inventory: "Países (WU7)"), not invented here pointing nowhere.
|
||||
class PantallaBuscar extends StatefulWidget {
|
||||
const PantallaBuscar({super.key});
|
||||
|
||||
@@ -50,6 +75,33 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
String? _idiomaSeleccionado;
|
||||
int? _calidadMinima;
|
||||
|
||||
// Relocated from PantallaInicio (task 6.5) — genre-chip selection only
|
||||
// drives the LANDING grid, same as it always did on Inicio; it is
|
||||
// orthogonal to the free-text/filter search flow below.
|
||||
static const _generos = [
|
||||
'pop',
|
||||
'rock',
|
||||
'jazz',
|
||||
'classical',
|
||||
'electronic',
|
||||
'news',
|
||||
'talk',
|
||||
'hip-hop',
|
||||
'country',
|
||||
'metal',
|
||||
'reggae',
|
||||
'latin',
|
||||
];
|
||||
String? _generoSeleccionado;
|
||||
|
||||
int get _filtrosActivosCount =>
|
||||
(_paisSeleccionado != null ? 1 : 0) +
|
||||
(_idiomaSeleccionado != null ? 1 : 0) +
|
||||
(_calidadMinima != null ? 1 : 0);
|
||||
|
||||
bool get _hayBusquedaActiva =>
|
||||
_controller.text.trim().isNotEmpty || _filtrosActivosCount > 0;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
@@ -66,6 +118,15 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
);
|
||||
}
|
||||
|
||||
void _quitarTodosLosFiltros() {
|
||||
setState(() {
|
||||
_paisSeleccionado = null;
|
||||
_idiomaSeleccionado = null;
|
||||
_calidadMinima = null;
|
||||
});
|
||||
_buscar();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// S4-R3/S4-R5: this screen depends only on search state, so it watches
|
||||
@@ -81,9 +142,12 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
title: l10n.searchScreenTitle,
|
||||
subtitle: l10n.searchScreenSubtitle,
|
||||
glyph: PluriIconGlyph.search,
|
||||
trailing: PluriStatusPill(
|
||||
icon: Icons.tune_rounded,
|
||||
label: l10n.searchFiltersLabel,
|
||||
trailing: GestureDetector(
|
||||
onTap: _abrirFiltros,
|
||||
child: PluriStatusPill(
|
||||
icon: Icons.tune_rounded,
|
||||
label: l10n.searchFiltersLabel,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
@@ -119,44 +183,225 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
),
|
||||
),
|
||||
),
|
||||
_seccionFiltro(
|
||||
l10n.searchCountryFilterLabel,
|
||||
_paises.map((p) => (_countryLabel(l10n, p.$1), p.$2)).toList(),
|
||||
_paisSeleccionado,
|
||||
(v) {
|
||||
setState(() => _paisSeleccionado = v);
|
||||
_buscar();
|
||||
},
|
||||
),
|
||||
_seccionFiltro(
|
||||
l10n.searchLanguageFilterLabel,
|
||||
_idiomas.map((i) => (_languageLabel(l10n, i.$2), i.$1)).toList(),
|
||||
_idiomaSeleccionado,
|
||||
(v) {
|
||||
setState(() => _idiomaSeleccionado = v);
|
||||
_buscar();
|
||||
},
|
||||
),
|
||||
_seccionFiltroInt(
|
||||
l10n.searchMinQualityFilterLabel,
|
||||
const [
|
||||
('64 kbps', 64),
|
||||
('96 kbps', 96),
|
||||
('128 kbps', 128),
|
||||
('192 kbps', 192),
|
||||
('320 kbps', 320),
|
||||
],
|
||||
_calidadMinima,
|
||||
(v) {
|
||||
setState(() => _calidadMinima = v);
|
||||
_buscar();
|
||||
},
|
||||
),
|
||||
_resultados(estado, theme),
|
||||
if (_hayBusquedaActiva) ...[
|
||||
_barraFiltrosActivos(context, estado, theme),
|
||||
_resultados(estado, theme),
|
||||
] else ...[
|
||||
_seccionCercanas(context, theme, l10n),
|
||||
_seccionTendencias(context, theme, l10n),
|
||||
_chipGeneros(context, theme, l10n),
|
||||
if (context.select<EstadoRadio, String?>((e) => e.error) != null)
|
||||
_errorBanner(
|
||||
context,
|
||||
context.select<EstadoRadio, String?>((e) => e.error)!,
|
||||
theme,
|
||||
l10n,
|
||||
),
|
||||
_gridEmisoras(context, l10n),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ── Active-filter pills, results counter, sort (task 6.6/6.7) ──────────
|
||||
|
||||
Widget _barraFiltrosActivos(
|
||||
BuildContext context,
|
||||
EstadoBusqueda estado,
|
||||
ThemeData theme,
|
||||
) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final pills = <Widget>[
|
||||
if (_paisSeleccionado != null)
|
||||
_pillFiltro(
|
||||
_paisLabelSeleccionado(l10n) ?? _paisSeleccionado!,
|
||||
() {
|
||||
setState(() => _paisSeleccionado = null);
|
||||
_buscar();
|
||||
},
|
||||
),
|
||||
if (_idiomaSeleccionado != null)
|
||||
_pillFiltro(
|
||||
_idiomaLabelSeleccionado(l10n) ?? _idiomaSeleccionado!,
|
||||
() {
|
||||
setState(() => _idiomaSeleccionado = null);
|
||||
_buscar();
|
||||
},
|
||||
),
|
||||
if (_calidadMinima != null)
|
||||
_pillFiltro('≥$_calidadMinima kbps', () {
|
||||
setState(() => _calidadMinima = null);
|
||||
_buscar();
|
||||
}),
|
||||
];
|
||||
|
||||
if (pills.isEmpty && estado.resultados.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.horizontal,
|
||||
10,
|
||||
PluriLayout.horizontal,
|
||||
0,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (pills.isNotEmpty) Wrap(spacing: 8, runSpacing: 8, children: pills),
|
||||
if (pills.isNotEmpty && estado.resultados.isNotEmpty)
|
||||
const SizedBox(height: 10),
|
||||
if (!estado.cargando && estado.resultados.isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
l10n.searchResultsCount(estado.resultados.length),
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
PopupMenuButton<OrdenEmisoras>(
|
||||
icon: const Icon(Icons.swap_vert_rounded),
|
||||
tooltip: l10n.stationOrderTitle,
|
||||
onSelected:
|
||||
(criterio) => context
|
||||
.read<EstadoRadio>()
|
||||
.cambiarOrdenListas(criterio),
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
value: OrdenEmisoras.nombre,
|
||||
child: Text(l10n.stationOrderByName),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: OrdenEmisoras.calidad,
|
||||
child: Text(l10n.stationOrderByQuality),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: OrdenEmisoras.popularidad,
|
||||
child: Text(l10n.stationOrderByPopularity),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _pillFiltro(String label, VoidCallback onDeleted) {
|
||||
return Chip(
|
||||
label: Text(label),
|
||||
onDeleted: onDeleted,
|
||||
deleteIcon: const Icon(Icons.close, size: 18),
|
||||
visualDensity: VisualDensity.compact,
|
||||
);
|
||||
}
|
||||
|
||||
String? _paisLabelSeleccionado(AppLocalizations l10n) {
|
||||
for (final p in _paises) {
|
||||
if (p.$2 == _paisSeleccionado) return _countryLabel(l10n, p.$1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _idiomaLabelSeleccionado(AppLocalizations l10n) {
|
||||
for (final i in _idiomas) {
|
||||
if (i.$1 == _idiomaSeleccionado) return _languageLabel(l10n, i.$2);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Opens the filter picker as a bottom sheet (task 6.6 — "collapse the 3
|
||||
/// always-visible FilterChip rows into ... bottom-sheet pickers"). Each
|
||||
/// chip selection applies immediately and closes the sheet, mirroring the
|
||||
/// established "tap once, sheet closes" precedent already used by
|
||||
/// `pantalla_favoritos.dart`'s `_FilaFavorito._asignar` (WU4) — this
|
||||
/// avoids needing a `StatefulBuilder` to keep the sheet's own chip
|
||||
/// selection visually live while it stays open.
|
||||
Future<void> _abrirFiltros() async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(ctx) => SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.horizontal,
|
||||
4,
|
||||
PluriLayout.horizontal,
|
||||
0,
|
||||
),
|
||||
child: Text(
|
||||
l10n.searchFiltersLabel,
|
||||
style: Theme.of(ctx).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
_seccionFiltro(
|
||||
l10n.searchCountryFilterLabel,
|
||||
_paises
|
||||
.map((p) => (_countryLabel(l10n, p.$1), p.$2))
|
||||
.toList(),
|
||||
_paisSeleccionado,
|
||||
(v) {
|
||||
setState(() => _paisSeleccionado = v);
|
||||
_buscar();
|
||||
Navigator.of(ctx).pop();
|
||||
},
|
||||
),
|
||||
_seccionFiltro(
|
||||
l10n.searchLanguageFilterLabel,
|
||||
_idiomas
|
||||
.map((i) => (_languageLabel(l10n, i.$2), i.$1))
|
||||
.toList(),
|
||||
_idiomaSeleccionado,
|
||||
(v) {
|
||||
setState(() => _idiomaSeleccionado = v);
|
||||
_buscar();
|
||||
Navigator.of(ctx).pop();
|
||||
},
|
||||
),
|
||||
_seccionFiltroInt(
|
||||
l10n.searchMinQualityFilterLabel,
|
||||
_calidades,
|
||||
_calidadMinima,
|
||||
(v) {
|
||||
setState(() => _calidadMinima = v);
|
||||
_buscar();
|
||||
Navigator.of(ctx).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Renders every option as a `Wrap` of `FilterChip`s — deliberately NOT a
|
||||
/// horizontal `ListView` (the shape this used to have when these rows were
|
||||
/// always-visible inline strips, before task 6.6 moved them into this
|
||||
/// bottom sheet). Reason found at apply time: a horizontal `ListView` is
|
||||
/// lazily built by viewport, and nested inside the sheet's
|
||||
/// `SingleChildScrollView` its viewport-based build only ever realised the
|
||||
/// first few chips per row (confirmed empirically — country's 10 options
|
||||
/// built, but quality's 5th option, "320 kbps", silently never did). A
|
||||
/// `Wrap` lays out every child eagerly, has no lazy-build boundary, and
|
||||
/// suits a bottom sheet (vertical room to spare) better than a horizontal
|
||||
/// scroll strip anyway.
|
||||
Widget _seccionFiltro(
|
||||
String titulo,
|
||||
List<(String, String)> opciones,
|
||||
@@ -183,23 +428,21 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: opciones.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (_, i) {
|
||||
final (label, value) = opciones[i];
|
||||
final sel = seleccionado == value;
|
||||
return FilterChip(
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final (label, value) in opciones)
|
||||
FilterChip(
|
||||
label: Text(label),
|
||||
selected: sel,
|
||||
selected: seleccionado == value,
|
||||
visualDensity: VisualDensity.compact,
|
||||
onSelected: (_) => onChanged(sel ? null : value),
|
||||
);
|
||||
},
|
||||
),
|
||||
onSelected:
|
||||
(_) => onChanged(
|
||||
seleccionado == value ? null : value,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -233,23 +476,21 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: opciones.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (_, i) {
|
||||
final (label, value) = opciones[i];
|
||||
final sel = seleccionado == value;
|
||||
return FilterChip(
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final (label, value) in opciones)
|
||||
FilterChip(
|
||||
label: Text(label),
|
||||
selected: sel,
|
||||
selected: seleccionado == value,
|
||||
visualDensity: VisualDensity.compact,
|
||||
onSelected: (_) => onChanged(sel ? null : value),
|
||||
);
|
||||
},
|
||||
),
|
||||
onSelected:
|
||||
(_) => onChanged(
|
||||
seleccionado == value ? null : value,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -278,20 +519,35 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
final resultados = estado.resultados;
|
||||
|
||||
if (resultados.isEmpty) {
|
||||
final sinFiltros =
|
||||
_controller.text.isEmpty &&
|
||||
_paisSeleccionado == null &&
|
||||
_idiomaSeleccionado == null;
|
||||
return SizedBox(
|
||||
height: 260,
|
||||
child: PluriEmptyState(
|
||||
glyph: PluriIconGlyph.search,
|
||||
title: sinFiltros ? l10n.searchEmptyTitle : l10n.searchNoResultsTitle,
|
||||
subtitle:
|
||||
sinFiltros
|
||||
? l10n.searchEmptySubtitle
|
||||
: l10n.searchNoResultsSubtitle,
|
||||
),
|
||||
final sinFiltros = _controller.text.isEmpty && _filtrosActivosCount == 0;
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 260,
|
||||
child: PluriEmptyState(
|
||||
glyph: PluriIconGlyph.search,
|
||||
title:
|
||||
sinFiltros ? l10n.searchEmptyTitle : l10n.searchNoResultsTitle,
|
||||
subtitle:
|
||||
sinFiltros
|
||||
? l10n.searchEmptySubtitle
|
||||
: l10n.searchNoResultsSubtitle,
|
||||
),
|
||||
),
|
||||
// task 6.6 / spec "One-Tap Clear-All-Filters on Empty Results":
|
||||
// only offered once 1+ pill-filters are active AND the search
|
||||
// came back empty — not merely "no query typed yet".
|
||||
if (_filtrosActivosCount > 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: OutlinedButton(
|
||||
onPressed: _quitarTodosLosFiltros,
|
||||
child: Text(
|
||||
l10n.searchClearFiltersAction(_filtrosActivosCount),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -354,4 +610,326 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
'languageNameRussian' => l10n.languageNameRussian,
|
||||
_ => key,
|
||||
};
|
||||
|
||||
// ── Discovery landing state (relocated from PantallaInicio, task 6.5) ──
|
||||
|
||||
Widget _seccionCercanas(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
AppLocalizations l10n,
|
||||
) {
|
||||
// Nearby stations live in EstadoBusqueda (S4-R3).
|
||||
final busqueda = context.watch<EstadoBusqueda>();
|
||||
final pais = busqueda.paisCercanoDetectado;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.horizontal,
|
||||
8,
|
||||
PluriLayout.horizontal,
|
||||
0,
|
||||
),
|
||||
child: PluriGlassSurface(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
pais == null ? l10n.nearYou : l10n.nearYouInCountry(pais),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed:
|
||||
busqueda.cargandoCercanas
|
||||
? null
|
||||
: busqueda.cargarEmisorasCercanas,
|
||||
icon:
|
||||
busqueda.cargandoCercanas
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.my_location_rounded, size: 18),
|
||||
label: Text(l10n.detectAction),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (busqueda.errorCercanas != null)
|
||||
Text(
|
||||
busqueda.errorCercanas!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
if (busqueda.cercanas.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 76,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: busqueda.cercanas.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (context, i) {
|
||||
final emisora = busqueda.cercanas[i];
|
||||
return SizedBox(
|
||||
width: 260,
|
||||
child: TarjetaEmisora(
|
||||
emisora: emisora,
|
||||
esCompacta: true,
|
||||
onTap: () => reproducirMinimizado(context, emisora),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _seccionTendencias(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
AppLocalizations l10n,
|
||||
) {
|
||||
final cargando = context.select<EstadoRadio, bool>(
|
||||
(e) => e.cargandoPopulares,
|
||||
);
|
||||
final tendencias = context.select<EstadoRadio, List<Emisora>>(
|
||||
(e) => e.tendencias,
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.horizontal,
|
||||
8,
|
||||
PluriLayout.horizontal,
|
||||
0,
|
||||
),
|
||||
child: PluriGlassSurface(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.liveRadar, style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 56,
|
||||
child:
|
||||
cargando
|
||||
? ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: 5,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (_, __) => _ChipShimmer(theme: theme),
|
||||
)
|
||||
: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: tendencias.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (context, i) {
|
||||
final e = tendencias[i];
|
||||
return ActionChip(
|
||||
avatar: const Icon(
|
||||
Icons.graphic_eq_rounded,
|
||||
size: 18,
|
||||
),
|
||||
label: Text(e.nombre, maxLines: 1),
|
||||
onPressed: () => reproducirMinimizado(context, e),
|
||||
).pluriFadeIn(
|
||||
context,
|
||||
delay: Duration(milliseconds: i * 50),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _chipGeneros(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
AppLocalizations l10n,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.horizontal,
|
||||
16,
|
||||
PluriLayout.horizontal,
|
||||
8,
|
||||
),
|
||||
child: PluriGlassSurface(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.genresTitle, style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children:
|
||||
_generos.map((g) {
|
||||
final seleccionado = _generoSeleccionado == g;
|
||||
return FilterChip(
|
||||
label: Text(_genreName(l10n, g)),
|
||||
selected: seleccionado,
|
||||
onSelected: (_) {
|
||||
setState(() {
|
||||
_generoSeleccionado = seleccionado ? null : g;
|
||||
});
|
||||
if (!seleccionado) {
|
||||
context.read<EstadoBusqueda>().buscar(tag: g);
|
||||
} else {
|
||||
context.read<EstadoRadio>().cargarPopulares();
|
||||
}
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _errorBanner(
|
||||
BuildContext context,
|
||||
String error,
|
||||
ThemeData theme,
|
||||
AppLocalizations l10n,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: PluriGlassSurface(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.wifi_off, color: theme.colorScheme.error),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(error)),
|
||||
TextButton(
|
||||
onPressed: () => context.read<EstadoRadio>().cargarPopulares(),
|
||||
child: Text(l10n.retryAction),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _gridEmisoras(BuildContext context, AppLocalizations l10n) {
|
||||
final porGenero = _generoSeleccionado != null;
|
||||
final emisoras =
|
||||
porGenero
|
||||
? context.select<EstadoBusqueda, List<Emisora>>((b) => b.resultados)
|
||||
: context.select<EstadoRadio, List<Emisora>>(
|
||||
(e) => e.emisorasInicio,
|
||||
);
|
||||
final cargando =
|
||||
context.select<EstadoRadio, bool>((e) => e.cargandoPopulares) ||
|
||||
(porGenero && context.select<EstadoBusqueda, bool>((b) => b.cargando));
|
||||
|
||||
if (cargando) {
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.horizontal,
|
||||
0,
|
||||
PluriLayout.horizontal,
|
||||
PluriLayout.compactGap,
|
||||
),
|
||||
itemCount: 12,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 0.78,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
itemBuilder: (_, __) => const TarjetaEmisoraShimmer(),
|
||||
);
|
||||
}
|
||||
|
||||
if (emisoras.isEmpty) {
|
||||
return SizedBox(
|
||||
height: 260,
|
||||
child: PluriEmptyState(
|
||||
glyph: PluriIconGlyph.home,
|
||||
title: l10n.noStationsAvailable,
|
||||
subtitle: l10n.noStationsAvailableSubtitle,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.horizontal,
|
||||
0,
|
||||
PluriLayout.horizontal,
|
||||
PluriLayout.compactGap,
|
||||
),
|
||||
itemCount: emisoras.length,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 0.78,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
itemBuilder:
|
||||
(context, i) => TarjetaEmisora(
|
||||
emisora: emisoras[i],
|
||||
onTap: () => reproducirMinimizado(context, emisoras[i]),
|
||||
).pluriFadeSlideIn(context, delay: Duration(milliseconds: i * 30), beginY: 0.1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _genreName(AppLocalizations l10n, String tag) => switch (tag) {
|
||||
'pop' => l10n.genrePop,
|
||||
'rock' => l10n.genreRock,
|
||||
'jazz' => l10n.genreJazz,
|
||||
'classical' => l10n.genreClassical,
|
||||
'electronic' => l10n.genreElectronic,
|
||||
'news' => l10n.genreNews,
|
||||
'talk' => l10n.genreTalk,
|
||||
'hip-hop' => l10n.genreHipHop,
|
||||
'country' => l10n.genreCountry,
|
||||
'metal' => l10n.genreMetal,
|
||||
'reggae' => l10n.genreReggae,
|
||||
'latin' => l10n.genreLatin,
|
||||
_ => tag,
|
||||
};
|
||||
|
||||
class _ChipShimmer extends StatelessWidget {
|
||||
final ThemeData theme;
|
||||
const _ChipShimmer({required this.theme});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return shimmer.Shimmer.fromColors(
|
||||
baseColor: theme.colorScheme.surfaceContainerHighest,
|
||||
highlightColor: theme.colorScheme.surface,
|
||||
child: Container(
|
||||
width: 120,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shimmer/shimmer.dart' as shimmer;
|
||||
|
||||
import '../estado/estado_busqueda.dart';
|
||||
import '../estado/estado_ecualizador.dart';
|
||||
import '../estado/estado_navegacion.dart';
|
||||
import '../estado/estado_radio.dart';
|
||||
@@ -11,10 +10,8 @@ import '../l10n/display_names.dart';
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../modelos/emisora.dart';
|
||||
import '../servicios/servicio_audio.dart';
|
||||
import '../tema/pluri_animate.dart';
|
||||
import '../tema/pluriwave_theme.dart';
|
||||
import '../widgets/pluri_glass_surface.dart';
|
||||
import '../widgets/pluri_icon.dart';
|
||||
import '../widgets/pluri_layout.dart';
|
||||
import '../widgets/pluri_premium_widgets.dart';
|
||||
import '../widgets/visualizador_audio.dart';
|
||||
@@ -32,22 +29,6 @@ class PantallaInicio extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PantallaInicioState extends State<PantallaInicio> {
|
||||
static const _generos = [
|
||||
'pop',
|
||||
'rock',
|
||||
'jazz',
|
||||
'classical',
|
||||
'electronic',
|
||||
'news',
|
||||
'talk',
|
||||
'hip-hop',
|
||||
'country',
|
||||
'metal',
|
||||
'reggae',
|
||||
'latin',
|
||||
];
|
||||
String? _generoSeleccionado;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// S4-R5: no root watch on EstadoRadio. Every field is consumed through
|
||||
@@ -55,41 +36,28 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
// (which notify EstadoRadio) no longer rebuild this screen.
|
||||
final theme = Theme.of(context);
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final error = context.select<EstadoRadio, String?>((e) => e.error);
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => context.read<EstadoRadio>().cargarPopulares(),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
// WU5: replaces the old PluriScreenHeader hero. The discovery
|
||||
// sections below (_seccionCercanas onward) are deliberately left
|
||||
// in place — WU6 relocates them to Buscar and deletes them from
|
||||
// here; removing them now would leave that content nowhere until
|
||||
// WU6 lands.
|
||||
const SliverToBoxAdapter(child: _EscucharHero()),
|
||||
SliverToBoxAdapter(child: _seccionTusEmisoras(context, theme, l10n)),
|
||||
SliverToBoxAdapter(child: _seccionCercanas(context, theme, l10n)),
|
||||
SliverToBoxAdapter(child: _seccionTendencias(context, theme, l10n)),
|
||||
SliverToBoxAdapter(child: _chipGeneros(context, theme, l10n)),
|
||||
if (error != null)
|
||||
SliverToBoxAdapter(
|
||||
child: _errorBanner(context, error, theme, l10n),
|
||||
),
|
||||
SliverPadding(
|
||||
// ADR-7(b): the mini player is hidden on this whole screen
|
||||
// (app.dart), so its content needs less bottom padding than
|
||||
// every other root — escucharBottomChromeInset, not the plain
|
||||
// bottomChromeInset every other root/scrollable uses.
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.horizontal,
|
||||
0,
|
||||
PluriLayout.horizontal,
|
||||
PluriLayout.escucharBottomChromeInset,
|
||||
),
|
||||
sliver: _gridEmisoras(context, l10n),
|
||||
),
|
||||
],
|
||||
),
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
// WU5 built the hero; WU6 relocated the discovery sections that
|
||||
// used to follow it (_seccionCercanas, _seccionTendencias,
|
||||
// _chipGeneros, _errorBanner, the browse grid) into
|
||||
// PantallaBuscar's landing state and DELETED them here (this WU's
|
||||
// own task 6.5 — completing WU5's task 5.9 deferral). Escuchar's
|
||||
// content is now just the hero and the favorites preview below;
|
||||
// pull-to-refresh was dropped along with the grid it refreshed —
|
||||
// the retry button that used to live in the (now relocated) error
|
||||
// banner already covers manual recovery on Buscar.
|
||||
const SliverToBoxAdapter(child: _EscucharHero()),
|
||||
SliverToBoxAdapter(child: _seccionTusEmisoras(context, theme, l10n)),
|
||||
// ADR-7(b): the mini player is hidden on this whole screen
|
||||
// (app.dart), so its content needs less bottom padding than every
|
||||
// other root — escucharBottomChromeInset, not the plain
|
||||
// bottomChromeInset every other root/scrollable uses.
|
||||
const SliverToBoxAdapter(
|
||||
child: SizedBox(height: PluriLayout.escucharBottomChromeInset),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -175,315 +143,6 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _seccionCercanas(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
AppLocalizations l10n,
|
||||
) {
|
||||
// Nearby stations live in EstadoBusqueda (S4-R3).
|
||||
final busqueda = context.watch<EstadoBusqueda>();
|
||||
final pais = busqueda.paisCercanoDetectado;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.horizontal,
|
||||
8,
|
||||
PluriLayout.horizontal,
|
||||
0,
|
||||
),
|
||||
child: PluriGlassSurface(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
pais == null ? l10n.nearYou : l10n.nearYouInCountry(pais),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed:
|
||||
busqueda.cargandoCercanas
|
||||
? null
|
||||
: busqueda.cargarEmisorasCercanas,
|
||||
icon:
|
||||
busqueda.cargandoCercanas
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.my_location_rounded, size: 18),
|
||||
label: Text(l10n.detectAction),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (busqueda.errorCercanas != null)
|
||||
Text(
|
||||
busqueda.errorCercanas!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
if (busqueda.cercanas.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 76,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: busqueda.cercanas.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (context, i) {
|
||||
final emisora = busqueda.cercanas[i];
|
||||
return SizedBox(
|
||||
width: 260,
|
||||
child: TarjetaEmisora(
|
||||
emisora: emisora,
|
||||
esCompacta: true,
|
||||
onTap: () => reproducirMinimizado(context, emisora),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _seccionTendencias(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
AppLocalizations l10n,
|
||||
) {
|
||||
final cargando = context.select<EstadoRadio, bool>(
|
||||
(e) => e.cargandoPopulares,
|
||||
);
|
||||
final tendencias = context.select<EstadoRadio, List<Emisora>>(
|
||||
(e) => e.tendencias,
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.horizontal,
|
||||
8,
|
||||
PluriLayout.horizontal,
|
||||
0,
|
||||
),
|
||||
child: PluriGlassSurface(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.liveRadar, style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 56,
|
||||
child:
|
||||
cargando
|
||||
? ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: 5,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (_, __) => _ChipShimmer(theme: theme),
|
||||
)
|
||||
: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: tendencias.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (context, i) {
|
||||
final e = tendencias[i];
|
||||
return ActionChip(
|
||||
avatar: const Icon(
|
||||
Icons.graphic_eq_rounded,
|
||||
size: 18,
|
||||
),
|
||||
label: Text(e.nombre, maxLines: 1),
|
||||
onPressed: () => reproducirMinimizado(context, e),
|
||||
).pluriFadeIn(
|
||||
context,
|
||||
delay: Duration(milliseconds: i * 50),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _chipGeneros(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
AppLocalizations l10n,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.horizontal,
|
||||
16,
|
||||
PluriLayout.horizontal,
|
||||
8,
|
||||
),
|
||||
child: PluriGlassSurface(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.genresTitle, style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children:
|
||||
_generos.map((g) {
|
||||
final seleccionado = _generoSeleccionado == g;
|
||||
return FilterChip(
|
||||
label: Text(_genreName(l10n, g)),
|
||||
selected: seleccionado,
|
||||
onSelected: (_) {
|
||||
setState(() {
|
||||
_generoSeleccionado = seleccionado ? null : g;
|
||||
});
|
||||
if (!seleccionado) {
|
||||
context.read<EstadoBusqueda>().buscar(tag: g);
|
||||
} else {
|
||||
context.read<EstadoRadio>().cargarPopulares();
|
||||
}
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _errorBanner(
|
||||
BuildContext context,
|
||||
String error,
|
||||
ThemeData theme,
|
||||
AppLocalizations l10n,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: PluriGlassSurface(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.wifi_off, color: theme.colorScheme.error),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(error)),
|
||||
TextButton(
|
||||
onPressed: () => context.read<EstadoRadio>().cargarPopulares(),
|
||||
child: Text(l10n.retryAction),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _gridEmisoras(BuildContext context, AppLocalizations l10n) {
|
||||
final porGenero = _generoSeleccionado != null;
|
||||
final emisoras =
|
||||
porGenero
|
||||
? context.select<EstadoBusqueda, List<Emisora>>((b) => b.resultados)
|
||||
: context.select<EstadoRadio, List<Emisora>>(
|
||||
(e) => e.emisorasInicio,
|
||||
);
|
||||
final cargando =
|
||||
context.select<EstadoRadio, bool>((e) => e.cargandoPopulares) ||
|
||||
(porGenero && context.select<EstadoBusqueda, bool>((b) => b.cargando));
|
||||
|
||||
if (cargando) {
|
||||
return SliverGrid(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(_, __) => const TarjetaEmisoraShimmer(),
|
||||
childCount: 12,
|
||||
),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 0.78,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (emisoras.isEmpty) {
|
||||
return SliverFillRemaining(
|
||||
child: PluriEmptyState(
|
||||
glyph: PluriIconGlyph.home,
|
||||
title: l10n.noStationsAvailable,
|
||||
subtitle: l10n.noStationsAvailableSubtitle,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverGrid(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, i) => TarjetaEmisora(
|
||||
emisora: emisoras[i],
|
||||
onTap: () => reproducirMinimizado(context, emisoras[i]),
|
||||
).pluriFadeSlideIn(
|
||||
context,
|
||||
delay: Duration(milliseconds: i * 30),
|
||||
beginY: 0.1,
|
||||
),
|
||||
childCount: emisoras.length,
|
||||
),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 0.78,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _genreName(AppLocalizations l10n, String tag) => switch (tag) {
|
||||
'pop' => l10n.genrePop,
|
||||
'rock' => l10n.genreRock,
|
||||
'jazz' => l10n.genreJazz,
|
||||
'classical' => l10n.genreClassical,
|
||||
'electronic' => l10n.genreElectronic,
|
||||
'news' => l10n.genreNews,
|
||||
'talk' => l10n.genreTalk,
|
||||
'hip-hop' => l10n.genreHipHop,
|
||||
'country' => l10n.genreCountry,
|
||||
'metal' => l10n.genreMetal,
|
||||
'reggae' => l10n.genreReggae,
|
||||
'latin' => l10n.genreLatin,
|
||||
_ => tag,
|
||||
};
|
||||
|
||||
class _ChipShimmer extends StatelessWidget {
|
||||
final ThemeData theme;
|
||||
const _ChipShimmer({required this.theme});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return shimmer.Shimmer.fromColors(
|
||||
baseColor: theme.colorScheme.surfaceContainerHighest,
|
||||
highlightColor: theme.colorScheme.surface,
|
||||
child: Container(
|
||||
width: 120,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// WU5, design ADR-7: the Escuchar embedded player. `EstadoRadio` is the
|
||||
|
||||
Reference in New Issue
Block a user