mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-04 09:35:16 -07:00
formatting
This commit is contained in:
@@ -1,13 +1,29 @@
|
||||
import { Copy, GripHorizontal, Minus, Pause, Play, Plus, Scissors, Square, Trash2 } from 'lucide-react';
|
||||
import {
|
||||
Copy,
|
||||
GripHorizontal,
|
||||
Minus,
|
||||
Pause,
|
||||
Play,
|
||||
Plus,
|
||||
Scissors,
|
||||
Square,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import WaveSurfer from 'wavesurfer.js';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import { useMoveStoryItem, useTrimStoryItem, useSplitStoryItem, useDuplicateStoryItem, useRemoveStoryItem } from '@/lib/hooks/useStories';
|
||||
import { useStoryStore } from '@/stores/storyStore';
|
||||
import type { StoryItemDetail } from '@/lib/api/types';
|
||||
import {
|
||||
useDuplicateStoryItem,
|
||||
useMoveStoryItem,
|
||||
useRemoveStoryItem,
|
||||
useSplitStoryItem,
|
||||
useTrimStoryItem,
|
||||
} from '@/lib/hooks/useStories';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import { useStoryStore } from '@/stores/storyStore';
|
||||
|
||||
// Clip waveform component with trim support
|
||||
function ClipWaveform({
|
||||
@@ -15,7 +31,7 @@ function ClipWaveform({
|
||||
width,
|
||||
trimStartMs,
|
||||
trimEndMs,
|
||||
duration
|
||||
duration,
|
||||
}: {
|
||||
generationId: string;
|
||||
width: number;
|
||||
@@ -28,15 +44,13 @@ function ClipWaveform({
|
||||
|
||||
// Calculate the full waveform width based on the original duration
|
||||
// The visible portion (width) represents the effective duration after trimming
|
||||
const effectiveDurationMs = (duration * 1000) - trimStartMs - trimEndMs;
|
||||
const fullWaveformWidth = effectiveDurationMs > 0
|
||||
? (width / effectiveDurationMs) * (duration * 1000)
|
||||
: width;
|
||||
const effectiveDurationMs = duration * 1000 - trimStartMs - trimEndMs;
|
||||
const fullWaveformWidth =
|
||||
effectiveDurationMs > 0 ? (width / effectiveDurationMs) * (duration * 1000) : width;
|
||||
|
||||
// Calculate how much to offset the waveform to hide the trimmed start
|
||||
const offsetX = effectiveDurationMs > 0
|
||||
? (trimStartMs / (duration * 1000)) * fullWaveformWidth
|
||||
: 0;
|
||||
const offsetX =
|
||||
effectiveDurationMs > 0 ? (trimStartMs / (duration * 1000)) * fullWaveformWidth : 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (!waveformRef.current || fullWaveformWidth < 20) return;
|
||||
@@ -130,7 +144,10 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
const [trimmingItem, setTrimmingItem] = useState<string | null>(null);
|
||||
const [trimSide, setTrimSide] = useState<'start' | 'end' | null>(null);
|
||||
const [trimStartX, setTrimStartX] = useState(0);
|
||||
const [tempTrimValues, setTempTrimValues] = useState<{ trim_start_ms: number; trim_end_ms: number } | null>(null);
|
||||
const [tempTrimValues, setTempTrimValues] = useState<{
|
||||
trim_start_ms: number;
|
||||
trim_end_ms: number;
|
||||
} | null>(null);
|
||||
|
||||
// Track editor height from store (shared with FloatingGenerateBox)
|
||||
const editorHeight = useStoryStore((state) => state.trackEditorHeight);
|
||||
@@ -156,10 +173,10 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
...items.map((item) => {
|
||||
const trimStart = item.trim_start_ms || 0;
|
||||
const trimEnd = item.trim_end_ms || 0;
|
||||
const effectiveDuration = (item.duration * 1000) - trimStart - trimEnd;
|
||||
const effectiveDuration = item.duration * 1000 - trimStart - trimEnd;
|
||||
return item.start_time_ms + effectiveDuration;
|
||||
}),
|
||||
0
|
||||
0,
|
||||
);
|
||||
setActiveStory(storyId, items, totalDuration);
|
||||
}
|
||||
@@ -214,11 +231,8 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
// Calculate total duration (using effective durations)
|
||||
const totalDurationMs = useMemo(() => {
|
||||
if (items.length === 0) return 10000; // Default 10 seconds
|
||||
return Math.max(
|
||||
...items.map((item) => item.start_time_ms + getEffectiveDuration(item)),
|
||||
10000
|
||||
);
|
||||
}, [items]);
|
||||
return Math.max(...items.map((item) => item.start_time_ms + getEffectiveDuration(item)), 10000);
|
||||
}, [items, getEffectiveDuration]);
|
||||
|
||||
// Calculate timeline width - at least full container width
|
||||
const contentWidth = (totalDurationMs / 1000) * pixelsPerSecond + 200; // Content width with padding
|
||||
@@ -246,15 +260,9 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const msToPixels = useCallback(
|
||||
(ms: number) => (ms / 1000) * pixelsPerSecond,
|
||||
[pixelsPerSecond]
|
||||
);
|
||||
const msToPixels = useCallback((ms: number) => (ms / 1000) * pixelsPerSecond, [pixelsPerSecond]);
|
||||
|
||||
const pixelsToMs = useCallback(
|
||||
(px: number) => (px / pixelsPerSecond) * 1000,
|
||||
[pixelsPerSecond]
|
||||
);
|
||||
const pixelsToMs = useCallback((px: number) => (px / pixelsPerSecond) * 1000, [pixelsPerSecond]);
|
||||
|
||||
const handleZoomIn = () => {
|
||||
setPixelsPerSecond((prev) => Math.min(prev * 1.5, MAX_PIXELS_PER_SECOND));
|
||||
@@ -265,22 +273,28 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
};
|
||||
|
||||
// Resize handlers
|
||||
const handleResizeStart = useCallback((e: React.MouseEvent) => {
|
||||
const handleResizeStart = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setIsResizing(true);
|
||||
resizeStartY.current = e.clientY;
|
||||
resizeStartHeight.current = editorHeight;
|
||||
}, [editorHeight]);
|
||||
},
|
||||
[editorHeight],
|
||||
);
|
||||
|
||||
const handleResizeMove = useCallback((e: MouseEvent) => {
|
||||
const handleResizeMove = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (!isResizing) return;
|
||||
const deltaY = resizeStartY.current - e.clientY;
|
||||
const newHeight = Math.min(
|
||||
MAX_EDITOR_HEIGHT,
|
||||
Math.max(MIN_EDITOR_HEIGHT, resizeStartHeight.current + deltaY)
|
||||
Math.max(MIN_EDITOR_HEIGHT, resizeStartHeight.current + deltaY),
|
||||
);
|
||||
setEditorHeight(newHeight);
|
||||
}, [isResizing, setEditorHeight]);
|
||||
},
|
||||
[isResizing, setEditorHeight],
|
||||
);
|
||||
|
||||
const handleResizeEnd = useCallback(() => {
|
||||
setIsResizing(false);
|
||||
@@ -328,8 +342,11 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
const trimStartItemRef = useRef<{ item: StoryItemDetail; initialTrimStart: number; initialTrimEnd: number } | null>(null);
|
||||
const trimStartItemRef = useRef<{
|
||||
item: StoryItemDetail;
|
||||
initialTrimStart: number;
|
||||
initialTrimEnd: number;
|
||||
} | null>(null);
|
||||
|
||||
const handleTrimMove = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
@@ -347,11 +364,21 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
if (trimSide === 'start') {
|
||||
// Moving right increases trim_start (trims more from start)
|
||||
// Moving left decreases trim_start (restores from start)
|
||||
newTrimStart = Math.round(Math.max(0, Math.min(initialTrimStart + deltaMs, originalDurationMs - initialTrimEnd - 100)));
|
||||
newTrimStart = Math.round(
|
||||
Math.max(
|
||||
0,
|
||||
Math.min(initialTrimStart + deltaMs, originalDurationMs - initialTrimEnd - 100),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Moving right decreases trim_end (restores from end)
|
||||
// Moving left increases trim_end (trims more from end)
|
||||
newTrimEnd = Math.round(Math.max(0, Math.min(initialTrimEnd - deltaMs, originalDurationMs - initialTrimStart - 100)));
|
||||
newTrimEnd = Math.round(
|
||||
Math.max(
|
||||
0,
|
||||
Math.min(initialTrimEnd - deltaMs, originalDurationMs - initialTrimStart - 100),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Validate that we don't exceed duration
|
||||
@@ -365,7 +392,7 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
trim_end_ms: newTrimEnd,
|
||||
});
|
||||
},
|
||||
[trimmingItem, trimSide, trimStartX, pixelsToMs]
|
||||
[trimmingItem, trimSide, trimStartX, pixelsToMs],
|
||||
);
|
||||
|
||||
const handleTrimEnd = useCallback(() => {
|
||||
@@ -403,7 +430,7 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -448,9 +475,18 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}, [selectedClipId, items, currentTimeMs, getEffectiveDuration, storyId, splitItem, toast, setSelectedClipId]);
|
||||
}, [
|
||||
selectedClipId,
|
||||
items,
|
||||
currentTimeMs,
|
||||
getEffectiveDuration,
|
||||
storyId,
|
||||
splitItem,
|
||||
toast,
|
||||
setSelectedClipId,
|
||||
]);
|
||||
|
||||
const handleDuplicate = useCallback(() => {
|
||||
if (!selectedClipId) return;
|
||||
@@ -468,7 +504,7 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}, [selectedClipId, storyId, duplicateItem, toast]);
|
||||
|
||||
@@ -491,7 +527,7 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}, [selectedClipId, storyId, removeItem, toast, setSelectedClipId]);
|
||||
|
||||
@@ -539,10 +575,7 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
}
|
||||
}, [trimmingItem, handleTrimMove, handleTrimEnd]);
|
||||
|
||||
const handleDragStart = (
|
||||
e: React.MouseEvent,
|
||||
item: StoryItemDetail
|
||||
) => {
|
||||
const handleDragStart = (e: React.MouseEvent, item: StoryItemDetail) => {
|
||||
e.stopPropagation();
|
||||
if (!tracksRef.current) return;
|
||||
|
||||
@@ -568,7 +601,7 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
|
||||
setDragPosition({ x: Math.max(0, x), y });
|
||||
},
|
||||
[draggingItem, dragOffset]
|
||||
[draggingItem, dragOffset],
|
||||
);
|
||||
|
||||
const handleDragEnd = useCallback(() => {
|
||||
@@ -610,7 +643,7 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -650,7 +683,10 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 border-t bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 z-50">
|
||||
<div className="border-t bg-background/30 backdrop-blur-2xl overflow-hidden relative" ref={containerRef}>
|
||||
<div
|
||||
className="border-t bg-background/30 backdrop-blur-2xl overflow-hidden relative"
|
||||
ref={containerRef}
|
||||
>
|
||||
{/* Resize handle at top */}
|
||||
<button
|
||||
type="button"
|
||||
@@ -666,13 +702,15 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
{/* Play controls - left side */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={handlePlayPause}>
|
||||
{isCurrentlyPlaying ? (
|
||||
<Pause className="h-4 w-4" />
|
||||
) : (
|
||||
<Play className="h-4 w-4" />
|
||||
)}
|
||||
{isCurrentlyPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={handleStop} disabled={!isCurrentlyPlaying}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={handleStop}
|
||||
disabled={!isCurrentlyPlaying}
|
||||
>
|
||||
<Square className="h-3 w-3" />
|
||||
</Button>
|
||||
<span className="text-xs text-muted-foreground tabular-nums ml-2">
|
||||
@@ -738,7 +776,7 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
key={trackNumber}
|
||||
className={cn(
|
||||
'border-b flex items-center justify-center',
|
||||
index % 2 === 0 ? 'bg-background' : 'bg-muted/10'
|
||||
index % 2 === 0 ? 'bg-background' : 'bg-muted/10',
|
||||
)}
|
||||
style={{ height: `${TRACK_HEIGHT}px` }}
|
||||
>
|
||||
@@ -792,7 +830,7 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
key={trackNumber}
|
||||
className={cn(
|
||||
'absolute left-0 right-0 border-b pointer-events-none',
|
||||
index % 2 === 0 ? 'bg-background' : 'bg-muted/10'
|
||||
index % 2 === 0 ? 'bg-background' : 'bg-muted/10',
|
||||
)}
|
||||
style={{
|
||||
top: `${index * TRACK_HEIGHT}px`,
|
||||
@@ -816,11 +854,19 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
const isTrimming = trimmingItem === item.id;
|
||||
|
||||
// Use temporary trim values during trimming for visual feedback
|
||||
const displayTrimStart = isTrimming && tempTrimValues ? tempTrimValues.trim_start_ms : (item.trim_start_ms || 0);
|
||||
const displayTrimEnd = isTrimming && tempTrimValues ? tempTrimValues.trim_end_ms : (item.trim_end_ms || 0);
|
||||
const effectiveDuration = (item.duration * 1000) - displayTrimStart - displayTrimEnd;
|
||||
const displayTrimStart =
|
||||
isTrimming && tempTrimValues
|
||||
? tempTrimValues.trim_start_ms
|
||||
: item.trim_start_ms || 0;
|
||||
const displayTrimEnd =
|
||||
isTrimming && tempTrimValues ? tempTrimValues.trim_end_ms : item.trim_end_ms || 0;
|
||||
const effectiveDuration = item.duration * 1000 - displayTrimStart - displayTrimEnd;
|
||||
|
||||
const style = getClipStyle({ ...item, trim_start_ms: displayTrimStart, trim_end_ms: displayTrimEnd });
|
||||
const style = getClipStyle({
|
||||
...item,
|
||||
trim_start_ms: displayTrimStart,
|
||||
trim_end_ms: displayTrimEnd,
|
||||
});
|
||||
const clipWidth = msToPixels(effectiveDuration);
|
||||
|
||||
return (
|
||||
@@ -829,7 +875,7 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
className={cn(
|
||||
'absolute rounded select-none overflow-visible z-10',
|
||||
isSelected && 'ring-2 ring-primary ring-offset-1',
|
||||
isTrimming && 'ring-2 ring-accent'
|
||||
isTrimming && 'ring-2 ring-accent',
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
@@ -840,7 +886,7 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {
|
||||
'bg-accent/80 hover:bg-accent border border-accent-foreground/20',
|
||||
'flex flex-col justify-center',
|
||||
isDragging && 'opacity-80 shadow-lg z-20',
|
||||
!isDragging && 'transition-all duration-100'
|
||||
!isDragging && 'transition-all duration-100',
|
||||
)}
|
||||
onClick={(e) => handleClipClick(e, item)}
|
||||
onMouseDown={(e) => {
|
||||
|
||||
@@ -58,14 +58,11 @@ export const useStoryStore = create<StoryPlaybackState>((set, get) => ({
|
||||
// Calculate total duration from items
|
||||
const maxEndTimeMs = Math.max(
|
||||
...items.map((item) => item.start_time_ms + item.duration * 1000),
|
||||
0
|
||||
0,
|
||||
);
|
||||
|
||||
// Find the minimum start time (first item)
|
||||
const minStartTimeMs = Math.min(
|
||||
...items.map((item) => item.start_time_ms),
|
||||
0
|
||||
);
|
||||
const minStartTimeMs = Math.min(...items.map((item) => item.start_time_ms), 0);
|
||||
|
||||
// If resuming the same story, keep position; otherwise start at first item
|
||||
const currentState = get();
|
||||
@@ -75,7 +72,11 @@ export const useStoryStore = create<StoryPlaybackState>((set, get) => ({
|
||||
console.log('[StoryStore] Play called:', {
|
||||
storyId,
|
||||
itemCount: items.length,
|
||||
items: items.map(i => ({ id: i.generation_id, start: i.start_time_ms, duration: i.duration })),
|
||||
items: items.map((i) => ({
|
||||
id: i.generation_id,
|
||||
start: i.start_time_ms,
|
||||
duration: i.duration,
|
||||
})),
|
||||
maxEndTimeMs,
|
||||
minStartTimeMs,
|
||||
startTimeMs,
|
||||
|
||||
Reference in New Issue
Block a user