Files
omnivore/packages/web/components/templates/article/EpubContainer.tsx
2024-02-14 17:12:28 +08:00

365 lines
11 KiB
TypeScript

import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
import { Box, VStack } from '../../elements/LayoutPrimitives'
import { v4 as uuidv4 } from 'uuid'
import { nanoid } from 'nanoid'
import { useState, useEffect, useRef, useMemo } from 'react'
import {
getCurrentLocalTheme,
getTheme,
isDarkTheme,
} from '../../../lib/themeUpdater'
import PSPDFKit from 'pspdfkit'
import { Instance, HighlightAnnotation, List, Annotation, Rect } from 'pspdfkit'
import type { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import { createHighlightMutation } from '../../../lib/networking/mutations/createHighlightMutation'
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
import { articleReadingProgressMutation } from '../../../lib/networking/mutations/articleReadingProgressMutation'
import { mergeHighlightMutation } from '../../../lib/networking/mutations/mergeHighlightMutation'
import { useCanShareNative } from '../../../lib/hooks/useCanShareNative'
import { pspdfKitKey } from '../../../lib/appConfig'
import { NotebookModal } from './NotebookModal'
import { HighlightNoteModal } from './HighlightNoteModal'
import { showErrorToast } from '../../../lib/toastHelpers'
import { DEFAULT_HEADER_HEIGHT } from '../homeFeed/HeaderSpacer'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import Epub, { EpubCFI } from 'epubjs'
import { Rendition, Contents } from 'epubjs/types'
import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts'
import { theme } from '../../tokens/stitches.config'
export type EpubContainerProps = {
viewer: UserBasicData
article: ArticleAttributes
showHighlightsModal: boolean
setShowHighlightsModal: React.Dispatch<React.SetStateAction<boolean>>
}
type EpubPatch = {
cfi: string
base: string
}
export default function EpubContainer(props: EpubContainerProps): JSX.Element {
const epubRef = useRef<HTMLDivElement | null>(null)
const renditionRef = useRef<Rendition | undefined>(undefined)
const [shareTarget, setShareTarget] =
useState<Highlight | undefined>(undefined)
const [touchStart, setTouchStart] = useState(0)
const [notebookKey, setNotebookKey] = useState<string>(uuidv4())
const [noteTarget, setNoteTarget] = useState<Highlight | undefined>(undefined)
const [noteTargetPageIndex, setNoteTargetPageIndex] =
useState<number | undefined>(undefined)
const highlightsRef = useRef<Highlight[]>([])
const book = useMemo(() => {
console.log('loading book: ', props.article.url)
const book = Epub(props.article.url, {
openAs: 'epub',
})
book.loaded.navigation.then((nav) => {
console.log('navigated to: ', nav)
})
console.log('loaded book: ', book)
return book
}, [props.article])
useEffect(() => {
if (!epubRef.current || !book || !book.loaded) {
return
}
if (renditionRef.current) {
console.log('book already rendered')
return
}
const epubOptions = {}
const node = epubRef.current
renditionRef.current = book.renderTo(node, {
width: '100%',
height: '100%',
})
renditionRef.current?.on('touchstart', (event: TouchEvent) => {
setTouchStart(event.changedTouches[0].screenX)
})
renditionRef.current?.on('touchend', (event: TouchEvent) => {
console.log('touchend: ', event)
const _touchEnd = event.changedTouches[0].screenX
if (touchStart < _touchEnd) {
renditionRef.current?.next()
}
if (touchStart > _touchEnd) {
renditionRef.current?.prev()
}
})
renditionRef.current?.on('keydown', (ev: KeyboardEvent) => {
if (ev.key == 'ArrowRight') {
renditionRef.current?.next()
} else if (ev.key == 'ArrowLeft') {
renditionRef.current?.prev()
}
})
const themeId = getCurrentLocalTheme()
if (themeId) {
const readerTheme = getTheme(themeId)
renditionRef.current.themes.override(
'color',
readerTheme.colors.readerFont.value,
true
)
renditionRef.current.themes.override(
'background',
readerTheme.colors.readerBg.value,
true
)
renditionRef.current.themes.override(
'backgroundColor',
readerTheme.colors.readerBg.value,
true
)
renditionRef.current.themes.default({
'a:hover': {
color: 'unset !important',
},
})
}
renditionRef.current.display()
// }
}, [book, epubRef, renditionRef])
/*
useEffect(() => {
async function setRenderSelection(cfirange: string, contents: Contents) {
if (!renditionRef.current) {
return
}
console.log('contents of path: ', contents.content.innerHTML)
const ranges = await Promise.all(
highlightsRef.current
.map((highlight) => highlight.patch)
.map((patch) => JSON.parse(patch) as EpubPatch)
.map((patch) => book.getRange(patch.cfi))
)
// console.log('cfis: ', cfis)
// const ranges = cfis.map((cfi) => cfi.toRange(document))
console.log(
'ranges: ',
ranges,
ranges.map((range) => range.getBoundingClientRect())
)
// setSelections(
// selections.concat({
// text: renditionRef.current.getRange(cfiRange).toString(),
// cfiRange,
// })
// )
function rectsOverlap(l1: DOMRect, l2: DOMRect): boolean {
console.log('checking overlap: ', l1, l2)
return (
l1.left < l2.right &&
l1.right > l2.left &&
l1.top > l2.bottom &&
l1.bottom < l2.top
)
}
const highlightCfi = await book.getRange(cfirange)
console.log(
'highlightCfi',
highlightCfi,
highlightCfi.getBoundingClientRect()
)
const selectionRange = contents.window.getSelection()?.getRangeAt(0)
console.log('selectionRange: ', selectionRange)
let overlap = false
if (selectionRange) {
const highlightRect = selectionRange.getClientRects()
ranges.forEach((range) => {
const rects = range.getClientRects()
console.log(' existing rects: ', rects)
console.log(' highlightRects: ', highlightRect)
for (let ei = 0; ei < rects.length; ei++) {
const check = rects.item(ei)
if (!check) {
continue
}
for (let ni = 0; ni < rects.length; ni++) {
const newRect = highlightRect.item(ni)
if (!newRect) {
continue
}
if (rectsOverlap(newRect, check)) {
overlap = true
}
}
}
})
}
console.log('found an overlap: ', overlap)
const highlightId = uuidv4()
const highlight = await createHighlightMutation({
id: highlightId,
shortId: nanoid(8),
type: 'HIGHLIGHT',
patch: JSON.stringify({
base: renditionRef.current.epubcfi,
cfi: cfirange,
}),
articleId: props.article.id,
})
if (!highlight) {
showErrorToast('Unable to create highlight')
return
}
highlightsRef.current.push(highlight)
renditionRef.current.annotations.highlight(
cfirange,
{
omnivoreHighlight: highlight,
},
undefined,
'hl',
{
fill: theme.colors.highlightBackground.toString(),
'fill-opacity': '0.3',
'mix-blend-mode': 'multiply',
}
)
contents.window.getSelection()?.removeAllRanges()
}
renditionRef.current?.on('selected', setRenderSelection)
return () => {
renditionRef.current?.off('selected', setRenderSelection)
}
}, [renditionRef])
*/
useEffect(() => {
const keyDown = (ev: KeyboardEvent) => {
console.log('keydown: ', ev.key)
if (ev.key == 'ArrowRight') {
renditionRef.current?.next()
} else if (ev.key == 'ArrowLeft') {
renditionRef.current?.prev()
}
}
document.addEventListener('keydown', keyDown)
return () => {
document.removeEventListener('keydown', keyDown)
}
})
return (
<Box
css={{
padding: '120px',
paddingTop: '10px',
paddingBottom: '0px',
// minHeight: '100vh',
'@sm': {
'--blockquote-padding': '1em 2em',
'--blockquote-icon-font-size': '1.7rem',
'--figure-margin': '2.6875rem auto',
'--hr-margin': '2em',
margin: `0px 0px`,
},
// '@md': {
// maxWidth: styles.maxWidthPercentage
// ? `${styles.maxWidthPercentage}%`
// : 1024 - styles.margin,
// },
'@mdDown': {
padding: '0px',
paddingBottom: '0px',
},
width: '100%',
height: `calc(100vh - ${DEFAULT_HEADER_HEIGHT})`,
}}
>
<Box
ref={epubRef}
css={{
width: '100%',
height: '100%',
}}
>
{/* EPUB CONTAINER
<div ></div> */}
</Box>
{noteTarget && (
<HighlightNoteModal
highlight={noteTarget}
libraryItemId={props.article.id}
author={props.article.author ?? ''}
title={props.article.title}
onUpdate={(highlight: Highlight) => {
const savedHighlight = highlightsRef.current.find(
(other: Highlight) => {
return other.id == highlight.id
}
)
if (savedHighlight) {
savedHighlight.annotation = highlight.annotation
}
}}
onOpenChange={() => {
setNoteTarget(undefined)
}}
/>
)}
{props.showHighlightsModal && (
<NotebookModal
key={notebookKey}
viewer={props.viewer}
item={props.article}
onClose={(updatedHighlights, deletedAnnotations) => {
console.log(
'closed PDF notebook: ',
updatedHighlights,
deletedAnnotations
)
deletedAnnotations.forEach((highlight) => {
const event = new CustomEvent('deleteHighlightbyId', {
detail: highlight.id,
})
document.dispatchEvent(event)
})
props.setShowHighlightsModal(false)
}}
viewHighlightInReader={(highlightId) => {
const event = new CustomEvent('scrollToHighlightId', {
detail: highlightId,
})
document.dispatchEvent(event)
props.setShowHighlightsModal(false)
}}
/>
)}
</Box>
)
}