feat(ui): Add custom title functionality (#825)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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[] = [];
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
22
src/components/Common/PageTitle/index.tsx
Normal file
22
src/components/Common/PageTitle/index.tsx
Normal 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;
|
||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>({
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user