diff --git a/.gitignore b/.gitignore index 35b2e63..2c5f478 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,5 @@ node_modules/ **/node_modules/ */build/ package-lock.json -CLAUDE.md -.claude/ +AGENTS.md +.codex/ diff --git a/backend/src/db/api/tour_pages.js b/backend/src/db/api/tour_pages.js index c38e37b..fe51dbf 100644 --- a/backend/src/db/api/tour_pages.js +++ b/backend/src/db/api/tour_pages.js @@ -25,6 +25,7 @@ class Tour_pagesDBApi extends GenericDBApi { 'slug', 'background_image_url', 'background_video_url', + 'background_embed_url', 'background_audio_url', 'ui_schema_json', ]; @@ -72,6 +73,7 @@ class Tour_pagesDBApi extends GenericDBApi { sort_order: data.sort_order || null, background_image_url: data.background_image_url || null, background_video_url: data.background_video_url || null, + background_embed_url: data.background_embed_url || null, background_audio_url: data.background_audio_url || null, background_audio_autoplay: data.background_audio_autoplay !== undefined diff --git a/backend/src/db/migrations/20260613000001-add-background-embed-url-to-tour-pages.js b/backend/src/db/migrations/20260613000001-add-background-embed-url-to-tour-pages.js new file mode 100644 index 0000000..c1eec1b --- /dev/null +++ b/backend/src/db/migrations/20260613000001-add-background-embed-url-to-tour-pages.js @@ -0,0 +1,12 @@ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.addColumn('tour_pages', 'background_embed_url', { + type: Sequelize.TEXT, + allowNull: true, + }); + }, + + async down(queryInterface) { + await queryInterface.removeColumn('tour_pages', 'background_embed_url'); + }, +}; diff --git a/backend/src/db/models/tour_pages.js b/backend/src/db/models/tour_pages.js index 78e4c48..46ecf31 100644 --- a/backend/src/db/models/tour_pages.js +++ b/backend/src/db/models/tour_pages.js @@ -62,6 +62,10 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.TEXT, }, + background_embed_url: { + type: DataTypes.TEXT, + }, + background_audio_url: { type: DataTypes.TEXT, }, diff --git a/backend/src/middlewares/runtime-public.js b/backend/src/middlewares/runtime-public.js index 4da8363..bcc0498 100644 --- a/backend/src/middlewares/runtime-public.js +++ b/backend/src/middlewares/runtime-public.js @@ -18,6 +18,7 @@ const PUBLIC_RUNTIME_ENTITY_FIELDS = { 'sort_order', 'background_image_url', 'background_video_url', + 'background_embed_url', 'background_audio_url', 'background_loop', 'requires_auth', diff --git a/backend/src/services/projects.js b/backend/src/services/projects.js index 5e1d52e..5376642 100644 --- a/backend/src/services/projects.js +++ b/backend/src/services/projects.js @@ -448,6 +448,11 @@ class ProjectsService extends BaseProjectsService { assetPathMap.get(pageData.background_video_url) || pageData.background_video_url; } + if (pageData.background_embed_url) { + pageData.background_embed_url = + assetPathMap.get(pageData.background_embed_url) || + pageData.background_embed_url; + } if (pageData.background_audio_url) { pageData.background_audio_url = assetPathMap.get(pageData.background_audio_url) || diff --git a/frontend/src/components/Constructor/BackgroundSettingsEditor.tsx b/frontend/src/components/Constructor/BackgroundSettingsEditor.tsx index 0310b30..5531109 100644 --- a/frontend/src/components/Constructor/BackgroundSettingsEditor.tsx +++ b/frontend/src/components/Constructor/BackgroundSettingsEditor.tsx @@ -1,7 +1,7 @@ /** * BackgroundSettingsEditor Component * - * Compact editor for background image, video, or audio settings. + * Compact editor for background image, video, 360/embed, or audio settings. * Used in the element editor panel for background menu items. * For video backgrounds, includes playback settings (autoplay, loop, muted, start/end time). */ @@ -15,7 +15,7 @@ import type { import { addFallbackAssetOption } from '../../lib/constructorHelpers'; interface BackgroundSettingsEditorProps { - type: 'image' | 'video' | 'audio'; + type: 'image' | 'video' | 'embed' | 'audio'; value: string; options: AssetOption[]; durationNote?: string; @@ -38,6 +38,7 @@ interface BackgroundSettingsEditorProps { const LABELS: Record = { image: 'Background image', video: 'Background video', + embed: 'Background 360', audio: 'Background audio', }; diff --git a/frontend/src/components/Constructor/CanvasBackground.tsx b/frontend/src/components/Constructor/CanvasBackground.tsx index 2c197e1..61c3ba2 100644 --- a/frontend/src/components/Constructor/CanvasBackground.tsx +++ b/frontend/src/components/Constructor/CanvasBackground.tsx @@ -1,7 +1,7 @@ /** * CanvasBackground Component * - * Background image, video, and audio for the constructor canvas. + * Background image, video, 360/embed, and audio for the constructor canvas. * Handles blob URLs, Next.js Image optimization, and previous background overlay. * Supports custom video playback settings (autoplay, loop, muted, start/end time). */ @@ -18,6 +18,7 @@ import { useBackgroundVideoPlayback } from '../../hooks/useBackgroundVideoPlayba import { useBackgroundAudioPlayback } from '../../hooks/useBackgroundAudioPlayback'; import PreviousBackgroundOverlay from '../PreviousBackgroundOverlay'; import { baseURLApi } from '../../config'; +import { buildChromeFreeEmbedUrl } from '../../lib/embedUrl'; /** * Schedule a callback to run after the next browser paint. @@ -45,6 +46,7 @@ interface HTMLVideoElementWithRVFC extends HTMLVideoElement { interface CanvasBackgroundProps { backgroundImageUrl?: string; backgroundVideoUrl?: string; + backgroundEmbedUrl?: string; backgroundAudioUrl?: string; previousBgImageUrl?: string; previousBgVideoUrl?: string; @@ -76,6 +78,7 @@ interface CanvasBackgroundProps { const CanvasBackground: React.FC = ({ backgroundImageUrl, backgroundVideoUrl, + backgroundEmbedUrl, backgroundAudioUrl, previousBgImageUrl, previousBgVideoUrl, @@ -96,6 +99,20 @@ const CanvasBackground: React.FC = ({ audioStoragePath, pauseAudio = false, }) => { + const hasEmbedBackground = Boolean(backgroundEmbedUrl); + const embedBackgroundSrc = useMemo( + () => + backgroundEmbedUrl ? buildChromeFreeEmbedUrl(backgroundEmbedUrl) : '', + [backgroundEmbedUrl], + ); + + useEffect(() => { + if (!backgroundEmbedUrl) return; + scheduleAfterPaint(() => { + onBackgroundReady?.(); + }); + }, [backgroundEmbedUrl, onBackgroundReady]); + // During page switching with video paused, keep showing the previous video URL. // This prevents black flash when the video element would remount with a new URL. // The old video element stays visible (paused at frozen frame) until new page is ready. @@ -141,7 +158,7 @@ const CanvasBackground: React.FC = ({ // Track video buffering via canplay/waiting events useEffect(() => { const video = videoRef.current; - if (!backgroundVideoUrl || !video) { + if (!backgroundVideoUrl || hasEmbedBackground || !video) { setIsVideoBuffering(false); // CRITICAL: Also notify parent that buffering is done when there's no video // Without this, parent's isBackgroundVideoBuffering stays stuck at true from previous page @@ -170,7 +187,7 @@ const CanvasBackground: React.FC = ({ video.removeEventListener('canplay', handleCanPlay); video.removeEventListener('waiting', handleWaiting); }; - }, [backgroundVideoUrl, onVideoBufferStateChange]); + }, [backgroundVideoUrl, hasEmbedBackground, onVideoBufferStateChange]); // Fallback to proxy URL if presigned URL fails (e.g., CORS, expiration) const videoSrc = useMemo(() => { @@ -413,7 +430,17 @@ const CanvasBackground: React.FC = ({ {/* Background image - z-1 keeps it below backdrop blur layer (z-5). Image layer stays visible while video buffers (fallback behavior). When video is ready, image fades out via opacity transition. */} - {backgroundImageUrl && ( + {backgroundEmbedUrl && ( +