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
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;
    }
  }
}