From be1ae20d9089e81cb84a8294be76fe680a24b60f Mon Sep 17 00:00:00 2001 From: Hyteq Date: Sun, 20 Jul 2025 11:00:22 +0300 Subject: format --- apps/web/src/components/editor/audio-waveform.tsx | 196 +- .../components/editor/media-panel/drag-overlay.tsx | 114 +- .../src/components/editor/media-panel/index.tsx | 110 +- .../web/src/components/editor/media-panel/store.ts | 146 +- .../src/components/editor/media-panel/tabbar.tsx | 248 +-- .../components/editor/media-panel/views/media.tsx | 596 ++--- .../components/editor/media-panel/views/text.tsx | 106 +- apps/web/src/components/editor/preview-panel.tsx | 936 ++++---- .../editor/properties-panel/audio-properties.tsx | 10 +- .../components/editor/properties-panel/index.tsx | 166 +- .../editor/properties-panel/media-properties.tsx | 10 +- .../editor/properties-panel/property-item.tsx | 94 +- .../editor/properties-panel/text-properties.tsx | 152 +- apps/web/src/components/editor/selection-box.tsx | 116 +- apps/web/src/components/editor/snap-indicator.tsx | 76 +- apps/web/src/components/editor/speed-control.tsx | 72 +- .../web/src/components/editor/timeline-element.tsx | 664 +++--- .../src/components/editor/timeline-playhead.tsx | 232 +- apps/web/src/components/editor/timeline-track.tsx | 2302 ++++++++++---------- apps/web/src/components/editor/timeline.tsx | 2123 +++++++++--------- 20 files changed, 4236 insertions(+), 4233 deletions(-) (limited to 'apps/web/src/components/editor') diff --git a/apps/web/src/components/editor/audio-waveform.tsx b/apps/web/src/components/editor/audio-waveform.tsx index 75a5ae06..cae2359b 100644 --- a/apps/web/src/components/editor/audio-waveform.tsx +++ b/apps/web/src/components/editor/audio-waveform.tsx @@ -1,115 +1,117 @@ -import React, { useEffect, useRef, useState } from 'react'; -import WaveSurfer from 'wavesurfer.js'; +import React, { useEffect, useRef, useState } from "react"; +import WaveSurfer from "wavesurfer.js"; interface AudioWaveformProps { - audioUrl: string; - height?: number; - className?: string; + audioUrl: string; + height?: number; + className?: string; } -const AudioWaveform: React.FC = ({ - audioUrl, - height = 32, - className = '' +const AudioWaveform: React.FC = ({ + audioUrl, + height = 32, + className = "", }) => { - const waveformRef = useRef(null); - const wavesurfer = useRef(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(false); + const waveformRef = useRef(null); + const wavesurfer = useRef(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(false); - useEffect(() => { - let mounted = true; + useEffect(() => { + let mounted = true; - const initWaveSurfer = async () => { - if (!waveformRef.current || !audioUrl) return; + const initWaveSurfer = async () => { + if (!waveformRef.current || !audioUrl) return; - try { - // Clean up any existing instance - if (wavesurfer.current) { - try { - wavesurfer.current.destroy(); - } catch (e) { - // Silently ignore destroy errors - } - wavesurfer.current = null; - } + try { + // Clean up any existing instance + if (wavesurfer.current) { + try { + wavesurfer.current.destroy(); + } catch (e) { + // Silently ignore destroy errors + } + wavesurfer.current = null; + } - wavesurfer.current = WaveSurfer.create({ - container: waveformRef.current, - waveColor: 'rgba(255, 255, 255, 0.6)', - progressColor: 'rgba(255, 255, 255, 0.9)', - cursorColor: 'transparent', - barWidth: 2, - barGap: 1, - height: height, - normalize: true, - interact: false, - }); + wavesurfer.current = WaveSurfer.create({ + container: waveformRef.current, + waveColor: "rgba(255, 255, 255, 0.6)", + progressColor: "rgba(255, 255, 255, 0.9)", + cursorColor: "transparent", + barWidth: 2, + barGap: 1, + height: height, + normalize: true, + interact: false, + }); - // Event listeners - wavesurfer.current.on('ready', () => { - if (mounted) { - setIsLoading(false); - setError(false); - } - }); + // Event listeners + wavesurfer.current.on("ready", () => { + if (mounted) { + setIsLoading(false); + setError(false); + } + }); - wavesurfer.current.on('error', (err) => { - console.error('WaveSurfer error:', err); - if (mounted) { - setError(true); - setIsLoading(false); - } - }); + wavesurfer.current.on("error", (err) => { + console.error("WaveSurfer error:", err); + if (mounted) { + setError(true); + setIsLoading(false); + } + }); - await wavesurfer.current.load(audioUrl); + await wavesurfer.current.load(audioUrl); + } catch (err) { + console.error("Failed to initialize WaveSurfer:", err); + if (mounted) { + setError(true); + setIsLoading(false); + } + } + }; - } catch (err) { - console.error('Failed to initialize WaveSurfer:', err); - if (mounted) { - setError(true); - setIsLoading(false); - } - } - }; + initWaveSurfer(); - initWaveSurfer(); + return () => { + mounted = false; + if (wavesurfer.current) { + try { + wavesurfer.current.destroy(); + } catch (e) { + // Silently ignore destroy errors + } + wavesurfer.current = null; + } + }; + }, [audioUrl, height]); - return () => { - mounted = false; - if (wavesurfer.current) { - try { - wavesurfer.current.destroy(); - } catch (e) { - // Silently ignore destroy errors - } - wavesurfer.current = null; - } - }; - }, [audioUrl, height]); + if (error) { + return ( +
+ Audio unavailable +
+ ); + } - if (error) { - return ( -
- Audio unavailable -
- ); - } - - return ( -
- {isLoading && ( -
- Loading... -
- )} -
-
- ); + return ( +
+ {isLoading && ( +
+ Loading... +
+ )} +
+
+ ); }; -export default AudioWaveform; \ No newline at end of file +export default AudioWaveform; diff --git a/apps/web/src/components/editor/media-panel/drag-overlay.tsx b/apps/web/src/components/editor/media-panel/drag-overlay.tsx index 3f3820db..0c75d146 100644 --- a/apps/web/src/components/editor/media-panel/drag-overlay.tsx +++ b/apps/web/src/components/editor/media-panel/drag-overlay.tsx @@ -1,57 +1,57 @@ -import { Upload, Plus, Image } from "lucide-react"; -import { Button } from "@/components/ui/button"; - -interface MediaDragOverlayProps { - isVisible: boolean; - isProcessing?: boolean; - progress?: number; - onClick?: () => void; - isEmptyState?: boolean; -} - -export function MediaDragOverlay({ - isVisible, - isProcessing = false, - progress = 0, - onClick, - isEmptyState = false, -}: MediaDragOverlayProps) { - if (!isVisible) return null; - - const handleClick = (e: React.MouseEvent) => { - if (isProcessing || !onClick) return; - e.preventDefault(); - e.stopPropagation(); - onClick(); - }; - - return ( -
-
- -
- -
-

- {isProcessing - ? `Processing your files (${progress}%)` - : "Drag and drop videos, photos, and audio files here"} -

-
- - {isProcessing && ( -
-
-
-
-
- )} -
- ); -} +import { Upload, Plus, Image } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +interface MediaDragOverlayProps { + isVisible: boolean; + isProcessing?: boolean; + progress?: number; + onClick?: () => void; + isEmptyState?: boolean; +} + +export function MediaDragOverlay({ + isVisible, + isProcessing = false, + progress = 0, + onClick, + isEmptyState = false, +}: MediaDragOverlayProps) { + if (!isVisible) return null; + + const handleClick = (e: React.MouseEvent) => { + if (isProcessing || !onClick) return; + e.preventDefault(); + e.stopPropagation(); + onClick(); + }; + + return ( +
+
+ +
+ +
+

+ {isProcessing + ? `Processing your files (${progress}%)` + : "Drag and drop videos, photos, and audio files here"} +

+
+ + {isProcessing && ( +
+
+
+
+
+ )} +
+ ); +} diff --git a/apps/web/src/components/editor/media-panel/index.tsx b/apps/web/src/components/editor/media-panel/index.tsx index 9f7322c7..50fee601 100644 --- a/apps/web/src/components/editor/media-panel/index.tsx +++ b/apps/web/src/components/editor/media-panel/index.tsx @@ -1,55 +1,55 @@ -"use client"; - -import { TabBar } from "./tabbar"; -import { MediaView } from "./views/media"; -import { useMediaPanelStore, Tab } from "./store"; -import { TextView } from "./views/text"; - -export function MediaPanel() { - const { activeTab } = useMediaPanelStore(); - - const viewMap: Record = { - media: , - audio: ( -
Audio view coming soon...
- ), - text: , - stickers: ( -
- Stickers view coming soon... -
- ), - effects: ( -
- Effects view coming soon... -
- ), - transitions: ( -
- Transitions view coming soon... -
- ), - captions: ( -
- Captions view coming soon... -
- ), - filters: ( -
- Filters view coming soon... -
- ), - adjustment: ( -
- Adjustment view coming soon... -
- ), - }; - - return ( -
- -
{viewMap[activeTab]}
-
- ); -} +"use client"; + +import { TabBar } from "./tabbar"; +import { MediaView } from "./views/media"; +import { useMediaPanelStore, Tab } from "./store"; +import { TextView } from "./views/text"; + +export function MediaPanel() { + const { activeTab } = useMediaPanelStore(); + + const viewMap: Record = { + media: , + audio: ( +
Audio view coming soon...
+ ), + text: , + stickers: ( +
+ Stickers view coming soon... +
+ ), + effects: ( +
+ Effects view coming soon... +
+ ), + transitions: ( +
+ Transitions view coming soon... +
+ ), + captions: ( +
+ Captions view coming soon... +
+ ), + filters: ( +
+ Filters view coming soon... +
+ ), + adjustment: ( +
+ Adjustment view coming soon... +
+ ), + }; + + return ( +
+ +
{viewMap[activeTab]}
+
+ ); +} diff --git a/apps/web/src/components/editor/media-panel/store.ts b/apps/web/src/components/editor/media-panel/store.ts index 9668f285..0150c39a 100644 --- a/apps/web/src/components/editor/media-panel/store.ts +++ b/apps/web/src/components/editor/media-panel/store.ts @@ -1,73 +1,73 @@ -import { - CaptionsIcon, - ArrowLeftRightIcon, - SparklesIcon, - StickerIcon, - MusicIcon, - VideoIcon, - BlendIcon, - SlidersHorizontalIcon, - LucideIcon, - TypeIcon, -} from "lucide-react"; -import { create } from "zustand"; - -export type Tab = - | "media" - | "audio" - | "text" - | "stickers" - | "effects" - | "transitions" - | "captions" - | "filters" - | "adjustment"; - -export const tabs: { [key in Tab]: { icon: LucideIcon; label: string } } = { - media: { - icon: VideoIcon, - label: "Media", - }, - audio: { - icon: MusicIcon, - label: "Audio", - }, - text: { - icon: TypeIcon, - label: "Text", - }, - stickers: { - icon: StickerIcon, - label: "Stickers", - }, - effects: { - icon: SparklesIcon, - label: "Effects", - }, - transitions: { - icon: ArrowLeftRightIcon, - label: "Transitions", - }, - captions: { - icon: CaptionsIcon, - label: "Captions", - }, - filters: { - icon: BlendIcon, - label: "Filters", - }, - adjustment: { - icon: SlidersHorizontalIcon, - label: "Adjustment", - }, -}; - -interface MediaPanelStore { - activeTab: Tab; - setActiveTab: (tab: Tab) => void; -} - -export const useMediaPanelStore = create((set) => ({ - activeTab: "media", - setActiveTab: (tab) => set({ activeTab: tab }), -})); +import { + CaptionsIcon, + ArrowLeftRightIcon, + SparklesIcon, + StickerIcon, + MusicIcon, + VideoIcon, + BlendIcon, + SlidersHorizontalIcon, + LucideIcon, + TypeIcon, +} from "lucide-react"; +import { create } from "zustand"; + +export type Tab = + | "media" + | "audio" + | "text" + | "stickers" + | "effects" + | "transitions" + | "captions" + | "filters" + | "adjustment"; + +export const tabs: { [key in Tab]: { icon: LucideIcon; label: string } } = { + media: { + icon: VideoIcon, + label: "Media", + }, + audio: { + icon: MusicIcon, + label: "Audio", + }, + text: { + icon: TypeIcon, + label: "Text", + }, + stickers: { + icon: StickerIcon, + label: "Stickers", + }, + effects: { + icon: SparklesIcon, + label: "Effects", + }, + transitions: { + icon: ArrowLeftRightIcon, + label: "Transitions", + }, + captions: { + icon: CaptionsIcon, + label: "Captions", + }, + filters: { + icon: BlendIcon, + label: "Filters", + }, + adjustment: { + icon: SlidersHorizontalIcon, + label: "Adjustment", + }, +}; + +interface MediaPanelStore { + activeTab: Tab; + setActiveTab: (tab: Tab) => void; +} + +export const useMediaPanelStore = create((set) => ({ + activeTab: "media", + setActiveTab: (tab) => set({ activeTab: tab }), +})); diff --git a/apps/web/src/components/editor/media-panel/tabbar.tsx b/apps/web/src/components/editor/media-panel/tabbar.tsx index b05bee7e..50793243 100644 --- a/apps/web/src/components/editor/media-panel/tabbar.tsx +++ b/apps/web/src/components/editor/media-panel/tabbar.tsx @@ -1,124 +1,124 @@ -"use client"; - -import { cn } from "@/lib/utils"; -import { Tab, tabs, useMediaPanelStore } from "./store"; -import { Button } from "@/components/ui/button"; -import { ChevronRight, ChevronLeft } from "lucide-react"; -import { useRef, useState, useEffect } from "react"; - -export function TabBar() { - const { activeTab, setActiveTab } = useMediaPanelStore(); - const scrollContainerRef = useRef(null); - const [isAtEnd, setIsAtEnd] = useState(false); - const [isAtStart, setIsAtStart] = useState(true); - - const scrollToEnd = () => { - if (scrollContainerRef.current) { - scrollContainerRef.current.scrollTo({ - left: scrollContainerRef.current.scrollWidth, - }); - setIsAtEnd(true); - setIsAtStart(false); - } - }; - - const scrollToStart = () => { - if (scrollContainerRef.current) { - scrollContainerRef.current.scrollTo({ - left: 0, - }); - setIsAtStart(true); - setIsAtEnd(false); - } - }; - - const checkScrollPosition = () => { - if (scrollContainerRef.current) { - const { scrollLeft, scrollWidth, clientWidth } = - scrollContainerRef.current; - const isAtEndNow = scrollLeft + clientWidth >= scrollWidth - 1; - const isAtStartNow = scrollLeft <= 1; - setIsAtEnd(isAtEndNow); - setIsAtStart(isAtStartNow); - } - }; - - // We're using useEffect because we need to sync with external DOM scroll events - useEffect(() => { - const container = scrollContainerRef.current; - if (!container) return; - - checkScrollPosition(); - container.addEventListener("scroll", checkScrollPosition); - - const resizeObserver = new ResizeObserver(checkScrollPosition); - resizeObserver.observe(container); - - return () => { - container.removeEventListener("scroll", checkScrollPosition); - resizeObserver.disconnect(); - }; - }, []); - - return ( -
- -
- {(Object.keys(tabs) as Tab[]).map((tabKey) => { - const tab = tabs[tabKey]; - return ( -
setActiveTab(tabKey)} - key={tabKey} - > - - {tab.label} -
- ); - })} -
- -
- ); -} - -function ScrollButton({ - direction, - onClick, - isVisible, -}: { - direction: "left" | "right"; - onClick: () => void; - isVisible: boolean; -}) { - if (!isVisible) return null; - - const Icon = direction === "left" ? ChevronLeft : ChevronRight; - - return ( -
- -
- ); -} +"use client"; + +import { cn } from "@/lib/utils"; +import { Tab, tabs, useMediaPanelStore } from "./store"; +import { Button } from "@/components/ui/button"; +import { ChevronRight, ChevronLeft } from "lucide-react"; +import { useRef, useState, useEffect } from "react"; + +export function TabBar() { + const { activeTab, setActiveTab } = useMediaPanelStore(); + const scrollContainerRef = useRef(null); + const [isAtEnd, setIsAtEnd] = useState(false); + const [isAtStart, setIsAtStart] = useState(true); + + const scrollToEnd = () => { + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollTo({ + left: scrollContainerRef.current.scrollWidth, + }); + setIsAtEnd(true); + setIsAtStart(false); + } + }; + + const scrollToStart = () => { + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollTo({ + left: 0, + }); + setIsAtStart(true); + setIsAtEnd(false); + } + }; + + const checkScrollPosition = () => { + if (scrollContainerRef.current) { + const { scrollLeft, scrollWidth, clientWidth } = + scrollContainerRef.current; + const isAtEndNow = scrollLeft + clientWidth >= scrollWidth - 1; + const isAtStartNow = scrollLeft <= 1; + setIsAtEnd(isAtEndNow); + setIsAtStart(isAtStartNow); + } + }; + + // We're using useEffect because we need to sync with external DOM scroll events + useEffect(() => { + const container = scrollContainerRef.current; + if (!container) return; + + checkScrollPosition(); + container.addEventListener("scroll", checkScrollPosition); + + const resizeObserver = new ResizeObserver(checkScrollPosition); + resizeObserver.observe(container); + + return () => { + container.removeEventListener("scroll", checkScrollPosition); + resizeObserver.disconnect(); + }; + }, []); + + return ( +
+ +
+ {(Object.keys(tabs) as Tab[]).map((tabKey) => { + const tab = tabs[tabKey]; + return ( +
setActiveTab(tabKey)} + key={tabKey} + > + + {tab.label} +
+ ); + })} +
+ +
+ ); +} + +function ScrollButton({ + direction, + onClick, + isVisible, +}: { + direction: "left" | "right"; + onClick: () => void; + isVisible: boolean; +}) { + if (!isVisible) return null; + + const Icon = direction === "left" ? ChevronLeft : ChevronRight; + + return ( +
+ +
+ ); +} diff --git a/apps/web/src/components/editor/media-panel/views/media.tsx b/apps/web/src/components/editor/media-panel/views/media.tsx index 631fe39a..712dd922 100644 --- a/apps/web/src/components/editor/media-panel/views/media.tsx +++ b/apps/web/src/components/editor/media-panel/views/media.tsx @@ -1,298 +1,298 @@ -"use client"; - -import { useDragDrop } from "@/hooks/use-drag-drop"; -import { processMediaFiles } from "@/lib/media-processing"; -import { useMediaStore, type MediaItem } from "@/stores/media-store"; -import { Image, Loader2, Music, Plus, Video } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; -import { toast } from "sonner"; -import { Button } from "@/components/ui/button"; -import { MediaDragOverlay } from "@/components/editor/media-panel/drag-overlay"; -import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuTrigger, -} from "@/components/ui/context-menu"; -import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { DraggableMediaItem } from "@/components/ui/draggable-item"; -import { useProjectStore } from "@/stores/project-store"; -import { useTimelineStore } from "@/stores/timeline-store"; - -export function MediaView() { - const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore(); - const { activeProject } = useProjectStore(); - const fileInputRef = useRef(null); - const [isProcessing, setIsProcessing] = useState(false); - const [progress, setProgress] = useState(0); - const [searchQuery, setSearchQuery] = useState(""); - const [mediaFilter, setMediaFilter] = useState("all"); - - const processFiles = async (files: FileList | File[]) => { - if (!files || files.length === 0) return; - if (!activeProject) { - toast.error("No active project"); - return; - } - - setIsProcessing(true); - setProgress(0); - try { - // Process files (extract metadata, generate thumbnails, etc.) - const processedItems = await processMediaFiles(files, (p) => - setProgress(p) - ); - // Add each processed media item to the store - for (const item of processedItems) { - await addMediaItem(activeProject.id, item); - } - } catch (error) { - // Show error toast if processing fails - console.error("Error processing files:", error); - toast.error("Failed to process files"); - } finally { - setIsProcessing(false); - setProgress(0); - } - }; - - const { isDragOver, dragProps } = useDragDrop({ - // When files are dropped, process them - onDrop: processFiles, - }); - - const handleFileSelect = () => fileInputRef.current?.click(); // Open file picker - - const handleFileChange = (e: React.ChangeEvent) => { - // When files are selected via file picker, process them - if (e.target.files) processFiles(e.target.files); - e.target.value = ""; // Reset input - }; - - const handleRemove = async (e: React.MouseEvent, id: string) => { - // Remove a media item from the store - e.stopPropagation(); - - if (!activeProject) { - toast.error("No active project"); - return; - } - - // Media store now handles cascade deletion automatically - await removeMediaItem(activeProject.id, id); - }; - - const formatDuration = (duration: number) => { - // Format seconds as mm:ss - const min = Math.floor(duration / 60); - const sec = Math.floor(duration % 60); - return `${min}:${sec.toString().padStart(2, "0")}`; - }; - - const [filteredMediaItems, setFilteredMediaItems] = useState(mediaItems); - - useEffect(() => { - const filtered = mediaItems.filter((item) => { - if (mediaFilter && mediaFilter !== "all" && item.type !== mediaFilter) { - return false; - } - - if ( - searchQuery && - !item.name.toLowerCase().includes(searchQuery.toLowerCase()) - ) { - return false; - } - - return true; - }); - - setFilteredMediaItems(filtered); - }, [mediaItems, mediaFilter, searchQuery]); - - const renderPreview = (item: MediaItem) => { - // Render a preview for each media type (image, video, audio, unknown) - if (item.type === "image") { - return ( -
- {item.name} -
- ); - } - - if (item.type === "video") { - if (item.thumbnailUrl) { - return ( -
- {item.name} -
-
- {item.duration && ( -
- {formatDuration(item.duration)} -
- )} -
- ); - } - return ( -
-
- ); - } - - if (item.type === "audio") { - return ( -
- - Audio - {item.duration && ( - - {formatDuration(item.duration)} - - )} -
- ); - } - - return ( -
- - Unknown -
- ); - }; - - return ( - <> - {/* Hidden file input for uploading media */} - - -
-
- {/* Search and filter controls */} -
- - setSearchQuery(e.target.value)} - /> - -
-
- -
- {isDragOver || filteredMediaItems.length === 0 ? ( - - ) : ( -
- {/* Render each media item as a draggable button */} - {filteredMediaItems.map((item) => ( - - - - useTimelineStore - .getState() - .addMediaAtTime(item, currentTime) - } - rounded={false} - /> - - - Export clips - handleRemove(e, item.id)} - > - Delete - - - - ))} -
- )} -
-
- - ); -} +"use client"; + +import { useDragDrop } from "@/hooks/use-drag-drop"; +import { processMediaFiles } from "@/lib/media-processing"; +import { useMediaStore, type MediaItem } from "@/stores/media-store"; +import { Image, Loader2, Music, Plus, Video } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { MediaDragOverlay } from "@/components/editor/media-panel/drag-overlay"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { DraggableMediaItem } from "@/components/ui/draggable-item"; +import { useProjectStore } from "@/stores/project-store"; +import { useTimelineStore } from "@/stores/timeline-store"; + +export function MediaView() { + const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore(); + const { activeProject } = useProjectStore(); + const fileInputRef = useRef(null); + const [isProcessing, setIsProcessing] = useState(false); + const [progress, setProgress] = useState(0); + const [searchQuery, setSearchQuery] = useState(""); + const [mediaFilter, setMediaFilter] = useState("all"); + + const processFiles = async (files: FileList | File[]) => { + if (!files || files.length === 0) return; + if (!activeProject) { + toast.error("No active project"); + return; + } + + setIsProcessing(true); + setProgress(0); + try { + // Process files (extract metadata, generate thumbnails, etc.) + const processedItems = await processMediaFiles(files, (p) => + setProgress(p), + ); + // Add each processed media item to the store + for (const item of processedItems) { + await addMediaItem(activeProject.id, item); + } + } catch (error) { + // Show error toast if processing fails + console.error("Error processing files:", error); + toast.error("Failed to process files"); + } finally { + setIsProcessing(false); + setProgress(0); + } + }; + + const { isDragOver, dragProps } = useDragDrop({ + // When files are dropped, process them + onDrop: processFiles, + }); + + const handleFileSelect = () => fileInputRef.current?.click(); // Open file picker + + const handleFileChange = (e: React.ChangeEvent) => { + // When files are selected via file picker, process them + if (e.target.files) processFiles(e.target.files); + e.target.value = ""; // Reset input + }; + + const handleRemove = async (e: React.MouseEvent, id: string) => { + // Remove a media item from the store + e.stopPropagation(); + + if (!activeProject) { + toast.error("No active project"); + return; + } + + // Media store now handles cascade deletion automatically + await removeMediaItem(activeProject.id, id); + }; + + const formatDuration = (duration: number) => { + // Format seconds as mm:ss + const min = Math.floor(duration / 60); + const sec = Math.floor(duration % 60); + return `${min}:${sec.toString().padStart(2, "0")}`; + }; + + const [filteredMediaItems, setFilteredMediaItems] = useState(mediaItems); + + useEffect(() => { + const filtered = mediaItems.filter((item) => { + if (mediaFilter && mediaFilter !== "all" && item.type !== mediaFilter) { + return false; + } + + if ( + searchQuery && + !item.name.toLowerCase().includes(searchQuery.toLowerCase()) + ) { + return false; + } + + return true; + }); + + setFilteredMediaItems(filtered); + }, [mediaItems, mediaFilter, searchQuery]); + + const renderPreview = (item: MediaItem) => { + // Render a preview for each media type (image, video, audio, unknown) + if (item.type === "image") { + return ( +
+ {item.name} +
+ ); + } + + if (item.type === "video") { + if (item.thumbnailUrl) { + return ( +
+ {item.name} +
+
+ {item.duration && ( +
+ {formatDuration(item.duration)} +
+ )} +
+ ); + } + return ( +
+
+ ); + } + + if (item.type === "audio") { + return ( +
+ + Audio + {item.duration && ( + + {formatDuration(item.duration)} + + )} +
+ ); + } + + return ( +
+ + Unknown +
+ ); + }; + + return ( + <> + {/* Hidden file input for uploading media */} + + +
+
+ {/* Search and filter controls */} +
+ + setSearchQuery(e.target.value)} + /> + +
+
+ +
+ {isDragOver || filteredMediaItems.length === 0 ? ( + + ) : ( +
+ {/* Render each media item as a draggable button */} + {filteredMediaItems.map((item) => ( + + + + useTimelineStore + .getState() + .addMediaAtTime(item, currentTime) + } + rounded={false} + /> + + + Export clips + handleRemove(e, item.id)} + > + Delete + + + + ))} +
+ )} +
+
+ + ); +} diff --git a/apps/web/src/components/editor/media-panel/views/text.tsx b/apps/web/src/components/editor/media-panel/views/text.tsx index 6d0d310b..b47edd8d 100644 --- a/apps/web/src/components/editor/media-panel/views/text.tsx +++ b/apps/web/src/components/editor/media-panel/views/text.tsx @@ -1,53 +1,53 @@ -import { DraggableMediaItem } from "@/components/ui/draggable-item"; -import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; -import { useTimelineStore } from "@/stores/timeline-store"; -import { type TextElement } from "@/types/timeline"; - -let textData: TextElement = { - id: "default-text", - type: "text", - name: "Default text", - content: "Default text", - fontSize: 48, - fontFamily: "Arial", - color: "#ffffff", - backgroundColor: "transparent", - textAlign: "center" as const, - fontWeight: "normal" as const, - fontStyle: "normal" as const, - textDecoration: "none" as const, - x: 0, - y: 0, - rotation: 0, - opacity: 1, - duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION, - startTime: 0, - trimStart: 0, - trimEnd: 0, -}; - -export function TextView() { - return ( -
- - Default text -
- } - dragData={{ - id: textData.id, - type: textData.type, - name: textData.name, - content: textData.content, - }} - aspectRatio={1} - onAddToTimeline={(currentTime) => - useTimelineStore.getState().addTextAtTime(textData, currentTime) - } - showLabel={false} - /> -
- ); -} +import { DraggableMediaItem } from "@/components/ui/draggable-item"; +import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; +import { useTimelineStore } from "@/stores/timeline-store"; +import { type TextElement } from "@/types/timeline"; + +let textData: TextElement = { + id: "default-text", + type: "text", + name: "Default text", + content: "Default text", + fontSize: 48, + fontFamily: "Arial", + color: "#ffffff", + backgroundColor: "transparent", + textAlign: "center" as const, + fontWeight: "normal" as const, + fontStyle: "normal" as const, + textDecoration: "none" as const, + x: 0, + y: 0, + rotation: 0, + opacity: 1, + duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION, + startTime: 0, + trimStart: 0, + trimEnd: 0, +}; + +export function TextView() { + return ( +
+ + Default text +
+ } + dragData={{ + id: textData.id, + type: textData.type, + name: textData.name, + content: textData.content, + }} + aspectRatio={1} + onAddToTimeline={(currentTime) => + useTimelineStore.getState().addTextAtTime(textData, currentTime) + } + showLabel={false} + /> +
+ ); +} diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index 2ecee391..ea66de74 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -10,11 +10,11 @@ import { VideoPlayer } from "@/components/ui/video-player"; import { AudioPlayer } from "@/components/ui/audio-player"; import { Button } from "@/components/ui/button"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, - DropdownMenuSeparator, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; import { Play, Pause, Expand } from "lucide-react"; import { useState, useRef, useEffect } from "react"; @@ -25,473 +25,473 @@ import { BackgroundSettings } from "../background-settings"; import { useProjectStore } from "@/stores/project-store"; interface ActiveElement { - element: TimelineElement; - track: TimelineTrack; - mediaItem: MediaItem | null; + element: TimelineElement; + track: TimelineTrack; + mediaItem: MediaItem | null; } export function PreviewPanel() { - const { tracks } = useTimelineStore(); - const { mediaItems } = useMediaStore(); - const { currentTime } = usePlaybackStore(); - const { canvasSize } = useEditorStore(); - const previewRef = useRef(null); - const containerRef = useRef(null); - const [previewDimensions, setPreviewDimensions] = useState({ - width: 0, - height: 0, - }); - const { activeProject } = useProjectStore(); - - // Calculate optimal preview size that fits in container while maintaining aspect ratio - useEffect(() => { - const updatePreviewSize = () => { - if (!containerRef.current) return; - - const container = containerRef.current.getBoundingClientRect(); - const computedStyle = getComputedStyle(containerRef.current); - - // Get padding values - const paddingTop = parseFloat(computedStyle.paddingTop); - const paddingBottom = parseFloat(computedStyle.paddingBottom); - const paddingLeft = parseFloat(computedStyle.paddingLeft); - const paddingRight = parseFloat(computedStyle.paddingRight); - - // Get gap value (gap-4 = 1rem = 16px) - const gap = parseFloat(computedStyle.gap) || 16; - - // Get toolbar height if it exists - const toolbar = containerRef.current.querySelector("[data-toolbar]"); - const toolbarHeight = toolbar - ? toolbar.getBoundingClientRect().height - : 0; - - // Calculate available space after accounting for padding, gap, and toolbar - const availableWidth = container.width - paddingLeft - paddingRight; - const availableHeight = - container.height - - paddingTop - - paddingBottom - - toolbarHeight - - (toolbarHeight > 0 ? gap : 0); - - const targetRatio = canvasSize.width / canvasSize.height; - const containerRatio = availableWidth / availableHeight; - - let width, height; - - if (containerRatio > targetRatio) { - // Container is wider - constrain by height - height = availableHeight; - width = height * targetRatio; - } else { - // Container is taller - constrain by width - width = availableWidth; - height = width / targetRatio; - } - - setPreviewDimensions({ width, height }); - }; - - updatePreviewSize(); - - const resizeObserver = new ResizeObserver(updatePreviewSize); - if (containerRef.current) { - resizeObserver.observe(containerRef.current); - } - - return () => resizeObserver.disconnect(); - }, [canvasSize.width, canvasSize.height]); - - // Get active elements at current time - const getActiveElements = (): ActiveElement[] => { - const activeElements: ActiveElement[] = []; - - tracks.forEach((track) => { - track.elements.forEach((element) => { - const elementStart = element.startTime; - const elementEnd = - element.startTime + - (element.duration - element.trimStart - element.trimEnd); - - if (currentTime >= elementStart && currentTime < elementEnd) { - let mediaItem = null; - - // Only get media item for media elements - if (element.type === "media") { - mediaItem = - element.mediaId === "test" - ? null // Test elements don't have a real media item - : mediaItems.find((item) => item.id === element.mediaId) || - null; - } - - activeElements.push({ element, track, mediaItem }); - } - }); - }); - - return activeElements; - }; - - const activeElements = getActiveElements(); - - // Check if there are any elements in the timeline at all - const hasAnyElements = tracks.some((track) => track.elements.length > 0); - - // Get media elements for blur background (video/image only) - const getBlurBackgroundElements = (): ActiveElement[] => { - return activeElements.filter( - ({ element, mediaItem }) => - element.type === "media" && - mediaItem && - (mediaItem.type === "video" || mediaItem.type === "image") && - element.mediaId !== "test" // Exclude test elements - ); - }; - - const blurBackgroundElements = getBlurBackgroundElements(); - - // Render blur background layer - const renderBlurBackground = () => { - if ( - !activeProject?.backgroundType || - activeProject.backgroundType !== "blur" || - blurBackgroundElements.length === 0 - ) { - return null; - } - - // Use the first media element for background (could be enhanced to use primary/focused element) - const backgroundElement = blurBackgroundElements[0]; - const { element, mediaItem } = backgroundElement; - - if (!mediaItem) return null; - - const blurIntensity = activeProject.blurIntensity || 8; - - if (mediaItem.type === "video") { - return ( -
- -
- ); - } - - if (mediaItem.type === "image") { - return ( -
- {mediaItem.name} -
- ); - } - - return null; - }; - - // Render an element - const renderElement = (elementData: ActiveElement, index: number) => { - const { element, mediaItem } = elementData; - - // Text elements - if (element.type === "text") { - const fontClassName = - FONT_CLASS_MAP[element.fontFamily as keyof typeof FONT_CLASS_MAP] || ""; - - const scaleRatio = previewDimensions.width / canvasSize.width; - - return ( -
-
- {element.content} -
-
- ); - } - - // Media elements - if (element.type === "media") { - // Test elements - if (!mediaItem || element.mediaId === "test") { - return ( -
-
-
🎬
-

