feat(eq): restyle equalizer screen and add custom presets
Restyle the Ecualizador settings screen to the new visual language while keeping the equalizer at 5 bands (spike-resolved, Engram id 2498 - band count is device-reported via just_audio's AndroidEqualizer, not app-chosen; the approved mockup's 7 sliders would silently no-op on typical hardware). - Restyle EcualizadorWidget in place: strip its internal title + preset chip row (the pushed screen's header now carries the title), add a habilitado parameter that greys/disables every slider when EQ is off. Widen PresetsEcualizadorWidget additively (personalizados param) so custom presets can join the chip row without a second implementation. - Add servicio_presets_personalizados.dart (new file, own SharedPreferences key eq_custom_presets_v1) for custom EQ preset persistence - kept out of servicio_ecualizador.dart, which has an empty-git-diff success criterion for this change. preset_ecualizador.dart is unchanged: a custom preset is just a PresetEcualizador with a user-supplied name. - Extend EstadoEcualizador with presetsPersonalizados, guardarPresetPersonalizado (validates non-empty name), eliminarPresetPersonalizado. The load is a new explicit cargarPresetsPersonalizados(), deliberately NOT folded into cargarPersistido(): that method is exercised ~30 times by estado_ecualizador_test.dart (protected, must stay unmodified) via Fakes only, with no SharedPreferences awareness in that file. - Build out the Ecualizador screen body: base-vs-per-station explainer banner, a "Salida activa" row surfaced on the main screen (previously Advanced-only), an "Emisoras con ajuste propio" drill-down sourced from the existing presetsPorEmisora map, and a "Guardar como preset" action. New coverage lives in new files rather than touching the three protected EQ test files: ecualizador_widget_test.dart (component-level, did not exist before this commit), servicio_presets_personalizados_test.dart, and estado_ecualizador_presets_personalizados_test.dart. servicio_ecualizador.dart, servicio_audio.dart and the three protected EQ test files keep an empty git diff. Full suite: 713/713 green (2 skipped, unchanged), up from 682. size:exception - realized 1,954 changed lines (25 files, plus this docs update) against the 400-550 forecast: lib/ + ARB alone is ~650 lines, near the top of the forecast band by itself since this WU also had to build out a screen body WU3a only stubbed; the rest is 4 test files (675 lines) and 11 new ARB keys regenerating 13 lib/l10n/gen files (~546 lines) - the same pattern every prior work unit in this branch has hit. Not splittable: WU14 reuses this unit's editor component by exact runtime type and cannot begin until this lands as a whole.
This commit is contained in:
@@ -7,6 +7,7 @@ import '../modelos/preset_ecualizador.dart';
|
|||||||
import '../servicios/servicio_audio.dart';
|
import '../servicios/servicio_audio.dart';
|
||||||
import '../servicios/servicio_dispositivo_audio.dart';
|
import '../servicios/servicio_dispositivo_audio.dart';
|
||||||
import '../servicios/servicio_ecualizador.dart';
|
import '../servicios/servicio_ecualizador.dart';
|
||||||
|
import '../servicios/servicio_presets_personalizados.dart';
|
||||||
|
|
||||||
/// Equalizer state extracted from `EstadoRadio` (S4-R1).
|
/// Equalizer state extracted from `EstadoRadio` (S4-R1).
|
||||||
///
|
///
|
||||||
@@ -30,8 +31,11 @@ class EstadoEcualizador extends ChangeNotifier {
|
|||||||
required this.audio,
|
required this.audio,
|
||||||
ServicioEcualizador? servicio,
|
ServicioEcualizador? servicio,
|
||||||
ServicioDispositivoAudio? dispositivoAudio,
|
ServicioDispositivoAudio? dispositivoAudio,
|
||||||
|
ServicioPresetsPersonalizados? presetsPersonalizadosService,
|
||||||
String? Function()? emisoraActualUuid,
|
String? Function()? emisoraActualUuid,
|
||||||
}) : servicio = servicio ?? ServicioEcualizador(),
|
}) : servicio = servicio ?? ServicioEcualizador(),
|
||||||
|
_presetsPersonalizadosService =
|
||||||
|
presetsPersonalizadosService ?? ServicioPresetsPersonalizados(),
|
||||||
_dispositivoAudio = dispositivoAudio,
|
_dispositivoAudio = dispositivoAudio,
|
||||||
_emisoraActualUuid = emisoraActualUuid ?? (() => null);
|
_emisoraActualUuid = emisoraActualUuid ?? (() => null);
|
||||||
|
|
||||||
@@ -39,6 +43,12 @@ class EstadoEcualizador extends ChangeNotifier {
|
|||||||
final ServicioEcualizador servicio;
|
final ServicioEcualizador servicio;
|
||||||
final ServicioDispositivoAudio? _dispositivoAudio;
|
final ServicioDispositivoAudio? _dispositivoAudio;
|
||||||
|
|
||||||
|
/// Persistence for user-named custom presets (design ADR-5 hazard box).
|
||||||
|
/// Deliberately a SEPARATE service/key from [servicio] — see
|
||||||
|
/// [cargarPresetsPersonalizados] for why its load is not folded into
|
||||||
|
/// [cargarPersistido].
|
||||||
|
final ServicioPresetsPersonalizados _presetsPersonalizadosService;
|
||||||
|
|
||||||
/// Callback into the owner (EstadoRadio) for the currently playing station;
|
/// Callback into the owner (EstadoRadio) for the currently playing station;
|
||||||
/// keeps this notifier free of any station-list coupling.
|
/// keeps this notifier free of any station-list coupling.
|
||||||
final String? Function() _emisoraActualUuid;
|
final String? Function() _emisoraActualUuid;
|
||||||
@@ -61,6 +71,11 @@ class EstadoEcualizador extends ChangeNotifier {
|
|||||||
/// every session without needing a SharedPreferences key or migration.
|
/// every session without needing a SharedPreferences key or migration.
|
||||||
final Map<String, String> _nombresPlataforma = {};
|
final Map<String, String> _nombresPlataforma = {};
|
||||||
|
|
||||||
|
/// User-named custom presets (WU13). Loaded explicitly via
|
||||||
|
/// [cargarPresetsPersonalizados], not as part of [cargarPersistido] —
|
||||||
|
/// see that method's doc for why.
|
||||||
|
List<PresetEcualizador> _presetsPersonalizados = [];
|
||||||
|
|
||||||
PresetEcualizador _presetPrincipal = PresetEcualizador.flat;
|
PresetEcualizador _presetPrincipal = PresetEcualizador.flat;
|
||||||
PresetEcualizador _presetActual = PresetEcualizador.flat;
|
PresetEcualizador _presetActual = PresetEcualizador.flat;
|
||||||
bool _activo = true;
|
bool _activo = true;
|
||||||
@@ -88,6 +103,9 @@ class EstadoEcualizador extends ChangeNotifier {
|
|||||||
Map<String, PresetEcualizador> get presetsMatriz =>
|
Map<String, PresetEcualizador> get presetsMatriz =>
|
||||||
Map.unmodifiable(_presetsMatriz);
|
Map.unmodifiable(_presetsMatriz);
|
||||||
|
|
||||||
|
List<PresetEcualizador> get presetsPersonalizados =>
|
||||||
|
List.unmodifiable(_presetsPersonalizados);
|
||||||
|
|
||||||
bool get emisoraActualTienePresetPropio {
|
bool get emisoraActualTienePresetPropio {
|
||||||
final uuid = _emisoraActualUuid();
|
final uuid = _emisoraActualUuid();
|
||||||
if (uuid == null) return false;
|
if (uuid == null) return false;
|
||||||
@@ -424,6 +442,52 @@ class EstadoEcualizador extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Loads the persisted custom-preset list (WU13, `eq-custom-presets`
|
||||||
|
/// spec — the Settings EQ screen's preset chip row).
|
||||||
|
///
|
||||||
|
/// Deliberately NOT part of [cargarPersistido]: that method is exercised
|
||||||
|
/// roughly 30 times by `estado_ecualizador_test.dart` — one of this
|
||||||
|
/// change's protected EQ test files, required to pass **unmodified** —
|
||||||
|
/// via Fakes for [servicio]/[_dispositivoAudio] only, with no
|
||||||
|
/// SharedPreferences awareness anywhere in that file. Folding a third,
|
||||||
|
/// always-real-by-default collaborator into [cargarPersistido] would
|
||||||
|
/// introduce a real SharedPreferences call into every one of those
|
||||||
|
/// cases. Called explicitly by the Settings EQ screen instead, the same
|
||||||
|
/// way `refrescarDispositivoActual` is already called from screen
|
||||||
|
/// `initState`, not from [cargarPersistido].
|
||||||
|
Future<void> cargarPresetsPersonalizados() async {
|
||||||
|
_presetsPersonalizados = await _presetsPersonalizadosService.listar();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves the CURRENT effective preset's bands (`presetActual`) as a new
|
||||||
|
/// named custom preset (spec "Custom Preset Save").
|
||||||
|
///
|
||||||
|
/// Returns `false` — and persists nothing — when [nombre] is empty or
|
||||||
|
/// whitespace-only (spec "Custom Preset Naming Validates Non-Empty
|
||||||
|
/// Input"), the same non-crashing validate-before-persist shape
|
||||||
|
/// [renombrarDispositivo] already uses elsewhere in this class.
|
||||||
|
Future<bool> guardarPresetPersonalizado(String nombre) async {
|
||||||
|
final nombreValido = nombre.trim();
|
||||||
|
if (nombreValido.isEmpty) return false;
|
||||||
|
|
||||||
|
final preset = PresetEcualizador(
|
||||||
|
nombre: nombreValido,
|
||||||
|
bandas: List<double>.from(_presetActual.bandas),
|
||||||
|
);
|
||||||
|
await _presetsPersonalizadosService.guardar(preset);
|
||||||
|
_presetsPersonalizados = await _presetsPersonalizadosService.listar();
|
||||||
|
notifyListeners();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the custom preset named [nombre], if present.
|
||||||
|
Future<void> eliminarPresetPersonalizado(String nombre) async {
|
||||||
|
await _presetsPersonalizadosService.eliminar(nombre);
|
||||||
|
_presetsPersonalizados = await _presetsPersonalizadosService.listar();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
/// Persists a custom display name for [deviceId].
|
/// Persists a custom display name for [deviceId].
|
||||||
///
|
///
|
||||||
/// Empty names are silently ignored so the existing name is preserved.
|
/// Empty names are silently ignored so the existing name is preserved.
|
||||||
|
|||||||
@@ -95,6 +95,17 @@
|
|||||||
"equalizerPerStationTitle": "Use custom EQ for this favorite",
|
"equalizerPerStationTitle": "Use custom EQ for this favorite",
|
||||||
"equalizerPerStationActive": "Active for {stationName}",
|
"equalizerPerStationActive": "Active for {stationName}",
|
||||||
"equalizerPerStationMain": "Using main EQ for {stationName}",
|
"equalizerPerStationMain": "Using main EQ for {stationName}",
|
||||||
|
"equalizerBaseExplainer": "This is the base equalizer: it applies to every station without its own setting. A station's own EQ is set from its own playback screen and overrides this one.",
|
||||||
|
"equalizerActiveOutputLabel": "Active output",
|
||||||
|
"equalizerActiveOutputDefault": "This device's speaker",
|
||||||
|
"equalizerStationsWithOwnEqTitle": "Stations with their own EQ",
|
||||||
|
"equalizerStationsWithOwnEqSubtitle": "Ignore this base equalizer",
|
||||||
|
"equalizerStationsWithOwnEqEmpty": "No stations have their own EQ yet.",
|
||||||
|
"equalizerSaveAsPresetAction": "Save as preset",
|
||||||
|
"equalizerSavePresetDialogTitle": "Save as preset",
|
||||||
|
"equalizerSavePresetNameLabel": "Preset name",
|
||||||
|
"equalizerSavePresetEmptyNameError": "Enter a name for the preset.",
|
||||||
|
"equalizerSavePresetConfirm": "Save",
|
||||||
"preferredStationTitle": "Preferred station",
|
"preferredStationTitle": "Preferred station",
|
||||||
"preferredStationDescription": "Preselected for new alarms and available for quick playback.",
|
"preferredStationDescription": "Preselected for new alarms and available for quick playback.",
|
||||||
"preferredStationNoStationsTitle": "No stations available yet",
|
"preferredStationNoStationsTitle": "No stations available yet",
|
||||||
|
|||||||
@@ -95,6 +95,17 @@
|
|||||||
"equalizerPerStationTitle": "Usar EQ propio para esta favorita",
|
"equalizerPerStationTitle": "Usar EQ propio para esta favorita",
|
||||||
"equalizerPerStationActive": "Activo para {stationName}",
|
"equalizerPerStationActive": "Activo para {stationName}",
|
||||||
"equalizerPerStationMain": "Usando EQ principal para {stationName}",
|
"equalizerPerStationMain": "Usando EQ principal para {stationName}",
|
||||||
|
"equalizerBaseExplainer": "Este es el ecualizador base: se aplica a todas las emisoras que no tengan un ajuste propio. El de una emisora concreta se define desde su propia reproducción y manda sobre este.",
|
||||||
|
"equalizerActiveOutputLabel": "Salida activa",
|
||||||
|
"equalizerActiveOutputDefault": "El altavoz de este dispositivo",
|
||||||
|
"equalizerStationsWithOwnEqTitle": "Emisoras con ajuste propio",
|
||||||
|
"equalizerStationsWithOwnEqSubtitle": "Ignoran este ecualizador base",
|
||||||
|
"equalizerStationsWithOwnEqEmpty": "Ninguna emisora tiene ajuste propio todavía.",
|
||||||
|
"equalizerSaveAsPresetAction": "Guardar como preset",
|
||||||
|
"equalizerSavePresetDialogTitle": "Guardar como preset",
|
||||||
|
"equalizerSavePresetNameLabel": "Nombre del preset",
|
||||||
|
"equalizerSavePresetEmptyNameError": "Ingresá un nombre para el preset.",
|
||||||
|
"equalizerSavePresetConfirm": "Guardar",
|
||||||
"preferredStationTitle": "Emisora preferida",
|
"preferredStationTitle": "Emisora preferida",
|
||||||
"preferredStationDescription": "Se preselecciona al crear alarmas y puede iniciarse como reproducción rápida.",
|
"preferredStationDescription": "Se preselecciona al crear alarmas y puede iniciarse como reproducción rápida.",
|
||||||
"preferredStationNoStationsTitle": "Todavía no hay emisoras disponibles",
|
"preferredStationNoStationsTitle": "Todavía no hay emisoras disponibles",
|
||||||
|
|||||||
@@ -496,6 +496,72 @@ abstract class AppLocalizations {
|
|||||||
/// **'Usando EQ principal para {stationName}'**
|
/// **'Usando EQ principal para {stationName}'**
|
||||||
String equalizerPerStationMain(Object stationName);
|
String equalizerPerStationMain(Object stationName);
|
||||||
|
|
||||||
|
/// No description provided for @equalizerBaseExplainer.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Este es el ecualizador base: se aplica a todas las emisoras que no tengan un ajuste propio. El de una emisora concreta se define desde su propia reproducción y manda sobre este.'**
|
||||||
|
String get equalizerBaseExplainer;
|
||||||
|
|
||||||
|
/// No description provided for @equalizerActiveOutputLabel.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Salida activa'**
|
||||||
|
String get equalizerActiveOutputLabel;
|
||||||
|
|
||||||
|
/// No description provided for @equalizerActiveOutputDefault.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'El altavoz de este dispositivo'**
|
||||||
|
String get equalizerActiveOutputDefault;
|
||||||
|
|
||||||
|
/// No description provided for @equalizerStationsWithOwnEqTitle.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Emisoras con ajuste propio'**
|
||||||
|
String get equalizerStationsWithOwnEqTitle;
|
||||||
|
|
||||||
|
/// No description provided for @equalizerStationsWithOwnEqSubtitle.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Ignoran este ecualizador base'**
|
||||||
|
String get equalizerStationsWithOwnEqSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @equalizerStationsWithOwnEqEmpty.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Ninguna emisora tiene ajuste propio todavía.'**
|
||||||
|
String get equalizerStationsWithOwnEqEmpty;
|
||||||
|
|
||||||
|
/// No description provided for @equalizerSaveAsPresetAction.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Guardar como preset'**
|
||||||
|
String get equalizerSaveAsPresetAction;
|
||||||
|
|
||||||
|
/// No description provided for @equalizerSavePresetDialogTitle.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Guardar como preset'**
|
||||||
|
String get equalizerSavePresetDialogTitle;
|
||||||
|
|
||||||
|
/// No description provided for @equalizerSavePresetNameLabel.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Nombre del preset'**
|
||||||
|
String get equalizerSavePresetNameLabel;
|
||||||
|
|
||||||
|
/// No description provided for @equalizerSavePresetEmptyNameError.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Ingresá un nombre para el preset.'**
|
||||||
|
String get equalizerSavePresetEmptyNameError;
|
||||||
|
|
||||||
|
/// No description provided for @equalizerSavePresetConfirm.
|
||||||
|
///
|
||||||
|
/// In es, this message translates to:
|
||||||
|
/// **'Guardar'**
|
||||||
|
String get equalizerSavePresetConfirm;
|
||||||
|
|
||||||
/// No description provided for @preferredStationTitle.
|
/// No description provided for @preferredStationTitle.
|
||||||
///
|
///
|
||||||
/// In es, this message translates to:
|
/// In es, this message translates to:
|
||||||
|
|||||||
@@ -223,6 +223,43 @@ class AppLocalizationsAr extends AppLocalizations {
|
|||||||
return 'استخدام المعادل الرئيسي لـ $stationName';
|
return 'استخدام المعادل الرئيسي لـ $stationName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerBaseExplainer =>
|
||||||
|
'Este es el ecualizador base: se aplica a todas las emisoras que no tengan un ajuste propio. El de una emisora concreta se define desde su propia reproducción y manda sobre este.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputLabel => 'Salida activa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputDefault => 'El altavoz de este dispositivo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqTitle => 'Emisoras con ajuste propio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqSubtitle =>
|
||||||
|
'Ignoran este ecualizador base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqEmpty =>
|
||||||
|
'Ninguna emisora tiene ajuste propio todavía.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSaveAsPresetAction => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetDialogTitle => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetNameLabel => 'Nombre del preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetEmptyNameError =>
|
||||||
|
'Ingresá un nombre para el preset.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetConfirm => 'Guardar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get preferredStationTitle => 'المحطة المفضلة';
|
String get preferredStationTitle => 'المحطة المفضلة';
|
||||||
|
|
||||||
|
|||||||
@@ -226,6 +226,43 @@ class AppLocalizationsBn extends AppLocalizations {
|
|||||||
return '$stationName-এর জন্য মূল ইকুয়ালাইজার ব্যবহার করা হচ্ছে';
|
return '$stationName-এর জন্য মূল ইকুয়ালাইজার ব্যবহার করা হচ্ছে';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerBaseExplainer =>
|
||||||
|
'Este es el ecualizador base: se aplica a todas las emisoras que no tengan un ajuste propio. El de una emisora concreta se define desde su propia reproducción y manda sobre este.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputLabel => 'Salida activa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputDefault => 'El altavoz de este dispositivo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqTitle => 'Emisoras con ajuste propio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqSubtitle =>
|
||||||
|
'Ignoran este ecualizador base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqEmpty =>
|
||||||
|
'Ninguna emisora tiene ajuste propio todavía.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSaveAsPresetAction => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetDialogTitle => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetNameLabel => 'Nombre del preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetEmptyNameError =>
|
||||||
|
'Ingresá un nombre para el preset.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetConfirm => 'Guardar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get preferredStationTitle => 'পছন্দের স্টেশন';
|
String get preferredStationTitle => 'পছন্দের স্টেশন';
|
||||||
|
|
||||||
|
|||||||
@@ -226,6 +226,43 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
return 'Haupt-EQ für $stationName wird verwendet';
|
return 'Haupt-EQ für $stationName wird verwendet';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerBaseExplainer =>
|
||||||
|
'Este es el ecualizador base: se aplica a todas las emisoras que no tengan un ajuste propio. El de una emisora concreta se define desde su propia reproducción y manda sobre este.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputLabel => 'Salida activa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputDefault => 'El altavoz de este dispositivo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqTitle => 'Emisoras con ajuste propio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqSubtitle =>
|
||||||
|
'Ignoran este ecualizador base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqEmpty =>
|
||||||
|
'Ninguna emisora tiene ajuste propio todavía.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSaveAsPresetAction => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetDialogTitle => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetNameLabel => 'Nombre del preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetEmptyNameError =>
|
||||||
|
'Ingresá un nombre para el preset.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetConfirm => 'Guardar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get preferredStationTitle => 'Bevorzugter Sender';
|
String get preferredStationTitle => 'Bevorzugter Sender';
|
||||||
|
|
||||||
|
|||||||
@@ -224,6 +224,42 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
return 'Using main EQ for $stationName';
|
return 'Using main EQ for $stationName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerBaseExplainer =>
|
||||||
|
'This is the base equalizer: it applies to every station without its own setting. A station\'s own EQ is set from its own playback screen and overrides this one.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputLabel => 'Active output';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputDefault => 'This device\'s speaker';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqTitle => 'Stations with their own EQ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqSubtitle => 'Ignore this base equalizer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqEmpty =>
|
||||||
|
'No stations have their own EQ yet.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSaveAsPresetAction => 'Save as preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetDialogTitle => 'Save as preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetNameLabel => 'Preset name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetEmptyNameError =>
|
||||||
|
'Enter a name for the preset.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetConfirm => 'Save';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get preferredStationTitle => 'Preferred station';
|
String get preferredStationTitle => 'Preferred station';
|
||||||
|
|
||||||
|
|||||||
@@ -225,6 +225,43 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
return 'Usando EQ principal para $stationName';
|
return 'Usando EQ principal para $stationName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerBaseExplainer =>
|
||||||
|
'Este es el ecualizador base: se aplica a todas las emisoras que no tengan un ajuste propio. El de una emisora concreta se define desde su propia reproducción y manda sobre este.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputLabel => 'Salida activa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputDefault => 'El altavoz de este dispositivo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqTitle => 'Emisoras con ajuste propio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqSubtitle =>
|
||||||
|
'Ignoran este ecualizador base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqEmpty =>
|
||||||
|
'Ninguna emisora tiene ajuste propio todavía.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSaveAsPresetAction => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetDialogTitle => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetNameLabel => 'Nombre del preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetEmptyNameError =>
|
||||||
|
'Ingresá un nombre para el preset.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetConfirm => 'Guardar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get preferredStationTitle => 'Emisora preferida';
|
String get preferredStationTitle => 'Emisora preferida';
|
||||||
|
|
||||||
|
|||||||
@@ -227,6 +227,43 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
return 'EQ principal utilisé pour $stationName';
|
return 'EQ principal utilisé pour $stationName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerBaseExplainer =>
|
||||||
|
'Este es el ecualizador base: se aplica a todas las emisoras que no tengan un ajuste propio. El de una emisora concreta se define desde su propia reproducción y manda sobre este.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputLabel => 'Salida activa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputDefault => 'El altavoz de este dispositivo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqTitle => 'Emisoras con ajuste propio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqSubtitle =>
|
||||||
|
'Ignoran este ecualizador base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqEmpty =>
|
||||||
|
'Ninguna emisora tiene ajuste propio todavía.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSaveAsPresetAction => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetDialogTitle => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetNameLabel => 'Nombre del preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetEmptyNameError =>
|
||||||
|
'Ingresá un nombre para el preset.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetConfirm => 'Guardar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get preferredStationTitle => 'Station préférée';
|
String get preferredStationTitle => 'Station préférée';
|
||||||
|
|
||||||
|
|||||||
@@ -225,6 +225,43 @@ class AppLocalizationsHi extends AppLocalizations {
|
|||||||
return '$stationName के लिए मुख्य EQ इस्तेमाल हो रहा है';
|
return '$stationName के लिए मुख्य EQ इस्तेमाल हो रहा है';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerBaseExplainer =>
|
||||||
|
'Este es el ecualizador base: se aplica a todas las emisoras que no tengan un ajuste propio. El de una emisora concreta se define desde su propia reproducción y manda sobre este.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputLabel => 'Salida activa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputDefault => 'El altavoz de este dispositivo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqTitle => 'Emisoras con ajuste propio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqSubtitle =>
|
||||||
|
'Ignoran este ecualizador base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqEmpty =>
|
||||||
|
'Ninguna emisora tiene ajuste propio todavía.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSaveAsPresetAction => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetDialogTitle => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetNameLabel => 'Nombre del preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetEmptyNameError =>
|
||||||
|
'Ingresá un nombre para el preset.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetConfirm => 'Guardar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get preferredStationTitle => 'पसंदीदा स्टेशन';
|
String get preferredStationTitle => 'पसंदीदा स्टेशन';
|
||||||
|
|
||||||
|
|||||||
@@ -225,6 +225,43 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
return 'Menggunakan EQ utama untuk $stationName';
|
return 'Menggunakan EQ utama untuk $stationName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerBaseExplainer =>
|
||||||
|
'Este es el ecualizador base: se aplica a todas las emisoras que no tengan un ajuste propio. El de una emisora concreta se define desde su propia reproducción y manda sobre este.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputLabel => 'Salida activa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputDefault => 'El altavoz de este dispositivo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqTitle => 'Emisoras con ajuste propio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqSubtitle =>
|
||||||
|
'Ignoran este ecualizador base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqEmpty =>
|
||||||
|
'Ninguna emisora tiene ajuste propio todavía.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSaveAsPresetAction => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetDialogTitle => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetNameLabel => 'Nombre del preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetEmptyNameError =>
|
||||||
|
'Ingresá un nombre para el preset.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetConfirm => 'Guardar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get preferredStationTitle => 'Stasiun pilihan';
|
String get preferredStationTitle => 'Stasiun pilihan';
|
||||||
|
|
||||||
|
|||||||
@@ -225,6 +225,43 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
return 'EQ principale in uso per $stationName';
|
return 'EQ principale in uso per $stationName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerBaseExplainer =>
|
||||||
|
'Este es el ecualizador base: se aplica a todas las emisoras que no tengan un ajuste propio. El de una emisora concreta se define desde su propia reproducción y manda sobre este.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputLabel => 'Salida activa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputDefault => 'El altavoz de este dispositivo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqTitle => 'Emisoras con ajuste propio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqSubtitle =>
|
||||||
|
'Ignoran este ecualizador base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqEmpty =>
|
||||||
|
'Ninguna emisora tiene ajuste propio todavía.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSaveAsPresetAction => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetDialogTitle => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetNameLabel => 'Nombre del preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetEmptyNameError =>
|
||||||
|
'Ingresá un nombre para el preset.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetConfirm => 'Guardar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get preferredStationTitle => 'Emittente preferita';
|
String get preferredStationTitle => 'Emittente preferita';
|
||||||
|
|
||||||
|
|||||||
@@ -217,6 +217,43 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
return '$stationName でメインEQを使用中';
|
return '$stationName でメインEQを使用中';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerBaseExplainer =>
|
||||||
|
'Este es el ecualizador base: se aplica a todas las emisoras que no tengan un ajuste propio. El de una emisora concreta se define desde su propia reproducción y manda sobre este.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputLabel => 'Salida activa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputDefault => 'El altavoz de este dispositivo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqTitle => 'Emisoras con ajuste propio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqSubtitle =>
|
||||||
|
'Ignoran este ecualizador base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqEmpty =>
|
||||||
|
'Ninguna emisora tiene ajuste propio todavía.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSaveAsPresetAction => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetDialogTitle => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetNameLabel => 'Nombre del preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetEmptyNameError =>
|
||||||
|
'Ingresá un nombre para el preset.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetConfirm => 'Guardar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get preferredStationTitle => '優先局';
|
String get preferredStationTitle => '優先局';
|
||||||
|
|
||||||
|
|||||||
@@ -224,6 +224,43 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
return 'Usando o EQ principal para $stationName';
|
return 'Usando o EQ principal para $stationName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerBaseExplainer =>
|
||||||
|
'Este es el ecualizador base: se aplica a todas las emisoras que no tengan un ajuste propio. El de una emisora concreta se define desde su propia reproducción y manda sobre este.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputLabel => 'Salida activa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputDefault => 'El altavoz de este dispositivo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqTitle => 'Emisoras con ajuste propio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqSubtitle =>
|
||||||
|
'Ignoran este ecualizador base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqEmpty =>
|
||||||
|
'Ninguna emisora tiene ajuste propio todavía.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSaveAsPresetAction => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetDialogTitle => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetNameLabel => 'Nombre del preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetEmptyNameError =>
|
||||||
|
'Ingresá un nombre para el preset.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetConfirm => 'Guardar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get preferredStationTitle => 'Estação preferida';
|
String get preferredStationTitle => 'Estação preferida';
|
||||||
|
|
||||||
|
|||||||
@@ -226,6 +226,43 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
return 'Используется основной EQ для $stationName';
|
return 'Используется основной EQ для $stationName';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerBaseExplainer =>
|
||||||
|
'Este es el ecualizador base: se aplica a todas las emisoras que no tengan un ajuste propio. El de una emisora concreta se define desde su propia reproducción y manda sobre este.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputLabel => 'Salida activa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputDefault => 'El altavoz de este dispositivo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqTitle => 'Emisoras con ajuste propio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqSubtitle =>
|
||||||
|
'Ignoran este ecualizador base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqEmpty =>
|
||||||
|
'Ninguna emisora tiene ajuste propio todavía.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSaveAsPresetAction => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetDialogTitle => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetNameLabel => 'Nombre del preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetEmptyNameError =>
|
||||||
|
'Ingresá un nombre para el preset.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetConfirm => 'Guardar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get preferredStationTitle => 'Предпочитаемая станция';
|
String get preferredStationTitle => 'Предпочитаемая станция';
|
||||||
|
|
||||||
|
|||||||
@@ -216,6 +216,43 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
return '正在为 $stationName 使用主均衡器';
|
return '正在为 $stationName 使用主均衡器';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerBaseExplainer =>
|
||||||
|
'Este es el ecualizador base: se aplica a todas las emisoras que no tengan un ajuste propio. El de una emisora concreta se define desde su propia reproducción y manda sobre este.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputLabel => 'Salida activa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerActiveOutputDefault => 'El altavoz de este dispositivo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqTitle => 'Emisoras con ajuste propio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqSubtitle =>
|
||||||
|
'Ignoran este ecualizador base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerStationsWithOwnEqEmpty =>
|
||||||
|
'Ninguna emisora tiene ajuste propio todavía.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSaveAsPresetAction => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetDialogTitle => 'Guardar como preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetNameLabel => 'Nombre del preset';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetEmptyNameError =>
|
||||||
|
'Ingresá un nombre para el preset.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get equalizerSavePresetConfirm => 'Guardar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get preferredStationTitle => '首选电台';
|
String get preferredStationTitle => '首选电台';
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../../estado/estado_ecualizador.dart';
|
import '../../estado/estado_ecualizador.dart';
|
||||||
import '../../estado/estado_radio.dart';
|
import '../../estado/estado_radio.dart';
|
||||||
import '../../l10n/gen/app_localizations.dart';
|
import '../../l10n/gen/app_localizations.dart';
|
||||||
|
import '../../tema/pluriwave_theme.dart';
|
||||||
import '../../widgets/ecualizador_widget.dart';
|
import '../../widgets/ecualizador_widget.dart';
|
||||||
import '../../widgets/pluri_glass_surface.dart';
|
import '../../widgets/pluri_glass_surface.dart';
|
||||||
import '../../widgets/pluri_layout.dart';
|
import '../../widgets/pluri_layout.dart';
|
||||||
@@ -15,8 +18,10 @@ import '../../widgets/pluri_push_scaffold.dart';
|
|||||||
/// [PluriPushScaffold] now carries the title and the very next row already
|
/// [PluriPushScaffold] now carries the title and the very next row already
|
||||||
/// shows the same active/disabled state.
|
/// shows the same active/disabled state.
|
||||||
///
|
///
|
||||||
/// This body is a placeholder pending WU13's restyle (ADR-3's own component
|
/// WU13 (design ADR-5) restyled [EcualizadorWidget] itself and added the
|
||||||
/// inventory: "body rewritten by WU13").
|
/// base-vs-per-station explainer, the "Salida activa" summary row, the
|
||||||
|
/// "Emisoras con ajuste propio" drill-down, and the "Guardar como preset"
|
||||||
|
/// custom-preset flow — see `_CuerpoEcualizador` below.
|
||||||
class PantallaAjustesEcualizador extends StatelessWidget {
|
class PantallaAjustesEcualizador extends StatelessWidget {
|
||||||
const PantallaAjustesEcualizador({super.key});
|
const PantallaAjustesEcualizador({super.key});
|
||||||
|
|
||||||
@@ -30,13 +35,32 @@ class PantallaAjustesEcualizador extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CuerpoEcualizador extends StatelessWidget {
|
class _CuerpoEcualizador extends StatefulWidget {
|
||||||
const _CuerpoEcualizador();
|
const _CuerpoEcualizador();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_CuerpoEcualizador> createState() => _CuerpoEcualizadorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CuerpoEcualizadorState extends State<_CuerpoEcualizador> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final eq = context.read<EstadoEcualizador>();
|
||||||
|
// Fire-and-forget, mirroring the established
|
||||||
|
// `pantalla_ajustes_salida_audio.dart` pattern: both calls are
|
||||||
|
// genuinely async (a SharedPreferences read / a native re-query), so
|
||||||
|
// their completion never lands inside THIS build — no
|
||||||
|
// "setState() during build" risk.
|
||||||
|
unawaited(eq.cargarPresetsPersonalizados());
|
||||||
|
unawaited(eq.refrescarDispositivoActual());
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// EQ state comes from EstadoEcualizador (S4-R1/S4-R5); EstadoRadio is
|
// EQ state comes from EstadoEcualizador (S4-R1/S4-R5); EstadoRadio is
|
||||||
// only consulted for the current station + favorite flag.
|
// only consulted for the current station + favorite flag and for
|
||||||
|
// resolving station names in the "ajuste propio" drill-down.
|
||||||
return Consumer2<EstadoRadio, EstadoEcualizador>(
|
return Consumer2<EstadoRadio, EstadoEcualizador>(
|
||||||
builder: (ctx, estado, eq, _) {
|
builder: (ctx, estado, eq, _) {
|
||||||
final disponible = eq.disponible;
|
final disponible = eq.disponible;
|
||||||
@@ -77,16 +101,34 @@ class _CuerpoEcualizador extends StatelessWidget {
|
|||||||
eq.cambiarModoEmisoraActual(usarPropio: usarPropio),
|
eq.cambiarModoEmisoraActual(usarPropio: usarPropio),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_BannerExplicacionBase(l10n: l10n),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
_FilaSalidaActiva(eq: eq, l10n: l10n),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
_FilaEmisorasConAjustePropio(eq: eq, estado: estado, l10n: l10n),
|
||||||
|
const SizedBox(height: 12),
|
||||||
PresetsEcualizadorWidget(
|
PresetsEcualizadorWidget(
|
||||||
presetActual: eq.presetActual,
|
presetActual: eq.presetActual,
|
||||||
|
personalizados: eq.presetsPersonalizados,
|
||||||
onSeleccionar: (p) => eq.cambiarPreset(p),
|
onSeleccionar: (p) => eq.cambiarPreset(p),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
EcualizadorWidget(
|
EcualizadorWidget(
|
||||||
preset: eq.presetActual,
|
preset: eq.presetActual,
|
||||||
|
habilitado: eq.activo,
|
||||||
onCambio: (p) => eq.cambiarPreset(p),
|
onCambio: (p) => eq.cambiarPreset(p),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: TextButton.icon(
|
||||||
|
key: const Key('eq-save-preset-action'),
|
||||||
|
onPressed: () => _abrirDialogoGuardarPreset(context, eq),
|
||||||
|
icon: const Icon(Icons.bookmark_add_outlined, size: 20),
|
||||||
|
label: Text(l10n.equalizerSaveAsPresetAction),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -94,3 +136,281 @@ class _CuerpoEcualizador extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _abrirDialogoGuardarPreset(
|
||||||
|
BuildContext context,
|
||||||
|
EstadoEcualizador eq,
|
||||||
|
) {
|
||||||
|
return showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => _DialogoGuardarPreset(eq: eq),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Base-vs-per-station explainer (spec `eq-custom-presets` "Base-vs-Per-
|
||||||
|
/// Station Explainer Preserved"): always visible, distinguishing the base
|
||||||
|
/// (global/device) EQ this screen edits from a station's own override.
|
||||||
|
class _BannerExplicacionBase extends StatelessWidget {
|
||||||
|
const _BannerExplicacionBase({required this.l10n});
|
||||||
|
|
||||||
|
final AppLocalizations l10n;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final tokens = context.pluriTokens;
|
||||||
|
return Container(
|
||||||
|
key: const Key('eq-base-explainer-banner'),
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: tokens.liveGreen.withValues(alpha: 0.09),
|
||||||
|
borderRadius: BorderRadius.circular(tokens.radiusSm),
|
||||||
|
border: Border.all(color: tokens.liveGreen.withValues(alpha: 0.26)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.info_outline_rounded, size: 20, color: tokens.liveGreen),
|
||||||
|
const SizedBox(width: 11),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
l10n.equalizerBaseExplainer,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// "Salida activa" row (spec `eq-custom-presets` "Active Output Surfaced on
|
||||||
|
/// the Main Screen"): surfaced here instead of only inside the Advanced
|
||||||
|
/// (multi-device) screen, and kept live via the SAME `notifyListeners()`
|
||||||
|
/// calls `_onDispositivoCambiado` already fires — no new plumbing needed
|
||||||
|
/// beyond reading `dispositivoActualId` here.
|
||||||
|
class _FilaSalidaActiva extends StatelessWidget {
|
||||||
|
const _FilaSalidaActiva({required this.eq, required this.l10n});
|
||||||
|
|
||||||
|
final EstadoEcualizador eq;
|
||||||
|
final AppLocalizations l10n;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final deviceId = eq.dispositivoActualId;
|
||||||
|
final nombre =
|
||||||
|
deviceId == null
|
||||||
|
? l10n.equalizerActiveOutputDefault
|
||||||
|
: eq.nombreVisible(deviceId, eq.nombrePlataforma(deviceId));
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
key: const Key('eq-active-output-row'),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.speaker_group_rounded, size: 20),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
l10n.equalizerActiveOutputLabel,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
nombre,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// "Emisoras con ajuste propio" drill-down row (spec `eq-custom-presets`
|
||||||
|
/// "Stations-With-Own-EQ Drill-Down"): sourced from the existing
|
||||||
|
/// `presetsPorEmisora` map, no new state.
|
||||||
|
class _FilaEmisorasConAjustePropio extends StatelessWidget {
|
||||||
|
const _FilaEmisorasConAjustePropio({
|
||||||
|
required this.eq,
|
||||||
|
required this.estado,
|
||||||
|
required this.l10n,
|
||||||
|
});
|
||||||
|
|
||||||
|
final EstadoEcualizador eq;
|
||||||
|
final EstadoRadio estado;
|
||||||
|
final AppLocalizations l10n;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final uuids = eq.presetsPorEmisora.keys.toList();
|
||||||
|
return Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: InkWell(
|
||||||
|
key: const Key('eq-stations-own-eq-row'),
|
||||||
|
borderRadius: BorderRadius.circular(context.pluriTokens.radiusSm),
|
||||||
|
onTap:
|
||||||
|
() => PluriPushScaffold.push(
|
||||||
|
context,
|
||||||
|
(_) => _PantallaEmisorasConAjustePropio(
|
||||||
|
uuids: uuids,
|
||||||
|
nombrePorUuid: (uuid) => _resolverNombreEmisora(estado, uuid),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.tune_rounded, size: 20),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.equalizerStationsWithOwnEqTitle,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
l10n.equalizerStationsWithOwnEqSubtitle,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
key: const Key('eq-stations-own-eq-count'),
|
||||||
|
'${uuids.length}',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
const Icon(Icons.chevron_right_rounded, size: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves a station uuid to its display name via [EstadoRadio.listaFavoritos]
|
||||||
|
/// (the only local, synchronous list of known stations) — falls back to the
|
||||||
|
/// raw uuid for a station that has its own EQ but is not (or no longer) a
|
||||||
|
/// favorite, e.g. one set from the player's per-station EQ sheet (WU14).
|
||||||
|
String _resolverNombreEmisora(EstadoRadio estado, String uuid) {
|
||||||
|
for (final emisora in estado.listaFavoritos) {
|
||||||
|
if (emisora.uuid == uuid) return emisora.nombre;
|
||||||
|
}
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Destination screen for the drill-down row above.
|
||||||
|
class _PantallaEmisorasConAjustePropio extends StatelessWidget {
|
||||||
|
const _PantallaEmisorasConAjustePropio({
|
||||||
|
required this.uuids,
|
||||||
|
required this.nombrePorUuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<String> uuids;
|
||||||
|
final String Function(String uuid) nombrePorUuid;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
return PluriPushScaffold(
|
||||||
|
title: l10n.equalizerStationsWithOwnEqTitle,
|
||||||
|
body:
|
||||||
|
uuids.isEmpty
|
||||||
|
? Center(child: Text(l10n.equalizerStationsWithOwnEqEmpty))
|
||||||
|
: ListView.separated(
|
||||||
|
padding: PluriLayout.pageContentPadding,
|
||||||
|
itemCount: uuids.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(height: 4),
|
||||||
|
itemBuilder: (ctx, i) {
|
||||||
|
final uuid = uuids[i];
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.radio_rounded, size: 20),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(child: Text(nombrePorUuid(uuid))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// "Guardar como preset" dialog (spec `eq-custom-presets` "Custom Preset
|
||||||
|
/// Save" / "Custom Preset Naming Validates Non-Empty Input").
|
||||||
|
class _DialogoGuardarPreset extends StatefulWidget {
|
||||||
|
const _DialogoGuardarPreset({required this.eq});
|
||||||
|
|
||||||
|
final EstadoEcualizador eq;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_DialogoGuardarPreset> createState() => _DialogoGuardarPresetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialogoGuardarPresetState extends State<_DialogoGuardarPreset> {
|
||||||
|
late final TextEditingController _nombreCtrl;
|
||||||
|
String? _error;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_nombreCtrl = TextEditingController();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nombreCtrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _confirmar() async {
|
||||||
|
final guardado = await widget.eq.guardarPresetPersonalizado(
|
||||||
|
_nombreCtrl.text,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (!guardado) {
|
||||||
|
setState(
|
||||||
|
() =>
|
||||||
|
_error =
|
||||||
|
AppLocalizations.of(context).equalizerSavePresetEmptyNameError,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(l10n.equalizerSavePresetDialogTitle),
|
||||||
|
content: TextField(
|
||||||
|
key: const Key('eq-save-preset-name-field'),
|
||||||
|
controller: _nombreCtrl,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: l10n.equalizerSavePresetNameLabel,
|
||||||
|
errorText: _error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
FilledButton(
|
||||||
|
key: const Key('eq-save-preset-confirm-button'),
|
||||||
|
onPressed: _confirmar,
|
||||||
|
child: Text(l10n.equalizerSavePresetConfirm),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../modelos/preset_ecualizador.dart';
|
||||||
|
import 'persistencia_tolerante.dart';
|
||||||
|
|
||||||
|
/// Persistence for USER-NAMED custom EQ presets (design ADR-5 hazard box,
|
||||||
|
/// `eq-custom-presets` spec — "Custom Preset Save").
|
||||||
|
///
|
||||||
|
/// Deliberately its OWN file and OWN SharedPreferences key
|
||||||
|
/// (`eq_custom_presets_v1`), never folded into [ServicioEcualizador]: that
|
||||||
|
/// file has an empty-`git diff` success criterion for this change, so
|
||||||
|
/// nothing custom-preset-related may be added there. A custom preset is
|
||||||
|
/// just a [PresetEcualizador] with a user-supplied `nombre` — the model
|
||||||
|
/// itself needs no change (`toJson`/`desdeJson` already exist).
|
||||||
|
class ServicioPresetsPersonalizados {
|
||||||
|
ServicioPresetsPersonalizados({SharedPreferences? prefs}) : _prefs = prefs;
|
||||||
|
|
||||||
|
static const _keyPresetsPersonalizados = 'eq_custom_presets_v1';
|
||||||
|
|
||||||
|
final SharedPreferences? _prefs;
|
||||||
|
|
||||||
|
Future<SharedPreferences> _resolverPrefs() async =>
|
||||||
|
_prefs ?? SharedPreferences.getInstance();
|
||||||
|
|
||||||
|
/// Returns every saved custom preset, in save order.
|
||||||
|
Future<List<PresetEcualizador>> listar() async {
|
||||||
|
final prefs = await _resolverPrefs();
|
||||||
|
return _leer(prefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves [preset] under its own name. A preset already saved under the
|
||||||
|
/// same name is replaced (last write wins) rather than duplicated.
|
||||||
|
Future<void> guardar(PresetEcualizador preset) async {
|
||||||
|
final prefs = await _resolverPrefs();
|
||||||
|
final actuales =
|
||||||
|
_leer(prefs)
|
||||||
|
..removeWhere((p) => p.nombre == preset.nombre)
|
||||||
|
..add(preset);
|
||||||
|
await _guardarTodos(prefs, actuales);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the custom preset named [nombre], if present. A safe no-op
|
||||||
|
/// when no preset with that name exists.
|
||||||
|
Future<void> eliminar(String nombre) async {
|
||||||
|
final prefs = await _resolverPrefs();
|
||||||
|
final actuales = _leer(prefs)..removeWhere((p) => p.nombre == nombre);
|
||||||
|
await _guardarTodos(prefs, actuales);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the persisted list with per-entry tolerance
|
||||||
|
/// (persistence-resilience D1/D6, same shared helper `ServicioEcualizador`
|
||||||
|
/// uses): an entry that fails to parse is skipped and logged, its
|
||||||
|
/// siblings survive. A top-level decode failure degrades to an empty
|
||||||
|
/// list (also logged) — custom presets are explicit-only user writes and
|
||||||
|
/// trivially re-creatable, so there is no flag/quarantine here, matching
|
||||||
|
/// `ServicioEcualizador`'s own documented D6 asymmetry vs. Alarms/Stations.
|
||||||
|
List<PresetEcualizador> _leer(SharedPreferences prefs) {
|
||||||
|
final raw = prefs.getString(_keyPresetsPersonalizados);
|
||||||
|
if (raw == null || raw.isEmpty) return [];
|
||||||
|
try {
|
||||||
|
final data = jsonDecode(raw) as List<dynamic>;
|
||||||
|
final resultado = parseListaTolerante<PresetEcualizador>(
|
||||||
|
data,
|
||||||
|
(entrada) => PresetEcualizador.desdeJson(entrada),
|
||||||
|
subsistema: 'presets_personalizados',
|
||||||
|
coleccion: _keyPresetsPersonalizados,
|
||||||
|
);
|
||||||
|
return resultado.validas;
|
||||||
|
} catch (e) {
|
||||||
|
registrarSaltoPersistencia(
|
||||||
|
subsistema: 'presets_personalizados',
|
||||||
|
detalle: _keyPresetsPersonalizados,
|
||||||
|
razon: e.toString(),
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _guardarTodos(
|
||||||
|
SharedPreferences prefs,
|
||||||
|
List<PresetEcualizador> presets,
|
||||||
|
) async {
|
||||||
|
final serializado = presets.map((p) => p.toJson()).toList();
|
||||||
|
await prefs.setString(_keyPresetsPersonalizados, jsonEncode(serializado));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,10 +9,16 @@ class EcualizadorWidget extends StatefulWidget {
|
|||||||
final PresetEcualizador preset;
|
final PresetEcualizador preset;
|
||||||
final void Function(PresetEcualizador) onCambio;
|
final void Function(PresetEcualizador) onCambio;
|
||||||
|
|
||||||
|
/// Design ADR-5: greys and disables every slider when the equalizer
|
||||||
|
/// itself is off, instead of leaving fully-interactive controls that
|
||||||
|
/// silently do nothing.
|
||||||
|
final bool habilitado;
|
||||||
|
|
||||||
const EcualizadorWidget({
|
const EcualizadorWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.preset,
|
required this.preset,
|
||||||
required this.onCambio,
|
required this.onCambio,
|
||||||
|
this.habilitado = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -50,91 +56,92 @@ class _EcualizadorWidgetState extends State<EcualizadorWidget> {
|
|||||||
final tokens = context.pluriTokens;
|
final tokens = context.pluriTokens;
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
|
|
||||||
|
// Design ADR-5: the title + preset Chip row that used to live here is
|
||||||
|
// gone — the pushed screen's own 56px header carries the title now, and
|
||||||
|
// the preset chip ROW (a different, still-reusable widget,
|
||||||
|
// [PresetsEcualizadorWidget] below) is rendered by the caller alongside
|
||||||
|
// this widget instead of being duplicated inside it.
|
||||||
return PluriGlassSurface(
|
return PluriGlassSurface(
|
||||||
borderRadius: BorderRadius.circular(tokens.radiusLg),
|
borderRadius: BorderRadius.circular(tokens.radiusLg),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
for (int i = 0; i < 5; i++)
|
||||||
children: [
|
Expanded(
|
||||||
Text(
|
child: AnimatedOpacity(
|
||||||
l10n.equalizerTitle,
|
duration: const Duration(milliseconds: 150),
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
opacity: widget.habilitado ? 1.0 : 0.4,
|
||||||
fontWeight: FontWeight.w700,
|
child: Card(
|
||||||
),
|
color: tokens.listSurface.withValues(alpha: 0.6),
|
||||||
),
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
const Spacer(),
|
shape: RoundedRectangleBorder(
|
||||||
Chip(
|
borderRadius: BorderRadius.circular(tokens.radiusSm),
|
||||||
label: Text(
|
),
|
||||||
_nombrePreset(l10n, widget.preset.nombre),
|
child: Padding(
|
||||||
style: theme.textTheme.labelMedium,
|
padding: const EdgeInsets.symmetric(
|
||||||
),
|
vertical: 10,
|
||||||
backgroundColor: theme.colorScheme.secondaryContainer
|
horizontal: 4,
|
||||||
.withValues(alpha: 0.75),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 14),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
for (int i = 0; i < 5; i++)
|
|
||||||
Expanded(
|
|
||||||
child: Card(
|
|
||||||
color: theme.colorScheme.surfaceContainerHighest.withValues(
|
|
||||||
alpha: 0.35,
|
|
||||||
),
|
),
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
child: Column(
|
||||||
shape: RoundedRectangleBorder(
|
children: [
|
||||||
borderRadius: BorderRadius.circular(14),
|
SizedBox(
|
||||||
),
|
height: 152,
|
||||||
child: Padding(
|
child: Semantics(
|
||||||
padding: const EdgeInsets.symmetric(
|
slider: true,
|
||||||
vertical: 8,
|
enabled: widget.habilitado,
|
||||||
horizontal: 4,
|
label: l10n.equalizerBandLabel(_etiquetas[i]),
|
||||||
),
|
value: l10n.equalizerBandValue(
|
||||||
child: Column(
|
_bandas[i].toStringAsFixed(1),
|
||||||
children: [
|
),
|
||||||
SizedBox(
|
child: RotatedBox(
|
||||||
height: 152,
|
quarterTurns: 3,
|
||||||
child: Semantics(
|
child: SliderTheme(
|
||||||
slider: true,
|
data: SliderTheme.of(context).copyWith(
|
||||||
label: l10n.equalizerBandLabel(_etiquetas[i]),
|
trackHeight: 5,
|
||||||
value: l10n.equalizerBandValue(
|
activeTrackColor: tokens.liveGreen,
|
||||||
_bandas[i].toStringAsFixed(1),
|
thumbColor: tokens.liveGreen,
|
||||||
),
|
inactiveTrackColor:
|
||||||
child: RotatedBox(
|
theme.colorScheme.surfaceContainerHighest,
|
||||||
quarterTurns: 3,
|
overlayColor: tokens.liveGreen.withValues(
|
||||||
|
alpha: 0.15,
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Slider(
|
child: Slider(
|
||||||
value: _bandas[i],
|
value: _bandas[i],
|
||||||
min: -12.0,
|
min: -12.0,
|
||||||
max: 12.0,
|
max: 12.0,
|
||||||
divisions: 24,
|
divisions: 24,
|
||||||
onChanged: (v) => _actualizarBanda(i, v),
|
onChanged:
|
||||||
|
widget.habilitado
|
||||||
|
? (v) => _actualizarBanda(i, v)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
),
|
||||||
'${_bandas[i].toStringAsFixed(1)}dB',
|
Text(
|
||||||
style: theme.textTheme.labelSmall,
|
'${_bandas[i].toStringAsFixed(1)}dB',
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
color: tokens.liveGreen,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
Text(
|
),
|
||||||
_etiquetas[i],
|
Text(
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
_etiquetas[i],
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
),
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
),
|
||||||
],
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -158,21 +165,31 @@ class PresetsEcualizadorWidget extends StatelessWidget {
|
|||||||
final PresetEcualizador presetActual;
|
final PresetEcualizador presetActual;
|
||||||
final void Function(PresetEcualizador) onSeleccionar;
|
final void Function(PresetEcualizador) onSeleccionar;
|
||||||
|
|
||||||
|
/// User-saved custom presets (WU13, `eq-custom-presets` spec), appended
|
||||||
|
/// after the 6 fixed ones. Additive-only parameter — defaults to empty so
|
||||||
|
/// this stays the same widget, not a new one (design ADR-5: "stays
|
||||||
|
/// as-is" means no restyle, not that it can never gain new data to show).
|
||||||
|
/// `_nombrePreset`'s default case already falls through to the raw name,
|
||||||
|
/// so a custom preset's chip label needs no special-casing here.
|
||||||
|
final List<PresetEcualizador> personalizados;
|
||||||
|
|
||||||
const PresetsEcualizadorWidget({
|
const PresetsEcualizadorWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.presetActual,
|
required this.presetActual,
|
||||||
required this.onSeleccionar,
|
required this.onSeleccionar,
|
||||||
|
this.personalizados = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final todos = [...PresetEcualizador.presets, ...personalizados];
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 6,
|
runSpacing: 6,
|
||||||
children:
|
children:
|
||||||
PresetEcualizador.presets.map((p) {
|
todos.map((p) {
|
||||||
final selected = p.nombre == presetActual.nombre;
|
final selected = p.nombre == presetActual.nombre;
|
||||||
return ChoiceChip(
|
return ChoiceChip(
|
||||||
label: Text(_nombrePreset(l10n, p.nombre)),
|
label: Text(_nombrePreset(l10n, p.nombre)),
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
| 9 | `feat(vacaciones): add vacation range manager screen` | 8 | 350-450 | Medium | Monitor |
|
| 9 | `feat(vacaciones): add vacation range manager screen` | 8 | 350-450 | Medium | Monitor |
|
||||||
| 10 | `feat(alarmas): rewrite alarm editor with inline time widget` | 8 | 500-650 | High | **Yes — indivisible new widget**‡ |
|
| 10 | `feat(alarmas): rewrite alarm editor with inline time widget` | 8 | 500-650 | High | **Yes — indivisible new widget**‡ |
|
||||||
| 11 | `feat(alarma-sonando): restyle ringing screen, drop live countdown label` | 1 | 200-300 | Low (safety-critical review attention: High) | No |
|
| 11 | `feat(alarma-sonando): restyle ringing screen, drop live countdown label` | 1 | 200-300 | Low (safety-critical review attention: High) | No |
|
||||||
| 13 | `feat(eq): restyle equalizer screen and add custom presets` | 3a | 400-550 | Medium-High | Monitor |
|
| 13 | `feat(eq): restyle equalizer screen and add custom presets` | 3a | ~~400-550~~ → **REALIZED: 1,871** (1,793+ / 78-, 25 files) | Medium-High | **Yes — retroactive, see WU13 section** |
|
||||||
| 14 | `feat(reproductor): restructure full player with tool-tray and EQ sheet` | 13 | 450-600 | Medium-High | Monitor |
|
| 14 | `feat(reproductor): restructure full player with tool-tray and EQ sheet` | 13 | 450-600 | Medium-High | Monitor |
|
||||||
| 15 | `feat(grabaciones): add recordings library screen` | 3b | ~~300-400~~ → **REALIZED: 1,767** (1,767+ / 0-, 22 files) | Medium | Monitor§ |
|
| 15 | `feat(grabaciones): add recordings library screen` | 3b | ~~300-400~~ → **REALIZED: 1,767** (1,767+ / 0-, 22 files) | Medium | Monitor§ |
|
||||||
| 15b | `fix(grabaciones): wire the recordings library into Settings navigation` | 15 | 60-100 | Low | No |
|
| 15b | `fix(grabaciones): wire the recordings library into Settings navigation` | 15 | 60-100 | Low | No |
|
||||||
@@ -735,32 +735,67 @@ Stations-With-Own-EQ Drill-Down, Base-vs-Per-Station Explainer Preserved, Active
|
|||||||
> **Hazard, restated: persistence for custom presets must NOT go in `servicio_ecualizador.dart`.** That file has an
|
> **Hazard, restated: persistence for custom presets must NOT go in `servicio_ecualizador.dart`.** That file has an
|
||||||
> empty-`git diff` success criterion.
|
> empty-`git diff` success criterion.
|
||||||
|
|
||||||
- [ ] 13.1 RED — the Ecualizador screen renders exactly **5** vertical sliders (regression guard — reject any
|
- [x] 13.1 RED — the Ecualizador screen renders exactly **5** vertical sliders (regression guard — reject any
|
||||||
future 7-slider change on sight).
|
future 7-slider change on sight). **Corrected at apply time**: named test file `ecualizador_widget_test.dart`
|
||||||
- [ ] 13.2 RED — `servicio_presets_personalizados_test.dart`: save/list/delete round-trip for a named custom preset
|
did not exist before this commit (`ecualizador_widget.dart` had zero coverage) — created new, plus the
|
||||||
|
screen-level assertion in `pantalla_ajustes_ecualizador_test.dart` (the spec scenario's own GIVEN is "the
|
||||||
|
Ecualizador settings screen").
|
||||||
|
- [x] 13.2 RED — `servicio_presets_personalizados_test.dart`: save/list/delete round-trip for a named custom preset
|
||||||
via its **own** SharedPreferences key `eq_custom_presets_v1`.
|
via its **own** SharedPreferences key `eq_custom_presets_v1`.
|
||||||
- [ ] 13.3 RED — submitting an empty/whitespace-only preset name shows a validation message and persists nothing.
|
- [x] 13.3 RED — submitting an empty/whitespace-only preset name shows a validation message and persists nothing.
|
||||||
- [ ] 13.4 RED — the "Emisoras con ajuste propio" drill-down lists exactly the stations present in
|
Covered at both the state layer (new `estado_ecualizador_presets_personalizados_test.dart` — see 13.7's note)
|
||||||
|
and the UI layer (`pantalla_ajustes_ecualizador_test.dart`'s "Guardar como preset" dialog scenarios).
|
||||||
|
- [x] 13.4 RED — the "Emisoras con ajuste propio" drill-down lists exactly the stations present in
|
||||||
`presetsPorEmisora`.
|
`presetsPorEmisora`.
|
||||||
- [ ] 13.5 RED — the "Salida activa" row is visible on the main screen (not only Advanced) and updates on a
|
- [x] 13.5 RED — the "Salida activa" row is visible on the main screen (not only Advanced) and updates on a
|
||||||
simulated device change; the base-vs-per-station explainer banner is visible.
|
simulated device change; the base-vs-per-station explainer banner is visible.
|
||||||
- [ ] 13.6 GREEN — create `lib/servicios/servicio_presets_personalizados.dart` (**new file**, own SharedPreferences
|
- [x] 13.6 GREEN — create `lib/servicios/servicio_presets_personalizados.dart` (**new file**, own SharedPreferences
|
||||||
key). **Do NOT add any member to `servicio_ecualizador.dart`.** `lib/modelos/preset_ecualizador.dart` stays
|
key). **Do NOT add any member to `servicio_ecualizador.dart`.** `lib/modelos/preset_ecualizador.dart` stays
|
||||||
unchanged — a user preset is a `PresetEcualizador` with a user-supplied `nombre`; `toJson`/`desdeJson`
|
unchanged — a user preset is a `PresetEcualizador` with a user-supplied `nombre`; `toJson`/`desdeJson`
|
||||||
already exist.
|
already exist.
|
||||||
- [ ] 13.7 GREEN — extend `EstadoEcualizador` with `presetsPersonalizados`, `guardarPresetPersonalizado(nombre)`,
|
- [x] 13.7 GREEN — extend `EstadoEcualizador` with `presetsPersonalizados`, `guardarPresetPersonalizado(nombre)`,
|
||||||
`eliminarPresetPersonalizado(nombre)`, reading/writing only through the new service.
|
`eliminarPresetPersonalizado(nombre)`, reading/writing only through the new service. **Correction found at
|
||||||
- [ ] 13.8 GREEN — restyle `lib/widgets/ecualizador_widget.dart` in place: strip the internal title + preset chip
|
apply time**: the new custom-preset LOAD (`cargarPresetsPersonalizados()`) is deliberately NOT folded into
|
||||||
row (lines 59-77 — the pushed header now carries the title), restyle the 5 `Card`/`RotatedBox`/`Slider`
|
`cargarPersistido()` — that method is exercised ~30 times by `estado_ecualizador_test.dart` (one of the three
|
||||||
tiles to the vertical-track look, add the `habilitado` parameter. Keep the `for (int i = 0; i < 5; i++)`
|
protected EQ test files) via Fakes for `servicio`/`dispositivoAudio` ONLY, with zero SharedPreferences
|
||||||
|
awareness anywhere in that file; adding a third always-real-by-default collaborator to it would have
|
||||||
|
introduced real SharedPreferences I/O into every one of those cases. Called explicitly from the screen's
|
||||||
|
`initState` instead (same pattern `refrescarDispositivoActual` already uses). New coverage lives in a NEW
|
||||||
|
file, `test/estado/estado_ecualizador_presets_personalizados_test.dart` — NOT added to
|
||||||
|
`estado_ecualizador_test.dart`, which stays unmodified.
|
||||||
|
- [x] 13.8 GREEN — restyle `lib/widgets/ecualizador_widget.dart` in place: strip the internal title + preset chip
|
||||||
|
row (re-verified at the CURRENT lines 59-77 of the pre-WU13 file, not trusted from the design doc's
|
||||||
|
reference), restyle the 5 `Card`/`RotatedBox`/`Slider` tiles to the vertical-track look (token-driven accent
|
||||||
|
colour, `AnimatedOpacity` grey-out), add the `habilitado` parameter (disables + greys every slider).
|
||||||
|
`PresetsEcualizadorWidget` additively widened with an optional `personalizados` list (default empty) so
|
||||||
|
custom presets can join the chip row without a second implementation. Kept the `for (int i = 0; i < 5; i++)`
|
||||||
loop bound literally `5`.
|
loop bound literally `5`.
|
||||||
- [ ] 13.9 GREEN — build `lib/pantallas/ajustes/pantalla_ajustes_ecualizador.dart` wiring
|
- [x] 13.9 GREEN — build out `lib/pantallas/ajustes/pantalla_ajustes_ecualizador.dart` (the WU3a placeholder body)
|
||||||
`EstadoEcualizador.presetPrincipal` / `cambiarPresetPrincipal`, surfacing "Salida activa" and the drill-down
|
wiring `EstadoEcualizador.presetPrincipal` / `cambiarPresetPrincipal`, surfacing "Salida activa" (own private
|
||||||
row.
|
row, live via the existing device-change `notifyListeners()`) and the drill-down row (own private row +
|
||||||
- [ ] 13.10 REFACTOR — reviewer check: confirm `git diff` is **empty** for `lib/servicios/servicio_ecualizador.dart`,
|
destination screen, station uuids resolved against `EstadoRadio.listaFavoritos` with a raw-uuid fallback for
|
||||||
|
a non-favorite station). Added the base-vs-per-station explainer banner and the "Guardar como preset" action
|
||||||
|
+ dialog (validates via `guardarPresetPersonalizado`, shows an inline `errorText` on empty/whitespace name).
|
||||||
|
- [x] 13.10 REFACTOR — reviewer check: confirmed `git diff` is **empty** for `lib/servicios/servicio_ecualizador.dart`,
|
||||||
`lib/modelos/preset_ecualizador.dart`, and the band-application block at
|
`lib/modelos/preset_ecualizador.dart`, and the band-application block at
|
||||||
`lib/servicios/servicio_audio.dart:749-762`.
|
`lib/servicios/servicio_audio.dart:749-762` (this WU never touches `servicio_audio.dart` at all).
|
||||||
- [ ] 13.11 Verify — the three EQ test files pass **unmodified**; exactly 5 sliders render.
|
- [x] 13.11 Verify — the three EQ test files (`servicio_ecualizador_test.dart`, `estado_ecualizador_test.dart`,
|
||||||
|
`servicio_audio_eq_reapply_test.dart`) pass **unmodified**; exactly 5 sliders render (component AND screen
|
||||||
|
level). Full suite: 713/713 green (2 skipped, unchanged), up from 682. `flutter analyze`: 1 issue, identical
|
||||||
|
to baseline. Literal-encoding scan: zero hits.
|
||||||
|
|
||||||
|
**`size:exception` recorded.** Realized: **1,871 changed lines** (1,793+/78-) across 25 files against the 400-550
|
||||||
|
forecast — same "a strict-TDD commit carries its test files, and any ARB touch drags 13 generated files with it"
|
||||||
|
pattern as every prior WU (Engram `reference/estimating-strict-tdd-diffs`, id 2514). Breakdown: `lib/` production
|
||||||
|
code alone is 628 lines (`pantalla_ajustes_ecualizador.dart` +326, `ecualizador_widget.dart` restyle ~151,
|
||||||
|
`estado_ecualizador.dart` +64, new `servicio_presets_personalizados.dart` +87) plus 22 ARB source lines — at the top
|
||||||
|
edge of the forecast band by itself given this WU also had to build out a screen body WU3a only stubbed; the 4
|
||||||
|
test files (2 new state/service-layer files plus the widget test and the extended screen test) add 675 lines, and
|
||||||
|
11 new ARB keys drag in 546 more via the 13 regenerated `lib/l10n/gen/*.dart` files. Justification: the restyle,
|
||||||
|
the new persistence service, the state-layer extension and the screen wiring (explainer/salida-activa/drill-down/
|
||||||
|
save-flow) are one cohesive vertical slice — WU13 must land as a whole before WU14 can reuse its editor component
|
||||||
|
by exact runtime type; splitting further would leave either an unstyled widget with no screen consumer or a screen
|
||||||
|
half-wired to a service that doesn't exist yet.
|
||||||
|
|
||||||
## WU14 — Reproductor completo + per-station EQ sheet
|
## WU14 — Reproductor completo + per-station EQ sheet
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pluriwave/estado/estado_ecualizador.dart';
|
||||||
|
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
||||||
|
|
||||||
|
import '../helpers/fakes.dart';
|
||||||
|
|
||||||
|
/// WU13 task 13.7 — `EstadoEcualizador`'s new custom-preset members
|
||||||
|
/// (`presetsPersonalizados`, `guardarPresetPersonalizado`,
|
||||||
|
/// `eliminarPresetPersonalizado`), reading/writing only through the new
|
||||||
|
/// `ServicioPresetsPersonalizados` (design ADR-5 hazard box).
|
||||||
|
///
|
||||||
|
/// **Correction found at apply time, deliberate**: this is a NEW file, not
|
||||||
|
/// an addition to `estado_ecualizador_test.dart`. That file is one of the
|
||||||
|
/// three EQ test files this change's master guard requires to pass
|
||||||
|
/// **unmodified** — it is exercised ~30 times via `cargarPersistido()`
|
||||||
|
/// using Fakes for `servicio`/`dispositivoAudio` ONLY, with no
|
||||||
|
/// SharedPreferences awareness anywhere in the file. Because of that,
|
||||||
|
/// `cargarPersistido()` itself is intentionally left untouched by this WU
|
||||||
|
/// (see `cargarPresetsPersonalizados` below) rather than folding a third,
|
||||||
|
/// always-real-by-default collaborator into a method exercised by a file
|
||||||
|
/// that must never change.
|
||||||
|
void main() {
|
||||||
|
EstadoEcualizador buildEq({FakeServicioPresetsPersonalizados? servicio}) {
|
||||||
|
return EstadoEcualizador(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
servicio: FakeServicioEcualizador(),
|
||||||
|
presetsPersonalizadosService:
|
||||||
|
servicio ?? FakeServicioPresetsPersonalizados(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('presetsPersonalizados starts empty before loading', () {
|
||||||
|
final eq = buildEq();
|
||||||
|
|
||||||
|
expect(eq.presetsPersonalizados, isEmpty);
|
||||||
|
eq.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'cargarPresetsPersonalizados populates presetsPersonalizados from the service',
|
||||||
|
() async {
|
||||||
|
final fakeServicio = FakeServicioPresetsPersonalizados();
|
||||||
|
final preexistente = PresetEcualizador(
|
||||||
|
nombre: 'Ya guardado',
|
||||||
|
bandas: [1, 2, 3, 4, 5],
|
||||||
|
);
|
||||||
|
await fakeServicio.guardar(preexistente);
|
||||||
|
final eq = buildEq(servicio: fakeServicio);
|
||||||
|
|
||||||
|
await eq.cargarPresetsPersonalizados();
|
||||||
|
|
||||||
|
expect(eq.presetsPersonalizados, hasLength(1));
|
||||||
|
expect(eq.presetsPersonalizados.single.nombre, equals('Ya guardado'));
|
||||||
|
eq.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'guardarPresetPersonalizado saves the CURRENT effective preset bands under the given name',
|
||||||
|
() async {
|
||||||
|
final eq = buildEq();
|
||||||
|
await eq.cambiarPreset(PresetEcualizador.jazz);
|
||||||
|
|
||||||
|
final guardado = await eq.guardarPresetPersonalizado('Mi preset');
|
||||||
|
|
||||||
|
expect(guardado, isTrue);
|
||||||
|
expect(eq.presetsPersonalizados, hasLength(1));
|
||||||
|
expect(eq.presetsPersonalizados.single.nombre, equals('Mi preset'));
|
||||||
|
expect(
|
||||||
|
eq.presetsPersonalizados.single.bandas,
|
||||||
|
equals(PresetEcualizador.jazz.bandas),
|
||||||
|
);
|
||||||
|
eq.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('guardarPresetPersonalizado notifies listeners on success', () async {
|
||||||
|
final eq = buildEq();
|
||||||
|
var avisos = 0;
|
||||||
|
eq.addListener(() => avisos++);
|
||||||
|
|
||||||
|
await eq.guardarPresetPersonalizado('Preset con nombre');
|
||||||
|
|
||||||
|
expect(avisos, greaterThanOrEqualTo(1));
|
||||||
|
eq.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'guardarPresetPersonalizado rejects an empty name: no save, returns false',
|
||||||
|
() async {
|
||||||
|
final eq = buildEq();
|
||||||
|
|
||||||
|
final guardado = await eq.guardarPresetPersonalizado('');
|
||||||
|
|
||||||
|
expect(guardado, isFalse);
|
||||||
|
expect(eq.presetsPersonalizados, isEmpty);
|
||||||
|
eq.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'guardarPresetPersonalizado rejects a whitespace-only name: no save, returns false',
|
||||||
|
() async {
|
||||||
|
final eq = buildEq();
|
||||||
|
|
||||||
|
final guardado = await eq.guardarPresetPersonalizado(' ');
|
||||||
|
|
||||||
|
expect(guardado, isFalse);
|
||||||
|
expect(eq.presetsPersonalizados, isEmpty);
|
||||||
|
eq.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'guardarPresetPersonalizado trims surrounding whitespace from a valid name',
|
||||||
|
() async {
|
||||||
|
final eq = buildEq();
|
||||||
|
|
||||||
|
final guardado = await eq.guardarPresetPersonalizado(' Con espacios ');
|
||||||
|
|
||||||
|
expect(guardado, isTrue);
|
||||||
|
expect(eq.presetsPersonalizados.single.nombre, equals('Con espacios'));
|
||||||
|
eq.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'eliminarPresetPersonalizado removes a previously saved preset',
|
||||||
|
() async {
|
||||||
|
final eq = buildEq();
|
||||||
|
await eq.guardarPresetPersonalizado('Para borrar');
|
||||||
|
expect(eq.presetsPersonalizados, hasLength(1));
|
||||||
|
|
||||||
|
await eq.eliminarPresetPersonalizado('Para borrar');
|
||||||
|
|
||||||
|
expect(eq.presetsPersonalizados, isEmpty);
|
||||||
|
eq.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'guardarPresetPersonalizado reads/writes only through the injected service '
|
||||||
|
'(never touches ServicioEcualizador)',
|
||||||
|
() async {
|
||||||
|
final fakeEcualizador = FakeServicioEcualizador();
|
||||||
|
final eq = EstadoEcualizador(
|
||||||
|
audio: FakeServicioAudio(),
|
||||||
|
servicio: fakeEcualizador,
|
||||||
|
presetsPersonalizadosService: FakeServicioPresetsPersonalizados(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await eq.guardarPresetPersonalizado('Aislado');
|
||||||
|
|
||||||
|
// ServicioEcualizador's own config is completely untouched by the
|
||||||
|
// custom-preset save — it lives in a separate service (ADR-5 hazard
|
||||||
|
// box), not folded into servicio_ecualizador.dart's persistence.
|
||||||
|
expect(fakeEcualizador.config.porEmisora, isEmpty);
|
||||||
|
eq.dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import 'package:pluriwave/servicios/servicio_dispositivo_audio.dart';
|
|||||||
import 'package:pluriwave/servicios/servicio_ecualizador.dart';
|
import 'package:pluriwave/servicios/servicio_ecualizador.dart';
|
||||||
import 'package:pluriwave/servicios/servicio_favoritos.dart';
|
import 'package:pluriwave/servicios/servicio_favoritos.dart';
|
||||||
import 'package:pluriwave/servicios/servicio_grabacion_radio.dart';
|
import 'package:pluriwave/servicios/servicio_grabacion_radio.dart';
|
||||||
|
import 'package:pluriwave/servicios/servicio_presets_personalizados.dart';
|
||||||
import 'package:pluriwave/servicios/servicio_radio.dart';
|
import 'package:pluriwave/servicios/servicio_radio.dart';
|
||||||
|
|
||||||
class FakeServicioAudio extends ServicioAudio {
|
class FakeServicioAudio extends ServicioAudio {
|
||||||
@@ -477,6 +478,27 @@ class FakeServicioEcualizador extends ServicioEcualizador {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// In-memory fake for [ServicioPresetsPersonalizados] (WU13). Avoids any
|
||||||
|
/// real SharedPreferences I/O in state-layer tests, matching every other
|
||||||
|
/// `Fake*` service in this file.
|
||||||
|
class FakeServicioPresetsPersonalizados extends ServicioPresetsPersonalizados {
|
||||||
|
final List<PresetEcualizador> _presets = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<PresetEcualizador>> listar() async => List.from(_presets);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> guardar(PresetEcualizador preset) async {
|
||||||
|
_presets.removeWhere((p) => p.nombre == preset.nombre);
|
||||||
|
_presets.add(preset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> eliminar(String nombre) async {
|
||||||
|
_presets.removeWhere((p) => p.nombre == nombre);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A [ServicioDispositivoAudio] fake that throws on [obtenerDispositivoActual].
|
/// A [ServicioDispositivoAudio] fake that throws on [obtenerDispositivoActual].
|
||||||
///
|
///
|
||||||
/// Used to test the graceful-failure path in [EstadoEcualizador.cargarPersistido].
|
/// Used to test the graceful-failure path in [EstadoEcualizador.cargarPersistido].
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:pluriwave/estado/estado_ecualizador.dart';
|
import 'package:pluriwave/estado/estado_ecualizador.dart';
|
||||||
import 'package:pluriwave/estado/estado_radio.dart';
|
import 'package:pluriwave/estado/estado_radio.dart';
|
||||||
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
||||||
|
import 'package:pluriwave/modelos/dispositivo_audio.dart';
|
||||||
|
import 'package:pluriwave/modelos/emisora.dart';
|
||||||
|
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
||||||
import 'package:pluriwave/pantallas/ajustes/pantalla_ajustes_ecualizador.dart';
|
import 'package:pluriwave/pantallas/ajustes/pantalla_ajustes_ecualizador.dart';
|
||||||
import 'package:pluriwave/widgets/pluri_push_scaffold.dart';
|
import 'package:pluriwave/widgets/pluri_push_scaffold.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -17,6 +20,12 @@ import '../../helpers/fakes_alarmas.dart';
|
|||||||
/// a [PluriPushScaffold] and its moved controls (the enable switch) still
|
/// a [PluriPushScaffold] and its moved controls (the enable switch) still
|
||||||
/// respond exactly as they did inside the old `_SeccionEcualizador`.
|
/// respond exactly as they did inside the old `_SeccionEcualizador`.
|
||||||
///
|
///
|
||||||
|
/// WU13 restyled the header away (design ADR-5 — the strip that used to
|
||||||
|
/// duplicate "Equalizer" as `EcualizadorWidget`'s own internal title is
|
||||||
|
/// gone), added the base-vs-per-station explainer, the "Salida activa"
|
||||||
|
/// row, the "Emisoras con ajuste propio" drill-down and the custom-preset
|
||||||
|
/// save flow — see the `WU13` group below.
|
||||||
|
///
|
||||||
/// Pre-existing project constraint (see `pantalla_ajustes_test.dart`):
|
/// Pre-existing project constraint (see `pantalla_ajustes_test.dart`):
|
||||||
/// PluriGlassSurface paints a background over ListTile's ink layer (here,
|
/// PluriGlassSurface paints a background over ListTile's ink layer (here,
|
||||||
/// via SwitchListTile), which Flutter flags as a warning-level assertion,
|
/// via SwitchListTile), which Flutter flags as a warning-level assertion,
|
||||||
@@ -43,17 +52,33 @@ void main() {
|
|||||||
'${Directory.current.path}/test/fixtures/emisoras_custom_vacio.json',
|
'${Directory.current.path}/test/fixtures/emisoras_custom_vacio.json',
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<EstadoRadio> crearEstado() async {
|
Future<EstadoRadio> crearEstado({
|
||||||
|
Map<String, PresetEcualizador>? porEmisora,
|
||||||
|
FakeServicioDispositivoAudio? dispositivoAudio,
|
||||||
|
bool eqMultiDeviceEnabled = false,
|
||||||
|
List<Emisora> favoritosIniciales = const [],
|
||||||
|
}) async {
|
||||||
|
final favoritos = FakeServicioFavoritos();
|
||||||
|
for (final emisora in favoritosIniciales) {
|
||||||
|
await favoritos.agregar(emisora);
|
||||||
|
}
|
||||||
final estado = EstadoRadio(
|
final estado = EstadoRadio(
|
||||||
audio: FakeServicioAudio(),
|
audio: FakeServicioAudio(),
|
||||||
favoritos: FakeServicioFavoritos(),
|
favoritos: favoritos,
|
||||||
radio: FakeServicioRadio(),
|
radio: FakeServicioRadio(),
|
||||||
servicioEcualizador: FakeServicioEcualizador(),
|
servicioEcualizador: FakeServicioEcualizador(
|
||||||
|
porEmisora: porEmisora,
|
||||||
|
eqMultiDeviceEnabled: eqMultiDeviceEnabled,
|
||||||
|
),
|
||||||
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
servicioGrabacion: FakeServicioGrabacionRadioInactiva(),
|
||||||
|
dispositivoAudio: dispositivoAudio,
|
||||||
resolverArchivoCustom: archivoCustomVacio,
|
resolverArchivoCustom: archivoCustomVacio,
|
||||||
iniciarAutomaticamente: false,
|
iniciarAutomaticamente: false,
|
||||||
);
|
);
|
||||||
await estado.ecualizador.cargarPersistido();
|
await estado.ecualizador.cargarPersistido();
|
||||||
|
if (favoritosIniciales.isNotEmpty) {
|
||||||
|
await estado.cargarFavoritos();
|
||||||
|
}
|
||||||
return estado;
|
return estado;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,10 +107,11 @@ void main() {
|
|||||||
await tester.pumpWidget(buildScreen(estado));
|
await tester.pumpWidget(buildScreen(estado));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// "Equalizer" also appears inside EcualizadorWidget's own pre-existing
|
// WU13 stripped EcualizadorWidget's own internal "Equalizer" title +
|
||||||
// internal header, which WU3a does not touch (ecualizador_widget.dart's
|
// preset Chip row (design ADR-5) — the pushed screen's AppBar is now
|
||||||
// header strip is WU13's job per design ADR-5) — so we assert on the
|
// the ONLY place this title renders. Still asserting on the AppBar
|
||||||
// AppBar's title specifically rather than a bare text match.
|
// specifically (not a bare text match) keeps this test meaningful even
|
||||||
|
// if a future WU reintroduces a second on-screen occurrence.
|
||||||
expect(find.byType(PluriPushScaffold), findsOneWidget);
|
expect(find.byType(PluriPushScaffold), findsOneWidget);
|
||||||
final appBar = tester.widget<AppBar>(find.byType(AppBar));
|
final appBar = tester.widget<AppBar>(find.byType(AppBar));
|
||||||
expect((appBar.title as Text).data, equals('Equalizer'));
|
expect((appBar.title as Text).data, equals('Equalizer'));
|
||||||
@@ -107,4 +133,242 @@ void main() {
|
|||||||
|
|
||||||
expect(estado.ecualizador.activo, equals(!before));
|
expect(estado.ecualizador.activo, equals(!before));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('WU13 — restyle, custom presets, salida activa, drill-down', () {
|
||||||
|
testWidgets('exactly 5 sliders render on the Ecualizador screen', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
// Spec `eq-custom-presets` "Five-Band Equalizer (Regression Guard)":
|
||||||
|
// GIVEN the user opens the Ecualizador screen THEN exactly 5 sliders
|
||||||
|
// render — asserted here at the SCREEN level (task 13.1's own GIVEN),
|
||||||
|
// in addition to the component-level guard in
|
||||||
|
// `ecualizador_widget_test.dart`.
|
||||||
|
_suppressListTileInkAssertion();
|
||||||
|
final estado = await crearEstado();
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildScreen(estado));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(Slider), findsNWidgets(5));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('base-vs-per-station explainer banner is visible', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
_suppressListTileInkAssertion();
|
||||||
|
final estado = await crearEstado();
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildScreen(estado));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('eq-base-explainer-banner')), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Salida activa row is visible and shows a default label when no device is tracked',
|
||||||
|
(tester) async {
|
||||||
|
_suppressListTileInkAssertion();
|
||||||
|
final estado = await crearEstado();
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildScreen(estado));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('eq-active-output-row')), findsOneWidget);
|
||||||
|
expect(find.text('Active output'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(const Key('eq-active-output-row')),
|
||||||
|
matching: find.text("This device's speaker"),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('Salida activa row updates on a simulated device change', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
_suppressListTileInkAssertion();
|
||||||
|
final dispositivoAudio = FakeServicioDispositivoAudio();
|
||||||
|
final estado = await crearEstado(
|
||||||
|
dispositivoAudio: dispositivoAudio,
|
||||||
|
eqMultiDeviceEnabled: true,
|
||||||
|
);
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildScreen(estado));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
dispositivoAudio.emitirDispositivo(
|
||||||
|
const DispositivoAudio(
|
||||||
|
id: 'bt_a2dp:AA:BB:CC:DD:EE:FF',
|
||||||
|
tipo: TipoDispositivo.bluetoothA2dp,
|
||||||
|
nombre: 'Auriculares BT',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(const Key('eq-active-output-row')),
|
||||||
|
matching: find.text('Auriculares BT'),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Emisoras con ajuste propio drill-down lists exactly the overridden stations',
|
||||||
|
(tester) async {
|
||||||
|
_suppressListTileInkAssertion();
|
||||||
|
const favorita = Emisora(
|
||||||
|
uuid: 'uuid-favorita',
|
||||||
|
nombre: 'Radio Favorita',
|
||||||
|
url: 'https://example.com/favorita',
|
||||||
|
);
|
||||||
|
final estado = await crearEstado(
|
||||||
|
porEmisora: {
|
||||||
|
'uuid-favorita': PresetEcualizador.rock,
|
||||||
|
'uuid-no-favorita': PresetEcualizador.jazz,
|
||||||
|
},
|
||||||
|
favoritosIniciales: const [favorita],
|
||||||
|
);
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildScreen(estado));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('eq-stations-own-eq-row')), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
tester
|
||||||
|
.widget<Text>(find.byKey(const Key('eq-stations-own-eq-count')))
|
||||||
|
.data,
|
||||||
|
equals('2'),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('eq-stations-own-eq-row')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// The favorite station resolves to its display name; the other
|
||||||
|
// uuid (never seen among favorites) falls back to the raw uuid —
|
||||||
|
// same fallback chain `nombreVisible` already uses for devices.
|
||||||
|
expect(find.text('Radio Favorita'), findsOneWidget);
|
||||||
|
expect(find.text('uuid-no-favorita'), findsOneWidget);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'drill-down shows an empty state when no station has its own EQ',
|
||||||
|
(tester) async {
|
||||||
|
_suppressListTileInkAssertion();
|
||||||
|
final estado = await crearEstado();
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildScreen(estado));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('eq-stations-own-eq-row')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('No stations have their own EQ yet.'), findsOneWidget);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Guardar como preset persists the current bands; the preset appears in the chip row',
|
||||||
|
(tester) async {
|
||||||
|
_suppressListTileInkAssertion();
|
||||||
|
final estado = await crearEstado();
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildScreen(estado));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.ensureVisible(
|
||||||
|
find.byKey(const Key('eq-save-preset-action')),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const Key('eq-save-preset-action')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.enterText(
|
||||||
|
find.byKey(const Key('eq-save-preset-name-field')),
|
||||||
|
'Mi preset',
|
||||||
|
);
|
||||||
|
await tester.tap(
|
||||||
|
find.byKey(const Key('eq-save-preset-confirm-button')),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AlertDialog), findsNothing);
|
||||||
|
expect(find.text('Mi preset'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
estado.ecualizador.presetsPersonalizados.map((p) => p.nombre),
|
||||||
|
contains('Mi preset'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Guardar como preset with an empty name shows a validation message and persists nothing',
|
||||||
|
(tester) async {
|
||||||
|
_suppressListTileInkAssertion();
|
||||||
|
final estado = await crearEstado();
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildScreen(estado));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.ensureVisible(
|
||||||
|
find.byKey(const Key('eq-save-preset-action')),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const Key('eq-save-preset-action')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(
|
||||||
|
find.byKey(const Key('eq-save-preset-confirm-button')),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AlertDialog), findsOneWidget);
|
||||||
|
expect(find.text('Enter a name for the preset.'), findsOneWidget);
|
||||||
|
expect(estado.ecualizador.presetsPersonalizados, isEmpty);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Guardar como preset with a whitespace-only name shows the same validation message',
|
||||||
|
(tester) async {
|
||||||
|
_suppressListTileInkAssertion();
|
||||||
|
final estado = await crearEstado();
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildScreen(estado));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.ensureVisible(
|
||||||
|
find.byKey(const Key('eq-save-preset-action')),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const Key('eq-save-preset-action')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.enterText(
|
||||||
|
find.byKey(const Key('eq-save-preset-name-field')),
|
||||||
|
' ',
|
||||||
|
);
|
||||||
|
await tester.tap(
|
||||||
|
find.byKey(const Key('eq-save-preset-confirm-button')),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AlertDialog), findsOneWidget);
|
||||||
|
expect(estado.ecualizador.presetsPersonalizados, isEmpty);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
||||||
|
import 'package:pluriwave/servicios/servicio_presets_personalizados.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
/// WU13 task 13.2 — persistence for user-named custom EQ presets.
|
||||||
|
///
|
||||||
|
/// Design ADR-5 hazard box: this lives in its OWN file and OWN
|
||||||
|
/// SharedPreferences key (`eq_custom_presets_v1`), deliberately separate
|
||||||
|
/// from `ServicioEcualizador` — that file has an empty-`git diff` success
|
||||||
|
/// criterion for this change, so persistence for custom presets must never
|
||||||
|
/// be added there.
|
||||||
|
void main() {
|
||||||
|
setUp(() {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('listar returns an empty list when nothing was saved yet', () async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final servicio = ServicioPresetsPersonalizados(prefs: prefs);
|
||||||
|
|
||||||
|
expect(await servicio.listar(), isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('guardar then listar round-trips a named custom preset', () async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final servicio = ServicioPresetsPersonalizados(prefs: prefs);
|
||||||
|
// Not `const`: PresetEcualizador's constructor asserts `bandas.length
|
||||||
|
// == 5`, and `List.length` is not constant-foldable in an assert here
|
||||||
|
// (same const-eval limitation the branch already hit for
|
||||||
|
// `DateTime(...)` fixtures in WU9) — the model's own static presets
|
||||||
|
// (e.g. `PresetEcualizador.flat`) are declared `final`, not `const`,
|
||||||
|
// for the same reason.
|
||||||
|
final preset = PresetEcualizador(
|
||||||
|
nombre: 'Mi preset',
|
||||||
|
bandas: [1.0, -2.0, 3.0, -4.0, 5.0],
|
||||||
|
);
|
||||||
|
|
||||||
|
await servicio.guardar(preset);
|
||||||
|
final guardados = await servicio.listar();
|
||||||
|
|
||||||
|
expect(guardados, hasLength(1));
|
||||||
|
expect(guardados.single, equals(preset));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('guardar persists under its OWN SharedPreferences key', () async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final servicio = ServicioPresetsPersonalizados(prefs: prefs);
|
||||||
|
final preset = PresetEcualizador(nombre: 'Otro', bandas: [0, 0, 0, 0, 0]);
|
||||||
|
|
||||||
|
await servicio.guardar(preset);
|
||||||
|
|
||||||
|
expect(prefs.getString('eq_custom_presets_v1'), isNotNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('guardar with a repeated name replaces the previous entry', () async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final servicio = ServicioPresetsPersonalizados(prefs: prefs);
|
||||||
|
final v1 = PresetEcualizador(nombre: 'Mismo', bandas: [1, 1, 1, 1, 1]);
|
||||||
|
final v2 = PresetEcualizador(nombre: 'Mismo', bandas: [2, 2, 2, 2, 2]);
|
||||||
|
|
||||||
|
await servicio.guardar(v1);
|
||||||
|
await servicio.guardar(v2);
|
||||||
|
final guardados = await servicio.listar();
|
||||||
|
|
||||||
|
expect(guardados, hasLength(1));
|
||||||
|
expect(guardados.single.bandas, equals(v2.bandas));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('guardar preserves insertion order across multiple presets', () async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final servicio = ServicioPresetsPersonalizados(prefs: prefs);
|
||||||
|
final primero = PresetEcualizador(
|
||||||
|
nombre: 'Primero',
|
||||||
|
bandas: [0, 0, 0, 0, 0],
|
||||||
|
);
|
||||||
|
final segundo = PresetEcualizador(
|
||||||
|
nombre: 'Segundo',
|
||||||
|
bandas: [0, 0, 0, 0, 0],
|
||||||
|
);
|
||||||
|
|
||||||
|
await servicio.guardar(primero);
|
||||||
|
await servicio.guardar(segundo);
|
||||||
|
final guardados = await servicio.listar();
|
||||||
|
|
||||||
|
expect(guardados.map((p) => p.nombre).toList(), ['Primero', 'Segundo']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('eliminar removes the named preset only', () async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final servicio = ServicioPresetsPersonalizados(prefs: prefs);
|
||||||
|
final conservar = PresetEcualizador(
|
||||||
|
nombre: 'Conservar',
|
||||||
|
bandas: [0, 0, 0, 0, 0],
|
||||||
|
);
|
||||||
|
final borrar = PresetEcualizador(nombre: 'Borrar', bandas: [0, 0, 0, 0, 0]);
|
||||||
|
await servicio.guardar(conservar);
|
||||||
|
await servicio.guardar(borrar);
|
||||||
|
|
||||||
|
await servicio.eliminar('Borrar');
|
||||||
|
final guardados = await servicio.listar();
|
||||||
|
|
||||||
|
expect(guardados, hasLength(1));
|
||||||
|
expect(guardados.single.nombre, equals('Conservar'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('eliminar on an unknown name is a safe no-op', () async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final servicio = ServicioPresetsPersonalizados(prefs: prefs);
|
||||||
|
|
||||||
|
await servicio.eliminar('no existe');
|
||||||
|
|
||||||
|
expect(await servicio.listar(), isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'a corrupt top-level payload degrades to an empty list, never throws',
|
||||||
|
() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString('eq_custom_presets_v1', 'not valid json{{{');
|
||||||
|
final servicio = ServicioPresetsPersonalizados(prefs: prefs);
|
||||||
|
|
||||||
|
await expectLater(servicio.listar(), completion(isEmpty));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
||||||
|
import 'package:pluriwave/modelos/preset_ecualizador.dart';
|
||||||
|
import 'package:pluriwave/widgets/ecualizador_widget.dart';
|
||||||
|
|
||||||
|
/// WU13 task 13.1 — first-class regression guard (design ADR-5, spec
|
||||||
|
/// `eq-custom-presets` "Five-Band Equalizer"): the equalizer widget must
|
||||||
|
/// always render exactly 5 vertical sliders. Band count is device-reported
|
||||||
|
/// via `just_audio`'s `AndroidEqualizer` (spike, Engram id 2498), not
|
||||||
|
/// app-chosen — any future change rendering 7 sliders is rejected on sight.
|
||||||
|
///
|
||||||
|
/// **Correction found at apply time**: `tasks.md`'s WU13 Verify command
|
||||||
|
/// names this file as if it already existed ("Modified tests:
|
||||||
|
/// ecualizador_widget_test.dart"). No such file existed before this commit
|
||||||
|
/// (`ecualizador_widget.dart` had zero test coverage) — created new instead.
|
||||||
|
void main() {
|
||||||
|
Widget buildWidget({
|
||||||
|
PresetEcualizador? preset,
|
||||||
|
bool habilitado = true,
|
||||||
|
void Function(PresetEcualizador)? onCambio,
|
||||||
|
}) {
|
||||||
|
return MaterialApp(
|
||||||
|
locale: const Locale('en'),
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Scaffold(
|
||||||
|
body: EcualizadorWidget(
|
||||||
|
preset: preset ?? PresetEcualizador.flat,
|
||||||
|
habilitado: habilitado,
|
||||||
|
onCambio: onCambio ?? (_) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('renders exactly 5 vertical sliders, one per band', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(buildWidget());
|
||||||
|
|
||||||
|
expect(find.byType(Slider), findsNWidgets(5));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('renders exactly 5 sliders regardless of the preset selected', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
// Regression guard restated: a future change proposing 7 sliders (the
|
||||||
|
// rejected mockup band count) must fail this test regardless of which
|
||||||
|
// fixed preset is active.
|
||||||
|
await tester.pumpWidget(buildWidget(preset: PresetEcualizador.jazz));
|
||||||
|
|
||||||
|
expect(find.byType(Slider), findsNWidgets(5));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('dragging a slider reports the updated band back via onCambio', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
PresetEcualizador? reportado;
|
||||||
|
await tester.pumpWidget(buildWidget(onCambio: (p) => reportado = p));
|
||||||
|
|
||||||
|
final slider = tester.widget<Slider>(find.byType(Slider).first);
|
||||||
|
slider.onChanged?.call(6.0);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(reportado, isNotNull);
|
||||||
|
expect(reportado!.bandas.length, 5);
|
||||||
|
expect(reportado!.bandas.first, 6.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'habilitado: false disables every slider (greyed, non-interactive)',
|
||||||
|
(tester) async {
|
||||||
|
await tester.pumpWidget(buildWidget(habilitado: false));
|
||||||
|
|
||||||
|
final sliders = tester.widgetList<Slider>(find.byType(Slider));
|
||||||
|
expect(sliders, hasLength(5));
|
||||||
|
for (final slider in sliders) {
|
||||||
|
expect(slider.onChanged, isNull);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('habilitado: true (default) keeps every slider interactive', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(buildWidget());
|
||||||
|
|
||||||
|
final sliders = tester.widgetList<Slider>(find.byType(Slider));
|
||||||
|
expect(sliders, hasLength(5));
|
||||||
|
for (final slider in sliders) {
|
||||||
|
expect(slider.onChanged, isNotNull);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user