feat(notes): improve notes, add search, add transcription to the topbar
This commit is contained in:
@@ -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,28 +122,31 @@ 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
|
||||||
if let selected = selectedNote, notesToDelete.contains(where: { $0.id == selected.id }) {
|
if let selected = selectedNote, notesToDelete.contains(where: { $0.id == selected.id }) {
|
||||||
selectedNote = nil
|
selectedNote = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the notes
|
// Small delay to ensure UI updates before deletion
|
||||||
notesToDelete.forEach { note in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||||
viewContext.delete(note)
|
withAnimation {
|
||||||
}
|
// Delete the notes
|
||||||
|
notesToDelete.forEach { note in
|
||||||
|
viewContext.delete(note)
|
||||||
|
}
|
||||||
|
|
||||||
saveContext()
|
saveContext()
|
||||||
|
|
||||||
// After deletion, if no notes are left, ensure selection is nil
|
// After deletion, if no notes are left, ensure selection is nil
|
||||||
// Otherwise, select the first remaining note if nothing is selected
|
// Otherwise, select the first remaining note if nothing is selected
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if notes.isEmpty {
|
if notes.isEmpty {
|
||||||
selectedNote = nil
|
selectedNote = nil
|
||||||
} else if selectedNote == nil && !notes.isEmpty {
|
} else if selectedNote == nil && !notes.isEmpty {
|
||||||
selectedNote = notes.first
|
selectedNote = notes.first
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,6 +193,7 @@ struct NoteEditorContentView: View {
|
|||||||
@Environment(\.managedObjectContext) private var viewContext
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
let onAddNote: () -> Void
|
let onAddNote: () -> Void
|
||||||
@State private var showTranscription = false
|
@State private var showTranscription = false
|
||||||
|
@State private var forceTranscriptionTrigger = 0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
@@ -197,7 +215,12 @@ struct NoteEditorContentView: View {
|
|||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
|
let wasHidden = !showTranscription
|
||||||
showTranscription.toggle()
|
showTranscription.toggle()
|
||||||
|
if wasHidden {
|
||||||
|
// Trigger immediate transcription when showing
|
||||||
|
forceTranscriptionTrigger += 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Image(systemName: showTranscription ? "text.bubble.fill" : "text.bubble")
|
Image(systemName: showTranscription ? "text.bubble.fill" : "text.bubble")
|
||||||
@@ -232,7 +255,8 @@ struct NoteEditorContentView: View {
|
|||||||
NoteEditorContentOnly(
|
NoteEditorContentOnly(
|
||||||
drawing: bindingForDrawing(),
|
drawing: bindingForDrawing(),
|
||||||
text: bindingForText(),
|
text: bindingForText(),
|
||||||
showTranscription: $showTranscription
|
showTranscription: $showTranscription,
|
||||||
|
forceTranscriptionTrigger: forceTranscriptionTrigger
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.background(Color(.systemGroupedBackground))
|
.background(Color(.systemGroupedBackground))
|
||||||
@@ -242,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()
|
||||||
}
|
}
|
||||||
@@ -252,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()
|
||||||
}
|
}
|
||||||
@@ -272,6 +300,7 @@ struct NoteEditorContentOnly: View {
|
|||||||
@Binding var drawing: PKDrawing
|
@Binding var drawing: PKDrawing
|
||||||
@Binding var text: String
|
@Binding var text: String
|
||||||
@Binding var showTranscription: Bool
|
@Binding var showTranscription: Bool
|
||||||
|
let forceTranscriptionTrigger: Int
|
||||||
|
|
||||||
@State private var isRecognizing = false
|
@State private var isRecognizing = false
|
||||||
@State private var viewAppeared = false
|
@State private var viewAppeared = false
|
||||||
@@ -348,6 +377,31 @@ struct NoteEditorContentOnly: View {
|
|||||||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
.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) {
|
private func recognizeHandwriting(_ drawing: PKDrawing) {
|
||||||
|
|||||||
Reference in New Issue
Block a user