Implement Dictionary Import export support for iOS companion app(v1)
This commit is contained in:
parent
5f59b61ee8
commit
cf8d821436
177
VoiceInk/Services/DictionaryImportExportService.swift
Normal file
177
VoiceInk/Services/DictionaryImportExportService.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user