mirror of
https://github.com/SoPat712/Speeder.git
synced 2026-08-19 11:52:31 -04:00
feat: expand shortcuts and improve YouTube navigation
This commit is contained in:
@@ -159,8 +159,8 @@ var tc = {
|
||||
lastInteractedMedia: null
|
||||
};
|
||||
|
||||
var MIN_SPEED = 0.0625;
|
||||
var MAX_SPEED = 16;
|
||||
var MIN_SPEED = Number(keyBindingUtils.MIN_SPEED) || 0.01;
|
||||
var MAX_SPEED = Number(keyBindingUtils.MAX_SPEED) || 100;
|
||||
var YT_NATIVE_MIN = 0.25;
|
||||
var YT_NATIVE_MAX = 2.0;
|
||||
var YT_NATIVE_STEP = 0.05;
|
||||
@@ -652,6 +652,7 @@ function normalizeStoredBinding(binding, fallbackCode) {
|
||||
var normalized = {
|
||||
action: binding.action,
|
||||
code: normalizedCode,
|
||||
shiftKey: binding.shiftKey === true,
|
||||
disabled: false,
|
||||
value: keyBindingUtils.sanitizeActionValue(
|
||||
binding.action,
|
||||
@@ -1360,7 +1361,8 @@ function matchesKeyBinding(binding, event) {
|
||||
binding.disabled !== true &&
|
||||
typeof binding.code === "string" &&
|
||||
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);
|
||||
|
||||
function getKeyBindings(action, what = "value") {
|
||||
@@ -4208,7 +4213,7 @@ function setupListener(root) {
|
||||
if (config.skipResetDisarm !== true) {
|
||||
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.targetSpeed = speed;
|
||||
video.vsc.targetSpeedSourceKey = getVideoSourceKey(video);
|
||||
@@ -4240,7 +4245,7 @@ function setupListener(root) {
|
||||
if (!video || typeof video.playbackRate === "undefined" || !video.vsc)
|
||||
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);
|
||||
if (tc.settings.forceLastSavedSpeed) {
|
||||
if (pendingRateChange) {
|
||||
@@ -4354,12 +4359,13 @@ function inIframe() {
|
||||
|
||||
function attachKeydownListeners(doc) {
|
||||
// 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];
|
||||
|
||||
docs.forEach(function(keyDoc) {
|
||||
if (keyDoc.vscKeydownListenerAttached) return;
|
||||
keyDoc.addEventListener(
|
||||
var keyTarget = keyDoc.defaultView || keyDoc;
|
||||
if (keyTarget.vscKeydownListenerAttached) return;
|
||||
keyTarget.addEventListener(
|
||||
"keydown",
|
||||
function(event) {
|
||||
if (
|
||||
@@ -4411,7 +4417,7 @@ function attachKeydownListeners(doc) {
|
||||
},
|
||||
true
|
||||
);
|
||||
keyDoc.vscKeydownListenerAttached = true;
|
||||
keyTarget.vscKeydownListenerAttached = true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4816,6 +4822,16 @@ function resolveActionMediaTargets(event, specificVideo) {
|
||||
|
||||
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 =
|
||||
tc.lastPointerPosition && tc.lastPointerPosition.document === docContext
|
||||
? tc.lastPointerPosition
|
||||
|
||||
@@ -54,11 +54,7 @@ function normalizedSettingsForExport(rawStorage) {
|
||||
// lastSpeed is useful user data, but it is intentionally outside the
|
||||
// managed options diff because content scripts update it at runtime.
|
||||
if (Object.prototype.hasOwnProperty.call(raw, "lastSpeed")) {
|
||||
if (
|
||||
Number.isFinite(expanded.lastSpeed) &&
|
||||
expanded.lastSpeed >= 0.0625 &&
|
||||
expanded.lastSpeed <= 16
|
||||
) {
|
||||
if (Number.isFinite(expanded.lastSpeed)) {
|
||||
normalized.lastSpeed = expanded.lastSpeed;
|
||||
}
|
||||
}
|
||||
@@ -117,11 +113,7 @@ function persistImportedSyncSettings(currentRaw, importedRaw, callback) {
|
||||
importedRaw &&
|
||||
Object.prototype.hasOwnProperty.call(importedRaw, "lastSpeed")
|
||||
) {
|
||||
if (
|
||||
Number.isFinite(expanded.lastSpeed) &&
|
||||
expanded.lastSpeed >= 0.0625 &&
|
||||
expanded.lastSpeed <= 16
|
||||
) {
|
||||
if (Number.isFinite(expanded.lastSpeed)) {
|
||||
mutation.set.lastSpeed = expanded.lastSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<select id="addShortcutSelector">
|
||||
<select id="addShortcutSelector" aria-label="Add another shortcut">
|
||||
<option value="">Add shortcut…</option>
|
||||
</select>
|
||||
</section>
|
||||
@@ -843,7 +843,7 @@
|
||||
</div>
|
||||
<div class="site-rule-option site-rule-option-field">
|
||||
<label
|
||||
>Preferred speed (0.0625–16):<br /><em
|
||||
>Preferred speed (0.01–100):<br /><em
|
||||
>Overrides the Preferred speed action for matching
|
||||
URLs. It does not force every video to start at this
|
||||
speed.</em
|
||||
@@ -1022,10 +1022,10 @@
|
||||
<section class="settings-card action-card">
|
||||
<div class="section-heading">
|
||||
<h3>Actions</h3>
|
||||
<p class="section-intro">Save, restore, export, or import settings.</p>
|
||||
<p class="section-intro">Changes save automatically.</p>
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<button id="save">Save Changes</button>
|
||||
<button id="save">Save Now</button>
|
||||
<button id="restore">Restore Defaults</button>
|
||||
<button id="exportSettings">Export Settings</button>
|
||||
<button id="importSettings">Import Settings</button>
|
||||
|
||||
@@ -164,9 +164,32 @@ function createDefaultBinding(action, code, value) {
|
||||
|
||||
var tcDefaults = vscGetSettingsDefaults();
|
||||
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) {
|
||||
optionsSyncSettingsLoaded = loaded === true;
|
||||
if (!optionsSyncSettingsLoaded) {
|
||||
clearTimeout(autoSaveTimer);
|
||||
autoSaveTimer = null;
|
||||
}
|
||||
var saveButton = document.getElementById("save");
|
||||
if (saveButton) {
|
||||
saveButton.disabled = !optionsSyncSettingsLoaded;
|
||||
@@ -302,33 +325,15 @@ function refreshAddShortcutSelector() {
|
||||
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) => {
|
||||
if (!usedActions.has(action)) {
|
||||
const option = document.createElement("option");
|
||||
option.value = action;
|
||||
option.text = actionLabels[action];
|
||||
selector.appendChild(option);
|
||||
}
|
||||
const option = document.createElement("option");
|
||||
option.value = action;
|
||||
option.text = actionLabels[action];
|
||||
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.options[0].text = "Add shortcut\u2026";
|
||||
}
|
||||
selector.disabled = false;
|
||||
selector.options[0].text = "Add shortcut\u2026";
|
||||
}
|
||||
|
||||
function refreshSiteRuleAddShortcutSelector(ruleEl) {
|
||||
@@ -340,32 +345,19 @@ function refreshSiteRuleAddShortcutSelector(ruleEl) {
|
||||
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) {
|
||||
if (!usedActions.has(action)) {
|
||||
var option = document.createElement("option");
|
||||
option.value = action;
|
||||
option.textContent = actionLabels[action];
|
||||
selector.appendChild(option);
|
||||
}
|
||||
var option = document.createElement("option");
|
||||
option.value = action;
|
||||
option.textContent = actionLabels[action];
|
||||
selector.appendChild(option);
|
||||
});
|
||||
|
||||
var overrideShortcutsOn =
|
||||
ruleEl.querySelector(".override-shortcuts") &&
|
||||
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.options[0].text = "Add shortcut\u2026";
|
||||
}
|
||||
selector.disabled = !overrideShortcutsOn;
|
||||
selector.options[0].text = "Add shortcut\u2026";
|
||||
}
|
||||
|
||||
function getGlobalBindingSnapshotForSiteShortcut(action) {
|
||||
@@ -424,7 +416,10 @@ function readOptionalPreferredSpeedInput(input) {
|
||||
if (!rawValue) return undefined;
|
||||
var parsed = Number(rawValue);
|
||||
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) {
|
||||
@@ -560,6 +555,7 @@ function normalizeStoredBinding(binding, fallbackCode) {
|
||||
|
||||
var normalized = {
|
||||
code: normalizedCode,
|
||||
shiftKey: binding.shiftKey === true,
|
||||
disabled: false
|
||||
};
|
||||
|
||||
@@ -578,7 +574,7 @@ function formatBindingCode(code) {
|
||||
function getBindingLabel(binding) {
|
||||
if (!binding) return "";
|
||||
if (binding.disabled) return "";
|
||||
return formatBindingCode(binding.code);
|
||||
return (binding.shiftKey ? "Shift+" : "") + formatBindingCode(binding.code);
|
||||
}
|
||||
|
||||
function setShortcutInputBinding(input, binding) {
|
||||
@@ -591,6 +587,7 @@ function captureBindingFromEvent(event) {
|
||||
if (typeof event.code !== "string" || event.code.length === 0) return null;
|
||||
return {
|
||||
code: event.code,
|
||||
shiftKey: event.shiftKey === true,
|
||||
disabled: false
|
||||
};
|
||||
}
|
||||
@@ -600,6 +597,7 @@ function recordKeyPress(event) {
|
||||
|
||||
if (event.key === "Backspace") {
|
||||
setShortcutInputBinding(event.target, null);
|
||||
scheduleAutoSave();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
@@ -607,6 +605,7 @@ function recordKeyPress(event) {
|
||||
|
||||
if (event.key === "Escape") {
|
||||
setShortcutInputBinding(event.target, createDisabledBinding());
|
||||
scheduleAutoSave();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
@@ -616,6 +615,7 @@ function recordKeyPress(event) {
|
||||
if (!binding) return;
|
||||
|
||||
setShortcutInputBinding(event.target, binding);
|
||||
scheduleAutoSave();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
@@ -770,6 +770,7 @@ function createKeyBindings(item) {
|
||||
keyBindings.push({
|
||||
action: action,
|
||||
code: binding.code,
|
||||
shiftKey: binding.shiftKey === true,
|
||||
disabled: binding.disabled === true,
|
||||
value: bindingValue,
|
||||
force: false,
|
||||
@@ -807,6 +808,8 @@ function validate() {
|
||||
}
|
||||
|
||||
function save_options() {
|
||||
clearTimeout(autoSaveTimer);
|
||||
autoSaveTimer = null;
|
||||
var status = document.getElementById("status");
|
||||
if (!optionsSyncSettingsLoaded) {
|
||||
status.textContent =
|
||||
@@ -1092,6 +1095,7 @@ function save_options() {
|
||||
shortcuts.push({
|
||||
action: action,
|
||||
code: binding.code,
|
||||
shiftKey: binding.shiftKey === true,
|
||||
disabled: binding.disabled === true,
|
||||
value: shortcutValue,
|
||||
force: forceCheckbox ? forceCheckbox.checked : false
|
||||
@@ -1551,6 +1555,7 @@ function initControlBarEditor() {
|
||||
var zone = e.target.closest(".cb-dropzone");
|
||||
if (zone) {
|
||||
e.preventDefault();
|
||||
scheduleAutoSave();
|
||||
}
|
||||
|
||||
clearControlBarDropTargets(null);
|
||||
@@ -1825,13 +1830,16 @@ function restore_options(callback) {
|
||||
|
||||
document.querySelectorAll(".customs:not([id])").forEach((row) => row.remove());
|
||||
|
||||
var usedDefaultShortcutRows = new Set();
|
||||
storage.keyBindings.forEach((item) => {
|
||||
var row = document.getElementById(item.action);
|
||||
var normalizedBinding = normalizeStoredBinding(item);
|
||||
|
||||
if (!row) {
|
||||
if (!row || usedDefaultShortcutRows.has(item.action)) {
|
||||
add_shortcut(item.action, item.value);
|
||||
row = document.querySelector(".shortcut-row.customs:last-of-type");
|
||||
} else {
|
||||
usedDefaultShortcutRows.add(item.action);
|
||||
}
|
||||
|
||||
if (!row) return;
|
||||
@@ -2030,11 +2038,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
if (siteRuleForShortcut) {
|
||||
refreshSiteRuleAddShortcutSelector(siteRuleForShortcut);
|
||||
}
|
||||
scheduleAutoSave();
|
||||
return;
|
||||
}
|
||||
var removeSiteRuleButton = targetEl.closest(".remove-site-rule");
|
||||
if (removeSiteRuleButton) {
|
||||
removeSiteRuleButton.closest(".site-rule").remove();
|
||||
scheduleAutoSave();
|
||||
return;
|
||||
}
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
root.SpeederShared = root.SpeederShared || {};
|
||||
root.SpeederShared.keyBindings = exports;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function() {
|
||||
var MIN_SPEED = 0.01;
|
||||
var MAX_SPEED = 100;
|
||||
|
||||
function normalizeBindingKey(key) {
|
||||
if (typeof key !== "string" || key.length === 0) return null;
|
||||
if (key === "Spacebar") return " ";
|
||||
@@ -127,15 +130,15 @@
|
||||
if (!Number.isFinite(numericValue)) return "must be a finite number";
|
||||
if (
|
||||
(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 (
|
||||
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 (
|
||||
(action === "rewind" || action === "advance") &&
|
||||
@@ -162,6 +165,8 @@
|
||||
}
|
||||
|
||||
return {
|
||||
MAX_SPEED: MAX_SPEED,
|
||||
MIN_SPEED: MIN_SPEED,
|
||||
getActionValueError: getActionValueError,
|
||||
getLegacyKeyCode: getLegacyKeyCode,
|
||||
inferBindingCode: inferBindingCode,
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
var LEGACY_SITE_RULES_DIFF_FORMAT = "defaults-diff-v1";
|
||||
var SITE_RULES_DIFF_FORMAT = "defaults-diff-v2";
|
||||
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 LEGACY_SYNC_KEYS = [
|
||||
"resetSpeed",
|
||||
@@ -363,7 +365,7 @@
|
||||
return clampFiniteNumber(value, 250, 1000, undefined);
|
||||
}
|
||||
if (key === "preferredSpeed") {
|
||||
return clampFiniteNumber(value, 0.0625, 16, undefined);
|
||||
return clampFiniteNumber(value, MIN_SPEED, MAX_SPEED, undefined);
|
||||
}
|
||||
if (key === "shortcuts" && Array.isArray(value)) {
|
||||
return sanitizeStoredBindingValues(value).map(function(shortcut) {
|
||||
@@ -749,9 +751,9 @@
|
||||
var value = Number(normalized.value);
|
||||
var invalid = !Number.isFinite(value);
|
||||
if (action === "slower" || action === "faster") {
|
||||
invalid = invalid || value <= 0 || value > 16;
|
||||
invalid = invalid || value <= 0 || value > MAX_SPEED;
|
||||
} 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") {
|
||||
invalid = invalid || value < 0;
|
||||
} else if (action === "louder" || action === "softer") {
|
||||
@@ -885,8 +887,8 @@
|
||||
|
||||
expanded.lastSpeed = clampFiniteNumber(
|
||||
expanded.lastSpeed,
|
||||
0.0625,
|
||||
16,
|
||||
MIN_SPEED,
|
||||
MAX_SPEED,
|
||||
DEFAULT_SETTINGS.lastSpeed
|
||||
);
|
||||
expanded.hideWithControlsTimer = clampFiniteNumber(
|
||||
|
||||
@@ -320,6 +320,7 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
||||
src: "blob:https://www.youtube.com/hover-preview",
|
||||
mountRect: makeRect(0, 0, 320, 180)
|
||||
});
|
||||
preview.mount.id = "inline-preview-player";
|
||||
document.dispatchEvent(
|
||||
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",
|
||||
mountRect: makeRect(0, 0, 1280, 720)
|
||||
});
|
||||
main.mount.id = "movie_player";
|
||||
document.dispatchEvent(
|
||||
new MouseEvent("mousemove", { bubbles: true, clientX: 100, clientY: 90 })
|
||||
);
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
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(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 () => {
|
||||
bootInject();
|
||||
await settleLifecycle();
|
||||
|
||||
@@ -106,6 +106,48 @@ describe("options page", () => {
|
||||
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 () => {
|
||||
await setupOptions({ sync: { siteRules: [] } });
|
||||
|
||||
|
||||
@@ -67,6 +67,22 @@ describe("options.js", () => {
|
||||
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 () => {
|
||||
const chrome = bootOptions({ syncData: { rememberSpeed: false } });
|
||||
await flushAsyncWork(3);
|
||||
|
||||
@@ -244,7 +244,7 @@ describe("canonical settings storage", () => {
|
||||
forceLastSavedSpeed: "true",
|
||||
controllerOpacity: 3,
|
||||
subtitleNudgeInterval: -5,
|
||||
preferredSpeed: 50
|
||||
preferredSpeed: 500
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -257,7 +257,7 @@ describe("canonical settings storage", () => {
|
||||
expect(rule.forceLastSavedSpeed).toBe(true);
|
||||
expect(rule.controllerOpacity).toBe(1);
|
||||
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", () => {
|
||||
|
||||
@@ -158,6 +158,11 @@ describe("shared helpers", () => {
|
||||
"NumpadAdd"
|
||||
);
|
||||
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", () => {
|
||||
|
||||
Reference in New Issue
Block a user