From d71a912442cc1f5254606c11159ee69868fe09aa Mon Sep 17 00:00:00 2001 From: chrox Date: Thu, 7 Nov 2024 17:07:04 +0100 Subject: [PATCH] Draw highlights in bookview --- .../src/app/reader/components/BookGrid.tsx | 2 +- .../app/reader/components/FoliateViewer.tsx | 40 +++++++++-- .../src/app/reader/components/Ribbon.tsx | 6 +- .../reader/components/annotator/Annotator.tsx | 72 ++++++++++++++++--- .../components/annotator/HighlightOptions.tsx | 6 +- .../src/app/reader/hooks/useFoliateEvents.ts | 17 ++++- apps/readest-app/src/store/readerStore.ts | 6 +- 7 files changed, 128 insertions(+), 21 deletions(-) diff --git a/apps/readest-app/src/app/reader/components/BookGrid.tsx b/apps/readest-app/src/app/reader/components/BookGrid.tsx index d9abf7e5..93582d9e 100644 --- a/apps/readest-app/src/app/reader/components/BookGrid.tsx +++ b/apps/readest-app/src/app/reader/components/BookGrid.tsx @@ -39,7 +39,7 @@ const BookGrid: React.FC = ({ 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 (
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(null); const [view, setView] = useState(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
; }; diff --git a/apps/readest-app/src/app/reader/components/Ribbon.tsx b/apps/readest-app/src/app/reader/components/Ribbon.tsx index 5d014d76..29b630c7 100644 --- a/apps/readest-app/src/app/reader/components/Ribbon.tsx +++ b/apps/readest-app/src/app/reader/components/Ribbon.tsx @@ -1,3 +1,4 @@ +import clsx from 'clsx'; import React from 'react'; interface RibbonProps { @@ -6,7 +7,10 @@ interface RibbonProps { const Ribbon: React.FC = ({ width }) => { return ( -
+
= ({ bookKey }) => { const bookState = books[bookKey]!; const config = bookState.config!; const progress = bookState.progress!; + const view = getFoliateView(bookKey); const [selection, setSelection] = useState(); 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(() => { 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 }, diff --git a/apps/readest-app/src/app/reader/components/annotator/HighlightOptions.tsx b/apps/readest-app/src/app/reader/components/annotator/HighlightOptions.tsx index 1b95ffef..2565df9a 100644 --- a/apps/readest-app/src/app/reader/components/annotator/HighlightOptions.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/HighlightOptions.tsx @@ -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 = ({ style, onHandleHighlight }) => { @@ -20,13 +20,13 @@ const HighlightOptions: React.FC = ({ 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]; diff --git a/apps/readest-app/src/app/reader/hooks/useFoliateEvents.ts b/apps/readest-app/src/app/reader/hooks/useFoliateEvents.ts index bcb1c35d..a893fe5a 100644 --- a/apps/readest-app/src/app/reader/hooks/useFoliateEvents.ts +++ b/apps/readest-app/src/app/reader/hooks/useFoliateEvents.ts @@ -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]); }; diff --git a/apps/readest-app/src/store/readerStore.ts b/apps/readest-app/src/store/readerStore.ts index 895314fc..6eb2af9e 100644 --- a/apps/readest-app/src/store/readerStore.ts +++ b/apps/readest-app/src/store/readerStore.ts @@ -325,7 +325,11 @@ export const useReaderStore = create((set, get) => ({ ...state.books, [key]: { ...book, - config: updatedConfig, + config: { + ...book.config, + lastUpdated: Date.now(), + booknotes, + }, }, }, };