only-bible-app v3.7.2+24

#kotlin#android#ios

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
fe24dd6 1
import "dart:convert";
fe24dd6 2
import "dart:io";
fe24dd6 3
fe24dd6 4
import "package:async_redux/async_redux.dart";
0228ae8 5
import "package:file_selector/file_selector.dart";
fe24dd6 6
import "package:flutter/foundation.dart";
fe24dd6 7
import "package:flutter/material.dart";
fe24dd6 8
import "package:only_bible_app/dialog.dart";
fe24dd6 9
import "package:only_bible_app/gen/bible.gen.dart";
fe24dd6 10
import "package:only_bible_app/store/app_state.dart";
fe24dd6 11
import "package:only_bible_app/utils.dart";
0228ae8 12
import "package:path_provider/path_provider.dart";
0228ae8 13
import "package:share_plus/share_plus.dart";
fe24dd6 14
fe24dd6 15
String buildBackupFileName([DateTime? now]) {
fe24dd6 16
  final n = now ?? DateTime.now();
fe24dd6 17
  final y = n.year.toString().padLeft(4, "0");
fe24dd6 18
  final m = n.month.toString().padLeft(2, "0");
fe24dd6 19
  final d = n.day.toString().padLeft(2, "0");
fe24dd6 20
  return "only_bible_app_backup_$y-$m-$d.json";
fe24dd6 21
}
fe24dd6 22
fe24dd6 23
String buildBackupJson(AppState state) {
fe24dd6 24
  return const JsonEncoder.withIndent("  ").convert(state.toJson());
fe24dd6 25
}
fe24dd6 26
fe24dd6 27
class BackupParseException implements Exception {
fe24dd6 28
  final String message;
fe24dd6 29
fe24dd6 30
  const BackupParseException(this.message);
fe24dd6 31
fe24dd6 32
  @override
fe24dd6 33
  String toString() => message;
fe24dd6 34
}
fe24dd6 35
fe24dd6 36
Future<AppState> parseBackupState(
fe24dd6 37
  String contents, {
fe24dd6 38
  Future<Bible> Function(String) loadBibleFn = loadBible,
fe24dd6 39
}) async {
fe24dd6 40
  final Map<String, dynamic> json;
fe24dd6 41
  try {
fe24dd6 42
    final decoded = jsonDecode(contents);
fe24dd6 43
    if (decoded is! Map<String, dynamic>) {
fe24dd6 44
      throw const FormatException("backup is not a JSON object");
fe24dd6 45
    }
fe24dd6 46
    json = decoded;
fe24dd6 47
  } catch (_) {
fe24dd6 48
    throw const BackupParseException("That file isn't a valid backup");
fe24dd6 49
  }
fe24dd6 50
fe24dd6 51
  final bibleName = json["bibleName"] as String? ?? "en_kjv";
fe24dd6 52
  final Bible bible;
fe24dd6 53
  try {
fe24dd6 54
    bible = await loadBibleFn(bibleName);
fe24dd6 55
  } catch (_) {
fe24dd6 56
    throw const BackupParseException("That file isn't a valid backup");
fe24dd6 57
  }
fe24dd6 58
fe24dd6 59
  try {
fe24dd6 60
    return AppState.fromJson(json, bible);
fe24dd6 61
  } catch (_) {
fe24dd6 62
    throw const BackupParseException("That file isn't a valid backup");
fe24dd6 63
  }
fe24dd6 64
}
fe24dd6 65
fe24dd6 66
class ExportAppStateAction extends ReduxAction<AppState> {
fe24dd6 67
  final BuildContext buildContext;
fe24dd6 68
fe24dd6 69
  ExportAppStateAction(this.buildContext);
fe24dd6 70
fe24dd6 71
  @override
fe24dd6 72
  Future<AppState?> reduce() async {
fe24dd6 73
    final jsonString = buildBackupJson(state);
fe24dd6 74
    final fileName = buildBackupFileName();
0228ae8 75
    // file_selector's save-location dialog isn't supported on Android/iOS
0228ae8 76
    // (only desktop and web), so mobile exports go through the share sheet
0228ae8 77
    // (Save to Files, AirDrop, etc.) instead.
0228ae8 78
    final canPickSaveLocation = kIsWeb || isDesktop;
fe24dd6 79
    try {
0228ae8 80
      if (canPickSaveLocation) {
0228ae8 81
        final location = await getSaveLocation(
0228ae8 82
          suggestedName: fileName,
0228ae8 83
          acceptedTypeGroups: const [
0228ae8 84
            XTypeGroup(label: "JSON", extensions: ["json"]),
0228ae8 85
          ],
0228ae8 86
        );
0228ae8 87
        if (location == null) return null; // user cancelled
0228ae8 88
        final file = XFile.fromData(
0228ae8 89
          utf8.encode(jsonString),
0228ae8 90
          mimeType: "application/json",
0228ae8 91
          name: fileName,
0228ae8 92
        );
0228ae8 93
        await file.saveTo(location.path);
0228ae8 94
      } else {
0228ae8 95
        final dir = await getTemporaryDirectory();
0228ae8 96
        final path = "${dir.path}/$fileName";
fe24dd6 97
        await File(path).writeAsString(jsonString);
0228ae8 98
        final result = await SharePlus.instance.share(
0228ae8 99
          ShareParams(files: [XFile(path)]),
0228ae8 100
        );
0228ae8 101
        if (result.status == ShareResultStatus.dismissed) return null;
fe24dd6 102
      }
fe24dd6 103
    } catch (_) {
fe24dd6 104
      if (buildContext.mounted) {
fe24dd6 105
        showError(buildContext, "Failed to export backup");
fe24dd6 106
      }
fe24dd6 107
      return null;
fe24dd6 108
    }
fe24dd6 109
    if (buildContext.mounted) {
0228ae8 110
      showAlert(
0228ae8 111
        buildContext,
0228ae8 112
        "Backup Exported",
0228ae8 113
        "Your backup was saved successfully.",
0228ae8 114
      );
fe24dd6 115
    }
fe24dd6 116
    return null;
fe24dd6 117
  }
fe24dd6 118
}
fe24dd6 119
fe24dd6 120
class ImportAppStateAction extends ReduxAction<AppState> {
fe24dd6 121
  final BuildContext buildContext;
fe24dd6 122
fe24dd6 123
  ImportAppStateAction(this.buildContext);
fe24dd6 124
fe24dd6 125
  @override
fe24dd6 126
  Future<AppState?> reduce() async {
fe24dd6 127
    final confirmed = await showConfirm(
fe24dd6 128
      buildContext,
fe24dd6 129
      "Import Backup",
fe24dd6 130
      "This will replace all your current highlights and settings. Continue?",
fe24dd6 131
    );
fe24dd6 132
    if (!confirmed) return null;
fe24dd6 133
    if (!buildContext.mounted) return null;
fe24dd6 134
0228ae8 135
    final file = await openFile(
0228ae8 136
      acceptedTypeGroups: const [
0228ae8 137
        XTypeGroup(label: "JSON", extensions: ["json"]),
0228ae8 138
      ],
fe24dd6 139
    );
0228ae8 140
    if (file == null) return null; // user cancelled
fe24dd6 141
fe24dd6 142
    try {
0228ae8 143
      final bytes = await file.readAsBytes();
fe24dd6 144
      final newState = await parseBackupState(utf8.decode(bytes));
fe24dd6 145
      if (buildContext.mounted) {
0228ae8 146
        showAlert(
0228ae8 147
          buildContext,
0228ae8 148
          "Backup Imported",
0228ae8 149
          "Your backup was restored successfully.",
0228ae8 150
        );
fe24dd6 151
      }
fe24dd6 152
      return newState;
fe24dd6 153
    } on BackupParseException catch (err) {
fe24dd6 154
      if (buildContext.mounted) {
fe24dd6 155
        showError(buildContext, err.message);
fe24dd6 156
      }
fe24dd6 157
      return null;
0228ae8 158
    } catch (_) {
0228ae8 159
      if (buildContext.mounted) {
0228ae8 160
        showError(buildContext, "That file isn't a valid backup");
0228ae8 161
      }
0228ae8 162
      return null;
fe24dd6 163
    }
fe24dd6 164
  }
fe24dd6 165
}