Skip to content

Commit

Permalink
Refactor: Use SeriesPoster component for all series posters outside o…
Browse files Browse the repository at this point in the history
…f main collection page (#1080)

* Fix message in BackgroundImagePlaceholderDiv

* FileInfo: fix overflowing subtitle languages

* Refactor: Use SeriesPoster component for all series posters outside of main collection page

* Fix IDE warnings
  • Loading branch information
harshithmohan authored Sep 21, 2024
1 parent 67b412c commit 36158f5
Show file tree
Hide file tree
Showing 14 changed files with 320 additions and 281 deletions.
2 changes: 1 addition & 1 deletion src/components/BackgroundImagePlaceholderDiv.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const BackgroundImagePlaceholderDiv = React.memo((props: Props) => {
setImageError(
imageSource === null
? (
'Image is not available. Run the validate image action or wait for the image queue to settle.'
'Image is not available. Run the validate image action or wait for the queue to settle.'
)
: (
'No image metadata.'
Expand Down
2 changes: 1 addition & 1 deletion src/components/Collection/Filter/DefaultCriteria.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const DefaultCriteria = ({ criteria }: Props) => {
const selectedCondition = useSelector(
(state: RootState) => {
const value: boolean | undefined = state.collection.filterConditions[criteria.Expression];
if (value === true || value === false) {
if (value !== undefined) {
return value ? '1' : '0';
}
return value;
Expand Down
2 changes: 1 addition & 1 deletion src/components/Panels/ShokoPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const ShokoPanel = (
<div
className={cx(
'flex grow flex-col shoko-scrollbar',
disableOverflow === false && 'overflow-y-auto',
!disableOverflow && 'overflow-y-auto',
contentClassName,
)}
style={{ overflowAnchor: 'none' }} // To fix scroll jumping around randomly when queue items change
Expand Down
108 changes: 108 additions & 0 deletions src/components/SeriesPoster.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { useMemo } from 'react';
import { Link } from 'react-router-dom';
import cx from 'classnames';

import BackgroundImagePlaceholderDiv from '@/components/BackgroundImagePlaceholderDiv';

import type { ImageType } from '@/core/types/api/common';

type Props = {
children?: React.ReactNode;
title: string;
subtitle?: string;
image: ImageType | null;
shokoId?: number | null;
anidbSeriesId?: number;
anidbEpisodeId?: number;
inCollection?: boolean;
};

const baseClassName = 'w-56 flex flex-col shrink-0';

const SeriesPoster = React.memo((props: Props) => {
const {
anidbEpisodeId,
anidbSeriesId,
children,
image,
inCollection,
shokoId,
subtitle,
title,
} = props;

const isAnidb = useMemo(() => {
if (shokoId) return false;
return anidbSeriesId !== undefined || anidbEpisodeId !== undefined;
}, [anidbEpisodeId, anidbSeriesId, shokoId]);

const content = (
<>
<BackgroundImagePlaceholderDiv
image={image}
className="h-80 rounded-lg border border-panel-border drop-shadow-md"
hidePlaceholderOnHover
overlayOnHover
zoomOnHover
>
{isAnidb && (
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-y-3 text-sm font-semibold opacity-0 transition-opacity group-hover:opacity-100">
<div className="metadata-link-icon AniDB" />
View on AniDB
</div>
)}

{children}

{inCollection && (
<div className="absolute bottom-4 left-3 flex w-[90%] justify-center rounded-lg bg-panel-background-overlay py-2 text-sm font-semibold text-panel-text opacity-100 transition-opacity group-hover:opacity-0">
In Collection
</div>
)}
</BackgroundImagePlaceholderDiv>

<div
className="mt-3 truncate text-center text-sm font-semibold"
data-tooltip-id="tooltip"
data-tooltip-content={title}
data-tooltip-delay-show={500}
>
{title}
</div>

{subtitle && (
<div
className="truncate text-center text-sm font-semibold opacity-65"
title={subtitle}
>
{subtitle}
</div>
)}
</>
);

if (shokoId) {
return (
<Link className={cx(baseClassName, 'group')} to={`/webui/collection/series/${shokoId}`}>
{content}
</Link>
);
}

if (anidbSeriesId ?? anidbEpisodeId) {
return (
<a
href={`https://anidb.net/${anidbSeriesId ? `anime/${anidbSeriesId}` : `episode/${anidbEpisodeId}`}`}
className={cx(baseClassName, 'group')}
target="_blank"
rel="noopener noreferrer"
>
{content}
</a>
);
}

return <div className={baseClassName}>{content}</div>;
});

export default SeriesPoster;
1 change: 1 addition & 0 deletions src/core/types/api/series.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export type SeriesIDsType = {

export type AniDBSeriesType = {
ID: number;
ShokoID?: number;
Type: SeriesTypeEnum;
Restricted: boolean;
Title: string;
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useMediaInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const getAudioInfo = (file: FileType) => {

const subtitleLanguages = map(file.MediaInfo?.Subtitles, item => item.LanguageCode).filter(item => !!item);
if (subtitleLanguages && subtitleLanguages.length > 0) {
info.push(`${subtitleLanguages.length > 1 ? 'Multi Subs' : 'Subs'} (${subtitleLanguages.join(',')})`);
info.push(`${subtitleLanguages.length > 1 ? 'Multi Subs' : 'Subs'} (${subtitleLanguages.join(', ')})`);
}

return info;
Expand Down
110 changes: 37 additions & 73 deletions src/pages/collection/series/SeriesOverview.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import React, { useMemo, useState } from 'react';
import { useParams } from 'react-router';
import { Link } from 'react-router-dom';
import { mdiEarth, mdiOpenInNew } from '@mdi/js';
import { Icon } from '@mdi/react';
import cx from 'classnames';
import { flatMap, get, round, toNumber } from 'lodash';
import { flatMap, get, map, round, toNumber } from 'lodash';

import BackgroundImagePlaceholderDiv from '@/components/BackgroundImagePlaceholderDiv';
import CharacterImage from '@/components/CharacterImage';
import EpisodeSummary from '@/components/Collection/Episode/EpisodeSummary';
import SeriesMetadata from '@/components/Collection/SeriesMetadata';
import MultiStateButton from '@/components/Input/MultiStateButton';
import ShokoPanel from '@/components/Panels/ShokoPanel';
import SeriesPoster from '@/components/SeriesPoster';
import {
useRelatedAnimeQuery,
useSeriesCastQuery,
Expand Down Expand Up @@ -169,82 +168,47 @@ const SeriesOverview = () => {
</div>

{relatedAnime.length > 0 && (
<ShokoPanel title="Related Anime" className="w-full" transparent>
<div className={cx('flex gap-x-5', relatedAnime.length > 5 && ('mb-4'))}>
{relatedAnime.map((item) => {
const thumbnail = get(item, 'Poster', null);
const itemRelation = item.Relation.replace(/([a-z])([A-Z])/g, '$1 $2');
return (
<Link
key={item.ID}
to={`/webui/collection/series/${item.ShokoID}`}
className={cx(
'flex w-[13.875rem] shrink-0 flex-col gap-y-2 text-center font-semibold',
!item.ShokoID && 'pointer-events-none',
)}
>
<BackgroundImagePlaceholderDiv
image={thumbnail}
className="group h-[19.875rem] w-[13.875rem] rounded-lg border border-panel-border drop-shadow-md"
hidePlaceholderOnHover
overlayOnHover
zoomOnHover
>
{item.ShokoID && (
<div className="absolute bottom-4 left-3 flex w-[90%] justify-center rounded-lg bg-panel-background-overlay py-2 text-sm font-semibold text-panel-text opacity-100 transition-opacity group-hover:opacity-0">
In Collection
</div>
)}
</BackgroundImagePlaceholderDiv>
<span className="line-clamp-1 text-ellipsis text-sm">{item.Title}</span>
<span className="text-sm text-panel-text-important">{itemRelation}</span>
</Link>
);
})}
</div>
<ShokoPanel
title="Related Anime"
className="w-full"
transparent
contentClassName={cx('!flex-row gap-x-6', relatedAnime.length > 7 && 'pb-4')}
>
{map(relatedAnime, item => (
<SeriesPoster
key={item.ID}
image={item.Poster}
title={item.Title}
subtitle={item.Relation.replace(/([a-z])([A-Z])/g, '$1 $2')}
shokoId={item.ShokoID}
anidbSeriesId={item.ID}
inCollection={!!item.ShokoID}
/>
))}
</ShokoPanel>
)}

{similarAnime.length > 0 && (
<ShokoPanel title="Similar Anime" className="w-full" transparent>
<div className={cx('shoko-scrollbar flex gap-x-5', similarAnime.length > 5 && ('mb-4'))}>
{similarAnime.map((item) => {
const thumbnail = get(item, 'Poster', null);
return (
<Link
key={item.ID}
to={`/webui/collection/series/${item.ShokoID}`}
className={cx(
'flex w-[13.875rem] shrink-0 flex-col gap-y-2 text-center font-semibold',
!item.ShokoID && 'pointer-events-none',
)}
>
<BackgroundImagePlaceholderDiv
image={thumbnail}
className="group h-[19.875rem] w-[13.875rem] rounded-lg border border-panel-border drop-shadow-md"
hidePlaceholderOnHover
overlayOnHover
zoomOnHover
>
{item.ShokoID && (
<div className="absolute bottom-4 left-3 flex w-[90%] justify-center rounded-lg bg-panel-background-overlay py-2 text-sm font-semibold text-panel-text opacity-100 transition-opacity group-hover:opacity-0">
In Collection
</div>
)}
</BackgroundImagePlaceholderDiv>
<span className="line-clamp-1 text-ellipsis text-sm">{item.Title}</span>
<span className="text-sm text-panel-text-important">
{round(item.UserApproval.Value, 2)}
% (
{item.UserApproval.Votes}
&nbsp;votes)
</span>
</Link>
);
})}
</div>
<ShokoPanel
title="Similar Anime"
className="w-full"
transparent
contentClassName={cx('!flex-row gap-x-6', similarAnime.length > 7 && 'pb-4')}
>
{map(similarAnime, item => (
<SeriesPoster
key={item.ID}
image={item.Poster}
title={item.Title}
subtitle={`${round(item.UserApproval.Value, 2)}% (${item.UserApproval.Votes} votes)`}
shokoId={item.ShokoID}
anidbSeriesId={item.ID}
inCollection={!!item.ShokoID}
/>
))}
</ShokoPanel>
)}

<ShokoPanel title="Top 20 Seiyuu" className="w-full" transparent>
<div className="z-10 flex w-full gap-x-6">
{cast?.filter(credit => credit.RoleName === 'Seiyuu' && credit.Character).slice(0, 20).map(seiyuu => (
Expand Down
78 changes: 21 additions & 57 deletions src/pages/dashboard/components/EpisodeDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import React, { useMemo } from 'react';
import { Link } from 'react-router-dom';
import cx from 'classnames';

import BackgroundImagePlaceholderDiv from '@/components/BackgroundImagePlaceholderDiv';
import SeriesPoster from '@/components/SeriesPoster';
import { EpisodeTypeEnum } from '@/core/types/api/episode';
import { convertTimeSpanToMs, dayjs } from '@/core/util';

Expand All @@ -23,41 +21,6 @@ const CalendarConfig = {
sameElse: 'dddd',
};

const DateSection: React.FC<{ airDate: dayjs.Dayjs, relativeTime: string }> = ({ airDate, relativeTime }) => (
<div>
<p className="truncate text-center text-sm font-semibold">{airDate.format('MMMM Do, YYYY')}</p>
<p className="truncate text-center text-sm font-semibold opacity-65">{relativeTime}</p>
</div>
);

const ImageSection: React.FC<
{ episode: DashboardEpisodeDetailsType, percentage: string | null, isInCollection: boolean }
> = ({ episode, isInCollection, percentage }) => (
<BackgroundImagePlaceholderDiv
image={episode.SeriesPoster}
className="h-80 rounded-lg border border-panel-border drop-shadow-md"
hidePlaceholderOnHover
overlayOnHover
zoomOnHover
>
{percentage && <div className="absolute bottom-0 left-0 h-1 bg-panel-text-primary" style={{ width: percentage }} />}
{isInCollection && (
<div className="absolute bottom-4 left-3 flex w-[90%] justify-center rounded-lg bg-panel-background-overlay py-2 text-sm font-semibold text-panel-text opacity-100 transition-opacity group-hover:opacity-0">
In Collection
</div>
)}
</BackgroundImagePlaceholderDiv>
);

const TitleSection: React.FC<{ episode: DashboardEpisodeDetailsType, title: string }> = ({ episode, title }) => (
<div>
<p className="truncate text-center text-sm font-semibold" title={episode.SeriesTitle}>
{episode.SeriesTitle}
</p>
<p className="truncate text-center text-sm font-semibold opacity-65" title={title}>{title}</p>
</div>
);

const anidbEpisodePrefixes = (type: EpisodeTypeEnum, epNumber: number): string => {
const fullPrefixes = (prefix: string) => `${prefix}${epNumber}`;
// Prefixes for episode types base on https://wiki.anidb.net/Content:Episodes#Type
Expand Down Expand Up @@ -95,29 +58,30 @@ function EpisodeDetails({ episode, isInCollection = false, showDate = false }: P
[episode.Type, episode.Title, episode.Number],
);

const content = (
<>
{showDate && <DateSection airDate={airDate} relativeTime={relativeTime} />}
<ImageSection episode={episode} percentage={percentage} isInCollection={isInCollection} />
<TitleSection episode={episode} title={title} />
</>
);

return (
<div
key={`episode-${episode.IDs.ID}`}
className={cx(
'mr-6 flex w-56 shrink-0 flex-col justify-center gap-y-3 last:mr-0',
episode.IDs.ShokoSeries && 'group',
)}
className="flex w-56 shrink-0 flex-col gap-y-3"
>
{episode.IDs.ShokoSeries
? (
<Link className="flex flex-col gap-y-3" to={`/webui/collection/series/${episode.IDs.ShokoSeries}`}>
{content}
</Link>
)
: content}
{showDate && (
<div>
<div className="truncate text-center text-sm font-semibold">{airDate.format('MMMM Do, YYYY')}</div>
<div className="truncate text-center text-sm font-semibold opacity-65">{relativeTime}</div>
</div>
)}

<SeriesPoster
image={episode.SeriesPoster}
title={episode.SeriesTitle}
subtitle={title}
shokoId={episode.IDs.ShokoSeries}
anidbEpisodeId={episode.IDs.ID}
inCollection={isInCollection}
>
{percentage && (
<div className="absolute bottom-0 left-0 h-1 bg-panel-text-primary" style={{ width: percentage }} />
)}
</SeriesPoster>
</div>
);
}
Expand Down
Loading

0 comments on commit 36158f5

Please sign in to comment.