pdf-synctex-controls.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. import classNames from 'classnames'
  2. import { memo, useCallback, useMemo } from 'react'
  3. import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
  4. import { useLayoutContext } from '../../../shared/context/layout-context'
  5. import { useTranslation } from 'react-i18next'
  6. import OLTooltip from '@/shared/components/ol/ol-tooltip'
  7. import OLButton from '@/shared/components/ol/ol-button'
  8. import MaterialIcon from '@/shared/components/material-icon'
  9. import { Placement } from 'react-bootstrap/types'
  10. import useSynctex from '../hooks/use-synctex'
  11. import { useFeatureFlag } from '@/shared/context/split-test-context'
  12. import OLSpinner from '@/shared/components/ol/ol-spinner'
  13. const GoToCodeButton = memo(function GoToCodeButton({
  14. syncToCode,
  15. syncToCodeInFlight,
  16. isDetachLayout,
  17. }: {
  18. syncToCode: ({ visualOffset }: { visualOffset: number }) => void
  19. syncToCodeInFlight: boolean
  20. isDetachLayout?: boolean
  21. }) {
  22. const { t } = useTranslation()
  23. const buttonClasses = classNames('synctex-control', {
  24. 'detach-synctex-control': !!isDetachLayout,
  25. })
  26. let buttonIcon = null
  27. if (syncToCodeInFlight) {
  28. buttonIcon = <OLSpinner size="sm" />
  29. } else if (!isDetachLayout) {
  30. buttonIcon = (
  31. <MaterialIcon type="arrow_left_alt" className="synctex-control-icon" />
  32. )
  33. }
  34. const syncToCodeWithButton = useCallback(() => {
  35. syncToCode({ visualOffset: 72 })
  36. }, [syncToCode])
  37. const overlayProps = useMemo(
  38. () => ({
  39. placement: (isDetachLayout ? 'bottom' : 'right') as Placement,
  40. }),
  41. [isDetachLayout]
  42. )
  43. return (
  44. <OLTooltip
  45. id="sync-to-code"
  46. description={t('go_to_pdf_location_in_code')}
  47. overlayProps={overlayProps}
  48. >
  49. <span>
  50. <OLButton
  51. variant="secondary"
  52. size="sm"
  53. onClick={syncToCodeWithButton}
  54. disabled={syncToCodeInFlight}
  55. className={buttonClasses}
  56. aria-label={t('go_to_pdf_location_in_code')}
  57. >
  58. {buttonIcon}
  59. {isDetachLayout ? <span>&nbsp;{t('show_in_code')}</span> : ''}
  60. </OLButton>
  61. </span>
  62. </OLTooltip>
  63. )
  64. })
  65. const GoToPdfButton = memo(function GoToPdfButton({
  66. syncToPdf,
  67. syncToPdfInFlight,
  68. isDetachLayout,
  69. canSyncToPdf,
  70. }: {
  71. syncToPdf: () => void
  72. syncToPdfInFlight: boolean
  73. canSyncToPdf: boolean
  74. isDetachLayout?: boolean
  75. }) {
  76. const { t } = useTranslation()
  77. const tooltipPlacement = isDetachLayout ? 'bottom' : 'right'
  78. const buttonClasses = classNames('synctex-control', {
  79. 'detach-synctex-control': !!isDetachLayout,
  80. })
  81. let buttonIcon = null
  82. if (syncToPdfInFlight) {
  83. buttonIcon = <OLSpinner size="sm" />
  84. } else if (!isDetachLayout) {
  85. buttonIcon = (
  86. <MaterialIcon type="arrow_right_alt" className="synctex-control-icon" />
  87. )
  88. }
  89. return (
  90. <OLTooltip
  91. id="sync-to-pdf"
  92. description={t('go_to_code_location_in_pdf')}
  93. overlayProps={{ placement: tooltipPlacement }}
  94. >
  95. <span>
  96. <OLButton
  97. variant="secondary"
  98. size="sm"
  99. onClick={syncToPdf}
  100. disabled={syncToPdfInFlight || !canSyncToPdf}
  101. className={buttonClasses}
  102. aria-label={t('go_to_code_location_in_pdf')}
  103. >
  104. {buttonIcon}
  105. {isDetachLayout ? <span>&nbsp;{t('show_in_pdf')}</span> : ''}
  106. </OLButton>
  107. </span>
  108. </OLTooltip>
  109. )
  110. })
  111. function PdfSynctexControls() {
  112. const { detachRole } = useLayoutContext()
  113. const { pdfUrl, pdfViewer, position } = useCompileContext()
  114. const {
  115. syncToCode,
  116. syncToPdf,
  117. syncToCodeInFlight,
  118. syncToPdfInFlight,
  119. canSyncToPdf,
  120. } = useSynctex()
  121. const visualPreviewEnabled = useFeatureFlag('visual-preview')
  122. if (visualPreviewEnabled) {
  123. return null
  124. }
  125. if (!position) {
  126. return null
  127. }
  128. if (!pdfUrl || pdfViewer === 'native') {
  129. return null
  130. }
  131. if (detachRole === 'detacher') {
  132. return (
  133. <GoToPdfButton
  134. syncToPdf={syncToPdf}
  135. syncToPdfInFlight={syncToPdfInFlight}
  136. isDetachLayout
  137. canSyncToPdf={canSyncToPdf}
  138. />
  139. )
  140. } else if (detachRole === 'detached') {
  141. return (
  142. <GoToCodeButton
  143. syncToCode={syncToCode}
  144. syncToCodeInFlight={syncToCodeInFlight}
  145. isDetachLayout
  146. />
  147. )
  148. } else {
  149. return (
  150. <>
  151. <GoToPdfButton
  152. syncToPdf={syncToPdf}
  153. syncToPdfInFlight={syncToPdfInFlight}
  154. canSyncToPdf={canSyncToPdf}
  155. />
  156. <GoToCodeButton
  157. syncToCode={syncToCode}
  158. syncToCodeInFlight={syncToCodeInFlight}
  159. />
  160. </>
  161. )
  162. }
  163. }
  164. export default memo(PdfSynctexControls)