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:
parent
51cd7c70ba
commit
032e8aa920
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
128
frontend/src/Movie/MovieImage.tsx
Normal file
128
frontend/src/Movie/MovieImage.tsx
Normal 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;
|
@ -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;
|
|
23
frontend/src/Movie/MoviePoster.tsx
Normal file
23
frontend/src/Movie/MoviePoster.tsx
Normal 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;
|
Loading…
Reference in New Issue
Block a user