mirror of
https://github.com/SoPat712/videospeed.git
synced 2026-04-21 04:42:35 -04:00
1837 lines
56 KiB
JavaScript
1837 lines
56 KiB
JavaScript
var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
|
|
var speederShared =
|
|
typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
|
|
var controllerUtils = speederShared.controllerUtils || {};
|
|
var keyBindingUtils = speederShared.keyBindings || {};
|
|
var popupControlUtils = speederShared.popupControls || {};
|
|
|
|
var keyBindings = [];
|
|
|
|
var bindingCodeAliases = {
|
|
Space: "Space",
|
|
ArrowLeft: "Left",
|
|
ArrowUp: "Up",
|
|
ArrowRight: "Right",
|
|
ArrowDown: "Down",
|
|
Numpad0: "Num 0",
|
|
Numpad1: "Num 1",
|
|
Numpad2: "Num 2",
|
|
Numpad3: "Num 3",
|
|
Numpad4: "Num 4",
|
|
Numpad5: "Num 5",
|
|
Numpad6: "Num 6",
|
|
Numpad7: "Num 7",
|
|
Numpad8: "Num 8",
|
|
Numpad9: "Num 9",
|
|
NumpadMultiply: "Num *",
|
|
NumpadAdd: "Num +",
|
|
NumpadSubtract: "Num -",
|
|
NumpadDecimal: "Num .",
|
|
NumpadDivide: "Num /",
|
|
Backquote: "`",
|
|
Minus: "-",
|
|
Equal: "=",
|
|
BracketLeft: "[",
|
|
BracketRight: "]",
|
|
Backslash: "\\",
|
|
Semicolon: ";",
|
|
Quote: "'",
|
|
Comma: ",",
|
|
Period: ".",
|
|
Slash: "/"
|
|
};
|
|
|
|
var modifierKeys = new Set([
|
|
"Alt",
|
|
"AltGraph",
|
|
"Control",
|
|
"Fn",
|
|
"Hyper",
|
|
"Meta",
|
|
"OS",
|
|
"Shift"
|
|
]);
|
|
|
|
var controllerLocations = Array.isArray(controllerUtils.controllerLocations)
|
|
? controllerUtils.controllerLocations.slice()
|
|
: [
|
|
"top-left",
|
|
"top-center",
|
|
"top-right",
|
|
"middle-right",
|
|
"bottom-right",
|
|
"bottom-center",
|
|
"bottom-left",
|
|
"middle-left"
|
|
];
|
|
|
|
var controllerButtonDefs = {
|
|
rewind: { icon: "\u00AB", name: "Rewind" },
|
|
slower: { icon: "\u2212", name: "Decrease speed" },
|
|
faster: { icon: "+", name: "Increase speed" },
|
|
advance: { icon: "\u00BB", name: "Advance" },
|
|
display: { icon: "\u00D7", name: "Close controller" },
|
|
reset: { icon: "\u21BB", name: "Reset speed" },
|
|
fast: { icon: "\u2605", name: "Preferred speed" },
|
|
nudge: { icon: "\u2713", name: "Subtitle nudge" },
|
|
pause: { icon: "\u23EF", name: "Play / Pause" },
|
|
muted: { icon: "M", name: "Mute / Unmute" },
|
|
louder: { icon: "+", name: "Increase volume" },
|
|
softer: { icon: "\u2212", name: "Decrease volume" },
|
|
mark: { icon: "\u2691", name: "Set marker" },
|
|
jump: { icon: "\u21E5", name: "Jump to marker" },
|
|
settings: { icon: "\u2699", name: "Settings" },
|
|
};
|
|
var popupExcludedButtonIds = new Set(["settings"]);
|
|
|
|
/** Lucide picker only — not control-bar blocks (chip uses subtitleNudgeOn/Off). */
|
|
var lucideSubtitleNudgeActionLabels = {
|
|
subtitleNudgeOn: "Subtitle nudge — enabled",
|
|
subtitleNudgeOff: "Subtitle nudge — disabled"
|
|
};
|
|
|
|
function sanitizePopupButtonOrder(buttonIds) {
|
|
return popupControlUtils.sanitizeButtonOrder(
|
|
buttonIds,
|
|
controllerButtonDefs,
|
|
popupExcludedButtonIds
|
|
);
|
|
}
|
|
|
|
/** Cached custom Lucide SVGs (mirrors chrome.storage.local customButtonIcons). */
|
|
var customButtonIconsLive = {};
|
|
|
|
function fillControlBarIconElement(icon, buttonId) {
|
|
if (!icon || !buttonId) return;
|
|
var doc = icon.ownerDocument || document;
|
|
if (buttonId === "nudge") {
|
|
vscClearElement(icon);
|
|
icon.className = "cb-icon cb-icon-nudge-pair";
|
|
function nudgeChipMarkup(action) {
|
|
var c = customButtonIconsLive[action];
|
|
if (c && c.svg) return c.svg;
|
|
if (typeof vscIconSvgString === "function") {
|
|
return vscIconSvgString(action, 14) || "";
|
|
}
|
|
return "";
|
|
}
|
|
function appendChip(action, stateKey) {
|
|
var sp = document.createElement("span");
|
|
sp.className = "cb-nudge-chip";
|
|
sp.setAttribute("data-nudge-state", stateKey);
|
|
var inner = nudgeChipMarkup(action);
|
|
if (inner) {
|
|
var wrap = vscCreateSvgWrap(doc, inner, "vsc-btn-icon");
|
|
if (wrap) {
|
|
sp.appendChild(wrap);
|
|
}
|
|
}
|
|
icon.appendChild(sp);
|
|
}
|
|
appendChip("subtitleNudgeOn", "on");
|
|
var sep = document.createElement("span");
|
|
sep.className = "cb-nudge-sep";
|
|
sep.textContent = "/";
|
|
icon.appendChild(sep);
|
|
appendChip("subtitleNudgeOff", "off");
|
|
return;
|
|
}
|
|
icon.className = "cb-icon";
|
|
var custom = customButtonIconsLive[buttonId];
|
|
if (custom && custom.svg) {
|
|
if (vscSetSvgContent(icon, custom.svg)) return;
|
|
}
|
|
if (typeof vscIconSvgString === "function") {
|
|
var svgHtml = vscIconSvgString(buttonId, 16);
|
|
if (svgHtml) {
|
|
if (vscSetSvgContent(icon, svgHtml)) return;
|
|
}
|
|
}
|
|
vscClearElement(icon);
|
|
var def = controllerButtonDefs[buttonId];
|
|
icon.textContent = (def && def.icon) || "?";
|
|
}
|
|
|
|
function createDefaultBinding(action, code, value) {
|
|
return {
|
|
action: action,
|
|
code: code,
|
|
value: value,
|
|
force: false,
|
|
predefined: true
|
|
};
|
|
}
|
|
|
|
var tcDefaults = {
|
|
speed: 1.0,
|
|
lastSpeed: 1.0,
|
|
rememberSpeed: false,
|
|
audioBoolean: false,
|
|
startHidden: false,
|
|
hideWithYouTubeControls: false,
|
|
hideWithControls: false,
|
|
hideWithControlsTimer: 2.0,
|
|
controllerLocation: "top-left",
|
|
forceLastSavedSpeed: false,
|
|
enabled: true,
|
|
controllerOpacity: 0.3,
|
|
controllerMarginTop: 0,
|
|
controllerMarginRight: 0,
|
|
controllerMarginBottom: 65,
|
|
controllerMarginLeft: 0,
|
|
keyBindings: [
|
|
createDefaultBinding("display", "KeyV", 0),
|
|
createDefaultBinding("move", "KeyP", 0),
|
|
createDefaultBinding("slower", "KeyS", 0.1),
|
|
createDefaultBinding("faster", "KeyD", 0.1),
|
|
createDefaultBinding("rewind", "KeyZ", 10),
|
|
createDefaultBinding("advance", "KeyX", 10),
|
|
createDefaultBinding("reset", "KeyR", 1),
|
|
createDefaultBinding("fast", "KeyG", 1.8),
|
|
createDefaultBinding("toggleSubtitleNudge", "KeyN", 0)
|
|
],
|
|
siteRules: [
|
|
{
|
|
pattern: "/^https:\\/\\/(www\\.)?youtube\\.com\\/(?!shorts\\/).*/",
|
|
enabled: true,
|
|
enableSubtitleNudge: true,
|
|
subtitleNudgeInterval: 50
|
|
},
|
|
{
|
|
pattern: "/^https:\\/\\/(www\\.)?youtube\\.com\\/shorts\\/.*/",
|
|
enabled: true,
|
|
rememberSpeed: true,
|
|
controllerMarginTop: 60,
|
|
controllerMarginBottom: 85
|
|
}
|
|
],
|
|
controllerButtons: ["rewind", "slower", "faster", "advance", "display"],
|
|
showPopupControlBar: true,
|
|
popupMatchHoverControls: true,
|
|
popupControllerButtons: ["rewind", "slower", "faster", "advance", "display"],
|
|
enableSubtitleNudge: false,
|
|
subtitleNudgeInterval: 50,
|
|
subtitleNudgeAmount: 0.001
|
|
};
|
|
|
|
const actionLabels = {
|
|
display: "Show/hide controller",
|
|
move: "Move controller",
|
|
slower: "Decrease speed",
|
|
faster: "Increase speed",
|
|
rewind: "Rewind",
|
|
advance: "Advance",
|
|
reset: "Reset speed",
|
|
fast: "Preferred speed",
|
|
toggleSubtitleNudge: "Toggle subtitle nudge",
|
|
pause: "Play / Pause",
|
|
muted: "Mute / Unmute",
|
|
louder: "Increase volume",
|
|
softer: "Decrease volume",
|
|
mark: "Set marker",
|
|
jump: "Jump to marker"
|
|
};
|
|
|
|
const speedBindingActions = ["slower", "faster", "fast", "softer", "louder"];
|
|
const requiredShortcutActions = new Set(["display", "slower", "faster"]);
|
|
|
|
function formatSpeedBindingDisplay(action, value) {
|
|
if (!speedBindingActions.includes(action)) {
|
|
return value;
|
|
}
|
|
var n = Number(value);
|
|
if (!isFinite(n)) {
|
|
return value;
|
|
}
|
|
return n.toFixed(2);
|
|
}
|
|
|
|
function getDefaultShortcutValue(action) {
|
|
if (action === "louder" || action === "softer") {
|
|
return 0.1;
|
|
}
|
|
var defaultBinding = tcDefaults.keyBindings.find(function (binding) {
|
|
return binding.action === action;
|
|
});
|
|
if (defaultBinding && Number.isFinite(Number(defaultBinding.value))) {
|
|
return Number(defaultBinding.value);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function resolveShortcutValue(action, value) {
|
|
if (value === undefined || value === null) {
|
|
return getDefaultShortcutValue(action);
|
|
}
|
|
var numericValue = Number(value);
|
|
if (Number.isFinite(numericValue)) {
|
|
return numericValue;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
const customActionsNoValues = [
|
|
"reset",
|
|
"display",
|
|
"move",
|
|
"muted",
|
|
"pause",
|
|
"mark",
|
|
"jump",
|
|
"toggleSubtitleNudge"
|
|
];
|
|
|
|
function refreshAddShortcutSelector() {
|
|
const selector = document.getElementById("addShortcutSelector");
|
|
if (!selector) return;
|
|
|
|
// Clear existing options except the first one
|
|
while (selector.options.length > 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) => {
|
|
if (!usedActions.has(action)) {
|
|
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";
|
|
}
|
|
}
|
|
|
|
function ensureDefaultBinding(storage, action, code, value) {
|
|
if (storage.keyBindings.some((item) => item.action === action)) return;
|
|
|
|
storage.keyBindings.push(createDefaultBinding(action, code, value));
|
|
}
|
|
|
|
function normalizeControllerLocation(location) {
|
|
return controllerUtils.normalizeControllerLocation(
|
|
location,
|
|
tcDefaults.controllerLocation
|
|
);
|
|
}
|
|
|
|
function clampMarginPxInput(el, fallback) {
|
|
return controllerUtils.clampControllerMarginPx(el && el.value, fallback);
|
|
}
|
|
|
|
function parseFiniteNumberOrFallback(value, fallback) {
|
|
var numericValue = parseFloat(value);
|
|
return Number.isFinite(numericValue) ? numericValue : fallback;
|
|
}
|
|
|
|
function updateSiteRuleToggleIcon(toggleButton, action) {
|
|
if (!toggleButton) return;
|
|
var iconEl = toggleButton.querySelector(".site-rule-toggle-icon");
|
|
if (!iconEl) return;
|
|
|
|
if (typeof vscIconSvgString === "function" && typeof vscSetSvgContent === "function") {
|
|
var svgHtml = vscIconSvgString(action, 16);
|
|
if (svgHtml && vscSetSvgContent(iconEl, svgHtml)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
iconEl.textContent = action === "chevronUp" ? "\u2212" : "\u2026";
|
|
}
|
|
|
|
function setSiteRuleExpandedState(ruleEl, expanded) {
|
|
if (!ruleEl) return;
|
|
|
|
var ruleBody = ruleEl.querySelector(".site-rule-body");
|
|
var toggleButton = ruleEl.querySelector(".toggle-site-rule");
|
|
if (ruleBody) {
|
|
ruleBody.style.display = expanded ? "block" : "none";
|
|
}
|
|
|
|
ruleEl.classList.toggle("collapsed", !expanded);
|
|
|
|
if (!toggleButton) return;
|
|
var label = expanded ? "Collapse site rule" : "Expand site rule";
|
|
toggleButton.title = label;
|
|
toggleButton.setAttribute("aria-label", label);
|
|
toggleButton.setAttribute("aria-expanded", expanded ? "true" : "false");
|
|
updateSiteRuleToggleIcon(toggleButton, expanded ? "chevronUp" : "moreHorizontal");
|
|
}
|
|
|
|
function setSiteOverrideContainerState(container, enabled) {
|
|
if (!container) return;
|
|
|
|
container.classList.toggle("site-override-disabled", !enabled);
|
|
container.setAttribute("aria-disabled", enabled ? "false" : "true");
|
|
|
|
Array.prototype.forEach.call(
|
|
container.querySelectorAll("input, select, textarea, button"),
|
|
function (control) {
|
|
control.disabled = !enabled;
|
|
}
|
|
);
|
|
|
|
Array.prototype.forEach.call(
|
|
container.querySelectorAll(".cb-block"),
|
|
function (block) {
|
|
block.draggable = enabled;
|
|
}
|
|
);
|
|
}
|
|
|
|
function applySiteRuleOverrideState(ruleEl, checkboxClass, containerClass) {
|
|
if (!ruleEl) return;
|
|
var checkbox = ruleEl.querySelector("." + checkboxClass);
|
|
var container = ruleEl.querySelector("." + containerClass);
|
|
if (!container) return;
|
|
|
|
container.style.display = "block";
|
|
setSiteOverrideContainerState(container, checkbox ? checkbox.checked : false);
|
|
}
|
|
|
|
function syncSiteRuleField(ruleEl, rule, key, isCheckbox) {
|
|
var input = ruleEl.querySelector(".site-" + key);
|
|
if (!input) return;
|
|
var globalEl = document.getElementById(key);
|
|
var value;
|
|
if (rule && rule[key] !== undefined) {
|
|
value = rule[key];
|
|
} else if (globalEl) {
|
|
value = isCheckbox ? globalEl.checked : globalEl.value;
|
|
} else {
|
|
return;
|
|
}
|
|
if (isCheckbox) input.checked = Boolean(value);
|
|
else input.value = value;
|
|
}
|
|
|
|
function normalizeBindingKey(key) {
|
|
return keyBindingUtils.normalizeBindingKey(key);
|
|
}
|
|
|
|
function getLegacyKeyCode(binding) {
|
|
return keyBindingUtils.getLegacyKeyCode(binding);
|
|
}
|
|
|
|
function legacyBindingKeyToCode(key) {
|
|
return keyBindingUtils.legacyBindingKeyToCode(key);
|
|
}
|
|
|
|
function legacyKeyCodeToCode(keyCode) {
|
|
return keyBindingUtils.legacyKeyCodeToCode(keyCode);
|
|
}
|
|
|
|
function inferBindingCode(binding, fallbackCode) {
|
|
return keyBindingUtils.inferBindingCode(binding, fallbackCode);
|
|
}
|
|
|
|
function createDisabledBinding() {
|
|
return {
|
|
code: null,
|
|
disabled: true
|
|
};
|
|
}
|
|
|
|
function normalizeStoredBinding(binding, fallbackCode) {
|
|
if (!binding) {
|
|
if (!fallbackCode) return null;
|
|
return {
|
|
code: fallbackCode,
|
|
disabled: false
|
|
};
|
|
}
|
|
|
|
if (
|
|
binding.disabled === true ||
|
|
(binding.code === null &&
|
|
binding.key === null &&
|
|
binding.keyCode === null)
|
|
) {
|
|
return createDisabledBinding();
|
|
}
|
|
|
|
var normalizedCode = inferBindingCode(binding, fallbackCode);
|
|
if (!normalizedCode) {
|
|
return null;
|
|
}
|
|
|
|
var normalized = {
|
|
code: normalizedCode,
|
|
disabled: false
|
|
};
|
|
|
|
return normalized;
|
|
}
|
|
|
|
function formatBindingCode(code) {
|
|
if (typeof code !== "string" || code.length === 0) return "";
|
|
if (bindingCodeAliases[code]) return bindingCodeAliases[code];
|
|
if (/^Key[A-Z]$/.test(code)) return code.substring(3);
|
|
if (/^Digit[0-9]$/.test(code)) return code.substring(5);
|
|
if (/^F([1-9]|1[0-2])$/.test(code)) return code;
|
|
return code;
|
|
}
|
|
|
|
function getBindingLabel(binding) {
|
|
if (!binding) return "";
|
|
if (binding.disabled) return "";
|
|
return formatBindingCode(binding.code);
|
|
}
|
|
|
|
function setShortcutInputBinding(input, binding) {
|
|
input.vscBinding = binding ? Object.assign({}, binding) : null;
|
|
input.value = getBindingLabel(binding);
|
|
}
|
|
|
|
function captureBindingFromEvent(event) {
|
|
if (modifierKeys.has(event.key)) return null;
|
|
if (typeof event.code !== "string" || event.code.length === 0) return null;
|
|
return {
|
|
code: event.code,
|
|
disabled: false
|
|
};
|
|
}
|
|
|
|
function recordKeyPress(event) {
|
|
if (event.key === "Tab") return;
|
|
|
|
if (event.key === "Backspace") {
|
|
setShortcutInputBinding(event.target, null);
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
return;
|
|
}
|
|
|
|
if (event.key === "Escape") {
|
|
setShortcutInputBinding(event.target, createDisabledBinding());
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
return;
|
|
}
|
|
|
|
var binding = captureBindingFromEvent(event);
|
|
if (!binding) return;
|
|
|
|
setShortcutInputBinding(event.target, binding);
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
|
|
function inputFilterNumbersOnly(event) {
|
|
var char = event.key;
|
|
if (
|
|
typeof char !== "string" ||
|
|
char.length !== 1 ||
|
|
!/[\d\.]$/.test(char) ||
|
|
!/^\d+(\.\d*)?$/.test(event.target.value + char)
|
|
) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
}
|
|
|
|
function inputFocus(event) {
|
|
event.target.value = "";
|
|
}
|
|
|
|
function inputBlur(event) {
|
|
setShortcutInputBinding(event.target, event.target.vscBinding || null);
|
|
}
|
|
|
|
function updateCustomShortcutInputText(inputItem, bindingOrKeyCode) {
|
|
if (
|
|
bindingOrKeyCode &&
|
|
typeof bindingOrKeyCode === "object" &&
|
|
!Array.isArray(bindingOrKeyCode)
|
|
) {
|
|
setShortcutInputBinding(inputItem, bindingOrKeyCode);
|
|
return;
|
|
}
|
|
|
|
if (typeof bindingOrKeyCode === "string") {
|
|
setShortcutInputBinding(inputItem, { code: bindingOrKeyCode, disabled: false });
|
|
return;
|
|
}
|
|
|
|
setShortcutInputBinding(
|
|
inputItem,
|
|
normalizeStoredBinding({ keyCode: bindingOrKeyCode })
|
|
);
|
|
}
|
|
|
|
function appendSelectOptions(select, options) {
|
|
options.forEach(function (optionData) {
|
|
var option = document.createElement("option");
|
|
option.value = optionData.value;
|
|
option.textContent = optionData.label;
|
|
select.appendChild(option);
|
|
});
|
|
}
|
|
|
|
function add_shortcut(action, value) {
|
|
if (!action) return;
|
|
|
|
var div = document.createElement("div");
|
|
div.setAttribute("class", "shortcut-row customs");
|
|
div.dataset.action = action;
|
|
|
|
var actionLabel = document.createElement("div");
|
|
actionLabel.className = "shortcut-label";
|
|
actionLabel.textContent = actionLabels[action] || action;
|
|
|
|
var keyInput = document.createElement("input");
|
|
keyInput.className = "customKey";
|
|
keyInput.type = "text";
|
|
keyInput.placeholder = "press a key";
|
|
|
|
var valueInput = document.createElement("input");
|
|
valueInput.className = "customValue";
|
|
valueInput.type = "text";
|
|
valueInput.placeholder = "value";
|
|
if (customActionsNoValues.includes(action)) {
|
|
valueInput.value = "N/A";
|
|
valueInput.disabled = true;
|
|
} else {
|
|
valueInput.value = formatSpeedBindingDisplay(
|
|
action,
|
|
resolveShortcutValue(action, value)
|
|
);
|
|
}
|
|
|
|
var removeButton = document.createElement("button");
|
|
removeButton.className = "removeParent";
|
|
removeButton.type = "button";
|
|
removeButton.textContent = "\u00d7";
|
|
|
|
div.appendChild(actionLabel);
|
|
div.appendChild(keyInput);
|
|
div.appendChild(valueInput);
|
|
div.appendChild(removeButton);
|
|
|
|
var customsElement = document.querySelector(".shortcuts-grid");
|
|
customsElement.appendChild(div);
|
|
|
|
refreshAddShortcutSelector();
|
|
}
|
|
|
|
function createKeyBindings(item) {
|
|
var action = item.dataset.action || item.querySelector(".customDo").value;
|
|
var input = item.querySelector(".customKey");
|
|
var valueInput = item.querySelector(".customValue");
|
|
var predefined = !!item.id;
|
|
var binding = normalizeStoredBinding(input.vscBinding);
|
|
|
|
if (!binding) {
|
|
if (requiredShortcutActions.has(action)) {
|
|
return {
|
|
valid: false,
|
|
message:
|
|
"Error: Shortcut for " +
|
|
(actionLabels[action] || action) +
|
|
" cannot be empty. Unable to save"
|
|
};
|
|
}
|
|
binding = createDisabledBinding();
|
|
}
|
|
|
|
if (binding.disabled === true && requiredShortcutActions.has(action)) {
|
|
return {
|
|
valid: false,
|
|
message:
|
|
"Error: Shortcut for " +
|
|
(actionLabels[action] || action) +
|
|
" cannot be empty. Unable to save"
|
|
};
|
|
}
|
|
|
|
keyBindings.push({
|
|
action: action,
|
|
code: binding.code,
|
|
disabled: binding.disabled === true,
|
|
value: customActionsNoValues.includes(action)
|
|
? 0
|
|
: Number(valueInput.value),
|
|
force: false,
|
|
predefined: predefined
|
|
});
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
function validate() {
|
|
var valid = true;
|
|
var status = document.getElementById("status");
|
|
|
|
// Validate site rules patterns
|
|
document.querySelectorAll(".site-rule").forEach((ruleEl) => {
|
|
var pattern = ruleEl.querySelector(".site-pattern").value.trim();
|
|
if (pattern.length === 0) return;
|
|
|
|
if (pattern.startsWith("/")) {
|
|
try {
|
|
var lastSlash = pattern.lastIndexOf("/");
|
|
if (lastSlash > 0) {
|
|
new RegExp(pattern.substring(1, lastSlash), pattern.substring(lastSlash + 1));
|
|
}
|
|
} catch (err) {
|
|
status.textContent =
|
|
"Error: Invalid site rule regex: " + pattern + ". Unable to save";
|
|
valid = false;
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
|
|
return valid;
|
|
}
|
|
|
|
function save_options() {
|
|
if (validate() === false) return;
|
|
|
|
keyBindings = [];
|
|
var status = document.getElementById("status");
|
|
var saveError = null;
|
|
|
|
// Collect shortcuts from the main shortcuts section (both default and custom)
|
|
Array.from(document.querySelectorAll("#customs .shortcut-row")).forEach((item) => {
|
|
if (saveError) return;
|
|
var result = createKeyBindings(item);
|
|
if (!result.valid) saveError = result.message;
|
|
});
|
|
|
|
if (saveError) {
|
|
status.textContent = saveError;
|
|
return;
|
|
}
|
|
|
|
var settings = {};
|
|
settings.rememberSpeed = document.getElementById("rememberSpeed").checked;
|
|
settings.forceLastSavedSpeed =
|
|
document.getElementById("forceLastSavedSpeed").checked;
|
|
settings.audioBoolean = document.getElementById("audioBoolean").checked;
|
|
settings.enabled = document.getElementById("enabled").checked;
|
|
settings.startHidden = document.getElementById("startHidden").checked;
|
|
settings.hideWithControls = document.getElementById("hideWithControls").checked;
|
|
settings.hideWithControlsTimer =
|
|
Math.min(15, Math.max(0.1, parseFloat(document.getElementById("hideWithControlsTimer").value) || tcDefaults.hideWithControlsTimer));
|
|
|
|
// Sync back to the legacy key if it exists, for backward compatibility
|
|
settings.hideWithYouTubeControls = settings.hideWithControls;
|
|
|
|
if (settings.hideWithControlsTimer < 0.1) settings.hideWithControlsTimer = 0.1;
|
|
if (settings.hideWithControlsTimer > 15) settings.hideWithControlsTimer = 15;
|
|
|
|
settings.controllerLocation = normalizeControllerLocation(
|
|
document.getElementById("controllerLocation").value
|
|
);
|
|
settings.controllerOpacity =
|
|
parseFiniteNumberOrFallback(
|
|
document.getElementById("controllerOpacity").value,
|
|
tcDefaults.controllerOpacity
|
|
);
|
|
|
|
settings.controllerMarginTop = clampMarginPxInput(
|
|
document.getElementById("controllerMarginTop"),
|
|
tcDefaults.controllerMarginTop
|
|
);
|
|
settings.controllerMarginBottom = clampMarginPxInput(
|
|
document.getElementById("controllerMarginBottom"),
|
|
tcDefaults.controllerMarginBottom
|
|
);
|
|
|
|
settings.keyBindings = keyBindings;
|
|
settings.enableSubtitleNudge =
|
|
document.getElementById("enableSubtitleNudge").checked;
|
|
settings.subtitleNudgeInterval =
|
|
parseInt(document.getElementById("subtitleNudgeInterval").value, 10) ||
|
|
tcDefaults.subtitleNudgeInterval;
|
|
settings.subtitleNudgeAmount = tcDefaults.subtitleNudgeAmount;
|
|
|
|
if (settings.subtitleNudgeInterval < 10) {
|
|
settings.subtitleNudgeInterval = 10;
|
|
}
|
|
if (settings.subtitleNudgeInterval > 1000) {
|
|
settings.subtitleNudgeInterval = 1000;
|
|
}
|
|
|
|
settings.controllerButtons = getControlBarOrder();
|
|
settings.showPopupControlBar =
|
|
document.getElementById("showPopupControlBar").checked;
|
|
settings.popupMatchHoverControls =
|
|
document.getElementById("popupMatchHoverControls").checked;
|
|
settings.popupControllerButtons = sanitizePopupButtonOrder(getPopupControlBarOrder());
|
|
|
|
// Collect site rules
|
|
settings.siteRules = [];
|
|
document.querySelectorAll(".site-rule").forEach((ruleEl) => {
|
|
var pattern = ruleEl.querySelector(".site-pattern").value.trim();
|
|
if (pattern.length === 0) return;
|
|
|
|
var rule = { pattern: pattern };
|
|
|
|
// Handle Enable toggle
|
|
rule.enabled = ruleEl.querySelector(".site-enabled").checked;
|
|
|
|
if (ruleEl.querySelector(".override-placement").checked) {
|
|
rule.controllerLocation = normalizeControllerLocation(
|
|
ruleEl.querySelector(".site-controllerLocation").value
|
|
);
|
|
rule.controllerMarginTop = clampMarginPxInput(
|
|
ruleEl.querySelector(".site-controllerMarginTop"),
|
|
clampMarginPxInput(
|
|
document.getElementById("controllerMarginTop"),
|
|
tcDefaults.controllerMarginTop
|
|
)
|
|
);
|
|
rule.controllerMarginBottom = clampMarginPxInput(
|
|
ruleEl.querySelector(".site-controllerMarginBottom"),
|
|
clampMarginPxInput(
|
|
document.getElementById("controllerMarginBottom"),
|
|
tcDefaults.controllerMarginBottom
|
|
)
|
|
);
|
|
}
|
|
|
|
if (ruleEl.querySelector(".override-visibility").checked) {
|
|
rule.startHidden = ruleEl.querySelector(".site-startHidden").checked;
|
|
}
|
|
|
|
if (ruleEl.querySelector(".override-autohide").checked) {
|
|
rule.hideWithControls = ruleEl.querySelector(".site-hideWithControls").checked;
|
|
var st = parseFloat(
|
|
ruleEl.querySelector(".site-hideWithControlsTimer").value
|
|
);
|
|
rule.hideWithControlsTimer = Math.min(
|
|
15,
|
|
Math.max(0.1, Number.isFinite(st) ? st : settings.hideWithControlsTimer)
|
|
);
|
|
}
|
|
|
|
if (ruleEl.querySelector(".override-playback").checked) {
|
|
rule.rememberSpeed = ruleEl.querySelector(".site-rememberSpeed").checked;
|
|
rule.forceLastSavedSpeed =
|
|
ruleEl.querySelector(".site-forceLastSavedSpeed").checked;
|
|
rule.audioBoolean = ruleEl.querySelector(".site-audioBoolean").checked;
|
|
}
|
|
|
|
if (ruleEl.querySelector(".override-opacity").checked) {
|
|
rule.controllerOpacity =
|
|
parseFiniteNumberOrFallback(
|
|
ruleEl.querySelector(".site-controllerOpacity").value,
|
|
settings.controllerOpacity
|
|
);
|
|
}
|
|
|
|
if (ruleEl.querySelector(".override-subtitleNudge").checked) {
|
|
rule.enableSubtitleNudge =
|
|
ruleEl.querySelector(".site-enableSubtitleNudge").checked;
|
|
var nudgeIv = parseInt(
|
|
ruleEl.querySelector(".site-subtitleNudgeInterval").value,
|
|
10
|
|
);
|
|
rule.subtitleNudgeInterval = Math.min(
|
|
1000,
|
|
Math.max(
|
|
10,
|
|
Number.isFinite(nudgeIv) ? nudgeIv : settings.subtitleNudgeInterval
|
|
)
|
|
);
|
|
}
|
|
|
|
if (ruleEl.querySelector(".override-controlbar").checked) {
|
|
var activeZone = ruleEl.querySelector(".site-cb-active");
|
|
if (activeZone) {
|
|
rule.controllerButtons = readControlBarOrder(activeZone);
|
|
}
|
|
}
|
|
|
|
if (ruleEl.querySelector(".override-popup-controlbar").checked) {
|
|
rule.showPopupControlBar =
|
|
ruleEl.querySelector(".site-showPopupControlBar").checked;
|
|
var popupActiveZone = ruleEl.querySelector(".site-popup-cb-active");
|
|
if (popupActiveZone) {
|
|
rule.popupControllerButtons = sanitizePopupButtonOrder(
|
|
readControlBarOrder(popupActiveZone)
|
|
);
|
|
}
|
|
}
|
|
|
|
if (ruleEl.querySelector(".override-shortcuts").checked) {
|
|
var shortcuts = [];
|
|
ruleEl.querySelectorAll(".site-shortcuts-container .customs").forEach((shortcutRow) => {
|
|
if (saveError) return;
|
|
var action = shortcutRow.dataset.action;
|
|
var keyInput = shortcutRow.querySelector(".customKey");
|
|
var valueInput = shortcutRow.querySelector(".customValue");
|
|
var forceCheckbox = shortcutRow.querySelector(".customForce");
|
|
var binding = normalizeStoredBinding(keyInput.vscBinding);
|
|
|
|
if (!binding) {
|
|
if (requiredShortcutActions.has(action)) {
|
|
saveError =
|
|
"Error: Site rule shortcut for " +
|
|
(actionLabels[action] || action) +
|
|
" cannot be empty. Unable to save";
|
|
return;
|
|
}
|
|
binding = createDisabledBinding();
|
|
}
|
|
|
|
if (binding.disabled === true && requiredShortcutActions.has(action)) {
|
|
saveError =
|
|
"Error: Site rule shortcut for " +
|
|
(actionLabels[action] || action) +
|
|
" cannot be empty. Unable to save";
|
|
return;
|
|
}
|
|
|
|
shortcuts.push({
|
|
action: action,
|
|
code: binding.code,
|
|
disabled: binding.disabled === true,
|
|
value: customActionsNoValues.includes(action)
|
|
? 0
|
|
: Number(valueInput.value),
|
|
force: forceCheckbox ? forceCheckbox.checked : false
|
|
});
|
|
});
|
|
if (saveError) return;
|
|
if (shortcuts.length > 0) rule.shortcuts = shortcuts;
|
|
}
|
|
|
|
settings.siteRules.push(rule);
|
|
});
|
|
|
|
// Legacy keys to remove
|
|
const legacyKeys = [
|
|
"resetSpeed", "speedStep", "fastSpeed", "rewindTime", "advanceTime",
|
|
"resetKeyCode", "slowerKeyCode", "fasterKeyCode", "rewindKeyCode",
|
|
"advanceKeyCode", "fastKeyCode", "blacklist"
|
|
];
|
|
|
|
chrome.storage.sync.remove(legacyKeys, function () {
|
|
chrome.storage.sync.set(settings, function () {
|
|
status.textContent = "Options saved";
|
|
setTimeout(function () {
|
|
status.textContent = "";
|
|
}, 1000);
|
|
});
|
|
});
|
|
}
|
|
|
|
function ensureAllDefaultBindings(storage) {
|
|
tcDefaults.keyBindings.forEach((binding) => {
|
|
ensureDefaultBinding(storage, binding.action, binding.code, binding.value);
|
|
});
|
|
}
|
|
|
|
function addSiteRuleShortcut(container, action, binding, value, force) {
|
|
var div = document.createElement("div");
|
|
div.setAttribute("class", "shortcut-row customs");
|
|
div.dataset.action = action;
|
|
|
|
var actionLabel = document.createElement("div");
|
|
actionLabel.className = "shortcut-label";
|
|
var actionLabels = {
|
|
display: "Show/hide controller",
|
|
move: "Move controller",
|
|
slower: "Decrease speed",
|
|
faster: "Increase speed",
|
|
rewind: "Rewind",
|
|
advance: "Advance",
|
|
reset: "Reset speed",
|
|
fast: "Preferred speed",
|
|
toggleSubtitleNudge: "Toggle subtitle nudge",
|
|
pause: "Play / Pause",
|
|
muted: "Mute / Unmute",
|
|
louder: "Increase volume",
|
|
softer: "Decrease volume",
|
|
mark: "Set marker",
|
|
jump: "Jump to marker"
|
|
};
|
|
var actionLabelText = actionLabels[action] || action;
|
|
if (action === "toggleSubtitleNudge") {
|
|
// Check if the site rule is for YouTube.
|
|
// We look up the pattern from the site rule element this container belongs to.
|
|
var ruleEl = container.closest(".site-rule");
|
|
var pattern = ruleEl ? ruleEl.querySelector(".site-pattern").value : "";
|
|
if (!pattern.toLowerCase().includes("youtube.com")) {
|
|
actionLabelText += " (only for YouTube embeds)";
|
|
}
|
|
}
|
|
actionLabel.textContent = actionLabelText;
|
|
|
|
var keyInput = document.createElement("input");
|
|
keyInput.className = "customKey";
|
|
keyInput.type = "text";
|
|
keyInput.placeholder = "press a key";
|
|
updateCustomShortcutInputText(keyInput, binding || createDisabledBinding());
|
|
|
|
var valueInput = document.createElement("input");
|
|
valueInput.className = "customValue";
|
|
valueInput.type = "text";
|
|
valueInput.placeholder = "value (0.10)";
|
|
if (customActionsNoValues.includes(action)) {
|
|
valueInput.value = "N/A";
|
|
valueInput.disabled = true;
|
|
} else {
|
|
valueInput.value = formatSpeedBindingDisplay(
|
|
action,
|
|
resolveShortcutValue(action, value)
|
|
);
|
|
}
|
|
|
|
var forceLabel = document.createElement("label");
|
|
forceLabel.className = "force-label";
|
|
forceLabel.title = "Prevent website from capturing this key";
|
|
|
|
var forceCheckbox = document.createElement("input");
|
|
forceCheckbox.type = "checkbox";
|
|
forceCheckbox.className = "customForce";
|
|
forceCheckbox.checked = force === true || force === "true";
|
|
|
|
var forceText = document.createElement("span");
|
|
forceText.textContent = "Block site from capturing keypress";
|
|
forceText.className = "force-text";
|
|
|
|
forceLabel.appendChild(forceCheckbox);
|
|
forceLabel.appendChild(forceText);
|
|
|
|
div.appendChild(actionLabel);
|
|
div.appendChild(keyInput);
|
|
div.appendChild(valueInput);
|
|
div.appendChild(forceLabel);
|
|
|
|
container.appendChild(div);
|
|
}
|
|
|
|
function createSiteRule(rule) {
|
|
var template = document.getElementById("siteRuleTemplate");
|
|
var clone = template.content.cloneNode(true);
|
|
var ruleEl = clone.querySelector(".site-rule");
|
|
|
|
var pattern = rule && rule.pattern ? rule.pattern : "";
|
|
ruleEl.querySelector(".site-pattern").value = pattern;
|
|
|
|
// Make the rule body collapsed by default
|
|
setSiteRuleExpandedState(ruleEl, false);
|
|
|
|
var enabledCheckbox = ruleEl.querySelector(".site-enabled");
|
|
var contentEl = ruleEl.querySelector(".site-rule-content");
|
|
|
|
function updateDisabledState() {
|
|
if (enabledCheckbox.checked) {
|
|
contentEl.classList.remove("disabled-rule");
|
|
} else {
|
|
contentEl.classList.add("disabled-rule");
|
|
}
|
|
}
|
|
|
|
enabledCheckbox.addEventListener("change", updateDisabledState);
|
|
|
|
if (rule) {
|
|
if (rule.enabled !== undefined) {
|
|
enabledCheckbox.checked = rule.enabled;
|
|
} else if (rule.disableExtension !== undefined) {
|
|
enabledCheckbox.checked = !rule.disableExtension;
|
|
} else {
|
|
enabledCheckbox.checked = true;
|
|
}
|
|
} else {
|
|
enabledCheckbox.checked = true;
|
|
}
|
|
updateDisabledState();
|
|
|
|
var placementKeys = [
|
|
"controllerLocation",
|
|
"controllerMarginTop",
|
|
"controllerMarginBottom"
|
|
];
|
|
var hasPlacementOverride =
|
|
rule && placementKeys.some(function (k) { return rule[k] !== undefined; });
|
|
ruleEl.querySelector(".override-placement").checked = Boolean(hasPlacementOverride);
|
|
syncSiteRuleField(ruleEl, rule, "controllerLocation", false);
|
|
syncSiteRuleField(ruleEl, rule, "controllerMarginTop", false);
|
|
syncSiteRuleField(ruleEl, rule, "controllerMarginBottom", false);
|
|
applySiteRuleOverrideState(ruleEl, "override-placement", "site-placement-container");
|
|
|
|
ruleEl.querySelector(".override-visibility").checked = Boolean(
|
|
rule && rule.startHidden !== undefined
|
|
);
|
|
syncSiteRuleField(ruleEl, rule, "startHidden", true);
|
|
applySiteRuleOverrideState(ruleEl, "override-visibility", "site-visibility-container");
|
|
|
|
var hasAutohideOverride = Boolean(
|
|
rule &&
|
|
(rule.hideWithControls !== undefined ||
|
|
rule.hideWithControlsTimer !== undefined)
|
|
);
|
|
ruleEl.querySelector(".override-autohide").checked = hasAutohideOverride;
|
|
syncSiteRuleField(ruleEl, rule, "hideWithControls", true);
|
|
syncSiteRuleField(ruleEl, rule, "hideWithControlsTimer", false);
|
|
applySiteRuleOverrideState(ruleEl, "override-autohide", "site-autohide-container");
|
|
|
|
var hasPlaybackOverride = Boolean(
|
|
rule &&
|
|
(rule.rememberSpeed !== undefined ||
|
|
rule.forceLastSavedSpeed !== undefined ||
|
|
rule.audioBoolean !== undefined)
|
|
);
|
|
ruleEl.querySelector(".override-playback").checked = hasPlaybackOverride;
|
|
syncSiteRuleField(ruleEl, rule, "rememberSpeed", true);
|
|
syncSiteRuleField(ruleEl, rule, "forceLastSavedSpeed", true);
|
|
syncSiteRuleField(ruleEl, rule, "audioBoolean", true);
|
|
applySiteRuleOverrideState(ruleEl, "override-playback", "site-playback-container");
|
|
|
|
ruleEl.querySelector(".override-opacity").checked = Boolean(
|
|
rule && rule.controllerOpacity !== undefined
|
|
);
|
|
syncSiteRuleField(ruleEl, rule, "controllerOpacity", false);
|
|
applySiteRuleOverrideState(ruleEl, "override-opacity", "site-opacity-container");
|
|
|
|
var hasSubtitleNudgeOverride = Boolean(
|
|
rule &&
|
|
(rule.enableSubtitleNudge !== undefined ||
|
|
rule.subtitleNudgeInterval !== undefined)
|
|
);
|
|
ruleEl.querySelector(".override-subtitleNudge").checked = hasSubtitleNudgeOverride;
|
|
syncSiteRuleField(ruleEl, rule, "enableSubtitleNudge", true);
|
|
syncSiteRuleField(ruleEl, rule, "subtitleNudgeInterval", false);
|
|
applySiteRuleOverrideState(
|
|
ruleEl,
|
|
"override-subtitleNudge",
|
|
"site-subtitleNudge-container"
|
|
);
|
|
|
|
var hasControlbarOverride = Boolean(rule && Array.isArray(rule.controllerButtons));
|
|
ruleEl.querySelector(".override-controlbar").checked = hasControlbarOverride;
|
|
populateControlBarZones(
|
|
ruleEl.querySelector(".site-cb-active"),
|
|
ruleEl.querySelector(".site-cb-available"),
|
|
hasControlbarOverride ? rule.controllerButtons : getControlBarOrder()
|
|
);
|
|
applySiteRuleOverrideState(ruleEl, "override-controlbar", "site-controlbar-container");
|
|
|
|
var hasPopupControlbarOverride = Boolean(
|
|
rule &&
|
|
(rule.showPopupControlBar !== undefined ||
|
|
Array.isArray(rule.popupControllerButtons))
|
|
);
|
|
ruleEl.querySelector(".override-popup-controlbar").checked =
|
|
hasPopupControlbarOverride;
|
|
populateControlBarZones(
|
|
ruleEl.querySelector(".site-popup-cb-active"),
|
|
ruleEl.querySelector(".site-popup-cb-available"),
|
|
hasPopupControlbarOverride && Array.isArray(rule.popupControllerButtons)
|
|
? sanitizePopupButtonOrder(rule.popupControllerButtons)
|
|
: getPopupControlBarOrder(),
|
|
function (id) {
|
|
return !popupExcludedButtonIds.has(id);
|
|
}
|
|
);
|
|
syncSiteRuleField(ruleEl, rule, "showPopupControlBar", true);
|
|
applySiteRuleOverrideState(
|
|
ruleEl,
|
|
"override-popup-controlbar",
|
|
"site-popup-controlbar-container"
|
|
);
|
|
|
|
var hasShortcutOverride = Boolean(
|
|
rule && Array.isArray(rule.shortcuts) && rule.shortcuts.length > 0
|
|
);
|
|
ruleEl.querySelector(".override-shortcuts").checked = hasShortcutOverride;
|
|
var container = ruleEl.querySelector(".site-shortcuts-container");
|
|
if (hasShortcutOverride) {
|
|
rule.shortcuts.forEach((shortcut) => {
|
|
addSiteRuleShortcut(
|
|
container,
|
|
shortcut.action,
|
|
shortcut,
|
|
shortcut.value,
|
|
shortcut.force
|
|
);
|
|
});
|
|
} else {
|
|
populateDefaultSiteShortcuts(container);
|
|
}
|
|
applySiteRuleOverrideState(ruleEl, "override-shortcuts", "site-shortcuts-container");
|
|
|
|
document.getElementById("siteRulesContainer").appendChild(ruleEl);
|
|
}
|
|
|
|
function populateDefaultSiteShortcuts(container) {
|
|
var bindings = [];
|
|
document.querySelectorAll("#customs .shortcut-row").forEach((row) => {
|
|
var action = row.dataset.action;
|
|
if (!action) return;
|
|
|
|
var keyInput = row.querySelector(".customKey");
|
|
var binding = normalizeStoredBinding(keyInput && keyInput.vscBinding);
|
|
if (!binding) return;
|
|
|
|
var valueInput = row.querySelector(".customValue");
|
|
bindings.push({
|
|
action: action,
|
|
code: binding.code,
|
|
disabled: binding.disabled === true,
|
|
value: customActionsNoValues.includes(action)
|
|
? 0
|
|
: Number(valueInput && valueInput.value),
|
|
force: false
|
|
});
|
|
});
|
|
|
|
if (bindings.length === 0) {
|
|
bindings = tcDefaults.keyBindings.slice();
|
|
}
|
|
|
|
bindings.forEach((binding) => {
|
|
addSiteRuleShortcut(container, binding.action, binding, binding.value, false);
|
|
});
|
|
}
|
|
|
|
function createControlBarBlock(buttonId) {
|
|
var def = controllerButtonDefs[buttonId];
|
|
if (!def) return null;
|
|
|
|
var block = document.createElement("div");
|
|
block.className = "cb-block";
|
|
block.dataset.buttonId = buttonId;
|
|
block.draggable = true;
|
|
|
|
var grip = document.createElement("span");
|
|
grip.className = "cb-grip";
|
|
|
|
var icon = document.createElement("span");
|
|
icon.className = "cb-icon";
|
|
fillControlBarIconElement(icon, buttonId);
|
|
|
|
var label = document.createElement("span");
|
|
label.className = "cb-label";
|
|
label.textContent = def.name;
|
|
|
|
block.appendChild(grip);
|
|
block.appendChild(icon);
|
|
block.appendChild(label);
|
|
|
|
return block;
|
|
}
|
|
|
|
function populateControlBarZones(activeZone, availableZone, activeIds, allowButtonId) {
|
|
vscClearElement(activeZone);
|
|
vscClearElement(availableZone);
|
|
|
|
var allowed = function (id) {
|
|
if (!controllerButtonDefs[id]) return false;
|
|
return typeof allowButtonId === "function" ? Boolean(allowButtonId(id)) : true;
|
|
};
|
|
|
|
activeIds.forEach(function (id) {
|
|
if (!allowed(id)) return;
|
|
var block = createControlBarBlock(id);
|
|
if (block) activeZone.appendChild(block);
|
|
});
|
|
|
|
Object.keys(controllerButtonDefs).forEach(function (id) {
|
|
if (!allowed(id)) return;
|
|
if (!activeIds.includes(id)) {
|
|
var block = createControlBarBlock(id);
|
|
if (block) availableZone.appendChild(block);
|
|
}
|
|
});
|
|
}
|
|
|
|
function readControlBarOrder(activeZone) {
|
|
var blocks = activeZone.querySelectorAll(".cb-block");
|
|
return Array.from(blocks).map(function (block) {
|
|
return block.dataset.buttonId;
|
|
});
|
|
}
|
|
|
|
function populateControlBarEditor(activeIds) {
|
|
populateControlBarZones(
|
|
document.getElementById("controlBarActive"),
|
|
document.getElementById("controlBarAvailable"),
|
|
activeIds
|
|
);
|
|
}
|
|
|
|
function getControlBarOrder() {
|
|
return readControlBarOrder(document.getElementById("controlBarActive"));
|
|
}
|
|
|
|
function populatePopupControlBarEditor(activeIds) {
|
|
var popupActiveIds = sanitizePopupButtonOrder(activeIds);
|
|
populateControlBarZones(
|
|
document.getElementById("popupControlBarActive"),
|
|
document.getElementById("popupControlBarAvailable"),
|
|
popupActiveIds,
|
|
function (id) {
|
|
return !popupExcludedButtonIds.has(id);
|
|
}
|
|
);
|
|
}
|
|
|
|
function getPopupControlBarOrder() {
|
|
return sanitizePopupButtonOrder(
|
|
readControlBarOrder(document.getElementById("popupControlBarActive"))
|
|
);
|
|
}
|
|
|
|
function updatePopupEditorDisabledState() {
|
|
var checkbox = document.getElementById("popupMatchHoverControls");
|
|
var wrap = document.getElementById("popupCbEditorWrap");
|
|
if (!checkbox || !wrap) return;
|
|
if (checkbox.checked) {
|
|
wrap.classList.add("cb-editor-disabled");
|
|
} else {
|
|
wrap.classList.remove("cb-editor-disabled");
|
|
}
|
|
}
|
|
|
|
function getDragAfterElement(container, x, y) {
|
|
var elements = Array.from(
|
|
container.querySelectorAll(".cb-block:not(.cb-dragging)")
|
|
);
|
|
|
|
for (var i = 0; i < elements.length; i++) {
|
|
var box = elements[i].getBoundingClientRect();
|
|
var centerX = box.left + box.width / 2;
|
|
var centerY = box.top + box.height / 2;
|
|
var rowThresh = box.height * 0.5;
|
|
|
|
if (y - centerY > rowThresh) continue;
|
|
if (centerY - y > rowThresh) return elements[i];
|
|
if (x < centerX) return elements[i];
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function initControlBarEditor() {
|
|
var draggedBlock = null;
|
|
|
|
function clearControlBarDropTargets(activeZone) {
|
|
document.querySelectorAll(".cb-dropzone.cb-over").forEach(function (zone) {
|
|
if (zone !== activeZone) {
|
|
zone.classList.remove("cb-over");
|
|
}
|
|
});
|
|
}
|
|
|
|
document.addEventListener("dragstart", function (e) {
|
|
var block = e.target.closest(".cb-block");
|
|
if (!block) return;
|
|
draggedBlock = block;
|
|
e.dataTransfer.effectAllowed = "move";
|
|
e.dataTransfer.setData("text/plain", block.dataset.buttonId);
|
|
requestAnimationFrame(function () {
|
|
block.classList.add("cb-dragging");
|
|
});
|
|
});
|
|
|
|
document.addEventListener("dragend", function (e) {
|
|
var block = e.target.closest(".cb-block");
|
|
if (!block) return;
|
|
block.classList.remove("cb-dragging");
|
|
draggedBlock = null;
|
|
clearControlBarDropTargets(null);
|
|
});
|
|
|
|
document.addEventListener("dragover", function (e) {
|
|
var zone = e.target.closest(".cb-dropzone");
|
|
if (!zone) {
|
|
clearControlBarDropTargets(null);
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
if (e.dataTransfer) {
|
|
e.dataTransfer.dropEffect = "move";
|
|
}
|
|
clearControlBarDropTargets(zone);
|
|
zone.classList.add("cb-over");
|
|
|
|
if (!draggedBlock) return;
|
|
|
|
var afterEl = getDragAfterElement(zone, e.clientX, e.clientY);
|
|
if (afterEl) {
|
|
zone.insertBefore(draggedBlock, afterEl);
|
|
} else {
|
|
zone.appendChild(draggedBlock);
|
|
}
|
|
});
|
|
|
|
document.addEventListener("drop", function (e) {
|
|
var zone = e.target.closest(".cb-dropzone");
|
|
if (zone) {
|
|
e.preventDefault();
|
|
}
|
|
|
|
clearControlBarDropTargets(null);
|
|
});
|
|
}
|
|
|
|
var lucidePickerSelectedSlug = null;
|
|
var lucideSearchTimer = null;
|
|
|
|
function setLucideStatus(msg) {
|
|
var el = document.getElementById("lucideIconStatus");
|
|
if (el) el.textContent = msg || "";
|
|
}
|
|
|
|
function repaintAllCbIconsFromCustomMap() {
|
|
document.querySelectorAll(".cb-block .cb-icon").forEach(function (icon) {
|
|
var block = icon.closest(".cb-block");
|
|
if (!block) return;
|
|
fillControlBarIconElement(icon, block.dataset.buttonId);
|
|
});
|
|
}
|
|
|
|
function persistCustomButtonIcons(map, callback) {
|
|
chrome.storage.local.set({ customButtonIcons: map }, function () {
|
|
if (chrome.runtime.lastError) {
|
|
setLucideStatus(
|
|
"Could not save icons: " + chrome.runtime.lastError.message
|
|
);
|
|
return;
|
|
}
|
|
customButtonIconsLive = map;
|
|
if (callback) callback();
|
|
repaintAllCbIconsFromCustomMap();
|
|
});
|
|
}
|
|
|
|
function initLucideButtonIconsUI() {
|
|
var actionSel = document.getElementById("lucideIconActionSelect");
|
|
var searchInput = document.getElementById("lucideIconSearch");
|
|
var resultsEl = document.getElementById("lucideIconResults");
|
|
var previewEl = document.getElementById("lucideIconPreview");
|
|
if (!actionSel || !searchInput || !resultsEl || !previewEl) return;
|
|
if (typeof getLucideTagsMap !== "function") return;
|
|
|
|
if (!actionSel.dataset.lucideInit) {
|
|
actionSel.dataset.lucideInit = "1";
|
|
vscClearElement(actionSel);
|
|
Object.keys(controllerButtonDefs).forEach(function (aid) {
|
|
if (aid === "nudge") {
|
|
Object.keys(lucideSubtitleNudgeActionLabels).forEach(function (subId) {
|
|
var o2 = document.createElement("option");
|
|
o2.value = subId;
|
|
o2.textContent =
|
|
lucideSubtitleNudgeActionLabels[subId] + " (" + subId + ")";
|
|
actionSel.appendChild(o2);
|
|
});
|
|
return;
|
|
}
|
|
var o = document.createElement("option");
|
|
o.value = aid;
|
|
o.textContent =
|
|
controllerButtonDefs[aid].name + " (" + aid + ")";
|
|
actionSel.appendChild(o);
|
|
});
|
|
}
|
|
|
|
function renderResults(slugs) {
|
|
vscClearElement(resultsEl);
|
|
slugs.forEach(function (slug) {
|
|
var b = document.createElement("button");
|
|
b.type = "button";
|
|
b.className = "lucide-result-tile";
|
|
b.dataset.slug = slug;
|
|
b.title = slug;
|
|
b.setAttribute("aria-label", slug);
|
|
if (slug === lucidePickerSelectedSlug) {
|
|
b.classList.add("lucide-picked");
|
|
}
|
|
var url =
|
|
typeof lucideIconSvgUrl === "function" ? lucideIconSvgUrl(slug) : "";
|
|
if (url) {
|
|
var img = document.createElement("img");
|
|
img.className = "lucide-result-thumb";
|
|
img.src = url;
|
|
img.alt = "";
|
|
img.loading = "lazy";
|
|
b.appendChild(img);
|
|
} else {
|
|
b.textContent = slug.slice(0, 3);
|
|
}
|
|
b.addEventListener("click", function () {
|
|
lucidePickerSelectedSlug = slug;
|
|
Array.prototype.forEach.call(
|
|
resultsEl.querySelectorAll("button"),
|
|
function (x) {
|
|
x.classList.toggle("lucide-picked", x.dataset.slug === slug);
|
|
}
|
|
);
|
|
fetchLucideSvg(slug)
|
|
.then(function (txt) {
|
|
var safe = sanitizeLucideSvg(txt);
|
|
if (!safe) throw new Error("Bad SVG");
|
|
if (!vscSetSvgContent(previewEl, safe)) {
|
|
throw new Error("Preview render failed");
|
|
}
|
|
setLucideStatus("Preview: " + slug);
|
|
})
|
|
.catch(function (e) {
|
|
vscClearElement(previewEl);
|
|
setLucideStatus(
|
|
"Could not load: " + slug + " — " + e.message
|
|
);
|
|
});
|
|
});
|
|
resultsEl.appendChild(b);
|
|
});
|
|
}
|
|
|
|
if (!searchInput.dataset.lucideBound) {
|
|
searchInput.dataset.lucideBound = "1";
|
|
searchInput.addEventListener("input", function () {
|
|
clearTimeout(lucideSearchTimer);
|
|
lucideSearchTimer = setTimeout(function () {
|
|
getLucideTagsMap(chrome.storage.local, false)
|
|
.then(function (map) {
|
|
var q = searchInput.value;
|
|
if (!q.trim()) {
|
|
vscClearElement(resultsEl);
|
|
return;
|
|
}
|
|
renderResults(searchLucideSlugs(map, q, 48));
|
|
})
|
|
.catch(function (e) {
|
|
setLucideStatus("Icon list error: " + e.message);
|
|
});
|
|
}, 200);
|
|
});
|
|
}
|
|
|
|
var applyBtn = document.getElementById("lucideIconApply");
|
|
if (applyBtn && !applyBtn.dataset.lucideBound) {
|
|
applyBtn.dataset.lucideBound = "1";
|
|
applyBtn.addEventListener("click", function () {
|
|
var action = actionSel.value;
|
|
var slug = lucidePickerSelectedSlug;
|
|
if (!action || !slug) {
|
|
setLucideStatus("Pick an action and click an icon first.");
|
|
return;
|
|
}
|
|
fetchLucideSvg(slug)
|
|
.then(function (txt) {
|
|
var safe = sanitizeLucideSvg(txt);
|
|
if (!safe) throw new Error("Sanitize failed");
|
|
var next = Object.assign({}, customButtonIconsLive);
|
|
next[action] = { slug: slug, svg: safe };
|
|
persistCustomButtonIcons(next, function () {
|
|
setLucideStatus(
|
|
"Saved " +
|
|
slug +
|
|
" for " +
|
|
action +
|
|
". Reload pages for the hover bar."
|
|
);
|
|
});
|
|
})
|
|
.catch(function (e) {
|
|
setLucideStatus("Apply failed: " + e.message);
|
|
});
|
|
});
|
|
}
|
|
|
|
var clrOne = document.getElementById("lucideIconClearAction");
|
|
if (clrOne && !clrOne.dataset.lucideBound) {
|
|
clrOne.dataset.lucideBound = "1";
|
|
clrOne.addEventListener("click", function () {
|
|
var action = actionSel.value;
|
|
if (!action) return;
|
|
var next = Object.assign({}, customButtonIconsLive);
|
|
delete next[action];
|
|
persistCustomButtonIcons(next, function () {
|
|
setLucideStatus("Cleared custom icon for " + action + ".");
|
|
});
|
|
});
|
|
}
|
|
|
|
var clrAll = document.getElementById("lucideIconClearAll");
|
|
if (clrAll && !clrAll.dataset.lucideBound) {
|
|
clrAll.dataset.lucideBound = "1";
|
|
clrAll.addEventListener("click", function () {
|
|
persistCustomButtonIcons({}, function () {
|
|
setLucideStatus("All custom icons cleared.");
|
|
});
|
|
});
|
|
}
|
|
|
|
var reloadTags = document.getElementById("lucideIconReloadTags");
|
|
if (reloadTags && !reloadTags.dataset.lucideBound) {
|
|
reloadTags.dataset.lucideBound = "1";
|
|
reloadTags.addEventListener("click", function () {
|
|
getLucideTagsMap(chrome.storage.local, true)
|
|
.then(function () {
|
|
setLucideStatus("Icon name list refreshed.");
|
|
})
|
|
.catch(function (e) {
|
|
setLucideStatus("Refresh failed: " + e.message);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
function restore_options() {
|
|
chrome.storage.sync.get(tcDefaults, function (storage) {
|
|
chrome.storage.local.get(["customButtonIcons"], function (loc) {
|
|
customButtonIconsLive =
|
|
loc && loc.customButtonIcons && typeof loc.customButtonIcons === "object"
|
|
? loc.customButtonIcons
|
|
: {};
|
|
|
|
document.getElementById("rememberSpeed").checked = storage.rememberSpeed;
|
|
document.getElementById("forceLastSavedSpeed").checked =
|
|
storage.forceLastSavedSpeed;
|
|
document.getElementById("audioBoolean").checked = storage.audioBoolean;
|
|
document.getElementById("enabled").checked = storage.enabled;
|
|
document.getElementById("startHidden").checked = storage.startHidden;
|
|
|
|
// Migration/Normalization for hideWithControls
|
|
const hideWithControls = typeof storage.hideWithControls !== "undefined"
|
|
? storage.hideWithControls
|
|
: storage.hideWithYouTubeControls;
|
|
|
|
document.getElementById("hideWithControls").checked = hideWithControls;
|
|
document.getElementById("hideWithControlsTimer").value =
|
|
storage.hideWithControlsTimer || tcDefaults.hideWithControlsTimer;
|
|
|
|
document.getElementById("controllerLocation").value =
|
|
normalizeControllerLocation(storage.controllerLocation);
|
|
document.getElementById("controllerOpacity").value =
|
|
storage.controllerOpacity;
|
|
document.getElementById("controllerMarginTop").value =
|
|
storage.controllerMarginTop ?? tcDefaults.controllerMarginTop;
|
|
document.getElementById("controllerMarginBottom").value =
|
|
storage.controllerMarginBottom ?? tcDefaults.controllerMarginBottom;
|
|
document.getElementById("showPopupControlBar").checked =
|
|
storage.showPopupControlBar !== false;
|
|
document.getElementById("enableSubtitleNudge").checked =
|
|
storage.enableSubtitleNudge;
|
|
document.getElementById("subtitleNudgeInterval").value =
|
|
storage.subtitleNudgeInterval;
|
|
|
|
if (!Array.isArray(storage.keyBindings) || storage.keyBindings.length === 0) {
|
|
storage.keyBindings = tcDefaults.keyBindings.slice();
|
|
}
|
|
|
|
ensureAllDefaultBindings(storage);
|
|
|
|
document.querySelectorAll(".customs:not([id])").forEach((row) => row.remove());
|
|
|
|
storage.keyBindings.forEach((item) => {
|
|
var row = document.getElementById(item.action);
|
|
var normalizedBinding = normalizeStoredBinding(item);
|
|
|
|
if (!row) {
|
|
add_shortcut(item.action, item.value);
|
|
row = document.querySelector(".shortcut-row.customs:last-of-type");
|
|
}
|
|
|
|
if (!row) return;
|
|
|
|
var keyInput = row.querySelector(".customKey");
|
|
if (keyInput) {
|
|
updateCustomShortcutInputText(keyInput, normalizedBinding || null);
|
|
}
|
|
|
|
var valueInput = row.querySelector(".customValue");
|
|
if (customActionsNoValues.includes(item.action)) {
|
|
if (valueInput) {
|
|
valueInput.value = "N/A";
|
|
valueInput.disabled = true;
|
|
}
|
|
} else if (valueInput) {
|
|
valueInput.value = formatSpeedBindingDisplay(item.action, item.value);
|
|
}
|
|
});
|
|
|
|
refreshAddShortcutSelector();
|
|
|
|
// Load site rules (use defaults if none in storage or empty array)
|
|
var siteRules =
|
|
Array.isArray(storage.siteRules) && storage.siteRules.length > 0
|
|
? storage.siteRules
|
|
: tcDefaults.siteRules || [];
|
|
|
|
vscClearElement(document.getElementById("siteRulesContainer"));
|
|
if (siteRules.length > 0) {
|
|
siteRules.forEach((rule) => {
|
|
if (rule && rule.pattern) {
|
|
createSiteRule(rule);
|
|
}
|
|
});
|
|
}
|
|
|
|
var controllerButtons = Array.isArray(storage.controllerButtons)
|
|
? storage.controllerButtons
|
|
: tcDefaults.controllerButtons;
|
|
populateControlBarEditor(controllerButtons);
|
|
|
|
document.getElementById("popupMatchHoverControls").checked =
|
|
storage.popupMatchHoverControls !== false;
|
|
|
|
var popupButtons = Array.isArray(storage.popupControllerButtons)
|
|
? storage.popupControllerButtons
|
|
: tcDefaults.popupControllerButtons;
|
|
populatePopupControlBarEditor(popupButtons);
|
|
updatePopupEditorDisabledState();
|
|
|
|
initLucideButtonIconsUI();
|
|
});
|
|
});
|
|
}
|
|
|
|
function restore_defaults() {
|
|
document.querySelectorAll(".customs:not([id])").forEach((el) => el.remove());
|
|
|
|
chrome.storage.local.remove(
|
|
["customButtonIcons", "lucideTagsCacheV1", "lucideTagsCacheV1At"],
|
|
function () {}
|
|
);
|
|
|
|
chrome.storage.sync.set(tcDefaults, function () {
|
|
restore_options();
|
|
var status = document.getElementById("status");
|
|
status.textContent = "Default options restored";
|
|
setTimeout(function () {
|
|
status.textContent = "";
|
|
}, 1000);
|
|
});
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
var manifest = chrome.runtime.getManifest();
|
|
var versionElement = document.getElementById("app-version");
|
|
if (versionElement) {
|
|
versionElement.textContent = manifest.version;
|
|
}
|
|
|
|
restore_options();
|
|
initControlBarEditor();
|
|
|
|
document.getElementById("popupMatchHoverControls")
|
|
.addEventListener("change", updatePopupEditorDisabledState);
|
|
|
|
document.getElementById("save").addEventListener("click", save_options);
|
|
|
|
const addSelector = document.getElementById("addShortcutSelector");
|
|
if (addSelector) {
|
|
addSelector.addEventListener("change", function (e) {
|
|
if (e.target.value) {
|
|
add_shortcut(e.target.value);
|
|
e.target.value = ""; // Reset selector
|
|
}
|
|
});
|
|
}
|
|
document
|
|
.getElementById("restore")
|
|
.addEventListener("click", restore_defaults);
|
|
document
|
|
.getElementById("addSiteRule")
|
|
.addEventListener("click", function () {
|
|
createSiteRule(null);
|
|
});
|
|
|
|
function eventCaller(event, className, funcName) {
|
|
if (!event.target.classList || !event.target.classList.contains(className)) {
|
|
return;
|
|
}
|
|
funcName(event);
|
|
}
|
|
|
|
document.addEventListener("keypress", (event) =>
|
|
eventCaller(event, "customValue", inputFilterNumbersOnly)
|
|
);
|
|
document.addEventListener("focus", (event) =>
|
|
eventCaller(event, "customKey", inputFocus)
|
|
);
|
|
document.addEventListener("blur", (event) =>
|
|
eventCaller(event, "customKey", inputBlur)
|
|
);
|
|
document.addEventListener("keydown", (event) =>
|
|
eventCaller(event, "customKey", recordKeyPress)
|
|
);
|
|
document.addEventListener("click", (event) => {
|
|
var target = event.target;
|
|
var targetEl = target && target.closest ? target : target.parentElement;
|
|
if (!targetEl) return;
|
|
|
|
var removeParentButton = targetEl.closest(".removeParent");
|
|
if (removeParentButton) {
|
|
removeParentButton.parentNode.remove();
|
|
refreshAddShortcutSelector();
|
|
return;
|
|
}
|
|
var removeSiteRuleButton = targetEl.closest(".remove-site-rule");
|
|
if (removeSiteRuleButton) {
|
|
removeSiteRuleButton.closest(".site-rule").remove();
|
|
return;
|
|
}
|
|
var toggleButton = targetEl.closest(".toggle-site-rule");
|
|
if (toggleButton) {
|
|
var ruleEl = toggleButton.closest(".site-rule");
|
|
var isCollapsed = ruleEl.classList.contains("collapsed");
|
|
setSiteRuleExpandedState(ruleEl, isCollapsed);
|
|
return;
|
|
}
|
|
});
|
|
document.addEventListener("change", (event) => {
|
|
if (event.target.classList.contains("customDo")) {
|
|
var valueInput = event.target.nextElementSibling.nextElementSibling;
|
|
if (customActionsNoValues.includes(event.target.value)) {
|
|
valueInput.disabled = true;
|
|
valueInput.value = 0;
|
|
} else {
|
|
valueInput.disabled = false;
|
|
}
|
|
}
|
|
|
|
// Site rule: show/hide optional override sections
|
|
var siteOverrideContainers = {
|
|
"override-placement": "site-placement-container",
|
|
"override-visibility": "site-visibility-container",
|
|
"override-autohide": "site-autohide-container",
|
|
"override-playback": "site-playback-container",
|
|
"override-opacity": "site-opacity-container",
|
|
"override-subtitleNudge": "site-subtitleNudge-container",
|
|
"override-controlbar": "site-controlbar-container",
|
|
"override-popup-controlbar": "site-popup-controlbar-container",
|
|
"override-shortcuts": "site-shortcuts-container"
|
|
};
|
|
for (var ocb in siteOverrideContainers) {
|
|
if (event.target.classList.contains(ocb)) {
|
|
var siteRuleRoot = event.target.closest(".site-rule");
|
|
var targetBox = siteRuleRoot.querySelector(
|
|
"." + siteOverrideContainers[ocb]
|
|
);
|
|
if (targetBox) {
|
|
setSiteOverrideContainerState(targetBox, event.target.checked);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
});
|