@@ -28,11 +30,14 @@ const PullToRefresh: React.FC = () => {
instructionsReleaseToRefresh: ReactDOMServer.renderToString(
),
instructionsRefreshing: ReactDOMServer.renderToString(
),
distReload: 60,
+ distIgnore: 15,
+ shouldPullToRefresh: () =>
+ !window.scrollY && document.body.style.overflow !== 'hidden',
});
return () => {
PR.destroyAll();
};
- }, []);
+ }, [router]);
return
;
};
diff --git a/src/components/RequestCard/index.tsx b/src/components/RequestCard/index.tsx
index 9ccbcde02..27bb33834 100644
--- a/src/components/RequestCard/index.tsx
+++ b/src/components/RequestCard/index.tsx
@@ -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
diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx
index 6c232dc89..e5a00de75 100644
--- a/src/components/RequestList/RequestItem/index.tsx
+++ b/src/components/RequestList/RequestItem/index.tsx
@@ -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 (
@@ -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 (
{
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
diff --git a/src/components/Settings/SettingsJobsCache/index.tsx b/src/components/Settings/SettingsJobsCache/index.tsx
index 7317c8e82..f3402e2ed 100644
--- a/src/components/Settings/SettingsJobsCache/index.tsx
+++ b/src/components/Settings/SettingsJobsCache/index.tsx
@@ -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
{appDataPath}/cache/images.',
+ imagecachecount: 'Images Cached',
+ imagecachesize: 'Total Cache Size',
});
interface Job {
@@ -132,7 +141,8 @@ const SettingsJobs = () => {
} = useSWR
('/api/v1/settings/jobs', {
refreshInterval: 5000,
});
- const { data: cacheData, mutate: cacheRevalidate } = useSWR(
+ const { data: appData } = useSWR('/api/v1/status/appdata');
+ const { data: cacheData, mutate: cacheRevalidate } = useSWR(
'/api/v1/settings/cache',
{
refreshInterval: 10000,
@@ -435,7 +445,7 @@ const SettingsJobs = () => {
- {cacheData
+ {cacheData?.apiCaches
?.filter(
(cache) =>
!(
@@ -465,6 +475,41 @@ const SettingsJobs = () => {
+
+
{intl.formatMessage(messages.imagecache)}
+
+ {intl.formatMessage(messages.imagecacheDescription, {
+ code: (msg: React.ReactNode) => (
+ {msg}
+ ),
+ appDataPath: appData ? appData.appDataPath : '/app/config',
+ })}
+
+
+
+
+
+
+ {intl.formatMessage(messages.cachename)}
+
+ {intl.formatMessage(messages.imagecachecount)}
+
+ {intl.formatMessage(messages.imagecachesize)}
+
+
+
+
+ The Movie Database (tmdb)
+
+ {intl.formatNumber(cacheData?.imageCache.tmdb.imageCount ?? 0)}
+
+
+ {formatBytes(cacheData?.imageCache.tmdb.size ?? 0)}
+
+
+
+
+
>
);
};
diff --git a/src/components/Settings/SettingsMain.tsx b/src/components/Settings/SettingsMain.tsx
index ef0810f5e..7d4e188e5 100644
--- a/src/components/Settings/SettingsMain.tsx
+++ b/src/components/Settings/SettingsMain.tsx
@@ -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',
diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx
index 7c38a4238..1792027e9 100644
--- a/src/components/TvDetails/index.tsx
+++ b/src/components/TvDetails/index.tsx
@@ -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
;
@@ -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}
/>
diff --git a/src/context/LanguageContext.tsx b/src/context/LanguageContext.tsx
index 0cf4d7d79..115f4f4b6 100644
--- a/src/context/LanguageContext.tsx
+++ b/src/context/LanguageContext.tsx
@@ -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',
diff --git a/src/hooks/useDeepLinks.ts b/src/hooks/useDeepLinks.ts
new file mode 100644
index 000000000..983086591
--- /dev/null
+++ b/src/hooks/useDeepLinks.ts
@@ -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;
diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json
index b5acab96c..726d6ede5 100644
--- a/src/i18n/locale/en.json
+++ b/src/i18n/locale/en.json
@@ -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
{appDataPath}/cache/images.",
+ "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",
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index d546f0a8b..4738b8cf8 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -43,6 +43,8 @@ const loadLocaleData = (locale: AvailableLocale): Promise
=> {
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':