feat: expand shortcuts and improve YouTube navigation

This commit is contained in:
joshpatra committed 2026-07-26 10:19:11 -04:00
1 parent 04f1414118
commit a2d7f0d9d6
11 files changed
+212 -69

No files matched your search

+25 -9
View File
@@ -159,8 +159,8 @@ var tc = {
lastInteractedMedia: null lastInteractedMedia: null
}; };
var MIN_SPEED = 0.0625; var MIN_SPEED = Number(keyBindingUtils.MIN_SPEED) || 0.01;
var MAX_SPEED = 16; var MAX_SPEED = Number(keyBindingUtils.MAX_SPEED) || 100;
var YT_NATIVE_MIN = 0.25; var YT_NATIVE_MIN = 0.25;
var YT_NATIVE_MAX = 2.0; var YT_NATIVE_MAX = 2.0;
var YT_NATIVE_STEP = 0.05; var YT_NATIVE_STEP = 0.05;
@@ -652,6 +652,7 @@ function normalizeStoredBinding(binding, fallbackCode) {
var normalized = { var normalized = {
action: binding.action, action: binding.action,
code: normalizedCode, code: normalizedCode,
shiftKey: binding.shiftKey === true,
disabled: false, disabled: false,
value: keyBindingUtils.sanitizeActionValue( value: keyBindingUtils.sanitizeActionValue(
binding.action, binding.action,
@@ -1360,7 +1361,8 @@ function matchesKeyBinding(binding, event) {
binding.disabled !== true && binding.disabled !== true &&
typeof binding.code === "string" && typeof binding.code === "string" &&
binding.code.length > 0 && binding.code.length > 0 &&
binding.code === event.code binding.code === event.code &&
(binding.shiftKey === true) === event.shiftKey
); );
} }
@@ -2564,6 +2566,9 @@ function loadInitialRuntimeSettings(attempt) {
}); });
} }
// Install before async settings hydration so SPA-owned window capture handlers
// cannot hide later key events from Speeder.
attachKeydownListeners(document);
loadInitialRuntimeSettings(0); loadInitialRuntimeSettings(0);
function getKeyBindings(action, what = "value") { function getKeyBindings(action, what = "value") {
@@ -4208,7 +4213,7 @@ function setupListener(root) {
if (config.skipResetDisarm !== true) { if (config.skipResetDisarm !== true) {
video.vsc.resetToggleArmed = false; video.vsc.resetToggleArmed = false;
} }
var speed = video.playbackRate; // Preserve full precision (e.g. 0.0625) var speed = video.playbackRate; // Preserve full precision (e.g. 0.01)
video.vsc.speedIndicator.textContent = speed.toFixed(2); video.vsc.speedIndicator.textContent = speed.toFixed(2);
video.vsc.targetSpeed = speed; video.vsc.targetSpeed = speed;
video.vsc.targetSpeedSourceKey = getVideoSourceKey(video); video.vsc.targetSpeedSourceKey = getVideoSourceKey(video);
@@ -4240,7 +4245,7 @@ function setupListener(root) {
if (!video || typeof video.playbackRate === "undefined" || !video.vsc) if (!video || typeof video.playbackRate === "undefined" || !video.vsc)
return; return;
if (shouldIgnoreSuppressedRateChange(video)) return; if (shouldIgnoreSuppressedRateChange(video)) return;
var currentSpeed = video.playbackRate; // Preserve full precision (e.g. 0.0625) var currentSpeed = video.playbackRate; // Preserve full precision (e.g. 0.01)
var pendingRateChange = takePendingRateChange(video, currentSpeed); var pendingRateChange = takePendingRateChange(video, currentSpeed);
if (tc.settings.forceLastSavedSpeed) { if (tc.settings.forceLastSavedSpeed) {
if (pendingRateChange) { if (pendingRateChange) {
@@ -4354,12 +4359,13 @@ function inIframe() {
function attachKeydownListeners(doc) { function attachKeydownListeners(doc) {
// Content scripts already run in every frame. Keeping each listener scoped // Content scripts already run in every frame. Keeping each listener scoped
// to its own document avoids duplicate shortcuts and stale iframe ownership. // to its own frame avoids duplicate shortcuts and stale iframe ownership.
var docs = [doc]; var docs = [doc];
docs.forEach(function(keyDoc) { docs.forEach(function(keyDoc) {
if (keyDoc.vscKeydownListenerAttached) return; var keyTarget = keyDoc.defaultView || keyDoc;
keyDoc.addEventListener( if (keyTarget.vscKeydownListenerAttached) return;
keyTarget.addEventListener(
"keydown", "keydown",
function(event) { function(event) {
if ( if (
@@ -4411,7 +4417,7 @@ function attachKeydownListeners(doc) {
}, },
true true
); );
keyDoc.vscKeydownListenerAttached = true; keyTarget.vscKeydownListenerAttached = true;
}); });
} }
@@ -4816,6 +4822,16 @@ function resolveActionMediaTargets(event, specificVideo) {
if (tc.settings.shortcutTargetMode === "all") return candidates; if (tc.settings.shortcutTargetMode === "all") return candidates;
if (
isOnYouTube() &&
/^\/(watch|live)(\/|$)/.test(location.pathname)
) {
var youtubeMain = candidates.find(function(video) {
return video.closest && video.closest("#movie_player");
});
if (youtubeMain) return [youtubeMain];
}
var pointer = var pointer =
tc.lastPointerPosition && tc.lastPointerPosition.document === docContext tc.lastPointerPosition && tc.lastPointerPosition.document === docContext
? tc.lastPointerPosition ? tc.lastPointerPosition
+2 -10
View File
@@ -54,11 +54,7 @@ function normalizedSettingsForExport(rawStorage) {
// lastSpeed is useful user data, but it is intentionally outside the // lastSpeed is useful user data, but it is intentionally outside the
// managed options diff because content scripts update it at runtime. // managed options diff because content scripts update it at runtime.
if (Object.prototype.hasOwnProperty.call(raw, "lastSpeed")) { if (Object.prototype.hasOwnProperty.call(raw, "lastSpeed")) {
if ( if (Number.isFinite(expanded.lastSpeed)) {
Number.isFinite(expanded.lastSpeed) &&
expanded.lastSpeed >= 0.0625 &&
expanded.lastSpeed <= 16
) {
normalized.lastSpeed = expanded.lastSpeed; normalized.lastSpeed = expanded.lastSpeed;
} }
} }
@@ -117,11 +113,7 @@ function persistImportedSyncSettings(currentRaw, importedRaw, callback) {
importedRaw && importedRaw &&
Object.prototype.hasOwnProperty.call(importedRaw, "lastSpeed") Object.prototype.hasOwnProperty.call(importedRaw, "lastSpeed")
) { ) {
if ( if (Number.isFinite(expanded.lastSpeed)) {
Number.isFinite(expanded.lastSpeed) &&
expanded.lastSpeed >= 0.0625 &&
expanded.lastSpeed <= 16
) {
mutation.set.lastSpeed = expanded.lastSpeed; mutation.set.lastSpeed = expanded.lastSpeed;
} }
} }
+4 -4
View File
@@ -250,7 +250,7 @@
</div> </div>
</div> </div>
<select id="addShortcutSelector"> <select id="addShortcutSelector" aria-label="Add another shortcut">
<option value="">Add shortcut&hellip;</option> <option value="">Add shortcut&hellip;</option>
</select> </select>
</section> </section>
@@ -843,7 +843,7 @@
</div> </div>
<div class="site-rule-option site-rule-option-field"> <div class="site-rule-option site-rule-option-field">
<label <label
>Preferred speed (0.0625&ndash;16):<br /><em >Preferred speed (0.01&ndash;100):<br /><em
>Overrides the Preferred speed action for matching >Overrides the Preferred speed action for matching
URLs. It does not force every video to start at this URLs. It does not force every video to start at this
speed.</em speed.</em
@@ -1022,10 +1022,10 @@
<section class="settings-card action-card"> <section class="settings-card action-card">
<div class="section-heading"> <div class="section-heading">
<h3>Actions</h3> <h3>Actions</h3>
<p class="section-intro">Save, restore, export, or import settings.</p> <p class="section-intro">Changes save automatically.</p>
</div> </div>
<div class="action-row"> <div class="action-row">
<button id="save">Save Changes</button> <button id="save">Save Now</button>
<button id="restore">Restore Defaults</button> <button id="restore">Restore Defaults</button>
<button id="exportSettings">Export Settings</button> <button id="exportSettings">Export Settings</button>
<button id="importSettings">Import Settings</button> <button id="importSettings">Import Settings</button>
+52 -34
View File
@@ -164,9 +164,32 @@ function createDefaultBinding(action, code, value) {
var tcDefaults = vscGetSettingsDefaults(); var tcDefaults = vscGetSettingsDefaults();
var optionsSyncSettingsLoaded = false; var optionsSyncSettingsLoaded = false;
var autoSaveTimer = null;
function scheduleAutoSave() {
if (!optionsSyncSettingsLoaded) return;
clearTimeout(autoSaveTimer);
var sourceSaveButton = document.getElementById("save");
var scheduledTimer = setTimeout(function () {
if (
autoSaveTimer !== scheduledTimer ||
!sourceSaveButton ||
!sourceSaveButton.isConnected
) {
return;
}
autoSaveTimer = null;
save_options();
}, 300);
autoSaveTimer = scheduledTimer;
}
function setOptionsSyncSettingsLoaded(loaded) { function setOptionsSyncSettingsLoaded(loaded) {
optionsSyncSettingsLoaded = loaded === true; optionsSyncSettingsLoaded = loaded === true;
if (!optionsSyncSettingsLoaded) {
clearTimeout(autoSaveTimer);
autoSaveTimer = null;
}
var saveButton = document.getElementById("save"); var saveButton = document.getElementById("save");
if (saveButton) { if (saveButton) {
saveButton.disabled = !optionsSyncSettingsLoaded; saveButton.disabled = !optionsSyncSettingsLoaded;
@@ -302,34 +325,16 @@ function refreshAddShortcutSelector() {
selector.remove(1); selector.remove(1);
} }
// Find all currently used actions
const usedActions = new Set();
document.querySelectorAll(".shortcut-row").forEach((row) => {
const action = row.dataset.action;
if (action) {
usedActions.add(action);
}
});
// Add all unused actions
Object.keys(actionLabels).forEach((action) => { Object.keys(actionLabels).forEach((action) => {
if (!usedActions.has(action)) {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = action; option.value = action;
option.text = actionLabels[action]; option.text = actionLabels[action];
selector.appendChild(option); selector.appendChild(option);
}
}); });
// If no available actions, hide or disable the selector
if (selector.options.length === 1) {
selector.disabled = true;
selector.options[0].text = "All shortcuts added";
} else {
selector.disabled = false; selector.disabled = false;
selector.options[0].text = "Add shortcut\u2026"; selector.options[0].text = "Add shortcut\u2026";
} }
}
function refreshSiteRuleAddShortcutSelector(ruleEl) { function refreshSiteRuleAddShortcutSelector(ruleEl) {
if (!ruleEl) return; if (!ruleEl) return;
@@ -340,33 +345,20 @@ function refreshSiteRuleAddShortcutSelector(ruleEl) {
selector.remove(1); selector.remove(1);
} }
var usedActions = new Set();
ruleEl.querySelectorAll(".site-shortcuts-rows .shortcut-row.customs").forEach(function (row) {
var action = row.dataset.action;
if (action) usedActions.add(action);
});
Object.keys(actionLabels).forEach(function (action) { Object.keys(actionLabels).forEach(function (action) {
if (!usedActions.has(action)) {
var option = document.createElement("option"); var option = document.createElement("option");
option.value = action; option.value = action;
option.textContent = actionLabels[action]; option.textContent = actionLabels[action];
selector.appendChild(option); selector.appendChild(option);
}
}); });
var overrideShortcutsOn = var overrideShortcutsOn =
ruleEl.querySelector(".override-shortcuts") && ruleEl.querySelector(".override-shortcuts") &&
ruleEl.querySelector(".override-shortcuts").checked; ruleEl.querySelector(".override-shortcuts").checked;
if (selector.options.length === 1) {
selector.disabled = true;
selector.options[0].text = "All shortcuts added";
} else {
selector.disabled = !overrideShortcutsOn; selector.disabled = !overrideShortcutsOn;
selector.options[0].text = "Add shortcut\u2026"; selector.options[0].text = "Add shortcut\u2026";
} }
}
function getGlobalBindingSnapshotForSiteShortcut(action) { function getGlobalBindingSnapshotForSiteShortcut(action) {
var row = document.querySelector( var row = document.querySelector(
@@ -424,7 +416,10 @@ function readOptionalPreferredSpeedInput(input) {
if (!rawValue) return undefined; if (!rawValue) return undefined;
var parsed = Number(rawValue); var parsed = Number(rawValue);
if (!Number.isFinite(parsed)) return undefined; if (!Number.isFinite(parsed)) return undefined;
return Math.min(16, Math.max(0.0625, parsed)); return Math.min(
keyBindingUtils.MAX_SPEED,
Math.max(keyBindingUtils.MIN_SPEED, parsed)
);
} }
function updateSiteRuleToggleIcon(toggleButton, action) { function updateSiteRuleToggleIcon(toggleButton, action) {
@@ -560,6 +555,7 @@ function normalizeStoredBinding(binding, fallbackCode) {
var normalized = { var normalized = {
code: normalizedCode, code: normalizedCode,
shiftKey: binding.shiftKey === true,
disabled: false disabled: false
}; };
@@ -578,7 +574,7 @@ function formatBindingCode(code) {
function getBindingLabel(binding) { function getBindingLabel(binding) {
if (!binding) return ""; if (!binding) return "";
if (binding.disabled) return ""; if (binding.disabled) return "";
return formatBindingCode(binding.code); return (binding.shiftKey ? "Shift+" : "") + formatBindingCode(binding.code);
} }
function setShortcutInputBinding(input, binding) { function setShortcutInputBinding(input, binding) {
@@ -591,6 +587,7 @@ function captureBindingFromEvent(event) {
if (typeof event.code !== "string" || event.code.length === 0) return null; if (typeof event.code !== "string" || event.code.length === 0) return null;
return { return {
code: event.code, code: event.code,
shiftKey: event.shiftKey === true,
disabled: false disabled: false
}; };
} }
@@ -600,6 +597,7 @@ function recordKeyPress(event) {
if (event.key === "Backspace") { if (event.key === "Backspace") {
setShortcutInputBinding(event.target, null); setShortcutInputBinding(event.target, null);
scheduleAutoSave();
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
return; return;
@@ -607,6 +605,7 @@ function recordKeyPress(event) {
if (event.key === "Escape") { if (event.key === "Escape") {
setShortcutInputBinding(event.target, createDisabledBinding()); setShortcutInputBinding(event.target, createDisabledBinding());
scheduleAutoSave();
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
return; return;
@@ -616,6 +615,7 @@ function recordKeyPress(event) {
if (!binding) return; if (!binding) return;
setShortcutInputBinding(event.target, binding); setShortcutInputBinding(event.target, binding);
scheduleAutoSave();
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }
@@ -770,6 +770,7 @@ function createKeyBindings(item) {
keyBindings.push({ keyBindings.push({
action: action, action: action,
code: binding.code, code: binding.code,
shiftKey: binding.shiftKey === true,
disabled: binding.disabled === true, disabled: binding.disabled === true,
value: bindingValue, value: bindingValue,
force: false, force: false,
@@ -807,6 +808,8 @@ function validate() {
} }
function save_options() { function save_options() {
clearTimeout(autoSaveTimer);
autoSaveTimer = null;
var status = document.getElementById("status"); var status = document.getElementById("status");
if (!optionsSyncSettingsLoaded) { if (!optionsSyncSettingsLoaded) {
status.textContent = status.textContent =
@@ -1092,6 +1095,7 @@ function save_options() {
shortcuts.push({ shortcuts.push({
action: action, action: action,
code: binding.code, code: binding.code,
shiftKey: binding.shiftKey === true,
disabled: binding.disabled === true, disabled: binding.disabled === true,
value: shortcutValue, value: shortcutValue,
force: forceCheckbox ? forceCheckbox.checked : false force: forceCheckbox ? forceCheckbox.checked : false
@@ -1551,6 +1555,7 @@ function initControlBarEditor() {
var zone = e.target.closest(".cb-dropzone"); var zone = e.target.closest(".cb-dropzone");
if (zone) { if (zone) {
e.preventDefault(); e.preventDefault();
scheduleAutoSave();
} }
clearControlBarDropTargets(null); clearControlBarDropTargets(null);
@@ -1825,13 +1830,16 @@ function restore_options(callback) {
document.querySelectorAll(".customs:not([id])").forEach((row) => row.remove()); document.querySelectorAll(".customs:not([id])").forEach((row) => row.remove());
var usedDefaultShortcutRows = new Set();
storage.keyBindings.forEach((item) => { storage.keyBindings.forEach((item) => {
var row = document.getElementById(item.action); var row = document.getElementById(item.action);
var normalizedBinding = normalizeStoredBinding(item); var normalizedBinding = normalizeStoredBinding(item);
if (!row) { if (!row || usedDefaultShortcutRows.has(item.action)) {
add_shortcut(item.action, item.value); add_shortcut(item.action, item.value);
row = document.querySelector(".shortcut-row.customs:last-of-type"); row = document.querySelector(".shortcut-row.customs:last-of-type");
} else {
usedDefaultShortcutRows.add(item.action);
} }
if (!row) return; if (!row) return;
@@ -2030,11 +2038,13 @@ document.addEventListener("DOMContentLoaded", function () {
if (siteRuleForShortcut) { if (siteRuleForShortcut) {
refreshSiteRuleAddShortcutSelector(siteRuleForShortcut); refreshSiteRuleAddShortcutSelector(siteRuleForShortcut);
} }
scheduleAutoSave();
return; return;
} }
var removeSiteRuleButton = targetEl.closest(".remove-site-rule"); var removeSiteRuleButton = targetEl.closest(".remove-site-rule");
if (removeSiteRuleButton) { if (removeSiteRuleButton) {
removeSiteRuleButton.closest(".site-rule").remove(); removeSiteRuleButton.closest(".site-rule").remove();
scheduleAutoSave();
return; return;
} }
var toggleButton = targetEl.closest(".toggle-site-rule"); var toggleButton = targetEl.closest(".toggle-site-rule");
@@ -2104,4 +2114,12 @@ document.addEventListener("DOMContentLoaded", function () {
} }
} }
}); });
document.addEventListener("change", (event) => {
if (
event.target.id !== "addShortcutSelector" &&
!event.target.closest("#lucideIconSettings")
) {
scheduleAutoSave();
}
});
}); });
+9 -4
View File
@@ -8,6 +8,9 @@
root.SpeederShared = root.SpeederShared || {}; root.SpeederShared = root.SpeederShared || {};
root.SpeederShared.keyBindings = exports; root.SpeederShared.keyBindings = exports;
})(typeof globalThis !== "undefined" ? globalThis : this, function() { })(typeof globalThis !== "undefined" ? globalThis : this, function() {
var MIN_SPEED = 0.01;
var MAX_SPEED = 100;
function normalizeBindingKey(key) { function normalizeBindingKey(key) {
if (typeof key !== "string" || key.length === 0) return null; if (typeof key !== "string" || key.length === 0) return null;
if (key === "Spacebar") return " "; if (key === "Spacebar") return " ";
@@ -127,15 +130,15 @@
if (!Number.isFinite(numericValue)) return "must be a finite number"; if (!Number.isFinite(numericValue)) return "must be a finite number";
if ( if (
(action === "slower" || action === "faster") && (action === "slower" || action === "faster") &&
(numericValue <= 0 || numericValue > 16) (numericValue <= 0 || numericValue > MAX_SPEED)
) { ) {
return "must be greater than 0 and no more than 16"; return "must be greater than 0 and no more than " + MAX_SPEED;
} }
if ( if (
action === "fast" && action === "fast" &&
(numericValue < 0.0625 || numericValue > 16) (numericValue < MIN_SPEED || numericValue > MAX_SPEED)
) { ) {
return "must be between 0.0625 and 16"; return "must be between " + MIN_SPEED + " and " + MAX_SPEED;
} }
if ( if (
(action === "rewind" || action === "advance") && (action === "rewind" || action === "advance") &&
@@ -162,6 +165,8 @@
} }
return { return {
MAX_SPEED: MAX_SPEED,
MIN_SPEED: MIN_SPEED,
getActionValueError: getActionValueError, getActionValueError: getActionValueError,
getLegacyKeyCode: getLegacyKeyCode, getLegacyKeyCode: getLegacyKeyCode,
inferBindingCode: inferBindingCode, inferBindingCode: inferBindingCode,
+7 -5
View File
@@ -4,6 +4,8 @@
var LEGACY_SITE_RULES_DIFF_FORMAT = "defaults-diff-v1"; var LEGACY_SITE_RULES_DIFF_FORMAT = "defaults-diff-v1";
var SITE_RULES_DIFF_FORMAT = "defaults-diff-v2"; var SITE_RULES_DIFF_FORMAT = "defaults-diff-v2";
var REMOVED_DEFAULT_RULE_KEYS_META = "removedDefaultRuleKeys"; var REMOVED_DEFAULT_RULE_KEYS_META = "removedDefaultRuleKeys";
var MIN_SPEED = 0.01;
var MAX_SPEED = 100;
var DEFAULT_BUTTONS = ["rewind", "slower", "faster", "advance", "display"]; var DEFAULT_BUTTONS = ["rewind", "slower", "faster", "advance", "display"];
var LEGACY_SYNC_KEYS = [ var LEGACY_SYNC_KEYS = [
"resetSpeed", "resetSpeed",
@@ -363,7 +365,7 @@
return clampFiniteNumber(value, 250, 1000, undefined); return clampFiniteNumber(value, 250, 1000, undefined);
} }
if (key === "preferredSpeed") { if (key === "preferredSpeed") {
return clampFiniteNumber(value, 0.0625, 16, undefined); return clampFiniteNumber(value, MIN_SPEED, MAX_SPEED, undefined);
} }
if (key === "shortcuts" && Array.isArray(value)) { if (key === "shortcuts" && Array.isArray(value)) {
return sanitizeStoredBindingValues(value).map(function(shortcut) { return sanitizeStoredBindingValues(value).map(function(shortcut) {
@@ -749,9 +751,9 @@
var value = Number(normalized.value); var value = Number(normalized.value);
var invalid = !Number.isFinite(value); var invalid = !Number.isFinite(value);
if (action === "slower" || action === "faster") { if (action === "slower" || action === "faster") {
invalid = invalid || value <= 0 || value > 16; invalid = invalid || value <= 0 || value > MAX_SPEED;
} else if (action === "fast") { } else if (action === "fast") {
invalid = invalid || value < 0.0625 || value > 16; invalid = invalid || value < MIN_SPEED || value > MAX_SPEED;
} else if (action === "rewind" || action === "advance") { } else if (action === "rewind" || action === "advance") {
invalid = invalid || value < 0; invalid = invalid || value < 0;
} else if (action === "louder" || action === "softer") { } else if (action === "louder" || action === "softer") {
@@ -885,8 +887,8 @@
expanded.lastSpeed = clampFiniteNumber( expanded.lastSpeed = clampFiniteNumber(
expanded.lastSpeed, expanded.lastSpeed,
0.0625, MIN_SPEED,
16, MAX_SPEED,
DEFAULT_SETTINGS.lastSpeed DEFAULT_SETTINGS.lastSpeed
); );
expanded.hideWithControlsTimer = clampFiniteNumber( expanded.hideWithControlsTimer = clampFiniteNumber(
+48 -1
View File
@@ -320,6 +320,7 @@ describe("inject.js media/controller lifecycle regressions", () => {
src: "blob:https://www.youtube.com/hover-preview", src: "blob:https://www.youtube.com/hover-preview",
mountRect: makeRect(0, 0, 320, 180) mountRect: makeRect(0, 0, 320, 180)
}); });
preview.mount.id = "inline-preview-player";
document.dispatchEvent( document.dispatchEvent(
new MouseEvent("mousemove", { bubbles: true, clientX: 100, clientY: 90 }) new MouseEvent("mousemove", { bubbles: true, clientX: 100, clientY: 90 })
); );
@@ -333,6 +334,10 @@ describe("inject.js media/controller lifecycle regressions", () => {
src: "blob:https://www.youtube.com/main-player", src: "blob:https://www.youtube.com/main-player",
mountRect: makeRect(0, 0, 1280, 720) mountRect: makeRect(0, 0, 1280, 720)
}); });
main.mount.id = "movie_player";
document.dispatchEvent(
new MouseEvent("mousemove", { bubbles: true, clientX: 100, clientY: 90 })
);
document.dispatchEvent( document.dispatchEvent(
new KeyboardEvent("keydown", { new KeyboardEvent("keydown", {
bubbles: true, bubbles: true,
@@ -342,11 +347,53 @@ describe("inject.js media/controller lifecycle regressions", () => {
}) })
); );
expect(window.tc.lastPointerPosition).toBeNull(); expect(window.tc.lastPointerPosition).not.toBeNull();
expect(preview.video.playbackRate).toBe(1); expect(preview.video.playbackRate).toBe(1);
expect(main.video.playbackRate).toBe(1.1); expect(main.video.playbackRate).toBe(1.1);
}); });
it("captures SPA shortcuts before later page handlers and distinguishes Shift", async () => {
vi.useFakeTimers();
bootInject({
syncGetDelayMs: 25,
syncData: {
keyBindings: [
{ action: "advance", code: "KeyX", value: 10 },
{ action: "advance", code: "KeyX", shiftKey: true, value: 3 }
]
}
});
expect(window.vscKeydownListenerAttached).toBe(true);
window.addEventListener(
"keydown",
(event) => event.stopImmediatePropagation(),
true
);
await vi.advanceTimersByTimeAsync(25);
await settleLifecycle();
const { video } = createControlledVideo();
video.currentTime = 50;
video.dispatchEvent(
new KeyboardEvent("keydown", {
bubbles: true,
code: "KeyX",
key: "x"
})
);
video.dispatchEvent(
new KeyboardEvent("keydown", {
bubbles: true,
code: "KeyX",
key: "X",
shiftKey: true
})
);
expect(video.currentTime).toBe(63);
});
it("skips ambient loops by default and includes them when explicitly enabled", async () => { it("skips ambient loops by default and includes them when explicitly enabled", async () => {
bootInject(); bootInject();
await settleLifecycle(); await settleLifecycle();
+42
View File
@@ -106,6 +106,48 @@ describe("options page", () => {
expect(chrome.storage.sync.set).not.toHaveBeenCalled(); expect(chrome.storage.sync.set).not.toHaveBeenCalled();
}); });
it("adds and restores duplicate actions with Shift bindings", async () => {
const chrome = await setupOptions();
const selector = document.getElementById("addShortcutSelector");
expect(selector.querySelector('option[value="rewind"]')).not.toBeNull();
selector.value = "rewind";
selector.dispatchEvent(new window.Event("change", { bubbles: true }));
const duplicate = document.querySelector(
'.shortcut-row.customs[data-action="rewind"]'
);
duplicate.querySelector(".customKey").dispatchEvent(
new window.KeyboardEvent("keydown", {
key: "Z",
code: "KeyZ",
shiftKey: true,
bubbles: true
})
);
duplicate.querySelector(".customValue").value = "3";
globalThis.save_options();
expect(
chrome.storage.sync.__state.keyBindings.filter(
(binding) => binding.action === "rewind"
)
).toEqual([
expect.objectContaining({ value: 10, shiftKey: false }),
expect.objectContaining({ value: 3, shiftKey: true })
]);
globalThis.restore_options();
await flushAsyncWork();
expect(
document.querySelectorAll('.shortcut-row[data-action="rewind"]')
).toHaveLength(2);
expect(
document.querySelector(
'.shortcut-row.customs[data-action="rewind"] .customKey'
).value
).toBe("Shift+Z");
});
it("shows a more-menu trigger for collapsed site rules and a collapse trigger when open", async () => { it("shows a more-menu trigger for collapsed site rules and a collapse trigger when open", async () => {
await setupOptions({ sync: { siteRules: [] } }); await setupOptions({ sync: { siteRules: [] } });
+16
View File
@@ -67,6 +67,22 @@ describe("options.js", () => {
expect(chrome.storage.sync.set).not.toHaveBeenCalled(); expect(chrome.storage.sync.set).not.toHaveBeenCalled();
}); });
it("automatically saves changed settings", async () => {
const chrome = bootOptions({ syncData: { rememberSpeed: false } });
await flushAsyncWork(3);
vi.useFakeTimers();
chrome.storage.sync.set.mockClear();
const rememberSpeed = document.getElementById("rememberSpeed");
rememberSpeed.checked = true;
rememberSpeed.dispatchEvent(new Event("change", { bubbles: true }));
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(300);
expect(chrome.storage.sync._dump().rememberSpeed).toBe(true);
});
it("does not partially save options when a required site shortcut is invalid", async () => { it("does not partially save options when a required site shortcut is invalid", async () => {
const chrome = bootOptions({ syncData: { rememberSpeed: false } }); const chrome = bootOptions({ syncData: { rememberSpeed: false } });
await flushAsyncWork(3); await flushAsyncWork(3);
+2 -2
View File
@@ -244,7 +244,7 @@ describe("canonical settings storage", () => {
forceLastSavedSpeed: "true", forceLastSavedSpeed: "true",
controllerOpacity: 3, controllerOpacity: 3,
subtitleNudgeInterval: -5, subtitleNudgeInterval: -5,
preferredSpeed: 50 preferredSpeed: 500
} }
] ]
}); });
@@ -257,7 +257,7 @@ describe("canonical settings storage", () => {
expect(rule.forceLastSavedSpeed).toBe(true); expect(rule.forceLastSavedSpeed).toBe(true);
expect(rule.controllerOpacity).toBe(1); expect(rule.controllerOpacity).toBe(1);
expect(rule.subtitleNudgeInterval).toBe(250); expect(rule.subtitleNudgeInterval).toBe(250);
expect(rule.preferredSpeed).toBe(16); expect(rule.preferredSpeed).toBe(100);
}); });
it("falls back for null and blank numeric settings instead of coercing zero", () => { it("falls back for null and blank numeric settings instead of coercing zero", () => {
+5
View File
@@ -158,6 +158,11 @@ describe("shared helpers", () => {
"NumpadAdd" "NumpadAdd"
); );
expect(keyBindingUtils.getLegacyKeyCode({ key: 65 })).toBe(65); expect(keyBindingUtils.getLegacyKeyCode({ key: 65 })).toBe(65);
expect(keyBindingUtils.getActionValueError("fast", 0.01)).toBeNull();
expect(keyBindingUtils.getActionValueError("fast", 100)).toBeNull();
expect(keyBindingUtils.getActionValueError("fast", 0.009)).toContain(
"between 0.01 and 100"
);
}); });
it("builds and parses import/export payloads", () => { it("builds and parses import/export payloads", () => {