Improve manga grid/list rendering

This commit is contained in:
schroda
2025-01-13 01:29:40 +01:00
parent 63a6cfcfb3
commit 74c61a5a29
6 changed files with 372 additions and 363 deletions

View File

@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
export type SelectableCollectionReturnType<Id extends number | string, Key extends string = string> = { export type SelectableCollectionReturnType<Id extends number | string, Key extends string = string> = {
selectedItemIds: Id[]; selectedItemIds: Id[];
@@ -52,73 +52,75 @@ export const useSelectableCollection = <Id extends number | string, Key extends
lastSelectedItemInfoRef.current = undefined; lastSelectedItemInfoRef.current = undefined;
} }
const handleSelection: SelectableCollectionReturnType<Id, Key>['handleSelection'] = ( const handleSelection: SelectableCollectionReturnType<Id, Key>['handleSelection'] = useCallback(
id, (id, selected, { selectRange = false, key = currentKey } = {}) => {
selected, const deselect = !selected;
{ selectRange = false, key = currentKey } = {},
) => {
const deselect = !selected;
const { id: lastSelectedItemId, key: lastSelectedItemIdKey } = lastSelectedItemInfoRef.current ?? {}; const { id: lastSelectedItemId, key: lastSelectedItemIdKey } = lastSelectedItemInfoRef.current ?? {};
lastSelectedItemInfoRef.current = { id, key }; lastSelectedItemInfoRef.current = { id, key };
const isSelectRange = selectRange && key === lastSelectedItemIdKey && lastSelectedItemId !== undefined; const isSelectRange = selectRange && key === lastSelectedItemIdKey && lastSelectedItemId !== undefined;
const indexOfLastSelectedItemId = isSelectRange ? itemIds.indexOf(lastSelectedItemId) : -1; const indexOfLastSelectedItemId = isSelectRange ? itemIds.indexOf(lastSelectedItemId) : -1;
const indexOfSelectedId = isSelectRange ? itemIds.indexOf(id) : -1; const indexOfSelectedId = isSelectRange ? itemIds.indexOf(id) : -1;
const selectedIds = isSelectRange const selectedIds = isSelectRange
? itemIds.slice( ? itemIds.slice(
Math.min(indexOfLastSelectedItemId, indexOfSelectedId), Math.min(indexOfLastSelectedItemId, indexOfSelectedId),
Math.max(indexOfLastSelectedItemId, indexOfSelectedId) + 1, Math.max(indexOfLastSelectedItemId, indexOfSelectedId) + 1,
) )
: [id]; : [id];
if (deselect) {
setKeyToSelectedItemIds((prevState) => ({
...prevState,
[key]: prevState[key]?.filter((selectedItemId) => !selectedIds.includes(selectedItemId)) ?? [],
}));
return;
}
if (deselect) {
setKeyToSelectedItemIds((prevState) => ({ setKeyToSelectedItemIds((prevState) => ({
...prevState, ...prevState,
[key]: prevState[key]?.filter((selectedItemId) => !selectedIds.includes(selectedItemId)) ?? [], [key]: [...new Set([...(prevState[key] ?? []), ...selectedIds])],
})); }));
return; },
} [currentKey, itemIds],
);
setKeyToSelectedItemIds((prevState) => ({ const handleSelectAll = useCallback(
...prevState, (selectAll: boolean, ids: Id[], key: Key = currentKey) => {
[key]: [...new Set([...(prevState[key] ?? []), ...selectedIds])], switch (selectAll) {
})); case true:
}; setKeyToSelectedItemIds((prevState) => ({
...prevState,
[key]: [...ids],
}));
break;
case false:
setKeyToSelectedItemIds((prevState) => ({
...prevState,
[key]: [],
}));
break;
default:
break;
}
},
[currentKey],
);
const handleSelectAll = (selectAll: boolean, ids: Id[], key: Key = currentKey) => { const setSelectionForKey = useCallback((key: Key, ids: Id[]) => {
switch (selectAll) {
case true:
setKeyToSelectedItemIds((prevState) => ({
...prevState,
[key]: [...ids],
}));
break;
case false:
setKeyToSelectedItemIds((prevState) => ({
...prevState,
[key]: [],
}));
break;
default:
break;
}
};
const setSelectionForKey = (key: Key, ids: Id[]) => {
setKeyToSelectedItemIds((prevState) => ({ setKeyToSelectedItemIds((prevState) => ({
...prevState, ...prevState,
[key]: [...ids], [key]: [...ids],
})); }));
}; }, []);
const getSelectionForKey = (key: Key) => keyToSelectedItemIds[key]; const getSelectionForKey = useCallback((key: Key) => keyToSelectedItemIds[key], [keyToSelectedItemIds]);
const clearSelection = () => { const clearSelection = useCallback(() => {
setKeyToSelectedItemIds({}); setKeyToSelectedItemIds({});
}; }, []);
return { return {
selectedItemIds, selectedItemIds,

View File

@@ -19,6 +19,8 @@ interface LibraryMangaGridProps
isLoading: boolean; isLoading: boolean;
} }
const loadMoreNoop = () => undefined;
export const LibraryMangaGrid: React.FC<LibraryMangaGridProps> = ({ export const LibraryMangaGrid: React.FC<LibraryMangaGridProps> = ({
showFilteredOutMessage, showFilteredOutMessage,
message, message,
@@ -41,7 +43,7 @@ export const LibraryMangaGrid: React.FC<LibraryMangaGridProps> = ({
gridWrapperProps={{ sx: { p: 1 } }} gridWrapperProps={{ sx: { p: 1 } }}
{...gridProps} {...gridProps}
hasNextPage={false} hasNextPage={false}
loadMore={() => undefined} loadMore={loadMoreNoop}
message={showFilteredOutMessage ? t('library.error.label.no_matches') : message} message={showFilteredOutMessage ? t('library.error.label.no_matches') : message}
messageExtra={showFilteredOutMessage ? undefined : messageExtra} messageExtra={showFilteredOutMessage ? undefined : messageExtra}
gridLayout={options.gridLayout} gridLayout={options.gridLayout}

View File

@@ -122,10 +122,13 @@ export function Library() {
currentKey: activeTab?.id.toString(), currentKey: activeTab?.id.toString(),
}); });
const handleSelect: typeof handleSelection = (id, selected, selectOptions) => { const handleSelect: typeof handleSelection = useCallback(
setIsSelectModeActive(!!(selectedItemIds.length + (selected ? 1 : -1))); (id, selected, selectOptions) => {
handleSelection(id, selected, selectOptions); setIsSelectModeActive(!!(selectedItemIds.length + (selected ? 1 : -1)));
}; handleSelection(id, selected, selectOptions);
},
[setIsSelectModeActive, handleSelection],
);
const selectedMangas = useMemo( const selectedMangas = useMemo(
() => () =>
@@ -212,7 +215,6 @@ export function Library() {
isSelectModeActive, isSelectModeActive,
areNoItemsSelected, areNoItemsSelected,
areAllItemsSelected, areAllItemsSelected,
selectedItemIds.length,
mangas.length, mangas.length,
activeTab, activeTab,
showTabSize, showTabSize,

View File

@@ -7,7 +7,7 @@
*/ */
import PopupState, { bindMenu } from 'material-ui-popup-state'; import PopupState, { bindMenu } from 'material-ui-popup-state';
import { useMemo, useState } from 'react'; import { memo, useCallback, useMemo, useState } from 'react';
import { useLongPress } from 'use-long-press'; import { useLongPress } from 'use-long-press';
import { MangaActionMenuItems, SingleModeProps } from '@/modules/manga/components/MangaActionMenuItems.tsx'; import { MangaActionMenuItems, SingleModeProps } from '@/modules/manga/components/MangaActionMenuItems.tsx';
import { Menu } from '@/modules/core/components/menu/Menu.tsx'; import { Menu } from '@/modules/core/components/menu/Menu.tsx';
@@ -42,7 +42,7 @@ const getMangaLinkTo = (
} }
}; };
export const MangaCard = (props: MangaCardProps) => { export const MangaCard = memo((props: MangaCardProps) => {
const { manga, gridLayout, inLibraryIndicator, selected, handleSelection, mode = 'default' } = props; const { manga, gridLayout, inLibraryIndicator, selected, handleSelection, mode = 'default' } = props;
const { id, firstUnreadChapter, downloadCount, unreadCount } = manga; const { id, firstUnreadChapter, downloadCount, unreadCount } = manga;
const { const {
@@ -58,76 +58,59 @@ export const MangaCard = (props: MangaCardProps) => {
const [isMigrateDialogOpen, setIsMigrateDialogOpen] = useState(false); const [isMigrateDialogOpen, setIsMigrateDialogOpen] = useState(false);
const handleClick = (event: React.MouseEvent | React.TouchEvent, openMenu?: () => void) => { const handleClick = useCallback(
const isDefaultMode = mode === 'default'; (event: React.MouseEvent | React.TouchEvent, openMenu?: () => void) => {
const isSourceMode = mode === 'source'; const isDefaultMode = mode === 'default';
const isMigrateSelectMode = mode === 'migrate.select'; const isSourceMode = mode === 'source';
const isSelectionMode = selected !== null; const isMigrateSelectMode = mode === 'migrate.select';
const isLongPress = !!openMenu; const isSelectionMode = selected !== null;
const isLongPress = !!openMenu;
const shouldHandleClick = const shouldHandleClick =
isMigrateSelectMode || isSelectionMode || ((isDefaultMode || isSourceMode) && isLongPress); isMigrateSelectMode || isSelectionMode || ((isDefaultMode || isSourceMode) && isLongPress);
if (!shouldHandleClick) { if (!shouldHandleClick) {
return; return;
} }
event.preventDefault(); event.preventDefault();
if (isSourceMode) { if (isSourceMode) {
updateLibraryState(); updateLibraryState();
return; return;
} }
if (isSelectionMode) { if (isSelectionMode) {
handleSelection?.(id, !selected, { selectRange: event.shiftKey }); handleSelection?.(id, !selected, { selectRange: event.shiftKey });
return; return;
} }
if (isDefaultMode) { if (isDefaultMode) {
openMenu?.(); openMenu?.();
return; return;
} }
if (isMigrateSelectMode) { if (isMigrateSelectMode) {
setIsMigrateDialogOpen(true); setIsMigrateDialogOpen(true);
} }
}; },
[mode, selected, updateLibraryState, handleSelection],
);
const longPressBind = useLongPress((e, { context }) => { const longPressBind = useLongPress(
e.shiftKey = true; useCallback(
handleClick(e, context as () => {}); (e: any, { context }: any) => {
}); e.shiftKey = true;
handleClick(e, context as () => {});
},
[handleClick],
),
);
const MangaCardComponent = useMemo( const MangaCardComponent = useMemo(
() => (gridLayout === GridLayout.List ? MangaListCard : MangaGridCard), () => (gridLayout === GridLayout.List ? MangaListCard : MangaGridCard),
[gridLayout], [gridLayout],
); );
const continueReadingButton = useMemo(
() => (
<ContinueReadingButton
showContinueReadingButton={showContinueReadingButton && mode === 'default'}
chapter={firstUnreadChapter}
mangaLinkTo={mangaLinkTo}
/>
),
[showContinueReadingButton, firstUnreadChapter, mangaLinkTo],
);
const mangaBadges = useMemo(
() => (
<MangaBadges
inLibraryIndicator={inLibraryIndicator}
isInLibrary={isInLibrary}
unread={unreadCount}
downloadCount={downloadCount}
updateLibraryState={updateLibraryState}
mode={mode}
/>
),
[inLibraryIndicator, isInLibrary, unreadCount, downloadCount, updateLibraryState],
);
return ( return (
<> <>
{isMigrateDialogOpen && ( {isMigrateDialogOpen && (
@@ -144,8 +127,23 @@ export const MangaCard = (props: MangaCardProps) => {
mangaLinkTo={mangaLinkTo} mangaLinkTo={mangaLinkTo}
isInLibrary={isInLibrary} isInLibrary={isInLibrary}
inLibraryIndicator={inLibraryIndicator} inLibraryIndicator={inLibraryIndicator}
continueReadingButton={continueReadingButton} continueReadingButton={
mangaBadges={mangaBadges} <ContinueReadingButton
showContinueReadingButton={showContinueReadingButton && mode === 'default'}
chapter={firstUnreadChapter}
mangaLinkTo={mangaLinkTo}
/>
}
mangaBadges={
<MangaBadges
inLibraryIndicator={inLibraryIndicator}
isInLibrary={isInLibrary}
unread={unreadCount}
downloadCount={downloadCount}
updateLibraryState={updateLibraryState}
mode={mode}
/>
}
/> />
{!!handleSelection && popupState.isOpen && ( {!!handleSelection && popupState.isOpen && (
<Menu {...bindMenu(popupState)}> <Menu {...bindMenu(popupState)}>
@@ -165,4 +163,4 @@ export const MangaCard = (props: MangaCardProps) => {
</PopupState> </PopupState>
</> </>
); );
}; });

View File

@@ -14,7 +14,7 @@ import CardActionArea from '@mui/material/CardActionArea';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Tooltip from '@mui/material/Tooltip'; import Tooltip from '@mui/material/Tooltip';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import { useRef } from 'react'; import { memo, useRef } from 'react';
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx'; import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
import { MangaOptionButton } from '@/modules/manga/components/MangaOptionButton.tsx'; import { MangaOptionButton } from '@/modules/manga/components/MangaOptionButton.tsx';
import { Mangas } from '@/modules/manga/services/Mangas.ts'; import { Mangas } from '@/modules/manga/services/Mangas.ts';
@@ -39,166 +39,169 @@ const BottomGradientDoubledDown = styled('div')({
background: 'linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 100%)', background: 'linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 100%)',
}); });
export const MangaGridCard = ({ export const MangaGridCard = memo(
manga, ({
longPressBind, manga,
popupState, longPressBind,
handleClick, popupState,
mangaLinkTo, handleClick,
selected, mangaLinkTo,
inLibraryIndicator, selected,
isInLibrary, inLibraryIndicator,
gridLayout, isInLibrary,
handleSelection, gridLayout,
continueReadingButton, handleSelection,
mangaBadges, continueReadingButton,
mode, mangaBadges,
}: SpecificMangaCardProps) => { mode,
const optionButtonRef = useRef<HTMLButtonElement>(null); }: SpecificMangaCardProps) => {
const optionButtonRef = useRef<HTMLButtonElement>(null);
const { id, title } = manga; const { id, title } = manga;
return ( return (
<Link <Link
component={RouterLink} component={RouterLink}
{...longPressBind(() => popupState.open(optionButtonRef.current))} {...longPressBind(() => popupState.open(optionButtonRef.current))}
onClick={handleClick} onClick={handleClick}
to={mangaLinkTo} to={mangaLinkTo}
state={{ mangaTitle: title }} state={{ mangaTitle: title }}
sx={{ textDecoration: 'none', touchCallout: 'none' }} sx={{ textDecoration: 'none', touchCallout: 'none' }}
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
m: 0.25,
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',
},
'&:hover .source-manga-library-state-button': {
display: 'inline-flex',
},
'&:hover .source-manga-library-state-indicator': {
display: mode === 'source' ? 'none' : 'flex',
},
},
}}
> >
<Card <Box
sx={{ sx={{
// force standard aspect ratio of manga covers
aspectRatio: MANGA_COVER_ASPECT_RATIO,
display: 'flex', display: 'flex',
flexDirection: 'column',
m: 0.25,
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',
},
'&:hover .source-manga-library-state-button': {
display: 'inline-flex',
},
'&:hover .source-manga-library-state-indicator': {
display: mode === 'source' ? 'none' : 'flex',
},
},
}} }}
> >
<CardActionArea <Card
sx={{ sx={{
position: 'relative', // force standard aspect ratio of manga covers
height: '100%', aspectRatio: MANGA_COVER_ASPECT_RATIO,
display: 'flex',
}} }}
> >
<SpinnerImage <CardActionArea
alt={title}
src={Mangas.getThumbnailUrl(manga)}
imgStyle={
inLibraryIndicator && isInLibrary
? {
height: '100%',
width: '100%',
objectFit: 'cover',
filter: 'brightness(0.4)',
}
: {
height: '100%',
width: '100%',
objectFit: 'cover',
}
}
spinnerStyle={{
display: 'grid',
placeItems: 'center',
}}
/>
<Stack
direction="row"
sx={{ sx={{
alignItems: 'start', position: 'relative',
justifyContent: 'space-between', height: '100%',
position: 'absolute',
top: (theme) => theme.spacing(1),
left: (theme) => theme.spacing(1),
right: (theme) => theme.spacing(1),
}} }}
> >
{mangaBadges} <SpinnerImage
<MangaOptionButton alt={title}
ref={optionButtonRef} src={Mangas.getThumbnailUrl(manga)}
popupState={popupState} imgStyle={
id={id} inLibraryIndicator && isInLibrary
selected={selected} ? {
handleSelection={handleSelection} height: '100%',
width: '100%',
objectFit: 'cover',
filter: 'brightness(0.4)',
}
: {
height: '100%',
width: '100%',
objectFit: 'cover',
}
}
spinnerStyle={{
display: 'grid',
placeItems: 'center',
}}
/> />
</Stack>
<>
{gridLayout !== GridLayout.Comfortable && (
<>
<BottomGradient />
<BottomGradientDoubledDown />
</>
)}
<Stack <Stack
direction="row" direction="row"
sx={{ sx={{
justifyContent: gridLayout !== GridLayout.Comfortable ? 'space-between' : 'end', alignItems: 'start',
alignItems: 'end', justifyContent: 'space-between',
position: 'absolute', position: 'absolute',
bottom: 0, top: (theme) => theme.spacing(1),
width: '100%', left: (theme) => theme.spacing(1),
p: 1, right: (theme) => theme.spacing(1),
gap: 1,
}} }}
> >
{gridLayout !== GridLayout.Comfortable && ( {mangaBadges}
<Tooltip title={title} placement="top"> <MangaOptionButton
<TypographyMaxLines ref={optionButtonRef}
component="h3" popupState={popupState}
sx={{ id={id}
color: 'white', selected={selected}
textShadow: '0px 0px 3px #000000', handleSelection={handleSelection}
}} />
>
{title}
</TypographyMaxLines>
</Tooltip>
)}
{continueReadingButton}
</Stack> </Stack>
</> <>
</CardActionArea> {gridLayout !== GridLayout.Comfortable && (
</Card> <>
{gridLayout === GridLayout.Comfortable && ( <BottomGradient />
<Stack sx={{ pb: 1 }}> <BottomGradientDoubledDown />
<Tooltip title={title} placement="top"> </>
<TypographyMaxLines )}
component="h3" <Stack
sx={{ direction="row"
color: (theme) => (selected ? theme.palette.primary.contrastText : 'text.primary'), sx={{
height: '3rem', justifyContent: gridLayout !== GridLayout.Comfortable ? 'space-between' : 'end',
pt: 0.5, alignItems: 'end',
}} position: 'absolute',
> bottom: 0,
{title} width: '100%',
</TypographyMaxLines> p: 1,
</Tooltip> gap: 1,
</Stack> }}
)} >
</Box> {gridLayout !== GridLayout.Comfortable && (
</Link> <Tooltip title={title} placement="top">
); <TypographyMaxLines
}; component="h3"
sx={{
color: 'white',
textShadow: '0px 0px 3px #000000',
}}
>
{title}
</TypographyMaxLines>
</Tooltip>
)}
{continueReadingButton}
</Stack>
</>
</CardActionArea>
</Card>
{gridLayout === GridLayout.Comfortable && (
<Stack sx={{ pb: 1 }}>
<Tooltip title={title} placement="top">
<TypographyMaxLines
component="h3"
sx={{
color: (theme) =>
selected ? theme.palette.primary.contrastText : 'text.primary',
height: '3rem',
pt: 0.5,
}}
>
{title}
</TypographyMaxLines>
</Tooltip>
</Stack>
)}
</Box>
</Link>
);
},
);

View File

@@ -14,120 +14,122 @@ import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Tooltip from '@mui/material/Tooltip'; import Tooltip from '@mui/material/Tooltip';
import { Link as RouterLink } from 'react-router-dom'; import { Link as RouterLink } from 'react-router-dom';
import { useRef } from 'react'; import { memo, useRef } from 'react';
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx'; import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
import { TypographyMaxLines } from '@/modules/core/components/TypographyMaxLines.tsx'; import { TypographyMaxLines } from '@/modules/core/components/TypographyMaxLines.tsx';
import { SpecificMangaCardProps } from '@/modules/manga/Manga.types.ts'; import { SpecificMangaCardProps } from '@/modules/manga/Manga.types.ts';
import { Mangas } from '@/modules/manga/services/Mangas.ts'; import { Mangas } from '@/modules/manga/services/Mangas.ts';
import { MangaOptionButton } from '@/modules/manga/components/MangaOptionButton.tsx'; import { MangaOptionButton } from '@/modules/manga/components/MangaOptionButton.tsx';
export const MangaListCard = ({ export const MangaListCard = memo(
manga, ({
longPressBind, manga,
popupState, longPressBind,
handleClick, popupState,
mangaLinkTo, handleClick,
selected, mangaLinkTo,
inLibraryIndicator, selected,
isInLibrary, inLibraryIndicator,
handleSelection, isInLibrary,
continueReadingButton, handleSelection,
mangaBadges, continueReadingButton,
mode, mangaBadges,
}: SpecificMangaCardProps) => { mode,
const optionButtonRef = useRef<HTMLButtonElement>(null); }: SpecificMangaCardProps) => {
const optionButtonRef = useRef<HTMLButtonElement>(null);
const { id, title } = manga; const { id, title } = manga;
return ( return (
<Card> <Card>
<CardActionArea <CardActionArea
component={RouterLink} component={RouterLink}
to={mangaLinkTo} to={mangaLinkTo}
state={{ mangaTitle: title }} state={{ mangaTitle: title }}
onClick={handleClick} onClick={handleClick}
{...longPressBind(() => popupState.open(optionButtonRef.current))} {...longPressBind(() => popupState.open(optionButtonRef.current))}
sx={{
touchCallout: 'none',
'@media (hover: hover) and (pointer: fine)': {
'&:hover .manga-option-button': {
visibility: 'visible',
pointerEvents: 'all',
},
'&:hover .source-manga-library-state-button': {
display: 'inline-flex',
},
'&:hover .source-manga-library-state-indicator': {
display: mode === 'source' ? 'none' : 'inline-flex',
},
},
}}
>
<CardContent
sx={{ sx={{
display: 'flex', touchCallout: 'none',
justifyContent: 'space-between', '@media (hover: hover) and (pointer: fine)': {
alignItems: 'center', '&:hover .manga-option-button': {
padding: 1.5, visibility: 'visible',
position: 'relative', pointerEvents: 'all',
},
'&:hover .source-manga-library-state-button': {
display: 'inline-flex',
},
'&:hover .source-manga-library-state-indicator': {
display: mode === 'source' ? 'none' : 'inline-flex',
},
},
}} }}
> >
<Avatar <CardContent
variant="rounded"
sx={{
width: 56,
height: 56,
flex: '0 0 auto',
marginRight: 1,
}}
>
<SpinnerImage
spinnerStyle={{ small: true }}
imgStyle={{
objectFit: 'cover',
width: '100%',
height: '100%',
imageRendering: 'pixelated',
filter: inLibraryIndicator && isInLibrary ? 'brightness(0.4)' : undefined,
}}
alt={manga.title}
src={Mangas.getThumbnailUrl(manga)}
/>
</Avatar>
<Box
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'row', justifyContent: 'space-between',
flexGrow: 1,
width: 'min-content',
}}
>
<Tooltip title={title} placement="top">
<TypographyMaxLines variant="h6" component="h3">
{title}
</TypographyMaxLines>
</Tooltip>
</Box>
<Stack
direction="row"
sx={{
alignItems: 'center', alignItems: 'center',
gap: 0.5, padding: 1.5,
position: 'relative',
}} }}
> >
{mangaBadges} <Avatar
{continueReadingButton} variant="rounded"
<MangaOptionButton sx={{
ref={optionButtonRef} width: 56,
popupState={popupState} height: 56,
id={id} flex: '0 0 auto',
selected={selected} marginRight: 1,
handleSelection={handleSelection} }}
asCheckbox >
/> <SpinnerImage
</Stack> spinnerStyle={{ small: true }}
</CardContent> imgStyle={{
</CardActionArea> objectFit: 'cover',
</Card> width: '100%',
); height: '100%',
}; imageRendering: 'pixelated',
filter: inLibraryIndicator && isInLibrary ? 'brightness(0.4)' : undefined,
}}
alt={manga.title}
src={Mangas.getThumbnailUrl(manga)}
/>
</Avatar>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
flexGrow: 1,
width: 'min-content',
}}
>
<Tooltip title={title} placement="top">
<TypographyMaxLines variant="h6" component="h3">
{title}
</TypographyMaxLines>
</Tooltip>
</Box>
<Stack
direction="row"
sx={{
alignItems: 'center',
gap: 0.5,
}}
>
{mangaBadges}
{continueReadingButton}
<MangaOptionButton
ref={optionButtonRef}
popupState={popupState}
id={id}
selected={selected}
handleSelection={handleSelection}
asCheckbox
/>
</Stack>
</CardContent>
</CardActionArea>
</Card>
);
},
);