Merge branch 'develop' into feature-change-jellyfin-ip

This commit is contained in:
Fallenbagel
2023-06-13 05:38:41 +05:00
committed by GitHub
68 changed files with 3340 additions and 2031 deletions

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
id="svg2"
viewBox="0 0 712.60077 712.5481"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<rect
style="opacity:0;fill:#ffffff;stroke-width:4.12543"
id="rect249"
width="712.60077"
height="712.5481"
x="-0.00071160076"
y="2.0223413e-11" />
<rect
style="fill:#ffffff"
id="rect289"
width="230.18982"
height="229.82355"
x="241.20476"
y="241.36227" />
<g
id="layer1"
transform="matrix(0.70249853,0,0,0.70249853,88.770447,96.84571)">
<path
id="path3427"
d="m 327.06546,642.18589 c -45.39663,-45.86009 -82.73776,-83.3683 -82.98029,-83.3516 -0.24253,0.0167 -7.23324,6.65975 -15.53493,14.7623 l -15.09396,14.73193 -40.13624,-40.38805 C 151.24511,525.72706 108.73555,482.86504 78.854363,452.69158 l -54.329437,-54.86086 83.720394,-82.90796 83.72039,-82.90797 -15.19316,-15.20441 -15.19315,-15.20443 95.18008,-94.29313 c 52.34904,-51.86121 95.35849,-94.293118 95.57653,-94.293118 0.21805,0 37.39519,37.357576 82.61589,83.016832 45.22068,45.659256 82.53772,83.131956 82.92673,83.272666 0.38901,0.14071 7.46336,-6.49498 15.72077,-14.746 l 15.01348,-15.00184 7.14591,7.19902 c 73.95232,74.50189 181.50599,183.56427 181.36678,183.9109 -0.10065,0.25064 -37.54056,37.44106 -83.19981,82.64536 -45.65926,45.2043 -83.10802,82.41429 -83.21946,82.68884 -0.11145,0.27456 6.50478,7.34753 14.70272,15.71771 l 14.90534,15.21851 -15.3888,15.28883 c -21.09609,20.95904 -162.95155,161.27018 -169.79551,167.947 l -5.52526,5.39033 z m 89.8523,-204.1566 c 64.84836,-37.53366 117.81919,-68.54793 117.71294,-68.92058 -0.15927,-0.55862 -233.55022,-136.2489 -236.27084,-137.3646 -0.68441,-0.28068 -0.85761,27.45642 -0.85761,137.33982 0,99.83563 0.20749,137.62237 0.75471,137.43996 0.41509,-0.13837 53.81245,-30.96093 118.6608,-68.4946 z"
style="fill:#52b54b;fill-opacity:1;stroke:none" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -338,6 +338,7 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
<TitleCard
key={`collection-movie-${title.id}`}
id={title.id}
isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
@@ -348,7 +349,7 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
/>
))}
/>
<div className="pb-8" />
<div className="extra-bottom-space relative" />
</div>
);
};

View File

@@ -71,7 +71,7 @@ const Badge = (
'bg-indigo-500 bg-opacity-80 border border-indigo-500 !text-indigo-100'
);
if (href) {
badgeStyle.push('hover:bg-indigo-500 bg-opacity-100');
badgeStyle.push('hover:bg-indigo-500 hover:bg-opacity-100');
}
}

View File

