From 6b450a3f21e2133312983bab047701d530b475a5 Mon Sep 17 00:00:00 2001 From: shiva raj badu Date: Sun, 29 Mar 2026 16:34:38 +0545 Subject: [PATCH] refactor: Revamp Present mode --- .../components/PresentationRender.tsx | 40 +- .../components/PresentationMode.tsx | 436 ++++++++++-------- .../components/PresentationPage.tsx | 4 +- electron/servers/nextjs/tsconfig.tsbuildinfo | 2 +- 4 files changed, 281 insertions(+), 201 deletions(-) diff --git a/electron/servers/nextjs/app/(presentation-generator)/components/PresentationRender.tsx b/electron/servers/nextjs/app/(presentation-generator)/components/PresentationRender.tsx index e2781787..9b9a3675 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/components/PresentationRender.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/components/PresentationRender.tsx @@ -6,40 +6,54 @@ import { V1ContentRender } from '../../(presentation-generator)/components/V1Con const BASE_WIDTH = 1280; const BASE_HEIGHT = 720; -const SlideScale = ({ slide, theme }: { slide: any, theme?: any }) => { +const SlideScale = ({ + slide, + theme, + isEditMode = true, + /** Fill viewport; scale may exceed 1 so slides appear larger in present mode */ + presentMode = false, +}: { slide: any; theme?: any; isEditMode?: boolean; presentMode?: boolean }) => { const containerRef = useRef(null); - const [containerWidth, setContainerWidth] = useState(0); + const [box, setBox] = useState({ w: 0, h: 0 }); const scale = useMemo(() => { - // Slight padding to avoid overflow due to borders/scrollbars - const safeWidth = Math.max(0, containerWidth + 20); + if (presentMode) { + const { w, h } = box; + if (w < 1 || h < 1) return 1; + const sx = (w / BASE_WIDTH) * 0.995; + const sy = (h / BASE_HEIGHT) * 0.995; + return Math.min(sx, sy); + } + const safeWidth = Math.max(0, box.w + 20); if (!safeWidth) return 1; return Math.min((safeWidth / BASE_WIDTH) * 0.98, 1); - }, [containerWidth]); + }, [presentMode, box.w, box.h]); useEffect(() => { if (!containerRef.current) return; const el = containerRef.current; const ro = new ResizeObserver(() => { - // Use clientWidth so we match the actual available column width - setContainerWidth(el.clientWidth); + setBox({ w: el.clientWidth, h: el.clientHeight }); }); ro.observe(el); - // Initial measure - setContainerWidth(el.clientWidth); + setBox({ w: el.clientWidth, h: el.clientHeight }); return () => ro.disconnect(); }, []); return (
{ aria-hidden="true" /> */} - +
diff --git a/electron/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationMode.tsx b/electron/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationMode.tsx index 41b8d913..4d610cd2 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationMode.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationMode.tsx @@ -1,5 +1,6 @@ "use client"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; + +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ChevronLeft, ChevronRight, @@ -8,297 +9,360 @@ import { Maximize2, StickyNote, EyeOff, + Keyboard, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Slide } from "../../types/slide"; -import { V1ContentRender } from "../../components/V1ContentRender"; - - +import SlideScale from "../../components/PresentationRender"; +import type { Theme } from "../../services/api/types"; interface PresentationModeProps { slides: Slide[]; currentSlide: number; - + theme?: Theme | null; isFullscreen: boolean; onFullscreenToggle: () => void; onExit: () => void; onSlideChange: (slideNumber: number) => void; } -const PresentationMode: React.FC = ({ +const CHROME_HIDE_MS = 800; +const PresentationMode: React.FC = ({ slides, currentSlide, - + theme, isFullscreen, onFullscreenToggle, onExit, onSlideChange, - - }) => { - if (slides === undefined || slides === null || slides.length === 0) { - return null; - } - + const rootRef = useRef(null); + const hideChromeTimerRef = useRef | null>(null); const [showSpeakerNotes, setShowSpeakerNotes] = useState(true); + const [chromeVisible, setChromeVisible] = useState(true); + const currentSpeakerNote = useMemo( () => slides[currentSlide]?.speaker_note?.trim() || "", [slides, currentSlide] ); + const activeSlide = slides[currentSlide]; - const recomputeScale = useCallback(() => { - if (typeof window === "undefined") return; - const padding = isFullscreen ? 0 : 64; // match p-8 when not fullscreen - const fullscreenMargin = isFullscreen ? 16 : 0; // small safety margin to prevent clipping - const availableWidth = Math.max(window.innerWidth - padding - fullscreenMargin, 0); - const availableHeight = Math.max(window.innerHeight - padding - fullscreenMargin, 0); - const baseW = 1280; - const baseH = 720; - const s = Math.min(availableWidth / baseW, availableHeight / baseH); - + const bumpChromeVisibility = useCallback(() => { + setChromeVisible(true); + if (hideChromeTimerRef.current) clearTimeout(hideChromeTimerRef.current); + hideChromeTimerRef.current = setTimeout(() => { + if (isFullscreen) setChromeVisible(false); + }, CHROME_HIDE_MS); }, [isFullscreen]); useEffect(() => { - recomputeScale(); - window.addEventListener("resize", recomputeScale); - return () => window.removeEventListener("resize", recomputeScale); - }, [recomputeScale]); + rootRef.current?.focus({ preventScroll: true }); + }, []); + useEffect(() => { + if (!isFullscreen) { + setChromeVisible(true); + if (hideChromeTimerRef.current) { + clearTimeout(hideChromeTimerRef.current); + hideChromeTimerRef.current = null; + } + return; + } + bumpChromeVisibility(); + return () => { + if (hideChromeTimerRef.current) clearTimeout(hideChromeTimerRef.current); + }; + }, [isFullscreen, bumpChromeVisibility]); - // Modify the handleKeyPress to prevent default behavior - const handleKeyPress = useCallback( + const handlePointerActivity = useCallback(() => { + bumpChromeVisibility(); + }, [bumpChromeVisibility]); + + const goNext = useCallback(() => { + if (currentSlide < slides.length - 1) onSlideChange(currentSlide + 1); + }, [currentSlide, slides.length, onSlideChange]); + + const goPrev = useCallback(() => { + if (currentSlide > 0) onSlideChange(currentSlide - 1); + }, [currentSlide, onSlideChange]); + + const handleKeyDown = useCallback( (event: KeyboardEvent) => { - event.preventDefault(); // Prevent default scroll behavior + const navKeys = [ + "ArrowRight", + "ArrowLeft", + "ArrowUp", + "ArrowDown", + " ", + "Home", + "End", + "PageDown", + "PageUp", + ]; + if (navKeys.includes(event.key)) { + event.preventDefault(); + } + + if (event.repeat) { + if (event.key === " " || event.key === "ArrowRight" || event.key === "ArrowLeft") { + return; + } + } switch (event.key) { case "ArrowRight": case "ArrowDown": - case " ": // Space key - if (currentSlide < slides.length - 1) { - onSlideChange(currentSlide + 1); - } + case " ": + case "PageDown": + goNext(); break; case "ArrowLeft": case "ArrowUp": - if (currentSlide > 0) { - onSlideChange(currentSlide - 1); + case "PageUp": + goPrev(); + break; + case "Home": + if (currentSlide !== 0) onSlideChange(0); + break; + case "End": + if (slides.length > 0 && currentSlide !== slides.length - 1) { + onSlideChange(slides.length - 1); } break; case "Escape": - // If fullscreen is active, only exit fullscreen on first ESC. Second ESC exits present mode. if (document.fullscreenElement) { - try { document.exitFullscreen(); } catch (_) { } + try { + document.exitFullscreen(); + } catch { + /* ignore */ + } return; } onExit(); break; case "f": case "F": - onFullscreenToggle(); + if (!event.ctrlKey && !event.metaKey && !event.altKey) { + onFullscreenToggle(); + } break; case "n": case "N": - setShowSpeakerNotes((prev) => !prev); + if (!event.ctrlKey && !event.metaKey && !event.altKey) { + setShowSpeakerNotes((prev) => !prev); + } + break; + default: break; } }, - [currentSlide, slides.length, onSlideChange, onExit, onFullscreenToggle, isFullscreen] + [currentSlide, slides.length, onSlideChange, onExit, onFullscreenToggle, goNext, goPrev] ); - // Add both keydown and keyup listeners useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - // Prevent default behavior for arrow keys and space - if ( - ["ArrowRight", "ArrowLeft", "ArrowUp", "ArrowDown", " "].includes(e.key) - ) { - e.preventDefault(); - } - handleKeyPress(e); - }; - window.addEventListener("keydown", handleKeyDown); - return () => { - window.removeEventListener("keydown", handleKeyDown); - }; - }, [handleKeyPress]); - - // Add click handlers for the slide area - const handleSlideClick = (e: React.MouseEvent) => { - // Don't trigger navigation if clicking on controls - if ((e.target as HTMLElement).closest(".presentation-controls")) { - return; - } + return () => window.removeEventListener("keydown", handleKeyDown); + }, [handleKeyDown]); + const handleSlideAreaClick = (e: React.MouseEvent) => { + if ((e.target as HTMLElement).closest(".presentation-controls")) return; const clickX = e.clientX; - const windowWidth = window.innerWidth; - - if (clickX < windowWidth / 3) { - if (currentSlide > 0) { - onSlideChange(currentSlide - 1); - } - } else if (clickX > (windowWidth * 2) / 3) { - if (currentSlide < slides.length - 1) { - onSlideChange(currentSlide + 1); - } - } + const w = window.innerWidth; + if (clickX < w / 5) goPrev(); + else if (clickX > (w * 4) / 5) goNext(); }; - // Handle Escape key separately - useEffect(() => { - const handleEscKey = (e: KeyboardEvent) => { - if (e.key === "Escape" && isFullscreen) { - onFullscreenToggle(); // Just toggle fullscreen, don't exit presentation - } - }; + const progress = slides.length > 0 ? ((currentSlide + 1) / slides.length) * 100 : 0; - document.addEventListener("keydown", handleEscKey); - return () => document.removeEventListener("keydown", handleEscKey); - }, [isFullscreen, onFullscreenToggle]); + if (slides === undefined || slides === null || slides.length === 0) { + return null; + } return (
- {/* Controls - Only show when not in fullscreen */} - {!isFullscreen && ( - <> -
- - -
+ + Slide {currentSlide + 1} of {slides.length} + -
- - - {currentSlide + 1} / {slides.length} - - -
- - )} - - {/* Centered 16:9 stage for consistent alignment in normal + fullscreen modes */} -
-
- {slides.length > 0 && slides.map((slide, index) => ( -
- -
- ))} + {/* Top bar — fullscreen: auto-hide */} +
+
+ +
- {currentSpeakerNote && ( -
+ {/* Slide stage — large viewport; SlideScale uses width+height so slides scale up */} +
+
+ {activeSlide ? ( + + ) : null} +
+
+ + {/* Progress */} +
+
+
+ + {/* Bottom controls */} +
+ +
+ {currentSlide + 1} / {slides.length} +
+ +
+
+ + + ← → space · Home/End · F fullscreen · N notes · Esc exit + +
+
+ + {currentSpeakerNote ? ( +
{showSpeakerNotes ? ( -
-
+
+
- + Speaker notes
-
+
{currentSpeakerNote}
) : ( )}
- )} + ) : null}
); }; diff --git a/electron/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx b/electron/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx index 94795e98..8f8cc876 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx @@ -61,6 +61,7 @@ const PresentationPage: React.FC = ({ const { isPresentMode, stream, + currentSlide: presentSlideFromUrl, handleSlideClick, toggleFullscreen, handlePresentExit, @@ -93,7 +94,8 @@ const PresentationPage: React.FC = ({ return (