Audit 10.2 (t4:511): GrupoAjustes's eyebrow now sits OUTSIDE the PluriGlassSurface, at title-tier (20px) padding -- it used to share the card's own 16px content padding. Audit 10.5 (t4:514/526): FilaAjuste gains an optional iconColor. Wired on the two rows the prototype actually accents: AUDIO's Ecualizador (brand cyan) and STATIONS' Grupos de favoritos (warmCoral). Item 10.10 (two extra AUDIO rows -- "Calidad de streaming" and "Reproduccion sin interrupciones") is NOT implemented: neither concept has any backing state or service anywhere in this app today. Building them for real means inventing two new persisted settings, and a genuine streaming-quality preference would plausibly need to touch the protected servicio_audio.dart to actually affect playback -- out of scope for a visual-fidelity pass. Decorative rows that open nothing would be dead UI, which is worse than leaving the gap disclosed.
302 lines
12 KiB
Dart
302 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../estado/estado_ecualizador.dart';
|
|
import '../estado/estado_grabacion.dart';
|
|
import '../estado/estado_idioma.dart';
|
|
import '../estado/estado_radio.dart';
|
|
import '../l10n/display_names.dart';
|
|
import '../l10n/gen/app_localizations.dart';
|
|
import '../modelos/archivo_grabacion.dart';
|
|
import '../modelos/emisora.dart';
|
|
import '../tema/pluriwave_tokens.dart';
|
|
import '../widgets/pluri_layout.dart';
|
|
import '../widgets/pluri_push_scaffold.dart';
|
|
import '../widgets/pluri_root_header.dart';
|
|
import '../widgets/pluri_sleep_timer_sheet.dart';
|
|
import 'ajustes/pantalla_ajustes_backup.dart';
|
|
import 'ajustes/pantalla_ajustes_ecualizador.dart';
|
|
import 'ajustes/pantalla_ajustes_emisora_preferida.dart';
|
|
import 'ajustes/pantalla_ajustes_emisoras_personalizadas.dart';
|
|
import 'ajustes/pantalla_ajustes_grupos_favoritos.dart';
|
|
import 'ajustes/pantalla_ajustes_idioma.dart';
|
|
import 'ajustes/pantalla_ajustes_info.dart';
|
|
import 'ajustes/pantalla_ajustes_musica_local.dart';
|
|
import 'ajustes/pantalla_ajustes_orden_listas.dart';
|
|
import 'ajustes/pantalla_ajustes_salida_audio.dart';
|
|
import 'ajustes/pantalla_ajustes_timer_sueno.dart';
|
|
import 'ajustes/widgets/fila_ajuste.dart';
|
|
import 'pantalla_grabaciones.dart';
|
|
|
|
class PantallaAjustes extends StatelessWidget {
|
|
const PantallaAjustes({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context);
|
|
|
|
return ListView(
|
|
padding: PluriLayout.pageListPadding,
|
|
children: [
|
|
// S1 (Tier 1 visual fidelity): the prototype has no global AppBar —
|
|
// this root now draws its own 56px title row instead of relying on
|
|
// app.dart's removed shared chrome (which is also where the
|
|
// sleep-timer action used to live).
|
|
PluriRootHeader(
|
|
title: l10n.settingsTitle,
|
|
onSleepTimer: () => showPluriSleepTimerSheet(context),
|
|
),
|
|
const Padding(
|
|
padding: PluriLayout.pageContentPadding,
|
|
child: _AjustesContent(),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Design ADR-3: the Settings root is grouped nav rows only — each
|
|
/// [FilaAjuste] pushes its own detail screen via `PluriPushScaffold.push`,
|
|
/// carrying zero inline controls. All 12 sections are now decomposed across
|
|
/// WU3a (AUDIO, EMISORAS) and WU3b (GRABACIONES Y MÚSICA, APLICACIÓN); sleep
|
|
/// timer and backup/restore stay reachable throughout — nothing was dropped.
|
|
class _AjustesContent extends StatelessWidget {
|
|
const _AjustesContent();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context);
|
|
// S8 (Tier 1 visual fidelity): the prototype shows every row's current
|
|
// value (t4 lines 512-539, 625). 8 of the 12 rows below read it from
|
|
// state already provided at the app root — no new providers needed.
|
|
//
|
|
// S4-R5 convention (see pantalla_inicio.dart, pantalla_buscar.dart):
|
|
// `context.select` per scalar, NOT a root `context.watch<EstadoRadio>()`
|
|
// — EstadoRadio also notifies on audio buffer/position events, which
|
|
// this screen has nothing to do with. A root watch here rebuilds the
|
|
// WHOLE settings list (including the Grabaciones row's FutureBuilder,
|
|
// which would re-issue `listarGrabaciones()` on every single one of
|
|
// those unrelated notifications) far more often than intended.
|
|
final gruposCount = context.select<EstadoRadio, int>(
|
|
(e) => e.gruposFavoritos.length,
|
|
);
|
|
final emisoraPreferida = context.select<EstadoRadio, Emisora?>(
|
|
(e) => e.emisoraPreferida,
|
|
);
|
|
final emisorasCustomCount = context.select<EstadoRadio, int>(
|
|
(e) => e.emisorasCustom.length,
|
|
);
|
|
final ordenListas = context.select<EstadoRadio, OrdenEmisoras>(
|
|
(e) => e.ordenListas,
|
|
);
|
|
final timerActivo = context.select<EstadoRadio, bool>(
|
|
(e) => e.timer.activo,
|
|
);
|
|
final ecualizadorActivo = context.select<EstadoEcualizador, bool>(
|
|
(e) => e.activo,
|
|
);
|
|
final grabacion = context.watch<EstadoGrabacion>();
|
|
final idioma = context.select<EstadoIdioma, Locale?>(
|
|
(e) => e.localeSeleccionado,
|
|
);
|
|
|
|
return Column(
|
|
children: [
|
|
GrupoAjustes(
|
|
titulo: l10n.settingsGroupAudioTitle,
|
|
filas: [
|
|
FilaAjuste(
|
|
icon: Icons.equalizer_rounded,
|
|
// Audit 10.5 (t4 line 514): the first AUDIO row's icon is
|
|
// brand cyan.
|
|
iconColor: PluriWaveTokens.brand,
|
|
titulo: l10n.equalizerTitle,
|
|
valor:
|
|
ecualizadorActivo
|
|
? l10n.equalizerActive
|
|
: l10n.equalizerDisabled,
|
|
onTap:
|
|
() => PluriPushScaffold.push(
|
|
context,
|
|
(_) => const PantallaAjustesEcualizador(),
|
|
),
|
|
),
|
|
// No `valor`: per-device output naming needs a friendly-name
|
|
// lookup this root doesn't have (only a raw device id).
|
|
FilaAjuste(
|
|
icon: Icons.devices_rounded,
|
|
titulo: l10n.advancedEqSectionTitle,
|
|
onTap:
|
|
() => PluriPushScaffold.push(
|
|
context,
|
|
(_) => const PantallaAjustesSalidaAudio(),
|
|
),
|
|
),
|
|
FilaAjuste(
|
|
icon: Icons.bedtime_rounded,
|
|
titulo: l10n.timerSectionTitle,
|
|
// Reuses the equalizer's own "Active" string (generic enough
|
|
// in every locale) — shown only while running, matching how
|
|
// the preferred-station row shows nothing when unset.
|
|
valor: timerActivo ? l10n.equalizerActive : null,
|
|
onTap:
|
|
() => PluriPushScaffold.push(
|
|
context,
|
|
(_) => const PantallaAjustesTimerSueno(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
GrupoAjustes(
|
|
titulo: l10n.settingsGroupStationsTitle,
|
|
filas: [
|
|
FilaAjuste(
|
|
icon: Icons.playlist_add_check_circle_rounded,
|
|
// Audit 10.5 (t4 line 526): the first STATIONS row's icon
|
|
// is warmCoral (the prototype's own `folder` row accent).
|
|
iconColor: PluriWaveTokens.dark.warmCoral,
|
|
titulo: l10n.favoriteGroupsTitle,
|
|
valor: '$gruposCount',
|
|
onTap:
|
|
() => PluriPushScaffold.push(
|
|
context,
|
|
(_) => const PantallaAjustesGruposFavoritos(),
|
|
),
|
|
),
|
|
FilaAjuste(
|
|
icon: Icons.radio_rounded,
|
|
titulo: l10n.preferredStationTitle,
|
|
valor:
|
|
emisoraPreferida != null
|
|
? localizedStationName(l10n, emisoraPreferida.nombre)
|
|
: l10n.dash,
|
|
onTap:
|
|
() => PluriPushScaffold.push(
|
|
context,
|
|
(_) => const PantallaAjustesEmisoraPreferida(),
|
|
),
|
|
),
|
|
FilaAjuste(
|
|
icon: Icons.add_circle_outline_rounded,
|
|
titulo: l10n.customStationsTitle,
|
|
valor: '$emisorasCustomCount',
|
|
onTap:
|
|
() => PluriPushScaffold.push(
|
|
context,
|
|
(_) => const PantallaAjustesEmisorasPersonalizadas(),
|
|
),
|
|
),
|
|
FilaAjuste(
|
|
icon: Icons.sort_rounded,
|
|
titulo: l10n.stationOrderTitle,
|
|
valor:
|
|
ordenListas == OrdenEmisoras.calidad
|
|
? l10n.stationOrderByQuality
|
|
: l10n.stationOrderByName,
|
|
onTap:
|
|
() => PluriPushScaffold.push(
|
|
context,
|
|
(_) => const PantallaAjustesOrdenListas(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
GrupoAjustes(
|
|
titulo: l10n.settingsGroupRecordingsTitle,
|
|
filas: [
|
|
// FutureBuilder-wrapped (not a plain FilaAjuste): the count ·
|
|
// size value needs an async disk listing
|
|
// (EstadoGrabacion.listarGrabaciones), same source
|
|
// PantallaGrabaciones itself reads.
|
|
FutureBuilder<List<ArchivoGrabacion>>(
|
|
future: grabacion.listarGrabaciones(),
|
|
builder: (context, snapshot) {
|
|
final archivos = snapshot.data;
|
|
return FilaAjuste(
|
|
icon: Icons.radio_button_checked_rounded,
|
|
titulo: l10n.recordingsSectionTitle,
|
|
valor:
|
|
archivos == null
|
|
? null
|
|
: '${archivos.length} · ${_totalMb(archivos)} MB',
|
|
// WU15b: this row opens the recordings LIBRARY
|
|
// (PantallaGrabaciones), matching the approved mockup's
|
|
// "Ajustes > Grabaciones" screen. The folder/size
|
|
// settings form (PantallaAjustesGrabaciones) is still
|
|
// reachable, but now from within the library via its own
|
|
// settings action.
|
|
onTap:
|
|
() => PluriPushScaffold.push(
|
|
context,
|
|
(_) => const PantallaGrabaciones(),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
// No `valor`: the configured folder is a SAF tree URI resolved
|
|
// by an async native channel call
|
|
// (FuenteMusicaLocalAutoImpl.carpetaActual) this root would
|
|
// need to invoke itself, unmocked, just to render a value.
|
|
FilaAjuste(
|
|
icon: Icons.library_music_outlined,
|
|
titulo: l10n.localMusicSectionTitle,
|
|
onTap:
|
|
() => PluriPushScaffold.push(
|
|
context,
|
|
(_) => const PantallaAjustesMusicaLocal(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
GrupoAjustes(
|
|
titulo: l10n.settingsGroupApplicationTitle,
|
|
filas: [
|
|
FilaAjuste(
|
|
icon: Icons.language_rounded,
|
|
titulo: l10n.languageSectionTitle,
|
|
valor: nombreIdiomaActual(idioma, l10n),
|
|
onTap:
|
|
() => PluriPushScaffold.push(
|
|
context,
|
|
(_) => const PantallaAjustesIdioma(),
|
|
),
|
|
),
|
|
// No `valor`: there is no persisted "last backup" timestamp to
|
|
// read — showing one here would mean fabricating it.
|
|
FilaAjuste(
|
|
icon: Icons.backup_outlined,
|
|
titulo: l10n.backupSectionTitle,
|
|
onTap:
|
|
() => PluriPushScaffold.push(
|
|
context,
|
|
(_) => const PantallaAjustesBackup(),
|
|
),
|
|
),
|
|
// No `valor`: PackageInfo.fromPlatform() has no test-environment
|
|
// fallback anywhere else in this codebase either (see
|
|
// PantallaAjustesInfo's own FutureBuilder) — wiring it here
|
|
// would add a value this pass cannot deterministically test.
|
|
FilaAjuste(
|
|
icon: Icons.info_outline_rounded,
|
|
titulo: l10n.infoSectionTitle,
|
|
onTap:
|
|
() => PluriPushScaffold.push(
|
|
context,
|
|
(_) => const PantallaAjustesInfo(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
static int _totalMb(List<ArchivoGrabacion> archivos) {
|
|
final totalBytes = archivos.fold<int>(0, (a, b) => a + b.tamanoBytes);
|
|
return (totalBytes / (1024 * 1024)).round();
|
|
}
|
|
}
|