import Foundation import AppKit import UniformTypeIdentifiers import KeyboardShortcuts import LaunchAtLogin struct GeneralSettings: Codable { let toggleMiniRecorderShortcut: KeyboardShortcuts.Shortcut? let toggleMiniRecorderShortcut2: KeyboardShortcuts.Shortcut? let selectedHotkey1RawValue: String? let selectedHotkey2RawValue: String? let launchAtLoginEnabled: Bool? let isMenuBarOnly: Bool? let useAppleScriptPaste: Bool? let recorderType: String? let doNotMaintainTranscriptHistory: Bool? let isAudioCleanupEnabled: Bool? let audioRetentionPeriod: Int? let isSoundFeedbackEnabled: Bool? let isSystemMuteEnabled: Bool? let isPauseMediaEnabled: Bool? let isTextFormattingEnabled: Bool? } struct VoiceInkExportedSettings: Codable { let version: String let customPrompts: [CustomPrompt] let powerModeConfigs: [PowerModeConfig] let dictionaryItems: [DictionaryItem]? let wordReplacements: [String: String]? let generalSettings: GeneralSettings? let customEmojis: [String]? let customCloudModels: [CustomCloudModel]? } class ImportExportService { static let shared = ImportExportService() private let currentSettingsVersion: String private let dictionaryItemsKey = "CustomDictionaryItems" private let wordReplacementsKey = "wordReplacements" private let keyIsMenuBarOnly = "IsMenuBarOnly" private let keyUseAppleScriptPaste = "UseAppleScriptPaste" private let keyRecorderType = "RecorderType" private let keyDoNotMaintainTranscriptHistory = "DoNotMaintainTranscriptHistory" private let keyIsAudioCleanupEnabled = "IsAudioCleanupEnabled" private let keyAudioRetentionPeriod = "AudioRetentionPeriod" private let keyIsSoundFeedbackEnabled = "isSoundFeedbackEnabled" private let keyIsSystemMuteEnabled = "isSystemMuteEnabled" private let keyIsTextFormattingEnabled = "IsTextFormattingEnabled" private init() { if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String { self.currentSettingsVersion = version } else { self.currentSettingsVersion = "0.0.0" } } @MainActor func exportSettings(enhancementService: AIEnhancementService, whisperPrompt: WhisperPrompt, hotkeyManager: HotkeyManager, menuBarManager: MenuBarManager, mediaController: MediaController, playbackController: PlaybackController, soundManager: SoundManager, whisperState: WhisperState) { let powerModeManager = PowerModeManager.shared let emojiManager = EmojiManager.shared let exportablePrompts = enhancementService.customPrompts.filter { !$0.isPredefined } let powerConfigs = powerModeManager.configurations // Export custom models let customModels = CustomModelManager.shared.customModels var exportedDictionaryItems: [DictionaryItem]? = nil if let data = UserDefaults.standard.data(forKey: dictionaryItemsKey), let items = try? JSONDecoder().decode([DictionaryItem].self, from: data) { exportedDictionaryItems = items } let exportedWordReplacements = UserDefaults.standard.dictionary(forKey: wordReplacementsKey) as? [String: String] let generalSettingsToExport = GeneralSettings( toggleMiniRecorderShortcut: KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder), toggleMiniRecorderShortcut2: KeyboardShortcuts.getShortcut(for: .toggleMiniRecorder2), selectedHotkey1RawValue: hotkeyManager.selectedHotkey1.rawValue, selectedHotkey2RawValue: hotkeyManager.selectedHotkey2.rawValue, launchAtLoginEnabled: LaunchAtLogin.isEnabled, isMenuBarOnly: menuBarManager.isMenuBarOnly, useAppleScriptPaste: UserDefaults.standard.bool(forKey: keyUseAppleScriptPaste), recorderType: whisperState.recorderType, doNotMaintainTranscriptHistory: UserDefaults.standard.bool(forKey: keyDoNotMaintainTranscriptHistory), isAudioCleanupEnabled: UserDefaults.standard.bool(forKey: keyIsAudioCleanupEnabled), audioRetentionPeriod: UserDefaults.standard.integer(forKey: keyAudioRetentionPeriod), isSoundFeedbackEnabled: soundManager.isEnabled, isSystemMuteEnabled: mediaController.isSystemMuteEnabled, isPauseMediaEnabled: playbackController.isPauseMediaEnabled, isTextFormattingEnabled: UserDefaults.standard.object(forKey: keyIsTextFormattingEnabled) as? Bool ?? true ) let exportedSettings = VoiceInkExportedSettings( version: currentSettingsVersion, customPrompts: exportablePrompts, powerModeConfigs: powerConfigs, dictionaryItems: exportedDictionaryItems, wordReplacements: exportedWordReplacements, generalSettings: generalSettingsToExport, customEmojis: emojiManager.customEmojis, customCloudModels: customModels ) let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted do { let jsonData = try encoder.encode(exportedSettings) let savePanel = NSSavePanel() savePanel.allowedContentTypes = [UTType.json] savePanel.nameFieldStringValue = "VoiceInk_Settings_Backup.json" savePanel.title = "Export VoiceInk Settings" savePanel.message = "Choose a location to save your settings." DispatchQueue.main.async { if savePanel.runModal() == .OK { if let url = savePanel.url { do { try jsonData.write(to: url) self.showAlert(title: "Export Successful", message: "Your settings have been successfully exported to \(url.lastPathComponent).") } catch { self.showAlert(title: "Export Error", message: "Could not save settings to file: \(error.localizedDescription)") } } } else { self.showAlert(title: "Export Canceled", message: "The settings export operation was canceled.") } } } catch { self.showAlert(title: "Export Error", message: "Could not encode settings to JSON: \(error.localizedDescription)") } } @MainActor func importSettings(enhancementService: AIEnhancementService, whisperPrompt: WhisperPrompt, hotkeyManager: HotkeyManager, menuBarManager: MenuBarManager, mediaController: MediaController, playbackController: PlaybackController, soundManager: SoundManager, whisperState: WhisperState) { let openPanel = NSOpenPanel() openPanel.allowedContentTypes = [UTType.json] openPanel.canChooseFiles = true openPanel.canChooseDirectories = false openPanel.allowsMultipleSelection = false openPanel.title = "Import VoiceInk Settings" openPanel.message = "Choose a settings file to import. This will overwrite ALL settings (prompts, power modes, dictionary, general app settings)." DispatchQueue.main.async { if openPanel.runModal() == .OK { guard let url = openPanel.url else { self.showAlert(title: "Import Error", message: "Could not get the file URL from the open panel.") return } do { let jsonData = try Data(contentsOf: url) let decoder = JSONDecoder() let importedSettings = try decoder.decode(VoiceInkExportedSettings.self, from: jsonData) if importedSettings.version != self.currentSettingsVersion { self.showAlert(title: "Version Mismatch", message: "The imported settings file (version \(importedSettings.version)) is from a different version than your application (version \(self.currentSettingsVersion)). Proceeding with import, but be aware of potential incompatibilities.") } let predefinedPrompts = enhancementService.customPrompts.filter { $0.isPredefined } enhancementService.customPrompts = predefinedPrompts + importedSettings.customPrompts let powerModeManager = PowerModeManager.shared powerModeManager.configurations = importedSettings.powerModeConfigs powerModeManager.saveConfigurations() // Import Custom Models if let modelsToImport = importedSettings.customCloudModels { let customModelManager = CustomModelManager.shared customModelManager.customModels = modelsToImport customModelManager.saveCustomModels() // Ensure they are persisted whisperState.refreshAllAvailableModels() // Refresh the UI print("Successfully imported \(modelsToImport.count) custom models.") } else { print("No custom models found in the imported file.") } if let customEmojis = importedSettings.customEmojis { let emojiManager = EmojiManager.shared for emoji in customEmojis { _ = emojiManager.addCustomEmoji(emoji) } } if let itemsToImport = importedSettings.dictionaryItems { Task { await whisperPrompt.saveDictionaryItems(itemsToImport) } } else { print("No dictionary items (for spelling) found in the imported file. Existing items remain unchanged.") } if let replacementsToImport = importedSettings.wordReplacements { UserDefaults.standard.set(replacementsToImport, forKey: self.wordReplacementsKey) } else { print("No word replacements found in the imported file. Existing replacements remain unchanged.") } if let general = importedSettings.generalSettings { if let shortcut = general.toggleMiniRecorderShortcut { KeyboardShortcuts.setShortcut(shortcut, for: .toggleMiniRecorder) } if let shortcut2 = general.toggleMiniRecorderShortcut2 { KeyboardShortcuts.setShortcut(shortcut2, for: .toggleMiniRecorder2) } if let hotkeyRaw = general.selectedHotkey1RawValue, let hotkey = HotkeyManager.HotkeyOption(rawValue: hotkeyRaw) { hotkeyManager.selectedHotkey1 = hotkey } if let hotkeyRaw2 = general.selectedHotkey2RawValue, let hotkey2 = HotkeyManager.HotkeyOption(rawValue: hotkeyRaw2) { hotkeyManager.selectedHotkey2 = hotkey2 } if let launch = general.launchAtLoginEnabled { LaunchAtLogin.isEnabled = launch } if let menuOnly = general.isMenuBarOnly { menuBarManager.isMenuBarOnly = menuOnly } if let appleScriptPaste = general.useAppleScriptPaste { UserDefaults.standard.set(appleScriptPaste, forKey: self.keyUseAppleScriptPaste) } if let recType = general.recorderType { whisperState.recorderType = recType } if let doNotMaintainHistory = general.doNotMaintainTranscriptHistory { UserDefaults.standard.set(doNotMaintainHistory, forKey: self.keyDoNotMaintainTranscriptHistory) } if let audioCleanup = general.isAudioCleanupEnabled { UserDefaults.standard.set(audioCleanup, forKey: self.keyIsAudioCleanupEnabled) } if let audioRetention = general.audioRetentionPeriod { UserDefaults.standard.set(audioRetention, forKey: self.keyAudioRetentionPeriod) } if let soundFeedback = general.isSoundFeedbackEnabled { soundManager.isEnabled = soundFeedback } if let muteSystem = general.isSystemMuteEnabled { mediaController.isSystemMuteEnabled = muteSystem } if let pauseMedia = general.isPauseMediaEnabled { playbackController.isPauseMediaEnabled = pauseMedia } if let textFormattingEnabled = general.isTextFormattingEnabled { UserDefaults.standard.set(textFormattingEnabled, forKey: self.keyIsTextFormattingEnabled) } } self.showRestartAlert(message: "Settings imported successfully from \(url.lastPathComponent). All settings (including general app settings) have been applied.") } catch { self.showAlert(title: "Import Error", message: "Error importing settings: \(error.localizedDescription). The file might be corrupted or not in the correct format.") } } else { self.showAlert(title: "Import Canceled", message: "The settings import operation was canceled.") } } } private func showAlert(title: String, message: String) { DispatchQueue.main.async { let alert = NSAlert() alert.messageText = title alert.informativeText = message alert.alertStyle = .informational alert.addButton(withTitle: "OK") alert.runModal() } } private func showRestartAlert(message: String) { DispatchQueue.main.async { let alert = NSAlert() alert.messageText = "Import Successful" alert.informativeText = message + "\n\nIMPORTANT: If you were using AI enhancement features, please make sure to reconfigure your API keys in the Enhancement section.\n\nIt is recommended to restart VoiceInk for all changes to take full effect." alert.alertStyle = .informational alert.addButton(withTitle: "OK") alert.addButton(withTitle: "Configure API Keys") let response = alert.runModal() if response == .alertSecondButtonReturn { NotificationCenter.default.post( name: .navigateToDestination, object: nil, userInfo: ["destination": "Enhancement"] ) } } } }