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:
co-authored by
Claude Opus 5
parent
f289d02c37
commit
3da673662f
@@ -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)) }
|
||||
}
|
||||
Reference in New Issue
Block a user