Compare commits
48 Commits
preview-je
...
pr-934
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd18a9fe9a | ||
|
|
70aab8b0a9 | ||
|
|
1c176dc71e | ||
|
|
bd4da6d5fc | ||
|
|
12f908de7f | ||
|
|
c7d2386799 | ||
|
|
61dcd8e487 | ||
|
|
9aee8887d3 | ||
|
|
2348f23f43 | ||
|
|
b04f280fbd | ||
|
|
6b0909264d | ||
|
|
7923fc67e8 | ||
|
|
afed6e8879 | ||
|
|
341c7171cd | ||
|
|
3ab184be20 | ||
|
|
7be97a0e12 | ||
|
|
78e6fdb7bf | ||
|
|
fc95e9da4b | ||
|
|
e6b5707190 | ||
|
|
1218876fbf | ||
|
|
b9336fc589 | ||
|
|
20762143db | ||
|
|
9d10efd277 | ||
|
|
ba81ed25e2 | ||
|
|
05a4adf369 | ||
|
|
1449883629 | ||
|
|
9f4329e243 | ||
|
|
5743105db4 | ||
|
|
fce8231710 | ||
|
|
53be59ad51 | ||
|
|
7b8adf5bdf | ||
|
|
7ee45ef6e1 | ||
|
|
50488db05a | ||
|
|
a132105b45 | ||
|
|
cb172cc737 | ||
|
|
cee3be2c64 | ||
|
|
8ecb459808 | ||
|
|
372b3649b5 | ||
|
|
ee472e11af | ||
|
|
1fd3a00284 | ||
|
|
c97c40e04c | ||
|
|
12f4d78692 | ||
|
|
18e3f140eb | ||
|
|
226e910728 | ||
|
|
fb6140972a | ||
|
|
9499b364af | ||
|
|
a15f0ec029 | ||
|
|
5c269368aa |
@@ -85,6 +85,7 @@ export interface MovieDetails {
|
||||
mediaUrl?: string;
|
||||
watchProviders?: WatchProviders[];
|
||||
keywords: Keyword[];
|
||||
onUserWatchlist?: boolean;
|
||||
}
|
||||
|
||||
export const mapProductionCompany = (
|
||||
@@ -101,7 +102,8 @@ export const mapProductionCompany = (
|
||||
|
||||
export const mapMovieDetails = (
|
||||
movie: TmdbMovieDetails,
|
||||
media?: Media
|
||||
media?: Media,
|
||||
userWatchlist?: boolean
|
||||
): MovieDetails => ({
|
||||
id: movie.id,
|
||||
adult: movie.adult,
|
||||
@@ -148,4 +150,5 @@ export const mapMovieDetails = (
|
||||
id: keyword.id,
|
||||
name: keyword.name,
|
||||
})),
|
||||
onUserWatchlist: userWatchlist,
|
||||
});
|
||||
|
||||
@@ -111,6 +111,7 @@ export interface TvDetails {
|
||||
keywords: Keyword[];
|
||||
mediaInfo?: Media;
|
||||
watchProviders?: WatchProviders[];
|
||||
onUserWatchlist?: boolean;
|
||||
}
|
||||
|
||||
const mapEpisodeResult = (episode: TmdbTvEpisodeResult): Episode => ({
|
||||
@@ -161,7 +162,8 @@ export const mapNetwork = (network: TmdbNetwork): TvNetwork => ({
|
||||
|
||||
export const mapTvDetails = (
|
||||
show: TmdbTvDetails,
|
||||
media?: Media
|
||||
media?: Media,
|
||||
userWatchlist?: boolean
|
||||
): TvDetails => ({
|
||||
createdBy: show.created_by,
|
||||
episodeRunTime: show.episode_run_time,
|
||||
@@ -223,4 +225,5 @@ export const mapTvDetails = (
|
||||
})),
|
||||
mediaInfo: media,
|
||||
watchProviders: mapWatchProviders(show['watch/providers']?.results ?? {}),
|
||||
onUserWatchlist: userWatchlist,
|
||||
});
|
||||
|
||||
@@ -3,7 +3,9 @@ import RottenTomatoes from '@server/api/rating/rottentomatoes';
|
||||
import { type RatingResponse } from '@server/api/ratings';
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import { MediaType } from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import { Watchlist } from '@server/entity/Watchlist';
|
||||
import logger from '@server/logger';
|
||||
import { mapMovieDetails } from '@server/models/Movie';
|
||||
import { mapMovieResult } from '@server/models/Search';
|
||||
@@ -22,7 +24,24 @@ movieRoutes.get('/:id', async (req, res, next) => {
|
||||
|
||||
const media = await Media.getMedia(tmdbMovie.id, MediaType.MOVIE);
|
||||
|
||||
return res.status(200).json(mapMovieDetails(tmdbMovie, media));
|
||||
const onUserWatchlist = await getRepository(Watchlist).exist({
|
||||
where: {
|
||||
tmdbId: Number(req.params.id),
|
||||
requestedBy: {
|
||||
id: req.user?.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const data = mapMovieDetails(tmdbMovie, media, onUserWatchlist);
|
||||
|
||||
// TMDB issue where it doesnt fallback to English when no overview is available in requested locale.
|
||||
if (!data.overview) {
|
||||
const tvEnglish = await tmdb.getMovie({ movieId: Number(req.params.id) });
|
||||
data.overview = tvEnglish.overview;
|
||||
}
|
||||
|
||||
return res.status(200).json(data);
|
||||
} catch (e) {
|
||||
logger.debug('Something went wrong retrieving movie', {
|
||||
label: 'API',
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import RottenTomatoes from '@server/api/rating/rottentomatoes';
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import { MediaType } from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import { Watchlist } from '@server/entity/Watchlist';
|
||||
import logger from '@server/logger';
|
||||
import { mapTvResult } from '@server/models/Search';
|
||||
import { mapSeasonWithEpisodes, mapTvDetails } from '@server/models/Tv';
|
||||
@@ -19,7 +21,24 @@ tvRoutes.get('/:id', async (req, res, next) => {
|
||||
|
||||
const media = await Media.getMedia(tv.id, MediaType.TV);
|
||||
|
||||
return res.status(200).json(mapTvDetails(tv, media));
|
||||
const onUserWatchlist = await getRepository(Watchlist).exist({
|
||||
where: {
|
||||
tmdbId: Number(req.params.id),
|
||||
requestedBy: {
|
||||
id: req.user?.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const data = mapTvDetails(tv, media, onUserWatchlist);
|
||||
|
||||
// TMDB issue where it doesnt fallback to English when no overview is available in requested locale.
|
||||
if (!data.overview) {
|
||||
const tvEnglish = await tmdb.getTvShow({ tvId: Number(req.params.id) });
|
||||
data.overview = tvEnglish.overview;
|
||||
}
|
||||
|
||||
return res.status(200).json(data);
|
||||
} catch (e) {
|
||||
logger.debug('Something went wrong retrieving series', {
|
||||
label: 'API',
|
||||
|
||||
@@ -3,6 +3,7 @@ import RTAudRotten from '@app/assets/rt_aud_rotten.svg';
|
||||
import RTFresh from '@app/assets/rt_fresh.svg';
|
||||
import RTRotten from '@app/assets/rt_rotten.svg';
|
||||
import ImdbLogo from '@app/assets/services/imdb.svg';
|
||||
import Spinner from '@app/assets/spinner.svg';
|
||||
import TmdbLogo from '@app/assets/tmdb_logo.svg';
|
||||
import Button from '@app/components/Common/Button';
|
||||
import CachedImage from '@app/components/Common/CachedImage';
|
||||
@@ -25,7 +26,7 @@ import useLocale from '@app/hooks/useLocale';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import ErrorPage from '@app/pages/_error';
|
||||
import { sortCrewPriority } from '@app/utils/creditHelpers';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
|
||||
@@ -41,10 +42,12 @@ import {
|
||||
import {
|
||||
ChevronDoubleDownIcon,
|
||||
ChevronDoubleUpIcon,
|
||||
MinusCircleIcon,
|
||||
StarIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
import { type RatingResponse } from '@server/api/ratings';
|
||||
import { IssueStatus } from '@server/constants/issue';
|
||||
import { MediaStatus } from '@server/constants/media';
|
||||
import { MediaStatus, MediaType } from '@server/constants/media';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
|
||||
import { countries } from 'country-flag-icons';
|
||||
@@ -55,6 +58,7 @@ import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages('components.MovieDetails', {
|
||||
@@ -94,6 +98,12 @@ const messages = defineMessages('components.MovieDetails', {
|
||||
rtaudiencescore: 'Rotten Tomatoes Audience Score',
|
||||
tmdbuserscore: 'TMDB User Score',
|
||||
imdbuserscore: 'IMDB User Score',
|
||||
watchlistSuccess: '<strong>{title}</strong> added to watchlist successfully!',
|
||||
watchlistDeleted:
|
||||
'<strong>{title}</strong> Removed from watchlist successfully!',
|
||||
watchlistError: 'Something went wrong try again.',
|
||||
removefromwatchlist: 'Remove From Watchlist',
|
||||
addtowatchlist: 'Add To Watchlist',
|
||||
});
|
||||
|
||||
interface MovieDetailsProps {
|
||||
@@ -112,7 +122,12 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
const minStudios = 3;
|
||||
const [showMoreStudios, setShowMoreStudios] = useState(false);
|
||||
const [showIssueModal, setShowIssueModal] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
||||
const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
|
||||
!movie?.onUserWatchlist
|
||||
);
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const { addToast } = useToasts();
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -154,7 +169,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Error statusCode={404} />;
|
||||
return <ErrorPage statusCode={404} />;
|
||||
}
|
||||
|
||||
const showAllStudios = data.productionCompanies.length <= minStudios + 1;
|
||||
@@ -287,6 +302,80 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
return intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' });
|
||||
}
|
||||
|
||||
const onClickWatchlistBtn = async (): Promise<void> => {
|
||||
setIsUpdating(true);
|
||||
|
||||
const res = await fetch('/api/v1/watchlist', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tmdbId: movie?.id,
|
||||
mediaType: MediaType.MOVIE,
|
||||
title: movie?.title,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
addToast(intl.formatMessage(messages.watchlistError), {
|
||||
appearance: 'error',
|
||||
autoDismiss: true,
|
||||
});
|
||||
|
||||
setIsUpdating(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data) {
|
||||
addToast(
|
||||
<span>
|
||||
{intl.formatMessage(messages.watchlistSuccess, {
|
||||
title: movie?.title,
|
||||
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
|
||||
})}
|
||||
</span>,
|
||||
{ appearance: 'success', autoDismiss: true }
|
||||
);
|
||||
}
|
||||
|
||||
setIsUpdating(false);
|
||||
setToggleWatchlist((prevState) => !prevState);
|
||||
};
|
||||
|
||||
const onClickDeleteWatchlistBtn = async (): Promise<void> => {
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
const res = await fetch(`/api/v1/watchlist/${movie?.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
|
||||
if (res.status === 204) {
|
||||
addToast(
|
||||
<span>
|
||||
{intl.formatMessage(messages.watchlistDeleted, {
|
||||
title: movie?.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);
|
||||
setToggleWatchlist((prevState) => !prevState);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="media-page"
|
||||
@@ -408,6 +497,40 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
</span>
|
||||
</div>
|
||||
<div className="media-actions">
|
||||
<>
|
||||
{toggleWatchlist ? (
|
||||
<Tooltip content={intl.formatMessage(messages.addtowatchlist)}>
|
||||
<Button
|
||||
buttonType={'ghost'}
|
||||
className="z-40 mr-2"
|
||||
buttonSize={'md'}
|
||||
onClick={onClickWatchlistBtn}
|
||||
>
|
||||
{isUpdating ? (
|
||||
<Spinner className="h-3" />
|
||||
) : (
|
||||
<StarIcon className={'h-3 text-amber-300'} />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip
|
||||
content={intl.formatMessage(messages.removefromwatchlist)}
|
||||
>
|
||||
<Button
|
||||
className="z-40 mr-2"
|
||||
buttonSize={'md'}
|
||||
onClick={onClickDeleteWatchlistBtn}
|
||||
>
|
||||
{isUpdating ? (
|
||||
<Spinner className="h-3" />
|
||||
) : (
|
||||
<MinusCircleIcon className={'h-3'} />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
<PlayButton links={mediaLinks} />
|
||||
<RequestButton
|
||||
mediaType="movie"
|
||||
|
||||
@@ -2,6 +2,7 @@ import RTAudFresh from '@app/assets/rt_aud_fresh.svg';
|
||||
import RTAudRotten from '@app/assets/rt_aud_rotten.svg';
|
||||
import RTFresh from '@app/assets/rt_fresh.svg';
|
||||
import RTRotten from '@app/assets/rt_rotten.svg';
|
||||
import Spinner from '@app/assets/spinner.svg';
|
||||
import TmdbLogo from '@app/assets/tmdb_logo.svg';
|
||||
import Badge from '@app/components/Common/Badge';
|
||||
import Button from '@app/components/Common/Button';
|
||||
@@ -40,11 +41,19 @@ import {
|
||||
FilmIcon,
|
||||
PlayIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { ChevronDownIcon } from '@heroicons/react/24/solid';
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
MinusCircleIcon,
|
||||
StarIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
import type { RTRating } from '@server/api/rating/rottentomatoes';
|
||||
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
||||
import { IssueStatus } from '@server/constants/issue';
|
||||
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
||||
import {
|
||||
MediaRequestStatus,
|
||||
MediaStatus,
|
||||
MediaType,
|
||||
} from '@server/constants/media';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import type { Crew } from '@server/models/common';
|
||||
import type { TvDetails as TvDetailsType } from '@server/models/Tv';
|
||||
@@ -55,6 +64,7 @@ import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages('components.TvDetails', {
|
||||
@@ -89,6 +99,12 @@ const messages = defineMessages('components.TvDetails', {
|
||||
rtcriticsscore: 'Rotten Tomatoes Tomatometer',
|
||||
rtaudiencescore: 'Rotten Tomatoes Audience Score',
|
||||
tmdbuserscore: 'TMDB User Score',
|
||||
watchlistSuccess: '<strong>{title}</strong> added to watchlist successfully!',
|
||||
watchlistDeleted:
|
||||
'<strong>{title}</strong> Removed from watchlist successfully!',
|
||||
watchlistError: 'Something went wrong try again.',
|
||||
removefromwatchlist: 'Remove From Watchlist',
|
||||
addtowatchlist: 'Add To Watchlist',
|
||||
});
|
||||
|
||||
interface TvDetailsProps {
|
||||
@@ -106,7 +122,12 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
router.query.manage == '1' ? true : false
|
||||
);
|
||||
const [showIssueModal, setShowIssueModal] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
||||
const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
|
||||
!tv?.onUserWatchlist
|
||||
);
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const { addToast } = useToasts();
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -302,6 +323,82 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
return intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' });
|
||||
}
|
||||
|
||||
const onClickWatchlistBtn = async (): Promise<void> => {
|
||||
setIsUpdating(true);
|
||||
|
||||
const res = await fetch('/api/v1/watchlist', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tmdbId: tv?.id,
|
||||
mediaType: MediaType.TV,
|
||||
title: tv?.name,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
addToast(intl.formatMessage(messages.watchlistError), {
|
||||
appearance: 'error',
|
||||
autoDismiss: true,
|
||||
});
|
||||
|
||||
setIsUpdating(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data) {
|
||||
addToast(
|
||||
<span>
|
||||
{intl.formatMessage(messages.watchlistSuccess, {
|
||||
title: tv?.name,
|
||||
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
|
||||
})}
|
||||
</span>,
|
||||
{ appearance: 'success', autoDismiss: true }
|
||||
);
|
||||
}
|
||||
|
||||
setIsUpdating(false);
|
||||
setToggleWatchlist((prevState) => !prevState);
|
||||
};
|
||||
|
||||
const onClickDeleteWatchlistBtn = async (): Promise<void> => {
|
||||
setIsUpdating(true);
|
||||
|
||||
const res = await fetch('/api/v1/watchlist/' + tv?.id, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
addToast(intl.formatMessage(messages.watchlistError), {
|
||||
appearance: 'error',
|
||||
autoDismiss: true,
|
||||
});
|
||||
|
||||
setIsUpdating(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.status === 204) {
|
||||
addToast(
|
||||
<span>
|
||||
{intl.formatMessage(messages.watchlistDeleted, {
|
||||
title: tv?.name,
|
||||
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
|
||||
})}
|
||||
</span>,
|
||||
{ appearance: 'info', autoDismiss: true }
|
||||
);
|
||||
setIsUpdating(false);
|
||||
setToggleWatchlist((prevState) => !prevState);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="media-page"
|
||||
@@ -433,6 +530,40 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
</span>
|
||||
</div>
|
||||
<div className="media-actions">
|
||||
<>
|
||||
{toggleWatchlist ? (
|
||||
<Tooltip content={intl.formatMessage(messages.addtowatchlist)}>
|
||||
<Button
|
||||
buttonType={'ghost'}
|
||||
className="z-40 mr-2"
|
||||
buttonSize={'md'}
|
||||
onClick={onClickWatchlistBtn}
|
||||
>
|
||||
{isUpdating ? (
|
||||
<Spinner className="h-3" />
|
||||
) : (
|
||||
<StarIcon className={'h-3 text-amber-300'} />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip
|
||||
content={intl.formatMessage(messages.removefromwatchlist)}
|
||||
>
|
||||
<Button
|
||||
className="z-40 mr-2"
|
||||
buttonSize={'md'}
|
||||
onClick={onClickDeleteWatchlistBtn}
|
||||
>
|
||||
{isUpdating ? (
|
||||
<Spinner className="h-3" />
|
||||
) : (
|
||||
<MinusCircleIcon className={'h-3'} />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
<PlayButton links={mediaLinks} />
|
||||
<RequestButton
|
||||
mediaType="tv"
|
||||
|
||||
@@ -56,14 +56,6 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
|
||||
`/api/v1/user?take=${children}`
|
||||
);
|
||||
|
||||
data?.forEach((user, pos) => {
|
||||
if (
|
||||
existingUsers?.results.some((data) => data.jellyfinUserId === user.id)
|
||||
) {
|
||||
data?.splice(pos, 1);
|
||||
}
|
||||
});
|
||||
|
||||
const importUsers = async () => {
|
||||
setImporting(true);
|
||||
|
||||
@@ -209,64 +201,71 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700 bg-gray-600">
|
||||
{data?.map((user) => (
|
||||
<tr key={`user-${user.id}`}>
|
||||
<td className="whitespace-nowrap px-4 py-4 text-sm font-medium leading-5 text-gray-100">
|
||||
<span
|
||||
role="checkbox"
|
||||
tabIndex={0}
|
||||
aria-checked={isSelectedUser(user.id)}
|
||||
onClick={() => toggleUser(user.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === 'Space') {
|
||||
toggleUser(user.id);
|
||||
}
|
||||
}}
|
||||
className="relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none"
|
||||
>
|
||||
{data
|
||||
?.filter(
|
||||
(user) =>
|
||||
!existingUsers?.results.some(
|
||||
(u) => u.jellyfinUserId === user.id
|
||||
)
|
||||
)
|
||||
.map((user) => (
|
||||
<tr key={`user-${user.id}`}>
|
||||
<td className="whitespace-nowrap px-4 py-4 text-sm font-medium leading-5 text-gray-100">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isSelectedUser(user.id)
|
||||
? 'bg-indigo-500'
|
||||
: 'bg-gray-800'
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
></span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isSelectedUser(user.id)
|
||||
? 'translate-x-5'
|
||||
: 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6">
|
||||
<div className="flex items-center">
|
||||
<Image
|
||||
className="h-10 w-10 flex-shrink-0 rounded-full"
|
||||
src={user.thumb}
|
||||
alt=""
|
||||
width={40}
|
||||
height={40}
|
||||
/>
|
||||
<div className="ml-4">
|
||||
<div className="text-base font-bold leading-5">
|
||||
{user.username}
|
||||
</div>
|
||||
{/* {user.username &&
|
||||
role="checkbox"
|
||||
tabIndex={0}
|
||||
aria-checked={isSelectedUser(user.id)}
|
||||
onClick={() => toggleUser(user.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === 'Space') {
|
||||
toggleUser(user.id);
|
||||
}
|
||||
}}
|
||||
className="relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isSelectedUser(user.id)
|
||||
? 'bg-indigo-500'
|
||||
: 'bg-gray-800'
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
></span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isSelectedUser(user.id)
|
||||
? 'translate-x-5'
|
||||
: 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6">
|
||||
<div className="flex items-center">
|
||||
<Image
|
||||
className="h-10 w-10 flex-shrink-0 rounded-full"
|
||||
src={user.thumb}
|
||||
alt=""
|
||||
width={40}
|
||||
height={40}
|
||||
/>
|
||||
<div className="ml-4">
|
||||
<div className="text-base font-bold leading-5">
|
||||
{user.username}
|
||||
</div>
|
||||
{/* {user.username &&
|
||||
user.username.toLowerCase() !==
|
||||
user.email && (
|
||||
<div className="text-sm leading-5 text-gray-300">
|
||||
{user.email}
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -286,6 +286,7 @@
|
||||
"components.MediaSlider.ShowMoreCard.seemore": "See More",
|
||||
"components.MovieDetails.MovieCast.fullcast": "Full Cast",
|
||||
"components.MovieDetails.MovieCrew.fullcrew": "Full Crew",
|
||||
"components.MovieDetails.addtowatchlist": "Add To Watchlist",
|
||||
"components.MovieDetails.budget": "Budget",
|
||||
"components.MovieDetails.cast": "Cast",
|
||||
"components.MovieDetails.digitalrelease": "Digital Release",
|
||||
@@ -306,6 +307,7 @@
|
||||
"components.MovieDetails.productioncountries": "Production {countryCount, plural, one {Country} other {Countries}}",
|
||||
"components.MovieDetails.recommendations": "Recommendations",
|
||||
"components.MovieDetails.releasedate": "{releaseCount, plural, one {Release Date} other {Release Dates}}",
|
||||
"components.MovieDetails.removefromwatchlist": "Remove From Watchlist",
|
||||
"components.MovieDetails.reportissue": "Report an Issue",
|
||||
"components.MovieDetails.revenue": "Revenue",
|
||||
"components.MovieDetails.rtaudiencescore": "Rotten Tomatoes Audience Score",
|
||||
@@ -319,6 +321,9 @@
|
||||
"components.MovieDetails.theatricalrelease": "Theatrical Release",
|
||||
"components.MovieDetails.tmdbuserscore": "TMDB User Score",
|
||||
"components.MovieDetails.viewfullcrew": "View Full Crew",
|
||||
"components.MovieDetails.watchlistDeleted": "<strong>{title}</strong> Removed from watchlist successfully!",
|
||||
"components.MovieDetails.watchlistError": "Something went wrong try again.",
|
||||
"components.MovieDetails.watchlistSuccess": "<strong>{title}</strong> added to watchlist successfully!",
|
||||
"components.MovieDetails.watchtrailer": "Watch Trailer",
|
||||
"components.NotificationTypeSelector.adminissuecommentDescription": "Get notified when other users comment on issues.",
|
||||
"components.NotificationTypeSelector.adminissuereopenedDescription": "Get notified when issues are reopened by other users.",
|
||||
@@ -1071,6 +1076,7 @@
|
||||
"components.TvDetails.Season.somethingwentwrong": "Something went wrong while retrieving season data.",
|
||||
"components.TvDetails.TvCast.fullseriescast": "Full Series Cast",
|
||||
"components.TvDetails.TvCrew.fullseriescrew": "Full Series Crew",
|
||||
"components.TvDetails.addtowatchlist": "Add To Watchlist",
|
||||
"components.TvDetails.anime": "Anime",
|
||||
"components.TvDetails.cast": "Cast",
|
||||
"components.TvDetails.episodeCount": "{episodeCount, plural, one {# Episode} other {# Episodes}}",
|
||||
@@ -1088,6 +1094,7 @@
|
||||
"components.TvDetails.play4k": "Play 4K on {mediaServerName}",
|
||||
"components.TvDetails.productioncountries": "Production {countryCount, plural, one {Country} other {Countries}}",
|
||||
"components.TvDetails.recommendations": "Recommendations",
|
||||
"components.TvDetails.removefromwatchlist": "Remove From Watchlist",
|
||||
"components.TvDetails.reportissue": "Report an Issue",
|
||||
"components.TvDetails.rtaudiencescore": "Rotten Tomatoes Audience Score",
|
||||
"components.TvDetails.rtcriticsscore": "Rotten Tomatoes Tomatometer",
|
||||
@@ -1100,6 +1107,9 @@
|
||||
"components.TvDetails.streamingproviders": "Currently Streaming On",
|
||||
"components.TvDetails.tmdbuserscore": "TMDB User Score",
|
||||
"components.TvDetails.viewfullcrew": "View Full Crew",
|
||||
"components.TvDetails.watchlistDeleted": "<strong>{title}</strong> Removed from watchlist successfully!",
|
||||
"components.TvDetails.watchlistError": "Something went wrong try again.",
|
||||
"components.TvDetails.watchlistSuccess": "<strong>{title}</strong> added to watchlist successfully!",
|
||||
"components.TvDetails.watchtrailer": "Watch Trailer",
|
||||
"components.UserList.accounttype": "Type",
|
||||
"components.UserList.admin": "Admin",
|
||||
|
||||
59
src/i18n/locale/sl.json
Normal file
59
src/i18n/locale/sl.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"components.Discover.CreateSlider.editsuccess": "Urejen drsnik in shranjene nastavitve prilagajanja odkrivanja.",
|
||||
"components.CollectionDetails.numberofmovies": "{count} film/ov",
|
||||
"components.Discover.CreateSlider.slidernameplaceholder": "Ime drsnika",
|
||||
"components.Discover.DiscoverTv.sortFirstAirDateAsc": "Premiera ↓",
|
||||
"components.AppDataWarning.dockerVolumeMissingDescription": "Pripenjanje nosilca <code>{appDataPath}</code> ni bilo pravilno konfigurirano. Vsi podatki bodo izbrisani, ko se vsebnik zaustavi ali znova zažene.",
|
||||
"components.Discover.DiscoverMovies.sortPopularityDesc": "Priljubljenost ↑",
|
||||
"components.AirDateBadge.airsrelative": "Predvajanje {relativeTime}",
|
||||
"components.CollectionDetails.overview": "Pregled",
|
||||
"components.Discover.DiscoverMovies.activefilters": "{count, plural, one {# Active Filter} drugo {# Active Filters}}",
|
||||
"components.Discover.DiscoverMovies.sortTmdbRatingAsc": "Ocena TMDB ↓",
|
||||
"components.AirDateBadge.airedrelative": "Predvajano {relativeTime}",
|
||||
"components.Discover.CreateSlider.searchStudios": "Iskanje studiev …",
|
||||
"components.Discover.DiscoverMovies.sortReleaseDateDesc": "Datum izdaje ↑",
|
||||
"components.Discover.CreateSlider.providetmdbnetwork": "Navedite ID omrežja TMDB",
|
||||
"components.Discover.CreateSlider.addfail": "Novega drsnika ni bilo mogoče ustvariti.",
|
||||
"components.CollectionDetails.requestcollection": "Zahtevaj zbirko",
|
||||
"components.Discover.DiscoverMovieGenre.genreMovies": "Filmi: {genre}",
|
||||
"components.Discover.DiscoverMovieLanguage.languageMovies": "Filmi: {language}",
|
||||
"components.Discover.DiscoverTv.activefilters": "{count, plural, one {# Active Filter} other {# Active Filters}}",
|
||||
"components.Discover.DiscoverMovies.sortPopularityAsc": "Priljubljenost ↓",
|
||||
"components.Discover.CreateSlider.needresults": "Imeti morate vsaj 1 rezultat.",
|
||||
"components.Discover.CreateSlider.addcustomslider": "Ustvari drsnik po meri",
|
||||
"components.Discover.DiscoverTv.sortPopularityAsc": "Priljubljenost ↓",
|
||||
"components.Discover.CreateSlider.editSlider": "Uredi drsnik",
|
||||
"components.Discover.DiscoverTv.sortTitleAsc": "Naslov (a-ž) ↓",
|
||||
"components.Discover.CreateSlider.validationDatarequired": "Navesti morate vrednost podatkov.",
|
||||
"components.Discover.DiscoverTv.sortFirstAirDateDesc": "Premiera ↑",
|
||||
"components.Discover.DiscoverTv.discovertv": "Serije",
|
||||
"components.Discover.DiscoverSliderEdit.deletefail": "Drsnika ni bilo mogoče izbrisati.",
|
||||
"components.Discover.CreateSlider.providetmdbstudio": "Navedite ID studia v TMDB",
|
||||
"components.Discover.DiscoverMovies.sortTitleDesc": "Naslov (a-ž) ↑",
|
||||
"components.Discover.DiscoverStudio.studioMovies": "{studio} filmi",
|
||||
"components.Discover.DiscoverTv.sortPopularityDesc": "Priljubljenost ↑",
|
||||
"components.Discover.CreateSlider.searchGenres": "Išči žanre …",
|
||||
"components.Discover.CreateSlider.editfail": "Drsnika ni bilo mogoče urediti.",
|
||||
"components.Discover.CreateSlider.starttyping": "Tipkajte za iskanje.",
|
||||
"components.Discover.DiscoverSliderEdit.enable": "Preklopi vidnost",
|
||||
"components.Discover.CreateSlider.addSlider": "Dodaj drsnik",
|
||||
"components.CollectionDetails.requestcollection4k": "Zahtevaj zbirko 4K",
|
||||
"components.Discover.CreateSlider.providetmdbsearch": "Vnesite iskalno poizvedbo",
|
||||
"components.Discover.DiscoverNetwork.networkSeries": "{network} serije",
|
||||
"components.Discover.CreateSlider.providetmdbkeywordid": "Navedite ID ključne besede TMDB",
|
||||
"components.Discover.DiscoverMovieKeyword.keywordMovies": "Filmi: {keywordTitle}",
|
||||
"components.Discover.CreateSlider.validationTitlerequired": "Navesti morate naslov.",
|
||||
"components.Discover.DiscoverMovies.sortReleaseDateAsc": "Datum izdaje ↓",
|
||||
"components.Discover.CreateSlider.nooptions": "Ni zadetkov.",
|
||||
"components.Discover.DiscoverMovies.sortTmdbRatingDesc": "Ocena TMDB ↑",
|
||||
"components.Discover.CreateSlider.searchKeywords": "Iskanje po ključnih besedah …",
|
||||
"components.Discover.CreateSlider.addsuccess": "Ustvarjen nov drsnik in shranjene nastavitve prilagajanja odkrivanja.",
|
||||
"components.Discover.DiscoverSliderEdit.deletesuccess": "Drsnik je bil uspešno izbrisan.",
|
||||
"components.Discover.DiscoverMovies.discovermovies": "Filmi",
|
||||
"components.Discover.DiscoverMovies.sortTitleAsc": "Naslov (a-ž) ↓",
|
||||
"components.Discover.CreateSlider.providetmdbgenreid": "Navedite ID žanra TMDB",
|
||||
"components.Discover.DiscoverTv.sortTitleDesc": "Naslov (a-ž) ↑",
|
||||
"components.Discover.DiscoverSliderEdit.remove": "Odstrani",
|
||||
"components.Discover.DiscoverTv.sortTmdbRatingAsc": "Ocena TMDB ↓",
|
||||
"components.Discover.DiscoverTvGenre.genreSeries": "{genre} Series"
|
||||
}
|
||||
1308
src/i18n/locale/tr.json
Normal file
1308
src/i18n/locale/tr.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user