chore: implement unsafe changes
This commit is contained in:
parent
f27f7ec7a3
commit
cc70f7dd9e
File diff suppressed because one or more lines are too long
|
|
@ -1,5 +1,5 @@
|
|||
import type { NextRequest } from "next/server";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
export async function GET(_request: NextRequest) {
|
||||
return new Response("OK", { status: 200 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import crypto from "node:crypto";
|
||||
import { db, eq, waitlist } from "@opencut/db";
|
||||
import { checkBotId } from "botid/server";
|
||||
import crypto from "crypto";
|
||||
import { nanoid } from "nanoid";
|
||||
import { cookies } from "next/headers";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
|
|
@ -17,20 +17,30 @@ const TOKEN_EXPIRY = 60 * 60 * 1000;
|
|||
|
||||
async function validateCSRFToken(request: NextRequest): Promise<boolean> {
|
||||
const clientToken = request.headers.get("x-csrf-token");
|
||||
if (!clientToken) return false;
|
||||
if (!clientToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cookieStore = await cookies();
|
||||
const cookieValue = cookieStore.get(CSRF_TOKEN_NAME)?.value;
|
||||
if (!cookieValue) return false;
|
||||
if (!cookieValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [token, timestamp, signature] = cookieValue.split(":");
|
||||
if (!(token && timestamp && signature)) return false;
|
||||
if (!(token && timestamp && signature)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (clientToken !== token) return false;
|
||||
if (clientToken !== token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const tokenTime = Number.parseInt(timestamp);
|
||||
if (now - tokenTime > TOKEN_EXPIRY) return false;
|
||||
const tokenTime = Number.parseInt(timestamp, 10);
|
||||
if (now - tokenTime > TOKEN_EXPIRY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expectedSignature = crypto
|
||||
.createHmac("sha256", env.BETTER_AUTH_SECRET)
|
||||
|
|
@ -95,8 +105,6 @@ export async function POST(request: NextRequest) {
|
|||
const firstError = error.errors[0];
|
||||
return NextResponse.json({ error: firstError.message }, { status: 400 });
|
||||
}
|
||||
|
||||
console.error("Waitlist signup error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import crypto from "crypto";
|
||||
import crypto from "node:crypto";
|
||||
import { cookies } from "next/headers";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { env } from "@/env";
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ export async function generateMetadata({
|
|||
|
||||
const data = await getSinglePost(slug);
|
||||
|
||||
if (!(data && data.post)) return {};
|
||||
if (!data?.post) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
title: data.post.title,
|
||||
|
|
@ -58,7 +60,9 @@ export async function generateMetadata({
|
|||
|
||||
export async function generateStaticParams() {
|
||||
const data = await getPosts();
|
||||
if (!(data && data.posts.length)) return [];
|
||||
if (!data?.posts.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.posts.map((post) => ({
|
||||
slug: post.slug,
|
||||
|
|
@ -68,7 +72,9 @@ export async function generateStaticParams() {
|
|||
async function Page({ params }: PageProps) {
|
||||
const slug = (await params).slug;
|
||||
const data = await getSinglePost(slug);
|
||||
if (!(data && data.post)) return notFound();
|
||||
if (!data?.post) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const html = await processHtmlContent(data.post.content);
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ export const metadata: Metadata = {
|
|||
|
||||
export default async function BlogPage() {
|
||||
const data = await getPosts();
|
||||
if (!(data && data.posts)) return <div>No posts yet</div>;
|
||||
if (!data?.posts) {
|
||||
return <div>No posts yet</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ async function getContributors(): Promise<Contributor[]> {
|
|||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Failed to fetch contributors");
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -54,8 +53,7 @@ async function getContributors(): Promise<Contributor[]> {
|
|||
);
|
||||
|
||||
return filteredContributors;
|
||||
} catch (error) {
|
||||
console.error("Error fetching contributors:", error);
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -123,7 +121,7 @@ export default async function ContributorsPage() {
|
|||
</div>
|
||||
|
||||
<div className="mx-auto flex max-w-4xl flex-col justify-center gap-6 md:flex-row">
|
||||
{topContributors.map((contributor, index) => (
|
||||
{topContributors.map((contributor, _index) => (
|
||||
<Link
|
||||
className="group block flex-1"
|
||||
href={contributor.html_url}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,9 @@ export default function Editor() {
|
|||
|
||||
useEffect(() => {
|
||||
const initProject = async () => {
|
||||
if (!projectId) return;
|
||||
if (!projectId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeProject?.id === projectId) {
|
||||
return;
|
||||
|
|
@ -54,7 +56,7 @@ export default function Editor() {
|
|||
|
||||
try {
|
||||
await loadProject(projectId);
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
handledProjectIds.current.add(projectId);
|
||||
|
||||
const newProjectId = await createNewProject("Untitled Project");
|
||||
|
|
|
|||
|
|
@ -91,7 +91,6 @@ export default function ProjectsPage() {
|
|||
|
||||
const handleCreateProject = async () => {
|
||||
const projectId = await createNewProject("New Project");
|
||||
console.log("projectId", projectId);
|
||||
router.push(`/editor/${projectId}`);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function DeleteProjectDialog({
|
||||
isOpen,
|
||||
|
|
|
|||
|
|
@ -18,14 +18,12 @@ export function EditorHeader() {
|
|||
const [newName, setNewName] = useState(activeProject?.name || "");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleExport = () => {
|
||||
// TODO: Implement export functionality
|
||||
// NOTE: This is already being worked on
|
||||
console.log("Export project");
|
||||
};
|
||||
const handleExport = () => {};
|
||||
|
||||
const handleNameClick = () => {
|
||||
if (!activeProject) return;
|
||||
if (!activeProject) {
|
||||
return;
|
||||
}
|
||||
setNewName(activeProject.name);
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
|
@ -34,8 +32,7 @@ export function EditorHeader() {
|
|||
if (activeProject && newName.trim() && newName !== activeProject.name) {
|
||||
try {
|
||||
await renameProject(activeProject.id, newName.trim());
|
||||
} catch (error) {
|
||||
console.error("Failed to rename project:", error);
|
||||
} catch (_error) {
|
||||
setNewName(activeProject.name);
|
||||
}
|
||||
}
|
||||
|
|
@ -43,8 +40,11 @@ export function EditorHeader() {
|
|||
};
|
||||
|
||||
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") handleNameSave();
|
||||
else if (e.key === "Escape") setIsEditing(false);
|
||||
if (e.key === "Enter") {
|
||||
handleNameSave();
|
||||
} else if (e.key === "Escape") {
|
||||
setIsEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const leftContent = (
|
||||
|
|
|
|||
|
|
@ -22,14 +22,16 @@ const AudioWaveform: React.FC<AudioWaveformProps> = ({
|
|||
let mounted = true;
|
||||
|
||||
const initWaveSurfer = async () => {
|
||||
if (!(waveformRef.current && audioUrl)) return;
|
||||
if (!(waveformRef.current && audioUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Clean up any existing instance
|
||||
if (wavesurfer.current) {
|
||||
try {
|
||||
wavesurfer.current.destroy();
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
// Silently ignore destroy errors
|
||||
}
|
||||
wavesurfer.current = null;
|
||||
|
|
@ -55,8 +57,7 @@ const AudioWaveform: React.FC<AudioWaveformProps> = ({
|
|||
}
|
||||
});
|
||||
|
||||
wavesurfer.current.on("error", (err) => {
|
||||
console.error("WaveSurfer error:", err);
|
||||
wavesurfer.current.on("error", (_err) => {
|
||||
if (mounted) {
|
||||
setError(true);
|
||||
setIsLoading(false);
|
||||
|
|
@ -64,8 +65,7 @@ const AudioWaveform: React.FC<AudioWaveformProps> = ({
|
|||
});
|
||||
|
||||
await wavesurfer.current.load(audioUrl);
|
||||
} catch (err) {
|
||||
console.error("Failed to initialize WaveSurfer:", err);
|
||||
} catch (_err) {
|
||||
if (mounted) {
|
||||
setError(true);
|
||||
setIsLoading(false);
|
||||
|
|
@ -80,7 +80,7 @@ const AudioWaveform: React.FC<AudioWaveformProps> = ({
|
|||
if (wavesurfer.current) {
|
||||
try {
|
||||
wavesurfer.current.destroy();
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
// Silently ignore destroy errors
|
||||
}
|
||||
wavesurfer.current = null;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { Image, Plus, Upload } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Upload } from "lucide-react";
|
||||
|
||||
interface MediaDragOverlayProps {
|
||||
isVisible: boolean;
|
||||
|
|
@ -16,10 +15,14 @@ export function MediaDragOverlay({
|
|||
onClick,
|
||||
isEmptyState = false,
|
||||
}: MediaDragOverlayProps) {
|
||||
if (!isVisible) return null;
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
if (isProcessing || !onClick) return;
|
||||
if (isProcessing || !onClick) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
|
|
|
|||
|
|
@ -46,7 +46,9 @@ export function TabBar() {
|
|||
// We're using useEffect because we need to sync with external DOM scroll events
|
||||
useEffect(() => {
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container) return;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkScrollPosition();
|
||||
container.addEventListener("scroll", checkScrollPosition);
|
||||
|
|
@ -58,7 +60,7 @@ export function TabBar() {
|
|||
container.removeEventListener("scroll", checkScrollPosition);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
}, [checkScrollPosition]);
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
|
|
@ -106,7 +108,9 @@ function ScrollButton({
|
|||
onClick: () => void;
|
||||
isVisible: boolean;
|
||||
}) {
|
||||
if (!isVisible) return null;
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Icon = direction === "left" ? ChevronLeft : ChevronRight;
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@ export function MediaView() {
|
|||
const [mediaFilter, setMediaFilter] = useState("all");
|
||||
|
||||
const processFiles = async (files: FileList | File[]) => {
|
||||
if (!files || files.length === 0) return;
|
||||
if (!files || files.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (!activeProject) {
|
||||
toast.error("No active project");
|
||||
return;
|
||||
|
|
@ -53,9 +55,7 @@ export function MediaView() {
|
|||
for (const item of processedItems) {
|
||||
await addMediaItem(activeProject.id, item);
|
||||
}
|
||||
} catch (error) {
|
||||
// Show error toast if processing fails
|
||||
console.error("Error processing files:", error);
|
||||
} catch (_error) {
|
||||
toast.error("Failed to process files");
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
|
|
@ -72,7 +72,9 @@ export function MediaView() {
|
|||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// When files are selected via file picker, process them
|
||||
if (e.target.files) processFiles(e.target.files);
|
||||
if (e.target.files) {
|
||||
processFiles(e.target.files);
|
||||
}
|
||||
e.target.value = ""; // Reset input
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,9 @@ export function PreviewPanel() {
|
|||
|
||||
useEffect(() => {
|
||||
const updatePreviewSize = () => {
|
||||
if (!containerRef.current) return;
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
let availableWidth, availableHeight;
|
||||
|
||||
|
|
@ -190,7 +192,9 @@ export function PreviewPanel() {
|
|||
const backgroundElement = blurBackgroundElements[0];
|
||||
const { element, mediaItem } = backgroundElement;
|
||||
|
||||
if (!mediaItem) return null;
|
||||
if (!mediaItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const blurIntensity = activeProject.blurIntensity || 8;
|
||||
|
||||
|
|
@ -459,7 +463,9 @@ function FullscreenToolbar({
|
|||
const progress = totalDuration > 0 ? (currentTime / totalDuration) * 100 : 0;
|
||||
|
||||
const handleTimelineClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!hasAnyElements) return;
|
||||
if (!hasAnyElements) {
|
||||
return;
|
||||
}
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const clickX = e.clientX - rect.left;
|
||||
const percentage = Math.max(0, Math.min(1, clickX / rect.width));
|
||||
|
|
@ -468,7 +474,9 @@ function FullscreenToolbar({
|
|||
};
|
||||
|
||||
const handleTimelineDrag = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!hasAnyElements) return;
|
||||
if (!hasAnyElements) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export function PropertiesPanel() {
|
|||
|
||||
const handleFpsChange = (value: string) => {
|
||||
const fps = Number.parseFloat(value);
|
||||
if (!isNaN(fps) && fps > 0) {
|
||||
if (!Number.isNaN(fps) && fps > 0) {
|
||||
updateProjectFps(fps);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export function TextProperties({
|
|||
className="!text-xs h-7 w-12 rounded-sm text-center [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
onChange={(e) =>
|
||||
updateTextElement(trackId, element.id, {
|
||||
fontSize: Number.parseInt(e.target.value),
|
||||
fontSize: Number.parseInt(e.target.value, 10),
|
||||
})
|
||||
}
|
||||
type="number"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ export function SelectionBox({
|
|||
const selectionBoxRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!(isActive && startPos && currentPos && containerRef.current)) return;
|
||||
if (!(isActive && startPos && currentPos && containerRef.current)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = containerRef.current;
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
|
|
@ -44,7 +46,9 @@ export function SelectionBox({
|
|||
}
|
||||
}, [startPos, currentPos, isActive, containerRef]);
|
||||
|
||||
if (!(isActive && startPos && currentPos)) return null;
|
||||
if (!(isActive && startPos && currentPos)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -78,8 +78,8 @@ export function Timeline() {
|
|||
const { currentTime, duration, seek, setDuration, isPlaying, toggle } =
|
||||
usePlaybackStore();
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [_isProcessing, setIsProcessing] = useState(false);
|
||||
const [_progress, setProgress] = useState(0);
|
||||
const dragCounterRef = useRef(0);
|
||||
const timelineRef = useRef<HTMLDivElement>(null);
|
||||
const rulerRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -142,7 +142,6 @@ export function Timeline() {
|
|||
containerRef: tracksContainerRef,
|
||||
playheadRef,
|
||||
onSelectionComplete: (elements) => {
|
||||
console.log(JSON.stringify({ onSelectionComplete: elements.length }));
|
||||
setSelectedElements(elements);
|
||||
},
|
||||
});
|
||||
|
|
@ -194,12 +193,6 @@ export function Timeline() {
|
|||
|
||||
// Only process as click if we tracked a mouse down on timeline background
|
||||
if (!isMouseDown) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
ignoredClickWithoutMouseDown: true,
|
||||
timeStamp: e.timeStamp,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -209,15 +202,6 @@ export function Timeline() {
|
|||
const deltaTime = e.timeStamp - downTime;
|
||||
|
||||
if (deltaX > 5 || deltaY > 5 || deltaTime > 500) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
ignoredDragNotClick: true,
|
||||
deltaX,
|
||||
deltaY,
|
||||
deltaTime,
|
||||
timeStamp: e.timeStamp,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -241,9 +225,6 @@ export function Timeline() {
|
|||
clearSelectedElements();
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear selected elements when clicking empty timeline area
|
||||
console.log(JSON.stringify({ clearingSelectedElements: true }));
|
||||
clearSelectedElements();
|
||||
|
||||
// Determine if we're clicking in ruler or tracks area
|
||||
|
|
@ -259,7 +240,9 @@ export function Timeline() {
|
|||
const rulerContent = rulerScrollRef.current?.querySelector(
|
||||
"[data-radix-scroll-area-viewport]"
|
||||
) as HTMLElement;
|
||||
if (!rulerContent) return;
|
||||
if (!rulerContent) {
|
||||
return;
|
||||
}
|
||||
const rect = rulerContent.getBoundingClientRect();
|
||||
mouseX = e.clientX - rect.left;
|
||||
scrollLeft = rulerContent.scrollLeft;
|
||||
|
|
@ -268,7 +251,9 @@ export function Timeline() {
|
|||
const tracksContent = tracksScrollRef.current?.querySelector(
|
||||
"[data-radix-scroll-area-viewport]"
|
||||
) as HTMLElement;
|
||||
if (!tracksContent) return;
|
||||
if (!tracksContent) {
|
||||
return;
|
||||
}
|
||||
const rect = tracksContent.getBoundingClientRect();
|
||||
mouseX = e.clientX - rect.left;
|
||||
scrollLeft = tracksContent.scrollLeft;
|
||||
|
|
@ -293,11 +278,10 @@ export function Timeline() {
|
|||
duration,
|
||||
zoomLevel,
|
||||
seek,
|
||||
rulerScrollRef,
|
||||
tracksScrollRef,
|
||||
clearSelectedElements,
|
||||
isSelecting,
|
||||
justFinishedSelecting,
|
||||
activeProject?.fps,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
@ -305,7 +289,7 @@ export function Timeline() {
|
|||
useEffect(() => {
|
||||
const totalDuration = getTotalDuration();
|
||||
setDuration(Math.max(totalDuration, 10)); // Minimum 10 seconds for empty timeline
|
||||
}, [tracks, setDuration, getTotalDuration]);
|
||||
}, [setDuration, getTotalDuration]);
|
||||
|
||||
// Old marquee system removed - using new SelectionBox component instead
|
||||
|
||||
|
|
@ -374,8 +358,7 @@ export function Timeline() {
|
|||
|
||||
useTimelineStore.getState().addMediaToNewTrack(mediaItem);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing dropped item data:", error);
|
||||
} catch (_error) {
|
||||
toast.error("Failed to add item to timeline");
|
||||
}
|
||||
} else if (e.dataTransfer.files?.length > 0) {
|
||||
|
|
@ -403,9 +386,7 @@ export function Timeline() {
|
|||
useTimelineStore.getState().addMediaToNewTrack(addedItem);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Show error if file processing fails
|
||||
console.error("Error processing external files:", error);
|
||||
} catch (_error) {
|
||||
toast.error("Failed to process dropped files");
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
|
|
@ -433,12 +414,16 @@ export function Timeline() {
|
|||
"[data-radix-scroll-area-viewport]"
|
||||
) as HTMLElement;
|
||||
|
||||
if (!(rulerViewport && tracksViewport)) return;
|
||||
if (!(rulerViewport && tracksViewport)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Horizontal scroll synchronization between ruler and tracks
|
||||
const handleRulerScroll = () => {
|
||||
const now = Date.now();
|
||||
if (isUpdatingRef.current || now - lastRulerSync.current < 16) return;
|
||||
if (isUpdatingRef.current || now - lastRulerSync.current < 16) {
|
||||
return;
|
||||
}
|
||||
lastRulerSync.current = now;
|
||||
isUpdatingRef.current = true;
|
||||
tracksViewport.scrollLeft = rulerViewport.scrollLeft;
|
||||
|
|
@ -446,7 +431,9 @@ export function Timeline() {
|
|||
};
|
||||
const handleTracksScroll = () => {
|
||||
const now = Date.now();
|
||||
if (isUpdatingRef.current || now - lastTracksSync.current < 16) return;
|
||||
if (isUpdatingRef.current || now - lastTracksSync.current < 16) {
|
||||
return;
|
||||
}
|
||||
lastTracksSync.current = now;
|
||||
isUpdatingRef.current = true;
|
||||
rulerViewport.scrollLeft = tracksViewport.scrollLeft;
|
||||
|
|
@ -460,8 +447,9 @@ export function Timeline() {
|
|||
if (trackLabelsViewport) {
|
||||
const handleTrackLabelsScroll = () => {
|
||||
const now = Date.now();
|
||||
if (isUpdatingRef.current || now - lastVerticalSync.current < 16)
|
||||
if (isUpdatingRef.current || now - lastVerticalSync.current < 16) {
|
||||
return;
|
||||
}
|
||||
lastVerticalSync.current = now;
|
||||
isUpdatingRef.current = true;
|
||||
tracksViewport.scrollTop = trackLabelsViewport.scrollTop;
|
||||
|
|
@ -469,8 +457,9 @@ export function Timeline() {
|
|||
};
|
||||
const handleTracksVerticalScroll = () => {
|
||||
const now = Date.now();
|
||||
if (isUpdatingRef.current || now - lastVerticalSync.current < 16)
|
||||
if (isUpdatingRef.current || now - lastVerticalSync.current < 16) {
|
||||
return;
|
||||
}
|
||||
lastVerticalSync.current = now;
|
||||
isUpdatingRef.current = true;
|
||||
trackLabelsViewport.scrollTop = tracksViewport.scrollTop;
|
||||
|
|
@ -579,12 +568,24 @@ export function Timeline() {
|
|||
const getTimeInterval = (zoom: number) => {
|
||||
const pixelsPerSecond =
|
||||
TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoom;
|
||||
if (pixelsPerSecond >= 200) return 0.1; // Every 0.1s when very zoomed in
|
||||
if (pixelsPerSecond >= 100) return 0.5; // Every 0.5s when zoomed in
|
||||
if (pixelsPerSecond >= 50) return 1; // Every 1s at normal zoom
|
||||
if (pixelsPerSecond >= 25) return 2; // Every 2s when zoomed out
|
||||
if (pixelsPerSecond >= 12) return 5; // Every 5s when more zoomed out
|
||||
if (pixelsPerSecond >= 6) return 10; // Every 10s when very zoomed out
|
||||
if (pixelsPerSecond >= 200) {
|
||||
return 0.1; // Every 0.1s when very zoomed in
|
||||
}
|
||||
if (pixelsPerSecond >= 100) {
|
||||
return 0.5; // Every 0.5s when zoomed in
|
||||
}
|
||||
if (pixelsPerSecond >= 50) {
|
||||
return 1; // Every 1s at normal zoom
|
||||
}
|
||||
if (pixelsPerSecond >= 25) {
|
||||
return 2; // Every 2s when zoomed out
|
||||
}
|
||||
if (pixelsPerSecond >= 12) {
|
||||
return 5; // Every 5s when more zoomed out
|
||||
}
|
||||
if (pixelsPerSecond >= 6) {
|
||||
return 10; // Every 10s when very zoomed out
|
||||
}
|
||||
return 30; // Every 30s when extremely zoomed out
|
||||
};
|
||||
|
||||
|
|
@ -593,7 +594,9 @@ export function Timeline() {
|
|||
|
||||
return Array.from({ length: markerCount }, (_, i) => {
|
||||
const time = i * interval;
|
||||
if (time > duration) return null;
|
||||
if (time > duration) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isMainMarker =
|
||||
time % (interval >= 1 ? Math.max(1, interval) : 1) === 0;
|
||||
|
|
@ -717,47 +720,43 @@ export function Timeline() {
|
|||
{tracks.length === 0 ? (
|
||||
<div />
|
||||
) : (
|
||||
<>
|
||||
{tracks.map((track, index) => (
|
||||
<ContextMenu key={track.id}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div
|
||||
className="absolute right-0 left-0 border-muted/30 border-b py-[0.05rem]"
|
||||
onClick={(e) => {
|
||||
// If clicking empty area (not on a element), deselect all elements
|
||||
if (
|
||||
!(e.target as HTMLElement).closest(
|
||||
".timeline-element"
|
||||
)
|
||||
) {
|
||||
clearSelectedElements();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
top: `${getCumulativeHeightBefore(tracks, index)}px`,
|
||||
height: `${getTrackHeight(track.type)}px`,
|
||||
}}
|
||||
>
|
||||
<TimelineTrackContent
|
||||
onSnapPointChange={handleSnapPointChange}
|
||||
track={track}
|
||||
zoomLevel={zoomLevel}
|
||||
/>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem
|
||||
onClick={() => toggleTrackMute(track.id)}
|
||||
>
|
||||
{track.muted ? "Unmute Track" : "Mute Track"}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem>
|
||||
Track settings (soon)
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
))}
|
||||
</>
|
||||
tracks.map((track, index) => (
|
||||
<ContextMenu key={track.id}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div
|
||||
className="absolute right-0 left-0 border-muted/30 border-b py-[0.05rem]"
|
||||
onClick={(e) => {
|
||||
// If clicking empty area (not on a element), deselect all elements
|
||||
if (
|
||||
!(e.target as HTMLElement).closest(
|
||||
".timeline-element"
|
||||
)
|
||||
) {
|
||||
clearSelectedElements();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
top: `${getCumulativeHeightBefore(tracks, index)}px`,
|
||||
height: `${getTrackHeight(track.type)}px`,
|
||||
}}
|
||||
>
|
||||
<TimelineTrackContent
|
||||
onSnapPointChange={handleSnapPointChange}
|
||||
track={track}
|
||||
zoomLevel={zoomLevel}
|
||||
/>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem
|
||||
onClick={() => toggleTrackMute(track.id)}
|
||||
>
|
||||
{track.muted ? "Unmute Track" : "Mute Track"}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem>Track settings (soon)</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
|
@ -812,7 +811,9 @@ function TimelineToolbar({
|
|||
|
||||
// Action handlers
|
||||
const handleSplitSelected = () => {
|
||||
if (selectedElements.length === 0) return;
|
||||
if (selectedElements.length === 0) {
|
||||
return;
|
||||
}
|
||||
let splitCount = 0;
|
||||
selectedElements.forEach(({ trackId, elementId }) => {
|
||||
const track = tracks.find((t) => t.id === trackId);
|
||||
|
|
@ -824,7 +825,9 @@ function TimelineToolbar({
|
|||
(element.duration - element.trimStart - element.trimEnd);
|
||||
if (currentTime > effectiveStart && currentTime < effectiveEnd) {
|
||||
const newElementId = splitElement(trackId, elementId, currentTime);
|
||||
if (newElementId) splitCount++;
|
||||
if (newElementId) {
|
||||
splitCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -834,9 +837,13 @@ function TimelineToolbar({
|
|||
};
|
||||
|
||||
const handleDuplicateSelected = () => {
|
||||
if (selectedElements.length === 0) return;
|
||||
if (selectedElements.length === 0) {
|
||||
return;
|
||||
}
|
||||
const canDuplicate = selectedElements.length === 1;
|
||||
if (!canDuplicate) return;
|
||||
if (!canDuplicate) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectedElements.forEach(({ trackId, elementId }) => {
|
||||
const track = tracks.find((t) => t.id === trackId);
|
||||
|
|
@ -868,7 +875,9 @@ function TimelineToolbar({
|
|||
const { trackId, elementId } = selectedElements[0];
|
||||
const track = tracks.find((t) => t.id === trackId);
|
||||
const element = track?.elements.find((c) => c.id === elementId);
|
||||
if (!element) return;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
const effectiveStart = element.startTime;
|
||||
const effectiveEnd =
|
||||
element.startTime +
|
||||
|
|
@ -888,7 +897,9 @@ function TimelineToolbar({
|
|||
const { trackId, elementId } = selectedElements[0];
|
||||
const track = tracks.find((t) => t.id === trackId);
|
||||
const element = track?.elements.find((c) => c.id === elementId);
|
||||
if (!element) return;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
const effectiveStart = element.startTime;
|
||||
const effectiveEnd =
|
||||
element.startTime +
|
||||
|
|
@ -915,7 +926,9 @@ function TimelineToolbar({
|
|||
};
|
||||
|
||||
const handleDeleteSelected = () => {
|
||||
if (selectedElements.length === 0) return;
|
||||
if (selectedElements.length === 0) {
|
||||
return;
|
||||
}
|
||||
selectedElements.forEach(({ trackId, elementId }) => {
|
||||
if (rippleEditingEnabled) {
|
||||
removeElementFromTrackWithRipple(trackId, elementId);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Copy,
|
||||
MoreVertical,
|
||||
Music,
|
||||
RefreshCw,
|
||||
Scissors,
|
||||
SplitSquareHorizontal,
|
||||
Trash2,
|
||||
Type,
|
||||
} from "lucide-react";
|
||||
import { Copy, RefreshCw, Scissors, Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
|
|
@ -23,8 +12,7 @@ import { useTimelineElementResize } from "@/hooks/use-timeline-element-resize";
|
|||
import { useMediaStore } from "@/stores/media-store";
|
||||
import { usePlaybackStore } from "@/stores/playback-store";
|
||||
import { useTimelineStore } from "@/stores/timeline-store";
|
||||
import { type TimelineElementProps, TrackType } from "@/types/timeline";
|
||||
import { Button } from "../../ui/button";
|
||||
import type { TimelineElementProps } from "@/types/timeline";
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
|
|
@ -32,16 +20,6 @@ import {
|
|||
ContextMenuSeparator,
|
||||
ContextMenuTrigger,
|
||||
} from "../../ui/context-menu";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "../../ui/dropdown-menu";
|
||||
import AudioWaveform from "../audio-waveform";
|
||||
|
||||
export function TimelineElement({
|
||||
|
|
@ -69,7 +47,7 @@ export function TimelineElement({
|
|||
} = useTimelineStore();
|
||||
const { currentTime } = usePlaybackStore();
|
||||
|
||||
const [elementMenuOpen, setElementMenuOpen] = useState(false);
|
||||
const [_elementMenuOpen, _setElementMenuOpen] = useState(false);
|
||||
|
||||
const {
|
||||
resizing,
|
||||
|
|
@ -122,7 +100,7 @@ export function TimelineElement({
|
|||
const { id, ...elementWithoutId } = element;
|
||||
addElementToTrack(track.id, {
|
||||
...elementWithoutId,
|
||||
name: element.name + " (copy)",
|
||||
name: `${element.name} (copy)`,
|
||||
startTime:
|
||||
element.startTime +
|
||||
(element.duration - element.trimStart - element.trimEnd) +
|
||||
|
|
@ -150,7 +128,9 @@ export function TimelineElement({
|
|||
input.accept = "video/*,audio/*,image/*";
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const success = await replaceElementMedia(track.id, element.id, file);
|
||||
|
|
@ -159,11 +139,8 @@ export function TimelineElement({
|
|||
} else {
|
||||
toast.error("Failed to replace clip");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
toast.error("Failed to replace clip");
|
||||
console.log(
|
||||
JSON.stringify({ error: "Failed to replace clip", details: error })
|
||||
);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
|
|
@ -234,8 +211,8 @@ export function TimelineElement({
|
|||
);
|
||||
}
|
||||
|
||||
const VIDEO_TILE_PADDING = 16;
|
||||
const OVERLAY_SPACE_MULTIPLIER = 1.5;
|
||||
const _VIDEO_TILE_PADDING = 16;
|
||||
const _OVERLAY_SPACE_MULTIPLIER = 1.5;
|
||||
|
||||
if (mediaItem.type === "video" && mediaItem.thumbnailUrl) {
|
||||
const trackHeight = getTrackHeight(track.type);
|
||||
|
|
@ -329,10 +306,8 @@ export function TimelineElement({
|
|||
)} ${isSelected ? "border-foreground border-t-[0.5px] border-b-[0.5px]" : ""} ${
|
||||
isBeingDragged ? "z-50" : "z-10"
|
||||
}`}
|
||||
onClick={(e) => onElementClick && onElementClick(e, element)}
|
||||
onContextMenu={(e) =>
|
||||
onElementMouseDown && onElementMouseDown(e, element)
|
||||
}
|
||||
onClick={(e) => onElementClick?.(e, element)}
|
||||
onContextMenu={(e) => onElementMouseDown?.(e, element)}
|
||||
onMouseDown={handleElementMouseDown}
|
||||
>
|
||||
<div className="absolute inset-0 flex h-full items-center">
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import {
|
||||
getTotalTracksHeight,
|
||||
TIMELINE_CONSTANTS,
|
||||
} from "@/constants/timeline-constants";
|
||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||
import { useTimelinePlayhead } from "@/hooks/use-timeline-playhead";
|
||||
import type { TimelineTrack } from "@/types/timeline";
|
||||
|
||||
|
|
|
|||
|
|
@ -20,9 +20,7 @@ import type {
|
|||
} from "@/types/timeline";
|
||||
import {
|
||||
canElementGoOnTrack,
|
||||
ensureMainTrack,
|
||||
getMainTrack,
|
||||
sortTracksByOrder,
|
||||
type TimelineTrack,
|
||||
} from "@/types/timeline";
|
||||
import { TimelineElement } from "./timeline-element";
|
||||
|
|
@ -114,7 +112,7 @@ export function TimelineTrackContent({
|
|||
|
||||
const timelineRef = useRef<HTMLDivElement>(null);
|
||||
const [isDropping, setIsDropping] = useState(false);
|
||||
const [dropPosition, setDropPosition] = useState<number | null>(null);
|
||||
const [_dropPosition, setDropPosition] = useState<number | null>(null);
|
||||
const [wouldOverlap, setWouldOverlap] = useState(false);
|
||||
const dragCounterRef = useRef(0);
|
||||
const [mouseDownLocation, setMouseDownLocation] = useState<{
|
||||
|
|
@ -124,10 +122,14 @@ export function TimelineTrackContent({
|
|||
|
||||
// Set up mouse event listeners for drag
|
||||
useEffect(() => {
|
||||
if (!dragState.isDragging) return;
|
||||
if (!dragState.isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!timelineRef.current) return;
|
||||
if (!timelineRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// On first mouse move during drag, ensure the element is selected
|
||||
if (dragState.elementId && dragState.trackId) {
|
||||
|
|
@ -218,7 +220,9 @@ export function TimelineTrackContent({
|
|||
};
|
||||
|
||||
const handleMouseUp = (e: MouseEvent) => {
|
||||
if (!(dragState.elementId && dragState.trackId)) return;
|
||||
if (!(dragState.elementId && dragState.trackId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If this track initiated the drag, we should handle the mouse up regardless of where it occurs
|
||||
const isTrackThatStartedDrag = dragState.trackId === track.id;
|
||||
|
|
@ -249,7 +253,9 @@ export function TimelineTrackContent({
|
|||
const isMouseOverThisTrack =
|
||||
e.clientY >= timelineRect.top && e.clientY <= timelineRect.bottom;
|
||||
|
||||
if (!(isMouseOverThisTrack || isTrackThatStartedDrag)) return;
|
||||
if (!(isMouseOverThisTrack || isTrackThatStartedDrag)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const finalTime = dragState.currentTime;
|
||||
|
||||
|
|
@ -394,6 +400,12 @@ export function TimelineTrackContent({
|
|||
selectedElements,
|
||||
selectElement,
|
||||
onSnapPointChange,
|
||||
currentTime,
|
||||
rippleEditingEnabled,
|
||||
snapElementEdge,
|
||||
snappingEnabled,
|
||||
track.elements.some,
|
||||
updateElementStartTimeWithRipple,
|
||||
]);
|
||||
|
||||
const handleElementMouseDown = (
|
||||
|
|
@ -488,7 +500,9 @@ export function TimelineTrackContent({
|
|||
"application/x-media-item"
|
||||
);
|
||||
|
||||
if (!(hasTimelineElement || hasMediaItem)) return;
|
||||
if (!(hasTimelineElement || hasMediaItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate drop position for overlap checking
|
||||
const trackContainer = e.currentTarget.querySelector(
|
||||
|
|
@ -557,7 +571,7 @@ export function TimelineTrackContent({
|
|||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
// Continue with default behavior
|
||||
}
|
||||
} else if (hasTimelineElement) {
|
||||
|
|
@ -588,8 +602,12 @@ export function TimelineTrackContent({
|
|||
const movingElementEnd = snappedTime + movingElementDuration;
|
||||
|
||||
wouldOverlap = track.elements.some((existingElement) => {
|
||||
if (fromTrackId === track.id && existingElement.id === elementId)
|
||||
if (
|
||||
fromTrackId === track.id &&
|
||||
existingElement.id === elementId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const existingStart = existingElement.startTime;
|
||||
const existingEnd =
|
||||
|
|
@ -603,7 +621,7 @@ export function TimelineTrackContent({
|
|||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
// Continue with default behavior
|
||||
}
|
||||
}
|
||||
|
|
@ -632,7 +650,9 @@ export function TimelineTrackContent({
|
|||
"application/x-media-item"
|
||||
);
|
||||
|
||||
if (!(hasTimelineElement || hasMediaItem)) return;
|
||||
if (!(hasTimelineElement || hasMediaItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
dragCounterRef.current++;
|
||||
setIsDropping(true);
|
||||
|
|
@ -648,7 +668,9 @@ export function TimelineTrackContent({
|
|||
"application/x-media-item"
|
||||
);
|
||||
|
||||
if (!(hasTimelineElement || hasMediaItem)) return;
|
||||
if (!(hasTimelineElement || hasMediaItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
dragCounterRef.current--;
|
||||
|
||||
|
|
@ -663,16 +685,6 @@ export function TimelineTrackContent({
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Debug logging
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
message: "Drop event started in timeline track",
|
||||
dataTransferTypes: Array.from(e.dataTransfer.types),
|
||||
trackId: track.id,
|
||||
trackType: track.type,
|
||||
})
|
||||
);
|
||||
|
||||
// Reset all drag states
|
||||
dragCounterRef.current = 0;
|
||||
setIsDropping(false);
|
||||
|
|
@ -685,12 +697,16 @@ export function TimelineTrackContent({
|
|||
"application/x-media-item"
|
||||
);
|
||||
|
||||
if (!(hasTimelineElement || hasMediaItem)) return;
|
||||
if (!(hasTimelineElement || hasMediaItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trackContainer = e.currentTarget.querySelector(
|
||||
".track-elements-container"
|
||||
) as HTMLElement;
|
||||
if (!trackContainer) return;
|
||||
if (!trackContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = trackContainer.getBoundingClientRect();
|
||||
const mouseX = Math.max(0, e.clientX - rect.left);
|
||||
|
|
@ -699,7 +715,7 @@ export function TimelineTrackContent({
|
|||
mouseX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel);
|
||||
const projectStore = useProjectStore.getState();
|
||||
const projectFps = projectStore.activeProject?.fps || 30;
|
||||
const snappedTime = snapTimeToFrame(newStartTime, projectFps);
|
||||
const _snappedTime = snapTimeToFrame(newStartTime, projectFps);
|
||||
|
||||
// Calculate drop position relative to tracks
|
||||
const currentTrackIndex = tracks.findIndex((t) => t.id === track.id);
|
||||
|
|
@ -720,7 +736,9 @@ export function TimelineTrackContent({
|
|||
const timelineElementData = e.dataTransfer.getData(
|
||||
"application/x-timeline-element"
|
||||
);
|
||||
if (!timelineElementData) return;
|
||||
if (!timelineElementData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
elementId,
|
||||
|
|
@ -759,8 +777,9 @@ export function TimelineTrackContent({
|
|||
|
||||
const hasOverlap = track.elements.some((existingElement) => {
|
||||
// Skip the element being moved if it's on the same track
|
||||
if (fromTrackId === track.id && existingElement.id === elementId)
|
||||
if (fromTrackId === track.id && existingElement.id === elementId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const existingStart = existingElement.startTime;
|
||||
const existingEnd =
|
||||
|
|
@ -813,7 +832,9 @@ export function TimelineTrackContent({
|
|||
const mediaItemData = e.dataTransfer.getData(
|
||||
"application/x-media-item"
|
||||
);
|
||||
if (!mediaItemData) return;
|
||||
if (!mediaItemData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dragData: DragData = JSON.parse(mediaItemData);
|
||||
|
||||
|
|
@ -850,7 +871,9 @@ export function TimelineTrackContent({
|
|||
const newTargetTrack = updatedTracks.find(
|
||||
(t) => t.id === targetTrackId
|
||||
);
|
||||
if (!newTargetTrack) return;
|
||||
if (!newTargetTrack) {
|
||||
return;
|
||||
}
|
||||
targetTrack = newTargetTrack;
|
||||
}
|
||||
|
||||
|
|
@ -940,7 +963,9 @@ export function TimelineTrackContent({
|
|||
const newTargetTrack = updatedTracks.find(
|
||||
(t) => t.id === targetTrackId
|
||||
);
|
||||
if (!newTargetTrack) return;
|
||||
if (!newTargetTrack) {
|
||||
return;
|
||||
}
|
||||
targetTrack = newTargetTrack;
|
||||
} else if (
|
||||
mainTrack.elements.length === 0 &&
|
||||
|
|
@ -970,7 +995,9 @@ export function TimelineTrackContent({
|
|||
const newTargetTrack = updatedTracks.find(
|
||||
(t) => t.id === targetTrackId
|
||||
);
|
||||
if (!newTargetTrack) return;
|
||||
if (!newTargetTrack) {
|
||||
return;
|
||||
}
|
||||
targetTrack = newTargetTrack;
|
||||
}
|
||||
} else if (isAudio) {
|
||||
|
|
@ -999,12 +1026,16 @@ export function TimelineTrackContent({
|
|||
const newTargetTrack = updatedTracks.find(
|
||||
(t) => t.id === targetTrackId
|
||||
);
|
||||
if (!newTargetTrack) return;
|
||||
if (!newTargetTrack) {
|
||||
return;
|
||||
}
|
||||
targetTrack = newTargetTrack;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetTrack) return;
|
||||
if (!targetTrack) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for overlaps with existing elements in target track
|
||||
const newElementDuration = mediaItem.duration || 5;
|
||||
|
|
@ -1046,8 +1077,7 @@ export function TimelineTrackContent({
|
|||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error handling drop:", error);
|
||||
} catch (_error) {
|
||||
toast.error("Failed to add media to track");
|
||||
}
|
||||
};
|
||||
|
|
@ -1087,66 +1117,64 @@ export function TimelineTrackContent({
|
|||
: ""}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{track.elements.map((element) => {
|
||||
const isSelected = selectedElements.some(
|
||||
(c) => c.trackId === track.id && c.elementId === element.id
|
||||
);
|
||||
track.elements.map((element) => {
|
||||
const isSelected = selectedElements.some(
|
||||
(c) => c.trackId === track.id && c.elementId === element.id
|
||||
);
|
||||
|
||||
const handleElementSplit = () => {
|
||||
const { currentTime } = usePlaybackStore();
|
||||
const { splitElement } = useTimelineStore();
|
||||
const splitTime = currentTime;
|
||||
const effectiveStart = element.startTime;
|
||||
const effectiveEnd =
|
||||
element.startTime +
|
||||
(element.duration - element.trimStart - element.trimEnd);
|
||||
const _handleElementSplit = () => {
|
||||
const { currentTime } = usePlaybackStore();
|
||||
const { splitElement } = useTimelineStore();
|
||||
const splitTime = currentTime;
|
||||
const effectiveStart = element.startTime;
|
||||
const effectiveEnd =
|
||||
element.startTime +
|
||||
(element.duration - element.trimStart - element.trimEnd);
|
||||
|
||||
if (splitTime > effectiveStart && splitTime < effectiveEnd) {
|
||||
const secondElementId = splitElement(
|
||||
track.id,
|
||||
element.id,
|
||||
splitTime
|
||||
);
|
||||
if (!secondElementId) {
|
||||
toast.error("Failed to split element");
|
||||
}
|
||||
} else {
|
||||
toast.error("Playhead must be within element to split");
|
||||
if (splitTime > effectiveStart && splitTime < effectiveEnd) {
|
||||
const secondElementId = splitElement(
|
||||
track.id,
|
||||
element.id,
|
||||
splitTime
|
||||
);
|
||||
if (!secondElementId) {
|
||||
toast.error("Failed to split element");
|
||||
}
|
||||
};
|
||||
} else {
|
||||
toast.error("Playhead must be within element to split");
|
||||
}
|
||||
};
|
||||
|
||||
const handleElementDuplicate = () => {
|
||||
const { addElementToTrack } = useTimelineStore.getState();
|
||||
const { id, ...elementWithoutId } = element;
|
||||
addElementToTrack(track.id, {
|
||||
...elementWithoutId,
|
||||
name: element.name + " (copy)",
|
||||
startTime:
|
||||
element.startTime +
|
||||
(element.duration - element.trimStart - element.trimEnd) +
|
||||
0.1,
|
||||
});
|
||||
};
|
||||
const _handleElementDuplicate = () => {
|
||||
const { addElementToTrack } = useTimelineStore.getState();
|
||||
const { id, ...elementWithoutId } = element;
|
||||
addElementToTrack(track.id, {
|
||||
...elementWithoutId,
|
||||
name: `${element.name} (copy)`,
|
||||
startTime:
|
||||
element.startTime +
|
||||
(element.duration - element.trimStart - element.trimEnd) +
|
||||
0.1,
|
||||
});
|
||||
};
|
||||
|
||||
const handleElementDelete = () => {
|
||||
const { removeElementFromTrack } = useTimelineStore.getState();
|
||||
removeElementFromTrack(track.id, element.id);
|
||||
};
|
||||
const _handleElementDelete = () => {
|
||||
const { removeElementFromTrack } = useTimelineStore.getState();
|
||||
removeElementFromTrack(track.id, element.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<TimelineElement
|
||||
element={element}
|
||||
isSelected={isSelected}
|
||||
key={element.id}
|
||||
onElementClick={handleElementClick}
|
||||
onElementMouseDown={handleElementMouseDown}
|
||||
track={track}
|
||||
zoomLevel={zoomLevel}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
return (
|
||||
<TimelineElement
|
||||
element={element}
|
||||
isSelected={isSelected}
|
||||
key={element.id}
|
||||
onElementClick={handleElementClick}
|
||||
onElementMouseDown={handleElementMouseDown}
|
||||
track={track}
|
||||
zoomLevel={zoomLevel}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,16 +9,14 @@ import { RiDiscordFill, RiTwitterXLine } from "react-icons/ri";
|
|||
import { getStars } from "@/lib/fetch-github-stars";
|
||||
|
||||
export function Footer() {
|
||||
const [star, setStar] = useState<string>();
|
||||
const [_star, setStar] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStars = async () => {
|
||||
try {
|
||||
const data = await getStars();
|
||||
setStar(data);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch GitHub stars", err);
|
||||
}
|
||||
} catch (_err) {}
|
||||
};
|
||||
|
||||
fetchStars();
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ const modifier: {
|
|||
};
|
||||
|
||||
function getKeyWithModifier(key: string) {
|
||||
if (key === "Ctrl") return getPlatformSpecialKey();
|
||||
if (key === "Ctrl") {
|
||||
return getPlatformSpecialKey();
|
||||
}
|
||||
return modifier[key] || key;
|
||||
}
|
||||
|
||||
|
|
@ -50,8 +52,9 @@ const ShortcutItem = ({
|
|||
if (
|
||||
key.includes("Cmd") &&
|
||||
shortcut.keys.includes(key.replace("Cmd", "Ctrl"))
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
|
@ -152,7 +155,9 @@ export const KeyboardShortcutsHelp = () => {
|
|||
const categories = Array.from(new Set(shortcuts.map((s) => s.category)));
|
||||
|
||||
useEffect(() => {
|
||||
if (!(recordingKey && recordingShortcut)) return;
|
||||
if (!(recordingKey && recordingShortcut)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -186,7 +191,7 @@ export const KeyboardShortcutsHelp = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
const handleClickOutside = (_e: MouseEvent) => {
|
||||
setRecordingKey(null);
|
||||
setRecordingShortcut(null);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -40,7 +40,9 @@ export function Handlebars({
|
|||
const measureRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!measureRef.current) return;
|
||||
if (!measureRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const measureContent = () => {
|
||||
if (measureRef.current) {
|
||||
|
|
@ -56,7 +58,7 @@ export function Handlebars({
|
|||
const timer = setTimeout(measureContent, 50);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [children, rightHandleX]);
|
||||
}, [rightHandleX]);
|
||||
|
||||
useEffect(() => {
|
||||
leftHandleX.set(leftHandle);
|
||||
|
|
@ -70,7 +72,7 @@ export function Handlebars({
|
|||
onRangeChange?.(leftHandle, rightHandle);
|
||||
}, [leftHandle, rightHandle, onRangeChange]);
|
||||
|
||||
const handleLeftDrag = (event: any, info: PanInfo) => {
|
||||
const handleLeftDrag = (_event: any, info: PanInfo) => {
|
||||
const newLeft = Math.max(
|
||||
0,
|
||||
Math.min(leftHandle + info.offset.x, rightHandle - minWidth)
|
||||
|
|
@ -78,7 +80,7 @@ export function Handlebars({
|
|||
setLeftHandle(newLeft);
|
||||
};
|
||||
|
||||
const handleRightDrag = (event: any, info: PanInfo) => {
|
||||
const handleRightDrag = (_event: any, info: PanInfo) => {
|
||||
const newRight = Math.max(
|
||||
leftHandle + minWidth,
|
||||
Math.min(contentWidth, rightHandle + info.offset.x)
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ function Title({ title }: { title: string }) {
|
|||
return <h2 className="font-bold text-lg md:text-xl">{title}</h2>;
|
||||
}
|
||||
|
||||
function Subtitle({ subtitle }: { subtitle: string }) {
|
||||
function _Subtitle({ subtitle }: { subtitle: string }) {
|
||||
return <h3 className="font-medium text-lg">{subtitle}</h3>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { storageService } from "@/lib/storage/storage-service";
|
||||
import { useMediaStore } from "@/stores/media-store";
|
||||
import { useProjectStore } from "@/stores/project-store";
|
||||
|
||||
interface StorageContextType {
|
||||
|
|
@ -61,7 +60,6 @@ export function StorageProvider({ children }: StorageProviderProps) {
|
|||
error: null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize storage:", error);
|
||||
setStatus({
|
||||
isInitialized: false,
|
||||
isLoading: false,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,9 @@ export function AudioPlayer({
|
|||
// Sync playback events
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current;
|
||||
if (!(audio && isInClipRange)) return;
|
||||
if (!(audio && isInClipRange)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleSeekEvent = (e: CustomEvent) => {
|
||||
// Always update audio time, even if outside clip range
|
||||
|
|
@ -94,7 +96,9 @@ export function AudioPlayer({
|
|||
// Sync playback state
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
if (!audio) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPlaying && isInClipRange && !trackMuted) {
|
||||
audio.play().catch(() => {});
|
||||
|
|
@ -106,7 +110,9 @@ export function AudioPlayer({
|
|||
// Sync volume and speed
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
if (!audio) {
|
||||
return;
|
||||
}
|
||||
|
||||
audio.volume = volume;
|
||||
audio.muted = muted || trackMuted;
|
||||
|
|
|
|||
|
|
@ -2,11 +2,6 @@
|
|||
|
||||
import * as React from "react";
|
||||
import * as RechartsPrimitive from "recharts";
|
||||
import {
|
||||
NameType,
|
||||
Payload,
|
||||
ValueType,
|
||||
} from "recharts/types/component/DefaultTooltipContent";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,9 @@ export function DraggableMediaItem({
|
|||
"data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=";
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDragging) return;
|
||||
if (!isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
setDragPosition({ x: e.clientX, y: e.clientY });
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
|
|||
<nav
|
||||
aria-label="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
role="navigation"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ const SidebarProvider = React.forwardRef<
|
|||
return isMobile
|
||||
? setOpenMobile((open) => !open)
|
||||
: setOpen((open) => !open);
|
||||
}, [isMobile, setOpen, setOpenMobile]);
|
||||
}, [isMobile, setOpen]);
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
|
|
@ -126,7 +126,7 @@ const SidebarProvider = React.forwardRef<
|
|||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
||||
[state, open, setOpen, isMobile, openMobile, toggleSidebar]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -33,7 +33,9 @@ export function VideoPlayer({
|
|||
// Sync playback events
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!(video && isInClipRange)) return;
|
||||
if (!(video && isInClipRange)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleSeekEvent = (e: CustomEvent) => {
|
||||
// Always update video time, even if outside clip range
|
||||
|
|
@ -94,7 +96,9 @@ export function VideoPlayer({
|
|||
// Sync playback state
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
if (!video) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPlaying && isInClipRange) {
|
||||
video.play().catch(() => {});
|
||||
|
|
@ -106,7 +110,9 @@ export function VideoPlayer({
|
|||
// Sync volume and speed
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
if (!video) {
|
||||
return;
|
||||
}
|
||||
|
||||
video.volume = volume;
|
||||
video.muted = muted;
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ type ActionFunc<A extends Action> = A extends ActionWithArgs
|
|||
: (_?: undefined, trigger?: InvocationTriggers) => void;
|
||||
|
||||
type BoundActionList = {
|
||||
[A in Action]?: Array<ActionFunc<A>>;
|
||||
[A in Action]?: ActionFunc<A>[];
|
||||
};
|
||||
|
||||
const boundActions: BoundActionList = {};
|
||||
|
|
|
|||
|
|
@ -97,7 +97,9 @@ export function frameToTime(frame: number, fps: number): number {
|
|||
}
|
||||
|
||||
export function snapTimeToFrame(time: number, fps: number): number {
|
||||
if (fps <= 0) return time; // Fallback for invalid FPS
|
||||
if (fps <= 0) {
|
||||
return time; // Fallback for invalid FPS
|
||||
}
|
||||
const frame = timeToFrame(time, fps);
|
||||
return frameToTime(frame, fps);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export function useLogin() {
|
|||
provider: "google",
|
||||
callbackURL: "/projects",
|
||||
});
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
setError("Failed to sign in with Google. Please try again.");
|
||||
setIsGoogleLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export function useSignUp() {
|
|||
});
|
||||
|
||||
router.push("/editor");
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
setError("Failed to sign up with Google. Please try again.");
|
||||
setIsGoogleLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useActionHandler } from "@/constants/actions";
|
||||
import { usePlaybackStore } from "@/stores/playback-store";
|
||||
|
|
|
|||
|
|
@ -14,13 +14,19 @@ export function useKeybindingsListener() {
|
|||
useEffect(() => {
|
||||
const handleKeyDown = (ev: KeyboardEvent) => {
|
||||
// Do not check keybinds if the mode is disabled
|
||||
if (!keybindingsEnabled) return;
|
||||
if (!keybindingsEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const binding = getKeybindingString(ev);
|
||||
if (!binding) return;
|
||||
if (!binding) {
|
||||
return;
|
||||
}
|
||||
|
||||
const boundAction = keybindings[binding];
|
||||
if (!boundAction) return;
|
||||
if (!boundAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { usePlaybackStore } from "@/stores/playback-store";
|
||||
import { useTimelineStore } from "@/stores/timeline-store";
|
||||
|
|
@ -15,7 +15,7 @@ export const usePlaybackControls = () => {
|
|||
separateAudio,
|
||||
} = useTimelineStore();
|
||||
|
||||
const handleSplitSelectedElement = useCallback(() => {
|
||||
const _handleSplitSelectedElement = useCallback(() => {
|
||||
if (selectedElements.length !== 1) {
|
||||
toast.error("Select exactly one element to split");
|
||||
return;
|
||||
|
|
@ -25,7 +25,9 @@ export const usePlaybackControls = () => {
|
|||
const track = tracks.find((t) => t.id === trackId);
|
||||
const element = track?.elements.find((e) => e.id === elementId);
|
||||
|
||||
if (!element) return;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const effectiveStart = element.startTime;
|
||||
const effectiveEnd =
|
||||
|
|
@ -40,7 +42,7 @@ export const usePlaybackControls = () => {
|
|||
splitElement(trackId, elementId, currentTime);
|
||||
}, [selectedElements, tracks, currentTime, splitElement]);
|
||||
|
||||
const handleSplitAndKeepLeftCallback = useCallback(() => {
|
||||
const _handleSplitAndKeepLeftCallback = useCallback(() => {
|
||||
if (selectedElements.length !== 1) {
|
||||
toast.error("Select exactly one element");
|
||||
return;
|
||||
|
|
@ -50,7 +52,9 @@ export const usePlaybackControls = () => {
|
|||
const track = tracks.find((t) => t.id === trackId);
|
||||
const element = track?.elements.find((e) => e.id === elementId);
|
||||
|
||||
if (!element) return;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const effectiveStart = element.startTime;
|
||||
const effectiveEnd =
|
||||
|
|
@ -65,7 +69,7 @@ export const usePlaybackControls = () => {
|
|||
splitAndKeepLeft(trackId, elementId, currentTime);
|
||||
}, [selectedElements, tracks, currentTime, splitAndKeepLeft]);
|
||||
|
||||
const handleSplitAndKeepRightCallback = useCallback(() => {
|
||||
const _handleSplitAndKeepRightCallback = useCallback(() => {
|
||||
if (selectedElements.length !== 1) {
|
||||
toast.error("Select exactly one element");
|
||||
return;
|
||||
|
|
@ -75,7 +79,9 @@ export const usePlaybackControls = () => {
|
|||
const track = tracks.find((t) => t.id === trackId);
|
||||
const element = track?.elements.find((e) => e.id === elementId);
|
||||
|
||||
if (!element) return;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const effectiveStart = element.startTime;
|
||||
const effectiveEnd =
|
||||
|
|
@ -90,7 +96,7 @@ export const usePlaybackControls = () => {
|
|||
splitAndKeepRight(trackId, elementId, currentTime);
|
||||
}, [selectedElements, tracks, currentTime, splitAndKeepRight]);
|
||||
|
||||
const handleSeparateAudioCallback = useCallback(() => {
|
||||
const _handleSeparateAudioCallback = useCallback(() => {
|
||||
if (selectedElements.length !== 1) {
|
||||
toast.error("Select exactly one media element to separate audio");
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ export function useSelectionBox({
|
|||
// Mouse down handler to start selection
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (!isEnabled) return;
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only start selection on empty space clicks
|
||||
if ((e.target as HTMLElement).closest(".timeline-element")) {
|
||||
|
|
@ -58,7 +60,9 @@ export function useSelectionBox({
|
|||
// Function to select elements within the selection box
|
||||
const selectElementsInBox = useCallback(
|
||||
(startPos: { x: number; y: number }, endPos: { x: number; y: number }) => {
|
||||
if (!containerRef.current) return;
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = containerRef.current;
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
|
|
@ -69,7 +73,7 @@ export function useSelectionBox({
|
|||
const endX = endPos.x - containerRect.left;
|
||||
const endY = endPos.y - containerRect.top;
|
||||
|
||||
const selectionRect = {
|
||||
const _selectionRect = {
|
||||
left: Math.min(startX, endX),
|
||||
top: Math.min(startY, endY),
|
||||
right: Math.max(startX, endX),
|
||||
|
|
@ -122,11 +126,6 @@ export function useSelectionBox({
|
|||
selectedElements.push({ trackId, elementId });
|
||||
}
|
||||
});
|
||||
|
||||
// Always call the callback - with elements or empty array to clear selection
|
||||
console.log(
|
||||
JSON.stringify({ selectElementsInBox: selectedElements.length })
|
||||
);
|
||||
onSelectionComplete(selectedElements);
|
||||
},
|
||||
[containerRef, onSelectionComplete]
|
||||
|
|
@ -134,7 +133,9 @@ export function useSelectionBox({
|
|||
|
||||
// Effect to track selection box movement
|
||||
useEffect(() => {
|
||||
if (!selectionBox) return;
|
||||
if (!selectionBox) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const deltaX = Math.abs(e.clientX - selectionBox.startPos.x);
|
||||
|
|
@ -161,17 +162,11 @@ export function useSelectionBox({
|
|||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
console.log(
|
||||
JSON.stringify({ mouseUp: { wasActive: selectionBox?.isActive } })
|
||||
);
|
||||
|
||||
// If we had an active selection, mark that we just finished selecting
|
||||
if (selectionBox?.isActive) {
|
||||
console.log(JSON.stringify({ settingJustFinishedSelecting: true }));
|
||||
setJustFinishedSelecting(true);
|
||||
// Clear the flag after a short delay to allow click events to check it
|
||||
setTimeout(() => {
|
||||
console.log(JSON.stringify({ clearingJustFinishedSelecting: true }));
|
||||
setJustFinishedSelecting(false);
|
||||
}, 50);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,9 @@ export function useTimelineElementResize({
|
|||
|
||||
// Set up document-level mouse listeners during resize (like proper drag behavior)
|
||||
useEffect(() => {
|
||||
if (!resizing) return;
|
||||
if (!resizing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleDocumentMouseMove = (e: MouseEvent) => {
|
||||
updateTrimFromMouseMove({ clientX: e.clientX });
|
||||
|
|
@ -60,7 +62,7 @@ export function useTimelineElementResize({
|
|||
document.removeEventListener("mousemove", handleDocumentMouseMove);
|
||||
document.removeEventListener("mouseup", handleDocumentMouseUp);
|
||||
};
|
||||
}, [resizing]); // Re-run when resizing state changes
|
||||
}, [resizing, handleResizeEnd, updateTrimFromMouseMove]); // Re-run when resizing state changes
|
||||
|
||||
const handleResizeStart = (
|
||||
e: React.MouseEvent,
|
||||
|
|
@ -91,7 +93,9 @@ export function useTimelineElementResize({
|
|||
// Media elements - check the media type
|
||||
if (element.type === "media") {
|
||||
const mediaItem = mediaItems.find((item) => item.id === element.mediaId);
|
||||
if (!mediaItem) return false;
|
||||
if (!mediaItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Images can be extended (static content)
|
||||
if (mediaItem.type === "image") {
|
||||
|
|
@ -107,7 +111,9 @@ export function useTimelineElementResize({
|
|||
};
|
||||
|
||||
const updateTrimFromMouseMove = (e: { clientX: number }) => {
|
||||
if (!resizing) return;
|
||||
if (!resizing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaX = e.clientX - resizing.startX;
|
||||
// Reasonable sensitivity for resize operations - similar to timeline scale
|
||||
|
|
|
|||
|
|
@ -42,17 +42,21 @@ export function useTimelinePlayhead({
|
|||
setIsScrubbing(true);
|
||||
handleScrub(e);
|
||||
},
|
||||
[duration, zoomLevel]
|
||||
[handleScrub]
|
||||
);
|
||||
|
||||
// Ruler mouse down handler
|
||||
const handleRulerMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
// Only handle left mouse button
|
||||
if (e.button !== 0) return;
|
||||
if (e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't interfere if clicking on the playhead itself
|
||||
if (playheadRef?.current?.contains(e.target as Node)) return;
|
||||
if (playheadRef?.current?.contains(e.target as Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
setIsDraggingRuler(true);
|
||||
|
|
@ -62,13 +66,15 @@ export function useTimelinePlayhead({
|
|||
setIsScrubbing(true);
|
||||
handleScrub(e);
|
||||
},
|
||||
[duration, zoomLevel]
|
||||
[handleScrub, playheadRef?.current?.contains]
|
||||
);
|
||||
|
||||
const handleScrub = useCallback(
|
||||
(e: MouseEvent | React.MouseEvent) => {
|
||||
const ruler = rulerRef.current;
|
||||
if (!ruler) return;
|
||||
if (!ruler) {
|
||||
return;
|
||||
}
|
||||
const rect = ruler.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const rawTime = Math.max(0, Math.min(duration, x / (50 * zoomLevel)));
|
||||
|
|
@ -84,7 +90,9 @@ export function useTimelinePlayhead({
|
|||
|
||||
// Mouse move/up event handlers
|
||||
useEffect(() => {
|
||||
if (!isScrubbing) return;
|
||||
if (!isScrubbing) {
|
||||
return;
|
||||
}
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
handleScrub(e);
|
||||
// Mark that we've dragged if ruler drag is active
|
||||
|
|
@ -94,7 +102,9 @@ export function useTimelinePlayhead({
|
|||
};
|
||||
const onMouseUp = (e: MouseEvent) => {
|
||||
setIsScrubbing(false);
|
||||
if (scrubTime !== null) seek(scrubTime); // finalize seek
|
||||
if (scrubTime !== null) {
|
||||
seek(scrubTime); // finalize seek
|
||||
}
|
||||
setScrubTime(null);
|
||||
|
||||
// Handle ruler click vs drag
|
||||
|
|
@ -130,7 +140,9 @@ export function useTimelinePlayhead({
|
|||
const tracksViewport = tracksScrollRef.current?.querySelector(
|
||||
"[data-radix-scroll-area-viewport]"
|
||||
) as HTMLElement;
|
||||
if (!(rulerViewport && tracksViewport)) return;
|
||||
if (!(rulerViewport && tracksViewport)) {
|
||||
return;
|
||||
}
|
||||
const playheadPx = playheadPosition * 50 * zoomLevel; // TIMELINE_CONSTANTS.PIXELS_PER_SECOND = 50
|
||||
const viewportWidth = rulerViewport.clientWidth;
|
||||
const scrollMin = 0;
|
||||
|
|
@ -146,7 +158,7 @@ export function useTimelinePlayhead({
|
|||
) {
|
||||
rulerViewport.scrollLeft = tracksViewport.scrollLeft = desiredScroll;
|
||||
}
|
||||
}, [playheadPosition, duration, zoomLevel, rulerScrollRef, tracksScrollRef]);
|
||||
}, [playheadPosition, zoomLevel, rulerScrollRef, tracksScrollRef]);
|
||||
|
||||
return {
|
||||
playheadPosition,
|
||||
|
|
|
|||
|
|
@ -29,9 +29,9 @@ export function useTimelineSnapping({
|
|||
const findSnapPoints = useCallback(
|
||||
(
|
||||
tracks: TimelineTrack[],
|
||||
currentTime: number,
|
||||
_currentTime: number,
|
||||
playheadTime: number,
|
||||
zoomLevel: number,
|
||||
_zoomLevel: number,
|
||||
excludeElementId?: string
|
||||
): SnapPoint[] => {
|
||||
const snapPoints: SnapPoint[] = [];
|
||||
|
|
@ -41,7 +41,9 @@ export function useTimelineSnapping({
|
|||
tracks.forEach((track) => {
|
||||
track.elements.forEach((element) => {
|
||||
// Skip the element being dragged
|
||||
if (element.id === excludeElementId) return;
|
||||
if (element.id === excludeElementId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elementStart = element.startTime;
|
||||
const elementEnd =
|
||||
|
|
@ -161,7 +163,7 @@ export function useTimelineSnapping({
|
|||
|
||||
// Adjust the snapped time back for end edge
|
||||
if (!snapToStart && snapResult.snapPoint) {
|
||||
snapResult.snappedTime = snapResult.snappedTime - elementDuration;
|
||||
snapResult.snappedTime -= elementDuration;
|
||||
}
|
||||
|
||||
return snapResult;
|
||||
|
|
|
|||
|
|
@ -156,7 +156,9 @@ function toast({ ...props }: Toast) {
|
|||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss();
|
||||
if (!open) {
|
||||
dismiss();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -179,7 +181,7 @@ function useToast() {
|
|||
listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}, [state]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
|
|
|||
|
|
@ -17,18 +17,13 @@ const url =
|
|||
const key = process.env.MARBLE_WORKSPACE_KEY ?? "cmd4iw9mm0006l804kwqv0k46";
|
||||
|
||||
async function fetchFromMarble<T>(endpoint: string): Promise<T> {
|
||||
try {
|
||||
const response = await fetch(`${url}/${key}/${endpoint}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch ${endpoint}: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching ${endpoint}:`, error);
|
||||
throw error;
|
||||
const response = await fetch(`${url}/${key}/${endpoint}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch ${endpoint}: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
export async function getPosts() {
|
||||
|
|
|
|||
|
|
@ -17,13 +17,14 @@ export async function getStars(): Promise<string> {
|
|||
throw new Error("Invalid stargazers_count from GitHub API");
|
||||
}
|
||||
|
||||
if (count >= 1_000_000)
|
||||
return (count / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
|
||||
if (count >= 1000)
|
||||
return (count / 1000).toFixed(1).replace(/\.0$/, "") + "k";
|
||||
if (count >= 1_000_000) {
|
||||
return `${(count / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;
|
||||
}
|
||||
if (count >= 1000) {
|
||||
return `${(count / 1000).toFixed(1).replace(/\.0$/, "")}k`;
|
||||
}
|
||||
return count.toString();
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch GitHub stars:", error);
|
||||
} catch (_error) {
|
||||
return "1.5k";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import { toBlobURL } from "@ffmpeg/util";
|
|||
let ffmpeg: FFmpeg | null = null;
|
||||
|
||||
export const initFFmpeg = async (): Promise<FFmpeg> => {
|
||||
if (ffmpeg) return ffmpeg;
|
||||
if (ffmpeg) {
|
||||
return ffmpeg;
|
||||
}
|
||||
|
||||
ffmpeg = new FFmpeg();
|
||||
|
||||
|
|
@ -132,17 +134,18 @@ export const getVideoInfo = async (
|
|||
let ffmpegOutput = "";
|
||||
let listening = true;
|
||||
const listener = (data: string) => {
|
||||
if (listening) ffmpegOutput += data;
|
||||
if (listening) {
|
||||
ffmpegOutput += data;
|
||||
}
|
||||
};
|
||||
ffmpeg.on("log", ({ message }) => listener(message));
|
||||
|
||||
// Run ffmpeg to get info (stderr will contain the info)
|
||||
try {
|
||||
await ffmpeg.exec(["-i", inputName, "-f", "null", "-"]);
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
listening = false;
|
||||
await ffmpeg.deleteFile(inputName);
|
||||
console.error("FFmpeg execution failed:", error);
|
||||
throw new Error(
|
||||
"Failed to extract video info. The file may be corrupted or in an unsupported format."
|
||||
);
|
||||
|
|
@ -163,8 +166,8 @@ export const getVideoInfo = async (
|
|||
if (durationMatch) {
|
||||
const [, h, m, s] = durationMatch;
|
||||
duration =
|
||||
Number.parseInt(h) * 3600 +
|
||||
Number.parseInt(m) * 60 +
|
||||
Number.parseInt(h, 10) * 3600 +
|
||||
Number.parseInt(m, 10) * 60 +
|
||||
Number.parseFloat(s);
|
||||
}
|
||||
|
||||
|
|
@ -175,8 +178,8 @@ export const getVideoInfo = async (
|
|||
height = 0,
|
||||
fps = 0;
|
||||
if (videoStreamMatch) {
|
||||
width = Number.parseInt(videoStreamMatch[1]);
|
||||
height = Number.parseInt(videoStreamMatch[2]);
|
||||
width = Number.parseInt(videoStreamMatch[1], 10);
|
||||
height = Number.parseInt(videoStreamMatch[2], 10);
|
||||
fps = Number.parseFloat(videoStreamMatch[3]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,11 +52,7 @@ export async function processMediaFiles(
|
|||
|
||||
// Generate thumbnail using FFmpeg
|
||||
thumbnailUrl = await generateThumbnail(file, 1);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"FFmpeg processing failed, falling back to basic processing:",
|
||||
error
|
||||
);
|
||||
} catch (_error) {
|
||||
// Fallback to basic processing
|
||||
const videoResult = await generateVideoThumbnail(file);
|
||||
thumbnailUrl = videoResult.thumbnailUrl;
|
||||
|
|
@ -90,8 +86,7 @@ export async function processMediaFiles(
|
|||
const percent = Math.round((completed / total) * 100);
|
||||
onProgress(percent);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error processing file:", file.name, error);
|
||||
} catch (_error) {
|
||||
toast.error(`Failed to process ${file.name}`);
|
||||
URL.revokeObjectURL(url); // Clean up on error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,9 @@ class StorageService {
|
|||
async loadProject(id: string): Promise<TProject | null> {
|
||||
const serializedProject = await this.projectsAdapter.get(id);
|
||||
|
||||
if (!serializedProject) return null;
|
||||
if (!serializedProject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert back to TProject format
|
||||
return {
|
||||
|
|
@ -142,7 +144,9 @@ class StorageService {
|
|||
mediaMetadataAdapter.get(id),
|
||||
]);
|
||||
|
||||
if (!(file && metadata)) return null;
|
||||
if (!(file && metadata)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create new object URL for the file
|
||||
const url = URL.createObjectURL(file);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ export const checkElementOverlaps = (elements: TimelineElement[]): boolean => {
|
|||
(current.duration - current.trimStart - current.trimEnd);
|
||||
|
||||
// Check if current element overlaps with next element
|
||||
if (currentEnd > next.startTime) return true; // Overlap detected
|
||||
if (currentEnd > next.startTime) {
|
||||
return true; // Overlap detected
|
||||
}
|
||||
}
|
||||
|
||||
return false; // No overlaps
|
||||
|
|
@ -36,7 +38,7 @@ export const resolveElementOverlaps = (
|
|||
const current = { ...sortedElements[i] };
|
||||
|
||||
if (resolvedElements.length > 0) {
|
||||
const previous = resolvedElements[resolvedElements.length - 1];
|
||||
const previous = resolvedElements.at(-1);
|
||||
const previousEnd =
|
||||
previous.startTime +
|
||||
(previous.duration - previous.trimStart - previous.trimEnd);
|
||||
|
|
|
|||
|
|
@ -50,7 +50,9 @@ export function isDOMElement(el: any): el is HTMLElement {
|
|||
|
||||
export function isTypableElement(el: HTMLElement): boolean {
|
||||
// If content editable, then it is editable
|
||||
if (el.isContentEditable) return true;
|
||||
if (el.isContentEditable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If element is an input and the input is enabled, then it is typable
|
||||
if (el.tagName === "INPUT") {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ export async function getWaitlistCount() {
|
|||
.select({ count: sql<number>`count(*)` })
|
||||
.from(waitlist);
|
||||
return result[0]?.count || 0;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch waitlist count:", error);
|
||||
} catch (_error) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ const findBestCanvasPreset = (aspectRatio: number): CanvasSize => {
|
|||
return { width: bestMatch.width, height: bestMatch.height };
|
||||
};
|
||||
|
||||
export const useEditorStore = create<EditorState>((set, get) => ({
|
||||
export const useEditorStore = create<EditorState>((set, _get) => ({
|
||||
// Initial states
|
||||
isInitializing: true,
|
||||
isPanelsReady: false,
|
||||
|
|
@ -81,11 +81,9 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||
},
|
||||
|
||||
initializeApp: async () => {
|
||||
console.log("Initializing video editor...");
|
||||
set({ isInitializing: true, isPanelsReady: false });
|
||||
|
||||
set({ isPanelsReady: true, isInitializing: false });
|
||||
console.log("Video editor ready");
|
||||
},
|
||||
|
||||
setCanvasSize: (size) => {
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ export const useKeybindingsStore = create<KeybindingsState>()(
|
|||
|
||||
importKeybindings: (config: KeybindingConfig) => {
|
||||
// Validate all keys and actions
|
||||
for (const [key, action] of Object.entries(config)) {
|
||||
for (const [key, _action] of Object.entries(config)) {
|
||||
// Validate the key format
|
||||
if (typeof key !== "string" || key.length === 0) {
|
||||
throw new Error(`Invalid key format: ${key}`);
|
||||
|
|
@ -169,7 +169,9 @@ function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
|
|||
|
||||
// We will always have a non-modifier key
|
||||
const key = getPressedKey(ev);
|
||||
if (!key) return null;
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// All key combos backed by modifiers are valid shortcuts (whether currently typing or not)
|
||||
if (modifierKey) {
|
||||
|
|
@ -186,7 +188,9 @@ function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
|
|||
}
|
||||
|
||||
// no modifier key here then we do not do anything while on input
|
||||
if (isDOMElement(target) && isTypableElement(target)) return null;
|
||||
if (isDOMElement(target) && isTypableElement(target)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// single key while not input
|
||||
return `${key}` as ShortcutKey;
|
||||
|
|
@ -203,16 +207,30 @@ function getPressedKey(ev: KeyboardEvent): string | null {
|
|||
}
|
||||
|
||||
// Check for special keys
|
||||
if (key === "tab") return "tab";
|
||||
if (key === " " || key === "space") return "space";
|
||||
if (key === "home") return "home";
|
||||
if (key === "end") return "end";
|
||||
if (key === "delete") return "delete";
|
||||
if (key === "backspace") return "backspace";
|
||||
if (key === "tab") {
|
||||
return "tab";
|
||||
}
|
||||
if (key === " " || key === "space") {
|
||||
return "space";
|
||||
}
|
||||
if (key === "home") {
|
||||
return "home";
|
||||
}
|
||||
if (key === "end") {
|
||||
return "end";
|
||||
}
|
||||
if (key === "delete") {
|
||||
return "delete";
|
||||
}
|
||||
if (key === "backspace") {
|
||||
return "backspace";
|
||||
}
|
||||
|
||||
// Check letter keys
|
||||
const isLetter = key.length === 1 && key >= "a" && key <= "z";
|
||||
if (isLetter) return key;
|
||||
if (isLetter) {
|
||||
return key;
|
||||
}
|
||||
|
||||
// Check number keys using physical position for AZERTY support
|
||||
if (code.startsWith("Digit")) {
|
||||
|
|
@ -224,10 +242,14 @@ function getPressedKey(ev: KeyboardEvent): string | null {
|
|||
|
||||
// Fallback for other layouts
|
||||
const isDigit = key.length === 1 && key >= "0" && key <= "9";
|
||||
if (isDigit) return key;
|
||||
if (isDigit) {
|
||||
return key;
|
||||
}
|
||||
|
||||
// Check if slash, period or enter
|
||||
if (key === "/" || key === "." || key === "enter") return key;
|
||||
if (key === "/" || key === "." || key === "enter") {
|
||||
return key;
|
||||
}
|
||||
|
||||
// If no other cases match, this is not a valid key
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { create } from "zustand";
|
||||
import { storageService } from "@/lib/storage/storage-service";
|
||||
import { generateUUID } from "@/lib/utils";
|
||||
import { useTimelineStore } from "./timeline-store";
|
||||
|
||||
export type MediaType = "image" | "video" | "audio";
|
||||
|
||||
|
|
@ -174,8 +173,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
|
|||
// Save to persistent storage in background
|
||||
try {
|
||||
await storageService.saveMediaItem(projectId, newItem);
|
||||
} catch (error) {
|
||||
console.error("Failed to save media item:", error);
|
||||
} catch (_error) {
|
||||
// Remove from local state if save failed
|
||||
set((state) => ({
|
||||
mediaItems: state.mediaItems.filter((media) => media.id !== newItem.id),
|
||||
|
|
@ -188,7 +186,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
|
|||
const item = state.mediaItems.find((media) => media.id === id);
|
||||
|
||||
// Cleanup object URLs to prevent memory leaks
|
||||
if (item && item.url) {
|
||||
if (item?.url) {
|
||||
URL.revokeObjectURL(item.url);
|
||||
if (item.thumbnailUrl) {
|
||||
URL.revokeObjectURL(item.thumbnailUrl);
|
||||
|
|
@ -203,9 +201,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
|
|||
// Remove from persistent storage
|
||||
try {
|
||||
await storageService.deleteMediaItem(projectId, id);
|
||||
} catch (error) {
|
||||
console.error("Failed to delete media item:", error);
|
||||
}
|
||||
} catch (_error) {}
|
||||
},
|
||||
|
||||
loadProjectMedia: async (projectId) => {
|
||||
|
|
@ -227,11 +223,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
|
|||
width: width || item.width,
|
||||
height: height || item.height,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to regenerate thumbnail for video ${item.id}:`,
|
||||
error
|
||||
);
|
||||
} catch (_error) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
|
@ -240,8 +232,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
|
|||
);
|
||||
|
||||
set({ mediaItems: updatedMediaItems });
|
||||
} catch (error) {
|
||||
console.error("Failed to load media items:", error);
|
||||
} catch (_error) {
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
|
|
@ -269,9 +260,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
|
|||
await Promise.all(
|
||||
mediaIds.map((id) => storageService.deleteMediaItem(projectId, id))
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to clear media items from storage:", error);
|
||||
}
|
||||
} catch (_error) {}
|
||||
},
|
||||
|
||||
clearAllMedia: () => {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ interface PlaybackStore extends PlaybackState, PlaybackControls {
|
|||
let playbackTimer: number | null = null;
|
||||
|
||||
const startTimer = (store: () => PlaybackStore) => {
|
||||
if (playbackTimer) cancelAnimationFrame(playbackTimer);
|
||||
if (playbackTimer) {
|
||||
cancelAnimationFrame(playbackTimer);
|
||||
}
|
||||
|
||||
// Use requestAnimationFrame for smoother updates
|
||||
const updateTime = () => {
|
||||
|
|
|
|||
|
|
@ -89,9 +89,6 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||
} else {
|
||||
throw new Error(`Project with id ${id} not found`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load project:", error);
|
||||
throw error; // Re-throw so the editor page can handle it
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
|
|
@ -99,7 +96,9 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||
|
||||
saveCurrentProject: async () => {
|
||||
const { activeProject } = get();
|
||||
if (!activeProject) return;
|
||||
if (!activeProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Save project metadata and timeline data in parallel
|
||||
|
|
@ -109,9 +108,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||
timelineStore.saveProjectTimeline(activeProject.id),
|
||||
]);
|
||||
await get().loadAllProjects(); // Refresh the list
|
||||
} catch (error) {
|
||||
console.error("Failed to save project:", error);
|
||||
}
|
||||
} catch (_error) {}
|
||||
},
|
||||
|
||||
loadAllProjects: async () => {
|
||||
|
|
@ -122,8 +119,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||
try {
|
||||
const projects = await storageService.loadAllProjects();
|
||||
set({ savedProjects: projects });
|
||||
} catch (error) {
|
||||
console.error("Failed to load projects:", error);
|
||||
} catch (_error) {
|
||||
} finally {
|
||||
set({ isLoading: false, isInitialized: true });
|
||||
}
|
||||
|
|
@ -148,9 +144,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||
mediaStore.clearAllMedia();
|
||||
timelineStore.clearTimeline();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete project:", error);
|
||||
}
|
||||
} catch (_error) {}
|
||||
},
|
||||
|
||||
closeProject: () => {
|
||||
|
|
@ -193,7 +187,6 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||
set({ activeProject: updatedProject });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to rename project:", error);
|
||||
toast.error("Failed to rename project", {
|
||||
description:
|
||||
error instanceof Error ? error.message : "Please try again",
|
||||
|
|
@ -241,7 +234,6 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||
await get().loadAllProjects();
|
||||
return newProject.id;
|
||||
} catch (error) {
|
||||
console.error("Failed to duplicate project:", error);
|
||||
toast.error("Failed to duplicate project", {
|
||||
description:
|
||||
error instanceof Error ? error.message : "Please try again",
|
||||
|
|
@ -252,7 +244,9 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||
|
||||
updateProjectBackground: async (backgroundColor: string) => {
|
||||
const { activeProject } = get();
|
||||
if (!activeProject) return;
|
||||
if (!activeProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedProject = {
|
||||
...activeProject,
|
||||
|
|
@ -264,8 +258,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||
await storageService.saveProject(updatedProject);
|
||||
set({ activeProject: updatedProject });
|
||||
await get().loadAllProjects(); // Refresh the list
|
||||
} catch (error) {
|
||||
console.error("Failed to update project background:", error);
|
||||
} catch (_error) {
|
||||
toast.error("Failed to update background", {
|
||||
description: "Please try again",
|
||||
});
|
||||
|
|
@ -277,7 +270,9 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||
options?: { backgroundColor?: string; blurIntensity?: number }
|
||||
) => {
|
||||
const { activeProject } = get();
|
||||
if (!activeProject) return;
|
||||
if (!activeProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedProject = {
|
||||
...activeProject,
|
||||
|
|
@ -293,8 +288,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||
await storageService.saveProject(updatedProject);
|
||||
set({ activeProject: updatedProject });
|
||||
await get().loadAllProjects(); // Refresh the list
|
||||
} catch (error) {
|
||||
console.error("Failed to update background type:", error);
|
||||
} catch (_error) {
|
||||
toast.error("Failed to update background", {
|
||||
description: "Please try again",
|
||||
});
|
||||
|
|
@ -303,7 +297,9 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||
|
||||
updateProjectFps: async (fps: number) => {
|
||||
const { activeProject } = get();
|
||||
if (!activeProject) return;
|
||||
if (!activeProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedProject = {
|
||||
...activeProject,
|
||||
|
|
@ -315,8 +311,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||
await storageService.saveProject(updatedProject);
|
||||
set({ activeProject: updatedProject });
|
||||
await get().loadAllProjects(); // Refresh the list
|
||||
} catch (error) {
|
||||
console.error("Failed to update project FPS:", error);
|
||||
} catch (_error) {
|
||||
toast.error("Failed to update project FPS", {
|
||||
description: "Please try again",
|
||||
});
|
||||
|
|
@ -336,22 +331,31 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||
const [key, order] = sortOption.split("-");
|
||||
|
||||
if (key !== "createdAt" && key !== "name") {
|
||||
console.warn(`Invalid sort key: ${key}`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const aValue = a[key];
|
||||
const bValue = b[key];
|
||||
|
||||
if (aValue === undefined || bValue === undefined) return 0;
|
||||
|
||||
if (order === "asc") {
|
||||
if (aValue < bValue) return -1;
|
||||
if (aValue > bValue) return 1;
|
||||
if (aValue === undefined || bValue === undefined) {
|
||||
return 0;
|
||||
}
|
||||
if (aValue > bValue) return -1;
|
||||
if (aValue < bValue) return 1;
|
||||
|
||||
if (order === "asc") {
|
||||
if (aValue < bValue) {
|
||||
return -1;
|
||||
}
|
||||
if (aValue > bValue) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (aValue > bValue) {
|
||||
return -1;
|
||||
}
|
||||
if (aValue < bValue) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -224,9 +224,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
if (activeProject) {
|
||||
try {
|
||||
await storageService.saveTimeline(activeProject.id, get()._tracks);
|
||||
} catch (error) {
|
||||
console.error("Failed to auto-save timeline:", error);
|
||||
}
|
||||
} catch (_error) {}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -268,8 +266,10 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
|
||||
undo: () => {
|
||||
const { history, redoStack, _tracks } = get();
|
||||
if (history.length === 0) return;
|
||||
const prev = history[history.length - 1];
|
||||
if (history.length === 0) {
|
||||
return;
|
||||
}
|
||||
const prev = history.at(-1);
|
||||
updateTracksAndSave(prev);
|
||||
set({
|
||||
history: history.slice(0, -1),
|
||||
|
|
@ -383,7 +383,9 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
const { _tracks } = get();
|
||||
const trackToRemove = _tracks.find((t) => t.id === trackId);
|
||||
|
||||
if (!trackToRemove) return;
|
||||
if (!trackToRemove) {
|
||||
return;
|
||||
}
|
||||
|
||||
get().pushHistory();
|
||||
|
||||
|
|
@ -419,7 +421,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
duration: range.endTime - range.startTime,
|
||||
});
|
||||
} else {
|
||||
const lastRange = mergedRanges[mergedRanges.length - 1];
|
||||
const lastRange = mergedRanges.at(-1);
|
||||
if (range.startTime <= lastRange.endTime) {
|
||||
// Overlapping or adjacent ranges, merge them
|
||||
lastRange.endTime = Math.max(lastRange.endTime, range.endTime);
|
||||
|
|
@ -476,26 +478,22 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
// Validate element type matches track type
|
||||
const track = get()._tracks.find((t) => t.id === trackId);
|
||||
if (!track) {
|
||||
console.error("Track not found:", trackId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use utility function for validation
|
||||
const validation = validateElementTrackCompatibility(elementData, track);
|
||||
if (!validation.isValid) {
|
||||
console.error(validation.errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// For media elements, validate mediaId exists
|
||||
if (elementData.type === "media" && !elementData.mediaId) {
|
||||
console.error("Media element must have mediaId");
|
||||
return;
|
||||
}
|
||||
|
||||
// For text elements, validate required text properties
|
||||
if (elementData.type === "text" && !elementData.content) {
|
||||
console.error("Text element must have content");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -589,7 +587,9 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
const track = _tracks.find((t) => t.id === trackId);
|
||||
const element = track?.elements.find((e) => e.id === elementId);
|
||||
|
||||
if (!(element && track)) return;
|
||||
if (!(element && track)) {
|
||||
return;
|
||||
}
|
||||
|
||||
get().pushHistory();
|
||||
|
||||
|
|
@ -658,7 +658,9 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
(element) => element.id === elementId
|
||||
);
|
||||
|
||||
if (!(elementToMove && toTrack)) return;
|
||||
if (!(elementToMove && toTrack)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate element type compatibility with target track
|
||||
const validation = validateElementTrackCompatibility(
|
||||
|
|
@ -666,7 +668,6 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
toTrack
|
||||
);
|
||||
if (!validation.isValid) {
|
||||
console.error(validation.errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -700,7 +701,9 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
trimEnd,
|
||||
pushHistory = true
|
||||
) => {
|
||||
if (pushHistory) get().pushHistory();
|
||||
if (pushHistory) {
|
||||
get().pushHistory();
|
||||
}
|
||||
updateTracksAndSave(
|
||||
get()._tracks.map((track) =>
|
||||
track.id === trackId
|
||||
|
|
@ -723,7 +726,9 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
duration,
|
||||
pushHistory = true
|
||||
) => {
|
||||
if (pushHistory) get().pushHistory();
|
||||
if (pushHistory) {
|
||||
get().pushHistory();
|
||||
}
|
||||
updateTracksAndSave(
|
||||
get()._tracks.map((track) =>
|
||||
track.id === trackId
|
||||
|
|
@ -744,7 +749,9 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
startTime,
|
||||
pushHistory = true
|
||||
) => {
|
||||
if (pushHistory) get().pushHistory();
|
||||
if (pushHistory) {
|
||||
get().pushHistory();
|
||||
}
|
||||
updateTracksAndSave(
|
||||
get()._tracks.map((track) =>
|
||||
track.id === trackId
|
||||
|
|
@ -771,7 +778,9 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
const track = _tracks.find((t) => t.id === trackId);
|
||||
const element = track?.elements.find((e) => e.id === elementId);
|
||||
|
||||
if (!(element && track)) return;
|
||||
if (!(element && track)) {
|
||||
return;
|
||||
}
|
||||
|
||||
get().pushHistory();
|
||||
|
||||
|
|
@ -801,7 +810,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
|
||||
// For ripple editing, we need to move elements that come after the moved element
|
||||
const currentElementStart = currentElement.startTime;
|
||||
const currentElementEnd =
|
||||
const _currentElementEnd =
|
||||
currentElement.startTime +
|
||||
(currentElement.duration -
|
||||
currentElement.trimStart -
|
||||
|
|
@ -880,14 +889,18 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
const track = _tracks.find((t) => t.id === trackId);
|
||||
const element = track?.elements.find((c) => c.id === elementId);
|
||||
|
||||
if (!element) return null;
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const effectiveStart = element.startTime;
|
||||
const effectiveEnd =
|
||||
element.startTime +
|
||||
(element.duration - element.trimStart - element.trimEnd);
|
||||
|
||||
if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return null;
|
||||
if (splitTime <= effectiveStart || splitTime >= effectiveEnd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
get().pushHistory();
|
||||
|
||||
|
|
@ -935,14 +948,18 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
const track = _tracks.find((t) => t.id === trackId);
|
||||
const element = track?.elements.find((c) => c.id === elementId);
|
||||
|
||||
if (!element) return;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const effectiveStart = element.startTime;
|
||||
const effectiveEnd =
|
||||
element.startTime +
|
||||
(element.duration - element.trimStart - element.trimEnd);
|
||||
|
||||
if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return;
|
||||
if (splitTime <= effectiveStart || splitTime >= effectiveEnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
get().pushHistory();
|
||||
|
||||
|
|
@ -976,14 +993,18 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
const track = _tracks.find((t) => t.id === trackId);
|
||||
const element = track?.elements.find((c) => c.id === elementId);
|
||||
|
||||
if (!element) return;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const effectiveStart = element.startTime;
|
||||
const effectiveEnd =
|
||||
element.startTime +
|
||||
(element.duration - element.trimStart - element.trimEnd);
|
||||
|
||||
if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return;
|
||||
if (splitTime <= effectiveStart || splitTime >= effectiveEnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
get().pushHistory();
|
||||
|
||||
|
|
@ -1016,7 +1037,9 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
const track = _tracks.find((t) => t.id === trackId);
|
||||
const element = track?.elements.find((c) => c.id === elementId);
|
||||
|
||||
if (!element || track?.type !== "media") return null;
|
||||
if (!element || track?.type !== "media") {
|
||||
return null;
|
||||
}
|
||||
|
||||
get().pushHistory();
|
||||
|
||||
|
|
@ -1071,13 +1094,17 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
const track = _tracks.find((t) => t.id === trackId);
|
||||
const element = track?.elements.find((c) => c.id === elementId);
|
||||
|
||||
if (!element || element.type !== "media") return false;
|
||||
if (!element || element.type !== "media") {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const mediaStore = useMediaStore.getState();
|
||||
const projectStore = useProjectStore.getState();
|
||||
|
||||
if (!projectStore.activeProject) return false;
|
||||
if (!projectStore.activeProject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Import required media processing functions
|
||||
const {
|
||||
|
|
@ -1088,7 +1115,9 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
} = await import("./media-store");
|
||||
|
||||
const fileType = getFileType(newFile);
|
||||
if (!fileType) return false;
|
||||
if (!fileType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Process the new media file
|
||||
const mediaData: any = {
|
||||
|
|
@ -1123,7 +1152,9 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
(item) => item.file === newFile
|
||||
);
|
||||
|
||||
if (!newMediaItem) return false;
|
||||
if (!newMediaItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
get().pushHistory();
|
||||
|
||||
|
|
@ -1150,20 +1181,16 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
error: "Failed to replace element media",
|
||||
details: error,
|
||||
})
|
||||
);
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
getTotalDuration: () => {
|
||||
const { _tracks } = get();
|
||||
if (_tracks.length === 0) return 0;
|
||||
if (_tracks.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const trackEndTimes = _tracks.map((track) =>
|
||||
track.elements.reduce((maxEnd, element) => {
|
||||
|
|
@ -1184,19 +1211,25 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
const tracks = await storageService.loadTimeline(projectId);
|
||||
const mediaItems = await storageService.loadAllMediaItems(projectId);
|
||||
|
||||
if (!(tracks && mediaItems.length)) return null;
|
||||
if (!(tracks && mediaItems.length)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const firstMediaElement = tracks
|
||||
.flatMap((track) => track.elements)
|
||||
.filter((element) => element.type === "media")
|
||||
.sort((a, b) => a.startTime - b.startTime)[0];
|
||||
|
||||
if (!firstMediaElement) return null;
|
||||
if (!firstMediaElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mediaItem = mediaItems.find(
|
||||
(item) => item.id === firstMediaElement.mediaId
|
||||
);
|
||||
if (!mediaItem) return null;
|
||||
if (!mediaItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mediaItem.type === "video" && mediaItem.file) {
|
||||
const { generateVideoThumbnail } = await import(
|
||||
|
|
@ -1210,16 +1243,17 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Failed to get project thumbnail:", error);
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
redo: () => {
|
||||
const { redoStack } = get();
|
||||
if (redoStack.length === 0) return;
|
||||
const next = redoStack[redoStack.length - 1];
|
||||
if (redoStack.length === 0) {
|
||||
return;
|
||||
}
|
||||
const next = redoStack.at(-1);
|
||||
updateTracksAndSave(next);
|
||||
set({ redoStack: redoStack.slice(0, -1) });
|
||||
},
|
||||
|
|
@ -1295,8 +1329,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
}
|
||||
// Clear history when loading a project
|
||||
set({ history: [], redoStack: [] });
|
||||
} catch (error) {
|
||||
console.error("Failed to load timeline:", error);
|
||||
} catch (_error) {
|
||||
// Initialize with default on error
|
||||
const defaultTracks = ensureMainTrack([]);
|
||||
updateTracks(defaultTracks);
|
||||
|
|
@ -1307,9 +1340,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
saveProjectTimeline: async (projectId) => {
|
||||
try {
|
||||
await storageService.saveTimeline(projectId, get()._tracks);
|
||||
} catch (error) {
|
||||
console.error("Failed to save timeline:", error);
|
||||
}
|
||||
} catch (_error) {}
|
||||
},
|
||||
|
||||
clearTimeline: () => {
|
||||
|
|
@ -1332,7 +1363,9 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||
|
||||
checkElementOverlap: (trackId, startTime, duration, excludeElementId) => {
|
||||
const track = get()._tracks.find((t) => t.id === trackId);
|
||||
if (!track) return false;
|
||||
if (!track) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const overlap = track.elements.some((element) => {
|
||||
const elementEnd =
|
||||
|
|
|
|||
|
|
@ -90,18 +90,28 @@ export interface TimelineTrack {
|
|||
export function sortTracksByOrder(tracks: TimelineTrack[]): TimelineTrack[] {
|
||||
return [...tracks].sort((a, b) => {
|
||||
// Text tracks always go to the top
|
||||
if (a.type === "text" && b.type !== "text") return -1;
|
||||
if (b.type === "text" && a.type !== "text") return 1;
|
||||
if (a.type === "text" && b.type !== "text") {
|
||||
return -1;
|
||||
}
|
||||
if (b.type === "text" && a.type !== "text") {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Audio tracks always go to bottom
|
||||
if (a.type === "audio" && b.type !== "audio") return 1;
|
||||
if (b.type === "audio" && a.type !== "audio") return -1;
|
||||
if (a.type === "audio" && b.type !== "audio") {
|
||||
return 1;
|
||||
}
|
||||
if (b.type === "audio" && a.type !== "audio") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Main track goes above audio but below text tracks
|
||||
if (a.isMain && !b.isMain && b.type !== "audio" && b.type !== "text")
|
||||
if (a.isMain && !b.isMain && b.type !== "audio" && b.type !== "text") {
|
||||
return 1;
|
||||
if (b.isMain && !a.isMain && a.type !== "audio" && a.type !== "text")
|
||||
}
|
||||
if (b.isMain && !a.isMain && a.type !== "audio" && a.type !== "text") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Within same category, maintain creation order
|
||||
return 0;
|
||||
|
|
|
|||
Loading…
Reference in New Issue