202 lines
7.3 KiB
Swift
202 lines
7.3 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?
|
|
private let isLoadingWorkspace: Bool
|
|
@Binding private var selection: WorkspaceTreeSelection?
|
|
private let childrenFor: (WorkspaceItem) -> [WorkspaceItem]
|
|
private let isLoadingChildren: (WorkspaceItem) -> Bool
|
|
private let onExpandItem: (WorkspaceItem) -> Void
|
|
private let onSelectFile: (WorkspaceFile) -> Void
|
|
private let onSelectProject: (Project) -> Void
|
|
private let onOpenWorkspace: () -> Void
|
|
|
|
public init(
|
|
workspace: Workspace?,
|
|
isLoadingWorkspace: Bool = false,
|
|
selection: Binding<WorkspaceTreeSelection?>,
|
|
childrenFor: @escaping (WorkspaceItem) -> [WorkspaceItem] = { $0.children ?? [] },
|
|
isLoadingChildren: @escaping (WorkspaceItem) -> Bool = { _ in false },
|
|
onExpandItem: @escaping (WorkspaceItem) -> Void = { _ in },
|
|
onSelectFile: @escaping (WorkspaceFile) -> Void,
|
|
onSelectProject: @escaping (Project) -> Void,
|
|
onOpenWorkspace: @escaping () -> Void
|
|
) {
|
|
self.workspace = workspace
|
|
self.isLoadingWorkspace = isLoadingWorkspace
|
|
self._selection = selection
|
|
self.childrenFor = childrenFor
|
|
self.isLoadingChildren = isLoadingChildren
|
|
self.onExpandItem = onExpandItem
|
|
self.onSelectFile = onSelectFile
|
|
self.onSelectProject = onSelectProject
|
|
self.onOpenWorkspace = onOpenWorkspace
|
|
}
|
|
|
|
public var body: some View {
|
|
Group {
|
|
if let workspace {
|
|
List(selection: $selection) {
|
|
if isLoadingWorkspace {
|
|
Label("Scanning Workspace...", systemImage: "progress.indicator")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Section(workspace.name) {
|
|
ForEach(workspace.items, id: \.stableTreeID) { item in
|
|
WorkspaceItemRow(
|
|
item: item,
|
|
selection: $selection,
|
|
childrenFor: childrenFor,
|
|
isLoadingChildren: isLoadingChildren,
|
|
onExpandItem: onExpandItem,
|
|
onSelectFile: onSelectFile,
|
|
onSelectProject: onSelectProject
|
|
)
|
|
}
|
|
}
|
|
}
|
|
.listStyle(.sidebar)
|
|
} else {
|
|
ContentUnavailableView {
|
|
Label(isLoadingWorkspace ? "Loading Workspace" : "No Workspace", systemImage: "folder")
|
|
} description: {
|
|
Text(isLoadingWorkspace ? "Scanning Workspace..." : "Choose a folder to browse Markdown files.")
|
|
} actions: {
|
|
if isLoadingWorkspace {
|
|
ProgressView()
|
|
} else {
|
|
Button("Open Workspace...", action: onOpenWorkspace)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Workspace")
|
|
.transaction { transaction in
|
|
transaction.animation = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct WorkspaceItemRow: View {
|
|
let item: WorkspaceItem
|
|
@Binding var selection: WorkspaceTreeSelection?
|
|
let childrenFor: (WorkspaceItem) -> [WorkspaceItem]
|
|
let isLoadingChildren: (WorkspaceItem) -> Bool
|
|
let onExpandItem: (WorkspaceItem) -> Void
|
|
let onSelectFile: (WorkspaceFile) -> Void
|
|
let onSelectProject: (Project) -> Void
|
|
@State private var isExpanded = false
|
|
|
|
var body: some View {
|
|
switch item {
|
|
case .folder(let folder):
|
|
DisclosureGroup(isExpanded: expansionBinding) {
|
|
children
|
|
} label: {
|
|
Label(folder.name, systemImage: "folder")
|
|
.contentShape(Rectangle())
|
|
.onTapGesture {
|
|
selection = .folder(folder.url)
|
|
}
|
|
}
|
|
.tag(WorkspaceTreeSelection.folder(folder.url))
|
|
case .file(let file):
|
|
Button {
|
|
selection = .file(file.url)
|
|
if file.kind == .markdown {
|
|
onSelectFile(file)
|
|
}
|
|
} label: {
|
|
Label(file.name, systemImage: iconName(for: file.kind))
|
|
.foregroundStyle(file.kind == .markdown ? .primary : .secondary)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.tag(WorkspaceTreeSelection.file(file.url))
|
|
case .project(let project):
|
|
DisclosureGroup(isExpanded: expansionBinding) {
|
|
children
|
|
} 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 var expansionBinding: Binding<Bool> {
|
|
Binding(
|
|
get: { isExpanded },
|
|
set: { newValue in
|
|
var transaction = Transaction()
|
|
transaction.animation = nil
|
|
withTransaction(transaction) {
|
|
isExpanded = newValue
|
|
}
|
|
if newValue {
|
|
onExpandItem(item)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var children: some View {
|
|
if isLoadingChildren(item) {
|
|
Label("Loading...", systemImage: "progress.indicator")
|
|
.foregroundStyle(.secondary)
|
|
} else {
|
|
ForEach(childrenFor(item), id: \.stableTreeID) { child in
|
|
WorkspaceItemRow(
|
|
item: child,
|
|
selection: $selection,
|
|
childrenFor: childrenFor,
|
|
isLoadingChildren: isLoadingChildren,
|
|
onExpandItem: onExpandItem,
|
|
onSelectFile: onSelectFile,
|
|
onSelectProject: onSelectProject
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|