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.
180 lines
6.6 KiB
Dart
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),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|