feat(frontend): add option to hide all available items from discovery (#699)

pull/691/head
sct 4 years ago committed by GitHub
parent e1032ff5df
commit 6c1742e94c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -62,6 +62,9 @@ components:
applicationUrl: applicationUrl:
type: string type: string
example: https://os.example.com example: https://os.example.com
hideAvailable:
type: boolean
example: false
defaultPermissions: defaultPermissions:
type: number type: number
example: 32 example: 32

@ -9,4 +9,5 @@ export interface PublicSettingsResponse {
initialized: boolean; initialized: boolean;
movie4kEnabled: boolean; movie4kEnabled: boolean;
series4kEnabled: boolean; series4kEnabled: boolean;
hideAvailable: boolean;
} }

@ -49,6 +49,7 @@ export interface MainSettings {
apiKey: string; apiKey: string;
applicationUrl: string; applicationUrl: string;
defaultPermissions: number; defaultPermissions: number;
hideAvailable: boolean;
} }
interface PublicSettings { interface PublicSettings {
@ -58,6 +59,7 @@ interface PublicSettings {
interface FullPublicSettings extends PublicSettings { interface FullPublicSettings extends PublicSettings {
movie4kEnabled: boolean; movie4kEnabled: boolean;
series4kEnabled: boolean; series4kEnabled: boolean;
hideAvailable: boolean;
} }
export interface NotificationAgentConfig { export interface NotificationAgentConfig {
@ -150,6 +152,7 @@ class Settings {
main: { main: {
apiKey: '', apiKey: '',
applicationUrl: '', applicationUrl: '',
hideAvailable: false,
defaultPermissions: Permission.REQUEST, defaultPermissions: Permission.REQUEST,
}, },
plex: { plex: {
@ -281,6 +284,7 @@ class Settings {
series4kEnabled: this.data.sonarr.some( series4kEnabled: this.data.sonarr.some(
(sonarr) => sonarr.is4k && sonarr.isDefault (sonarr) => sonarr.is4k && sonarr.isDefault
), ),
hideAvailable: this.data.main.hideAvailable,
}; };
} }

@ -5,6 +5,8 @@ import ListView from '../Common/ListView';
import { LanguageContext } from '../../context/LanguageContext'; import { LanguageContext } from '../../context/LanguageContext';
import { defineMessages, FormattedMessage } from 'react-intl'; import { defineMessages, FormattedMessage } from 'react-intl';
import Header from '../Common/Header'; import Header from '../Common/Header';
import useSettings from '../../hooks/useSettings';
import { MediaStatus } from '../../../server/constants/media';
const messages = defineMessages({ const messages = defineMessages({
discovermovies: 'Popular Movies', discovermovies: 'Popular Movies',
@ -18,6 +20,7 @@ interface SearchResult {
} }
const DiscoverMovies: React.FC = () => { const DiscoverMovies: React.FC = () => {
const settings = useSettings();
const { locale } = useContext(LanguageContext); const { locale } = useContext(LanguageContext);
const { data, error, size, setSize } = useSWRInfinite<SearchResult>( const { data, error, size, setSize } = useSWRInfinite<SearchResult>(
(pageIndex: number, previousPageData: SearchResult | null) => { (pageIndex: number, previousPageData: SearchResult | null) => {
@ -45,11 +48,20 @@ const DiscoverMovies: React.FC = () => {
return <div>{error}</div>; return <div>{error}</div>;
} }
const titles = data?.reduce( let titles = (data ?? []).reduce(
(a, v) => [...a, ...v.results], (a, v) => [...a, ...v.results],
[] as MovieResult[] [] as MovieResult[]
); );
if (settings.currentSettings.hideAvailable) {
titles = titles.filter(
(i) =>
(i.mediaType === 'movie' || i.mediaType === 'tv') &&
i.mediaInfo?.status !== MediaStatus.AVAILABLE &&
i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE
);
}
const isEmpty = !isLoadingInitialData && titles?.length === 0; const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd = const isReachingEnd =
isEmpty || (data && data[data.length - 1]?.results.length < 20); isEmpty || (data && data[data.length - 1]?.results.length < 20);

@ -5,6 +5,8 @@ import ListView from '../Common/ListView';
import { defineMessages, FormattedMessage } from 'react-intl'; import { defineMessages, FormattedMessage } from 'react-intl';
import { LanguageContext } from '../../context/LanguageContext'; import { LanguageContext } from '../../context/LanguageContext';
import Header from '../Common/Header'; import Header from '../Common/Header';
import useSettings from '../../hooks/useSettings';
import { MediaStatus } from '../../../server/constants/media';
const messages = defineMessages({ const messages = defineMessages({
discovertv: 'Popular Series', discovertv: 'Popular Series',
@ -18,6 +20,7 @@ interface SearchResult {
} }
const DiscoverTv: React.FC = () => { const DiscoverTv: React.FC = () => {
const settings = useSettings();
const { locale } = useContext(LanguageContext); const { locale } = useContext(LanguageContext);
const { data, error, size, setSize } = useSWRInfinite<SearchResult>( const { data, error, size, setSize } = useSWRInfinite<SearchResult>(
(pageIndex: number, previousPageData: SearchResult | null) => { (pageIndex: number, previousPageData: SearchResult | null) => {
@ -45,7 +48,18 @@ const DiscoverTv: React.FC = () => {
return <div>{error}</div>; return <div>{error}</div>;
} }
const titles = data?.reduce((a, v) => [...a, ...v.results], [] as TvResult[]); let titles = (data ?? []).reduce(
(a, v) => [...a, ...v.results],
[] as TvResult[]
);
if (settings.currentSettings.hideAvailable) {
titles = titles.filter(
(i) =>
i.mediaInfo?.status !== MediaStatus.AVAILABLE &&
i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE
);
}
const isEmpty = !isLoadingInitialData && titles?.length === 0; const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd = const isReachingEnd =

@ -9,6 +9,8 @@ import ListView from '../Common/ListView';
import { LanguageContext } from '../../context/LanguageContext'; import { LanguageContext } from '../../context/LanguageContext';
import { defineMessages, FormattedMessage } from 'react-intl'; import { defineMessages, FormattedMessage } from 'react-intl';
import Header from '../Common/Header'; import Header from '../Common/Header';
import useSettings from '../../hooks/useSettings';
import { MediaStatus } from '../../../server/constants/media';
const messages = defineMessages({ const messages = defineMessages({
trending: 'Trending', trending: 'Trending',
@ -22,6 +24,7 @@ interface SearchResult {
} }
const Trending: React.FC = () => { const Trending: React.FC = () => {
const settings = useSettings();
const { locale } = useContext(LanguageContext); const { locale } = useContext(LanguageContext);
const { data, error, size, setSize } = useSWRInfinite<SearchResult>( const { data, error, size, setSize } = useSWRInfinite<SearchResult>(
(pageIndex: number, previousPageData: SearchResult | null) => { (pageIndex: number, previousPageData: SearchResult | null) => {
@ -51,11 +54,20 @@ const Trending: React.FC = () => {
return <div>{error}</div>; return <div>{error}</div>;
} }
const titles = data?.reduce( let titles = (data ?? []).reduce(
(a, v) => [...a, ...v.results], (a, v) => [...a, ...v.results],
[] as (MovieResult | TvResult | PersonResult)[] [] as (MovieResult | TvResult | PersonResult)[]
); );
if (settings.currentSettings.hideAvailable) {
titles = titles.filter(
(i) =>
(i.mediaType === 'movie' || i.mediaType === 'tv') &&
i.mediaInfo?.status !== MediaStatus.AVAILABLE &&
i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE
);
}
const isEmpty = !isLoadingInitialData && titles?.length === 0; const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd = const isReachingEnd =
isEmpty || (data && data[data.length - 1]?.results.length < 20); isEmpty || (data && data[data.length - 1]?.results.length < 20);

@ -5,6 +5,8 @@ import ListView from '../Common/ListView';
import { LanguageContext } from '../../context/LanguageContext'; import { LanguageContext } from '../../context/LanguageContext';
import { defineMessages, FormattedMessage } from 'react-intl'; import { defineMessages, FormattedMessage } from 'react-intl';
import Header from '../Common/Header'; import Header from '../Common/Header';
import useSettings from '../../hooks/useSettings';
import { MediaStatus } from '../../../server/constants/media';
const messages = defineMessages({ const messages = defineMessages({
upcomingmovies: 'Upcoming Movies', upcomingmovies: 'Upcoming Movies',
@ -18,6 +20,7 @@ interface SearchResult {
} }
const UpcomingMovies: React.FC = () => { const UpcomingMovies: React.FC = () => {
const settings = useSettings();
const { locale } = useContext(LanguageContext); const { locale } = useContext(LanguageContext);
const { data, error, size, setSize } = useSWRInfinite<SearchResult>( const { data, error, size, setSize } = useSWRInfinite<SearchResult>(
(pageIndex: number, previousPageData: SearchResult | null) => { (pageIndex: number, previousPageData: SearchResult | null) => {
@ -47,11 +50,19 @@ const UpcomingMovies: React.FC = () => {
return <div>{error}</div>; return <div>{error}</div>;
} }
const titles = data?.reduce( let titles = (data ?? []).reduce(
(a, v) => [...a, ...v.results], (a, v) => [...a, ...v.results],
[] as MovieResult[] [] as MovieResult[]
); );
if (settings.currentSettings.hideAvailable) {
titles = titles.filter(
(i) =>
i.mediaInfo?.status !== MediaStatus.AVAILABLE &&
i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE
);
}
const isEmpty = !isLoadingInitialData && titles?.length === 0; const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd = const isReachingEnd =
isEmpty || (data && data[data.length - 1]?.results.length < 20); isEmpty || (data && data[data.length - 1]?.results.length < 20);

@ -1,12 +1,14 @@
import Link from 'next/link'; import Link from 'next/link';
import React, { useContext } from 'react'; import React, { useContext, useEffect } from 'react';
import { useSWRInfinite } from 'swr'; import { useSWRInfinite } from 'swr';
import { MediaStatus } from '../../../server/constants/media';
import type { import type {
MovieResult, MovieResult,
PersonResult, PersonResult,
TvResult, TvResult,
} from '../../../server/models/Search'; } from '../../../server/models/Search';
import { LanguageContext } from '../../context/LanguageContext'; import { LanguageContext } from '../../context/LanguageContext';
import useSettings from '../../hooks/useSettings';
import PersonCard from '../PersonCard'; import PersonCard from '../PersonCard';
import Slider from '../Slider'; import Slider from '../Slider';
import TitleCard from '../TitleCard'; import TitleCard from '../TitleCard';
@ -34,8 +36,9 @@ const MediaSlider: React.FC<MediaSliderProps> = ({
sliderKey, sliderKey,
hideWhenEmpty = false, hideWhenEmpty = false,
}) => { }) => {
const settings = useSettings();
const { locale } = useContext(LanguageContext); const { locale } = useContext(LanguageContext);
const { data, error } = useSWRInfinite<MixedResult>( const { data, error, setSize, size } = useSWRInfinite<MixedResult>(
(pageIndex: number, previousPageData: MixedResult | null) => { (pageIndex: number, previousPageData: MixedResult | null) => {
if (previousPageData && pageIndex + 1 > previousPageData.totalPages) { if (previousPageData && pageIndex + 1 > previousPageData.totalPages) {
return null; return null;
@ -48,15 +51,34 @@ const MediaSlider: React.FC<MediaSliderProps> = ({
} }
); );
if (hideWhenEmpty && (data?.[0].results ?? []).length === 0) { let titles = (data ?? []).reduce(
return null;
}
const titles = (data ?? []).reduce(
(a, v) => [...a, ...v.results], (a, v) => [...a, ...v.results],
[] as (MovieResult | TvResult | PersonResult)[] [] as (MovieResult | TvResult | PersonResult)[]
); );
if (settings.currentSettings.hideAvailable) {
titles = titles.filter(
(i) =>
(i.mediaType === 'movie' || i.mediaType === 'tv') &&
i.mediaInfo?.status !== MediaStatus.AVAILABLE &&
i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE
);
}
useEffect(() => {
if (
titles.length < 24 &&
size < 5 &&
(data?.[0]?.totalResults ?? 0) > size * 20
) {
setSize(size + 1);
}
}, [titles, setSize, size, data]);
if (hideWhenEmpty && (data?.[0].results ?? []).length === 0) {
return null;
}
const finalTitles = titles.slice(0, 20).map((title) => { const finalTitles = titles.slice(0, 20).map((title) => {
switch (title.mediaType) { switch (title.mediaType) {
case 'movie': case 'movie':

@ -7,6 +7,8 @@ import Header from '../Common/Header';
import type { MovieDetails } from '../../../server/models/Movie'; import type { MovieDetails } from '../../../server/models/Movie';
import { LanguageContext } from '../../context/LanguageContext'; import { LanguageContext } from '../../context/LanguageContext';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import useSettings from '../../hooks/useSettings';
import { MediaStatus } from '../../../server/constants/media';
const messages = defineMessages({ const messages = defineMessages({
recommendations: 'Recommendations', recommendations: 'Recommendations',
@ -21,6 +23,7 @@ interface SearchResult {
} }
const MovieRecommendations: React.FC = () => { const MovieRecommendations: React.FC = () => {
const settings = useSettings();
const intl = useIntl(); const intl = useIntl();
const router = useRouter(); const router = useRouter();
const { locale } = useContext(LanguageContext); const { locale } = useContext(LanguageContext);
@ -55,11 +58,19 @@ const MovieRecommendations: React.FC = () => {
return <div>{error}</div>; return <div>{error}</div>;
} }
const titles = data?.reduce( let titles = (data ?? []).reduce(
(a, v) => [...a, ...v.results], (a, v) => [...a, ...v.results],
[] as MovieResult[] [] as MovieResult[]
); );
if (settings.currentSettings.hideAvailable) {
titles = titles.filter(
(i) =>
i.mediaInfo?.status !== MediaStatus.AVAILABLE &&
i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE
);
}
const isEmpty = !isLoadingInitialData && titles?.length === 0; const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd = const isReachingEnd =
isEmpty || (data && data[data.length - 1]?.results.length < 20); isEmpty || (data && data[data.length - 1]?.results.length < 20);

@ -7,6 +7,8 @@ import Header from '../Common/Header';
import { LanguageContext } from '../../context/LanguageContext'; import { LanguageContext } from '../../context/LanguageContext';
import type { MovieDetails } from '../../../server/models/Movie'; import type { MovieDetails } from '../../../server/models/Movie';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { MediaStatus } from '../../../server/constants/media';
import useSettings from '../../hooks/useSettings';
const messages = defineMessages({ const messages = defineMessages({
similar: 'Similar Titles', similar: 'Similar Titles',
@ -21,6 +23,7 @@ interface SearchResult {
} }
const MovieSimilar: React.FC = () => { const MovieSimilar: React.FC = () => {
const settings = useSettings();
const router = useRouter(); const router = useRouter();
const intl = useIntl(); const intl = useIntl();
const { locale } = useContext(LanguageContext); const { locale } = useContext(LanguageContext);
@ -55,11 +58,19 @@ const MovieSimilar: React.FC = () => {
return <div>{error}</div>; return <div>{error}</div>;
} }
const titles = data?.reduce( let titles = (data ?? []).reduce(
(a, v) => [...a, ...v.results], (a, v) => [...a, ...v.results],
[] as MovieResult[] [] as MovieResult[]
); );
if (settings.currentSettings.hideAvailable) {
titles = titles.filter(
(i) =>
i.mediaInfo?.status !== MediaStatus.AVAILABLE &&
i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE
);
}
const isEmpty = !isLoadingInitialData && titles?.length === 0; const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd = const isReachingEnd =
isEmpty || (data && data[data.length - 1]?.results.length < 20); isEmpty || (data && data[data.length - 1]?.results.length < 20);

@ -1,5 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import React, { useContext, useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { import {
MediaRequestStatus, MediaRequestStatus,
@ -7,7 +7,7 @@ import {
} from '../../../server/constants/media'; } from '../../../server/constants/media';
import Media from '../../../server/entity/Media'; import Media from '../../../server/entity/Media';
import { MediaRequest } from '../../../server/entity/MediaRequest'; import { MediaRequest } from '../../../server/entity/MediaRequest';
import { SettingsContext } from '../../context/SettingsContext'; import useSettings from '../../hooks/useSettings';
import { Permission, useUser } from '../../hooks/useUser'; import { Permission, useUser } from '../../hooks/useUser';
import ButtonWithDropdown from '../Common/ButtonWithDropdown'; import ButtonWithDropdown from '../Common/ButtonWithDropdown';
import RequestModal from '../RequestModal'; import RequestModal from '../RequestModal';
@ -58,7 +58,7 @@ const RequestButton: React.FC<RequestButtonProps> = ({
is4kShowComplete = false, is4kShowComplete = false,
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const settings = useContext(SettingsContext); const settings = useSettings();
const { hasPermission } = useUser(); const { hasPermission } = useUser();
const [showRequestModal, setShowRequestModal] = useState(false); const [showRequestModal, setShowRequestModal] = useState(false);
const [showRequest4kModal, setShowRequest4kModal] = useState(false); const [showRequest4kModal, setShowRequest4kModal] = useState(false);

@ -11,6 +11,8 @@ import { useUser, Permission } from '../../hooks/useUser';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import { messages as permissionMessages } from '../UserEdit'; import { messages as permissionMessages } from '../UserEdit';
import PermissionOption, { PermissionItem } from '../PermissionOption'; import PermissionOption, { PermissionItem } from '../PermissionOption';
import Badge from '../Common/Badge';
import globalMessages from '../../i18n/globalMessages';
const messages = defineMessages({ const messages = defineMessages({
generalsettings: 'General Settings', generalsettings: 'General Settings',
@ -25,6 +27,7 @@ const messages = defineMessages({
toastSettingsSuccess: 'Settings saved.', toastSettingsSuccess: 'Settings saved.',
toastSettingsFailure: 'Something went wrong saving settings.', toastSettingsFailure: 'Something went wrong saving settings.',
defaultPermissions: 'Default User Permissions', defaultPermissions: 'Default User Permissions',
hideAvailable: 'Hide available media',
}); });
const SettingsMain: React.FC = () => { const SettingsMain: React.FC = () => {
@ -166,6 +169,7 @@ const SettingsMain: React.FC = () => {
initialValues={{ initialValues={{
applicationUrl: data?.applicationUrl, applicationUrl: data?.applicationUrl,
defaultPermissions: data?.defaultPermissions ?? 0, defaultPermissions: data?.defaultPermissions ?? 0,
hideAvailable: data?.hideAvailable,
}} }}
enableReinitialize enableReinitialize
onSubmit={async (values) => { onSubmit={async (values) => {
@ -173,6 +177,7 @@ const SettingsMain: React.FC = () => {
await axios.post('/api/v1/settings/main', { await axios.post('/api/v1/settings/main', {
applicationUrl: values.applicationUrl, applicationUrl: values.applicationUrl,
defaultPermissions: values.defaultPermissions, defaultPermissions: values.defaultPermissions,
hideAvailable: values.hideAvailable,
}); });
addToast(intl.formatMessage(messages.toastSettingsSuccess), { addToast(intl.formatMessage(messages.toastSettingsSuccess), {
@ -256,6 +261,30 @@ const SettingsMain: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
<span className="mr-2">
{intl.formatMessage(messages.hideAvailable)}
</span>
<Badge badgeType="warning">
{intl.formatMessage(globalMessages.experimental)}
</Badge>
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Field
type="checkbox"
id="hideAvailable"
name="hideAvailable"
onChange={() => {
setFieldValue('hideAvailable', !values.hideAvailable);
}}
className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
/>
</div>
</div>
<div className="mt-6"> <div className="mt-6">
<div role="group" aria-labelledby="label-permissions"> <div role="group" aria-labelledby="label-permissions">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline"> <div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline">

@ -7,6 +7,8 @@ import { LanguageContext } from '../../context/LanguageContext';
import Header from '../Common/Header'; import Header from '../Common/Header';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { TvDetails } from '../../../server/models/Tv'; import { TvDetails } from '../../../server/models/Tv';
import { MediaStatus } from '../../../server/constants/media';
import useSettings from '../../hooks/useSettings';
const messages = defineMessages({ const messages = defineMessages({
recommendations: 'Recommendations', recommendations: 'Recommendations',
@ -21,6 +23,7 @@ interface SearchResult {
} }
const TvRecommendations: React.FC = () => { const TvRecommendations: React.FC = () => {
const settings = useSettings();
const router = useRouter(); const router = useRouter();
const intl = useIntl(); const intl = useIntl();
const { locale } = useContext(LanguageContext); const { locale } = useContext(LanguageContext);
@ -55,7 +58,18 @@ const TvRecommendations: React.FC = () => {
return <div>{error}</div>; return <div>{error}</div>;
} }
const titles = data?.reduce((a, v) => [...a, ...v.results], [] as TvResult[]); let titles = (data ?? []).reduce(
(a, v) => [...a, ...v.results],
[] as TvResult[]
);
if (settings.currentSettings.hideAvailable) {
titles = titles.filter(
(i) =>
i.mediaInfo?.status !== MediaStatus.AVAILABLE &&
i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE
);
}
const isEmpty = !isLoadingInitialData && titles?.length === 0; const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd = const isReachingEnd =

@ -7,6 +7,8 @@ import { LanguageContext } from '../../context/LanguageContext';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import type { TvDetails } from '../../../server/models/Tv'; import type { TvDetails } from '../../../server/models/Tv';
import Header from '../Common/Header'; import Header from '../Common/Header';
import { MediaStatus } from '../../../server/constants/media';
import useSettings from '../../hooks/useSettings';
const messages = defineMessages({ const messages = defineMessages({
similar: 'Similar Series', similar: 'Similar Series',
@ -21,6 +23,7 @@ interface SearchResult {
} }
const TvSimilar: React.FC = () => { const TvSimilar: React.FC = () => {
const settings = useSettings();
const router = useRouter(); const router = useRouter();
const intl = useIntl(); const intl = useIntl();
const { locale } = useContext(LanguageContext); const { locale } = useContext(LanguageContext);
@ -55,11 +58,19 @@ const TvSimilar: React.FC = () => {
return <div>{error}</div>; return <div>{error}</div>;
} }
const titles = data?.reduce( let titles = (data ?? []).reduce(
(a, v) => [...a, ...v.results], (a, v) => [...a, ...v.results],
[] as MovieResult[] [] as MovieResult[]
); );
if (settings.currentSettings.hideAvailable) {
titles = titles.filter(
(i) =>
i.mediaInfo?.status !== MediaStatus.AVAILABLE &&
i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE
);
}
const isEmpty = !isLoadingInitialData && titles?.length === 0; const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd = const isReachingEnd =
isEmpty || (data && data[data.length - 1]?.results.length < 20); isEmpty || (data && data[data.length - 1]?.results.length < 20);

@ -2,7 +2,7 @@ import React from 'react';
import { PublicSettingsResponse } from '../../server/interfaces/api/settingsInterfaces'; import { PublicSettingsResponse } from '../../server/interfaces/api/settingsInterfaces';
import useSWR from 'swr'; import useSWR from 'swr';
interface SettingsContextProps { export interface SettingsContextProps {
currentSettings: PublicSettingsResponse; currentSettings: PublicSettingsResponse;
} }
@ -10,6 +10,7 @@ const defaultSettings = {
initialized: false, initialized: false,
movie4kEnabled: false, movie4kEnabled: false,
series4kEnabled: false, series4kEnabled: false,
hideAvailable: false,
}; };
export const SettingsContext = React.createContext<SettingsContextProps>({ export const SettingsContext = React.createContext<SettingsContextProps>({

@ -0,0 +1,13 @@
import { useContext } from 'react';
import {
SettingsContext,
SettingsContextProps,
} from '../context/SettingsContext';
const useSettings = (): SettingsContextProps => {
const settings = useContext(SettingsContext);
return settings;
};
export default useSettings;

@ -21,6 +21,7 @@ const globalMessages = defineMessages({
deleting: 'Deleting…', deleting: 'Deleting…',
close: 'Close', close: 'Close',
edit: 'Edit', edit: 'Edit',
experimental: 'Experimental',
}); });
export default globalMessages; export default globalMessages;

@ -351,6 +351,7 @@
"components.Settings.edit": "Edit", "components.Settings.edit": "Edit",
"components.Settings.generalsettings": "General Settings", "components.Settings.generalsettings": "General Settings",
"components.Settings.generalsettingsDescription": "These are settings related to general Overseerr configuration.", "components.Settings.generalsettingsDescription": "These are settings related to general Overseerr configuration.",
"components.Settings.hideAvailable": "Hide available media",
"components.Settings.hostname": "Hostname/IP", "components.Settings.hostname": "Hostname/IP",
"components.Settings.jobname": "Job Name", "components.Settings.jobname": "Job Name",
"components.Settings.librariesRemaining": "Libraries Remaining: {count}", "components.Settings.librariesRemaining": "Libraries Remaining: {count}",
@ -517,6 +518,7 @@
"i18n.delete": "Delete", "i18n.delete": "Delete",
"i18n.deleting": "Deleting…", "i18n.deleting": "Deleting…",
"i18n.edit": "Edit", "i18n.edit": "Edit",
"i18n.experimental": "Experimental",
"i18n.failed": "Failed", "i18n.failed": "Failed",
"i18n.movies": "Movies", "i18n.movies": "Movies",
"i18n.partiallyavailable": "Partially Available", "i18n.partiallyavailable": "Partially Available",

@ -135,6 +135,7 @@ CoreApp.getInitialProps = async (initialProps) => {
let user = undefined; let user = undefined;
let currentSettings: PublicSettingsResponse = { let currentSettings: PublicSettingsResponse = {
initialized: false, initialized: false,
hideAvailable: false,
movie4kEnabled: false, movie4kEnabled: false,
series4kEnabled: false, series4kEnabled: false,
}; };

Loading…
Cancel
Save