Skip to content

feat: add disable/enable track buttons, remove extra track labels - #20

Merged
ronak-create merged 2 commits into
ronak-create:mainfrom
PlkMarudny:disable-tracks
Jul 15, 2026
Merged

feat: add disable/enable track buttons, remove extra track labels#20
ronak-create merged 2 commits into
ronak-create:mainfrom
PlkMarudny:disable-tracks

Conversation

@PlkMarudny

@PlkMarudny PlkMarudny commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Tracks can be disabled by buttons at the beginning of the track; they are dimmed and buttons state clearly shows the current status. 'video' and 'audio' labels removed as redundant, there are already A(i) and V(i) labels.

image

Type of change

  • Bug fix
  • New feature (GUI)
  • Docs
  • Refactor / internal

How was it verified?

  • node --check server.js && node --check app.js && node --check mcp-server.js passes
  • Opened the editor and confirmed the change in preview
  • Confirmed the change in an export (fast or realtime), if it affects rendering
  • Updated CLAUDE.md / README.md if the schema, props, or API changed

Checklist

  • No new runtime dependencies added
  • Preview and export render identically (single compositor)
  • Commits are focused and messages are descriptive

Summary by CodeRabbit

  • New Features

    • Added controls to enable or disable individual timeline tracks.
    • Track enable/disable states are now saved and restored between sessions.
    • Disabled tracks are excluded from playback, editing interactions, selection/hit-testing, and fast export; export warns which disabled tracks will be omitted.
  • Style

    • Reduced the timeline track header width.
    • Improved disabled-state visuals and track toggle styling.
  • Documentation

    • Added an export overlay status area for showing export-related warnings.
Tracks can be disabled by buttons at the beginning of the track; they are dimmed and buttons state clearly shows the current status. 'video' and 'audio' lables removed as redundant, there are already A(i) and V(i) labels.
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e439eb2a-5ddd-448a-9474-3f51580b8ce3

📥 Commits

Reviewing files that changed from the base of the PR and between 1216fec and d7c406c.

📒 Files selected for processing (3)
  • app.js
  • index.html
  • style.css
🚧 Files skipped from review as they are similar to previous changes (2)
  • style.css
  • app.js

📝 Walkthrough

Walkthrough

Disabled-track state is persisted in localStorage, exposed through timeline toggle controls, and enforced across playback, editing interactions, frame rendering, and fast export. Export setup now warns when disabled tracks contain clips, with corresponding styling and compact timeline headers.

Changes

Disabled Track Flow

Layer / File(s) Summary
Track state and timeline controls
app.js, style.css
Disabled tracks are loaded and saved through localStorage; header toggles update ARIA attributes, icons, disabled styling, and timeline header layout.
Playback and editing enforcement
app.js
Playback, paused seeking, frame composition, selection overlays, and clip hit-testing ignore disabled tracks.
Fast export filtering and warning
app.js, index.html, style.css
Export setup warns when disabled tracks contain clips; video seeking, audio mixing, and frame asset preparation skip disabled tracks.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Editor
  participant TrackState
  participant PlaybackRenderer
  participant ExportSetup
  participant FastExport
  Editor->>TrackState: toggle track enabled state
  TrackState->>PlaybackRenderer: apply enabled-track filtering
  PlaybackRenderer->>PlaybackRenderer: skip disabled playback and rendering
  TrackState->>ExportSetup: provide disabled track set
  ExportSetup->>FastExport: start export with warning state
  FastExport->>FastExport: omit disabled video, audio, and assets
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main UI change: adding track enable/disable buttons and removing redundant track labels.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
app.js (3)

2209-2220: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove redundant .slice() call.

Since Array.prototype.filter() already returns a newly created shallow copy of the array, the subsequent .slice() before .reverse() is redundant and can be omitted.

