improve lots of stuff around the editor
This commit is contained in:
@ -4,20 +4,25 @@ import Link from "next/link";
|
|||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { ChevronLeft, Download } from "lucide-react";
|
import { ChevronLeft, Download } from "lucide-react";
|
||||||
import { useProjectStore } from "@/stores/project-store";
|
import { useProjectStore } from "@/stores/project-store";
|
||||||
import { useMediaStore } from "@/stores/media-store";
|
|
||||||
import { useTimelineStore } from "@/stores/timeline-store";
|
import { useTimelineStore } from "@/stores/timeline-store";
|
||||||
import { HeaderBase } from "./header-base";
|
import { HeaderBase } from "./header-base";
|
||||||
|
|
||||||
export function EditorHeader() {
|
export function EditorHeader() {
|
||||||
const { activeProject } = useProjectStore();
|
const { activeProject } = useProjectStore();
|
||||||
const { mediaItems } = useMediaStore();
|
const { getTotalDuration } = useTimelineStore();
|
||||||
const { tracks } = useTimelineStore();
|
|
||||||
|
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
// TODO: Implement export functionality
|
// TODO: Implement export functionality
|
||||||
console.log("Export project");
|
console.log("Export project");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Format duration from seconds to MM:SS format
|
||||||
|
const formatDuration = (seconds: number): string => {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = Math.floor(seconds % 60);
|
||||||
|
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
|
||||||
|
};
|
||||||
|
|
||||||
const leftContent = (
|
const leftContent = (
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
@ -30,9 +35,7 @@ export function EditorHeader() {
|
|||||||
|
|
||||||
const centerContent = (
|
const centerContent = (
|
||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
<span>{mediaItems.length} media</span>
|
<span>{formatDuration(getTotalDuration())}</span>
|
||||||
<span>•</span>
|
|
||||||
<span>{tracks.length} tracks</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -18,6 +18,9 @@ export function PreviewPanel() {
|
|||||||
? mediaItems.find((item) => item.id === firstClip.mediaId)
|
? mediaItems.find((item) => item.id === firstClip.mediaId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// Calculate dynamic aspect ratio - default to 16:9 if no media
|
||||||
|
const aspectRatio = firstMediaItem?.aspectRatio || 16 / 9;
|
||||||
|
|
||||||
const renderPreviewContent = () => {
|
const renderPreviewContent = () => {
|
||||||
if (!firstMediaItem) {
|
if (!firstMediaItem) {
|
||||||
return (
|
return (
|
||||||
@ -32,7 +35,7 @@ export function PreviewPanel() {
|
|||||||
<ImageTimelineTreatment
|
<ImageTimelineTreatment
|
||||||
src={firstMediaItem.url}
|
src={firstMediaItem.url}
|
||||||
alt={firstMediaItem.name}
|
alt={firstMediaItem.name}
|
||||||
targetAspectRatio={16 / 9}
|
targetAspectRatio={aspectRatio}
|
||||||
className="w-full h-full rounded-lg"
|
className="w-full h-full rounded-lg"
|
||||||
backgroundType="blur"
|
backgroundType="blur"
|
||||||
/>
|
/>
|
||||||
@ -68,8 +71,17 @@ export function PreviewPanel() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col items-center justify-center p-4">
|
<div className="h-full flex flex-col items-center justify-center p-4 overflow-hidden">
|
||||||
<div className="aspect-video bg-black/90 w-full max-w-4xl rounded-lg shadow-lg relative group overflow-hidden">
|
<div
|
||||||
|
className="bg-black/90 rounded-lg shadow-lg relative group overflow-hidden flex-shrink"
|
||||||
|
style={{
|
||||||
|
aspectRatio: aspectRatio.toString(),
|
||||||
|
width: aspectRatio > 1 ? "100%" : "auto",
|
||||||
|
height: aspectRatio <= 1 ? "100%" : "auto",
|
||||||
|
maxWidth: "100%",
|
||||||
|
maxHeight: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{renderPreviewContent()}
|
{renderPreviewContent()}
|
||||||
|
|
||||||
{/* Playback Controls Overlay */}
|
{/* Playback Controls Overlay */}
|
||||||
@ -103,6 +115,16 @@ export function PreviewPanel() {
|
|||||||
Preview: {firstMediaItem.name}
|
Preview: {firstMediaItem.name}
|
||||||
{firstMediaItem.type === "image" &&
|
{firstMediaItem.type === "image" &&
|
||||||
" (with CapCut-style treatment)"}
|
" (with CapCut-style treatment)"}
|
||||||
|
<br />
|
||||||
|
<span className="text-xs text-muted-foreground/70">
|
||||||
|
Aspect Ratio: {aspectRatio.toFixed(2)} (
|
||||||
|
{aspectRatio > 1
|
||||||
|
? "Landscape"
|
||||||
|
: aspectRatio < 1
|
||||||
|
? "Portrait"
|
||||||
|
: "Square"}
|
||||||
|
)
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -472,19 +472,12 @@ function TimelineTrackComponent({ track }: { track: TimelineTrack }) {
|
|||||||
|
|
||||||
if (mediaItem.type === "image") {
|
if (mediaItem.type === "image") {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex items-center gap-2">
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
<div className="w-16 h-12 flex-shrink-0">
|
<img
|
||||||
<ImageTimelineTreatment
|
src={mediaItem.url}
|
||||||
src={mediaItem.url}
|
alt={mediaItem.name}
|
||||||
alt={mediaItem.name}
|
className="w-full h-full object-cover"
|
||||||
targetAspectRatio={16 / 9}
|
/>
|
||||||
className="rounded-sm"
|
|
||||||
backgroundType="mirror"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-foreground/80 truncate flex-1">
|
|
||||||
{clip.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -536,9 +529,9 @@ function TimelineTrackComponent({ track }: { track: TimelineTrack }) {
|
|||||||
track.clips.map((clip, index) => (
|
track.clips.map((clip, index) => (
|
||||||
<div
|
<div
|
||||||
key={clip.id}
|
key={clip.id}
|
||||||
className={`timeline-clip h-full rounded-sm border cursor-grab active:cursor-grabbing transition-colors ${getTrackColor(track.type)} flex items-center px-2 min-w-[80px] overflow-hidden`}
|
className={`timeline-clip h-full rounded-sm border cursor-grab active:cursor-grabbing transition-colors ${getTrackColor(track.type)} flex items-center py-3 min-w-[80px] overflow-hidden`}
|
||||||
style={{
|
style={{
|
||||||
width: `${Math.max(80, (clip.duration / 30) * 400)}px`,
|
width: `${Math.max(80, clip.duration * 50)}px`,
|
||||||
}}
|
}}
|
||||||
draggable={true}
|
draggable={true}
|
||||||
onDragStart={(e) => handleClipDragStart(e, clip)}
|
onDragStart={(e) => handleClipDragStart(e, clip)}
|
||||||
|
@ -33,9 +33,12 @@ interface TimelineStore {
|
|||||||
clipId: string,
|
clipId: string,
|
||||||
newIndex: number
|
newIndex: number
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
// Computed values
|
||||||
|
getTotalDuration: () => number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTimelineStore = create<TimelineStore>((set) => ({
|
export const useTimelineStore = create<TimelineStore>((set, get) => ({
|
||||||
tracks: [],
|
tracks: [],
|
||||||
|
|
||||||
addTrack: (type) => {
|
addTrack: (type) => {
|
||||||
@ -134,4 +137,17 @@ export const useTimelineStore = create<TimelineStore>((set) => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getTotalDuration: () => {
|
||||||
|
const { tracks } = get();
|
||||||
|
if (tracks.length === 0) return 0;
|
||||||
|
|
||||||
|
// Calculate the duration of each track (sum of all clips in that track)
|
||||||
|
const trackDurations = tracks.map((track) =>
|
||||||
|
track.clips.reduce((total, clip) => total + clip.duration, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return the maximum track duration (longest track determines project duration)
|
||||||
|
return Math.max(...trackDurations, 0);
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
Reference in New Issue
Block a user