Files
OpenAppLock/OpenAppLock/Services/SampleRules.swift
Brendan Chen 8e3b8b70f5 feat: track limit budgets in a usage ledger and surface them in a Usage section
- RuleUsage + UsageLedger: per-rule, per-day minutes/opens in app-group
  defaults (monotonic minutes, incrementing opens, midnight rollover by
  day-keying); MockUsageLedger for tests and seeded UI scenarios
- usage-aware status: a limit rule whose daily budget is spent on an
  enabled day is active (blocking) until next midnight; Hard Mode gating
  and app-list locking honor it; soft unblock pauses until midnight
- RuleEnforcer shields spent limit rules (always Block mode) while the
  app runs
- new Usage section under Blocked Apps: '18m of 45m used today · 27m
  left', '2 of 5 opens today · 3 opens left', 'Blocked until tomorrow'
- new 'limits' seed scenario + UI tests
2026-06-12 20:22:48 -04:00

96 lines
3.7 KiB
Swift

//
// SampleRules.swift
// OpenAppLock
//
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,
usage: MockUsageLedger? = nil,
now: Date = .now,
calendar: Calendar = .current
) {
// Shared list so seeded rules demonstrate the app-list UI. The count
// is display-only; UI tests never decode real tokens.
let distractions = AppList(name: "Distractions", selectionCount: 3)
context.insert(distractions)
let rules: [BlockingRule]
switch scenario {
case .standard:
rules = [
activeRule(named: "Work Time", hardMode: false, now: now, calendar: calendar),
upcomingRule(named: "Sleep", now: now, calendar: calendar),
]
case .hardModeActive:
rules = [
activeRule(named: "Locked In", hardMode: true, now: now, calendar: calendar),
upcomingRule(named: "Sleep", now: now, calendar: calendar),
]
case .limits:
let timeKeeper = BlockingRule(
name: "Time Keeper", kind: .timeLimit, days: Weekday.everyDay,
dailyLimitMinutes: 45)
let gateKeeper = BlockingRule(
name: "Gate Keeper", kind: .openLimit, days: Weekday.everyDay, maxOpens: 5)
let doomScroll = BlockingRule(
name: "Doom Scroll", kind: .timeLimit, days: Weekday.everyDay,
dailyLimitMinutes: 30)
usage?.usageByRule[timeKeeper.id] = RuleUsage(minutesUsed: 18)
usage?.usageByRule[gateKeeper.id] = RuleUsage(opensUsed: 2)
usage?.usageByRule[doomScroll.id] = RuleUsage(minutesUsed: 30)
rules = [timeKeeper, gateKeeper, doomScroll]
}
// Relationships are wired only after both sides are managed
// (see BlockingRule.appList).
for rule in rules {
context.insert(rule)
rule.appList = distractions
}
}
/// 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)
}
}