Implement Dictionary Import export support for iOS companion app(v1)

This commit is contained in:
Beingpax 2025-11-07 11:31:41 +05:45
parent 5f59b61ee8
commit cf8d821436
3 changed files with 212 additions and 10 deletions

View File

@ -0,0 +1,177 @@
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 = "CustomDictionaryItems"
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()
}
}
}

View File

@ -47,7 +47,7 @@ struct DictionarySettingsView: View {
.background(Circle()
.fill(Color(.windowBackgroundColor).opacity(0.9))
.shadow(color: .black.opacity(0.1), radius: 10, y: 5))
VStack(spacing: 8) {
Text("Dictionary Settings")
.font(.system(size: 28, weight: .bold))
@ -74,10 +74,36 @@ struct DictionarySettingsView: View {
private var sectionSelector: some View {
VStack(alignment: .leading, spacing: 20) {
Text("Select Section")
.font(.title2)
.fontWeight(.semibold)
HStack {
Text("Select Section")
.font(.title2)
.fontWeight(.semibold)
Spacer()
HStack(spacing: 12) {
Button(action: {
DictionaryImportExportService.shared.importDictionary()
}) {
Image(systemName: "square.and.arrow.down")
.font(.system(size: 18))
.foregroundColor(.blue)
}
.buttonStyle(.plain)
.help("Import dictionary items and word replacements")
Button(action: {
DictionaryImportExportService.shared.exportDictionary()
}) {
Image(systemName: "square.and.arrow.up")
.font(.system(size: 18))
.foregroundColor(.blue)
}
.buttonStyle(.plain)
.help("Export dictionary items and word replacements")
}
}
HStack(spacing: 20) {
ForEach(DictionarySection.allCases, id: \.self) { section in
SectionCard(

View File

@ -1,11 +1,11 @@
import SwiftUI
struct DictionaryItem: Identifiable, Hashable, Codable {
let id: UUID
var word: String
init(id: UUID = UUID(), word: String) {
self.id = id
var id: String { word }
init(word: String) {
self.word = word
}
@ -15,15 +15,14 @@ struct DictionaryItem: Identifiable, Hashable, Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
word = try container.decode(String.self, forKey: .word)
_ = try? container.decodeIfPresent(UUID.self, forKey: .id)
_ = try? container.decodeIfPresent(Date.self, forKey: .dateAdded)
_ = try? container.decodeIfPresent(Bool.self, forKey: .isEnabled)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(word, forKey: .word)
}
}