diff --git a/VoiceInk/MiniRecorderShortcutManager.swift b/VoiceInk/MiniRecorderShortcutManager.swift index 407a802..1bb3588 100644 --- a/VoiceInk/MiniRecorderShortcutManager.swift +++ b/VoiceInk/MiniRecorderShortcutManager.swift @@ -175,8 +175,8 @@ class MiniRecorderShortcutManager: ObservableObject { let powerModeManager = PowerModeManager.shared - if powerModeManager.isPowerModeEnabled { - let availableConfigurations = powerModeManager.getAllAvailableConfigurations() + if !powerModeManager.enabledConfigurations.isEmpty { + let availableConfigurations = powerModeManager.enabledConfigurations if index < availableConfigurations.count { let selectedConfig = availableConfigurations[index] powerModeManager.setActiveConfiguration(selectedConfig) diff --git a/VoiceInk/PowerMode/ActiveWindowService.swift b/VoiceInk/PowerMode/ActiveWindowService.swift index 5d05139..6eebe7c 100644 --- a/VoiceInk/PowerMode/ActiveWindowService.swift +++ b/VoiceInk/PowerMode/ActiveWindowService.swift @@ -25,10 +25,6 @@ class ActiveWindowService: ObservableObject { } func applyConfigurationForCurrentApp() async { - guard PowerModeManager.shared.isPowerModeEnabled else { - return - } - guard let frontmostApp = NSWorkspace.shared.frontmostApplication, let bundleIdentifier = frontmostApp.bundleIdentifier else { return } @@ -59,13 +55,16 @@ class ActiveWindowService: ObservableObject { } } - let config = PowerModeManager.shared.getConfigurationForApp(bundleIdentifier) ?? PowerModeManager.shared.defaultConfig - - await MainActor.run { - PowerModeManager.shared.setActiveConfiguration(config) + if let config = PowerModeManager.shared.getConfigurationForApp(bundleIdentifier) { + await MainActor.run { + PowerModeManager.shared.setActiveConfiguration(config) + } + await applyConfiguration(config) + } else { + await MainActor.run { + PowerModeManager.shared.setActiveConfiguration(nil) + } } - - await applyConfiguration(config) } /// Applies a specific configuration diff --git a/VoiceInk/PowerMode/PowerModeConfig.swift b/VoiceInk/PowerMode/PowerModeConfig.swift index 6a554b4..6e66400 100644 --- a/VoiceInk/PowerMode/PowerModeConfig.swift +++ b/VoiceInk/PowerMode/PowerModeConfig.swift @@ -13,12 +13,12 @@ struct PowerModeConfig: Codable, Identifiable, Equatable { var useScreenCapture: Bool var selectedAIProvider: String? var selectedAIModel: String? - // NEW: Automatically press the Return key after pasting var isAutoSendEnabled: Bool = false + var isEnabled: Bool = true // Custom coding keys to handle migration from selectedWhisperModel enum CodingKeys: String, CodingKey { - case id, name, emoji, appConfigs, urlConfigs, isAIEnhancementEnabled, selectedPrompt, selectedLanguage, useScreenCapture, selectedAIProvider, selectedAIModel, isAutoSendEnabled + case id, name, emoji, appConfigs, urlConfigs, isAIEnhancementEnabled, selectedPrompt, selectedLanguage, useScreenCapture, selectedAIProvider, selectedAIModel, isAutoSendEnabled, isEnabled case selectedWhisperModel // Old key case selectedTranscriptionModelName // New key } @@ -26,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) { + selectedAIProvider: String? = nil, selectedAIModel: String? = nil, isAutoSendEnabled: Bool = false, isEnabled: Bool = true) { self.id = id self.name = name self.emoji = emoji @@ -40,6 +40,7 @@ struct PowerModeConfig: Codable, Identifiable, Equatable { self.selectedAIModel = selectedAIModel self.selectedTranscriptionModelName = selectedTranscriptionModelName ?? UserDefaults.standard.string(forKey: "CurrentTranscriptionModel") self.selectedLanguage = selectedLanguage ?? UserDefaults.standard.string(forKey: "SelectedLanguage") ?? "en" + self.isEnabled = isEnabled } init(from decoder: Decoder) throws { @@ -56,6 +57,7 @@ struct PowerModeConfig: Codable, Identifiable, Equatable { selectedAIProvider = try container.decodeIfPresent(String.self, forKey: .selectedAIProvider) 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 if let newModelName = try container.decodeIfPresent(String.self, forKey: .selectedTranscriptionModelName) { selectedTranscriptionModelName = newModelName @@ -81,6 +83,7 @@ struct PowerModeConfig: Codable, Identifiable, Equatable { try container.encodeIfPresent(selectedAIModel, forKey: .selectedAIModel) try container.encode(isAutoSendEnabled, forKey: .isAutoSendEnabled) try container.encodeIfPresent(selectedTranscriptionModelName, forKey: .selectedTranscriptionModelName) + try container.encode(isEnabled, forKey: .isEnabled) } @@ -124,111 +127,64 @@ struct URLConfig: Codable, Identifiable, Equatable { class PowerModeManager: ObservableObject { static let shared = PowerModeManager() @Published var configurations: [PowerModeConfig] = [] - @Published var defaultConfig: PowerModeConfig - @Published var isPowerModeEnabled: Bool @Published var activeConfiguration: PowerModeConfig? - + private let configKey = "powerModeConfigurationsV2" - private let defaultConfigKey = "defaultPowerModeConfigV2" - private let powerModeEnabledKey = "isPowerModeEnabled" private let activeConfigIdKey = "activeConfigurationId" - + private init() { - // Load power mode enabled state or default to false if not set - if UserDefaults.standard.object(forKey: powerModeEnabledKey) != nil { - self.isPowerModeEnabled = UserDefaults.standard.bool(forKey: powerModeEnabledKey) - } else { - self.isPowerModeEnabled = false - UserDefaults.standard.set(false, forKey: powerModeEnabledKey) - } - - // Initialize default config with default values - if let data = UserDefaults.standard.data(forKey: defaultConfigKey), - let config = try? JSONDecoder().decode(PowerModeConfig.self, from: data) { - defaultConfig = config - } else { - // Get default values from UserDefaults if available - let defaultModelName = UserDefaults.standard.string(forKey: "CurrentTranscriptionModel") - let defaultLanguage = UserDefaults.standard.string(forKey: "SelectedLanguage") ?? "en" - - defaultConfig = PowerModeConfig( - id: UUID(), - name: "Default Configuration", - emoji: "⚙️", - isAIEnhancementEnabled: false, - selectedPrompt: nil, - selectedTranscriptionModelName: defaultModelName, - selectedLanguage: defaultLanguage - ) - saveDefaultConfig() - } loadConfigurations() - - // Set the active configuration, either from saved ID or default to the default config + + // Set the active configuration from saved ID if let activeConfigIdString = UserDefaults.standard.string(forKey: activeConfigIdKey), let activeConfigId = UUID(uuidString: activeConfigIdString) { - if let savedConfig = configurations.first(where: { $0.id == activeConfigId }) { - activeConfiguration = savedConfig - } else if activeConfigId == defaultConfig.id { - activeConfiguration = defaultConfig - } else { - activeConfiguration = defaultConfig - } + activeConfiguration = configurations.first { $0.id == activeConfigId } } else { - activeConfiguration = defaultConfig + activeConfiguration = nil } } - + private func loadConfigurations() { if let data = UserDefaults.standard.data(forKey: configKey), let configs = try? JSONDecoder().decode([PowerModeConfig].self, from: data) { configurations = configs } } - + func saveConfigurations() { if let data = try? JSONEncoder().encode(configurations) { UserDefaults.standard.set(data, forKey: configKey) } } - - private func saveDefaultConfig() { - if let data = try? JSONEncoder().encode(defaultConfig) { - UserDefaults.standard.set(data, forKey: defaultConfigKey) - } - } - + func addConfiguration(_ config: PowerModeConfig) { if !configurations.contains(where: { $0.id == config.id }) { configurations.append(config) saveConfigurations() } } - + func removeConfiguration(with id: UUID) { configurations.removeAll { $0.id == id } saveConfigurations() } - + func getConfiguration(with id: UUID) -> PowerModeConfig? { return configurations.first { $0.id == id } } - + func updateConfiguration(_ config: PowerModeConfig) { - if config.id == defaultConfig.id { - defaultConfig = config - saveDefaultConfig() - } else if let index = configurations.firstIndex(where: { $0.id == config.id }) { + if let index = configurations.firstIndex(where: { $0.id == config.id }) { configurations[index] = config saveConfigurations() } } - + // Get configuration for a specific URL func getConfigurationForURL(_ url: String) -> PowerModeConfig? { let cleanedURL = cleanURL(url) - for config in configurations { + for config in configurations.filter({ $0.isEnabled }) { if let urlConfigs = config.urlConfigs { for urlConfig in urlConfigs { let configURL = cleanURL(urlConfig.url) @@ -244,7 +200,7 @@ class PowerModeManager: ObservableObject { // Get configuration for an application bundle ID func getConfigurationForApp(_ bundleId: String) -> PowerModeConfig? { - for config in configurations { + for config in configurations.filter({ $0.isEnabled }) { if let appConfigs = config.appConfigs { if appConfigs.contains(where: { $0.bundleIdentifier == bundleId }) { return config @@ -254,6 +210,27 @@ class PowerModeManager: ObservableObject { return nil } + // Enable a configuration + func enableConfiguration(with id: UUID) { + if let index = configurations.firstIndex(where: { $0.id == id }) { + configurations[index].isEnabled = true + saveConfigurations() + } + } + + // Disable a configuration + func disableConfiguration(with id: UUID) { + if let index = configurations.firstIndex(where: { $0.id == id }) { + configurations[index].isEnabled = false + saveConfigurations() + } + } + + // Get all enabled configurations + var enabledConfigurations: [PowerModeConfig] { + return configurations.filter { $0.isEnabled } + } + // Add app configuration func addAppConfig(_ appConfig: AppConfig, to config: PowerModeConfig) { if var updatedConfig = configurations.first(where: { $0.id == config.id }) { @@ -263,7 +240,7 @@ class PowerModeManager: ObservableObject { updateConfiguration(updatedConfig) } } - + // Remove app configuration func removeAppConfig(_ appConfig: AppConfig, from config: PowerModeConfig) { if var updatedConfig = configurations.first(where: { $0.id == config.id }) { @@ -271,7 +248,7 @@ class PowerModeManager: ObservableObject { updateConfiguration(updatedConfig) } } - + // Add URL configuration func addURLConfig(_ urlConfig: URLConfig, to config: PowerModeConfig) { if var updatedConfig = configurations.first(where: { $0.id == config.id }) { @@ -281,7 +258,7 @@ class PowerModeManager: ObservableObject { updateConfiguration(updatedConfig) } } - + // Remove URL configuration func removeURLConfig(_ urlConfig: URLConfig, from config: PowerModeConfig) { if var updatedConfig = configurations.first(where: { $0.id == config.id }) { @@ -289,7 +266,7 @@ class PowerModeManager: ObservableObject { updateConfiguration(updatedConfig) } } - + // Clean URL for comparison func cleanURL(_ url: String) -> String { return url.lowercased() @@ -298,33 +275,25 @@ class PowerModeManager: ObservableObject { .replacingOccurrences(of: "www.", with: "") .trimmingCharacters(in: .whitespacesAndNewlines) } - - // Save power mode enabled state - func savePowerModeEnabled() { - UserDefaults.standard.set(isPowerModeEnabled, forKey: powerModeEnabledKey) - } - + // Set active configuration - func setActiveConfiguration(_ config: PowerModeConfig) { + func setActiveConfiguration(_ config: PowerModeConfig?) { activeConfiguration = config - UserDefaults.standard.set(config.id.uuidString, forKey: activeConfigIdKey) + UserDefaults.standard.set(config?.id.uuidString, forKey: activeConfigIdKey) self.objectWillChange.send() } - + // Get current active configuration - var currentActiveConfiguration: PowerModeConfig { - return activeConfiguration ?? defaultConfig + var currentActiveConfiguration: PowerModeConfig? { + return activeConfiguration } - - // Get all available configurations in order (default first, then custom configurations) + + // Get all available configurations in order func getAllAvailableConfigurations() -> [PowerModeConfig] { - return [defaultConfig] + configurations + return configurations } - + func isEmojiInUse(_ emoji: String) -> Bool { - if defaultConfig.emoji == emoji { - return true - } return configurations.contains { $0.emoji == emoji } } } \ No newline at end of file diff --git a/VoiceInk/PowerMode/PowerModeConfigView.swift b/VoiceInk/PowerMode/PowerModeConfigView.swift index bb65ee3..f2d5928 100644 --- a/VoiceInk/PowerMode/PowerModeConfigView.swift +++ b/VoiceInk/PowerMode/PowerModeConfigView.swift @@ -96,19 +96,6 @@ struct ConfigurationView: View { _isAutoSendEnabled = State(initialValue: latestConfig.isAutoSendEnabled) _selectedAIProvider = State(initialValue: latestConfig.selectedAIProvider) _selectedAIModel = State(initialValue: latestConfig.selectedAIModel) - case .editDefault(let config): - // Always use the latest default config - let latestConfig = powerModeManager.defaultConfig - _isAIEnhancementEnabled = State(initialValue: latestConfig.isAIEnhancementEnabled) - _selectedPromptId = State(initialValue: latestConfig.selectedPrompt.flatMap { UUID(uuidString: $0) }) - _selectedTranscriptionModelName = State(initialValue: latestConfig.selectedTranscriptionModelName) - _selectedLanguage = State(initialValue: latestConfig.selectedLanguage) - _configName = State(initialValue: latestConfig.name) - _selectedEmoji = State(initialValue: latestConfig.emoji) - _useScreenCapture = State(initialValue: latestConfig.useScreenCapture) - _isAutoSendEnabled = State(initialValue: latestConfig.isAutoSendEnabled) - _selectedAIProvider = State(initialValue: latestConfig.selectedAIProvider) - _selectedAIModel = State(initialValue: latestConfig.selectedAIModel) } } @@ -159,8 +146,6 @@ struct ConfigurationView: View { } } .buttonStyle(.plain) - .disabled(mode.isEditingDefault) - .opacity(mode.isEditingDefault ? 0.5 : 1) .popover(isPresented: $isShowingEmojiPicker, arrowEdge: .bottom) { EmojiPickerView( selectedEmoji: $selectedEmoji, @@ -173,12 +158,9 @@ struct ConfigurationView: View { .textFieldStyle(.plain) .foregroundColor(.primary) .tint(.accentColor) - .disabled(mode.isEditingDefault) .focused($isNameFieldFocused) .onAppear { - if !mode.isEditingDefault { - isNameFieldFocused = true - } + isNameFieldFocused = true } } .padding(.horizontal, 20) @@ -196,154 +178,152 @@ struct ConfigurationView: View { // } // SECTION 1: TRIGGERS - if !mode.isEditingDefault { - VStack(spacing: 16) { - // Section Header - SectionHeader(title: "When to Trigger") - - // Applications Subsection - VStack(alignment: .leading, spacing: 12) { - HStack { - Text("Applications") - .font(.subheadline) - .foregroundColor(.secondary) - - Spacer() - - Button(action: { - loadInstalledApps() - isShowingAppPicker = true - }) { - Label("Add App", systemImage: "plus.circle.fill") - .font(.subheadline) - } - .buttonStyle(.plain) - } - - if selectedAppConfigs.isEmpty { - HStack { - Spacer() - Text("No applications added") - .foregroundColor(.secondary) - .font(.subheadline) - Spacer() - } - .padding() - .background(CardBackground(isSelected: false)) - } else { - // Grid of selected apps that wraps to next line - LazyVGrid(columns: [GridItem(.adaptive(minimum: 50, maximum: 55), spacing: 10)], spacing: 10) { - ForEach(selectedAppConfigs) { appConfig in - VStack { - ZStack(alignment: .topTrailing) { - // App icon - completely filling the container - if let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: appConfig.bundleIdentifier) { - Image(nsImage: NSWorkspace.shared.icon(forFile: appURL.path)) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 50, height: 50) - .clipShape(RoundedRectangle(cornerRadius: 10)) - } else { - Image(systemName: "app.fill") - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 50, height: 50) - .clipShape(RoundedRectangle(cornerRadius: 10)) - } - - // Remove button - Button(action: { - selectedAppConfigs.removeAll(where: { $0.id == appConfig.id }) - }) { - Image(systemName: "xmark.circle.fill") - .font(.system(size: 14)) - .foregroundColor(.white) - .background(Circle().fill(Color.black.opacity(0.6))) - } - .buttonStyle(.plain) - .offset(x: 6, y: -6) - } - } - .frame(width: 50, height: 50) - .background(CardBackground(isSelected: false, cornerRadius: 10)) - } - } - } - } - - Divider() - - // Websites Subsection - VStack(alignment: .leading, spacing: 12) { - Text("Websites") + VStack(spacing: 16) { + // Section Header + SectionHeader(title: "When to Trigger") + + // Applications Subsection + VStack(alignment: .leading, spacing: 12) { + HStack { + Text("Applications") .font(.subheadline) .foregroundColor(.secondary) - - // Add URL Field - HStack { - TextField("Enter website URL (e.g., google.com)", text: $newWebsiteURL) - .textFieldStyle(.roundedBorder) - .onSubmit { - addWebsite() - } - - Button(action: addWebsite) { - Image(systemName: "plus.circle.fill") - .foregroundColor(.accentColor) - .font(.system(size: 18)) - } - .buttonStyle(.plain) - .disabled(newWebsiteURL.isEmpty) - } - if websiteConfigs.isEmpty { - HStack { - Spacer() - Text("No websites added") - .foregroundColor(.secondary) - .font(.subheadline) - Spacer() - } - .padding() - .background(CardBackground(isSelected: false)) - } else { - // Grid of website tags that wraps to next line - LazyVGrid(columns: [GridItem(.adaptive(minimum: 100, maximum: 160), spacing: 10)], spacing: 10) { - ForEach(websiteConfigs) { urlConfig in - HStack(spacing: 4) { - Image(systemName: "globe") - .font(.system(size: 11)) - .foregroundColor(.accentColor) - - Text(urlConfig.url) - .font(.system(size: 11)) - .lineLimit(1) - - Spacer(minLength: 0) + Spacer() + + Button(action: { + loadInstalledApps() + isShowingAppPicker = true + }) { + Label("Add App", systemImage: "plus.circle.fill") + .font(.subheadline) + } + .buttonStyle(.plain) + } + + if selectedAppConfigs.isEmpty { + HStack { + Spacer() + Text("No applications added") + .foregroundColor(.secondary) + .font(.subheadline) + Spacer() + } + .padding() + .background(CardBackground(isSelected: false)) + } else { + // Grid of selected apps that wraps to next line + LazyVGrid(columns: [GridItem(.adaptive(minimum: 50, maximum: 55), spacing: 10)], spacing: 10) { + ForEach(selectedAppConfigs) { appConfig in + VStack { + ZStack(alignment: .topTrailing) { + // App icon - completely filling the container + if let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: appConfig.bundleIdentifier) { + Image(nsImage: NSWorkspace.shared.icon(forFile: appURL.path)) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 50, height: 50) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } else { + Image(systemName: "app.fill") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 50, height: 50) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + // Remove button Button(action: { - websiteConfigs.removeAll(where: { $0.id == urlConfig.id }) + selectedAppConfigs.removeAll(where: { $0.id == appConfig.id }) }) { Image(systemName: "xmark.circle.fill") - .font(.system(size: 9)) - .foregroundColor(.secondary) + .font(.system(size: 14)) + .foregroundColor(.white) + .background(Circle().fill(Color.black.opacity(0.6))) } .buttonStyle(.plain) + .offset(x: 6, y: -6) } - .padding(.horizontal, 8) - .padding(.vertical, 6) - .frame(height: 28) - .background(CardBackground(isSelected: false, cornerRadius: 10)) } + .frame(width: 50, height: 50) + .background(CardBackground(isSelected: false, cornerRadius: 10)) } - .padding(8) } } } - .padding() - .background(CardBackground(isSelected: false)) - .padding(.horizontal) + + Divider() + + // Websites Subsection + VStack(alignment: .leading, spacing: 12) { + Text("Websites") + .font(.subheadline) + .foregroundColor(.secondary) + + // Add URL Field + HStack { + TextField("Enter website URL (e.g., google.com)", text: $newWebsiteURL) + .textFieldStyle(.roundedBorder) + .onSubmit { + addWebsite() + } + + Button(action: addWebsite) { + Image(systemName: "plus.circle.fill") + .foregroundColor(.accentColor) + .font(.system(size: 18)) + } + .buttonStyle(.plain) + .disabled(newWebsiteURL.isEmpty) + } + + if websiteConfigs.isEmpty { + HStack { + Spacer() + Text("No websites added") + .foregroundColor(.secondary) + .font(.subheadline) + Spacer() + } + .padding() + .background(CardBackground(isSelected: false)) + } else { + // Grid of website tags that wraps to next line + LazyVGrid(columns: [GridItem(.adaptive(minimum: 100, maximum: 160), spacing: 10)], spacing: 10) { + ForEach(websiteConfigs) { urlConfig in + HStack(spacing: 4) { + Image(systemName: "globe") + .font(.system(size: 11)) + .foregroundColor(.accentColor) + + Text(urlConfig.url) + .font(.system(size: 11)) + .lineLimit(1) + + Spacer(minLength: 0) + + Button(action: { + websiteConfigs.removeAll(where: { $0.id == urlConfig.id }) + }) { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 9)) + .foregroundColor(.secondary) + } + .buttonStyle(.plain) + } + .padding(.horizontal, 8) + .padding(.vertical, 6) + .frame(height: 28) + .background(CardBackground(isSelected: false, cornerRadius: 10)) + } + } + .padding(8) + } + } } + .padding() + .background(CardBackground(isSelected: false)) + .padding(.horizontal) // SECTION 2: TRANSCRIPTION VStack(spacing: 16) { @@ -713,20 +693,6 @@ struct ConfigurationView: View { updatedConfig.selectedAIProvider = selectedAIProvider updatedConfig.selectedAIModel = selectedAIModel return updatedConfig - - case .editDefault(let config): - var updatedConfig = config - updatedConfig.name = configName - updatedConfig.emoji = selectedEmoji - updatedConfig.isAIEnhancementEnabled = isAIEnhancementEnabled - updatedConfig.selectedPrompt = selectedPromptId?.uuidString - updatedConfig.selectedTranscriptionModelName = selectedTranscriptionModelName - updatedConfig.selectedLanguage = selectedLanguage - updatedConfig.useScreenCapture = useScreenCapture - updatedConfig.isAutoSendEnabled = isAutoSendEnabled - updatedConfig.selectedAIProvider = selectedAIProvider - updatedConfig.selectedAIModel = selectedAIModel - return updatedConfig } } @@ -790,7 +756,7 @@ struct ConfigurationView: View { switch mode { case .add: powerModeManager.addConfiguration(config) - case .edit, .editDefault: + case .edit: powerModeManager.updateConfiguration(config) } diff --git a/VoiceInk/PowerMode/PowerModePopover.swift b/VoiceInk/PowerMode/PowerModePopover.swift index 7baa14c..c2d211d 100644 --- a/VoiceInk/PowerMode/PowerModePopover.swift +++ b/VoiceInk/PowerMode/PowerModePopover.swift @@ -18,18 +18,28 @@ struct PowerModePopover: View { ScrollView { VStack(alignment: .leading, spacing: 4) { - // Default Configuration - PowerModeRow( - config: powerModeManager.defaultConfig, - isSelected: selectedConfig?.id == powerModeManager.defaultConfig.id, - action: { - powerModeManager.setActiveConfiguration(powerModeManager.defaultConfig) - selectedConfig = powerModeManager.defaultConfig - // Apply configuration immediately - applySelectedConfiguration() + // "Disable" option if a power mode is active + if powerModeManager.activeConfiguration != nil { + Button(action: { + powerModeManager.setActiveConfiguration(nil) + selectedConfig = nil + // Here we might want to revert to a default state, + // but for now, we'll just deactivate the power mode. + }) { + HStack { + Text("Disable Power Mode") + .foregroundColor(.red.opacity(0.9)) + .font(.system(size: 13)) + Spacer() + Image(systemName: "xmark.circle.fill") + .foregroundColor(.red.opacity(0.9)) + } + .padding(.vertical, 4) + .padding(.horizontal, 8) } - ) - + .buttonStyle(.plain) + } + // Custom Configurations ForEach(powerModeManager.configurations) { config in PowerModeRow( diff --git a/VoiceInk/PowerMode/PowerModeValidator.swift b/VoiceInk/PowerMode/PowerModeValidator.swift index 4a1c1e0..a8f863b 100644 --- a/VoiceInk/PowerMode/PowerModeValidator.swift +++ b/VoiceInk/PowerMode/PowerModeValidator.swift @@ -64,43 +64,41 @@ struct PowerModeValidator { errors.append(.duplicateName(config.name)) } - // For non-default modes, check that there's at least one trigger - if !mode.isEditingDefault { - if (config.appConfigs == nil || config.appConfigs?.isEmpty == true) && - (config.urlConfigs == nil || config.urlConfigs?.isEmpty == true) { - errors.append(.noTriggers) - } - - // Check for duplicate app configurations - if let appConfigs = config.appConfigs { - for appConfig in appConfigs { - for existingConfig in powerModeManager.configurations { - // Skip checking against itself when editing - if case .edit(let editConfig) = mode, existingConfig.id == editConfig.id { - continue - } - - if let existingAppConfigs = existingConfig.appConfigs, - existingAppConfigs.contains(where: { $0.bundleIdentifier == appConfig.bundleIdentifier }) { - errors.append(.duplicateAppTrigger(appConfig.appName, existingConfig.name)) - } + // For all modes, check that there's at least one trigger + if (config.appConfigs == nil || config.appConfigs?.isEmpty == true) && + (config.urlConfigs == nil || config.urlConfigs?.isEmpty == true) { + errors.append(.noTriggers) + } + + // Check for duplicate app configurations + if let appConfigs = config.appConfigs { + for appConfig in appConfigs { + for existingConfig in powerModeManager.configurations { + // Skip checking against itself when editing + if case .edit(let editConfig) = mode, existingConfig.id == editConfig.id { + continue + } + + if let existingAppConfigs = existingConfig.appConfigs, + existingAppConfigs.contains(where: { $0.bundleIdentifier == appConfig.bundleIdentifier }) { + errors.append(.duplicateAppTrigger(appConfig.appName, existingConfig.name)) } } } - - // Check for duplicate website configurations - if let urlConfigs = config.urlConfigs { - for urlConfig in urlConfigs { - for existingConfig in powerModeManager.configurations { - // Skip checking against itself when editing - if case .edit(let editConfig) = mode, existingConfig.id == editConfig.id { - continue - } - - if let existingUrlConfigs = existingConfig.urlConfigs, - existingUrlConfigs.contains(where: { $0.url == urlConfig.url }) { - errors.append(.duplicateWebsiteTrigger(urlConfig.url, existingConfig.name)) - } + } + + // Check for duplicate website configurations + if let urlConfigs = config.urlConfigs { + for urlConfig in urlConfigs { + for existingConfig in powerModeManager.configurations { + // Skip checking against itself when editing + if case .edit(let editConfig) = mode, existingConfig.id == editConfig.id { + continue + } + + if let existingUrlConfigs = existingConfig.urlConfigs, + existingUrlConfigs.contains(where: { $0.url == urlConfig.url }) { + errors.append(.duplicateWebsiteTrigger(urlConfig.url, existingConfig.name)) } } } diff --git a/VoiceInk/PowerMode/PowerModeView.swift b/VoiceInk/PowerMode/PowerModeView.swift index 8c3654f..669a068 100644 --- a/VoiceInk/PowerMode/PowerModeView.swift +++ b/VoiceInk/PowerMode/PowerModeView.swift @@ -17,22 +17,15 @@ extension View { enum ConfigurationMode: Hashable { case add case edit(PowerModeConfig) - case editDefault(PowerModeConfig) var isAdding: Bool { if case .add = self { return true } return false } - var isEditingDefault: Bool { - if case .editDefault = self { return true } - return false - } - var title: String { switch self { case .add: return "Add Power Mode" - case .editDefault: return "Edit Default Power Mode" case .edit: return "Edit Power Mode" } } @@ -45,9 +38,6 @@ enum ConfigurationMode: Hashable { case .edit(let config): hasher.combine(1) // Use a unique value for edit hasher.combine(config.id) - case .editDefault(let config): - hasher.combine(2) // Use a unique value for editDefault - hasher.combine(config.id) } } @@ -58,8 +48,6 @@ enum ConfigurationMode: Hashable { return true case (.edit(let lhsConfig), .edit(let rhsConfig)): return lhsConfig.id == rhsConfig.id - case (.editDefault(let lhsConfig), .editDefault(let rhsConfig)): - return lhsConfig.id == rhsConfig.id default: return false } @@ -100,14 +88,7 @@ struct PowerModeView: View { ) Spacer() - - Toggle("", isOn: $powerModeManager.isPowerModeEnabled) - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .labelsHidden() - .scaleEffect(1.2) - .onChange(of: powerModeManager.isPowerModeEnabled) { oldValue, newValue in - powerModeManager.savePowerModeEnabled() - } + } } Text("Automatically apply custom configurations based on the app/website you are using") @@ -117,125 +98,68 @@ struct PowerModeView: View { .padding(.horizontal) .padding(.top, 8) - if powerModeManager.isPowerModeEnabled { - // Configurations Container - VStack(spacing: 0) { - // Default Configuration Section - VStack(alignment: .leading, spacing: 16) { - Text("Default Power Mode") - .font(.headline) - .foregroundColor(.primary) - .padding(.horizontal) - .padding(.top, 16) - - ConfigurationRow( - config: powerModeManager.defaultConfig, - isEditing: false, - isDefault: true, - action: { - configurationMode = .editDefault(powerModeManager.defaultConfig) - navigationPath.append(configurationMode!) - } - ) + // Configurations Container + VStack(spacing: 0) { + // Custom Configurations Section + VStack(alignment: .leading, spacing: 16) { + Text("Custom Power Modes") + .font(.headline) + .foregroundColor(.primary) .padding(.horizontal) - } - // Divider between sections - Divider() - .padding(.vertical, 16) - - // Custom Configurations Section - VStack(alignment: .leading, spacing: 16) { - Text("Custom Power Modes") - .font(.headline) - .foregroundColor(.primary) - .padding(.horizontal) - - if powerModeManager.configurations.isEmpty { - VStack(spacing: 20) { - Image(systemName: "square.grid.2x2") - .font(.system(size: 36)) - .foregroundColor(.secondary) - - Text("No custom power modes") - .font(.title3) - .fontWeight(.medium) - - Text("Create a new mode for specific apps/websites") - .font(.subheadline) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - } - .frame(maxWidth: .infinity) - .padding(.vertical, 30) - } else { - PowerModeConfigurationsGrid( - powerModeManager: powerModeManager, - onEditConfig: { config in - configurationMode = .edit(config) - navigationPath.append(configurationMode!) - } - ) + if powerModeManager.configurations.isEmpty { + VStack(spacing: 20) { + Image(systemName: "square.grid.2x2") + .font(.system(size: 36)) + .foregroundColor(.secondary) + + Text("No power modes configured") + .font(.title3) + .fontWeight(.medium) + + Text("Create a new power mode to get started.") + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) } - } - - Spacer(minLength: 24) - - // Add Configuration button at the bottom (centered) - HStack { - VoiceInkButton( - title: "Add New Power Mode", - action: { - configurationMode = .add + .frame(maxWidth: .infinity) + .padding(.vertical, 30) + } else { + PowerModeConfigurationsGrid( + powerModeManager: powerModeManager, + onEditConfig: { config in + configurationMode = .edit(config) navigationPath.append(configurationMode!) } ) } - .padding(.horizontal) - .padding(.bottom, 16) } - .background( - RoundedRectangle(cornerRadius: 16) - .fill(Color(NSColor.controlBackgroundColor)) - ) - .overlay( - RoundedRectangle(cornerRadius: 16) - .stroke(Color(NSColor.separatorColor), lineWidth: 1) - ) - .shadow(color: Color(NSColor.shadowColor).opacity(0.05), radius: 5, y: 2) - .padding(.horizontal) - } else { - // Disabled state - VStack(spacing: 24) { - Image(systemName: "bolt.slash.circle") - .font(.system(size: 56)) - .foregroundColor(.secondary) - - Text("Power Mode is disabled") - .font(.title2) - .fontWeight(.semibold) - - Text("Enable Power Mode to create context-specific configurations that automatically apply based on your current app or website.") - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - .frame(maxWidth: 400) - + + Spacer(minLength: 24) + + // Add Configuration button at the bottom (centered) + HStack { VoiceInkButton( - title: "Enable Power Mode", + title: "Add New Power Mode", action: { - powerModeManager.isPowerModeEnabled = true - powerModeManager.savePowerModeEnabled() + configurationMode = .add + navigationPath.append(configurationMode!) } ) - .frame(maxWidth: .infinity) } - .frame(maxWidth: .infinity) - .padding(40) - .background(Color(NSColor.controlBackgroundColor)) - .cornerRadius(16) - .shadow(color: Color(NSColor.shadowColor).opacity(0.05), radius: 5, y: 2) .padding(.horizontal) + .padding(.bottom, 16) } + .background( + RoundedRectangle(cornerRadius: 16) + .fill(Color(NSColor.controlBackgroundColor)) + ) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(Color(NSColor.separatorColor), lineWidth: 1) + ) + .shadow(color: Color(NSColor.shadowColor).opacity(0.05), radius: 5, y: 2) + .padding(.horizontal) } .padding(.bottom, 24) } @@ -246,7 +170,7 @@ struct PowerModeView: View { } } } -} + // New component for section headers diff --git a/VoiceInk/PowerMode/PowerModeViewComponents.swift b/VoiceInk/PowerMode/PowerModeViewComponents.swift index 9ac35fb..27ac6e0 100644 --- a/VoiceInk/PowerMode/PowerModeViewComponents.swift +++ b/VoiceInk/PowerMode/PowerModeViewComponents.swift @@ -63,6 +63,7 @@ struct PowerModeConfigurationsGrid: View { config: config, isEditing: false, isDefault: false, + powerModeManager: powerModeManager, action: { onEditConfig(config) } @@ -86,9 +87,10 @@ struct PowerModeConfigurationsGrid: View { } struct ConfigurationRow: View { - let config: PowerModeConfig + @State var config: PowerModeConfig let isEditing: Bool let isDefault: Bool + let powerModeManager: PowerModeManager let action: () -> Void @EnvironmentObject var enhancementService: AIEnhancementService @EnvironmentObject var whisperState: WhisperState @@ -148,155 +150,99 @@ struct ConfigurationRow: View { } var body: some View { - Button(action: action) { - VStack(spacing: 0) { - // Top row: Emoji, Name, and App/Website counts - HStack(spacing: 12) { - // Left: Emoji/Icon - ZStack { - Circle() - .fill(isDefault ? Color.accentColor.opacity(0.15) : Color(.controlBackgroundColor)) - .frame(width: 40, height: 40) - - if isDefault { - Image(systemName: "gearshape.fill") - .font(.system(size: 18)) - .foregroundColor(.accentColor) - } else { - Text(config.emoji) - .font(.system(size: 20)) - } - } - - // Middle: Name and badge - VStack(alignment: .leading, spacing: 3) { - HStack(spacing: 6) { - Text(config.name) - .font(.system(size: 15, weight: .semibold)) + HStack { + Button(action: action) { + VStack(spacing: 0) { + // Top row: Emoji, Name, and App/Website counts + HStack(spacing: 12) { + // Left: Emoji/Icon + ZStack { + Circle() + .fill(isDefault ? Color.accentColor.opacity(0.15) : Color(.controlBackgroundColor)) + .frame(width: 40, height: 40) if isDefault { - Text("Default") - .font(.system(size: 10, weight: .medium)) - .padding(.horizontal, 5) - .padding(.vertical, 2) - .background(Capsule().fill(Color.accentColor.opacity(0.15))) + Image(systemName: "gearshape.fill") + .font(.system(size: 18)) .foregroundColor(.accentColor) + } else { + Text(config.emoji) + .font(.system(size: 20)) } } - if isDefault { - Text("Fallback power mode") - .font(.caption2) - .foregroundColor(.secondary) - } - } - - Spacer() - - // Right: App Icons and Website Count - if !isDefault { - HStack(alignment: .center, spacing: 6) { - // App Count - if appCount > 0 { - HStack(spacing: 3) { - Text(appText) - .font(.caption) - .foregroundColor(.secondary) - - Image(systemName: "app.fill") - .font(.system(size: 9)) - .foregroundColor(.secondary) + // Middle: Name and badge + VStack(alignment: .leading, spacing: 3) { + HStack(spacing: 6) { + Text(config.name) + .font(.system(size: 15, weight: .semibold)) + + if isDefault { + Text("Default") + .font(.system(size: 10, weight: .medium)) + .padding(.horizontal, 5) + .padding(.vertical, 2) + .background(Capsule().fill(Color.accentColor.opacity(0.15))) + .foregroundColor(.accentColor) } } - // Website Count - if websiteCount > 0 { - HStack(spacing: 3) { - Text(websiteText) - .font(.caption) - .foregroundColor(.secondary) - - Image(systemName: "globe") - .font(.system(size: 9)) - .foregroundColor(.secondary) + if isDefault { + Text("Fallback power mode") + .font(.caption2) + .foregroundColor(.secondary) + } + } + + Spacer() + + // Right: App Icons and Website Count + if !isDefault { + HStack(alignment: .center, spacing: 6) { + // App Count + if appCount > 0 { + HStack(spacing: 3) { + Text(appText) + .font(.caption) + .foregroundColor(.secondary) + + Image(systemName: "app.fill") + .font(.system(size: 9)) + .foregroundColor(.secondary) + } + } + + // Website Count + if websiteCount > 0 { + HStack(spacing: 3) { + Text(websiteText) + .font(.caption) + .foregroundColor(.secondary) + + Image(systemName: "globe") + .font(.system(size: 9)) + .foregroundColor(.secondary) + } } } } } - } - .padding(.vertical, 12) - .padding(.horizontal, 14) - - // Only add divider and settings row if we have settings - if selectedModel != nil || selectedLanguage != nil || config.isAIEnhancementEnabled { - Divider() - .padding(.horizontal, 16) + .padding(.vertical, 12) + .padding(.horizontal, 14) - // Settings badges in specified order - HStack(spacing: 8) { - // 1. Voice Model badge - if let model = selectedModel, model != "Default" { - HStack(spacing: 4) { - Image(systemName: "waveform") - .font(.system(size: 10)) - Text(model) - .font(.caption) - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Capsule() - .fill(Color(.controlBackgroundColor))) - .overlay( - Capsule() - .stroke(Color(.separatorColor), lineWidth: 0.5) - ) - } + // Only add divider and settings row if we have settings + if selectedModel != nil || selectedLanguage != nil || config.isAIEnhancementEnabled { + Divider() + .padding(.horizontal, 16) - // 2. Language badge - if let language = selectedLanguage, language != "Default" { - HStack(spacing: 4) { - Image(systemName: "globe") - .font(.system(size: 10)) - Text(language) - .font(.caption) - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Capsule() - .fill(Color(.controlBackgroundColor))) - .overlay( - Capsule() - .stroke(Color(.separatorColor), lineWidth: 0.5) - ) - } - - // 3. AI Model badge if specified (moved before AI Enhancement) - if config.isAIEnhancementEnabled, let modelName = config.selectedAIModel, !modelName.isEmpty { - HStack(spacing: 4) { - Image(systemName: "cpu") - .font(.system(size: 10)) - // Display a shortened version of the model name if it's too long (increased limit) - Text(modelName.count > 20 ? String(modelName.prefix(18)) + "..." : modelName) - .font(.caption) - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Capsule() - .fill(Color(.controlBackgroundColor))) - .overlay( - Capsule() - .stroke(Color(.separatorColor), lineWidth: 0.5) - ) - } - - // 4. AI Enhancement badge - if config.isAIEnhancementEnabled { - // Context Awareness badge (moved before AI Enhancement) - if config.useScreenCapture { + // Settings badges in specified order + HStack(spacing: 8) { + // 1. Voice Model badge + if let model = selectedModel, model != "Default" { HStack(spacing: 4) { - Image(systemName: "camera.viewfinder") + Image(systemName: "waveform") .font(.system(size: 10)) - Text("Context Awareness") + Text(model) .font(.caption) } .padding(.horizontal, 8) @@ -309,28 +255,98 @@ struct ConfigurationRow: View { ) } - HStack(spacing: 4) { - Image(systemName: "sparkles") - .font(.system(size: 10)) - Text(selectedPrompt?.title ?? "AI") - .font(.caption) + // 2. Language badge + if let language = selectedLanguage, language != "Default" { + HStack(spacing: 4) { + Image(systemName: "globe") + .font(.system(size: 10)) + Text(language) + .font(.caption) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Capsule() + .fill(Color(.controlBackgroundColor))) + .overlay( + Capsule() + .stroke(Color(.separatorColor), lineWidth: 0.5) + ) } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Capsule() - .fill(Color.accentColor.opacity(0.1))) - .foregroundColor(.accentColor) + + // 3. AI Model badge if specified (moved before AI Enhancement) + if config.isAIEnhancementEnabled, let modelName = config.selectedAIModel, !modelName.isEmpty { + HStack(spacing: 4) { + Image(systemName: "cpu") + .font(.system(size: 10)) + // Display a shortened version of the model name if it's too long (increased limit) + Text(modelName.count > 20 ? String(modelName.prefix(18)) + "..." : modelName) + .font(.caption) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Capsule() + .fill(Color(.controlBackgroundColor))) + .overlay( + Capsule() + .stroke(Color(.separatorColor), lineWidth: 0.5) + ) + } + + // 4. AI Enhancement badge + if config.isAIEnhancementEnabled { + // Context Awareness badge (moved before AI Enhancement) + if config.useScreenCapture { + HStack(spacing: 4) { + Image(systemName: "camera.viewfinder") + .font(.system(size: 10)) + Text("Context Awareness") + .font(.caption) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Capsule() + .fill(Color(.controlBackgroundColor))) + .overlay( + Capsule() + .stroke(Color(.separatorColor), lineWidth: 0.5) + ) + } + + HStack(spacing: 4) { + Image(systemName: "sparkles") + .font(.system(size: 10)) + Text(selectedPrompt?.title ?? "AI") + .font(.caption) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Capsule() + .fill(Color.accentColor.opacity(0.1))) + .foregroundColor(.accentColor) + } + + Spacer() } - - Spacer() + .padding(.vertical, 10) + .padding(.horizontal, 16) } - .padding(.vertical, 10) - .padding(.horizontal, 16) } + .background(CardBackground(isSelected: isEditing)) } - .background(CardBackground(isSelected: isEditing)) + .buttonStyle(.plain) + + Toggle("", isOn: $config.isEnabled) + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .labelsHidden() + .onChange(of: config.isEnabled) { oldValue, newValue in + if newValue { + powerModeManager.enableConfiguration(with: config.id) + } else { + powerModeManager.disableConfiguration(with: config.id) + } + } } - .buttonStyle(.plain) + .opacity(config.isEnabled ? 1.0 : 0.5) } private var isSelected: Bool { diff --git a/VoiceInk/Services/ImportExportService.swift b/VoiceInk/Services/ImportExportService.swift index e464e5d..39c7862 100644 --- a/VoiceInk/Services/ImportExportService.swift +++ b/VoiceInk/Services/ImportExportService.swift @@ -26,7 +26,6 @@ struct VoiceInkExportedSettings: Codable { let version: String let customPrompts: [CustomPrompt] let powerModeConfigs: [PowerModeConfig] - let defaultPowerModeConfig: PowerModeConfig let dictionaryItems: [DictionaryItem]? let wordReplacements: [String: String]? let generalSettings: GeneralSettings? @@ -67,7 +66,6 @@ class ImportExportService { let exportablePrompts = enhancementService.customPrompts.filter { !$0.isPredefined } let powerConfigs = powerModeManager.configurations - let defaultPowerConfig = powerModeManager.defaultConfig // Export custom models let customModels = CustomModelManager.shared.customModels @@ -102,7 +100,6 @@ class ImportExportService { version: currentSettingsVersion, customPrompts: exportablePrompts, powerModeConfigs: powerConfigs, - defaultPowerModeConfig: defaultPowerConfig, dictionaryItems: exportedDictionaryItems, wordReplacements: exportedWordReplacements, generalSettings: generalSettingsToExport, @@ -172,9 +169,7 @@ class ImportExportService { let powerModeManager = PowerModeManager.shared powerModeManager.configurations = importedSettings.powerModeConfigs - powerModeManager.defaultConfig = importedSettings.defaultPowerModeConfig powerModeManager.saveConfigurations() - powerModeManager.updateConfiguration(powerModeManager.defaultConfig) // Import Custom Models if let modelsToImport = importedSettings.customCloudModels { diff --git a/VoiceInk/Views/Recorder/MiniRecorderView.swift b/VoiceInk/Views/Recorder/MiniRecorderView.swift index 60ca62d..4241a62 100644 --- a/VoiceInk/Views/Recorder/MiniRecorderView.swift +++ b/VoiceInk/Views/Recorder/MiniRecorderView.swift @@ -36,10 +36,10 @@ struct MiniRecorderView: View { private var rightButton: some View { Group { - if powerModeManager.isPowerModeEnabled { + if !powerModeManager.enabledConfigurations.isEmpty { RecorderToggleButton( - isEnabled: powerModeManager.isPowerModeEnabled, - icon: powerModeManager.currentActiveConfiguration.emoji, + isEnabled: !powerModeManager.enabledConfigurations.isEmpty, + icon: powerModeManager.currentActiveConfiguration?.emoji ?? "⚙️", color: .orange, disabled: false ) { diff --git a/VoiceInk/Views/Recorder/NotchRecorderView.swift b/VoiceInk/Views/Recorder/NotchRecorderView.swift index d0ca24e..16703f6 100644 --- a/VoiceInk/Views/Recorder/NotchRecorderView.swift +++ b/VoiceInk/Views/Recorder/NotchRecorderView.swift @@ -54,10 +54,10 @@ struct NotchRecorderView: View { private var rightToggleButton: some View { Group { - if powerModeManager.isPowerModeEnabled { + if !powerModeManager.enabledConfigurations.isEmpty { RecorderToggleButton( - isEnabled: powerModeManager.isPowerModeEnabled, - icon: powerModeManager.currentActiveConfiguration.emoji, + isEnabled: !powerModeManager.enabledConfigurations.isEmpty, + icon: powerModeManager.currentActiveConfiguration?.emoji ?? "⚙️", color: .orange, disabled: false ) { diff --git a/VoiceInk/Whisper/WhisperState.swift b/VoiceInk/Whisper/WhisperState.swift index 7008860..ae6f3c6 100644 --- a/VoiceInk/Whisper/WhisperState.swift +++ b/VoiceInk/Whisper/WhisperState.swift @@ -351,9 +351,8 @@ class WhisperState: NSObject, ObservableObject { CursorPaster.pasteAtCursor(text, shouldPreserveClipboard: true) - // Automatically press Enter if the active Power Mode configuration allows it. let powerMode = PowerModeManager.shared - if powerMode.isPowerModeEnabled && powerMode.currentActiveConfiguration.isAutoSendEnabled { + if let activeConfig = powerMode.currentActiveConfiguration, activeConfig.isAutoSendEnabled { // Slight delay to ensure the paste operation completes DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { CursorPaster.pressEnter()