import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shimmer/shimmer.dart' as shimmer; import '../estado/estado_radio.dart'; import '../l10n/display_names.dart'; import '../l10n/gen/app_localizations.dart'; import '../modelos/emisora.dart'; import '../tema/pluriwave_tokens.dart'; /// Item 23 / audit 4.3 + 6.5 (t4:226-232, 302-306): a flat, background-less /// station row — square thumbnail, name, meta line, and a caller-supplied /// set of trailing actions. Shared by Favoritos and search results, which /// the prototype draws identically apart from their trailing actions. /// Grabaciones (12.4) has its own row: a different domain model /// (recordings, not stations) with different trailing actions. class FilaEmisoraPlana extends StatelessWidget { const FilaEmisoraPlana({ super.key, required this.emisora, required this.meta, this.leading, this.trailing = const [], this.onTap, }); final Emisora emisora; /// Pre-joined meta line (e.g. "genre - country - kbps"). Omitted from /// layout entirely when empty, rather than reserving blank space. final String meta; /// e.g. a drag handle (Favoritos only — search results have none). final Widget? leading; /// e.g. a favourite toggle, a circular play button, an overflow menu. final List trailing; final VoidCallback? onTap; /// t4:226, t4:302: 48x48, radius 12. static const double lado = 48; static const double radio = 12; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final stationName = localizedStationName(l10n, emisora.nombre); return Padding( padding: const EdgeInsets.all(8), child: Row( children: [ if (leading != null) ...[leading!, const SizedBox(width: 4)], ClipRRect( key: const ValueKey('fila-emisora-plana-arte'), borderRadius: BorderRadius.circular(radio), child: SizedBox( width: lado, height: lado, child: _ArteFilaEmisora(emisora: emisora), ), ), const SizedBox(width: 12), Expanded( child: Material( type: MaterialType.transparency, child: InkWell( borderRadius: BorderRadius.circular(14), onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // t4:229, t4:303: 15px/w700/lh1.25. Text( stationName, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w700, height: 1.25, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), if (meta.isNotEmpty) ...[ const SizedBox(height: 2), // t4:229, t4:303: 12px/rgba(242,247,250,.58). Text( meta, style: TextStyle( fontSize: 12, color: Theme.of( context, ).colorScheme.onSurface.withValues(alpha: 0.58), ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ], ), ), ), ), ), ...trailing, ], ), ); } } /// t4:230, t4:305: a 44x44 circular affordance, `rgba(33,212,217,.14)` /// background, `play_arrow` at 22px brand teal. class BotonReproducirCircular extends StatelessWidget { const BotonReproducirCircular({ super.key, required this.onPressed, this.tooltip, }); final VoidCallback? onPressed; final String? tooltip; @override Widget build(BuildContext context) { return Material( key: const ValueKey('boton-reproducir-circular'), color: PluriWaveTokens.brand.withValues(alpha: 0.14), shape: const CircleBorder(), child: IconButton( tooltip: tooltip, icon: const Icon( Icons.play_arrow_rounded, size: 22, color: PluriWaveTokens.brand, ), onPressed: onPressed, constraints: const BoxConstraints.tightFor(width: 44, height: 44), ), ); } } /// t4:304: a 44x44 favourite toggle, 22px icon at 55% opacity. Mirrors /// `TarjetaEmisora`'s own toggle+snackbar behaviour (duplicated rather than /// extracted from it — `tarjeta_emisora.dart`'s exact widget tree is pinned /// by several pre-existing tests this change must not disturb). class BotonFavoritoEmisora extends StatefulWidget { const BotonFavoritoEmisora({super.key, required this.emisora}); final Emisora emisora; @override State createState() => _BotonFavoritoEmisoraState(); } class _BotonFavoritoEmisoraState extends State { bool _toggling = false; Future _toggle() async { if (_toggling) return; setState(() => _toggling = true); final estado = context.read(); final esFav = await estado.toggleFavorito(widget.emisora); if (mounted) setState(() => _toggling = false); if (mounted) { final l10n = AppLocalizations.of(context); final stationName = localizedStationName(l10n, widget.emisora.nombre); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( esFav ? l10n.favoritesAddedMessage(stationName) : l10n.favoritesRemovedMessage(stationName), ), duration: const Duration(seconds: 2), ), ); } } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final esFavorito = context.select( (estado) => estado.listaFavoritos.any((e) => e.uuid == widget.emisora.uuid), ); return Semantics( container: true, button: true, toggled: esFavorito, label: esFavorito ? l10n.favoritesRemoveTooltip : l10n.favoritesAddTooltip, child: Material( key: const ValueKey('boton-favorito-emisora'), color: Colors.transparent, child: InkWell( customBorder: const CircleBorder(), onTap: _toggling ? null : _toggle, child: SizedBox( width: 44, height: 44, child: Icon( esFavorito ? Icons.favorite_rounded : Icons.favorite_outline_rounded, size: 22, color: Theme.of( context, ).colorScheme.onSurface.withValues(alpha: 0.55), ), ), ), ), ); } } class _ArteFilaEmisora extends StatelessWidget { const _ArteFilaEmisora({required this.emisora}); final Emisora emisora; @override Widget build(BuildContext context) { final theme = Theme.of(context); if (emisora.favicon != null && emisora.favicon!.isNotEmpty) { return CachedNetworkImage( imageUrl: emisora.favicon!, fit: BoxFit.cover, placeholder: (_, __) => _shimmer(theme), errorWidget: (_, __, ___) => _iconoFallback(theme), ); } return _iconoFallback(theme); } Widget _shimmer(ThemeData theme) => shimmer.Shimmer.fromColors( baseColor: theme.colorScheme.surfaceContainerHighest, highlightColor: theme.colorScheme.surface, child: Container(color: theme.colorScheme.surfaceContainerHighest), ); Widget _iconoFallback(ThemeData theme) => Container( color: theme.colorScheme.primaryContainer, child: Icon( Icons.radio_rounded, size: 22, color: theme.colorScheme.onPrimaryContainer, ), ); }