Merge pull request #462 from Beingpax/powermode-enhancements
Add Power Mode keyboard shortcuts and improve session management
This commit is contained in:
commit
c7fe067252
@ -43,6 +43,7 @@ class HotkeyManager: ObservableObject {
|
||||
|
||||
private var whisperState: WhisperState
|
||||
private var miniRecorderShortcutManager: MiniRecorderShortcutManager
|
||||
private var powerModeShortcutManager: PowerModeShortcutManager
|
||||
|
||||
// MARK: - Helper Properties
|
||||
private var canProcessHotkeyAction: Bool {
|
||||
@ -73,7 +74,7 @@ class HotkeyManager: ObservableObject {
|
||||
private var shortcutCurrentKeyState = false
|
||||
private var lastShortcutTriggerTime: Date?
|
||||
private let shortcutCooldownInterval: TimeInterval = 0.5
|
||||
|
||||
|
||||
enum HotkeyOption: String, CaseIterable {
|
||||
case none = "none"
|
||||
case rightOption = "rightOption"
|
||||
@ -127,6 +128,7 @@ class HotkeyManager: ObservableObject {
|
||||
|
||||
self.whisperState = whisperState
|
||||
self.miniRecorderShortcutManager = MiniRecorderShortcutManager(whisperState: whisperState)
|
||||
self.powerModeShortcutManager = PowerModeShortcutManager(whisperState: whisperState)
|
||||
|
||||
KeyboardShortcuts.onKeyUp(for: .pasteLastTranscription) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
@ -24,7 +24,16 @@ class ActiveWindowService: ObservableObject {
|
||||
self.whisperState = whisperState
|
||||
}
|
||||
|
||||
func applyConfigurationForCurrentApp() async {
|
||||
func applyConfiguration(powerModeId: UUID? = nil) async {
|
||||
if let powerModeId = powerModeId,
|
||||
let config = PowerModeManager.shared.getConfiguration(with: powerModeId) {
|
||||
await MainActor.run {
|
||||
PowerModeManager.shared.setActiveConfiguration(config)
|
||||
}
|
||||
await PowerModeSessionManager.shared.beginSession(with: config)
|
||||
return
|
||||
}
|
||||
|
||||
guard let frontmostApp = NSWorkspace.shared.frontmostApplication,
|
||||
let bundleIdentifier = frontmostApp.bundleIdentifier else {
|
||||
return
|
||||
@ -60,8 +69,6 @@ class ActiveWindowService: ObservableObject {
|
||||
PowerModeManager.shared.setActiveConfiguration(config)
|
||||
}
|
||||
await PowerModeSessionManager.shared.beginSession(with: config)
|
||||
} else {
|
||||
// If no config found, keep the current active configuration (don't clear it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import KeyboardShortcuts
|
||||
|
||||
struct PowerModeConfig: Codable, Identifiable, Equatable {
|
||||
var id: UUID
|
||||
@ -16,9 +17,10 @@ struct PowerModeConfig: Codable, Identifiable, Equatable {
|
||||
var isAutoSendEnabled: Bool = false
|
||||
var isEnabled: Bool = true
|
||||
var isDefault: Bool = false
|
||||
var hotkeyShortcut: String? = nil
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, name, emoji, appConfigs, urlConfigs, isAIEnhancementEnabled, selectedPrompt, selectedLanguage, useScreenCapture, selectedAIProvider, selectedAIModel, isAutoSendEnabled, isEnabled, isDefault
|
||||
case id, name, emoji, appConfigs, urlConfigs, isAIEnhancementEnabled, selectedPrompt, selectedLanguage, useScreenCapture, selectedAIProvider, selectedAIModel, isAutoSendEnabled, isEnabled, isDefault, hotkeyShortcut
|
||||
case selectedWhisperModel
|
||||
case selectedTranscriptionModelName
|
||||
}
|
||||
@ -26,7 +28,7 @@ struct PowerModeConfig: Codable, Identifiable, Equatable {
|
||||
init(id: UUID = UUID(), name: String, emoji: String, appConfigs: [AppConfig]? = nil,
|
||||
urlConfigs: [URLConfig]? = nil, isAIEnhancementEnabled: Bool, selectedPrompt: String? = nil,
|
||||
selectedTranscriptionModelName: String? = nil, selectedLanguage: String? = nil, useScreenCapture: Bool = false,
|
||||
selectedAIProvider: String? = nil, selectedAIModel: String? = nil, isAutoSendEnabled: Bool = false, isEnabled: Bool = true, isDefault: Bool = false) {
|
||||
selectedAIProvider: String? = nil, selectedAIModel: String? = nil, isAutoSendEnabled: Bool = false, isEnabled: Bool = true, isDefault: Bool = false, hotkeyShortcut: String? = nil) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.emoji = emoji
|
||||
@ -42,6 +44,7 @@ struct PowerModeConfig: Codable, Identifiable, Equatable {
|
||||
self.selectedLanguage = selectedLanguage ?? UserDefaults.standard.string(forKey: "SelectedLanguage") ?? "en"
|
||||
self.isEnabled = isEnabled
|
||||
self.isDefault = isDefault
|
||||
self.hotkeyShortcut = hotkeyShortcut
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
@ -60,6 +63,7 @@ struct PowerModeConfig: Codable, Identifiable, Equatable {
|
||||
isAutoSendEnabled = try container.decodeIfPresent(Bool.self, forKey: .isAutoSendEnabled) ?? false
|
||||
isEnabled = try container.decodeIfPresent(Bool.self, forKey: .isEnabled) ?? true
|
||||
isDefault = try container.decodeIfPresent(Bool.self, forKey: .isDefault) ?? false
|
||||
hotkeyShortcut = try container.decodeIfPresent(String.self, forKey: .hotkeyShortcut)
|
||||
|
||||
if let newModelName = try container.decodeIfPresent(String.self, forKey: .selectedTranscriptionModelName) {
|
||||
selectedTranscriptionModelName = newModelName
|
||||
@ -87,6 +91,7 @@ struct PowerModeConfig: Codable, Identifiable, Equatable {
|
||||
try container.encodeIfPresent(selectedTranscriptionModelName, forKey: .selectedTranscriptionModelName)
|
||||
try container.encode(isEnabled, forKey: .isEnabled)
|
||||
try container.encode(isDefault, forKey: .isDefault)
|
||||
try container.encodeIfPresent(hotkeyShortcut, forKey: .hotkeyShortcut)
|
||||
}
|
||||
|
||||
|
||||
@ -155,6 +160,7 @@ class PowerModeManager: ObservableObject {
|
||||
if let data = try? JSONEncoder().encode(configurations) {
|
||||
UserDefaults.standard.set(data, forKey: configKey)
|
||||
}
|
||||
NotificationCenter.default.post(name: NSNotification.Name("PowerModeConfigurationsDidChange"), object: nil)
|
||||
}
|
||||
|
||||
func addConfiguration(_ config: PowerModeConfig) {
|
||||
@ -165,6 +171,7 @@ class PowerModeManager: ObservableObject {
|
||||
}
|
||||
|
||||
func removeConfiguration(with id: UUID) {
|
||||
KeyboardShortcuts.setShortcut(nil, for: .powerMode(id: id))
|
||||
configurations.removeAll { $0.id == id }
|
||||
saveConfigurations()
|
||||
}
|
||||
@ -221,18 +228,18 @@ class PowerModeManager: ObservableObject {
|
||||
return configurations.contains { $0.isDefault }
|
||||
}
|
||||
|
||||
func setAsDefault(configId: UUID) {
|
||||
// Clear any existing default
|
||||
func setAsDefault(configId: UUID, skipSave: Bool = false) {
|
||||
for index in configurations.indices {
|
||||
configurations[index].isDefault = false
|
||||
}
|
||||
|
||||
// Set the specified config as default
|
||||
|
||||
if let index = configurations.firstIndex(where: { $0.id == configId }) {
|
||||
configurations[index].isDefault = true
|
||||
}
|
||||
|
||||
saveConfigurations()
|
||||
|
||||
if !skipSave {
|
||||
saveConfigurations()
|
||||
}
|
||||
}
|
||||
|
||||
func enableConfiguration(with id: UUID) {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import SwiftUI
|
||||
import KeyboardShortcuts
|
||||
|
||||
struct ConfigurationView: View {
|
||||
let mode: ConfigurationMode
|
||||
@ -42,6 +43,9 @@ struct ConfigurationView: View {
|
||||
@State private var isEditingPrompt = false
|
||||
@State private var selectedPromptForEdit: CustomPrompt?
|
||||
|
||||
// PowerMode hotkey configuration
|
||||
@State private var powerModeConfigId: UUID = UUID()
|
||||
|
||||
private func languageSelectionDisabled() -> Bool {
|
||||
guard let selectedModelName = effectiveModelName,
|
||||
let model = whisperState.allAvailableModels.first(where: { $0.name == selectedModelName })
|
||||
@ -83,10 +87,12 @@ struct ConfigurationView: View {
|
||||
init(mode: ConfigurationMode, powerModeManager: PowerModeManager) {
|
||||
self.mode = mode
|
||||
self.powerModeManager = powerModeManager
|
||||
|
||||
|
||||
// Always fetch the most current configuration data
|
||||
switch mode {
|
||||
case .add:
|
||||
let newId = UUID()
|
||||
_powerModeConfigId = State(initialValue: newId)
|
||||
_isAIEnhancementEnabled = State(initialValue: true)
|
||||
_selectedPromptId = State(initialValue: nil)
|
||||
_selectedTranscriptionModelName = State(initialValue: nil)
|
||||
@ -102,6 +108,7 @@ struct ConfigurationView: View {
|
||||
case .edit(let config):
|
||||
// Get the latest version of this config from PowerModeManager
|
||||
let latestConfig = powerModeManager.getConfiguration(with: config.id) ?? config
|
||||
_powerModeConfigId = State(initialValue: latestConfig.id)
|
||||
_isAIEnhancementEnabled = State(initialValue: latestConfig.isAIEnhancementEnabled)
|
||||
_selectedPromptId = State(initialValue: latestConfig.selectedPrompt.flatMap { UUID(uuidString: $0) })
|
||||
_selectedTranscriptionModelName = State(initialValue: latestConfig.selectedTranscriptionModelName)
|
||||
@ -613,12 +620,30 @@ struct ConfigurationView: View {
|
||||
|
||||
HStack {
|
||||
Toggle("Auto Send", isOn: $isAutoSendEnabled)
|
||||
|
||||
|
||||
InfoTip(
|
||||
title: "Auto Send",
|
||||
message: "Automatically presses the Return/Enter key after pasting text. This is useful for chat applications or forms where its not necessary to to make changes to the transcribed text"
|
||||
)
|
||||
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
HStack {
|
||||
Text("Keyboard Shortcut")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
KeyboardShortcuts.Recorder(for: .powerMode(id: powerModeConfigId))
|
||||
.controlSize(.small)
|
||||
|
||||
InfoTip(
|
||||
title: "Power Mode Hotkey",
|
||||
message: "Assign a unique keyboard shortcut to instantly activate this Power Mode and start recording"
|
||||
)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
@ -705,9 +730,13 @@ struct ConfigurationView: View {
|
||||
}
|
||||
|
||||
private func getConfigForForm() -> PowerModeConfig {
|
||||
let shortcut = KeyboardShortcuts.getShortcut(for: .powerMode(id: powerModeConfigId))
|
||||
let hotkeyString = shortcut != nil ? "configured" : nil
|
||||
|
||||
switch mode {
|
||||
case .add:
|
||||
return PowerModeConfig(
|
||||
id: powerModeConfigId,
|
||||
name: configName,
|
||||
emoji: selectedEmoji,
|
||||
appConfigs: selectedAppConfigs.isEmpty ? nil : selectedAppConfigs,
|
||||
@ -720,7 +749,8 @@ struct ConfigurationView: View {
|
||||
selectedAIProvider: selectedAIProvider,
|
||||
selectedAIModel: selectedAIModel,
|
||||
isAutoSendEnabled: isAutoSendEnabled,
|
||||
isDefault: isDefault
|
||||
isDefault: isDefault,
|
||||
hotkeyShortcut: hotkeyString
|
||||
)
|
||||
case .edit(let config):
|
||||
var updatedConfig = config
|
||||
@ -737,6 +767,7 @@ struct ConfigurationView: View {
|
||||
updatedConfig.selectedAIProvider = selectedAIProvider
|
||||
updatedConfig.selectedAIModel = selectedAIModel
|
||||
updatedConfig.isDefault = isDefault
|
||||
updatedConfig.hotkeyShortcut = hotkeyString
|
||||
return updatedConfig
|
||||
}
|
||||
}
|
||||
@ -817,19 +848,17 @@ struct ConfigurationView: View {
|
||||
return
|
||||
}
|
||||
|
||||
// If validation passes, save the configuration
|
||||
if isDefault {
|
||||
powerModeManager.setAsDefault(configId: config.id, skipSave: true)
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case .add:
|
||||
powerModeManager.addConfiguration(config)
|
||||
case .edit:
|
||||
powerModeManager.updateConfiguration(config)
|
||||
}
|
||||
|
||||
// Handle default flag separately to ensure only one config is default
|
||||
if isDefault {
|
||||
powerModeManager.setAsDefault(configId: config.id)
|
||||
}
|
||||
|
||||
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,30 +41,38 @@ class PowerModeSessionManager {
|
||||
return
|
||||
}
|
||||
|
||||
let originalState = ApplicationState(
|
||||
isEnhancementEnabled: enhancementService.isEnhancementEnabled,
|
||||
useScreenCaptureContext: enhancementService.useScreenCaptureContext,
|
||||
selectedPromptId: enhancementService.selectedPromptId?.uuidString,
|
||||
selectedAIProvider: enhancementService.getAIService()?.selectedProvider.rawValue,
|
||||
selectedAIModel: enhancementService.getAIService()?.currentModel,
|
||||
selectedLanguage: UserDefaults.standard.string(forKey: "SelectedLanguage"),
|
||||
transcriptionModelName: whisperState.currentTranscriptionModel?.name
|
||||
)
|
||||
// Only capture baseline if NO session exists
|
||||
if loadSession() == nil {
|
||||
let originalState = ApplicationState(
|
||||
isEnhancementEnabled: enhancementService.isEnhancementEnabled,
|
||||
useScreenCaptureContext: enhancementService.useScreenCaptureContext,
|
||||
selectedPromptId: enhancementService.selectedPromptId?.uuidString,
|
||||
selectedAIProvider: enhancementService.getAIService()?.selectedProvider.rawValue,
|
||||
selectedAIModel: enhancementService.getAIService()?.currentModel,
|
||||
selectedLanguage: UserDefaults.standard.string(forKey: "SelectedLanguage"),
|
||||
transcriptionModelName: whisperState.currentTranscriptionModel?.name
|
||||
)
|
||||
|
||||
let newSession = PowerModeSession(
|
||||
id: UUID(),
|
||||
startTime: Date(),
|
||||
originalState: originalState
|
||||
)
|
||||
saveSession(newSession)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateSessionSnapshot), name: .AppSettingsDidChange, object: nil)
|
||||
let newSession = PowerModeSession(
|
||||
id: UUID(),
|
||||
startTime: Date(),
|
||||
originalState: originalState
|
||||
)
|
||||
saveSession(newSession)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateSessionSnapshot), name: .AppSettingsDidChange, object: nil)
|
||||
}
|
||||
|
||||
// Always apply the new configuration
|
||||
isApplyingPowerModeConfig = true
|
||||
await applyConfiguration(config)
|
||||
isApplyingPowerModeConfig = false
|
||||
}
|
||||
|
||||
var hasActiveSession: Bool {
|
||||
return loadSession() != nil
|
||||
}
|
||||
|
||||
func endSession() async {
|
||||
guard let session = loadSession() else { return }
|
||||
|
||||
|
||||
84
VoiceInk/PowerMode/PowerModeShortcutManager.swift
Normal file
84
VoiceInk/PowerMode/PowerModeShortcutManager.swift
Normal file
@ -0,0 +1,84 @@
|
||||
import Foundation
|
||||
import KeyboardShortcuts
|
||||
|
||||
@MainActor
|
||||
class PowerModeShortcutManager {
|
||||
private weak var whisperState: WhisperState?
|
||||
private var registeredPowerModeIds: Set<UUID> = []
|
||||
|
||||
init(whisperState: WhisperState) {
|
||||
self.whisperState = whisperState
|
||||
|
||||
setupPowerModeHotkeys()
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(powerModeConfigurationsDidChange),
|
||||
name: NSNotification.Name("PowerModeConfigurationsDidChange"),
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
@objc private func powerModeConfigurationsDidChange() {
|
||||
Task { @MainActor in
|
||||
setupPowerModeHotkeys()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupPowerModeHotkeys() {
|
||||
let powerModesWithShortcuts = Set(PowerModeManager.shared.configurations
|
||||
.filter { $0.hotkeyShortcut != nil }
|
||||
.map { $0.id })
|
||||
|
||||
// Remove shortcuts for deleted or updated configs
|
||||
let idsToRemove = registeredPowerModeIds.subtracting(powerModesWithShortcuts)
|
||||
idsToRemove.forEach { id in
|
||||
KeyboardShortcuts.setShortcut(nil, for: .powerMode(id: id))
|
||||
registeredPowerModeIds.remove(id)
|
||||
}
|
||||
|
||||
// Add new shortcuts
|
||||
PowerModeManager.shared.configurations.forEach { config in
|
||||
guard config.hotkeyShortcut != nil else { return }
|
||||
guard !registeredPowerModeIds.contains(config.id) else { return }
|
||||
|
||||
KeyboardShortcuts.onKeyUp(for: .powerMode(id: config.id)) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
Task { @MainActor in
|
||||
await self.handlePowerModeHotkey(powerModeId: config.id)
|
||||
}
|
||||
}
|
||||
|
||||
registeredPowerModeIds.insert(config.id)
|
||||
}
|
||||
}
|
||||
|
||||
private func handlePowerModeHotkey(powerModeId: UUID) async {
|
||||
guard let whisperState = whisperState,
|
||||
canProcessHotkeyAction(whisperState: whisperState) else { return }
|
||||
|
||||
guard let config = PowerModeManager.shared.getConfiguration(with: powerModeId),
|
||||
config.hotkeyShortcut != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
await whisperState.toggleMiniRecorder(powerModeId: powerModeId)
|
||||
}
|
||||
|
||||
private func canProcessHotkeyAction(whisperState: WhisperState) -> Bool {
|
||||
whisperState.recordingState != .transcribing &&
|
||||
whisperState.recordingState != .enhancing &&
|
||||
whisperState.recordingState != .busy
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PowerMode Keyboard Shortcut Names
|
||||
extension KeyboardShortcuts.Name {
|
||||
static func powerMode(id: UUID) -> Self {
|
||||
Self("powerMode_\(id.uuidString)")
|
||||
}
|
||||
}
|
||||
@ -32,10 +32,10 @@ extension WhisperState {
|
||||
|
||||
// MARK: - Mini Recorder Management
|
||||
|
||||
func toggleMiniRecorder() async {
|
||||
func toggleMiniRecorder(powerModeId: UUID? = nil) async {
|
||||
if isMiniRecorderVisible {
|
||||
if recordingState == .recording {
|
||||
await toggleRecord()
|
||||
await toggleRecord(powerModeId: powerModeId)
|
||||
} else {
|
||||
await cancelRecording()
|
||||
}
|
||||
@ -46,7 +46,7 @@ extension WhisperState {
|
||||
isMiniRecorderVisible = true // This will call showRecorderPanel() via didSet
|
||||
}
|
||||
|
||||
await toggleRecord()
|
||||
await toggleRecord(powerModeId: powerModeId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -138,7 +138,7 @@ class WhisperState: NSObject, ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func toggleRecord() async {
|
||||
func toggleRecord(powerModeId: UUID? = nil) async {
|
||||
if recordingState == .recording {
|
||||
await recorder.stopRecording()
|
||||
if let recordedFile {
|
||||
@ -195,7 +195,14 @@ class WhisperState: NSObject, ObservableObject {
|
||||
self.recordingState = .recording
|
||||
}
|
||||
|
||||
await ActiveWindowService.shared.applyConfigurationForCurrentApp()
|
||||
if let powerModeId = powerModeId {
|
||||
await ActiveWindowService.shared.applyConfiguration(powerModeId: powerModeId)
|
||||
} else {
|
||||
let hasActiveSession = await PowerModeSessionManager.shared.hasActiveSession
|
||||
if !hasActiveSession {
|
||||
await ActiveWindowService.shared.applyConfiguration()
|
||||
}
|
||||
}
|
||||
|
||||
// Load model and capture context in background without blocking
|
||||
Task.detached { [weak self] in
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user