forked from akai/readest
180 lines
5.8 KiB
TypeScript
180 lines
5.8 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react';
|
|
import { useFoliateEvents } from '../hooks/useFoliateEvents';
|
|
import { BookDoc } from '@/libs/document';
|
|
import { BookConfig, BookNote } from '@/types/book';
|
|
import { useReaderStore } from '@/store/readerStore';
|
|
import { getStyles } from '@/utils/style';
|
|
import { ONE_COLUMN_MAX_INLINE_SIZE } from '@/services/constants';
|
|
|
|
export interface FoliateView extends HTMLElement {
|
|
open: (book: BookDoc) => Promise<void>;
|
|
close: () => void;
|
|
init: (options: { lastLocation: string }) => void;
|
|
goTo: (href: string) => void;
|
|
goToFraction: (fraction: number) => void;
|
|
goLeft: () => void;
|
|
goRight: () => void;
|
|
getCFI: (index: number, range: Range) => string;
|
|
addAnnotation: (note: BookNote, remove?: boolean) => { index: number; label: string };
|
|
renderer: {
|
|
setStyles?: (css: string) => void;
|
|
setAttribute: (name: string, value: string | number) => void;
|
|
removeAttribute: (name: string) => void;
|
|
next: () => Promise<void>;
|
|
prev: () => Promise<void>;
|
|
};
|
|
}
|
|
|
|
const wrappedFoliateView = (originalView: FoliateView): FoliateView => {
|
|
const originalAddAnnotation = originalView.addAnnotation.bind(originalView);
|
|
originalView.addAnnotation = (note: BookNote, remove = false) => {
|
|
// transform BookNote to foliate annotation
|
|
const annotation = {
|
|
...note,
|
|
value: note.cfi,
|
|
};
|
|
return originalAddAnnotation(annotation, remove);
|
|
};
|
|
return originalView;
|
|
};
|
|
|
|
const FoliateViewer: React.FC<{
|
|
bookKey: string;
|
|
bookDoc: BookDoc;
|
|
bookConfig: BookConfig;
|
|
}> = ({ bookKey, bookDoc, bookConfig }) => {
|
|
const viewRef = useRef<HTMLDivElement>(null);
|
|
const [view, setView] = useState<FoliateView | null>(null);
|
|
const [viewInited, setViewInited] = useState(false);
|
|
const isViewCreated = useRef(false);
|
|
const { setFoliateView } = useReaderStore();
|
|
const setProgress = useReaderStore((state) => state.setProgress);
|
|
|
|
const progressRelocateHandler = (event: Event) => {
|
|
const detail = (event as CustomEvent).detail;
|
|
// console.log('relocate:', detail);
|
|
setProgress(bookKey, detail.cfi, detail.tocItem, detail.section, detail.location, detail.range);
|
|
};
|
|
|
|
const handleKeydown = (event: KeyboardEvent) => {
|
|
if (event.key === 'Backspace') {
|
|
event.preventDefault();
|
|
}
|
|
window.postMessage(
|
|
{
|
|
type: 'iframe-keydown',
|
|
key: event.key,
|
|
code: event.code,
|
|
ctrlKey: event.ctrlKey,
|
|
shiftKey: event.shiftKey,
|
|
altKey: event.altKey,
|
|
metaKey: event.metaKey,
|
|
},
|
|
'*',
|
|
);
|
|
};
|
|
|
|
const handleMousedown = (event: MouseEvent) => {
|
|
window.postMessage(
|
|
{
|
|
type: 'iframe-mousedown',
|
|
button: event.button,
|
|
clientX: event.clientX,
|
|
clientY: event.clientY,
|
|
ctrlKey: event.ctrlKey,
|
|
shiftKey: event.shiftKey,
|
|
altKey: event.altKey,
|
|
metaKey: event.metaKey,
|
|
},
|
|
'*',
|
|
);
|
|
};
|
|
|
|
const docLoadHandler = (event: Event) => {
|
|
const detail = (event as CustomEvent).detail;
|
|
if (detail.doc) {
|
|
if (!detail.doc.isEventListenersAdded) {
|
|
detail.doc.isEventListenersAdded = true;
|
|
detail.doc.addEventListener('keydown', handleKeydown);
|
|
detail.doc.addEventListener('mousedown', handleMousedown);
|
|
}
|
|
}
|
|
};
|
|
|
|
useFoliateEvents(view, { onLoad: docLoadHandler, onRelocate: progressRelocateHandler });
|
|
|
|
useEffect(() => {
|
|
if (isViewCreated.current) return;
|
|
const openBook = async () => {
|
|
console.log('Opening book', bookKey);
|
|
await import('foliate-js/view.js');
|
|
const view = wrappedFoliateView(document.createElement('foliate-view') as FoliateView);
|
|
document.body.append(view);
|
|
viewRef.current?.appendChild(view);
|
|
setView(view);
|
|
setFoliateView(bookKey, view);
|
|
|
|
await view.open(bookDoc);
|
|
const viewSettings = bookConfig.viewSettings!;
|
|
view.renderer.setStyles?.(getStyles(bookConfig));
|
|
const isScrolled = viewSettings.scrolled!;
|
|
const marginPx = viewSettings.marginPx!;
|
|
const gapPercent = viewSettings.gapPercent!;
|
|
const animated = viewSettings.animated!;
|
|
const maxColumnCount = viewSettings.maxColumnCount!;
|
|
const maxInlineSize =
|
|
maxColumnCount === 1 ? ONE_COLUMN_MAX_INLINE_SIZE : viewSettings.maxInlineSize!;
|
|
const maxBlockSize = viewSettings.maxBlockSize!;
|
|
if (animated) {
|
|
view.renderer.setAttribute('animated', '');
|
|
} else {
|
|
view.renderer.removeAttribute('animated');
|
|
}
|
|
view.renderer.setAttribute('animated', animated ? 'animated' : '');
|
|
view.renderer.setAttribute('flow', isScrolled ? 'scrolled' : 'paginated');
|
|
view.renderer.setAttribute('margin', `${marginPx}px`);
|
|
view.renderer.setAttribute('gap', `${gapPercent}%`);
|
|
view.renderer.setAttribute('max-column-count', maxColumnCount);
|
|
view.renderer.setAttribute('max-inline-size', `${maxInlineSize}px`);
|
|
view.renderer.setAttribute('max-block-size', `${maxBlockSize}px`);
|
|
|
|
const lastLocation = bookConfig.location;
|
|
if (lastLocation) {
|
|
await view.init({ lastLocation });
|
|
} else {
|
|
await view.goToFraction(0);
|
|
}
|
|
setViewInited(true);
|
|
};
|
|
|
|
openBook();
|
|
isViewCreated.current = true;
|
|
|
|
return () => {
|
|
console.log('Closing book', bookKey);
|
|
view?.close();
|
|
view?.remove();
|
|
};
|
|
}, []);
|
|
|
|
const initAnnotations = () => {
|
|
const { booknotes = [] } = bookConfig;
|
|
const annotations = booknotes.filter((item) => item.type === 'annotation');
|
|
try {
|
|
Promise.all(annotations.map((annotation) => view?.addAnnotation(annotation)));
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (viewInited) {
|
|
initAnnotations();
|
|
}
|
|
}, [viewInited]);
|
|
|
|
return <div className='foliate-viewer h-[100%] w-[100%]' ref={viewRef} />;
|
|
};
|
|
|
|
export default FoliateViewer;
|