feat(editor): implement document loading and saving
This commit is contained in:
parent
93d095feeb
commit
1e15778364
1 changed files with 94 additions and 7 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import UniformTypeIdentifiers
|
||||||
import SaplingCore
|
import SaplingCore
|
||||||
import SaplingWorkspace
|
import SaplingWorkspace
|
||||||
import SaplingGit
|
import SaplingGit
|
||||||
|
|
@ -40,6 +41,7 @@ private final class SaplingAppModel: ObservableObject {
|
||||||
@Published var editorViewModel: HybridMarkdownEditorViewModel?
|
@Published var editorViewModel: HybridMarkdownEditorViewModel?
|
||||||
@Published var gitStatuses: [GitFileStatus] = []
|
@Published var gitStatuses: [GitFileStatus] = []
|
||||||
@Published var configuration: SaplingConfiguration
|
@Published var configuration: SaplingConfiguration
|
||||||
|
@Published var editorErrorMessage: String?
|
||||||
|
|
||||||
private let gitProvider: any GitProvider
|
private let gitProvider: any GitProvider
|
||||||
private let configurationStore: any ConfigurationStore
|
private let configurationStore: any ConfigurationStore
|
||||||
|
|
@ -71,11 +73,40 @@ private final class SaplingAppModel: ObservableObject {
|
||||||
if file.url == SaplingSampleData.document.url {
|
if file.url == SaplingSampleData.document.url {
|
||||||
setSelectedDocument(SaplingSampleData.document)
|
setSelectedDocument(SaplingSampleData.document)
|
||||||
} else {
|
} else {
|
||||||
setSelectedDocument(MarkdownDocument(
|
openDocument(at: file.url)
|
||||||
url: file.url,
|
}
|
||||||
title: file.name,
|
}
|
||||||
content: "# \(file.name)\n\nTODO: Load document contents from disk."
|
|
||||||
))
|
func openDocument(at url: URL) {
|
||||||
|
let didStartAccessing = url.startAccessingSecurityScopedResource()
|
||||||
|
defer {
|
||||||
|
if didStartAccessing {
|
||||||
|
url.stopAccessingSecurityScopedResource()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let document = try HybridMarkdownEditorViewModel.loadDocument(at: url)
|
||||||
|
setSelectedDocument(document)
|
||||||
|
editorErrorMessage = nil
|
||||||
|
logger.info("Opened document: \(document.title)", category: .editor)
|
||||||
|
} catch {
|
||||||
|
editorErrorMessage = "Unable to open \(url.lastPathComponent): \(error.localizedDescription)"
|
||||||
|
logger.error("Failed to open document: \(error)", category: .editor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveSelectedDocument() {
|
||||||
|
guard let editorViewModel else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
try editorViewModel.save()
|
||||||
|
selectedDocument = editorViewModel.document
|
||||||
|
editorErrorMessage = nil
|
||||||
|
logger.info("Saved document: \(editorViewModel.document.title)", category: .editor)
|
||||||
|
} catch {
|
||||||
|
editorErrorMessage = "Unable to save \(editorViewModel.document.title): \(error.localizedDescription)"
|
||||||
|
logger.error("Failed to save document: \(error)", category: .editor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,6 +154,7 @@ private final class SaplingAppModel: ObservableObject {
|
||||||
|
|
||||||
private struct MainWindow: View {
|
private struct MainWindow: View {
|
||||||
@ObservedObject var model: SaplingAppModel
|
@ObservedObject var model: SaplingAppModel
|
||||||
|
@State private var isImportingMarkdown = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationSplitView {
|
NavigationSplitView {
|
||||||
|
|
@ -138,18 +170,73 @@ private struct MainWindow: View {
|
||||||
viewModel: editorViewModel,
|
viewModel: editorViewModel,
|
||||||
renderer: MarkdownRenderer()
|
renderer: MarkdownRenderer()
|
||||||
)
|
)
|
||||||
.navigationTitle(editorViewModel.document.title)
|
.navigationTitle(navigationTitle(for: editorViewModel))
|
||||||
} else {
|
} else {
|
||||||
EmptyEditorPlaceholder()
|
EmptyEditorPlaceholder()
|
||||||
}
|
}
|
||||||
} detail: {
|
} detail: {
|
||||||
SaplingInspectorView(
|
SaplingInspectorView(
|
||||||
project: model.selectedProject,
|
project: model.selectedProject,
|
||||||
document: model.selectedDocument,
|
document: model.editorViewModel?.document ?? model.selectedDocument,
|
||||||
statuses: model.gitStatuses
|
statuses: model.gitStatuses
|
||||||
)
|
)
|
||||||
.frame(minWidth: 260)
|
.frame(minWidth: 260)
|
||||||
}
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItemGroup {
|
||||||
|
Button {
|
||||||
|
isImportingMarkdown = true
|
||||||
|
} label: {
|
||||||
|
Label("Open Markdown", systemImage: "doc.badge.plus")
|
||||||
|
}
|
||||||
|
.help("Open Markdown")
|
||||||
|
|
||||||
|
Button {
|
||||||
|
model.saveSelectedDocument()
|
||||||
|
} label: {
|
||||||
|
Label("Save", systemImage: "square.and.arrow.down")
|
||||||
|
}
|
||||||
|
.help("Save")
|
||||||
|
.disabled(model.editorViewModel?.hasUnsavedChanges != true)
|
||||||
|
.keyboardShortcut("s", modifiers: .command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fileImporter(
|
||||||
|
isPresented: $isImportingMarkdown,
|
||||||
|
allowedContentTypes: [.saplingMarkdown, .plainText],
|
||||||
|
allowsMultipleSelection: false
|
||||||
|
) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let urls):
|
||||||
|
guard let url = urls.first else { return }
|
||||||
|
model.openDocument(at: url)
|
||||||
|
case .failure(let error):
|
||||||
|
model.editorErrorMessage = "Unable to open document: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert(
|
||||||
|
"Editor Error",
|
||||||
|
isPresented: Binding(
|
||||||
|
get: { model.editorErrorMessage != nil },
|
||||||
|
set: { if !$0 { model.editorErrorMessage = nil } }
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Button("OK", role: .cancel) {
|
||||||
|
model.editorErrorMessage = nil
|
||||||
|
}
|
||||||
|
} message: {
|
||||||
|
Text(model.editorErrorMessage ?? "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func navigationTitle(for viewModel: HybridMarkdownEditorViewModel) -> String {
|
||||||
|
viewModel.hasUnsavedChanges ? "\(viewModel.document.title) *" : viewModel.document.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension UTType {
|
||||||
|
static var saplingMarkdown: UTType {
|
||||||
|
UTType(filenameExtension: "md") ?? .plainText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue