feat: introduce reusable app lists for rules

- AppList @Model with launch migration from legacy inline selections
  (rules with identical selections share one list; idempotent)
- rules point at one app list; Block/Allow Only belongs to the rule and
  is offered only in the Schedule editor (limit kinds sanitize to Block)
- app-list picker sheet (select / create / edit / delete with in-use
  protection) replaces the per-rule selection sheet
- Hard Mode locks app-list editing while any hard rule is blocking
- SwiftData stability: relationships are only wired between managed
  models, and unit tests share one in-memory container (fresh context +
  data wipe per test) — per-test container creation trapped
  intermittently inside SwiftData

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-12 20:02:35 -04:00
parent ee92b1b248
commit d8d7028d60
22 changed files with 880 additions and 138 deletions

View File

@@ -23,7 +23,16 @@ Common attributes across all types:
- **Name** — user-editable, free text (presets: "Work Time", "Weekend Zen", "Locked In", "Sleep", "Wind Down", "Deep Sleep", "Laser Focus", "Reading Time", "Gym Time"; defaults for new rules: "In the Zone" (schedule), "Time Keeper" (time limit))
- **Days of week** — 7-day toggle set, summarized as "Weekdays" / "Weekends" / "Every day" / custom
- **App selection** — apps, categories, and websites; selection mode is either **Block** (block selected) or **Allow Only** (block everything except selected)
- **App List** *(OpenAppLock refinement)* — each rule points to exactly one
**App List**: a named, reusable selection of apps/categories/websites stored
independently of any rule. When editing a rule the user picks an existing
list or creates a new one; editing a list affects every rule that uses it.
Deleting a list detaches it from its rules (they fall back to "no apps").
- **Selection mode** — **Block** (block the list) or **Allow Only** (block
everything except the list). The mode belongs to the *rule*, not the list.
Only **Schedule** rules offer the choice; Time Limit and Open Limit rules
are always Block (a usage budget over "everything except X" is not
meaningful, and drafts of those kinds are sanitized back to Block).
- **Hard Mode** — boolean, PRO-gated in Opal; subtitle "No unblocks allowed". When off, the rule detail shows "Unblocks allowed: Yes"
- **Enabled/disabled** — a rule can be disabled without deleting ("Disable Rule")
@@ -147,7 +156,11 @@ Sections (each an inset rounded group with a small icon + caption header):
row of 7 circular toggles `S M T W T F S`; selected = filled white circle
with black letter, unselected = dark circle.
3. **🛡 Apps are blocked**
- Row: `Selected Apps``N Apps ` — pushes the App Picker.
- Row: `App List``<list name> · N Apps ` (or `Choose ` when none) —
presents the App List picker.
- A segmented `Block | Allow Only` row (Schedule editor only) chooses how
the rule interprets its list; the section header reads "Apps are
blocked" / "Only these apps are allowed" accordingly.
4. **Hard Mode** `⚡PRO` badge — subtitle "No unblocks allowed"; trailing
toggle.
5. **Block Adult Content** *(OpenAppLock addition — not in the Opal video)*
@@ -209,8 +222,22 @@ Full-height sheet:
> parties cannot enumerate installed apps; the system-sanctioned route is
> `FamilyActivityPicker` (FamilyControls), which provides its own
> category/app/website UI and returns opaque tokens. **v1 of OpenAppLock should
> embed `FamilyActivityPicker`** instead of cloning Opal's custom picker, and
> keep the `Block`/`Allow Only` segmented control as our own wrapper state.
> embed `FamilyActivityPicker`** instead of cloning Opal's custom picker.
>
> **App Lists (OpenAppLock):** the selection itself lives on a reusable
> **App List** (`@Model AppList`: name + encoded `FamilyActivitySelection`).
> The editor's App List row presents a picker sheet listing saved lists
> (checkmark on the rule's current list; tap to select), an Edit affordance
> per list, and a "New List" flow — a name field plus an embedded
> `FamilyActivityPicker`. The `Block`/`Allow Only` segmented control lives in
> the Schedule rule editor (it is rule state, not list state). Legacy rules
> that stored an inline selection are migrated at launch: one list per
> distinct selection (rules sharing identical selection data share a list),
> named "<rule name> Apps". Lists in use by a rule cannot be deleted from the
> picker. While any **Hard Mode** rule is actively blocking, all lists are
> read-only — the picker hides Edit/Delete and shows a lock notice — because
> editing a list would be a back door out of the hard block. Creating new
> lists and selecting lists for other rules remain available.
---