mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-01 12:18:35 -05:00
Merge remote-tracking branch 'overseerr/develop' into develop
This commit is contained in:
@@ -1,18 +1,27 @@
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import type { ImageProps } from 'next/image';
|
||||
import type { ImageLoader, ImageProps } from 'next/image';
|
||||
import Image from 'next/image';
|
||||
|
||||
const imageLoader: ImageLoader = ({ src }) => src;
|
||||
|
||||
/**
|
||||
* 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 = (props: ImageProps) => {
|
||||
const CachedImage = ({ src, ...props }: ImageProps) => {
|
||||
const { currentSettings } = useSettings();
|
||||
|
||||
return <Image unoptimized={!currentSettings.cacheImages} {...props} />;
|
||||
let imageUrl = src;
|
||||
|
||||
if (typeof imageUrl === 'string' && imageUrl.startsWith('http')) {
|
||||
const parsedUrl = new URL(imageUrl);
|
||||
|
||||
if (parsedUrl.host === 'image.tmdb.org' && currentSettings.cacheImages) {
|
||||
imageUrl = imageUrl.replace('https://image.tmdb.org', '/imageproxy');
|
||||
}
|
||||
}
|
||||
|
||||
return <Image unoptimized loader={imageLoader} src={imageUrl} {...props} />;
|
||||
};
|
||||
|
||||
export default CachedImage;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import CachedImage from '@app/components/Common/CachedImage';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
|
||||
@@ -30,11 +31,15 @@ const CompanyCard = ({ image, url, name }: CompanyCardProps) => {
|
||||
role="link"
|
||||
tabIndex={0}
|
||||
>
|
||||
<img
|
||||
src={image}
|
||||
alt={name}
|
||||
className="relative z-40 max-h-full max-w-full"
|
||||
/>
|
||||
<div className="relative h-full w-full">
|
||||
<CachedImage
|
||||
src={image}
|
||||
alt={name}
|
||||
className="relative z-40 h-full w-full"
|
||||
layout="fill"
|
||||
objectFit="contain"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 z-0 h-12 rounded-b-xl bg-gradient-to-t ${
|
||||
isHovered ? 'from-gray-800' : 'from-gray-900'
|
||||
|
||||
@@ -7,6 +7,7 @@ import PageTitle from '@app/components/Common/PageTitle';
|
||||
import IssueComment from '@app/components/IssueDetails/IssueComment';
|
||||
import IssueDescription from '@app/components/IssueDetails/IssueDescription';
|
||||
import { issueOptions } from '@app/components/IssueModal/constants';
|
||||
import useDeepLinks from '@app/hooks/useDeepLinks';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
@@ -91,6 +92,13 @@ const IssueDetails = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
const { mediaUrl, mediaUrl4k } = useDeepLinks({
|
||||
mediaUrl: data?.mediaInfo?.mediaUrl,
|
||||
mediaUrl4k: data?.mediaInfo?.mediaUrl4k,
|
||||
iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
|
||||
iOSPlexUrl4k: data?.mediaInfo?.iOSPlexUrl4k,
|
||||
});
|
||||
|
||||
const CommentSchema = Yup.object().shape({
|
||||
message: Yup.string().required(),
|
||||
});
|
||||
@@ -359,7 +367,7 @@ const IssueDetails = () => {
|
||||
{issueData?.media.mediaUrl && (
|
||||
<Button
|
||||
as="a"
|
||||
href={issueData?.media.mediaUrl}
|
||||
href={mediaUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="w-full"
|
||||
@@ -405,7 +413,7 @@ const IssueDetails = () => {
|
||||
{issueData?.media.mediaUrl4k && (
|
||||
<Button
|
||||
as="a"
|
||||
href={issueData?.media.mediaUrl4k}
|
||||
href={mediaUrl4k}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="w-full"
|
||||
@@ -621,7 +629,7 @@ const IssueDetails = () => {
|
||||
{issueData?.media.mediaUrl && (
|
||||
<Button
|
||||
as="a"
|
||||
href={issueData?.media.mediaUrl}
|
||||
href={mediaUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="w-full"
|
||||
@@ -667,7 +675,7 @@ const IssueDetails = () => {
|
||||
{issueData?.media.mediaUrl4k && (
|
||||
<Button
|
||||
as="a"
|
||||
href={issueData?.media.mediaUrl4k}
|
||||
href={mediaUrl4k}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="w-full"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import TitleCard from '@app/components/TitleCard';
|
||||
import { ArrowCircleRightIcon } from '@heroicons/react/solid';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -15,6 +17,18 @@ interface ShowMoreCardProps {
|
||||
const ShowMoreCard = ({ url, posters }: ShowMoreCardProps) => {
|
||||
const intl = useIntl();
|
||||
const [isHovered, setHovered] = useState(false);
|
||||
const { ref, inView } = useInView({
|
||||
triggerOnce: true,
|
||||
});
|
||||
|
||||
if (!inView) {
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<TitleCard.Placeholder />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={url}>
|
||||
<a
|
||||
|
||||
@@ -18,6 +18,7 @@ import PersonCard from '@app/components/PersonCard';
|
||||
import RequestButton from '@app/components/RequestButton';
|
||||
import Slider from '@app/components/Slider';
|
||||
import StatusBadge from '@app/components/StatusBadge';
|
||||
import useDeepLinks from '@app/hooks/useDeepLinks';
|
||||
import useLocale from '@app/hooks/useLocale';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
@@ -129,31 +130,12 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
setShowManager(router.query.manage == '1' ? true : false);
|
||||
}, [router.query.manage]);
|
||||
|
||||
const [plexUrl, setPlexUrl] = useState(data?.mediaInfo?.mediaUrl);
|
||||
const [plexUrl4k, setPlexUrl4k] = useState(data?.mediaInfo?.mediaUrl4k);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (
|
||||
settings.currentSettings.mediaServerType === MediaServerType.PLEX &&
|
||||
(/iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
||||
(navigator.userAgent === 'MacIntel' && navigator.maxTouchPoints > 1))
|
||||
) {
|
||||
setPlexUrl(data.mediaInfo?.iOSPlexUrl);
|
||||
setPlexUrl4k(data.mediaInfo?.iOSPlexUrl4k);
|
||||
} else {
|
||||
setPlexUrl(data.mediaInfo?.mediaUrl);
|
||||
setPlexUrl4k(data.mediaInfo?.mediaUrl4k);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
data,
|
||||
data?.mediaInfo?.iOSPlexUrl,
|
||||
data?.mediaInfo?.iOSPlexUrl4k,
|
||||
data?.mediaInfo?.mediaUrl,
|
||||
data?.mediaInfo?.mediaUrl4k,
|
||||
settings.currentSettings.mediaServerType,
|
||||
]);
|
||||
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
|
||||
mediaUrl: data?.mediaInfo?.mediaUrl,
|
||||
mediaUrl4k: data?.mediaInfo?.mediaUrl4k,
|
||||
iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
|
||||
iOSPlexUrl4k: data?.mediaInfo?.iOSPlexUrl4k,
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
@@ -378,7 +360,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
}
|
||||
tmdbId={data.mediaInfo?.tmdbId}
|
||||
mediaType="movie"
|
||||
plexUrl={plexUrl}
|
||||
plexUrl={plexUrl4k}
|
||||
serviceUrl={data.mediaInfo?.serviceUrl4k}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { RefreshIcon } from '@heroicons/react/outline';
|
||||
import Router from 'next/router';
|
||||
import { useRouter } from 'next/router';
|
||||
import PR from 'pulltorefreshjs';
|
||||
import { useEffect } from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
|
||||
const PullToRefresh: React.FC = () => {
|
||||
const PullToRefresh = () => {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
PR.init({
|
||||
mainElement: '#pull-to-refresh',
|
||||
onRefresh() {
|
||||
Router.reload();
|
||||
router.reload();
|
||||
},
|
||||
iconArrow: ReactDOMServer.renderToString(
|
||||
<div className="p-2">
|
||||
@@ -28,11 +30,14 @@ const PullToRefresh: React.FC = () => {
|
||||
instructionsReleaseToRefresh: ReactDOMServer.renderToString(<div />),
|
||||
instructionsRefreshing: ReactDOMServer.renderToString(<div />),
|
||||
distReload: 60,
|
||||
distIgnore: 15,
|
||||
shouldPullToRefresh: () =>
|
||||
!window.scrollY && document.body.style.overflow !== 'hidden',
|
||||
});
|
||||
return () => {
|
||||
PR.destroyAll();
|
||||
};
|
||||
}, []);
|
||||
}, [router]);
|
||||
|
||||
return <div id="pull-to-refresh"></div>;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import CachedImage from '@app/components/Common/CachedImage';
|
||||
import Tooltip from '@app/components/Common/Tooltip';
|
||||
import RequestModal from '@app/components/RequestModal';
|
||||
import StatusBadge from '@app/components/StatusBadge';
|
||||
import useDeepLinks from '@app/hooks/useDeepLinks';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import { withProperties } from '@app/utils/typeHelpers';
|
||||
@@ -61,6 +62,13 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
|
||||
const { hasPermission } = useUser();
|
||||
const intl = useIntl();
|
||||
|
||||
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
|
||||
mediaUrl: requestData?.media?.mediaUrl,
|
||||
mediaUrl4k: requestData?.media?.mediaUrl4k,
|
||||
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
|
||||
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
|
||||
});
|
||||
|
||||
const deleteRequest = async () => {
|
||||
await axios.delete(`/api/v1/media/${requestData?.media.id}`);
|
||||
mutate('/api/v1/media?filter=allavailable&take=20&sort=mediaAdded');
|
||||
@@ -138,11 +146,7 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
|
||||
).length > 0
|
||||
}
|
||||
is4k={requestData.is4k}
|
||||
plexUrl={
|
||||
requestData.is4k
|
||||
? requestData.media.mediaUrl4k
|
||||
: requestData.media.mediaUrl
|
||||
}
|
||||
plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
|
||||
serviceUrl={
|
||||
requestData.is4k
|
||||
? requestData.media.serviceUrl4k
|
||||
@@ -217,6 +221,13 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
||||
fallbackData: request,
|
||||
});
|
||||
|
||||
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
|
||||
mediaUrl: requestData?.media?.mediaUrl,
|
||||
mediaUrl4k: requestData?.media?.mediaUrl4k,
|
||||
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
|
||||
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
|
||||
});
|
||||
|
||||
const modifyRequest = async (type: 'approve' | 'decline') => {
|
||||
const response = await axios.post(`/api/v1/request/${request.id}/${type}`);
|
||||
|
||||
@@ -396,11 +407,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
||||
is4k={requestData.is4k}
|
||||
tmdbId={requestData.media.tmdbId}
|
||||
mediaType={requestData.type}
|
||||
plexUrl={
|
||||
requestData.is4k
|
||||
? requestData.media.mediaUrl4k
|
||||
: requestData.media.mediaUrl
|
||||
}
|
||||
plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
|
||||
serviceUrl={
|
||||
requestData.is4k
|
||||
? requestData.media.serviceUrl4k
|
||||
|
||||
@@ -4,6 +4,7 @@ import CachedImage from '@app/components/Common/CachedImage';
|
||||
import ConfirmButton from '@app/components/Common/ConfirmButton';
|
||||
import RequestModal from '@app/components/RequestModal';
|
||||
import StatusBadge from '@app/components/StatusBadge';
|
||||
import useDeepLinks from '@app/hooks/useDeepLinks';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import {
|
||||
@@ -61,6 +62,13 @@ const RequestItemError = ({
|
||||
revalidateList();
|
||||
};
|
||||
|
||||
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
|
||||
mediaUrl: requestData?.media?.mediaUrl,
|
||||
mediaUrl4k: requestData?.media?.mediaUrl4k,
|
||||
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
|
||||
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex h-64 w-full flex-col justify-center rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-red-500 xl:h-28 xl:flex-row">
|
||||
<div className="flex w-full flex-col justify-between overflow-hidden sm:flex-row">
|
||||
@@ -130,11 +138,7 @@ const RequestItemError = ({
|
||||
).length > 0
|
||||
}
|
||||
is4k={requestData.is4k}
|
||||
plexUrl={
|
||||
requestData.is4k
|
||||
? requestData.media.mediaUrl4k
|
||||
: requestData.media.mediaUrl
|
||||
}
|
||||
plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
|
||||
serviceUrl={
|
||||
requestData.is4k
|
||||
? requestData.media.serviceUrl4k
|
||||
@@ -316,6 +320,13 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
|
||||
mediaUrl: requestData?.media?.mediaUrl,
|
||||
mediaUrl4k: requestData?.media?.mediaUrl4k,
|
||||
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
|
||||
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
|
||||
});
|
||||
|
||||
if (!title && !error) {
|
||||
return (
|
||||
<div
|
||||
@@ -462,11 +473,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
is4k={requestData.is4k}
|
||||
tmdbId={requestData.media.tmdbId}
|
||||
mediaType={requestData.type}
|
||||
plexUrl={
|
||||
requestData.is4k
|
||||
? requestData.media.mediaUrl4k
|
||||
: requestData.media.mediaUrl
|
||||
}
|
||||
plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
|
||||
serviceUrl={
|
||||
requestData.is4k
|
||||
? requestData.media.serviceUrl4k
|
||||
|
||||
@@ -13,7 +13,10 @@ import { Transition } from '@headlessui/react';
|
||||
import { PlayIcon, StopIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import { PencilIcon } from '@heroicons/react/solid';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import type { CacheItem } from '@server/interfaces/api/settingsInterfaces';
|
||||
import type {
|
||||
CacheItem,
|
||||
CacheResponse,
|
||||
} from '@server/interfaces/api/settingsInterfaces';
|
||||
import type { JobId } from '@server/lib/settings';
|
||||
import axios from 'axios';
|
||||
import cronstrue from 'cronstrue/i18n';
|
||||
@@ -58,6 +61,7 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
|
||||
'sonarr-scan': 'Sonarr Scan',
|
||||
'download-sync': 'Download Sync',
|
||||
'download-sync-reset': 'Download Sync Reset',
|
||||
'image-cache-cleanup': 'Image Cache Cleanup',
|
||||
editJobSchedule: 'Modify Job',
|
||||
jobScheduleEditSaved: 'Job edited successfully!',
|
||||
jobScheduleEditFailed: 'Something went wrong while saving the job.',
|
||||
@@ -67,6 +71,11 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
|
||||
'Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}',
|
||||
editJobScheduleSelectorMinutes:
|
||||
'Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}',
|
||||
imagecache: 'Image Cache',
|
||||
imagecacheDescription:
|
||||
'When enabled in settings, Overseerr will proxy and cache images from pre-configured external sources. Cached images are saved into your config folder. You can find the files in <code>{appDataPath}/cache/images</code>.',
|
||||
imagecachecount: 'Images Cached',
|
||||
imagecachesize: 'Total Cache Size',
|
||||
});
|
||||
|
||||
interface Job {
|
||||
@@ -132,7 +141,8 @@ const SettingsJobs = () => {
|
||||
} = useSWR<Job[]>('/api/v1/settings/jobs', {
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
const { data: cacheData, mutate: cacheRevalidate } = useSWR<CacheItem[]>(
|
||||
const { data: appData } = useSWR('/api/v1/status/appdata');
|
||||
const { data: cacheData, mutate: cacheRevalidate } = useSWR<CacheResponse>(
|
||||
'/api/v1/settings/cache',
|
||||
{
|
||||
refreshInterval: 10000,
|
||||
@@ -435,7 +445,7 @@ const SettingsJobs = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<Table.TBody>
|
||||
{cacheData
|
||||
{cacheData?.apiCaches
|
||||
?.filter(
|
||||
(cache) =>
|
||||
!(
|
||||
@@ -465,6 +475,41 @@ const SettingsJobs = () => {
|
||||
</Table.TBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="heading">{intl.formatMessage(messages.imagecache)}</h3>
|
||||
<p className="description">
|
||||
{intl.formatMessage(messages.imagecacheDescription, {
|
||||
code: (msg: React.ReactNode) => (
|
||||
<code className="bg-opacity-50">{msg}</code>
|
||||
),
|
||||
appDataPath: appData ? appData.appDataPath : '/app/config',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="section">
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<Table.TH>{intl.formatMessage(messages.cachename)}</Table.TH>
|
||||
<Table.TH>
|
||||
{intl.formatMessage(messages.imagecachecount)}
|
||||
</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.imagecachesize)}</Table.TH>
|
||||
</tr>
|
||||
</thead>
|
||||
<Table.TBody>
|
||||
<tr>
|
||||
<Table.TD>The Movie Database (tmdb)</Table.TD>
|
||||
<Table.TD>
|
||||
{intl.formatNumber(cacheData?.imageCache.tmdb.imageCount ?? 0)}
|
||||
</Table.TD>
|
||||
<Table.TD>
|
||||
{formatBytes(cacheData?.imageCache.tmdb.size ?? 0)}
|
||||
</Table.TD>
|
||||
</tr>
|
||||
</Table.TBody>
|
||||
</Table>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@ const messages = defineMessages({
|
||||
'Do NOT enable this setting unless you understand what you are doing!',
|
||||
cacheImages: 'Enable Image Caching',
|
||||
cacheImagesTip:
|
||||
'Cache and serve optimized images (requires a significant amount of disk space)',
|
||||
'Cache externally sourced images (requires a significant amount of disk space)',
|
||||
trustProxy: 'Enable Proxy Support',
|
||||
trustProxyTip:
|
||||
'Allow Overseerr to correctly register client IP addresses behind a proxy',
|
||||
|
||||
@@ -22,6 +22,7 @@ import RequestModal from '@app/components/RequestModal';
|
||||
import Slider from '@app/components/Slider';
|
||||
import StatusBadge from '@app/components/StatusBadge';
|
||||
import Season from '@app/components/TvDetails/Season';
|
||||
import useDeepLinks from '@app/hooks/useDeepLinks';
|
||||
import useLocale from '@app/hooks/useLocale';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
@@ -125,31 +126,12 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
setShowManager(router.query.manage == '1' ? true : false);
|
||||
}, [router.query.manage]);
|
||||
|
||||
const [plexUrl, setPlexUrl] = useState(data?.mediaInfo?.mediaUrl);
|
||||
const [plexUrl4k, setPlexUrl4k] = useState(data?.mediaInfo?.mediaUrl4k);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (
|
||||
settings.currentSettings.mediaServerType === MediaServerType.PLEX &&
|
||||
(/iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
||||
(navigator.userAgent === 'MacIntel' && navigator.maxTouchPoints > 1))
|
||||
) {
|
||||
setPlexUrl(data.mediaInfo?.iOSPlexUrl);
|
||||
setPlexUrl4k(data.mediaInfo?.iOSPlexUrl4k);
|
||||
} else {
|
||||
setPlexUrl(data.mediaInfo?.mediaUrl);
|
||||
setPlexUrl4k(data.mediaInfo?.mediaUrl4k);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
data,
|
||||
data?.mediaInfo?.iOSPlexUrl,
|
||||
data?.mediaInfo?.iOSPlexUrl4k,
|
||||
data?.mediaInfo?.mediaUrl,
|
||||
data?.mediaInfo?.mediaUrl4k,
|
||||
settings.currentSettings.mediaServerType,
|
||||
]);
|
||||
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
|
||||
mediaUrl: data?.mediaInfo?.mediaUrl,
|
||||
mediaUrl4k: data?.mediaInfo?.mediaUrl4k,
|
||||
iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
|
||||
iOSPlexUrl4k: data?.mediaInfo?.iOSPlexUrl4k,
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
@@ -984,9 +966,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
tvdbId={data.externalIds.tvdbId}
|
||||
imdbId={data.externalIds.imdbId}
|
||||
rtUrl={ratingData?.url}
|
||||
mediaUrl={
|
||||
data.mediaInfo?.mediaUrl ?? data.mediaInfo?.mediaUrl4k
|
||||
}
|
||||
mediaUrl={plexUrl ?? plexUrl4k}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ export type AvailableLocale =
|
||||
| 'el'
|
||||
| 'es'
|
||||
| 'fr'
|
||||
| 'hr'
|
||||
| 'hu'
|
||||
| 'it'
|
||||
| 'ja'
|
||||
@@ -60,6 +61,10 @@ export const availableLanguages: AvailableLanguageObject = {
|
||||
code: 'fr',
|
||||
display: 'Français',
|
||||
},
|
||||
hr: {
|
||||
code: 'hr',
|
||||
display: 'Hrvatski',
|
||||
},
|
||||
it: {
|
||||
code: 'it',
|
||||
display: 'Italiano',
|
||||
|
||||
45
src/hooks/useDeepLinks.ts
Normal file
45
src/hooks/useDeepLinks.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface useDeepLinksProps {
|
||||
mediaUrl?: string;
|
||||
mediaUrl4k?: string;
|
||||
iOSPlexUrl?: string;
|
||||
iOSPlexUrl4k?: string;
|
||||
}
|
||||
|
||||
const useDeepLinks = ({
|
||||
mediaUrl,
|
||||
mediaUrl4k,
|
||||
iOSPlexUrl,
|
||||
iOSPlexUrl4k,
|
||||
}: useDeepLinksProps) => {
|
||||
const [returnedMediaUrl, setReturnedMediaUrl] = useState(mediaUrl);
|
||||
const [returnedMediaUrl4k, setReturnedMediaUrl4k] = useState(mediaUrl4k);
|
||||
const settings = useSettings();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
settings.currentSettings.mediaServerType === MediaServerType.PLEX &&
|
||||
(/iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
||||
(navigator.userAgent === 'MacIntel' && navigator.maxTouchPoints > 1))
|
||||
) {
|
||||
setReturnedMediaUrl(iOSPlexUrl);
|
||||
setReturnedMediaUrl4k(iOSPlexUrl4k);
|
||||
} else {
|
||||
setReturnedMediaUrl(mediaUrl);
|
||||
setReturnedMediaUrl4k(mediaUrl4k);
|
||||
}
|
||||
}, [
|
||||
iOSPlexUrl,
|
||||
iOSPlexUrl4k,
|
||||
mediaUrl,
|
||||
mediaUrl4k,
|
||||
settings.currentSettings.mediaServerType,
|
||||
]);
|
||||
|
||||
return { mediaUrl: returnedMediaUrl, mediaUrl4k: returnedMediaUrl4k };
|
||||
};
|
||||
|
||||
export default useDeepLinks;
|
||||
@@ -649,6 +649,11 @@
|
||||
"components.Settings.SettingsJobsCache.flushcache": "Flush Cache",
|
||||
"components.Settings.SettingsJobsCache.jelly-recently-added-scan": "Jellyfin Recently Added Scan",
|
||||
"components.Settings.SettingsJobsCache.jellyfin-full-scan": "Jellyfin Full Library Scan",
|
||||
"components.Settings.SettingsJobsCache.image-cache-cleanup": "Image Cache Cleanup",
|
||||
"components.Settings.SettingsJobsCache.imagecache": "Image Cache",
|
||||
"components.Settings.SettingsJobsCache.imagecacheDescription": "When enabled in settings, Overseerr will proxy and cache images from pre-configured external sources. Cached images are saved into your config folder. You can find the files in <code>{appDataPath}/cache/images</code>.",
|
||||
"components.Settings.SettingsJobsCache.imagecachecount": "Images Cached",
|
||||
"components.Settings.SettingsJobsCache.imagecachesize": "Total Cache Size",
|
||||
"components.Settings.SettingsJobsCache.jobScheduleEditFailed": "Something went wrong while saving the job.",
|
||||
"components.Settings.SettingsJobsCache.jobScheduleEditSaved": "Job edited successfully!",
|
||||
"components.Settings.SettingsJobsCache.jobcancelled": "{jobname} canceled.",
|
||||
@@ -759,7 +764,7 @@
|
||||
"components.Settings.applicationTitle": "Application Title",
|
||||
"components.Settings.applicationurl": "Application URL",
|
||||
"components.Settings.cacheImages": "Enable Image Caching",
|
||||
"components.Settings.cacheImagesTip": "Cache and serve optimized images (requires a significant amount of disk space)",
|
||||
"components.Settings.cacheImagesTip": "Cache externally sourced images (requires 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",
|
||||
|
||||
@@ -43,6 +43,8 @@ const loadLocaleData = (locale: AvailableLocale): Promise<any> => {
|
||||
return import('../i18n/locale/es.json');
|
||||
case 'fr':
|
||||
return import('../i18n/locale/fr.json');
|
||||
case 'hr':
|
||||
return import('../i18n/locale/hr.json');
|
||||
case 'hu':
|
||||
return import('../i18n/locale/hu.json');
|
||||
case 'it':
|
||||
|
||||
Reference in New Issue
Block a user