43 | ) => {
44 | const skey = String(key);
45 | await this.acquire(skey);
46 | try {
47 | await callback();
48 | } finally {
49 | this.release(skey);
50 | }
51 | };
52 | }
53 |
54 | export default AsyncLock;
55 |
--------------------------------------------------------------------------------
/server/utils/dateHelpers.ts:
--------------------------------------------------------------------------------
1 | import { addYears } from 'date-fns';
2 | import { Between } from 'typeorm';
3 |
4 | export const AfterDate = (date: Date) => Between(date, addYears(date, 100));
5 |
--------------------------------------------------------------------------------
/server/utils/restartFlag.ts:
--------------------------------------------------------------------------------
1 | import type { MainSettings } from '@server/lib/settings';
2 | import { getSettings } from '@server/lib/settings';
3 |
4 | class RestartFlag {
5 | private settings: MainSettings;
6 |
7 | public initializeSettings(settings: MainSettings): void {
8 | this.settings = { ...settings };
9 | }
10 |
11 | public isSet(): boolean {
12 | const settings = getSettings().main;
13 |
14 | return (
15 | this.settings.csrfProtection !== settings.csrfProtection ||
16 | this.settings.trustProxy !== settings.trustProxy
17 | );
18 | }
19 | }
20 |
21 | const restartFlag = new RestartFlag();
22 |
23 | export default restartFlag;
24 |
--------------------------------------------------------------------------------
/server/utils/typeHelpers.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | TmdbCollectionResult,
3 | TmdbMovieDetails,
4 | TmdbMovieResult,
5 | TmdbPersonDetails,
6 | TmdbPersonResult,
7 | TmdbTvDetails,
8 | TmdbTvResult,
9 | } from '@server/api/themoviedb/interfaces';
10 |
11 | export const isMovie = (
12 | movie:
13 | | TmdbMovieResult
14 | | TmdbTvResult
15 | | TmdbPersonResult
16 | | TmdbCollectionResult
17 | ): movie is TmdbMovieResult => {
18 | return (movie as TmdbMovieResult).title !== undefined;
19 | };
20 |
21 | export const isPerson = (
22 | person:
23 | | TmdbMovieResult
24 | | TmdbTvResult
25 | | TmdbPersonResult
26 | | TmdbCollectionResult
27 | ): person is TmdbPersonResult => {
28 | return (person as TmdbPersonResult).known_for !== undefined;
29 | };
30 |
31 | export const isCollection = (
32 | collection:
33 | | TmdbMovieResult
34 | | TmdbTvResult
35 | | TmdbPersonResult
36 | | TmdbCollectionResult
37 | ): collection is TmdbCollectionResult => {
38 | return (collection as TmdbCollectionResult).media_type === 'collection';
39 | };
40 |
41 | export const isMovieDetails = (
42 | movie: TmdbMovieDetails | TmdbTvDetails | TmdbPersonDetails
43 | ): movie is TmdbMovieDetails => {
44 | return (movie as TmdbMovieDetails).title !== undefined;
45 | };
46 |
47 | export const isTvDetails = (
48 | tv: TmdbMovieDetails | TmdbTvDetails | TmdbPersonDetails
49 | ): tv is TmdbTvDetails => {
50 | return (tv as TmdbTvDetails).number_of_seasons !== undefined;
51 | };
52 |
--------------------------------------------------------------------------------
/src/assets/ellipsis.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/extlogos/discord.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/extlogos/lunasea.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/extlogos/pushbullet.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/extlogos/pushover.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/extlogos/slack.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/extlogos/telegram.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/infinity.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/rt_fresh.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/rt_rotten.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/services/imdb.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/services/radarr.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/services/trakt.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/spinner.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/tmdb_logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/AirDateBadge/index.tsx:
--------------------------------------------------------------------------------
1 | import Badge from '@app/components/Common/Badge';
2 | import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
3 |
4 | const messages = defineMessages({
5 | airedrelative: 'Aired {relativeTime}',
6 | airsrelative: 'Airing {relativeTime}',
7 | });
8 |
9 | type AirDateBadgeProps = {
10 | airDate: string;
11 | };
12 |
13 | const AirDateBadge = ({ airDate }: AirDateBadgeProps) => {
14 | const WEEK = 1000 * 60 * 60 * 24 * 8;
15 | const intl = useIntl();
16 | const dAirDate = new Date(airDate);
17 | const nowDate = new Date();
18 | const alreadyAired = dAirDate.getTime() < nowDate.getTime();
19 |
20 | const compareWeek = new Date(
21 | alreadyAired ? Date.now() - WEEK : Date.now() + WEEK
22 | );
23 |
24 | let showRelative = false;
25 |
26 | if (
27 | (alreadyAired && dAirDate.getTime() > compareWeek.getTime()) ||
28 | (!alreadyAired && dAirDate.getTime() < compareWeek.getTime())
29 | ) {
30 | showRelative = true;
31 | }
32 |
33 | return (
34 |
35 |
36 | {intl.formatDate(dAirDate, {
37 | year: 'numeric',
38 | month: 'long',
39 | day: 'numeric',
40 | timeZone: 'UTC',
41 | })}
42 |
43 | {showRelative && (
44 |
45 | {intl.formatMessage(
46 | alreadyAired ? messages.airedrelative : messages.airsrelative,
47 | {
48 | relativeTime: (
49 |
54 | ),
55 | }
56 | )}
57 |
58 | )}
59 |
60 | );
61 | };
62 |
63 | export default AirDateBadge;
64 |
--------------------------------------------------------------------------------
/src/components/AppDataWarning/index.tsx:
--------------------------------------------------------------------------------
1 | import Alert from '@app/components/Common/Alert';
2 | import { defineMessages, useIntl } from 'react-intl';
3 | import useSWR from 'swr';
4 |
5 | const messages = defineMessages({
6 | dockerVolumeMissingDescription:
7 | 'The {appDataPath}
volume mount was not configured properly. All data will be cleared when the container is stopped or restarted.',
8 | });
9 |
10 | const AppDataWarning = () => {
11 | const intl = useIntl();
12 | const { data, error } = useSWR<{ appData: boolean; appDataPath: string }>(
13 | '/api/v1/status/appdata'
14 | );
15 |
16 | if (!data && !error) {
17 | return null;
18 | }
19 |
20 | if (!data) {
21 | return null;
22 | }
23 |
24 | return (
25 | <>
26 | {!data.appData && (
27 | (
30 | {msg}
31 | ),
32 | appDataPath: data.appDataPath,
33 | })}
34 | />
35 | )}
36 | >
37 | );
38 | };
39 |
40 | export default AppDataWarning;
41 |
--------------------------------------------------------------------------------
/src/components/Common/Alert/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ExclamationTriangleIcon,
3 | InformationCircleIcon,
4 | XCircleIcon,
5 | } from '@heroicons/react/24/solid';
6 |
7 | interface AlertProps {
8 | title?: React.ReactNode;
9 | type?: 'warning' | 'info' | 'error';
10 | children?: React.ReactNode;
11 | }
12 |
13 | const Alert = ({ title, children, type }: AlertProps) => {
14 | let design = {
15 | bgColor:
16 | 'border border-yellow-500 backdrop-blur bg-yellow-400 bg-opacity-20',
17 | titleColor: 'text-yellow-100',
18 | textColor: 'text-yellow-300',
19 | svg: ,
20 | };
21 |
22 | switch (type) {
23 | case 'info':
24 | design = {
25 | bgColor:
26 | 'border border-indigo-500 backdrop-blur bg-indigo-400 bg-opacity-20',
27 | titleColor: 'text-gray-100',
28 | textColor: 'text-gray-300',
29 | svg: ,
30 | };
31 | break;
32 | case 'error':
33 | design = {
34 | bgColor: 'bg-red-600',
35 | titleColor: 'text-red-100',
36 | textColor: 'text-red-300',
37 | svg: ,
38 | };
39 | break;
40 | }
41 |
42 | return (
43 |
44 |
45 |
{design.svg}
46 |
47 | {title && (
48 |
49 | {title}
50 |
51 | )}
52 | {children && (
53 |
54 | {children}
55 |
56 | )}
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | export default Alert;
64 |
--------------------------------------------------------------------------------
/src/components/Common/CachedImage/index.tsx:
--------------------------------------------------------------------------------
1 | import useSettings from '@app/hooks/useSettings';
2 | import type { ImageLoader, ImageProps } from 'next/image';
3 | import Image from 'next/image';
4 |
5 | const imageLoader: ImageLoader = ({ src }) => src;
6 |
7 | /**
8 | * The CachedImage component should be used wherever
9 | * we want to offer the option to locally cache images.
10 | **/
11 | const CachedImage = ({ src, ...props }: ImageProps) => {
12 | const { currentSettings } = useSettings();
13 |
14 | let imageUrl = src;
15 |
16 | if (typeof imageUrl === 'string' && imageUrl.startsWith('http')) {
17 | const parsedUrl = new URL(imageUrl);
18 |
19 | if (parsedUrl.host === 'image.tmdb.org' && currentSettings.cacheImages) {
20 | imageUrl = imageUrl.replace('https://image.tmdb.org', '/imageproxy');
21 | }
22 | }
23 |
24 | return ;
25 | };
26 |
27 | export default CachedImage;
28 |
--------------------------------------------------------------------------------
/src/components/Common/ConfirmButton/index.tsx:
--------------------------------------------------------------------------------
1 | import Button from '@app/components/Common/Button';
2 | import useClickOutside from '@app/hooks/useClickOutside';
3 | import { forwardRef, useRef, useState } from 'react';
4 |
5 | interface ConfirmButtonProps {
6 | onClick: () => void;
7 | confirmText: React.ReactNode;
8 | className?: string;
9 | children: React.ReactNode;
10 | }
11 |
12 | const ConfirmButton = forwardRef(
13 | ({ onClick, children, confirmText, className }, parentRef) => {
14 | const ref = useRef(null);
15 | useClickOutside(ref, () => setIsClicked(false));
16 | const [isClicked, setIsClicked] = useState(false);
17 | return (
18 |
53 | );
54 | }
55 | );
56 |
57 | ConfirmButton.displayName = 'ConfirmButton';
58 |
59 | export default ConfirmButton;
60 |
--------------------------------------------------------------------------------
/src/components/Common/Header/index.tsx:
--------------------------------------------------------------------------------
1 | interface HeaderProps {
2 | extraMargin?: number;
3 | subtext?: React.ReactNode;
4 | children: React.ReactNode;
5 | }
6 |
7 | const Header = ({ children, extraMargin = 0, subtext }: HeaderProps) => {
8 | return (
9 |
10 |
11 |
15 | {children}
16 |
17 | {subtext &&
{subtext}
}
18 |
19 |
20 | );
21 | };
22 |
23 | export default Header;
24 |
--------------------------------------------------------------------------------
/src/components/Common/List/index.tsx:
--------------------------------------------------------------------------------
1 | import { withProperties } from '@app/utils/typeHelpers';
2 |
3 | interface ListItemProps {
4 | title: string;
5 | className?: string;
6 | children: React.ReactNode;
7 | }
8 |
9 | const ListItem = ({ title, className, children }: ListItemProps) => {
10 | return (
11 |
12 |
13 |
{title}
14 |
15 | {children}
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | interface ListProps {
23 | title: string;
24 | subTitle?: string;
25 | children: React.ReactNode;
26 | }
27 |
28 | const List = ({ title, subTitle, children }: ListProps) => {
29 | return (
30 | <>
31 |
32 |
{title}
33 | {subTitle &&
{subTitle}
}
34 |
35 |
38 | >
39 | );
40 | };
41 |
42 | export default withProperties(List, { Item: ListItem });
43 |
--------------------------------------------------------------------------------
/src/components/Common/LoadingSpinner/index.tsx:
--------------------------------------------------------------------------------
1 | export const SmallLoadingSpinner = () => {
2 | return (
3 |
4 |
26 |
27 | );
28 | };
29 |
30 | const LoadingSpinner = () => {
31 | return (
32 |
33 |
55 |
56 | );
57 | };
58 |
59 | export default LoadingSpinner;
60 |
--------------------------------------------------------------------------------
/src/components/Common/PageTitle/index.tsx:
--------------------------------------------------------------------------------
1 | import useSettings from '@app/hooks/useSettings';
2 | import Head from 'next/head';
3 |
4 | interface PageTitleProps {
5 | title: string | (string | undefined)[];
6 | }
7 |
8 | const PageTitle = ({ title }: PageTitleProps) => {
9 | const settings = useSettings();
10 |
11 | const titleText = `${
12 | Array.isArray(title) ? title.filter(Boolean).join(' - ') : title
13 | } - ${settings.currentSettings.applicationTitle}`;
14 |
15 | return (
16 |
17 | {titleText}
18 |
19 | );
20 | };
21 |
22 | export default PageTitle;
23 |
--------------------------------------------------------------------------------
/src/components/Common/PlayButton/index.tsx:
--------------------------------------------------------------------------------
1 | import ButtonWithDropdown from '@app/components/Common/ButtonWithDropdown';
2 |
3 | interface PlayButtonProps {
4 | links: PlayButtonLink[];
5 | }
6 |
7 | export interface PlayButtonLink {
8 | text: string;
9 | url: string;
10 | svg: React.ReactNode;
11 | }
12 |
13 | const PlayButton = ({ links }: PlayButtonProps) => {
14 | if (!links || !links.length) {
15 | return null;
16 | }
17 |
18 | return (
19 |
23 | {links[0].svg}
24 | {links[0].text}
25 | >
26 | }
27 | onClick={() => {
28 | window.open(links[0].url, '_blank');
29 | }}
30 | >
31 | {links.length > 1 &&
32 | links.slice(1).map((link, i) => {
33 | return (
34 | {
37 | window.open(link.url, '_blank');
38 | }}
39 | buttonType="ghost"
40 | >
41 | {link.svg}
42 | {link.text}
43 |
44 | );
45 | })}
46 |
47 | );
48 | };
49 |
50 | export default PlayButton;
51 |
--------------------------------------------------------------------------------
/src/components/Common/ProgressCircle/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | interface ProgressCircleProps {
4 | className?: string;
5 | progress?: number;
6 | useHeatLevel?: boolean;
7 | }
8 |
9 | const ProgressCircle = ({
10 | className,
11 | progress = 0,
12 | useHeatLevel,
13 | }: ProgressCircleProps) => {
14 | const ref = useRef(null);
15 |
16 | let color = '';
17 | let emptyColor = 'text-gray-300';
18 |
19 | if (useHeatLevel) {
20 | color = 'text-green-500';
21 |
22 | if (progress <= 50) {
23 | color = 'text-yellow-500';
24 | }
25 |
26 | if (progress <= 10) {
27 | color = 'text-red-500';
28 | }
29 |
30 | if (progress === 0) {
31 | emptyColor = 'text-red-600';
32 | }
33 | }
34 |
35 | useEffect(() => {
36 | if (ref && ref.current) {
37 | const radius = ref.current?.r.baseVal.value;
38 | const circumference = (radius ?? 0) * 2 * Math.PI;
39 | const offset = circumference - (progress / 100) * circumference;
40 | ref.current.style.strokeDashoffset = `${offset}`;
41 | ref.current.style.strokeDasharray = `${circumference} ${circumference}`;
42 | }
43 | });
44 |
45 | return (
46 |
71 | );
72 | };
73 |
74 | export default ProgressCircle;
75 |
--------------------------------------------------------------------------------
/src/components/Common/SensitiveInput/index.tsx:
--------------------------------------------------------------------------------
1 | import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/solid';
2 | import { Field } from 'formik';
3 | import { useState } from 'react';
4 |
5 | interface CustomInputProps extends React.ComponentProps<'input'> {
6 | as?: 'input';
7 | }
8 |
9 | interface CustomFieldProps extends React.ComponentProps {
10 | as?: 'field';
11 | }
12 |
13 | type SensitiveInputProps = CustomInputProps | CustomFieldProps;
14 |
15 | const SensitiveInput = ({ as = 'input', ...props }: SensitiveInputProps) => {
16 | const [isHidden, setHidden] = useState(true);
17 | const Component = as === 'input' ? 'input' : Field;
18 | const componentProps =
19 | as === 'input'
20 | ? props
21 | : {
22 | ...props,
23 | as: props.type === 'textarea' && !isHidden ? 'textarea' : undefined,
24 | };
25 | return (
26 | <>
27 |
42 |
52 | >
53 | );
54 | };
55 |
56 | export default SensitiveInput;
57 |
--------------------------------------------------------------------------------
/src/components/Common/SlideCheckbox/index.tsx:
--------------------------------------------------------------------------------
1 | type SlideCheckboxProps = {
2 | onClick: () => void;
3 | checked?: boolean;
4 | };
5 |
6 | const SlideCheckbox = ({ onClick, checked = false }: SlideCheckboxProps) => {
7 | return (
8 | {
13 | onClick();
14 | }}
15 | onKeyDown={(e) => {
16 | if (e.key === 'Enter' || e.key === 'Space') {
17 | onClick();
18 | }
19 | }}
20 | className={`relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none`}
21 | >
22 |
28 |
34 |
35 | );
36 | };
37 |
38 | export default SlideCheckbox;
39 |
--------------------------------------------------------------------------------
/src/components/Common/Tag/index.tsx:
--------------------------------------------------------------------------------
1 | import { TagIcon } from '@heroicons/react/24/outline';
2 | import React from 'react';
3 |
4 | type TagProps = {
5 | children: React.ReactNode;
6 | iconSvg?: JSX.Element;
7 | };
8 |
9 | const Tag = ({ children, iconSvg }: TagProps) => {
10 | return (
11 |
12 | {iconSvg ? (
13 | React.cloneElement(iconSvg, {
14 | className: 'mr-1 h-4 w-4',
15 | })
16 | ) : (
17 |
18 | )}
19 | {children}
20 |
21 | );
22 | };
23 |
24 | export default Tag;
25 |
--------------------------------------------------------------------------------
/src/components/Common/Tooltip/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import type { Config } from 'react-popper-tooltip';
4 | import { usePopperTooltip } from 'react-popper-tooltip';
5 |
6 | type TooltipProps = {
7 | content: React.ReactNode;
8 | children: React.ReactElement;
9 | tooltipConfig?: Partial;
10 | className?: string;
11 | };
12 |
13 | const Tooltip = ({
14 | children,
15 | content,
16 | tooltipConfig,
17 | className,
18 | }: TooltipProps) => {
19 | const { getTooltipProps, setTooltipRef, setTriggerRef, visible } =
20 | usePopperTooltip({
21 | followCursor: true,
22 | offset: [-28, 6],
23 | placement: 'auto-end',
24 | ...tooltipConfig,
25 | });
26 |
27 | const tooltipStyle = [
28 | 'z-50 text-sm absolute font-normal bg-gray-800 px-2 py-1 rounded border border-gray-600 shadow text-gray-100',
29 | ];
30 |
31 | if (className) {
32 | tooltipStyle.push(className);
33 | }
34 |
35 | return (
36 | <>
37 | {React.cloneElement(children, { ref: setTriggerRef })}
38 | {visible &&
39 | content &&
40 | ReactDOM.createPortal(
41 |
47 | {content}
48 |
,
49 | document.body
50 | )}
51 | >
52 | );
53 | };
54 |
55 | export default Tooltip;
56 |
--------------------------------------------------------------------------------
/src/components/CompanyCard/index.tsx:
--------------------------------------------------------------------------------
1 | import CachedImage from '@app/components/Common/CachedImage';
2 | import Link from 'next/link';
3 | import { useState } from 'react';
4 |
5 | interface CompanyCardProps {
6 | name: string;
7 | image: string;
8 | url: string;
9 | }
10 |
11 | const CompanyCard = ({ image, url, name }: CompanyCardProps) => {
12 | const [isHovered, setHovered] = useState(false);
13 |
14 | return (
15 |
16 | {
23 | setHovered(true);
24 | }}
25 | onMouseLeave={() => setHovered(false)}
26 | onKeyDown={(e) => {
27 | if (e.key === 'Enter') {
28 | setHovered(true);
29 | }
30 | }}
31 | role="link"
32 | tabIndex={0}
33 | >
34 |
35 |
42 |
43 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default CompanyCard;
54 |
--------------------------------------------------------------------------------
/src/components/CompanyTag/index.tsx:
--------------------------------------------------------------------------------
1 | import Spinner from '@app/assets/spinner.svg';
2 | import Tag from '@app/components/Common/Tag';
3 | import { BuildingOffice2Icon } from '@heroicons/react/24/outline';
4 | import type { ProductionCompany, TvNetwork } from '@server/models/common';
5 | import useSWR from 'swr';
6 |
7 | type CompanyTagProps = {
8 | type: 'studio' | 'network';
9 | companyId: number;
10 | };
11 |
12 | const CompanyTag = ({ companyId, type }: CompanyTagProps) => {
13 | const { data, error } = useSWR(
14 | `/api/v1/${type}/${companyId}`
15 | );
16 |
17 | if (!data && !error) {
18 | return (
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | return }>{data?.name};
26 | };
27 |
28 | export default CompanyTag;
29 |
--------------------------------------------------------------------------------
/src/components/Discover/DiscoverMovieGenre/index.tsx:
--------------------------------------------------------------------------------
1 | import Header from '@app/components/Common/Header';
2 | import ListView from '@app/components/Common/ListView';
3 | import PageTitle from '@app/components/Common/PageTitle';
4 | import useDiscover from '@app/hooks/useDiscover';
5 | import globalMessages from '@app/i18n/globalMessages';
6 | import Error from '@app/pages/_error';
7 | import type { MovieResult } from '@server/models/Search';
8 | import { useRouter } from 'next/router';
9 | import { defineMessages, useIntl } from 'react-intl';
10 |
11 | const messages = defineMessages({
12 | genreMovies: '{genre} Movies',
13 | });
14 |
15 | const DiscoverMovieGenre = () => {
16 | const router = useRouter();
17 | const intl = useIntl();
18 |
19 | const {
20 | isLoadingInitialData,
21 | isEmpty,
22 | isLoadingMore,
23 | isReachingEnd,
24 | titles,
25 | fetchMore,
26 | error,
27 | firstResultData,
28 | } = useDiscover(
29 | `/api/v1/discover/movies/genre/${router.query.genreId}`
30 | );
31 |
32 | if (error) {
33 | return ;
34 | }
35 |
36 | const title = isLoadingInitialData
37 | ? intl.formatMessage(globalMessages.loading)
38 | : intl.formatMessage(messages.genreMovies, {
39 | genre: firstResultData?.genre.name,
40 | });
41 |
42 | return (
43 | <>
44 |
45 |
46 |
47 |
48 | 0)
53 | }
54 | isReachingEnd={isReachingEnd}
55 | onScrollBottom={fetchMore}
56 | />
57 | >
58 | );
59 | };
60 |
61 | export default DiscoverMovieGenre;
62 |
--------------------------------------------------------------------------------
/src/components/Discover/DiscoverTvGenre/index.tsx:
--------------------------------------------------------------------------------
1 | import Header from '@app/components/Common/Header';
2 | import ListView from '@app/components/Common/ListView';
3 | import PageTitle from '@app/components/Common/PageTitle';
4 | import useDiscover from '@app/hooks/useDiscover';
5 | import globalMessages from '@app/i18n/globalMessages';
6 | import Error from '@app/pages/_error';
7 | import type { TvResult } from '@server/models/Search';
8 | import { useRouter } from 'next/router';
9 | import { defineMessages, useIntl } from 'react-intl';
10 |
11 | const messages = defineMessages({
12 | genreSeries: '{genre} Series',
13 | });
14 |
15 | const DiscoverTvGenre = () => {
16 | const router = useRouter();
17 | const intl = useIntl();
18 |
19 | const {
20 | isLoadingInitialData,
21 | isEmpty,
22 | isLoadingMore,
23 | isReachingEnd,
24 | titles,
25 | fetchMore,
26 | error,
27 | firstResultData,
28 | } = useDiscover(
29 | `/api/v1/discover/tv/genre/${router.query.genreId}`
30 | );
31 |
32 | if (error) {
33 | return ;
34 | }
35 |
36 | const title = isLoadingInitialData
37 | ? intl.formatMessage(globalMessages.loading)
38 | : intl.formatMessage(messages.genreSeries, {
39 | genre: firstResultData?.genre.name,
40 | });
41 |
42 | return (
43 | <>
44 |
45 |
46 |
47 |
48 | 0)
53 | }
54 | isReachingEnd={isReachingEnd}
55 | onScrollBottom={fetchMore}
56 | />
57 | >
58 | );
59 | };
60 |
61 | export default DiscoverTvGenre;
62 |
--------------------------------------------------------------------------------
/src/components/Discover/DiscoverTvUpcoming.tsx:
--------------------------------------------------------------------------------
1 | import Header from '@app/components/Common/Header';
2 | import ListView from '@app/components/Common/ListView';
3 | import PageTitle from '@app/components/Common/PageTitle';
4 | import useDiscover from '@app/hooks/useDiscover';
5 | import Error from '@app/pages/_error';
6 | import type { TvResult } from '@server/models/Search';
7 | import { defineMessages, useIntl } from 'react-intl';
8 |
9 | const messages = defineMessages({
10 | upcomingtv: 'Upcoming Series',
11 | });
12 |
13 | const DiscoverTvUpcoming = () => {
14 | const intl = useIntl();
15 |
16 | const {
17 | isLoadingInitialData,
18 | isEmpty,
19 | isLoadingMore,
20 | isReachingEnd,
21 | titles,
22 | fetchMore,
23 | error,
24 | } = useDiscover('/api/v1/discover/tv/upcoming');
25 |
26 | if (error) {
27 | return ;
28 | }
29 |
30 | return (
31 | <>
32 |
33 |
34 | {intl.formatMessage(messages.upcomingtv)}
35 |
36 | 0)
42 | }
43 | onScrollBottom={fetchMore}
44 | />
45 | >
46 | );
47 | };
48 |
49 | export default DiscoverTvUpcoming;
50 |
--------------------------------------------------------------------------------
/src/components/Discover/MovieGenreList/index.tsx:
--------------------------------------------------------------------------------
1 | import Header from '@app/components/Common/Header';
2 | import LoadingSpinner from '@app/components/Common/LoadingSpinner';
3 | import PageTitle from '@app/components/Common/PageTitle';
4 | import { genreColorMap } from '@app/components/Discover/constants';
5 | import GenreCard from '@app/components/GenreCard';
6 | import Error from '@app/pages/_error';
7 | import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
8 | import { defineMessages, useIntl } from 'react-intl';
9 | import useSWR from 'swr';
10 |
11 | const messages = defineMessages({
12 | moviegenres: 'Movie Genres',
13 | });
14 |
15 | const MovieGenreList = () => {
16 | const intl = useIntl();
17 | const { data, error } = useSWR(
18 | `/api/v1/discover/genreslider/movie`
19 | );
20 |
21 | if (!data && !error) {
22 | return ;
23 | }
24 |
25 | if (!data) {
26 | return ;
27 | }
28 |
29 | return (
30 | <>
31 |
32 |
33 | {intl.formatMessage(messages.moviegenres)}
34 |
35 |
36 | {data.map((genre, index) => (
37 | -
38 |
46 |
47 | ))}
48 |
49 | >
50 | );
51 | };
52 |
53 | export default MovieGenreList;
54 |
--------------------------------------------------------------------------------
/src/components/Discover/MovieGenreSlider/index.tsx:
--------------------------------------------------------------------------------
1 | import { genreColorMap } from '@app/components/Discover/constants';
2 | import GenreCard from '@app/components/GenreCard';
3 | import Slider from '@app/components/Slider';
4 | import { ArrowRightCircleIcon } from '@heroicons/react/24/outline';
5 | import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
6 | import Link from 'next/link';
7 | import React from 'react';
8 | import { defineMessages, useIntl } from 'react-intl';
9 | import useSWR from 'swr';
10 |
11 | const messages = defineMessages({
12 | moviegenres: 'Movie Genres',
13 | });
14 |
15 | const MovieGenreSlider = () => {
16 | const intl = useIntl();
17 | const { data, error } = useSWR(
18 | `/api/v1/discover/genreslider/movie`,
19 | {
20 | refreshInterval: 0,
21 | revalidateOnFocus: false,
22 | }
23 | );
24 |
25 | return (
26 | <>
27 |
35 | (
40 |
48 | ))}
49 | placeholder={}
50 | emptyMessage=""
51 | />
52 | >
53 | );
54 | };
55 |
56 | export default React.memo(MovieGenreSlider);
57 |
--------------------------------------------------------------------------------
/src/components/Discover/RecentRequestsSlider/index.tsx:
--------------------------------------------------------------------------------
1 | import { sliderTitles } from '@app/components/Discover/constants';
2 | import RequestCard from '@app/components/RequestCard';
3 | import Slider from '@app/components/Slider';
4 | import { ArrowRightCircleIcon } from '@heroicons/react/24/outline';
5 | import type { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces';
6 | import Link from 'next/link';
7 | import { useIntl } from 'react-intl';
8 | import useSWR from 'swr';
9 |
10 | const RecentRequestsSlider = () => {
11 | const intl = useIntl();
12 | const { data: requests, error: requestError } =
13 | useSWR(
14 | '/api/v1/request?filter=all&take=10&sort=modified&skip=0',
15 | {
16 | revalidateOnMount: true,
17 | }
18 | );
19 |
20 | if (requests && requests.results.length === 0 && !requestError) {
21 | return null;
22 | }
23 |
24 | return (
25 | <>
26 |
34 | (
38 |
42 | ))}
43 | placeholder={}
44 | />
45 | >
46 | );
47 | };
48 |
49 | export default RecentRequestsSlider;
50 |
--------------------------------------------------------------------------------
/src/components/Discover/RecentlyAddedSlider/index.tsx:
--------------------------------------------------------------------------------
1 | import Slider from '@app/components/Slider';
2 | import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard';
3 | import { Permission, useUser } from '@app/hooks/useUser';
4 | import type { MediaResultsResponse } from '@server/interfaces/api/mediaInterfaces';
5 | import { defineMessages, useIntl } from 'react-intl';
6 | import useSWR from 'swr';
7 |
8 | const messages = defineMessages({
9 | recentlyAdded: 'Recently Added',
10 | });
11 |
12 | const RecentlyAddedSlider = () => {
13 | const intl = useIntl();
14 | const { hasPermission } = useUser();
15 | const { data: media, error: mediaError } = useSWR(
16 | '/api/v1/media?filter=allavailable&take=20&sort=mediaAdded',
17 | { revalidateOnMount: true }
18 | );
19 |
20 | if (
21 | (media && !media.results.length && !mediaError) ||
22 | !hasPermission([Permission.MANAGE_REQUESTS, Permission.RECENT_VIEW], {
23 | type: 'or',
24 | })
25 | ) {
26 | return null;
27 | }
28 |
29 | return (
30 | <>
31 |
32 |
33 | {intl.formatMessage(messages.recentlyAdded)}
34 |
35 |
36 | (
40 |
47 | ))}
48 | />
49 | >
50 | );
51 | };
52 |
53 | export default RecentlyAddedSlider;
54 |
--------------------------------------------------------------------------------
/src/components/Discover/Trending.tsx:
--------------------------------------------------------------------------------
1 | import Header from '@app/components/Common/Header';
2 | import ListView from '@app/components/Common/ListView';
3 | import PageTitle from '@app/components/Common/PageTitle';
4 | import useDiscover from '@app/hooks/useDiscover';
5 | import Error from '@app/pages/_error';
6 | import type {
7 | MovieResult,
8 | PersonResult,
9 | TvResult,
10 | } from '@server/models/Search';
11 | import { defineMessages, useIntl } from 'react-intl';
12 |
13 | const messages = defineMessages({
14 | trending: 'Trending',
15 | });
16 |
17 | const Trending = () => {
18 | const intl = useIntl();
19 | const {
20 | isLoadingInitialData,
21 | isEmpty,
22 | isLoadingMore,
23 | isReachingEnd,
24 | titles,
25 | fetchMore,
26 | error,
27 | } = useDiscover(
28 | '/api/v1/discover/trending'
29 | );
30 |
31 | if (error) {
32 | return ;
33 | }
34 |
35 | return (
36 | <>
37 |
38 |
39 | {intl.formatMessage(messages.trending)}
40 |
41 | 0)
46 | }
47 | isReachingEnd={isReachingEnd}
48 | onScrollBottom={fetchMore}
49 | />
50 | >
51 | );
52 | };
53 |
54 | export default Trending;
55 |
--------------------------------------------------------------------------------
/src/components/Discover/TvGenreList/index.tsx:
--------------------------------------------------------------------------------
1 | import Header from '@app/components/Common/Header';
2 | import LoadingSpinner from '@app/components/Common/LoadingSpinner';
3 | import PageTitle from '@app/components/Common/PageTitle';
4 | import { genreColorMap } from '@app/components/Discover/constants';
5 | import GenreCard from '@app/components/GenreCard';
6 | import Error from '@app/pages/_error';
7 | import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
8 | import { defineMessages, useIntl } from 'react-intl';
9 | import useSWR from 'swr';
10 |
11 | const messages = defineMessages({
12 | seriesgenres: 'Series Genres',
13 | });
14 |
15 | const TvGenreList = () => {
16 | const intl = useIntl();
17 | const { data, error } = useSWR(
18 | `/api/v1/discover/genreslider/tv`
19 | );
20 |
21 | if (!data && !error) {
22 | return ;
23 | }
24 |
25 | if (!data) {
26 | return ;
27 | }
28 |
29 | return (
30 | <>
31 |
32 |
33 | {intl.formatMessage(messages.seriesgenres)}
34 |
35 |
36 | {data.map((genre, index) => (
37 | -
38 |
46 |
47 | ))}
48 |
49 | >
50 | );
51 | };
52 |
53 | export default TvGenreList;
54 |
--------------------------------------------------------------------------------
/src/components/Discover/TvGenreSlider/index.tsx:
--------------------------------------------------------------------------------
1 | import { genreColorMap } from '@app/components/Discover/constants';
2 | import GenreCard from '@app/components/GenreCard';
3 | import Slider from '@app/components/Slider';
4 | import { ArrowRightCircleIcon } from '@heroicons/react/24/outline';
5 | import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
6 | import Link from 'next/link';
7 | import React from 'react';
8 | import { defineMessages, useIntl } from 'react-intl';
9 | import useSWR from 'swr';
10 |
11 | const messages = defineMessages({
12 | tvgenres: 'Series Genres',
13 | });
14 |
15 | const TvGenreSlider = () => {
16 | const intl = useIntl();
17 | const { data, error } = useSWR(
18 | `/api/v1/discover/genreslider/tv`,
19 | {
20 | refreshInterval: 0,
21 | revalidateOnFocus: false,
22 | }
23 | );
24 |
25 | return (
26 | <>
27 |
35 | (
40 |
48 | ))}
49 | placeholder={}
50 | emptyMessage=""
51 | />
52 | >
53 | );
54 | };
55 |
56 | export default React.memo(TvGenreSlider);
57 |
--------------------------------------------------------------------------------
/src/components/Discover/Upcoming.tsx:
--------------------------------------------------------------------------------
1 | import Header from '@app/components/Common/Header';
2 | import ListView from '@app/components/Common/ListView';
3 | import PageTitle from '@app/components/Common/PageTitle';
4 | import useDiscover from '@app/hooks/useDiscover';
5 | import Error from '@app/pages/_error';
6 | import type { MovieResult } from '@server/models/Search';
7 | import { defineMessages, useIntl } from 'react-intl';
8 |
9 | const messages = defineMessages({
10 | upcomingmovies: 'Upcoming Movies',
11 | });
12 |
13 | const UpcomingMovies = () => {
14 | const intl = useIntl();
15 |
16 | const {
17 | isLoadingInitialData,
18 | isEmpty,
19 | isLoadingMore,
20 | isReachingEnd,
21 | titles,
22 | fetchMore,
23 | error,
24 | } = useDiscover('/api/v1/discover/movies/upcoming');
25 |
26 | if (error) {
27 | return ;
28 | }
29 |
30 | return (
31 | <>
32 |
33 |
34 | {intl.formatMessage(messages.upcomingmovies)}
35 |
36 | 0)
41 | }
42 | isReachingEnd={isReachingEnd}
43 | onScrollBottom={fetchMore}
44 | />
45 | >
46 | );
47 | };
48 |
49 | export default UpcomingMovies;
50 |
--------------------------------------------------------------------------------
/src/components/GenreTag/index.tsx:
--------------------------------------------------------------------------------
1 | import Spinner from '@app/assets/spinner.svg';
2 | import Tag from '@app/components/Common/Tag';
3 | import { RectangleStackIcon } from '@heroicons/react/24/outline';
4 | import type { TmdbGenre } from '@server/api/themoviedb/interfaces';
5 | import useSWR from 'swr';
6 |
7 | type GenreTagProps = {
8 | type: 'tv' | 'movie';
9 | genreId: number;
10 | };
11 |
12 | const GenreTag = ({ genreId, type }: GenreTagProps) => {
13 | const { data, error } = useSWR(`/api/v1/genres/${type}`);
14 |
15 | if (!data && !error) {
16 | return (
17 |
18 |
19 |
20 | );
21 | }
22 |
23 | const genre = data?.find((genre) => genre.id === genreId);
24 |
25 | return }>{genre?.name};
26 | };
27 |
28 | export default GenreTag;
29 |
--------------------------------------------------------------------------------
/src/components/IssueModal/constants.ts:
--------------------------------------------------------------------------------
1 | import { IssueType } from '@server/constants/issue';
2 | import type { MessageDescriptor } from 'react-intl';
3 | import { defineMessages } from 'react-intl';
4 |
5 | const messages = defineMessages({
6 | issueAudio: 'Audio',
7 | issueVideo: 'Video',
8 | issueSubtitles: 'Subtitle',
9 | issueOther: 'Other',
10 | });
11 |
12 | interface IssueOption {
13 | name: MessageDescriptor;
14 | issueType: IssueType;
15 | mediaType?: 'movie' | 'tv';
16 | }
17 |
18 | export const issueOptions: IssueOption[] = [
19 | {
20 | name: messages.issueVideo,
21 | issueType: IssueType.VIDEO,
22 | },
23 | {
24 | name: messages.issueAudio,
25 | issueType: IssueType.AUDIO,
26 | },
27 | {
28 | name: messages.issueSubtitles,
29 | issueType: IssueType.SUBTITLES,
30 | },
31 | {
32 | name: messages.issueOther,
33 | issueType: IssueType.OTHER,
34 | },
35 | ];
36 |
--------------------------------------------------------------------------------
/src/components/IssueModal/index.tsx:
--------------------------------------------------------------------------------
1 | import CreateIssueModal from '@app/components/IssueModal/CreateIssueModal';
2 | import { Transition } from '@headlessui/react';
3 |
4 | interface IssueModalProps {
5 | show?: boolean;
6 | onCancel: () => void;
7 | mediaType: 'movie' | 'tv';
8 | tmdbId: number;
9 | issueId?: never;
10 | }
11 |
12 | const IssueModal = ({ show, mediaType, onCancel, tmdbId }: IssueModalProps) => (
13 |
23 |
28 |
29 | );
30 |
31 | export default IssueModal;
32 |
--------------------------------------------------------------------------------
/src/components/JSONEditor/index.tsx:
--------------------------------------------------------------------------------
1 | import 'ace-builds/src-noconflict/ace';
2 | import 'ace-builds/src-noconflict/mode-json';
3 | import 'ace-builds/src-noconflict/theme-dracula';
4 | import type { HTMLAttributes } from 'react';
5 | import AceEditor from 'react-ace';
6 | interface JSONEditorProps extends HTMLAttributes {
7 | name: string;
8 | value: string;
9 | onUpdate: (value: string) => void;
10 | }
11 |
12 | const JSONEditor = ({ name, value, onUpdate, onBlur }: JSONEditorProps) => {
13 | return (
14 |
27 | );
28 | };
29 |
30 | export default JSONEditor;
31 |
--------------------------------------------------------------------------------
/src/components/KeywordTag/index.tsx:
--------------------------------------------------------------------------------
1 | import Spinner from '@app/assets/spinner.svg';
2 | import Tag from '@app/components/Common/Tag';
3 | import type { Keyword } from '@server/models/common';
4 | import useSWR from 'swr';
5 |
6 | type KeywordTagProps = {
7 | keywordId: number;
8 | };
9 |
10 | const KeywordTag = ({ keywordId }: KeywordTagProps) => {
11 | const { data, error } = useSWR(`/api/v1/keyword/${keywordId}`);
12 |
13 | if (!data && !error) {
14 | return (
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | return {data?.name};
22 | };
23 |
24 | export default KeywordTag;
25 |
--------------------------------------------------------------------------------
/src/components/Layout/Notifications/index.tsx:
--------------------------------------------------------------------------------
1 | import { BellIcon } from '@heroicons/react/24/outline';
2 |
3 | const Notifications = () => {
4 | return (
5 |
11 | );
12 | };
13 |
14 | export default Notifications;
15 |
--------------------------------------------------------------------------------
/src/components/PlexLoginButton/index.tsx:
--------------------------------------------------------------------------------
1 | import globalMessages from '@app/i18n/globalMessages';
2 | import PlexOAuth from '@app/utils/plex';
3 | import { ArrowLeftOnRectangleIcon } from '@heroicons/react/24/outline';
4 | import { useState } from 'react';
5 | import { defineMessages, useIntl } from 'react-intl';
6 |
7 | const messages = defineMessages({
8 | signinwithplex: 'Sign In',
9 | signingin: 'Signing In…',
10 | });
11 |
12 | const plexOAuth = new PlexOAuth();
13 |
14 | interface PlexLoginButtonProps {
15 | onAuthToken: (authToken: string) => void;
16 | isProcessing?: boolean;
17 | onError?: (message: string) => void;
18 | }
19 |
20 | const PlexLoginButton = ({
21 | onAuthToken,
22 | onError,
23 | isProcessing,
24 | }: PlexLoginButtonProps) => {
25 | const intl = useIntl();
26 | const [loading, setLoading] = useState(false);
27 |
28 | const getPlexLogin = async () => {
29 | setLoading(true);
30 | try {
31 | const authToken = await plexOAuth.login();
32 | setLoading(false);
33 | onAuthToken(authToken);
34 | } catch (e) {
35 | if (onError) {
36 | onError(e.message);
37 | }
38 | setLoading(false);
39 | }
40 | };
41 | return (
42 |
43 |
61 |
62 | );
63 | };
64 |
65 | export default PlexLoginButton;
66 |
--------------------------------------------------------------------------------
/src/components/Search/index.tsx:
--------------------------------------------------------------------------------
1 | import Header from '@app/components/Common/Header';
2 | import ListView from '@app/components/Common/ListView';
3 | import PageTitle from '@app/components/Common/PageTitle';
4 | import useDiscover from '@app/hooks/useDiscover';
5 | import Error from '@app/pages/_error';
6 | import type {
7 | MovieResult,
8 | PersonResult,
9 | TvResult,
10 | } from '@server/models/Search';
11 | import { useRouter } from 'next/router';
12 | import { defineMessages, useIntl } from 'react-intl';
13 |
14 | const messages = defineMessages({
15 | search: 'Search',
16 | searchresults: 'Search Results',
17 | });
18 |
19 | const Search = () => {
20 | const intl = useIntl();
21 | const router = useRouter();
22 |
23 | const {
24 | isLoadingInitialData,
25 | isEmpty,
26 | isLoadingMore,
27 | isReachingEnd,
28 | titles,
29 | fetchMore,
30 | error,
31 | } = useDiscover(
32 | `/api/v1/search`,
33 | {
34 | query: router.query.query,
35 | },
36 | { hideAvailable: false }
37 | );
38 |
39 | if (error) {
40 | return ;
41 | }
42 |
43 | return (
44 | <>
45 |
46 |
47 | {intl.formatMessage(messages.searchresults)}
48 |
49 | 0)
54 | }
55 | isReachingEnd={isReachingEnd}
56 | onScrollBottom={fetchMore}
57 | />
58 | >
59 | );
60 | };
61 |
62 | export default Search;
63 |
--------------------------------------------------------------------------------
/src/components/ServiceWorkerSetup/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import { useUser } from '@app/hooks/useUser';
4 | import { useEffect } from 'react';
5 |
6 | const ServiceWorkerSetup = () => {
7 | const { user } = useUser();
8 | useEffect(() => {
9 | if ('serviceWorker' in navigator && user?.id) {
10 | navigator.serviceWorker
11 | .register('/sw.js')
12 | .then(async (registration) => {
13 | console.log(
14 | '[SW] Registration successful, scope is:',
15 | registration.scope
16 | );
17 | })
18 | .catch(function (error) {
19 | console.log('[SW] Service worker registration failed, error:', error);
20 | });
21 | }
22 | }, [user]);
23 | return null;
24 | };
25 |
26 | export default ServiceWorkerSetup;
27 |
--------------------------------------------------------------------------------
/src/components/Settings/CopyButton.tsx:
--------------------------------------------------------------------------------
1 | import { ClipboardDocumentIcon } from '@heroicons/react/24/solid';
2 | import { useEffect } from 'react';
3 | import { defineMessages, useIntl } from 'react-intl';
4 | import { useToasts } from 'react-toast-notifications';
5 | import useClipboard from 'react-use-clipboard';
6 |
7 | const messages = defineMessages({
8 | copied: 'Copied API key to clipboard.',
9 | });
10 |
11 | const CopyButton = ({ textToCopy }: { textToCopy: string }) => {
12 | const intl = useIntl();
13 | const [isCopied, setCopied] = useClipboard(textToCopy, {
14 | successDuration: 1000,
15 | });
16 | const { addToast } = useToasts();
17 |
18 | useEffect(() => {
19 | if (isCopied) {
20 | addToast(intl.formatMessage(messages.copied), {
21 | appearance: 'info',
22 | autoDismiss: true,
23 | });
24 | }
25 | }, [isCopied, addToast, intl]);
26 |
27 | return (
28 |
37 | );
38 | };
39 |
40 | export default CopyButton;
41 |
--------------------------------------------------------------------------------
/src/components/Settings/SettingsBadge.tsx:
--------------------------------------------------------------------------------
1 | import Badge from '@app/components/Common/Badge';
2 | import Tooltip from '@app/components/Common/Tooltip';
3 | import globalMessages from '@app/i18n/globalMessages';
4 | import { defineMessages, useIntl } from 'react-intl';
5 |
6 | const messages = defineMessages({
7 | advancedTooltip:
8 | 'Incorrectly configuring this setting may result in broken functionality',
9 | experimentalTooltip:
10 | 'Enabling this setting may result in unexpected application behavior',
11 | restartrequiredTooltip:
12 | 'Overseerr must be restarted for changes to this setting to take effect',
13 | });
14 |
15 | const SettingsBadge = ({
16 | badgeType,
17 | className,
18 | }: {
19 | badgeType: 'advanced' | 'experimental' | 'restartRequired';
20 | className?: string;
21 | }) => {
22 | const intl = useIntl();
23 |
24 | switch (badgeType) {
25 | case 'advanced':
26 | return (
27 |
28 |
29 | {intl.formatMessage(globalMessages.advanced)}
30 |
31 |
32 | );
33 | case 'experimental':
34 | return (
35 |
36 |
37 | {intl.formatMessage(globalMessages.experimental)}
38 |
39 |
40 | );
41 | case 'restartRequired':
42 | return (
43 |
44 |
45 | {intl.formatMessage(globalMessages.restartRequired)}
46 |
47 |
48 | );
49 | default:
50 | return null;
51 | }
52 | };
53 |
54 | export default SettingsBadge;
55 |
--------------------------------------------------------------------------------
/src/components/TitleCard/Placeholder.tsx:
--------------------------------------------------------------------------------
1 | interface PlaceholderProps {
2 | canExpand?: boolean;
3 | }
4 |
5 | const Placeholder = ({ canExpand = false }: PlaceholderProps) => {
6 | return (
7 |
14 | );
15 | };
16 |
17 | export default Placeholder;
18 |
--------------------------------------------------------------------------------
/src/components/ToastContainer/index.tsx:
--------------------------------------------------------------------------------
1 | import type { ToastContainerProps } from 'react-toast-notifications';
2 |
3 | const ToastContainer = ({ hasToasts, ...props }: ToastContainerProps) => {
4 | return (
5 |
15 | );
16 | };
17 |
18 | export default ToastContainer;
19 |
--------------------------------------------------------------------------------
/src/context/InteractionContext.tsx:
--------------------------------------------------------------------------------
1 | import useInteraction from '@app/hooks/useInteraction';
2 | import React from 'react';
3 |
4 | interface InteractionContextProps {
5 | isTouch?: boolean;
6 | children?: React.ReactNode;
7 | }
8 |
9 | export const InteractionContext = React.createContext({
10 | isTouch: false,
11 | });
12 |
13 | export const InteractionProvider = ({ children }: InteractionContextProps) => {
14 | const isTouch = useInteraction();
15 |
16 | return (
17 |
18 | {children}
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/context/SettingsContext.tsx:
--------------------------------------------------------------------------------
1 | import type { PublicSettingsResponse } from '@server/interfaces/api/settingsInterfaces';
2 | import React from 'react';
3 | import useSWR from 'swr';
4 |
5 | export interface SettingsContextProps {
6 | currentSettings: PublicSettingsResponse;
7 | children?: React.ReactNode;
8 | }
9 |
10 | const defaultSettings = {
11 | initialized: false,
12 | applicationTitle: 'Overseerr',
13 | applicationUrl: '',
14 | hideAvailable: false,
15 | localLogin: true,
16 | movie4kEnabled: false,
17 | series4kEnabled: false,
18 | region: '',
19 | originalLanguage: '',
20 | partialRequestsEnabled: true,
21 | cacheImages: false,
22 | vapidPublic: '',
23 | enablePushRegistration: false,
24 | locale: 'en',
25 | emailEnabled: false,
26 | newPlexLogin: true,
27 | };
28 |
29 | export const SettingsContext = React.createContext({
30 | currentSettings: defaultSettings,
31 | });
32 |
33 | export const SettingsProvider = ({
34 | children,
35 | currentSettings,
36 | }: SettingsContextProps) => {
37 | const { data, error } = useSWR(
38 | '/api/v1/settings/public',
39 | { fallbackData: currentSettings }
40 | );
41 |
42 | let newSettings = defaultSettings;
43 |
44 | if (data && !error) {
45 | newSettings = data;
46 | }
47 |
48 | return (
49 |
50 | {children}
51 |
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/src/context/UserContext.tsx:
--------------------------------------------------------------------------------
1 | import type { User } from '@app/hooks/useUser';
2 | import { useUser } from '@app/hooks/useUser';
3 | import { useRouter } from 'next/dist/client/router';
4 | import { useEffect, useRef } from 'react';
5 |
6 | interface UserContextProps {
7 | initialUser: User;
8 | children?: React.ReactNode;
9 | }
10 |
11 | /**
12 | * This UserContext serves the purpose of just preparing the useUser hooks
13 | * cache on server side render. It also will handle redirecting the user to
14 | * the login page if their session ever becomes invalid.
15 | */
16 | export const UserContext = ({ initialUser, children }: UserContextProps) => {
17 | const { user, error, revalidate } = useUser({ initialData: initialUser });
18 | const router = useRouter();
19 | const routing = useRef(false);
20 |
21 | useEffect(() => {
22 | revalidate();
23 | }, [router.pathname, revalidate]);
24 |
25 | useEffect(() => {
26 | if (
27 | !router.pathname.match(/(setup|login|resetpassword)/) &&
28 | (!user || error) &&
29 | !routing.current
30 | ) {
31 | routing.current = true;
32 | location.href = '/login';
33 | }
34 | }, [router, user, error]);
35 |
36 | return <>{children}>;
37 | };
38 |
--------------------------------------------------------------------------------
/src/hooks/useClickOutside.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | /**
4 | * useClickOutside
5 | *
6 | * Simple hook to add an event listener to the body and allow a callback to
7 | * be triggered when clicking outside of the target ref
8 | *
9 | * @param ref Any HTML Element ref
10 | * @param callback Callback triggered when clicking outside of ref element
11 | */
12 | const useClickOutside = (
13 | ref: React.RefObject,
14 | callback: (e: MouseEvent) => void
15 | ): void => {
16 | useEffect(() => {
17 | const handleBodyClick = (e: MouseEvent) => {
18 | if (ref.current && !ref.current.contains(e.target as Node)) {
19 | callback(e);
20 | }
21 | };
22 | document.body.addEventListener('click', handleBodyClick, { capture: true });
23 |
24 | return () => {
25 | document.body.removeEventListener('click', handleBodyClick);
26 | };
27 | }, [ref, callback]);
28 | };
29 |
30 | export default useClickOutside;
31 |
--------------------------------------------------------------------------------
/src/hooks/useDebouncedState.ts:
--------------------------------------------------------------------------------
1 | import type { Dispatch, SetStateAction } from 'react';
2 | import { useEffect, useState } from 'react';
3 |
4 | /**
5 | * A hook to help with debouncing state
6 | *
7 | * This hook basically acts the same as useState except it is also
8 | * returning a deobuncedValue that can be used for things like
9 | * debouncing input into a search field
10 | *
11 | * @param initialValue Initial state value
12 | * @param debounceTime Debounce time in ms
13 | */
14 | const useDebouncedState = (
15 | initialValue: S,
16 | debounceTime = 300
17 | ): [S, S, Dispatch>] => {
18 | const [value, setValue] = useState(initialValue);
19 | const [finalValue, setFinalValue] = useState(initialValue);
20 |
21 | useEffect(() => {
22 | const timeout = setTimeout(() => {
23 | setFinalValue(value);
24 | }, debounceTime);
25 |
26 | return () => {
27 | clearTimeout(timeout);
28 | };
29 | }, [value, debounceTime]);
30 |
31 | return [value, finalValue, setValue];
32 | };
33 |
34 | export default useDebouncedState;
35 |
--------------------------------------------------------------------------------
/src/hooks/useDeepLinks.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | interface useDeepLinksProps {
4 | plexUrl?: string;
5 | plexUrl4k?: string;
6 | iOSPlexUrl?: string;
7 | iOSPlexUrl4k?: string;
8 | }
9 |
10 | const useDeepLinks = ({
11 | plexUrl,
12 | plexUrl4k,
13 | iOSPlexUrl,
14 | iOSPlexUrl4k,
15 | }: useDeepLinksProps) => {
16 | const [returnedPlexUrl, setReturnedPlexUrl] = useState(plexUrl);
17 | const [returnedPlexUrl4k, setReturnedPlexUrl4k] = useState(plexUrl4k);
18 |
19 | useEffect(() => {
20 | if (
21 | /iPad|iPhone|iPod/.test(navigator.userAgent) ||
22 | (navigator.userAgent.includes('Mac') && navigator.maxTouchPoints > 1)
23 | ) {
24 | setReturnedPlexUrl(iOSPlexUrl);
25 | setReturnedPlexUrl4k(iOSPlexUrl4k);
26 | } else {
27 | setReturnedPlexUrl(plexUrl);
28 | setReturnedPlexUrl4k(plexUrl4k);
29 | }
30 | }, [iOSPlexUrl, iOSPlexUrl4k, plexUrl, plexUrl4k]);
31 |
32 | return { plexUrl: returnedPlexUrl, plexUrl4k: returnedPlexUrl4k };
33 | };
34 |
35 | export default useDeepLinks;
36 |
--------------------------------------------------------------------------------
/src/hooks/useIsTouch.ts:
--------------------------------------------------------------------------------
1 | import { InteractionContext } from '@app/context/InteractionContext';
2 | import { useContext } from 'react';
3 |
4 | export const useIsTouch = (): boolean => {
5 | const { isTouch } = useContext(InteractionContext);
6 | return isTouch ?? false;
7 | };
8 |
--------------------------------------------------------------------------------
/src/hooks/useLocale.ts:
--------------------------------------------------------------------------------
1 | import type { LanguageContextProps } from '@app/context/LanguageContext';
2 | import { LanguageContext } from '@app/context/LanguageContext';
3 | import { useContext } from 'react';
4 |
5 | const useLocale = (): Omit => {
6 | const languageContext = useContext(LanguageContext);
7 |
8 | return languageContext;
9 | };
10 |
11 | export default useLocale;
12 |
--------------------------------------------------------------------------------
/src/hooks/useLockBodyScroll.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | /**
4 | * Hook to lock the body scroll whenever a component is mounted or
5 | * whenever isLocked is set to true.
6 | *
7 | * You can pass in true always to cause a lock on mount/dismount of the component
8 | * using this hook.
9 | *
10 | * @param isLocked Toggle the scroll lock
11 | * @param disabled Disables the entire hook (allows conditional skipping of the lock)
12 | */
13 | export const useLockBodyScroll = (
14 | isLocked: boolean,
15 | disabled?: boolean
16 | ): void => {
17 | useEffect(() => {
18 | const originalOverflowStyle = window.getComputedStyle(
19 | document.body
20 | ).overflow;
21 | const originalTouchActionStyle = window.getComputedStyle(
22 | document.body
23 | ).touchAction;
24 | if (isLocked && !disabled) {
25 | document.body.style.overflow = 'hidden';
26 | document.body.style.touchAction = 'none';
27 | }
28 | return () => {
29 | if (!disabled) {
30 | document.body.style.overflow = originalOverflowStyle;
31 | document.body.style.touchAction = originalTouchActionStyle;
32 | }
33 | };
34 | }, [isLocked, disabled]);
35 | };
36 |
--------------------------------------------------------------------------------
/src/hooks/useRouteGuard.ts:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import { useEffect } from 'react';
3 | import type { Permission, PermissionCheckOptions } from './useUser';
4 | import { useUser } from './useUser';
5 |
6 | const useRouteGuard = (
7 | permission: Permission | Permission[],
8 | options?: PermissionCheckOptions
9 | ): void => {
10 | const router = useRouter();
11 | const { user, hasPermission } = useUser();
12 |
13 | useEffect(() => {
14 | if (user && !hasPermission(permission, options)) {
15 | router.push('/');
16 | }
17 | }, [user, permission, router, hasPermission, options]);
18 | };
19 |
20 | export default useRouteGuard;
21 |
--------------------------------------------------------------------------------
/src/hooks/useSettings.ts:
--------------------------------------------------------------------------------
1 | import type { SettingsContextProps } from '@app/context/SettingsContext';
2 | import { SettingsContext } from '@app/context/SettingsContext';
3 | import { useContext } from 'react';
4 |
5 | const useSettings = (): SettingsContextProps => {
6 | const settings = useContext(SettingsContext);
7 |
8 | return settings;
9 | };
10 |
11 | export default useSettings;
12 |
--------------------------------------------------------------------------------
/src/i18n/globalMessages.ts:
--------------------------------------------------------------------------------
1 | import { defineMessages } from 'react-intl';
2 |
3 | const globalMessages = defineMessages({
4 | available: 'Available',
5 | partiallyavailable: 'Partially Available',
6 | deleted: 'Deleted',
7 | processing: 'Processing',
8 | unavailable: 'Unavailable',
9 | notrequested: 'Not Requested',
10 | requested: 'Requested',
11 | requesting: 'Requesting…',
12 | request: 'Request',
13 | request4k: 'Request in 4K',
14 | failed: 'Failed',
15 | pending: 'Pending',
16 | declined: 'Declined',
17 | approved: 'Approved',
18 | completed: 'Completed',
19 | movie: 'Movie',
20 | movies: 'Movies',
21 | collection: 'Collection',
22 | tvshow: 'Series',
23 | tvshows: 'Series',
24 | cancel: 'Cancel',
25 | canceling: 'Canceling…',
26 | approve: 'Approve',
27 | decline: 'Decline',
28 | delete: 'Delete',
29 | retry: 'Retry',
30 | retrying: 'Retrying…',
31 | view: 'View',
32 | deleting: 'Deleting…',
33 | test: 'Test',
34 | testing: 'Testing…',
35 | save: 'Save Changes',
36 | saving: 'Saving…',
37 | import: 'Import',
38 | importing: 'Importing…',
39 | close: 'Close',
40 | edit: 'Edit',
41 | areyousure: 'Are you sure?',
42 | back: 'Back',
43 | next: 'Next',
44 | previous: 'Previous',
45 | status: 'Status',
46 | all: 'All',
47 | experimental: 'Experimental',
48 | advanced: 'Advanced',
49 | restartRequired: 'Restart Required',
50 | loading: 'Loading…',
51 | settings: 'Settings',
52 | usersettings: 'User Settings',
53 | delimitedlist: '{a}, {b}',
54 | showingresults:
55 | 'Showing {from} to {to} of {total} results',
56 | resultsperpage: 'Display {pageSize} results per page',
57 | noresults: 'No results.',
58 | open: 'Open',
59 | resolved: 'Resolved',
60 | specials: 'Specials',
61 | });
62 |
63 | export default globalMessages;
64 |
--------------------------------------------------------------------------------
/src/i18n/locale/sl.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/src/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import PageTitle from '@app/components/Common/PageTitle';
2 | import { ArrowRightCircleIcon } from '@heroicons/react/24/outline';
3 | import Link from 'next/link';
4 | import { defineMessages, useIntl } from 'react-intl';
5 |
6 | const messages = defineMessages({
7 | errormessagewithcode: '{statusCode} - {error}',
8 | pagenotfound: 'Page Not Found',
9 | returnHome: 'Return Home',
10 | });
11 |
12 | const Custom404 = () => {
13 | const intl = useIntl();
14 |
15 | return (
16 |
31 | );
32 | };
33 |
34 | export default Custom404;
35 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import type { DocumentContext, DocumentInitialProps } from 'next/document';
2 | import Document, { Head, Html, Main, NextScript } from 'next/document';
3 |
4 | class MyDocument extends Document {
5 | static async getInitialProps(
6 | ctx: DocumentContext
7 | ): Promise {
8 | const initialProps = await Document.getInitialProps(ctx);
9 |
10 | return initialProps;
11 | }
12 |
13 | render(): JSX.Element {
14 | return (
15 |
16 |
17 |
18 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
30 | }
31 |
32 | export default MyDocument;
33 |
--------------------------------------------------------------------------------
/src/pages/collection/[collectionId]/index.tsx:
--------------------------------------------------------------------------------
1 | import CollectionDetails from '@app/components/CollectionDetails';
2 | import type { Collection } from '@server/models/Collection';
3 | import axios from 'axios';
4 | import type { GetServerSideProps, NextPage } from 'next';
5 |
6 | interface CollectionPageProps {
7 | collection?: Collection;
8 | }
9 |
10 | const CollectionPage: NextPage = ({ collection }) => {
11 | return ;
12 | };
13 |
14 | export const getServerSideProps: GetServerSideProps<
15 | CollectionPageProps
16 | > = async (ctx) => {
17 | const response = await axios.get(
18 | `http://${process.env.HOST || 'localhost'}:${
19 | process.env.PORT || 5055
20 | }/api/v1/collection/${ctx.query.collectionId}`,
21 | {
22 | headers: ctx.req?.headers?.cookie
23 | ? { cookie: ctx.req.headers.cookie }
24 | : undefined,
25 | }
26 | );
27 |
28 | return {
29 | props: {
30 | collection: response.data,
31 | },
32 | };
33 | };
34 |
35 | export default CollectionPage;
36 |
--------------------------------------------------------------------------------
/src/pages/discover/movies/genre/[genreId]/index.tsx:
--------------------------------------------------------------------------------
1 | import DiscoverMovieGenre from '@app/components/Discover/DiscoverMovieGenre';
2 | import type { NextPage } from 'next';
3 |
4 | const DiscoverMoviesGenrePage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default DiscoverMoviesGenrePage;
9 |
--------------------------------------------------------------------------------
/src/pages/discover/movies/genres.tsx:
--------------------------------------------------------------------------------
1 | import MovieGenreList from '@app/components/Discover/MovieGenreList';
2 | import type { NextPage } from 'next';
3 |
4 | const MovieGenresPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default MovieGenresPage;
9 |
--------------------------------------------------------------------------------
/src/pages/discover/movies/index.tsx:
--------------------------------------------------------------------------------
1 | import DiscoverMovies from '@app/components/Discover/DiscoverMovies';
2 | import type { NextPage } from 'next';
3 |
4 | const DiscoverMoviesPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default DiscoverMoviesPage;
9 |
--------------------------------------------------------------------------------
/src/pages/discover/movies/keyword/index.tsx:
--------------------------------------------------------------------------------
1 | import DiscoverMovieKeyword from '@app/components/Discover/DiscoverMovieKeyword';
2 | import type { NextPage } from 'next';
3 |
4 | const DiscoverMoviesKeywordPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default DiscoverMoviesKeywordPage;
9 |
--------------------------------------------------------------------------------
/src/pages/discover/movies/language/[language]/index.tsx:
--------------------------------------------------------------------------------
1 | import DiscoverMovieLanguage from '@app/components/Discover/DiscoverMovieLanguage';
2 | import type { NextPage } from 'next';
3 |
4 | const DiscoverMovieLanguagePage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default DiscoverMovieLanguagePage;
9 |
--------------------------------------------------------------------------------
/src/pages/discover/movies/studio/[studioId]/index.tsx:
--------------------------------------------------------------------------------
1 | import DiscoverMovieStudio from '@app/components/Discover/DiscoverStudio';
2 | import type { NextPage } from 'next';
3 |
4 | const DiscoverMoviesStudioPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default DiscoverMoviesStudioPage;
9 |
--------------------------------------------------------------------------------
/src/pages/discover/movies/upcoming.tsx:
--------------------------------------------------------------------------------
1 | import UpcomingMovies from '@app/components/Discover/Upcoming';
2 | import type { NextPage } from 'next';
3 |
4 | const UpcomingMoviesPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default UpcomingMoviesPage;
9 |
--------------------------------------------------------------------------------
/src/pages/discover/trending.tsx:
--------------------------------------------------------------------------------
1 | import Trending from '@app/components/Discover/Trending';
2 | import type { NextPage } from 'next';
3 |
4 | const TrendingPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default TrendingPage;
9 |
--------------------------------------------------------------------------------
/src/pages/discover/tv/genre/[genreId]/index.tsx:
--------------------------------------------------------------------------------
1 | import DiscoverTvGenre from '@app/components/Discover/DiscoverTvGenre';
2 | import type { NextPage } from 'next';
3 |
4 | const DiscoverTvGenrePage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default DiscoverTvGenrePage;
9 |
--------------------------------------------------------------------------------
/src/pages/discover/tv/genres.tsx:
--------------------------------------------------------------------------------
1 | import TvGenreList from '@app/components/Discover/TvGenreList';
2 | import type { NextPage } from 'next';
3 |
4 | const TvGenresPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default TvGenresPage;
9 |
--------------------------------------------------------------------------------
/src/pages/discover/tv/index.tsx:
--------------------------------------------------------------------------------
1 | import DiscoverTv from '@app/components/Discover/DiscoverTv';
2 | import type { NextPage } from 'next';
3 |
4 | const DiscoverTvPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default DiscoverTvPage;
9 |
--------------------------------------------------------------------------------
/src/pages/discover/tv/keyword/index.tsx:
--------------------------------------------------------------------------------
1 | import DiscoverTvKeyword from '@app/components/Discover/DiscoverTvKeyword';
2 | import type { NextPage } from 'next';
3 |
4 | const DiscoverTvKeywordPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default DiscoverTvKeywordPage;
9 |
--------------------------------------------------------------------------------
/src/pages/discover/tv/language/[language]/index.tsx:
--------------------------------------------------------------------------------
1 | import DiscoverTvLanguage from '@app/components/Discover/DiscoverTvLanguage';
2 | import type { NextPage } from 'next';
3 |
4 | const DiscoverTvLanguagePage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default DiscoverTvLanguagePage;
9 |
--------------------------------------------------------------------------------
/src/pages/discover/tv/network/[networkId]/index.tsx:
--------------------------------------------------------------------------------
1 | import DiscoverNetwork from '@app/components/Discover/DiscoverNetwork';
2 | import type { NextPage } from 'next';
3 |
4 | const DiscoverTvNetworkPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default DiscoverTvNetworkPage;
9 |
--------------------------------------------------------------------------------
/src/pages/discover/tv/upcoming.tsx:
--------------------------------------------------------------------------------
1 | import DiscoverTvUpcoming from '@app/components/Discover/DiscoverTvUpcoming';
2 | import type { NextPage } from 'next';
3 |
4 | const DiscoverTvPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default DiscoverTvPage;
9 |
--------------------------------------------------------------------------------
/src/pages/discover/watchlist.tsx:
--------------------------------------------------------------------------------
1 | import DiscoverWatchlist from '@app/components/Discover/DiscoverWatchlist';
2 | import type { NextPage } from 'next';
3 |
4 | const WatchlistPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default WatchlistPage;
9 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import Discover from '@app/components/Discover';
2 | import type { NextPage } from 'next';
3 |
4 | const Index: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default Index;
9 |
--------------------------------------------------------------------------------
/src/pages/issues/[issueId]/index.tsx:
--------------------------------------------------------------------------------
1 | import IssueDetails from '@app/components/IssueDetails';
2 | import useRouteGuard from '@app/hooks/useRouteGuard';
3 | import { Permission } from '@app/hooks/useUser';
4 | import type { NextPage } from 'next';
5 |
6 | const IssuePage: NextPage = () => {
7 | useRouteGuard(
8 | [
9 | Permission.MANAGE_ISSUES,
10 | Permission.CREATE_ISSUES,
11 | Permission.VIEW_ISSUES,
12 | ],
13 | {
14 | type: 'or',
15 | }
16 | );
17 | return ;
18 | };
19 |
20 | export default IssuePage;
21 |
--------------------------------------------------------------------------------
/src/pages/issues/index.tsx:
--------------------------------------------------------------------------------
1 | import IssueList from '@app/components/IssueList';
2 | import useRouteGuard from '@app/hooks/useRouteGuard';
3 | import { Permission } from '@app/hooks/useUser';
4 | import type { NextPage } from 'next';
5 |
6 | const IssuePage: NextPage = () => {
7 | useRouteGuard(
8 | [
9 | Permission.MANAGE_ISSUES,
10 | Permission.CREATE_ISSUES,
11 | Permission.VIEW_ISSUES,
12 | ],
13 | {
14 | type: 'or',
15 | }
16 | );
17 | return ;
18 | };
19 |
20 | export default IssuePage;
21 |
--------------------------------------------------------------------------------
/src/pages/login/index.tsx:
--------------------------------------------------------------------------------
1 | import Login from '@app/components/Login';
2 | import type { NextPage } from 'next';
3 |
4 | const LoginPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default LoginPage;
9 |
--------------------------------------------------------------------------------
/src/pages/login/plex/loading.tsx:
--------------------------------------------------------------------------------
1 | import LoadingSpinner from '@app/components/Common/LoadingSpinner';
2 |
3 | const PlexLoading = () => {
4 | return (
5 |
6 |
7 |
8 | );
9 | };
10 |
11 | export default PlexLoading;
12 |
--------------------------------------------------------------------------------
/src/pages/movie/[movieId]/cast.tsx:
--------------------------------------------------------------------------------
1 | import MovieCast from '@app/components/MovieDetails/MovieCast';
2 | import type { NextPage } from 'next';
3 |
4 | const MovieCastPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default MovieCastPage;
9 |
--------------------------------------------------------------------------------
/src/pages/movie/[movieId]/crew.tsx:
--------------------------------------------------------------------------------
1 | import MovieCrew from '@app/components/MovieDetails/MovieCrew';
2 | import type { NextPage } from 'next';
3 |
4 | const MovieCrewPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default MovieCrewPage;
9 |
--------------------------------------------------------------------------------
/src/pages/movie/[movieId]/index.tsx:
--------------------------------------------------------------------------------
1 | import MovieDetails from '@app/components/MovieDetails';
2 | import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
3 | import axios from 'axios';
4 | import type { GetServerSideProps, NextPage } from 'next';
5 |
6 | interface MoviePageProps {
7 | movie?: MovieDetailsType;
8 | }
9 |
10 | const MoviePage: NextPage = ({ movie }) => {
11 | return ;
12 | };
13 |
14 | export const getServerSideProps: GetServerSideProps = async (
15 | ctx
16 | ) => {
17 | const response = await axios.get(
18 | `http://${process.env.HOST || 'localhost'}:${
19 | process.env.PORT || 5055
20 | }/api/v1/movie/${ctx.query.movieId}`,
21 | {
22 | headers: ctx.req?.headers?.cookie
23 | ? { cookie: ctx.req.headers.cookie }
24 | : undefined,
25 | }
26 | );
27 |
28 | return {
29 | props: {
30 | movie: response.data,
31 | },
32 | };
33 | };
34 |
35 | export default MoviePage;
36 |
--------------------------------------------------------------------------------
/src/pages/movie/[movieId]/recommendations.tsx:
--------------------------------------------------------------------------------
1 | import MovieRecommendations from '@app/components/MovieDetails/MovieRecommendations';
2 | import type { NextPage } from 'next';
3 |
4 | const MovieRecommendationsPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default MovieRecommendationsPage;
9 |
--------------------------------------------------------------------------------
/src/pages/movie/[movieId]/similar.tsx:
--------------------------------------------------------------------------------
1 | import MovieSimilar from '@app/components/MovieDetails/MovieSimilar';
2 | import type { NextPage } from 'next';
3 |
4 | const MovieSimilarPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default MovieSimilarPage;
9 |
--------------------------------------------------------------------------------
/src/pages/person/[personId]/index.tsx:
--------------------------------------------------------------------------------
1 | import PersonDetails from '@app/components/PersonDetails';
2 | import type { NextPage } from 'next';
3 |
4 | const MoviePage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default MoviePage;
9 |
--------------------------------------------------------------------------------
/src/pages/profile/index.tsx:
--------------------------------------------------------------------------------
1 | import UserProfile from '@app/components/UserProfile';
2 | import type { NextPage } from 'next';
3 |
4 | const UserPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default UserPage;
9 |
--------------------------------------------------------------------------------
/src/pages/profile/requests.tsx:
--------------------------------------------------------------------------------
1 | import RequestList from '@app/components/RequestList';
2 | import type { NextPage } from 'next';
3 |
4 | const UserRequestsPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default UserRequestsPage;
9 |
--------------------------------------------------------------------------------
/src/pages/profile/settings/index.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserGeneralSettings from '@app/components/UserProfile/UserSettings/UserGeneralSettings';
3 | import type { NextPage } from 'next';
4 |
5 | const UserSettingsPage: NextPage = () => {
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default UserSettingsPage;
14 |
--------------------------------------------------------------------------------
/src/pages/profile/settings/main.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserGeneralSettings from '@app/components/UserProfile/UserSettings/UserGeneralSettings';
3 | import type { NextPage } from 'next';
4 |
5 | const UserSettingsMainPage: NextPage = () => {
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default UserSettingsMainPage;
14 |
--------------------------------------------------------------------------------
/src/pages/profile/settings/notifications/discord.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserNotificationSettings from '@app/components/UserProfile/UserSettings/UserNotificationSettings';
3 | import UserNotificationsDiscord from '@app/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord';
4 | import type { NextPage } from 'next';
5 |
6 | const NotificationsPage: NextPage = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default NotificationsPage;
17 |
--------------------------------------------------------------------------------
/src/pages/profile/settings/notifications/email.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserNotificationSettings from '@app/components/UserProfile/UserSettings/UserNotificationSettings';
3 | import UserNotificationsEmail from '@app/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail';
4 | import type { NextPage } from 'next';
5 |
6 | const NotificationsPage: NextPage = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default NotificationsPage;
17 |
--------------------------------------------------------------------------------
/src/pages/profile/settings/notifications/pushbullet.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserNotificationSettings from '@app/components/UserProfile/UserSettings/UserNotificationSettings';
3 | import UserNotificationsPushbullet from '@app/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushbullet';
4 | import type { NextPage } from 'next';
5 |
6 | const NotificationsPage: NextPage = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default NotificationsPage;
17 |
--------------------------------------------------------------------------------
/src/pages/profile/settings/notifications/pushover.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserNotificationSettings from '@app/components/UserProfile/UserSettings/UserNotificationSettings';
3 | import UserNotificationsPushover from '@app/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushover';
4 | import type { NextPage } from 'next';
5 |
6 | const NotificationsPage: NextPage = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default NotificationsPage;
17 |
--------------------------------------------------------------------------------
/src/pages/profile/settings/notifications/telegram.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserNotificationSettings from '@app/components/UserProfile/UserSettings/UserNotificationSettings';
3 | import UserNotificationsTelegram from '@app/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram';
4 | import type { NextPage } from 'next';
5 |
6 | const NotificationsPage: NextPage = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default NotificationsPage;
17 |
--------------------------------------------------------------------------------
/src/pages/profile/settings/notifications/webpush.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserNotificationSettings from '@app/components/UserProfile/UserSettings/UserNotificationSettings';
3 | import UserWebPushSettings from '@app/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush';
4 | import type { NextPage } from 'next';
5 |
6 | const WebPushProfileNotificationsPage: NextPage = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default WebPushProfileNotificationsPage;
17 |
--------------------------------------------------------------------------------
/src/pages/profile/settings/password.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserPasswordChange from '@app/components/UserProfile/UserSettings/UserPasswordChange';
3 | import type { NextPage } from 'next';
4 |
5 | const UserPassswordPage: NextPage = () => {
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default UserPassswordPage;
14 |
--------------------------------------------------------------------------------
/src/pages/profile/settings/permissions.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserPermissions from '@app/components/UserProfile/UserSettings/UserPermissions';
3 | import type { NextPage } from 'next';
4 |
5 | const UserPermissionsPage: NextPage = () => {
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default UserPermissionsPage;
14 |
--------------------------------------------------------------------------------
/src/pages/profile/watchlist.tsx:
--------------------------------------------------------------------------------
1 | import DiscoverWatchlist from '@app/components/Discover/DiscoverWatchlist';
2 | import type { NextPage } from 'next';
3 |
4 | const UserWatchlistPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default UserWatchlistPage;
9 |
--------------------------------------------------------------------------------
/src/pages/requests/index.tsx:
--------------------------------------------------------------------------------
1 | import RequestList from '@app/components/RequestList';
2 | import type { NextPage } from 'next';
3 |
4 | const RequestsPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default RequestsPage;
9 |
--------------------------------------------------------------------------------
/src/pages/resetpassword/[guid]/index.tsx:
--------------------------------------------------------------------------------
1 | import ResetPassword from '@app/components/ResetPassword';
2 | import type { NextPage } from 'next';
3 |
4 | const ResetPasswordPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default ResetPasswordPage;
9 |
--------------------------------------------------------------------------------
/src/pages/resetpassword/index.tsx:
--------------------------------------------------------------------------------
1 | import RequestResetLink from '@app/components/ResetPassword/RequestResetLink';
2 | import type { NextPage } from 'next';
3 |
4 | const RequestResetLinkPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default RequestResetLinkPage;
9 |
--------------------------------------------------------------------------------
/src/pages/search.tsx:
--------------------------------------------------------------------------------
1 | import Search from '@app/components/Search';
2 |
3 | const SearchPage = () => {
4 | return ;
5 | };
6 |
7 | export default SearchPage;
8 |
--------------------------------------------------------------------------------
/src/pages/settings/about.tsx:
--------------------------------------------------------------------------------
1 | import SettingsAbout from '@app/components/Settings/SettingsAbout';
2 | import SettingsLayout from '@app/components/Settings/SettingsLayout';
3 | import useRouteGuard from '@app/hooks/useRouteGuard';
4 | import { Permission } from '@app/hooks/useUser';
5 | import type { NextPage } from 'next';
6 |
7 | const SettingsAboutPage: NextPage = () => {
8 | useRouteGuard(Permission.ADMIN);
9 | return (
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default SettingsAboutPage;
17 |
--------------------------------------------------------------------------------
/src/pages/settings/index.tsx:
--------------------------------------------------------------------------------
1 | import SettingsLayout from '@app/components/Settings/SettingsLayout';
2 | import SettingsMain from '@app/components/Settings/SettingsMain';
3 | import useRouteGuard from '@app/hooks/useRouteGuard';
4 | import { Permission } from '@app/hooks/useUser';
5 | import type { NextPage } from 'next';
6 |
7 | const SettingsPage: NextPage = () => {
8 | useRouteGuard(Permission.ADMIN);
9 | return (
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default SettingsPage;
17 |
--------------------------------------------------------------------------------
/src/pages/settings/jobs.tsx:
--------------------------------------------------------------------------------
1 | import SettingsJobs from '@app/components/Settings/SettingsJobsCache';
2 | import SettingsLayout from '@app/components/Settings/SettingsLayout';
3 | import useRouteGuard from '@app/hooks/useRouteGuard';
4 | import { Permission } from '@app/hooks/useUser';
5 | import type { NextPage } from 'next';
6 |
7 | const SettingsMainPage: NextPage = () => {
8 | useRouteGuard(Permission.ADMIN);
9 | return (
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default SettingsMainPage;
17 |
--------------------------------------------------------------------------------
/src/pages/settings/logs.tsx:
--------------------------------------------------------------------------------
1 | import SettingsLayout from '@app/components/Settings/SettingsLayout';
2 | import SettingsLogs from '@app/components/Settings/SettingsLogs';
3 | import useRouteGuard from '@app/hooks/useRouteGuard';
4 | import { Permission } from '@app/hooks/useUser';
5 | import type { NextPage } from 'next';
6 |
7 | const SettingsLogsPage: NextPage = () => {
8 | useRouteGuard(Permission.ADMIN);
9 | return (
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default SettingsLogsPage;
17 |
--------------------------------------------------------------------------------
/src/pages/settings/main.tsx:
--------------------------------------------------------------------------------
1 | import SettingsLayout from '@app/components/Settings/SettingsLayout';
2 | import SettingsMain from '@app/components/Settings/SettingsMain';
3 | import useRouteGuard from '@app/hooks/useRouteGuard';
4 | import { Permission } from '@app/hooks/useUser';
5 | import type { NextPage } from 'next';
6 |
7 | const SettingsMainPage: NextPage = () => {
8 | useRouteGuard(Permission.ADMIN);
9 | return (
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default SettingsMainPage;
17 |
--------------------------------------------------------------------------------
/src/pages/settings/notifications/discord.tsx:
--------------------------------------------------------------------------------
1 | import NotificationsDiscord from '@app/components/Settings/Notifications/NotificationsDiscord';
2 | import SettingsLayout from '@app/components/Settings/SettingsLayout';
3 | import SettingsNotifications from '@app/components/Settings/SettingsNotifications';
4 | import useRouteGuard from '@app/hooks/useRouteGuard';
5 | import { Permission } from '@app/hooks/useUser';
6 | import type { NextPage } from 'next';
7 |
8 | const NotificationsPage: NextPage = () => {
9 | useRouteGuard(Permission.ADMIN);
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default NotificationsPage;
20 |
--------------------------------------------------------------------------------
/src/pages/settings/notifications/email.tsx:
--------------------------------------------------------------------------------
1 | import NotificationsEmail from '@app/components/Settings/Notifications/NotificationsEmail';
2 | import SettingsLayout from '@app/components/Settings/SettingsLayout';
3 | import SettingsNotifications from '@app/components/Settings/SettingsNotifications';
4 | import useRouteGuard from '@app/hooks/useRouteGuard';
5 | import { Permission } from '@app/hooks/useUser';
6 | import type { NextPage } from 'next';
7 |
8 | const NotificationsPage: NextPage = () => {
9 | useRouteGuard(Permission.ADMIN);
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default NotificationsPage;
20 |
--------------------------------------------------------------------------------
/src/pages/settings/notifications/gotify.tsx:
--------------------------------------------------------------------------------
1 | import NotificationsGotify from '@app/components/Settings/Notifications/NotificationsGotify';
2 | import SettingsLayout from '@app/components/Settings/SettingsLayout';
3 | import SettingsNotifications from '@app/components/Settings/SettingsNotifications';
4 | import useRouteGuard from '@app/hooks/useRouteGuard';
5 | import { Permission } from '@app/hooks/useUser';
6 | import type { NextPage } from 'next';
7 |
8 | const NotificationsPage: NextPage = () => {
9 | useRouteGuard(Permission.ADMIN);
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default NotificationsPage;
20 |
--------------------------------------------------------------------------------
/src/pages/settings/notifications/lunasea.tsx:
--------------------------------------------------------------------------------
1 | import NotificationsLunaSea from '@app/components/Settings/Notifications/NotificationsLunaSea';
2 | import SettingsLayout from '@app/components/Settings/SettingsLayout';
3 | import SettingsNotifications from '@app/components/Settings/SettingsNotifications';
4 | import useRouteGuard from '@app/hooks/useRouteGuard';
5 | import { Permission } from '@app/hooks/useUser';
6 | import type { NextPage } from 'next';
7 |
8 | const NotificationsPage: NextPage = () => {
9 | useRouteGuard(Permission.ADMIN);
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default NotificationsPage;
20 |
--------------------------------------------------------------------------------
/src/pages/settings/notifications/pushbullet.tsx:
--------------------------------------------------------------------------------
1 | import NotificationsPushbullet from '@app/components/Settings/Notifications/NotificationsPushbullet';
2 | import SettingsLayout from '@app/components/Settings/SettingsLayout';
3 | import SettingsNotifications from '@app/components/Settings/SettingsNotifications';
4 | import useRouteGuard from '@app/hooks/useRouteGuard';
5 | import { Permission } from '@app/hooks/useUser';
6 | import type { NextPage } from 'next';
7 |
8 | const NotificationsPage: NextPage = () => {
9 | useRouteGuard(Permission.ADMIN);
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default NotificationsPage;
20 |
--------------------------------------------------------------------------------
/src/pages/settings/notifications/pushover.tsx:
--------------------------------------------------------------------------------
1 | import NotificationsPushover from '@app/components/Settings/Notifications/NotificationsPushover';
2 | import SettingsLayout from '@app/components/Settings/SettingsLayout';
3 | import SettingsNotifications from '@app/components/Settings/SettingsNotifications';
4 | import useRouteGuard from '@app/hooks/useRouteGuard';
5 | import { Permission } from '@app/hooks/useUser';
6 | import type { NextPage } from 'next';
7 |
8 | const NotificationsPage: NextPage = () => {
9 | useRouteGuard(Permission.ADMIN);
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default NotificationsPage;
20 |
--------------------------------------------------------------------------------
/src/pages/settings/notifications/slack.tsx:
--------------------------------------------------------------------------------
1 | import NotificationsSlack from '@app/components/Settings/Notifications/NotificationsSlack';
2 | import SettingsLayout from '@app/components/Settings/SettingsLayout';
3 | import SettingsNotifications from '@app/components/Settings/SettingsNotifications';
4 | import useRouteGuard from '@app/hooks/useRouteGuard';
5 | import { Permission } from '@app/hooks/useUser';
6 | import type { NextPage } from 'next';
7 |
8 | const NotificationsSlackPage: NextPage = () => {
9 | useRouteGuard(Permission.ADMIN);
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default NotificationsSlackPage;
20 |
--------------------------------------------------------------------------------
/src/pages/settings/notifications/telegram.tsx:
--------------------------------------------------------------------------------
1 | import NotificationsTelegram from '@app/components/Settings/Notifications/NotificationsTelegram';
2 | import SettingsLayout from '@app/components/Settings/SettingsLayout';
3 | import SettingsNotifications from '@app/components/Settings/SettingsNotifications';
4 | import useRouteGuard from '@app/hooks/useRouteGuard';
5 | import { Permission } from '@app/hooks/useUser';
6 | import type { NextPage } from 'next';
7 |
8 | const NotificationsPage: NextPage = () => {
9 | useRouteGuard(Permission.ADMIN);
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default NotificationsPage;
20 |
--------------------------------------------------------------------------------
/src/pages/settings/notifications/webhook.tsx:
--------------------------------------------------------------------------------
1 | import NotificationsWebhook from '@app/components/Settings/Notifications/NotificationsWebhook';
2 | import SettingsLayout from '@app/components/Settings/SettingsLayout';
3 | import SettingsNotifications from '@app/components/Settings/SettingsNotifications';
4 | import useRouteGuard from '@app/hooks/useRouteGuard';
5 | import { Permission } from '@app/hooks/useUser';
6 | import type { NextPage } from 'next';
7 |
8 | const NotificationsPage: NextPage = () => {
9 | useRouteGuard(Permission.ADMIN);
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default NotificationsPage;
20 |
--------------------------------------------------------------------------------
/src/pages/settings/notifications/webpush.tsx:
--------------------------------------------------------------------------------
1 | import NotificationsWebPush from '@app/components/Settings/Notifications/NotificationsWebPush';
2 | import SettingsLayout from '@app/components/Settings/SettingsLayout';
3 | import SettingsNotifications from '@app/components/Settings/SettingsNotifications';
4 | import useRouteGuard from '@app/hooks/useRouteGuard';
5 | import { Permission } from '@app/hooks/useUser';
6 | import type { NextPage } from 'next';
7 |
8 | const NotificationsWebPushPage: NextPage = () => {
9 | useRouteGuard(Permission.ADMIN);
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default NotificationsWebPushPage;
20 |
--------------------------------------------------------------------------------
/src/pages/settings/plex.tsx:
--------------------------------------------------------------------------------
1 | import SettingsLayout from '@app/components/Settings/SettingsLayout';
2 | import SettingsPlex from '@app/components/Settings/SettingsPlex';
3 | import useRouteGuard from '@app/hooks/useRouteGuard';
4 | import { Permission } from '@app/hooks/useUser';
5 | import type { NextPage } from 'next';
6 |
7 | const PlexSettingsPage: NextPage = () => {
8 | useRouteGuard(Permission.ADMIN);
9 | return (
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default PlexSettingsPage;
17 |
--------------------------------------------------------------------------------
/src/pages/settings/services.tsx:
--------------------------------------------------------------------------------
1 | import SettingsLayout from '@app/components/Settings/SettingsLayout';
2 | import SettingsServices from '@app/components/Settings/SettingsServices';
3 | import useRouteGuard from '@app/hooks/useRouteGuard';
4 | import { Permission } from '@app/hooks/useUser';
5 | import type { NextPage } from 'next';
6 |
7 | const ServicesSettingsPage: NextPage = () => {
8 | useRouteGuard(Permission.ADMIN);
9 | return (
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default ServicesSettingsPage;
17 |
--------------------------------------------------------------------------------
/src/pages/settings/users.tsx:
--------------------------------------------------------------------------------
1 | import SettingsLayout from '@app/components/Settings/SettingsLayout';
2 | import SettingsUsers from '@app/components/Settings/SettingsUsers';
3 | import useRouteGuard from '@app/hooks/useRouteGuard';
4 | import { Permission } from '@app/hooks/useUser';
5 | import type { NextPage } from 'next';
6 |
7 | const SettingsUsersPage: NextPage = () => {
8 | useRouteGuard(Permission.ADMIN);
9 | return (
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default SettingsUsersPage;
17 |
--------------------------------------------------------------------------------
/src/pages/setup.tsx:
--------------------------------------------------------------------------------
1 | import Setup from '@app/components/Setup';
2 | import type { NextPage } from 'next';
3 |
4 | const SetupPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default SetupPage;
9 |
--------------------------------------------------------------------------------
/src/pages/tv/[tvId]/cast.tsx:
--------------------------------------------------------------------------------
1 | import TvCast from '@app/components/TvDetails/TvCast';
2 | import type { NextPage } from 'next';
3 |
4 | const TvCastPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default TvCastPage;
9 |
--------------------------------------------------------------------------------
/src/pages/tv/[tvId]/crew.tsx:
--------------------------------------------------------------------------------
1 | import TvCrew from '@app/components/TvDetails/TvCrew';
2 | import type { NextPage } from 'next';
3 |
4 | const TvCrewPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default TvCrewPage;
9 |
--------------------------------------------------------------------------------
/src/pages/tv/[tvId]/index.tsx:
--------------------------------------------------------------------------------
1 | import TvDetails from '@app/components/TvDetails';
2 | import type { TvDetails as TvDetailsType } from '@server/models/Tv';
3 | import axios from 'axios';
4 | import type { GetServerSideProps, NextPage } from 'next';
5 |
6 | interface TvPageProps {
7 | tv?: TvDetailsType;
8 | }
9 |
10 | const TvPage: NextPage = ({ tv }) => {
11 | return ;
12 | };
13 |
14 | export const getServerSideProps: GetServerSideProps = async (
15 | ctx
16 | ) => {
17 | const response = await axios.get(
18 | `http://${process.env.HOST || 'localhost'}:${
19 | process.env.PORT || 5055
20 | }/api/v1/tv/${ctx.query.tvId}`,
21 | {
22 | headers: ctx.req?.headers?.cookie
23 | ? { cookie: ctx.req.headers.cookie }
24 | : undefined,
25 | }
26 | );
27 |
28 | return {
29 | props: {
30 | tv: response.data,
31 | },
32 | };
33 | };
34 |
35 | export default TvPage;
36 |
--------------------------------------------------------------------------------
/src/pages/tv/[tvId]/recommendations.tsx:
--------------------------------------------------------------------------------
1 | import TvRecommendations from '@app/components/TvDetails/TvRecommendations';
2 | import type { NextPage } from 'next';
3 |
4 | const TvRecommendationsPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default TvRecommendationsPage;
9 |
--------------------------------------------------------------------------------
/src/pages/tv/[tvId]/similar.tsx:
--------------------------------------------------------------------------------
1 | import TvSimilar from '@app/components/TvDetails/TvSimilar';
2 | import type { NextPage } from 'next';
3 |
4 | const TvSimilarPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default TvSimilarPage;
9 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/index.tsx:
--------------------------------------------------------------------------------
1 | import UserProfile from '@app/components/UserProfile';
2 | import type { NextPage } from 'next';
3 |
4 | const UserPage: NextPage = () => {
5 | return ;
6 | };
7 |
8 | export default UserPage;
9 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/requests.tsx:
--------------------------------------------------------------------------------
1 | import RequestList from '@app/components/RequestList';
2 | import useRouteGuard from '@app/hooks/useRouteGuard';
3 | import { Permission } from '@app/hooks/useUser';
4 | import type { NextPage } from 'next';
5 |
6 | const UserRequestsPage: NextPage = () => {
7 | useRouteGuard([Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW], {
8 | type: 'or',
9 | });
10 | return ;
11 | };
12 |
13 | export default UserRequestsPage;
14 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/settings/index.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserGeneralSettings from '@app/components/UserProfile/UserSettings/UserGeneralSettings';
3 | import useRouteGuard from '@app/hooks/useRouteGuard';
4 | import { Permission } from '@app/hooks/useUser';
5 | import type { NextPage } from 'next';
6 |
7 | const UserSettingsPage: NextPage = () => {
8 | useRouteGuard(Permission.MANAGE_USERS);
9 | return (
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default UserSettingsPage;
17 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/settings/main.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserGeneralSettings from '@app/components/UserProfile/UserSettings/UserGeneralSettings';
3 | import useRouteGuard from '@app/hooks/useRouteGuard';
4 | import { Permission } from '@app/hooks/useUser';
5 | import type { NextPage } from 'next';
6 |
7 | const UserSettingsMainPage: NextPage = () => {
8 | useRouteGuard(Permission.MANAGE_USERS);
9 | return (
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default UserSettingsMainPage;
17 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/settings/notifications/discord.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserNotificationSettings from '@app/components/UserProfile/UserSettings/UserNotificationSettings';
3 | import UserNotificationsDiscord from '@app/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord';
4 | import useRouteGuard from '@app/hooks/useRouteGuard';
5 | import { Permission } from '@app/hooks/useUser';
6 | import type { NextPage } from 'next';
7 |
8 | const NotificationsPage: NextPage = () => {
9 | useRouteGuard(Permission.MANAGE_USERS);
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default NotificationsPage;
20 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/settings/notifications/email.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserNotificationSettings from '@app/components/UserProfile/UserSettings/UserNotificationSettings';
3 | import UserNotificationsEmail from '@app/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail';
4 | import useRouteGuard from '@app/hooks/useRouteGuard';
5 | import { Permission } from '@app/hooks/useUser';
6 | import type { NextPage } from 'next';
7 |
8 | const NotificationsPage: NextPage = () => {
9 | useRouteGuard(Permission.MANAGE_USERS);
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default NotificationsPage;
20 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/settings/notifications/pushbullet.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserNotificationSettings from '@app/components/UserProfile/UserSettings/UserNotificationSettings';
3 | import UserNotificationsPushbullet from '@app/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushbullet';
4 | import useRouteGuard from '@app/hooks/useRouteGuard';
5 | import { Permission } from '@app/hooks/useUser';
6 | import type { NextPage } from 'next';
7 |
8 | const NotificationsPage: NextPage = () => {
9 | useRouteGuard(Permission.MANAGE_USERS);
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default NotificationsPage;
20 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/settings/notifications/pushover.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserNotificationSettings from '@app/components/UserProfile/UserSettings/UserNotificationSettings';
3 | import UserNotificationsPushover from '@app/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushover';
4 | import useRouteGuard from '@app/hooks/useRouteGuard';
5 | import { Permission } from '@app/hooks/useUser';
6 | import type { NextPage } from 'next';
7 |
8 | const NotificationsPage: NextPage = () => {
9 | useRouteGuard(Permission.MANAGE_USERS);
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default NotificationsPage;
20 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/settings/notifications/telegram.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserNotificationSettings from '@app/components/UserProfile/UserSettings/UserNotificationSettings';
3 | import UserNotificationsTelegram from '@app/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram';
4 | import useRouteGuard from '@app/hooks/useRouteGuard';
5 | import { Permission } from '@app/hooks/useUser';
6 | import type { NextPage } from 'next';
7 |
8 | const NotificationsPage: NextPage = () => {
9 | useRouteGuard(Permission.MANAGE_USERS);
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default NotificationsPage;
20 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/settings/notifications/webpush.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserNotificationSettings from '@app/components/UserProfile/UserSettings/UserNotificationSettings';
3 | import UserWebPushSettings from '@app/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush';
4 | import useRouteGuard from '@app/hooks/useRouteGuard';
5 | import { Permission } from '@app/hooks/useUser';
6 | import type { NextPage } from 'next';
7 |
8 | const WebPushNotificationsPage: NextPage = () => {
9 | useRouteGuard(Permission.MANAGE_USERS);
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default WebPushNotificationsPage;
20 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/settings/password.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserPasswordChange from '@app/components/UserProfile/UserSettings/UserPasswordChange';
3 | import useRouteGuard from '@app/hooks/useRouteGuard';
4 | import { Permission } from '@app/hooks/useUser';
5 | import type { NextPage } from 'next';
6 |
7 | const UserPassswordPage: NextPage = () => {
8 | useRouteGuard(Permission.MANAGE_USERS);
9 | return (
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default UserPassswordPage;
17 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/settings/permissions.tsx:
--------------------------------------------------------------------------------
1 | import UserSettings from '@app/components/UserProfile/UserSettings';
2 | import UserPermissions from '@app/components/UserProfile/UserSettings/UserPermissions';
3 | import useRouteGuard from '@app/hooks/useRouteGuard';
4 | import { Permission } from '@app/hooks/useUser';
5 | import type { NextPage } from 'next';
6 |
7 | const UserPermissionsPage: NextPage = () => {
8 | useRouteGuard(Permission.MANAGE_USERS);
9 | return (
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default UserPermissionsPage;
17 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/watchlist.tsx:
--------------------------------------------------------------------------------
1 | import DiscoverWatchlist from '@app/components/Discover/DiscoverWatchlist';
2 | import useRouteGuard from '@app/hooks/useRouteGuard';
3 | import { Permission } from '@app/hooks/useUser';
4 | import type { NextPage } from 'next';
5 |
6 | const UserRequestsPage: NextPage = () => {
7 | useRouteGuard([Permission.MANAGE_REQUESTS, Permission.WATCHLIST_VIEW], {
8 | type: 'or',
9 | });
10 | return ;
11 | };
12 |
13 | export default UserRequestsPage;
14 |
--------------------------------------------------------------------------------
/src/pages/users/index.tsx:
--------------------------------------------------------------------------------
1 | import UserList from '@app/components/UserList';
2 | import useRouteGuard from '@app/hooks/useRouteGuard';
3 | import { Permission } from '@app/hooks/useUser';
4 | import type { NextPage } from 'next';
5 |
6 | const UsersPage: NextPage = () => {
7 | useRouteGuard(Permission.MANAGE_USERS);
8 | return ;
9 | };
10 |
11 | export default UsersPage;
12 |
--------------------------------------------------------------------------------
/src/types/custom.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | declare module '*.svg' {
3 | const content: any;
4 | export default content;
5 | }
6 |
7 | declare module '*.jpg' {
8 | const content: any;
9 | export default content;
10 | }
11 | declare module '*.jpeg' {
12 | const content: any;
13 | export default content;
14 | }
15 |
16 | declare module '*.gif' {
17 | const content: any;
18 | export default content;
19 | }
20 |
21 | declare module '*.png' {
22 | const content: any;
23 | export default content;
24 | }
25 |
26 | declare module '*.css' {
27 | interface IClassNames {
28 | [className: string]: string;
29 | }
30 | const classNames: IClassNames;
31 | export = classNames;
32 | }
33 |
--------------------------------------------------------------------------------
/src/types/react-intl-auto.d.ts:
--------------------------------------------------------------------------------
1 | import type { MessageDescriptor } from 'react-intl';
2 |
3 | declare module 'react-intl' {
4 | interface ExtractableMessage {
5 | [key: string]: string;
6 | }
7 |
8 | export function defineMessages(
9 | messages: T
10 | ): { [K in keyof T]: MessageDescriptor };
11 | }
12 |
--------------------------------------------------------------------------------
/src/utils/creditHelpers.ts:
--------------------------------------------------------------------------------
1 | import type { Crew } from '@server/models/common';
2 | const priorityJobs = [
3 | 'Director',
4 | 'Creator',
5 | 'Screenplay',
6 | 'Writer',
7 | 'Composer',
8 | 'Editor',
9 | 'Producer',
10 | 'Co-Producer',
11 | 'Executive Producer',
12 | 'Animation',
13 | ];
14 |
15 | export const sortCrewPriority = (crew: Crew[]): Crew[] => {
16 | return crew
17 | .filter((person) => priorityJobs.includes(person.job))
18 | .sort((a, b) => {
19 | const aScore = priorityJobs.findIndex((job) => job.includes(a.job));
20 | const bScore = priorityJobs.findIndex((job) => job.includes(b.job));
21 |
22 | return aScore - bScore;
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/src/utils/numberHelpers.ts:
--------------------------------------------------------------------------------
1 | export const formatBytes = (bytes: number, decimals = 2): string => {
2 | if (bytes === 0) return '0 Bytes';
3 |
4 | const k = 1024;
5 | const dm = decimals < 0 ? 0 : decimals;
6 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
7 |
8 | const i = Math.floor(Math.log(bytes) / Math.log(k));
9 |
10 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
11 | };
12 |
--------------------------------------------------------------------------------
/src/utils/polyfillIntl.ts:
--------------------------------------------------------------------------------
1 | import { shouldPolyfill as shouldPolyfillDisplayNames } from '@formatjs/intl-displaynames/should-polyfill';
2 | import { shouldPolyfill as shouldPolyfillLocale } from '@formatjs/intl-locale/should-polyfill';
3 | import { shouldPolyfill as shouldPolyfillPluralrules } from '@formatjs/intl-pluralrules/should-polyfill';
4 |
5 | const polyfillLocale = async () => {
6 | if (shouldPolyfillLocale()) {
7 | await import('@formatjs/intl-locale/polyfill');
8 | }
9 | };
10 |
11 | const polyfillPluralRules = async (locale: string) => {
12 | const unsupportedLocale = shouldPolyfillPluralrules(locale);
13 | // This locale is supported
14 | if (!unsupportedLocale) {
15 | return;
16 | }
17 | // Load the polyfill 1st BEFORE loading data
18 | await import('@formatjs/intl-pluralrules/polyfill-force');
19 | await import(`@formatjs/intl-pluralrules/locale-data/${unsupportedLocale}`);
20 | };
21 |
22 | const polyfillDisplayNames = async (locale: string) => {
23 | const unsupportedLocale = shouldPolyfillDisplayNames(locale);
24 | // This locale is supported
25 | if (!unsupportedLocale) {
26 | return;
27 | }
28 | // Load the polyfill 1st BEFORE loading data
29 | await import('@formatjs/intl-displaynames/polyfill-force');
30 | await import(`@formatjs/intl-displaynames/locale-data/${unsupportedLocale}`);
31 | };
32 |
33 | export const polyfillIntl = async (locale: string) => {
34 | await polyfillLocale();
35 | await polyfillPluralRules(locale);
36 | await polyfillDisplayNames(locale);
37 | };
38 |
--------------------------------------------------------------------------------
/src/utils/refreshIntervalHelper.ts:
--------------------------------------------------------------------------------
1 | import type { DownloadingItem } from '@server/lib/downloadtracker';
2 |
3 | export const refreshIntervalHelper = (
4 | downloadItem: {
5 | downloadStatus: DownloadingItem[] | undefined;
6 | downloadStatus4k: DownloadingItem[] | undefined;
7 | },
8 | timer: number
9 | ) => {
10 | if (
11 | (downloadItem.downloadStatus ?? []).length > 0 ||
12 | (downloadItem.downloadStatus4k ?? []).length > 0
13 | ) {
14 | return timer;
15 | } else {
16 | return 0;
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/src/utils/typeHelpers.ts:
--------------------------------------------------------------------------------
1 | export type Undefinable = T | undefined;
2 | export type Nullable = T | null;
3 | export type Maybe = T | null | undefined;
4 |
5 | /**
6 | * Helps type objects with an arbitrary number of properties that are
7 | * usually being defined at export.
8 | *
9 | * @param component Main object you want to apply properties to
10 | * @param properties Object of properties you want to type on the main component
11 | */
12 | export function withProperties(
13 | component: A,
14 | properties: B
15 | ): A & B {
16 | (Object.keys(properties) as (keyof B)[]).forEach((key) => {
17 | Object.assign(component, { [key]: properties[key] });
18 | });
19 | return component as A & B;
20 | }
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "strictPropertyInitialization": false,
17 | "experimentalDecorators": true,
18 | "emitDecoratorMetadata": true,
19 | "useUnknownInCatchVariables": false,
20 | "incremental": true,
21 | "baseUrl": "src",
22 | "paths": {
23 | "@server/*": ["../server/*"],
24 | "@app/*": ["*"]
25 | }
26 | },
27 | "include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx"],
28 | "exclude": ["node_modules"]
29 | }
30 |
--------------------------------------------------------------------------------