The shield titled itself with the first enabled rule whose selection contained the app, regardless of whether that rule was the one actually blocking. When several rules covered the same app (e.g. a dormant schedule + the active limit) it named the wrong rule. Drop the rule-name lookup entirely: every shield now carries a generic "App Blocked" title. Open-limit shields keep their functional detail (running count + "Open (N left)" button) beneath that title. The text-only decision moves into a pure, unit-tested ShieldPresentation in Shared/, and the now-dead ShieldLookup.snapshot(containingApplication:) is removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
48 lines
1.8 KiB
Swift
48 lines
1.8 KiB
Swift
//
|
|
// ShieldPresentation.swift
|
|
// OpenAppLock
|
|
//
|
|
|
|
/// The text a shield shows, decided independently of any UI type so it can be
|
|
/// unit tested. Shields never name the rule that blocks an app: when several
|
|
/// rules cover the same app the responsible rule can't be told apart, so every
|
|
/// shield carries the same generic "App Blocked" title. Open-limit shields add
|
|
/// their functional detail (remaining opens + an "Open" button) beneath it.
|
|
struct ShieldPresentation: Equatable {
|
|
let title: String
|
|
let subtitle: String
|
|
/// Label for the secondary "Open" button, or `nil` when the shield offers
|
|
/// no way through (a full block, or a spent open-limit budget).
|
|
let secondaryButton: String?
|
|
|
|
static let blockedTitle = "App Blocked"
|
|
|
|
/// A plain, fully-blocked app: no counts, no way through.
|
|
static let blocked = ShieldPresentation(
|
|
title: blockedTitle,
|
|
subtitle: "This app is blocked by OpenAppLock.",
|
|
secondaryButton: nil
|
|
)
|
|
|
|
/// An open-limit shield. While opens remain it shows the running count and
|
|
/// an "Open (N left)" button; once spent it reads like a plain block.
|
|
static func openLimit(
|
|
opensUsed: Int, maxOpens: Int, sessionMinutes: Int
|
|
) -> ShieldPresentation {
|
|
let remaining = max(0, maxOpens - opensUsed)
|
|
guard remaining > 0 else {
|
|
return ShieldPresentation(
|
|
title: blockedTitle,
|
|
subtitle: "No opens left today — the block lifts tomorrow.",
|
|
secondaryButton: nil
|
|
)
|
|
}
|
|
return ShieldPresentation(
|
|
title: blockedTitle,
|
|
subtitle: "Opened \(opensUsed) of \(maxOpens) times today. "
|
|
+ "Each open lasts \(sessionMinutes) minutes.",
|
|
secondaryButton: remaining == 1 ? "Open (1 left)" : "Open (\(remaining) left)"
|
|
)
|
|
}
|
|
}
|