From 7a72117b6a625a61a552a67f5cdb64d809e81c5b Mon Sep 17 00:00:00 2001 From: Dmitri Date: Mon, 15 Jun 2026 11:59:44 +0200 Subject: [PATCH] made mute button global --- .../src/components/RuntimePresentation.tsx | 39 ++-------- frontend/src/hooks/useVideoSoundControl.ts | 4 + frontend/src/lib/presentationAudio.ts | 75 +++++++++++++++++++ frontend/src/pages/constructor.tsx | 46 ++++-------- 4 files changed, 97 insertions(+), 67 deletions(-) create mode 100644 frontend/src/lib/presentationAudio.ts diff --git a/frontend/src/components/RuntimePresentation.tsx b/frontend/src/components/RuntimePresentation.tsx index 1523ca3..685e38e 100644 --- a/frontend/src/components/RuntimePresentation.tsx +++ b/frontend/src/components/RuntimePresentation.tsx @@ -45,6 +45,7 @@ import { usePageNavigationState } from '../hooks/usePageNavigationState'; import { useTransitionPlayback } from '../hooks/useTransitionPlayback'; import { useNetworkAware } from '../hooks/useNetworkAware'; import { resolveAssetPlaybackUrl } from '../lib/assetUrl'; +import { presentationHasAudio } from '../lib/presentationAudio'; import { downloadManager } from '../lib/offline/DownloadManager'; import { isSafari } from '../lib/browserUtils'; import { logger } from '../lib/logger'; @@ -817,39 +818,9 @@ export default function RuntimePresentation({ selectedPage?.background_audio_end_time != null ? parseFloat(String(selectedPage.background_audio_end_time)) : null; - const hasElementAudio = useMemo( - () => - pageElements.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; - }), - [pageElements], + const hasPresentationAudio = useMemo( + () => presentationHasAudio(pages), + [pages], ); // Global sound control starts muted for browser autoplay compatibility. @@ -857,7 +828,7 @@ export default function RuntimePresentation({ pageHasSound: pageVideoMuted === false, hasBackgroundVideo: Boolean(backgroundVideoUrl), hasBackgroundAudio: Boolean(backgroundAudioUrl), - hasElementAudio, + hasPresentationAudio, }); // Note: useBackgroundVideoPlayback is handled internally by CanvasBackground component diff --git a/frontend/src/hooks/useVideoSoundControl.ts b/frontend/src/hooks/useVideoSoundControl.ts index 17301d2..738f4b8 100644 --- a/frontend/src/hooks/useVideoSoundControl.ts +++ b/frontend/src/hooks/useVideoSoundControl.ts @@ -24,6 +24,8 @@ export interface UseVideoSoundControlOptions { hasBackgroundAudio?: boolean; /** Whether page elements have hover/click audio effects or media player sound */ hasElementAudio?: boolean; + /** Whether any page in the presentation can produce sound */ + hasPresentationAudio?: boolean; } export interface UseVideoSoundControlResult { @@ -61,6 +63,7 @@ export function useVideoSoundControl({ hasBackgroundVideo, hasBackgroundAudio = false, hasElementAudio = false, + hasPresentationAudio = false, }: UseVideoSoundControlOptions): UseVideoSoundControlResult { const { isMuted } = useGlobalAudioMute(); @@ -71,6 +74,7 @@ export function useVideoSoundControl({ return { isMuted, showSoundButton: + hasPresentationAudio || (pageHasSound && hasBackgroundVideo) || hasBackgroundAudio || hasElementAudio, diff --git a/frontend/src/lib/presentationAudio.ts b/frontend/src/lib/presentationAudio.ts new file mode 100644 index 0000000..d9cdfa5 --- /dev/null +++ b/frontend/src/lib/presentationAudio.ts @@ -0,0 +1,75 @@ +import type { CanvasElement, ConstructorSchema } from '../types/constructor'; +import type { TourPage } from '../types/entities'; +import { parseJsonObject } from './parseJson'; + +type PageWithAudioFields = Pick< + TourPage, + | 'background_audio_url' + | 'background_video_url' + | 'background_video_muted' + | 'ui_schema_json' +>; + +const asRecords = (value: unknown): Record[] => + Array.isArray(value) + ? value.filter( + (item): item is Record => + Boolean(item) && typeof item === 'object', + ) + : []; + +const elementHasAudio = (element: Partial): boolean => { + if (element.hoverAudioUrl || element.clickAudioUrl) return true; + + if ( + (element.type === 'audio_player' || element.type === 'video_player') && + element.mediaUrl && + !element.mediaMuted + ) { + return true; + } + + const galleryCards = asRecords(element.galleryCards); + if ( + galleryCards.some( + (card) => card.mediaType === 'video' || Boolean(card.videoUrl), + ) + ) { + return true; + } + + const infoPanelSections = asRecords(element.infoPanelSections); + if ( + infoPanelSections.some((section) => + asRecords(section.images).some( + (item) => item.itemType === 'video' || Boolean(item.videoUrl), + ), + ) + ) { + return true; + } + + return false; +}; + +export const elementsHaveAudio = ( + elements: Array> = [], +): boolean => elements.some(elementHasAudio); + +export const pageHasAudio = (page: PageWithAudioFields): boolean => { + if (page.background_audio_url) return true; + if (page.background_video_url && page.background_video_muted === false) { + return true; + } + + const schema = parseJsonObject( + page.ui_schema_json, + {} as ConstructorSchema, + ); + return Array.isArray(schema.elements) && elementsHaveAudio(schema.elements); +}; + +export const presentationHasAudio = ( + pages: PageWithAudioFields[] = [], + extraElements: Array> = [], +): boolean => pages.some(pageHasAudio) || elementsHaveAudio(extraElements); diff --git a/frontend/src/pages/constructor.tsx b/frontend/src/pages/constructor.tsx index bbad2d5..a4ec698 100644 --- a/frontend/src/pages/constructor.tsx +++ b/frontend/src/pages/constructor.tsx @@ -48,6 +48,7 @@ import { backgroundAudioController } from '../lib/backgroundAudioController'; import { isSafari } from '../lib/browserUtils'; import { resolveAssetPlaybackUrl } from '../lib/assetUrl'; import { downloadManager } from '../lib/offline/DownloadManager'; +import { presentationHasAudio } from '../lib/presentationAudio'; import { parseJsonObject } from '../lib/parseJson'; import { resolveNavigationTarget, @@ -366,39 +367,18 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { [elements], ); - const hasElementAudio = useMemo( + const hasPresentationAudio = 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], + presentationHasAudio(pages, elements) || + Boolean(backgroundAudioUrl) || + Boolean(backgroundVideoUrl && backgroundVideoMuted === false), + [ + pages, + elements, + backgroundAudioUrl, + backgroundVideoUrl, + backgroundVideoMuted, + ], ); // Global sound control starts muted for browser autoplay compatibility. @@ -406,7 +386,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { pageHasSound: backgroundVideoMuted === false, hasBackgroundVideo: Boolean(backgroundVideoUrl), hasBackgroundAudio: Boolean(backgroundAudioUrl), - hasElementAudio, + hasPresentationAudio, }); // Look up current element for gallery carousel (so it receives updates from element editor)