Implement auto-activation of stories in StoryTrackEditor and improve playback state management

- Added useEffect to automatically activate the story when the editor is shown, ensuring the playhead is visible.
- Introduced setActiveStory function in storyStore to manage story activation without playback.
- Updated playback state checks to reflect the current playing status accurately.
- Enhanced UI to always display the playhead for better user experience during playback.
This commit is contained in:
Jamie Pine
2026-01-28 22:22:47 -08:00
parent 036d90dc8e
commit b55d8cc567
2 changed files with 41 additions and 12 deletions
@@ -139,16 +139,32 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
// Playback state // Playback state
const isPlaying = useStoryStore((state) => state.isPlaying); const isPlaying = useStoryStore((state) => state.isPlaying);
const currentTimeMs = useStoryStore((state) => state.currentTimeMs); const currentTimeMs = useStoryStore((state) => state.currentTimeMs);
const storeTotalDurationMs = useStoryStore((state) => state.totalDurationMs);
const playbackStoryId = useStoryStore((state) => state.playbackStoryId); const playbackStoryId = useStoryStore((state) => state.playbackStoryId);
const play = useStoryStore((state) => state.play); const play = useStoryStore((state) => state.play);
const pause = useStoryStore((state) => state.pause); const pause = useStoryStore((state) => state.pause);
const stop = useStoryStore((state) => state.stop); const stop = useStoryStore((state) => state.stop);
const seek = useStoryStore((state) => state.seek); const seek = useStoryStore((state) => state.seek);
const setActiveStory = useStoryStore((state) => state.setActiveStory);
const isActiveStory = playbackStoryId === storyId; const isActiveStory = playbackStoryId === storyId;
const isCurrentlyPlaying = isPlaying && isActiveStory; const isCurrentlyPlaying = isPlaying && isActiveStory;
// Auto-activate this story when the editor is shown so playhead is visible
useEffect(() => {
if (items.length > 0 && !isActiveStory) {
const totalDuration = Math.max(
...items.map((item) => {
const trimStart = item.trim_start_ms || 0;
const trimEnd = item.trim_end_ms || 0;
const effectiveDuration = (item.duration * 1000) - trimStart - trimEnd;
return item.start_time_ms + effectiveDuration;
}),
0
);
setActiveStory(storyId, items, totalDuration);
}
}, [storyId, items, isActiveStory, setActiveStory]);
// Sort items by start time for play // Sort items by start time for play
const sortedItems = useMemo(() => { const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a.start_time_ms - b.start_time_ms); return [...items].sort((a, b) => a.start_time_ms - b.start_time_ms);
@@ -656,11 +672,11 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
<Play className="h-4 w-4" /> <Play className="h-4 w-4" />
)} )}
</Button> </Button>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={handleStop} disabled={!isActiveStory}> <Button variant="ghost" size="icon" className="h-7 w-7" onClick={handleStop} disabled={!isCurrentlyPlaying}>
<Square className="h-3 w-3" /> <Square className="h-3 w-3" />
</Button> </Button>
<span className="text-xs text-muted-foreground tabular-nums ml-2"> <span className="text-xs text-muted-foreground tabular-nums ml-2">
{formatTime(isActiveStory ? currentTimeMs : 0)} / {formatTime(isActiveStory ? storeTotalDurationMs : 0)} {formatTime(currentTimeMs)} / {formatTime(totalDurationMs)}
</span> </span>
</div> </div>
@@ -875,15 +891,13 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
); );
})} })}
{/* Playhead */} {/* Playhead - always visible */}
{isActiveStory && ( <div
<div className="absolute top-0 bottom-0 w-1 bg-accent z-30 pointer-events-none rounded-full"
className="absolute top-0 bottom-0 w-1 bg-accent z-30 pointer-events-none rounded-full" style={{ left: `${playheadLeft}px` }}
style={{ left: `${playheadLeft}px` }} >
> <div className="absolute -top-1 left-1/2 -translate-x-1/2 w-3 h-3 bg-accent rounded-full" />
<div className="absolute -top-1 left-1/2 -translate-x-1/2 w-3 h-3 bg-accent rounded-full" /> </div>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
+15
View File
@@ -28,6 +28,7 @@ interface StoryPlaybackState {
stop: () => void; stop: () => void;
seek: (timeMs: number) => void; seek: (timeMs: number) => void;
setPlaybackTiming: (contextTime: number, storyTime: number) => void; // Set timing anchors for Web Audio API setPlaybackTiming: (contextTime: number, storyTime: number) => void; // Set timing anchors for Web Audio API
setActiveStory: (storyId: string, items: StoryItemDetail[], totalDurationMs: number) => void; // Activate story for seeking without playing
} }
const DEFAULT_TRACK_EDITOR_HEIGHT = 250; const DEFAULT_TRACK_EDITOR_HEIGHT = 250;
@@ -126,4 +127,18 @@ export const useStoryStore = create<StoryPlaybackState>((set, get) => ({
playbackStartStoryTime: storyTime, playbackStartStoryTime: storyTime,
}); });
}, },
setActiveStory: (storyId, items, totalDurationMs) => {
const currentState = get();
// Only update if switching to a different story
if (currentState.playbackStoryId !== storyId) {
set({
playbackStoryId: storyId,
playbackItems: items,
totalDurationMs,
currentTimeMs: 0,
isPlaying: false,
});
}
},
})); }));