import clsx from 'clsx'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { PiDotsThreeVerticalBold } from 'react-icons/pi'; import { VscLibrary } from 'react-icons/vsc'; import { Insets } from '@/types/misc'; import { useEnv } from '@/context/EnvContext'; import { useThemeStore } from '@/store/themeStore'; import { useReaderStore } from '@/store/readerStore'; import { useBookDataStore } from '@/store/bookDataStore'; import { useSidebarStore } from '@/store/sidebarStore'; import { useTranslation } from '@/hooks/useTranslation'; import { useSettingsStore } from '@/store/settingsStore'; import { useTrafficLightStore } from '@/store/trafficLightStore'; import { useTrafficLight } from '@/hooks/useTrafficLight'; import { useResponsiveSize } from '@/hooks/useResponsiveSize'; import { useSpatialNavigation } from '@/app/reader/hooks/useSpatialNavigation'; import { getHighlightColorHex } from '../utils/annotatorUtil'; import { annotationToolQuickActions } from './annotator/AnnotationTools'; import { AnnotationToolType } from '@/types/annotator'; import { saveViewSettings } from '@/helpers/settings'; import { HighlighterIcon } from '@/components/HighlighterIcon'; import Dropdown from '@/components/Dropdown'; import ModalPortal from '@/components/ModalPortal'; import WindowButtons from '@/components/WindowButtons'; import QuickActionMenu from './annotator/QuickActionMenu'; import SidebarToggler from './SidebarToggler'; import BookmarkToggler from './BookmarkToggler'; import NotebookToggler from './NotebookToggler'; import SettingsToggler from './SettingsToggler'; import TranslationToggler from './TranslationToggler'; import ViewMenu from './ViewMenu'; import SyncInfoDialog from './SyncInfoDialog'; interface HeaderBarProps { bookKey: string; bookTitle: string; isTopLeft: boolean; isHoveredAnim: boolean; gridInsets: Insets; screenInsets: Insets; onCloseBook: (bookKey: string) => void; onGoToLibrary: () => void; onDropdownOpenChange?: (isOpen: boolean) => void; } const HeaderBar: React.FC = ({ bookKey, bookTitle, isTopLeft, isHoveredAnim, gridInsets, screenInsets, onCloseBook, onGoToLibrary, onDropdownOpenChange, }) => { const _ = useTranslation(); const { envConfig, appService } = useEnv(); const { settings } = useSettingsStore(); const { isTrafficLightVisible } = useTrafficLight(); const { trafficLightInFullscreen, setTrafficLightVisibility } = useTrafficLightStore(); const { bookKeys, hoveredBookKey } = useReaderStore(); const { isDarkMode, systemUIVisible, statusBarHeight } = useThemeStore(); const { isSideBarVisible, getIsSideBarVisible } = useSidebarStore(); const { getView, getViewSettings, setHoveredBookKey } = useReaderStore(); const { getBookData, getConfig } = useBookDataStore(); const viewSettings = getViewSettings(bookKey); const bookData = getBookData(bookKey); const bookConfig = getConfig(bookKey); const lastSyncedAt = Math.max(bookConfig?.lastSyncedAtConfig || 0, bookConfig?.lastSyncedAtNotes || 0) || undefined; const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isMetaHashDialogOpen, setIsMetaHashDialogOpen] = useState(false); const [headerWidth, setHeaderWidth] = useState(0); const view = getView(bookKey); const iconSize16 = useResponsiveSize(16); const iconSize18 = useResponsiveSize(18); const headerRef = useRef(null); const docs = view?.renderer.getContents() ?? []; const pointerInDoc = docs.some(({ doc }) => doc?.body?.style.cursor === 'pointer'); const enableAnnotationQuickActions = viewSettings?.enableAnnotationQuickActions; const annotationQuickActionButton = annotationToolQuickActions.find( (button) => button.type === viewSettings?.annotationQuickAction, ) || annotationToolQuickActions[0]!; const annotationQuickAction = viewSettings?.annotationQuickAction; const AnnotationToolQuickActionIcon = annotationQuickActionButton.Icon; const highlightStyle = settings.globalReadSettings.highlightStyle; const highlightColor = settings.globalReadSettings.highlightStyles[highlightStyle]; const highlightHexColor = getHighlightColorHex(settings, highlightColor); const handleToggleDropdown = (isOpen: boolean) => { setIsDropdownOpen(isOpen); onDropdownOpenChange?.(isOpen); if (!isOpen) setHoveredBookKey(''); }; const handleAnnotationQuickActionSelect = (action: AnnotationToolType | null) => { if (viewSettings?.annotationQuickAction === action) action = null; saveViewSettings(envConfig, bookKey, 'annotationQuickAction', action, false, true); }; useEffect(() => { if (!appService?.hasTrafficLight) return; if (hoveredBookKey === bookKey && isTopLeft) { setTrafficLightVisibility(true, { x: 10, y: 20 }); } else if (!hoveredBookKey) { setTimeout(() => { if (!getIsSideBarVisible()) { setTrafficLightVisibility(false); } }, 100); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [appService, hoveredBookKey]); useEffect(() => { const header = headerRef.current; if (!header) return; const observer = new ResizeObserver(([entry]) => { if (entry) setHeaderWidth(entry.contentRect.width); }); observer.observe(header); return () => observer.disconnect(); }, []); // Check if mouse is outside header area to avoid false positive event of MouseLeave when clicking inside header on Windows const isMouseOutsideHeader = useCallback((clientX: number, clientY: number) => { if (!headerRef.current) return true; const rect = headerRef.current.getBoundingClientRect(); return ( clientX <= rect.left || clientX >= rect.right || clientY <= rect.top || clientY >= rect.bottom ); }, []); const isHeaderCompact = headerWidth > 0 && headerWidth < 350; const insets = window.innerWidth < 640 ? screenInsets : gridInsets; const isHeaderVisible = hoveredBookKey === bookKey || isDropdownOpen; useSpatialNavigation(headerRef, isHeaderVisible); const trafficLightInHeader = appService?.hasTrafficLight && !trafficLightInFullscreen && !isSideBarVisible && isTopLeft; const windowButtonVisible = appService?.hasWindowBar && !isTrafficLightVisible && !trafficLightInHeader; return (
setHoveredBookKey(bookKey)} onMouseEnter={() => !appService?.isMobile && setHoveredBookKey(bookKey)} onTouchStart={() => !appService?.isMobile && setHoveredBookKey(bookKey)} />
!appService?.isMobile && setHoveredBookKey(bookKey)} onMouseLeave={(e) => { if (!appService?.isMobile && isMouseOutsideHeader(e.clientX, e.clientY)) { setHoveredBookKey(''); } }} >
{!isSideBarVisible && (
)}
{enableAnnotationQuickActions && ( ) : ( ) } onToggle={handleToggleDropdown} > )}
{!isHeaderCompact && } } onToggle={handleToggleDropdown} > setIsMetaHashDialogOpen(true)} /> {isMetaHashDialogOpen && ( setIsMetaHashDialogOpen(false)} /> )} { setHoveredBookKey(null); onCloseBook(bookKey); }} />
); }; export default HeaderBar;