feat(ui): Add custom title functionality (#825)
This commit is contained in:
@@ -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"
|
||||
|
||||
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 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,
|
||||
|
||||
Reference in New Issue
Block a user