mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
Merge branch 'staging' into pr/vishesh711/284
This commit is contained in:
@@ -0,0 +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 (
|
||||
<div
|
||||
className="flex flex-col items-center justify-center gap-4 h-full text-center rounded-lg bg-foreground/5 hover:bg-foreground/10 transition-all duration-200 p-8"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<Upload className="h-10 w-10 text-foreground" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-muted-foreground max-w-sm">
|
||||
{isProcessing
|
||||
? `Processing your files (${progress}%)`
|
||||
: "Drag and drop videos, photos, and audio files here"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isProcessing && (
|
||||
<div className="w-full max-w-xs">
|
||||
<div className="w-full bg-muted/50 rounded-full h-2">
|
||||
<div
|
||||
className="bg-primary h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,11 +3,11 @@
|
||||
import { useDragDrop } from "@/hooks/use-drag-drop";
|
||||
import { processMediaFiles } from "@/lib/media-processing";
|
||||
import { useMediaStore, type MediaItem } from "@/stores/media-store";
|
||||
import { Image, Music, Plus, Upload, Video } from "lucide-react";
|
||||
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 { DragOverlay } from "@/components/ui/drag-overlay";
|
||||
import { MediaDragOverlay } from "@/components/editor/media-panel/drag-overlay";
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} 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();
|
||||
@@ -203,9 +204,6 @@ export function MediaView() {
|
||||
className={`h-full flex flex-col gap-1 transition-colors relative ${isDragOver ? "bg-accent/30" : ""}`}
|
||||
{...dragProps}
|
||||
>
|
||||
{/* Show overlay when dragging files over the panel */}
|
||||
<DragOverlay isVisible={isDragOver} />
|
||||
|
||||
<div className="p-3 pb-2">
|
||||
{/* Search and filter controls */}
|
||||
<div className="flex gap-2">
|
||||
@@ -227,47 +225,31 @@ export function MediaView() {
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={handleFileSelect}
|
||||
disabled={isProcessing}
|
||||
className="flex-none bg-transparent min-w-[30px] whitespace-nowrap overflow-hidden px-2 justify-center items-center"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Plus className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-3 pt-0">
|
||||
{/* Show message if no media, otherwise show media grid */}
|
||||
{filteredMediaItems.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center h-full">
|
||||
<div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mb-4">
|
||||
<Image className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No media in project
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground/70 mt-1">
|
||||
Drag files here or use the button below
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleFileSelect}
|
||||
disabled={isProcessing}
|
||||
className="flex-none bg-transparent min-w-[30px] whitespace-nowrap overflow-hidden px-2 justify-center items-center mt-2"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Upload className="h-4 w-4 animate-spin" />
|
||||
<span className="hidden md:inline ml-2">{progress}%</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span
|
||||
className="hidden sm:inline ml-2"
|
||||
aria-label="Add file"
|
||||
>
|
||||
Add
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{isDragOver || filteredMediaItems.length === 0 ? (
|
||||
<MediaDragOverlay
|
||||
isVisible={true}
|
||||
isProcessing={isProcessing}
|
||||
progress={progress}
|
||||
onClick={handleFileSelect}
|
||||
isEmptyState={filteredMediaItems.length === 0 && !isDragOver}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="grid gap-2"
|
||||
@@ -288,6 +270,11 @@ export function MediaView() {
|
||||
name: item.name,
|
||||
}}
|
||||
showPlusOnDrag={false}
|
||||
onAddToTimeline={(currentTime) =>
|
||||
useTimelineStore
|
||||
.getState()
|
||||
.addMediaAtTime(item, currentTime)
|
||||
}
|
||||
rounded={false}
|
||||
/>
|
||||
</ContextMenuTrigger>
|
||||
|
||||
@@ -1,4 +1,30 @@
|
||||
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 (
|
||||
@@ -11,12 +37,15 @@ export function TextView() {
|
||||
</div>
|
||||
}
|
||||
dragData={{
|
||||
id: "default-text",
|
||||
type: "text",
|
||||
name: "Default text",
|
||||
content: "Default text",
|
||||
id: textData.id,
|
||||
type: textData.type,
|
||||
name: textData.name,
|
||||
content: textData.content,
|
||||
}}
|
||||
aspectRatio={1}
|
||||
onAddToTimeline={(currentTime) =>
|
||||
useTimelineStore.getState().addTextAtTime(textData, currentTime)
|
||||
}
|
||||
showLabel={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useTimelineElementResize } from "@/hooks/use-timeline-element-resize";
|
||||
import {
|
||||
getTrackElementClasses,
|
||||
TIMELINE_CONSTANTS,
|
||||
getTrackHeight,
|
||||
} from "@/constants/timeline-constants";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -267,35 +268,99 @@ export function TimelineElement({
|
||||
);
|
||||
}
|
||||
|
||||
const TILE_ASPECT_RATIO = 16 / 9;
|
||||
|
||||
if (mediaItem.type === "image") {
|
||||
// Calculate tile size based on 16:9 aspect ratio
|
||||
const trackHeight = getTrackHeight(track.type);
|
||||
const tileHeight = trackHeight - 8; // Account for padding
|
||||
const tileWidth = tileHeight * TILE_ASPECT_RATIO;
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<div className="bg-[#004D52] py-3 w-full h-full">
|
||||
<img
|
||||
src={mediaItem.url}
|
||||
alt={mediaItem.name}
|
||||
className="w-full h-full object-cover"
|
||||
draggable={false}
|
||||
<div className="bg-[#004D52] py-3 w-full h-full relative">
|
||||
{/* Background with tiled images */}
|
||||
<div
|
||||
className="absolute top-3 bottom-3 left-0 right-0"
|
||||
style={{
|
||||
backgroundImage: mediaItem.url
|
||||
? `url(${mediaItem.url})`
|
||||
: "none",
|
||||
backgroundRepeat: "repeat-x",
|
||||
backgroundSize: `${tileWidth}px ${tileHeight}px`,
|
||||
backgroundPosition: "left center",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
aria-label={`Tiled background of ${mediaItem.name}`}
|
||||
/>
|
||||
{/* Overlay with vertical borders */}
|
||||
<div
|
||||
className="absolute top-3 bottom-3 left-0 right-0 pointer-events-none"
|
||||
style={{
|
||||
backgroundImage: `repeating-linear-gradient(
|
||||
to right,
|
||||
transparent 0px,
|
||||
transparent ${tileWidth - 1}px,
|
||||
rgba(255, 255, 255, 0.6) ${tileWidth - 1}px,
|
||||
rgba(255, 255, 255, 0.6) ${tileWidth}px
|
||||
)`,
|
||||
backgroundPosition: "left center",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const VIDEO_TILE_PADDING = 16;
|
||||
const OVERLAY_SPACE_MULTIPLIER = 1.5;
|
||||
|
||||
if (mediaItem.type === "video" && mediaItem.thumbnailUrl) {
|
||||
const trackHeight = getTrackHeight(track.type);
|
||||
const tileHeight = trackHeight - VIDEO_TILE_PADDING;
|
||||
const tileWidth = tileHeight * TILE_ASPECT_RATIO;
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex items-center gap-2">
|
||||
<div className="w-8 h-8 flex-shrink-0">
|
||||
<img
|
||||
src={mediaItem.thumbnailUrl}
|
||||
alt={mediaItem.name}
|
||||
className="w-full h-full object-cover rounded-sm"
|
||||
draggable={false}
|
||||
<div className="flex-1 h-full relative overflow-hidden">
|
||||
{/* Background with tiled thumbnails */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage: mediaItem.thumbnailUrl
|
||||
? `url(${mediaItem.thumbnailUrl})`
|
||||
: "none",
|
||||
backgroundRepeat: "repeat-x",
|
||||
backgroundSize: `${tileWidth}px ${tileHeight}px`,
|
||||
backgroundPosition: "left center",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
aria-label={`Tiled thumbnail of ${mediaItem.name}`}
|
||||
/>
|
||||
{/* Overlay with vertical borders */}
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
style={{
|
||||
backgroundImage: `repeating-linear-gradient(
|
||||
to right,
|
||||
transparent 0px,
|
||||
transparent ${tileWidth - 1}px,
|
||||
rgba(255, 255, 255, 0.6) ${tileWidth - 1}px,
|
||||
rgba(255, 255, 255, 0.6) ${tileWidth}px
|
||||
)`,
|
||||
backgroundPosition: "left center",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-foreground/80 truncate flex-1">
|
||||
{element.name}
|
||||
</span>
|
||||
{elementWidth > tileWidth * OVERLAY_SPACE_MULTIPLIER ? (
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 bg-black/70 text-white text-xs px-2 py-1 rounded pointer-events-none max-w-[40%] truncate">
|
||||
{element.name}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-foreground/80 truncate flex-shrink-0 max-w-[120px]">
|
||||
{element.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
TypeIcon,
|
||||
Magnet,
|
||||
Lock,
|
||||
LockOpen,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -571,29 +572,7 @@ export function Timeline() {
|
||||
|
||||
if (dragData.type === "text") {
|
||||
// Always create new text track to avoid overlaps
|
||||
const newTrackId = addTrack("text");
|
||||
|
||||
addElementToTrack(newTrackId, {
|
||||
type: "text",
|
||||
name: dragData.name || "Text",
|
||||
content: dragData.content || "Default Text",
|
||||
duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
|
||||
startTime: 0,
|
||||
trimStart: 0,
|
||||
trimEnd: 0,
|
||||
fontSize: 48,
|
||||
fontFamily: "Arial",
|
||||
color: "#ffffff",
|
||||
backgroundColor: "transparent",
|
||||
textAlign: "center",
|
||||
fontWeight: "normal",
|
||||
fontStyle: "normal",
|
||||
textDecoration: "none",
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
});
|
||||
useTimelineStore.getState().addTextToNewTrack(dragData);
|
||||
} else {
|
||||
// Handle media items
|
||||
const mediaItem = mediaItems.find((item: any) => item.id === dragData.id);
|
||||
@@ -602,19 +581,7 @@ export function Timeline() {
|
||||
return;
|
||||
}
|
||||
|
||||
const trackType = dragData.type === "audio" ? "audio" : "media";
|
||||
let targetTrack = tracks.find((t) => t.type === trackType);
|
||||
const newTrackId = targetTrack ? targetTrack.id : addTrack(trackType);
|
||||
|
||||
addElementToTrack(newTrackId, {
|
||||
type: "media",
|
||||
mediaId: mediaItem.id,
|
||||
name: mediaItem.name,
|
||||
duration: mediaItem.duration || 5,
|
||||
startTime: 0,
|
||||
trimStart: 0,
|
||||
trimEnd: 0,
|
||||
});
|
||||
useTimelineStore.getState().addMediaToNewTrack(mediaItem);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing dropped item data:", error);
|
||||
@@ -642,18 +609,7 @@ export function Timeline() {
|
||||
item.name === processedItem.name && item.url === processedItem.url
|
||||
);
|
||||
if (addedItem) {
|
||||
const trackType =
|
||||
processedItem.type === "audio" ? "audio" : "media";
|
||||
const newTrackId = addTrack(trackType);
|
||||
addElementToTrack(newTrackId, {
|
||||
type: "media",
|
||||
mediaId: addedItem.id,
|
||||
name: addedItem.name,
|
||||
duration: addedItem.duration || 5,
|
||||
startTime: 0,
|
||||
trimStart: 0,
|
||||
trimEnd: 0,
|
||||
});
|
||||
useTimelineStore.getState().addMediaToNewTrack(addedItem);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1048,7 +1004,11 @@ export function Timeline() {
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="text" size="icon" onClick={toggleSnapping}>
|
||||
<Lock className="h-4 w-4" />
|
||||
{snappingEnabled ? (
|
||||
<Lock className="h-4 w-4" />
|
||||
) : (
|
||||
<LockOpen className="h-4 w-4 text-primary" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Auto snapping</TooltipContent>
|
||||
|
||||
Reference in New Issue
Block a user