mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
fix(ui): browser image caching cors race condition
Must set cross origin whenever we load an image from a URL to prevent race conditions where browser caches an image with no CORS, then canvas attempts to load it with CORS, resulting in browser rejecting the request before it is made
This commit is contained in:
committed by
Mary Hipp Rogers
parent
75daef2aba
commit
0e523ca2c1
@@ -1,6 +1,11 @@
|
||||
import { atom } from 'nanostores';
|
||||
import { atom, computed } from 'nanostores';
|
||||
|
||||
/**
|
||||
* The user's auth token.
|
||||
*/
|
||||
export const $authToken = atom<string | undefined>();
|
||||
|
||||
/**
|
||||
* The crossOrigin value to use for all image loading. Depends on whether the user is authenticated.
|
||||
*/
|
||||
export const $crossOrigin = computed($authToken, (token) => (token ? 'use-credentials' : 'anonymous'));
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex, Icon, IconButton, Image, Skeleton, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { $crossOrigin } from 'app/store/nanostores/authToken';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { round } from 'es-toolkit/compat';
|
||||
import { useRefImageEntity } from 'features/controlLayers/components/RefImage/useRefImageEntity';
|
||||
@@ -68,6 +70,7 @@ export const RefImagePreview = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const id = useRefImageIdContext();
|
||||
const entity = useRefImageEntity(id);
|
||||
const crossOrigin = useStore($crossOrigin);
|
||||
const mainModelConfig = useAppSelector(selectMainModelConfig);
|
||||
const selectedEntityId = useAppSelector(selectSelectedRefEntityId);
|
||||
const isPanelOpen = useAppSelector(selectIsRefImagePanelOpen);
|
||||
@@ -146,6 +149,7 @@ export const RefImagePreview = memo(() => {
|
||||
>
|
||||
<Image
|
||||
src={imageDTO?.thumbnail_url}
|
||||
crossOrigin={crossOrigin}
|
||||
objectFit="contain"
|
||||
aspectRatio="1/1"
|
||||
height={imageDTO?.height}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Selector, Store } from '@reduxjs/toolkit';
|
||||
import { $authToken } from 'app/store/nanostores/authToken';
|
||||
import { $authToken, $crossOrigin } from 'app/store/nanostores/authToken';
|
||||
import { roundDownToMultiple, roundUpToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import { clamp } from 'es-toolkit/compat';
|
||||
import type {
|
||||
@@ -494,7 +494,7 @@ export async function loadImage(src: string, fetchUrlFirst?: boolean): Promise<H
|
||||
const imageElement = new Image();
|
||||
imageElement.onload = () => resolve(imageElement);
|
||||
imageElement.onerror = (error) => reject(error);
|
||||
imageElement.crossOrigin = $authToken.get() ? 'use-credentials' : 'anonymous';
|
||||
imageElement.crossOrigin = $crossOrigin.get();
|
||||
imageElement.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import type { ImageProps, SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Image } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $crossOrigin } from 'app/store/nanostores/authToken';
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { singleImageDndSource } from 'features/dnd/dnd';
|
||||
import type { DndDragPreviewSingleImageState } from 'features/dnd/DndDragPreviewSingleImage';
|
||||
@@ -29,6 +31,8 @@ type Props = {
|
||||
export const DndImage = memo(
|
||||
forwardRef(({ imageDTO, asThumbnail, ...rest }: Props, forwardedRef) => {
|
||||
const store = useAppStore();
|
||||
const crossOrigin = useStore($crossOrigin);
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const ref = useRef<HTMLImageElement>(null);
|
||||
useImperativeHandle(forwardedRef, () => ref.current!, []);
|
||||
@@ -76,6 +80,7 @@ export const DndImage = memo(
|
||||
height={imageDTO.height}
|
||||
sx={sx}
|
||||
data-is-dragging={isDragging}
|
||||
crossOrigin={crossOrigin}
|
||||
{...rest}
|
||||
/>
|
||||
{dragPreviewState?.type === 'single-image' ? createSingleImageDragPreview(dragPreviewState) : null}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Flex, Image, Text } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { $crossOrigin } from 'app/store/nanostores/authToken';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
@@ -16,6 +18,8 @@ type Props = {
|
||||
|
||||
export const BoardTooltip = ({ board, boardCounts }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const crossOrigin = useStore($crossOrigin);
|
||||
|
||||
const isVideoEnabled = useFeatureStatus('video');
|
||||
|
||||
const { currentData: coverImage } = useGetImageDTOQuery(board?.cover_image_name ?? skipToken);
|
||||
@@ -25,6 +29,7 @@ export const BoardTooltip = ({ board, boardCounts }: Props) => {
|
||||
{coverImage && (
|
||||
<Image
|
||||
src={coverImage.thumbnail_url}
|
||||
crossOrigin={crossOrigin}
|
||||
draggable={false}
|
||||
objectFit="cover"
|
||||
maxW={150}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex, Icon, Image, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { $crossOrigin } from 'app/store/nanostores/authToken';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import type { AddImageToBoardDndTargetData } from 'features/dnd/dnd';
|
||||
import { addImageToBoardDndTarget } from 'features/dnd/dnd';
|
||||
@@ -34,6 +36,7 @@ interface GalleryBoardProps {
|
||||
const GalleryBoard = ({ board, isSelected }: GalleryBoardProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
|
||||
const autoAssignBoardOnClick = useAppSelector(selectAutoAssignBoardOnClick);
|
||||
const selectedBoardId = useAppSelector(selectSelectedBoardId);
|
||||
@@ -111,12 +114,14 @@ const GalleryBoard = ({ board, isSelected }: GalleryBoardProps) => {
|
||||
export default memo(GalleryBoard);
|
||||
|
||||
const CoverImage = ({ board }: { board: BoardDTO }) => {
|
||||
const crossOrigin = useStore($crossOrigin);
|
||||
const { currentData: coverImage } = useGetImageDTOQuery(board.cover_image_name ?? skipToken);
|
||||
|
||||
if (coverImage) {
|
||||
return (
|
||||
<Image
|
||||
src={coverImage.thumbnail_url}
|
||||
crossOrigin={crossOrigin}
|
||||
draggable={false}
|
||||
objectFit="cover"
|
||||
w={10}
|
||||
|
||||
@@ -2,7 +2,9 @@ import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { draggable, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import type { FlexProps } from '@invoke-ai/ui-library';
|
||||
import { Flex, Icon, Image } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { $crossOrigin } from 'app/store/nanostores/authToken';
|
||||
import type { AppDispatch, AppGetState } from 'app/store/store';
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { uniq } from 'es-toolkit';
|
||||
@@ -87,6 +89,8 @@ const buildOnClick =
|
||||
|
||||
export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
const store = useAppStore();
|
||||
const crossOrigin = useStore($crossOrigin);
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragPreviewState, setDragPreviewState] = useState<
|
||||
DndDragPreviewSingleImageState | DndDragPreviewMultipleImageState | null
|
||||
@@ -210,6 +214,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
<Image
|
||||
pointerEvents="none"
|
||||
src={imageDTO.thumbnail_url}
|
||||
crossOrigin={crossOrigin}
|
||||
w={imageDTO.width}
|
||||
fallback={<GalleryImagePlaceholder />}
|
||||
objectFit="contain"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { draggable, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { Flex, Image } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { $crossOrigin } from 'app/store/nanostores/authToken';
|
||||
import type { AppDispatch, AppGetState } from 'app/store/store';
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { uniq } from 'es-toolkit';
|
||||
@@ -83,6 +85,8 @@ const buildOnClick =
|
||||
|
||||
export const GalleryVideo = memo(({ videoDTO }: Props) => {
|
||||
const store = useAppStore();
|
||||
const crossOrigin = useStore($crossOrigin);
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragPreviewState, setDragPreviewState] = useState<
|
||||
DndDragPreviewSingleVideoState | DndDragPreviewMultipleVideoState | null
|
||||
@@ -200,6 +204,7 @@ export const GalleryVideo = memo(({ videoDTO }: Props) => {
|
||||
<Image
|
||||
pointerEvents="none"
|
||||
src={videoDTO.thumbnail_url}
|
||||
crossOrigin={crossOrigin}
|
||||
w={videoDTO.width}
|
||||
fallback={<GalleryVideoPlaceholder />}
|
||||
objectFit="contain"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Box, Flex, Image } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $crossOrigin } from 'app/store/nanostores/authToken';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useBoolean } from 'common/hooks/useBoolean';
|
||||
import { preventDefault } from 'common/util/stopPropagation';
|
||||
@@ -12,6 +14,8 @@ import type { ComparisonProps } from './common';
|
||||
import { fitDimsToContainer, getSecondImageDims } from './common';
|
||||
|
||||
export const ImageComparisonHover = memo(({ firstImage, secondImage, rect }: ComparisonProps) => {
|
||||
const crossOrigin = useStore($crossOrigin);
|
||||
|
||||
const comparisonFit = useAppSelector(selectComparisonFit);
|
||||
const imageContainerRef = useRef<HTMLDivElement>(null);
|
||||
const mouseOver = useBoolean(false);
|
||||
@@ -53,6 +57,7 @@ export const ImageComparisonHover = memo(({ firstImage, secondImage, rect }: Com
|
||||
id="image-comparison-hover-first-image"
|
||||
src={firstImage.image_url}
|
||||
fallbackSrc={firstImage.thumbnail_url}
|
||||
crossOrigin={crossOrigin}
|
||||
w={fittedDims.width}
|
||||
h={fittedDims.height}
|
||||
maxW="full"
|
||||
@@ -89,6 +94,7 @@ export const ImageComparisonHover = memo(({ firstImage, secondImage, rect }: Com
|
||||
id="image-comparison-hover-second-image"
|
||||
src={secondImage.image_url}
|
||||
fallbackSrc={secondImage.thumbnail_url}
|
||||
crossOrigin={crossOrigin}
|
||||
w={compareImageDims.width}
|
||||
h={compareImageDims.height}
|
||||
maxW={fittedDims.width}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Flex, Image } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $crossOrigin } from 'app/store/nanostores/authToken';
|
||||
import type { ComparisonProps } from 'features/gallery/components/ImageViewer/common';
|
||||
import { ImageComparisonLabel } from 'features/gallery/components/ImageViewer/ImageComparisonLabel';
|
||||
import { VerticalResizeHandle } from 'features/ui/components/tabs/ResizeHandle';
|
||||
@@ -41,6 +43,8 @@ export const ImageComparisonSideBySide = memo(({ firstImage, secondImage }: Comp
|
||||
ImageComparisonSideBySide.displayName = 'ImageComparisonSideBySide';
|
||||
|
||||
const SideBySideImage = memo(({ imageDTO, type }: { imageDTO: ImageDTO; type: 'first' | 'second' }) => {
|
||||
const crossOrigin = useStore($crossOrigin);
|
||||
|
||||
return (
|
||||
<Flex position="relative" w="full" h="full" alignItems="center" justifyContent="center">
|
||||
<Flex position="absolute" maxW="full" maxH="full" aspectRatio={imageDTO.width / imageDTO.height}>
|
||||
@@ -52,6 +56,7 @@ const SideBySideImage = memo(({ imageDTO, type }: { imageDTO: ImageDTO; type: 'f
|
||||
maxH="full"
|
||||
src={imageDTO.image_url}
|
||||
fallbackSrc={imageDTO.thumbnail_url}
|
||||
crossOrigin={crossOrigin}
|
||||
objectFit="contain"
|
||||
borderRadius="base"
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Box, Flex, Icon, Image } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $crossOrigin } from 'app/store/nanostores/authToken';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { preventDefault } from 'common/util/stopPropagation';
|
||||
import { TRANSPARENCY_CHECKERBOARD_PATTERN_DARK_DATAURL } from 'features/controlLayers/konva/patterns/transparency-checkerboard-pattern';
|
||||
@@ -21,6 +23,7 @@ const HANDLE_LEFT_INITIAL_PX = `calc(${INITIAL_POS} - ${HANDLE_HITBOX / 2}px)`;
|
||||
|
||||
export const ImageComparisonSlider = memo(({ firstImage, secondImage, rect }: ComparisonProps) => {
|
||||
const comparisonFit = useAppSelector(selectComparisonFit);
|
||||
const crossOrigin = useStore($crossOrigin);
|
||||
|
||||
// How far the handle is from the left - this will be a CSS calculation that takes into account the handle width
|
||||
const [left, setLeft] = useState(HANDLE_LEFT_INITIAL_PX);
|
||||
@@ -132,6 +135,7 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage, rect }: Co
|
||||
id="image-comparison-second-image"
|
||||
src={secondImage.image_url}
|
||||
fallbackSrc={secondImage.thumbnail_url}
|
||||
crossOrigin={crossOrigin}
|
||||
w={compareImageDims.width}
|
||||
h={compareImageDims.height}
|
||||
maxW={fittedDims.width}
|
||||
@@ -154,6 +158,7 @@ export const ImageComparisonSlider = memo(({ firstImage, secondImage, rect }: Co
|
||||
id="image-comparison-first-image"
|
||||
src={firstImage.image_url}
|
||||
fallbackSrc={firstImage.thumbnail_url}
|
||||
crossOrigin={crossOrigin}
|
||||
w={fittedDims.width}
|
||||
h={fittedDims.height}
|
||||
objectFit="cover"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Flex, Icon, Image } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $crossOrigin } from 'app/store/nanostores/authToken';
|
||||
import { typedMemo } from 'common/util/typedMemo';
|
||||
import { PiImage } from 'react-icons/pi';
|
||||
|
||||
@@ -10,6 +12,8 @@ export const MODEL_IMAGE_THUMBNAIL_SIZE = '40px';
|
||||
const FALLBACK_ICON_SIZE = '24px';
|
||||
|
||||
const ModelImage = ({ image_url }: Props) => {
|
||||
const crossOrigin = useStore($crossOrigin);
|
||||
|
||||
if (!image_url) {
|
||||
return (
|
||||
<Flex
|
||||
@@ -27,6 +31,7 @@ const ModelImage = ({ image_url }: Props) => {
|
||||
return (
|
||||
<Image
|
||||
src={image_url}
|
||||
crossOrigin={crossOrigin}
|
||||
objectFit="cover"
|
||||
objectPosition="50% 50%"
|
||||
height={MODEL_IMAGE_THUMBNAIL_SIZE}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Box, IconButton, Image } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $crossOrigin } from 'app/store/nanostores/authToken';
|
||||
import { dropzoneAccept } from 'common/hooks/useImageUploadButton';
|
||||
import { typedMemo } from 'common/util/typedMemo';
|
||||
import { toast } from 'features/toast/toast';
|
||||
@@ -14,6 +16,8 @@ type Props = {
|
||||
};
|
||||
|
||||
const ModelImageUpload = ({ model_key, model_image }: Props) => {
|
||||
const crossOrigin = useStore($crossOrigin);
|
||||
|
||||
const [image, setImage] = useState<string | null>(model_image || null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -84,6 +88,7 @@ const ModelImageUpload = ({ model_key, model_image }: Props) => {
|
||||
<Box position="relative" flexShrink={0}>
|
||||
<Image
|
||||
src={image}
|
||||
crossOrigin={crossOrigin}
|
||||
objectFit="cover"
|
||||
objectPosition="50% 50%"
|
||||
height={108}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { FormControlProps } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex, FormControl, FormControlGroup, FormLabel, Image, Input, Textarea } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { $crossOrigin } from 'app/store/nanostores/authToken';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import {
|
||||
@@ -148,6 +150,7 @@ const formControlProps: FormControlProps = {
|
||||
|
||||
const Thumbnail = ({ id }: { id?: string | null }) => {
|
||||
const { t } = useTranslation();
|
||||
const crossOrigin = useStore($crossOrigin);
|
||||
|
||||
const { data } = useGetWorkflowQuery(id ?? skipToken);
|
||||
|
||||
@@ -163,6 +166,7 @@ const Thumbnail = ({ id }: { id?: string | null }) => {
|
||||
<Box position="relative" flexShrink={0}>
|
||||
<Image
|
||||
src={data.thumbnail_url}
|
||||
crossOrigin={crossOrigin}
|
||||
objectFit="cover"
|
||||
objectPosition="50% 50%"
|
||||
w={100}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Badge, Flex, Icon, Image, Spacer, Text } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $crossOrigin } from 'app/store/nanostores/authToken';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { LockedWorkflowIcon } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryListItemActions/LockedWorkflowIcon';
|
||||
import { ShareWorkflowButton } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryListItemActions/ShareWorkflow';
|
||||
@@ -36,6 +38,7 @@ export const WorkflowListItem = memo(({ workflow }: { workflow: WorkflowRecordLi
|
||||
const dispatch = useAppDispatch();
|
||||
const workflowId = useAppSelector(selectWorkflowId);
|
||||
const loadWorkflowWithDialog = useLoadWorkflowWithDialog();
|
||||
const crossOrigin = useStore($crossOrigin);
|
||||
|
||||
const isActive = useMemo(() => {
|
||||
return workflowId === workflow.workflow_id;
|
||||
@@ -66,6 +69,7 @@ export const WorkflowListItem = memo(({ workflow }: { workflow: WorkflowRecordLi
|
||||
<Flex p={2} pr={0}>
|
||||
<Image
|
||||
src={workflow.thumbnail_url ?? undefined}
|
||||
crossOrigin={crossOrigin}
|
||||
fallbackStrategy="beforeLoadOrError"
|
||||
fallback={workflow.category === 'default' ? <DefaultThumbnailFallback /> : <UserThumbnailFallback />}
|
||||
objectFit="cover"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Box, Flex, Image, Spinner, Text } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $crossOrigin } from 'app/store/nanostores/authToken';
|
||||
import { PromptExpansionResultOverlay } from 'features/prompt/PromptExpansion/PromptExpansionResultOverlay';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -10,6 +11,7 @@ import { promptExpansionApi } from './state';
|
||||
export const PromptExpansionOverlay = memo(() => {
|
||||
const { isSuccess, isPending, result, imageDTO } = useStore(promptExpansionApi.$state);
|
||||
const { t } = useTranslation();
|
||||
const crossOrigin = useStore($crossOrigin);
|
||||
|
||||
// Show result overlay when completed
|
||||
if (isSuccess) {
|
||||
@@ -48,7 +50,14 @@ export const PromptExpansionOverlay = memo(() => {
|
||||
borderRadius="base"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Image src={imageDTO.thumbnail_url} objectFit="contain" w="full" h="full" borderRadius="base" />
|
||||
<Image
|
||||
src={imageDTO.thumbnail_url}
|
||||
crossOrigin={crossOrigin}
|
||||
objectFit="contain"
|
||||
w="full"
|
||||
h="full"
|
||||
borderRadius="base"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Flex, Icon, Image, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $crossOrigin } from 'app/store/nanostores/authToken';
|
||||
import { typedMemo } from 'common/util/typedMemo';
|
||||
import { PiImage } from 'react-icons/pi';
|
||||
|
||||
@@ -6,6 +8,8 @@ const IMAGE_THUMBNAIL_SIZE = '40px';
|
||||
const FALLBACK_ICON_SIZE = '24px';
|
||||
|
||||
const StylePresetImage = ({ presetImageUrl, imageWidth }: { presetImageUrl: string | null; imageWidth?: number }) => {
|
||||
const crossOrigin = useStore($crossOrigin);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
closeOnScroll
|
||||
@@ -14,6 +18,7 @@ const StylePresetImage = ({ presetImageUrl, imageWidth }: { presetImageUrl: stri
|
||||
presetImageUrl && (
|
||||
<Image
|
||||
src={presetImageUrl}
|
||||
crossOrigin={crossOrigin}
|
||||
draggable={false}
|
||||
objectFit="cover"
|
||||
maxW={150}
|
||||
@@ -27,6 +32,7 @@ const StylePresetImage = ({ presetImageUrl, imageWidth }: { presetImageUrl: stri
|
||||
>
|
||||
<Image
|
||||
src={presetImageUrl || ''}
|
||||
crossOrigin={crossOrigin}
|
||||
fallbackStrategy="beforeLoadOrError"
|
||||
fallback={
|
||||
<Flex
|
||||
|
||||
Reference in New Issue
Block a user