summaryrefslogtreecommitdiff
path: root/apps/web
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2025-07-21 18:16:54 +0200
committerMaze Winther <mazewinther@gmail.com>2025-07-21 18:16:54 +0200
commitfb51cc2597b7f167eddf74125b28d2433b96052f (patch)
treef6a11eccf476762dcecf29d28af3cc01a6238a97 /apps/web
parent263f14ef2a83b0cc78189f764b0c1859fa392539 (diff)
parentad771a0b56f84586cb0a8e8f7a0cbdd786198c08 (diff)
downloadopencut-fb51cc2597b7f167eddf74125b28d2433b96052f.tar.gz
Merge branch 'staging'
Diffstat (limited to 'apps/web')
-rw-r--r--apps/web/src/components/editor/media-panel/index.tsx5
-rw-r--r--apps/web/src/components/editor/media-panel/views/audio.tsx18
-rw-r--r--apps/web/src/components/editor/media-panel/views/media.tsx8
-rw-r--r--apps/web/src/components/editor/timeline.tsx3
-rw-r--r--apps/web/src/components/landing/hero.tsx2
-rw-r--r--apps/web/src/hooks/use-editor-actions.ts1
-rw-r--r--apps/web/src/hooks/use-timeline-element-resize.ts56
-rw-r--r--apps/web/src/stores/keybindings-store.ts11
-rw-r--r--apps/web/src/stores/timeline-store.ts37
9 files changed, 103 insertions, 38 deletions
diff --git a/apps/web/src/components/editor/media-panel/index.tsx b/apps/web/src/components/editor/media-panel/index.tsx
index 9f7322c7..90ac84f0 100644
--- a/apps/web/src/components/editor/media-panel/index.tsx
+++ b/apps/web/src/components/editor/media-panel/index.tsx
@@ -4,15 +4,14 @@ import { TabBar } from "./tabbar";
import { MediaView } from "./views/media";
import { useMediaPanelStore, Tab } from "./store";
import { TextView } from "./views/text";
+import { AudioView } from "./views/audio";
export function MediaPanel() {
const { activeTab } = useMediaPanelStore();
const viewMap: Record<Tab, React.ReactNode> = {
media: <MediaView />,
- audio: (
- <div className="p-4 text-muted-foreground">Audio view coming soon...</div>
- ),
+ audio: <AudioView />,
text: <TextView />,
stickers: (
<div className="p-4 text-muted-foreground">
diff --git a/apps/web/src/components/editor/media-panel/views/audio.tsx b/apps/web/src/components/editor/media-panel/views/audio.tsx
new file mode 100644
index 00000000..7ddf465c
--- /dev/null
+++ b/apps/web/src/components/editor/media-panel/views/audio.tsx
@@ -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>
+ );
+}
diff --git a/apps/web/src/components/editor/media-panel/views/media.tsx b/apps/web/src/components/editor/media-panel/views/media.tsx
index 631fe39a..0189720f 100644
--- a/apps/web/src/components/editor/media-panel/views/media.tsx
+++ b/apps/web/src/components/editor/media-panel/views/media.tsx
@@ -208,10 +208,10 @@ export function MediaView() {
{/* Search and filter controls */}
<div className="flex gap-2">
<Select value={mediaFilter} onValueChange={setMediaFilter}>
- <SelectTrigger className="w-[80px] h-full text-xs">
+ <SelectTrigger className="w-[80px] h-9 text-xs">
<SelectValue />
</SelectTrigger>
- <SelectContent className="">
+ <SelectContent>
<SelectItem value="all">All</SelectItem>
<SelectItem value="video">Video</SelectItem>
<SelectItem value="audio">Audio</SelectItem>
@@ -221,7 +221,7 @@ export function MediaView() {
<Input
type="text"
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}
onChange={(e) => setSearchQuery(e.target.value)}
/>
@@ -230,7 +230,7 @@ export function MediaView() {
size="lg"
onClick={handleFileSelect}
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 ? (
<Loader2 className="h-4 w-4 animate-spin" />
diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx
index 9e68fd4d..6e8c453c 100644
--- a/apps/web/src/components/editor/timeline.tsx
+++ b/apps/web/src/components/editor/timeline.tsx
@@ -433,7 +433,6 @@ export function Timeline() {
// Action handlers for toolbar
const handleSplitSelected = () => {
if (selectedElements.length === 0) {
- toast.error("No elements selected");
return;
}
let splitCount = 0;
@@ -459,7 +458,6 @@ export function Timeline() {
const handleDuplicateSelected = () => {
if (selectedElements.length === 0) {
- toast.error("No elements selected");
return;
}
const canDuplicate = selectedElements.length === 1;
@@ -553,7 +551,6 @@ export function Timeline() {
const handleDeleteSelected = () => {
if (selectedElements.length === 0) {
- toast.error("No elements selected");
return;
}
selectedElements.forEach(({ trackId, elementId }) => {
diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx
index b905bbde..dba958ac 100644
--- a/apps/web/src/components/landing/hero.tsx
+++ b/apps/web/src/components/landing/hero.tsx
@@ -33,7 +33,7 @@ export function Hero() {
className="mb-8 flex justify-center"
>
<SponsorButton
- href="https://vercel.com/?utm_source=opencut"
+ href="https://vercel.com/home?utm_source=opencut"
logo={VercelIcon}
companyName="Vercel"
/>
diff --git a/apps/web/src/hooks/use-editor-actions.ts b/apps/web/src/hooks/use-editor-actions.ts
index e0d9b9eb..4e8f6aef 100644
--- a/apps/web/src/hooks/use-editor-actions.ts
+++ b/apps/web/src/hooks/use-editor-actions.ts
@@ -102,7 +102,6 @@ export function useEditorActions() {
useActionHandler("delete-selected", () => {
if (selectedElements.length === 0) {
- toast.error("No elements selected");
return;
}
selectedElements.forEach(
diff --git a/apps/web/src/hooks/use-timeline-element-resize.ts b/apps/web/src/hooks/use-timeline-element-resize.ts
index f16ca8e2..3afa637b 100644
--- a/apps/web/src/hooks/use-timeline-element-resize.ts
+++ b/apps/web/src/hooks/use-timeline-element-resize.ts
@@ -29,7 +29,12 @@ export function useTimelineElementResize({
}: UseTimelineElementResizeProps) {
const [resizing, setResizing] = useState<ResizeState | null>(null);
const { mediaItems } = useMediaStore();
- const { updateElementStartTime } = useTimelineStore();
+ const {
+ updateElementStartTime,
+ updateElementTrim,
+ updateElementDuration,
+ pushHistory,
+ } = useTimelineStore();
// Set up document-level mouse listeners during resize (like proper drag behavior)
useEffect(() => {
@@ -61,6 +66,9 @@ export function useTimelineElementResize({
e.stopPropagation();
e.preventDefault();
+ // Push history once at the start of the resize operation
+ pushHistory();
+
setResizing({
elementId,
side,
@@ -112,13 +120,14 @@ export function useTimelineElementResize({
const trimDelta = newTrimStart - resizing.initialTrimStart;
const newStartTime = element.startTime + trimDelta;
- onUpdateTrim(
+ updateElementTrim(
track.id,
element.id,
newTrimStart,
- resizing.initialTrimEnd
+ resizing.initialTrimEnd,
+ false
);
- updateElementStartTime(track.id, element.id, newStartTime);
+ updateElementStartTime(track.id, element.id, newStartTime, false);
} else {
// Trying to extend beyond trimStart = 0
if (canExtendElementDuration()) {
@@ -128,22 +137,29 @@ export function useTimelineElementResize({
const newDuration = element.duration + extensionAmount;
// Keep trimStart at 0 and extend the element
- onUpdateTrim(track.id, element.id, 0, resizing.initialTrimEnd);
- onUpdateDuration(track.id, element.id, newDuration);
- updateElementStartTime(track.id, element.id, newStartTime);
+ updateElementTrim(
+ track.id,
+ element.id,
+ 0,
+ resizing.initialTrimEnd,
+ false
+ );
+ updateElementDuration(track.id, element.id, newDuration, false);
+ updateElementStartTime(track.id, element.id, newStartTime, false);
} else {
// Video/Audio: can't extend beyond original content - limit to trimStart = 0
const newTrimStart = 0;
const trimDelta = newTrimStart - resizing.initialTrimStart;
const newStartTime = element.startTime + trimDelta;
- onUpdateTrim(
+ updateElementTrim(
track.id,
element.id,
newTrimStart,
- resizing.initialTrimEnd
+ resizing.initialTrimEnd,
+ false
);
- updateElementStartTime(track.id, element.id, newStartTime);
+ updateElementStartTime(track.id, element.id, newStartTime, false);
}
}
} else {
@@ -159,27 +175,35 @@ export function useTimelineElementResize({
const newTrimEnd = 0; // Reset trimEnd to 0 since we're extending
// Update duration first, then trim
- onUpdateDuration(track.id, element.id, newDuration);
- onUpdateTrim(
+ updateElementDuration(track.id, element.id, newDuration, false);
+ updateElementTrim(
track.id,
element.id,
resizing.initialTrimStart,
- newTrimEnd
+ newTrimEnd,
+ false
);
} else {
// 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 {
// Normal trimming within original duration
const maxTrimEnd = element.duration - resizing.initialTrimStart - 0.1; // Leave at least 0.1s visible
const newTrimEnd = Math.max(0, Math.min(maxTrimEnd, calculated));
- onUpdateTrim(
+ updateElementTrim(
track.id,
element.id,
resizing.initialTrimStart,
- newTrimEnd
+ newTrimEnd,
+ false
);
}
}
diff --git a/apps/web/src/stores/keybindings-store.ts b/apps/web/src/stores/keybindings-store.ts
index 60c71afd..3e525b86 100644
--- a/apps/web/src/stores/keybindings-store.ts
+++ b/apps/web/src/stores/keybindings-store.ts
@@ -195,6 +195,7 @@ function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
function getPressedKey(ev: KeyboardEvent): string | null {
// Sometimes the property code is not available on the KeyboardEvent object
const key = (ev.key ?? "").toLowerCase();
+ const code = ev.code ?? "";
// Check arrow keys
if (key.startsWith("arrow")) {
@@ -213,7 +214,15 @@ function getPressedKey(ev: KeyboardEvent): string | null {
const isLetter = key.length === 1 && key >= "a" && key <= "z";
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";
if (isDigit) return key;
diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts
index 1fcfc60e..01ad1635 100644
--- a/apps/web/src/stores/timeline-store.ts
+++ b/apps/web/src/stores/timeline-store.ts
@@ -106,17 +106,20 @@ interface TimelineStore {
trackId: string,
elementId: string,
trimStart: number,
- trimEnd: number
+ trimEnd: number,
+ pushHistory?: boolean
) => void;
updateElementDuration: (
trackId: string,
elementId: string,
- duration: number
+ duration: number,
+ pushHistory?: boolean
) => void;
updateElementStartTime: (
trackId: string,
elementId: string,
- startTime: number
+ startTime: number,
+ pushHistory?: boolean
) => void;
toggleTrackMute: (trackId: string) => void;
@@ -687,8 +690,14 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
updateTracksAndSave(newTracks);
},
- updateElementTrim: (trackId, elementId, trimStart, trimEnd) => {
- get().pushHistory();
+ updateElementTrim: (
+ trackId,
+ elementId,
+ trimStart,
+ trimEnd,
+ pushHistory = true
+ ) => {
+ if (pushHistory) get().pushHistory();
updateTracksAndSave(
get()._tracks.map((track) =>
track.id === trackId
@@ -705,8 +714,13 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
);
},
- updateElementDuration: (trackId, elementId, duration) => {
- get().pushHistory();
+ updateElementDuration: (
+ trackId,
+ elementId,
+ duration,
+ pushHistory = true
+ ) => {
+ if (pushHistory) get().pushHistory();
updateTracksAndSave(
get()._tracks.map((track) =>
track.id === trackId
@@ -721,8 +735,13 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
);
},
- updateElementStartTime: (trackId, elementId, startTime) => {
- get().pushHistory();
+ updateElementStartTime: (
+ trackId,
+ elementId,
+ startTime,
+ pushHistory = true
+ ) => {
+ if (pushHistory) get().pushHistory();
updateTracksAndSave(
get()._tracks.map((track) =>
track.id === trackId