fix(buscar): rebuild the offline banner with offlineAccent
Audit 13.1 (t4 line 641): the banner rendered wifi_off in colorScheme.error (red) inside a generic glass card. Rebuilt using offlineAccent (#E8879A) — a token that existed since Tier 1 with no consumer — for the tint, border and icon, plus the prototype's title/ detail layout and a restyled Retry chip. New offlineBannerTitle key translated to all 13 locales.
This commit is contained in:
@@ -306,6 +306,7 @@
|
|||||||
"detectAction": "اكتشاف",
|
"detectAction": "اكتشاف",
|
||||||
"liveRadar": "رادار مباشر",
|
"liveRadar": "رادار مباشر",
|
||||||
"genresTitle": "الأنواع",
|
"genresTitle": "الأنواع",
|
||||||
|
"offlineBannerTitle": "لا يوجد اتصال",
|
||||||
"retryAction": "إعادة المحاولة",
|
"retryAction": "إعادة المحاولة",
|
||||||
"noStationsAvailable": "لا توجد محطات متاحة",
|
"noStationsAvailable": "لا توجد محطات متاحة",
|
||||||
"noStationsAvailableSubtitle": "جرّب التحديث أو اختيار نوع آخر لالتقاط الإشارة من جديد.",
|
"noStationsAvailableSubtitle": "جرّب التحديث أو اختيار نوع آخر لالتقاط الإشارة من جديد.",
|
||||||
|
|||||||
@@ -306,6 +306,7 @@
|
|||||||
"detectAction": "সনাক্ত করুন",
|
"detectAction": "সনাক্ত করুন",
|
||||||
"liveRadar": "লাইভ রাডার",
|
"liveRadar": "লাইভ রাডার",
|
||||||
"genresTitle": "ধরন",
|
"genresTitle": "ধরন",
|
||||||
|
"offlineBannerTitle": "সংযোগ নেই",
|
||||||
"retryAction": "আবার চেষ্টা করুন",
|
"retryAction": "আবার চেষ্টা করুন",
|
||||||
"noStationsAvailable": "কোনো স্টেশন উপলব্ধ নেই",
|
"noStationsAvailable": "কোনো স্টেশন উপলব্ধ নেই",
|
||||||
"noStationsAvailableSubtitle": "সিগন্যাল আবার ধরতে রিফ্রেশ করুন বা অন্য ধরন বেছে নিন।",
|
"noStationsAvailableSubtitle": "সিগন্যাল আবার ধরতে রিফ্রেশ করুন বা অন্য ধরন বেছে নিন।",
|
||||||
|
|||||||
@@ -306,6 +306,7 @@
|
|||||||
"detectAction": "Erkennen",
|
"detectAction": "Erkennen",
|
||||||
"liveRadar": "Live-Radar",
|
"liveRadar": "Live-Radar",
|
||||||
"genresTitle": "Genres",
|
"genresTitle": "Genres",
|
||||||
|
"offlineBannerTitle": "Keine Verbindung",
|
||||||
"retryAction": "Erneut versuchen",
|
"retryAction": "Erneut versuchen",
|
||||||
"noStationsAvailable": "Keine Sender verfügbar",
|
"noStationsAvailable": "Keine Sender verfügbar",
|
||||||
"noStationsAvailableSubtitle": "Versuche zu aktualisieren oder ein anderes Genre auszuwählen, um wieder ein Signal zu empfangen.",
|
"noStationsAvailableSubtitle": "Versuche zu aktualisieren oder ein anderes Genre auszuwählen, um wieder ein Signal zu empfangen.",
|
||||||
|
|||||||
@@ -369,6 +369,7 @@
|
|||||||
"detectAction": "Detect",
|
"detectAction": "Detect",
|
||||||
"liveRadar": "Live radar",
|
"liveRadar": "Live radar",
|
||||||
"genresTitle": "Genres",
|
"genresTitle": "Genres",
|
||||||
|
"offlineBannerTitle": "No connection",
|
||||||
"retryAction": "Retry",
|
"retryAction": "Retry",
|
||||||
"noStationsAvailable": "No stations available",
|
"noStationsAvailable": "No stations available",
|
||||||
"noStationsAvailableSubtitle": "Try refreshing or choosing another genre to capture a signal again.",
|
"noStationsAvailableSubtitle": "Try refreshing or choosing another genre to capture a signal again.",
|
||||||
|
|||||||
@@ -369,6 +369,7 @@
|
|||||||
"detectAction": "Detectar",
|
"detectAction": "Detectar",
|
||||||
"liveRadar": "Radar en directo",
|
"liveRadar": "Radar en directo",
|
||||||
"genresTitle": "Géneros",
|
"genresTitle": "Géneros",
|
||||||
|
"offlineBannerTitle": "Sin conexión",
|
||||||
"retryAction": "Reintentar",
|
"retryAction": "Reintentar",
|
||||||
"noStationsAvailable": "No hay emisoras disponibles",
|
"noStationsAvailable": "No hay emisoras disponibles",
|
||||||
"noStationsAvailableSubtitle": "Probá refrescar o elegir otro género para volver a capturar señal.",
|
"noStationsAvailableSubtitle": "Probá refrescar o elegir otro género para volver a capturar señal.",
|
||||||
|
|||||||
@@ -306,6 +306,7 @@
|
|||||||
"detectAction": "Détecter",
|
"detectAction": "Détecter",
|
||||||
"liveRadar": "Radar en direct",
|
"liveRadar": "Radar en direct",
|
||||||
"genresTitle": "Genres",
|
"genresTitle": "Genres",
|
||||||
|
"offlineBannerTitle": "Aucune connexion",
|
||||||
"retryAction": "Réessayer",
|
"retryAction": "Réessayer",
|
||||||
"noStationsAvailable": "Aucune station disponible",
|
"noStationsAvailable": "Aucune station disponible",
|
||||||
"noStationsAvailableSubtitle": "Essayez d’actualiser ou de choisir un autre genre pour capter à nouveau un signal.",
|
"noStationsAvailableSubtitle": "Essayez d’actualiser ou de choisir un autre genre pour capter à nouveau un signal.",
|
||||||
|
|||||||
@@ -306,6 +306,7 @@
|
|||||||
"detectAction": "पहचानें",
|
"detectAction": "पहचानें",
|
||||||
"liveRadar": "लाइव रडार",
|
"liveRadar": "लाइव रडार",
|
||||||
"genresTitle": "शैलियाँ",
|
"genresTitle": "शैलियाँ",
|
||||||
|
"offlineBannerTitle": "कोई कनेक्शन नहीं",
|
||||||
"retryAction": "फिर कोशिश करें",
|
"retryAction": "फिर कोशिश करें",
|
||||||
"noStationsAvailable": "कोई स्टेशन उपलब्ध नहीं",
|
"noStationsAvailable": "कोई स्टेशन उपलब्ध नहीं",
|
||||||
"noStationsAvailableSubtitle": "सिग्नल फिर पकड़ने के लिए रीफ़्रेश करें या दूसरी शैली चुनें।",
|
"noStationsAvailableSubtitle": "सिग्नल फिर पकड़ने के लिए रीफ़्रेश करें या दूसरी शैली चुनें।",
|
||||||
|
|||||||
@@ -306,6 +306,7 @@
|
|||||||
"detectAction": "Deteksi",
|
"detectAction": "Deteksi",
|
||||||
"liveRadar": "Radar langsung",
|
"liveRadar": "Radar langsung",
|
||||||
"genresTitle": "Genre",
|
"genresTitle": "Genre",
|
||||||
|
"offlineBannerTitle": "Tidak ada koneksi",
|
||||||
"retryAction": "Coba lagi",
|
"retryAction": "Coba lagi",
|
||||||
"noStationsAvailable": "Tidak ada stasiun tersedia",
|
"noStationsAvailable": "Tidak ada stasiun tersedia",
|
||||||
"noStationsAvailableSubtitle": "Coba segarkan atau pilih genre lain untuk menangkap sinyal kembali.",
|
"noStationsAvailableSubtitle": "Coba segarkan atau pilih genre lain untuk menangkap sinyal kembali.",
|
||||||
|
|||||||
@@ -306,6 +306,7 @@
|
|||||||
"detectAction": "Rileva",
|
"detectAction": "Rileva",
|
||||||
"liveRadar": "Radar live",
|
"liveRadar": "Radar live",
|
||||||
"genresTitle": "Generi",
|
"genresTitle": "Generi",
|
||||||
|
"offlineBannerTitle": "Nessuna connessione",
|
||||||
"retryAction": "Riprova",
|
"retryAction": "Riprova",
|
||||||
"noStationsAvailable": "Nessuna emittente disponibile",
|
"noStationsAvailable": "Nessuna emittente disponibile",
|
||||||
"noStationsAvailableSubtitle": "Prova ad aggiornare o a scegliere un altro genere per captare di nuovo un segnale.",
|
"noStationsAvailableSubtitle": "Prova ad aggiornare o a scegliere un altro genere per captare di nuovo un segnale.",
|
||||||
|
|||||||
@@ -306,6 +306,7 @@
|
|||||||
"detectAction": "検出",
|
"detectAction": "検出",
|
||||||
"liveRadar": "ライブレーダー",
|
"liveRadar": "ライブレーダー",
|
||||||
"genresTitle": "ジャンル",
|
"genresTitle": "ジャンル",
|
||||||
|
"offlineBannerTitle": "接続なし",
|
||||||
"retryAction": "再試行",
|
"retryAction": "再試行",
|
||||||
"noStationsAvailable": "利用できる局がありません",
|
"noStationsAvailable": "利用できる局がありません",
|
||||||
"noStationsAvailableSubtitle": "再読み込みするか別のジャンルを選んで、もう一度電波をつかまえてください。",
|
"noStationsAvailableSubtitle": "再読み込みするか別のジャンルを選んで、もう一度電波をつかまえてください。",
|
||||||
|
|||||||
@@ -306,6 +306,7 @@
|
|||||||
"detectAction": "Detectar",
|
"detectAction": "Detectar",
|
||||||
"liveRadar": "Radar ao vivo",
|
"liveRadar": "Radar ao vivo",
|
||||||
"genresTitle": "Gêneros",
|
"genresTitle": "Gêneros",
|
||||||
|
"offlineBannerTitle": "Sem conexão",
|
||||||
"retryAction": "Tentar novamente",
|
"retryAction": "Tentar novamente",
|
||||||
"noStationsAvailable": "Não há estações disponíveis",
|
"noStationsAvailable": "Não há estações disponíveis",
|
||||||
"noStationsAvailableSubtitle": "Tente atualizar ou escolher outro gênero para captar sinal novamente.",
|
"noStationsAvailableSubtitle": "Tente atualizar ou escolher outro gênero para captar sinal novamente.",
|
||||||
|
|||||||
@@ -306,6 +306,7 @@
|
|||||||
"detectAction": "Определить",
|
"detectAction": "Определить",
|
||||||
"liveRadar": "Радар в эфире",
|
"liveRadar": "Радар в эфире",
|
||||||
"genresTitle": "Жанры",
|
"genresTitle": "Жанры",
|
||||||
|
"offlineBannerTitle": "Нет подключения",
|
||||||
"retryAction": "Повторить",
|
"retryAction": "Повторить",
|
||||||
"noStationsAvailable": "Нет доступных станций",
|
"noStationsAvailable": "Нет доступных станций",
|
||||||
"noStationsAvailableSubtitle": "Попробуйте обновить или выбрать другой жанр, чтобы снова поймать сигнал.",
|
"noStationsAvailableSubtitle": "Попробуйте обновить или выбрать другой жанр, чтобы снова поймать сигнал.",
|
||||||
|
|||||||
@@ -306,6 +306,7 @@
|
|||||||
"detectAction": "检测",
|
"detectAction": "检测",
|
||||||
"liveRadar": "直播雷达",
|
"liveRadar": "直播雷达",
|
||||||
"genresTitle": "类型",
|
"genresTitle": "类型",
|
||||||
|
"offlineBannerTitle": "无连接",
|
||||||
"retryAction": "重试",
|
"retryAction": "重试",
|
||||||
"noStationsAvailable": "没有可用电台",
|
"noStationsAvailable": "没有可用电台",
|
||||||
"noStationsAvailableSubtitle": "尝试刷新或选择其他类型,重新捕捉信号。",
|
"noStationsAvailableSubtitle": "尝试刷新或选择其他类型,重新捕捉信号。",
|
||||||
|
|||||||
@@ -1438,6 +1438,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'Géneros'**
|
/// **'Géneros'**
|
||||||
String get genresTitle;
|
String get genresTitle;
|
||||||
|
|
||||||
|
/// No description provided for @offlineBannerTitle.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Sin conexión'**
|
||||||
|
String get offlineBannerTitle;
|
||||||
|
|
||||||
/// No description provided for @retryAction.
|
/// No description provided for @retryAction.
|
||||||
///
|
///
|
||||||
/// In es, this message translates to:
|
/// In es, this message translates to:
|
||||||
|
|||||||
@@ -766,6 +766,9 @@ class AppLocalizationsAr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get genresTitle => 'الأنواع';
|
String get genresTitle => 'الأنواع';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get offlineBannerTitle => 'لا يوجد اتصال';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get retryAction => 'إعادة المحاولة';
|
String get retryAction => 'إعادة المحاولة';
|
||||||
|
|
||||||
|
|||||||
@@ -769,6 +769,9 @@ class AppLocalizationsBn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get genresTitle => 'ধরন';
|
String get genresTitle => 'ধরন';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get offlineBannerTitle => 'সংযোগ নেই';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get retryAction => 'আবার চেষ্টা করুন';
|
String get retryAction => 'আবার চেষ্টা করুন';
|
||||||
|
|
||||||
|
|||||||
@@ -772,6 +772,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get genresTitle => 'Genres';
|
String get genresTitle => 'Genres';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get offlineBannerTitle => 'Keine Verbindung';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get retryAction => 'Erneut versuchen';
|
String get retryAction => 'Erneut versuchen';
|
||||||
|
|
||||||
|
|||||||
@@ -765,6 +765,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get genresTitle => 'Genres';
|
String get genresTitle => 'Genres';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get offlineBannerTitle => 'No connection';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get retryAction => 'Retry';
|
String get retryAction => 'Retry';
|
||||||
|
|
||||||
|
|||||||
@@ -770,6 +770,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get genresTitle => 'Géneros';
|
String get genresTitle => 'Géneros';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get offlineBannerTitle => 'Sin conexión';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get retryAction => 'Reintentar';
|
String get retryAction => 'Reintentar';
|
||||||
|
|
||||||
|
|||||||
@@ -774,6 +774,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get genresTitle => 'Genres';
|
String get genresTitle => 'Genres';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get offlineBannerTitle => 'Aucune connexion';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get retryAction => 'Réessayer';
|
String get retryAction => 'Réessayer';
|
||||||
|
|
||||||
|
|||||||
@@ -766,6 +766,9 @@ class AppLocalizationsHi extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get genresTitle => 'शैलियाँ';
|
String get genresTitle => 'शैलियाँ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get offlineBannerTitle => 'कोई कनेक्शन नहीं';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get retryAction => 'फिर कोशिश करें';
|
String get retryAction => 'फिर कोशिश करें';
|
||||||
|
|
||||||
|
|||||||
@@ -766,6 +766,9 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get genresTitle => 'Genre';
|
String get genresTitle => 'Genre';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get offlineBannerTitle => 'Tidak ada koneksi';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get retryAction => 'Coba lagi';
|
String get retryAction => 'Coba lagi';
|
||||||
|
|
||||||
|
|||||||
@@ -772,6 +772,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get genresTitle => 'Generi';
|
String get genresTitle => 'Generi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get offlineBannerTitle => 'Nessuna connessione';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get retryAction => 'Riprova';
|
String get retryAction => 'Riprova';
|
||||||
|
|
||||||
|
|||||||
@@ -740,6 +740,9 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get genresTitle => 'ジャンル';
|
String get genresTitle => 'ジャンル';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get offlineBannerTitle => '接続なし';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get retryAction => '再試行';
|
String get retryAction => '再試行';
|
||||||
|
|
||||||
|
|||||||
@@ -769,6 +769,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get genresTitle => 'Gêneros';
|
String get genresTitle => 'Gêneros';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get offlineBannerTitle => 'Sem conexão';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get retryAction => 'Tentar novamente';
|
String get retryAction => 'Tentar novamente';
|
||||||
|
|
||||||
|
|||||||
@@ -770,6 +770,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get genresTitle => 'Жанры';
|
String get genresTitle => 'Жанры';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get offlineBannerTitle => 'Нет подключения';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get retryAction => 'Повторить';
|
String get retryAction => 'Повторить';
|
||||||
|
|
||||||
|
|||||||
@@ -737,6 +737,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get genresTitle => '类型';
|
String get genresTitle => '类型';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get offlineBannerTitle => '无连接';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get retryAction => '重试';
|
String get retryAction => '重试';
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import '../estado/estado_radio.dart';
|
|||||||
import '../l10n/gen/app_localizations.dart';
|
import '../l10n/gen/app_localizations.dart';
|
||||||
import '../modelos/emisora.dart';
|
import '../modelos/emisora.dart';
|
||||||
import '../tema/pluri_animate.dart';
|
import '../tema/pluri_animate.dart';
|
||||||
|
import '../tema/pluriwave_theme.dart';
|
||||||
import '../widgets/pluri_glass_surface.dart';
|
import '../widgets/pluri_glass_surface.dart';
|
||||||
import '../widgets/pluri_icon.dart';
|
import '../widgets/pluri_icon.dart';
|
||||||
import '../widgets/pluri_layout.dart';
|
import '../widgets/pluri_layout.dart';
|
||||||
@@ -847,26 +848,81 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Audit 13.1 (t4 line 641): the prototype's offline banner is a
|
||||||
|
// rgba(207,102,121,X) tinted card — this consolidates on the ONE
|
||||||
|
// `offlineAccent` token (`#E8879A`) the proposal named for this exact
|
||||||
|
// banner (`pluriwave_tokens.dart:77`), which existed unused until now,
|
||||||
|
// rather than introducing a second near-identical raw hex for the tint.
|
||||||
Widget _errorBanner(
|
Widget _errorBanner(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
String error,
|
String error,
|
||||||
ThemeData theme,
|
ThemeData theme,
|
||||||
AppLocalizations l10n,
|
AppLocalizations l10n,
|
||||||
) {
|
) {
|
||||||
|
final tokens = context.pluriTokens;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: PluriGlassSurface(
|
child: DecoratedBox(
|
||||||
padding: const EdgeInsets.all(12),
|
key: const ValueKey('offline-banner'),
|
||||||
child: Row(
|
decoration: BoxDecoration(
|
||||||
children: [
|
color: tokens.offlineAccent.withValues(alpha: 0.14),
|
||||||
Icon(Icons.wifi_off, color: theme.colorScheme.error),
|
borderRadius: BorderRadius.circular(16),
|
||||||
const SizedBox(width: 8),
|
border: Border.all(
|
||||||
Expanded(child: Text(error)),
|
color: tokens.offlineAccent.withValues(alpha: 0.4),
|
||||||
TextButton(
|
),
|
||||||
onPressed: () => context.read<EstadoRadio>().cargarPopulares(),
|
),
|
||||||
child: Text(l10n.retryAction),
|
child: Padding(
|
||||||
),
|
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 13),
|
||||||
],
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.wifi_off, size: 22, color: tokens.offlineAccent),
|
||||||
|
const SizedBox(width: 11),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.offlineBannerTitle,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13.5,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
error,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11.5,
|
||||||
|
color: theme.colorScheme.onSurface.withValues(
|
||||||
|
alpha: 0.65,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
TextButton(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
backgroundColor: Colors.white.withValues(alpha: 0.1),
|
||||||
|
foregroundColor: theme.colorScheme.onSurface,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 7,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
textStyle: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () => context.read<EstadoRadio>().cargarPopulares(),
|
||||||
|
child: Text(l10n.retryAction),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'package:pluriwave/estado/estado_radio.dart';
|
|||||||
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
||||||
import 'package:pluriwave/pantallas/pantalla_buscar.dart';
|
import 'package:pluriwave/pantallas/pantalla_buscar.dart';
|
||||||
import 'package:pluriwave/pantallas/pantalla_favoritos.dart';
|
import 'package:pluriwave/pantallas/pantalla_favoritos.dart';
|
||||||
|
import 'package:pluriwave/tema/pluriwave_theme.dart';
|
||||||
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
|
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@@ -25,33 +26,30 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('Buscar — landing state (empty query shows discovery content)', () {
|
group('Buscar — landing state (empty query shows discovery content)', () {
|
||||||
testWidgets(
|
testWidgets('consulta vacia muestra cerca de vos, generos y tendencias; '
|
||||||
'consulta vacia muestra cerca de vos, generos y tendencias; '
|
'el grid de resultados de busqueda no aparece', (tester) async {
|
||||||
'el grid de resultados de busqueda no aparece',
|
_setLargeSurfaceSize(tester);
|
||||||
(tester) async {
|
final estado = _crearEstado(
|
||||||
_setLargeSurfaceSize(tester);
|
radio: FakeServicioRadio(
|
||||||
final estado = _crearEstado(
|
populares: [emisoraDemo(uuid: 'pop-1', nombre: 'Populares Uno')],
|
||||||
radio: FakeServicioRadio(
|
tendencias: [emisoraDemo(uuid: 'tr-1', nombre: 'Tendencia Uno')],
|
||||||
populares: [emisoraDemo(uuid: 'pop-1', nombre: 'Populares Uno')],
|
),
|
||||||
tendencias: [emisoraDemo(uuid: 'tr-1', nombre: 'Tendencia Uno')],
|
);
|
||||||
),
|
addTearDown(estado.dispose);
|
||||||
);
|
await tester.runAsync(estado.inicializar);
|
||||||
addTearDown(estado.dispose);
|
|
||||||
await tester.runAsync(estado.inicializar);
|
|
||||||
|
|
||||||
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
||||||
await _pumpStableFrame(tester);
|
await _pumpStableFrame(tester);
|
||||||
|
|
||||||
final l10n = _l10nDe(tester);
|
final l10n = _l10nDe(tester);
|
||||||
expect(find.text(l10n.nearYou), findsOneWidget);
|
expect(find.text(l10n.nearYou), findsOneWidget);
|
||||||
expect(find.text(l10n.liveRadar), findsOneWidget);
|
expect(find.text(l10n.liveRadar), findsOneWidget);
|
||||||
expect(find.text(l10n.genresTitle), findsOneWidget);
|
expect(find.text(l10n.genresTitle), findsOneWidget);
|
||||||
expect(find.text('Populares Uno'), findsOneWidget);
|
expect(find.text('Populares Uno'), findsOneWidget);
|
||||||
// The search-results empty state must NOT show — this is the
|
// The search-results empty state must NOT show — this is the
|
||||||
// landing state, not "you searched and got nothing".
|
// landing state, not "you searched and got nothing".
|
||||||
expect(find.text(l10n.searchEmptyTitle), findsNothing);
|
expect(find.text(l10n.searchEmptyTitle), findsNothing);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'escribir una consulta reemplaza el contenido de descubrimiento por '
|
'escribir una consulta reemplaza el contenido de descubrimiento por '
|
||||||
@@ -110,38 +108,37 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets('quitar un pill vuelve a buscar sin ese filtro', (
|
||||||
'quitar un pill vuelve a buscar sin ese filtro',
|
tester,
|
||||||
(tester) async {
|
) async {
|
||||||
_setLargeSurfaceSize(tester);
|
_setLargeSurfaceSize(tester);
|
||||||
final estado = _crearEstado(
|
final estado = _crearEstado(
|
||||||
radio: FakeServicioRadio(
|
radio: FakeServicioRadio(
|
||||||
busqueda: [emisoraDemo(uuid: 'es-1', nombre: 'Radio Espana')],
|
busqueda: [emisoraDemo(uuid: 'es-1', nombre: 'Radio Espana')],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
addTearDown(estado.dispose);
|
addTearDown(estado.dispose);
|
||||||
await tester.runAsync(estado.inicializar);
|
await tester.runAsync(estado.inicializar);
|
||||||
|
|
||||||
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
||||||
await _pumpStableFrame(tester);
|
await _pumpStableFrame(tester);
|
||||||
|
|
||||||
final l10n = _l10nDe(tester);
|
final l10n = _l10nDe(tester);
|
||||||
await _abrirYSeleccionarPais(tester, l10n.countrySpain);
|
await _abrirYSeleccionarPais(tester, l10n.countrySpain);
|
||||||
await _pumpStableFrame(tester);
|
await _pumpStableFrame(tester);
|
||||||
expect(find.text(l10n.countrySpain), findsOneWidget);
|
expect(find.text(l10n.countrySpain), findsOneWidget);
|
||||||
|
|
||||||
final pillPais = find.ancestor(
|
final pillPais = find.ancestor(
|
||||||
of: find.text(l10n.countrySpain),
|
of: find.text(l10n.countrySpain),
|
||||||
matching: find.byType(Chip),
|
matching: find.byType(Chip),
|
||||||
);
|
);
|
||||||
await tester.tap(
|
await tester.tap(
|
||||||
find.descendant(of: pillPais, matching: find.byIcon(Icons.close)),
|
find.descendant(of: pillPais, matching: find.byIcon(Icons.close)),
|
||||||
);
|
);
|
||||||
await _pumpStableFrame(tester);
|
await _pumpStableFrame(tester);
|
||||||
|
|
||||||
expect(find.text(l10n.countrySpain), findsNothing);
|
expect(find.text(l10n.countrySpain), findsNothing);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'dos filtros activos y cero resultados ofrecen quitar ambos de una '
|
'dos filtros activos y cero resultados ofrecen quitar ambos de una '
|
||||||
@@ -207,128 +204,150 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets('elegir un criterio reordena la pagina actual sin una nueva '
|
||||||
'elegir un criterio reordena la pagina actual sin una nueva '
|
'solicitud de red con parametro order', (tester) async {
|
||||||
'solicitud de red con parametro order',
|
_setLargeSurfaceSize(tester);
|
||||||
(tester) async {
|
final radio = FakeServicioRadio(
|
||||||
_setLargeSurfaceSize(tester);
|
busqueda: [
|
||||||
final radio = FakeServicioRadio(
|
emisoraDemo(
|
||||||
busqueda: [
|
uuid: 'alta',
|
||||||
emisoraDemo(uuid: 'alta', nombre: 'Estacion Alta')
|
nombre: 'Estacion Alta',
|
||||||
.copyWith(bitrate: 320, votes: 1),
|
).copyWith(bitrate: 320, votes: 1),
|
||||||
emisoraDemo(uuid: 'popular', nombre: 'Estacion Popular')
|
emisoraDemo(
|
||||||
.copyWith(bitrate: 64, votes: 900),
|
uuid: 'popular',
|
||||||
],
|
nombre: 'Estacion Popular',
|
||||||
);
|
).copyWith(bitrate: 64, votes: 900),
|
||||||
final estado = _crearEstado(radio: radio);
|
],
|
||||||
addTearDown(estado.dispose);
|
);
|
||||||
await tester.runAsync(estado.inicializar);
|
final estado = _crearEstado(radio: radio);
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
await tester.runAsync(estado.inicializar);
|
||||||
|
|
||||||
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
||||||
await _pumpStableFrame(tester);
|
await _pumpStableFrame(tester);
|
||||||
await tester.enterText(find.byType(SearchBar), 'estacion');
|
await tester.enterText(find.byType(SearchBar), 'estacion');
|
||||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||||
await _pumpStableFrame(tester);
|
await _pumpStableFrame(tester);
|
||||||
|
|
||||||
// Default global ordering is OrdenEmisoras.calidad (bitrate desc):
|
// Default global ordering is OrdenEmisoras.calidad (bitrate desc):
|
||||||
// "Estacion Alta" (320 kbps) must render above "Estacion Popular".
|
// "Estacion Alta" (320 kbps) must render above "Estacion Popular".
|
||||||
final yAltaAntes = tester.getTopLeft(find.text('Estacion Alta')).dy;
|
final yAltaAntes = tester.getTopLeft(find.text('Estacion Alta')).dy;
|
||||||
final yPopularAntes = tester.getTopLeft(
|
final yPopularAntes = tester.getTopLeft(find.text('Estacion Popular')).dy;
|
||||||
find.text('Estacion Popular'),
|
expect(yAltaAntes, lessThan(yPopularAntes));
|
||||||
).dy;
|
|
||||||
expect(yAltaAntes, lessThan(yPopularAntes));
|
|
||||||
|
|
||||||
final llamadasAntes = radio.buscarCalls;
|
final llamadasAntes = radio.buscarCalls;
|
||||||
|
|
||||||
final l10n = _l10nDe(tester);
|
final l10n = _l10nDe(tester);
|
||||||
await tester.tap(find.byIcon(Icons.swap_vert_rounded));
|
await tester.tap(find.byIcon(Icons.swap_vert_rounded));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.text(l10n.stationOrderByPopularity));
|
await tester.tap(find.text(l10n.stationOrderByPopularity));
|
||||||
await _pumpStableFrame(tester);
|
await _pumpStableFrame(tester);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
radio.buscarCalls,
|
radio.buscarCalls,
|
||||||
llamadasAntes,
|
llamadasAntes,
|
||||||
reason: 'sorting must not trigger a new /json request',
|
reason: 'sorting must not trigger a new /json request',
|
||||||
);
|
);
|
||||||
|
|
||||||
final yAltaDespues = tester.getTopLeft(find.text('Estacion Alta')).dy;
|
final yAltaDespues = tester.getTopLeft(find.text('Estacion Alta')).dy;
|
||||||
final yPopularDespues = tester.getTopLeft(
|
final yPopularDespues =
|
||||||
find.text('Estacion Popular'),
|
tester.getTopLeft(find.text('Estacion Popular')).dy;
|
||||||
).dy;
|
expect(yPopularDespues, lessThan(yAltaDespues));
|
||||||
expect(yPopularDespues, lessThan(yAltaDespues));
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
group('Buscar — contenido relocalizado desde PantallaInicio (WU6 6.5)', () {
|
group('Buscar — contenido relocalizado desde PantallaInicio (WU6 6.5)', () {
|
||||||
testWidgets(
|
testWidgets('el grid de descubrimiento muestra custom + populares; tocar '
|
||||||
'el grid de descubrimiento muestra custom + populares; tocar '
|
'reproduce via EstadoRadio y el boton de favorito usa el flujo '
|
||||||
'reproduce via EstadoRadio y el boton de favorito usa el flujo '
|
'existente', (tester) async {
|
||||||
'existente',
|
_setLargeSurfaceSize(tester);
|
||||||
(tester) async {
|
final audio = FakeServicioAudio();
|
||||||
_setLargeSurfaceSize(tester);
|
final favoritos = FakeServicioFavoritos();
|
||||||
final audio = FakeServicioAudio();
|
final radio = FakeServicioRadio();
|
||||||
final favoritos = FakeServicioFavoritos();
|
final custom = emisoraDemo(uuid: 'custom-1', nombre: 'Custom Uno');
|
||||||
final radio = FakeServicioRadio();
|
final archivo = await _crearArchivoCustom([custom]);
|
||||||
final custom = emisoraDemo(uuid: 'custom-1', nombre: 'Custom Uno');
|
final estado = _crearEstado(
|
||||||
final archivo = await _crearArchivoCustom([custom]);
|
audio: audio,
|
||||||
final estado = _crearEstado(
|
favoritos: favoritos,
|
||||||
audio: audio,
|
radio: radio,
|
||||||
favoritos: favoritos,
|
resolverArchivoCustom: () async => archivo,
|
||||||
radio: radio,
|
);
|
||||||
resolverArchivoCustom: () async => archivo,
|
addTearDown(estado.dispose);
|
||||||
);
|
await tester.runAsync(estado.inicializar);
|
||||||
addTearDown(estado.dispose);
|
|
||||||
await tester.runAsync(estado.inicializar);
|
|
||||||
|
|
||||||
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
||||||
await _pumpStableFrame(tester);
|
await _pumpStableFrame(tester);
|
||||||
|
|
||||||
expect(find.text('Custom Uno'), findsOneWidget);
|
expect(find.text('Custom Uno'), findsOneWidget);
|
||||||
|
|
||||||
await tester.tap(find.text('Custom Uno'));
|
await tester.tap(find.text('Custom Uno'));
|
||||||
await _pumpStableFrame(tester);
|
await _pumpStableFrame(tester);
|
||||||
expect(
|
expect(
|
||||||
audio.emisorasReproducidas.map((e) => e.uuid),
|
audio.emisorasReproducidas.map((e) => e.uuid),
|
||||||
contains('custom-1'),
|
contains('custom-1'),
|
||||||
);
|
);
|
||||||
expect(radio.ultimoUuidClick, 'custom-1');
|
expect(radio.ultimoUuidClick, 'custom-1');
|
||||||
|
|
||||||
final tarjetaCustom = find.ancestor(
|
final tarjetaCustom = find.ancestor(
|
||||||
of: find.text('Custom Uno'),
|
of: find.text('Custom Uno'),
|
||||||
matching: find.byType(TarjetaEmisora),
|
matching: find.byType(TarjetaEmisora),
|
||||||
);
|
);
|
||||||
final botonFavorito =
|
final botonFavorito =
|
||||||
find
|
find
|
||||||
.descendant(of: tarjetaCustom, matching: find.byType(InkWell))
|
.descendant(of: tarjetaCustom, matching: find.byType(InkWell))
|
||||||
.last;
|
.last;
|
||||||
expect(botonFavorito, findsOneWidget);
|
expect(botonFavorito, findsOneWidget);
|
||||||
|
|
||||||
await tester.ensureVisible(botonFavorito);
|
await tester.ensureVisible(botonFavorito);
|
||||||
await _pumpStableFrame(tester);
|
await _pumpStableFrame(tester);
|
||||||
await tester.tap(botonFavorito);
|
await tester.tap(botonFavorito);
|
||||||
await _pumpStableFrame(tester);
|
await _pumpStableFrame(tester);
|
||||||
|
|
||||||
expect(favoritos.toggleCalls, 1);
|
expect(favoritos.toggleCalls, 1);
|
||||||
expect(await favoritos.esFavorito(custom.uuid), isTrue);
|
expect(await favoritos.esFavorito(custom.uuid), isTrue);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
testWidgets('permite reintentar manualmente tras fallo inicial agotado', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
_setLargeSurfaceSize(tester);
|
||||||
|
final radio = FakeServicioRadio(
|
||||||
|
erroresPopularesPorLlamada: [Exception('sin red')],
|
||||||
|
popularesPorLlamada: [
|
||||||
|
const [],
|
||||||
|
[emisoraDemo(uuid: 'api-1', nombre: 'API Uno')],
|
||||||
|
],
|
||||||
|
tendenciasPorLlamada: [
|
||||||
|
const [],
|
||||||
|
[emisoraDemo(uuid: 'trend-1', nombre: 'Trend Uno')],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
final estado = _crearEstado(radio: radio);
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
await tester.runAsync(estado.inicializar);
|
||||||
|
|
||||||
|
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
||||||
|
await _pumpStableFrame(tester);
|
||||||
|
|
||||||
|
expect(find.text('Sin conexión a la API de radio'), findsOneWidget);
|
||||||
|
final l10n = _l10nDe(tester);
|
||||||
|
expect(find.text(l10n.retryAction), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.text(l10n.retryAction));
|
||||||
|
await _pumpStableFrame(tester);
|
||||||
|
|
||||||
|
expect(radio.obtenerPopularesCalls, 2);
|
||||||
|
expect(find.text('Sin conexión a la API de radio'), findsNothing);
|
||||||
|
expect(find.text('API Uno'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'permite reintentar manualmente tras fallo inicial agotado',
|
'visual fidelity (audit 13.1, proto t4 line 641): banner sin conexion '
|
||||||
|
'usa offlineAccent, no colorScheme.error',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
_setLargeSurfaceSize(tester);
|
_setLargeSurfaceSize(tester);
|
||||||
final radio = FakeServicioRadio(
|
final radio = FakeServicioRadio(
|
||||||
erroresPopularesPorLlamada: [Exception('sin red')],
|
erroresPopularesPorLlamada: [Exception('sin red')],
|
||||||
popularesPorLlamada: [
|
|
||||||
const [],
|
|
||||||
[emisoraDemo(uuid: 'api-1', nombre: 'API Uno')],
|
|
||||||
],
|
|
||||||
tendenciasPorLlamada: [
|
|
||||||
const [],
|
|
||||||
[emisoraDemo(uuid: 'trend-1', nombre: 'Trend Uno')],
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
final estado = _crearEstado(radio: radio);
|
final estado = _crearEstado(radio: radio);
|
||||||
addTearDown(estado.dispose);
|
addTearDown(estado.dispose);
|
||||||
@@ -337,16 +356,37 @@ void main() {
|
|||||||
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
await tester.pumpWidget(_conProviders(estado, _testApp()));
|
||||||
await _pumpStableFrame(tester);
|
await _pumpStableFrame(tester);
|
||||||
|
|
||||||
expect(find.text('Sin conexión a la API de radio'), findsOneWidget);
|
final banner = find.byKey(const ValueKey('offline-banner'));
|
||||||
|
expect(banner, findsOneWidget);
|
||||||
|
final contexto = tester.element(banner);
|
||||||
|
final tokens = contexto.pluriTokens;
|
||||||
|
final errorColor = Theme.of(contexto).colorScheme.error;
|
||||||
|
|
||||||
|
final caja = tester.widget<DecoratedBox>(banner);
|
||||||
|
final decoracion = caja.decoration as BoxDecoration;
|
||||||
|
expect(
|
||||||
|
decoracion.color,
|
||||||
|
tokens.offlineAccent.withValues(alpha: 0.14),
|
||||||
|
reason:
|
||||||
|
'prototype t4 line 641: rgba(207,102,121,.14) — this app '
|
||||||
|
'consolidates on the offlineAccent token (#E8879A)',
|
||||||
|
);
|
||||||
|
expect(decoracion.color, isNot(errorColor));
|
||||||
|
expect(
|
||||||
|
decoracion.borderRadius,
|
||||||
|
BorderRadius.circular(16),
|
||||||
|
reason: 'prototype t4 line 641: border-radius:16px',
|
||||||
|
);
|
||||||
|
|
||||||
|
final icono = tester.widget<Icon>(
|
||||||
|
find.descendant(of: banner, matching: find.byIcon(Icons.wifi_off)),
|
||||||
|
);
|
||||||
|
expect(icono.color, tokens.offlineAccent);
|
||||||
|
expect(icono.color, isNot(errorColor));
|
||||||
|
|
||||||
final l10n = _l10nDe(tester);
|
final l10n = _l10nDe(tester);
|
||||||
|
expect(find.text(l10n.offlineBannerTitle), findsOneWidget);
|
||||||
expect(find.text(l10n.retryAction), findsOneWidget);
|
expect(find.text(l10n.retryAction), findsOneWidget);
|
||||||
|
|
||||||
await tester.tap(find.text(l10n.retryAction));
|
|
||||||
await _pumpStableFrame(tester);
|
|
||||||
|
|
||||||
expect(radio.obtenerPopularesCalls, 2);
|
|
||||||
expect(find.text('Sin conexión a la API de radio'), findsNothing);
|
|
||||||
expect(find.text('API Uno'), findsOneWidget);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user