feat(canvas): 画布能力 - 完善 LibTV 导入与资源预览

This commit is contained in:
ddCat
2026-08-19 07:16:07 +08:00
parent 4b9d510129
commit d2a089f1ec
36 changed files with 1722 additions and 68 deletions

View File

@@ -6,6 +6,7 @@ import { CONTENT_MODERATION_ERROR_CODE, generationErrorMessage, isContentModerat
import { generationTaskShowsProgress, generationTaskStageLabel, generationTaskStatusLabel, isGenerationTaskSubmissionUncertain } from "@/lib/generation-task-display";
import { canvasRichTextHTML } from "@/lib/canvas/canvas-rich-text";
import { loadCanvasDrawingPreview } from "@/lib/canvas/canvas-drawing-storage";
import { buildLibTVImagePreviewUrl } from "@/lib/canvas/libtv-import";
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
import type { CanvasTheme } from "@/lib/canvas-theme";
import { formatBytes } from "@/lib/image-utils";
@@ -417,10 +418,11 @@ function ImageContent({ node, theme, isBatchRoot, batchCount, batchExpanded, bat
const imageContainerRef = useRef<HTMLDivElement>(null);
const nearViewport = useNearViewport(imageContainerRef);
const { url, loading } = useNodeResourceUrl(node, nearViewport);
const importedFromLibTV = node.metadata?.importSource?.provider === "libtv";
return (
<BatchFrame batchCount={isBatchRoot ? batchCount : 0} batchExpanded={batchExpanded} batchOpening={batchOpening} batchRecovering={batchRecovering} theme={theme} onToggleBatch={onToggleBatch}>
<div ref={imageContainerRef} className="h-full w-full overflow-hidden rounded-[var(--node-radius)]">
{url ? <img src={url} alt={node.title} loading="lazy" decoding="async" draggable={false} onDragStart={(event) => event.preventDefault()} className={`pointer-events-none block h-full w-full select-none ${node.metadata?.freeResize ? "object-fill" : "object-contain"}`} /> : <div className="grid size-full place-items-center" style={{ color: theme.node.muted }}>{loading ? <LoaderCircle className="size-5 animate-spin" /> : <ImageIcon className="size-5 opacity-45" />}</div>}
{url ? <img src={url} alt={node.title} loading={importedFromLibTV ? "eager" : "lazy"} decoding="async" draggable={false} onDragStart={(event) => event.preventDefault()} className={`pointer-events-none block h-full w-full select-none ${node.metadata?.freeResize ? "object-fill" : "object-contain"}`} /> : <div className="grid size-full place-items-center" style={{ color: theme.node.muted }}>{loading ? <LoaderCircle className="size-5 animate-spin" /> : <ImageIcon className="size-5 opacity-45" />}</div>}
</div>
</BatchFrame>
);
@@ -432,7 +434,9 @@ function DeferredMediaLoad({ icon, label, disabled, onClick }: { icon: ReactNode
function useNodeResourceUrl(node: CanvasNodeData, eager: boolean) {
const storageKey = node.metadata?.storageKey || "";
const fallback = node.metadata?.content || "";
const content = node.metadata?.content || "";
const fallback = node.metadata?.previewContent
|| (node.type === CanvasNodeType.Image && node.metadata?.importSource?.provider === "libtv" ? buildLibTVImagePreviewUrl(content) : content);
const isRemoteResource = Boolean(resourceIdFromStorageKey(storageKey));
const [url, setUrl] = useState(isRemoteResource ? "" : fallback);
const [loading, setLoading] = useState(isRemoteResource && eager);