Persist the exploration, proposal, spec, design, tasks, and verify/archive reports produced during the multi-device EQ, alarm-countdown, and notification-visual-polish SDD cycles.
130 lines
7.9 KiB
Markdown
130 lines
7.9 KiB
Markdown
# Design: EQ Device Auto-Switch UX
|
|
|
|
## Technical Approach
|
|
|
|
Fix the startup device-seeding bug by adding a single `obtenerDispositivoActual()` call inside `cargarPersistido()` after the subscription is wired. Add a `nombresDispositivos` persistence layer (JSON map in SharedPreferences) exposed through `EstadoEcualizador`. Rewrite `_SeccionEcualizadorAvanzado` device rows with connection dot, display name fallback chain, and tap-to-edit bottom sheet containing rename TextField + embedded `EcualizadorWidget`.
|
|
|
|
## Architecture Decisions
|
|
|
|
### Decision: Bug fix location
|
|
|
|
| Option | Tradeoff | Decision |
|
|
|--------|----------|----------|
|
|
| A. Call `obtenerDispositivoActual()` in `cargarPersistido()` after `_configurarSuscripcionDispositivo()` | Minimal change, Dart-only, no platform touch | **Chosen** |
|
|
| B. Self-initializing stream (emit current device on `onListen` from Kotlin) | Single code path for all events | Rejected: requires Kotlin change, out of scope |
|
|
| C. Persist last-known device ID to SP | Survives process kill | Rejected: stale-state risk, higher complexity |
|
|
|
|
**Rationale**: The `obtenerDispositivoActual()` method channel already exists and is tested on the platform side. One `await` call seeds `_dispositivoActualId`, triggers first-seen bootstrap, and re-resolves the preset. Zero platform code changes.
|
|
|
|
### Decision: Device names persistence location
|
|
|
|
| Option | Tradeoff | Decision |
|
|
|--------|----------|----------|
|
|
| A. New SP key `eq_nombres_dispositivos_v1` in `ServicioEcualizador` as `Map<String, String>` JSON | Reuses existing `_leerMapa`/`_guardarMapa` pattern (adapted for String values), co-located with other EQ SP keys | **Chosen** |
|
|
| B. Field on `DispositivoAudio` model | Requires model change + breaks value-equality contract (id-only) | Rejected |
|
|
| C. Separate `ServicioNombresDispositivo` | Over-engineered for a single map | Rejected |
|
|
|
|
**Rationale**: `ServicioEcualizador` already manages 6 SP keys with the same read/write JSON pattern. Adding a 7th key with a simpler `Map<String, String>` type keeps all EQ persistence in one service. The `DispositivoAudio` model stays pure (id-based equality, no persistence coupling).
|
|
|
|
### Decision: Modal architecture
|
|
|
|
| Option | Tradeoff | Decision |
|
|
|--------|----------|----------|
|
|
| A. `showModalBottomSheet` with rename + embedded `EcualizadorWidget` | Established pattern (used 4x in settings), `isScrollControlled: true` handles tall content | **Chosen** |
|
|
| B. Inline expanded accordion per device | Always visible, but very tall list, complex expand/collapse state | Rejected |
|
|
| C. Full-screen dialog / page route | Overkill for rename + 5-band slider | Rejected |
|
|
|
|
**Rationale**: The app already uses `showModalBottomSheet` with `showDragHandle: true` and `isScrollControlled: true` in `_editarGrupo`, `_editarTamanoMaximo`, `_FormularioDuracionTimer`, and `_mostrarFormularioAnadir`. Same pattern, consistent UX.
|
|
|
|
### Decision: Display name resolution
|
|
|
|
| Option | Tradeoff | Decision |
|
|
|--------|----------|----------|
|
|
| A. Fallback chain: `nombresDispositivos[id]` > `DispositivoAudio.nombre` > `id` | Clean, handles all null/empty cases, no model changes | **Chosen** |
|
|
| B. Merge custom name into a new model field | Couples persistence to model | Rejected |
|
|
|
|
**Rationale**: The display name is a pure UI concern. `DispositivoAudio.nombre` comes from platform (`productName`) and can be null/empty on some devices. The fallback chain is computed at render time in the settings widget, keeping the model and state layers clean.
|
|
|
|
### Decision: Connection status derivation
|
|
|
|
**Choice**: Compare `entry.key == eq.dispositivoActualId` per row to show a green dot. No new API needed -- `EstadoEcualizador.dispositivoActualId` getter already exists.
|
|
|
|
**Rejected**: Tracking a `Set<String> connectedDevices` from the stream -- unnecessary since only one audio output is active at a time on Android.
|
|
|
|
## Data Flow
|
|
|
|
```
|
|
App startup
|
|
|
|
|
v
|
|
cargarPersistido()
|
|
|-- servicio.cargar() --> loads config + nombresDispositivos
|
|
|-- _configurarSuscripcionDispositivo() --> subscribes to EventChannel
|
|
|-- obtenerDispositivoActual() --> seeds _dispositivoActualId [NEW]
|
|
|-- _onDispositivoCambiado() --> first-seen bootstrap + re-resolve
|
|
|-- audio.aplicarPreset() --> correct preset applied
|
|
v
|
|
Settings UI (tap device row)
|
|
|
|
|
v
|
|
showModalBottomSheet
|
|
|-- TextField (custom name) --> renombrarDispositivo(id, name)
|
|
| |-> servicio.guardarNombresDispositivos(map)
|
|
| |-> notifyListeners()
|
|
|
|
|
|-- EcualizadorWidget(preset, onCambio)
|
|
|-> eq.cambiarPresetDispositivo(id, preset) [NEW convenience method]
|
|
```
|
|
|
|
## File Changes
|
|
|
|
| File | Action | Description |
|
|
|------|--------|-------------|
|
|
| `lib/estado/estado_ecualizador.dart` | Modify | Add `obtenerDispositivoActual()` call in `cargarPersistido()` after subscription setup. Add `_nombresDispositivos` map, `renombrarDispositivo()`, `obtenerNombreDispositivo()`, `nombreVisible()` fallback helper. |
|
|
| `lib/servicios/servicio_ecualizador.dart` | Modify | Add `_keyNombresDispositivos = 'eq_nombres_dispositivos_v1'`. Add `cargarNombresDispositivos()`, `guardarNombresDispositivos()` methods. Load names inside `cargar()` and include in `ConfiguracionEcualizador`. |
|
|
| `lib/servicios/servicio_ecualizador.dart` (ConfiguracionEcualizador) | Modify | Add `nombresDispositivos` field (`Map<String, String>`). |
|
|
| `lib/pantallas/pantalla_ajustes.dart` | Modify | Rewrite `_SeccionEcualizadorAvanzado` device rows: add connection dot (green `CircleAvatar` 8px), display name via fallback chain, `onTap` → `_mostrarEdicionDispositivo()`. Add new `_DialogoEdicionDispositivo` StatefulWidget (bottom sheet body: TextField for name + `EcualizadorWidget`). |
|
|
| `lib/l10n/app_en.arb` | Modify | Add ~5 new keys: `advancedEqDeviceConnected`, `advancedEqDeviceRenameLabel`, `advancedEqDeviceEditTitle`, `advancedEqDeviceNameHint`, `advancedEqDeviceRenamed`. |
|
|
| `lib/l10n/app_*.arb` (12 files) | Modify | Translate the new keys to all supported locales. |
|
|
| `test/` | Create | Tests for: startup device seeding, rename persistence round-trip, display name fallback chain, connection indicator logic. |
|
|
|
|
## Interfaces / Contracts
|
|
|
|
```dart
|
|
// ConfiguracionEcualizador -- add field
|
|
class ConfiguracionEcualizador {
|
|
// ... existing fields ...
|
|
final Map<String, String> nombresDispositivos; // deviceId -> custom name
|
|
}
|
|
|
|
// EstadoEcualizador -- new public API
|
|
Map<String, String> get nombresDispositivos;
|
|
Future<void> renombrarDispositivo(String deviceId, String nombre);
|
|
String obtenerNombreDispositivo(String deviceId); // returns custom name or ''
|
|
String nombreVisible(String deviceId, String platformName); // fallback chain
|
|
```
|
|
|
|
```dart
|
|
// ServicioEcualizador -- new methods
|
|
Future<Map<String, String>> cargarNombresDispositivos();
|
|
Future<void> guardarNombresDispositivos(Map<String, String> nombres);
|
|
```
|
|
|
|
## Testing Strategy
|
|
|
|
| Layer | What to Test | Approach |
|
|
|-------|-------------|----------|
|
|
| Unit | `cargarPersistido()` seeds `_dispositivoActualId` when multi-device ON | Fake `ServicioDispositivoAudio` returns known device; assert `dispositivoActualId != null` after load |
|
|
| Unit | `renombrarDispositivo` persists and notifies | Call rename, verify SP key written and listeners notified |
|
|
| Unit | `nombreVisible` fallback chain: custom > platform > id | Three cases: all present, no custom, no platform |
|
|
| Unit | Connection indicator: `entry.key == dispositivoActualId` | Verify boolean derivation in widget test or pure logic |
|
|
| Widget | Bottom sheet renders rename + EQ | `pumpWidget` with `_DialogoEdicionDispositivo`, verify TextField and `EcualizadorWidget` present |
|
|
|
|
## Migration / Rollout
|
|
|
|
No migration required. The new SP key `eq_nombres_dispositivos_v1` is created on first rename. If the code is reverted, the key is simply ignored -- no data loss or corruption. The startup fix is additive (one extra method call).
|
|
|
|
## Open Questions
|
|
|
|
- None blocking. The `nombresDispositivos` omission from export schema v4 is acknowledged and deferred per proposal.
|