mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
Merge branch 'main' of https://github.com/OpenCut-app/OpenCut
This commit is contained in:
+241
-169
@@ -46,6 +46,13 @@ import {
|
|||||||
} from "@hugeicons/core-free-icons";
|
} from "@hugeicons/core-free-icons";
|
||||||
import { OcVideoIcon } from "@opencut/ui/icons";
|
import { OcVideoIcon } from "@opencut/ui/icons";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
} from "@/components/ui/context-menu";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
@@ -534,10 +541,25 @@ function ProjectItem({
|
|||||||
const isSelected = selectedProjectIdSet.has(project.id);
|
const isSelected = selectedProjectIdSet.has(project.id);
|
||||||
const selectedProjectCount = selectedProjectIds.length;
|
const selectedProjectCount = selectedProjectIds.length;
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
|
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
|
||||||
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
|
const [isInfoDialogOpen, setIsInfoDialogOpen] = useState(false);
|
||||||
|
const editor = useEditor();
|
||||||
const durationLabel = formatProjectDuration({ duration: project.duration });
|
const durationLabel = formatProjectDuration({ duration: project.duration });
|
||||||
const isMultiSelect = selectedProjectCount > 1;
|
const isMultiSelect = selectedProjectCount > 1;
|
||||||
const isGridView = viewMode === "grid";
|
const isGridView = viewMode === "grid";
|
||||||
|
|
||||||
|
const handleRename = () => setIsRenameDialogOpen(true);
|
||||||
|
const handleDuplicate = async () => {
|
||||||
|
await duplicateProjects({ editor, ids: [project.id] });
|
||||||
|
};
|
||||||
|
const handleDeleteClick = () => setIsDeleteDialogOpen(true);
|
||||||
|
const handleInfoClick = () => setIsInfoDialogOpen(true);
|
||||||
|
const handleDeleteConfirm = async () => {
|
||||||
|
await deleteProjects({ editor, ids: [project.id] });
|
||||||
|
setIsDeleteDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleCheckboxChange = ({
|
const handleCheckboxChange = ({
|
||||||
checked,
|
checked,
|
||||||
shiftKey,
|
shiftKey,
|
||||||
@@ -622,9 +644,8 @@ function ProjectItem({
|
|||||||
|
|
||||||
const listContent = (
|
const listContent = (
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-4 py-2 px-4 border-b border-border/50 ${
|
className={`flex items-center gap-4 py-2 px-4 border-b border-border/50 ${isSelected ? "bg-primary/5" : ""
|
||||||
isSelected ? "bg-primary/5" : ""
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
@@ -635,7 +656,7 @@ function ProjectItem({
|
|||||||
shiftKey: event.shiftKey,
|
shiftKey: event.shiftKey,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onCheckedChange={() => {}}
|
onCheckedChange={() => { }}
|
||||||
className="size-5 shrink-0"
|
className="size-5 shrink-0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -643,176 +664,71 @@ function ProjectItem({
|
|||||||
{listRowContent}
|
{listRowContent}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{!isMultiSelect && (
|
{!isMultiSelect && (
|
||||||
<ProjectMenu
|
<ProjectMenu
|
||||||
isOpen={isDropdownOpen}
|
isOpen={isDropdownOpen}
|
||||||
onOpenChange={setIsDropdownOpen}
|
onOpenChange={setIsDropdownOpen}
|
||||||
project={project}
|
variant="list"
|
||||||
variant="list"
|
onRenameClick={handleRename}
|
||||||
/>
|
onDuplicateClick={handleDuplicate}
|
||||||
)}
|
onDeleteClick={handleDeleteClick}
|
||||||
|
onInfoClick={handleInfoClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const cardContent = isGridView ? gridContent : listContent;
|
|
||||||
|
|
||||||
if (!isGridView) {
|
|
||||||
return <div className="group relative">{listContent}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="group relative">
|
|
||||||
<Link href={`/editor/${project.id}`} className="block">
|
|
||||||
{cardContent}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{isGridView && (
|
|
||||||
<>
|
|
||||||
<Checkbox
|
|
||||||
checked={isSelected}
|
|
||||||
onMouseDown={(event) => event.preventDefault()}
|
|
||||||
onClick={(event) => {
|
|
||||||
handleCheckboxChange({
|
|
||||||
checked: !isSelected,
|
|
||||||
shiftKey: event.shiftKey,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onCheckedChange={() => {}}
|
|
||||||
className={`absolute z-10 size-5 top-3 left-3 ${
|
|
||||||
isSelected || isDropdownOpen
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0 group-hover:opacity-100"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!isMultiSelect && (
|
|
||||||
<ProjectMenu
|
|
||||||
isOpen={isDropdownOpen}
|
|
||||||
onOpenChange={setIsDropdownOpen}
|
|
||||||
project={project}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProjectMenu({
|
|
||||||
isOpen,
|
|
||||||
onOpenChange,
|
|
||||||
project,
|
|
||||||
variant = "grid",
|
|
||||||
}: {
|
|
||||||
isOpen: boolean;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
project: TProjectMetadata;
|
|
||||||
variant?: "grid" | "list";
|
|
||||||
}) {
|
|
||||||
const editor = useEditor();
|
|
||||||
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
|
|
||||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
|
||||||
const [isInfoDialogOpen, setIsInfoDialogOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleMenuClick = ({
|
|
||||||
event,
|
|
||||||
}: {
|
|
||||||
event: MouseEvent<HTMLButtonElement>;
|
|
||||||
}) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMenuKeyDown = ({
|
|
||||||
event,
|
|
||||||
}: {
|
|
||||||
event: KeyboardEvent<HTMLButtonElement>;
|
|
||||||
}) => {
|
|
||||||
if (event.key !== "Enter" && event.key !== " ") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRename = () => {
|
|
||||||
setIsRenameDialogOpen(true);
|
|
||||||
onOpenChange(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDuplicate = async () => {
|
|
||||||
await duplicateProjects({ editor, ids: [project.id] });
|
|
||||||
onOpenChange(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteClick = () => {
|
|
||||||
setIsDeleteDialogOpen(true);
|
|
||||||
onOpenChange(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteConfirm = async () => {
|
|
||||||
await deleteProjects({ editor, ids: [project.id] });
|
|
||||||
setIsDeleteDialogOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInfoClick = () => {
|
|
||||||
setIsInfoDialogOpen(true);
|
|
||||||
onOpenChange(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isGrid = variant === "grid";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu open={isOpen} onOpenChange={onOpenChange}>
|
<ContextMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
<Button
|
<div className="group relative">
|
||||||
variant="background"
|
{isGridView ? (
|
||||||
className={
|
<>
|
||||||
isGrid
|
<Link href={`/editor/${project.id}`} className="block">
|
||||||
? `absolute z-10 top-3 right-3 ${isOpen ? "opacity-100" : "opacity-0 group-hover:opacity-100"}`
|
{gridContent}
|
||||||
: "!bg-transparent !shadow-none"
|
</Link>
|
||||||
}
|
|
||||||
size="icon"
|
<Checkbox
|
||||||
aria-label="Project menu"
|
checked={isSelected}
|
||||||
onClick={(event) =>
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
handleMenuClick({
|
onClick={(event) => {
|
||||||
event: event as unknown as MouseEvent<HTMLButtonElement>,
|
handleCheckboxChange({
|
||||||
})
|
checked: !isSelected,
|
||||||
}
|
shiftKey: event.shiftKey,
|
||||||
onMouseDown={(event) => event.stopPropagation()}
|
});
|
||||||
onKeyDown={(event) =>
|
}}
|
||||||
handleMenuKeyDown({
|
onCheckedChange={() => {}}
|
||||||
event: event as unknown as KeyboardEvent<HTMLButtonElement>,
|
className={`absolute z-10 size-5 top-3 left-3 ${
|
||||||
})
|
isSelected || isDropdownOpen
|
||||||
}
|
? "opacity-100"
|
||||||
>
|
: "opacity-0 group-hover:opacity-100"
|
||||||
<HugeiconsIcon
|
}`}
|
||||||
icon={MoreHorizontalIcon}
|
/>
|
||||||
className="text-foreground"
|
|
||||||
aria-hidden="true"
|
{!isMultiSelect && (
|
||||||
/>
|
<ProjectMenu
|
||||||
</Button>
|
isOpen={isDropdownOpen}
|
||||||
</DropdownMenuTrigger>
|
onOpenChange={setIsDropdownOpen}
|
||||||
<DropdownMenuContent className="w-48" align="end">
|
onRenameClick={handleRename}
|
||||||
<DropdownMenuItem onClick={handleRename}>
|
onDuplicateClick={handleDuplicate}
|
||||||
<HugeiconsIcon icon={Edit03Icon} />
|
onDeleteClick={handleDeleteClick}
|
||||||
Rename
|
onInfoClick={handleInfoClick}
|
||||||
</DropdownMenuItem>
|
/>
|
||||||
<DropdownMenuItem onClick={handleDuplicate}>
|
)}
|
||||||
<HugeiconsIcon icon={Copy01Icon} />
|
</>
|
||||||
Duplicate
|
) : (
|
||||||
</DropdownMenuItem>
|
listContent
|
||||||
<DropdownMenuItem onClick={handleInfoClick}>
|
)}
|
||||||
<HugeiconsIcon icon={InformationCircleIcon} />
|
</div>
|
||||||
Info
|
</ContextMenuTrigger>
|
||||||
</DropdownMenuItem>
|
<ProjectContextMenuContent
|
||||||
<DropdownMenuItem variant="destructive" onClick={handleDeleteClick}>
|
onRenameClick={handleRename}
|
||||||
<HugeiconsIcon icon={Delete02Icon} />
|
onDuplicateClick={handleDuplicate}
|
||||||
Delete
|
onDeleteClick={handleDeleteClick}
|
||||||
</DropdownMenuItem>
|
onInfoClick={handleInfoClick}
|
||||||
</DropdownMenuContent>
|
/>
|
||||||
</DropdownMenu>
|
</ContextMenu>
|
||||||
|
|
||||||
<RenameProjectDialog
|
<RenameProjectDialog
|
||||||
isOpen={isRenameDialogOpen}
|
isOpen={isRenameDialogOpen}
|
||||||
@@ -840,6 +756,162 @@ function ProjectMenu({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ProjectContextMenuContent({
|
||||||
|
onRenameClick,
|
||||||
|
onDuplicateClick,
|
||||||
|
onDeleteClick,
|
||||||
|
onInfoClick,
|
||||||
|
}: {
|
||||||
|
onRenameClick: () => void;
|
||||||
|
onDuplicateClick: () => void;
|
||||||
|
onDeleteClick: () => void;
|
||||||
|
onInfoClick: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ContextMenuContent>
|
||||||
|
<ContextMenuItem
|
||||||
|
icon={<HugeiconsIcon icon={Edit03Icon} />}
|
||||||
|
onClick={onRenameClick}
|
||||||
|
>
|
||||||
|
Rename
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
icon={<HugeiconsIcon icon={Copy01Icon} />}
|
||||||
|
onClick={onDuplicateClick}
|
||||||
|
>
|
||||||
|
Duplicate
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
icon={<HugeiconsIcon icon={InformationCircleIcon} />}
|
||||||
|
onClick={onInfoClick}
|
||||||
|
>
|
||||||
|
Info
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator />
|
||||||
|
<ContextMenuItem
|
||||||
|
variant="destructive"
|
||||||
|
icon={<HugeiconsIcon icon={Delete02Icon} />}
|
||||||
|
onClick={onDeleteClick}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ContextMenuContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProjectMenu({
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
variant = "grid",
|
||||||
|
onRenameClick,
|
||||||
|
onDuplicateClick,
|
||||||
|
onDeleteClick,
|
||||||
|
onInfoClick,
|
||||||
|
}: {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
variant?: "grid" | "list";
|
||||||
|
onRenameClick: () => void;
|
||||||
|
onDuplicateClick: () => void;
|
||||||
|
onDeleteClick: () => void;
|
||||||
|
onInfoClick: () => void;
|
||||||
|
}) {
|
||||||
|
const handleMenuClick = ({
|
||||||
|
event,
|
||||||
|
}: {
|
||||||
|
event: MouseEvent<HTMLButtonElement>;
|
||||||
|
}) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuKeyDown = ({
|
||||||
|
event,
|
||||||
|
}: {
|
||||||
|
event: KeyboardEvent<HTMLButtonElement>;
|
||||||
|
}) => {
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRename = () => {
|
||||||
|
onRenameClick();
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDuplicate = () => {
|
||||||
|
onDuplicateClick();
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteClick = () => {
|
||||||
|
onDeleteClick();
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInfoClick = () => {
|
||||||
|
onInfoClick();
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isGrid = variant === "grid";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu open={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="background"
|
||||||
|
className={
|
||||||
|
isGrid
|
||||||
|
? `absolute z-10 top-3 right-3 ${isOpen ? "opacity-100" : "opacity-0 group-hover:opacity-100"}`
|
||||||
|
: "!bg-transparent !shadow-none"
|
||||||
|
}
|
||||||
|
size="icon"
|
||||||
|
aria-label="Project menu"
|
||||||
|
onClick={(event) =>
|
||||||
|
handleMenuClick({
|
||||||
|
event: event as unknown as MouseEvent<HTMLButtonElement>,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
|
onKeyDown={(event) =>
|
||||||
|
handleMenuKeyDown({
|
||||||
|
event: event as unknown as KeyboardEvent<HTMLButtonElement>,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<HugeiconsIcon
|
||||||
|
icon={MoreHorizontalIcon}
|
||||||
|
className="text-foreground"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-48" align="end">
|
||||||
|
<DropdownMenuItem onClick={handleRename}>
|
||||||
|
<HugeiconsIcon icon={Edit03Icon} />
|
||||||
|
Rename
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={handleDuplicate}>
|
||||||
|
<HugeiconsIcon icon={Copy01Icon} />
|
||||||
|
Duplicate
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={handleInfoClick}>
|
||||||
|
<HugeiconsIcon icon={InformationCircleIcon} />
|
||||||
|
Info
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem variant="destructive" onClick={handleDeleteClick}>
|
||||||
|
<HugeiconsIcon icon={Delete02Icon} />
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ProjectsSkeleton() {
|
function ProjectsSkeleton() {
|
||||||
const skeletonIds = Array.from(
|
const skeletonIds = Array.from(
|
||||||
{ length: 24 },
|
{ length: 24 },
|
||||||
|
|||||||
Reference in New Issue
Block a user