{element.name}

-
-
- ); - } - - // Video elements - if (mediaItem.type === "video") { - return ( -
- -
- ); - } - - // Image elements - if (mediaItem.type === "image") { - return ( -
- {mediaItem.name} -
- ); - } - - // Audio elements (no visual representation) - if (mediaItem.type === "audio") { - return ( -
- -
- ); - } - } - - return null; - }; - - return ( -
-
-
- {hasAnyElements ? ( -
- {renderBlurBackground()} - {activeElements.length === 0 ? ( -
- No elements at current time -
- ) : ( - activeElements.map((elementData, index) => - renderElement(elementData, index) - ) - )} - {/* Show message when blur is selected but no media available */} - {activeProject?.backgroundType === "blur" && - blurBackgroundElements.length === 0 && - activeElements.length > 0 && ( -
- Add a video or image to use blur background -
- )} -
- ) : null} - -
- - -
-
- ); + const { tracks } = useTimelineStore(); + const { mediaItems } = useMediaStore(); + const { currentTime } = usePlaybackStore(); + const { canvasSize } = useEditorStore(); + const previewRef = useRef(null); + const containerRef = useRef(null); + const [previewDimensions, setPreviewDimensions] = useState({ + width: 0, + height: 0, + }); + const { activeProject } = useProjectStore(); + + // Calculate optimal preview size that fits in container while maintaining aspect ratio + useEffect(() => { + const updatePreviewSize = () => { + if (!containerRef.current) return; + + const container = containerRef.current.getBoundingClientRect(); + const computedStyle = getComputedStyle(containerRef.current); + + // Get padding values + const paddingTop = parseFloat(computedStyle.paddingTop); + const paddingBottom = parseFloat(computedStyle.paddingBottom); + const paddingLeft = parseFloat(computedStyle.paddingLeft); + const paddingRight = parseFloat(computedStyle.paddingRight); + + // Get gap value (gap-4 = 1rem = 16px) + const gap = parseFloat(computedStyle.gap) || 16; + + // Get toolbar height if it exists + const toolbar = containerRef.current.querySelector("[data-toolbar]"); + const toolbarHeight = toolbar + ? toolbar.getBoundingClientRect().height + : 0; + + // Calculate available space after accounting for padding, gap, and toolbar + const availableWidth = container.width - paddingLeft - paddingRight; + const availableHeight = + container.height - + paddingTop - + paddingBottom - + toolbarHeight - + (toolbarHeight > 0 ? gap : 0); + + const targetRatio = canvasSize.width / canvasSize.height; + const containerRatio = availableWidth / availableHeight; + + let width, height; + + if (containerRatio > targetRatio) { + // Container is wider - constrain by height + height = availableHeight; + width = height * targetRatio; + } else { + // Container is taller - constrain by width + width = availableWidth; + height = width / targetRatio; + } + + setPreviewDimensions({ width, height }); + }; + + updatePreviewSize(); + + const resizeObserver = new ResizeObserver(updatePreviewSize); + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + return () => resizeObserver.disconnect(); + }, [canvasSize.width, canvasSize.height]); + + // Get active elements at current time + const getActiveElements = (): ActiveElement[] => { + const activeElements: ActiveElement[] = []; + + tracks.forEach((track) => { + track.elements.forEach((element) => { + const elementStart = element.startTime; + const elementEnd = + element.startTime + + (element.duration - element.trimStart - element.trimEnd); + + if (currentTime >= elementStart && currentTime < elementEnd) { + let mediaItem = null; + + // Only get media item for media elements + if (element.type === "media") { + mediaItem = + element.mediaId === "test" + ? null // Test elements don't have a real media item + : mediaItems.find((item) => item.id === element.mediaId) || + null; + } + + activeElements.push({ element, track, mediaItem }); + } + }); + }); + + return activeElements; + }; + + const activeElements = getActiveElements(); + + // Check if there are any elements in the timeline at all + const hasAnyElements = tracks.some((track) => track.elements.length > 0); + + // Get media elements for blur background (video/image only) + const getBlurBackgroundElements = (): ActiveElement[] => { + return activeElements.filter( + ({ element, mediaItem }) => + element.type === "media" && + mediaItem && + (mediaItem.type === "video" || mediaItem.type === "image") && + element.mediaId !== "test", // Exclude test elements + ); + }; + + const blurBackgroundElements = getBlurBackgroundElements(); + + // Render blur background layer + const renderBlurBackground = () => { + if ( + !activeProject?.backgroundType || + activeProject.backgroundType !== "blur" || + blurBackgroundElements.length === 0 + ) { + return null; + } + + // Use the first media element for background (could be enhanced to use primary/focused element) + const backgroundElement = blurBackgroundElements[0]; + const { element, mediaItem } = backgroundElement; + + if (!mediaItem) return null; + + const blurIntensity = activeProject.blurIntensity || 8; + + if (mediaItem.type === "video") { + return ( +
+ +
+ ); + } + + if (mediaItem.type === "image") { + return ( +
+ {mediaItem.name} +
+ ); + } + + return null; + }; + + // Render an element + const renderElement = (elementData: ActiveElement, index: number) => { + const { element, mediaItem } = elementData; + + // Text elements + if (element.type === "text") { + const fontClassName = + FONT_CLASS_MAP[element.fontFamily as keyof typeof FONT_CLASS_MAP] || ""; + + const scaleRatio = previewDimensions.width / canvasSize.width; + + return ( +
+
+ {element.content} +
+
+ ); + } + + // Media elements + if (element.type === "media") { + // Test elements + if (!mediaItem || element.mediaId === "test") { + return ( +
+
+
🎬
+

