fix(escuchar,reproductor): add the section-heading token and screen 1-3 polish

Audit S7 plus the open items on screens 1-3. Escuchar gains the
prototype's own 52px "now listening" eyebrow header (t4:53), which is
structurally different from the 56px title row every other root uses
(t4:325) -- the earlier wiring test asserted PluriRootHeader on all five
roots, an over-broad premise now corrected to guard what actually holds:
no AppBar, and the sleep-timer action still reachable.
This commit is contained in:
2026-07-30 14:48:06 +02:00
parent 7ff156852f
commit 01615f9751
34 changed files with 1129 additions and 218 deletions
+3 -1
View File
@@ -816,5 +816,7 @@
"welcomeBullet3Title": "منبهات موسيقية", "welcomeBullet3Title": "منبهات موسيقية",
"welcomeCtaLabel": "ابدأ الاستماع", "welcomeCtaLabel": "ابدأ الاستماع",
"welcomeHeadline": "عالمك، على الهواء مباشرة", "welcomeHeadline": "عالمك، على الهواء مباشرة",
"yourStationsTitle": "محطاتك" "yourStationsTitle": "محطاتك",
"nowListeningLabel": "الاستماع الآن",
"popularNowTitle": "الأكثر شيوعًا الآن"
} }
+3 -1
View File
@@ -816,5 +816,7 @@
"welcomeBullet3Title": "সুরেলা অ্যালার্ম", "welcomeBullet3Title": "সুরেলা অ্যালার্ম",
"welcomeCtaLabel": "শোনা শুরু করুন", "welcomeCtaLabel": "শোনা শুরু করুন",
"welcomeHeadline": "আপনার বিশ্ব, সরাসরি", "welcomeHeadline": "আপনার বিশ্ব, সরাসরি",
"yourStationsTitle": "আপনার স্টেশন" "yourStationsTitle": "আপনার স্টেশন",
"nowListeningLabel": "এখন শোনা হচ্ছে",
"popularNowTitle": "এখন জনপ্রিয়"
} }
+3 -1
View File
@@ -816,5 +816,7 @@
"welcomeBullet3Title": "Musikalarme", "welcomeBullet3Title": "Musikalarme",
"welcomeCtaLabel": "Jetzt hören", "welcomeCtaLabel": "Jetzt hören",
"welcomeHeadline": "Deine Welt, live", "welcomeHeadline": "Deine Welt, live",
"yourStationsTitle": "Deine Sender" "yourStationsTitle": "Deine Sender",
"nowListeningLabel": "Läuft gerade",
"popularNowTitle": "Jetzt beliebt"
} }
+2
View File
@@ -358,6 +358,7 @@
"yourStationsTitle": "Your stations", "yourStationsTitle": "Your stations",
"seeAllAction": "See all", "seeAllAction": "See all",
"openFullPlayerTooltip": "Open full player", "openFullPlayerTooltip": "Open full player",
"nowListeningLabel": "Now listening",
"nothingPlayingTitle": "Nothing playing yet", "nothingPlayingTitle": "Nothing playing yet",
"nothingPlayingSubtitle": "Pick a station from Your stations or search to start.", "nothingPlayingSubtitle": "Pick a station from Your stations or search to start.",
"nearYou": "Near you", "nearYou": "Near you",
@@ -375,6 +376,7 @@
"exploreTrendingSubtitle": "Today", "exploreTrendingSubtitle": "Today",
"exploreNewTitle": "New", "exploreNewTitle": "New",
"exploreNewSubtitle": "This week", "exploreNewSubtitle": "This week",
"popularNowTitle": "Popular now",
"offlineBannerTitle": "No connection", "offlineBannerTitle": "No connection",
"retryAction": "Retry", "retryAction": "Retry",
"noStationsAvailable": "No stations available", "noStationsAvailable": "No stations available",
+2
View File
@@ -358,6 +358,7 @@
"yourStationsTitle": "Tus emisoras", "yourStationsTitle": "Tus emisoras",
"seeAllAction": "Ver todas", "seeAllAction": "Ver todas",
"openFullPlayerTooltip": "Abrir reproductor completo", "openFullPlayerTooltip": "Abrir reproductor completo",
"nowListeningLabel": "Escuchando ahora",
"nothingPlayingTitle": "Todavía no estás escuchando nada", "nothingPlayingTitle": "Todavía no estás escuchando nada",
"nothingPlayingSubtitle": "Elegí una emisora de Tus emisoras o buscá una para empezar.", "nothingPlayingSubtitle": "Elegí una emisora de Tus emisoras o buscá una para empezar.",
"nearYou": "Cerca de vos", "nearYou": "Cerca de vos",
@@ -375,6 +376,7 @@
"exploreTrendingSubtitle": "Hoy", "exploreTrendingSubtitle": "Hoy",
"exploreNewTitle": "Novedades", "exploreNewTitle": "Novedades",
"exploreNewSubtitle": "Esta semana", "exploreNewSubtitle": "Esta semana",
"popularNowTitle": "Populares ahora",
"offlineBannerTitle": "Sin conexión", "offlineBannerTitle": "Sin conexión",
"retryAction": "Reintentar", "retryAction": "Reintentar",
"noStationsAvailable": "No hay emisoras disponibles", "noStationsAvailable": "No hay emisoras disponibles",
+3 -1
View File
@@ -816,5 +816,7 @@
"welcomeBullet3Title": "Alarmes musicales", "welcomeBullet3Title": "Alarmes musicales",
"welcomeCtaLabel": "Commencer à écouter", "welcomeCtaLabel": "Commencer à écouter",
"welcomeHeadline": "Votre monde, en direct", "welcomeHeadline": "Votre monde, en direct",
"yourStationsTitle": "Vos stations" "yourStationsTitle": "Vos stations",
"nowListeningLabel": "En cours d'écoute",
"popularNowTitle": "Populaire maintenant"
} }
+3 -1
View File
@@ -816,5 +816,7 @@
"welcomeBullet3Title": "संगीत अलार्म", "welcomeBullet3Title": "संगीत अलार्म",
"welcomeCtaLabel": "सुनना शुरू करें", "welcomeCtaLabel": "सुनना शुरू करें",
"welcomeHeadline": "आपकी दुनिया, लाइव", "welcomeHeadline": "आपकी दुनिया, लाइव",
"yourStationsTitle": "आपके स्टेशन" "yourStationsTitle": "आपके स्टेशन",
"nowListeningLabel": "अभी सुन रहे हैं",
"popularNowTitle": "अभी लोकप्रिय"
} }
+3 -1
View File
@@ -816,5 +816,7 @@
"welcomeBullet3Title": "Alarm musik", "welcomeBullet3Title": "Alarm musik",
"welcomeCtaLabel": "Mulai mendengarkan", "welcomeCtaLabel": "Mulai mendengarkan",
"welcomeHeadline": "Duniamu, secara langsung", "welcomeHeadline": "Duniamu, secara langsung",
"yourStationsTitle": "Stasiun Anda" "yourStationsTitle": "Stasiun Anda",
"nowListeningLabel": "Sedang mendengarkan",
"popularNowTitle": "Populer sekarang"
} }
+3 -1
View File
@@ -816,5 +816,7 @@
"welcomeBullet3Title": "Sveglie musicali", "welcomeBullet3Title": "Sveglie musicali",
"welcomeCtaLabel": "Inizia ad ascoltare", "welcomeCtaLabel": "Inizia ad ascoltare",
"welcomeHeadline": "Il tuo mondo, in diretta", "welcomeHeadline": "Il tuo mondo, in diretta",
"yourStationsTitle": "Le tue emittenti" "yourStationsTitle": "Le tue emittenti",
"nowListeningLabel": "In ascolto ora",
"popularNowTitle": "Popolari ora"
} }
+3 -1
View File
@@ -816,5 +816,7 @@
"welcomeBullet3Title": "音楽アラーム", "welcomeBullet3Title": "音楽アラーム",
"welcomeCtaLabel": "聴き始める", "welcomeCtaLabel": "聴き始める",
"welcomeHeadline": "あなたの世界を、ライブで", "welcomeHeadline": "あなたの世界を、ライブで",
"yourStationsTitle": "あなたの局" "yourStationsTitle": "あなたの局",
"nowListeningLabel": "再生中",
"popularNowTitle": "今人気"
} }
+3 -1
View File
@@ -816,5 +816,7 @@
"welcomeBullet3Title": "Alarmes musicais", "welcomeBullet3Title": "Alarmes musicais",
"welcomeCtaLabel": "Começar a ouvir", "welcomeCtaLabel": "Começar a ouvir",
"welcomeHeadline": "Seu mundo, ao vivo", "welcomeHeadline": "Seu mundo, ao vivo",
"yourStationsTitle": "Suas estações" "yourStationsTitle": "Suas estações",
"nowListeningLabel": "Ouvindo agora",
"popularNowTitle": "Populares agora"
} }
+3 -1
View File
@@ -816,5 +816,7 @@
"welcomeBullet3Title": "Музыкальные будильники", "welcomeBullet3Title": "Музыкальные будильники",
"welcomeCtaLabel": "Начать слушать", "welcomeCtaLabel": "Начать слушать",
"welcomeHeadline": "Ваш мир, в прямом эфире", "welcomeHeadline": "Ваш мир, в прямом эфире",
"yourStationsTitle": "Ваши станции" "yourStationsTitle": "Ваши станции",
"nowListeningLabel": "Сейчас слушаете",
"popularNowTitle": "Популярно сейчас"
} }
+3 -1
View File
@@ -816,5 +816,7 @@
"welcomeBullet3Title": "音乐闹钟", "welcomeBullet3Title": "音乐闹钟",
"welcomeCtaLabel": "开始收听", "welcomeCtaLabel": "开始收听",
"welcomeHeadline": "你的世界,直播中", "welcomeHeadline": "你的世界,直播中",
"yourStationsTitle": "你的电台" "yourStationsTitle": "你的电台",
"nowListeningLabel": "正在收听",
"popularNowTitle": "当前热门"
} }
+12
View File
@@ -1402,6 +1402,12 @@ abstract class AppLocalizations {
/// **'Abrir reproductor completo'** /// **'Abrir reproductor completo'**
String get openFullPlayerTooltip; String get openFullPlayerTooltip;
/// No description provided for @nowListeningLabel.
///
/// In es, this message translates to:
/// **'Escuchando ahora'**
String get nowListeningLabel;
/// No description provided for @nothingPlayingTitle. /// No description provided for @nothingPlayingTitle.
/// ///
/// In es, this message translates to: /// In es, this message translates to:
@@ -1474,6 +1480,12 @@ abstract class AppLocalizations {
/// **'Esta semana'** /// **'Esta semana'**
String get exploreNewSubtitle; String get exploreNewSubtitle;
/// No description provided for @popularNowTitle.
///
/// In es, this message translates to:
/// **'Populares ahora'**
String get popularNowTitle;
/// No description provided for @offlineBannerTitle. /// No description provided for @offlineBannerTitle.
/// ///
/// In es, this message translates to: /// In es, this message translates to:
+6
View File
@@ -745,6 +745,9 @@ class AppLocalizationsAr extends AppLocalizations {
@override @override
String get openFullPlayerTooltip => 'فتح المشغل الكامل'; String get openFullPlayerTooltip => 'فتح المشغل الكامل';
@override
String get nowListeningLabel => 'الاستماع الآن';
@override @override
String get nothingPlayingTitle => 'لا تستمع إلى شيء بعد'; String get nothingPlayingTitle => 'لا تستمع إلى شيء بعد';
@@ -784,6 +787,9 @@ class AppLocalizationsAr extends AppLocalizations {
@override @override
String get exploreNewSubtitle => 'هذا الأسبوع'; String get exploreNewSubtitle => 'هذا الأسبوع';
@override
String get popularNowTitle => 'الأكثر شيوعًا الآن';
@override @override
String get offlineBannerTitle => 'لا يوجد اتصال'; String get offlineBannerTitle => 'لا يوجد اتصال';
+6
View File
@@ -748,6 +748,9 @@ class AppLocalizationsBn extends AppLocalizations {
@override @override
String get openFullPlayerTooltip => 'সম্পূর্ণ প্লেয়ার খুলুন'; String get openFullPlayerTooltip => 'সম্পূর্ণ প্লেয়ার খুলুন';
@override
String get nowListeningLabel => 'এখন শোনা হচ্ছে';
@override @override
String get nothingPlayingTitle => 'এখনও কিছু চলছে না'; String get nothingPlayingTitle => 'এখনও কিছু চলছে না';
@@ -787,6 +790,9 @@ class AppLocalizationsBn extends AppLocalizations {
@override @override
String get exploreNewSubtitle => 'এই সপ্তাহে'; String get exploreNewSubtitle => 'এই সপ্তাহে';
@override
String get popularNowTitle => 'এখন জনপ্রিয়';
@override @override
String get offlineBannerTitle => 'সংযোগ নেই'; String get offlineBannerTitle => 'সংযোগ নেই';
+6
View File
@@ -751,6 +751,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get openFullPlayerTooltip => 'Vollständigen Player öffnen'; String get openFullPlayerTooltip => 'Vollständigen Player öffnen';
@override
String get nowListeningLabel => 'Läuft gerade';
@override @override
String get nothingPlayingTitle => 'Es läuft noch nichts'; String get nothingPlayingTitle => 'Es läuft noch nichts';
@@ -790,6 +793,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get exploreNewSubtitle => 'Diese Woche'; String get exploreNewSubtitle => 'Diese Woche';
@override
String get popularNowTitle => 'Jetzt beliebt';
@override @override
String get offlineBannerTitle => 'Keine Verbindung'; String get offlineBannerTitle => 'Keine Verbindung';
+6
View File
@@ -744,6 +744,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get openFullPlayerTooltip => 'Open full player'; String get openFullPlayerTooltip => 'Open full player';
@override
String get nowListeningLabel => 'Now listening';
@override @override
String get nothingPlayingTitle => 'Nothing playing yet'; String get nothingPlayingTitle => 'Nothing playing yet';
@@ -783,6 +786,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get exploreNewSubtitle => 'This week'; String get exploreNewSubtitle => 'This week';
@override
String get popularNowTitle => 'Popular now';
@override @override
String get offlineBannerTitle => 'No connection'; String get offlineBannerTitle => 'No connection';
+6
View File
@@ -749,6 +749,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get openFullPlayerTooltip => 'Abrir reproductor completo'; String get openFullPlayerTooltip => 'Abrir reproductor completo';
@override
String get nowListeningLabel => 'Escuchando ahora';
@override @override
String get nothingPlayingTitle => 'Todavía no estás escuchando nada'; String get nothingPlayingTitle => 'Todavía no estás escuchando nada';
@@ -788,6 +791,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get exploreNewSubtitle => 'Esta semana'; String get exploreNewSubtitle => 'Esta semana';
@override
String get popularNowTitle => 'Populares ahora';
@override @override
String get offlineBannerTitle => 'Sin conexión'; String get offlineBannerTitle => 'Sin conexión';
+6
View File
@@ -753,6 +753,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get openFullPlayerTooltip => 'Ouvrir le lecteur complet'; String get openFullPlayerTooltip => 'Ouvrir le lecteur complet';
@override
String get nowListeningLabel => 'En cours d\'écoute';
@override @override
String get nothingPlayingTitle => 'Rien n\'est en cours de lecture'; String get nothingPlayingTitle => 'Rien n\'est en cours de lecture';
@@ -792,6 +795,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get exploreNewSubtitle => 'Cette semaine'; String get exploreNewSubtitle => 'Cette semaine';
@override
String get popularNowTitle => 'Populaire maintenant';
@override @override
String get offlineBannerTitle => 'Aucune connexion'; String get offlineBannerTitle => 'Aucune connexion';
+6
View File
@@ -745,6 +745,9 @@ class AppLocalizationsHi extends AppLocalizations {
@override @override
String get openFullPlayerTooltip => 'पूरा प्लेयर खोलें'; String get openFullPlayerTooltip => 'पूरा प्लेयर खोलें';
@override
String get nowListeningLabel => 'अभी सुन रहे हैं';
@override @override
String get nothingPlayingTitle => 'अभी तक कुछ नहीं चल रहा'; String get nothingPlayingTitle => 'अभी तक कुछ नहीं चल रहा';
@@ -784,6 +787,9 @@ class AppLocalizationsHi extends AppLocalizations {
@override @override
String get exploreNewSubtitle => 'इस सप्ताह'; String get exploreNewSubtitle => 'इस सप्ताह';
@override
String get popularNowTitle => 'अभी लोकप्रिय';
@override @override
String get offlineBannerTitle => 'कोई कनेक्शन नहीं'; String get offlineBannerTitle => 'कोई कनेक्शन नहीं';
+6
View File
@@ -745,6 +745,9 @@ class AppLocalizationsId extends AppLocalizations {
@override @override
String get openFullPlayerTooltip => 'Buka pemutar lengkap'; String get openFullPlayerTooltip => 'Buka pemutar lengkap';
@override
String get nowListeningLabel => 'Sedang mendengarkan';
@override @override
String get nothingPlayingTitle => 'Belum ada yang diputar'; String get nothingPlayingTitle => 'Belum ada yang diputar';
@@ -784,6 +787,9 @@ class AppLocalizationsId extends AppLocalizations {
@override @override
String get exploreNewSubtitle => 'Minggu ini'; String get exploreNewSubtitle => 'Minggu ini';
@override
String get popularNowTitle => 'Populer sekarang';
@override @override
String get offlineBannerTitle => 'Tidak ada koneksi'; String get offlineBannerTitle => 'Tidak ada koneksi';
+6
View File
@@ -751,6 +751,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get openFullPlayerTooltip => 'Apri il player completo'; String get openFullPlayerTooltip => 'Apri il player completo';
@override
String get nowListeningLabel => 'In ascolto ora';
@override @override
String get nothingPlayingTitle => 'Non stai ancora ascoltando nulla'; String get nothingPlayingTitle => 'Non stai ancora ascoltando nulla';
@@ -790,6 +793,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get exploreNewSubtitle => 'Questa settimana'; String get exploreNewSubtitle => 'Questa settimana';
@override
String get popularNowTitle => 'Popolari ora';
@override @override
String get offlineBannerTitle => 'Nessuna connessione'; String get offlineBannerTitle => 'Nessuna connessione';
+6
View File
@@ -720,6 +720,9 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get openFullPlayerTooltip => 'フルプレーヤーを開く'; String get openFullPlayerTooltip => 'フルプレーヤーを開く';
@override
String get nowListeningLabel => '再生中';
@override @override
String get nothingPlayingTitle => 'まだ何も再生されていません'; String get nothingPlayingTitle => 'まだ何も再生されていません';
@@ -758,6 +761,9 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get exploreNewSubtitle => '今週'; String get exploreNewSubtitle => '今週';
@override
String get popularNowTitle => '今人気';
@override @override
String get offlineBannerTitle => '接続なし'; String get offlineBannerTitle => '接続なし';
+6
View File
@@ -748,6 +748,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get openFullPlayerTooltip => 'Abrir player completo'; String get openFullPlayerTooltip => 'Abrir player completo';
@override
String get nowListeningLabel => 'Ouvindo agora';
@override @override
String get nothingPlayingTitle => 'Ainda não há nada tocando'; String get nothingPlayingTitle => 'Ainda não há nada tocando';
@@ -787,6 +790,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get exploreNewSubtitle => 'Nesta semana'; String get exploreNewSubtitle => 'Nesta semana';
@override
String get popularNowTitle => 'Populares agora';
@override @override
String get offlineBannerTitle => 'Sem conexão'; String get offlineBannerTitle => 'Sem conexão';
+6
View File
@@ -749,6 +749,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get openFullPlayerTooltip => 'Открыть полный плеер'; String get openFullPlayerTooltip => 'Открыть полный плеер';
@override
String get nowListeningLabel => 'Сейчас слушаете';
@override @override
String get nothingPlayingTitle => 'Пока ничего не воспроизводится'; String get nothingPlayingTitle => 'Пока ничего не воспроизводится';
@@ -788,6 +791,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get exploreNewSubtitle => 'На этой неделе'; String get exploreNewSubtitle => 'На этой неделе';
@override
String get popularNowTitle => 'Популярно сейчас';
@override @override
String get offlineBannerTitle => 'Нет подключения'; String get offlineBannerTitle => 'Нет подключения';
+6
View File
@@ -717,6 +717,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get openFullPlayerTooltip => '打开完整播放器'; String get openFullPlayerTooltip => '打开完整播放器';
@override
String get nowListeningLabel => '正在收听';
@override @override
String get nothingPlayingTitle => '目前没有正在播放的内容'; String get nothingPlayingTitle => '目前没有正在播放的内容';
@@ -755,6 +758,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get exploreNewSubtitle => '本周'; String get exploreNewSubtitle => '本周';
@override
String get popularNowTitle => '当前热门';
@override @override
String get offlineBannerTitle => '无连接'; String get offlineBannerTitle => '无连接';
+232 -42
View File
@@ -14,7 +14,6 @@ import '../tema/pluriwave_theme.dart';
import '../widgets/pluri_glass_surface.dart'; import '../widgets/pluri_glass_surface.dart';
import '../widgets/pluri_layout.dart'; import '../widgets/pluri_layout.dart';
import '../widgets/pluri_premium_widgets.dart'; import '../widgets/pluri_premium_widgets.dart';
import '../widgets/pluri_root_header.dart';
import '../widgets/pluri_sleep_timer_sheet.dart'; import '../widgets/pluri_sleep_timer_sheet.dart';
import '../widgets/visualizador_audio.dart'; import '../widgets/visualizador_audio.dart';
@@ -38,35 +37,46 @@ class _PantallaInicioState extends State<PantallaInicio> {
final theme = Theme.of(context); final theme = Theme.of(context);
final l10n = AppLocalizations.of(context); final l10n = AppLocalizations.of(context);
return CustomScrollView( // Audit 1.1 (t4 lines 49-51): a full-bleed art/gradient/noise wash sits
slivers: [ // BEHIND the whole screen, not just inside the hero card -- the Stack
// S1 (Tier 1 visual fidelity): the prototype has no global AppBar — // wrapper is new; everything that used to be the bare CustomScrollView
// this root now draws its own 56px title row instead of relying on // return value is unchanged below.
// app.dart's removed shared chrome (which is also where the return Stack(
// sleep-timer action used to live). fit: StackFit.expand,
SliverToBoxAdapter( children: [
child: PluriRootHeader( const _FondoEscuchar(),
title: l10n.navHome, CustomScrollView(
onSleepTimer: () => showPluriSleepTimerSheet(context), slivers: [
), // Audit 1.2 (t4 line 53): unlike the other 3 roots' plain 56px
), // PluriRootHeader, Escuchar's own header is a 52px "Now
// WU5 built the hero; WU6 relocated the discovery sections that // listening" eyebrow row -- the tab bar already identifies this
// used to follow it (_seccionCercanas, _seccionTendencias, // destination, so it never repeats "Escuchar" as a title.
// _chipGeneros, _errorBanner, the browse grid) into SliverToBoxAdapter(
// PantallaBuscar's landing state and DELETED them here (this WU's child: _CabeceraEscuchar(
// own task 6.5 — completing WU5's task 5.9 deferral). Escuchar's onSleepTimer: () => showPluriSleepTimerSheet(context),
// 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 // WU5 built the hero; WU6 relocated the discovery sections that
// banner already covers manual recovery on Buscar. // used to follow it (_seccionCercanas, _seccionTendencias,
const SliverToBoxAdapter(child: _EscucharHero()), // _chipGeneros, _errorBanner, the browse grid) into
SliverToBoxAdapter(child: _seccionTusEmisoras(context, theme, l10n)), // PantallaBuscar's landing state and DELETED them here (this WU's
// ADR-7(b): the mini player is hidden on this whole screen // own task 6.5 — completing WU5's task 5.9 deferral). Escuchar's
// (app.dart), so its content needs less bottom padding than every // content is now just the hero and the favorites preview below;
// other root — escucharBottomChromeInset, not the plain // pull-to-refresh was dropped along with the grid it refreshed —
// bottomChromeInset every other root/scrollable uses. // the retry button that used to live in the (now relocated) error
const SliverToBoxAdapter( // banner already covers manual recovery on Buscar.
child: SizedBox(height: PluriLayout.escucharBottomChromeInset), 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),
),
],
), ),
], ],
); );
@@ -112,9 +122,10 @@ class _PantallaInicioState extends State<PantallaInicio> {
Flexible( Flexible(
child: Text( child: Text(
l10n.yourStationsTitle, l10n.yourStationsTitle,
style: theme.textTheme.titleMedium?.copyWith( // S7 (visual fidelity, t4 line 80): the in-page
fontWeight: FontWeight.w900, // section heading style (15/w800/ls-.2), not
), // titleMedium(16)/w900.
style: context.pluriType.inPageSectionHeading,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
@@ -187,6 +198,134 @@ class _PantallaInicioState extends State<PantallaInicio> {
} }
} }
/// Audit 1.1 (t4 lines 49-51): a full-bleed backdrop behind the ENTIRE
/// screen -- 400px of the current station's own art at 50% opacity, a
/// gradient fading it into the page background by 430px, and a faint noise
/// texture. Reads `emisoraActual` ITSELF (not a parent watch), so this stays
/// the only new subscriber -- `_PantallaInicioState.build()` keeps its
/// documented S4-R5 "no root watch" rule. Renders the gradient/noise even
/// with nothing playing (there is simply no image layer to show then) so
/// the page keeps the same base depth in every state.
class _FondoEscuchar extends StatelessWidget {
const _FondoEscuchar();
static const double _altoArte = 400;
static const double _altoDegradado = 430;
@override
Widget build(BuildContext context) {
final emisora = context.select<EstadoRadio, Emisora?>(
(e) => e.emisoraActual,
);
final tokens = context.pluriTokens;
final favicon = emisora?.favicon;
return IgnorePointer(
key: const ValueKey('escuchar-background-art'),
child: Stack(
children: [
if (favicon != null && favicon.isNotEmpty)
SizedBox(
width: double.infinity,
height: _altoArte,
child: Opacity(
opacity: 0.5,
child: CachedNetworkImage(
imageUrl: favicon,
fit: BoxFit.cover,
errorWidget: (_, __, ___) => const SizedBox.shrink(),
),
),
),
SizedBox(
width: double.infinity,
height: _altoDegradado,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
tokens.deepViolet.withValues(alpha: 0.4),
tokens.deepViolet.withValues(alpha: 0.86),
tokens.deepViolet,
],
stops: const [0, 0.58, 1],
),
),
),
),
SizedBox(
width: double.infinity,
height: _altoDegradado,
child: Opacity(
opacity: 0.05,
child: Image.asset(
'assets/images/noise_soft.png',
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const SizedBox.shrink(),
),
),
),
],
),
);
}
}
/// Audit 1.2 (t4 line 53): Escuchar's own header -- 52px, an eyebrow-styled
/// "Now listening" label (not a screen title -- the tab bar already
/// identifies this destination) and a single trailing action.
///
/// The prototype also draws a `cast` icon here. This app has no casting
/// service anywhere in `lib/servicios` (checked before writing this) --
/// inventing a Chromecast-style button with no backing capability would fail
/// the same "don't invent a capability absent from the domain" test WU5/WU9
/// already established elsewhere in this app, so it is omitted, not faked.
/// The ONE real action this row keeps is the sleep timer, which used to be
/// [PluriRootHeader]'s bedtime button on this exact screen.
class _CabeceraEscuchar extends StatelessWidget {
const _CabeceraEscuchar({required this.onSleepTimer});
final VoidCallback onSleepTimer;
static const double altura = 52;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return SizedBox(
height: altura,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 10, 0),
child: Row(
children: [
Expanded(
child: Text(
l10n.nowListeningLabel,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w800,
letterSpacing: 0.3,
color: const Color(0xFFF2F7FA).withValues(alpha: 0.55),
),
),
),
IconButton(
icon: const Icon(Icons.bedtime_outlined),
iconSize: 23,
tooltip: l10n.sleepTimer,
onPressed: onSleepTimer,
),
],
),
),
);
}
}
/// WU5, design ADR-7: the Escuchar embedded player. `EstadoRadio` is the /// WU5, design ADR-7: the Escuchar embedded player. `EstadoRadio` is the
/// single source of truth (`emisoraActual` already feeds `MiniReproductor` /// single source of truth (`emisoraActual` already feeds `MiniReproductor`
/// and `PantallaReproductor`) — this hero is a third VIEW, never a third /// and `PantallaReproductor`) — this hero is a third VIEW, never a third
@@ -292,8 +431,14 @@ class _EscucharHero extends StatelessWidget {
const SizedBox(height: 6), const SizedBox(height: 6),
Text( Text(
stationName, stationName,
// Audit 1.5 (t4 line 60): 24/w800/ls-.5/height 1.12
// -- titleLarge is 22 with neither letter-spacing
// nor an explicit line height.
style: theme.textTheme.titleLarge?.copyWith( style: theme.textTheme.titleLarge?.copyWith(
fontSize: 24,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
letterSpacing: -0.5,
height: 1.12,
), ),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@@ -334,12 +479,55 @@ class _EscucharHero extends StatelessWidget {
const SizedBox(height: 14), const SizedBox(height: 14),
_FilaTransporteEscuchar(emisora: emisora), _FilaTransporteEscuchar(emisora: emisora),
const SizedBox(height: 10), const SizedBox(height: 10),
Align( // Audit 1.9 (t4 line 78): a CENTRED pill -- padding:9px 15px,
alignment: Alignment.centerLeft, // radius:999, rgba(255,255,255,.07) + border .1, 12px/w700,
child: OutlinedButton.icon( // icon expand_less 17px -- not a left-aligned OutlinedButton
icon: const Icon(Icons.tune_rounded, size: 18), // with tune_rounded at 18px.
label: Text(l10n.openFullPlayerTooltip), Center(
onPressed: () => PantallaReproductor.abrir(context, emisora), child: Material(
type: MaterialType.transparency,
child: InkWell(
borderRadius: BorderRadius.circular(999),
onTap: () => PantallaReproductor.abrir(context, emisora),
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.07),
borderRadius: BorderRadius.circular(999),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 9,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.expand_less_rounded,
size: 17,
color: theme.colorScheme.onSurface.withValues(
alpha: 0.78,
),
),
const SizedBox(width: 6),
Text(
l10n.openFullPlayerTooltip,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: theme.colorScheme.onSurface.withValues(
alpha: 0.78,
),
),
),
],
),
),
),
),
), ),
), ),
], ],
@@ -474,8 +662,10 @@ class _FilaTransporteEscuchar extends StatelessWidget {
s == EstadoReproduccion.cargando || s == EstadoReproduccion.cargando ||
s == EstadoReproduccion.reconectando; s == EstadoReproduccion.reconectando;
return SizedBox( return SizedBox(
width: 56, // Audit 1.8 (t4 line 73): 72x72 with a 38px icon, not 56x56
height: 56, // with a 28px icon.
width: 72,
height: 72,
child: FilledButton( child: FilledButton(
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
shape: const CircleBorder(), shape: const CircleBorder(),
@@ -503,7 +693,7 @@ class _FilaTransporteEscuchar extends StatelessWidget {
reproduciendo reproduciendo
? Icons.pause_rounded ? Icons.pause_rounded
: Icons.play_arrow_rounded, : Icons.play_arrow_rounded,
size: 28, size: 38,
), ),
), ),
); );
+263 -160
View File
@@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -13,6 +15,7 @@ import '../servicios/servicio_audio.dart';
import '../servicios/servicio_timer.dart'; import '../servicios/servicio_timer.dart';
import '../tema/pluri_animate.dart'; import '../tema/pluri_animate.dart';
import '../tema/pluriwave_theme.dart'; import '../tema/pluriwave_theme.dart';
import '../tema/pluriwave_tokens.dart';
import '../widgets/ecualizador_widget.dart'; import '../widgets/ecualizador_widget.dart';
import '../widgets/pluri_glass_surface.dart'; import '../widgets/pluri_glass_surface.dart';
import '../widgets/pluri_premium_widgets.dart'; import '../widgets/pluri_premium_widgets.dart';
@@ -117,83 +120,110 @@ class _PantallaReproductorState extends State<PantallaReproductor> {
); );
}, },
), ),
body: SafeArea( // Audit 2.2 (t4 lines 108-109): a full-bleed blurred backdrop of the
child: SingleChildScrollView( // station's own art sits BEHIND the scrollable content -- the Stack
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), // wrapper is new; the SafeArea/SingleChildScrollView subtree below is
child: Column( // unchanged.
children: [ body: Stack(
_ArteReproductor( fit: StackFit.expand,
emisora: emisoraActiva, children: [
estadoStream: estado.estadoStream, _FondoReproductor(emisora: emisoraActiva, tokens: tokens),
).pluriScaleIn( SafeArea(
context, child: SingleChildScrollView(
begin: 0.86, padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
duration: const Duration(milliseconds: 420), child: Column(
curve: Curves.easeOutBack, children: [
_ArteReproductor(
emisora: emisoraActiva,
estadoStream: estado.estadoStream,
).pluriScaleIn(
context,
begin: 0.86,
duration: const Duration(milliseconds: 420),
curve: Curves.easeOutBack,
),
const SizedBox(height: 18),
Text(
emisoraActiva.nombre,
// Audit 2.3 (t4 line 116): 26/w800/ls-.5/height 1.15 --
// headlineSmall is 24/w700 with neither letter-spacing
// nor an explicit line height.
style: theme.textTheme.headlineSmall?.copyWith(
fontSize: 26,
fontWeight: FontWeight.w800,
letterSpacing: -0.5,
height: 1.15,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
).pluriFadeIn(
context,
delay: const Duration(milliseconds: 150),
),
const SizedBox(height: 6),
_SubtituloInfo(emisora: emisoraActiva).pluriFadeIn(
context,
delay: const Duration(milliseconds: 190),
),
const SizedBox(height: 14),
// Audit 2.10 (t4 lines 120-122): the bars sit directly on
// the background -- no wrapping glass card. The key moves
// from the (removed) PluriGlassSurface onto the
// visualizer itself so existing finders keep working.
VisualizadorAudio(
key: const Key('player-visualizer'),
estadoStream: estado.estadoStream,
androidAudioSessionIdStream:
estado.audio.androidAudioSessionIdStream,
// Audit 2.5 (t4 lines 120-122): 30 discrete bars at
// 40px, gradient ending at 45% alpha (not the hero's
// 30%).
barras: 30,
color: tokens.warmCoral,
altura: 40,
barrasDiscretas: true,
gradienteFinAlpha: 0.45,
).pluriFadeIn(
context,
delay: const Duration(milliseconds: 270),
),
const SizedBox(height: 22),
_Controles(
estado: estado,
emisora: emisoraActiva,
esFavorito: esFavorito,
).pluriFadeSlideIn(
context,
delay: const Duration(milliseconds: 310),
beginY: 0.3,
),
const SizedBox(height: 20),
_BandejaHerramientas(
estado: estado,
eq: eq,
emisora: emisoraActiva,
compartir: _compartir,
).pluriFadeIn(
context,
delay: const Duration(milliseconds: 350),
),
const SizedBox(height: 12),
// Item 26 / audit 2.4 (t4:114-140): the quality row is
// LAST, after the tool tray -- it used to sit right after
// the subtitle, 4 positions too early.
_FilaCalidad(
estado: estado,
emisora: emisoraActiva,
).pluriFadeIn(
context,
delay: const Duration(milliseconds: 390),
),
],
), ),
const SizedBox(height: 18), ),
Text(
emisoraActiva.nombre,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
).pluriFadeIn(context, delay: const Duration(milliseconds: 150)),
const SizedBox(height: 6),
_SubtituloInfo(
emisora: emisoraActiva,
).pluriFadeIn(context, delay: const Duration(milliseconds: 190)),
const SizedBox(height: 14),
PluriGlassSurface(
key: const Key('player-visualizer'),
borderRadius: BorderRadius.circular(tokens.radiusLg),
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
child: VisualizadorAudio(
estadoStream: estado.estadoStream,
androidAudioSessionIdStream:
estado.audio.androidAudioSessionIdStream,
// Audit 2.5 (t4 lines 120-122): 30 discrete bars at 40px,
// gradient ending at 45% alpha (not the hero's 30%).
barras: 30,
color: tokens.warmCoral,
altura: 40,
barrasDiscretas: true,
gradienteFinAlpha: 0.45,
),
).pluriFadeIn(context, delay: const Duration(milliseconds: 270)),
const SizedBox(height: 22),
_Controles(
estado: estado,
emisora: emisoraActiva,
esFavorito: esFavorito,
).pluriFadeSlideIn(
context,
delay: const Duration(milliseconds: 310),
beginY: 0.3,
),
const SizedBox(height: 20),
_BandejaHerramientas(
estado: estado,
eq: eq,
emisora: emisoraActiva,
compartir: _compartir,
).pluriFadeIn(context, delay: const Duration(milliseconds: 350)),
const SizedBox(height: 12),
// Item 26 / audit 2.4 (t4:114-140): the quality row is LAST,
// after the tool tray -- it used to sit right after the
// subtitle, 4 positions too early.
_FilaCalidad(
estado: estado,
emisora: emisoraActiva,
).pluriFadeIn(context, delay: const Duration(milliseconds: 390)),
],
), ),
), ],
), ),
); );
} }
@@ -201,25 +231,32 @@ class _PantallaReproductorState extends State<PantallaReproductor> {
/// Square art (design proposal WU14 row: "square art" replaces the old /// Square art (design proposal WU14 row: "square art" replaces the old
/// circular `_WaveHero`). Loading/error overlays and the fallback icon are /// circular `_WaveHero`). Loading/error overlays and the fallback icon are
/// unchanged from the prior circular version — only the clip shape and the /// unchanged from the prior circular version.
/// decorative halo geometry changed from circle to rounded-square. ///
/// Audit 2.1 (t4 line 114): the prototype draws a FIXED 246x246 square,
/// radius 28, with only a drop shadow -- no halo, no ring. The previous
/// `width*0.62`-derived size plus a radial-gradient halo Container and a
/// bordered ring Container (neither present in the prototype) are both
/// removed; `_radio` (28) is deliberately its own constant, distinct from
/// every named token radius, mirroring this codebase's established
/// one-off-radius precedent (e.g. the ringing screen's stop button).
class _ArteReproductor extends StatelessWidget { class _ArteReproductor extends StatelessWidget {
final Emisora emisora; final Emisora emisora;
final Stream<EstadoReproduccion> estadoStream; final Stream<EstadoReproduccion> estadoStream;
const _ArteReproductor({required this.emisora, required this.estadoStream}); const _ArteReproductor({required this.emisora, required this.estadoStream});
static const double _lado = 246;
static const double _radio = 28;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final t = context.pluriTokens; final radio = BorderRadius.circular(_radio);
final size = MediaQuery.of(context).size.width * 0.62;
final radio = BorderRadius.circular(t.radiusLg);
return StreamBuilder<EstadoReproduccion>( return StreamBuilder<EstadoReproduccion>(
stream: estadoStream, stream: estadoStream,
builder: (context, snapshot) { builder: (context, snapshot) {
final reproduciendo = snapshot.data == EstadoReproduccion.reproduciendo;
// S7-R3: reconectando renders as loading, never as error. // S7-R3: reconectando renders as loading, never as error.
final cargando = final cargando =
snapshot.data == EstadoReproduccion.cargando || snapshot.data == EstadoReproduccion.cargando ||
@@ -228,83 +265,62 @@ class _ArteReproductor extends StatelessWidget {
return SizedBox( return SizedBox(
key: const Key('player-hero-art'), key: const Key('player-hero-art'),
width: size + 40, width: _lado,
height: size + 40, height: _lado,
child: Stack( child: DecoratedBox(
alignment: Alignment.center, decoration: BoxDecoration(
children: [ borderRadius: radio,
Container( boxShadow: [
width: size + 34, BoxShadow(
height: size + 34, color: Colors.black.withValues(alpha: 0.55),
decoration: BoxDecoration( blurRadius: 60,
borderRadius: BorderRadius.circular(t.radiusLg + 16), offset: const Offset(0, 26),
gradient: RadialGradient(
colors: [
t.electricMagenta.withValues(
alpha: reproduciendo ? 0.35 : 0.18,
),
t.deepViolet.withValues(alpha: 0.0),
],
),
), ),
), ],
Container( ),
width: size + 12, child: PluriGlassSurface(
height: size + 12, borderRadius: radio,
decoration: BoxDecoration( padding: EdgeInsets.zero,
borderRadius: BorderRadius.circular(t.radiusLg + 4), child: ClipRRect(
border: Border.all(color: t.glassBorder),
),
),
PluriGlassSurface(
borderRadius: radio, borderRadius: radio,
padding: EdgeInsets.zero, child: Stack(
child: SizedBox( fit: StackFit.expand,
width: size, children: [
height: size, if (emisora.favicon != null && emisora.favicon!.isNotEmpty)
child: ClipRRect( CachedNetworkImage(
borderRadius: radio, imageUrl: emisora.favicon!,
child: Stack( fit: BoxFit.cover,
fit: StackFit.expand, placeholder: (_, __) => _shimmer(theme),
children: [ errorWidget: (_, __, ___) => _iconoFallback(theme),
if (emisora.favicon != null && )
emisora.favicon!.isNotEmpty) else
CachedNetworkImage( _iconoFallback(theme),
imageUrl: emisora.favicon!, if (cargando)
fit: BoxFit.cover, Container(
placeholder: (_, __) => _shimmer(theme), color: Colors.black45,
errorWidget: (_, __, ___) => _iconoFallback(theme), child: Center(
) child: CircularProgressIndicator(
else color: theme.colorScheme.onSurface,
_iconoFallback(theme), ),
if (cargando) ),
Container( ),
color: Colors.black45, if (hayError)
child: Center( Container(
child: CircularProgressIndicator( color: Colors.black54,
color: theme.colorScheme.onSurface, child: Center(
), child: Icon(
Icons.wifi_off_rounded,
size: 56,
color: theme.colorScheme.onSurface.withValues(
alpha: 0.85,
), ),
), ),
if (hayError) ),
Container( ),
color: Colors.black54, ],
child: Center(
child: Icon(
Icons.wifi_off_rounded,
size: 56,
color: theme.colorScheme.onSurface.withValues(
alpha: 0.85,
),
),
),
),
],
),
),
), ),
), ),
], ),
), ),
); );
}, },
@@ -327,6 +343,63 @@ class _ArteReproductor extends StatelessWidget {
); );
} }
/// Audit 2.2 (t4 lines 108-109): a full-bleed blurred backdrop of the
/// CURRENT station's own art, sitting behind the whole screen. Unlike the
/// ringing screen's `_FondoArteDifuminado` (which cannot use
/// `Emisora.favicon` -- a cold-boot download after a screen-off wake would
/// hang widget tests without a mocked `HttpClient`), this screen already
/// renders `emisora.favicon` via `CachedNetworkImage` for its OWN foreground
/// art (`_ArteReproductor` above) with no such hazard -- reusing it here for
/// the backdrop is equally safe. Renders the gradient tint even without a
/// favicon, so the page keeps the same base depth in every state.
class _FondoReproductor extends StatelessWidget {
const _FondoReproductor({required this.emisora, required this.tokens});
final Emisora emisora;
final PluriWaveTokens tokens;
@override
Widget build(BuildContext context) {
final favicon = emisora.favicon;
return Positioned.fill(
key: const ValueKey('player-background-art'),
child: IgnorePointer(
child: Stack(
fit: StackFit.expand,
children: [
if (favicon != null && favicon.isNotEmpty)
ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 26, sigmaY: 26),
child: Opacity(
opacity: 0.45,
child: CachedNetworkImage(
imageUrl: favicon,
fit: BoxFit.cover,
errorWidget: (_, __, ___) => const SizedBox.shrink(),
),
),
),
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
tokens.deepViolet.withValues(alpha: 0.55),
tokens.deepViolet.withValues(alpha: 0.9),
tokens.deepViolet,
],
stops: const [0, 0.46, 1],
),
),
),
],
),
),
);
}
}
/// Single subtitle line (WU14: collapses the old `_InfoChips` `Wrap` of /// Single subtitle line (WU14: collapses the old `_InfoChips` `Wrap` of
/// separate chips — country/language now join as one line; codec/bitrate /// separate chips — country/language now join as one line; codec/bitrate
/// moved into their own [_FilaCalidad] row below). /// moved into their own [_FilaCalidad] row below).
@@ -755,9 +828,10 @@ class _Controles extends StatelessWidget {
); );
} }
return PluriGlassSurface( // Audit 2.9 (t4 lines 124-131): the transport row sits directly on
borderRadius: BorderRadius.circular(28), // the background -- the prototype draws no wrapping card here.
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
@@ -874,6 +948,13 @@ class _Controles extends StatelessWidget {
/// One tile of the 4-tile tool tray (WU14: EQ propio / Grabar / sleep timer /// One tile of the 4-tile tool tray (WU14: EQ propio / Grabar / sleep timer
/// / Compartir), each opening its own bottom sheet (Compartir invokes /// / Compartir), each opening its own bottom sheet (Compartir invokes
/// directly instead — there is no sheet content for it). /// directly instead — there is no sheet content for it).
///
/// Audit 2.8 (t4 lines 133-136): only the FIRST tile ("EQ propio") is
/// accented -- tinted background/border, brand-colour icon, bold brand
/// label. The other three share a neutral OPAQUE surface (not `listSurface`
/// at 55% alpha) with a full-weight icon/label -- this app's ambient
/// default colour, not the `tokens.electricMagenta` fallback every tile
/// used to render in regardless of [accent].
class _TileHerramienta extends StatelessWidget { class _TileHerramienta extends StatelessWidget {
const _TileHerramienta({ const _TileHerramienta({
super.key, super.key,
@@ -881,6 +962,7 @@ class _TileHerramienta extends StatelessWidget {
required this.label, required this.label,
required this.onTap, required this.onTap,
this.iconColor, this.iconColor,
this.accent = false,
}); });
final IconData icon; final IconData icon;
@@ -888,26 +970,42 @@ class _TileHerramienta extends StatelessWidget {
final VoidCallback onTap; final VoidCallback onTap;
final Color? iconColor; final Color? iconColor;
/// See class doc — `false` (neutral) is every tile except "EQ propio".
final bool accent;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final tokens = context.pluriTokens; final tokens = context.pluriTokens;
final resolvedIconColor =
iconColor ?? (accent ? tokens.electricMagenta : null);
return Material( return Material(
type: MaterialType.transparency, type: MaterialType.transparency,
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(tokens.radiusSm), borderRadius: BorderRadius.circular(tokens.radiusMd),
onTap: onTap, onTap: onTap,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 6), padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: tokens.listSurface.withValues(alpha: 0.55), color:
borderRadius: BorderRadius.circular(tokens.radiusSm), accent
? tokens.electricMagenta.withValues(alpha: 0.16)
: tokens.listSurface,
// Audit 2.8 (t4 lines 133-136): radius 18 for BOTH variants --
// `radiusMd`, not the smaller `radiusSm` every tile used before.
borderRadius: BorderRadius.circular(tokens.radiusMd),
border: Border.all(
color:
accent
? tokens.electricMagenta.withValues(alpha: 0.4)
: Colors.white.withValues(alpha: 0.09),
),
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(icon, size: 23, color: iconColor ?? tokens.electricMagenta), Icon(icon, size: 23, color: resolvedIconColor),
const SizedBox(height: 5), const SizedBox(height: 5),
Text( Text(
label, label,
@@ -915,7 +1013,11 @@ class _TileHerramienta extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: theme.textTheme.labelSmall?.copyWith( style: theme.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.w800, fontWeight: accent ? FontWeight.w800 : FontWeight.w700,
color:
accent
? tokens.electricMagenta
: theme.colorScheme.onSurface.withValues(alpha: 0.72),
), ),
), ),
], ],
@@ -952,6 +1054,7 @@ class _BandejaHerramientas extends StatelessWidget {
icon: Icons.equalizer_rounded, icon: Icons.equalizer_rounded,
label: l10n.playerToolEqLabel, label: l10n.playerToolEqLabel,
onTap: () => _mostrarHojaEq(context, eq, emisora), onTap: () => _mostrarHojaEq(context, eq, emisora),
accent: true,
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
+20 -2
View File
@@ -2,8 +2,11 @@ import 'package:flutter/material.dart';
/// Design ADR-1: the named type scale, split out of [PluriWaveTokens] into /// Design ADR-1: the named type scale, split out of [PluriWaveTokens] into
/// its own ThemeExtension so screens stop copy-pasting /// its own ThemeExtension so screens stop copy-pasting
/// `.copyWith(fontWeight: w900, letterSpacing: …)`. The set is closed at six /// `.copyWith(fontWeight: w900, letterSpacing: …)`. The set was closed at
/// styles — a seventh requires amending ADR-1. /// six styles; S7 (visual fidelity pass) amended ADR-1 to add a seventh —
/// [inPageSectionHeading] — rather than have four screens each keep
/// copy-pasting their own `titleMedium.copyWith(fontWeight: w900)` override,
/// the exact duplication this extension exists to eliminate.
/// ///
/// [eyebrowLabel] never applies `toUpperCase()` — casing is a /// [eyebrowLabel] never applies `toUpperCase()` — casing is a
/// locale-hostile transform (Turkish dotless i, and scripts with no case at /// locale-hostile transform (Turkish dotless i, and scripts with no case at
@@ -18,6 +21,7 @@ class PluriWaveTypography extends ThemeExtension<PluriWaveTypography> {
required this.cardTitle, required this.cardTitle,
required this.bodyStrong, required this.bodyStrong,
required this.eyebrowLabel, required this.eyebrowLabel,
required this.inPageSectionHeading,
}); });
/// 88 / w800 / height 1.0 / ls -2.0. Alarm ringing, inline time editor. /// 88 / w800 / height 1.0 / ls -2.0. Alarm ringing, inline time editor.
@@ -41,6 +45,14 @@ class PluriWaveTypography extends ThemeExtension<PluriWaveTypography> {
/// headers. /// headers.
final TextStyle eyebrowLabel; final TextStyle eyebrowLabel;
/// 15 / w800 / ls -0.2. S7 (visual fidelity, t4 lines 80, 153, 164, 173):
/// an in-page section heading living directly on a screen's content —
/// "Tus emisoras", "Cerca de ti", "Explorar por", "Populares ahora".
/// Distinct from both [sectionTitle] (23, a ROOT screen's own title) and
/// [screenTitle] (19, a pushed screen's AppBar title) — this is smaller
/// than either, for a heading living one level below the screen itself.
final TextStyle inPageSectionHeading;
/// Builds the scale from [family] — the SAME resolved Google Fonts /// Builds the scale from [family] — the SAME resolved Google Fonts
/// TextTheme instance [PluriWaveTheme.dark] already computes. Building the /// TextTheme instance [PluriWaveTheme.dark] already computes. Building the
/// styles anywhere else would re-merge the font family, which is the /// styles anywhere else would re-merge the font family, which is the
@@ -79,6 +91,7 @@ class PluriWaveTypography extends ThemeExtension<PluriWaveTypography> {
letterSpacing: 0.8, letterSpacing: 0.8,
color: const Color(0xFFF2F7FA).withValues(alpha: 0.42), color: const Color(0xFFF2F7FA).withValues(alpha: 0.42),
), ),
inPageSectionHeading: style(15, FontWeight.w800, letterSpacing: -0.2),
); );
} }
@@ -90,6 +103,7 @@ class PluriWaveTypography extends ThemeExtension<PluriWaveTypography> {
TextStyle? cardTitle, TextStyle? cardTitle,
TextStyle? bodyStrong, TextStyle? bodyStrong,
TextStyle? eyebrowLabel, TextStyle? eyebrowLabel,
TextStyle? inPageSectionHeading,
}) { }) {
return PluriWaveTypography( return PluriWaveTypography(
heroTime: heroTime ?? this.heroTime, heroTime: heroTime ?? this.heroTime,
@@ -98,6 +112,7 @@ class PluriWaveTypography extends ThemeExtension<PluriWaveTypography> {
cardTitle: cardTitle ?? this.cardTitle, cardTitle: cardTitle ?? this.cardTitle,
bodyStrong: bodyStrong ?? this.bodyStrong, bodyStrong: bodyStrong ?? this.bodyStrong,
eyebrowLabel: eyebrowLabel ?? this.eyebrowLabel, eyebrowLabel: eyebrowLabel ?? this.eyebrowLabel,
inPageSectionHeading: inPageSectionHeading ?? this.inPageSectionHeading,
); );
} }
@@ -117,6 +132,9 @@ class PluriWaveTypography extends ThemeExtension<PluriWaveTypography> {
bodyStrong: TextStyle.lerp(bodyStrong, other.bodyStrong, t) ?? bodyStrong, bodyStrong: TextStyle.lerp(bodyStrong, other.bodyStrong, t) ?? bodyStrong,
eyebrowLabel: eyebrowLabel:
TextStyle.lerp(eyebrowLabel, other.eyebrowLabel, t) ?? eyebrowLabel, TextStyle.lerp(eyebrowLabel, other.eyebrowLabel, t) ?? eyebrowLabel,
inPageSectionHeading:
TextStyle.lerp(inPageSectionHeading, other.inPageSectionHeading, t) ??
inPageSectionHeading,
); );
} }
} }
+294
View File
@@ -10,6 +10,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/modelos/emisora.dart'; import 'package:pluriwave/modelos/emisora.dart';
import 'package:pluriwave/pantallas/pantalla_inicio.dart'; import 'package:pluriwave/pantallas/pantalla_inicio.dart';
import 'package:pluriwave/widgets/pluri_root_header.dart';
import 'package:pluriwave/widgets/visualizador_audio.dart'; import 'package:pluriwave/widgets/visualizador_audio.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';
@@ -404,6 +405,299 @@ void main() {
expect(texto.style?.color, const Color(0xFF21D4D9)); expect(texto.style?.color, const Color(0xFF21D4D9));
}, },
); );
testWidgets(
'S7 visual fidelity: "Tus emisoras" uses the in-page section heading '
'style (15/w800/ls-.2), not titleMedium(16)/w900',
(tester) async {
_setLargeSurfaceSize(tester);
final favoritos = FakeServicioFavoritos();
final estado = EstadoRadio(
audio: FakeServicioAudio(),
favoritos: favoritos,
radio: FakeServicioRadio(),
servicioEcualizador: FakeServicioEcualizador(),
servicioGrabacion: FakeServicioGrabacionRadio(),
resolverArchivoCustom: _archivoCustomVacio,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
await tester.runAsync(estado.inicializar);
await favoritos.agregar(emisoraDemo(uuid: 'f1', nombre: 'Favorita Uno'));
await estado.cargarFavoritos();
await tester.pumpWidget(
_conProviders(estado, _testApp(const PantallaInicio())),
);
await _pumpStableFrame(tester);
final texto = tester.widget<Text>(find.text('Tus emisoras'));
expect(texto.style?.fontSize, 15, reason: 'prototype t4 line 80');
expect(texto.style?.fontWeight, FontWeight.w800);
expect(texto.style?.letterSpacing, -0.2);
},
);
testWidgets(
'visual fidelity (audit 1.1): a full-bleed art/gradient/noise wash '
'sits behind the whole screen, keyed so it can be found',
(tester) async {
_setLargeSurfaceSize(tester);
final favoritos = FakeServicioFavoritos();
final estado = EstadoRadio(
audio: FakeServicioAudio(),
favoritos: favoritos,
radio: FakeServicioRadio(),
servicioEcualizador: FakeServicioEcualizador(),
servicioGrabacion: FakeServicioGrabacionRadio(),
resolverArchivoCustom: _archivoCustomVacio,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
await tester.runAsync(estado.inicializar);
final sonando = emisoraDemo(uuid: 'f1', nombre: 'Favorita Uno');
await favoritos.agregar(sonando);
await estado.cargarFavoritos();
await tester.runAsync(() => estado.reproducir(sonando));
await tester.pumpWidget(
_conProviders(estado, _testApp(const PantallaInicio())),
);
await _pumpBounded(tester);
expect(
find.byKey(const ValueKey('escuchar-background-art')),
findsOneWidget,
reason:
'prototype t4 lines 49-51: full-bleed art at .5 opacity + a '
'gradient + a noise texture',
);
},
);
testWidgets(
'visual fidelity (audit 1.1): the background wash still renders with '
'nothing playing (gradient/noise only, no station art to show)',
(tester) async {
_setLargeSurfaceSize(tester);
final estado = EstadoRadio(
audio: FakeServicioAudio(),
favoritos: FakeServicioFavoritos(),
radio: FakeServicioRadio(),
servicioEcualizador: FakeServicioEcualizador(),
servicioGrabacion: FakeServicioGrabacionRadio(),
resolverArchivoCustom: _archivoCustomVacio,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
await tester.runAsync(estado.inicializar);
await tester.pumpWidget(
_conProviders(estado, _testApp(const PantallaInicio())),
);
await _pumpStableFrame(tester);
expect(
find.byKey(const ValueKey('escuchar-background-art')),
findsOneWidget,
);
},
);
testWidgets('visual fidelity (audit 1.2): Escuchar has its own 52px "Now '
'listening" eyebrow header, not the shared 56px PluriRootHeader', (
tester,
) async {
_setLargeSurfaceSize(tester);
final estado = EstadoRadio(
audio: FakeServicioAudio(),
favoritos: FakeServicioFavoritos(),
radio: FakeServicioRadio(),
servicioEcualizador: FakeServicioEcualizador(),
servicioGrabacion: FakeServicioGrabacionRadio(),
resolverArchivoCustom: _archivoCustomVacio,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
await tester.runAsync(estado.inicializar);
await tester.pumpWidget(
_conProviders(estado, _testApp(const PantallaInicio())),
);
await _pumpStableFrame(tester);
expect(find.byType(PluriRootHeader), findsNothing);
expect(
find.text('Escuchando ahora'),
findsOneWidget,
reason: 'prototype t4 line 53: "ESCUCHANDO AHORA"',
);
expect(find.byIcon(Icons.bedtime_outlined), findsOneWidget);
final cabeceras = tester
.widgetList<SizedBox>(find.byType(SizedBox))
.where((s) => s.height == 52);
expect(cabeceras, isNotEmpty, reason: 'prototype t4 line 53: height:52px');
});
testWidgets(
"visual fidelity (audit 1.2): the header's bedtime icon still opens "
'the shared sleep-timer sheet',
(tester) async {
_setLargeSurfaceSize(tester);
final estado = EstadoRadio(
audio: FakeServicioAudio(),
favoritos: FakeServicioFavoritos(),
radio: FakeServicioRadio(),
servicioEcualizador: FakeServicioEcualizador(),
servicioGrabacion: FakeServicioGrabacionRadio(),
resolverArchivoCustom: _archivoCustomVacio,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
await tester.runAsync(estado.inicializar);
await tester.pumpWidget(
_conProviders(estado, _testApp(const PantallaInicio())),
);
await _pumpStableFrame(tester);
await tester.tap(find.byIcon(Icons.bedtime_outlined));
await _pumpStableFrame(tester);
expect(find.byType(ActionChip), findsWidgets);
},
);
testWidgets('visual fidelity (audit 1.5): the station name is 24/w800/ls-.5/'
'height 1.12, not titleLarge(22) with no letter-spacing/height', (
tester,
) async {
_setLargeSurfaceSize(tester);
final favoritos = FakeServicioFavoritos();
final estado = EstadoRadio(
audio: FakeServicioAudio(),
favoritos: favoritos,
radio: FakeServicioRadio(),
servicioEcualizador: FakeServicioEcualizador(),
servicioGrabacion: FakeServicioGrabacionRadio(),
resolverArchivoCustom: _archivoCustomVacio,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
await tester.runAsync(estado.inicializar);
// Deliberately NOT added to favoritos: the "Tus emisoras" grid also
// renders a station's bare name, and this assertion targets the
// hero's OWN station-name Text specifically -- a favorited station
// with the same name would render twice and make `find.text` throw.
final sonando = emisoraDemo(uuid: 'f1', nombre: 'Radio Horizonte');
await tester.runAsync(() => estado.reproducir(sonando));
await tester.pumpWidget(
_conProviders(estado, _testApp(const PantallaInicio())),
);
await _pumpBounded(tester);
final texto = tester.widget<Text>(find.text('Radio Horizonte'));
expect(texto.style?.fontSize, 24, reason: 'prototype t4 line 60');
expect(texto.style?.fontWeight, FontWeight.w800);
expect(texto.style?.letterSpacing, -0.5);
expect(texto.style?.height, 1.12);
});
testWidgets(
'visual fidelity (audit 1.8): the hero play button is 72x72 with a '
'38px icon, not 56x56 with a 28px icon',
(tester) async {
_setLargeSurfaceSize(tester);
final favoritos = FakeServicioFavoritos();
final estado = EstadoRadio(
audio: FakeServicioAudio(),
favoritos: favoritos,
radio: FakeServicioRadio(),
servicioEcualizador: FakeServicioEcualizador(),
servicioGrabacion: FakeServicioGrabacionRadio(),
resolverArchivoCustom: _archivoCustomVacio,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
await tester.runAsync(estado.inicializar);
final sonando = emisoraDemo(uuid: 'f1', nombre: 'Favorita Uno');
await favoritos.agregar(sonando);
await estado.cargarFavoritos();
await tester.runAsync(() => estado.reproducir(sonando));
await tester.pumpWidget(
_conProviders(estado, _testApp(const PantallaInicio())),
);
await _pumpBounded(tester);
final botones = tester
.widgetList<SizedBox>(find.byType(SizedBox))
.where((s) => s.width == 72 && s.height == 72);
expect(botones, isNotEmpty, reason: 'prototype t4 line 73');
final iconos = tester
.widgetList<Icon>(find.byType(Icon))
.where(
(i) =>
i.icon == Icons.play_arrow_rounded ||
i.icon == Icons.pause_rounded,
);
expect(iconos, isNotEmpty);
expect(iconos.first.size, 38);
},
);
testWidgets(
'visual fidelity (audit 1.9): the EQ/record/quality pill is centred, '
'pill-shaped and uses expand_less at 17px -- not a left-aligned '
'OutlinedButton with tune_rounded',
(tester) async {
_setLargeSurfaceSize(tester);
final favoritos = FakeServicioFavoritos();
final estado = EstadoRadio(
audio: FakeServicioAudio(),
favoritos: favoritos,
radio: FakeServicioRadio(),
servicioEcualizador: FakeServicioEcualizador(),
servicioGrabacion: FakeServicioGrabacionRadio(),
resolverArchivoCustom: _archivoCustomVacio,
iniciarAutomaticamente: false,
);
addTearDown(estado.dispose);
await tester.runAsync(estado.inicializar);
final sonando = emisoraDemo(uuid: 'f1', nombre: 'Favorita Uno');
await favoritos.agregar(sonando);
await estado.cargarFavoritos();
await tester.runAsync(() => estado.reproducir(sonando));
await tester.pumpWidget(
_conProviders(estado, _testApp(const PantallaInicio())),
);
await _pumpBounded(tester);
expect(find.byType(OutlinedButton), findsNothing);
expect(
find.byIcon(Icons.expand_less_rounded),
findsOneWidget,
reason: 'prototype t4 line 78: expand_less at 17px',
);
final icono = tester.widget<Icon>(find.byIcon(Icons.expand_less_rounded));
expect(icono.size, 17);
final pildoras = tester
.widgetList<DecoratedBox>(find.byType(DecoratedBox))
.map((d) => d.decoration)
.whereType<BoxDecoration>()
.where((d) => d.borderRadius == BorderRadius.circular(999));
expect(
pildoras,
isNotEmpty,
reason: 'prototype t4 line 78: border-radius:999px pill',
);
},
);
} }
/// Mirrors the app.dart wiring: EstadoRadio owns the domain notifiers and /// Mirrors the app.dart wiring: EstadoRadio owns the domain notifiers and
@@ -9,7 +9,10 @@ import 'package:pluriwave/l10n/gen/app_localizations.dart';
import 'package:pluriwave/modelos/emisora.dart'; import 'package:pluriwave/modelos/emisora.dart';
import 'package:pluriwave/modelos/preset_ecualizador.dart'; import 'package:pluriwave/modelos/preset_ecualizador.dart';
import 'package:pluriwave/pantallas/pantalla_reproductor.dart'; import 'package:pluriwave/pantallas/pantalla_reproductor.dart';
import 'package:pluriwave/tema/pluriwave_tokens.dart';
import 'package:pluriwave/widgets/ecualizador_widget.dart'; import 'package:pluriwave/widgets/ecualizador_widget.dart';
import 'package:pluriwave/widgets/pluri_glass_surface.dart';
import 'package:pluriwave/widgets/visualizador_audio.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';
@@ -548,4 +551,157 @@ void main() {
); );
}, },
); );
group('Visual fidelity (screens 1-3 closing pass)', () {
testWidgets('audit 2.1: the hero art is a fixed 246x246, not width*0.62', (
tester,
) async {
final estado = crearEstado();
addTearDown(estado.dispose);
await montarPantalla(tester, estado);
final sizedBox = tester.widget<SizedBox>(
find.byKey(const Key('player-hero-art')),
);
expect(sizedBox.width, 246, reason: 'prototype t4 line 114');
expect(sizedBox.height, 246);
});
testWidgets(
'audit 2.2: a full-bleed backdrop sits behind the whole screen',
(tester) async {
const estacion = Emisora(
uuid: 'demo-uuid',
nombre: 'Radio Demo',
url: 'https://stream.demo/radio',
favicon: 'https://example.com/art.png',
);
final estado = crearEstado();
addTearDown(estado.dispose);
await montarPantalla(tester, estado, estacion: estacion);
expect(
find.byKey(const Key('player-background-art')),
findsOneWidget,
reason: 'prototype t4 lines 108-109: full-bleed blurred backdrop',
);
},
);
testWidgets('audit 2.2: the backdrop still renders (gradient only) with no '
'station art available', (tester) async {
final estado = crearEstado();
addTearDown(estado.dispose);
await montarPantalla(tester, estado);
expect(find.byKey(const Key('player-background-art')), findsOneWidget);
});
testWidgets('audit 2.3: the title is 26/w800/ls-.5/height 1.15, not '
'headlineSmall(24)/w700', (tester) async {
final estado = crearEstado();
addTearDown(estado.dispose);
await montarPantalla(tester, estado);
final texto = tester.widget<Text>(find.text(emisora.nombre));
expect(texto.style?.fontSize, 26, reason: 'prototype t4 line 116');
expect(texto.style?.fontWeight, FontWeight.w800);
expect(texto.style?.letterSpacing, -0.5);
expect(texto.style?.height, 1.15);
});
testWidgets(
'audit 2.8: only the EQ propio tile is accented -- tinted brand '
'background/border, the other three share a neutral opaque surface',
(tester) async {
final estado = crearEstado();
addTearDown(estado.dispose);
await montarPantalla(tester, estado);
final eqDecoration =
tester
.widget<Container>(
find
.descendant(
of: find.byKey(const Key('player-tool-eq')),
matching: find.byType(Container),
)
.first,
)
.decoration
as BoxDecoration;
expect(
eqDecoration.color,
PluriWaveTokens.brand.withValues(alpha: 0.16),
reason: 'prototype t4 line 133',
);
final recordDecoration =
tester
.widget<Container>(
find
.descendant(
of: find.byKey(const Key('player-tool-record')),
matching: find.byType(Container),
)
.first,
)
.decoration
as BoxDecoration;
expect(
recordDecoration.color,
isNot(PluriWaveTokens.brand.withValues(alpha: 0.16)),
reason: 'prototype t4 line 134: no explicit accent colour',
);
},
);
testWidgets(
'audit 2.9: the transport row is not wrapped in a glass surface '
'card',
(tester) async {
final estado = crearEstado();
addTearDown(estado.dispose);
await montarPantalla(tester, estado);
expect(
find.ancestor(
of: find.byIcon(Icons.favorite_outline_rounded),
matching: find.byType(PluriGlassSurface),
),
findsNothing,
reason: 'prototype t4 lines 124-131: bare row, no glass card',
);
},
);
testWidgets(
'audit 2.10: the visualizer is not wrapped in its own glass card',
(tester) async {
final estado = crearEstado();
addTearDown(estado.dispose);
await montarPantalla(tester, estado);
final visualizador = find.byKey(const Key('player-visualizer'));
expect(tester.widget(visualizador), isA<VisualizadorAudio>());
expect(
find.ancestor(
of: visualizador,
matching: find.byType(PluriGlassSurface),
),
findsNothing,
reason:
'prototype t4 lines 120-122: bars sit on the bare '
'background',
);
},
);
});
} }
+23 -3
View File
@@ -102,8 +102,19 @@ void main() {
); );
testWidgets( testWidgets(
'Escuchar draws its own PluriRootHeader with the tab title, no AppBar', 'Escuchar is the documented header exception: a 52px "now listening" '
'eyebrow instead of a PluriRootHeader title row, and still no AppBar',
(tester) async { (tester) async {
// The prototype gives Escuchar a STRUCTURALLY different header from
// every other root (t4 line 53 vs line 325): 52px rather than 56, an
// eyebrow at 15px/w800/55% opacity rather than a 23px title, and
// trailing actions rather than a CTA. This test originally asserted
// `PluriRootHeader` on all five roots — an over-broad premise, since
// Escuchar's header was never a title row in the design.
//
// What still holds unconditionally, and is what this guards: no
// AppBar anywhere (systemic item S1), and the sleep-timer action that
// used to live in the removed global chrome is still reachable here.
_suppressListTileInkAssertion(); _suppressListTileInkAssertion();
setLargeSurface(tester); setLargeSurface(tester);
final estado = crearEstadoRadio(); final estado = crearEstadoRadio();
@@ -112,9 +123,18 @@ void main() {
await tester.pumpWidget(testApp(estado, const PantallaInicio())); await tester.pumpWidget(testApp(estado, const PantallaInicio()));
await pumpStable(tester); await pumpStable(tester);
expect(find.byType(PluriRootHeader), findsOneWidget);
expect(titleInHeader('Listen'), findsOneWidget);
expect(find.byType(AppBar), findsNothing); expect(find.byType(AppBar), findsNothing);
expect(
find.byType(PluriRootHeader),
findsNothing,
reason: 'Escuchar uses its own eyebrow header, not the shared one',
);
expect(find.text('Now listening'), findsOneWidget);
expect(
find.byIcon(Icons.bedtime_outlined),
findsOneWidget,
reason: 'the sleep-timer action survived the global AppBar removal',
);
}, },
); );
+14
View File
@@ -65,6 +65,20 @@ void main() {
expect(type.eyebrowLabel.letterSpacing, 0.8); expect(type.eyebrowLabel.letterSpacing, 0.8);
}); });
test(
'S7 (visual fidelity): inPageSectionHeading is 15 / w800 / '
'letter-spacing -0.2 -- the ADR-1 amendment adding a seventh style',
() {
expect(
type.inPageSectionHeading.fontSize,
15,
reason: 'prototype t4 lines 80, 153, 164, 173',
);
expect(type.inPageSectionHeading.fontWeight, FontWeight.w800);
expect(type.inPageSectionHeading.letterSpacing, -0.2);
},
);
test('S9 (Tier 1 visual fidelity): eyebrowLabel bakes in the prototype ' test('S9 (Tier 1 visual fidelity): eyebrowLabel bakes in the prototype '
"colour rgba(242,247,250,.42) — it used to carry no colour at all, " "colour rgba(242,247,250,.42) — it used to carry no colour at all, "
'so it rendered at full onSurface opacity', () { 'so it rendered at full onSurface opacity', () {