feat: edit a transcript in the Notitie editor, delete button per transcript
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,8 @@ final class AppModel: ObservableObject {
|
|||||||
@Published private(set) var historyRevision = 0
|
@Published private(set) var historyRevision = 0
|
||||||
@Published private(set) var dictionaryRevision = 0
|
@Published private(set) var dictionaryRevision = 0
|
||||||
@Published private(set) var searchFocusTick = 0
|
@Published private(set) var searchFocusTick = 0
|
||||||
|
/// The transcript currently open in the Notitie editor, if any.
|
||||||
|
@Published var editingEntryID: UUID?
|
||||||
@Published var lastError: String?
|
@Published var lastError: String?
|
||||||
@Published var section: Section = .transcripts
|
@Published var section: Section = .transcripts
|
||||||
@Published var confirmingClearHistory = false
|
@Published var confirmingClearHistory = false
|
||||||
@@ -375,6 +377,12 @@ final class AppModel: ObservableObject {
|
|||||||
|
|
||||||
// MARK: preview
|
// MARK: preview
|
||||||
|
|
||||||
|
/// Opens a transcript in the editor; edits go straight back into the history.
|
||||||
|
func edit(entry id: UUID) {
|
||||||
|
editingEntryID = id
|
||||||
|
section = .note
|
||||||
|
}
|
||||||
|
|
||||||
private func loadPreviewData() {
|
private func loadPreviewData() {
|
||||||
note.text = "Notities voor vrijdag: Supabase-migratie plannen en Claude Code uitproberen."
|
note.text = "Notities voor vrijdag: Supabase-migratie plannen en Claude Code uitproberen."
|
||||||
try? dictionary.load()
|
try? dictionary.load()
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ struct HistoryView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private struct HistoryRow: View {
|
private struct HistoryRow: View {
|
||||||
|
@EnvironmentObject private var model: AppModel
|
||||||
let entry: HistoryEntry
|
let entry: HistoryEntry
|
||||||
let isSelected: Bool
|
let isSelected: Bool
|
||||||
@ObservedObject var list: HistoryViewModel
|
@ObservedObject var list: HistoryViewModel
|
||||||
@@ -90,6 +91,10 @@ private struct HistoryRow: View {
|
|||||||
Spacer(minLength: Space.s4)
|
Spacer(minLength: Space.s4)
|
||||||
Button(Strings.copy) { copy(entry.finalText) }
|
Button(Strings.copy) { copy(entry.finalText) }
|
||||||
.buttonStyle(KeyButtonStyle())
|
.buttonStyle(KeyButtonStyle())
|
||||||
|
Button(Strings.edit) { model.edit(entry: entry.id) }
|
||||||
|
.buttonStyle(KeyButtonStyle())
|
||||||
|
Button(Strings.delete) { list.delete(id: entry.id) }
|
||||||
|
.buttonStyle(KeyButtonStyle())
|
||||||
}
|
}
|
||||||
.padding(.horizontal, Space.s4)
|
.padding(.horizontal, Space.s4)
|
||||||
.padding(.vertical, Space.s3)
|
.padding(.vertical, Space.s3)
|
||||||
@@ -100,6 +105,7 @@ private struct HistoryRow: View {
|
|||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button(Strings.copy) { copy(entry.finalText) }
|
Button(Strings.copy) { copy(entry.finalText) }
|
||||||
Button(Strings.copyOriginal) { copy(entry.rawText) }
|
Button(Strings.copyOriginal) { copy(entry.rawText) }
|
||||||
|
Button(Strings.edit) { model.edit(entry: entry.id) }
|
||||||
Divider()
|
Divider()
|
||||||
Button(Strings.delete, role: .destructive) { list.delete(id: entry.id) }
|
Button(Strings.delete, role: .destructive) { list.delete(id: entry.id) }
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-10
@@ -8,6 +8,20 @@ struct NoteView: View {
|
|||||||
@ObservedObject var note: NoteStore
|
@ObservedObject var note: NoteStore
|
||||||
@State private var confirmingClear = false
|
@State private var confirmingClear = false
|
||||||
|
|
||||||
|
private var editedEntry: HistoryEntry? {
|
||||||
|
model.editingEntryID.flatMap { model.historyList.entry(id: $0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The note, or the transcript being edited.
|
||||||
|
private var text: Binding<String> {
|
||||||
|
if let entry = editedEntry {
|
||||||
|
return Binding(
|
||||||
|
get: { model.historyList.entry(id: entry.id)?.finalText ?? "" },
|
||||||
|
set: { model.historyList.updateText(id: entry.id, to: $0) })
|
||||||
|
}
|
||||||
|
return $note.text
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: Layout.panelGap) {
|
VStack(spacing: Layout.panelGap) {
|
||||||
header
|
header
|
||||||
@@ -15,13 +29,14 @@ struct NoteView: View {
|
|||||||
PaperBackground()
|
PaperBackground()
|
||||||
if SnapshotMode.isActive {
|
if SnapshotMode.isActive {
|
||||||
// ImageRenderer cannot draw an NSView; the snapshot shows the text instead.
|
// ImageRenderer cannot draw an NSView; the snapshot shows the text instead.
|
||||||
Text(note.text)
|
Text(text.wrappedValue)
|
||||||
.textStyle(Typography.reading)
|
.textStyle(Typography.reading)
|
||||||
.foregroundStyle(Palette.inkPrimary)
|
.foregroundStyle(Palette.inkPrimary)
|
||||||
.padding(Space.s5)
|
.padding(Space.s5)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||||
} else {
|
} else {
|
||||||
NoteEditor(text: $note.text, isEditable: !note.isReadOnly)
|
NoteEditor(text: text, isEditable: editedEntry != nil || !note.isReadOnly)
|
||||||
|
.id(editedEntry?.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
footer
|
footer
|
||||||
@@ -29,31 +44,51 @@ struct NoteView: View {
|
|||||||
.confirmationDialog(Strings.clearNoteConfirm, isPresented: $confirmingClear) {
|
.confirmationDialog(Strings.clearNoteConfirm, isPresented: $confirmingClear) {
|
||||||
Button(Strings.clear, role: .destructive) { note.text = "" }
|
Button(Strings.clear, role: .destructive) { note.text = "" }
|
||||||
}
|
}
|
||||||
|
.onChange(of: model.section) { _, new in
|
||||||
|
if new != .note { model.editingEntryID = nil }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var header: some View {
|
private var header: some View {
|
||||||
HStack(spacing: Space.s4) {
|
HStack(spacing: Space.s4) {
|
||||||
SilkLabel(Strings.note)
|
if let entry = editedEntry {
|
||||||
|
SilkLabel("\(Strings.transcript) · \(entry.date.formatted(Date.FormatStyle().hour().minute().second().locale(Locale(identifier: "nl_NL"))))")
|
||||||
|
} else {
|
||||||
|
SilkLabel(Strings.note)
|
||||||
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(Strings.characters(note.text.count))
|
Text(Strings.characters(text.wrappedValue.count))
|
||||||
.textStyle(Typography.counterSmall)
|
.textStyle(Typography.counterSmall)
|
||||||
.foregroundStyle(Palette.inkSecondary)
|
.foregroundStyle(Palette.inkSecondary)
|
||||||
Button(Strings.copy) {
|
Button(Strings.copy) {
|
||||||
NSPasteboard.general.clearContents()
|
NSPasteboard.general.clearContents()
|
||||||
NSPasteboard.general.setString(note.text, forType: .string)
|
NSPasteboard.general.setString(text.wrappedValue, forType: .string)
|
||||||
}
|
}
|
||||||
.buttonStyle(KeyButtonStyle())
|
.buttonStyle(KeyButtonStyle())
|
||||||
.disabled(note.text.isEmpty)
|
.disabled(text.wrappedValue.isEmpty)
|
||||||
Button(Strings.clear) { confirmingClear = true }
|
if editedEntry != nil {
|
||||||
.buttonStyle(KeyButtonStyle())
|
Button(Strings.done) {
|
||||||
.disabled(note.text.isEmpty)
|
model.editingEntryID = nil
|
||||||
|
model.section = .transcripts
|
||||||
|
}
|
||||||
|
.buttonStyle(KeyButtonStyle(kind: .record))
|
||||||
|
} else {
|
||||||
|
Button(Strings.clear) { confirmingClear = true }
|
||||||
|
.buttonStyle(KeyButtonStyle())
|
||||||
|
.disabled(note.text.isEmpty)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var footer: some View {
|
private var footer: some View {
|
||||||
Text(note.isReadOnly ? Strings.noteUnreadable : Strings.noteHint(model.hotkeyBinding.displayName))
|
Text(footerText)
|
||||||
.textStyle(Typography.label)
|
.textStyle(Typography.label)
|
||||||
.foregroundStyle(Palette.inkTertiary)
|
.foregroundStyle(Palette.inkTertiary)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var footerText: String {
|
||||||
|
if editedEntry != nil { return Strings.editingTranscriptHint }
|
||||||
|
return note.isReadOnly ? Strings.noteUnreadable : Strings.noteHint(model.hotkeyBinding.displayName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ enum Strings {
|
|||||||
// note
|
// note
|
||||||
static let note = "Notitie"
|
static let note = "Notitie"
|
||||||
static let clear = "Wis"
|
static let clear = "Wis"
|
||||||
|
static let edit = "Bewerk"
|
||||||
|
static let done = "Klaar"
|
||||||
|
static let transcript = "Transcriptie"
|
||||||
|
static let editingTranscriptHint = "Wijzigingen worden direct in de transcriptie bewaard."
|
||||||
static func characters(_ count: Int) -> String { count == 1 ? "1 teken" : "\(count) tekens" }
|
static func characters(_ count: Int) -> String { count == 1 ? "1 teken" : "\(count) tekens" }
|
||||||
static func noteHint(_ keyName: String) -> String { "Zet de cursor in het vak en houd \(keyName) ingedrukt om hier te dicteren." }
|
static func noteHint(_ keyName: String) -> String { "Zet de cursor in het vak en houd \(keyName) ingedrukt om hier te dicteren." }
|
||||||
static let clearNoteConfirm = "Notitie wissen?"
|
static let clearNoteConfirm = "Notitie wissen?"
|
||||||
|
|||||||
@@ -108,6 +108,14 @@ public final class HistoryStore {
|
|||||||
changed()
|
changed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replaces the final text of an entry. The highlighted corrections no longer match an edited text, so they are dropped.
|
||||||
|
public func updateText(id: UUID, to text: String) {
|
||||||
|
guard let index = entries.firstIndex(where: { $0.id == id }), entries[index].finalText != text else { return }
|
||||||
|
entries[index].finalText = text
|
||||||
|
entries[index].corrections = []
|
||||||
|
changed()
|
||||||
|
}
|
||||||
|
|
||||||
public func removeAll() {
|
public func removeAll() {
|
||||||
entries = []
|
entries = []
|
||||||
changed()
|
changed()
|
||||||
|
|||||||
@@ -78,6 +78,15 @@ public final class HistoryViewModel: ObservableObject {
|
|||||||
store.remove(id: id)
|
store.remove(id: id)
|
||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func entry(id: UUID) -> HistoryEntry? {
|
||||||
|
store.entries.first { $0.id == id }
|
||||||
|
}
|
||||||
|
|
||||||
|
public func updateText(id: UUID, to text: String) {
|
||||||
|
store.updateText(id: id, to: text)
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dictionary rows with warnings, search, editing and undo.
|
/// Dictionary rows with warnings, search, editing and undo.
|
||||||
|
|||||||
@@ -146,3 +146,24 @@ struct HistoryGroupingTests {
|
|||||||
#expect(days[0].day > days[1].day)
|
#expect(days[0].day > days[1].day)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct HistoryEditTests {
|
||||||
|
@Test func updateTextReplacesFinalTextAndDropsCorrections() {
|
||||||
|
let store = HistoryStore(fileURL: FileManager.default.temporaryDirectory.appending(path: "sst-tests-\(UUID().uuidString)/history.json"))
|
||||||
|
let item = HistoryEntry(
|
||||||
|
date: Date(), duration: 2, rawText: "cloud code", finalText: "Claude Code",
|
||||||
|
corrections: [AppliedCorrection(heard: "cloud code", wrote: "Claude Code", rule: "cloud code", start: 0, length: 11)])
|
||||||
|
store.add(item)
|
||||||
|
store.updateText(id: item.id, to: "Claude Code is fijn")
|
||||||
|
#expect(store.entries[0].finalText == "Claude Code is fijn")
|
||||||
|
#expect(store.entries[0].rawText == "cloud code")
|
||||||
|
#expect(store.entries[0].corrections.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func updateTextOfUnknownIdIsHarmless() {
|
||||||
|
let store = HistoryStore(fileURL: FileManager.default.temporaryDirectory.appending(path: "sst-tests-\(UUID().uuidString)/history.json"))
|
||||||
|
store.updateText(id: UUID(), to: "x")
|
||||||
|
#expect(store.entries.isEmpty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user