From cd413fd89221c2de9f4df6cfe2e8ba36af6336da Mon Sep 17 00:00:00 2001 From: Akanksha Trehun Date: Thu, 22 Oct 2026 20:55:48 +0530 Subject: [PATCH] feat: undo/redo and project recovery with versioned project files - One immutable EditState (recipe minus the sound preference, plus image overlay settings) in a bounded history: 100 steps, no duplicates, slider drags and typing coalesced into one step. - Undo/redo buttons and Cmd/Ctrl+Z, Shift+Cmd+Z, Ctrl+Y; left to the browser inside text fields; disabled during export. - Project record (edits + source description, no media) saved locally and flushed on pagehide; missing-source recovery that resumes when the same file is chosen again. - Save/open .reframe.json with strict validation and a migration table. - "Delete saved data" removes everything the editor stored. - Reset settings is now undoable and no longer deletes saved data. - Fix two pre-existing test failures on main (duplicate imports in exportEstimate.test.ts, URL state leaking between hook tests). Co-Authored-By: Claude Opus 5.5 --- README.md | 6 +- docs/ARCHITECTURE.md | 2 + docs/PROJECTS.md | 141 ++++ src/components/HistoryControls.tsx | 65 ++ src/components/ProjectPanel.tsx | 223 +++++++ src/components/VideoEditor.tsx | 104 ++- .../__tests__/ProjectControls.test.tsx | 105 +++ src/hooks/tests/useKeyboardShortcuts.test.tsx | 132 ++++ src/hooks/tests/useVideoEditor.test.ts | 3 + src/hooks/tests/useVideoEditorProject.test.ts | 341 ++++++++++ src/hooks/useKeyboardShortcuts.ts | 17 +- src/hooks/useVideoEditor.ts | 607 +++++++++++++----- src/lib/editHistory.ts | 205 ++++++ src/lib/exportEstimate.test.ts | 8 +- src/lib/historyShortcuts.ts | 57 ++ src/lib/project.ts | 375 +++++++++++ src/lib/tests/editHistory.test.ts | 194 ++++++ src/lib/tests/project.test.ts | 259 ++++++++ 18 files changed, 2657 insertions(+), 187 deletions(-) create mode 100644 docs/PROJECTS.md create mode 100644 src/components/HistoryControls.tsx create mode 100644 src/components/ProjectPanel.tsx create mode 100644 src/components/__tests__/ProjectControls.test.tsx create mode 100644 src/hooks/tests/useKeyboardShortcuts.test.tsx create mode 100644 src/hooks/tests/useVideoEditorProject.test.ts create mode 100644 src/lib/editHistory.ts create mode 100644 src/lib/historyShortcuts.ts create mode 100644 src/lib/project.ts create mode 100644 src/lib/tests/editHistory.test.ts create mode 100644 src/lib/tests/project.test.ts diff --git a/README.md b/README.md index 9a26ce5f0..e2f74b595 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,8 @@ Reframe is a **browser-based video editor** — everything happens on your devic - **Audio Control** — Keep or mute audio independently - **Speed Control** — 0.25x to 4x playback speed with smooth audio adjustment - **Quality Settings** — CRF slider for quality vs. file size trade-offs +- **Undo / Redo** — every edit can be undone; a slider drag counts as one step +- **Project recovery** — edits survive a refresh, and project files (`.reframe.json`) save and reopen your edits without the video. See [docs/PROJECTS.md](docs/PROJECTS.md) - **Smooth UX** — Lottie animations, live export progress, instant download Everything stays on your device. No servers. No tracking. No login. @@ -95,6 +97,8 @@ Everything stays on your device. No servers. No tracking. No login. | Shortcut | Action | |----------|--------| | Ctrl+Enter / Cmd+Enter | Export video | +| Ctrl+Z / Cmd+Z | Undo the last edit | +| Ctrl+Y or Ctrl+Shift+Z / Shift+Cmd+Z | Redo | | Space | Play/pause video preview | | M | Toggle audio mute | | Escape | Cancel export | @@ -439,7 +443,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide including development ## Privacy -Reframe processes all videos **100% client-side**. Your video files are never uploaded to any server. You can even use Reframe offline (after first load). The source code is fully open for inspection. +Reframe processes all videos **100% client-side**. Your video files are never uploaded to any server. You can even use Reframe offline (after first load). Edit settings and a copy of the open video are kept in your own browser so a refresh doesn't lose work; **Delete saved data** in the editor removes them. The source code is fully open for inspection. --- ## Contributors diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7dd8e2303..8a5782538 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -114,6 +114,8 @@ Reframe uses a custom React hook (`useVideoEditor`) for state management instead - **Type safety** — Full TypeScript support - **Sufficient for scope** — App state is relatively simple and doesn't require complex state management +All user-visible edits (the `EditRecipe` minus the completion-sound preference, plus the image overlay's position, size and opacity) live in one `EditState`. `useVideoEditor` holds it in a bounded undo history (`src/lib/editHistory.ts`), and the preview, the export and undo/redo all read that same state. Saving, recovery and project files are described in [PROJECTS.md](PROJECTS.md). + ### Component Architecture Components are organized by feature: diff --git a/docs/PROJECTS.md b/docs/PROJECTS.md new file mode 100644 index 000000000..9a35af1dd --- /dev/null +++ b/docs/PROJECTS.md @@ -0,0 +1,141 @@ +# Undo, recovery and project files + +This covers how Reframe keeps edit history, what it saves in the browser, and the `.reframe.json` project format. Nothing here sends data anywhere. Everything stays in the user's browser or in files the user saves themselves. + +## Edit state + +`EditState` (`src/lib/editHistory.ts`) is the single source of truth for every edit that changes the exported video: + +| Included | Where it lives | +|---|---| +| Preset, custom size, framing, trim, rotation | `recipe` | +| Keep audio, normalise, volume, speed | `recipe` | +| Quality, format, brightness, contrast, saturation, sharpness, stabilisation, denoise | `recipe` | +| Text overlays (text, position, size, colour, weight, font name) | `recipe.textOverlays` | +| Image overlay position, size and opacity | `imageOverlay` | + +Not included, on purpose: + +- **The completion-sound preference.** It's a setting, not an edit, so undo never flips it. +- **Files** (the source video, the image overlay file, background music). They are binary assets, not edit values, so adding or removing the image file is not an undo step. +- **Transient UI state.** Hover, open panels, the selected text overlay, export progress, object URLs and worker state. +- **The multi-track timeline state.** It isn't connected to the UI or the export yet (see `PHASE_1_MVP.md`). + +`useVideoEditor` exposes `recipe` derived from `EditState`. `handleExport` passes that same object, plus the image overlay settings, to `exportVideo`. So after an undo the preview and the export can't disagree. + +## Undo and redo + +- **Snapshots.** Each history entry is an immutable `EditState`, about 1 KB, never media. Undo and redo just move between entries. +- **Bounded.** `HISTORY_LIMIT` is 100 entries. The oldest are dropped first. +- **No duplicates.** A change that leaves the state equal to the current one is not recorded. +- **Coalescing.** Repeated changes to the same continuous control within `COALESCE_WINDOW_MS` (600 ms) merge into one step: + - Continuous controls are sliders, trim handles, custom size, and text-overlay text, position, size and colour. + - The window slides, so a long drag stays one step. + - Dragging back to the starting value removes the step. + - Discrete choices (preset, format, rotation, toggles, image overlay position) always get their own step, however fast they're clicked. + - Adding or removing a text overlay is always its own step. +- **When history resets.** Choosing a new video, restoring a session and deleting saved data each start a fresh history. +- **During export.** Undo and redo are disabled while an export runs. The running export has already taken its settings. +- **Reset settings.** "Reset settings" and the `R` shortcut are now a single undoable step. They no longer delete saved data. + +**Shortcuts.** macOS uses ⌘Z and ⇧⌘Z. Other platforms use Ctrl+Z, and Ctrl+Y or Ctrl+Shift+Z. They work while a slider, checkbox or button has focus. In text fields and `contenteditable` they are left to the browser, so they undo the typing. See `src/lib/historyShortcuts.ts`. + +## What is saved in the browser + +| Key | Store | Contents | +|---|---|---| +| `reframe:project` | localStorage | The current project document (below). Written 500 ms after the last edit while a video is open, and on `pagehide`. | +| `reframe-session` / `files` / `currentVideo` | IndexedDB | One copy of the open source video, overwritten when a new video is chosen. Unchanged from before. | +| `reframe:recipe`, `editorState`, `soundOnCompletion` | localStorage | Existing settings records, kept for compatibility. | + +The project record is stored in localStorage rather than IndexedDB because it is only a few KB and a synchronous write can be flushed reliably when the page is hidden. + +**Recovery on load**, in this order: + +1. A `?settings=` share link wins. +2. Otherwise the saved project's edits are restored. +3. Otherwise the older URL and recipe records are used, as before. + +Then the stored video copy is read from IndexedDB: + +- **The copy is present.** The editor opens with the video and the edits. +- **The copy is missing, or IndexedDB fails.** This can happen after eviction, private mode, or a quota error when saving. A notice names the file, its size and length, and asks the user to choose it again. + - Choosing a file with the same name and byte size resumes the project, trims included. + - A different file keeps the other settings but resets the trim points, like any new video. + - "Discard saved edits" forgets the project. + +**Failures are reported in the "Saved on this device" panel:** + +- If the video copy can't be stored, the user is told they'll need to choose the file again after a refresh. +- If the edit record can't be written, they're told to use "Save project file". + +**Delete saved data** asks for confirmation. It then removes the project record, the recipe and overlay records and the stored video copy, closes the video and resets the editor. Project files the user saved are not affected. + +## Project file format + +"Save project file" downloads `