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,11 +52,8 @@ 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,
{ selectRange = false, key = currentKey } = {},
) => {
const deselect = !selected; const deselect = !selected;
const { id: lastSelectedItemId, key: lastSelectedItemIdKey } = lastSelectedItemInfoRef.current ?? {}; const { id: lastSelectedItemId, key: lastSelectedItemIdKey } = lastSelectedItemInfoRef.current ?? {};
@@ -86,9 +83,12 @@ export const useSelectableCollection = <Id extends number | string, Key extends
...prevState, ...prevState,
[key]: [...new Set([...(prevState[key] ?? []), ...selectedIds])], [key]: [...new Set([...(prevState[key] ?? []), ...selectedIds])],
})); }));
}; },
[currentKey, itemIds],
);
const handleSelectAll = (selectAll: boolean, ids: Id[], key: Key = currentKey) => { const handleSelectAll = useCallback(
(selectAll: boolean, ids: Id[], key: Key = currentKey) => {
switch (selectAll) { switch (selectAll) {
case true: case true:
setKeyToSelectedItemIds((prevState) => ({ setKeyToSelectedItemIds((prevState) => ({
@@ -105,20 +105,22 @@ export const useSelectableCollection = <Id extends number | string, Key extends
default: default:
break; break;
} }
}; },
[currentKey],
);
const setSelectionForKey = (key: Key, ids: Id[]) => { const setSelectionForKey = useCallback((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(
(id, selected, selectOptions) => {
setIsSelectModeActive(!!(selectedItemIds.length + (selected ? 1 : -1))); setIsSelectModeActive(!!(selectedItemIds.length + (selected ? 1 : -1)));
handleSelection(id, selected, selectOptions); 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,7 +58,8 @@ 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(
(event: React.MouseEvent | React.TouchEvent, openMenu?: () => void) => {
const isDefaultMode = mode === 'default'; const isDefaultMode = mode === 'default';
const isSourceMode = mode === 'source'; const isSourceMode = mode === 'source';
const isMigrateSelectMode = mode === 'migrate.select'; const isMigrateSelectMode = mode === 'migrate.select';
@@ -91,43 +92,25 @@ export const MangaCard = (props: MangaCardProps) => {
if (isMigrateSelectMode) { if (isMigrateSelectMode) {
setIsMigrateDialogOpen(true); setIsMigrateDialogOpen(true);
} }
}; },
[mode, selected, updateLibraryState, handleSelection],
);
const longPressBind = useLongPress((e, { context }) => { const longPressBind = useLongPress(
useCallback(
(e: any, { context }: any) => {
e.shiftKey = true; e.shiftKey = true;
handleClick(e, context as () => {}); 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,7 +39,8 @@ 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, manga,
longPressBind, longPressBind,
popupState, popupState,
@@ -53,7 +54,7 @@ export const MangaGridCard = ({
continueReadingButton, continueReadingButton,
mangaBadges, mangaBadges,
mode, mode,
}: SpecificMangaCardProps) => { }: SpecificMangaCardProps) => {
const optionButtonRef = useRef<HTMLButtonElement>(null); const optionButtonRef = useRef<HTMLButtonElement>(null);
const { id, title } = manga; const { id, title } = manga;
@@ -188,7 +189,8 @@ export const MangaGridCard = ({
<TypographyMaxLines <TypographyMaxLines
component="h3" component="h3"
sx={{ sx={{
color: (theme) => (selected ? theme.palette.primary.contrastText : 'text.primary'), color: (theme) =>
selected ? theme.palette.primary.contrastText : 'text.primary',
height: '3rem', height: '3rem',
pt: 0.5, pt: 0.5,
}} }}
@@ -201,4 +203,5 @@ export const MangaGridCard = ({
</Box> </Box>
</Link> </Link>
); );
}; },
);

View File

@@ -14,14 +14,15 @@ 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, manga,
longPressBind, longPressBind,
popupState, popupState,
@@ -34,7 +35,7 @@ export const MangaListCard = ({
continueReadingButton, continueReadingButton,
mangaBadges, mangaBadges,
mode, mode,
}: SpecificMangaCardProps) => { }: SpecificMangaCardProps) => {
const optionButtonRef = useRef<HTMLButtonElement>(null); const optionButtonRef = useRef<HTMLButtonElement>(null);
const { id, title } = manga; const { id, title } = manga;
@@ -130,4 +131,5 @@ export const MangaListCard = ({
</CardActionArea> </CardActionArea>
</Card> </Card>
); );
}; },
);