vOOice/VoiceInk/Views/Dictionary/DictionarySettingsView.swift
Beingpax 60125c316b Migrate dictionary data from UserDefaults to SwiftData
Migrates vocabulary words and word replacements from UserDefaults to SwiftData for better data management and persistence.

Changes:
- Create VocabularyWord and WordReplacement SwiftData models
- Add dual ModelConfiguration setup (default.store for transcripts, dictionary.store for dictionary data)
- Implement DictionaryMigrationService for one-time UserDefaults→SwiftData migration
- Rename "Correct Spellings" to "Vocabulary" for clearer terminology
- Update all dictionary views to use @Query instead of manager classes
- Update all services to fetch from SwiftData using FetchDescriptor
- Enhance word replacement duplicate detection (now checks during add AND edit)
- Update import/export services to work with SwiftData
- Preserve all existing functionality with improved data integrity

Technical details:
- Separate store files: default.store (transcripts) + dictionary.store (vocabulary + replacements)
- Migration flag: "HasMigratedDictionaryToSwiftData_v2"
- All CRUD operations properly implemented with duplicate detection
2025-12-28 12:09:43 +05:45

165 lines
5.5 KiB
Swift

import SwiftUI
import SwiftData
struct DictionarySettingsView: View {
@Environment(\.modelContext) private var modelContext
@State private var selectedSection: DictionarySection = .replacements
let whisperPrompt: WhisperPrompt
enum DictionarySection: String, CaseIterable {
case replacements = "Word Replacements"
case spellings = "Vocabulary"
var description: String {
switch self {
case .spellings:
return "Add words to help VoiceInk recognize them properly"
case .replacements:
return "Automatically replace specific words/phrases with custom formatted text "
}
}
var icon: String {
switch self {
case .spellings:
return "character.book.closed.fill"
case .replacements:
return "arrow.2.squarepath"
}
}
}
var body: some View {
ScrollView {
VStack(spacing: 0) {
heroSection
mainContent
}
}
.frame(minWidth: 600, minHeight: 500)
.background(Color(NSColor.controlBackgroundColor))
}
private var heroSection: some View {
VStack(spacing: 24) {
Image(systemName: "brain.filled.head.profile")
.font(.system(size: 40))
.foregroundStyle(.blue)
.padding(20)
.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))
Text("Enhance VoiceInk's transcription accuracy by teaching it your vocabulary")
.font(.system(size: 15))
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 400)
}
}
.padding(.vertical, 40)
.frame(maxWidth: .infinity)
}
private var mainContent: some View {
VStack(spacing: 40) {
sectionSelector
selectedSectionContent
}
.padding(.horizontal, 32)
.padding(.vertical, 40)
}
private var sectionSelector: some View {
VStack(alignment: .leading, spacing: 20) {
HStack {
Text("Select Section")
.font(.title2)
.fontWeight(.semibold)
Spacer()
HStack(spacing: 12) {
Button(action: {
DictionaryImportExportService.shared.importDictionary(into: modelContext)
}) {
Image(systemName: "square.and.arrow.down")
.font(.system(size: 18))
.foregroundColor(.blue)
}
.buttonStyle(.plain)
.help("Import vocabulary and word replacements")
Button(action: {
DictionaryImportExportService.shared.exportDictionary(from: modelContext)
}) {
Image(systemName: "square.and.arrow.up")
.font(.system(size: 18))
.foregroundColor(.blue)
}
.buttonStyle(.plain)
.help("Export vocabulary and word replacements")
}
}
HStack(spacing: 20) {
ForEach(DictionarySection.allCases, id: \.self) { section in
SectionCard(
section: section,
isSelected: selectedSection == section,
action: { selectedSection = section }
)
}
}
}
}
private var selectedSectionContent: some View {
VStack(alignment: .leading, spacing: 20) {
switch selectedSection {
case .spellings:
VocabularyView(whisperPrompt: whisperPrompt)
.background(CardBackground(isSelected: false))
case .replacements:
WordReplacementView()
.background(CardBackground(isSelected: false))
}
}
}
}
struct SectionCard: View {
let section: DictionarySettingsView.DictionarySection
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
VStack(alignment: .leading, spacing: 12) {
Image(systemName: section.icon)
.font(.system(size: 28))
.symbolRenderingMode(.hierarchical)
.foregroundStyle(isSelected ? .blue : .secondary)
VStack(alignment: .leading, spacing: 4) {
Text(section.rawValue)
.font(.headline)
Text(section.description)
.font(.subheadline)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(CardBackground(isSelected: isSelected))
}
.buttonStyle(.plain)
}
}