mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 19:36:36 +08:00
186 lines
7.1 KiB
TypeScript
186 lines
7.1 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
|
|
import { filesAPI, type FileEntry, type FileListing } from '../api/files'
|
|
import { libraryAPI } from '../api/library'
|
|
import type { Library } from '../types'
|
|
import { settingOn } from './autoOrganizeModel'
|
|
import { AutoOrganizeSettingsPanel } from './AutoOrganizeSettingsPanel'
|
|
import { FileBrowserRoots } from './FileBrowserRoots'
|
|
import { FileEntriesTable } from './FileEntriesTable'
|
|
import { FileManagerToolbar } from './FileManagerToolbar'
|
|
import { FileOperationsPanel } from './FileOperationsPanel'
|
|
import { ManualOrganizePanel } from './ManualOrganizePanel'
|
|
import { useAutoOrganizeSettings } from './useAutoOrganizeSettings'
|
|
import { useFileOperations } from './useFileOperations'
|
|
import { isCloudLibraryPath } from './fileManagerModel'
|
|
import { useManualOrganize } from './useManualOrganize'
|
|
|
|
// FileManagerPage provides a focused local storage view:
|
|
// browse allowed roots, optionally recurse, and perform safe local operations.
|
|
export function FileManagerPage() {
|
|
const [libraries, setLibraries] = useState<Library[]>([])
|
|
const [path, setPath] = useState('')
|
|
const [data, setData] = useState<FileListing | null>(null)
|
|
const [error, setError] = useState('')
|
|
const [loading, setLoading] = useState(true)
|
|
const [recursive, setRecursive] = useState(false)
|
|
const [scrapeAfter, setScrapeAfter] = useState(true)
|
|
const autoOrganize = useAutoOrganizeSettings({ onScrapeAfterChange: setScrapeAfter })
|
|
|
|
const currentDir = useMemo(() => {
|
|
if (data?.path) return data.path
|
|
return path
|
|
}, [data?.path, path])
|
|
const localLibraries = useMemo(
|
|
() => libraries.filter((library) => !isCloudLibraryPath(library.path)),
|
|
[libraries],
|
|
)
|
|
const autoMoveKeepsSeeding = autoOrganize.moveKeepsSeeding
|
|
|
|
const refresh = useCallback(() => {
|
|
setLoading(true)
|
|
setError('')
|
|
filesAPI
|
|
.list(path, recursive ? 5000 : 1000, recursive)
|
|
.then(setData)
|
|
.catch((err: unknown) => {
|
|
const msg =
|
|
(err as { response?: { data?: { error?: string } } })?.response?.data?.error ??
|
|
'加载失败'
|
|
setError(msg)
|
|
})
|
|
.finally(() => setLoading(false))
|
|
}, [path, recursive])
|
|
|
|
useEffect(() => {
|
|
refresh()
|
|
}, [refresh])
|
|
|
|
useEffect(() => {
|
|
libraryAPI.list({ includeHidden: true }).then(setLibraries).catch(() => undefined)
|
|
}, [])
|
|
|
|
const fileOperations = useFileOperations({ currentDir, path, refresh })
|
|
const manualOrganize = useManualOrganize({
|
|
currentDir,
|
|
localLibraries,
|
|
selectedPath: fileOperations.selected?.path,
|
|
selectedPaths: fileOperations.selectedPaths,
|
|
scrapeAfter,
|
|
keepSeeding: settingOn(autoOrganize.config.keepSeeding),
|
|
onScrapeAfterChange: setScrapeAfter,
|
|
onClearSelected: () => fileOperations.setSelectedPaths([]),
|
|
refresh,
|
|
})
|
|
|
|
const enter = (e: FileEntry) => {
|
|
if (e.is_dir) setPath(e.path)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<header>
|
|
<h1 className="font-display text-3xl font-bold text-ink-600">文件管理</h1>
|
|
<p className="text-sm text-ink-50">
|
|
在下载目录中选择文件夹或视频,直接设置目标并整理入库。
|
|
</p>
|
|
</header>
|
|
|
|
<FileManagerToolbar
|
|
currentPath={data?.path}
|
|
parentPath={data?.parent}
|
|
recursive={recursive}
|
|
onRoot={() => setPath('')}
|
|
onParent={setPath}
|
|
onRefresh={refresh}
|
|
onRecursiveChange={setRecursive}
|
|
/>
|
|
|
|
<AutoOrganizeSettingsPanel
|
|
config={autoOrganize.config}
|
|
currentDir={currentDir}
|
|
activeTab={autoOrganize.activeTab}
|
|
dirty={autoOrganize.dirty}
|
|
loading={autoOrganize.loading}
|
|
saving={autoOrganize.saving}
|
|
running={autoOrganize.running}
|
|
moveKeepsSeeding={autoMoveKeepsSeeding}
|
|
onRefresh={autoOrganize.refresh}
|
|
onSave={() => void autoOrganize.save()}
|
|
onRunNow={autoOrganize.runNow}
|
|
onTabChange={autoOrganize.setActiveTab}
|
|
onConfigChange={autoOrganize.changeConfig}
|
|
/>
|
|
|
|
{data?.path && (
|
|
<ManualOrganizePanel
|
|
organizeSource={manualOrganize.organizeSource}
|
|
selectedCount={fileOperations.selectedPaths.length}
|
|
localLibraries={localLibraries}
|
|
organizeLibraryID={manualOrganize.organizeLibraryID}
|
|
organizeDestPath={manualOrganize.organizeDestPath}
|
|
organizeMediaType={manualOrganize.organizeMediaType}
|
|
organizeTransferMode={manualOrganize.organizeTransferMode}
|
|
manualMoveKeepsSeeding={manualOrganize.manualMoveKeepsSeeding}
|
|
scanAfter={manualOrganize.scanAfter}
|
|
scrapeAfter={scrapeAfter}
|
|
organizeReady={manualOrganize.organizeReady}
|
|
organizeBusy={manualOrganize.organizeBusy}
|
|
previewItems={manualOrganize.previewItems}
|
|
onClearSelected={() => fileOperations.setSelectedPaths([])}
|
|
onLibraryChange={manualOrganize.setOrganizeLibraryID}
|
|
onDestPathChange={manualOrganize.setOrganizeDestPath}
|
|
onMediaTypeChange={manualOrganize.setOrganizeMediaType}
|
|
onTransferModeChange={manualOrganize.setOrganizeTransferMode}
|
|
onScanAfterChange={manualOrganize.setScanAfter}
|
|
onScrapeAfterChange={manualOrganize.onScrapeAfterChange}
|
|
onPreview={manualOrganize.preview}
|
|
onRun={manualOrganize.run}
|
|
/>
|
|
)}
|
|
|
|
{data?.path && (
|
|
<FileOperationsPanel
|
|
selected={fileOperations.selected}
|
|
folderName={fileOperations.folderName}
|
|
renameTo={fileOperations.renameTo}
|
|
destPath={fileOperations.destPath}
|
|
transferMode={fileOperations.transferMode}
|
|
busy={fileOperations.busy}
|
|
onFolderNameChange={fileOperations.setFolderName}
|
|
onRenameToChange={fileOperations.setRenameTo}
|
|
onDestPathChange={fileOperations.setDestPath}
|
|
onTransferModeChange={fileOperations.setTransferMode}
|
|
onCreateFolder={fileOperations.createFolder}
|
|
onRenameSelected={fileOperations.renameSelected}
|
|
onDeleteSelected={fileOperations.deleteSelected}
|
|
onTransferSelected={fileOperations.transferSelected}
|
|
/>
|
|
)}
|
|
|
|
{loading && <p className="text-sand-500">加载中…</p>}
|
|
{error && <div className="glass-panel !border-red-400/40 text-red-400">{error}</div>}
|
|
|
|
{!loading && data && !data.path && data.roots && (
|
|
<FileBrowserRoots roots={data.roots} onOpen={setPath} />
|
|
)}
|
|
|
|
{!loading && data?.entries && data.entries.length > 0 && (
|
|
<FileEntriesTable
|
|
basePath={data.path || ''}
|
|
entries={data.entries}
|
|
recursive={recursive}
|
|
selectedPath={fileOperations.selected?.path}
|
|
selectedPaths={fileOperations.selectedPaths}
|
|
onSelectAll={(checked) => fileOperations.setSelectedPaths(checked ? data.entries?.map((entry) => entry.path) ?? [] : [])}
|
|
onToggleSelectedPath={fileOperations.toggleSelectedPath}
|
|
onEnter={enter}
|
|
onChoose={fileOperations.choose}
|
|
/>
|
|
)}
|
|
|
|
{!loading && data?.entries && data.entries.length === 0 && <p className="text-ink-50">空目录。</p>}
|
|
</div>
|
|
)
|
|
}
|