vOOice/VoiceInk/Services/DictionaryImportExportService.swift
2025-11-14 11:12:41 +05:45

187 lines
8.0 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 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()
}
}
}