145 lines
4.9 KiB
Swift
145 lines
4.9 KiB
Swift
import AppKit
|
|
|
|
@MainActor
|
|
final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
private let prefs = Prefs.shared
|
|
private let controller = DictationController.shared
|
|
private let hud = HUDController()
|
|
private let settings = SettingsWindowController()
|
|
|
|
private var status: StatusItemController!
|
|
private var hotkey: HotkeyMonitor!
|
|
private var permissionTimer: Timer?
|
|
|
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
NSApp.setActivationPolicy(.accessory)
|
|
Log.info("launched v\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev"), hotkey=\(prefs.hotkey.label), mode=\(prefs.triggerMode.rawValue)")
|
|
|
|
controller.attach(hud: hud)
|
|
NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.willSleepNotification, object: nil, queue: .main) { [weak self] _ in
|
|
MainActor.assumeIsolated { self?.controller.cancel() }
|
|
}
|
|
|
|
status = StatusItemController()
|
|
status.onToggle = { [weak self] in self?.controller.toggle() }
|
|
status.onCancel = { [weak self] in self?.controller.cancel() }
|
|
status.onSettings = { [weak self] in self?.settings.show() }
|
|
|
|
controller.onStateChange = { [weak self] state in
|
|
self?.status.update(state: state)
|
|
}
|
|
controller.onError = { [weak self] message in
|
|
self?.presentError(message)
|
|
}
|
|
|
|
hotkey = HotkeyMonitor(spec: prefs.hotkey)
|
|
hotkey.handler = { [weak self] event in
|
|
self?.handle(event)
|
|
}
|
|
startHotkeyWithRetry()
|
|
|
|
NotificationCenter.default.addObserver(
|
|
forName: .tinysttHotkeyChanged,
|
|
object: nil,
|
|
queue: .main
|
|
) { [weak self] _ in
|
|
MainActor.assumeIsolated {
|
|
guard let self else { return }
|
|
self.controller.cancel()
|
|
self.hotkey.update(spec: self.prefs.hotkey)
|
|
self.startHotkeyWithRetry()
|
|
self.status.update(state: self.controller.state)
|
|
}
|
|
}
|
|
|
|
NotificationCenter.default.addObserver(forName: .init("tinystt.recorder"), object: nil, queue: .main) { [weak self] notification in
|
|
MainActor.assumeIsolated {
|
|
self?.hotkey.suspended = notification.object as? Bool ?? false
|
|
self?.controller.cancel()
|
|
}
|
|
}
|
|
|
|
Task {
|
|
_ = await Permissions.requestMicrophone()
|
|
await ModelManager.shared.refresh()
|
|
if ModelManager.shared.state == .needsDownload {
|
|
await ModelManager.shared.download()
|
|
}
|
|
SpeechEngine.warmUp()
|
|
}
|
|
|
|
if !UserDefaults.standard.bool(forKey: "didShowOnboarding") || CommandLine.arguments.contains("--settings") {
|
|
UserDefaults.standard.set(true, forKey: "didShowOnboarding")
|
|
settings.show()
|
|
}
|
|
|
|
if !Permissions.accessibility {
|
|
Permissions.promptAccessibility()
|
|
}
|
|
}
|
|
|
|
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
|
settings.show()
|
|
return true
|
|
}
|
|
|
|
func applicationWillTerminate(_ notification: Notification) {
|
|
controller.cancel()
|
|
SpeechEngine.resetWarm()
|
|
hotkey?.stop()
|
|
permissionTimer?.invalidate()
|
|
}
|
|
|
|
// MARK: - 热键
|
|
|
|
private func startHotkeyWithRetry() {
|
|
if hotkey.start() {
|
|
Log.info("hotkey tap running")
|
|
permissionTimer?.invalidate()
|
|
permissionTimer = nil
|
|
return
|
|
}
|
|
|
|
Log.error("hotkey tap failed (no accessibility permission?)")
|
|
// 没有辅助功能权限时,定期重试(授权后无需重启即可生效)
|
|
guard permissionTimer == nil else { return }
|
|
permissionTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in
|
|
MainActor.assumeIsolated {
|
|
guard let self else { return }
|
|
if self.hotkey.start() {
|
|
self.permissionTimer?.invalidate()
|
|
self.permissionTimer = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func handle(_ event: HotkeyMonitor.Event) {
|
|
switch event {
|
|
case .pressed:
|
|
Log.info("hotkey pressed, state=\(controller.state)")
|
|
if prefs.triggerMode == .toggle {
|
|
controller.toggle()
|
|
} else {
|
|
controller.start()
|
|
}
|
|
case .released:
|
|
if prefs.triggerMode == .hold {
|
|
controller.stop()
|
|
}
|
|
case .cancel:
|
|
controller.cancel()
|
|
}
|
|
}
|
|
|
|
private func presentError(_ message: String) {
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
let alert = NSAlert()
|
|
alert.messageText = "TinySTT"
|
|
alert.informativeText = message
|
|
alert.alertStyle = .warning
|
|
alert.addButton(withTitle: "知道了")
|
|
alert.runModal()
|
|
}
|
|
}
|