Merge pull request #18895 from overleaf/dp-presentation-mode

Add pdf presentation mode

GitOrigin-RevId: e6ac1ae339e9690a733a110c6f0a33149e869dd6
This commit is contained in:
David 2024-06-14 13:14:23 +01:00 committed by Copybot
parent 805ec8c2e0
commit dcb7944b05
8 changed files with 173 additions and 8 deletions

View file

@ -84,7 +84,6 @@ const httpPermissionsPolicy = {
'camera',
'display-capture',
'encrypted-media',
'fullscreen',
'gamepad',
'geolocation',
'gyroscope',
@ -107,6 +106,7 @@ const httpPermissionsPolicy = {
],
allowed: {
autoplay: 'self "https://videos.ctfassets.net"',
fullscreen: 'self',
},
}

View file

@ -935,6 +935,7 @@
"postal_code": "",
"premium_feature": "",
"premium_plan_label": "",
"present": "",
"previous_page": "",
"price": "",
"primarily_work_study_question": "",
@ -1635,6 +1636,7 @@
"youve_unlinked_all_users": "",
"zoom_in": "",
"zoom_out": "",
"zoom_to": "",
"zotero_cta": "",
"zotero_groups_loading_error": "",
"zotero_groups_relink": "",

View file

@ -15,6 +15,7 @@ import { getPdfCachingMetrics } from '../util/metrics'
import { debugConsole } from '@/utils/debugging'
import { usePdfPreviewContext } from '@/features/pdf-preview/components/pdf-preview-provider'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import usePresentationMode from '../hooks/use-presentation-mode'
type PdfJsViewerProps = {
url: string
@ -48,13 +49,17 @@ function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) {
const [initialised, setInitialised] = useState(false)
const handlePageChange = useCallback(
newPage => {
(newPage: number) => {
if (!totalPages || newPage < 1 || newPage > totalPages) {
return
}
setPage(newPage)
if (pdfJsWrapper?.viewer) {
pdfJsWrapper.viewer.currentPageNumber = newPage
}
},
[pdfJsWrapper, setPage]
[pdfJsWrapper, setPage, totalPages]
)
// create the viewer when the container is mounted
@ -461,6 +466,14 @@ function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) {
[initialised, setZoom]
)
const requestPresentationMode = usePresentationMode(
pdfJsWrapper,
page,
handlePageChange,
scale,
setScale
)
// Don't render the toolbar until we have the necessary information
const toolbarInfoLoaded =
rawScale !== null && page !== null && totalPages !== null
@ -487,6 +500,7 @@ function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) {
{hasNewPdfToolbar ? (
toolbarInfoLoaded && (
<PdfViewerControlsToolbar
requestPresentationMode={requestPresentationMode}
setZoom={setZoom}
rawScale={rawScale}
setPage={handlePageChange}

View file

@ -9,6 +9,7 @@ import PdfViewerControlsMenuButton from './pdf-viewer-controls-menu-button'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
type PdfViewerControlsToolbarProps = {
requestPresentationMode: () => void
setZoom: (zoom: string) => void
rawScale: number
setPage: (page: number) => void
@ -17,6 +18,7 @@ type PdfViewerControlsToolbarProps = {
}
function PdfViewerControlsToolbar({
requestPresentationMode,
setZoom,
rawScale,
setPage,
@ -54,6 +56,7 @@ function PdfViewerControlsToolbar({
return createPortal(
<div className="pdfjs-viewer-controls" ref={pdfControlsRef}>
<InnerControlsComponent
requestPresentationMode={requestPresentationMode}
setZoom={setZoom}
rawScale={rawScale}
setPage={setPage}
@ -67,6 +70,7 @@ function PdfViewerControlsToolbar({
}
type InnerControlsProps = {
requestPresentationMode: () => void
setZoom: (zoom: string) => void
rawScale: number
setPage: (page: number) => void
@ -75,6 +79,7 @@ type InnerControlsProps = {
}
function PdfViewerControlsToolbarFull({
requestPresentationMode,
setZoom,
rawScale,
setPage,
@ -90,13 +95,18 @@ function PdfViewerControlsToolbarFull({
/>
<div className="pdfjs-zoom-controls">
<PdfZoomButtons setZoom={setZoom} />
<PdfZoomDropdown rawScale={rawScale} setZoom={setZoom} />
<PdfZoomDropdown
requestPresentationMode={requestPresentationMode}
rawScale={rawScale}
setZoom={setZoom}
/>
</div>
</>
)
}
function PdfViewerControlsToolbarSmall({
requestPresentationMode,
setZoom,
rawScale,
setPage,
@ -105,7 +115,11 @@ function PdfViewerControlsToolbarSmall({
}: InnerControlsProps) {
return (
<div className="pdfjs-viewer-controls-small">
<PdfZoomDropdown rawScale={rawScale} setZoom={setZoom} />
<PdfZoomDropdown
requestPresentationMode={requestPresentationMode}
rawScale={rawScale}
setZoom={setZoom}
/>
<PdfViewerControlsMenuButton
setZoom={setZoom}
setPage={setPage}

View file

@ -19,6 +19,7 @@ const shortcuts = isMac
}
type PdfZoomDropdownProps = {
requestPresentationMode: () => void
setZoom: (zoom: string) => void
rawScale: number
}
@ -29,7 +30,11 @@ const rawScaleToPercentage = (rawScale: number) => {
return `${Math.round(rawScale * 100)}%`
}
function PdfZoomDropdown({ setZoom, rawScale }: PdfZoomDropdownProps) {
function PdfZoomDropdown({
requestPresentationMode,
setZoom,
rawScale,
}: PdfZoomDropdownProps) {
const { t } = useTranslation()
const [customZoomValue, setCustomZoomValue] = useState<string>(
@ -44,7 +49,16 @@ function PdfZoomDropdown({ setZoom, rawScale }: PdfZoomDropdownProps) {
<ControlledDropdown
id="pdf-zoom-dropdown"
onSelect={eventKey => {
if (eventKey !== 'custom-zoom') setZoom(eventKey)
if (eventKey === 'custom-zoom') {
return
}
if (eventKey === 'present') {
requestPresentationMode()
return
}
setZoom(eventKey)
}}
pullRight
>
@ -102,7 +116,14 @@ function PdfZoomDropdown({ setZoom, rawScale }: PdfZoomDropdownProps) {
<MenuItem draggable={false} key="page-height" eventKey="page-height">
{t('fit_to_height')}
</MenuItem>
{document.fullscreenEnabled && <MenuItem divider />}
{document.fullscreenEnabled && (
<MenuItem draggable={false} key="present" eventKey="present">
{t('present')}
</MenuItem>
)}
<MenuItem divider />
<MenuItem header>{t('zoom_to')}</MenuItem>
{zoomValues.map(value => (
<MenuItem draggable={false} key={value} eventKey={value}>
{rawScaleToPercentage(Number(value))}

View file

@ -0,0 +1,112 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import PDFJSWrapper from '../util/pdf-js-wrapper'
type StoredPDFState = {
scrollMode?: number
spreadMode?: number
currentScaleValue?: string
}
export default function usePresentationMode(
pdfJsWrapper: PDFJSWrapper | null | undefined,
page: number | null,
handlePageChange: (page: number) => void,
scale: string,
setScale: (scale: string) => void
): () => void {
const storedState = useRef<StoredPDFState>({})
const [presentationMode, setPresentationMode] = useState(false)
const arrowKeyListener = useCallback(
event => {
if (page !== null) {
switch (event.key) {
case 'ArrowLeft':
case 'ArrowUp':
handlePageChange(page - 1)
break
case 'ArrowRight':
case 'ArrowDown':
handlePageChange(page + 1)
break
case ' ':
if (event.shiftKey) {
handlePageChange(page - 1)
} else {
handlePageChange(page + 1)
}
break
}
}
},
[page, handlePageChange]
)
useEffect(() => {
if (presentationMode) {
window.addEventListener('keydown', arrowKeyListener)
return () => {
window.removeEventListener('keydown', arrowKeyListener)
}
}
}, [presentationMode, arrowKeyListener])
const requestPresentationMode = useCallback(() => {
if (pdfJsWrapper) {
pdfJsWrapper.container.parentNode.requestFullscreen()
}
}, [pdfJsWrapper])
const handleEnterFullscreen = useCallback(() => {
if (pdfJsWrapper) {
storedState.current.scrollMode = pdfJsWrapper.viewer.scrollMode
storedState.current.spreadMode = pdfJsWrapper.viewer.spreadMode
storedState.current.currentScaleValue = scale
setScale('page-fit')
pdfJsWrapper.viewer.scrollMode = 3 // page
pdfJsWrapper.viewer.spreadMode = 0 // none
setPresentationMode(true)
}
}, [pdfJsWrapper, setScale, scale])
const handleExitFullscreen = useCallback(() => {
if (pdfJsWrapper) {
pdfJsWrapper.viewer.scrollMode = storedState.current.scrollMode
pdfJsWrapper.viewer.spreadMode = storedState.current.spreadMode
if (storedState.current.currentScaleValue !== undefined) {
setScale(storedState.current.currentScaleValue)
}
setPresentationMode(false)
}
}, [pdfJsWrapper, setScale])
const handleFullscreenChange = useCallback(() => {
if (pdfJsWrapper) {
const fullscreen =
document.fullscreenElement === pdfJsWrapper.container.parentNode
if (fullscreen) {
handleEnterFullscreen()
} else {
handleExitFullscreen()
}
}
}, [pdfJsWrapper, handleEnterFullscreen, handleExitFullscreen])
useEffect(() => {
window.addEventListener('fullscreenchange', handleFullscreenChange)
return () => {
window.removeEventListener('fullscreenchange', handleFullscreenChange)
}
}, [handleFullscreenChange])
return requestPresentationMode
}

View file

@ -1383,6 +1383,7 @@
"premium_feature": "Premium feature",
"premium_features": "Premium features",
"premium_plan_label": "Youre using <b>Overleaf Premium</b>",
"present": "Present",
"presentation": "Presentation",
"press_and_awards": "Press &amp; awards",
"previous_page": "Previous page",
@ -2276,6 +2277,7 @@
"zip_contents_too_large": "Zip contents too large",
"zoom_in": "Zoom in",
"zoom_out": "Zoom out",
"zoom_to": "Zoom to",
"zotero": "Zotero",
"zotero_and_mendeley_integrations": "<0>Zotero</0> and <0>Mendeley</0> integrations",
"zotero_cta": "Get Zotero integration",

View file

@ -9,7 +9,7 @@ describe('HttpPermissionsPolicy', function () {
const response = await fetch(BASE_URL)
expect(response.headers.get('permissions-policy')).to.equal(
'accelerometer=(), attribution-reporting=(), browsing-topics=(), camera=(), display-capture=(), encrypted-media=(), fullscreen=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), identity-credentials-get=(), idle-detection=(), local-fonts=(), magnetometer=(), microphone=(), midi=(), otp-credentials=(), payment=(), picture-in-picture=(), screen-wake-lock=(), serial=(), storage-access=(), usb=(), window-management=(), xr-spatial-tracking=(), autoplay=(self "https://videos.ctfassets.net")'
'accelerometer=(), attribution-reporting=(), browsing-topics=(), camera=(), display-capture=(), encrypted-media=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), identity-credentials-get=(), idle-detection=(), local-fonts=(), magnetometer=(), microphone=(), midi=(), otp-credentials=(), payment=(), picture-in-picture=(), screen-wake-lock=(), serial=(), storage-access=(), usb=(), window-management=(), xr-spatial-tracking=(), autoplay=(self "https://videos.ctfassets.net"), fullscreen=(self)'
)
})