import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import '../modelos/emisora.dart'; import '../modelos/preset_ecualizador.dart'; import '../servicios/servicio_audio.dart'; import '../servicios/servicio_ecualizador.dart'; import '../servicios/servicio_favoritos.dart'; import '../servicios/servicio_radio.dart'; import '../servicios/servicio_timer.dart'; /// Estado global de la app con ChangeNotifier (Provider). class EstadoRadio extends ChangeNotifier { EstadoRadio({ ServicioAudio? audio, ServicioFavoritos? favoritos, ServicioRadio? radio, ServicioEcualizador? servicioEcualizador, Future Function()? resolverArchivoCustom, bool iniciarAutomaticamente = true, }) : audio = audio ?? ServicioAudio(), favoritos = favoritos ?? ServicioFavoritos(), radio = radio ?? ServicioRadio(), servicioEcualizador = servicioEcualizador ?? ServicioEcualizador(), _resolverArchivoCustom = resolverArchivoCustom { timer = ServicioTimer(this.audio); _escucharErroresReproduccion(); if (iniciarAutomaticamente) { _initFuture = _init(); } } final ServicioAudio audio; final ServicioFavoritos favoritos; final ServicioRadio radio; final ServicioEcualizador servicioEcualizador; final Future Function()? _resolverArchivoCustom; late final ServicioTimer timer; StreamSubscription? _suscripcionEstadoAudio; Future? _initFuture; // Errores de reproducción → SnackBar. final _errorController = StreamController.broadcast(); Stream get errorStream => _errorController.stream; List _populares = []; List _tendencias = []; List _resultadosBusqueda = []; List _listaFavoritos = []; List _emisorasCustom = []; // Presets EQ guardados por uuid de emisora. final Map _presetsEmisoraMap = {}; PresetEcualizador _presetPrincipal = PresetEcualizador.flat; PresetEcualizador _presetActual = PresetEcualizador.flat; bool _cargandoPopulares = false; bool _cargandoBusqueda = false; String? _errorCarga; List get populares => _populares; List get tendencias => _tendencias; List get resultadosBusqueda => _resultadosBusqueda; List get listaFavoritos => _listaFavoritos; List get emisorasCustom => _emisorasCustom; bool get cargandoPopulares => _cargandoPopulares; bool get cargandoBusqueda => _cargandoBusqueda; String? get error => _errorCarga; Emisora? get emisoraActual => audio.emisoraActual; Stream get estadoStream => audio.estadoStream; PresetEcualizador get presetEcualizador => _presetActual; PresetEcualizador get presetPrincipalEcualizador => _presetPrincipal; bool get ecualizadorDisponible => audio.ecualizadorDisponible; bool get emisoraActualEsFavorita { final actual = emisoraActual; if (actual == null) return false; return _listaFavoritos.any((e) => e.uuid == actual.uuid); } bool get emisoraActualTienePresetPropio { final actual = emisoraActual; if (actual == null) return false; return tienePresetEcualizadorPorEmisora(actual.uuid); } /// Lista principal (home): custom + populares, sin duplicados. List get emisorasInicio { final mapa = {}; for (final emisora in _emisorasCustom) { mapa[emisora.uuid] = emisora; } for (final emisora in _populares) { mapa.putIfAbsent(emisora.uuid, () => emisora); } return mapa.values.toList(); } Future inicializar() { _initFuture ??= _init(); return _initFuture!; } Future _init() async { await _cargarEcualizadorPersistido(); await Future.wait([ cargarPopulares(), cargarFavoritos(), _cargarEmisorasCustom(), ]); } /// Escucha el stream de estado del audio y gestiona errores de reproducción. void _escucharErroresReproduccion() { _suscripcionEstadoAudio = audio.estadoStream.listen((estado) { if (estado == EstadoReproduccion.error) { if (timer.activo) { timer.cancelar(); } notifyListeners(); } }); } Future _cargarEcualizadorPersistido() async { try { final config = await servicioEcualizador.cargar(); _presetPrincipal = config.principal; _presetActual = config.principal; _presetsEmisoraMap ..clear() ..addAll(config.porEmisora); await audio.aplicarPreset(_presetPrincipal); } catch (_) { _presetPrincipal = PresetEcualizador.flat; _presetActual = PresetEcualizador.flat; _presetsEmisoraMap.clear(); } } Future 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 = 'Sin conexión a la API de radio'; } finally { _cargandoPopulares = false; notifyListeners(); } } Future cargarFavoritos() async { _listaFavoritos = await favoritos.obtenerTodos(); notifyListeners(); } Future buscar({ String? nombre, String? pais, String? idioma, String? tag, }) async { _cargandoBusqueda = true; _resultadosBusqueda = []; notifyListeners(); try { _resultadosBusqueda = await radio.buscar( nombre: nombre, pais: pais, idioma: idioma, tag: tag, ); } catch (_) { _errorController.add('Error en la búsqueda. Comprueba tu conexión.'); } finally { _cargandoBusqueda = false; notifyListeners(); } } Future reproducir(Emisora emisora) async { try { await audio.reproducir(emisora); unawaited(radio.registrarClick(emisora.uuid)); await _aplicarPresetActivo(_presetParaEmisora(emisora.uuid)); notifyListeners(); } catch (e) { if (timer.activo) { timer.cancelar(); } final mensajeError = e.toString().replaceFirst('Exception: ', ''); _errorController.add( mensajeError.isNotEmpty && mensajeError != 'Exception' ? mensajeError : 'No se puede reproducir "${emisora.nombre}"', ); notifyListeners(); } } Future togglePlay() async { await audio.togglePlay(); notifyListeners(); } Future toggleFavorito(Emisora emisora) async { final esFav = await favoritos.toggleFavorito(emisora); if (!esFav) { await deshabilitarPresetEcualizadorPorEmisora(emisora.uuid, notificar: false); } await cargarFavoritos(); notifyListeners(); return esFav; } Future esFavorito(String uuid) => favoritos.esFavorito(uuid); // ── Ecualizador ─────────────────────────────────────────────────────────── bool tienePresetEcualizadorPorEmisora(String uuid) => _presetsEmisoraMap.containsKey(uuid); PresetEcualizador? presetEcualizadorPorEmisora(String uuid) => _presetsEmisoraMap[uuid]; PresetEcualizador _presetParaEmisora(String uuid) => _presetsEmisoraMap[uuid] ?? _presetPrincipal; Future _aplicarPresetActivo(PresetEcualizador preset) async { _presetActual = preset; await audio.aplicarPreset(preset); } Future cambiarPresetPrincipalEcualizador( PresetEcualizador preset, { bool notificar = true, }) async { _presetPrincipal = preset; await servicioEcualizador.guardarPrincipal(preset); final actual = emisoraActual; final puedeAplicarAhora = actual == null || !_presetsEmisoraMap.containsKey(actual.uuid); if (puedeAplicarAhora) { await _aplicarPresetActivo(preset); } if (notificar) notifyListeners(); } Future guardarPresetEcualizadorPorEmisora( String uuid, PresetEcualizador preset, { bool notificar = true, }) async { _presetsEmisoraMap[uuid] = preset; await servicioEcualizador.guardarPorEmisora(uuid, preset); if (emisoraActual?.uuid == uuid) { await _aplicarPresetActivo(preset); } if (notificar) notifyListeners(); } Future habilitarPresetEcualizadorPorEmisora( String uuid, { PresetEcualizador? base, bool notificar = true, }) async { final presetBase = base ?? _presetsEmisoraMap[uuid] ?? _presetPrincipal; await guardarPresetEcualizadorPorEmisora( uuid, presetBase, notificar: notificar, ); } Future deshabilitarPresetEcualizadorPorEmisora( String uuid, { bool notificar = true, }) async { _presetsEmisoraMap.remove(uuid); await servicioEcualizador.eliminarPorEmisora(uuid); if (emisoraActual?.uuid == uuid) { await _aplicarPresetActivo(_presetPrincipal); } if (notificar) notifyListeners(); } Future cambiarModoEcualizadorEmisoraActual({required bool usarPropio}) async { final actual = emisoraActual; if (actual == null) return; if (usarPropio) { await habilitarPresetEcualizadorPorEmisora(actual.uuid); } else { await deshabilitarPresetEcualizadorPorEmisora(actual.uuid); } } Future cambiarPresetEcualizador( PresetEcualizador preset, { bool guardarPorEmisora = true, }) async { final actual = emisoraActual; final usarPresetPropio = guardarPorEmisora && actual != null && _presetsEmisoraMap.containsKey(actual.uuid); if (usarPresetPropio) { await guardarPresetEcualizadorPorEmisora(actual.uuid, preset); return; } await cambiarPresetPrincipalEcualizador(preset); } Future cambiarBandaEcualizador(int index, double db) async { final bandas = List.from(_presetActual.bandas); if (index < 0 || index >= bandas.length) return; bandas[index] = db; final modificado = PresetEcualizador( nombre: 'Personalizado', bandas: bandas, ); await cambiarPresetEcualizador(modificado); } // ── Emisoras personalizadas ─────────────────────────────────────────────── Future _archivoCustom() async { if (_resolverArchivoCustom != null) { return _resolverArchivoCustom!(); } final dir = await getApplicationDocumentsDirectory(); return File('${dir.path}/emisoras_custom.json'); } Future _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.from(e as Map))) .toList(); } catch (_) { _emisorasCustom = []; } notifyListeners(); } Future _guardarEmisorasCustom() async { final archivo = await _archivoCustom(); await archivo.writeAsString( jsonEncode(_emisorasCustom.map((e) => e.toMap()).toList()), ); } Future agregarEmisoraCustom(Emisora emisora) async { _emisorasCustom.removeWhere((e) => e.uuid == emisora.uuid); _emisorasCustom.add(emisora); await _guardarEmisorasCustom(); notifyListeners(); } // Compatibilidad con el nombre histórico (typo original). Future agregarEmitoraCustom(Emisora emisora) => agregarEmisoraCustom(emisora); Future eliminarEmisoraCustom(String uuid) async { _emisorasCustom.removeWhere((e) => e.uuid == uuid); await _guardarEmisorasCustom(); notifyListeners(); } // Compatibilidad con el nombre histórico (typo original). Future eliminarEmitoraCustom(String uuid) => eliminarEmisoraCustom(uuid); // ── Export / Import ─────────────────────────────────────────────────────── /// Genera el JSON de toda la configuración. Future> exportarConfig() async { final favs = await favoritos.obtenerTodos(); return { 'version': 1, 'exportedAt': DateTime.now().toIso8601String(), 'favoritos': favs.map((e) => e.toMap()).toList(), 'emisorasCustom': _emisorasCustom.map((e) => e.toMap()).toList(), 'presetPrincipalEcualizador': _presetPrincipal.toJson(), 'presetsEcualizador': _presetsEmisoraMap.map( (uuid, preset) => MapEntry(uuid, preset.toJson()), ), }; } /// Importa configuración desde un JSON exportado previamente. Future importarConfig(Map data) async { final version = data['version'] as int? ?? 1; if (version != 1) throw Exception('Versión de configuración no compatible'); final favRaw = data['favoritos'] as List? ?? []; for (final raw in favRaw) { final emisora = Emisora.fromMap(Map.from(raw as Map)); await favoritos.agregar(emisora); } final customRaw = data['emisorasCustom'] as List? ?? []; _emisorasCustom = customRaw .map((e) => Emisora.fromMap(Map.from(e as Map))) .toList(); await _guardarEmisorasCustom(); final principalRaw = data['presetPrincipalEcualizador']; if (principalRaw is Map) { _presetPrincipal = PresetEcualizador.desdeJson( Map.from(principalRaw), ); } else { _presetPrincipal = PresetEcualizador.flat; } final presetsRaw = data['presetsEcualizador'] as Map? ?? {}; _presetsEmisoraMap ..clear() ..addAll( presetsRaw.map( (uuid, presetJson) => MapEntry( uuid as String, PresetEcualizador.desdeJson( Map.from(presetJson as Map), ), ), ), ); await servicioEcualizador.guardarConfiguracion( ConfiguracionEcualizador( principal: _presetPrincipal, porEmisora: _presetsEmisoraMap, ), ); final actual = emisoraActual; final presetActivo = actual == null ? _presetPrincipal : _presetParaEmisora(actual.uuid); await _aplicarPresetActivo(presetActivo); await cargarFavoritos(); notifyListeners(); } // ── Timer ───────────────────────────────────────────────────────────────── void iniciarTimer(int minutos) { timer.iniciar(minutos); notifyListeners(); } void cancelarTimer() { timer.cancelar(); notifyListeners(); } @override void dispose() { _suscripcionEstadoAudio?.cancel(); _errorController.close(); audio.dispose(); timer.dispose(); super.dispose(); } }