Files
guangya_client/guangya_mac/WorkspaceShell.swift
T

168 lines
7.2 KiB
Swift

import SwiftUI
import UniformTypeIdentifiers
enum WorkspaceTool: String, CaseIterable, Identifiable {
case scan = "文件扫描与清理"
case rename = "批量重命名"
case tmdb = "TMDB 整理"
case settings = "工作区设置"
var id: String { rawValue }
var icon: String {
switch self {
case .scan: return "magnifyingglass.circle.fill"
case .rename: return "character.cursor.ibeam"
case .tmdb: return "film.stack"
case .settings: return "gearshape"
}
}
var subtitle: String {
switch self {
case .scan: return "空文件夹、重复文件与相似文件夹统一扫描"
case .rename: return "按字符、正则和组合规则批量修改名称"
case .tmdb: return "配置并执行媒体识别整理"
case .settings: return "代理和本机工具配置"
}
}
}
struct WorkspaceView: View {
@ObservedObject var model: AppModel
@State private var selectedFileIDs: Set<String> = []
@State private var renameFile: CloudFile?
@State private var isCreatingFolder = false
@State private var isImporting = false
@State private var isShowingSettings = false
@State private var isShowingDuplicates = false
@State private var isInspectorPresented = false
@State private var selectedTool: WorkspaceTool?
@State private var folderName = ""
@State private var columnVisibility: NavigationSplitViewVisibility = .all
@State private var previewFile: CloudFile?
@State private var globalSearchText = ""
private var selectedFile: CloudFile? {
selectedFileIDs.count == 1 ? model.files.first { $0.id == selectedFileIDs.first } : nil
}
var body: some View {
ZStack {
AppBackdrop()
NavigationSplitView(columnVisibility: $columnVisibility) {
WorkspaceSidebar(model: model, selectedTool: $selectedTool, columnVisibility: $columnVisibility)
.navigationSplitViewColumnWidth(min: 210, ideal: 238, max: 280)
} detail: {
if let selectedTool {
ToolWorkspacePage(model: model, tool: selectedTool)
} else {
FilesBrowser(
model: model,
selectedFileIDs: $selectedFileIDs,
globalSearchText: $globalSearchText,
renameFile: $renameFile,
isCreatingFolder: $isCreatingFolder,
isImporting: $isImporting,
isShowingSettings: $isShowingSettings,
isShowingDuplicates: $isShowingDuplicates,
onPreview: { previewFile = $0 },
onOpenTool: { selectedTool = $0 },
onShowInspector: { isInspectorPresented = true }
)
.frame(minWidth: 680, maxWidth: .infinity, maxHeight: .infinity)
.inspector(isPresented: $isInspectorPresented) {
FileInspector(
model: model,
file: selectedFile,
onPreview: { previewFile = $0 },
onClose: { isInspectorPresented = false }
)
.inspectorColumnWidth(min: 280, ideal: 320, max: 420)
}
}
}
.scrollContentBackground(.hidden)
.toolbar {
ToolbarItem(placement: .principal) {
AppTitleToolbar(model: model, selectedTool: selectedTool, searchText: $globalSearchText)
}
ToolbarItem(placement: .primaryAction) {
if selectedTool == nil {
Button { isInspectorPresented.toggle() } label: {
Image(systemName: "sidebar.trailing")
}
.help(isInspectorPresented ? "隐藏详情检查器" : "显示详情检查器")
.accessibilityLabel(isInspectorPresented ? "隐藏详情检查器" : "显示详情检查器")
.keyboardShortcut("i", modifiers: [.command, .option])
.disabled(selectedFile == nil)
}
}
}
}
.frame(minWidth: 1040, minHeight: 700)
.sheet(item: $renameFile) { file in RenameSheet(file: file) { name in Task { await model.rename(file, to: name) } } }
.sheet(isPresented: $isCreatingFolder) {
CreateFolderSheet(name: $folderName) { name in
isCreatingFolder = false
folderName = ""
Task { await model.createFolder(name: name) }
} onCancel: { isCreatingFolder = false; folderName = "" }
}
.sheet(isPresented: $isShowingSettings) { WorkspaceSettings(model: model) }
.sheet(isPresented: $isShowingDuplicates) { DuplicateResults(model: model) }
.sheet(item: $previewFile) { file in FilePreviewSheet(model: model, file: file) }
.fileImporter(isPresented: $isImporting, allowedContentTypes: [.item], allowsMultipleSelection: false) { result in
switch result {
case .success(let urls): if let url = urls.first { Task { await model.upload(url: url) } }
case .failure(let error): model.errorMessage = error.localizedDescription
}
}
.alert("提示", isPresented: Binding(get: { !model.lastActionMessage.isEmpty }, set: { if !$0 { model.lastActionMessage = "" } })) {
Button("确定", role: .cancel) { model.lastActionMessage = "" }
} message: { Text(model.lastActionMessage) }
.task {
if model.files.isEmpty && !model.isLoadingFiles { await model.refresh() }
}
.onChange(of: selectedTool) { _, tool in
if tool != nil { isInspectorPresented = false }
}
.onChange(of: model.section) { _, _ in
selectedFileIDs = []
selectedTool = nil
isInspectorPresented = false
globalSearchText = ""
Task { await model.refresh() }
}
.onChange(of: model.files.map(\.id)) { _, ids in
selectedFileIDs.formIntersection(Set(ids))
if selectedFileIDs.isEmpty { isInspectorPresented = false }
}
.onChange(of: model.folderPath) { _, _ in
selectedFileIDs = []
isInspectorPresented = false
globalSearchText = ""
}
}
}
private struct AppTitleToolbar: View {
@ObservedObject var model: AppModel
let selectedTool: WorkspaceTool?
@Binding var searchText: String
private var supportsSearch: Bool {
selectedTool == nil && [.files, .photos, .videos, .audio, .documents].contains(model.section)
}
var body: some View {
HStack(spacing: 12) {
Label(selectedTool?.rawValue ?? model.section.rawValue, systemImage: selectedTool?.icon ?? model.section.icon)
.font(.headline.weight(.semibold))
if supportsSearch {
TextField("搜索当前列表", text: $searchText)
.textFieldStyle(.roundedBorder)
.frame(width: 260)
}
}
}
}