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:
type: string
example: https://os.example.com
hideAvailable:
type: boolean
example: false
defaultPermissions:
type: number
example: 32

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

@ -49,6 +49,7 @@ export interface MainSettings {
apiKey: string;
applicationUrl: string;
defaultPermissions: number;
hideAvailable: boolean;
}
interface PublicSettings {
@ -58,6 +59,7 @@ interface PublicSettings {
interface FullPublicSettings extends PublicSettings {
movie4kEnabled: boolean;
series4kEnabled: boolean;
hideAvailable: boolean;
}
export interface NotificationAgentConfig {
@ -150,6 +152,7 @@ class Settings {
main: {
apiKey: '',
applicationUrl: '',
hideAvailable: false,
defaultPermissions: Permission.REQUEST,
},
plex: {
@ -281,6 +284,7 @@ class Settings {
series4kEnabled: this.data.sonarr.some(
(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 { defineMessages, FormattedMessage } from 'react-intl';
import Header from '../Common/Header';
import useSettings from '../../hooks/useSettings';
import { MediaStatus } from '../../../server/constants/media';
const messages = defineMessages({
discovermovies: 'Popular Movies',
@ -18,6 +20,7 @@ interface SearchResult {
}
const DiscoverMovies: React.FC = () => {
const settings = useSettings();
const { locale } = useContext(LanguageContext);
const { data, error, size, setSize } = useSWRInfinite<SearchResult>(
(pageIndex: number, previousPageData: SearchResult | null) => {
@ -45,11 +48,20 @@ const DiscoverMovies: React.FC = () => {
return <div>{error}</div>;
}
const titles = data?.reduce(
let titles = (data ?? []).reduce(
(a, v) => [...a, ...v.results],
[] 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 isReachingEnd =
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 { LanguageContext } from '../../context/LanguageContext';
import Header from '../Common/Header';
import useSettings from '../../hooks/useSettings';
import { MediaStatus } from '../../../server/constants/media';
const messages = defineMessages({
discovertv: 'Popular Series',
@ -18,6 +20,7 @@ interface SearchResult {
}
const DiscoverTv: React.FC = () => {
const settings = useSettings();
const { locale } = useContext(LanguageContext);
const { data, error, size, setSize } = useSWRInfinite<SearchResult>(
(pageIndex: number, previousPageData: SearchResult | null) => {
@ -45,7 +48,18 @@ const DiscoverTv: React.FC = () => {
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 isReachingEnd =

@ -9,6 +9,8 @@ import ListView from '../Common/ListView';
import { LanguageContext } from '../../context/LanguageContext';
import { defineMessages, FormattedMessage } from 'react-intl';
import Header from '../Common/Header';
import useSettings from '../../hooks/useSettings';
import { MediaStatus } from '../../../server/constants/media';
const messages = defineMessages({
trending: 'Trending',
@ -22,6 +24,7 @@ interface SearchResult {
}
const Trending: React.FC = () => {
const settings = useSettings();
const { locale } = useContext(LanguageContext);
const { data, error, size, setSize } = useSWRInfinite<SearchResult>(
(pageIndex: number, previousPageData: SearchResult | null) => {
@ -51,11 +54,20 @@ const Trending: React.FC = () => {
return <div>{error}</div>;
}
const titles = data?.reduce(
let titles = (data ?? []).reduce(
(a, v) => [...a, ...v.results],
[] 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 isReachingEnd =
isEmpty || (data && data[data.length - 1]?.results.length < 20);

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

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

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

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

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

@ -11,6 +11,8 @@ import { useUser, Permission } from '../../hooks/useUser';
import { useToasts } from 'react-toast-notifications';
import { messages as permissionMessages } from '../UserEdit';
import PermissionOption, { PermissionItem } from '../PermissionOption';
import Badge from '../Common/Badge';
import globalMessages from '../../i18n/globalMessages';
const messages = defineMessages({
generalsettings: 'General Settings',
@ -25,6 +27,7 @@ const messages = defineMessages({
toastSettingsSuccess: 'Settings saved.',
toastSettingsFailure: 'Something went wrong saving settings.',
defaultPermissions: 'Default User Permissions',
hideAvailable: 'Hide available media',
});
const SettingsMain: React.FC = () => {
@ -166,6 +169,7 @@ const SettingsMain: React.FC = () => {
initialValues={{
applicationUrl: data?.applicationUrl,
defaultPermissions: data?.defaultPermissions ?? 0,
hideAvailable: data?.hideAvailable,
}}
enableReinitialize
onSubmit={async (values) => {
@ -173,6 +177,7 @@ const SettingsMain: React.FC = () => {
await axios.post('/api/v1/settings/main', {
applicationUrl: values.applicationUrl,
defaultPermissions: values.defaultPermissions,
hideAvailable: values.hideAvailable,
});
addToast(intl.formatMessage(messages.toastSettingsSuccess), {
@ -256,6 +261,30 @@ const SettingsMain: React.FC = () => {
</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 role="group" aria-labelledby="label-permissions">
<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 { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { TvDetails } from '../../../server/models/Tv';
import { MediaStatus } from '../../../server/constants/media';
import useSettings from '../../hooks/useSettings';
const messages = defineMessages({
recommendations: 'Recommendations',
@ -21,6 +23,7 @@ interface SearchResult {
}
const TvRecommendations: React.FC = () => {
const settings = useSettings();
const router = useRouter();
const intl = useIntl();
const { locale } = useContext(LanguageContext);
@ -55,7 +58,18 @@ const TvRecommendations: React.FC = () => {
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 isReachingEnd =

@ -7,6 +7,8 @@ import { LanguageContext } from '../../context/LanguageContext';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import type { TvDetails } from '../../../server/models/Tv';
import Header from '../Common/Header';
import { MediaStatus } from '../../../server/constants/media';
import useSettings from '../../hooks/useSettings';
const messages = defineMessages({
similar: 'Similar Series',
@ -21,6 +23,7 @@ interface SearchResult {
}
const TvSimilar: React.FC = () => {
const settings = useSettings();
const router = useRouter();
const intl = useIntl();
const { locale } = useContext(LanguageContext);
@ -55,11 +58,19 @@ const TvSimilar: React.FC = () => {
return <div>{error}</div>;
}
const titles = data?.reduce(
let titles = (data ?? []).reduce(
(a, v) => [...a, ...v.results],
[] 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 isReachingEnd =
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 useSWR from 'swr';
interface SettingsContextProps {
export interface SettingsContextProps {
currentSettings: PublicSettingsResponse;
}
@ -10,6 +10,7 @@ const defaultSettings = {
initialized: false,
movie4kEnabled: false,
series4kEnabled: false,
hideAvailable: false,
};
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…',
close: 'Close',
edit: 'Edit',
experimental: 'Experimental',
});
export default globalMessages;

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

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

Loading…
Cancel
Save