Issue 3 (feedback-pruebas): the prototype (t4:523/534/541) draws a
16px gap between the AUDIO/STATIONS/RECORDINGS/APPLICATION cards, not
12 -- a plain unwired literal that happened to collide with the
sectionGap/panelGap tokens' own value without actually citing the
prototype.
Issue 3 (feedback-pruebas): ReorderableListView.padding wrapped
header/rows/footer with a single horizontal value (16), which doubled
up on top of PluriRootHeader's own internal inset -- landing the
title at 36px instead of the 20px every other root uses -- while also
applying card-tier padding to the flat FilaEmisoraPlana rows (row
tier, matching the same widget's fix on Buscar) and leaving the
populated-state top gap at an unwired 4 that didn't match this same
screen's own empty state (0) or the footer CTA's prototype value (8).
Zeroes the list-level padding and gives the header, chip strip, rows,
and footer CTA their own correctly-tiered insets instead.
Issue 3 (partial): the results area had no top gap against the filter
row in one state and reused the horizontal constant for a vertical axis
in another. Applies the 3-tier scale properly -- row tier for
background-less placeholders, card tier for card states.
The rest of the app's spacing review is still outstanding.
FilaAjuste's title Text had no maxLines/overflow, and neither did its
trailing current-value Text. An unbounded value (e.g. a real station
name in "Emisora preferida") let the trailing Row claim unbounded
width, squeezing the title down until it wrapped across several lines
that the row's fixed height then cut short.
Constrain the title to a single ellipsized line and cap the trailing
value's width the same way. FilaAjuste backs all 12 settings rows, so
every row is protected, not just the one that happened to expose it.
Vacaciones ranges could be created but never edited or removed --
EstadoAlarmas already had crearRangoVacaciones/eliminarRangoVacaciones
with no UI affordance reaching them, and no update path at all.
Add EstadoAlarmas.editarRangoVacaciones and wire tap-to-edit /
swipe-to-delete (with confirmation) onto every range card, mirroring
the alarm list's own Dismissible + confirm-dialog pattern exactly. This
covers the active-range hero too: a freshly created range is active
immediately and only ever renders there, never in the
scheduled/past lists, so it needed the same affordances or a user's
very first range could never be fixed.
TarjetaEmisora had the only good fallback for a station with no artwork --
a deterministic pick from 4 bundled illustrations with a gradient/glyph
last resort. FilaEmisoraPlana's flat rows, the Escuchar hero, the "Tus
emisoras" grid cell, the mini player and the full player each had their
own, separate, flat primaryContainer square instead.
Extract the good fallback into PluriStationArtFallback and use it from
every one of those call sites. The selection formula (asset order,
codeUnits-sum modulo) is preserved exactly, since navegacion_auto.dart
mirrors the same formula independently for Android Auto's own drawable
rotation.
Two user-reported bugs from on-device testing.
The favourites overflow menu carried `constraints: tightFor(38x42)`,
which sizes the POPUP rather than the button -- every item was clipped to
its first letter, so users saw "M" and "E" instead of the labels. The
existing test passed throughout because find.text matches a Text widget
whether or not it is visually clipped; the new guard measures the laid-out
width instead.
The bottom bar's ink splash had no shape, painting a hard square over the
icon, and the active tab's lift, dim, icon size and label all changed
instantly while the balloon slid -- the balloon glided and its contents
teleported. All four now share the balloon's duration and curve.
Audit 6.2 (t4:292-293): active filter pills are brand-teal tinted with
a 15px close glyph and radius 10 -- was Material's own Chip theming.
Audit 6.3 (t4:294-295): an "Idioma" entry chip is now always reachable
once a search is active -- there was no standalone entry point for
language filtering before (only bundled inside "Filtros"). Opens the
same existing filter sheet rather than a new idioma-only picker.
Audit 6.4 (t4:299): the results-count line now uses eyebrowLabel
styling -- was labelLarge (14/w800).
Audit 13.3/13.4 (t4:649-651): the search-results loading skeleton
gains the missing "BUSCANDO EMISORAS..." eyebrow, and its row gap is
now 4px, not 10.
Audit 13.5/13.6 (t4:657-668): a new purpose-built _TarjetaSinResultados
replaces PluriEmptyState ONLY in the search-no-results branch (that
shared widget is left untouched for its other unrelated call sites --
favorites empty state, the discovery grid). The title now quotes the
typed query ("Sin resultados para <<jazzz>>"), and the clear-filters
pill sits INSIDE the card. New ARB key searchNoResultsForQueryTitle.
Item 6.1 (compact 60px active-search header replacing the persistent
glass pill) is NOT implemented: existing tests confirmed the quality
(bitrate) filter has NO entry point anywhere in the app besides the
header's "Filtros" pill (added first via _abrirYSeleccionarCalidad
while a country filter is ALREADY active, i.e. mid active-search).
Hiding PluriRootHeader during active search would make quality
filtering unreachable -- a real capability regression the task's own
"presentation changes, capability does not" principle forbids.
Re-plumbing where that filter lives is a bigger change than this
visual-fidelity pass should make unilaterally.
Audit 9b.1 (t4:446): a solid brand-teal "Add" header action -- the
prototype's own mid-page CTA (audit 9b.6, still present, now dashed)
is a SECOND, additional entry point, not a replacement.
Audit 9b.2 (t4:448): the teal explanatory banner ("alarms marked
pause-during-vacations won't ring...") is now ALWAYS visible -- never
rendered before. New ARB key vacationExplainerBanner, all 13 locales.
Audit 9b.6 (t4:487): the bottom CTA is now dashed-border with a
date_range glyph -- was a solid OutlinedButton with an add glyph.
Reuses the dashed-painter shape already established in
pantalla_favoritos.dart's custom-station CTA (audit 4.5), duplicated
rather than shared.
Audit 9b.7 (t4:489): "Rangos pasados" is now a collapsible row --
icon, title, count, chevron -- COLLAPSED by default, expanding on tap.
Was always fully expanded inline. Updated the pre-existing widget test
to tap-then-assert instead of asserting immediate visibility.
Item 9b.3 (the eyebrow literally reading "EN CURSO") is NOT
implemented: the eyebrow STYLING is already correct (audit 9b.4), and
the residual copy gap is a shared ARB string
(vacationSummaryActiveCountdown) also used compactly in
pantalla_alarmas.dart's vacation summary row -- diverging its wording
just for this screen's eyebrow, or forcing a shoutier tone into that
other compact usage, is not worth it for a trivial-rated copy nuance.
Audit 9.4 (t4:419): "Lunes, 3 de agosto" now renders between the
schedule pill and the hero time -- purely additive, a new sibling
Text touching neither element. New formato_fechas.dart helper
fechaLargaConDiaSemana (locale-aware via DateFormat.MMMMEEEEd).
Audit 9.10 (t4:433): the highlighted snooze tile gains a "usual"
qualifier (new ARB key alarmSnoozeUsualLabel) alongside the original
flat label -- resolved DIFFERENTLY than its sibling 9.9 (permanently
rejected, id 2525): instead of splitting the flat
alarmSnoozeOptionLabel string into two differently-sized Text nodes
(which would make it vanish from the render tree the protected
dismiss-guard test locates via find.text), a SEPARATE small Text is
added alongside it. The original label stays a single, untouched Text
node, still inside the same FilledButton the guard taps.
Verified against the full ringing-screen test surface (34 tests
across 5 files, including the protected dismiss-guard file) -- all
green, dismiss-guard file reconfirmed byte-identical to main.
Audit 8.5 (t4:381): the "REPETIR" eyebrow above the weekday circles --
never rendered before. New ARB key alarmRepeatSectionLabel.
Audit 8.6 (t4:392-401): the station picker, volume, fade-in and
vacation toggle now share ONE bordered card with sangred divider lines
-- were four separately-spaced widgets. Restyled _CampoSelectorEmisora
as a flat nav row (icon/bold label/muted value/chevron, matching
FilaAjuste's own convention) so it fits inside the card instead of
drawing its own outlined InputDecoration chrome -- used by BOTH the
primary and fallback (Advanced) station fields. Snooze duration, "use
current station", and the Advanced section stay OUTSIDE the card,
exactly where they were (audit 8.8's own documented deliberate
extras). A transparent Material sits inside the card's coloured box so
the fade-in ListTile and the vacation SwitchListTile still paint their
ink/background correctly (the same class of constraint already
documented for PluriGlassSurface elsewhere in this codebase).
Audit 8.7 (t4:396): the volume row is now a compact 112px track with a
live percentage label -- was a bare full-width Slider with no visible
value. New ArtB key alarmVolumeLabel.
Item 8.8 (extra name/type-selector/snooze-selector/"use current
station"/Advanced fields) stays exactly as documented: deliberate,
required by the hard constraint that the editor keep its date field,
fallback-station picker and sound dropdown.
Audit 7.1 (t4:325): the "New" action is a solid brand-teal pill with a
plain add glyph -- was a tonal button with auto_awesome.
Audit 7.2 (t4:326-330): the next-alarm banner is warmCoral-tinted with
the Skip chip BESIDE the text on the same row -- was an opaque default
card with the skip action stacked below as an OutlinedButton.
Audit 7.3 (t4:332): the vacation row gains a trailing "d-d MON"
date-range pill for the active-or-next range, built from the existing
EstadoAlarmas.rangoVacacionesActivo/vacacionesProximas() accessors --
new formato_fechas.dart helper, no new state.
Audit 7.4 (t4:337-345): the alarm card now shows a recurrence label
next to the giant time (reusing the existing oneTimeOption/
dailyOption/weekdaysOption strings) and a themed station-icon slot
next to the station name. The real per-station favicon is NOT
rendered here -- same network-image hazard already documented for the
ringing screen's audit 9.2 (Emisora.favicon is a network URL;
Image.network hangs widget tests without a mocked HttpClient). The
custom switch shape (52x32/26px thumb) is also left as Switch.adaptive
-- a disclosed sub-gap, not a silent drop.
Item 7.6 (_AccesoDiagnostico, an Android-reliability debug row) is a
pre-approved addition not in the prototype -- informational only, no
action needed.
Audit 4.1 (t4:216): the manage-groups (create_new_folder) and sort
(swap_vert) actions now live in PluriRootHeader's own actions slot as
two icon buttons -- were an ActionChip inside the chip strip plus a
PopupMenuButton sharing a Row with it. The prototype's own back arrow
stays absent (binding decision: this root keeps its bottom tab bar,
unlike the prototype's pushed-with-back-arrow shape) -- this closes
the remaining gap in what was a partial fix.
Audit 4.2 (t4:219-221): group filter chips are solid brand teal with
dark text when active, listSurface + a faint border when not -- was
Material's own ChoiceChip theming (electricMagenta@24% selected).
Updated pantalla_favoritos_test.dart's two "Manage lists" text finders
to locate the relocated action by key instead (the action is now an
icon-only IconButton with a tooltip, not a labelled chip).
Audit 5.4 (t4:254/260): "Tus idiomas" and "Todos" eyebrows now sit
OUTSIDE any card at title-tier (20px) padding, styled with
eyebrowLabel -- were titleMedium w900 inside a PluriGlassSurface.
"Todos" also gains its missing total count ("{title} · {count}").
Audit 5.7 (t4:252): the header gained a real `search` action -- toggles
an inline TextField that filters the country list by name or ISO
code, not a decorative no-op button. New ARB key countriesSearchHint.
Item 5.6 (station count as a subtitle line, not a trailing widget) was
already fixed as an undocumented side effect of Tier3's row rebuild
(3303bd3) -- reconfirmed by direct read, no change needed here.
Audit 11.1 (t4:566): the master enable switch moves to
PluriPushScaffold's header actions -- was the body's first
SwitchListTile row. Its realtime/pending explainer subtitle stays
behind as a plain caption so no information is lost.
Audit 11.2 (t4:571): explainer banner radius is 16 (a local one-off,
matching neither of the 3 named tokens), not radiusSm's 14.
Audit 11.3 (t4:574-577): preset chips are solid brand-teal with dark
text when active, listSurface + a faint border when not -- was
Material's own ChoiceChip theming (primaryContainer/grey).
Audit 11.5 (t4:585): band sliders use a new 20x20 _GlowSliderThumbShape
(brand-teal blurred glow + solid thumb), replacing the Material
default round thumb.
Audit 11.7 (t4:584): the dB label is brand teal at 90% alpha, not
liveGreen -- a leftover wrong colour family 11.6's slider-only fix
never touched.
Updated 2 pre-existing tests (pantalla_ajustes_ecualizador_test.dart,
pantalla_ajustes_test.dart) to locate the enable switch by key instead
of by the "Enable equalizer" text it no longer renders next to.
Audit 10.2 (t4:511): GrupoAjustes's eyebrow now sits OUTSIDE the
PluriGlassSurface, at title-tier (20px) padding -- it used to share
the card's own 16px content padding.
Audit 10.5 (t4:514/526): FilaAjuste gains an optional iconColor.
Wired on the two rows the prototype actually accents: AUDIO's
Ecualizador (brand cyan) and STATIONS' Grupos de favoritos (warmCoral).
Item 10.10 (two extra AUDIO rows -- "Calidad de streaming" and
"Reproduccion sin interrupciones") is NOT implemented: neither concept
has any backing state or service anywhere in this app today. Building
them for real means inventing two new persisted settings, and a
genuine streaming-quality preference would plausibly need to touch
the protected servicio_audio.dart to actually affect playback --
out of scope for a visual-fidelity pass. Decorative rows that open
nothing would be dead UI, which is worse than leaving the gap
disclosed.
Screen 14 / audit 14.3+14.7: welcome headline now 34px/ls-1.2 (was
headlineMedium's 28/ls-1.0), CTA restyled to a radius-18 rounded
rectangle instead of Material 3's default StadiumBorder (t4:696,715).
Screen 12 / audit 12.2+12.3: the recordings storage card now shows the
bold "X of Y used" headline ABOVE a 6px/radius-3 bar, followed by a
real folder-path + purge-policy caption below it -- was the bar first
with the "used" string as its only (small, generic) caption (t4:613).
New ARB key recordingsLibraryStorageFolderCaption, translated to all
13 locales.
Item 12.5 (folder/max-size settings living behind the header action
instead of inline on this screen) is a deliberate structural split
from WU15b; the audit itself scopes it out of this pass.
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.
Tier 4 visual fidelity, audit 14.4/14.5/14.6/14.8 (t4 lines 693-701):
body copy is 14.5px (was bodyMedium's 14), bullet titles are
13.5px/w800 and subtitles 11.5px (was 14/w800 and 12), the gap between
bullets tightens from 16 to 12, and the 76x76 logo mark is now clipped
to a 20px rounded rect instead of painted square.
Tier 4 visual fidelity, audit 9.1/9.5/9.6/9.8 (t4 lines 411-428):
- 9.1: new _HaloPulsante renders the prototype's 420px amber radial
gradient behind the hero content. Deliberately BOUNDED (one
grow-and-settle cycle), not the prototype's literal `infinite` CSS
animation: this screen's protected dismiss-guard test calls
pumpAndSettle() after every mount/interaction, and a genuinely
infinite AnimationController.repeat() would hang it forever with no
way to fix it. Skips its Animate wrapper under reduced motion, same
as every other entry animation in this app.
- 9.5: the hero time gets a local letterSpacing/height override
(-4/0.95) instead of the shared heroTime token's -2.0/1.0 — the
alarm editor's hour block is the token's other consumer and still
wants height:1, so the shared style is untouched.
- 9.6: ringing-screen art grows from 168/radiusLg(30) to 180/36 (t4
line 421), a local constant like the existing stop-button radius.
- 9.8: the POSPONER eyebrow regains its 19px warmCoral snooze icon.
All 5 protected files (including the dismiss-guard test) remain
empty-diff against main.
Tier 4 visual fidelity, audit 7.5 (t4 line 334): the prototype stacks
alarm cards with a 10px gap; the build used 12. Each gap is now keyed
per alarm id so a guard test can pin the exact value between two known
cards.
Tier 4 visual fidelity, audit 4.5 (t4 line 235): the border and the
label/icon now use the prototype's two DIFFERENT opacities
(rgba(255,255,255,.16) stroke vs rgba(242,247,250,.6) text/icon,
previously one shared 50% colour for both), padding is a uniform 14
instead of symmetric(18,16), and the icon is a plain add glyph at 20px
instead of add_circle_outline_rounded.
Tier 4 visual fidelity, audit 1.11/1.12 (t4 line 80): a pill badge next
to "Tus emisoras" now shows the TOTAL favorite count (not the 8-capped
grid size), and "Ver todas" is plain 12px/w800 brand-teal text instead
of a Material TextButton with its own padding and splash.
Item 1.4 (hero art radius) was already fixed as a side effect of an
earlier commit — no change needed here.
Tier 4 visual fidelity, audit S10/10.6/10.8/10.9 (t4 lines 514-516):
GrupoAjustes' row divider now indents 47px instead of running
full-bleed, FilaAjuste's leading icon drops from Material's 24px
default to 21px, the row title is a local 14px override of cardTitle
(14.5), and the chevron shrinks to 19px at 40% opacity instead of the
24px full-opacity default.
Item 24 / audit 5.1-5.3, 5.5 (t4:255-269): Paises' "Tus idiomas" was
a Wrap of non-interactive Chips, and the full list was a plain
ListTile with no ISO column and no onTap. Both now share one tappable
row (ISO code, name, station count, chevron); the first "Tus idiomas"
row gets the prototype's teal-tinted highlight. The one production
call site wires the tap to filter Buscar by that country's code and
pop back -- EstadoBusqueda.buscar(pais: ...) already accepts any ISO
alpha-2 code, not just the ~10 presets in the filter sheet.
Item 25 / audit 13.2 (t4:643-646): a reconnect card (rotating ring,
station name, "Reconectando...", a stop affordance) replaces the
complete absence of any reconnect signal outside a word in the mini
player. Ships WITHOUT the prototype's attempt counter: the only live
ControladorReconexion instance is a private field of
PluriWaveAudioHandler inside servicio_audio.dart, a file this task
requires stay byte-identical to main, and nothing else re-exposes it.
Reconstructing a count from estadoStream's reconectando emissions
would not be faithful (the stream can emit it many times per actual
backoff attempt), so that was deliberately not attempted.
Item 23 / audit 4.3, 12.4 (t4:226-232, 616-619): Favoritos and
Grabaciones rows were full glass cards / ListTiles with two stacked
buttons and no artwork slot. Replace with flat, background-less rows
via a new shared FilaEmisoraPlana widget (square art, name+meta, a
circular play affordance) plus a bespoke Grabaciones row (44x12
placeholder art -- recordings carry no per-station favicon, so this
is a themed fallback, not invented artwork).
Favoritos keeps "Move to list" / "Remove from favorites" behind an
overflow menu (same underlying methods, unchanged) instead of two
always-visible buttons, since dropping either would be a functional
regression the prototype's own row doesn't have to solve for.
Also 12.1 (t4:610): the Grabaciones header action is folder_open, not
a generic gear.
Item 21 / audit 9b.4-9b.5 (t4:451-462, 469-479): the active vacation
range showed a LinearProgressIndicator the prototype never draws.
Replace it with the screen's real signature element -- a start/end
date pair (day+month, weekday, connector rule) -- reused with a flat
connector for the "programados"/"pasados" rows, replacing their plain
ListTiles with proper cards.
Audit 8.1-8.4: opaque bottom-anchored sheet with a grab handle, a framed
time card and 7 circular day buttons.
The date field, fallback-station picker and sound dropdown all stay
reachable -- the prototype omits them, but presentation changes never
remove capability.
Audit 11.10/11.8 (t4 lines 570-602): the prototype draws the explainer
banner, the band sliders, "Salida activa + Guardar como preset +
Restablecer a plano" and "Emisoras con ajuste propio" as 4 separate
cards. The build nested everything in one outer PluriGlassSurface, so
it read as a single merged block, and had no reset-to-flat action at
all.
Removes the outer wrapper from _CuerpoEcualizador. EcualizadorWidget
already draws its own card (design ADR-5, unchanged); the explainer
banner already has its own tinted background (unchanged); "Emisoras
con ajuste propio" now wraps itself in its own PluriGlassSurface; a
new _TarjetaSalidaYAcciones groups Salida activa with two tappable
rows — "Guardar como preset" (relocated from a floating end-aligned
button) and the new "Restablecer a plano", which applies
PresetEcualizador.flat via the existing EstadoEcualizador.cambiarPreset
— the same call the "Plano" preset chip already makes, not a new
capability.
The 5-slider band count, servicio_ecualizador.dart and
preset_ecualizador.dart are untouched.
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.
Audit 1.10 (t4 lines 82-88): the prototype shows a 2x2+ grid of 8
favorite stations; the build was a 260px-wide horizontal strip capped
at 6.
Raises the cap to 8 and replaces the horizontal ListView with a
GridView.builder (2 columns, gap 10). Adds a dedicated _CeldaTusEmisoras
cell (44px square art radius 11, name + genre) rather than reusing
TarjetaEmisora(esCompacta: true), since that widget always renders a
favorite button and a live badge that this prototype cell never draws.
Audit 1.7/2.5 (t4 lines 66-68, 120-122): the prototype draws 30
discrete bottom-anchored bars (radius 2, vertical gradient); the
build's VisualizadorAudio painted a single continuous oscilloscope
stroke, so its `barras: 30` parameter never produced bars.
Adds an opt-in `barrasDiscretas` mode (default false, byte-identical
continuous rendering preserved for any other caller) plus a
`gradienteFinAlpha` knob for the two screens' differing gradient
end-alpha (.3 vs .45). Wires it into the Escuchar hero and the full
player, correcting the player's bar count/height to match the
prototype (26->30 bars, 46->40px) at the same time.
Audit 14.2 (t4 lines 693/706): content was top-anchored in a plain
SingleChildScrollView; the prototype anchors it to the bottom with two
flexible spacers, so the CTA sits at the screen edge. Restructured as
a scrollable Expanded region (safe on short screens / large text
scale) with the CTA pinned below it, always at the bottom.
Audit 14.1 (t4 lines 687-689): added the full-bleed blurred banner
backdrop, reusing aurora_wave_banner.png (the same asset the deleted
PluriScreenHeader used) since this app has no bundled equivalent of
the prototype's mockup-only banner.jpg.
No monetization content touched (PRO pill, "14 días", pricing card,
free-version link all remain absent per the binding no-monetization
decision).
Audit 13.1 (t4 line 641): the banner rendered wifi_off in
colorScheme.error (red) inside a generic glass card. Rebuilt using
offlineAccent (#E8879A) — a token that existed since Tier 1 with no
consumer — for the tint, border and icon, plus the prototype's title/
detail layout and a restyled Retry chip. New offlineBannerTitle key
translated to all 13 locales.
Audit 1.6 (t4 line 62): the Escuchar hero never rendered "genre ·
country · bitrate kbps" between the station name and the visualizer.
Built from fields Emisora already carries (tags/pais/bitrate) — no
new fields, no service calls — omitting gracefully whatever a station
lacks.
Audit 9.3 (t4 line 415): the ringing screen never showed its schedule
pill. Reuses the alarmScheduleOnce/alarmScheduleWeekdays ARB keys that
already existed with no consumer, plus a new alarmScheduleDaily key
(translated to all 13 locales) for the recurring-alarm case shown in
the prototype.
Audit 9.7 (t4 line 423): the station name was cardTitle (14.5px/w700),
5.5px and 100 weight units under the prototype's 20px/w800.
Audit 9.9 (snooze tiles as number-over-unit) is intentionally NOT
included in this commit: it would require the tile to stop rendering
as a single flat "{minutes} min" Text node, which is exactly what
pantalla_alarma_sonando_dismiss_guard_test.dart taps via
find.text(l10n.alarmSnoozeOptionLabel(N)) in four places. That file
must stay untouched, so this restyle is deferred pending a decision
on how to restructure the tap target safely.
Audit 9.11 (t4 line 434): the Stop button was a FilledButton in
colorScheme.primary (brand cyan), the wrong colour family for the
largest element on the ringing screen. The prototype draws a neutral
rgba(255,255,255,.08) surface with a rgba(255,255,255,.16) border and
radius 24 (none of PluriWaveTokens' three named radii).
Audit 1.3: the hero art shipped at 84 where the prototype draws 132
(t4 line 56), with a 24 corner radius rather than the shared radiusMd.
Adds a dimension guard. Every existing test on this screen asserts
behaviour only, which is why an artwork 36% too small went unnoticed.
PluriScreenHeader was a 38-radius glass hero: an aurora banner at 24%
opacity, a black-to-transparent scrim, two radial orbs, a 120px app-mark
watermark and a 56px tri-gradient glyph badge. None of it is in the
prototype (t4) -- every root's title is plain text in its own 56px
row, which PluriRootHeader (S1) already provides. Delete the class
(and its now-orphaned _Orb helper) and its four call sites (Buscar,
Favoritos x2, Alarmas, Ajustes landed with S8).
Two of those call sites carried the hero's only functional bit besides
the title, so PluriRootHeader gains an `actions` slot (rendered before
the shared bedtime button) to keep them reachable:
- Alarmas' create-alarm button (FilledButton.tonalIcon, unchanged
shape, just relocated)
- Buscar's filters entry point (the same tappable PluriStatusPill,
just relocated)
Every other retired trailing pill (Ajustes' "Secure" status,
Favoritos' collection-count badges, Alarmas' alarm-count badge) was
purely decorative and matches the prototype by simply disappearing.
S2, Tier 1 visual-fidelity pass (audit id 2521).
The prototype puts a trailing current-value string on nearly every
settings row (t4 lines 512-539, 625 -- "3 guardados", "Alfabetico",
"Espanol", "7 . 84 MB"). FilaAjuste only accepted icon/titulo/onTap,
so every row was value-blind.
Add an optional `valor` slot to FilaAjuste (13px, rgba(242,247,250,.55),
rendered before the chevron). Wire 8 of the 12 built rows to state
already available at the settings root: equalizer on/off, sleep-timer
active, favourite-group count, preferred station name, custom-station
count, sort order, recordings count-and-size (FutureBuilder over
EstadoGrabacion.listarGrabaciones), and the current language (hoisted
pantalla_ajustes_idioma.dart's native-name list to module level so the
root can read it without duplicating it). Salida de audio, Musica
local, Backup and Info's version are left without a value -- each
lacks a low-risk, deterministically-testable data source (see the
apply-progress note for the reason per row).
Reading EstadoRadio for these values through a root `context.watch`
would rebuild the whole settings list -- including the Grabaciones
FutureBuilder's disk read -- on every unrelated audio notification;
this follows the codebase's existing S4-R5 convention of narrow
`context.select` per field instead.
Ajustes' own PluriRootHeader/PluriScreenHeader edit (S2 in this same
pass) landed in this commit too, since both touched the same header
block in pantalla_ajustes.dart at the same time.
S8, Tier 1 visual-fidelity pass (audit id 2521).
The prototype (t4) draws no global app bar anywhere: every root paints
a plain ~56px title row inside its own content instead (Alarmas line
325, Ajustes line 511, Explorar line 641). app.dart wrapped every tab
in PluriWaveScaffold(appBar: AppBar(title: Text(appTitle), ...)),
adding 56dp of chrome and a "PluriWave" title the prototype never
shows.
Add PluriRootHeader, a shared 56px title-row widget reused by all 5
roots. Extract app.dart's old _mostrarTimerDialog (only reachable from
the removed AppBar action) into a free function,
showPluriSleepTimerSheet, so every root's header can open the same
sheet directly and the sleep-timer feature stays reachable from every
tab with no behaviour change.
S1, Tier 1 visual-fidelity pass (audit id 2521).
The CI Flutter SDK predates v3.41 and only exposes ReorderableListView's
onReorder; the newer onReorderItem broke the build with three analyzer
errors. onReorder exists in both SDKs, so use it and compensate for its
pre-removal newIndex internally.
Fixing the call site surfaced a real ordering bug: _onReorder located the
target neighbour in the untrimmed global list, while ServicioFavoritos
.reordenar inserts into the list after the station is removed. Dragging a
station downwards past its neighbours therefore landed it one slot too far.
Adds a mid-list downward-drag test, the only case that separates the two
coordinate spaces.
Pre-existing bug, reproduces identically before this branch's changes:
_editarGrupo (pantalla_ajustes_grupos_favoritos.dart) and
_editarTamanoMaximo (pantalla_ajustes_grabaciones.dart) each created a
TextEditingController, awaited showModalBottomSheet, then disposed the
controller immediately on resolve - racing the sheet's own close
animation, which still holds a bound TextField for a couple more
frames. Manifests as "A TextEditingController was used after being
disposed" plus a couple of cascading framework-internal symptoms.
Fix: extract each sheet's content into its own StatefulWidget
(_HojaEditarGrupo, _HojaTamanoMaximo) that owns the controller in its
own State. Flutter only calls State.dispose() once the widget is
actually removed from the tree, i.e. after the close animation
finishes, so there is no dispose-timing decision left for the caller
to get wrong.
_editarGrupo's own test previously suppressed the crash via a
FlutterError.onError override instead of fixing it; that suppression
is removed here. _editarTamanoMaximo had no coverage at all for this
interaction; added it. Strict TDD: confirmed both sites fail without
the fix (RED) before applying it (GREEN).
Build the first-run welcome surface from mockup screen 14, stripped of
its entire monetization block: no PRO pill, no "14 dias PRO gratis"
trial line, no pricing card, no secondary "free version" link. Ships
only logo, headline, body copy, exactly 3 feature bullets, and the
single "Empezar a escuchar" CTA, as a full-screen route (not a modal).
Spanish copy is re-cast from the mockup's "tu" form to the app's
established voseo register (matching ~750 existing app_es.arb lines),
using "auto" instead of "coche" per the one existing precedent. The 3
bullet icons reuse existing tokens (electricMagenta/liveGreen/
warmCoral) that already match the mockup's own hex values for them.
CTA switches to the Escuchar tab and pops the route. Wiring this
screen into the real first-launch flow (main.dart/app.dart) is left
for a follow-up unit, same shape as WU15/WU15b - this WU only covers
the isolated, tested screen per its own task list and verify command.
WU17.
Restructure pantalla_reproductor.dart onto PluriPushScaffold (design
ADR-2 - this screen is the documented single consumer of titleOverride,
a centered live/not-playing status pill, and the non-default
keyboard_arrow_down leadingIcon). Square art replaces the old circular
hero, favorite moves from the AppBar into the transport row (the
redundant live-indicator dot is dropped - the AppBar pill already covers
that signal), the old separate info chips collapse into a single
subtitle line, and a new quality row surfaces codec/bitrate with a
"Cambiar" action that reconnects the current stream (this app has no
per-station alternate-quality capability to invoke, so this reuses the
same reproducir() call the existing error-state retry button already
uses, rather than a dead button or an invented picker).
The always-expanded recording panel and the standalone sleep-timer
button both become tool-tray tiles (EQ propio / Grabar / sleep timer /
Compartir), each opening its own bottom sheet. "EQ propio" opens a sheet
hosting EcualizadorWidget - the exact same component WU13 restyled for
Settings, bound via the existing presetParaEmisora/guardarPresetPorEmisora
per-station persistence path. No second editor was created; the
multi-device-eq resolution hierarchy is untouched.
pantalla_reproductor.dart had zero test coverage before this commit (907
lines) - writing it first surfaced two pre-existing bugs blocking any
coverage at all, both fixed: initState called estado.reproducir()
directly, which notifies listeners synchronously before its first await
and threw "setState() during build" the instant the screen mounted
against a fresh Provider tree (fixed via addPostFrameCallback); and the
body Column had no scrollable ancestor and overflowed even a generously
tall viewport (fixed by wrapping it in a SingleChildScrollView, a real
UX improvement and not just a test workaround).
The three protected EQ test files (servicio_ecualizador_test.dart,
estado_ecualizador_test.dart, servicio_audio_eq_reapply_test.dart) stay
unmodified. Full suite: 730/730 green (2 skipped, unchanged), up from 713.
size:exception - realized 1,410 changed lines (25 files including this
docs update) against the 450-600 forecast: the restructured screen file
alone is 658 lines (a near-total rewrite of a 907-line file, not a
patch), its new test file (first-ever coverage) is 519 lines, and a new
test fake plus a togglePlay() override account for the rest. Not
splittable: the restructure, the tool tray, and the EQ-sheet wiring are
one cohesive change to one screen.
Restyle the Ecualizador settings screen to the new visual language while
keeping the equalizer at 5 bands (spike-resolved, Engram id 2498 - band
count is device-reported via just_audio's AndroidEqualizer, not app-chosen;
the approved mockup's 7 sliders would silently no-op on typical hardware).
- Restyle EcualizadorWidget in place: strip its internal title + preset
chip row (the pushed screen's header now carries the title), add a
habilitado parameter that greys/disables every slider when EQ is off.
Widen PresetsEcualizadorWidget additively (personalizados param) so
custom presets can join the chip row without a second implementation.
- Add servicio_presets_personalizados.dart (new file, own SharedPreferences
key eq_custom_presets_v1) for custom EQ preset persistence - kept out of
servicio_ecualizador.dart, which has an empty-git-diff success criterion
for this change. preset_ecualizador.dart is unchanged: a custom preset is
just a PresetEcualizador with a user-supplied name.
- Extend EstadoEcualizador with presetsPersonalizados,
guardarPresetPersonalizado (validates non-empty name),
eliminarPresetPersonalizado. The load is a new explicit
cargarPresetsPersonalizados(), deliberately NOT folded into
cargarPersistido(): that method is exercised ~30 times by
estado_ecualizador_test.dart (protected, must stay unmodified) via Fakes
only, with no SharedPreferences awareness in that file.
- Build out the Ecualizador screen body: base-vs-per-station explainer
banner, a "Salida activa" row surfaced on the main screen (previously
Advanced-only), an "Emisoras con ajuste propio" drill-down sourced from
the existing presetsPorEmisora map, and a "Guardar como preset" action.
New coverage lives in new files rather than touching the three protected
EQ test files: ecualizador_widget_test.dart (component-level, did not
exist before this commit), servicio_presets_personalizados_test.dart, and
estado_ecualizador_presets_personalizados_test.dart. servicio_ecualizador.dart,
servicio_audio.dart and the three protected EQ test files keep an empty
git diff. Full suite: 713/713 green (2 skipped, unchanged), up from 682.
size:exception - realized 1,954 changed lines (25 files, plus this docs
update) against the 400-550 forecast: lib/ + ARB alone is ~650 lines, near
the top of the forecast band by itself since this WU also had to build out
a screen body WU3a only stubbed; the rest is 4 test files (675 lines) and
11 new ARB keys regenerating 13 lib/l10n/gen files (~546 lines) - the same
pattern every prior work unit in this branch has hit. Not splittable: WU14
reuses this unit's editor component by exact runtime type and cannot begin
until this lands as a whole.
Replace the glass-card container with a full-bleed blurred-art
background, giant heroTime display, 3 fixed snooze tiles (3/5/10 min,
one highlighted), and a full-width stop pill. The status label now
also renders a static "Subiendo volumen" line - no seconds counter -
when the alarm has a configured fade-in, per resolution 4: the
native-to-Flutter progress channel a live counter would need is
deliberately absent from this architecture.
The dismiss guard and force-stop retry banner are untouched: the
banner is byte-identical to its pre-restyle form, only repositioned,
and the guard test's own diff against main stays empty.
size:exception: 530 changed lines (440+/90-) against the 200-300
forecast - lib/ alone is 302 lines, at the edge of the band; the two
touched test files account for the rest. Not split further: this is
one cohesive restyle to the single screen in this branch where an
inconsistent intermediate state is least acceptable.
Replace the native showTimePicker dialog in the alarm editor sheet with
a giant inline HH:MM editor (drag/tap to adjust, wraps at 23:59-00:00).
Weekday circles now render unconditionally (disabled outside weekly
mode) instead of being gated behind an `if`.
The date field, fallback-station picker, and sound dropdown are not
dropped: per resolution 3 they move into a collapsed "Advanced" section
so the mockup's weekday-circles-only layout does not lose capability.
Volume/fade-in sliders get a cosmetic type-scale restyle only.
size:exception: 993 changed lines (891+/102-) against the 500-650
forecast - lib/ production code alone is 429 lines, within band; new
test files and 13 regenerated l10n/gen files account for the rest, the
same pattern every prior work unit in this branch has hit.
Add the Vacaciones manager screen per design ADR-6: an active-range
hero (name, days-remaining countdown, determinate progress bar, and a
per-alarm pause-impact line), a "PROGRAMADOS" upcoming-ranges list, an
"Add range" CTA, and a "Rangos pasados" history section. This is the
real destination WU8's Alarmas-root summary row pushes to, replacing
WU8's own temporary placeholder (_PantallaVacacionesTemporal, now
deleted).
EstadoAlarmas gains 4 pure query methods (rangoVacacionesActivo,
vacacionesProximas, vacacionesPasadas, impactoDeRango) -- read-only
over _alarmas/_vacaciones, no writes, no rescheduling, no native
bridge calls. impactoDeRango mirrors ServicioProgramacionAlarmas's own
pause predicate exactly, so the screen never disagrees with the
scheduler about which alarms are paused. ImpactoVacaciones joins
RangoVacaciones in alarma_musical.dart.
The add-range form (_EditorVacacionesSheet, _PickerButton) moved
verbatim from pantalla_alarmas.dart to its one remaining consumer.
estado_alarmas.dart's scheduling/snooze paths and the ringing screen's
dismiss guard are untouched; both test files pass unmodified.
New ARB keys (en/es only; other 11 locales are WU18's job):
vacationImpact{Paused,Continues}Label, vacationUpcomingSectionTitle,
vacationPastSectionTitle, addVacationRangeCta,
vacationNoActiveRangeHint.