feat(subtitles): 字幕库验证码由智能体视觉接管;字幕站原站视图下载当前字幕
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
023063edf4
commit
c30a758a74
24 files changed
+466
-41
No files matched your search
@@ -11,6 +11,7 @@ import {
|
||||
} from "~/api/browser.ts";
|
||||
import { LoadingState } from "~/components/console/empty-state.tsx";
|
||||
import { StatusBadge, type StatusTone } from "~/components/console/status-badge.tsx";
|
||||
import { SubtitleFromPage } from "~/components/subtitles/subtitle-from-page.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { apiData, errorMessage } from "~/lib/api.ts";
|
||||
import { qk } from "~/lib/query-keys.ts";
|
||||
@@ -209,7 +210,7 @@ function DesktopView({
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<ResourceDownload session={s} />
|
||||
{site.kind === "subtitle" ? <SubtitleFromPage session={s} /> : <ResourceDownload session={s} />}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<StatusBadge tone={conn.tone}>{conn.text}</StatusBadge>
|
||||
<StatusBadge tone={CONTROLLER_TONE[s.controller] ?? "neutral"}>
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from "~/api/browser.ts";
|
||||
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
import { StatusBadge, type StatusTone } from "~/components/console/status-badge.tsx";
|
||||
import { SubtitleFromPage } from "~/components/subtitles/subtitle-from-page.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { Input } from "~/components/ui/input.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
@@ -154,7 +155,7 @@ export function SiteOriginView({ site }: { site: SiteListItem }) {
|
||||
<p className="text-caption text-muted-foreground">验证码等需本人操作在此完成。</p>
|
||||
<ReloadNotice session={s} />
|
||||
|
||||
<ResourceDownload session={s} />
|
||||
{site.kind === "subtitle" ? <SubtitleFromPage session={s} /> : <ResourceDownload session={s} />}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<StatusBadge tone={conn.tone}>{conn.text}</StatusBadge>
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
SubtitleWriteView,
|
||||
} from "@umt/contracts";
|
||||
import { Download, ExternalLink, FileUp, LoaderCircle, Search, Undo2 } from "lucide-react";
|
||||
import { useId, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
useApplySubtitles,
|
||||
@@ -68,11 +68,14 @@ export function SubtitleDialog({
|
||||
title,
|
||||
open,
|
||||
onOpenChange,
|
||||
fromSession,
|
||||
}: {
|
||||
entityId: string;
|
||||
title: string;
|
||||
open: boolean;
|
||||
onOpenChange: (o: boolean) => void;
|
||||
/** 原站视图会话:打开时自动下载它当前页的字幕并配对 */
|
||||
fromSession?: string;
|
||||
}) {
|
||||
const inputId = useId();
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
@@ -86,7 +89,20 @@ export function SubtitleDialog({
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [tab, setTab] = useState<"upload" | "online">("upload");
|
||||
const list = targets.data?.targets ?? [];
|
||||
const busy = parse.isPending || apply.isPending;
|
||||
const fetchPage = useFetchSubtitle();
|
||||
const busy = parse.isPending || apply.isPending || fetchPage.isPending;
|
||||
const fetchedFrom = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (!open || !fromSession || fetchedFrom.current === fromSession) return;
|
||||
fetchedFrom.current = fromSession;
|
||||
fetchPage
|
||||
.mutateAsync({ entityId, sessionId: fromSession })
|
||||
.then((r) => {
|
||||
setRows((prev) => [...prev, ...r.items.map((it) => ({ ...it, target: it.targetLocationId ?? SKIP }))]);
|
||||
setSkipped((prev) => [...prev, ...r.skipped]);
|
||||
})
|
||||
.catch((err) => toast.error(errorMessage(err, "下载当前字幕失败")));
|
||||
}, [open, fromSession, entityId, fetchPage]);
|
||||
|
||||
const load = async (files: File[]) => {
|
||||
if (!files.length) return;
|
||||
@@ -207,6 +223,12 @@ export function SubtitleDialog({
|
||||
/>
|
||||
)}
|
||||
|
||||
{fetchPage.isPending ? (
|
||||
<p className="flex items-center gap-2 text-caption text-muted-foreground">
|
||||
<LoaderCircle className="size-4 animate-spin" />
|
||||
正在下载原站视图当前页的字幕…
|
||||
</p>
|
||||
) : null}
|
||||
{rows.length ? (
|
||||
<ul className="space-y-2">
|
||||
{rows.map((r) => (
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { BrowserSessionView } from "@umt/contracts";
|
||||
import { Download } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { EntityPicker, type PickedEntity } from "~/components/library/identity-entity-picker.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "~/components/ui/dialog.tsx";
|
||||
import { SubtitleDialog } from "./subtitle-dialog.tsx";
|
||||
|
||||
/**
|
||||
* 字幕站的原站视图:人在浏览器里打开一条字幕的详情页(验证码已在浏览器里过)→「下载当前字幕」→ 选作品 →
|
||||
* 补字幕对话框自动下载这条字幕、按季集配对到视频,可改写到哪个视频后写入。
|
||||
*/
|
||||
export function SubtitleFromPage({ session }: { session: BrowserSessionView }) {
|
||||
const [picking, setPicking] = useState(false);
|
||||
const [entity, setEntity] = useState<PickedEntity | null>(null);
|
||||
const [writing, setWriting] = useState<PickedEntity | null>(null);
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" disabled={session.controller === "closed"} onClick={() => setPicking(true)}>
|
||||
<Download />
|
||||
下载当前字幕
|
||||
</Button>
|
||||
<Dialog open={picking} onOpenChange={setPicking}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>下载当前字幕</DialogTitle>
|
||||
<DialogDescription>
|
||||
先在原站视图里打开一条字幕的详情页。选这条字幕属于哪部作品,下一步再选写到哪个视频。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<EntityPicker value={entity} onChange={setEntity} />
|
||||
<DialogFooter>
|
||||
<Button
|
||||
disabled={!entity}
|
||||
onClick={() => {
|
||||
setWriting(entity);
|
||||
setPicking(false);
|
||||
}}
|
||||
>
|
||||
下载并配对
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{writing ? (
|
||||
<SubtitleDialog
|
||||
key={writing.id}
|
||||
entityId={writing.id}
|
||||
title={writing.title}
|
||||
fromSession={session.id}
|
||||
open
|
||||
onOpenChange={(o) => !o && setWriting(null)}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@umt/web",
|
||||
"version": "0.3.24",
|
||||
"version": "0.3.25",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Reference in new issue
Block a user