mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-14 00:56:37 +08:00
refactor: split cloud browser hooks
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
|
export type CloudMountResult = {
|
||||||
|
already_mounted?: boolean
|
||||||
|
estimate_message?: string
|
||||||
|
message?: string
|
||||||
|
reason?: string
|
||||||
|
skipped?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function apiErrorMessage(err: unknown, fallback: string): string {
|
||||||
|
return (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showCloudMountResult(res: unknown, label: string) {
|
||||||
|
const out = res as CloudMountResult
|
||||||
|
if (out.skipped) {
|
||||||
|
toast(`已跳过「${label}」:和已挂载目录重叠`)
|
||||||
|
return 'skipped'
|
||||||
|
}
|
||||||
|
if (out.already_mounted) {
|
||||||
|
toast(`「${label}」已经挂载,后台会刷新扫描并自动入库`)
|
||||||
|
return 'mounted'
|
||||||
|
}
|
||||||
|
toast.success(`已挂载「${label}」,${out.message ?? '后台会递归扫描并自动加入媒体库'}。${out.estimate_message ?? ''}`)
|
||||||
|
return 'mounted'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeCloudFolderInput(value: string | null): string {
|
||||||
|
const name = (value ?? '').trim()
|
||||||
|
if (!name) return ''
|
||||||
|
if (name === '.' || name === '..' || /[\\/]/.test(name)) {
|
||||||
|
toast.error('文件夹名称不能包含路径分隔符')
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
@@ -1,50 +1,28 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import toast from 'react-hot-toast'
|
|
||||||
|
|
||||||
import { libraryAPI } from '../api/library'
|
import { libraryAPI } from '../api/library'
|
||||||
import {
|
import {
|
||||||
cloudAPI,
|
cloudAPI,
|
||||||
storageAPI,
|
|
||||||
type CloudEntry,
|
type CloudEntry,
|
||||||
type CloudScanStatus,
|
|
||||||
type StorageType,
|
type StorageType,
|
||||||
} from '../api/storage_config'
|
} from '../api/storage_config'
|
||||||
import { confirmAction } from '../components/confirmAction'
|
|
||||||
import type { Library } from '../types'
|
import type { Library } from '../types'
|
||||||
import {
|
import { apiErrorMessage } from './cloudBrowserModel'
|
||||||
TYPE_LABEL,
|
import { cloudLibraryProvider } from './storageConfigModel'
|
||||||
cloudLibraryProvider,
|
import { useCloudBrowserFileActions } from './useCloudBrowserFileActions'
|
||||||
cloudMountDisplayPath,
|
import { useCloudBrowserMounts } from './useCloudBrowserMounts'
|
||||||
} from './storageConfigModel'
|
import { useCloudBrowserScan } from './useCloudBrowserScan'
|
||||||
|
|
||||||
type CloudMountResult = {
|
|
||||||
already_mounted?: boolean
|
|
||||||
estimate_message?: string
|
|
||||||
library?: Library
|
|
||||||
message?: string
|
|
||||||
reason?: string
|
|
||||||
skipped?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
function apiErrorMessage(err: unknown, fallback: string): string {
|
|
||||||
return (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCloudBrowser(type: StorageType) {
|
export function useCloudBrowser(type: StorageType) {
|
||||||
const [stack, setStack] = useState<{ id: string; name: string }[]>([{ id: '', name: '根目录' }])
|
const [stack, setStack] = useState<{ id: string; name: string }[]>([{ id: '', name: '根目录' }])
|
||||||
const [items, setItems] = useState<CloudEntry[]>([])
|
const [items, setItems] = useState<CloudEntry[]>([])
|
||||||
const [mounts, setMounts] = useState<Library[]>([])
|
const [mounts, setMounts] = useState<Library[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [mounting, setMounting] = useState(false)
|
|
||||||
const [batchMounting, setBatchMounting] = useState(false)
|
|
||||||
const [scanBusy, setScanBusy] = useState(false)
|
|
||||||
const [cancelBusy, setCancelBusy] = useState(false)
|
|
||||||
const [scanStatuses, setScanStatuses] = useState<CloudScanStatus[]>([])
|
|
||||||
const [mountMediaType, setMountMediaType] = useState('auto')
|
const [mountMediaType, setMountMediaType] = useState('auto')
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
const cur = stack[stack.length - 1]
|
const cur = stack[stack.length - 1]
|
||||||
const load = async (dir: string) => {
|
const load = useCallback(async (dir: string) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError('')
|
setError('')
|
||||||
try {
|
try {
|
||||||
@@ -57,197 +35,46 @@ export function useCloudBrowser(type: StorageType) {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}, [type])
|
||||||
|
|
||||||
const loadMounts = async () => {
|
const loadMounts = useCallback(async () => {
|
||||||
const libs = await libraryAPI.list({ includeHidden: true })
|
const libs = await libraryAPI.list({ includeHidden: true })
|
||||||
setMounts(libs.filter((lib) => cloudLibraryProvider(lib.path) === type))
|
setMounts(libs.filter((lib) => cloudLibraryProvider(lib.path) === type))
|
||||||
}
|
}, [type])
|
||||||
|
|
||||||
const loadScanStatus = async () => {
|
const scan = useCloudBrowserScan(type)
|
||||||
const r = await storageAPI.cloudScanStatus()
|
const mountActions = useCloudBrowserMounts({
|
||||||
setScanStatuses((r.items ?? []).filter((item) => !type || item.provider === type))
|
items,
|
||||||
}
|
loadMounts,
|
||||||
|
mountMediaType,
|
||||||
|
stack,
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
const fileActions = useCloudBrowserFileActions({
|
||||||
|
load,
|
||||||
|
loadMounts,
|
||||||
|
setLoading,
|
||||||
|
stack,
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load(cur.id).catch(() => undefined)
|
load(cur.id).catch(() => undefined)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [cur.id, load])
|
||||||
}, [stack.length, type])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadMounts().catch(() => undefined)
|
loadMounts().catch(() => undefined)
|
||||||
loadScanStatus().catch(() => undefined)
|
}, [loadMounts])
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [type])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = window.setInterval(() => {
|
|
||||||
loadScanStatus().catch(() => undefined)
|
|
||||||
}, 3000)
|
|
||||||
return () => window.clearInterval(timer)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [type])
|
|
||||||
|
|
||||||
const enter = (entry: CloudEntry) => setStack((current) => [...current, { id: entry.id, name: entry.name }])
|
const enter = (entry: CloudEntry) => setStack((current) => [...current, { id: entry.id, name: entry.name }])
|
||||||
const goTo = (index: number) => setStack((current) => current.slice(0, index + 1))
|
const goTo = (index: number) => setStack((current) => current.slice(0, index + 1))
|
||||||
const goUp = () => setStack((current) => (current.length > 1 ? current.slice(0, -1) : current))
|
const goUp = () => setStack((current) => (current.length > 1 ? current.slice(0, -1) : current))
|
||||||
const currentMountPath = () => cloudMountDisplayPath(type, stack)
|
|
||||||
const childMountPath = (child: CloudEntry) => cloudMountDisplayPath(type, stack, child)
|
|
||||||
const currentDir = () => stack[stack.length - 1]?.id ?? ''
|
|
||||||
|
|
||||||
const handleMountResult = (res: unknown, label: string) => {
|
|
||||||
const out = res as CloudMountResult
|
|
||||||
if (out.skipped) {
|
|
||||||
toast(`已跳过「${label}」:和已挂载目录重叠`)
|
|
||||||
return 'skipped'
|
|
||||||
}
|
|
||||||
if (out.already_mounted) {
|
|
||||||
toast(`「${label}」已经挂载,后台会刷新扫描并自动入库`)
|
|
||||||
return 'mounted'
|
|
||||||
}
|
|
||||||
toast.success(`已挂载「${label}」,${out.message ?? '后台会递归扫描并自动加入媒体库'}。${out.estimate_message ?? ''}`)
|
|
||||||
return 'mounted'
|
|
||||||
}
|
|
||||||
|
|
||||||
const doImport = async (entry: CloudEntry) => {
|
|
||||||
const ref = type === 'cloud115' ? entry.pick_code || entry.id : entry.id
|
|
||||||
try {
|
|
||||||
await cloudAPI.import(type, ref, entry.name, entry.size)
|
|
||||||
toast.success(`已导入「${entry.name}」,可在媒体库中 302 播放`)
|
|
||||||
} catch (err: unknown) {
|
|
||||||
toast.error(apiErrorMessage(err, '导入失败'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeFolderInput = (value: string | null) => {
|
|
||||||
const name = (value ?? '').trim()
|
|
||||||
if (!name) return ''
|
|
||||||
if (name === '.' || name === '..' || /[\\/]/.test(name)) {
|
|
||||||
toast.error('文件夹名称不能包含路径分隔符')
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
const createFolder = async () => {
|
|
||||||
const name = normalizeFolderInput(window.prompt('新建文件夹名称') ?? '')
|
|
||||||
if (!name) return
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
await cloudAPI.mkdir(type, currentDir(), name)
|
|
||||||
toast.success(`已新建文件夹「${name}」`)
|
|
||||||
await load(currentDir())
|
|
||||||
} catch (err: unknown) {
|
|
||||||
toast.error(apiErrorMessage(err, '新建文件夹失败'))
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const renameFolder = async (entry: CloudEntry) => {
|
|
||||||
if (!entry.is_dir) return
|
|
||||||
const name = normalizeFolderInput(window.prompt('重命名文件夹', entry.name) ?? '')
|
|
||||||
if (!name || name === entry.name) return
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
await cloudAPI.rename(type, entry.id, name)
|
|
||||||
toast.success(`已重命名为「${name}」`)
|
|
||||||
await load(currentDir())
|
|
||||||
await loadMounts()
|
|
||||||
} catch (err: unknown) {
|
|
||||||
toast.error(apiErrorMessage(err, '重命名失败'))
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mountCurrent = async () => {
|
|
||||||
setMounting(true)
|
|
||||||
try {
|
|
||||||
const label = TYPE_LABEL[type] ?? type
|
|
||||||
const name = cur.id ? cur.name : label
|
|
||||||
const res = await cloudAPI.mount(type, cur.id, name, mountMediaType, currentMountPath())
|
|
||||||
handleMountResult(res, cur.name)
|
|
||||||
await loadMounts()
|
|
||||||
} catch (err: unknown) {
|
|
||||||
toast.error(apiErrorMessage(err, '挂载失败'))
|
|
||||||
} finally {
|
|
||||||
setMounting(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mountVisibleDirectories = async () => {
|
|
||||||
const dirs = items.filter((item) => item.is_dir)
|
|
||||||
if (dirs.length === 0) {
|
|
||||||
toast.error('当前目录下没有可挂载的子目录')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setBatchMounting(true)
|
|
||||||
let ok = 0
|
|
||||||
let skipped = 0
|
|
||||||
let failed = 0
|
|
||||||
for (const dir of dirs) {
|
|
||||||
try {
|
|
||||||
const result = await cloudAPI.mount(type, dir.id, dir.name, 'auto', childMountPath(dir))
|
|
||||||
const state = handleMountResult(result, dir.name)
|
|
||||||
if (state === 'skipped') skipped += 1
|
|
||||||
else ok += 1
|
|
||||||
} catch {
|
|
||||||
failed += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (failed > 0) {
|
|
||||||
toast.error(`已挂载 ${ok} 个目录,跳过 ${skipped} 个重叠目录,失败 ${failed} 个`)
|
|
||||||
} else {
|
|
||||||
toast.success(`已挂载 ${ok} 个目录,跳过 ${skipped} 个重叠目录,后台会自动生成 302/STRM 播放入口`)
|
|
||||||
}
|
|
||||||
await loadMounts()
|
|
||||||
setBatchMounting(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeMount = async (lib: Library) => {
|
|
||||||
const ok = await confirmAction({
|
|
||||||
title: '移除网盘挂载',
|
|
||||||
message: `仅移除「${lib.name}」在本项目中的媒体库和媒体记录,不会删除网盘文件。`,
|
|
||||||
confirmText: '移除',
|
|
||||||
})
|
|
||||||
if (!ok) return
|
|
||||||
await libraryAPI.remove(lib.id)
|
|
||||||
toast.success('已移除挂载')
|
|
||||||
await loadMounts()
|
|
||||||
}
|
|
||||||
|
|
||||||
const scanAllCloudLibraries = async () => {
|
|
||||||
setScanBusy(true)
|
|
||||||
try {
|
|
||||||
const r = await storageAPI.scanAllCloud()
|
|
||||||
setScanStatuses(r.items ?? [])
|
|
||||||
toast.success(r.message ?? '已开始扫描所有启用的网盘媒体库')
|
|
||||||
} catch (err: unknown) {
|
|
||||||
toast.error(apiErrorMessage(err, '启动扫描失败'))
|
|
||||||
} finally {
|
|
||||||
setScanBusy(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelCloudScans = async () => {
|
|
||||||
setCancelBusy(true)
|
|
||||||
try {
|
|
||||||
const r = await storageAPI.cancelCloudScan('', type)
|
|
||||||
toast.success(r.message ?? `已中断 ${r.cancelled} 个扫描任务`)
|
|
||||||
await loadScanStatus()
|
|
||||||
} catch (err: unknown) {
|
|
||||||
toast.error(apiErrorMessage(err, '中断扫描失败'))
|
|
||||||
} finally {
|
|
||||||
setCancelBusy(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
batchMounting,
|
batchMounting: mountActions.batchMounting,
|
||||||
cancelBusy,
|
cancelBusy: scan.cancelBusy,
|
||||||
cancelCloudScans,
|
cancelCloudScans: scan.cancelCloudScans,
|
||||||
createFolder,
|
createFolder: fileActions.createFolder,
|
||||||
enter,
|
enter,
|
||||||
error,
|
error,
|
||||||
goTo,
|
goTo,
|
||||||
@@ -255,18 +82,18 @@ export function useCloudBrowser(type: StorageType) {
|
|||||||
hasDirectories: items.some((item) => item.is_dir),
|
hasDirectories: items.some((item) => item.is_dir),
|
||||||
items,
|
items,
|
||||||
loading,
|
loading,
|
||||||
mountCurrent,
|
mountCurrent: mountActions.mountCurrent,
|
||||||
mountMediaType,
|
mountMediaType,
|
||||||
mounting,
|
mounting: mountActions.mounting,
|
||||||
mounts,
|
mounts,
|
||||||
mountVisibleDirectories,
|
mountVisibleDirectories: mountActions.mountVisibleDirectories,
|
||||||
removeMount,
|
removeMount: mountActions.removeMount,
|
||||||
renameFolder,
|
renameFolder: fileActions.renameFolder,
|
||||||
scanAllCloudLibraries,
|
scanAllCloudLibraries: scan.scanAllCloudLibraries,
|
||||||
scanBusy,
|
scanBusy: scan.scanBusy,
|
||||||
scanStatuses,
|
scanStatuses: scan.scanStatuses,
|
||||||
setMountMediaType,
|
setMountMediaType,
|
||||||
stack,
|
stack,
|
||||||
doImport,
|
doImport: fileActions.doImport,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type { Dispatch, SetStateAction } from 'react'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
|
import { cloudAPI, type CloudEntry, type StorageType } from '../api/storage_config'
|
||||||
|
import { apiErrorMessage, normalizeCloudFolderInput } from './cloudBrowserModel'
|
||||||
|
|
||||||
|
type CloudStackItem = { id: string; name: string }
|
||||||
|
|
||||||
|
type UseCloudBrowserFileActionsOptions = {
|
||||||
|
load: (dir: string) => Promise<void>
|
||||||
|
loadMounts: () => Promise<void>
|
||||||
|
setLoading: Dispatch<SetStateAction<boolean>>
|
||||||
|
stack: CloudStackItem[]
|
||||||
|
type: StorageType
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCloudBrowserFileActions({
|
||||||
|
load,
|
||||||
|
loadMounts,
|
||||||
|
setLoading,
|
||||||
|
stack,
|
||||||
|
type,
|
||||||
|
}: UseCloudBrowserFileActionsOptions) {
|
||||||
|
const currentDir = () => stack[stack.length - 1]?.id ?? ''
|
||||||
|
|
||||||
|
const doImport = async (entry: CloudEntry) => {
|
||||||
|
const ref = type === 'cloud115' ? entry.pick_code || entry.id : entry.id
|
||||||
|
try {
|
||||||
|
await cloudAPI.import(type, ref, entry.name, entry.size)
|
||||||
|
toast.success(`已导入「${entry.name}」,可在媒体库中 302 播放`)
|
||||||
|
} catch (err: unknown) {
|
||||||
|
toast.error(apiErrorMessage(err, '导入失败'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createFolder = async () => {
|
||||||
|
const name = normalizeCloudFolderInput(window.prompt('新建文件夹名称') ?? '')
|
||||||
|
if (!name) return
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
await cloudAPI.mkdir(type, currentDir(), name)
|
||||||
|
toast.success(`已新建文件夹「${name}」`)
|
||||||
|
await load(currentDir())
|
||||||
|
} catch (err: unknown) {
|
||||||
|
toast.error(apiErrorMessage(err, '新建文件夹失败'))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renameFolder = async (entry: CloudEntry) => {
|
||||||
|
if (!entry.is_dir) return
|
||||||
|
const name = normalizeCloudFolderInput(window.prompt('重命名文件夹', entry.name) ?? '')
|
||||||
|
if (!name || name === entry.name) return
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
await cloudAPI.rename(type, entry.id, name)
|
||||||
|
toast.success(`已重命名为「${name}」`)
|
||||||
|
await load(currentDir())
|
||||||
|
await loadMounts()
|
||||||
|
} catch (err: unknown) {
|
||||||
|
toast.error(apiErrorMessage(err, '重命名失败'))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createFolder,
|
||||||
|
doImport,
|
||||||
|
renameFolder,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
|
import { cloudAPI, type CloudEntry, type StorageType } from '../api/storage_config'
|
||||||
|
import { libraryAPI } from '../api/library'
|
||||||
|
import { confirmAction } from '../components/confirmAction'
|
||||||
|
import type { Library } from '../types'
|
||||||
|
import {
|
||||||
|
TYPE_LABEL,
|
||||||
|
cloudMountDisplayPath,
|
||||||
|
} from './storageConfigModel'
|
||||||
|
import { apiErrorMessage, showCloudMountResult } from './cloudBrowserModel'
|
||||||
|
|
||||||
|
type CloudStackItem = { id: string; name: string }
|
||||||
|
|
||||||
|
type UseCloudBrowserMountsOptions = {
|
||||||
|
items: CloudEntry[]
|
||||||
|
loadMounts: () => Promise<void>
|
||||||
|
mountMediaType: string
|
||||||
|
stack: CloudStackItem[]
|
||||||
|
type: StorageType
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCloudBrowserMounts({
|
||||||
|
items,
|
||||||
|
loadMounts,
|
||||||
|
mountMediaType,
|
||||||
|
stack,
|
||||||
|
type,
|
||||||
|
}: UseCloudBrowserMountsOptions) {
|
||||||
|
const [mounting, setMounting] = useState(false)
|
||||||
|
const [batchMounting, setBatchMounting] = useState(false)
|
||||||
|
|
||||||
|
const cur = stack[stack.length - 1]
|
||||||
|
const currentMountPath = () => cloudMountDisplayPath(type, stack)
|
||||||
|
const childMountPath = (child: CloudEntry) => cloudMountDisplayPath(type, stack, child)
|
||||||
|
|
||||||
|
const mountCurrent = async () => {
|
||||||
|
setMounting(true)
|
||||||
|
try {
|
||||||
|
const label = TYPE_LABEL[type] ?? type
|
||||||
|
const name = cur.id ? cur.name : label
|
||||||
|
const res = await cloudAPI.mount(type, cur.id, name, mountMediaType, currentMountPath())
|
||||||
|
showCloudMountResult(res, cur.name)
|
||||||
|
await loadMounts()
|
||||||
|
} catch (err: unknown) {
|
||||||
|
toast.error(apiErrorMessage(err, '挂载失败'))
|
||||||
|
} finally {
|
||||||
|
setMounting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mountVisibleDirectories = async () => {
|
||||||
|
const dirs = items.filter((item) => item.is_dir)
|
||||||
|
if (dirs.length === 0) {
|
||||||
|
toast.error('当前目录下没有可挂载的子目录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setBatchMounting(true)
|
||||||
|
let ok = 0
|
||||||
|
let skipped = 0
|
||||||
|
let failed = 0
|
||||||
|
for (const dir of dirs) {
|
||||||
|
try {
|
||||||
|
const result = await cloudAPI.mount(type, dir.id, dir.name, 'auto', childMountPath(dir))
|
||||||
|
const state = showCloudMountResult(result, dir.name)
|
||||||
|
if (state === 'skipped') skipped += 1
|
||||||
|
else ok += 1
|
||||||
|
} catch {
|
||||||
|
failed += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (failed > 0) {
|
||||||
|
toast.error(`已挂载 ${ok} 个目录,跳过 ${skipped} 个重叠目录,失败 ${failed} 个`)
|
||||||
|
} else {
|
||||||
|
toast.success(`已挂载 ${ok} 个目录,跳过 ${skipped} 个重叠目录,后台会自动生成 302/STRM 播放入口`)
|
||||||
|
}
|
||||||
|
await loadMounts()
|
||||||
|
setBatchMounting(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeMount = async (lib: Library) => {
|
||||||
|
const ok = await confirmAction({
|
||||||
|
title: '移除网盘挂载',
|
||||||
|
message: `仅移除「${lib.name}」在本项目中的媒体库和媒体记录,不会删除网盘文件。`,
|
||||||
|
confirmText: '移除',
|
||||||
|
})
|
||||||
|
if (!ok) return
|
||||||
|
await libraryAPI.remove(lib.id)
|
||||||
|
toast.success('已移除挂载')
|
||||||
|
await loadMounts()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
batchMounting,
|
||||||
|
mountCurrent,
|
||||||
|
mounting,
|
||||||
|
mountVisibleDirectories,
|
||||||
|
removeMount,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
|
import { storageAPI, type CloudScanStatus, type StorageType } from '../api/storage_config'
|
||||||
|
import { apiErrorMessage } from './cloudBrowserModel'
|
||||||
|
|
||||||
|
export function useCloudBrowserScan(type: StorageType) {
|
||||||
|
const [scanBusy, setScanBusy] = useState(false)
|
||||||
|
const [cancelBusy, setCancelBusy] = useState(false)
|
||||||
|
const [scanStatuses, setScanStatuses] = useState<CloudScanStatus[]>([])
|
||||||
|
|
||||||
|
const loadScanStatus = useCallback(async () => {
|
||||||
|
const r = await storageAPI.cloudScanStatus()
|
||||||
|
setScanStatuses((r.items ?? []).filter((item) => !type || item.provider === type))
|
||||||
|
}, [type])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadScanStatus().catch(() => undefined)
|
||||||
|
}, [loadScanStatus])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = window.setInterval(() => {
|
||||||
|
loadScanStatus().catch(() => undefined)
|
||||||
|
}, 3000)
|
||||||
|
return () => window.clearInterval(timer)
|
||||||
|
}, [loadScanStatus])
|
||||||
|
|
||||||
|
const scanAllCloudLibraries = async () => {
|
||||||
|
setScanBusy(true)
|
||||||
|
try {
|
||||||
|
const r = await storageAPI.scanAllCloud()
|
||||||
|
setScanStatuses(r.items ?? [])
|
||||||
|
toast.success(r.message ?? '已开始扫描所有启用的网盘媒体库')
|
||||||
|
} catch (err: unknown) {
|
||||||
|
toast.error(apiErrorMessage(err, '启动扫描失败'))
|
||||||
|
} finally {
|
||||||
|
setScanBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelCloudScans = async () => {
|
||||||
|
setCancelBusy(true)
|
||||||
|
try {
|
||||||
|
const r = await storageAPI.cancelCloudScan('', type)
|
||||||
|
toast.success(r.message ?? `已中断 ${r.cancelled} 个扫描任务`)
|
||||||
|
await loadScanStatus()
|
||||||
|
} catch (err: unknown) {
|
||||||
|
toast.error(apiErrorMessage(err, '中断扫描失败'))
|
||||||
|
} finally {
|
||||||
|
setCancelBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cancelBusy,
|
||||||
|
cancelCloudScans,
|
||||||
|
scanAllCloudLibraries,
|
||||||
|
scanBusy,
|
||||||
|
scanStatuses,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user