// // 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 @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 { 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 { 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) } } }