Files
OpenAppLock/OpenAppLock/Logic/UsageDisplay.swift
Brendan Chen 32b1694e0a feat: re-skin to native three-tab UI and add Uninstall Protection
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>
2026-06-13 22:19:24 -04:00

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"
}
}
}