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,77 @@
|
||||
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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user