275 lines
8.6 KiB
Dart
275 lines
8.6 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';
|
|
import '../tema/pluriwave_theme.dart';
|
|
import 'pluri_glass_surface.dart';
|
|
import 'pluri_icon.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 _toggling = false;
|
|
|
|
Future<void> _toggle() async {
|
|
if (_toggling) return;
|
|
setState(() => _toggling = true);
|
|
final estado = context.read<EstadoRadio>();
|
|
final esFav = await estado.toggleFavorito(widget.emisora);
|
|
if (mounted) setState(() => _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 t = context.pluriTokens;
|
|
return Semantics(
|
|
button: widget.onTap != null,
|
|
label: 'Emisora ${widget.emisora.nombre}',
|
|
child: PluriGlassSurface(
|
|
padding: EdgeInsets.zero,
|
|
borderRadius: BorderRadius.circular(widget.esCompacta ? t.radiusMd : t.radiusLg),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: widget.onTap,
|
|
child: widget.esCompacta ? _buildCompacta() : _buildCompleta(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCompleta() {
|
|
final t = context.pluriTokens;
|
|
return Stack(
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
AspectRatio(
|
|
aspectRatio: 1,
|
|
child: _logo(60),
|
|
),
|
|
Padding(
|
|
padding: EdgeInsets.fromLTRB(t.spacingMd, t.spacingSm, t.spacingMd, t.spacingMd),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
widget.emisora.nombre,
|
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
if (widget.emisora.pais != null)
|
|
Padding(
|
|
padding: EdgeInsets.only(top: t.spacingXs),
|
|
child: Text(
|
|
widget.emisora.pais!,
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.onSurface
|
|
.withValues(alpha: 0.72),
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Positioned(
|
|
top: t.spacingSm,
|
|
right: t.spacingSm,
|
|
child: _botonFavorito(mini: true),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildCompacta() {
|
|
final t = context.pluriTokens;
|
|
final subtitulo = [widget.emisora.pais, widget.emisora.idioma]
|
|
.where((s) => s != null && s.isNotEmpty)
|
|
.join(' · ');
|
|
|
|
return Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: t.spacingSm, vertical: t.spacingXs),
|
|
child: Row(
|
|
children: [
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(t.radiusSm),
|
|
child: SizedBox(width: 48, height: 48, child: _logo(24)),
|
|
),
|
|
SizedBox(width: t.spacingSm),
|
|
Expanded(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
widget.emisora.nombre,
|
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
if (subtitulo.isNotEmpty)
|
|
Text(
|
|
subtitulo,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.bodySmall
|
|
?.copyWith(color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.72)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
_botonFavorito(mini: false),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _botonFavorito({required bool mini}) {
|
|
final t = context.pluriTokens;
|
|
final esFavorito = context.select<EstadoRadio, bool>(
|
|
(estado) => estado.listaFavoritos.any((e) => e.uuid == widget.emisora.uuid),
|
|
);
|
|
|
|
final icono = mini
|
|
? Icon(
|
|
esFavorito ? Icons.favorite_rounded : Icons.favorite_outline_rounded,
|
|
color: esFavorito ? t.warmCoral : Colors.white.withValues(alpha: 0.82),
|
|
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(
|
|
button: true,
|
|
toggled: esFavorito,
|
|
label: esFavorito ? 'Quitar de favoritos' : 'Añadir a favoritos',
|
|
child: Material(
|
|
color: mini ? t.glassSurface : Colors.transparent,
|
|
shape: const CircleBorder(),
|
|
child: InkWell(
|
|
customBorder: const CircleBorder(),
|
|
onTap: _toggling ? null : _toggle,
|
|
child: SizedBox(
|
|
width: mini ? 36 : 44,
|
|
height: mini ? 36 : 44,
|
|
child: Center(child: icono),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _logo(double iconSize) {
|
|
if (widget.emisora.favicon != null && widget.emisora.favicon!.isNotEmpty) {
|
|
return CachedNetworkImage(
|
|
imageUrl: widget.emisora.favicon!,
|
|
fit: BoxFit.cover,
|
|
placeholder: (_, __) => _shimmer(),
|
|
errorWidget: (_, __, ___) => _iconoFallback(iconSize),
|
|
);
|
|
}
|
|
return _iconoFallback(iconSize);
|
|
}
|
|
|
|
Widget _shimmer() {
|
|
final theme = Theme.of(context);
|
|
return Shimmer.fromColors(
|
|
baseColor: theme.colorScheme.surfaceContainerHighest,
|
|
highlightColor: theme.colorScheme.surface,
|
|
child: Container(color: theme.colorScheme.surfaceContainerHighest),
|
|
);
|
|
}
|
|
|
|
Widget _iconoFallback(double size) {
|
|
final t = context.pluriTokens;
|
|
return DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [t.deepViolet, t.electricMagenta.withValues(alpha: 0.8)],
|
|
),
|
|
),
|
|
child: Center(
|
|
child: PluriIcon(
|
|
glyph: PluriIconGlyph.player,
|
|
variant: PluriIconVariant.filled,
|
|
size: size,
|
|
semanticLabel: 'Icono de emisora',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|