From 071e09dad1e5ef6715f1d949445afba261811e3d Mon Sep 17 00:00:00 2001 From: chrox Date: Tue, 5 Nov 2024 00:08:45 +0100 Subject: [PATCH] Bookmark view in sidebar --- .../src/app/reader/components/BookGrid.tsx | 8 +- .../app/reader/components/BookmarkToggler.tsx | 8 +- .../app/reader/components/FoliateViewer.tsx | 10 +- .../src/app/reader/components/FooterBar.tsx | 10 +- .../src/app/reader/components/SectionInfo.tsx | 4 +- .../app/reader/components/SidebarToggler.tsx | 6 +- .../components/sidebar/BookmarkView.tsx | 117 ++++++++++++++++++ .../app/reader/components/sidebar/Content.tsx | 8 +- .../app/reader/components/sidebar/Header.tsx | 5 +- .../app/reader/components/sidebar/SideBar.tsx | 4 +- .../app/reader/components/sidebar/TOCView.tsx | 20 +-- apps/readest-app/src/libs/document.ts | 3 +- apps/readest-app/src/store/readerStore.ts | 46 +++++-- apps/readest-app/src/types/book.ts | 17 ++- apps/readest-app/src/utils/toc.ts | 16 +++ 15 files changed, 216 insertions(+), 66 deletions(-) create mode 100644 apps/readest-app/src/app/reader/components/sidebar/BookmarkView.tsx create mode 100644 apps/readest-app/src/utils/toc.ts diff --git a/apps/readest-app/src/app/reader/components/BookGrid.tsx b/apps/readest-app/src/app/reader/components/BookGrid.tsx index 815ebd5a..63e31943 100644 --- a/apps/readest-app/src/app/reader/components/BookGrid.tsx +++ b/apps/readest-app/src/app/reader/components/BookGrid.tsx @@ -35,9 +35,9 @@ const BookGrid: React.FC = ({ bookKeys, bookStates, onCloseBook } {bookStates.map((bookState, index) => { const bookKey = bookKeys[index]!; const isBookmarked = bookmarkRibbons[bookKey]; - const { book, config, bookDoc } = bookState; - if (!book || !config || !bookDoc) return null; - const { section, pageinfo, progress, chapter } = config; + const { book, config, progress, bookDoc } = bookState; + if (!book || !config || !progress || !bookDoc) return null; + const { section, pageinfo, tocLabel: chapter } = progress; const marginGap = config.viewSettings ? `${config.viewSettings!.gapPercent!}%` : ''; return ( @@ -62,7 +62,7 @@ const BookGrid: React.FC = ({ bookKeys, bookStates, onCloseBook } /> )} - + {isFontLayoutSettingsDialogOpen && } ); diff --git a/apps/readest-app/src/app/reader/components/BookmarkToggler.tsx b/apps/readest-app/src/app/reader/components/BookmarkToggler.tsx index 1892bd7d..6077e3b5 100644 --- a/apps/readest-app/src/app/reader/components/BookmarkToggler.tsx +++ b/apps/readest-app/src/app/reader/components/BookmarkToggler.tsx @@ -13,17 +13,23 @@ const BookmarkToggler: React.FC = ({ bookKey }) => { const { books, updateBookmarks, setBookmarkRibbonVisibility } = useReaderStore(); const bookState = books[bookKey]!; const config = bookState.config!; + const progress = bookState.progress!; const [isBookmarked, setIsBookmarked] = useState(false); const toggleBookmark = () => { - const { location: cfi, bookmarks = [] } = config; + const { bookmarks = [] } = config; + const { location: cfi, tocHref: href, range } = progress; if (!cfi) return; if (!isBookmarked) { setIsBookmarked(true); + const text = range?.startContainer.textContent?.slice(0, 128) || ''; + const truncatedText = text.length === 128 ? text + '...' : text; const bookmark: BookNote = { type: 'bookmark', cfi, + href, + text: truncatedText, note: '', created: Date.now(), }; diff --git a/apps/readest-app/src/app/reader/components/FoliateViewer.tsx b/apps/readest-app/src/app/reader/components/FoliateViewer.tsx index cfe5ded8..0997fb1b 100644 --- a/apps/readest-app/src/app/reader/components/FoliateViewer.tsx +++ b/apps/readest-app/src/app/reader/components/FoliateViewer.tsx @@ -36,15 +36,7 @@ const FoliateViewer: React.FC<{ const progressRelocateHandler = (event: Event) => { const detail = (event as CustomEvent).detail; // console.log('relocate:', detail); - setProgress( - bookKey, - detail.fraction, - detail.cfi, - detail.tocItem?.href, - detail.tocItem?.label, - detail.section, - detail.location, - ); + setProgress(bookKey, detail.cfi, detail.tocItem, detail.section, detail.location, detail.range); }; const handleKeydown = (event: KeyboardEvent) => { diff --git a/apps/readest-app/src/app/reader/components/FooterBar.tsx b/apps/readest-app/src/app/reader/components/FooterBar.tsx index 3a3d27af..55626402 100644 --- a/apps/readest-app/src/app/reader/components/FooterBar.tsx +++ b/apps/readest-app/src/app/reader/components/FooterBar.tsx @@ -6,11 +6,11 @@ import { useReaderStore } from '@/store/readerStore'; interface FooterBarProps { bookKey: string; - progress: number | undefined; + pageinfo?: { current: number; total: number } | undefined; isHoveredAnim: boolean; } -const FooterBar: React.FC = ({ bookKey, progress, isHoveredAnim }) => { +const FooterBar: React.FC = ({ bookKey, pageinfo, isHoveredAnim }) => { const { isSideBarVisible, hoveredBookKey, setHoveredBookKey, getFoliateView } = useReaderStore(); const handleProgressChange = (event: React.ChangeEvent) => { @@ -28,7 +28,7 @@ const FooterBar: React.FC = ({ bookKey, progress, isHoveredAnim const foliateView = getFoliateView(bookKey); foliateView?.goRight(); }; - + const progressFraction = pageinfo ? pageinfo.current / pageinfo.total : 0; return (
= ({ bookKey, progress, isHoveredAnim - {progress ? `${Math.round(progress * 100)}%` : ''} + {pageinfo ? `${Math.round(progressFraction * 100)}%` : ''} handleProgressChange(e)} />
); }; diff --git a/apps/readest-app/src/app/reader/components/SidebarToggler.tsx b/apps/readest-app/src/app/reader/components/SidebarToggler.tsx index 9d2550f8..c157d270 100644 --- a/apps/readest-app/src/app/reader/components/SidebarToggler.tsx +++ b/apps/readest-app/src/app/reader/components/SidebarToggler.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { VscLayoutSidebarLeft, VscLayoutSidebarLeftOff } from 'react-icons/vsc'; +import { TbLayoutSidebar, TbLayoutSidebarFilled } from 'react-icons/tb'; import { useReaderStore } from '@/store/readerStore'; @@ -20,9 +20,9 @@ const SidebarToggler: React.FC = ({ bookKey }) => { return ( ); diff --git a/apps/readest-app/src/app/reader/components/sidebar/BookmarkView.tsx b/apps/readest-app/src/app/reader/components/sidebar/BookmarkView.tsx new file mode 100644 index 00000000..42d0d252 --- /dev/null +++ b/apps/readest-app/src/app/reader/components/sidebar/BookmarkView.tsx @@ -0,0 +1,117 @@ +import React, { useEffect, useState } from 'react'; + +import * as CFI from 'foliate-js/epubcfi.js'; +import { useReaderStore } from '@/store/readerStore'; +import { findParentPath } from '@/utils/toc'; +import { TOCItem } from '@/libs/document'; +import { BookNote } from '@/types/book'; +import clsx from 'clsx'; + +interface BookmarkGroup { + id: number; + href: string; + label: string; + bookmarks: BookNote[]; +} + +interface BookmarkItemProps { + bookKey: string; + text: string; + cfi: string; + toc: TOCItem[]; +} + +const BookmarkItem: React.FC = ({ bookKey, text, cfi }) => { + const { getFoliateView } = useReaderStore(); + const { books } = useReaderStore(); + const [isCurrentBookmark, setIsCurrentBookmark] = useState(false); + + const bookState = books[bookKey]!; + const progress = bookState.progress!; + + useEffect(() => { + const { location } = progress; + const start = CFI.collapse(location); + const end = CFI.collapse(location, true); + setIsCurrentBookmark(CFI.compare(start, cfi) * CFI.compare(end, cfi) <= 0); + }, [progress]); + + const handleClickItem = (event: React.MouseEvent) => { + event.preventDefault(); + getFoliateView(bookKey)?.goTo(cfi); + }; + console.log('isCurrentBookmark', isCurrentBookmark); + return ( +
  • + {text} +
  • + ); +}; + +const BookmarkView: React.FC<{ + bookKey: string; + toc: TOCItem[]; +}> = ({ bookKey, toc }) => { + const { books } = useReaderStore(); + const bookState = books[bookKey]!; + const config = bookState.config!; + const { bookmarks = [] } = config; + + const bookmarkGroups: { [href: string]: BookmarkGroup } = {}; + for (const bookmark of bookmarks) { + const parentPath = findParentPath(toc, bookmark.href); + if (parentPath.length > 0) { + const href = parentPath[0]!.href || ''; + const label = parentPath[0]!.label || ''; + const id = toc.findIndex((item) => item.href === href) || Infinity; + bookmark.href = href; + if (!bookmarkGroups[href]) { + bookmarkGroups[href] = { id, href, label, bookmarks: [] }; + } + bookmarkGroups[href].bookmarks.push(bookmark); + } + } + + Object.values(bookmarkGroups).forEach((group) => { + group.bookmarks.sort((a, b) => { + return CFI.compare(a.cfi, b.cfi); + }); + }); + + const sortedGroups = Object.values(bookmarkGroups).sort((a, b) => { + return a.id - b.id; + }); + + return ( +
    +
    +
      + {sortedGroups.map((group) => ( +
    • +

      {group.label}

      +
        + {group.bookmarks.map((item) => ( + + ))} +
      +
    • + ))} +
    +
    +
    + ); +}; + +export default BookmarkView; 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 7213b18e..ac23effd 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/Content.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/Content.tsx @@ -1,6 +1,8 @@ import React from 'react'; -import TOCView from './TOCView'; + import { BookDoc } from '@/libs/document'; +import TOCView from './TOCView'; +import BookmarkView from './BookmarkView'; const SidebarContent: React.FC<{ activeTab: string; @@ -8,12 +10,12 @@ const SidebarContent: React.FC<{ currentHref: string | null; sideBarBookKey: string; }> = ({ activeTab, bookDoc, currentHref, sideBarBookKey }) => ( -
    +
    {activeTab === 'toc' && bookDoc.toc && ( )} {activeTab === 'annotations' &&
    Annotations
    } - {activeTab === 'bookmarks' &&
    Bookmarks
    } + {activeTab === 'bookmarks' && }
    ); diff --git a/apps/readest-app/src/app/reader/components/sidebar/Header.tsx b/apps/readest-app/src/app/reader/components/sidebar/Header.tsx index 9eee7184..3cad85a7 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/Header.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/Header.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { GiBookshelf } from 'react-icons/gi'; -import { CiSearch } from 'react-icons/ci'; +import { FiSearch } from 'react-icons/fi'; + import { MdOutlineMenu, MdOutlinePushPin, MdPushPin } from 'react-icons/md'; import Dropdown from '@/components/Dropdown'; import BookMenu from './BookMenu'; @@ -19,7 +20,7 @@ const SidebarHeader: React.FC<{
    { if (!books || !sideBarBookKey) return; const bookState = books[sideBarBookKey] || DEFAULT_BOOK_STATE; - const { config } = bookState; + const { progress } = bookState; setBookState(bookState); - setCurrentHref(config?.href || null); + setCurrentHref(progress?.tocHref || null); }, [books, sideBarBookKey]); const handleClickOverlay = () => { 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 66c3c190..5eb3d23a 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/TOCView.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/TOCView.tsx @@ -3,23 +3,9 @@ import React, { useEffect, useRef, useState } from 'react'; import { md5 } from 'js-md5'; import { TOCItem } from '@/libs/document'; import { useReaderStore } from '@/store/readerStore'; +import { findParentPath } from '@/utils/toc'; import { useFoliateEvents } from '../../hooks/useFoliateEvents'; -const findParentPath = (toc: TOCItem[], href: string): TOCItem[] => { - for (const item of toc) { - if (item.href === href) { - return [item]; - } - if (item.subitems) { - const path = findParentPath(item.subitems, href); - if (path.length) { - return [item, ...path]; - } - } - } - return []; -}; - const getHrefMd5 = (href: string) => md5(JSON.stringify(href)); const createExpanderIcon = (isExpanded: boolean) => { @@ -87,7 +73,7 @@ const TOCItemView: React.FC<{ )} -
    +
      {toc && toc.map((item) => ( diff --git a/apps/readest-app/src/libs/document.ts b/apps/readest-app/src/libs/document.ts index 00e4d94f..72ceb0bb 100644 --- a/apps/readest-app/src/libs/document.ts +++ b/apps/readest-app/src/libs/document.ts @@ -33,8 +33,9 @@ Map.groupBy ??= (iterable, callbackfn) => { export type DocumentFile = File; export interface TOCItem { + id: number; label: string; - href?: string; + href: string; subitems?: TOCItem[]; } diff --git a/apps/readest-app/src/store/readerStore.ts b/apps/readest-app/src/store/readerStore.ts index c6b11290..6f3c3792 100644 --- a/apps/readest-app/src/store/readerStore.ts +++ b/apps/readest-app/src/store/readerStore.ts @@ -1,10 +1,10 @@ import { create } from 'zustand'; -import { BookNote, BookContent, Book, BookConfig, PageInfo } from '@/types/book'; +import { BookNote, BookContent, Book, BookConfig, PageInfo, BookProgress } from '@/types/book'; import { EnvConfigType } from '@/services/environment'; import { SystemSettings } from '@/types/settings'; import { FoliateView } from '@/app/reader/components/FoliateViewer'; -import { BookDoc, DocumentLoader } from '@/libs/document'; +import { BookDoc, DocumentLoader, TOCItem } from '@/libs/document'; export interface BookState { key: string; @@ -13,6 +13,7 @@ export interface BookState { book?: Book | null; file?: File | null; config?: BookConfig | null; + progress?: BookProgress | null; bookDoc?: BookDoc | null; isPrimary?: boolean; } @@ -51,12 +52,11 @@ interface ReaderStore { setSettings: (settings: SystemSettings) => void; setProgress: ( key: string, - progress: number, location: string, - href: string, - chapter: string, + tocItem: TOCItem, section: PageInfo, pageinfo: PageInfo, + range: Range, ) => void; setConfig: (key: string, config: BookConfig) => void; setFoliateView: (key: string, view: FoliateView) => void; @@ -83,6 +83,7 @@ export const DEFAULT_BOOK_STATE = { file: null, book: null, config: null, + progress: null, bookDoc: null, isPrimary: true, }; @@ -210,6 +211,18 @@ export const useReaderStore = create((set, get) => ({ console.log('Loading book', key); const { book: loadedBookDoc } = await new DocumentLoader(file).open(); bookDoc = loadedBookDoc as BookDoc; + const updateTocID = (items: TOCItem[], index = 0): number => { + items.forEach((item) => { + if (item.id === undefined) { + item.id = index++; + } + if (item.subitems) { + index = updateTocID(item.subitems, index); + } + }); + return index; + }; + updateTocID(bookDoc.toc); set((state) => ({ bookDocCache: { ...state.bookDocCache, @@ -228,6 +241,7 @@ export const useReaderStore = create((set, get) => ({ book, file, config, + progress: {} as BookProgress, bookDoc, isPrimary, }, @@ -248,12 +262,11 @@ export const useReaderStore = create((set, get) => ({ setProgress: ( key: string, - progress: number, location: string, - href: string, - chapter: string, + tocItem: TOCItem, section: PageInfo, pageinfo: PageInfo, + range: Range, ) => set((state) => { const book = state.books[key]; @@ -266,13 +279,24 @@ export const useReaderStore = create((set, get) => ({ config: { ...book.config, lastUpdated: Date.now(), - href, - chapter, - progress, + href: tocItem.href, + chapter: tocItem.label, + progress: [pageinfo.current, pageinfo.total], location, section, pageinfo, }, + progress: { + ...book.progress, + progress: [pageinfo.current, pageinfo.total], + location, + tocHref: tocItem.href, + tocLabel: tocItem.label, + tocId: tocItem.id, + section, + pageinfo, + range, + }, }, }, }; diff --git a/apps/readest-app/src/types/book.ts b/apps/readest-app/src/types/book.ts index eab81b12..70ba2b12 100644 --- a/apps/readest-app/src/types/book.ts +++ b/apps/readest-app/src/types/book.ts @@ -33,6 +33,7 @@ export interface PageInfo { export interface BookNote { type: BookNoteType; cfi: string; + href: string; text?: string; style?: string; customStyle?: string; @@ -74,14 +75,20 @@ export interface BookFont { export interface ViewSettings extends BookLayout, BookStyle, BookFont {} +export interface BookProgress { + location: string; + tocHref: string; + tocLabel: string; + tocId: number; + section: PageInfo; + pageinfo: PageInfo; + range: Range; +} + export interface BookConfig { lastUpdated: number; - progress?: number; + progress?: [number, number]; location?: string; - href?: string; - chapter?: string; - section?: PageInfo; - pageinfo?: PageInfo; bookmarks?: BookNote[]; annotations?: BookNote[]; diff --git a/apps/readest-app/src/utils/toc.ts b/apps/readest-app/src/utils/toc.ts new file mode 100644 index 00000000..a07ffbc1 --- /dev/null +++ b/apps/readest-app/src/utils/toc.ts @@ -0,0 +1,16 @@ +import { TOCItem } from '@/libs/document'; + +export const findParentPath = (toc: TOCItem[], href: string): TOCItem[] => { + for (const item of toc) { + if (item.href === href) { + return [item]; + } + if (item.subitems) { + const path = findParentPath(item.subitems, href); + if (path.length) { + return [item, ...path]; + } + } + } + return []; +};