feat: clone Opal's rules feature with hard block enforcement
Rebuild the app around recurring screen-time blocking rules modeled on Opal's My Apps tab: - Onboarding with FamilyControls Screen Time authorization - Apps home with Blocked Apps tiles and live rule cards - New Rule sheet: Schedule/Time Limit/Open Limit types + preset gallery - Rule editors: time windows (incl. overnight), day picker, app selection via FamilyActivityPicker (Block/Allow Only), rename, Hold to Commit - Hard Mode: active hard rules cannot be edited, disabled, deleted, or unblocked until their window ends; soft rules pause until next window - Shield enforcement through per-rule ManagedSettingsStore + RuleEnforcer - 73 unit tests (Swift Testing) + 16 UI tests (XCUITest) with launch-arg harness for in-memory storage, mocked authorization, seeded scenarios - docs/RULES_FEATURE_SPEC.md: spec derived from the reference recording Known gap: background window transitions and time/open-limit thresholds need a DeviceActivityMonitor extension; shields currently sync while the app is running. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
65
Severed/Services/SampleRules.swift
Normal file
65
Severed/Services/SampleRules.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// SampleRules.swift
|
||||
// Severed
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
/// Builds deterministic rules for UI-test scenarios, positioned relative to
|
||||
/// "now" so an active window is genuinely active whenever the test runs.
|
||||
enum SampleRules {
|
||||
static func seed(
|
||||
_ scenario: LaunchConfiguration.SeedScenario,
|
||||
into context: ModelContext,
|
||||
now: Date = .now,
|
||||
calendar: Calendar = .current
|
||||
) {
|
||||
switch scenario {
|
||||
case .standard:
|
||||
context.insert(activeRule(named: "Work Time", hardMode: false, now: now, calendar: calendar))
|
||||
context.insert(upcomingRule(named: "Sleep", now: now, calendar: calendar))
|
||||
case .hardModeActive:
|
||||
context.insert(activeRule(named: "Locked In", hardMode: true, now: now, calendar: calendar))
|
||||
context.insert(upcomingRule(named: "Sleep", now: now, calendar: calendar))
|
||||
}
|
||||
}
|
||||
|
||||
/// A schedule rule whose window started up to an hour ago and runs for
|
||||
/// several hours, clamped so it never crosses midnight accidentally.
|
||||
static func activeRule(
|
||||
named name: String, hardMode: Bool, now: Date, calendar: Calendar = .current
|
||||
) -> BlockingRule {
|
||||
let nowMinutes = minutesIntoDay(of: now, calendar: calendar)
|
||||
let start = max(0, nowMinutes - 60)
|
||||
let end = min(24 * 60 - 1, nowMinutes + 6 * 60)
|
||||
return BlockingRule(
|
||||
name: name,
|
||||
hardMode: hardMode,
|
||||
days: Weekday.everyDay,
|
||||
startMinutes: start,
|
||||
endMinutes: end
|
||||
)
|
||||
}
|
||||
|
||||
/// A schedule rule starting a couple of hours from now (possibly wrapping
|
||||
/// past midnight, which simply makes it start tomorrow).
|
||||
static func upcomingRule(
|
||||
named name: String, now: Date, calendar: Calendar = .current
|
||||
) -> BlockingRule {
|
||||
let nowMinutes = minutesIntoDay(of: now, calendar: calendar)
|
||||
let start = (nowMinutes + 2 * 60) % (24 * 60)
|
||||
let end = (start + 8 * 60) % (24 * 60)
|
||||
return BlockingRule(
|
||||
name: name,
|
||||
days: Weekday.everyDay,
|
||||
startMinutes: start,
|
||||
endMinutes: end
|
||||
)
|
||||
}
|
||||
|
||||
private static func minutesIntoDay(of date: Date, calendar: Calendar) -> Int {
|
||||
let components = calendar.dateComponents([.hour, .minute], from: date)
|
||||
return (components.hour ?? 0) * 60 + (components.minute ?? 0)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user