diff --git a/openspec/changes/rediseno-funcional/specs/alarm-vacation-ranges/spec.md b/openspec/changes/rediseno-funcional/specs/alarm-vacation-ranges/spec.md new file mode 100644 index 0000000..f8c0f14 --- /dev/null +++ b/openspec/changes/rediseno-funcional/specs/alarm-vacation-ranges/spec.md @@ -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 diff --git a/openspec/changes/rediseno-funcional/specs/app-navigation-shell/spec.md b/openspec/changes/rediseno-funcional/specs/app-navigation-shell/spec.md new file mode 100644 index 0000000..a9ba4b7 --- /dev/null +++ b/openspec/changes/rediseno-funcional/specs/app-navigation-shell/spec.md @@ -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 diff --git a/openspec/changes/rediseno-funcional/specs/eq-custom-presets/spec.md b/openspec/changes/rediseno-funcional/specs/eq-custom-presets/spec.md new file mode 100644 index 0000000..886977a --- /dev/null +++ b/openspec/changes/rediseno-funcional/specs/eq-custom-presets/spec.md @@ -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 diff --git a/openspec/changes/rediseno-funcional/specs/favorites-organization/spec.md b/openspec/changes/rediseno-funcional/specs/favorites-organization/spec.md new file mode 100644 index 0000000..6adc44b --- /dev/null +++ b/openspec/changes/rediseno-funcional/specs/favorites-organization/spec.md @@ -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 diff --git a/openspec/changes/rediseno-funcional/specs/multi-device-eq/spec.md b/openspec/changes/rediseno-funcional/specs/multi-device-eq/spec.md new file mode 100644 index 0000000..fa8c6dd --- /dev/null +++ b/openspec/changes/rediseno-funcional/specs/multi-device-eq/spec.md @@ -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 diff --git a/openspec/changes/rediseno-funcional/specs/native-alarms/spec.md b/openspec/changes/rediseno-funcional/specs/native-alarms/spec.md new file mode 100644 index 0000000..80fd3a5 --- /dev/null +++ b/openspec/changes/rediseno-funcional/specs/native-alarms/spec.md @@ -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 diff --git a/openspec/changes/rediseno-funcional/specs/onboarding-welcome/spec.md b/openspec/changes/rediseno-funcional/specs/onboarding-welcome/spec.md new file mode 100644 index 0000000..e396c39 --- /dev/null +++ b/openspec/changes/rediseno-funcional/specs/onboarding-welcome/spec.md @@ -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 diff --git a/openspec/changes/rediseno-funcional/specs/recordings-library/spec.md b/openspec/changes/rediseno-funcional/specs/recordings-library/spec.md new file mode 100644 index 0000000..091a423 --- /dev/null +++ b/openspec/changes/rediseno-funcional/specs/recordings-library/spec.md @@ -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 diff --git a/openspec/changes/rediseno-funcional/specs/station-discovery-browse/spec.md b/openspec/changes/rediseno-funcional/specs/station-discovery-browse/spec.md new file mode 100644 index 0000000..f2beab9 --- /dev/null +++ b/openspec/changes/rediseno-funcional/specs/station-discovery-browse/spec.md @@ -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 . + +#### 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 . + +#### 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