Files
pluriwave/lib/pantallas/ajustes/pantalla_ajustes_backup.dart
T
FreeTLab e57f7bb17b fix(alarmas): reload and re-sync alarms after backup import
Importing a backup wrote the alarm/vacation/exception block straight to
SharedPreferences but never told EstadoAlarmas about it, so the UI kept
showing the pre-import alarms, a later edit could persist that stale
state back over the imported one, and imported alarms were never
(re)scheduled with the Android native layer. The backup screen now
calls EstadoAlarmas.cargarPersistidasSinRecalcular() followed by
refrescarProgramacion() after a successful import, extracted into a
directly-testable aplicarImportacionConfig() function.
2026-08-28 22:47:38 +02:00

180 lines
6.6 KiB
Dart

import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:share_plus/share_plus.dart' show Share, XFile;
import '../../estado/estado_alarmas.dart';
import '../../estado/estado_radio.dart';
import '../../l10n/gen/app_localizations.dart';
import '../../widgets/pluri_glass_surface.dart';
import '../../widgets/pluri_layout.dart';
import '../../widgets/pluri_push_scaffold.dart';
/// Applies a successfully-parsed backup to BOTH independent notifiers that
/// own pieces of it (fix/import-alarmas-y-paywall).
///
/// `EstadoRadio.importarConfig` writes the raw alarm/vacation/exception JSON
/// block straight to SharedPreferences, but `EstadoAlarmas` is a separate
/// long-lived `ChangeNotifier` that loaded its alarms into memory at
/// construction and never re-reads storage on its own — `EstadoRadio` stays
/// deliberately free of a dependency on it. Without the two calls below the
/// imported block is invisible to the running app: the UI keeps showing the
/// pre-import alarms, a later edit would persist that stale in-memory list
/// OVER the imported one, and the imported alarms would never be
/// (re)scheduled with the Android native layer even after a restart.
///
/// Extracted as a top-level function (rather than inlined in `_importar`)
/// so this exact production sequence — not a reimplementation of it — is
/// directly unit-testable without depending on the `file_picker` platform
/// channel or the confirmation dialog.
Future<void> aplicarImportacionConfig(
EstadoRadio estado,
EstadoAlarmas alarmas,
Map<String, dynamic> json,
) async {
await estado.importarConfig(json);
// Re-reads from storage — clears ServicioAlarmas' in-memory cache so the
// just-imported alarms/vacations/exceptions (same JSON block, same
// notifier) replace the stale ones.
await alarmas.cargarPersistidasSinRecalcular();
// Recomputes next-run times against the (now fresh) imported data and
// re-syncs every alarm with the Android native scheduler.
await alarmas.refrescarProgramacion();
}
/// APLICACIÓN group · "Copia de seguridad" (design ADR-3). Body moved
/// verbatim from the former `_SeccionBackup` in `pantalla_ajustes.dart` —
/// only the panel header's icon and title were removed (the pushed screen's
/// title now carries them); every method below is unchanged.
class PantallaAjustesBackup extends StatelessWidget {
const PantallaAjustesBackup({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return PluriPushScaffold(
title: l10n.backupSectionTitle,
body: ListView(
padding: PluriLayout.pageContentPadding,
children: const [_CuerpoBackup()],
),
);
}
}
class _CuerpoBackup extends StatelessWidget {
const _CuerpoBackup();
Future<void> _exportar(BuildContext context) async {
final l10n = AppLocalizations.of(context);
try {
final estado = context.read<EstadoRadio>();
// 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');
await file.writeAsString(json);
await Share.shareXFiles(
[XFile(file.path)],
subject: l10n.backupShareSubject,
text: l10n.backupShareText(DateTime.now().toLocal()),
);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.backupExportError(e.toString()))),
);
}
}
}
Future<void> _importar(BuildContext context) async {
final l10n = AppLocalizations.of(context);
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['json'],
);
if (result == null || result.files.single.path == null) return;
final file = File(result.files.single.path!);
final contenido = await file.readAsString();
if (!context.mounted) return;
// Parsing is owned by ServicioExportImport (S4-R4): null = malformed.
final json = context.read<EstadoRadio>().parsearConfigJson(contenido);
if (json == null) {
throw const FormatException('invalid backup file');
}
if (context.mounted) {
final confirmar = await showDialog<bool>(
context: context,
builder:
(ctx) => AlertDialog(
title: Text(AppLocalizations.of(ctx).backupImportTitle),
content: Text(
AppLocalizations.of(ctx).backupImportConfirmMessage,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: Text(AppLocalizations.of(ctx).cancelAction),
),
FilledButton(
onPressed: () => Navigator.pop(ctx, true),
child: Text(AppLocalizations.of(ctx).backupImportTitle),
),
],
),
);
if (confirmar != true) return;
if (context.mounted) {
final estado = context.read<EstadoRadio>();
final alarmas = context.read<EstadoAlarmas>();
final messenger = ScaffoldMessenger.of(context);
await aplicarImportacionConfig(estado, alarmas, json);
messenger.showSnackBar(
SnackBar(content: Text(l10n.backupImportSuccess)),
);
}
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.backupImportError(e.toString()))),
);
}
}
}
@override
Widget build(BuildContext context) {
return PluriGlassSurface(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.upload_outlined),
title: Text(AppLocalizations.of(context).backupExportTitle),
subtitle: Text(AppLocalizations.of(context).backupExportSubtitle),
onTap: () => _exportar(context),
),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.download_outlined),
title: Text(AppLocalizations.of(context).backupImportTitle),
subtitle: Text(AppLocalizations.of(context).backupImportSubtitle),
onTap: () => _importar(context),
),
],
),
);
}
}