You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

pdf-preview.tsx 3.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. import type { FC } from 'react'
  2. import { createPortal } from 'react-dom'
  3. import 'react-pdf-highlighter/dist/style.css'
  4. import { PdfHighlighter, PdfLoader } from 'react-pdf-highlighter'
  5. import { t } from 'i18next'
  6. import { RiCloseLine, RiZoomInLine, RiZoomOutLine } from '@remixicon/react'
  7. import React, { useState } from 'react'
  8. import { useHotkeys } from 'react-hotkeys-hook'
  9. import Loading from '@/app/components/base/loading'
  10. import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
  11. import Tooltip from '@/app/components/base/tooltip'
  12. import { noop } from 'lodash-es'
  13. type PdfPreviewProps = {
  14. url: string
  15. onCancel: () => void
  16. }
  17. const PdfPreview: FC<PdfPreviewProps> = ({
  18. url,
  19. onCancel,
  20. }) => {
  21. const media = useBreakpoints()
  22. const [scale, setScale] = useState(1)
  23. const [position, setPosition] = useState({ x: 0, y: 0 })
  24. const isMobile = media === MediaType.mobile
  25. const zoomIn = () => {
  26. setScale(prevScale => Math.min(prevScale * 1.2, 15))
  27. setPosition({ x: position.x - 50, y: position.y - 50 })
  28. }
  29. const zoomOut = () => {
  30. setScale((prevScale) => {
  31. const newScale = Math.max(prevScale / 1.2, 0.5)
  32. if (newScale === 1)
  33. setPosition({ x: 0, y: 0 })
  34. else
  35. setPosition({ x: position.x + 50, y: position.y + 50 })
  36. return newScale
  37. })
  38. }
  39. useHotkeys('esc', onCancel)
  40. useHotkeys('up', zoomIn)
  41. useHotkeys('down', zoomOut)
  42. return createPortal(
  43. <div
  44. className={`fixed inset-0 z-[1000] flex items-center justify-center bg-black/80 ${!isMobile && 'p-8'}`}
  45. onClick={e => e.stopPropagation()}
  46. tabIndex={-1}
  47. >
  48. <div
  49. className='h-[95vh] max-h-full w-[100vw] max-w-full overflow-hidden'
  50. style={{ transform: `scale(${scale})`, transformOrigin: 'center', scrollbarWidth: 'none', msOverflowStyle: 'none' }}
  51. >
  52. <PdfLoader
  53. workerSrc='/pdf.worker.min.mjs'
  54. url={url}
  55. beforeLoad={<div className='flex h-64 items-center justify-center'><Loading type='app' /></div>}
  56. >
  57. {(pdfDocument) => {
  58. return (
  59. <PdfHighlighter
  60. pdfDocument={pdfDocument}
  61. enableAreaSelection={event => event.altKey}
  62. scrollRef={noop}
  63. onScrollChange={noop}
  64. onSelectionFinished={() => null}
  65. highlightTransform={() => { return <div/> }}
  66. highlights={[]}
  67. />
  68. )
  69. }}
  70. </PdfLoader>
  71. </div>
  72. <Tooltip popupContent={t('common.operation.zoomOut')}>
  73. <div className='absolute right-24 top-6 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg'
  74. onClick={zoomOut}>
  75. <RiZoomOutLine className='h-4 w-4 text-gray-500'/>
  76. </div>
  77. </Tooltip>
  78. <Tooltip popupContent={t('common.operation.zoomIn')}>
  79. <div className='absolute right-16 top-6 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg'
  80. onClick={zoomIn}>
  81. <RiZoomInLine className='h-4 w-4 text-gray-500'/>
  82. </div>
  83. </Tooltip>
  84. <Tooltip popupContent={t('common.operation.cancel')}>
  85. <div
  86. className='absolute right-6 top-6 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-white/8 backdrop-blur-[2px]'
  87. onClick={onCancel}>
  88. <RiCloseLine className='h-4 w-4 text-gray-500'/>
  89. </div>
  90. </Tooltip>
  91. </div>,
  92. document.body,
  93. )
  94. }
  95. export default PdfPreview