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>
133 lines
4.5 KiB
Swift
133 lines
4.5 KiB
Swift
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)) }
|
|
}
|