From 055d8365fefcc0f854f42e70b0dc52bc6f16c1df Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Wed, 24 Jun 2026 21:46:07 +0330 Subject: [PATCH] feat(studio): per-scene loop plays on hover (scene.demo end-to-end) Wires the per-scene loop video all the way to the scene card: - studio-svc: SavedSceneResponse now includes Demo (was stored + copied but never serialized); MapSceneResponse passes s.Demo. - Scene type gains image?/demo?; parseScene reads them from the loaded scene data. - SceneThumbnailBlock shows scene.image as the still and plays scene.demo (muted, looped) on hover, resetting on mouse-leave. Existing projects backfilled (saved_scenes.image/demo from content.scenes). Both services rebuilt + deployed. Co-Authored-By: Claude Opus 4.8 --- .../Application/Services/StudioService.cs | 2 +- .../Models/Responses/Responses.cs | 1 + .../studio/timeline/SceneThumbnailBlock.tsx | 30 +++++++++++++++++-- src/lib/studio-scene-data.ts | 2 ++ src/lib/studio-types.ts | 4 +++ 5 files changed, 35 insertions(+), 4 deletions(-) diff --git a/services/studio/FlatRender.StudioSvc/Application/Services/StudioService.cs b/services/studio/FlatRender.StudioSvc/Application/Services/StudioService.cs index 50f53a1..36caae0 100644 --- a/services/studio/FlatRender.StudioSvc/Application/Services/StudioService.cs +++ b/services/studio/FlatRender.StudioSvc/Application/Services/StudioService.cs @@ -473,7 +473,7 @@ public class StudioService(StudioDbContext db) ); private static SavedSceneResponse MapSceneResponse(SavedScene s) => new( - s.Id, s.OriginalSceneId, s.Key, s.Title, s.Image, s.SceneType, + s.Id, s.OriginalSceneId, s.Key, s.Title, s.Image, s.Demo, s.SceneType, s.Sort, s.SceneLengthSec, s.MinDurationSec, s.MaxDurationSec, s.OverlapAtEndSec, s.CanHandleDuration, s.ManualColorSelection, s.SelectedColorPresetId, s.Contents.Select(MapContentResponse).ToList(), diff --git a/services/studio/FlatRender.StudioSvc/Models/Responses/Responses.cs b/services/studio/FlatRender.StudioSvc/Models/Responses/Responses.cs index 4587772..4c90d25 100644 --- a/services/studio/FlatRender.StudioSvc/Models/Responses/Responses.cs +++ b/services/studio/FlatRender.StudioSvc/Models/Responses/Responses.cs @@ -62,6 +62,7 @@ public record SavedSceneResponse( string Key, string? Title, string? Image, + string? Demo, string SceneType, int Sort, decimal SceneLengthSec, diff --git a/src/components/studio/timeline/SceneThumbnailBlock.tsx b/src/components/studio/timeline/SceneThumbnailBlock.tsx index 70a601b..5570739 100644 --- a/src/components/studio/timeline/SceneThumbnailBlock.tsx +++ b/src/components/studio/timeline/SceneThumbnailBlock.tsx @@ -52,6 +52,7 @@ export function SceneThumbnailBlock({ const [editName, setEditName] = useState(scene.name); const resizeRef = useRef({ startX: 0, startDuration: scene.duration }); const inputRef = useRef(null); + const videoRef = useRef(null); const duration = resizeDuration ?? scene.duration; const width = Math.max(80, duration * pxPerSecond); @@ -134,6 +135,16 @@ export function SceneThumbnailBlock({ onSelect(); } }} + onMouseEnter={() => { + void videoRef.current?.play().catch(() => {}); + }} + onMouseLeave={() => { + const v = videoRef.current; + if (v) { + v.pause(); + v.currentTime = 0; + } + }} className={cn( "group relative h-20 w-full cursor-pointer overflow-hidden rounded-lg", isActive @@ -141,10 +152,10 @@ export function SceneThumbnailBlock({ : "hover:ring-1 hover:ring-gray-300" )} > - {/* Background: real thumbnail or gradient placeholder */} - {scene.thumbnailUrl ? ( + {/* Background: template still, Konva preview, or gradient placeholder */} + {scene.image || scene.thumbnailUrl ? ( )} + {/* Hover: play the template's per-scene loop preview */} + {scene.demo ? ( +