import { Pause, Play, X } from 'lucide-react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { parseSignedReadUrlExpiresAtMs, SIGNED_READ_URL_CACHE_SAFETY_WINDOW_MS, } from '../../../../packages/shared/src/utils/signedReadUrlCache'; import type { AdminAssetReadUrlResponse } from '../api/adminApiClient'; import { getAdminAssetReadUrl, isAdminApiError } from '../api/adminApiClient'; import type { AdminEditorImageSequenceFramePayload } from '../api/adminApiTypes'; const ADMIN_ASSET_READ_EXPIRE_SECONDS = 300; const ADMIN_ASSET_READ_DISPATCH_SPACING_MS = 40; const ADMIN_ASSET_READ_RETRY_DELAYS_MS = [400, 1_200, 3_000] as const; const ADMIN_ASSET_THUMBNAIL_ROOT_MARGIN = '240px 0px'; const AUDIO_ASSET_COVER_SRC = `${import.meta.env.DEV ? import.meta.env.BASE_URL : '/'}creation-home/audio-asset-cover.png`; let adminAssetReadDispatchTail = Promise.resolve(); export interface AdminPreviewableEditorAsset { assetId: string; label: string; imageSrc: string; objectKey?: string | null; assetKind?: string | null; thumbnailSrc?: string | null; imageSequenceFrames?: AdminEditorImageSequenceFramePayload[] | null; imageSequenceDurationMs?: number | null; sourceResourceId?: string | null; sourceImageSrc?: string | null; sourceObjectKey?: string | null; sourceAssetObjectId?: string | null; sourceLabel?: string | null; } export function AdminEditorAssetThumbnail({ entry, token, altPrefix = '素材', }: { entry: AdminPreviewableEditorAsset; token: string; altPrefix?: string; }) { const thumbnailSource = resolveAdminAssetThumbnailSource(entry); const { observeElement, shouldLoad } = useAdminAssetThumbnailVisibility(); const { resolvedUrl: imageSrc } = useAdminResolvedAssetUrl( token, thumbnailSource.src, thumbnailSource.objectKey, shouldLoad, ); const alt = `${altPrefix}:${entry.label || entry.assetId}`; return imageSrc ? ( {alt} ) : (
); } export function AdminEditorAssetPreviewDialog({ entry, token, onClose, }: { entry: AdminPreviewableEditorAsset; token: string; onClose: () => void; }) { const sourceEntry = entry.sourceImageSrc?.trim() ? { assetId: entry.sourceResourceId ?? `${entry.assetId}-source`, label: entry.sourceLabel?.trim() || '原图', imageSrc: entry.sourceImageSrc, objectKey: entry.sourceObjectKey, } : null; return (

{entry.label || entry.assetId}

{entry.assetId}
{sourceEntry ? (

原图

) : null}
); } export function AdminEditorAssetPreviewMedia({ entry, token, }: { entry: AdminPreviewableEditorAsset; token: string; }) { const mediaKind = resolveAdminAssetMediaKind(entry); const label = entry.label || entry.assetId; if (mediaKind === 'image-sequence') { const sequence = normalizeAdminImageSequence(entry); return sequence ? ( ) : (
序列数据损坏,无法预览
); } return ( ); } function AdminEditorStandardAssetPreviewMedia({ entry, mediaKind, token, }: { entry: AdminPreviewableEditorAsset; mediaKind: Exclude; token: string; }) { const isAudio = mediaKind === 'audio'; const isVideo = mediaKind === 'video'; const { resolvedUrl: mediaSrc } = useAdminResolvedAssetUrl( token, entry.imageSrc, entry.objectKey, ); const { resolvedUrl: posterSrc } = useAdminResolvedAssetUrl( token, isVideo ? (entry.thumbnailSrc ?? '') : '', null, ); const label = entry.label || entry.assetId; if (isAudio) { return (
{`音频封面:${label}`} {mediaSrc ? (