diff --git a/src/components/manga/ChapterActionMenuItems.tsx b/src/components/manga/ChapterActionMenuItems.tsx new file mode 100644 index 00000000..16e27c28 --- /dev/null +++ b/src/components/manga/ChapterActionMenuItems.tsx @@ -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['handleSelection']; + canBeDownloaded: boolean; +}; + +type SelectModeProps = { + selectedChapters: IChapterWithMeta[]; +}; + +type Props = + | (BaseProps & SingleModeProps & PropertiesNever) + | (BaseProps & PropertiesNever & 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 && ( + + )} + {shouldShowMenuItem(canBeDownloaded) && ( + performAction('download', downloadableChapters)} + title={getMenuItemTitle('download', downloadableChapters.length)} + /> + )} + {shouldShowMenuItem(isDownloaded) && ( + performAction('delete', downloadedChapters)} + title={getMenuItemTitle('delete', downloadedChapters.length)} + /> + )} + {shouldShowMenuItem(!isBookmarked) && ( + performAction('bookmark', unbookmarkedChapters)} + title={getMenuItemTitle('bookmark', unbookmarkedChapters.length)} + /> + )} + {shouldShowMenuItem(isBookmarked) && ( + performAction('unbookmark', bookmarkedChapters)} + title={getMenuItemTitle('unbookmark', bookmarkedChapters.length)} + /> + )} + {shouldShowMenuItem(!isRead) && ( + performAction('mark_as_read', unreadChapters)} + title={getMenuItemTitle('mark_as_read', unreadChapters.length)} + /> + )} + {shouldShowMenuItem(isRead) && ( + performAction('mark_as_unread', readChapters)} + title={getMenuItemTitle('mark_as_unread', readChapters.length)} + /> + )} + {isSingleMode && ( + performAction('mark_prev_as_read', [])} + Icon={DoneAll} + title={t('chapter.action.mark_as_read.add.label.action.previous')} + /> + )} + + ); +}; diff --git a/src/components/manga/ChapterCard.tsx b/src/components/manga/ChapterCard.tsx index a4a403b7..9c010ae2 100644 --- a/src/components/manga/ChapterCard.tsx +++ b/src/components/manga/ChapterCard.tsx @@ -7,32 +7,23 @@ */ 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 RemoveDone from '@mui/icons-material/RemoveDone'; -import { CardActionArea, Checkbox, ListItemIcon, ListItemText, Stack, Tooltip } from '@mui/material'; +import { CardActionArea, Checkbox, Stack, Tooltip } from '@mui/material'; import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; 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 Typography from '@mui/material/Typography'; -import React from 'react'; +import React, { TouchEvent } from 'react'; import { Link } from 'react-router-dom'; 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 { 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 { useMetadataServerSettings } from '@/util/metadataServerSettings.ts'; +import { ChapterActionMenuItems } from '@/components/manga/ChapterActionMenuItems.tsx'; +import { Menu } from '@/components/manga/Menu.tsx'; interface IProps { chapter: TChapter; @@ -50,77 +41,6 @@ export const ChapterCard: React.FC = (props: IProps) => { const { chapter, allChapters, downloadChapter: dc, showChapterNumber, onSelect, selected } = props; const isSelecting = selected !== null; - const { settings: metadataServerSettings } = useMetadataServerSettings(); - - const [anchorEl, setAnchorEl] = React.useState(null); - - const handleMenuClick = (event: React.MouseEvent) => { - // 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: 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) => { if (isSelecting) { event.preventDefault(); @@ -130,116 +50,113 @@ export const ChapterCard: React.FC = (props: IProps) => { }; const { isDownloaded } = chapter; - const canBeDownloaded = !chapter.isDownloaded && dc === undefined; return (
  • - + {(popupState) => { + const bindTriggerProps = bindTrigger(popupState); + + const preventDefaultAction = (e: React.BaseSyntheticEvent) => { + e.stopPropagation(); + e.preventDefault(); + }; + + const handleClickOpenMenu = (e: React.BaseSyntheticEvent) => { + preventDefaultAction(e); + bindTriggerProps.onClick(e as any); + }; + + const handleTouchStart = (e: React.BaseSyntheticEvent) => { + preventDefaultAction(e); + bindTriggerProps.onTouchStart(e as TouchEvent); + }; + + return ( + <> + + + + + + {chapter.isBookmarked && ( + + )} + {showChapterNumber + ? `${t('chapter.title')} ${chapter.chapterNumber}` + : chapter.name} + + {chapter.scanlator} + + {getUploadDateString(Number(chapter.uploadDate ?? 0))} + {isDownloaded && ` • ${t('chapter.status.label.downloaded')}`} + + + + {dc && } + + {selected === null ? ( + + + + + + ) : ( + + + + )} + + + + {!isSelecting && popupState.isOpen && ( + + {(onClose) => ( + onSelect(true)} + canBeDownloaded={!chapter.isDownloaded && !dc} + /> + )} + + )} + + ); }} - > - - - - - {chapter.isBookmarked && ( - - )} - {showChapterNumber ? `${t('chapter.title')} ${chapter.chapterNumber}` : chapter.name} - - {chapter.scanlator} - - {getUploadDateString(Number(chapter.uploadDate ?? 0))} - {isDownloaded && ` • ${t('chapter.status.label.downloaded')}`} - - - - {dc && } - - {selected === null ? ( - - - - - - ) : ( - - - - )} - - - - - - - - {t('chapter.action.label.select')} - - {isDownloaded && ( - - - - - {t('chapter.action.download.delete.label.action')} - - )} - {canBeDownloaded && ( - - - - - {t('chapter.action.download.add.label.action')} - - )} - sendChange('isBookmarked', !chapter.isBookmarked)}> - - {chapter.isBookmarked && } - {!chapter.isBookmarked && } - - - {chapter.isBookmarked && t('chapter.action.bookmark.remove.label.action')} - {!chapter.isBookmarked && t('chapter.action.bookmark.add.label.action')} - - - sendChange('isRead', !chapter.isRead)}> - - {chapter.isRead && } - {!chapter.isRead && } - - - {chapter.isRead && t('chapter.action.mark_as_read.remove.label.action')} - {!chapter.isRead && t('chapter.action.mark_as_read.add.label.action.current')} - - - sendChange('markPrevRead', true)}> - - - - {t('chapter.action.mark_as_read.add.label.action.previous')} - - - +
  • ); }; diff --git a/src/components/manga/ChapterList.tsx b/src/components/manga/ChapterList.tsx index c7435db7..4de07a49 100644 --- a/src/components/manga/ChapterList.tsx +++ b/src/components/manga/ChapterList.tsx @@ -8,27 +8,27 @@ import { Box, CircularProgress, Stack, styled, Tooltip } from '@mui/material'; import Typography from '@mui/material/Typography'; -import React, { ComponentProps, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { Virtuoso } from 'react-virtuoso'; import { useTranslation } from 'react-i18next'; import IconButton from '@mui/material/IconButton'; import DownloadIcon from '@mui/icons-material/Download'; 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 { ChapterCard } from '@/components/manga/ChapterCard'; import { ResumeFab } from '@/components/manga/ResumeFAB'; import { filterAndSortChapters, useChapterOptions } from '@/components/manga/util'; import { EmptyView } from '@/components/util/EmptyView'; -import { makeToast } from '@/components/util/Toast'; import { ChaptersToolbarMenu } from '@/components/manga/ChaptersToolbarMenu'; import { SelectionFAB } from '@/components/manga/SelectionFAB'; import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab'; -import { DownloadType, UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts'; -import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts'; +import { DownloadType } from '@/lib/graphql/generated/graphql.ts'; import { useSelectableCollection } from '@/components/collection/useSelectableCollection.ts'; 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 }) => ({ 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 { chapter: TChapter; downloadChapter: DownloadType | undefined; @@ -108,8 +76,6 @@ export const ChapterList: React.FC = ({ manga, isRefreshing }) => { const { areNoItemsSelected, areAllItemsSelected, selectedItemIds, handleSelectAll, handleSelection } = useSelectableCollection(chapters.length, { currentKey: 'default' }); - const { settings: metadataServerSettings } = useMetadataServerSettings(); - const visibleChapters = useMemo(() => filterAndSortChapters(chapters, options), [chapters, options]); const nextChapterIndexToRead = (manga.lastReadChapter?.sourceOrder ?? 0) + 1; @@ -118,70 +84,6 @@ export const ChapterList: React.FC = ({ manga, isRefreshing }) => { const areAllChaptersRead = manga.unreadCount === 0; const areAllChaptersDownloaded = manga.downloadCount === manga.chapters.totalCount; - const handleFabAction: ComponentProps['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; - - 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 noChaptersMatchingFilter = !noChaptersFound && visibleChapters.length === 0; @@ -208,11 +110,7 @@ export const ChapterList: React.FC = ({ manga, isRefreshing }) => { return ( {(handleClose) => ( - + )} ); @@ -253,7 +151,12 @@ export const ChapterList: React.FC = ({ manga, isRefreshing }) => { handleFabAction('mark_as_read', chaptersWithMeta)} + onClick={() => + Chapters.markAsRead( + ChaptersWithMeta.getChapters(ChaptersWithMeta.getNonRead(chaptersWithMeta)), + true, + ) + } > @@ -261,7 +164,11 @@ export const ChapterList: React.FC = ({ manga, isRefreshing }) => { handleFabAction('download', chaptersWithMeta)} + onClick={() => + Chapters.download( + ChaptersWithMeta.getIds(ChaptersWithMeta.getNonDownloaded(chaptersWithMeta)), + ) + } > diff --git a/src/components/manga/ChapterSelectionFABActionItems.tsx b/src/components/manga/ChapterSelectionFABActionItems.tsx deleted file mode 100644 index 3a1dcd7f..00000000 --- a/src/components/manga/ChapterSelectionFABActionItems.tsx +++ /dev/null @@ -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 ( - <> - !c.isDownloaded && dc === undefined, - )} - onClick={handleAction} - title={t('chapter.action.download.add.button.selected')} - /> - chapter.isDownloaded)} - onClick={handleAction} - title={t('chapter.action.download.delete.button.selected')} - /> - !chapter.isBookmarked)} - onClick={handleAction} - title={t('chapter.action.bookmark.add.button.selected')} - /> - chapter.isBookmarked)} - onClick={handleAction} - title={t('chapter.action.bookmark.remove.button.selected')} - /> - !chapter.isRead)} - onClick={handleAction} - title={t('chapter.action.mark_as_read.add.button.selected')} - /> - chapter.isRead)} - onClick={handleAction} - title={t('chapter.action.mark_as_read.remove.button.selected')} - /> - - ); -}; diff --git a/src/components/manga/MangaActionMenu.tsx b/src/components/manga/MangaActionMenu.tsx index 2c557c02..6d8f7abb 100644 --- a/src/components/manga/MangaActionMenu.tsx +++ b/src/components/manga/MangaActionMenu.tsx @@ -7,8 +7,6 @@ */ 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 Delete from '@mui/icons-material/Delete'; import Download from '@mui/icons-material/Download'; @@ -22,8 +20,8 @@ import { useState } from 'react'; import { TManga } from '@/typings.ts'; import { MangaAction, MangaDownloadInfo, Mangas, MangaUnreadInfo } from '@/lib/data/Mangas.ts'; import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts'; -import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts'; import { CategorySelect } from '@/components/navbar/action/CategorySelect.tsx'; +import { MenuItem } from '@/components/manga/MenuItem.tsx'; export const MangaActionMenu = ({ manga, @@ -35,7 +33,6 @@ export const MangaActionMenu = ({ } & ReturnType) => { const { t } = useTranslation(); - const { settings } = useMetadataServerSettings(); const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false); const isFullyDownloaded = manga.downloadCount === manga.chapters.totalCount; @@ -50,7 +47,7 @@ export const MangaActionMenu = ({ const performAction = (action: MangaAction) => { Mangas.performAction(action, [manga.id], { - autoDeleteChapters: settings.deleteChaptersManuallyMarkedRead, + wasManuallyMarkedAsRead: true, }).catch(() => {}); bindMenuProps.onClose(); @@ -60,57 +57,50 @@ export const MangaActionMenu = ({ <> {!!handleSelection && ( - - - - - {t('chapter.action.label.select')} - + )} {!isFullyDownloaded && ( - performAction('download')}> - - - - {t('chapter.action.download.add.label.action')} - + performAction('download')} + Icon={Download} + title={t('chapter.action.download.add.label.action')} + /> )} {hasDownloadedChapters && ( - performAction('delete')}> - - - - {t('chapter.action.download.delete.label.action')} - + performAction('delete')} + Icon={Delete} + title={t('chapter.action.download.delete.label.action')} + /> )} {hasUnreadChapters && ( - performAction('mark_as_read')}> - - - - {t('chapter.action.mark_as_read.add.label.action.current')} - + performAction('mark_as_read')} + Icon={Done} + title={t('chapter.action.mark_as_read.add.label.action.current')} + /> )} {hasReadChapters && ( - performAction('mark_as_unread')}> - - - - {t('chapter.action.mark_as_read.remove.label.action')} - + performAction('mark_as_unread')} + Icon={RemoveDone} + title={t('chapter.action.mark_as_read.remove.label.action')} + /> )} - setIsCategorySelectOpen(true)}> - - - {t('manga.action.category.label.action')} - - performAction('remove_from_library')}> - - - - {t('manga.action.library.remove.label.action')} - + setIsCategorySelectOpen(true)} + Icon={Label} + title={t('manga.action.category.label.action')} + /> + performAction('remove_from_library')} + Icon={FavoriteBorderIcon} + title={t('manga.action.library.remove.label.action')} + /> {isCategorySelectOpen && ( { + const countSuffix = count > 0 ? ` (${count})` : ''; + return `${translate(actionToTranslationKey[action].action.selected)}${countSuffix}`; +}; + export const MangasSelectionFABActionItems = ({ selectedMangas, handleClose, @@ -29,66 +33,66 @@ export const MangasSelectionFABActionItems = ({ selectedMangas: TManga[]; handleClose: (selectionModeState: boolean) => void; }) => { - const { t } = useTranslation(); - const { settings } = useMetadataServerSettings(); const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false); const handleAction = (action: MangaAction, mangas: TManga[]) => { Mangas.performAction(action, Mangas.getIds(mangas), { - autoDeleteChapters: settings.deleteChaptersManuallyMarkedRead, + wasManuallyMarkedAsRead: true, }).catch(() => {}); 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 ( <> - - action="download" + handleAction('download', downloadableMangas)} + title={getMenuItemTitle('download', downloadableMangas.length)} /> - - action="delete" + handleAction('delete', downloadedMangas)} + title={getMenuItemTitle('delete', downloadedMangas.length)} /> - - action="mark_as_read" + handleAction('mark_as_read', unreadMangas)} + title={getMenuItemTitle('mark_as_read', unreadMangas.length)} /> - - action="mark_as_unread" + handleAction('mark_as_unread', readMangas)} + title={getMenuItemTitle('mark_as_unread', readMangas.length)} /> - - action="change_categories" + setIsCategorySelectOpen(true)} - title={t('manga.action.category.label.action')} + onClick={() => handleAction('change_categories', selectedMangas)} + title={getMenuItemTitle('change_categories', selectedMangas.length)} /> - - action="remove_from_library" + handleAction('remove_from_library', selectedMangas)} + title={getMenuItemTitle('remove_from_library', selectedMangas.length)} /> {isCategorySelectOpen && ( & + Required> & { children: (onClose: () => void) => JSX.Element }) => ( + + {children(() => onClose({}, 'backdropClick'))} + +); diff --git a/src/components/manga/MenuItem.tsx b/src/components/manga/MenuItem.tsx new file mode 100644 index 00000000..ce734f31 --- /dev/null +++ b/src/components/manga/MenuItem.tsx @@ -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 & { muiName: string }; + onClick: () => void; + isDisabled?: boolean; +} + +export const MenuItem = ({ onClick, title, Icon, isDisabled }: IProps) => ( + + + + + {title} + +); diff --git a/src/components/manga/SelectionFAB.tsx b/src/components/manga/SelectionFAB.tsx index e2c86844..474da598 100644 --- a/src/components/manga/SelectionFAB.tsx +++ b/src/components/manga/SelectionFAB.tsx @@ -7,14 +7,16 @@ */ import MoreHoriz from '@mui/icons-material/MoreHoriz'; -import { Fab, Menu, Box, styled } from '@mui/material'; -import React, { useRef, useState } from 'react'; +import { Fab, Box, styled } from '@mui/material'; +import React from 'react'; import { useTranslation } from 'react-i18next'; +import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state'; import { DEFAULT_FAB_STYLE } from '@/components/util/StyledFab'; import { TranslationKey } from '@/typings.ts'; +import { Menu } from '@/components/manga/Menu.tsx'; interface SelectionFABProps { - children: (handleClose: () => void) => React.ReactNode; + children: (handleClose: () => void) => JSX.Element; selectedItemsCount: number; title: TranslationKey; } @@ -32,29 +34,29 @@ const FabContainer = styled(Box)(({ theme }) => ({ export const SelectionFAB: React.FC = ({ children, selectedItemsCount, title }) => { const { t } = useTranslation(); - const anchorEl = useRef(); - const [open, setOpen] = useState(false); - const handleClose = () => setOpen(false); - return ( - - setOpen(true)}> - {`${selectedItemsCount} ${t(title, { count: selectedItemsCount })}`} - - - - {children(handleClose)} - - + + {(popupState) => ( + <> + + + {`${selectedItemsCount} ${t(title, { count: selectedItemsCount })}`} + + + + + {(onClose) => children(onClose)} + + + )} + ); }; diff --git a/src/components/manga/SelectionFABActionItem.tsx b/src/components/manga/SelectionFABActionItem.tsx deleted file mode 100644 index 23976853..00000000 --- a/src/components/manga/SelectionFABActionItem.tsx +++ /dev/null @@ -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: Action; - matchingItems: Item[]; - title: string; - Icon: OverridableComponent & { muiName: string }; - onClick: (action: Action, items: Item[]) => void; -} - -export const SelectionFABActionItem = ({ - action, - matchingItems, - onClick, - title, - Icon, -}: IProps) => { - const count = matchingItems.length; - return ( - onClick(action, matchingItems)} disabled={count === 0}> - - - - - {title} - {count > 0 ? ` (${count})` : ''} - - - ); -}; diff --git a/src/lib/data/Chapters.ts b/src/lib/data/Chapters.ts index 88a44bc5..c6df91f5 100644 --- a/src/lib/data/Chapters.ts +++ b/src/lib/data/Chapters.ts @@ -6,25 +6,97 @@ * 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; -type ChapterBookmarkInfo = Pick; +export type ChapterAction = 'download' | 'delete' | 'bookmark' | 'unbookmark' | 'mark_as_read' | 'mark_as_unread'; + +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; +export type ChapterDownloadInfo = ChapterIdInfo & Pick; +export type ChapterBookmarkInfo = ChapterIdInfo & Pick; +export type ChapterReadInfo = ChapterIdInfo & Pick; export class Chapters { static getIds(chapters: { id: number }[]): number[] { return chapters.map((chapter) => chapter.id); } - static isDeletable({ isDownloaded }: ChapterDownloadInfo): boolean { + static isDownloaded({ isDownloaded }: ChapterDownloadInfo): boolean { return isDownloaded; } + static getDownloaded(chapters: Chapter[]): Chapter[] { + return chapters.filter(Chapters.isDownloaded); + } + static isAutoDeletable( { isBookmarked, ...chapter }: ChapterDownloadInfo & ChapterBookmarkInfo, canDeleteBookmarked: boolean = false, ): boolean { - return Chapters.isDeletable(chapter) && (!isBookmarked || canDeleteBookmarked); + return Chapters.isDownloaded(chapter) && (!isBookmarked || canDeleteBookmarked); } static getAutoDeletable( @@ -33,4 +105,139 @@ export class Chapters { ): Chapters[] { return chapters.filter((chapter) => Chapters.isAutoDeletable(chapter, canDeleteBookmarked)); } + + static isBookmarked({ isBookmarked }: ChapterBookmarkInfo): boolean { + return isBookmarked; + } + + static getBookmarked(chapters: Chapter[]): Chapter[] { + return chapters.filter(Chapters.isBookmarked); + } + + static getNonBookmarked(chapters: Chapter[]): Chapter[] { + return chapters.filter((chapter) => !Chapters.isBookmarked(chapter)); + } + + static isRead({ isRead }: ChapterReadInfo): boolean { + return isRead; + } + + static getRead(chapters: Chapter[]): Chapter[] { + return chapters.filter(Chapters.isRead); + } + + static getNonRead(chapters: Chapter[]): Chapter[] { + return chapters.filter((chapter) => !Chapters.isRead(chapter)); + } + + static async download(chapterIds: number[]): Promise { + return Chapters.executeAction( + 'download', + chapterIds.length, + () => requestManager.addChaptersToDownloadQueue(chapterIds).response, + ); + } + + static async delete(chapterIds: number[]): Promise { + return Chapters.executeAction( + 'delete', + chapterIds.length, + () => requestManager.deleteDownloadedChapters(chapterIds).response, + ); + } + + static async markAsRead( + chapters: (ChapterDownloadInfo & ChapterBookmarkInfo)[], + wasManuallyMarkedAsRead: boolean = false, + ): Promise { + 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 { + return Chapters.executeAction( + 'mark_as_unread', + chapterIds.length, + () => requestManager.updateChapters(chapterIds, { isRead: false }).response, + ); + } + + static async bookmark(chapterIds: number[]): Promise { + return Chapters.executeAction( + 'bookmark', + chapterIds.length, + () => requestManager.updateChapters(chapterIds, { isBookmarked: true }).response, + ); + } + + static async unBookmark(chapterIds: number[]): Promise { + return Chapters.executeAction( + 'unbookmark', + chapterIds.length, + () => requestManager.updateChapters(chapterIds, { isBookmarked: false }).response, + ); + } + + private static async executeAction( + action: ChapterAction, + itemCount: number, + fnToExecute: () => Promise, + ): Promise { + 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: 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 { + 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}"`); + } + } } diff --git a/src/lib/data/ChaptersWithMeta.ts b/src/lib/data/ChaptersWithMeta.ts new file mode 100644 index 00000000..494f2066 --- /dev/null +++ b/src/lib/data/ChaptersWithMeta.ts @@ -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(chapters: Chapter[]): Chapter[] { + return chapters.filter(({ chapter }) => Chapters.isDownloaded(chapter)); + } + + static getNonDownloaded(chapters: Chapter[]): Chapter[] { + return chapters.filter(({ chapter }) => !Chapters.isDownloaded(chapter)); + } + + static getDownloadable(chapters: Chapter[]): Chapter[] { + return chapters.filter(({ chapter, downloadChapter }) => !Chapters.isDownloaded(chapter) && !downloadChapter); + } + + static getBookmarked(chapters: Chapter[]): Chapter[] { + return chapters.filter(({ chapter }) => Chapters.isBookmarked(chapter)); + } + + static getNonBookmarked(chapters: Chapter[]): Chapter[] { + return chapters.filter(({ chapter }) => !Chapters.isBookmarked(chapter)); + } + + static getRead(chapters: Chapter[]): Chapter[] { + return chapters.filter(({ chapter }) => Chapters.isRead(chapter)); + } + + static getNonRead(chapters: Chapter[]): Chapter[] { + return chapters.filter(({ chapter }) => !Chapters.isRead(chapter)); + } +} diff --git a/src/lib/data/Mangas.ts b/src/lib/data/Mangas.ts index 37b5a932..ab2261cf 100644 --- a/src/lib/data/Mangas.ts +++ b/src/lib/data/Mangas.ts @@ -15,7 +15,6 @@ import { UpdateMangaCategoriesPatchInput, } from '@/lib/graphql/generated/graphql.ts'; import { Chapters } from '@/lib/data/Chapters.ts'; -import { getMetadataServerSettings } from '@/util/metadataServerSettings.ts'; import { makeToast } from '@/components/util/Toast.tsx'; export type MangaAction = @@ -26,33 +25,54 @@ export type MangaAction = | 'remove_from_library' | 'change_categories'; -const actionToTranslationKey: { +export const actionToTranslationKey: { [key in MangaAction]: { + action: { + selected: TranslationKey; + }; success: TranslationKey; error: TranslationKey; }; } = { download: { + action: { + selected: 'chapter.action.download.add.button.selected', + }, success: 'chapter.action.download.add.label.success', error: 'chapter.action.download.add.label.error', }, delete: { + action: { + selected: 'chapter.action.download.delete.button.selected', + }, success: 'chapter.action.download.delete.label.success', error: 'chapter.action.download.delete.label.error', }, mark_as_read: { + action: { + 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: { + 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', }, remove_from_library: { + action: { + selected: 'manga.action.library.remove.button.selected', + }, success: 'manga.action.library.remove.label.success', error: 'manga.action.library.remove.label.error', }, change_categories: { + action: { + selected: 'manga.action.category.button.selected', + }, success: 'manga.action.category.label.success', error: 'manga.action.category.label.error', }, @@ -124,45 +144,22 @@ export class Mangas { static async downloadChapters(mangaIds: number[]): Promise { const chapters = await Mangas.getChapterIdsWithState(mangaIds, { isDownloaded: false }); - return Mangas.executeAction( - 'download', - chapters.length, - () => requestManager.addChaptersToDownloadQueue(Chapters.getIds(chapters)).response, - ); + return Chapters.download(Chapters.getIds(chapters)); } static async deleteChapters(mangaIds: number[]): Promise { const chapters = await Mangas.getChapterIdsWithState(mangaIds, { isDownloaded: true }); - return Mangas.executeAction( - 'delete', - chapters.length, - () => requestManager.deleteDownloadedChapters(Chapters.getIds(chapters)).response, - ); + return Chapters.delete(Chapters.getIds(chapters)); } - static async markAsRead(mangaIds: number[], deleteChapters: boolean = false): Promise { - const [chapters, { deleteChaptersWithBookmark }] = await Promise.all([ - Mangas.getChapterIdsWithState(mangaIds, { isRead: false }), - 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 markAsRead(mangaIds: number[], wasManuallyMarkedAsRead: boolean = false): Promise { + const chapters = await Mangas.getChapterIdsWithState(mangaIds, { isRead: false }); + return Chapters.markAsRead(chapters, wasManuallyMarkedAsRead); } static async markAsUnread(mangaIds: number[]): Promise { const chapters = await Mangas.getChapterIdsWithState(mangaIds, { isRead: true }); - return Mangas.executeAction( - 'mark_as_unread', - chapters.length, - () => requestManager.updateChapters(Chapters.getIds(chapters), { isRead: false }).response, - ); + return Chapters.markAsUnread(Chapters.getIds(chapters)); } static async removeFromLibrary(mangaIds: number[]): Promise { @@ -199,13 +196,13 @@ export class Mangas { action: Action, mangaIds: number[], { - autoDeleteChapters, + wasManuallyMarkedAsRead, changeCategoriesPatch, }: Action extends 'mark_as_read' - ? { autoDeleteChapters: boolean; changeCategoriesPatch?: never } + ? { wasManuallyMarkedAsRead: boolean; changeCategoriesPatch?: never } : Action extends 'change_categories' - ? { autoDeleteChapters?: never; changeCategoriesPatch: UpdateMangaCategoriesPatchInput } - : { autoDeleteChapters?: boolean; changeCategoriesPatch?: UpdateMangaCategoriesPatchInput }, + ? { wasManuallyMarkedAsRead?: never; changeCategoriesPatch: UpdateMangaCategoriesPatchInput } + : { wasManuallyMarkedAsRead?: boolean; changeCategoriesPatch?: UpdateMangaCategoriesPatchInput }, ): Promise { switch (action) { case 'download': @@ -213,7 +210,7 @@ export class Mangas { case 'delete': return Mangas.deleteChapters(mangaIds); case 'mark_as_read': - return Mangas.markAsRead(mangaIds, autoDeleteChapters!); + return Mangas.markAsRead(mangaIds, wasManuallyMarkedAsRead!); case 'mark_as_unread': return Mangas.markAsUnread(mangaIds); case 'remove_from_library': @@ -221,7 +218,7 @@ export class Mangas { case 'change_categories': return Mangas.changeCategories(mangaIds, changeCategoriesPatch!); default: - throw new Error(`performMangasAction::performAction: unknown action "${action}"`); + throw new Error(`Mangas::performAction: unknown action "${action}"`); } } } diff --git a/src/lib/requests/RequestManager.ts b/src/lib/requests/RequestManager.ts index 14773f27..c68858fc 100644 --- a/src/lib/requests/RequestManager.ts +++ b/src/lib/requests/RequestManager.ts @@ -1452,6 +1452,13 @@ export class RequestManager { return this.doRequest(GQLMethod.USE_QUERY, GET_CHAPTERS, variables, options); } + public getChapters( + variables: GetChaptersQueryVariables, + options?: QueryOptions, + ): AbortabaleApolloQueryResponse { + return this.doRequest(GQLMethod.QUERY, GET_CHAPTERS, variables, options); + } + public useGetMangaChapters( mangaId: number | string, options?: QueryHookOptions,