Save reading progress in book config
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useFoliateEvents } from '../hooks/useFoliateEvents';
|
||||
import { BookDoc } from '@/libs/document';
|
||||
import { BookConfig } from '@/types/book';
|
||||
|
||||
type FoliateViewerProps = {
|
||||
bookId: string;
|
||||
bookConfig: BookConfig;
|
||||
bookDoc: BookDoc;
|
||||
};
|
||||
|
||||
const getCSS = (spacing: number, justify: boolean, hyphenate: boolean) => `
|
||||
@namespace epub "http://www.idpf.org/2007/ops";
|
||||
html {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
/* https://github.com/whatwg/html/issues/5426 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
a:link {
|
||||
color: lightblue;
|
||||
}
|
||||
}
|
||||
p, li, blockquote, dd {
|
||||
line-height: ${spacing};
|
||||
text-align: ${justify ? 'justify' : 'start'};
|
||||
-webkit-hyphens: ${hyphenate ? 'auto' : 'manual'};
|
||||
hyphens: ${hyphenate ? 'auto' : 'manual'};
|
||||
-webkit-hyphenate-limit-before: 3;
|
||||
-webkit-hyphenate-limit-after: 2;
|
||||
-webkit-hyphenate-limit-lines: 2;
|
||||
hanging-punctuation: allow-end last;
|
||||
widows: 2;
|
||||
}
|
||||
/* prevent the above from overriding the align attribute */
|
||||
[align="left"] { text-align: left; }
|
||||
[align="right"] { text-align: right; }
|
||||
[align="center"] { text-align: center; }
|
||||
[align="justify"] { text-align: justify; }
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
aside[epub|type~="endnote"],
|
||||
aside[epub|type~="footnote"],
|
||||
aside[epub|type~="note"],
|
||||
aside[epub|type~="rearnote"] {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface FoliateView extends HTMLElement {
|
||||
open: (book: BookDoc) => Promise<void>;
|
||||
init: (options: { lastLocation: string }) => void;
|
||||
goToFraction: (fraction: number) => void;
|
||||
renderer: {
|
||||
setStyles: (css: string) => void;
|
||||
next: () => Promise<void>;
|
||||
prev: () => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
const FoliateViewer: React.FC<FoliateViewerProps> = ({ bookId, bookConfig, bookDoc }) => {
|
||||
const viewRef = useRef<HTMLDivElement>(null);
|
||||
const [view, setView] = useState<FoliateView | null>(null);
|
||||
const isViewCreated = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isViewCreated.current) return;
|
||||
const openBook = async () => {
|
||||
await import('foliate-js/view.js');
|
||||
const view = document.createElement('foliate-view') as FoliateView;
|
||||
document.body.append(view);
|
||||
viewRef.current?.appendChild(view);
|
||||
setView(view);
|
||||
await view.open(bookDoc);
|
||||
if ('setStyles' in view.renderer) {
|
||||
view.renderer.setStyles(getCSS(2.4, true, true));
|
||||
}
|
||||
const lastLocation = bookConfig.location;
|
||||
if (lastLocation) {
|
||||
view.init({ lastLocation });
|
||||
} else {
|
||||
view.goToFraction(0);
|
||||
}
|
||||
};
|
||||
|
||||
openBook();
|
||||
isViewCreated.current = true;
|
||||
}, [bookDoc]);
|
||||
|
||||
useFoliateEvents(view, bookId);
|
||||
|
||||
const handleTap = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
const { clientX } = event;
|
||||
const width = window.innerWidth;
|
||||
const leftThreshold = width * 0.5;
|
||||
const rightThreshold = width * 0.5;
|
||||
|
||||
if (clientX < leftThreshold) {
|
||||
view?.renderer.prev();
|
||||
} else if (clientX > rightThreshold) {
|
||||
view?.renderer.next();
|
||||
}
|
||||
};
|
||||
|
||||
return <div ref={viewRef} onClick={handleTap} />;
|
||||
};
|
||||
|
||||
export default FoliateViewer;
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { BookDoc, DocumentLoader } from '@/libs/document';
|
||||
|
||||
import Spinner from '@/components/Spinner';
|
||||
import FoliateViewer from './FoliateViewer';
|
||||
|
||||
interface ReaderContentProps {
|
||||
isClosingBook: boolean;
|
||||
}
|
||||
|
||||
const ReaderContent: React.FC<ReaderContentProps> = ({ isClosingBook }) => {
|
||||
const searchParams = useSearchParams();
|
||||
const id = searchParams.get('id');
|
||||
|
||||
const [bookDoc, setBookDoc] = React.useState<BookDoc>();
|
||||
const { envConfig } = useEnv();
|
||||
const { library, books, fetchBook, setLibrary } = useReaderStore();
|
||||
const defaultBookState = { loading: true, error: null, file: null, book: null, config: null };
|
||||
const bookState = id ? books[id] || defaultBookState : defaultBookState;
|
||||
|
||||
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.initAppService().then((appService) => {
|
||||
config.lastUpdated = Date.now();
|
||||
appService.saveBookConfig(book, config);
|
||||
appService.saveLibraryBooks(library);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (id && !file) {
|
||||
envConfig.initAppService().then((appService) => {
|
||||
fetchBook(appService, id);
|
||||
});
|
||||
}
|
||||
if (!bookDoc && bookState.file) {
|
||||
const loadDocument = async () => {
|
||||
if (file) {
|
||||
const { book } = await new DocumentLoader(file).open();
|
||||
setBookDoc(book);
|
||||
}
|
||||
};
|
||||
loadDocument();
|
||||
}
|
||||
return;
|
||||
}, [isClosingBook, bookState.file, envConfig, fetchBook, id]);
|
||||
|
||||
if (!id || !bookDoc || !bookState.config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{bookState.loading && <Spinner loading={bookState.loading} />}
|
||||
{bookState.error && (
|
||||
<div className='text-center'>
|
||||
<h2 className='text-red-500'>{bookState.error}</h2>
|
||||
</div>
|
||||
)}
|
||||
<FoliateViewer bookId={id} bookDoc={bookDoc} bookConfig={bookState.config} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReaderContent;
|
||||
Reference in New Issue
Block a user