diff --git a/src/components/MangaCard.tsx b/src/components/MangaCard.tsx index 961462d8..e09ffb36 100644 --- a/src/components/MangaCard.tsx +++ b/src/components/MangaCard.tsx @@ -6,74 +6,19 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import Card from '@mui/material/Card'; -import CardActionArea from '@mui/material/CardActionArea'; -import Typography from '@mui/material/Typography'; -import { Link as RouterLink } from 'react-router-dom'; -import { Avatar, Box, Button, CardContent, Link, Stack, styled, Tooltip } from '@mui/material'; -import { useTranslation } from 'react-i18next'; import PopupState, { bindMenu } from 'material-ui-popup-state'; -import { useRef, useState } from 'react'; +import { useMemo, useState } from 'react'; import { useLongPress } from 'use-long-press'; -import { isMobile } from 'react-device-detect'; import { GridLayout, useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext'; -import { SpinnerImage } from '@/components/util/SpinnerImage'; -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 { MangaActionMenuItems, SingleModeProps } from '@/components/manga/MangaActionMenuItems.tsx'; import { Menu } from '@/components/menu/Menu.tsx'; import { MigrateDialog } from '@/components/MigrateDialog.tsx'; -import { Mangas } from '@/lib/data/Mangas.ts'; -import { TypographyMaxLines } from '@/components/atoms/TypographyMaxLines.tsx'; import { useManageMangaLibraryState } from '@/components/manga/useManageMangaLibraryState.tsx'; - -const BottomGradient = styled('div')({ - position: 'absolute', - bottom: 0, - width: '100%', - height: '30%', - background: 'linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 100%)', -}); - -const BottomGradientDoubledDown = styled('div')({ - position: 'absolute', - bottom: 0, - width: '100%', - height: '20%', - background: 'linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 100%)', -}); - -const MangaTitle = TypographyMaxLines; - -const GridMangaTitle = styled(MangaTitle)({ - fontSize: '1.05rem', -}); - -const BadgeContainer = styled('div')({ - display: 'flex', - height: 'fit-content', - borderRadius: '5px', - overflow: 'hidden', - '& p': { - color: 'white', - padding: '0.1em', - paddingInline: '0.2em', - fontSize: '1.05rem', - }, -}); - -type MangaCardMode = 'default' | 'source' | 'migrate.search' | 'migrate.select'; - -export interface MangaCardProps { - manga: TPartialManga; - gridLayout?: GridLayout; - inLibraryIndicator?: boolean; - selected?: boolean | null; - handleSelection?: SelectableCollectionReturnType['handleSelection']; - mode?: MangaCardMode; -} +import { MangaGridCard } from '@/components/manga/MangaGridCard.tsx'; +import { MangaListCard } from '@/components/manga/MangaListCard.tsx'; +import { MangaCardMode, MangaCardProps } from '@/components/manga/MangaCard.types'; +import { ContinueReadingButton } from '@/components/manga/ContinueReadingButton.tsx'; +import { MangaBadges } from '@/components/manga/MangaBadges.tsx'; const getMangaLinkTo = ( mode: MangaCardMode, @@ -95,15 +40,10 @@ const getMangaLinkTo = ( }; export const MangaCard = (props: MangaCardProps) => { - const { t } = useTranslation(); - - const optionButtonRef = useRef(null); - const { manga, gridLayout, inLibraryIndicator, selected, handleSelection, mode = 'default' } = props; - const { id, title, downloadCount, unreadCount: unread, latestReadChapter, firstUnreadChapter, chapters } = manga; - const thumbnailUrl = Mangas.getThumbnailUrl(manga); + const { id, latestReadChapter, firstUnreadChapter, chapters, downloadCount, unreadCount } = manga; const { - options: { showContinueReadingButton, showUnreadBadge, showDownloadBadge }, + options: { showContinueReadingButton }, } = useLibraryOptionsContext(); const { CategorySelectComponent, updateLibraryState, isInLibrary } = useManageMangaLibraryState(manga); @@ -155,229 +95,35 @@ export const MangaCard = (props: MangaCardProps) => { handleClick(e, context as () => {}); }); - if (gridLayout !== GridLayout.List) { - return ( - <> - {isMigrateDialogOpen && ( - setIsMigrateDialogOpen(false)} /> - )} - - {(popupState) => ( - <> - popupState.open(optionButtonRef.current))} - onClick={handleClick} - to={mangaLinkTo} - sx={{ textDecoration: 'none', touchCallout: 'none' }} - > - theme.palette.primary.main, - backgroundColor: (theme) => (selected ? theme.palette.primary.main : undefined), - '@media (hover: hover) and (pointer: fine)': { - '&:hover .manga-option-button': { - visibility: 'visible', - pointerEvents: 'all', - }, - }, - '&:hover .source-manga-library-state-button': { - display: isMobile ? 'none' : 'inline-flex', - }, - '&:hover .source-manga-library-state-indicator': { - display: 'none', - }, - }} - > - - - - - - {inLibraryIndicator && ( - - )} - {inLibraryIndicator && isInLibrary && ( - - {t('manga.button.in_library')} - - )} - {showUnreadBadge && (unread ?? 0) > 0 && ( - - {unread} - - )} - {showDownloadBadge && (downloadCount ?? 0) > 0 && ( - - {downloadCount} - - )} - - - - <> - {gridLayout !== GridLayout.Comfortable && ( - <> - - - - )} - - {gridLayout !== GridLayout.Comfortable && ( - - - {title} - - - )} - - - - - - {gridLayout === GridLayout.Comfortable && ( - - - {title} - - - )} - - - {!!handleSelection && popupState.isOpen && ( - - {(onClose, setHideMenu) => ( - - )} - - )} - {CategorySelectComponent} - - )} - - - ); - } + const MangaCardComponent = useMemo( + () => (gridLayout === GridLayout.List ? MangaListCard : MangaGridCard), + [gridLayout], + ); + + const continueReadingButton = useMemo( + () => ( + + ), + [showContinueReadingButton, isLatestChapterRead, nextChapterIndexToRead, mangaLinkTo], + ); + + const mangaBadges = useMemo( + () => ( + + ), + [inLibraryIndicator, isInLibrary, unreadCount, downloadCount, updateLibraryState], + ); return ( <> @@ -387,139 +133,17 @@ export const MangaCard = (props: MangaCardProps) => { {(popupState) => ( <> - - popupState.open(optionButtonRef.current))} - sx={{ - touchCallout: 'none', - '@media (hover: hover) and (pointer: fine)': { - '&:hover .manga-option-button': { - visibility: 'visible', - pointerEvents: 'all', - }, - }, - '&:hover .source-manga-library-state-button': { - display: isMobile ? 'none' : 'inline-flex', - }, - '&:hover .source-manga-library-state-indicator': { - display: 'none', - }, - }} - > - - - - - - - {title} - - - - - {inLibraryIndicator && ( - - )} - {inLibraryIndicator && isInLibrary && ( - - {t('manga.button.in_library')} - - )} - {showUnreadBadge && (unread ?? 0) > 0 && ( - - {unread} - - )} - {showDownloadBadge && (downloadCount ?? 0) > 0 && ( - - {downloadCount} - - )} - - - - - - - + {!!handleSelection && popupState.isOpen && ( {(onClose, setHideMenu) => ( diff --git a/src/components/MangaGrid.tsx b/src/components/MangaGrid.tsx index b21de5be..7d301ecf 100644 --- a/src/components/MangaGrid.tsx +++ b/src/components/MangaGrid.tsx @@ -13,13 +13,14 @@ import { GridItemProps, GridStateSnapshot, VirtuosoGrid } from 'react-virtuoso'; import { useLocation } from 'react-router-dom'; import { EmptyView } from '@/components/util/EmptyView'; import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder'; -import { MangaCard, MangaCardProps } from '@/components/MangaCard'; +import { MangaCard } from '@/components/MangaCard'; import { GridLayout } from '@/components/context/LibraryOptionsContext'; import { useLocalStorage, useSessionStorage } from '@/util/useStorage.tsx'; import { TManga, TPartialManga } from '@/typings.ts'; import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts'; import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab.tsx'; import { AppStorage } from '@/util/AppStorage.ts'; +import { MangaCardProps } from '@/components/manga/MangaCard.types.tsx'; const GridContainer = React.forwardRef(({ children, ...props }, ref) => ( diff --git a/src/components/manga/MangaBadges.tsx b/src/components/manga/MangaBadges.tsx new file mode 100644 index 00000000..42f937ff --- /dev/null +++ b/src/components/manga/MangaBadges.tsx @@ -0,0 +1,88 @@ +/* + * 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 styled from '@mui/material/styles/styled'; +import { useTranslation } from 'react-i18next'; +import Button from '@mui/material/Button/Button'; +import Typography from '@mui/material/Typography/Typography'; +import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext.tsx'; + +const BadgeContainer = styled('div')({ + display: 'flex', + height: 'fit-content', + borderRadius: '5px', + overflow: 'hidden', + '& p': { + color: 'white', + padding: '0.1em', + paddingInline: '0.2em', + fontSize: '1.05rem', + }, +}); + +export const MangaBadges = ({ + inLibraryIndicator, + updateLibraryState, + isInLibrary, + unread, + downloadCount, +}: { + inLibraryIndicator?: boolean; + updateLibraryState: () => void; + isInLibrary: boolean; + unread?: number; + downloadCount?: number; +}) => { + const { t } = useTranslation(); + + const { + options: { showUnreadBadge, showDownloadBadge }, + } = useLibraryOptionsContext(); + + return ( + + {inLibraryIndicator && ( + + )} + {inLibraryIndicator && isInLibrary && ( + + {t('manga.button.in_library')} + + )} + {showUnreadBadge && (unread ?? 0) > 0 && ( + {unread} + )} + {showDownloadBadge && (downloadCount ?? 0) > 0 && ( + + {downloadCount} + + )} + + ); +}; diff --git a/src/components/manga/MangaCard.types.tsx b/src/components/manga/MangaCard.types.tsx new file mode 100644 index 00000000..c821ffa4 --- /dev/null +++ b/src/components/manga/MangaCard.types.tsx @@ -0,0 +1,35 @@ +/* + * 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 { LongPressPointerHandlers, LongPressResult } from 'use-long-press/lib/use-long-press.types'; +import { PopupState } from 'material-ui-popup-state/hooks'; +import { TManga, TPartialManga } from '@/typings.ts'; +import { GridLayout } from '@/components/context/LibraryOptionsContext.tsx'; +import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts'; +import { useManageMangaLibraryState } from '@/components/manga/useManageMangaLibraryState.tsx'; + +export type MangaCardMode = 'default' | 'source' | 'migrate.search' | 'migrate.select'; + +export interface MangaCardProps { + manga: TPartialManga; + gridLayout?: GridLayout; + inLibraryIndicator?: boolean; + selected?: boolean | null; + handleSelection?: SelectableCollectionReturnType['handleSelection']; + mode?: MangaCardMode; +} + +export type SpecificMangaCardProps = Omit & + Pick, 'isInLibrary'> & { + longPressBind: LongPressResult; + popupState: PopupState; + handleClick: (event: React.MouseEvent | React.TouchEvent) => void; + mangaLinkTo: string; + continueReadingButton: JSX.Element; + mangaBadges: JSX.Element; + }; diff --git a/src/components/manga/MangaGridCard.tsx b/src/components/manga/MangaGridCard.tsx new file mode 100644 index 00000000..200ef657 --- /dev/null +++ b/src/components/manga/MangaGridCard.tsx @@ -0,0 +1,207 @@ +/* + * 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 Link from '@mui/material/Link/Link'; +import { Link as RouterLink } from 'react-router-dom'; +import Box from '@mui/material/Box/Box'; +import Card from '@mui/material/Card/Card'; +import CardActionArea from '@mui/material/CardActionArea/CardActionArea'; +import Stack from '@mui/material/Stack/Stack'; +import Tooltip from '@mui/material/Tooltip/Tooltip'; +import styled from '@mui/material/styles/styled'; +import { isMobile } from 'react-device-detect'; +import { useRef } from 'react'; +import { SpinnerImage } from '@/components/util/SpinnerImage.tsx'; +import { MangaOptionButton } from '@/components/manga/MangaOptionButton.tsx'; +import { GridLayout } from '@/components/context/LibraryOptionsContext.tsx'; +import { TypographyMaxLines } from '@/components/atoms/TypographyMaxLines.tsx'; +import { Mangas } from '@/lib/data/Mangas.ts'; +import { SpecificMangaCardProps } from '@/components/manga/MangaCard.types.tsx'; + +const BottomGradient = styled('div')({ + position: 'absolute', + bottom: 0, + width: '100%', + height: '30%', + background: 'linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 100%)', +}); + +const BottomGradientDoubledDown = styled('div')({ + position: 'absolute', + bottom: 0, + width: '100%', + height: '20%', + background: 'linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 100%)', +}); + +const GridMangaTitle = styled(TypographyMaxLines)({ + fontSize: '1.05rem', +}); + +export const MangaGridCard = ({ + manga, + longPressBind, + popupState, + handleClick, + mangaLinkTo, + selected, + inLibraryIndicator, + isInLibrary, + gridLayout, + handleSelection, + continueReadingButton, + mangaBadges, +}: SpecificMangaCardProps) => { + const optionButtonRef = useRef(null); + + const { id, title } = manga; + + return ( + popupState.open(optionButtonRef.current))} + onClick={handleClick} + to={mangaLinkTo} + sx={{ textDecoration: 'none', touchCallout: 'none' }} + > + theme.palette.primary.main, + backgroundColor: (theme) => (selected ? theme.palette.primary.main : undefined), + '@media (hover: hover) and (pointer: fine)': { + '&:hover .manga-option-button': { + visibility: 'visible', + pointerEvents: 'all', + }, + }, + '&:hover .source-manga-library-state-button': { + display: isMobile ? 'none' : 'inline-flex', + }, + '&:hover .source-manga-library-state-indicator': { + display: 'none', + }, + }} + > + + + + + {mangaBadges} + + + <> + {gridLayout !== GridLayout.Comfortable && ( + <> + + + + )} + + {gridLayout !== GridLayout.Comfortable && ( + + + {title} + + + )} + {continueReadingButton} + + + + + {gridLayout === GridLayout.Comfortable && ( + + + {title} + + + )} + + + ); +}; diff --git a/src/components/manga/MangaListCard.tsx b/src/components/manga/MangaListCard.tsx new file mode 100644 index 00000000..20a469ea --- /dev/null +++ b/src/components/manga/MangaListCard.tsx @@ -0,0 +1,124 @@ +/* + * 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 Card from '@mui/material/Card/Card'; +import CardContent from '@mui/material/CardContent/CardContent'; +import CardActionArea from '@mui/material/CardActionArea/CardActionArea'; +import Avatar from '@mui/material/Avatar/Avatar'; +import Box from '@mui/material/Box/Box'; +import Stack from '@mui/material/Stack/Stack'; +import Tooltip from '@mui/material/Tooltip/Tooltip'; +import { isMobile } from 'react-device-detect'; +import { Link as RouterLink } from 'react-router-dom'; +import { useRef } from 'react'; +import { SpinnerImage } from '@/components/util/SpinnerImage.tsx'; +import { TypographyMaxLines } from '@/components/atoms/TypographyMaxLines.tsx'; +import { SpecificMangaCardProps } from '@/components/manga/MangaCard.types.tsx'; +import { Mangas } from '@/lib/data/Mangas.ts'; +import { MangaOptionButton } from '@/components/manga/MangaOptionButton.tsx'; + +export const MangaListCard = ({ + manga, + longPressBind, + popupState, + handleClick, + mangaLinkTo, + selected, + inLibraryIndicator, + isInLibrary, + handleSelection, + continueReadingButton, + mangaBadges, +}: SpecificMangaCardProps) => { + const optionButtonRef = useRef(null); + + const { id, title } = manga; + + return ( + + popupState.open(optionButtonRef.current))} + sx={{ + touchCallout: 'none', + '@media (hover: hover) and (pointer: fine)': { + '&:hover .manga-option-button': { + visibility: 'visible', + pointerEvents: 'all', + }, + }, + '&:hover .source-manga-library-state-button': { + display: isMobile ? 'none' : 'inline-flex', + }, + '&:hover .source-manga-library-state-indicator': { + display: 'none', + }, + }} + > + + + + + + + {title} + + + + {mangaBadges} + {continueReadingButton} + + + + + + ); +}; diff --git a/src/screens/SearchAll.tsx b/src/screens/SearchAll.tsx index 32d705eb..f019239e 100644 --- a/src/screens/SearchAll.tsx +++ b/src/screens/SearchAll.tsx @@ -21,7 +21,7 @@ import { LangSelect } from '@/components/navbar/action/LangSelect'; import { MangaGrid } from '@/components/MangaGrid'; import { useDebounce } from '@/util/useDebounce.ts'; import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext.tsx'; -import { MangaCardProps } from '@/components/MangaCard.tsx'; +import { MangaCardProps } from '@/components/manga/MangaCard.types.tsx'; type SourceLoadingState = { isLoading: boolean; hasResults: boolean; emptySearch: boolean }; type SourceToLoadingStateMap = Map; diff --git a/src/util/defaultPromiseErrorHandler.ts b/src/util/defaultPromiseErrorHandler.ts index 3e79e0df..96131f93 100644 --- a/src/util/defaultPromiseErrorHandler.ts +++ b/src/util/defaultPromiseErrorHandler.ts @@ -6,6 +6,11 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -export const defaultPromiseErrorHandler = (name: string) => (error: any) => +export const defaultPromiseErrorHandler = (name: string) => (error: any) => { + if (process.env.NODE_ENV === 'production') { + return; + } + // eslint-disable-next-line no-console console.error(`${name} failed due to`, error); +};