Files
suwayomi-material-you-webui/src/components/MangaCard.tsx
schroda 58eb2fead3 Feature/enforce license notice in each file via eslint rule (#304)
* [ESLint] Add "eslint-plugin-header" as dev dependency

* [ESLint::eslint-plugin-header] Setup rule

* [ESLint::eslint-plugin-header] Fix errors
2023-05-18 13:17:41 +02:00

276 lines
11 KiB
TypeScript

/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* 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/.
*/
import React from 'react';
import Card from '@mui/material/Card';
import CardActionArea from '@mui/material/CardActionArea';
import Typography from '@mui/material/Typography';
import { Link } from 'react-router-dom';
import { Avatar, CardContent, Grid } from '@mui/material';
import useLocalStorage from 'util/useLocalStorage';
import SpinnerImage from 'components/util/SpinnerImage';
import { Box, styled } from '@mui/system';
import { GridLayout, useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
import { BACK } from 'util/useBackTo';
import { IMangaCard } from 'typings';
import { useTranslation } from 'react-i18next';
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%)',
});
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)({
position: 'absolute',
bottom: 0,
padding: '0.5em',
fontSize: '1.05rem',
});
const BadgeContainer = styled('div')({
display: 'flex',
height: 'fit-content',
borderRadius: '5px',
overflow: 'hidden',
'& p': {
color: 'white',
padding: '0.1em',
paddingInline: '0.2em',
fontSize: '1.05rem',
},
});
const truncateText = (str: string, maxLength: number) => {
const ending = '...';
// trim the string to the maximum length
const trimmedString = str.substr(0, maxLength - ending.length);
if (trimmedString.length < str.length) {
return trimmedString + ending;
}
return str;
};
interface IProps {
manga: IMangaCard;
gridLayout?: GridLayout;
dimensions: number;
inLibraryIndicator?: boolean;
}
const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref) => {
const { t } = useTranslation();
const {
manga: { id, title, thumbnailUrl, downloadCount, unreadCount: unread, inLibrary },
gridLayout,
dimensions,
inLibraryIndicator,
} = props;
const {
options: { showUnreadBadge, showDownloadBadge },
} = useLibraryOptionsContext();
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const [useCache] = useLocalStorage<boolean>('useCache', true);
const [ItemWidth] = useLocalStorage<number>('ItemWidth', 300);
const mangaLinkTo = { pathname: `/manga/${id}/`, state: { backLink: BACK } };
if (gridLayout !== GridLayout.List) {
const cols = Math.ceil(dimensions / ItemWidth);
return (
<Grid item columns={cols} xs={1}>
<Link to={mangaLinkTo} style={gridLayout === GridLayout.Comfortable ? { textDecoration: 'none' } : {}}>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
}}
>
<Card
sx={{
// force standard aspect ratio of manga covers
aspectRatio: '225/350',
display: 'flex',
}}
ref={ref}
>
<CardActionArea
sx={{
position: 'relative',
height: '100%',
}}
>
<BadgeContainer
sx={{
position: 'absolute',
top: 5,
left: 5,
}}
>
{inLibraryIndicator && inLibrary && (
<Typography sx={{ backgroundColor: 'primary.dark', zIndex: '1' }}>
{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>
<SpinnerImage
alt={title}
src={`${serverAddress}${thumbnailUrl}?useCache=${useCache}`}
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 />
<MangaTitle
sx={{
color: 'white',
textShadow: '0px 0px 3px #000000',
}}
title={title}
>
{truncateText(title, 61)}
</MangaTitle>
</>
)}
</CardActionArea>
</Card>
{gridLayout === GridLayout.Comfortable && (
<MangaTitle
sx={{
position: 'relative',
color: 'text.primary',
}}
title={title}
>
{truncateText(title, 61)}
</MangaTitle>
)}
</Box>
</Link>
</Grid>
);
}
return (
<Grid item xs={12}>
<Card>
<CardActionArea component={Link} to={mangaLinkTo}>
<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={`${serverAddress}${thumbnailUrl}?useCache=${useCache}`}
/>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
flexGrow: 1,
width: 'min-content',
}}
>
<Typography variant="h5" component="h2" title={title}>
{truncateText(title, 61)}
</Typography>
</Box>
<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>
</CardContent>
</CardActionArea>
</Card>
</Grid>
);
});
export default MangaCard;