Files
OpenAppLock/Severed/Views/Components/HoldToCommitButton.swift
Brendan Chen 3aac2004d2 feat: clone Opal's rules feature with hard block enforcement
Rebuild the app around recurring screen-time blocking rules modeled on
Opal's My Apps tab:

- Onboarding with FamilyControls Screen Time authorization
- Apps home with Blocked Apps tiles and live rule cards
- New Rule sheet: Schedule/Time Limit/Open Limit types + preset gallery
- Rule editors: time windows (incl. overnight), day picker, app selection
  via FamilyActivityPicker (Block/Allow Only), rename, Hold to Commit
- Hard Mode: active hard rules cannot be edited, disabled, deleted, or
  unblocked until their window ends; soft rules pause until next window
- Shield enforcement through per-rule ManagedSettingsStore + RuleEnforcer
- 73 unit tests (Swift Testing) + 16 UI tests (XCUITest) with launch-arg
  harness for in-memory storage, mocked authorization, seeded scenarios
- docs/RULES_FEATURE_SPEC.md: spec derived from the reference recording

Known gap: background window transitions and time/open-limit thresholds
need a DeviceActivityMonitor extension; shields currently sync while the
app is running.
2026-06-12 12:35:52 -04:00

55 lines
1.7 KiB
Swift

//
// HoldToCommitButton.swift
// Severed
//
import SwiftUI
/// The deliberate-friction commit button: the user must press and hold for a
/// second to save a rule, mirroring the reference app's "Hold to Commit".
struct HoldToCommitButton: View {
var title = "Hold to Commit"
var holdDuration: Double = 1.0
let action: () -> Void
@State private var progress: Double = 0
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 28)
.fill(Theme.surfaceElevated)
GeometryReader { proxy in
RoundedRectangle(cornerRadius: 28)
.fill(Theme.commitGradient)
.opacity(0.85)
.frame(width: proxy.size.width * progress)
.animation(.linear(duration: 0.1), value: progress == 0)
}
Text(title)
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(.white)
}
.frame(height: 56)
.clipShape(RoundedRectangle(cornerRadius: 28))
.contentShape(RoundedRectangle(cornerRadius: 28))
.onLongPressGesture(minimumDuration: holdDuration) {
progress = 0
action()
} onPressingChanged: { pressing in
withAnimation(.linear(duration: pressing ? holdDuration : 0.2)) {
progress = pressing ? 1 : 0
}
}
.accessibilityElement(children: .ignore)
.accessibilityAddTraits(.isButton)
.accessibilityIdentifier("holdToCommitButton")
.accessibilityLabel(title)
}
}
#Preview {
HoldToCommitButton { }
.padding()
.background(Theme.background)
}