mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-31 15:37:49 +00:00
handle thumbnails error (#883)
This commit is contained in:
1 parent
952d1e936c
commit
dbedb25930
7 files changed
+166
-50
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -32,6 +32,11 @@ import {
|
||||
import { CloseButton } from "@storyteller/ui-close-button";
|
||||
import { ActionReminderModal } from "@storyteller/ui-action-reminder-modal";
|
||||
import { TaskMediaFileClass } from "@storyteller/api-enums";
|
||||
import {
|
||||
getThumbnailUrl,
|
||||
THUMBNAIL_SIZES,
|
||||
getPlaceholderForMediaClass,
|
||||
} from "@storyteller/common";
|
||||
|
||||
type InProgressTask = {
|
||||
id: string;
|
||||
@@ -132,15 +137,10 @@ const CompletedCard = ({
|
||||
src={task.thumbnailUrl}
|
||||
alt={task.title}
|
||||
onError={(e) => {
|
||||
console.log("Failed to load thumbnail", e);
|
||||
let errorPlaceholder = "/resources/placeholders/placeholder.png";
|
||||
switch (task.mediaFileClass) {
|
||||
case TaskMediaFileClass.Video:
|
||||
errorPlaceholder =
|
||||
"/resources/placeholders/placeholder_play.png";
|
||||
break;
|
||||
}
|
||||
e.currentTarget.src = errorPlaceholder;
|
||||
e.currentTarget.src = getPlaceholderForMediaClass(
|
||||
task.mediaFileClass,
|
||||
);
|
||||
e.currentTarget.style.opacity = "0.3";
|
||||
}}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
@@ -290,13 +290,12 @@ export const TaskQueue = () => {
|
||||
.map((t: TaskQueueItem) => ({
|
||||
id: t.id,
|
||||
...formatTitleParts(t),
|
||||
thumbnailUrl: t.completed_item?.primary_media_file
|
||||
?.maybe_thumbnail_url_template
|
||||
? t.completed_item?.primary_media_file?.maybe_thumbnail_url_template.replace(
|
||||
"{WIDTH}",
|
||||
"250",
|
||||
)
|
||||
: undefined,
|
||||
thumbnailUrl:
|
||||
getThumbnailUrl(
|
||||
t.completed_item?.primary_media_file
|
||||
?.maybe_thumbnail_url_template,
|
||||
{ width: THUMBNAIL_SIZES.MEDIUM },
|
||||
) || undefined,
|
||||
imageUrls: t.completed_item?.primary_media_file?.cdn_url
|
||||
? [t.completed_item?.primary_media_file?.cdn_url]
|
||||
: [],
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "./lib/assets";
|
||||
export * from "./lib/enums";
|
||||
export * from "./lib/interfaces";
|
||||
export * from "./lib/types";
|
||||
export * from "./lib/utils";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./thumbnail-utils";
|
||||
@@ -0,0 +1,120 @@
|
||||
export const THUMBNAIL_SIZES = {
|
||||
SMALL: 128,
|
||||
MEDIUM: 250,
|
||||
LARGE: 512,
|
||||
EXTRA_LARGE: 1024,
|
||||
} as const;
|
||||
|
||||
export type ThumbnailSize =
|
||||
(typeof THUMBNAIL_SIZES)[keyof typeof THUMBNAIL_SIZES];
|
||||
|
||||
interface ThumbnailOptions {
|
||||
width?: ThumbnailSize | number;
|
||||
addCors?: boolean;
|
||||
}
|
||||
|
||||
export function getThumbnailUrl(
|
||||
urlTemplate: string | null | undefined,
|
||||
options: ThumbnailOptions = {}
|
||||
): string | null {
|
||||
if (!urlTemplate) return null;
|
||||
|
||||
const { width = THUMBNAIL_SIZES.MEDIUM, addCors = false } = options;
|
||||
|
||||
let url = urlTemplate.replace("{WIDTH}", width.toString());
|
||||
|
||||
if (addCors) {
|
||||
const corsUrl = addCorsParam(url);
|
||||
if (corsUrl) url = corsUrl;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function addCorsParam(url: string | null | undefined): string | null {
|
||||
if (!url) return null;
|
||||
return `${url}?cors=1`;
|
||||
}
|
||||
|
||||
interface MediaThumbnailOptions {
|
||||
size?: ThumbnailSize | number;
|
||||
addCors?: boolean;
|
||||
}
|
||||
|
||||
export function getMediaThumbnail(
|
||||
mediaLinks:
|
||||
| {
|
||||
maybe_thumbnail_template?: string | null;
|
||||
cdn_url?: string | null;
|
||||
maybe_video_previews?: {
|
||||
animated?: string | null;
|
||||
} | null;
|
||||
}
|
||||
| null
|
||||
| undefined,
|
||||
mediaClass: string | undefined,
|
||||
options: MediaThumbnailOptions = {}
|
||||
): string | null {
|
||||
if (!mediaLinks) return null;
|
||||
|
||||
const { size = THUMBNAIL_SIZES.MEDIUM, addCors = false } = options;
|
||||
|
||||
let thumbnailUrl: string | null = null;
|
||||
|
||||
if (mediaClass === "video" && mediaLinks.maybe_video_previews?.animated) {
|
||||
thumbnailUrl = mediaLinks.maybe_video_previews.animated;
|
||||
} else if (mediaLinks.maybe_thumbnail_template) {
|
||||
thumbnailUrl = getThumbnailUrl(mediaLinks.maybe_thumbnail_template, {
|
||||
width: size,
|
||||
});
|
||||
} else if (mediaLinks.cdn_url) {
|
||||
thumbnailUrl = mediaLinks.cdn_url;
|
||||
}
|
||||
|
||||
if (thumbnailUrl && addCors) {
|
||||
thumbnailUrl = addCorsParam(thumbnailUrl);
|
||||
}
|
||||
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
export function getContextImageThumbnail(
|
||||
contextImage: {
|
||||
media_links: {
|
||||
maybe_thumbnail_template?: string | null;
|
||||
cdn_url: string;
|
||||
};
|
||||
},
|
||||
options: MediaThumbnailOptions = {}
|
||||
): { thumbnail: string; fullSize: string } {
|
||||
const { size = THUMBNAIL_SIZES.SMALL } = options;
|
||||
const fullSizeWidth = THUMBNAIL_SIZES.LARGE;
|
||||
|
||||
const thumbnail = contextImage.media_links.maybe_thumbnail_template
|
||||
? getThumbnailUrl(contextImage.media_links.maybe_thumbnail_template, {
|
||||
width: size,
|
||||
}) || contextImage.media_links.cdn_url
|
||||
: contextImage.media_links.cdn_url;
|
||||
|
||||
const fullSize = contextImage.media_links.maybe_thumbnail_template
|
||||
? getThumbnailUrl(contextImage.media_links.maybe_thumbnail_template, {
|
||||
width: fullSizeWidth,
|
||||
}) || contextImage.media_links.cdn_url
|
||||
: contextImage.media_links.cdn_url;
|
||||
|
||||
return { thumbnail, fullSize };
|
||||
}
|
||||
|
||||
export const PLACEHOLDER_IMAGES = {
|
||||
DEFAULT: "/resources/gifs/artcraft-logo-load.gif",
|
||||
VIDEO: "/resources/gifs/artcraft-logo-load.gif",
|
||||
} as const;
|
||||
|
||||
export function getPlaceholderForMediaClass(mediaClass?: string): string {
|
||||
switch (mediaClass) {
|
||||
case "video":
|
||||
return PLACEHOLDER_IMAGES.VIDEO;
|
||||
default:
|
||||
return PLACEHOLDER_IMAGES.DEFAULT;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,11 @@ import {
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { GalleryDraggableItem } from "./GalleryDraggableItem";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import {
|
||||
getThumbnailUrl,
|
||||
THUMBNAIL_SIZES,
|
||||
PLACEHOLDER_IMAGES,
|
||||
} from "@storyteller/common";
|
||||
import {
|
||||
galleryModalVisibleDuringDrag,
|
||||
galleryReopenAfterDragSignal,
|
||||
@@ -262,7 +267,6 @@ export const GalleryModal = React.memo(
|
||||
response = await api.listUserMediaFiles(query);
|
||||
|
||||
if (response.success && response.data) {
|
||||
const thumbnail_size = 250;
|
||||
const newItems = response.data
|
||||
.filter(
|
||||
(item: any) => item.media_type !== FilterMediaType.SCENE_JSON
|
||||
@@ -272,15 +276,12 @@ export const GalleryModal = React.memo(
|
||||
label: getLabel(item),
|
||||
thumbnail:
|
||||
item.media_class === "video"
|
||||
? item.media_links.maybe_video_previews.animated
|
||||
? item.media_links.maybe_video_previews?.animated
|
||||
: item.media_class === "dimensional"
|
||||
? item.cover_image?.maybe_cover_image_public_bucket_url
|
||||
: item.media_links.maybe_thumbnail_template?.replace(
|
||||
"{WIDTH}",
|
||||
thumbnail_size.toString()
|
||||
),
|
||||
// TODO(bt): Thumbnail template URL may be wrong for videos and other asset types, but I'm
|
||||
// trying to plumb it through for the inpainting editor, which is just images.
|
||||
: getThumbnailUrl(item.media_links.maybe_thumbnail_template, {
|
||||
width: THUMBNAIL_SIZES.MEDIUM,
|
||||
}),
|
||||
thumbnailUrlTemplate: item.media_links.maybe_thumbnail_template,
|
||||
fullImage: item.media_links.cdn_url,
|
||||
createdAt: item.created_at,
|
||||
@@ -289,7 +290,6 @@ export const GalleryModal = React.memo(
|
||||
(item.filter_media_classes
|
||||
? item.filter_media_classes[0]
|
||||
: "image"),
|
||||
// assetType for 3D assets so drop handlers can decide between character vs object
|
||||
assetType:
|
||||
item.media_class === "dimensional"
|
||||
? item.maybe_animation_type ||
|
||||
@@ -641,9 +641,9 @@ export const GalleryModal = React.memo(
|
||||
selected={selectedItemIds.includes(item.id)}
|
||||
onClick={() => handleItemClick(item)}
|
||||
onImageError={(e) => {
|
||||
// NB: Replace the broken thumbnail with a placeholder.
|
||||
e.currentTarget.src =
|
||||
"/resources/placeholders/placeholder.png";
|
||||
PLACEHOLDER_IMAGES.DEFAULT;
|
||||
e.currentTarget.style.opacity = "0.3";
|
||||
handleImageError(item.thumbnail!);
|
||||
}}
|
||||
disableTooltipAndBadge={mode === "select"}
|
||||
|
||||
@@ -26,6 +26,12 @@ import {
|
||||
} from "@storyteller/model-list";
|
||||
import useEmblaCarousel from "embla-carousel-react";
|
||||
import type { EmblaOptionsType } from "embla-carousel";
|
||||
import {
|
||||
addCorsParam,
|
||||
getContextImageThumbnail,
|
||||
THUMBNAIL_SIZES,
|
||||
PLACEHOLDER_IMAGES,
|
||||
} from "@storyteller/common";
|
||||
|
||||
interface LightboxModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -301,7 +307,7 @@ export function LightboxModal({
|
||||
|
||||
setMediaLoaded(false);
|
||||
const img = new Image();
|
||||
img.src = `${selectedImageUrl}?cors=1`;
|
||||
img.src = addCorsParam(selectedImageUrl) || selectedImageUrl;
|
||||
|
||||
const handleLoad = () => setMediaLoaded(true);
|
||||
const handleError = () => setMediaLoaded(true);
|
||||
@@ -376,7 +382,7 @@ export function LightboxModal({
|
||||
<div className="relative flex h-full items-center justify-center overflow-hidden rounded-lg bg-black/20">
|
||||
<img
|
||||
data-lightbox-modal="true"
|
||||
src={`${url}?cors=1`}
|
||||
src={addCorsParam(url) || url}
|
||||
alt={`${imageAlt || "Generated image"} ${idx + 1}`}
|
||||
className="h-full w-full object-contain"
|
||||
onError={(e) => {
|
||||
@@ -384,7 +390,8 @@ export function LightboxModal({
|
||||
if (idx === selectedIndex) {
|
||||
setMediaLoaded(true);
|
||||
e.currentTarget.src =
|
||||
"/resources/placeholders/placeholder.png";
|
||||
PLACEHOLDER_IMAGES.DEFAULT;
|
||||
e.currentTarget.style.opacity = "0.3";
|
||||
}
|
||||
}}
|
||||
onLoad={() => {
|
||||
@@ -421,7 +428,7 @@ export function LightboxModal({
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={`${url}?cors=1`}
|
||||
src={addCorsParam(url) || url}
|
||||
alt={`Thumbnail ${idx + 1}`}
|
||||
className="h-full w-full object-cover bg-black/20"
|
||||
/>
|
||||
@@ -520,7 +527,6 @@ export function LightboxModal({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Context Images */}
|
||||
{contextImages && contextImages.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-sm font-medium text-base-fg/90">
|
||||
@@ -528,30 +534,19 @@ export function LightboxModal({
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-2">
|
||||
{contextImages.map((contextImage, index) => {
|
||||
const thumbnailUrl = contextImage.media_links
|
||||
.maybe_thumbnail_template
|
||||
? contextImage.media_links.maybe_thumbnail_template.replace(
|
||||
"{WIDTH}",
|
||||
"128"
|
||||
)
|
||||
: contextImage.media_links.cdn_url;
|
||||
|
||||
const fullSizeUrl = contextImage.media_links
|
||||
.maybe_thumbnail_template
|
||||
? contextImage.media_links.maybe_thumbnail_template.replace(
|
||||
"{WIDTH}",
|
||||
"512"
|
||||
)
|
||||
: contextImage.media_links.cdn_url;
|
||||
const { thumbnail, fullSize } =
|
||||
getContextImageThumbnail(contextImage, {
|
||||
size: THUMBNAIL_SIZES.SMALL,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={contextImage.media_token}
|
||||
className="glass relative aspect-square overflow-hidden rounded-lg w-14 border-2 border-white/30 hover:border-white/80 transition-all group cursor-pointer hover:cursor-zoom-in"
|
||||
onClick={() => setRefPreviewUrl(fullSizeUrl)}
|
||||
onClick={() => setRefPreviewUrl(fullSize)}
|
||||
>
|
||||
<img
|
||||
src={thumbnailUrl}
|
||||
src={thumbnail}
|
||||
alt={`Reference image ${index + 1}`}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
@@ -743,7 +738,7 @@ export function LightboxModal({
|
||||
</Modal.DragHandle>
|
||||
<div className="relative flex h-full items-center justify-center overflow-hidden rounded-xl bg-black/30">
|
||||
<img
|
||||
src={`${refPreviewUrl}?cors=1`}
|
||||
src={addCorsParam(refPreviewUrl) || refPreviewUrl}
|
||||
alt="Reference preview"
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user