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); handleSelection?.(id, !selected);
}} }}
to={mangaLinkTo} to={mangaLinkTo}
style={gridLayout === GridLayout.Comfortable ? { textDecoration: 'none' } : {}} style={{ textDecoration: 'none' }}
> >
<Box <Box
sx={{ sx={{

View File

@@ -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}
/> />

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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 = {