use-presentation-mode.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import { useCallback, useEffect, useRef, useState } from 'react'
  2. import PDFJSWrapper from '../util/pdf-js-wrapper'
  3. import { sendMB } from '@/infrastructure/event-tracking'
  4. import { debugConsole } from '@/utils/debugging'
  5. type StoredPDFState = {
  6. scrollMode?: number
  7. spreadMode?: number
  8. currentScaleValue?: string
  9. }
  10. export default function usePresentationMode(
  11. pdfJsWrapper: PDFJSWrapper | null | undefined,
  12. page: number | null,
  13. handlePageChange: (page: number) => void,
  14. scale: string,
  15. setScale: (scale: string) => void
  16. ): () => void {
  17. const storedState = useRef<StoredPDFState>({})
  18. const [presentationMode, setPresentationMode] = useState(false)
  19. const nextPage = useCallback(() => {
  20. if (page !== null) {
  21. handlePageChange(page + 1)
  22. }
  23. }, [handlePageChange, page])
  24. const previousPage = useCallback(() => {
  25. if (page !== null) {
  26. handlePageChange(page - 1)
  27. }
  28. }, [handlePageChange, page])
  29. const clickListener = useCallback(
  30. (event: MouseEvent) => {
  31. if ((event.target as HTMLElement).tagName === 'A') {
  32. return
  33. }
  34. if (event.shiftKey) {
  35. previousPage()
  36. } else {
  37. nextPage()
  38. }
  39. },
  40. [nextPage, previousPage]
  41. )
  42. const arrowKeyListener = useCallback(
  43. (event: KeyboardEvent) => {
  44. switch (event.key) {
  45. case 'ArrowLeft':
  46. case 'ArrowUp':
  47. case 'PageUp':
  48. case 'Backspace':
  49. previousPage()
  50. break
  51. case 'ArrowRight':
  52. case 'ArrowDown':
  53. case 'PageDown':
  54. nextPage()
  55. break
  56. case ' ':
  57. if (event.shiftKey) {
  58. previousPage()
  59. } else {
  60. nextPage()
  61. }
  62. break
  63. }
  64. },
  65. [nextPage, previousPage]
  66. )
  67. const isMouseWheelScrollingRef = useRef(false)
  68. const mouseWheelListener = useCallback(
  69. (event: WheelEvent) => {
  70. if (
  71. !isMouseWheelScrollingRef.current &&
  72. !event.ctrlKey // Avoid trackpad pinching
  73. ) {
  74. isMouseWheelScrollingRef.current = true
  75. if (event.deltaY > 0) {
  76. nextPage()
  77. } else {
  78. previousPage()
  79. }
  80. setTimeout(() => {
  81. isMouseWheelScrollingRef.current = false
  82. }, 200)
  83. }
  84. },
  85. [nextPage, previousPage]
  86. )
  87. useEffect(() => {
  88. if (presentationMode) {
  89. window.addEventListener('keydown', arrowKeyListener)
  90. window.addEventListener('click', clickListener)
  91. window.addEventListener('wheel', mouseWheelListener)
  92. return () => {
  93. window.removeEventListener('keydown', arrowKeyListener)
  94. window.removeEventListener('click', clickListener)
  95. window.removeEventListener('wheel', mouseWheelListener)
  96. }
  97. }
  98. }, [presentationMode, arrowKeyListener, clickListener, mouseWheelListener])
  99. const requestPresentationMode = useCallback(() => {
  100. sendMB('pdf-viewer-enter-presentation-mode')
  101. if (pdfJsWrapper) {
  102. pdfJsWrapper.container.parentElement
  103. ?.requestFullscreen()
  104. .catch(debugConsole.error)
  105. }
  106. }, [pdfJsWrapper])
  107. const handleEnterFullscreen = useCallback(() => {
  108. if (pdfJsWrapper) {
  109. storedState.current.scrollMode = pdfJsWrapper.viewer.scrollMode
  110. storedState.current.spreadMode = pdfJsWrapper.viewer.spreadMode
  111. storedState.current.currentScaleValue = scale
  112. setScale('page-fit')
  113. pdfJsWrapper.viewer.scrollMode = 3 // page
  114. pdfJsWrapper.viewer.spreadMode = 0 // none
  115. pdfJsWrapper.fetchAllData()
  116. setPresentationMode(true)
  117. }
  118. }, [pdfJsWrapper, setScale, scale])
  119. const handleExitFullscreen = useCallback(() => {
  120. if (pdfJsWrapper) {
  121. pdfJsWrapper.viewer.scrollMode = storedState.current.scrollMode!
  122. pdfJsWrapper.viewer.spreadMode = storedState.current.spreadMode!
  123. if (storedState.current.currentScaleValue !== undefined) {
  124. setScale(storedState.current.currentScaleValue)
  125. }
  126. setPresentationMode(false)
  127. }
  128. }, [pdfJsWrapper, setScale])
  129. const handleFullscreenChange = useCallback(() => {
  130. if (pdfJsWrapper) {
  131. const fullscreen =
  132. document.fullscreenElement === pdfJsWrapper.container.parentNode
  133. if (fullscreen) {
  134. handleEnterFullscreen()
  135. } else {
  136. handleExitFullscreen()
  137. }
  138. }
  139. }, [pdfJsWrapper, handleEnterFullscreen, handleExitFullscreen])
  140. useEffect(() => {
  141. window.addEventListener('fullscreenchange', handleFullscreenChange)
  142. return () => {
  143. window.removeEventListener('fullscreenchange', handleFullscreenChange)
  144. }
  145. }, [handleFullscreenChange])
  146. return requestPresentationMode
  147. }