diff --git a/android/app/src/main/kotlin/es/freetimelab/pluriwave/MainActivity.kt b/android/app/src/main/kotlin/es/freetimelab/pluriwave/MainActivity.kt index 6fbac41..3c70ffc 100644 --- a/android/app/src/main/kotlin/es/freetimelab/pluriwave/MainActivity.kt +++ b/android/app/src/main/kotlin/es/freetimelab/pluriwave/MainActivity.kt @@ -1,5 +1,5 @@ package es.freetimelab.pluriwave -import io.flutter.embedding.android.FlutterActivity +import com.ryanheise.audioservice.AudioServiceActivity -class MainActivity : FlutterActivity() +class MainActivity : AudioServiceActivity() diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 1c007ec..76091e4 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index 1c007ec..76091e4 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 3da2c83..6a0086a 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index 3da2c83..6a0086a 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 63cdc63..7c11560 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png index 63cdc63..7c11560 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 7e3200a..ff2ddcf 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index 7e3200a..ff2ddcf 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 27b1878..11dfdfd 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index 27b1878..11dfdfd 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/lib/app.dart b/lib/app.dart index 41ac6a7..287d045 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -37,7 +37,7 @@ class PluriWaveApp extends StatelessWidget { textTheme: GoogleFonts.interTextTheme( ThemeData(brightness: brightness).textTheme, ), - cardTheme: CardTheme( + cardTheme: CardThemeData( elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), color: colorScheme.surfaceContainerLow, diff --git a/lib/estado/estado_radio.dart b/lib/estado/estado_radio.dart index 4f3748b..bed6da1 100644 --- a/lib/estado/estado_radio.dart +++ b/lib/estado/estado_radio.dart @@ -1,22 +1,24 @@ 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_favoritos.dart'; import '../servicios/servicio_radio.dart'; import '../servicios/servicio_timer.dart'; /// Estado global de la app con ChangeNotifier (Provider). -/// -/// Errores de reproducción se emiten por [errorStream] para mostrar como -/// SnackBar — no bloquean la UI. class EstadoRadio extends ChangeNotifier { final ServicioAudio audio = ServicioAudio(); final ServicioFavoritos favoritos = ServicioFavoritos(); final ServicioRadio radio = ServicioRadio(); late final ServicioTimer timer; - // Errores de reproducción → SnackBar en el UI + // Errores de reproducción → SnackBar final _errorController = StreamController.broadcast(); Stream get errorStream => _errorController.stream; @@ -24,10 +26,15 @@ class EstadoRadio extends ChangeNotifier { List _tendencias = []; List _resultadosBusqueda = []; List _listafavoritos = []; + List _emisorasCustom = []; + + // Presets EQ guardados por uuid de emisora + final Map _presetsEmisoraMap = {}; + PresetEcualizador _presetActual = PresetEcualizador.flat; bool _cargandoPopulares = false; bool _cargandoBusqueda = false; - String? _errorCarga; // solo para errores de carga de lista (banner estático) + String? _errorCarga; EstadoRadio() { timer = ServicioTimer(audio); @@ -38,16 +45,20 @@ class EstadoRadio extends ChangeNotifier { 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; + bool get ecualizadorDisponible => audio.ecualizadorDisponible; Future _init() async { await Future.wait([ cargarPopulares(), cargarFavoritos(), + _cargarEmisoresCustom(), ]); } @@ -92,7 +103,6 @@ class EstadoRadio extends ChangeNotifier { tag: tag, ); } catch (e) { - // Error de búsqueda → toast, no bloquear pantalla _errorController.add('Error en la búsqueda. Comprueba tu conexión.'); } finally { _cargandoBusqueda = false; @@ -104,9 +114,11 @@ class EstadoRadio extends ChangeNotifier { try { await audio.reproducir(emisora); radio.registrarClick(emisora.uuid); // fire & forget + // Restaurar preset del ecualizador de esta emisora + final preset = _presetsEmisoraMap[emisora.uuid] ?? PresetEcualizador.flat; + await cambiarPresetEcualizador(preset, guardPorEmisora: false); notifyListeners(); } catch (e) { - // Error de reproducción → SnackBar, no pintar en medio de la UI _errorController.add('No se puede reproducir "${emisora.nombre}"'); } } @@ -119,11 +131,125 @@ class EstadoRadio extends ChangeNotifier { Future toggleFavorito(Emisora emisora) async { final esFav = await favoritos.toggleFavorito(emisora); await cargarFavoritos(); + notifyListeners(); return esFav; } Future esFavorito(String uuid) => favoritos.esFavorito(uuid); + // ── Ecualizador ────────────────────────────────────────────────────────── + + Future cambiarPresetEcualizador( + PresetEcualizador preset, { + bool guardPorEmisora = true, + }) async { + _presetActual = preset; + await audio.aplicarPreset(preset); + if (guardPorEmisora && emisoraActual != null) { + _presetsEmisoraMap[emisoraActual!.uuid] = preset; + } + notifyListeners(); + } + + Future cambiarBandaEcualizador(int index, double db) async { + final bandas = List.from(_presetActual.bandas); + if (index >= 0 && index < bandas.length) bandas[index] = db; + _presetActual = PresetEcualizador(nombre: 'Personalizado', bandas: bandas); + await audio.setBanda(index, db); + if (emisoraActual != null) { + _presetsEmisoraMap[emisoraActual!.uuid] = _presetActual; + } + notifyListeners(); + } + + // ── Emisoras personalizadas ─────────────────────────────────────────────── + + Future _archivoCustom() async { + final dir = await getApplicationDocumentsDirectory(); + return File('${dir.path}/emisoras_custom.json'); + } + + Future _cargarEmisoresCustom() async { + try { + final f = await _archivoCustom(); + if (!await f.exists()) return; + final data = jsonDecode(await f.readAsString()) as List; + _emisorasCustom = data + .map((e) => Emisora.fromMap(Map.from(e as Map))) + .toList(); + } catch (_) { + _emisorasCustom = []; + } + notifyListeners(); + } + + Future _guardarEmisoresCustom() async { + final f = await _archivoCustom(); + await f.writeAsString(jsonEncode(_emisorasCustom.map((e) => e.toMap()).toList())); + } + + Future agregarEmitoraCustom(Emisora emisora) async { + _emisorasCustom.removeWhere((e) => e.uuid == emisora.uuid); + _emisorasCustom.add(emisora); + await _guardarEmisoresCustom(); + notifyListeners(); + } + + Future eliminarEmitoraCustom(String uuid) async { + _emisorasCustom.removeWhere((e) => e.uuid == uuid); + await _guardarEmisoresCustom(); + notifyListeners(); + } + + // ── 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(), + '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'); + + // Importar favoritos + final favRaw = data['favoritos'] as List? ?? []; + for (final raw in favRaw) { + final emisora = Emisora.fromMap(Map.from(raw as Map)); + await favoritos.agregar(emisora); + } + + // Importar emisoras custom + final customRaw = data['emisorasCustom'] as List? ?? []; + _emisorasCustom = customRaw + .map((e) => Emisora.fromMap(Map.from(e as Map))) + .toList(); + await _guardarEmisoresCustom(); + + // Importar presets EQ + final presetsRaw = data['presetsEcualizador'] as Map? ?? {}; + _presetsEmisoraMap.clear(); + presetsRaw.forEach((uuid, presetJson) { + _presetsEmisoraMap[uuid as String] = + PresetEcualizador.desdeJson(Map.from(presetJson as Map)); + }); + + await cargarFavoritos(); + notifyListeners(); + } + + // ── Timer ───────────────────────────────────────────────────────────────── + void iniciarTimer(int minutos) { timer.iniciar(minutos); notifyListeners(); diff --git a/lib/main.dart b/lib/main.dart index fd1edca..090a9fb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,8 +6,6 @@ import 'servicios/servicio_audio.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - // Inicializar audio_service para reproducción en background. - // El handler se registra globalmente para que ServicioAudio lo use. final handler = await AudioService.init( builder: () => PluriWaveAudioHandler(), config: const AudioServiceConfig( diff --git a/lib/modelos/preset_ecualizador.dart b/lib/modelos/preset_ecualizador.dart new file mode 100644 index 0000000..147b0d7 --- /dev/null +++ b/lib/modelos/preset_ecualizador.dart @@ -0,0 +1,29 @@ +/// Modelo de preset de ecualizador. +/// 5 bandas: 60Hz, 250Hz, 1kHz, 4kHz, 16kHz +class PresetEcualizador { + final String nombre; + final List bandas; // 5 valores entre -12.0 y +12.0 dB + + const PresetEcualizador({required this.nombre, required this.bandas}) + : assert(bandas.length == 5); + + static final flat = PresetEcualizador(nombre: 'Flat', bandas: [0.0, 0.0, 0.0, 0.0, 0.0]); + static final rock = PresetEcualizador(nombre: 'Rock', bandas: [2.0, 1.0, -1.0, 2.0, 3.0]); + static final pop = PresetEcualizador(nombre: 'Pop', bandas: [1.0, 1.5, 0.5, 1.0, 1.5]); + static final bassBoost = PresetEcualizador(nombre: 'Bass Boost', bandas: [5.0, 3.0, -1.0, 0.5, 0.0]); + static final jazz = PresetEcualizador(nombre: 'Jazz', bandas: [3.0, -1.0, -1.5, 2.0, 4.0]); + static final voz = PresetEcualizador(nombre: 'Voz', bandas: [-2.0, -1.0, 2.0, 3.0, 1.0]); + + static final presets = [flat, rock, pop, bassBoost, jazz, voz]; + + factory PresetEcualizador.desdeJson(Map json) { + final raw = (json['bandas'] as List?)?.map((e) => (e as num).toDouble()).toList() ?? []; + final bandas = List.generate(5, (i) => i < raw.length ? raw[i] : 0.0); + return PresetEcualizador(nombre: json['nombre'] as String? ?? 'Personalizado', bandas: bandas); + } + + Map toJson() => {'nombre': nombre, 'bandas': bandas}; + + PresetEcualizador copyWithBandas(List bandas) => + PresetEcualizador(nombre: 'Personalizado', bandas: bandas); +} diff --git a/lib/pantallas/pantalla_ajustes.dart b/lib/pantallas/pantalla_ajustes.dart index 68d1ba8..44b9813 100644 --- a/lib/pantallas/pantalla_ajustes.dart +++ b/lib/pantallas/pantalla_ajustes.dart @@ -1,42 +1,387 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:share_plus/share_plus.dart' show Share, XFile; +import 'package:path_provider/path_provider.dart'; +import 'package:uuid/uuid.dart'; import '../estado/estado_radio.dart'; +import '../modelos/emisora.dart'; +import '../modelos/preset_ecualizador.dart'; +import '../widgets/ecualizador_widget.dart'; -/// Pantalla de ajustes — por ahora muestra info de la app. -/// En Fase 3 se añadirá Export/Import config y gestión PRO. class PantallaAjustes extends StatelessWidget { const PantallaAjustes({super.key}); @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final estado = context.read(); - return Scaffold( appBar: AppBar(title: const Text('Ajustes')), body: ListView( + children: const [ + _SeccionEcualizador(), + Divider(), + _SeccionEmisoras(), + Divider(), + _SeccionBackup(), + Divider(), + _SeccionInfo(), + ], + ), + ); + } +} + +// ── Sección Ecualizador ─────────────────────────────────────────────────────── + +class _SeccionEcualizador extends StatelessWidget { + const _SeccionEcualizador(); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (ctx, estado, _) { + final disponible = estado.ecualizadorDisponible; + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + const Icon(Icons.equalizer), + const SizedBox(width: 12), + Text('Ecualizador', style: Theme.of(ctx).textTheme.titleMedium), + const Spacer(), + if (!disponible) + Chip( + label: const Text('Reproduce una emisora para activar'), + visualDensity: VisualDensity.compact, + ), + ], + ), + if (disponible) ...[ + const SizedBox(height: 8), + PresetsEcualizadorWidget( + presetActual: estado.presetEcualizador, + onSeleccionar: (p) => estado.cambiarPresetEcualizador(p), + ), + const SizedBox(height: 12), + EcualizadorWidget( + preset: estado.presetEcualizador, + onCambio: (p) { + for (int i = 0; i < p.bandas.length; i++) { + estado.cambiarBandaEcualizador(i, p.bandas[i]); + } + }, + ), + ], + ], + ), + ); + }, + ); + } +} + +// ── Sección Emisoras personalizadas ────────────────────────────────────────── + +class _SeccionEmisoras extends StatelessWidget { + const _SeccionEmisoras(); + + @override + Widget build(BuildContext context) { + final estado = context.watch(); + final custom = estado.emisorasCustom; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), + child: Row( + children: [ + const Icon(Icons.add_circle_outline), + const SizedBox(width: 12), + Text('Emisoras personalizadas', + style: Theme.of(context).textTheme.titleMedium), + const Spacer(), + TextButton.icon( + icon: const Icon(Icons.add), + label: const Text('Añadir'), + onPressed: () => _mostrarFormularioAnadir(context), + ), + ], + ), + ), + if (custom.isEmpty) + const Padding( + padding: EdgeInsets.fromLTRB(16, 4, 16, 12), + child: Text('No hay emisoras personalizadas.', + style: TextStyle(color: Colors.grey)), + ) + else + for (final emisora in custom) + ListTile( + leading: const Icon(Icons.radio), + title: Text(emisora.nombre), + subtitle: Text(emisora.url, maxLines: 1, overflow: TextOverflow.ellipsis), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.play_arrow), + tooltip: 'Reproducir', + onPressed: () => context.read().reproducir(emisora), + ), + IconButton( + icon: const Icon(Icons.delete_outline), + tooltip: 'Eliminar', + onPressed: () => context.read().eliminarEmitoraCustom(emisora.uuid), + ), + ], + ), + ), + ], + ); + } + + Future _mostrarFormularioAnadir(BuildContext context) async { + await showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (ctx) => const _FormularioEmisora(), + ); + } +} + +class _FormularioEmisora extends StatefulWidget { + const _FormularioEmisora(); + + @override + State<_FormularioEmisora> createState() => _FormularioEmisoraState(); +} + +class _FormularioEmisoraState extends State<_FormularioEmisora> { + final _formKey = GlobalKey(); + final _nombreCtrl = TextEditingController(); + final _urlCtrl = TextEditingController(); + final _paisCtrl = TextEditingController(); + bool _guardando = false; + + @override + void dispose() { + _nombreCtrl.dispose(); + _urlCtrl.dispose(); + _paisCtrl.dispose(); + super.dispose(); + } + + Future _guardar() async { + if (!_formKey.currentState!.validate()) return; + setState(() => _guardando = true); + + final emisora = Emisora( + uuid: const Uuid().v4(), + nombre: _nombreCtrl.text.trim(), + url: _urlCtrl.text.trim(), + pais: _paisCtrl.text.trim().isEmpty ? null : _paisCtrl.text.trim(), + ); + + await context.read().agregarEmitoraCustom(emisora); + if (mounted) Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + final bottom = MediaQuery.of(context).viewInsets.bottom; + return Padding( + padding: EdgeInsets.fromLTRB(16, 16, 16, 16 + bottom), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('Añadir emisora', style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 16), + TextFormField( + controller: _nombreCtrl, + decoration: const InputDecoration(labelText: 'Nombre *', border: OutlineInputBorder()), + validator: (v) => v == null || v.trim().isEmpty ? 'Campo obligatorio' : null, + ), + const SizedBox(height: 12), + TextFormField( + controller: _urlCtrl, + decoration: const InputDecoration( + labelText: 'URL del stream *', + hintText: 'http://stream.ejemplo.com:8000/radio', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.url, + validator: (v) { + if (v == null || v.trim().isEmpty) return 'Campo obligatorio'; + final uri = Uri.tryParse(v.trim()); + if (uri == null || !uri.hasScheme) return 'URL no válida'; + return null; + }, + ), + const SizedBox(height: 12), + TextFormField( + controller: _paisCtrl, + decoration: const InputDecoration( + labelText: 'País (opcional)', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 20), + FilledButton( + onPressed: _guardando ? null : _guardar, + child: _guardando + ? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2)) + : const Text('Guardar emisora'), + ), + ], + ), + ), + ); + } +} + +// ── Sección Backup ──────────────────────────────────────────────────────────── + +class _SeccionBackup extends StatelessWidget { + const _SeccionBackup(); + + Future _exportar(BuildContext context) async { + try { + final estado = context.read(); + final config = await estado.exportarConfig(); + final json = const JsonEncoder.withIndent(' ').convert(config); + + final dir = await getTemporaryDirectory(); + final file = File('${dir.path}/pluriwave-backup.json'); + await file.writeAsString(json); + + await Share.shareXFiles( + [XFile(file.path)], + subject: 'PluriWave — copia de seguridad', + text: 'Configuración de PluriWave exportada el ${DateTime.now().toLocal()}', + ); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error al exportar: $e')), + ); + } + } + } + + Future _importar(BuildContext context) async { + try { + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['json'], + ); + if (result == null || result.files.single.path == null) return; + + final file = File(result.files.single.path!); + final json = jsonDecode(await file.readAsString()) as Map; + + if (context.mounted) { + final confirmar = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Importar configuración'), + content: const Text( + 'Esto añadirá los favoritos, emisoras y presets del fichero. ' + '¿Continuar?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('Cancelar')), + FilledButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('Importar')), + ], + ), + ); + if (confirmar != true) return; + if (context.mounted) { + await context.read().importarConfig(json); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Configuración importada correctamente')), + ); + } + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error al importar: $e')), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), + child: Row( + children: [ + const Icon(Icons.backup_outlined), + const SizedBox(width: 12), + Text('Copia de seguridad', + style: Theme.of(context).textTheme.titleMedium), + ], + ), + ), + ListTile( + leading: const Icon(Icons.upload_outlined), + title: const Text('Exportar configuración'), + subtitle: const Text('Favoritos, emisoras custom y presets de EQ'), + onTap: () => _exportar(context), + ), + ListTile( + leading: const Icon(Icons.download_outlined), + title: const Text('Importar configuración'), + subtitle: const Text('Restaurar desde un fichero de copia de seguridad'), + onTap: () => _importar(context), + ), + ], + ); + } +} + +// ── Sección Info ────────────────────────────────────────────────────────────── + +class _SeccionInfo extends StatelessWidget { + const _SeccionInfo(); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (ctx, estado, _) => Column( children: [ - // Info app ListTile( leading: const Icon(Icons.info_outline), title: const Text('PluriWave'), subtitle: const Text('v0.3.0 — Radio mundial'), ), - const Divider(), - // Favoritos FutureBuilder( future: estado.favoritos.obtenerTodos().then((l) => l.length), builder: (ctx, snap) => ListTile( leading: const Icon(Icons.favorite_outline), title: const Text('Favoritos guardados'), - trailing: Text( - snap.data?.toString() ?? '—', - style: theme.textTheme.bodyLarge, - ), + trailing: Text(snap.data?.toString() ?? '—', + style: Theme.of(ctx).textTheme.bodyLarge), ), ), - const Divider(), - // Filtro emisoras ListTile( leading: const Icon(Icons.verified_outlined), title: const Text('Filtro de emisoras'), @@ -44,39 +389,11 @@ class PantallaAjustes extends StatelessWidget { trailing: const Icon(Icons.check_circle, color: Colors.green), ), ListTile( - leading: const Icon(Icons.music_off_outlined), + leading: const Icon(Icons.music_note_outlined), title: const Text('Audio en background'), - subtitle: const Text('Activo — continúa al apagar pantalla'), + subtitle: const Text('Continúa al apagar la pantalla'), trailing: const Icon(Icons.check_circle, color: Colors.green), ), - const Divider(), - // Próximamente - const Padding( - padding: EdgeInsets.fromLTRB(16, 12, 16, 4), - child: Text('PRÓXIMAMENTE', style: TextStyle(fontSize: 12, letterSpacing: 1.2)), - ), - ListTile( - leading: const Icon(Icons.upload_outlined), - title: const Text('Exportar configuración'), - subtitle: const Text('Favoritos, radios custom, presets EQ'), - enabled: false, - ), - ListTile( - leading: const Icon(Icons.download_outlined), - title: const Text('Importar configuración'), - enabled: false, - ), - ListTile( - leading: const Icon(Icons.add_circle_outline), - title: const Text('Añadir radio personalizada'), - enabled: false, - ), - ListTile( - leading: const Icon(Icons.equalizer_outlined), - title: const Text('Ecualizador'), - subtitle: const Text('5 bandas, presets por emisora'), - enabled: false, - ), ], ), ); diff --git a/lib/servicios/servicio_audio.dart b/lib/servicios/servicio_audio.dart index d1b8cd6..d0ad12e 100644 --- a/lib/servicios/servicio_audio.dart +++ b/lib/servicios/servicio_audio.dart @@ -1,6 +1,7 @@ import 'package:audio_service/audio_service.dart'; import 'package:just_audio/just_audio.dart'; import '../modelos/emisora.dart'; +import '../modelos/preset_ecualizador.dart'; /// Estado de reproducción expuesto al UI. enum EstadoReproduccion { detenido, cargando, reproduciendo, pausado, error } @@ -10,20 +11,14 @@ enum EstadoReproduccion { detenido, cargando, reproduciendo, pausado, error } // ───────────────────────────────────────────────────────────── PluriWaveAudioHandler? _handlerGlobal; -/// Registra el handler. Llamar desde main.dart tras AudioService.init. void registrarHandler(PluriWaveAudioHandler handler) { _handlerGlobal = handler; } /// Wrapper de alto nivel para el UI. -/// -/// Delega TODA la reproducción al [PluriWaveAudioHandler] para garantizar -/// que el audio siga vivo en background con notificación foreground. class ServicioAudio { PluriWaveAudioHandler get _handler { - assert(_handlerGlobal != null, - 'ServicioAudio: handler no registrado. ' - 'Llama registrarHandler() en main.dart tras AudioService.init.'); + assert(_handlerGlobal != null, 'registrarHandler() no fue llamado en main.dart'); return _handlerGlobal!; } @@ -68,55 +63,52 @@ class ServicioAudio { } Future detener() => _handler.stop(); - - Future setVolumen(double vol) => _handler.setVolume(vol.clamp(0.0, 1.0)); - + Future setVolumen(double vol) => _handler.setVolumen(vol); double get volumen => _handler.volumen; bool get estaSonando => _handler.playbackState.value.playing; - - /// No-op: el handler se limpia en main.dart al cerrar la app. Future dispose() async {} + + // ── Ecualizador ────────────────────────────────────────────────────────── + AndroidEqualizer? get ecualizador => _handler.ecualizador; + bool get ecualizadorDisponible => _handler.ecualizadorDisponible; + PresetEcualizador get presetActual => _handler.presetActual; + + Future aplicarPreset(PresetEcualizador preset) => + _handler.aplicarPreset(preset); + + Future setBanda(int index, double db) => + _handler.setBanda(index, db); } // ───────────────────────────────────────────────────────────── -// AudioHandler — núcleo del audio en background +// AudioHandler // ───────────────────────────────────────────────────────────── - -/// Handler de audio_service. -/// -/// Gestiona la reproducción con `just_audio` y mantiene la notificación -/// foreground activa mientras hay audio reproduciéndose. -/// -/// ### Inicialización en main.dart -/// ```dart -/// final handler = await AudioService.init( -/// builder: () => PluriWaveAudioHandler(), -/// config: const AudioServiceConfig( -/// androidNotificationChannelId: 'es.freetimelab.pluriwave.audio', -/// androidNotificationChannelName: 'PluriWave Radio', -/// androidNotificationOngoing: true, -/// androidStopForegroundOnPause: true, -/// androidNotificationIcon: 'drawable/ic_stat_radio', -/// ), -/// ); -/// registrarHandler(handler); -/// ``` class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler { - final AudioPlayer _player = AudioPlayer(); + final AndroidEqualizer _eq = AndroidEqualizer(); + + late final AudioPlayer _player = AudioPlayer( + audioPipeline: AudioPipeline(androidAudioEffects: [_eq]), + ); + Emisora? emisoraActual; double _volumen = 1.0; double get volumen => _volumen; + AndroidEqualizer? get ecualizador => _eq; + bool _eqDisponible = false; + bool get ecualizadorDisponible => _eqDisponible; + + PresetEcualizador _presetActual = PresetEcualizador.flat; + PresetEcualizador get presetActual => _presetActual; + PluriWaveAudioHandler() { _setupStreams(); } void _setupStreams() { - // Propagar estado del player → playbackState (lo que ve la notificación) _player.playerStateStream.listen((state) { final playing = state.playing; final proc = state.processingState; - playbackState.add(playbackState.value.copyWith( controls: [ if (playing) MediaControl.pause else MediaControl.play, @@ -131,7 +123,6 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler { )); }); - // Actualizar bufferedPosition _player.bufferedPositionStream.listen((pos) { playbackState.add(playbackState.value.copyWith(bufferedPosition: pos)); }); @@ -154,6 +145,8 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler { await _player.stop(); await _player.setUrl(item.id); await _player.play(); + // Habilitar ecualizador tras reproducir (necesita audio activo) + await _activarEcualizador(); } on PlayerException catch (e) { playbackState.add(playbackState.value.copyWith( processingState: AudioProcessingState.error, @@ -164,6 +157,52 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler { } } + Future _activarEcualizador() async { + try { + final params = await _eq.parameters; + _eqDisponible = params.bands.isNotEmpty; + if (_eqDisponible) { + await _eq.setEnabled(true); + await aplicarPreset(_presetActual); + } + } catch (_) { + _eqDisponible = false; + } + } + + /// Aplica un preset al ecualizador nativo Android. + Future aplicarPreset(PresetEcualizador preset) async { + _presetActual = preset; + if (!_eqDisponible) return; + try { + final params = await _eq.parameters; + for (int i = 0; i < params.bands.length && i < preset.bandas.length; i++) { + await params.bands[i].setGain(preset.bandas[i]); + } + } catch (_) {} + } + + /// Ajusta una banda individual. + Future setBanda(int index, double db) async { + if (!_eqDisponible) return; + final bandas = List.from(_presetActual.bandas); + if (index >= 0 && index < bandas.length) { + bandas[index] = db; + _presetActual = _presetActual.copyWithBandas(bandas); + } + try { + final params = await _eq.parameters; + if (index < params.bands.length) { + await params.bands[index].setGain(db); + } + } catch (_) {} + } + + Future setVolumen(double vol) async { + _volumen = vol.clamp(0.0, 1.0); + await _player.setVolume(_volumen); + } + @override Future play() => _player.play(); @@ -181,11 +220,6 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler { @override Future seek(Duration position) => _player.seek(position); - Future setVolume(double vol) async { - _volumen = vol.clamp(0.0, 1.0); - await _player.setVolume(_volumen); - } - @override Future onTaskRemoved() async { await stop(); diff --git a/lib/widgets/ecualizador_widget.dart b/lib/widgets/ecualizador_widget.dart new file mode 100644 index 0000000..472e953 --- /dev/null +++ b/lib/widgets/ecualizador_widget.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import '../modelos/preset_ecualizador.dart'; + +/// Widget de ecualizador con 5 sliders verticales. +/// Basado en JaviHogar EcualizadorWidget, adaptado a Material You. +class EcualizadorWidget extends StatefulWidget { + final PresetEcualizador preset; + final void Function(PresetEcualizador) onCambio; + + const EcualizadorWidget({super.key, required this.preset, required this.onCambio}); + + @override + State createState() => _EcualizadorWidgetState(); +} + +class _EcualizadorWidgetState extends State { + late List _bandas; + final List _etiquetas = ['60Hz', '250Hz', '1kHz', '4kHz', '16kHz']; + + @override + void initState() { + super.initState(); + _bandas = List.from(widget.preset.bandas); + } + + @override + void didUpdateWidget(EcualizadorWidget old) { + super.didUpdateWidget(old); + if (old.preset.nombre != widget.preset.nombre) { + setState(() => _bandas = List.from(widget.preset.bandas)); + } + } + + void _actualizarBanda(int index, double valor) { + setState(() => _bandas[index] = valor); + widget.onCambio(PresetEcualizador(nombre: 'Personalizado', bandas: List.from(_bandas))); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('Ecualizador', style: theme.textTheme.titleMedium), + const SizedBox(height: 16), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + for (int i = 0; i < 5; i++) + Expanded( + child: Column( + children: [ + SizedBox( + height: 160, + child: RotatedBox( + quarterTurns: 3, + child: Slider( + value: _bandas[i], + min: -12.0, + max: 12.0, + divisions: 24, + onChanged: (v) => _actualizarBanda(i, v), + ), + ), + ), + Text( + '${_bandas[i].toStringAsFixed(1)}dB', + style: theme.textTheme.labelSmall, + textAlign: TextAlign.center, + ), + Text( + _etiquetas[i], + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } +} + +/// Chips de presets predefinidos. +class PresetsEcualizadorWidget extends StatelessWidget { + final PresetEcualizador presetActual; + final void Function(PresetEcualizador) onSeleccionar; + + const PresetsEcualizadorWidget({ + super.key, + required this.presetActual, + required this.onSeleccionar, + }); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 4, + children: PresetEcualizador.presets.map((p) { + return ChoiceChip( + label: Text(p.nombre), + selected: p.nombre == presetActual.nombre, + onSelected: (_) => onSeleccionar(p), + ); + }).toList(), + ); + } +} diff --git a/lib/widgets/tarjeta_emisora.dart b/lib/widgets/tarjeta_emisora.dart index 008d4d2..b2faf29 100644 --- a/lib/widgets/tarjeta_emisora.dart +++ b/lib/widgets/tarjeta_emisora.dart @@ -1,10 +1,13 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:shimmer/shimmer.dart'; +import '../estado/estado_radio.dart'; import '../modelos/emisora.dart'; /// Tarjeta compacta para mostrar una emisora en listas y grids. -class TarjetaEmisora extends StatelessWidget { +/// Incluye botón de favorito visible en ambos modos. +class TarjetaEmisora extends StatefulWidget { final Emisora emisora; final VoidCallback? onTap; final bool esCompacta; @@ -16,48 +19,96 @@ class TarjetaEmisora extends StatelessWidget { this.esCompacta = false, }); + @override + State createState() => _TarjetaEmisoraState(); +} + +class _TarjetaEmisoraState extends State { + bool _esFavorito = false; + bool _toggling = false; + + @override + void initState() { + super.initState(); + _checkFavorito(); + } + + Future _checkFavorito() async { + final fav = await context.read().esFavorito(widget.emisora.uuid); + if (mounted) setState(() => _esFavorito = fav); + } + + Future _toggle() async { + if (_toggling) return; + _toggling = true; + final estado = context.read(); + final esFav = await estado.toggleFavorito(widget.emisora); + if (mounted) setState(() => _esFavorito = esFav); + _toggling = false; + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(esFav + ? '${widget.emisora.nombre} añadida a favoritos' + : '${widget.emisora.nombre} eliminada de favoritos'), + duration: const Duration(seconds: 2), + )); + } + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); return Card( clipBehavior: Clip.antiAlias, child: InkWell( - onTap: onTap, - child: esCompacta ? _buildCompacta(theme) : _buildCompleta(theme), + onTap: widget.onTap, + child: widget.esCompacta + ? _buildCompacta(theme) + : _buildCompleta(theme), ), ); } Widget _buildCompleta(ThemeData theme) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + return Stack( children: [ - AspectRatio( - aspectRatio: 1, - child: _logo(theme, 60), - ), - Padding( - padding: const EdgeInsets.all(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - emisora.nombre, - style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - if (emisora.pais != null) - Text( - emisora.pais!, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: 1, + child: _logo(theme, 60), + ), + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.emisora.nombre, + style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), + if (widget.emisora.pais != null) + Text( + widget.emisora.pais!, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + // Botón favorito superpuesto (esquina superior derecha) + Positioned( + top: 4, + right: 4, + child: _botonFavorito(theme, mini: true), ), ], ); @@ -67,22 +118,46 @@ class TarjetaEmisora extends StatelessWidget { return ListTile( leading: SizedBox(width: 48, height: 48, child: _logo(theme, 24)), title: Text( - emisora.nombre, + widget.emisora.nombre, maxLines: 1, overflow: TextOverflow.ellipsis, ), subtitle: Text( - [emisora.pais, emisora.idioma].where((s) => s != null).join(' · '), + [widget.emisora.pais, widget.emisora.idioma] + .where((s) => s != null && s.isNotEmpty) + .join(' · '), maxLines: 1, overflow: TextOverflow.ellipsis, ), + trailing: _botonFavorito(theme, mini: false), + ); + } + + Widget _botonFavorito(ThemeData theme, {required bool mini}) { + return Material( + color: mini + ? theme.colorScheme.surface.withValues(alpha: 0.8) + : Colors.transparent, + shape: const CircleBorder(), + child: InkWell( + customBorder: const CircleBorder(), + onTap: _toggle, + child: Padding( + padding: EdgeInsets.all(mini ? 6 : 4), + child: Icon( + _esFavorito ? Icons.favorite_rounded : Icons.favorite_outline_rounded, + color: _esFavorito ? theme.colorScheme.error : theme.colorScheme.onSurfaceVariant, + size: mini ? 18 : 22, + ), + ), + ), ); } Widget _logo(ThemeData theme, double iconSize) { - if (emisora.favicon != null && emisora.favicon!.isNotEmpty) { + if (widget.emisora.favicon != null && widget.emisora.favicon!.isNotEmpty) { return CachedNetworkImage( - imageUrl: emisora.favicon!, + imageUrl: widget.emisora.favicon!, fit: BoxFit.cover, placeholder: (_, __) => _shimmer(theme), errorWidget: (_, __, ___) => _iconoFallback(theme, iconSize), diff --git a/pubspec.lock b/pubspec.lock index 3b0a6da..2a1b4c7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.1" + audio_service: + dependency: "direct main" + description: + name: audio_service + sha256: cb122c7c2639d2a992421ef96b67948ad88c5221da3365ccef1031393a76e044 + url: "https://pub.dev" + source: hosted + version: "0.18.18" + audio_service_platform_interface: + dependency: transitive + description: + name: audio_service_platform_interface + sha256: "6283782851f6c8b501b60904a32fc7199dc631172da0629d7301e66f672ab777" + url: "https://pub.dev" + source: hosted + version: "0.1.3" + audio_service_web: + dependency: transitive + description: + name: audio_service_web + sha256: b8ea9243201ee53383157fbccf13d5d2a866b5dda922ec19d866d1d5d70424df + url: "https://pub.dev" + source: hosted + version: "0.1.4" + audio_session: + dependency: "direct main" + description: + name: audio_session + sha256: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac" + url: "https://pub.dev" + source: hosted + version: "0.1.25" boolean_selector: dependency: transitive description: @@ -17,6 +49,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" characters: dependency: transitive description: @@ -33,6 +89,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" collection: dependency: transitive description: @@ -41,6 +105,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" cupertino_icons: dependency: "direct main" description: @@ -57,24 +137,178 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810 + url: "https://pub.dev" + source: hosted + version: "8.3.7" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_animate: + dependency: "direct main" + description: + name: flutter_animate + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" + url: "https://pub.dev" + source: hosted + version: "4.5.2" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "5.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" + url: "https://pub.dev" + source: hosted + version: "2.0.34" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.dev" + source: hosted + version: "0.1.3" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 + url: "https://pub.dev" + source: hosted + version: "6.3.3" + hooks: + dependency: transitive + description: + name: hooks + sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + just_audio: + dependency: "direct main" + description: + name: just_audio + sha256: f978d5b4ccea08f267dae0232ec5405c1b05d3f3cd63f82097ea46c015d5c09e + url: "https://pub.dev" + source: hosted + version: "0.9.46" + just_audio_platform_interface: + dependency: transitive + description: + name: just_audio_platform_interface + sha256: "2532c8d6702528824445921c5ff10548b518b13f808c2e34c2fd54793b999a6a" + url: "https://pub.dev" + source: hosted + version: "4.6.0" + just_audio_web: + dependency: transitive + description: + name: just_audio_web + sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663" + url: "https://pub.dev" + source: hosted + version: "0.4.16" leak_tracker: dependency: transitive description: @@ -103,10 +337,18 @@ packages: dependency: transitive description: name: lints - sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "5.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: @@ -131,6 +373,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + url: "https://pub.dev" + source: hosted + version: "0.17.6" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" path: dependency: transitive description: @@ -139,6 +421,174 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba" + url: "https://pub.dev" + source: hosted + version: "2.2.23" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da + url: "https://pub.dev" + source: hosted + version: "10.1.4" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b + url: "https://pub.dev" + source: hosted + version: "5.0.2" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.dev" + source: hosted + version: "2.5.5" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + url: "https://pub.dev" + source: hosted + version: "2.4.23" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -152,6 +602,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.2" + sqflite: + dependency: "direct main" + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40" + url: "https://pub.dev" + source: hosted + version: "2.4.2+3" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: @@ -176,6 +666,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" term_glyph: dependency: transitive description: @@ -192,6 +690,86 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.9" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" + url: "https://pub.dev" + source: hosted + version: "6.3.29" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.dev" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" vector_math: dependency: transitive description: @@ -208,6 +786,38 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: - dart: ">=3.11.1 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index 26feb52..9032e12 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,8 @@ dependencies: # Utils share_plus: ^10.1.3 + file_picker: ^8.1.7 + uuid: ^4.5.1 url_launcher: ^6.3.1 # Ads (activar cuando tengamos Ad Unit IDs)