mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-07 05:46:38 +08:00
refactor: split manual file organize hook
This commit is contained in:
@@ -1,10 +1,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import toast from 'react-hot-toast'
|
|
||||||
|
|
||||||
import { filesAPI, type FileEntry, type FileListing } from '../api/files'
|
import { filesAPI, type FileEntry, type FileListing } from '../api/files'
|
||||||
import { libraryAPI } from '../api/library'
|
import { libraryAPI } from '../api/library'
|
||||||
import { toolsAPI } from '../api/tools'
|
|
||||||
import { confirmAction } from '../components/confirmAction'
|
|
||||||
import type { Library } from '../types'
|
import type { Library } from '../types'
|
||||||
import { settingOn } from './autoOrganizeModel'
|
import { settingOn } from './autoOrganizeModel'
|
||||||
import { AutoOrganizeSettingsPanel } from './AutoOrganizeSettingsPanel'
|
import { AutoOrganizeSettingsPanel } from './AutoOrganizeSettingsPanel'
|
||||||
@@ -15,13 +12,8 @@ import { FileOperationsPanel } from './FileOperationsPanel'
|
|||||||
import { ManualOrganizePanel } from './ManualOrganizePanel'
|
import { ManualOrganizePanel } from './ManualOrganizePanel'
|
||||||
import { useAutoOrganizeSettings } from './useAutoOrganizeSettings'
|
import { useAutoOrganizeSettings } from './useAutoOrganizeSettings'
|
||||||
import { useFileOperations } from './useFileOperations'
|
import { useFileOperations } from './useFileOperations'
|
||||||
import {
|
import { isCloudLibraryPath } from './fileManagerModel'
|
||||||
formatScanSummary,
|
import { useManualOrganize } from './useManualOrganize'
|
||||||
formatScrapeSummary,
|
|
||||||
isCloudLibraryPath,
|
|
||||||
summarizeOrganizeResults,
|
|
||||||
type OrganizePreviewItem,
|
|
||||||
} from './fileManagerModel'
|
|
||||||
|
|
||||||
// FileManagerPage provides a focused local storage view:
|
// FileManagerPage provides a focused local storage view:
|
||||||
// browse allowed roots, optionally recurse, and perform safe local operations.
|
// browse allowed roots, optionally recurse, and perform safe local operations.
|
||||||
@@ -32,14 +24,7 @@ export function FileManagerPage() {
|
|||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [recursive, setRecursive] = useState(false)
|
const [recursive, setRecursive] = useState(false)
|
||||||
const [organizeLibraryID, setOrganizeLibraryID] = useState('')
|
|
||||||
const [organizeDestPath, setOrganizeDestPath] = useState('')
|
|
||||||
const [organizeTransferMode, setOrganizeTransferMode] = useState('hardlink')
|
|
||||||
const [organizeMediaType, setOrganizeMediaType] = useState('auto')
|
|
||||||
const [scanAfter, setScanAfter] = useState(true)
|
|
||||||
const [scrapeAfter, setScrapeAfter] = useState(true)
|
const [scrapeAfter, setScrapeAfter] = useState(true)
|
||||||
const [organizeBusy, setOrganizeBusy] = useState('')
|
|
||||||
const [previewItems, setPreviewItems] = useState<OrganizePreviewItem[]>([])
|
|
||||||
const autoOrganize = useAutoOrganizeSettings({ onScrapeAfterChange: setScrapeAfter })
|
const autoOrganize = useAutoOrganizeSettings({ onScrapeAfterChange: setScrapeAfter })
|
||||||
|
|
||||||
const currentDir = useMemo(() => {
|
const currentDir = useMemo(() => {
|
||||||
@@ -51,7 +36,6 @@ export function FileManagerPage() {
|
|||||||
[libraries],
|
[libraries],
|
||||||
)
|
)
|
||||||
const autoMoveKeepsSeeding = autoOrganize.moveKeepsSeeding
|
const autoMoveKeepsSeeding = autoOrganize.moveKeepsSeeding
|
||||||
const manualMoveKeepsSeeding = organizeTransferMode === 'move' && settingOn(autoOrganize.config.keepSeeding)
|
|
||||||
|
|
||||||
const refresh = useCallback(() => {
|
const refresh = useCallback(() => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -77,79 +61,22 @@ export function FileManagerPage() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const fileOperations = useFileOperations({ currentDir, path, refresh })
|
const fileOperations = useFileOperations({ currentDir, path, refresh })
|
||||||
|
const manualOrganize = useManualOrganize({
|
||||||
useEffect(() => {
|
currentDir,
|
||||||
if (!organizeLibraryID) return
|
localLibraries,
|
||||||
const lib = localLibraries.find((item) => item.id === organizeLibraryID)
|
selectedPath: fileOperations.selected?.path,
|
||||||
if (!lib) {
|
selectedPaths: fileOperations.selectedPaths,
|
||||||
setOrganizeLibraryID('')
|
scrapeAfter,
|
||||||
return
|
keepSeeding: settingOn(autoOrganize.config.keepSeeding),
|
||||||
}
|
onScrapeAfterChange: setScrapeAfter,
|
||||||
setOrganizeDestPath(lib.path)
|
onClearSelected: () => fileOperations.setSelectedPaths([]),
|
||||||
setOrganizeMediaType(lib.type || 'auto')
|
refresh,
|
||||||
}, [localLibraries, organizeLibraryID])
|
})
|
||||||
|
|
||||||
const enter = (e: FileEntry) => {
|
const enter = (e: FileEntry) => {
|
||||||
if (e.is_dir) setPath(e.path)
|
if (e.is_dir) setPath(e.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
const organizeSources = fileOperations.selectedPaths.length > 0 ? fileOperations.selectedPaths : [fileOperations.selected?.path || currentDir].filter(Boolean)
|
|
||||||
const organizeSource = organizeSources.length === 1 ? organizeSources[0] : `${organizeSources.length} 个已选项目`
|
|
||||||
const organizeReady = organizeSources.length > 0 && Boolean(organizeDestPath.trim())
|
|
||||||
|
|
||||||
const runManualOrganize = async (dryRun: boolean) => {
|
|
||||||
if (!organizeReady) {
|
|
||||||
toast.error('请选择来源文件/文件夹,并设置目标媒体库或目的路径')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!dryRun) {
|
|
||||||
const ok = await confirmAction({
|
|
||||||
title: '确认整理入库',
|
|
||||||
message: `来源:${organizeSources.join('\n')}\n目标:${organizeDestPath}\n方式:${organizeTransferMode}${scanAfter ? '\n整理完成后会扫描入库。' : ''}${scanAfter && scrapeAfter ? '\n扫描后会自动刮削。' : ''}`,
|
|
||||||
confirmText: '开始整理',
|
|
||||||
})
|
|
||||||
if (!ok) return
|
|
||||||
}
|
|
||||||
setOrganizeBusy(dryRun ? 'preview' : 'run')
|
|
||||||
try {
|
|
||||||
const results = []
|
|
||||||
for (const sourcePath of organizeSources) {
|
|
||||||
results.push(await toolsAPI.organizeDirectory({
|
|
||||||
source_path: sourcePath,
|
|
||||||
dest_path: organizeDestPath.trim(),
|
|
||||||
transfer_mode: organizeTransferMode,
|
|
||||||
media_type: organizeMediaType === 'auto' ? undefined : organizeMediaType,
|
|
||||||
scan_after: !dryRun && scanAfter,
|
|
||||||
scrape_after: !dryRun && scanAfter && scrapeAfter,
|
|
||||||
library_id: !dryRun && scanAfter && organizeLibraryID ? organizeLibraryID : undefined,
|
|
||||||
dry_run: dryRun,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
const summary = summarizeOrganizeResults(results)
|
|
||||||
setPreviewItems(summary.preview)
|
|
||||||
if (summary.total === 0) {
|
|
||||||
toast(`未发现可整理视频:${organizeSource}`, {
|
|
||||||
icon: '!',
|
|
||||||
duration: 6000,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (dryRun) {
|
|
||||||
toast.success(`预览完成:新增 ${summary.organized} · 替换 ${summary.replaced} · 纠偏 ${summary.reclassified} · 跳过 ${summary.skipped}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const scanText = scanAfter ? formatScanSummary(summary.scans) : ''
|
|
||||||
const scrapeText = scanAfter && scrapeAfter ? formatScrapeSummary(summary.scrapes) : ''
|
|
||||||
toast.success(`整理完成:新增 ${summary.organized} · 替换 ${summary.replaced} · 纠偏 ${summary.reclassified} · 跳过 ${summary.skipped}${scanText}${scrapeText}`)
|
|
||||||
fileOperations.setSelectedPaths([])
|
|
||||||
refresh()
|
|
||||||
} catch (err: unknown) {
|
|
||||||
toast.error((err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '整理失败')
|
|
||||||
} finally {
|
|
||||||
setOrganizeBusy('')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<header>
|
<header>
|
||||||
@@ -187,28 +114,28 @@ export function FileManagerPage() {
|
|||||||
|
|
||||||
{data?.path && (
|
{data?.path && (
|
||||||
<ManualOrganizePanel
|
<ManualOrganizePanel
|
||||||
organizeSource={organizeSource}
|
organizeSource={manualOrganize.organizeSource}
|
||||||
selectedCount={fileOperations.selectedPaths.length}
|
selectedCount={fileOperations.selectedPaths.length}
|
||||||
localLibraries={localLibraries}
|
localLibraries={localLibraries}
|
||||||
organizeLibraryID={organizeLibraryID}
|
organizeLibraryID={manualOrganize.organizeLibraryID}
|
||||||
organizeDestPath={organizeDestPath}
|
organizeDestPath={manualOrganize.organizeDestPath}
|
||||||
organizeMediaType={organizeMediaType}
|
organizeMediaType={manualOrganize.organizeMediaType}
|
||||||
organizeTransferMode={organizeTransferMode}
|
organizeTransferMode={manualOrganize.organizeTransferMode}
|
||||||
manualMoveKeepsSeeding={manualMoveKeepsSeeding}
|
manualMoveKeepsSeeding={manualOrganize.manualMoveKeepsSeeding}
|
||||||
scanAfter={scanAfter}
|
scanAfter={manualOrganize.scanAfter}
|
||||||
scrapeAfter={scrapeAfter}
|
scrapeAfter={scrapeAfter}
|
||||||
organizeReady={organizeReady}
|
organizeReady={manualOrganize.organizeReady}
|
||||||
organizeBusy={organizeBusy}
|
organizeBusy={manualOrganize.organizeBusy}
|
||||||
previewItems={previewItems}
|
previewItems={manualOrganize.previewItems}
|
||||||
onClearSelected={() => fileOperations.setSelectedPaths([])}
|
onClearSelected={() => fileOperations.setSelectedPaths([])}
|
||||||
onLibraryChange={setOrganizeLibraryID}
|
onLibraryChange={manualOrganize.setOrganizeLibraryID}
|
||||||
onDestPathChange={setOrganizeDestPath}
|
onDestPathChange={manualOrganize.setOrganizeDestPath}
|
||||||
onMediaTypeChange={setOrganizeMediaType}
|
onMediaTypeChange={manualOrganize.setOrganizeMediaType}
|
||||||
onTransferModeChange={setOrganizeTransferMode}
|
onTransferModeChange={manualOrganize.setOrganizeTransferMode}
|
||||||
onScanAfterChange={setScanAfter}
|
onScanAfterChange={manualOrganize.setScanAfter}
|
||||||
onScrapeAfterChange={setScrapeAfter}
|
onScrapeAfterChange={manualOrganize.onScrapeAfterChange}
|
||||||
onPreview={() => runManualOrganize(true)}
|
onPreview={manualOrganize.preview}
|
||||||
onRun={() => runManualOrganize(false)}
|
onRun={manualOrganize.run}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
|
import { toolsAPI } from '../api/tools'
|
||||||
|
import { confirmAction } from '../components/confirmAction'
|
||||||
|
import type { Library } from '../types'
|
||||||
|
import {
|
||||||
|
formatScanSummary,
|
||||||
|
formatScrapeSummary,
|
||||||
|
summarizeOrganizeResults,
|
||||||
|
type OrganizePreviewItem,
|
||||||
|
} from './fileManagerModel'
|
||||||
|
|
||||||
|
type UseManualOrganizeOptions = {
|
||||||
|
currentDir: string
|
||||||
|
localLibraries: Library[]
|
||||||
|
selectedPath?: string
|
||||||
|
selectedPaths: string[]
|
||||||
|
scrapeAfter: boolean
|
||||||
|
keepSeeding: boolean
|
||||||
|
onScrapeAfterChange: (value: boolean) => void
|
||||||
|
onClearSelected: () => void
|
||||||
|
refresh: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useManualOrganize({
|
||||||
|
currentDir,
|
||||||
|
localLibraries,
|
||||||
|
selectedPath,
|
||||||
|
selectedPaths,
|
||||||
|
scrapeAfter,
|
||||||
|
keepSeeding,
|
||||||
|
onScrapeAfterChange,
|
||||||
|
onClearSelected,
|
||||||
|
refresh,
|
||||||
|
}: UseManualOrganizeOptions) {
|
||||||
|
const [organizeLibraryID, setOrganizeLibraryID] = useState('')
|
||||||
|
const [organizeDestPath, setOrganizeDestPath] = useState('')
|
||||||
|
const [organizeTransferMode, setOrganizeTransferMode] = useState('hardlink')
|
||||||
|
const [organizeMediaType, setOrganizeMediaType] = useState('auto')
|
||||||
|
const [scanAfter, setScanAfter] = useState(true)
|
||||||
|
const [organizeBusy, setOrganizeBusy] = useState('')
|
||||||
|
const [previewItems, setPreviewItems] = useState<OrganizePreviewItem[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!organizeLibraryID) return
|
||||||
|
const lib = localLibraries.find((item) => item.id === organizeLibraryID)
|
||||||
|
if (!lib) {
|
||||||
|
setOrganizeLibraryID('')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setOrganizeDestPath(lib.path)
|
||||||
|
setOrganizeMediaType(lib.type || 'auto')
|
||||||
|
}, [localLibraries, organizeLibraryID])
|
||||||
|
|
||||||
|
const organizeSources = useMemo(
|
||||||
|
() => selectedPaths.length > 0 ? selectedPaths : [selectedPath || currentDir].filter(Boolean),
|
||||||
|
[currentDir, selectedPath, selectedPaths],
|
||||||
|
)
|
||||||
|
const organizeSource = organizeSources.length === 1 ? organizeSources[0] : `${organizeSources.length} 个已选项目`
|
||||||
|
const organizeReady = organizeSources.length > 0 && Boolean(organizeDestPath.trim())
|
||||||
|
const manualMoveKeepsSeeding = organizeTransferMode === 'move' && keepSeeding
|
||||||
|
|
||||||
|
const runManualOrganize = async (dryRun: boolean) => {
|
||||||
|
if (!organizeReady) {
|
||||||
|
toast.error('请选择来源文件/文件夹,并设置目标媒体库或目的路径')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!dryRun) {
|
||||||
|
const ok = await confirmAction({
|
||||||
|
title: '确认整理入库',
|
||||||
|
message: `来源:${organizeSources.join('\n')}\n目标:${organizeDestPath}\n方式:${organizeTransferMode}${scanAfter ? '\n整理完成后会扫描入库。' : ''}${scanAfter && scrapeAfter ? '\n扫描后会自动刮削。' : ''}`,
|
||||||
|
confirmText: '开始整理',
|
||||||
|
})
|
||||||
|
if (!ok) return
|
||||||
|
}
|
||||||
|
setOrganizeBusy(dryRun ? 'preview' : 'run')
|
||||||
|
try {
|
||||||
|
const results = []
|
||||||
|
for (const sourcePath of organizeSources) {
|
||||||
|
results.push(await toolsAPI.organizeDirectory({
|
||||||
|
source_path: sourcePath,
|
||||||
|
dest_path: organizeDestPath.trim(),
|
||||||
|
transfer_mode: organizeTransferMode,
|
||||||
|
media_type: organizeMediaType === 'auto' ? undefined : organizeMediaType,
|
||||||
|
scan_after: !dryRun && scanAfter,
|
||||||
|
scrape_after: !dryRun && scanAfter && scrapeAfter,
|
||||||
|
library_id: !dryRun && scanAfter && organizeLibraryID ? organizeLibraryID : undefined,
|
||||||
|
dry_run: dryRun,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
const summary = summarizeOrganizeResults(results)
|
||||||
|
setPreviewItems(summary.preview)
|
||||||
|
if (summary.total === 0) {
|
||||||
|
toast(`未发现可整理视频:${organizeSource}`, {
|
||||||
|
icon: '!',
|
||||||
|
duration: 6000,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (dryRun) {
|
||||||
|
toast.success(`预览完成:新增 ${summary.organized} · 替换 ${summary.replaced} · 纠偏 ${summary.reclassified} · 跳过 ${summary.skipped}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const scanText = scanAfter ? formatScanSummary(summary.scans) : ''
|
||||||
|
const scrapeText = scanAfter && scrapeAfter ? formatScrapeSummary(summary.scrapes) : ''
|
||||||
|
toast.success(`整理完成:新增 ${summary.organized} · 替换 ${summary.replaced} · 纠偏 ${summary.reclassified} · 跳过 ${summary.skipped}${scanText}${scrapeText}`)
|
||||||
|
onClearSelected()
|
||||||
|
refresh()
|
||||||
|
} catch (err: unknown) {
|
||||||
|
toast.error((err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '整理失败')
|
||||||
|
} finally {
|
||||||
|
setOrganizeBusy('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
organizeLibraryID,
|
||||||
|
organizeDestPath,
|
||||||
|
organizeTransferMode,
|
||||||
|
organizeMediaType,
|
||||||
|
scanAfter,
|
||||||
|
organizeBusy,
|
||||||
|
previewItems,
|
||||||
|
organizeSource,
|
||||||
|
organizeReady,
|
||||||
|
manualMoveKeepsSeeding,
|
||||||
|
setOrganizeLibraryID,
|
||||||
|
setOrganizeDestPath,
|
||||||
|
setOrganizeTransferMode,
|
||||||
|
setOrganizeMediaType,
|
||||||
|
setScanAfter,
|
||||||
|
onScrapeAfterChange,
|
||||||
|
preview: () => runManualOrganize(true),
|
||||||
|
run: () => runManualOrganize(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user