mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
Merge pull request [#483](https://github.com/mazeincoding/AppCut/issues/483)
This commit is contained in:
@@ -211,7 +211,7 @@ export function PreviewPanel() {
|
|||||||
setDragState({
|
setDragState({
|
||||||
isDragging: true,
|
isDragging: true,
|
||||||
elementId: element.id,
|
elementId: element.id,
|
||||||
trackId: trackId,
|
trackId,
|
||||||
startX: e.clientX,
|
startX: e.clientX,
|
||||||
startY: e.clientY,
|
startY: e.clientY,
|
||||||
initialElementX: element.x,
|
initialElementX: element.x,
|
||||||
@@ -355,8 +355,22 @@ export function PreviewPanel() {
|
|||||||
handleTextMouseDown(e, element, elementData.track.id)
|
handleTextMouseDown(e, element, elementData.track.id)
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
left: `${50 + ((dragState.isDragging && dragState.elementId === element.id ? dragState.currentX : element.x) / canvasSize.width) * 100}%`,
|
left: `${
|
||||||
top: `${50 + ((dragState.isDragging && dragState.elementId === element.id ? dragState.currentY : element.y) / canvasSize.height) * 100}%`,
|
50 +
|
||||||
|
((dragState.isDragging && dragState.elementId === element.id
|
||||||
|
? dragState.currentX
|
||||||
|
: element.x) /
|
||||||
|
canvasSize.width) *
|
||||||
|
100
|
||||||
|
}%`,
|
||||||
|
top: `${
|
||||||
|
50 +
|
||||||
|
((dragState.isDragging && dragState.elementId === element.id
|
||||||
|
? dragState.currentY
|
||||||
|
: element.y) /
|
||||||
|
canvasSize.height) *
|
||||||
|
100
|
||||||
|
}%`,
|
||||||
transform: `translate(-50%, -50%) rotate(${element.rotation}deg) scale(${scaleRatio})`,
|
transform: `translate(-50%, -50%) rotate(${element.rotation}deg) scale(${scaleRatio})`,
|
||||||
opacity: element.opacity,
|
opacity: element.opacity,
|
||||||
zIndex: 100 + index, // Text elements on top
|
zIndex: 100 + index, // Text elements on top
|
||||||
|
|||||||
@@ -552,7 +552,7 @@ export function Timeline() {
|
|||||||
|
|
||||||
{/* Timeline Ruler */}
|
{/* Timeline Ruler */}
|
||||||
<div
|
<div
|
||||||
className="flex-1 relative overflow-hidden h-4"
|
className="flex-1 relative overflow-hidden h-10"
|
||||||
onWheel={(e) => {
|
onWheel={(e) => {
|
||||||
// Check if this is horizontal scrolling - if so, don't handle it here
|
// Check if this is horizontal scrolling - if so, don't handle it here
|
||||||
if (e.shiftKey || Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
|
if (e.shiftKey || Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
|
||||||
@@ -567,7 +567,7 @@ export function Timeline() {
|
|||||||
<ScrollArea className="w-full" ref={rulerScrollRef}>
|
<ScrollArea className="w-full" ref={rulerScrollRef}>
|
||||||
<div
|
<div
|
||||||
ref={rulerRef}
|
ref={rulerRef}
|
||||||
className="relative h-4 select-none cursor-default"
|
className="relative h-10 select-none cursor-default"
|
||||||
style={{
|
style={{
|
||||||
width: `${dynamicTimelineWidth}px`,
|
width: `${dynamicTimelineWidth}px`,
|
||||||
}}
|
}}
|
||||||
@@ -601,13 +601,17 @@ export function Timeline() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className={`absolute top-0 bottom-0 ${
|
className={`absolute top-0 h-4 ${
|
||||||
isMainMarker
|
isMainMarker
|
||||||
? "border-l border-muted-foreground/40"
|
? "border-l border-muted-foreground/40"
|
||||||
: "border-l border-muted-foreground/20"
|
: "border-l border-muted-foreground/20"
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
left: `${time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel}px`,
|
left: `${
|
||||||
|
time *
|
||||||
|
TIMELINE_CONSTANTS.PIXELS_PER_SECOND *
|
||||||
|
zoomLevel
|
||||||
|
}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -624,10 +628,16 @@ export function Timeline() {
|
|||||||
const secs = seconds % 60;
|
const secs = seconds % 60;
|
||||||
|
|
||||||
if (hours > 0) {
|
if (hours > 0) {
|
||||||
return `${hours}:${minutes.toString().padStart(2, "0")}:${Math.floor(secs).toString().padStart(2, "0")}`;
|
return `${hours}:${minutes
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}:${Math.floor(secs)
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}`;
|
||||||
}
|
}
|
||||||
if (minutes > 0) {
|
if (minutes > 0) {
|
||||||
return `${minutes}:${Math.floor(secs).toString().padStart(2, "0")}`;
|
return `${minutes}:${Math.floor(secs)
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}`;
|
||||||
}
|
}
|
||||||
if (interval >= 1) {
|
if (interval >= 1) {
|
||||||
return `${Math.floor(secs)}s`;
|
return `${Math.floor(secs)}s`;
|
||||||
@@ -710,7 +720,10 @@ export function Timeline() {
|
|||||||
<div
|
<div
|
||||||
className="relative flex-1"
|
className="relative flex-1"
|
||||||
style={{
|
style={{
|
||||||
height: `${Math.max(200, Math.min(800, getTotalTracksHeight(tracks)))}px`,
|
height: `${Math.max(
|
||||||
|
200,
|
||||||
|
Math.min(800, getTotalTracksHeight(tracks))
|
||||||
|
)}px`,
|
||||||
width: `${dynamicTimelineWidth}px`,
|
width: `${dynamicTimelineWidth}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -724,7 +737,10 @@ export function Timeline() {
|
|||||||
<div
|
<div
|
||||||
className="absolute left-0 right-0 border-b border-muted/30 py-[0.05rem]"
|
className="absolute left-0 right-0 border-b border-muted/30 py-[0.05rem]"
|
||||||
style={{
|
style={{
|
||||||
top: `${getCumulativeHeightBefore(tracks, index)}px`,
|
top: `${getCumulativeHeightBefore(
|
||||||
|
tracks,
|
||||||
|
index
|
||||||
|
)}px`,
|
||||||
height: `${getTrackHeight(track.type)}px`,
|
height: `${getTrackHeight(track.type)}px`,
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -1092,7 +1108,9 @@ function TimelineToolbar({
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button variant="text" size="icon" onClick={toggleRippleEditing}>
|
<Button variant="text" size="icon" onClick={toggleRippleEditing}>
|
||||||
<Link
|
<Link
|
||||||
className={`h-4 w-4 ${rippleEditingEnabled ? "text-primary" : ""}`}
|
className={`h-4 w-4 ${
|
||||||
|
rippleEditingEnabled ? "text-primary" : ""
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export function Handlebars({ children }: HandlebarsProps) {
|
|||||||
whileDrag={{ scale: 1.1, cursor: "grabbing" }}
|
whileDrag={{ scale: 1.1, cursor: "grabbing" }}
|
||||||
transition={{ type: "spring", stiffness: 400, damping: 30 }}
|
transition={{ type: "spring", stiffness: 400, damping: 30 }}
|
||||||
>
|
>
|
||||||
<div className="w-2 h-8 rounded-full bg-yellow-500"></div>
|
<div className="w-2 h-8 rounded-full bg-yellow-500" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -81,7 +81,7 @@ export function Handlebars({ children }: HandlebarsProps) {
|
|||||||
whileDrag={{ scale: 1.1, cursor: "grabbing" }}
|
whileDrag={{ scale: 1.1, cursor: "grabbing" }}
|
||||||
transition={{ type: "spring", stiffness: 400, damping: 30 }}
|
transition={{ type: "spring", stiffness: 400, damping: 30 }}
|
||||||
>
|
>
|
||||||
<div className="w-2 h-8 rounded-full bg-yellow-500"></div>
|
<div className="w-2 h-8 rounded-full bg-yellow-500" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -26,57 +26,99 @@ export function useEditorActions() {
|
|||||||
const { activeProject } = useProjectStore();
|
const { activeProject } = useProjectStore();
|
||||||
|
|
||||||
// Playback actions
|
// Playback actions
|
||||||
useActionHandler("toggle-play", () => {
|
useActionHandler(
|
||||||
|
"toggle-play",
|
||||||
|
() => {
|
||||||
toggle();
|
toggle();
|
||||||
}, undefined);
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
useActionHandler("stop-playback", () => {
|
useActionHandler(
|
||||||
|
"stop-playback",
|
||||||
|
() => {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
toggle();
|
toggle();
|
||||||
}
|
}
|
||||||
seek(0);
|
seek(0);
|
||||||
}, undefined);
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
useActionHandler("seek-forward", (args) => {
|
useActionHandler(
|
||||||
|
"seek-forward",
|
||||||
|
(args) => {
|
||||||
const seconds = args?.seconds ?? 1;
|
const seconds = args?.seconds ?? 1;
|
||||||
seek(Math.min(duration, currentTime + seconds));
|
seek(Math.min(duration, currentTime + seconds));
|
||||||
}, undefined);
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
useActionHandler("seek-backward", (args) => {
|
useActionHandler(
|
||||||
|
"seek-backward",
|
||||||
|
(args) => {
|
||||||
const seconds = args?.seconds ?? 1;
|
const seconds = args?.seconds ?? 1;
|
||||||
seek(Math.max(0, currentTime - seconds));
|
seek(Math.max(0, currentTime - seconds));
|
||||||
}, undefined);
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
useActionHandler("frame-step-forward", () => {
|
useActionHandler(
|
||||||
|
"frame-step-forward",
|
||||||
|
() => {
|
||||||
const projectFps = activeProject?.fps || 30;
|
const projectFps = activeProject?.fps || 30;
|
||||||
seek(Math.min(duration, currentTime + 1 / projectFps));
|
seek(Math.min(duration, currentTime + 1 / projectFps));
|
||||||
}, undefined);
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
useActionHandler("frame-step-backward", () => {
|
useActionHandler(
|
||||||
|
"frame-step-backward",
|
||||||
|
() => {
|
||||||
const projectFps = activeProject?.fps || 30;
|
const projectFps = activeProject?.fps || 30;
|
||||||
seek(Math.max(0, currentTime - 1 / projectFps));
|
seek(Math.max(0, currentTime - 1 / projectFps));
|
||||||
}, undefined);
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
useActionHandler("jump-forward", (args) => {
|
useActionHandler(
|
||||||
|
"jump-forward",
|
||||||
|
(args) => {
|
||||||
const seconds = args?.seconds ?? 5;
|
const seconds = args?.seconds ?? 5;
|
||||||
seek(Math.min(duration, currentTime + seconds));
|
seek(Math.min(duration, currentTime + seconds));
|
||||||
}, undefined);
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
useActionHandler("jump-backward", (args) => {
|
useActionHandler(
|
||||||
|
"jump-backward",
|
||||||
|
(args) => {
|
||||||
const seconds = args?.seconds ?? 5;
|
const seconds = args?.seconds ?? 5;
|
||||||
seek(Math.max(0, currentTime - seconds));
|
seek(Math.max(0, currentTime - seconds));
|
||||||
}, undefined);
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
useActionHandler("goto-start", () => {
|
useActionHandler(
|
||||||
|
"goto-start",
|
||||||
|
() => {
|
||||||
seek(0);
|
seek(0);
|
||||||
}, undefined);
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
useActionHandler("goto-end", () => {
|
useActionHandler(
|
||||||
|
"goto-end",
|
||||||
|
() => {
|
||||||
seek(duration);
|
seek(duration);
|
||||||
}, undefined);
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
// Timeline editing actions
|
// Timeline editing actions
|
||||||
useActionHandler("split-element", () => {
|
useActionHandler(
|
||||||
|
"split-element",
|
||||||
|
() => {
|
||||||
if (selectedElements.length !== 1) {
|
if (selectedElements.length !== 1) {
|
||||||
toast.error("Select exactly one element to split");
|
toast.error("Select exactly one element to split");
|
||||||
return;
|
return;
|
||||||
@@ -98,9 +140,13 @@ export function useEditorActions() {
|
|||||||
toast.error("Playhead must be within selected element");
|
toast.error("Playhead must be within selected element");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, undefined);
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
useActionHandler("delete-selected", () => {
|
useActionHandler(
|
||||||
|
"delete-selected",
|
||||||
|
() => {
|
||||||
if (selectedElements.length === 0) {
|
if (selectedElements.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -110,9 +156,13 @@ export function useEditorActions() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
clearSelectedElements();
|
clearSelectedElements();
|
||||||
}, undefined);
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
useActionHandler("select-all", () => {
|
useActionHandler(
|
||||||
|
"select-all",
|
||||||
|
() => {
|
||||||
const allElements = tracks.flatMap((track: any) =>
|
const allElements = tracks.flatMap((track: any) =>
|
||||||
track.elements.map((element: any) => ({
|
track.elements.map((element: any) => ({
|
||||||
trackId: track.id,
|
trackId: track.id,
|
||||||
@@ -120,9 +170,13 @@ export function useEditorActions() {
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
setSelectedElements(allElements);
|
setSelectedElements(allElements);
|
||||||
}, undefined);
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
useActionHandler("duplicate-selected", () => {
|
useActionHandler(
|
||||||
|
"duplicate-selected",
|
||||||
|
() => {
|
||||||
if (selectedElements.length !== 1) {
|
if (selectedElements.length !== 1) {
|
||||||
toast.error("Select exactly one element to duplicate");
|
toast.error("Select exactly one element to duplicate");
|
||||||
return;
|
return;
|
||||||
@@ -144,18 +198,32 @@ export function useEditorActions() {
|
|||||||
startTime: newStartTime,
|
startTime: newStartTime,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, undefined);
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
useActionHandler("toggle-snapping", () => {
|
useActionHandler(
|
||||||
|
"toggle-snapping",
|
||||||
|
() => {
|
||||||
toggleSnapping();
|
toggleSnapping();
|
||||||
}, undefined);
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
// History actions
|
// History actions
|
||||||
useActionHandler("undo", () => {
|
useActionHandler(
|
||||||
|
"undo",
|
||||||
|
() => {
|
||||||
undo();
|
undo();
|
||||||
}, undefined);
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
useActionHandler("redo", () => {
|
useActionHandler(
|
||||||
|
"redo",
|
||||||
|
() => {
|
||||||
redo();
|
redo();
|
||||||
}, undefined);
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user