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>
143 lines
5.4 KiB
Swift
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)
|
|
}
|
|
}
|