Files
pluriwave/openspec/changes/eq-device-autoswitch-ux/design.md
T
FreeTLab bccc5c48b8
Build & Deploy PluriWave / Análisis de código (push) Successful in 38s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m30s
docs(openspec): add SDD artifact trail for recent alarm and EQ changes
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.
2026-07-04 12:42:11 +02:00

7.9 KiB

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

// 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
// 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.