improved full screen functionality

This commit is contained in:
Dmitri 2026-07-02 09:02:00 +02:00
parent 10bb6da35f
commit f7219ece35
3 changed files with 150 additions and 91 deletions

View File

@ -51,6 +51,12 @@ import { downloadManager } from '../lib/offline/DownloadManager';
import { isSafari } from '../lib/browserUtils';
import { logger } from '../lib/logger';
import { backgroundAudioController } from '../lib/backgroundAudioController';
import {
exitNativeFullscreen,
getFullscreenElement,
requestDocumentFullscreen,
requestParentFullscreenExit,
} from '../lib/fullscreen';
import {
resolveNavigationTarget,
isTransitionBlocking,
@ -473,24 +479,40 @@ export default function RuntimePresentation({
const toggleFullscreen = useCallback(async () => {
try {
if (!document.fullscreenElement) {
await document.documentElement.requestFullscreen();
setIsFullscreen(true);
} else {
await document.exitFullscreen();
setIsFullscreen(false);
const fullscreenElement = getFullscreenElement();
if (!isFullscreen && !fullscreenElement) {
const entered = await requestDocumentFullscreen(
'runtime-global-control',
);
setIsFullscreen(entered);
return;
}
if (fullscreenElement) {
await exitNativeFullscreen();
} else {
requestParentFullscreenExit('runtime-global-control');
}
setIsFullscreen(false);
} catch (err) {
const parentExitRequested = requestParentFullscreenExit(
'runtime-global-control',
);
if (parentExitRequested) {
setIsFullscreen(false);
return;
}
logger.error(
'Fullscreen toggle failed:',
err instanceof Error ? err : { error: err },
);
}
}, []);
}, [isFullscreen]);
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(Boolean(document.fullscreenElement));
setIsFullscreen(Boolean(getFullscreenElement()));
};
document.addEventListener('fullscreenchange', handleFullscreenChange);

View File

@ -19,6 +19,13 @@ import { resolveAssetPlaybackUrl } from '../../lib/assetUrl';
import { getFontByKey, getFontStyle } from '../../lib/fonts';
import { useGlobalAudioMute } from '../../hooks/useGlobalAudioMute';
import { buildChromeFreeEmbedUrl, isValidEmbedUrl } from '../../lib/embedUrl';
import {
exitNativeFullscreen,
getFullscreenElement,
isNativeFullscreenElement,
requestElementFullscreen,
requestEmbeddingFrameFullscreen,
} from '../../lib/fullscreen';
interface ImageDetailPanelProps {
element: CanvasElement;
@ -32,72 +39,6 @@ interface ImageDetailPanelProps {
onDetailPositionChange?: (xPercent: number, yPercent: number) => void;
}
const getFullscreenElement = (): Element | null => {
return (
document.fullscreenElement ??
(
document as Document & {
webkitFullscreenElement?: Element | null;
}
).webkitFullscreenElement ??
null
);
};
const isNativeFullscreenElement = (element: Element | null): boolean =>
!!element && getFullscreenElement() === element;
const exitNativeFullscreen = async (): Promise<void> => {
if (document.exitFullscreen) {
await document.exitFullscreen();
return;
}
const webkitExitFullscreen = (
document as Document & {
webkitExitFullscreen?: () => Promise<void>;
}
).webkitExitFullscreen;
if (webkitExitFullscreen) {
await webkitExitFullscreen.call(document);
}
};
const requestEmbeddingFrameFullscreen = async (): Promise<boolean> => {
if (window.parent === window) return false;
try {
const frame = window.frameElement as
| (HTMLElement & {
webkitRequestFullscreen?: () => Promise<void>;
})
| null;
if (frame?.requestFullscreen) {
await frame.requestFullscreen();
return true;
}
if (frame?.webkitRequestFullscreen) {
await frame.webkitRequestFullscreen();
return true;
}
} catch {
// Cross-origin parents are not directly accessible from the iframe.
}
window.parent.postMessage(
{
type: 'tour-builder:request-fullscreen',
source: 'image-detail-panel',
},
'*',
);
return false;
};
const ImageDetailPanel: React.FC<ImageDetailPanelProps> = ({
element,
image,
@ -197,26 +138,11 @@ const ImageDetailPanel: React.FC<ImageDetailPanelProps> = ({
try {
// Enter fullscreen
if (panel.requestFullscreen) {
await panel.requestFullscreen();
} else if (
(
panel as HTMLDivElement & {
webkitRequestFullscreen?: () => Promise<void>;
}
).webkitRequestFullscreen
) {
// Safari fallback
await (
panel as HTMLDivElement & {
webkitRequestFullscreen: () => Promise<void>;
}
).webkitRequestFullscreen();
}
await requestElementFullscreen(panel);
} catch {
// Fullscreen can be blocked inside iframes. Try to fullscreen the
// embedding iframe before falling back to filling the iframe viewport.
await requestEmbeddingFrameFullscreen();
await requestEmbeddingFrameFullscreen('image-detail-panel');
} finally {
setIsFullscreen(true);
}

View File

@ -0,0 +1,111 @@
export const getFullscreenElement = (): Element | null => {
return (
document.fullscreenElement ??
(
document as Document & {
webkitFullscreenElement?: Element | null;
}
).webkitFullscreenElement ??
null
);
};
export const isNativeFullscreenElement = (element: Element | null): boolean =>
!!element && getFullscreenElement() === element;
export const exitNativeFullscreen = async (): Promise<void> => {
if (document.exitFullscreen) {
await document.exitFullscreen();
return;
}
const webkitExitFullscreen = (
document as Document & {
webkitExitFullscreen?: () => Promise<void>;
}
).webkitExitFullscreen;
if (webkitExitFullscreen) {
await webkitExitFullscreen.call(document);
}
};
const isEmbeddedWindow = (): boolean => window.parent !== window;
export const requestElementFullscreen = async (
element: HTMLElement,
): Promise<void> => {
if (element.requestFullscreen) {
await element.requestFullscreen();
return;
}
const webkitRequestFullscreen = (
element as HTMLElement & {
webkitRequestFullscreen?: () => Promise<void>;
}
).webkitRequestFullscreen;
if (webkitRequestFullscreen) {
await webkitRequestFullscreen.call(element);
return;
}
throw new Error('Fullscreen API is not available');
};
export const requestEmbeddingFrameFullscreen = async (
source = 'runtime',
): Promise<boolean> => {
if (!isEmbeddedWindow()) return false;
try {
const frame = window.frameElement as
| (HTMLElement & {
webkitRequestFullscreen?: () => Promise<void>;
})
| null;
if (frame) {
await requestElementFullscreen(frame);
return true;
}
} catch {
// Cross-origin parents are not directly accessible from the iframe.
}
window.parent.postMessage(
{
type: 'tour-builder:request-fullscreen',
source,
},
'*',
);
return true;
};
export const requestDocumentFullscreen = async (
source = 'runtime',
): Promise<boolean> => {
try {
await requestElementFullscreen(document.documentElement);
return true;
} catch {
return requestEmbeddingFrameFullscreen(source);
}
};
export const requestParentFullscreenExit = (source = 'runtime'): boolean => {
if (!isEmbeddedWindow()) return false;
window.parent.postMessage(
{
type: 'tour-builder:exit-fullscreen',
source,
},
'*',
);
return true;
};