diff --git a/frontend/src/components/Runtime/RuntimeControls.tsx b/frontend/src/components/Runtime/RuntimeControls.tsx index a35fc2b..1bc17f5 100644 --- a/frontend/src/components/Runtime/RuntimeControls.tsx +++ b/frontend/src/components/Runtime/RuntimeControls.tsx @@ -46,6 +46,10 @@ interface RuntimeControlsProps { isMuted?: boolean; /** Callback to toggle all presentation sound on/off */ onSoundToggle?: () => void; + /** Whether to show the offline download button */ + showOfflineButton?: boolean; + /** Whether to show the fullscreen button */ + showFullscreenButton?: boolean; } /** @@ -468,6 +472,8 @@ export default function RuntimeControls({ showSoundButton = false, isMuted = true, onSoundToggle, + showOfflineButton = true, + showFullscreenButton = true, }: RuntimeControlsProps) { // Counter-scale to resist pinch-zoom const counterScale = useCounterZoom(); @@ -520,18 +526,22 @@ export default function RuntimeControls({ onTouchEnd={stopControlEvent} onTouchEndCapture={stopControlEvent} > - - + {showOfflineButton && ( + + )} + {showFullscreenButton && ( + + )} {showSoundButton && onSoundToggle && ( { }); // Canvas scale for responsive UI elements and letterbox mode - const { cssVars: canvasCssVars, letterboxStyles } = useCanvasScale({ + const { + cssVars: canvasCssVars, + letterboxStyles, + canvasWidth, + canvasHeight, + } = useCanvasScale({ designWidth: project?.design_width, designHeight: project?.design_height, }); @@ -241,13 +247,6 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { backgroundAudioEndTime, } = usePageBackground(); - // Global sound control starts muted for browser autoplay compatibility. - const soundControl = useVideoSoundControl({ - pageHasSound: backgroundVideoMuted === false, - hasBackgroundVideo: Boolean(backgroundVideoUrl), - hasBackgroundAudio: Boolean(backgroundAudioUrl), - }); - // Network-aware transitions: skip video on slow networks, use CSS fade instead const { shouldUseVideoTransitions, networkInfo } = useNetworkAware(); @@ -367,6 +366,49 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { [elements], ); + const hasElementAudio = useMemo( + () => + elements.some((element) => { + if (element.hoverAudioUrl || element.clickAudioUrl) return true; + if ( + (element.type === 'audio_player' || + element.type === 'video_player') && + element.mediaUrl && + !element.mediaMuted + ) { + return true; + } + if ( + element.galleryCards?.some( + (card: GalleryCarouselMediaItem) => + card.mediaType === 'video' || Boolean(card.videoUrl), + ) + ) { + return true; + } + if ( + element.infoPanelSections?.some((section) => + section.images?.some( + (item: InfoPanelImage) => + item.itemType === 'video' || Boolean(item.videoUrl), + ), + ) + ) { + return true; + } + return false; + }), + [elements], + ); + + // Global sound control starts muted for browser autoplay compatibility. + const soundControl = useVideoSoundControl({ + pageHasSound: backgroundVideoMuted === false, + hasBackgroundVideo: Boolean(backgroundVideoUrl), + hasBackgroundAudio: Boolean(backgroundAudioUrl), + hasElementAudio, + }); + // Look up current element for gallery carousel (so it receives updates from element editor) const activeGalleryCarouselElement = useMemo(() => { if (!activeGalleryCarousel) return null; @@ -2184,6 +2226,27 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { + {!isConstructorEditMode && + !activeGalleryCarousel && + !activeInfoPanelGallery && + soundControl.showSoundButton && ( + undefined} + canvasWidth={canvasWidth} + canvasHeight={canvasHeight} + showOfflineButton={false} + showFullscreenButton={false} + showSoundButton={soundControl.showSoundButton} + isMuted={soundControl.isMuted} + onSoundToggle={soundControl.toggleSound} + /> + )} + {/* ElementEditorPanel now uses ConstructorContext for all state */} {pages.length > 0 && hasEditorSelection && (