Bookmark view in sidebar
This commit is contained in:
@@ -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) => (
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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 [];
|
||||
};
|
||||
Reference in New Issue
Block a user