151 lines
5.2 KiB
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
|
|
}
|
|
}
|
|
}
|