diff --git a/apps/web/src/components/editor/media-panel.tsx b/apps/web/src/components/editor/media-panel.tsx
index 95cfa0a..f2b3f65 100644
--- a/apps/web/src/components/editor/media-panel.tsx
+++ b/apps/web/src/components/editor/media-panel.tsx
@@ -84,17 +84,20 @@ export function MediaPanel() {
useEffect(() => {
const filtered = mediaItems.filter((item) => {
- if (mediaFilter && mediaFilter !== 'all' && item.type !== mediaFilter) {
+ if (mediaFilter && mediaFilter !== "all" && item.type !== mediaFilter) {
return false;
}
-
- if (searchQuery && !item.name.toLowerCase().includes(searchQuery.toLowerCase())) {
+
+ if (
+ searchQuery &&
+ !item.name.toLowerCase().includes(searchQuery.toLowerCase())
+ ) {
return false;
}
-
+
return true;
});
-
+
setFilteredMediaItems(filtered);
}, [mediaItems, mediaFilter, searchQuery]);
@@ -209,23 +212,23 @@ export function MediaPanel() {
{/* Button to add/upload media */}
{/* Search and filter controls */}
-
setMediaFilter(e.target.value)}
- className="px-2 py-1 text-xs border rounded bg-background"
- >
- All
- Video
- Audio
- Image
-
-
setSearchQuery(e.target.value)}
- />
+
setMediaFilter(e.target.value)}
+ className="px-2 py-1 text-xs border rounded bg-background"
+ >
+ All
+ Video
+ Audio
+ Image
+
+
setSearchQuery(e.target.value)}
+ />
{/* Add media button */}
{isProcessing ? (
<>
-
- Processing...
+
+ Processing...
>
) : (
<>
- Add
+
+ Add
+
>
)}
-
+
@@ -276,7 +284,15 @@ export function MediaPanel() {
{renderPreview(item)}
-
{item.name}
+
+ {item.name.length > 8
+ ? `${item.name.slice(0, 4)}...${item.name.slice(-3)}`
+ : item.name}
+
{/* Show remove button on hover */}
diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx
index 624a549..33bfe68 100644
--- a/apps/web/src/components/editor/preview-panel.tsx
+++ b/apps/web/src/components/editor/preview-panel.tsx
@@ -5,16 +5,17 @@ import { useMediaStore } from "@/stores/media-store";
import { usePlaybackStore } from "@/stores/playback-store";
import { VideoPlayer } from "@/components/ui/video-player";
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";
// 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() {
const { tracks } = useTimelineStore();
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 [showDebug, setShowDebug] = useState(SHOW_DEBUG_INFO);
const previewRef = useRef
(null);
@@ -30,12 +31,14 @@ export function PreviewPanel() {
tracks.forEach((track) => {
track.clips.forEach((clip) => {
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) {
- const mediaItem = clip.mediaId === "test"
- ? { type: "test", name: clip.name, url: "", thumbnailUrl: "" }
- : mediaItems.find((item) => item.id === clip.mediaId);
+ const mediaItem =
+ clip.mediaId === "test"
+ ? { type: "test", name: clip.name, url: "", thumbnailUrl: "" }
+ : mediaItems.find((item) => item.id === clip.mediaId);
if (mediaItem || clip.mediaId === "test") {
activeClips.push({ clip, track, mediaItem });
@@ -134,13 +137,19 @@ export function PreviewPanel() {
{
- const preset = canvasPresets.find(p => `${p.width}x${p.height}` === e.target.value);
- if (preset) setCanvasSize({ width: preset.width, height: preset.height });
+ const preset = canvasPresets.find(
+ (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"
>
- {canvasPresets.map(preset => (
-
+ {canvasPresets.map((preset) => (
+
{preset.name} ({preset.width}×{preset.height})
))}
@@ -154,12 +163,30 @@ export function PreviewPanel() {
onClick={() => setShowDebug(!showDebug)}
className="text-xs"
>
- Debug {showDebug ? 'ON' : 'OFF'}
+ Debug {showDebug ? "ON" : "OFF"}
)}
-
- {isPlaying ? : }
+
+ {muted || volume === 0 ? (
+
+ ) : (
+
+ )}
+ {muted || volume === 0 ? "Unmute" : "Mute"}
+
+
+
+ {isPlaying ? (
+
+ ) : (
+
+ )}
{isPlaying ? "Pause" : "Play"}
@@ -177,7 +204,9 @@ export function PreviewPanel() {
>
{activeClips.length === 0 ? (
- {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"}
) : (
activeClips.map((clipData, index) => renderClip(clipData, index))
@@ -188,7 +217,9 @@ export function PreviewPanel() {
{/* Debug Info Panel - Conditionally rendered */}
{showDebug && (
-
Debug: Active Clips ({activeClips.length})
+
+ Debug: Active Clips ({activeClips.length})
+
{activeClips.map((clipData, index) => (
{clipData.clip.name}
- ({clipData.mediaItem?.type || 'test'})
+
+ ({clipData.mediaItem?.type || "test"})
+
))}
{activeClips.length === 0 && (
diff --git a/apps/web/src/components/ui/video-player.tsx b/apps/web/src/components/ui/video-player.tsx
index 32a1291..d8a72e4 100644
--- a/apps/web/src/components/ui/video-player.tsx
+++ b/apps/web/src/components/ui/video-player.tsx
@@ -4,108 +4,128 @@ import { useRef, useEffect } from "react";
import { usePlaybackStore } from "@/stores/playback-store";
interface VideoPlayerProps {
- src: string;
- poster?: string;
- className?: string;
- clipStartTime: number;
- trimStart: number;
- trimEnd: number;
- clipDuration: number;
+ src: string;
+ poster?: string;
+ className?: string;
+ clipStartTime: number;
+ trimStart: number;
+ trimEnd: number;
+ clipDuration: number;
}
export function VideoPlayer({
- src,
- poster,
- className = "",
- clipStartTime,
- trimStart,
- trimEnd,
- clipDuration
+ src,
+ poster,
+ className = "",
+ clipStartTime,
+ trimStart,
+ trimEnd,
+ clipDuration,
}: VideoPlayerProps) {
- const videoRef = useRef
(null);
- const { isPlaying, currentTime, volume, speed } = usePlaybackStore();
+ const videoRef = useRef(null);
+ const { isPlaying, currentTime, volume, speed, muted } = usePlaybackStore();
- // Calculate if we're within this clip's timeline range
- const clipEndTime = clipStartTime + (clipDuration - trimStart - trimEnd);
- const isInClipRange = currentTime >= clipStartTime && currentTime < clipEndTime;
+ // Calculate if we're within this clip's timeline range
+ const clipEndTime = clipStartTime + (clipDuration - trimStart - trimEnd);
+ const isInClipRange =
+ currentTime >= clipStartTime && currentTime < clipEndTime;
- // Sync playback events
- useEffect(() => {
- const video = videoRef.current;
- if (!video || !isInClipRange) return;
+ // Sync playback events
+ useEffect(() => {
+ const video = videoRef.current;
+ if (!video || !isInClipRange) return;
- const handleSeekEvent = (e: CustomEvent) => {
- // Always update video time, even if outside clip range
- const timelineTime = e.detail.time;
- const videoTime = Math.max(trimStart, Math.min(
- clipDuration - trimEnd,
- timelineTime - clipStartTime + trimStart
- ));
- video.currentTime = videoTime;
- };
+ const handleSeekEvent = (e: CustomEvent) => {
+ // Always update video time, even if outside clip range
+ const timelineTime = e.detail.time;
+ const videoTime = Math.max(
+ trimStart,
+ Math.min(
+ clipDuration - trimEnd,
+ timelineTime - clipStartTime + trimStart
+ )
+ );
+ video.currentTime = videoTime;
+ };
- const handleUpdateEvent = (e: CustomEvent) => {
- // Always update video time, even if outside clip range
- const timelineTime = e.detail.time;
- const targetTime = Math.max(trimStart, Math.min(
- clipDuration - trimEnd,
- timelineTime - clipStartTime + trimStart
- ));
+ const handleUpdateEvent = (e: CustomEvent) => {
+ // Always update video time, even if outside clip range
+ const timelineTime = e.detail.time;
+ const targetTime = Math.max(
+ trimStart,
+ Math.min(
+ clipDuration - trimEnd,
+ timelineTime - clipStartTime + trimStart
+ )
+ );
- if (Math.abs(video.currentTime - targetTime) > 0.5) {
- video.currentTime = targetTime;
- }
- };
+ if (Math.abs(video.currentTime - targetTime) > 0.5) {
+ video.currentTime = targetTime;
+ }
+ };
- const handleSpeed = (e: CustomEvent) => {
- video.playbackRate = e.detail.speed;
- };
+ const handleSpeed = (e: CustomEvent) => {
+ video.playbackRate = e.detail.speed;
+ };
- window.addEventListener("playback-seek", handleSeekEvent as EventListener);
- window.addEventListener("playback-update", handleUpdateEvent as EventListener);
- window.addEventListener("playback-speed", handleSpeed as EventListener);
-
- return () => {
- window.removeEventListener("playback-seek", handleSeekEvent as EventListener);
- window.removeEventListener("playback-update", handleUpdateEvent as EventListener);
- window.removeEventListener("playback-speed", handleSpeed as EventListener);
- };
- }, [clipStartTime, trimStart, trimEnd, clipDuration, isInClipRange]);
-
- // Sync playback state
- useEffect(() => {
- const video = videoRef.current;
- if (!video) return;
-
- if (isPlaying && isInClipRange) {
- video.play().catch(() => { });
- } else {
- video.pause();
- }
- }, [isPlaying, isInClipRange]);
-
- // Sync volume and speed
- useEffect(() => {
- const video = videoRef.current;
- if (!video) return;
-
- video.volume = volume;
- video.playbackRate = speed;
- }, [volume, speed]);
-
- return (
- e.preventDefault()}
- />
+ window.addEventListener("playback-seek", handleSeekEvent as EventListener);
+ window.addEventListener(
+ "playback-update",
+ handleUpdateEvent as EventListener
);
-}
\ No newline at end of file
+ window.addEventListener("playback-speed", handleSpeed as EventListener);
+
+ return () => {
+ window.removeEventListener(
+ "playback-seek",
+ handleSeekEvent as EventListener
+ );
+ window.removeEventListener(
+ "playback-update",
+ handleUpdateEvent as EventListener
+ );
+ window.removeEventListener(
+ "playback-speed",
+ handleSpeed as EventListener
+ );
+ };
+ }, [clipStartTime, trimStart, trimEnd, clipDuration, isInClipRange]);
+
+ // Sync playback state
+ useEffect(() => {
+ const video = videoRef.current;
+ if (!video) return;
+
+ if (isPlaying && isInClipRange) {
+ video.play().catch(() => {});
+ } else {
+ video.pause();
+ }
+ }, [isPlaying, isInClipRange]);
+
+ // Sync volume and speed
+ useEffect(() => {
+ const video = videoRef.current;
+ if (!video) return;
+
+ video.volume = volume;
+ video.muted = muted;
+ video.playbackRate = speed;
+ }, [volume, speed, muted]);
+
+ return (
+ e.preventDefault()}
+ />
+ );
+}
diff --git a/apps/web/src/stores/playback-store.ts b/apps/web/src/stores/playback-store.ts
index a4794b7..0127edf 100644
--- a/apps/web/src/stores/playback-store.ts
+++ b/apps/web/src/stores/playback-store.ts
@@ -10,7 +10,7 @@ let playbackTimer: number | null = null;
const startTimer = (store: any) => {
if (playbackTimer) cancelAnimationFrame(playbackTimer);
-
+
// Use requestAnimationFrame for smoother updates
const updateTime = () => {
const state = store();
@@ -18,14 +18,16 @@ const startTimer = (store: any) => {
const now = performance.now();
const delta = (now - lastUpdate) / 1000; // Convert to seconds
lastUpdate = now;
-
- const newTime = state.currentTime + (delta * state.speed);
+
+ const newTime = state.currentTime + delta * state.speed;
if (newTime >= state.duration) {
state.pause();
} else {
state.setCurrentTime(newTime);
// 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);
@@ -47,6 +49,8 @@ export const usePlaybackStore = create((set, get) => ({
currentTime: 0,
duration: 0,
volume: 1,
+ muted: false,
+ previousVolume: 1,
speed: 1.0,
play: () => {
@@ -72,22 +76,53 @@ export const usePlaybackStore = create((set, get) => ({
const { duration } = get();
const clampedTime = Math.max(0, Math.min(duration, time));
set({ currentTime: clampedTime });
-
- // Notify video elements to seek
- const event = new CustomEvent('playback-seek', { detail: { time: clampedTime } });
+
+ const event = new CustomEvent("playback-seek", {
+ detail: { time: clampedTime },
+ });
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) => {
const newSpeed = Math.max(0.1, Math.min(2.0, speed));
set({ speed: newSpeed });
-
- const event = new CustomEvent('playback-speed', { detail: { speed: newSpeed } });
+
+ const event = new CustomEvent("playback-speed", {
+ detail: { speed: newSpeed },
+ });
window.dispatchEvent(event);
},
setDuration: (duration: number) => set({ duration }),
setCurrentTime: (time: number) => set({ currentTime: time }),
-}));
\ No newline at end of file
+
+ 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();
+ }
+ },
+}));
diff --git a/apps/web/src/types/playback.ts b/apps/web/src/types/playback.ts
index 3fbd1a0..8758ea0 100644
--- a/apps/web/src/types/playback.ts
+++ b/apps/web/src/types/playback.ts
@@ -4,6 +4,8 @@ export interface PlaybackState {
duration: number;
volume: number;
speed: number;
+ muted: boolean;
+ previousVolume?: number;
}
export interface PlaybackControls {
@@ -13,4 +15,7 @@ export interface PlaybackControls {
setVolume: (volume: number) => void;
setSpeed: (speed: number) => void;
toggle: () => void;
-}
\ No newline at end of file
+ mute: () => void;
+ unmute: () => void;
+ toggleMute: () => void;
+}