Compare commits
2 Commits
57e130c4e3
...
17f4329ff2
| Author | SHA1 | Date | |
|---|---|---|---|
| 17f4329ff2 | |||
| b7624eae6c |
@@ -25,6 +25,11 @@ struct HandwritingCanvasView: UIViewRepresentable {
|
||||
// Store canvas in coordinator for later access
|
||||
context.coordinator.canvasView = canvasView
|
||||
|
||||
// Restore saved tool if available
|
||||
if let savedTool = Coordinator.savedTool {
|
||||
canvasView.tool = savedTool
|
||||
}
|
||||
|
||||
return canvasView
|
||||
}
|
||||
|
||||
@@ -45,6 +50,8 @@ struct HandwritingCanvasView: UIViewRepresentable {
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, PKCanvasViewDelegate {
|
||||
static var savedTool: PKTool?
|
||||
|
||||
var drawing: Binding<PKDrawing>
|
||||
var onDrawingChanged: ((PKDrawing) -> Void)?
|
||||
var canvasView: PKCanvasView?
|
||||
@@ -78,6 +85,9 @@ struct HandwritingCanvasView: UIViewRepresentable {
|
||||
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
|
||||
drawing.wrappedValue = canvasView.drawing
|
||||
onDrawingChanged?(canvasView.drawing)
|
||||
|
||||
// Save the current tool for persistence
|
||||
Coordinator.savedTool = canvasView.tool
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,25 @@ struct NotesListView: View {
|
||||
private var notes: FetchedResults<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 {
|
||||
NavigationView {
|
||||
// Left sidebar - list of notes
|
||||
List(selection: $selectedNote) {
|
||||
ForEach(notes, id: \.id) { note in
|
||||
ForEach(filteredNotes, id: \.id) { note in
|
||||
NoteRowView(note: note)
|
||||
.tag(note)
|
||||
.onTapGesture {
|
||||
@@ -47,6 +60,7 @@ struct NotesListView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.searchable(text: $searchText, prompt: "Search notes")
|
||||
|
||||
// Right panel - note editor or placeholder
|
||||
if let note = selectedNote {
|
||||
@@ -108,28 +122,31 @@ struct NotesListView: View {
|
||||
}
|
||||
|
||||
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
|
||||
if let selected = selectedNote, notesToDelete.contains(where: { $0.id == selected.id }) {
|
||||
selectedNote = nil
|
||||
}
|
||||
// Clear selection if we're deleting the selected note
|
||||
if let selected = selectedNote, notesToDelete.contains(where: { $0.id == selected.id }) {
|
||||
selectedNote = nil
|
||||
}
|
||||
|
||||
// Delete the notes
|
||||
notesToDelete.forEach { note in
|
||||
viewContext.delete(note)
|
||||
}
|
||||
// Small delay to ensure UI updates before deletion
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||
withAnimation {
|
||||
// Delete the notes
|
||||
notesToDelete.forEach { note in
|
||||
viewContext.delete(note)
|
||||
}
|
||||
|
||||
saveContext()
|
||||
saveContext()
|
||||
|
||||
// After deletion, if no notes are left, ensure selection is nil
|
||||
// Otherwise, select the first remaining note if nothing is selected
|
||||
DispatchQueue.main.async {
|
||||
if notes.isEmpty {
|
||||
selectedNote = nil
|
||||
} else if selectedNote == nil && !notes.isEmpty {
|
||||
selectedNote = notes.first
|
||||
// After deletion, if no notes are left, ensure selection is nil
|
||||
// Otherwise, select the first remaining note if nothing is selected
|
||||
DispatchQueue.main.async {
|
||||
if notes.isEmpty {
|
||||
selectedNote = nil
|
||||
} else if selectedNote == nil && !notes.isEmpty {
|
||||
selectedNote = notes.first
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,6 +193,7 @@ struct NoteEditorContentView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
let onAddNote: () -> Void
|
||||
@State private var showTranscription = false
|
||||
@State private var forceTranscriptionTrigger = 0
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
@@ -197,7 +215,12 @@ struct NoteEditorContentView: View {
|
||||
|
||||
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")
|
||||
@@ -232,7 +255,8 @@ struct NoteEditorContentView: View {
|
||||
NoteEditorContentOnly(
|
||||
drawing: bindingForDrawing(),
|
||||
text: bindingForText(),
|
||||
showTranscription: $showTranscription
|
||||
showTranscription: $showTranscription,
|
||||
forceTranscriptionTrigger: forceTranscriptionTrigger
|
||||
)
|
||||
}
|
||||
.background(Color(.systemGroupedBackground))
|
||||
@@ -242,6 +266,8 @@ struct NoteEditorContentView: View {
|
||||
Binding(
|
||||
get: { note.pkDrawing },
|
||||
set: { newValue in
|
||||
// Guard against modifying deleted objects
|
||||
guard note.managedObjectContext != nil else { return }
|
||||
note.pkDrawing = newValue
|
||||
saveContext()
|
||||
}
|
||||
@@ -252,6 +278,8 @@ struct NoteEditorContentView: View {
|
||||
Binding(
|
||||
get: { note.text ?? "" },
|
||||
set: { newValue in
|
||||
// Guard against modifying deleted objects
|
||||
guard note.managedObjectContext != nil else { return }
|
||||
note.text = newValue
|
||||
saveContext()
|
||||
}
|
||||
@@ -272,6 +300,7 @@ struct NoteEditorContentOnly: View {
|
||||
@Binding var drawing: PKDrawing
|
||||
@Binding var text: String
|
||||
@Binding var showTranscription: Bool
|
||||
let forceTranscriptionTrigger: Int
|
||||
|
||||
@State private var isRecognizing = false
|
||||
@State private var viewAppeared = false
|
||||
@@ -348,6 +377,31 @@ struct NoteEditorContentOnly: View {
|
||||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
.onChange(of: forceTranscriptionTrigger) { _ in
|
||||
forceRecognizeHandwriting()
|
||||
}
|
||||
}
|
||||
|
||||
private func forceRecognizeHandwriting() {
|
||||
guard !drawing.bounds.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
// Cancel any pending recognition
|
||||
recognitionWorkItem?.cancel()
|
||||
|
||||
// Perform recognition immediately without debounce
|
||||
Task { @MainActor in
|
||||
isRecognizing = true
|
||||
showTranscription = true // Auto-show transcription when forcing
|
||||
|
||||
if let recognizedText = await HandwritingRecognizer.recognizeTextAsync(from: drawing) {
|
||||
text = recognizedText
|
||||
isRecognizing = false
|
||||
} else {
|
||||
isRecognizing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func recognizeHandwriting(_ drawing: PKDrawing) {
|
||||
|
||||
@@ -30,21 +30,47 @@ struct ScrollableCanvasView: UIViewRepresentable {
|
||||
// Allow finger touches to pass through for scrolling
|
||||
canvasView.allowsFingerDrawing = false
|
||||
|
||||
// Set initial canvas size - will be updated in updateUIView with proper bounds
|
||||
let canvasWidth: CGFloat = 800 // Placeholder width
|
||||
let canvasHeight: CGFloat = 3000 // Start with a large canvas
|
||||
// Set initial canvas size based on drawing bounds
|
||||
let drawingBounds = drawing.bounds
|
||||
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)
|
||||
|
||||
scrollView.addSubview(canvasView)
|
||||
scrollView.contentSize = canvasView.frame.size
|
||||
|
||||
// Allow scrolling beyond content
|
||||
// Allow scrolling beyond content in both directions
|
||||
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
|
||||
context.coordinator.canvasView = canvasView
|
||||
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
|
||||
}
|
||||
|
||||
@@ -56,23 +82,24 @@ struct ScrollableCanvasView: UIViewRepresentable {
|
||||
}
|
||||
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
|
||||
let drawingBounds = drawing.bounds
|
||||
let currentWidth = canvasView.frame.width
|
||||
let currentHeight = canvasView.frame.height
|
||||
let requiredWidth: CGFloat
|
||||
let requiredHeight: CGFloat
|
||||
|
||||
if drawingBounds.isEmpty {
|
||||
requiredWidth = 2000
|
||||
requiredHeight = 3000
|
||||
} else {
|
||||
requiredWidth = max(drawingBounds.maxX + 500, 2000) // Add padding to the right
|
||||
requiredHeight = max(drawingBounds.maxY + 500, 3000) // Add padding below
|
||||
}
|
||||
|
||||
// Update canvas size if needed
|
||||
if requiredHeight > currentHeight || abs(canvasView.frame.width - canvasWidth) > 1 {
|
||||
canvasView.frame = CGRect(x: 0, y: 0, width: canvasWidth, height: requiredHeight)
|
||||
if requiredWidth > currentWidth || requiredHeight > currentHeight {
|
||||
canvasView.frame = CGRect(x: 0, y: 0, width: requiredWidth, height: requiredHeight)
|
||||
scrollView.contentSize = canvasView.frame.size
|
||||
}
|
||||
|
||||
@@ -86,7 +113,9 @@ struct ScrollableCanvasView: UIViewRepresentable {
|
||||
Coordinator(drawing: $drawing, onDrawingChanged: onDrawingChanged)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, PKCanvasViewDelegate {
|
||||
class Coordinator: NSObject, PKCanvasViewDelegate, UIScrollViewDelegate {
|
||||
static var savedTool: PKTool?
|
||||
|
||||
var drawing: Binding<PKDrawing>
|
||||
var onDrawingChanged: ((PKDrawing) -> Void)?
|
||||
var canvasView: PKCanvasView?
|
||||
@@ -121,6 +150,54 @@ struct ScrollableCanvasView: UIViewRepresentable {
|
||||
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
|
||||
drawing.wrappedValue = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user