diff --git a/src/components/Collection/Series/EpisodeDetails.tsx b/src/components/Collection/Series/EpisodeDetails.tsx index def6f8451..eb231c516 100644 --- a/src/components/Collection/Series/EpisodeDetails.tsx +++ b/src/components/Collection/Series/EpisodeDetails.tsx @@ -45,7 +45,7 @@ function EpisodeDetails({ episode }: { episode: EpisodeType }) {  Votes) -
+
{episode.AniDB?.Description !== '' ? episode.AniDB?.Description : 'Episode description not available.'}
diff --git a/src/components/Collection/SeriesInfo.tsx b/src/components/Collection/SeriesInfo.tsx new file mode 100644 index 000000000..fbb538088 --- /dev/null +++ b/src/components/Collection/SeriesInfo.tsx @@ -0,0 +1,146 @@ +import React, { useMemo } from 'react'; +import { useParams } from 'react-router'; +import { Link } from 'react-router-dom'; + +import { useGetSeriesQuery } from '@/core/rtkQuery/splitV3Api/seriesApi'; +import { useGetSeriesOverviewQuery } from '@/core/rtkQuery/splitV3Api/webuiApi'; +import { dayjs, formatThousand } from '@/core/util'; + +import type { SeriesDetailsType } from '@/core/types/api/series'; +import type { WebuiSeriesDetailsType } from '@/core/types/api/webui'; + +const SeriesInfo = () => { + const { seriesId } = useParams(); + + // Series Data; + const seriesOverviewData = useGetSeriesOverviewQuery({ SeriesID: seriesId! }, { skip: !seriesId }); + const overview = useMemo(() => seriesOverviewData?.data || {} as WebuiSeriesDetailsType, [seriesOverviewData]); + const seriesData = useGetSeriesQuery({ seriesId: seriesId!, includeDataFrom: ['AniDB'] }, { skip: !seriesId }); + const series = useMemo(() => seriesData?.data ?? {} as SeriesDetailsType, [seriesData]); + + const startDate = useMemo(() => dayjs(series.AniDB?.AirDate), [series]); + const endDate = useMemo(() => (series.AniDB?.EndDate !== null ? dayjs(series.AniDB?.EndDate) : null), [series]); + const airDate = () => { + if (endDate) { + if (startDate.format('MMM DD, YYYY') === endDate.format('MMM DD, YYYY')) { + return startDate.format('MMM DD, YYYY'); + } + return `${startDate.format('MMM DD, YYYY')} - ${endDate.format('MMM DD, YYYY')}`; + } + return `${startDate.format('MMM DD, YYYY')} - Ongoing`; + }; + + return ( +
+
+
+
File Count
+
+ EP: + {formatThousand(series.Sizes.Local.Episodes)} + {series.Sizes.Local.Specials !== 0 && ( + <> + | + SP: + {formatThousand(series.Sizes.Local.Specials)} + + )} +
+
+
+
Watched
+
+ EP: + {formatThousand(series.Sizes.Watched.Episodes)} + {(series.Sizes.Total.Specials !== 0 && series.Sizes.Local.Specials !== 0) && ( + <> + | + SP: + {formatThousand(series.Sizes.Watched.Specials)} + + )} +
+
+ + {(series.Sizes.Total.Episodes - series.Sizes.Local.Episodes !== 0 + || series.Sizes.Total.Specials - series.Sizes.Local.Specials !== 0) && ( +
+
Missing
+
+ {series.Sizes.Total.Episodes - series.Sizes.Local.Episodes !== 0 && ( + <> + EP: + {formatThousand(series.Sizes.Total.Episodes - series.Sizes.Local.Episodes)} + + )} + {series.Sizes.Total.Episodes - series.Sizes.Local.Episodes !== 0 + && series.Sizes.Total.Specials - series.Sizes.Local.Specials !== 0 + && |} + {series.Sizes.Total.Specials - series.Sizes.Local.Specials !== 0 && ( + <> + SP: + {formatThousand(series.Sizes.Total.Specials - series.Sizes.Local.Specials)} + + )} +
+
+ )} +
+
+
+
Type
+ {series.AniDB?.Type} +
+
+
Source
+ {overview.SourceMaterial} +
+
+
Air Date
+
+ {airDate()} +
+
+
+
Status
+ {/* TODO: Check if there are more status types */} + {(series.AniDB?.EndDate && dayjs(series.AniDB.EndDate).isAfter(dayjs())) ? 'Ongoing' : 'Finished'} +
+
+
Episodes
+
+ {series.Sizes.Total.Episodes} +  Episodes + | + {series.Sizes.Total.Specials} +  Specials +
+
+
+
Length
+ {/* TODO: Get episode length */} +
-- Minutes/Episode
+
+
+
Season
+ {overview?.FirstAirSeason + ? ( + + {overview.FirstAirSeason.Name} + + ) + : '--'} +
+
+
Studio
+
{overview?.Studios?.[0] ? overview?.Studios?.[0].Name : 'Studio Not Listed'}
+
+
+
+ ); +}; + +export default SeriesInfo; diff --git a/src/components/Collection/SeriesMetadata.tsx b/src/components/Collection/SeriesMetadata.tsx new file mode 100644 index 000000000..204404428 --- /dev/null +++ b/src/components/Collection/SeriesMetadata.tsx @@ -0,0 +1,69 @@ +import React, { useMemo } from 'react'; +import { mdiCloseCircleOutline, mdiOpenInNew, mdiPencilCircleOutline, mdiPlusCircleOutline } from '@mdi/js'; +import { Icon } from '@mdi/react'; + +import Button from '@/components/Input/Button'; + +const MetadataLink = ({ id, site }: { site: string, id: number | number[] }) => { + const linkId = Array.isArray(id) ? id[0] : id; + + const siteLink = useMemo(() => { + switch (site) { + case 'AniDB': + return `https://anidb.net/anime/${linkId}`; + case 'TMDB': + return `https://www.themoviedb.org/movie/${linkId}`; + case 'TvDB': + // TODO: Figure how to get trakt series link using ID + return '#'; + case 'TraktTv': + // TODO: Figure how to get trakt series link using ID + return '#'; + default: + return '#'; + } + }, [linkId, site]); + + return ( +
+
+
+ {linkId + ? ( + + {site} + + + ) + : 'Series Not Linked'} +
+ {site !== 'AniDB' && ( +
+ {linkId + ? ( + <> + + + + ) + : ( + + )} +
+ )} +
+ ); +}; + +export default MetadataLink; diff --git a/src/css/common.css b/src/css/common.css index dcc861ed4..039a3c06c 100644 --- a/src/css/common.css +++ b/src/css/common.css @@ -204,7 +204,8 @@ input:autofill { background-position: center; background-size: cover; - &.anidb { + /* stylelint-disable-next-line selector-class-pattern */ + &.AniDB { background-image: url('/images/community/24-AniDB.png'); } diff --git a/src/pages/collection/Series.tsx b/src/pages/collection/Series.tsx index 06efd60e2..64f2ebdd0 100644 --- a/src/pages/collection/Series.tsx +++ b/src/pages/collection/Series.tsx @@ -3,17 +3,13 @@ import { Outlet, useParams } from 'react-router'; import { Link, NavLink, useOutletContext } from 'react-router-dom'; import { mdiAccountGroupOutline, - mdiAlertCircleOutline, - mdiCalendarMonthOutline, mdiChevronRight, - mdiEyeOutline, mdiFileDocumentMultipleOutline, mdiFilmstrip, mdiImageMultipleOutline, mdiInformationOutline, mdiPencilCircleOutline, mdiTagTextOutline, - mdiTelevision, } from '@mdi/js'; import { Icon } from '@mdi/react'; import cx from 'classnames'; @@ -22,28 +18,19 @@ import { get, isArray } from 'lodash'; import BackgroundImagePlaceholderDiv from '@/components/BackgroundImagePlaceholderDiv'; import AnidbDescription from '@/components/Collection/AnidbDescription'; import EditSeriesModal from '@/components/Collection/Series/EditSeriesModal'; +import SeriesInfo from '@/components/Collection/SeriesInfo'; import { useGetGroupQuery } from '@/core/rtkQuery/splitV3Api/collectionApi'; import { useGetSeriesImagesQuery, useGetSeriesQuery, useGetSeriesTagsQuery, } from '@/core/rtkQuery/splitV3Api/seriesApi'; -import { dayjs, formatThousand } from '@/core/util'; import useMainPoster from '@/hooks/useMainPoster'; import type { CollectionGroupType } from '@/core/types/api/collection'; import type { SeriesDetailsType } from '@/core/types/api/series'; import type { TagType } from '@/core/types/api/tags'; -const IconNotification = ({ text }) => ( -
- -
- {text} -
-
-); - const SeriesTab = ({ icon, text, to }) => ( { setFanartUri(`/api/v3/Image/${randomImage.Source}/${randomImage.Type}/${randomImage.ID}`); }, [images, imagesData]); - const [airDate, endDate, isSeriesOngoing] = useMemo(() => { - const tempAirDate = dayjs(series.AniDB?.AirDate); - const tempEndDate = dayjs(series.AniDB?.EndDate); - return [tempAirDate, tempEndDate, series.AniDB?.EndDate ? tempEndDate.isAfter(dayjs()) : true]; - }, [series]); - if (!seriesId || !seriesData.isSuccess) return null; return ( <>
-
-
-
-
- Entire Collection - - {group.Size > 1 && ( - <> - - {group.Name} - - - - )} -
-
- {isSeriesOngoing && } - {/* TODO: Check whether new files are added */} - {/* */} -
-
-
-
{series.Name}
-
- {series.AniDB?.Titles.find(title => title.Type === 'Main')?.Name} -
-
-
- - {series?.AniDB?.Type} -
-
- - - {airDate.format('MMMM Do, YYYY')} - {!airDate.isSame(endDate) && ( - <> -  -  - {endDate.toString() === 'Invalid Date' ? 'Current' : endDate.format('MMMM Do, YYYY')} - - )} - +
+
+ +
+
+
+ + Entire Collection + + + {group.Size > 1 && ( + <> + + {group.Name} + + + + )}
-
- -
-
- EP: - {formatThousand(series.Sizes.Local.Episodes)} - / - {formatThousand(series.Sizes.Total.Episodes)} -
- {series.Sizes.Total.Specials !== 0 && ( - <> - | -
- SP: - {formatThousand(series.Sizes.Local.Specials)} - / - {formatThousand(series.Sizes.Total.Specials)} -
- - )} +
+
+
+
{series.Name}
+
+ Original Title:  + {series.AniDB?.Titles.find(title => title.Type === 'Main')?.Name}
-
-
- -
-
- EP: - {formatThousand(series.Sizes.Watched.Episodes)} - / - {formatThousand(series.Sizes.Local.Episodes)} -
- {series.Sizes.Total.Specials !== 0 && ( - <> - | -
- SP: - {formatThousand(series.Sizes.Watched.Specials)} - / - {formatThousand(series.Sizes.Local.Specials)} -
- - )} +
+ {tags.slice(0, 13).map(tag => )} + + +
- {(series.Sizes.Total.Episodes - series.Sizes.Local.Episodes !== 0 - || series.Sizes.Total.Specials - series.Sizes.Local.Specials !== 0) && ( -
- - {series.Sizes.Total.Episodes - series.Sizes.Local.Episodes !== 0 && ( -
-
- EP: - {formatThousand(series.Sizes.Total.Episodes - series.Sizes.Local.Episodes)} -
-
- )} - {series.Sizes.Total.Episodes - series.Sizes.Local.Episodes !== 0 && |} - {series.Sizes.Total.Specials - series.Sizes.Local.Specials !== 0 && ( -
- SP: - {formatThousand(series.Sizes.Total.Specials - series.Sizes.Local.Specials)} -
- )} -
- )} -
-
- {tags.slice(0, 7).map(tag => )} +
-
- +
@@ -243,7 +147,7 @@ const Series = () => { onClick={() => setShowEditSeriesModal(true)} > -  Edit +  Edit Series
diff --git a/src/pages/collection/series/SeriesOverview.tsx b/src/pages/collection/series/SeriesOverview.tsx index 0d40ab926..bdc8231cd 100644 --- a/src/pages/collection/series/SeriesOverview.tsx +++ b/src/pages/collection/series/SeriesOverview.tsx @@ -1,20 +1,12 @@ import React, { useMemo } from 'react'; import { useParams } from 'react-router'; import { Link } from 'react-router-dom'; -import { - mdiChevronRight, - mdiCloseCircleOutline, - mdiOpenInNew, - mdiPencilCircleOutline, - mdiPlusCircleOutline, -} from '@mdi/js'; -import { Icon } from '@mdi/react'; import cx from 'classnames'; import { get, round, toNumber } from 'lodash'; import BackgroundImagePlaceholderDiv from '@/components/BackgroundImagePlaceholderDiv'; import EpisodeDetails from '@/components/Collection/Series/EpisodeDetails'; -import Button from '@/components/Input/Button'; +import SeriesMetadata from '@/components/Collection/SeriesMetadata'; import ShokoPanel from '@/components/Panels/ShokoPanel'; import { useGetAniDBRelatedQuery, @@ -22,15 +14,10 @@ import { useGetSeriesQuery, useNextUpEpisodeQuery, } from '@/core/rtkQuery/splitV3Api/seriesApi'; -import { useGetSeriesOverviewQuery } from '@/core/rtkQuery/splitV3Api/webuiApi'; -import { dayjs } from '@/core/util'; import useEpisodeThumbnail from '@/hooks/useEpisodeThumbnail'; import type { EpisodeType } from '@/core/types/api/episode'; import type { SeriesAniDBRelatedType, SeriesAniDBSimilarType } from '@/core/types/api/series'; -import type { WebuiSeriesDetailsType } from '@/core/types/api/webui'; - -const links = ['TMDB', 'TvDB', 'MAL', 'AniList', 'TraktTv']; const NextUpEpisode = ({ nextUpEpisode }: { nextUpEpisode: EpisodeType }) => { const thumbnail = useEpisodeThumbnail(nextUpEpisode); @@ -45,76 +32,11 @@ const NextUpEpisode = ({ nextUpEpisode }: { nextUpEpisode: EpisodeType }) => { ); }; -const MetadataLink = ({ id, series, site }: { site: string, id: number | number[], series: string }) => { - const linkId = Array.isArray(id) ? id[0] : id; - - const siteLink = useMemo(() => { - switch (site) { - case 'TMDB': - return `https://www.themoviedb.org/movie/${linkId}`; - case 'TvDB': - // TODO: Figure how to get tvdb series link using ID - return '#'; - case 'MAL': - return `https://myanimelist.net/anime/${linkId}`; - case 'AniList': - return `https://anilist.co/anime/${linkId}`; - case 'TraktTv': - // TODO: Figure how to get trakt series link using ID - return '#'; - default: - return '#'; - } - }, [linkId, site]); - - return ( -
-
- -
- {linkId - ? ( - <> - - - - ) - : ( - - )} -
-
- ); -}; - const SeriesOverview = () => { const { seriesId } = useParams(); const seriesData = useGetSeriesQuery({ seriesId: seriesId!, includeDataFrom: ['AniDB'] }, { skip: !seriesId }); const series = useMemo(() => seriesData?.data, [seriesData]); - const seriesOverviewData = useGetSeriesOverviewQuery({ SeriesID: seriesId! }, { skip: !seriesId }); - const overview = seriesOverviewData?.data || {} as WebuiSeriesDetailsType; const nextUpEpisodeData = useNextUpEpisodeQuery({ seriesId: toNumber(seriesId) }); const nextUpEpisode: EpisodeType = nextUpEpisodeData?.data ?? {} as EpisodeType; const relatedData = useGetAniDBRelatedQuery({ seriesId: seriesId! }, { skip: !seriesId }); @@ -122,132 +44,33 @@ const SeriesOverview = () => { const similarData = useGetAniDBSimilarQuery({ seriesId: seriesId! }, { skip: !seriesId }); const similar: SeriesAniDBSimilarType[] = similarData?.data ?? [] as SeriesAniDBSimilarType[]; - const jpOfficialSite = useMemo(() => series?.Links.find(link => link.Name === 'Official Site (JP)'), [series]); - const enOfficialSite = useMemo(() => series?.Links.find(link => link.Name === 'Official Site (EN)'), [series]); + // Links + const metadataLinks = ['AniDB', 'TMDB', 'TvDB', 'TraktTv']; if (!seriesId || !series) return null; return ( <>
- -
-
Source
- {overview.SourceMaterial} -
- -
-
Episodes
-
- {series.Sizes.Total.Episodes} -  Episodes -
-
- {series.Sizes.Total.Specials} -  Specials -
-
- -
-
Length
- {/* TODO: Get episode length */} -
-- Minutes/Episode
-
- -
-
Status
- {/* TODO: Check if there are more status types */} - {(series.AniDB?.EndDate && dayjs(series.AniDB.EndDate).isAfter(dayjs())) ? 'Ongoing' : 'Finished'} -
- -
-
Season
- {overview?.FirstAirSeason - ? ( - - {overview.FirstAirSeason.Name} - - ) - : '--'} -
- -
-
Studio
-
{overview?.Studios?.[0] ? overview?.Studios?.[0].Name : 'Studio Not Listed'}
-
- - {/*
*/} - {/*
Producers
*/} - {/* {overview?.Producers?.map(item =>
{item.Name}
)} */} - {/*
*/} - -
-
Links
- {/* TODO: Only showing links with Official JP and EN sites for now. To be changed */} - {jpOfficialSite && ( - - )} - {enOfficialSite && ( - - )} -
-
- -
- +
+ {get(nextUpEpisode, 'Name', false) ? :
No Episode Data Available!
}
- Metadata Sites - - - - } - className="flex grow-0" + title="Metadata Sites" + className="flex w-full max-w-[42.188rem] grow-0" transparent > -
- {links.map(site => ( +
+ {metadataLinks.map(site => (
- +
))}