Files
ScarriffleandClaude Opus 5 3da673662f 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>
2026-09-05 21:06:06 +02:00

78 lines
2.6 KiB
Swift

import Foundation
import Observation
import os
private let log = Logger(subsystem: "com.scarriffleservices.onyx", category: "Notes")
/// Die Notizen und ihre Ablage.
///
/// Eine Datei für alles: Notizen sind kurz, und fünfzig davon sind ein paar
/// Kilobyte. Eine Datei je Notiz wäre der Ordnung wegen — nicht der Sache.
///
/// **Nicht in `UserDefaults`.** Was dort steht, liest jedes Programm des
/// Benutzers mit `defaults read` mit, und eine Notiz ist oft genau das, was man
/// sich merkt, weil man es nicht im Kopf behalten will.
@MainActor
@Observable
public final class NotesStore {
public private(set) var notes: [Note] = []
@ObservationIgnored private let fileURL: URL
private struct Archive: Codable {
var version = 1
var notes: [Note]
}
public init(directory: URL? = nil) {
let folder = directory ?? Self.defaultDirectory
try? FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
fileURL = folder.appendingPathComponent("notes.json")
load()
}
public static var defaultDirectory: URL {
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("Onyx/Notes", isDirectory: true)
}
/// Legt eine leere Notiz an und gibt sie zurück, damit die Anzeige den
/// Schreibcursor hineinsetzen kann.
///
/// Sie steht noch **nicht** in der Liste: eine leere Notiz ist keine, und
/// wer auf „Neu" drückt und es sich anders überlegt, soll keine Leerzeile
/// hinterlassen. Erst der erste Buchstabe macht sie zur Notiz.
public func makeDraft() -> Note { Note() }
public func update(_ note: Note) {
var updated = note
updated.updatedAt = Date()
notes = NoteRules.merge(notes, updating: updated)
save()
}
public func remove(_ note: Note) {
notes.removeAll { $0.id == note.id }
save()
}
private func load() {
guard let data = try? Data(contentsOf: fileURL) else { return }
guard let archive = try? JSONDecoder().decode(Archive.self, from: data),
archive.version == 1 else {
log.notice("Unbekannte Fassung der Notizdatei — sie bleibt unangetastet")
return
}
notes = NoteRules.sorted(archive.notes)
}
private func save() {
do {
try JSONEncoder().encode(Archive(notes: notes)).write(to: fileURL, options: .atomic)
} catch {
log.error("Notizen nicht schreibbar: \(error.localizedDescription, privacy: .public)")
}
}
}