Commit 176fc78498
Verified · cmc
Layout: unified · split
Sources/Orgstar/ContentView.swift +64 −5
| @@ -46,7 +46,7 @@ struct ContentView: View { | ||
| 46 | 46 | |
| 47 | 47 | private var main: some View { |
| 48 | 48 | NavigationSplitView { |
| 49 | Sidebar(workspace: workspace, selection: $selectedFile) | |
| 49 | Sidebar(workspace: workspace, selection: $selectedFile) { commands.showingFolderPicker = true } | |
| 50 | 50 | .navigationSplitViewColumnWidth(min: 200, ideal: 260) |
| 51 | 51 | } content: { |
| 52 | 52 | Group { |
| @@ -372,23 +372,82 @@ struct ContentView: View { | ||
| 372 | 372 | struct Sidebar: View { |
| 373 | 373 | let workspace: WorkspaceModel |
| 374 | 374 | @Binding var selection: String? |
| 375 | var addFolder: () -> Void = {} | |
| 376 | /// Folders shown collapsed, by path, one per line. | |
| 377 | @AppStorage("collapsedFolders") private var collapsedText = "" | |
| 378 | @State private var removing: String? | |
| 379 | ||
| 380 | private var collapsed: Set<String> { Set(collapsedText.split(separator: "\n").map(String.init)) } | |
| 381 | ||
| 382 | private func expanded(_ root: String) -> Binding<Bool> { | |
| 383 | Binding { | |
| 384 | !collapsed.contains(root) | |
| 385 | } set: { isExpanded in | |
| 386 | var set = collapsed | |
| 387 | if isExpanded { set.remove(root) } else { set.insert(root) } | |
| 388 | collapsedText = set.sorted().joined(separator: "\n") | |
| 389 | } | |
| 390 | } | |
| 375 | 391 | |
| 376 | 392 | var body: some View { |
| 377 | 393 | List(selection: $selection) { |
| 378 | 394 | ForEach(workspace.rootPaths, id: \.self) { root in |
| 379 | Section((root as NSString).lastPathComponent) { | |
| 395 | Section(isExpanded: expanded(root)) { | |
| 380 | 396 | OutlineGroup(workspace.tree(forRoot: root).children ?? [], children: \.children) { node in |
| 381 | 397 | Label(node.name, systemImage: icon(node)) |
| 382 | 398 | .foregroundStyle(node.file?.kind == .conflict ? .orange : .primary) |
| 383 | 399 | .tag(node.file?.path) |
| 384 | 400 | } |
| 385 | } | |
| 386 | .contextMenu { | |
| 387 | Button("Remove Folder") { try? workspace.removeRoot(path: root) } | |
| 401 | } header: { | |
| 402 | Text((root as NSString).lastPathComponent) | |
| 403 | .help(root) | |
| 404 | .contextMenu { folderMenu(root) } | |
| 388 | 405 | } |
| 389 | 406 | } |
| 390 | 407 | } |
| 391 | 408 | .listStyle(.sidebar) |
| 409 | .safeAreaInset(edge: .bottom, spacing: 0) { | |
| 410 | HStack(spacing: 4) { | |
| 411 | Button("Add Folder…", systemImage: "plus", action: addFolder) | |
| 412 | .help("Add a folder (⇧⌘O)") | |
| 413 | Menu { | |
| 414 | ForEach(workspace.rootPaths, id: \.self) { root in | |
| 415 | Button((root as NSString).lastPathComponent) { removing = root } | |
| 416 | } | |
| 417 | } label: { | |
| 418 | Label("Remove Folder", systemImage: "minus") | |
| 419 | } | |
| 420 | .menuIndicator(.hidden) | |
| 421 | .fixedSize() | |
| 422 | .disabled(workspace.rootPaths.isEmpty) | |
| 423 | .help("Remove a folder from the sidebar") | |
| 424 | Spacer() | |
| 425 | } | |
| 426 | .labelStyle(.iconOnly) | |
| 427 | .buttonStyle(.borderless) | |
| 428 | .menuStyle(.borderlessButton) | |
| 429 | .padding(.horizontal, 10) | |
| 430 | .padding(.vertical, 6) | |
| 431 | } | |
| 432 | .confirmationDialog( | |
| 433 | "Remove “\(removing.map { ($0 as NSString).lastPathComponent } ?? "")” from the sidebar?", | |
| 434 | isPresented: Binding { removing != nil } set: { if !$0 { removing = nil } } | |
| 435 | ) { | |
| 436 | Button("Remove") { | |
| 437 | if let root = removing { try? workspace.removeRoot(path: root) } | |
| 438 | removing = nil | |
| 439 | } | |
| 440 | } message: { | |
| 441 | Text("The folder and its files stay on disk.") | |
| 442 | } | |
| 443 | } | |
| 444 | ||
| 445 | @ViewBuilder | |
| 446 | private func folderMenu(_ root: String) -> some View { | |
| 447 | Button(collapsed.contains(root) ? "Expand" : "Collapse") { expanded(root).wrappedValue.toggle() } | |
| 448 | Button("Show in Finder") { NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: root)]) } | |
| 449 | Divider() | |
| 450 | Button("Remove from Sidebar…") { removing = root } | |
| 392 | 451 | } |
| 393 | 452 | |
| 394 | 453 | private func icon(_ node: FileTreeNode) -> String { |