import clsx from 'clsx'; import React from 'react'; import { MdCheck } from 'react-icons/md'; import { useEnv } from '@/context/EnvContext'; import { useBookDataStore } from '@/store/bookDataStore'; import { useReaderStore } from '@/store/readerStore'; import { useLibraryStore } from '@/store/libraryStore'; import { useSidebarStore } from '@/store/sidebarStore'; import { useTranslation } from '@/hooks/useTranslation'; import { useSettingsStore } from '@/store/settingsStore'; import { useParallelViewStore } from '@/store/parallelViewStore'; import { isWebAppPlatform } from '@/services/environment'; import { eventDispatcher } from '@/utils/event'; import { FIXED_LAYOUT_FORMATS } from '@/types/book'; import { DOWNLOAD_READEST_URL } from '@/services/constants'; import { saveViewSettings } from '@/helpers/settings'; import { setProofreadRulesVisibility } from '@/app/reader/components/ProofreadRules'; import { setAboutDialogVisible } from '@/components/AboutWindow'; import useBooksManager from '../../hooks/useBooksManager'; import MenuItem from '@/components/MenuItem'; import Menu from '@/components/Menu'; interface BookMenuProps { menuClassName?: string; setIsDropdownOpen?: (isOpen: boolean) => void; } const BookMenu: React.FC = ({ menuClassName, setIsDropdownOpen }) => { const _ = useTranslation(); const { envConfig } = useEnv(); const { settings } = useSettingsStore(); const { bookKeys, recreateViewer, getViewSettings } = useReaderStore(); const { getVisibleLibrary } = useLibraryStore(); const { openParallelView } = useBooksManager(); const { sideBarBookKey } = useSidebarStore(); const { getConfig } = useBookDataStore(); const { parallelViews, setParallel, unsetParallel } = useParallelViewStore(); const viewSettings = getViewSettings(sideBarBookKey!); const [isSortedTOC, setIsSortedTOC] = React.useState(viewSettings?.sortedTOC || false); // Used purely to grey out "Clear Annotations" when there's nothing to // clear. The actual delete + confirm dialog lives in Annotator (which // outlives this dropdown menu, so the dialog isn't unmounted along // with the menu when the user clicks the entry). const annotationsToClear = React.useMemo(() => { if (!sideBarBookKey) return 0; const cfg = getConfig(sideBarBookKey); if (!cfg?.booknotes) return 0; return cfg.booknotes.filter((n) => n.type === 'annotation' && !n.deletedAt).length; }, [sideBarBookKey, getConfig]); const handleParallelView = (id: string) => { openParallelView(id); setIsDropdownOpen?.(false); }; const handleReloadPage = () => { window.location.reload(); setIsDropdownOpen?.(false); }; const showAboutReadest = () => { setAboutDialogVisible(true); setIsDropdownOpen?.(false); }; const downloadReadest = () => { window.open(DOWNLOAD_READEST_URL, '_blank'); setIsDropdownOpen?.(false); }; const handleExportAnnotations = () => { eventDispatcher.dispatch('export-annotations', { bookKey: sideBarBookKey }); setIsDropdownOpen?.(false); }; const handleImportAnnotations = () => { eventDispatcher.dispatch('import-annotations', { bookKey: sideBarBookKey }); setIsDropdownOpen?.(false); }; const handleToggleSortTOC = () => { setIsSortedTOC((prev) => !prev); setIsDropdownOpen?.(false); if (sideBarBookKey) { saveViewSettings(envConfig, sideBarBookKey, 'sortedTOC', !isSortedTOC, true, false).then( () => { recreateViewer(envConfig, sideBarBookKey); }, ); } }; const handleSetParallel = () => { setParallel(bookKeys); setIsDropdownOpen?.(false); }; const handleUnsetParallel = () => { unsetParallel(bookKeys); setIsDropdownOpen?.(false); }; const showProofreadRulesWindow = () => { setProofreadRulesVisibility(true); setIsDropdownOpen?.(false); }; const handlePullKOSync = () => { eventDispatcher.dispatch('pull-kosync', { bookKey: sideBarBookKey }); setIsDropdownOpen?.(false); }; const handlePushKOSync = () => { eventDispatcher.dispatch('push-kosync', { bookKey: sideBarBookKey }); setIsDropdownOpen?.(false); }; const handlePushReadwise = () => { eventDispatcher.dispatch('readwise-push-all', { bookKey: sideBarBookKey }); setIsDropdownOpen?.(false); }; const handlePushHardcoverNotes = () => { eventDispatcher.dispatch('hardcover-push-notes', { bookKey: sideBarBookKey }); setIsDropdownOpen?.(false); }; const handlePushHardcoverProgress = () => { eventDispatcher.dispatch('hardcover-push-progress', { bookKey: sideBarBookKey }); setIsDropdownOpen?.(false); }; // Routed through Annotator (per-book, long-lived) so that the // confirmation dialog isn't unmounted with the dropdown menu. const handleClearAnnotations = () => { eventDispatcher.dispatch('clear-annotations', { bookKey: sideBarBookKey }); setIsDropdownOpen?.(false); }; return ( setIsDropdownOpen?.(false)} > 1 ? 'lg:tooltip lg:tooltip-bottom' : ''} tooltip={parallelViews.length > 0 ? _('Disable') : _('Enable')} Icon={parallelViews.length > 0 && bookKeys.length > 1 ? MdCheck : undefined} >
    {getVisibleLibrary() .filter((book) => !FIXED_LAYOUT_FORMATS.has(book.format)) .filter((book) => !!book.downloadedAt) .slice(0, 20) .map((book) => ( { (e.target as HTMLImageElement).style.display = 'none'; }} /> } label={book.title} labelClass='max-w-36' onClick={() => handleParallelView(book.hash)} /> ))}
{bookKeys.length > 1 && (parallelViews.length > 0 ? ( ) : ( ))} {(settings.kosync.enabled || settings.readwise.enabled || settings.hardcover.enabled) && ( )} {settings.kosync.enabled && (
)} {settings.readwise.enabled && (
)} {settings.hardcover.enabled && (
)} {isWebAppPlatform() && }
); }; export default BookMenu;