Commit Graph
225 Commits
Author SHA1 Message Date
FreeTLab f4f9e87970 docs(alarmas): fix helper name typo in vacation delete comment 2026-08-01 12:08:14 +02:00
FreeTLab 597701f497 fix(alarmas): add a delete action to the vacation range edit sheet
The vacation edit sheet could save changes to an existing range but had
no way to remove it, forcing users back to the swipe-to-delete gesture
on the list. When editing (not creating) a range, the sheet now shows
an outlined delete action next to Save; it reuses the existing
confirmation dialog and EstadoAlarmas.eliminarRangoVacaciones exactly
as the swipe gesture already does, then pops on success.
2026-08-01 12:06:00 +02:00
FreeTLab 4d54908be6 fix(ui): add top-inset awareness to PluriRootHeader
PluriRootHeader rendered its 56px title/actions row flush at y=0 on
every device, since app.dart's root SafeArea(top: false) deliberately
excludes the top inset (so each root's full-bleed background paints
edge-to-edge behind the status bar) but the header itself never added
MediaQuery.paddingOf(context).top anywhere. The header now wraps its
existing 56px content row in an outer top padding equal to that inset,
so total rendered height is height + topInset while `height` keeps
meaning the content row's own height (verified no call site did
total-height math against the old fixed constant).
2026-08-01 12:00:48 +02:00
FreeTLab cfd8bc9e6a fix(auto): preserve phone-chosen station order in Android Auto folders
Android Auto's Favoritos/Todas/Mis emisoras folders always re-sorted by
a hardcoded quality criterion in ConstructorArbolAuto.hijos/hijosGrupo,
discarding whatever order the caller passed in. EstadoRadio now pushes
already-ordered snapshots (listaFavoritosManual for Favoritos, and the
ordenListas-sorted populares/emisorasCustom getters for Todas/Mis
emisoras, re-pushed immediately on cambiarOrdenListas), and hijos/
hijosGrupo stop re-sorting so that order survives into the car.
2026-08-01 11:26:24 +02:00
FreeTLab f2528c930b fix(alarmas): decode native failures with the real channel key names
The first pass read 'alarmaId'/'tipo' from the channel payload while the
native side sends 'alarmId'/'type'/'atMillis' (AlarmScheduler.kt:1389).
Every entry would have been dropped silently in production.

The tests passed because the fake was seeded with the same guessed keys,
so they confirmed the mistake instead of catching it. Decoding now goes
through FalloProgramacionNativo.fromMap -- the single place native key
names appear -- and the fixtures build through that same constructor.
2026-07-31 23:27:45 +02:00
FreeTLab a8dca83cd9 feat(alarmas): surface the three native scheduling failures in Dart
Completes the bridge the native side already exposed. AlarmScheduler and
PluriWaveAlarmService record a pre-notice that could not be armed, a
refused foreground-service start, and a per-alarm reschedule that failed
after a reboot -- but nothing read them, so all three still ended at
logcat.

EstadoAlarmas now drains them at startup and turns each into a per-alarm
exception, which the card already knows how to mark. An alarm that never
reached the OS stops looking identical to one that did.

The read is deliberately tolerant: a failure to read is logged and
swallowed, never surfaced as an alarm error, so a diagnostics gap cannot
masquerade as a scheduling problem.
2026-07-31 23:24:01 +02:00
FreeTLab 7722f204ca feat(alarmas): verify native registration after a successful save
android.programar() returning without throwing was treated as proof
the OS registered the alarm -- this is exactly the gap the reported
case fell through. guardarAlarma now cross-checks a fresh native
pending-alarm count against how many alarms Dart believes are
active-with-a-next-run right after a successful schedule call, and
records a failure for the just-saved alarm when the native count
falls short.

