refactor(state): extract export/import service and equalizer state from EstadoRadio
- New ServicioExportImport owns the v2 backup envelope, pretty JSON encode and graceful decode; byte-compatible with existing exports, locked by a round-trip test - pantalla_ajustes delegates backup serialization to the service (inline jsonDecode/jsonEncode removed) - New EstadoEcualizador ChangeNotifier owns all EQ state and persistence (principal/current/per-station presets, active flag), exposed via its own provider so EQ changes no longer rebuild EstadoRadio consumers - EstadoRadio slims down ~210 lines and keeps 15 delegating compat members marked TODO(S4b) for the next slice to remove - Player EQ toggle rewired to the new provider to avoid going stale - 4 new tests (103 total green), flutter analyze clean
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
@@ -9,6 +8,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:share_plus/share_plus.dart' show Share, XFile;
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../estado/estado_ecualizador.dart';
|
||||
import '../estado/estado_idioma.dart';
|
||||
import '../estado/estado_radio.dart';
|
||||
import '../l10n/display_names.dart';
|
||||
@@ -336,10 +336,7 @@ class _SeccionTimerSueno extends StatelessWidget {
|
||||
for (final segundos in presets)
|
||||
InputChip(
|
||||
label: Text(
|
||||
_formatearDuracionTimer(
|
||||
l10n,
|
||||
Duration(seconds: segundos),
|
||||
),
|
||||
_formatearDuracionTimer(l10n, Duration(seconds: segundos)),
|
||||
),
|
||||
onDeleted:
|
||||
presets.length <= 1
|
||||
@@ -574,14 +571,16 @@ class _SeccionEcualizador extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<EstadoRadio>(
|
||||
builder: (ctx, estado, _) {
|
||||
final disponible = estado.ecualizadorDisponible;
|
||||
// EQ state comes from EstadoEcualizador (S4-R1/S4-R5); EstadoRadio is
|
||||
// only consulted for the current station + favorite flag.
|
||||
return Consumer2<EstadoRadio, EstadoEcualizador>(
|
||||
builder: (ctx, estado, eq, _) {
|
||||
final disponible = eq.disponible;
|
||||
final l10n = AppLocalizations.of(ctx);
|
||||
final emisoraActual = estado.emisoraActual;
|
||||
final mostrarModoPorEmisora =
|
||||
emisoraActual != null && estado.emisoraActualEsFavorita;
|
||||
final usandoEqPropio = estado.emisoraActualTienePresetPropio;
|
||||
final usandoEqPropio = eq.emisoraActualTienePresetPropio;
|
||||
|
||||
return PluriGlassSurface(
|
||||
child: Column(
|
||||
@@ -598,9 +597,7 @@ class _SeccionEcualizador extends StatelessWidget {
|
||||
const Spacer(),
|
||||
Chip(
|
||||
label: Text(
|
||||
estado.ecualizadorActivo
|
||||
? l10n.equalizerActive
|
||||
: l10n.equalizerDisabled,
|
||||
eq.activo ? l10n.equalizerActive : l10n.equalizerDisabled,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
@@ -615,8 +612,8 @@ class _SeccionEcualizador extends StatelessWidget {
|
||||
? l10n.equalizerRealtimeSubtitle
|
||||
: l10n.equalizerPendingSubtitle,
|
||||
),
|
||||
value: estado.ecualizadorActivo,
|
||||
onChanged: estado.cambiarEcualizadorActivo,
|
||||
value: eq.activo,
|
||||
onChanged: eq.cambiarActivo,
|
||||
),
|
||||
if (mostrarModoPorEmisora) ...[
|
||||
const SizedBox(height: 8),
|
||||
@@ -631,20 +628,18 @@ class _SeccionEcualizador extends StatelessWidget {
|
||||
value: usandoEqPropio,
|
||||
onChanged:
|
||||
(usarPropio) =>
|
||||
estado.cambiarModoEcualizadorEmisoraActual(
|
||||
usarPropio: usarPropio,
|
||||
),
|
||||
eq.cambiarModoEmisoraActual(usarPropio: usarPropio),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
PresetsEcualizadorWidget(
|
||||
presetActual: estado.presetEcualizador,
|
||||
onSeleccionar: (p) => estado.cambiarPresetEcualizador(p),
|
||||
presetActual: eq.presetActual,
|
||||
onSeleccionar: (p) => eq.cambiarPreset(p),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
EcualizadorWidget(
|
||||
preset: estado.presetEcualizador,
|
||||
onCambio: (p) => estado.cambiarPresetEcualizador(p),
|
||||
preset: eq.presetActual,
|
||||
onCambio: (p) => eq.cambiarPreset(p),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1176,8 +1171,8 @@ class _SeccionBackup extends StatelessWidget {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
try {
|
||||
final estado = context.read<EstadoRadio>();
|
||||
final config = await estado.exportarConfig();
|
||||
final json = const JsonEncoder.withIndent(' ').convert(config);
|
||||
// JSON serialization is owned by ServicioExportImport (S4-R4).
|
||||
final json = await estado.exportarConfigJson();
|
||||
|
||||
final dir = await getTemporaryDirectory();
|
||||
final file = File('${dir.path}/pluriwave-backup.json');
|
||||
@@ -1207,8 +1202,14 @@ class _SeccionBackup extends StatelessWidget {
|
||||
if (result == null || result.files.single.path == null) return;
|
||||
|
||||
final file = File(result.files.single.path!);
|
||||
final json =
|
||||
jsonDecode(await file.readAsString()) as Map<String, dynamic>;
|
||||
if (!context.mounted) return;
|
||||
// Parsing is owned by ServicioExportImport (S4-R4): null = malformed.
|
||||
final json = context.read<EstadoRadio>().parsearConfigJson(
|
||||
await file.readAsString(),
|
||||
);
|
||||
if (json == null) {
|
||||
throw const FormatException('invalid backup file');
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
final confirmar = await showDialog<bool>(
|
||||
@@ -1365,10 +1366,7 @@ class _SeccionInfo extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
String _formatearDuracionTimer(
|
||||
AppLocalizations l10n,
|
||||
Duration duracion,
|
||||
) {
|
||||
String _formatearDuracionTimer(AppLocalizations l10n, Duration duracion) {
|
||||
final horas = duracion.inHours;
|
||||
final minutos = duracion.inMinutes.remainder(60);
|
||||
final segundos = duracion.inSeconds.remainder(60);
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
import '../estado/estado_ecualizador.dart';
|
||||
import '../estado/estado_radio.dart';
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../modelos/emisora.dart';
|
||||
@@ -76,6 +77,9 @@ class _PantallaReproductorState extends State<PantallaReproductor>
|
||||
final theme = Theme.of(context);
|
||||
final tokens = context.pluriTokens;
|
||||
final estado = context.watch<EstadoRadio>();
|
||||
// EQ toggle state lives in EstadoEcualizador (S4-R1); EstadoRadio no
|
||||
// longer notifies on EQ changes, so this screen watches both.
|
||||
final eq = context.watch<EstadoEcualizador>();
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final emisoraActiva = estado.emisoraActual ?? widget.emisora;
|
||||
final esFavorito = estado.listaFavoritos.any(
|
||||
@@ -94,18 +98,11 @@ class _PantallaReproductorState extends State<PantallaReproductor>
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
estado.ecualizadorActivo
|
||||
? Icons.equalizer_rounded
|
||||
: Icons.equalizer_outlined,
|
||||
color: estado.ecualizadorActivo ? tokens.warmCoral : null,
|
||||
eq.activo ? Icons.equalizer_rounded : Icons.equalizer_outlined,
|
||||
color: eq.activo ? tokens.warmCoral : null,
|
||||
),
|
||||
tooltip:
|
||||
estado.ecualizadorActivo
|
||||
? l10n.equalizerDisable
|
||||
: l10n.equalizerEnable,
|
||||
onPressed:
|
||||
() =>
|
||||
estado.cambiarEcualizadorActivo(!estado.ecualizadorActivo),
|
||||
tooltip: eq.activo ? l10n.equalizerDisable : l10n.equalizerEnable,
|
||||
onPressed: () => eq.cambiarActivo(!eq.activo),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
|
||||
Reference in New Issue
Block a user