mirror of
https://github.com/SoPat712/Speeder.git
synced 2026-10-08 03:03:14 -04:00
feat: expand shortcuts and improve YouTube navigation
This commit is contained in:
11 files changed
+212
-69
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -250,7 +250,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<select id="addShortcutSelector">
|
<select id="addShortcutSelector" aria-label="Add another shortcut">
|
||||||
<option value="">Add shortcut…</option>
|
<option value="">Add shortcut…</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–16):<br /><em
|
>Preferred speed (0.01–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>
|
||||||
|
|||||||
@@ -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,33 +325,15 @@ 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) {
|
||||||
@@ -340,32 +345,19 @@ 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) {
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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: [] } });
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
Reference in new issue
Block a user