import AVFoundation import ApplicationServices import SwiftUI import SstCore import SstWhisper @MainActor final class AppModel: ObservableObject { @Published var state: DictationState = .idle @Published var modelStatus = "Model laden…" @Published var modelReady = false @Published var lastError: String? @Published var micGranted = false @Published var accessibilityGranted = false @Published var inputMonitoringGranted = false private let transcriber = WhisperKitTranscriber() private let hotkey = HotkeyMonitor() private let controller: DictationController init() { controller = DictationController( recorder: AudioRecorder(), transcriber: transcriber, cleaner: PassthroughCleaner(), inserter: TextInserter(pasteboard: SystemPasteboard(), paster: CGEventPaster()) ) controller.onStateChange = { [weak self] in self?.state = $0 if $0 == .recording { self?.lastError = nil } } controller.onError = { [weak self] in self?.lastError = $0 } hotkey.onEvent = { [weak self] event in guard let self, self.modelReady else { return } if event == .pressed, AVCaptureDevice.authorizationStatus(for: .audio) != .authorized { self.lastError = "Microfoon-toegang ontbreekt" return } Task { await self.controller.handle(event) } } Task { await start() } } var statusLine: String { guard modelReady else { return modelStatus } switch state { case .idle: return "Klaar — houd Fn ingedrukt om te dicteren" case .recording: return "Luistert…" case .transcribing: return "Transcriberen…" case .inserting: return "Invoegen…" } } var iconName: String { switch state { case .idle: if lastError != nil { return "exclamationmark.triangle" } return modelReady ? "mic" : "mic.slash" case .recording: return "mic.fill" case .transcribing, .inserting: return "waveform" } } func refreshPermissions() { micGranted = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized accessibilityGranted = AXIsProcessTrusted() inputMonitoringGranted = CGPreflightListenEventAccess() } func retryHotkey() { refreshPermissions() do { try hotkey.start() lastError = nil } catch { lastError = error.localizedDescription } } private func start() async { _ = await AVCaptureDevice.requestAccess(for: .audio) if !CGPreflightListenEventAccess() { _ = CGRequestListenEventAccess() } if !AXIsProcessTrusted() { _ = AXIsProcessTrustedWithOptions(["AXTrustedCheckOptionPrompt": true] as CFDictionary) } retryHotkey() transcriber.onProgress = { [weak self] fraction in Task { @MainActor in self?.modelStatus = fraction < 1 ? "Model downloaden… \(Int(fraction * 100))%" : "Model laden…" } } do { try await transcriber.load() modelReady = true modelStatus = "Klaar" } catch { modelStatus = "Model laden mislukt" lastError = error.localizedDescription } } }