feat(ui): Add custom title functionality (#825)

This commit is contained in:
TheCatLady
2021-02-03 05:44:10 -05:00
committed by GitHub
parent 3ffd5ab0ee
commit 35c6bfc021
35 changed files with 162 additions and 42 deletions

View File

@@ -94,6 +94,9 @@ components:
type: string type: string
example: 'anapikey' example: 'anapikey'
readOnly: true readOnly: true
applicationTitle:
type: string
example: Overseerr
applicationUrl: applicationUrl:
type: string type: string
example: https://os.example.com example: https://os.example.com

View File

@@ -7,10 +7,11 @@ export interface SettingsAboutResponse {
export interface PublicSettingsResponse { export interface PublicSettingsResponse {
initialized: boolean; initialized: boolean;
movie4kEnabled: boolean; applicationTitle: string;
series4kEnabled: boolean;
hideAvailable: boolean; hideAvailable: boolean;
localLogin: boolean; localLogin: boolean;
movie4kEnabled: boolean;
series4kEnabled: boolean;
} }
export interface CacheItem { export interface CacheItem {

View File

@@ -203,7 +203,10 @@ class DiscordAgent
description: payload.message, description: payload.message,
color, color,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
author: { name: 'Overseerr', url: settings.main.applicationUrl }, author: {
name: settings.main.applicationTitle,
url: settings.main.applicationUrl,
},
fields: [ fields: [
...fields, ...fields,
// If we have extra data, map it to fields for discord notifications // If we have extra data, map it to fields for discord notifications
@@ -236,6 +239,7 @@ class DiscordAgent
): Promise<boolean> { ): Promise<boolean> {
logger.debug('Sending discord notification', { label: 'Notifications' }); logger.debug('Sending discord notification', { label: 'Notifications' });
try { try {
const settings = getSettings();
const webhookUrl = this.getSettings().options.webhookUrl; const webhookUrl = this.getSettings().options.webhookUrl;
if (!webhookUrl) { if (!webhookUrl) {
@@ -243,7 +247,7 @@ class DiscordAgent
} }
await axios.post(webhookUrl, { await axios.post(webhookUrl, {
username: 'Overseerr', username: settings.main.applicationTitle,
embeds: [this.buildEmbed(type, payload)], embeds: [this.buildEmbed(type, payload)],
} as DiscordWebhookPayload); } as DiscordWebhookPayload);

View File

@@ -58,7 +58,7 @@ class SlackAgent
payload: NotificationPayload payload: NotificationPayload
): SlackBlockEmbed { ): SlackBlockEmbed {
const settings = getSettings(); const settings = getSettings();
let header = 'Overseerr'; let header = settings.main.applicationTitle;
let actionUrl: string | undefined; let actionUrl: string | undefined;
const fields: EmbedField[] = []; const fields: EmbedField[] = [];

View File

@@ -50,6 +50,7 @@ export interface SonarrSettings extends DVRSettings {
export interface MainSettings { export interface MainSettings {
apiKey: string; apiKey: string;
applicationTitle: string;
applicationUrl: string; applicationUrl: string;
csrfProtection: boolean; csrfProtection: boolean;
defaultPermissions: number; defaultPermissions: number;
@@ -63,10 +64,11 @@ interface PublicSettings {
} }
interface FullPublicSettings extends PublicSettings { interface FullPublicSettings extends PublicSettings {
movie4kEnabled: boolean; applicationTitle: string;
series4kEnabled: boolean;
hideAvailable: boolean; hideAvailable: boolean;
localLogin: boolean; localLogin: boolean;
movie4kEnabled: boolean;
series4kEnabled: boolean;
} }
export interface NotificationAgentConfig { export interface NotificationAgentConfig {
@@ -160,6 +162,7 @@ class Settings {
clientId: uuidv4(), clientId: uuidv4(),
main: { main: {
apiKey: '', apiKey: '',
applicationTitle: 'Overseerr',
applicationUrl: '', applicationUrl: '',
csrfProtection: false, csrfProtection: false,
defaultPermissions: Permission.REQUEST, defaultPermissions: Permission.REQUEST,
@@ -292,14 +295,15 @@ class Settings {
get fullPublicSettings(): FullPublicSettings { get fullPublicSettings(): FullPublicSettings {
return { return {
...this.data.public, ...this.data.public,
applicationTitle: this.data.main.applicationTitle,
hideAvailable: this.data.main.hideAvailable,
localLogin: this.data.main.localLogin,
movie4kEnabled: this.data.radarr.some( movie4kEnabled: this.data.radarr.some(
(radarr) => radarr.is4k && radarr.isDefault (radarr) => radarr.is4k && radarr.isDefault
), ),
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,
localLogin: this.data.main.localLogin,
}; };
} }

View File

@@ -1,5 +1,4 @@
import axios from 'axios'; import axios from 'axios';
import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
@@ -18,6 +17,7 @@ import Modal from '../Common/Modal';
import Slider from '../Slider'; import Slider from '../Slider';
import TitleCard from '../TitleCard'; import TitleCard from '../TitleCard';
import Transition from '../Transition'; import Transition from '../Transition';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
overviewunavailable: 'Overview unavailable.', overviewunavailable: 'Overview unavailable.',
@@ -108,9 +108,7 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
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})`, 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})`,
}} }}
> >
<Head> <PageTitle title={data.name} />
<title>{data.name} - Overseerr</title>
</Head>
<Transition <Transition
enter="opacity-0 transition duration-300" enter="opacity-0 transition duration-300"
enterFrom="opacity-0" enterFrom="opacity-0"

View File

@@ -0,0 +1,22 @@
import React from 'react';
import useSettings from '../../../hooks/useSettings';
import Head from 'next/head';
interface PageTitleProps {
title: string | (string | undefined)[];
}
const PageTitle: React.FC<PageTitleProps> = ({ title }) => {
const settings = useSettings();
return (
<Head>
<title>
{Array.isArray(title) ? title.filter(Boolean).join(' - ') : title} -{' '}
{settings.currentSettings.applicationTitle}
</title>
</Head>
);
};
export default PageTitle;

View File

@@ -3,10 +3,11 @@ import { useSWRInfinite } from 'swr';
import type { MovieResult } from '../../../server/models/Search'; import type { MovieResult } from '../../../server/models/Search';
import ListView from '../Common/ListView'; import ListView from '../Common/ListView';
import { LanguageContext } from '../../context/LanguageContext'; import { LanguageContext } from '../../context/LanguageContext';
import { defineMessages, FormattedMessage } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import Header from '../Common/Header'; import Header from '../Common/Header';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
import { MediaStatus } from '../../../server/constants/media'; import { MediaStatus } from '../../../server/constants/media';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
discovermovies: 'Popular Movies', discovermovies: 'Popular Movies',
@@ -20,6 +21,7 @@ interface SearchResult {
} }
const DiscoverMovies: React.FC = () => { const DiscoverMovies: React.FC = () => {
const intl = useIntl();
const settings = useSettings(); 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>(
@@ -68,6 +70,7 @@ const DiscoverMovies: React.FC = () => {
return ( return (
<> <>
<PageTitle title={intl.formatMessage(messages.discovermovies)} />
<div className="mt-1 mb-5"> <div className="mt-1 mb-5">
<Header> <Header>
<FormattedMessage {...messages.discovermovies} /> <FormattedMessage {...messages.discovermovies} />

View File

@@ -2,11 +2,12 @@ import React, { useContext } from 'react';
import { useSWRInfinite } from 'swr'; import { useSWRInfinite } from 'swr';
import type { TvResult } from '../../../server/models/Search'; import type { TvResult } from '../../../server/models/Search';
import ListView from '../Common/ListView'; import ListView from '../Common/ListView';
import { defineMessages, FormattedMessage } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } 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 useSettings from '../../hooks/useSettings';
import { MediaStatus } from '../../../server/constants/media'; import { MediaStatus } from '../../../server/constants/media';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
discovertv: 'Popular Series', discovertv: 'Popular Series',
@@ -20,6 +21,7 @@ interface SearchResult {
} }
const DiscoverTv: React.FC = () => { const DiscoverTv: React.FC = () => {
const intl = useIntl();
const settings = useSettings(); 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>(
@@ -67,6 +69,7 @@ const DiscoverTv: React.FC = () => {
return ( return (
<> <>
<PageTitle title={intl.formatMessage(messages.discovertv)} />
<div className="mt-1 mb-5"> <div className="mt-1 mb-5">
<Header> <Header>
<FormattedMessage {...messages.discovertv} /> <FormattedMessage {...messages.discovertv} />

View File

@@ -7,10 +7,11 @@ import type {
} from '../../../server/models/Search'; } from '../../../server/models/Search';
import ListView from '../Common/ListView'; import ListView from '../Common/ListView';
import { LanguageContext } from '../../context/LanguageContext'; import { LanguageContext } from '../../context/LanguageContext';
import { defineMessages, FormattedMessage } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import Header from '../Common/Header'; import Header from '../Common/Header';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
import { MediaStatus } from '../../../server/constants/media'; import { MediaStatus } from '../../../server/constants/media';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
trending: 'Trending', trending: 'Trending',
@@ -24,6 +25,7 @@ interface SearchResult {
} }
const Trending: React.FC = () => { const Trending: React.FC = () => {
const intl = useIntl();
const settings = useSettings(); 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>(
@@ -74,6 +76,7 @@ const Trending: React.FC = () => {
return ( return (
<> <>
<PageTitle title={intl.formatMessage(messages.trending)} />
<div className="mt-1 mb-5"> <div className="mt-1 mb-5">
<Header> <Header>
<FormattedMessage {...messages.trending} /> <FormattedMessage {...messages.trending} />

View File

@@ -3,10 +3,11 @@ import { useSWRInfinite } from 'swr';
import type { MovieResult } from '../../../server/models/Search'; import type { MovieResult } from '../../../server/models/Search';
import ListView from '../Common/ListView'; import ListView from '../Common/ListView';
import { LanguageContext } from '../../context/LanguageContext'; import { LanguageContext } from '../../context/LanguageContext';
import { defineMessages, FormattedMessage } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import Header from '../Common/Header'; import Header from '../Common/Header';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
import { MediaStatus } from '../../../server/constants/media'; import { MediaStatus } from '../../../server/constants/media';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
upcomingmovies: 'Upcoming Movies', upcomingmovies: 'Upcoming Movies',
@@ -20,6 +21,7 @@ interface SearchResult {
} }
const UpcomingMovies: React.FC = () => { const UpcomingMovies: React.FC = () => {
const intl = useIntl();
const settings = useSettings(); 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>(
@@ -69,6 +71,7 @@ const UpcomingMovies: React.FC = () => {
return ( return (
<> <>
<PageTitle title={intl.formatMessage(messages.upcomingmovies)} />
<div className="mt-1 mb-5"> <div className="mt-1 mb-5">
<Header> <Header>
<FormattedMessage {...messages.upcomingmovies} /> <FormattedMessage {...messages.upcomingmovies} />

View File

@@ -8,8 +8,10 @@ import type { MediaResultsResponse } from '../../../server/interfaces/api/mediaI
import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces'; import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces';
import RequestCard from '../RequestCard'; import RequestCard from '../RequestCard';
import MediaSlider from '../MediaSlider'; import MediaSlider from '../MediaSlider';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
discover: 'Discover',
recentrequests: 'Recent Requests', recentrequests: 'Recent Requests',
popularmovies: 'Popular Movies', popularmovies: 'Popular Movies',
populartv: 'Popular Series', populartv: 'Popular Series',
@@ -35,6 +37,7 @@ const Discover: React.FC = () => {
return ( return (
<> <>
<PageTitle title={intl.formatMessage(messages.discover)} />
<div className="mt-6 mb-4 md:flex md:items-center md:justify-between"> <div className="mt-6 mb-4 md:flex md:items-center md:justify-between">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="inline-flex items-center text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate"> <div className="inline-flex items-center text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate">

View File

@@ -176,7 +176,7 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
<div className="flex-shrink-0 flex items-center px-4"> <div className="flex-shrink-0 flex items-center px-4">
<span className="text-xl text-gray-50"> <span className="text-xl text-gray-50">
<a href="/"> <a href="/">
<img src="/logo.png" alt="Overseerr Logo" /> <img src="/logo.png" alt="Logo" />
</a> </a>
</span> </span>
</div> </div>
@@ -238,7 +238,7 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
<div className="flex items-center flex-shrink-0 px-4"> <div className="flex items-center flex-shrink-0 px-4">
<span className="text-2xl text-gray-50"> <span className="text-2xl text-gray-50">
<a href="/"> <a href="/">
<img src="/logo.png" alt="Overseerr Logo" /> <img src="/logo.png" alt="Logo" />
</a> </a>
</span> </span>
</div> </div>

View File

@@ -10,8 +10,10 @@ import LanguagePicker from '../Layout/LanguagePicker';
import LocalLogin from './LocalLogin'; import LocalLogin from './LocalLogin';
import Accordion from '../Common/Accordion'; import Accordion from '../Common/Accordion';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
signin: 'Sign In',
signinheader: 'Sign in to continue', signinheader: 'Sign in to continue',
signinwithplex: 'Use your Plex account', signinwithplex: 'Use your Plex account',
signinwithoverseerr: 'Use your Overseerr account', signinwithoverseerr: 'Use your Overseerr account',
@@ -59,6 +61,7 @@ const Login: React.FC = () => {
return ( return (
<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)} />
<ImageFader <ImageFader
backgroundImages={[ backgroundImages={[
'/images/rotate1.jpg', '/images/rotate1.jpg',
@@ -73,11 +76,7 @@ const Login: React.FC = () => {
<LanguagePicker /> <LanguagePicker />
</div> </div>
<div className="relative z-40 px-4 sm:mx-auto sm:w-full sm:max-w-md"> <div className="relative z-40 px-4 sm:mx-auto sm:w-full sm:max-w-md">
<img <img src="/logo.png" className="w-auto mx-auto max-h-32" alt="Logo" />
src="/logo.png"
className="w-auto mx-auto max-h-32"
alt="Overseerr Logo"
/>
<h2 className="mt-2 text-3xl font-extrabold leading-9 text-center text-gray-100"> <h2 className="mt-2 text-3xl font-extrabold leading-9 text-center text-gray-100">
<FormattedMessage {...messages.signinheader} /> <FormattedMessage {...messages.signinheader} />
</h2> </h2>

View File

@@ -9,6 +9,7 @@ import Error from '../../../pages/_error';
import Header from '../../Common/Header'; import Header from '../../Common/Header';
import LoadingSpinner from '../../Common/LoadingSpinner'; import LoadingSpinner from '../../Common/LoadingSpinner';
import PersonCard from '../../PersonCard'; import PersonCard from '../../PersonCard';
import PageTitle from '../../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
fullcast: 'Full Cast', fullcast: 'Full Cast',
@@ -32,6 +33,7 @@ const MovieCast: React.FC = () => {
return ( return (
<> <>
<PageTitle title={[intl.formatMessage(messages.fullcast), data.title]} />
<div className="mt-1 mb-5"> <div className="mt-1 mb-5">
<Header <Header
subtext={ subtext={

View File

@@ -9,6 +9,7 @@ import Error from '../../../pages/_error';
import Header from '../../Common/Header'; import Header from '../../Common/Header';
import LoadingSpinner from '../../Common/LoadingSpinner'; import LoadingSpinner from '../../Common/LoadingSpinner';
import PersonCard from '../../PersonCard'; import PersonCard from '../../PersonCard';
import PageTitle from '../../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
fullcrew: 'Full Crew', fullcrew: 'Full Crew',
@@ -32,6 +33,7 @@ const MovieCrew: React.FC = () => {
return ( return (
<> <>
<PageTitle title={[intl.formatMessage(messages.fullcrew), data.title]} />
<div className="mt-1 mb-5"> <div className="mt-1 mb-5">
<Header <Header
subtext={ subtext={

View File

@@ -9,6 +9,7 @@ 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 useSettings from '../../hooks/useSettings';
import { MediaStatus } from '../../../server/constants/media'; import { MediaStatus } from '../../../server/constants/media';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
recommendations: 'Recommendations', recommendations: 'Recommendations',
@@ -77,6 +78,9 @@ const MovieRecommendations: React.FC = () => {
return ( return (
<> <>
<PageTitle
title={[intl.formatMessage(messages.recommendations), movieData?.title]}
/>
<div className="mt-1 mb-5"> <div className="mt-1 mb-5">
<Header <Header
subtext={ subtext={

View File

@@ -9,6 +9,7 @@ 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 { MediaStatus } from '../../../server/constants/media';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
similar: 'Similar Titles', similar: 'Similar Titles',
@@ -77,6 +78,9 @@ const MovieSimilar: React.FC = () => {
return ( return (
<> <>
<PageTitle
title={[intl.formatMessage(messages.similar), movieData?.title]}
/>
<div className="mt-1 mb-5"> <div className="mt-1 mb-5">
<Header <Header
subtext={ subtext={

View File

@@ -27,7 +27,6 @@ import RTAudFresh from '../../assets/rt_aud_fresh.svg';
import RTAudRotten from '../../assets/rt_aud_rotten.svg'; import RTAudRotten from '../../assets/rt_aud_rotten.svg';
import type { RTRating } from '../../../server/api/rottentomatoes'; import type { RTRating } from '../../../server/api/rottentomatoes';
import Error from '../../pages/_error'; import Error from '../../pages/_error';
import Head from 'next/head';
import ExternalLinkBlock from '../ExternalLinkBlock'; import ExternalLinkBlock from '../ExternalLinkBlock';
import { sortCrewPriority } from '../../utils/creditHelpers'; import { sortCrewPriority } from '../../utils/creditHelpers';
import StatusBadge from '../StatusBadge'; import StatusBadge from '../StatusBadge';
@@ -36,6 +35,7 @@ import MediaSlider from '../MediaSlider';
import ConfirmButton from '../Common/ConfirmButton'; import ConfirmButton from '../Common/ConfirmButton';
import DownloadBlock from '../DownloadBlock'; import DownloadBlock from '../DownloadBlock';
import ButtonWithDropdown from '../Common/ButtonWithDropdown'; import ButtonWithDropdown from '../Common/ButtonWithDropdown';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
releasedate: 'Release Date', releasedate: 'Release Date',
@@ -137,10 +137,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
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})`, 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})`,
}} }}
> >
<Head> <PageTitle title={data.title} />
<title>{data.title} - Overseerr</title>
</Head>
<SlideOver <SlideOver
show={showManager} show={showManager}
title={intl.formatMessage(messages.manageModalTitle)} title={intl.formatMessage(messages.manageModalTitle)}
@@ -181,7 +178,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
<div className="mb-6"> <div className="mb-6">
{data?.mediaInfo && {data?.mediaInfo &&
data?.mediaInfo.status !== MediaStatus.AVAILABLE && ( data?.mediaInfo.status !== MediaStatus.AVAILABLE && (
<div className="flex flex-col sm:flex-row flex-nowrap mb-2"> <div className="flex flex-col mb-2 sm:flex-row flex-nowrap">
<Button <Button
onClick={() => markAvailable()} onClick={() => markAvailable()}
className="w-full sm:mb-0" className="w-full sm:mb-0"
@@ -205,7 +202,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
)} )}
{data?.mediaInfo && {data?.mediaInfo &&
data?.mediaInfo.status4k !== MediaStatus.AVAILABLE && ( data?.mediaInfo.status4k !== MediaStatus.AVAILABLE && (
<div className="flex flex-col sm:flex-row flex-nowrap mb-2"> <div className="flex flex-col mb-2 sm:flex-row flex-nowrap">
<Button <Button
onClick={() => markAvailable(true)} onClick={() => markAvailable(true)}
className="w-full sm:mb-0" className="w-full sm:mb-0"

View File

@@ -12,6 +12,7 @@ import { LanguageContext } from '../../context/LanguageContext';
import ImageFader from '../Common/ImageFader'; 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';
const messages = defineMessages({ const messages = defineMessages({
appearsin: 'Appears in', appearsin: 'Appears in',
@@ -172,6 +173,7 @@ const PersonDetails: React.FC = () => {
return ( return (
<> <>
<PageTitle title={data.name} />
{(sortedCrew || sortedCast) && ( {(sortedCrew || sortedCast) && (
<div className="absolute top-0 left-0 right-0 z-0 h-96"> <div className="absolute top-0 left-0 right-0 z-0 h-96">
<ImageFader <ImageFader

View File

@@ -7,6 +7,7 @@ import Header from '../Common/Header';
import Table from '../Common/Table'; import Table from '../Common/Table';
import Button from '../Common/Button'; import Button from '../Common/Button';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
requests: 'Requests', requests: 'Requests',
@@ -54,6 +55,7 @@ const RequestList: React.FC = () => {
return ( return (
<> <>
<PageTitle title={intl.formatMessage(messages.requests)} />
<div className="flex flex-col justify-between md:items-end md:flex-row"> <div className="flex flex-col justify-between md:items-end md:flex-row">
<Header>{intl.formatMessage(messages.requests)}</Header> <Header>{intl.formatMessage(messages.requests)}</Header>
<div className="flex flex-col mt-2 md:flex-row"> <div className="flex flex-col mt-2 md:flex-row">

View File

@@ -10,8 +10,10 @@ import ListView from '../Common/ListView';
import { LanguageContext } from '../../context/LanguageContext'; import { LanguageContext } from '../../context/LanguageContext';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import Header from '../Common/Header'; import Header from '../Common/Header';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
search: 'Search',
searchresults: 'Search Results', searchresults: 'Search Results',
}); });
@@ -65,6 +67,7 @@ const Search: React.FC = () => {
return ( return (
<> <>
<PageTitle title={intl.formatMessage(messages.search)} />
<div className="mt-1 mb-5"> <div className="mt-1 mb-5">
<Header>{intl.formatMessage(messages.searchresults)}</Header> <Header>{intl.formatMessage(messages.searchresults)}</Header>
</div> </div>

View File

@@ -2,8 +2,10 @@ import React from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
settings: 'Settings',
menuGeneralSettings: 'General Settings', menuGeneralSettings: 'General Settings',
menuPlexSettings: 'Plex', menuPlexSettings: 'Plex',
menuServices: 'Services', menuServices: 'Services',
@@ -91,6 +93,7 @@ const SettingsLayout: React.FC = ({ children }) => {
}; };
return ( return (
<> <>
<PageTitle title={intl.formatMessage(messages.settings)} />
<div className="mt-6"> <div className="mt-6">
<div className="sm:hidden"> <div className="sm:hidden">
<select <select

View File

@@ -12,6 +12,7 @@ import { useToasts } from 'react-toast-notifications';
import Badge from '../Common/Badge'; import Badge from '../Common/Badge';
import globalMessages from '../../i18n/globalMessages'; import globalMessages from '../../i18n/globalMessages';
import PermissionEdit from '../PermissionEdit'; import PermissionEdit from '../PermissionEdit';
import * as Yup from 'yup';
const messages = defineMessages({ const messages = defineMessages({
generalsettings: 'General Settings', generalsettings: 'General Settings',
@@ -20,6 +21,7 @@ const messages = defineMessages({
save: 'Save Changes', save: 'Save Changes',
saving: 'Saving…', saving: 'Saving…',
apikey: 'API Key', apikey: 'API Key',
applicationTitle: 'Application Title',
applicationurl: 'Application URL', applicationurl: 'Application URL',
toastApiKeySuccess: 'New API key generated!', toastApiKeySuccess: 'New API key generated!',
toastApiKeyFailure: 'Something went wrong while generating a new API key.', toastApiKeyFailure: 'Something went wrong while generating a new API key.',
@@ -38,6 +40,7 @@ const messages = defineMessages({
localLogin: 'Enable Local User Sign-In', localLogin: 'Enable Local User Sign-In',
localLoginTip: localLoginTip:
'Disabling this option only prevents new sign-ins (no user data is deleted)', 'Disabling this option only prevents new sign-ins (no user data is deleted)',
validationApplicationTitle: 'You must provide an application title',
}); });
const SettingsMain: React.FC = () => { const SettingsMain: React.FC = () => {
@@ -47,6 +50,11 @@ const SettingsMain: React.FC = () => {
const { data, error, revalidate } = useSWR<MainSettings>( const { data, error, revalidate } = useSWR<MainSettings>(
'/api/v1/settings/main' '/api/v1/settings/main'
); );
const MainSettingsSchema = Yup.object().shape({
applicationTitle: Yup.string().required(
intl.formatMessage(messages.validationApplicationTitle)
),
});
const regenerate = async () => { const regenerate = async () => {
try { try {
@@ -82,6 +90,7 @@ const SettingsMain: React.FC = () => {
<div className="section"> <div className="section">
<Formik <Formik
initialValues={{ initialValues={{
applicationTitle: data?.applicationTitle,
applicationUrl: data?.applicationUrl, applicationUrl: data?.applicationUrl,
csrfProtection: data?.csrfProtection, csrfProtection: data?.csrfProtection,
defaultPermissions: data?.defaultPermissions ?? 0, defaultPermissions: data?.defaultPermissions ?? 0,
@@ -90,9 +99,11 @@ const SettingsMain: React.FC = () => {
trustProxy: data?.trustProxy, trustProxy: data?.trustProxy,
}} }}
enableReinitialize enableReinitialize
validationSchema={MainSettingsSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
await axios.post('/api/v1/settings/main', { await axios.post('/api/v1/settings/main', {
applicationTitle: values.applicationTitle,
applicationUrl: values.applicationUrl, applicationUrl: values.applicationUrl,
csrfProtection: values.csrfProtection, csrfProtection: values.csrfProtection,
defaultPermissions: values.defaultPermissions, defaultPermissions: values.defaultPermissions,
@@ -115,7 +126,7 @@ const SettingsMain: React.FC = () => {
} }
}} }}
> >
{({ isSubmitting, values, setFieldValue }) => { {({ errors, touched, isSubmitting, values, setFieldValue }) => {
return ( return (
<Form className="section"> <Form className="section">
{userHasPermission(Permission.ADMIN) && ( {userHasPermission(Permission.ADMIN) && (
@@ -160,6 +171,24 @@ const SettingsMain: React.FC = () => {
</div> </div>
</div> </div>
)} )}
<div className="form-row">
<label htmlFor="applicationTitle" className="text-label">
{intl.formatMessage(messages.applicationTitle)}
</label>
<div className="form-input">
<div className="flex max-w-lg rounded-md shadow-sm">
<Field
id="applicationTitle"
name="applicationTitle"
type="text"
placeholder="Overseerr"
/>
</div>
{errors.applicationTitle && touched.applicationTitle && (
<div className="error">{errors.applicationTitle}</div>
)}
</div>
</div>
<div className="form-row"> <div className="form-row">
<label htmlFor="applicationUrl" className="text-label"> <label htmlFor="applicationUrl" className="text-label">
{intl.formatMessage(messages.applicationurl)} {intl.formatMessage(messages.applicationurl)}

View File

@@ -10,8 +10,10 @@ import axios from 'axios';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import Badge from '../Common/Badge'; import Badge from '../Common/Badge';
import LanguagePicker from '../Layout/LanguagePicker'; import LanguagePicker from '../Layout/LanguagePicker';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
setup: 'Setup',
finish: 'Finish Setup', finish: 'Finish Setup',
finishing: 'Finishing…', finishing: 'Finishing…',
continue: 'Continue', continue: 'Continue',
@@ -44,6 +46,7 @@ const Setup: React.FC = () => {
return ( return (
<div className="relative flex flex-col justify-center min-h-screen py-12 bg-gray-900"> <div className="relative flex flex-col justify-center min-h-screen py-12 bg-gray-900">
<PageTitle title={intl.formatMessage(messages.setup)} />
<ImageFader <ImageFader
backgroundImages={[ backgroundImages={[
'/images/rotate1.jpg', '/images/rotate1.jpg',
@@ -61,11 +64,11 @@ const Setup: React.FC = () => {
<img <img
src="/logo.png" src="/logo.png"
className="w-auto mx-auto mb-10 max-h-32" className="w-auto mx-auto mb-10 max-h-32"
alt="Overseerr Logo" alt="Logo"
/> />
<nav className="relative z-50"> <nav className="relative z-50">
<ul <ul
className="bg-gray-800 bg-opacity-50 border border-gray-600 divide-y divide-gray-600 rounded-md md:flex md:divide-y-0" className="bg-gray-800 bg-opacity-50 border border-gray-600 divide-y divide-gray-600 rounded-md md:flex md:divide-y-0"
style={{ backdropFilter: 'blur(5px)' }} style={{ backdropFilter: 'blur(5px)' }}
> >
<SetupSteps <SetupSteps

View File

@@ -9,6 +9,7 @@ import Error from '../../../pages/_error';
import Header from '../../Common/Header'; import Header from '../../Common/Header';
import LoadingSpinner from '../../Common/LoadingSpinner'; import LoadingSpinner from '../../Common/LoadingSpinner';
import PersonCard from '../../PersonCard'; import PersonCard from '../../PersonCard';
import PageTitle from '../../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
fullseriescast: 'Full Series Cast', fullseriescast: 'Full Series Cast',
@@ -32,6 +33,9 @@ const TvCast: React.FC = () => {
return ( return (
<> <>
<PageTitle
title={[intl.formatMessage(messages.fullseriescast), data.name]}
/>
<div className="mt-1 mb-5"> <div className="mt-1 mb-5">
<Header <Header
subtext={ subtext={

View File

@@ -9,6 +9,7 @@ import Error from '../../../pages/_error';
import Header from '../../Common/Header'; import Header from '../../Common/Header';
import LoadingSpinner from '../../Common/LoadingSpinner'; import LoadingSpinner from '../../Common/LoadingSpinner';
import PersonCard from '../../PersonCard'; import PersonCard from '../../PersonCard';
import PageTitle from '../../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
fullseriescrew: 'Full Series Crew', fullseriescrew: 'Full Series Crew',
@@ -32,6 +33,9 @@ const TvCrew: React.FC = () => {
return ( return (
<> <>
<PageTitle
title={[intl.formatMessage(messages.fullseriescrew), data.name]}
/>
<div className="mt-1 mb-5"> <div className="mt-1 mb-5">
<Header <Header
subtext={ subtext={

View File

@@ -9,6 +9,7 @@ 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 { MediaStatus } from '../../../server/constants/media';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
recommendations: 'Recommendations', recommendations: 'Recommendations',
@@ -77,6 +78,9 @@ const TvRecommendations: React.FC = () => {
return ( return (
<> <>
<PageTitle
title={[intl.formatMessage(messages.recommendations), tvData?.name]}
/>
<div className="mt-1 mb-5"> <div className="mt-1 mb-5">
<Header <Header
subtext={ subtext={

View File

@@ -9,6 +9,7 @@ import type { TvDetails } from '../../../server/models/Tv';
import Header from '../Common/Header'; import Header from '../Common/Header';
import { MediaStatus } from '../../../server/constants/media'; import { MediaStatus } from '../../../server/constants/media';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
similar: 'Similar Series', similar: 'Similar Series',
@@ -77,6 +78,7 @@ const TvSimilar: React.FC = () => {
return ( return (
<> <>
<PageTitle title={[intl.formatMessage(messages.similar), tvData?.name]} />
<div className="mt-1 mb-5"> <div className="mt-1 mb-5">
<Header <Header
subtext={ subtext={

View File

@@ -27,7 +27,6 @@ import RTRotten from '../../assets/rt_rotten.svg';
import RTAudFresh from '../../assets/rt_aud_fresh.svg'; import RTAudFresh from '../../assets/rt_aud_fresh.svg';
import RTAudRotten from '../../assets/rt_aud_rotten.svg'; import RTAudRotten from '../../assets/rt_aud_rotten.svg';
import type { RTRating } from '../../../server/api/rottentomatoes'; import type { RTRating } from '../../../server/api/rottentomatoes';
import Head from 'next/head';
import { ANIME_KEYWORD_ID } from '../../../server/api/themoviedb/constants'; import { ANIME_KEYWORD_ID } from '../../../server/api/themoviedb/constants';
import ExternalLinkBlock from '../ExternalLinkBlock'; import ExternalLinkBlock from '../ExternalLinkBlock';
import { sortCrewPriority } from '../../utils/creditHelpers'; import { sortCrewPriority } from '../../utils/creditHelpers';
@@ -38,6 +37,7 @@ import MediaSlider from '../MediaSlider';
import ConfirmButton from '../Common/ConfirmButton'; import ConfirmButton from '../Common/ConfirmButton';
import DownloadBlock from '../DownloadBlock'; import DownloadBlock from '../DownloadBlock';
import ButtonWithDropdown from '../Common/ButtonWithDropdown'; import ButtonWithDropdown from '../Common/ButtonWithDropdown';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
firstAirDate: 'First Air Date', firstAirDate: 'First Air Date',
@@ -156,9 +156,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
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})`, 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})`,
}} }}
> >
<Head> <PageTitle title={data.name} />
<title>{data.name} - Overseerr</title>
</Head>
<RequestModal <RequestModal
tmdbId={data.id} tmdbId={data.id}
show={showRequestModal} show={showRequestModal}
@@ -209,7 +207,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
<div className="mb-6"> <div className="mb-6">
{data?.mediaInfo && {data?.mediaInfo &&
data?.mediaInfo.status !== MediaStatus.AVAILABLE && ( data?.mediaInfo.status !== MediaStatus.AVAILABLE && (
<div className="flex flex-col sm:flex-row flex-nowrap mb-2"> <div className="flex flex-col mb-2 sm:flex-row flex-nowrap">
<Button <Button
onClick={() => markAvailable()} onClick={() => markAvailable()}
className="w-full sm:mb-0" className="w-full sm:mb-0"
@@ -233,7 +231,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
)} )}
{data?.mediaInfo && {data?.mediaInfo &&
data?.mediaInfo.status4k !== MediaStatus.AVAILABLE && ( data?.mediaInfo.status4k !== MediaStatus.AVAILABLE && (
<div className="flex flex-col sm:flex-row flex-nowrap mb-2"> <div className="flex flex-col mb-2 sm:flex-row flex-nowrap">
<Button <Button
onClick={() => markAvailable(true)} onClick={() => markAvailable(true)}
className="w-full sm:mb-0" className="w-full sm:mb-0"

View File

@@ -11,6 +11,7 @@ import PermissionEdit from '../PermissionEdit';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { UserType } from '../../../server/constants/user'; import { UserType } from '../../../server/constants/user';
import PageTitle from '../Common/PageTitle';
export const messages = defineMessages({ export const messages = defineMessages({
edituser: 'Edit User', edituser: 'Edit User',
@@ -85,6 +86,7 @@ const UserEdit: React.FC = () => {
> >
{({ isSubmitting, handleSubmit }) => ( {({ isSubmitting, handleSubmit }) => (
<Form> <Form>
<PageTitle title={intl.formatMessage(messages.edituser)} />
<div> <div>
<div className="flex flex-col justify-between sm:flex-row"> <div className="flex flex-col justify-between sm:flex-row">
<Header> <Header>

View File

@@ -20,8 +20,10 @@ import * as Yup from 'yup';
import AddUserIcon from '../../assets/useradd.svg'; import AddUserIcon from '../../assets/useradd.svg';
import Alert from '../Common/Alert'; import Alert from '../Common/Alert';
import BulkEditModal from './BulkEditModal'; import BulkEditModal from './BulkEditModal';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({ const messages = defineMessages({
users: 'Users',
userlist: 'User List', userlist: 'User List',
importfromplex: 'Import Users from Plex', importfromplex: 'Import Users from Plex',
importfromplexerror: 'Something went wrong while importing users from Plex.', importfromplexerror: 'Something went wrong while importing users from Plex.',
@@ -178,6 +180,7 @@ const UserList: React.FC = () => {
return ( return (
<> <>
<PageTitle title={intl.formatMessage(messages.users)} />
<Transition <Transition
enter="opacity-0 transition duration-300" enter="opacity-0 transition duration-300"
enterFrom="opacity-0" enterFrom="opacity-0"

View File

@@ -8,10 +8,11 @@ export interface SettingsContextProps {
const defaultSettings = { const defaultSettings = {
initialized: false, initialized: false,
movie4kEnabled: false, applicationTitle: 'Overseerr',
series4kEnabled: false,
hideAvailable: false, hideAvailable: false,
localLogin: false, localLogin: false,
movie4kEnabled: false,
series4kEnabled: false,
}; };
export const SettingsContext = React.createContext<SettingsContextProps>({ export const SettingsContext = React.createContext<SettingsContextProps>({

View File

@@ -206,6 +206,7 @@
"components.RequestModal.seasonnumber": "Season {number}", "components.RequestModal.seasonnumber": "Season {number}",
"components.RequestModal.selectseason": "Select season(s)", "components.RequestModal.selectseason": "Select season(s)",
"components.RequestModal.status": "Status", "components.RequestModal.status": "Status",
"components.Search.search": "Search",
"components.Search.searchresults": "Search Results", "components.Search.searchresults": "Search Results",
"components.Settings.Notifications.NotificationsPushover.accessToken": "Access Token", "components.Settings.Notifications.NotificationsPushover.accessToken": "Access Token",
"components.Settings.Notifications.NotificationsPushover.agentenabled": "Enable Agent", "components.Settings.Notifications.NotificationsPushover.agentenabled": "Enable Agent",
@@ -413,6 +414,7 @@
"components.Settings.address": "Address", "components.Settings.address": "Address",
"components.Settings.addsonarr": "Add Sonarr Server", "components.Settings.addsonarr": "Add Sonarr Server",
"components.Settings.apikey": "API Key", "components.Settings.apikey": "API Key",
"components.Settings.applicationTitle": "Application Title",
"components.Settings.applicationurl": "Application URL", "components.Settings.applicationurl": "Application URL",
"components.Settings.autoapprovedrequests": "Enable Notifications for Automatic Approvals", "components.Settings.autoapprovedrequests": "Enable Notifications for Automatic Approvals",
"components.Settings.cancelscan": "Cancel Scan", "components.Settings.cancelscan": "Cancel Scan",
@@ -474,6 +476,7 @@
"components.Settings.serverpresetManualMessage": "Manual configuration", "components.Settings.serverpresetManualMessage": "Manual configuration",
"components.Settings.serverpresetPlaceholder": "Plex Server", "components.Settings.serverpresetPlaceholder": "Plex Server",
"components.Settings.serverpresetRefreshing": "Retrieving servers…", "components.Settings.serverpresetRefreshing": "Retrieving servers…",
"components.Settings.settings": "Settings",
"components.Settings.settingUpPlex": "Setting Up Plex", "components.Settings.settingUpPlex": "Setting Up Plex",
"components.Settings.settingUpPlexDescription": "To set up Plex, you can either enter your details manually or select a server retrieved from <RegisterPlexTVLink>plex.tv</RegisterPlexTVLink>. Press the button to the right of the dropdown to check connectivity and retrieve available servers.", "components.Settings.settingUpPlexDescription": "To set up Plex, you can either enter your details manually or select a server retrieved from <RegisterPlexTVLink>plex.tv</RegisterPlexTVLink>. Press the button to the right of the dropdown to check connectivity and retrieve available servers.",
"components.Settings.sonarrSettingsDescription": "Configure your Sonarr connection below. You can have multiple Sonarr configurations, but only two can be active as defaults at any time (one for standard HD and one for 4K). Administrators can override the server which is used for new requests.", "components.Settings.sonarrSettingsDescription": "Configure your Sonarr connection below. You can have multiple Sonarr configurations, but only two can be active as defaults at any time (one for standard HD and one for 4K). Administrators can override the server which is used for new requests.",
@@ -593,6 +596,7 @@
"components.UserList.userdeleteerror": "Something went wrong while deleting the user.", "components.UserList.userdeleteerror": "Something went wrong while deleting the user.",
"components.UserList.userlist": "User List", "components.UserList.userlist": "User List",
"components.UserList.username": "Username", "components.UserList.username": "Username",
"components.UserList.users": "Users",
"components.UserList.userssaved": "Users saved!", "components.UserList.userssaved": "Users saved!",
"components.UserList.usertype": "User Type", "components.UserList.usertype": "User Type",
"components.UserList.validationemailrequired": "Must enter a valid email address", "components.UserList.validationemailrequired": "Must enter a valid email address",

View File

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