Replace the custom themed presentation with the bare native iOS design language across a three-tab TabView (Home / Rules / Settings): - MainTabView hosts the enforcement lifecycle (refresh loop, rule-change and scene-active reconcile) so it runs regardless of the selected tab - HomeView: "Currently Blocking" + "Usage" sections (replaces AppsHomeView) - RulesListView: rules grouped into Schedule / Time Limit / Open Limit - SettingsView: Uninstall Protection toggle + Manage App Lists - AppListLibraryView / ManageAppListsView: shared App List management - AppSettings: app-group-backed settings store Uninstall Protection: while enabled and any Hard Mode rule is actively blocking, deny device app-removal via a dedicated ManagedSettingsStore (RulePolicy.shouldDenyAppRemoval -> RuleEnforcer -> ShieldController). Slim AppListPickerSheet to the picker role; drop AppsHomeView and the custom Theme/HoldToCommitButton/RuleCardView chrome. Update spec section 6 and the unit/UI test suites accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
50 lines
2.0 KiB
Swift
50 lines
2.0 KiB
Swift
//
|
|
// UsageDisplay.swift
|
|
// OpenAppLock
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// Strings for the home screen's Usage section. Used values clamp to the
|
|
/// budget so overshoot (thresholds can fire late) never reads "50m of 45m".
|
|
enum UsageDisplay {
|
|
/// The usage subtitle prefixed with the rule's type, so the kind is clear
|
|
/// without relying on an icon: "Time Limit · 18m of 45m used today".
|
|
/// Schedule rules (no usage text) fall back to just the type name.
|
|
static func typedSubtitle(for rule: BlockingRule, usage: RuleUsage) -> String {
|
|
let usageText = subtitle(for: rule, usage: usage)
|
|
guard !usageText.isEmpty else { return rule.kind.displayName }
|
|
return "\(rule.kind.displayName) · \(usageText)"
|
|
}
|
|
|
|
/// "18m of 45m used today" / "2 of 5 opens today".
|
|
static func subtitle(for rule: BlockingRule, usage: RuleUsage) -> String {
|
|
switch rule.configuration {
|
|
case .schedule:
|
|
""
|
|
case .timeLimit(let config):
|
|
"\(min(usage.minutesUsed, config.dailyLimitMinutes))m of "
|
|
+ "\(config.dailyLimitMinutes)m used today"
|
|
case .openLimit(let config):
|
|
"\(min(usage.opensUsed, config.maxOpens)) of \(config.maxOpens) opens today"
|
|
}
|
|
}
|
|
|
|
/// "27m left" / "3 opens left", or the blocked/unblocked state once the
|
|
/// budget is spent.
|
|
static func remainingLabel(for rule: BlockingRule, usage: RuleUsage, isPaused: Bool) -> String {
|
|
guard !rule.limitReached(given: usage) else {
|
|
return isPaused ? "Unblocked until tomorrow" : "Blocked until tomorrow"
|
|
}
|
|
switch rule.configuration {
|
|
case .schedule:
|
|
return ""
|
|
case .timeLimit(let config):
|
|
return "\(config.dailyLimitMinutes - usage.minutesUsed)m left"
|
|
case .openLimit(let config):
|
|
let remaining = config.maxOpens - usage.opensUsed
|
|
return remaining == 1 ? "1 open left" : "\(remaining) opens left"
|
|
}
|
|
}
|
|
}
|