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
fe24dd6 1
# Backup Export/Import Design
fe24dd6 2
fe24dd6 3
## Problem
fe24dd6 4
fe24dd6 5
Users have no way to back up or transfer their highlights, highlight history,
fe24dd6 6
reading position, and display settings. All of this lives only in
fe24dd6 7
`app_state.json` inside the app's private documents directory
fe24dd6 8
(`lib/store/app_persistor.dart`), which is invisible to the user and lost on
fe24dd6 9
uninstall or device change. We want a "Backup" option in the settings bottom
fe24dd6 10
sheet that lets a user export their full app state to a JSON file they
fe24dd6 11
control, and later import it back (e.g. on a new device, or as manual
fe24dd6 12
insurance).
fe24dd6 13
fe24dd6 14
## Scope
fe24dd6 15
fe24dd6 16
- Export the **entire `AppState`** (highlights, highlight history, reading
fe24dd6 17
  position, display settings) as a JSON file, using a native "Save As" dialog
fe24dd6 18
  so the user picks the destination.
fe24dd6 19
- Import a previously-exported JSON file via a native file-open dialog,
fe24dd6 20
  **replacing** the current `AppState` wholesale after user confirmation.
fe24dd6 21
- Both actions are added to the existing settings bottom sheet
fe24dd6 22
  (`lib/widgets/settings_sheet.dart`).
fe24dd6 23
- Out of scope: merge-on-import, partial export (e.g. highlights only),
fe24dd6 24
  automatic/scheduled backups, cloud sync.
fe24dd6 25
fe24dd6 26
## Architecture
fe24dd6 27
fe24dd6 28
### New dependency
fe24dd6 29
fe24dd6 30
Add `file_picker` to `pubspec.yaml`. It covers both directions needed here:
fe24dd6 31
fe24dd6 32
- `FilePicker.platform.saveFile(...)` — native "Save As" dialog for export.
fe24dd6 33
- `FilePicker.platform.pickFiles(...)` — native file-open dialog for import.
fe24dd6 34
fe24dd6 35
No other new dependency is needed; `share_plus` remains used only for its
fe24dd6 36
existing purpose (`ShareVersesAction`).
fe24dd6 37
fe24dd6 38
### New actions
fe24dd6 39
fe24dd6 40
A new file, `lib/store/actions_backup.dart`, holds two async_redux actions
fe24dd6 41
following the existing async-action convention seen in
fe24dd6 42
`lib/store/actions_navigation.dart` (`ShareVersesAction`,
fe24dd6 43
`ShowSettingsAction`) and `lib/store/actions_state.dart`
fe24dd6 44
(`TogglePlayAction`) — i.e. `Future<AppState?> reduce() async`, constructed
fe24dd6 45
with a `BuildContext buildContext` for showing dialogs/errors via the
fe24dd6 46
existing helpers in `lib/dialog.dart`:
fe24dd6 47
fe24dd6 48
- **`ExportAppStateAction({required BuildContext buildContext})`**
fe24dd6 49
- **`ImportAppStateAction({required BuildContext buildContext})`**
fe24dd6 50
fe24dd6 51
### UI
fe24dd6 52
fe24dd6 53
`SettingsSheet` gets a new section (styled like the existing Material cards)
fe24dd6 54
below the current toggles: two rows, "Export Backup" (download icon,
fe24dd6 55
dispatches `ExportAppStateAction`) and "Import Backup" (upload icon,
fe24dd6 56
dispatches `ImportAppStateAction`).
fe24dd6 57
fe24dd6 58
## Data flow
fe24dd6 59
fe24dd6 60
### Export
fe24dd6 61
fe24dd6 62
1. User taps "Export Backup".
fe24dd6 63
2. `ExportAppStateAction.reduce()` serializes `state.toJson()` to a
fe24dd6 64
   pretty-printed JSON string (`JsonEncoder.withIndent`).
fe24dd6 65
3. Builds a suggested filename: `only_bible_app_backup_<yyyy-MM-dd>.json`.
fe24dd6 66
4. Calls `FilePicker.platform.saveFile(fileName: ..., bytes: utf8.encode(json))`.
fe24dd6 67
5. On success, shows a confirmation via `showAlert` (`lib/dialog.dart`). On
fe24dd6 68
   cancel (user backs out of the dialog), no-ops silently.
fe24dd6 69
6. Returns `null` — no state change.
fe24dd6 70
fe24dd6 71
### Import
fe24dd6 72
fe24dd6 73
1. User taps "Import Backup".
fe24dd6 74
2. `ImportAppStateAction.reduce()` shows a Yes/No confirmation dialog (same
fe24dd6 75
   two-button pattern as `showReportError` in `lib/dialog.dart:81-117`),
fe24dd6 76
   warning that current highlights/history/settings will be replaced.
fe24dd6 77
3. On "No" or dialog dismissal, no-ops.
fe24dd6 78
4. On "Yes", calls `FilePicker.platform.pickFiles(type: FileType.custom,
fe24dd6 79
   allowedExtensions: ['json'])`. On cancel, no-ops.
fe24dd6 80
5. Reads the picked file, `jsonDecode`s it.
fe24dd6 81
6. Resolves `bibleName` from the JSON (falls back the same way
fe24dd6 82
   `main.dart:79-93` does) and loads the corresponding `Bible` via the
fe24dd6 83
   existing `loadBible()` helper.
fe24dd6 84
7. Constructs `AppState.fromJson(json, bible)`, wrapped in try/catch exactly
fe24dd6 85
   like the startup-restore logic in `main.dart:79-93`.
fe24dd6 86
8. On success, `reduce()` returns the new `AppState`. async_redux applies it,
fe24dd6 87
   and `AppPersistor` auto-persists it to `app_state.json` on its normal
fe24dd6 88
   throttle (1s) — no separate persistence step needed.
fe24dd6 89
9. On any failure in steps 4-7, the current state is left untouched (`reduce()`
fe24dd6 90
   returns `null`), so the on-disk backup is never partially overwritten.
fe24dd6 91
fe24dd6 92
## Error handling
fe24dd6 93
fe24dd6 94
| Situation | Behavior |
fe24dd6 95
|---|---|
fe24dd6 96
| User cancels save/open dialog | Silent no-op, no error shown |
fe24dd6 97
| Export write fails | `showError(buildContext, "Failed to export backup")` |
fe24dd6 98
| Import: invalid JSON / wrong shape / unloadable `bibleName` | Caught broadly (mirrors startup try/catch); `showError(buildContext, "That file isn't a valid backup")`; state unchanged |
fe24dd6 99
| User declines the "replace state?" confirmation | No-op |
fe24dd6 100
fe24dd6 101
## Testing
fe24dd6 102
fe24dd6 103
- Unit test: `AppState.toJson()` → `AppState.fromJson()` round-trip
fe24dd6 104
  preserves highlights, highlight history, reading position, and settings.
fe24dd6 105
- Unit test: malformed/garbage JSON passed to the import path is rejected
fe24dd6 106
  without throwing past the action (surfaces as the "invalid backup" error,
fe24dd6 107
  state unchanged).
fe24dd6 108
- Manual verification: run the app (`flutter run -d macos`, since macOS
fe24dd6 109
  support was recently added) and click through a real export → import
fe24dd6 110
  round-trip, confirming the native dialogs appear and highlights survive
fe24dd6 111
  the round-trip.