39948-vm/frontend/src/hooks/useTransitionPreview.ts
2026-04-14 03:10:27 +00:00

182 lines
5.0 KiB
TypeScript

/**
* useTransitionPreview Hook
*
* Manages transition video preview state in the constructor.
* Used to preview forward and reverse transitions before navigation.
*/
import { useState, useCallback, useMemo } from 'react';
import type { TransitionPreviewState } from '../types/presentation';
export type { TransitionPreviewState };
/**
* Navigation element with transition configuration
*/
interface TransitionElement {
type: string;
label?: string;
navLabel?: string;
transitionVideoUrl?: string;
transitionReverseMode?: 'auto_reverse' | 'separate_video';
reverseVideoUrl?: string;
transitionDurationSec?: number;
}
interface UseTransitionPreviewOptions {
/** Function to check if element type is a navigation type */
isNavigationElementType: (type: string) => boolean;
/** Callback when error occurs (no video configured, etc.) */
onError?: (message: string) => void;
}
interface UseTransitionPreviewResult {
/** Current preview state (null if not previewing) */
preview: TransitionPreviewState | null;
/** Pending navigation page ID (used for actual navigation after preview) */
pendingPageId: string;
/** Open transition preview for an element */
openPreview: (
element: TransitionElement,
direction: 'forward' | 'back',
) => void;
/** Open transition preview with a specific target page */
openPreviewWithTarget: (
element: TransitionElement,
direction: 'forward' | 'back',
targetPageId: string,
) => void;
/** Close the preview */
closePreview: () => void;
/** Whether a preview is currently active */
isActive: boolean;
}
/**
* Hook for managing transition preview state.
* Handles opening previews for forward and back navigation.
*
* @example
* const {
* preview,
* pendingPageId,
* openPreview,
* closePreview,
* isActive,
* } = useTransitionPreview({
* isNavigationElementType,
* onError: setErrorMessage,
* });
*
* // Open preview when clicking element
* const handleClick = () => {
* openPreviewWithTarget(element, 'forward', targetPage.id);
* };
*
* // Use with useTransitionPlayback hook
* useTransitionPlayback({
* transition: preview,
* onComplete: (targetId) => {
* switchToPage(targetId);
* closePreview();
* },
* });
*/
export function useTransitionPreview({
isNavigationElementType,
onError,
}: UseTransitionPreviewOptions): UseTransitionPreviewResult {
const [preview, setPreview] = useState<TransitionPreviewState | null>(null);
const [pendingPageId, setPendingPageId] = useState('');
const openPreview = useCallback(
(element: TransitionElement, direction: 'forward' | 'back') => {
if (!isNavigationElementType(element.type)) {
return;
}
// Check if transition video is configured
if (!element.transitionVideoUrl) {
onError?.(
'Select transition video asset to preview transition playback.',
);
return;
}
// For back navigation, check if reversed video is available
// (either manually uploaded or backend-generated)
if (direction === 'back' && !element.reverseVideoUrl) {
onError?.(
'Reversed video not available. Save the page to generate it.',
);
return;
}
// Determine reverse mode:
// - 'none' for forward navigation
// - 'separate' for back navigation (uses pre-reversed video)
const reverseMode = direction === 'back' ? 'separate' : 'none';
const previewState: TransitionPreviewState = {
videoUrl: element.transitionVideoUrl,
storageKey: element.transitionVideoUrl, // Raw storage path for cache lookup
reverseMode,
reverseVideoUrl: element.reverseVideoUrl,
reverseStorageKey: element.reverseVideoUrl,
durationSec: element.transitionDurationSec,
title: `${element.navLabel || element.label || 'Transition'} · ${direction}`,
isBack: direction === 'back', // Track for history management
};
setPreview(previewState);
},
[isNavigationElementType, onError],
);
const openPreviewWithTarget = useCallback(
(
element: TransitionElement,
direction: 'forward' | 'back',
targetPageId: string,
) => {
setPendingPageId(targetPageId);
openPreview(element, direction);
},
[openPreview],
);
const closePreview = useCallback(() => {
setPreview(null);
setPendingPageId('');
}, []);
const isActive = useMemo(() => preview !== null, [preview]);
return {
preview,
pendingPageId,
openPreview,
openPreviewWithTarget,
closePreview,
isActive,
};
}
/**
* Get navigation direction from element configuration.
* Maps navType and element type to forward/back.
*/
export function getTransitionDirection(element: {
type: string;
navType?: 'forward' | 'back';
}): 'forward' | 'back' {
if (element.navType === 'back') return 'back';
if (element.navType === 'forward') return 'forward';
// Infer from element type
if (element.type === 'navigation_prev') return 'back';
return 'forward';
}
export default useTransitionPreview;