mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
chore: implement unsafe changes
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user