Add Tables of Content(TOC) in SideBar
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "Readest",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"identifier": "com.bilingify.digest",
|
||||
"build": {
|
||||
"frontendDist": "../out",
|
||||
|
||||
@@ -32,12 +32,3 @@ foliate-view {
|
||||
height: 100vh;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
/* Hide scrollbar for IE and Edge */
|
||||
*{
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
@@ -10,10 +10,14 @@ interface BookCardProps {
|
||||
|
||||
const BookCard: React.FC<BookCardProps> = ({ cover, title, author }) => {
|
||||
return (
|
||||
<div className='flex w-full items-center'>
|
||||
<img src={cover} alt='Book cover' className='mr-4 w-[15%] rounded-sm object-cover' />
|
||||
<div className='flex h-20 w-full items-center'>
|
||||
<img
|
||||
src={cover}
|
||||
alt='Book cover'
|
||||
className='mr-4 aspect-auto max-h-20 w-[15%] max-w-14 rounded-sm object-cover shadow-md'
|
||||
/>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<h4 className='w-[90%] truncate text-base font-semibold'>{title}</h4>
|
||||
<h4 className='line-clamp-2 w-[90%] text-sm font-semibold'>{title}</h4>
|
||||
<p className='truncate text-sm text-gray-600'>{formatAuthors(author)}</p>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useFoliateEvents } from '../hooks/useFoliateEvents';
|
||||
import { BookDoc } from '@/libs/document';
|
||||
import { BookConfig } from '@/types/book';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
|
||||
type FoliateViewerProps = {
|
||||
bookId: string;
|
||||
@@ -53,6 +54,7 @@ const getCSS = (spacing: number, justify: boolean, hyphenate: boolean) => `
|
||||
export interface FoliateView extends HTMLElement {
|
||||
open: (book: BookDoc) => Promise<void>;
|
||||
init: (options: { lastLocation: string }) => void;
|
||||
goTo: (href: string) => void;
|
||||
goToFraction: (fraction: number) => void;
|
||||
renderer: {
|
||||
setStyles: (css: string) => void;
|
||||
@@ -65,15 +67,18 @@ const FoliateViewer: React.FC<FoliateViewerProps> = ({ bookId, bookConfig, bookD
|
||||
const viewRef = useRef<HTMLDivElement>(null);
|
||||
const [view, setView] = useState<FoliateView | null>(null);
|
||||
const isViewCreated = useRef(false);
|
||||
const { setFoliateView } = useReaderStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (isViewCreated.current) return;
|
||||
const openBook = async () => {
|
||||
console.log('opening book');
|
||||
await import('foliate-js/view.js');
|
||||
const view = document.createElement('foliate-view') as FoliateView;
|
||||
document.body.append(view);
|
||||
viewRef.current?.appendChild(view);
|
||||
setView(view);
|
||||
setFoliateView(view);
|
||||
await view.open(bookDoc);
|
||||
if ('setStyles' in view.renderer) {
|
||||
view.renderer.setStyles(getCSS(2.4, true, true));
|
||||
|
||||
@@ -13,55 +13,35 @@ import Spinner from '@/components/Spinner';
|
||||
import FoliateViewer from './FoliateViewer';
|
||||
import SideBar from './SideBar';
|
||||
|
||||
interface ReaderContentProps {}
|
||||
|
||||
const ReaderContent: React.FC<ReaderContentProps> = ({}) => {
|
||||
const ReaderContent = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const id = searchParams.get('id');
|
||||
|
||||
const [bookDoc, setBookDoc] = React.useState<BookDoc>();
|
||||
const { envConfig } = useEnv();
|
||||
const { library, books, settings, fetchBook, setLibrary } = useReaderStore();
|
||||
const { books, settings, fetchBook, saveConfig, saveSettings } = useReaderStore();
|
||||
const defaultBookState = { loading: true, error: null, file: null, book: null, config: null };
|
||||
const bookState = id ? books[id] || defaultBookState : defaultBookState;
|
||||
|
||||
const [sideBarWidth, setSideBarWidth] = useState(
|
||||
settings.globalReadSettings.sideBarWidth ?? '20%',
|
||||
settings.globalReadSettings.sideBarWidth ?? '25%',
|
||||
);
|
||||
const [isSideBarPinned, setIsSideBarPinned] = useState(
|
||||
settings.globalReadSettings.isSideBarPinned ?? true,
|
||||
);
|
||||
const [isSideBarVisible, setSideBarVisibility] = useState(isSideBarPinned);
|
||||
const [isClosingBook, setClosingBook] = useState(false);
|
||||
const [isTopBarVisible, setTopBarVisibility] = useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const { file, book, config } = bookState;
|
||||
if (isClosingBook) {
|
||||
if (book && config) {
|
||||
book.lastUpdated = Date.now();
|
||||
const bookIndex = library.findIndex((b) => b.hash === book.hash);
|
||||
if (bookIndex !== -1) {
|
||||
library[bookIndex] = book;
|
||||
}
|
||||
setLibrary(library);
|
||||
envConfig.getAppService().then((appService) => {
|
||||
config.lastUpdated = Date.now();
|
||||
appService.saveBookConfig(book, config);
|
||||
appService.saveLibraryBooks(library);
|
||||
appService.saveSettings(settings);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const { file } = bookState;
|
||||
if (id && !file) {
|
||||
fetchBook(envConfig, id);
|
||||
}
|
||||
if (!bookDoc && bookState.file) {
|
||||
if (!bookDoc && file) {
|
||||
const loadDocument = async () => {
|
||||
if (file) {
|
||||
const { book } = await new DocumentLoader(file).open();
|
||||
@@ -70,7 +50,7 @@ const ReaderContent: React.FC<ReaderContentProps> = ({}) => {
|
||||
};
|
||||
loadDocument();
|
||||
}
|
||||
}, [isClosingBook, bookState.file, envConfig, fetchBook, id]);
|
||||
}, [bookState.file, envConfig, fetchBook, id]);
|
||||
|
||||
const handleResize = (newWidth: string) => {
|
||||
setSideBarWidth(newWidth);
|
||||
@@ -86,20 +66,35 @@ const ReaderContent: React.FC<ReaderContentProps> = ({}) => {
|
||||
};
|
||||
|
||||
const handleCloseBook = () => {
|
||||
setClosingBook(true);
|
||||
const { book, config } = bookState;
|
||||
if (book && config) {
|
||||
saveConfig(envConfig, book, config);
|
||||
saveSettings(envConfig, settings);
|
||||
}
|
||||
router.back();
|
||||
};
|
||||
|
||||
const topBarWidth = isSideBarPinned && isSideBarVisible ? `calc(100% - ${sideBarWidth})` : '100%';
|
||||
|
||||
if (!id || !bookDoc || !bookState.config || !bookState.book) {
|
||||
return null;
|
||||
return (
|
||||
<div className={'flex-1 overflow-hidden'}>
|
||||
{bookState.loading && <Spinner loading={bookState.loading} />}
|
||||
{bookState.error && (
|
||||
<div className='text-center'>
|
||||
<h2 className='text-red-500'>{bookState.error}</h2>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex h-screen overflow-hidden'>
|
||||
<div className='flex h-screen'>
|
||||
<SideBar
|
||||
book={bookState.book}
|
||||
tocData={bookDoc.toc}
|
||||
currentHref={bookState.config.href ?? null}
|
||||
width={sideBarWidth}
|
||||
isVisible={isSideBarVisible}
|
||||
isPinned={isSideBarPinned}
|
||||
@@ -109,13 +104,7 @@ const ReaderContent: React.FC<ReaderContentProps> = ({}) => {
|
||||
onSetVisibility={(visibility: boolean) => setSideBarVisibility(visibility)}
|
||||
/>
|
||||
|
||||
<div className={`flex-1`}>
|
||||
{bookState.loading && <Spinner loading={bookState.loading} />}
|
||||
{bookState.error && (
|
||||
<div className='text-center'>
|
||||
<h2 className='text-red-500'>{bookState.error}</h2>
|
||||
</div>
|
||||
)}
|
||||
<div className={'flex-1 overflow-hidden'}>
|
||||
<div
|
||||
className={`topbar absolute top-0 z-10 h-10 border-b ${
|
||||
isTopBarVisible ? 'opacity-100' : 'opacity-0'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Book } from '@/types/book';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
MdSearch,
|
||||
MdOutlinePushPin,
|
||||
@@ -10,10 +10,17 @@ import {
|
||||
} from 'react-icons/md';
|
||||
import { GiBookshelf } from 'react-icons/gi';
|
||||
|
||||
import { TOCItem } from '@/libs/document';
|
||||
import BookCard from './BookCard';
|
||||
import TOCView from './TOCView';
|
||||
|
||||
interface SideBarProps {
|
||||
const MIN_SIDEBAR_WIDTH = 0.15;
|
||||
const MAX_SIDEBAR_WIDTH = 0.45;
|
||||
|
||||
const SideBar: React.FC<{
|
||||
book: Book;
|
||||
tocData: TOCItem[];
|
||||
currentHref: string | null;
|
||||
width: string;
|
||||
isVisible: boolean;
|
||||
isPinned: boolean;
|
||||
@@ -21,13 +28,10 @@ interface SideBarProps {
|
||||
onTogglePin: () => void;
|
||||
onResize: (newWidth: string) => void;
|
||||
onGoToLibrary: () => void;
|
||||
}
|
||||
|
||||
const MIN_SIDEBAR_WIDTH = '10em';
|
||||
const MAX_SIDEBAR_WIDTH = '40em';
|
||||
|
||||
const SideBar: React.FC<SideBarProps> = ({
|
||||
}> = ({
|
||||
book,
|
||||
tocData,
|
||||
currentHref,
|
||||
width,
|
||||
isPinned,
|
||||
isVisible,
|
||||
@@ -37,7 +41,6 @@ const SideBar: React.FC<SideBarProps> = ({
|
||||
onGoToLibrary,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState('toc');
|
||||
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {}, [isPinned, isVisible]);
|
||||
|
||||
@@ -54,8 +57,8 @@ const SideBar: React.FC<SideBarProps> = ({
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const newWidthPx = e.clientX;
|
||||
const width = `${Math.round((newWidthPx / window.innerWidth) * 10000) / 100}%`;
|
||||
const minWidthPx = parseFloat(MIN_SIDEBAR_WIDTH) * 16;
|
||||
const maxWidthPx = parseFloat(MAX_SIDEBAR_WIDTH) * 16;
|
||||
const minWidthPx = MIN_SIDEBAR_WIDTH * window.innerWidth;
|
||||
const maxWidthPx = MAX_SIDEBAR_WIDTH * window.innerWidth;
|
||||
if (newWidthPx >= minWidthPx && newWidthPx <= maxWidthPx) {
|
||||
onResize(width);
|
||||
}
|
||||
@@ -72,13 +75,12 @@ const SideBar: React.FC<SideBarProps> = ({
|
||||
className='sidebar-container z-20 h-full bg-gray-200'
|
||||
style={{
|
||||
width: `${width}`,
|
||||
height: '100%',
|
||||
minWidth: MIN_SIDEBAR_WIDTH,
|
||||
maxWidth: MAX_SIDEBAR_WIDTH,
|
||||
minWidth: `${MIN_SIDEBAR_WIDTH * 100}%`,
|
||||
maxWidth: `${MAX_SIDEBAR_WIDTH * 100}%`,
|
||||
position: isPinned ? 'relative' : 'absolute',
|
||||
}}
|
||||
>
|
||||
<div ref={sidebarRef} className={'sidebar h-full'}>
|
||||
<div className={'sidebar h-full'}>
|
||||
<div className='flex h-10 items-center justify-between pl-1.5 pr-3'>
|
||||
<div className='flex items-center'>
|
||||
<button className='btn btn-ghost h-8 min-h-8 w-8 p-0' onClick={onGoToLibrary}>
|
||||
@@ -97,10 +99,17 @@ const SideBar: React.FC<SideBarProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-b p-3 shadow-sm'>
|
||||
<div className='border-b px-3 shadow-sm'>
|
||||
<BookCard cover={book.coverImageUrl!} title={book.title} author={book.author} />
|
||||
</div>
|
||||
<div className='absolute bottom-0 flex w-full'>
|
||||
<div className='sidebar-content overflow-y-auto'>
|
||||
{activeTab === 'toc' && (
|
||||
<TOCView toc={tocData} bookId={book.hash} currentHref={currentHref} />
|
||||
)}
|
||||
{activeTab === 'bookmarks' && <div>Bookmarks</div>}
|
||||
{activeTab === 'annotations' && <div>Annotations</div>}
|
||||
</div>
|
||||
<div className='bottom-tab absolute bottom-0 flex w-full bg-gray-200'>
|
||||
<button
|
||||
className={`m-1.5 flex-1 rounded-md p-2 ${activeTab === 'toc' ? 'bg-gray-300' : ''}`}
|
||||
onClick={() => setActiveTab('toc')}
|
||||
@@ -120,11 +129,6 @@ const SideBar: React.FC<SideBarProps> = ({
|
||||
<MdBookmarkBorder size={20} className='mx-auto' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='p-4'>
|
||||
{activeTab === 'toc' && <div>Table of Contents</div>}
|
||||
{activeTab === 'bookmarks' && <div>Bookmarks</div>}
|
||||
{activeTab === 'annotations' && <div>Annotations</div>}
|
||||
</div>
|
||||
<div
|
||||
className='drag-bar bg-base-300 absolute right-0 top-0 h-full w-0.5 cursor-col-resize'
|
||||
onMouseDown={handleMouseDown}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { TOCItem } from '@/libs/document';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
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 createExpanderIcon = (isExpanded: boolean) => {
|
||||
return (
|
||||
<svg
|
||||
viewBox='0 0 8 10'
|
||||
width='8'
|
||||
height='10'
|
||||
className={`transform transition-transform ${isExpanded ? 'rotate-90' : 'rotate-0'}`}
|
||||
style={{ transformOrigin: 'center' }}
|
||||
>
|
||||
<polygon points='0 0, 8 5, 0 10' />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const TOCItemView: React.FC<{
|
||||
bookId: string;
|
||||
item: TOCItem;
|
||||
depth: number;
|
||||
setCurrentHref: (href: string) => void;
|
||||
currentHref: string | null;
|
||||
expandedItems: string[];
|
||||
}> = ({ bookId, item, depth, setCurrentHref, currentHref, expandedItems }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(expandedItems.includes(item.href || ''));
|
||||
const { foliateView } = useReaderStore();
|
||||
|
||||
const handleToggleExpand = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setIsExpanded((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleClickItem = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
if (item.href) {
|
||||
foliateView?.goTo(item.href);
|
||||
setCurrentHref(item.href);
|
||||
}
|
||||
};
|
||||
|
||||
const isActive = currentHref === item.href;
|
||||
|
||||
useEffect(() => {
|
||||
setIsExpanded(expandedItems.includes(item.href || ''));
|
||||
}, [expandedItems, item.href]);
|
||||
|
||||
return (
|
||||
<li className='w-full' style={{ paddingTop: '1px' }}>
|
||||
<span
|
||||
role='treeitem'
|
||||
tabIndex={-1}
|
||||
onClick={item.href ? handleClickItem : undefined}
|
||||
style={{ paddingInlineStart: `${(depth + 1) * 12}px` }}
|
||||
aria-expanded={isExpanded ? 'true' : 'false'}
|
||||
aria-selected={isActive ? 'true' : 'false'}
|
||||
data-href={item.href}
|
||||
className={`flex w-full cursor-pointer items-center rounded-md py-2 ${
|
||||
isActive ? 'bg-gray-300 hover:bg-gray-400' : 'hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
{item.subitems && (
|
||||
<span onClick={handleToggleExpand} className='inline-block cursor-pointer'>
|
||||
{createExpanderIcon(isExpanded)}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className='ml-2 truncate text-ellipsis font-sans text-sm font-light'
|
||||
style={{
|
||||
maxWidth: 'calc(100% - 24px)',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
</span>
|
||||
{item.subitems && isExpanded && (
|
||||
<ol role='group'>
|
||||
{item.subitems.map((subitem) => (
|
||||
<TOCItemView
|
||||
bookId={bookId}
|
||||
key={subitem.label}
|
||||
item={subitem}
|
||||
depth={depth + 1}
|
||||
setCurrentHref={setCurrentHref}
|
||||
currentHref={currentHref}
|
||||
expandedItems={expandedItems}
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const TOCView: React.FC<{
|
||||
bookId: string;
|
||||
toc: TOCItem[];
|
||||
currentHref: string | null;
|
||||
}> = ({ bookId, toc, currentHref: href }) => {
|
||||
const [currentHref, setCurrentHref] = useState<string | null>(href);
|
||||
const [expandedItems, setExpandedItems] = useState<string[]>([]);
|
||||
const { foliateView } = useReaderStore();
|
||||
const tocRef = useRef<HTMLUListElement | null>(null);
|
||||
|
||||
const tocRelocateHandler = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
const { tocItem } = detail;
|
||||
if (tocItem?.href) {
|
||||
setCurrentHref(tocItem.href);
|
||||
}
|
||||
};
|
||||
|
||||
useFoliateEvents(foliateView, bookId, { onRelocate: tocRelocateHandler });
|
||||
|
||||
const expandParents = (toc: TOCItem[], href: string) => {
|
||||
const parentPath = findParentPath(toc, href).map((item) => item.href);
|
||||
setExpandedItems(parentPath.filter(Boolean) as string[]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const currentItem = tocRef.current?.querySelector(`[data-href="${currentHref}"]`);
|
||||
if (currentItem) {
|
||||
const rect = currentItem.getBoundingClientRect();
|
||||
const isVisible = rect.top >= 0 && rect.bottom <= window.innerHeight;
|
||||
if (!isVisible) {
|
||||
(currentItem as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
(currentItem as HTMLElement).setAttribute('aria-current', 'page');
|
||||
}
|
||||
if (currentHref) {
|
||||
expandParents(toc, currentHref);
|
||||
}
|
||||
}, [toc, currentHref]);
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<div className='max-h-[calc(100vh-168px)] overflow-y-auto rounded border'>
|
||||
<ul role='tree' ref={tocRef} className='overflow-y-auto px-2'>
|
||||
{toc &&
|
||||
toc.map((item) => (
|
||||
<TOCItemView
|
||||
bookId={bookId}
|
||||
key={item.label}
|
||||
item={item}
|
||||
depth={0}
|
||||
setCurrentHref={setCurrentHref}
|
||||
currentHref={currentHref}
|
||||
expandedItems={expandedItems}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TOCView;
|
||||
@@ -15,13 +15,14 @@ export const useFoliateEvents = (
|
||||
const setProgress = useReaderStore((state) => state.setProgress);
|
||||
|
||||
const defaultLoadHandler = (event: Event) => {
|
||||
console.log('load event:', event);
|
||||
const detail = (event as CustomEvent).detail;
|
||||
console.log('load event:', detail);
|
||||
};
|
||||
|
||||
const defaultRelocateHandler = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
// console.log('relocate:', detail);
|
||||
setProgress(bookId, detail.fraction, detail.cfi, detail.location);
|
||||
setProgress(bookId, detail.fraction, detail.cfi, detail.tocItem?.href, detail.location);
|
||||
};
|
||||
|
||||
const onLoad = handlers?.onLoad || defaultLoadHandler;
|
||||
|
||||
@@ -25,7 +25,7 @@ const ReaderPage = () => {
|
||||
};
|
||||
|
||||
initLibrary();
|
||||
}, [envConfig, setLibrary]);
|
||||
}, [envConfig, setSettings, setLibrary]);
|
||||
|
||||
return (
|
||||
settings.globalReadSettings && (
|
||||
|
||||
@@ -32,6 +32,12 @@ Map.groupBy ??= (iterable, callbackfn) => {
|
||||
|
||||
export type DocumentFile = File;
|
||||
|
||||
export interface TOCItem {
|
||||
label: string;
|
||||
href?: string;
|
||||
subitems?: TOCItem[];
|
||||
}
|
||||
|
||||
export interface BookDoc {
|
||||
metadata: {
|
||||
title: string;
|
||||
@@ -39,6 +45,7 @@ export interface BookDoc {
|
||||
editor?: string;
|
||||
publisher?: string;
|
||||
};
|
||||
toc: Array<TOCItem>;
|
||||
getCover(): Promise<Blob | null>;
|
||||
}
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ export abstract class BaseAppService implements AppService {
|
||||
|
||||
async loadBookContent(book: Book): Promise<BookContent> {
|
||||
const fp = getFilename(book);
|
||||
let file = await new RemoteFile(this.fs.getURL(`${this.localBooksDir}/${fp}`), fp).open();
|
||||
const file = await new RemoteFile(this.fs.getURL(`${this.localBooksDir}/${fp}`), fp).open();
|
||||
return { book, file, config: await this.loadBookConfig(book) };
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { create } from 'zustand';
|
||||
import { BookNote, BookContent, Book, BookConfig, PageInfo } from '@/types/book';
|
||||
import { EnvConfigType } from '@/services/environment';
|
||||
import { SystemSettings } from '@/types/settings';
|
||||
import { FoliateView } from '@/app/reader/components/FoliateViewer';
|
||||
|
||||
interface BookState {
|
||||
loading?: boolean;
|
||||
@@ -10,28 +11,55 @@ interface BookState {
|
||||
book?: Book | null;
|
||||
file?: File | null;
|
||||
config?: BookConfig | null;
|
||||
fraction?: number;
|
||||
}
|
||||
|
||||
interface ReaderStore {
|
||||
library: Book[];
|
||||
books: Record<string, BookState>;
|
||||
settings: SystemSettings;
|
||||
foliateView: FoliateView | null;
|
||||
|
||||
setLibrary: (books: Book[]) => void;
|
||||
setSettings: (settings: SystemSettings) => void;
|
||||
setProgress: (
|
||||
id: string,
|
||||
progress: number,
|
||||
location: string,
|
||||
href: string,
|
||||
pageinfo: PageInfo,
|
||||
) => void;
|
||||
setFoliateView: (view: FoliateView | null) => void;
|
||||
|
||||
saveConfig: (envConfig: EnvConfigType, book: Book, config: BookConfig) => void;
|
||||
saveSettings: (envConfig: EnvConfigType, settings: SystemSettings) => void;
|
||||
|
||||
fetchBook: (envConfig: EnvConfigType, id: string) => Promise<Book | null>;
|
||||
setProgress: (id: string, progress: number, location: string, pageinfo: PageInfo) => void;
|
||||
addBookmark: (id: string, bookmark: BookNote) => void;
|
||||
}
|
||||
|
||||
export const useReaderStore = create<ReaderStore>((set) => ({
|
||||
export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
library: [],
|
||||
books: {},
|
||||
settings: {} as SystemSettings,
|
||||
foliateView: null,
|
||||
|
||||
setFoliateView: (view) => set({ foliateView: view }),
|
||||
setLibrary: (books: Book[]) => set({ library: books }),
|
||||
setSettings: (settings: SystemSettings) => set({ settings }),
|
||||
|
||||
saveConfig: async (envConfig: EnvConfigType, book: Book, config: BookConfig) => {
|
||||
const appService = await envConfig.getAppService();
|
||||
const { library } = get();
|
||||
const bookIndex = library.findIndex((b) => b.hash === book.hash);
|
||||
if (bookIndex !== -1) {
|
||||
book.lastUpdated = Date.now();
|
||||
library[bookIndex] = book;
|
||||
}
|
||||
set({ library });
|
||||
config.lastUpdated = Date.now();
|
||||
appService.saveBookConfig(book, config);
|
||||
appService.saveLibraryBooks(library);
|
||||
},
|
||||
saveSettings: async (envConfig: EnvConfigType, settings: SystemSettings) => {
|
||||
const appService = await envConfig.getAppService();
|
||||
await appService.saveSettings(settings);
|
||||
@@ -74,7 +102,7 @@ export const useReaderStore = create<ReaderStore>((set) => ({
|
||||
}
|
||||
},
|
||||
|
||||
setProgress: (id: string, progress: number, location: string, pageinfo: PageInfo) =>
|
||||
setProgress: (id: string, progress: number, location: string, href: string, pageinfo: PageInfo) =>
|
||||
set((state) => {
|
||||
const book = state.books[id];
|
||||
if (!book) return state;
|
||||
@@ -86,6 +114,7 @@ export const useReaderStore = create<ReaderStore>((set) => ({
|
||||
config: {
|
||||
...book.config,
|
||||
lastUpdated: Date.now(),
|
||||
href,
|
||||
progress,
|
||||
location,
|
||||
pageinfo,
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface BookConfig {
|
||||
lastUpdated: number;
|
||||
progress?: number;
|
||||
location?: string;
|
||||
href?: string;
|
||||
pageinfo?: PageInfo;
|
||||
bookmarks?: BookNote[];
|
||||
annotations?: BookNote[];
|
||||
|
||||
@@ -32,6 +32,10 @@ interface LanguageMap {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
interface Contributor {
|
||||
name: LanguageMap;
|
||||
}
|
||||
|
||||
const formatLanguageMap = (x: string | LanguageMap): string => {
|
||||
if (!x) return '';
|
||||
if (typeof x === 'string') return x;
|
||||
@@ -41,7 +45,7 @@ const formatLanguageMap = (x: string | LanguageMap): string => {
|
||||
|
||||
const listFormat = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' });
|
||||
|
||||
const formatContributors = (contributors: any) =>
|
||||
export const formatAuthors = (contributors: string | Contributor | [string | Contributor]) =>
|
||||
Array.isArray(contributors)
|
||||
? listFormat.format(
|
||||
contributors.map((contributor) =>
|
||||
@@ -51,5 +55,3 @@ const formatContributors = (contributors: any) =>
|
||||
: typeof contributors === 'string'
|
||||
? contributors
|
||||
: formatLanguageMap(contributors?.name);
|
||||
|
||||
export const formatAuthors = (authors: any) => formatContributors(authors);
|
||||
|
||||
Reference in New Issue
Block a user