Reduce rerenders of reader desktop nav bar

This commit is contained in:
schroda
2025-11-29 15:26:57 +01:00
parent b5d71adeae
commit 3d8a81bc5d
6 changed files with 68 additions and 42 deletions

View File

@@ -9,7 +9,7 @@
import { ChapterAction, ChapterListOptions, ChapterSortMode } from '@/features/chapter/Chapter.types.ts'; import { ChapterAction, ChapterListOptions, ChapterSortMode } from '@/features/chapter/Chapter.types.ts';
import { TranslationKey } from '@/base/Base.types.ts'; import { TranslationKey } from '@/base/Base.types.ts';
export const FALLBACK_CHAPTER = { id: -1, name: '', realUrl: '', isBookmarked: false }; export const FALLBACK_CHAPTER = { id: -1, name: '', realUrl: '', isDownloaded: false, isBookmarked: false };
export const DEFAULT_CHAPTER_OPTIONS: ChapterListOptions = { export const DEFAULT_CHAPTER_OPTIONS: ChapterListOptions = {
unread: undefined, unread: undefined,

View File

@@ -10,18 +10,23 @@ import { Virtuoso, VirtuosoProps } from 'react-virtuoso';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { ReaderStateChapters } from '@/features/reader/Reader.types.ts'; import { ReaderStateChapters } from '@/features/reader/Reader.types.ts';
import { ChapterListCard } from '@/features/chapter/components/cards/ChapterListCard.tsx'; import { ChapterListCard } from '@/features/chapter/components/cards/ChapterListCard.tsx';
import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
const onSelectNoop = () => {}; const onSelectNoop = () => {};
export const ReaderChapterList = ({ export const ReaderChapterList = ({
currentChapter, currentChapterId,
chapters, chapters,
style, style,
}: Pick<ReaderStateChapters, 'chapters' | 'currentChapter'> & Pick<VirtuosoProps<any, any>, 'style'>) => { }: { currentChapterId: ChapterIdInfo['id'] | undefined } & Pick<ReaderStateChapters, 'chapters'> &
const currentChapterIndex = useMemo( Pick<VirtuosoProps<any, any>, 'style'>) => {
() => currentChapter && chapters.findIndex((chapter) => chapter.id === currentChapter.id), const currentChapterIndex = useMemo(() => {
[currentChapter, chapters], if (currentChapterId === undefined) {
); return 0;
}
return chapters.findIndex((chapter) => chapter.id === currentChapterId);
}, [currentChapterId, chapters]);
return ( return (
<Virtuoso <Virtuoso
@@ -29,7 +34,7 @@ export const ReaderChapterList = ({
height: `calc(${chapters.length} * 100px)`, height: `calc(${chapters.length} * 100px)`,
...style, ...style,
}} }}
initialTopMostItemIndex={currentChapterIndex ?? 0} initialTopMostItemIndex={currentChapterIndex}
totalCount={chapters.length} totalCount={chapters.length}
computeItemKey={(index) => chapters[index].id} computeItemKey={(index) => chapters[index].id}
itemContent={(index) => ( itemContent={(index) => (

View File

@@ -60,9 +60,20 @@ const BaseReaderNavBarDesktop = ({
}: ReaderNavBarDesktopProps & Pick<NavbarContextType, 'setReaderNavBarWidth'>) => { }: ReaderNavBarDesktopProps & Pick<NavbarContextType, 'setReaderNavBarWidth'>) => {
const { t } = useTranslation(); const { t } = useTranslation();
const manga = useReaderStore((state) => state.manga); const manga = useReaderStore((state) => state.manga);
const { chapters, currentChapter, previousChapter, nextChapter } = useReaderChaptersStore((state) => ({ const {
chapters,
currentChapterId,
currentChapterName,
currentChapterNumber,
currentChapterScanlator,
previousChapter,
nextChapter,
} = useReaderChaptersStore((state) => ({
chapters: state.chapters.chapters, chapters: state.chapters.chapters,
currentChapter: state.chapters.currentChapter, currentChapterId: state.chapters.currentChapter?.id,
currentChapterName: state.chapters.currentChapter?.name,
currentChapterNumber: state.chapters.currentChapter?.chapterNumber,
currentChapterScanlator: state.chapters.currentChapter?.scanlator,
previousChapter: state.chapters.previousChapter, previousChapter: state.chapters.previousChapter,
nextChapter: state.chapters.nextChapter, nextChapter: state.chapters.nextChapter,
})); }));
@@ -113,13 +124,13 @@ const BaseReaderNavBarDesktop = ({
</IconButton> </IconButton>
</CustomTooltip> </CustomTooltip>
</Stack> </Stack>
{manga && currentChapter ? ( {manga && currentChapterId !== undefined ? (
<> <>
<ReaderNavBarDesktopMetadata <ReaderNavBarDesktopMetadata
mangaId={manga.id} mangaId={manga.id}
mangaTitle={manga.title} mangaTitle={manga.title}
chapterTitle={currentChapter.name} chapterTitle={currentChapterName ?? ''}
scanlator={currentChapter.scanlator} scanlator={currentChapterScanlator}
/> />
<ReaderNavBarDesktopActions /> <ReaderNavBarDesktopActions />
</> </>
@@ -132,7 +143,9 @@ const BaseReaderNavBarDesktop = ({
<ReaderNavBarDesktopPageNavigation /> <ReaderNavBarDesktopPageNavigation />
<ReaderNavBarDesktopChapterNavigation <ReaderNavBarDesktopChapterNavigation
chapters={chapters} chapters={chapters}
currentChapter={currentChapter} currentChapterId={currentChapterId}
currentChapterName={currentChapterName}
currentChapterNumber={currentChapterNumber}
nextChapter={nextChapter} nextChapter={nextChapter}
previousChapter={previousChapter} previousChapter={previousChapter}
/> />

View File

@@ -25,19 +25,17 @@ import { requestManager } from '@/lib/requests/RequestManager.ts';
import { useReaderChaptersStore, useReaderPagesStore } from '@/features/reader/stores/ReaderStore.ts'; import { useReaderChaptersStore, useReaderPagesStore } from '@/features/reader/stores/ReaderStore.ts';
import { ChapterDownloadInfo, ChapterIdInfo } from '@/features/chapter/Chapter.types.ts'; import { ChapterDownloadInfo, ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
const DownloadButton = ({ const DownloadButton = ({ id = -1, isDownloaded }: ChapterIdInfo & ChapterDownloadInfo) => {
currentChapter,
}: {
currentChapter: NullAndUndefined<ChapterIdInfo & ChapterDownloadInfo>;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const downloadStatus = Chapters.useDownloadStatusFromCache(currentChapter?.id ?? -1); const downloadStatus = Chapters.useDownloadStatusFromCache(id);
if (currentChapter && Chapters.isDownloaded(currentChapter)) { const isDisabled = id === undefined;
if (id !== undefined && isDownloaded) {
return ( return (
<CustomTooltip title={t(CHAPTER_ACTION_TO_TRANSLATION.delete.action.single)}> <CustomTooltip title={t(CHAPTER_ACTION_TO_TRANSLATION.delete.action.single)}>
<IconButton onClick={() => Chapters.performAction('delete', [currentChapter.id], {})} color="inherit"> <IconButton onClick={() => Chapters.performAction('delete', [id], {})} color="inherit">
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
</CustomTooltip> </CustomTooltip>
@@ -49,10 +47,10 @@ const DownloadButton = ({
} }
return ( return (
<CustomTooltip title={t(CHAPTER_ACTION_TO_TRANSLATION.download.action.single)} disabled={!currentChapter}> <CustomTooltip title={t(CHAPTER_ACTION_TO_TRANSLATION.download.action.single)} disabled={isDisabled}>
<IconButton <IconButton
disabled={!currentChapter} disabled={isDisabled}
onClick={() => Chapters.performAction('download', [currentChapter?.id ?? -1], {})} onClick={() => Chapters.performAction('download', [id], {})}
color="inherit" color="inherit"
> >
<DownloadIcon /> <DownloadIcon />
@@ -62,9 +60,12 @@ const DownloadButton = ({
}; };
export const ReaderNavBarDesktopActions = memo(() => { export const ReaderNavBarDesktopActions = memo(() => {
const currentChapter = useReaderChaptersStore((state) => state.chapters.currentChapter); const { id, isDownloaded, isBookmarked, realUrl } = useReaderChaptersStore((state) => ({
id: state.chapters.currentChapter?.id ?? FALLBACK_CHAPTER.id,
const { id, isBookmarked, realUrl } = currentChapter ?? FALLBACK_CHAPTER; isDownloaded: state.chapters.currentChapter?.isDownloaded ?? FALLBACK_CHAPTER.isDownloaded,
isBookmarked: state.chapters.currentChapter?.isBookmarked ?? FALLBACK_CHAPTER.isBookmarked,
realUrl: state.chapters.currentChapter?.realUrl ?? FALLBACK_CHAPTER.realUrl,
}));
const { t } = useTranslation(); const { t } = useTranslation();
const { pageLoadStates, setPageLoadStates, setRetryFailedPagesKeyPrefix } = useReaderPagesStore((state) => ({ const { pageLoadStates, setPageLoadStates, setRetryFailedPagesKeyPrefix } = useReaderPagesStore((state) => ({
@@ -102,7 +103,7 @@ export const ReaderNavBarDesktopActions = memo(() => {
<ReplayIcon /> <ReplayIcon />
</IconButton> </IconButton>
</CustomTooltip> </CustomTooltip>
<DownloadButton currentChapter={currentChapter} /> <DownloadButton id={id} isDownloaded={isDownloaded} />
<CustomTooltip title={t('global.button.open_browser')} disabled={!realUrl}> <CustomTooltip title={t('global.button.open_browser')} disabled={!realUrl}>
<IconButton disabled={!realUrl} href={realUrl ?? ''} rel="noreferrer" target="_blank" color="inherit"> <IconButton disabled={!realUrl} href={realUrl ?? ''} rel="noreferrer" target="_blank" color="inherit">
<IconBrowser /> <IconBrowser />

View File

@@ -24,23 +24,30 @@ import { ReaderService } from '@/features/reader/services/ReaderService.ts';
import { ReaderControls } from '@/features/reader/services/ReaderControls.ts'; import { ReaderControls } from '@/features/reader/services/ReaderControls.ts';
import { ReaderStateChapters } from '@/features/reader/Reader.types.ts'; import { ReaderStateChapters } from '@/features/reader/Reader.types.ts';
import { withPropsFrom } from '@/base/hoc/withPropsFrom.tsx'; import { withPropsFrom } from '@/base/hoc/withPropsFrom.tsx';
import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
const BaseReaderNavBarDesktopChapterNavigation = ({ const BaseReaderNavBarDesktopChapterNavigation = ({
currentChapter, currentChapterId,
currentChapterName,
currentChapterNumber,
previousChapter, previousChapter,
nextChapter, nextChapter,
chapters = [], chapters = [],
readerThemeDirection, readerThemeDirection,
}: Pick<ReaderStateChapters, 'chapters' | 'currentChapter' | 'previousChapter' | 'nextChapter'> & { }: {
readerThemeDirection: ReturnType<typeof ReaderService.useGetThemeDirection>; currentChapterId: ChapterIdInfo['id'] | undefined;
}) => { currentChapterName: string | undefined;
currentChapterNumber: number | undefined;
} & Pick<ReaderStateChapters, 'chapters' | 'previousChapter' | 'nextChapter'> & {
readerThemeDirection: ReturnType<typeof ReaderService.useGetThemeDirection>;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const popupState = usePopupState({ variant: 'popover', popupId: 'reader-nav-bar-desktop-chapter-list' }); const popupState = usePopupState({ variant: 'popover', popupId: 'reader-nav-bar-desktop-chapter-list' });
useLayoutEffect(() => { useLayoutEffect(() => {
popupState.close(); popupState.close();
}, [currentChapter?.id]); }, [currentChapterId]);
return ( return (
<Stack sx={{ flexDirection: 'row', gap: 1 }} dir="ltr"> <Stack sx={{ flexDirection: 'row', gap: 1 }} dir="ltr">
@@ -63,15 +70,15 @@ const BaseReaderNavBarDesktopChapterNavigation = ({
<Select <Select
{...bindTrigger(popupState)} {...bindTrigger(popupState)}
open={popupState.isOpen} open={popupState.isOpen}
value={currentChapter?.id ?? 0} value={currentChapterId ?? 0}
// hide actual select menu // hide actual select menu
MenuProps={{ sx: { visibility: 'hidden' } }} MenuProps={{ sx: { visibility: 'hidden' } }}
label={t('chapter.title_one')} label={t('chapter.title_one')}
labelId="reader-nav-bar-desktop-chapter-select" labelId="reader-nav-bar-desktop-chapter-select"
> >
{/* hacky way to use the select component with a custom menu, the only possible value that is needed is the current chapter */} {/* hacky way to use the select component with a custom menu, the only possible value that is needed is the current chapter */}
<MenuItem key={currentChapter?.id} value={currentChapter?.id ?? 0}> <MenuItem key={currentChapterId} value={currentChapterId ?? 0}>
{currentChapter ? `#${currentChapter.chapterNumber} ${currentChapter.name}` : ''} {currentChapterNumber !== undefined ? `#${currentChapterNumber} ${currentChapterName}` : ''}
</MenuItem> </MenuItem>
</Select> </Select>
</FormControl> </FormControl>
@@ -105,7 +112,7 @@ const BaseReaderNavBarDesktopChapterNavigation = ({
minHeight: '150px', minHeight: '150px',
maxHeight: '300px', maxHeight: '300px',
}} }}
currentChapter={currentChapter} currentChapterId={currentChapterId}
chapters={chapters} chapters={chapters}
/> />
</Box> </Box>

View File

@@ -32,8 +32,8 @@ const BaseReaderBottomBarMobile = ({
topOffset = 0, topOffset = 0,
}: ReaderBottomBarMobileProps & { topOffset?: number }) => { }: ReaderBottomBarMobileProps & { topOffset?: number }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentChapter, chapters } = useReaderChaptersStore((state) => ({ const { currentChapterId, chapters } = useReaderChaptersStore((state) => ({
currentChapter: state.chapters.currentChapter, currentChapterId: state.chapters.currentChapter?.id,
chapters: state.chapters.chapters, chapters: state.chapters.chapters,
})); }));
@@ -50,7 +50,7 @@ const BaseReaderBottomBarMobile = ({
useLayoutEffect(() => { useLayoutEffect(() => {
chapterListPopupState.close(); chapterListPopupState.close();
}, [currentChapter?.id]); }, [currentChapterId]);
return ( return (
<> <>
@@ -112,7 +112,7 @@ const BaseReaderBottomBarMobile = ({
minHeight: '15vh', minHeight: '15vh',
maxHeight: '75vh', maxHeight: '75vh',
}} }}
currentChapter={currentChapter} currentChapterId={currentChapterId}
chapters={chapters} chapters={chapters}
/> />
</DialogContent> </DialogContent>