Compare commits

..
Author SHA1 Message Date
Cursor AgentandMaze 9cf858c0c2 Fix fractional timeline ticks from clip retime
Co-authored-by: Maze <mazeincoding@users.noreply.github.com>
2026-04-25 15:47:38 +00:00
Maze Winther f1b45b4328 chore: remove stale files 2026-04-22 14:36:51 +02:00
Maze Winther 7428dc75cf chore: bump opencut-wasm version 2026-04-22 14:24:28 +02:00
Maze Winther 1301d027e9 fix: wasm WebGL only until WebGPU is fixed 2026-04-22 14:20:40 +02:00
Maze Winther f5f31b43df refactor: migration run() and pure v1-to-v2 transform 2026-04-22 02:41:18 +02:00
Maze Winther a709af0059 chore: formatting 2026-04-22 02:05:41 +02:00
Maze Winther 75eebf52bc fix: project loading thing 2026-04-21 20:45:33 +02:00
Maze Winther 1582423dbf chore: delete ffmpeg public assets 2026-04-21 13:00:26 +02:00
Maze Winther 148d38d7c9 docs: ts conventions to agents.md 2026-04-21 12:47:21 +02:00
Maze Winther 3e89d29985 refactor: restructure files to their domains, new preview overlay system, and dep graph 2026-04-20 11:31:17 +02:00
Maze Winther 729d10592f fix: WebGL frame rendering and opencut-wasm 0.2.8 2026-04-19 07:42:52 +02:00
Maze Winther 9da365bc01 docs: add prerequisites for rust 2026-04-19 07:23:15 +02:00
Maze Winther 8bdc894690 fix: migrate commands to CommandResult selection contract 2026-04-18 16:01:35 +02:00
Maze Winther 0387bd4c80 delete legacy folder 2026-04-18 15:35:04 +02:00
Maze Winther b1d0b10354 fix: default audio waveform color from timeline theme 2026-04-18 15:11:52 +02:00
Maze Winther 94eed96cad chore: update opencut-wasm version to 0.2.6 in Cargo.lock 2026-04-18 15:03:00 +02:00
Maze Winther 794d4ae149 fix: align LayerUniformBuffer to 16 bytes for WebGL compat 2026-04-18 15:01:59 +02:00
Maze Winther 011f51051c feat: clip volume line and timeline waveform improvements 2026-04-18 01:40:18 +02:00
Maze Winther c46d28891f feat: text / custom masks, fix preview pan, refactor selection 2026-04-15 15:45:50 +02:00
Maze Winther 66e4367bc4 feat: history view in feedback popover 2026-04-15 06:39:52 +02:00
Maze Winther 8161de00e6 chore: formatting 2026-04-15 06:26:55 +02:00
Maze Winther eb28942dab fix: audio waveform height in timeline 2026-04-15 06:05:13 +02:00
Maze Winther 68f175d3be fix: correct schema path in drizzle config 2026-04-15 06:04:29 +02:00
Maze Winther d5c1b7bf0e fix: load dotenv before env validation in drizzle config 2026-04-15 06:03:23 +02:00
Maze Winther 0868d9cf13 tests: delete 2026-04-15 05:53:06 +02:00
Maze Winther 8a3577d2e7 review: fixes 2026-04-15 05:52:33 +02:00
Maze Winther ac43a5a73f feat: move/resize multiple elements 2026-04-15 05:43:29 +02:00
Maze Winther 6312fc5b07 fix: repair scene tracks broken by v23-to-v24 migration 2026-04-15 04:45:15 +02:00
Maze Winther 2da93aa98f changelog: rephrasing 2026-04-15 04:34:36 +02:00
Maze Winther f4bd689f51 chore: force deployment 2026-04-15 03:59:52 +02:00
Maze Winther 4f7dc5ee3a fix: remap split-element keybinding to split 2026-04-15 03:57:51 +02:00
Maze Winther 4e8b7d3745 fix: skip unknown actions in keyboard shortcuts help 2026-04-15 03:47:57 +02:00
Maze Winther ee372266ca chore: force deployment 2026-04-15 03:35:07 +02:00
Maze Winther e10b0a3355 tests: remove 2026-04-15 02:44:52 +02:00
577 changed files with 17649 additions and 12640 deletions
+14
View File
@@ -21,3 +21,17 @@ Each app is a frontend that calls into Rust. Logic is never duplicated between a
- Read components before using them. They may already apply classes, which affects what you need to pass and how to override them.
### TypeScript
Function signatures should make the call site readable and let the function evolve without breaking callers. Positional parameters fail both: `formatTime(30, 24)` hides which number is which, and adding, removing, or reordering an argument silently breaks every caller whose types happen to still line up. A single destructured object fixes both at once - each argument names itself at the call site, and the shape can grow without churn. So signatures default to one object parameter:
```tsx
// ❌ meaning depends on order; the shape can't evolve without touching every caller
function formatTime(seconds: number, fps: number) { ... }
// ✅ each argument names itself; fields can be added, reordered, or made optional freely
function formatTime({ seconds, fps }: { seconds: number; fps: number }) { ... }
```
The one real exception is type predicates (`element is VideoElement`) — the language requires a positional subject, so the reasoning above doesn't get to apply.
Generated
+1 -1
View File
@@ -3820,7 +3820,7 @@ dependencies = [
[[package]]
name = "opencut-wasm"
version = "0.2.5"
version = "0.2.9"
dependencies = [
"bridge",
"compositor",
+13
View File
@@ -85,6 +85,19 @@ If you want to get ready for `apps/desktop`, see [`apps/desktop/README.md`](apps
Only needed if you're editing `rust/wasm` and want the web app to use your local build instead of the published package.
**Prerequisites** — install these once before anything else:
```bash
# Rust toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# build the WASM package
cargo install wasm-pack
# reruns the build on file changes, used by bun dev:wasm
cargo install cargo-watch
```
1. Build the package once from the repo root:
```bash
+2 -2
View File
@@ -1,6 +1,6 @@
use gpui::{
App, Application, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div,
prelude::*, px, rgb, size,
div, prelude::*, px, rgb, size, App, Application, Bounds, Context, SharedString, Window,
WindowBounds, WindowOptions,
};
struct AppWindow {
+9 -5
View File
@@ -1,22 +1,26 @@
import type { Config } from "drizzle-kit";
import * as dotenv from "dotenv";
import { webEnv } from "@/lib/env/web";
if (webEnv.NODE_ENV === "production") {
if (process.env.NODE_ENV === "production") {
dotenv.config({ path: ".env.production" });
} else {
dotenv.config({ path: ".env.local" });
}
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error("DATABASE_URL is not set");
}
export default {
schema: "./src/schema.ts",
schema: "./src/lib/db/schema.ts",
dialect: "postgresql",
migrations: {
table: "drizzle_migrations",
},
dbCredentials: {
url: webEnv.DATABASE_URL,
url: databaseUrl,
},
out: "./migrations",
strict: webEnv.NODE_ENV === "production",
strict: process.env.NODE_ENV === "production",
} satisfies Config;
+1 -1
View File
@@ -52,7 +52,7 @@
"nanoid": "^5.1.5",
"next": "16.1.3",
"next-themes": "^0.4.4",
"opencut-wasm": "^0.2.5",
"opencut-wasm": "^0.2.9",
"pg": "^8.16.2",
"postgres": "^3.4.5",
"radix-ui": "^1.4.3",
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -5,8 +5,8 @@ import { toast } from "sonner";
import {
type KeyboardShortcut,
useKeyboardShortcutsHelp,
} from "@/hooks/use-keyboard-shortcuts-help";
import { useKeybindingsStore } from "@/stores/keybindings-store";
} from "@/actions/use-keyboard-shortcuts-help";
import { useKeybindingsStore } from "@/actions/keybindings-store";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -1,209 +1,209 @@
import type {
KeybindingConfig,
ShortcutKey,
} from "@/lib/actions/keybinding";
import type { TActionWithOptionalArgs } from "./types";
export type TActionCategory =
| "playback"
| "navigation"
| "editing"
| "selection"
| "history"
| "timeline"
| "controls"
| "assets";
export interface TActionBaseDefinition {
description: string;
category: TActionCategory;
args?: Record<string, unknown>;
}
export interface TActionDefinition extends TActionBaseDefinition {
defaultShortcuts?: readonly ShortcutKey[];
}
export const ACTIONS = {
"toggle-play": {
description: "Play/Pause",
category: "playback",
},
"stop-playback": {
description: "Stop playback",
category: "playback",
},
"seek-forward": {
description: "Seek forward 1 second",
category: "playback",
args: { seconds: "number" },
},
"seek-backward": {
description: "Seek backward 1 second",
category: "playback",
args: { seconds: "number" },
},
"frame-step-forward": {
description: "Frame step forward",
category: "navigation",
},
"frame-step-backward": {
description: "Frame step backward",
category: "navigation",
},
"jump-forward": {
description: "Jump forward 5 seconds",
category: "navigation",
args: { seconds: "number" },
},
"jump-backward": {
description: "Jump backward 5 seconds",
category: "navigation",
args: { seconds: "number" },
},
"goto-start": {
description: "Go to timeline start",
category: "navigation",
},
"goto-end": {
description: "Go to timeline end",
category: "navigation",
},
split: {
description: "Split elements at playhead",
category: "editing",
},
"split-left": {
description: "Split and remove left",
category: "editing",
},
"split-right": {
description: "Split and remove right",
category: "editing",
},
"delete-selected": {
description: "Delete selected elements",
category: "editing",
},
"copy-selected": {
description: "Copy selected elements",
category: "editing",
},
"paste-copied": {
description: "Paste elements at playhead",
category: "editing",
},
"toggle-snapping": {
description: "Toggle snapping",
category: "editing",
},
"toggle-ripple-editing": {
description: "Toggle ripple editing",
category: "editing",
},
"toggle-source-audio": {
description: "Extract or recover source audio",
category: "editing",
},
"select-all": {
description: "Select all elements",
category: "selection",
},
"cancel-interaction": {
description: "Cancel current interaction",
category: "controls",
},
"deselect-all": {
description: "Deselect all elements",
category: "selection",
},
"duplicate-selected": {
description: "Duplicate selected element",
category: "selection",
},
"toggle-elements-muted-selected": {
description: "Mute/unmute selected elements",
category: "selection",
},
"toggle-elements-visibility-selected": {
description: "Show/hide selected elements",
category: "selection",
},
"toggle-bookmark": {
description: "Toggle bookmark at playhead",
category: "timeline",
},
undo: {
description: "Undo",
category: "history",
},
redo: {
description: "Redo",
category: "history",
},
"remove-media-asset": {
description: "Remove media asset",
category: "assets",
args: { projectId: "string", assetId: "string" },
},
"remove-media-assets": {
description: "Remove media assets",
category: "assets",
args: { projectId: "string", assetIds: "string[]" },
},
} as const satisfies Record<string, TActionBaseDefinition>;
export type TAction = keyof typeof ACTIONS;
const ACTION_DEFAULT_SHORTCUTS = {
"toggle-play": ["space", "k"],
"seek-forward": ["l"],
"seek-backward": ["j"],
"frame-step-forward": ["right"],
"frame-step-backward": ["left"],
"jump-forward": ["shift+right"],
"jump-backward": ["shift+left"],
"goto-start": ["home", "enter"],
"goto-end": ["end"],
split: ["s"],
"split-left": ["q"],
"split-right": ["w"],
"delete-selected": ["backspace", "delete"],
"copy-selected": ["ctrl+c"],
"paste-copied": ["ctrl+v"],
"toggle-snapping": ["n"],
"select-all": ["ctrl+a"],
"cancel-interaction": ["escape"],
"duplicate-selected": ["ctrl+d"],
undo: ["ctrl+z"],
redo: ["ctrl+shift+z", "ctrl+y"],
} as const satisfies Partial<Record<TActionWithOptionalArgs, readonly ShortcutKey[]>>;
const ACTION_DEFAULT_SHORTCUTS_BY_ACTION: Partial<
Record<TAction, readonly ShortcutKey[]>
> = ACTION_DEFAULT_SHORTCUTS;
export function getActionDefinition({
action,
}: {
action: TAction;
}): TActionDefinition {
return {
...ACTIONS[action],
defaultShortcuts: ACTION_DEFAULT_SHORTCUTS_BY_ACTION[action],
};
}
export function getDefaultShortcuts(): KeybindingConfig {
const shortcuts: KeybindingConfig = {};
for (const [action, defaultShortcuts] of Object.entries(
ACTION_DEFAULT_SHORTCUTS,
) as Array<[TActionWithOptionalArgs, readonly ShortcutKey[]]>) {
for (const shortcut of defaultShortcuts) {
shortcuts[shortcut] = action;
}
}
return shortcuts;
}
import type {
KeybindingConfig,
ShortcutKey,
} from "@/actions/keybinding";
import type { TActionWithOptionalArgs } from "./types";
export type TActionCategory =
| "playback"
| "navigation"
| "editing"
| "selection"
| "history"
| "timeline"
| "controls"
| "assets";
export interface TActionBaseDefinition {
description: string;
category: TActionCategory;
args?: Record<string, unknown>;
}
export interface TActionDefinition extends TActionBaseDefinition {
defaultShortcuts?: readonly ShortcutKey[];
}
export const ACTIONS = {
"toggle-play": {
description: "Play/Pause",
category: "playback",
},
"stop-playback": {
description: "Stop playback",
category: "playback",
},
"seek-forward": {
description: "Seek forward 1 second",
category: "playback",
args: { seconds: "number" },
},
"seek-backward": {
description: "Seek backward 1 second",
category: "playback",
args: { seconds: "number" },
},
"frame-step-forward": {
description: "Frame step forward",
category: "navigation",
},
"frame-step-backward": {
description: "Frame step backward",
category: "navigation",
},
"jump-forward": {
description: "Jump forward 5 seconds",
category: "navigation",
args: { seconds: "number" },
},
"jump-backward": {
description: "Jump backward 5 seconds",
category: "navigation",
args: { seconds: "number" },
},
"goto-start": {
description: "Go to timeline start",
category: "navigation",
},
"goto-end": {
description: "Go to timeline end",
category: "navigation",
},
split: {
description: "Split elements at playhead",
category: "editing",
},
"split-left": {
description: "Split and remove left",
category: "editing",
},
"split-right": {
description: "Split and remove right",
category: "editing",
},
"delete-selected": {
description: "Delete current selection",
category: "editing",
},
"copy-selected": {
description: "Copy selected elements",
category: "editing",
},
"paste-copied": {
description: "Paste elements at playhead",
category: "editing",
},
"toggle-snapping": {
description: "Toggle snapping",
category: "editing",
},
"toggle-ripple-editing": {
description: "Toggle ripple editing",
category: "editing",
},
"toggle-source-audio": {
description: "Extract or recover source audio",
category: "editing",
},
"select-all": {
description: "Select all elements",
category: "selection",
},
"cancel-interaction": {
description: "Cancel current interaction",
category: "controls",
},
"deselect-all": {
description: "Deselect all elements",
category: "selection",
},
"duplicate-selected": {
description: "Duplicate selected element",
category: "selection",
},
"toggle-elements-muted-selected": {
description: "Mute/unmute selected elements",
category: "selection",
},
"toggle-elements-visibility-selected": {
description: "Show/hide selected elements",
category: "selection",
},
"toggle-bookmark": {
description: "Toggle bookmark at playhead",
category: "timeline",
},
undo: {
description: "Undo",
category: "history",
},
redo: {
description: "Redo",
category: "history",
},
"remove-media-asset": {
description: "Remove media asset",
category: "assets",
args: { projectId: "string", assetId: "string" },
},
"remove-media-assets": {
description: "Remove media assets",
category: "assets",
args: { projectId: "string", assetIds: "string[]" },
},
} as const satisfies Record<string, TActionBaseDefinition>;
export type TAction = keyof typeof ACTIONS;
const ACTION_DEFAULT_SHORTCUTS = {
"toggle-play": ["space", "k"],
"seek-forward": ["l"],
"seek-backward": ["j"],
"frame-step-forward": ["right"],
"frame-step-backward": ["left"],
"jump-forward": ["shift+right"],
"jump-backward": ["shift+left"],
"goto-start": ["home", "enter"],
"goto-end": ["end"],
split: ["s"],
"split-left": ["q"],
"split-right": ["w"],
"delete-selected": ["backspace", "delete"],
"copy-selected": ["ctrl+c"],
"paste-copied": ["ctrl+v"],
"toggle-snapping": ["n"],
"select-all": ["ctrl+a"],
"cancel-interaction": ["escape"],
"duplicate-selected": ["ctrl+d"],
undo: ["ctrl+z"],
redo: ["ctrl+shift+z", "ctrl+y"],
} as const satisfies Partial<Record<TActionWithOptionalArgs, readonly ShortcutKey[]>>;
const ACTION_DEFAULT_SHORTCUTS_BY_ACTION: Partial<
Record<TAction, readonly ShortcutKey[]>
> = ACTION_DEFAULT_SHORTCUTS;
export function getActionDefinition({
action,
}: {
action: TAction;
}): TActionDefinition {
return {
...ACTIONS[action],
defaultShortcuts: ACTION_DEFAULT_SHORTCUTS_BY_ACTION[action],
};
}
export function getDefaultShortcuts(): KeybindingConfig {
const shortcuts: KeybindingConfig = {};
for (const [action, defaultShortcuts] of Object.entries(
ACTION_DEFAULT_SHORTCUTS,
) as Array<[TActionWithOptionalArgs, readonly ShortcutKey[]]>) {
for (const shortcut of defaultShortcuts) {
shortcuts[shortcut] = action;
}
}
return shortcuts;
}
@@ -2,11 +2,11 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import type { TActionWithOptionalArgs } from "@/lib/actions";
import { getDefaultShortcuts } from "@/lib/actions";
import type { TActionWithOptionalArgs } from "@/actions";
import { getDefaultShortcuts } from "@/actions";
import { isTypableDOMElement } from "@/utils/browser";
import { isAppleDevice } from "@/utils/platform";
import type { KeybindingConfig, ShortcutKey } from "@/lib/actions/keybinding";
import type { KeybindingConfig, ShortcutKey } from "@/actions/keybinding";
import { runMigrations, CURRENT_VERSION } from "./keybindings/migrations";
const defaultKeybindings: KeybindingConfig = getDefaultShortcuts();
@@ -2,6 +2,7 @@ import { v2ToV3 } from "./v2-to-v3";
import { v3ToV4 } from "./v3-to-v4";
import { v4ToV5 } from "./v4-to-v5";
import { v5ToV6 } from "./v5-to-v6";
import { v6ToV7 } from "./v6-to-v7";
type MigrationFn = ({ state }: { state: unknown }) => unknown;
@@ -10,9 +11,10 @@ const migrations: Record<number, MigrationFn> = {
3: v3ToV4,
4: v4ToV5,
5: v5ToV6,
6: v6ToV7,
};
export const CURRENT_VERSION = 6;
export const CURRENT_VERSION = 7;
export function runMigrations({
state,
@@ -1,5 +1,5 @@
import type { KeybindingConfig, ShortcutKey } from "@/lib/actions/keybinding";
import type { TActionWithOptionalArgs } from "@/lib/actions";
import type { KeybindingConfig, ShortcutKey } from "@/actions/keybinding";
import type { TActionWithOptionalArgs } from "@/actions";
interface V2State {
keybindings: KeybindingConfig;
@@ -1,6 +1,6 @@
import type { TActionWithOptionalArgs } from "@/lib/actions";
import type { ShortcutKey } from "@/lib/actions/keybinding";
import type { KeybindingConfig } from "@/lib/actions/keybinding";
import type { TActionWithOptionalArgs } from "@/actions";
import type { ShortcutKey } from "@/actions/keybinding";
import type { KeybindingConfig } from "@/actions/keybinding";
interface V3State {
keybindings: KeybindingConfig;
@@ -1,4 +1,4 @@
import type { KeybindingConfig } from "@/lib/actions/keybinding";
import type { KeybindingConfig } from "@/actions/keybinding";
interface V4State {
keybindings: KeybindingConfig;
@@ -1,4 +1,4 @@
import type { KeybindingConfig } from "@/lib/actions/keybinding";
import type { KeybindingConfig } from "@/actions/keybinding";
interface V5State {
keybindings: KeybindingConfig;
@@ -0,0 +1,19 @@
import type { KeybindingConfig } from "@/actions/keybinding";
interface V6State {
keybindings: KeybindingConfig;
isCustomized: boolean;
}
export function v6ToV7({ state }: { state: unknown }): unknown {
const v6 = state as V6State;
const keybindings = { ...v6.keybindings };
for (const key of Object.keys(keybindings) as Array<keyof KeybindingConfig>) {
if (keybindings[key] === ("split-element" as never)) {
keybindings[key] = "split";
}
}
return { ...v6, keybindings };
}
@@ -5,8 +5,8 @@ import type {
TActionHandlerOptions,
TArgOfAction,
TInvocationTrigger,
} from "@/lib/actions";
import { bindAction, unbindAction } from "@/lib/actions";
} from "@/actions";
import { bindAction, unbindAction } from "@/actions";
export function useActionHandler<A extends TAction>(
action: A,
@@ -1,39 +1,47 @@
"use client";
import { useEffect, useRef } from "react";
import { useTimelineStore } from "@/stores/timeline-store";
import { useActionHandler } from "@/hooks/actions/use-action-handler";
import { useEditor } from "../use-editor";
import { useElementSelection } from "../timeline/element/use-element-selection";
import { TICKS_PER_SECOND } from "@/lib/wasm";
import { useKeyframeSelection } from "../timeline/element/use-keyframe-selection";
import { getElementsAtTime, hasMediaId } from "@/lib/timeline";
import { cancelInteraction } from "@/lib/cancel-interaction";
import { invokeAction } from "@/lib/actions";
import { canToggleSourceAudio } from "@/lib/timeline/audio-separation";
import { useTimelineStore } from "@/timeline/timeline-store";
import { useActionHandler } from "@/actions/use-action-handler";
import { useEditor } from "@/editor/use-editor";
import { useElementSelection } from "@/timeline/hooks/element/use-element-selection";
import { TICKS_PER_SECOND } from "@/wasm";
import { useKeyframeSelection } from "@/timeline/hooks/element/use-keyframe-selection";
import { getElementsAtTime, hasMediaId } from "@/timeline";
import { cancelInteraction } from "@/editor/cancel-interaction";
import { invokeAction } from "@/actions";
import { canToggleSourceAudio } from "@/timeline/audio-separation";
import {
activateScope,
clearActiveScope,
type ScopeEntry,
} from "@/lib/selection/scope";
} from "@/selection/scope";
export function useEditorActions() {
const editor = useEditor();
const { selectedElements, setElementSelection } = useElementSelection();
const { selectedKeyframes, clearKeyframeSelection } = useKeyframeSelection();
const selectedMaskPointSelection = useEditor((e) =>
e.selection.getSelectedMaskPointSelection(),
);
const toggleSnapping = useTimelineStore((s) => s.toggleSnapping);
const rippleEditingEnabled = useTimelineStore((s) => s.rippleEditingEnabled);
const toggleRippleEditing = useTimelineStore((s) => s.toggleRippleEditing);
const hasTimelineSelectionRef = useRef(false);
const clearTimelineSelectionRef = useRef(() => {});
const clearTimelineActiveSelectionRef = useRef(() => {});
const timelineScopeRef = useRef<ScopeEntry | null>(null);
const hasTimelineSelection =
selectedElements.length > 0 || selectedKeyframes.length > 0;
selectedElements.length > 0 ||
selectedKeyframes.length > 0 ||
selectedMaskPointSelection !== null;
hasTimelineSelectionRef.current = hasTimelineSelection;
clearTimelineSelectionRef.current = () => {
setElementSelection({ elements: [] });
clearKeyframeSelection();
editor.selection.clearSelection();
};
clearTimelineActiveSelectionRef.current = () => {
editor.selection.clearMostSpecificSelection();
};
if (!timelineScopeRef.current) {
@@ -42,6 +50,9 @@ export function useEditorActions() {
clear: () => {
clearTimelineSelectionRef.current();
},
clearActive: () => {
clearTimelineActiveSelectionRef.current();
},
};
}
@@ -257,17 +268,36 @@ export function useEditorActions() {
useActionHandler(
"delete-selected",
() => {
if (selectedKeyframes.length > 0) {
editor.timeline.removeKeyframes({ keyframes: selectedKeyframes });
clearKeyframeSelection();
return;
switch (editor.selection.getActiveSelectionKind()) {
case "mask-points":
if (!selectedMaskPointSelection) {
return;
}
editor.timeline.deleteCustomMaskPoints({
trackId: selectedMaskPointSelection.trackId,
elementId: selectedMaskPointSelection.elementId,
maskId: selectedMaskPointSelection.maskId,
pointIds: selectedMaskPointSelection.pointIds,
});
return;
case "keyframes":
if (selectedKeyframes.length === 0) {
return;
}
editor.timeline.removeKeyframes({ keyframes: selectedKeyframes });
clearKeyframeSelection();
return;
case "elements":
if (selectedElements.length === 0) {
return;
}
editor.timeline.deleteElements({
elements: selectedElements,
});
return;
default:
return;
}
if (selectedElements.length === 0) {
return;
}
editor.timeline.deleteElements({
elements: selectedElements,
});
},
undefined,
);
@@ -343,8 +373,7 @@ export function useEditorActions() {
"deselect-all",
() => {
if (!clearActiveScope()) {
setElementSelection({ elements: [] });
clearKeyframeSelection();
editor.selection.clearMostSpecificSelection();
}
},
undefined,
@@ -1,7 +1,7 @@
import { useEffect } from "react";
import { invokeAction } from "@/lib/actions";
import { useEditor } from "@/hooks/use-editor";
import { useKeybindingsStore } from "@/stores/keybindings-store";
import { invokeAction } from "@/actions";
import { useEditor } from "@/editor/use-editor";
import { useKeybindingsStore } from "@/actions/keybindings-store";
import { isTypableDOMElement } from "@/utils/browser";
/**
@@ -1,8 +1,8 @@
"use client";
import { useMemo } from "react";
import { useKeybindingsStore } from "@/stores/keybindings-store";
import { ACTIONS, type TActionWithOptionalArgs } from "@/lib/actions";
import { useKeybindingsStore } from "@/actions/keybindings-store";
import { ACTIONS, type TActionWithOptionalArgs } from "@/actions";
import {
getPlatformAlternateKey,
getPlatformSpecialKey,
@@ -57,6 +57,9 @@ export function useKeyboardShortcutsHelp() {
[TActionWithOptionalArgs, string[]]
>) {
const actionDef = ACTIONS[action];
if (!actionDef) {
continue;
}
result.push({
id: action,
keys,
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
import {
composeAnimationValue,
createAnimationBinding,
} from "@/lib/animation/binding-values";
} from "@/animation/binding-values";
describe("binding values", () => {
test("formats composed animated colors as hex", () => {
@@ -1,4 +1,4 @@
import type { ScalarAnimationKey } from "@/lib/animation/types";
import type { ScalarAnimationKey } from "@/animation/types";
const BEZIER_SOLVE_ITERATIONS = 20;
@@ -12,7 +12,7 @@ import type {
DiscreteValue,
Vector2AnimationBinding,
VectorValue,
} from "@/lib/animation/types";
} from "@/animation/types";
import { clamp } from "@/utils/math";
interface LinearRgba {
@@ -1,12 +1,12 @@
import {
getDefaultLeftHandle,
getDefaultRightHandle,
} from "@/lib/animation/bezier";
} from "@/animation/bezier";
import type {
CurveHandle,
NormalizedCubicBezier,
ScalarAnimationKey,
} from "@/lib/animation/types";
} from "@/animation/types";
const VALUE_EPSILON = 1e-6;
@@ -1,9 +1,9 @@
import type { ParamValues } from "@/lib/params";
import type { Effect } from "@/lib/effects/types";
import type { ParamValues } from "@/params";
import type { Effect } from "@/effects/types";
import type {
ElementAnimations,
EffectParamPath,
} from "@/lib/animation/types";
} from "@/animation/types";
import { removeElementKeyframe } from "./keyframes";
import { resolveAnimationPathValueAtTime } from "./resolve";
@@ -5,7 +5,7 @@ import type {
ScalarAnimationChannel,
ScalarGraphChannel,
ScalarGraphKeyframeContext,
} from "@/lib/animation/types";
} from "@/animation/types";
export interface EditableScalarChannels {
binding: AnimationBindingInstance;
@@ -1,12 +1,12 @@
import type {
ElementAnimations,
GraphicParamPath,
} from "@/lib/animation/types";
import type { ParamValues } from "@/lib/params";
} from "@/animation/types";
import type { ParamValues } from "@/params";
import {
getGraphicDefinition,
resolveGraphicParams,
} from "@/lib/graphics";
} from "@/graphics";
import { resolveAnimationPathValueAtTime } from "./resolve";
export const GRAPHIC_PARAM_PATH_PREFIX = "params.";
@@ -7,7 +7,7 @@ import type {
ScalarAnimationChannel,
ScalarAnimationKey,
ScalarSegmentType,
} from "@/lib/animation/types";
} from "@/animation/types";
import { clamp } from "@/utils/math";
import {
getBezierPoint,
@@ -4,7 +4,7 @@ import type {
AnimationPath,
ElementAnimations,
ElementKeyframe,
} from "@/lib/animation/types";
} from "@/animation/types";
import {
type AnimationComponentValue,
composeAnimationValue,
@@ -13,7 +13,7 @@ import type {
ScalarAnimationKey,
ScalarCurveKeyframePatch,
ScalarSegmentType,
} from "@/lib/animation/types";
} from "@/animation/types";
import { generateUUID } from "@/utils/id";
import {
cloneAnimationBinding,
@@ -2,8 +2,8 @@ import type {
AnimationPropertyGroup,
AnimationPropertyPath,
ElementAnimations,
} from "@/lib/animation/types";
import { ANIMATION_PROPERTY_GROUPS } from "@/lib/animation/types";
} from "@/animation/types";
import { ANIMATION_PROPERTY_GROUPS } from "@/animation/types";
import { getKeyframeAtTime } from "./keyframe-query";
export interface GroupKeyframeRef {
@@ -3,20 +3,20 @@ import type {
AnimationInterpolation,
AnimationPropertyPath,
AnimationValue,
} from "@/lib/animation/types";
} from "@/animation/types";
import { parseColorToLinearRgba } from "./binding-values";
import type { TimelineElement } from "@/lib/timeline";
import { MIN_TRANSFORM_SCALE } from "@/lib/animation/transform";
import type { TimelineElement } from "@/timeline";
import { MIN_TRANSFORM_SCALE } from "@/animation/transform";
import {
CORNER_RADIUS_MAX,
CORNER_RADIUS_MIN,
} from "@/lib/text/background";
} from "@/text/background";
import {
canElementHaveAudio,
isVisualElement,
} from "@/lib/timeline/element-utils";
import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "@/lib/timeline/audio-constants";
import { DEFAULTS } from "@/lib/timeline/defaults";
} from "@/timeline/element-utils";
import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "@/timeline/audio-constants";
import { DEFAULTS } from "@/timeline/defaults";
import { snapToStep } from "@/utils/math";
export interface NumericSpec {
@@ -5,8 +5,8 @@ import type {
AnimationPropertyPath,
AnimationValueForPath,
ElementAnimations,
} from "@/lib/animation/types";
import type { Transform } from "@/lib/rendering";
} from "@/animation/types";
import type { Transform } from "@/rendering";
import {
type AnimationComponentValue,
composeAnimationValue,
@@ -3,20 +3,20 @@ import type {
AnimationInterpolation,
AnimationPath,
AnimationValue,
} from "@/lib/animation/types";
} from "@/animation/types";
import {
parseEffectParamPath,
isEffectParamPath,
} from "@/lib/animation/effect-param-channel";
} from "@/animation/effect-param-channel";
import {
isGraphicParamPath,
parseGraphicParamPath,
} from "@/lib/animation/graphic-param-channel";
import type { ParamDefinition } from "@/lib/params";
import { effectsRegistry, registerDefaultEffects } from "@/lib/effects";
import { getGraphicDefinition } from "@/lib/graphics";
import type { TimelineElement } from "@/lib/timeline";
import { isVisualElement } from "@/lib/timeline/element-utils";
} from "@/animation/graphic-param-channel";
import type { ParamDefinition } from "@/params";
import { effectsRegistry, registerDefaultEffects } from "@/effects";
import { getGraphicDefinition } from "@/graphics";
import type { TimelineElement } from "@/timeline";
import { isVisualElement } from "@/timeline/element-utils";
import { snapToStep } from "@/utils/math";
import {
coerceAnimationValueForProperty,
@@ -0,0 +1,35 @@
import { getElementKeyframes } from "@/animation";
import type { SceneTracks } from "@/timeline";
import type { SnapPoint } from "@/timeline/snapping";
export function getAnimationKeyframeSnapPointsForTimeline({
tracks,
excludeElementIds,
}: {
tracks: SceneTracks;
excludeElementIds?: Set<string>;
}): SnapPoint[] {
const snapPoints: SnapPoint[] = [];
const orderedTracks = [...tracks.overlay, tracks.main, ...tracks.audio];
for (const track of orderedTracks) {
for (const element of track.elements) {
if (excludeElementIds?.has(element.id)) {
continue;
}
for (const keyframe of getElementKeyframes({
animations: element.animations,
})) {
snapPoints.push({
time: element.startTime + keyframe.time,
type: "keyframe",
elementId: element.id,
trackId: track.id,
});
}
}
}
return snapPoints;
}
@@ -1,4 +1,4 @@
import type { ParamValues } from "@/lib/params";
import type { ParamValues } from "@/params";
export const ANIMATION_PROPERTY_PATHS = [
"transform.positionX",
+1 -1
View File
@@ -1,4 +1,4 @@
import { auth } from "@/lib/auth/server";
import { auth } from "@/auth/server";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);
+2 -2
View File
@@ -1,7 +1,7 @@
import { type NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { checkRateLimit } from "@/lib/rate-limit";
import { submitFeedback, MAX_MESSAGE_LENGTH } from "@/lib/feedback";
import { checkRateLimit } from "@/auth/rate-limit";
import { submitFeedback, MAX_MESSAGE_LENGTH } from "@/feedback";
const submitSchema = z.object({
message: z
+2 -2
View File
@@ -1,7 +1,7 @@
import { webEnv } from "@/lib/env/web";
import { webEnv } from "@/env/web";
import { type NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { checkRateLimit } from "@/lib/rate-limit";
import { checkRateLimit } from "@/auth/rate-limit";
const searchParamsSchema = z.object({
q: z.string().max(500, "Query too long").optional(),
+2 -2
View File
@@ -4,8 +4,8 @@ import { notFound } from "next/navigation";
import { BasePage } from "@/app/base-page";
import Prose from "@/components/ui/prose";
import { Separator } from "@/components/ui/separator";
import { getPosts, getSinglePost, processHtmlContent } from "@/lib/blog/query";
import type { Author, Post } from "@/lib/blog/types";
import { getPosts, getSinglePost, processHtmlContent } from "@/blog/query";
import type { Author, Post } from "@/blog/types";
type PageProps = {
params: Promise<{ slug: string }>;
+2 -2
View File
@@ -2,8 +2,8 @@ import type { Metadata } from "next";
import Link from "next/link";
import { BasePage } from "@/app/base-page";
import { Separator } from "@/components/ui/separator";
import { getPosts } from "@/lib/blog/query";
import type { Post } from "@/lib/blog/types";
import { getPosts } from "@/blog/query";
import type { Post } from "@/blog/types";
export const metadata: Metadata = {
title: "Blog - OpenCut",
@@ -3,15 +3,15 @@ import Link from "next/link";
import { notFound } from "next/navigation";
import { BasePage } from "@/app/base-page";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { getReleaseByVersion, getSortedReleases } from "@/lib/changelog/utils";
import { getReleaseByVersion, getSortedReleases } from "@/changelog/utils";
import {
ReleaseArticle,
ReleaseMeta,
ReleaseTitle,
ReleaseDescription,
ReleaseChanges,
} from "@/lib/changelog/components/release";
import { CopyMarkdownButton } from "@/lib/changelog/components/copy-markdown-button";
} from "@/changelog/components/release";
import { CopyMarkdownButton } from "@/changelog/components/copy-markdown-button";
type Props = { params: Promise<{ version: string }> };
+2 -2
View File
@@ -4,14 +4,14 @@ import { Separator } from "@/components/ui/separator";
import {
type Release as ReleaseType,
getSortedReleases,
} from "@/lib/changelog/utils";
} from "@/changelog/utils";
import {
ReleaseArticle,
ReleaseMeta,
ReleaseTitle,
ReleaseDescription,
ReleaseChanges,
} from "@/lib/changelog/components/release";
} from "@/changelog/components/release";
export const metadata: Metadata = {
title: "Changelog - OpenCut",
+1 -1
View File
@@ -3,7 +3,7 @@ import Link from "next/link";
import { GitHubContributeSection } from "@/components/gitHub-contribute-section";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent } from "@/components/ui/card";
import { EXTERNAL_TOOLS } from "@/lib/site/external-tools";
import { EXTERNAL_TOOLS } from "@/site/external-tools";
import { BasePage } from "../base-page";
export const metadata: Metadata = {
+67 -9
View File
@@ -8,21 +8,32 @@ import {
} from "@/components/ui/resizable";
import { AssetsPanel } from "@/components/editor/panels/assets";
import { PropertiesPanel } from "@/components/editor/panels/properties";
import { Timeline } from "@/components/editor/panels/timeline";
import { PreviewPanel } from "@/components/editor/panels/preview";
import { Timeline } from "@/timeline/components";
import { PreviewPanel } from "@/preview/components";
import { EditorHeader } from "@/components/editor/editor-header";
import { EditorProvider } from "@/components/providers/editor-provider";
import { Onboarding } from "@/components/editor/onboarding";
import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog";
import { usePanelStore } from "@/stores/panel-store";
import { usePasteMedia } from "@/hooks/use-paste-media";
import { MigrationDialog } from "@/project/components/migration-dialog";
import { usePanelStore } from "@/editor/panel-store";
import { usePasteMedia } from "@/media/use-paste-media";
import { MobileGate } from "@/components/editor/mobile-gate";
import { useState } from "react";
import { useEditor } from "@/hooks/use-editor";
import { useMemo, useState } from "react";
import { useEditor } from "@/editor/use-editor";
import { Cancel01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { Button } from "@/components/ui/button";
import { ChangelogNotification } from "@/lib/changelog/components/changelog-notification";
import { ChangelogNotification } from "@/changelog/components/changelog-notification";
import {
createPreviewOverlayControl,
isPreviewOverlayVisible,
mergePreviewOverlaySources,
} from "@/preview/overlays";
import { usePreviewStore } from "@/preview/preview-store";
import { getGuidePreviewOverlaySource } from "@/guides";
import {
bookmarkNotesPreviewOverlay,
getBookmarkPreviewOverlaySource,
} from "@/timeline/bookmarks/index";
export default function Editor() {
const params = useParams();
@@ -70,6 +81,49 @@ function DegradedRendererBanner() {
function EditorLayout() {
usePasteMedia();
const { panels, setPanel } = usePanelStore();
const activeScene = useEditor((editor) =>
editor.scenes.getActiveSceneOrNull(),
);
const currentTime = useEditor((editor) => editor.playback.getCurrentTime());
const activeGuide = usePreviewStore((state) => state.activeGuide);
const overlays = usePreviewStore((state) => state.overlays);
const setOverlayVisibility = usePreviewStore(
(state) => state.setOverlayVisibility,
);
const showBookmarkNotes = isPreviewOverlayVisible({
overlay: bookmarkNotesPreviewOverlay,
overlays,
});
const overlaySource = useMemo(
() =>
mergePreviewOverlaySources({
sources: [
getGuidePreviewOverlaySource({
guideId: activeGuide,
}),
activeScene
? getBookmarkPreviewOverlaySource({
bookmarks: activeScene.bookmarks,
time: currentTime,
isVisible: showBookmarkNotes,
})
: {
definitions: [bookmarkNotesPreviewOverlay],
instances: [],
},
],
}),
[activeGuide, activeScene, currentTime, showBookmarkNotes],
);
const overlayControls = useMemo(
() =>
overlaySource.definitions.map((overlay) =>
createPreviewOverlayControl({ overlay, overlays }),
),
[overlaySource.definitions, overlays],
);
return (
<ResizablePanelGroup
@@ -111,7 +165,11 @@ function EditorLayout() {
minSize={30}
className="min-h-0 min-w-0 flex-1"
>
<PreviewPanel />
<PreviewPanel
overlayControls={overlayControls}
overlayInstances={overlaySource.instances}
onOverlayVisibilityChange={setOverlayVisibility}
/>
</ResizablePanel>
<ResizableHandle withHandle />
+2 -2
View File
@@ -2,11 +2,11 @@ import { ThemeProvider } from "next-themes";
import Script from "next/script";
import "./globals.css";
import { Toaster } from "../components/ui/sonner";
import { ChangelogNotification } from "@/lib/changelog/components/changelog-notification";
import { ChangelogNotification } from "@/changelog/components/changelog-notification";
import { TooltipProvider } from "../components/ui/tooltip";
import { baseMetaData } from "./metadata";
import { BotIdClient } from "botid/client";
import { webEnv } from "@/lib/env/web";
import { webEnv } from "@/env/web";
import { Inter } from "next/font/google";
const siteFont = Inter({ subsets: ["latin"] });
+1 -1
View File
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
import { SITE_INFO, SITE_URL } from "@/lib/site/brand";
import { SITE_INFO, SITE_URL } from "@/site/brand";
export const baseMetaData: Metadata = {
metadataBase: new URL(SITE_URL),
+1 -1
View File
@@ -2,7 +2,7 @@ import { Hero } from "@/components/landing/hero";
import { Header } from "@/components/header";
import { Footer } from "@/components/footer";
import type { Metadata } from "next";
import { SITE_URL } from "@/lib/site/brand";
import { SITE_URL } from "@/site/brand";
export const metadata: Metadata = {
alternates: {
+1 -1
View File
@@ -7,7 +7,7 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
import { Separator } from "@/components/ui/separator";
import { SOCIAL_LINKS } from "@/lib/site/social";
import { SOCIAL_LINKS } from "@/site/social";
export const metadata: Metadata = {
title: "Privacy Policy - OpenCut",
+8 -8
View File
@@ -7,20 +7,20 @@ import type { KeyboardEvent, MouseEvent } from "react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import type { EditorCore } from "@/core";
import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog";
import { StoragePersistenceDialog } from "@/components/editor/dialogs/storage-persistence-dialog";
import { MigrationDialog } from "@/project/components/migration-dialog";
import { StoragePersistenceDialog } from "@/services/storage/components/storage-persistence-dialog";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { useEditor } from "@/hooks/use-editor";
import { useEditor } from "@/editor/use-editor";
import { useProjectsStore } from "./store";
import type {
TProjectMetadata,
TProjectSortKey,
TProjectSortOption,
} from "@/lib/project/types";
} from "@/project/types";
import { formatTimecode, mediaTimeToSeconds } from "opencut-wasm";
import { formatDate } from "@/utils/date";
import { HugeiconsIcon } from "@hugeicons/react";
@@ -62,11 +62,11 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { DeleteProjectDialog } from "@/components/editor/dialogs/delete-project-dialog";
import { ProjectInfoDialog } from "@/components/editor/dialogs/project-info-dialog";
import { RenameProjectDialog } from "@/components/editor/dialogs/rename-project-dialog";
import { DeleteProjectDialog } from "@/project/components/delete-project-dialog";
import { ProjectInfoDialog } from "@/project/components/project-info-dialog";
import { RenameProjectDialog } from "@/project/components/rename-project-dialog";
import { cn } from "@/utils/ui";
import { ChangelogNotification } from "@/lib/changelog/components/changelog-notification";
import { ChangelogNotification } from "@/changelog/components/changelog-notification";
const formatProjectDuration = ({
duration,
}: {
+1 -1
View File
@@ -1,6 +1,6 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import type { TProjectSortKey } from "@/lib/project/types";
import type { TProjectSortKey } from "@/project/types";
export type ProjectsViewMode = "grid" | "list";
+1 -1
View File
@@ -1,5 +1,5 @@
import type { MetadataRoute } from "next";
import { SITE_URL } from "@/lib/site/brand";
import { SITE_URL } from "@/site/brand";
export default function robots(): MetadataRoute.Robots {
return {
+2 -2
View File
@@ -1,6 +1,6 @@
import { Feed } from "feed";
import { getPosts } from "@/lib/blog/query";
import { SITE_INFO, SITE_URL } from "@/lib/site/brand";
import { getPosts } from "@/blog/query";
import { SITE_INFO, SITE_URL } from "@/site/brand";
export async function GET() {
try {
+2 -2
View File
@@ -1,5 +1,5 @@
import { SITE_URL } from "@/lib/site/brand";
import { getPosts } from "@/lib/blog/query";
import { SITE_URL } from "@/site/brand";
import { getPosts } from "@/blog/query";
import type { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
+1 -1
View File
@@ -3,7 +3,7 @@ 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 "@/lib/site/sponsors";
import { SPONSORS, type Sponsor } from "@/site/sponsors";
import { HugeiconsIcon } from "@hugeicons/react";
import { LinkSquare02Icon } from "@hugeicons/core-free-icons";
import { cn } from "@/utils/ui";
+1 -1
View File
@@ -7,7 +7,7 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
import { Separator } from "@/components/ui/separator";
import { SOCIAL_LINKS } from "@/lib/site/social";
import { SOCIAL_LINKS } from "@/site/social";
export const metadata: Metadata = {
title: "Terms of Service - OpenCut",
@@ -1,5 +1,5 @@
import { createAuthClient } from "better-auth/react";
import { webEnv } from "@/lib/env/web";
import { webEnv } from "@/env/web";
export const { signIn, signUp, useSession } = createAuthClient({
baseURL: webEnv.NEXT_PUBLIC_SITE_URL,
@@ -1,6 +1,6 @@
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
import { webEnv } from "@/lib/env/web";
import { webEnv } from "@/env/web";
const redis = new Redis({
url: webEnv.UPSTASH_REDIS_REST_URL,
@@ -1,8 +1,8 @@
import { betterAuth, type RateLimit } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { Redis } from "@upstash/redis";
import { db } from "@/lib/db";
import { webEnv } from "@/lib/env/web";
import { db } from "@/db";
import { webEnv } from "@/env/web";
const redis = new Redis({
url: webEnv.UPSTASH_REDIS_REST_URL,
@@ -5,7 +5,7 @@ import type {
MarblePost,
MarblePostList,
MarbleTagList,
} from "@/lib/blog/types";
} from "@/blog/types";
import { unified } from "unified";
import rehypeParse from "rehype-parse";
import rehypeStringify from "rehype-stringify";
@@ -47,7 +47,12 @@ const EMPTY_AUTHORS: MarbleAuthorList = {
};
function isMarbleConfigured() {
return !["", "placeholder", "build-placeholder", "your_workspace_key_here"].includes(key);
return ![
"",
"placeholder",
"build-placeholder",
"your_workspace_key_here",
].includes(key);
}
async function fetchFromMarble<T>({
@@ -1,4 +1,4 @@
import type { TCanvasSize } from "@/lib/project/types";
import type { TCanvasSize } from "@/project/types";
export const DEFAULT_CANVAS_PRESETS: TCanvasSize[] = [
{ width: 1920, height: 1080 },
@@ -43,7 +43,7 @@ changes:
- type: improved
text: "The properties panel has been redesigned with a tabbed layout."
- type: new
text: "Volume control for audio and video elements, both in the properties panel and inline on timeline clips."
text: "Volume control for audio and video elements."
- type: new
text: "Speed control for video and audio clips. Includes a maintain pitch option so audio doesn't chipmunk or deepen when you change the rate."
- type: improved
@@ -71,7 +71,7 @@ changes:
- type: new
text: "Preview zoom and panning. Zoom into the canvas to work on fine details, and pan around to navigate when zoomed in."
- type: new
text: "Stickers panel now has content. Browse and add logos, country flags, and shapes."
text: "Stickers panel now has content. Browse and add country flags and shapes."
- type: improved
text: "You can now select multiple assets in the assets panel. Useful for deleting several at once."
- type: fixed
+19
View File
@@ -0,0 +1,19 @@
---
version: "0.3.0"
date: "2026-03-04"
published: false
title: "Title"
description: "Description"
changes:
- type: new
text: "Layout guides are back and expanded. TikTok was the only option before. Now includes a grid guide plus TikTok, Instagram Reels, YouTube Shorts, and Snapchat Spotlight."
- type: new
text: "Two new mask types: text and custom (draw any shape with the pen tool using bezier curves). Both support feather, invert, and stroke."
- type: fixed
text: "Preview panning now keeps working while the Masks tab is open."
- type: new
text: "Audio clips have a horizontal volume line you can drag up or down to change volume. The line position reflects the current volume setting and is consistent across clips: same dB always means the same position."
- type: improved
text: "Audio waveforms now show the actual shape of the audio. Quiet parts are visible, loud parts are tall, and any bar that peaks above 0 dBFS gets a colored tip at the top."
- type: fixed
text: "Trimming or cutting an audio clip no longer causes the waveform to compress or show the wrong section of audio."
@@ -1,4 +1,4 @@
import { PasteCommand } from "@/lib/commands/timeline";
import { PasteCommand } from "@/commands/timeline";
import type { ClipboardHandler } from "../types";
export const ElementsClipboardHandler = {
@@ -1,7 +1,7 @@
import { getKeyframeById } from "@/lib/animation";
import type { SelectedKeyframeRef } from "@/lib/animation/types";
import type { TimelineElement } from "@/lib/timeline";
import { PasteKeyframesCommand } from "@/lib/commands/timeline";
import { getKeyframeById } from "@/animation";
import type { SelectedKeyframeRef } from "@/animation/types";
import type { TimelineElement } from "@/timeline";
import { PasteKeyframesCommand } from "@/commands/timeline";
import type {
ClipboardHandler,
KeyframeClipboardCurvePatch,
@@ -5,13 +5,13 @@ import type {
AnimationValue,
ScalarCurveKeyframePatch,
SelectedKeyframeRef,
} from "@/lib/animation/types";
import type { Command } from "@/lib/commands/base-command";
} from "@/animation/types";
import type { Command } from "@/commands/base-command";
import type {
CreateTimelineElement,
ElementRef,
TrackType,
} from "@/lib/timeline";
} from "@/timeline";
export interface ElementClipboardItem {
trackId: string;
+31
View File
@@ -0,0 +1,31 @@
import type { EditorSelectionPatch } from "@/selection/editor-selection";
import type { ElementRef } from "@/timeline/types";
export interface CommandResult {
selection?: EditorSelectionPatch;
}
export function createElementSelectionResult(
selectedElements: ElementRef[],
): CommandResult {
return {
selection: {
selectedElements,
selectedKeyframes: [],
keyframeSelectionAnchor: null,
selectedMaskPoints: null,
},
};
}
export abstract class Command {
abstract execute(): CommandResult | undefined;
undo(): void {
throw new Error("Undo not implemented for this command");
}
redo(): CommandResult | undefined {
return this.execute();
}
}
@@ -10,7 +10,7 @@ export class BatchCommand extends Command {
for (const command of this.commands) {
const result = command.execute();
if (result?.select !== undefined) {
if (result?.selection !== undefined) {
latestSelectionResult = result;
}
}
@@ -29,7 +29,7 @@ export class BatchCommand extends Command {
for (const command of this.commands) {
const result = command.redo();
if (result?.select !== undefined) {
if (result?.selection !== undefined) {
latestSelectionResult = result;
}
}
@@ -1,13 +1,13 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import { toast } from "sonner";
import type { MediaAsset } from "@/lib/media/types";
import type { MediaAsset } from "@/media/types";
import { generateUUID } from "@/utils/id";
import { storageService } from "@/services/storage/service";
import type { FrameRate } from "opencut-wasm";
import { hasMediaId } from "@/lib/timeline/element-utils";
import { frameRatesEqual, getHighestImportedVideoFps } from "@/lib/fps/utils";
import { UpdateProjectSettingsCommand } from "@/lib/commands/project";
import { hasMediaId } from "@/timeline/element-utils";
import { frameRatesEqual, getHighestImportedVideoFps } from "@/fps/utils";
import { UpdateProjectSettingsCommand } from "@/commands/project";
export class AddMediaAssetCommand extends Command {
private assetId: string;
@@ -1,10 +1,12 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { MediaAsset } from "@/lib/media/types";
import type { MediaAsset } from "@/media/types";
import { buildWaveformSourceKey } from "@/media/waveform-summary";
import { storageService } from "@/services/storage/service";
import { videoCache } from "@/services/video-cache/service";
import { hasMediaId } from "@/lib/timeline/element-utils";
import type { SceneTracks } from "@/lib/timeline";
import { waveformCache } from "@/services/waveform-cache/service";
import { hasMediaId } from "@/timeline/element-utils";
import type { SceneTracks } from "@/timeline";
export class RemoveMediaAssetCommand extends Command {
private savedAssets: MediaAsset[] | null = null;
@@ -41,6 +43,12 @@ export class RemoveMediaAssetCommand extends Command {
}
videoCache.clearVideo({ mediaId: this.assetId });
waveformCache.clearSource({
sourceKey: buildWaveformSourceKey({
kind: "media",
id: this.assetId,
}),
});
editor.media.setAssets({
assets: assets.filter((media) => media.id !== this.assetId),
@@ -1,6 +1,6 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TProject, TProjectSettings } from "@/lib/project/types";
import type { TProject, TProjectSettings } from "@/project/types";
export class UpdateProjectSettingsCommand extends Command {
private savedSettings: TProjectSettings | null = null;
@@ -1,7 +1,7 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/lib/timeline";
import { buildDefaultScene } from "@/lib/scenes";
import type { TScene } from "@/timeline";
import { buildDefaultScene } from "@/timeline/scenes";
export class CreateSceneCommand extends Command {
private savedScenes: TScene[] | null = null;
@@ -1,7 +1,7 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/lib/timeline";
import { canDeleteScene, getFallbackSceneAfterDelete } from "@/lib/scenes";
import type { TScene } from "@/timeline";
import { canDeleteScene, getFallbackSceneAfterDelete } from "@/timeline/scenes";
export class DeleteSceneCommand extends Command {
private savedScenes: TScene[] | null = null;
@@ -1,8 +1,8 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/lib/timeline";
import { updateSceneInArray } from "@/lib/scenes";
import { getFrameTime, moveBookmarkInArray } from "@/lib/timeline/bookmarks";
import type { TScene } from "@/timeline";
import { updateSceneInArray } from "@/timeline/scenes";
import { getFrameTime, moveBookmarkInArray } from "@/timeline/bookmarks/index";
export class MoveBookmarkCommand extends Command {
private savedScenes: TScene[] | null = null;
@@ -1,11 +1,11 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/lib/timeline";
import { updateSceneInArray } from "@/lib/scenes";
import type { TScene } from "@/timeline";
import { updateSceneInArray } from "@/timeline/scenes";
import {
getFrameTime,
removeBookmarkFromArray,
} from "@/lib/timeline/bookmarks";
} from "@/timeline/bookmarks/index";
export class RemoveBookmarkCommand extends Command {
private savedScenes: TScene[] | null = null;
@@ -1,7 +1,7 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/lib/timeline";
import { updateSceneInArray } from "@/lib/scenes";
import type { TScene } from "@/timeline";
import { updateSceneInArray } from "@/timeline/scenes";
export class RenameSceneCommand extends Command {
private savedScenes: TScene[] | null = null;
@@ -1,8 +1,11 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/lib/timeline";
import { updateSceneInArray } from "@/lib/scenes";
import { getFrameTime, toggleBookmarkInArray } from "@/lib/timeline/bookmarks";
import type { TScene } from "@/timeline";
import { updateSceneInArray } from "@/timeline/scenes";
import {
getFrameTime,
toggleBookmarkInArray,
} from "@/timeline/bookmarks/index";
export class ToggleBookmarkCommand extends Command {
private savedScenes: TScene[] | null = null;

Some files were not shown because too many files have changed in this diff Show More