Compare commits

...

2 Commits

Author SHA1 Message Date
megamit
abef384196 fix: updated i18n extract 2022-01-04 20:09:46 +00:00
megamit
7de426f483 feat: add base path setting for reverse proxy support
issue #274
2022-01-04 20:09:46 +00:00
87 changed files with 382 additions and 171 deletions

View File

@@ -3,10 +3,13 @@
// previously cached resources to be updated from the network. // previously cached resources to be updated from the network.
// This variable is intentionally declared and unused. // This variable is intentionally declared and unused.
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const OFFLINE_VERSION = 3; const OFFLINE_VERSION = 4;
const CACHE_NAME = "offline"; const CACHE_NAME = "offline";
// use scope path to determine if we are running in basePath
const BASE_PATH = (new URL(self.registration.scope)).pathname.replace(/\/$/, '')
// Customize this with a different URL if needed. // Customize this with a different URL if needed.
const OFFLINE_URL = "/offline.html"; const OFFLINE_URL = BASE_PATH + "/offline.html";
self.addEventListener("install", (event) => { self.addEventListener("install", (event) => {
event.waitUntil( event.waitUntil(
@@ -120,16 +123,16 @@ self.addEventListener('notificationclick', (event) => {
event.notification.close(); event.notification.close();
if (event.action === 'approve') { if (event.action === 'approve') {
fetch(`/api/v1/request/${notificationData.requestId}/approve`, { fetch(`${BASE_PATH}/api/v1/request/${notificationData.requestId}/approve`, {
method: 'POST', method: 'POST',
}); });
} else if (event.action === 'decline') { } else if (event.action === 'decline') {
fetch(`/api/v1/request/${notificationData.requestId}/decline`, { fetch(`${BASE_PATH}/api/v1/request/${notificationData.requestId}/decline`, {
method: 'POST', method: 'POST',
}); });
} }
if (notificationData.actionUrl) { if (notificationData.actionUrl) {
clients.openWindow(notificationData.actionUrl); clients.openWindow(BASE_PATH + notificationData.actionUrl);
} }
}, false); }, false);

View File

@@ -2,7 +2,7 @@ import { getClientIp } from '@supercharge/request-ip';
import { TypeormStore } from 'connect-typeorm/out'; import { TypeormStore } from 'connect-typeorm/out';
import cookieParser from 'cookie-parser'; import cookieParser from 'cookie-parser';
import csurf from 'csurf'; import csurf from 'csurf';
import express, { NextFunction, Request, Response } from 'express'; import express, { NextFunction, Request, Response, Router } from 'express';
import * as OpenApiValidator from 'express-openapi-validator'; import * as OpenApiValidator from 'express-openapi-validator';
import session, { Store } from 'express-session'; import session, { Store } from 'express-session';
import next from 'next'; import next from 'next';
@@ -30,17 +30,13 @@ import routes from './routes';
import { getAppVersion } from './utils/appVersion'; import { getAppVersion } from './utils/appVersion';
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml'); const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
const NEXT_CONFIG_PATH = path.join(__dirname, '../next.config.js');
logger.info(`Starting Overseerr version ${getAppVersion()}`); logger.info(`Starting Overseerr version ${getAppVersion()}`);
const dev = process.env.NODE_ENV !== 'production'; const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
app
.prepare()
.then(async () => {
const dbConnection = await createConnection();
createConnection()
.then(async (dbConnection) => {
// Run migrations in production // Run migrations in production
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
await dbConnection.query('PRAGMA foreign_keys=OFF'); await dbConnection.query('PRAGMA foreign_keys=OFF');
@@ -51,6 +47,34 @@ app
// Load Settings // Load Settings
const settings = getSettings().load(); const settings = getSettings().load();
const { default: nextConfig } = await import(NEXT_CONFIG_PATH);
let basePath = settings.main.basePath;
if (basePath) {
// Ensure base path is free of invalid characters
basePath = encodeURI(basePath);
logger.debug(`Serving from base path ${basePath}`);
nextConfig.rewrites = async () => ({
beforeFiles: [
/* Strip off base path for serving pages */
{
source: basePath + '/:path*',
destination: '/:path*',
},
],
});
nextConfig.publicRuntimeConfig = { basePath };
}
const app = next({ dev, conf: nextConfig });
const handle = app.getRequestHandler();
if (basePath) {
app.setAssetPrefix(basePath + '/');
}
await app.prepare();
// Migrate library types // Migrate library types
if ( if (
settings.plex.libraries.length > 1 && settings.plex.libraries.length > 1 &&
@@ -131,7 +155,9 @@ app
// Set up sessions // Set up sessions
const sessionRespository = getRepository(Session); const sessionRespository = getRepository(Session);
server.use( const apiRoutes = Router();
server.use(basePath || '/', apiRoutes);
apiRoutes.use(
'/api', '/api',
session({ session({
secret: settings.clientId, secret: settings.clientId,
@@ -150,8 +176,8 @@ app
}) })
); );
const apiDocs = YAML.load(API_SPEC_PATH); const apiDocs = YAML.load(API_SPEC_PATH);
server.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiDocs)); apiRoutes.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiDocs));
server.use( apiRoutes.use(
OpenApiValidator.middleware({ OpenApiValidator.middleware({
apiSpec: API_SPEC_PATH, apiSpec: API_SPEC_PATH,
validateRequests: true, validateRequests: true,
@@ -162,15 +188,15 @@ app
* OpenAPI validator. Otherwise, they are treated as objects instead of strings * OpenAPI validator. Otherwise, they are treated as objects instead of strings
* and response validation will fail * and response validation will fail
*/ */
server.use((_req, res, next) => { apiRoutes.use((_req, res, next) => {
const original = res.json; const original = res.json;
res.json = function jsonp(json) { res.json = function jsonp(json) {
return original.call(this, JSON.parse(JSON.stringify(json))); return original.call(this, JSON.parse(JSON.stringify(json)));
}; };
next(); next();
}); });
server.use('/api/v1', routes); apiRoutes.use('/api/v1', routes);
server.get('*', (req, res) => handle(req, res)); apiRoutes.get('*', (req, res) => handle(req, res));
server.use( server.use(
( (
err: { status: number; message: string; errors: string[] }, err: { status: number; message: string; errors: string[] },

View File

@@ -92,6 +92,7 @@ export interface MainSettings {
trustProxy: boolean; trustProxy: boolean;
partialRequestsEnabled: boolean; partialRequestsEnabled: boolean;
locale: string; locale: string;
basePath: string;
} }
interface PublicSettings { interface PublicSettings {
@@ -271,6 +272,7 @@ class Settings {
trustProxy: false, trustProxy: false,
partialRequestsEnabled: true, partialRequestsEnabled: true,
locale: 'en', locale: 'en',
basePath: '',
}, },
plex: { plex: {
name: '', name: '',

View File

@@ -1,6 +1,5 @@
import { DownloadIcon } from '@heroicons/react/outline'; import { DownloadIcon } from '@heroicons/react/outline';
import { uniq } from 'lodash'; import { uniq } from 'lodash';
import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
@@ -15,6 +14,7 @@ import ButtonWithDropdown from '../Common/ButtonWithDropdown';
import CachedImage from '../Common/CachedImage'; import CachedImage from '../Common/CachedImage';
import LoadingSpinner from '../Common/LoadingSpinner'; import LoadingSpinner from '../Common/LoadingSpinner';
import PageTitle from '../Common/PageTitle'; import PageTitle from '../Common/PageTitle';
import Link from '../Link';
import RequestModal from '../RequestModal'; import RequestModal from '../RequestModal';
import Slider from '../Slider'; import Slider from '../Slider';
import StatusBadge from '../StatusBadge'; import StatusBadge from '../StatusBadge';

View File

@@ -1,6 +1,7 @@
import Image, { ImageProps } from 'next/image'; import Image, { ImageProps } from 'next/image';
import React from 'react'; import React from 'react';
import useSettings from '../../../hooks/useSettings'; import useSettings from '../../../hooks/useSettings';
import addBasePath from '../../../utils/addBasePath';
/** /**
* The CachedImage component should be used wherever * The CachedImage component should be used wherever
@@ -12,6 +13,10 @@ import useSettings from '../../../hooks/useSettings';
const CachedImage: React.FC<ImageProps> = (props) => { const CachedImage: React.FC<ImageProps> = (props) => {
const { currentSettings } = useSettings(); const { currentSettings } = useSettings();
/* Check for internal urls and add base path */
if (typeof props.src === 'string' && !props.src.includes('://')) {
props = { ...props, src: addBasePath(props.src) };
}
return <Image unoptimized={!currentSettings.cacheImages} {...props} />; return <Image unoptimized={!currentSettings.cacheImages} {...props} />;
}; };

View File

@@ -1,8 +1,8 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { hasPermission, Permission } from '../../../../server/lib/permissions'; import { hasPermission, Permission } from '../../../../server/lib/permissions';
import { useRouter } from '../../../hooks/useRouter';
import { useUser } from '../../../hooks/useUser'; import { useUser } from '../../../hooks/useUser';
import Link from '../../Link';
export interface SettingsRoute { export interface SettingsRoute {
text: string; text: string;

View File

@@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import type { MovieResult } from '../../../../server/models/Search'; import type { MovieResult } from '../../../../server/models/Search';
import { useRouter } from '../../../hooks/useRouter';
import ListView from '../../Common/ListView'; import ListView from '../../Common/ListView';
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'; import PageTitle from '../../Common/PageTitle';
import { useRouter } from 'next/router';
import globalMessages from '../../../i18n/globalMessages'; import globalMessages from '../../../i18n/globalMessages';
import useDiscover from '../../../hooks/useDiscover'; import useDiscover from '../../../hooks/useDiscover';
import Error from '../../../pages/_error'; import Error from '../../../pages/_error';

View File

@@ -1,10 +1,10 @@
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
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, 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'; import PageTitle from '../../Common/PageTitle';
import { useRouter } from 'next/router';
import globalMessages from '../../../i18n/globalMessages'; import globalMessages from '../../../i18n/globalMessages';
import useDiscover from '../../../hooks/useDiscover'; import useDiscover from '../../../hooks/useDiscover';
import Error from '../../../pages/_error'; import Error from '../../../pages/_error';

View File

@@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import type { MovieResult } from '../../../../server/models/Search'; import type { MovieResult } from '../../../../server/models/Search';
import { useRouter } from '../../../hooks/useRouter';
import ListView from '../../Common/ListView'; import ListView from '../../Common/ListView';
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'; import PageTitle from '../../Common/PageTitle';
import { useRouter } from 'next/router';
import globalMessages from '../../../i18n/globalMessages'; import globalMessages from '../../../i18n/globalMessages';
import useDiscover from '../../../hooks/useDiscover'; import useDiscover from '../../../hooks/useDiscover';
import Error from '../../../pages/_error'; import Error from '../../../pages/_error';

View File

@@ -4,10 +4,10 @@ import ListView from '../../Common/ListView';
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'; import PageTitle from '../../Common/PageTitle';
import { useRouter } from 'next/router';
import globalMessages from '../../../i18n/globalMessages'; import globalMessages from '../../../i18n/globalMessages';
import useDiscover from '../../../hooks/useDiscover'; import useDiscover from '../../../hooks/useDiscover';
import Error from '../../../pages/_error'; import Error from '../../../pages/_error';
import { useRouter } from '../../../hooks/useRouter';
const messages = defineMessages({ const messages = defineMessages({
genreSeries: '{genre} Series', genreSeries: '{genre} Series',

View File

@@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import type { TvResult } from '../../../../server/models/Search'; import type { TvResult } from '../../../../server/models/Search';
import { useRouter } from '../../../hooks/useRouter';
import ListView from '../../Common/ListView'; import ListView from '../../Common/ListView';
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'; import PageTitle from '../../Common/PageTitle';
import { useRouter } from 'next/router';
import globalMessages from '../../../i18n/globalMessages'; import globalMessages from '../../../i18n/globalMessages';
import useDiscover from '../../../hooks/useDiscover'; import useDiscover from '../../../hooks/useDiscover';
import Error from '../../../pages/_error'; import Error from '../../../pages/_error';

View File

@@ -1,10 +1,10 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline'; import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces'; import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
import GenreCard from '../../GenreCard'; import GenreCard from '../../GenreCard';
import Link from '../../Link';
import Slider from '../../Slider'; import Slider from '../../Slider';
import { genreColorMap } from '../constants'; import { genreColorMap } from '../constants';

View File

@@ -1,10 +1,10 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline'; import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces'; import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
import GenreCard from '../../GenreCard'; import GenreCard from '../../GenreCard';
import Link from '../../Link';
import Slider from '../../Slider'; import Slider from '../../Slider';
import { genreColorMap } from '../constants'; import { genreColorMap } from '../constants';

View File

@@ -1,11 +1,11 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline'; import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
import type { MediaResultsResponse } from '../../../server/interfaces/api/mediaInterfaces'; import type { MediaResultsResponse } from '../../../server/interfaces/api/mediaInterfaces';
import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces'; import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces';
import PageTitle from '../Common/PageTitle'; import PageTitle from '../Common/PageTitle';
import Link from '../Link';
import MediaSlider from '../MediaSlider'; import MediaSlider from '../MediaSlider';
import RequestCard from '../RequestCard'; import RequestCard from '../RequestCard';
import Slider from '../Slider'; import Slider from '../Slider';

View File

@@ -1,7 +1,7 @@
import Link from 'next/link';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { withProperties } from '../../utils/typeHelpers'; import { withProperties } from '../../utils/typeHelpers';
import CachedImage from '../Common/CachedImage'; import CachedImage from '../Common/CachedImage';
import Link from '../Link';
interface GenreCardProps { interface GenreCardProps {
name: string; name: string;

View File

@@ -4,13 +4,13 @@ import {
EyeIcon, EyeIcon,
UserIcon, UserIcon,
} from '@heroicons/react/solid'; } from '@heroicons/react/solid';
import Link from 'next/link';
import React from 'react'; import React from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import type Issue from '../../../server/entity/Issue'; import type Issue from '../../../server/entity/Issue';
import globalMessages from '../../i18n/globalMessages'; import globalMessages from '../../i18n/globalMessages';
import Button from '../Common/Button'; import Button from '../Common/Button';
import { issueOptions } from '../IssueModal/constants'; import { issueOptions } from '../IssueModal/constants';
import Link from '../Link';
interface IssueBlockProps { interface IssueBlockProps {
issue: Issue; issue: Issue;

View File

@@ -3,7 +3,6 @@ import { ExclamationIcon } from '@heroicons/react/outline';
import { DotsVerticalIcon } from '@heroicons/react/solid'; import { DotsVerticalIcon } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import Link from 'next/link';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl'; import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
@@ -12,6 +11,7 @@ import type { default as IssueCommentType } from '../../../../server/entity/Issu
import { Permission, useUser } from '../../../hooks/useUser'; import { Permission, useUser } from '../../../hooks/useUser';
import Button from '../../Common/Button'; import Button from '../../Common/Button';
import Modal from '../../Common/Modal'; import Modal from '../../Common/Modal';
import Link from '../../Link';
import Transition from '../../Transition'; import Transition from '../../Transition';
const messages = defineMessages({ const messages = defineMessages({

View File

@@ -8,8 +8,6 @@ import {
import { RefreshIcon } from '@heroicons/react/solid'; import { RefreshIcon } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl'; import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
@@ -20,6 +18,7 @@ import { MediaType } from '../../../server/constants/media';
import type Issue from '../../../server/entity/Issue'; import type Issue from '../../../server/entity/Issue';
import type { MovieDetails } from '../../../server/models/Movie'; import type { MovieDetails } from '../../../server/models/Movie';
import type { TvDetails } from '../../../server/models/Tv'; import type { TvDetails } from '../../../server/models/Tv';
import { useRouter } from '../../hooks/useRouter';
import { Permission, useUser } from '../../hooks/useUser'; import { Permission, useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages'; import globalMessages from '../../i18n/globalMessages';
import Error from '../../pages/_error'; import Error from '../../pages/_error';
@@ -30,6 +29,7 @@ import LoadingSpinner from '../Common/LoadingSpinner';
import Modal from '../Common/Modal'; import Modal from '../Common/Modal';
import PageTitle from '../Common/PageTitle'; import PageTitle from '../Common/PageTitle';
import { issueOptions } from '../IssueModal/constants'; import { issueOptions } from '../IssueModal/constants';
import Link from '../Link';
import Transition from '../Transition'; import Transition from '../Transition';
import IssueComment from './IssueComment'; import IssueComment from './IssueComment';
import IssueDescription from './IssueDescription'; import IssueDescription from './IssueDescription';

View File

@@ -1,5 +1,4 @@
import { EyeIcon } from '@heroicons/react/solid'; import { EyeIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import React from 'react'; import React from 'react';
import { useInView } from 'react-intersection-observer'; import { useInView } from 'react-intersection-observer';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl'; import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
@@ -15,6 +14,7 @@ import Badge from '../../Common/Badge';
import Button from '../../Common/Button'; import Button from '../../Common/Button';
import CachedImage from '../../Common/CachedImage'; import CachedImage from '../../Common/CachedImage';
import { issueOptions } from '../../IssueModal/constants'; import { issueOptions } from '../../IssueModal/constants';
import Link from '../../Link';
const messages = defineMessages({ const messages = defineMessages({
openeduserdate: '{date} by {user}', openeduserdate: '{date} by {user}',

View File

@@ -4,12 +4,12 @@ import {
FilterIcon, FilterIcon,
SortDescendingIcon, SortDescendingIcon,
} from '@heroicons/react/solid'; } from '@heroicons/react/solid';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
import { IssueResultsResponse } from '../../../server/interfaces/api/issueInterfaces'; import { IssueResultsResponse } from '../../../server/interfaces/api/issueInterfaces';
import Button from '../../components/Common/Button'; import Button from '../../components/Common/Button';
import { useRouter } from '../../hooks/useRouter';
import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams'; import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams';
import globalMessages from '../../i18n/globalMessages'; import globalMessages from '../../i18n/globalMessages';
import Header from '../Common/Header'; import Header from '../Common/Header';

View File

@@ -3,7 +3,6 @@ import { ExclamationIcon } from '@heroicons/react/outline';
import { ArrowCircleRightIcon } from '@heroicons/react/solid'; import { ArrowCircleRightIcon } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import { Field, Formik } from 'formik'; import { Field, Formik } from 'formik';
import Link from 'next/link';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
@@ -18,6 +17,7 @@ import { Permission, useUser } from '../../../hooks/useUser';
import globalMessages from '../../../i18n/globalMessages'; import globalMessages from '../../../i18n/globalMessages';
import Button from '../../Common/Button'; import Button from '../../Common/Button';
import Modal from '../../Common/Modal'; import Modal from '../../Common/Modal';
import Link from '../../Link';
import { issueOptions } from '../constants'; import { issueOptions } from '../constants';
const messages = defineMessages({ const messages = defineMessages({

View File

@@ -6,12 +6,13 @@ import {
UsersIcon, UsersIcon,
XIcon, XIcon,
} from '@heroicons/react/outline'; } from '@heroicons/react/outline';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { ReactNode, useRef } from 'react'; import React, { ReactNode, useRef } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useClickOutside from '../../../hooks/useClickOutside'; import useClickOutside from '../../../hooks/useClickOutside';
import { useRouter } from '../../../hooks/useRouter';
import { Permission, useUser } from '../../../hooks/useUser'; import { Permission, useUser } from '../../../hooks/useUser';
import addBasePath from '../../../utils/addBasePath';
import Link from '../../Link';
import Transition from '../../Transition'; import Transition from '../../Transition';
import VersionStatus from '../VersionStatus'; import VersionStatus from '../VersionStatus';
@@ -130,8 +131,8 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
> >
<div className="flex items-center flex-shrink-0 px-2"> <div className="flex items-center flex-shrink-0 px-2">
<span className="px-4 text-xl text-gray-50"> <span className="px-4 text-xl text-gray-50">
<a href="/"> <a href={addBasePath('/')}>
<img src="/logo_full.svg" alt="Logo" /> <img src={addBasePath('/logo_full.svg')} alt="Logo" />
</a> </a>
</span> </span>
</div> </div>
@@ -199,8 +200,8 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
<div className="flex flex-col flex-1 pt-8 pb-4 overflow-y-auto"> <div className="flex flex-col flex-1 pt-8 pb-4 overflow-y-auto">
<div className="flex items-center flex-shrink-0"> <div className="flex items-center flex-shrink-0">
<span className="px-4 text-2xl text-gray-50"> <span className="px-4 text-2xl text-gray-50">
<a href="/"> <a href={addBasePath('/')}>
<img src="/logo_full.svg" alt="Logo" /> <img src={addBasePath('/logo_full.svg')} alt="Logo" />
</a> </a>
</span> </span>
</div> </div>

View File

@@ -1,11 +1,11 @@
import { LogoutIcon } from '@heroicons/react/outline'; import { LogoutIcon } from '@heroicons/react/outline';
import { CogIcon, UserIcon } from '@heroicons/react/solid'; import { CogIcon, UserIcon } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import Link from 'next/link';
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useClickOutside from '../../../hooks/useClickOutside'; import useClickOutside from '../../../hooks/useClickOutside';
import { useUser } from '../../../hooks/useUser'; import { useUser } from '../../../hooks/useUser';
import Link from '../../Link';
import Transition from '../../Transition'; import Transition from '../../Transition';
const messages = defineMessages({ const messages = defineMessages({

View File

@@ -4,11 +4,11 @@ import {
CodeIcon, CodeIcon,
ServerIcon, ServerIcon,
} from '@heroicons/react/outline'; } from '@heroicons/react/outline';
import Link from 'next/link';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
import { StatusResponse } from '../../../../server/interfaces/api/settingsInterfaces'; import { StatusResponse } from '../../../../server/interfaces/api/settingsInterfaces';
import Link from '../../Link';
const messages = defineMessages({ const messages = defineMessages({
streamdevelop: 'Overseerr Develop', streamdevelop: 'Overseerr Develop',

View File

@@ -1,9 +1,9 @@
import { MenuAlt2Icon } from '@heroicons/react/outline'; import { MenuAlt2Icon } from '@heroicons/react/outline';
import { ArrowLeftIcon } from '@heroicons/react/solid'; import { ArrowLeftIcon } from '@heroicons/react/solid';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { AvailableLocale } from '../../context/LanguageContext'; import { AvailableLocale } from '../../context/LanguageContext';
import useLocale from '../../hooks/useLocale'; import useLocale from '../../hooks/useLocale';
import { useRouter } from '../../hooks/useRouter';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
import { useUser } from '../../hooks/useUser'; import { useUser } from '../../hooks/useUser';
import SearchInput from './SearchInput'; import SearchInput from './SearchInput';

View File

@@ -0,0 +1,12 @@
import NextLink, { LinkProps } from 'next/link';
import React from 'react';
import addBasePath from '../../utils/addBasePath';
const Link: React.FC<LinkProps> = ({ href, ...props }) => {
if (props.as) {
props.as = addBasePath(props.as);
}
return <NextLink href={addBasePath(href)} {...props} />;
};
export default Link;

View File

@@ -1,7 +1,7 @@
import { NProgress } from '@tanem/react-nprogress'; import { NProgress } from '@tanem/react-nprogress';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { useRouter } from 'next/router'; import { useRouter } from '../../hooks/useRouter';
interface BarProps { interface BarProps {
progress: number; progress: number;

View File

@@ -1,13 +1,13 @@
import { LoginIcon, SupportIcon } from '@heroicons/react/outline'; import { LoginIcon, SupportIcon } from '@heroicons/react/outline';
import axios from 'axios'; import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import Link from 'next/link';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import * as Yup from 'yup'; import * as Yup from 'yup';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
import Button from '../Common/Button'; import Button from '../Common/Button';
import SensitiveInput from '../Common/SensitiveInput'; import SensitiveInput from '../Common/SensitiveInput';
import Link from '../Link';
const messages = defineMessages({ const messages = defineMessages({
email: 'Email Address', email: 'Email Address',

View File

@@ -1,11 +1,12 @@
import { XCircleIcon } from '@heroicons/react/solid'; import { XCircleIcon } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import { useRouter } from 'next/dist/client/router';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
import { useRouter } from '../../hooks/useRouter';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
import { useUser } from '../../hooks/useUser'; import { useUser } from '../../hooks/useUser';
import addBasePath from '../../utils/addBasePath';
import Accordion from '../Common/Accordion'; import Accordion from '../Common/Accordion';
import ImageFader from '../Common/ImageFader'; import ImageFader from '../Common/ImageFader';
import PageTitle from '../Common/PageTitle'; import PageTitle from '../Common/PageTitle';
@@ -81,7 +82,11 @@ const Login: React.FC = () => {
<LanguagePicker /> <LanguagePicker />
</div> </div>
<div className="relative z-40 flex flex-col items-center px-4 mt-10 sm:mx-auto sm:w-full sm:max-w-md"> <div className="relative z-40 flex flex-col items-center px-4 mt-10 sm:mx-auto sm:w-full sm:max-w-md">
<img src="/logo_stacked.svg" className="max-w-full mb-10" alt="Logo" /> <img
src={addBasePath('/logo_stacked.svg')}
className="max-w-full mb-10"
alt="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">
{intl.formatMessage(messages.signinheader)} {intl.formatMessage(messages.signinheader)}
</h2> </h2>

View File

@@ -1,7 +1,7 @@
import { ArrowCircleRightIcon } from '@heroicons/react/solid'; import { ArrowCircleRightIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import Link from '../../Link';
const messages = defineMessages({ const messages = defineMessages({
seemore: 'See More', seemore: 'See More',

View File

@@ -1,5 +1,4 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline'; import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useSWRInfinite } from 'swr'; import { useSWRInfinite } from 'swr';
import { MediaStatus } from '../../../server/constants/media'; import { MediaStatus } from '../../../server/constants/media';
@@ -9,6 +8,7 @@ import type {
TvResult, TvResult,
} from '../../../server/models/Search'; } from '../../../server/models/Search';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
import Link from '../Link';
import PersonCard from '../PersonCard'; import PersonCard from '../PersonCard';
import Slider from '../Slider'; import Slider from '../Slider';
import TitleCard from '../TitleCard'; import TitleCard from '../TitleCard';

View File

@@ -1,13 +1,13 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
import { MovieDetails } from '../../../../server/models/Movie'; import { MovieDetails } from '../../../../server/models/Movie';
import { useRouter } from '../../../hooks/useRouter';
import Error from '../../../pages/_error'; 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 PageTitle from '../../Common/PageTitle'; import PageTitle from '../../Common/PageTitle';
import Link from '../../Link';
import PersonCard from '../../PersonCard'; import PersonCard from '../../PersonCard';
const messages = defineMessages({ const messages = defineMessages({

View File

@@ -1,13 +1,13 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
import { MovieDetails } from '../../../../server/models/Movie'; import { MovieDetails } from '../../../../server/models/Movie';
import { useRouter } from '../../../hooks/useRouter';
import Error from '../../../pages/_error'; 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 PageTitle from '../../Common/PageTitle'; import PageTitle from '../../Common/PageTitle';
import Link from '../../Link';
import PersonCard from '../../PersonCard'; import PersonCard from '../../PersonCard';
const messages = defineMessages({ const messages = defineMessages({

View File

@@ -1,4 +1,3 @@
import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
@@ -10,6 +9,7 @@ import Error from '../../pages/_error';
import Header from '../Common/Header'; import Header from '../Common/Header';
import ListView from '../Common/ListView'; import ListView from '../Common/ListView';
import PageTitle from '../Common/PageTitle'; import PageTitle from '../Common/PageTitle';
import Link from '../Link';
const messages = defineMessages({ const messages = defineMessages({
recommendations: 'Recommendations', recommendations: 'Recommendations',

View File

@@ -1,15 +1,15 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
import type { MovieDetails } from '../../../server/models/Movie'; import type { MovieDetails } from '../../../server/models/Movie';
import type { MovieResult } from '../../../server/models/Search'; import type { MovieResult } from '../../../server/models/Search';
import useDiscover from '../../hooks/useDiscover'; import useDiscover from '../../hooks/useDiscover';
import { useRouter } from '../../hooks/useRouter';
import Error from '../../pages/_error'; import Error from '../../pages/_error';
import Header from '../Common/Header'; import Header from '../Common/Header';
import ListView from '../Common/ListView'; import ListView from '../Common/ListView';
import PageTitle from '../Common/PageTitle'; import PageTitle from '../Common/PageTitle';
import Link from '../Link';
const messages = defineMessages({ const messages = defineMessages({
similar: 'Similar Titles', similar: 'Similar Titles',

View File

@@ -14,8 +14,6 @@ import {
import { hasFlag } from 'country-flag-icons'; import { hasFlag } from 'country-flag-icons';
import 'country-flag-icons/3x2/flags.css'; import 'country-flag-icons/3x2/flags.css';
import { uniqBy } from 'lodash'; import { uniqBy } from 'lodash';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
@@ -29,6 +27,7 @@ import RTFresh from '../../assets/rt_fresh.svg';
import RTRotten from '../../assets/rt_rotten.svg'; import RTRotten from '../../assets/rt_rotten.svg';
import TmdbLogo from '../../assets/tmdb_logo.svg'; import TmdbLogo from '../../assets/tmdb_logo.svg';
import useLocale from '../../hooks/useLocale'; import useLocale from '../../hooks/useLocale';
import { useRouter } from '../../hooks/useRouter';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
import { Permission, useUser } from '../../hooks/useUser'; import { Permission, useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages'; import globalMessages from '../../i18n/globalMessages';
@@ -41,6 +40,7 @@ import PageTitle from '../Common/PageTitle';
import PlayButton, { PlayButtonLink } from '../Common/PlayButton'; import PlayButton, { PlayButtonLink } from '../Common/PlayButton';
import ExternalLinkBlock from '../ExternalLinkBlock'; import ExternalLinkBlock from '../ExternalLinkBlock';
import IssueModal from '../IssueModal'; import IssueModal from '../IssueModal';
import Link from '../Link';
import ManageSlideOver from '../ManageSlideOver'; import ManageSlideOver from '../ManageSlideOver';
import MediaSlider from '../MediaSlider'; import MediaSlider from '../MediaSlider';
import PersonCard from '../PersonCard'; import PersonCard from '../PersonCard';

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import addBasePath from '../../utils/addBasePath';
interface PWAHeaderProps { interface PWAHeaderProps {
applicationTitle?: string; applicationTitle?: string;
@@ -10,148 +11,148 @@ const PWAHeader: React.FC<PWAHeaderProps> = ({ applicationTitle }) => {
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="180x180" sizes="180x180"
href="/apple-touch-icon.png" href={addBasePath('/apple-touch-icon.png')}
/> />
<link <link
rel="icon" rel="icon"
type="image/png" type="image/png"
sizes="32x32" sizes="32x32"
href="/favicon-32x32.png" href={addBasePath('/favicon-32x32.png')}
/> />
<link <link
rel="icon" rel="icon"
type="image/png" type="image/png"
sizes="16x16" sizes="16x16"
href="/favicon-16x16.png" href={addBasePath('/favicon-16x16.png')}
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-2048-2732.jpg" href={addBasePath('/apple-splash-2048-2732.jpg')}
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-2732-2048.jpg" href={addBasePath('/apple-splash-2732-2048.jpg')}
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-1668-2388.jpg" href={addBasePath('/apple-splash-1668-2388.jpg')}
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-2388-1668.jpg" href={addBasePath('/apple-splash-2388-1668.jpg')}
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-1536-2048.jpg" href={addBasePath('/apple-splash-1536-2048.jpg')}
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-2048-1536.jpg" href={addBasePath('/apple-splash-2048-1536.jpg')}
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-1668-2224.jpg" href={addBasePath('/apple-splash-1668-2224.jpg')}
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-2224-1668.jpg" href={addBasePath('/apple-splash-2224-1668.jpg')}
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-1620-2160.jpg" href={addBasePath('/apple-splash-1620-2160.jpg')}
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-2160-1620.jpg" href={addBasePath('/apple-splash-2160-1620.jpg')}
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-1284-2778.jpg" href={addBasePath('/apple-splash-1284-2778.jpg')}
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-2778-1284.jpg" href={addBasePath('/apple-splash-2778-1284.jpg')}
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-1170-2532.jpg" href={addBasePath('/apple-splash-1170-2532.jpg')}
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-2532-1170.jpg" href={addBasePath('/apple-splash-2532-1170.jpg')}
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-1125-2436.jpg" href={addBasePath('/apple-splash-1125-2436.jpg')}
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-2436-1125.jpg" href={addBasePath('/apple-splash-2436-1125.jpg')}
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-1242-2688.jpg" href={addBasePath('/apple-splash-1242-2688.jpg')}
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-2688-1242.jpg" href={addBasePath('/apple-splash-2688-1242.jpg')}
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-828-1792.jpg" href={addBasePath('/apple-splash-828-1792.jpg')}
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-1792-828.jpg" href={addBasePath('/apple-splash-1792-828.jpg')}
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-1242-2208.jpg" href={addBasePath('/apple-splash-1242-2208.jpg')}
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-2208-1242.jpg" href={addBasePath('/apple-splash-2208-1242.jpg')}
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-750-1334.jpg" href={addBasePath('/apple-splash-750-1334.jpg')}
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-1334-750.jpg" href={addBasePath('/apple-splash-1334-750.jpg')}
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-640-1136.jpg" href={addBasePath('/apple-splash-640-1136.jpg')}
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/> />
<link <link
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
href="/apple-splash-1136-640.jpg" href={addBasePath('/apple-splash-1136-640.jpg')}
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/> />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
@@ -161,7 +162,7 @@ const PWAHeader: React.FC<PWAHeaderProps> = ({ applicationTitle }) => {
/> />
<link <link
rel="manifest" rel="manifest"
href="/site.webmanifest" href={addBasePath('/site.webmanifest')}
crossOrigin="use-credentials" crossOrigin="use-credentials"
/> />
<meta name="application-name" content={applicationTitle ?? 'Overseerr'} /> <meta name="application-name" content={applicationTitle ?? 'Overseerr'} />

View File

@@ -1,7 +1,7 @@
import { UserCircleIcon } from '@heroicons/react/solid'; import { UserCircleIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import React, { useState } from 'react'; import React, { useState } from 'react';
import CachedImage from '../Common/CachedImage'; import CachedImage from '../Common/CachedImage';
import Link from '../Link';
interface PersonCardProps { interface PersonCardProps {
personId: number; personId: number;

View File

@@ -1,5 +1,4 @@
import { groupBy } from 'lodash'; import { groupBy } from 'lodash';
import { useRouter } from 'next/router';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import TruncateMarkup from 'react-truncate-markup'; import TruncateMarkup from 'react-truncate-markup';
@@ -7,6 +6,7 @@ import useSWR from 'swr';
import type { PersonCombinedCreditsResponse } from '../../../server/interfaces/api/personInterfaces'; import type { PersonCombinedCreditsResponse } from '../../../server/interfaces/api/personInterfaces';
import type { PersonDetail } from '../../../server/models/Person'; import type { PersonDetail } from '../../../server/models/Person';
import Ellipsis from '../../assets/ellipsis.svg'; import Ellipsis from '../../assets/ellipsis.svg';
import { useRouter } from '../../hooks/useRouter';
import globalMessages from '../../i18n/globalMessages'; import globalMessages from '../../i18n/globalMessages';
import Error from '../../pages/_error'; import Error from '../../pages/_error';
import CachedImage from '../Common/CachedImage'; import CachedImage from '../Common/CachedImage';

View File

@@ -8,7 +8,6 @@ import {
XIcon, XIcon,
} from '@heroicons/react/solid'; } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import Link from 'next/link';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { MediaRequestStatus } from '../../../server/constants/media'; import { MediaRequestStatus } from '../../../server/constants/media';
@@ -17,6 +16,7 @@ import useRequestOverride from '../../hooks/useRequestOverride';
import globalMessages from '../../i18n/globalMessages'; import globalMessages from '../../i18n/globalMessages';
import Badge from '../Common/Badge'; import Badge from '../Common/Badge';
import Button from '../Common/Button'; import Button from '../Common/Button';
import Link from '../Link';
import RequestModal from '../RequestModal'; import RequestModal from '../RequestModal';
const messages = defineMessages({ const messages = defineMessages({

View File

@@ -6,7 +6,6 @@ import {
XIcon, XIcon,
} from '@heroicons/react/solid'; } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import Link from 'next/link';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useInView } from 'react-intersection-observer'; import { useInView } from 'react-intersection-observer';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
@@ -25,6 +24,7 @@ import { withProperties } from '../../utils/typeHelpers';
import Badge from '../Common/Badge'; import Badge from '../Common/Badge';
import Button from '../Common/Button'; import Button from '../Common/Button';
import CachedImage from '../Common/CachedImage'; import CachedImage from '../Common/CachedImage';
import Link from '../Link';
import RequestModal from '../RequestModal'; import RequestModal from '../RequestModal';
import StatusBadge from '../StatusBadge'; import StatusBadge from '../StatusBadge';

View File

@@ -6,7 +6,6 @@ import {
XIcon, XIcon,
} from '@heroicons/react/solid'; } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import Link from 'next/link';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useInView } from 'react-intersection-observer'; import { useInView } from 'react-intersection-observer';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl'; import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
@@ -25,6 +24,7 @@ import Badge from '../../Common/Badge';
import Button from '../../Common/Button'; import Button from '../../Common/Button';
import CachedImage from '../../Common/CachedImage'; import CachedImage from '../../Common/CachedImage';
import ConfirmButton from '../../Common/ConfirmButton'; import ConfirmButton from '../../Common/ConfirmButton';
import Link from '../../Link';
import RequestModal from '../../RequestModal'; import RequestModal from '../../RequestModal';
import StatusBadge from '../../StatusBadge'; import StatusBadge from '../../StatusBadge';

View File

@@ -4,12 +4,11 @@ import {
FilterIcon, FilterIcon,
SortDescendingIcon, SortDescendingIcon,
} from '@heroicons/react/solid'; } from '@heroicons/react/solid';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces'; import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces';
import { useRouter } from '../../hooks/useRouter';
import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams'; import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams';
import { useUser } from '../../hooks/useUser'; import { useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages'; import globalMessages from '../../i18n/globalMessages';
@@ -17,6 +16,7 @@ import Button from '../Common/Button';
import Header from '../Common/Header'; import Header from '../Common/Header';
import LoadingSpinner from '../Common/LoadingSpinner'; import LoadingSpinner from '../Common/LoadingSpinner';
import PageTitle from '../Common/PageTitle'; import PageTitle from '../Common/PageTitle';
import Link from '../Link';
import RequestItem from './RequestItem'; import RequestItem from './RequestItem';
const messages = defineMessages({ const messages = defineMessages({

View File

@@ -1,9 +1,9 @@
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'; import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { QuotaStatus } from '../../../../server/interfaces/api/userInterfaces'; import { QuotaStatus } from '../../../../server/interfaces/api/userInterfaces';
import ProgressCircle from '../../Common/ProgressCircle'; import ProgressCircle from '../../Common/ProgressCircle';
import Link from '../../Link';
const messages = defineMessages({ const messages = defineMessages({
requestsremaining: requestsremaining:

View File

@@ -5,6 +5,7 @@ import useSWR from 'swr';
import { SonarrSeries } from '../../../../server/api/servarr/sonarr'; import { SonarrSeries } from '../../../../server/api/servarr/sonarr';
import globalMessages from '../../../i18n/globalMessages'; import globalMessages from '../../../i18n/globalMessages';
import Alert from '../../Common/Alert'; import Alert from '../../Common/Alert';
import CachedImage from '../../Common/CachedImage';
import { SmallLoadingSpinner } from '../../Common/LoadingSpinner'; import { SmallLoadingSpinner } from '../../Common/LoadingSpinner';
import Modal from '../../Common/Modal'; import Modal from '../../Common/Modal';
@@ -72,7 +73,7 @@ const SearchByNameModal: React.FC<SearchByNameModalProps> = ({
} `} } `}
> >
<div className="flex items-center flex-none w-24 space-x-4"> <div className="flex items-center flex-none w-24 space-x-4">
<img <CachedImage
src={ src={
item.remotePoster ?? item.remotePoster ??
'/images/overseerr_poster_not_found.png' '/images/overseerr_poster_not_found.png'

View File

@@ -1,14 +1,15 @@
import { ArrowLeftIcon, MailIcon } from '@heroicons/react/solid'; import { ArrowLeftIcon, MailIcon } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import Link from 'next/link';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import * as Yup from 'yup'; import * as Yup from 'yup';
import addBasePath from '../../utils/addBasePath';
import Button from '../Common/Button'; import Button from '../Common/Button';
import ImageFader from '../Common/ImageFader'; import ImageFader from '../Common/ImageFader';
import PageTitle from '../Common/PageTitle'; import PageTitle from '../Common/PageTitle';
import LanguagePicker from '../Layout/LanguagePicker'; import LanguagePicker from '../Layout/LanguagePicker';
import Link from '../Link';
const messages = defineMessages({ const messages = defineMessages({
passwordreset: 'Password Reset', passwordreset: 'Password Reset',
@@ -49,7 +50,11 @@ const ResetPassword: React.FC = () => {
<LanguagePicker /> <LanguagePicker />
</div> </div>
<div className="relative z-40 flex flex-col items-center px-4 mt-10 sm:mx-auto sm:w-full sm:max-w-md"> <div className="relative z-40 flex flex-col items-center px-4 mt-10 sm:mx-auto sm:w-full sm:max-w-md">
<img src="/logo_stacked.svg" className="max-w-full mb-10" alt="Logo" /> <img
src={addBasePath('/logo_stacked.svg')}
className="max-w-full mb-10"
alt="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">
{intl.formatMessage(messages.resetpassword)} {intl.formatMessage(messages.resetpassword)}
</h2> </h2>

View File

@@ -1,16 +1,17 @@
import { SupportIcon } from '@heroicons/react/outline'; import { SupportIcon } from '@heroicons/react/outline';
import axios from 'axios'; import axios from 'axios';
import { Form, Formik } from 'formik'; import { Form, Formik } from 'formik';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { useRouter } from '../../hooks/useRouter';
import globalMessages from '../../i18n/globalMessages'; import globalMessages from '../../i18n/globalMessages';
import addBasePath from '../../utils/addBasePath';
import Button from '../Common/Button'; import Button from '../Common/Button';
import ImageFader from '../Common/ImageFader'; import ImageFader from '../Common/ImageFader';
import SensitiveInput from '../Common/SensitiveInput'; import SensitiveInput from '../Common/SensitiveInput';
import LanguagePicker from '../Layout/LanguagePicker'; import LanguagePicker from '../Layout/LanguagePicker';
import Link from '../Link';
const messages = defineMessages({ const messages = defineMessages({
passwordreset: 'Password Reset', passwordreset: 'Password Reset',
@@ -64,7 +65,11 @@ const ResetPassword: React.FC = () => {
<LanguagePicker /> <LanguagePicker />
</div> </div>
<div className="relative z-40 flex flex-col items-center px-4 mt-10 sm:mx-auto sm:w-full sm:max-w-md"> <div className="relative z-40 flex flex-col items-center px-4 mt-10 sm:mx-auto sm:w-full sm:max-w-md">
<img src="/logo_stacked.svg" className="max-w-full mb-10" alt="Logo" /> <img
src={addBasePath('/logo_stacked.svg')}
className="max-w-full mb-10"
alt="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">
{intl.formatMessage(messages.resetpassword)} {intl.formatMessage(messages.resetpassword)}
</h2> </h2>

View File

@@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { useRouter } from 'next/router';
import { import {
TvResult, TvResult,
MovieResult, MovieResult,
PersonResult, PersonResult,
} from '../../../server/models/Search'; } from '../../../server/models/Search';
import { useRouter } from '../../hooks/useRouter';
import ListView from '../Common/ListView'; import ListView from '../Common/ListView';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import Header from '../Common/Header'; import Header from '../Common/Header';

View File

@@ -3,6 +3,7 @@ import axios from 'axios';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
import { useUser } from '../../hooks/useUser'; import { useUser } from '../../hooks/useUser';
import addBasePath from '../../utils/addBasePath';
const ServiceWorkerSetup: React.FC = () => { const ServiceWorkerSetup: React.FC = () => {
const { currentSettings } = useSettings(); const { currentSettings } = useSettings();
@@ -10,7 +11,7 @@ const ServiceWorkerSetup: React.FC = () => {
useEffect(() => { useEffect(() => {
if ('serviceWorker' in navigator && user?.id) { if ('serviceWorker' in navigator && user?.id) {
navigator.serviceWorker navigator.serviceWorker
.register('/sw.js') .register(addBasePath('/sw.js'))
.then(async (registration) => { .then(async (registration) => {
console.log( console.log(
'[SW] Registration successful, scope is:', '[SW] Registration successful, scope is:',

View File

@@ -3,7 +3,6 @@ import { QuestionMarkCircleIcon, RefreshIcon } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import Link from 'next/link';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
@@ -12,6 +11,7 @@ import * as Yup from 'yup';
import globalMessages from '../../../../i18n/globalMessages'; import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button'; import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner'; import LoadingSpinner from '../../../Common/LoadingSpinner';
import Link from '../../../Link';
import NotificationTypeSelector from '../../../NotificationTypeSelector'; import NotificationTypeSelector from '../../../NotificationTypeSelector';
const JSONEditor = dynamic(() => import('../../../JSONEditor'), { ssr: false }); const JSONEditor = dynamic(() => import('../../../JSONEditor'), { ssr: false });

View File

@@ -8,7 +8,6 @@ import {
PlayIcon, PlayIcon,
} from '@heroicons/react/solid'; } from '@heroicons/react/solid';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
@@ -17,6 +16,7 @@ import {
LogMessage, LogMessage,
LogsResultsResponse, LogsResultsResponse,
} from '../../../../server/interfaces/api/settingsInterfaces'; } from '../../../../server/interfaces/api/settingsInterfaces';
import { useRouter } from '../../../hooks/useRouter';
import { useUpdateQueryParams } from '../../../hooks/useUpdateQueryParams'; import { useUpdateQueryParams } from '../../../hooks/useUpdateQueryParams';
import globalMessages from '../../../i18n/globalMessages'; import globalMessages from '../../../i18n/globalMessages';
import Error from '../../../pages/_error'; import Error from '../../../pages/_error';

View File

@@ -33,6 +33,9 @@ const messages = defineMessages({
apikey: 'API Key', apikey: 'API Key',
applicationTitle: 'Application Title', applicationTitle: 'Application Title',
applicationurl: 'Application URL', applicationurl: 'Application URL',
basePath: 'Base Path',
basePathTip:
'Configure Overseerr to run under a sub-path of a domain (Overseerr must be restarted for changes to take effect)',
region: 'Discover Region', region: 'Discover Region',
regionTip: 'Filter content by regional availability', regionTip: 'Filter content by regional availability',
originallanguage: 'Discover Language', originallanguage: 'Discover Language',
@@ -44,7 +47,7 @@ const messages = defineMessages({
hideAvailable: 'Hide Available Media', hideAvailable: 'Hide Available Media',
csrfProtection: 'Enable CSRF Protection', csrfProtection: 'Enable CSRF Protection',
csrfProtectionTip: csrfProtectionTip:
'Set external API access to read-only (requires HTTPS, and Overseerr must be reloaded for changes to take effect)', 'Set external API access to read-only (requires HTTPS, and Overseerr must be restarted for changes to take effect)',
csrfProtectionHoverTip: csrfProtectionHoverTip:
'Do NOT enable this setting unless you understand what you are doing!', 'Do NOT enable this setting unless you understand what you are doing!',
cacheImages: 'Enable Image Caching', cacheImages: 'Enable Image Caching',
@@ -52,10 +55,12 @@ const messages = defineMessages({
'Optimize and store all images locally (consumes a significant amount of disk space)', 'Optimize and store all images locally (consumes a significant amount of disk space)',
trustProxy: 'Enable Proxy Support', trustProxy: 'Enable Proxy Support',
trustProxyTip: trustProxyTip:
'Allow Overseerr to correctly register client IP addresses behind a proxy (Overseerr must be reloaded for changes to take effect)', 'Allow Overseerr to correctly register client IP addresses behind a proxy (Overseerr must be restarted for changes to take effect)',
validationApplicationTitle: 'You must provide an application title', validationApplicationTitle: 'You must provide an application title',
validationApplicationUrl: 'You must provide a valid URL', validationApplicationUrl: 'You must provide a valid URL',
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash', validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
validationBasePathLeadingSlash: 'Path must begin with a leading slash',
validationBasePathTrailingSlash: 'Path must not end in a trailing slash',
partialRequestsEnabled: 'Allow Partial Series Requests', partialRequestsEnabled: 'Allow Partial Series Requests',
locale: 'Display Language', locale: 'Display Language',
}); });
@@ -81,12 +86,18 @@ const SettingsMain: React.FC = () => {
.test( .test(
'no-trailing-slash', 'no-trailing-slash',
intl.formatMessage(messages.validationApplicationUrlTrailingSlash), intl.formatMessage(messages.validationApplicationUrlTrailingSlash),
(value) => { (value) => !value || !value.endsWith('/')
if (value?.substr(value.length - 1) === '/') { ),
return false; basePath: Yup.string()
} .test(
return true; 'leading-slash',
} intl.formatMessage(messages.validationBasePathLeadingSlash),
(value) => !value || value.startsWith('/')
)
.test(
'no-trailing-slash',
intl.formatMessage(messages.validationBasePathTrailingSlash),
(value) => !value || !value.endsWith('/')
), ),
}); });
@@ -132,6 +143,7 @@ const SettingsMain: React.FC = () => {
initialValues={{ initialValues={{
applicationTitle: data?.applicationTitle, applicationTitle: data?.applicationTitle,
applicationUrl: data?.applicationUrl, applicationUrl: data?.applicationUrl,
basePath: data?.basePath,
csrfProtection: data?.csrfProtection, csrfProtection: data?.csrfProtection,
hideAvailable: data?.hideAvailable, hideAvailable: data?.hideAvailable,
locale: data?.locale ?? 'en', locale: data?.locale ?? 'en',
@@ -147,6 +159,7 @@ const SettingsMain: React.FC = () => {
await axios.post('/api/v1/settings/main', { await axios.post('/api/v1/settings/main', {
applicationTitle: values.applicationTitle, applicationTitle: values.applicationTitle,
applicationUrl: values.applicationUrl, applicationUrl: values.applicationUrl,
basePath: values.basePath,
csrfProtection: values.csrfProtection, csrfProtection: values.csrfProtection,
hideAvailable: values.hideAvailable, hideAvailable: values.hideAvailable,
locale: values.locale, locale: values.locale,
@@ -255,6 +268,27 @@ const SettingsMain: React.FC = () => {
)} )}
</div> </div>
</div> </div>
<div className="form-row">
<label htmlFor="basePath" className="text-label">
<span>{intl.formatMessage(messages.basePath)}</span>
<span className="label-tip">
{intl.formatMessage(messages.basePathTip)}
</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field
id="basePath"
name="basePath"
type="text"
inputMode="url"
/>
</div>
{errors.basePath && touched.basePath && (
<div className="error">{errors.basePath}</div>
)}
</div>
</div>
<div className="form-row"> <div className="form-row">
<label htmlFor="trustProxy" className="checkbox-label"> <label htmlFor="trustProxy" className="checkbox-label">
<span>{intl.formatMessage(messages.trustProxy)}</span> <span>{intl.formatMessage(messages.trustProxy)}</span>

View File

@@ -1,9 +1,10 @@
import axios from 'axios'; import axios from 'axios';
import { useRouter } from 'next/router';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR, { mutate } from 'swr'; import useSWR, { mutate } from 'swr';
import useLocale from '../../hooks/useLocale'; import useLocale from '../../hooks/useLocale';
import { useRouter } from '../../hooks/useRouter';
import addBasePath from '../../utils/addBasePath';
import AppDataWarning from '../AppDataWarning'; import AppDataWarning from '../AppDataWarning';
import Badge from '../Common/Badge'; import Badge from '../Common/Badge';
import Button from '../Common/Button'; import Button from '../Common/Button';
@@ -72,7 +73,7 @@ const Setup: React.FC = () => {
</div> </div>
<div className="relative z-40 px-4 sm:mx-auto sm:w-full sm:max-w-4xl"> <div className="relative z-40 px-4 sm:mx-auto sm:w-full sm:max-w-4xl">
<img <img
src="/logo_stacked.svg" src={addBasePath('/logo_stacked.svg')}
className="max-w-full mb-10 sm:max-w-md sm:mx-auto" className="max-w-full mb-10 sm:max-w-md sm:mx-auto"
alt="Logo" alt="Logo"
/> />

View File

@@ -1,6 +1,5 @@
import { DownloadIcon } from '@heroicons/react/outline'; import { DownloadIcon } from '@heroicons/react/outline';
import { BellIcon, CheckIcon, ClockIcon } from '@heroicons/react/solid'; import { BellIcon, CheckIcon, ClockIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { MediaStatus } from '../../../server/constants/media'; import { MediaStatus } from '../../../server/constants/media';
@@ -12,6 +11,7 @@ import globalMessages from '../../i18n/globalMessages';
import { withProperties } from '../../utils/typeHelpers'; import { withProperties } from '../../utils/typeHelpers';
import Button from '../Common/Button'; import Button from '../Common/Button';
import CachedImage from '../Common/CachedImage'; import CachedImage from '../Common/CachedImage';
import Link from '../Link';
import RequestModal from '../RequestModal'; import RequestModal from '../RequestModal';
import Transition from '../Transition'; import Transition from '../Transition';
import Placeholder from './Placeholder'; import Placeholder from './Placeholder';

View File

@@ -1,13 +1,13 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
import type { TvDetails } from '../../../../server/models/Tv'; import type { TvDetails } from '../../../../server/models/Tv';
import { useRouter } from '../../../hooks/useRouter';
import Error from '../../../pages/_error'; 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 PageTitle from '../../Common/PageTitle'; import PageTitle from '../../Common/PageTitle';
import Link from '../../Link';
import PersonCard from '../../PersonCard'; import PersonCard from '../../PersonCard';
const messages = defineMessages({ const messages = defineMessages({

View File

@@ -1,13 +1,13 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
import type { TvDetails } from '../../../../server/models/Tv'; import type { TvDetails } from '../../../../server/models/Tv';
import { useRouter } from '../../../hooks/useRouter';
import Error from '../../../pages/_error'; 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 PageTitle from '../../Common/PageTitle'; import PageTitle from '../../Common/PageTitle';
import Link from '../../Link';
import PersonCard from '../../PersonCard'; import PersonCard from '../../PersonCard';
const messages = defineMessages({ const messages = defineMessages({

View File

@@ -1,15 +1,15 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
import type { TvResult } from '../../../server/models/Search'; import type { TvResult } from '../../../server/models/Search';
import { TvDetails } from '../../../server/models/Tv'; import { TvDetails } from '../../../server/models/Tv';
import useDiscover from '../../hooks/useDiscover'; import useDiscover from '../../hooks/useDiscover';
import { useRouter } from '../../hooks/useRouter';
import Error from '../../pages/_error'; import Error from '../../pages/_error';
import Header from '../Common/Header'; import Header from '../Common/Header';
import ListView from '../Common/ListView'; import ListView from '../Common/ListView';
import PageTitle from '../Common/PageTitle'; import PageTitle from '../Common/PageTitle';
import Link from '../Link';
const messages = defineMessages({ const messages = defineMessages({
recommendations: 'Recommendations', recommendations: 'Recommendations',

View File

@@ -1,15 +1,15 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
import type { TvResult } from '../../../server/models/Search'; import type { TvResult } from '../../../server/models/Search';
import type { TvDetails } from '../../../server/models/Tv'; import type { TvDetails } from '../../../server/models/Tv';
import useDiscover from '../../hooks/useDiscover'; import useDiscover from '../../hooks/useDiscover';
import { useRouter } from '../../hooks/useRouter';
import Error from '../../pages/_error'; import Error from '../../pages/_error';
import Header from '../Common/Header'; import Header from '../Common/Header';
import ListView from '../Common/ListView'; import ListView from '../Common/ListView';
import PageTitle from '../Common/PageTitle'; import PageTitle from '../Common/PageTitle';
import Link from '../Link';
const messages = defineMessages({ const messages = defineMessages({
similar: 'Similar Series', similar: 'Similar Series',

View File

@@ -7,8 +7,6 @@ import {
} from '@heroicons/react/outline'; } from '@heroicons/react/outline';
import { hasFlag } from 'country-flag-icons'; import { hasFlag } from 'country-flag-icons';
import 'country-flag-icons/3x2/flags.css'; import 'country-flag-icons/3x2/flags.css';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
@@ -24,6 +22,7 @@ import RTFresh from '../../assets/rt_fresh.svg';
import RTRotten from '../../assets/rt_rotten.svg'; import RTRotten from '../../assets/rt_rotten.svg';
import TmdbLogo from '../../assets/tmdb_logo.svg'; import TmdbLogo from '../../assets/tmdb_logo.svg';
import useLocale from '../../hooks/useLocale'; import useLocale from '../../hooks/useLocale';
import { useRouter } from '../../hooks/useRouter';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
import { Permission, useUser } from '../../hooks/useUser'; import { Permission, useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages'; import globalMessages from '../../i18n/globalMessages';
@@ -36,6 +35,7 @@ import PageTitle from '../Common/PageTitle';
import PlayButton, { PlayButtonLink } from '../Common/PlayButton'; import PlayButton, { PlayButtonLink } from '../Common/PlayButton';
import ExternalLinkBlock from '../ExternalLinkBlock'; import ExternalLinkBlock from '../ExternalLinkBlock';
import IssueModal from '../IssueModal'; import IssueModal from '../IssueModal';
import Link from '../Link';
import ManageSlideOver from '../ManageSlideOver'; import ManageSlideOver from '../ManageSlideOver';
import MediaSlider from '../MediaSlider'; import MediaSlider from '../MediaSlider';
import PersonCard from '../PersonCard'; import PersonCard from '../PersonCard';

View File

@@ -9,8 +9,6 @@ import {
} from '@heroicons/react/solid'; } from '@heroicons/react/solid';
import axios from 'axios'; import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
@@ -18,6 +16,7 @@ import useSWR from 'swr';
import * as Yup from 'yup'; import * as Yup from 'yup';
import type { UserResultsResponse } from '../../../server/interfaces/api/userInterfaces'; import type { UserResultsResponse } from '../../../server/interfaces/api/userInterfaces';
import { hasPermission } from '../../../server/lib/permissions'; import { hasPermission } from '../../../server/lib/permissions';
import { useRouter } from '../../hooks/useRouter';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams'; import { useUpdateQueryParams } from '../../hooks/useUpdateQueryParams';
import { Permission, User, UserType, useUser } from '../../hooks/useUser'; import { Permission, User, UserType, useUser } from '../../hooks/useUser';
@@ -31,6 +30,7 @@ import Modal from '../Common/Modal';
import PageTitle from '../Common/PageTitle'; import PageTitle from '../Common/PageTitle';
import SensitiveInput from '../Common/SensitiveInput'; import SensitiveInput from '../Common/SensitiveInput';
import Table from '../Common/Table'; import Table from '../Common/Table';
import Link from '../Link';
import Transition from '../Transition'; import Transition from '../Transition';
import BulkEditModal from './BulkEditModal'; import BulkEditModal from './BulkEditModal';

View File

@@ -1,9 +1,9 @@
import { CogIcon, UserIcon } from '@heroicons/react/solid'; import { CogIcon, UserIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { Permission, User, useUser } from '../../../hooks/useUser'; import { Permission, User, useUser } from '../../../hooks/useUser';
import Button from '../../Common/Button'; import Button from '../../Common/Button';
import Link from '../../Link';
const messages = defineMessages({ const messages = defineMessages({
settings: 'Edit Settings', settings: 'Edit Settings',

View File

@@ -1,7 +1,6 @@
import { SaveIcon } from '@heroicons/react/outline'; import { SaveIcon } from '@heroicons/react/outline';
import axios from 'axios'; import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
@@ -12,6 +11,7 @@ import {
AvailableLocale, AvailableLocale,
} from '../../../../context/LanguageContext'; } from '../../../../context/LanguageContext';
import useLocale from '../../../../hooks/useLocale'; import useLocale from '../../../../hooks/useLocale';
import { useRouter } from '../../../../hooks/useRouter';
import useSettings from '../../../../hooks/useSettings'; import useSettings from '../../../../hooks/useSettings';
import { Permission, UserType, useUser } from '../../../../hooks/useUser'; import { Permission, UserType, useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages'; import globalMessages from '../../../../i18n/globalMessages';

View File

@@ -1,13 +1,13 @@
import { SaveIcon } from '@heroicons/react/outline'; import { SaveIcon } from '@heroicons/react/outline';
import axios from 'axios'; import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces'; import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import { useRouter } from '../../../../hooks/useRouter';
import { useUser } from '../../../../hooks/useUser'; import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages'; import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button'; import Button from '../../../Common/Button';

View File

@@ -1,13 +1,13 @@
import { SaveIcon } from '@heroicons/react/outline'; import { SaveIcon } from '@heroicons/react/outline';
import axios from 'axios'; import axios from 'axios';
import { Form, Formik } from 'formik'; import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces'; import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import { useRouter } from '../../../../hooks/useRouter';
import { useUser } from '../../../../hooks/useUser'; import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages'; import globalMessages from '../../../../i18n/globalMessages';
import Badge from '../../../Common/Badge'; import Badge from '../../../Common/Badge';

View File

@@ -1,12 +1,12 @@
import axios from 'axios'; import axios from 'axios';
import { Form, Formik } from 'formik'; import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces'; import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import { useRouter } from '../../../../hooks/useRouter';
import { useUser } from '../../../../hooks/useUser'; import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages'; import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button'; import Button from '../../../Common/Button';

View File

@@ -1,12 +1,12 @@
import axios from 'axios'; import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces'; import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import { useRouter } from '../../../../hooks/useRouter';
import useSettings from '../../../../hooks/useSettings'; import useSettings from '../../../../hooks/useSettings';
import { useUser } from '../../../../hooks/useUser'; import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages'; import globalMessages from '../../../../i18n/globalMessages';

View File

@@ -1,13 +1,13 @@
import { SaveIcon } from '@heroicons/react/outline'; import { SaveIcon } from '@heroicons/react/outline';
import axios from 'axios'; import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces'; import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import { useRouter } from '../../../../hooks/useRouter';
import { useUser } from '../../../../hooks/useUser'; import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages'; import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button'; import Button from '../../../Common/Button';

View File

@@ -1,12 +1,12 @@
import { SaveIcon } from '@heroicons/react/outline'; import { SaveIcon } from '@heroicons/react/outline';
import axios from 'axios'; import axios from 'axios';
import { Form, Formik } from 'formik'; import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR, { mutate } from 'swr'; import useSWR, { mutate } from 'swr';
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces'; import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import { useRouter } from '../../../../hooks/useRouter';
import { useUser } from '../../../../hooks/useUser'; import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages'; import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button'; import Button from '../../../Common/Button';

View File

@@ -1,5 +1,4 @@
import { CloudIcon, MailIcon } from '@heroicons/react/solid'; import { CloudIcon, MailIcon } from '@heroicons/react/solid';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
@@ -8,6 +7,7 @@ import DiscordLogo from '../../../../assets/extlogos/discord.svg';
import PushbulletLogo from '../../../../assets/extlogos/pushbullet.svg'; import PushbulletLogo from '../../../../assets/extlogos/pushbullet.svg';
import PushoverLogo from '../../../../assets/extlogos/pushover.svg'; import PushoverLogo from '../../../../assets/extlogos/pushover.svg';
import TelegramLogo from '../../../../assets/extlogos/telegram.svg'; import TelegramLogo from '../../../../assets/extlogos/telegram.svg';
import { useRouter } from '../../../../hooks/useRouter';
import { useUser } from '../../../../hooks/useUser'; import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages'; import globalMessages from '../../../../i18n/globalMessages';
import Error from '../../../../pages/_error'; import Error from '../../../../pages/_error';

View File

@@ -1,12 +1,12 @@
import { SaveIcon } from '@heroicons/react/outline'; import { SaveIcon } from '@heroicons/react/outline';
import axios from 'axios'; import axios from 'axios';
import { Form, Formik } from 'formik'; import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { useRouter } from '../../../../hooks/useRouter';
import { Permission, useUser } from '../../../../hooks/useUser'; import { Permission, useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages'; import globalMessages from '../../../../i18n/globalMessages';
import Error from '../../../../pages/_error'; import Error from '../../../../pages/_error';

View File

@@ -1,11 +1,11 @@
import { SaveIcon } from '@heroicons/react/outline'; import { SaveIcon } from '@heroicons/react/outline';
import axios from 'axios'; import axios from 'axios';
import { Form, Formik } from 'formik'; import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
import { useRouter } from '../../../../hooks/useRouter';
import { useUser } from '../../../../hooks/useUser'; import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages'; import globalMessages from '../../../../i18n/globalMessages';
import Error from '../../../../pages/_error'; import Error from '../../../../pages/_error';

View File

@@ -1,9 +1,9 @@
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
import { UserSettingsNotificationsResponse } from '../../../../server/interfaces/api/userSettingsInterfaces'; import { UserSettingsNotificationsResponse } from '../../../../server/interfaces/api/userSettingsInterfaces';
import { hasPermission, Permission } from '../../../../server/lib/permissions'; import { hasPermission, Permission } from '../../../../server/lib/permissions';
import { useRouter } from '../../../hooks/useRouter';
import useSettings from '../../../hooks/useSettings'; import useSettings from '../../../hooks/useSettings';
import { useUser } from '../../../hooks/useUser'; import { useUser } from '../../../hooks/useUser';
import globalMessages from '../../../i18n/globalMessages'; import globalMessages from '../../../i18n/globalMessages';

View File

@@ -1,6 +1,4 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline'; import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
@@ -10,12 +8,14 @@ import {
} from '../../../server/interfaces/api/userInterfaces'; } from '../../../server/interfaces/api/userInterfaces';
import { MovieDetails } from '../../../server/models/Movie'; import { MovieDetails } from '../../../server/models/Movie';
import { TvDetails } from '../../../server/models/Tv'; import { TvDetails } from '../../../server/models/Tv';
import { useRouter } from '../../hooks/useRouter';
import { Permission, useUser } from '../../hooks/useUser'; import { Permission, useUser } from '../../hooks/useUser';
import Error from '../../pages/_error'; import Error from '../../pages/_error';
import ImageFader from '../Common/ImageFader'; import ImageFader from '../Common/ImageFader';
import LoadingSpinner from '../Common/LoadingSpinner'; import LoadingSpinner from '../Common/LoadingSpinner';
import PageTitle from '../Common/PageTitle'; import PageTitle from '../Common/PageTitle';
import ProgressCircle from '../Common/ProgressCircle'; import ProgressCircle from '../Common/ProgressCircle';
import Link from '../Link';
import RequestCard from '../RequestCard'; import RequestCard from '../RequestCard';
import Slider from '../Slider'; import Slider from '../Slider';
import ProfileHeader from './ProfileHeader'; import ProfileHeader from './ProfileHeader';

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { useUser, User } from '../hooks/useUser'; import { useUser, User } from '../hooks/useUser';
import { useRouter } from 'next/dist/client/router'; import { useRouter } from 'next/dist/client/router';
import addBasePath from '../utils/addBasePath';
interface UserContextProps { interface UserContextProps {
initialUser: User; initialUser: User;
@@ -30,7 +31,7 @@ export const UserContext: React.FC<UserContextProps> = ({
!routing.current !routing.current
) { ) {
routing.current = true; routing.current = true;
location.href = '/login'; location.href = addBasePath('/login');
} }
}, [router, user, error]); }, [router, user, error]);

View File

@@ -1,5 +1,5 @@
import { useRouter } from 'next/router';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useRouter } from './useRouter';
import { Permission, PermissionCheckOptions, useUser } from './useUser'; import { Permission, PermissionCheckOptions, useUser } from './useUser';
const useRouteGuard = ( const useRouteGuard = (

67
src/hooks/useRouter.ts Normal file
View File

@@ -0,0 +1,67 @@
import { NextRouter, useRouter as useNextRouter } from 'next/router';
import { useMemo } from 'react';
import addBasePath from '../utils/addBasePath';
const basePath = addBasePath('');
const urlPropertyFields = [
'pathname',
'events',
'route',
'query',
'asPath',
'components',
'isFallback',
'basePath',
'locale',
'locales',
'defaultLocale',
'isReady',
'isPreview',
'isLocaleDomain',
'domainLocales',
];
const coreMethodFields = ['reload', 'back', 'prefetch', 'beforePopState'];
const wrapRouter = (target: any): NextRouter => {
const router = {
push(url: URL, as?: URL, options?: never) {
return target.push(
addBasePath(url),
as ? addBasePath(as) : undefined,
options
);
},
replace(url: URL, as?: URL, options?: never) {
return target.push(
addBasePath(url),
as ? addBasePath(as) : undefined,
options
);
},
get basePath() {
return basePath;
},
};
urlPropertyFields.forEach((field: string) => {
Object.defineProperty(router, field, {
get() {
return target[field] as string;
},
});
});
coreMethodFields.forEach((field: string) => {
(router as any)[field] = (...args: any[]) => {
return target[field](...args);
};
});
return router as NextRouter;
};
export const useRouter = (): NextRouter => {
const router = useNextRouter();
return useMemo(() => wrapRouter(router), [router]);
};

View File

@@ -2,8 +2,8 @@
import type { UrlObject } from 'url'; import type { UrlObject } from 'url';
import { useEffect, useState, Dispatch, SetStateAction } from 'react'; import { useEffect, useState, Dispatch, SetStateAction } from 'react';
import useDebouncedState from './useDebouncedState'; import useDebouncedState from './useDebouncedState';
import { useRouter } from 'next/router';
import type { Nullable } from '../utils/typeHelpers'; import type { Nullable } from '../utils/typeHelpers';
import { useRouter } from './useRouter';
type Url = string | UrlObject; type Url = string | UrlObject;

View File

@@ -1,6 +1,7 @@
import { NextRouter, useRouter } from 'next/router'; import { NextRouter } from 'next/router';
import { ParsedUrlQuery } from 'querystring'; import { ParsedUrlQuery } from 'querystring';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useRouter } from './useRouter';
type UseQueryParamReturnedFunction = ( type UseQueryParamReturnedFunction = (
query: ParsedUrlQuery, query: ParsedUrlQuery,

View File

@@ -680,13 +680,15 @@
"components.Settings.apikey": "API Key", "components.Settings.apikey": "API Key",
"components.Settings.applicationTitle": "Application Title", "components.Settings.applicationTitle": "Application Title",
"components.Settings.applicationurl": "Application URL", "components.Settings.applicationurl": "Application URL",
"components.Settings.basePath": "Base Path",
"components.Settings.basePathTip": "Configure Overseerr to run under a sub-path of a domain (Overseerr must be restarted for changes to take effect)",
"components.Settings.cacheImages": "Enable Image Caching", "components.Settings.cacheImages": "Enable Image Caching",
"components.Settings.cacheImagesTip": "Optimize and store all images locally (consumes a significant amount of disk space)", "components.Settings.cacheImagesTip": "Optimize and store all images locally (consumes a significant amount of disk space)",
"components.Settings.cancelscan": "Cancel Scan", "components.Settings.cancelscan": "Cancel Scan",
"components.Settings.copied": "Copied API key to clipboard.", "components.Settings.copied": "Copied API key to clipboard.",
"components.Settings.csrfProtection": "Enable CSRF Protection", "components.Settings.csrfProtection": "Enable CSRF Protection",
"components.Settings.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!", "components.Settings.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!",
"components.Settings.csrfProtectionTip": "Set external API access to read-only (requires HTTPS, and Overseerr must be reloaded for changes to take effect)", "components.Settings.csrfProtectionTip": "Set external API access to read-only (requires HTTPS, and Overseerr must be restarted for changes to take effect)",
"components.Settings.currentlibrary": "Current Library: {name}", "components.Settings.currentlibrary": "Current Library: {name}",
"components.Settings.default": "Default", "components.Settings.default": "Default",
"components.Settings.default4k": "Default 4K", "components.Settings.default4k": "Default 4K",
@@ -758,10 +760,12 @@
"components.Settings.toastSettingsFailure": "Something went wrong while saving settings.", "components.Settings.toastSettingsFailure": "Something went wrong while saving settings.",
"components.Settings.toastSettingsSuccess": "Settings saved successfully!", "components.Settings.toastSettingsSuccess": "Settings saved successfully!",
"components.Settings.trustProxy": "Enable Proxy Support", "components.Settings.trustProxy": "Enable Proxy Support",
"components.Settings.trustProxyTip": "Allow Overseerr to correctly register client IP addresses behind a proxy (Overseerr must be reloaded for changes to take effect)", "components.Settings.trustProxyTip": "Allow Overseerr to correctly register client IP addresses behind a proxy (Overseerr must be restarted for changes to take effect)",
"components.Settings.validationApplicationTitle": "You must provide an application title", "components.Settings.validationApplicationTitle": "You must provide an application title",
"components.Settings.validationApplicationUrl": "You must provide a valid URL", "components.Settings.validationApplicationUrl": "You must provide a valid URL",
"components.Settings.validationApplicationUrlTrailingSlash": "URL must not end in a trailing slash", "components.Settings.validationApplicationUrlTrailingSlash": "URL must not end in a trailing slash",
"components.Settings.validationBasePathLeadingSlash": "Path must begin with a leading slash",
"components.Settings.validationBasePathTrailingSlash": "Path must not end in a trailing slash",
"components.Settings.validationHostnameRequired": "You must provide a valid hostname or IP address", "components.Settings.validationHostnameRequired": "You must provide a valid hostname or IP address",
"components.Settings.validationPortRequired": "You must provide a valid port number", "components.Settings.validationPortRequired": "You must provide a valid port number",
"components.Settings.validationWebAppUrl": "You must provide a valid Plex Web App URL", "components.Settings.validationWebAppUrl": "You must provide a valid Plex Web App URL",

View File

@@ -1,8 +1,8 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline'; import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import PageTitle from '../components/Common/PageTitle'; import PageTitle from '../components/Common/PageTitle';
import Link from '../components/Link';
const messages = defineMessages({ const messages = defineMessages({
errormessagewithcode: '{statusCode} - {error}', errormessagewithcode: '{statusCode} - {error}',

View File

@@ -19,7 +19,9 @@ import { SettingsProvider } from '../context/SettingsContext';
import { UserContext } from '../context/UserContext'; import { UserContext } from '../context/UserContext';
import { User } from '../hooks/useUser'; import { User } from '../hooks/useUser';
import '../styles/globals.css'; import '../styles/globals.css';
import addBasePath from '../utils/addBasePath';
const basePath = addBasePath('');
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const loadLocaleData = (locale: AvailableLocale): Promise<any> => { const loadLocaleData = (locale: AvailableLocale): Promise<any> => {
switch (locale) { switch (locale) {
@@ -85,6 +87,10 @@ if (typeof window === 'undefined') {
global.Intl = require('intl'); global.Intl = require('intl');
} }
if (basePath) {
axios.defaults.baseURL = basePath;
}
const CoreApp: Omit<NextAppComponentType, 'origGetInitialProps'> = ({ const CoreApp: Omit<NextAppComponentType, 'origGetInitialProps'> = ({
Component, Component,
pageProps, pageProps,
@@ -172,9 +178,10 @@ CoreApp.getInitialProps = async (initialProps) => {
}; };
if (ctx.res) { if (ctx.res) {
const serverUrl = `http://localhost:${process.env.PORT || 5055}${basePath}`;
// Check if app is initialized and redirect if necessary // Check if app is initialized and redirect if necessary
const response = await axios.get<PublicSettingsResponse>( const response = await axios.get<PublicSettingsResponse>(
`http://localhost:${process.env.PORT || 5055}/api/v1/settings/public` `${serverUrl}/api/v1/settings/public`
); );
currentSettings = response.data; currentSettings = response.data;
@@ -184,22 +191,21 @@ CoreApp.getInitialProps = async (initialProps) => {
if (!initialized) { if (!initialized) {
if (!router.pathname.match(/(setup|login\/plex)/)) { if (!router.pathname.match(/(setup|login\/plex)/)) {
ctx.res.writeHead(307, { ctx.res.writeHead(307, {
Location: '/setup', Location: addBasePath('/setup'),
}); });
ctx.res.end(); ctx.res.end();
} }
} else { } else {
try { try {
// Attempt to get the user by running a request to the local api // Attempt to get the user by running a request to the local api
const response = await axios.get<User>( const response = await axios.get<User>(`${serverUrl}/api/v1/auth/me`, {
`http://localhost:${process.env.PORT || 5055}/api/v1/auth/me`, headers: ctx.req ? { cookie: ctx.req.headers.cookie } : undefined,
{ headers: ctx.req ? { cookie: ctx.req.headers.cookie } : undefined } });
);
user = response.data; user = response.data;
if (router.pathname.match(/(setup|login)/)) { if (router.pathname.match(/(setup|login)/)) {
ctx.res.writeHead(307, { ctx.res.writeHead(307, {
Location: '/', Location: addBasePath('/'),
}); });
ctx.res.end(); ctx.res.end();
} }
@@ -209,7 +215,7 @@ CoreApp.getInitialProps = async (initialProps) => {
// before anything actually renders // before anything actually renders
if (!router.pathname.match(/(login|setup|resetpassword)/)) { if (!router.pathname.match(/(login|setup|resetpassword)/)) {
ctx.res.writeHead(307, { ctx.res.writeHead(307, {
Location: '/login', Location: addBasePath('/login'),
}); });
ctx.res.end(); ctx.res.end();
} }
@@ -223,7 +229,7 @@ CoreApp.getInitialProps = async (initialProps) => {
); );
const locale = user?.settings?.locale const locale = user?.settings?.locale
? user.settings.locale ? user?.settings.locale
: currentSettings.locale; : currentSettings.locale;
const messages = await loadLocaleData(locale as AvailableLocale); const messages = await loadLocaleData(locale as AvailableLocale);

View File

@@ -1,9 +1,9 @@
import { ArrowCircleRightIcon } from '@heroicons/react/outline'; import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Link from 'next/link';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import PageTitle from '../components/Common/PageTitle'; import PageTitle from '../components/Common/PageTitle';
import Link from '../components/Link';
import type { Undefinable } from '../utils/typeHelpers'; import type { Undefinable } from '../utils/typeHelpers';
interface ErrorProps { interface ErrorProps {

View File

@@ -3,6 +3,7 @@ import { GetServerSideProps, NextPage } from 'next';
import React from 'react'; import React from 'react';
import type { Collection } from '../../../../server/models/Collection'; import type { Collection } from '../../../../server/models/Collection';
import CollectionDetails from '../../../components/CollectionDetails'; import CollectionDetails from '../../../components/CollectionDetails';
import addBasePath from '../../../utils/addBasePath';
interface CollectionPageProps { interface CollectionPageProps {
collection?: Collection; collection?: Collection;
@@ -12,12 +13,14 @@ const CollectionPage: NextPage<CollectionPageProps> = ({ collection }) => {
return <CollectionDetails collection={collection} />; return <CollectionDetails collection={collection} />;
}; };
const basePath = addBasePath('');
export const getServerSideProps: GetServerSideProps<CollectionPageProps> = export const getServerSideProps: GetServerSideProps<CollectionPageProps> =
async (ctx) => { async (ctx) => {
const response = await axios.get<Collection>( const response = await axios.get<Collection>(
`http://localhost:${process.env.PORT || 5055}/api/v1/collection/${ `http://localhost:${
ctx.query.collectionId process.env.PORT || 5055
}`, }${basePath}/api/v1/collection/${ctx.query.collectionId}`,
{ {
headers: ctx.req?.headers?.cookie headers: ctx.req?.headers?.cookie
? { cookie: ctx.req.headers.cookie } ? { cookie: ctx.req.headers.cookie }

View File

@@ -3,11 +3,14 @@ import { NextPage } from 'next';
import React from 'react'; import React from 'react';
import type { MovieDetails as MovieDetailsType } from '../../../../server/models/Movie'; import type { MovieDetails as MovieDetailsType } from '../../../../server/models/Movie';
import MovieDetails from '../../../components/MovieDetails'; import MovieDetails from '../../../components/MovieDetails';
import addBasePath from '../../../utils/addBasePath';
interface MoviePageProps { interface MoviePageProps {
movie?: MovieDetailsType; movie?: MovieDetailsType;
} }
const basePath = addBasePath('');
const MoviePage: NextPage<MoviePageProps> = ({ movie }) => { const MoviePage: NextPage<MoviePageProps> = ({ movie }) => {
return <MovieDetails movie={movie} />; return <MovieDetails movie={movie} />;
}; };
@@ -15,7 +18,7 @@ const MoviePage: NextPage<MoviePageProps> = ({ movie }) => {
MoviePage.getInitialProps = async (ctx) => { MoviePage.getInitialProps = async (ctx) => {
if (ctx.req) { if (ctx.req) {
const response = await axios.get<MovieDetailsType>( const response = await axios.get<MovieDetailsType>(
`http://localhost:${process.env.PORT || 5055}/api/v1/movie/${ `http://localhost:${process.env.PORT || 5055}${basePath}/api/v1/movie/${
ctx.query.movieId ctx.query.movieId
}`, }`,
{ {

View File

@@ -3,11 +3,14 @@ import { NextPage } from 'next';
import React from 'react'; import React from 'react';
import type { TvDetails as TvDetailsType } from '../../../../server/models/Tv'; import type { TvDetails as TvDetailsType } from '../../../../server/models/Tv';
import TvDetails from '../../../components/TvDetails'; import TvDetails from '../../../components/TvDetails';
import addBasePath from '../../../utils/addBasePath';
interface TvPageProps { interface TvPageProps {
tv?: TvDetailsType; tv?: TvDetailsType;
} }
const basePath = addBasePath('');
const TvPage: NextPage<TvPageProps> = ({ tv }) => { const TvPage: NextPage<TvPageProps> = ({ tv }) => {
return <TvDetails tv={tv} />; return <TvDetails tv={tv} />;
}; };
@@ -15,7 +18,7 @@ const TvPage: NextPage<TvPageProps> = ({ tv }) => {
TvPage.getInitialProps = async (ctx) => { TvPage.getInitialProps = async (ctx) => {
if (ctx.req) { if (ctx.req) {
const response = await axios.get<TvDetailsType>( const response = await axios.get<TvDetailsType>(
`http://localhost:${process.env.PORT || 5055}/api/v1/tv/${ `http://localhost:${process.env.PORT || 5055}${basePath}/api/v1/tv/${
ctx.query.tvId ctx.query.tvId
}`, }`,
{ {

20
src/utils/addBasePath.ts Normal file
View File

@@ -0,0 +1,20 @@
import getConfig from 'next/config';
import { UrlObject } from 'url';
const {
publicRuntimeConfig: { basePath = '' },
} = getConfig();
declare type Url = string | UrlObject;
export default function addBasePath(url: UrlObject): UrlObject;
export default function addBasePath(url: string): string;
export default function addBasePath(url: Url): Url;
export default function addBasePath(url: Url): Url {
if (typeof url === 'string') {
return basePath + url;
}
url.pathname = basePath + url.pathname;
return url;
}

View File

@@ -1,5 +1,6 @@
import axios from 'axios'; import axios from 'axios';
import Bowser from 'bowser'; import Bowser from 'bowser';
import addBasePath from './addBasePath';
interface PlexHeaders { interface PlexHeaders {
Accept: string; Accept: string;
@@ -177,7 +178,7 @@ class PlexOAuth {
//Set url to login/plex/loading so browser doesn't block popup //Set url to login/plex/loading so browser doesn't block popup
const newWindow = window.open( const newWindow = window.open(
'/login/plex/loading', addBasePath('/login/plex/loading'),
title, title,
'scrollbars=yes, width=' + 'scrollbars=yes, width=' +
w + w +