feat: add menu bar app and bundle script
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
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 }
|
||||
controller.onError = { [weak self] in self?.lastError = $0 }
|
||||
hotkey.onEvent = { [weak self] event in
|
||||
guard let self, self.modelReady else { 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: 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()
|
||||
do {
|
||||
try await transcriber.load()
|
||||
modelReady = true
|
||||
modelStatus = "Klaar"
|
||||
} catch {
|
||||
modelStatus = "Model laden mislukt"
|
||||
lastError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
struct MenuContent: View {
|
||||
@ObservedObject var model: AppModel
|
||||
|
||||
var body: some View {
|
||||
Text(model.statusLine)
|
||||
if let error = model.lastError {
|
||||
Text("Fout: \(error)")
|
||||
}
|
||||
Divider()
|
||||
permissionRow("Microfoon", granted: model.micGranted, pane: "Privacy_Microphone")
|
||||
permissionRow("Accessibility", granted: model.accessibilityGranted, pane: "Privacy_Accessibility")
|
||||
permissionRow("Input Monitoring", granted: model.inputMonitoringGranted, pane: "Privacy_ListenEvent")
|
||||
Button("Rechten opnieuw controleren") { model.retryHotkey() }
|
||||
Divider()
|
||||
Button("Stop") { NSApplication.shared.terminate(nil) }
|
||||
.keyboardShortcut("q")
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func permissionRow(_ title: String, granted: Bool, pane: String) -> some View {
|
||||
if granted {
|
||||
Text("✓ \(title)")
|
||||
} else {
|
||||
Button("⚠︎ \(title) — open Instellingen") {
|
||||
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?\(pane)") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct SstApp: App {
|
||||
@StateObject private var model = AppModel()
|
||||
|
||||
var body: some Scene {
|
||||
MenuBarExtra {
|
||||
MenuContent(model: model)
|
||||
} label: {
|
||||
Image(systemName: model.iconName)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user