feat(settings): storage picker and export screen

The store picker and the export screen were the two frontend surfaces
the own-store work left unbuilt, so both backends shipped unreachable.
Settings gains a Storage section holding them: a full-screen picker over
Own / an installed external provider (dimmed when none is present, named
after the provider's own app), and an export screen with a per-list tick
and the two SAF destinations, a folder or a single zip. The picker asks
for the provider's runtime permission before writing the mode, so a
denial leaves the readable store in place instead of dropping the user on
the gate; a refusal is reported with a route to app settings.

Making the mode switchable at runtime had two consequences:

- reminders are armed off whichever store was active when they were
  scheduled, so a switch rebuilds the set. ReminderScheduler.sync() is
  now serialised — it is a read-modify-write over ScheduledReminderStore,
  and overlapping runs each wrote their own set as the whole truth
- the permission gate is the only screen an External user can reach once
  their provider app stops answering, so it offers the way back to our
  own store

ExportWriter no longer deletes a previous export before recreating it (a
failure in between lost both), lists the target directory once instead of
per document, and carries a typed ExportFailure so the screen can report
in the user's language rather than an exception message.
This commit is contained in:
2026-09-04 15:37:48 +02:00
parent 9ff6027e50
commit ec2e2eb59d
17 changed files with 845 additions and 79 deletions

View File

@@ -19,10 +19,11 @@ iCalendar has landed, and the Material 3 Expressive UI is built through **M5**:
lists → task list (swipe gestures, inline add, smart-list section headers) →
detail / edit with full CRUD, date-time pickers, priority, percent-complete,
conflict-safe saves, per-task reminders, and subtask create + reparent — plus a
one-time reminder onboarding step and a Settings screen. Remaining work is
hardening the new store (`OWN-STORE.md` phase 6), the **frontend surfaces for
what has landed** (a storage-mode picker, an export screen), then M6 (Glance
widget, translations, F-Droid release) and the sync adapter.
one-time reminder onboarding step and a Settings screen. The frontend surfaces
for the new store have landed too: a Settings **Storage** section with the
store picker and an **export screen**. Remaining work is verifying all of it on
a device, then M6 (Glance widget, translations, F-Droid release) and the sync
adapter.
---
@@ -147,8 +148,17 @@ what Posture B means (our own store, coexisting with everything — *not* squatt
- ✅ Export to iCalendar (step 3) — a v1 feature now that own-mode data lives
only in our app's private storage. One `.ics` per list, to a folder or a zip,
via SAF. Backend only.
- **Frontend surfaces for the above**a storage-mode picker in Settings and
an export screen. The backend is done and unused until these exist.
- **Frontend surfaces for the above**Settings gained a **Storage** section
holding both: a full-screen store picker (Own / an installed external provider,
which is dimmed when none is present) that asks for the provider's runtime
permission *before* committing the switch, and an export screen with a per-list
tick and the two SAF destinations, a folder or a single zip. Two consequences
of the mode becoming switchable at runtime came with it: reminders are re-armed
against the new store on every switch (`AgendulaApp` listens on
`ProviderResolver.onModeChanged`; previously only a restart, a boot or an edit
resynced them), and the permission gate offers a way back to our own store —
otherwise a user whose provider app went away is held on a gate with Settings
behind it.
- ⬜ File the DAVx5 issue (step 4) — non-blocking, cheap, serves F-Droid users.
Note it now means "sync into an app that has no provider", so the ask has
changed shape.
@@ -264,9 +274,9 @@ These carry over from [`PLAN.md`](PLAN.md) §9; resolved ones are struck through
resolved: our own store expands a series at read time and writes an edit to
one occurrence as a `RECURRENCE-ID` override sharing the master's UID; in
External mode the edit still goes through the instances URI.
6. **Resolver ordering / mode-selection UX** `autoMode()` picks a sane default
today (see [`ARCHITECTURE.md`](ARCHITECTURE.md) §4.1); the Settings override it
assumes is not built yet.
6. ~~**Resolver ordering / mode-selection UX**~~ resolved: `autoMode()` picks the
default (see [`ARCHITECTURE.md`](ARCHITECTURE.md) §4.1) and Settings → Storage
→ Task store is the override it always assumed.
7. ~~**Sync protocol coverage**, account model, conflict resolution — the next
design discussion.~~ Taken up in [`SYNC.md`](SYNC.md); the remaining opens
live on that document's list.