// // RuleSnapshot.swift // OpenAppLock // import Foundation /// Codable mirror of a rule, written to the app group by the app whenever /// rules change so the Screen Time extensions (which cannot open the /// SwiftData store) know what to enforce. struct RuleSnapshot: Codable, Equatable { var id: UUID var name: String var kindRaw: String var isEnabled: Bool var hardMode: Bool var blockAdultContent: Bool var selectionModeRaw: String var selectionData: Data? var dayNumbers: [Int] var dailyLimitMinutes: Int var maxOpens: Int var pausedUntil: Date? var kind: RuleKind { RuleKind(rawValue: kindRaw) ?? .schedule } var days: Set { Set(dayNumbers.compactMap(Weekday.init(rawValue:))) } func isScheduledToday(at now: Date, calendar: Calendar = .current) -> Bool { guard let weekday = Weekday(rawValue: calendar.component(.weekday, from: now)) else { return false } return days.contains(weekday) } /// Whether the given usage exhausts this rule's daily budget. func limitReached(given usage: RuleUsage) -> Bool { switch kind { case .schedule: false case .timeLimit: usage.minutesUsed >= dailyLimitMinutes case .openLimit: usage.opensUsed >= maxOpens } } /// Whether the user unblocked this rule for the rest of the day. func isPaused(at now: Date) -> Bool { guard let pausedUntil else { return false } return pausedUntil > now } } /// Persistence for the rule mirror in the shared app-group defaults. final class RuleSnapshotStore { private static let key = "ruleSnapshots" private let defaults: UserDefaults init(defaults: UserDefaults = AppGroup.defaults) { self.defaults = defaults } func save(_ snapshots: [RuleSnapshot]) { guard let data = try? JSONEncoder().encode(snapshots) else { return } defaults.set(data, forKey: Self.key) } func load() -> [RuleSnapshot] { guard let data = defaults.data(forKey: Self.key), let snapshots = try? JSONDecoder().decode([RuleSnapshot].self, from: data) else { return [] } return snapshots } func snapshot(for ruleID: UUID) -> RuleSnapshot? { load().first { $0.id == ruleID } } }