import type { Locale } from "@app/i18n"; import { t } from "@app/i18n"; import { isVideoFilename } from "@app/types"; import { Button, Input, toast } from "@app/ui"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useEffect, useState } from "react"; import { go, queryParam, setQueryParams } from "~/lib/nav"; import { createTRPCReactClient, trpc } from "~/lib/trpc"; function pathOf(segments: string[]): string { return segments.length > 0 ? `/${segments.join("/")}` : "/"; } function fileRelPath(dirPath: string, basename: string): string { return dirPath === "/" ? `/${basename}` : `${dirPath.replace(/\/+$/, "")}/${basename}`; } function BrowseInner(props: { locale: Locale }) { const { locale } = props; const mounts = trpc.media.mountList.useQuery(); const [mountId, setMountId] = useState(() => queryParam("mount") ?? ""); const [path, setPath] = useState("/"); const [crumbs, setCrumbs] = useState([]); useEffect(() => { setQueryParams({ mount: mountId || null }); }, [mountId]); // 挂载列表返回后:URL 里的 mount 有效则保留;否则自动选中第一个已挂载目录 useEffect(() => { const list = mounts.data?.mounts ?? []; if (list.length === 0) return; if (mountId && list.some((m) => m.id === mountId)) return; const first = list[0]; if (first) setMountId(first.id); }, [mounts.data, mountId]); const dir = trpc.media.mountListDir.useQuery({ mountId, path }, { enabled: Boolean(mountId) }); const scan = trpc.media.libraryScan.useMutation(); const ensure = trpc.media.libraryEnsurePath.useMutation(); const utils = trpc.useUtils(); const batchScrape = trpc.media.scrapeRescrapePath.useMutation({ onSuccess: (data) => { toast.success(t(locale, "browse.batchDone", { message: data.message })); void utils.media.libraryList.invalidate(); void utils.media.libraryGroups.invalidate(); }, onError: () => toast.error(t(locale, "browse.batchError")), }); const bindPath = trpc.media.scrapeBindPath.useMutation({ onSuccess: (data) => { toast.success(t(locale, "browse.batchDone", { message: data.message })); setBindOpen(false); setBindQ(""); void utils.media.libraryList.invalidate(); void utils.media.libraryGroups.invalidate(); }, onError: () => toast.error(t(locale, "browse.batchError")), }); const [bindOpen, setBindOpen] = useState(false); const [bindQ, setBindQ] = useState(""); const bindSearch = trpc.media.scrapeSearch.useQuery( { q: bindQ }, { enabled: bindOpen && bindQ.length > 1 }, ); function goRoot() { setCrumbs([]); setPath("/"); } function goUp() { const next = crumbs.slice(0, -1); setCrumbs(next); setPath(pathOf(next)); } function goToCrumb(index: number) { const next = crumbs.slice(0, index + 1); setCrumbs(next); setPath(pathOf(next)); } function enter(name: string) { const next = [...crumbs, name]; setCrumbs(next); setPath(pathOf(next)); } async function playFile(basename: string) { if (!mountId) return; try { const item = await ensure.mutateAsync({ mountId, path: fileRelPath(path, basename) }); go(`/${locale}/watch?id=${item.id}`); } catch { // error surfaced via ensure.error } } const mountList = mounts.data?.mounts ?? []; return (

{t(locale, "browse.title")}

{mountId && ( )} {mountId && path !== "/" && ( <> )}
{(batchScrape.data || bindPath.data) && (

{t(locale, "browse.batchDone", { message: batchScrape.data?.message ?? bindPath.data?.message ?? "", })}

)} {scan.data && (

{t(locale, "browse.scanDone", { count: scan.data.scanned })}

)} {scan.isError && (

{t(locale, "browse.scanError")}

)}
{crumbs.map((c, i) => ( {" / "} ))} {crumbs.length > 0 && ( )}
{mountId && dir.isPending && (

{t(locale, "browse.loading")}

)} {mountId && dir.isError && (

{t(locale, "browse.loadError")} {dir.error instanceof Error && dir.error.message ? `:${dir.error.message}` : ""}

)} {ensure.isError && (

{t(locale, "browse.playError")}

)} {bindOpen && mountId && (

{t(locale, "browse.bindFolderTitle")}

{t(locale, "browse.bindFolderHint")}

{path}

setBindQ(e.target.value)} />
    {(bindSearch.data ?? []).map((hit) => (
  • {hit.nameCn || hit.name}
    {hit.nameCn && hit.name && (
    {hit.name}
    )}
  • ))}
)}
); } const client = createTRPCReactClient(); export function BrowseBody(props: { locale: Locale }) { const [qc] = useState(() => new QueryClient()); return ( ); }