feat(stations): add quality filters and list ordering
This commit is contained in:
@@ -62,6 +62,8 @@ class _AjustesContent extends StatelessWidget {
|
||||
SizedBox(height: 12),
|
||||
_SeccionIdioma(),
|
||||
SizedBox(height: 12),
|
||||
_SeccionOrdenListas(),
|
||||
SizedBox(height: 12),
|
||||
_SeccionEmisoraPreferida(),
|
||||
SizedBox(height: 12),
|
||||
_SeccionEmisoras(),
|
||||
@@ -641,6 +643,56 @@ class _SeccionEcualizador extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _SeccionOrdenListas extends StatelessWidget {
|
||||
const _SeccionOrdenListas();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final estado = context.watch<EstadoRadio>();
|
||||
return PluriGlassSurface(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.sort_rounded),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Orden de emisoras',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SegmentedButton<OrdenEmisoras>(
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: OrdenEmisoras.nombre,
|
||||
icon: Icon(Icons.sort_by_alpha_rounded),
|
||||
label: Text('Por nombre'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: OrdenEmisoras.calidad,
|
||||
icon: Icon(Icons.hd_rounded),
|
||||
label: Text('Por calidad'),
|
||||
),
|
||||
],
|
||||
selected: {estado.ordenListas},
|
||||
onSelectionChanged: (value) {
|
||||
estado.cambiarOrdenListas(value.first);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Se aplica a favoritos, b?squedas, emisoras cercanas y listados r?pidos.',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SeccionEmisoraPreferida extends StatelessWidget {
|
||||
const _SeccionEmisoraPreferida();
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
final _controller = TextEditingController();
|
||||
String? _paisSeleccionado;
|
||||
String? _idiomaSeleccionado;
|
||||
int? _calidadMinima;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -60,6 +61,7 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
nombre: q.isNotEmpty ? q : null,
|
||||
pais: _paisSeleccionado,
|
||||
idioma: _idiomaSeleccionado,
|
||||
minBitrate: _calidadMinima,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,7 +110,6 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
),
|
||||
),
|
||||
),
|
||||
_seccionCercanas(estado),
|
||||
_seccionFiltro(
|
||||
'Pais',
|
||||
_paises.map((p) => (p.$1, p.$2)).toList(),
|
||||
@@ -127,80 +128,20 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
_buscar();
|
||||
},
|
||||
),
|
||||
_seccionFiltroInt(
|
||||
'Calidad m?nima',
|
||||
const [('64 kbps', 64), ('96 kbps', 96), ('128 kbps', 128), ('192 kbps', 192), ('320 kbps', 320)],
|
||||
_calidadMinima,
|
||||
(v) {
|
||||
setState(() => _calidadMinima = v);
|
||||
_buscar();
|
||||
},
|
||||
),
|
||||
_resultados(estado, theme),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _seccionCercanas(EstadoRadio estado) {
|
||||
final theme = Theme.of(context);
|
||||
final pais = estado.paisCercanoDetectado;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(PluriLayout.horizontal, 8, PluriLayout.horizontal, 0),
|
||||
child: PluriGlassSurface(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
pais == null ? 'Emisoras cercanas' : 'Emisoras cercanas - $pais',
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: estado.cargandoCercanas
|
||||
? null
|
||||
: estado.cargarEmisorasCercanas,
|
||||
icon: estado.cargandoCercanas
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.my_location_rounded, size: 18),
|
||||
label: const Text('Buscar cerca'),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (estado.errorCercanas != null)
|
||||
Text(
|
||||
estado.errorCercanas!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
if (estado.emisorasCercanas.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 76,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: estado.emisorasCercanas.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (context, i) {
|
||||
final emisora = estado.emisorasCercanas[i];
|
||||
return SizedBox(
|
||||
width: 260,
|
||||
child: TarjetaEmisora(
|
||||
emisora: emisora,
|
||||
esCompacta: true,
|
||||
onTap: () => reproducirMinimizado(context, emisora),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _seccionFiltro(
|
||||
String titulo,
|
||||
@@ -247,6 +188,51 @@ class _PantallaBuscarState extends State<PantallaBuscar> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _seccionFiltroInt(
|
||||
String titulo,
|
||||
List<(String, int)> opciones,
|
||||
int? seleccionado,
|
||||
void Function(int?) onChanged,
|
||||
) {
|
||||
final theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(PluriLayout.horizontal, 8, PluriLayout.horizontal, 0),
|
||||
child: PluriGlassSurface(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
titulo,
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: opciones.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (_, i) {
|
||||
final (label, value) = opciones[i];
|
||||
final sel = seleccionado == value;
|
||||
return FilterChip(
|
||||
label: Text(label),
|
||||
selected: sel,
|
||||
visualDensity: VisualDensity.compact,
|
||||
onSelected: (_) => onChanged(sel ? null : value),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _resultados(EstadoRadio estado, ThemeData theme) {
|
||||
if (estado.cargandoBusqueda) {
|
||||
return const SizedBox(
|
||||
|
||||
@@ -361,6 +361,7 @@ class _GrabacionWidget extends StatelessWidget {
|
||||
final theme = Theme.of(context);
|
||||
final grabacion = estado.estadoGrabacion;
|
||||
final activa = grabacion.activa;
|
||||
final hayUltimaGrabacion = estado.ultimaGrabacion != null;
|
||||
|
||||
return PluriGlassSurface(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
@@ -397,19 +398,43 @@ class _GrabacionWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FilledButton.tonalIcon(
|
||||
icon: Icon(activa ? Icons.stop_rounded : Icons.mic_rounded),
|
||||
label: Text(activa ? 'Parar' : 'Grabar'),
|
||||
onPressed:
|
||||
activa
|
||||
? estado.detenerGrabacion
|
||||
: () => _mostrarDialogoGrabacion(context),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
alignment: WrapAlignment.end,
|
||||
children: [
|
||||
FilledButton.tonalIcon(
|
||||
icon: Icon(activa ? Icons.stop_rounded : Icons.mic_rounded),
|
||||
label: Text(activa ? 'Parar' : 'Grabar'),
|
||||
onPressed:
|
||||
activa
|
||||
? estado.detenerGrabacion
|
||||
: () => _mostrarDialogoGrabacion(context),
|
||||
),
|
||||
if (!activa && hayUltimaGrabacion)
|
||||
IconButton.filledTonal(
|
||||
tooltip: 'Abrir ?ltima grabaci?n',
|
||||
icon: const Icon(Icons.audio_file_rounded),
|
||||
onPressed: () => _abrirUltimaGrabacion(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _abrirUltimaGrabacion(BuildContext context) async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final abierto = await estado.abrirUltimaGrabacion();
|
||||
if (!context.mounted) return;
|
||||
if (!abierto) {
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(content: Text('No se pudo abrir la ?ltima grabaci?n')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _mostrarDialogoGrabacion(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
|
||||
Reference in New Issue
Block a user