Feature/make selection logic reusable (#515)
* Introduce "selectable collection" hook * Extract "select all" button into component * Make "SelectionFABActionItem" reusable Was to tightly coupled to chapters * Make "SelectionFAB" reusable Was to tightly coupled to chapters
This commit is contained in:
34
src/components/collection/SelectableCollectionSelectAll.tsx
Normal file
34
src/components/collection/SelectableCollectionSelectAll.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Tooltip } from '@mui/material';
|
||||||
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export const SelectableCollectionSelectAll = ({
|
||||||
|
areAllItemsSelected,
|
||||||
|
areNoItemsSelected,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
areAllItemsSelected: boolean;
|
||||||
|
areNoItemsSelected: boolean;
|
||||||
|
onChange: (checked: boolean) => void;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={t(!areAllItemsSelected ? 'global.button.select_all' : 'global.button.clear')}>
|
||||||
|
<Checkbox
|
||||||
|
sx={{ padding: '8px' }}
|
||||||
|
checked={areAllItemsSelected}
|
||||||
|
indeterminate={!areNoItemsSelected && !areAllItemsSelected}
|
||||||
|
onChange={(_, checked) => onChange(checked)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
41
src/components/collection/useSelectableCollection.ts
Normal file
41
src/components/collection/useSelectableCollection.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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 { useState } from 'react';
|
||||||
|
|
||||||
|
export const useSelectableCollection = <Id extends number | string>(totalCount: number) => {
|
||||||
|
const [selectedItemIds, setSelectedItemIds] = useState<Id[]>([]);
|
||||||
|
|
||||||
|
const areAllItemsSelected = selectedItemIds.length === totalCount;
|
||||||
|
const areNoItemsSelected = !selectedItemIds.length;
|
||||||
|
|
||||||
|
const handleSelection = (id: Id, selected: boolean) => {
|
||||||
|
const deselect = !selected;
|
||||||
|
if (deselect) {
|
||||||
|
setSelectedItemIds(selectedItemIds.filter((selectedItemId) => selectedItemId !== id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedItemIds([...new Set([...selectedItemIds, id])]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectAll = (selectAll: boolean, itemIds: Id[]) => {
|
||||||
|
switch (selectAll) {
|
||||||
|
case true:
|
||||||
|
setSelectedItemIds([...itemIds]);
|
||||||
|
break;
|
||||||
|
case false:
|
||||||
|
setSelectedItemIds([]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { selectedItemIds, handleSelection, handleSelectAll, areAllItemsSelected, areNoItemsSelected };
|
||||||
|
};
|
||||||
@@ -8,10 +8,9 @@
|
|||||||
|
|
||||||
import { Box, CircularProgress, Stack, styled, Tooltip } from '@mui/material';
|
import { Box, CircularProgress, Stack, styled, Tooltip } from '@mui/material';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import React, { ComponentProps, useMemo, useState } from 'react';
|
import React, { ComponentProps, useMemo } from 'react';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Checkbox from '@mui/material/Checkbox';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import DownloadIcon from '@mui/icons-material/Download';
|
import DownloadIcon from '@mui/icons-material/Download';
|
||||||
import DoneAllIcon from '@mui/icons-material/DoneAll';
|
import DoneAllIcon from '@mui/icons-material/DoneAll';
|
||||||
@@ -27,6 +26,9 @@ import { SelectionFAB } from '@/components/manga/SelectionFAB';
|
|||||||
import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab';
|
import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab';
|
||||||
import { DownloadType, UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
|
import { DownloadType, UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';
|
import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';
|
||||||
|
import { useSelectableCollection } from '@/components/collection/useSelectableCollection.ts';
|
||||||
|
import { SelectableCollectionSelectAll } from '@/components/collection/SelectableCollectionSelectAll.tsx';
|
||||||
|
import { ChapterSelectionFABActionItems } from '@/components/manga/ChapterSelectionFABActionItems.tsx';
|
||||||
|
|
||||||
const ChapterListHeader = styled(Stack)(({ theme }) => ({
|
const ChapterListHeader = styled(Stack)(({ theme }) => ({
|
||||||
margin: 8,
|
margin: 8,
|
||||||
@@ -96,7 +98,6 @@ interface IProps {
|
|||||||
export const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
|
export const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [selection, setSelection] = useState<number[] | null>(null);
|
|
||||||
const { data: downloaderData } = requestManager.useDownloadSubscription();
|
const { data: downloaderData } = requestManager.useDownloadSubscription();
|
||||||
const queue = (downloaderData?.downloadChanged.queue as DownloadType[]) ?? [];
|
const queue = (downloaderData?.downloadChanged.queue as DownloadType[]) ?? [];
|
||||||
|
|
||||||
@@ -104,6 +105,9 @@ export const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
|
|||||||
const { data: chaptersData, loading: isLoading } = requestManager.useGetMangaChapters(manga.id);
|
const { data: chaptersData, loading: isLoading } = requestManager.useGetMangaChapters(manga.id);
|
||||||
const chapters = useMemo(() => chaptersData?.chapters.nodes ?? [], [chaptersData?.chapters.nodes]);
|
const chapters = useMemo(() => chaptersData?.chapters.nodes ?? [], [chaptersData?.chapters.nodes]);
|
||||||
|
|
||||||
|
const { areNoItemsSelected, areAllItemsSelected, selectedItemIds, handleSelectAll, handleSelection } =
|
||||||
|
useSelectableCollection(chapters.length);
|
||||||
|
|
||||||
const { settings: metadataServerSettings } = useMetadataServerSettings();
|
const { settings: metadataServerSettings } = useMetadataServerSettings();
|
||||||
|
|
||||||
const visibleChapters = useMemo(() => filterAndSortChapters(chapters, options), [chapters, options]);
|
const visibleChapters = useMemo(() => filterAndSortChapters(chapters, options), [chapters, options]);
|
||||||
@@ -111,41 +115,13 @@ export const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
|
|||||||
const nextChapterIndexToRead = (manga.lastReadChapter?.sourceOrder ?? 0) + 1;
|
const nextChapterIndexToRead = (manga.lastReadChapter?.sourceOrder ?? 0) + 1;
|
||||||
const isLatestChapterRead = manga.chapters.totalCount === manga.lastReadChapter?.sourceOrder;
|
const isLatestChapterRead = manga.chapters.totalCount === manga.lastReadChapter?.sourceOrder;
|
||||||
|
|
||||||
const areAllChaptersSelected = selection?.length === chapters.length;
|
|
||||||
const areNoneChaptersSelected = !selection;
|
|
||||||
|
|
||||||
const areAllChaptersRead = manga.unreadCount === 0;
|
const areAllChaptersRead = manga.unreadCount === 0;
|
||||||
const areAllChaptersDownloaded = manga.downloadCount === manga.chapters.totalCount;
|
const areAllChaptersDownloaded = manga.downloadCount === manga.chapters.totalCount;
|
||||||
|
|
||||||
const handleSelection = (index: number) => {
|
const handleFabAction: ComponentProps<typeof ChapterSelectionFABActionItems>['onAction'] = (
|
||||||
const chapter = visibleChapters[index];
|
action,
|
||||||
if (!chapter) return;
|
actionChapters,
|
||||||
|
) => {
|
||||||
if (selection === null) {
|
|
||||||
setSelection([chapter.id]);
|
|
||||||
} else if (selection.includes(chapter.id)) {
|
|
||||||
const newSelection = selection.filter((cid) => cid !== chapter.id);
|
|
||||||
setSelection(newSelection.length > 0 ? newSelection : null);
|
|
||||||
} else {
|
|
||||||
setSelection([...selection, chapter.id]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectAll = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const selectAll = event.target.checked;
|
|
||||||
switch (selectAll) {
|
|
||||||
case true:
|
|
||||||
setSelection(visibleChapters.map((c) => c.id));
|
|
||||||
break;
|
|
||||||
case false:
|
|
||||||
setSelection(null);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFabAction: ComponentProps<typeof SelectionFAB>['onAction'] = (action, actionChapters) => {
|
|
||||||
if (actionChapters.length === 0) return;
|
if (actionChapters.length === 0) return;
|
||||||
const chapterIds = actionChapters
|
const chapterIds = actionChapters
|
||||||
.filter(({ chapter }) => {
|
.filter(({ chapter }) => {
|
||||||
@@ -215,27 +191,31 @@ export const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
|
|||||||
const downloadChapter = queue?.find(
|
const downloadChapter = queue?.find(
|
||||||
(cd) => cd.chapter.sourceOrder === chapter.sourceOrder && cd.chapter.manga.id === chapter.manga.id,
|
(cd) => cd.chapter.sourceOrder === chapter.sourceOrder && cd.chapter.manga.id === chapter.manga.id,
|
||||||
);
|
);
|
||||||
const selected = selection?.includes(chapter.id) ?? null;
|
const selected = !areNoItemsSelected ? selectedItemIds.includes(chapter.id) : null;
|
||||||
return {
|
return {
|
||||||
chapter,
|
chapter,
|
||||||
downloadChapter,
|
downloadChapter,
|
||||||
selected,
|
selected,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
[queue, selection, visibleChapters],
|
[queue, selectedItemIds, visibleChapters],
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedChapters = useMemo(() => {
|
|
||||||
if (!selection) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return chaptersWithMeta.filter(({ chapter }) => selection.includes(chapter.id));
|
|
||||||
}, [selection, chapters]);
|
|
||||||
|
|
||||||
const chapterListFAB = useMemo(() => {
|
const chapterListFAB = useMemo(() => {
|
||||||
if (selectedChapters) {
|
const selectedChapters = chaptersWithMeta.filter((chapter) => chapter.selected);
|
||||||
return <SelectionFAB selectedChapters={selectedChapters} onAction={handleFabAction} />;
|
|
||||||
|
if (selectedChapters.length) {
|
||||||
|
return (
|
||||||
|
<SelectionFAB selectedItemsCount={selectedChapters.length} title="chapter.title">
|
||||||
|
{(handleClose) => (
|
||||||
|
<ChapterSelectionFABActionItems
|
||||||
|
selectedChapters={selectedChapters}
|
||||||
|
onAction={handleFabAction}
|
||||||
|
handleClose={handleClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SelectionFAB>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLatestChapterRead) {
|
if (!isLatestChapterRead) {
|
||||||
@@ -243,7 +223,7 @@ export const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, [selectedChapters, isLatestChapterRead]);
|
}, [chaptersWithMeta, isLatestChapterRead]);
|
||||||
|
|
||||||
if (isLoading || (noChaptersFound && isRefreshing)) {
|
if (isLoading || (noChaptersFound && isRefreshing)) {
|
||||||
return (
|
return (
|
||||||
@@ -287,16 +267,13 @@ export const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ChaptersToolbarMenu options={options} optionsDispatch={dispatch} />
|
<ChaptersToolbarMenu options={options} optionsDispatch={dispatch} />
|
||||||
<Tooltip
|
<SelectableCollectionSelectAll
|
||||||
title={t(!areAllChaptersSelected ? 'global.button.select_all' : 'global.button.clear')}
|
areAllItemsSelected={areAllItemsSelected}
|
||||||
>
|
areNoItemsSelected={areNoItemsSelected}
|
||||||
<Checkbox
|
onChange={(checked) =>
|
||||||
sx={{ padding: '8px' }}
|
handleSelectAll(checked, checked ? chapters.map((chapter) => chapter.id) : [])
|
||||||
checked={areAllChaptersSelected}
|
}
|
||||||
indeterminate={!areAllChaptersSelected && !areNoneChaptersSelected}
|
/>
|
||||||
onChange={handleSelectAll}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</ChapterListHeader>
|
</ChapterListHeader>
|
||||||
|
|
||||||
@@ -317,7 +294,7 @@ export const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
|
|||||||
{...chaptersWithMeta[index]}
|
{...chaptersWithMeta[index]}
|
||||||
allChapters={chapters}
|
allChapters={chapters}
|
||||||
showChapterNumber={options.showChapterNumber}
|
showChapterNumber={options.showChapterNumber}
|
||||||
onSelect={() => handleSelection(index)}
|
onSelect={(selected) => handleSelection(chaptersWithMeta[index].chapter.id, selected)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
useWindowScroll={window.innerWidth < 900}
|
useWindowScroll={window.innerWidth < 900}
|
||||||
|
|||||||
85
src/components/manga/ChapterSelectionFABActionItems.tsx
Normal file
85
src/components/manga/ChapterSelectionFABActionItems.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* 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 Download from '@mui/icons-material/Download';
|
||||||
|
import Delete from '@mui/icons-material/Delete';
|
||||||
|
import BookmarkAdd from '@mui/icons-material/BookmarkAdd';
|
||||||
|
import BookmarkRemove from '@mui/icons-material/BookmarkRemove';
|
||||||
|
import Done from '@mui/icons-material/Done';
|
||||||
|
import RemoveDone from '@mui/icons-material/RemoveDone';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { SelectionFABActionItem } from '@/components/manga/SelectionFABActionItem.tsx';
|
||||||
|
import { IChapterWithMeta } from '@/components/manga/ChapterList.tsx';
|
||||||
|
|
||||||
|
type SelectionAction = 'download' | 'delete' | 'bookmark' | 'unbookmark' | 'mark_as_read' | 'mark_as_unread';
|
||||||
|
|
||||||
|
export const ChapterSelectionFABActionItems = ({
|
||||||
|
selectedChapters,
|
||||||
|
onAction,
|
||||||
|
handleClose,
|
||||||
|
}: {
|
||||||
|
selectedChapters: IChapterWithMeta[];
|
||||||
|
onAction: (action: SelectionAction, chapters: IChapterWithMeta[]) => void;
|
||||||
|
handleClose: () => void;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleAction = (action: SelectionAction, chapters: IChapterWithMeta[]) => {
|
||||||
|
onAction(action, chapters);
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SelectionFABActionItem
|
||||||
|
action="download"
|
||||||
|
Icon={Download}
|
||||||
|
matchingItems={selectedChapters.filter(
|
||||||
|
({ chapter: c, downloadChapter: dc }) => !c.isDownloaded && dc === undefined,
|
||||||
|
)}
|
||||||
|
onClick={handleAction}
|
||||||
|
title={t('chapter.action.download.add.button.selected')}
|
||||||
|
/>
|
||||||
|
<SelectionFABActionItem
|
||||||
|
action="delete"
|
||||||
|
Icon={Delete}
|
||||||
|
matchingItems={selectedChapters.filter(({ chapter }) => chapter.isDownloaded)}
|
||||||
|
onClick={handleAction}
|
||||||
|
title={t('chapter.action.download.delete.button.selected')}
|
||||||
|
/>
|
||||||
|
<SelectionFABActionItem
|
||||||
|
action="bookmark"
|
||||||
|
Icon={BookmarkAdd}
|
||||||
|
matchingItems={selectedChapters.filter(({ chapter }) => !chapter.isBookmarked)}
|
||||||
|
onClick={handleAction}
|
||||||
|
title={t('chapter.action.bookmark.add.button.selected')}
|
||||||
|
/>
|
||||||
|
<SelectionFABActionItem
|
||||||
|
action="unbookmark"
|
||||||
|
Icon={BookmarkRemove}
|
||||||
|
matchingItems={selectedChapters.filter(({ chapter }) => chapter.isBookmarked)}
|
||||||
|
onClick={handleAction}
|
||||||
|
title={t('chapter.action.bookmark.remove.button.selected')}
|
||||||
|
/>
|
||||||
|
<SelectionFABActionItem
|
||||||
|
action="mark_as_read"
|
||||||
|
Icon={Done}
|
||||||
|
matchingItems={selectedChapters.filter(({ chapter }) => !chapter.isRead)}
|
||||||
|
onClick={handleAction}
|
||||||
|
title={t('chapter.action.mark_as_read.add.button.selected')}
|
||||||
|
/>
|
||||||
|
<SelectionFABActionItem
|
||||||
|
action="mark_as_unread"
|
||||||
|
Icon={RemoveDone}
|
||||||
|
matchingItems={selectedChapters.filter(({ chapter }) => chapter.isRead)}
|
||||||
|
onClick={handleAction}
|
||||||
|
title={t('chapter.action.mark_as_read.remove.button.selected')}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -10,32 +10,22 @@ import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
|||||||
import { Fab, Menu, Box } from '@mui/material';
|
import { Fab, Menu, Box } from '@mui/material';
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { IChapterWithMeta } from '@/components/manga/ChapterList';
|
|
||||||
import { SelectionFABActionItem } from '@/components/manga/SelectionFABActionItem';
|
|
||||||
import { DEFAULT_FAB_STYLE } from '@/components/util/StyledFab';
|
import { DEFAULT_FAB_STYLE } from '@/components/util/StyledFab';
|
||||||
|
import { TranslationKey } from '@/typings.ts';
|
||||||
export type SelectionAction = 'download' | 'delete' | 'bookmark' | 'unbookmark' | 'mark_as_read' | 'mark_as_unread';
|
|
||||||
|
|
||||||
interface SelectionFABProps {
|
interface SelectionFABProps {
|
||||||
selectedChapters: IChapterWithMeta[];
|
children: (handleClose: () => void) => React.ReactNode;
|
||||||
onAction: (action: SelectionAction, chapters: IChapterWithMeta[]) => void;
|
selectedItemsCount: number;
|
||||||
|
title: TranslationKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SelectionFAB: React.FC<SelectionFABProps> = (props) => {
|
export const SelectionFAB: React.FC<SelectionFABProps> = ({ children, selectedItemsCount, title }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { selectedChapters, onAction } = props;
|
|
||||||
const count = selectedChapters.length;
|
|
||||||
|
|
||||||
const anchorEl = useRef<HTMLElement>();
|
const anchorEl = useRef<HTMLElement>();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const handleClose = () => setOpen(false);
|
const handleClose = () => setOpen(false);
|
||||||
|
|
||||||
const handleAction = (action: SelectionAction, chapters: IChapterWithMeta[]) => {
|
|
||||||
onAction(action, chapters);
|
|
||||||
handleClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -47,7 +37,7 @@ export const SelectionFAB: React.FC<SelectionFABProps> = (props) => {
|
|||||||
ref={anchorEl}
|
ref={anchorEl}
|
||||||
>
|
>
|
||||||
<Fab variant="extended" color="primary" id="selectionMenuButton" onClick={() => setOpen(true)}>
|
<Fab variant="extended" color="primary" id="selectionMenuButton" onClick={() => setOpen(true)}>
|
||||||
{`${count} ${t('chapter.title', { count })}`}
|
{`${selectedItemsCount} ${t(title, { count: selectedItemsCount })}`}
|
||||||
<MoreHoriz sx={{ ml: 1 }} />
|
<MoreHoriz sx={{ ml: 1 }} />
|
||||||
</Fab>
|
</Fab>
|
||||||
<Menu
|
<Menu
|
||||||
@@ -61,44 +51,7 @@ export const SelectionFAB: React.FC<SelectionFABProps> = (props) => {
|
|||||||
'aria-labelledby': 'selectionMenuButton',
|
'aria-labelledby': 'selectionMenuButton',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectionFABActionItem
|
{children(handleClose)}
|
||||||
action="download"
|
|
||||||
matchingChapters={selectedChapters.filter(
|
|
||||||
({ chapter: c, downloadChapter: dc }) => !c.isDownloaded && dc === undefined,
|
|
||||||
)}
|
|
||||||
onClick={handleAction}
|
|
||||||
title={t('chapter.action.download.add.button.selected')}
|
|
||||||
/>
|
|
||||||
<SelectionFABActionItem
|
|
||||||
action="delete"
|
|
||||||
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.isDownloaded)}
|
|
||||||
onClick={handleAction}
|
|
||||||
title={t('chapter.action.download.delete.button.selected')}
|
|
||||||
/>
|
|
||||||
<SelectionFABActionItem
|
|
||||||
action="bookmark"
|
|
||||||
matchingChapters={selectedChapters.filter(({ chapter }) => !chapter.isBookmarked)}
|
|
||||||
onClick={handleAction}
|
|
||||||
title={t('chapter.action.bookmark.add.button.selected')}
|
|
||||||
/>
|
|
||||||
<SelectionFABActionItem
|
|
||||||
action="unbookmark"
|
|
||||||
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.isBookmarked)}
|
|
||||||
onClick={handleAction}
|
|
||||||
title={t('chapter.action.bookmark.remove.button.selected')}
|
|
||||||
/>
|
|
||||||
<SelectionFABActionItem
|
|
||||||
action="mark_as_read"
|
|
||||||
matchingChapters={selectedChapters.filter(({ chapter }) => !chapter.isRead)}
|
|
||||||
onClick={handleAction}
|
|
||||||
title={t('chapter.action.mark_as_read.add.button.selected')}
|
|
||||||
/>
|
|
||||||
<SelectionFABActionItem
|
|
||||||
action="mark_as_unread"
|
|
||||||
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.isRead)}
|
|
||||||
onClick={handleAction}
|
|
||||||
title={t('chapter.action.mark_as_read.remove.button.selected')}
|
|
||||||
/>
|
|
||||||
</Menu>
|
</Menu>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,38 +6,28 @@
|
|||||||
* 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 BookmarkAdd from '@mui/icons-material/BookmarkAdd';
|
|
||||||
import BookmarkRemove from '@mui/icons-material/BookmarkRemove';
|
|
||||||
import Delete from '@mui/icons-material/Delete';
|
|
||||||
import Done from '@mui/icons-material/Done';
|
|
||||||
import Download from '@mui/icons-material/Download';
|
|
||||||
import RemoveDone from '@mui/icons-material/RemoveDone';
|
|
||||||
import { ListItemIcon, ListItemText, MenuItem } from '@mui/material';
|
import { ListItemIcon, ListItemText, MenuItem } from '@mui/material';
|
||||||
import React from 'react';
|
import { OverridableComponent } from '@mui/material/OverridableComponent';
|
||||||
import type { IChapterWithMeta } from '@/components/manga/ChapterList';
|
import { SvgIconTypeMap } from '@mui/material/SvgIcon/SvgIcon';
|
||||||
import type { SelectionAction } from '@/components/manga/SelectionFAB';
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps<Action, Item> {
|
||||||
action: SelectionAction;
|
action: Action;
|
||||||
matchingChapters: IChapterWithMeta[];
|
matchingItems: Item[];
|
||||||
title: string;
|
title: string;
|
||||||
onClick: (action: SelectionAction, chapters: IChapterWithMeta[]) => void;
|
Icon: OverridableComponent<SvgIconTypeMap> & { muiName: string };
|
||||||
|
onClick: (action: Action, items: Item[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ICONS = {
|
export const SelectionFABActionItem = <Action extends string, Item>({
|
||||||
download: Download,
|
action,
|
||||||
delete: Delete,
|
matchingItems,
|
||||||
bookmark: BookmarkAdd,
|
onClick,
|
||||||
unbookmark: BookmarkRemove,
|
title,
|
||||||
mark_as_read: Done,
|
Icon,
|
||||||
mark_as_unread: RemoveDone,
|
}: IProps<Action, Item>) => {
|
||||||
};
|
const count = matchingItems.length;
|
||||||
|
|
||||||
export const SelectionFABActionItem: React.FC<IProps> = ({ action, matchingChapters, onClick, title }) => {
|
|
||||||
const count = matchingChapters.length;
|
|
||||||
const Icon = ICONS[action];
|
|
||||||
return (
|
return (
|
||||||
<MenuItem onClick={() => onClick(action, matchingChapters)} disabled={count === 0}>
|
<MenuItem onClick={() => onClick(action, matchingItems)} disabled={count === 0}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Icon fontSize="small" />
|
<Icon fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
|||||||
Reference in New Issue
Block a user