diff --git a/src/components/MangaCard.tsx b/src/components/MangaCard.tsx index 105bc8aa..c6a15c91 100644 --- a/src/components/MangaCard.tsx +++ b/src/components/MangaCard.tsx @@ -12,7 +12,6 @@ import Typography from '@mui/material/Typography'; import { Link } from 'react-router-dom'; import { Avatar, Box, CardContent, Stack, styled, Tooltip } from '@mui/material'; import { useTranslation } from 'react-i18next'; -import React from 'react'; import PopupState, { bindMenu } from 'material-ui-popup-state'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { GridLayout, useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext'; @@ -21,7 +20,8 @@ import { TManga, TPartialManga } from '@/typings.ts'; import { ContinueReadingButton } from '@/components/manga/ContinueReadingButton.tsx'; import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts'; import { MangaOptionButton } from '@/components/manga/MangaOptionButton.tsx'; -import { MangaActionMenu } from '@/components/manga/MangaActionMenu.tsx'; +import { MangaActionMenuItems, SingleModeProps } from '@/components/manga/MangaActionMenuItems.tsx'; +import { Menu } from '@/components/manga/Menu.tsx'; const BottomGradient = styled('div')({ position: 'absolute', @@ -270,11 +270,16 @@ export const MangaCard = (props: IProps) => { {!!handleSelection && popupState.isOpen && ( - ['manga']} - handleSelection={handleSelection} - /> + + {(onClose, setHideMenu) => ( + + )} + )} )} @@ -380,11 +385,16 @@ export const MangaCard = (props: IProps) => { {!!handleSelection && popupState.isOpen && ( - ['manga']} - handleSelection={handleSelection} - /> + + {(onClose, setHideMenu) => ( + + )} + )} )} diff --git a/src/components/manga/MangaActionMenu.tsx b/src/components/manga/MangaActionMenu.tsx deleted file mode 100644 index 6d8f7abb..00000000 --- a/src/components/manga/MangaActionMenu.tsx +++ /dev/null @@ -1,117 +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 Menu from '@mui/material/Menu'; -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 { bindMenu } from 'material-ui-popup-state'; -import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; -import Label from '@mui/icons-material/Label'; -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 { CategorySelect } from '@/components/navbar/action/CategorySelect.tsx'; -import { MenuItem } from '@/components/manga/MenuItem.tsx'; - -export const MangaActionMenu = ({ - manga, - handleSelection, - ...bindMenuProps -}: { - manga: Pick & MangaDownloadInfo & MangaUnreadInfo; - handleSelection?: SelectableCollectionReturnType['handleSelection']; -} & ReturnType) => { - const { t } = useTranslation(); - - const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false); - - const isFullyDownloaded = manga.downloadCount === manga.chapters.totalCount; - const hasDownloadedChapters = !!manga.downloadCount; - const hasUnreadChapters = !!manga.unreadCount; - const hasReadChapters = manga.unreadCount !== manga.chapters.totalCount; - - const handleSelect = () => { - handleSelection?.(manga.id, true); - bindMenuProps.onClose(); - }; - - const performAction = (action: MangaAction) => { - Mangas.performAction(action, [manga.id], { - wasManuallyMarkedAsRead: true, - }).catch(() => {}); - - bindMenuProps.onClose(); - }; - - return ( - <> - - {!!handleSelection && ( - - )} - {!isFullyDownloaded && ( - performAction('download')} - Icon={Download} - title={t('chapter.action.download.add.label.action')} - /> - )} - {hasDownloadedChapters && ( - performAction('delete')} - Icon={Delete} - title={t('chapter.action.download.delete.label.action')} - /> - )} - {hasUnreadChapters && ( - performAction('mark_as_read')} - Icon={Done} - title={t('chapter.action.mark_as_read.add.label.action.current')} - /> - )} - {hasReadChapters && ( - performAction('mark_as_unread')} - Icon={RemoveDone} - title={t('chapter.action.mark_as_read.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 && ( - { - setIsCategorySelectOpen(false); - bindMenuProps.onClose(); - }} - mangaId={manga.id} - /> - )} - - ); -}; diff --git a/src/components/manga/MangaActionMenuItems.tsx b/src/components/manga/MangaActionMenuItems.tsx new file mode 100644 index 00000000..82393b8b --- /dev/null +++ b/src/components/manga/MangaActionMenuItems.tsx @@ -0,0 +1,169 @@ +/* + * 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 FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; +import Label from '@mui/icons-material/Label'; +import { useMemo, useState } from 'react'; +import { t as translate } from 'i18next'; +import { TManga } from '@/typings.ts'; +import { actionToTranslationKey, MangaAction, MangaDownloadInfo, Mangas, MangaUnreadInfo } from '@/lib/data/Mangas.ts'; +import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts'; +import { CategorySelect } from '@/components/navbar/action/CategorySelect.tsx'; +import { MenuItem } from '@/components/manga/MenuItem.tsx'; + +const ACTION_DISABLES_SELECTION_MODE: MangaAction[] = ['remove_from_library'] as const; + +const createGetMenuItemTitle = + (isSingleMode: boolean) => + (action: MangaAction, 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: (selectionModeState: boolean) => void; setHideMenu: (hide: boolean) => void }; + +export type SingleModeProps = { + manga: Pick & MangaDownloadInfo & MangaUnreadInfo; + handleSelection?: SelectableCollectionReturnType['handleSelection']; +}; + +type SelectModeProps = { + selectedMangas: TManga[]; +}; + +type Props = + | (BaseProps & SingleModeProps & PropertiesNever) + | (BaseProps & PropertiesNever & SelectModeProps); + +export const MangaActionMenuItems = ({ manga, handleSelection, selectedMangas = [], onClose, setHideMenu }: Props) => { + const { t } = useTranslation(); + + const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false); + + const isSingleMode = !!manga; + + const getMenuItemTitle = createGetMenuItemTitle(isSingleMode); + const shouldShowMenuItem = createShouldShowMenuItem(isSingleMode); + const isMenuItemDisabled = createIsMenuItemDisabled(isSingleMode); + + const isFullyDownloaded = !!manga && manga.downloadCount === manga.chapters.totalCount; + const hasDownloadedChapters = !!manga?.downloadCount; + const hasUnreadChapters = !!manga?.unreadCount; + const hasReadChapters = !!manga && manga.unreadCount !== manga.chapters.totalCount; + + const handleSelect = () => { + handleSelection?.(manga.id, true); + onClose(true); + }; + + const performAction = (action: MangaAction, mangas: TManga[]) => { + Mangas.performAction(action, manga ? [manga.id] : Mangas.getIds(mangas), { + wasManuallyMarkedAsRead: true, + }).catch(() => {}); + + onClose(!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 ( + <> + {!!handleSelection && isSingleMode && ( + + )} + {shouldShowMenuItem(!isFullyDownloaded) && ( + performAction('download', downloadableMangas)} + title={getMenuItemTitle('download', downloadableMangas.length)} + /> + )} + {shouldShowMenuItem(hasDownloadedChapters) && ( + performAction('delete', downloadedMangas)} + title={getMenuItemTitle('delete', downloadedMangas.length)} + /> + )} + {shouldShowMenuItem(hasUnreadChapters) && ( + performAction('mark_as_read', unreadMangas)} + title={getMenuItemTitle('mark_as_read', unreadMangas.length)} + /> + )} + {shouldShowMenuItem(hasReadChapters) && ( + performAction('mark_as_unread', readMangas)} + title={getMenuItemTitle('mark_as_unread', readMangas.length)} + /> + )} + { + setIsCategorySelectOpen(true); + setHideMenu(true); + }} + Icon={Label} + title={t('manga.action.category.label.action')} + /> + performAction('remove_from_library', selectedMangas)} + Icon={FavoriteBorderIcon} + title={t('manga.action.library.remove.label.action')} + /> + {isCategorySelectOpen && ( + { + setIsCategorySelectOpen(false); + onClose(true); + }} + mangaId={manga?.id as undefined} // either mangaId or mangaIds is undefined, however, ts is not able to infer it correctly and raises an error + mangaIds={Mangas.getIds(selectedMangas)} + /> + )} + + ); +}; diff --git a/src/components/manga/MangasSelectionFABActionItems.tsx b/src/components/manga/MangasSelectionFABActionItems.tsx deleted file mode 100644 index 5fdd2e92..00000000 --- a/src/components/manga/MangasSelectionFABActionItems.tsx +++ /dev/null @@ -1,109 +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 Done from '@mui/icons-material/Done'; -import RemoveDone from '@mui/icons-material/RemoveDone'; -import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; -import Label from '@mui/icons-material/Label'; -import { useMemo, useState } from 'react'; -import { t as translate } from 'i18next'; -import { MenuItem } from '@/components/manga/MenuItem.tsx'; -import { TManga } from '@/typings.ts'; -import { actionToTranslationKey, MangaAction, Mangas } from '@/lib/data/Mangas.ts'; -import { CategorySelect } from '@/components/navbar/action/CategorySelect.tsx'; - -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 = ({ - selectedMangas, - handleClose, -}: { - selectedMangas: TManga[]; - handleClose: (selectionModeState: boolean) => void; -}) => { - const [isCategorySelectOpen, setIsCategorySelectOpen] = useState(false); - - const handleAction = (action: MangaAction, mangas: TManga[]) => { - Mangas.performAction(action, Mangas.getIds(mangas), { - 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 ( - <> - handleAction('download', downloadableMangas)} - title={getMenuItemTitle('download', downloadableMangas.length)} - /> - handleAction('delete', downloadedMangas)} - title={getMenuItemTitle('delete', downloadedMangas.length)} - /> - handleAction('mark_as_read', unreadMangas)} - title={getMenuItemTitle('mark_as_read', unreadMangas.length)} - /> - handleAction('mark_as_unread', readMangas)} - title={getMenuItemTitle('mark_as_unread', readMangas.length)} - /> - handleAction('change_categories', selectedMangas)} - title={getMenuItemTitle('change_categories', selectedMangas.length)} - /> - handleAction('remove_from_library', selectedMangas)} - title={getMenuItemTitle('remove_from_library', selectedMangas.length)} - /> - {isCategorySelectOpen && ( - { - setIsCategorySelectOpen(false); - handleClose(true); - }} - mangaIds={Mangas.getIds(selectedMangas)} - /> - )} - - ); -}; diff --git a/src/components/manga/Menu.tsx b/src/components/manga/Menu.tsx index a7fe46af..6f60eadd 100644 --- a/src/components/manga/Menu.tsx +++ b/src/components/manga/Menu.tsx @@ -8,14 +8,29 @@ import { MenuProps } from '@mui/material/Menu/Menu'; import { Menu as MuiMenu } from '@mui/material'; +import { useState } from 'react'; export const Menu = ({ children, onClose, ...props }: Omit & - Required> & { children: (onClose: () => void) => JSX.Element }) => ( - - {children(() => onClose({}, 'backdropClick'))} - -); + Required> & { + children: (onClose: () => void, setHideMenu: (hide: boolean) => void) => JSX.Element; + }) => { + const [shouldHideMenu, setShouldHideMenu] = useState(false); + + return ( + + {children(() => { + onClose({}, 'backdropClick'); + setShouldHideMenu(false); + }, setShouldHideMenu)} + + ); +}; diff --git a/src/components/manga/SelectionFAB.tsx b/src/components/manga/SelectionFAB.tsx index 474da598..fdd8ee54 100644 --- a/src/components/manga/SelectionFAB.tsx +++ b/src/components/manga/SelectionFAB.tsx @@ -16,7 +16,7 @@ import { TranslationKey } from '@/typings.ts'; import { Menu } from '@/components/manga/Menu.tsx'; interface SelectionFABProps { - children: (handleClose: () => void) => JSX.Element; + children: (handleClose: () => void, setHideMenu: (hide: boolean) => void) => JSX.Element; selectedItemsCount: number; title: TranslationKey; } @@ -53,7 +53,7 @@ export const SelectionFAB: React.FC = ({ children, selectedIt 'aria-labelledby': 'selectionMenuButton', }} > - {(onClose) => children(onClose)} + {(onClose, setHideMenu) => children(onClose, setHideMenu)} )} diff --git a/src/lib/data/Mangas.ts b/src/lib/data/Mangas.ts index ab2261cf..f60be2db 100644 --- a/src/lib/data/Mangas.ts +++ b/src/lib/data/Mangas.ts @@ -28,6 +28,7 @@ export type MangaAction = export const actionToTranslationKey: { [key in MangaAction]: { action: { + single: TranslationKey; selected: TranslationKey; }; success: TranslationKey; @@ -36,6 +37,7 @@ export const actionToTranslationKey: { } = { download: { action: { + single: 'chapter.action.download.add.label.action', selected: 'chapter.action.download.add.button.selected', }, success: 'chapter.action.download.add.label.success', @@ -43,6 +45,7 @@ export const actionToTranslationKey: { }, delete: { action: { + single: 'chapter.action.download.delete.label.action', selected: 'chapter.action.download.delete.button.selected', }, success: 'chapter.action.download.delete.label.success', @@ -50,6 +53,7 @@ export const actionToTranslationKey: { }, 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', @@ -57,6 +61,7 @@ export const actionToTranslationKey: { }, 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', @@ -64,6 +69,7 @@ export const actionToTranslationKey: { }, remove_from_library: { action: { + single: 'manga.action.library.remove.label.action', selected: 'manga.action.library.remove.button.selected', }, success: 'manga.action.library.remove.label.success', @@ -71,6 +77,7 @@ export const actionToTranslationKey: { }, change_categories: { action: { + single: 'manga.action.category.label.action', selected: 'manga.action.category.button.selected', }, success: 'manga.action.category.label.success', diff --git a/src/screens/Library.tsx b/src/screens/Library.tsx index daceb8d0..bec29f22 100644 --- a/src/screens/Library.tsx +++ b/src/screens/Library.tsx @@ -25,8 +25,8 @@ import { TManga } from '@/typings.ts'; import { SelectableCollectionSelectMode } from '@/components/collection/SelectableCollectionSelectMode.tsx'; import { useGetVisibleLibraryMangas } from '@/components/library/useGetVisibleLibraryMangas.ts'; import { SelectionFAB } from '@/components/manga/SelectionFAB.tsx'; -import { MangasSelectionFABActionItems } from '@/components/manga/MangasSelectionFABActionItems.tsx'; import { PARTIAL_MANGA_FIELDS } from '@/lib/graphql/Fragments.ts'; +import { MangaActionMenuItems } from '@/components/manga/MangaActionMenuItems.tsx'; const StyledGridWrapper = styled(Box)(({ theme }) => ({ // TabsMenu height + TabsMenu bottom padding - grid item top padding @@ -264,13 +264,14 @@ export function Library() { {isSelectModeActive && ( - {(handleClose) => ( - ( + { + onClose={(selectionModeState) => { handleClose(); setIsSelectModeActive(selectionModeState); }} + setHideMenu={setHideMenu} /> )}