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.
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. |