Files
pluriwave/lib/estado/estado_radio.dart
T
FreeTLab 52855e75c2 refactor(state): extract recording and search state, scope screen rebuilds
- New EstadoGrabacion owns the recording service, subscription, directory/size preferences and open-file actions
- New EstadoBusqueda owns search, nearby stations, pagination and the min-bitrate filter
- New orden_emisoras.dart with the OrdenEmisoras enum, shared sorter and list identity memoization so context.select comparisons work on derived lists
- Large screens (inicio, buscar, favoritos, ajustes, reproductor) consume scoped selects/dedicated notifiers instead of root context.watch<EstadoRadio>, so audio buffer events no longer rebuild whole screens
- Remove all 15 TODO(S4b) compat members from EstadoRadio; consumers use the dedicated providers. EstadoRadio drops from ~1121 to 753 lines, keeping playback/stations/favorites orchestration
- 8 new tests including a rebuild-scoping probe (110 total green), flutter analyze clean
2026-06-11 21:43:18 +02:00

754 lines
26 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart' show Locale;
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../l10n/display_names.dart';
import '../l10n/gen/app_localizations.dart';
import '../modelos/emisora.dart';
import '../modelos/grupo_favoritos.dart';
import '../modelos/preset_ecualizador.dart';
import 'estado_busqueda.dart';
import 'estado_ecualizador.dart';
import 'estado_grabacion.dart';
import 'orden_emisoras.dart';
import '../servicios/servicio_audio.dart';
import '../servicios/servicio_ecualizador.dart';
import '../servicios/servicio_export_import.dart';
import '../servicios/servicio_favoritos.dart';
import '../servicios/servicio_grabacion_radio.dart';
import '../servicios/servicio_radio.dart';
import '../servicios/servicio_timer.dart';
export 'orden_emisoras.dart' show OrdenEmisoras;
/// Estado global de la app con ChangeNotifier (Provider).
///
/// S4 end-state: playback + stations + favorites orchestration. EQ, recording
/// and search state live in their own notifiers (EstadoEcualizador,
/// EstadoGrabacion, EstadoBusqueda) created here during the S4 transition and
/// exposed app-wide through ListenableProviders in app.dart.
class EstadoRadio extends ChangeNotifier {
EstadoRadio({
ServicioAudio? audio,
ServicioFavoritos? favoritos,
ServicioRadio? radio,
ServicioEcualizador? servicioEcualizador,
ServicioGrabacionRadio? servicioGrabacion,
SharedPreferences? prefs,
Future<File> Function()? resolverArchivoCustom,
bool iniciarAutomaticamente = true,
}) : audio = audio ?? ServicioAudio(),
favoritos = favoritos ?? ServicioFavoritos(),
radio = radio ?? ServicioRadio(),
servicioEcualizador =
servicioEcualizador ?? ServicioEcualizador(prefs: prefs),
_prefs = prefs,
_resolverArchivoCustom = resolverArchivoCustom {
ecualizador = EstadoEcualizador(
audio: this.audio,
servicio: this.servicioEcualizador,
emisoraActualUuid: () => emisoraActual?.uuid,
);
grabacion = EstadoGrabacion(
servicio: servicioGrabacion ?? ServicioGrabacionRadio(prefs: prefs),
emisoraActual: () => emisoraActual,
alError: _errorController.add,
);
busqueda = EstadoBusqueda(
radio: this.radio,
ordenListas: () => _ordenListas,
textos: () => _textos,
alError: _errorController.add,
);
timer = ServicioTimer(this.audio);
_escucharErroresReproduccion();
if (iniciarAutomaticamente) {
_initFuture = _init();
}
}
final ServicioAudio audio;
final ServicioFavoritos favoritos;
final ServicioRadio radio;
final ServicioEcualizador servicioEcualizador;
/// Domain notifiers extracted from this class (S4). Created and disposed
/// here (they need EstadoRadio's services and callbacks at construction);
/// exposed app-wide through ListenableProviders in app.dart.
late final EstadoEcualizador ecualizador;
late final EstadoGrabacion grabacion;
late final EstadoBusqueda busqueda;
static const ServicioExportImport _exportImport = ServicioExportImport();
final SharedPreferences? _prefs;
final Future<File> Function()? _resolverArchivoCustom;
/// Single startup instance injected from main() (S3-R4); falls back to
/// getInstance() only when nothing was injected (tests, legacy callers).
Future<SharedPreferences> _resolverPrefs() async =>
_prefs ?? SharedPreferences.getInstance();
AppLocalizations get _textos {
final actual = _l10n;
if (actual != null) return actual;
return lookupAppLocalizations(const Locale('es'));
}
void configurarLocalizaciones(AppLocalizations l10n) {
_l10n = l10n;
audio.configurarLocalizaciones(l10n);
grabacion.configurarLocalizaciones(l10n);
// The alarm bridge gets its localizations through
// EstadoAlarmas.configurarLocalizaciones (Decision 3.2) — the old
// static ServicioAlarmasAndroid shim is gone.
}
late final ServicioTimer timer;
StreamSubscription<EstadoReproduccion>? _suscripcionEstadoAudio;
Future<void>? _initFuture;
int _revisionReproduccion = 0;
Emisora? _emisoraSeleccionada;
String? _emisoraPreferidaUuid;
AppLocalizations? _l10n;
// Errores de reproducción → SnackBar.
final _errorController = StreamController<String>.broadcast();
Stream<String> get errorStream => _errorController.stream;
List<Emisora> _populares = [];
List<Emisora> _tendencias = [];
List<Emisora> _listaFavoritos = [];
List<GrupoFavoritos> _gruposFavoritos = [];
List<Emisora> _emisorasCustom = [];
bool _cargandoPopulares = false;
String? _errorCarga;
// Identity-memoized derived lists so `context.select` consumers only
// rebuild when the underlying data actually changes (S4-R5).
final _memoPopulares = MemoLista<Emisora>();
final _memoTendencias = MemoLista<Emisora>();
final _memoFavoritos = MemoLista<Emisora>();
final _memoGrupos = MemoLista<GrupoFavoritos>();
final _memoCustom = MemoLista<Emisora>();
final _memoInicio = MemoLista<Emisora>();
final _memoDisponibles = MemoLista<Emisora>();
final _memoTimerPresets = MemoLista<int>();
static const _keyEmisoraPreferida = 'emisora_preferida_uuid_v1';
static const _keyOrdenListas = 'orden_listas_emisoras_v1';
static const _keyTimerSuenoPresets = 'timer_sueno_presets_segundos_v1';
static const _timerSuenoPresetsDefecto = <int>[
180,
300,
600,
900,
1800,
3600,
5400,
7200,
10800,
];
List<int> _timerSuenoPresetsSegundos = List<int>.from(
_timerSuenoPresetsDefecto,
);
OrdenEmisoras _ordenListas = OrdenEmisoras.calidad;
List<Emisora> get populares => _memoPopulares.obtener([
_populares,
_ordenListas,
], () => ordenarEmisoras(_populares, _ordenListas));
List<Emisora> get tendencias => _memoTendencias.obtener([
_tendencias,
_ordenListas,
], () => ordenarEmisoras(_tendencias, _ordenListas));
List<Emisora> get listaFavoritos => _memoFavoritos.obtener([
_listaFavoritos,
_ordenListas,
], () => ordenarEmisoras(_listaFavoritos, _ordenListas));
List<GrupoFavoritos> get gruposFavoritos => _memoGrupos.obtener([
_gruposFavoritos,
], () => List<GrupoFavoritos>.unmodifiable(_gruposFavoritos));
List<Emisora> get emisorasCustom => _memoCustom.obtener([
_emisorasCustom,
_ordenListas,
], () => ordenarEmisoras(_emisorasCustom, _ordenListas));
bool get cargandoPopulares => _cargandoPopulares;
String? get error => _errorCarga;
Emisora? get emisoraActual => _emisoraSeleccionada ?? audio.emisoraActual;
Emisora? get emisoraPreferida => _resolverEmisoraPreferida();
String? get emisoraPreferidaUuid => emisoraPreferida?.uuid;
Stream<EstadoReproduccion> get estadoStream => audio.estadoStream;
OrdenEmisoras get ordenListas => _ordenListas;
List<int> get timerSuenoPresetsSegundos => _memoTimerPresets.obtener([
_timerSuenoPresetsSegundos,
], () => List<int>.unmodifiable(_timerSuenoPresetsSegundos));
bool get emisoraActualEsFavorita {
final actual = emisoraActual;
if (actual == null) return false;
return _listaFavoritos.any((e) => e.uuid == actual.uuid);
}
/// Lista principal (home): custom + populares, sin duplicados.
List<Emisora> get emisorasInicio =>
_memoInicio.obtener([_emisorasCustom, _populares], () {
final mapa = <String, Emisora>{};
for (final emisora in _emisorasCustom) {
mapa[emisora.uuid] = emisora;
}
for (final emisora in _populares) {
mapa.putIfAbsent(emisora.uuid, () => emisora);
}
return mapa.values.toList();
});
List<Emisora> get emisorasDisponiblesPreferencia => _memoDisponibles.obtener(
[
_listaFavoritos,
_emisorasCustom,
_populares,
_tendencias,
busqueda.resultados,
busqueda.cercanas,
],
() {
final mapa = <String, Emisora>{};
for (final emisora in _listaFavoritos) {
mapa[emisora.uuid] = emisora;
}
for (final emisora in _emisorasCustom) {
mapa.putIfAbsent(emisora.uuid, () => emisora);
}
for (final emisora in _populares) {
mapa.putIfAbsent(emisora.uuid, () => emisora);
}
for (final emisora in _tendencias) {
mapa.putIfAbsent(emisora.uuid, () => emisora);
}
for (final emisora in busqueda.resultados) {
mapa.putIfAbsent(emisora.uuid, () => emisora);
}
for (final emisora in busqueda.cercanas) {
mapa.putIfAbsent(emisora.uuid, () => emisora);
}
return mapa.values.toList();
},
);
Future<void> inicializar() {
_initFuture ??= _init();
return _initFuture!;
}
Future<void> _init() async {
await grabacion.inicializar();
await ecualizador.cargarPersistido();
await _cargarOrdenListas();
await _cargarEmisoraPreferida();
await _cargarTimerSuenoPresets();
await Future.wait([
cargarPopulares(),
cargarFavoritos(),
cargarGruposFavoritos(),
_cargarEmisorasCustom(),
]);
await _normalizarEmisoraPreferida();
}
/// Escucha el stream de estado del audio y gestiona errores de reproducción.
void _escucharErroresReproduccion() {
_suscripcionEstadoAudio = audio.estadoStream.listen((estado) {
if (estado == EstadoReproduccion.error && timer.activo) {
unawaited(timer.cancelar());
}
if ((estado == EstadoReproduccion.detenido ||
estado == EstadoReproduccion.pausado ||
estado == EstadoReproduccion.error) &&
grabacion.activa) {
unawaited(grabacion.detener());
}
notifyListeners();
});
}
Future<void> cargarPopulares() async {
_cargandoPopulares = true;
_errorCarga = null;
notifyListeners();
try {
final results = await Future.wait([
radio.obtenerPopulares(limit: 30),
radio.obtenerTendencias(limit: 20),
]);
_populares = results[0];
_tendencias = results[1];
} catch (_) {
_errorCarga = _textos.radioApiConnectionError;
} finally {
_cargandoPopulares = false;
notifyListeners();
}
}
Future<void> cargarFavoritos() async {
_listaFavoritos = await favoritos.obtenerTodos();
await _normalizarEmisoraPreferida();
notifyListeners();
}
Future<void> cargarGruposFavoritos() async {
_gruposFavoritos = await favoritos.obtenerGrupos();
notifyListeners();
}
Future<void> crearGrupoFavoritos(String nombre) async {
await favoritos.crearGrupo(nombre);
await cargarGruposFavoritos();
}
Future<void> renombrarGrupoFavoritos(String id, String nombre) async {
await favoritos.renombrarGrupo(id, nombre);
await cargarGruposFavoritos();
}
Future<void> eliminarGrupoFavoritos(String id) async {
await favoritos.eliminarGrupo(id);
await Future.wait([cargarFavoritos(), cargarGruposFavoritos()]);
}
Future<void> asignarGrupoFavorito(String uuid, String grupoId) async {
await favoritos.asignarGrupo(uuid, grupoId);
await cargarFavoritos();
}
Future<void> cambiarEmisoraPreferida(Emisora? emisora) async {
_emisoraPreferidaUuid = emisora?.uuid;
final prefs = await _resolverPrefs();
if (_emisoraPreferidaUuid == null) {
await prefs.remove(_keyEmisoraPreferida);
} else {
await prefs.setString(_keyEmisoraPreferida, _emisoraPreferidaUuid!);
}
notifyListeners();
}
Future<void> reproducirEmisoraPreferida() async {
final preferida = emisoraPreferida;
if (preferida == null) return;
await reproducir(preferida);
}
Future<void> _cargarTimerSuenoPresets() async {
try {
final prefs = await _resolverPrefs();
final raw = prefs.getString(_keyTimerSuenoPresets);
if (raw == null) return;
final decoded = jsonDecode(raw);
if (decoded is! List) return;
final presets =
decoded
.whereType<num>()
.map((n) => n.toInt())
.where((s) => s > 0)
.toSet()
.toList()
..sort();
if (presets.isNotEmpty) {
_timerSuenoPresetsSegundos = presets.take(12).toList();
}
} catch (_) {
_timerSuenoPresetsSegundos = List<int>.from(_timerSuenoPresetsDefecto);
}
}
Future<void> _cargarEmisoraPreferida() async {
final prefs = await _resolverPrefs();
_emisoraPreferidaUuid = prefs.getString(_keyEmisoraPreferida);
}
Future<void> _cargarOrdenListas() async {
final prefs = await _resolverPrefs();
final raw = prefs.getString(_keyOrdenListas);
_ordenListas = switch (raw) {
'nombre' => OrdenEmisoras.nombre,
'calidad' => OrdenEmisoras.calidad,
_ => OrdenEmisoras.calidad,
};
}
Future<void> cambiarOrdenListas(OrdenEmisoras orden) async {
_ordenListas = orden;
final prefs = await _resolverPrefs();
await prefs.setString(_keyOrdenListas, orden.name);
// Search owns its own listeners (S4-R3) but sorts with this preference.
busqueda.notificarCambioOrden();
notifyListeners();
}
Future<void> _normalizarEmisoraPreferida() async {
final preferida = _resolverEmisoraPreferida();
if (preferida?.uuid == _emisoraPreferidaUuid) return;
_emisoraPreferidaUuid = preferida?.uuid;
final prefs = await _resolverPrefs();
if (_emisoraPreferidaUuid == null) {
await prefs.remove(_keyEmisoraPreferida);
} else {
await prefs.setString(_keyEmisoraPreferida, _emisoraPreferidaUuid!);
}
}
Emisora? _resolverEmisoraPreferida() {
final uuid = _emisoraPreferidaUuid;
if (uuid != null) {
for (final emisora in _listaFavoritos) {
if (emisora.uuid == uuid) return emisora;
}
}
if (_listaFavoritos.isNotEmpty) return _listaFavoritos.first;
if (uuid != null) {
for (final emisora in emisorasDisponiblesPreferencia) {
if (emisora.uuid == uuid) return emisora;
}
}
final disponibles = emisorasDisponiblesPreferencia;
return disponibles.isEmpty ? null : disponibles.first;
}
Future<void> reproducir(Emisora emisora) async {
final revision = ++_revisionReproduccion;
if (grabacion.activa) {
await grabacion.detener();
}
_emisoraSeleccionada = emisora;
notifyListeners();
try {
await audio.reproducir(emisora);
if (revision != _revisionReproduccion) return;
unawaited(radio.registrarClick(emisora.uuid));
await ecualizador.aplicarPresetActivo(
ecualizador.presetParaEmisora(emisora.uuid),
);
if (revision != _revisionReproduccion) return;
notifyListeners();
} catch (e) {
if (revision != _revisionReproduccion) return;
if (timer.activo) {
unawaited(timer.cancelar());
}
final mensajeError = e.toString().replaceFirst('Exception: ', '');
_emisoraSeleccionada = audio.emisoraActual;
_errorController.add(
mensajeError.isNotEmpty && mensajeError != 'Exception'
? mensajeError
: _textos.radioCannotPlayStation(
localizedStationName(_textos, emisora.nombre),
),
);
notifyListeners();
}
}
Future<void> detenerReproduccion() async {
if (grabacion.activa) {
await grabacion.detener();
}
await audio.detener();
notifyListeners();
}
Future<void> togglePlay() async {
if (audio.estaSonando && grabacion.activa) {
await grabacion.detener();
}
await audio.togglePlay();
notifyListeners();
}
Future<bool> toggleFavorito(Emisora emisora) async {
final esFav = await favoritos.toggleFavorito(emisora);
if (!esFav) {
await ecualizador.deshabilitarPresetPorEmisora(
emisora.uuid,
notificar: false,
);
}
await cargarFavoritos();
return esFav;
}
Future<bool> esFavorito(String uuid) => favoritos.esFavorito(uuid);
// ── Emisoras personalizadas ───────────────────────────────────────────────
Future<File> _archivoCustom() async {
if (_resolverArchivoCustom != null) {
return _resolverArchivoCustom();
}
final dir = await getApplicationDocumentsDirectory();
return File('${dir.path}/emisoras_custom.json');
}
Future<void> _cargarEmisorasCustom() async {
try {
final archivo = await _archivoCustom();
if (!await archivo.exists()) {
_emisorasCustom = [];
notifyListeners();
return;
}
final data = jsonDecode(await archivo.readAsString()) as List;
_emisorasCustom =
data
.map((e) => Emisora.fromMap(Map<String, dynamic>.from(e as Map)))
.toList();
} catch (_) {
_emisorasCustom = [];
}
notifyListeners();
}
Future<void> _guardarEmisorasCustom() async {
final archivo = await _archivoCustom();
await archivo.writeAsString(
jsonEncode(_emisorasCustom.map((e) => e.toMap()).toList()),
);
}
Future<void> agregarEmisoraCustom(Emisora emisora) async {
// Reassign (not mutate) so identity-memoized views refresh (S4-R5).
_emisorasCustom = [
..._emisorasCustom.where((e) => e.uuid != emisora.uuid),
emisora,
];
await _guardarEmisorasCustom();
notifyListeners();
}
// Compatibilidad con el nombre histórico (typo original).
Future<void> agregarEmitoraCustom(Emisora emisora) =>
agregarEmisoraCustom(emisora);
Future<void> eliminarEmisoraCustom(String uuid) async {
_emisorasCustom = _emisorasCustom.where((e) => e.uuid != uuid).toList();
await _guardarEmisorasCustom();
notifyListeners();
}
// Compatibilidad con el nombre histórico (typo original).
Future<void> eliminarEmitoraCustom(String uuid) =>
eliminarEmisoraCustom(uuid);
// ── Export / Import ───────────────────────────────────────────────────────
static const _keyAlarmasConfig = 'alarmas_musicales_v1';
/// Genera el JSON de toda la configuración (v2 — portabilidad completa).
/// La forma del sobre v2 vive en [ServicioExportImport] (S4-R4).
Future<Map<String, dynamic>> exportarConfig() async {
final favs = await favoritos.obtenerTodos();
final grupos = await favoritos.obtenerGrupos();
final prefs = await _resolverPrefs();
// Alarmas: leemos el JSON crudo de SharedPreferences para no duplicar
// lógica de ServicioAlarmas y evitar inyectar una dependencia nueva.
final alarmasRaw = prefs.getString(_keyAlarmasConfig);
final alarmasData =
alarmasRaw != null
? jsonDecode(alarmasRaw) as Map<String, dynamic>
: null;
return _exportImport.construirExportacion(
gruposFavoritos: grupos,
favoritos: favs,
emisorasCustom: _emisorasCustom,
presetPrincipal: ecualizador.presetPrincipal,
presetsPorEmisora: ecualizador.presetsPorEmisora,
alarmas: alarmasData,
emisoraPreferidaUuid: _emisoraPreferidaUuid,
ordenListas: _ordenListas.name,
timerSuenoPresetsSegundos: _timerSuenoPresetsSegundos,
);
}
/// Exportación lista para compartir como archivo (JSON con indentación).
Future<String> exportarConfigJson() async =>
_exportImport.exportar(await exportarConfig());
/// Parsea un backup JSON; null cuando el contenido no es válido (S4-R4).
Map<String, dynamic>? parsearConfigJson(String raw) =>
_exportImport.importar(raw);
/// Importa configuración desde un JSON exportado previamente.
/// Soporta v1 (sin grupos, sin alarmas) y v2 (portabilidad completa).
Future<void> importarConfig(Map<String, dynamic> data) async {
final version = data['version'] as int? ?? 1;
if (version > 2) throw Exception(_textos.unsupportedConfigVersion);
final prefs = await _resolverPrefs();
// ── Grupos de favoritos (v2) ──────────────────────────────────────────
// Restauramos primero para que al agregar favoritos ya existan los grupos.
if (version >= 2) {
final gruposRaw = data['gruposFavoritos'] as List? ?? [];
for (final raw in gruposRaw) {
final g = GrupoFavoritos.fromMap(Map<String, dynamic>.from(raw as Map));
// Usamos insert directo para preservar id, orden y nombre originales.
await favoritos.restaurarGrupo(g);
}
await cargarGruposFavoritos();
}
// ── Favoritos ─────────────────────────────────────────────────────────
final favRaw = data['favoritos'] as List? ?? [];
for (final raw in favRaw) {
final emisora = Emisora.fromMap(Map<String, dynamic>.from(raw as Map));
await favoritos.agregar(emisora);
}
// ── Emisoras custom ───────────────────────────────────────────────────
final customRaw = data['emisorasCustom'] as List? ?? [];
_emisorasCustom =
customRaw
.map((e) => Emisora.fromMap(Map<String, dynamic>.from(e as Map)))
.toList();
await _guardarEmisorasCustom();
// ── Ecualizador ───────────────────────────────────────────────────────
final principalRaw = data['presetPrincipalEcualizador'];
final presetPrincipal =
principalRaw is Map
? PresetEcualizador.desdeJson(
Map<String, dynamic>.from(principalRaw),
)
: PresetEcualizador.flat;
final presetsRaw = data['presetsEcualizador'] as Map? ?? {};
final presetsPorEmisora = presetsRaw.map<String, PresetEcualizador>(
(uuid, presetJson) => MapEntry(
uuid as String,
PresetEcualizador.desdeJson(
Map<String, dynamic>.from(presetJson as Map),
),
),
);
await ecualizador.importarConfiguracion(
principal: presetPrincipal,
porEmisora: presetsPorEmisora,
);
// ── Alarmas (v2) ──────────────────────────────────────────────────────
if (version >= 2) {
final alarmasData = data['alarmas'];
if (alarmasData is Map<String, dynamic>) {
// Escribimos el bloque JSON tal como estaba en el dispositivo origen.
// ServicioAlarmas lo leerá con su propio fromJson al siguiente acceso.
await prefs.setString(_keyAlarmasConfig, jsonEncode(alarmasData));
}
}
// ── Preferencias de usuario (v2) ──────────────────────────────────────
if (version >= 2) {
final preferidaUuid = data['emisoraPreferidaUuid'] as String?;
_emisoraPreferidaUuid = preferidaUuid;
if (preferidaUuid == null) {
await prefs.remove(_keyEmisoraPreferida);
} else {
await prefs.setString(_keyEmisoraPreferida, preferidaUuid);
}
final ordenRaw = data['ordenListas'] as String?;
_ordenListas = switch (ordenRaw) {
'nombre' => OrdenEmisoras.nombre,
'calidad' => OrdenEmisoras.calidad,
_ => OrdenEmisoras.calidad,
};
await prefs.setString(_keyOrdenListas, _ordenListas.name);
final timerPresetsRaw = data['timerSuenoPresetsSegundos'] as List?;
if (timerPresetsRaw != null) {
await guardarTimerSuenoPresetsSegundos(
timerPresetsRaw.whereType<num>().map((n) => n.toInt()).toList(),
);
}
}
await cargarFavoritos();
notifyListeners();
}
// ── Timer ─────────────────────────────────────────────────────────────────
void iniciarTimer(int minutos) {
timer.iniciar(minutos);
notifyListeners();
}
void iniciarTimerDuracion(Duration duracion) {
timer.iniciarDuracion(duracion);
notifyListeners();
}
void cancelarTimer() {
unawaited(timer.cancelar());
notifyListeners();
}
Future<void> guardarTimerSuenoPresetsSegundos(List<int> segundos) async {
final normalizados =
segundos
.where((s) => s > 0)
.map((s) => s.clamp(1, const Duration(hours: 23).inSeconds))
.toSet()
.toList()
..sort();
_timerSuenoPresetsSegundos =
normalizados.isEmpty
? List<int>.from(_timerSuenoPresetsDefecto)
: normalizados.take(12).toList();
final prefs = await _resolverPrefs();
await prefs.setString(
_keyTimerSuenoPresets,
jsonEncode(_timerSuenoPresetsSegundos),
);
notifyListeners();
}
Future<void> agregarTimerSuenoPreset(Duration duracion) async {
await guardarTimerSuenoPresetsSegundos([
..._timerSuenoPresetsSegundos,
duracion.inSeconds,
]);
}
Future<void> eliminarTimerSuenoPreset(int segundos) async {
await guardarTimerSuenoPresetsSegundos(
_timerSuenoPresetsSegundos.where((s) => s != segundos).toList(),
);
}
Future<void> restaurarTimerSuenoPresets() async {
_timerSuenoPresetsSegundos = List<int>.from(_timerSuenoPresetsDefecto);
final prefs = await _resolverPrefs();
await prefs.remove(_keyTimerSuenoPresets);
notifyListeners();
}
@override
void dispose() {
_suscripcionEstadoAudio?.cancel();
_errorController.close();
ecualizador.dispose();
busqueda.dispose();
grabacion.dispose();
audio.dispose();
timer.dispose();
super.dispose();
}
}