summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/media-panel
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2025-08-28 22:39:19 +0200
committerMaze Winther <mazewinther@gmail.com>2025-08-28 22:39:19 +0200
commit2d834a49d7d222d7f70ee6b4cf7ab2827b88bb0e (patch)
treedb8a33eb12c6294cc0000caf05c600fe3c33069e /apps/web/src/components/editor/media-panel
parent792fb6cdc06ebcd578d9e27821eb25a7dba9dbd2 (diff)
parenta7d90f7eac90966920c750134d7de9b611b2c38f (diff)
downloadopencut-2d834a49d7d222d7f70ee6b4cf7ab2827b88bb0e.tar.gz
Merge branch 'staging'
Diffstat (limited to 'apps/web/src/components/editor/media-panel')
-rw-r--r--apps/web/src/components/editor/media-panel/views/base-view.tsx67
-rw-r--r--apps/web/src/components/editor/media-panel/views/captions.tsx22
-rw-r--r--apps/web/src/components/editor/media-panel/views/media.tsx64
-rw-r--r--apps/web/src/components/editor/media-panel/views/settings.tsx2
-rw-r--r--apps/web/src/components/editor/media-panel/views/stickers.tsx33
-rw-r--r--apps/web/src/components/editor/media-panel/views/text.tsx44
6 files changed, 65 insertions, 167 deletions
diff --git a/apps/web/src/components/editor/media-panel/views/base-view.tsx b/apps/web/src/components/editor/media-panel/views/base-view.tsx
deleted file mode 100644
index 2cd33535..00000000
--- a/apps/web/src/components/editor/media-panel/views/base-view.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import { ScrollArea } from "@/components/ui/scroll-area";
-import { Separator } from "@/components/ui/separator";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-
-interface BaseViewProps {
- children?: React.ReactNode;
- defaultTab?: string;
- tabs?: {
- value: string;
- label: string;
- content: React.ReactNode;
- }[];
- className?: string;
- ref?: React.RefObject<HTMLDivElement>;
-}
-
-function ViewContent({
- children,
- className,
-}: {
- children: React.ReactNode;
- className?: string;
-}) {
- return (
- <ScrollArea className="flex-1">
- <div className={`p-5 h-full ${className}`}>{children}</div>
- </ScrollArea>
- );
-}
-
-export function BaseView({
- children,
- defaultTab,
- tabs,
- className = "",
- ref,
-}: BaseViewProps) {
- return (
- <div className={`h-full flex flex-col ${className}`} ref={ref}>
- {!tabs || tabs.length === 0 ? (
- <ViewContent className={className}>{children}</ViewContent>
- ) : (
- <Tabs defaultValue={defaultTab} className="flex flex-col h-full">
- <div className="px-3 pt-4 pb-0">
- <TabsList>
- {tabs.map((tab) => (
- <TabsTrigger key={tab.value} value={tab.value}>
- {tab.label}
- </TabsTrigger>
- ))}
- </TabsList>
- </div>
- <Separator className="mt-4" />
- {tabs.map((tab) => (
- <TabsContent
- key={tab.value}
- value={tab.value}
- className="mt-0 flex-1 flex flex-col min-h-0"
- >
- <ViewContent>{tab.content}</ViewContent>
- </TabsContent>
- ))}
- </Tabs>
- )}
- </div>
- );
-}
diff --git a/apps/web/src/components/editor/media-panel/views/captions.tsx b/apps/web/src/components/editor/media-panel/views/captions.tsx
index 4fe2cbd7..59403a8d 100644
--- a/apps/web/src/components/editor/media-panel/views/captions.tsx
+++ b/apps/web/src/components/editor/media-panel/views/captions.tsx
@@ -1,11 +1,12 @@
import { Button } from "@/components/ui/button";
import { PropertyGroup } from "../../properties-panel/property-item";
-import { BaseView } from "./base-view";
+import { PanelBaseView as BaseView } from "@/components/editor/panel-base-view";
import { Language, LanguageSelect } from "@/components/language-select";
import { useState, useRef, useEffect } from "react";
-import { extractTimelineAudio } from "@/lib/ffmpeg-utils";
+import { extractTimelineAudio } from "@/lib/mediabunny-utils";
import { encryptWithRandomKey, arrayBufferToBase64 } from "@/lib/zk-encryption";
import { useTimelineStore } from "@/stores/timeline-store";
+import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants";
import { Loader2, Shield, Trash2, Upload } from "lucide-react";
import {
Dialog,
@@ -162,24 +163,13 @@ export function Captions() {
// Add all caption elements to the same track
shortCaptions.forEach((caption, index) => {
addElementToTrack(captionTrackId, {
- type: "text",
+ ...DEFAULT_TEXT_ELEMENT,
name: `Caption ${index + 1}`,
content: caption.text,
duration: caption.duration,
startTime: caption.startTime,
- trimStart: 0,
- trimEnd: 0,
- fontSize: 65,
- fontFamily: "Arial",
- color: "#ffffff",
- textAlign: "center",
- fontWeight: "bold",
- fontStyle: "normal",
- textDecoration: "none",
- x: 0,
- y: 0,
- rotation: 0,
- opacity: 1,
+ fontSize: 65, // Larger for captions
+ fontWeight: "bold", // Bold for captions
} as TextElement);
});
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 c84dde70..07a44a7c 100644
--- a/apps/web/src/components/editor/media-panel/views/media.tsx
+++ b/apps/web/src/components/editor/media-panel/views/media.tsx
@@ -2,7 +2,8 @@
import { useDragDrop } from "@/hooks/use-drag-drop";
import { processMediaFiles } from "@/lib/media-processing";
-import { useMediaStore, type MediaItem } from "@/stores/media-store";
+import { useMediaStore } from "@/stores/media-store";
+import { MediaFile } from "@/types/media";
import {
ArrowDown01,
CloudUpload,
@@ -13,7 +14,7 @@ import {
Music,
Video,
} from "lucide-react";
-import { useRef, useState, useMemo, useEffect } from "react";
+import { useRef, useState, useMemo } from "react";
import { useHighlightScroll } from "@/hooks/use-highlight-scroll";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
@@ -47,7 +48,7 @@ function MediaItemWithContextMenu({
children,
onRemove,
}: {
- item: MediaItem;
+ item: MediaFile;
children: React.ReactNode;
onRemove: (e: React.MouseEvent, id: string) => Promise<void>;
}) {
@@ -68,14 +69,12 @@ function MediaItemWithContextMenu({
}
export function MediaView() {
- const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore();
+ const { mediaFiles, addMediaFile, removeMediaFile } = useMediaStore();
const { activeProject } = useProjectStore();
const { mediaViewMode, setMediaViewMode } = usePanelStore();
const fileInputRef = useRef<HTMLInputElement>(null);
const [isProcessing, setIsProcessing] = useState(false);
const [progress, setProgress] = useState(0);
- const [searchQuery, setSearchQuery] = useState("");
- const [mediaFilter, setMediaFilter] = useState("all");
const [sortBy, setSortBy] = useState<"name" | "type" | "duration" | "size">(
"name"
);
@@ -96,16 +95,13 @@ export function MediaView() {
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);
+ await addMediaFile(activeProject.id, item);
}
} catch (error) {
- // Show error toast if processing fails
console.error("Error processing files:", error);
toast.error("Failed to process files");
} finally {
@@ -122,13 +118,11 @@ export function MediaView() {
const handleFileSelect = () => fileInputRef.current?.click(); // Open file picker
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- // 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) {
@@ -136,8 +130,7 @@ export function MediaView() {
return;
}
- // Media store now handles cascade deletion automatically
- await removeMediaItem(activeProject.id, id);
+ await removeMediaFile(activeProject.id, id);
};
const formatDuration = (duration: number) => {
@@ -147,28 +140,15 @@ export function MediaView() {
return `${min}:${sec.toString().padStart(2, "0")}`;
};
- const [filteredMediaItems, setFilteredMediaItems] = useState(mediaItems);
-
- useEffect(() => {
- let filtered = mediaItems.filter((item) => {
+ const filteredMediaItems = useMemo(() => {
+ let filtered = mediaFiles.filter((item) => {
if (item.ephemeral) return false;
- if (mediaFilter && mediaFilter !== "all" && item.type !== mediaFilter) {
- return false;
- }
-
- if (
- searchQuery &&
- !item.name.toLowerCase().includes(searchQuery.toLowerCase())
- ) {
- return false;
- }
-
return true;
});
filtered.sort((a, b) => {
- let valueA: any;
- let valueB: any;
+ let valueA: string | number;
+ let valueB: string | number;
switch (sortBy) {
case "name":
@@ -196,8 +176,8 @@ export function MediaView() {
return 0;
});
- setFilteredMediaItems(filtered);
- }, [mediaItems, mediaFilter, searchQuery, sortBy, sortOrder]);
+ return filtered;
+ }, [mediaFiles, sortBy, sortOrder]);
const previewComponents = useMemo(() => {
const previews = new Map<string, React.ReactNode>();
@@ -276,7 +256,7 @@ export function MediaView() {
return previews;
}, [filteredMediaItems]);
- const renderPreview = (item: MediaItem) => previewComponents.get(item.id);
+ const renderPreview = (item: MediaFile) => previewComponents.get(item.id);
return (
<>
@@ -481,12 +461,14 @@ function GridView({
highlightedId,
registerElement,
}: {
- filteredMediaItems: MediaItem[];
- renderPreview: (item: MediaItem) => React.ReactNode;
+ filteredMediaItems: MediaFile[];
+ renderPreview: (item: MediaFile) => React.ReactNode;
handleRemove: (e: React.MouseEvent, id: string) => Promise<void>;
highlightedId: string | null;
registerElement: (id: string, element: HTMLElement | null) => void;
}) {
+ const { addElementAtTime } = useTimelineStore();
+
return (
<div
className="grid gap-2"
@@ -507,7 +489,7 @@ function GridView({
}}
showPlusOnDrag={false}
onAddToTimeline={(currentTime) =>
- useTimelineStore.getState().addMediaAtTime(item, currentTime)
+ addElementAtTime(item, currentTime)
}
rounded={false}
variant="card"
@@ -527,12 +509,14 @@ function ListView({
highlightedId,
registerElement,
}: {
- filteredMediaItems: MediaItem[];
- renderPreview: (item: MediaItem) => React.ReactNode;
+ filteredMediaItems: MediaFile[];
+ renderPreview: (item: MediaFile) => React.ReactNode;
handleRemove: (e: React.MouseEvent, id: string) => Promise<void>;
highlightedId: string | null;
registerElement: (id: string, element: HTMLElement | null) => void;
}) {
+ const { addElementAtTime } = useTimelineStore();
+
return (
<div className="space-y-1">
{filteredMediaItems.map((item) => (
@@ -548,7 +532,7 @@ function ListView({
}}
showPlusOnDrag={false}
onAddToTimeline={(currentTime) =>
- useTimelineStore.getState().addMediaAtTime(item, currentTime)
+ addElementAtTime(item, currentTime)
}
variant="compact"
isHighlighted={highlightedId === item.id}
diff --git a/apps/web/src/components/editor/media-panel/views/settings.tsx b/apps/web/src/components/editor/media-panel/views/settings.tsx
index d33b4cce..df1ec54f 100644
--- a/apps/web/src/components/editor/media-panel/views/settings.tsx
+++ b/apps/web/src/components/editor/media-panel/views/settings.tsx
@@ -7,7 +7,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
-import { BaseView } from "./base-view";
+import { PanelBaseView as BaseView } from "@/components/editor/panel-base-view";
import {
PropertyItem,
PropertyItemLabel,
diff --git a/apps/web/src/components/editor/media-panel/views/stickers.tsx b/apps/web/src/components/editor/media-panel/views/stickers.tsx
index 7d3e073e..41afe285 100644
--- a/apps/web/src/components/editor/media-panel/views/stickers.tsx
+++ b/apps/web/src/components/editor/media-panel/views/stickers.tsx
@@ -35,9 +35,10 @@ import {
ICONIFY_HOSTS,
POPULAR_COLLECTIONS,
} from "@/lib/iconify-api";
-import { cn, generateUUID } from "@/lib/utils";
+import { cn } from "@/lib/utils";
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
import Image from "next/image";
+import type { MediaFile } from "@/types/media";
import { DraggableMediaItem } from "@/components/ui/draggable-item";
import { InputWithBack } from "@/components/ui/input-with-back";
import { StickerCategory } from "@/stores/stickers-store";
@@ -185,9 +186,9 @@ function EmptyView({ message }: { message: string }) {
function StickersContentView({ category }: { category: StickerCategory }) {
const { activeProject } = useProjectStore();
- const { addMediaAtTime } = useTimelineStore();
+ const { addElementAtTime } = useTimelineStore();
const { currentTime } = usePlaybackStore();
- const { addMediaItem } = useMediaStore();
+ const { addMediaFile } = useMediaStore();
const {
searchQuery,
selectedCollection,
@@ -286,9 +287,9 @@ function StickersContentView({ category }: { category: StickerCategory }) {
throw new Error("Failed to download sticker");
}
- const mediaItem = {
+ const mediaItem: Omit<MediaFile, "id"> = {
name: iconName.replace(":", "-"),
- type: "image" as const,
+ type: "image",
file,
url: URL.createObjectURL(file),
width: 200,
@@ -297,16 +298,16 @@ function StickersContentView({ category }: { category: StickerCategory }) {
ephemeral: false,
};
- await addMediaItem(activeProject.id, mediaItem);
+ await addMediaFile(activeProject.id, mediaItem);
const added = useMediaStore
.getState()
- .mediaItems.find(
+ .mediaFiles.find(
(m) => m.url === mediaItem.url && m.name === mediaItem.name
);
if (!added) throw new Error("Sticker not in media store");
- addMediaAtTime(added, currentTime);
+ addElementAtTime(added, currentTime);
toast.success(`Added "${iconName}" to timeline`);
} catch (error) {
@@ -565,7 +566,12 @@ interface StickerItemProps {
capSize?: boolean;
}
-function StickerItem({ iconName, onAdd, isAdding, capSize = false }: StickerItemProps) {
+function StickerItem({
+ iconName,
+ onAdd,
+ isAdding,
+ capSize = false,
+}: StickerItemProps) {
const [imageError, setImageError] = useState(false);
const [hostIndex, setHostIndex] = useState(0);
@@ -601,7 +607,10 @@ function StickerItem({ iconName, onAdd, isAdding, capSize = false }: StickerItem
className="w-full h-full object-contain"
style={
capSize
- ? { maxWidth: "var(--sticker-max, 160px)", maxHeight: "var(--sticker-max, 160px)" }
+ ? {
+ maxWidth: "var(--sticker-max, 160px)",
+ maxHeight: "var(--sticker-max, 160px)",
+ }
: undefined
}
onError={() => {
@@ -631,8 +640,8 @@ function StickerItem({ iconName, onAdd, isAdding, capSize = false }: StickerItem
name={displayName}
preview={preview}
dragData={{
- type: "sticker",
- iconName: iconName,
+ id: "sticker-placeholder",
+ type: "image",
name: displayName,
}}
onAddToTimeline={() => onAdd(iconName)}
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 49eacbc7..22fc2413 100644
--- a/apps/web/src/components/editor/media-panel/views/text.tsx
+++ b/apps/web/src/components/editor/media-panel/views/text.tsx
@@ -1,31 +1,7 @@
import { DraggableMediaItem } from "@/components/ui/draggable-item";
-import { BaseView } from "./base-view";
-import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+import { PanelBaseView as BaseView } from "@/components/editor/panel-base-view";
import { useTimelineStore } from "@/stores/timeline-store";
-import { type TextElement } from "@/types/timeline";
-
-const 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,
-};
+import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants";
export function TextView() {
return (
@@ -38,14 +14,20 @@ export function TextView() {
</div>
}
dragData={{
- id: textData.id,
- type: textData.type,
- name: textData.name,
- content: textData.content,
+ id: "temp-text-id",
+ type: DEFAULT_TEXT_ELEMENT.type,
+ name: DEFAULT_TEXT_ELEMENT.name,
+ content: DEFAULT_TEXT_ELEMENT.content,
}}
aspectRatio={1}
onAddToTimeline={(currentTime) =>
- useTimelineStore.getState().addTextAtTime(textData, currentTime)
+ useTimelineStore.getState().addElementAtTime(
+ {
+ ...DEFAULT_TEXT_ELEMENT,
+ id: "temp-text-id",
+ },
+ currentTime
+ )
}
showLabel={false}
/>