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.
This commit is contained in:
65
Severed/Models/Weekday.swift
Normal file
65
Severed/Models/Weekday.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// Weekday.swift
|
||||
// Severed
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A day of the week, using `Calendar` weekday numbering (1 = Sunday … 7 = Saturday).
|
||||
enum Weekday: Int, CaseIterable, Codable, Hashable, Sendable {
|
||||
case sunday = 1
|
||||
case monday = 2
|
||||
case tuesday = 3
|
||||
case wednesday = 4
|
||||
case thursday = 5
|
||||
case friday = 6
|
||||
case saturday = 7
|
||||
|
||||
static let weekdays: Set<Weekday> = [.monday, .tuesday, .wednesday, .thursday, .friday]
|
||||
static let weekends: Set<Weekday> = [.saturday, .sunday]
|
||||
static let everyDay: Set<Weekday> = Set(Weekday.allCases)
|
||||
|
||||
/// Display order used by the day picker, matching the reference UI: S M T W T F S.
|
||||
static let displayOrder: [Weekday] = [
|
||||
.sunday, .monday, .tuesday, .wednesday, .thursday, .friday, .saturday,
|
||||
]
|
||||
|
||||
/// Single-letter label for the circular day toggles.
|
||||
var shortLabel: String {
|
||||
switch self {
|
||||
case .sunday, .saturday: "S"
|
||||
case .monday: "M"
|
||||
case .tuesday, .thursday: "T"
|
||||
case .wednesday: "W"
|
||||
case .friday: "F"
|
||||
}
|
||||
}
|
||||
|
||||
/// Three-letter abbreviation used in custom day summaries ("Mon, Wed, Fri").
|
||||
var abbreviation: String {
|
||||
switch self {
|
||||
case .sunday: "Sun"
|
||||
case .monday: "Mon"
|
||||
case .tuesday: "Tue"
|
||||
case .wednesday: "Wed"
|
||||
case .thursday: "Thu"
|
||||
case .friday: "Fri"
|
||||
case .saturday: "Sat"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Set<Weekday> {
|
||||
/// Human-readable summary shown next to the day picker and in rule details:
|
||||
/// "Weekdays", "Weekends", "Every day", "Never", or a list like "Mon, Wed, Fri".
|
||||
var summary: String {
|
||||
if self == Weekday.everyDay { return "Every day" }
|
||||
if self == Weekday.weekdays { return "Weekdays" }
|
||||
if self == Weekday.weekends { return "Weekends" }
|
||||
if isEmpty { return "Never" }
|
||||
return Weekday.displayOrder
|
||||
.filter(contains)
|
||||
.map(\.abbreviation)
|
||||
.joined(separator: ", ")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user