275 lines
9.5 KiB
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)
|
|
}
|
|
}
|
|
}
|