@@ -5,6 +5,7 @@ import useVerticalScroll from '@app/hooks/useVerticalScroll';
import globalMessages from '@app/i18n/globalMessages';
import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces';
import type {
CollectionResult,
MovieResult,
PersonResult,
TvResult,
@@ -12,7 +13,7 @@ import type {
import { useIntl } from 'react-intl';
type ListViewProps = {
items?: (TvResult | MovieResult | PersonResult)[];
items?: (TvResult | MovieResult | PersonResult | CollectionResult)[];
plexItems?: WatchlistItem[];
isEmpty?: boolean;
isLoading?: boolean;
@@ -57,7 +58,9 @@ const ListView = ({
case 'movie':
titleCard = (
<TitleCard
key={title.id}
id={title.id}
isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
@@ -75,7 +78,9 @@ const ListView = ({
case 'tv':
titleCard = (
<TitleCard
key={title.id}
id={title.id}
isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
@@ -90,6 +95,18 @@ const ListView = ({
/>
);
break;
case 'collection':
titleCard = (
<TitleCard
id={title.id}
image={title.posterPath}
summary={title.overview}
title={title.title}
mediaType={title.mediaType}
canExpand
/>
);
break;
case 'person':
titleCard = (
<PersonCard

View File

@@ -2,6 +2,7 @@ import Button from '@app/components/Common/Button';
import Tooltip from '@app/components/Common/Tooltip';
import { sliderTitles } from '@app/components/Discover/constants';
import MediaSlider from '@app/components/MediaSlider';
import { WatchProviderSelector } from '@app/components/Selector';
import { encodeURIExtraParams } from '@app/hooks/useDiscover';
import type {
TmdbCompanySearchResponse,
@@ -55,7 +56,7 @@ type CreateOption = {
dataUrl: string;
params?: string;
titlePlaceholderText: string;
dataPlaceholderText: string;
dataPlaceholderText?: string;
};
const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
@@ -276,6 +277,20 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
titlePlaceholderText: intl.formatMessage(messages.slidernameplaceholder),
dataPlaceholderText: intl.formatMessage(messages.providetmdbsearch),
},
{
type: DiscoverSliderType.TMDB_MOVIE_STREAMING_SERVICES,
title: intl.formatMessage(sliderTitles.tmdbmoviestreamingservices),
dataUrl: '/api/v1/discover/movies',
params: 'watchRegion=$regionValue&watchProviders=$providersValue',
titlePlaceholderText: intl.formatMessage(messages.slidernameplaceholder),
},
{
type: DiscoverSliderType.TMDB_TV_STREAMING_SERVICES,
title: intl.formatMessage(sliderTitles.tmdbtvstreamingservices),
dataUrl: '/api/v1/discover/tv',
params: 'watchRegion=$regionValue&watchProviders=$providersValue',
titlePlaceholderText: intl.formatMessage(messages.slidernameplaceholder),
},
];
return (
@@ -417,6 +432,40 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
/>
);
break;
case DiscoverSliderType.TMDB_MOVIE_STREAMING_SERVICES:
dataInput = (
<WatchProviderSelector
type={'movie'}
region={slider?.data?.split(',')[0]}
activeProviders={
slider?.data
?.split(',')[1]
.split('|')
.map((v) => Number(v)) ?? []
}
onChange={(region, providers) => {
setFieldValue('data', `${region},${providers.join('|')}`);
}}
/>
);
break;
case DiscoverSliderType.TMDB_TV_STREAMING_SERVICES:
dataInput = (
<WatchProviderSelector
type={'tv'}
region={slider?.data?.split(',')[0]}
activeProviders={
slider?.data
?.split(',')[1]
.split('|')
.map((v) => Number(v)) ?? []
}
onChange={(region, providers) => {
setFieldValue('data', `${region},${providers.join('|')}`);
}}
/>
);
break;
default:
dataInput = (
<Field
@@ -488,10 +537,25 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
'$value',
encodeURIExtraParams(values.data)
)}
extraParams={activeOption.params?.replace(
'$value',
encodeURIExtraParams(values.data)
)}
extraParams={
activeOption.type ===
DiscoverSliderType.TMDB_MOVIE_STREAMING_SERVICES ||
activeOption.type ===
DiscoverSliderType.TMDB_TV_STREAMING_SERVICES
? activeOption.params
?.replace(
'$regionValue',
encodeURIExtraParams(values?.data.split(',')[0])
)
.replace(
'$providersValue',
encodeURIExtraParams(values?.data.split(',')[1])
)
: activeOption.params?.replace(
'$value',
encodeURIExtraParams(values.data)
)
}
onNewTitles={updateResultCount}
/>
</div>

View File

@@ -164,6 +164,10 @@ const DiscoverSliderEdit = ({
return intl.formatMessage(sliderTitles.tmdbnetwork);
case DiscoverSliderType.TMDB_SEARCH:
return intl.formatMessage(sliderTitles.tmdbsearch);
case DiscoverSliderType.TMDB_MOVIE_STREAMING_SERVICES:
return intl.formatMessage(sliderTitles.tmdbmoviestreamingservices);
case DiscoverSliderType.TMDB_TV_STREAMING_SERVICES:
return intl.formatMessage(sliderTitles.tmdbtvstreamingservices);
default:
return 'Unknown Slider';
}
@@ -195,7 +199,9 @@ const DiscoverSliderEdit = ({
className={`${slider.data ? 'mb-4' : 'mb-0'} flex space-x-2 md:mb-0`}
>
<Bars3Icon className="h-6 w-6" />
<div>{getSliderTitle(slider)}</div>
<div className="w-7/12 truncate md:w-full">
{getSliderTitle(slider)}
</div>
</div>
<div
className={`pointer-events-none ${

View File

@@ -10,7 +10,7 @@ import { useRouter } from 'next/router';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
discoverwatchlist: 'Your Plex Watchlist',
discoverwatchlist: 'Your Watchlist',
watchlist: 'Plex Watchlist',
});

View File

@@ -35,8 +35,10 @@ const messages = defineMessages({
ratingText: 'Ratings between {minValue} and {maxValue}',
clearfilters: 'Clear Active Filters',
tmdbuserscore: 'TMDB User Score',
tmdbuservotecount: 'TMDB User Vote Count',
runtime: 'Runtime',
streamingservices: 'Streaming Services',
voteCount: 'Number of votes between {minValue} and {maxValue}',
});
type FilterSlideoverProps = {
@@ -246,6 +248,45 @@ const FilterSlideover = ({
})}
/>
</div>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.tmdbuservotecount)}
</span>
<div className="relative z-0">
<MultiRangeSlider
min={0}
max={1000}
defaultMaxValue={
currentFilters.voteCountLte
? Number(currentFilters.voteCountLte)
: undefined
}
defaultMinValue={
currentFilters.voteCountGte
? Number(currentFilters.voteCountGte)
: undefined
}
onUpdateMin={(min) => {
updateQueryParams(
'voteCountGte',
min !== 0 && Number(currentFilters.voteCountLte) !== 1000
? min.toString()
: undefined
);
}}
onUpdateMax={(max) => {
updateQueryParams(
'voteCountLte',
max !== 1000 && Number(currentFilters.voteCountGte) !== 0
? max.toString()
: undefined
);
}}
subText={intl.formatMessage(messages.voteCount, {
minValue: currentFilters.voteCountGte ?? 0,
maxValue: currentFilters.voteCountLte ?? 1000,
})}
/>
</div>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.streamingservices)}
</span>

View File

@@ -1,6 +1,6 @@
import Slider from '@app/components/Slider';
import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard';
import { UserType, useUser } from '@app/hooks/useUser';
import { useUser } from '@app/hooks/useUser';
import { ArrowRightCircleIcon } from '@heroicons/react/24/outline';
import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces';
import Link from 'next/link';
@@ -8,7 +8,7 @@ import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
const messages = defineMessages({
plexwatchlist: 'Your Plex Watchlist',
plexwatchlist: 'Your Watchlist',
emptywatchlist:
'Media added to your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> will appear here.',
});
@@ -22,12 +22,11 @@ const PlexWatchlistSlider = () => {
totalPages: number;
totalResults: number;
results: WatchlistItem[];
}>(user?.userType === UserType.PLEX ? '/api/v1/discover/watchlist' : null, {
}>('/api/v1/discover/watchlist', {
revalidateOnMount: true,
});
if (
user?.userType !== UserType.PLEX ||
(watchlistItems &&
watchlistItems.results.length === 0 &&
!user?.settings?.watchlistSyncMovies &&
@@ -69,6 +68,7 @@ const PlexWatchlistSlider = () => {
key={`watchlist-slider-item-${item.ratingKey}`}
tmdbId={item.tmdbId}
type={item.mediaType}
isAddedToWatchlist={true}
/>
))}
/>

View File

@@ -74,7 +74,7 @@ export const sliderTitles = defineMessages({
recentlyAdded: 'Recently Added',
upcoming: 'Upcoming Movies',
trending: 'Trending',
plexwatchlist: 'Your Plex Watchlist',
plexwatchlist: 'Your Watchlist',
moviegenres: 'Movie Genres',
tvgenres: 'Series Genres',
studios: 'Studios',
@@ -86,6 +86,8 @@ export const sliderTitles = defineMessages({
tmdbnetwork: 'TMDB Network',
tmdbstudio: 'TMDB Studio',
tmdbsearch: 'TMDB Search',
tmdbmoviestreamingservices: 'TMDB Movie Streaming Services',
tmdbtvstreamingservices: 'TMDB TV Streaming Services',
});
export const QueryFilterOptions = z.object({
@@ -102,6 +104,8 @@ export const QueryFilterOptions = z.object({
withRuntimeLte: z.string().optional(),
voteAverageGte: z.string().optional(),
voteAverageLte: z.string().optional(),
voteCountLte: z.string().optional(),
voteCountGte: z.string().optional(),
watchRegion: z.string().optional(),
watchProviders: z.string().optional(),
});
@@ -167,6 +171,14 @@ export const prepareFilterValues = (
filterValues.voteAverageLte = values.voteAverageLte;
}
if (values.voteCountGte) {
filterValues.voteCountGte = values.voteCountGte;
}
if (values.voteCountLte) {
filterValues.voteCountLte = values.voteCountLte;
}
if (values.watchProviders) {
filterValues.watchProviders = values.watchProviders;
}
@@ -188,6 +200,12 @@ export const countActiveFilters = (filterValues: FilterOptions): number => {
delete clonedFilters.voteAverageLte;
}
if (clonedFilters.voteCountGte || filterValues.voteCountLte) {
totalCount += 1;
delete clonedFilters.voteCountGte;
delete clonedFilters.voteCountLte;
}
if (clonedFilters.withRuntimeGte || filterValues.withRuntimeLte) {
totalCount += 1;
delete clonedFilters.withRuntimeGte;

View File

@@ -365,6 +365,36 @@ const Discover = () => {
/>
);
break;
case DiscoverSliderType.TMDB_MOVIE_STREAMING_SERVICES:
sliderComponent = (
<MediaSlider
sliderKey={`custom-slider-${slider.id}`}
title={slider.title ?? ''}
url="/api/v1/discover/movies"
extraParams={`watchRegion=${
slider.data?.split(',')[0]
}&watchProviders=${slider.data?.split(',')[1]}`}
linkUrl={`/discover/movies?watchRegion=${
slider.data?.split(',')[0]
}&watchProviders=${slider.data?.split(',')[1]}`}
/>
);
break;
case DiscoverSliderType.TMDB_TV_STREAMING_SERVICES:
sliderComponent = (
<MediaSlider
sliderKey={`custom-slider-${slider.id}`}
title={slider.title ?? ''}
url="/api/v1/discover/tv"
extraParams={`watchRegion=${
slider.data?.split(',')[0]
}&watchProviders=${slider.data?.split(',')[1]}`}
linkUrl={`/discover/tv?watchRegion=${
slider.data?.split(',')[0]
}&watchProviders=${slider.data?.split(',')[1]}`}
/>
);
break;
}
if (isEditing) {

View File

@@ -1,3 +1,4 @@
import EmbyLogo from '@app/assets/services/emby.svg';
import ImdbLogo from '@app/assets/services/imdb.svg';
import JellyfinLogo from '@app/assets/services/jellyfin.svg';
import PlexLogo from '@app/assets/services/plex.svg';
@@ -9,6 +10,7 @@ import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';
import { MediaType } from '@server/constants/media';
import { MediaServerType } from '@server/constants/server';
import getConfig from 'next/config';
interface ExternalLinkBlockProps {
mediaType: 'movie' | 'tv';
@@ -28,6 +30,7 @@ const ExternalLinkBlock = ({
mediaUrl,
}: ExternalLinkBlockProps) => {
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
const { locale } = useLocale();
return (
@@ -41,6 +44,8 @@ const ExternalLinkBlock = ({
>
{settings.currentSettings.mediaServerType === MediaServerType.PLEX ? (
<PlexLogo />
) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? (
<EmbyLogo />
) : (
<JellyfinLogo />
)}

View File

@@ -0,0 +1,118 @@
import { ArrowPathIcon } from '@heroicons/react/24/outline';
import { useRouter } from 'next/router';
import { useEffect, useRef, useState } from 'react';
const PullToRefresh = () => {
const router = useRouter();
const [pullStartPoint, setPullStartPoint] = useState(0);
const [pullChange, setPullChange] = useState(0);
const refreshDiv = useRef<HTMLDivElement>(null);
// Various pull down thresholds that determine icon location
const pullDownInitThreshold = pullChange > 20;
const pullDownStopThreshold = 120;
const pullDownReloadThreshold = pullChange > 340;
const pullDownIconLocation = pullChange / 3;
useEffect(() => {
// Reload function that is called when reload threshold has been hit
// Add loading class to determine when to add spin animation
const forceReload = () => {
refreshDiv.current?.classList.add('loading');
setTimeout(() => {
router.reload();
}, 1000);
};
const html = document.querySelector('html');
// Determines if we are at the top of the page
// Locks or unlocks page when pulling down to refresh
const pullStart = (e: TouchEvent) => {
setPullStartPoint(e.targetTouches[0].screenY);
if (window.scrollY === 0 && window.scrollX === 0) {
refreshDiv.current?.classList.add('block');
refreshDiv.current?.classList.remove('hidden');
document.body.style.touchAction = 'none';
document.body.style.overscrollBehavior = 'none';
if (html) {
html.style.overscrollBehaviorY = 'none';
}
} else {
refreshDiv.current?.classList.remove('block');
refreshDiv.current?.classList.add('hidden');
}
};
// Tracks how far we have pulled down the refresh icon
const pullDown = async (e: TouchEvent) => {
const screenY = e.targetTouches[0].screenY;
const pullLength =
pullStartPoint < screenY ? Math.abs(screenY - pullStartPoint) : 0;
setPullChange(pullLength);
};
// Will reload the page if we are past the threshold
// Otherwise, we reset the pull
const pullFinish = () => {
setPullStartPoint(0);
if (pullDownReloadThreshold) {
forceReload();
} else {
setPullChange(0);
}
document.body.style.touchAction = 'auto';
document.body.style.overscrollBehaviorY = 'auto';
if (html) {
html.style.overscrollBehaviorY = 'auto';
}
};
window.addEventListener('touchstart', pullStart, { passive: false });
window.addEventListener('touchmove', pullDown, { passive: false });
window.addEventListener('touchend', pullFinish, { passive: false });
return () => {
window.removeEventListener('touchstart', pullStart);
window.removeEventListener('touchmove', pullDown);
window.removeEventListener('touchend', pullFinish);
};
}, [pullDownInitThreshold, pullDownReloadThreshold, pullStartPoint, router]);
return (
<div
ref={refreshDiv}
className="absolute left-0 right-0 top-0 z-50 m-auto w-fit transition-all ease-out"
id="refreshIcon"
style={{
top:
pullDownIconLocation < pullDownStopThreshold && pullDownInitThreshold
? pullDownIconLocation
: pullDownInitThreshold
? pullDownStopThreshold
: '',
}}
>
<div
className={`${
refreshDiv.current?.classList.contains('loading') && 'animate-spin'
} relative -top-24 h-9 w-9 rounded-full border-4 border-gray-800 bg-gray-800 shadow-md shadow-black ring-1 ring-gray-700`}
style={{ animationDirection: 'reverse' }}
>
<ArrowPathIcon
className={`rounded-full ${
pullDownReloadThreshold && 'rotate-180'
} text-indigo-500 transition-all duration-300`}
/>
</div>
</div>
);
};
export default PullToRefresh;

View File

@@ -1,8 +1,8 @@
import MobileMenu from '@app/components/Layout/MobileMenu';
import PullToRefresh from '@app/components/Layout/PullToRefresh';
import SearchInput from '@app/components/Layout/SearchInput';
import Sidebar from '@app/components/Layout/Sidebar';
import UserDropdown from '@app/components/Layout/UserDropdown';
import PullToRefresh from '@app/components/PullToRefresh';
import type { AvailableLocale } from '@app/context/LanguageContext';
import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';

View File

@@ -95,7 +95,9 @@ const MediaSlider = ({
case 'movie':
return (
<TitleCard
key={title.id}
id={title.id}
isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
@@ -109,7 +111,9 @@ const MediaSlider = ({
case 'tv':
return (
<TitleCard
key={title.id}
id={title.id}
isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}

View File

@@ -62,7 +62,7 @@ const messages = defineMessages({
'Get notified when issues are reopened by other users.',
mediaautorequested: 'Request Automatically Submitted',
mediaautorequestedDescription:
'Get notified when new media requests are automatically submitted for items on your Plex Watchlist.',
'Get notified when new media requests are automatically submitted for items on Your Watchlist.',
});
export const hasNotificationType = (

View File

@@ -133,6 +133,7 @@ const PersonDetails = () => {
return (
<li key={`list-cast-item-${media.id}-${index}`}>
<TitleCard
key={media.id}
id={media.id}
title={media.mediaType === 'movie' ? media.title : media.name}
userScore={media.voteAverage}
@@ -173,6 +174,7 @@ const PersonDetails = () => {
return (
<li key={`list-crew-item-${media.id}-${index}`}>
<TitleCard
key={media.id}
id={media.id}
title={media.mediaType === 'movie' ? media.title : media.name}
userScore={media.voteAverage}

View File

@@ -1,45 +0,0 @@
import { ArrowPathIcon } from '@heroicons/react/24/outline';
import { useRouter } from 'next/router';
import PR from 'pulltorefreshjs';
import { useEffect } from 'react';
import ReactDOMServer from 'react-dom/server';
const PullToRefresh = () => {
const router = useRouter();
useEffect(() => {
PR.init({
mainElement: '#pull-to-refresh',
onRefresh() {
router.reload();
},
iconArrow: ReactDOMServer.renderToString(
<div className="p-2">
<ArrowPathIcon className="z-50 m-auto h-9 w-9 rounded-full border-4 border-gray-800 bg-gray-800 text-indigo-500 ring-1 ring-gray-700" />
</div>
),
iconRefreshing: ReactDOMServer.renderToString(
<div
className="animate-spin p-2"
style={{ animationDirection: 'reverse' }}
>
<ArrowPathIcon className="z-50 m-auto h-9 w-9 rounded-full border-4 border-gray-800 bg-gray-800 text-indigo-500 ring-1 ring-gray-700" />
</div>
),
instructionsPullToRefresh: ReactDOMServer.renderToString(<div />),
instructionsReleaseToRefresh: ReactDOMServer.renderToString(<div />),
instructionsRefreshing: ReactDOMServer.renderToString(<div />),
distReload: 60,
distIgnore: 15,
shouldPullToRefresh: () =>
!window.scrollY && document.body.style.overflow !== 'hidden',
});
return () => {
PR.destroyAll();
};
}, [router]);
return <div id="pull-to-refresh"></div>;
};
export default PullToRefresh;

View File

@@ -169,15 +169,19 @@ export const GenreSelector = ({
loadDefaultGenre();
}, [defaultValue, type]);
const loadGenreOptions = async () => {
const loadGenreOptions = async (inputValue: string) => {
const results = await axios.get<GenreSliderItem[]>(
`/api/v1/discover/genreslider/${type}`
);
return results.data.map((result) => ({
label: result.name,
value: result.id,
}));
return results.data
.map((result) => ({
label: result.name,
value: result.id,
}))
.filter(({ label }) =>
label.toLowerCase().includes(inputValue.toLowerCase())
);
};
return (
@@ -305,7 +309,9 @@ export const WatchProviderSelector = ({
useEffect(() => {
onChange(watchRegion, activeProvider);
}, [activeProvider, watchRegion, onChange]);
// removed onChange as a dependency as we only need to call it when the value(s) change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeProvider, watchRegion]);
const orderedData = useMemo(() => {
if (!data) {
@@ -344,7 +350,7 @@ export const WatchProviderSelector = ({
<SmallLoadingSpinner />
) : (
<div className="grid">
<div className="grid grid-cols-6 gap-2">
<div className="provider-icons grid gap-2">
{initialProviders.map((provider) => {
const isActive = activeProvider.includes(provider.id);
return (
@@ -353,7 +359,7 @@ export const WatchProviderSelector = ({
key={`prodiver-${provider.id}`}
>
<div
className={`provider-container relative h-full w-full cursor-pointer rounded-lg p-2 ring-1 ${
className={`provider-container relative w-full cursor-pointer rounded-lg p-2 ring-1 ${
isActive
? 'bg-gray-600 ring-indigo-500 hover:bg-gray-500'
: 'bg-gray-700 ring-gray-500 hover:bg-gray-600'
@@ -386,7 +392,7 @@ export const WatchProviderSelector = ({
})}
</div>
{showMore && otherProviders.length > 0 && (
<div className="relative top-2 grid grid-cols-6 gap-2">
<div className="provider-icons relative top-2 grid gap-2">
{otherProviders.map((provider) => {
const isActive = activeProvider.includes(provider.id);
return (
@@ -395,7 +401,7 @@ export const WatchProviderSelector = ({
key={`prodiver-${provider.id}`}
>
<div
className={`provider-container relative h-full w-full cursor-pointer rounded-lg p-2 ring-1 transition ${
className={`provider-container relative w-full cursor-pointer rounded-lg p-2 ring-1 transition ${
isActive
? 'bg-gray-600 ring-indigo-500 hover:bg-gray-500'
: 'bg-gray-700 ring-gray-500 hover:bg-gray-600'

View File

@@ -57,6 +57,9 @@ const messages = defineMessages({
testFirstTags: 'Test connection to load tags',
tags: 'Tags',
enableSearch: 'Enable Automatic Search',
tagRequests: 'Tag Requests',
tagRequestsInfo:
"Automatically add an additional tag with the requester's user ID & display name",
validationApplicationUrl: 'You must provide a valid URL',
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
validationBaseUrlLeadingSlash: 'URL base must have a leading slash',
@@ -238,6 +241,7 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
externalUrl: radarr?.externalUrl,
syncEnabled: radarr?.syncEnabled ?? false,
enableSearch: !radarr?.preventSearch,
tagRequests: radarr?.tagRequests ?? false,
}}
validationSchema={RadarrSettingsSchema}
onSubmit={async (values) => {
@@ -263,6 +267,7 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
externalUrl: values.externalUrl,
syncEnabled: values.syncEnabled,
preventSearch: !values.enableSearch,
tagRequests: values.tagRequests,
};
if (!radarr) {
await axios.post('/api/v1/settings/radarr', submission);
@@ -713,6 +718,21 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
/>
</div>
</div>
<div className="form-row">
<label htmlFor="tagRequests" className="checkbox-label">
{intl.formatMessage(messages.tagRequests)}
<span className="label-tip">
{intl.formatMessage(messages.tagRequestsInfo)}
</span>
</label>
<div className="form-input-area">
<Field
type="checkbox"
id="tagRequests"
name="tagRequests"
/>
</div>
</div>
</div>
</Modal>
);

View File

@@ -62,6 +62,9 @@ const messages = defineMessages({
syncEnabled: 'Enable Scan',
externalUrl: 'External URL',
enableSearch: 'Enable Automatic Search',
tagRequests: 'Tag Requests',
tagRequestsInfo:
"Automatically add an additional tag with the requester's user ID & display name",
validationApplicationUrl: 'You must provide a valid URL',
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
validationBaseUrlLeadingSlash: 'Base URL must have a leading slash',
@@ -252,6 +255,7 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
externalUrl: sonarr?.externalUrl,
syncEnabled: sonarr?.syncEnabled ?? false,
enableSearch: !sonarr?.preventSearch,
tagRequests: sonarr?.tagRequests ?? false,
}}
validationSchema={SonarrSettingsSchema}
onSubmit={async (values) => {
@@ -292,6 +296,7 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
externalUrl: values.externalUrl,
syncEnabled: values.syncEnabled,
preventSearch: !values.enableSearch,
tagRequests: values.tagRequests,
};
if (!sonarr) {
await axios.post('/api/v1/settings/sonarr', submission);
@@ -960,6 +965,21 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
/>
</div>
</div>
<div className="form-row">
<label htmlFor="tagRequests" className="checkbox-label">
{intl.formatMessage(messages.tagRequests)}
<span className="label-tip">
{intl.formatMessage(messages.tagRequestsInfo)}
</span>
</label>
<div className="form-input-area">
<Field
type="checkbox"
id="tagRequests"
name="tagRequests"
/>
</div>
</div>
</div>
</Modal>
);

View File

@@ -11,6 +11,7 @@ export interface TmdbTitleCardProps {
tvdbId?: number;
type: 'movie' | 'tv';
canExpand?: boolean;
isAddedToWatchlist?: boolean;
}
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
@@ -23,6 +24,7 @@ const TmdbTitleCard = ({
tvdbId,
type,
canExpand,
isAddedToWatchlist = false,
}: TmdbTitleCardProps) => {
const { hasPermission } = useUser();
@@ -56,7 +58,11 @@ const TmdbTitleCard = ({
return isMovie(title) ? (
<TitleCard
key={title.id}
id={title.id}
isAddedToWatchlist={
title.mediaInfo?.watchlists?.length || isAddedToWatchlist
}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
@@ -68,7 +74,11 @@ const TmdbTitleCard = ({
/>
) : (
<TitleCard
key={title.id}
id={title.id}
isAddedToWatchlist={
title.mediaInfo?.watchlists?.length || isAddedToWatchlist
}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}

View File

@@ -10,12 +10,21 @@ import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import { withProperties } from '@app/utils/typeHelpers';
import { Transition } from '@headlessui/react';
import { ArrowDownTrayIcon } from '@heroicons/react/24/outline';
import {
ArrowDownTrayIcon,
MinusCircleIcon,
StarIcon,
} from '@heroicons/react/24/outline';
import { MediaStatus } from '@server/constants/media';
import type { Watchlist } from '@server/entity/Watchlist';
import type { MediaType } from '@server/models/Search';
import axios from 'axios';
import Link from 'next/link';
import type React from 'react';
import { Fragment, useCallback, useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import { mutate } from 'swr';
interface TitleCardProps {
id: number;
@@ -23,13 +32,24 @@ interface TitleCardProps {
summary?: string;
year?: string;
title: string;
userScore: number;
userScore?: number;
mediaType: MediaType;
status?: MediaStatus;
canExpand?: boolean;
inProgress?: boolean;
isAddedToWatchlist?: number | boolean;
}
const messages = defineMessages({
addToWatchList: 'Add to watchlist',
watchlistSuccess:
'<strong>{title}</strong> added to watchlist successfully!',
watchlistDeleted:
'<strong>{title}</strong> Removed from watchlist successfully!',
watchlistCancel: 'watchlist for <strong>{title}</strong> canceled.',
watchlistError: 'Something went wrong try again.',
});
const TitleCard = ({
id,
image,
@@ -38,6 +58,7 @@ const TitleCard = ({
title,
status,
mediaType,
isAddedToWatchlist = false,
inProgress = false,
canExpand = false,
}: TitleCardProps) => {
@@ -48,6 +69,10 @@ const TitleCard = ({
const [currentStatus, setCurrentStatus] = useState(status);
const [showDetail, setShowDetail] = useState(false);
const [showRequestModal, setShowRequestModal] = useState(false);
const { addToast } = useToasts();
const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
!isAddedToWatchlist
);
// Just to get the year from the date
if (year) {
@@ -68,12 +93,73 @@ const TitleCard = ({
[]
);
const onClickWatchlistBtn = async (): Promise<void> => {
setIsUpdating(true);
try {
const response = await axios.post<Watchlist>('/api/v1/watchlist', {
tmdbId: id,
mediaType,
title,
});
mutate('/api/v1/discover/watchlist');
if (response.data) {
addToast(
<span>
{intl.formatMessage(messages.watchlistSuccess, {
title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
}
} catch (e) {
addToast(intl.formatMessage(messages.watchlistError), {
appearance: 'error',
autoDismiss: true,
});
} finally {
setIsUpdating(false);
setToggleWatchlist((prevState) => !prevState);
}
};
const onClickDeleteWatchlistBtn = async (): Promise<void> => {
setIsUpdating(true);
try {
const response = await axios.delete<Watchlist>('/api/v1/watchlist/' + id);
if (response.status === 204) {
addToast(
<span>
{intl.formatMessage(messages.watchlistDeleted, {
title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'info', autoDismiss: true }
);
}
} catch (e) {
addToast(intl.formatMessage(messages.watchlistError), {
appearance: 'error',
autoDismiss: true,
});
} finally {
setIsUpdating(false);
mutate('/api/v1/discover/watchlist');
setToggleWatchlist((prevState) => !prevState);
}
};
const closeModal = useCallback(() => setShowRequestModal(false), []);
const showRequestButton = hasPermission(
[
Permission.REQUEST,
mediaType === 'movie' ? Permission.REQUEST_MOVIE : Permission.REQUEST_TV,
mediaType === 'movie' || mediaType === 'collection'
? Permission.REQUEST_MOVIE
: Permission.REQUEST_TV,
],
{ type: 'or' }
);
@@ -86,7 +172,13 @@ const TitleCard = ({
<RequestModal
tmdbId={id}
show={showRequestModal}
type={mediaType === 'movie' ? 'movie' : 'tv'}
type={
mediaType === 'movie'
? 'movie'
: mediaType === 'collection'
? 'collection'
: 'tv'
}
onComplete={requestComplete}
onUpdating={requestUpdating}
onCancel={closeModal}
@@ -130,7 +222,7 @@ const TitleCard = ({
<div className="absolute left-0 right-0 flex items-center justify-between p-2">
<div
className={`pointer-events-none z-40 rounded-full border bg-opacity-80 shadow-md ${
mediaType === 'movie'
mediaType === 'movie' || mediaType === 'collection'
? 'border-blue-500 bg-blue-600'
: 'border-purple-600 bg-purple-600'
}`}
@@ -138,9 +230,33 @@ const TitleCard = ({
<div className="flex h-4 items-center px-2 py-2 text-center text-xs font-medium uppercase tracking-wider text-white sm:h-5">
{mediaType === 'movie'
? intl.formatMessage(globalMessages.movie)
: mediaType === 'collection'
? intl.formatMessage(globalMessages.collection)
: intl.formatMessage(globalMessages.tvshow)}
</div>
</div>
{showDetail && (
<>
{toggleWatchlist ? (
<Button
buttonType={'ghost'}
className="z-40"
buttonSize={'sm'}
onClick={onClickWatchlistBtn}
>
<StarIcon className={'h-3 text-amber-300'} />
</Button>
) : (
<Button
className="z-40"
buttonSize={'sm'}
onClick={onClickDeleteWatchlistBtn}
>
<MinusCircleIcon className={'h-3'} />
</Button>
)}
</>
)}
{currentStatus && currentStatus !== MediaStatus.UNKNOWN && (
<div className="pointer-events-none z-40 flex items-center">
<StatusBadgeMini
@@ -177,7 +293,15 @@ const TitleCard = ({
leaveTo="opacity-0"
>
<div className="absolute inset-0 overflow-hidden rounded-xl">
<Link href={mediaType === 'movie' ? `/movie/${id}` : `/tv/${id}`}>
<Link
href={
mediaType === 'movie'
? `/movie/${id}`
: mediaType === 'collection'
? `/collection/${id}`
: `/tv/${id}`
}
>
<a
className="absolute inset-0 h-full w-full cursor-pointer overflow-hidden text-left"
style={{

View File

@@ -15,13 +15,20 @@ export const useLockBodyScroll = (
disabled?: boolean
): void => {
useEffect(() => {
const originalStyle = window.getComputedStyle(document.body).overflow;
const originalOverflowStyle = window.getComputedStyle(
document.body
).overflow;
const originalTouchActionStyle = window.getComputedStyle(
document.body
).touchAction;
if (isLocked && !disabled) {
document.body.style.overflow = 'hidden';
document.body.style.touchAction = 'none';
}
return () => {
if (!disabled) {
document.body.style.overflow = originalStyle;
document.body.style.overflow = originalOverflowStyle;
document.body.style.touchAction = originalTouchActionStyle;
}
};
}, [isLocked, disabled]);

View File

@@ -16,6 +16,7 @@ const globalMessages = defineMessages({
approved: 'Approved',
movie: 'Movie',
movies: 'Movies',
collection: 'Collection',
tvshow: 'Series',
tvshows: 'Series',
cancel: 'Cancel',

View File

@@ -59,7 +59,7 @@
"components.Discover.DiscoverTvGenre.genreSeries": "{genre} Series",
"components.Discover.DiscoverTvKeyword.keywordSeries": "{keywordTitle} Series",
"components.Discover.DiscoverTvLanguage.languageSeries": "{language} Series",
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Your Plex Watchlist",
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Your Watchlist",
"components.Discover.DiscoverWatchlist.watchlist": "Plex Watchlist",
"components.Discover.FilterSlideover.activefilters": "{count, plural, one {# Active Filter} other {# Active Filters}}",
"components.Discover.FilterSlideover.clearfilters": "Clear Active Filters",
@@ -76,7 +76,9 @@
"components.Discover.FilterSlideover.streamingservices": "Streaming Services",
"components.Discover.FilterSlideover.studio": "Studio",
"components.Discover.FilterSlideover.tmdbuserscore": "TMDB User Score",
"components.Discover.FilterSlideover.tmdbuservotecount": "TMDB User Vote Count",
"components.Discover.FilterSlideover.to": "To",
"components.Discover.FilterSlideover.voteCount": "Number of votes between {minValue} and {maxValue}",
"components.Discover.MovieGenreList.moviegenres": "Movie Genres",
"components.Discover.MovieGenreSlider.moviegenres": "Movie Genres",
"components.Discover.NetworkSlider.networks": "Networks",
@@ -92,7 +94,8 @@
"components.Discover.emptywatchlist": "Media added to your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> will appear here.",
"components.Discover.moviegenres": "Movie Genres",
"components.Discover.networks": "Networks",
"components.Discover.plexwatchlist": "Your Plex Watchlist",
"components.Discover.noRequests": "No requests.",
"components.Discover.plexwatchlist": "Your Watchlist",
"components.Discover.popularmovies": "Popular Movies",
"components.Discover.populartv": "Popular Series",
"components.Discover.recentlyAdded": "Recently Added",
@@ -105,11 +108,13 @@
"components.Discover.studios": "Studios",
"components.Discover.tmdbmoviegenre": "TMDB Movie Genre",
"components.Discover.tmdbmoviekeyword": "TMDB Movie Keyword",
"components.Discover.tmdbmoviestreamingservices": "TMDB Movie Streaming Services",
"components.Discover.tmdbnetwork": "TMDB Network",
"components.Discover.tmdbsearch": "TMDB Search",
"components.Discover.tmdbstudio": "TMDB Studio",
"components.Discover.tmdbtvgenre": "TMDB Series Genre",
"components.Discover.tmdbtvkeyword": "TMDB Series Keyword",
"components.Discover.tmdbtvstreamingservices": "TMDB TV Streaming Services",
"components.Discover.trending": "Trending",
"components.Discover.tvgenres": "Series Genres",
"components.Discover.upcoming": "Upcoming Movies",
@@ -198,6 +203,8 @@
"components.Layout.Sidebar.browsemovies": "Movies",
"components.Layout.Sidebar.browsetv": "Series",
"components.Layout.Sidebar.dashboard": "Discover",
"components.Layout.Sidebar.browsemovies": "Movies",
"components.Layout.Sidebar.browsetv": "Series",
"components.Layout.Sidebar.issues": "Issues",
"components.Layout.Sidebar.requests": "Requests",
"components.Layout.Sidebar.settings": "Settings",
@@ -320,7 +327,7 @@
"components.NotificationTypeSelector.mediaapproved": "Request Approved",
"components.NotificationTypeSelector.mediaapprovedDescription": "Send notifications when media requests are manually approved.",
"components.NotificationTypeSelector.mediaautorequested": "Request Automatically Submitted",
"components.NotificationTypeSelector.mediaautorequestedDescription": "Get notified when new media requests are automatically submitted for items on your Plex Watchlist.",
"components.NotificationTypeSelector.mediaautorequestedDescription": "Get notified when new media requests are automatically submitted for items on Your Watchlist.",
"components.NotificationTypeSelector.mediaavailable": "Request Available",
"components.NotificationTypeSelector.mediaavailableDescription": "Send notifications when media requests become available.",
"components.NotificationTypeSelector.mediadeclined": "Request Declined",
@@ -706,6 +713,8 @@
"components.Settings.RadarrModal.servername": "Server Name",
"components.Settings.RadarrModal.ssl": "Use SSL",
"components.Settings.RadarrModal.syncEnabled": "Enable Scan",
"components.Settings.RadarrModal.tagRequests": "Tag Requests",
"components.Settings.RadarrModal.tagRequestsInfo": "Automatically add an additional tag with the requester's user ID & display name",
"components.Settings.RadarrModal.tags": "Tags",
"components.Settings.RadarrModal.testFirstQualityProfiles": "Test connection to load quality profiles",
"components.Settings.RadarrModal.testFirstRootFolders": "Test connection to load root folders",
@@ -885,6 +894,8 @@
"components.Settings.SonarrModal.servername": "Server Name",
"components.Settings.SonarrModal.ssl": "Use SSL",
"components.Settings.SonarrModal.syncEnabled": "Enable Scan",
"components.Settings.SonarrModal.tagRequests": "Tag Requests",
"components.Settings.SonarrModal.tagRequestsInfo": "Automatically add an additional tag with the requester's user ID & display name",
"components.Settings.SonarrModal.tags": "Tags",
"components.Settings.SonarrModal.testFirstLanguageProfiles": "Test connection to load language profiles",
"components.Settings.SonarrModal.testFirstQualityProfiles": "Test connection to load quality profiles",
@@ -1222,6 +1233,11 @@
"components.UserProfile.seriesrequest": "Series Requests",
"components.UserProfile.totalrequests": "Total Requests",
"components.UserProfile.unlimited": "Unlimited",
"components.TitleCard.addToWatchList": "Add to watchlist",
"components.TitleCard.watchlistCancel": "watchlist for <strong>{title}</strong> canceled.",
"components.TitleCard.watchlistDeleted": "<strong>{title}</strong> Removed from watchlist successfully!",
"components.TitleCard.watchlistError": "Something went wrong try again.",
"components.TitleCard.watchlistSuccess": "<strong>{title}</strong> added to watchlist successfully!",
"i18n.advanced": "Advanced",
"i18n.all": "All",
"i18n.approve": "Approve",
@@ -1232,6 +1248,7 @@
"i18n.cancel": "Cancel",
"i18n.canceling": "Canceling…",
"i18n.close": "Close",
"i18n.collection": "Collection",
"i18n.decline": "Decline",
"i18n.declined": "Declined",
"i18n.delete": "Delete",

View File

@@ -12,7 +12,7 @@
"components.Discover.DiscoverStudio.studioMovies": "Фільми {studio}",
"components.Discover.DiscoverTvGenre.genreSeries": "Серіали в жанрі \"{genre}\"",
"components.Discover.DiscoverTvLanguage.languageSeries": "Серіали мовою \"{language}\"",
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Your Plex Watchlist",
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Your Watchlist",
"components.Discover.DiscoverWatchlist.watchlist": "Список спостереження Plex",
"components.Discover.MovieGenreList.moviegenres": "Фільми за жанрами",
"components.Discover.MovieGenreSlider.moviegenres": "Фільми за жанрами",

View File

@@ -274,6 +274,8 @@
"components.Layout.UserDropdown.signout": "登出",
"components.Layout.UserDropdown.settings": "用户设定",
"components.Layout.UserDropdown.myprofile": "个人档案",
"components.Layout.Sidebar.browsemovies": "电影",
"components.Layout.Sidebar.browsetv": "电视节目",
"components.Layout.Sidebar.users": "用户",
"components.Layout.Sidebar.settings": "设定",
"components.Layout.Sidebar.requests": "请求",
@@ -289,6 +291,7 @@
"components.Discover.trending": "趋势",
"components.Discover.recentrequests": "最新请求",
"components.Discover.recentlyAdded": "最新添加",
"components.Discover.RecentlyAddedSlider.recentlyAdded": "最近添加",
"components.Discover.populartv": "热门电视节目",
"components.Discover.popularmovies": "热门电影",
"components.Discover.discovertv": "热门电视节目",

View File

@@ -1,7 +1,7 @@
import MovieDetails from '@app/components/MovieDetails';
import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
import axios from 'axios';
import type { NextPage } from 'next';
import type { GetServerSideProps, NextPage } from 'next';
interface MoviePageProps {
movie?: MovieDetailsType;
@@ -11,25 +11,25 @@ const MoviePage: NextPage<MoviePageProps> = ({ movie }) => {
return <MovieDetails movie={movie} />;
};
MoviePage.getInitialProps = async (ctx) => {
if (ctx.req) {
const response = await axios.get<MovieDetailsType>(
`http://localhost:${process.env.PORT || 5055}/api/v1/movie/${
ctx.query.movieId
}`,
{
headers: ctx.req?.headers?.cookie
? { cookie: ctx.req.headers.cookie }
: undefined,
}
);
export const getServerSideProps: GetServerSideProps<MoviePageProps> = async (
ctx
) => {
const response = await axios.get<MovieDetailsType>(
`http://localhost:${process.env.PORT || 5055}/api/v1/movie/${
ctx.query.movieId
}`,
{
headers: ctx.req?.headers?.cookie
? { cookie: ctx.req.headers.cookie }
: undefined,
}
);
return {
return {
props: {
movie: response.data,
};
}
return {};
},
};
};
export default MoviePage;

View File

@@ -1,7 +1,7 @@
import TvDetails from '@app/components/TvDetails';
import type { TvDetails as TvDetailsType } from '@server/models/Tv';
import axios from 'axios';
import type { NextPage } from 'next';
import type { GetServerSideProps, NextPage } from 'next';
interface TvPageProps {
tv?: TvDetailsType;
@@ -11,25 +11,23 @@ const TvPage: NextPage<TvPageProps> = ({ tv }) => {
return <TvDetails tv={tv} />;
};
TvPage.getInitialProps = async (ctx) => {
if (ctx.req) {
const response = await axios.get<TvDetailsType>(
`http://localhost:${process.env.PORT || 5055}/api/v1/tv/${
ctx.query.tvId
}`,
{
headers: ctx.req?.headers?.cookie
? { cookie: ctx.req.headers.cookie }
: undefined,
}
);
export const getServerSideProps: GetServerSideProps<TvPageProps> = async (
ctx
) => {
const response = await axios.get<TvDetailsType>(
`http://localhost:${process.env.PORT || 5055}/api/v1/tv/${ctx.query.tvId}`,
{
headers: ctx.req?.headers?.cookie
? { cookie: ctx.req.headers.cookie }
: undefined,
}
);
return {
return {
props: {
tv: response.data,
};
}
return {};
},
};
};
export default TvPage;

View File

@@ -17,7 +17,7 @@
body {
@apply bg-gray-900;
overscroll-behavior-y: contain;
-webkit-overflow-scrolling: touch;
}
code {
@@ -73,6 +73,10 @@
grid-template-columns: repeat(auto-fill, minmax(16.5rem, 1fr));
}
.provider-icons {
grid-template-columns: repeat(auto-fill, minmax(3.5rem, 1fr));
}
.slider-header {
@apply relative mt-6 mb-4 flex;
}