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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user