forked from akai/readest
This commit is contained in:
@@ -2,8 +2,6 @@ import clsx from 'clsx';
|
||||
import * as React from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { MdDelete, MdOpenInNew, MdOutlineCancel, MdInfoOutline } from 'react-icons/md';
|
||||
import { LuFolderPlus } from 'react-icons/lu';
|
||||
import { PiPlus } from 'react-icons/pi';
|
||||
import { Book } from '@/types/book';
|
||||
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
|
||||
@@ -17,12 +15,12 @@ import { navigateToLibrary, navigateToReader, showReaderWindow } from '@/utils/n
|
||||
import { createBookFilter, createBookSorter } from '../utils/libraryUtils';
|
||||
import { formatTitle } from '@/utils/book';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { isMd5 } from '@/utils/md5';
|
||||
|
||||
import Alert from '@/components/Alert';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import ModalPortal from '@/components/ModalPortal';
|
||||
import BookshelfItem, { generateBookshelfItems } from './BookshelfItem';
|
||||
import SelectModeActions from './SelectModeActions';
|
||||
import GroupingModal from './GroupingModal';
|
||||
|
||||
interface BookshelfProps {
|
||||
@@ -372,72 +370,17 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
<Spinner loading />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className='fixed bottom-0 left-0 right-0 z-40'
|
||||
style={{
|
||||
paddingBottom: `${(safeAreaInsets?.bottom || 0) + 16}px`,
|
||||
}}
|
||||
>
|
||||
{isSelectMode && showSelectModeActions && (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center justify-center shadow-lg',
|
||||
'bg-base-300 text-base-content text-xs',
|
||||
'mx-auto w-fit space-x-6 rounded-lg p-4',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
onClick={openSelectedBooks}
|
||||
className={clsx(
|
||||
'flex flex-col items-center justify-center gap-1',
|
||||
(!selectedBooks.length || !selectedBooks.every((id) => isMd5(id))) &&
|
||||
'btn-disabled opacity-50',
|
||||
)}
|
||||
>
|
||||
<MdOpenInNew />
|
||||
<div>{_('Open')}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={groupSelectedBooks}
|
||||
className={clsx(
|
||||
'flex flex-col items-center justify-center gap-1',
|
||||
!selectedBooks.length && 'btn-disabled opacity-50',
|
||||
)}
|
||||
>
|
||||
<LuFolderPlus />
|
||||
<div>{_('Group')}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={openBookDetails}
|
||||
className={clsx(
|
||||
'flex flex-col items-center justify-center gap-1',
|
||||
(selectedBooks.length !== 1 || !selectedBooks.every((id) => isMd5(id))) &&
|
||||
'btn-disabled opacity-50',
|
||||
)}
|
||||
>
|
||||
<MdInfoOutline />
|
||||
<div>{_('Details')}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={deleteSelectedBooks}
|
||||
className={clsx(
|
||||
'flex flex-col items-center justify-center gap-1',
|
||||
!selectedBooks.length && 'btn-disabled opacity-50',
|
||||
)}
|
||||
>
|
||||
<MdDelete className='text-red-500' />
|
||||
<div className='text-red-500'>{_('Delete')}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSetSelectMode(false)}
|
||||
className={clsx('flex flex-col items-center justify-center gap-1')}
|
||||
>
|
||||
<MdOutlineCancel />
|
||||
<div>{_('Cancel')}</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isSelectMode && showSelectModeActions && (
|
||||
<SelectModeActions
|
||||
selectedBooks={selectedBooks}
|
||||
safeAreaBottom={safeAreaInsets?.bottom || 0}
|
||||
onOpen={openSelectedBooks}
|
||||
onGroup={groupSelectedBooks}
|
||||
onDetails={openBookDetails}
|
||||
onDelete={deleteSelectedBooks}
|
||||
onCancel={() => handleSetSelectMode(false)}
|
||||
/>
|
||||
)}
|
||||
{showGroupingModal && (
|
||||
<ModalPortal>
|
||||
<GroupingModal
|
||||
|
||||
@@ -24,6 +24,7 @@ const ImportMenu: React.FC<ImportMenuProps> = ({ setIsDropdownOpen, onImportBook
|
||||
'dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow',
|
||||
appService?.isMobile ? 'no-triangle' : 'dropdown-center',
|
||||
)}
|
||||
onCancel={() => setIsDropdownOpen?.(false)}
|
||||
>
|
||||
<MenuItem label={_('From Local File')} onClick={handleImportBooks} />
|
||||
</Menu>
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import clsx from 'clsx';
|
||||
import { MdDelete, MdOpenInNew, MdOutlineCancel, MdInfoOutline } from 'react-icons/md';
|
||||
import { LuFolderPlus } from 'react-icons/lu';
|
||||
import { useKeyDownActions } from '@/hooks/useKeyDownActions';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { isMd5 } from '@/utils/md5';
|
||||
|
||||
interface SelectModeActionsProps {
|
||||
selectedBooks: string[];
|
||||
safeAreaBottom: number;
|
||||
onOpen: () => void;
|
||||
onGroup: () => void;
|
||||
onDetails: () => void;
|
||||
onDelete: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const SelectModeActions: React.FC<SelectModeActionsProps> = ({
|
||||
selectedBooks,
|
||||
safeAreaBottom,
|
||||
onOpen,
|
||||
onGroup,
|
||||
onDetails,
|
||||
onDelete,
|
||||
onCancel,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
|
||||
const hasSelection = selectedBooks.length > 0;
|
||||
const hasValidBooks = selectedBooks.every((id) => isMd5(id));
|
||||
const hasSingleSelection = selectedBooks.length === 1;
|
||||
const divRef = useKeyDownActions({ onCancel });
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={divRef}
|
||||
className='fixed bottom-0 left-0 right-0 z-40'
|
||||
style={{
|
||||
paddingBottom: `${safeAreaBottom + 16}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center justify-center shadow-lg',
|
||||
'bg-base-300 text-base-content text-xs',
|
||||
'mx-auto w-fit space-x-6 rounded-lg p-4',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
onClick={onOpen}
|
||||
className={clsx(
|
||||
'flex flex-col items-center justify-center gap-1',
|
||||
(!hasSelection || !hasValidBooks) && 'btn-disabled opacity-50',
|
||||
)}
|
||||
>
|
||||
<MdOpenInNew />
|
||||
<div>{_('Open')}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={onGroup}
|
||||
className={clsx(
|
||||
'flex flex-col items-center justify-center gap-1',
|
||||
!hasSelection && 'btn-disabled opacity-50',
|
||||
)}
|
||||
>
|
||||
<LuFolderPlus />
|
||||
<div>{_('Group')}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={onDetails}
|
||||
className={clsx(
|
||||
'flex flex-col items-center justify-center gap-1',
|
||||
(!hasSingleSelection || !hasValidBooks) && 'btn-disabled opacity-50',
|
||||
)}
|
||||
>
|
||||
<MdInfoOutline />
|
||||
<div>{_('Details')}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className={clsx(
|
||||
'flex flex-col items-center justify-center gap-1',
|
||||
!hasSelection && 'btn-disabled opacity-50',
|
||||
)}
|
||||
>
|
||||
<MdDelete className='text-red-500' />
|
||||
<div className='text-red-500'>{_('Delete')}</div>
|
||||
</button>
|
||||
<button onClick={onCancel} className='flex flex-col items-center justify-center gap-1'>
|
||||
<MdOutlineCancel />
|
||||
<div>{_('Cancel')}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectModeActions;
|
||||
@@ -231,6 +231,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
'settings-menu dropdown-content no-triangle border-base-100',
|
||||
'z-20 mt-2 max-w-[90vw] shadow-2xl',
|
||||
)}
|
||||
onCancel={() => setIsDropdownOpen?.(false)}
|
||||
>
|
||||
{user ? (
|
||||
<MenuItem
|
||||
|
||||
@@ -85,7 +85,10 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu className='view-menu dropdown-content no-triangle border-base-100 z-20 mt-2 shadow-2xl'>
|
||||
<Menu
|
||||
className='view-menu dropdown-content no-triangle border-base-100 z-20 mt-2 shadow-2xl'
|
||||
onCancel={() => setIsDropdownOpen?.(false)}
|
||||
>
|
||||
{viewOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
|
||||
@@ -53,8 +53,9 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
const router = useRouter();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { setLibrary } = useLibraryStore();
|
||||
const { hoveredBookKey } = useReaderStore();
|
||||
const { hoveredBookKey, getView } = useReaderStore();
|
||||
const { settings, setSettings } = useSettingsStore();
|
||||
const { sideBarBookKey } = useSidebarStore();
|
||||
const { isSideBarVisible, getIsSideBarVisible, setSideBarVisible } = useSidebarStore();
|
||||
const { isNotebookVisible, getIsNotebookVisible, setNotebookVisible } = useNotebookStore();
|
||||
const { isDarkMode, systemUIAlwaysHidden, isRoundedWindow } = useThemeStore();
|
||||
@@ -76,11 +77,14 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (event: CustomEvent) => {
|
||||
const view = getView(sideBarBookKey!);
|
||||
if (event.detail.keyName === 'Back') {
|
||||
if (getIsSideBarVisible()) {
|
||||
setSideBarVisible(false);
|
||||
} else if (getIsNotebookVisible()) {
|
||||
setNotebookVisible(false);
|
||||
} else if (view?.history.canGoBack) {
|
||||
view.history.back();
|
||||
} else {
|
||||
eventDispatcher.dispatch('close-reader');
|
||||
router.back();
|
||||
@@ -100,15 +104,16 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
}, [appService?.isAndroidApp]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!appService?.isAndroidApp) return;
|
||||
eventDispatcher.onSync('native-key-down', handleKeyDown);
|
||||
if (appService?.isAndroidApp) {
|
||||
eventDispatcher.onSync('native-key-down', handleKeyDown);
|
||||
}
|
||||
return () => {
|
||||
if (appService?.isAndroidApp) {
|
||||
eventDispatcher.offSync('native-key-down', handleKeyDown);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appService?.isAndroidApp, isSideBarVisible, isNotebookVisible]);
|
||||
}, [appService?.isAndroidApp, sideBarBookKey, isSideBarVisible, isNotebookVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitiating.current) return;
|
||||
|
||||
@@ -153,6 +153,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ bookKey, setIsDropdownOpen }) => {
|
||||
maxWidth: `${window.innerWidth - 40}px`,
|
||||
marginRight: window.innerWidth < 640 ? '-36px' : '0px',
|
||||
}}
|
||||
onCancel={() => setIsDropdownOpen?.(false)}
|
||||
>
|
||||
{bookData.bookDoc?.rendition?.layout === 'pre-paginated' && (
|
||||
<>
|
||||
|
||||
@@ -25,6 +25,7 @@ const MobileFooterBar: React.FC<FooterBarChildProps> = ({
|
||||
<>
|
||||
<ColorPanel actionTab={actionTab} bottomOffset={bottomOffset} />
|
||||
<NavigationPanel
|
||||
bookKey={bookKey}
|
||||
actionTab={actionTab}
|
||||
progressFraction={progressFraction}
|
||||
progressValid={progressValid}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
|
||||
import { RiArrowGoBackLine, RiArrowGoForwardLine } from 'react-icons/ri';
|
||||
import { RiArrowLeftDoubleLine, RiArrowRightDoubleLine } from 'react-icons/ri';
|
||||
import { getNavigationIcon, getNavigationLabel, getNavigationHandler } from './utils';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { ViewSettings } from '@/types/book';
|
||||
import { NavigationHandlers } from './types';
|
||||
@@ -11,6 +12,7 @@ import Button from '@/components/Button';
|
||||
import Slider from '@/components/Slider';
|
||||
|
||||
interface NavigationPanelProps {
|
||||
bookKey: string;
|
||||
actionTab: string;
|
||||
progressFraction: number;
|
||||
progressValid: boolean;
|
||||
@@ -21,6 +23,7 @@ interface NavigationPanelProps {
|
||||
}
|
||||
|
||||
export const NavigationPanel: React.FC<NavigationPanelProps> = ({
|
||||
bookKey,
|
||||
actionTab,
|
||||
progressFraction,
|
||||
progressValid,
|
||||
@@ -30,6 +33,8 @@ export const NavigationPanel: React.FC<NavigationPanelProps> = ({
|
||||
sliderHeight,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { getView } = useReaderStore();
|
||||
const view = getView(bookKey);
|
||||
|
||||
const [progressValue, setProgressValue] = React.useState(
|
||||
progressValid ? progressFraction * 100 : 0,
|
||||
@@ -99,7 +104,7 @@ export const NavigationPanel: React.FC<NavigationPanelProps> = ({
|
||||
)}
|
||||
onClick={navigationHandlers.onGoBack}
|
||||
label={_('Go Back')}
|
||||
disabled={!navigationHandlers.onGoBack}
|
||||
disabled={!view?.history.canGoBack}
|
||||
/>
|
||||
<Button
|
||||
icon={getNavigationIcon(
|
||||
@@ -109,7 +114,7 @@ export const NavigationPanel: React.FC<NavigationPanelProps> = ({
|
||||
)}
|
||||
onClick={navigationHandlers.onGoForward}
|
||||
label={_('Go Forward')}
|
||||
disabled={!navigationHandlers.onGoForward}
|
||||
disabled={!view?.history.canGoForward}
|
||||
/>
|
||||
<Button
|
||||
icon={getNavigationIcon(viewSettings?.rtl, <RiArrowRightSLine />, <RiArrowLeftSLine />)}
|
||||
|
||||
@@ -90,6 +90,7 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
|
||||
return (
|
||||
<Menu
|
||||
className={clsx('book-menu dropdown-content border-base-100 z-20 shadow-2xl', menuClassName)}
|
||||
onCancel={() => setIsDropdownOpen?.(false)}
|
||||
>
|
||||
<MenuItem
|
||||
label={_('Parallel Read')}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useKeyDownActions } from '@/hooks/useKeyDownActions';
|
||||
|
||||
const Alert: React.FC<{
|
||||
title: string;
|
||||
@@ -9,9 +10,12 @@ const Alert: React.FC<{
|
||||
onConfirm: () => void;
|
||||
}> = ({ title, message, onCancel, onConfirm }) => {
|
||||
const _ = useTranslation();
|
||||
const divRef = useKeyDownActions({ onCancel, onConfirm });
|
||||
|
||||
return (
|
||||
<div className={clsx('z-[100] flex justify-center px-4')}>
|
||||
<div
|
||||
ref={divRef}
|
||||
role='alert'
|
||||
className={clsx(
|
||||
'alert flex items-center justify-between',
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useKeyDownActions } from '@/hooks/useKeyDownActions';
|
||||
|
||||
interface MenuProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
const Menu: React.FC<MenuProps> = ({ children, className, style }) => {
|
||||
const Menu: React.FC<MenuProps> = ({ children, className, style, onCancel }) => {
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useKeyDownActions({ onCancel, elementRef: menuRef });
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (menuRef.current) {
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { RefObject, useEffect, useRef } from 'react';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useDeviceControlStore } from '@/store/deviceStore';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
|
||||
interface UseKeyDownOptions {
|
||||
onCancel?: () => void;
|
||||
onConfirm?: () => void;
|
||||
enabled?: boolean;
|
||||
elementRef?: RefObject<HTMLElement>;
|
||||
}
|
||||
|
||||
export const useKeyDownActions = ({
|
||||
onCancel,
|
||||
onConfirm,
|
||||
enabled = true,
|
||||
elementRef: providedRef,
|
||||
}: UseKeyDownOptions) => {
|
||||
const { appService } = useEnv();
|
||||
const { acquireBackKeyInterception, releaseBackKeyInterception } = useDeviceControlStore();
|
||||
const internalRef = useRef<HTMLDivElement>(null);
|
||||
const elementRef = providedRef || internalRef;
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent | CustomEvent) => {
|
||||
if (event instanceof CustomEvent) {
|
||||
if (event.detail.keyName === 'Back') {
|
||||
onCancel?.();
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (event.key === 'Escape') {
|
||||
onCancel?.();
|
||||
} else if (event.key === 'Enter') {
|
||||
onConfirm?.();
|
||||
}
|
||||
event.stopPropagation();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
if (elementRef.current) {
|
||||
elementRef.current.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
if (appService?.isAndroidApp) {
|
||||
acquireBackKeyInterception?.();
|
||||
eventDispatcher.onSync('native-key-down', handleKeyDown);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
|
||||
if (appService?.isAndroidApp) {
|
||||
releaseBackKeyInterception?.();
|
||||
eventDispatcher.offSync('native-key-down', handleKeyDown);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [enabled, appService?.isAndroidApp]);
|
||||
|
||||
return internalRef;
|
||||
};
|
||||
Reference in New Issue
Block a user