Skip to content

Commit

Permalink
QoL Improvements (ShokoAnime#654)
Browse files Browse the repository at this point in the history
* Add link to series on Recently Imported.

* Hide Group Link for Single Series.

* Fix Series listing.

* Update "In Collection" Design and Indicator Check.

* Use IDs.ShokoFile Instead of IDs.ID to Avoid Duplicate Key Issue When Importing Multiples.

* Fix TopNav Icon and Text Colors.

* Fix lint issues

* Remove `?` in Series.tsx

---------

Co-authored-by: Harshith Mohan <[email protected]>
  • Loading branch information
ElementalCrisis and harshithmohan authored Oct 24, 2023
1 parent 281f957 commit 66aafb0
Show file tree
Hide file tree
Showing 10 changed files with 104 additions and 66 deletions.
2 changes: 1 addition & 1 deletion src/components/BackgroundImagePlaceholderDiv.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function BackgroundImagePlaceholderDiv(props: Props) {
<div className={`${className} relative overflow-hidden`}>
<div
className={cx(
'absolute w-full h-full flex flex-col top-0 left-0 text-center z-[-1] rounded-md',
'absolute w-full h-full flex flex-col top-0 left-0 text-center z-[-1] rounded-lg',
zoomOnHover && 'group-hover:scale-105 transition-transform',
)}
style={{ background: backgroundImage ? `center / cover no-repeat url('${backgroundImage.src}')` : undefined }}
Expand Down
4 changes: 2 additions & 2 deletions src/components/Collection/ListViewItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,12 @@ const ListViewItem = ({ isSeries, isSidebarOpen, item, mainSeries }: Props) => {
<Icon
path={mdiPencilCircleOutline}
size="2rem"
className="text-panel-icon-important"
className="text-panel-icon"
/>
</Link>
</div>
{showGroupIndicator && groupCount > 1 && (
<div className="text-panel-text-transparent absolute bottom-0 left-0 flex w-full justify-center rounded-bl-md bg-panel-background-overlay py-1.5 text-sm font-semibold opacity-100 transition-opacity group-hover:opacity-0">
<div className="absolute bottom-0 left-0 flex w-full justify-center rounded-bl-md bg-panel-background-overlay py-1.5 text-sm font-semibold opacity-100 transition-opacity group-hover:opacity-0">
{groupCount}
&nbsp;Series
</div>
Expand Down
21 changes: 11 additions & 10 deletions src/components/Layout/TopNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,21 @@ const MenuItem = (
<NavLink
to={id}
key={id}
className={({ isActive }) => cx('flex items-center gap-x-2', (isActive || isHighlighted) && 'text-header-primary')}
className={({ isActive }) =>
cx('flex items-center gap-x-2', (isActive || isHighlighted) && 'text-topnav-icon-primary')}
onClick={(e) => {
e.preventDefault();
onClick();
}}
>
<Icon className="text-topnav-icon-primary" path={icon} size={0.8333} />
<Icon path={icon} size={0.8333} />
{text}
</NavLink>
);

const ExternalLinkMenuItem = ({ icon, url }: { url: string, icon: string }) => (
<a href={url} target="_blank" rel="noreferrer noopener">
<Icon className="text-topnav-icon-primary" path={icon} size={0.8333} />
<Icon className="text-topnav-icon" path={icon} size={0.8333} />
</a>
);

Expand Down Expand Up @@ -153,7 +154,7 @@ function TopNav() {
className={({ isActive }) => cx('flex items-center gap-x-2', isActive && 'text-topnav-text-primary')}
onClick={closeModalsAndSubmenus}
>
<Icon className="text-topnav-icon-primary" path={icon} size={0.8333} />
<Icon path={icon} size={0.8333} />
{text}
</NavLink>
), []);
Expand All @@ -170,16 +171,16 @@ function TopNav() {
<div className="mx-auto flex w-full max-w-[120rem] items-center justify-between px-8 py-6">
<div className="flex items-center gap-x-2">
<ShokoIcon className="w-6" />
<span className="mt-1 text-xl font-semibold">Shoko</span>
<span className="mt-1 text-xl font-semibold text-header-text">Shoko</span>
</div>
<div className="flex items-center gap-x-8">
<div className="flex items-center gap-x-2">
<div
className={cx(['cursor-pointer', showQueueModal ? 'text-topnav-text-primary' : undefined])}
className={cx(['cursor-pointer', showQueueModal ? 'text-header-icon-primary' : undefined])}
onClick={handleQueueModalOpen}
title="Show Queue Modal"
>
<Icon className="text-header-icon" path={mdiServer} size={0.8333} />
<Icon path={mdiServer} size={0.8333} />
</div>
<span className="text-header-text-important">
{(queueItems.HasherQueueState.queueCount + queueItems.GeneralQueueState.queueCount
Expand All @@ -192,15 +193,15 @@ function TopNav() {
? <img src={currentUser.data?.Avatar} alt="avatar" className="h-8 w-8 rounded-full" />
: currentUser.data?.Username.charAt(0)}
</div>
<span>{currentUser.data?.Username}</span>
<span className="text-header-text">{currentUser.data?.Username}</span>
<Icon path={mdiChevronDown} size={0.6666} />
</div>
<NavLink
to="settings"
className={({ isActive }) => (isActive ? 'text-topnav-text-primary' : '')}
className={({ isActive }) => (isActive ? 'text-header-icon-primary' : '')}
onClick={() => closeModalsAndSubmenus()}
>
<Icon className="text-header-icon" path={mdiCogOutline} size={0.8333} />
<Icon path={mdiCogOutline} size={0.8333} />
</NavLink>
</div>
</div>
Expand Down
4 changes: 3 additions & 1 deletion src/css/theme-shoko-gray.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
--button-secondary-text: #cfd8e3;
--header-background: #2c333e;
--header-icon: #cfd8e3;
--header-icon-primary: #44a3ff;
--header-text: #cfd8e3;
--header-text-important: #10c469;
--header-user-background: #44a3ff;
Expand Down Expand Up @@ -43,8 +44,9 @@
--slider-color-alt: #44a3ff;
--topnav-background: #353d4a;
--topnav-border: #21242b;
--topnav-icon: #cfd8e3;
--topnav-icon-important: #10c469;
--topnav-icon-primary: #cfd8e3;
--topnav-icon-primary: #44a3ff;
--topnav-icon-warning: #f9c851;
--topnav-text: #cfd8e3;
--topnav-text-primary: #44a3ff;
Expand Down
18 changes: 11 additions & 7 deletions src/pages/collection/Series.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,17 @@ const Series = () => {
<div className="flex gap-x-2">
<Link className="font-semibold text-panel-text-primary" to="/webui/collection">Entire Collection</Link>
<Icon path={mdiChevronRight} size={1} />
<Link
className="font-semibold text-panel-text-primary"
to={`/webui/collection/group/${series.IDs?.ParentGroup}`}
>
{group.Name}
</Link>
<Icon path={mdiChevronRight} size={1} />
{group.Size > 1 && (
<>
<Link
className="font-semibold text-panel-text-primary"
to={`/webui/collection/group/${series.IDs.ParentGroup}`}
>
{group.Name}
</Link>
<Icon path={mdiChevronRight} size={1} />
</>
)}
</div>
<div className="flex gap-x-3">
{isSeriesOngoing && <IconNotification text="Series is Ongoing" />}
Expand Down
96 changes: 59 additions & 37 deletions src/pages/dashboard/components/EpisodeDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import React, { useMemo } from 'react';
import { Link } from 'react-router-dom';
import { mdiLayersTripleOutline } from '@mdi/js';
import { Icon } from '@mdi/react';
import cx from 'classnames';
import moment from 'moment';

import BackgroundImagePlaceholderDiv from '@/components/BackgroundImagePlaceholderDiv';
import { EpisodeTypeEnum } from '@/core/types/api/episode';

import type { DashboardEpisodeDetailsType } from '@/core/types/api/dashboard';

type Props = {
episode: DashboardEpisodeDetailsType;
showDate?: boolean;
isInCollection?: boolean;
};

const CalendarConfig: moment.CalendarSpec = {
sameDay: '[Today]',
nextDay: '[Tomorrow]',
Expand All @@ -18,60 +23,77 @@ const CalendarConfig: moment.CalendarSpec = {
sameElse: 'dddd',
};

type Props = {
episode: DashboardEpisodeDetailsType;
showDate?: boolean;
isInCollection?: boolean;
};
const DateSection: React.FC<{ airDate: moment.Moment, relativeTime: string }> = ({ airDate, relativeTime }) => (
<>
<p className="truncate text-center text-sm font-semibold">{airDate.format('MMMM Do, YYYY')}</p>
<p className="mb-2 truncate text-center text-sm font-semibold opacity-65">{relativeTime}</p>
</>
);

const ImageSection: React.FC<
{ episode: DashboardEpisodeDetailsType, percentage: string | null, isInCollection: boolean }
> = ({ episode, isInCollection, percentage }) => (
<BackgroundImagePlaceholderDiv
image={episode.SeriesPoster}
className="mb-3 h-80 rounded-lg border border-panel-border drop-shadow-md"
hidePlaceholderOnHover
zoomOnHover
>
{percentage && <div className="absolute bottom-0 left-0 h-1 bg-panel-text-primary" style={{ width: percentage }} />}
{isInCollection && (
<div className="absolute bottom-0 left-0 flex w-full justify-center bg-panel-background-overlay py-1.5 text-sm font-semibold text-panel-text opacity-100 transition-opacity group-hover:opacity-0">
In Collection
</div>
)}
<div className="pointer-events-none z-50 flex h-full bg-panel-background-transparent p-3 opacity-0 group-hover:pointer-events-auto group-hover:opacity-100" />
</BackgroundImagePlaceholderDiv>
);

function EpisodeDetails(props: Props): React.ReactNode {
const { episode, isInCollection, showDate } = props;
const TitleSection: React.FC<{ episode: DashboardEpisodeDetailsType, title: string }> = ({ episode, title }) => (
<>
<p className="mb-1 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>
</>
);

function EpisodeDetails({ episode, isInCollection = false, showDate = false }: Props): React.ReactNode {
const percentage = useMemo(() => {
if (episode.ResumePosition == null) return null;
const duration = moment.duration(episode.Duration);
const resumePosition = moment.duration(episode.ResumePosition);
return `${((resumePosition.asMilliseconds() / duration.asMilliseconds()) * 100).toFixed(2)}%`;
}, [episode.Duration, episode.ResumePosition]);

const airDate = useMemo(() => moment(episode.AirDate), [episode.AirDate]);
const relativeTime = useMemo(() => airDate.calendar(CalendarConfig), [airDate]);
const title = useMemo(
() => `${episode.Type === EpisodeTypeEnum.Normal ? '' : episode.Type[0]}${episode.Number} - ${episode.Title}`,
[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 (
<Link
<div
key={`episode-${episode.IDs.ID}`}
className="group mr-4 flex w-56 shrink-0 flex-col justify-center last:mr-0"
to={`/webui/collection/series/${episode.IDs.ShokoSeries}/episodes`}
className={cx('mr-4 flex w-56 shrink-0 flex-col justify-center last:mr-0', episode.IDs.ShokoSeries && 'group')}
>
{showDate
{episode.IDs.ShokoSeries
? (
<>
<p className="truncate text-center text-sm font-semibold">{airDate.format('MMMM Do, YYYY')}</p>
<p className="mb-2 truncate text-center text-sm font-semibold opacity-65">{relativeTime}</p>
</>
<Link to={`/webui/collection/series/${episode.IDs.ShokoSeries}`}>
{content}
</Link>
)
: null}
<BackgroundImagePlaceholderDiv
image={episode.SeriesPoster}
className="mb-3 h-80 rounded border border-panel-border drop-shadow-md"
zoomOnHover
>
{percentage && (
<div className="absolute bottom-0 left-0 h-1 bg-panel-text-primary" style={{ width: percentage }} />
)}
{isInCollection && (
<div className="absolute right-3 top-3 rounded bg-panel-background-transparent p-1">
<Icon path={mdiLayersTripleOutline} size={0.75} title="Episode is Already in Collection!" />
</div>
)}
</BackgroundImagePlaceholderDiv>
<p className="mb-1 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>
</Link>
: content}
</div>
);
}

Expand Down
10 changes: 7 additions & 3 deletions src/pages/dashboard/components/SeriesDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ function SeriesDetails(props: { series: SeriesType }): JSX.Element {
>
<BackgroundImagePlaceholderDiv
image={mainPoster}
className="mb-3 h-80 rounded border border-panel-border drop-shadow-md"
className="mb-3 h-80 rounded-md border border-panel-border drop-shadow-md"
hidePlaceholderOnHover
zoomOnHover
/>
>
<div className="pointer-events-none z-50 flex h-full bg-panel-background-transparent p-3 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100" />
</BackgroundImagePlaceholderDiv>
<p className="mb-1 truncate text-center text-sm font-semibold" title={series.Name}>{series.Name}</p>
<p className="truncate text-center text-sm font-semibold opacity-65" title={`${series.Size} Files`}>
{series.Size}
&nbsp;Files
&nbsp;
{series.Size === 1 ? 'File' : 'Files'}
</p>
</Link>
);
Expand Down
2 changes: 1 addition & 1 deletion src/pages/dashboard/panels/RecentlyImported.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const RecentlyImported = () => {
<div className="shoko-scrollbar relative flex">
<TransitionDiv show={!showSeries} className="absolute flex">
{(episodes.data?.length ?? 0) > 0
? episodes.data?.map(item => <EpisodeDetails episode={item} key={item.IDs.ID} />)
? episodes.data?.map(item => <EpisodeDetails episode={item} key={item.IDs.ShokoFile} />)
: <div className="mt-4 flex w-full justify-center font-semibold">No Recently Imported Episodes!</div>}
</TransitionDiv>
<TransitionDiv show={showSeries} className="absolute flex">
Expand Down
11 changes: 7 additions & 4 deletions src/pages/dashboard/panels/UpcomingAnime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ const UpcomingAnime = () => {
<div className="shoko-scrollbar relative flex">
<TransitionDiv show={!showAll} className="absolute flex w-full">
{(localItems.data?.length ?? 0) > 0
? localItems.data?.map(item => (
<EpisodeDetails episode={item} showDate key={item.IDs.ID} isInCollection={item.IDs.ShokoFile !== null} />
))
? localItems.data?.map(item => <EpisodeDetails episode={item} showDate key={item.IDs.ID} />)
: (
<div className="mt-4 flex w-full flex-col justify-center gap-y-2 text-center">
<div>No Upcoming Anime.</div>
Expand All @@ -46,7 +44,12 @@ const UpcomingAnime = () => {
<TransitionDiv show={showAll} className="absolute flex w-full">
{(items.data?.length ?? 0) > 0
? items.data?.map(item => (
<EpisodeDetails episode={item} showDate key={item.IDs.ID} isInCollection={item.IDs.ShokoFile !== null} />
<EpisodeDetails
episode={item}
showDate
key={item.IDs.ID}
isInCollection={item.IDs.ShokoSeries !== null}
/>
))
: (
<div className="mt-4 flex w-full flex-col justify-center gap-y-2 text-center">
Expand Down
2 changes: 2 additions & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ module.exports = {
'button-secondary-text': 'var(--button-secondary-text)',
'header-background': 'var(--header-background)',
'header-icon': 'var(--header-icon)',
'header-icon-primary': 'var(--header-icon-primary)',
'header-text': 'var(--header-text)',
'header-text-important': 'var(--header-text-important)',
'header-user-background': 'var(--header-user-background)',
Expand Down Expand Up @@ -77,6 +78,7 @@ module.exports = {
'slider-color-alt': 'var(--slider-color-alt)',
'topnav-background': 'var(--topnav-background)',
'topnav-border': 'var(--topnav-border)',
'topnav-icon': 'var(--topnav-icon)',
'topnav-icon-important': 'var(--topnav-icon-important)',
'topnav-icon-primary': 'var(--topnav-icon-primary)',
'topnav-icon-warning': 'var(--topnav-icon-warning)',
Expand Down

0 comments on commit 66aafb0

Please sign in to comment.