feat: 优化云盘工作区与批量文件工具
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user