feat(worterbuch): basic app implementation
This commit is contained in:
@@ -9,78 +9,11 @@ import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct ContentView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
|
||||
animation: .default)
|
||||
private var items: FetchedResults<Item>
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
ForEach(items) { item in
|
||||
NavigationLink {
|
||||
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
|
||||
} label: {
|
||||
Text(item.timestamp!, formatter: itemFormatter)
|
||||
}
|
||||
}
|
||||
.onDelete(perform: deleteItems)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
EditButton()
|
||||
}
|
||||
ToolbarItem {
|
||||
Button(action: addItem) {
|
||||
Label("Add Item", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
Text("Select an item")
|
||||
}
|
||||
}
|
||||
|
||||
private func addItem() {
|
||||
withAnimation {
|
||||
let newItem = Item(context: viewContext)
|
||||
newItem.timestamp = Date()
|
||||
|
||||
do {
|
||||
try viewContext.save()
|
||||
} catch {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
let nsError = error as NSError
|
||||
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteItems(offsets: IndexSet) {
|
||||
withAnimation {
|
||||
offsets.map { items[$0] }.forEach(viewContext.delete)
|
||||
|
||||
do {
|
||||
try viewContext.save()
|
||||
} catch {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
let nsError = error as NSError
|
||||
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||
}
|
||||
}
|
||||
VocabularyGridView()
|
||||
}
|
||||
}
|
||||
|
||||
private let itemFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .short
|
||||
formatter.timeStyle = .medium
|
||||
return formatter
|
||||
}()
|
||||
|
||||
#Preview {
|
||||
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
|
||||
}
|
||||
|
||||
32
WorterBuch/DictionaryHelper.swift
Normal file
32
WorterBuch/DictionaryHelper.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// DictionaryHelper.swift
|
||||
// WorterBuch
|
||||
//
|
||||
// Created by Oliver Hnát on 01.12.2025.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class DictionaryHelper {
|
||||
|
||||
static func showDefinition(for text: String) {
|
||||
guard UIReferenceLibraryViewController.dictionaryHasDefinition(forTerm: text) else {
|
||||
print("No definition found for: \(text)")
|
||||
return
|
||||
}
|
||||
|
||||
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootViewController = windowScene.windows.first?.rootViewController else {
|
||||
return
|
||||
}
|
||||
|
||||
let dictionaryViewController = UIReferenceLibraryViewController(term: text)
|
||||
var topController = rootViewController
|
||||
|
||||
while let presented = topController.presentedViewController {
|
||||
topController = presented
|
||||
}
|
||||
|
||||
topController.present(dictionaryViewController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
127
WorterBuch/FieldEditorView.swift
Normal file
127
WorterBuch/FieldEditorView.swift
Normal file
@@ -0,0 +1,127 @@
|
||||
//
|
||||
// FieldEditorView.swift
|
||||
// WorterBuch
|
||||
//
|
||||
// Created by Oliver Hnát on 01.12.2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import PencilKit
|
||||
|
||||
enum FieldType {
|
||||
case germanWord
|
||||
case germanExplanation
|
||||
case englishTranslation
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .germanWord: return "German Word"
|
||||
case .germanExplanation: return "German Explanation"
|
||||
case .englishTranslation: return "English Translation"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FieldEditorView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Binding var drawing: PKDrawing
|
||||
@Binding var text: String
|
||||
let fieldType: FieldType
|
||||
|
||||
@State private var isRecognizing = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack(spacing: 0) {
|
||||
// Handwriting canvas
|
||||
HandwritingCanvasView(
|
||||
drawing: $drawing,
|
||||
onDrawingChanged: { newDrawing in
|
||||
recognizeHandwriting(newDrawing)
|
||||
},
|
||||
isEditable: true
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 400)
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(12)
|
||||
.padding()
|
||||
|
||||
// Transcribed text section
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Text("Transcribed Text")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if isRecognizing {
|
||||
ProgressView()
|
||||
.scaleEffect(0.8)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
TextEditor(text: $text)
|
||||
.font(.body)
|
||||
.frame(minHeight: 100)
|
||||
.padding(8)
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(8)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color.gray.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.navigationTitle(fieldType.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Clear") {
|
||||
drawing = PKDrawing()
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
}
|
||||
|
||||
private func recognizeHandwriting(_ drawing: PKDrawing) {
|
||||
print("=== recognizeHandwriting called ===")
|
||||
print("Drawing bounds: \(drawing.bounds)")
|
||||
print("Drawing is empty: \(drawing.bounds.isEmpty)")
|
||||
|
||||
guard !drawing.bounds.isEmpty else {
|
||||
print("Drawing bounds are empty, skipping recognition")
|
||||
return
|
||||
}
|
||||
|
||||
print("Starting recognition...")
|
||||
isRecognizing = true
|
||||
Task {
|
||||
if let recognizedText = await HandwritingRecognizer.recognizeTextAsync(from: drawing) {
|
||||
print("Recognition successful: \(recognizedText)")
|
||||
await MainActor.run {
|
||||
text = recognizedText
|
||||
isRecognizing = false
|
||||
}
|
||||
} else {
|
||||
print("Recognition returned nil")
|
||||
await MainActor.run {
|
||||
isRecognizing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
74
WorterBuch/HandwritingCanvasView.swift
Normal file
74
WorterBuch/HandwritingCanvasView.swift
Normal file
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// HandwritingCanvasView.swift
|
||||
// WorterBuch
|
||||
//
|
||||
// Created by Oliver Hnát on 01.12.2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import PencilKit
|
||||
|
||||
struct HandwritingCanvasView: UIViewRepresentable {
|
||||
@Binding var drawing: PKDrawing
|
||||
var onDrawingChanged: ((PKDrawing) -> Void)?
|
||||
var isEditable: Bool = true
|
||||
|
||||
func makeUIView(context: Context) -> PKCanvasView {
|
||||
let canvasView = PKCanvasView()
|
||||
canvasView.drawing = drawing
|
||||
canvasView.delegate = context.coordinator
|
||||
canvasView.drawingPolicy = .anyInput
|
||||
canvasView.isOpaque = false
|
||||
canvasView.backgroundColor = .clear
|
||||
canvasView.isUserInteractionEnabled = isEditable
|
||||
|
||||
// Use the shared tool picker and show it after a delay
|
||||
DispatchQueue.main.async {
|
||||
if isEditable, let window = canvasView.window {
|
||||
let toolPicker = PKToolPicker.shared(for: window)
|
||||
toolPicker?.setVisible(true, forFirstResponder: canvasView)
|
||||
toolPicker?.addObserver(canvasView)
|
||||
canvasView.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
return canvasView
|
||||
}
|
||||
|
||||
func updateUIView(_ canvasView: PKCanvasView, context: Context) {
|
||||
if canvasView.drawing != drawing {
|
||||
canvasView.drawing = drawing
|
||||
}
|
||||
canvasView.isUserInteractionEnabled = isEditable
|
||||
|
||||
// Ensure tool picker is visible when editable
|
||||
if isEditable {
|
||||
DispatchQueue.main.async {
|
||||
if let window = canvasView.window {
|
||||
let toolPicker = PKToolPicker.shared(for: window)
|
||||
toolPicker?.setVisible(true, forFirstResponder: canvasView)
|
||||
canvasView.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(drawing: $drawing, onDrawingChanged: onDrawingChanged)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, PKCanvasViewDelegate {
|
||||
var drawing: Binding<PKDrawing>
|
||||
var onDrawingChanged: ((PKDrawing) -> Void)?
|
||||
|
||||
init(drawing: Binding<PKDrawing>, onDrawingChanged: ((PKDrawing) -> Void)?) {
|
||||
self.drawing = drawing
|
||||
self.onDrawingChanged = onDrawingChanged
|
||||
}
|
||||
|
||||
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
|
||||
drawing.wrappedValue = canvasView.drawing
|
||||
onDrawingChanged?(canvasView.drawing)
|
||||
}
|
||||
}
|
||||
}
|
||||
117
WorterBuch/HandwritingRecognizer.swift
Normal file
117
WorterBuch/HandwritingRecognizer.swift
Normal file
@@ -0,0 +1,117 @@
|
||||
//
|
||||
// HandwritingRecognizer.swift
|
||||
// WorterBuch
|
||||
//
|
||||
// Created by Oliver Hnát on 01.12.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PencilKit
|
||||
import Vision
|
||||
import UIKit
|
||||
|
||||
class HandwritingRecognizer {
|
||||
|
||||
static func recognizeText(from drawing: PKDrawing, completion: @escaping (String?) -> Void) {
|
||||
// Check if drawing has content
|
||||
guard !drawing.bounds.isEmpty else {
|
||||
print("Drawing bounds are empty")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Convert drawing to image with high scale for better recognition
|
||||
let bounds = drawing.bounds
|
||||
print("Drawing bounds: \(bounds)")
|
||||
|
||||
// Create a high-resolution image with white background for better recognition
|
||||
let scale: CGFloat = 4.0
|
||||
let imageSize = CGSize(width: bounds.width * scale, height: bounds.height * scale)
|
||||
|
||||
// Create image with white background
|
||||
UIGraphicsBeginImageContextWithOptions(bounds.size, true, scale)
|
||||
guard let context = UIGraphicsGetCurrentContext() else {
|
||||
print("Failed to create graphics context")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Fill with white background
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fill(CGRect(origin: .zero, size: bounds.size))
|
||||
|
||||
// Draw the PKDrawing
|
||||
let drawingImage = drawing.image(from: bounds, scale: scale)
|
||||
drawingImage.draw(at: .zero)
|
||||
|
||||
guard let image = UIGraphicsGetImageFromCurrentImageContext() else {
|
||||
UIGraphicsEndImageContext()
|
||||
print("Failed to create image from context")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
guard let cgImage = image.cgImage else {
|
||||
print("Failed to create CGImage from drawing")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
print("Created image with size: \(image.size), scale: \(image.scale), pixel size: \(imageSize)")
|
||||
|
||||
// Create Vision request
|
||||
let request = VNRecognizeTextRequest { request, error in
|
||||
if let error = error {
|
||||
print("Text recognition error: \(error.localizedDescription)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
guard let observations = request.results as? [VNRecognizedTextObservation] else {
|
||||
print("No text observations found")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
print("Found \(observations.count) text observations")
|
||||
|
||||
// Combine all recognized text
|
||||
let recognizedStrings = observations.compactMap { observation in
|
||||
observation.topCandidates(1).first?.string
|
||||
}
|
||||
|
||||
print("Recognized strings: \(recognizedStrings)")
|
||||
|
||||
let recognizedText = recognizedStrings.joined(separator: " ")
|
||||
completion(recognizedText.isEmpty ? nil : recognizedText)
|
||||
}
|
||||
|
||||
// Configure for German language and handwriting
|
||||
request.recognitionLanguages = ["de-DE", "en-US"]
|
||||
request.recognitionLevel = .accurate
|
||||
request.usesLanguageCorrection = true
|
||||
|
||||
// Enable automatic language detection as fallback
|
||||
request.automaticallyDetectsLanguage = true
|
||||
|
||||
// Perform request
|
||||
let requestHandler = VNImageRequestHandler(cgImage: cgImage, options: [:])
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
do {
|
||||
try requestHandler.perform([request])
|
||||
} catch {
|
||||
print("Failed to perform text recognition: \(error.localizedDescription)")
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func recognizeTextAsync(from drawing: PKDrawing) async -> String? {
|
||||
await withCheckedContinuation { continuation in
|
||||
recognizeText(from: drawing) { result in
|
||||
continuation.resume(returning: result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict/>
|
||||
</plist>
|
||||
|
||||
@@ -14,15 +14,17 @@ struct PersistenceController {
|
||||
static let preview: PersistenceController = {
|
||||
let result = PersistenceController(inMemory: true)
|
||||
let viewContext = result.container.viewContext
|
||||
for _ in 0..<10 {
|
||||
let newItem = Item(context: viewContext)
|
||||
newItem.timestamp = Date()
|
||||
for i in 0..<5 {
|
||||
let entry = VocabularyEntry.create(in: viewContext)
|
||||
entry.timestamp = Date()
|
||||
entry.id = UUID()
|
||||
entry.germanWordText = "Beispielwort \(i + 1)"
|
||||
entry.germanExplanationText = "Dies ist eine Erklärung des deutschen Wortes"
|
||||
entry.englishTranslationText = "Example word \(i + 1)"
|
||||
}
|
||||
do {
|
||||
try viewContext.save()
|
||||
} catch {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
let nsError = error as NSError
|
||||
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||
}
|
||||
|
||||
61
WorterBuch/VocabularyEntry+Extensions.swift
Normal file
61
WorterBuch/VocabularyEntry+Extensions.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// VocabularyEntry+Extensions.swift
|
||||
// WorterBuch
|
||||
//
|
||||
// Created by Oliver Hnát on 01.12.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PencilKit
|
||||
import CoreData
|
||||
|
||||
extension VocabularyEntry {
|
||||
|
||||
// MARK: - German Word Drawing
|
||||
|
||||
var germanWordPKDrawing: PKDrawing {
|
||||
get {
|
||||
guard let data = germanWordDrawing else { return PKDrawing() }
|
||||
return (try? PKDrawing(data: data)) ?? PKDrawing()
|
||||
}
|
||||
set {
|
||||
germanWordDrawing = newValue.dataRepresentation()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - German Explanation Drawing
|
||||
|
||||
var germanExplanationPKDrawing: PKDrawing {
|
||||
get {
|
||||
guard let data = germanExplanationDrawing else { return PKDrawing() }
|
||||
return (try? PKDrawing(data: data)) ?? PKDrawing()
|
||||
}
|
||||
set {
|
||||
germanExplanationDrawing = newValue.dataRepresentation()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - English Translation Drawing
|
||||
|
||||
var englishTranslationPKDrawing: PKDrawing {
|
||||
get {
|
||||
guard let data = englishTranslationDrawing else { return PKDrawing() }
|
||||
return (try? PKDrawing(data: data)) ?? PKDrawing()
|
||||
}
|
||||
set {
|
||||
englishTranslationDrawing = newValue.dataRepresentation()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience Initializer
|
||||
|
||||
static func create(in context: NSManagedObjectContext) -> VocabularyEntry {
|
||||
let entry = VocabularyEntry(context: context)
|
||||
entry.id = UUID()
|
||||
entry.timestamp = Date()
|
||||
entry.germanWordText = ""
|
||||
entry.germanExplanationText = ""
|
||||
entry.englishTranslationText = ""
|
||||
return entry
|
||||
}
|
||||
}
|
||||
61
WorterBuch/VocabularyFieldCell.swift
Normal file
61
WorterBuch/VocabularyFieldCell.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// VocabularyFieldCell.swift
|
||||
// WorterBuch
|
||||
//
|
||||
// Created by Oliver Hnát on 01.12.2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import PencilKit
|
||||
|
||||
struct VocabularyFieldCell: View {
|
||||
let drawing: PKDrawing
|
||||
let text: String
|
||||
let onTap: () -> Void
|
||||
let onLongPress: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
// Handwriting preview
|
||||
if !drawing.bounds.isEmpty {
|
||||
Image(uiImage: drawing.image(from: drawing.bounds, scale: 2.0))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(height: 60)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color(.systemGray6))
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(Color(.systemGray6))
|
||||
.frame(height: 60)
|
||||
.overlay(
|
||||
Text("Tap to write")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
)
|
||||
}
|
||||
|
||||
// Transcribed text
|
||||
Text(text.isEmpty ? " " : text.lowercased())
|
||||
.font(.caption)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(2)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.frame(height: 30)
|
||||
}
|
||||
.padding(8)
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(8)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color.gray.opacity(0.2), lineWidth: 1)
|
||||
)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
onTap()
|
||||
}
|
||||
.onLongPressGesture {
|
||||
onLongPress()
|
||||
}
|
||||
}
|
||||
}
|
||||
255
WorterBuch/VocabularyGridView.swift
Normal file
255
WorterBuch/VocabularyGridView.swift
Normal file
@@ -0,0 +1,255 @@
|
||||
//
|
||||
// VocabularyGridView.swift
|
||||
// WorterBuch
|
||||
//
|
||||
// Created by Oliver Hnát on 01.12.2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import PencilKit
|
||||
import CoreData
|
||||
|
||||
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 selectedEntry: VocabularyEntry?
|
||||
@State private var selectedFieldType: FieldType?
|
||||
@State private var showingFieldEditor = false
|
||||
|
||||
// Temporary bindings for editing
|
||||
@State private var editingDrawing: PKDrawing = PKDrawing()
|
||||
@State private var editingText: String = ""
|
||||
|
||||
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
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 12) {
|
||||
ForEach(filteredEntries, id: \.id) { entry in
|
||||
VocabularyEntryRow(
|
||||
entry: entry,
|
||||
onSelectField: { fieldType in
|
||||
openFieldEditor(for: entry, fieldType: fieldType)
|
||||
},
|
||||
onLongPress: { fieldType in
|
||||
showDefinition(for: entry, fieldType: fieldType)
|
||||
}
|
||||
)
|
||||
}
|
||||
.onDelete(perform: deleteEntries)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.navigationTitle("Wörterbuch")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(action: addEntry) {
|
||||
Label("Add Entry", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingFieldEditor) {
|
||||
if let entry = selectedEntry, let fieldType = selectedFieldType {
|
||||
FieldEditorView(
|
||||
drawing: bindingForDrawing(entry: entry, fieldType: fieldType),
|
||||
text: bindingForText(entry: entry, fieldType: fieldType),
|
||||
fieldType: fieldType
|
||||
)
|
||||
.onDisappear {
|
||||
saveContext()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
}
|
||||
|
||||
private func openFieldEditor(for entry: VocabularyEntry, fieldType: FieldType) {
|
||||
selectedEntry = entry
|
||||
selectedFieldType = fieldType
|
||||
showingFieldEditor = true
|
||||
}
|
||||
|
||||
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 deleteEntries(offsets: IndexSet) {
|
||||
withAnimation {
|
||||
offsets.map { filteredEntries[$0] }.forEach(viewContext.delete)
|
||||
saveContext()
|
||||
}
|
||||
}
|
||||
|
||||
private func saveContext() {
|
||||
do {
|
||||
try viewContext.save()
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
print("Error saving context: \(nsError), \(nsError.userInfo)")
|
||||
}
|
||||
}
|
||||
|
||||
private func showDefinition(for entry: VocabularyEntry, fieldType: FieldType) {
|
||||
let text: String
|
||||
switch fieldType {
|
||||
case .germanWord:
|
||||
text = entry.germanWordText ?? ""
|
||||
case .germanExplanation:
|
||||
text = entry.germanExplanationText ?? ""
|
||||
case .englishTranslation:
|
||||
text = entry.englishTranslationText ?? ""
|
||||
}
|
||||
|
||||
if !text.isEmpty {
|
||||
DictionaryHelper.showDefinition(for: text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct VocabularyEntryRow: View {
|
||||
@ObservedObject var entry: VocabularyEntry
|
||||
let onSelectField: (FieldType) -> Void
|
||||
let onLongPress: (FieldType) -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
// German word
|
||||
VocabularyFieldCell(
|
||||
drawing: entry.germanWordPKDrawing,
|
||||
text: entry.germanWordText ?? "",
|
||||
onTap: { onSelectField(.germanWord) },
|
||||
onLongPress: { onLongPress(.germanWord) }
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
// German explanation
|
||||
VocabularyFieldCell(
|
||||
drawing: entry.germanExplanationPKDrawing,
|
||||
text: entry.germanExplanationText ?? "",
|
||||
onTap: { onSelectField(.germanExplanation) },
|
||||
onLongPress: { onLongPress(.germanExplanation) }
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
// English translation
|
||||
VocabularyFieldCell(
|
||||
drawing: entry.englishTranslationPKDrawing,
|
||||
text: entry.englishTranslationText ?? "",
|
||||
onTap: { onSelectField(.englishTranslation) },
|
||||
onLongPress: { onLongPress(.englishTranslation) }
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array/>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudKit</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict/>
|
||||
</plist>
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1" systemVersion="11A491" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="true" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Item" representedClassName="Item" syncable="YES" codeGenerationType="class">
|
||||
<entity name="VocabularyEntry" representedClassName="VocabularyEntry" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="germanWordDrawing" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="germanWordText" optional="YES" attributeType="String"/>
|
||||
<attribute name="germanExplanationDrawing" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="germanExplanationText" optional="YES" attributeType="String"/>
|
||||
<attribute name="englishTranslationDrawing" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="englishTranslationText" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Item" positionX="-63" positionY="-18" width="128" height="44"/>
|
||||
<element name="VocabularyEntry" positionX="-63" positionY="-18" width="128" height="178"/>
|
||||
</elements>
|
||||
</model>
|
||||
Reference in New Issue
Block a user