import 'dart:ui' show Locale, PlatformDispatcher; import 'package:flutter/foundation.dart'; import 'package:geocoding/geocoding.dart'; import 'package:geolocator/geolocator.dart'; import '../l10n/gen/app_localizations.dart'; import '../modelos/emisora.dart'; import '../servicios/servicio_radio.dart'; import 'orden_emisoras.dart'; /// Search state extracted from `EstadoRadio` (S4-R3). /// /// Owns the search query/filters, paged results, the nearby-stations lookup /// and every loading flag. Notifies ONLY its own listeners so search activity /// never rebuilds `EstadoRadio` consumers (S4-R5). class EstadoBusqueda extends ChangeNotifier { EstadoBusqueda({ required this.radio, OrdenEmisoras Function()? ordenListas, AppLocalizations Function()? textos, void Function(String mensaje)? alError, }) : _ordenListas = ordenListas ?? (() => OrdenEmisoras.calidad), _textos = textos ?? (() => lookupAppLocalizations(const Locale('es'))), _alError = alError; static const int _tamanoPagina = 30; static const int _maxResultadosEnMemoria = 180; final ServicioRadio radio; /// Current list ordering, owned by EstadoRadio (user preference). final OrdenEmisoras Function() _ordenListas; final AppLocalizations Function() _textos; /// User-visible error sink (EstadoRadio routes it to its snackbar stream). final void Function(String mensaje)? _alError; List _resultados = []; List _cercanas = []; bool _cargando = false; bool _cargandoMas = false; bool _hayMas = true; bool _cargandoCercanas = false; String? _paisCercanoDetectado; String? _errorCercanas; int _offset = 0; String? _ultimoNombre; String? _ultimoPais; String? _ultimoIdioma; String? _ultimoTag; int? _ultimoMinBitrate; final _memoResultados = MemoLista(); final _memoCercanas = MemoLista(); List get resultados => _memoResultados.obtener([ _resultados, _ordenListas(), ], () => ordenarEmisoras(_resultados, _ordenListas())); List get cercanas => _memoCercanas.obtener([ _cercanas, _ordenListas(), ], () => ordenarEmisoras(_cercanas, _ordenListas())); bool get cargando => _cargando; bool get cargandoMas => _cargandoMas; bool get hayMas => _hayMas; bool get cargandoCercanas => _cargandoCercanas; String? get paisCercanoDetectado => _paisCercanoDetectado; String? get errorCercanas => _errorCercanas; /// Re-renders sorted views after the user changes the list ordering /// (called by EstadoRadio, which owns that preference). void notificarCambioOrden() => notifyListeners(); Future buscar({ String? nombre, String? pais, String? idioma, String? tag, int? minBitrate, }) async { _ultimoNombre = nombre; _ultimoPais = pais; _ultimoIdioma = idioma; _ultimoTag = tag; _ultimoMinBitrate = minBitrate; _offset = 0; _hayMas = true; _cargando = true; _resultados = []; notifyListeners(); try { final pagina = await _buscarPaginaFiltrada( nombre: nombre, pais: pais, idioma: idioma, tag: tag, minBitrate: minBitrate, ); _resultados = pagina; } catch (_) { _alError?.call(_textos().radioSearchError); } finally { _cargando = false; notifyListeners(); } } Future cargarMas() async { if (_cargando || _cargandoMas || !_hayMas) return; _cargandoMas = true; notifyListeners(); try { final pagina = await _buscarPaginaFiltrada( nombre: _ultimoNombre, pais: _ultimoPais, idioma: _ultimoIdioma, tag: _ultimoTag, minBitrate: _ultimoMinBitrate, ); final porUuid = { for (final emisora in _resultados) emisora.uuid: emisora, }; for (final emisora in pagina) { porUuid[emisora.uuid] = emisora; } var nuevaLista = porUuid.values.toList(); if (nuevaLista.length > _maxResultadosEnMemoria) { nuevaLista = nuevaLista.sublist( nuevaLista.length - _maxResultadosEnMemoria, ); } _resultados = nuevaLista; // _buscarPaginaFiltrada actualiza offset/hayMas usando páginas crudas. _hayMas = _hayMas && pagina.isNotEmpty; } catch (_) { _alError?.call(_textos().radioLoadMoreStationsError); } finally { _cargandoMas = false; notifyListeners(); } } Future> _buscarPaginaFiltrada({ String? nombre, String? pais, String? idioma, String? tag, int? minBitrate, }) async { final acumuladas = []; var intentos = 0; while (intentos < 4 && acumuladas.isEmpty && _hayMas) { final pagina = await radio.buscar( nombre: nombre, pais: pais, idioma: idioma, tag: tag, limit: _tamanoPagina, offset: _offset, ); _offset += pagina.length; _hayMas = pagina.length == _tamanoPagina; acumuladas.addAll(_filtrarMinBitrate(pagina, minBitrate)); intentos++; } return acumuladas; } List _filtrarMinBitrate(List emisoras, int? minBitrate) { if (minBitrate == null || minBitrate <= 0) return emisoras; return emisoras.where((e) => (e.bitrate ?? 0) >= minBitrate).toList(); } Future cargarEmisorasCercanas() async { _cargandoCercanas = true; _errorCercanas = null; notifyListeners(); try { var pais = PlatformDispatcher.instance.locale.countryCode; final servicioActivo = await Geolocator.isLocationServiceEnabled(); if (servicioActivo) { var permiso = await Geolocator.checkPermission(); if (permiso == LocationPermission.denied) { permiso = await Geolocator.requestPermission(); } if (permiso == LocationPermission.always || permiso == LocationPermission.whileInUse) { final posicion = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.low, timeLimit: Duration(seconds: 8), ), ); final marcas = await placemarkFromCoordinates( posicion.latitude, posicion.longitude, ); if (marcas.isNotEmpty) { pais = marcas.first.isoCountryCode ?? pais; } } } if (pais == null || pais.isEmpty) { throw StateError('nearby-region-not-detected'); } _paisCercanoDetectado = pais; _cercanas = _filtrarMinBitrate( await radio.buscar(pais: pais, limit: 30), _ultimoMinBitrate, ); } catch (_) { _errorCercanas = _textos().radioNearbyStationsError; _cercanas = []; } finally { _cargandoCercanas = false; notifyListeners(); } } }