Privacy-first video editor, with a focus on simplicity and ease of use.
lib/- domain logic (specific to this app)utils/- small helper utils (generic, could be copy-pasted into any other app)
The editor uses a singleton EditorCore that manages all editor state through specialized managers.
EditorCore (singleton)
├── playback: PlaybackManager
├── timeline: TimelineManager
├── scene: SceneManager
├── project: ProjectManager
├── media: MediaManager
└── renderer: RendererManager
Always use the useEditor() hook:
import { useEditor } from '@/hooks/use-editor';
function MyComponent() {
const editor = useEditor();
const tracks = editor.timeline.getTracks();
// Call methods
editor.timeline.addTrack({ type: 'media' });
// Display data (auto re-renders on changes)
return <div>{tracks.length} tracks</div>;
}The hook:
- Returns the singleton instance
- Subscribes to all manager changes
- Automatically re-renders when state changes
Use EditorCore.getInstance() directly:
// In utilities, event handlers, or non-React code
import { EditorCore } from "@/core";
const editor = EditorCore.getInstance();
await editor.export({ format: "mp4", quality: "high" });Actions are the trigger layer for user-initiated operations. The single source of truth is @/lib/actions/definitions.ts.
To add a new action:
- Add it to
ACTIONSin@/lib/actions/definitions.ts:
export const ACTIONS = {
"my-action": {
description: "What the action does",
category: "editing",
defaultShortcuts: ["ctrl+m"],
},
// ...
};- Add handler in
@/hooks/use-editor-actions.ts:
useActionHandler(
"my-action",
() => {
// implementation
},
undefined,
);In components, use invokeAction() for user-triggered operations:
import { invokeAction } from '@/lib/actions';
// Good - uses action system
const handleSplit = () => invokeAction("split-selected");
// Avoid - bypasses UX layer (toasts, validation feedback)
const handleSplit = () => editor.timeline.splitElements({ ... });Direct editor.xxx() calls are for internal use (commands, tests, complex multi-step operations).
Commands handle undo/redo. They live in @/lib/commands/ organized by domain (timeline, media, scene).
Each command extends Command from @/lib/commands/base-command and implements:
execute()- saves current state, then does the mutationundo()- restores the saved state
Actions and commands work together: actions are "what triggered this", commands are "how to do it (and undo it)".