fix(ci): resolve premium UI analyzer errors
Build & Deploy Pluriwave / Build APK + AAB release (push) Has been skipped
Build & Deploy Pluriwave / Análisis de código (push) Failing after 9s

This commit is contained in:
2026-05-20 20:07:24 +02:00
parent c707fc9911
commit 9c51454d57
9 changed files with 535 additions and 287 deletions
+78 -64
View File
@@ -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(),
),
],
),
),
), ),
),
),
); );
} }
} }
+144 -67
View File
@@ -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),
),
],
),
),
); );
} }
} }
+62 -25
View File
@@ -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),
); );
} }
} }
+16 -9
View File
@@ -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',
),
),
); );
} }
}, },
), ),
], ],
),
), ),
), ),
); );
+98 -51
View File
@@ -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(
+55 -22
View File
@@ -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',
}; };
} }
+74 -42
View File
@@ -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
View File
@@ -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:
+1
View File
@@ -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