统一音频素材封面缩略图
新增音频素材统一封面图。 素材库、创作主页精选和后台缩略图改为使用音频封面。 补充音频封面展示回归测试。
This commit is contained in:
@@ -125,12 +125,10 @@ function installIntersectionObserverMock() {
|
||||
return observers;
|
||||
}
|
||||
|
||||
function triggerIntersection(
|
||||
observer: {
|
||||
callback: IntersectionObserverCallback;
|
||||
instance: IntersectionObserver;
|
||||
},
|
||||
) {
|
||||
function triggerIntersection(observer: {
|
||||
callback: IntersectionObserverCallback;
|
||||
instance: IntersectionObserver;
|
||||
}) {
|
||||
act(() => {
|
||||
observer.callback(
|
||||
[{ isIntersecting: true } as IntersectionObserverEntry],
|
||||
@@ -157,15 +155,15 @@ describe('CreationLandingView', () => {
|
||||
|
||||
renderCreationLanding();
|
||||
|
||||
expect(
|
||||
screen.getByRole('main', { name: '陶泥儿创作主页' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole('main', { name: '陶泥儿创作主页' })).toBeTruthy();
|
||||
expect(
|
||||
screen.getByRole('heading', {
|
||||
name: '陶泥儿 - 开启全民精品游戏创作',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText('登录即送100泥点,可以免费制作50个素材')).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText('登录即送100泥点,可以免费制作50个素材'),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole('heading', { name: '创作工具' })).toBeTruthy();
|
||||
expect(screen.getByText('游戏视觉规范')).toBeTruthy();
|
||||
expect(screen.getByRole('heading', { name: '陶泥儿精选' })).toBeTruthy();
|
||||
@@ -193,7 +191,9 @@ describe('CreationLandingView', () => {
|
||||
</AuthUiContext.Provider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('heading', { name: '最近项目' })).toBeTruthy();
|
||||
expect(
|
||||
await screen.findByRole('heading', { name: '最近项目' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText('最新项目')).toBeTruthy();
|
||||
expect(screen.getByText('旧项目')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '查看全部' }));
|
||||
@@ -302,10 +302,7 @@ describe('CreationLandingView', () => {
|
||||
|
||||
render(
|
||||
<AuthUiContext.Provider value={createAuthValue()}>
|
||||
<CreationLandingView
|
||||
onOpenProject={vi.fn()}
|
||||
onOpenProjects={vi.fn()}
|
||||
/>
|
||||
<CreationLandingView onOpenProject={vi.fn()} onOpenProjects={vi.fn()} />
|
||||
</AuthUiContext.Provider>,
|
||||
);
|
||||
|
||||
@@ -375,9 +372,13 @@ describe('CreationLandingView', () => {
|
||||
const assetCard = await screen.findByText('角色英雄');
|
||||
const card = assetCard.closest('.creation-landing__asset-card');
|
||||
expect(card).toBeTruthy();
|
||||
expect(within(card as HTMLElement).getByText('character hero prompt')).toBeTruthy();
|
||||
expect(
|
||||
within(card as HTMLElement).getByText('character hero prompt'),
|
||||
).toBeTruthy();
|
||||
expect(within(card as HTMLElement).getByText('创作者A')).toBeTruthy();
|
||||
expect(within(card as HTMLElement).queryByText('user-internal-author')).toBeNull();
|
||||
expect(
|
||||
within(card as HTMLElement).queryByText('user-internal-author'),
|
||||
).toBeNull();
|
||||
expect(within(card as HTMLElement).getByText('20泥点')).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -500,18 +501,56 @@ describe('CreationLandingView', () => {
|
||||
expect(within(dialog).getByText('生成总成本')).toBeTruthy();
|
||||
expect(within(dialog).getByText('60泥点')).toBeTruthy();
|
||||
expect(within(dialog).getByText('创作者A')).toBeTruthy();
|
||||
expect(
|
||||
within(dialog).getByLabelText('素材缩略图导航'),
|
||||
).toBeTruthy();
|
||||
expect(within(dialog).getByLabelText('素材缩略图导航')).toBeTruthy();
|
||||
|
||||
await user.click(within(dialog).getByRole('button', { name: '查看待机动画' }));
|
||||
await user.click(
|
||||
within(dialog).getByRole('button', { name: '查看待机动画' }),
|
||||
);
|
||||
expect(
|
||||
within(dialog).getByRole('button', { name: '查看待机动画' }).getAttribute(
|
||||
'aria-current',
|
||||
),
|
||||
within(dialog)
|
||||
.getByRole('button', { name: '查看待机动画' })
|
||||
.getAttribute('aria-current'),
|
||||
).toBe('true');
|
||||
});
|
||||
|
||||
it('uses the unified audio cover image for featured audio assets', async () => {
|
||||
const user = userEvent.setup();
|
||||
listEditorProjectsMock.mockResolvedValueOnce(projectItems);
|
||||
listPublicEditorProjectResourcesMock.mockResolvedValueOnce([
|
||||
{
|
||||
resourceId: 'resource-music',
|
||||
projectId: 'project-newest',
|
||||
label: '背景音乐',
|
||||
imageSrc: '/generated-editor-audios/music.mp3',
|
||||
objectKey: 'generated-editor-audios/music.mp3',
|
||||
width: 1,
|
||||
height: 1,
|
||||
sourceType: 'generated',
|
||||
prompt: '舒缓背景音乐',
|
||||
actualPrompt: null,
|
||||
model: 'audio1.0',
|
||||
provider: 'ark',
|
||||
taskId: 'task-audio-1',
|
||||
assetKind: 'background-music',
|
||||
priceMudPoints: 5,
|
||||
},
|
||||
]);
|
||||
|
||||
renderCreationLanding();
|
||||
|
||||
await user.click(screen.getByRole('tab', { name: '音乐' }));
|
||||
const card = (await screen.findByText('背景音乐')).closest(
|
||||
'.creation-landing__asset-card',
|
||||
);
|
||||
expect(card).toBeTruthy();
|
||||
const image = within(card as HTMLElement).getByRole('img', {
|
||||
name: '背景音乐',
|
||||
});
|
||||
expect(image.getAttribute('src')).toBe(
|
||||
'/creation-home/audio-asset-cover.svg',
|
||||
);
|
||||
});
|
||||
|
||||
it('uses Taonier featured as the generated project resource waterfall instead of creation entries', async () => {
|
||||
listEditorProjectsMock.mockResolvedValueOnce(projectItems);
|
||||
listPublicEditorProjectResourcesMock.mockResolvedValueOnce([
|
||||
@@ -564,9 +603,9 @@ describe('CreationLandingView', () => {
|
||||
expect(await screen.findByText('素材包 A')).toBeTruthy();
|
||||
expect(screen.getByText('素材包 B')).toBeTruthy();
|
||||
expect(screen.queryByText('上传素材')).toBeNull();
|
||||
expect(
|
||||
screen.getByLabelText('用户素材瀑布流').className,
|
||||
).toContain('creation-landing__asset-waterfall');
|
||||
expect(screen.getByLabelText('用户素材瀑布流').className).toContain(
|
||||
'creation-landing__asset-waterfall',
|
||||
);
|
||||
const firstPreview = container.querySelector(
|
||||
'.creation-landing__asset-preview',
|
||||
) as HTMLElement | null;
|
||||
@@ -665,7 +704,9 @@ describe('CreationLandingView', () => {
|
||||
await waitFor(() => expect(observers).toHaveLength(1));
|
||||
triggerIntersection(observers[0]!);
|
||||
|
||||
expect(await screen.findByRole('button', { name: '读取更多素材失败,点击重试' })).toBeTruthy();
|
||||
expect(
|
||||
await screen.findByRole('button', { name: '读取更多素材失败,点击重试' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText('素材包 A')).toBeTruthy();
|
||||
expect(screen.getByLabelText('用户素材瀑布流')).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -61,8 +61,7 @@ const CREATION_HOME_ASSETS = {
|
||||
projectNew: '/creation-home/project-new.png',
|
||||
} as const;
|
||||
|
||||
const SHOWCASE_LIKE_ICON_SRC =
|
||||
'/creation-home/showcase-like-thumb.png';
|
||||
const SHOWCASE_LIKE_ICON_SRC = '/creation-home/showcase-like-thumb.png';
|
||||
|
||||
const CREATION_FEATURES: CreationFeatureItem[] = [
|
||||
{
|
||||
@@ -137,7 +136,9 @@ function mergeShowcaseResources(
|
||||
current: EditorProjectResourceSnapshot[],
|
||||
incoming: EditorProjectResourceSnapshot[],
|
||||
) {
|
||||
const seenResourceIds = new Set(current.map((resource) => resource.resourceId));
|
||||
const seenResourceIds = new Set(
|
||||
current.map((resource) => resource.resourceId),
|
||||
);
|
||||
return [
|
||||
...current,
|
||||
...incoming.filter((resource) => {
|
||||
@@ -189,10 +190,17 @@ function ShowcaseAudioPreview({
|
||||
const { resolvedUrl } = useResolvedAssetReadUrl(preview.src, {
|
||||
objectKey: preview.objectKey,
|
||||
});
|
||||
const coverSrc =
|
||||
preview.coverSrc?.trim() || '/creation-home/audio-asset-cover.svg';
|
||||
|
||||
return (
|
||||
<div className="creation-landing__asset-audio-preview">
|
||||
<Music2 aria-hidden="true" />
|
||||
<img
|
||||
alt={preview.label}
|
||||
className="creation-landing__asset-audio-cover"
|
||||
draggable={false}
|
||||
src={coverSrc}
|
||||
/>
|
||||
{controls && resolvedUrl ? (
|
||||
<audio controls preload="metadata" src={resolvedUrl} />
|
||||
) : null}
|
||||
@@ -271,11 +279,7 @@ function ShowcaseVideoPreview({
|
||||
);
|
||||
}
|
||||
|
||||
function ShowcaseBundlePreview({
|
||||
item,
|
||||
}: {
|
||||
item: ShowcaseAssetItem;
|
||||
}) {
|
||||
function ShowcaseBundlePreview({ item }: { item: ShowcaseAssetItem }) {
|
||||
const visiblePreviews = item.previews.slice(0, 4);
|
||||
const primaryPreview = visiblePreviews[0];
|
||||
|
||||
@@ -396,7 +400,9 @@ function CreationShowcaseModal({
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{isLikePending ? '处理中' : isLiked ? '已点赞' : '点赞'}</span>
|
||||
<span>
|
||||
{isLikePending ? '处理中' : isLiked ? '已点赞' : '点赞'}
|
||||
</span>
|
||||
</PlatformActionButton>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -418,7 +424,8 @@ function CreationShowcaseModal({
|
||||
onClick={() => onSelectIndex(index)}
|
||||
>
|
||||
<span className="creation-landing__showcase-thumb-media">
|
||||
{preview.mediaType === 'image' ? (
|
||||
{preview.mediaType === 'image' ||
|
||||
preview.mediaType === 'audio' ? (
|
||||
<ShowcasePreview preview={preview} variant="thumb" />
|
||||
) : (
|
||||
getPreviewIcon(preview)
|
||||
@@ -455,9 +462,13 @@ export function CreationLandingView({
|
||||
>([]);
|
||||
const [showcaseCampaign, setShowcaseCampaign] =
|
||||
useState<EditorShowcaseCampaignSnapshot | null>(null);
|
||||
const [showcaseNextCursor, setShowcaseNextCursor] = useState<string | null>(null);
|
||||
const [showcaseNextCursor, setShowcaseNextCursor] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [assetError, setAssetError] = useState<string | null>(null);
|
||||
const [showcaseLoadMoreError, setShowcaseLoadMoreError] = useState<string | null>(null);
|
||||
const [showcaseLoadMoreError, setShowcaseLoadMoreError] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [activeShowcaseTab, setActiveShowcaseTab] =
|
||||
useState<ShowcaseTabId>('packs');
|
||||
const [selectedShowcaseItem, setSelectedShowcaseItem] =
|
||||
@@ -499,9 +510,7 @@ export function CreationLandingView({
|
||||
}
|
||||
const message =
|
||||
error instanceof Error ? error.message : '读取最近项目失败';
|
||||
setProjectError(
|
||||
message,
|
||||
);
|
||||
setProjectError(message);
|
||||
})
|
||||
.finally(() => setIsLoadingProjects(false));
|
||||
}, [authUi, isAuthenticated]);
|
||||
@@ -540,47 +549,58 @@ export function CreationLandingView({
|
||||
refreshShowcaseResources();
|
||||
}, [refreshShowcaseResources]);
|
||||
|
||||
const loadMoreShowcaseResources = useCallback((options: { force?: boolean } = {}) => {
|
||||
const cursor = showcaseNextCursor;
|
||||
if (!cursor || isLoadingShowcase || isLoadingMoreShowcase) {
|
||||
return;
|
||||
}
|
||||
if (showcaseLoadingMoreCursorRef.current === cursor) {
|
||||
return;
|
||||
}
|
||||
if (!options.force && showcaseFailedLoadMoreCursorRef.current === cursor) {
|
||||
return;
|
||||
}
|
||||
showcaseLoadingMoreCursorRef.current = cursor;
|
||||
setIsLoadingMoreShowcase(true);
|
||||
setShowcaseLoadMoreError(null);
|
||||
listPublicEditorProjectResources({ cursor })
|
||||
.then((page) => {
|
||||
const normalizedPage = normalizeShowcaseResourcePage(page);
|
||||
setProjectShowcaseResources((current) =>
|
||||
mergeShowcaseResources(current, normalizedPage.resources),
|
||||
);
|
||||
setShowcaseCampaign((current) => normalizedPage.campaign ?? current);
|
||||
setShowcaseNextCursor(normalizedPage.nextCursor);
|
||||
showcaseFailedLoadMoreCursorRef.current = null;
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
showcaseFailedLoadMoreCursorRef.current = cursor;
|
||||
setShowcaseLoadMoreError(
|
||||
error instanceof Error ? error.message : '读取陶泥儿精选失败',
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
if (showcaseLoadingMoreCursorRef.current === cursor) {
|
||||
showcaseLoadingMoreCursorRef.current = null;
|
||||
}
|
||||
setIsLoadingMoreShowcase(false);
|
||||
});
|
||||
}, [isLoadingMoreShowcase, isLoadingShowcase, showcaseNextCursor]);
|
||||
const loadMoreShowcaseResources = useCallback(
|
||||
(options: { force?: boolean } = {}) => {
|
||||
const cursor = showcaseNextCursor;
|
||||
if (!cursor || isLoadingShowcase || isLoadingMoreShowcase) {
|
||||
return;
|
||||
}
|
||||
if (showcaseLoadingMoreCursorRef.current === cursor) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!options.force &&
|
||||
showcaseFailedLoadMoreCursorRef.current === cursor
|
||||
) {
|
||||
return;
|
||||
}
|
||||
showcaseLoadingMoreCursorRef.current = cursor;
|
||||
setIsLoadingMoreShowcase(true);
|
||||
setShowcaseLoadMoreError(null);
|
||||
listPublicEditorProjectResources({ cursor })
|
||||
.then((page) => {
|
||||
const normalizedPage = normalizeShowcaseResourcePage(page);
|
||||
setProjectShowcaseResources((current) =>
|
||||
mergeShowcaseResources(current, normalizedPage.resources),
|
||||
);
|
||||
setShowcaseCampaign((current) => normalizedPage.campaign ?? current);
|
||||
setShowcaseNextCursor(normalizedPage.nextCursor);
|
||||
showcaseFailedLoadMoreCursorRef.current = null;
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
showcaseFailedLoadMoreCursorRef.current = cursor;
|
||||
setShowcaseLoadMoreError(
|
||||
error instanceof Error ? error.message : '读取陶泥儿精选失败',
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
if (showcaseLoadingMoreCursorRef.current === cursor) {
|
||||
showcaseLoadingMoreCursorRef.current = null;
|
||||
}
|
||||
setIsLoadingMoreShowcase(false);
|
||||
});
|
||||
},
|
||||
[isLoadingMoreShowcase, isLoadingShowcase, showcaseNextCursor],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const element = showcaseLoadMoreRef.current;
|
||||
if (!element || !showcaseNextCursor || isLoadingShowcase || isLoadingMoreShowcase) {
|
||||
if (
|
||||
!element ||
|
||||
!showcaseNextCursor ||
|
||||
isLoadingShowcase ||
|
||||
isLoadingMoreShowcase
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (showcaseFailedLoadMoreCursorRef.current === showcaseNextCursor) {
|
||||
@@ -608,28 +628,31 @@ export function CreationLandingView({
|
||||
showcaseNextCursor,
|
||||
]);
|
||||
|
||||
const createProject = useCallback((tool?: CreationFeatureTool) => {
|
||||
if (authUi && !authUi.user) {
|
||||
authUi.openLoginModal(() => createProject(tool));
|
||||
return;
|
||||
}
|
||||
setIsCreatingProject(true);
|
||||
setProjectError(null);
|
||||
createEditorProject()
|
||||
.then((project) =>
|
||||
onOpenProject(project.projectId, tool ? { tool } : { guide: true }),
|
||||
)
|
||||
.catch((error: unknown) => {
|
||||
if (isUnauthorizedError(error)) {
|
||||
authUi?.openLoginModal(() => createProject(tool));
|
||||
return;
|
||||
}
|
||||
setProjectError(
|
||||
error instanceof Error ? error.message : '创建项目失败',
|
||||
);
|
||||
})
|
||||
.finally(() => setIsCreatingProject(false));
|
||||
}, [authUi, onOpenProject]);
|
||||
const createProject = useCallback(
|
||||
(tool?: CreationFeatureTool) => {
|
||||
if (authUi && !authUi.user) {
|
||||
authUi.openLoginModal(() => createProject(tool));
|
||||
return;
|
||||
}
|
||||
setIsCreatingProject(true);
|
||||
setProjectError(null);
|
||||
createEditorProject()
|
||||
.then((project) =>
|
||||
onOpenProject(project.projectId, tool ? { tool } : { guide: true }),
|
||||
)
|
||||
.catch((error: unknown) => {
|
||||
if (isUnauthorizedError(error)) {
|
||||
authUi?.openLoginModal(() => createProject(tool));
|
||||
return;
|
||||
}
|
||||
setProjectError(
|
||||
error instanceof Error ? error.message : '创建项目失败',
|
||||
);
|
||||
})
|
||||
.finally(() => setIsCreatingProject(false));
|
||||
},
|
||||
[authUi, onOpenProject],
|
||||
);
|
||||
|
||||
const handleFeatureClick = useCallback(
|
||||
(feature: CreationFeatureItem) => {
|
||||
@@ -646,13 +669,16 @@ export function CreationLandingView({
|
||||
(showcaseId: string, likeCount: number) => {
|
||||
setProjectShowcaseResources((current) =>
|
||||
current.map((resource) =>
|
||||
resource.showcaseId === showcaseId || resource.resourceId === showcaseId
|
||||
resource.showcaseId === showcaseId ||
|
||||
resource.resourceId === showcaseId
|
||||
? { ...resource, likeCount }
|
||||
: resource,
|
||||
),
|
||||
);
|
||||
setSelectedShowcaseItem((current) =>
|
||||
current?.showcaseId === showcaseId ? { ...current, likeCount } : current,
|
||||
current?.showcaseId === showcaseId
|
||||
? { ...current, likeCount }
|
||||
: current,
|
||||
);
|
||||
},
|
||||
[],
|
||||
@@ -783,7 +809,10 @@ export function CreationLandingView({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="creation-landing__asset-waterfall" aria-label="用户素材瀑布流">
|
||||
<div
|
||||
className="creation-landing__asset-waterfall"
|
||||
aria-label="用户素材瀑布流"
|
||||
>
|
||||
{showcaseItems.map((item) => {
|
||||
const showcaseId = item.showcaseId?.trim();
|
||||
const isLiked = Boolean(
|
||||
@@ -830,7 +859,11 @@ export function CreationLandingView({
|
||||
disabled={isLikePending}
|
||||
onClick={() => handleToggleShowcaseLike(item)}
|
||||
>
|
||||
<img src={SHOWCASE_LIKE_ICON_SRC} alt="" aria-hidden="true" />
|
||||
<img
|
||||
src={SHOWCASE_LIKE_ICON_SRC}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{item.likeCount ?? 0}</span>
|
||||
</button>
|
||||
) : null}
|
||||
@@ -1015,7 +1048,11 @@ export function CreationLandingView({
|
||||
<h2>陶泥儿精选</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="creation-landing__tabs" role="tablist" aria-label="用户素材分类">
|
||||
<div
|
||||
className="creation-landing__tabs"
|
||||
role="tablist"
|
||||
aria-label="用户素材分类"
|
||||
>
|
||||
{SHOWCASE_TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
|
||||
@@ -458,6 +458,9 @@ describe('creationShowcaseModel', () => {
|
||||
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0]?.previews[0]?.mediaType).toBe('audio');
|
||||
expect(items[0]?.previews[0]?.coverSrc).toBe(
|
||||
'/creation-home/audio-asset-cover.svg',
|
||||
);
|
||||
expect(items[0]?.author).toBe('当前用户');
|
||||
expect(items[0]?.cost).toBe('5泥点');
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ export type ShowcaseAssetPreview = {
|
||||
id: string;
|
||||
label: string;
|
||||
src: string;
|
||||
coverSrc?: string | null;
|
||||
objectKey?: string | null;
|
||||
mediaType: ShowcaseAssetMediaType;
|
||||
width?: number;
|
||||
@@ -55,6 +56,7 @@ type ShowcaseAssetGroup = {
|
||||
};
|
||||
|
||||
const UNKNOWN_META_VALUE = '-';
|
||||
const AUDIO_ASSET_COVER_SRC = '/creation-home/audio-asset-cover.svg';
|
||||
|
||||
const SHOWCASE_COST_KEYS = [
|
||||
'priceMudPoints',
|
||||
@@ -63,7 +65,10 @@ const SHOWCASE_COST_KEYS = [
|
||||
'mudPointCost',
|
||||
] as const;
|
||||
|
||||
const SHOWCASE_AUTHOR_KEYS = ['authorDisplayName', 'authorPublicUserCode'] as const;
|
||||
const SHOWCASE_AUTHOR_KEYS = [
|
||||
'authorDisplayName',
|
||||
'authorPublicUserCode',
|
||||
] as const;
|
||||
|
||||
export const SHOWCASE_TABS: Array<{ id: ShowcaseTabId; label: string }> = [
|
||||
{ id: 'packs', label: '素材包' },
|
||||
@@ -183,7 +188,10 @@ function projectResourceToShowcaseAsset(
|
||||
|
||||
return {
|
||||
...record,
|
||||
assetId: resource.assetId?.trim() || resource.showcaseId?.trim() || resource.resourceId,
|
||||
assetId:
|
||||
resource.assetId?.trim() ||
|
||||
resource.showcaseId?.trim() ||
|
||||
resource.resourceId,
|
||||
folderId: `project:${resource.projectId}`,
|
||||
label: fallbackLabel,
|
||||
imageSrc: resource.imageSrc,
|
||||
@@ -216,7 +224,9 @@ function normalizeShowcaseCategory(value: string): ShowcaseTabId | null {
|
||||
}
|
||||
|
||||
function resolveAssetShowcaseCategory(asset: EditorAssetSnapshot) {
|
||||
return normalizeShowcaseCategory(normalizeText(assetRecord(asset).showcaseCategory));
|
||||
return normalizeShowcaseCategory(
|
||||
normalizeText(assetRecord(asset).showcaseCategory),
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeShowcaseMediaRef(value: string | null | undefined) {
|
||||
@@ -307,11 +317,15 @@ function findSpecReference(asset: EditorAssetSnapshot) {
|
||||
}
|
||||
|
||||
function findCharacterReference(asset: EditorAssetSnapshot) {
|
||||
return findReference(asset, (reference) => /角色图片|角色图/u.test(reference.title));
|
||||
return findReference(asset, (reference) =>
|
||||
/角色图片|角色图/u.test(reference.title),
|
||||
);
|
||||
}
|
||||
|
||||
function findUiDesignReference(asset: EditorAssetSnapshot) {
|
||||
return findReference(asset, (reference) => /UI设计图|UI 图|UI图片/u.test(reference.title));
|
||||
return findReference(asset, (reference) =>
|
||||
/UI设计图|UI 图|UI图片/u.test(reference.title),
|
||||
);
|
||||
}
|
||||
|
||||
function isAssetKind(asset: EditorAssetSnapshot, kinds: readonly string[]) {
|
||||
@@ -336,7 +350,9 @@ function isVideoAsset(asset: EditorAssetSnapshot) {
|
||||
);
|
||||
}
|
||||
|
||||
function resolveAssetMediaType(asset: EditorAssetSnapshot): ShowcaseAssetMediaType {
|
||||
function resolveAssetMediaType(
|
||||
asset: EditorAssetSnapshot,
|
||||
): ShowcaseAssetMediaType {
|
||||
if (isAudioAsset(asset)) {
|
||||
return 'audio';
|
||||
}
|
||||
@@ -435,11 +451,16 @@ function mergeAssetAliasGroups(
|
||||
|
||||
function sortGroupAssets(assets: EditorAssetSnapshot[]) {
|
||||
return [...assets].sort((left, right) => {
|
||||
const leftRank = isSpecAsset(left) || isCharacterAsset(left) || isAssetKind(left, ['ui-design'])
|
||||
? 0
|
||||
: 1;
|
||||
const leftRank =
|
||||
isSpecAsset(left) ||
|
||||
isCharacterAsset(left) ||
|
||||
isAssetKind(left, ['ui-design'])
|
||||
? 0
|
||||
: 1;
|
||||
const rightRank =
|
||||
isSpecAsset(right) || isCharacterAsset(right) || isAssetKind(right, ['ui-design'])
|
||||
isSpecAsset(right) ||
|
||||
isCharacterAsset(right) ||
|
||||
isAssetKind(right, ['ui-design'])
|
||||
? 0
|
||||
: 1;
|
||||
if (leftRank !== rightRank) {
|
||||
@@ -454,7 +475,12 @@ function groupPackAssets(assets: EditorAssetSnapshot[]) {
|
||||
assets.forEach((asset) => {
|
||||
const specReference = findSpecReference(asset);
|
||||
if (specReference) {
|
||||
addAssetToGroup(groups, getReferenceKey(specReference), asset, specReference.label);
|
||||
addAssetToGroup(
|
||||
groups,
|
||||
getReferenceKey(specReference),
|
||||
asset,
|
||||
specReference.label,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isSpecAsset(asset)) {
|
||||
@@ -466,7 +492,10 @@ function groupPackAssets(assets: EditorAssetSnapshot[]) {
|
||||
const specAssets = assets.filter(isSpecAsset);
|
||||
specAssets.forEach((asset) => {
|
||||
const group = mergeAssetAliasGroups(groups, asset, asset.label);
|
||||
if (group && !group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)) {
|
||||
if (
|
||||
group &&
|
||||
!group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)
|
||||
) {
|
||||
group.assets.unshift(asset);
|
||||
}
|
||||
});
|
||||
@@ -500,12 +529,20 @@ function groupCharacterAssets(assets: EditorAssetSnapshot[]) {
|
||||
);
|
||||
return;
|
||||
}
|
||||
addAssetToGroup(groups, `standalone:${asset.assetId}`, asset, asset.label);
|
||||
});
|
||||
addAssetToGroup(
|
||||
groups,
|
||||
`standalone:${asset.assetId}`,
|
||||
asset,
|
||||
asset.label,
|
||||
);
|
||||
});
|
||||
const characterAssets = assets.filter(isCharacterAsset);
|
||||
characterAssets.forEach((asset) => {
|
||||
const group = mergeAssetAliasGroups(groups, asset, asset.label);
|
||||
if (group && !group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)) {
|
||||
if (
|
||||
group &&
|
||||
!group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)
|
||||
) {
|
||||
group.assets.unshift(asset);
|
||||
}
|
||||
});
|
||||
@@ -518,23 +555,43 @@ function groupCharacterAssets(assets: EditorAssetSnapshot[]) {
|
||||
function groupUiAssets(assets: EditorAssetSnapshot[]) {
|
||||
const groups = new Map<string, ShowcaseAssetGroup>();
|
||||
assets
|
||||
.filter((asset) => isUiAsset(asset) || /\bui\b|界面|图标|icon/u.test(getAssetHaystack(asset)))
|
||||
.filter(
|
||||
(asset) =>
|
||||
isUiAsset(asset) ||
|
||||
/\bui\b|界面|图标|icon/u.test(getAssetHaystack(asset)),
|
||||
)
|
||||
.forEach((asset) => {
|
||||
if (isAssetKind(asset, ['ui-design'])) {
|
||||
addAssetToGroup(groups, getSelfAssetKey(asset), asset, asset.label);
|
||||
return;
|
||||
}
|
||||
const uiReference = findUiDesignReference(asset) ?? findSpecReference(asset);
|
||||
const uiReference =
|
||||
findUiDesignReference(asset) ?? findSpecReference(asset);
|
||||
if (uiReference) {
|
||||
addAssetToGroup(groups, getReferenceKey(uiReference), asset, uiReference.label);
|
||||
addAssetToGroup(
|
||||
groups,
|
||||
getReferenceKey(uiReference),
|
||||
asset,
|
||||
uiReference.label,
|
||||
);
|
||||
return;
|
||||
}
|
||||
addAssetToGroup(groups, `standalone:${asset.assetId}`, asset, asset.label);
|
||||
});
|
||||
const uiDesignAssets = assets.filter((asset) => isAssetKind(asset, ['ui-design']));
|
||||
addAssetToGroup(
|
||||
groups,
|
||||
`standalone:${asset.assetId}`,
|
||||
asset,
|
||||
asset.label,
|
||||
);
|
||||
});
|
||||
const uiDesignAssets = assets.filter((asset) =>
|
||||
isAssetKind(asset, ['ui-design']),
|
||||
);
|
||||
uiDesignAssets.forEach((asset) => {
|
||||
const group = mergeAssetAliasGroups(groups, asset, asset.label);
|
||||
if (group && !group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)) {
|
||||
if (
|
||||
group &&
|
||||
!group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)
|
||||
) {
|
||||
group.assets.unshift(asset);
|
||||
}
|
||||
});
|
||||
@@ -588,13 +645,18 @@ function resolveAssetPrompt(asset: EditorAssetSnapshot) {
|
||||
|
||||
const generationFields = asset.generationInputs?.fields ?? [];
|
||||
const firstPromptField =
|
||||
generationFields.find((field) => /提示词|描述|设定|prompt/u.test(field.title)) ??
|
||||
generationFields[0];
|
||||
generationFields.find((field) =>
|
||||
/提示词|描述|设定|prompt/u.test(field.title),
|
||||
) ?? generationFields[0];
|
||||
return firstPromptField?.value?.trim() || UNKNOWN_META_VALUE;
|
||||
}
|
||||
|
||||
function resolveGroupPrompt(assets: EditorAssetSnapshot[]) {
|
||||
return assets.map(resolveAssetPrompt).find((value) => value !== UNKNOWN_META_VALUE) ?? UNKNOWN_META_VALUE;
|
||||
return (
|
||||
assets
|
||||
.map(resolveAssetPrompt)
|
||||
.find((value) => value !== UNKNOWN_META_VALUE) ?? UNKNOWN_META_VALUE
|
||||
);
|
||||
}
|
||||
|
||||
function resolveAssetAuthor(
|
||||
@@ -672,7 +734,12 @@ function inferAssetCost(asset: EditorAssetSnapshot) {
|
||||
| '480p'
|
||||
| '720p'
|
||||
| '1080p',
|
||||
normalizeInt(asset.durationSeconds, DEFAULT_VIDEO_DURATION_SECONDS, 4, 15),
|
||||
normalizeInt(
|
||||
asset.durationSeconds,
|
||||
DEFAULT_VIDEO_DURATION_SECONDS,
|
||||
4,
|
||||
15,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (asset.sourceType === 'generated' || kind) {
|
||||
@@ -715,12 +782,14 @@ function resolveGroupCost(assets: EditorAssetSnapshot[]) {
|
||||
}
|
||||
|
||||
function toAssetPreview(asset: EditorAssetSnapshot): ShowcaseAssetPreview {
|
||||
const mediaType = resolveAssetMediaType(asset);
|
||||
return {
|
||||
id: asset.assetId,
|
||||
label: asset.label,
|
||||
src: asset.imageSrc,
|
||||
coverSrc: mediaType === 'audio' ? AUDIO_ASSET_COVER_SRC : null,
|
||||
objectKey: asset.objectKey,
|
||||
mediaType: resolveAssetMediaType(asset),
|
||||
mediaType,
|
||||
width: asset.width,
|
||||
height: asset.height,
|
||||
};
|
||||
@@ -731,7 +800,9 @@ function toLibraryShowcaseItem(
|
||||
fallbackAuthorName?: string | null,
|
||||
): ShowcaseAssetItem | null {
|
||||
const sortedAssets = sortGroupAssets(group.assets);
|
||||
const previews = sortedAssets.map(toAssetPreview).filter((preview) => preview.src.trim());
|
||||
const previews = sortedAssets
|
||||
.map(toAssetPreview)
|
||||
.filter((preview) => preview.src.trim());
|
||||
if (!previews.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -27,25 +27,30 @@ vi.mock('../common/PlatformMediaFrame', () => ({
|
||||
}) => {
|
||||
const imageSrc =
|
||||
src?.trim() ||
|
||||
(objectKey?.trim() ? `https://signed.example.com/${objectKey.trim()}` : '');
|
||||
(objectKey?.trim()
|
||||
? `https://signed.example.com/${objectKey.trim()}`
|
||||
: '');
|
||||
return (
|
||||
<div className="platform-media-frame">
|
||||
{imageSrc ? (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={alt}
|
||||
data-object-key={objectKey?.trim() || undefined}
|
||||
/>
|
||||
) : null}
|
||||
{previewOverlay}
|
||||
</div>
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={alt}
|
||||
data-object-key={objectKey?.trim() || undefined}
|
||||
/>
|
||||
) : null}
|
||||
{previewOverlay}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
useResolvedAssetReadUrlMock.mockImplementation(
|
||||
(source: string | null | undefined, options?: { objectKey?: string | null }) => ({
|
||||
(
|
||||
source: string | null | undefined,
|
||||
options?: { objectKey?: string | null },
|
||||
) => ({
|
||||
resolvedUrl: options?.objectKey
|
||||
? `https://signed.example.com/${options.objectKey}`
|
||||
: (source ?? ''),
|
||||
@@ -138,9 +143,7 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
const onDownloadAsset = vi.fn();
|
||||
renderAssetRow({ asset, onDownloadAsset });
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: '下载素材账号素材A' }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '下载素材账号素材A' }));
|
||||
|
||||
expect(onDownloadAsset).toHaveBeenCalledWith(asset);
|
||||
});
|
||||
@@ -154,20 +157,26 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
const onDownloadAsset = vi.fn();
|
||||
renderAssetRow({ asset, addAssetLayer, onDownloadAsset });
|
||||
|
||||
fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), {
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
});
|
||||
fireEvent.contextMenu(
|
||||
screen.getByRole('button', { name: '添加账号素材A' }),
|
||||
{
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
},
|
||||
);
|
||||
expect(screen.getByRole('menu')).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '下载素材账号素材A' }));
|
||||
expect(onDownloadAsset).toHaveBeenCalledWith(asset);
|
||||
expect(screen.queryByRole('menu')).toBeNull();
|
||||
|
||||
fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), {
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
});
|
||||
fireEvent.contextMenu(
|
||||
screen.getByRole('button', { name: '添加账号素材A' }),
|
||||
{
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
},
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '添加账号素材A' }));
|
||||
expect(addAssetLayer).toHaveBeenCalledWith(asset);
|
||||
expect(screen.queryByRole('menu')).toBeNull();
|
||||
@@ -199,7 +208,9 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
fireEvent.keyDown(input, { key: 'Enter' });
|
||||
|
||||
expect(input.className).toContain('platform-text-field');
|
||||
expect(input.className).toContain('image-canvas-editor__asset-rename-input');
|
||||
expect(input.className).toContain(
|
||||
'image-canvas-editor__asset-rename-input',
|
||||
);
|
||||
expect(commitAssetRename).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: 'asset-1' }),
|
||||
);
|
||||
@@ -226,7 +237,7 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
).toBe('42');
|
||||
});
|
||||
|
||||
it('uses icon thumbnails for audio assets instead of rendering media as images', () => {
|
||||
it('uses the audio cover thumbnail for audio assets', () => {
|
||||
renderAssetRow({
|
||||
asset: createAsset({
|
||||
label: '胜利音效.mp3',
|
||||
@@ -236,13 +247,13 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
});
|
||||
|
||||
expect(screen.getByText('音频')).toBeTruthy();
|
||||
expect(screen.queryByRole('img', { name: '素材:胜利音效.mp3' })).toBeNull();
|
||||
const image = screen.getByRole('img', { name: '素材:胜利音效.mp3' });
|
||||
expect(image.getAttribute('src')).toBe(
|
||||
'/creation-home/audio-asset-cover.svg',
|
||||
);
|
||||
expect(
|
||||
document.querySelector('.image-canvas-editor__asset-media-overlay'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
document.querySelector('.image-canvas-editor__media-preview--audio'),
|
||||
).toBeTruthy();
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('uses a stable visual preview for video asset cards without image thumbnails', () => {
|
||||
@@ -250,13 +261,16 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
asset: createAsset({
|
||||
label: '生成视频.mp4',
|
||||
src: '/generated-character-drafts/editor/asset-library/video/demo.mp4',
|
||||
objectKey: 'generated-character-drafts/editor/asset-library/video/demo.mp4',
|
||||
objectKey:
|
||||
'generated-character-drafts/editor/asset-library/video/demo.mp4',
|
||||
mediaType: 'video',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(screen.getByText('视频')).toBeTruthy();
|
||||
expect(screen.queryByRole('img', { name: '素材:生成视频.mp4' })).toBeNull();
|
||||
expect(
|
||||
screen.queryByRole('img', { name: '素材:生成视频.mp4' }),
|
||||
).toBeNull();
|
||||
const video = screen.getByLabelText(
|
||||
'视频素材预览:生成视频.mp4',
|
||||
) as HTMLVideoElement;
|
||||
@@ -315,10 +329,13 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
screen.queryByRole('button', { name: '删除素材账号素材A' }),
|
||||
).toBeNull();
|
||||
|
||||
fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), {
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
});
|
||||
fireEvent.contextMenu(
|
||||
screen.getByRole('button', { name: '添加账号素材A' }),
|
||||
{
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
},
|
||||
);
|
||||
|
||||
const submitMenuItem = screen.getByRole('menuitem', {
|
||||
name: /提交精选审核/u,
|
||||
@@ -327,10 +344,13 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
fireEvent.click(submitMenuItem);
|
||||
expect(submitAssetToShowcase).toHaveBeenCalledWith(asset);
|
||||
|
||||
fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), {
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
});
|
||||
fireEvent.contextMenu(
|
||||
screen.getByRole('button', { name: '添加账号素材A' }),
|
||||
{
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
},
|
||||
);
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '删除' }));
|
||||
expect(deleteUploadedAsset).toHaveBeenCalledWith(asset);
|
||||
});
|
||||
@@ -339,10 +359,13 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
const submitAssetToShowcase = vi.fn();
|
||||
renderAssetRow({ submitAssetToShowcase });
|
||||
|
||||
fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), {
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
});
|
||||
fireEvent.contextMenu(
|
||||
screen.getByRole('button', { name: '添加账号素材A' }),
|
||||
{
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
},
|
||||
);
|
||||
|
||||
const submitMenuItem = screen.getByRole('menuitem', {
|
||||
name: /提交精选审核/u,
|
||||
@@ -360,10 +383,13 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
const submitAssetToShowcase = vi.fn();
|
||||
renderAssetRow({ asset, submitAssetToShowcase });
|
||||
|
||||
fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), {
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
});
|
||||
fireEvent.contextMenu(
|
||||
screen.getByRole('button', { name: '添加账号素材A' }),
|
||||
{
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
},
|
||||
);
|
||||
|
||||
const submitMenuItem = screen.getByRole('menuitem', {
|
||||
name: /提交精选审核/u,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Check, Download, Music2, Pencil, Video, X } from 'lucide-react';
|
||||
import { Check, Download, Pencil, Video, X } from 'lucide-react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
getCanvasMediaPreviewMarker,
|
||||
} from './ImageCanvasMediaModel';
|
||||
|
||||
const AUDIO_ASSET_COVER_SRC = '/creation-home/audio-asset-cover.svg';
|
||||
|
||||
export type ImageCanvasAssetRowViewProps = {
|
||||
asset: EditorAsset;
|
||||
assetPointerDragRef: { current: AssetPointerDragState | null };
|
||||
@@ -38,7 +40,10 @@ export type ImageCanvasAssetRowViewProps = {
|
||||
setAssetPointerDrag: Dispatch<SetStateAction<AssetPointerDragState | null>>;
|
||||
setSelectedAssetIds: Dispatch<SetStateAction<Set<string>>>;
|
||||
updateAssetMoveDropFolder: (folderId: string | null) => void;
|
||||
addUploadedFiles: (files: FileList | File[], options?: UploadFilesOptions) => void;
|
||||
addUploadedFiles: (
|
||||
files: FileList | File[],
|
||||
options?: UploadFilesOptions,
|
||||
) => void;
|
||||
moveAssetToFolder: (assetId: string, folderId: string) => void;
|
||||
startRenamingAsset: (asset: EditorAsset) => void;
|
||||
commitAssetRename: (asset: EditorAsset) => void;
|
||||
@@ -81,7 +86,9 @@ export function ImageCanvasAssetRowView({
|
||||
const isFailedUpload = asset.uploadStatus === 'failed';
|
||||
const showcaseReviewStatus = asset.showcaseReviewStatus ?? null;
|
||||
const canSubmitShowcaseAsset =
|
||||
asset.sourceType === 'generated' && asset.persisted && !showcaseReviewStatus;
|
||||
asset.sourceType === 'generated' &&
|
||||
asset.persisted &&
|
||||
!showcaseReviewStatus;
|
||||
const showcaseMenuLabel =
|
||||
showcaseReviewStatus === 'pending'
|
||||
? '精选审核中'
|
||||
@@ -153,13 +160,7 @@ export function ImageCanvasAssetRowView({
|
||||
? '视频'
|
||||
: `${asset.width} x ${asset.height}`;
|
||||
const mediaOverlay =
|
||||
asset.mediaType === 'audio' ? (
|
||||
<div
|
||||
className={`image-canvas-editor__asset-media-overlay ${mediaPreviewClassName}`}
|
||||
>
|
||||
<Music2 className="h-5 w-5" />
|
||||
</div>
|
||||
) : asset.mediaType === 'video' ? (
|
||||
asset.mediaType === 'video' ? (
|
||||
<div
|
||||
className={`image-canvas-editor__asset-media-overlay ${mediaPreviewClassName}`}
|
||||
>
|
||||
@@ -279,9 +280,7 @@ export function ImageCanvasAssetRowView({
|
||||
rowClassName={[
|
||||
'image-canvas-editor__asset-row',
|
||||
isUploadingAsset ? 'image-canvas-editor__asset-row--uploading' : '',
|
||||
isFailedUpload
|
||||
? 'image-canvas-editor__asset-row--upload-failed'
|
||||
: '',
|
||||
isFailedUpload ? 'image-canvas-editor__asset-row--upload-failed' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
@@ -291,12 +290,18 @@ export function ImageCanvasAssetRowView({
|
||||
imageSrc={
|
||||
asset.mediaType === 'image'
|
||||
? asset.src
|
||||
: (asset.thumbnailSrc ?? '')
|
||||
: asset.mediaType === 'audio'
|
||||
? AUDIO_ASSET_COVER_SRC
|
||||
: (asset.thumbnailSrc ?? '')
|
||||
}
|
||||
objectKey={asset.mediaType === 'image' ? asset.objectKey : undefined}
|
||||
previewNode={videoPreview}
|
||||
titleNode={
|
||||
isUploadingAsset || isFailedUpload ? <span>{asset.label}</span> : titleNode
|
||||
isUploadingAsset || isFailedUpload ? (
|
||||
<span>{asset.label}</span>
|
||||
) : (
|
||||
titleNode
|
||||
)
|
||||
}
|
||||
actions={actions}
|
||||
draggable={!isRenaming && !isUploadingAsset && !isFailedUpload}
|
||||
|
||||
Reference in New Issue
Block a user