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