merge: incorporate main's safearea/auto-order/vacaciones fixes
This commit is contained in:
@@ -394,7 +394,14 @@ class EstadoRadio extends ChangeNotifier {
|
||||
_cargandoPopulares = false;
|
||||
// Design "live snapshot the source prefers": Android Auto's `Todas`
|
||||
// folder mirrors the same populares list the phone just loaded.
|
||||
_fuenteAuto?.actualizarSnapshot(todas: _populares);
|
||||
//
|
||||
// Fix `android-auto-orden`: pushes the SORTED [populares] getter, not
|
||||
// the raw [_populares] field — the same [_ordenListas] setting the
|
||||
// phone's own discovery lists (e.g. Buscar's `tendencias`) already
|
||||
// sort by must also govern this folder's order, not the API's raw
|
||||
// arrival order. `navegacion_auto.dart`'s `hijos()` no longer
|
||||
// re-sorts, so whatever order arrives here IS what the driver sees.
|
||||
_fuenteAuto?.actualizarSnapshot(todas: populares);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -402,7 +409,13 @@ class EstadoRadio extends ChangeNotifier {
|
||||
Future<void> cargarFavoritos() async {
|
||||
_listaFavoritos = await favoritos.obtenerTodos();
|
||||
await _normalizarEmisoraPreferida();
|
||||
_fuenteAuto?.actualizarSnapshot(favoritos: _listaFavoritos);
|
||||
// Fix `android-auto-orden`: pushes the documented manual-order accessor
|
||||
// explicitly. [listaFavoritosManual] is backed by the same list as
|
||||
// [_listaFavoritos] today (obtenerTodos() already returns the persisted
|
||||
// manual order), but naming the intent here — "the exact order the
|
||||
// Favoritos screen shows and reorders" — keeps this call from silently
|
||||
// drifting onto a re-sorted list in a future refactor.
|
||||
_fuenteAuto?.actualizarSnapshot(favoritos: listaFavoritosManual);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -520,6 +533,16 @@ class EstadoRadio extends ChangeNotifier {
|
||||
await prefs.setString(_keyOrdenListas, orden.name);
|
||||
// Search owns its own listeners (S4-R3) but sorts with this preference.
|
||||
busqueda.notificarCambioOrden();
|
||||
// Fix `android-auto-orden`: Todas/Mis emisoras' Android Auto order is
|
||||
// derived from this same setting (see cargarPopulares/
|
||||
// _cargarEmisorasCustom above) — without an immediate re-push, a live
|
||||
// car session would keep showing the OLD order until the next full
|
||||
// reload instead of updating right away, same as the phone does via
|
||||
// this method's own memoized getters.
|
||||
_fuenteAuto?.actualizarSnapshot(
|
||||
todas: populares,
|
||||
misEmisoras: emisorasCustom,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -659,7 +682,9 @@ class EstadoRadio extends ChangeNotifier {
|
||||
detalle: 'resolucion de ruta',
|
||||
razon: e.toString(),
|
||||
);
|
||||
_fuenteAuto?.actualizarSnapshot(misEmisoras: _emisorasCustom);
|
||||
// Fix `android-auto-orden`: pushes the SORTED [emisorasCustom] getter
|
||||
// (see the doc on this method's other 3 identical call sites below).
|
||||
_fuenteAuto?.actualizarSnapshot(misEmisoras: emisorasCustom);
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
@@ -685,7 +710,8 @@ class EstadoRadio extends ChangeNotifier {
|
||||
razon: e.toString(),
|
||||
);
|
||||
}
|
||||
_fuenteAuto?.actualizarSnapshot(misEmisoras: _emisorasCustom);
|
||||
// Fix `android-auto-orden`: sorted getter, not the raw field.
|
||||
_fuenteAuto?.actualizarSnapshot(misEmisoras: emisorasCustom);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -701,7 +727,8 @@ class EstadoRadio extends ChangeNotifier {
|
||||
if (!await archivo.exists()) {
|
||||
_emisorasCustom = [];
|
||||
_customDegradado = false;
|
||||
_fuenteAuto?.actualizarSnapshot(misEmisoras: _emisorasCustom);
|
||||
// Fix `android-auto-orden`: sorted getter, not the raw field.
|
||||
_fuenteAuto?.actualizarSnapshot(misEmisoras: emisorasCustom);
|
||||
notifyListeners();
|
||||
return null;
|
||||
}
|
||||
@@ -714,7 +741,8 @@ class EstadoRadio extends ChangeNotifier {
|
||||
detalle: archivo.path,
|
||||
razon: e.toString(),
|
||||
);
|
||||
_fuenteAuto?.actualizarSnapshot(misEmisoras: _emisorasCustom);
|
||||
// Fix `android-auto-orden`: sorted getter, not the raw field.
|
||||
_fuenteAuto?.actualizarSnapshot(misEmisoras: emisorasCustom);
|
||||
notifyListeners();
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -842,10 +842,38 @@ class _EditorVacacionesSheetState extends State<_EditorVacacionesSheet> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.icon(
|
||||
onPressed: _guardar,
|
||||
icon: const Icon(Icons.check_rounded),
|
||||
label: Text(l10n.saveRangeAction),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed: _guardar,
|
||||
icon: const Icon(Icons.check_rounded),
|
||||
label: Text(l10n.saveRangeAction),
|
||||
),
|
||||
),
|
||||
// Fix `vacaciones-delete`: only when EDITING an existing
|
||||
// range (never when creating one -- there is nothing to
|
||||
// delete yet). Reuses the exact same confirmation dialog
|
||||
// (`_confirmarEliminarRango`) and deletion method
|
||||
// (`eliminarRangoVacaciones`) the swipe-to-delete gesture
|
||||
// already uses on both `_HeroRangoActivo` and
|
||||
// `_TarjetaRangoVacaciones` -- no new deletion path.
|
||||
if (widget.rango != null) ...[
|
||||
const SizedBox(width: 10),
|
||||
OutlinedButton.icon(
|
||||
key: const ValueKey('vacation-delete-button'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.error,
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
onPressed: _eliminar,
|
||||
icon: const Icon(Icons.delete_outline_rounded),
|
||||
label: Text(l10n.deleteAction),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -903,6 +931,21 @@ class _EditorVacacionesSheetState extends State<_EditorVacacionesSheet> {
|
||||
}
|
||||
if (mounted) Navigator.pop(context);
|
||||
}
|
||||
|
||||
/// Fix `vacaciones-delete`: mirrors `_guardar`'s pop-on-success shape,
|
||||
/// but confirms first (via the same `_confirmarEliminarRango` dialog the
|
||||
/// swipe gesture uses) and calls `eliminarRangoVacaciones` instead of
|
||||
/// saving. Only reachable when [widget.rango] is non-null (the delete
|
||||
/// button itself is hidden otherwise).
|
||||
Future<void> _eliminar() async {
|
||||
final rango = widget.rango;
|
||||
if (rango == null) return;
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final confirmado = await _confirmarEliminarRango(context, l10n);
|
||||
if (!confirmado || !mounted) return;
|
||||
await context.read<EstadoAlarmas>().eliminarRangoVacaciones(rango.id);
|
||||
if (mounted) Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
class _PickerButton extends StatelessWidget {
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/foundation.dart' show visibleForTesting;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../estado/orden_emisoras.dart';
|
||||
import '../modelos/emisora.dart';
|
||||
import '../modelos/grupo_favoritos.dart';
|
||||
import '../modelos/pista_local.dart';
|
||||
@@ -350,15 +349,23 @@ class ConstructorArbolAuto {
|
||||
extras: _contentStyleLista,
|
||||
);
|
||||
|
||||
/// Leaf items for [parentId], sorted via [ordenarEmisoras] and capped at
|
||||
/// [_maxItemsPorCarpeta] (Design "which stations surface & ordering" —
|
||||
/// avoids driver distraction and Auto list limits). Unknown [parentId]
|
||||
/// (or an empty [emisoras]) returns an empty list instead of throwing.
|
||||
/// Leaf items for [parentId], PRESERVING the incoming [emisoras] order and
|
||||
/// capped at [_maxItemsPorCarpeta] (Design "which stations surface &
|
||||
/// ordering" — avoids driver distraction and Auto list limits).
|
||||
///
|
||||
/// Fix `android-auto-orden`: this used to force
|
||||
/// `ordenarEmisoras(emisoras, OrdenEmisoras.calidad)` unconditionally,
|
||||
/// silently discarding whatever order the caller actually wanted —
|
||||
/// Favoritos' manual drag-reorder order, or the global `ordenListas`
|
||||
/// setting for Todas/Mis emisoras. Every caller (`EstadoRadio.
|
||||
/// cargarFavoritos`/`cargarPopulares`/`_cargarEmisorasCustom`/
|
||||
/// `cambiarOrdenListas`) now pushes an already-ordered snapshot, so this
|
||||
/// only slices and maps — it must never re-sort. Unknown [parentId] (or
|
||||
/// an empty [emisoras]) returns an empty list instead of throwing.
|
||||
List<MediaItem> hijos(String parentId, {required List<Emisora> emisoras}) {
|
||||
if (!_idsCarpetas.contains(parentId)) return const [];
|
||||
if (emisoras.isEmpty) return const [];
|
||||
final ordenadas = ordenarEmisoras(emisoras, OrdenEmisoras.calidad);
|
||||
return ordenadas.take(_maxItemsPorCarpeta).map(itemEmisora).toList();
|
||||
return emisoras.take(_maxItemsPorCarpeta).map(itemEmisora).toList();
|
||||
}
|
||||
|
||||
/// Maps a single [Emisora] to a playable `MediaItem`: id `emisora:<uuid>`
|
||||
@@ -824,10 +831,12 @@ class ConstructorArbolAuto {
|
||||
}
|
||||
|
||||
/// Members of the favorite group identified by [grupoMediaId] (a
|
||||
/// `grupo:<id>` id), sorted and capped like every other folder (Spec "Car
|
||||
/// requests a group folder's stations"). An unknown/stale/malformed id
|
||||
/// returns an empty list instead of throwing (Spec "Car requests an
|
||||
/// unknown or stale group id").
|
||||
/// `grupo:<id>` id), PRESERVING the incoming [favoritos] order (Favoritos'
|
||||
/// manual order — see [hijos]' doc, fix `android-auto-orden`) and capped
|
||||
/// like every other folder (Spec "Car requests a group folder's
|
||||
/// stations"). An unknown/stale/malformed id returns an empty list
|
||||
/// instead of throwing (Spec "Car requests an unknown or stale group
|
||||
/// id").
|
||||
List<MediaItem> hijosGrupo(
|
||||
String grupoMediaId, {
|
||||
required List<Emisora> favoritos,
|
||||
@@ -837,8 +846,7 @@ class ConstructorArbolAuto {
|
||||
if (id.isEmpty) return const [];
|
||||
final miembros = favoritos.where((e) => e.grupoFavoritosId == id).toList();
|
||||
if (miembros.isEmpty) return const [];
|
||||
final ordenados = ordenarEmisoras(miembros, OrdenEmisoras.calidad);
|
||||
return ordenados.take(_maxItemsPorCarpeta).map(itemEmisora).toList();
|
||||
return miembros.take(_maxItemsPorCarpeta).map(itemEmisora).toList();
|
||||
}
|
||||
|
||||
/// Equalizer preset-selection media-id prefix (decision
|
||||
|
||||
@@ -29,41 +29,59 @@ class PluriRootHeader extends StatelessWidget {
|
||||
/// needs nothing extra here).
|
||||
final List<Widget> actions;
|
||||
|
||||
/// Fix `safearea-top-inset`: this is the CONTENT row's height only —
|
||||
/// NOT this widget's total rendered height. `app.dart`'s root
|
||||
/// `SafeArea(top: false, ...)` deliberately excludes the top inset (so
|
||||
/// each root's own full-bleed background paints genuinely edge-to-edge
|
||||
/// behind the status bar), which left this header's title/actions row
|
||||
/// with zero top-inset awareness — flush at y=0 under the status bar /
|
||||
/// camera cutout on every device. This widget now adds
|
||||
/// `MediaQuery.paddingOf(context).top` ABOVE this content height itself
|
||||
/// (see [build]), so the total rendered height is
|
||||
/// `height + MediaQuery.paddingOf(context).top`. Callers doing
|
||||
/// total-height math (none currently do — checked every `PluriRootHeader`
|
||||
/// call site) must add that inset separately; this constant's MEANING
|
||||
/// (content height) is unchanged.
|
||||
static const double height = 56;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final type = context.pluriType;
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: Padding(
|
||||
// S5: the prototype's own header padding is title-tier on the
|
||||
// left, row-tier on the right (t4 e.g. Alarmas
|
||||
// `padding:0 12px 0 20px`).
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.titleHorizontal,
|
||||
0,
|
||||
PluriLayout.rowHorizontal,
|
||||
0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: type.sectionTitle,
|
||||
final topInset = MediaQuery.paddingOf(context).top;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: topInset),
|
||||
child: SizedBox(
|
||||
height: height,
|
||||
child: Padding(
|
||||
key: const ValueKey('pluri-root-header-content'),
|
||||
// S5: the prototype's own header padding is title-tier on the
|
||||
// left, row-tier on the right (t4 e.g. Alarmas
|
||||
// `padding:0 12px 0 20px`).
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
PluriLayout.titleHorizontal,
|
||||
0,
|
||||
PluriLayout.rowHorizontal,
|
||||
0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: type.sectionTitle,
|
||||
),
|
||||
),
|
||||
),
|
||||
...actions,
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bedtime_outlined),
|
||||
tooltip: l10n.sleepTimer,
|
||||
onPressed: onSleepTimer,
|
||||
),
|
||||
],
|
||||
...actions,
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bedtime_outlined),
|
||||
tooltip: l10n.sleepTimer,
|
||||
onPressed: onSleepTimer,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user