Files
MeBox/web/src/pages/useAdminLibraryPanel.ts
T
2026-06-27 14:45:37 +08:00

182 lines
6.3 KiB
TypeScript

import { FormEvent, useEffect, useState } from 'react'
import toast from 'react-hot-toast'
import { libraryAPI } from '../api/library'
import type { Library, LibraryRoot } from '../types'
import { confirmAction } from '../components/confirmAction'
import { apiErrorMessage, createRootPayload, emptyRootDraft, rootDraftKey, type RootDraft } from './adminLibraryPanelModel'
export function useAdminLibraryPanel() {
const { libs, refresh } = useAdminLibraryList()
const createForm = useCreateLibraryForm(refresh)
const newRoots = useNewLibraryRootDrafts(refresh)
const editableRoots = useEditableRootDrafts()
const rootActions = useEditableLibraryRootActions(refresh, editableRoots)
const libraryActions = useLibraryActions(refresh)
return { libs, createForm, newRoots, editableRoots, rootActions, libraryActions }
}
function useAdminLibraryList() {
const [libs, setLibs] = useState<Library[]>([])
const refresh = () => libraryAPI.list({ includeHidden: true }).then(setLibs)
useEffect(() => {
refresh().catch(() => undefined)
}, [])
return { libs, refresh }
}
function useCreateLibraryForm(refresh: () => Promise<void>) {
const [name, setName] = useState('')
const [roots, setRoots] = useState<RootDraft[]>([emptyRootDraft()])
const [type, setType] = useState('movie')
const handleCreate = async (e: FormEvent) => {
e.preventDefault()
try {
const payload = createRootPayload(roots)
if (payload.length === 0) {
toast.error('请至少填写一个路径')
return
}
await libraryAPI.createWithRoots(name, type, payload)
toast.success('媒体库已创建')
setName('')
setRoots([emptyRootDraft()])
await refresh()
} catch (err: unknown) {
toast.error(apiErrorMessage(err, '创建失败'))
}
}
const updateRoot = (index: number, patch: Partial<RootDraft>) => {
setRoots((prev) => prev.map((root, i) => (i === index ? { ...root, ...patch } : root)))
}
return {
name,
type,
roots,
setName,
setType,
updateRoot,
addRoot: () => setRoots((prev) => [...prev, emptyRootDraft()]),
removeRoot: (index: number) => setRoots((prev) => (prev.length <= 1 ? prev : prev.filter((_, i) => i !== index))),
handleCreate,
}
}
function useNewLibraryRootDrafts(refresh: () => Promise<void>) {
const [newRootByLibrary, setNewRootByLibrary] = useState<Record<string, RootDraft>>({})
const newRootDraft = (libraryID: string) => newRootByLibrary[libraryID] ?? emptyRootDraft()
const setNewRootDraft = (libraryID: string, patch: Partial<RootDraft>) => {
setNewRootByLibrary((prev) => ({ ...prev, [libraryID]: { ...newRootDraft(libraryID), ...patch } }))
}
const addLibraryRoot = async (libraryID: string) => {
const draft = newRootDraft(libraryID)
if (!draft.path?.trim()) {
toast.error('请填写路径')
return
}
await libraryAPI.addRoot(libraryID, { ...draft, path: draft.path.trim(), name: draft.name?.trim() })
setNewRootByLibrary((prev) => ({ ...prev, [libraryID]: emptyRootDraft() }))
toast.success('路径已添加')
await refresh()
}
return { newRootDraft, setNewRootDraft, addLibraryRoot }
}
function useEditableRootDrafts() {
const [rootDrafts, setRootDrafts] = useState<Record<string, RootDraft>>({})
const editableRootDraft = (libraryID: string, root: LibraryRoot): RootDraft => {
const key = rootDraftKey(libraryID, root.id)
return rootDrafts[key] ?? {
name: root.name ?? '',
path: root.path,
enabled: root.enabled,
sort_order: root.sort_order,
}
}
const setEditableRootDraft = (libraryID: string, root: LibraryRoot, patch: Partial<RootDraft>) => {
const key = rootDraftKey(libraryID, root.id)
setRootDrafts((prev) => ({ ...prev, [key]: { ...editableRootDraft(libraryID, root), ...patch } }))
}
const clearEditableRootDraft = (libraryID: string, rootID: string) => {
setRootDrafts((prev) => {
const next = { ...prev }
delete next[rootDraftKey(libraryID, rootID)]
return next
})
}
return { editableRootDraft, setEditableRootDraft, clearEditableRootDraft }
}
type EditableRootDrafts = ReturnType<typeof useEditableRootDrafts>
function useEditableLibraryRootActions(refresh: () => Promise<void>, drafts: EditableRootDrafts) {
const saveLibraryRoot = async (libraryID: string, root: LibraryRoot) => {
const draft = drafts.editableRootDraft(libraryID, root)
if (!draft.path?.trim()) {
toast.error('请填写路径')
return
}
await libraryAPI.updateRoot(libraryID, root.id, {
name: draft.name?.trim(),
path: draft.path.trim(),
enabled: draft.enabled,
sort_order: draft.sort_order,
})
drafts.clearEditableRootDraft(libraryID, root.id)
toast.success('路径已保存')
await refresh()
}
const scanLibraryRoot = async (libraryID: string, root: LibraryRoot) => {
if (!root.id) return
await libraryAPI.scanRoot(libraryID, root.id)
toast.success('路径扫描已加入后台任务')
}
const toggleLibraryRoot = async (libraryID: string, root: LibraryRoot) => {
const enabled = !drafts.editableRootDraft(libraryID, root).enabled
drafts.setEditableRootDraft(libraryID, root, { enabled })
await libraryAPI.updateRoot(libraryID, root.id, { enabled })
await refresh()
}
const removeLibraryRoot = async (library: Library, root: LibraryRoot) => {
if (!(await confirmAction({ title: '删除媒体库路径', message: `确定删除「${root.path}」?`, confirmText: '删除' }))) return
await libraryAPI.removeRoot(library.id, root.id)
toast.success('路径已删除')
await refresh()
}
return { saveLibraryRoot, scanLibraryRoot, toggleLibraryRoot, removeLibraryRoot }
}
function useLibraryActions(refresh: () => Promise<void>) {
const scanLibrary = async (library: Library) => {
const result = await libraryAPI.scan(library.id)
if (result.queued) toast.success('云盘扫描已加入后台队列,会自动入库')
else toast.success(`扫描完成,新增 ${result.added},更新 ${result.updated ?? 0}`)
}
const removeLibrary = async (library: Library) => {
if (!(await confirmAction({ title: '删除媒体库', message: `确定删除「${library.name}」?`, confirmText: '删除' }))) return
await libraryAPI.remove(library.id)
toast.success('已删除')
await refresh()
}
return { scanLibrary, removeLibrary }
}