mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
lots more icons to do but
This commit is contained in:
@@ -21,6 +21,8 @@
|
||||
"@ffmpeg/util": "^0.12.2",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@hugeicons/core-free-icons": "^3.1.1",
|
||||
"@hugeicons/react": "^1.1.4",
|
||||
"@huggingface/transformers": "^3.8.1",
|
||||
"@opencut/env": "workspace:*",
|
||||
"@opencut/ui": "workspace:*",
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { OcGithubIcon } from "@opencut/ui/icons";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { GitHubContributeSection } from "@/components/gitHub-contribute-section";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { EXTERNAL_TOOLS, SOCIAL_LINKS } from "@/constants/site-constants";
|
||||
import { EXTERNAL_TOOLS } from "@/constants/site-constants";
|
||||
import { BasePage } from "../base-page";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -87,7 +84,6 @@ export default async function ContributorsPage() {
|
||||
{otherContributors.length > 0 && (
|
||||
<AllContributorsSection contributors={otherContributors} />
|
||||
)}
|
||||
{contributors.length === 0 && <EmptyState />}
|
||||
<ExternalToolsSection />
|
||||
<GitHubContributeSection
|
||||
title="Join the community"
|
||||
@@ -210,36 +206,6 @@ function AllContributorsSection({
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyState() {
|
||||
return (
|
||||
<div className="flex flex-col gap-8 py-20 text-center">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="bg-muted/50 mx-auto flex size-20 items-center justify-center rounded-full">
|
||||
<OcGithubIcon className="text-muted-foreground size-8" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<h3 className="text-xl font-medium">No contributors found</h3>
|
||||
<p className="text-muted-foreground mx-auto max-w-md">
|
||||
Unable to load contributors at the moment. Check back later or view
|
||||
on GitHub.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href={`${SOCIAL_LINKS.github}/graphs/contributors`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button variant="outline" className="gap-2">
|
||||
<OcGithubIcon />
|
||||
View on GitHub
|
||||
<ExternalLink />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ExternalToolsSection() {
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ArrowDown01,
|
||||
Calendar,
|
||||
ChevronLeft,
|
||||
MoreHorizontal,
|
||||
Plus,
|
||||
Search,
|
||||
Trash2,
|
||||
Video,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -41,6 +30,18 @@ import {
|
||||
import { useEditor } from "@/hooks/use-editor";
|
||||
import type { TProjectMetadata } from "@/types/project";
|
||||
import { formatDate } from "@/utils/date";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import {
|
||||
ArrowHorizontalIcon,
|
||||
ArrowLeft01Icon,
|
||||
Calendar04Icon,
|
||||
Delete02Icon,
|
||||
MultiplicationSignIcon,
|
||||
PlusSignIcon,
|
||||
Search01Icon,
|
||||
SortingNineOneIcon,
|
||||
Video01Icon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
|
||||
export default function ProjectsPage() {
|
||||
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
||||
@@ -160,7 +161,7 @@ export default function ProjectsPage() {
|
||||
href="/"
|
||||
className="hover:text-muted-foreground flex items-center gap-1 transition-colors"
|
||||
>
|
||||
<ChevronLeft className="size-5! shrink-0" />
|
||||
<HugeiconsIcon icon={ArrowLeft01Icon} className="size-5! shrink-0" />
|
||||
<span className="text-sm font-medium">Back</span>
|
||||
</Link>
|
||||
<div className="block md:hidden">
|
||||
@@ -171,7 +172,7 @@ export default function ProjectsPage() {
|
||||
size="sm"
|
||||
onClick={handleCancelSelection}
|
||||
>
|
||||
<X />
|
||||
<HugeiconsIcon icon={MultiplicationSignIcon} />
|
||||
Cancel
|
||||
</Button>
|
||||
{selectedProjects.size > 0 && (
|
||||
@@ -180,7 +181,7 @@ export default function ProjectsPage() {
|
||||
size="sm"
|
||||
onClick={() => setIsBulkDeleteDialogOpen(true)}
|
||||
>
|
||||
<Trash2 />
|
||||
<HugeiconsIcon icon={Delete02Icon} />
|
||||
Delete ({selectedProjects.size})
|
||||
</Button>
|
||||
)}
|
||||
@@ -210,7 +211,7 @@ export default function ProjectsPage() {
|
||||
{isSelectionMode ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={handleCancelSelection}>
|
||||
<X />
|
||||
<HugeiconsIcon icon={MultiplicationSignIcon} />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
@@ -218,7 +219,7 @@ export default function ProjectsPage() {
|
||||
disabled={selectedProjects.size === 0}
|
||||
onClick={() => setIsBulkDeleteDialogOpen(true)}
|
||||
>
|
||||
<Trash2 />
|
||||
<HugeiconsIcon icon={Delete02Icon} />
|
||||
Delete {selectedProjects.size} projects
|
||||
</Button>
|
||||
</div>
|
||||
@@ -257,9 +258,8 @@ export default function ProjectsPage() {
|
||||
variant="outline"
|
||||
className="size-9 items-center justify-center"
|
||||
>
|
||||
<ArrowDown01
|
||||
strokeWidth={1.5}
|
||||
className="!size-[1.05rem]"
|
||||
<HugeiconsIcon
|
||||
icon={SortingNineOneIcon}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Button>
|
||||
@@ -430,7 +430,10 @@ function ProjectCard({
|
||||
/>
|
||||
) : (
|
||||
<div className="bg-muted/50 flex size-full items-center justify-center">
|
||||
<Video className="text-muted-foreground size-12 shrink-0" />
|
||||
<HugeiconsIcon
|
||||
icon={Video01Icon}
|
||||
className="text-muted-foreground size-12 shrink-0"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -458,7 +461,10 @@ function ProjectCard({
|
||||
}`}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
<MoreHorizontal aria-hidden="true" />
|
||||
<HugeiconsIcon
|
||||
icon={ArrowHorizontalIcon}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
@@ -506,7 +512,7 @@ function ProjectCard({
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-muted-foreground flex items-center gap-1.5 text-sm">
|
||||
<Calendar className="size-4!" />
|
||||
<HugeiconsIcon icon={Calendar04Icon} className="size-4" />
|
||||
<span>Created {formatDate({ date: project.createdAt })}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -575,7 +581,7 @@ function ProjectsLoader() {
|
||||
function CreateButton({ onClick }: { onClick?: () => void }) {
|
||||
return (
|
||||
<Button className="flex" onClick={onClick}>
|
||||
<Plus />
|
||||
<HugeiconsIcon icon={PlusSignIcon} />
|
||||
<span className="text-sm font-medium">New project</span>
|
||||
</Button>
|
||||
);
|
||||
@@ -596,7 +602,10 @@ function EmptyState({
|
||||
<div className="flex flex-col items-center justify-center gap-6 py-16 text-center">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="bg-muted/30 flex size-16 items-center justify-center rounded-full">
|
||||
<Search className="text-muted-foreground size-8" />
|
||||
<HugeiconsIcon
|
||||
icon={Search01Icon}
|
||||
className="text-muted-foreground size-8"
|
||||
/>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium">No results found</h3>
|
||||
<p className="text-muted-foreground max-w-md">
|
||||
@@ -614,7 +623,10 @@ function EmptyState({
|
||||
<div className="flex flex-col items-center justify-center gap-6 py-16 text-center">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="bg-muted/30 flex size-16 items-center justify-center rounded-full">
|
||||
<Video className="text-muted-foreground size-8" />
|
||||
<HugeiconsIcon
|
||||
icon={Video01Icon}
|
||||
className="text-muted-foreground size-8"
|
||||
/>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium">No projects yet</h3>
|
||||
<p className="text-muted-foreground max-w-md">
|
||||
@@ -623,7 +635,7 @@ function EmptyState({
|
||||
</p>
|
||||
</div>
|
||||
<Button size="lg" className="gap-2" onClick={onCreateProject}>
|
||||
<Plus />
|
||||
<HugeiconsIcon icon={PlusSignIcon} />
|
||||
Create your first project
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ArrowDown,
|
||||
Clock3,
|
||||
Folder,
|
||||
LayoutGrid,
|
||||
List,
|
||||
MoreHorizontal,
|
||||
Plus,
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import {
|
||||
ArrowHorizontalIcon,
|
||||
Clock03Icon,
|
||||
Folder03Icon,
|
||||
GridViewIcon,
|
||||
LeftToRightListDashIcon,
|
||||
PlusSignIcon,
|
||||
SearchIcon,
|
||||
SortingNineOneIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
|
||||
const projects = [
|
||||
{
|
||||
@@ -110,7 +111,7 @@ export default function ProjectsPage() {
|
||||
href="/projects"
|
||||
className="flex items-center gap-3 rounded-2xl bg-blue-50/50 px-4 py-3 text-sm font-medium text-[#00A3FF]"
|
||||
>
|
||||
<LayoutGrid className="size-5" />
|
||||
<HugeiconsIcon icon={GridViewIcon} className="size-5" />
|
||||
Projects
|
||||
</a>
|
||||
<div className="px-4 pt-6 pb-2">
|
||||
@@ -122,14 +123,20 @@ export default function ProjectsPage() {
|
||||
href="/projects"
|
||||
className="flex items-center gap-3 rounded-2xl px-4 py-3 text-sm font-medium text-slate-500 hover:bg-slate-50 hover:text-slate-900"
|
||||
>
|
||||
<Folder className="size-5 text-slate-400" />
|
||||
<HugeiconsIcon
|
||||
icon={Folder03Icon}
|
||||
className="size-5 text-slate-400"
|
||||
/>
|
||||
Marketing
|
||||
</a>
|
||||
<a
|
||||
href="/projects"
|
||||
className="flex items-center gap-3 rounded-2xl px-4 py-3 text-sm font-medium text-slate-500 hover:bg-slate-50 hover:text-slate-900"
|
||||
>
|
||||
<Folder className="size-5 text-slate-400" />
|
||||
<HugeiconsIcon
|
||||
icon={Folder03Icon}
|
||||
className="size-5 text-slate-400"
|
||||
/>
|
||||
Social media
|
||||
</a>
|
||||
</nav>
|
||||
@@ -143,7 +150,10 @@ export default function ProjectsPage() {
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="group relative">
|
||||
<Search className="absolute top-1/2 left-3.5 size-4 -translate-y-1/2 text-slate-400" />
|
||||
<HugeiconsIcon
|
||||
icon={SearchIcon}
|
||||
className="absolute top-1/2 left-3.5 size-4 -translate-y-1/2 text-slate-400"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
@@ -157,18 +167,22 @@ export default function ProjectsPage() {
|
||||
className="rounded-full bg-slate-100 p-2 text-slate-900"
|
||||
aria-label="Grid view"
|
||||
>
|
||||
<LayoutGrid className="size-4" aria-hidden="true">
|
||||
<title>Grid view</title>
|
||||
</LayoutGrid>
|
||||
<HugeiconsIcon
|
||||
icon={GridViewIcon}
|
||||
className="size-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full p-2 text-slate-400 hover:text-slate-600"
|
||||
aria-label="List view"
|
||||
>
|
||||
<List className="size-4" aria-hidden="true">
|
||||
<title>List view</title>
|
||||
</List>
|
||||
<HugeiconsIcon
|
||||
icon={LeftToRightListDashIcon}
|
||||
className="size-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -176,7 +190,7 @@ export default function ProjectsPage() {
|
||||
type="button"
|
||||
className="flex items-center gap-2 rounded-full bg-[#00A3FF] px-5 py-2.5 text-sm font-semibold text-white hover:bg-[#008BE0] active:scale-95"
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
<HugeiconsIcon icon={PlusSignIcon} className="size-4" />
|
||||
New project
|
||||
</button>
|
||||
</div>
|
||||
@@ -210,7 +224,7 @@ export default function ProjectsPage() {
|
||||
className="inline-flex items-center gap-1.5 rounded-lg p-1 text-sm font-medium text-slate-500 hover:text-slate-900"
|
||||
>
|
||||
Name
|
||||
<ArrowDown className="size-3.5" />
|
||||
<HugeiconsIcon icon={SortingNineOneIcon} className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -243,9 +257,11 @@ export default function ProjectsPage() {
|
||||
className="rounded-full bg-white/90 p-2 text-slate-600 backdrop-blur-sm hover:bg-white hover:text-[#00A3FF]"
|
||||
aria-label="Project menu"
|
||||
>
|
||||
<MoreHorizontal className="size-4" aria-hidden="true">
|
||||
<title>Project menu</title>
|
||||
</MoreHorizontal>
|
||||
<HugeiconsIcon
|
||||
icon={ArrowHorizontalIcon}
|
||||
className="size-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -268,7 +284,11 @@ export default function ProjectsPage() {
|
||||
{name}
|
||||
</h3>
|
||||
<div className="mt-2 flex items-center gap-2 text-slate-400">
|
||||
<Clock3 className="size-4" aria-hidden="true" />
|
||||
<HugeiconsIcon
|
||||
icon={Clock03Icon}
|
||||
className="size-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p className="text-xs font-medium">{createdAtLabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -281,42 +301,33 @@ export default function ProjectsPage() {
|
||||
</main>
|
||||
|
||||
<style jsx global>{`
|
||||
.project-card:hover .card-actions {
|
||||
.project-card:hover .card-actions
|
||||
opacity: 1;
|
||||
}
|
||||
.checkbox-wrapper:checked + div {
|
||||
.checkbox-wrapper:checked + div
|
||||
border-color: var(--brand-blue);
|
||||
background-color: #eff6ff;
|
||||
}
|
||||
.controls-row.has-selection .controls-select {
|
||||
.controls-row.has-selection .controls-select
|
||||
opacity: 1;
|
||||
}
|
||||
.project-select-wrapper {
|
||||
.project-select-wrapper
|
||||
opacity: 0;
|
||||
}
|
||||
.project-card:hover .project-select-wrapper {
|
||||
.project-card:hover .project-select-wrapper
|
||||
opacity: 1;
|
||||
}
|
||||
.project-card:has(.project-select:checked) .project-select-wrapper {
|
||||
.project-card:has(.project-select:checked) .project-select-wrapper
|
||||
opacity: 1;
|
||||
}
|
||||
.multi-select .project-menu {
|
||||
.multi-select .project-menu
|
||||
opacity: 0 !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
.custom-checkbox {
|
||||
.custom-checkbox
|
||||
appearance: none;
|
||||
background-color: #fff;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
display: inline-grid;
|
||||
place-content: center;
|
||||
}
|
||||
.custom-checkbox:checked {
|
||||
.custom-checkbox:checked
|
||||
background-color: var(--brand-blue);
|
||||
border-color: var(--brand-blue);
|
||||
}
|
||||
.custom-checkbox:checked::after {
|
||||
.custom-checkbox:checked::after
|
||||
content: "";
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
@@ -329,7 +340,6 @@ export default function ProjectsPage() {
|
||||
86% 10%,
|
||||
40% 70%
|
||||
);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { BasePage } from "@/app/base-page";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { SPONSORS, type Sponsor } from "@/constants/site-constants";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { LinkSquare02Icon } from "@hugeicons/core-free-icons";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Sponsors - OpenCut",
|
||||
@@ -25,9 +26,9 @@ export default function SponsorsPage() {
|
||||
<h1 className="text-5xl font-bold tracking-tight md:text-6xl">
|
||||
Sponsors
|
||||
</h1>
|
||||
<p className="text-muted-foreground mx-auto max-w-2xl text-xl leading-relaxed">
|
||||
Support OpenCut and help us build the future of free and open-source
|
||||
video editing.
|
||||
<p className="text-muted-foreground mx-auto max-w-2xl text-xl leading-relaxed text-pretty">
|
||||
Support OpenCut and help us build the future of privacy-first video
|
||||
editing.
|
||||
</p>
|
||||
</div>
|
||||
<SponsorsGrid />
|
||||
@@ -51,7 +52,7 @@ function SponsorCard({ sponsor }: { sponsor: Sponsor }) {
|
||||
href={sponsor.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="size-full"
|
||||
className="size-full"
|
||||
>
|
||||
<Card className="h-full">
|
||||
<CardContent className="flex h-full flex-col justify-center gap-8 p-8">
|
||||
@@ -67,7 +68,10 @@ function SponsorCard({ sponsor }: { sponsor: Sponsor }) {
|
||||
<h3 className="text-xl font-semibold group-hover:underline">
|
||||
{sponsor.name}
|
||||
</h3>
|
||||
<ExternalLink className="text-muted-foreground size-4" />
|
||||
<HugeiconsIcon
|
||||
icon={LinkSquare02Icon}
|
||||
className="text-muted-foreground size-4"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-muted-foreground">{sponsor.description}</p>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Upload } from "lucide-react";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { UploadIcon } from "@hugeicons/core-free-icons";
|
||||
|
||||
interface MediaDragOverlayProps {
|
||||
isVisible: boolean;
|
||||
@@ -34,7 +35,7 @@ export function MediaDragOverlay({
|
||||
onClick={(event) => handleClick({ event })}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<Upload className="text-foreground size-10" />
|
||||
<HugeiconsIcon icon={UploadIcon} className="text-foreground size-10" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -59,7 +59,7 @@ export function TabBar() {
|
||||
type="button"
|
||||
aria-label={tab.label}
|
||||
className={cn(
|
||||
"flex cursor-pointer flex-col items-center gap-0.5",
|
||||
"flex cursor-pointer flex-col items-center gap-0.5 [&>svg]:size-4.5! opacity-100 hover:opacity-75",
|
||||
activeTab === tabKey
|
||||
? "text-primary !opacity-100"
|
||||
: "text-muted-foreground",
|
||||
@@ -72,7 +72,7 @@ export function TabBar() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<tab.icon className="size-[1.1rem]! opacity-100 hover:opacity-75" />
|
||||
<tab.icon className=" " />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
|
||||
@@ -13,7 +13,8 @@ import { extractTimelineAudio } from "@/lib/media/mediabunny";
|
||||
import { useEditor } from "@/hooks/use-editor";
|
||||
import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants";
|
||||
import { TRANSCRIPTION_LANGUAGES } from "@/constants/transcription-constants";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { Loading03Icon } from "@hugeicons/core-free-icons";
|
||||
import type {
|
||||
TranscriptionLanguage,
|
||||
TranscriptionProgress,
|
||||
@@ -21,6 +22,7 @@ import type {
|
||||
import { transcriptionService } from "@/services/transcription";
|
||||
import { decodeAudioToFloat32 } from "@/lib/media/audio";
|
||||
import { buildCaptionChunks } from "@/lib/transcription/caption";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
|
||||
export function Captions() {
|
||||
const [selectedLanguage, setSelectedLanguage] =
|
||||
@@ -143,7 +145,7 @@ export function Captions() {
|
||||
onClick={handleGenerateTranscript}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{isProcessing && <Loader2 className="mr-1 animate-spin" />}
|
||||
{isProcessing && <Spinner className="mr-1" />}
|
||||
{isProcessing ? processingStep : "Generate transcript"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
ArrowDown01,
|
||||
CloudUpload,
|
||||
Grid2X2,
|
||||
ImageIcon,
|
||||
List,
|
||||
Loader2,
|
||||
Music,
|
||||
Video,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -44,6 +33,17 @@ import { useAssetsPanelStore } from "@/stores/assets-panel-store";
|
||||
import type { MediaAsset } from "@/types/assets";
|
||||
import type { CreateTimelineElement } from "@/types/timeline";
|
||||
import { cn } from "@/utils/ui";
|
||||
import {
|
||||
CloudUploadIcon,
|
||||
GridViewIcon,
|
||||
LeftToRightListDashIcon,
|
||||
SortingOneNineIcon,
|
||||
Image02Icon,
|
||||
Loading03Icon,
|
||||
MusicNote03Icon,
|
||||
Video01Icon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react";
|
||||
|
||||
export function MediaView() {
|
||||
const editor = useEditor();
|
||||
@@ -207,9 +207,9 @@ export function MediaView() {
|
||||
className="w-full"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
<HugeiconsIcon icon={Loading03Icon} className="animate-spin" />
|
||||
) : (
|
||||
<CloudUpload />
|
||||
<HugeiconsIcon icon={CloudUploadIcon} />
|
||||
)}
|
||||
<span>Upload</span>
|
||||
</Button>
|
||||
@@ -229,12 +229,9 @@ export function MediaView() {
|
||||
className="items-center justify-center"
|
||||
>
|
||||
{mediaViewMode === "grid" ? (
|
||||
<List strokeWidth={1.5} className="!size-[1.05rem]" />
|
||||
<HugeiconsIcon icon={LeftToRightListDashIcon} />
|
||||
) : (
|
||||
<Grid2X2
|
||||
strokeWidth={1.5}
|
||||
className="!size-[1.05rem]"
|
||||
/>
|
||||
<HugeiconsIcon icon={GridViewIcon} />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -255,10 +252,7 @@ export function MediaView() {
|
||||
disabled={isProcessing}
|
||||
className="items-center justify-center"
|
||||
>
|
||||
<ArrowDown01
|
||||
strokeWidth={1.5}
|
||||
className="!size-[1.05rem]"
|
||||
/>
|
||||
<HugeiconsIcon icon={SortingOneNineIcon} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
@@ -531,12 +525,12 @@ function MediaDurationLabel({ duration }: { duration?: number }) {
|
||||
}
|
||||
|
||||
function MediaTypePlaceholder({
|
||||
icon: Icon,
|
||||
icon,
|
||||
label,
|
||||
duration,
|
||||
variant,
|
||||
}: {
|
||||
icon: LucideIcon;
|
||||
icon: IconSvgElement;
|
||||
label: string;
|
||||
duration?: number;
|
||||
variant: "muted" | "bordered";
|
||||
@@ -550,7 +544,7 @@ function MediaTypePlaceholder({
|
||||
variant === "muted" ? "bg-muted/30" : "border",
|
||||
)}
|
||||
>
|
||||
<Icon className={iconClassName} aria-hidden="true" />
|
||||
<HugeiconsIcon icon={icon} className={iconClassName} />
|
||||
<span className="text-xs">{label}</span>
|
||||
<MediaDurationLabel duration={duration} />
|
||||
</div>
|
||||
@@ -596,7 +590,10 @@ function MediaPreview({
|
||||
/>
|
||||
{shouldShowVideoOverlay ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded bg-black/20">
|
||||
<Video className="size-6 text-white drop-shadow-md" />
|
||||
<HugeiconsIcon
|
||||
icon={Video01Icon}
|
||||
className="size-6 text-white drop-shadow-md"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{shouldShowDurationBadge ? (
|
||||
@@ -608,7 +605,7 @@ function MediaPreview({
|
||||
|
||||
return (
|
||||
<MediaTypePlaceholder
|
||||
icon={Video}
|
||||
icon={Video01Icon}
|
||||
label="Video"
|
||||
duration={item.duration}
|
||||
variant="muted"
|
||||
@@ -619,7 +616,7 @@ function MediaPreview({
|
||||
if (item.type === "audio") {
|
||||
return (
|
||||
<MediaTypePlaceholder
|
||||
icon={Music}
|
||||
icon={MusicNote03Icon}
|
||||
label="Audio"
|
||||
duration={item.duration}
|
||||
variant="bordered"
|
||||
@@ -628,7 +625,7 @@ function MediaPreview({
|
||||
}
|
||||
|
||||
return (
|
||||
<MediaTypePlaceholder icon={ImageIcon} label="Unknown" variant="muted" />
|
||||
<MediaTypePlaceholder icon={Image02Icon} label="Unknown" variant="muted" />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { PipetteIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { memo, useCallback, useMemo } from "react";
|
||||
import { PanelBaseView as BaseView } from "@/components/editor/panel-base-view";
|
||||
@@ -31,6 +30,8 @@ import {
|
||||
PropertyItemLabel,
|
||||
PropertyItemValue,
|
||||
} from "../../properties-panel/property-item";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { DropperIcon } from "@hugeicons/core-free-icons";
|
||||
|
||||
export function SettingsView() {
|
||||
return <ProjectSettingsTabs />;
|
||||
@@ -355,7 +356,7 @@ function BackgroundView() {
|
||||
<PropertyGroup title="Colors" defaultExpanded={false}>
|
||||
<div className="grid w-full grid-cols-4 gap-2">
|
||||
<div className="border-foreground/15 hover:border-primary flex aspect-square w-full cursor-pointer items-center justify-center rounded-sm border">
|
||||
<PipetteIcon className="size-4" />
|
||||
<HugeiconsIcon icon={DropperIcon} className="size-4" />
|
||||
</div>
|
||||
<BackgroundPreviews
|
||||
backgrounds={colors}
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
HeartIcon,
|
||||
ListFilter,
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
PlusIcon,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -33,6 +26,14 @@ import { useSoundSearch } from "@/hooks/use-sound-search";
|
||||
import { useSoundsStore } from "@/stores/sounds-store";
|
||||
import type { SavedSound, SoundEffect } from "@/types/sounds";
|
||||
import { cn } from "@/utils/ui";
|
||||
import {
|
||||
FavouriteIcon,
|
||||
FilterMailIcon,
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
PlusSignIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
|
||||
export function SoundsView() {
|
||||
return (
|
||||
@@ -250,7 +251,7 @@ function SoundEffectsView() {
|
||||
size="icon"
|
||||
className={cn(showCommercialOnly && "text-primary")}
|
||||
>
|
||||
<ListFilter />
|
||||
<HugeiconsIcon icon={FilterMailIcon} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
@@ -406,9 +407,9 @@ function SavedSoundsView() {
|
||||
if (savedSounds.length === 0) {
|
||||
return (
|
||||
<div className="bg-panel flex h-full flex-col items-center justify-center gap-3 p-4">
|
||||
<HeartIcon
|
||||
<HugeiconsIcon
|
||||
icon={FavouriteIcon}
|
||||
className="text-muted-foreground size-10"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<div className="flex flex-col gap-2 text-center">
|
||||
<p className="text-lg font-medium">No saved sounds</p>
|
||||
@@ -527,9 +528,9 @@ function AudioItem({ sound, isPlaying, onPlay }: AudioItemProps) {
|
||||
<div className="bg-accent relative flex size-12 shrink-0 items-center justify-center overflow-hidden rounded-md">
|
||||
<div className="from-primary/20 absolute inset-0 bg-gradient-to-br to-transparent" />
|
||||
{isPlaying ? (
|
||||
<PauseIcon className="size-5" />
|
||||
<HugeiconsIcon icon={PauseIcon} className="size-5" />
|
||||
) : (
|
||||
<PlayIcon className="size-5" />
|
||||
<HugeiconsIcon icon={PlayIcon} className="size-5" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -549,7 +550,7 @@ function AudioItem({ sound, isPlaying, onPlay }: AudioItemProps) {
|
||||
onClick={handleAddToTimeline}
|
||||
title="Add to timeline"
|
||||
>
|
||||
<PlusIcon />
|
||||
<HugeiconsIcon icon={PlusSignIcon} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="text"
|
||||
@@ -562,7 +563,10 @@ function AudioItem({ sound, isPlaying, onPlay }: AudioItemProps) {
|
||||
onClick={handleSaveClick}
|
||||
title={isSaved ? "Remove from saved" : "Save sound"}
|
||||
>
|
||||
<HeartIcon className={`${isSaved ? "fill-current" : ""}`} />
|
||||
<HugeiconsIcon
|
||||
icon={FavouriteIcon}
|
||||
className={`${isSaved ? "fill-current" : ""}`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ArrowRight,
|
||||
Clock,
|
||||
Grid3X3,
|
||||
Hash,
|
||||
Loader2,
|
||||
Smile,
|
||||
Sparkles,
|
||||
StickerIcon,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import type { CSSProperties } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
@@ -37,6 +26,17 @@ import {
|
||||
import { useStickersStore } from "@/stores/stickers-store";
|
||||
import type { StickerCategory } from "@/types/stickers";
|
||||
import { cn } from "@/utils/ui";
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
HappyIcon,
|
||||
ClockIcon,
|
||||
LayoutGridIcon,
|
||||
MultiplicationSignIcon,
|
||||
SparklesIcon,
|
||||
HashtagIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
|
||||
function isStickerCategory(value: string): value is StickerCategory {
|
||||
return STICKER_CATEGORIES.includes(value as StickerCategory);
|
||||
@@ -57,25 +57,25 @@ export function StickersView() {
|
||||
{
|
||||
value: "all",
|
||||
label: "All",
|
||||
icon: <Grid3X3 className="size-3" />,
|
||||
icon: <HugeiconsIcon icon={LayoutGridIcon} className="size-3" />,
|
||||
content: <StickersContentView category="all" />,
|
||||
},
|
||||
{
|
||||
value: "general",
|
||||
label: "Icons",
|
||||
icon: <Sparkles className="size-3" />,
|
||||
icon: <HugeiconsIcon icon={SparklesIcon} className="size-3" />,
|
||||
content: <StickersContentView category="general" />,
|
||||
},
|
||||
{
|
||||
value: "brands",
|
||||
label: "Brands",
|
||||
icon: <Hash className="size-3" />,
|
||||
icon: <HugeiconsIcon icon={HashtagIcon} className="size-3" />,
|
||||
content: <StickersContentView category="brands" />,
|
||||
},
|
||||
{
|
||||
value: "emoji",
|
||||
label: "Emoji",
|
||||
icon: <Smile className="size-3" />,
|
||||
icon: <HugeiconsIcon icon={HappyIcon} className="size-3" />,
|
||||
content: <StickersContentView category="emoji" />,
|
||||
},
|
||||
]}
|
||||
@@ -150,9 +150,9 @@ function CollectionGrid({
|
||||
function EmptyView({ message }: { message: string }) {
|
||||
return (
|
||||
<div className="bg-panel flex h-full flex-col items-center justify-center gap-3 p-4">
|
||||
<StickerIcon
|
||||
<HugeiconsIcon
|
||||
icon={HappyIcon}
|
||||
className="text-muted-foreground size-10"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<div className="flex flex-col gap-2 text-center">
|
||||
<p className="text-lg font-medium">No stickers found</p>
|
||||
@@ -334,7 +334,10 @@ function StickersContentView({ category }: { category: StickerCategory }) {
|
||||
{recentStickers.length > 0 && viewMode === "browse" && (
|
||||
<div className="h-full">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Clock className="text-muted-foreground size-4" />
|
||||
<HugeiconsIcon
|
||||
icon={ClockIcon}
|
||||
className="text-muted-foreground size-4"
|
||||
/>
|
||||
<span className="text-sm font-medium">Recent</span>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
@@ -344,7 +347,10 @@ function StickersContentView({ category }: { category: StickerCategory }) {
|
||||
onClick={clearRecentStickers}
|
||||
className="hover:bg-accent ml-auto flex size-5 items-center justify-center rounded p-0"
|
||||
>
|
||||
<X className="text-muted-foreground size-3" />
|
||||
<HugeiconsIcon
|
||||
icon={MultiplicationSignIcon}
|
||||
className="text-muted-foreground size-3"
|
||||
/>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@@ -366,7 +372,7 @@ function StickersContentView({ category }: { category: StickerCategory }) {
|
||||
<div className="h-full">
|
||||
{isLoadingCollection ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="text-muted-foreground size-6 animate-spin" />
|
||||
<Spinner className="text-muted-foreground size-6" />
|
||||
</div>
|
||||
) : showCollectionItems ? (
|
||||
<StickerGrid
|
||||
@@ -376,7 +382,7 @@ function StickersContentView({ category }: { category: StickerCategory }) {
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="text-muted-foreground size-6 animate-spin" />
|
||||
<Spinner className="text-muted-foreground size-6" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -386,7 +392,7 @@ function StickersContentView({ category }: { category: StickerCategory }) {
|
||||
<div className="h-full">
|
||||
{isSearching ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="text-muted-foreground size-6 animate-spin" />
|
||||
<Spinner className="text-muted-foreground size-6" />
|
||||
</div>
|
||||
) : searchResults?.icons.length ? (
|
||||
<>
|
||||
@@ -433,7 +439,7 @@ function StickersContentView({ category }: { category: StickerCategory }) {
|
||||
<div className="h-full space-y-4">
|
||||
{isLoadingCollections ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="text-muted-foreground size-6 animate-spin" />
|
||||
<Spinner className="text-muted-foreground size-6" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -489,7 +495,7 @@ function CollectionItem({ title, subtitle, onClick }: CollectionItemProps) {
|
||||
<p className="font-medium">{title}</p>
|
||||
<p className="text-muted-foreground text-xs">{subtitle}</p>
|
||||
</div>
|
||||
<ArrowRight />
|
||||
<HugeiconsIcon icon={ArrowRightIcon} className="size-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -593,7 +599,7 @@ function StickerItem({
|
||||
/>
|
||||
{isAdding && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center rounded-md bg-black/60">
|
||||
<Loader2 className="size-6 animate-spin text-white" />
|
||||
<Spinner className="size-6 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -52,7 +52,7 @@ export function DraggableItem({
|
||||
const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 });
|
||||
const dragRef = useRef<HTMLDivElement>(null);
|
||||
const editor = useEditor();
|
||||
const highlightClassName = "ring-2 ring-primary rounded-sm bg-primary/10";
|
||||
const highlightClassName = `ring-2 ring-primary bg-primary/10 ${isRounded ? "rounded-sm" : ""}`;
|
||||
|
||||
const handleAddToTimeline = () => {
|
||||
onAddToTimeline?.({ currentTime: editor.playback.getCurrentTime() });
|
||||
@@ -111,7 +111,7 @@ export function DraggableItem({
|
||||
ratio={aspectRatio}
|
||||
className={cn(
|
||||
"bg-panel-accent relative overflow-hidden",
|
||||
isRounded && "rounded-md",
|
||||
isRounded && "rounded-sm",
|
||||
isDraggable && "[&::-webkit-drag-ghost]:opacity-0",
|
||||
)}
|
||||
draggable={isDraggable}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { Button } from "../ui/button";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { KeyboardShortcutsHelp } from "../keyboard-shortcuts-help";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -21,7 +20,14 @@ import { ThemeToggle } from "../theme-toggle";
|
||||
import { SOCIAL_LINKS } from "@/constants/site-constants";
|
||||
import { toast } from "sonner";
|
||||
import { useEditor } from "@/hooks/use-editor";
|
||||
import { OcLeftArrowIcon, OcPencilIcon, OcTrashIcon } from "@opencut/ui/icons";
|
||||
import {
|
||||
ArrowLeft02Icon,
|
||||
Edit03Icon,
|
||||
Delete02Icon,
|
||||
Keyboard,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { ShortcutsDialog } from "./shortcuts-dialog";
|
||||
|
||||
export function EditorHeader() {
|
||||
return (
|
||||
@@ -30,7 +36,6 @@ export function EditorHeader() {
|
||||
<ProjectDropdown />
|
||||
</div>
|
||||
<nav className="flex items-center gap-2">
|
||||
<KeyboardShortcutsHelp />
|
||||
<ExportButton />
|
||||
<ThemeToggle />
|
||||
</nav>
|
||||
@@ -39,8 +44,9 @@ export function EditorHeader() {
|
||||
}
|
||||
|
||||
function ProjectDropdown() {
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
|
||||
const [openDialog, setOpenDialog] = useState<
|
||||
"delete" | "rename" | "shortcuts" | null
|
||||
>(null);
|
||||
const [isExiting, setIsExiting] = useState(false);
|
||||
const router = useRouter();
|
||||
const editor = useEditor();
|
||||
@@ -78,7 +84,7 @@ function ProjectDropdown() {
|
||||
error instanceof Error ? error.message : "Please try again",
|
||||
});
|
||||
} finally {
|
||||
setIsRenameDialogOpen(false);
|
||||
setOpenDialog(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -94,7 +100,7 @@ function ProjectDropdown() {
|
||||
error instanceof Error ? error.message : "Please try again",
|
||||
});
|
||||
} finally {
|
||||
setIsDeleteDialogOpen(false);
|
||||
setOpenDialog(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -119,25 +125,32 @@ function ProjectDropdown() {
|
||||
onClick={handleExit}
|
||||
disabled={isExiting}
|
||||
>
|
||||
<OcLeftArrowIcon className="size-4" />
|
||||
Projects
|
||||
<HugeiconsIcon icon={ArrowLeft02Icon} className="size-4" />
|
||||
Exit project
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-1.5"
|
||||
onClick={() => setIsRenameDialogOpen(true)}
|
||||
onClick={() => setOpenDialog("rename")}
|
||||
>
|
||||
<OcPencilIcon className="size-4" />
|
||||
<HugeiconsIcon icon={Edit03Icon} className="size-4" />
|
||||
Rename project
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
className="flex items-center gap-1.5"
|
||||
onClick={() => setIsDeleteDialogOpen(true)}
|
||||
onClick={() => setOpenDialog("delete")}
|
||||
>
|
||||
<OcTrashIcon className="size-4" />
|
||||
Delete Project
|
||||
<HugeiconsIcon icon={Delete02Icon} className="size-4" />
|
||||
Delete project
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-1.5"
|
||||
onClick={() => setOpenDialog("shortcuts")}
|
||||
>
|
||||
<HugeiconsIcon icon={Keyboard} className="size-4" />
|
||||
Keyboard shortcuts
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href={SOCIAL_LINKS.discord}
|
||||
@@ -152,17 +165,21 @@ function ProjectDropdown() {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<RenameProjectDialog
|
||||
isOpen={isRenameDialogOpen}
|
||||
onOpenChange={setIsRenameDialogOpen}
|
||||
isOpen={openDialog === "rename"}
|
||||
onOpenChange={(isOpen) => setOpenDialog(isOpen ? "rename" : null)}
|
||||
onConfirm={(newName) => handleSaveProjectName(newName)}
|
||||
projectName={activeProject?.metadata.name || ""}
|
||||
/>
|
||||
<DeleteProjectDialog
|
||||
isOpen={isDeleteDialogOpen}
|
||||
onOpenChange={setIsDeleteDialogOpen}
|
||||
isOpen={openDialog === "delete"}
|
||||
onOpenChange={(isOpen) => setOpenDialog(isOpen ? "delete" : null)}
|
||||
onConfirm={handleDeleteProject}
|
||||
projectName={activeProject?.metadata.name || ""}
|
||||
/>
|
||||
<ShortcutsDialog
|
||||
isOpen={openDialog === "shortcuts"}
|
||||
onOpenChange={(isOpen) => setOpenDialog(isOpen ? "shortcuts" : null)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { OcTransitionUpIcon } from "@opencut/ui/icons";
|
||||
import { TransitionTopIcon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||
import { Button } from "../ui/button";
|
||||
import { Label } from "../ui/label";
|
||||
@@ -53,7 +54,7 @@ export function ExportButton() {
|
||||
}}
|
||||
>
|
||||
<div className="relative flex items-center gap-1.5 rounded-[0.8rem] bg-linear-270 from-[#2567EC] to-[#37B6F7] px-4 py-1 shadow-[0_1px_3px_0px_rgba(0,0,0,0.65)]">
|
||||
<OcTransitionUpIcon className="z-50" />
|
||||
<HugeiconsIcon icon={TransitionTopIcon} className="z-50 size-4" />
|
||||
<span className="z-50 text-[0.875rem]">Export</span>
|
||||
<div className="absolute top-0 left-0 z-10 flex size-full items-center justify-center rounded-[0.8rem] bg-linear-to-t from-white/0 to-white/50">
|
||||
<div className="absolute top-[0.08rem] z-50 h-[calc(100%-2px)] w-[calc(100%-2px)] rounded-[0.8rem] bg-linear-270 from-[#2567EC] to-[#37B6F7]"></div>
|
||||
|
||||
@@ -4,7 +4,8 @@ import { ScrollArea } from "../../ui/scroll-area";
|
||||
import { AudioProperties } from "./audio-properties";
|
||||
import { VideoProperties } from "./video-properties";
|
||||
import { TextProperties } from "./text-properties";
|
||||
import { SquareSlashIcon } from "lucide-react";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { Settings05Icon } from "@hugeicons/core-free-icons";
|
||||
import { useEditor } from "@/hooks/use-editor";
|
||||
import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
|
||||
|
||||
@@ -29,12 +30,12 @@ export function PropertiesPanel() {
|
||||
);
|
||||
}
|
||||
if (element.type === "audio") {
|
||||
return <AudioProperties key={element.id} element={element} />;
|
||||
return <AudioProperties key={element.id} _element={element} />;
|
||||
}
|
||||
if (element.type === "video" || element.type === "image") {
|
||||
return (
|
||||
<div key={element.id}>
|
||||
<VideoProperties element={element} />
|
||||
<VideoProperties _element={element} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -51,12 +52,13 @@ export function PropertiesPanel() {
|
||||
function EmptyView() {
|
||||
return (
|
||||
<div className="bg-panel flex h-full flex-col items-center justify-center gap-3 p-4">
|
||||
<SquareSlashIcon
|
||||
className="text-muted-foreground size-10"
|
||||
strokeWidth={1.5}
|
||||
<HugeiconsIcon
|
||||
icon={Settings05Icon}
|
||||
className="text-muted-foreground/75 size-10"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
<div className="flex flex-col gap-2 text-center">
|
||||
<p className="text-lg font-medium">It’s empty here</p>
|
||||
<p className="text-lg font-medium">It's empty here</p>
|
||||
<p className="text-muted-foreground text-sm text-balance">
|
||||
Click an element on the timeline to edit its properties
|
||||
</p>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { cn } from "@/utils/ui";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { ArrowDownIcon } from "@hugeicons/core-free-icons";
|
||||
|
||||
interface PropertyItemProps {
|
||||
direction?: "row" | "column";
|
||||
@@ -79,7 +80,10 @@ export function PropertyGroup({
|
||||
<PropertyItemLabel className={cn("cursor-pointer", titleClassName)}>
|
||||
{title}
|
||||
</PropertyItemLabel>
|
||||
<ChevronDown className={cn("size-3", !isExpanded && "-rotate-90")} />
|
||||
<HugeiconsIcon
|
||||
icon={ArrowDownIcon}
|
||||
className={cn("size-3", !isExpanded && "-rotate-90")}
|
||||
/>
|
||||
</button>
|
||||
{isExpanded && <PropertyItemValue>{children}</PropertyItemValue>}
|
||||
</PropertyItem>
|
||||
|
||||
@@ -21,7 +21,8 @@ import { ColorPicker } from "@/components/ui/color-picker";
|
||||
import { cn } from "@/utils/ui";
|
||||
import { capitalizeFirstLetter, uppercase } from "@/utils/strings";
|
||||
import { clamp } from "@/utils/math";
|
||||
import { Grid2x2 } from "lucide-react";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { LayoutGridIcon } from "@hugeicons/core-free-icons";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -386,7 +387,7 @@ export function TextProperties({
|
||||
}
|
||||
className="bg-panel-accent size-9 overflow-hidden rounded-full p-0"
|
||||
>
|
||||
<Grid2x2
|
||||
<HugeiconsIcon icon={LayoutGridIcon}
|
||||
className={cn(
|
||||
"text-foreground",
|
||||
element.backgroundColor === "transparent" &&
|
||||
|
||||
+20
-25
@@ -8,7 +8,7 @@ import {
|
||||
useKeyboardShortcutsHelp,
|
||||
} from "@/hooks/use-keyboard-shortcuts-help";
|
||||
import { useKeybindingsStore } from "@/stores/keybindings-store";
|
||||
import { Button } from "./ui/button";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -16,11 +16,15 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "./ui/dialog";
|
||||
} from "../ui/dialog";
|
||||
|
||||
export function KeyboardShortcutsHelp() {
|
||||
const [open, setOpen] = useState(false);
|
||||
export function ShortcutsDialog({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) {
|
||||
const [recordingShortcut, setRecordingShortcut] =
|
||||
useState<KeyboardShortcut | null>(null);
|
||||
|
||||
@@ -35,7 +39,6 @@ export function KeyboardShortcutsHelp() {
|
||||
isRecording,
|
||||
} = useKeybindingsStore();
|
||||
|
||||
// Get shortcuts from centralized hook
|
||||
const { shortcuts } = useKeyboardShortcutsHelp();
|
||||
|
||||
const categories = Array.from(new Set(shortcuts.map((s) => s.category)));
|
||||
@@ -49,7 +52,6 @@ export function KeyboardShortcutsHelp() {
|
||||
|
||||
const keyString = getKeybindingString(e);
|
||||
if (keyString) {
|
||||
// Auto-save the new keybinding
|
||||
const conflict = validateKeybinding(
|
||||
keyString,
|
||||
recordingShortcut.action,
|
||||
@@ -62,11 +64,11 @@ export function KeyboardShortcutsHelp() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove old keybindings for this action
|
||||
const oldKeys = getKeybindingsForAction(recordingShortcut.action);
|
||||
oldKeys.forEach((key) => removeKeybinding(key));
|
||||
for (const key of oldKeys) {
|
||||
removeKeybinding(key);
|
||||
}
|
||||
|
||||
// Add new keybinding
|
||||
updateKeybinding(keyString, recordingShortcut.action);
|
||||
|
||||
setIsRecording(false);
|
||||
@@ -103,18 +105,12 @@ export function KeyboardShortcutsHelp() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="text" size="sm" className="gap-2">
|
||||
<Keyboard />
|
||||
Shortcuts
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="flex max-h-[80vh] max-w-2xl flex-col p-0">
|
||||
<DialogHeader className="flex-shrink-0 p-6 pb-0">
|
||||
<DialogHeader className="flex-shrink-0 gap-2 p-6 pb-0">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Keyboard className="size-5" />
|
||||
Keyboard Shortcuts
|
||||
Keyboard shortcuts
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Speed up your video editing workflow with these keyboard shortcuts.
|
||||
@@ -123,13 +119,13 @@ export function KeyboardShortcutsHelp() {
|
||||
</DialogHeader>
|
||||
|
||||
<div className="scrollbar-thin flex-grow overflow-y-auto">
|
||||
<div className="space-y-6 p-6 pt-2">
|
||||
<div className="flex flex-col gap-6 p-6 pt-2">
|
||||
{categories.map((category) => (
|
||||
<div key={category} className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
|
||||
{category}
|
||||
</h3>
|
||||
<div className="space-y-1">
|
||||
<div className="flex flex-col gap-1">
|
||||
{shortcuts
|
||||
.filter((shortcut) => shortcut.category === category)
|
||||
.map((shortcut) => (
|
||||
@@ -139,7 +135,7 @@ export function KeyboardShortcutsHelp() {
|
||||
isRecording={
|
||||
shortcut.action === recordingShortcut?.action
|
||||
}
|
||||
onStartRecording={handleStartRecording}
|
||||
onStartRecording={() => handleStartRecording(shortcut)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -164,9 +160,8 @@ function ShortcutItem({
|
||||
}: {
|
||||
shortcut: KeyboardShortcut;
|
||||
isRecording: boolean;
|
||||
onStartRecording: (shortcut: KeyboardShortcut) => void;
|
||||
onStartRecording: (params: { shortcut: KeyboardShortcut }) => void;
|
||||
}) {
|
||||
// Filter out lowercase duplicates for display - if both "j" and "J" exist, only show "J"
|
||||
const displayKeys = shortcut.keys.filter((key: string) => {
|
||||
if (
|
||||
key.includes("Cmd") &&
|
||||
@@ -195,7 +190,7 @@ function ShortcutItem({
|
||||
<EditableShortcutKey
|
||||
key={keyId}
|
||||
isRecording={isRecording}
|
||||
onStartRecording={() => onStartRecording(shortcut)}
|
||||
onStartRecording={() => onStartRecording({ shortcut })}
|
||||
>
|
||||
{keyPart}
|
||||
</EditableShortcutKey>
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useEditor } from "@/hooks/use-editor";
|
||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||
import { Bookmark } from "lucide-react";
|
||||
import { Bookmark02Icon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
|
||||
interface TimelineBookmarksRowProps {
|
||||
zoomLevel: number;
|
||||
@@ -84,10 +85,10 @@ export function TimelineBookmark({
|
||||
onClick={(event) => handleBookmarkClick({ event })}
|
||||
>
|
||||
<div className="text-primary absolute top-[-1px] left-[-5px]">
|
||||
<Bookmark
|
||||
<HugeiconsIcon
|
||||
icon={Bookmark02Icon}
|
||||
aria-hidden="true"
|
||||
className="fill-primary size-3"
|
||||
focusable="false"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Eye,
|
||||
EyeOff,
|
||||
VolumeOff,
|
||||
Volume2,
|
||||
type LucideIcon,
|
||||
EyeIcon,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
Delete02Icon,
|
||||
ViewIcon,
|
||||
ViewOffSlashIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeOffIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react";
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
@@ -133,6 +132,7 @@ export function Timeline() {
|
||||
selectionBox,
|
||||
handleMouseDown: handleSelectionMouseDown,
|
||||
isSelecting,
|
||||
shouldIgnoreClick,
|
||||
} = useSelectionBox({
|
||||
containerRef: tracksContainerRef,
|
||||
onSelectionComplete: (elements) => {
|
||||
@@ -267,17 +267,12 @@ export function Timeline() {
|
||||
}}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center justify-end gap-2">
|
||||
{/* Debug main track */}
|
||||
{/* {isMainTrack(track) && (
|
||||
<div className="size-2 rounded-full bg-red-500" />
|
||||
)} */}
|
||||
|
||||
{canTracktHaveAudio(track) && (
|
||||
<TrackToggleIcon
|
||||
isOff={track.muted}
|
||||
icons={{
|
||||
on: Volume2,
|
||||
off: VolumeOff,
|
||||
on: VolumeHighIcon,
|
||||
off: VolumeOffIcon,
|
||||
}}
|
||||
onClick={() =>
|
||||
editor.timeline.toggleTrackMute({
|
||||
@@ -291,8 +286,8 @@ export function Timeline() {
|
||||
<TrackToggleIcon
|
||||
isOff={track.hidden}
|
||||
icons={{
|
||||
on: Eye,
|
||||
off: EyeOff,
|
||||
on: ViewIcon,
|
||||
off: ViewOffSlashIcon,
|
||||
}}
|
||||
onClick={() =>
|
||||
editor.timeline.toggleTrackVisibility({
|
||||
@@ -397,6 +392,7 @@ export function Timeline() {
|
||||
onElementMouseDown={handleElementMouseDown}
|
||||
onElementClick={handleElementClick}
|
||||
onTrackMouseDown={handleSelectionMouseDown}
|
||||
shouldIgnoreClick={shouldIgnoreClick}
|
||||
/>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
@@ -409,7 +405,7 @@ export function Timeline() {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Volume2 />
|
||||
<HugeiconsIcon icon={VolumeHighIcon} />
|
||||
<span>
|
||||
{canTracktHaveAudio(track) && track.muted
|
||||
? "Unmute track"
|
||||
@@ -424,7 +420,7 @@ export function Timeline() {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<EyeIcon />
|
||||
<HugeiconsIcon icon={ViewIcon} />
|
||||
<span>
|
||||
{canTrackBeHidden(track) && track.hidden
|
||||
? "Show track"
|
||||
@@ -440,7 +436,7 @@ export function Timeline() {
|
||||
}}
|
||||
variant="destructive"
|
||||
>
|
||||
<Trash2 />
|
||||
<HugeiconsIcon icon={Delete02Icon} />
|
||||
Delete track
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
@@ -467,20 +463,22 @@ function TrackToggleIcon({
|
||||
}: {
|
||||
isOff: boolean;
|
||||
icons: {
|
||||
on: LucideIcon;
|
||||
off: LucideIcon;
|
||||
on: IconSvgElement;
|
||||
off: IconSvgElement;
|
||||
};
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{isOff ? (
|
||||
<icons.off
|
||||
<HugeiconsIcon
|
||||
icon={icons.off}
|
||||
className="text-destructive size-4 cursor-pointer"
|
||||
onClick={onClick}
|
||||
/>
|
||||
) : (
|
||||
<icons.on
|
||||
<HugeiconsIcon
|
||||
icon={icons.on}
|
||||
className="text-muted-foreground size-4 cursor-pointer"
|
||||
onClick={onClick}
|
||||
/>
|
||||
|
||||
@@ -213,10 +213,6 @@ export function TimelineElement({
|
||||
Duplicate {element.type === "text" ? "text" : "clip"}
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
<ContextMenuItem disabled>
|
||||
<ArrowUpDown className="mr-2 size-4" />
|
||||
Move to track (Coming soon)
|
||||
</ContextMenuItem>
|
||||
{selectedElements.length === 1 && hasMediaId(element) && (
|
||||
<>
|
||||
<ContextMenuItem
|
||||
@@ -289,7 +285,6 @@ function ElementInner({
|
||||
className="absolute inset-0 size-full cursor-pointer"
|
||||
onClick={(e) => onElementClick(e, element)}
|
||||
onMouseDown={(e) => onElementMouseDown(e, element)}
|
||||
onContextMenu={(e) => onElementMouseDown(e, element)}
|
||||
>
|
||||
<div className="absolute inset-0 flex h-full items-center">
|
||||
<ElementContent
|
||||
@@ -300,7 +295,9 @@ function ElementInner({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(hasAudio ? isMuted : canElementBeHidden(element) && element.hidden) && (
|
||||
{(hasAudio
|
||||
? isMuted
|
||||
: canElementBeHidden(element) && element.hidden) && (
|
||||
<div className="bg-opacity-50 pointer-events-none absolute inset-0 flex items-center justify-center bg-black">
|
||||
{hasAudio ? (
|
||||
<VolumeX className="size-6 text-white" />
|
||||
@@ -381,8 +378,8 @@ function ElementContent({
|
||||
src={`https://api.iconify.design/${element.iconName}.svg?width=20&height=20`}
|
||||
alt={element.name}
|
||||
className="size-5 shrink-0"
|
||||
width={20}
|
||||
height={20}
|
||||
width={20}
|
||||
height={20}
|
||||
unoptimized
|
||||
/>
|
||||
<span className="truncate text-xs text-white">{element.name}</span>
|
||||
|
||||
@@ -7,21 +7,8 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Pause,
|
||||
Play,
|
||||
SkipBack,
|
||||
Bookmark,
|
||||
Magnet,
|
||||
Link,
|
||||
ZoomOut,
|
||||
ZoomIn,
|
||||
Copy,
|
||||
Trash2,
|
||||
Snowflake,
|
||||
ArrowLeftToLine,
|
||||
ArrowRightToLine,
|
||||
SplitSquareHorizontal,
|
||||
Scissors,
|
||||
LayersIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
@@ -38,6 +25,28 @@ import { ScenesView } from "../scenes-view";
|
||||
import { type TAction, invokeAction } from "@/lib/actions";
|
||||
import { cn } from "@/utils/ui";
|
||||
import { useTimelineStore } from "@/stores/timeline-store";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Bookmark02Icon,
|
||||
Delete02Icon,
|
||||
SnowIcon,
|
||||
ScissorIcon,
|
||||
MagnetIcon,
|
||||
Link04Icon,
|
||||
SearchAddIcon,
|
||||
SearchMinusIcon,
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
Copy01Icon,
|
||||
ArrowLeft03Icon,
|
||||
ArrowRight03Icon,
|
||||
LayoutAlignLeftIcon,
|
||||
LayoutAlignRightIcon,
|
||||
AlignLeftIcon,
|
||||
AlignRightIcon,
|
||||
Layers01Icon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
|
||||
export function TimelineToolbar({
|
||||
zoomLevel,
|
||||
@@ -60,18 +69,20 @@ export function TimelineToolbar({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-10 items-center justify-between border-b px-2 py-1">
|
||||
<ToolbarLeftSection />
|
||||
<ScrollArea className="scrollbar-hidden">
|
||||
<div className="flex h-10 items-center justify-between border-b px-2 py-1">
|
||||
<ToolbarLeftSection />
|
||||
|
||||
<SceneSelector />
|
||||
<SceneSelector />
|
||||
|
||||
<ToolbarRightSection
|
||||
zoomLevel={zoomLevel}
|
||||
minZoom={minZoom}
|
||||
onZoomChange={(zoom) => setZoomLevel({ zoom })}
|
||||
onZoom={handleZoom}
|
||||
/>
|
||||
</div>
|
||||
<ToolbarRightSection
|
||||
zoomLevel={zoomLevel}
|
||||
minZoom={minZoom}
|
||||
onZoomChange={(zoom) => setZoomLevel({ zoom })}
|
||||
onZoom={handleZoom}
|
||||
/>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -96,7 +107,13 @@ function ToolbarLeftSection() {
|
||||
<div className="flex items-center gap-1">
|
||||
<TooltipProvider delayDuration={500}>
|
||||
<ToolbarButton
|
||||
icon={isPlaying ? <Pause /> : <Play />}
|
||||
icon={
|
||||
isPlaying ? (
|
||||
<HugeiconsIcon icon={PauseIcon} />
|
||||
) : (
|
||||
<HugeiconsIcon icon={PlayIcon} />
|
||||
)
|
||||
}
|
||||
tooltip={isPlaying ? "Pause" : "Play"}
|
||||
onClick={({ event }) =>
|
||||
handleAction({ action: "toggle-play", event })
|
||||
@@ -109,14 +126,14 @@ function ToolbarLeftSection() {
|
||||
onClick={({ event }) => handleAction({ action: "goto-start", event })}
|
||||
/>
|
||||
|
||||
<div className="bg-border mx-2 h-10 w-px" />
|
||||
<div className="bg-border mx-1 h-6 w-px" />
|
||||
|
||||
<TimeDisplay />
|
||||
|
||||
<div className="bg-border mx-1 h-10 w-px" />
|
||||
<div className="bg-border mx-1 h-6 w-px" />
|
||||
|
||||
<ToolbarButton
|
||||
icon={<Scissors />}
|
||||
icon={<HugeiconsIcon icon={ScissorIcon} />}
|
||||
tooltip="Split element"
|
||||
onClick={({ event }) =>
|
||||
handleAction({ action: "split-selected", event })
|
||||
@@ -124,18 +141,18 @@ function ToolbarLeftSection() {
|
||||
/>
|
||||
|
||||
<ToolbarButton
|
||||
icon={<ArrowLeftToLine />}
|
||||
tooltip="Split and keep left"
|
||||
icon={<HugeiconsIcon icon={AlignLeftIcon} />}
|
||||
tooltip="Split left"
|
||||
onClick={({ event }) =>
|
||||
handleAction({ action: "split-selected-left", event })
|
||||
handleAction({ action: "split-selected-right", event })
|
||||
}
|
||||
/>
|
||||
|
||||
<ToolbarButton
|
||||
icon={<ArrowRightToLine />}
|
||||
tooltip="Split and keep right"
|
||||
icon={<HugeiconsIcon icon={AlignRightIcon} />}
|
||||
tooltip="Split right"
|
||||
onClick={({ event }) =>
|
||||
handleAction({ action: "split-selected-right", event })
|
||||
handleAction({ action: "split-selected-left", event })
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -147,7 +164,7 @@ function ToolbarLeftSection() {
|
||||
/>
|
||||
|
||||
<ToolbarButton
|
||||
icon={<Copy />}
|
||||
icon={<HugeiconsIcon icon={Copy01Icon} />}
|
||||
tooltip="Duplicate element"
|
||||
onClick={({ event }) =>
|
||||
handleAction({ action: "duplicate-selected", event })
|
||||
@@ -155,14 +172,14 @@ function ToolbarLeftSection() {
|
||||
/>
|
||||
|
||||
<ToolbarButton
|
||||
icon={<Snowflake />}
|
||||
icon={<HugeiconsIcon icon={SnowIcon} />}
|
||||
tooltip="Coming soon" /* freeze frame */
|
||||
disabled={true}
|
||||
onClick={({ event: _event }) => {}}
|
||||
/>
|
||||
|
||||
<ToolbarButton
|
||||
icon={<Trash2 />}
|
||||
icon={<HugeiconsIcon icon={Delete02Icon} />}
|
||||
tooltip="Delete element"
|
||||
onClick={({ event }) =>
|
||||
handleAction({ action: "delete-selected", event })
|
||||
@@ -174,7 +191,8 @@ function ToolbarLeftSection() {
|
||||
<Tooltip>
|
||||
<ToolbarButton
|
||||
icon={
|
||||
<Bookmark
|
||||
<HugeiconsIcon
|
||||
icon={Bookmark02Icon}
|
||||
className={currentBookmarked ? "fill-primary text-primary" : ""}
|
||||
/>
|
||||
}
|
||||
@@ -228,7 +246,7 @@ function SceneSelector() {
|
||||
<SplitButtonSeparator />
|
||||
<ScenesView>
|
||||
<SplitButtonRight onClick={() => {}} type="button">
|
||||
<LayersIcon className="size-4" />
|
||||
<HugeiconsIcon icon={Layers01Icon} className="size-4" />
|
||||
</SplitButtonRight>
|
||||
</ScenesView>
|
||||
</SplitButton>
|
||||
@@ -259,7 +277,10 @@ function ToolbarRightSection({
|
||||
<TooltipProvider delayDuration={500}>
|
||||
<ToolbarButton
|
||||
icon={
|
||||
<Magnet className={cn(snappingEnabled ? "text-primary" : "")} />
|
||||
<HugeiconsIcon
|
||||
icon={MagnetIcon}
|
||||
className={cn(snappingEnabled ? "text-primary" : "")}
|
||||
/>
|
||||
}
|
||||
tooltip="Auto snapping"
|
||||
onClick={() => toggleSnapping()}
|
||||
@@ -267,7 +288,13 @@ function ToolbarRightSection({
|
||||
|
||||
<ToolbarButton
|
||||
icon={
|
||||
<Link className={cn(rippleEditingEnabled ? "text-primary" : "")} />
|
||||
<HugeiconsIcon
|
||||
icon={Link04Icon}
|
||||
className={cn(
|
||||
rippleEditingEnabled ? "text-primary" : "",
|
||||
"scale-110",
|
||||
)}
|
||||
/>
|
||||
}
|
||||
tooltip="Ripple editing"
|
||||
onClick={() => toggleRippleEditing()}
|
||||
@@ -283,7 +310,7 @@ function ToolbarRightSection({
|
||||
type="button"
|
||||
onClick={() => onZoom({ direction: "out" })}
|
||||
>
|
||||
<ZoomOut />
|
||||
<HugeiconsIcon icon={SearchAddIcon} />
|
||||
</Button>
|
||||
<Slider
|
||||
className="w-24"
|
||||
@@ -299,7 +326,7 @@ function ToolbarRightSection({
|
||||
type="button"
|
||||
onClick={() => onZoom({ direction: "in" })}
|
||||
>
|
||||
<ZoomIn />
|
||||
<HugeiconsIcon icon={SearchMinusIcon} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,6 +31,7 @@ interface TimelineTrackContentProps {
|
||||
track: TimelineTrack;
|
||||
}) => void;
|
||||
onTrackMouseDown?: (event: React.MouseEvent) => void;
|
||||
shouldIgnoreClick?: () => boolean;
|
||||
}
|
||||
|
||||
export function TimelineTrackContent({
|
||||
@@ -45,6 +46,7 @@ export function TimelineTrackContent({
|
||||
onElementMouseDown,
|
||||
onElementClick,
|
||||
onTrackMouseDown,
|
||||
shouldIgnoreClick,
|
||||
}: TimelineTrackContentProps) {
|
||||
const editor = useEditor();
|
||||
const { isElementSelected, clearElementSelection } = useElementSelection();
|
||||
@@ -66,7 +68,10 @@ export function TimelineTrackContent({
|
||||
return (
|
||||
<button
|
||||
className={cn("size-full", hasSelectedElements && "bg-panel-accent/35")}
|
||||
onClick={clearElementSelection}
|
||||
onClick={() => {
|
||||
if (shouldIgnoreClick?.()) return;
|
||||
clearElementSelection();
|
||||
}}
|
||||
onMouseDown={(event) => {
|
||||
onTrackMouseDown?.(event);
|
||||
}}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Button } from "./ui/button";
|
||||
import { OcGithubIcon } from "@opencut/ui/icons";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { SOCIAL_LINKS } from "@/constants/site-constants";
|
||||
import {
|
||||
GithubIcon,
|
||||
Link04Icon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
|
||||
export function GitHubContributeSection({
|
||||
title,
|
||||
@@ -24,7 +27,7 @@ export function GitHubContributeSection({
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button className="w-full" size="lg">
|
||||
<OcGithubIcon className="size-4" />
|
||||
<HugeiconsIcon icon={GithubIcon} />
|
||||
Start contributing
|
||||
</Button>
|
||||
</Link>
|
||||
@@ -34,7 +37,7 @@ export function GitHubContributeSection({
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button variant="outline" className="w-full" size="lg">
|
||||
<ExternalLink className="size-4" />
|
||||
<HugeiconsIcon icon={Link04Icon} />
|
||||
Report issues
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@@ -7,7 +7,8 @@ import { Button } from "./ui/button";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { ThemeToggle } from "./theme-toggle";
|
||||
import { OcGithubIcon, OcMenuIcon } from "@opencut/ui/icons";
|
||||
import { GithubIcon, Menu02Icon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { cn } from "@/utils/ui";
|
||||
import { DEFAULT_LOGO_URL, SOCIAL_LINKS } from "@/constants/site-constants";
|
||||
|
||||
@@ -62,13 +63,13 @@ export function Header() {
|
||||
className="flex items-center justify-center p-0"
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
>
|
||||
<OcMenuIcon size={30} />
|
||||
<HugeiconsIcon icon={Menu02Icon} size={30} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="hidden items-center gap-3 md:flex">
|
||||
<Link href={SOCIAL_LINKS.github}>
|
||||
<Button className="bg-background text-sm" variant="outline">
|
||||
<OcGithubIcon className="size-4" />
|
||||
<HugeiconsIcon icon={GithubIcon} className="size-4" />
|
||||
31k+
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "./ui/button";
|
||||
import { Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { cn } from "@/utils/ui";
|
||||
import { Sun03Icon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
|
||||
interface ThemeToggleProps {
|
||||
className?: string;
|
||||
@@ -28,7 +29,7 @@ export function ThemeToggle({
|
||||
onToggle?.(e);
|
||||
}}
|
||||
>
|
||||
<Sun className={cn("!size-[1.1rem]", iconClassName)} />
|
||||
<HugeiconsIcon icon={Sun03Icon} className={cn("!size-[1.1rem]", iconClassName)} />
|
||||
<span className="sr-only">{theme === "dark" ? "Light" : "Dark"}</span>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -231,7 +231,7 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative flex-1">
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { HugeiconsIcon, type HugeiconsIconProps } from "@hugeicons/react";
|
||||
import { Loading03Icon } from "@hugeicons/core-free-icons";
|
||||
import { cn } from "@/utils/ui";
|
||||
|
||||
function Spinner({ className, ...props }: Omit<HugeiconsIconProps, "icon">) {
|
||||
return (
|
||||
<HugeiconsIcon
|
||||
icon={Loading03Icon}
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
className={cn("size-4 animate-spin", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Spinner }
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { TrackType } from "@/types/timeline";
|
||||
import { Video, TypeIcon, Music, Sticker } from "lucide-react";
|
||||
import {
|
||||
Happy01Icon,
|
||||
MusicNote03Icon,
|
||||
TextIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { OcVideoIcon } from "@opencut/ui/icons";
|
||||
|
||||
export const TRACK_COLORS: Record<TrackType, { background: string }> = {
|
||||
video: {
|
||||
@@ -37,8 +43,23 @@ export const TIMELINE_CONSTANTS = {
|
||||
} as const;
|
||||
|
||||
export const TRACK_ICONS: Record<TrackType, React.ReactNode> = {
|
||||
video: <Video className="text-muted-foreground size-4 shrink-0" />,
|
||||
text: <TypeIcon className="text-muted-foreground size-4 shrink-0" />,
|
||||
audio: <Music className="text-muted-foreground size-4 shrink-0" />,
|
||||
sticker: <Sticker className="text-muted-foreground size-4 shrink-0" />,
|
||||
video: <OcVideoIcon className="text-muted-foreground size-4 shrink-0" />,
|
||||
text: (
|
||||
<HugeiconsIcon
|
||||
icon={TextIcon}
|
||||
className="text-muted-foreground size-4 shrink-0"
|
||||
/>
|
||||
),
|
||||
audio: (
|
||||
<HugeiconsIcon
|
||||
icon={MusicNote03Icon}
|
||||
className="text-muted-foreground size-4 shrink-0"
|
||||
/>
|
||||
),
|
||||
sticker: (
|
||||
<HugeiconsIcon
|
||||
icon={Happy01Icon}
|
||||
className="text-muted-foreground size-4 shrink-0"
|
||||
/>
|
||||
),
|
||||
} as const;
|
||||
|
||||
@@ -510,13 +510,9 @@ export function useElementInteraction({
|
||||
element: TimelineElement;
|
||||
track: TimelineTrack;
|
||||
}) => {
|
||||
event.stopPropagation();
|
||||
mouseDownLocationRef.current = { x: event.clientX, y: event.clientY };
|
||||
|
||||
const isRightClick = event.button === 2;
|
||||
const isMultiSelect = event.metaKey || event.ctrlKey || event.shiftKey;
|
||||
|
||||
// right-click
|
||||
// right-click: don't stop propagation so ContextMenu can open
|
||||
if (isRightClick) {
|
||||
const alreadySelected = isElementSelected({
|
||||
trackId: track.id,
|
||||
@@ -532,6 +528,12 @@ export function useElementInteraction({
|
||||
return;
|
||||
}
|
||||
|
||||
// left-click: stop propagation for drag operations
|
||||
event.stopPropagation();
|
||||
mouseDownLocationRef.current = { x: event.clientX, y: event.clientY };
|
||||
|
||||
const isMultiSelect = event.metaKey || event.ctrlKey || event.shiftKey;
|
||||
|
||||
// multi-select: toggle selection
|
||||
if (isMultiSelect) {
|
||||
handleSelectionClick({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||
import { getCumulativeHeightBefore, getTrackHeight } from "@/lib/timeline";
|
||||
import { useEditor } from "../use-editor";
|
||||
@@ -98,6 +98,7 @@ export function useSelectionBox({
|
||||
const [selectionBox, setSelectionBox] = useState<SelectionBoxState | null>(
|
||||
null,
|
||||
);
|
||||
const justFinishedSelectingRef = useRef(false);
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
({ clientX, clientY }: React.MouseEvent) => {
|
||||
@@ -195,6 +196,12 @@ export function useSelectionBox({
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (selectionBox?.isActive) {
|
||||
justFinishedSelectingRef.current = true;
|
||||
requestAnimationFrame(() => {
|
||||
justFinishedSelectingRef.current = false;
|
||||
});
|
||||
}
|
||||
setSelectionBox(null);
|
||||
};
|
||||
|
||||
@@ -223,9 +230,14 @@ export function useSelectionBox({
|
||||
};
|
||||
}, [selectionBox, containerRef]);
|
||||
|
||||
const shouldIgnoreClick = useCallback(() => {
|
||||
return justFinishedSelectingRef.current;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
selectionBox,
|
||||
handleMouseDown,
|
||||
isSelecting: selectionBox?.isActive || false,
|
||||
shouldIgnoreClick,
|
||||
};
|
||||
}
|
||||
|
||||
+34
-24
@@ -1,17 +1,18 @@
|
||||
import {
|
||||
CaptionsIcon,
|
||||
ArrowLeftRightIcon,
|
||||
SparklesIcon,
|
||||
StickerIcon,
|
||||
MusicIcon,
|
||||
VideoIcon,
|
||||
BlendIcon,
|
||||
SlidersHorizontalIcon,
|
||||
type LucideIcon,
|
||||
TypeIcon,
|
||||
SettingsIcon,
|
||||
} from "lucide-react";
|
||||
import type { ElementType } from "react";
|
||||
import { create } from "zustand";
|
||||
import {
|
||||
ArrowRightDoubleIcon,
|
||||
ClosedCaptionIcon,
|
||||
Folder03Icon,
|
||||
Happy01Icon,
|
||||
HeadphonesIcon,
|
||||
MagicWand05Icon,
|
||||
TextIcon,
|
||||
Settings01Icon,
|
||||
SlidersHorizontalIcon,
|
||||
ColorsIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react";
|
||||
|
||||
export const TAB_KEYS = [
|
||||
"media",
|
||||
@@ -28,48 +29,57 @@ export const TAB_KEYS = [
|
||||
|
||||
export type Tab = (typeof TAB_KEYS)[number];
|
||||
|
||||
const createHugeiconsIcon =
|
||||
({ icon }: { icon: IconSvgElement }) =>
|
||||
({ className }: { className?: string }) => (
|
||||
<HugeiconsIcon icon={icon} className={className} />
|
||||
);
|
||||
|
||||
export const tabs = {
|
||||
media: {
|
||||
icon: VideoIcon,
|
||||
icon: createHugeiconsIcon({ icon: Folder03Icon }),
|
||||
label: "Media",
|
||||
},
|
||||
sounds: {
|
||||
icon: MusicIcon,
|
||||
icon: createHugeiconsIcon({ icon: HeadphonesIcon }),
|
||||
label: "Sounds",
|
||||
},
|
||||
text: {
|
||||
icon: TypeIcon,
|
||||
icon: createHugeiconsIcon({ icon: TextIcon }),
|
||||
label: "Text",
|
||||
},
|
||||
stickers: {
|
||||
icon: StickerIcon,
|
||||
icon: createHugeiconsIcon({ icon: Happy01Icon }),
|
||||
label: "Stickers",
|
||||
},
|
||||
effects: {
|
||||
icon: SparklesIcon,
|
||||
icon: createHugeiconsIcon({ icon: MagicWand05Icon }),
|
||||
label: "Effects",
|
||||
},
|
||||
transitions: {
|
||||
icon: ArrowLeftRightIcon,
|
||||
icon: createHugeiconsIcon({ icon: ArrowRightDoubleIcon }),
|
||||
label: "Transitions",
|
||||
},
|
||||
captions: {
|
||||
icon: CaptionsIcon,
|
||||
icon: createHugeiconsIcon({ icon: ClosedCaptionIcon }),
|
||||
label: "Captions",
|
||||
},
|
||||
filters: {
|
||||
icon: BlendIcon,
|
||||
icon: createHugeiconsIcon({ icon: ColorsIcon }),
|
||||
label: "Filters",
|
||||
},
|
||||
adjustment: {
|
||||
icon: SlidersHorizontalIcon,
|
||||
icon: createHugeiconsIcon({ icon: SlidersHorizontalIcon }),
|
||||
label: "Adjustment",
|
||||
},
|
||||
settings: {
|
||||
icon: SettingsIcon,
|
||||
icon: createHugeiconsIcon({ icon: Settings01Icon }),
|
||||
label: "Settings",
|
||||
},
|
||||
} satisfies Record<Tab, { icon: LucideIcon; label: string }>;
|
||||
} satisfies Record<
|
||||
Tab,
|
||||
{ icon: ElementType<{ className?: string }>; label: string }
|
||||
>;
|
||||
|
||||
type MediaViewMode = "grid" | "list";
|
||||
|
||||
Reference in New Issue
Block a user