From e5d461af53c20f91a993829f87aa291dda8d671a Mon Sep 17 00:00:00 2001 From: freetlab Date: Thu, 30 Jul 2026 00:43:27 +0200 Subject: [PATCH] fix(buscar): build the 2x2 "Explorar por" grid, add Novedades MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit 3.2 (t4 lines 162-171): the prototype groups Países, Géneros, Tendencias and the entirely-missing Novedades into one 2x2 entry-point grid. The build had them as three unrelated always-visible widgets (a Países ListTile, a Géneros FilterChip Wrap, a Tendencias ActionChip strip) and no Novedades entry at all. Replaces all three with a single grid section, capability-preserving: Países still pushes PantallaPaises; Géneros and Tendencias now open their exact existing content in a picker sheet instead of always-on- screen (Géneros auto-closes on selection, matching this screen's other single-choice filter sheets; Tendencias stays open, a browse list, not a filter). Novedades re-triggers the existing discovery refresh, since no distinct "new stations" feed exists anywhere in the domain. New ARB key set (exploreByTitle/exploreTrendingTitle/ exploreTrendingSubtitle/exploreNewTitle/exploreNewSubtitle) translated across all 13 locales with zero anti-copy allowlist entries needed. Also fixes a pre-existing PluriEmptyState overflow this restructure exposed (unrelated to the grid itself, confirmed via isolated repro): wraps its Column in a SingleChildScrollView so a too-tall title/ subtitle scrolls instead of throwing a hard RenderFlex overflow. --- lib/l10n/app_ar.arb | 5 + lib/l10n/app_bn.arb | 5 + lib/l10n/app_de.arb | 5 + lib/l10n/app_en.arb | 5 + lib/l10n/app_es.arb | 5 + lib/l10n/app_fr.arb | 5 + lib/l10n/app_hi.arb | 5 + lib/l10n/app_id.arb | 5 + lib/l10n/app_it.arb | 5 + lib/l10n/app_ja.arb | 5 + lib/l10n/app_pt.arb | 5 + lib/l10n/app_ru.arb | 5 + lib/l10n/app_zh.arb | 5 + lib/l10n/gen/app_localizations.dart | 30 ++ lib/l10n/gen/app_localizations_ar.dart | 15 + lib/l10n/gen/app_localizations_bn.dart | 15 + lib/l10n/gen/app_localizations_de.dart | 15 + lib/l10n/gen/app_localizations_en.dart | 15 + lib/l10n/gen/app_localizations_es.dart | 15 + lib/l10n/gen/app_localizations_fr.dart | 15 + lib/l10n/gen/app_localizations_hi.dart | 15 + lib/l10n/gen/app_localizations_id.dart | 15 + lib/l10n/gen/app_localizations_it.dart | 15 + lib/l10n/gen/app_localizations_ja.dart | 15 + lib/l10n/gen/app_localizations_pt.dart | 15 + lib/l10n/gen/app_localizations_ru.dart | 15 + lib/l10n/gen/app_localizations_zh.dart | 15 + lib/pantallas/pantalla_buscar.dart | 266 +++++++++++++++--- lib/widgets/pluri_premium_widgets.dart | 57 ++-- .../pantalla_buscar_shimmer_test.dart | 11 + test/pantallas/pantalla_buscar_test.dart | 189 ++++++++++++- 31 files changed, 755 insertions(+), 58 deletions(-) diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index c9b3ca5..cd2ce2d 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -306,6 +306,11 @@ "detectAction": "اكتشاف", "liveRadar": "رادار مباشر", "genresTitle": "الأنواع", + "exploreByTitle": "استكشاف حسب", + "exploreTrendingTitle": "الرائجة", + "exploreTrendingSubtitle": "اليوم", + "exploreNewTitle": "جديد", + "exploreNewSubtitle": "هذا الأسبوع", "offlineBannerTitle": "لا يوجد اتصال", "retryAction": "إعادة المحاولة", "noStationsAvailable": "لا توجد محطات متاحة", diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb index 8c2e783..5cb5c3e 100644 --- a/lib/l10n/app_bn.arb +++ b/lib/l10n/app_bn.arb @@ -306,6 +306,11 @@ "detectAction": "সনাক্ত করুন", "liveRadar": "লাইভ রাডার", "genresTitle": "ধরন", + "exploreByTitle": "যেভাবে অন্বেষণ করুন", + "exploreTrendingTitle": "ট্রেন্ডিং", + "exploreTrendingSubtitle": "আজ", + "exploreNewTitle": "নতুন", + "exploreNewSubtitle": "এই সপ্তাহে", "offlineBannerTitle": "সংযোগ নেই", "retryAction": "আবার চেষ্টা করুন", "noStationsAvailable": "কোনো স্টেশন উপলব্ধ নেই", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 5e42509..9c696c9 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -306,6 +306,11 @@ "detectAction": "Erkennen", "liveRadar": "Live-Radar", "genresTitle": "Genres", + "exploreByTitle": "Entdecken nach", + "exploreTrendingTitle": "Trends", + "exploreTrendingSubtitle": "Heute", + "exploreNewTitle": "Neuheiten", + "exploreNewSubtitle": "Diese Woche", "offlineBannerTitle": "Keine Verbindung", "retryAction": "Erneut versuchen", "noStationsAvailable": "Keine Sender verfügbar", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 81d2f71..974c148 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -369,6 +369,11 @@ "detectAction": "Detect", "liveRadar": "Live radar", "genresTitle": "Genres", + "exploreByTitle": "Browse by", + "exploreTrendingTitle": "Trending", + "exploreTrendingSubtitle": "Today", + "exploreNewTitle": "New", + "exploreNewSubtitle": "This week", "offlineBannerTitle": "No connection", "retryAction": "Retry", "noStationsAvailable": "No stations available", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 5795fe9..701838f 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -369,6 +369,11 @@ "detectAction": "Detectar", "liveRadar": "Radar en directo", "genresTitle": "Géneros", + "exploreByTitle": "Explorar por", + "exploreTrendingTitle": "Tendencias", + "exploreTrendingSubtitle": "Hoy", + "exploreNewTitle": "Novedades", + "exploreNewSubtitle": "Esta semana", "offlineBannerTitle": "Sin conexión", "retryAction": "Reintentar", "noStationsAvailable": "No hay emisoras disponibles", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 963d463..308a4fa 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -306,6 +306,11 @@ "detectAction": "Détecter", "liveRadar": "Radar en direct", "genresTitle": "Genres", + "exploreByTitle": "Explorer par", + "exploreTrendingTitle": "Tendances", + "exploreTrendingSubtitle": "Aujourd'hui", + "exploreNewTitle": "Nouveautés", + "exploreNewSubtitle": "Cette semaine", "offlineBannerTitle": "Aucune connexion", "retryAction": "Réessayer", "noStationsAvailable": "Aucune station disponible", diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index 9d9f26f..7188a1c 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -306,6 +306,11 @@ "detectAction": "पहचानें", "liveRadar": "लाइव रडार", "genresTitle": "शैलियाँ", + "exploreByTitle": "इस अनुसार खोजें", + "exploreTrendingTitle": "ट्रेंडिंग", + "exploreTrendingSubtitle": "आज", + "exploreNewTitle": "नया", + "exploreNewSubtitle": "इस सप्ताह", "offlineBannerTitle": "कोई कनेक्शन नहीं", "retryAction": "फिर कोशिश करें", "noStationsAvailable": "कोई स्टेशन उपलब्ध नहीं", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 8953821..0ba0d60 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -306,6 +306,11 @@ "detectAction": "Deteksi", "liveRadar": "Radar langsung", "genresTitle": "Genre", + "exploreByTitle": "Jelajahi berdasarkan", + "exploreTrendingTitle": "Tren", + "exploreTrendingSubtitle": "Hari ini", + "exploreNewTitle": "Baru", + "exploreNewSubtitle": "Minggu ini", "offlineBannerTitle": "Tidak ada koneksi", "retryAction": "Coba lagi", "noStationsAvailable": "Tidak ada stasiun tersedia", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index decc030..a7decd8 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -306,6 +306,11 @@ "detectAction": "Rileva", "liveRadar": "Radar live", "genresTitle": "Generi", + "exploreByTitle": "Esplora per", + "exploreTrendingTitle": "Tendenze", + "exploreTrendingSubtitle": "Oggi", + "exploreNewTitle": "Novità", + "exploreNewSubtitle": "Questa settimana", "offlineBannerTitle": "Nessuna connessione", "retryAction": "Riprova", "noStationsAvailable": "Nessuna emittente disponibile", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 3cb5cab..ed12162 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -306,6 +306,11 @@ "detectAction": "検出", "liveRadar": "ライブレーダー", "genresTitle": "ジャンル", + "exploreByTitle": "カテゴリで探す", + "exploreTrendingTitle": "トレンド", + "exploreTrendingSubtitle": "今日", + "exploreNewTitle": "新着", + "exploreNewSubtitle": "今週", "offlineBannerTitle": "接続なし", "retryAction": "再試行", "noStationsAvailable": "利用できる局がありません", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index bdbf31f..c0f69f1 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -306,6 +306,11 @@ "detectAction": "Detectar", "liveRadar": "Radar ao vivo", "genresTitle": "Gêneros", + "exploreByTitle": "Navegar por", + "exploreTrendingTitle": "Tendências", + "exploreTrendingSubtitle": "Hoje", + "exploreNewTitle": "Novidades", + "exploreNewSubtitle": "Nesta semana", "offlineBannerTitle": "Sem conexão", "retryAction": "Tentar novamente", "noStationsAvailable": "Não há estações disponíveis", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 74ddbf9..4e0f140 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -306,6 +306,11 @@ "detectAction": "Определить", "liveRadar": "Радар в эфире", "genresTitle": "Жанры", + "exploreByTitle": "Обзор по", + "exploreTrendingTitle": "В тренде", + "exploreTrendingSubtitle": "Сегодня", + "exploreNewTitle": "Новинки", + "exploreNewSubtitle": "На этой неделе", "offlineBannerTitle": "Нет подключения", "retryAction": "Повторить", "noStationsAvailable": "Нет доступных станций", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 5591b9f..8e997a1 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -306,6 +306,11 @@ "detectAction": "检测", "liveRadar": "直播雷达", "genresTitle": "类型", + "exploreByTitle": "浏览分类", + "exploreTrendingTitle": "热门", + "exploreTrendingSubtitle": "今天", + "exploreNewTitle": "最新", + "exploreNewSubtitle": "本周", "offlineBannerTitle": "无连接", "retryAction": "重试", "noStationsAvailable": "没有可用电台", diff --git a/lib/l10n/gen/app_localizations.dart b/lib/l10n/gen/app_localizations.dart index 4f57a64..19440ae 100644 --- a/lib/l10n/gen/app_localizations.dart +++ b/lib/l10n/gen/app_localizations.dart @@ -1438,6 +1438,36 @@ abstract class AppLocalizations { /// **'Géneros'** String get genresTitle; + /// No description provided for @exploreByTitle. + /// + /// In es, this message translates to: + /// **'Explorar por'** + String get exploreByTitle; + + /// No description provided for @exploreTrendingTitle. + /// + /// In es, this message translates to: + /// **'Tendencias'** + String get exploreTrendingTitle; + + /// No description provided for @exploreTrendingSubtitle. + /// + /// In es, this message translates to: + /// **'Hoy'** + String get exploreTrendingSubtitle; + + /// No description provided for @exploreNewTitle. + /// + /// In es, this message translates to: + /// **'Novedades'** + String get exploreNewTitle; + + /// No description provided for @exploreNewSubtitle. + /// + /// In es, this message translates to: + /// **'Esta semana'** + String get exploreNewSubtitle; + /// No description provided for @offlineBannerTitle. /// /// In es, this message translates to: diff --git a/lib/l10n/gen/app_localizations_ar.dart b/lib/l10n/gen/app_localizations_ar.dart index 8fdf136..e849dba 100644 --- a/lib/l10n/gen/app_localizations_ar.dart +++ b/lib/l10n/gen/app_localizations_ar.dart @@ -766,6 +766,21 @@ class AppLocalizationsAr extends AppLocalizations { @override String get genresTitle => 'الأنواع'; + @override + String get exploreByTitle => 'استكشاف حسب'; + + @override + String get exploreTrendingTitle => 'الرائجة'; + + @override + String get exploreTrendingSubtitle => 'اليوم'; + + @override + String get exploreNewTitle => 'جديد'; + + @override + String get exploreNewSubtitle => 'هذا الأسبوع'; + @override String get offlineBannerTitle => 'لا يوجد اتصال'; diff --git a/lib/l10n/gen/app_localizations_bn.dart b/lib/l10n/gen/app_localizations_bn.dart index aaee917..30a16bd 100644 --- a/lib/l10n/gen/app_localizations_bn.dart +++ b/lib/l10n/gen/app_localizations_bn.dart @@ -769,6 +769,21 @@ class AppLocalizationsBn extends AppLocalizations { @override String get genresTitle => 'ধরন'; + @override + String get exploreByTitle => 'যেভাবে অন্বেষণ করুন'; + + @override + String get exploreTrendingTitle => 'ট্রেন্ডিং'; + + @override + String get exploreTrendingSubtitle => 'আজ'; + + @override + String get exploreNewTitle => 'নতুন'; + + @override + String get exploreNewSubtitle => 'এই সপ্তাহে'; + @override String get offlineBannerTitle => 'সংযোগ নেই'; diff --git a/lib/l10n/gen/app_localizations_de.dart b/lib/l10n/gen/app_localizations_de.dart index ecb6ffb..e2276a3 100644 --- a/lib/l10n/gen/app_localizations_de.dart +++ b/lib/l10n/gen/app_localizations_de.dart @@ -772,6 +772,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String get genresTitle => 'Genres'; + @override + String get exploreByTitle => 'Entdecken nach'; + + @override + String get exploreTrendingTitle => 'Trends'; + + @override + String get exploreTrendingSubtitle => 'Heute'; + + @override + String get exploreNewTitle => 'Neuheiten'; + + @override + String get exploreNewSubtitle => 'Diese Woche'; + @override String get offlineBannerTitle => 'Keine Verbindung'; diff --git a/lib/l10n/gen/app_localizations_en.dart b/lib/l10n/gen/app_localizations_en.dart index 99c064d..cc164f8 100644 --- a/lib/l10n/gen/app_localizations_en.dart +++ b/lib/l10n/gen/app_localizations_en.dart @@ -765,6 +765,21 @@ class AppLocalizationsEn extends AppLocalizations { @override String get genresTitle => 'Genres'; + @override + String get exploreByTitle => 'Browse by'; + + @override + String get exploreTrendingTitle => 'Trending'; + + @override + String get exploreTrendingSubtitle => 'Today'; + + @override + String get exploreNewTitle => 'New'; + + @override + String get exploreNewSubtitle => 'This week'; + @override String get offlineBannerTitle => 'No connection'; diff --git a/lib/l10n/gen/app_localizations_es.dart b/lib/l10n/gen/app_localizations_es.dart index a7679a0..5b14e48 100644 --- a/lib/l10n/gen/app_localizations_es.dart +++ b/lib/l10n/gen/app_localizations_es.dart @@ -770,6 +770,21 @@ class AppLocalizationsEs extends AppLocalizations { @override String get genresTitle => 'Géneros'; + @override + String get exploreByTitle => 'Explorar por'; + + @override + String get exploreTrendingTitle => 'Tendencias'; + + @override + String get exploreTrendingSubtitle => 'Hoy'; + + @override + String get exploreNewTitle => 'Novedades'; + + @override + String get exploreNewSubtitle => 'Esta semana'; + @override String get offlineBannerTitle => 'Sin conexión'; diff --git a/lib/l10n/gen/app_localizations_fr.dart b/lib/l10n/gen/app_localizations_fr.dart index 3559466..00a4797 100644 --- a/lib/l10n/gen/app_localizations_fr.dart +++ b/lib/l10n/gen/app_localizations_fr.dart @@ -774,6 +774,21 @@ class AppLocalizationsFr extends AppLocalizations { @override String get genresTitle => 'Genres'; + @override + String get exploreByTitle => 'Explorer par'; + + @override + String get exploreTrendingTitle => 'Tendances'; + + @override + String get exploreTrendingSubtitle => 'Aujourd\'hui'; + + @override + String get exploreNewTitle => 'Nouveautés'; + + @override + String get exploreNewSubtitle => 'Cette semaine'; + @override String get offlineBannerTitle => 'Aucune connexion'; diff --git a/lib/l10n/gen/app_localizations_hi.dart b/lib/l10n/gen/app_localizations_hi.dart index e5fc9fd..3c8b7fd 100644 --- a/lib/l10n/gen/app_localizations_hi.dart +++ b/lib/l10n/gen/app_localizations_hi.dart @@ -766,6 +766,21 @@ class AppLocalizationsHi extends AppLocalizations { @override String get genresTitle => 'शैलियाँ'; + @override + String get exploreByTitle => 'इस अनुसार खोजें'; + + @override + String get exploreTrendingTitle => 'ट्रेंडिंग'; + + @override + String get exploreTrendingSubtitle => 'आज'; + + @override + String get exploreNewTitle => 'नया'; + + @override + String get exploreNewSubtitle => 'इस सप्ताह'; + @override String get offlineBannerTitle => 'कोई कनेक्शन नहीं'; diff --git a/lib/l10n/gen/app_localizations_id.dart b/lib/l10n/gen/app_localizations_id.dart index 36e8e5d..106f059 100644 --- a/lib/l10n/gen/app_localizations_id.dart +++ b/lib/l10n/gen/app_localizations_id.dart @@ -766,6 +766,21 @@ class AppLocalizationsId extends AppLocalizations { @override String get genresTitle => 'Genre'; + @override + String get exploreByTitle => 'Jelajahi berdasarkan'; + + @override + String get exploreTrendingTitle => 'Tren'; + + @override + String get exploreTrendingSubtitle => 'Hari ini'; + + @override + String get exploreNewTitle => 'Baru'; + + @override + String get exploreNewSubtitle => 'Minggu ini'; + @override String get offlineBannerTitle => 'Tidak ada koneksi'; diff --git a/lib/l10n/gen/app_localizations_it.dart b/lib/l10n/gen/app_localizations_it.dart index 6b727a9..dccb305 100644 --- a/lib/l10n/gen/app_localizations_it.dart +++ b/lib/l10n/gen/app_localizations_it.dart @@ -772,6 +772,21 @@ class AppLocalizationsIt extends AppLocalizations { @override String get genresTitle => 'Generi'; + @override + String get exploreByTitle => 'Esplora per'; + + @override + String get exploreTrendingTitle => 'Tendenze'; + + @override + String get exploreTrendingSubtitle => 'Oggi'; + + @override + String get exploreNewTitle => 'Novità'; + + @override + String get exploreNewSubtitle => 'Questa settimana'; + @override String get offlineBannerTitle => 'Nessuna connessione'; diff --git a/lib/l10n/gen/app_localizations_ja.dart b/lib/l10n/gen/app_localizations_ja.dart index 5ac92e8..f3d206d 100644 --- a/lib/l10n/gen/app_localizations_ja.dart +++ b/lib/l10n/gen/app_localizations_ja.dart @@ -740,6 +740,21 @@ class AppLocalizationsJa extends AppLocalizations { @override String get genresTitle => 'ジャンル'; + @override + String get exploreByTitle => 'カテゴリで探す'; + + @override + String get exploreTrendingTitle => 'トレンド'; + + @override + String get exploreTrendingSubtitle => '今日'; + + @override + String get exploreNewTitle => '新着'; + + @override + String get exploreNewSubtitle => '今週'; + @override String get offlineBannerTitle => '接続なし'; diff --git a/lib/l10n/gen/app_localizations_pt.dart b/lib/l10n/gen/app_localizations_pt.dart index a5e051b..e73a879 100644 --- a/lib/l10n/gen/app_localizations_pt.dart +++ b/lib/l10n/gen/app_localizations_pt.dart @@ -769,6 +769,21 @@ class AppLocalizationsPt extends AppLocalizations { @override String get genresTitle => 'Gêneros'; + @override + String get exploreByTitle => 'Navegar por'; + + @override + String get exploreTrendingTitle => 'Tendências'; + + @override + String get exploreTrendingSubtitle => 'Hoje'; + + @override + String get exploreNewTitle => 'Novidades'; + + @override + String get exploreNewSubtitle => 'Nesta semana'; + @override String get offlineBannerTitle => 'Sem conexão'; diff --git a/lib/l10n/gen/app_localizations_ru.dart b/lib/l10n/gen/app_localizations_ru.dart index 57d7b94..66f929c 100644 --- a/lib/l10n/gen/app_localizations_ru.dart +++ b/lib/l10n/gen/app_localizations_ru.dart @@ -770,6 +770,21 @@ class AppLocalizationsRu extends AppLocalizations { @override String get genresTitle => 'Жанры'; + @override + String get exploreByTitle => 'Обзор по'; + + @override + String get exploreTrendingTitle => 'В тренде'; + + @override + String get exploreTrendingSubtitle => 'Сегодня'; + + @override + String get exploreNewTitle => 'Новинки'; + + @override + String get exploreNewSubtitle => 'На этой неделе'; + @override String get offlineBannerTitle => 'Нет подключения'; diff --git a/lib/l10n/gen/app_localizations_zh.dart b/lib/l10n/gen/app_localizations_zh.dart index 056783f..9ac3ed4 100644 --- a/lib/l10n/gen/app_localizations_zh.dart +++ b/lib/l10n/gen/app_localizations_zh.dart @@ -737,6 +737,21 @@ class AppLocalizationsZh extends AppLocalizations { @override String get genresTitle => '类型'; + @override + String get exploreByTitle => '浏览分类'; + + @override + String get exploreTrendingTitle => '热门'; + + @override + String get exploreTrendingSubtitle => '今天'; + + @override + String get exploreNewTitle => '最新'; + + @override + String get exploreNewSubtitle => '本周'; + @override String get offlineBannerTitle => '无连接'; diff --git a/lib/pantallas/pantalla_buscar.dart b/lib/pantallas/pantalla_buscar.dart index 5324d8b..e005462 100644 --- a/lib/pantallas/pantalla_buscar.dart +++ b/lib/pantallas/pantalla_buscar.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shimmer/shimmer.dart' as shimmer; @@ -8,6 +10,7 @@ import '../l10n/gen/app_localizations.dart'; import '../modelos/emisora.dart'; import '../tema/pluri_animate.dart'; import '../tema/pluriwave_theme.dart'; +import '../tema/pluriwave_tokens.dart'; import '../widgets/pluri_glass_surface.dart'; import '../widgets/pluri_icon.dart'; import '../widgets/pluri_layout.dart'; @@ -199,9 +202,11 @@ class _PantallaBuscarState extends State { _resultados(estado, theme), ] else ...[ _seccionCercanas(context, theme, l10n), - _seccionTendencias(context, theme, l10n), - _chipGeneros(context, theme, l10n), - _seccionPaises(context, theme, l10n), + // Audit 3.2 (t4 lines 162-171): a single "Explorar por" 2x2 grid + // replaces the always-visible Tendencias chip strip, Géneros + // chip Wrap and Países ListTile — each cell keeps its EXACT + // existing capability, just behind a tap instead of always-on. + _seccionExplorarPor(context, theme, l10n), if (context.select((e) => e.error) != null) _errorBanner( context, @@ -798,6 +803,12 @@ class _PantallaBuscarState extends State { } else { context.read().cargarPopulares(); } + // Audit 3.2: only reachable from the "Explorar por" + // Géneros sheet now — tap-once-and-close, matching + // this screen's other single-choice filter sheets. + if (Navigator.canPop(context)) { + Navigator.of(context).pop(); + } }, ); }).toList(), @@ -808,43 +819,157 @@ class _PantallaBuscarState extends State { ); } - /// WU7, `station-discovery-browse` spec: the "Países entry point" the - /// spec's landing-state scenario lists. Deferred from WU6 (this class's - /// own doc comment: `PantallaPaises` did not exist yet); added here now - /// that it does, alongside the screen it targets — an unreachable screen - /// would repeat the WU15/WU15b lesson (a fully-tested screen shipped with - /// no navigation path to it). - Widget _seccionPaises( + /// Audit 3.2 (t4 lines 162-171): "Explorar por" 2x2 grid. Replaces the + /// old always-visible Países `ListTile` (WU7), Géneros chip `Wrap` and + /// Tendencias chip strip — 4 cells, adding the entirely-missing + /// Novedades entry. Task constraint: presentation changes, capability + /// does not — Países still pushes `PantallaPaises` (WU7's own reasoning + /// for why that screen must stay reachable still applies); Géneros and + /// Tendencias now open their EXACT existing content in a picker sheet + /// instead of always-on-screen, so no selection logic is duplicated. + Widget _seccionExplorarPor( BuildContext context, ThemeData theme, AppLocalizations l10n, ) { - return Padding( - padding: const EdgeInsets.fromLTRB( - PluriLayout.horizontal, - 8, - PluriLayout.horizontal, - 0, - ), - child: PluriGlassSurface( - padding: EdgeInsets.zero, - // `PluriGlassSurface` paints via `DecoratedBox`, not `Material` — a - // tappable `ListTile` needs its own `Material` ancestor or its ink - // splash silently fails to paint (Flutter's own debug assertion). - child: Material( - type: MaterialType.transparency, - child: ListTile( - leading: const Icon(Icons.public_rounded), - title: Text(l10n.countriesScreenTitle), - trailing: const Icon(Icons.chevron_right_rounded), - onTap: - () => PluriPushScaffold.push( - context, - (_) => const PantallaPaises(), - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB( + PluriLayout.titleHorizontal, + 8, + PluriLayout.titleHorizontal, + 0, + ), + child: Text( + l10n.exploreByTitle, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w900, + ), ), ), - ), + const SizedBox(height: 10), + Padding( + key: const Key('explore-by-grid'), + padding: const EdgeInsets.symmetric( + horizontal: PluriLayout.horizontal, + ), + // A manually-built 2-per-row layout, NOT `GridView` + + // `childAspectRatio`: a fixed aspect ratio sizes each cell's + // height as a function of the SCREEN width, which overflowed at + // a narrow/default test viewport even though the same ratio fit + // fine at the wider viewport this was first verified against. + // `Expanded` cells size their height from their OWN content, + // which cannot overflow this way at any width. + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _CeldaExplorarPor( + key: const Key('explore-cell-paises'), + icon: Icons.public_rounded, + color: PluriWaveTokens.brand, + title: l10n.countriesScreenTitle, + // No subtitle: a live count needs + // EstadoBusqueda.cargarPaises() triggered from this + // screen, which was tried and reverted — it forced + // an extra rebuild that (harmlessly in production, + // but fatally under the default 800-wide test + // viewport) surfaced a pre-existing, unrelated + // PluriEmptyState overflow in the empty discovery + // grid below. Not worth the coupling for a cosmetic + // badge. + subtitle: null, + onTap: + () => PluriPushScaffold.push( + context, + (_) => const PantallaPaises(), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: _CeldaExplorarPor( + key: const Key('explore-cell-generos'), + icon: Icons.library_music_rounded, + color: context.pluriTokens.liveGreen, + title: l10n.genresTitle, + subtitle: '${_generos.length}', + onTap: () => _abrirGenerosSheet(theme, l10n), + ), + ), + ], + ), + const SizedBox(height: 10), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _CeldaExplorarPor( + key: const Key('explore-cell-tendencias'), + icon: Icons.trending_up_rounded, + color: context.pluriTokens.warmCoral, + title: l10n.exploreTrendingTitle, + subtitle: l10n.exploreTrendingSubtitle, + onTap: () => _abrirTendenciasSheet(theme, l10n), + ), + ), + const SizedBox(width: 10), + Expanded( + child: _CeldaExplorarPor( + key: const Key('explore-cell-novedades'), + icon: Icons.fiber_new_rounded, + color: PluriWaveTokens.skyBlue, + title: l10n.exploreNewTitle, + subtitle: l10n.exploreNewSubtitle, + // No distinct "new stations" feed exists in the + // domain (ServicioRadio/EstadoRadio have no such + // concept, and Emisora carries no added/changed + // timestamp) — this re-runs the SAME discovery + // refresh the offline banner's retry action already + // calls, rather than inventing one. + onTap: + () => unawaited( + context.read().cargarPopulares(), + ), + ), + ), + ], + ), + ], + ), + ), + ], + ); + } + + /// Opens [_chipGeneros]'s EXACT existing content/selection logic behind + /// a tap instead of always-on-screen — same capability, same state + /// (`_generoSeleccionado`), same calls (`EstadoBusqueda.buscar(tag:)` / + /// `EstadoRadio.cargarPopulares()`). Auto-closes on selection, matching + /// this screen's country/language/quality filter sheets (single-choice + /// picker, not a browse list). + Future _abrirGenerosSheet(ThemeData theme, AppLocalizations l10n) { + return showModalBottomSheet( + context: context, + showDragHandle: true, + builder: (ctx) => SafeArea(child: _chipGeneros(ctx, theme, l10n)), + ); + } + + /// Opens [_seccionTendencias]'s EXACT existing content/tap-to-play logic + /// behind a tap. Deliberately does NOT auto-close on selection — unlike + /// Géneros, this is a browse-and-preview list (tapping a station starts + /// playback in place, the same as tapping any station card elsewhere in + /// the app; it does not "choose" a single filter value). + Future _abrirTendenciasSheet(ThemeData theme, AppLocalizations l10n) { + return showModalBottomSheet( + context: context, + showDragHandle: true, + builder: (ctx) => SafeArea(child: _seccionTendencias(ctx, theme, l10n)), ); } @@ -1037,3 +1162,76 @@ class _ChipShimmer extends StatelessWidget { ); } } + +/// Audit 3.2 (t4 lines 167-170): one "Explorar por" grid cell — icon, +/// title (13.5/w800) and subtitle (11/55%). Radius 16 is a local one-off +/// (like `_errorBanner`'s), matching neither of the 3 named token radii. +class _CeldaExplorarPor extends StatelessWidget { + const _CeldaExplorarPor({ + super.key, + required this.icon, + required this.color, + required this.title, + required this.subtitle, + required this.onTap, + }); + + final IconData icon; + final Color color; + final String title; + final String? subtitle; + final VoidCallback onTap; + + static const _radio = 16.0; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return PluriGlassSurface( + borderRadius: BorderRadius.circular(_radio), + padding: const EdgeInsets.all(14), + child: Material( + type: MaterialType.transparency, + child: InkWell( + borderRadius: BorderRadius.circular(_radio), + onTap: onTap, + child: Row( + children: [ + Icon(icon, size: 24, color: color), + const SizedBox(width: 11), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 13.5, + fontWeight: FontWeight.w800, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (subtitle != null) + Text( + subtitle!, + style: TextStyle( + fontSize: 11, + color: theme.colorScheme.onSurface.withValues( + alpha: 0.55, + ), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/pluri_premium_widgets.dart b/lib/widgets/pluri_premium_widgets.dart index 4c89b92..b67ead8 100644 --- a/lib/widgets/pluri_premium_widgets.dart +++ b/lib/widgets/pluri_premium_widgets.dart @@ -74,31 +74,42 @@ class PluriEmptyState extends StatelessWidget { child: PluriGlassSurface( borderRadius: BorderRadius.circular(t.radiusLg + 10), padding: const EdgeInsets.all(28), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - PluriIcon( - glyph: glyph, - variant: PluriIconVariant.activeGlow, - size: 58, - ), - const SizedBox(height: 18), - Text( - title, - textAlign: TextAlign.center, - style: theme.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w900, + // Bugfix: every call site constrains this widget to a fixed + // height (`SizedBox(height: 260)`). At a narrow width with + // realistic two-line title/subtitle copy, the icon + gaps + + // wrapped text can exceed what's actually left after this + // surface's own padding — a hard RenderFlex overflow rather + // than a graceful clip. `SingleChildScrollView` is a no-op + // visually whenever the content already fits (the normal + // case, on any real device width this was designed for) and + // only engages scrolling in the rare case it does not. + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + PluriIcon( + glyph: glyph, + variant: PluriIconVariant.activeGlow, + size: 58, ), - ), - const SizedBox(height: 8), - Text( - subtitle, - textAlign: TextAlign.center, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface.withValues(alpha: 0.72), + const SizedBox(height: 18), + Text( + title, + textAlign: TextAlign.center, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w900, + ), ), - ), - ], + const SizedBox(height: 8), + Text( + subtitle, + textAlign: TextAlign.center, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.72), + ), + ), + ], + ), ), ), ), diff --git a/test/pantallas/pantalla_buscar_shimmer_test.dart b/test/pantallas/pantalla_buscar_shimmer_test.dart index b798c8b..e9c24ca 100644 --- a/test/pantallas/pantalla_buscar_shimmer_test.dart +++ b/test/pantallas/pantalla_buscar_shimmer_test.dart @@ -21,6 +21,17 @@ import '../helpers/fakes.dart'; /// provider from the start, not just `EstadoBusqueda`. A non-empty query is /// entered first so the screen leaves the landing state and reaches the /// search-results branch this test actually targets. +/// +/// Bugfix (surfaced while landing audit item 18's "Explorar por" grid): +/// on the FIRST pump here (landing state, empty `EstadoRadio`, default +/// narrow test viewport, no `_setLargeSurfaceSize`), `_gridEmisoras` +/// renders its empty-state branch — `PluriEmptyState`, in a fixed +/// `SizedBox(height: 260)`. That widget's own Column overflowed by 10px +/// at this width with real ARB copy (`pluri_premium_widgets.dart`, +/// fixed by wrapping it in a `SingleChildScrollView` — a no-op whenever +/// content already fits, which is every real device width). This test IS +/// the end-to-end regression guard for that fix: it renders the exact +/// scenario that surfaced it. class _BusquedaCargando extends EstadoBusqueda { _BusquedaCargando() : super(radio: FakeServicioRadio()); diff --git a/test/pantallas/pantalla_buscar_test.dart b/test/pantallas/pantalla_buscar_test.dart index 3348aaf..d1d8694 100644 --- a/test/pantallas/pantalla_buscar_test.dart +++ b/test/pantallas/pantalla_buscar_test.dart @@ -9,7 +9,9 @@ import 'package:pluriwave/estado/estado_radio.dart'; import 'package:pluriwave/l10n/gen/app_localizations.dart'; import 'package:pluriwave/pantallas/pantalla_buscar.dart'; import 'package:pluriwave/pantallas/pantalla_favoritos.dart'; +import 'package:pluriwave/pantallas/pantalla_paises.dart'; import 'package:pluriwave/tema/pluriwave_theme.dart'; +import 'package:pluriwave/tema/pluriwave_tokens.dart'; import 'package:pluriwave/widgets/tarjeta_emisora.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -43,8 +45,14 @@ void main() { final l10n = _l10nDe(tester); expect(find.text(l10n.nearYou), findsOneWidget); - expect(find.text(l10n.liveRadar), findsOneWidget); + // Audit 3.2 (t4 lines 164-170): the "Explorar por" 2x2 grid replaced + // the old always-visible Tendencias chip strip and Géneros chip Wrap + // (both now live behind this grid's own cells, task 18's scope). + expect(find.text(l10n.exploreByTitle), findsOneWidget); + expect(find.text(l10n.countriesScreenTitle), findsOneWidget); expect(find.text(l10n.genresTitle), findsOneWidget); + expect(find.text(l10n.exploreTrendingTitle), findsOneWidget); + expect(find.text(l10n.exploreNewTitle), findsOneWidget); expect(find.text('Populares Uno'), findsOneWidget); // The search-results empty state must NOT show — this is the // landing state, not "you searched and got nothing". @@ -434,6 +442,185 @@ void main() { }, ); }); + + group('Buscar — "Explorar por" 2x2 grid (audit 3.2)', () { + testWidgets('renders exactly 4 cells arranged 2 per row', (tester) async { + _setLargeSurfaceSize(tester); + final estado = _crearEstado(); + addTearDown(estado.dispose); + await tester.runAsync(estado.inicializar); + + await tester.pumpWidget(_conProviders(estado, _testApp())); + await _pumpStableFrame(tester); + + expect(find.byKey(const Key('explore-cell-paises')), findsOneWidget); + expect(find.byKey(const Key('explore-cell-generos')), findsOneWidget); + expect(find.byKey(const Key('explore-cell-tendencias')), findsOneWidget); + expect(find.byKey(const Key('explore-cell-novedades')), findsOneWidget); + + // Geometry, not GridView internals: prototype t4 line 166 + // (grid-template-columns:1fr 1fr) means Países/Géneros share a row + // and Tendencias/Novedades share the NEXT row, 2 columns each. + final paises = tester.getTopLeft( + find.byKey(const Key('explore-cell-paises')), + ); + final generos = tester.getTopLeft( + find.byKey(const Key('explore-cell-generos')), + ); + final tendencias = tester.getTopLeft( + find.byKey(const Key('explore-cell-tendencias')), + ); + final novedades = tester.getTopLeft( + find.byKey(const Key('explore-cell-novedades')), + ); + expect(paises.dy, generos.dy, reason: 'Países/Géneros share a row'); + expect( + tendencias.dy, + novedades.dy, + reason: 'Tendencias/Novedades share a row', + ); + expect( + paises.dy, + lessThan(tendencias.dy), + reason: 'the first row sits above the second', + ); + expect(paises.dx, lessThan(generos.dx), reason: 'Países is column 1'); + expect( + tendencias.dx, + lessThan(novedades.dx), + reason: 'Tendencias is column 1', + ); + }); + + testWidgets( + 'each cell icon uses the prototype accent colour (t4 lines 167-170)', + (tester) async { + _setLargeSurfaceSize(tester); + final estado = _crearEstado(); + addTearDown(estado.dispose); + await tester.runAsync(estado.inicializar); + + await tester.pumpWidget(_conProviders(estado, _testApp())); + await _pumpStableFrame(tester); + + Color colorDe(String key) { + final icono = tester.widget( + find.descendant( + of: find.byKey(Key(key)), + matching: find.byType(Icon), + ), + ); + return icono.color!; + } + + final contexto = tester.element( + find.byKey(const Key('explore-cell-generos')), + ); + expect(colorDe('explore-cell-paises'), PluriWaveTokens.brand); + expect(colorDe('explore-cell-generos'), contexto.pluriTokens.liveGreen); + expect( + colorDe('explore-cell-tendencias'), + contexto.pluriTokens.warmCoral, + ); + expect(colorDe('explore-cell-novedades'), PluriWaveTokens.skyBlue); + }, + ); + + testWidgets('tapping Paises pushes PantallaPaises', (tester) async { + _setLargeSurfaceSize(tester); + final estado = _crearEstado(); + addTearDown(estado.dispose); + await tester.runAsync(estado.inicializar); + + await tester.pumpWidget(_conProviders(estado, _testApp())); + await _pumpStableFrame(tester); + + await tester.tap(find.byKey(const Key('explore-cell-paises'))); + await _pumpStableFrame(tester); + + expect(find.byType(PantallaPaises), findsOneWidget); + }); + + testWidgets( + 'tapping Generos opens a picker sheet; selecting a genre closes it ' + 'and filters the discovery grid (same capability, relocated)', + (tester) async { + _setLargeSurfaceSize(tester); + final estado = _crearEstado( + radio: FakeServicioRadio( + busqueda: [emisoraDemo(uuid: 'rock-1', nombre: 'Rock Uno')], + ), + ); + addTearDown(estado.dispose); + await tester.runAsync(estado.inicializar); + + await tester.pumpWidget(_conProviders(estado, _testApp())); + await _pumpStableFrame(tester); + + await tester.tap(find.byKey(const Key('explore-cell-generos'))); + await tester.pumpAndSettle(); + + final l10n = _l10nDe(tester); + await tester.tap(find.text(l10n.genreRock)); + await tester.pumpAndSettle(); + + // Tap-once picker (matches this file's country/language/quality + // filter sheets) — the sheet closes and the grid below now shows + // the genre-filtered results. + expect(find.byType(BottomSheet), findsNothing); + expect(find.text('Rock Uno'), findsOneWidget); + }, + ); + + testWidgets('tapping Tendencias opens a sheet listing the trending strip; ' + 'tapping a station plays it via the existing flow', (tester) async { + _setLargeSurfaceSize(tester); + final audio = FakeServicioAudio(); + final estado = _crearEstado( + audio: audio, + radio: FakeServicioRadio( + tendencias: [emisoraDemo(uuid: 'trend-1', nombre: 'Trend Uno')], + ), + ); + addTearDown(estado.dispose); + await tester.runAsync(estado.inicializar); + + await tester.pumpWidget(_conProviders(estado, _testApp())); + await _pumpStableFrame(tester); + + await tester.tap(find.byKey(const Key('explore-cell-tendencias'))); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Trend Uno')); + await _pumpStableFrame(tester); + + expect( + audio.emisorasReproducidas.map((e) => e.uuid), + contains('trend-1'), + ); + }); + + testWidgets( + 'tapping Novedades re-triggers the discovery refresh (no distinct ' + '"new stations" feed exists in the domain)', + (tester) async { + _setLargeSurfaceSize(tester); + final radio = FakeServicioRadio(); + final estado = _crearEstado(radio: radio); + addTearDown(estado.dispose); + await tester.runAsync(estado.inicializar); + + await tester.pumpWidget(_conProviders(estado, _testApp())); + await _pumpStableFrame(tester); + final llamadasAntes = radio.obtenerPopularesCalls; + + await tester.tap(find.byKey(const Key('explore-cell-novedades'))); + await _pumpStableFrame(tester); + + expect(radio.obtenerPopularesCalls, greaterThan(llamadasAntes)); + }, + ); + }); } EstadoRadio _crearEstado({