Add bookmark toggler and ribbon
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
import { BookState, useReaderStore } from '@/store/readerStore';
|
||||
import HeaderBar from './HeaderBar';
|
||||
import FoliateViewer from './FoliateViewer';
|
||||
import PageInfo from './PageInfo';
|
||||
import FooterBar from './FooterBar';
|
||||
import SectionInfo from './SectionInfo';
|
||||
import getGridTemplate from '@/utils/grid';
|
||||
|
||||
import SettingsDialog from './settings/SettingsDialog';
|
||||
import FoliateViewer from './FoliateViewer';
|
||||
import getGridTemplate from '@/utils/grid';
|
||||
import SectionInfo from './SectionInfo';
|
||||
import HeaderBar from './HeaderBar';
|
||||
import FooterBar from './FooterBar';
|
||||
import PageInfo from './PageInfo';
|
||||
import Ribbon from './Ribbon';
|
||||
|
||||
interface BookGridProps {
|
||||
bookKeys: string[];
|
||||
@@ -16,7 +18,7 @@ interface BookGridProps {
|
||||
}
|
||||
|
||||
const BookGrid: React.FC<BookGridProps> = ({ bookKeys, bookStates, onCloseBook }) => {
|
||||
const { isSideBarPinned, isSideBarVisible, sideBarWidth } = useReaderStore();
|
||||
const { isSideBarPinned, isSideBarVisible, sideBarWidth, bookmarkRibbons } = useReaderStore();
|
||||
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useReaderStore();
|
||||
const gridWidth = isSideBarPinned && isSideBarVisible ? `calc(100% - ${sideBarWidth})` : '100%';
|
||||
const gridTemplate = getGridTemplate(bookKeys.length, window.innerWidth / window.innerHeight);
|
||||
@@ -32,6 +34,7 @@ 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;
|
||||
@@ -39,6 +42,7 @@ const BookGrid: React.FC<BookGridProps> = ({ bookKeys, bookStates, onCloseBook }
|
||||
|
||||
return (
|
||||
<div key={bookKey} className='relative h-full w-full overflow-hidden'>
|
||||
{isBookmarked && <Ribbon width={marginGap} />}
|
||||
<HeaderBar
|
||||
bookKey={bookKey}
|
||||
bookTitle={book.title}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { MdOutlineBookmarkAdd, MdOutlineBookmark } from 'react-icons/md';
|
||||
import * as CFI from 'foliate-js/epubcfi.js';
|
||||
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { BookNote } from '@/types/book';
|
||||
|
||||
interface BookmarkTogglerProps {
|
||||
bookKey: string;
|
||||
}
|
||||
|
||||
const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
|
||||
const { books, updateBookmarks, setBookmarkRibbonVisibility } = useReaderStore();
|
||||
const bookState = books[bookKey]!;
|
||||
const config = bookState.config!;
|
||||
|
||||
const [isBookmarked, setIsBookmarked] = useState(false);
|
||||
|
||||
const toggleBookmark = () => {
|
||||
const { location: cfi, bookmarks = [] } = config;
|
||||
if (!cfi) return;
|
||||
if (!isBookmarked) {
|
||||
setIsBookmarked(true);
|
||||
const bookmark: BookNote = {
|
||||
type: 'bookmark',
|
||||
cfi,
|
||||
note: '',
|
||||
created: Date.now(),
|
||||
};
|
||||
bookmarks.push(bookmark);
|
||||
updateBookmarks(bookKey, bookmarks);
|
||||
} else {
|
||||
setIsBookmarked(false);
|
||||
const start = CFI.collapse(cfi);
|
||||
const end = CFI.collapse(cfi, true);
|
||||
updateBookmarks(
|
||||
bookKey,
|
||||
bookmarks.filter((item) => CFI.compare(start, item.cfi) * CFI.compare(end, item.cfi) > 0),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const { location: cfi, bookmarks = [] } = config;
|
||||
if (!cfi) return;
|
||||
|
||||
const start = CFI.collapse(cfi);
|
||||
const end = CFI.collapse(cfi, true);
|
||||
const locationBookmarked = bookmarks.some(
|
||||
(item) => CFI.compare(start, item.cfi) * CFI.compare(end, item.cfi) <= 0,
|
||||
);
|
||||
setIsBookmarked(locationBookmarked);
|
||||
setBookmarkRibbonVisibility(bookKey, locationBookmarked);
|
||||
}, [config]);
|
||||
|
||||
return (
|
||||
<button onClick={toggleBookmark} className='p-2'>
|
||||
{isBookmarked ? <MdOutlineBookmark size={20} /> : <MdOutlineBookmarkAdd size={20} />}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookmarkToggler;
|
||||
@@ -2,11 +2,12 @@ import clsx from 'clsx';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
|
||||
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import SidebarToggler from './SidebarToggler';
|
||||
import BookmarkToggler from './BookmarkToggler';
|
||||
import ViewMenu from './ViewMenu';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
|
||||
interface HeaderBarProps {
|
||||
bookKey: string;
|
||||
@@ -25,17 +26,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
}) => {
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const { hoveredBookKey, setHoveredBookKey, sideBarBookKey, setSideBarBookKey } = useReaderStore();
|
||||
const { isSideBarVisible, toggleSideBar } = useReaderStore();
|
||||
|
||||
const toggleSidebarForBook = (bookKey: string) => {
|
||||
if (sideBarBookKey === bookKey) {
|
||||
toggleSideBar();
|
||||
} else {
|
||||
setSideBarBookKey(bookKey);
|
||||
if (!isSideBarVisible) toggleSideBar();
|
||||
}
|
||||
};
|
||||
const { hoveredBookKey, isSideBarVisible, setHoveredBookKey } = useReaderStore();
|
||||
|
||||
const handleToggleDropdown = (isOpen: boolean) => {
|
||||
setIsDropdownOpen(isOpen);
|
||||
@@ -56,12 +47,9 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
onMouseEnter={() => setHoveredBookKey(bookKey)}
|
||||
onMouseLeave={() => setHoveredBookKey('')}
|
||||
>
|
||||
<div className='sidebar-toggler mr-auto flex h-full items-center'>
|
||||
<SidebarToggler
|
||||
isSidebarVisible={isSideBarVisible}
|
||||
isCurrentBook={sideBarBookKey === bookKey}
|
||||
toggleSidebar={() => toggleSidebarForBook(bookKey)}
|
||||
></SidebarToggler>
|
||||
<div className='sidebar-bookmark-toggler mr-auto flex h-full items-center'>
|
||||
<SidebarToggler bookKey={bookKey} />
|
||||
<BookmarkToggler bookKey={bookKey} />
|
||||
</div>
|
||||
|
||||
<div className='header-title flex flex-1 items-center justify-center'>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
interface RibbonProps {
|
||||
width: string;
|
||||
}
|
||||
|
||||
const Ribbon: React.FC<RibbonProps> = ({ width }) => {
|
||||
return (
|
||||
<div className='absolute inset-0 z-10 flex h-11 justify-center' style={{ width }}>
|
||||
<svg
|
||||
stroke='currentColor'
|
||||
fill='currentColor'
|
||||
strokeWidth='0'
|
||||
version='1'
|
||||
viewBox='0 0 20 45'
|
||||
enableBackground='new 0 0 20 45'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path fill='#F44336' d='M 20 45 L 10 35 L 0 45 L 0 0 L 20 0'></path>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Ribbon;
|
||||
@@ -1,24 +1,31 @@
|
||||
import React from 'react';
|
||||
import { VscLayoutSidebarLeft, VscLayoutSidebarLeftOff } from 'react-icons/vsc';
|
||||
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
|
||||
interface SidebarTogglerProps {
|
||||
isSidebarVisible: boolean;
|
||||
isCurrentBook: boolean;
|
||||
toggleSidebar: () => void;
|
||||
bookKey: string;
|
||||
}
|
||||
|
||||
const SidebarToggler: React.FC<SidebarTogglerProps> = ({
|
||||
isSidebarVisible,
|
||||
isCurrentBook,
|
||||
toggleSidebar,
|
||||
}) => (
|
||||
<button onClick={toggleSidebar} className='p-2'>
|
||||
{isCurrentBook && isSidebarVisible ? (
|
||||
<VscLayoutSidebarLeft size={16} />
|
||||
) : (
|
||||
<VscLayoutSidebarLeftOff size={16} />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
const SidebarToggler: React.FC<SidebarTogglerProps> = ({ bookKey }) => {
|
||||
const { isSideBarVisible, setSideBarBookKey, sideBarBookKey, toggleSideBar } = useReaderStore();
|
||||
const handleToggleSidebar = () => {
|
||||
if (sideBarBookKey === bookKey) {
|
||||
toggleSideBar();
|
||||
} else {
|
||||
setSideBarBookKey(bookKey);
|
||||
if (!isSideBarVisible) toggleSideBar();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<button onClick={handleToggleSidebar} className='p-2'>
|
||||
{sideBarBookKey == bookKey && isSideBarVisible ? (
|
||||
<VscLayoutSidebarLeft size={16} />
|
||||
) : (
|
||||
<VscLayoutSidebarLeftOff size={16} />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarToggler;
|
||||
|
||||
@@ -24,6 +24,7 @@ interface ReaderStore {
|
||||
books: Record<string, BookState>;
|
||||
foliateViews: Record<string, FoliateView>;
|
||||
bookDocCache: Record<string, BookDoc>;
|
||||
bookmarkRibbons: Record<string, boolean>;
|
||||
|
||||
hoveredBookKey: string | null;
|
||||
sideBarBookKey: string | null;
|
||||
@@ -39,6 +40,8 @@ interface ReaderStore {
|
||||
setSideBarVisibility: (visible: boolean) => void;
|
||||
setSideBarPin: (pinned: boolean) => void;
|
||||
|
||||
setBookmarkRibbonVisibility: (key: string, visible: boolean) => void;
|
||||
|
||||
isFontLayoutSettingsDialogOpen: boolean;
|
||||
isFontLayoutSettingsGlobal: boolean;
|
||||
setFontLayoutSettingsDialogOpen: (open: boolean) => void;
|
||||
@@ -70,7 +73,7 @@ interface ReaderStore {
|
||||
initBookState: (envConfig: EnvConfigType, id: string, key: string, isPrimary?: boolean) => void;
|
||||
|
||||
clearBookState: (key: string) => void;
|
||||
addBookmark: (key: string, bookmark: BookNote) => void;
|
||||
updateBookmarks: (key: string, bookmarks: BookNote[]) => void;
|
||||
}
|
||||
|
||||
export const DEFAULT_BOOK_STATE = {
|
||||
@@ -91,6 +94,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
books: {},
|
||||
foliateViews: {},
|
||||
bookDocCache: {},
|
||||
bookmarkRibbons: {},
|
||||
|
||||
hoveredBookKey: null,
|
||||
sideBarBookKey: null,
|
||||
@@ -274,7 +278,15 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
};
|
||||
}),
|
||||
|
||||
addBookmark: (key: string, bookmark: BookNote) =>
|
||||
setBookmarkRibbonVisibility: (key: string, visible: boolean) =>
|
||||
set((state) => ({
|
||||
bookmarkRibbons: {
|
||||
...state.bookmarkRibbons,
|
||||
[key]: visible,
|
||||
},
|
||||
})),
|
||||
|
||||
updateBookmarks: (key: string, bookmarks: BookNote[]) =>
|
||||
set((state) => {
|
||||
const book = state.books[key];
|
||||
if (!book) return state;
|
||||
@@ -286,7 +298,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
config: {
|
||||
...book.config,
|
||||
lastUpdated: Date.now(),
|
||||
bookmarks: [...(book.config?.bookmarks || []), bookmark],
|
||||
bookmarks,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
export type BookFormat = 'EPUB' | 'PDF' | 'MOBI' | 'CBZ' | 'FB2' | 'FBZ';
|
||||
export type BookNoteType = 'bookmark' | 'highlight' | 'annotation';
|
||||
export type BookNoteStyle =
|
||||
| 'underline'
|
||||
| 'squiggly'
|
||||
| 'strikethrough'
|
||||
| 'yellow'
|
||||
| 'orange'
|
||||
| 'red'
|
||||
| 'magenta'
|
||||
| 'aqua'
|
||||
| 'lime'
|
||||
| 'custom';
|
||||
|
||||
export interface Book {
|
||||
hash: string;
|
||||
@@ -19,14 +31,14 @@ export interface PageInfo {
|
||||
}
|
||||
|
||||
export interface BookNote {
|
||||
type: BookNoteType;
|
||||
cfi: string;
|
||||
start: string;
|
||||
end: string;
|
||||
page: number;
|
||||
noteText?: string;
|
||||
annotation?: string;
|
||||
lastModified: number;
|
||||
removalTimestamp?: number;
|
||||
text?: string;
|
||||
style?: string;
|
||||
customStyle?: string;
|
||||
note: string;
|
||||
created: number;
|
||||
modified?: number;
|
||||
}
|
||||
|
||||
export interface BookLayout {
|
||||
|
||||
Reference in New Issue
Block a user