{element.name}

+
+
+ ); + } + + // Video elements + if (mediaItem.type === "video") { + return ( +
+ +
+ ); + } + + // Image elements + if (mediaItem.type === "image") { + return ( +
+ {mediaItem.name} +
+ ); + } + + // Audio elements (no visual representation) + if (mediaItem.type === "audio") { + return ( +
+ +
+ ); + } + } + + return null; + }; + + return ( +
+
+
+ {hasAnyElements ? ( +
+ {renderBlurBackground()} + {activeElements.length === 0 ? ( +
+ No elements at current time +
+ ) : ( + activeElements.map((elementData, index) => + renderElement(elementData, index), + ) + )} + {/* Show message when blur is selected but no media available */} + {activeProject?.backgroundType === "blur" && + blurBackgroundElements.length === 0 && + activeElements.length > 0 && ( +
+ Add a video or image to use blur background +
+ )} +
+ ) : null} + +
+ + +
+
+ ); } function PreviewToolbar({ hasAnyElements }: { hasAnyElements: boolean }) { - const { isPlaying, toggle, currentTime } = usePlaybackStore(); - const { setCanvasSize, setCanvasSizeToOriginal } = useEditorStore(); - const { getTotalDuration } = useTimelineStore(); - const { activeProject } = useProjectStore(); - const { - currentPreset, - isOriginal, - getOriginalAspectRatio, - getDisplayName, - canvasPresets, - } = useAspectRatio(); - - const handlePresetSelect = (preset: { width: number; height: number }) => { - setCanvasSize({ width: preset.width, height: preset.height }); - }; - - const handleOriginalSelect = () => { - const aspectRatio = getOriginalAspectRatio(); - setCanvasSizeToOriginal(aspectRatio); - }; - - return ( -
-
-

- - {formatTimeCode( - currentTime, - "HH:MM:SS:FF", - activeProject?.fps || 30 - )} - - / - - {formatTimeCode( - getTotalDuration(), - "HH:MM:SS:FF", - activeProject?.fps || 30 - )} - -

-
- -
- - - - - - - - Original - - - {canvasPresets.map((preset) => ( - handlePresetSelect(preset)} - className={cn( - "text-xs", - currentPreset?.name === preset.name && "font-semibold" - )} - > - {preset.name} - - ))} - - - -
-
- ); + const { isPlaying, toggle, currentTime } = usePlaybackStore(); + const { setCanvasSize, setCanvasSizeToOriginal } = useEditorStore(); + const { getTotalDuration } = useTimelineStore(); + const { activeProject } = useProjectStore(); + const { + currentPreset, + isOriginal, + getOriginalAspectRatio, + getDisplayName, + canvasPresets, + } = useAspectRatio(); + + const handlePresetSelect = (preset: { width: number; height: number }) => { + setCanvasSize({ width: preset.width, height: preset.height }); + }; + + const handleOriginalSelect = () => { + const aspectRatio = getOriginalAspectRatio(); + setCanvasSizeToOriginal(aspectRatio); + }; + + return ( +
+
+

+ + {formatTimeCode( + currentTime, + "HH:MM:SS:FF", + activeProject?.fps || 30, + )} + + / + + {formatTimeCode( + getTotalDuration(), + "HH:MM:SS:FF", + activeProject?.fps || 30, + )} + +

+
+ +
+ + + + + + + + Original + + + {canvasPresets.map((preset) => ( + handlePresetSelect(preset)} + className={cn( + "text-xs", + currentPreset?.name === preset.name && "font-semibold", + )} + > + {preset.name} + + ))} + + + +
+
+ ); } diff --git a/apps/web/src/components/editor/properties-panel/audio-properties.tsx b/apps/web/src/components/editor/properties-panel/audio-properties.tsx index 384f0c5e..5e2a781f 100644 --- a/apps/web/src/components/editor/properties-panel/audio-properties.tsx +++ b/apps/web/src/components/editor/properties-panel/audio-properties.tsx @@ -1,5 +1,5 @@ -import { MediaElement } from "@/types/timeline"; - -export function AudioProperties({ element }: { element: MediaElement }) { - return
Audio properties
; -} \ No newline at end of file +import { MediaElement } from "@/types/timeline"; + +export function AudioProperties({ element }: { element: MediaElement }) { + return
Audio properties
; +} diff --git a/apps/web/src/components/editor/properties-panel/index.tsx b/apps/web/src/components/editor/properties-panel/index.tsx index 9e28850f..7d2845be 100644 --- a/apps/web/src/components/editor/properties-panel/index.tsx +++ b/apps/web/src/components/editor/properties-panel/index.tsx @@ -10,100 +10,100 @@ import { AudioProperties } from "./audio-properties"; import { MediaProperties } from "./media-properties"; import { TextProperties } from "./text-properties"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from "../../ui/select"; import { FPS_PRESETS } from "@/constants/timeline-constants"; export function PropertiesPanel() { - const { activeProject, updateProjectFps } = useProjectStore(); - const { getDisplayName, canvasSize } = useAspectRatio(); - const { selectedElements, tracks } = useTimelineStore(); - const { mediaItems } = useMediaStore(); + const { activeProject, updateProjectFps } = useProjectStore(); + const { getDisplayName, canvasSize } = useAspectRatio(); + const { selectedElements, tracks } = useTimelineStore(); + const { mediaItems } = useMediaStore(); - const handleFpsChange = (value: string) => { - const fps = parseFloat(value); - if (!isNaN(fps) && fps > 0) { - updateProjectFps(fps); - } - }; + const handleFpsChange = (value: string) => { + const fps = parseFloat(value); + if (!isNaN(fps) && fps > 0) { + updateProjectFps(fps); + } + }; - const emptyView = ( -
- {/* Media Properties */} -
- - - -
- - -
-
-
- ); + const emptyView = ( +
+ {/* Media Properties */} +
+ + + +
+ + +
+
+
+ ); - return ( - - {selectedElements.length > 0 - ? selectedElements.map(({ trackId, elementId }) => { - const track = tracks.find((t) => t.id === trackId); - const element = track?.elements.find((e) => e.id === elementId); + return ( + + {selectedElements.length > 0 + ? selectedElements.map(({ trackId, elementId }) => { + const track = tracks.find((t) => t.id === trackId); + const element = track?.elements.find((e) => e.id === elementId); - if (element?.type === "text") { - return ( -
- -
- ); - } - if (element?.type === "media") { - const mediaItem = mediaItems.find( - (item) => item.id === element.mediaId - ); + if (element?.type === "text") { + return ( +
+ +
+ ); + } + if (element?.type === "media") { + const mediaItem = mediaItems.find( + (item) => item.id === element.mediaId, + ); - if (mediaItem?.type === "audio") { - return ; - } + if (mediaItem?.type === "audio") { + return ; + } - return ( -
- -
- ); - } - return null; - }) - : emptyView} -
- ); + return ( +
+ +
+ ); + } + return null; + }) + : emptyView} +
+ ); } function PropertyItem({ label, value }: { label: string; value: string }) { - return ( -
- - {value} -
- ); + return ( +
+ + {value} +
+ ); } diff --git a/apps/web/src/components/editor/properties-panel/media-properties.tsx b/apps/web/src/components/editor/properties-panel/media-properties.tsx index c2c514cf..fe2d448e 100644 --- a/apps/web/src/components/editor/properties-panel/media-properties.tsx +++ b/apps/web/src/components/editor/properties-panel/media-properties.tsx @@ -1,5 +1,5 @@ -import { MediaElement } from "@/types/timeline"; - -export function MediaProperties({ element }: { element: MediaElement }) { - return
Media properties
; -} +import { MediaElement } from "@/types/timeline"; + +export function MediaProperties({ element }: { element: MediaElement }) { + return
Media properties
; +} diff --git a/apps/web/src/components/editor/properties-panel/property-item.tsx b/apps/web/src/components/editor/properties-panel/property-item.tsx index beb8c799..30f1b40c 100644 --- a/apps/web/src/components/editor/properties-panel/property-item.tsx +++ b/apps/web/src/components/editor/properties-panel/property-item.tsx @@ -1,47 +1,47 @@ -import { cn } from "@/lib/utils"; - -interface PropertyItemProps { - direction?: "row" | "column"; - children: React.ReactNode; - className?: string; -} - -export function PropertyItem({ - direction = "row", - children, - className, -}: PropertyItemProps) { - return ( -
- {children} -
- ); -} - -export function PropertyItemLabel({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { - return ; -} - -export function PropertyItemValue({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { - return
{children}
; -} +import { cn } from "@/lib/utils"; + +interface PropertyItemProps { + direction?: "row" | "column"; + children: React.ReactNode; + className?: string; +} + +export function PropertyItem({ + direction = "row", + children, + className, +}: PropertyItemProps) { + return ( +
+ {children} +
+ ); +} + +export function PropertyItemLabel({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return ; +} + +export function PropertyItemValue({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return
{children}
; +} diff --git a/apps/web/src/components/editor/properties-panel/text-properties.tsx b/apps/web/src/components/editor/properties-panel/text-properties.tsx index e13dabc8..4d58752b 100644 --- a/apps/web/src/components/editor/properties-panel/text-properties.tsx +++ b/apps/web/src/components/editor/properties-panel/text-properties.tsx @@ -1,76 +1,76 @@ -import { Textarea } from "@/components/ui/textarea"; -import { FontPicker } from "@/components/ui/font-picker"; -import { FontFamily } from "@/constants/font-constants"; -import { TextElement } from "@/types/timeline"; -import { useTimelineStore } from "@/stores/timeline-store"; -import { Slider } from "@/components/ui/slider"; -import { Input } from "@/components/ui/input"; -import { - PropertyItem, - PropertyItemLabel, - PropertyItemValue, -} from "./property-item"; - -export function TextProperties({ - element, - trackId, -}: { - element: TextElement; - trackId: string; -}) { - const { updateTextElement } = useTimelineStore(); - - return ( -
-