feat(web): 前端插槽设施,作品页与整理记录行挂「补字幕」
- lib/slots.ts 按位置注册(侧栏入口、作品页操作区 / 面板区、整理记录行操作、设置分区),默认直接显示; components/slots 挂载点:面板类直接渲染,动作类渲染成按钮 / 菜单项、对话框在菜单外渲染 - 作品页操作区与面板区、整理记录行菜单接入插槽;补字幕改由 features/subtitles.tsx 注册, 作品页按钮行为不变,整理记录行菜单新增「补字幕」(成功 / 部分成功的入库类整理) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
079c3eafe9
commit
62119a9b30
7 files changed
+313
-11
No files matched your search
@@ -14,7 +14,6 @@ import {
|
||||
ArrowLeft,
|
||||
CalendarClock,
|
||||
CalendarPlus,
|
||||
Captions,
|
||||
ChevronRight,
|
||||
ExternalLink,
|
||||
FolderTree,
|
||||
@@ -89,9 +88,9 @@ import {
|
||||
import { EntityNamingButton } from "~/components/naming/entity-naming-dialog.tsx";
|
||||
import { useReleaseSearchLauncher } from "~/components/search/search-sections.tsx";
|
||||
import { EmbyBadge } from "~/components/search/work-card.tsx";
|
||||
import { SlotPanels, useSlotActions } from "~/components/slots/slot-mounts.tsx";
|
||||
import { CELL_LABEL, type CellKind, cellKind } from "~/components/subscriptions/model.ts";
|
||||
import { SubscriptionFormDialog } from "~/components/subscriptions/subscription-form.tsx";
|
||||
import { SubtitleDialog } from "~/components/subtitles/subtitle-dialog.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { useUiPrefs } from "~/lib/ui-prefs.ts";
|
||||
@@ -1110,7 +1109,7 @@ export default function LibraryDetailPage() {
|
||||
const entity = useEntityDetail(id);
|
||||
const [merging, setMerging] = useState(false);
|
||||
const [subscribing, setSubscribing] = useState(false);
|
||||
const [subtitling, setSubtitling] = useState(false);
|
||||
const entityActions = useSlotActions("entity.actions");
|
||||
const launcher = useReleaseSearchLauncher();
|
||||
const ms = mediaServers.useList();
|
||||
|
||||
@@ -1147,6 +1146,7 @@ export default function LibraryDetailPage() {
|
||||
}
|
||||
const d = detail.data;
|
||||
const e = d.entity;
|
||||
const slotEntity = { id: e.id, title: e.title, kind: e.kind, status: e.status };
|
||||
const redirected = entity.data?.redirectedFrom;
|
||||
const credits = {
|
||||
cast: creditList(metaValue(e.metadata, "cast")),
|
||||
@@ -1200,12 +1200,13 @@ export default function LibraryDetailPage() {
|
||||
合并作品
|
||||
</Button>
|
||||
) : null}
|
||||
{e.status !== "merged" ? (
|
||||
<Button variant="outline" size="sm" onClick={() => setSubtitling(true)}>
|
||||
<Captions />
|
||||
补字幕
|
||||
{/* 作品页操作区插槽(ADR-006):补字幕等功能模块的入口 */}
|
||||
{entityActions.items({ entity: slotEntity }).map((a) => (
|
||||
<Button key={a.id} variant="outline" size="sm" onClick={() => entityActions.open(a, { entity: slotEntity })}>
|
||||
{a.icon ? <a.icon /> : null}
|
||||
{a.label}
|
||||
</Button>
|
||||
) : null}
|
||||
))}
|
||||
<EntityNamingButton entityId={e.id} kind={e.kind} />
|
||||
</div>
|
||||
<TechInfo className="-mt-3 mb-5" items={[{ label: "作品 ID", value: e.id, wide: true }]} />
|
||||
@@ -1230,6 +1231,8 @@ export default function LibraryDetailPage() {
|
||||
<UnitsPanel entity={entity} detail={d} />
|
||||
) : null}
|
||||
<DownloadsPanel detail={d} />
|
||||
{/* 作品页面板区插槽(ADR-006) */}
|
||||
<SlotPanels position="entity.panels" props={{ entity: slotEntity }} />
|
||||
</div>
|
||||
<MergeDialog entity={e} open={merging} onOpenChange={setMerging} />
|
||||
{subscribing ? (
|
||||
@@ -1244,9 +1247,7 @@ export default function LibraryDetailPage() {
|
||||
onClose={() => setSubscribing(false)}
|
||||
/>
|
||||
) : null}
|
||||
{subtitling ? (
|
||||
<SubtitleDialog entityId={e.id} title={e.title} open onOpenChange={(o) => !o && setSubtitling(false)} />
|
||||
) : null}
|
||||
{entityActions.dialog}
|
||||
{launcher.dialog}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -66,9 +66,11 @@ import {
|
||||
} from "~/components/organize/record-parts.ts";
|
||||
import { SourceSelection } from "~/components/organize/source-selection.tsx";
|
||||
import { type OrganizeSource, OrganizeWizard } from "~/components/organize-wizard/organize-wizard.tsx";
|
||||
import { useSlotActions } from "~/components/slots/slot-mounts.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover.tsx";
|
||||
import { reasonLine } from "~/lib/reasons.ts";
|
||||
import type { ActionSlot } from "~/lib/slots.ts";
|
||||
import { useSse } from "~/lib/sse.ts";
|
||||
import { cn, formatBytes, formatDateTime } from "~/lib/utils.ts";
|
||||
|
||||
@@ -222,12 +224,17 @@ function RowMenu({
|
||||
onReorganize,
|
||||
onDelete,
|
||||
onOnlyEntity,
|
||||
slotActions,
|
||||
onSlotAction,
|
||||
}: {
|
||||
r: Rec;
|
||||
onOpen: () => void;
|
||||
onReorganize: () => void;
|
||||
onDelete: () => void;
|
||||
onOnlyEntity: () => void;
|
||||
/** 整理记录行操作插槽(ADR-006):功能模块注册的菜单项 */
|
||||
slotActions: ActionSlot<"organize.record.actions">[];
|
||||
onSlotAction: (a: ActionSlot<"organize.record.actions">) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const run = (fn: () => void) => () => {
|
||||
@@ -276,6 +283,11 @@ function RowMenu({
|
||||
只看这个作品
|
||||
</MenuItem>
|
||||
) : null}
|
||||
{slotActions.map((a) => (
|
||||
<MenuItem key={a.id} icon={a.icon ? <a.icon /> : null} onClick={run(() => onSlotAction(a))}>
|
||||
{a.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
{!isRunning(r) ? (
|
||||
<MenuItem icon={<Trash2 />} onClick={run(onDelete)} danger>
|
||||
删除…
|
||||
@@ -356,6 +368,7 @@ function StatsLine({
|
||||
|
||||
export default function OrganizePage() {
|
||||
const [params, setParams] = useSearchParams();
|
||||
const recordActions = useSlotActions("organize.record.actions");
|
||||
const f = useQueryState(RECORD_FILTER_KEYS);
|
||||
const { set: setFilter, values: v } = f;
|
||||
const [qInput, setQInput] = useState(v.q);
|
||||
@@ -636,6 +649,8 @@ export default function OrganizePage() {
|
||||
v.group ? { mediaEntityId: r.media.entityId, group: v.group } : { mediaEntityId: r.media.entityId },
|
||||
)
|
||||
}
|
||||
slotActions={recordActions.items({ record: r })}
|
||||
onSlotAction={(a) => recordActions.open(a, { record: r })}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -771,6 +786,7 @@ export default function OrganizePage() {
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{recordActions.dialog}
|
||||
<DeleteRecordsDialog
|
||||
records={toDelete ?? []}
|
||||
open={!!toDelete}
|
||||
|
||||
Reference in new issue
Block a user