Sapling/Sources/SaplingApp/SaplingApp.swift

157 lines
4.3 KiB
Swift
Raw Normal View History

2026-05-29 15:19:33 +02:00
import SwiftUI
import SaplingCore
import SaplingWorkspace
import SaplingGit
import SaplingStorage
import SaplingEditor
import SaplingRenderer
import SaplingUI
@main
struct SaplingApplication: App {
@StateObject private var model = SaplingAppModel()
var body: some Scene {
WindowGroup {
MainWindow(model: model)
}
#if os(macOS)
.windowStyle(.titleBar)
.windowToolbarStyle(.unified)
#endif
}
}
@MainActor
private final class SaplingAppModel: ObservableObject {
@Published var workspace: Workspace
@Published var selectedProject: Project?
@Published var selectedDocument: MarkdownDocument?
@Published var editorViewModel: HybridMarkdownEditorViewModel?
@Published var gitStatuses: [GitFileStatus] = []
private let gitProvider: any GitProvider
private let workspaceManager: any WorkspaceManaging
init() {
let gitProvider = MockGitProvider()
self.gitProvider = gitProvider
self.workspaceManager = LocalWorkspaceManager(
gitProvider: gitProvider,
metadataStore: InMemoryWorkspaceMetadataStore()
)
let workspace = workspaceManager.sampleWorkspace()
self.workspace = workspace
self.selectedProject = workspace.firstProject
setSelectedDocument(SaplingSampleData.document)
Task {
await refreshGitStatus()
}
}
func select(file: WorkspaceFile) {
guard file.kind == .markdown else { return }
if file.url == SaplingSampleData.document.url {
setSelectedDocument(SaplingSampleData.document)
} else {
setSelectedDocument(MarkdownDocument(
url: file.url,
title: file.name,
content: "# \(file.name)\n\nTODO: Load document contents from disk."
))
}
}
func select(project: Project) {
selectedProject = project
Task {
await refreshGitStatus()
}
}
private func refreshGitStatus() async {
guard let selectedProject else {
gitStatuses = []
return
}
do {
let repository = gitProvider.repository(at: selectedProject.repositoryURL)
gitStatuses = try await repository.status()
} catch {
gitStatuses = [
GitFileStatus(path: "Unable to load status: \(error)", state: .conflicted)
]
}
}
private func setSelectedDocument(_ document: MarkdownDocument) {
selectedDocument = document
editorViewModel = HybridMarkdownEditorViewModel(document: document)
}
}
private struct MainWindow: View {
@ObservedObject var model: SaplingAppModel
var body: some View {
NavigationSplitView {
WorkspaceTreeView(
workspace: model.workspace,
onSelectFile: model.select(file:),
onSelectProject: model.select(project:)
)
.frame(minWidth: 220)
} content: {
if let editorViewModel = model.editorViewModel {
HybridMarkdownEditor(
viewModel: editorViewModel,
renderer: MarkdownRenderer()
)
.navigationTitle(editorViewModel.document.title)
} else {
EmptyEditorPlaceholder()
}
} detail: {
SaplingInspectorView(
project: model.selectedProject,
document: model.selectedDocument,
statuses: model.gitStatuses
)
.frame(minWidth: 260)
}
}
}
private extension Workspace {
var firstProject: Project? {
for item in items {
if let project = item.firstProject {
return project
}
}
return nil
}
}
private extension WorkspaceItem {
var firstProject: Project? {
switch self {
case .project(let project):
return project
case .folder(let folder):
for child in folder.children {
if let project = child.firstProject {
return project
}
}
return nil
case .file, .subproject:
return nil
}
}
}