# Tasks: EQ Device Auto-Switch UX ## Review Workload Forecast | Field | Value | |-------|-------| | Estimated changed lines | 380–520 | | 400-line budget risk | High | | Chained PRs recommended | Yes | | Suggested split | PR 1 (service + state) → PR 2 (UI + l10n) → PR 3 (widget tests) | | Delivery strategy | ask-on-risk | | Chain strategy | pending | Decision needed before apply: Yes Chained PRs recommended: Yes Chain strategy: pending 400-line budget risk: High ### Suggested Work Units | Unit | Goal | Likely PR | Notes | |------|------|-----------|-------| | 1 | Bug fix + rename persistence layer (service + state) | PR 1 | base = main; ~150 lines; no UI dependency | | 2 | Settings UI rewrite + edit modal + l10n | PR 2 | base = PR 1; ~220 lines; depends on Unit 1 APIs | | 3 | Widget tests for modal + integration tests | PR 3 | base = PR 2; ~120 lines; closes test coverage gap | --- ## Phase 1: Foundation — Service Layer (PR 1 slice) > Strict TDD: RED first, then GREEN, then REFACTOR for each task group. - [ ] 1.1 **RED** — Write failing unit tests in `test/servicios/servicio_ecualizador_test.dart` for `cargarNombresDispositivos()` returning empty map when SP key absent - [ ] 1.2 **RED** — Add failing test: `guardarNombresDispositivos({id: name})` writes JSON to `eq_nombres_dispositivos_v1` SP key - [ ] 1.3 **RED** — Add failing test: round-trip save → load returns same map - [ ] 1.4 **GREEN** — Add `_keyNombresDispositivos = 'eq_nombres_dispositivos_v1'` to `lib/servicios/servicio_ecualizador.dart` - [ ] 1.5 **GREEN** — Implement `cargarNombresDispositivos()` and `guardarNombresDispositivos()` in `ServicioEcualizador` (reuse existing `_leerMapa`/`_guardarMapa` JSON pattern adapted for `Map`) - [ ] 1.6 **GREEN** — Add `nombresDispositivos` field to `ConfiguracionEcualizador` data class; include in `cargar()` and wire into `ServicioEcualizador.cargar()` call - [ ] 1.7 **REFACTOR** — Ensure no duplicate SP read calls; confirm `cargar()` returns `nombresDispositivos` in a single pass; update any `copyWith` if present --- ## Phase 2: Core — State Layer Bug Fix + Rename API (PR 1 slice) - [ ] 2.1 **RED** — Write failing unit tests in `test/estado/estado_ecualizador_test.dart`: after `cargarPersistido()` with `eqMultiDeviceEnabled=true` and fake device service returning `'bt_a2dp:AA:BB'`, assert `dispositivoActualId == 'bt_a2dp:AA:BB'` (spec scenario: correct preset at startup) - [ ] 2.2 **RED** — Add failing test: first-seen device is bootstrapped with copy of resolved preset after `cargarPersistido()` (spec scenario: first-seen device bootstrapped at startup) - [ ] 2.3 **RED** — Add failing test: `obtenerDispositivoActual()` throws → `dispositivoActualId` falls back to `'builtin_speaker'` (spec scenario: graceful failure) - [ ] 2.4 **RED** — Add failing test: `eqMultiDeviceEnabled=false` → `obtenerDispositivoActual()` never called (spec scenario: toggle off) - [ ] 2.5 **GREEN** — In `lib/estado/estado_ecualizador.dart`, inside `cargarPersistido()`, after `_configurarSuscripcionDispositivo()`, add `try/catch` `await obtenerDispositivoActual()` call that seeds `_dispositivoActualId` then calls `_onDispositivoCambiado()` to trigger bootstrap + preset resolution - [ ] 2.6 **RED** — Add failing tests for rename API: `renombrarDispositivo(id, nombre)` writes to `_nombresDispositivos`, calls `notifyListeners()`; empty string is no-op (spec scenarios: rename persists, empty name not persisted) - [ ] 2.7 **RED** — Add failing test: `obtenerNombreDispositivo(id)` returns stored name or empty string - [ ] 2.8 **RED** — Add failing tests for `nombreVisible(deviceId, platformName)` fallback chain: custom > platform > id (all three spec scenarios) - [ ] 2.9 **GREEN** — Add `_nombresDispositivos` map field; implement `renombrarDispositivo()`, `obtenerNombreDispositivo()`, `nombreVisible()`, and `get nombresDispositivos` getter in `lib/estado/estado_ecualizador.dart`; wire to service persist on rename - [ ] 2.10 **REFACTOR** — Extract fallback chain to a single helper, ensure `_nombresDispositivos` is loaded from `ConfiguracionEcualizador.nombresDispositivos` in `cargarPersistido()` --- ## Phase 3: Integration — Settings UI Rewrite (PR 2 slice) - [ ] 3.1 **RED** — Write failing widget test in `test/pantallas/pantalla_ajustes_test.dart`: connection dot is present on active device row, absent on inactive rows (spec scenario: active device shows green indicator) - [ ] 3.2 **RED** — Add failing widget test: tapping a device row opens a bottom sheet with a `TextField` pre-filled with resolved display name and an `EcualizadorWidget` (spec scenario: modal opens on device row tap) - [ ] 3.3 **RED** — Add failing widget test: `EcualizadorWidget` in modal is reachable by scrolling on small viewport (spec scenario: modal is scrollable) - [ ] 3.4 **GREEN** — Rewrite `_SeccionEcualizadorAvanzado` device rows in `lib/pantallas/pantalla_ajustes.dart`: add green dot `Icon` gated on `entry.key == eq.dispositivoActualId`; display name via `eq.nombreVisible(id, device.nombre)`; add `onTap` handler - [ ] 3.5 **GREEN** — Implement `_DialogoEdicionDispositivo` `StatefulWidget` in `lib/pantallas/pantalla_ajustes.dart`: `showModalBottomSheet` with `showDragHandle: true`, `isScrollControlled: true`, `TextField` pre-filled with display name, embedded `EcualizadorWidget`, confirm/dismiss logic calling `eq.renombrarDispositivo(id, name)` - [ ] 3.6 **RED** — Add failing widget test: renaming in modal and confirming calls `renombrarDispositivo`; device row reflects new name after close (spec scenario: rename confirmed in modal persists) - [ ] 3.7 **RED** — Add failing widget test: dismissing modal without confirming leaves name unchanged (spec scenario: modal dismiss without save) - [ ] 3.8 **GREEN** — Wire dismiss-without-save path (no `renombrarDispositivo` call on drag-dismiss) - [ ] 3.9 **REFACTOR** — Extract modal into its own file if `pantalla_ajustes.dart` exceeds 400 lines; ensure fallback chain is called from `EstadoEcualizador`, not inlined in UI --- ## Phase 4: Localisation (PR 2 slice) - [ ] 4.1 Add ~5 new ARB keys to `lib/l10n/app_en.arb`: `eqDeviceEditTitle`, `eqDeviceNameLabel`, `eqDeviceNameHint`, `eqDeviceNameConfirm`, `eqDeviceConnected` - [ ] 4.2 Translate all 5 keys in the 12 remaining locale files (`app_es.arb`, `app_pt.arb`, `app_fr.arb`, `app_de.arb`, `app_it.arb`, `app_ja.arb`, `app_ko.arb`, `app_zh.arb`, `app_ru.arb`, `app_ar.arb`, `app_hi.arb`, `app_tr.arb`) - [ ] 4.3 Run `flutter gen-l10n` and verify no ARB parse errors; confirm generated `.g.dart` includes all new keys - [ ] 4.4 Replace any hardcoded strings in `_SeccionEcualizadorAvanzado` and `_DialogoEdicionDispositivo` with l10n references --- ## Phase 5: Cleanup + Verification (PR 3 slice) - [ ] 5.1 Run full test suite; confirm all RED tasks from Phases 1–3 are GREEN - [ ] 5.2 Verify spec scenario coverage: cross-check each scenario in `openspec/changes/eq-device-autoswitch-ux/spec.md` against a corresponding test (add any gap tests) - [ ] 5.3 Run `flutter analyze`; resolve any new lint warnings introduced by this change - [ ] 5.4 Remove any `TODO`/`FIXME` comments introduced during GREEN phase - [ ] 5.5 Confirm `nombresDispositivos` omission from export schema v4 is documented in design open questions (no code change needed — already deferred per proposal)