layout: add overlay scrollbar for TOC on Android (#1506)
This commit is contained in:
@@ -76,6 +76,8 @@
|
||||
"jwt-decode": "^4.0.0",
|
||||
"marked": "^15.0.12",
|
||||
"next": "15.3.3",
|
||||
"overlayscrollbars": "^2.11.4",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"posthog-js": "^1.246.0",
|
||||
"react": "19.0.0",
|
||||
"react-color": "^2.19.3",
|
||||
|
||||
@@ -117,6 +117,10 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
const makeBookAvailable = async (book: Book) => {
|
||||
if (book.uploadedAt && !book.downloadedAt) {
|
||||
if (await appService?.isBookAvailable(book)) {
|
||||
if (!book.downloadedAt) {
|
||||
book.downloadedAt = Date.now();
|
||||
updateBook(envConfig, book);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
let available = false;
|
||||
|
||||
@@ -4,6 +4,8 @@ import clsx from 'clsx';
|
||||
import * as React from 'react';
|
||||
import { useState, useRef, useEffect, Suspense } from 'react';
|
||||
import { ReadonlyURLSearchParams, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
|
||||
import { Book } from '@/types/book';
|
||||
import { AppService } from '@/types/system';
|
||||
@@ -604,34 +606,33 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
)}
|
||||
{libraryLoaded &&
|
||||
(libraryBooks.some((book) => !book.deletedAt) ? (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx(
|
||||
'scroll-container drop-zone flex-grow overflow-y-auto',
|
||||
isDragging && 'drag-over',
|
||||
)}
|
||||
style={{
|
||||
paddingTop: '0px',
|
||||
paddingRight: `${insets.right}px`,
|
||||
paddingBottom: `${insets.bottom}px`,
|
||||
paddingLeft: `${insets.left}px`,
|
||||
}}
|
||||
>
|
||||
<DropIndicator />
|
||||
<Bookshelf
|
||||
libraryBooks={libraryBooks}
|
||||
isSelectMode={isSelectMode}
|
||||
isSelectAll={isSelectAll}
|
||||
isSelectNone={isSelectNone}
|
||||
handleImportBooks={handleImportBooks}
|
||||
handleBookUpload={handleBookUpload}
|
||||
handleBookDownload={handleBookDownload}
|
||||
handleBookDelete={handleBookDelete}
|
||||
handleSetSelectMode={handleSetSelectMode}
|
||||
handleShowDetailsBook={handleShowDetailsBook}
|
||||
booksTransferProgress={booksTransferProgress}
|
||||
/>
|
||||
</div>
|
||||
<OverlayScrollbarsComponent options={{ scrollbars: { autoHide: 'scroll' } }} defer>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx('scroll-container drop-zone flex-grow', isDragging && 'drag-over')}
|
||||
style={{
|
||||
paddingTop: '0px',
|
||||
paddingRight: `${insets.right}px`,
|
||||
paddingBottom: `${insets.bottom}px`,
|
||||
paddingLeft: `${insets.left}px`,
|
||||
}}
|
||||
>
|
||||
<DropIndicator />
|
||||
<Bookshelf
|
||||
libraryBooks={libraryBooks}
|
||||
isSelectMode={isSelectMode}
|
||||
isSelectAll={isSelectAll}
|
||||
isSelectNone={isSelectNone}
|
||||
handleImportBooks={handleImportBooks}
|
||||
handleBookUpload={handleBookUpload}
|
||||
handleBookDownload={handleBookDownload}
|
||||
handleBookDelete={handleBookDelete}
|
||||
handleSetSelectMode={handleSetSelectMode}
|
||||
handleShowDetailsBook={handleShowDetailsBook}
|
||||
booksTransferProgress={booksTransferProgress}
|
||||
/>
|
||||
</div>
|
||||
</OverlayScrollbarsComponent>
|
||||
) : (
|
||||
<div className='hero drop-zone h-screen items-center justify-center'>
|
||||
<DropIndicator />
|
||||
|
||||
@@ -8,12 +8,12 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useParallelViewStore } from '@/store/parallelViewStore';
|
||||
import { isWebAppPlatform } from '@/services/environment';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { DOWNLOAD_READEST_URL } from '@/services/constants';
|
||||
import useBooksManager from '../../hooks/useBooksManager';
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
import { useParallelViewStore } from '@/store/parallelViewStore';
|
||||
|
||||
interface BookMenuProps {
|
||||
menuClassName?: string;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { BookDoc } from '@/libs/document';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
|
||||
import TOCView from './TOCView';
|
||||
import BooknoteView from './BooknoteView';
|
||||
import TabNavigation from './TabNavigation';
|
||||
@@ -13,38 +16,12 @@ const SidebarContent: React.FC<{
|
||||
sideBarBookKey: string;
|
||||
}> = ({ bookDoc, sideBarBookKey }) => {
|
||||
const { appService } = useEnv();
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const { getConfig, setConfig } = useBookDataStore();
|
||||
const config = getConfig(sideBarBookKey);
|
||||
const [activeTab, setActiveTab] = useState(config?.viewSettings?.sideBarTab || 'toc');
|
||||
const [fade, setFade] = useState(false);
|
||||
const [targetTab, setTargetTab] = useState(activeTab);
|
||||
|
||||
useEffect(() => {
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
let scrollTimeout: ReturnType<typeof setTimeout>;
|
||||
const showScrollbar = () => {
|
||||
container.classList.remove('hidden-scrollbar');
|
||||
};
|
||||
const hideScrollbar = () => {
|
||||
container.classList.add('hidden-scrollbar');
|
||||
};
|
||||
|
||||
hideScrollbar();
|
||||
const handleScroll = () => {
|
||||
showScrollbar();
|
||||
clearTimeout(scrollTimeout);
|
||||
scrollTimeout = setTimeout(hideScrollbar, 2000);
|
||||
};
|
||||
container.addEventListener('scroll', handleScroll);
|
||||
return () => {
|
||||
container.removeEventListener('scroll', handleScroll);
|
||||
clearTimeout(scrollTimeout);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sideBarBookKey) return;
|
||||
const config = getConfig(sideBarBookKey!)!;
|
||||
@@ -74,23 +51,24 @@ const SidebarContent: React.FC<{
|
||||
'font-sans text-base font-normal sm:text-sm',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className={clsx(
|
||||
'scroll-container min-h-0 flex-1 overflow-y-auto transition-opacity duration-300 ease-in-out',
|
||||
{ 'opacity-0': fade, 'opacity-100': !fade },
|
||||
)}
|
||||
>
|
||||
{targetTab === 'toc' && bookDoc.toc && (
|
||||
<TOCView toc={bookDoc.toc} bookKey={sideBarBookKey} />
|
||||
)}
|
||||
{targetTab === 'annotations' && (
|
||||
<BooknoteView type='annotation' toc={bookDoc.toc ?? []} bookKey={sideBarBookKey} />
|
||||
)}
|
||||
{targetTab === 'bookmarks' && (
|
||||
<BooknoteView type='bookmark' toc={bookDoc.toc ?? []} bookKey={sideBarBookKey} />
|
||||
)}
|
||||
</div>
|
||||
<OverlayScrollbarsComponent options={{ scrollbars: { autoHide: 'scroll' } }} defer>
|
||||
<div
|
||||
className={clsx(
|
||||
'scroll-container min-h-0 flex-1 transition-opacity duration-300 ease-in-out',
|
||||
{ 'opacity-0': fade, 'opacity-100': !fade },
|
||||
)}
|
||||
>
|
||||
{targetTab === 'toc' && bookDoc.toc && (
|
||||
<TOCView toc={bookDoc.toc} bookKey={sideBarBookKey} />
|
||||
)}
|
||||
{targetTab === 'annotations' && (
|
||||
<BooknoteView type='annotation' toc={bookDoc.toc ?? []} bookKey={sideBarBookKey} />
|
||||
)}
|
||||
{targetTab === 'bookmarks' && (
|
||||
<BooknoteView type='bookmark' toc={bookDoc.toc ?? []} bookKey={sideBarBookKey} />
|
||||
)}
|
||||
</div>
|
||||
</OverlayScrollbarsComponent>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
|
||||
@@ -79,6 +79,7 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
||||
if (isVisible && searchTerm) {
|
||||
handleSearchTermChange(searchTerm);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -98,7 +99,7 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
||||
clearTimeout(searchTimeout.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [onHideSearchBar]);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { impactFeedback } from '@tauri-apps/plugin-haptics';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
@@ -152,12 +152,13 @@ const SideBar: React.FC<{
|
||||
});
|
||||
};
|
||||
|
||||
const handleHideSearchBar = () => {
|
||||
const handleHideSearchBar = useCallback(() => {
|
||||
setIsSearchBarVisible(false);
|
||||
setSearchResults(null);
|
||||
setSearchTerm('');
|
||||
getView(sideBarBookKey)?.clearSearch();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sideBarBookKey]);
|
||||
|
||||
useShortcuts({ onToggleSearchBar: handleToggleSearchBar, onEscape: handleHideSearchBar }, [
|
||||
sideBarBookKey,
|
||||
@@ -257,7 +258,10 @@ const SideBar: React.FC<{
|
||||
<SidebarContent bookDoc={bookDoc} sideBarBookKey={sideBarBookKey!} />
|
||||
)}
|
||||
<div
|
||||
className='drag-bar absolute right-0 top-0 -m-3 h-full w-0.5 cursor-col-resize p-3'
|
||||
className={clsx(
|
||||
'drag-bar absolute right-0 top-0 -m-3 h-full w-0.5 cursor-col-resize p-3',
|
||||
isMobile && 'hidden',
|
||||
)}
|
||||
onMouseDown={handleHorizontalDragStart}
|
||||
onTouchStart={handleHorizontalDragStart}
|
||||
></div>
|
||||
|
||||
@@ -202,16 +202,6 @@ foliate-view {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.scroll-container.hidden-scrollbar {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.pull-indicator {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
|
||||
Generated
+22
@@ -152,6 +152,12 @@ importers:
|
||||
next:
|
||||
specifier: 15.3.3
|
||||
version: 15.3.3(@babel/core@7.26.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
overlayscrollbars:
|
||||
specifier: ^2.11.4
|
||||
version: 2.11.4
|
||||
overlayscrollbars-react:
|
||||
specifier: ^0.5.6
|
||||
version: 0.5.6(overlayscrollbars@2.11.4)(react@19.0.0)
|
||||
posthog-js:
|
||||
specifier: ^1.246.0
|
||||
version: 1.246.0
|
||||
@@ -4787,6 +4793,15 @@ packages:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
overlayscrollbars-react@0.5.6:
|
||||
resolution: {integrity: sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw==}
|
||||
peerDependencies:
|
||||
overlayscrollbars: ^2.0.0
|
||||
react: '>=16.8.0'
|
||||
|
||||
overlayscrollbars@2.11.4:
|
||||
resolution: {integrity: sha512-GKYQo3OZ1QWnppNjQVv5hfpn+glYUxc6+ufW+ivdXUyLWFNc01XoH2Z36KGM4I8e5pXYeA3ElNItcXiLvmUhnQ==}
|
||||
|
||||
p-limit@2.3.0:
|
||||
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -11836,6 +11851,13 @@ snapshots:
|
||||
type-check: 0.4.0
|
||||
word-wrap: 1.2.5
|
||||
|
||||
overlayscrollbars-react@0.5.6(overlayscrollbars@2.11.4)(react@19.0.0):
|
||||
dependencies:
|
||||
overlayscrollbars: 2.11.4
|
||||
react: 19.0.0
|
||||
|
||||
overlayscrollbars@2.11.4: {}
|
||||
|
||||
p-limit@2.3.0:
|
||||
dependencies:
|
||||
p-try: 2.2.0
|
||||
|
||||
Reference in New Issue
Block a user