Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- Ctrl/Cmd-click an inspector **slider** to reset that property to its default
and clear its keyframes (same as Ctrl/Cmd-clicking the label — scale → 1,
opacity → 1, brightness → 100, …).
- **Import from URL** — `POST /api/import-url` downloads an HTTPS video, audio
or image into `./media/` and returns a same-origin `/media/…` src. The
editor **+ URL** button and `fablecut_import_media` (now accepts `https://`
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ same time.
- **Zoom to selection** (<kbd>⇧Z</kbd>) frames all selected clips, not just one
- **IN/OUT work area** — set markers with <kbd>i</kbd> and <kbd>o</kbd> (<kbd>⇧I</kbd> / <kbd>⇧O</kbd> to clear). Enabling **Limit** constrains playback to the marked range and maps <kbd>Home</kbd> / <kbd>End</kbd> to the IN and OUT positions rather than the full timeline. <kbd>t</kbd> splits clips at the markers; <kbd>⇧t</kbd> trims clips to the work (between marker in and marker out) area.
- **Find & close gaps** — a gap is a stretch where every enabled track is empty (black frames). <kbd>g</kbd> jumps the playhead to the next shared gap (wraps; respects IN/OUT when both are set). <kbd>⇧G</kbd> closes the gap under the playhead by pulling later clips left on all enabled tracks.
- **Reset a property** — <kbd>Ctrl/Cmd+click</kbd> an inspector **label** restores that effect/prop to its default *and* clears every keyframe on the channel (paired fields like Crop L/R reset together; transition labels clear the in/out transition). <kbd>Shift+click</kbd> the same label is playhead-local: if you are parked on a keyframe it removes **that** keyframe only; otherwise it sets the value at the playhead to the default (auto-keys if the channel is already animated).
- **Reset a property** — <kbd>Ctrl/Cmd+click</kbd> an inspector **label** or **slider** restores that effect/prop to its default *and* clears every keyframe on the channel (scale → 1, opacity → 1, paired fields like Crop L/R reset together; transition labels clear the in/out transition). <kbd>Shift+click</kbd> a label is playhead-local: if you are parked on a keyframe it removes **that** keyframe only; otherwise it sets the value at the playhead to the default (auto-keys if the channel is already animated).
- **Replace media** — the inspector's **Source** button (any video/audio/image/svg
clip) swaps the underlying file while keeping position, trim, keyframes,
transitions and every effect. Pick another item already in the bin or
Expand Down
49 changes: 35 additions & 14 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,19 @@ function resetPropAtPlayhead(c, k) {
if (k === "font") ensureFont(String(def));
return true;
}
function applyInspectorReset(keys, channelWide) {
const c = getClip(state.selId);
if (!c || !keys.length) return;
pushUndo();
let refused = false;
for (const k of keys) {
if (channelWide) resetPropChannel(c, k);
else refused = !resetPropAtPlayhead(c, k) || refused;
}
if (refused) toast("Move the playhead over the clip to edit its keyframes");
scheduleSave();
renderInspector();
}
/* ◆ : add a keyframe at the playhead, or remove the one already there.
Refused (false) when the playhead is off the clip — there is no "at the
playhead" then, and clamping would plant a keyframe on the clip's edge. */
Expand Down Expand Up @@ -3609,7 +3622,7 @@ function renderInspector(lite) {
const slider = (k, min, max, step, val, unit = "") => {
const shown = fmtInspNum(val, step);
return row(k[0].toUpperCase() + k.slice(1),
`<input type="range" data-k="${k}" min="${min}" max="${max}" step="${step}" value="${shown}">
`<input type="range" data-k="${k}" min="${min}" max="${max}" step="${step}" value="${shown}" title="Ctrl/Cmd-click: reset to default">
<span class="val" data-val="${k}" data-unit="${unit}">${shown}${unit}</span>`, k);
};
let html = (state.selIds.size > 1
Expand Down Expand Up @@ -3706,7 +3719,7 @@ function renderInspector(lite) {
html += `<div class="insp-section"><h3>Text</h3>
${row("Content", `<textarea data-k="text">${p.text}</textarea>`, "", "text")}
${row(hasTextBox(p) && p.boxFit ? "Max size" : "Font size",
`<input type="range" data-k="fontSize" min="12" max="300" step="1" value="${fmtInspNum(p.fontSize, 1)}">
`<input type="range" data-k="fontSize" min="12" max="300" step="1" value="${fmtInspNum(p.fontSize, 1)}" title="Ctrl/Cmd-click: reset to default">
<span class="val" data-val="fontSize" data-unit="px">${fmtInspNum(p.fontSize, 1)}px</span>`, "fontSize")}
${row("Box W/H", `<span class="insp-ctrls">
<input type="number" data-k="boxW" min="0" step="1" value="${p.boxW || 0}" title="Width in px (0 = no box — hug content)" style="max-width:64px">
Expand Down Expand Up @@ -3761,22 +3774,14 @@ function renderInspector(lite) {
const local = e.shiftKey && !all;
if (!all && !local) return;
e.preventDefault();
const keys = lab.dataset.reset.split(",").map((s) => s.trim()).filter(Boolean);
if (!keys.length) return;
pushUndo();
let refused = false;
for (const k of keys) {
if (all) resetPropChannel(c, k); // channel-wide: playhead-independent
else refused = !resetPropAtPlayhead(c, k) || refused;
}
if (refused) toast("Move the playhead over the clip to edit its keyframes");
scheduleSave();
renderInspector();
applyInspectorReset(lab.dataset.reset.split(",").map((s) => s.trim()).filter(Boolean), all);
});
});
els.inspector.querySelectorAll("[data-k]").forEach((input) => {
const k = input.dataset.k;
input.addEventListener("input", () => {
input.addEventListener("input", (e) => {
if (!input.isConnected) return;
if (input.type === "range" && (e.ctrlKey || e.metaKey)) return;
let v = input.type === "checkbox" ? input.checked
: input.type === "range" || input.type === "number" ? parseFloat(input.value)
: input.value;
Expand Down Expand Up @@ -3885,6 +3890,22 @@ function renderInspector(lite) {
syncInspectorOffClip(c);
renderKfGraphsPanel();
}
/* One listener for every slider: capture so preventDefault runs before the
range jumps the thumb to the click. */
els.inspector.addEventListener("pointerdown", (e) => {
if (!(e.ctrlKey || e.metaKey)) return;
const input = e.target.closest?.("input[type=range][data-k]");
if (!input || !els.inspector.contains(input)) return;
e.preventDefault();
const k = input.dataset.k;
if (!k || !Object.hasOwn(DEFAULT_PROPS, k)) return;
applyInspectorReset([k], true);
}, true);
els.inspector.addEventListener("contextmenu", (e) => {
if (!(e.ctrlKey || e.metaKey)) return;
if (!e.target.closest?.("input[type=range][data-k]")) return;
e.preventDefault();
});

/* Off the clip, keyframed fields show their edge value but must not be
editable — a write would land clamped on the clip's edge. Disables those
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ <h2>Keyboard shortcuts</h2>
<tr><td><kbd>Ctrl</kbd>+<kbd>A</kbd> / <kbd>Esc</kbd></td><td>Select all / deselect</td></tr>
<tr><td><kbd>←</kbd> <kbd>→</kbd></td><td>Step 1 frame (⇧ = 1 second)</td></tr>
<tr><td><kbd>Ctrl/Cmd</kbd>+<kbd>←</kbd> / <kbd>Ctrl/Cmd</kbd>+<kbd>→</kbd></td><td>Go to previous / next keyframe (inspector follows)</td></tr>
<tr><td><kbd>Ctrl/Cmd</kbd>+click inspector label</td><td>Reset property + clear all its keyframes</td></tr>
<tr><td><kbd>Ctrl/Cmd</kbd>+click inspector label or slider</td><td>Reset property + clear all its keyframes</td></tr>
<tr><td><kbd>⇧</kbd>+click inspector label</td><td>Reset value at playhead, or remove that keyframe</td></tr>
<tr><td><kbd>[</kbd> / <kbd>]</kbd></td><td>Trim selected in / out to playhead</td></tr>
<tr><td><kbd>Home</kbd> / <kbd>End</kbd></td><td>Jump to start / end</td></tr>
Expand Down
9 changes: 9 additions & 0 deletions test/assets.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,12 @@ test("package.json declares no runtime dependencies", () => {
"FableCut must stay zero-runtime-dependency");
assert.ok(pkg.scripts?.test, "package.json needs a test script so CI can run the suite");
});

test("inspector sliders advertise and handle Ctrl/Cmd-click reset", () => {
const app = fs.readFileSync(path.join(ROOT, "app.js"), "utf8");
assert.match(app, /els\.inspector\.addEventListener\("pointerdown"/,
"slider reset is delegated on the inspector, not bound per range");
assert.match(app, /Ctrl\/Cmd-click: reset to default/);
const html = fs.readFileSync(path.join(ROOT, "index.html"), "utf8");
assert.match(html, /inspector label or slider/);
});
Loading