Files
onyx/Onyx/LockSettingsView.swift
ScarriffleandClaude Opus 5 a3399d3a96 Zwei Anzeigen für einen Handgriff sind eine zu viel
Apples eigene Einblendung lässt sich aushängen, ohne Root und ohne SIP
anzufassen: `com.apple.OSDUIHelper` liegt in der GUI-Domäne des angemeldeten
Benutzers, nicht im System. `launchctl bootout` dort hinein genügt.

Standardmäßig aus, denn es verschwindet **jede** Einblendung — auch Helligkeit
und Feststelltaste, und für die Feststelltaste hat Onyx keinen Ersatz. Beim
Beenden wird wieder eingehängt: eine Einstellung darf nicht überleben, was sie
eingestellt hat.

Dazu zwei Fehler im Anlernen behoben. Der Posenname stand als roher Schlüssel da
— die Zeichenketten liegen im Bündel von `FaceProvider`, gesucht wurden sie im
Hauptbündel. Und „Gesicht vergleichen" blieb nach dem Anlernen grau, weil
`isEnrolled` bei jedem Aufruf den Schlüsselbund las: richtig, aber für SwiftUI
unsichtbar, weil es nichts gab, woran eine Ansicht die Änderung hätte bemerken
können.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 22:26:41 +02:00

143 lines
5.4 KiB
Swift

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
controller.refreshEnrollment()
}
}
}
}
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
// Sonst bliebe „Gesicht vergleichen" grau, obwohl gerade eines
// angelernt wurde.
controller.refreshEnrollment()
}
}
}
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)
.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)
}
}