From a03f2c585fdea893b9fb9fc8b205c4a1dd3531ec Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Sat, 5 Jul 2025 01:36:13 +0800 Subject: [PATCH] layout: fix overlay scrollbar with virtualized list, closes #1524 (#1530) --- .../src/app/reader/components/FooterBar.tsx | 13 ++- .../app/reader/components/sidebar/Content.tsx | 14 ++- .../app/reader/components/sidebar/TOCView.tsx | 106 ++++++++++++------ 3 files changed, 92 insertions(+), 41 deletions(-) diff --git a/apps/readest-app/src/app/reader/components/FooterBar.tsx b/apps/readest-app/src/app/reader/components/FooterBar.tsx index 8cbad219..91bda848 100644 --- a/apps/readest-app/src/app/reader/components/FooterBar.tsx +++ b/apps/readest-app/src/app/reader/components/FooterBar.tsx @@ -15,6 +15,7 @@ import { RxLineHeight } from 'react-icons/rx'; import { useEnv } from '@/context/EnvContext'; import { useReaderStore } from '@/store/readerStore'; import { useSidebarStore } from '@/store/sidebarStore'; +import { useBookDataStore } from '@/store/bookDataStore'; import { useTranslation } from '@/hooks/useTranslation'; import { useResponsiveSize } from '@/hooks/useResponsiveSize'; import { eventDispatcher } from '@/utils/event'; @@ -45,6 +46,7 @@ const FooterBar: React.FC = ({ }) => { const _ = useTranslation(); const { envConfig, appService } = useEnv(); + const { getConfig, setConfig } = useBookDataStore(); const { hoveredBookKey, setHoveredBookKey } = useReaderStore(); const { getView, getViewState, getProgress, getViewSettings } = useReaderStore(); const { isSideBarVisible, setSideBarVisible } = useSidebarStore(); @@ -55,6 +57,7 @@ const FooterBar: React.FC = ({ const marginIconSize = useResponsiveSize(20); const view = getView(bookKey); + const config = getConfig(bookKey); const progress = getProgress(bookKey); const viewSettings = getViewSettings(bookKey); const viewState = getViewState(bookKey); @@ -131,15 +134,17 @@ const FooterBar: React.FC = ({ handleSpeakText(); } else if (tab === 'toc') { setHoveredBookKey(''); - if (viewSettings) { - viewSettings.sideBarTab = 'toc'; + if (config && config.viewSettings) { + config.viewSettings.sideBarTab = 'toc'; + setConfig(bookKey, config); } setSideBarVisible(true); } else if (tab === 'note') { setHoveredBookKey(''); setSideBarVisible(true); - if (viewSettings) { - viewSettings.sideBarTab = 'annotations'; + if (config && config.viewSettings) { + config.viewSettings.sideBarTab = 'annotations'; + setConfig(bookKey, config); } } }; diff --git a/apps/readest-app/src/app/reader/components/sidebar/Content.tsx b/apps/readest-app/src/app/reader/components/sidebar/Content.tsx index 8f2ddd99..1fcd6bf7 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/Content.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/Content.tsx @@ -51,12 +51,16 @@ const SidebarContent: React.FC<{ 'font-sans text-base font-normal sm:text-sm', )} > - +
{targetTab === 'toc' && bookDoc.toc && ( diff --git a/apps/readest-app/src/app/reader/components/sidebar/TOCView.tsx b/apps/readest-app/src/app/reader/components/sidebar/TOCView.tsx index fe928c64..963141f5 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/TOCView.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/TOCView.tsx @@ -1,7 +1,10 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { FixedSizeList as VirtualList } from 'react-window'; +import { useOverlayScrollbars } from 'overlayscrollbars-react'; +import 'overlayscrollbars/overlayscrollbars.css'; import { TOCItem } from '@/libs/document'; +import { useEnv } from '@/context/EnvContext'; import { useReaderStore } from '@/store/readerStore'; import { useSidebarStore } from '@/store/sidebarStore'; import { findParentPath } from '@/utils/toc'; @@ -32,6 +35,7 @@ const TOCView: React.FC<{ bookKey: string; toc: TOCItem[]; }> = ({ bookKey, toc }) => { + const { appService } = useEnv(); const { getView, getProgress, getViewState, getViewSettings } = useReaderStore(); const { sideBarBookKey, isSideBarVisible } = useSidebarStore(); const viewSettings = getViewSettings(bookKey)!; @@ -42,9 +46,41 @@ const TOCView: React.FC<{ const [containerHeight, setContainerHeight] = useState(400); const containerRef = useRef(null); + const listOuterRef = useRef(null); const vitualListRef = useRef(null); const staticListRef = useRef(null); + const [initialize] = useOverlayScrollbars({ + defer: true, + options: { + scrollbars: { + autoHide: 'scroll', + }, + showNativeOverlaidScrollbars: false, + }, + events: { + initialized(osInstance) { + const { viewport } = osInstance.elements(); + viewport.style.overflowX = `var(--os-viewport-overflow-x)`; + viewport.style.overflowY = `var(--os-viewport-overflow-y)`; + }, + }, + }); + + useEffect(() => { + const { current: root } = containerRef; + const { current: virtualOuter } = listOuterRef; + + if (root && virtualOuter) { + initialize({ + target: root, + elements: { + viewport: virtualOuter, + }, + }); + } + }, [initialize]); + useTextTranslation(bookKey, containerRef.current, false, 'translation-target-toc'); useEffect(() => { @@ -78,8 +114,11 @@ const TOCView: React.FC<{ }; }, []); - const activeHref = useMemo(() => progress?.sectionHref || null, [progress]); + const activeHref = useMemo(() => progress?.sectionHref || null, [progress?.sectionHref]); const flatItems = useFlattenedTOC(toc, expandedItems); + const activeItemIndex = useMemo(() => { + return flatItems.findIndex((item) => item.item.href === activeHref); + }, [flatItems, activeHref]); const handleToggleExpand = useCallback((item: TOCItem) => { const href = item.href || ''; @@ -136,7 +175,7 @@ const TOCView: React.FC<{ const virtualItemSize = useMemo(() => { return window.innerWidth >= 640 && !viewSettings?.translationEnabled ? 37 : 57; - }, [viewSettings]); + }, [viewSettings?.translationEnabled]); const virtualListData = useMemo( () => ({ @@ -163,40 +202,43 @@ const TOCView: React.FC<{ useEffect(() => { if (flatItems.length > 0) { - setTimeout(scrollToActiveItem, 0); + setTimeout(scrollToActiveItem, appService?.isAndroidApp ? 300 : 0); } }, [flatItems, scrollToActiveItem]); - return ( -
- {flatItems.length > 256 ? ( - - {VirtualListRow} - - ) : ( -
- {flatItems.map((flatItem, index) => ( - - ))} -
- )} + return flatItems.length > 256 ? ( +
+ = 0 + ? Math.max(0, activeItemIndex * virtualItemSize - containerHeight / 2) + : undefined + } + > + {VirtualListRow} + +
+ ) : ( +
+ {flatItems.map((flatItem, index) => ( + + ))}
); }; - export default TOCView;