Feature/add retry button for failed image requests (#550)
* Improve img loading placeholder Prevent showing a loading placeholder in case the image is already cached. * Add failed img load retry button
This commit is contained in:
@@ -113,7 +113,7 @@ export const MangaCard = (props: IProps) => {
|
|||||||
handleSelection?.(id, !selected);
|
handleSelection?.(id, !selected);
|
||||||
}}
|
}}
|
||||||
to={mangaLinkTo}
|
to={mangaLinkTo}
|
||||||
style={gridLayout === GridLayout.Comfortable ? { textDecoration: 'none' } : {}}
|
style={{ textDecoration: 'none' }}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -82,9 +82,7 @@ export const Page = forwardRef((props: IProps, ref: any) => {
|
|||||||
...imgStyle,
|
...imgStyle,
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
width: '70vw',
|
width: '70vw',
|
||||||
padding: '50px calc(50% - 20px)',
|
|
||||||
backgroundColor: '#525252',
|
backgroundColor: '#525252',
|
||||||
marginBottom: 10,
|
|
||||||
}}
|
}}
|
||||||
imgStyle={imgStyle}
|
imgStyle={imgStyle}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { MouseEvent, useEffect, useRef, useState } from 'react';
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import { IReaderProps } from '@/typings';
|
import { IReaderProps } from '@/typings';
|
||||||
import { Page } from '@/components/reader/Page';
|
import { Page } from '@/components/reader/Page';
|
||||||
@@ -134,11 +134,9 @@ export function DoublePagedPager(props: IReaderProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener('keydown', keyboardControl);
|
document.addEventListener('keydown', keyboardControl);
|
||||||
selfRef.current?.addEventListener('click', clickControl);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', keyboardControl);
|
document.removeEventListener('keydown', keyboardControl);
|
||||||
selfRef.current?.removeEventListener('click', clickControl);
|
|
||||||
};
|
};
|
||||||
}, [selfRef, curPage, settings.readerType, prevChapter, nextChapter, pagesLoadState, pagesToSpreadState]);
|
}, [selfRef, curPage, settings.readerType, prevChapter, nextChapter, pagesLoadState, pagesToSpreadState]);
|
||||||
|
|
||||||
@@ -157,7 +155,7 @@ export function DoublePagedPager(props: IReaderProps) {
|
|||||||
}, [settings.offsetFirstPage]);
|
}, [settings.offsetFirstPage]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box ref={selfRef}>
|
<Box ref={selfRef} onClick={clickControl}>
|
||||||
<Box id="preload" sx={{ display: 'none' }}>
|
<Box id="preload" sx={{ display: 'none' }}>
|
||||||
{pages.map((page) => (
|
{pages.map((page) => (
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
import { MouseEvent as ReactMouseEvent, useEffect, useRef } from 'react';
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import { IReaderProps } from '@/typings';
|
import { IReaderProps } from '@/typings';
|
||||||
import { Page } from '@/components/reader/Page';
|
import { Page } from '@/components/reader/Page';
|
||||||
@@ -87,7 +87,7 @@ export function HorizontalPager(props: IReaderProps) {
|
|||||||
selfRef.current?.removeEventListener('mousemove', dragScreen);
|
selfRef.current?.removeEventListener('mousemove', dragScreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clickControl(e: MouseEvent) {
|
function clickControl(e: ReactMouseEvent) {
|
||||||
if (e.clientX >= window.innerWidth * 0.85) {
|
if (e.clientX >= window.innerWidth * 0.85) {
|
||||||
goRight();
|
goRight();
|
||||||
} else if (e.clientX <= window.innerWidth * 0.15) {
|
} else if (e.clientX <= window.innerWidth * 0.15) {
|
||||||
@@ -129,11 +129,9 @@ export function HorizontalPager(props: IReaderProps) {
|
|||||||
if (settings.loadNextOnEnding) {
|
if (settings.loadNextOnEnding) {
|
||||||
document.addEventListener('scroll', handleLoadNextonEnding);
|
document.addEventListener('scroll', handleLoadNextonEnding);
|
||||||
}
|
}
|
||||||
selfRef.current?.addEventListener('mousedown', clickControl);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('scroll', handleLoadNextonEnding);
|
document.removeEventListener('scroll', handleLoadNextonEnding);
|
||||||
selfRef.current?.removeEventListener('mousedown', clickControl);
|
|
||||||
};
|
};
|
||||||
}, [selfRef, curPage, prevChapter, nextChapter]);
|
}, [selfRef, curPage, prevChapter, nextChapter]);
|
||||||
|
|
||||||
@@ -174,6 +172,7 @@ export function HorizontalPager(props: IReaderProps) {
|
|||||||
overflowX: 'visible',
|
overflowX: 'visible',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
}}
|
}}
|
||||||
|
onClick={clickControl}
|
||||||
>
|
>
|
||||||
{pages.map((page) => (
|
{pages.map((page) => (
|
||||||
<Page
|
<Page
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
import { MouseEvent, useEffect, useRef } from 'react';
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import { IReaderProps } from '@/typings';
|
import { IReaderProps } from '@/typings';
|
||||||
import { Page } from '@/components/reader/Page';
|
import { Page } from '@/components/reader/Page';
|
||||||
@@ -80,11 +80,9 @@ export function PagedPager(props: IReaderProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener('keydown', keyboardControl);
|
document.addEventListener('keydown', keyboardControl);
|
||||||
selfRef.current?.addEventListener('click', clickControl);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', keyboardControl);
|
document.removeEventListener('keydown', keyboardControl);
|
||||||
selfRef.current?.removeEventListener('click', clickControl);
|
|
||||||
};
|
};
|
||||||
}, [selfRef, curPage, settings.readerType, prevChapter, nextChapter]);
|
}, [selfRef, curPage, settings.readerType, prevChapter, nextChapter]);
|
||||||
|
|
||||||
@@ -107,6 +105,7 @@ export function PagedPager(props: IReaderProps) {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
}}
|
}}
|
||||||
|
onClick={clickControl}
|
||||||
>
|
>
|
||||||
<Page key={curPage} index={curPage} onImageLoad={() => {}} src={pages[curPage].src} settings={settings} />
|
<Page key={curPage} index={curPage} onImageLoad={() => {}} src={pages[curPage].src} settings={settings} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -6,10 +6,13 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState, CSSProperties } from 'react';
|
import React, { useState, CSSProperties, useEffect } from 'react';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { Theme, SxProps } from '@mui/material';
|
import { Theme, SxProps, Stack, Button } from '@mui/material';
|
||||||
|
|
||||||
|
import BrokenImageIcon from '@mui/icons-material/BrokenImage';
|
||||||
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
src: string;
|
src: string;
|
||||||
@@ -25,41 +28,66 @@ interface IProps {
|
|||||||
|
|
||||||
export function SpinnerImage(props: IProps) {
|
export function SpinnerImage(props: IProps) {
|
||||||
const { src, alt, onImageLoad, imgRef, spinnerStyle, imgStyle } = props;
|
const { src, alt, onImageLoad, imgRef, spinnerStyle, imgStyle } = props;
|
||||||
const [imageSrc, setImagsrc] = useState<string>('');
|
|
||||||
|
const [imgLoadRetryKey, setImgLoadRetryKey] = useState(0);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean | undefined>(undefined);
|
||||||
|
const [hasError, setHasError] = useState(false);
|
||||||
|
|
||||||
|
const updateImageState = (loading: boolean, error: boolean = false) => {
|
||||||
|
setIsLoading(loading);
|
||||||
|
setHasError(error);
|
||||||
|
|
||||||
|
if (!loading && !error) {
|
||||||
|
onImageLoad?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const img = new Image();
|
// only activate the loading state in case the image has not been cached yet.
|
||||||
img.src = src;
|
// otherwise, the loading placeholder will always be visible before the actual image is shown, which looks like flickering
|
||||||
|
const timeout = setTimeout(() => setIsLoading((prevState) => (prevState === undefined ? true : prevState)), 1);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, []);
|
||||||
|
|
||||||
img.onload = () => {
|
return (
|
||||||
setImagsrc(src);
|
<>
|
||||||
onImageLoad?.();
|
{(isLoading || hasError) && (
|
||||||
};
|
<Box sx={spinnerStyle}>
|
||||||
|
<Stack height="100%" alignItems="center" justifyContent="center">
|
||||||
img.onerror = () => {
|
{isLoading && <CircularProgress thickness={5} />}
|
||||||
// Setting to an actual image so CSS styling works consistently
|
{hasError && (
|
||||||
setImagsrc('/notFound.svg');
|
<>
|
||||||
};
|
<BrokenImageIcon />
|
||||||
|
<Button
|
||||||
return () => {
|
startIcon={<RefreshIcon />}
|
||||||
img.onload = null;
|
onClick={(e) => {
|
||||||
img.onerror = null;
|
e.stopPropagation();
|
||||||
};
|
e.preventDefault();
|
||||||
}, [src]);
|
setIsLoading(true);
|
||||||
|
setHasError(false);
|
||||||
if (imageSrc.length === 0) {
|
setImgLoadRetryKey((prevState) => (prevState + 1) % 100);
|
||||||
return (
|
}}
|
||||||
<Box sx={spinnerStyle}>
|
size="large"
|
||||||
<CircularProgress thickness={5} />
|
>
|
||||||
</Box>
|
Retry
|
||||||
);
|
</Button>
|
||||||
}
|
</>
|
||||||
|
)}
|
||||||
if (imageSrc === 'Not Found') {
|
</Stack>
|
||||||
return <Box sx={spinnerStyle} />;
|
</Box>
|
||||||
}
|
)}
|
||||||
|
<img
|
||||||
return <img style={imgStyle} ref={imgRef} src={imageSrc} alt={alt} draggable={false} />;
|
key={`${src}_${imgLoadRetryKey}`}
|
||||||
|
style={{ ...imgStyle, display: isLoading || hasError ? 'none' : undefined }}
|
||||||
|
ref={imgRef}
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
onLoad={() => updateImageState(false)}
|
||||||
|
onError={() => updateImageState(false, true)}
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SpinnerImage.defaultProps = {
|
SpinnerImage.defaultProps = {
|
||||||
|
|||||||
Reference in New Issue
Block a user