sync_settings_ui 0.5.0: BackupSlot.json on data_transfer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWXTUM9dpnCS2wtFkx4
This commit is contained in:
Krzysztof Rudnicki 2026-11-01 19:06:32 +01:00
parent 254c8ce240
commit f9899b9c56
4 changed files with 120 additions and 7 deletions

View File

@ -4,3 +4,4 @@ build/
pubspec.lock
coverage/
*.iml
pubspec_overrides.yaml

View File

@ -1,12 +1,15 @@
import 'package:data_transfer_flutter/data_transfer_flutter.dart';
/// An injectable local export/import delegate for [SyncSettingsScreen]'s
/// optional "Backup" section.
///
/// Deliberately not a shared implementation: the actual export/import format
/// differs per app (todo: Markdown notes; home_inventory: inventory export;
/// workout_app: a permission-gated `BackupService`), so this package only
/// carries the label and the two callbacks. Passing `null` for
/// `SyncSettingsScreen.backup` omits the section entirely (diet_guard,
/// wake_alarm have no local backup at all).
/// The export/import CONTENT differs per app (todo: Markdown notes;
/// home_inventory: inventory export; workout_app: a permission-gated
/// `BackupService`), so the default constructor only carries the label and
/// the two callbacks. Apps whose backup is a JSON file use [BackupSlot.json],
/// which adds the shared file plumbing (`data_transfer_flutter`). Passing
/// `null` for `SyncSettingsScreen.backup` omits the section entirely
/// (diet_guard, wake_alarm have no local backup at all).
class BackupSlot {
/// Creates a [BackupSlot] labelled [label] (e.g. `'notes'`,
/// `'inventory'`), backed by [export] and [import].
@ -16,6 +19,28 @@ class BackupSlot {
required this.import,
});
/// A slot whose backup is a JSON file, using `data_transfer_flutter`.
///
/// Export saves `build()` as [fileName] to device storage (system Save-as
/// on Android/iOS, a save dialog on desktop, a download on web). Import
/// lets the user pick a JSON file and hands the decoded object to
/// [restore]; a cancelled picker restores nothing.
factory BackupSlot.json({
required String label,
required String fileName,
required Future<Map<String, Object?>> Function() build,
required Future<void> Function(Map<String, Object?> json) restore,
}) => BackupSlot(
label: label,
export: () async {
await exportJson(fileName: fileName, json: await build());
},
import: () async {
final json = await pickJson();
if (json != null) await restore(json);
},
);
/// Noun describing what this exports/imports, used in the section's
/// button text ("Export $label" / "Import $label").
final String label;

View File

@ -5,7 +5,7 @@ description: >-
injected closures -- no keystore or platform plugin access lives in this
package itself.
publish_to: 'none'
version: 0.4.3
version: 0.5.0
environment:
sdk: ^3.12.2
@ -16,6 +16,11 @@ dependencies:
url: https://github.com/kuhyx/utils
ref: crdt_sync_dart-v0.11.3
path: crdt_sync_dart
data_transfer_flutter:
git:
url: https://github.com/kuhyx/utils
ref: data_transfer_flutter-v0.1.0
path: data_transfer_flutter
flutter:
sdk: flutter

View File

@ -1,3 +1,6 @@
import 'dart:convert';
import 'package:data_transfer_flutter/data_transfer_flutter.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sync_settings_ui/sync_settings_ui.dart';
@ -16,4 +19,83 @@ void main() {
expect(exported, isTrue);
expect(imported, isTrue);
});
group('BackupSlot.json', () {
late _FakePlatform fake;
late DataTransferPlatform original;
setUp(() {
original = DataTransfer.platform;
fake = _FakePlatform();
DataTransfer.platform = fake;
});
tearDown(() => DataTransfer.platform = original);
test('export saves the built map under fileName', () async {
final slot = BackupSlot.json(
label: 'inventory',
fileName: 'inv.json',
build: () async => {'items': 3},
restore: (_) async {},
);
await slot.export();
expect(fake.saved.single.$1, 'inv.json');
expect(jsonDecode(fake.saved.single.$2), {'items': 3});
});
test('import hands the picked map to restore', () async {
fake.picked = '{"items":3}';
Map<String, Object?>? restored;
final slot = BackupSlot.json(
label: 'inventory',
fileName: 'inv.json',
build: () async => {},
restore: (json) async => restored = json,
);
await slot.import();
expect(restored, {'items': 3});
});
test('a cancelled picker restores nothing', () async {
var called = false;
final slot = BackupSlot.json(
label: 'inventory',
fileName: 'inv.json',
build: () async => {},
restore: (_) async => called = true,
);
await slot.import();
expect(called, isFalse);
});
});
}
class _FakePlatform implements DataTransferPlatform {
String? picked;
final saved = <(String, String)>[];
@override
Future<bool> saveText({
required String fileName,
required String text,
}) async {
saved.add((fileName, text));
return true;
}
@override
Future<bool> shareText({
required String fileName,
required String text,
}) async => true;
@override
Future<String?> pickText() async => picked;
}