pdf-synctex-controls.tsx 4.7 KB

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