feat: wire list view models into AppModel, add strings, list helpers and debug snapshot mode

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 15:46:29 +02:00
co-authored by Claude Sonnet 5.5
parent fffd9d7022
commit daacfa20c8
6 changed files with 179 additions and 9 deletions
+59 -8
View File
@@ -37,6 +37,8 @@ final class AppModel: ObservableObject {
let history: HistoryStore
let dictionary: DictionaryStore
let storage = ModelStorage()
let historyList: HistoryViewModel
let dictionaryList: DictionaryViewModel
// Observable state
@Published private(set) var state: DictationState = .idle
@@ -62,13 +64,19 @@ final class AppModel: ObservableObject {
private var loadingVariant: String?
private var cancellables = Set<AnyCancellable>()
init() {
let settings = AppSettings()
convenience init() {
self.init(preview: false)
}
/// `preview: true` is for snapshots: sample data in a temporary folder, no permission prompts, no model, no hotkey.
init(preview: Bool) {
let settings = preview ? AppSettings(defaults: UserDefaults(suiteName: "sst-preview")!) : AppSettings()
let policy = InsertionPolicy()
let knowledge = SystemWordKnowledge()
let corrector = DictionaryCorrector(knowledge: knowledge)
let dictionary = DictionaryStore(fileURL: AppPaths.dictionary)
let history = HistoryStore(fileURL: AppPaths.history)
let folder = FileManager.default.temporaryDirectory.appending(path: "sst-preview-\(UUID().uuidString)", directoryHint: .isDirectory)
let dictionary = DictionaryStore(fileURL: preview ? folder.appending(path: "dictionary.txt") : AppPaths.dictionary)
let history = HistoryStore(fileURL: preview ? folder.appending(path: "history.json") : AppPaths.history)
let levelSource = self.levelSource
let recorder = AudioRecorder()
recorder.onLevel = { levelSource.update($0) }
@@ -78,6 +86,8 @@ final class AppModel: ObservableObject {
self.corrector = corrector
self.dictionary = dictionary
self.history = history
self.historyList = HistoryViewModel(store: history)
self.dictionaryList = DictionaryViewModel(store: dictionary, knowledge: knowledge)
self.controller = DictationController(
recorder: recorder,
transcriber: transcriber,
@@ -89,7 +99,11 @@ final class AppModel: ObservableObject {
vocabularyProvider: { settings.usePriming ? VocabularyBuilder.build(from: dictionary.entries) : [] })
wire()
Task { await start() }
if preview {
loadPreviewData()
} else {
Task { await start() }
}
}
// MARK: derived
@@ -100,7 +114,7 @@ final class AppModel: ObservableObject {
/// Why recording is not possible right now, or nil when it is.
var recordingBlocker: String? {
if !permissions.allGranted { return Strings.openSystemSettings }
if !permissions.allGranted { return Strings.permissionsMissing }
switch modelStatus {
case .ready: return nil
case .loading(let variant): return Strings.modelLoading(title(for: variant))
@@ -181,9 +195,13 @@ final class AppModel: ObservableObject {
dictionary.onChange = { [weak self] in
guard let self else { return }
self.corrector.update(entries: self.dictionary.entries)
self.dictionaryList.reload()
self.dictionaryRevision += 1
}
history.onChange = { [weak self] in self?.historyRevision += 1 }
history.onChange = { [weak self] in
self?.historyRevision += 1
self?.historyList.refresh()
}
NotificationCenter.default.addObserver(forName: NSApplication.willTerminateNotification, object: nil, queue: .main) { [history] _ in
MainActor.assumeIsolated { history.flush() }
}
@@ -240,7 +258,7 @@ final class AppModel: ObservableObject {
}
func stopWindowRecording() {
guard policy.windowSession else { return }
guard isRecording else { return }
Task {
await controller.handle(.released)
policy.windowSession = false
@@ -310,4 +328,37 @@ final class AppModel: ObservableObject {
history.removeAll()
history.flush()
}
func openDictionaryFile() {
NSWorkspace.shared.open(dictionary.fileURL)
}
// MARK: preview
private func loadPreviewData() {
try? dictionary.load()
try? dictionary.apply { file in
file.append(.term("Anthropic"))
file.append(.term("Supabase"))
file.append(.correction(heard: "cloud code", wrote: "Claude Code"))
file.append(.correction(heard: "cloud", wrote: "Claude"))
}
let now = Date()
history.add(HistoryEntry(date: now.addingTimeInterval(-86_400 * 2), duration: 5.1,
rawText: "een oudere notitie over de planning", finalText: "Een oudere notitie over de planning."))
history.add(HistoryEntry(date: now.addingTimeInterval(-3_600), duration: 7.2,
rawText: "ik wil de meeting verplaatsen naar tomorrow morning",
finalText: "Ik wil de meeting verplaatsen naar tomorrow morning."))
history.add(HistoryEntry(
date: now.addingTimeInterval(-300), duration: 4.4,
rawText: "ik gebruik cloud code met supabase", finalText: "Ik gebruik Claude Code met Supabase.",
corrections: [
AppliedCorrection(heard: "cloud code", wrote: "Claude Code", rule: "cloud code", start: 11, length: 11),
AppliedCorrection(heard: "supabase", wrote: "Supabase", rule: "Supabase", start: 27, length: 8),
]))
activeVariant = ModelCatalog.defaultVariant
modelStatus = .ready
lastDuration = 4.4
historyList.selection = history.entries.first?.id
}
}