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

pull/828/head
TheCatLady 4 years ago committed by GitHub
parent 3ffd5ab0ee
commit 35c6bfc021
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

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

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

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

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

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

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

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

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

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

@ -8,8 +8,10 @@ import type { MediaResultsResponse } from '../../../server/interfaces/api/mediaI
import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces';
import RequestCard from '../RequestCard';
import MediaSlider from '../MediaSlider';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({
discover: 'Discover',
recentrequests: 'Recent Requests',
popularmovies: 'Popular Movies',
populartv: 'Popular Series',
@ -35,6 +37,7 @@ const Discover: React.FC = () => {
return (
<>
<PageTitle title={intl.formatMessage(messages.discover)} />
<div className="mt-6 mb-4 md:flex md:items-center md:justify-between">
<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">

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

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

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

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

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

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

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

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

@ -7,6 +7,7 @@ import Header from '../Common/Header';
import Table from '../Common/Table';
import Button from '../Common/Button';
import { defineMessages, useIntl } from 'react-intl';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({
requests: 'Requests',
@ -54,6 +55,7 @@ const RequestList: React.FC = () => {
return (
<>
<PageTitle title={intl.formatMessage(messages.requests)} />
<div className="flex flex-col justify-between md:items-end md:flex-row">
<Header>{intl.formatMessage(messages.requests)}</Header>
<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 { defineMessages, useIntl } from 'react-intl';
import Header from '../Common/Header';
import PageTitle from '../Common/PageTitle';
const messages = defineMessages({
search: 'Search',
searchresults: 'Search Results',
});
@ -65,6 +67,7 @@ const Search: React.FC = () => {
return (
<>
<PageTitle title={intl.formatMessage(messages.search)} />
<div className="mt-1 mb-5">
<Header>{intl.formatMessage(messages.searchresults)}</Header>
</div>

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

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

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

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

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

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

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

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

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

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

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

@ -206,6 +206,7 @@
"components.RequestModal.seasonnumber": "Season {number}",
"components.RequestModal.selectseason": "Select season(s)",
"components.RequestModal.status": "Status",
"components.Search.search": "Search",
"components.Search.searchresults": "Search Results",
"components.Settings.Notifications.NotificationsPushover.accessToken": "Access Token",
"components.Settings.Notifications.NotificationsPushover.agentenabled": "Enable Agent",
@ -413,6 +414,7 @@
"components.Settings.address": "Address",
"components.Settings.addsonarr": "Add Sonarr Server",
"components.Settings.apikey": "API Key",
"components.Settings.applicationTitle": "Application Title",
"components.Settings.applicationurl": "Application URL",
"components.Settings.autoapprovedrequests": "Enable Notifications for Automatic Approvals",
"components.Settings.cancelscan": "Cancel Scan",
@ -474,6 +476,7 @@
"components.Settings.serverpresetManualMessage": "Manual configuration",
"components.Settings.serverpresetPlaceholder": "Plex Server",
"components.Settings.serverpresetRefreshing": "Retrieving servers…",
"components.Settings.settings": "Settings",
"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.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.userlist": "User List",
"components.UserList.username": "Username",
"components.UserList.users": "Users",
"components.UserList.userssaved": "Users saved!",
"components.UserList.usertype": "User Type",
"components.UserList.validationemailrequired": "Must enter a valid email address",

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

Loading…
Cancel
Save