diff --git a/packages/gitbook/src/components/DocumentView/Drawing.tsx b/packages/gitbook/src/components/DocumentView/Drawing.tsx index db187db04..bfcf40ff9 100644 --- a/packages/gitbook/src/components/DocumentView/Drawing.tsx +++ b/packages/gitbook/src/components/DocumentView/Drawing.tsx @@ -31,7 +31,7 @@ export async function Drawing(props: BlockProps) { alt="Drawing" sizes={imageBlockSizes} zoom - loading="lazy" + loading={context.mode === 'print' ? 'eager' : 'lazy'} /> ); diff --git a/packages/gitbook/src/components/DocumentView/Embed.tsx b/packages/gitbook/src/components/DocumentView/Embed.tsx index 3977d30af..81689f84b 100644 --- a/packages/gitbook/src/components/DocumentView/Embed.tsx +++ b/packages/gitbook/src/components/DocumentView/Embed.tsx @@ -60,7 +60,7 @@ export async function Embed(props: BlockProps) { sources={{ light: { src: embed.icon } }} sizes={[{ width: 20 }]} resize={context.contentContext.imageResizer} - loading="lazy" + loading={context.mode === 'print' ? 'eager' : 'lazy'} /> ) : null } diff --git a/packages/gitbook/src/components/DocumentView/Images.tsx b/packages/gitbook/src/components/DocumentView/Images.tsx index dc9dcf91d..4eebf7250 100644 --- a/packages/gitbook/src/components/DocumentView/Images.tsx +++ b/packages/gitbook/src/components/DocumentView/Images.tsx @@ -115,7 +115,7 @@ async function ImageBlock(props: { } : null, }} - loading={isEstimatedOffscreen ? 'lazy' : 'eager'} + loading={isEstimatedOffscreen && context.mode !== 'print' ? 'lazy' : 'eager'} zoom inlineStyle={{ maxWidth: '100%', diff --git a/packages/gitbook/src/components/DocumentView/InlineImage.tsx b/packages/gitbook/src/components/DocumentView/InlineImage.tsx index 854c9cdc2..5cce8db72 100644 --- a/packages/gitbook/src/components/DocumentView/InlineImage.tsx +++ b/packages/gitbook/src/components/DocumentView/InlineImage.tsx @@ -51,7 +51,7 @@ export async function InlineImage(props: InlineProps) { } : null, }} - loading="lazy" + loading={context.mode === 'print' ? 'eager' : 'lazy'} style={[size === 'line' ? ['max-h-lh', 'h-lh', 'w-auto'] : null]} inline zoom={!isInLink} diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx index cba695038..97c1c9098 100644 --- a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx +++ b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx @@ -120,7 +120,7 @@ export async function RecordCard( : ['h-auto', 'aspect-video'], objectFits )} - loading={isOffscreen ? 'lazy' : 'eager'} + loading={isOffscreen && context.mode !== 'print' ? 'lazy' : 'eager'} /> ) : null}
( }, }, }} - loading="lazy" + loading={context.mode === 'print' ? 'eager' : 'lazy'} /> ) : ( ( size: image.file?.dimensions, }, }} - loading="lazy" + loading={context.mode === 'print' ? 'eager' : 'lazy'} /> {image.text} diff --git a/packages/gitbook/src/components/PDF/ImagesLoadingStatus.tsx b/packages/gitbook/src/components/PDF/ImagesLoadingStatus.tsx new file mode 100644 index 000000000..26f32f76e --- /dev/null +++ b/packages/gitbook/src/components/PDF/ImagesLoadingStatus.tsx @@ -0,0 +1,141 @@ +'use client'; + +import { tString } from '@/intl/translate'; +import type { TranslationLanguage } from '@/intl/translations'; +import assertNever from 'assert-never'; +import * as React from 'react'; + +export function ImagesLoadingStatus(props: { + language: TranslationLanguage; +}) { + const { language } = props; + const state = useImagesLoadingState(); + return ( +

+ {(() => { + switch (state.status) { + case 'pending': + return null; + case 'loading': + return tString( + language, + 'pdf_images_loading', + `${state.loadedImages}`, + `${state.totalImages}` + ); + case 'ready': + return tString(language, 'pdf_images_loaded'); + default: + assertNever(state); + } + })()} +

+ ); +} + +/** + * Keep track of images loading state. + */ +function useImagesLoadingState() { + const [totalImages, setTotalImages] = React.useState(null); + const [loadedImages, setLoadedImages] = React.useState(0); + const attachedImages = React.useRef(new WeakSet()); + const rafRef = React.useRef(0); + + const calculateImageStatus = React.useCallback(() => { + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + } + rafRef.current = requestAnimationFrame(() => { + const images = Array.from(document.images); + setTotalImages(images.length); + setLoadedImages(images.filter((image) => image.complete).length); + }); + }, []); + + React.useEffect(() => () => cancelAnimationFrame(rafRef.current), []); + + const attachListeners = React.useCallback( + (images: HTMLImageElement[]) => { + images.forEach((image) => { + if (image.complete || attachedImages.current.has(image)) { + return; + } + + if (image.loading !== 'eager') { + console.warn('An image is not in "eager" mode', image); + } + + attachedImages.current.add(image); + image.addEventListener('load', calculateImageStatus); + image.addEventListener('error', calculateImageStatus); + }); + }, + [calculateImageStatus] + ); + + React.useEffect(() => { + calculateImageStatus(); + attachListeners(Array.from(document.images)); + + let raf: number; + + const observer = new MutationObserver((mutationsList) => { + cancelAnimationFrame(raf); + raf = requestAnimationFrame(() => { + const newImages: HTMLImageElement[] = []; + let shouldRecalculate = false; + for (const mutation of mutationsList) { + if (mutation.type !== 'childList') { + continue; + } + if (mutation.removedNodes.length > 0) { + // Images may have been removed; recalculate counts. + shouldRecalculate = true; + } + mutation.addedNodes.forEach((node) => { + if (node instanceof HTMLImageElement) { + newImages.push(node); + shouldRecalculate = true; + } else if (node instanceof HTMLElement) { + const nestedImages = Array.from( + node.getElementsByTagName('img') + ) as HTMLImageElement[]; + if (nestedImages.length > 0) { + newImages.push(...nestedImages); + shouldRecalculate = true; + } + } + }); + } + if (newImages.length > 0) { + attachListeners(newImages); + } + if (shouldRecalculate) { + calculateImageStatus(); + } + }); + }); + + observer.observe(document.body, { childList: true, subtree: true }); + + return () => { + cancelAnimationFrame(raf); + observer.disconnect(); + Array.from(document.images).forEach((image) => { + image.removeEventListener('load', calculateImageStatus); + image.removeEventListener('error', calculateImageStatus); + }); + }; + }, [attachListeners, calculateImageStatus]); + + if (totalImages === null) { + return { status: 'pending' } as const; + } + + return { + status: totalImages === loadedImages ? 'ready' : 'loading', + totalImages, + loadedImages, + } as const; +} diff --git a/packages/gitbook/src/components/PDF/PDFPage.tsx b/packages/gitbook/src/components/PDF/PDFPage.tsx index bd9603e4a..4f6060d74 100644 --- a/packages/gitbook/src/components/PDF/PDFPage.tsx +++ b/packages/gitbook/src/components/PDF/PDFPage.tsx @@ -24,11 +24,12 @@ import { tcls } from '@/lib/tailwind'; import { defaultCustomization } from '@/lib/utils'; import { type PDFSearchParams, getPDFSearchParams } from './urls'; +import { PDFPrintControls } from './PDFPrintControls'; import { PageControlButtons } from './PageControlButtons'; -import { PrintButton } from './PrintButton'; import './pdf.css'; import { sanitizeGitBookAppURL } from '@/lib/app'; import { getPageDocument } from '@/lib/data'; +import { ImagesLoadingStatus } from './ImagesLoadingStatus'; const DEFAULT_LIMIT = 100; @@ -118,28 +119,9 @@ export async function PDFPage(props: {
) : null} -
- - - +
+ +
-
-

- {customization.title ?? space.title} -

+
+

{customization.title ?? space.title}

); @@ -201,18 +181,8 @@ async function PDFPageGroup(props: { space: Space; page: RevisionPageGroup }) { return ( -
-

{page.title}

+
+

{page.title}

); @@ -227,9 +197,9 @@ async function PDFPageDocument(props: { return ( -

{page.title}

+

{page.title}

{page.description ? ( -

{page.description}

+

{page.description}

) : null} {document ? ( diff --git a/packages/gitbook/src/components/PDF/PDFPrintControls.tsx b/packages/gitbook/src/components/PDF/PDFPrintControls.tsx new file mode 100644 index 000000000..4b5530375 --- /dev/null +++ b/packages/gitbook/src/components/PDF/PDFPrintControls.tsx @@ -0,0 +1,18 @@ +import { tString } from '@/intl/translate'; +import type { TranslationLanguage } from '@/intl/translations'; +import { Icon } from '@gitbook/icons'; + +import { PrintButton } from './PrintButton'; + +export function PDFPrintControls(props: { language: TranslationLanguage }) { + const { language } = props; + + return ( + + + + ); +} diff --git a/packages/gitbook/src/components/PDF/PrintButton.tsx b/packages/gitbook/src/components/PDF/PrintButton.tsx index 17796ad40..f2a22c7fd 100644 --- a/packages/gitbook/src/components/PDF/PrintButton.tsx +++ b/packages/gitbook/src/components/PDF/PrintButton.tsx @@ -1,19 +1,7 @@ 'use client'; -import * as React from 'react'; +import type { ComponentPropsWithRef } from 'react'; -import type { PolymorphicComponentProp } from '@/components/utils/types'; - -export function PrintButton(props: PolymorphicComponentProp<'button'>) { - const { className, children, ...rest } = props; - - const onClick = React.useCallback(() => { - window.print(); - }, []); - - return ( - - ); +export function PrintButton(props: Omit, 'onClick'>) { + return