171 lines
7.4 KiB
Swift
171 lines
7.4 KiB
Swift
import SwiftUI
|
|
import UniformTypeIdentifiers
|
|
|
|
enum WorkspaceTool: String, CaseIterable, Identifiable {
|
|
case media = "影视库"
|
|
case scan = "文件扫描与清理"
|
|
case rename = "批量重命名"
|
|
case tmdb = "TMDB 整理"
|
|
case settings = "工作区设置"
|
|
|
|
var id: String { rawValue }
|
|
var icon: String {
|
|
switch self {
|
|
case .media: return "play.tv.fill"
|
|
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 .media: return "海报墙、媒体详情与播放入口"
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|