diff --git a/VoiceInk/PowerMode/ActiveWindowService.swift b/VoiceInk/PowerMode/ActiveWindowService.swift index 1a3cc0a..8c898f2 100644 --- a/VoiceInk/PowerMode/ActiveWindowService.swift +++ b/VoiceInk/PowerMode/ActiveWindowService.swift @@ -27,7 +27,6 @@ class ActiveWindowService: ObservableObject { func applyConfigurationForCurrentApp() async { guard let frontmostApp = NSWorkspace.shared.frontmostApplication, let bundleIdentifier = frontmostApp.bundleIdentifier else { - await PowerModeSessionManager.shared.endSession() return } @@ -53,7 +52,7 @@ class ActiveWindowService: ObservableObject { } if configToApply == nil { - configToApply = PowerModeManager.shared.getWildcardConfiguration() + configToApply = PowerModeManager.shared.getDefaultConfiguration() } if let config = configToApply { @@ -61,11 +60,7 @@ class ActiveWindowService: ObservableObject { PowerModeManager.shared.setActiveConfiguration(config) } await PowerModeSessionManager.shared.beginSession(with: config) - } else { - await MainActor.run { - PowerModeManager.shared.setActiveConfiguration(nil) - } - await PowerModeSessionManager.shared.endSession() } + // If no config found, keep the current active configuration (don't clear it) } } diff --git a/VoiceInk/PowerMode/PowerModeConfig.swift b/VoiceInk/PowerMode/PowerModeConfig.swift index 80a68ab..1ea9d90 100644 --- a/VoiceInk/PowerMode/PowerModeConfig.swift +++ b/VoiceInk/PowerMode/PowerModeConfig.swift @@ -15,9 +15,10 @@ struct PowerModeConfig: Codable, Identifiable, Equatable { var selectedAIModel: String? var isAutoSendEnabled: Bool = false var isEnabled: Bool = true + var isDefault: Bool = false enum CodingKeys: String, CodingKey { - case id, name, emoji, appConfigs, urlConfigs, isAIEnhancementEnabled, selectedPrompt, selectedLanguage, useScreenCapture, selectedAIProvider, selectedAIModel, isAutoSendEnabled, isEnabled + case id, name, emoji, appConfigs, urlConfigs, isAIEnhancementEnabled, selectedPrompt, selectedLanguage, useScreenCapture, selectedAIProvider, selectedAIModel, isAutoSendEnabled, isEnabled, isDefault case selectedWhisperModel case selectedTranscriptionModelName } @@ -25,7 +26,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) { + selectedAIProvider: String? = nil, selectedAIModel: String? = nil, isAutoSendEnabled: Bool = false, isEnabled: Bool = true, isDefault: Bool = false) { self.id = id self.name = name self.emoji = emoji @@ -40,6 +41,7 @@ struct PowerModeConfig: Codable, Identifiable, Equatable { self.selectedTranscriptionModelName = selectedTranscriptionModelName ?? UserDefaults.standard.string(forKey: "CurrentTranscriptionModel") self.selectedLanguage = selectedLanguage ?? UserDefaults.standard.string(forKey: "SelectedLanguage") ?? "en" self.isEnabled = isEnabled + self.isDefault = isDefault } init(from decoder: Decoder) throws { @@ -57,6 +59,7 @@ struct PowerModeConfig: Codable, Identifiable, Equatable { selectedAIModel = try container.decodeIfPresent(String.self, forKey: .selectedAIModel) 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 if let newModelName = try container.decodeIfPresent(String.self, forKey: .selectedTranscriptionModelName) { selectedTranscriptionModelName = newModelName @@ -83,6 +86,7 @@ struct PowerModeConfig: Codable, Identifiable, Equatable { try container.encode(isAutoSendEnabled, forKey: .isAutoSendEnabled) try container.encodeIfPresent(selectedTranscriptionModelName, forKey: .selectedTranscriptionModelName) try container.encode(isEnabled, forKey: .isEnabled) + try container.encode(isDefault, forKey: .isDefault) } @@ -204,15 +208,26 @@ class PowerModeManager: ObservableObject { return nil } - func getWildcardConfiguration() -> PowerModeConfig? { - for config in configurations.filter({ $0.isEnabled }) { - if let urlConfigs = config.urlConfigs { - if urlConfigs.contains(where: { $0.url == "*" }) { - return config - } - } + func getDefaultConfiguration() -> PowerModeConfig? { + return configurations.first { $0.isEnabled && $0.isDefault } + } + + func hasDefaultConfiguration() -> Bool { + return configurations.contains { $0.isDefault } + } + + func setAsDefault(configId: UUID) { + // Clear any existing default + for index in configurations.indices { + configurations[index].isDefault = false } - return nil + + // Set the specified config as default + if let index = configurations.firstIndex(where: { $0.id == configId }) { + configurations[index].isDefault = true + } + + saveConfigurations() } func enableConfiguration(with id: UUID) { diff --git a/VoiceInk/PowerMode/PowerModeConfigView.swift b/VoiceInk/PowerMode/PowerModeConfigView.swift index d5627d9..fb35a73 100644 --- a/VoiceInk/PowerMode/PowerModeConfigView.swift +++ b/VoiceInk/PowerMode/PowerModeConfigView.swift @@ -36,6 +36,7 @@ struct ConfigurationView: View { // New state for screen capture toggle @State private var useScreenCapture = false @State private var isAutoSendEnabled = false + @State private var isDefault = false // State for prompt editing (similar to EnhancementSettingsView) @State private var isEditingPrompt = false @@ -44,6 +45,14 @@ struct ConfigurationView: View { // Whisper state for model selection @EnvironmentObject private var whisperState: WhisperState + // Computed property to check if current config is the default + private var isCurrentConfigDefault: Bool { + if case .edit(let config) = mode { + return config.isDefault + } + return false + } + private var filteredApps: [(url: URL, name: String, bundleId: String, icon: NSImage)] { if searchText.isEmpty { return installedApps @@ -77,6 +86,7 @@ struct ConfigurationView: View { _selectedEmoji = State(initialValue: "✏️") _useScreenCapture = State(initialValue: false) _isAutoSendEnabled = State(initialValue: false) + _isDefault = State(initialValue: false) // Default to current global AI provider/model for new configurations - use UserDefaults only _selectedAIProvider = State(initialValue: UserDefaults.standard.string(forKey: "selectedAIProvider")) _selectedAIModel = State(initialValue: nil) // Initialize to nil and set it after view appears @@ -93,6 +103,7 @@ struct ConfigurationView: View { _websiteConfigs = State(initialValue: latestConfig.urlConfigs ?? []) _useScreenCapture = State(initialValue: latestConfig.useScreenCapture) _isAutoSendEnabled = State(initialValue: latestConfig.isAutoSendEnabled) + _isDefault = State(initialValue: latestConfig.isDefault) _selectedAIProvider = State(initialValue: latestConfig.selectedAIProvider) _selectedAIModel = State(initialValue: latestConfig.selectedAIModel) } @@ -131,33 +142,50 @@ struct ConfigurationView: View { ScrollView { VStack(spacing: 20) { // Main Input Section - HStack(spacing: 16) { - Button(action: { - isShowingEmojiPicker.toggle() - }) { - ZStack { - Circle() - .fill(Color.accentColor.opacity(0.15)) - .frame(width: 48, height: 48) - - Text(selectedEmoji) - .font(.system(size: 24)) + VStack(spacing: 16) { + HStack(spacing: 16) { + Button(action: { + isShowingEmojiPicker.toggle() + }) { + ZStack { + Circle() + .fill(Color.accentColor.opacity(0.15)) + .frame(width: 48, height: 48) + + Text(selectedEmoji) + .font(.system(size: 24)) + } } - } - .buttonStyle(.plain) - .popover(isPresented: $isShowingEmojiPicker, arrowEdge: .bottom) { - EmojiPickerView( - selectedEmoji: $selectedEmoji, - isPresented: $isShowingEmojiPicker - ) + .buttonStyle(.plain) + .popover(isPresented: $isShowingEmojiPicker, arrowEdge: .bottom) { + EmojiPickerView( + selectedEmoji: $selectedEmoji, + isPresented: $isShowingEmojiPicker + ) + } + + TextField("Name your power mode", text: $configName) + .font(.system(size: 18, weight: .bold)) + .textFieldStyle(.plain) + .foregroundColor(.primary) + .tint(.accentColor) + .focused($isNameFieldFocused) } - TextField("Name your power mode", text: $configName) - .font(.system(size: 18, weight: .bold)) - .textFieldStyle(.plain) - .foregroundColor(.primary) - .tint(.accentColor) - .focused($isNameFieldFocused) + // Default Power Mode Toggle + if !powerModeManager.hasDefaultConfiguration() || isCurrentConfigDefault { + HStack { + Toggle("Set as default power mode", isOn: $isDefault) + .font(.system(size: 14)) + + InfoTip( + title: "Default Power Mode", + message: "Default power mode is used when no specific app or website matches are found" + ) + + Spacer() + } + } } .padding(.horizontal, 20) .padding(.vertical, 16) @@ -649,7 +677,8 @@ struct ConfigurationView: View { useScreenCapture: useScreenCapture, selectedAIProvider: selectedAIProvider, selectedAIModel: selectedAIModel, - isAutoSendEnabled: isAutoSendEnabled + isAutoSendEnabled: isAutoSendEnabled, + isDefault: isDefault ) case .edit(let config): var updatedConfig = config @@ -665,6 +694,7 @@ struct ConfigurationView: View { updatedConfig.isAutoSendEnabled = isAutoSendEnabled updatedConfig.selectedAIProvider = selectedAIProvider updatedConfig.selectedAIModel = selectedAIModel + updatedConfig.isDefault = isDefault return updatedConfig } } @@ -753,6 +783,11 @@ struct ConfigurationView: View { powerModeManager.updateConfiguration(config) } + // Handle default flag separately to ensure only one config is default + if isDefault { + powerModeManager.setAsDefault(configId: config.id) + } + presentationMode.wrappedValue.dismiss() } } diff --git a/VoiceInk/PowerMode/PowerModeViewComponents.swift b/VoiceInk/PowerMode/PowerModeViewComponents.swift index b0a70c1..0687f15 100644 --- a/VoiceInk/PowerMode/PowerModeViewComponents.swift +++ b/VoiceInk/PowerMode/PowerModeViewComponents.swift @@ -158,6 +158,12 @@ struct ConfigurationRow: View { HStack(spacing: 6) { Text(config.name) .font(.system(size: 15, weight: .semibold)) + + if config.isDefault { + Image(systemName: "star.fill") + .font(.system(size: 12)) + .foregroundColor(.accentColor) + } } HStack(spacing: 12) { diff --git a/VoiceInk/Whisper/WhisperState.swift b/VoiceInk/Whisper/WhisperState.swift index c38e151..5f0399f 100644 --- a/VoiceInk/Whisper/WhisperState.swift +++ b/VoiceInk/Whisper/WhisperState.swift @@ -220,7 +220,6 @@ class WhisperState: NSObject, ObservableObject { await MainActor.run { recordingState = .idle } - await PowerModeSessionManager.shared.endSession() await cleanupModelResources() return } @@ -377,7 +376,6 @@ class WhisperState: NSObject, ObservableObject { } await self.dismissMiniRecorder() - await PowerModeSessionManager.shared.endSession() } catch { do { @@ -412,7 +410,6 @@ class WhisperState: NSObject, ObservableObject { } await self.dismissMiniRecorder() - await PowerModeSessionManager.shared.endSession() } }