import 'dart:convert'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart' show Share, XFile; import 'package:uuid/uuid.dart'; import '../estado/estado_radio.dart'; import '../modelos/emisora.dart'; import '../widgets/ecualizador_widget.dart'; import '../widgets/pluri_glass_surface.dart'; import '../widgets/pluri_icon.dart'; import '../widgets/pluri_wave_scaffold.dart'; class PantallaAjustes extends StatelessWidget { const PantallaAjustes({super.key}); @override Widget build(BuildContext context) { return PluriWaveScaffold( appBar: AppBar(title: const Text('Ajustes')), body: ListView( padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), children: const [ _SeccionEcualizador(), SizedBox(height: 12), _SeccionEmisoras(), SizedBox(height: 12), _SeccionBackup(), SizedBox(height: 12), _SeccionInfo(), ], ), ); } } class _SeccionEcualizador extends StatelessWidget { const _SeccionEcualizador(); @override Widget build(BuildContext context) { return Consumer( builder: (ctx, estado, _) { final disponible = estado.ecualizadorDisponible; final emisoraActual = estado.emisoraActual; final mostrarModoPorEmisora = emisoraActual != null && estado.emisoraActualEsFavorita; final usandoEqPropio = estado.emisoraActualTienePresetPropio; return PluriGlassSurface( 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) const Chip( label: Text('Se guarda aunque no esté activo'), visualDensity: VisualDensity.compact, ), ], ), if (mostrarModoPorEmisora) ...[ const SizedBox(height: 8), SwitchListTile.adaptive( contentPadding: EdgeInsets.zero, title: const Text('Usar EQ propio para esta favorita'), subtitle: Text( usandoEqPropio ? 'Activo para ${emisoraActual.nombre}' : 'Usando EQ principal para ${emisoraActual.nombre}', ), value: usandoEqPropio, onChanged: (usarPropio) => estado.cambiarModoEcualizadorEmisoraActual( usarPropio: usarPropio, ), ), ], const SizedBox(height: 8), PresetsEcualizadorWidget( presetActual: estado.presetEcualizador, onSeleccionar: (p) => estado.cambiarPresetEcualizador(p), ), const SizedBox(height: 12), EcualizadorWidget( preset: estado.presetEcualizador, onCambio: (p) => estado.cambiarPresetEcualizador(p), ), ], ), ); }, ); } } class _SeccionEmisoras extends StatelessWidget { const _SeccionEmisoras(); @override Widget build(BuildContext context) { final estado = context.watch(); final custom = estado.emisorasCustom; return PluriGlassSurface( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ 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.only(top: 8), child: Text( 'No hay emisoras personalizadas.', style: TextStyle(color: Colors.grey), ), ) else for (final emisora in custom) ListTile( contentPadding: EdgeInsets.zero, 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'), ), ], ), ), ); } } 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) { final estado = context.read(); final messenger = ScaffoldMessenger.of(context); await estado.importarConfig(json); messenger.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 PluriGlassSurface( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.backup_outlined), const SizedBox(width: 12), Text( 'Copia de seguridad', style: Theme.of(context).textTheme.titleMedium, ), ], ), ListTile( contentPadding: EdgeInsets.zero, 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( contentPadding: EdgeInsets.zero, 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), ), ], ), ); } } class _SeccionInfo extends StatelessWidget { const _SeccionInfo(); @override Widget build(BuildContext context) { return Consumer( builder: (ctx, estado, _) => PluriGlassSurface( child: Column( children: [ const ListTile( contentPadding: EdgeInsets.zero, leading: PluriIcon( glyph: PluriIconGlyph.settings, variant: PluriIconVariant.filled, ), title: Text('PluriWave'), subtitle: Text('v0.3.0 — Radio mundial'), ), FutureBuilder( future: estado.favoritos.obtenerTodos().then((l) => l.length), builder: (ctx, snap) => ListTile( contentPadding: EdgeInsets.zero, leading: const Icon(Icons.favorite_outline), title: const Text('Favoritos guardados'), trailing: Text( snap.data?.toString() ?? '—', style: Theme.of(ctx).textTheme.bodyLarge, ), ), ), const ListTile( contentPadding: EdgeInsets.zero, leading: Icon(Icons.verified_outlined), title: Text('Filtro de emisoras'), subtitle: Text('Solo emisoras verificadas como activas'), trailing: Icon(Icons.check_circle, color: Colors.green), ), const ListTile( contentPadding: EdgeInsets.zero, leading: Icon(Icons.music_note_outlined), title: Text('Audio en background'), subtitle: Text('Continúa al apagar la pantalla'), trailing: Icon(Icons.check_circle, color: Colors.green), ), ], ), ), ); } }