FakePuertoAlarmasAndroid.alarmasNativasPendientes now defaults to a
count derived from programar()/cancelar() calls (mirroring the real
native scheduler's own registry) instead of a frozen 0, while any
test that explicitly assigns the field keeps getting exactly that
value regardless of what programar/cancelar do afterward -- verified
against the full suite, no regressions.
2026-07-31 21:26:41 +02:00
FreeTLab c107c0e18a feat(alarmas): surface scheduling failures on the alarm card
Wires EstadoAlarmas.ultimaExcepcionPara into PantallaAlarmas: an
alarm with an outstanding scheduling-failure exception now shows a
calm warning line (distinguishing a pre-notice-only failure from the
alarm itself not being registered) with a tap target into the
reliability diagnostics screen. The warning is its own small tap
target nested inside the existing card InkWell, so tap-to-edit,
swipe-to-delete and the hero "Saltar" chip are untouched.

Adds alarmCardSchedulingFailedMessage/alarmCardPreNoticeFailedMessage
to all 13 ARB locales with real per-language translations (verified
against arb_parity_test and arb_anti_copy_test).
2026-07-31 21:19:57 +02:00
FreeTLab fd1b91fe9e fix(alarmas): wire scheduling failures into per-alarm exceptions
guardarAlarma/posponerAlarma/posponerProximaDesdePreaviso now record
a scheduling failure via ServicioAlarmas.registrarFalloProgramacion
on catch and clear it on a successful (re)schedule, in addition to
the existing transient EstadoAlarmas.error string. This makes the
failure visible per-alarm via ultimaExcepcionPara instead of only a
generic app-wide message.

Also fixes _sincronizarTodas: a single alarm's android.programar
throw used to abort the whole loop, silently skipping every sibling
alarm scheduled AFTER it on that pass (including on every app launch,
via inicializar). Each alarm's outcome is now independent.
2026-07-31 21:08:21 +02:00
FreeTLab 47d0b8a053 feat(alarmas): record and clear per-alarm scheduling failures
Adds ServicioAlarmas.registrarFalloProgramacion/limpiarFalloProgramacion,
persisting a scheduling-reliability failure through the same
ExcepcionAlarma model saltarProxima already uses. Only one failure
record is kept per alarm (latest attempt wins) and skipNext entries
for any alarm are never touched. EstadoAlarmas wiring follows next.
2026-07-31 21:00:51 +02:00
FreeTLab 88bd251eba fix(alarmas): scope schedule-skip exceptions to skipNext only
ExcepcionAlarma._esValida matched ANY exception tipo against an
occurrence, treating it as a user skip. Only the 'skipNext' tipo
existed until now, but the next commits reuse the same model to
record scheduling-reliability failures per alarm (so the alarms list
can surface them via ultimaExcepcionPara) -- without this guard, a
recorded failure would be silently treated as if the user asked to
skip that occurrence, corrupting scheduling. Adds tipo constants to
ExcepcionAlarma for the upcoming failure kinds.
2026-07-31 20:59:02 +02:00
FreeTLab 3f80291e78 feat(auto): equalizer folder in the browse tree, one toggle on playback
Build & Deploy PluriWave / Análisis de código (push) Successful in 26s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m14s
On-device feedback: two identical icons on the car's now-playing screen,
one of which looked dead. It worked -- but head units render custom
actions icon-first, so cycling six presets behind one static glyph was
invisible.

A monochrome icon cannot encode which of six presets is active. Android
Auto separates the idioms deliberately: custom actions for stateless
toggles, browsable lists for choosing among options.

- Playback screen keeps one action: equalizer on/off, state-aware icons
- New Ecualizador folder lists Desactivar plus the six presets by name,
  active one marked
- The preset-cycling action and its drawable are removed

Supersedes the redesign's no-equalizer-folder rule, which predated
knowing custom actions do not surface state in a car.

# Conflicts:
#	lib/l10n/app_ar.arb
#	lib/l10n/app_bn.arb
#	lib/l10n/app_de.arb
#	lib/l10n/app_en.arb
#	lib/l10n/app_es.arb
#	lib/l10n/app_fr.arb
#	lib/l10n/app_hi.arb
#	lib/l10n/app_id.arb
#	lib/l10n/app_it.arb
#	lib/l10n/app_ja.arb
#	lib/l10n/app_pt.arb
#	lib/l10n/app_ru.arb
#	lib/l10n/app_zh.arb
2026-07-31 19:37:14 +02:00
FreeTLab f19666508d fix(auto): remove the preset-cycling custom action, superseded by folder
The equalizer's preset-cycling custom action (eq_preset_siguiente) and
its ic_auto_eq_preset drawable are no longer needed now that the
"Ecualizador" folder lists all six presets directly: the folder replaces
what the cycle action did, and this frees a scarce Android Auto custom
action slot.

The on/off toggle is now the equalizer's only custom action.
2026-07-31 19:15:31 +02:00
FreeTLab 8423ccdd0c feat(auto): add an Ecualizador browsable folder with preset selection
On-device feedback showed the equalizer's preset-cycling custom action
looked dead: many head units render custom actions icon-first, and a
monochrome icon cannot legibly encode "which of six presets" the way a
browsable list's text rows can.

This adds an "Ecualizador" folder to the car's browse tree, listing
"Desactivar" first, then the six factory presets by name, with the
currently-active one marked. Selecting a preset routes through the same
playFromMediaId seam every other browse-tree leaf already uses; picking
a preset while the equalizer is off turns it on and applies that preset.

Supersedes the earlier "no equalizer folder" rule (commit 2403da3),
which predated this feedback -- see decision auto/ecualizador-diseno.

The preset-cycling custom action still coexists with the folder in this
commit; it is removed in the next one.
2026-07-31 19:11:01 +02:00
FreeTLab 049ab78acb feat(alarmas): replace one-line reliability button with full diagnostics screen
Surface all six DiagnosticoAlarmasAndroid fields instead of three: the
battery-optimization exemption and native pending-alarm count were
already collected but silently dropped by the old widget. Each failing
signal now offers a "Fix this" action that opens the right system
settings screen (exact alarms, notifications, full-screen intent,
battery optimization), guarded by SDK level and never crashing when a
ROM lacks that screen. Manufacturers known for aggressive background
killing (Xiaomi/Redmi/POCO, Huawei, Oppo, Vivo, OnePlus, Samsung) get
an honest explanation that Autostart must be enabled manually, since
there is no API to detect or grant it.

Notifications now deep-links straight to
ACTION_APP_NOTIFICATION_SETTINGS via a new openNotificationSettings
native method, instead of reusing the runtime permission popup meant
for first-time alarm creation.

New copy is added to all 13 ARB locales with real per-language
translations (not Spanish copies), verified by the ARB parity and
anti-copy tests plus the corruption scanner.
2026-07-31 19:10:52 +02:00
FreeTLab ef9705a30e feat(alarmas): add pure diagnostic mapping and autostart-guidance logic
DiagnosticoAlarmasAndroid already collected six raw reliability fields
but only three ever reached the UI. Add a pure-Dart mapping that turns
the raw snapshot into five ordered signals with a clear ok/needs-
attention state (exact alarms, notifications, full-screen intent,
battery-optimization exemption, native pending-alarm count), plus a
manufacturer check for vendors known to require manually enabling
Autostart (Xiaomi/Redmi/POCO, Huawei, Oppo, Vivo, OnePlus, Samsung),
since there is no public API to detect or grant that setting.
2026-07-31 18:32:42 +02:00
FreeTLab 25405564ee fix(auto): give the equalizer actions distinct, state-aware icons
On a car head unit the custom actions render icon-first, so two actions
sharing ic_stat_pluriwave were indistinguishable and the toggle gave no
sign of whether the equalizer was on.

Each action now has its own drawable, and the toggle swaps between
ic_auto_eq_on and ic_auto_eq_off so its state is legible at a glance.
2026-07-31 18:11:57 +02:00
FreeTLab 4168dc5019 fix(alarmas): show which days a weekday alarm actually fires on
The alarms list showed a generic "Días" label for a diasSemana alarm
instead of its actual configured days. Render the real recurrence (e.g.
"Lun, Mié, Vie") by reusing the SAME per-day abbreviation the editor's own
day-picker circles already use -- no new formatting scheme, no new ARB
keys for the days themselves.

Also surface fade/volume/vacation-pause state on the card, each only when
it is a genuinely useful deviation from the common case: a fade badge when
fadeInSegundos > 0 (reusing the existing alarmFadeInLabel key), a volume
percentage when it differs from the 85% default, and a vacation-paused
badge when the alarm is both configured to pause and a vacation range is
currently active (mirrors the exact predicate ServicioProgramacionAlarmas
already uses). One compact line, not a badge per field.

Fixes a text-collision regression in pantalla_alarmas_editor_test.dart:
opening the editor for an alarm whose own day now renders on its card
(e.g. "Lun") made a bare find.text(weekday) ambiguous against the editor's
day-picker circle with the same label -- scoped that finder to the
BottomSheet subtree.
2026-07-31 01:05:46 +02:00
FreeTLab 491585ad12 fix(eq): re-apply the equalizer after an audio-focus interruption
The equalizer stopped applying after another app interrupted audio (e.g. a
navigation app's voice prompt): play a station with EQ working, let the
prompt speak, resume -- the audio sounds flat until the station is
re-tapped.

debeReaplicarEcualizador only re-attaches the equalizer when the native
player session id actually changes. A short transient interruption keeps
the SAME session (no id rotation), so that trigger never fires, while
Android's AudioEffect framework can let a higher-priority client silently
disable this app's effect instance in the meantime.

Add reaplicarEcualizador() to ObjetivoAudioInterrumpible, implemented as a
thin delegate to the existing _activarEcualizador() (already the correct
idempotent setEnabled + re-push-gains path). ServicioAudioSession calls it
on resume-from-pause (after reanudar()) and on un-duck (after
setAtenuado(false)) -- additive to the existing session-id trigger, not a
replacement. The method takes no argument, so it can only re-assert
whatever enabled/disabled state the handler already holds -- an
interruption cycle with the equalizer OFF stays OFF.
2026-07-31 00:56:37 +02:00
FreeTLab 9eff760462 feat(auto): equalizer enable/disable and preset cycling from the car
Expose the equalizer's on/off toggle and preset choice as PlaybackStateCompat
custom actions on the now-playing screen. The redesign's removal of the
in-car equalizer FOLDER from the browse tree stays as-is (2403da3) -- this
is a different surface (playback screen custom actions, not a browse
folder) and does not reintroduce it.

Deliberately just 2 actions -- an on/off toggle plus a cycling preset
action, not one action per preset -- since Android Auto only surfaces a
limited number of custom actions. Both reuse the existing
setEcualizadorActivo/aplicarPreset entry points (the same ones
EstadoEcualizador's phone settings screen uses), so a car tap and a phone
tap behave identically and both keep the action labels in sync. Reuses the
bundled ic_stat_pluriwave drawable (the notification's own equalizer-bars
icon) -- zero new native assets. The 5-band constraint is untouched.

New pure, unit-tested functions in servicio_audio.dart: presetSiguiente,
nombrePresetVisible, controlesEcualizadorPersonalizados. New ARB keys
(eqCustomActionEnableLabel/DisableLabel/PresetLabel) across all 13 locales,
regenerated via flutter gen-l10n.
2026-07-31 00:54:05 +02:00
FreeTLab 1b0bea5492 fix(auto): fall back to on-brand artwork when a station or track has none
Stations and tracks with no artwork showed empty tiles in the car. The
browse tree's itemEmisora/_itemLocal already fell back to the rotating
station_art_* drawable via artUriPara/artUriLocal, but the "now playing"
MediaItem built when actually playing something (car tap, phone-initiated
play, folder-queue advance, direct local-track tap) did not, so the car's
now-playing screen still went blank.

Reuse the SAME artUriPara/artUriLocal fallback (already the project's one
selection scheme, mirroring PluriStationArtFallback) at every "now playing"
construction site: reproducirPorMediaId, ServicioAudio.reproducir (now via
the extracted, unit-tested mediaItemParaEmisora), construirMediaItemColaLocal
and reproducirPistaLocal.

Guard the reverse direction too: emisoraDesdeMediaItem (extracted from the
handler's private method, now unit-tested) only reflects artUri back into
Emisora.favicon when it passes faviconUsable, so the phone UI's
CachedNetworkImage widgets never attempt a doomed fetch of the car's
android.resource:// fallback URI -- they keep falling back to
PluriStationArtFallback exactly as before.
2026-07-31 00:47:26 +02:00
FreeTLab 6822432a51 feat(auto): play a local-music folder's subfolders recursively too
totalPistas counted only DIRECT audio children, so "Reproducir carpeta"/
"Aleatorio" were hidden for a folder that contains only subfolders, and
playing a folder queued only its direct tracks.

Add a bounded recursive walk (pistasRecursivas) that collects every track
beneath a folder, depth-first, sorted by name at each level. Bounded on
two independent axes to keep a single tap's native SAF round-trips and
in-memory list size predictable on a deep or wide library:
- depth: 4 levels below the tapped folder (profundidadMaximaRecursivaLocal)
- count: 500 tracks total (limitePistasRecursivasLocal)

The folder-play/shuffle actions are now offered whenever the recursive
count is > 0, and "Reproducir carpeta"/"Aleatorio" queue everything found,
not just direct children.
2026-07-31 00:43:20 +02:00
FreeTLab eea8ec31e6 fix(auto): sort local-music subfolders before files
itemsLocales sorted a folder's children by name only, mixing directories
and files. A subfolder whose name sorted after enough tracks (e.g. "Live"
behind 80 numbered tracks) landed on a later "Más..." page, making it
unreachable without paging through every track first.

Sort directories before files, then by name within each group -- the
standard file-browser convention. Subfolders now always land on page 0.
2026-07-31 00:38:39 +02:00
FreeTLab db6f4a3a11 fix(alarma-sonando): put the date line below the hero time
The prototype's order is pill (t4:415-416), then 7:30 at 88px (t4:417),
then "Lunes, 3 de agosto" at 14px (t4:419). An earlier pass rendered the
date between the pill and the time and cited "t4 line 419" as its
justification -- but that line number is where the date SITS in the
source, which is exactly why it comes last.

Both the code and the test encoded the same misreading, so the test
passed while the screen was wrong.
2026-07-30 22:22:50 +02:00
FreeTLab 3bb92c0536 fix(grabaciones): correct the gap between the storage card and rows
Issue 3 (feedback-pruebas): t4:617 draws a 16px gap between the
storage usage card and the recordings list below it, not 12.
2026-07-30 22:14:51 +02:00
FreeTLab 9a75027d57 fix(paises): correct the gap between the language and country lists
Issue 3 (feedback-pruebas): t4:260 draws a 14px gap between "Tus
idiomas" and "Todos", not 16.
2026-07-30 22:13:43 +02:00
FreeTLab 93b7ec2af9 fix(alarma-sonando): make the snooze block's vertical gaps uniform
Issue 3 (feedback-pruebas): t4:427 wraps the POSPONER eyebrow, the
snooze tiles and the Stop button in a single flex column with a
uniform gap:12 -- this screen carried a 10/14 pair instead, matching
neither the prototype nor each other.

The dismiss-guard test (protected, untouched) only asserts behaviour
via find.text/find.byType, so this pure value change is safe against
it -- re-verified empty diff after this commit.
2026-07-30 22:12:24 +02:00
FreeTLab 5bbf750b63 fix(ajustes): correct the gap between stacked settings groups
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.
2026-07-30 22:08:40 +02:00
FreeTLab 7faf56900f fix(favoritos): stop the header padding from doubling up
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.
2026-07-30 22:06:36 +02:00
FreeTLab fc866d7ec9 fix(buscar): correct the gaps between the filter row and the results
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.
2026-07-30 20:18:59 +02:00
FreeTLab 727e18737a fix(radio): persist the last-played station across restarts
EstadoRadio.emisoraActual only ever reflected in-memory state
(_emisoraSeleccionada or the live audio service), so stopping playback
and reopening the app left the Escuchar hero empty even though the
user had a station selected right before closing it.

Persist the station whenever it changes (reproducir(), and the
Android-Auto out-of-band reconciliation path) and restore it as
_emisoraSeleccionada on the next cold start, only when nothing is
already selected. This never touches the audio service directly: no
playback starts and estadoStream/estaSonando stay at their stopped
default, matching how every consumer already gates "is it playing" on
the playback-status stream rather than on emisoraActual itself.
2026-07-30 19:18:14 +02:00
FreeTLab c6ab295c54 fix(timer): show the live countdown in the sleep timer sheet
showPluriSleepTimerSheet already had a working countdown branch
(ServicioTimer.tiempoRestanteStream), but every preset and the custom
duration flow popped the sheet immediately after starting the timer --
so the countdown never rendered in the primary flow, only if the user
happened to reopen the sheet afterwards.

Stop popping the sheet on start; the existing Consumer<EstadoRadio>
already reacts to iniciarTimerDuracion's notifyListeners and swaps to
the countdown view live. Also make the sheet scroll-controlled: at a
realistic phone width the countdown's title + description + headline-
sized remaining-time text overflowed the default half-screen cap that
never mattered while the sheet always closed before that view could
render.
2026-07-30 19:14:15 +02:00
FreeTLab e75f010b98 fix(ajustes): stop settings row titles from wrapping and cutting off
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.
2026-07-30 19:08:06 +02:00
FreeTLab c7e1a212ca fix(vacaciones): edit and delete vacation ranges
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.
2026-07-30 19:05:28 +02:00
FreeTLab d1a911e587 fix(widgets): share station-art fallback across every surface
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.
2026-07-30 18:54:31 +02:00
FreeTLab 4be2156e58 fix(nav,favoritos): unclip the overflow menu and smooth the tab transition
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.
2026-07-30 18:26:25 +02:00
FreeTLab 42d35a2541 fix(buscar): filter pills, Idioma chip, results eyebrow, states screen
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.
2026-07-30 17:04:44 +02:00
FreeTLab f61b0b9163 fix(vacaciones): header Add action, info banner, dashed CTA, collapse
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.
2026-07-30 16:53:04 +02:00
FreeTLab 36d7d5f692 fix(alarma-sonando): date line, additive snooze qualifier
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.
2026-07-30 16:45:29 +02:00
FreeTLab dcb415b3f8 fix(alarmas-editor): REPETIR eyebrow, grouped card, volume row
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.
2026-07-30 16:42:04 +02:00
FreeTLab c7d137c82e fix(alarmas): New pill, banner layout, vacation pill, card recurrence
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.
2026-07-30 16:30:20 +02:00
FreeTLab dcd8488874 fix(favoritos): header actions row, chip colours
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).
2026-07-30 16:22:12 +02:00
FreeTLab 94b1e901d1 fix(paises): eyebrows outside the card, country count, header search
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.
2026-07-30 16:18:44 +02:00
FreeTLab 533896b9fa fix(ecualizador): header switch, banner radius, preset chips, glow thumb
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.
2026-07-30 16:14:11 +02:00
FreeTLab b5b9829faa fix(ajustes): group eyebrow outside the card, per-row accent icons
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.
2026-07-30 16:05:28 +02:00
FreeTLab e036f99a61 fix(bienvenida,grabaciones): headline size, CTA radius, storage caption
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.
2026-07-30 16:00:30 +02:00
FreeTLab 01615f9751 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.
2026-07-30 14:48:06 +02:00
FreeTLab 7ff156852f fix(bienvenida): match body/bullet type sizes and clip the logo mark
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.
2026-07-30 13:02:40 +02:00
FreeTLab 038ec7f3b8 fix(alarma-sonando): pulsing halo, hero-time metrics, art size, snooze icon
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.
2026-07-30 12:58:19 +02:00
FreeTLab 8fcb734ab0 fix(alarmas): tighten the gap between stacked alarm cards to 10px
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.
2026-07-30 12:45:41 +02:00