fix(ci): resolve premium UI analyzer errors
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
@@ -47,7 +47,8 @@ class _SeccionEcualizador extends StatelessWidget {
|
||||
builder: (ctx, estado, _) {
|
||||
final disponible = estado.ecualizadorDisponible;
|
||||
final emisoraActual = estado.emisoraActual;
|
||||
final mostrarModoPorEmisora = emisoraActual != null && estado.emisoraActualEsFavorita;
|
||||
final mostrarModoPorEmisora =
|
||||
emisoraActual != null && estado.emisoraActualEsFavorita;
|
||||
final usandoEqPropio = estado.emisoraActualTienePresetPropio;
|
||||
|
||||
return PluriGlassSurface(
|
||||
@@ -58,7 +59,10 @@ class _SeccionEcualizador extends StatelessWidget {
|
||||
children: [
|
||||
const Icon(Icons.equalizer),
|
||||
const SizedBox(width: 12),
|
||||
Text('Ecualizador', style: Theme.of(ctx).textTheme.titleMedium),
|
||||
Text(
|
||||
'Ecualizador',
|
||||
style: Theme.of(ctx).textTheme.titleMedium,
|
||||
),
|
||||
const Spacer(),
|
||||
if (!disponible)
|
||||
const Chip(
|
||||
@@ -78,7 +82,11 @@ class _SeccionEcualizador extends StatelessWidget {
|
||||
: 'Usando EQ principal para ${emisoraActual.nombre}',
|
||||
),
|
||||
value: usandoEqPropio,
|
||||
onChanged: (usarPropio) => estado.cambiarModoEcualizadorEmisoraActual(usarPropio: usarPropio),
|
||||
onChanged:
|
||||
(usarPropio) =>
|
||||
estado.cambiarModoEcualizadorEmisoraActual(
|
||||
usarPropio: usarPropio,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
@@ -115,7 +123,10 @@ class _SeccionEmisoras extends StatelessWidget {
|
||||
children: [
|
||||
const Icon(Icons.add_circle_outline),
|
||||
const SizedBox(width: 12),
|
||||
Text('Emisoras personalizadas', style: Theme.of(context).textTheme.titleMedium),
|
||||
Text(
|
||||
'Emisoras personalizadas',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.add),
|
||||
@@ -127,7 +138,10 @@ class _SeccionEmisoras extends StatelessWidget {
|
||||
if (custom.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
child: Text('No hay emisoras personalizadas.', style: TextStyle(color: Colors.grey)),
|
||||
child: Text(
|
||||
'No hay emisoras personalizadas.',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
)
|
||||
else
|
||||
for (final emisora in custom)
|
||||
@@ -135,19 +149,27 @@ class _SeccionEmisoras extends StatelessWidget {
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.radio),
|
||||
title: Text(emisora.nombre),
|
||||
subtitle: Text(emisora.url, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
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<EstadoRadio>().reproducir(emisora),
|
||||
onPressed:
|
||||
() => context.read<EstadoRadio>().reproducir(emisora),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
tooltip: 'Eliminar',
|
||||
onPressed: () => context.read<EstadoRadio>().eliminarEmitoraCustom(emisora.uuid),
|
||||
onPressed:
|
||||
() => context
|
||||
.read<EstadoRadio>()
|
||||
.eliminarEmitoraCustom(emisora.uuid),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -214,12 +236,22 @@ class _FormularioEmisoraState extends State<_FormularioEmisora> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text('Añadir emisora', style: Theme.of(context).textTheme.titleLarge),
|
||||
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,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nombre *',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator:
|
||||
(v) =>
|
||||
v == null || v.trim().isEmpty
|
||||
? 'Campo obligatorio'
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
@@ -240,14 +272,22 @@ class _FormularioEmisoraState extends State<_FormularioEmisora> {
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _paisCtrl,
|
||||
decoration: const InputDecoration(labelText: 'País (opcional)', border: OutlineInputBorder()),
|
||||
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'),
|
||||
child:
|
||||
_guardando
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Guardar emisora'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -272,44 +312,68 @@ class _SeccionBackup extends StatelessWidget {
|
||||
await Share.shareXFiles(
|
||||
[XFile(file.path)],
|
||||
subject: 'PluriWave — copia de seguridad',
|
||||
text: 'Configuración de PluriWave exportada el ${DateTime.now().toLocal()}',
|
||||
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')));
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Error al exportar: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _importar(BuildContext context) async {
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['json']);
|
||||
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<String, dynamic>;
|
||||
final json =
|
||||
jsonDecode(await file.readAsString()) as Map<String, dynamic>;
|
||||
|
||||
if (context.mounted) {
|
||||
final confirmar = await showDialog<bool>(
|
||||
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')),
|
||||
],
|
||||
),
|
||||
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<EstadoRadio>().importarConfig(json);
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Configuración importada correctamente')));
|
||||
final estado = context.read<EstadoRadio>();
|
||||
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')));
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Error al importar: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -324,7 +388,10 @@ class _SeccionBackup extends StatelessWidget {
|
||||
children: [
|
||||
const Icon(Icons.backup_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text('Copia de seguridad', style: Theme.of(context).textTheme.titleMedium),
|
||||
Text(
|
||||
'Copia de seguridad',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
ListTile(
|
||||
@@ -338,7 +405,9 @@ class _SeccionBackup extends StatelessWidget {
|
||||
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'),
|
||||
subtitle: const Text(
|
||||
'Restaurar desde un fichero de copia de seguridad',
|
||||
),
|
||||
onTap: () => _importar(context),
|
||||
),
|
||||
],
|
||||
@@ -353,41 +422,49 @@ class _SeccionInfo extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<EstadoRadio>(
|
||||
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'),
|
||||
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<int>(
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
FutureBuilder<int>(
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../estado/estado_radio.dart';
|
||||
import '../tema/pluriwave_theme.dart';
|
||||
import '../widgets/pluri_glass_surface.dart';
|
||||
import '../widgets/pluri_icon.dart';
|
||||
import '../widgets/tarjeta_emisora.dart';
|
||||
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
|
||||
|
||||
const _paises = [
|
||||
('España', 'ES'), ('USA', 'US'), ('México', 'MX'), ('Argentina', 'AR'),
|
||||
('UK', 'GB'), ('Francia', 'FR'), ('Alemania', 'DE'), ('Italia', 'IT'),
|
||||
('Brasil', 'BR'), ('Japón', 'JP'),
|
||||
('España', 'ES'),
|
||||
('USA', 'US'),
|
||||
('México', 'MX'),
|
||||
('Argentina', 'AR'),
|
||||
('UK', 'GB'),
|
||||
('Francia', 'FR'),
|
||||
('Alemania', 'DE'),
|
||||
('Italia', 'IT'),
|
||||
('Brasil', 'BR'),
|
||||
('Japón', 'JP'),
|
||||
];
|
||||
|
||||
const _idiomas = [
|
||||
'spanish', 'english', 'french', 'german', 'portuguese',
|
||||
'italian', 'japanese', 'arabic', 'russian',
|
||||
'spanish',
|
||||
'english',
|
||||
'french',
|
||||
'german',
|
||||
'portuguese',
|
||||
'italian',
|
||||
'japanese',
|
||||
'arabic',
|
||||
'russian',
|
||||
];
|
||||
|
||||
class PantallaBuscar extends StatefulWidget {
|
||||
@@ -60,7 +73,10 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
child: SearchBar(
|
||||
controller: _controller,
|
||||
hintText: 'Nombre de la emisora...',
|
||||
leading: const PluriIcon(glyph: PluriIconGlyph.search, variant: PluriIconVariant.filled),
|
||||
leading: const PluriIcon(
|
||||
glyph: PluriIconGlyph.search,
|
||||
variant: PluriIconVariant.filled,
|
||||
),
|
||||
trailing: [
|
||||
if (_controller.text.isNotEmpty)
|
||||
IconButton(
|
||||
@@ -77,14 +93,24 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
),
|
||||
),
|
||||
),
|
||||
_seccionFiltro('País', _paises.map((p) => (p.$1, p.$2)).toList(), _paisSeleccionado, (v) {
|
||||
setState(() => _paisSeleccionado = v);
|
||||
_buscar();
|
||||
}),
|
||||
_seccionFiltro('Idioma', _idiomas.map((i) => (i, i)).toList(), _idiomaSeleccionado, (v) {
|
||||
setState(() => _idiomaSeleccionado = v);
|
||||
_buscar();
|
||||
}),
|
||||
_seccionFiltro(
|
||||
'País',
|
||||
_paises.map((p) => (p.$1, p.$2)).toList(),
|
||||
_paisSeleccionado,
|
||||
(v) {
|
||||
setState(() => _paisSeleccionado = v);
|
||||
_buscar();
|
||||
},
|
||||
),
|
||||
_seccionFiltro(
|
||||
'Idioma',
|
||||
_idiomas.map((i) => (i, i)).toList(),
|
||||
_idiomaSeleccionado,
|
||||
(v) {
|
||||
setState(() => _idiomaSeleccionado = v);
|
||||
_buscar();
|
||||
},
|
||||
),
|
||||
Expanded(child: _resultados(estado, theme)),
|
||||
],
|
||||
);
|
||||
@@ -138,15 +164,25 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
final resultados = estado.resultadosBusqueda;
|
||||
|
||||
if (resultados.isEmpty) {
|
||||
final sinFiltros = _controller.text.isEmpty && _paisSeleccionado == null && _idiomaSeleccionado == null;
|
||||
final sinFiltros =
|
||||
_controller.text.isEmpty &&
|
||||
_paisSeleccionado == null &&
|
||||
_idiomaSeleccionado == null;
|
||||
return Center(
|
||||
child: PluriGlassSurface(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const PluriIcon(glyph: PluriIconGlyph.search, variant: PluriIconVariant.activeGlow, size: 44),
|
||||
const PluriIcon(
|
||||
glyph: PluriIconGlyph.search,
|
||||
variant: PluriIconVariant.activeGlow,
|
||||
size: 44,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(sinFiltros ? 'Buscá una emisora' : 'Sin resultados', style: theme.textTheme.titleMedium),
|
||||
Text(
|
||||
sinFiltros ? 'Buscá una emisora' : 'Sin resultados',
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -157,11 +193,12 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: resultados.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 6),
|
||||
itemBuilder: (context, i) => TarjetaEmisora(
|
||||
emisora: resultados[i],
|
||||
esCompacta: true,
|
||||
onTap: () => context.read<EstadoRadio>().reproducir(resultados[i]),
|
||||
).animate().fadeIn(delay: (i * 20).ms),
|
||||
itemBuilder:
|
||||
(context, i) => TarjetaEmisora(
|
||||
emisora: resultados[i],
|
||||
esCompacta: true,
|
||||
onTap: () => context.read<EstadoRadio>().reproducir(resultados[i]),
|
||||
).animate().fadeIn(delay: (i * 20).ms),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../estado/estado_radio.dart';
|
||||
import '../tema/pluriwave_theme.dart';
|
||||
import '../widgets/pluri_glass_surface.dart';
|
||||
import '../widgets/pluri_icon.dart';
|
||||
import '../widgets/tarjeta_emisora.dart';
|
||||
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
|
||||
|
||||
class PantallaFavoritos extends StatelessWidget {
|
||||
const PantallaFavoritos({super.key});
|
||||
@@ -22,13 +21,19 @@ class PantallaFavoritos extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const PluriIcon(glyph: PluriIconGlyph.favorites, variant: PluriIconVariant.activeGlow, size: 52),
|
||||
const PluriIcon(
|
||||
glyph: PluriIconGlyph.favorites,
|
||||
variant: PluriIconVariant.activeGlow,
|
||||
size: 52,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('Sin favoritos aún', style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tocá ♥ en cualquier emisora para guardarla',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -38,8 +43,7 @@ class PantallaFavoritos extends StatelessWidget {
|
||||
|
||||
return ReorderableListView.builder(
|
||||
padding: const EdgeInsets.all(12),
|
||||
onReorder: (oldIndex, newIndex) async {
|
||||
if (newIndex > oldIndex) newIndex--;
|
||||
onReorderItem: (oldIndex, newIndex) async {
|
||||
final emisora = favoritos[oldIndex];
|
||||
await estado.favoritos.reordenar(emisora.uuid, newIndex);
|
||||
await estado.cargarFavoritos();
|
||||
@@ -72,13 +76,16 @@ class PantallaFavoritos extends StatelessWidget {
|
||||
await estado.cargarFavoritos();
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('${emisora.nombre} eliminada de favoritos')),
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'${emisora.nombre} eliminada de favoritos',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shimmer/shimmer.dart' as shimmer;
|
||||
@@ -7,7 +7,7 @@ import '../estado/estado_radio.dart';
|
||||
import '../tema/pluriwave_theme.dart';
|
||||
import '../widgets/pluri_glass_surface.dart';
|
||||
import '../widgets/pluri_icon.dart';
|
||||
import '../widgets/tarjeta_emisora.dart';
|
||||
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
|
||||
|
||||
/// Pantalla principal: emisoras populares y por género.
|
||||
class PantallaInicio extends StatefulWidget {
|
||||
@@ -19,8 +19,18 @@ class PantallaInicio extends StatefulWidget {
|
||||
|
||||
class _PantallaInicioState extends State<PantallaInicio> {
|
||||
static const _generos = [
|
||||
'pop', 'rock', 'jazz', 'classical', 'electronic', 'news',
|
||||
'talk', 'hip-hop', 'country', 'metal', 'reggae', 'latin',
|
||||
'pop',
|
||||
'rock',
|
||||
'jazz',
|
||||
'classical',
|
||||
'electronic',
|
||||
'news',
|
||||
'talk',
|
||||
'hip-hop',
|
||||
'country',
|
||||
'metal',
|
||||
'reggae',
|
||||
'latin',
|
||||
];
|
||||
String? _generoSeleccionado;
|
||||
|
||||
@@ -37,7 +47,8 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
SliverToBoxAdapter(child: _heroHeader(context)),
|
||||
SliverToBoxAdapter(child: _seccionTendencias(estado, theme)),
|
||||
SliverToBoxAdapter(child: _chipGeneros(context, theme)),
|
||||
if (estado.error != null) SliverToBoxAdapter(child: _errorBanner(estado, theme)),
|
||||
if (estado.error != null)
|
||||
SliverToBoxAdapter(child: _errorBanner(estado, theme)),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.symmetric(horizontal: t.spacingMd),
|
||||
sliver: _gridEmisoras(estado),
|
||||
@@ -51,16 +62,33 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
final t = context.pluriTokens;
|
||||
final theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(t.spacingMd, t.spacingSm, t.spacingMd, t.spacingSm),
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
t.spacingMd,
|
||||
t.spacingSm,
|
||||
t.spacingMd,
|
||||
t.spacingSm,
|
||||
),
|
||||
child: PluriGlassSurface(
|
||||
borderRadius: BorderRadius.circular(t.radiusLg),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const PluriIcon(glyph: PluriIconGlyph.home, variant: PluriIconVariant.activeGlow, size: 30),
|
||||
const PluriIcon(
|
||||
glyph: PluriIconGlyph.home,
|
||||
variant: PluriIconVariant.activeGlow,
|
||||
size: 30,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text('PluriWave', style: theme.textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.w700)),
|
||||
Text('Ondas vivas globales', style: theme.textTheme.titleMedium?.copyWith(color: t.warmCoral)),
|
||||
Text(
|
||||
'PluriWave',
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Ondas vivas globales',
|
||||
style: theme.textTheme.titleMedium?.copyWith(color: t.warmCoral),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -79,26 +107,31 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 56,
|
||||
child: estado.cargandoPopulares
|
||||
? ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: 5,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (_, __) => _ChipShimmer(theme: theme),
|
||||
)
|
||||
: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: estado.tendencias.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (context, i) {
|
||||
final e = estado.tendencias[i];
|
||||
return ActionChip(
|
||||
avatar: const Icon(Icons.graphic_eq_rounded, size: 18),
|
||||
label: Text(e.nombre, maxLines: 1),
|
||||
onPressed: () => context.read<EstadoRadio>().reproducir(e),
|
||||
).animate().fadeIn(delay: (i * 50).ms);
|
||||
},
|
||||
),
|
||||
child:
|
||||
estado.cargandoPopulares
|
||||
? ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: 5,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (_, __) => _ChipShimmer(theme: theme),
|
||||
)
|
||||
: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: estado.tendencias.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (context, i) {
|
||||
final e = estado.tendencias[i];
|
||||
return ActionChip(
|
||||
avatar: const Icon(
|
||||
Icons.graphic_eq_rounded,
|
||||
size: 18,
|
||||
),
|
||||
label: Text(e.nombre, maxLines: 1),
|
||||
onPressed:
|
||||
() => context.read<EstadoRadio>().reproducir(e),
|
||||
).animate().fadeIn(delay: (i * 50).ms);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -119,23 +152,24 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: _generos.map((g) {
|
||||
final seleccionado = _generoSeleccionado == g;
|
||||
return FilterChip(
|
||||
label: Text(g),
|
||||
selected: seleccionado,
|
||||
onSelected: (_) {
|
||||
setState(() {
|
||||
_generoSeleccionado = seleccionado ? null : g;
|
||||
});
|
||||
if (!seleccionado) {
|
||||
context.read<EstadoRadio>().buscar(tag: g);
|
||||
} else {
|
||||
context.read<EstadoRadio>().cargarPopulares();
|
||||
}
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
children:
|
||||
_generos.map((g) {
|
||||
final seleccionado = _generoSeleccionado == g;
|
||||
return FilterChip(
|
||||
label: Text(g),
|
||||
selected: seleccionado,
|
||||
onSelected: (_) {
|
||||
setState(() {
|
||||
_generoSeleccionado = seleccionado ? null : g;
|
||||
});
|
||||
if (!seleccionado) {
|
||||
context.read<EstadoRadio>().buscar(tag: g);
|
||||
} else {
|
||||
context.read<EstadoRadio>().cargarPopulares();
|
||||
}
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -153,7 +187,10 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
Icon(Icons.wifi_off, color: theme.colorScheme.error),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(estado.error!)),
|
||||
TextButton(onPressed: estado.cargarPopulares, child: const Text('Reintentar')),
|
||||
TextButton(
|
||||
onPressed: estado.cargarPopulares,
|
||||
child: const Text('Reintentar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -161,12 +198,20 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
}
|
||||
|
||||
Widget _gridEmisoras(EstadoRadio estado) {
|
||||
final emisoras = _generoSeleccionado != null ? estado.resultadosBusqueda : estado.emisorasInicio;
|
||||
final cargando = estado.cargandoPopulares || (_generoSeleccionado != null && estado.cargandoBusqueda);
|
||||
final emisoras =
|
||||
_generoSeleccionado != null
|
||||
? estado.resultadosBusqueda
|
||||
: estado.emisorasInicio;
|
||||
final cargando =
|
||||
estado.cargandoPopulares ||
|
||||
(_generoSeleccionado != null && estado.cargandoBusqueda);
|
||||
|
||||
if (cargando) {
|
||||
return SliverGrid(
|
||||
delegate: SliverChildBuilderDelegate((_, __) => const TarjetaEmisoraShimmer(), childCount: 12),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(_, __) => const TarjetaEmisoraShimmer(),
|
||||
childCount: 12,
|
||||
),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 0.85,
|
||||
@@ -177,7 +222,9 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
||||
}
|
||||
|
||||
if (emisoras.isEmpty) {
|
||||
return const SliverFillRemaining(child: Center(child: Text('No hay emisoras disponibles')));
|
||||
return const SliverFillRemaining(
|
||||
child: Center(child: Text('No hay emisoras disponibles')),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverGrid(
|
||||
|
||||
Reference in New Issue
Block a user