feat: add option to cache images locally (#1213)

pull/1214/head
sct 3 years ago committed by GitHub
parent dfd4ff9229
commit 0ca3d43749
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

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

Loading…
Cancel
Save