import Foundation import AppKit import UniformTypeIdentifiers struct DictionaryExportData: Codable { let version: String let dictionaryItems: [String] let wordReplacements: [String: String] let exportDate: Date } class DictionaryImportExportService { static let shared = DictionaryImportExportService() private let dictionaryItemsKey = "CustomVocabularyItems" private let wordReplacementsKey = "wordReplacements" private init() {} func exportDictionary() { var dictionaryWords: [String] = [] if let data = UserDefaults.standard.data(forKey: dictionaryItemsKey), let items = try? JSONDecoder().decode([DictionaryItem].self, from: data) { dictionaryWords = items.map { $0.word } } let wordReplacements = UserDefaults.standard.dictionary(forKey: wordReplacementsKey) as? [String: String] ?? [:] let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0.0" let exportData = DictionaryExportData( version: version, dictionaryItems: dictionaryWords, wordReplacements: wordReplacements, exportDate: Date() ) let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted encoder.dateEncodingStrategy = .iso8601 do { let jsonData = try encoder.encode(exportData) let savePanel = NSSavePanel() savePanel.allowedContentTypes = [UTType.json] savePanel.nameFieldStringValue = "VoiceInk_Dictionary.json" savePanel.title = "Export Dictionary Data" savePanel.message = "Choose a location to save your dictionary items and word replacements." DispatchQueue.main.async { if savePanel.runModal() == .OK { if let url = savePanel.url { do { try jsonData.write(to: url) self.showAlert(title: "Export Successful", message: "Dictionary data exported successfully to \(url.lastPathComponent).") } catch { self.showAlert(title: "Export Error", message: "Could not save dictionary data: \(error.localizedDescription)") } } } else { self.showAlert(title: "Export Canceled", message: "Export operation was canceled.") } } } catch { self.showAlert(title: "Export Error", message: "Could not encode dictionary data: \(error.localizedDescription)") } } func importDictionary() { let openPanel = NSOpenPanel() openPanel.allowedContentTypes = [UTType.json] openPanel.canChooseFiles = true openPanel.canChooseDirectories = false openPanel.allowsMultipleSelection = false openPanel.title = "Import Dictionary Data" openPanel.message = "Choose a dictionary file to import. New items will be added, existing items will be kept." 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.") return } do { let jsonData = try Data(contentsOf: url) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let importedData = try decoder.decode(DictionaryExportData.self, from: jsonData) var existingItems: [DictionaryItem] = [] if let data = UserDefaults.standard.data(forKey: self.dictionaryItemsKey), let items = try? JSONDecoder().decode([DictionaryItem].self, from: data) { existingItems = items } let existingWordsLower = Set(existingItems.map { $0.word.lowercased() }) let originalExistingCount = existingItems.count var newWordsAdded = 0 for importedWord in importedData.dictionaryItems { if !existingWordsLower.contains(importedWord.lowercased()) { existingItems.append(DictionaryItem(word: importedWord)) newWordsAdded += 1 } } if let encoded = try? JSONEncoder().encode(existingItems) { UserDefaults.standard.set(encoded, forKey: self.dictionaryItemsKey) } var existingReplacements = UserDefaults.standard.dictionary(forKey: self.wordReplacementsKey) as? [String: String] ?? [:] var addedCount = 0 var updatedCount = 0 for (importedKey, importedReplacement) in importedData.wordReplacements { let normalizedImportedKey = self.normalizeReplacementKey(importedKey) let importedWords = self.extractWords(from: normalizedImportedKey) var modifiedExisting: [String: String] = [:] for (existingKey, existingReplacement) in existingReplacements { var existingWords = self.extractWords(from: existingKey) var modified = false for importedWord in importedWords { if let index = existingWords.firstIndex(where: { $0.lowercased() == importedWord.lowercased() }) { existingWords.remove(at: index) modified = true } } if !existingWords.isEmpty { let newKey = existingWords.joined(separator: ", ") modifiedExisting[newKey] = existingReplacement } if modified { updatedCount += 1 } } existingReplacements = modifiedExisting existingReplacements[normalizedImportedKey] = importedReplacement addedCount += 1 } UserDefaults.standard.set(existingReplacements, forKey: self.wordReplacementsKey) var message = "Dictionary data imported successfully from \(url.lastPathComponent).\n\n" message += "Dictionary Items: \(newWordsAdded) added, \(originalExistingCount) kept\n" message += "Word Replacements: \(addedCount) added, \(updatedCount) updated" self.showAlert(title: "Import Successful", message: message) } catch { self.showAlert(title: "Import Error", message: "Error importing dictionary data: \(error.localizedDescription). The file might be corrupted or not in the correct format.") } } else { self.showAlert(title: "Import Canceled", message: "Import operation was canceled.") } } } private func normalizeReplacementKey(_ key: String) -> String { let words = extractWords(from: key) return words.joined(separator: ", ") } private func extractWords(from key: String) -> [String] { return key .split(separator: ",") .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } } 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() } } }