Files
suwayomi-material-you-webui/src/components/MangaCard.tsx
T

394 lines
20 KiB
TypeScript
Raw Normal View History

/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
2021-01-26 23:32:12 +03:30
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
2021-01-26 23:32:12 +03:30
2021-09-09 17:51:22 +04:30
import Card from '@mui/material/Card';
import CardActionArea from '@mui/material/CardActionArea';
import Typography from '@mui/material/Typography';
2021-01-19 20:20:28 +03:30
import { Link } from 'react-router-dom';
2023-12-19 15:17:00 +01:00
import { Avatar, Box, CardContent, Stack, styled, Tooltip } from '@mui/material';
2023-03-23 13:59:32 +01:00
import { useTranslation } from 'react-i18next';
2023-12-25 21:37:45 +01:00
import React from 'react';
import PopupState, { bindMenu } from 'material-ui-popup-state';
2023-10-28 00:32:02 +02:00
import { requestManager } from '@/lib/requests/RequestManager.ts';
2023-06-05 01:42:39 +02:00
import { GridLayout, useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
2023-10-28 00:32:02 +02:00
import { SpinnerImage } from '@/components/util/SpinnerImage';
2023-12-25 21:37:45 +01:00
import { TManga, TPartialManga } from '@/typings.ts';
2023-12-19 15:17:00 +01:00
import { ContinueReadingButton } from '@/components/manga/ContinueReadingButton.tsx';
2023-12-25 21:37:45 +01:00
import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts';
import { MangaOptionButton } from '@/components/manga/MangaOptionButton.tsx';
import { MangaActionMenu } from '@/components/manga/MangaActionMenu.tsx';
2020-12-25 17:42:22 +01:00
2021-10-31 21:02:45 +03:30
const BottomGradient = styled('div')({
position: 'absolute',
bottom: 0,
width: '100%',
height: '30%',
background: 'linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 100%)',
});
2021-05-28 19:37:26 +04:30
2021-10-31 21:02:45 +03:30
const BottomGradientDoubledDown = styled('div')({
position: 'absolute',
bottom: 0,
width: '100%',
height: '20%',
background: 'linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 100%)',
});
const MangaTitle = styled(Typography)({
2023-06-17 16:50:17 +02:00
lineHeight: '1.5rem',
maxHeight: '3rem',
display: '-webkit-box',
WebkitLineClamp: '2',
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
textOverflow: 'ellipsis',
});
const GridMangaTitle = styled(MangaTitle)({
2021-10-31 21:02:45 +03:30
fontSize: '1.05rem',
});
const BadgeContainer = styled('div')({
display: 'flex',
height: 'fit-content',
2021-10-31 21:02:45 +03:30
borderRadius: '5px',
overflow: 'hidden',
'& p': {
color: 'white',
padding: '0.1em',
paddingInline: '0.2em',
fontSize: '1.05rem',
},
});
2020-12-25 17:42:22 +01:00
interface IProps {
2023-10-15 16:03:08 +02:00
manga: TPartialManga;
2023-02-06 10:06:33 +01:00
gridLayout?: GridLayout;
inLibraryIndicator?: boolean;
2023-12-25 21:37:45 +01:00
selected?: boolean | null;
handleSelection?: SelectableCollectionReturnType<TManga['id']>['handleSelection'];
2020-12-25 17:42:22 +01:00
}
2023-02-06 10:06:33 +01:00
2023-10-28 00:32:02 +02:00
export const MangaCard = (props: IProps) => {
2023-03-23 13:59:32 +01:00
const { t } = useTranslation();
2023-12-25 21:37:45 +01:00
const { manga, gridLayout, inLibraryIndicator, selected, handleSelection } = props;
2020-12-25 17:42:22 +01:00
const {
2023-12-25 21:37:45 +01:00
id,
title,
thumbnailUrl: tmpThumbnailUrl,
downloadCount,
unreadCount: unread,
inLibrary,
lastReadChapter,
chapters,
} = manga;
2023-09-06 21:47:13 +02:00
const thumbnailUrl = tmpThumbnailUrl ?? 'nonExistingMangaUrl';
2023-02-06 10:06:33 +01:00
const {
2023-12-19 15:17:00 +01:00
options: { showContinueReadingButton, showUnreadBadge, showDownloadBadge },
2023-02-06 10:06:33 +01:00
} = useLibraryOptionsContext();
2021-10-31 21:02:45 +03:30
2023-06-08 14:25:24 +02:00
const mangaLinkTo = `/manga/${id}/`;
2022-11-21 01:33:45 +01:00
2023-12-19 15:17:00 +01:00
const nextChapterIndexToRead = (lastReadChapter?.sourceOrder ?? 0) + 1;
const isLatestChapterRead = chapters?.totalCount === lastReadChapter?.sourceOrder;
if (gridLayout !== GridLayout.List) {
2022-03-06 21:03:15 +00:00
return (
2023-12-25 21:37:45 +01:00
<PopupState variant="popover" popupId="manga-card-action-menu">
{(popupState) => (
<>
<Link
onClick={(e) => {
if (selected === null) {
return;
}
e.preventDefault();
handleSelection?.(id, !selected);
2021-10-31 21:02:45 +03:30
}}
2023-12-25 21:37:45 +01:00
to={mangaLinkTo}
style={gridLayout === GridLayout.Comfortable ? { textDecoration: 'none' } : {}}
2022-03-06 16:45:25 +00:00
>
2023-12-25 21:37:45 +01:00
<Box
2022-03-06 21:03:15 +00:00
sx={{
2023-12-25 21:37:45 +01:00
display: 'flex',
flexDirection: 'column',
margin: '2px',
outline: selected ? '4px solid' : undefined,
borderRadius: selected ? '1px' : undefined,
outlineColor: (theme) => theme.palette.primary.main,
backgroundColor: (theme) => (selected ? theme.palette.primary.main : undefined),
'@media (hover: hover) and (pointer: fine)': {
'&:hover .manga-option-button': {
visibility: 'visible',
pointerEvents: 'all',
},
},
2022-03-06 16:45:25 +00:00
}}
2022-03-06 21:03:15 +00:00
>
2023-12-25 21:37:45 +01:00
<Card
2023-12-19 15:17:00 +01:00
sx={{
2023-12-25 21:37:45 +01:00
// force standard aspect ratio of manga covers
aspectRatio: '225/350',
display: 'flex',
2023-12-19 15:17:00 +01:00
}}
>
2023-12-25 21:37:45 +01:00
<CardActionArea
sx={{
position: 'relative',
height: '100%',
}}
>
<Stack
alignItems="start"
justifyContent="space-between"
direction="row"
sx={{
position: 'absolute',
top: 5,
left: 5,
right: 5,
}}
>
<BadgeContainer>
{inLibraryIndicator && inLibrary && (
<Typography sx={{ backgroundColor: 'primary.dark', zIndex: '1' }}>
{t('manga.button.in_library')}
</Typography>
)}
{showUnreadBadge && (unread ?? 0) > 0 && (
<Typography sx={{ backgroundColor: 'primary.dark' }}>
{unread}
</Typography>
)}
{showDownloadBadge && (downloadCount ?? 0) > 0 && (
<Typography
sx={{
backgroundColor: 'success.dark',
}}
>
{downloadCount}
</Typography>
)}
</BadgeContainer>
<MangaOptionButton
popupState={popupState}
id={id}
selected={selected}
handleSelection={handleSelection}
/>
</Stack>
<SpinnerImage
alt={title}
src={requestManager.getValidImgUrlFor(thumbnailUrl)}
imgStyle={
inLibraryIndicator && inLibrary
? {
height: '100%',
width: '100%',
objectFit: 'cover',
filter: 'brightness(0.4)',
}
: {
height: '100%',
width: '100%',
objectFit: 'cover',
}
}
spinnerStyle={{
display: 'grid',
placeItems: 'center',
}}
/>
<>
{gridLayout !== GridLayout.Comfortable && (
<>
<BottomGradient />
<BottomGradientDoubledDown />
</>
)}
<Stack
direction="row"
justifyContent={
gridLayout !== GridLayout.Comfortable ? 'space-between' : 'end'
}
alignItems="end"
2023-12-19 15:17:00 +01:00
sx={{
2023-12-25 21:37:45 +01:00
position: 'absolute',
bottom: 0,
width: '100%',
margin: '0.5em 0',
padding: '0 0.5em',
gap: '0.5em',
2023-12-19 15:17:00 +01:00
}}
>
2023-12-25 21:37:45 +01:00
{gridLayout !== GridLayout.Comfortable && (
<Tooltip title={title} placement="top">
<GridMangaTitle
sx={{
color: 'white',
textShadow: '0px 0px 3px #000000',
}}
>
{title}
</GridMangaTitle>
</Tooltip>
)}
<ContinueReadingButton
showContinueReadingButton={showContinueReadingButton}
isLatestChapterRead={isLatestChapterRead}
nextChapterIndexToRead={nextChapterIndexToRead}
mangaLinkTo={mangaLinkTo}
/>
</Stack>
</>
</CardActionArea>
</Card>
{gridLayout === GridLayout.Comfortable && (
<Tooltip title={title} placement="top">
<GridMangaTitle
sx={{
position: 'relative',
width: '100%',
bottom: 0,
margin: '0.5em 0',
padding: '0 0.5em',
color: 'text.primary',
height: '3rem',
}}
>
{title}
</GridMangaTitle>
</Tooltip>
)}
</Box>
</Link>
{!!handleSelection && popupState.isOpen && (
<MangaActionMenu
{...bindMenu(popupState)}
manga={manga as React.ComponentProps<typeof MangaActionMenu>['manga']}
handleSelection={handleSelection}
/>
)}
</>
)}
</PopupState>
);
}
return (
<PopupState variant="popover" popupId="manga-card-action-menu">
{(popupState) => (
<>
<Card>
<CardActionArea
component={Link}
to={mangaLinkTo}
onClick={(e) => {
if (selected === null) {
return;
}
e.preventDefault();
handleSelection?.(id, !selected);
}}
>
<CardContent
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: 2,
position: 'relative',
}}
>
<Avatar
variant="rounded"
sx={
inLibraryIndicator && inLibrary
? {
width: 56,
height: 56,
flex: '0 0 auto',
marginRight: 2,
imageRendering: 'pixelated',
filter: 'brightness(0.4)',
}
: {
width: 56,
height: 56,
flex: '0 0 auto',
marginRight: 2,
imageRendering: 'pixelated',
}
}
src={requestManager.getValidImgUrlFor(thumbnailUrl)}
/>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
flexGrow: 1,
width: 'min-content',
}}
>
<Tooltip title={title} placement="top">
<MangaTitle variant="h5">{title}</MangaTitle>
</Tooltip>
</Box>
<Stack direction="row" alignItems="center" gap="5px">
<BadgeContainer>
{inLibraryIndicator && inLibrary && (
<Typography sx={{ backgroundColor: 'primary.dark' }}>
{t('manga.button.in_library')}
</Typography>
)}
{showUnreadBadge && unread! > 0 && (
<Typography sx={{ backgroundColor: 'primary.dark' }}>{unread}</Typography>
)}
{showDownloadBadge && downloadCount! > 0 && (
<Typography
sx={{
backgroundColor: 'success.dark',
}}
>
{downloadCount}
</Typography>
)}
</BadgeContainer>
2023-12-19 15:17:00 +01:00
<ContinueReadingButton
showContinueReadingButton={showContinueReadingButton}
isLatestChapterRead={isLatestChapterRead}
nextChapterIndexToRead={nextChapterIndexToRead}
mangaLinkTo={mangaLinkTo}
/>
2023-12-25 21:37:45 +01:00
<MangaOptionButton
popupState={popupState}
id={id}
selected={selected}
handleSelection={handleSelection}
asCheckbox
/>
2023-12-19 15:17:00 +01:00
</Stack>
2023-12-25 21:37:45 +01:00
</CardContent>
2023-06-17 16:50:17 +02:00
</CardActionArea>
</Card>
2023-12-25 21:37:45 +01:00
{!!handleSelection && popupState.isOpen && (
<MangaActionMenu
{...bindMenu(popupState)}
manga={manga as React.ComponentProps<typeof MangaActionMenu>['manga']}
handleSelection={handleSelection}
2023-12-19 15:17:00 +01:00
/>
2023-12-25 21:37:45 +01:00
)}
</>
)}
</PopupState>
2020-12-25 17:42:22 +01:00
);
2023-06-17 16:50:17 +02:00
};