1
0
mirror of https://github.com/Radarr/Radarr.git synced 2024-11-20 01:42:35 +01:00

Convert movie images to TypeScript

(cherry picked from commit ee99c3895de497bb1c99193ba16c56393b8ff593)

Closes #10402
This commit is contained in:
Mark McDowall 2024-09-02 16:05:19 -07:00 committed by Bogdan
parent 51cd7c70ba
commit 032e8aa920
5 changed files with 154 additions and 229 deletions

View File

@ -9,8 +9,10 @@ export type MovieStatus =
| 'released' | 'released'
| 'deleted'; | 'deleted';
export type CoverType = 'poster' | 'fanart';
export interface Image { export interface Image {
coverType: string; coverType: CoverType;
url: string; url: string;
remoteUrl: string; remoteUrl: string;
} }

View File

@ -1,198 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LazyLoad from 'react-lazyload';
function findImage(images, coverType) {
return images.find((image) => image.coverType === coverType);
}
function getUrl(image, coverType, size) {
const imageUrl = image?.url ?? image?.remoteUrl;
if (imageUrl) {
return imageUrl.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`);
}
}
class MovieImage extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const pixelRatio = Math.ceil(window.devicePixelRatio);
const {
images,
coverType,
size
} = props;
const image = findImage(images, coverType);
this.state = {
pixelRatio,
image,
url: getUrl(image, coverType, pixelRatio * size),
isLoaded: false,
hasError: false
};
}
componentDidMount() {
if (!this.state.url && this.props.onError) {
this.props.onError();
}
}
componentDidUpdate() {
const {
images,
coverType,
placeholder,
size,
onError
} = this.props;
const {
image,
pixelRatio
} = this.state;
const nextImage = findImage(images, coverType);
if (nextImage && (!image || nextImage.url !== image.url)) {
this.setState({
image: nextImage,
url: getUrl(nextImage, coverType, pixelRatio * size),
hasError: false
// Don't reset isLoaded, as we want to immediately try to
// show the new image, whether an image was shown previously
// or the placeholder was shown.
});
} else if (!nextImage && image) {
this.setState({
image: nextImage,
url: placeholder,
hasError: false
});
if (onError) {
onError();
}
}
}
//
// Listeners
onError = () => {
this.setState({
hasError: true
});
if (this.props.onError) {
this.props.onError();
}
};
onLoad = () => {
this.setState({
isLoaded: true,
hasError: false
});
if (this.props.onLoad) {
this.props.onLoad();
}
};
//
// Render
render() {
const {
className,
style,
placeholder,
size,
lazy,
overflow
} = this.props;
const {
url,
hasError,
isLoaded
} = this.state;
if (hasError || !url) {
return (
<img
className={className}
style={style}
src={placeholder}
/>
);
}
if (lazy) {
return (
<LazyLoad
height={size}
offset={100}
overflow={overflow}
placeholder={
<img
className={className}
style={style}
src={placeholder}
/>
}
>
<img
className={className}
style={style}
src={url}
onError={this.onError}
onLoad={this.onLoad}
rel="noreferrer"
/>
</LazyLoad>
);
}
return (
<img
className={className}
style={style}
src={isLoaded ? url : placeholder}
onError={this.onError}
onLoad={this.onLoad}
/>
);
}
}
MovieImage.propTypes = {
className: PropTypes.string,
style: PropTypes.object,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
coverType: PropTypes.string.isRequired,
placeholder: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
lazy: PropTypes.bool.isRequired,
overflow: PropTypes.bool.isRequired,
onError: PropTypes.func,
onLoad: PropTypes.func
};
MovieImage.defaultProps = {
size: 250,
lazy: true,
overflow: false
};
export default MovieImage;

View File

@ -0,0 +1,128 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import LazyLoad from 'react-lazyload';
import { CoverType, Image } from './Movie';
function findImage(images: Image[], coverType: CoverType) {
return images.find((image) => image.coverType === coverType);
}
function getUrl(image: Image, coverType: CoverType, size: number) {
const imageUrl = image?.url ?? image?.remoteUrl;
return imageUrl
? imageUrl.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`)
: null;
}
export interface MovieImageProps {
className?: string;
style?: object;
images: Image[];
coverType: CoverType;
placeholder: string;
size?: number;
lazy?: boolean;
overflow?: boolean;
onError?: () => void;
onLoad?: () => void;
}
const pixelRatio = Math.max(Math.round(window.devicePixelRatio), 1);
function MovieImage({
className,
style,
images,
coverType,
placeholder,
size = 250,
lazy = true,
overflow = false,
onError,
onLoad,
}: MovieImageProps) {
const [url, setUrl] = useState<string | null>(null);
const [hasError, setHasError] = useState(false);
const [isLoaded, setIsLoaded] = useState(false);
const image = useRef<Image | null>(null);
const handleLoad = useCallback(() => {
setHasError(false);
setIsLoaded(true);
onLoad?.();
}, [setHasError, setIsLoaded, onLoad]);
const handleError = useCallback(() => {
setHasError(true);
setIsLoaded(false);
onError?.();
}, [setHasError, setIsLoaded, onError]);
useEffect(() => {
const nextImage = findImage(images, coverType);
if (nextImage && (!image.current || nextImage.url !== image.current.url)) {
// Don't reset isLoaded, as we want to immediately try to
// show the new image, whether an image was shown previously
// or the placeholder was shown.
image.current = nextImage;
setUrl(getUrl(nextImage, coverType, pixelRatio * size));
setHasError(false);
} else if (!nextImage) {
if (image.current) {
image.current = null;
setUrl(placeholder);
setHasError(false);
onError?.();
}
}
}, [images, coverType, placeholder, size, onError]);
useEffect(() => {
if (!image.current) {
onError?.();
}
// This should only run once when the component mounts,
// so we don't need to include the other dependencies.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (hasError || !url) {
return <img className={className} style={style} src={placeholder} />;
}
if (lazy) {
return (
<LazyLoad
height={size}
offset={100}
overflow={overflow}
placeholder={
<img className={className} style={style} src={placeholder} />
}
>
<img
className={className}
style={style}
src={url}
rel="noreferrer"
onError={handleError}
onLoad={handleLoad}
/>
</LazyLoad>
);
}
return (
<img
className={className}
style={style}
src={isLoaded ? url : placeholder}
onError={handleError}
onLoad={handleLoad}
/>
);
}
export default MovieImage;

View File

@ -1,30 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import MovieImage from './MovieImage';
const posterPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAAD3CAMAAAC+Te+kAAAAZlBMVEUvLi8vLy8vLzAvMDAwLy8wLzAwMDAwMDEwMTExMDAxMDExMTExMTIxMjIyMjIyMjMyMzMzMjMzMzMzMzQzNDQ0NDQ0NDU0NTU1NTU1NTY1NjY2NTY2NjY2Njc2Nzc3Njc3Nzc3NziHChLWAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+MKCgEdHeShUbsAAALZSURBVHja7dxNcuwgDEZR1qAVmP1vMrNUJe91GfTzCSpXo575lAymjYWGXRIDKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKNA/AZ3fcTR0/owjofNDnAadnwPoPnS+xTXQeQZ0rkQ/dC4H0Gzo7ITO3bgGOnug/2PcAF3Mczt0fUj0QncG7znQBupw3PkWqh8qpkagpnyqjuArkkxaC02kRqGypCZANVYFdJZCdy9WTRVB5znQ6qTmjFFBWnOhdg20Lqnp0CpqAbRmAJRAK5JaA32zngTNvv910OSkVkJTs1oLtWugeTkNQZ/nkT2rotBHldUwNE6VQTVWGTQ6AHKggqGaBS23JkKf0hUgE1qa01Ro5fzPhoapR0HtCGg4q0poSCqFRgaAFhqxqqEr1EOgmdJaqHdaHQq1I6CunPZAHdY2aIJUBN2V9kE3H1Wd0BXrNVA7BLpgdUCtALo8pZqhdgd0Z6OyE7q1pdoH3dv7tS7o7iZ1E3R/N70Huuz795cQao65vvkqooT+vEgDdPcbj2s3zxTv9Qt/7cuhdgfUo2yAOplyqNuphfqZSqhFmEJo0HkcdPZCo0rRymRxpwSawHR+YtyBZihfvi+nQO0OqCmcYahGqYPGS4qCUJkzBpUpJdCkordyaFZxXi1UUpaZAJ2XQFOLh8ug2XXjVdD0+vYiqLIO3w1VH8EogtoxUPnpGxe04zyTA1p57i4T2nTmbnnnUuLMg1afYE2C1h+1zYEKjlknQLtPg9tb3YzU+dL054qOBb8cvcz3DlqBZhUmhdrnKo9j+pR0rkN5UHkznZHPtJIYN2TTCe1poTUyk9nWPO0bt8Ys7Ug34mlUMONtPUXMaEdXnXN1MnUzN2Z9q3Lr8XQN1DaLQJpXpiamZwltYdIUHShQoECBAgUKFChQoECBAgUKFChQoECBAgUKFChQoECBAgUKFCjQ+vgCff/mEp/vtiIAAAAASUVORK5CYII=';
function MoviePoster(props) {
return (
<MovieImage
{...props}
coverType="poster"
placeholder={posterPlaceholder}
/>
);
}
MoviePoster.propTypes = {
...MovieImage.propTypes,
coverType: PropTypes.string,
placeholder: PropTypes.string,
overflow: PropTypes.bool,
size: PropTypes.number.isRequired
};
MoviePoster.defaultProps = {
...MovieImage.defaultProps,
size: 250
};
export default MoviePoster;

View File

@ -0,0 +1,23 @@
import React from 'react';
import MovieImage, { MovieImageProps } from './MovieImage';
const posterPlaceholder =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAAD3CAMAAAC+Te+kAAAAZlBMVEUvLi8vLy8vLzAvMDAwLy8wLzAwMDAwMDEwMTExMDAxMDExMTExMTIxMjIyMjIyMjMyMzMzMjMzMzMzMzQzNDQ0NDQ0NDU0NTU1NTU1NTY1NjY2NTY2NjY2Njc2Nzc3Njc3Nzc3NziHChLWAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+MKCgEdHeShUbsAAALZSURBVHja7dxNcuwgDEZR1qAVmP1vMrNUJe91GfTzCSpXo575lAymjYWGXRIDKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKNA/AZ3fcTR0/owjofNDnAadnwPoPnS+xTXQeQZ0rkQ/dC4H0Gzo7ITO3bgGOnug/2PcAF3Mczt0fUj0QncG7znQBupw3PkWqh8qpkagpnyqjuArkkxaC02kRqGypCZANVYFdJZCdy9WTRVB5znQ6qTmjFFBWnOhdg20Lqnp0CpqAbRmAJRAK5JaA32zngTNvv910OSkVkJTs1oLtWugeTkNQZ/nkT2rotBHldUwNE6VQTVWGTQ6AHKggqGaBS23JkKf0hUgE1qa01Ro5fzPhoapR0HtCGg4q0poSCqFRgaAFhqxqqEr1EOgmdJaqHdaHQq1I6CunPZAHdY2aIJUBN2V9kE3H1Wd0BXrNVA7BLpgdUCtALo8pZqhdgd0Z6OyE7q1pdoH3dv7tS7o7iZ1E3R/N70Huuz795cQao65vvkqooT+vEgDdPcbj2s3zxTv9Qt/7cuhdgfUo2yAOplyqNuphfqZSqhFmEJo0HkcdPZCo0rRymRxpwSawHR+YtyBZihfvi+nQO0OqCmcYahGqYPGS4qCUJkzBpUpJdCkordyaFZxXi1UUpaZAJ2XQFOLh8ug2XXjVdD0+vYiqLIO3w1VH8EogtoxUPnpGxe04zyTA1p57i4T2nTmbnnnUuLMg1afYE2C1h+1zYEKjlknQLtPg9tb3YzU+dL054qOBb8cvcz3DlqBZhUmhdrnKo9j+pR0rkN5UHkznZHPtJIYN2TTCe1poTUyk9nWPO0bt8Ys7Ug34mlUMONtPUXMaEdXnXN1MnUzN2Z9q3Lr8XQN1DaLQJpXpiamZwltYdIUHShQoECBAgUKFChQoECBAgUKFChQoECBAgUKFChQoECBAgUKFCjQ+vgCff/mEp/vtiIAAAAASUVORK5CYII=';
interface MoviePosterProps
extends Omit<MovieImageProps, 'coverType' | 'placeholder'> {
size?: 250 | 500;
}
function MoviePoster({ size = 250, ...otherProps }: MoviePosterProps) {
return (
<MovieImage
{...otherProps}
size={size}
coverType="poster"
placeholder={posterPlaceholder}
/>
);
}
export default MoviePoster;