feat: export notes as portable Markdown

This commit is contained in:
Valdemar Lorenzen
2026-10-20 13:50:36 +02:00
parent bfdc1ced7d
commit 510cc1df1f
6 changed files with 328 additions and 5 deletions
@@ -37,6 +37,8 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
var browseNotes: (() -> Void)?
var newNote: (() -> Void)?
var deleteNote: (() -> Void)?
var copyNoteMarkdown: (() -> Void)?
var exportNoteMarkdown: (() -> Void)?
private enum ChoiceID: Equatable {
case language(String)
case note(UUID)
@@ -86,7 +88,9 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
ActionEntry(title: "OpenAI · gpt-6-luna", symbol: "checkmark", color: SunsetPalette.text),
ActionEntry(title: "Browse Notes", symbol: "list.bullet", color: SunsetPalette.text, commandKey: "P"),
ActionEntry(title: "New Note", symbol: "square.and.pencil", color: SunsetPalette.text, commandKey: "N"),
ActionEntry(title: "Delete Note", symbol: "trash", color: .systemRed)
ActionEntry(title: "Delete Note", symbol: "trash", color: .systemRed),
ActionEntry(title: "Copy as Markdown", symbol: "doc.on.doc", color: SunsetPalette.text),
ActionEntry(title: "Export as Markdown…", symbol: "square.and.arrow.up", color: SunsetPalette.text)
]
view = ActionsPanelView()
view.wantsLayer = true
@@ -259,7 +263,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
}
func showNotesActions(relativeTo anchor: NSView) {
show(title: "Notes", relativeTo: anchor, available: [12, 13, 14],
show(title: "Notes", relativeTo: anchor, available: [12, 13, 14, 15, 16],
searchAccessibilityLabel: "Search notes actions")
}
@@ -466,7 +470,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
}
private func handleCommandKey(_ key: String) -> Bool {
guard isShown, availableEntries == [12, 13, 14] else { return false }
guard isShown, availableEntries == [12, 13, 14, 15, 16] else { return false }
switch key {
case "p": close(); browseNotes?()
case "n": close(); newNote?()
@@ -540,6 +544,8 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
case 12: browseNotes?()
case 13: newNote?()
case 14: deleteNote?()
case 15: copyNoteMarkdown?()
case 16: exportNoteMarkdown?()
default: break
}
}
+48
View File
@@ -1,6 +1,7 @@
import AppKit
import ConjureKit
import Highlighter
import UniformTypeIdentifiers
@MainActor
enum NotesAppearance {
@@ -77,6 +78,8 @@ final class NotesController: NSViewController, NSTextViewDelegate {
actions.newNote = { [weak self] in self?.newNote() }
actions.browseNotes = { [weak self] in self?.browseNotes() }
actions.deleteNote = { [weak self] in self?.deleteNote() }
actions.copyNoteMarkdown = { [weak self] in self?.copyAsMarkdown() }
actions.exportNoteMarkdown = { [weak self] in self?.exportAsMarkdown() }
let tools = [
("textformat.size", "Heading"), ("bold", "Bold"), ("italic", "Italic"),
("strikethrough", "Strikethrough"), ("chevron.left.forwardslash.chevron.right", "Inline code"),
@@ -534,6 +537,51 @@ final class NotesController: NSViewController, NSTextViewDelegate {
catch { showError(error) }
}
private func copyAsMarkdown() {
actions.close(animated: false)
do {
let export = try NoteMarkdownExport(contents: editor.markdownContents)
let clipboard = NSPasteboard.general
clipboard.clearContents()
guard clipboard.setString(export.clipboardText, forType: .string) else {
throw CocoaError(.fileWriteUnknown)
}
} catch { showExportError(error) }
}
private func exportAsMarkdown() {
guard let window = view.window, window.attachedSheet == nil else { return }
actions.close(animated: false)
do {
let export = try NoteMarkdownExport(contents: editor.markdownContents)
let panel = NSSavePanel()
panel.title = "Export as Markdown"
panel.prompt = "Export"
panel.canCreateDirectories = true
panel.nameFieldStringValue = export.hasImages ? "Note" : "Note.md"
if export.hasImages {
panel.message = "Create a new folder containing note.md and its images. Choose a folder name that does not already exist."
} else {
panel.allowedContentTypes = [UTType(filenameExtension: "md") ?? .plainText]
}
panel.beginSheetModal(for: window) { [weak self] response in
guard let self else { return }
defer { self.focusEditor() }
guard response == .OK, let url = panel.url else { return }
do { try export.write(to: url) }
catch { self.showExportError(error) }
}
} catch { showExportError(error) }
}
private func showExportError(_ error: Error) {
status.isHidden = false
status.stringValue = "Couldn’t export note: \(error.localizedDescription)"
status.toolTip = status.stringValue
status.textColor = .systemRed
}
private func deleteNote() {
guard let window = view.window, window.attachedSheet == nil, let id = selectedID else { return }
actions.close(animated: false)