127 lines
5.3 KiB
Swift
127 lines
5.3 KiB
Swift
import SwiftUI
|
|
import UIKit
|
|
|
|
struct GitAwareFileNavigatorView: View {
|
|
let tree: FileNode?
|
|
let fileStatuses: [GitFileStatus]
|
|
let onSelect: (FileNode) -> Void
|
|
var onCreateFile: ((String) -> Void)? = nil
|
|
var onCreateFolder: ((String) -> Void)? = nil
|
|
var onRename: ((FileNode) -> Void)? = nil
|
|
var onDelete: ((FileNode) -> Void)? = nil
|
|
|
|
private var statusMap: [String: GitFileStatus] {
|
|
Dictionary(uniqueKeysWithValues: fileStatuses.map { ($0.path, $0) })
|
|
}
|
|
|
|
var body: some View {
|
|
Group {
|
|
if let tree {
|
|
List {
|
|
GitAwareNodeRow(node: tree, statusMap: statusMap, depth: 0,
|
|
onSelect: onSelect, onCreateFile: onCreateFile,
|
|
onCreateFolder: onCreateFolder,
|
|
onRename: onRename, onDelete: onDelete)
|
|
}
|
|
.listStyle(.sidebar)
|
|
} else {
|
|
ContentUnavailableView("No Project Loaded",
|
|
systemImage: "folder.badge.questionmark",
|
|
description: Text("Check your daemon connection."))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct GitAwareNodeRow: View {
|
|
let node: FileNode
|
|
let statusMap: [String: GitFileStatus]
|
|
let depth: Int
|
|
let onSelect: (FileNode) -> Void
|
|
var onCreateFile: ((String) -> Void)?
|
|
var onCreateFolder: ((String) -> Void)?
|
|
var onRename: ((FileNode) -> Void)?
|
|
var onDelete: ((FileNode) -> Void)?
|
|
|
|
@State private var isExpanded = true
|
|
@State private var showDeleteAlert = false
|
|
|
|
private var fileStatus: GitFileStatus? {
|
|
statusMap[node.path] ?? statusMap.values.first { node.path.hasSuffix("/" + $0.path) }
|
|
}
|
|
|
|
var body: some View {
|
|
if node.isDirectory {
|
|
DisclosureGroup(isExpanded: $isExpanded) {
|
|
ForEach(node.children ?? []) { child in
|
|
GitAwareNodeRow(node: child, statusMap: statusMap, depth: depth + 1,
|
|
onSelect: onSelect, onCreateFile: onCreateFile,
|
|
onCreateFolder: onCreateFolder,
|
|
onRename: onRename, onDelete: onDelete)
|
|
}
|
|
} label: {
|
|
Label(node.name, systemImage: isExpanded ? "folder.fill" : "folder")
|
|
.font(.body)
|
|
}
|
|
.contextMenu { directoryContextMenu }
|
|
} else {
|
|
Button { onSelect(node) } label: {
|
|
HStack(spacing: 6) {
|
|
Image(systemName: fileIcon(for: node.name))
|
|
.font(.system(size: 12)).foregroundStyle(iconColor(for: node.name)).frame(width: 16)
|
|
Text(node.name)
|
|
.font(.system(.body, design: .monospaced))
|
|
.foregroundStyle(fileStatus == nil ? .primary : fileStatus!.statusSwiftUIColor)
|
|
Spacer()
|
|
if let s = fileStatus {
|
|
Text(badge(s.status))
|
|
.font(.system(.caption2, design: .monospaced).bold())
|
|
.foregroundStyle(s.statusSwiftUIColor)
|
|
}
|
|
}
|
|
.contentShape(Rectangle())
|
|
}
|
|
.buttonStyle(.plain)
|
|
.contextMenu { fileContextMenu }
|
|
.alert("Delete \"\(node.name)\"?", isPresented: $showDeleteAlert) {
|
|
Button("Delete", role: .destructive) { onDelete?(node) }
|
|
Button("Cancel", role: .cancel) {}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func badge(_ status: String) -> String {
|
|
switch status { case "modified": return "M"; case "added": return "A"
|
|
case "deleted": return "D"; case "untracked": return "?"; default: return "" }
|
|
}
|
|
|
|
private func fileIcon(for name: String) -> String {
|
|
switch (name as NSString).pathExtension.lowercased() {
|
|
case "swift": return "swift"; case "json": return "curlybraces"
|
|
case "md": return "doc.text"; case "sh": return "terminal"
|
|
case "xcconfig": return "gearshape"; default: return "doc"
|
|
}
|
|
}
|
|
private func iconColor(for name: String) -> Color {
|
|
switch (name as NSString).pathExtension.lowercased() {
|
|
case "swift": return .orange; case "json": return .yellow
|
|
case "md": return .blue; default: return .secondary
|
|
}
|
|
}
|
|
|
|
@ViewBuilder private var directoryContextMenu: some View {
|
|
Button { onCreateFile?(node.path) } label: { Label("New File", systemImage: "doc.badge.plus") }
|
|
Button { onCreateFolder?(node.path) } label: { Label("New Folder", systemImage: "folder.badge.plus") }
|
|
Divider()
|
|
Button { onRename?(node) } label: { Label("Rename…", systemImage: "pencil") }
|
|
Divider()
|
|
Button(role: .destructive) { showDeleteAlert = true } label: { Label("Delete", systemImage: "trash") }
|
|
}
|
|
|
|
@ViewBuilder private var fileContextMenu: some View {
|
|
Button { onSelect(node) } label: { Label("Open", systemImage: "arrow.up.right.square") }
|
|
Button { onRename?(node) } label: { Label("Rename…", systemImage: "pencil") }
|
|
Divider()
|
|
Button(role: .destructive) { showDeleteAlert = true } label: { Label("Delete", systemImage: "trash") }
|
|
}
|
|
}
|