diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 91532d7..7e6408f 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -4,6 +4,8 @@
+
+
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
+ NSLocationWhenInUseUsageDescription
+ PluriWave usa tu ubicacion aproximada para sugerirte emisoras cercanas.
diff --git a/lib/estado/estado_radio.dart b/lib/estado/estado_radio.dart
index 2de230b..85d3443 100644
--- a/lib/estado/estado_radio.dart
+++ b/lib/estado/estado_radio.dart
@@ -3,6 +3,8 @@ import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
+import 'package:geocoding/geocoding.dart';
+import 'package:geolocator/geolocator.dart';
import 'package:path_provider/path_provider.dart';
import '../modelos/emisora.dart';
@@ -51,6 +53,7 @@ class EstadoRadio extends ChangeNotifier {
List _populares = [];
List _tendencias = [];
List _resultadosBusqueda = [];
+ List _emisorasCercanas = [];
List _listaFavoritos = [];
List _emisorasCustom = [];
@@ -61,16 +64,31 @@ class EstadoRadio extends ChangeNotifier {
bool _cargandoPopulares = false;
bool _cargandoBusqueda = false;
- EstadoReproduccion _estadoReproduccion = EstadoReproduccion.detenido;
+ bool _cargandoMasBusqueda = false;
+ bool _hayMasBusqueda = true;
+ bool _cargandoCercanas = false;
+ String? _paisCercanoDetectado;
+ String? _errorCercanas;
+ int _offsetBusqueda = 0;
+ String? _ultimoNombreBusqueda;
+ String? _ultimoPaisBusqueda;
+ String? _ultimoIdiomaBusqueda;
+ String? _ultimoTagBusqueda;
String? _errorCarga;
List get populares => _populares;
List get tendencias => _tendencias;
List get resultadosBusqueda => _resultadosBusqueda;
+ List get emisorasCercanas => _emisorasCercanas;
List get listaFavoritos => _listaFavoritos;
List get emisorasCustom => _emisorasCustom;
bool get cargandoPopulares => _cargandoPopulares;
bool get cargandoBusqueda => _cargandoBusqueda;
+ bool get cargandoMasBusqueda => _cargandoMasBusqueda;
+ bool get hayMasBusqueda => _hayMasBusqueda;
+ bool get cargandoCercanas => _cargandoCercanas;
+ String? get paisCercanoDetectado => _paisCercanoDetectado;
+ String? get errorCercanas => _errorCercanas;
String? get error => _errorCarga;
Emisora? get emisoraActual => audio.emisoraActual;
Stream get estadoStream => audio.estadoStream;
@@ -119,7 +137,6 @@ class EstadoRadio extends ChangeNotifier {
/// Escucha el stream de estado del audio y gestiona errores de reproducción.
void _escucharErroresReproduccion() {
_suscripcionEstadoAudio = audio.estadoStream.listen((estado) {
- _estadoReproduccion = estado;
if (estado == EstadoReproduccion.error && timer.activo) {
unawaited(timer.cancelar());
}
@@ -167,42 +184,125 @@ class EstadoRadio extends ChangeNotifier {
notifyListeners();
}
+ static const int _tamanoPaginaBusqueda = 30;
+ static const int _maxResultadosBusquedaEnMemoria = 180;
+
Future buscar({
String? nombre,
String? pais,
String? idioma,
String? tag,
}) async {
+ _ultimoNombreBusqueda = nombre;
+ _ultimoPaisBusqueda = pais;
+ _ultimoIdiomaBusqueda = idioma;
+ _ultimoTagBusqueda = tag;
+ _offsetBusqueda = 0;
+ _hayMasBusqueda = true;
_cargandoBusqueda = true;
_resultadosBusqueda = [];
notifyListeners();
try {
- _resultadosBusqueda = await radio.buscar(
+ final pagina = await radio.buscar(
nombre: nombre,
pais: pais,
idioma: idioma,
tag: tag,
+ limit: _tamanoPaginaBusqueda,
+ offset: _offsetBusqueda,
);
+ _resultadosBusqueda = pagina;
+ _offsetBusqueda = pagina.length;
+ _hayMasBusqueda = pagina.length == _tamanoPaginaBusqueda;
} catch (_) {
- _errorController.add('Error en la búsqueda. Comprueba tu conexión.');
+ _errorController.add('Error en la busqueda. Comprueba tu conexion.');
} finally {
_cargandoBusqueda = false;
notifyListeners();
}
}
- Future reproducir(Emisora emisora) async {
- final actual = audio.emisoraActual;
- final mismaEmisoraActiva = actual?.uuid == emisora.uuid;
- final yaEstaConectandoOSonando =
- _estadoReproduccion == EstadoReproduccion.cargando ||
- _estadoReproduccion == EstadoReproduccion.reproduciendo ||
- audio.estaSonando;
- if (mismaEmisoraActiva && yaEstaConectandoOSonando) {
+ Future cargarMasBusqueda() async {
+ if (_cargandoBusqueda || _cargandoMasBusqueda || !_hayMasBusqueda) return;
+ _cargandoMasBusqueda = true;
+ notifyListeners();
+ try {
+ final pagina = await radio.buscar(
+ nombre: _ultimoNombreBusqueda,
+ pais: _ultimoPaisBusqueda,
+ idioma: _ultimoIdiomaBusqueda,
+ tag: _ultimoTagBusqueda,
+ limit: _tamanoPaginaBusqueda,
+ offset: _offsetBusqueda,
+ );
+ final porUuid = {
+ for (final emisora in _resultadosBusqueda) emisora.uuid: emisora,
+ };
+ for (final emisora in pagina) {
+ porUuid[emisora.uuid] = emisora;
+ }
+ var nuevaLista = porUuid.values.toList();
+ if (nuevaLista.length > _maxResultadosBusquedaEnMemoria) {
+ nuevaLista = nuevaLista.sublist(
+ nuevaLista.length - _maxResultadosBusquedaEnMemoria,
+ );
+ }
+ _resultadosBusqueda = nuevaLista;
+ _offsetBusqueda += pagina.length;
+ _hayMasBusqueda = pagina.length == _tamanoPaginaBusqueda;
+ } catch (_) {
+ _errorController.add('No se pudieron cargar mas emisoras.');
+ } finally {
+ _cargandoMasBusqueda = false;
notifyListeners();
- return;
}
+ }
+ 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 Exception('No se pudo detectar tu region');
+ }
+ _paisCercanoDetectado = pais;
+ _emisorasCercanas = await radio.buscar(pais: pais, limit: 30);
+ } catch (_) {
+ _errorCercanas = 'No pudimos detectar emisoras cercanas. Usa filtros por pais.';
+ _emisorasCercanas = [];
+ } finally {
+ _cargandoCercanas = false;
+ notifyListeners();
+ }
+ }
+
+ Future reproducir(Emisora emisora) async {
try {
notifyListeners();
await audio.reproducir(emisora);
diff --git a/lib/pantallas/pantalla_buscar.dart b/lib/pantallas/pantalla_buscar.dart
index 124b011..7a8220a 100644
--- a/lib/pantallas/pantalla_buscar.dart
+++ b/lib/pantallas/pantalla_buscar.dart
@@ -8,7 +8,7 @@ import '../widgets/pluri_icon.dart';
import '../widgets/pluri_premium_widgets.dart';
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
-import 'reproducir_y_abrir.dart';
+import 'reproducir_minimizado.dart';
const _paises = [
('Espana', 'ES'),
@@ -106,6 +106,7 @@ class _PantallaBuscarState extends State {
),
),
),
+ _seccionCercanas(estado),
_seccionFiltro(
'Pais',
_paises.map((p) => (p.$1, p.$2)).toList(),
@@ -129,6 +130,76 @@ class _PantallaBuscarState extends State {
);
}
+ Widget _seccionCercanas(EstadoRadio estado) {
+ final theme = Theme.of(context);
+ final pais = estado.paisCercanoDetectado;
+ return Padding(
+ padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
+ child: PluriGlassSurface(
+ padding: const EdgeInsets.all(12),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Expanded(
+ child: Text(
+ pais == null ? 'Emisoras cercanas' : 'Emisoras cercanas ? $pais',
+ style: theme.textTheme.labelLarge?.copyWith(
+ fontWeight: FontWeight.w900,
+ ),
+ ),
+ ),
+ TextButton.icon(
+ onPressed: estado.cargandoCercanas
+ ? null
+ : estado.cargarEmisorasCercanas,
+ icon: estado.cargandoCercanas
+ ? const SizedBox(
+ width: 16,
+ height: 16,
+ child: CircularProgressIndicator(strokeWidth: 2),
+ )
+ : const Icon(Icons.my_location_rounded, size: 18),
+ label: const Text('Buscar cerca'),
+ ),
+ ],
+ ),
+ if (estado.errorCercanas != null)
+ Text(
+ estado.errorCercanas!,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.error,
+ ),
+ ),
+ if (estado.emisorasCercanas.isNotEmpty) ...[
+ const SizedBox(height: 8),
+ SizedBox(
+ height: 76,
+ child: ListView.separated(
+ scrollDirection: Axis.horizontal,
+ itemCount: estado.emisorasCercanas.length,
+ separatorBuilder: (_, __) => const SizedBox(width: 8),
+ itemBuilder: (context, i) {
+ final emisora = estado.emisorasCercanas[i];
+ return SizedBox(
+ width: 260,
+ child: TarjetaEmisora(
+ emisora: emisora,
+ esCompacta: true,
+ onTap: () => reproducirMinimizado(context, emisora),
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+
Widget _seccionFiltro(
String titulo,
List<(String, String)> opciones,
@@ -197,13 +268,27 @@ class _PantallaBuscarState extends State {
return ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 120),
- itemCount: resultados.length,
+ itemCount: resultados.length + (estado.hayMasBusqueda ? 1 : 0),
separatorBuilder: (_, __) => const SizedBox(height: 10),
- itemBuilder: (context, i) => TarjetaEmisora(
- emisora: resultados[i],
- esCompacta: true,
- onTap: () => reproducirYAbrir(context, resultados[i]),
- ).animate().fadeIn(delay: (i * 20).ms).slideY(begin: 0.08),
+ itemBuilder: (context, i) {
+ if (i >= resultados.length) {
+ if (!estado.cargandoMasBusqueda) {
+ Future.microtask(estado.cargarMasBusqueda);
+ }
+ return const Padding(
+ padding: EdgeInsets.all(18),
+ child: Center(child: CircularProgressIndicator()),
+ );
+ }
+ if (i >= resultados.length - 5 && estado.hayMasBusqueda) {
+ Future.microtask(estado.cargarMasBusqueda);
+ }
+ return TarjetaEmisora(
+ emisora: resultados[i],
+ esCompacta: true,
+ onTap: () => reproducirMinimizado(context, resultados[i]),
+ ).animate().fadeIn(delay: (i.clamp(0, 12) * 20).ms).slideY(begin: 0.08);
+ },
);
}
}
diff --git a/lib/pantallas/pantalla_favoritos.dart b/lib/pantallas/pantalla_favoritos.dart
index 9dd52a3..ef78b43 100644
--- a/lib/pantallas/pantalla_favoritos.dart
+++ b/lib/pantallas/pantalla_favoritos.dart
@@ -7,7 +7,7 @@ import '../widgets/pluri_icon.dart';
import '../widgets/pluri_premium_widgets.dart';
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
-import 'reproducir_y_abrir.dart';
+import 'reproducir_minimizado.dart';
class PantallaFavoritos extends StatelessWidget {
const PantallaFavoritos({super.key});
@@ -87,7 +87,7 @@ class PantallaFavoritos extends StatelessWidget {
key: Key(emisora.uuid),
emisora: emisora,
esCompacta: true,
- onTap: () => reproducirYAbrir(context, emisora),
+ onTap: () => reproducirMinimizado(context, emisora),
),
),
IconButton.filledTonal(
diff --git a/lib/pantallas/pantalla_inicio.dart b/lib/pantallas/pantalla_inicio.dart
index ce6a1f7..f04fe48 100644
--- a/lib/pantallas/pantalla_inicio.dart
+++ b/lib/pantallas/pantalla_inicio.dart
@@ -10,7 +10,7 @@ import '../widgets/pluri_icon.dart';
import '../widgets/pluri_premium_widgets.dart';
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
-import 'reproducir_y_abrir.dart';
+import 'reproducir_minimizado.dart';
/// Pantalla principal: emisoras populares y por género.
class PantallaInicio extends StatefulWidget {
@@ -49,6 +49,7 @@ class _PantallaInicioState extends State {
slivers: [
SliverToBoxAdapter(child: _heroHeader(context, estado)),
const SliverToBoxAdapter(child: _AuroraWaveBanner()),
+ SliverToBoxAdapter(child: _seccionCercanas(estado, theme)),
SliverToBoxAdapter(child: _seccionTendencias(estado, theme)),
SliverToBoxAdapter(child: _chipGeneros(context, theme)),
if (estado.error != null)
@@ -87,6 +88,75 @@ class _PantallaInicioState extends State {
);
}
+ Widget _seccionCercanas(EstadoRadio estado, ThemeData theme) {
+ final pais = estado.paisCercanoDetectado;
+ return Padding(
+ padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
+ child: PluriGlassSurface(
+ padding: const EdgeInsets.all(12),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Expanded(
+ child: Text(
+ pais == null ? 'Cerca de vos' : 'Cerca de vos ? $pais',
+ style: theme.textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.w900,
+ ),
+ ),
+ ),
+ TextButton.icon(
+ onPressed: estado.cargandoCercanas
+ ? null
+ : estado.cargarEmisorasCercanas,
+ icon: estado.cargandoCercanas
+ ? const SizedBox(
+ width: 16,
+ height: 16,
+ child: CircularProgressIndicator(strokeWidth: 2),
+ )
+ : const Icon(Icons.my_location_rounded, size: 18),
+ label: const Text('Detectar'),
+ ),
+ ],
+ ),
+ if (estado.errorCercanas != null)
+ Text(
+ estado.errorCercanas!,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.error,
+ ),
+ ),
+ if (estado.emisorasCercanas.isNotEmpty) ...[
+ const SizedBox(height: 8),
+ SizedBox(
+ height: 76,
+ child: ListView.separated(
+ scrollDirection: Axis.horizontal,
+ itemCount: estado.emisorasCercanas.length,
+ separatorBuilder: (_, __) => const SizedBox(width: 8),
+ itemBuilder: (context, i) {
+ final emisora = estado.emisorasCercanas[i];
+ return SizedBox(
+ width: 260,
+ child: TarjetaEmisora(
+ emisora: emisora,
+ esCompacta: true,
+ onTap: () => reproducirMinimizado(context, emisora),
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+
Widget _seccionTendencias(EstadoRadio estado, ThemeData theme) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
@@ -120,7 +190,7 @@ class _PantallaInicioState extends State {
),
label: Text(e.nombre, maxLines: 1),
onPressed:
- () => reproducirYAbrir(context, e),
+ () => reproducirMinimizado(context, e),
).animate().fadeIn(delay: (i * 50).ms);
},
),
@@ -227,7 +297,7 @@ class _PantallaInicioState extends State {
delegate: SliverChildBuilderDelegate(
(context, i) => TarjetaEmisora(
emisora: emisoras[i],
- onTap: () => reproducirYAbrir(context, emisoras[i]),
+ onTap: () => reproducirMinimizado(context, emisoras[i]),
).animate().fadeIn(delay: (i * 30).ms).slideY(begin: 0.1),
childCount: emisoras.length,
),
diff --git a/lib/pantallas/reproducir_y_abrir.dart b/lib/pantallas/reproducir_minimizado.dart
similarity index 57%
rename from lib/pantallas/reproducir_y_abrir.dart
rename to lib/pantallas/reproducir_minimizado.dart
index 1935544..579f6cd 100644
--- a/lib/pantallas/reproducir_y_abrir.dart
+++ b/lib/pantallas/reproducir_minimizado.dart
@@ -5,11 +5,8 @@ import 'package:provider/provider.dart';
import '../estado/estado_radio.dart';
import '../modelos/emisora.dart';
-import 'pantalla_reproductor.dart';
-Future reproducirYAbrir(BuildContext context, Emisora emisora) async {
+void reproducirMinimizado(BuildContext context, Emisora emisora) {
final estado = context.read();
unawaited(estado.reproducir(emisora));
- if (!context.mounted) return;
- await PantallaReproductor.abrir(context, emisora);
}
diff --git a/lib/servicios/servicio_audio.dart b/lib/servicios/servicio_audio.dart
index 961a528..56a80b0 100644
--- a/lib/servicios/servicio_audio.dart
+++ b/lib/servicios/servicio_audio.dart
@@ -106,6 +106,7 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
PresetEcualizador _presetActual = PresetEcualizador.flat;
PresetEcualizador get presetActual => _presetActual;
+ Future _colaReproduccion = Future.value();
PluriWaveAudioHandler() {
_setupStreams();
@@ -213,12 +214,20 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
}
@override
- Future playMediaItem(MediaItem mediaItem) async {
+ Future playMediaItem(MediaItem mediaItem) {
+ _colaReproduccion = _colaReproduccion
+ .catchError((_) {})
+ .then((_) => _playMediaItemSerializado(mediaItem));
+ return _colaReproduccion;
+ }
+
+ Future _playMediaItemSerializado(MediaItem mediaItem) async {
this.mediaItem.add(mediaItem);
emisoraActual = _emisoraDesdeMediaItem(mediaItem);
playbackState.add(playbackState.value.copyWith(
processingState: AudioProcessingState.loading,
playing: false,
+ errorMessage: null,
));
try {
await _player.stop();
diff --git a/lib/servicios/servicio_radio.dart b/lib/servicios/servicio_radio.dart
index 425871e..1445f7e 100644
--- a/lib/servicios/servicio_radio.dart
+++ b/lib/servicios/servicio_radio.dart
@@ -105,8 +105,13 @@ class ServicioRadio {
}
/// Emisoras más votadas globalmente.
- Future> obtenerPopulares({int limit = 30}) async {
- return _get('/json/stations/topvote/$limit', {});
+ Future> obtenerPopulares({int limit = 30, int offset = 0}) async {
+ return _get('/json/stations/search', {
+ 'limit': limit.toString(),
+ 'offset': offset.toString(),
+ 'order': 'votes',
+ 'reverse': 'true',
+ });
}
/// Emisoras más escuchadas (por clicks) globalmente.
@@ -115,37 +120,41 @@ class ServicioRadio {
}
/// Buscar por nombre de emisora.
- Future> buscarPorNombre(String query, {int limit = 30}) async {
+ Future> buscarPorNombre(String query, {int limit = 30, int offset = 0}) async {
return _get('/json/stations/search', {
'name': query,
'limit': limit.toString(),
+ 'offset': offset.toString(),
'order': 'votes',
'reverse': 'true',
});
}
/// Buscar por código de país (ISO 3166-1 alpha-2, e.g. 'ES', 'US').
- Future> buscarPorPais(String codigoPais, {int limit = 50}) async {
+ Future> buscarPorPais(String codigoPais, {int limit = 50, int offset = 0}) async {
return _get('/json/stations/bycountrycodeexact/$codigoPais', {
'limit': limit.toString(),
+ 'offset': offset.toString(),
'order': 'votes',
'reverse': 'true',
});
}
/// Buscar por idioma (e.g. 'spanish', 'english').
- Future> buscarPorIdioma(String idioma, {int limit = 30}) async {
+ Future> buscarPorIdioma(String idioma, {int limit = 30, int offset = 0}) async {
return _get('/json/stations/bylanguageexact/$idioma', {
'limit': limit.toString(),
+ 'offset': offset.toString(),
'order': 'votes',
'reverse': 'true',
});
}
/// Buscar por tag/género (e.g. 'rock', 'jazz', 'pop').
- Future> buscarPorTag(String tag, {int limit = 30}) async {
+ Future> buscarPorTag(String tag, {int limit = 30, int offset = 0}) async {
return _get('/json/stations/bytagexact/$tag', {
'limit': limit.toString(),
+ 'offset': offset.toString(),
'order': 'votes',
'reverse': 'true',
});
@@ -158,6 +167,7 @@ class ServicioRadio {
String? idioma,
String? tag,
int limit = 30,
+ int offset = 0,
}) async {
return _get('/json/stations/search', {
if (nombre != null && nombre.isNotEmpty) 'name': nombre,
@@ -165,6 +175,7 @@ class ServicioRadio {
if (idioma != null && idioma.isNotEmpty) 'language': idioma,
if (tag != null && tag.isNotEmpty) 'tag': tag,
'limit': limit.toString(),
+ 'offset': offset.toString(),
'order': 'votes',
'reverse': 'true',
});
diff --git a/pubspec.lock b/pubspec.lock
index c2466b8..3c3b953 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -229,6 +229,86 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ geocoding:
+ dependency: "direct main"
+ description:
+ name: geocoding
+ sha256: d580c801cba9386b4fac5047c4c785a4e19554f46be42f4f5e5b7deacd088a66
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.0"
+ geocoding_android:
+ dependency: transitive
+ description:
+ name: geocoding_android
+ sha256: "1b13eca79b11c497c434678fed109c2be020b158cec7512c848c102bc7232603"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.3.1"
+ geocoding_ios:
+ dependency: transitive
+ description:
+ name: geocoding_ios
+ sha256: "18ab1c8369e2b0dcb3a8ccc907319334f35ee8cf4cfef4d9c8e23b13c65cb825"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.0"
+ geocoding_platform_interface:
+ dependency: transitive
+ description:
+ name: geocoding_platform_interface
+ sha256: "8c2c8226e5c276594c2e18bfe88b19110ed770aeb7c1ab50ede570be8b92229b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.0"
+ geolocator:
+ dependency: "direct main"
+ description:
+ name: geolocator
+ sha256: f62bcd90459e63210bbf9c35deb6a51c521f992a78de19a1fe5c11704f9530e2
+ url: "https://pub.dev"
+ source: hosted
+ version: "13.0.4"
+ geolocator_android:
+ dependency: transitive
+ description:
+ name: geolocator_android
+ sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.6.2"
+ geolocator_apple:
+ dependency: transitive
+ description:
+ name: geolocator_apple
+ sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.13"
+ geolocator_platform_interface:
+ dependency: transitive
+ description:
+ name: geolocator_platform_interface
+ sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.2.6"
+ geolocator_web:
+ dependency: transitive
+ description:
+ name: geolocator_web
+ sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.1.3"
+ geolocator_windows:
+ dependency: transitive
+ description:
+ name: geolocator_windows
+ sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.5"
glob:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index ba5810d..ee1957e 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -41,6 +41,8 @@ dependencies:
file_picker: ^8.1.7
uuid: ^4.5.1
url_launcher: ^6.3.1
+ geolocator: ^13.0.4
+ geocoding: ^3.0.0
# Ads (activar cuando tengamos Ad Unit IDs)
# google_mobile_ads: ^5.3.0
diff --git a/test/estado/estado_radio_test.dart b/test/estado/estado_radio_test.dart
index 31ffd41..4637012 100644
--- a/test/estado/estado_radio_test.dart
+++ b/test/estado/estado_radio_test.dart
@@ -197,7 +197,7 @@ void main() {
expect(notificaciones, greaterThan(antes));
});
- test('reproducir la misma emisora mientras suena no reinicia el stream', () async {
+ test('reproducir la misma emisora mientras suena fuerza recarga del stream', () async {
final audio = FakeServicioAudio();
final emisora = emisoraDemo(uuid: 'same-1', nombre: 'Misma');
final estado = EstadoRadio(
@@ -213,7 +213,7 @@ void main() {
await estado.reproducir(emisora);
await estado.reproducir(emisora);
- expect(audio.emisorasReproducidas, hasLength(1));
+ expect(audio.emisorasReproducidas, hasLength(2));
});
test('reordenar favoritos reindexa de forma determinística', () async {
@@ -229,6 +229,35 @@ void main() {
expect(lista.map((e) => e.orden).toList(), equals([0, 1, 2]));
});
+
+
+ test('cargarMasBusqueda pagina resultados y acota memoria', () async {
+ final emisoras = List.generate(
+ 70,
+ (i) => emisoraDemo(uuid: 'page-$i', nombre: 'Page $i'),
+ );
+ final estado = EstadoRadio(
+ audio: FakeServicioAudio(),
+ favoritos: FakeServicioFavoritos(),
+ radio: FakeServicioRadio(busqueda: emisoras),
+ servicioEcualizador: FakeServicioEcualizador(),
+ resolverArchivoCustom: _archivoCustomVacio,
+ iniciarAutomaticamente: false,
+ );
+
+ await estado.inicializar();
+ await estado.buscar(nombre: 'page');
+ expect(estado.resultadosBusqueda, hasLength(30));
+ expect(estado.hayMasBusqueda, isTrue);
+
+ await estado.cargarMasBusqueda();
+ expect(estado.resultadosBusqueda, hasLength(60));
+
+ await estado.cargarMasBusqueda();
+ expect(estado.resultadosBusqueda, hasLength(70));
+ expect(estado.hayMasBusqueda, isFalse);
+ });
+
test('toggleFavorito refresca lista global y evita estado stale', () async {
final favoritos = FakeServicioFavoritos();
final emisora = emisoraDemo(uuid: 'fav-sync', nombre: 'Sync');
diff --git a/test/helpers/fakes.dart b/test/helpers/fakes.dart
index bb3dd03..bee3248 100644
--- a/test/helpers/fakes.dart
+++ b/test/helpers/fakes.dart
@@ -147,7 +147,7 @@ class FakeServicioRadio extends ServicioRadio {
error is Exception ? error : Exception(error.toString());
@override
- Future> obtenerPopulares({int limit = 30}) async {
+ Future> obtenerPopulares({int limit = 30, int offset = 0}) async {
final llamada = obtenerPopularesCalls++;
if (llamada < _erroresPopularesPorLlamada.length) {
throw _normalizarError(_erroresPopularesPorLlamada[llamada]);
@@ -177,8 +177,9 @@ class FakeServicioRadio extends ServicioRadio {
String? idioma,
String? tag,
int limit = 30,
+ int offset = 0,
}) async {
- return _busqueda.take(limit).toList();
+ return _busqueda.skip(offset).take(limit).toList();
}
@override