feat: Improve settings import/export with API key guidance
This commit is contained in:
parent
fad746e39b
commit
4c32710d5e
260
VoiceInk/Services/ImportExportService.swift
Normal file
260
VoiceInk/Services/ImportExportService.swift
Normal file
@ -0,0 +1,260 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
import UniformTypeIdentifiers
|
||||
import KeyboardShortcuts
|
||||
import LaunchAtLogin
|
||||
|
||||
struct GeneralSettings: Codable {
|
||||
let toggleMiniRecorderShortcut: KeyboardShortcuts.Shortcut?
|
||||
let isPushToTalkEnabled: Bool?
|
||||
let pushToTalkKeyRawValue: String?
|
||||
let launchAtLoginEnabled: Bool?
|
||||
let isMenuBarOnly: Bool?
|
||||
let useAppleScriptPaste: Bool?
|
||||
let recorderType: String?
|
||||
let isAudioCleanupEnabled: Bool?
|
||||
let audioRetentionPeriod: Int?
|
||||
let isAutoCopyEnabled: Bool?
|
||||
let isSoundFeedbackEnabled: Bool?
|
||||
let isSystemMuteEnabled: Bool?
|
||||
}
|
||||
|
||||
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?
|
||||
}
|
||||
|
||||
class ImportExportService {
|
||||
static let shared = ImportExportService()
|
||||
private let currentSettingsVersion: String
|
||||
private let dictionaryItemsKey = "CustomDictionaryItems"
|
||||
private let wordReplacementsKey = "wordReplacements"
|
||||
|
||||
private let keyIsPushToTalkEnabled = "isPushToTalkEnabled"
|
||||
private let keyPushToTalkKey = "pushToTalkKey"
|
||||
private let keyIsMenuBarOnly = "IsMenuBarOnly"
|
||||
private let keyUseAppleScriptPaste = "UseAppleScriptPaste"
|
||||
private let keyRecorderType = "RecorderType"
|
||||
private let keyIsAudioCleanupEnabled = "IsAudioCleanupEnabled"
|
||||
private let keyAudioRetentionPeriod = "AudioRetentionPeriod"
|
||||
private let keyIsAutoCopyEnabled = "IsAutoCopyEnabled"
|
||||
private let keyIsSoundFeedbackEnabled = "isSoundFeedbackEnabled"
|
||||
private let keyIsSystemMuteEnabled = "isSystemMuteEnabled"
|
||||
|
||||
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, soundManager: SoundManager, whisperState: WhisperState) {
|
||||
let powerModeManager = PowerModeManager.shared
|
||||
|
||||
let exportablePrompts = enhancementService.customPrompts.filter { !$0.isPredefined }
|
||||
|
||||
let powerConfigs = powerModeManager.configurations
|
||||
let defaultPowerConfig = powerModeManager.defaultConfig
|
||||
|
||||
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),
|
||||
isPushToTalkEnabled: hotkeyManager.isPushToTalkEnabled,
|
||||
pushToTalkKeyRawValue: hotkeyManager.pushToTalkKey.rawValue,
|
||||
launchAtLoginEnabled: LaunchAtLogin.isEnabled,
|
||||
isMenuBarOnly: menuBarManager.isMenuBarOnly,
|
||||
useAppleScriptPaste: UserDefaults.standard.bool(forKey: keyUseAppleScriptPaste),
|
||||
recorderType: whisperState.recorderType,
|
||||
isAudioCleanupEnabled: UserDefaults.standard.bool(forKey: keyIsAudioCleanupEnabled),
|
||||
audioRetentionPeriod: UserDefaults.standard.integer(forKey: keyAudioRetentionPeriod),
|
||||
isAutoCopyEnabled: whisperState.isAutoCopyEnabled,
|
||||
isSoundFeedbackEnabled: soundManager.isEnabled,
|
||||
isSystemMuteEnabled: mediaController.isSystemMuteEnabled
|
||||
)
|
||||
|
||||
let settingsToExport = VoiceInkExportedSettings(
|
||||
version: currentSettingsVersion,
|
||||
customPrompts: exportablePrompts,
|
||||
powerModeConfigs: powerConfigs,
|
||||
defaultPowerModeConfig: defaultPowerConfig,
|
||||
dictionaryItems: exportedDictionaryItems,
|
||||
wordReplacements: exportedWordReplacements,
|
||||
generalSettings: generalSettingsToExport
|
||||
)
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
|
||||
do {
|
||||
let jsonData = try encoder.encode(settingsToExport)
|
||||
|
||||
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, 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.defaultConfig = importedSettings.defaultPowerModeConfig
|
||||
powerModeManager.saveConfigurations()
|
||||
powerModeManager.updateConfiguration(powerModeManager.defaultConfig)
|
||||
|
||||
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 pttEnabled = general.isPushToTalkEnabled {
|
||||
hotkeyManager.isPushToTalkEnabled = pttEnabled
|
||||
}
|
||||
if let pttKeyRaw = general.pushToTalkKeyRawValue,
|
||||
let pttKey = HotkeyManager.PushToTalkKey(rawValue: pttKeyRaw) {
|
||||
hotkeyManager.pushToTalkKey = pttKey
|
||||
}
|
||||
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 audioCleanup = general.isAudioCleanupEnabled {
|
||||
UserDefaults.standard.set(audioCleanup, forKey: self.keyIsAudioCleanupEnabled)
|
||||
}
|
||||
if let audioRetention = general.audioRetentionPeriod {
|
||||
UserDefaults.standard.set(audioRetention, forKey: self.keyAudioRetentionPeriod)
|
||||
}
|
||||
if let autoCopy = general.isAutoCopyEnabled {
|
||||
whisperState.isAutoCopyEnabled = autoCopy
|
||||
}
|
||||
if let soundFeedback = general.isSoundFeedbackEnabled {
|
||||
soundManager.isEnabled = soundFeedback
|
||||
}
|
||||
if let muteSystem = general.isSystemMuteEnabled {
|
||||
mediaController.isSystemMuteEnabled = muteSystem
|
||||
}
|
||||
}
|
||||
|
||||
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"]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ struct SettingsView: View {
|
||||
@EnvironmentObject private var menuBarManager: MenuBarManager
|
||||
@EnvironmentObject private var hotkeyManager: HotkeyManager
|
||||
@EnvironmentObject private var whisperState: WhisperState
|
||||
@EnvironmentObject private var enhancementService: AIEnhancementService
|
||||
@StateObject private var deviceManager = AudioDeviceManager.shared
|
||||
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = true
|
||||
@State private var showResetOnboardingAlert = false
|
||||
@ -191,6 +192,52 @@ struct SettingsView: View {
|
||||
AudioCleanupSettingsView()
|
||||
}
|
||||
|
||||
// Data Management Section
|
||||
SettingsSection(
|
||||
icon: "arrow.up.arrow.down.circle",
|
||||
title: "Data Management",
|
||||
subtitle: "Import or export your settings"
|
||||
) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Export your custom prompts, power modes, word replacements, keyboard shortcuts, and app preferences to a backup file. API keys are not included in the export.")
|
||||
.settingsDescription()
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Button {
|
||||
ImportExportService.shared.importSettings(
|
||||
enhancementService: enhancementService,
|
||||
whisperPrompt: whisperState.whisperPrompt,
|
||||
hotkeyManager: hotkeyManager,
|
||||
menuBarManager: menuBarManager,
|
||||
mediaController: MediaController.shared,
|
||||
soundManager: SoundManager.shared,
|
||||
whisperState: whisperState
|
||||
)
|
||||
} label: {
|
||||
Label("Import Settings...", systemImage: "arrow.down.doc")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.controlSize(.large)
|
||||
|
||||
Button {
|
||||
ImportExportService.shared.exportSettings(
|
||||
enhancementService: enhancementService,
|
||||
whisperPrompt: whisperState.whisperPrompt,
|
||||
hotkeyManager: hotkeyManager,
|
||||
menuBarManager: menuBarManager,
|
||||
mediaController: MediaController.shared,
|
||||
soundManager: SoundManager.shared,
|
||||
whisperState: whisperState
|
||||
)
|
||||
} label: {
|
||||
Label("Export Settings...", systemImage: "arrow.up.doc")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.controlSize(.large)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset Onboarding Section
|
||||
SettingsSection(
|
||||
icon: "arrow.counterclockwise",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user