extended background video functionality
This commit is contained in:
parent
b5f8f30360
commit
eb712e86f2
@ -70,6 +70,26 @@ class Tour_pagesDBApi extends GenericDBApi {
|
|||||||
background_video_url: data.background_video_url || null,
|
background_video_url: data.background_video_url || null,
|
||||||
background_audio_url: data.background_audio_url || null,
|
background_audio_url: data.background_audio_url || null,
|
||||||
background_loop: data.background_loop || false,
|
background_loop: data.background_loop || false,
|
||||||
|
background_video_autoplay:
|
||||||
|
data.background_video_autoplay !== undefined
|
||||||
|
? data.background_video_autoplay
|
||||||
|
: true,
|
||||||
|
background_video_loop:
|
||||||
|
data.background_video_loop !== undefined
|
||||||
|
? data.background_video_loop
|
||||||
|
: true,
|
||||||
|
background_video_muted:
|
||||||
|
data.background_video_muted !== undefined
|
||||||
|
? data.background_video_muted
|
||||||
|
: true,
|
||||||
|
background_video_start_time:
|
||||||
|
data.background_video_start_time !== undefined
|
||||||
|
? data.background_video_start_time
|
||||||
|
: null,
|
||||||
|
background_video_end_time:
|
||||||
|
data.background_video_end_time !== undefined
|
||||||
|
? data.background_video_end_time
|
||||||
|
: null,
|
||||||
requires_auth: data.requires_auth || false,
|
requires_auth: data.requires_auth || false,
|
||||||
ui_schema_json: data.ui_schema_json || null,
|
ui_schema_json: data.ui_schema_json || null,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,40 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** @type {import('sequelize-cli').Migration} */
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.addColumn('tour_pages', 'background_video_autoplay', {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: true,
|
||||||
|
});
|
||||||
|
await queryInterface.addColumn('tour_pages', 'background_video_loop', {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: true,
|
||||||
|
});
|
||||||
|
await queryInterface.addColumn('tour_pages', 'background_video_muted', {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: true,
|
||||||
|
});
|
||||||
|
await queryInterface.addColumn('tour_pages', 'background_video_start_time', {
|
||||||
|
type: Sequelize.DECIMAL(10, 1),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
});
|
||||||
|
await queryInterface.addColumn('tour_pages', 'background_video_end_time', {
|
||||||
|
type: Sequelize.DECIMAL(10, 1),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, _Sequelize) {
|
||||||
|
await queryInterface.removeColumn('tour_pages', 'background_video_autoplay');
|
||||||
|
await queryInterface.removeColumn('tour_pages', 'background_video_loop');
|
||||||
|
await queryInterface.removeColumn('tour_pages', 'background_video_muted');
|
||||||
|
await queryInterface.removeColumn('tour_pages', 'background_video_start_time');
|
||||||
|
await queryInterface.removeColumn('tour_pages', 'background_video_end_time');
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -73,6 +73,36 @@ module.exports = function (sequelize, DataTypes) {
|
|||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
background_video_autoplay: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
background_video_loop: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
background_video_muted: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
background_video_start_time: {
|
||||||
|
type: DataTypes.DECIMAL(10, 1),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
background_video_end_time: {
|
||||||
|
type: DataTypes.DECIMAL(10, 1),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
|
||||||
requires_auth: {
|
requires_auth: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -3,12 +3,21 @@
|
|||||||
*
|
*
|
||||||
* Compact editor for background image, video, or audio settings.
|
* Compact editor for background image, video, or audio settings.
|
||||||
* Used in the element editor panel for background menu items.
|
* Used in the element editor panel for background menu items.
|
||||||
|
* For video backgrounds, includes playback settings (autoplay, loop, muted, start/end time).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { AssetOption } from './types';
|
import type { AssetOption } from './types';
|
||||||
import { addFallbackAssetOption } from '../../lib/constructorHelpers';
|
import { addFallbackAssetOption } from '../../lib/constructorHelpers';
|
||||||
|
|
||||||
|
export interface VideoPlaybackSettings {
|
||||||
|
autoplay?: boolean;
|
||||||
|
loop?: boolean;
|
||||||
|
muted?: boolean;
|
||||||
|
startTime?: number | null;
|
||||||
|
endTime?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
interface BackgroundSettingsEditorProps {
|
interface BackgroundSettingsEditorProps {
|
||||||
type: 'image' | 'video' | 'audio';
|
type: 'image' | 'video' | 'audio';
|
||||||
value: string;
|
value: string;
|
||||||
@ -16,6 +25,13 @@ interface BackgroundSettingsEditorProps {
|
|||||||
durationNote?: string;
|
durationNote?: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
onClearImage?: () => void;
|
onClearImage?: () => void;
|
||||||
|
// Video-specific playback settings (only used when type='video')
|
||||||
|
videoAutoplay?: boolean;
|
||||||
|
videoLoop?: boolean;
|
||||||
|
videoMuted?: boolean;
|
||||||
|
videoStartTime?: number | null;
|
||||||
|
videoEndTime?: number | null;
|
||||||
|
onVideoSettingsChange?: (settings: VideoPlaybackSettings) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LABELS: Record<string, string> = {
|
const LABELS: Record<string, string> = {
|
||||||
@ -31,6 +47,12 @@ const BackgroundSettingsEditor: React.FC<BackgroundSettingsEditorProps> = ({
|
|||||||
durationNote,
|
durationNote,
|
||||||
onChange,
|
onChange,
|
||||||
onClearImage,
|
onClearImage,
|
||||||
|
videoAutoplay,
|
||||||
|
videoLoop,
|
||||||
|
videoMuted,
|
||||||
|
videoStartTime,
|
||||||
|
videoEndTime,
|
||||||
|
onVideoSettingsChange,
|
||||||
}) => {
|
}) => {
|
||||||
const label = LABELS[type] || 'Background';
|
const label = LABELS[type] || 'Background';
|
||||||
const selectOptions = addFallbackAssetOption(
|
const selectOptions = addFallbackAssetOption(
|
||||||
@ -39,6 +61,9 @@ const BackgroundSettingsEditor: React.FC<BackgroundSettingsEditorProps> = ({
|
|||||||
`Current ${type} · ${value}`,
|
`Current ${type} · ${value}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Show video playback settings when type is video and a video is selected
|
||||||
|
const showVideoSettings = type === 'video' && value && onVideoSettingsChange;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
|
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>
|
||||||
@ -66,6 +91,92 @@ const BackgroundSettingsEditor: React.FC<BackgroundSettingsEditorProps> = ({
|
|||||||
{durationNote && (
|
{durationNote && (
|
||||||
<p className='mt-1 text-[11px] text-gray-500'>{durationNote}</p>
|
<p className='mt-1 text-[11px] text-gray-500'>{durationNote}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Video Playback Settings */}
|
||||||
|
{showVideoSettings && (
|
||||||
|
<div className='mt-3 space-y-2 border-t border-gray-200 pt-3'>
|
||||||
|
<p className='text-[10px] font-semibold uppercase text-gray-500'>
|
||||||
|
Playback Settings
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<label className='flex cursor-pointer items-center gap-2 text-[11px] text-gray-700'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='h-3 w-3 rounded border-gray-300'
|
||||||
|
checked={videoAutoplay ?? true}
|
||||||
|
onChange={(e) =>
|
||||||
|
onVideoSettingsChange({ autoplay: e.target.checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Autoplay
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className='flex cursor-pointer items-center gap-2 text-[11px] text-gray-700'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='h-3 w-3 rounded border-gray-300'
|
||||||
|
checked={videoLoop ?? true}
|
||||||
|
onChange={(e) =>
|
||||||
|
onVideoSettingsChange({ loop: e.target.checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Loop
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className='flex cursor-pointer items-center gap-2 text-[11px] text-gray-700'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='h-3 w-3 rounded border-gray-300'
|
||||||
|
checked={!(videoMuted ?? true)}
|
||||||
|
onChange={(e) =>
|
||||||
|
onVideoSettingsChange({ muted: !e.target.checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Sound
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<div className='flex-1'>
|
||||||
|
<label className='mb-1 block text-[10px] text-gray-500'>
|
||||||
|
Start (sec)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='number'
|
||||||
|
step='0.1'
|
||||||
|
min='0'
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||||
|
placeholder='0.0'
|
||||||
|
value={videoStartTime ?? ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
onVideoSettingsChange({
|
||||||
|
startTime: e.target.value
|
||||||
|
? parseFloat(e.target.value)
|
||||||
|
: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex-1'>
|
||||||
|
<label className='mb-1 block text-[10px] text-gray-500'>
|
||||||
|
End (sec)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='number'
|
||||||
|
step='0.1'
|
||||||
|
min='0'
|
||||||
|
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||||
|
placeholder='auto'
|
||||||
|
value={videoEndTime ?? ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
onVideoSettingsChange({
|
||||||
|
endTime: e.target.value ? parseFloat(e.target.value) : null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,10 +3,12 @@
|
|||||||
*
|
*
|
||||||
* Background image, video, and audio for the constructor canvas.
|
* Background image, video, and audio for the constructor canvas.
|
||||||
* Handles blob URLs, Next.js Image optimization, and previous background overlay.
|
* Handles blob URLs, Next.js Image optimization, and previous background overlay.
|
||||||
|
* Supports custom video playback settings (autoplay, loop, muted, start/end time).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import NextImage from 'next/image';
|
import NextImage from 'next/image';
|
||||||
|
import { useBackgroundVideoPlayback } from '../../hooks/useBackgroundVideoPlayback';
|
||||||
|
|
||||||
interface CanvasBackgroundProps {
|
interface CanvasBackgroundProps {
|
||||||
backgroundImageUrl?: string;
|
backgroundImageUrl?: string;
|
||||||
@ -16,6 +18,12 @@ interface CanvasBackgroundProps {
|
|||||||
isSwitching?: boolean;
|
isSwitching?: boolean;
|
||||||
isNewBgReady?: boolean;
|
isNewBgReady?: boolean;
|
||||||
onBackgroundReady?: () => void;
|
onBackgroundReady?: () => void;
|
||||||
|
// Video playback settings
|
||||||
|
videoAutoplay?: boolean;
|
||||||
|
videoLoop?: boolean;
|
||||||
|
videoMuted?: boolean;
|
||||||
|
videoStartTime?: number | null;
|
||||||
|
videoEndTime?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CanvasBackground: React.FC<CanvasBackgroundProps> = ({
|
const CanvasBackground: React.FC<CanvasBackgroundProps> = ({
|
||||||
@ -26,7 +34,22 @@ const CanvasBackground: React.FC<CanvasBackgroundProps> = ({
|
|||||||
isSwitching = false,
|
isSwitching = false,
|
||||||
isNewBgReady = false,
|
isNewBgReady = false,
|
||||||
onBackgroundReady,
|
onBackgroundReady,
|
||||||
|
videoAutoplay = true,
|
||||||
|
videoLoop = true,
|
||||||
|
videoMuted = true,
|
||||||
|
videoStartTime = null,
|
||||||
|
videoEndTime = null,
|
||||||
}) => {
|
}) => {
|
||||||
|
// Use background video playback hook for custom start/end time handling
|
||||||
|
const { videoRef } = useBackgroundVideoPlayback({
|
||||||
|
videoUrl: backgroundVideoUrl,
|
||||||
|
autoplay: videoAutoplay,
|
||||||
|
loop: videoLoop,
|
||||||
|
muted: videoMuted,
|
||||||
|
startTime: videoStartTime,
|
||||||
|
endTime: videoEndTime,
|
||||||
|
});
|
||||||
|
|
||||||
const handleLoad = () => {
|
const handleLoad = () => {
|
||||||
onBackgroundReady?.();
|
onBackgroundReady?.();
|
||||||
};
|
};
|
||||||
@ -35,18 +58,21 @@ const CanvasBackground: React.FC<CanvasBackgroundProps> = ({
|
|||||||
onBackgroundReady?.();
|
onBackgroundReady?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// When endTime is set, we disable native loop and handle it via the hook
|
||||||
|
const useNativeLoop = videoEndTime == null ? videoLoop : false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Background image - z-1 keeps it below backdrop blur layer (z-5) */}
|
{/* Background image - z-1 keeps it below backdrop blur layer (z-5) */}
|
||||||
{backgroundImageUrl && (
|
{backgroundImageUrl && (
|
||||||
<div className='absolute inset-0 z-1 h-full w-full pointer-events-none select-none'>
|
<div className='pointer-events-none absolute inset-0 z-1 h-full w-full select-none'>
|
||||||
{backgroundImageUrl.startsWith('blob:') ? (
|
{backgroundImageUrl.startsWith('blob:') ? (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img
|
<img
|
||||||
key={`bg_image_${backgroundImageUrl}`}
|
key={`bg_image_${backgroundImageUrl}`}
|
||||||
src={backgroundImageUrl}
|
src={backgroundImageUrl}
|
||||||
alt='Background'
|
alt='Background'
|
||||||
className='absolute inset-0 w-full h-full object-cover'
|
className='absolute inset-0 h-full w-full object-cover'
|
||||||
draggable={false}
|
draggable={false}
|
||||||
onLoad={handleLoad}
|
onLoad={handleLoad}
|
||||||
onError={handleError}
|
onError={handleError}
|
||||||
@ -71,7 +97,7 @@ const CanvasBackground: React.FC<CanvasBackgroundProps> = ({
|
|||||||
{/* Previous background overlay - shows during page switch until new bg is ready */}
|
{/* Previous background overlay - shows during page switch until new bg is ready */}
|
||||||
{previousBgImageUrl && isSwitching && !isNewBgReady && (
|
{previousBgImageUrl && isSwitching && !isNewBgReady && (
|
||||||
<div
|
<div
|
||||||
className='absolute inset-0 pointer-events-none z-10'
|
className='pointer-events-none absolute inset-0 z-10'
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url("${previousBgImageUrl}")`,
|
backgroundImage: `url("${previousBgImageUrl}")`,
|
||||||
backgroundSize: 'cover',
|
backgroundSize: 'cover',
|
||||||
@ -83,12 +109,13 @@ const CanvasBackground: React.FC<CanvasBackgroundProps> = ({
|
|||||||
{/* Background video - z-1 keeps it below backdrop blur layer (z-5) */}
|
{/* Background video - z-1 keeps it below backdrop blur layer (z-5) */}
|
||||||
{backgroundVideoUrl && (
|
{backgroundVideoUrl && (
|
||||||
<video
|
<video
|
||||||
|
ref={videoRef}
|
||||||
key={`bg_video_${backgroundVideoUrl}`}
|
key={`bg_video_${backgroundVideoUrl}`}
|
||||||
className='absolute inset-0 z-1 w-full h-full object-cover'
|
className='absolute inset-0 z-1 h-full w-full object-cover'
|
||||||
src={backgroundVideoUrl}
|
src={backgroundVideoUrl}
|
||||||
autoPlay
|
autoPlay={videoAutoplay}
|
||||||
loop
|
loop={useNativeLoop}
|
||||||
muted
|
muted={videoMuted}
|
||||||
playsInline
|
playsInline
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -94,6 +94,20 @@ interface ElementEditorPanelProps {
|
|||||||
onBackgroundVideoChange: (value: string) => void;
|
onBackgroundVideoChange: (value: string) => void;
|
||||||
onBackgroundAudioChange: (value: string) => void;
|
onBackgroundAudioChange: (value: string) => void;
|
||||||
|
|
||||||
|
// Background video playback settings
|
||||||
|
backgroundVideoAutoplay: boolean;
|
||||||
|
backgroundVideoLoop: boolean;
|
||||||
|
backgroundVideoMuted: boolean;
|
||||||
|
backgroundVideoStartTime: number | null;
|
||||||
|
backgroundVideoEndTime: number | null;
|
||||||
|
onBackgroundVideoSettingsChange: (settings: {
|
||||||
|
autoplay?: boolean;
|
||||||
|
loop?: boolean;
|
||||||
|
muted?: boolean;
|
||||||
|
startTime?: number | null;
|
||||||
|
endTime?: number | null;
|
||||||
|
}) => void;
|
||||||
|
|
||||||
// Transition creation
|
// Transition creation
|
||||||
newTransitionName: string;
|
newTransitionName: string;
|
||||||
newTransitionVideoUrl: string;
|
newTransitionVideoUrl: string;
|
||||||
@ -220,6 +234,12 @@ export function ElementEditorPanel({
|
|||||||
onBackgroundImageChange,
|
onBackgroundImageChange,
|
||||||
onBackgroundVideoChange,
|
onBackgroundVideoChange,
|
||||||
onBackgroundAudioChange,
|
onBackgroundAudioChange,
|
||||||
|
backgroundVideoAutoplay,
|
||||||
|
backgroundVideoLoop,
|
||||||
|
backgroundVideoMuted,
|
||||||
|
backgroundVideoStartTime,
|
||||||
|
backgroundVideoEndTime,
|
||||||
|
onBackgroundVideoSettingsChange,
|
||||||
newTransitionName,
|
newTransitionName,
|
||||||
newTransitionVideoUrl,
|
newTransitionVideoUrl,
|
||||||
newTransitionSupportsReverse,
|
newTransitionSupportsReverse,
|
||||||
@ -288,6 +308,12 @@ export function ElementEditorPanel({
|
|||||||
onBackgroundVideoChange(value);
|
onBackgroundVideoChange(value);
|
||||||
if (value) onBackgroundImageChange('');
|
if (value) onBackgroundImageChange('');
|
||||||
}}
|
}}
|
||||||
|
videoAutoplay={backgroundVideoAutoplay}
|
||||||
|
videoLoop={backgroundVideoLoop}
|
||||||
|
videoMuted={backgroundVideoMuted}
|
||||||
|
videoStartTime={backgroundVideoStartTime}
|
||||||
|
videoEndTime={backgroundVideoEndTime}
|
||||||
|
onVideoSettingsChange={onBackgroundVideoSettingsChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -50,6 +50,12 @@ export interface TourPage {
|
|||||||
background_video_url?: string;
|
background_video_url?: string;
|
||||||
background_audio_url?: string;
|
background_audio_url?: string;
|
||||||
background_loop?: boolean;
|
background_loop?: boolean;
|
||||||
|
// Background video playback settings
|
||||||
|
background_video_autoplay?: boolean;
|
||||||
|
background_video_loop?: boolean;
|
||||||
|
background_video_muted?: boolean;
|
||||||
|
background_video_start_time?: number | null;
|
||||||
|
background_video_end_time?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -127,6 +133,12 @@ export interface CanvasBackgroundProps {
|
|||||||
isSwitching?: boolean;
|
isSwitching?: boolean;
|
||||||
isNewBgReady?: boolean;
|
isNewBgReady?: boolean;
|
||||||
onBackgroundReady?: () => void;
|
onBackgroundReady?: () => void;
|
||||||
|
// Video playback settings
|
||||||
|
videoAutoplay?: boolean;
|
||||||
|
videoLoop?: boolean;
|
||||||
|
videoMuted?: boolean;
|
||||||
|
videoStartTime?: number | null;
|
||||||
|
videoEndTime?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -161,6 +173,17 @@ export interface ElementEditorHeaderProps {
|
|||||||
onDragStart: (event: React.MouseEvent) => void;
|
onDragStart: (event: React.MouseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Video playback settings for background video
|
||||||
|
*/
|
||||||
|
export interface VideoPlaybackSettings {
|
||||||
|
autoplay?: boolean;
|
||||||
|
loop?: boolean;
|
||||||
|
muted?: boolean;
|
||||||
|
startTime?: number | null;
|
||||||
|
endTime?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Background settings editor props
|
* Background settings editor props
|
||||||
*/
|
*/
|
||||||
@ -171,6 +194,13 @@ export interface BackgroundSettingsEditorProps {
|
|||||||
durationNote?: string;
|
durationNote?: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
onImageClear?: () => void;
|
onImageClear?: () => void;
|
||||||
|
// Video-specific playback settings (only used when type='video')
|
||||||
|
videoAutoplay?: boolean;
|
||||||
|
videoLoop?: boolean;
|
||||||
|
videoMuted?: boolean;
|
||||||
|
videoStartTime?: number | null;
|
||||||
|
videoEndTime?: number | null;
|
||||||
|
onVideoSettingsChange?: (settings: VideoPlaybackSettings) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -209,6 +239,14 @@ export interface ElementEditorPanelProps {
|
|||||||
backgroundVideoDurationNote: string;
|
backgroundVideoDurationNote: string;
|
||||||
backgroundAudioDurationNote: string;
|
backgroundAudioDurationNote: string;
|
||||||
|
|
||||||
|
// Background video playback settings
|
||||||
|
backgroundVideoAutoplay: boolean;
|
||||||
|
backgroundVideoLoop: boolean;
|
||||||
|
backgroundVideoMuted: boolean;
|
||||||
|
backgroundVideoStartTime: number | null;
|
||||||
|
backgroundVideoEndTime: number | null;
|
||||||
|
onBackgroundVideoSettingsChange: (settings: VideoPlaybackSettings) => void;
|
||||||
|
|
||||||
// Transition form
|
// Transition form
|
||||||
newTransitionName: string;
|
newTransitionName: string;
|
||||||
newTransitionVideoUrl: string;
|
newTransitionVideoUrl: string;
|
||||||
|
|||||||
@ -333,23 +333,19 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) {
|
|||||||
galleryTitleFontFamily: String(settings.galleryTitleFontFamily || ''),
|
galleryTitleFontFamily: String(settings.galleryTitleFontFamily || ''),
|
||||||
galleryTextFontFamily: String(settings.galleryTextFontFamily || ''),
|
galleryTextFontFamily: String(settings.galleryTextFontFamily || ''),
|
||||||
galleryCards: Array.isArray(settings.galleryCards)
|
galleryCards: Array.isArray(settings.galleryCards)
|
||||||
? settings.galleryCards.map(
|
? settings.galleryCards.map((card: Record<string, unknown>) => ({
|
||||||
(card: Record<string, unknown>) => ({
|
id: String(card?.id || createLocalId()),
|
||||||
id: String(card?.id || createLocalId()),
|
imageUrl: String(card?.imageUrl ?? ''),
|
||||||
imageUrl: String(card?.imageUrl ?? ''),
|
title: String(card?.title ?? ''),
|
||||||
title: String(card?.title ?? ''),
|
description: String(card?.description ?? ''),
|
||||||
description: String(card?.description ?? ''),
|
}))
|
||||||
}),
|
|
||||||
)
|
|
||||||
: [],
|
: [],
|
||||||
carouselSlides: Array.isArray(settings.carouselSlides)
|
carouselSlides: Array.isArray(settings.carouselSlides)
|
||||||
? settings.carouselSlides.map(
|
? settings.carouselSlides.map((slide: Record<string, unknown>) => ({
|
||||||
(slide: Record<string, unknown>) => ({
|
id: String(slide?.id || createLocalId()),
|
||||||
id: String(slide?.id || createLocalId()),
|
imageUrl: String(slide?.imageUrl ?? ''),
|
||||||
imageUrl: String(slide?.imageUrl ?? ''),
|
caption: String(slide?.caption ?? ''),
|
||||||
caption: String(slide?.caption ?? ''),
|
}))
|
||||||
}),
|
|
||||||
)
|
|
||||||
: [],
|
: [],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import { extractPageLinksAndElements } from '../lib/extractPageLinks';
|
|||||||
import { usePageSwitch } from '../hooks/usePageSwitch';
|
import { usePageSwitch } from '../hooks/usePageSwitch';
|
||||||
import { useTransitionPlayback } from '../hooks/useTransitionPlayback';
|
import { useTransitionPlayback } from '../hooks/useTransitionPlayback';
|
||||||
import { useBackgroundTransition } from '../hooks/useBackgroundTransition';
|
import { useBackgroundTransition } from '../hooks/useBackgroundTransition';
|
||||||
|
import { useBackgroundVideoPlayback } from '../hooks/useBackgroundVideoPlayback';
|
||||||
import { resolveAssetPlaybackUrl } from '../lib/assetUrl';
|
import { resolveAssetPlaybackUrl } from '../lib/assetUrl';
|
||||||
import { logger } from '../lib/logger';
|
import { logger } from '../lib/logger';
|
||||||
import {
|
import {
|
||||||
@ -370,6 +371,32 @@ export default function RuntimePresentation({
|
|||||||
const backgroundImageUrl = pageSwitch.currentBgImageUrl;
|
const backgroundImageUrl = pageSwitch.currentBgImageUrl;
|
||||||
const backgroundVideoUrl = pageSwitch.currentBgVideoUrl;
|
const backgroundVideoUrl = pageSwitch.currentBgVideoUrl;
|
||||||
|
|
||||||
|
// Background video playback settings from selected page
|
||||||
|
const videoAutoplay = selectedPage?.background_video_autoplay ?? true;
|
||||||
|
const videoLoop = selectedPage?.background_video_loop ?? true;
|
||||||
|
const videoMuted = selectedPage?.background_video_muted ?? true;
|
||||||
|
const videoStartTime =
|
||||||
|
selectedPage?.background_video_start_time != null
|
||||||
|
? parseFloat(String(selectedPage.background_video_start_time))
|
||||||
|
: null;
|
||||||
|
const videoEndTime =
|
||||||
|
selectedPage?.background_video_end_time != null
|
||||||
|
? parseFloat(String(selectedPage.background_video_end_time))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Use background video playback hook for custom start/end time handling
|
||||||
|
const { videoRef: bgVideoRef } = useBackgroundVideoPlayback({
|
||||||
|
videoUrl: backgroundVideoUrl,
|
||||||
|
autoplay: videoAutoplay,
|
||||||
|
loop: videoLoop,
|
||||||
|
muted: videoMuted,
|
||||||
|
startTime: videoStartTime,
|
||||||
|
endTime: videoEndTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
// When endTime is set, we disable native loop and handle it via the hook
|
||||||
|
const useNativeLoop = videoEndTime == null ? videoLoop : false;
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center justify-center min-h-screen bg-gray-900'>
|
<div className='flex items-center justify-center min-h-screen bg-gray-900'>
|
||||||
@ -502,12 +529,13 @@ export default function RuntimePresentation({
|
|||||||
{/* Background video - z-1 keeps it below backdrop blur (z-5) */}
|
{/* Background video - z-1 keeps it below backdrop blur (z-5) */}
|
||||||
{backgroundVideoUrl && (
|
{backgroundVideoUrl && (
|
||||||
<video
|
<video
|
||||||
|
ref={bgVideoRef}
|
||||||
key={backgroundVideoUrl}
|
key={backgroundVideoUrl}
|
||||||
className='absolute inset-0 z-1 w-full h-full object-cover'
|
className='absolute inset-0 z-1 h-full w-full object-cover'
|
||||||
src={backgroundVideoUrl}
|
src={backgroundVideoUrl}
|
||||||
autoPlay
|
autoPlay={videoAutoplay}
|
||||||
loop
|
loop={useNativeLoop}
|
||||||
muted
|
muted={videoMuted}
|
||||||
playsInline
|
playsInline
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -26,6 +26,11 @@ export type {
|
|||||||
UseBackgroundTransitionOptions,
|
UseBackgroundTransitionOptions,
|
||||||
UseBackgroundTransitionResult,
|
UseBackgroundTransitionResult,
|
||||||
} from './useBackgroundTransition';
|
} from './useBackgroundTransition';
|
||||||
|
export { useBackgroundVideoPlayback } from './useBackgroundVideoPlayback';
|
||||||
|
export type {
|
||||||
|
UseBackgroundVideoPlaybackOptions,
|
||||||
|
UseBackgroundVideoPlaybackResult,
|
||||||
|
} from './useBackgroundVideoPlayback';
|
||||||
export { usePageDataLoader } from './usePageDataLoader';
|
export { usePageDataLoader } from './usePageDataLoader';
|
||||||
export type {
|
export type {
|
||||||
UsePageDataLoaderOptions,
|
UsePageDataLoaderOptions,
|
||||||
|
|||||||
180
frontend/src/hooks/useBackgroundVideoPlayback.ts
Normal file
180
frontend/src/hooks/useBackgroundVideoPlayback.ts
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
/**
|
||||||
|
* useBackgroundVideoPlayback Hook
|
||||||
|
*
|
||||||
|
* Manages background video playback with custom start/end times.
|
||||||
|
* Follows patterns from useTransitionPlayback for video time control.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useRef, useCallback, type RefObject } from 'react';
|
||||||
|
import { logger } from '../lib/logger';
|
||||||
|
|
||||||
|
export interface UseBackgroundVideoPlaybackOptions {
|
||||||
|
/** URL of the video to play */
|
||||||
|
videoUrl?: string;
|
||||||
|
/** Whether to autoplay the video (default: true) */
|
||||||
|
autoplay?: boolean;
|
||||||
|
/** Whether to loop the video (default: true) */
|
||||||
|
loop?: boolean;
|
||||||
|
/** Whether the video is muted (default: true) */
|
||||||
|
muted?: boolean;
|
||||||
|
/** Start time in seconds (default: null = start from beginning) */
|
||||||
|
startTime?: number | null;
|
||||||
|
/** End time in seconds (default: null = play to end) */
|
||||||
|
endTime?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseBackgroundVideoPlaybackResult {
|
||||||
|
/** Ref to attach to the video element */
|
||||||
|
videoRef: RefObject<HTMLVideoElement | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for managing background video playback with custom start/end times.
|
||||||
|
*
|
||||||
|
* When startTime is set, the video will seek to that position on load.
|
||||||
|
* When endTime is set, the video will either loop back to startTime or pause,
|
||||||
|
* depending on the loop setting.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { videoRef } = useBackgroundVideoPlayback({
|
||||||
|
* videoUrl: 'https://example.com/video.mp4',
|
||||||
|
* autoplay: true,
|
||||||
|
* loop: true,
|
||||||
|
* muted: true,
|
||||||
|
* startTime: 2.5,
|
||||||
|
* endTime: 10.0,
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* return <video ref={videoRef} src={videoUrl} />;
|
||||||
|
*/
|
||||||
|
export function useBackgroundVideoPlayback({
|
||||||
|
videoUrl,
|
||||||
|
autoplay = true,
|
||||||
|
loop = true,
|
||||||
|
muted = true,
|
||||||
|
startTime = null,
|
||||||
|
endTime = null,
|
||||||
|
}: UseBackgroundVideoPlaybackOptions): UseBackgroundVideoPlaybackResult {
|
||||||
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||||
|
// Store current values in refs for event handlers to access
|
||||||
|
const startTimeRef = useRef(startTime);
|
||||||
|
const endTimeRef = useRef(endTime);
|
||||||
|
const loopRef = useRef(loop);
|
||||||
|
const autoplayRef = useRef(autoplay);
|
||||||
|
|
||||||
|
// Update refs when values change
|
||||||
|
useEffect(() => {
|
||||||
|
startTimeRef.current = startTime;
|
||||||
|
}, [startTime]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
endTimeRef.current = endTime;
|
||||||
|
}, [endTime]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loopRef.current = loop;
|
||||||
|
}, [loop]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
autoplayRef.current = autoplay;
|
||||||
|
}, [autoplay]);
|
||||||
|
|
||||||
|
// Seek to start time when specified and video is ready
|
||||||
|
const seekToStartTime = useCallback(() => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
const st = startTimeRef.current;
|
||||||
|
if (!video || st == null || st <= 0) return;
|
||||||
|
|
||||||
|
video.currentTime = st;
|
||||||
|
logger.info('Background video: seeking to start time', { startTime: st });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handle start time changes - seek immediately when startTime changes
|
||||||
|
useEffect(() => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
if (!video || !videoUrl) return;
|
||||||
|
|
||||||
|
// If video has loaded metadata, seek immediately
|
||||||
|
if (video.readyState >= 1 && startTime != null && startTime > 0) {
|
||||||
|
video.currentTime = startTime;
|
||||||
|
logger.info('Background video: seeking to start time', { startTime });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up listener for initial load (if not loaded yet)
|
||||||
|
const handleLoadedMetadata = () => {
|
||||||
|
seekToStartTime();
|
||||||
|
};
|
||||||
|
|
||||||
|
video.addEventListener('loadedmetadata', handleLoadedMetadata);
|
||||||
|
return () => {
|
||||||
|
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||||
|
};
|
||||||
|
}, [videoUrl, startTime, seekToStartTime]);
|
||||||
|
|
||||||
|
// Handle autoplay state changes
|
||||||
|
useEffect(() => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
if (!video || !videoUrl) return;
|
||||||
|
|
||||||
|
if (autoplay) {
|
||||||
|
video.play().catch((error) => {
|
||||||
|
// Autoplay blocked by browser - this is expected behavior
|
||||||
|
logger.info('Background video autoplay blocked by browser', {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
video.pause();
|
||||||
|
}
|
||||||
|
}, [videoUrl, autoplay]);
|
||||||
|
|
||||||
|
// Handle end time enforcement via timeupdate
|
||||||
|
useEffect(() => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
if (!video || !videoUrl) return;
|
||||||
|
|
||||||
|
const handleTimeUpdate = () => {
|
||||||
|
const currentEndTime = endTimeRef.current;
|
||||||
|
const currentLoop = loopRef.current;
|
||||||
|
const currentStartTime = startTimeRef.current;
|
||||||
|
|
||||||
|
// Skip if no end time is set
|
||||||
|
if (currentEndTime == null) return;
|
||||||
|
|
||||||
|
// Check if we've reached or passed the end time
|
||||||
|
if (video.currentTime >= currentEndTime) {
|
||||||
|
if (currentLoop) {
|
||||||
|
// Loop back to start time (or beginning)
|
||||||
|
const loopTarget = currentStartTime ?? 0;
|
||||||
|
video.currentTime = loopTarget;
|
||||||
|
logger.info('Background video: looping back', {
|
||||||
|
from: currentEndTime,
|
||||||
|
to: loopTarget,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Pause at end time
|
||||||
|
video.pause();
|
||||||
|
logger.info('Background video: paused at end time', {
|
||||||
|
endTime: currentEndTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
video.addEventListener('timeupdate', handleTimeUpdate);
|
||||||
|
return () => {
|
||||||
|
video.removeEventListener('timeupdate', handleTimeUpdate);
|
||||||
|
};
|
||||||
|
}, [videoUrl]);
|
||||||
|
|
||||||
|
// Handle muted state changes
|
||||||
|
useEffect(() => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
if (!video) return;
|
||||||
|
video.muted = muted;
|
||||||
|
}, [muted]);
|
||||||
|
|
||||||
|
return { videoRef };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useBackgroundVideoPlayback;
|
||||||
@ -25,6 +25,12 @@ interface TourPage {
|
|||||||
background_video_url?: string;
|
background_video_url?: string;
|
||||||
background_audio_url?: string;
|
background_audio_url?: string;
|
||||||
background_loop?: boolean;
|
background_loop?: boolean;
|
||||||
|
// Background video playback settings
|
||||||
|
background_video_autoplay?: boolean;
|
||||||
|
background_video_loop?: boolean;
|
||||||
|
background_video_muted?: boolean;
|
||||||
|
background_video_start_time?: number | null;
|
||||||
|
background_video_end_time?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseConstructorPageActionsOptions {
|
interface UseConstructorPageActionsOptions {
|
||||||
@ -42,6 +48,12 @@ interface UseConstructorPageActionsOptions {
|
|||||||
backgroundImageUrl: string;
|
backgroundImageUrl: string;
|
||||||
backgroundVideoUrl: string;
|
backgroundVideoUrl: string;
|
||||||
backgroundAudioUrl: string;
|
backgroundAudioUrl: string;
|
||||||
|
/** Background video playback settings */
|
||||||
|
backgroundVideoAutoplay: boolean;
|
||||||
|
backgroundVideoLoop: boolean;
|
||||||
|
backgroundVideoMuted: boolean;
|
||||||
|
backgroundVideoStartTime: number | null;
|
||||||
|
backgroundVideoEndTime: number | null;
|
||||||
/** Callback to reload data after operations */
|
/** Callback to reload data after operations */
|
||||||
onReload: (preservePageId?: string) => Promise<void>;
|
onReload: (preservePageId?: string) => Promise<void>;
|
||||||
/** Callback to set active page ID */
|
/** Callback to set active page ID */
|
||||||
@ -111,6 +123,11 @@ export function useConstructorPageActions({
|
|||||||
backgroundImageUrl,
|
backgroundImageUrl,
|
||||||
backgroundVideoUrl,
|
backgroundVideoUrl,
|
||||||
backgroundAudioUrl,
|
backgroundAudioUrl,
|
||||||
|
backgroundVideoAutoplay,
|
||||||
|
backgroundVideoLoop,
|
||||||
|
backgroundVideoMuted,
|
||||||
|
backgroundVideoStartTime,
|
||||||
|
backgroundVideoEndTime,
|
||||||
onReload,
|
onReload,
|
||||||
onSetActivePageId,
|
onSetActivePageId,
|
||||||
onSetMenuOpen,
|
onSetMenuOpen,
|
||||||
@ -154,6 +171,11 @@ export function useConstructorPageActions({
|
|||||||
background_video_url: backgroundVideoUrl,
|
background_video_url: backgroundVideoUrl,
|
||||||
background_audio_url: backgroundAudioUrl,
|
background_audio_url: backgroundAudioUrl,
|
||||||
background_loop: Boolean(backgroundAudioUrl),
|
background_loop: Boolean(backgroundAudioUrl),
|
||||||
|
background_video_autoplay: backgroundVideoAutoplay,
|
||||||
|
background_video_loop: backgroundVideoLoop,
|
||||||
|
background_video_muted: backgroundVideoMuted,
|
||||||
|
background_video_start_time: backgroundVideoStartTime,
|
||||||
|
background_video_end_time: backgroundVideoEndTime,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -186,6 +208,11 @@ export function useConstructorPageActions({
|
|||||||
backgroundAudioUrl,
|
backgroundAudioUrl,
|
||||||
backgroundImageUrl,
|
backgroundImageUrl,
|
||||||
backgroundVideoUrl,
|
backgroundVideoUrl,
|
||||||
|
backgroundVideoAutoplay,
|
||||||
|
backgroundVideoLoop,
|
||||||
|
backgroundVideoMuted,
|
||||||
|
backgroundVideoStartTime,
|
||||||
|
backgroundVideoEndTime,
|
||||||
elements,
|
elements,
|
||||||
onError,
|
onError,
|
||||||
onReload,
|
onReload,
|
||||||
|
|||||||
@ -29,6 +29,12 @@ export interface SwitchablePage {
|
|||||||
background_image_url?: string;
|
background_image_url?: string;
|
||||||
background_video_url?: string;
|
background_video_url?: string;
|
||||||
background_audio_url?: string;
|
background_audio_url?: string;
|
||||||
|
// Background video playback settings
|
||||||
|
background_video_autoplay?: boolean;
|
||||||
|
background_video_loop?: boolean;
|
||||||
|
background_video_muted?: boolean;
|
||||||
|
background_video_start_time?: number | null;
|
||||||
|
background_video_end_time?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -98,6 +98,12 @@ type TourPage = {
|
|||||||
background_video_url?: string;
|
background_video_url?: string;
|
||||||
background_audio_url?: string;
|
background_audio_url?: string;
|
||||||
background_loop?: boolean;
|
background_loop?: boolean;
|
||||||
|
// Background video playback settings
|
||||||
|
background_video_autoplay?: boolean;
|
||||||
|
background_video_loop?: boolean;
|
||||||
|
background_video_muted?: boolean;
|
||||||
|
background_video_start_time?: number | null;
|
||||||
|
background_video_end_time?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type NavigationElementType = Extract<
|
type NavigationElementType = Extract<
|
||||||
@ -165,6 +171,16 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
const [backgroundImageUrl, setBackgroundImageUrl] = useState('');
|
const [backgroundImageUrl, setBackgroundImageUrl] = useState('');
|
||||||
const [backgroundVideoUrl, setBackgroundVideoUrl] = useState('');
|
const [backgroundVideoUrl, setBackgroundVideoUrl] = useState('');
|
||||||
const [backgroundAudioUrl, setBackgroundAudioUrl] = useState('');
|
const [backgroundAudioUrl, setBackgroundAudioUrl] = useState('');
|
||||||
|
// Background video playback settings
|
||||||
|
const [backgroundVideoAutoplay, setBackgroundVideoAutoplay] = useState(true);
|
||||||
|
const [backgroundVideoLoop, setBackgroundVideoLoop] = useState(true);
|
||||||
|
const [backgroundVideoMuted, setBackgroundVideoMuted] = useState(true);
|
||||||
|
const [backgroundVideoStartTime, setBackgroundVideoStartTime] = useState<
|
||||||
|
number | null
|
||||||
|
>(null);
|
||||||
|
const [backgroundVideoEndTime, setBackgroundVideoEndTime] = useState<
|
||||||
|
number | null
|
||||||
|
>(null);
|
||||||
const [selectedMenuItem, setSelectedMenuItem] =
|
const [selectedMenuItem, setSelectedMenuItem] =
|
||||||
useState<EditorMenuItem>('none');
|
useState<EditorMenuItem>('none');
|
||||||
// Transition preview state managed by useTransitionPreview hook (below)
|
// Transition preview state managed by useTransitionPreview hook (below)
|
||||||
@ -340,6 +356,21 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
setBackgroundImageUrl(page?.background_image_url || '');
|
setBackgroundImageUrl(page?.background_image_url || '');
|
||||||
setBackgroundVideoUrl(page?.background_video_url || '');
|
setBackgroundVideoUrl(page?.background_video_url || '');
|
||||||
setBackgroundAudioUrl(page?.background_audio_url || '');
|
setBackgroundAudioUrl(page?.background_audio_url || '');
|
||||||
|
// Set background video playback settings
|
||||||
|
setBackgroundVideoAutoplay(page?.background_video_autoplay ?? true);
|
||||||
|
setBackgroundVideoLoop(page?.background_video_loop ?? true);
|
||||||
|
setBackgroundVideoMuted(page?.background_video_muted ?? true);
|
||||||
|
// Parse DECIMAL strings from database to numbers
|
||||||
|
setBackgroundVideoStartTime(
|
||||||
|
page?.background_video_start_time != null
|
||||||
|
? parseFloat(String(page.background_video_start_time))
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
setBackgroundVideoEndTime(
|
||||||
|
page?.background_video_end_time != null
|
||||||
|
? parseFloat(String(page.background_video_end_time))
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
|
||||||
// Use hook to resolve and set blob URLs for display
|
// Use hook to resolve and set blob URLs for display
|
||||||
await pageSwitch.switchToPage(
|
await pageSwitch.switchToPage(
|
||||||
@ -735,6 +766,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
backgroundImageUrl,
|
backgroundImageUrl,
|
||||||
backgroundVideoUrl,
|
backgroundVideoUrl,
|
||||||
backgroundAudioUrl,
|
backgroundAudioUrl,
|
||||||
|
backgroundVideoAutoplay,
|
||||||
|
backgroundVideoLoop,
|
||||||
|
backgroundVideoMuted,
|
||||||
|
backgroundVideoStartTime,
|
||||||
|
backgroundVideoEndTime,
|
||||||
onReload: loadData,
|
onReload: loadData,
|
||||||
onSetActivePageId: setActivePageId,
|
onSetActivePageId: setActivePageId,
|
||||||
onSetMenuOpen: setIsMenuOpen,
|
onSetMenuOpen: setIsMenuOpen,
|
||||||
@ -1023,6 +1059,21 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
setBackgroundImageUrl(activePage.background_image_url || '');
|
setBackgroundImageUrl(activePage.background_image_url || '');
|
||||||
setBackgroundVideoUrl(activePage.background_video_url || '');
|
setBackgroundVideoUrl(activePage.background_video_url || '');
|
||||||
setBackgroundAudioUrl(activePage.background_audio_url || '');
|
setBackgroundAudioUrl(activePage.background_audio_url || '');
|
||||||
|
// Set background video playback settings
|
||||||
|
setBackgroundVideoAutoplay(activePage.background_video_autoplay ?? true);
|
||||||
|
setBackgroundVideoLoop(activePage.background_video_loop ?? true);
|
||||||
|
setBackgroundVideoMuted(activePage.background_video_muted ?? true);
|
||||||
|
// Parse DECIMAL strings from database to numbers
|
||||||
|
setBackgroundVideoStartTime(
|
||||||
|
activePage.background_video_start_time != null
|
||||||
|
? parseFloat(String(activePage.background_video_start_time))
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
setBackgroundVideoEndTime(
|
||||||
|
activePage.background_video_end_time != null
|
||||||
|
? parseFloat(String(activePage.background_video_end_time))
|
||||||
|
: null,
|
||||||
|
);
|
||||||
// Resolve blob URLs via hook for display (handles initial load and route changes)
|
// Resolve blob URLs via hook for display (handles initial load and route changes)
|
||||||
// Only call if this page wasn't already initialized via switchToPage function
|
// Only call if this page wasn't already initialized via switchToPage function
|
||||||
if (lastInitializedPageIdRef.current !== activePage.id) {
|
if (lastInitializedPageIdRef.current !== activePage.id) {
|
||||||
@ -1372,6 +1423,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
isSwitching={pageSwitch.isSwitching}
|
isSwitching={pageSwitch.isSwitching}
|
||||||
isNewBgReady={pageSwitch.isNewBgReady}
|
isNewBgReady={pageSwitch.isNewBgReady}
|
||||||
onBackgroundReady={() => pageSwitch.markBackgroundReady()}
|
onBackgroundReady={() => pageSwitch.markBackgroundReady()}
|
||||||
|
videoAutoplay={backgroundVideoAutoplay}
|
||||||
|
videoLoop={backgroundVideoLoop}
|
||||||
|
videoMuted={backgroundVideoMuted}
|
||||||
|
videoStartTime={backgroundVideoStartTime}
|
||||||
|
videoEndTime={backgroundVideoEndTime}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Elements container - z-10 ensures they appear above backdrop layer */}
|
{/* Elements container - z-10 ensures they appear above backdrop layer */}
|
||||||
@ -1457,6 +1513,23 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
onBackgroundImageChange={setBackgroundImageUrl}
|
onBackgroundImageChange={setBackgroundImageUrl}
|
||||||
onBackgroundVideoChange={setBackgroundVideoUrl}
|
onBackgroundVideoChange={setBackgroundVideoUrl}
|
||||||
onBackgroundAudioChange={setBackgroundAudioUrl}
|
onBackgroundAudioChange={setBackgroundAudioUrl}
|
||||||
|
backgroundVideoAutoplay={backgroundVideoAutoplay}
|
||||||
|
backgroundVideoLoop={backgroundVideoLoop}
|
||||||
|
backgroundVideoMuted={backgroundVideoMuted}
|
||||||
|
backgroundVideoStartTime={backgroundVideoStartTime}
|
||||||
|
backgroundVideoEndTime={backgroundVideoEndTime}
|
||||||
|
onBackgroundVideoSettingsChange={(settings) => {
|
||||||
|
if (settings.autoplay !== undefined)
|
||||||
|
setBackgroundVideoAutoplay(settings.autoplay);
|
||||||
|
if (settings.loop !== undefined)
|
||||||
|
setBackgroundVideoLoop(settings.loop);
|
||||||
|
if (settings.muted !== undefined)
|
||||||
|
setBackgroundVideoMuted(settings.muted);
|
||||||
|
if (settings.startTime !== undefined)
|
||||||
|
setBackgroundVideoStartTime(settings.startTime);
|
||||||
|
if (settings.endTime !== undefined)
|
||||||
|
setBackgroundVideoEndTime(settings.endTime);
|
||||||
|
}}
|
||||||
newTransitionName={newTransitionName}
|
newTransitionName={newTransitionName}
|
||||||
newTransitionVideoUrl={newTransitionVideoUrl}
|
newTransitionVideoUrl={newTransitionVideoUrl}
|
||||||
newTransitionSupportsReverse={newTransitionSupportsReverse}
|
newTransitionSupportsReverse={newTransitionSupportsReverse}
|
||||||
|
|||||||
@ -29,6 +29,12 @@ export interface RuntimePage extends PreloadPage {
|
|||||||
sort_order?: number;
|
sort_order?: number;
|
||||||
ui_schema_json?: string;
|
ui_schema_json?: string;
|
||||||
environment?: 'dev' | 'stage' | 'production';
|
environment?: 'dev' | 'stage' | 'production';
|
||||||
|
// Background video playback settings
|
||||||
|
background_video_autoplay?: boolean;
|
||||||
|
background_video_loop?: boolean;
|
||||||
|
background_video_muted?: boolean;
|
||||||
|
background_video_start_time?: number | null;
|
||||||
|
background_video_end_time?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user