import SwiftUI import AVFoundation import OnyxDesign import FaceProvider import LockProvider /// Sperrbildschirm und Gesichtsvergleich. /// /// **Die Einschränkung steht oben und nicht unten.** Wer diese Seite öffnet, /// soll zuerst lesen, was das hier ist und was nicht — nicht erst, nachdem er /// sein Gesicht angelernt hat. struct LockSettingsView: View { @Bindable var model: AppModel let controller: LockController @State private var enrollment = FaceEnrollment() @State private var isEnrolling = false @State private var cameraDenied = false var body: some View { SettingsPage { OnyxFormCard(String(localized: "lock.section.what"), hint: String(localized: "lock.disclaimer.settings")) { EmptyView() } OnyxFormCard(String(localized: "lock.section.screen")) { OnyxFormRow(String(localized: "lock.enabled"), hint: String(localized: "lock.enabled.hint")) { Toggle("", isOn: $model.lockScreenEnabled).labelsHidden() } OnyxFormRow(String(localized: "lock.now")) { Button(String(localized: "lock.now.action")) { controller.lock() } } } OnyxFormCard(String(localized: "lock.section.face")) { OnyxFormRow(String(localized: "lock.face.enabled"), hint: String(localized: "lock.face.hint")) { Toggle("", isOn: $model.faceUnlockEnabled).labelsHidden() .disabled(!controller.isEnrolled) } OnyxFormRow(String(localized: "lock.face.enrolled"), hint: controller.isEnrolled ? nil : String(localized: "lock.face.notEnrolled")) { HStack(spacing: Onyx.Space.lg) { Button(controller.isEnrolled ? String(localized: "lock.face.again") : String(localized: "lock.face.enroll")) { Task { await beginEnrollment() } } if controller.isEnrolled { Button(String(localized: "lock.face.forget"), role: .destructive) { enrollment.reset() model.faceUnlockEnabled = false } } } } if cameraDenied { OnyxFormRow(String(localized: "lock.face.denied")) { Button(String(localized: "lock.face.openSettings")) { NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera")!) } } } } } .sheet(isPresented: $isEnrolling) { EnrollmentSheet(enrollment: enrollment) { isEnrolling = false } } } private func beginEnrollment() async { guard await FaceCapture.requestAccess() else { cameraDenied = true return } cameraDenied = false enrollment.reset() isEnrolling = true enrollment.start() } } /// Das Anlernen: Vorschau, Anweisung, Auslöser. private struct EnrollmentSheet: View { let enrollment: FaceEnrollment let onClose: () -> Void var body: some View { VStack(spacing: Onyx.Space.xl) { if let failure = enrollment.failure { Text(failure).foregroundStyle(Onyx.Color.critical) } else if enrollment.isFinished { VStack(spacing: Onyx.Space.md) { Image(systemName: "checkmark.circle.fill") .font(.system(size: Onyx.Symbol.hero)) .foregroundStyle(Onyx.Color.positive) Text("lock.enroll.done") } } else { CameraPreview(session: enrollment.session) .frame(width: 320, height: 240) .clipShape(RoundedRectangle(cornerRadius: Onyx.Radius.card, style: .continuous)) Text(enrollment.pose.localizedName, bundle: .main) .font(Onyx.Font.headline) Text(enrollment.seesFace ? "lock.enroll.ready" : "lock.enroll.noFace") .font(Onyx.Font.footnote) .foregroundStyle(enrollment.seesFace ? Onyx.Color.positive : Onyx.Color.textTertiary) Button(String(localized: "lock.enroll.capture")) { enrollment.captureCurrentPose() } .disabled(!enrollment.seesFace) } Button(enrollment.isFinished ? String(localized: "lock.enroll.close") : String(localized: "lock.enroll.cancel")) { enrollment.stop() onClose() } } .padding(Onyx.Space.xxl) .frame(width: 420) } }