mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
Merge branch 'staging'
This commit is contained in:
@@ -4,15 +4,14 @@ import { TabBar } from "./tabbar";
|
|||||||
import { MediaView } from "./views/media";
|
import { MediaView } from "./views/media";
|
||||||
import { useMediaPanelStore, Tab } from "./store";
|
import { useMediaPanelStore, Tab } from "./store";
|
||||||
import { TextView } from "./views/text";
|
import { TextView } from "./views/text";
|
||||||
|
import { AudioView } from "./views/audio";
|
||||||
|
|
||||||
export function MediaPanel() {
|
export function MediaPanel() {
|
||||||
const { activeTab } = useMediaPanelStore();
|
const { activeTab } = useMediaPanelStore();
|
||||||
|
|
||||||
const viewMap: Record<Tab, React.ReactNode> = {
|
const viewMap: Record<Tab, React.ReactNode> = {
|
||||||
media: <MediaView />,
|
media: <MediaView />,
|
||||||
audio: (
|
audio: <AudioView />,
|
||||||
<div className="p-4 text-muted-foreground">Audio view coming soon...</div>
|
|
||||||
),
|
|
||||||
text: <TextView />,
|
text: <TextView />,
|
||||||
stickers: (
|
stickers: (
|
||||||
<div className="p-4 text-muted-foreground">
|
<div className="p-4 text-muted-foreground">
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function AudioView() {
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col gap-2 p-4">
|
||||||
|
<Input
|
||||||
|
placeholder="Search songs and artists"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-2"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -208,10 +208,10 @@ export function MediaView() {
|
|||||||
{/* Search and filter controls */}
|
{/* Search and filter controls */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Select value={mediaFilter} onValueChange={setMediaFilter}>
|
<Select value={mediaFilter} onValueChange={setMediaFilter}>
|
||||||
<SelectTrigger className="w-[80px] h-full text-xs">
|
<SelectTrigger className="w-[80px] h-9 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="">
|
<SelectContent>
|
||||||
<SelectItem value="all">All</SelectItem>
|
<SelectItem value="all">All</SelectItem>
|
||||||
<SelectItem value="video">Video</SelectItem>
|
<SelectItem value="video">Video</SelectItem>
|
||||||
<SelectItem value="audio">Audio</SelectItem>
|
<SelectItem value="audio">Audio</SelectItem>
|
||||||
@@ -221,7 +221,7 @@ export function MediaView() {
|
|||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search media..."
|
placeholder="Search media..."
|
||||||
className="min-w-[60px] flex-1 h-full text-xs"
|
className="min-w-[60px] flex-1 h-9 text-xs"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -230,7 +230,7 @@ export function MediaView() {
|
|||||||
size="lg"
|
size="lg"
|
||||||
onClick={handleFileSelect}
|
onClick={handleFileSelect}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
className="flex-none bg-transparent min-w-[30px] whitespace-nowrap overflow-hidden px-2 justify-center items-center"
|
className="flex-none bg-transparent min-w-[30px] whitespace-nowrap overflow-hidden px-2 justify-center items-center h-9"
|
||||||
>
|
>
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
|||||||
@@ -433,7 +433,6 @@ export function Timeline() {
|
|||||||
// Action handlers for toolbar
|
// Action handlers for toolbar
|
||||||
const handleSplitSelected = () => {
|
const handleSplitSelected = () => {
|
||||||
if (selectedElements.length === 0) {
|
if (selectedElements.length === 0) {
|
||||||
toast.error("No elements selected");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let splitCount = 0;
|
let splitCount = 0;
|
||||||
@@ -459,7 +458,6 @@ export function Timeline() {
|
|||||||
|
|
||||||
const handleDuplicateSelected = () => {
|
const handleDuplicateSelected = () => {
|
||||||
if (selectedElements.length === 0) {
|
if (selectedElements.length === 0) {
|
||||||
toast.error("No elements selected");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const canDuplicate = selectedElements.length === 1;
|
const canDuplicate = selectedElements.length === 1;
|
||||||
@@ -553,7 +551,6 @@ export function Timeline() {
|
|||||||
|
|
||||||
const handleDeleteSelected = () => {
|
const handleDeleteSelected = () => {
|
||||||
if (selectedElements.length === 0) {
|
if (selectedElements.length === 0) {
|
||||||
toast.error("No elements selected");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
selectedElements.forEach(({ trackId, elementId }) => {
|
selectedElements.forEach(({ trackId, elementId }) => {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function Hero() {
|
|||||||
className="mb-8 flex justify-center"
|
className="mb-8 flex justify-center"
|
||||||
>
|
>
|
||||||
<SponsorButton
|
<SponsorButton
|
||||||
href="https://vercel.com/?utm_source=opencut"
|
href="https://vercel.com/home?utm_source=opencut"
|
||||||
logo={VercelIcon}
|
logo={VercelIcon}
|
||||||
companyName="Vercel"
|
companyName="Vercel"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ export function useEditorActions() {
|
|||||||
|
|
||||||
useActionHandler("delete-selected", () => {
|
useActionHandler("delete-selected", () => {
|
||||||
if (selectedElements.length === 0) {
|
if (selectedElements.length === 0) {
|
||||||
toast.error("No elements selected");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
selectedElements.forEach(
|
selectedElements.forEach(
|
||||||
|
|||||||
@@ -29,7 +29,12 @@ export function useTimelineElementResize({
|
|||||||
}: UseTimelineElementResizeProps) {
|
}: UseTimelineElementResizeProps) {
|
||||||
const [resizing, setResizing] = useState<ResizeState | null>(null);
|
const [resizing, setResizing] = useState<ResizeState | null>(null);
|
||||||
const { mediaItems } = useMediaStore();
|
const { mediaItems } = useMediaStore();
|
||||||
const { updateElementStartTime } = useTimelineStore();
|
const {
|
||||||
|
updateElementStartTime,
|
||||||
|
updateElementTrim,
|
||||||
|
updateElementDuration,
|
||||||
|
pushHistory,
|
||||||
|
} = useTimelineStore();
|
||||||
|
|
||||||
// Set up document-level mouse listeners during resize (like proper drag behavior)
|
// Set up document-level mouse listeners during resize (like proper drag behavior)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -61,6 +66,9 @@ export function useTimelineElementResize({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Push history once at the start of the resize operation
|
||||||
|
pushHistory();
|
||||||
|
|
||||||
setResizing({
|
setResizing({
|
||||||
elementId,
|
elementId,
|
||||||
side,
|
side,
|
||||||
@@ -112,13 +120,14 @@ export function useTimelineElementResize({
|
|||||||
const trimDelta = newTrimStart - resizing.initialTrimStart;
|
const trimDelta = newTrimStart - resizing.initialTrimStart;
|
||||||
const newStartTime = element.startTime + trimDelta;
|
const newStartTime = element.startTime + trimDelta;
|
||||||
|
|
||||||
onUpdateTrim(
|
updateElementTrim(
|
||||||
track.id,
|
track.id,
|
||||||
element.id,
|
element.id,
|
||||||
newTrimStart,
|
newTrimStart,
|
||||||
resizing.initialTrimEnd
|
resizing.initialTrimEnd,
|
||||||
|
false
|
||||||
);
|
);
|
||||||
updateElementStartTime(track.id, element.id, newStartTime);
|
updateElementStartTime(track.id, element.id, newStartTime, false);
|
||||||
} else {
|
} else {
|
||||||
// Trying to extend beyond trimStart = 0
|
// Trying to extend beyond trimStart = 0
|
||||||
if (canExtendElementDuration()) {
|
if (canExtendElementDuration()) {
|
||||||
@@ -128,22 +137,29 @@ export function useTimelineElementResize({
|
|||||||
const newDuration = element.duration + extensionAmount;
|
const newDuration = element.duration + extensionAmount;
|
||||||
|
|
||||||
// Keep trimStart at 0 and extend the element
|
// Keep trimStart at 0 and extend the element
|
||||||
onUpdateTrim(track.id, element.id, 0, resizing.initialTrimEnd);
|
updateElementTrim(
|
||||||
onUpdateDuration(track.id, element.id, newDuration);
|
track.id,
|
||||||
updateElementStartTime(track.id, element.id, newStartTime);
|
element.id,
|
||||||
|
0,
|
||||||
|
resizing.initialTrimEnd,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
updateElementDuration(track.id, element.id, newDuration, false);
|
||||||
|
updateElementStartTime(track.id, element.id, newStartTime, false);
|
||||||
} else {
|
} else {
|
||||||
// Video/Audio: can't extend beyond original content - limit to trimStart = 0
|
// Video/Audio: can't extend beyond original content - limit to trimStart = 0
|
||||||
const newTrimStart = 0;
|
const newTrimStart = 0;
|
||||||
const trimDelta = newTrimStart - resizing.initialTrimStart;
|
const trimDelta = newTrimStart - resizing.initialTrimStart;
|
||||||
const newStartTime = element.startTime + trimDelta;
|
const newStartTime = element.startTime + trimDelta;
|
||||||
|
|
||||||
onUpdateTrim(
|
updateElementTrim(
|
||||||
track.id,
|
track.id,
|
||||||
element.id,
|
element.id,
|
||||||
newTrimStart,
|
newTrimStart,
|
||||||
resizing.initialTrimEnd
|
resizing.initialTrimEnd,
|
||||||
|
false
|
||||||
);
|
);
|
||||||
updateElementStartTime(track.id, element.id, newStartTime);
|
updateElementStartTime(track.id, element.id, newStartTime, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -159,27 +175,35 @@ export function useTimelineElementResize({
|
|||||||
const newTrimEnd = 0; // Reset trimEnd to 0 since we're extending
|
const newTrimEnd = 0; // Reset trimEnd to 0 since we're extending
|
||||||
|
|
||||||
// Update duration first, then trim
|
// Update duration first, then trim
|
||||||
onUpdateDuration(track.id, element.id, newDuration);
|
updateElementDuration(track.id, element.id, newDuration, false);
|
||||||
onUpdateTrim(
|
updateElementTrim(
|
||||||
track.id,
|
track.id,
|
||||||
element.id,
|
element.id,
|
||||||
resizing.initialTrimStart,
|
resizing.initialTrimStart,
|
||||||
newTrimEnd
|
newTrimEnd,
|
||||||
|
false
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Can't extend - just set trimEnd to 0 (maximum possible extension)
|
// Can't extend - just set trimEnd to 0 (maximum possible extension)
|
||||||
onUpdateTrim(track.id, element.id, resizing.initialTrimStart, 0);
|
updateElementTrim(
|
||||||
|
track.id,
|
||||||
|
element.id,
|
||||||
|
resizing.initialTrimStart,
|
||||||
|
0,
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Normal trimming within original duration
|
// Normal trimming within original duration
|
||||||
const maxTrimEnd = element.duration - resizing.initialTrimStart - 0.1; // Leave at least 0.1s visible
|
const maxTrimEnd = element.duration - resizing.initialTrimStart - 0.1; // Leave at least 0.1s visible
|
||||||
const newTrimEnd = Math.max(0, Math.min(maxTrimEnd, calculated));
|
const newTrimEnd = Math.max(0, Math.min(maxTrimEnd, calculated));
|
||||||
|
|
||||||
onUpdateTrim(
|
updateElementTrim(
|
||||||
track.id,
|
track.id,
|
||||||
element.id,
|
element.id,
|
||||||
resizing.initialTrimStart,
|
resizing.initialTrimStart,
|
||||||
newTrimEnd
|
newTrimEnd,
|
||||||
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
|
|||||||
function getPressedKey(ev: KeyboardEvent): string | null {
|
function getPressedKey(ev: KeyboardEvent): string | null {
|
||||||
// Sometimes the property code is not available on the KeyboardEvent object
|
// Sometimes the property code is not available on the KeyboardEvent object
|
||||||
const key = (ev.key ?? "").toLowerCase();
|
const key = (ev.key ?? "").toLowerCase();
|
||||||
|
const code = ev.code ?? "";
|
||||||
|
|
||||||
// Check arrow keys
|
// Check arrow keys
|
||||||
if (key.startsWith("arrow")) {
|
if (key.startsWith("arrow")) {
|
||||||
@@ -213,7 +214,15 @@ function getPressedKey(ev: KeyboardEvent): string | null {
|
|||||||
const isLetter = key.length === 1 && key >= "a" && key <= "z";
|
const isLetter = key.length === 1 && key >= "a" && key <= "z";
|
||||||
if (isLetter) return key;
|
if (isLetter) return key;
|
||||||
|
|
||||||
// Check if number keys
|
// Check number keys using physical position for AZERTY support
|
||||||
|
if (code.startsWith("Digit")) {
|
||||||
|
const digit = code.slice(5);
|
||||||
|
if (digit.length === 1 && digit >= "0" && digit <= "9") {
|
||||||
|
return digit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for other layouts
|
||||||
const isDigit = key.length === 1 && key >= "0" && key <= "9";
|
const isDigit = key.length === 1 && key >= "0" && key <= "9";
|
||||||
if (isDigit) return key;
|
if (isDigit) return key;
|
||||||
|
|
||||||
|
|||||||
@@ -106,17 +106,20 @@ interface TimelineStore {
|
|||||||
trackId: string,
|
trackId: string,
|
||||||
elementId: string,
|
elementId: string,
|
||||||
trimStart: number,
|
trimStart: number,
|
||||||
trimEnd: number
|
trimEnd: number,
|
||||||
|
pushHistory?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
updateElementDuration: (
|
updateElementDuration: (
|
||||||
trackId: string,
|
trackId: string,
|
||||||
elementId: string,
|
elementId: string,
|
||||||
duration: number
|
duration: number,
|
||||||
|
pushHistory?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
updateElementStartTime: (
|
updateElementStartTime: (
|
||||||
trackId: string,
|
trackId: string,
|
||||||
elementId: string,
|
elementId: string,
|
||||||
startTime: number
|
startTime: number,
|
||||||
|
pushHistory?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
toggleTrackMute: (trackId: string) => void;
|
toggleTrackMute: (trackId: string) => void;
|
||||||
|
|
||||||
@@ -687,8 +690,14 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||||||
updateTracksAndSave(newTracks);
|
updateTracksAndSave(newTracks);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateElementTrim: (trackId, elementId, trimStart, trimEnd) => {
|
updateElementTrim: (
|
||||||
get().pushHistory();
|
trackId,
|
||||||
|
elementId,
|
||||||
|
trimStart,
|
||||||
|
trimEnd,
|
||||||
|
pushHistory = true
|
||||||
|
) => {
|
||||||
|
if (pushHistory) get().pushHistory();
|
||||||
updateTracksAndSave(
|
updateTracksAndSave(
|
||||||
get()._tracks.map((track) =>
|
get()._tracks.map((track) =>
|
||||||
track.id === trackId
|
track.id === trackId
|
||||||
@@ -705,8 +714,13 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateElementDuration: (trackId, elementId, duration) => {
|
updateElementDuration: (
|
||||||
get().pushHistory();
|
trackId,
|
||||||
|
elementId,
|
||||||
|
duration,
|
||||||
|
pushHistory = true
|
||||||
|
) => {
|
||||||
|
if (pushHistory) get().pushHistory();
|
||||||
updateTracksAndSave(
|
updateTracksAndSave(
|
||||||
get()._tracks.map((track) =>
|
get()._tracks.map((track) =>
|
||||||
track.id === trackId
|
track.id === trackId
|
||||||
@@ -721,8 +735,13 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateElementStartTime: (trackId, elementId, startTime) => {
|
updateElementStartTime: (
|
||||||
get().pushHistory();
|
trackId,
|
||||||
|
elementId,
|
||||||
|
startTime,
|
||||||
|
pushHistory = true
|
||||||
|
) => {
|
||||||
|
if (pushHistory) get().pushHistory();
|
||||||
updateTracksAndSave(
|
updateTracksAndSave(
|
||||||
get()._tracks.map((track) =>
|
get()._tracks.map((track) =>
|
||||||
track.id === trackId
|
track.id === trackId
|
||||||
|
|||||||
Reference in New Issue
Block a user