mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
Merge branch 'staging' of https://github.com/mazeincoding/AppCut into staging
This commit is contained in:
@@ -43,7 +43,7 @@
|
|||||||
"input-otp": "^1.4.1",
|
"input-otp": "^1.4.1",
|
||||||
"lucide-react": "^0.468.0",
|
"lucide-react": "^0.468.0",
|
||||||
"motion": "^12.18.1",
|
"motion": "^12.18.1",
|
||||||
"next": "^15.4.3",
|
"next": "^15.4.5",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"pg": "^8.16.2",
|
"pg": "^8.16.2",
|
||||||
"radix-ui": "^1.4.2",
|
"radix-ui": "^1.4.2",
|
||||||
|
|||||||
@@ -5,8 +5,14 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ExternalLink } from "lucide-react";
|
import { ExternalLink } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { GithubIcon } from "@/components/icons";
|
import {
|
||||||
|
GithubIcon,
|
||||||
|
MarbleIcon,
|
||||||
|
VercelIcon,
|
||||||
|
DataBuddyIcon,
|
||||||
|
} from "@/components/icons";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { EXTERNAL_TOOLS } from "@/constants/site";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Contributors - OpenCut",
|
title: "Contributors - OpenCut",
|
||||||
@@ -39,7 +45,7 @@ async function getContributors(): Promise<Contributor[]> {
|
|||||||
"User-Agent": "OpenCut-Web-App",
|
"User-Agent": "OpenCut-Web-App",
|
||||||
},
|
},
|
||||||
next: { revalidate: 600 }, // 10 minutes
|
next: { revalidate: 600 }, // 10 minutes
|
||||||
}
|
} as RequestInit
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -236,6 +242,53 @@ export default async function ContributorsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="mt-6 mb-20">
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<h2 className="text-2xl font-semibold mb-2">External Tools</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Tools we use to build OpenCut
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 max-w-4xl mx-auto">
|
||||||
|
{EXTERNAL_TOOLS.map((tool, index) => {
|
||||||
|
const IconComponent = {
|
||||||
|
MarbleIcon,
|
||||||
|
VercelIcon,
|
||||||
|
DataBuddyIcon,
|
||||||
|
}[tool.icon];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={tool.name}
|
||||||
|
href={tool.url}
|
||||||
|
target="_blank"
|
||||||
|
className="group block"
|
||||||
|
style={{
|
||||||
|
animationDelay: `${index * 100}ms`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card className="h-full bg-background/80 backdrop-blur-sm border-2 group-hover:border-muted-foreground/20 transition-all duration-300 group-hover:shadow-xl">
|
||||||
|
<CardContent className="p-6 text-center h-full flex flex-col">
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="w-12 h-12 mx-auto rounded-full bg-muted/50 flex items-center justify-center group-hover:bg-muted/70 transition-colors">
|
||||||
|
<IconComponent className="h-6 w-6" size={24} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-lg mb-2 group-hover:text-foreground/80 transition-colors">
|
||||||
|
{tool.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground flex-1">
|
||||||
|
{tool.description}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-32 text-center">
|
<div className="mt-32 text-center">
|
||||||
<div className="max-w-2xl mx-auto">
|
<div className="max-w-2xl mx-auto">
|
||||||
<h2 className="text-3xl font-bold mb-4">Join the community</h2>
|
<h2 className="text-3xl font-bold mb-4">Join the community</h2>
|
||||||
|
|||||||
@@ -19,22 +19,21 @@ const AudioWaveform: React.FC<AudioWaveformProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
|
let ws = wavesurfer.current;
|
||||||
|
|
||||||
const initWaveSurfer = async () => {
|
const initWaveSurfer = async () => {
|
||||||
if (!waveformRef.current || !audioUrl) return;
|
if (!waveformRef.current || !audioUrl) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Clean up any existing instance
|
// Clear any existing instance safely
|
||||||
if (wavesurfer.current) {
|
if (ws) {
|
||||||
try {
|
// Instead of immediately destroying, just set to null
|
||||||
wavesurfer.current.destroy();
|
// We'll destroy it outside this function
|
||||||
} catch (e) {
|
|
||||||
// Silently ignore destroy errors
|
|
||||||
}
|
|
||||||
wavesurfer.current = null;
|
wavesurfer.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
wavesurfer.current = WaveSurfer.create({
|
// Create a fresh instance
|
||||||
|
const newWaveSurfer = WaveSurfer.create({
|
||||||
container: waveformRef.current,
|
container: waveformRef.current,
|
||||||
waveColor: "rgba(255, 255, 255, 0.6)",
|
waveColor: "rgba(255, 255, 255, 0.6)",
|
||||||
progressColor: "rgba(255, 255, 255, 0.9)",
|
progressColor: "rgba(255, 255, 255, 0.9)",
|
||||||
@@ -46,15 +45,28 @@ const AudioWaveform: React.FC<AudioWaveformProps> = ({
|
|||||||
interact: false,
|
interact: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Assign to ref only if component is still mounted
|
||||||
|
if (mounted) {
|
||||||
|
wavesurfer.current = newWaveSurfer;
|
||||||
|
} else {
|
||||||
|
// Component unmounted during initialization, clean up
|
||||||
|
try {
|
||||||
|
newWaveSurfer.destroy();
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore destroy errors
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Event listeners
|
// Event listeners
|
||||||
wavesurfer.current.on("ready", () => {
|
newWaveSurfer.on("ready", () => {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setError(false);
|
setError(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
wavesurfer.current.on("error", (err) => {
|
newWaveSurfer.on("error", (err) => {
|
||||||
console.error("WaveSurfer error:", err);
|
console.error("WaveSurfer error:", err);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setError(true);
|
setError(true);
|
||||||
@@ -62,7 +74,7 @@ const AudioWaveform: React.FC<AudioWaveformProps> = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await wavesurfer.current.load(audioUrl);
|
await newWaveSurfer.load(audioUrl);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to initialize WaveSurfer:", err);
|
console.error("Failed to initialize WaveSurfer:", err);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -72,17 +84,50 @@ const AudioWaveform: React.FC<AudioWaveformProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
initWaveSurfer();
|
// First safely destroy previous instance if it exists
|
||||||
|
if (ws) {
|
||||||
|
// Use this pattern to safely destroy the previous instance
|
||||||
|
const wsToDestroy = ws;
|
||||||
|
// Detach from ref immediately
|
||||||
|
wavesurfer.current = null;
|
||||||
|
|
||||||
|
// Wait a tick to destroy so any pending operations can complete
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
try {
|
||||||
|
wsToDestroy.destroy();
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore errors during destroy
|
||||||
|
}
|
||||||
|
// Only initialize new instance after destroying the old one
|
||||||
|
if (mounted) {
|
||||||
|
initWaveSurfer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No previous instance to clean up, initialize directly
|
||||||
|
initWaveSurfer();
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
// Mark component as unmounted
|
||||||
mounted = false;
|
mounted = false;
|
||||||
if (wavesurfer.current) {
|
|
||||||
try {
|
// Store reference to current wavesurfer instance
|
||||||
wavesurfer.current.destroy();
|
const wsToDestroy = wavesurfer.current;
|
||||||
} catch (e) {
|
|
||||||
// Silently ignore destroy errors
|
// Immediately clear the ref to prevent accessing it after unmount
|
||||||
}
|
wavesurfer.current = null;
|
||||||
wavesurfer.current = null;
|
|
||||||
|
// If we have an instance to clean up, do it safely
|
||||||
|
if (wsToDestroy) {
|
||||||
|
// Delay destruction to avoid race conditions
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
try {
|
||||||
|
wsToDestroy.destroy();
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore destroy errors - they're expected
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [audioUrl, height]);
|
}, [audioUrl, height]);
|
||||||
|
|||||||
@@ -234,6 +234,7 @@ export function PreviewPanel() {
|
|||||||
|
|
||||||
tracks.forEach((track) => {
|
tracks.forEach((track) => {
|
||||||
track.elements.forEach((element) => {
|
track.elements.forEach((element) => {
|
||||||
|
if (element.hidden) return;
|
||||||
const elementStart = element.startTime;
|
const elementStart = element.startTime;
|
||||||
const elementEnd =
|
const elementEnd =
|
||||||
element.startTime +
|
element.startTime +
|
||||||
@@ -826,6 +827,18 @@ function PreviewToolbar({
|
|||||||
setCanvasSizeToOriginal(aspectRatio);
|
setCanvasSizeToOriginal(aspectRatio);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const totalDuration = getTotalDuration();
|
||||||
|
|
||||||
|
const skipBackward = () => {
|
||||||
|
const newTime = Math.max(0, currentTime - 1);
|
||||||
|
setCurrentTime(newTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
const skipForward = () => {
|
||||||
|
const newTime = Math.min(totalDuration, currentTime + 1);
|
||||||
|
setCurrentTime(newTime);
|
||||||
|
};
|
||||||
|
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
return (
|
return (
|
||||||
<FullscreenToolbar
|
<FullscreenToolbar
|
||||||
@@ -871,19 +884,41 @@ function PreviewToolbar({
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div className="flex items-center gap-1">
|
||||||
variant="text"
|
<Button
|
||||||
size="icon"
|
variant="text"
|
||||||
onClick={toggle}
|
size="icon"
|
||||||
disabled={!hasAnyElements}
|
onClick={skipBackward}
|
||||||
className="h-auto p-0"
|
disabled={!hasAnyElements}
|
||||||
>
|
className="h-auto p-0 text-white hover:text-white/80"
|
||||||
{isPlaying ? (
|
title="Skip backward 1s"
|
||||||
<Pause className="h-3 w-3" />
|
>
|
||||||
) : (
|
<SkipBack className="h-3 w-3" />
|
||||||
<Play className="h-3 w-3" />
|
</Button>
|
||||||
)}
|
<Button
|
||||||
</Button>
|
variant="text"
|
||||||
|
size="icon"
|
||||||
|
onClick={toggle}
|
||||||
|
disabled={!hasAnyElements}
|
||||||
|
className="h-auto p-0 text-white hover:text-white/80"
|
||||||
|
>
|
||||||
|
{isPlaying ? (
|
||||||
|
<Pause className="h-3 w-3" />
|
||||||
|
) : (
|
||||||
|
<Play className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
size="icon"
|
||||||
|
onClick={skipForward}
|
||||||
|
disabled={!hasAnyElements}
|
||||||
|
className="h-auto p-0 text-white hover:text-white/80"
|
||||||
|
title="Skip forward 1s"
|
||||||
|
>
|
||||||
|
<SkipForward className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<BackgroundSettings />
|
<BackgroundSettings />
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
Link,
|
Link,
|
||||||
ZoomIn,
|
ZoomIn,
|
||||||
ZoomOut,
|
ZoomOut,
|
||||||
|
Bookmark,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -651,6 +652,30 @@ export function Timeline() {
|
|||||||
);
|
);
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
|
{/* Bookmark markers */}
|
||||||
|
{(() => {
|
||||||
|
const { activeProject } = useProjectStore.getState();
|
||||||
|
if (!activeProject?.bookmarks?.length) return null;
|
||||||
|
|
||||||
|
return activeProject.bookmarks.map((bookmarkTime, i) => (
|
||||||
|
<div
|
||||||
|
key={`bookmark-${i}`}
|
||||||
|
className="absolute top-0 h-10 w-0.5 !bg-primary cursor-pointer"
|
||||||
|
style={{
|
||||||
|
left: `${bookmarkTime * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel}px`,
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
usePlaybackStore.getState().seek(bookmarkTime);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="absolute top-[-1px] left-[-5px] text-primary">
|
||||||
|
<Bookmark className="h-3 w-3 fill-primary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
@@ -773,6 +798,23 @@ export function Timeline() {
|
|||||||
<ContextMenuItem onClick={(e) => e.stopPropagation()}>
|
<ContextMenuItem onClick={(e) => e.stopPropagation()}>
|
||||||
Track settings (soon)
|
Track settings (soon)
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
|
{activeProject?.bookmarks?.length && activeProject.bookmarks.length > 0 && (
|
||||||
|
<>
|
||||||
|
<ContextMenuItem disabled>Bookmarks</ContextMenuItem>
|
||||||
|
{activeProject.bookmarks.map((bookmarkTime, i) => (
|
||||||
|
<ContextMenuItem
|
||||||
|
key={`bookmark-menu-${i}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
seek(bookmarkTime);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Bookmark className="h-3 w-3 mr-2 inline-block" />
|
||||||
|
{bookmarkTime.toFixed(1)}s
|
||||||
|
</ContextMenuItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
))}
|
))}
|
||||||
@@ -828,6 +870,7 @@ function TimelineToolbar({
|
|||||||
toggleRippleEditing,
|
toggleRippleEditing,
|
||||||
} = useTimelineStore();
|
} = useTimelineStore();
|
||||||
const { currentTime, duration, isPlaying, toggle } = usePlaybackStore();
|
const { currentTime, duration, isPlaying, toggle } = usePlaybackStore();
|
||||||
|
const { toggleBookmark, isBookmarked } = useProjectStore();
|
||||||
|
|
||||||
// Action handlers
|
// Action handlers
|
||||||
const handleSplitSelected = () => {
|
const handleSplitSelected = () => {
|
||||||
@@ -957,6 +1000,13 @@ function TimelineToolbar({
|
|||||||
const handleZoomSliderChange = (values: number[]) => {
|
const handleZoomSliderChange = (values: number[]) => {
|
||||||
setZoomLevel(values[0]);
|
setZoomLevel(values[0]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleBookmark = async () => {
|
||||||
|
await toggleBookmark(currentTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the current time is bookmarked
|
||||||
|
const currentBookmarked = isBookmarked(currentTime);
|
||||||
return (
|
return (
|
||||||
<div className="border-b flex items-center justify-between px-2 py-1">
|
<div className="border-b flex items-center justify-between px-2 py-1">
|
||||||
<div className="flex items-center gap-1 w-full">
|
<div className="flex items-center gap-1 w-full">
|
||||||
@@ -1088,6 +1138,17 @@ function TimelineToolbar({
|
|||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Delete element (Delete)</TooltipContent>
|
<TooltipContent>Delete element (Delete)</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<div className="w-px h-6 bg-border mx-1" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="text" size="icon" onClick={handleToggleBookmark}>
|
||||||
|
<Bookmark className={`h-4 w-4 ${currentBookmarked ? "fill-primary text-primary" : ""}`} />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{currentBookmarked ? "Remove bookmark" : "Add bookmark"}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@@ -1121,6 +1182,8 @@ function TimelineToolbar({
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<div className="h-6 w-px bg-border mx-1" />
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button variant="text" size="icon" onClick={handleZoomOut}>
|
<Button variant="text" size="icon" onClick={handleZoomOut}>
|
||||||
<ZoomOut className="h-4 w-4" />
|
<ZoomOut className="h-4 w-4" />
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ import {
|
|||||||
Type,
|
Type,
|
||||||
Copy,
|
Copy,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
EyeOff,
|
||||||
|
Eye,
|
||||||
|
Volume2,
|
||||||
|
VolumeX,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useMediaStore } from "@/stores/media-store";
|
import { useMediaStore } from "@/stores/media-store";
|
||||||
import { useTimelineStore } from "@/stores/timeline-store";
|
import { useTimelineStore } from "@/stores/timeline-store";
|
||||||
@@ -66,11 +70,18 @@ export function TimelineElement({
|
|||||||
addElementToTrack,
|
addElementToTrack,
|
||||||
replaceElementMedia,
|
replaceElementMedia,
|
||||||
rippleEditingEnabled,
|
rippleEditingEnabled,
|
||||||
|
toggleElementHidden,
|
||||||
} = useTimelineStore();
|
} = useTimelineStore();
|
||||||
const { currentTime } = usePlaybackStore();
|
const { currentTime } = usePlaybackStore();
|
||||||
|
|
||||||
const [elementMenuOpen, setElementMenuOpen] = useState(false);
|
const [elementMenuOpen, setElementMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const mediaItem =
|
||||||
|
element.type === "media"
|
||||||
|
? mediaItems.find((item) => item.id === element.mediaId)
|
||||||
|
: null;
|
||||||
|
const isAudio = mediaItem?.type === "audio";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
resizing,
|
resizing,
|
||||||
isResizing,
|
isResizing,
|
||||||
@@ -141,6 +152,11 @@ export function TimelineElement({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleElementHidden = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleElementHidden(track.id, element.id);
|
||||||
|
};
|
||||||
|
|
||||||
const handleReplaceClip = (e: React.MouseEvent) => {
|
const handleReplaceClip = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (element.type !== "media") {
|
if (element.type !== "media") {
|
||||||
@@ -332,7 +348,7 @@ export function TimelineElement({
|
|||||||
track.type
|
track.type
|
||||||
)} ${isSelected ? "border-b-[0.5px] border-t-[0.5px] border-foreground" : ""} ${
|
)} ${isSelected ? "border-b-[0.5px] border-t-[0.5px] border-foreground" : ""} ${
|
||||||
isBeingDragged ? "z-50" : "z-10"
|
isBeingDragged ? "z-50" : "z-10"
|
||||||
}`}
|
} ${element.hidden ? "opacity-50" : ""}`}
|
||||||
onClick={(e) => onElementClick && onElementClick(e, element)}
|
onClick={(e) => onElementClick && onElementClick(e, element)}
|
||||||
onMouseDown={handleElementMouseDown}
|
onMouseDown={handleElementMouseDown}
|
||||||
onContextMenu={(e) =>
|
onContextMenu={(e) =>
|
||||||
@@ -343,6 +359,16 @@ export function TimelineElement({
|
|||||||
{renderElementContent()}
|
{renderElementContent()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{element.hidden && (
|
||||||
|
<div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center pointer-events-none">
|
||||||
|
{isAudio ? (
|
||||||
|
<VolumeX className="h-6 w-6 text-white" />
|
||||||
|
) : (
|
||||||
|
<EyeOff className="h-6 w-6 text-white" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@@ -363,6 +389,29 @@ export function TimelineElement({
|
|||||||
<Scissors className="h-4 w-4 mr-2" />
|
<Scissors className="h-4 w-4 mr-2" />
|
||||||
Split at playhead
|
Split at playhead
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem onClick={handleToggleElementHidden}>
|
||||||
|
{isAudio ? (
|
||||||
|
element.hidden ? (
|
||||||
|
<Volume2 className="h-4 w-4 mr-2" />
|
||||||
|
) : (
|
||||||
|
<VolumeX className="h-4 w-4 mr-2" />
|
||||||
|
)
|
||||||
|
) : element.hidden ? (
|
||||||
|
<Eye className="h-4 w-4 mr-2" />
|
||||||
|
) : (
|
||||||
|
<EyeOff className="h-4 w-4 mr-2" />
|
||||||
|
)}
|
||||||
|
<span>
|
||||||
|
{isAudio
|
||||||
|
? element.hidden
|
||||||
|
? "Unmute"
|
||||||
|
: "Mute"
|
||||||
|
: element.hidden
|
||||||
|
? "Show"
|
||||||
|
: "Hide"}{" "}
|
||||||
|
{element.type === "text" ? "text" : "clip"}
|
||||||
|
</span>
|
||||||
|
</ContextMenuItem>
|
||||||
<ContextMenuItem onClick={handleElementDuplicateContext}>
|
<ContextMenuItem onClick={handleElementDuplicateContext}>
|
||||||
<Copy className="h-4 w-4 mr-2" />
|
<Copy className="h-4 w-4 mr-2" />
|
||||||
Duplicate {element.type === "text" ? "text" : "clip"}
|
Duplicate {element.type === "text" ? "text" : "clip"}
|
||||||
|
|||||||
@@ -113,3 +113,53 @@ export function BackgroundIcon({ className }: { className?: string }) {
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function MarbleIcon({
|
||||||
|
className = "",
|
||||||
|
size = 32,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
size?: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
>
|
||||||
|
<path fill="#202027" d="M0 0h256v256H0z" />
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
d="M116.032 94.016q1.408.256 6.272 9.856 4.856 9.472 11.904 24.576l4.096 8.576a42.4 42.4 0 0 0 4.992-6.784q2.304-3.96 5.76-10.624 3.96-7.56 5.504-9.728 6.144-9.344 13.312-19.84.896-1.408 3.2-1.92a4.8 4.8 0 0 1 1.28-.128q3.072 0 6.784 2.56 3.84 2.56 4.992 5.248a4.8 4.8 0 0 1 .256 1.92q0 1.28-.128 1.92l-.64 2.944q-.256 1.664-.512 3.968a3200 3200 0 0 0-5.888 46.848 56 56 0 0 0-.512 8.064l-.256 4.096a32 32 0 0 0-.128 2.944q0 1.672-.384 2.304-.384.64-1.408.64-.768 0-2.304-.384-4.096-.896-6.272-3.2-2.176-2.432-2.432-6.656-.128-2.176-.128-6.784 0-4.736.256-14.464l.128-7.04.256-10.112q-1.152 1.024-5.888 8.064a328 328 0 0 0-9.088 14.464q-4.48 7.56-5.888 11.648-1.28 3.2-2.56 4.736-1.152 1.536-2.816 1.536-1.536 0-3.968-1.408-6.912-4.224-8.448-11.648a336 336 0 0 0-6.016-23.68 40 40 0 0 0-2.56-6.272q-1.792-3.968-2.304-4.992l-1.664-3.328q-18.68 24.832-25.344 45.568-1.28 4.352-2.432 6.272-1.152 1.792-2.688 1.792-2.432 0-6.912-4.352Q72 158.144 72 154.304q0-2.04.768-4.096a108 108 0 0 1 16-30.08l3.968-5.248a432 432 0 0 0 12.16-16.64q3.072-4.48 8.192-4.48.896 0 2.944.256"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataBuddyIcon({
|
||||||
|
className = "",
|
||||||
|
size = 32,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
size?: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
viewBox="0 0 8 8"
|
||||||
|
shapeRendering="crispEdges"
|
||||||
|
>
|
||||||
|
<path d="M0 0h8v8H0z" />
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
d="M1 1h1v6H1zm1 0h4v1H2zm4 1h1v1H6zm0 1h1v1H6zm0 1h1v1H6zm0 1h1v1H6zM2 6h4v1H2zm1-3h1v1H3zm1 1h1v1H4z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,3 +9,25 @@ export const SITE_INFO = {
|
|||||||
twitterImage: "/open-graph/default.jpg",
|
twitterImage: "/open-graph/default.jpg",
|
||||||
favicon: "/favicon.ico",
|
favicon: "/favicon.ico",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const EXTERNAL_TOOLS = [
|
||||||
|
{
|
||||||
|
name: "Marble",
|
||||||
|
description:
|
||||||
|
"Modern headless CMS for content management and the blog for OpenCut",
|
||||||
|
url: "https://marblecms.com?utm_source=opencut",
|
||||||
|
icon: "MarbleIcon" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Vercel",
|
||||||
|
description: "Platform where we deploy and host OpenCut",
|
||||||
|
url: "https://vercel.com?utm_source=opencut",
|
||||||
|
icon: "VercelIcon" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Databuddy",
|
||||||
|
description: "GDPR compliant analytics and user insights for OpenCut",
|
||||||
|
url: "https://databuddy.cc?utm_source=opencut",
|
||||||
|
icon: "DataBuddyIcon" as const,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ class StorageService {
|
|||||||
backgroundColor: project.backgroundColor,
|
backgroundColor: project.backgroundColor,
|
||||||
backgroundType: project.backgroundType,
|
backgroundType: project.backgroundType,
|
||||||
blurIntensity: project.blurIntensity,
|
blurIntensity: project.blurIntensity,
|
||||||
|
bookmarks: project.bookmarks,
|
||||||
|
fps: project.fps,
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.projectsAdapter.set(project.id, serializedProject);
|
await this.projectsAdapter.set(project.id, serializedProject);
|
||||||
@@ -83,6 +85,8 @@ class StorageService {
|
|||||||
backgroundColor: serializedProject.backgroundColor,
|
backgroundColor: serializedProject.backgroundColor,
|
||||||
backgroundType: serializedProject.backgroundType,
|
backgroundType: serializedProject.backgroundType,
|
||||||
blurIntensity: serializedProject.blurIntensity,
|
blurIntensity: serializedProject.blurIntensity,
|
||||||
|
bookmarks: serializedProject.bookmarks,
|
||||||
|
fps: serializedProject.fps,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export interface StorageConfig {
|
|||||||
export type SerializedProject = Omit<TProject, "createdAt" | "updatedAt"> & {
|
export type SerializedProject = Omit<TProject, "createdAt" | "updatedAt"> & {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
bookmarks?: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extend FileSystemDirectoryHandle with missing async iterator methods
|
// Extend FileSystemDirectoryHandle with missing async iterator methods
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ interface ProjectStore {
|
|||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
updateProjectFps: (fps: number) => Promise<void>;
|
updateProjectFps: (fps: number) => Promise<void>;
|
||||||
|
|
||||||
|
// Bookmark methods
|
||||||
|
toggleBookmark: (time: number) => Promise<void>;
|
||||||
|
isBookmarked: (time: number) => boolean;
|
||||||
|
removeBookmark: (time: number) => Promise<void>;
|
||||||
|
|
||||||
getFilteredAndSortedProjects: (
|
getFilteredAndSortedProjects: (
|
||||||
searchQuery: string,
|
searchQuery: string,
|
||||||
sortOption: string
|
sortOption: string
|
||||||
@@ -47,6 +52,97 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
isInitialized: false,
|
isInitialized: false,
|
||||||
invalidProjectIds: new Set<string>(),
|
invalidProjectIds: new Set<string>(),
|
||||||
|
|
||||||
|
// Implementation of bookmark methods
|
||||||
|
toggleBookmark: async (time: number) => {
|
||||||
|
const { activeProject } = get();
|
||||||
|
if (!activeProject) return;
|
||||||
|
|
||||||
|
// Round time to the nearest frame
|
||||||
|
const fps = activeProject.fps || 30;
|
||||||
|
const frameTime = Math.round(time * fps) / fps;
|
||||||
|
|
||||||
|
const bookmarks = activeProject.bookmarks || [];
|
||||||
|
let updatedBookmarks: number[];
|
||||||
|
|
||||||
|
// Check if already bookmarked
|
||||||
|
const bookmarkIndex = bookmarks.findIndex(
|
||||||
|
(bookmark) => Math.abs(bookmark - frameTime) < 0.001
|
||||||
|
);
|
||||||
|
|
||||||
|
if (bookmarkIndex !== -1) {
|
||||||
|
// Remove bookmark
|
||||||
|
updatedBookmarks = bookmarks.filter((_, i) => i !== bookmarkIndex);
|
||||||
|
} else {
|
||||||
|
// Add bookmark
|
||||||
|
updatedBookmarks = [...bookmarks, frameTime].sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedProject = {
|
||||||
|
...activeProject,
|
||||||
|
bookmarks: updatedBookmarks,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await storageService.saveProject(updatedProject);
|
||||||
|
set({ activeProject: updatedProject });
|
||||||
|
await get().loadAllProjects(); // Refresh the list
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update project bookmarks:", error);
|
||||||
|
toast.error("Failed to update bookmarks", {
|
||||||
|
description: "Please try again",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isBookmarked: (time: number) => {
|
||||||
|
const { activeProject } = get();
|
||||||
|
if (!activeProject || !activeProject.bookmarks) return false;
|
||||||
|
|
||||||
|
// Round time to the nearest frame
|
||||||
|
const fps = activeProject.fps || 30;
|
||||||
|
const frameTime = Math.round(time * fps) / fps;
|
||||||
|
|
||||||
|
return activeProject.bookmarks.some(
|
||||||
|
(bookmark) => Math.abs(bookmark - frameTime) < 0.001
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeBookmark: async (time: number) => {
|
||||||
|
const { activeProject } = get();
|
||||||
|
if (!activeProject || !activeProject.bookmarks) return;
|
||||||
|
|
||||||
|
// Round time to the nearest frame
|
||||||
|
const fps = activeProject.fps || 30;
|
||||||
|
const frameTime = Math.round(time * fps) / fps;
|
||||||
|
|
||||||
|
const updatedBookmarks = activeProject.bookmarks.filter(
|
||||||
|
(bookmark) => Math.abs(bookmark - frameTime) >= 0.001
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updatedBookmarks.length === activeProject.bookmarks.length) {
|
||||||
|
// No bookmark found to remove
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedProject = {
|
||||||
|
...activeProject,
|
||||||
|
bookmarks: updatedBookmarks,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await storageService.saveProject(updatedProject);
|
||||||
|
set({ activeProject: updatedProject });
|
||||||
|
await get().loadAllProjects(); // Refresh the list
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update project bookmarks:", error);
|
||||||
|
toast.error("Failed to remove bookmark", {
|
||||||
|
description: "Please try again",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
createNewProject: async (name: string) => {
|
createNewProject: async (name: string) => {
|
||||||
const newProject: TProject = {
|
const newProject: TProject = {
|
||||||
id: generateUUID(),
|
id: generateUUID(),
|
||||||
@@ -57,6 +153,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
backgroundColor: "#000000",
|
backgroundColor: "#000000",
|
||||||
backgroundType: "color",
|
backgroundType: "color",
|
||||||
blurIntensity: 8,
|
blurIntensity: 8,
|
||||||
|
bookmarks: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
set({ activeProject: newProject });
|
set({ activeProject: newProject });
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ interface TimelineStore {
|
|||||||
pushHistory?: boolean
|
pushHistory?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
toggleTrackMute: (trackId: string) => void;
|
toggleTrackMute: (trackId: string) => void;
|
||||||
|
toggleElementHidden: (trackId: string, elementId: string) => void;
|
||||||
|
|
||||||
// Split operations for elements
|
// Split operations for elements
|
||||||
splitElement: (
|
splitElement: (
|
||||||
@@ -868,6 +869,24 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleElementHidden: (trackId, elementId) => {
|
||||||
|
get().pushHistory();
|
||||||
|
updateTracksAndSave(
|
||||||
|
get()._tracks.map((track) =>
|
||||||
|
track.id === trackId
|
||||||
|
? {
|
||||||
|
...track,
|
||||||
|
elements: track.elements.map((element) =>
|
||||||
|
element.id === elementId
|
||||||
|
? { ...element, hidden: !element.hidden }
|
||||||
|
: element
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: track
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
updateTextElement: (trackId, elementId, updates) => {
|
updateTextElement: (trackId, elementId, updates) => {
|
||||||
get().pushHistory();
|
get().pushHistory();
|
||||||
updateTracksAndSave(
|
updateTracksAndSave(
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ export interface TProject {
|
|||||||
backgroundType?: "color" | "blur";
|
backgroundType?: "color" | "blur";
|
||||||
blurIntensity?: number; // in pixels (4, 8, 18)
|
blurIntensity?: number; // in pixels (4, 8, 18)
|
||||||
fps?: number;
|
fps?: number;
|
||||||
|
bookmarks?: number[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface BaseTimelineElement {
|
|||||||
startTime: number;
|
startTime: number;
|
||||||
trimStart: number;
|
trimStart: number;
|
||||||
trimEnd: number;
|
trimEnd: number;
|
||||||
|
hidden?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Media element that references MediaStore
|
// Media element that references MediaStore
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
"input-otp": "^1.4.1",
|
"input-otp": "^1.4.1",
|
||||||
"lucide-react": "^0.468.0",
|
"lucide-react": "^0.468.0",
|
||||||
"motion": "^12.18.1",
|
"motion": "^12.18.1",
|
||||||
"next": "^15.4.3",
|
"next": "^15.4.5",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"pg": "^8.16.2",
|
"pg": "^8.16.2",
|
||||||
"radix-ui": "^1.4.2",
|
"radix-ui": "^1.4.2",
|
||||||
@@ -1314,7 +1314,7 @@
|
|||||||
|
|
||||||
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||||
|
|
||||||
"opencut/next": ["next@15.4.3", "", { "dependencies": { "@next/env": "15.4.3", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.3", "@next/swc-darwin-x64": "15.4.3", "@next/swc-linux-arm64-gnu": "15.4.3", "@next/swc-linux-arm64-musl": "15.4.3", "@next/swc-linux-x64-gnu": "15.4.3", "@next/swc-linux-x64-musl": "15.4.3", "@next/swc-win32-arm64-msvc": "15.4.3", "@next/swc-win32-x64-msvc": "15.4.3", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-uW7Qe6poVasNIE1X382nI29oxSdFJzjQzTgJFLD43MxyPfGKKxCMySllhBpvqr48f58Om+tLMivzRwBpXEytvA=="],
|
"opencut/next": ["next@15.4.5", "", { "dependencies": { "@next/env": "15.4.5", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.5", "@next/swc-darwin-x64": "15.4.5", "@next/swc-linux-arm64-gnu": "15.4.5", "@next/swc-linux-arm64-musl": "15.4.5", "@next/swc-linux-x64-gnu": "15.4.5", "@next/swc-linux-x64-musl": "15.4.5", "@next/swc-win32-arm64-msvc": "15.4.5", "@next/swc-win32-x64-msvc": "15.4.5", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-nJ4v+IO9CPmbmcvsPebIoX3Q+S7f6Fu08/dEWu0Ttfa+wVwQRh9epcmsyCPjmL2b8MxC+CkBR97jgDhUUztI3g=="],
|
||||||
|
|
||||||
"opencut/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"opencut/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
@@ -1392,23 +1392,23 @@
|
|||||||
|
|
||||||
"motion/framer-motion/motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="],
|
"motion/framer-motion/motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="],
|
||||||
|
|
||||||
"opencut/next/@next/env": ["@next/env@15.4.3", "", {}, "sha512-lKJ9KJAvaWzqurIsz6NWdQOLj96mdhuDMusLSYHw9HBe2On7BjUwU1WeRvq19x7NrEK3iOgMeSBV5qEhVH1cMw=="],
|
"opencut/next/@next/env": ["@next/env@15.4.5", "", {}, "sha512-ruM+q2SCOVCepUiERoxOmZY9ZVoecR3gcXNwCYZRvQQWRjhOiPJGmQ2fAiLR6YKWXcSAh7G79KEFxN3rwhs4LQ=="],
|
||||||
|
|
||||||
"opencut/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YAhZWKeEYY7LHQJiQ8fe3Y6ymfcDcTn7rDC8PDu/pdeIl1Z2LHD4uyPNuQUGCEQT//MSNv6oZCeQzZfTCKZv+A=="],
|
"opencut/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-84dAN4fkfdC7nX6udDLz9GzQlMUwEMKD7zsseXrl7FTeIItF8vpk1lhLEnsotiiDt+QFu3O1FVWnqwcRD2U3KA=="],
|
||||||
|
|
||||||
"opencut/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZPHRdd51xaxCMpT4viQ6h8TgYM1zPW1JIeksPY9wKlyvBVUQqrWqw8kEh1sa7/x0Ied+U7pYHkAkutrUwxbMcg=="],
|
"opencut/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-CL6mfGsKuFSyQjx36p2ftwMNSb8PQog8y0HO/ONLdQqDql7x3aJb/wB+LA651r4we2pp/Ck+qoRVUeZZEvSurA=="],
|
||||||
|
|
||||||
"opencut/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-QUdqftCXC5vw5cowucqi9FeOPQ0vdMxoOHLY0J5jPdercwSJFjdi9CkEO4Xkq1eG4t1TB/BG81n6rmTsWoILnw=="],
|
"opencut/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-1hTVd9n6jpM/thnDc5kYHD1OjjWYpUJrJxY4DlEacT7L5SEOXIifIdTye6SQNNn8JDZrcN+n8AWOmeJ8u3KlvQ=="],
|
||||||
|
|
||||||
"opencut/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-HTL31NsmoafX+r5g91Yj3+q34nrn1xKmCWVuNA+fUWO4X0pr+n83uGzLyEOn0kUqbMZ40KmWx+4wsbMoUChkiQ=="],
|
"opencut/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-4W+D/nw3RpIwGrqpFi7greZ0hjrCaioGErI7XHgkcTeWdZd146NNu1s4HnaHonLeNTguKnL2Urqvj28UJj6Gqw=="],
|
||||||
|
|
||||||
"opencut/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-HRQLWoeFkKXd2YCEEy9GhfwOijRm37x4w5r0MMVHxBKSA6ms3JoPUXvGhfHT6srnGRcEUWNrQ2vzkHir5ZWTSw=="],
|
"opencut/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-N6Mgdxe/Cn2K1yMHge6pclffkxzbSGOydXVKYOjYqQXZYjLCfN/CuFkaYDeDHY2VBwSHyM2fUjYBiQCIlxIKDA=="],
|
||||||
|
|
||||||
"opencut/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-NyXUx6G7AayaRGUsVPenuwhyAoyxjQuQPaK50AXoaAHPwRuif4WmSrXUs8/Y0HJIZh8E/YXRm9H7uuGfiacpuQ=="],
|
"opencut/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-YZ3bNDrS8v5KiqgWE0xZQgtXgCTUacgFtnEgI4ccotAASwSvcMPDLua7BWLuTfucoRv6mPidXkITJLd8IdJplQ=="],
|
||||||
|
|
||||||
"opencut/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-2CUTmpzN/7cL1a7GjdLkDFlfH3nwMwW8a6JiaAUsL9MtKmNNO3fnXqnY0Zk30fii3hVEl4dr7ztrpYt0t2CcGQ=="],
|
"opencut/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-9Wr4t9GkZmMNcTVvSloFtjzbH4vtT4a8+UHqDoVnxA5QyfWe6c5flTH1BIWPGNWSUlofc8dVJAE7j84FQgskvQ=="],
|
||||||
|
|
||||||
"opencut/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.3", "", { "os": "win32", "cpu": "x64" }, "sha512-i54YgUhvrUQxQD84SjAbkfWhYkOdm/DNRAVekCHLWxVg3aUbyC6NFQn9TwgCkX5QAS2pXCJo3kFboSFvrsd7dA=="],
|
"opencut/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.5", "", { "os": "win32", "cpu": "x64" }, "sha512-voWk7XtGvlsP+w8VBz7lqp8Y+dYw/MTI4KeS0gTVtfdhdJ5QwhXLmNrndFOin/MDoCvUaLWMkYKATaCoUkt2/A=="],
|
||||||
|
|
||||||
"opencut/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
"opencut/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user