Compare commits
6 Commits
5994f8021f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 17f4329ff2 | |||
| b7624eae6c | |||
| 57e130c4e3 | |||
| c64dc59897 | |||
| d0c8f222ae | |||
| 61e5a13388 |
@@ -35,6 +35,7 @@ struct FieldEditorView: View {
|
|||||||
@State private var viewAppeared = false
|
@State private var viewAppeared = false
|
||||||
@State private var showingTagPicker = false
|
@State private var showingTagPicker = false
|
||||||
@State private var newTagName = ""
|
@State private var newTagName = ""
|
||||||
|
@State private var recognitionWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
@@ -174,31 +175,31 @@ struct FieldEditorView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func recognizeHandwriting(_ drawing: PKDrawing) {
|
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 {
|
guard !drawing.bounds.isEmpty else {
|
||||||
print("Drawing bounds are empty, skipping recognition")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
print("Starting recognition...")
|
// Cancel any pending recognition
|
||||||
|
recognitionWorkItem?.cancel()
|
||||||
|
|
||||||
|
// Create new work item with debounce
|
||||||
|
let workItem = DispatchWorkItem { [drawing] in
|
||||||
|
Task { @MainActor in
|
||||||
isRecognizing = true
|
isRecognizing = true
|
||||||
Task {
|
|
||||||
if let recognizedText = await HandwritingRecognizer.recognizeTextAsync(from: drawing) {
|
if let recognizedText = await HandwritingRecognizer.recognizeTextAsync(from: drawing) {
|
||||||
print("Recognition successful: \(recognizedText)")
|
|
||||||
await MainActor.run {
|
|
||||||
text = recognizedText
|
text = recognizedText
|
||||||
isRecognizing = false
|
isRecognizing = false
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
print("Recognition returned nil")
|
|
||||||
await MainActor.run {
|
|
||||||
isRecognizing = false
|
isRecognizing = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recognitionWorkItem = workItem
|
||||||
|
|
||||||
|
// Execute after 3 seconds delay (same as NotesListView)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: workItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ struct HandwritingCanvasView: UIViewRepresentable {
|
|||||||
// Store canvas in coordinator for later access
|
// Store canvas in coordinator for later access
|
||||||
context.coordinator.canvasView = canvasView
|
context.coordinator.canvasView = canvasView
|
||||||
|
|
||||||
|
// Restore saved tool if available
|
||||||
|
if let savedTool = Coordinator.savedTool {
|
||||||
|
canvasView.tool = savedTool
|
||||||
|
}
|
||||||
|
|
||||||
return canvasView
|
return canvasView
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +50,8 @@ struct HandwritingCanvasView: UIViewRepresentable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Coordinator: NSObject, PKCanvasViewDelegate {
|
class Coordinator: NSObject, PKCanvasViewDelegate {
|
||||||
|
static var savedTool: PKTool?
|
||||||
|
|
||||||
var drawing: Binding<PKDrawing>
|
var drawing: Binding<PKDrawing>
|
||||||
var onDrawingChanged: ((PKDrawing) -> Void)?
|
var onDrawingChanged: ((PKDrawing) -> Void)?
|
||||||
var canvasView: PKCanvasView?
|
var canvasView: PKCanvasView?
|
||||||
@@ -78,6 +85,9 @@ struct HandwritingCanvasView: UIViewRepresentable {
|
|||||||
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
|
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
|
||||||
drawing.wrappedValue = canvasView.drawing
|
drawing.wrappedValue = canvasView.drawing
|
||||||
onDrawingChanged?(canvasView.drawing)
|
onDrawingChanged?(canvasView.drawing)
|
||||||
|
|
||||||
|
// Save the current tool for persistence
|
||||||
|
Coordinator.savedTool = canvasView.tool
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,9 +40,13 @@ class HandwritingRecognizer {
|
|||||||
context.setFillColor(UIColor.white.cgColor)
|
context.setFillColor(UIColor.white.cgColor)
|
||||||
context.fill(CGRect(origin: .zero, size: bounds.size))
|
context.fill(CGRect(origin: .zero, size: bounds.size))
|
||||||
|
|
||||||
// Draw the PKDrawing
|
// Force light mode appearance to ensure dark ink on white background
|
||||||
let drawingImage = drawing.image(from: bounds, scale: scale)
|
let lightTraits = UITraitCollection(userInterfaceStyle: .light)
|
||||||
drawingImage.draw(at: .zero)
|
var drawingImage: UIImage!
|
||||||
|
lightTraits.performAsCurrent {
|
||||||
|
drawingImage = drawing.image(from: bounds, scale: scale)
|
||||||
|
}
|
||||||
|
drawingImage.draw(at: CGPoint.zero)
|
||||||
|
|
||||||
guard let image = UIGraphicsGetImageFromCurrentImageContext() else {
|
guard let image = UIGraphicsGetImageFromCurrentImageContext() else {
|
||||||
UIGraphicsEndImageContext()
|
UIGraphicsEndImageContext()
|
||||||
@@ -83,7 +87,7 @@ class HandwritingRecognizer {
|
|||||||
|
|
||||||
print("Recognized strings: \(recognizedStrings)")
|
print("Recognized strings: \(recognizedStrings)")
|
||||||
|
|
||||||
let recognizedText = recognizedStrings.joined(separator: " ")
|
let recognizedText = recognizedStrings.joined(separator: "\n")
|
||||||
completion(recognizedText.isEmpty ? nil : recognizedText)
|
completion(recognizedText.isEmpty ? nil : recognizedText)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,25 @@ struct NotesListView: View {
|
|||||||
private var notes: FetchedResults<Note>
|
private var notes: FetchedResults<Note>
|
||||||
|
|
||||||
@State private var selectedNote: Note?
|
@State private var selectedNote: Note?
|
||||||
|
@State private var searchText = ""
|
||||||
|
|
||||||
|
var filteredNotes: [Note] {
|
||||||
|
if searchText.isEmpty {
|
||||||
|
return Array(notes)
|
||||||
|
} else {
|
||||||
|
return notes.filter { note in
|
||||||
|
let titleMatch = note.title.localizedCaseInsensitiveContains(searchText)
|
||||||
|
let textMatch = (note.text ?? "").localizedCaseInsensitiveContains(searchText)
|
||||||
|
return titleMatch || textMatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
// Left sidebar - list of notes
|
// Left sidebar - list of notes
|
||||||
List(selection: $selectedNote) {
|
List(selection: $selectedNote) {
|
||||||
ForEach(notes, id: \.id) { note in
|
ForEach(filteredNotes, id: \.id) { note in
|
||||||
NoteRowView(note: note)
|
NoteRowView(note: note)
|
||||||
.tag(note)
|
.tag(note)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
@@ -47,6 +60,7 @@ struct NotesListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.searchable(text: $searchText, prompt: "Search notes")
|
||||||
|
|
||||||
// Right panel - note editor or placeholder
|
// Right panel - note editor or placeholder
|
||||||
if let note = selectedNote {
|
if let note = selectedNote {
|
||||||
@@ -108,7 +122,6 @@ struct NotesListView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func deleteNotes(offsets: IndexSet) {
|
private func deleteNotes(offsets: IndexSet) {
|
||||||
withAnimation {
|
|
||||||
let notesToDelete = offsets.map { notes[$0] }
|
let notesToDelete = offsets.map { notes[$0] }
|
||||||
|
|
||||||
// Clear selection if we're deleting the selected note
|
// Clear selection if we're deleting the selected note
|
||||||
@@ -116,6 +129,9 @@ struct NotesListView: View {
|
|||||||
selectedNote = nil
|
selectedNote = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Small delay to ensure UI updates before deletion
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||||
|
withAnimation {
|
||||||
// Delete the notes
|
// Delete the notes
|
||||||
notesToDelete.forEach { note in
|
notesToDelete.forEach { note in
|
||||||
viewContext.delete(note)
|
viewContext.delete(note)
|
||||||
@@ -134,6 +150,7 @@ struct NotesListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func saveContext() {
|
private func saveContext() {
|
||||||
do {
|
do {
|
||||||
@@ -159,7 +176,7 @@ struct NoteRowView: View {
|
|||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(note.title)
|
Text(note.title)
|
||||||
.font(.body)
|
.font(.body)
|
||||||
.lineLimit(2)
|
.lineLimit(1)
|
||||||
|
|
||||||
if note.timestamp != nil {
|
if note.timestamp != nil {
|
||||||
Text(timeAgo)
|
Text(timeAgo)
|
||||||
@@ -175,6 +192,8 @@ struct NoteEditorContentView: View {
|
|||||||
@ObservedObject var note: Note
|
@ObservedObject var note: Note
|
||||||
@Environment(\.managedObjectContext) private var viewContext
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
let onAddNote: () -> Void
|
let onAddNote: () -> Void
|
||||||
|
@State private var showTranscription = false
|
||||||
|
@State private var forceTranscriptionTrigger = 0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
@@ -194,6 +213,22 @@ struct NoteEditorContentView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
withAnimation {
|
||||||
|
let wasHidden = !showTranscription
|
||||||
|
showTranscription.toggle()
|
||||||
|
if wasHidden {
|
||||||
|
// Trigger immediate transcription when showing
|
||||||
|
forceTranscriptionTrigger += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Image(systemName: showTranscription ? "text.bubble.fill" : "text.bubble")
|
||||||
|
.font(.body)
|
||||||
|
.foregroundColor(showTranscription ? .blue : .primary)
|
||||||
|
}
|
||||||
|
.padding(.trailing, 8)
|
||||||
|
|
||||||
Button(action: onAddNote) {
|
Button(action: onAddNote) {
|
||||||
Image(systemName: "plus")
|
Image(systemName: "plus")
|
||||||
.font(.body)
|
.font(.body)
|
||||||
@@ -219,7 +254,9 @@ struct NoteEditorContentView: View {
|
|||||||
// Note editor content
|
// Note editor content
|
||||||
NoteEditorContentOnly(
|
NoteEditorContentOnly(
|
||||||
drawing: bindingForDrawing(),
|
drawing: bindingForDrawing(),
|
||||||
text: bindingForText()
|
text: bindingForText(),
|
||||||
|
showTranscription: $showTranscription,
|
||||||
|
forceTranscriptionTrigger: forceTranscriptionTrigger
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.background(Color(.systemGroupedBackground))
|
.background(Color(.systemGroupedBackground))
|
||||||
@@ -229,6 +266,8 @@ struct NoteEditorContentView: View {
|
|||||||
Binding(
|
Binding(
|
||||||
get: { note.pkDrawing },
|
get: { note.pkDrawing },
|
||||||
set: { newValue in
|
set: { newValue in
|
||||||
|
// Guard against modifying deleted objects
|
||||||
|
guard note.managedObjectContext != nil else { return }
|
||||||
note.pkDrawing = newValue
|
note.pkDrawing = newValue
|
||||||
saveContext()
|
saveContext()
|
||||||
}
|
}
|
||||||
@@ -239,6 +278,8 @@ struct NoteEditorContentView: View {
|
|||||||
Binding(
|
Binding(
|
||||||
get: { note.text ?? "" },
|
get: { note.text ?? "" },
|
||||||
set: { newValue in
|
set: { newValue in
|
||||||
|
// Guard against modifying deleted objects
|
||||||
|
guard note.managedObjectContext != nil else { return }
|
||||||
note.text = newValue
|
note.text = newValue
|
||||||
saveContext()
|
saveContext()
|
||||||
}
|
}
|
||||||
@@ -258,9 +299,10 @@ struct NoteEditorContentView: View {
|
|||||||
struct NoteEditorContentOnly: View {
|
struct NoteEditorContentOnly: View {
|
||||||
@Binding var drawing: PKDrawing
|
@Binding var drawing: PKDrawing
|
||||||
@Binding var text: String
|
@Binding var text: String
|
||||||
|
@Binding var showTranscription: Bool
|
||||||
|
let forceTranscriptionTrigger: Int
|
||||||
|
|
||||||
@State private var isRecognizing = false
|
@State private var isRecognizing = false
|
||||||
@State private var showTranscription = false
|
|
||||||
@State private var viewAppeared = false
|
@State private var viewAppeared = false
|
||||||
@State private var recognitionWorkItem: DispatchWorkItem?
|
@State private var recognitionWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
@@ -334,23 +376,30 @@ struct NoteEditorContentOnly: View {
|
|||||||
.padding()
|
.padding()
|
||||||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: forceTranscriptionTrigger) { _ in
|
||||||
|
forceRecognizeHandwriting()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Toggle transcription button
|
private func forceRecognizeHandwriting() {
|
||||||
if !showTranscription {
|
guard !drawing.bounds.isEmpty else {
|
||||||
Button(action: {
|
return
|
||||||
withAnimation {
|
|
||||||
showTranscription = true
|
|
||||||
}
|
}
|
||||||
}) {
|
|
||||||
HStack {
|
// Cancel any pending recognition
|
||||||
Image(systemName: "text.bubble")
|
recognitionWorkItem?.cancel()
|
||||||
Text("Show Transcription")
|
|
||||||
}
|
// Perform recognition immediately without debounce
|
||||||
.font(.subheadline)
|
Task { @MainActor in
|
||||||
.foregroundColor(.blue)
|
isRecognizing = true
|
||||||
.padding(.vertical, 8)
|
showTranscription = true // Auto-show transcription when forcing
|
||||||
}
|
|
||||||
.padding(.bottom)
|
if let recognizedText = await HandwritingRecognizer.recognizeTextAsync(from: drawing) {
|
||||||
|
text = recognizedText
|
||||||
|
isRecognizing = false
|
||||||
|
} else {
|
||||||
|
isRecognizing = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -379,7 +428,7 @@ struct NoteEditorContentOnly: View {
|
|||||||
|
|
||||||
recognitionWorkItem = workItem
|
recognitionWorkItem = workItem
|
||||||
|
|
||||||
// Execute after 800ms delay (adjust as needed)
|
// Execute after 3 seconds delay
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8, execute: workItem)
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: workItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,21 +30,47 @@ struct ScrollableCanvasView: UIViewRepresentable {
|
|||||||
// Allow finger touches to pass through for scrolling
|
// Allow finger touches to pass through for scrolling
|
||||||
canvasView.allowsFingerDrawing = false
|
canvasView.allowsFingerDrawing = false
|
||||||
|
|
||||||
// Set initial canvas size - will be updated in updateUIView with proper bounds
|
// Set initial canvas size based on drawing bounds
|
||||||
let canvasWidth: CGFloat = 800 // Placeholder width
|
let drawingBounds = drawing.bounds
|
||||||
let canvasHeight: CGFloat = 3000 // Start with a large canvas
|
let canvasWidth: CGFloat
|
||||||
|
let canvasHeight: CGFloat
|
||||||
|
|
||||||
|
if drawingBounds.isEmpty {
|
||||||
|
canvasWidth = 2000
|
||||||
|
canvasHeight = 3000
|
||||||
|
} else {
|
||||||
|
canvasWidth = max(drawingBounds.maxX + 500, 2000)
|
||||||
|
canvasHeight = max(drawingBounds.maxY + 500, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
canvasView.frame = CGRect(x: 0, y: 0, width: canvasWidth, height: canvasHeight)
|
canvasView.frame = CGRect(x: 0, y: 0, width: canvasWidth, height: canvasHeight)
|
||||||
|
|
||||||
scrollView.addSubview(canvasView)
|
scrollView.addSubview(canvasView)
|
||||||
scrollView.contentSize = canvasView.frame.size
|
scrollView.contentSize = canvasView.frame.size
|
||||||
|
|
||||||
// Allow scrolling beyond content
|
// Allow scrolling beyond content in both directions
|
||||||
scrollView.alwaysBounceVertical = true
|
scrollView.alwaysBounceVertical = true
|
||||||
|
scrollView.alwaysBounceHorizontal = true
|
||||||
|
|
||||||
|
// Enable zooming
|
||||||
|
scrollView.minimumZoomScale = 0.25 // Zoom out to 25% (see full canvas)
|
||||||
|
scrollView.maximumZoomScale = 2.0 // Zoom in to 200%
|
||||||
|
scrollView.delegate = context.coordinator
|
||||||
|
|
||||||
// Store references in coordinator
|
// Store references in coordinator
|
||||||
context.coordinator.canvasView = canvasView
|
context.coordinator.canvasView = canvasView
|
||||||
context.coordinator.scrollView = scrollView
|
context.coordinator.scrollView = scrollView
|
||||||
|
|
||||||
|
// Add double-tap gesture for zoom toggle
|
||||||
|
let doubleTapGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleDoubleTap(_:)))
|
||||||
|
doubleTapGesture.numberOfTapsRequired = 2
|
||||||
|
scrollView.addGestureRecognizer(doubleTapGesture)
|
||||||
|
|
||||||
|
// Restore saved tool if available
|
||||||
|
if let savedTool = Coordinator.savedTool {
|
||||||
|
canvasView.tool = savedTool
|
||||||
|
}
|
||||||
|
|
||||||
return scrollView
|
return scrollView
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,23 +82,24 @@ struct ScrollableCanvasView: UIViewRepresentable {
|
|||||||
}
|
}
|
||||||
canvasView.isUserInteractionEnabled = isEditable
|
canvasView.isUserInteractionEnabled = isEditable
|
||||||
|
|
||||||
// Update canvas width to match scroll view bounds (once they're valid)
|
|
||||||
let canvasWidth = scrollView.bounds.width > 0 ? scrollView.bounds.width : 800
|
|
||||||
|
|
||||||
// Expand canvas if drawing extends beyond current bounds
|
// Expand canvas if drawing extends beyond current bounds
|
||||||
let drawingBounds = drawing.bounds
|
let drawingBounds = drawing.bounds
|
||||||
|
let currentWidth = canvasView.frame.width
|
||||||
let currentHeight = canvasView.frame.height
|
let currentHeight = canvasView.frame.height
|
||||||
|
let requiredWidth: CGFloat
|
||||||
let requiredHeight: CGFloat
|
let requiredHeight: CGFloat
|
||||||
|
|
||||||
if drawingBounds.isEmpty {
|
if drawingBounds.isEmpty {
|
||||||
|
requiredWidth = 2000
|
||||||
requiredHeight = 3000
|
requiredHeight = 3000
|
||||||
} else {
|
} else {
|
||||||
|
requiredWidth = max(drawingBounds.maxX + 500, 2000) // Add padding to the right
|
||||||
requiredHeight = max(drawingBounds.maxY + 500, 3000) // Add padding below
|
requiredHeight = max(drawingBounds.maxY + 500, 3000) // Add padding below
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update canvas size if needed
|
// Update canvas size if needed
|
||||||
if requiredHeight > currentHeight || abs(canvasView.frame.width - canvasWidth) > 1 {
|
if requiredWidth > currentWidth || requiredHeight > currentHeight {
|
||||||
canvasView.frame = CGRect(x: 0, y: 0, width: canvasWidth, height: requiredHeight)
|
canvasView.frame = CGRect(x: 0, y: 0, width: requiredWidth, height: requiredHeight)
|
||||||
scrollView.contentSize = canvasView.frame.size
|
scrollView.contentSize = canvasView.frame.size
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +113,9 @@ struct ScrollableCanvasView: UIViewRepresentable {
|
|||||||
Coordinator(drawing: $drawing, onDrawingChanged: onDrawingChanged)
|
Coordinator(drawing: $drawing, onDrawingChanged: onDrawingChanged)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Coordinator: NSObject, PKCanvasViewDelegate {
|
class Coordinator: NSObject, PKCanvasViewDelegate, UIScrollViewDelegate {
|
||||||
|
static var savedTool: PKTool?
|
||||||
|
|
||||||
var drawing: Binding<PKDrawing>
|
var drawing: Binding<PKDrawing>
|
||||||
var onDrawingChanged: ((PKDrawing) -> Void)?
|
var onDrawingChanged: ((PKDrawing) -> Void)?
|
||||||
var canvasView: PKCanvasView?
|
var canvasView: PKCanvasView?
|
||||||
@@ -121,6 +150,54 @@ struct ScrollableCanvasView: UIViewRepresentable {
|
|||||||
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
|
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
|
||||||
drawing.wrappedValue = canvasView.drawing
|
drawing.wrappedValue = canvasView.drawing
|
||||||
onDrawingChanged?(canvasView.drawing)
|
onDrawingChanged?(canvasView.drawing)
|
||||||
|
|
||||||
|
// Save the current tool for persistence
|
||||||
|
Coordinator.savedTool = canvasView.tool
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIScrollViewDelegate for zooming
|
||||||
|
|
||||||
|
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||||
|
return canvasView
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
|
||||||
|
// Snap back to normal zoom (1.0) if close enough
|
||||||
|
if scale > 0.9 && scale < 1.1 {
|
||||||
|
UIView.animate(withDuration: 0.2) {
|
||||||
|
scrollView.setZoomScale(1.0, animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
|
||||||
|
guard let scrollView = scrollView else { return }
|
||||||
|
|
||||||
|
let currentScale = scrollView.zoomScale
|
||||||
|
|
||||||
|
if abs(currentScale - 1.0) < 0.01 {
|
||||||
|
// Currently at 100%, zoom in to 1.5x
|
||||||
|
let location = gesture.location(in: canvasView)
|
||||||
|
let zoomRect = zoomRect(for: 1.5, center: location)
|
||||||
|
scrollView.zoom(to: zoomRect, animated: true)
|
||||||
|
} else {
|
||||||
|
// Not at 100%, zoom back to 100%
|
||||||
|
scrollView.setZoomScale(1.0, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func zoomRect(for scale: CGFloat, center: CGPoint) -> CGRect {
|
||||||
|
guard let scrollView = scrollView else { return .zero }
|
||||||
|
|
||||||
|
var zoomRect = CGRect.zero
|
||||||
|
zoomRect.size.width = scrollView.frame.size.width / scale
|
||||||
|
zoomRect.size.height = scrollView.frame.size.height / scale
|
||||||
|
|
||||||
|
// Center on the tap location
|
||||||
|
zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0)
|
||||||
|
zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0)
|
||||||
|
|
||||||
|
return zoomRect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
WorterBuch/TranslationService.swift
Normal file
34
WorterBuch/TranslationService.swift
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
//
|
||||||
|
// TranslationService.swift
|
||||||
|
// WorterBuch
|
||||||
|
//
|
||||||
|
// Created by Claude on 07.12.2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Translation
|
||||||
|
|
||||||
|
@available(iOS 18.0, *)
|
||||||
|
class TranslationService {
|
||||||
|
static func translate(text: String, from sourceLanguage: String = "de", to targetLanguage: String = "en") async -> String? {
|
||||||
|
do {
|
||||||
|
let sourceLocale = Locale.Language(identifier: sourceLanguage)
|
||||||
|
let targetLocale = Locale.Language(identifier: targetLanguage)
|
||||||
|
|
||||||
|
print("Attempting translation from \(sourceLanguage) to \(targetLanguage)")
|
||||||
|
|
||||||
|
// Create session - requires language pack to be installed
|
||||||
|
let session = TranslationSession(installedSource: sourceLocale, target: targetLocale)
|
||||||
|
|
||||||
|
let response = try await session.translate(text)
|
||||||
|
return response.targetText
|
||||||
|
} catch {
|
||||||
|
print("Translation error: \(error)")
|
||||||
|
print("To enable translation:")
|
||||||
|
print("1. Open the Translate app on your iPad")
|
||||||
|
print("2. Download German language pack")
|
||||||
|
print("3. Try the translate button again")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ struct VocabularyFieldCell: View {
|
|||||||
let drawing: PKDrawing
|
let drawing: PKDrawing
|
||||||
let text: String
|
let text: String
|
||||||
let onTap: () -> Void
|
let onTap: () -> Void
|
||||||
|
var showTranslateButton: Bool = false
|
||||||
|
var onTranslate: (() -> Void)? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
@@ -41,6 +43,7 @@ struct VocabularyFieldCell: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Transcribed text - selectable but not editable
|
// Transcribed text - selectable but not editable
|
||||||
|
HStack(spacing: 8) {
|
||||||
if !text.isEmpty {
|
if !text.isEmpty {
|
||||||
SelectableTextView(
|
SelectableTextView(
|
||||||
text: text.lowercased(),
|
text: text.lowercased(),
|
||||||
@@ -55,6 +58,16 @@ struct VocabularyFieldCell: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.frame(height: 40)
|
.frame(height: 40)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if showTranslateButton, let onTranslate = onTranslate {
|
||||||
|
Button(action: onTranslate) {
|
||||||
|
Image(systemName: "arrow.right.arrow.left.circle.fill")
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
.font(.system(size: 20))
|
||||||
|
}
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(8)
|
.padding(8)
|
||||||
.background(Color(.systemBackground))
|
.background(Color(.systemBackground))
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ struct VocabularyGridView: View {
|
|||||||
entry: entry,
|
entry: entry,
|
||||||
onSelectField: { fieldType in
|
onSelectField: { fieldType in
|
||||||
openFieldEditor(for: entry, fieldType: fieldType)
|
openFieldEditor(for: entry, fieldType: fieldType)
|
||||||
}
|
},
|
||||||
|
onTranslate: translateEntry
|
||||||
)
|
)
|
||||||
.listRowInsets(EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12))
|
.listRowInsets(EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12))
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
@@ -211,11 +212,35 @@ struct VocabularyGridView: View {
|
|||||||
print("Error saving context: \(nsError), \(nsError.userInfo)")
|
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 {
|
struct VocabularyEntryRow: View {
|
||||||
@ObservedObject var entry: VocabularyEntry
|
@ObservedObject var entry: VocabularyEntry
|
||||||
let onSelectField: (FieldType) -> Void
|
let onSelectField: (FieldType) -> Void
|
||||||
|
let onTranslate: (VocabularyEntry) -> Void
|
||||||
|
|
||||||
|
var shouldShowTranslateButton: Bool {
|
||||||
|
!(entry.germanWordText?.isEmpty ?? true) &&
|
||||||
|
(entry.englishTranslationText?.isEmpty ?? true)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
@@ -239,7 +264,9 @@ struct VocabularyEntryRow: View {
|
|||||||
VocabularyFieldCell(
|
VocabularyFieldCell(
|
||||||
drawing: entry.englishTranslationPKDrawing,
|
drawing: entry.englishTranslationPKDrawing,
|
||||||
text: entry.englishTranslationText ?? "",
|
text: entry.englishTranslationText ?? "",
|
||||||
onTap: { onSelectField(.englishTranslation) }
|
onTap: { onSelectField(.englishTranslation) },
|
||||||
|
showTranslateButton: shouldShowTranslateButton,
|
||||||
|
onTranslate: { onTranslate(entry) }
|
||||||
)
|
)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user