feat(worterbuch): basic app implementation
This commit is contained in:
@@ -9,78 +9,11 @@ import SwiftUI
|
|||||||
import CoreData
|
import CoreData
|
||||||
|
|
||||||
struct ContentView: View {
|
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 {
|
var body: some View {
|
||||||
NavigationView {
|
VocabularyGridView()
|
||||||
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)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let itemFormatter: DateFormatter = {
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateStyle = .short
|
|
||||||
formatter.timeStyle = .medium
|
|
||||||
return formatter
|
|
||||||
}()
|
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
|
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"?>
|
<?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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict/>
|
||||||
<key>UIBackgroundModes</key>
|
|
||||||
<array>
|
|
||||||
<string>remote-notification</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -14,15 +14,17 @@ struct PersistenceController {
|
|||||||
static let preview: PersistenceController = {
|
static let preview: PersistenceController = {
|
||||||
let result = PersistenceController(inMemory: true)
|
let result = PersistenceController(inMemory: true)
|
||||||
let viewContext = result.container.viewContext
|
let viewContext = result.container.viewContext
|
||||||
for _ in 0..<10 {
|
for i in 0..<5 {
|
||||||
let newItem = Item(context: viewContext)
|
let entry = VocabularyEntry.create(in: viewContext)
|
||||||
newItem.timestamp = Date()
|
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 {
|
do {
|
||||||
try viewContext.save()
|
try viewContext.save()
|
||||||
} catch {
|
} 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
|
let nsError = error as NSError
|
||||||
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
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"?>
|
<?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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<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>
|
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?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="">
|
<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="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>
|
</entity>
|
||||||
<elements>
|
<elements>
|
||||||
<element name="Item" positionX="-63" positionY="-18" width="128" height="44"/>
|
<element name="VocabularyEntry" positionX="-63" positionY="-18" width="128" height="178"/>
|
||||||
</elements>
|
</elements>
|
||||||
</model>
|
</model>
|
||||||
Reference in New Issue
Block a user