Files
WorterBuch/WorterBuch/VocabularyGridView.swift

275 lines
9.5 KiB
Swift

//
// VocabularyGridView.swift
// WorterBuch
//
// Created by Oliver Hnát on 01.12.2025.
//
import SwiftUI
import PencilKit
import CoreData
struct FieldSelection: Identifiable {
let id = UUID()
let entry: VocabularyEntry
let fieldType: FieldType
}
struct VocabularyGridView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \VocabularyEntry.timestamp, ascending: false)],
animation: .default
)
private var entries: FetchedResults<VocabularyEntry>
@State private var searchText = ""
@State private var fieldSelection: FieldSelection?
@State private var showingTagManager = false
var filteredEntries: [VocabularyEntry] {
if searchText.isEmpty {
return Array(entries)
} else {
return entries.filter { entry in
(entry.germanWordText?.localizedCaseInsensitiveContains(searchText) ?? false) ||
(entry.germanExplanationText?.localizedCaseInsensitiveContains(searchText) ?? false) ||
(entry.englishTranslationText?.localizedCaseInsensitiveContains(searchText) ?? false)
}
}
}
var body: some View {
NavigationView {
VStack(spacing: 0) {
// Search bar
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.secondary)
TextField("Search words, explanations, or translations", text: $searchText)
.textFieldStyle(.plain)
if !searchText.isEmpty {
Button(action: { searchText = "" }) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.secondary)
}
}
}
.padding(10)
.background(Color(.systemGray6))
.cornerRadius(10)
.padding()
// Column headers
HStack(spacing: 12) {
Text("German Word")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
Text("German Explanation")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
Text("English Translation")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.horizontal)
.padding(.bottom, 8)
Divider()
// Entries list
List {
ForEach(filteredEntries, id: \.id) { entry in
VocabularyEntryRow(
entry: entry,
onSelectField: { fieldType in
openFieldEditor(for: entry, fieldType: fieldType)
},
onTranslate: translateEntry
)
.listRowInsets(EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12))
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
deleteEntry(entry)
} label: {
Label("Delete", systemImage: "trash")
}
}
}
}
.listStyle(.plain)
.environment(\.defaultMinListRowHeight, 0)
}
.navigationTitle("Wörterbuch")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
HStack(spacing: 16) {
Button(action: { showingTagManager = true }) {
Label("Manage Tags", systemImage: "tag")
}
Button(action: addEntry) {
Label("Add Entry", systemImage: "plus")
}
}
}
}
.sheet(isPresented: $showingTagManager) {
TagManagerView()
}
.sheet(item: $fieldSelection) { selection in
FieldEditorView(
drawing: bindingForDrawing(entry: selection.entry, fieldType: selection.fieldType),
text: bindingForText(entry: selection.entry, fieldType: selection.fieldType),
fieldType: selection.fieldType,
entry: selection.entry
)
.onDisappear {
saveContext()
}
}
}
.navigationViewStyle(.stack)
}
private func openFieldEditor(for entry: VocabularyEntry, fieldType: FieldType) {
fieldSelection = FieldSelection(entry: entry, fieldType: fieldType)
}
private func bindingForDrawing(entry: VocabularyEntry, fieldType: FieldType) -> Binding<PKDrawing> {
Binding(
get: {
switch fieldType {
case .germanWord:
return entry.germanWordPKDrawing
case .germanExplanation:
return entry.germanExplanationPKDrawing
case .englishTranslation:
return entry.englishTranslationPKDrawing
}
},
set: { newValue in
switch fieldType {
case .germanWord:
entry.germanWordPKDrawing = newValue
case .germanExplanation:
entry.germanExplanationPKDrawing = newValue
case .englishTranslation:
entry.englishTranslationPKDrawing = newValue
}
}
)
}
private func bindingForText(entry: VocabularyEntry, fieldType: FieldType) -> Binding<String> {
Binding(
get: {
switch fieldType {
case .germanWord:
return entry.germanWordText ?? ""
case .germanExplanation:
return entry.germanExplanationText ?? ""
case .englishTranslation:
return entry.englishTranslationText ?? ""
}
},
set: { newValue in
switch fieldType {
case .germanWord:
entry.germanWordText = newValue
case .germanExplanation:
entry.germanExplanationText = newValue
case .englishTranslation:
entry.englishTranslationText = newValue
}
}
)
}
private func addEntry() {
withAnimation {
let _ = VocabularyEntry.create(in: viewContext)
saveContext()
}
}
private func deleteEntry(_ entry: VocabularyEntry) {
withAnimation {
viewContext.delete(entry)
saveContext()
}
}
private func saveContext() {
do {
try viewContext.save()
} catch {
let nsError = error as NSError
print("Error saving context: \(nsError), \(nsError.userInfo)")
}
}
private func translateEntry(_ entry: VocabularyEntry) {
guard let germanText = entry.germanWordText, !germanText.isEmpty else { return }
guard entry.englishTranslationText?.isEmpty ?? true else { return }
Task {
if #available(iOS 18.0, *) {
if let translation = await TranslationService.translate(text: germanText) {
await MainActor.run {
entry.englishTranslationText = translation
saveContext()
}
}
} else {
print("Translation requires iOS 18.0 or later")
}
}
}
}
struct VocabularyEntryRow: View {
@ObservedObject var entry: VocabularyEntry
let onSelectField: (FieldType) -> Void
let onTranslate: (VocabularyEntry) -> Void
var shouldShowTranslateButton: Bool {
!(entry.germanWordText?.isEmpty ?? true) &&
(entry.englishTranslationText?.isEmpty ?? true)
}
var body: some View {
HStack(spacing: 12) {
// German word
VocabularyFieldCell(
drawing: entry.germanWordPKDrawing,
text: entry.germanWordText ?? "",
onTap: { onSelectField(.germanWord) }
)
.frame(maxWidth: .infinity)
// German explanation
VocabularyFieldCell(
drawing: entry.germanExplanationPKDrawing,
text: entry.germanExplanationText ?? "",
onTap: { onSelectField(.germanExplanation) }
)
.frame(maxWidth: .infinity)
// English translation
VocabularyFieldCell(
drawing: entry.englishTranslationPKDrawing,
text: entry.englishTranslationText ?? "",
onTap: { onSelectField(.englishTranslation) },
showTranslateButton: shouldShowTranslateButton,
onTranslate: { onTranslate(entry) }
)
.frame(maxWidth: .infinity)
}
}
}