From 41d876e1561f979aa83cb18005874e85a4646139 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sun, 7 Apr 2024 15:21:25 +0200 Subject: [PATCH] Feature/utilize long press (#709) * Open manga action menu on long press * Open chapter action menu on long press --- src/components/MangaCard.tsx | 67 ++++--- src/components/chapter/ChapterCard.tsx | 206 ++++++++++----------- src/components/manga/MangaOptionButton.tsx | 158 ++++++++-------- 3 files changed, 221 insertions(+), 210 deletions(-) diff --git a/src/components/MangaCard.tsx b/src/components/MangaCard.tsx index b464e807..132cde29 100644 --- a/src/components/MangaCard.tsx +++ b/src/components/MangaCard.tsx @@ -9,11 +9,11 @@ import Card from '@mui/material/Card'; import CardActionArea from '@mui/material/CardActionArea'; import Typography from '@mui/material/Typography'; -import { Link } from 'react-router-dom'; -import { Avatar, Box, CardContent, Stack, styled, Tooltip } from '@mui/material'; +import { Link as RouterLink } from 'react-router-dom'; +import { Avatar, Box, CardContent, Link, Stack, styled, Tooltip } from '@mui/material'; import { useTranslation } from 'react-i18next'; import PopupState, { bindMenu } from 'material-ui-popup-state'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { useLongPress } from 'use-long-press'; import { GridLayout, useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext'; import { SpinnerImage } from '@/components/util/SpinnerImage'; @@ -94,6 +94,8 @@ 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, @@ -117,18 +119,36 @@ export const MangaCard = (props: MangaCardProps) => { const [isMigrateDialogOpen, setIsMigrateDialogOpen] = useState(false); - const handleClick = (e: React.MouseEvent | React.TouchEvent) => { - if (selected === null) { + const handleClick = (event: React.MouseEvent | React.TouchEvent, openMenu?: () => void) => { + const isDefaultMode = mode === 'default'; + const isMigrateSelectMode = mode === 'migrate.select'; + const isSelectionMode = selected !== null; + + const shouldHandleClick = isMigrateSelectMode || isSelectionMode || (isDefaultMode && !!openMenu); + if (!shouldHandleClick) { return; } - e.preventDefault(); - handleSelection?.(id, !selected, { selectRange: e.shiftKey }); + event.preventDefault(); + + if (isSelectionMode) { + handleSelection?.(id, !selected, { selectRange: event.shiftKey }); + return; + } + + if (isDefaultMode) { + openMenu?.(); + return; + } + + if (isMigrateSelectMode) { + setIsMigrateDialogOpen(true); + } }; - const longPressBind = useLongPress((e) => { + const longPressBind = useLongPress((e, { context }) => { e.shiftKey = true; - handleClick(e); + handleClick(e, context as () => {}); }); if (gridLayout !== GridLayout.List) { @@ -141,27 +161,11 @@ export const MangaCard = (props: MangaCardProps) => { {(popupState) => ( <> { - const isMigrateSelectMode = mode === 'migrate.select'; - const isSelectionMode = selected !== null; - - const shouldHandleClick = isMigrateSelectMode || isSelectionMode; - if (!shouldHandleClick) { - return; - } - - e.preventDefault(); - - if (isMigrateSelectMode) { - setIsMigrateDialogOpen(true); - return; - } - - handleClick(e); - }} + component={RouterLink} + {...longPressBind(() => popupState.open(optionButtonRef.current))} + onClick={handleClick} to={mangaLinkTo} - style={{ textDecoration: 'none' }} + sx={{ textDecoration: 'none', touchCallout: 'none' }} > { )} { <> { mangaLinkTo={mangaLinkTo} /> = (props: IProps) => { const { t } = useTranslation(); const theme = useTheme(); + const menuButtonRef = useRef(null); + const { chapter, allChapters, downloadChapter: dc, showChapterNumber, onSelect, selected } = props; const isSelecting = selected !== null; const { isDownloaded } = chapter; - const handleClick = (e: MouseEvent | TouchEvent) => { + const handleClick = (event: MouseEvent | TouchEvent) => { if (!isSelecting) return; - e.preventDefault(); - e.stopPropagation(); - onSelect(!selected, e.shiftKey); + event.preventDefault(); + event.stopPropagation(); + onSelect(!selected, event.shiftKey); }; - const longPressBind = useLongPress((e) => { - e.shiftKey = true; - handleClick(e); + const handleClickOpenMenu = ( + event: React.MouseEvent | React.TouchEvent, + openMenu: (e: React.SyntheticEvent) => void, + ) => { + event.stopPropagation(); + event.preventDefault(); + openMenu(event); + }; + + const longPressBind = useLongPress((event, { context: openMenu }) => { + if (!isSelecting && !!menuButtonRef.current) { + handleClickOpenMenu(event, () => (openMenu as (event: Element) => void)?.(menuButtonRef.current!)); + return; + } + + // eslint-disable-next-line no-param-reassign + event.shiftKey = true; + handleClick(event); }); 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 ( - <> - ( + <> + + handleClick(e)} + {...longPressBind(popupState.open)} > - handleClick(e)} - {...longPressBind()} > - - - - {chapter.isBookmarked && ( - - )} - {showChapterNumber - ? `${t('chapter.title')} ${chapter.chapterNumber}` - : chapter.name} - - {chapter.scanlator} - - {getUploadDateString(Number(chapter.uploadDate ?? 0))} - {isDownloaded && ` • ${t('chapter.status.label.downloaded')}`} - - + + + {chapter.isBookmarked && ( + + )} + {showChapterNumber + ? `${t('chapter.title')} ${chapter.chapterNumber}` + : chapter.name} + + {chapter.scanlator} + + {getUploadDateString(Number(chapter.uploadDate ?? 0))} + {isDownloaded && ` • ${t('chapter.status.label.downloaded')}`} + + - {dc && } + {dc && } - {selected === null ? ( - - - - - - ) : ( - + handleClickOpenMenu(e, popupState.open)} + onTouchStart={(e) => handleClickOpenMenu(e, popupState.open)} + aria-label="more" + size="large" > - - - )} - - - - {!isSelecting && popupState.isOpen && ( - - {(onClose) => ( - onSelect(true)} - canBeDownloaded={!chapter.isDownloaded && !dc} - /> + + + + ) : ( + + + )} - - )} - - ); - }} + + + + {!isSelecting && popupState.isOpen && ( + + {(onClose) => ( + onSelect(true)} + canBeDownloaded={!chapter.isDownloaded && !dc} + /> + )} + + )} + + )}
  • ); diff --git a/src/components/manga/MangaOptionButton.tsx b/src/components/manga/MangaOptionButton.tsx index 21c04def..b665769e 100644 --- a/src/components/manga/MangaOptionButton.tsx +++ b/src/components/manga/MangaOptionButton.tsx @@ -7,7 +7,7 @@ */ import { useTranslation } from 'react-i18next'; -import React, { TouchEvent, useMemo } from 'react'; +import { BaseSyntheticEvent, MouseEvent, TouchEvent, ChangeEvent, useMemo, forwardRef, ForwardedRef } from 'react'; import { Button, Tooltip } from '@mui/material'; import Checkbox from '@mui/material/Checkbox'; import IconButton from '@mui/material/IconButton'; @@ -18,97 +18,101 @@ import { isMobile } from 'react-device-detect'; import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts'; import { TManga } from '@/typings.ts'; -export const MangaOptionButton = ({ - id, - selected, - handleSelection, - asCheckbox = false, - popupState, -}: { - id: number; - selected?: boolean | null; - handleSelection?: SelectableCollectionReturnType['handleSelection']; - asCheckbox?: boolean; - popupState: PopupState; -}) => { - const { t } = useTranslation(); +export const MangaOptionButton = forwardRef( + ( + { + id, + selected, + handleSelection, + asCheckbox = false, + popupState, + }: { + id: number; + selected?: boolean | null; + handleSelection?: SelectableCollectionReturnType['handleSelection']; + asCheckbox?: boolean; + popupState: PopupState; + }, + ref: ForwardedRef, + ) => { + const { t } = useTranslation(); - const bindTriggerProps = useMemo(() => bindTrigger(popupState), [popupState]); + const bindTriggerProps = useMemo(() => bindTrigger(popupState), [popupState]); - const preventDefaultAction = (e: React.BaseSyntheticEvent) => { - e.stopPropagation(); - e.preventDefault(); - }; + const preventDefaultAction = (e: BaseSyntheticEvent) => { + e.stopPropagation(); + e.preventDefault(); + }; - const handleSelectionChange = (e: React.BaseSyntheticEvent, isSelected: boolean) => { - preventDefaultAction(e); - handleSelection?.(id, isSelected); - }; + const handleSelectionChange = (e: ChangeEvent, isSelected: boolean) => { + preventDefaultAction(e); + handleSelection?.(id, isSelected); + }; - const handleClick = (e: React.BaseSyntheticEvent) => { - preventDefaultAction(e); - bindTriggerProps.onClick(e as any); - }; + const handleClick = (e: MouseEvent | TouchEvent) => { + if (isMobile) return; - const handleTouchStart = (e: React.BaseSyntheticEvent) => { - preventDefaultAction(e); - bindTriggerProps.onTouchStart(e as TouchEvent); - }; + preventDefaultAction(e); + popupState.open(e); + bindTriggerProps.onClick(e as any); + }; - if (!handleSelection) { - return null; - } - - const isSelected = selected !== null; - if (isSelected) { - if (!asCheckbox) { + if (!handleSelection) { return null; } - return ( - - - - ); - } + const isSelected = selected !== null; + if (isSelected) { + if (!asCheckbox) { + return null; + } + + return ( + + + + ); + } + + if (asCheckbox) { + return ( + + + + + + ); + } - if (asCheckbox) { return ( - e.stopPropagation()} > - + ); - } - - return ( - - - - ); -}; + }, +);