From b67039dc6c661d6db71370f03e75d5d5dea0e9d2 Mon Sep 17 00:00:00 2001 From: Nathan Panchout Date: Tue, 30 Jun 2026 16:06:11 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20export=20the=20presentati?= =?UTF-8?q?on=20as=20a=20PDF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render the slides off-screen as A4 landscape pages and print them via the browser, one slide per page with the watermark. Add a "Download PDF" action to the floating bar. Closes #2446 --- CHANGELOG.md | 4 + .../__tests__/PresenterPrintDocument.spec.tsx | 80 +++++++++ .../components/PresenterFloatingBar.tsx | 13 +- .../components/PresenterOverlay.tsx | 17 ++ .../components/PresenterPrintDocument.tsx | 169 ++++++++++++++++++ .../docs/doc-presenter/utils_print.tsx | 150 ++++++++++++++++ 6 files changed, 432 insertions(+), 1 deletion(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/PresenterPrintDocument.spec.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterPrintDocument.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/doc-presenter/utils_print.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 6992ace6b..438a4b12f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to ## [Unreleased] +### Added + +- ✨(frontend) export presenter slides as PDF #2487 + ### Fixed - 🐛(frontend) hide Leave in the doc menu when not logged in #2626 diff --git a/src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/PresenterPrintDocument.spec.tsx b/src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/PresenterPrintDocument.spec.tsx new file mode 100644 index 000000000..cd9499cba --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-presenter/__tests__/PresenterPrintDocument.spec.tsx @@ -0,0 +1,80 @@ +import { render } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; + +import { + PRESENTER_PRINT_CONTENT_CLASS, + PRESENTER_PRINT_CSS, + PRESENTER_PRINT_LOGO_CLASS, + PRESENTER_PRINT_PAGE_CLASS, + PRESENTER_PRINT_PAGE_SELECTOR, + PRESENTER_PRINT_TITLE_CLASS, + PresenterPrintDocument, +} from '../components/PresenterPrintDocument'; +import type { PresenterBlock } from '../types'; + +const para = (text: string): PresenterBlock => ({ + children: [], + content: [{ type: 'text', text, styles: {} }], + id: 'paragraph', + props: { + backgroundColor: 'default', + textAlignment: 'left', + textColor: 'default', + }, + type: 'paragraph', +}); + +describe('PresenterPrintDocument', () => { + beforeEach(() => { + Object.defineProperty(window, 'matchMedia', { + configurable: true, + writable: true, + value: vi.fn().mockImplementation((query: string) => ({ + addEventListener: vi.fn(), + addListener: vi.fn(), + dispatchEvent: vi.fn(), + matches: false, + media: query, + onchange: null, + removeEventListener: vi.fn(), + removeListener: vi.fn(), + })), + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + test('renders one print page per slide', () => { + const { container } = render( + , + ); + + expect( + container.querySelectorAll(PRESENTER_PRINT_PAGE_SELECTOR), + ).toHaveLength(4); + expect( + container.querySelector(`.${PRESENTER_PRINT_TITLE_CLASS}`), + ).toHaveTextContent('Deck title'); + expect( + container.querySelectorAll(`.${PRESENTER_PRINT_LOGO_CLASS}`), + ).toHaveLength(4); + }); + + test('centers short slide content vertically in print pages', () => { + expect(PRESENTER_PRINT_CSS).toContain(`.${PRESENTER_PRINT_PAGE_CLASS} {`); + expect(PRESENTER_PRINT_CSS).toContain('justify-content: safe center;'); + expect(PRESENTER_PRINT_CSS).toContain( + `.${PRESENTER_PRINT_CONTENT_CLASS} {`, + ); + expect(PRESENTER_PRINT_CSS).toContain('max-height: 100%;'); + }); +}); diff --git a/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterFloatingBar.tsx b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterFloatingBar.tsx index 791c8ea93..4e2623480 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterFloatingBar.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterFloatingBar.tsx @@ -6,6 +6,7 @@ import { import { ChevronLeft, ChevronRight, + Download, Link, Maximize, Minimize, @@ -25,8 +26,10 @@ interface PresenterFloatingBarProps { onPrev: () => void; onNext: () => void; onCopyLink: () => void; + onExportPdf: () => void; onToggleFullscreen: () => void; onClose: () => void; + isExportingPdf: boolean; } const barCss = css` @@ -83,8 +86,10 @@ export const PresenterFloatingBar = ({ onPrev, onNext, onCopyLink, + onExportPdf, onToggleFullscreen, onClose, + isExportingPdf, }: PresenterFloatingBarProps) => { const { t } = useTranslation(); const isFirst = index <= 0; @@ -121,8 +126,14 @@ export const PresenterFloatingBar = ({ icon: , callback: onCopyLink, }, + { + label: t('Download PDF'), + icon: