only-bible-app v3.7.2+24
git clone https://git.pyrossh.dev/only-bible-app
The only bible app you will ever need. No ads. No in-app purchases. No distractions.
lib/store/actions_backup.dart
import "dart:convert";
import "dart:io";
import "package:async_redux/async_redux.dart";
import "package:file_selector/file_selector.dart";
import "package:flutter/foundation.dart";
import "package:flutter/material.dart";
import "package:only_bible_app/dialog.dart";
import "package:only_bible_app/gen/bible.gen.dart";
import "package:only_bible_app/store/app_state.dart";
import "package:only_bible_app/utils.dart";
import "package:path_provider/path_provider.dart";
import "package:share_plus/share_plus.dart";
String buildBackupFileName([DateTime? now]) {
final n = now ?? DateTime.now();
final y = n.year.toString().padLeft(4, "0");
final m = n.month.toString().padLeft(2, "0");
final d = n.day.toString().padLeft(2, "0");
return "only_bible_app_backup_$y-$m-$d.json";
}
String buildBackupJson(AppState state) {
return const JsonEncoder.withIndent(" ").convert(state.toJson());
}
class BackupParseException implements Exception {
final String message;
const BackupParseException(this.message);
String toString() => message;
}
Future<AppState> parseBackupState(
String contents, {
Future<Bible> Function(String) loadBibleFn = loadBible,
}) async {
final Map<String, dynamic> json;
try {
final decoded = jsonDecode(contents);
if (decoded is! Map<String, dynamic>) {
throw const FormatException("backup is not a JSON object");
}
json = decoded;
} catch (_) {
throw const BackupParseException("That file isn't a valid backup");
}
final bibleName = json["bibleName"] as String? ?? "en_kjv";
final Bible bible;
try {
bible = await loadBibleFn(bibleName);
} catch (_) {
throw const BackupParseException("That file isn't a valid backup");
}
try {
return AppState.fromJson(json, bible);
} catch (_) {
throw const BackupParseException("That file isn't a valid backup");
}
}
class ExportAppStateAction extends ReduxAction<AppState> {
final BuildContext buildContext;
ExportAppStateAction(this.buildContext);
Future<AppState?> reduce() async {
final jsonString = buildBackupJson(state);
final fileName = buildBackupFileName();
// file_selector's save-location dialog isn't supported on Android/iOS
// (only desktop and web), so mobile exports go through the share sheet
// (Save to Files, AirDrop, etc.) instead.
final canPickSaveLocation = kIsWeb || isDesktop;
try {
if (canPickSaveLocation) {
final location = await getSaveLocation(
suggestedName: fileName,
acceptedTypeGroups: const [
XTypeGroup(label: "JSON", extensions: ["json"]),
],
);
if (location == null) return null; // user cancelled
final file = XFile.fromData(
utf8.encode(jsonString),
mimeType: "application/json",
name: fileName,
);
await file.saveTo(location.path);
} else {
final dir = await getTemporaryDirectory();
final path = "${dir.path}/$fileName";
await File(path).writeAsString(jsonString);
final result = await SharePlus.instance.share(
ShareParams(files: [XFile(path)]),
);
if (result.status == ShareResultStatus.dismissed) return null;
}
} catch (_) {
if (buildContext.mounted) {
showError(buildContext, "Failed to export backup");
}
return null;
}
if (buildContext.mounted) {
showAlert(
buildContext,
"Backup Exported",
"Your backup was saved successfully.",
);
}
return null;
}
}
class ImportAppStateAction extends ReduxAction<AppState> {
final BuildContext buildContext;
ImportAppStateAction(this.buildContext);
Future<AppState?> reduce() async {
final confirmed = await showConfirm(
buildContext,
"Import Backup",
"This will replace all your current highlights and settings. Continue?",
);
if (!confirmed) return null;
if (!buildContext.mounted) return null;
final file = await openFile(
acceptedTypeGroups: const [
XTypeGroup(label: "JSON", extensions: ["json"]),
],
);
if (file == null) return null; // user cancelled
try {
final bytes = await file.readAsBytes();
final newState = await parseBackupState(utf8.decode(bytes));
if (buildContext.mounted) {
showAlert(
buildContext,
"Backup Imported",
"Your backup was restored successfully.",
);
}
return newState;
} on BackupParseException catch (err) {
if (buildContext.mounted) {
showError(buildContext, err.message);
}
return null;
} catch (_) {
if (buildContext.mounted) {
showError(buildContext, "That file isn't a valid backup");
}
return null;
}
}
}