182 lines
5.0 KiB
TypeScript
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;
|