Zwischenablage, Notizen und der Gesichtsvergleich

Drei neue Module nach dem Bauplan von `ShelfProvider`: reine Regeln getrennt vom
Zustand, versionierte Indexdatei, Inhalte in Dateien statt in `UserDefaults` —
was dort steht, liest jedes Programm mit `defaults read` mit.

Die Zwischenablage achtet `ConcealedType`, `TransientType` und
`AutoGeneratedType`. Diese Marken sind eine Verabredung zwischen Programmen und
kein Systemdienst; wer sie missachtet, sammelt fremde Passwörter ein.

Der Gesichtsvergleich stand im Plan als der eine echte Unsicherheitsfaktor. Ein
Spike hat ihn erledigt: `VNGenerateFaceprintRequest` gibt es nicht, aber
`VNGenerateImageFeaturePrintRequest` ist öffentlich, liefert 768 Elemente und
ein Abstandsmaß — Faktor 3,5 Trennschärfe. Kein eigenes CoreML-Modell nötig.

`FaceMatcher` entscheidet über mehrere Posen, mehrere Einzelbilder und eine
Sperre nach Fehlversuchen. Die Schwelle liegt bewusst nicht mittig: ein falsches
Ja ist ein Fremder am entsperrten Rechner, ein falsches Nein nur ein Passwort,
das man ohnehin kennt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-05 21:06:06 +02:00
co-authored by Claude Opus 5
parent f289d02c37
commit 3da673662f
17 changed files with 1536 additions and 4 deletions
@@ -0,0 +1,132 @@
import SwiftUI
import OnyxDesign
import OnyxWidgetKit
/// Kurze Notizen, direkt in der Kachel zu schreiben.
public struct NotesView: View {
private let store: NotesStore
private let compact: Bool
@State private var editing: Note?
@State private var draft = ""
@FocusState private var isWriting: Bool
public init(store: NotesStore, compact: Bool = false) {
self.store = store
self.compact = compact
}
public var body: some View {
VStack(alignment: .leading, spacing: Onyx.Space.sm) {
if let editing {
editor(for: editing)
} else {
list
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
private var list: some View {
VStack(alignment: .leading, spacing: 0) {
HStack {
Text("notes.name", bundle: .module)
.font(Onyx.Font.title)
.foregroundStyle(Onyx.Color.textPrimary)
Spacer()
Button { begin(store.makeDraft()) } label: {
Image(systemName: "square.and.pencil")
.font(.system(size: Onyx.Symbol.small))
}
.buttonStyle(.plain)
.foregroundStyle(Onyx.Color.accent)
}
.padding(.bottom, Onyx.Space.sm)
if store.notes.isEmpty {
OnyxNotice(symbol: "note.text",
text: String(localized: "notes.empty", bundle: .module))
} else {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(Array(store.notes.prefix(compact ? 4 : 20).enumerated()),
id: \.element.id) { index, note in
if index > 0 { OnyxHairline() }
Button { begin(note) } label: {
HStack {
Text(NoteRules.title(of: note))
.font(Onyx.Font.footnote)
.foregroundStyle(Onyx.Color.textPrimary)
.lineLimit(1)
Spacer(minLength: Onyx.Space.xs)
}
.padding(.vertical, Onyx.Space.sm)
.contentShape(.rect)
}
.buttonStyle(.plain)
}
}
}
.scrollIndicators(.never)
}
}
}
private func editor(for note: Note) -> some View {
VStack(alignment: .leading, spacing: Onyx.Space.sm) {
HStack {
Button { finish() } label: {
Image(systemName: "chevron.left")
.font(.system(size: Onyx.Symbol.small))
}
.buttonStyle(.plain)
.foregroundStyle(Onyx.Color.accent)
Spacer()
Button { store.remove(note); editing = nil } label: {
Image(systemName: "trash")
.font(.system(size: Onyx.Symbol.small))
}
.buttonStyle(.plain)
.foregroundStyle(Onyx.Color.textTertiary)
}
// `TextEditor` ohne eigenen Hintergrund: der Standardhintergrund
// ist hell und stünde als weißer Block in der schwarzen Kachel.
TextEditor(text: $draft)
.font(Onyx.Font.footnote)
.foregroundStyle(Onyx.Color.textPrimary)
.scrollContentBackground(.hidden)
.focused($isWriting)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.onAppear { isWriting = true }
}
private func begin(_ note: Note) {
editing = note
draft = note.text
}
private func finish() {
if var note = editing {
note.text = draft
store.update(note)
}
editing = nil
draft = ""
}
}
public struct NotesWidget: OnyxWidget {
public let id = "notes"
public var displayName: String { String(localized: "notes.name", bundle: .module) }
public let symbolName = "note.text"
private let store: NotesStore
public init(store: NotesStore) { self.store = store }
public func makeView() -> AnyView { AnyView(NotesView(store: store)) }
}