Files
pluriwave/lib/widgets/tarjeta_emisora.dart
ShanaiaBot 5fd3d6deb9
Some checks failed
Flutter CI/CD — PluriWave / Test + Build (push) Has been cancelled
feat(v0.3.0): ecualizador + favoritos en tarjeta + emisoras custom + export/import + fix MainActivity
- MainActivity: extiende AudioServiceActivity (fix pantalla en blanco)
- ServicioAudio: AndroidEqualizer en AudioPipeline, aplicarPreset(), setBanda()
- PresetEcualizador: modelo independiente (Flat/Rock/Pop/BassBoost/Jazz/Voz)
- EcualizadorWidget: 5 sliders verticales + PresetsEcualizadorWidget
- TarjetaEmisora: botón favorito visible en grid y lista (toggle con SnackBar)
- EstadoRadio: emisoras custom (CRUD), export/import JSON v1, presets por emisora
- PantallaAjustes: ecualizador interactivo, form añadir emisora, backup export/import
- pubspec: +file_picker ^8.1.7, +uuid ^4.5.1
2026-04-04 19:17:40 +02:00

211 lines
6.3 KiB
Dart

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
import '../estado/estado_radio.dart';
import '../modelos/emisora.dart';
/// Tarjeta compacta para mostrar una emisora en listas y grids.
/// Incluye botón de favorito visible en ambos modos.
class TarjetaEmisora extends StatefulWidget {
final Emisora emisora;
final VoidCallback? onTap;
final bool esCompacta;
const TarjetaEmisora({
super.key,
required this.emisora,
this.onTap,
this.esCompacta = false,
});
@override
State<TarjetaEmisora> createState() => _TarjetaEmisoraState();
}
class _TarjetaEmisoraState extends State<TarjetaEmisora> {
bool _esFavorito = false;
bool _toggling = false;
@override
void initState() {
super.initState();
_checkFavorito();
}
Future<void> _checkFavorito() async {
final fav = await context.read<EstadoRadio>().esFavorito(widget.emisora.uuid);
if (mounted) setState(() => _esFavorito = fav);
}
Future<void> _toggle() async {
if (_toggling) return;
_toggling = true;
final estado = context.read<EstadoRadio>();
final esFav = await estado.toggleFavorito(widget.emisora);
if (mounted) setState(() => _esFavorito = esFav);
_toggling = false;
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(esFav
? '${widget.emisora.nombre} añadida a favoritos'
: '${widget.emisora.nombre} eliminada de favoritos'),
duration: const Duration(seconds: 2),
));
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: widget.onTap,
child: widget.esCompacta
? _buildCompacta(theme)
: _buildCompleta(theme),
),
);
}
Widget _buildCompleta(ThemeData theme) {
return Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 1,
child: _logo(theme, 60),
),
Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.emisora.nombre,
style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (widget.emisora.pais != null)
Text(
widget.emisora.pais!,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
// Botón favorito superpuesto (esquina superior derecha)
Positioned(
top: 4,
right: 4,
child: _botonFavorito(theme, mini: true),
),
],
);
}
Widget _buildCompacta(ThemeData theme) {
return ListTile(
leading: SizedBox(width: 48, height: 48, child: _logo(theme, 24)),
title: Text(
widget.emisora.nombre,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
[widget.emisora.pais, widget.emisora.idioma]
.where((s) => s != null && s.isNotEmpty)
.join(' · '),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: _botonFavorito(theme, mini: false),
);
}
Widget _botonFavorito(ThemeData theme, {required bool mini}) {
return Material(
color: mini
? theme.colorScheme.surface.withValues(alpha: 0.8)
: Colors.transparent,
shape: const CircleBorder(),
child: InkWell(
customBorder: const CircleBorder(),
onTap: _toggle,
child: Padding(
padding: EdgeInsets.all(mini ? 6 : 4),
child: Icon(
_esFavorito ? Icons.favorite_rounded : Icons.favorite_outline_rounded,
color: _esFavorito ? theme.colorScheme.error : theme.colorScheme.onSurfaceVariant,
size: mini ? 18 : 22,
),
),
),
);
}
Widget _logo(ThemeData theme, double iconSize) {
if (widget.emisora.favicon != null && widget.emisora.favicon!.isNotEmpty) {
return CachedNetworkImage(
imageUrl: widget.emisora.favicon!,
fit: BoxFit.cover,
placeholder: (_, __) => _shimmer(theme),
errorWidget: (_, __, ___) => _iconoFallback(theme, iconSize),
);
}
return _iconoFallback(theme, iconSize);
}
Widget _shimmer(ThemeData theme) {
return Shimmer.fromColors(
baseColor: theme.colorScheme.surfaceContainerHighest,
highlightColor: theme.colorScheme.surface,
child: Container(color: theme.colorScheme.surfaceContainerHighest),
);
}
Widget _iconoFallback(ThemeData theme, double size) {
return Container(
color: theme.colorScheme.primaryContainer,
child: Icon(Icons.radio, size: size, color: theme.colorScheme.onPrimaryContainer),
);
}
}
/// Placeholder shimmer para listas en carga.
class TarjetaEmisoraShimmer extends StatelessWidget {
const TarjetaEmisoraShimmer({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Shimmer.fromColors(
baseColor: theme.colorScheme.surfaceContainerHighest,
highlightColor: theme.colorScheme.surface,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 1,
child: Container(color: theme.colorScheme.surfaceContainerHighest),
),
const SizedBox(height: 8),
Container(height: 14, color: theme.colorScheme.surfaceContainerHighest),
const SizedBox(height: 4),
Container(height: 12, width: 60, color: theme.colorScheme.surfaceContainerHighest),
],
),
);
}
}