chore: implement unsafe changes

This commit is contained in:
Dominik Koch 2025-07-23 16:28:21 +02:00
parent f27f7ec7a3
commit cc70f7dd9e
61 changed files with 679 additions and 529 deletions

File diff suppressed because one or more lines are too long

View File

@ -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 });
}

View File

@ -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 }

View File

@ -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";

View File

@ -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);

View File

@ -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">

View File

@ -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}

View File

@ -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");

View File

@ -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}`);
};

View File

@ -7,7 +7,6 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
export function DeleteProjectDialog({
isOpen,

View File

@ -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 = (

View File

@ -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;

View File

@ -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();

View File

@ -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;

View File

@ -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
};

View File

@ -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();

View File

@ -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);
}
};

View File

@ -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"

View File

@ -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

View File

@ -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);

View File

@ -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">

View File

@ -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";

View File

@ -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>

View File

@ -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();

View File

@ -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);
};

View File

@ -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)

View File

@ -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>;
}

View File

@ -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,

View File

@ -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;

View File

@ -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";

View File

@ -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 });

View File

@ -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}
/>
);

View File

@ -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 (

View File

@ -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;

View File

@ -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 = {};

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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";

View File

@ -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();

View File

@ -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;

View File

@ -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);
}

View File

@ -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

View File

@ -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,

View File

@ -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;

View File

@ -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,

View File

@ -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() {

View File

@ -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";
}
}

View File

@ -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]);
}

View File

@ -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
}

View File

@ -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);

View 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);

View File

@ -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") {

View File

@ -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;
}
}

View File

@ -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) => {

View File

@ -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;

View File

@ -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: () => {

View File

@ -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 = () => {

View File

@ -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;
});

View File

@ -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 =

View File

@ -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;