Files
suwayomi-material-you-webui/src/components/util/SpinnerImage.tsx

137 lines
4.8 KiB
TypeScript
Raw Normal View History

2021-05-28 19:37:26 +04:30
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
2021-05-28 19:37:26 +04:30
import { useState, CSSProperties, useEffect, forwardRef, ForwardedRef } from 'react';
2021-09-09 17:51:22 +04:30
import CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/material/Box';
import { Theme, SxProps, Stack, Button } from '@mui/material';
import BrokenImageIcon from '@mui/icons-material/BrokenImage';
import RefreshIcon from '@mui/icons-material/Refresh';
import { useTranslation } from 'react-i18next';
2024-03-01 01:17:23 +01:00
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
2021-05-28 19:37:26 +04:30
interface IProps {
src: string;
alt: string;
2021-05-28 19:37:26 +04:30
spinnerStyle?: SxProps<Theme> & { small?: boolean };
imgStyle?: CSSProperties;
2021-05-28 19:37:26 +04:30
onImageLoad?: () => void;
2021-05-28 19:37:26 +04:30
}
export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTMLImageElement | null>) => {
const { src, alt, onImageLoad, spinnerStyle: { small, ...spinnerStyle } = {}, imgStyle } = props;
2021-05-28 19:37:26 +04:30
const { t } = useTranslation();
2024-03-01 01:17:23 +01:00
const [imageSourceUrl, setImageSourceUrl] = useState('');
const [imgLoadRetryKey, setImgLoadRetryKey] = useState(0);
const [isLoading, setIsLoading] = useState<boolean | undefined>(undefined);
const [hasError, setHasError] = useState(false);
2021-05-28 19:37:26 +04:30
const updateImageState = (loading: boolean, error: boolean = false) => {
setIsLoading(loading);
setHasError(error);
if (!loading && !error) {
onImageLoad?.();
}
};
2021-05-28 19:37:26 +04:30
useEffect(() => {
let tmpImageSourceUrl: string;
2024-03-01 01:17:23 +01:00
const imageRequest = requestManager.requestImage(src);
let cacheTimeout: NodeJS.Timeout;
2024-03-01 01:17:23 +01:00
const fetchImage = async () => {
try {
const updateImage = async () => {
const image = await imageRequest.response;
updateImageState(false);
setImageSourceUrl(image);
tmpImageSourceUrl = image;
2024-03-01 01:17:23 +01:00
};
const checkCache = await Promise.race([
imageRequest.response,
new Promise((resolve) => {
cacheTimeout = setTimeout(resolve, 50);
}),
]);
2024-03-01 01:17:23 +01:00
const isImageCached = !!checkCache;
if (isImageCached) {
await updateImage();
return;
}
updateImageState(true);
await updateImage();
} catch (e) {
const wasAborted =
e instanceof Error && (e.name === 'AbortError' || e.message === 'Component was unmounted');
2024-03-01 01:17:23 +01:00
updateImageState(false, !wasAborted);
}
};
fetchImage().catch(defaultPromiseErrorHandler);
return () => {
if (tmpImageSourceUrl) {
URL.revokeObjectURL(tmpImageSourceUrl);
}
clearTimeout(cacheTimeout);
2024-03-01 01:17:23 +01:00
imageRequest.abortRequest(new Error('Component was unmounted'));
};
}, [imgLoadRetryKey]);
return (
<>
{(isLoading || hasError) && (
<Box sx={spinnerStyle}>
<Stack height="100%" alignItems="center" justifyContent="center">
{isLoading && <CircularProgress thickness={5} />}
2024-03-01 01:17:23 +01:00
{hasError && isLoading === false && (
<>
<BrokenImageIcon />
<Button
startIcon={!small && <RefreshIcon />}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
setImgLoadRetryKey((prevState) => (prevState + 1) % 100);
}}
size={small ? 'small' : 'large'}
>
{small ? <RefreshIcon /> : t('global.button.retry')}
</Button>
</>
)}
</Stack>
</Box>
)}
2024-03-01 01:17:23 +01:00
<img
key={`${src}_${imgLoadRetryKey}`}
2024-03-01 01:17:23 +01:00
style={{
...imgStyle,
display: !imageSourceUrl || isLoading || hasError ? 'none' : imgStyle?.display,
}}
ref={imgRef}
2024-03-01 01:17:23 +01:00
src={imageSourceUrl}
alt={alt}
draggable={false}
/>
</>
);
});