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.


docs/superpowers/specs/2026-07-18-backup-export-import-design.md
# Backup Export/Import Design

## Problem

Users have no way to back up or transfer their highlights, highlight history,
reading position, and display settings. All of this lives only in
`app_state.json` inside the app's private documents directory
(`lib/store/app_persistor.dart`), which is invisible to the user and lost on
uninstall or device change. We want a "Backup" option in the settings bottom
sheet that lets a user export their full app state to a JSON file they
control, and later import it back (e.g. on a new device, or as manual
insurance).

## Scope

- Export the **entire `AppState`** (highlights, highlight history, reading
  position, display settings) as a JSON file, using a native "Save As" dialog
  so the user picks the destination.
- Import a previously-exported JSON file via a native file-open dialog,
  **replacing** the current `AppState` wholesale after user confirmation.
- Both actions are added to the existing settings bottom sheet
  (`lib/widgets/settings_sheet.dart`).
- Out of scope: merge-on-import, partial export (e.g. highlights only),
  automatic/scheduled backups, cloud sync.

## Architecture

### New dependency

Add `file_picker` to `pubspec.yaml`. It covers both directions needed here:

- `FilePicker.platform.saveFile(...)` — native "Save As" dialog for export.
- `FilePicker.platform.pickFiles(...)` — native file-open dialog for import.

No other new dependency is needed; `share_plus` remains used only for its
existing purpose (`ShareVersesAction`).

### New actions

A new file, `lib/store/actions_backup.dart`, holds two async_redux actions
following the existing async-action convention seen in
`lib/store/actions_navigation.dart` (`ShareVersesAction`,
`ShowSettingsAction`) and `lib/store/actions_state.dart`
(`TogglePlayAction`) — i.e. `Future<AppState?> reduce() async`, constructed
with a `BuildContext buildContext` for showing dialogs/errors via the
existing helpers in `lib/dialog.dart`:

- **`ExportAppStateAction({required BuildContext buildContext})`**
- **`ImportAppStateAction({required BuildContext buildContext})`**

### UI

`SettingsSheet` gets a new section (styled like the existing Material cards)
below the current toggles: two rows, "Export Backup" (download icon,
dispatches `ExportAppStateAction`) and "Import Backup" (upload icon,
dispatches `ImportAppStateAction`).

## Data flow

### Export

1. User taps "Export Backup".
2. `ExportAppStateAction.reduce()` serializes `state.toJson()` to a
   pretty-printed JSON string (`JsonEncoder.withIndent`).
3. Builds a suggested filename: `only_bible_app_backup_<yyyy-MM-dd>.json`.
4. Calls `FilePicker.platform.saveFile(fileName: ..., bytes: utf8.encode(json))`.
5. On success, shows a confirmation via `showAlert` (`lib/dialog.dart`). On
   cancel (user backs out of the dialog), no-ops silently.
6. Returns `null` — no state change.

### Import

1. User taps "Import Backup".
2. `ImportAppStateAction.reduce()` shows a Yes/No confirmation dialog (same
   two-button pattern as `showReportError` in `lib/dialog.dart:81-117`),
   warning that current highlights/history/settings will be replaced.
3. On "No" or dialog dismissal, no-ops.
4. On "Yes", calls `FilePicker.platform.pickFiles(type: FileType.custom,
   allowedExtensions: ['json'])`. On cancel, no-ops.
5. Reads the picked file, `jsonDecode`s it.
6. Resolves `bibleName` from the JSON (falls back the same way
   `main.dart:79-93` does) and loads the corresponding `Bible` via the
   existing `loadBible()` helper.
7. Constructs `AppState.fromJson(json, bible)`, wrapped in try/catch exactly
   like the startup-restore logic in `main.dart:79-93`.
8. On success, `reduce()` returns the new `AppState`. async_redux applies it,
   and `AppPersistor` auto-persists it to `app_state.json` on its normal
   throttle (1s) — no separate persistence step needed.
9. On any failure in steps 4-7, the current state is left untouched (`reduce()`
   returns `null`), so the on-disk backup is never partially overwritten.

## Error handling

| Situation | Behavior |
|---|---|
| User cancels save/open dialog | Silent no-op, no error shown |
| Export write fails | `showError(buildContext, "Failed to export backup")` |
| Import: invalid JSON / wrong shape / unloadable `bibleName` | Caught broadly (mirrors startup try/catch); `showError(buildContext, "That file isn't a valid backup")`; state unchanged |
| User declines the "replace state?" confirmation | No-op |

## Testing

- Unit test: `AppState.toJson()``AppState.fromJson()` round-trip
  preserves highlights, highlight history, reading position, and settings.
- Unit test: malformed/garbage JSON passed to the import path is rejected
  without throwing past the action (surfaces as the "invalid backup" error,
  state unchanged).
- Manual verification: run the app (`flutter run -d macos`, since macOS
  support was recently added) and click through a real export → import
  round-trip, confirming the native dialogs appear and highlights survive
  the round-trip.