feat: add option to cache images locally (#1213)
This commit is contained in:
@@ -2,6 +2,9 @@ module.exports = {
|
||||
env: {
|
||||
commitTag: process.env.COMMIT_TAG || 'local',
|
||||
},
|
||||
images: {
|
||||
domains: ['image.tmdb.org'],
|
||||
},
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface PublicSettingsResponse {
|
||||
region: string;
|
||||
originalLanguage: string;
|
||||
partialRequestsEnabled: boolean;
|
||||
cacheImages: boolean;
|
||||
}
|
||||
|
||||
export interface CacheItem {
|
||||
|
||||
@@ -66,6 +66,7 @@ export interface MainSettings {
|
||||
applicationTitle: string;
|
||||
applicationUrl: string;
|
||||
csrfProtection: boolean;
|
||||
cacheImages: boolean;
|
||||
defaultPermissions: number;
|
||||
hideAvailable: boolean;
|
||||
localLogin: boolean;
|
||||
@@ -88,6 +89,7 @@ interface FullPublicSettings extends PublicSettings {
|
||||
region: string;
|
||||
originalLanguage: string;
|
||||
partialRequestsEnabled: boolean;
|
||||
cacheImages: boolean;
|
||||
}
|
||||
|
||||
export interface NotificationAgentConfig {
|
||||
@@ -195,6 +197,7 @@ class Settings {
|
||||
applicationTitle: 'Overseerr',
|
||||
applicationUrl: '',
|
||||
csrfProtection: false,
|
||||
cacheImages: false,
|
||||
defaultPermissions: Permission.REQUEST,
|
||||
hideAvailable: false,
|
||||
localLogin: true,
|
||||
@@ -349,6 +352,7 @@ class Settings {
|
||||
region: this.data.main.region,
|
||||
originalLanguage: this.data.main.originalLanguage,
|
||||
partialRequestsEnabled: this.data.main.partialRequestsEnabled,
|
||||
cacheImages: this.data.main.cacheImages,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useUser, Permission } from '../../hooks/useUser';
|
||||
import useSettings from '../../hooks/useSettings';
|
||||
import Link from 'next/link';
|
||||
import { uniq } from 'lodash';
|
||||
import CachedImage from '../Common/CachedImage';
|
||||
|
||||
const messages = defineMessages({
|
||||
overviewunavailable: 'Overview unavailable.',
|
||||
@@ -203,9 +204,26 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
|
||||
className="media-page"
|
||||
style={{
|
||||
height: 493,
|
||||
backgroundImage: `linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%), url(//image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath})`,
|
||||
}}
|
||||
>
|
||||
{data.backdropPath && (
|
||||
<div className="media-page-bg-image">
|
||||
<CachedImage
|
||||
alt=""
|
||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
priority
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<PageTitle title={data.name} />
|
||||
<Transition
|
||||
enter="opacity-0 transition duration-300"
|
||||
@@ -268,11 +286,20 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
|
||||
</Modal>
|
||||
</Transition>
|
||||
<div className="media-header">
|
||||
<img
|
||||
src={`//image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`}
|
||||
alt=""
|
||||
className="media-poster"
|
||||
/>
|
||||
<div className="media-poster">
|
||||
<CachedImage
|
||||
src={
|
||||
data.posterPath
|
||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
layout="responsive"
|
||||
width={600}
|
||||
height={900}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className="media-title">
|
||||
<div className="media-status">
|
||||
<StatusBadge
|
||||
|
||||
18
src/components/Common/CachedImage/index.tsx
Normal file
18
src/components/Common/CachedImage/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import Image, { ImageProps } from 'next/image';
|
||||
import React from 'react';
|
||||
import useSettings from '../../../hooks/useSettings';
|
||||
|
||||
/**
|
||||
* The CachedImage component should be used wherever
|
||||
* we want to offer the option to locally cache images.
|
||||
*
|
||||
* It uses the `next/image` Image component but overrides
|
||||
* the `unoptimized` prop based on the application setting `cacheImages`.
|
||||
**/
|
||||
const CachedImage: React.FC<ImageProps> = (props) => {
|
||||
const { currentSettings } = useSettings();
|
||||
|
||||
return <Image unoptimized={!currentSettings.cacheImages} {...props} />;
|
||||
};
|
||||
|
||||
export default CachedImage;
|
||||
@@ -4,13 +4,13 @@ import React, {
|
||||
HTMLAttributes,
|
||||
ForwardRefRenderFunction,
|
||||
} from 'react';
|
||||
import Image from 'next/image';
|
||||
import CachedImage from '../CachedImage';
|
||||
|
||||
interface ImageFaderProps extends HTMLAttributes<HTMLDivElement> {
|
||||
backgroundImages: string[];
|
||||
rotationSpeed?: number;
|
||||
isDarker?: boolean;
|
||||
useImage?: boolean;
|
||||
forceOptimize?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_ROTATION_SPEED = 6000;
|
||||
@@ -20,7 +20,7 @@ const ImageFader: ForwardRefRenderFunction<HTMLDivElement, ImageFaderProps> = (
|
||||
backgroundImages,
|
||||
rotationSpeed = DEFAULT_ROTATION_SPEED,
|
||||
isDarker,
|
||||
useImage,
|
||||
forceOptimize,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -46,6 +46,14 @@ const ImageFader: ForwardRefRenderFunction<HTMLDivElement, ImageFaderProps> = (
|
||||
'linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%)';
|
||||
}
|
||||
|
||||
let overrides = {};
|
||||
|
||||
if (forceOptimize) {
|
||||
overrides = {
|
||||
unoptimized: false,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{backgroundImages.map((imageUrl, i) => (
|
||||
@@ -54,29 +62,20 @@ const ImageFader: ForwardRefRenderFunction<HTMLDivElement, ImageFaderProps> = (
|
||||
className={`absolute inset-0 bg-cover bg-center transition-opacity duration-300 ease-in ${
|
||||
i === activeIndex ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
style={{
|
||||
backgroundImage: !useImage
|
||||
? `${gradient}, url(${imageUrl})`
|
||||
: undefined,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{useImage && (
|
||||
<>
|
||||
<Image
|
||||
className="absolute inset-0 w-full h-full"
|
||||
alt=""
|
||||
src={imageUrl}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{ backgroundImage: gradient }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<CachedImage
|
||||
className="absolute inset-0 w-full h-full"
|
||||
alt=""
|
||||
src={imageUrl}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
{...overrides}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{ backgroundImage: gradient }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@ const MovieGenreList: React.FC = () => {
|
||||
<li key={`genre-${genre.id}-${index}`}>
|
||||
<GenreCard
|
||||
name={genre.name}
|
||||
image={`https://www.themoviedb.org/t/p/w1280_filter(duotone,${
|
||||
image={`https://image.tmdb.org/t/p/w1280_filter(duotone,${
|
||||
genreColorMap[genre.id] ?? genreColorMap[0]
|
||||
})${genre.backdrops[4]}`}
|
||||
url={`/discover/movies/genre/${genre.id}`}
|
||||
|
||||
@@ -54,7 +54,7 @@ const MovieGenreSlider: React.FC = () => {
|
||||
<GenreCard
|
||||
key={`genre-${genre.id}-${index}`}
|
||||
name={genre.name}
|
||||
image={`https://www.themoviedb.org/t/p/w1280_filter(duotone,${
|
||||
image={`https://image.tmdb.org/t/p/w1280_filter(duotone,${
|
||||
genreColorMap[genre.id] ?? genreColorMap[0]
|
||||
})${genre.backdrops[4]}`}
|
||||
url={`/discover/movies/genre/${genre.id}`}
|
||||
|
||||
@@ -40,7 +40,7 @@ const TvGenreList: React.FC = () => {
|
||||
<li key={`genre-${genre.id}-${index}`}>
|
||||
<GenreCard
|
||||
name={genre.name}
|
||||
image={`https://www.themoviedb.org/t/p/w1280_filter(duotone,${
|
||||
image={`https://image.tmdb.org/t/p/w1280_filter(duotone,${
|
||||
genreColorMap[genre.id] ?? genreColorMap[0]
|
||||
})${genre.backdrops[4]}`}
|
||||
url={`/discover/tv/genre/${genre.id}`}
|
||||
|
||||
@@ -54,7 +54,7 @@ const TvGenreSlider: React.FC = () => {
|
||||
<GenreCard
|
||||
key={`genre-tv-${genre.id}-${index}`}
|
||||
name={genre.name}
|
||||
image={`https://www.themoviedb.org/t/p/w1280_filter(duotone,${
|
||||
image={`https://image.tmdb.org/t/p/w1280_filter(duotone,${
|
||||
genreColorMap[genre.id] ?? genreColorMap[0]
|
||||
})${genre.backdrops[4]}`}
|
||||
url={`/discover/tv/genre/${genre.id}`}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Link from 'next/link';
|
||||
import React, { useState } from 'react';
|
||||
import { withProperties } from '../../utils/typeHelpers';
|
||||
import CachedImage from '../Common/CachedImage';
|
||||
|
||||
interface GenreCardProps {
|
||||
name: string;
|
||||
@@ -27,9 +28,6 @@ const GenreCard: React.FC<GenreCardProps> = ({
|
||||
? 'bg-gray-700 scale-105 ring-gray-500 bg-opacity-100'
|
||||
: 'bg-gray-800 scale-100 ring-gray-700 bg-opacity-80'
|
||||
} rounded-xl bg-cover bg-center overflow-hidden`}
|
||||
style={{
|
||||
backgroundImage: `url("${image}")`,
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setHovered(true);
|
||||
}}
|
||||
@@ -42,6 +40,7 @@ const GenreCard: React.FC<GenreCardProps> = ({
|
||||
role="link"
|
||||
tabIndex={0}
|
||||
>
|
||||
<CachedImage src={image} alt="" layout="fill" objectFit="cover" />
|
||||
<div
|
||||
className={`absolute z-10 inset-0 w-full h-full transition duration-300 bg-gray-800 ${
|
||||
isHovered ? 'bg-opacity-10' : 'bg-opacity-30'
|
||||
|
||||
@@ -63,7 +63,7 @@ const Login: React.FC = () => {
|
||||
<div className="relative flex flex-col min-h-screen bg-gray-900 py-14">
|
||||
<PageTitle title={intl.formatMessage(messages.signin)} />
|
||||
<ImageFader
|
||||
useImage
|
||||
forceOptimize
|
||||
backgroundImages={[
|
||||
'/images/rotate1.jpg',
|
||||
'/images/rotate2.jpg',
|
||||
|
||||
@@ -31,6 +31,7 @@ import DownloadBlock from '../DownloadBlock';
|
||||
import PageTitle from '../Common/PageTitle';
|
||||
import useSettings from '../../hooks/useSettings';
|
||||
import PlayButton, { PlayButtonLink } from '../Common/PlayButton';
|
||||
import CachedImage from '../Common/CachedImage';
|
||||
|
||||
const messages = defineMessages({
|
||||
releasedate: 'Release Date',
|
||||
@@ -203,9 +204,26 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
||||
className="media-page"
|
||||
style={{
|
||||
height: 493,
|
||||
backgroundImage: `linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%), url(//image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath})`,
|
||||
}}
|
||||
>
|
||||
{data.backdropPath && (
|
||||
<div className="media-page-bg-image">
|
||||
<CachedImage
|
||||
alt=""
|
||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
priority
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<PageTitle title={data.title} />
|
||||
<SlideOver
|
||||
show={showManager}
|
||||
@@ -380,15 +398,20 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
||||
)}
|
||||
</SlideOver>
|
||||
<div className="media-header">
|
||||
<img
|
||||
src={
|
||||
data.posterPath
|
||||
? `//image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
className="media-poster"
|
||||
/>
|
||||
<div className="media-poster">
|
||||
<CachedImage
|
||||
src={
|
||||
data.posterPath
|
||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
layout="responsive"
|
||||
width={600}
|
||||
height={900}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className="media-title">
|
||||
<div className="media-status">
|
||||
<StatusBadge
|
||||
@@ -519,13 +542,23 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
||||
<div className="mb-6">
|
||||
<Link href={`/collection/${data.collection.id}`}>
|
||||
<a>
|
||||
<div
|
||||
className="relative z-0 transition duration-300 scale-100 bg-gray-800 bg-center bg-cover rounded-lg shadow-md cursor-pointer transform-gpu group hover:scale-105"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(180deg, rgba(31, 41, 55, 0.47) 0%, rgba(31, 41, 55, 0.80) 100%), url(//image.tmdb.org/t/p/w1440_and_h320_multi_faces/${data.collection.backdropPath})`,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between p-4 text-gray-200 transition duration-300 h-14 group-hover:text-white">
|
||||
<div className="relative z-0 overflow-hidden transition duration-300 scale-100 bg-gray-800 bg-center bg-cover rounded-lg shadow-md cursor-pointer transform-gpu group hover:scale-105 ring-1 ring-gray-700 hover:ring-gray-500">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<CachedImage
|
||||
src={`https://image.tmdb.org/t/p/w1440_and_h320_multi_faces/${data.collection.backdropPath}`}
|
||||
alt=""
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(180deg, rgba(31, 41, 55, 0.47) 0%, rgba(31, 41, 55, 0.80) 100%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 flex items-center justify-between p-4 text-gray-200 transition duration-300 h-14 group-hover:text-white">
|
||||
<div>{data.collection.name}</div>
|
||||
<Button buttonSize="sm">
|
||||
{intl.formatMessage(messages.view)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Link from 'next/link';
|
||||
import React, { useState } from 'react';
|
||||
import CachedImage from '../Common/CachedImage';
|
||||
|
||||
interface PersonCardProps {
|
||||
personId: number;
|
||||
@@ -47,11 +48,14 @@ const PersonCard: React.FC<PersonCardProps> = ({
|
||||
<div className="absolute inset-0 flex flex-col items-center w-full h-full p-2">
|
||||
<div className="relative flex justify-center w-full mt-2 mb-4 h-1/2">
|
||||
{profilePath ? (
|
||||
<img
|
||||
src={`https://image.tmdb.org/t/p/w600_and_h900_bestv2${profilePath}`}
|
||||
className="object-cover w-3/4 h-full bg-center bg-cover rounded-full ring-1 ring-gray-700"
|
||||
alt=""
|
||||
/>
|
||||
<div className="relative w-3/4 h-full overflow-hidden rounded-full ring-1 ring-gray-700">
|
||||
<CachedImage
|
||||
src={`https://image.tmdb.org/t/p/w600_and_h900_bestv2${profilePath}`}
|
||||
alt=""
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<svg
|
||||
className="h-full"
|
||||
|
||||
@@ -13,6 +13,7 @@ import ImageFader from '../Common/ImageFader';
|
||||
import Ellipsis from '../../assets/ellipsis.svg';
|
||||
import { groupBy } from 'lodash';
|
||||
import PageTitle from '../Common/PageTitle';
|
||||
import CachedImage from '../Common/CachedImage';
|
||||
|
||||
const messages = defineMessages({
|
||||
appearsin: 'Appearances',
|
||||
@@ -172,7 +173,7 @@ const PersonDetails: React.FC = () => {
|
||||
.filter((media) => media.backdropPath)
|
||||
.map(
|
||||
(media) =>
|
||||
`//image.tmdb.org/t/p/w1920_and_h800_multi_faces/${media.backdropPath}`
|
||||
`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${media.backdropPath}`
|
||||
)
|
||||
.slice(0, 6)}
|
||||
/>
|
||||
@@ -180,12 +181,14 @@ const PersonDetails: React.FC = () => {
|
||||
)}
|
||||
<div className="relative z-10 flex flex-col items-center mt-4 mb-8 md:flex-row md:items-start">
|
||||
{data.profilePath && (
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `url(https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.profilePath})`,
|
||||
}}
|
||||
className="flex-shrink-0 mb-6 mr-0 bg-center bg-cover rounded-full w-36 h-36 md:w-44 md:h-44 md:mb-0 md:mr-6"
|
||||
/>
|
||||
<div className="relative flex-shrink-0 mb-6 mr-0 overflow-hidden rounded-full w-36 h-36 md:w-44 md:h-44 md:mb-0 md:mr-6 ring-1 ring-gray-700">
|
||||
<CachedImage
|
||||
src={`https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.profilePath}`}
|
||||
alt=""
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-center text-gray-300 md:text-left">
|
||||
<h1 className="mb-4 text-3xl text-white md:text-4xl">{data.name}</h1>
|
||||
|
||||
@@ -18,6 +18,7 @@ import Link from 'next/link';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import globalMessages from '../../i18n/globalMessages';
|
||||
import StatusBadge from '../StatusBadge';
|
||||
import CachedImage from '../Common/CachedImage';
|
||||
|
||||
const messages = defineMessages({
|
||||
status: 'Status',
|
||||
@@ -97,13 +98,25 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex p-4 text-gray-400 bg-gray-700 bg-center bg-cover shadow rounded-xl w-72 sm:w-96 ring-1 ring-gray-700"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(135deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 75%), url(//image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath})`,
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col flex-1 min-w-0 pr-4">
|
||||
<div className="relative flex p-4 overflow-hidden text-gray-400 bg-gray-800 bg-center bg-cover shadow rounded-xl w-72 sm:w-96 ring-1 ring-gray-700">
|
||||
{title.backdropPath && (
|
||||
<div className="absolute inset-0 z-0">
|
||||
<CachedImage
|
||||
alt=""
|
||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(135deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 75%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative z-10 flex flex-col flex-1 min-w-0 pr-4">
|
||||
<Link
|
||||
href={
|
||||
request.type === 'movie'
|
||||
@@ -243,15 +256,17 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
|
||||
: `/tv/${requestData.media.tmdbId}`
|
||||
}
|
||||
>
|
||||
<a className="flex-shrink-0 w-20 sm:w-28">
|
||||
<img
|
||||
<a className="flex-shrink-0 w-20 overflow-hidden transition duration-300 scale-100 rounded-md shadow-sm cursor-pointer sm:w-28 transform-gpu hover:scale-105 hover:shadow-md">
|
||||
<CachedImage
|
||||
src={
|
||||
title.posterPath
|
||||
? `//image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
className="w-20 transition duration-300 scale-100 rounded-md shadow-sm cursor-pointer sm:w-28 transform-gpu hover:scale-105 hover:shadow-md"
|
||||
layout="responsive"
|
||||
width={600}
|
||||
height={900}
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
@@ -20,6 +20,7 @@ import Link from 'next/link';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import RequestModal from '../../RequestModal';
|
||||
import ConfirmButton from '../../Common/ConfirmButton';
|
||||
import CachedImage from '../../Common/CachedImage';
|
||||
|
||||
const messages = defineMessages({
|
||||
seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
|
||||
@@ -133,14 +134,23 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
||||
}}
|
||||
/>
|
||||
<div className="relative flex flex-col justify-between w-full py-4 overflow-hidden text-gray-400 bg-gray-800 shadow-md ring-1 ring-gray-700 rounded-xl xl:h-32 xl:flex-row">
|
||||
<div
|
||||
className="absolute inset-0 z-0 w-full bg-center bg-cover xl:w-2/3"
|
||||
style={{
|
||||
backgroundImage: title.backdropPath
|
||||
? `linear-gradient(90deg, rgba(31, 41, 55, 0.47) 0%, rgba(31, 41, 55, 1) 100%), url(//image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath})`
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
{title.backdropPath && (
|
||||
<div className="absolute inset-0 z-0 w-full bg-center bg-cover xl:w-2/3">
|
||||
<CachedImage
|
||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
|
||||
alt=""
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(90deg, rgba(31, 41, 55, 0.47) 0%, rgba(31, 41, 55, 1) 100%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative flex flex-col justify-between w-full overflow-hidden sm:flex-row">
|
||||
<div className="relative z-10 flex items-center w-full pl-4 pr-4 overflow-hidden xl:w-7/12 2xl:w-2/3 sm:pr-0">
|
||||
<Link
|
||||
@@ -150,15 +160,18 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
||||
: `/tv/${requestData.media.tmdbId}`
|
||||
}
|
||||
>
|
||||
<a className="flex-shrink-0 w-12 h-auto overflow-hidden transition duration-300 scale-100 rounded-md sm:w-14 transform-gpu hover:scale-105">
|
||||
<img
|
||||
<a className="relative flex-shrink-0 w-12 h-auto overflow-hidden transition duration-300 scale-100 rounded-md sm:w-14 transform-gpu hover:scale-105">
|
||||
<CachedImage
|
||||
src={
|
||||
title.posterPath
|
||||
? `//image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
className="object-cover"
|
||||
layout="responsive"
|
||||
width={600}
|
||||
height={900}
|
||||
objectFit="cover"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
@@ -39,6 +39,9 @@ const messages = defineMessages({
|
||||
'Sets external API access to read-only (requires HTTPS, and Overseerr must be reloaded for changes to take effect)',
|
||||
csrfProtectionHoverTip:
|
||||
'Do NOT enable this setting unless you understand what you are doing!',
|
||||
cacheImages: 'Cache & Optimize Images Locally',
|
||||
cacheImagesTip:
|
||||
'Enabling this option will cause all images to be optimized and stored locally. This uses a significant amount of disk space.',
|
||||
trustProxy: 'Enable Proxy Support',
|
||||
trustProxyTip:
|
||||
'Allows Overseerr to correctly register client IP addresses behind a proxy (Overseerr must be reloaded for changes to take effect)',
|
||||
@@ -144,6 +147,7 @@ const SettingsMain: React.FC = () => {
|
||||
originalLanguage: data?.originalLanguage,
|
||||
partialRequestsEnabled: data?.partialRequestsEnabled,
|
||||
trustProxy: data?.trustProxy,
|
||||
cacheImages: data?.cacheImages,
|
||||
}}
|
||||
enableReinitialize
|
||||
validationSchema={MainSettingsSchema}
|
||||
@@ -158,6 +162,7 @@ const SettingsMain: React.FC = () => {
|
||||
originalLanguage: values.originalLanguage,
|
||||
partialRequestsEnabled: values.partialRequestsEnabled,
|
||||
trustProxy: values.trustProxy,
|
||||
cacheImages: values.cacheImages,
|
||||
});
|
||||
|
||||
addToast(intl.formatMessage(messages.toastSettingsSuccess), {
|
||||
@@ -299,6 +304,26 @@ const SettingsMain: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="csrfProtection" className="checkbox-label">
|
||||
<span className="mr-2">
|
||||
{intl.formatMessage(messages.cacheImages)}
|
||||
</span>
|
||||
<Badge badgeType="warning">
|
||||
{intl.formatMessage(globalMessages.experimental)}
|
||||
</Badge>
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.cacheImagesTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="cacheImages"
|
||||
name="cacheImages"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="region" className="text-label">
|
||||
<span>{intl.formatMessage(messages.region)}</span>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useIsTouch } from '../../hooks/useIsTouch';
|
||||
import globalMessages from '../../i18n/globalMessages';
|
||||
import Spinner from '../../assets/spinner.svg';
|
||||
import { useUser, Permission } from '../../hooks/useUser';
|
||||
import CachedImage from '../Common/CachedImage';
|
||||
|
||||
const messages = defineMessages({
|
||||
movie: 'Movie',
|
||||
@@ -81,16 +82,13 @@ const TitleCard: React.FC<TitleCardProps> = ({
|
||||
onCancel={closeModal}
|
||||
/>
|
||||
<div
|
||||
className={`transition duration-300 transform-gpu scale-100 outline-none cursor-default relative bg-gray-800 bg-cover rounded-xl ring-1 ${
|
||||
className={`transition duration-300 transform-gpu scale-100 outline-none cursor-default relative bg-gray-800 bg-cover rounded-xl ring-1 overflow-hidden ${
|
||||
showDetail
|
||||
? 'scale-105 shadow-lg ring-gray-500'
|
||||
: 'shadow ring-gray-700'
|
||||
}`}
|
||||
style={{
|
||||
paddingBottom: '150%',
|
||||
backgroundImage: image
|
||||
? `url(//image.tmdb.org/t/p/w300_and_h450_face${image})`
|
||||
: `url('/images/overseerr_poster_not_found_logo_top.png')`,
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (!isTouch) {
|
||||
@@ -108,6 +106,17 @@ const TitleCard: React.FC<TitleCardProps> = ({
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="absolute inset-0 w-full h-full overflow-hidden">
|
||||
<CachedImage
|
||||
className="absolute inset-0 w-full h-full"
|
||||
alt=""
|
||||
src={
|
||||
image
|
||||
? `https://image.tmdb.org/t/p/w300_and_h450_face${image}`
|
||||
: `/images/overseerr_poster_not_found_logo_top.png`
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
<div className="absolute left-0 right-0 flex items-center justify-between p-2">
|
||||
<div
|
||||
className={`rounded-full z-40 pointer-events-none shadow ${
|
||||
|
||||
@@ -34,6 +34,7 @@ import DownloadBlock from '../DownloadBlock';
|
||||
import PageTitle from '../Common/PageTitle';
|
||||
import useSettings from '../../hooks/useSettings';
|
||||
import PlayButton, { PlayButtonLink } from '../Common/PlayButton';
|
||||
import CachedImage from '../Common/CachedImage';
|
||||
|
||||
const messages = defineMessages({
|
||||
firstAirDate: 'First Air Date',
|
||||
@@ -228,9 +229,26 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
||||
className="media-page"
|
||||
style={{
|
||||
height: 493,
|
||||
backgroundImage: `linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%), url(//image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath})`,
|
||||
}}
|
||||
>
|
||||
{data.backdropPath && (
|
||||
<div className="media-page-bg-image">
|
||||
<CachedImage
|
||||
alt=""
|
||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
priority
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<PageTitle title={data.name} />
|
||||
<RequestModal
|
||||
tmdbId={data.id}
|
||||
@@ -418,15 +436,20 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
||||
)}
|
||||
</SlideOver>
|
||||
<div className="media-header">
|
||||
<img
|
||||
src={
|
||||
data.posterPath
|
||||
? `//image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
className="media-poster"
|
||||
/>
|
||||
<div className="media-poster">
|
||||
<CachedImage
|
||||
src={
|
||||
data.posterPath
|
||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
layout="responsive"
|
||||
width={600}
|
||||
height={900}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className="media-title">
|
||||
<div className="media-status">
|
||||
<StatusBadge
|
||||
|
||||
@@ -48,7 +48,7 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
|
||||
<div className="flex-shrink-0">
|
||||
<div className="relative">
|
||||
<img
|
||||
className="w-24 h-24 bg-gray-600 rounded-full"
|
||||
className="w-24 h-24 bg-gray-600 rounded-full ring-1 ring-gray-700"
|
||||
src={user.avatar}
|
||||
alt=""
|
||||
/>
|
||||
|
||||
@@ -69,7 +69,7 @@ const UserProfile: React.FC = () => {
|
||||
.filter((media) => media.backdropPath)
|
||||
.map(
|
||||
(media) =>
|
||||
`//image.tmdb.org/t/p/w1920_and_h800_multi_faces/${media.backdropPath}`
|
||||
`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${media.backdropPath}`
|
||||
)
|
||||
.slice(0, 6)}
|
||||
/>
|
||||
|
||||
@@ -16,6 +16,7 @@ const defaultSettings = {
|
||||
region: '',
|
||||
originalLanguage: '',
|
||||
partialRequestsEnabled: true,
|
||||
cacheImages: false,
|
||||
};
|
||||
|
||||
export const SettingsContext = React.createContext<SettingsContextProps>({
|
||||
|
||||
@@ -536,6 +536,8 @@
|
||||
"components.Settings.apikey": "API Key",
|
||||
"components.Settings.applicationTitle": "Application Title",
|
||||
"components.Settings.applicationurl": "Application URL",
|
||||
"components.Settings.cacheImages": "Cache & Optimize Images Locally",
|
||||
"components.Settings.cacheImagesTip": "Enabling this option will cause all images to be optimized and stored locally. This uses a significant amount of disk space.",
|
||||
"components.Settings.cancelscan": "Cancel Scan",
|
||||
"components.Settings.copied": "Copied API key to clipboard.",
|
||||
"components.Settings.csrfProtection": "Enable CSRF Protection",
|
||||
|
||||
@@ -149,6 +149,7 @@ CoreApp.getInitialProps = async (initialProps) => {
|
||||
region: '',
|
||||
originalLanguage: '',
|
||||
partialRequestsEnabled: true,
|
||||
cacheImages: false,
|
||||
};
|
||||
|
||||
let locale = 'en';
|
||||
|
||||
@@ -42,7 +42,12 @@ a.slider-title {
|
||||
}
|
||||
|
||||
.media-page {
|
||||
@apply px-4 pt-16 -mx-4 -mt-16 bg-center bg-cover;
|
||||
@apply relative px-4 pt-16 -mx-4 -mt-16 bg-center bg-cover;
|
||||
}
|
||||
|
||||
.media-page-bg-image {
|
||||
@apply absolute inset-0 w-full h-full;
|
||||
z-index: -10;
|
||||
}
|
||||
|
||||
.media-header {
|
||||
@@ -50,7 +55,7 @@ a.slider-title {
|
||||
}
|
||||
|
||||
.media-poster {
|
||||
@apply w-32 rounded shadow md:rounded-lg md:shadow-2xl md:w-44 xl:w-52 xl:mr-4;
|
||||
@apply w-32 overflow-hidden rounded shadow md:rounded-lg md:shadow-2xl md:w-44 xl:w-52 xl:mr-4;
|
||||
}
|
||||
|
||||
.media-status {
|
||||
|
||||
Reference in New Issue
Block a user