Sapling/Sources/SaplingUI/WorkspaceTreeView.swift

151 lines
5.2 KiB
Swift

import SwiftUI
import SaplingCore
public enum WorkspaceTreeSelection: Hashable, Sendable {
case folder(URL)
case file(URL)
case project(URL)
case subproject(URL)
}
public struct WorkspaceTreeView: View {
private let workspace: Workspace?
@Binding private var selection: WorkspaceTreeSelection?
private let onSelectFile: (WorkspaceFile) -> Void
private let onSelectProject: (Project) -> Void
private let onOpenWorkspace: () -> Void
public init(
workspace: Workspace?,
selection: Binding<WorkspaceTreeSelection?>,
onSelectFile: @escaping (WorkspaceFile) -> Void,
onSelectProject: @escaping (Project) -> Void,
onOpenWorkspace: @escaping () -> Void
) {
self.workspace = workspace
self._selection = selection
self.onSelectFile = onSelectFile
self.onSelectProject = onSelectProject
self.onOpenWorkspace = onOpenWorkspace
}
public var body: some View {
Group {
if let workspace {
List(selection: $selection) {
Section(workspace.name) {
ForEach(workspace.items, id: \.stableTreeID) { item in
WorkspaceItemRow(
item: item,
selection: $selection,
onSelectFile: onSelectFile,
onSelectProject: onSelectProject
)
}
}
}
.listStyle(.sidebar)
} else {
ContentUnavailableView {
Label("No Workspace", systemImage: "folder")
} description: {
Text("Choose a folder to browse Markdown files.")
} actions: {
Button("Open Workspace...", action: onOpenWorkspace)
}
}
}
.navigationTitle("Workspace")
}
}
private struct WorkspaceItemRow: View {
let item: WorkspaceItem
@Binding var selection: WorkspaceTreeSelection?
let onSelectFile: (WorkspaceFile) -> Void
let onSelectProject: (Project) -> Void
var body: some View {
switch item {
case .folder(let folder):
DisclosureGroup {
ForEach(folder.children, id: \.stableTreeID) { child in
WorkspaceItemRow(
item: child,
selection: $selection,
onSelectFile: onSelectFile,
onSelectProject: onSelectProject
)
}
} label: {
Label(folder.name, systemImage: "folder")
.contentShape(Rectangle())
.onTapGesture {
selection = .folder(folder.url)
}
}
.tag(WorkspaceTreeSelection.folder(folder.url))
case .file(let file):
Button {
if file.kind == .markdown {
selection = .file(file.url)
onSelectFile(file)
}
} label: {
Label(file.name, systemImage: iconName(for: file.kind))
.foregroundStyle(file.kind == .markdown ? .primary : .secondary)
}
.buttonStyle(.plain)
.disabled(file.kind != .markdown)
.tag(WorkspaceTreeSelection.file(file.url))
case .project(let project):
DisclosureGroup {
ForEach(project.children, id: \.stableTreeID) { child in
WorkspaceItemRow(
item: child,
selection: $selection,
onSelectFile: onSelectFile,
onSelectProject: onSelectProject
)
}
} label: {
Label(project.name, systemImage: "leaf")
.fontWeight(.medium)
.foregroundStyle(.primary)
.contentShape(Rectangle())
.onTapGesture {
selection = .project(project.repositoryURL)
onSelectProject(project)
}
}
.tag(WorkspaceTreeSelection.project(project.repositoryURL))
case .subproject(let subproject):
Label(subproject.name, systemImage: "rectangle.connected.to.line.below")
.foregroundStyle(.secondary)
.tag(WorkspaceTreeSelection.subproject(subproject.repositoryURL))
}
}
private func iconName(for kind: WorkspaceFileKind) -> String {
switch kind {
case .markdown: "doc.plaintext"
case .attachment: "paperclip"
case .other: "doc"
}
}
}
private extension WorkspaceItem {
var stableTreeID: URL {
switch self {
case .folder(let folder):
return folder.url.standardizedFileURL
case .file(let file):
return file.url.standardizedFileURL
case .project(let project):
return project.repositoryURL.standardizedFileURL
case .subproject(let subproject):
return subproject.repositoryURL.standardizedFileURL
}
}
}