docs(sdd): add delta specs for the functional redesign
This commit is contained in:
@@ -0,0 +1,75 @@
|
|||||||
|
# Alarm Vacation Ranges Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Active vacation-range detection, per-alarm pause-impact computation,
|
||||||
|
upcoming ranges, past-ranges history, and the summary-row entry point from
|
||||||
|
the Alarmas root into a dedicated Vacaciones manager screen. Traceable to
|
||||||
|
WU8 (summary row), WU9 (manager screen + `EstadoAlarmas` queries).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Active Range Detection
|
||||||
|
|
||||||
|
`EstadoAlarmas` MUST expose a query returning the currently-active vacation
|
||||||
|
range (if today falls within one), including days remaining. It MUST
|
||||||
|
return no active range when today falls within none.
|
||||||
|
|
||||||
|
#### Scenario: Today falls within a range
|
||||||
|
|
||||||
|
- GIVEN a vacation range whose start-to-end span includes today
|
||||||
|
- WHEN the active-range query runs
|
||||||
|
- THEN it returns that range with a computed days-remaining count
|
||||||
|
|
||||||
|
#### Scenario: No active range
|
||||||
|
|
||||||
|
- GIVEN no configured range includes today
|
||||||
|
- WHEN the active-range query runs
|
||||||
|
- THEN it returns no active range
|
||||||
|
|
||||||
|
### Requirement: Per-Alarm Pause-Impact Computation
|
||||||
|
|
||||||
|
For the currently-active range, the system MUST compute which alarms are
|
||||||
|
paused (`sonarEnVacaciones == false`) and which are unaffected
|
||||||
|
(`sonarEnVacaciones == true`), producing data sufficient to render an
|
||||||
|
impact line (e.g. "Pausa 07:30 y 13:45 · 08:15 sigue").
|
||||||
|
|
||||||
|
#### Scenario: Mixed paused and unaffected alarms
|
||||||
|
|
||||||
|
- GIVEN 3 alarms exist, 2 with `sonarEnVacaciones == false` and 1 with `true`, during an active range
|
||||||
|
- WHEN the impact computation runs
|
||||||
|
- THEN it lists exactly those 2 as paused and exactly that 1 as unaffected
|
||||||
|
|
||||||
|
### Requirement: Upcoming Ranges Query
|
||||||
|
|
||||||
|
`EstadoAlarmas` MUST expose a query returning ranges that have not yet
|
||||||
|
started, ordered soonest-first.
|
||||||
|
|
||||||
|
#### Scenario: Two future ranges
|
||||||
|
|
||||||
|
- GIVEN 2 ranges with future start dates
|
||||||
|
- WHEN the upcoming-ranges query runs
|
||||||
|
- THEN both are returned, ordered by soonest start date first
|
||||||
|
|
||||||
|
### Requirement: Past-Ranges History Query
|
||||||
|
|
||||||
|
`EstadoAlarmas` MUST expose a query returning ranges whose end date has
|
||||||
|
already passed, distinct from the active and upcoming queries.
|
||||||
|
|
||||||
|
#### Scenario: A range that ended yesterday
|
||||||
|
|
||||||
|
- GIVEN a range whose end date was yesterday
|
||||||
|
- WHEN the past-ranges query runs
|
||||||
|
- THEN that range appears in the past-ranges result and in neither the active nor upcoming result
|
||||||
|
|
||||||
|
### Requirement: Vacation Summary Row Replaces the Inline Panel
|
||||||
|
|
||||||
|
The Alarmas root MUST show a summary row (range count and next-range
|
||||||
|
countdown) in place of the current full inline vacation-ranges list.
|
||||||
|
Tapping the row MUST push the dedicated Vacaciones manager screen.
|
||||||
|
|
||||||
|
#### Scenario: Tapping the summary row opens the manager screen
|
||||||
|
|
||||||
|
- GIVEN the Alarmas root shows the vacation summary row
|
||||||
|
- WHEN the user taps it
|
||||||
|
- THEN the Vacaciones manager screen opens as a second-level (push-chrome) screen with no tab bar
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# App Navigation Shell Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
The 5-root tab contract, the push-chrome rule for second-level screens, the
|
||||||
|
explicit Favoritos exemption from that rule, and root-to-root switching
|
||||||
|
(Escuchar "Ver todas" to Favoritos). Traceable to WU1.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Five-Tab Root Navigation
|
||||||
|
|
||||||
|
The app MUST render exactly 5 root tabs, in order: Escuchar, Buscar,
|
||||||
|
Favoritos, Alarmas, Ajustes, using the existing `PluriIconGlyph.*` icon set.
|
||||||
|
`PluriIconGlyph.home` MUST be the Escuchar tab's icon.
|
||||||
|
|
||||||
|
#### Scenario: App launch renders the 5 tabs in order
|
||||||
|
|
||||||
|
- GIVEN the app has finished cold-start
|
||||||
|
- WHEN the bottom navigation renders
|
||||||
|
- THEN exactly 5 tabs appear, labeled Escuchar, Buscar, Favoritos, Alarmas, Ajustes, in that order
|
||||||
|
|
||||||
|
#### Scenario: Escuchar uses the home glyph
|
||||||
|
|
||||||
|
- GIVEN the bottom navigation is rendered
|
||||||
|
- WHEN the Escuchar tab's icon is inspected
|
||||||
|
- THEN it is `PluriIconGlyph.home`
|
||||||
|
|
||||||
|
### Requirement: Push-Chrome on Second-Level Screens
|
||||||
|
|
||||||
|
Every genuine second-level screen (Reproductor completo, Países, Vacaciones,
|
||||||
|
Ecualizador, Grabaciones, and every new Settings detail screen) MUST render
|
||||||
|
a 56px header with a back affordance and MUST NOT render a bottom tab bar.
|
||||||
|
|
||||||
|
#### Scenario: Reproductor completo hides the tab bar
|
||||||
|
|
||||||
|
- GIVEN the user opens Reproductor completo from Escuchar
|
||||||
|
- WHEN the screen renders
|
||||||
|
- THEN it shows a 56px header with a back arrow and no bottom tab bar
|
||||||
|
|
||||||
|
#### Scenario: A Settings detail screen hides the tab bar
|
||||||
|
|
||||||
|
- GIVEN the user pushes any Settings detail screen from the Ajustes root
|
||||||
|
- WHEN the screen renders
|
||||||
|
- THEN it shows a 56px header with a back arrow and no bottom tab bar
|
||||||
|
|
||||||
|
### Requirement: Favoritos Tab-Bar Exemption
|
||||||
|
|
||||||
|
Favoritos MUST keep its bottom tab bar. This is the one deliberate exception
|
||||||
|
to the push-chrome rule: the source mockup draws this content as a pushed
|
||||||
|
screen only because its own 4-root IA had demoted Favoritos out of the root
|
||||||
|
set. In this app's 5-tab IA, Favoritos is root-level and MUST render the tab
|
||||||
|
bar like every other root.
|
||||||
|
|
||||||
|
#### Scenario: Favoritos renders with its tab bar
|
||||||
|
|
||||||
|
- GIVEN the user is on the Favoritos tab
|
||||||
|
- WHEN the screen renders
|
||||||
|
- THEN the bottom tab bar is visible, unlike every true second-level screen
|
||||||
|
|
||||||
|
### Requirement: Root-to-Root Switching Without Push
|
||||||
|
|
||||||
|
Navigating from Escuchar's "Tus emisoras" section to the full favorites list
|
||||||
|
MUST switch the active tab index to Favoritos. It MUST NOT push a new route.
|
||||||
|
|
||||||
|
#### Scenario: "Ver todas" switches tabs, not routes
|
||||||
|
|
||||||
|
- GIVEN the user is on Escuchar with 1 or more favorite stations
|
||||||
|
- WHEN they tap "Ver todas"
|
||||||
|
- THEN the active tab index becomes Favoritos
|
||||||
|
- AND no new route is pushed onto the navigation stack (back from Favoritos returns to whatever screen preceded Escuchar, not to Escuchar itself)
|
||||||
|
|
||||||
|
### Requirement: Escuchar Tab Rename Preserves the ARB Key
|
||||||
|
|
||||||
|
The former "Inicio" tab MUST be relabeled "Escuchar" (English: "Listen") by
|
||||||
|
changing the `navHome` key's VALUE, not by introducing a new key. The key
|
||||||
|
`navHome` MUST continue to exist, unchanged, in all 13 ARB locales.
|
||||||
|
|
||||||
|
#### Scenario: English and Spanish values are updated
|
||||||
|
|
||||||
|
- GIVEN `lib/l10n/app_en.arb` and `lib/l10n/app_es.arb`
|
||||||
|
- WHEN the `navHome` key is read
|
||||||
|
- THEN `app_en.arb` returns `"Listen"` and `app_es.arb` returns `"Escuchar"`
|
||||||
|
|
||||||
|
#### Scenario: Key parity across all 13 locales (edge case)
|
||||||
|
|
||||||
|
- GIVEN all 13 ARB files
|
||||||
|
- WHEN each is checked for the `navHome` key
|
||||||
|
- THEN every file still contains the key `navHome` (value content may differ), and no file is missing it
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# EQ Custom Presets Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
User-defined preset save/name/list on top of the existing 6 fixed presets,
|
||||||
|
a per-station EQ entry point from the player's tool tray, and a
|
||||||
|
"stations with their own EQ" drill-down. **Band count stays 5** — resolved
|
||||||
|
by spike (Engram id 2498), not a pending decision. Traceable to WU13
|
||||||
|
(Settings screen) and WU14 (player tool tray, shares WU13's editor
|
||||||
|
component).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Five-Band Equalizer (Regression Guard, First-Class)
|
||||||
|
|
||||||
|
The equalizer screen MUST render exactly **5** vertical sliders.
|
||||||
|
`assert(bandas.length == 5)` in `preset_ecualizador.dart` MUST remain
|
||||||
|
unchanged. Band count is device-reported via `just_audio`'s
|
||||||
|
`AndroidEqualizer` (`params.bands`), not app-chosen; any requirement or
|
||||||
|
implementation asserting 7 bands is rejected on sight.
|
||||||
|
|
||||||
|
#### Scenario: Ecualizador screen renders 5 sliders
|
||||||
|
|
||||||
|
- GIVEN the user opens the Ecualizador settings screen
|
||||||
|
- WHEN it renders
|
||||||
|
- THEN exactly 5 vertical sliders are shown, one per band
|
||||||
|
|
||||||
|
#### Scenario: Band-count regression is rejected (edge case)
|
||||||
|
|
||||||
|
- GIVEN a future change proposes 7 sliders or a modified `bandas.length` assert
|
||||||
|
- WHEN that change is reviewed against this requirement
|
||||||
|
- THEN it fails: 5 is the resolved, binding band count for this change
|
||||||
|
|
||||||
|
### Requirement: Custom Preset Save
|
||||||
|
|
||||||
|
The user MUST be able to save the current 5-band configuration as a new,
|
||||||
|
named custom preset, which then appears alongside the 6 fixed presets in
|
||||||
|
the preset chip row.
|
||||||
|
|
||||||
|
#### Scenario: Saving a custom preset
|
||||||
|
|
||||||
|
- GIVEN the user has adjusted the 5 sliders from a fixed preset
|
||||||
|
- WHEN they choose "Guardar como preset" and enter the name "Mi preset"
|
||||||
|
- THEN a new preset named "Mi preset" is persisted
|
||||||
|
- AND it appears in the preset chip row on next render
|
||||||
|
|
||||||
|
### Requirement: Custom Preset Naming Validates Non-Empty Input
|
||||||
|
|
||||||
|
The system MUST reject an empty or whitespace-only preset name without
|
||||||
|
crashing, and MUST NOT persist a preset in that case.
|
||||||
|
|
||||||
|
#### Scenario: Empty name is rejected
|
||||||
|
|
||||||
|
- GIVEN the "Guardar como preset" flow is open
|
||||||
|
- WHEN the user submits an empty name
|
||||||
|
- THEN a validation message is shown and no preset is saved
|
||||||
|
|
||||||
|
### Requirement: Per-Station EQ Entry Point From the Player
|
||||||
|
|
||||||
|
The player's tool tray MUST expose an "EQ propio" tile that opens a
|
||||||
|
per-station EQ bottom sheet. That sheet MUST reuse the same 5-band editor
|
||||||
|
component used by the Settings Ecualizador screen (not a separate
|
||||||
|
implementation).
|
||||||
|
|
||||||
|
#### Scenario: Opening per-station EQ from the player
|
||||||
|
|
||||||
|
- GIVEN a station is currently playing
|
||||||
|
- WHEN the user taps "EQ propio" in the tool tray
|
||||||
|
- THEN a bottom sheet opens with 5 sliders bound to that station's resolved preset
|
||||||
|
- AND changes made there apply through the existing per-station EQ persistence path
|
||||||
|
|
||||||
|
### Requirement: Stations-With-Own-EQ Drill-Down
|
||||||
|
|
||||||
|
The Settings Ecualizador screen MUST expose a row that navigates to a list
|
||||||
|
of stations that currently have a station-specific preset override,
|
||||||
|
sourced from the existing `presetsPorEmisora` map.
|
||||||
|
|
||||||
|
#### Scenario: Drill-down lists exactly the overridden stations
|
||||||
|
|
||||||
|
- GIVEN 2 stations have entries in `presetsPorEmisora` and N others do not
|
||||||
|
- WHEN the user opens the drill-down
|
||||||
|
- THEN exactly those 2 stations are listed
|
||||||
|
|
||||||
|
### Requirement: Base-vs-Per-Station Explainer Preserved
|
||||||
|
|
||||||
|
An info banner distinguishing the base (global/device) EQ from per-station
|
||||||
|
overrides MUST remain visible on the Ecualizador screen.
|
||||||
|
|
||||||
|
#### Scenario: Banner is visible on screen open
|
||||||
|
|
||||||
|
- GIVEN the user opens the Ecualizador screen
|
||||||
|
- WHEN it renders
|
||||||
|
- THEN the base-vs-per-station explainer banner is visible
|
||||||
|
|
||||||
|
### Requirement: Active Output Surfaced on the Main Screen
|
||||||
|
|
||||||
|
The "Salida activa" (active output device) row MUST be visible on the main
|
||||||
|
Ecualizador screen, not only in the Advanced sub-section.
|
||||||
|
|
||||||
|
#### Scenario: Output row updates on device change
|
||||||
|
|
||||||
|
- GIVEN the Ecualizador screen is open
|
||||||
|
- WHEN the active audio output device changes (e.g. Bluetooth connects)
|
||||||
|
- THEN the "Salida activa" row updates to reflect the new device
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Favorites Organization Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Chip-filtered flat list replacing stacked per-group panels, drag-to-reorder,
|
||||||
|
the sort action, and group management surfaced from Favoritos. Traceable to
|
||||||
|
WU4.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Chip-Filtered Flat List
|
||||||
|
|
||||||
|
Favoritos MUST render horizontally-scrollable group filter chips (e.g.
|
||||||
|
"Todas · N", one per favorite group) and a single flat list of favorite
|
||||||
|
stations filtered by the active chip, replacing the current stacked
|
||||||
|
per-group panel layout.
|
||||||
|
|
||||||
|
#### Scenario: Selecting a group chip filters the list
|
||||||
|
|
||||||
|
- GIVEN 2 or more favorite groups exist, each with members
|
||||||
|
- WHEN the user selects a specific group's chip
|
||||||
|
- THEN the flat list shows only that group's member stations
|
||||||
|
|
||||||
|
#### Scenario: "Todas" chip shows every favorite
|
||||||
|
|
||||||
|
- GIVEN the user has favorites across multiple groups
|
||||||
|
- WHEN the "Todas" chip is active
|
||||||
|
- THEN the flat list shows every favorite station regardless of group
|
||||||
|
|
||||||
|
### Requirement: Drag-to-Reorder Within the Active Filter
|
||||||
|
|
||||||
|
The user MUST be able to reorder favorite stations within the currently
|
||||||
|
active filter via a drag handle. The new order MUST persist across app
|
||||||
|
restarts.
|
||||||
|
|
||||||
|
#### Scenario: Reordering persists
|
||||||
|
|
||||||
|
- GIVEN a filtered list of 3+ favorites
|
||||||
|
- WHEN the user drags the 3rd item to the 1st position
|
||||||
|
- THEN the list reflects the new order immediately
|
||||||
|
- AND reopening the app shows the same order
|
||||||
|
|
||||||
|
### Requirement: Sort Action Using Existing Criteria
|
||||||
|
|
||||||
|
Favoritos MUST expose a sort action (`swap_vert`) using the existing
|
||||||
|
`OrdenEmisoras` criteria (name, quality). It MUST NOT introduce a sort
|
||||||
|
criterion with no backing implementation.
|
||||||
|
|
||||||
|
#### Scenario: Sort by name reorders alphabetically
|
||||||
|
|
||||||
|
- GIVEN the active filter shows 3+ favorites in a non-alphabetical order
|
||||||
|
- WHEN the user triggers sort-by-name
|
||||||
|
- THEN the list re-renders in alphabetical order
|
||||||
|
|
||||||
|
### Requirement: Group Management Reachable from Favoritos
|
||||||
|
|
||||||
|
Creating and managing favorite groups MUST be reachable directly from the
|
||||||
|
Favoritos screen, in addition to its existing entry point in Settings.
|
||||||
|
|
||||||
|
#### Scenario: Creating a group from Favoritos
|
||||||
|
|
||||||
|
- GIVEN the user is on the Favoritos screen
|
||||||
|
- WHEN they tap the create-group action
|
||||||
|
- THEN the group-creation flow opens and a newly created group appears as a new filter chip
|
||||||
|
|
||||||
|
### Requirement: Custom-Station CTA Preserved
|
||||||
|
|
||||||
|
The dashed "Añadir emisora personalizada" call-to-action MUST remain
|
||||||
|
reachable from Favoritos.
|
||||||
|
|
||||||
|
#### Scenario: Custom station CTA opens the add flow
|
||||||
|
|
||||||
|
- GIVEN the user is on Favoritos, any filter active
|
||||||
|
- WHEN they tap "Añadir emisora personalizada"
|
||||||
|
- THEN the custom-station add flow opens
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Delta for multi-device-eq
|
||||||
|
|
||||||
|
Existing spec: `openspec/specs/multi-device-eq/spec.md` (device-event
|
||||||
|
resolve-and-apply, first-seen bootstrap, cold start, connect-disconnect-
|
||||||
|
reconnect cycle, toggle-off behavior). None of that resolution logic
|
||||||
|
changes. This delta only ADDS a requirement about where the per-station
|
||||||
|
editing surface is reached from, and pins that the relocation has zero
|
||||||
|
effect on resolution. Traceable to WU14 (shares WU13's editor component).
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Per-Station EQ Entry Relocates, Resolution Logic Does Not
|
||||||
|
|
||||||
|
The per-station EQ editing surface MUST be reachable from the player's
|
||||||
|
tool tray (see `eq-custom-presets`). This relocation MUST NOT alter the
|
||||||
|
existing 4-level resolution hierarchy (matrix -> station -> device ->
|
||||||
|
global) or any of its resolve-and-apply, first-seen-bootstrap, cold-start,
|
||||||
|
or connect/disconnect/reconnect behavior.
|
||||||
|
|
||||||
|
#### Scenario: Editing per-station EQ from the player uses the existing hierarchy
|
||||||
|
|
||||||
|
- GIVEN a station has an existing entry in `presetsMatriz` or `presetsPorEmisora`
|
||||||
|
- WHEN the user edits it via the player's per-station EQ sheet
|
||||||
|
- THEN the change persists through the same resolution path already covered by `multi-device-eq`'s existing tests
|
||||||
|
- AND device-change resolve-and-apply behavior for that station is unaffected
|
||||||
|
|
||||||
|
#### Scenario: Existing multi-device-eq test suite is unaffected (regression guard)
|
||||||
|
|
||||||
|
- GIVEN the existing `multi-device-eq` scenarios (device-event resolve-and-apply, first-seen bootstrap, cold start, connect-disconnect-reconnect, toggle-off)
|
||||||
|
- WHEN the per-station entry point relocates to the player
|
||||||
|
- THEN every one of those scenarios continues to pass unmodified
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# Delta for native-alarms
|
||||||
|
|
||||||
|
Existing spec: `openspec/specs/native-alarms/spec.md` (native foreground
|
||||||
|
service, sole ring-audio ownership, fade-in curve, focus/no-volume-writes,
|
||||||
|
notification channel migration, "ring screen is pure UI"). None of that
|
||||||
|
native/audio behavior changes in this redesign — it is a visual restyle of
|
||||||
|
the Alarmas root, the alarm card, and the ringing screen. This delta ADDS
|
||||||
|
requirements for card affordances, the hero skip action, the editor's
|
||||||
|
preserved advanced fields, and the ringing screen's static status label.
|
||||||
|
Traceable to WU8 (card + hero), WU10 (editor), WU11 (ringing screen).
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Alarm Card Tap-to-Edit, Swipe-to-Delete
|
||||||
|
|
||||||
|
The simplified alarm card (giant time + station + switch, no always-visible
|
||||||
|
action row) MUST preserve edit and delete capability: tapping the card
|
||||||
|
MUST open the editor for that alarm; swiping it MUST trigger delete
|
||||||
|
(with confirmation). No capability present in the current always-visible
|
||||||
|
button row may be lost.
|
||||||
|
|
||||||
|
#### Scenario: Tap opens the editor
|
||||||
|
|
||||||
|
- GIVEN an alarm card is visible on the Alarmas root
|
||||||
|
- WHEN the user taps it
|
||||||
|
- THEN the editor sheet opens pre-filled with that alarm's data
|
||||||
|
|
||||||
|
#### Scenario: Swipe deletes with confirmation
|
||||||
|
|
||||||
|
- GIVEN an alarm card is visible
|
||||||
|
- WHEN the user swipes it and confirms
|
||||||
|
- THEN that alarm is deleted and its card is removed from the list
|
||||||
|
|
||||||
|
### Requirement: Hero Banner Inline Skip
|
||||||
|
|
||||||
|
The Alarmas root's hero banner MUST include an inline "Saltar" action that
|
||||||
|
skips the next occurrence of the featured (soonest-firing) alarm, in
|
||||||
|
addition to any per-card skip action.
|
||||||
|
|
||||||
|
#### Scenario: Tapping "Saltar" on the hero banner skips the featured alarm
|
||||||
|
|
||||||
|
- GIVEN the hero banner shows the soonest-firing alarm
|
||||||
|
- WHEN the user taps its inline "Saltar" action
|
||||||
|
- THEN that alarm's next occurrence is marked skipped, consistent with the existing skip-next behavior
|
||||||
|
|
||||||
|
### Requirement: Alarm Editor Preserves Date, Fallback Station, and Sound Fields
|
||||||
|
|
||||||
|
The rewritten editor (giant inline HH:MM display, always-visible weekday
|
||||||
|
circles) MUST NOT drop the existing one-time date field, the
|
||||||
|
fallback-station picker, or the sound dropdown. These MUST remain
|
||||||
|
reachable, e.g. under a collapsed "Advanced" section.
|
||||||
|
|
||||||
|
#### Scenario: One-time date alarm is still creatable
|
||||||
|
|
||||||
|
- GIVEN the user is creating a new alarm
|
||||||
|
- WHEN they choose a specific one-time date instead of a weekly recurrence
|
||||||
|
- THEN the date field is reachable and the saved alarm reflects that specific date, not a weekday pattern
|
||||||
|
|
||||||
|
#### Scenario: Fallback station and sound remain settable
|
||||||
|
|
||||||
|
- GIVEN the editor's Advanced section
|
||||||
|
- WHEN the user sets a fallback station and a sound choice
|
||||||
|
- THEN both persist and are reflected the next time the alarm is edited
|
||||||
|
|
||||||
|
### Requirement: Ringing Screen Shows a Static Status Label (No Live Countdown)
|
||||||
|
|
||||||
|
The ringing screen's "Subiendo volumen" status MUST render as a static
|
||||||
|
label with no live seconds counter. Adding a live counter is out of scope:
|
||||||
|
it would require native-to-Flutter progress plumbing that is deliberately
|
||||||
|
absent from the architecture.
|
||||||
|
|
||||||
|
#### Scenario: Status label has no numeric countdown
|
||||||
|
|
||||||
|
- GIVEN an alarm is ringing and fading in
|
||||||
|
- WHEN the status label renders
|
||||||
|
- THEN it shows "Subiendo volumen" (or equivalent localized text) with no accompanying seconds value that changes over time
|
||||||
|
|
||||||
|
### Requirement: Dismiss Guard and Force-Stop Banner Survive the Restyle (First-Class Regression Guard)
|
||||||
|
|
||||||
|
The ringing screen's single-exit dismiss guard and force-stop retry banner
|
||||||
|
MUST be preserved unmodified by the visual restyle.
|
||||||
|
|
||||||
|
#### Scenario: Dismiss guard test passes unmodified
|
||||||
|
|
||||||
|
- GIVEN `test/pantallas/pantalla_alarma_sonando_dismiss_guard_test.dart`
|
||||||
|
- WHEN the ringing screen restyle lands
|
||||||
|
- THEN that test file passes without being modified
|
||||||
|
|
||||||
|
#### Scenario: Force-stop retry banner still appears on failure
|
||||||
|
|
||||||
|
- GIVEN a stop action fails to actually silence the ring
|
||||||
|
- WHEN the failure is detected
|
||||||
|
- THEN the force-stop retry banner is shown, in whatever position the restyled layout places it
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Onboarding Welcome Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
A first-run welcome surface in the new visual language, explicitly
|
||||||
|
monetization-free. Traceable to WU17.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Full-Screen Welcome Route
|
||||||
|
|
||||||
|
The welcome screen MUST render as a full-screen route (not a modal
|
||||||
|
dialog), containing: logo mark, headline, body copy, exactly 3 feature
|
||||||
|
bullets, and exactly one primary call-to-action ("Empezar a escuchar").
|
||||||
|
|
||||||
|
#### Scenario: All elements render, one CTA
|
||||||
|
|
||||||
|
- GIVEN the app launches for the first time
|
||||||
|
- WHEN the welcome screen renders
|
||||||
|
- THEN a logo, headline, body copy, and 3 feature bullets are visible
|
||||||
|
- AND exactly 1 primary CTA is present
|
||||||
|
|
||||||
|
### Requirement: No Monetization Content (First-Class, Binding)
|
||||||
|
|
||||||
|
The welcome screen MUST NOT render a PRO pill, any trial-duration text
|
||||||
|
(e.g. a day count), any price, or a secondary "free version" link. This
|
||||||
|
follows the binding no-monetization decision for this change.
|
||||||
|
|
||||||
|
#### Scenario: Rendered content contains no monetization strings
|
||||||
|
|
||||||
|
- GIVEN the welcome screen is rendered
|
||||||
|
- WHEN its widget tree's text content is scanned
|
||||||
|
- THEN it contains none of: the substring "PRO", a currency amount, or a day-count trial phrase
|
||||||
|
- AND no secondary "free version" link is present
|
||||||
|
|
||||||
|
### Requirement: CTA Dismisses to Escuchar
|
||||||
|
|
||||||
|
Tapping the primary CTA MUST dismiss the welcome screen and land on the
|
||||||
|
Escuchar tab (index 0), with the welcome route removed from the
|
||||||
|
navigation stack.
|
||||||
|
|
||||||
|
#### Scenario: CTA navigates to Escuchar
|
||||||
|
|
||||||
|
- GIVEN the welcome screen is showing
|
||||||
|
- WHEN the user taps "Empezar a escuchar"
|
||||||
|
- THEN the app shows the Escuchar tab
|
||||||
|
- AND the welcome route is no longer on the back stack
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Recordings Library Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
A browsable recordings list with storage usage and per-recording
|
||||||
|
rename/share/delete, constrained to what `servicio_grabacion_radio.dart`
|
||||||
|
already exposes. Traceable to WU15.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Storage Usage Display
|
||||||
|
|
||||||
|
The screen MUST show used vs. total storage as a progress bar with a
|
||||||
|
caption (used amount, total amount, retention/path info).
|
||||||
|
|
||||||
|
#### Scenario: Storage usage renders proportionally
|
||||||
|
|
||||||
|
- GIVEN recordings occupy 84 MB of a 200 MB budget
|
||||||
|
- WHEN the screen renders
|
||||||
|
- THEN the progress bar fill reflects 84/200 and the caption states both values
|
||||||
|
|
||||||
|
### Requirement: Browsable Recordings List
|
||||||
|
|
||||||
|
The screen MUST list individual recordings sourced from the recordings
|
||||||
|
folder, each row showing name, date, duration, and size, with inline
|
||||||
|
playback.
|
||||||
|
|
||||||
|
#### Scenario: Recordings on disk render as rows
|
||||||
|
|
||||||
|
- GIVEN 3 recording files exist in the recordings folder
|
||||||
|
- WHEN the screen loads
|
||||||
|
- THEN exactly 3 rows render, each with correct name/date/duration/size
|
||||||
|
|
||||||
|
#### Scenario: No recordings (edge case)
|
||||||
|
|
||||||
|
- GIVEN the recordings folder is empty
|
||||||
|
- WHEN the screen loads
|
||||||
|
- THEN an empty state renders, not an error
|
||||||
|
|
||||||
|
#### Scenario: Row playback starts and stops
|
||||||
|
|
||||||
|
- GIVEN a recording row is visible
|
||||||
|
- WHEN the user taps its play control
|
||||||
|
- THEN playback of that recording starts
|
||||||
|
- AND tapping again stops it
|
||||||
|
|
||||||
|
### Requirement: Per-Recording Menu Constrained to Rename/Share/Delete
|
||||||
|
|
||||||
|
The "⋮" menu on each recording row MUST expose exactly three actions —
|
||||||
|
Rename, Share, Delete — constrained to capabilities already exposed by
|
||||||
|
`servicio_grabacion_radio.dart`. It MUST NOT expose an action that service
|
||||||
|
does not support.
|
||||||
|
|
||||||
|
#### Scenario: Menu shows exactly 3 actions
|
||||||
|
|
||||||
|
- GIVEN any recording row
|
||||||
|
- WHEN its "⋮" menu opens
|
||||||
|
- THEN exactly Rename, Share, and Delete are shown, no other action
|
||||||
|
|
||||||
|
#### Scenario: Delete removes the file and its row
|
||||||
|
|
||||||
|
- GIVEN a recording row's menu is open
|
||||||
|
- WHEN the user selects Delete and confirms
|
||||||
|
- THEN the underlying file is removed and the row disappears from the list
|
||||||
|
|
||||||
|
#### Scenario: Rename updates the displayed name
|
||||||
|
|
||||||
|
- GIVEN a recording row's menu is open
|
||||||
|
- WHEN the user selects Rename and submits a new, non-empty name
|
||||||
|
- THEN the row displays the new name and it persists across a reload
|
||||||
|
|
||||||
|
#### Scenario: Share invokes the platform share sheet
|
||||||
|
|
||||||
|
- GIVEN a recording row's menu is open
|
||||||
|
- WHEN the user selects Share
|
||||||
|
- THEN the platform share sheet opens with that recording's file
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# Station Discovery & Browse Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Discovery content as the Buscar tab's landing state, active-filter pills,
|
||||||
|
results counter, clear-all-filters, the Países browser over the verified
|
||||||
|
`/json/countries` contract, and a client-side search-sort control. Traceable
|
||||||
|
to WU5 (relocation), WU6 (Buscar), WU7 (Países + `_get` transport
|
||||||
|
extraction).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Buscar Landing State Shows Discovery Content
|
||||||
|
|
||||||
|
Buscar MUST render the existing discovery content (near-you, genres,
|
||||||
|
trending, countries entry point) as its landing state whenever no search
|
||||||
|
query is entered. Entering a query MUST replace that landing state with
|
||||||
|
search results.
|
||||||
|
|
||||||
|
#### Scenario: Empty query shows discovery content
|
||||||
|
|
||||||
|
- GIVEN the user opens the Buscar tab with no prior query
|
||||||
|
- WHEN the screen renders
|
||||||
|
- THEN near-you, genre, trending, and a Países entry point are visible
|
||||||
|
|
||||||
|
#### Scenario: Entering a query replaces the landing state
|
||||||
|
|
||||||
|
- GIVEN the Buscar tab is showing discovery content
|
||||||
|
- WHEN the user types a non-empty query
|
||||||
|
- THEN discovery content is replaced by the search-results view
|
||||||
|
|
||||||
|
### Requirement: Active-Filter Pills and Results Counter
|
||||||
|
|
||||||
|
The search-results view MUST show each active filter (country, language,
|
||||||
|
minimum quality) as a removable pill, and MUST show a results counter
|
||||||
|
("N RESULTADOS").
|
||||||
|
|
||||||
|
#### Scenario: Applying a filter shows a removable pill
|
||||||
|
|
||||||
|
- GIVEN the user has an active search
|
||||||
|
- WHEN they apply a country filter
|
||||||
|
- THEN a pill labeled with that country and a close (x) affordance appears
|
||||||
|
- AND the results counter reflects the filtered count
|
||||||
|
|
||||||
|
#### Scenario: Removing a pill re-runs the search without that filter
|
||||||
|
|
||||||
|
- GIVEN a country filter pill is active
|
||||||
|
- WHEN the user taps its close affordance
|
||||||
|
- THEN the filter is cleared and results/count update accordingly
|
||||||
|
|
||||||
|
### Requirement: One-Tap Clear-All-Filters on Empty Results
|
||||||
|
|
||||||
|
When 1 or more filters are active and the resulting search has zero results,
|
||||||
|
the system MUST offer a single action that clears every active filter at
|
||||||
|
once ("Quitar los N filtros").
|
||||||
|
|
||||||
|
#### Scenario: Two active filters, zero results
|
||||||
|
|
||||||
|
- GIVEN country and quality filters are both active and yield 0 results
|
||||||
|
- WHEN the empty state renders
|
||||||
|
- THEN a "Quitar los 2 filtros" action is shown
|
||||||
|
- AND tapping it clears both filters in one action, not one at a time
|
||||||
|
|
||||||
|
### Requirement: Países Browser Over the Verified Countries Contract
|
||||||
|
|
||||||
|
The Países screen (new second-level screen) MUST fetch from
|
||||||
|
**`/json/countries`** (optional substring filter `/json/countries/{filter}`)
|
||||||
|
and MUST NOT use `/json/countrycodes`, which is deprecated. It MUST parse
|
||||||
|
the `stationcount` field as a **string** via `int.tryParse`, never as a
|
||||||
|
direct `int` cast. Its request MUST NOT include `lastcheckok`, since that
|
||||||
|
parameter is meaningful only for station-listing calls.
|
||||||
|
Source: Engram `reference/radio-browser-countries-endpoint` (id 2500),
|
||||||
|
verified against <https://api.radio-browser.info/>.
|
||||||
|
|
||||||
|
#### Scenario: Countries list renders with counts
|
||||||
|
|
||||||
|
- GIVEN `/json/countries` returns a list of country objects
|
||||||
|
- WHEN the Países screen loads
|
||||||
|
- THEN "Tus idiomas" and the full alphabetical list render with each country's parsed station count
|
||||||
|
|
||||||
|
#### Scenario: `stationcount` arrives as a JSON string (edge case, critical)
|
||||||
|
|
||||||
|
- GIVEN a country entry has `"stationcount": "482"` (a JSON string, not a number)
|
||||||
|
- WHEN the screen parses and renders that entry
|
||||||
|
- THEN it displays `482` without throwing a type-cast error
|
||||||
|
|
||||||
|
#### Scenario: Countries request omits the station-only filter
|
||||||
|
|
||||||
|
- GIVEN the Países screen issues its `/json/countries` request
|
||||||
|
- WHEN the outgoing request is inspected
|
||||||
|
- THEN it does NOT include `lastcheckok`
|
||||||
|
|
||||||
|
### Requirement: Client-Side Search Sort Only
|
||||||
|
|
||||||
|
Buscar MUST expose an "Ordenar" control backed exclusively by client-side
|
||||||
|
ordering (`OrdenEmisoras`). The system MUST NOT adopt the Radio Browser
|
||||||
|
server-side `order`/`reverse` parameters for this control, because
|
||||||
|
server-side ordering sorts the full result set before `limit` is applied
|
||||||
|
while the app's client-side sort only reorders the page already fetched —
|
||||||
|
adopting it would silently change which stations users see, not just their
|
||||||
|
order. Every sort option rendered in the UI MUST map to a real
|
||||||
|
`OrdenEmisoras` case; the system MUST NOT render an option that does not
|
||||||
|
actually sort.
|
||||||
|
Source: Engram `reference/radio-browser-sort-order` (id 2505), verified
|
||||||
|
against <https://api.radio-browser.info/>.
|
||||||
|
|
||||||
|
#### Scenario: Selecting a sort option reorders the current page only
|
||||||
|
|
||||||
|
- GIVEN a set of search results is already fetched
|
||||||
|
- WHEN the user selects "Ordenar: calidad"
|
||||||
|
- THEN the currently-displayed page is reordered via `OrdenEmisoras.calidad`
|
||||||
|
- AND no new network request with an `order` parameter is issued
|
||||||
|
|
||||||
|
#### Scenario: Every rendered sort option is backed by a real case (regression guard)
|
||||||
|
|
||||||
|
- GIVEN the "Ordenar" control's rendered option list
|
||||||
|
- WHEN each option is checked against `OrdenEmisoras`
|
||||||
|
- THEN every option maps to an existing enum case with test coverage; no decorative option that does not sort is rendered
|
||||||
|
|
||||||
|
### Requirement: Existing Station Calls Unchanged by Transport Extraction
|
||||||
|
|
||||||
|
Extracting `_get`'s transport loop (server discovery, host rotation,
|
||||||
|
bounded retries, User-Agent, timeout, status check, `json.decode`) into a
|
||||||
|
shared helper for the Países call MUST NOT change the observable behavior
|
||||||
|
of the 8 existing `ServicioRadio` station methods: `obtenerPopulares`,
|
||||||
|
`obtenerTendencias`, `buscarPorNombre`, `buscarPorPais`, `buscarPorIdioma`,
|
||||||
|
`buscarPorTag`, `buscar`, `registrarClick`.
|
||||||
|
|
||||||
|
#### Scenario: Characterization tests pass unchanged before and after extraction
|
||||||
|
|
||||||
|
- GIVEN characterization tests exist for all 8 station methods
|
||||||
|
- WHEN the transport loop is extracted from `_get`
|
||||||
|
- THEN all 8 tests pass identically before and after the extraction
|
||||||
|
|
||||||
|
#### Scenario: Station calls still send `lastcheckok`
|
||||||
|
|
||||||
|
- GIVEN any of the 8 station methods issues a request
|
||||||
|
- WHEN the outgoing request is inspected
|
||||||
|
- THEN it still includes `lastcheckok=1`, unchanged from before the extraction
|
||||||
Reference in New Issue
Block a user