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:
schroda
2024-01-15 00:32:39 +01:00
committed by GitHub
parent 9962e0713d
commit 199b62341f
6 changed files with 70 additions and 48 deletions

View File

@@ -113,7 +113,7 @@ export const MangaCard = (props: IProps) => {
handleSelection?.(id, !selected);
}}
to={mangaLinkTo}
style={gridLayout === GridLayout.Comfortable ? { textDecoration: 'none' } : {}}
style={{ textDecoration: 'none' }}
>
<Box
sx={{

View File

@@ -82,9 +82,7 @@ export const Page = forwardRef((props: IProps, ref: any) => {
...imgStyle,
height: '100vh',
width: '70vw',
padding: '50px calc(50% - 20px)',
backgroundColor: '#525252',
marginBottom: 10,
}}
imgStyle={imgStyle}
/>

View File

@@ -6,7 +6,7 @@
* 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 { IReaderProps } from '@/typings';
import { Page } from '@/components/reader/Page';
@@ -134,11 +134,9 @@ export function DoublePagedPager(props: IReaderProps) {
useEffect(() => {
document.addEventListener('keydown', keyboardControl);
selfRef.current?.addEventListener('click', clickControl);
return () => {
document.removeEventListener('keydown', keyboardControl);
selfRef.current?.removeEventListener('click', clickControl);
};
}, [selfRef, curPage, settings.readerType, prevChapter, nextChapter, pagesLoadState, pagesToSpreadState]);
@@ -157,7 +155,7 @@ export function DoublePagedPager(props: IReaderProps) {
}, [settings.offsetFirstPage]);
return (
<Box ref={selfRef}>
<Box ref={selfRef} onClick={clickControl}>
<Box id="preload" sx={{ display: 'none' }}>
{pages.map((page) => (
<img

View File

@@ -6,7 +6,7 @@
* 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 { IReaderProps } from '@/typings';
import { Page } from '@/components/reader/Page';
@@ -87,7 +87,7 @@ export function HorizontalPager(props: IReaderProps) {
selfRef.current?.removeEventListener('mousemove', dragScreen);
}
function clickControl(e: MouseEvent) {
function clickControl(e: ReactMouseEvent) {
if (e.clientX >= window.innerWidth * 0.85) {
goRight();
} else if (e.clientX <= window.innerWidth * 0.15) {
@@ -129,11 +129,9 @@ export function HorizontalPager(props: IReaderProps) {
if (settings.loadNextOnEnding) {
document.addEventListener('scroll', handleLoadNextonEnding);
}
selfRef.current?.addEventListener('mousedown', clickControl);
return () => {
document.removeEventListener('scroll', handleLoadNextonEnding);
selfRef.current?.removeEventListener('mousedown', clickControl);
};
}, [selfRef, curPage, prevChapter, nextChapter]);
@@ -174,6 +172,7 @@ export function HorizontalPager(props: IReaderProps) {
overflowX: 'visible',
userSelect: 'none',
}}
onClick={clickControl}
>
{pages.map((page) => (
<Page

View File

@@ -6,7 +6,7 @@
* 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 { IReaderProps } from '@/typings';
import { Page } from '@/components/reader/Page';
@@ -80,11 +80,9 @@ export function PagedPager(props: IReaderProps) {
useEffect(() => {
document.addEventListener('keydown', keyboardControl);
selfRef.current?.addEventListener('click', clickControl);
return () => {
document.removeEventListener('keydown', keyboardControl);
selfRef.current?.removeEventListener('click', clickControl);
};
}, [selfRef, curPage, settings.readerType, prevChapter, nextChapter]);
@@ -107,6 +105,7 @@ export function PagedPager(props: IReaderProps) {
width: '100%',
height: '100vh',
}}
onClick={clickControl}
>
<Page key={curPage} index={curPage} onImageLoad={() => {}} src={pages[curPage].src} settings={settings} />
</Box>

View File

@@ -6,10 +6,13 @@
* 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 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 {
src: string;
@@ -25,41 +28,66 @@ interface IProps {
export function SpinnerImage(props: IProps) {
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(() => {
const img = new Image();
img.src = src;
// only activate the loading state in case the image has not been cached yet.
// 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 = () => {
setImagsrc(src);
onImageLoad?.();
};
img.onerror = () => {
// Setting to an actual image so CSS styling works consistently
setImagsrc('/notFound.svg');
};
return () => {
img.onload = null;
img.onerror = null;
};
}, [src]);
if (imageSrc.length === 0) {
return (
<Box sx={spinnerStyle}>
<CircularProgress thickness={5} />
</Box>
);
}
if (imageSrc === 'Not Found') {
return <Box sx={spinnerStyle} />;
}
return <img style={imgStyle} ref={imgRef} src={imageSrc} alt={alt} draggable={false} />;
return (
<>
{(isLoading || hasError) && (
<Box sx={spinnerStyle}>
<Stack height="100%" alignItems="center" justifyContent="center">
{isLoading && <CircularProgress thickness={5} />}
{hasError && (
<>
<BrokenImageIcon />
<Button
startIcon={<RefreshIcon />}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
setIsLoading(true);
setHasError(false);
setImgLoadRetryKey((prevState) => (prevState + 1) % 100);
}}
size="large"
>
Retry
</Button>
</>
)}
</Stack>
</Box>
)}
<img
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 = {