fix(ci): resolve premium UI analyzer errors
This commit is contained in:
+78
-64
@@ -7,8 +7,8 @@ import 'pantallas/pantalla_buscar.dart';
|
|||||||
import 'pantallas/pantalla_favoritos.dart';
|
import 'pantallas/pantalla_favoritos.dart';
|
||||||
import 'pantallas/pantalla_ajustes.dart';
|
import 'pantallas/pantalla_ajustes.dart';
|
||||||
import 'tema/pluriwave_theme.dart';
|
import 'tema/pluriwave_theme.dart';
|
||||||
import 'widgets/mini_reproductor.dart';
|
|
||||||
import 'widgets/pluri_icon.dart';
|
import 'widgets/pluri_icon.dart';
|
||||||
|
import 'package:pluriwave/widgets/mini_reproductor.dart';
|
||||||
|
|
||||||
class PluriWaveApp extends StatelessWidget {
|
class PluriWaveApp extends StatelessWidget {
|
||||||
const PluriWaveApp({super.key});
|
const PluriWaveApp({super.key});
|
||||||
@@ -113,18 +113,19 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: _indice == 3
|
appBar:
|
||||||
? null // PantallaAjustes tiene su propio AppBar
|
_indice == 3
|
||||||
: AppBar(
|
? null // PantallaAjustes tiene su propio AppBar
|
||||||
title: const Text('PluriWave'),
|
: AppBar(
|
||||||
actions: [
|
title: const Text('PluriWave'),
|
||||||
IconButton(
|
actions: [
|
||||||
icon: const Icon(Icons.bedtime_outlined),
|
IconButton(
|
||||||
tooltip: 'Timer de sueño',
|
icon: const Icon(Icons.bedtime_outlined),
|
||||||
onPressed: () => _mostrarTimerDialog(context),
|
tooltip: 'Timer de sueño',
|
||||||
),
|
onPressed: () => _mostrarTimerDialog(context),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
|
),
|
||||||
body: _paginas[_indice],
|
body: _paginas[_indice],
|
||||||
bottomNavigationBar: Column(
|
bottomNavigationBar: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -144,58 +145,71 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
|
|||||||
final estado = context.read<EstadoRadio>();
|
final estado = context.read<EstadoRadio>();
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => SafeArea(
|
builder:
|
||||||
child: Padding(
|
(ctx) => SafeArea(
|
||||||
padding: const EdgeInsets.all(24),
|
child: Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.all(24),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Text('Timer de sueño', style: Theme.of(ctx).textTheme.titleLarge),
|
children: [
|
||||||
const SizedBox(height: 16),
|
Text(
|
||||||
if (estado.timer.activo)
|
'Timer de sueño',
|
||||||
StreamBuilder<Duration>(
|
style: Theme.of(ctx).textTheme.titleLarge,
|
||||||
stream: estado.timer.tiempoRestanteStream,
|
),
|
||||||
builder: (ctx, snap) {
|
const SizedBox(height: 16),
|
||||||
final t = snap.data ?? Duration.zero;
|
if (estado.timer.activo)
|
||||||
final h = t.inHours;
|
StreamBuilder<Duration>(
|
||||||
final m = t.inMinutes.remainder(60).toString().padLeft(2, '0');
|
stream: estado.timer.tiempoRestanteStream,
|
||||||
final s = t.inSeconds.remainder(60).toString().padLeft(2, '0');
|
builder: (ctx, snap) {
|
||||||
return Column(
|
final t = snap.data ?? Duration.zero;
|
||||||
children: [
|
final h = t.inHours;
|
||||||
Text(
|
final m = t.inMinutes
|
||||||
'${h > 0 ? "${h}h " : ""}${m}m ${s}s',
|
.remainder(60)
|
||||||
style: Theme.of(ctx).textTheme.headlineMedium,
|
.toString()
|
||||||
),
|
.padLeft(2, '0');
|
||||||
const SizedBox(height: 8),
|
final s = t.inSeconds
|
||||||
FilledButton.tonal(
|
.remainder(60)
|
||||||
onPressed: () {
|
.toString()
|
||||||
estado.cancelarTimer();
|
.padLeft(2, '0');
|
||||||
Navigator.pop(ctx);
|
return Column(
|
||||||
},
|
children: [
|
||||||
child: const Text('Cancelar timer'),
|
Text(
|
||||||
),
|
'${h > 0 ? "${h}h " : ""}${m}m ${s}s',
|
||||||
],
|
style: Theme.of(ctx).textTheme.headlineMedium,
|
||||||
);
|
),
|
||||||
},
|
const SizedBox(height: 8),
|
||||||
)
|
FilledButton.tonal(
|
||||||
else
|
onPressed: () {
|
||||||
Wrap(
|
estado.cancelarTimer();
|
||||||
spacing: 8,
|
Navigator.pop(ctx);
|
||||||
children: [3, 5, 10, 15, 30, 60, 90, 120, 180]
|
},
|
||||||
.map((min) => ActionChip(
|
child: const Text('Cancelar timer'),
|
||||||
label: Text('$min min'),
|
),
|
||||||
onPressed: () {
|
],
|
||||||
estado.iniciarTimer(min);
|
);
|
||||||
Navigator.pop(ctx);
|
},
|
||||||
},
|
)
|
||||||
))
|
else
|
||||||
.toList(),
|
Wrap(
|
||||||
),
|
spacing: 8,
|
||||||
],
|
children:
|
||||||
|
[3, 5, 10, 15, 30, 60, 90, 120, 180]
|
||||||
|
.map(
|
||||||
|
(min) => ActionChip(
|
||||||
|
label: Text('$min min'),
|
||||||
|
onPressed: () {
|
||||||
|
estado.iniciarTimer(min);
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
@@ -47,7 +47,8 @@ class _SeccionEcualizador extends StatelessWidget {
|
|||||||
builder: (ctx, estado, _) {
|
builder: (ctx, estado, _) {
|
||||||
final disponible = estado.ecualizadorDisponible;
|
final disponible = estado.ecualizadorDisponible;
|
||||||
final emisoraActual = estado.emisoraActual;
|
final emisoraActual = estado.emisoraActual;
|
||||||
final mostrarModoPorEmisora = emisoraActual != null && estado.emisoraActualEsFavorita;
|
final mostrarModoPorEmisora =
|
||||||
|
emisoraActual != null && estado.emisoraActualEsFavorita;
|
||||||
final usandoEqPropio = estado.emisoraActualTienePresetPropio;
|
final usandoEqPropio = estado.emisoraActualTienePresetPropio;
|
||||||
|
|
||||||
return PluriGlassSurface(
|
return PluriGlassSurface(
|
||||||
@@ -58,7 +59,10 @@ class _SeccionEcualizador extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Icons.equalizer),
|
const Icon(Icons.equalizer),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text('Ecualizador', style: Theme.of(ctx).textTheme.titleMedium),
|
Text(
|
||||||
|
'Ecualizador',
|
||||||
|
style: Theme.of(ctx).textTheme.titleMedium,
|
||||||
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (!disponible)
|
if (!disponible)
|
||||||
const Chip(
|
const Chip(
|
||||||
@@ -78,7 +82,11 @@ class _SeccionEcualizador extends StatelessWidget {
|
|||||||
: 'Usando EQ principal para ${emisoraActual.nombre}',
|
: 'Usando EQ principal para ${emisoraActual.nombre}',
|
||||||
),
|
),
|
||||||
value: usandoEqPropio,
|
value: usandoEqPropio,
|
||||||
onChanged: (usarPropio) => estado.cambiarModoEcualizadorEmisoraActual(usarPropio: usarPropio),
|
onChanged:
|
||||||
|
(usarPropio) =>
|
||||||
|
estado.cambiarModoEcualizadorEmisoraActual(
|
||||||
|
usarPropio: usarPropio,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -115,7 +123,10 @@ class _SeccionEmisoras extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Icons.add_circle_outline),
|
const Icon(Icons.add_circle_outline),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text('Emisoras personalizadas', style: Theme.of(context).textTheme.titleMedium),
|
Text(
|
||||||
|
'Emisoras personalizadas',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
@@ -127,7 +138,10 @@ class _SeccionEmisoras extends StatelessWidget {
|
|||||||
if (custom.isEmpty)
|
if (custom.isEmpty)
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.only(top: 8),
|
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
|
else
|
||||||
for (final emisora in custom)
|
for (final emisora in custom)
|
||||||
@@ -135,19 +149,27 @@ class _SeccionEmisoras extends StatelessWidget {
|
|||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
leading: const Icon(Icons.radio),
|
leading: const Icon(Icons.radio),
|
||||||
title: Text(emisora.nombre),
|
title: Text(emisora.nombre),
|
||||||
subtitle: Text(emisora.url, maxLines: 1, overflow: TextOverflow.ellipsis),
|
subtitle: Text(
|
||||||
|
emisora.url,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.play_arrow),
|
icon: const Icon(Icons.play_arrow),
|
||||||
tooltip: 'Reproducir',
|
tooltip: 'Reproducir',
|
||||||
onPressed: () => context.read<EstadoRadio>().reproducir(emisora),
|
onPressed:
|
||||||
|
() => context.read<EstadoRadio>().reproducir(emisora),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete_outline),
|
icon: const Icon(Icons.delete_outline),
|
||||||
tooltip: 'Eliminar',
|
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,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
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),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _nombreCtrl,
|
controller: _nombreCtrl,
|
||||||
decoration: const InputDecoration(labelText: 'Nombre *', border: OutlineInputBorder()),
|
decoration: const InputDecoration(
|
||||||
validator: (v) => v == null || v.trim().isEmpty ? 'Campo obligatorio' : null,
|
labelText: 'Nombre *',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
validator:
|
||||||
|
(v) =>
|
||||||
|
v == null || v.trim().isEmpty
|
||||||
|
? 'Campo obligatorio'
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@@ -240,14 +272,22 @@ class _FormularioEmisoraState extends State<_FormularioEmisora> {
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _paisCtrl,
|
controller: _paisCtrl,
|
||||||
decoration: const InputDecoration(labelText: 'País (opcional)', border: OutlineInputBorder()),
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'País (opcional)',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: _guardando ? null : _guardar,
|
onPressed: _guardando ? null : _guardar,
|
||||||
child: _guardando
|
child:
|
||||||
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2))
|
_guardando
|
||||||
: const Text('Guardar emisora'),
|
? 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(
|
await Share.shareXFiles(
|
||||||
[XFile(file.path)],
|
[XFile(file.path)],
|
||||||
subject: 'PluriWave — copia de seguridad',
|
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) {
|
} catch (e) {
|
||||||
if (context.mounted) {
|
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 {
|
Future<void> _importar(BuildContext context) async {
|
||||||
try {
|
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;
|
if (result == null || result.files.single.path == null) return;
|
||||||
|
|
||||||
final file = File(result.files.single.path!);
|
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) {
|
if (context.mounted) {
|
||||||
final confirmar = await showDialog<bool>(
|
final confirmar = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder:
|
||||||
title: const Text('Importar configuración'),
|
(ctx) => AlertDialog(
|
||||||
content: const Text('Esto añadirá los favoritos, emisoras y presets del fichero. ¿Continuar?'),
|
title: const Text('Importar configuración'),
|
||||||
actions: [
|
content: const Text(
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancelar')),
|
'Esto añadirá los favoritos, emisoras y presets del fichero. ¿Continuar?',
|
||||||
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Importar')),
|
),
|
||||||
],
|
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 (confirmar != true) return;
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
await context.read<EstadoRadio>().importarConfig(json);
|
final estado = context.read<EstadoRadio>();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Configuración importada correctamente')));
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
await estado.importarConfig(json);
|
||||||
|
messenger.showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Configuración importada correctamente'),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (context.mounted) {
|
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: [
|
children: [
|
||||||
const Icon(Icons.backup_outlined),
|
const Icon(Icons.backup_outlined),
|
||||||
const SizedBox(width: 12),
|
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(
|
ListTile(
|
||||||
@@ -338,7 +405,9 @@ class _SeccionBackup extends StatelessWidget {
|
|||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
leading: const Icon(Icons.download_outlined),
|
leading: const Icon(Icons.download_outlined),
|
||||||
title: const Text('Importar configuración'),
|
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),
|
onTap: () => _importar(context),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -353,41 +422,49 @@ class _SeccionInfo extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<EstadoRadio>(
|
return Consumer<EstadoRadio>(
|
||||||
builder: (ctx, estado, _) => PluriGlassSurface(
|
builder:
|
||||||
child: Column(
|
(ctx, estado, _) => PluriGlassSurface(
|
||||||
children: [
|
child: Column(
|
||||||
const ListTile(
|
children: [
|
||||||
contentPadding: EdgeInsets.zero,
|
const ListTile(
|
||||||
leading: PluriIcon(glyph: PluriIconGlyph.settings, variant: PluriIconVariant.filled),
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text('PluriWave'),
|
leading: PluriIcon(
|
||||||
subtitle: Text('v0.3.0 — Radio mundial'),
|
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:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../estado/estado_radio.dart';
|
import '../estado/estado_radio.dart';
|
||||||
import '../tema/pluriwave_theme.dart';
|
|
||||||
import '../widgets/pluri_glass_surface.dart';
|
import '../widgets/pluri_glass_surface.dart';
|
||||||
import '../widgets/pluri_icon.dart';
|
import '../widgets/pluri_icon.dart';
|
||||||
import '../widgets/tarjeta_emisora.dart';
|
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
|
||||||
|
|
||||||
const _paises = [
|
const _paises = [
|
||||||
('España', 'ES'), ('USA', 'US'), ('México', 'MX'), ('Argentina', 'AR'),
|
('España', 'ES'),
|
||||||
('UK', 'GB'), ('Francia', 'FR'), ('Alemania', 'DE'), ('Italia', 'IT'),
|
('USA', 'US'),
|
||||||
('Brasil', 'BR'), ('Japón', 'JP'),
|
('México', 'MX'),
|
||||||
|
('Argentina', 'AR'),
|
||||||
|
('UK', 'GB'),
|
||||||
|
('Francia', 'FR'),
|
||||||
|
('Alemania', 'DE'),
|
||||||
|
('Italia', 'IT'),
|
||||||
|
('Brasil', 'BR'),
|
||||||
|
('Japón', 'JP'),
|
||||||
];
|
];
|
||||||
|
|
||||||
const _idiomas = [
|
const _idiomas = [
|
||||||
'spanish', 'english', 'french', 'german', 'portuguese',
|
'spanish',
|
||||||
'italian', 'japanese', 'arabic', 'russian',
|
'english',
|
||||||
|
'french',
|
||||||
|
'german',
|
||||||
|
'portuguese',
|
||||||
|
'italian',
|
||||||
|
'japanese',
|
||||||
|
'arabic',
|
||||||
|
'russian',
|
||||||
];
|
];
|
||||||
|
|
||||||
class PantallaBuscar extends StatefulWidget {
|
class PantallaBuscar extends StatefulWidget {
|
||||||
@@ -60,7 +73,10 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
|||||||
child: SearchBar(
|
child: SearchBar(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
hintText: 'Nombre de la emisora...',
|
hintText: 'Nombre de la emisora...',
|
||||||
leading: const PluriIcon(glyph: PluriIconGlyph.search, variant: PluriIconVariant.filled),
|
leading: const PluriIcon(
|
||||||
|
glyph: PluriIconGlyph.search,
|
||||||
|
variant: PluriIconVariant.filled,
|
||||||
|
),
|
||||||
trailing: [
|
trailing: [
|
||||||
if (_controller.text.isNotEmpty)
|
if (_controller.text.isNotEmpty)
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -77,14 +93,24 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_seccionFiltro('País', _paises.map((p) => (p.$1, p.$2)).toList(), _paisSeleccionado, (v) {
|
_seccionFiltro(
|
||||||
setState(() => _paisSeleccionado = v);
|
'País',
|
||||||
_buscar();
|
_paises.map((p) => (p.$1, p.$2)).toList(),
|
||||||
}),
|
_paisSeleccionado,
|
||||||
_seccionFiltro('Idioma', _idiomas.map((i) => (i, i)).toList(), _idiomaSeleccionado, (v) {
|
(v) {
|
||||||
setState(() => _idiomaSeleccionado = v);
|
setState(() => _paisSeleccionado = v);
|
||||||
_buscar();
|
_buscar();
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
|
_seccionFiltro(
|
||||||
|
'Idioma',
|
||||||
|
_idiomas.map((i) => (i, i)).toList(),
|
||||||
|
_idiomaSeleccionado,
|
||||||
|
(v) {
|
||||||
|
setState(() => _idiomaSeleccionado = v);
|
||||||
|
_buscar();
|
||||||
|
},
|
||||||
|
),
|
||||||
Expanded(child: _resultados(estado, theme)),
|
Expanded(child: _resultados(estado, theme)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -138,15 +164,25 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
|||||||
final resultados = estado.resultadosBusqueda;
|
final resultados = estado.resultadosBusqueda;
|
||||||
|
|
||||||
if (resultados.isEmpty) {
|
if (resultados.isEmpty) {
|
||||||
final sinFiltros = _controller.text.isEmpty && _paisSeleccionado == null && _idiomaSeleccionado == null;
|
final sinFiltros =
|
||||||
|
_controller.text.isEmpty &&
|
||||||
|
_paisSeleccionado == null &&
|
||||||
|
_idiomaSeleccionado == null;
|
||||||
return Center(
|
return Center(
|
||||||
child: PluriGlassSurface(
|
child: PluriGlassSurface(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
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),
|
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),
|
padding: const EdgeInsets.all(16),
|
||||||
itemCount: resultados.length,
|
itemCount: resultados.length,
|
||||||
separatorBuilder: (_, __) => const SizedBox(height: 6),
|
separatorBuilder: (_, __) => const SizedBox(height: 6),
|
||||||
itemBuilder: (context, i) => TarjetaEmisora(
|
itemBuilder:
|
||||||
emisora: resultados[i],
|
(context, i) => TarjetaEmisora(
|
||||||
esCompacta: true,
|
emisora: resultados[i],
|
||||||
onTap: () => context.read<EstadoRadio>().reproducir(resultados[i]),
|
esCompacta: true,
|
||||||
).animate().fadeIn(delay: (i * 20).ms),
|
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 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../estado/estado_radio.dart';
|
import '../estado/estado_radio.dart';
|
||||||
import '../tema/pluriwave_theme.dart';
|
|
||||||
import '../widgets/pluri_glass_surface.dart';
|
import '../widgets/pluri_glass_surface.dart';
|
||||||
import '../widgets/pluri_icon.dart';
|
import '../widgets/pluri_icon.dart';
|
||||||
import '../widgets/tarjeta_emisora.dart';
|
import 'package:pluriwave/widgets/tarjeta_emisora.dart';
|
||||||
|
|
||||||
class PantallaFavoritos extends StatelessWidget {
|
class PantallaFavoritos extends StatelessWidget {
|
||||||
const PantallaFavoritos({super.key});
|
const PantallaFavoritos({super.key});
|
||||||
@@ -22,13 +21,19 @@ class PantallaFavoritos extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
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),
|
const SizedBox(height: 16),
|
||||||
Text('Sin favoritos aún', style: theme.textTheme.titleMedium),
|
Text('Sin favoritos aún', style: theme.textTheme.titleMedium),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Tocá ♥ en cualquier emisora para guardarla',
|
'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(
|
return ReorderableListView.builder(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
onReorder: (oldIndex, newIndex) async {
|
onReorderItem: (oldIndex, newIndex) async {
|
||||||
if (newIndex > oldIndex) newIndex--;
|
|
||||||
final emisora = favoritos[oldIndex];
|
final emisora = favoritos[oldIndex];
|
||||||
await estado.favoritos.reordenar(emisora.uuid, newIndex);
|
await estado.favoritos.reordenar(emisora.uuid, newIndex);
|
||||||
await estado.cargarFavoritos();
|
await estado.cargarFavoritos();
|
||||||
@@ -72,13 +76,16 @@ class PantallaFavoritos extends StatelessWidget {
|
|||||||
await estado.cargarFavoritos();
|
await estado.cargarFavoritos();
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
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:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shimmer/shimmer.dart' as shimmer;
|
import 'package:shimmer/shimmer.dart' as shimmer;
|
||||||
@@ -7,7 +7,7 @@ import '../estado/estado_radio.dart';
|
|||||||
import '../tema/pluriwave_theme.dart';
|
import '../tema/pluriwave_theme.dart';
|
||||||
import '../widgets/pluri_glass_surface.dart';
|
import '../widgets/pluri_glass_surface.dart';
|
||||||
import '../widgets/pluri_icon.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.
|
/// Pantalla principal: emisoras populares y por género.
|
||||||
class PantallaInicio extends StatefulWidget {
|
class PantallaInicio extends StatefulWidget {
|
||||||
@@ -19,8 +19,18 @@ class PantallaInicio extends StatefulWidget {
|
|||||||
|
|
||||||
class _PantallaInicioState extends State<PantallaInicio> {
|
class _PantallaInicioState extends State<PantallaInicio> {
|
||||||
static const _generos = [
|
static const _generos = [
|
||||||
'pop', 'rock', 'jazz', 'classical', 'electronic', 'news',
|
'pop',
|
||||||
'talk', 'hip-hop', 'country', 'metal', 'reggae', 'latin',
|
'rock',
|
||||||
|
'jazz',
|
||||||
|
'classical',
|
||||||
|
'electronic',
|
||||||
|
'news',
|
||||||
|
'talk',
|
||||||
|
'hip-hop',
|
||||||
|
'country',
|
||||||
|
'metal',
|
||||||
|
'reggae',
|
||||||
|
'latin',
|
||||||
];
|
];
|
||||||
String? _generoSeleccionado;
|
String? _generoSeleccionado;
|
||||||
|
|
||||||
@@ -37,7 +47,8 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
|||||||
SliverToBoxAdapter(child: _heroHeader(context)),
|
SliverToBoxAdapter(child: _heroHeader(context)),
|
||||||
SliverToBoxAdapter(child: _seccionTendencias(estado, theme)),
|
SliverToBoxAdapter(child: _seccionTendencias(estado, theme)),
|
||||||
SliverToBoxAdapter(child: _chipGeneros(context, 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(
|
SliverPadding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: t.spacingMd),
|
padding: EdgeInsets.symmetric(horizontal: t.spacingMd),
|
||||||
sliver: _gridEmisoras(estado),
|
sliver: _gridEmisoras(estado),
|
||||||
@@ -51,16 +62,33 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
|||||||
final t = context.pluriTokens;
|
final t = context.pluriTokens;
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return Padding(
|
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(
|
child: PluriGlassSurface(
|
||||||
borderRadius: BorderRadius.circular(t.radiusLg),
|
borderRadius: BorderRadius.circular(t.radiusLg),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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),
|
const SizedBox(height: 10),
|
||||||
Text('PluriWave', style: theme.textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.w700)),
|
Text(
|
||||||
Text('Ondas vivas globales', style: theme.textTheme.titleMedium?.copyWith(color: t.warmCoral)),
|
'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),
|
const SizedBox(height: 8),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 56,
|
height: 56,
|
||||||
child: estado.cargandoPopulares
|
child:
|
||||||
? ListView.separated(
|
estado.cargandoPopulares
|
||||||
scrollDirection: Axis.horizontal,
|
? ListView.separated(
|
||||||
itemCount: 5,
|
scrollDirection: Axis.horizontal,
|
||||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
itemCount: 5,
|
||||||
itemBuilder: (_, __) => _ChipShimmer(theme: theme),
|
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||||
)
|
itemBuilder: (_, __) => _ChipShimmer(theme: theme),
|
||||||
: ListView.separated(
|
)
|
||||||
scrollDirection: Axis.horizontal,
|
: ListView.separated(
|
||||||
itemCount: estado.tendencias.length,
|
scrollDirection: Axis.horizontal,
|
||||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
itemCount: estado.tendencias.length,
|
||||||
itemBuilder: (context, i) {
|
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||||
final e = estado.tendencias[i];
|
itemBuilder: (context, i) {
|
||||||
return ActionChip(
|
final e = estado.tendencias[i];
|
||||||
avatar: const Icon(Icons.graphic_eq_rounded, size: 18),
|
return ActionChip(
|
||||||
label: Text(e.nombre, maxLines: 1),
|
avatar: const Icon(
|
||||||
onPressed: () => context.read<EstadoRadio>().reproducir(e),
|
Icons.graphic_eq_rounded,
|
||||||
).animate().fadeIn(delay: (i * 50).ms);
|
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(
|
Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 4,
|
runSpacing: 4,
|
||||||
children: _generos.map((g) {
|
children:
|
||||||
final seleccionado = _generoSeleccionado == g;
|
_generos.map((g) {
|
||||||
return FilterChip(
|
final seleccionado = _generoSeleccionado == g;
|
||||||
label: Text(g),
|
return FilterChip(
|
||||||
selected: seleccionado,
|
label: Text(g),
|
||||||
onSelected: (_) {
|
selected: seleccionado,
|
||||||
setState(() {
|
onSelected: (_) {
|
||||||
_generoSeleccionado = seleccionado ? null : g;
|
setState(() {
|
||||||
});
|
_generoSeleccionado = seleccionado ? null : g;
|
||||||
if (!seleccionado) {
|
});
|
||||||
context.read<EstadoRadio>().buscar(tag: g);
|
if (!seleccionado) {
|
||||||
} else {
|
context.read<EstadoRadio>().buscar(tag: g);
|
||||||
context.read<EstadoRadio>().cargarPopulares();
|
} else {
|
||||||
}
|
context.read<EstadoRadio>().cargarPopulares();
|
||||||
},
|
}
|
||||||
);
|
},
|
||||||
}).toList(),
|
);
|
||||||
|
}).toList(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -153,7 +187,10 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
|||||||
Icon(Icons.wifi_off, color: theme.colorScheme.error),
|
Icon(Icons.wifi_off, color: theme.colorScheme.error),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(child: Text(estado.error!)),
|
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) {
|
Widget _gridEmisoras(EstadoRadio estado) {
|
||||||
final emisoras = _generoSeleccionado != null ? estado.resultadosBusqueda : estado.emisorasInicio;
|
final emisoras =
|
||||||
final cargando = estado.cargandoPopulares || (_generoSeleccionado != null && estado.cargandoBusqueda);
|
_generoSeleccionado != null
|
||||||
|
? estado.resultadosBusqueda
|
||||||
|
: estado.emisorasInicio;
|
||||||
|
final cargando =
|
||||||
|
estado.cargandoPopulares ||
|
||||||
|
(_generoSeleccionado != null && estado.cargandoBusqueda);
|
||||||
|
|
||||||
if (cargando) {
|
if (cargando) {
|
||||||
return SliverGrid(
|
return SliverGrid(
|
||||||
delegate: SliverChildBuilderDelegate((_, __) => const TarjetaEmisoraShimmer(), childCount: 12),
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(_, __) => const TarjetaEmisoraShimmer(),
|
||||||
|
childCount: 12,
|
||||||
|
),
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 2,
|
crossAxisCount: 2,
|
||||||
childAspectRatio: 0.85,
|
childAspectRatio: 0.85,
|
||||||
@@ -177,7 +222,9 @@ class _PantallaInicioState extends State<PantallaInicio> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (emisoras.isEmpty) {
|
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(
|
return SliverGrid(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import 'pluri_glass_surface.dart';
|
|||||||
import 'pluri_icon.dart';
|
import 'pluri_icon.dart';
|
||||||
import 'visualizador_audio.dart';
|
import 'visualizador_audio.dart';
|
||||||
|
|
||||||
/// Barra inferior persistente con controles básicos de reproducción.
|
/// Barra inferior persistente con controles básicos de reproducción.
|
||||||
/// Toca la barra para abrir PantallaReproductor completa.
|
/// Toca la barra para abrir PantallaReproductor completa.
|
||||||
class MiniReproductor extends StatelessWidget {
|
class MiniReproductor extends StatelessWidget {
|
||||||
const MiniReproductor({super.key});
|
const MiniReproductor({super.key});
|
||||||
@@ -26,9 +26,17 @@ class MiniReproductor extends StatelessWidget {
|
|||||||
return SafeArea(
|
return SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
child: Padding(
|
child: 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(
|
child: PluriGlassSurface(
|
||||||
padding: EdgeInsets.symmetric(horizontal: t.spacingSm, vertical: t.spacingXs),
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: t.spacingSm,
|
||||||
|
vertical: t.spacingXs,
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(999),
|
borderRadius: BorderRadius.circular(999),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -67,27 +75,37 @@ class MiniReproductor extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
emisora.nombre,
|
emisora.nombre,
|
||||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
style: Theme.of(context)
|
||||||
fontWeight: FontWeight.w700,
|
.textTheme
|
||||||
),
|
.titleSmall
|
||||||
|
?.copyWith(fontWeight: FontWeight.w700),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
StreamBuilder<EstadoReproduccion>(
|
StreamBuilder<EstadoReproduccion>(
|
||||||
stream: estado.estadoStream,
|
stream: estado.estadoStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final s = snapshot.data ?? EstadoReproduccion.detenido;
|
final s =
|
||||||
final activo = s == EstadoReproduccion.reproduciendo;
|
snapshot.data ??
|
||||||
|
EstadoReproduccion.detenido;
|
||||||
|
final activo =
|
||||||
|
s == EstadoReproduccion.reproduciendo;
|
||||||
return Text(
|
return Text(
|
||||||
_labelEstado(s),
|
_labelEstado(s),
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(
|
||||||
color: activo
|
context,
|
||||||
? t.warmCoral
|
).textTheme.bodySmall?.copyWith(
|
||||||
: Theme.of(context)
|
color:
|
||||||
.colorScheme
|
activo
|
||||||
.onSurface
|
? t.warmCoral
|
||||||
.withValues(alpha: 0.7),
|
: Theme.of(context)
|
||||||
fontWeight: activo ? FontWeight.w600 : FontWeight.w400,
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withValues(alpha: 0.7),
|
||||||
|
fontWeight:
|
||||||
|
activo
|
||||||
|
? FontWeight.w600
|
||||||
|
: FontWeight.w400,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -128,16 +146,28 @@ class MiniReproductor extends StatelessWidget {
|
|||||||
return IconButton(
|
return IconButton(
|
||||||
tooltip: 'Reintentar',
|
tooltip: 'Reintentar',
|
||||||
icon: const Icon(Icons.refresh_rounded),
|
icon: const Icon(Icons.refresh_rounded),
|
||||||
onPressed: emisoraActual != null ? () => estado.reproducir(emisoraActual) : null,
|
onPressed:
|
||||||
constraints: const BoxConstraints.tightFor(width: 48, height: 48),
|
emisoraActual != null
|
||||||
|
? () => estado.reproducir(emisoraActual)
|
||||||
|
: null,
|
||||||
|
constraints: const BoxConstraints.tightFor(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Semantics(
|
return Semantics(
|
||||||
button: true,
|
button: true,
|
||||||
label: s == EstadoReproduccion.reproduciendo ? 'Pausar' : 'Reproducir',
|
label:
|
||||||
|
s == EstadoReproduccion.reproduciendo
|
||||||
|
? 'Pausar'
|
||||||
|
: 'Reproducir',
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
tooltip: s == EstadoReproduccion.reproduciendo ? 'Pausar' : 'Reproducir',
|
tooltip:
|
||||||
|
s == EstadoReproduccion.reproduciendo
|
||||||
|
? 'Pausar'
|
||||||
|
: 'Reproducir',
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
s == EstadoReproduccion.reproduciendo
|
s == EstadoReproduccion.reproduciendo
|
||||||
? Icons.pause_circle_filled_rounded
|
? Icons.pause_circle_filled_rounded
|
||||||
@@ -145,7 +175,10 @@ class MiniReproductor extends StatelessWidget {
|
|||||||
color: t.electricMagenta,
|
color: t.electricMagenta,
|
||||||
),
|
),
|
||||||
onPressed: estado.togglePlay,
|
onPressed: estado.togglePlay,
|
||||||
constraints: const BoxConstraints.tightFor(width: 48, height: 48),
|
constraints: const BoxConstraints.tightFor(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -162,7 +195,7 @@ class MiniReproductor extends StatelessWidget {
|
|||||||
EstadoReproduccion.cargando => 'Conectando...',
|
EstadoReproduccion.cargando => 'Conectando...',
|
||||||
EstadoReproduccion.reproduciendo => 'En directo',
|
EstadoReproduccion.reproduciendo => 'En directo',
|
||||||
EstadoReproduccion.pausado => 'Pausado',
|
EstadoReproduccion.pausado => 'Pausado',
|
||||||
EstadoReproduccion.error => 'Error de conexión',
|
EstadoReproduccion.error => 'Error de conexión',
|
||||||
EstadoReproduccion.detenido => 'Detenido',
|
EstadoReproduccion.detenido => 'Detenido',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import 'pluri_glass_surface.dart';
|
|||||||
import 'pluri_icon.dart';
|
import 'pluri_icon.dart';
|
||||||
|
|
||||||
/// Tarjeta compacta para mostrar una emisora en listas y grids.
|
/// Tarjeta compacta para mostrar una emisora en listas y grids.
|
||||||
/// Incluye botón de favorito visible en ambos modos.
|
/// Incluye botón de favorito visible en ambos modos.
|
||||||
class TarjetaEmisora extends StatefulWidget {
|
class TarjetaEmisora extends StatefulWidget {
|
||||||
final Emisora emisora;
|
final Emisora emisora;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
@@ -37,12 +37,16 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
|
|||||||
final esFav = await estado.toggleFavorito(widget.emisora);
|
final esFav = await estado.toggleFavorito(widget.emisora);
|
||||||
if (mounted) setState(() => _toggling = false);
|
if (mounted) setState(() => _toggling = false);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text(esFav
|
SnackBar(
|
||||||
? '${widget.emisora.nombre} añadida a favoritos'
|
content: Text(
|
||||||
: '${widget.emisora.nombre} eliminada de favoritos'),
|
esFav
|
||||||
duration: const Duration(seconds: 2),
|
? '${widget.emisora.nombre} añadida a favoritos'
|
||||||
));
|
: '${widget.emisora.nombre} eliminada de favoritos',
|
||||||
|
),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +58,9 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
|
|||||||
label: 'Emisora ${widget.emisora.nombre}',
|
label: 'Emisora ${widget.emisora.nombre}',
|
||||||
child: PluriGlassSurface(
|
child: PluriGlassSurface(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
borderRadius: BorderRadius.circular(widget.esCompacta ? t.radiusMd : t.radiusLg),
|
borderRadius: BorderRadius.circular(
|
||||||
|
widget.esCompacta ? t.radiusMd : t.radiusLg,
|
||||||
|
),
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
@@ -73,12 +79,14 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
AspectRatio(
|
AspectRatio(aspectRatio: 1, child: _logo(60)),
|
||||||
aspectRatio: 1,
|
|
||||||
child: _logo(60),
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(t.spacingMd, t.spacingSm, t.spacingMd, t.spacingMd),
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
t.spacingMd,
|
||||||
|
t.spacingSm,
|
||||||
|
t.spacingMd,
|
||||||
|
t.spacingMd,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -96,10 +104,9 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
widget.emisora.pais!,
|
widget.emisora.pais!,
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(context)
|
color: Theme.of(
|
||||||
.colorScheme
|
context,
|
||||||
.onSurface
|
).colorScheme.onSurface.withValues(alpha: 0.72),
|
||||||
.withValues(alpha: 0.72),
|
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@@ -121,12 +128,16 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
|
|||||||
|
|
||||||
Widget _buildCompacta() {
|
Widget _buildCompacta() {
|
||||||
final t = context.pluriTokens;
|
final t = context.pluriTokens;
|
||||||
final subtitulo = [widget.emisora.pais, widget.emisora.idioma]
|
final subtitulo = [
|
||||||
.where((s) => s != null && s.isNotEmpty)
|
widget.emisora.pais,
|
||||||
.join(' · ');
|
widget.emisora.idioma,
|
||||||
|
].where((s) => s != null && s.isNotEmpty).join(' · ');
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: t.spacingSm, vertical: t.spacingXs),
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: t.spacingSm,
|
||||||
|
vertical: t.spacingXs,
|
||||||
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
@@ -141,7 +152,9 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
widget.emisora.nombre,
|
widget.emisora.nombre,
|
||||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700),
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@@ -150,10 +163,11 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
|
|||||||
subtitulo,
|
subtitulo,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: Theme.of(context)
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
.textTheme
|
color: Theme.of(
|
||||||
.bodySmall
|
context,
|
||||||
?.copyWith(color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.72)),
|
).colorScheme.onSurface.withValues(alpha: 0.72),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -167,26 +181,37 @@ class _TarjetaEmisoraState extends State<TarjetaEmisora> {
|
|||||||
Widget _botonFavorito({required bool mini}) {
|
Widget _botonFavorito({required bool mini}) {
|
||||||
final t = context.pluriTokens;
|
final t = context.pluriTokens;
|
||||||
final esFavorito = context.select<EstadoRadio, bool>(
|
final esFavorito = context.select<EstadoRadio, bool>(
|
||||||
(estado) => estado.listaFavoritos.any((e) => e.uuid == widget.emisora.uuid),
|
(estado) =>
|
||||||
|
estado.listaFavoritos.any((e) => e.uuid == widget.emisora.uuid),
|
||||||
);
|
);
|
||||||
|
|
||||||
final icono = mini
|
final icono =
|
||||||
? Icon(
|
mini
|
||||||
esFavorito ? Icons.favorite_rounded : Icons.favorite_outline_rounded,
|
? Icon(
|
||||||
color: esFavorito ? t.warmCoral : Colors.white.withValues(alpha: 0.82),
|
esFavorito
|
||||||
size: 18,
|
? Icons.favorite_rounded
|
||||||
)
|
: Icons.favorite_outline_rounded,
|
||||||
: PluriIcon(
|
color:
|
||||||
glyph: PluriIconGlyph.favorites,
|
esFavorito
|
||||||
variant: esFavorito ? PluriIconVariant.activeGlow : PluriIconVariant.outline,
|
? t.warmCoral
|
||||||
size: 20,
|
: Colors.white.withValues(alpha: 0.82),
|
||||||
semanticLabel: esFavorito ? 'Quitar de favoritos' : 'Añadir a favoritos',
|
size: 18,
|
||||||
);
|
)
|
||||||
|
: PluriIcon(
|
||||||
|
glyph: PluriIconGlyph.favorites,
|
||||||
|
variant:
|
||||||
|
esFavorito
|
||||||
|
? PluriIconVariant.activeGlow
|
||||||
|
: PluriIconVariant.outline,
|
||||||
|
size: 20,
|
||||||
|
semanticLabel:
|
||||||
|
esFavorito ? 'Quitar de favoritos' : 'Añadir a favoritos',
|
||||||
|
);
|
||||||
|
|
||||||
return Semantics(
|
return Semantics(
|
||||||
button: true,
|
button: true,
|
||||||
toggled: esFavorito,
|
toggled: esFavorito,
|
||||||
label: esFavorito ? 'Quitar de favoritos' : 'Añadir a favoritos',
|
label: esFavorito ? 'Quitar de favoritos' : 'Añadir a favoritos',
|
||||||
child: Material(
|
child: Material(
|
||||||
color: mini ? t.glassSurface : Colors.transparent,
|
color: mini ? t.glassSurface : Colors.transparent,
|
||||||
shape: const CircleBorder(),
|
shape: const CircleBorder(),
|
||||||
@@ -264,9 +289,16 @@ class TarjetaEmisoraShimmer extends StatelessWidget {
|
|||||||
child: Container(color: theme.colorScheme.surfaceContainerHighest),
|
child: Container(color: theme.colorScheme.surfaceContainerHighest),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Container(height: 14, color: theme.colorScheme.surfaceContainerHighest),
|
Container(
|
||||||
|
height: 14,
|
||||||
|
color: theme.colorScheme.surfaceContainerHighest,
|
||||||
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Container(height: 12, width: 60, color: theme.colorScheme.surfaceContainerHighest),
|
Container(
|
||||||
|
height: 12,
|
||||||
|
width: 60,
|
||||||
|
color: theme.colorScheme.surfaceContainerHighest,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
+7
-7
@@ -353,10 +353,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.18"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -369,10 +369,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.18.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -414,7 +414,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
@@ -686,10 +686,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.9"
|
version: "0.7.11"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ dependencies:
|
|||||||
http: ^1.2.2
|
http: ^1.2.2
|
||||||
|
|
||||||
# Utils
|
# Utils
|
||||||
|
path: ^1.9.1
|
||||||
share_plus: ^10.1.3
|
share_plus: ^10.1.3
|
||||||
file_picker: ^8.1.7
|
file_picker: ^8.1.7
|
||||||
uuid: ^4.5.1
|
uuid: ^4.5.1
|
||||||
|
|||||||
Reference in New Issue
Block a user