Merge branch 'main' into bump-bun-version
This commit is contained in:
@ -84,11 +84,14 @@ export function MediaPanel() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filtered = mediaItems.filter((item) => {
|
const filtered = mediaItems.filter((item) => {
|
||||||
if (mediaFilter && mediaFilter !== 'all' && item.type !== mediaFilter) {
|
if (mediaFilter && mediaFilter !== "all" && item.type !== mediaFilter) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchQuery && !item.name.toLowerCase().includes(searchQuery.toLowerCase())) {
|
if (
|
||||||
|
searchQuery &&
|
||||||
|
!item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,17 +236,22 @@ export function MediaPanel() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleFileSelect}
|
onClick={handleFileSelect}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
className="flex-none min-w-[80px] whitespace-nowrap"
|
className="flex-none min-w-[30px] whitespace-nowrap overflow-hidden px-2 justify-center items-center"
|
||||||
>
|
>
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
<Upload className="h-4 w-4 mr-2 animate-spin" />
|
<Upload className="h-4 w-4 animate-spin" />
|
||||||
Processing...
|
<span className="hidden md:inline ml-2">Processing...</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
|
<span
|
||||||
|
className="hidden sm:inline ml-2"
|
||||||
|
aria-label="Add file"
|
||||||
|
>
|
||||||
Add
|
Add
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
@ -276,7 +284,15 @@ export function MediaPanel() {
|
|||||||
<AspectRatio ratio={item.aspectRatio}>
|
<AspectRatio ratio={item.aspectRatio}>
|
||||||
{renderPreview(item)}
|
{renderPreview(item)}
|
||||||
</AspectRatio>
|
</AspectRatio>
|
||||||
<span className="text-xs truncate px-1">{item.name}</span>
|
<span
|
||||||
|
className="text-xs truncate px-1 max-w-full"
|
||||||
|
aria-label={item.name}
|
||||||
|
title={item.name}
|
||||||
|
>
|
||||||
|
{item.name.length > 8
|
||||||
|
? `${item.name.slice(0, 4)}...${item.name.slice(-3)}`
|
||||||
|
: item.name}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Show remove button on hover */}
|
{/* Show remove button on hover */}
|
||||||
|
@ -5,16 +5,17 @@ import { useMediaStore } from "@/stores/media-store";
|
|||||||
import { usePlaybackStore } from "@/stores/playback-store";
|
import { usePlaybackStore } from "@/stores/playback-store";
|
||||||
import { VideoPlayer } from "@/components/ui/video-player";
|
import { VideoPlayer } from "@/components/ui/video-player";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Play, Pause } from "lucide-react";
|
import { Play, Pause, Volume2, VolumeX } from "lucide-react";
|
||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
|
|
||||||
// Debug flag - set to false to hide active clips info
|
// Debug flag - set to false to hide active clips info
|
||||||
const SHOW_DEBUG_INFO = process.env.NODE_ENV === 'development';
|
const SHOW_DEBUG_INFO = process.env.NODE_ENV === "development";
|
||||||
|
|
||||||
export function PreviewPanel() {
|
export function PreviewPanel() {
|
||||||
const { tracks } = useTimelineStore();
|
const { tracks } = useTimelineStore();
|
||||||
const { mediaItems } = useMediaStore();
|
const { mediaItems } = useMediaStore();
|
||||||
const { isPlaying, toggle, currentTime } = usePlaybackStore();
|
const { isPlaying, toggle, currentTime, muted, toggleMute, volume } =
|
||||||
|
usePlaybackStore();
|
||||||
const [canvasSize, setCanvasSize] = useState({ width: 1920, height: 1080 });
|
const [canvasSize, setCanvasSize] = useState({ width: 1920, height: 1080 });
|
||||||
const [showDebug, setShowDebug] = useState(SHOW_DEBUG_INFO);
|
const [showDebug, setShowDebug] = useState(SHOW_DEBUG_INFO);
|
||||||
const previewRef = useRef<HTMLDivElement>(null);
|
const previewRef = useRef<HTMLDivElement>(null);
|
||||||
@ -30,10 +31,12 @@ export function PreviewPanel() {
|
|||||||
tracks.forEach((track) => {
|
tracks.forEach((track) => {
|
||||||
track.clips.forEach((clip) => {
|
track.clips.forEach((clip) => {
|
||||||
const clipStart = clip.startTime;
|
const clipStart = clip.startTime;
|
||||||
const clipEnd = clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd);
|
const clipEnd =
|
||||||
|
clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd);
|
||||||
|
|
||||||
if (currentTime >= clipStart && currentTime < clipEnd) {
|
if (currentTime >= clipStart && currentTime < clipEnd) {
|
||||||
const mediaItem = clip.mediaId === "test"
|
const mediaItem =
|
||||||
|
clip.mediaId === "test"
|
||||||
? { type: "test", name: clip.name, url: "", thumbnailUrl: "" }
|
? { type: "test", name: clip.name, url: "", thumbnailUrl: "" }
|
||||||
: mediaItems.find((item) => item.id === clip.mediaId);
|
: mediaItems.find((item) => item.id === clip.mediaId);
|
||||||
|
|
||||||
@ -134,13 +137,19 @@ export function PreviewPanel() {
|
|||||||
<select
|
<select
|
||||||
value={`${canvasSize.width}x${canvasSize.height}`}
|
value={`${canvasSize.width}x${canvasSize.height}`}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const preset = canvasPresets.find(p => `${p.width}x${p.height}` === e.target.value);
|
const preset = canvasPresets.find(
|
||||||
if (preset) setCanvasSize({ width: preset.width, height: preset.height });
|
(p) => `${p.width}x${p.height}` === e.target.value
|
||||||
|
);
|
||||||
|
if (preset)
|
||||||
|
setCanvasSize({ width: preset.width, height: preset.height });
|
||||||
}}
|
}}
|
||||||
className="bg-background border rounded px-2 py-1 text-xs"
|
className="bg-background border rounded px-2 py-1 text-xs"
|
||||||
>
|
>
|
||||||
{canvasPresets.map(preset => (
|
{canvasPresets.map((preset) => (
|
||||||
<option key={preset.name} value={`${preset.width}x${preset.height}`}>
|
<option
|
||||||
|
key={preset.name}
|
||||||
|
value={`${preset.width}x${preset.height}`}
|
||||||
|
>
|
||||||
{preset.name} ({preset.width}×{preset.height})
|
{preset.name} ({preset.width}×{preset.height})
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
@ -154,12 +163,30 @@ export function PreviewPanel() {
|
|||||||
onClick={() => setShowDebug(!showDebug)}
|
onClick={() => setShowDebug(!showDebug)}
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
>
|
>
|
||||||
Debug {showDebug ? 'ON' : 'OFF'}
|
Debug {showDebug ? "ON" : "OFF"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button variant="outline" size="sm" onClick={toggle} className="ml-auto">
|
<Button
|
||||||
{isPlaying ? <Pause className="h-3 w-3 mr-1" /> : <Play className="h-3 w-3 mr-1" />}
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={toggleMute}
|
||||||
|
className="ml-auto"
|
||||||
|
>
|
||||||
|
{muted || volume === 0 ? (
|
||||||
|
<VolumeX className="h-3 w-3 mr-1" />
|
||||||
|
) : (
|
||||||
|
<Volume2 className="h-3 w-3 mr-1" />
|
||||||
|
)}
|
||||||
|
{muted || volume === 0 ? "Unmute" : "Mute"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button variant="outline" size="sm" onClick={toggle}>
|
||||||
|
{isPlaying ? (
|
||||||
|
<Pause className="h-3 w-3 mr-1" />
|
||||||
|
) : (
|
||||||
|
<Play className="h-3 w-3 mr-1" />
|
||||||
|
)}
|
||||||
{isPlaying ? "Pause" : "Play"}
|
{isPlaying ? "Pause" : "Play"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -177,7 +204,9 @@ export function PreviewPanel() {
|
|||||||
>
|
>
|
||||||
{activeClips.length === 0 ? (
|
{activeClips.length === 0 ? (
|
||||||
<div className="absolute inset-0 flex items-center justify-center text-white/50">
|
<div className="absolute inset-0 flex items-center justify-center text-white/50">
|
||||||
{tracks.length === 0 ? "Drop media to start editing" : "No clips at current time"}
|
{tracks.length === 0
|
||||||
|
? "Drop media to start editing"
|
||||||
|
: "No clips at current time"}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
activeClips.map((clipData, index) => renderClip(clipData, index))
|
activeClips.map((clipData, index) => renderClip(clipData, index))
|
||||||
@ -188,7 +217,9 @@ export function PreviewPanel() {
|
|||||||
{/* Debug Info Panel - Conditionally rendered */}
|
{/* Debug Info Panel - Conditionally rendered */}
|
||||||
{showDebug && (
|
{showDebug && (
|
||||||
<div className="border-t bg-background p-2 flex-shrink-0">
|
<div className="border-t bg-background p-2 flex-shrink-0">
|
||||||
<div className="text-xs font-medium mb-1">Debug: Active Clips ({activeClips.length})</div>
|
<div className="text-xs font-medium mb-1">
|
||||||
|
Debug: Active Clips ({activeClips.length})
|
||||||
|
</div>
|
||||||
<div className="flex gap-2 overflow-x-auto">
|
<div className="flex gap-2 overflow-x-auto">
|
||||||
{activeClips.map((clipData, index) => (
|
{activeClips.map((clipData, index) => (
|
||||||
<div
|
<div
|
||||||
@ -199,7 +230,9 @@ export function PreviewPanel() {
|
|||||||
{index + 1}
|
{index + 1}
|
||||||
</span>
|
</span>
|
||||||
<span>{clipData.clip.name}</span>
|
<span>{clipData.clip.name}</span>
|
||||||
<span className="text-muted-foreground">({clipData.mediaItem?.type || 'test'})</span>
|
<span className="text-muted-foreground">
|
||||||
|
({clipData.mediaItem?.type || "test"})
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{activeClips.length === 0 && (
|
{activeClips.length === 0 && (
|
||||||
|
@ -20,14 +20,15 @@ export function VideoPlayer({
|
|||||||
clipStartTime,
|
clipStartTime,
|
||||||
trimStart,
|
trimStart,
|
||||||
trimEnd,
|
trimEnd,
|
||||||
clipDuration
|
clipDuration,
|
||||||
}: VideoPlayerProps) {
|
}: VideoPlayerProps) {
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const { isPlaying, currentTime, volume, speed } = usePlaybackStore();
|
const { isPlaying, currentTime, volume, speed, muted } = usePlaybackStore();
|
||||||
|
|
||||||
// Calculate if we're within this clip's timeline range
|
// Calculate if we're within this clip's timeline range
|
||||||
const clipEndTime = clipStartTime + (clipDuration - trimStart - trimEnd);
|
const clipEndTime = clipStartTime + (clipDuration - trimStart - trimEnd);
|
||||||
const isInClipRange = currentTime >= clipStartTime && currentTime < clipEndTime;
|
const isInClipRange =
|
||||||
|
currentTime >= clipStartTime && currentTime < clipEndTime;
|
||||||
|
|
||||||
// Sync playback events
|
// Sync playback events
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -37,20 +38,26 @@ export function VideoPlayer({
|
|||||||
const handleSeekEvent = (e: CustomEvent) => {
|
const handleSeekEvent = (e: CustomEvent) => {
|
||||||
// Always update video time, even if outside clip range
|
// Always update video time, even if outside clip range
|
||||||
const timelineTime = e.detail.time;
|
const timelineTime = e.detail.time;
|
||||||
const videoTime = Math.max(trimStart, Math.min(
|
const videoTime = Math.max(
|
||||||
|
trimStart,
|
||||||
|
Math.min(
|
||||||
clipDuration - trimEnd,
|
clipDuration - trimEnd,
|
||||||
timelineTime - clipStartTime + trimStart
|
timelineTime - clipStartTime + trimStart
|
||||||
));
|
)
|
||||||
|
);
|
||||||
video.currentTime = videoTime;
|
video.currentTime = videoTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateEvent = (e: CustomEvent) => {
|
const handleUpdateEvent = (e: CustomEvent) => {
|
||||||
// Always update video time, even if outside clip range
|
// Always update video time, even if outside clip range
|
||||||
const timelineTime = e.detail.time;
|
const timelineTime = e.detail.time;
|
||||||
const targetTime = Math.max(trimStart, Math.min(
|
const targetTime = Math.max(
|
||||||
|
trimStart,
|
||||||
|
Math.min(
|
||||||
clipDuration - trimEnd,
|
clipDuration - trimEnd,
|
||||||
timelineTime - clipStartTime + trimStart
|
timelineTime - clipStartTime + trimStart
|
||||||
));
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (Math.abs(video.currentTime - targetTime) > 0.5) {
|
if (Math.abs(video.currentTime - targetTime) > 0.5) {
|
||||||
video.currentTime = targetTime;
|
video.currentTime = targetTime;
|
||||||
@ -62,13 +69,25 @@ export function VideoPlayer({
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("playback-seek", handleSeekEvent as EventListener);
|
window.addEventListener("playback-seek", handleSeekEvent as EventListener);
|
||||||
window.addEventListener("playback-update", handleUpdateEvent as EventListener);
|
window.addEventListener(
|
||||||
|
"playback-update",
|
||||||
|
handleUpdateEvent as EventListener
|
||||||
|
);
|
||||||
window.addEventListener("playback-speed", handleSpeed as EventListener);
|
window.addEventListener("playback-speed", handleSpeed as EventListener);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("playback-seek", handleSeekEvent as EventListener);
|
window.removeEventListener(
|
||||||
window.removeEventListener("playback-update", handleUpdateEvent as EventListener);
|
"playback-seek",
|
||||||
window.removeEventListener("playback-speed", handleSpeed as EventListener);
|
handleSeekEvent as EventListener
|
||||||
|
);
|
||||||
|
window.removeEventListener(
|
||||||
|
"playback-update",
|
||||||
|
handleUpdateEvent as EventListener
|
||||||
|
);
|
||||||
|
window.removeEventListener(
|
||||||
|
"playback-speed",
|
||||||
|
handleSpeed as EventListener
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}, [clipStartTime, trimStart, trimEnd, clipDuration, isInClipRange]);
|
}, [clipStartTime, trimStart, trimEnd, clipDuration, isInClipRange]);
|
||||||
|
|
||||||
@ -90,8 +109,9 @@ export function VideoPlayer({
|
|||||||
if (!video) return;
|
if (!video) return;
|
||||||
|
|
||||||
video.volume = volume;
|
video.volume = volume;
|
||||||
|
video.muted = muted;
|
||||||
video.playbackRate = speed;
|
video.playbackRate = speed;
|
||||||
}, [volume, speed]);
|
}, [volume, speed, muted]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<video
|
<video
|
||||||
@ -104,7 +124,7 @@ export function VideoPlayer({
|
|||||||
controls={false}
|
controls={false}
|
||||||
disablePictureInPicture
|
disablePictureInPicture
|
||||||
disableRemotePlayback
|
disableRemotePlayback
|
||||||
style={{ pointerEvents: 'none' }}
|
style={{ pointerEvents: "none" }}
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -19,13 +19,15 @@ const startTimer = (store: any) => {
|
|||||||
const delta = (now - lastUpdate) / 1000; // Convert to seconds
|
const delta = (now - lastUpdate) / 1000; // Convert to seconds
|
||||||
lastUpdate = now;
|
lastUpdate = now;
|
||||||
|
|
||||||
const newTime = state.currentTime + (delta * state.speed);
|
const newTime = state.currentTime + delta * state.speed;
|
||||||
if (newTime >= state.duration) {
|
if (newTime >= state.duration) {
|
||||||
state.pause();
|
state.pause();
|
||||||
} else {
|
} else {
|
||||||
state.setCurrentTime(newTime);
|
state.setCurrentTime(newTime);
|
||||||
// Notify video elements to sync
|
// Notify video elements to sync
|
||||||
window.dispatchEvent(new CustomEvent('playback-update', { detail: { time: newTime } }));
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("playback-update", { detail: { time: newTime } })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
playbackTimer = requestAnimationFrame(updateTime);
|
playbackTimer = requestAnimationFrame(updateTime);
|
||||||
@ -47,6 +49,8 @@ export const usePlaybackStore = create<PlaybackStore>((set, get) => ({
|
|||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
volume: 1,
|
volume: 1,
|
||||||
|
muted: false,
|
||||||
|
previousVolume: 1,
|
||||||
speed: 1.0,
|
speed: 1.0,
|
||||||
|
|
||||||
play: () => {
|
play: () => {
|
||||||
@ -73,21 +77,52 @@ export const usePlaybackStore = create<PlaybackStore>((set, get) => ({
|
|||||||
const clampedTime = Math.max(0, Math.min(duration, time));
|
const clampedTime = Math.max(0, Math.min(duration, time));
|
||||||
set({ currentTime: clampedTime });
|
set({ currentTime: clampedTime });
|
||||||
|
|
||||||
// Notify video elements to seek
|
const event = new CustomEvent("playback-seek", {
|
||||||
const event = new CustomEvent('playback-seek', { detail: { time: clampedTime } });
|
detail: { time: clampedTime },
|
||||||
|
});
|
||||||
window.dispatchEvent(event);
|
window.dispatchEvent(event);
|
||||||
},
|
},
|
||||||
|
|
||||||
setVolume: (volume: number) => set({ volume: Math.max(0, Math.min(1, volume)) }),
|
setVolume: (volume: number) =>
|
||||||
|
set((state) => ({
|
||||||
|
volume: Math.max(0, Math.min(1, volume)),
|
||||||
|
muted: volume === 0,
|
||||||
|
previousVolume: volume > 0 ? volume : state.previousVolume,
|
||||||
|
})),
|
||||||
|
|
||||||
setSpeed: (speed: number) => {
|
setSpeed: (speed: number) => {
|
||||||
const newSpeed = Math.max(0.1, Math.min(2.0, speed));
|
const newSpeed = Math.max(0.1, Math.min(2.0, speed));
|
||||||
set({ speed: newSpeed });
|
set({ speed: newSpeed });
|
||||||
|
|
||||||
const event = new CustomEvent('playback-speed', { detail: { speed: newSpeed } });
|
const event = new CustomEvent("playback-speed", {
|
||||||
|
detail: { speed: newSpeed },
|
||||||
|
});
|
||||||
window.dispatchEvent(event);
|
window.dispatchEvent(event);
|
||||||
},
|
},
|
||||||
|
|
||||||
setDuration: (duration: number) => set({ duration }),
|
setDuration: (duration: number) => set({ duration }),
|
||||||
setCurrentTime: (time: number) => set({ currentTime: time }),
|
setCurrentTime: (time: number) => set({ currentTime: time }),
|
||||||
|
|
||||||
|
mute: () => {
|
||||||
|
const { volume, previousVolume } = get();
|
||||||
|
set({
|
||||||
|
muted: true,
|
||||||
|
previousVolume: volume > 0 ? volume : previousVolume,
|
||||||
|
volume: 0,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
unmute: () => {
|
||||||
|
const { previousVolume } = get();
|
||||||
|
set({ muted: false, volume: previousVolume ?? 1 });
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMute: () => {
|
||||||
|
const { muted } = get();
|
||||||
|
if (muted) {
|
||||||
|
get().unmute();
|
||||||
|
} else {
|
||||||
|
get().mute();
|
||||||
|
}
|
||||||
|
},
|
||||||
}));
|
}));
|
@ -4,6 +4,8 @@ export interface PlaybackState {
|
|||||||
duration: number;
|
duration: number;
|
||||||
volume: number;
|
volume: number;
|
||||||
speed: number;
|
speed: number;
|
||||||
|
muted: boolean;
|
||||||
|
previousVolume?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlaybackControls {
|
export interface PlaybackControls {
|
||||||
@ -13,4 +15,7 @@ export interface PlaybackControls {
|
|||||||
setVolume: (volume: number) => void;
|
setVolume: (volume: number) => void;
|
||||||
setSpeed: (speed: number) => void;
|
setSpeed: (speed: number) => void;
|
||||||
toggle: () => void;
|
toggle: () => void;
|
||||||
|
mute: () => void;
|
||||||
|
unmute: () => void;
|
||||||
|
toggleMute: () => void;
|
||||||
}
|
}
|
Reference in New Issue
Block a user