178 lines
7.9 KiB
Swift
178 lines
7.9 KiB
Swift
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 replacementToOriginals: [String: Set<String>] = [:]
|
|
|
|
for (originals, replacement) in existingReplacements {
|
|
let replacementLower = replacement.lowercased()
|
|
let words = originals.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
|
|
replacementToOriginals[replacementLower, default: []].formUnion(words)
|
|
}
|
|
|
|
for (originals, replacement) in importedData.wordReplacements {
|
|
let replacementLower = replacement.lowercased()
|
|
let words = originals.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
|
|
replacementToOriginals[replacementLower, default: []].formUnion(words)
|
|
}
|
|
|
|
var mergedReplacements: [String: String] = [:]
|
|
for (replacementLower, originals) in replacementToOriginals {
|
|
var uniqueOriginals: [String] = []
|
|
var seenLower = Set<String>()
|
|
|
|
for original in originals {
|
|
let originalLower = original.lowercased()
|
|
if !seenLower.contains(originalLower) {
|
|
uniqueOriginals.append(original)
|
|
seenLower.insert(originalLower)
|
|
}
|
|
}
|
|
|
|
let mergedKey = uniqueOriginals.sorted().joined(separator: ", ")
|
|
|
|
let replacement = existingReplacements.first(where: { $0.value.lowercased() == replacementLower })?.value
|
|
?? importedData.wordReplacements.first(where: { $0.value.lowercased() == replacementLower })?.value
|
|
?? replacementLower
|
|
|
|
mergedReplacements[mergedKey] = replacement
|
|
}
|
|
|
|
UserDefaults.standard.set(mergedReplacements, 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: Merged into \(mergedReplacements.count) total rules"
|
|
|
|
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 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()
|
|
}
|
|
}
|
|
}
|