Bookmark view in sidebar

This commit is contained in:
chrox
2024-11-05 00:08:45 +01:00
parent ff71497209
commit 071e09dad1
15 changed files with 216 additions and 66 deletions
@@ -35,9 +35,9 @@ const BookGrid: React.FC<BookGridProps> = ({ 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<BookGridProps> = ({ bookKeys, bookStates, onCloseBook }
/>
</>
)}
<FooterBar bookKey={bookKey} progress={progress} isHoveredAnim={false} />
<FooterBar bookKey={bookKey} pageinfo={pageinfo} isHoveredAnim={false} />
{isFontLayoutSettingsDialogOpen && <SettingsDialog bookKey={bookKey} config={config} />}
</div>
);
@@ -13,17 +13,23 @@ const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ 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(),
};
@@ -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) => {
@@ -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<FooterBarProps> = ({ bookKey, progress, isHoveredAnim }) => {
const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim }) => {
const { isSideBarVisible, hoveredBookKey, setHoveredBookKey, getFoliateView } = useReaderStore();
const handleProgressChange = (event: React.ChangeEvent) => {
@@ -28,7 +28,7 @@ const FooterBar: React.FC<FooterBarProps> = ({ bookKey, progress, isHoveredAnim
const foliateView = getFoliateView(bookKey);
foliateView?.goRight();
};
const progressFraction = pageinfo ? pageinfo.current / pageinfo.total : 0;
return (
<div
className={clsx(
@@ -45,14 +45,14 @@ const FooterBar: React.FC<FooterBarProps> = ({ bookKey, progress, isHoveredAnim
<RiArrowLeftWideLine size={20} />
</button>
<span className='mx-2 text-center text-sm text-black'>
{progress ? `${Math.round(progress * 100)}%` : ''}
{pageinfo ? `${Math.round(progressFraction * 100)}%` : ''}
</span>
<input
type='range'
className='mx-2 w-full'
min={0}
max={100}
value={progress ? progress * 100 : 0}
value={pageinfo ? progressFraction * 100 : 0}
onChange={(e) => handleProgressChange(e)}
/>
<button className='btn btn-ghost mx-2 h-8 min-h-8 w-8 p-0' onClick={handleGoNext}>
@@ -12,9 +12,7 @@ const SectionInfo: React.FC<SectionInfoProps> = ({ chapter, gapLeft }) => {
className={clsx('pageinfo absolute right-0 top-0 flex h-8 items-end')}
style={{ left: gapLeft }}
>
<h2 className='text-center font-sans font-light text-slate-500' style={{ fontSize: '10px' }}>
{chapter || ''}
</h2>
<h2 className='text-center font-sans text-xs font-light text-slate-500'>{chapter || ''}</h2>
</div>
);
};
@@ -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<SidebarTogglerProps> = ({ bookKey }) => {
return (
<button onClick={handleToggleSidebar} className='p-2'>
{sideBarBookKey == bookKey && isSideBarVisible ? (
<VscLayoutSidebarLeft size={16} />
<TbLayoutSidebarFilled size={20} />
) : (
<VscLayoutSidebarLeftOff size={16} />
<TbLayoutSidebar size={20} />
)}
</button>
);
@@ -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<BookmarkItemProps> = ({ 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 (
<li
className={clsx(
'my-2 cursor-pointer rounded-lg p-2 text-sm',
isCurrentBookmark ? 'bg-base-300 hover:bg-gray-300' : 'hover:bg-base-300 bg-white',
)}
onClick={handleClickItem}
>
<span className='line-clamp-3'>{text}</span>
</li>
);
};
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 (
<div className='relative'>
<div className='max-h-[calc(100vh-173px)] overflow-y-auto rounded pt-2'>
<ul role='tree' className='overflow-y-auto px-2'>
{sortedGroups.map((group) => (
<li key={group.href} className='p-2'>
<h3 className='line-clamp-1 font-normal'>{group.label}</h3>
<ul>
{group.bookmarks.map((item) => (
<BookmarkItem
key={item.cfi}
bookKey={bookKey}
text={item.text || ''}
cfi={item.cfi}
toc={toc}
/>
))}
</ul>
</li>
))}
</ul>
</div>
</div>
);
};
export default BookmarkView;
@@ -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 }) => (
<div className='sidebar-content overflow-y-auto shadow-inner'>
<div className='sidebar-content overflow-y-auto font-sans text-sm font-light shadow-inner'>
{activeTab === 'toc' && bookDoc.toc && (
<TOCView toc={bookDoc.toc} bookKey={sideBarBookKey} currentHref={currentHref} />
)}
{activeTab === 'annotations' && <div>Annotations</div>}
{activeTab === 'bookmarks' && <div>Bookmarks</div>}
{activeTab === 'bookmarks' && <BookmarkView toc={bookDoc.toc} bookKey={sideBarBookKey} />}
</div>
);
@@ -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<{
</div>
<div className='flex size-[50%] min-w-20 items-center justify-between'>
<button className='btn btn-ghost left-0 h-8 min-h-8 w-8 p-0'>
<CiSearch size={20} className='fill-base-content' />
<FiSearch size={18} />
</button>
<Dropdown
className='dropdown-bottom flex justify-center'
@@ -35,9 +35,9 @@ const SideBar: React.FC<{
useEffect(() => {
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 = () => {
@@ -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<{
</span>
)}
<span
className='ml-2 truncate text-ellipsis font-sans text-sm font-light'
className='ml-2 truncate text-ellipsis'
style={{
maxWidth: 'calc(100% - 24px)',
whiteSpace: 'nowrap',
@@ -163,7 +149,7 @@ const TOCView: React.FC<{
return (
<div className='relative'>
<div className='max-h-[calc(100vh-173px)] overflow-y-auto rounded'>
<div className='max-h-[calc(100vh-173px)] overflow-y-auto rounded pt-2'>
<ul role='tree' ref={tocRef} className='overflow-y-auto px-2'>
{toc &&
toc.map((item) => (
+2 -1
View File
@@ -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[];
}
+35 -11
View File
@@ -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<ReaderStore>((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<ReaderStore>((set, get) => ({
book,
file,
config,
progress: {} as BookProgress,
bookDoc,
isPrimary,
},
@@ -248,12 +262,11 @@ export const useReaderStore = create<ReaderStore>((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<ReaderStore>((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,
},
},
},
};
+12 -5
View File
@@ -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[];
+16
View File
@@ -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 [];
};