fix: window session reset, model-row locking, startup order, hotkey release, counter, history gate, dictionary editor and search polish

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 16:04:37 +02:00
co-authored by Claude Sonnet 5.5
parent 31c34a1d4a
commit ab7359156b
6 changed files with 52 additions and 11 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ struct AppCommands: Commands {
Button(Strings.dictionary) { show(.dictionary) }
.keyboardShortcut("2", modifiers: .command)
Button(Strings.search) {
show(.transcripts)
show(model.section)
model.focusSearch()
}
.keyboardShortcut("f", modifiers: .command)
+35 -8
View File
@@ -47,6 +47,7 @@ final class AppModel: ObservableObject {
@Published private(set) var installedVariants: Set<String> = []
@Published private(set) var recordingStartedAt: Date?
@Published private(set) var lastDuration: TimeInterval?
@Published private(set) var loadingVariant: String?
/// Bumped whenever the history or dictionary changes, so views that cache derived data can refresh.
@Published private(set) var historyRevision = 0
@Published private(set) var dictionaryRevision = 0
@@ -61,7 +62,6 @@ final class AppModel: ObservableObject {
private let policy: InsertionPolicy
private let controller: DictationController
private var modelTask: Task<Void, Never>?
private var loadingVariant: String?
private var cancellables = Set<AnyCancellable>()
convenience init() {
@@ -123,6 +123,11 @@ final class AppModel: ObservableObject {
}
}
/// Like `recordingBlocker`, plus: a window recording is only kept when the history is on.
var windowRecordingBlocker: String? {
recordingBlocker ?? (settings.keepHistory ? nil : Strings.historyNeededForWindow)
}
var statusLine: String {
if let blocker = recordingBlocker, state == .idle { return blocker }
switch state {
@@ -153,14 +158,19 @@ final class AppModel: ObservableObject {
controller.onStateChange = { [weak self] state in
guard let self else { return }
self.state = state
if state != .recording, let started = self.recordingStartedAt {
// The counter shows the recording length, not the time spent transcribing and pasting.
self.lastDuration = Date().timeIntervalSince(started)
self.recordingStartedAt = nil
}
switch state {
case .recording:
self.lastError = nil
self.recordingStartedAt = Date()
self.levelSource.reset()
case .idle:
if let started = self.recordingStartedAt { self.lastDuration = Date().timeIntervalSince(started) }
self.recordingStartedAt = nil
// Also covers a window recording that was ended with the hotkey; the paste decision is already made.
self.policy.windowSession = false
self.levelSource.reset()
default: break
}
@@ -192,6 +202,9 @@ final class AppModel: ObservableObject {
permissions.objectWillChange
.sink { [weak self] _ in self?.objectWillChange.send() }
.store(in: &cancellables)
settings.objectWillChange
.sink { [weak self] _ in self?.objectWillChange.send() }
.store(in: &cancellables)
dictionary.onChange = { [weak self] in
guard let self else { return }
@@ -199,10 +212,15 @@ final class AppModel: ObservableObject {
self.dictionaryList.reload()
self.dictionaryRevision += 1
}
dictionary.onExternalChange = { [weak self] in self?.dictionaryList.externalChangeDetected() }
history.onChange = { [weak self] in
self?.historyRevision += 1
self?.historyList.refresh()
}
// Backstop for editors that write in place, which the folder watcher can miss.
NotificationCenter.default.addObserver(forName: NSApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated { self?.dictionary.reloadIfChanged() }
}
NotificationCenter.default.addObserver(forName: NSApplication.willTerminateNotification, object: nil, queue: .main) { [history] _ in
MainActor.assumeIsolated { history.flush() }
}
@@ -211,19 +229,28 @@ final class AppModel: ObservableObject {
private func start() async {
do { try dictionary.load() } catch { lastError = error.localizedDescription }
dictionary.startWatching()
if case .recoveredCorrupt = history.load() {
switch history.load() {
case .recoveredCorrupt:
lastError = "Het geschiedenisbestand was beschadigd en is opzij gezet."
case .unreadable:
lastError = Strings.historyUnreadable
case .empty, .loaded:
break
}
refreshInstalled()
if !ModelCatalog.all.contains(where: { $0.id == settings.modelVariant }) {
settings.modelVariant = ModelCatalog.defaultVariant
}
activateModel(settings.modelVariant) // the download must not wait for the microphone prompt
await permissions.requestMissing()
retryHotkey()
activateModel(settings.modelVariant)
}
func retryHotkey() {
permissions.refresh()
do {
try hotkey.start()
if lastError == HotkeyMonitor.HotkeyError.tapUnavailable.errorDescription { lastError = nil }
} catch {
lastError = error.localizedDescription
}
@@ -232,7 +259,7 @@ final class AppModel: ObservableObject {
// MARK: recording
private func handleHotkey(_ event: HotkeyEvent) {
guard modelReady else { return }
guard event == .released || modelReady else { return }
if event == .pressed, AVCaptureDevice.authorizationStatus(for: .audio) != .authorized {
lastError = Strings.microphoneMissing
return
@@ -250,7 +277,7 @@ final class AppModel: ObservableObject {
}
func startWindowRecording() {
guard recordingBlocker == nil, state == .idle else { return }
guard windowRecordingBlocker == nil, state == .idle else { return }
policy.windowSession = true
Task {
await controller.handle(.pressed)
@@ -305,7 +332,7 @@ final class AppModel: ObservableObject {
}
func deleteModel(_ variant: String) {
guard variant != activeVariant else { return }
guard variant != activeVariant, variant != loadingVariant else { return }
do {
try storage.delete(variant)
} catch {
+2 -2
View File
@@ -47,8 +47,8 @@ struct DeckView: View {
}
}
.buttonStyle(KeyButtonStyle(kind: .record, latched: model.isRecording))
.disabled(!model.isRecording && model.recordingBlocker != nil)
.help(model.recordingBlocker ?? "")
.disabled(!model.isRecording && model.windowRecordingBlocker != nil)
.help(model.windowRecordingBlocker ?? "")
Button(Strings.stop) { model.stopWindowRecording() }
.buttonStyle(KeyButtonStyle())
+8
View File
@@ -95,6 +95,7 @@ struct DictionaryView: View {
selection = nil
}
}
.onChange(of: model.searchFocusTick) { _, _ in searchFocused = true }
.onChange(of: list.rows) { _, rows in
if let s = selection, !rows.contains(where: { $0.id == s }) { selection = nil }
}
@@ -215,7 +216,9 @@ private struct EntryEditor: View {
HStack(spacing: Space.s3) {
Spacer()
Button(Strings.cancel, action: onClose).buttonStyle(KeyButtonStyle())
.keyboardShortcut(.cancelAction)
Button(Strings.save, action: save).buttonStyle(KeyButtonStyle(kind: .record)).disabled(!canSave)
.keyboardShortcut(.defaultAction)
}
}
.padding(Layout.panelPadding)
@@ -246,6 +249,11 @@ private struct EntryEditor: View {
private func save() {
if let id = editingID {
guard list.rows.contains(where: { $0.id == id }) else {
list.errorMessage = Strings.entryGone
onClose()
return
}
list.update(id: id, to: kind)
} else {
list.add(kind)
+3
View File
@@ -185,9 +185,12 @@ private struct ModelSettingsView: View {
Spacer()
if installed && !isActive {
Button(Strings.useModel) { model.activateModel(option.id) }.buttonStyle(KeyButtonStyle())
.disabled(model.loadingVariant != nil)
Button(Strings.remove) { model.deleteModel(option.id) }.buttonStyle(KeyButtonStyle())
.disabled(model.loadingVariant != nil)
} else if !installed {
Button(Strings.download) { model.activateModel(option.id) }.buttonStyle(KeyButtonStyle())
.disabled(model.loadingVariant != nil)
}
}
}
+3
View File
@@ -30,6 +30,7 @@ enum Strings {
static let undo = "Herstel"
static let save = "Bewaar"
static let editEntry = "Invoer"
static let entryGone = "Deze invoer bestaat niet meer; je wijziging is niet bewaard."
static let emptyDictionary = "Nog geen invoeren. Voeg een woord of een correctie toe met +."
// deck
@@ -66,6 +67,8 @@ enum Strings {
static let microphoneMissing = "Microfoon-toegang ontbreekt"
static let openSystemSettings = "Open Instellingen"
static let open = "Open"
static let historyNeededForWindow = "Zet ‘Transcripties bewaren’ aan om vanuit het venster op te nemen."
static let historyUnreadable = "Het geschiedenisbestand kon niet worden gelezen; nieuwe transcripties worden niet bewaard."
static let permissionsMissing = "Geef eerst de rechten onder Controles"
// settings