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:
schroda
2023-12-30 04:04:59 +01:00
committed by GitHub
parent d15be603b1
commit a3a52064cc
14 changed files with 799 additions and 587 deletions

View File

@@ -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<IProps> = ({ 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<IProps> = ({ manga, isRefreshing }) => {
const areAllChaptersRead = manga.unreadCount === 0;
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 noChaptersMatchingFilter = !noChaptersFound && visibleChapters.length === 0;
@@ -208,11 +110,7 @@ export const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
return (
<SelectionFAB selectedItemsCount={selectedChapters.length} title="chapter.title">
{(handleClose) => (
<ChapterSelectionFABActionItems
selectedChapters={selectedChapters}
onAction={handleFabAction}
handleClose={handleClose}
/>
<ChapterActionMenuItems selectedChapters={selectedChapters} onClose={handleClose} />
)}
</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')}>
<IconButton
disabled={areAllChaptersRead}
onClick={() => handleFabAction('mark_as_read', chaptersWithMeta)}
onClick={() =>
Chapters.markAsRead(
ChaptersWithMeta.getChapters(ChaptersWithMeta.getNonRead(chaptersWithMeta)),
true,
)
}
>
<DoneAllIcon />
</IconButton>
@@ -261,7 +164,11 @@ export const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
<Tooltip title={t('chapter.action.download.add.label.action')}>
<IconButton
disabled={areAllChaptersDownloaded}
onClick={() => handleFabAction('download', chaptersWithMeta)}
onClick={() =>
Chapters.download(
ChaptersWithMeta.getIds(ChaptersWithMeta.getNonDownloaded(chaptersWithMeta)),
)
}
>
<DownloadIcon />
</IconButton>