Draw highlights in bookview

This commit is contained in:
chrox
2024-11-07 17:07:04 +01:00
parent 490038e63d
commit d71a912442
7 changed files with 128 additions and 21 deletions
@@ -39,7 +39,7 @@ const BookGrid: React.FC<BookGridProps> = ({ bookKeys, bookStates, onCloseBook }
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!}%` : '';
const marginGap = `${config.viewSettings!.gapPercent}%`;
return (
<div
@@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { useFoliateEvents } from '../hooks/useFoliateEvents';
import { BookDoc } from '@/libs/document';
import { BookConfig } from '@/types/book';
import { BookConfig, BookNote } from '@/types/book';
import { useReaderStore } from '@/store/readerStore';
import { getStyles } from '@/utils/style';
@@ -14,6 +14,7 @@ export interface FoliateView extends HTMLElement {
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;
@@ -23,6 +24,19 @@ export interface FoliateView extends HTMLElement {
};
}
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;
@@ -30,6 +44,7 @@ const FoliateViewer: React.FC<{
}> = ({ 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);
@@ -92,7 +107,7 @@ const FoliateViewer: React.FC<{
const openBook = async () => {
console.log('Opening book', bookKey);
await import('foliate-js/view.js');
const view = document.createElement('foliate-view') as FoliateView;
const view = wrappedFoliateView(document.createElement('foliate-view') as FoliateView);
document.body.append(view);
viewRef.current?.appendChild(view);
setView(view);
@@ -123,10 +138,11 @@ const FoliateViewer: React.FC<{
const lastLocation = bookConfig.location;
if (lastLocation) {
view.init({ lastLocation });
await view.init({ lastLocation });
} else {
view.goToFraction(0);
await view.goToFraction(0);
}
setViewInited(true);
};
openBook();
@@ -139,6 +155,22 @@ const FoliateViewer: React.FC<{
};
}, []);
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} />;
};
@@ -1,3 +1,4 @@
import clsx from 'clsx';
import React from 'react';
interface RibbonProps {
@@ -6,7 +7,10 @@ interface RibbonProps {
const Ribbon: React.FC<RibbonProps> = ({ width }) => {
return (
<div className='absolute inset-0 z-10 flex h-11 justify-center' style={{ width }}>
<div
className={clsx('absolute inset-0 z-40 flex h-11 justify-center')}
style={{ width: width }}
>
<svg
stroke='currentColor'
fill='currentColor'
@@ -4,6 +4,7 @@ import { FiCopy } from 'react-icons/fi';
import { PiHighlighterFill } from 'react-icons/pi';
import { FaWikipediaW } from 'react-icons/fa';
import { BsPencilSquare } from 'react-icons/bs';
import { RiDeleteBinFill } from 'react-icons/ri';
import { useEnv } from '@/context/EnvContext';
import { BookNote } from '@/types/book';
@@ -13,11 +14,12 @@ import { getPosition, Position } from '@/utils/sel';
import useOutsideClick from '@/hooks/useOutsideClick';
import PopupButton from './PopupButton';
import HighlightOptions from './HighlightOptions';
import { Overlayer } from 'foliate-js/overlayer.js';
interface TextSelection {
annotated?: boolean;
text: string;
range: Range;
sel: Selection;
index: number;
}
@@ -28,6 +30,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const bookState = books[bookKey]!;
const config = bookState.config!;
const progress = bookState.progress!;
const view = getFoliateView(bookKey);
const [selection, setSelection] = useState<TextSelection | null>();
const [showPopup, setShowPopup] = useState(false);
@@ -50,7 +53,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const handlePointerup = () => {
const sel = doc.getSelection();
if (sel && sel.toString().trim().length > 0) {
setSelection({ text: sel.toString(), range: sel.getRangeAt(0), sel, index });
setSelection({ text: sel.toString(), range: sel.getRangeAt(0), index });
}
};
if (detail.doc) {
@@ -58,14 +61,54 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
}
};
const drawAnnotationHandler = (event: Event) => {
const detail = (event as CustomEvent).detail;
const { draw, annotation, doc, range } = detail;
const { style, color } = annotation as BookNote;
if (style === 'highlight') {
draw(Overlayer.highlight, { color });
} else if (['underline', 'squiggly'].includes(style as string)) {
const { defaultView } = doc;
const node = range.startContainer;
const el = node.nodeType === 1 ? node : node.parentElement;
const { writingMode } = defaultView.getComputedStyle(el);
draw(Overlayer[style as keyof typeof Overlayer], { writingMode, color });
}
};
const showAnnotationHandler = (event: Event) => {
const detail = (event as CustomEvent).detail;
const { value: cfi, index, range } = detail;
const { booknotes = [] } = config;
const annotations = booknotes.filter((booknote) => booknote.type === 'annotation');
const annotation = annotations.find((annotation) => annotation.cfi === cfi);
if (!annotation) return;
const selection = {
annotated: true,
text: annotation.text,
range,
index,
};
setSelection(selection as TextSelection);
};
useFoliateEvents(
view,
{
onLoad: docLoadHandler,
onDrawAnnotation: drawAnnotationHandler,
onShowAnnotation: showAnnotationHandler,
},
[bookState],
);
const popupRef = useOutsideClick<HTMLDivElement>(() => {
setShowPopup(false);
setSelection(null);
});
useFoliateEvents(getFoliateView(bookKey), { onLoad: docLoadHandler });
useEffect(() => {
setHighlightOptionsVisible(false);
setHighlightOptionsVisible(!!(selection && selection.annotated));
if (selection && selection.text.trim().length > 0) {
const gridFrame = document.querySelector(`#gridcell-${bookKey}`);
if (!gridFrame) return;
@@ -119,12 +162,12 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
if (selection) navigator.clipboard.writeText(selection.text);
};
const handleHighlight = () => {
const handleHighlight = (update = false) => {
if (!selection || !selection.text) return;
setHighlightOptionsVisible(true);
const { booknotes: annotations = [] } = config;
const { tocHref: href } = progress;
const cfi = getFoliateView(bookKey)?.getCFI(selection.index, selection.range);
const cfi = view?.getCFI(selection.index, selection.range);
if (!cfi) return;
const annotation: BookNote = {
type: 'annotation',
@@ -140,9 +183,17 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
(annotation) => annotation.cfi === cfi && annotation.type === 'annotation',
);
if (existingIndex !== -1) {
annotations[existingIndex] = annotation;
view?.addAnnotation(annotation, true);
if (update) {
annotations[existingIndex] = annotation;
view?.addAnnotation(annotation);
} else {
annotations.splice(existingIndex, 1);
setShowPopup(false);
}
} else {
annotations.push(annotation);
view?.addAnnotation(annotation);
}
const dedupedAnnotations = Array.from(
@@ -160,9 +211,14 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const handleSearch = () => {};
const handleDictionary = () => {};
const selectionAnnotated = selection?.annotated;
const buttons = [
{ tooltipText: 'Copy', Icon: FiCopy, onClick: handleCopy },
{ tooltipText: 'Highlight', Icon: PiHighlighterFill, onClick: handleHighlight },
{
tooltipText: selectionAnnotated ? 'Delete Highlight' : 'Highlight',
Icon: selectionAnnotated ? RiDeleteBinFill : PiHighlighterFill,
onClick: handleHighlight,
},
{ tooltipText: 'Annotate', Icon: BsPencilSquare, onClick: handleAnnotate },
{ tooltipText: 'Search', Icon: FiSearch, onClick: handleSearch },
{ tooltipText: 'Dictionary', Icon: FaWikipediaW, onClick: handleDictionary },
@@ -10,7 +10,7 @@ const colors = ['red', 'violet', 'blue', 'green', 'yellow'] as HighlightColor[];
interface HighlightOptionsProps {
style: React.CSSProperties;
onHandleHighlight: () => void;
onHandleHighlight: (update: boolean) => void;
}
const HighlightOptions: React.FC<HighlightOptionsProps> = ({ style, onHandleHighlight }) => {
@@ -20,13 +20,13 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({ style, onHandleHigh
const handleSelectStyle = (style: HighlightStyle) => {
globalReadSettings.highlightStyle = style;
setSettings(settings);
onHandleHighlight();
onHandleHighlight(true);
};
const handleSelectColor = (color: HighlightColor) => {
const style = globalReadSettings.highlightStyle;
globalReadSettings.highlightStyles[style] = color;
setSettings(settings);
onHandleHighlight();
onHandleHighlight(true);
};
const selectedStyle = globalReadSettings.highlightStyle;
const selectedColor = globalReadSettings.highlightStyles[selectedStyle];
@@ -4,21 +4,32 @@ import { FoliateView } from '@/app/reader/components/FoliateViewer';
type FoliateEventHandler = {
onLoad?: (event: Event) => void;
onRelocate?: (event: Event) => void;
onDrawAnnotation?: (event: Event) => void;
onShowAnnotation?: (event: Event) => void;
};
export const useFoliateEvents = (view: FoliateView | null, handlers?: FoliateEventHandler) => {
export const useFoliateEvents = (
view: FoliateView | null,
handlers?: FoliateEventHandler,
dependencies: React.DependencyList = [],
) => {
const onLoad = handlers?.onLoad;
const onRelocate = handlers?.onRelocate;
const onDrawAnnotation = handlers?.onDrawAnnotation;
const onShowAnnotation = handlers?.onShowAnnotation;
useEffect(() => {
if (!view) return;
if (onLoad) view.addEventListener('load', onLoad);
if (onRelocate) view.addEventListener('relocate', onRelocate);
if (onDrawAnnotation) view.addEventListener('draw-annotation', onDrawAnnotation);
if (onShowAnnotation) view.addEventListener('show-annotation', onShowAnnotation);
return () => {
if (onLoad) view.removeEventListener('load', onLoad);
if (onRelocate) view.removeEventListener('relocate', onRelocate);
if (onDrawAnnotation) view.removeEventListener('draw-annotation', onDrawAnnotation);
if (onShowAnnotation) view.removeEventListener('show-annotation', onShowAnnotation);
};
}, [view, onLoad, onRelocate]);
}, [view, ...dependencies]);
};
+5 -1
View File
@@ -325,7 +325,11 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
...state.books,
[key]: {
...book,
config: updatedConfig,
config: {
...book.config,
lastUpdated: Date.now(),
booknotes,
},
},
},
};