diff --git a/VoiceInk/AppIntents/AppShortcuts.swift b/VoiceInk/AppIntents/AppShortcuts.swift new file mode 100644 index 0000000..f4d5301 --- /dev/null +++ b/VoiceInk/AppIntents/AppShortcuts.swift @@ -0,0 +1,33 @@ +import AppIntents +import Foundation + +struct AppShortcuts : AppShortcutsProvider { + @AppShortcutsBuilder + static var appShortcuts: [AppShortcut] { + AppShortcut( + intent: ToggleMiniRecorderIntent(), + phrases: [ + "Toggle \(.applicationName) recorder", + "Start \(.applicationName) recording", + "Stop \(.applicationName) recording", + "Toggle recorder in \(.applicationName)", + "Start recording in \(.applicationName)", + "Stop recording in \(.applicationName)" + ], + shortTitle: "Toggle Recorder", + systemImageName: "mic.circle" + ) + + AppShortcut( + intent: DismissMiniRecorderIntent(), + phrases: [ + "Dismiss \(.applicationName) recorder", + "Cancel \(.applicationName) recording", + "Close \(.applicationName) recorder", + "Hide \(.applicationName) recorder" + ], + shortTitle: "Dismiss Recorder", + systemImageName: "xmark.circle" + ) + } +} diff --git a/VoiceInk/AppIntents/DismissMiniRecorderIntent.swift b/VoiceInk/AppIntents/DismissMiniRecorderIntent.swift new file mode 100644 index 0000000..5a2b5fe --- /dev/null +++ b/VoiceInk/AppIntents/DismissMiniRecorderIntent.swift @@ -0,0 +1,18 @@ +import AppIntents +import Foundation +import AppKit + +struct DismissMiniRecorderIntent: AppIntent { + static var title: LocalizedStringResource = "Dismiss VoiceInk Recorder" + static var description = IntentDescription("Dismiss the VoiceInk mini recorder and cancel any active recording.") + + static var openAppWhenRun: Bool = false + + @MainActor + func perform() async throws -> some IntentResult & ProvidesDialog { + NotificationCenter.default.post(name: .dismissMiniRecorder, object: nil) + + let dialog = IntentDialog(stringLiteral: "VoiceInk recorder dismissed") + return .result(dialog: dialog) + } +} diff --git a/VoiceInk/AppIntents/ToggleMiniRecorderIntent.swift b/VoiceInk/AppIntents/ToggleMiniRecorderIntent.swift new file mode 100644 index 0000000..af7a0e1 --- /dev/null +++ b/VoiceInk/AppIntents/ToggleMiniRecorderIntent.swift @@ -0,0 +1,32 @@ +import AppIntents +import Foundation +import AppKit + +struct ToggleMiniRecorderIntent: AppIntent { + static var title: LocalizedStringResource = "Toggle VoiceInk Recorder" + static var description = IntentDescription("Start or stop the VoiceInk mini recorder for voice transcription.") + + static var openAppWhenRun: Bool = false + + @MainActor + func perform() async throws -> some IntentResult & ProvidesDialog { + NotificationCenter.default.post(name: .toggleMiniRecorder, object: nil) + + let dialog = IntentDialog(stringLiteral: "VoiceInk recorder toggled") + return .result(dialog: dialog) + } +} + +enum IntentError: Error, LocalizedError { + case appNotAvailable + case serviceNotAvailable + + var errorDescription: String? { + switch self { + case .appNotAvailable: + return "VoiceInk app is not available" + case .serviceNotAvailable: + return "VoiceInk recording service is not available" + } + } +} diff --git a/VoiceInk/Notifications/AppNotifications.swift b/VoiceInk/Notifications/AppNotifications.swift index 8a40a4c..0c1ae30 100644 --- a/VoiceInk/Notifications/AppNotifications.swift +++ b/VoiceInk/Notifications/AppNotifications.swift @@ -5,6 +5,7 @@ extension Notification.Name { static let languageDidChange = Notification.Name("languageDidChange") static let promptDidChange = Notification.Name("promptDidChange") static let toggleMiniRecorder = Notification.Name("toggleMiniRecorder") + static let dismissMiniRecorder = Notification.Name("dismissMiniRecorder") static let didChangeModel = Notification.Name("didChangeModel") static let aiProviderKeyChanged = Notification.Name("aiProviderKeyChanged") static let licenseStatusChanged = Notification.Name("licenseStatusChanged") diff --git a/VoiceInk/VoiceInk.swift b/VoiceInk/VoiceInk.swift index 28a3c97..541996a 100644 --- a/VoiceInk/VoiceInk.swift +++ b/VoiceInk/VoiceInk.swift @@ -3,6 +3,7 @@ import SwiftData import Sparkle import AppKit import OSLog +import AppIntents @main struct VoiceInkApp: App { @@ -82,6 +83,8 @@ struct VoiceInkApp: App { activeWindowService.configure(with: enhancementService) activeWindowService.configureWhisperState(whisperState) _activeWindowService = StateObject(wrappedValue: activeWindowService) + + AppShortcuts.updateAppShortcutParameters() } var body: some Scene { diff --git a/VoiceInk/Whisper/WhisperState+UI.swift b/VoiceInk/Whisper/WhisperState+UI.swift index 347b5bf..6506441 100644 --- a/VoiceInk/Whisper/WhisperState+UI.swift +++ b/VoiceInk/Whisper/WhisperState+UI.swift @@ -90,6 +90,7 @@ extension WhisperState { func setupNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(handleToggleMiniRecorder), name: .toggleMiniRecorder, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(handleDismissMiniRecorder), name: .dismissMiniRecorder, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleLicenseStatusChanged), name: .licenseStatusChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handlePromptChange), name: .promptDidChange, object: nil) } @@ -100,6 +101,12 @@ extension WhisperState { } } + @objc public func handleDismissMiniRecorder() { + Task { + await dismissMiniRecorder() + } + } + @objc func handleLicenseStatusChanged() { self.licenseViewModel = LicenseViewModel() }