feat(notes): improve notes, add search, add transcription to the topbar

This commit is contained in:
2025-12-10 14:40:35 +01:00
parent 57e130c4e3
commit b7624eae6c

View File

@@ -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) {