♻️ Proposed cleanup
 function pickClipAt(pt, W, H) {
   const seq = [];
-  for (const tr of TRACKS.filter((tk) => tk.kind === "video" && isTrackEnabled(tk.id)).slice().reverse()) {
+  for (const tr of TRACKS.filter((tk) => tk.kind === "video" && isTrackEnabled(tk.id)).reverse()) {
     for (const c of project.clips.filter((c) => c.track === tr.id && activeAt(c, state.time)).sort((a, b) => a.start - b.start))
       if (isVisualClip(c)) seq.push(c);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app.js` around lines 2209 - 2220, Remove the redundant slice() call from the
TRACKS.filter(...).reverse() chain in pickClipAt, keeping the existing filtering
and reverse iteration behavior unchanged.

161-177: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Update the preview immediately when toggling tracks while paused.

Currently, toggling a track updates the DOM and state, but the canvas won't reflect the change until the next timeline interaction or playback frame. Calling seekMediaWhilePaused() and drawFrame() when playback is paused ensures the UI is immediately responsive to the track state change.

💡 Proposed fix to update the preview synchronously
 function toggleTrackEnabled(id) {
   if (state.disabledTracks.has(id)) state.disabledTracks.delete(id);
   else state.disabledTracks.add(id);
   saveDisabledTracks();
   const head = els.trackHeaders.querySelector(`.track-head[data-track="${id}"]`);
   const row = els.tracks.querySelector(`.track[data-track="${id}"]`);
   const on = isTrackEnabled(id);
   if (head) {
     head.classList.toggle("disabled", !on);
     const btn = head.querySelector(".track-toggle");
     if (btn) {
       btn.setAttribute("aria-pressed", on ? "true" : "false");
       btn.title = on ? "Disable track" : "Enable track";
     }
   }
   if (row) row.classList.toggle("disabled", !on);
+
+  if (!state.playing) {
+    seekMediaWhilePaused();
+    drawFrame();
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app.js` around lines 161 - 177, Update toggleTrackEnabled to refresh the
preview after changing the track state when playback is paused: call
seekMediaWhilePaused() and drawFrame() in the paused path, while preserving the
existing DOM updates and avoiding unnecessary refreshes during playback.

1699-1715: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Defer expensive property calculations for inactive or disabled clips.

evalProps(c, t) and mediaTimeAt(c, t) are computed for every clip on every frame, even when the clip is inactive or the track is disabled. Moving these inside the if block prevents unnecessary processing (such as keyframe interpolation) and significantly optimizes the render loop.

⚡ Proposed performance optimization
     const enabled = isTrackEnabled(c.track);
-    const p = evalProps(c, t);
-    const sp = clamp(+p.speed || 1, 0.1, 8);
-    const mt = mediaTimeAt(c, t);
-    if (state.playing && enabled && activeAt(c, t)) {
+    if (state.playing && enabled && activeAt(c, t)) {
+      const p = evalProps(c, t);
+      const sp = clamp(+p.speed || 1, 0.1, 8);
+      const mt = mediaTimeAt(c, t);
       if (el.playbackRate !== sp) { try { el.playbackRate = sp; } catch {} }
       if (el.paused) el.play().catch(() => {});
       if (Math.abs(el.currentTime - mt) > 0.25 * sp) { try { el.currentTime = mt; } catch {} }
       const vol = clamp(p.volume, 0, 4);
       const g = runtime.clipGain.get(c.id);
       if (g) g.gain.value = vol;
       else el.volume = clamp(vol, 0, 1);
     } else {
       if (!el.paused) el.pause();
       const g = runtime.clipGain.get(c.id);
       if (g) g.gain.value = 0;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app.js` around lines 1699 - 1715, Move the evalProps(c, t) and mediaTimeAt(c,
t) calls inside the state.playing && enabled && activeAt(c, t) branch, computing
p before speed and volume use and mt only where synchronization is needed; keep
inactive or disabled clips from performing these calculations while preserving
existing playback behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app.js`:
- Around line 2209-2220: Remove the redundant slice() call from the
TRACKS.filter(...).reverse() chain in pickClipAt, keeping the existing filtering
and reverse iteration behavior unchanged.
- Around line 161-177: Update toggleTrackEnabled to refresh the preview after
changing the track state when playback is paused: call seekMediaWhilePaused()
and drawFrame() in the paused path, while preserving the existing DOM updates
and avoiding unnecessary refreshes during playback.
- Around line 1699-1715: Move the evalProps(c, t) and mediaTimeAt(c, t) calls
inside the state.playing && enabled && activeAt(c, t) branch, computing p before
speed and volume use and mt only where synchronization is needed; keep inactive
or disabled clips from performing these calculations while preserving existing
playback behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9d715691-0449-45b6-a602-6c256ee80663

📥 Commits

Reviewing files that changed from the base of the PR and between f44074d and 1216fec.

📒 Files selected for processing (2)
  • app.js
  • style.css
@PlkMarudny PlkMarudny mentioned this pull request Jul 15, 2026
11 tasks

@ronak-create ronak-create left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — merging. Two follow-ups worth tracking:

  1. Persistence home: disabled-track state lives in a single global localStorage key rather than project.json. Consequences: the same project.json exports differently depending on browser state; MCP/agent workflows can't see or set the flag; and the state isn't per-project (disabling V2 here disables it in every project opened in this browser). Since #21 persists its work-area markers in project.json, a disabledTracks array there would fit the architecture better. Fine as a follow-up PR.

  2. Stale frame on re-enable while paused: seekMediaWhilePaused() skips disabled tracks and toggleTrackEnabled() doesn't force a reseek — re-enabling a video track after moving the playhead shows the old frame until the next seek. Calling seekMediaWhilePaused() at the end of toggleTrackEnabled() would fix it.

Also: nice catch zeroing the gain node in syncMedia's else branch. Verified node --check passes on the branch head, and export parity holds by code reading (fast export reuses drawFrame, and renderAudioMix filters disabled tracks).

@ronak-create
ronak-create merged commit 295c5a1 into ronak-create:main Jul 15, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants