Feature/cleanup chapter actions (#525)
* Extract chapter actions into "Chapters" * Extract chapter action menu into component * Rename "SelectionFABActionItem" to "MenuItem" * Decouple "MenuItem" from "SelectionFAB" * Use "MenuItem" in "ActionMenus" * Merge chapter action menus * Simplify chapter "mark as read" action
This commit is contained in:
204
src/components/manga/ChapterActionMenuItems.tsx
Normal file
204
src/components/manga/ChapterActionMenuItems.tsx
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
/*
|
||||||
|
* 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 CheckBoxOutlineBlank from '@mui/icons-material/CheckBoxOutlineBlank';
|
||||||
|
import Delete from '@mui/icons-material/Delete';
|
||||||
|
import Download from '@mui/icons-material/Download';
|
||||||
|
import RemoveDone from '@mui/icons-material/RemoveDone';
|
||||||
|
import Done from '@mui/icons-material/Done';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import BookmarkRemove from '@mui/icons-material/BookmarkRemove';
|
||||||
|
import BookmarkAdd from '@mui/icons-material/BookmarkAdd';
|
||||||
|
import DoneAll from '@mui/icons-material/DoneAll';
|
||||||
|
import { t as translate } from 'i18next';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts';
|
||||||
|
import {
|
||||||
|
actionToTranslationKey,
|
||||||
|
ChapterAction,
|
||||||
|
ChapterBookmarkInfo,
|
||||||
|
ChapterDownloadInfo,
|
||||||
|
ChapterReadInfo,
|
||||||
|
Chapters,
|
||||||
|
} from '@/lib/data/Chapters.ts';
|
||||||
|
import { TChapter } from '@/typings.ts';
|
||||||
|
import { MenuItem } from '@/components/manga/MenuItem.tsx';
|
||||||
|
import { IChapterWithMeta } from '@/components/manga/ChapterList.tsx';
|
||||||
|
import { ChaptersWithMeta } from '@/lib/data/ChaptersWithMeta.ts';
|
||||||
|
|
||||||
|
const createGetMenuItemTitle =
|
||||||
|
(isSingleMode: boolean) =>
|
||||||
|
(action: ChapterAction, count: number): string => {
|
||||||
|
const countSuffix = count > 0 ? ` (${count})` : '';
|
||||||
|
return `${translate(
|
||||||
|
actionToTranslationKey[action].action[isSingleMode ? 'single' : 'selected'],
|
||||||
|
)}${countSuffix}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createShouldShowMenuItem =
|
||||||
|
(isSingleMode: boolean) =>
|
||||||
|
(shouldBeVisible: boolean = false): boolean =>
|
||||||
|
isSingleMode ? shouldBeVisible : true;
|
||||||
|
|
||||||
|
const createIsMenuItemDisabled =
|
||||||
|
(isSingleMode: boolean) =>
|
||||||
|
(shouldBeDisabled: boolean): boolean =>
|
||||||
|
isSingleMode ? false : shouldBeDisabled;
|
||||||
|
|
||||||
|
type BaseProps = { onClose: () => void };
|
||||||
|
|
||||||
|
type SingleModeProps = {
|
||||||
|
chapter: ChapterDownloadInfo & ChapterBookmarkInfo & ChapterReadInfo;
|
||||||
|
allChapters: TChapter[];
|
||||||
|
handleSelection?: SelectableCollectionReturnType<TChapter['id']>['handleSelection'];
|
||||||
|
canBeDownloaded: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SelectModeProps = {
|
||||||
|
selectedChapters: IChapterWithMeta[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props =
|
||||||
|
| (BaseProps & SingleModeProps & PropertiesNever<SelectModeProps>)
|
||||||
|
| (BaseProps & PropertiesNever<SingleModeProps> & SelectModeProps);
|
||||||
|
|
||||||
|
export const ChapterActionMenuItems = ({
|
||||||
|
chapter,
|
||||||
|
allChapters,
|
||||||
|
handleSelection,
|
||||||
|
canBeDownloaded = false,
|
||||||
|
selectedChapters = [],
|
||||||
|
onClose,
|
||||||
|
}: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const isSingleMode = !!chapter;
|
||||||
|
const { isDownloaded, isRead, isBookmarked } = chapter ?? {};
|
||||||
|
|
||||||
|
const getMenuItemTitle = createGetMenuItemTitle(isSingleMode);
|
||||||
|
const shouldShowMenuItem = createShouldShowMenuItem(isSingleMode);
|
||||||
|
const isMenuItemDisabled = createIsMenuItemDisabled(isSingleMode);
|
||||||
|
|
||||||
|
const {
|
||||||
|
downloadableChapters,
|
||||||
|
downloadedChapters,
|
||||||
|
unbookmarkedChapters,
|
||||||
|
bookmarkedChapters,
|
||||||
|
unreadChapters,
|
||||||
|
readChapters,
|
||||||
|
} = useMemo(
|
||||||
|
() => ({
|
||||||
|
downloadableChapters: ChaptersWithMeta.getDownloadable(selectedChapters),
|
||||||
|
downloadedChapters: ChaptersWithMeta.getDownloaded(selectedChapters),
|
||||||
|
unbookmarkedChapters: ChaptersWithMeta.getNonBookmarked(selectedChapters),
|
||||||
|
bookmarkedChapters: ChaptersWithMeta.getBookmarked(selectedChapters),
|
||||||
|
unreadChapters: ChaptersWithMeta.getNonRead(selectedChapters),
|
||||||
|
readChapters: ChaptersWithMeta.getRead(selectedChapters),
|
||||||
|
}),
|
||||||
|
[selectedChapters],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelect = () => {
|
||||||
|
handleSelection?.(chapter.id, true);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const performAction = (action: ChapterAction | 'mark_prev_as_read', chaptersWithMeta: IChapterWithMeta[]) => {
|
||||||
|
const isMarkPrevAsRead = action === 'mark_prev_as_read';
|
||||||
|
const actualAction: ChapterAction = isMarkPrevAsRead ? 'mark_as_read' : action;
|
||||||
|
|
||||||
|
const getChapters = (): (ChapterDownloadInfo & ChapterBookmarkInfo & ChapterReadInfo)[] => {
|
||||||
|
// select mode
|
||||||
|
if (!chapter) {
|
||||||
|
return ChaptersWithMeta.getChapters(chaptersWithMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMarkPrevAsRead) {
|
||||||
|
return [chapter];
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = allChapters.findIndex(({ id: chapterId }) => chapterId === chapter.id);
|
||||||
|
|
||||||
|
const isFirstChapter = index + 1 > allChapters.length - 1;
|
||||||
|
if (isFirstChapter) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return allChapters.slice(index + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
Chapters.performAction(actualAction, chapter ? [chapter.id] : ChaptersWithMeta.getIds(chaptersWithMeta), {
|
||||||
|
chapters: getChapters(),
|
||||||
|
wasManuallyMarkedAsRead: true,
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isSingleMode && (
|
||||||
|
<MenuItem onClick={handleSelect} Icon={CheckBoxOutlineBlank} title={t('chapter.action.label.select')} />
|
||||||
|
)}
|
||||||
|
{shouldShowMenuItem(canBeDownloaded) && (
|
||||||
|
<MenuItem
|
||||||
|
Icon={Download}
|
||||||
|
isDisabled={isMenuItemDisabled(!downloadableChapters.length)}
|
||||||
|
onClick={() => performAction('download', downloadableChapters)}
|
||||||
|
title={getMenuItemTitle('download', downloadableChapters.length)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{shouldShowMenuItem(isDownloaded) && (
|
||||||
|
<MenuItem
|
||||||
|
Icon={Delete}
|
||||||
|
isDisabled={isMenuItemDisabled(!downloadedChapters.length)}
|
||||||
|
onClick={() => performAction('delete', downloadedChapters)}
|
||||||
|
title={getMenuItemTitle('delete', downloadedChapters.length)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{shouldShowMenuItem(!isBookmarked) && (
|
||||||
|
<MenuItem
|
||||||
|
Icon={BookmarkAdd}
|
||||||
|
isDisabled={isMenuItemDisabled(!unbookmarkedChapters.length)}
|
||||||
|
onClick={() => performAction('bookmark', unbookmarkedChapters)}
|
||||||
|
title={getMenuItemTitle('bookmark', unbookmarkedChapters.length)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{shouldShowMenuItem(isBookmarked) && (
|
||||||
|
<MenuItem
|
||||||
|
Icon={BookmarkRemove}
|
||||||
|
isDisabled={isMenuItemDisabled(!bookmarkedChapters.length)}
|
||||||
|
onClick={() => performAction('unbookmark', bookmarkedChapters)}
|
||||||
|
title={getMenuItemTitle('unbookmark', bookmarkedChapters.length)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{shouldShowMenuItem(!isRead) && (
|
||||||
|
<MenuItem
|
||||||
|
Icon={Done}
|
||||||
|
isDisabled={isMenuItemDisabled(!unreadChapters.length)}
|
||||||
|
onClick={() => performAction('mark_as_read', unreadChapters)}
|
||||||
|
title={getMenuItemTitle('mark_as_read', unreadChapters.length)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{shouldShowMenuItem(isRead) && (
|
||||||
|
<MenuItem
|
||||||
|
Icon={RemoveDone}
|
||||||
|
isDisabled={isMenuItemDisabled(!readChapters.length)}
|
||||||
|
onClick={() => performAction('mark_as_unread', readChapters)}
|
||||||
|
title={getMenuItemTitle('mark_as_unread', readChapters.length)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isSingleMode && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => performAction('mark_prev_as_read', [])}
|
||||||
|
Icon={DoneAll}
|
||||||
|
title={t('chapter.action.mark_as_read.add.label.action.previous')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,32 +7,23 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import BookmarkIcon from '@mui/icons-material/Bookmark';
|
import BookmarkIcon from '@mui/icons-material/Bookmark';
|
||||||
import BookmarkAdd from '@mui/icons-material/BookmarkAdd';
|
|
||||||
import BookmarkRemove from '@mui/icons-material/BookmarkRemove';
|
|
||||||
import CheckBoxOutlineBlank from '@mui/icons-material/CheckBoxOutlineBlank';
|
|
||||||
import Delete from '@mui/icons-material/Delete';
|
|
||||||
import Done from '@mui/icons-material/Done';
|
|
||||||
import DoneAll from '@mui/icons-material/DoneAll';
|
|
||||||
import Download from '@mui/icons-material/Download';
|
|
||||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||||
import RemoveDone from '@mui/icons-material/RemoveDone';
|
import { CardActionArea, Checkbox, Stack, Tooltip } from '@mui/material';
|
||||||
import { CardActionArea, Checkbox, ListItemIcon, ListItemText, Stack, Tooltip } from '@mui/material';
|
|
||||||
import Card from '@mui/material/Card';
|
import Card from '@mui/material/Card';
|
||||||
import CardContent from '@mui/material/CardContent';
|
import CardContent from '@mui/material/CardContent';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Menu from '@mui/material/Menu';
|
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import React from 'react';
|
import React, { TouchEvent } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
|
||||||
import { getUploadDateString } from '@/util/date';
|
import { getUploadDateString } from '@/util/date';
|
||||||
import { DownloadStateIndicator } from '@/components/molecules/DownloadStateIndicator';
|
import { DownloadStateIndicator } from '@/components/molecules/DownloadStateIndicator';
|
||||||
import { DownloadType, UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
|
import { DownloadType } from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { TChapter } from '@/typings.ts';
|
import { TChapter } from '@/typings.ts';
|
||||||
import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';
|
import { ChapterActionMenuItems } from '@/components/manga/ChapterActionMenuItems.tsx';
|
||||||
|
import { Menu } from '@/components/manga/Menu.tsx';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
chapter: TChapter;
|
chapter: TChapter;
|
||||||
@@ -50,77 +41,6 @@ export const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
const { chapter, allChapters, downloadChapter: dc, showChapterNumber, onSelect, selected } = props;
|
const { chapter, allChapters, downloadChapter: dc, showChapterNumber, onSelect, selected } = props;
|
||||||
const isSelecting = selected !== null;
|
const isSelecting = selected !== null;
|
||||||
|
|
||||||
const { settings: metadataServerSettings } = useMetadataServerSettings();
|
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
|
||||||
|
|
||||||
const handleMenuClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
// prevent parent tags from getting the event
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
setAnchorEl(event.currentTarget);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setAnchorEl(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
type UpdatePatchInput = UpdateChapterPatchInput & { markPrevRead?: boolean };
|
|
||||||
const sendChange = <Key extends keyof UpdatePatchInput>(key: Key, value: UpdatePatchInput[Key]) => {
|
|
||||||
handleClose();
|
|
||||||
|
|
||||||
const shouldDeleteChapter = ({ isBookmarked, isDownloaded }: TChapter) =>
|
|
||||||
isDownloaded && (!isBookmarked || metadataServerSettings.deleteChaptersWithBookmark);
|
|
||||||
|
|
||||||
const isMarkAsRead = (key === 'isRead' && value) || key === 'markPrevRead';
|
|
||||||
const shouldAutoDeleteChapters = isMarkAsRead && metadataServerSettings.deleteChaptersManuallyMarkedRead;
|
|
||||||
|
|
||||||
const chaptersToDelete = key === 'isRead' ? [chapter] : allChapters;
|
|
||||||
const chapterIdsToDelete = shouldAutoDeleteChapters
|
|
||||||
? chaptersToDelete.filter(shouldDeleteChapter).map(({ id: chapterId }) => chapterId)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (key === 'markPrevRead') {
|
|
||||||
const index = allChapters.findIndex(({ id: chapterId }) => chapterId === chapter.id);
|
|
||||||
|
|
||||||
const isFirstChapter = index + 1 > allChapters.length - 1;
|
|
||||||
if (isFirstChapter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
requestManager.updateChapters(
|
|
||||||
allChapters
|
|
||||||
.slice(index + 1)
|
|
||||||
.filter(({ isRead }) => !isRead)
|
|
||||||
.map(({ id: chapterId }) => chapterId),
|
|
||||||
{ isRead: true, chapterIdsToDelete },
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
requestManager.updateChapter(chapter.id, {
|
|
||||||
[key]: value,
|
|
||||||
lastPageRead: key === 'isRead' ? 0 : undefined,
|
|
||||||
chapterIdToDelete: chapterIdsToDelete[0],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const downloadChapter = () => {
|
|
||||||
requestManager.addChapterToDownloadQueue(chapter.id);
|
|
||||||
handleClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteChapter = () => {
|
|
||||||
requestManager.deleteDownloadedChapter(chapter.id);
|
|
||||||
handleClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelect = () => {
|
|
||||||
onSelect(true);
|
|
||||||
handleClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||||
if (isSelecting) {
|
if (isSelecting) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -130,116 +50,113 @@ export const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { isDownloaded } = chapter;
|
const { isDownloaded } = chapter;
|
||||||
const canBeDownloaded = !chapter.isDownloaded && dc === undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<Card
|
<PopupState variant="popover" popupId="chapter-card-action-menu">
|
||||||
sx={{
|
{(popupState) => {
|
||||||
position: 'relative',
|
const bindTriggerProps = bindTrigger(popupState);
|
||||||
margin: 1,
|
|
||||||
|
const preventDefaultAction = (e: React.BaseSyntheticEvent<unknown>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickOpenMenu = (e: React.BaseSyntheticEvent<unknown>) => {
|
||||||
|
preventDefaultAction(e);
|
||||||
|
bindTriggerProps.onClick(e as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchStart = (e: React.BaseSyntheticEvent<unknown>) => {
|
||||||
|
preventDefaultAction(e);
|
||||||
|
bindTriggerProps.onTouchStart(e as TouchEvent);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
margin: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardActionArea
|
||||||
|
component={Link}
|
||||||
|
to={`/manga/${chapter.manga.id}/chapter/${chapter.sourceOrder}`}
|
||||||
|
style={{
|
||||||
|
color: theme.palette.text[chapter.isRead ? 'disabled' : 'primary'],
|
||||||
|
}}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<CardContent
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 2,
|
||||||
|
'&:last-child': { pb: 2 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack direction="column" flex={1}>
|
||||||
|
<Typography variant="h5" component="h2">
|
||||||
|
{chapter.isBookmarked && (
|
||||||
|
<BookmarkIcon
|
||||||
|
color="primary"
|
||||||
|
sx={{ mr: 0.5, position: 'relative', top: '0.15em' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showChapterNumber
|
||||||
|
? `${t('chapter.title')} ${chapter.chapterNumber}`
|
||||||
|
: chapter.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption">{chapter.scanlator}</Typography>
|
||||||
|
<Typography variant="caption">
|
||||||
|
{getUploadDateString(Number(chapter.uploadDate ?? 0))}
|
||||||
|
{isDownloaded && ` • ${t('chapter.status.label.downloaded')}`}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{dc && <DownloadStateIndicator download={dc} />}
|
||||||
|
|
||||||
|
{selected === null ? (
|
||||||
|
<Tooltip title={t('global.button.options')}>
|
||||||
|
<IconButton
|
||||||
|
{...bindTriggerProps}
|
||||||
|
onClick={handleClickOpenMenu}
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
aria-label="more"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<MoreVertIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Tooltip
|
||||||
|
title={t(selected ? 'global.button.deselect' : 'global.button.select')}
|
||||||
|
>
|
||||||
|
<Checkbox checked={selected} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</CardActionArea>
|
||||||
|
</Card>
|
||||||
|
{!isSelecting && popupState.isOpen && (
|
||||||
|
<Menu {...bindMenu(popupState)}>
|
||||||
|
{(onClose) => (
|
||||||
|
<ChapterActionMenuItems
|
||||||
|
onClose={onClose}
|
||||||
|
chapter={chapter}
|
||||||
|
allChapters={allChapters}
|
||||||
|
handleSelection={() => onSelect(true)}
|
||||||
|
canBeDownloaded={!chapter.isDownloaded && !dc}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
</PopupState>
|
||||||
<CardActionArea
|
|
||||||
component={Link}
|
|
||||||
to={`/manga/${chapter.manga.id}/chapter/${chapter.sourceOrder}`}
|
|
||||||
style={{
|
|
||||||
color: theme.palette.text[chapter.isRead ? 'disabled' : 'primary'],
|
|
||||||
}}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
<CardContent
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: 2,
|
|
||||||
'&:last-child': { pb: 2 },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack direction="column" flex={1}>
|
|
||||||
<Typography variant="h5" component="h2">
|
|
||||||
{chapter.isBookmarked && (
|
|
||||||
<BookmarkIcon
|
|
||||||
color="primary"
|
|
||||||
sx={{ mr: 0.5, position: 'relative', top: '0.15em' }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{showChapterNumber ? `${t('chapter.title')} ${chapter.chapterNumber}` : chapter.name}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption">{chapter.scanlator}</Typography>
|
|
||||||
<Typography variant="caption">
|
|
||||||
{getUploadDateString(Number(chapter.uploadDate ?? 0))}
|
|
||||||
{isDownloaded && ` • ${t('chapter.status.label.downloaded')}`}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{dc && <DownloadStateIndicator download={dc} />}
|
|
||||||
|
|
||||||
{selected === null ? (
|
|
||||||
<Tooltip title={t('global.button.options')}>
|
|
||||||
<IconButton aria-label="more" onClick={handleMenuClick} size="large">
|
|
||||||
<MoreVertIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
<Tooltip title={t(selected ? 'global.button.deselect' : 'global.button.select')}>
|
|
||||||
<Checkbox checked={selected} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</CardActionArea>
|
|
||||||
<Menu anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}>
|
|
||||||
<MenuItem onClick={handleSelect}>
|
|
||||||
<ListItemIcon>
|
|
||||||
<CheckBoxOutlineBlank fontSize="small" />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>{t('chapter.action.label.select')}</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
{isDownloaded && (
|
|
||||||
<MenuItem onClick={deleteChapter}>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Delete fontSize="small" />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>{t('chapter.action.download.delete.label.action')}</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
{canBeDownloaded && (
|
|
||||||
<MenuItem onClick={downloadChapter}>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Download fontSize="small" />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>{t('chapter.action.download.add.label.action')}</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
<MenuItem onClick={() => sendChange('isBookmarked', !chapter.isBookmarked)}>
|
|
||||||
<ListItemIcon>
|
|
||||||
{chapter.isBookmarked && <BookmarkRemove fontSize="small" />}
|
|
||||||
{!chapter.isBookmarked && <BookmarkAdd fontSize="small" />}
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>
|
|
||||||
{chapter.isBookmarked && t('chapter.action.bookmark.remove.label.action')}
|
|
||||||
{!chapter.isBookmarked && t('chapter.action.bookmark.add.label.action')}
|
|
||||||
</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClick={() => sendChange('isRead', !chapter.isRead)}>
|
|
||||||
<ListItemIcon>
|
|
||||||
{chapter.isRead && <RemoveDone fontSize="small" />}
|
|
||||||
{!chapter.isRead && <Done fontSize="small" />}
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>
|
|
||||||
{chapter.isRead && t('chapter.action.mark_as_read.remove.label.action')}
|
|
||||||
{!chapter.isRead && t('chapter.action.mark_as_read.add.label.action.current')}
|
|
||||||
</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClick={() => sendChange('markPrevRead', true)}>
|
|
||||||
<ListItemIcon>
|
|
||||||
<DoneAll fontSize="small" />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>{t('chapter.action.mark_as_read.add.label.action.previous')}</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</Card>
|
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,27 +8,27 @@
|
|||||||
|
|
||||||
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 } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
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';
|
||||||
import { TChapter, TManga, TranslationKey } from '@/typings';
|
import { TChapter, TManga } from '@/typings';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { ChapterCard } from '@/components/manga/ChapterCard';
|
import { ChapterCard } from '@/components/manga/ChapterCard';
|
||||||
import { ResumeFab } from '@/components/manga/ResumeFAB';
|
import { ResumeFab } from '@/components/manga/ResumeFAB';
|
||||||
import { filterAndSortChapters, useChapterOptions } from '@/components/manga/util';
|
import { filterAndSortChapters, useChapterOptions } from '@/components/manga/util';
|
||||||
import { EmptyView } from '@/components/util/EmptyView';
|
import { EmptyView } from '@/components/util/EmptyView';
|
||||||
import { makeToast } from '@/components/util/Toast';
|
|
||||||
import { ChaptersToolbarMenu } from '@/components/manga/ChaptersToolbarMenu';
|
import { ChaptersToolbarMenu } from '@/components/manga/ChaptersToolbarMenu';
|
||||||
import { SelectionFAB } from '@/components/manga/SelectionFAB';
|
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 } from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';
|
|
||||||
import { useSelectableCollection } from '@/components/collection/useSelectableCollection.ts';
|
import { useSelectableCollection } from '@/components/collection/useSelectableCollection.ts';
|
||||||
import { SelectableCollectionSelectAll } from '@/components/collection/SelectableCollectionSelectAll.tsx';
|
import { SelectableCollectionSelectAll } from '@/components/collection/SelectableCollectionSelectAll.tsx';
|
||||||
import { ChapterSelectionFABActionItems } from '@/components/manga/ChapterSelectionFABActionItems.tsx';
|
import { Chapters } from '@/lib/data/Chapters.ts';
|
||||||
|
import { ChaptersWithMeta } from '@/lib/data/ChaptersWithMeta.ts';
|
||||||
|
import { ChapterActionMenuItems } from '@/components/manga/ChapterActionMenuItems.tsx';
|
||||||
|
|
||||||
const ChapterListHeader = styled(Stack)(({ theme }) => ({
|
const ChapterListHeader = styled(Stack)(({ theme }) => ({
|
||||||
margin: 8,
|
margin: 8,
|
||||||
@@ -52,38 +52,6 @@ const StyledVirtuoso = styled(Virtuoso)(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const actionsStrings: {
|
|
||||||
[key in 'download' | 'delete' | 'bookmark' | 'unbookmark' | 'mark_as_read' | 'mark_as_unread']: {
|
|
||||||
success: TranslationKey;
|
|
||||||
error: TranslationKey;
|
|
||||||
};
|
|
||||||
} = {
|
|
||||||
download: {
|
|
||||||
success: 'chapter.action.download.add.label.success',
|
|
||||||
error: 'chapter.action.download.add.label.error',
|
|
||||||
},
|
|
||||||
delete: {
|
|
||||||
success: 'chapter.action.download.delete.label.success',
|
|
||||||
error: 'chapter.action.download.delete.label.error',
|
|
||||||
},
|
|
||||||
bookmark: {
|
|
||||||
success: 'chapter.action.bookmark.add.label.success',
|
|
||||||
error: 'chapter.action.bookmark.add.label.error',
|
|
||||||
},
|
|
||||||
unbookmark: {
|
|
||||||
success: 'chapter.action.bookmark.remove.label.success',
|
|
||||||
error: 'chapter.action.bookmark.remove.label.error',
|
|
||||||
},
|
|
||||||
mark_as_read: {
|
|
||||||
success: 'chapter.action.mark_as_read.add.label.success',
|
|
||||||
error: 'chapter.action.mark_as_read.add.label.error',
|
|
||||||
},
|
|
||||||
mark_as_unread: {
|
|
||||||
success: 'chapter.action.mark_as_read.remove.label.success',
|
|
||||||
error: 'chapter.action.mark_as_read.remove.label.error',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface IChapterWithMeta {
|
export interface IChapterWithMeta {
|
||||||
chapter: TChapter;
|
chapter: TChapter;
|
||||||
downloadChapter: DownloadType | undefined;
|
downloadChapter: DownloadType | undefined;
|
||||||
@@ -108,8 +76,6 @@ export const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
|
|||||||
const { areNoItemsSelected, areAllItemsSelected, selectedItemIds, handleSelectAll, handleSelection } =
|
const { areNoItemsSelected, areAllItemsSelected, selectedItemIds, handleSelectAll, handleSelection } =
|
||||||
useSelectableCollection(chapters.length, { currentKey: 'default' });
|
useSelectableCollection(chapters.length, { currentKey: 'default' });
|
||||||
|
|
||||||
const { settings: metadataServerSettings } = useMetadataServerSettings();
|
|
||||||
|
|
||||||
const visibleChapters = useMemo(() => filterAndSortChapters(chapters, options), [chapters, options]);
|
const visibleChapters = useMemo(() => filterAndSortChapters(chapters, options), [chapters, options]);
|
||||||
|
|
||||||
const nextChapterIndexToRead = (manga.lastReadChapter?.sourceOrder ?? 0) + 1;
|
const nextChapterIndexToRead = (manga.lastReadChapter?.sourceOrder ?? 0) + 1;
|
||||||
@@ -118,70 +84,6 @@ export const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
|
|||||||
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 handleFabAction: ComponentProps<typeof ChapterSelectionFABActionItems>['onAction'] = (
|
|
||||||
action,
|
|
||||||
actionChapters,
|
|
||||||
) => {
|
|
||||||
if (actionChapters.length === 0) return;
|
|
||||||
const chapterIds = actionChapters
|
|
||||||
.filter(({ chapter }) => {
|
|
||||||
switch (action) {
|
|
||||||
case 'download':
|
|
||||||
return !chapter.isDownloaded;
|
|
||||||
case 'delete':
|
|
||||||
return chapter.isDownloaded;
|
|
||||||
case 'bookmark':
|
|
||||||
return !chapter.isBookmarked;
|
|
||||||
case 'unbookmark':
|
|
||||||
return chapter.isBookmarked;
|
|
||||||
case 'mark_as_read':
|
|
||||||
return !chapter.isRead;
|
|
||||||
case 'mark_as_unread':
|
|
||||||
return chapter.isRead;
|
|
||||||
default:
|
|
||||||
throw new Error(`ChapterList::handleFabAction: unknown action "${action}"`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(({ chapter }) => chapter.id);
|
|
||||||
|
|
||||||
let actionPromise: Promise<any>;
|
|
||||||
|
|
||||||
if (action === 'download') {
|
|
||||||
actionPromise = requestManager.addChaptersToDownloadQueue(chapterIds).response;
|
|
||||||
} else {
|
|
||||||
const change: UpdateChapterPatchInput = {};
|
|
||||||
|
|
||||||
if (action === 'bookmark') change.isBookmarked = true;
|
|
||||||
else if (action === 'unbookmark') change.isBookmarked = false;
|
|
||||||
else if (action === 'mark_as_read' || action === 'mark_as_unread') {
|
|
||||||
change.isRead = action === 'mark_as_read';
|
|
||||||
change.lastPageRead = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === 'delete') {
|
|
||||||
actionPromise = requestManager.deleteDownloadedChapters(chapterIds).response;
|
|
||||||
} else {
|
|
||||||
const shouldDeleteChapters =
|
|
||||||
action === 'mark_as_read' && metadataServerSettings.deleteChaptersManuallyMarkedRead;
|
|
||||||
const chapterIdsToDelete = shouldDeleteChapters
|
|
||||||
? actionChapters
|
|
||||||
.filter(
|
|
||||||
({ chapter }) =>
|
|
||||||
chapter.isDownloaded &&
|
|
||||||
(!chapter.isBookmarked || metadataServerSettings.deleteChaptersWithBookmark),
|
|
||||||
)
|
|
||||||
.map(({ chapter }) => chapter.id)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
actionPromise = requestManager.updateChapters(chapterIds, { ...change, chapterIdsToDelete }).response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
actionPromise
|
|
||||||
.then(() => makeToast(t(actionsStrings[action].success, { count: chapterIds.length }), 'success'))
|
|
||||||
.catch(() => makeToast(t(actionsStrings[action].error, { count: chapterIds.length }), 'error'));
|
|
||||||
};
|
|
||||||
|
|
||||||
const noChaptersFound = chapters.length === 0;
|
const noChaptersFound = chapters.length === 0;
|
||||||
const noChaptersMatchingFilter = !noChaptersFound && visibleChapters.length === 0;
|
const noChaptersMatchingFilter = !noChaptersFound && visibleChapters.length === 0;
|
||||||
|
|
||||||
@@ -208,11 +110,7 @@ export const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
|
|||||||
return (
|
return (
|
||||||
<SelectionFAB selectedItemsCount={selectedChapters.length} title="chapter.title">
|
<SelectionFAB selectedItemsCount={selectedChapters.length} title="chapter.title">
|
||||||
{(handleClose) => (
|
{(handleClose) => (
|
||||||
<ChapterSelectionFABActionItems
|
<ChapterActionMenuItems selectedChapters={selectedChapters} onClose={handleClose} />
|
||||||
selectedChapters={selectedChapters}
|
|
||||||
onAction={handleFabAction}
|
|
||||||
handleClose={handleClose}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</SelectionFAB>
|
</SelectionFAB>
|
||||||
);
|
);
|
||||||
@@ -253,7 +151,12 @@ export const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
|
|||||||
<Tooltip title={t('chapter.action.mark_as_read.add.label.action.current')}>
|
<Tooltip title={t('chapter.action.mark_as_read.add.label.action.current')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={areAllChaptersRead}
|
disabled={areAllChaptersRead}
|
||||||
onClick={() => handleFabAction('mark_as_read', chaptersWithMeta)}
|
onClick={() =>
|
||||||
|
Chapters.markAsRead(
|
||||||
|
ChaptersWithMeta.getChapters(ChaptersWithMeta.getNonRead(chaptersWithMeta)),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<DoneAllIcon />
|
<DoneAllIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -261,7 +164,11 @@ export const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
|
|||||||
<Tooltip title={t('chapter.action.download.add.label.action')}>
|
<Tooltip title={t('chapter.action.download.add.label.action')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={areAllChaptersDownloaded}
|
disabled={areAllChaptersDownloaded}
|
||||||
onClick={() => handleFabAction('download', chaptersWithMeta)}
|
onClick={() =>
|
||||||
|
Chapters.download(
|
||||||
|
ChaptersWithMeta.getIds(ChaptersWithMeta.getNonDownloaded(chaptersWithMeta)),
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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')}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -7,8 +7,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Menu from '@mui/material/Menu';
|
import Menu from '@mui/material/Menu';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
|
||||||
import { ListItemIcon, ListItemText } from '@mui/material';
|
|
||||||
import CheckBoxOutlineBlank from '@mui/icons-material/CheckBoxOutlineBlank';
|
import CheckBoxOutlineBlank from '@mui/icons-material/CheckBoxOutlineBlank';
|
||||||
import Delete from '@mui/icons-material/Delete';
|
import Delete from '@mui/icons-material/Delete';
|
||||||
import Download from '@mui/icons-material/Download';
|
import Download from '@mui/icons-material/Download';
|
||||||
@@ -22,8 +20,8 @@ import { useState } from 'react';
|
|||||||
import { TManga } from '@/typings.ts';
|
import { TManga } from '@/typings.ts';
|
||||||
import { MangaAction, MangaDownloadInfo, Mangas, MangaUnreadInfo } from '@/lib/data/Mangas.ts';
|
import { MangaAction, MangaDownloadInfo, Mangas, MangaUnreadInfo } from '@/lib/data/Mangas.ts';
|
||||||
import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts';
|
import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts';
|
||||||
import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';
|
|
||||||
import { CategorySelect } from '@/components/navbar/action/CategorySelect.tsx';
|
import { CategorySelect } from '@/components/navbar/action/CategorySelect.tsx';
|
||||||
|
import { MenuItem } from '@/components/manga/MenuItem.tsx';
|
||||||
|
|
||||||
export const MangaActionMenu = ({
|
export const MangaActionMenu = ({
|
||||||
manga,
|
manga,
|
||||||
@@ -35,7 +33,6 @@ export const MangaActionMenu = ({
|
|||||||
} & ReturnType<typeof bindMenu>) => {
|
} & ReturnType<typeof bindMenu>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { settings } = useMetadataServerSettings();
|
|
||||||
const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false);
|
const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false);
|
||||||
|
|
||||||
const isFullyDownloaded = manga.downloadCount === manga.chapters.totalCount;
|
const isFullyDownloaded = manga.downloadCount === manga.chapters.totalCount;
|
||||||
@@ -50,7 +47,7 @@ export const MangaActionMenu = ({
|
|||||||
|
|
||||||
const performAction = (action: MangaAction) => {
|
const performAction = (action: MangaAction) => {
|
||||||
Mangas.performAction(action, [manga.id], {
|
Mangas.performAction(action, [manga.id], {
|
||||||
autoDeleteChapters: settings.deleteChaptersManuallyMarkedRead,
|
wasManuallyMarkedAsRead: true,
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
|
|
||||||
bindMenuProps.onClose();
|
bindMenuProps.onClose();
|
||||||
@@ -60,57 +57,50 @@ export const MangaActionMenu = ({
|
|||||||
<>
|
<>
|
||||||
<Menu {...bindMenuProps} open={bindMenuProps.open && !isCategorySelectOpen}>
|
<Menu {...bindMenuProps} open={bindMenuProps.open && !isCategorySelectOpen}>
|
||||||
{!!handleSelection && (
|
{!!handleSelection && (
|
||||||
<MenuItem onClick={handleSelect}>
|
<MenuItem
|
||||||
<ListItemIcon>
|
onClick={handleSelect}
|
||||||
<CheckBoxOutlineBlank fontSize="small" />
|
Icon={CheckBoxOutlineBlank}
|
||||||
</ListItemIcon>
|
title={t('chapter.action.label.select')}
|
||||||
<ListItemText>{t('chapter.action.label.select')}</ListItemText>
|
/>
|
||||||
</MenuItem>
|
|
||||||
)}
|
)}
|
||||||
{!isFullyDownloaded && (
|
{!isFullyDownloaded && (
|
||||||
<MenuItem onClick={() => performAction('download')}>
|
<MenuItem
|
||||||
<ListItemIcon>
|
onClick={() => performAction('download')}
|
||||||
<Download fontSize="small" />
|
Icon={Download}
|
||||||
</ListItemIcon>
|
title={t('chapter.action.download.add.label.action')}
|
||||||
<ListItemText>{t('chapter.action.download.add.label.action')}</ListItemText>
|
/>
|
||||||
</MenuItem>
|
|
||||||
)}
|
)}
|
||||||
{hasDownloadedChapters && (
|
{hasDownloadedChapters && (
|
||||||
<MenuItem onClick={() => performAction('delete')}>
|
<MenuItem
|
||||||
<ListItemIcon>
|
onClick={() => performAction('delete')}
|
||||||
<Delete fontSize="small" />
|
Icon={Delete}
|
||||||
</ListItemIcon>
|
title={t('chapter.action.download.delete.label.action')}
|
||||||
<ListItemText>{t('chapter.action.download.delete.label.action')}</ListItemText>
|
/>
|
||||||
</MenuItem>
|
|
||||||
)}
|
)}
|
||||||
{hasUnreadChapters && (
|
{hasUnreadChapters && (
|
||||||
<MenuItem onClick={() => performAction('mark_as_read')}>
|
<MenuItem
|
||||||
<ListItemIcon>
|
onClick={() => performAction('mark_as_read')}
|
||||||
<Done fontSize="small" />
|
Icon={Done}
|
||||||
</ListItemIcon>
|
title={t('chapter.action.mark_as_read.add.label.action.current')}
|
||||||
<ListItemText>{t('chapter.action.mark_as_read.add.label.action.current')}</ListItemText>
|
/>
|
||||||
</MenuItem>
|
|
||||||
)}
|
)}
|
||||||
{hasReadChapters && (
|
{hasReadChapters && (
|
||||||
<MenuItem onClick={() => performAction('mark_as_unread')}>
|
<MenuItem
|
||||||
<ListItemIcon>
|
onClick={() => performAction('mark_as_unread')}
|
||||||
<RemoveDone fontSize="small" />
|
Icon={RemoveDone}
|
||||||
</ListItemIcon>
|
title={t('chapter.action.mark_as_read.remove.label.action')}
|
||||||
<ListItemText>{t('chapter.action.mark_as_read.remove.label.action')}</ListItemText>
|
/>
|
||||||
</MenuItem>
|
|
||||||
)}
|
)}
|
||||||
<MenuItem onClick={() => setIsCategorySelectOpen(true)}>
|
<MenuItem
|
||||||
<ListItemIcon>
|
onClick={() => setIsCategorySelectOpen(true)}
|
||||||
<Label fontSize="small" />
|
Icon={Label}
|
||||||
</ListItemIcon>
|
title={t('manga.action.category.label.action')}
|
||||||
<ListItemText>{t('manga.action.category.label.action')}</ListItemText>
|
/>
|
||||||
</MenuItem>
|
<MenuItem
|
||||||
<MenuItem onClick={() => performAction('remove_from_library')}>
|
onClick={() => performAction('remove_from_library')}
|
||||||
<ListItemIcon>
|
Icon={FavoriteBorderIcon}
|
||||||
<FavoriteBorderIcon fontSize="small" />
|
title={t('manga.action.library.remove.label.action')}
|
||||||
</ListItemIcon>
|
/>
|
||||||
<ListItemText>{t('manga.action.library.remove.label.action')}</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
</Menu>
|
||||||
{isCategorySelectOpen && (
|
{isCategorySelectOpen && (
|
||||||
<CategorySelect
|
<CategorySelect
|
||||||
|
|||||||
@@ -10,18 +10,22 @@ import Download from '@mui/icons-material/Download';
|
|||||||
import Delete from '@mui/icons-material/Delete';
|
import Delete from '@mui/icons-material/Delete';
|
||||||
import Done from '@mui/icons-material/Done';
|
import Done from '@mui/icons-material/Done';
|
||||||
import RemoveDone from '@mui/icons-material/RemoveDone';
|
import RemoveDone from '@mui/icons-material/RemoveDone';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
|
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
|
||||||
import Label from '@mui/icons-material/Label';
|
import Label from '@mui/icons-material/Label';
|
||||||
import { useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { SelectionFABActionItem } from '@/components/manga/SelectionFABActionItem.tsx';
|
import { t as translate } from 'i18next';
|
||||||
|
import { MenuItem } from '@/components/manga/MenuItem.tsx';
|
||||||
import { TManga } from '@/typings.ts';
|
import { TManga } from '@/typings.ts';
|
||||||
import { MangaAction, Mangas } from '@/lib/data/Mangas.ts';
|
import { actionToTranslationKey, MangaAction, Mangas } from '@/lib/data/Mangas.ts';
|
||||||
import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';
|
|
||||||
import { CategorySelect } from '@/components/navbar/action/CategorySelect.tsx';
|
import { CategorySelect } from '@/components/navbar/action/CategorySelect.tsx';
|
||||||
|
|
||||||
const ACTION_DISABLES_SELECTION_MODE: MangaAction[] = ['remove_from_library'] as const;
|
const ACTION_DISABLES_SELECTION_MODE: MangaAction[] = ['remove_from_library'] as const;
|
||||||
|
|
||||||
|
const getMenuItemTitle = (action: MangaAction, count: number): string => {
|
||||||
|
const countSuffix = count > 0 ? ` (${count})` : '';
|
||||||
|
return `${translate(actionToTranslationKey[action].action.selected)}${countSuffix}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const MangasSelectionFABActionItems = ({
|
export const MangasSelectionFABActionItems = ({
|
||||||
selectedMangas,
|
selectedMangas,
|
||||||
handleClose,
|
handleClose,
|
||||||
@@ -29,66 +33,66 @@ export const MangasSelectionFABActionItems = ({
|
|||||||
selectedMangas: TManga[];
|
selectedMangas: TManga[];
|
||||||
handleClose: (selectionModeState: boolean) => void;
|
handleClose: (selectionModeState: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const { settings } = useMetadataServerSettings();
|
|
||||||
const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false);
|
const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false);
|
||||||
|
|
||||||
const handleAction = (action: MangaAction, mangas: TManga[]) => {
|
const handleAction = (action: MangaAction, mangas: TManga[]) => {
|
||||||
Mangas.performAction(action, Mangas.getIds(mangas), {
|
Mangas.performAction(action, Mangas.getIds(mangas), {
|
||||||
autoDeleteChapters: settings.deleteChaptersManuallyMarkedRead,
|
wasManuallyMarkedAsRead: true,
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
handleClose(!ACTION_DISABLES_SELECTION_MODE.includes(action));
|
handleClose(!ACTION_DISABLES_SELECTION_MODE.includes(action));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { downloadableMangas, downloadedMangas, unreadMangas, readMangas } = useMemo(
|
||||||
|
() => ({
|
||||||
|
downloadableMangas: [
|
||||||
|
...Mangas.getNotDownloaded(selectedMangas),
|
||||||
|
...Mangas.getPartiallyDownloaded(selectedMangas),
|
||||||
|
],
|
||||||
|
downloadedMangas: [
|
||||||
|
...Mangas.getPartiallyDownloaded(selectedMangas),
|
||||||
|
...Mangas.getFullyDownloaded(selectedMangas),
|
||||||
|
],
|
||||||
|
unreadMangas: [...Mangas.getUnread(selectedMangas), ...Mangas.getPartiallyRead(selectedMangas)],
|
||||||
|
readMangas: [...Mangas.getPartiallyRead(selectedMangas), ...Mangas.getFullyRead(selectedMangas)],
|
||||||
|
}),
|
||||||
|
[selectedMangas],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SelectionFABActionItem<MangaAction, TManga>
|
<MenuItem
|
||||||
action="download"
|
|
||||||
Icon={Download}
|
Icon={Download}
|
||||||
matchingItems={[
|
isDisabled={!downloadableMangas.length}
|
||||||
...Mangas.getNotDownloaded(selectedMangas),
|
onClick={() => handleAction('download', downloadableMangas)}
|
||||||
...Mangas.getPartiallyDownloaded(selectedMangas),
|
title={getMenuItemTitle('download', downloadableMangas.length)}
|
||||||
]}
|
|
||||||
onClick={handleAction}
|
|
||||||
title={t('chapter.action.download.add.button.selected')}
|
|
||||||
/>
|
/>
|
||||||
<SelectionFABActionItem<MangaAction, TManga>
|
<MenuItem
|
||||||
action="delete"
|
|
||||||
Icon={Delete}
|
Icon={Delete}
|
||||||
matchingItems={[
|
isDisabled={!downloadedMangas.length}
|
||||||
...Mangas.getPartiallyDownloaded(selectedMangas),
|
onClick={() => handleAction('delete', downloadedMangas)}
|
||||||
...Mangas.getFullyDownloaded(selectedMangas),
|
title={getMenuItemTitle('delete', downloadedMangas.length)}
|
||||||
]}
|
|
||||||
onClick={handleAction}
|
|
||||||
title={t('chapter.action.download.delete.button.selected')}
|
|
||||||
/>
|
/>
|
||||||
<SelectionFABActionItem<MangaAction, TManga>
|
<MenuItem
|
||||||
action="mark_as_read"
|
|
||||||
Icon={Done}
|
Icon={Done}
|
||||||
matchingItems={[...Mangas.getUnread(selectedMangas), ...Mangas.getPartiallyRead(selectedMangas)]}
|
isDisabled={!unreadMangas.length}
|
||||||
onClick={handleAction}
|
onClick={() => handleAction('mark_as_read', unreadMangas)}
|
||||||
title={t('chapter.action.mark_as_read.add.button.selected')}
|
title={getMenuItemTitle('mark_as_read', unreadMangas.length)}
|
||||||
/>
|
/>
|
||||||
<SelectionFABActionItem<MangaAction, TManga>
|
<MenuItem
|
||||||
action="mark_as_unread"
|
|
||||||
Icon={RemoveDone}
|
Icon={RemoveDone}
|
||||||
matchingItems={[...Mangas.getPartiallyRead(selectedMangas), ...Mangas.getFullyRead(selectedMangas)]}
|
isDisabled={!readMangas.length}
|
||||||
onClick={handleAction}
|
onClick={() => handleAction('mark_as_unread', readMangas)}
|
||||||
title={t('chapter.action.mark_as_read.remove.button.selected')}
|
title={getMenuItemTitle('mark_as_unread', readMangas.length)}
|
||||||
/>
|
/>
|
||||||
<SelectionFABActionItem<MangaAction, TManga>
|
<MenuItem
|
||||||
action="change_categories"
|
|
||||||
Icon={Label}
|
Icon={Label}
|
||||||
matchingItems={selectedMangas}
|
onClick={() => handleAction('change_categories', selectedMangas)}
|
||||||
onClick={() => setIsCategorySelectOpen(true)}
|
title={getMenuItemTitle('change_categories', selectedMangas.length)}
|
||||||
title={t('manga.action.category.label.action')}
|
|
||||||
/>
|
/>
|
||||||
<SelectionFABActionItem<MangaAction, TManga>
|
<MenuItem
|
||||||
action="remove_from_library"
|
|
||||||
Icon={FavoriteBorderIcon}
|
Icon={FavoriteBorderIcon}
|
||||||
matchingItems={[...Mangas.getPartiallyRead(selectedMangas), ...Mangas.getFullyRead(selectedMangas)]}
|
onClick={() => handleAction('remove_from_library', selectedMangas)}
|
||||||
onClick={handleAction}
|
title={getMenuItemTitle('remove_from_library', selectedMangas.length)}
|
||||||
title={t('manga.action.library.remove.button.selected')}
|
|
||||||
/>
|
/>
|
||||||
{isCategorySelectOpen && (
|
{isCategorySelectOpen && (
|
||||||
<CategorySelect
|
<CategorySelect
|
||||||
|
|||||||
21
src/components/manga/Menu.tsx
Normal file
21
src/components/manga/Menu.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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 { MenuProps } from '@mui/material/Menu/Menu';
|
||||||
|
import { Menu as MuiMenu } from '@mui/material';
|
||||||
|
|
||||||
|
export const Menu = ({
|
||||||
|
children,
|
||||||
|
onClose,
|
||||||
|
...props
|
||||||
|
}: Omit<MenuProps, 'children' | 'onClose'> &
|
||||||
|
Required<Pick<MenuProps, 'onClose'>> & { children: (onClose: () => void) => JSX.Element }) => (
|
||||||
|
<MuiMenu {...props} onClose={onClose}>
|
||||||
|
{children(() => onClose({}, 'backdropClick'))}
|
||||||
|
</MuiMenu>
|
||||||
|
);
|
||||||
27
src/components/manga/MenuItem.tsx
Normal file
27
src/components/manga/MenuItem.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* 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 { ListItemIcon, ListItemText, MenuItem as MuiMenuItem } from '@mui/material';
|
||||||
|
import { OverridableComponent } from '@mui/material/OverridableComponent';
|
||||||
|
import { SvgIconTypeMap } from '@mui/material/SvgIcon/SvgIcon';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
title: string;
|
||||||
|
Icon: OverridableComponent<SvgIconTypeMap> & { muiName: string };
|
||||||
|
onClick: () => void;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MenuItem = ({ onClick, title, Icon, isDisabled }: IProps) => (
|
||||||
|
<MuiMenuItem onClick={onClick} disabled={isDisabled}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Icon fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>{title}</ListItemText>
|
||||||
|
</MuiMenuItem>
|
||||||
|
);
|
||||||
@@ -7,14 +7,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
||||||
import { Fab, Menu, Box, styled } from '@mui/material';
|
import { Fab, Box, styled } from '@mui/material';
|
||||||
import React, { useRef, useState } from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
|
||||||
import { DEFAULT_FAB_STYLE } from '@/components/util/StyledFab';
|
import { DEFAULT_FAB_STYLE } from '@/components/util/StyledFab';
|
||||||
import { TranslationKey } from '@/typings.ts';
|
import { TranslationKey } from '@/typings.ts';
|
||||||
|
import { Menu } from '@/components/manga/Menu.tsx';
|
||||||
|
|
||||||
interface SelectionFABProps {
|
interface SelectionFABProps {
|
||||||
children: (handleClose: () => void) => React.ReactNode;
|
children: (handleClose: () => void) => JSX.Element;
|
||||||
selectedItemsCount: number;
|
selectedItemsCount: number;
|
||||||
title: TranslationKey;
|
title: TranslationKey;
|
||||||
}
|
}
|
||||||
@@ -32,29 +34,29 @@ const FabContainer = styled(Box)(({ theme }) => ({
|
|||||||
export const SelectionFAB: React.FC<SelectionFABProps> = ({ children, selectedItemsCount, title }) => {
|
export const SelectionFAB: React.FC<SelectionFABProps> = ({ children, selectedItemsCount, title }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const anchorEl = useRef<HTMLElement>();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const handleClose = () => setOpen(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FabContainer ref={anchorEl}>
|
<PopupState variant="popover" popupId="selection-fab-menu">
|
||||||
<Fab variant="extended" color="primary" id="selectionMenuButton" onClick={() => setOpen(true)}>
|
{(popupState) => (
|
||||||
{`${selectedItemsCount} ${t(title, { count: selectedItemsCount })}`}
|
<>
|
||||||
<MoreHoriz sx={{ ml: 1 }} />
|
<FabContainer {...bindTrigger(popupState)}>
|
||||||
</Fab>
|
<Fab variant="extended" color="primary" id="selectionMenuButton">
|
||||||
<Menu
|
{`${selectedItemsCount} ${t(title, { count: selectedItemsCount })}`}
|
||||||
id="selectionMenu"
|
<MoreHoriz sx={{ ml: 1 }} />
|
||||||
anchorEl={anchorEl.current}
|
</Fab>
|
||||||
open={open}
|
</FabContainer>
|
||||||
onClose={handleClose}
|
<Menu
|
||||||
anchorOrigin={{ horizontal: 'right', vertical: 'top' }}
|
{...bindMenu(popupState)}
|
||||||
transformOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
id="selectionMenu"
|
||||||
MenuListProps={{
|
anchorOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||||
'aria-labelledby': 'selectionMenuButton',
|
transformOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
||||||
}}
|
MenuListProps={{
|
||||||
>
|
'aria-labelledby': 'selectionMenuButton',
|
||||||
{children(handleClose)}
|
}}
|
||||||
</Menu>
|
>
|
||||||
</FabContainer>
|
{(onClose) => children(onClose)}
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</PopupState>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { ListItemIcon, ListItemText, MenuItem } from '@mui/material';
|
|
||||||
import { OverridableComponent } from '@mui/material/OverridableComponent';
|
|
||||||
import { SvgIconTypeMap } from '@mui/material/SvgIcon/SvgIcon';
|
|
||||||
|
|
||||||
interface IProps<Action, Item> {
|
|
||||||
action: Action;
|
|
||||||
matchingItems: Item[];
|
|
||||||
title: string;
|
|
||||||
Icon: OverridableComponent<SvgIconTypeMap> & { muiName: string };
|
|
||||||
onClick: (action: Action, items: Item[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SelectionFABActionItem = <Action extends string, Item>({
|
|
||||||
action,
|
|
||||||
matchingItems,
|
|
||||||
onClick,
|
|
||||||
title,
|
|
||||||
Icon,
|
|
||||||
}: IProps<Action, Item>) => {
|
|
||||||
const count = matchingItems.length;
|
|
||||||
return (
|
|
||||||
<MenuItem onClick={() => onClick(action, matchingItems)} disabled={count === 0}>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Icon fontSize="small" />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText>
|
|
||||||
{title}
|
|
||||||
{count > 0 ? ` (${count})` : ''}
|
|
||||||
</ListItemText>
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -6,25 +6,97 @@
|
|||||||
* 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 { TChapter } from '@/typings.ts';
|
import { t as translate } from 'i18next';
|
||||||
|
import { TChapter, TranslationKey } from '@/typings.ts';
|
||||||
|
import { makeToast } from '@/components/util/Toast.tsx';
|
||||||
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
|
import { getMetadataServerSettings } from '@/util/metadataServerSettings.ts';
|
||||||
|
|
||||||
type ChapterDownloadInfo = Pick<TChapter, 'isDownloaded'>;
|
export type ChapterAction = 'download' | 'delete' | 'bookmark' | 'unbookmark' | 'mark_as_read' | 'mark_as_unread';
|
||||||
type ChapterBookmarkInfo = Pick<TChapter, 'isBookmarked'>;
|
|
||||||
|
export const actionToTranslationKey: {
|
||||||
|
[key in ChapterAction]: {
|
||||||
|
action: {
|
||||||
|
single: TranslationKey;
|
||||||
|
selected: TranslationKey;
|
||||||
|
};
|
||||||
|
success: TranslationKey;
|
||||||
|
error: TranslationKey;
|
||||||
|
};
|
||||||
|
} = {
|
||||||
|
download: {
|
||||||
|
action: {
|
||||||
|
single: 'chapter.action.download.add.label.action',
|
||||||
|
selected: 'chapter.action.download.add.button.selected',
|
||||||
|
},
|
||||||
|
success: 'chapter.action.download.add.label.success',
|
||||||
|
error: 'chapter.action.download.add.label.error',
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
action: {
|
||||||
|
single: 'chapter.action.download.delete.label.action',
|
||||||
|
selected: 'chapter.action.download.delete.button.selected',
|
||||||
|
},
|
||||||
|
success: 'chapter.action.download.delete.label.success',
|
||||||
|
error: 'chapter.action.download.delete.label.error',
|
||||||
|
},
|
||||||
|
bookmark: {
|
||||||
|
action: {
|
||||||
|
single: 'chapter.action.bookmark.add.label.action',
|
||||||
|
selected: 'chapter.action.bookmark.add.button.selected',
|
||||||
|
},
|
||||||
|
success: 'chapter.action.bookmark.add.label.success',
|
||||||
|
error: 'chapter.action.bookmark.add.label.error',
|
||||||
|
},
|
||||||
|
unbookmark: {
|
||||||
|
action: {
|
||||||
|
single: 'chapter.action.bookmark.remove.label.action',
|
||||||
|
selected: 'chapter.action.bookmark.remove.button.selected',
|
||||||
|
},
|
||||||
|
success: 'chapter.action.bookmark.remove.label.success',
|
||||||
|
error: 'chapter.action.bookmark.remove.label.error',
|
||||||
|
},
|
||||||
|
mark_as_read: {
|
||||||
|
action: {
|
||||||
|
single: 'chapter.action.mark_as_read.add.label.action.current',
|
||||||
|
selected: 'chapter.action.mark_as_read.add.button.selected',
|
||||||
|
},
|
||||||
|
success: 'chapter.action.mark_as_read.add.label.success',
|
||||||
|
error: 'chapter.action.mark_as_read.add.label.error',
|
||||||
|
},
|
||||||
|
mark_as_unread: {
|
||||||
|
action: {
|
||||||
|
single: 'chapter.action.mark_as_read.remove.label.action',
|
||||||
|
selected: 'chapter.action.mark_as_read.remove.button.selected',
|
||||||
|
},
|
||||||
|
success: 'chapter.action.mark_as_read.remove.label.success',
|
||||||
|
error: 'chapter.action.mark_as_read.remove.label.error',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChapterIdInfo = Pick<TChapter, 'id'>;
|
||||||
|
export type ChapterDownloadInfo = ChapterIdInfo & Pick<TChapter, 'isDownloaded'>;
|
||||||
|
export type ChapterBookmarkInfo = ChapterIdInfo & Pick<TChapter, 'isBookmarked'>;
|
||||||
|
export type ChapterReadInfo = ChapterIdInfo & Pick<TChapter, 'isRead'>;
|
||||||
|
|
||||||
export class Chapters {
|
export class Chapters {
|
||||||
static getIds(chapters: { id: number }[]): number[] {
|
static getIds(chapters: { id: number }[]): number[] {
|
||||||
return chapters.map((chapter) => chapter.id);
|
return chapters.map((chapter) => chapter.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
static isDeletable({ isDownloaded }: ChapterDownloadInfo): boolean {
|
static isDownloaded({ isDownloaded }: ChapterDownloadInfo): boolean {
|
||||||
return isDownloaded;
|
return isDownloaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getDownloaded<Chapter extends ChapterDownloadInfo>(chapters: Chapter[]): Chapter[] {
|
||||||
|
return chapters.filter(Chapters.isDownloaded);
|
||||||
|
}
|
||||||
|
|
||||||
static isAutoDeletable(
|
static isAutoDeletable(
|
||||||
{ isBookmarked, ...chapter }: ChapterDownloadInfo & ChapterBookmarkInfo,
|
{ isBookmarked, ...chapter }: ChapterDownloadInfo & ChapterBookmarkInfo,
|
||||||
canDeleteBookmarked: boolean = false,
|
canDeleteBookmarked: boolean = false,
|
||||||
): boolean {
|
): boolean {
|
||||||
return Chapters.isDeletable(chapter) && (!isBookmarked || canDeleteBookmarked);
|
return Chapters.isDownloaded(chapter) && (!isBookmarked || canDeleteBookmarked);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getAutoDeletable<Chapters extends ChapterDownloadInfo & ChapterBookmarkInfo>(
|
static getAutoDeletable<Chapters extends ChapterDownloadInfo & ChapterBookmarkInfo>(
|
||||||
@@ -33,4 +105,139 @@ export class Chapters {
|
|||||||
): Chapters[] {
|
): Chapters[] {
|
||||||
return chapters.filter((chapter) => Chapters.isAutoDeletable(chapter, canDeleteBookmarked));
|
return chapters.filter((chapter) => Chapters.isAutoDeletable(chapter, canDeleteBookmarked));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static isBookmarked({ isBookmarked }: ChapterBookmarkInfo): boolean {
|
||||||
|
return isBookmarked;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getBookmarked<Chapter extends ChapterBookmarkInfo>(chapters: Chapter[]): Chapter[] {
|
||||||
|
return chapters.filter(Chapters.isBookmarked);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getNonBookmarked<Chapter extends ChapterBookmarkInfo>(chapters: Chapter[]): Chapter[] {
|
||||||
|
return chapters.filter((chapter) => !Chapters.isBookmarked(chapter));
|
||||||
|
}
|
||||||
|
|
||||||
|
static isRead({ isRead }: ChapterReadInfo): boolean {
|
||||||
|
return isRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getRead<Chapter extends ChapterReadInfo>(chapters: Chapter[]): Chapter[] {
|
||||||
|
return chapters.filter(Chapters.isRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getNonRead<Chapter extends ChapterReadInfo>(chapters: Chapter[]): Chapter[] {
|
||||||
|
return chapters.filter((chapter) => !Chapters.isRead(chapter));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async download(chapterIds: number[]): Promise<void> {
|
||||||
|
return Chapters.executeAction(
|
||||||
|
'download',
|
||||||
|
chapterIds.length,
|
||||||
|
() => requestManager.addChaptersToDownloadQueue(chapterIds).response,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async delete(chapterIds: number[]): Promise<void> {
|
||||||
|
return Chapters.executeAction(
|
||||||
|
'delete',
|
||||||
|
chapterIds.length,
|
||||||
|
() => requestManager.deleteDownloadedChapters(chapterIds).response,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async markAsRead(
|
||||||
|
chapters: (ChapterDownloadInfo & ChapterBookmarkInfo)[],
|
||||||
|
wasManuallyMarkedAsRead: boolean = false,
|
||||||
|
): Promise<void> {
|
||||||
|
const { deleteChaptersManuallyMarkedRead, deleteChaptersWithBookmark } = await getMetadataServerSettings();
|
||||||
|
const chapterIdsToDelete =
|
||||||
|
deleteChaptersManuallyMarkedRead && wasManuallyMarkedAsRead
|
||||||
|
? Chapters.getIds(Chapters.getAutoDeletable(chapters, deleteChaptersWithBookmark))
|
||||||
|
: [];
|
||||||
|
return Chapters.executeAction(
|
||||||
|
'mark_as_read',
|
||||||
|
chapters.length,
|
||||||
|
() =>
|
||||||
|
requestManager.updateChapters(Chapters.getIds(chapters), {
|
||||||
|
isRead: true,
|
||||||
|
lastPageRead: 0,
|
||||||
|
chapterIdsToDelete,
|
||||||
|
}).response,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async markAsUnread(chapterIds: number[]): Promise<void> {
|
||||||
|
return Chapters.executeAction(
|
||||||
|
'mark_as_unread',
|
||||||
|
chapterIds.length,
|
||||||
|
() => requestManager.updateChapters(chapterIds, { isRead: false }).response,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async bookmark(chapterIds: number[]): Promise<void> {
|
||||||
|
return Chapters.executeAction(
|
||||||
|
'bookmark',
|
||||||
|
chapterIds.length,
|
||||||
|
() => requestManager.updateChapters(chapterIds, { isBookmarked: true }).response,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async unBookmark(chapterIds: number[]): Promise<void> {
|
||||||
|
return Chapters.executeAction(
|
||||||
|
'unbookmark',
|
||||||
|
chapterIds.length,
|
||||||
|
() => requestManager.updateChapters(chapterIds, { isBookmarked: false }).response,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async executeAction(
|
||||||
|
action: ChapterAction,
|
||||||
|
itemCount: number,
|
||||||
|
fnToExecute: () => Promise<unknown>,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fnToExecute();
|
||||||
|
makeToast(translate(actionToTranslationKey[action].success, { count: itemCount }), 'success');
|
||||||
|
} catch (e) {
|
||||||
|
makeToast(translate(actionToTranslationKey[action].error, { count: itemCount }), 'error');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async performAction<Action extends ChapterAction>(
|
||||||
|
action: Action,
|
||||||
|
chapterIds: number[],
|
||||||
|
{
|
||||||
|
wasManuallyMarkedAsRead,
|
||||||
|
chapters,
|
||||||
|
}: Action extends 'mark_as_read'
|
||||||
|
? { wasManuallyMarkedAsRead: boolean; chapters?: never }
|
||||||
|
: Action extends 'change_categories'
|
||||||
|
? {
|
||||||
|
wasManuallyMarkedAsRead?: never;
|
||||||
|
chapters: (ChapterDownloadInfo & ChapterBookmarkInfo & ChapterReadInfo)[];
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
wasManuallyMarkedAsRead?: boolean;
|
||||||
|
chapters?: (ChapterDownloadInfo & ChapterBookmarkInfo & ChapterReadInfo)[];
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
switch (action) {
|
||||||
|
case 'download':
|
||||||
|
return Chapters.download(chapterIds);
|
||||||
|
case 'delete':
|
||||||
|
return Chapters.delete(chapterIds);
|
||||||
|
case 'mark_as_read':
|
||||||
|
return Chapters.markAsRead(chapters!, wasManuallyMarkedAsRead!);
|
||||||
|
case 'mark_as_unread':
|
||||||
|
return Chapters.markAsUnread(chapterIds);
|
||||||
|
case 'bookmark':
|
||||||
|
return Chapters.bookmark(chapterIds);
|
||||||
|
case 'unbookmark':
|
||||||
|
return Chapters.unBookmark(chapterIds);
|
||||||
|
default:
|
||||||
|
throw new Error(`Chapters::performAction: unknown action "${action}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
54
src/lib/data/ChaptersWithMeta.ts
Normal file
54
src/lib/data/ChaptersWithMeta.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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 { TChapter } from '@/typings.ts';
|
||||||
|
import { DownloadType } from '@/lib/graphql/generated/graphql.ts';
|
||||||
|
import { Chapters } from '@/lib/data/Chapters.ts';
|
||||||
|
|
||||||
|
export type ChapterWithMetaType = {
|
||||||
|
chapter: TChapter;
|
||||||
|
downloadChapter: DownloadType | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ChaptersWithMeta {
|
||||||
|
static getChapters(chapters: ChapterWithMetaType[]): TChapter[] {
|
||||||
|
return chapters.map(({ chapter }) => chapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getIds(chapters: ChapterWithMetaType[]): number[] {
|
||||||
|
return Chapters.getIds(ChaptersWithMeta.getChapters(chapters));
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDownloaded<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
|
||||||
|
return chapters.filter(({ chapter }) => Chapters.isDownloaded(chapter));
|
||||||
|
}
|
||||||
|
|
||||||
|
static getNonDownloaded<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
|
||||||
|
return chapters.filter(({ chapter }) => !Chapters.isDownloaded(chapter));
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDownloadable<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
|
||||||
|
return chapters.filter(({ chapter, downloadChapter }) => !Chapters.isDownloaded(chapter) && !downloadChapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getBookmarked<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
|
||||||
|
return chapters.filter(({ chapter }) => Chapters.isBookmarked(chapter));
|
||||||
|
}
|
||||||
|
|
||||||
|
static getNonBookmarked<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
|
||||||
|
return chapters.filter(({ chapter }) => !Chapters.isBookmarked(chapter));
|
||||||
|
}
|
||||||
|
|
||||||
|
static getRead<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
|
||||||
|
return chapters.filter(({ chapter }) => Chapters.isRead(chapter));
|
||||||
|
}
|
||||||
|
|
||||||
|
static getNonRead<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
|
||||||
|
return chapters.filter(({ chapter }) => !Chapters.isRead(chapter));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
UpdateMangaCategoriesPatchInput,
|
UpdateMangaCategoriesPatchInput,
|
||||||
} from '@/lib/graphql/generated/graphql.ts';
|
} from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { Chapters } from '@/lib/data/Chapters.ts';
|
import { Chapters } from '@/lib/data/Chapters.ts';
|
||||||
import { getMetadataServerSettings } from '@/util/metadataServerSettings.ts';
|
|
||||||
import { makeToast } from '@/components/util/Toast.tsx';
|
import { makeToast } from '@/components/util/Toast.tsx';
|
||||||
|
|
||||||
export type MangaAction =
|
export type MangaAction =
|
||||||
@@ -26,33 +25,54 @@ export type MangaAction =
|
|||||||
| 'remove_from_library'
|
| 'remove_from_library'
|
||||||
| 'change_categories';
|
| 'change_categories';
|
||||||
|
|
||||||
const actionToTranslationKey: {
|
export const actionToTranslationKey: {
|
||||||
[key in MangaAction]: {
|
[key in MangaAction]: {
|
||||||
|
action: {
|
||||||
|
selected: TranslationKey;
|
||||||
|
};
|
||||||
success: TranslationKey;
|
success: TranslationKey;
|
||||||
error: TranslationKey;
|
error: TranslationKey;
|
||||||
};
|
};
|
||||||
} = {
|
} = {
|
||||||
download: {
|
download: {
|
||||||
|
action: {
|
||||||
|
selected: 'chapter.action.download.add.button.selected',
|
||||||
|
},
|
||||||
success: 'chapter.action.download.add.label.success',
|
success: 'chapter.action.download.add.label.success',
|
||||||
error: 'chapter.action.download.add.label.error',
|
error: 'chapter.action.download.add.label.error',
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
|
action: {
|
||||||
|
selected: 'chapter.action.download.delete.button.selected',
|
||||||
|
},
|
||||||
success: 'chapter.action.download.delete.label.success',
|
success: 'chapter.action.download.delete.label.success',
|
||||||
error: 'chapter.action.download.delete.label.error',
|
error: 'chapter.action.download.delete.label.error',
|
||||||
},
|
},
|
||||||
mark_as_read: {
|
mark_as_read: {
|
||||||
|
action: {
|
||||||
|
selected: 'chapter.action.mark_as_read.add.button.selected',
|
||||||
|
},
|
||||||
success: 'chapter.action.mark_as_read.add.label.success',
|
success: 'chapter.action.mark_as_read.add.label.success',
|
||||||
error: 'chapter.action.mark_as_read.add.label.error',
|
error: 'chapter.action.mark_as_read.add.label.error',
|
||||||
},
|
},
|
||||||
mark_as_unread: {
|
mark_as_unread: {
|
||||||
|
action: {
|
||||||
|
selected: 'chapter.action.mark_as_read.remove.button.selected',
|
||||||
|
},
|
||||||
success: 'chapter.action.mark_as_read.remove.label.success',
|
success: 'chapter.action.mark_as_read.remove.label.success',
|
||||||
error: 'chapter.action.mark_as_read.remove.label.error',
|
error: 'chapter.action.mark_as_read.remove.label.error',
|
||||||
},
|
},
|
||||||
remove_from_library: {
|
remove_from_library: {
|
||||||
|
action: {
|
||||||
|
selected: 'manga.action.library.remove.button.selected',
|
||||||
|
},
|
||||||
success: 'manga.action.library.remove.label.success',
|
success: 'manga.action.library.remove.label.success',
|
||||||
error: 'manga.action.library.remove.label.error',
|
error: 'manga.action.library.remove.label.error',
|
||||||
},
|
},
|
||||||
change_categories: {
|
change_categories: {
|
||||||
|
action: {
|
||||||
|
selected: 'manga.action.category.button.selected',
|
||||||
|
},
|
||||||
success: 'manga.action.category.label.success',
|
success: 'manga.action.category.label.success',
|
||||||
error: 'manga.action.category.label.error',
|
error: 'manga.action.category.label.error',
|
||||||
},
|
},
|
||||||
@@ -124,45 +144,22 @@ export class Mangas {
|
|||||||
|
|
||||||
static async downloadChapters(mangaIds: number[]): Promise<void> {
|
static async downloadChapters(mangaIds: number[]): Promise<void> {
|
||||||
const chapters = await Mangas.getChapterIdsWithState(mangaIds, { isDownloaded: false });
|
const chapters = await Mangas.getChapterIdsWithState(mangaIds, { isDownloaded: false });
|
||||||
return Mangas.executeAction(
|
return Chapters.download(Chapters.getIds(chapters));
|
||||||
'download',
|
|
||||||
chapters.length,
|
|
||||||
() => requestManager.addChaptersToDownloadQueue(Chapters.getIds(chapters)).response,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async deleteChapters(mangaIds: number[]): Promise<void> {
|
static async deleteChapters(mangaIds: number[]): Promise<void> {
|
||||||
const chapters = await Mangas.getChapterIdsWithState(mangaIds, { isDownloaded: true });
|
const chapters = await Mangas.getChapterIdsWithState(mangaIds, { isDownloaded: true });
|
||||||
return Mangas.executeAction(
|
return Chapters.delete(Chapters.getIds(chapters));
|
||||||
'delete',
|
|
||||||
chapters.length,
|
|
||||||
() => requestManager.deleteDownloadedChapters(Chapters.getIds(chapters)).response,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async markAsRead(mangaIds: number[], deleteChapters: boolean = false): Promise<void> {
|
static async markAsRead(mangaIds: number[], wasManuallyMarkedAsRead: boolean = false): Promise<void> {
|
||||||
const [chapters, { deleteChaptersWithBookmark }] = await Promise.all([
|
const chapters = await Mangas.getChapterIdsWithState(mangaIds, { isRead: false });
|
||||||
Mangas.getChapterIdsWithState(mangaIds, { isRead: false }),
|
return Chapters.markAsRead(chapters, wasManuallyMarkedAsRead);
|
||||||
getMetadataServerSettings(),
|
|
||||||
]);
|
|
||||||
const chapterIdsToDelete = deleteChapters
|
|
||||||
? Chapters.getIds(Chapters.getAutoDeletable(chapters, deleteChaptersWithBookmark))
|
|
||||||
: [];
|
|
||||||
return Mangas.executeAction(
|
|
||||||
'mark_as_read',
|
|
||||||
chapterIdsToDelete.length,
|
|
||||||
() =>
|
|
||||||
requestManager.updateChapters(Chapters.getIds(chapters), { isRead: true, chapterIdsToDelete }).response,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async markAsUnread(mangaIds: number[]): Promise<void> {
|
static async markAsUnread(mangaIds: number[]): Promise<void> {
|
||||||
const chapters = await Mangas.getChapterIdsWithState(mangaIds, { isRead: true });
|
const chapters = await Mangas.getChapterIdsWithState(mangaIds, { isRead: true });
|
||||||
return Mangas.executeAction(
|
return Chapters.markAsUnread(Chapters.getIds(chapters));
|
||||||
'mark_as_unread',
|
|
||||||
chapters.length,
|
|
||||||
() => requestManager.updateChapters(Chapters.getIds(chapters), { isRead: false }).response,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async removeFromLibrary(mangaIds: number[]): Promise<void> {
|
static async removeFromLibrary(mangaIds: number[]): Promise<void> {
|
||||||
@@ -199,13 +196,13 @@ export class Mangas {
|
|||||||
action: Action,
|
action: Action,
|
||||||
mangaIds: number[],
|
mangaIds: number[],
|
||||||
{
|
{
|
||||||
autoDeleteChapters,
|
wasManuallyMarkedAsRead,
|
||||||
changeCategoriesPatch,
|
changeCategoriesPatch,
|
||||||
}: Action extends 'mark_as_read'
|
}: Action extends 'mark_as_read'
|
||||||
? { autoDeleteChapters: boolean; changeCategoriesPatch?: never }
|
? { wasManuallyMarkedAsRead: boolean; changeCategoriesPatch?: never }
|
||||||
: Action extends 'change_categories'
|
: Action extends 'change_categories'
|
||||||
? { autoDeleteChapters?: never; changeCategoriesPatch: UpdateMangaCategoriesPatchInput }
|
? { wasManuallyMarkedAsRead?: never; changeCategoriesPatch: UpdateMangaCategoriesPatchInput }
|
||||||
: { autoDeleteChapters?: boolean; changeCategoriesPatch?: UpdateMangaCategoriesPatchInput },
|
: { wasManuallyMarkedAsRead?: boolean; changeCategoriesPatch?: UpdateMangaCategoriesPatchInput },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'download':
|
case 'download':
|
||||||
@@ -213,7 +210,7 @@ export class Mangas {
|
|||||||
case 'delete':
|
case 'delete':
|
||||||
return Mangas.deleteChapters(mangaIds);
|
return Mangas.deleteChapters(mangaIds);
|
||||||
case 'mark_as_read':
|
case 'mark_as_read':
|
||||||
return Mangas.markAsRead(mangaIds, autoDeleteChapters!);
|
return Mangas.markAsRead(mangaIds, wasManuallyMarkedAsRead!);
|
||||||
case 'mark_as_unread':
|
case 'mark_as_unread':
|
||||||
return Mangas.markAsUnread(mangaIds);
|
return Mangas.markAsUnread(mangaIds);
|
||||||
case 'remove_from_library':
|
case 'remove_from_library':
|
||||||
@@ -221,7 +218,7 @@ export class Mangas {
|
|||||||
case 'change_categories':
|
case 'change_categories':
|
||||||
return Mangas.changeCategories(mangaIds, changeCategoriesPatch!);
|
return Mangas.changeCategories(mangaIds, changeCategoriesPatch!);
|
||||||
default:
|
default:
|
||||||
throw new Error(`performMangasAction::performAction: unknown action "${action}"`);
|
throw new Error(`Mangas::performAction: unknown action "${action}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1452,6 +1452,13 @@ export class RequestManager {
|
|||||||
return this.doRequest(GQLMethod.USE_QUERY, GET_CHAPTERS, variables, options);
|
return this.doRequest(GQLMethod.USE_QUERY, GET_CHAPTERS, variables, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getChapters(
|
||||||
|
variables: GetChaptersQueryVariables,
|
||||||
|
options?: QueryOptions<GetChaptersQueryVariables, GetChaptersQuery>,
|
||||||
|
): AbortabaleApolloQueryResponse<GetChaptersQuery> {
|
||||||
|
return this.doRequest(GQLMethod.QUERY, GET_CHAPTERS, variables, options);
|
||||||
|
}
|
||||||
|
|
||||||
public useGetMangaChapters(
|
public useGetMangaChapters(
|
||||||
mangaId: number | string,
|
mangaId: number | string,
|
||||||
options?: QueryHookOptions<GetChaptersQuery, GetChaptersQueryVariables>,
|
options?: QueryHookOptions<GetChaptersQuery, GetChaptersQueryVariables>,
|
||||||
|
|||||||
Reference in New Issue
Block a user