diff --git a/frontend/src/Movie/Movie.ts b/frontend/src/Movie/Movie.ts index cf5d7de63..2d87bd53e 100644 --- a/frontend/src/Movie/Movie.ts +++ b/frontend/src/Movie/Movie.ts @@ -9,8 +9,10 @@ export type MovieStatus = | 'released' | 'deleted'; +export type CoverType = 'poster' | 'fanart'; + export interface Image { - coverType: string; + coverType: CoverType; url: string; remoteUrl: string; } diff --git a/frontend/src/Movie/MovieImage.js b/frontend/src/Movie/MovieImage.js deleted file mode 100644 index 667027c0b..000000000 --- a/frontend/src/Movie/MovieImage.js +++ /dev/null @@ -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 ( - - ); - } - - if (lazy) { - return ( - - } - > - - - ); - } - - return ( - - ); - } -} - -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; diff --git a/frontend/src/Movie/MovieImage.tsx b/frontend/src/Movie/MovieImage.tsx new file mode 100644 index 000000000..9c3261de8 --- /dev/null +++ b/frontend/src/Movie/MovieImage.tsx @@ -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(null); + const [hasError, setHasError] = useState(false); + const [isLoaded, setIsLoaded] = useState(false); + const image = useRef(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 ; + } + + if (lazy) { + return ( + + } + > + + + ); + } + + return ( + + ); +} + +export default MovieImage; diff --git a/frontend/src/Movie/MoviePoster.js b/frontend/src/Movie/MoviePoster.js deleted file mode 100644 index b0f89a828..000000000 --- a/frontend/src/Movie/MoviePoster.js +++ /dev/null @@ -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 ( - - ); -} - -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; diff --git a/frontend/src/Movie/MoviePoster.tsx b/frontend/src/Movie/MoviePoster.tsx new file mode 100644 index 000000000..10a9e3605 --- /dev/null +++ b/frontend/src/Movie/MoviePoster.tsx @@ -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 { + size?: 250 | 500; +} + +function MoviePoster({ size = 250, ...otherProps }: MoviePosterProps) { + return ( + + ); +} + +export default MoviePoster;