mirror of
https://github.com/SoPat712/videospeed.git
synced 2026-04-21 04:42:35 -04:00
Release v5.2.1
This commit is contained in:
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"kiroAgent.configureMCP": "Disabled"
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
# Available for Firefox
|
||||
[](https://addons.mozilla.org//firefox/addon/speeder/)
|
||||
[](https://addons.mozilla.org/firefox/addon/speeder/)
|
||||
|
||||
|
||||
# The science of accelerated playback
|
||||
@@ -33,7 +33,7 @@ last point to listen to it a few more times.
|
||||
|
||||

|
||||
|
||||
#### *Install [Chrome](https://chrome.google.com/webstore/detail/video-speed-controller/nffaoalbilbmmfgbnbgppjihopabppdk) or [Firefox](https://addons.mozilla.org/en-us/firefox/addon/videospeed/) Extension*
|
||||
#### *Install [Chrome](https://chrome.google.com/webstore/detail/video-speed-controller/nffaoalbilbmmfgbnbgppjihopabppdk) or [Firefox](https://addons.mozilla.org/en-us/firefox/addon/speeder/) Extension*
|
||||
|
||||
\*\* Once the extension is installed simply navigate to any page that offers
|
||||
HTML5 video ([example](https://www.youtube.com/watch?v=E9FxNzv1Tr8)), and you'll
|
||||
@@ -56,7 +56,7 @@ shortcuts with different values, which will allow you to quickly toggle between
|
||||
your most commonly used speeds. To add a new shortcut, open extension settings
|
||||
and click "Add New".
|
||||
|
||||

|
||||
<img width="1760" height="1330" alt="image" src="https://github.com/user-attachments/assets/32e814dd-93ea-4943-8ec9-3eca735447ac" />
|
||||
|
||||
Some sites may assign other functionality to one of the assigned shortcut keys —
|
||||
these collisions are inevitable, unfortunately. As a workaround, the extension
|
||||
|
||||
+57
-120
@@ -1,113 +1,20 @@
|
||||
// Import/Export functionality for Video Speed Controller settings
|
||||
|
||||
const EXPORTABLE_LOCAL_SETTINGS_KEYS = ["customButtonIcons"];
|
||||
|
||||
function getExportableLocalSettings(localStorage) {
|
||||
const exportable = {};
|
||||
const customButtonIcons =
|
||||
localStorage &&
|
||||
localStorage.customButtonIcons &&
|
||||
typeof localStorage.customButtonIcons === "object" &&
|
||||
!Array.isArray(localStorage.customButtonIcons)
|
||||
? localStorage.customButtonIcons
|
||||
: null;
|
||||
|
||||
if (customButtonIcons) {
|
||||
exportable.customButtonIcons = customButtonIcons;
|
||||
}
|
||||
|
||||
return exportable;
|
||||
}
|
||||
|
||||
function replaceImportableLocalSettings(localSettings, callback) {
|
||||
chrome.storage.local.remove(EXPORTABLE_LOCAL_SETTINGS_KEYS, function () {
|
||||
if (chrome.runtime.lastError) {
|
||||
showStatus(
|
||||
"Error: Failed to clear local icon overrides - " +
|
||||
chrome.runtime.lastError.message,
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!localSettings || Object.keys(localSettings).length === 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.local.set(localSettings, function () {
|
||||
if (chrome.runtime.lastError) {
|
||||
showStatus(
|
||||
"Error: Failed to save local icon overrides - " +
|
||||
chrome.runtime.lastError.message,
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
var speederShared =
|
||||
typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
|
||||
var importExportUtils = speederShared.importExport || {};
|
||||
|
||||
function generateBackupFilename() {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(now.getDate()).padStart(2, "0");
|
||||
const hours = String(now.getHours()).padStart(2, "0");
|
||||
const minutes = String(now.getMinutes()).padStart(2, "0");
|
||||
const seconds = String(now.getSeconds()).padStart(2, "0");
|
||||
return `speeder-backup_${year}-${month}-${day}_${hours}.${minutes}.${seconds}.json`;
|
||||
}
|
||||
|
||||
function getBackupManifestVersion() {
|
||||
var manifest = chrome.runtime.getManifest();
|
||||
return manifest && manifest.version ? manifest.version : "unknown";
|
||||
}
|
||||
|
||||
function getExportableSyncSettings(syncStorage) {
|
||||
return vscBuildStoredSettingsDiff(vscExpandStoredSettings(syncStorage));
|
||||
}
|
||||
|
||||
function getImportableSyncSettings(backup, rawSettings) {
|
||||
var importable = vscClonePlainData(rawSettings) || {};
|
||||
|
||||
if (
|
||||
backup &&
|
||||
backup.siteRulesFormat &&
|
||||
importable.siteRulesFormat === undefined
|
||||
) {
|
||||
importable.siteRulesFormat = backup.siteRulesFormat;
|
||||
}
|
||||
|
||||
if (
|
||||
backup &&
|
||||
backup.siteRulesMeta &&
|
||||
importable.siteRulesMeta === undefined
|
||||
) {
|
||||
importable.siteRulesMeta = backup.siteRulesMeta;
|
||||
}
|
||||
|
||||
return vscExpandStoredSettings(importable);
|
||||
return importExportUtils.generateBackupFilename(new Date());
|
||||
}
|
||||
|
||||
function exportSettings() {
|
||||
chrome.storage.sync.get(null, function (storage) {
|
||||
chrome.storage.local.get(
|
||||
EXPORTABLE_LOCAL_SETTINGS_KEYS,
|
||||
function (localStorage) {
|
||||
const localSettings = getExportableLocalSettings(localStorage);
|
||||
const syncSettings = getExportableSyncSettings(storage);
|
||||
const backup = {
|
||||
version: getBackupManifestVersion(),
|
||||
exportDate: new Date().toISOString(),
|
||||
settings: syncSettings
|
||||
};
|
||||
|
||||
if (Object.keys(localSettings).length > 0) {
|
||||
backup.localSettings = localSettings;
|
||||
}
|
||||
chrome.storage.local.get(null, function (localStorage) {
|
||||
const backup = importExportUtils.buildBackupPayload(
|
||||
storage,
|
||||
localStorage,
|
||||
new Date()
|
||||
);
|
||||
|
||||
const dataStr = JSON.stringify(backup, null, 2);
|
||||
const blob = new Blob([dataStr], { type: "application/json" });
|
||||
@@ -122,8 +29,7 @@ function exportSettings() {
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
showStatus("Settings exported successfully");
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -139,28 +45,58 @@ function importSettings() {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
try {
|
||||
const backup = JSON.parse(e.target.result);
|
||||
let settingsToImport = null;
|
||||
const parsedBackup = importExportUtils.parseImportText(e.target.result);
|
||||
|
||||
// Detect backup format: check for 'settings' wrapper or raw storage keys
|
||||
if (backup.settings && typeof backup.settings === "object") {
|
||||
settingsToImport = getImportableSyncSettings(backup, backup.settings);
|
||||
} else if (typeof backup === "object" && (backup.keyBindings || backup.rememberSpeed !== undefined)) {
|
||||
settingsToImport = getImportableSyncSettings(backup, backup);
|
||||
}
|
||||
|
||||
if (!settingsToImport) {
|
||||
if (!parsedBackup) {
|
||||
showStatus("Error: Invalid backup file format", true);
|
||||
return;
|
||||
}
|
||||
|
||||
var localToImport = getExportableLocalSettings(backup.localSettings);
|
||||
var settingsToImport = parsedBackup.settings;
|
||||
var localToImport = parsedBackup.localSettings;
|
||||
|
||||
function importLocalSettings(callback) {
|
||||
if (parsedBackup.isWrappedBackup !== true) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.local.clear(function () {
|
||||
if (chrome.runtime.lastError) {
|
||||
showStatus(
|
||||
"Error: Failed to clear local extension data - " +
|
||||
chrome.runtime.lastError.message,
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (localToImport && Object.keys(localToImport).length > 0) {
|
||||
chrome.storage.local.set(localToImport, function () {
|
||||
if (chrome.runtime.lastError) {
|
||||
showStatus(
|
||||
"Error: Failed to save local extension data - " +
|
||||
chrome.runtime.lastError.message,
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function afterLocalImport() {
|
||||
persistManagedSyncSettings(settingsToImport, function (error) {
|
||||
if (error) {
|
||||
chrome.storage.sync.clear(function () {
|
||||
chrome.storage.sync.set(settingsToImport, function () {
|
||||
if (chrome.runtime.lastError) {
|
||||
showStatus(
|
||||
"Error: Failed to save imported settings - " + error.message,
|
||||
"Error: Failed to save imported settings - " +
|
||||
chrome.runtime.lastError.message,
|
||||
true
|
||||
);
|
||||
return;
|
||||
@@ -174,9 +110,10 @@ function importSettings() {
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
replaceImportableLocalSettings(localToImport, afterLocalImport);
|
||||
importLocalSettings(afterLocalImport);
|
||||
} catch (err) {
|
||||
showStatus("Error: Failed to parse backup file - " + err.message, true);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
var isUserSeek = false; // Track if seek was user-initiated
|
||||
var lastToggleSpeed = {}; // Store last toggle speeds per video
|
||||
var sharedSettingsDefaults = vscGetSettingsDefaults();
|
||||
var speederShared =
|
||||
typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
|
||||
var controllerUtils = speederShared.controllerUtils || {};
|
||||
var keyBindingUtils = speederShared.keyBindings || {};
|
||||
var siteRuleUtils = speederShared.siteRules || {};
|
||||
|
||||
function getPrimaryVideoElement() {
|
||||
if (!tc.mediaElements || tc.mediaElements.length === 0) return null;
|
||||
@@ -13,37 +17,30 @@ function getPrimaryVideoElement() {
|
||||
|
||||
var tc = {
|
||||
settings: {
|
||||
lastSpeed: sharedSettingsDefaults.lastSpeed,
|
||||
enabled: sharedSettingsDefaults.enabled,
|
||||
lastSpeed: 1.0,
|
||||
enabled: true,
|
||||
speeds: {},
|
||||
displayKeyCode: sharedSettingsDefaults.displayKeyCode,
|
||||
rememberSpeed: sharedSettingsDefaults.rememberSpeed,
|
||||
forceLastSavedSpeed: sharedSettingsDefaults.forceLastSavedSpeed,
|
||||
audioBoolean: sharedSettingsDefaults.audioBoolean,
|
||||
startHidden: sharedSettingsDefaults.startHidden,
|
||||
hideWithYouTubeControls: sharedSettingsDefaults.hideWithYouTubeControls,
|
||||
hideWithControls: sharedSettingsDefaults.hideWithControls,
|
||||
hideWithControlsTimer: sharedSettingsDefaults.hideWithControlsTimer,
|
||||
controllerLocation: sharedSettingsDefaults.controllerLocation,
|
||||
controllerOpacity: sharedSettingsDefaults.controllerOpacity,
|
||||
controllerMarginTop: sharedSettingsDefaults.controllerMarginTop,
|
||||
controllerMarginRight: sharedSettingsDefaults.controllerMarginRight,
|
||||
controllerMarginBottom: sharedSettingsDefaults.controllerMarginBottom,
|
||||
controllerMarginLeft: sharedSettingsDefaults.controllerMarginLeft,
|
||||
keyBindings: Array.isArray(sharedSettingsDefaults.keyBindings)
|
||||
? sharedSettingsDefaults.keyBindings.slice()
|
||||
: [],
|
||||
siteRules: Array.isArray(sharedSettingsDefaults.siteRules)
|
||||
? sharedSettingsDefaults.siteRules.slice()
|
||||
: [],
|
||||
controllerButtons: Array.isArray(sharedSettingsDefaults.controllerButtons)
|
||||
? sharedSettingsDefaults.controllerButtons.slice()
|
||||
: ["rewind", "slower", "faster", "advance", "display"],
|
||||
rememberSpeed: false,
|
||||
forceLastSavedSpeed: false,
|
||||
audioBoolean: false,
|
||||
startHidden: false,
|
||||
hideWithYouTubeControls: false,
|
||||
hideWithControls: false,
|
||||
hideWithControlsTimer: 2.0,
|
||||
controllerLocation: "top-left",
|
||||
controllerOpacity: 0.3,
|
||||
controllerMarginTop: 0,
|
||||
controllerMarginRight: 0,
|
||||
controllerMarginBottom: 65,
|
||||
controllerMarginLeft: 0,
|
||||
keyBindings: [],
|
||||
siteRules: [],
|
||||
controllerButtons: ["rewind", "slower", "faster", "advance", "display"],
|
||||
defaultLogLevel: 3,
|
||||
logLevel: 3,
|
||||
enableSubtitleNudge: sharedSettingsDefaults.enableSubtitleNudge,
|
||||
subtitleNudgeInterval: sharedSettingsDefaults.subtitleNudgeInterval,
|
||||
subtitleNudgeAmount: sharedSettingsDefaults.subtitleNudgeAmount,
|
||||
enableSubtitleNudge: false,
|
||||
subtitleNudgeInterval: 50, // Default 50ms balances subtitle tracking with CPU cost
|
||||
subtitleNudgeAmount: 0.001,
|
||||
customButtonIcons: {}
|
||||
},
|
||||
mediaElements: [],
|
||||
@@ -64,10 +61,12 @@ var vscObservedRoots = new WeakSet();
|
||||
var requestIdle =
|
||||
typeof window.requestIdleCallback === "function"
|
||||
? window.requestIdleCallback.bind(window)
|
||||
: function (callback, options) {
|
||||
: function(callback, options) {
|
||||
return setTimeout(callback, (options && options.timeout) || 1);
|
||||
};
|
||||
var controllerLocations = [
|
||||
var controllerLocations = Array.isArray(controllerUtils.controllerLocations)
|
||||
? controllerUtils.controllerLocations.slice()
|
||||
: [
|
||||
"top-left",
|
||||
"top-center",
|
||||
"top-right",
|
||||
@@ -76,8 +75,9 @@ var controllerLocations = [
|
||||
"bottom-center",
|
||||
"bottom-left",
|
||||
"middle-left"
|
||||
];
|
||||
var defaultControllerLocation = controllerLocations[0];
|
||||
];
|
||||
var defaultControllerLocation =
|
||||
controllerUtils.defaultControllerLocation || controllerLocations[0];
|
||||
var controllerLocationStyles = {
|
||||
"top-left": {
|
||||
top: "10px",
|
||||
@@ -130,69 +130,20 @@ var controllerButtonDefs = {
|
||||
display: { label: "", className: "hideButton" },
|
||||
reset: { label: "\u21BB", className: "" },
|
||||
fast: { label: "", className: "" },
|
||||
settings: { label: "", className: "" },
|
||||
nudge: { label: "", className: "" },
|
||||
pause: { label: "", className: "" },
|
||||
muted: { label: "", className: "" },
|
||||
louder: { label: "", className: "" },
|
||||
softer: { label: "", className: "" },
|
||||
mark: { label: "", className: "" },
|
||||
jump: { label: "", className: "" }
|
||||
jump: { label: "", className: "" },
|
||||
settings: { label: "", className: "" }
|
||||
};
|
||||
|
||||
var keyCodeToEventKey = {
|
||||
32: " ",
|
||||
37: "ArrowLeft",
|
||||
38: "ArrowUp",
|
||||
39: "ArrowRight",
|
||||
40: "ArrowDown",
|
||||
96: "0",
|
||||
97: "1",
|
||||
98: "2",
|
||||
99: "3",
|
||||
100: "4",
|
||||
101: "5",
|
||||
102: "6",
|
||||
103: "7",
|
||||
104: "8",
|
||||
105: "9",
|
||||
106: "*",
|
||||
107: "+",
|
||||
109: "-",
|
||||
110: ".",
|
||||
111: "/",
|
||||
112: "F1",
|
||||
113: "F2",
|
||||
114: "F3",
|
||||
115: "F4",
|
||||
116: "F5",
|
||||
117: "F6",
|
||||
118: "F7",
|
||||
119: "F8",
|
||||
120: "F9",
|
||||
121: "F10",
|
||||
122: "F11",
|
||||
123: "F12",
|
||||
186: ";",
|
||||
188: "<",
|
||||
189: "-",
|
||||
187: "+",
|
||||
190: ">",
|
||||
191: "/",
|
||||
192: "~",
|
||||
219: "[",
|
||||
220: "\\",
|
||||
221: "]",
|
||||
222: "'",
|
||||
59: ";",
|
||||
61: "+",
|
||||
173: "-"
|
||||
};
|
||||
|
||||
function createDefaultBinding(action, key, keyCode, value) {
|
||||
function createDefaultBinding(action, code, value) {
|
||||
return {
|
||||
action: action,
|
||||
key: key,
|
||||
keyCode: keyCode,
|
||||
code: null,
|
||||
disabled: false,
|
||||
code: code,
|
||||
value: value,
|
||||
force: false,
|
||||
predefined: true
|
||||
@@ -203,89 +154,73 @@ function defaultKeyBindings(storage) {
|
||||
return [
|
||||
createDefaultBinding(
|
||||
"slower",
|
||||
"S",
|
||||
Number(storage.slowerKeyCode) || 83,
|
||||
"KeyS",
|
||||
Number(storage.speedStep) || 0.1
|
||||
),
|
||||
createDefaultBinding(
|
||||
"faster",
|
||||
"D",
|
||||
Number(storage.fasterKeyCode) || 68,
|
||||
"KeyD",
|
||||
Number(storage.speedStep) || 0.1
|
||||
),
|
||||
createDefaultBinding(
|
||||
"rewind",
|
||||
"Z",
|
||||
Number(storage.rewindKeyCode) || 90,
|
||||
"KeyZ",
|
||||
Number(storage.rewindTime) || 10
|
||||
),
|
||||
createDefaultBinding(
|
||||
"advance",
|
||||
"X",
|
||||
Number(storage.advanceKeyCode) || 88,
|
||||
"KeyX",
|
||||
Number(storage.advanceTime) || 10
|
||||
),
|
||||
createDefaultBinding(
|
||||
"reset",
|
||||
"R",
|
||||
Number(storage.resetKeyCode) || 82,
|
||||
0
|
||||
"KeyR",
|
||||
1.0
|
||||
),
|
||||
createDefaultBinding(
|
||||
"fast",
|
||||
"G",
|
||||
Number(storage.fastKeyCode) || 71,
|
||||
"KeyG",
|
||||
Number(storage.fastSpeed) || 1.8
|
||||
),
|
||||
createDefaultBinding(
|
||||
"move",
|
||||
"P",
|
||||
80,
|
||||
"KeyP",
|
||||
0
|
||||
),
|
||||
createDefaultBinding(
|
||||
"toggleSubtitleNudge",
|
||||
"N",
|
||||
78,
|
||||
"KeyN",
|
||||
0
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
function ensureDefaultKeyBinding(action, key, keyCode, value) {
|
||||
function ensureDefaultKeyBinding(action, code, value) {
|
||||
if (tc.settings.keyBindings.some((binding) => binding.action === action)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
tc.settings.keyBindings.push(
|
||||
createDefaultBinding(action, key, keyCode, value)
|
||||
createDefaultBinding(action, code, value)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
function getLegacyKeyCode(binding) {
|
||||
if (!binding) return null;
|
||||
if (Number.isInteger(binding.keyCode)) return binding.keyCode;
|
||||
if (typeof binding.key === "number" && Number.isInteger(binding.key)) {
|
||||
return binding.key;
|
||||
}
|
||||
return null;
|
||||
return keyBindingUtils.getLegacyKeyCode(binding);
|
||||
}
|
||||
|
||||
function normalizeControllerLocation(location) {
|
||||
if (controllerLocations.includes(location)) return location;
|
||||
return defaultControllerLocation;
|
||||
return controllerUtils.normalizeControllerLocation(
|
||||
location,
|
||||
defaultControllerLocation
|
||||
);
|
||||
}
|
||||
|
||||
var CONTROLLER_MARGIN_MAX_PX = 200;
|
||||
|
||||
function normalizeControllerMarginPx(value, fallback) {
|
||||
var n = Number(value);
|
||||
if (!Number.isFinite(n)) return fallback;
|
||||
return Math.min(
|
||||
CONTROLLER_MARGIN_MAX_PX,
|
||||
Math.max(0, Math.round(n))
|
||||
);
|
||||
return controllerUtils.clampControllerMarginPx(value, fallback);
|
||||
}
|
||||
|
||||
function applyControllerMargins(controller) {
|
||||
@@ -324,9 +259,7 @@ function applyControllerMargins(controller) {
|
||||
}
|
||||
|
||||
function getNextControllerLocation(location) {
|
||||
var normalizedLocation = normalizeControllerLocation(location);
|
||||
var currentIndex = controllerLocations.indexOf(normalizedLocation);
|
||||
return controllerLocations[(currentIndex + 1) % controllerLocations.length];
|
||||
return controllerUtils.getNextControllerLocation(location);
|
||||
}
|
||||
|
||||
function getControllerElement(videoOrController) {
|
||||
@@ -438,165 +371,6 @@ function applyControllerLocation(videoController, location) {
|
||||
);
|
||||
}
|
||||
|
||||
function getYouTubeAutoHidePlayer(video) {
|
||||
if (!video || !isOnYouTube()) return null;
|
||||
|
||||
return video.closest(".html5-video-player") || video.closest("#movie_player");
|
||||
}
|
||||
|
||||
function getAutoHideModeForVideo(video) {
|
||||
if (!tc.settings.hideWithControls) return "off";
|
||||
return getYouTubeAutoHidePlayer(video) ? "youtube" : "generic";
|
||||
}
|
||||
|
||||
function getControllerMountParent(video, parentHint) {
|
||||
var parentEl = parentHint || (video && (video.parentElement || video.parentNode));
|
||||
if (!parentEl) return null;
|
||||
|
||||
switch (true) {
|
||||
case location.hostname == "www.amazon.com":
|
||||
case location.hostname == "www.reddit.com":
|
||||
case /hbogo\./.test(location.hostname):
|
||||
return parentEl.parentElement || parentEl;
|
||||
case location.hostname == "www.facebook.com":
|
||||
var facebookParent = parentEl;
|
||||
for (
|
||||
var depth = 0;
|
||||
depth < 8 && facebookParent && facebookParent.parentElement;
|
||||
depth++
|
||||
) {
|
||||
facebookParent = facebookParent.parentElement;
|
||||
}
|
||||
return facebookParent || parentEl;
|
||||
case location.hostname == "tv.apple.com":
|
||||
var root = parentEl.getRootNode();
|
||||
var scrim = root && root.querySelector ? root.querySelector(".scrim") : null;
|
||||
return scrim || parentEl;
|
||||
case location.hostname == "www.youtube.com":
|
||||
case location.hostname == "m.youtube.com":
|
||||
case location.hostname == "music.youtube.com":
|
||||
return getYouTubeAutoHidePlayer(video) || parentEl;
|
||||
default:
|
||||
return parentEl;
|
||||
}
|
||||
}
|
||||
|
||||
function getControllerBehaviorSignature(video) {
|
||||
return JSON.stringify({
|
||||
startHidden: Boolean(tc.settings.startHidden),
|
||||
hideWithControls: Boolean(tc.settings.hideWithControls),
|
||||
hideWithControlsTimer: Number(tc.settings.hideWithControlsTimer),
|
||||
controllerLocation: normalizeControllerLocation(tc.settings.controllerLocation),
|
||||
controllerOpacity: Number(tc.settings.controllerOpacity),
|
||||
controllerMarginTop: normalizeControllerMarginPx(tc.settings.controllerMarginTop, 0),
|
||||
controllerMarginBottom: normalizeControllerMarginPx(
|
||||
tc.settings.controllerMarginBottom,
|
||||
0
|
||||
),
|
||||
controllerButtons: Array.isArray(tc.settings.controllerButtons)
|
||||
? tc.settings.controllerButtons.slice()
|
||||
: [],
|
||||
enableSubtitleNudge: Boolean(tc.settings.enableSubtitleNudge),
|
||||
subtitleNudgeInterval: Number(tc.settings.subtitleNudgeInterval),
|
||||
autoHideMode: getAutoHideModeForVideo(video),
|
||||
mediaTag: video && video.tagName ? video.tagName : ""
|
||||
});
|
||||
}
|
||||
|
||||
function rebuildControllerForVideo(video, parentHint, reason) {
|
||||
if (!video) return null;
|
||||
|
||||
var previous = video.vsc || null;
|
||||
var preservedState = previous
|
||||
? {
|
||||
mark: previous.mark,
|
||||
resetToggleArmed: previous.resetToggleArmed === true,
|
||||
subtitleNudgeEnabledOverride: previous.subtitleNudgeEnabledOverride,
|
||||
userHidden:
|
||||
Boolean(previous.div) &&
|
||||
previous.div.classList.contains("vsc-hidden")
|
||||
}
|
||||
: null;
|
||||
|
||||
if (previous) {
|
||||
previous.remove();
|
||||
}
|
||||
|
||||
if (!video.isConnected || !hasUsableMediaSource(video)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var nextController = new tc.videoController(
|
||||
video,
|
||||
parentHint || video.parentElement || video.parentNode
|
||||
);
|
||||
if (!nextController) return null;
|
||||
|
||||
if (preservedState) {
|
||||
nextController.mark = preservedState.mark;
|
||||
nextController.resetToggleArmed = preservedState.resetToggleArmed;
|
||||
|
||||
if (
|
||||
typeof preservedState.subtitleNudgeEnabledOverride === "boolean"
|
||||
) {
|
||||
nextController.subtitleNudgeEnabledOverride =
|
||||
preservedState.subtitleNudgeEnabledOverride;
|
||||
updateSubtitleNudgeIndicator(video);
|
||||
if (!preservedState.subtitleNudgeEnabledOverride) {
|
||||
nextController.stopSubtitleNudge();
|
||||
} else if (!video.paused && video.playbackRate !== 1.0) {
|
||||
nextController.startSubtitleNudge();
|
||||
}
|
||||
}
|
||||
|
||||
if (preservedState.userHidden && nextController.div) {
|
||||
nextController.div.classList.add("vsc-hidden");
|
||||
}
|
||||
}
|
||||
|
||||
log("Rebuilt controller: " + (reason || "refresh"), 4);
|
||||
return nextController;
|
||||
}
|
||||
|
||||
function refreshManagedController(video, parentHint) {
|
||||
if (!video || !video.vsc) return null;
|
||||
if (!video.isConnected) {
|
||||
removeController(video);
|
||||
return null;
|
||||
}
|
||||
|
||||
var controller = video.vsc;
|
||||
controller.parent = video.parentElement || parentHint || controller.parent;
|
||||
|
||||
var expectedMountParent = getControllerMountParent(video, controller.parent);
|
||||
var nextSignature = getControllerBehaviorSignature(video);
|
||||
var wrapper = controller.div;
|
||||
var needsRebuild =
|
||||
!wrapper ||
|
||||
!wrapper.isConnected ||
|
||||
!expectedMountParent ||
|
||||
wrapper.parentNode !== expectedMountParent ||
|
||||
controller.behaviorSignature !== nextSignature;
|
||||
|
||||
if (needsRebuild) {
|
||||
return rebuildControllerForVideo(
|
||||
video,
|
||||
controller.parent,
|
||||
"DOM/source/site-rule change"
|
||||
);
|
||||
}
|
||||
|
||||
controller.mountParent = expectedMountParent;
|
||||
controller.behaviorSignature = nextSignature;
|
||||
applyControllerLocation(controller, tc.settings.controllerLocation);
|
||||
var controllerEl = getControllerElement(controller);
|
||||
if (controllerEl) {
|
||||
controllerEl.style.opacity = String(tc.settings.controllerOpacity);
|
||||
}
|
||||
updateSubtitleNudgeIndicator(video);
|
||||
return controller;
|
||||
}
|
||||
|
||||
function captureSiteRuleBase() {
|
||||
tc.siteRuleBase = {
|
||||
startHidden: tc.settings.startHidden,
|
||||
@@ -615,7 +389,7 @@ function captureSiteRuleBase() {
|
||||
? tc.settings.controllerButtons.slice()
|
||||
: tc.settings.controllerButtons,
|
||||
keyBindings: Array.isArray(tc.settings.keyBindings)
|
||||
? tc.settings.keyBindings.map(function (binding) {
|
||||
? tc.settings.keyBindings.map(function(binding) {
|
||||
return Object.assign({}, binding);
|
||||
})
|
||||
: tc.settings.keyBindings
|
||||
@@ -641,7 +415,7 @@ function resetSettingsFromSiteRuleBase() {
|
||||
? base.controllerButtons.slice()
|
||||
: base.controllerButtons;
|
||||
tc.settings.keyBindings = Array.isArray(base.keyBindings)
|
||||
? base.keyBindings.map(function (binding) {
|
||||
? base.keyBindings.map(function(binding) {
|
||||
return Object.assign({}, binding);
|
||||
})
|
||||
: base.keyBindings;
|
||||
@@ -705,44 +479,41 @@ function cycleControllerLocation(video) {
|
||||
}
|
||||
|
||||
function normalizeBindingKey(key) {
|
||||
if (typeof key !== "string" || key.length === 0) return null;
|
||||
if (key === "Spacebar") return " ";
|
||||
if (key === "Esc") return "Escape";
|
||||
if (key.length === 1 && /[a-z]/i.test(key)) return key.toUpperCase();
|
||||
return key;
|
||||
return keyBindingUtils.normalizeBindingKey(key);
|
||||
}
|
||||
|
||||
function legacyKeyCodeToBinding(keyCode) {
|
||||
if (!Number.isInteger(keyCode)) return null;
|
||||
var key = keyCodeToEventKey[keyCode];
|
||||
if (!key && keyCode >= 48 && keyCode <= 57) {
|
||||
key = String.fromCharCode(keyCode);
|
||||
}
|
||||
if (!key && keyCode >= 65 && keyCode <= 90) {
|
||||
key = String.fromCharCode(keyCode);
|
||||
}
|
||||
function legacyBindingKeyToCode(key) {
|
||||
return keyBindingUtils.legacyBindingKeyToCode(key);
|
||||
}
|
||||
|
||||
function legacyKeyCodeToCode(keyCode) {
|
||||
return keyBindingUtils.legacyKeyCodeToCode(keyCode);
|
||||
}
|
||||
|
||||
function inferBindingCode(binding, fallbackCode) {
|
||||
return keyBindingUtils.inferBindingCode(binding, fallbackCode);
|
||||
}
|
||||
|
||||
function normalizeStoredBinding(binding, fallbackCode) {
|
||||
if (!binding) {
|
||||
if (!fallbackCode) return null;
|
||||
return {
|
||||
key: normalizeBindingKey(key),
|
||||
keyCode: keyCode,
|
||||
code: null,
|
||||
disabled: false
|
||||
code: fallbackCode,
|
||||
disabled: false,
|
||||
value: 0,
|
||||
force: "false",
|
||||
predefined: false
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeStoredBinding(binding, fallbackKeyCode) {
|
||||
var fallbackBinding = legacyKeyCodeToBinding(fallbackKeyCode);
|
||||
if (!binding) return fallbackBinding;
|
||||
}
|
||||
|
||||
if (
|
||||
binding.disabled === true ||
|
||||
(binding.key === null &&
|
||||
binding.keyCode === null &&
|
||||
binding.code === null)
|
||||
(binding.code === null &&
|
||||
binding.key === null &&
|
||||
binding.keyCode === null)
|
||||
) {
|
||||
return {
|
||||
action: binding.action,
|
||||
key: null,
|
||||
keyCode: null,
|
||||
code: null,
|
||||
disabled: true,
|
||||
value: Number(binding.value),
|
||||
@@ -751,46 +522,20 @@ function normalizeStoredBinding(binding, fallbackKeyCode) {
|
||||
};
|
||||
}
|
||||
|
||||
var normalizedCode = inferBindingCode(binding, fallbackCode);
|
||||
if (!normalizedCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalized = {
|
||||
action: binding.action,
|
||||
key: null,
|
||||
keyCode: null,
|
||||
code:
|
||||
typeof binding.code === "string" && binding.code.length > 0
|
||||
? binding.code
|
||||
: null,
|
||||
code: normalizedCode,
|
||||
disabled: false,
|
||||
value: Number(binding.value),
|
||||
force: String(binding.force) === "true" ? "true" : "false",
|
||||
predefined: Boolean(binding.predefined)
|
||||
};
|
||||
|
||||
if (typeof binding.key === "string") {
|
||||
normalized.key = normalizeBindingKey(binding.key);
|
||||
}
|
||||
|
||||
var legacyKeyCode = getLegacyKeyCode(binding);
|
||||
if (Number.isInteger(legacyKeyCode)) {
|
||||
var legacyBinding = legacyKeyCodeToBinding(legacyKeyCode);
|
||||
if (legacyBinding) {
|
||||
normalized.key = normalized.key || legacyBinding.key;
|
||||
normalized.keyCode = legacyKeyCode;
|
||||
}
|
||||
}
|
||||
|
||||
if (Number.isInteger(binding.keyCode)) {
|
||||
normalized.keyCode = binding.keyCode;
|
||||
}
|
||||
|
||||
if (!normalized.key && fallbackBinding) {
|
||||
normalized.key = fallbackBinding.key;
|
||||
if (normalized.keyCode === null) normalized.keyCode = fallbackBinding.keyCode;
|
||||
}
|
||||
|
||||
if (!normalized.key && !normalized.code && normalized.keyCode === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
@@ -907,19 +652,32 @@ function isSubtitleNudgeSupported(video) {
|
||||
return Boolean(video);
|
||||
}
|
||||
|
||||
function isSubtitleNudgeAvailableForVideo(video) {
|
||||
return isSubtitleNudgeSupported(video) && Boolean(tc.settings.enableSubtitleNudge);
|
||||
}
|
||||
|
||||
function isSubtitleNudgeEnabledForVideo(video) {
|
||||
if (!video || !video.vsc) return tc.settings.enableSubtitleNudge;
|
||||
if (!isSubtitleNudgeAvailableForVideo(video)) return false;
|
||||
|
||||
if (!video || !video.vsc) return true;
|
||||
|
||||
if (typeof video.vsc.subtitleNudgeEnabledOverride === "boolean") {
|
||||
return video.vsc.subtitleNudgeEnabledOverride;
|
||||
}
|
||||
|
||||
return tc.settings.enableSubtitleNudge;
|
||||
return true;
|
||||
}
|
||||
|
||||
function setSubtitleNudgeEnabledForVideo(video, enabled) {
|
||||
if (!video || !video.vsc) return false;
|
||||
|
||||
if (!isSubtitleNudgeAvailableForVideo(video)) {
|
||||
video.vsc.subtitleNudgeEnabledOverride = null;
|
||||
video.vsc.stopSubtitleNudge();
|
||||
updateSubtitleNudgeIndicator(video);
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalizedEnabled = Boolean(enabled);
|
||||
video.vsc.subtitleNudgeEnabledOverride = normalizedEnabled;
|
||||
|
||||
@@ -936,7 +694,7 @@ function setSubtitleNudgeEnabledForVideo(video, enabled) {
|
||||
if (flashEl) {
|
||||
flashEl.classList.add("visible");
|
||||
clearTimeout(flashEl._flashTimer);
|
||||
flashEl._flashTimer = setTimeout(function () {
|
||||
flashEl._flashTimer = setTimeout(function() {
|
||||
flashEl.classList.remove("visible");
|
||||
}, 1500);
|
||||
}
|
||||
@@ -983,14 +741,19 @@ function renderSubtitleNudgeIndicatorContent(target, isEnabled) {
|
||||
function updateSubtitleNudgeIndicator(video) {
|
||||
if (!video || !video.vsc) return;
|
||||
|
||||
var isAvailable = isSubtitleNudgeAvailableForVideo(video);
|
||||
var isEnabled = isSubtitleNudgeEnabledForVideo(video);
|
||||
var title = isEnabled ? "Subtitle nudge enabled" : "Subtitle nudge disabled";
|
||||
var title = !isAvailable
|
||||
? "Subtitle nudge unavailable on this site"
|
||||
: isEnabled
|
||||
? "Subtitle nudge enabled"
|
||||
: "Subtitle nudge disabled";
|
||||
|
||||
var indicator = video.vsc.subtitleNudgeIndicator;
|
||||
if (indicator) {
|
||||
renderSubtitleNudgeIndicatorContent(indicator, isEnabled);
|
||||
indicator.dataset.enabled = isEnabled ? "true" : "false";
|
||||
indicator.dataset.supported = "true";
|
||||
indicator.dataset.supported = isAvailable ? "true" : "false";
|
||||
indicator.title = title;
|
||||
indicator.setAttribute("aria-label", title);
|
||||
}
|
||||
@@ -999,7 +762,7 @@ function updateSubtitleNudgeIndicator(video) {
|
||||
if (flashEl) {
|
||||
renderSubtitleNudgeIndicatorContent(flashEl, isEnabled);
|
||||
flashEl.dataset.enabled = isEnabled ? "true" : "false";
|
||||
flashEl.dataset.supported = "true";
|
||||
flashEl.dataset.supported = isAvailable ? "true" : "false";
|
||||
flashEl.setAttribute("aria-label", title);
|
||||
}
|
||||
}
|
||||
@@ -1010,7 +773,7 @@ function schedulePersistLastSpeed(speed) {
|
||||
tc.pendingLastSpeedValue = speed;
|
||||
if (tc.pendingLastSpeedSave !== null) return;
|
||||
|
||||
tc.pendingLastSpeedSave = setTimeout(function () {
|
||||
tc.pendingLastSpeedSave = setTimeout(function() {
|
||||
var speedToPersist = tc.pendingLastSpeedValue;
|
||||
tc.pendingLastSpeedSave = null;
|
||||
|
||||
@@ -1018,7 +781,8 @@ function schedulePersistLastSpeed(speed) {
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.sync.set({ lastSpeed: speedToPersist }, function () { });
|
||||
chrome.storage.sync.set({ lastSpeed: speedToPersist }, function() {
|
||||
});
|
||||
tc.persistedLastSpeed = speedToPersist;
|
||||
}, 250);
|
||||
}
|
||||
@@ -1078,8 +842,8 @@ function applySourceTransitionPolicy(video, forceUpdate) {
|
||||
setSpeed(video, desiredSpeed, false, false);
|
||||
}
|
||||
|
||||
// Same-tab SPA or DOM-driven player swaps can change the effective rule output
|
||||
// after the media source updates, so refresh or rebuild controllers here too.
|
||||
// Same-tab SPA (e.g. YouTube watch → Shorts): URL can change while remember-speed
|
||||
// already ran on src mutation — re-apply margins / location / opacity for new rules.
|
||||
reapplySiteRulesAndControllerGeometry();
|
||||
}
|
||||
|
||||
@@ -1100,7 +864,7 @@ function scheduleSpeedRestore(video, desiredSpeed, reason) {
|
||||
clearTimeout(video.vsc.restoreSpeedTimer);
|
||||
}
|
||||
|
||||
video.vsc.restoreSpeedTimer = setTimeout(function () {
|
||||
video.vsc.restoreSpeedTimer = setTimeout(function() {
|
||||
if (!video.vsc) return;
|
||||
|
||||
if (Math.abs(video.playbackRate - desiredSpeed) > 0.01) {
|
||||
@@ -1147,19 +911,13 @@ function takePendingRateChange(video, currentSpeed) {
|
||||
}
|
||||
|
||||
function matchesKeyBinding(binding, event) {
|
||||
if (!binding || binding.disabled) return false;
|
||||
|
||||
var normalizedEventKey = normalizeBindingKey(event.key);
|
||||
if (binding.key && normalizedEventKey) {
|
||||
return binding.key === normalizedEventKey;
|
||||
}
|
||||
|
||||
if (binding.code && event.code) {
|
||||
return binding.code === event.code;
|
||||
}
|
||||
|
||||
var legacyKeyCode = getLegacyKeyCode(binding);
|
||||
return Number.isInteger(legacyKeyCode) && legacyKeyCode === event.keyCode;
|
||||
return Boolean(
|
||||
binding &&
|
||||
binding.disabled !== true &&
|
||||
typeof binding.code === "string" &&
|
||||
binding.code.length > 0 &&
|
||||
binding.code === event.code
|
||||
);
|
||||
}
|
||||
|
||||
function mediaSelector() {
|
||||
@@ -1189,7 +947,7 @@ function hasUsableMediaSource(node) {
|
||||
}
|
||||
|
||||
if (node.querySelectorAll) {
|
||||
return Array.from(node.querySelectorAll("source[src]")).some(function (
|
||||
return Array.from(node.querySelectorAll("source[src]")).some(function(
|
||||
source
|
||||
) {
|
||||
var src = source.getAttribute("src");
|
||||
@@ -1201,14 +959,8 @@ function hasUsableMediaSource(node) {
|
||||
}
|
||||
|
||||
function ensureController(node, parent) {
|
||||
if (!isMediaElement(node)) return node && node.vsc;
|
||||
|
||||
if (!node.isConnected) {
|
||||
removeController(node);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!node.vsc && !hasUsableMediaSource(node)) {
|
||||
if (!isMediaElement(node) || node.vsc) return node && node.vsc;
|
||||
if (!hasUsableMediaSource(node)) {
|
||||
log(
|
||||
`Deferring controller creation for ${node.tagName}: no usable source yet`,
|
||||
5
|
||||
@@ -1219,13 +971,9 @@ function ensureController(node, parent) {
|
||||
// href selects site rules; re-run on every new/usable media so margins/opacity match current URL.
|
||||
var siteDisabled = applySiteRuleOverrides();
|
||||
if (!tc.settings.enabled || siteDisabled) {
|
||||
removeController(node);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (node.vsc) {
|
||||
return refreshManagedController(node, parent);
|
||||
}
|
||||
refreshAllControllerGeometry();
|
||||
|
||||
log(
|
||||
`Creating controller for ${node.tagName}: ${node.src || node.currentSrc || "no src"}`,
|
||||
@@ -1338,7 +1086,7 @@ function patchAttachShadow() {
|
||||
}
|
||||
|
||||
var originalAttachShadow = Element.prototype.attachShadow;
|
||||
Element.prototype.attachShadow = function () {
|
||||
Element.prototype.attachShadow = function() {
|
||||
var shadowRoot = originalAttachShadow.apply(this, arguments);
|
||||
try {
|
||||
if (shadowRoot) {
|
||||
@@ -1369,8 +1117,7 @@ function log(message, level) {
|
||||
}
|
||||
}
|
||||
|
||||
chrome.storage.sync.get(null, function (storage) {
|
||||
storage = vscExpandStoredSettings(storage);
|
||||
chrome.storage.sync.get(tc.settings, function(storage) {
|
||||
var storedBindings = Array.isArray(storage.keyBindings)
|
||||
? storage.keyBindings
|
||||
: [];
|
||||
@@ -1381,6 +1128,18 @@ chrome.storage.sync.get(null, function (storage) {
|
||||
|
||||
if (tc.settings.keyBindings.length === 0) {
|
||||
tc.settings.keyBindings = defaultKeyBindings(storage);
|
||||
tc.settings.version = "0.5.3";
|
||||
chrome.storage.sync.set({
|
||||
keyBindings: tc.settings.keyBindings,
|
||||
version: tc.settings.version,
|
||||
rememberSpeed: tc.settings.rememberSpeed,
|
||||
forceLastSavedSpeed: tc.settings.forceLastSavedSpeed,
|
||||
audioBoolean: tc.settings.audioBoolean,
|
||||
startHidden: tc.settings.startHidden,
|
||||
enabled: tc.settings.enabled,
|
||||
controllerLocation: tc.settings.controllerLocation,
|
||||
controllerOpacity: tc.settings.controllerOpacity
|
||||
});
|
||||
}
|
||||
tc.settings.lastSpeed = Number(storage.lastSpeed);
|
||||
if (!isValidSpeed(tc.settings.lastSpeed) && tc.settings.lastSpeed !== 1.0) {
|
||||
@@ -1392,7 +1151,6 @@ chrome.storage.sync.get(null, function (storage) {
|
||||
tc.settings.lastSpeed = 1.0;
|
||||
}
|
||||
tc.persistedLastSpeed = tc.settings.lastSpeed;
|
||||
tc.settings.displayKeyCode = Number(storage.displayKeyCode);
|
||||
tc.settings.rememberSpeed = Boolean(storage.rememberSpeed);
|
||||
tc.settings.forceLastSavedSpeed = Boolean(storage.forceLastSavedSpeed);
|
||||
tc.settings.audioBoolean = Boolean(storage.audioBoolean);
|
||||
@@ -1447,25 +1205,17 @@ chrome.storage.sync.get(null, function (storage) {
|
||||
addedDefaultBinding =
|
||||
ensureDefaultKeyBinding(
|
||||
"display",
|
||||
"V",
|
||||
Number(storage.displayKeyCode) || 86,
|
||||
"KeyV",
|
||||
0
|
||||
) || addedDefaultBinding;
|
||||
addedDefaultBinding =
|
||||
ensureDefaultKeyBinding("move", "P", 80, 0) || addedDefaultBinding;
|
||||
ensureDefaultKeyBinding("move", "KeyP", 0) || addedDefaultBinding;
|
||||
addedDefaultBinding =
|
||||
ensureDefaultKeyBinding("toggleSubtitleNudge", "N", 78, 0) ||
|
||||
ensureDefaultKeyBinding("toggleSubtitleNudge", "KeyN", 0) ||
|
||||
addedDefaultBinding;
|
||||
|
||||
if (addedDefaultBinding) {
|
||||
var keyBindingsDiff = vscBuildStoredSettingsDiff({
|
||||
keyBindings: tc.settings.keyBindings
|
||||
});
|
||||
if (Object.prototype.hasOwnProperty.call(keyBindingsDiff, "keyBindings")) {
|
||||
chrome.storage.sync.set({ keyBindings: keyBindingsDiff.keyBindings });
|
||||
} else {
|
||||
chrome.storage.sync.remove("keyBindings");
|
||||
}
|
||||
chrome.storage.sync.set({ keyBindings: tc.settings.keyBindings });
|
||||
}
|
||||
captureSiteRuleBase();
|
||||
patchAttachShadow();
|
||||
@@ -1473,7 +1223,7 @@ chrome.storage.sync.get(null, function (storage) {
|
||||
// We use a global flag to ensure the listener is only attached once.
|
||||
if (!window.vscMessageListener) {
|
||||
chrome.runtime.onMessage.addListener(
|
||||
function (request, sender, sendResponse) {
|
||||
function(request, sender, sendResponse) {
|
||||
if (request.action === "rescan_page") {
|
||||
log("Re-scan command received from popup.", 4);
|
||||
initializeWhenReady(document, true);
|
||||
@@ -1514,7 +1264,7 @@ chrome.storage.sync.get(null, function (storage) {
|
||||
// Set the flag to prevent adding the listener again.
|
||||
window.vscMessageListener = true;
|
||||
}
|
||||
chrome.storage.local.get(["customButtonIcons"], function (loc) {
|
||||
chrome.storage.local.get(["customButtonIcons"], function(loc) {
|
||||
tc.settings.customButtonIcons =
|
||||
loc &&
|
||||
loc.customButtonIcons &&
|
||||
@@ -1524,18 +1274,18 @@ chrome.storage.sync.get(null, function (storage) {
|
||||
|
||||
if (!window.vscCustomIconListener) {
|
||||
window.vscCustomIconListener = true;
|
||||
chrome.storage.onChanged.addListener(function (changes, area) {
|
||||
chrome.storage.onChanged.addListener(function(changes, area) {
|
||||
if (area !== "local" || !changes.customButtonIcons) return;
|
||||
var nv = changes.customButtonIcons.newValue;
|
||||
tc.settings.customButtonIcons =
|
||||
nv && typeof nv === "object" ? nv : {};
|
||||
if (tc.mediaElements && tc.mediaElements.length) {
|
||||
tc.mediaElements.forEach(function (video) {
|
||||
tc.mediaElements.forEach(function(video) {
|
||||
if (!video.vsc || !video.vsc.div) return;
|
||||
var doc = video.ownerDocument;
|
||||
var shadow = video.vsc.div.shadowRoot;
|
||||
if (!shadow) return;
|
||||
shadow.querySelectorAll("button[data-action]").forEach(function (btn) {
|
||||
shadow.querySelectorAll("button[data-action]").forEach(function(btn) {
|
||||
var act = btn.dataset.action;
|
||||
if (!act) return;
|
||||
var svg =
|
||||
@@ -1580,6 +1330,7 @@ function getKeyBindings(action, what = "value") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function setKeyBindings(action, value) {
|
||||
tc.settings.keyBindings.find((item) => item.action === action)["value"] =
|
||||
value;
|
||||
@@ -1616,7 +1367,7 @@ function createControllerButton(doc, action, label, className) {
|
||||
}
|
||||
|
||||
function defineVideoController() {
|
||||
tc.videoController = function (target, parent) {
|
||||
tc.videoController = function(target, parent) {
|
||||
if (target.vsc) return target.vsc;
|
||||
tc.mediaElements.push(target);
|
||||
target.vsc = this;
|
||||
@@ -1636,7 +1387,7 @@ function defineVideoController() {
|
||||
tc.settings.controllerLocation
|
||||
);
|
||||
|
||||
log(`Creating video controller for ${target.tagName} with src: ${target.src || target.currentSrc || 'none'}`, 4);
|
||||
log(`Creating video controller for ${target.tagName} with src: ${target.src || target.currentSrc || "none"}`, 4);
|
||||
|
||||
let storedSpeed = sanitizeSpeed(resolveTargetSpeed(target), 1.0);
|
||||
this.targetSpeed = storedSpeed;
|
||||
@@ -1656,16 +1407,9 @@ function defineVideoController() {
|
||||
return;
|
||||
}
|
||||
|
||||
this.mountParent =
|
||||
this.div.parentNode || getControllerMountParent(target, this.parent);
|
||||
this.behaviorSignature = getControllerBehaviorSignature(target);
|
||||
log(`Controller created and attached to DOM. Hidden: ${this.div.classList.contains("vsc-hidden")}`, 4);
|
||||
|
||||
log(
|
||||
`Controller created and attached to DOM. Hidden: ${this.div.classList.contains('vsc-hidden')}`,
|
||||
4
|
||||
);
|
||||
|
||||
var mediaEventAction = function (event) {
|
||||
var mediaEventAction = function(event) {
|
||||
if (
|
||||
event.type === "loadedmetadata" ||
|
||||
event.type === "loadeddata" ||
|
||||
@@ -1783,7 +1527,7 @@ function defineVideoController() {
|
||||
this.startSubtitleNudge();
|
||||
};
|
||||
|
||||
tc.videoController.prototype.remove = function () {
|
||||
tc.videoController.prototype.remove = function() {
|
||||
this.stopSubtitleNudge();
|
||||
if (this.youTubeAutoHideObserver) {
|
||||
this.youTubeAutoHideObserver.disconnect();
|
||||
@@ -1815,7 +1559,7 @@ function defineVideoController() {
|
||||
if (idx != -1) tc.mediaElements.splice(idx, 1);
|
||||
};
|
||||
|
||||
tc.videoController.prototype.startSubtitleNudge = function () {
|
||||
tc.videoController.prototype.startSubtitleNudge = function() {
|
||||
if (
|
||||
!isSubtitleNudgeSupported(this.video) ||
|
||||
!isSubtitleNudgeEnabledForVideo(this.video) ||
|
||||
@@ -1888,7 +1632,7 @@ function defineVideoController() {
|
||||
log(`Nudge: Starting with interval ${tc.settings.subtitleNudgeInterval}ms.`, 5);
|
||||
};
|
||||
|
||||
tc.videoController.prototype.stopSubtitleNudge = function () {
|
||||
tc.videoController.prototype.stopSubtitleNudge = function() {
|
||||
if (this.nudgeAnimationId !== null) {
|
||||
clearTimeout(this.nudgeAnimationId);
|
||||
this.nudgeAnimationId = null;
|
||||
@@ -1906,7 +1650,7 @@ function defineVideoController() {
|
||||
// doesn't lose the user's intended speed if the site hijacks it.
|
||||
};
|
||||
|
||||
tc.videoController.prototype.performImmediateNudge = function () {
|
||||
tc.videoController.prototype.performImmediateNudge = function() {
|
||||
if (
|
||||
!isSubtitleNudgeSupported(this.video) ||
|
||||
!isSubtitleNudgeEnabledForVideo(this.video) ||
|
||||
@@ -1936,11 +1680,11 @@ function defineVideoController() {
|
||||
log(`Immediate nudge performed at rate ${targetRate.toFixed(2)}`, 5);
|
||||
};
|
||||
|
||||
tc.videoController.prototype.setupYouTubeAutoHide = function (wrapper) {
|
||||
if (!wrapper) return;
|
||||
tc.videoController.prototype.setupYouTubeAutoHide = function(wrapper) {
|
||||
if (!wrapper || !isOnYouTube()) return;
|
||||
|
||||
const video = this.video;
|
||||
const ytPlayer = getYouTubeAutoHidePlayer(video);
|
||||
const ytPlayer = video.closest(".html5-video-player");
|
||||
if (!ytPlayer) {
|
||||
log("YouTube player not found for auto-hide setup", 4);
|
||||
return;
|
||||
@@ -2012,7 +1756,7 @@ function defineVideoController() {
|
||||
};
|
||||
};
|
||||
|
||||
tc.videoController.prototype.setupGenericAutoHide = function (wrapper) {
|
||||
tc.videoController.prototype.setupGenericAutoHide = function(wrapper) {
|
||||
if (!wrapper) return;
|
||||
|
||||
const video = this.video;
|
||||
@@ -2071,7 +1815,7 @@ function defineVideoController() {
|
||||
log(`Generic auto-hide setup complete with ${tc.settings.hideWithControlsTimer}s timer`, 4);
|
||||
};
|
||||
|
||||
tc.videoController.prototype.initializeControls = function () {
|
||||
tc.videoController.prototype.initializeControls = function() {
|
||||
const doc = this.video.ownerDocument;
|
||||
const speed = this.video.playbackRate.toFixed(2);
|
||||
var wrapper = doc.createElement("div");
|
||||
@@ -2080,8 +1824,7 @@ function defineVideoController() {
|
||||
wrapper.classList.add("vsc-nosource");
|
||||
if (tc.settings.startHidden) wrapper.classList.add("vsc-hidden");
|
||||
// Use lower z-index for non-YouTube sites to avoid overlapping modals
|
||||
if (!getYouTubeAutoHidePlayer(this.video))
|
||||
wrapper.classList.add("vsc-non-youtube");
|
||||
if (!isOnYouTube()) wrapper.classList.add("vsc-non-youtube");
|
||||
var shadow = wrapper.attachShadow({ mode: "open" });
|
||||
var shadowStylesheet = doc.createElement("link");
|
||||
shadowStylesheet.rel = "stylesheet";
|
||||
@@ -2110,7 +1853,7 @@ function defineVideoController() {
|
||||
|
||||
var subtitleNudgeIndicator = null;
|
||||
|
||||
buttonConfig.forEach(function (btnId) {
|
||||
buttonConfig.forEach(function(btnId) {
|
||||
if (btnId === "nudge") {
|
||||
subtitleNudgeIndicator = doc.createElement("span");
|
||||
subtitleNudgeIndicator.id = "nudge-indicator";
|
||||
@@ -2142,20 +1885,22 @@ function defineVideoController() {
|
||||
this.subtitleNudgeIndicator = subtitleNudgeIndicator;
|
||||
this.nudgeFlashIndicator = nudgeFlashIndicator;
|
||||
this.resetButtonEl =
|
||||
shadow.querySelector('button[data-action="reset"]') || null;
|
||||
shadow.querySelector("button[data-action=\"reset\"]") || null;
|
||||
this.resetToggleArmed = false;
|
||||
if (subtitleNudgeIndicator) {
|
||||
updateSubtitleNudgeIndicator(this.video);
|
||||
}
|
||||
|
||||
function blurAfterPointerTap(target, e) {
|
||||
if (!target || typeof target.blur !== "function") return;
|
||||
var pt = e.pointerType;
|
||||
if (pt === "mouse" || pt === "touch" || (!pt && e.detail > 0)) {
|
||||
requestAnimationFrame(function () {
|
||||
requestAnimationFrame(function() {
|
||||
target.blur();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
dragHandle.addEventListener(
|
||||
"mousedown",
|
||||
(e) => {
|
||||
@@ -2197,7 +1942,7 @@ function defineVideoController() {
|
||||
|
||||
// Setup auto-hide observers if enabled
|
||||
if (tc.settings.hideWithControls) {
|
||||
if (getAutoHideModeForVideo(this.video) === "youtube") {
|
||||
if (isOnYouTube()) {
|
||||
this.setupYouTubeAutoHide(wrapper);
|
||||
} else {
|
||||
this.setupGenericAutoHide(wrapper);
|
||||
@@ -2271,22 +2016,21 @@ function defineVideoController() {
|
||||
|
||||
function applySiteRuleOverrides() {
|
||||
resetSettingsFromSiteRuleBase();
|
||||
tc.activeSiteRule = null;
|
||||
|
||||
if (!Array.isArray(tc.settings.siteRules) || tc.settings.siteRules.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentUrl = location.href;
|
||||
var matchedRule = vscMatchSiteRule(currentUrl, tc.settings.siteRules);
|
||||
var matchedRule = siteRuleUtils.matchSiteRule(currentUrl, tc.settings.siteRules);
|
||||
|
||||
if (!matchedRule) return false;
|
||||
|
||||
tc.activeSiteRule = matchedRule;
|
||||
log("Matched site rule overrides for current URL", 4);
|
||||
log(`Matched site rule: ${matchedRule.pattern}`, 4);
|
||||
|
||||
// Check if extension should be enabled/disabled on this site
|
||||
if (vscIsSiteRuleDisabled(matchedRule)) {
|
||||
if (siteRuleUtils.isSiteRuleDisabled(matchedRule)) {
|
||||
log(`Extension disabled for site: ${currentUrl}`, 4);
|
||||
return true;
|
||||
}
|
||||
@@ -2317,7 +2061,7 @@ function applySiteRuleOverrides() {
|
||||
[
|
||||
"controllerMarginTop",
|
||||
"controllerMarginBottom"
|
||||
].forEach(function (key) {
|
||||
].forEach(function(key) {
|
||||
tc.settings[key] = normalizeControllerMarginPx(tc.settings[key], 0);
|
||||
});
|
||||
|
||||
@@ -2346,22 +2090,23 @@ function applySiteRuleOverrides() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Re-match site rules for current URL and refresh or rebuild every controller. */
|
||||
/** Apply current tc.settings controller layout/opacity to every attached controller (after site rules). */
|
||||
function refreshAllControllerGeometry() {
|
||||
tc.mediaElements.forEach(function(video) {
|
||||
if (!video || !video.vsc) return;
|
||||
applyControllerLocation(video.vsc, tc.settings.controllerLocation);
|
||||
var controllerEl = getControllerElement(video.vsc);
|
||||
if (controllerEl) {
|
||||
controllerEl.style.opacity = String(tc.settings.controllerOpacity);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Re-match site rules for current URL and refresh controller position/opacity on every video. */
|
||||
function reapplySiteRulesAndControllerGeometry() {
|
||||
var siteDisabled = applySiteRuleOverrides();
|
||||
var videos = tc.mediaElements.slice();
|
||||
|
||||
if (!tc.settings.enabled || siteDisabled) {
|
||||
videos.forEach(function (video) {
|
||||
removeController(video);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
videos.forEach(function (video) {
|
||||
if (!video) return;
|
||||
ensureController(video, video.parentElement || video.parentNode);
|
||||
});
|
||||
if (!tc.settings.enabled || siteDisabled) return;
|
||||
refreshAllControllerGeometry();
|
||||
}
|
||||
|
||||
function shouldPreserveDesiredSpeed(video, speed) {
|
||||
@@ -2381,6 +2126,7 @@ function shouldPreserveDesiredSpeed(video, speed) {
|
||||
function setupListener(root) {
|
||||
root = root || document;
|
||||
if (root.vscRateListenerAttached) return;
|
||||
|
||||
function updateSpeedFromEvent(video, skipResetDisarm) {
|
||||
if (!video.vsc || !video.vsc.speedIndicator) return;
|
||||
if (!skipResetDisarm) {
|
||||
@@ -2401,9 +2147,10 @@ function setupListener(root) {
|
||||
else video.vsc.startSubtitleNudge();
|
||||
}
|
||||
}
|
||||
|
||||
root.addEventListener(
|
||||
"ratechange",
|
||||
function (event) {
|
||||
function(event) {
|
||||
if (tc.isNudging) return;
|
||||
var video = event.target;
|
||||
if (!video || typeof video.playbackRate === "undefined" || !video.vsc)
|
||||
@@ -2447,6 +2194,7 @@ function setupListener(root) {
|
||||
}
|
||||
|
||||
var vscInitializedDocuments = new Set();
|
||||
|
||||
function clearPendingInitialization(doc) {
|
||||
if (!doc || !doc.vscPendingInitializeHandler) return;
|
||||
|
||||
@@ -2483,7 +2231,7 @@ function initializeWhenReady(doc, forceReinit = false) {
|
||||
|
||||
if (doc.vscPendingInitializeHandler) return;
|
||||
|
||||
var pendingInitializeHandler = function () {
|
||||
var pendingInitializeHandler = function() {
|
||||
tryInitializeDocument(doc, doc.vscPendingForceReinit === true);
|
||||
};
|
||||
|
||||
@@ -2498,6 +2246,7 @@ function initializeWhenReady(doc, forceReinit = false) {
|
||||
setTimeout(pendingInitializeHandler, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function inIframe() {
|
||||
try {
|
||||
return window.self !== window.top;
|
||||
@@ -2510,13 +2259,14 @@ function attachKeydownListeners(doc) {
|
||||
var docs = [doc];
|
||||
try {
|
||||
if (inIframe() && window.top.document !== doc) docs.push(window.top.document);
|
||||
} catch (e) { }
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
docs.forEach(function (keyDoc) {
|
||||
docs.forEach(function(keyDoc) {
|
||||
if (keyDoc.vscKeydownListenerAttached) return;
|
||||
keyDoc.addEventListener(
|
||||
"keydown",
|
||||
function (event) {
|
||||
function(event) {
|
||||
if (
|
||||
!event.getModifierState ||
|
||||
event.getModifierState("Alt") ||
|
||||
@@ -2539,7 +2289,7 @@ function attachKeydownListeners(doc) {
|
||||
|
||||
if (!tc.mediaElements.length) return;
|
||||
|
||||
var item = tc.settings.keyBindings.find(function (binding) {
|
||||
var item = tc.settings.keyBindings.find(function(binding) {
|
||||
return matchesKeyBinding(binding, event);
|
||||
});
|
||||
|
||||
@@ -2563,24 +2313,24 @@ function attachMutationObserver(root) {
|
||||
|
||||
var pendingMutations = [];
|
||||
var mutationProcessingScheduled = false;
|
||||
var observer = new MutationObserver(function (mutations) {
|
||||
var observer = new MutationObserver(function(mutations) {
|
||||
pendingMutations.push(...mutations);
|
||||
if (mutationProcessingScheduled) return;
|
||||
|
||||
mutationProcessingScheduled = true;
|
||||
requestIdle(
|
||||
function () {
|
||||
function() {
|
||||
var mutationsToProcess = pendingMutations.splice(0);
|
||||
mutationProcessingScheduled = false;
|
||||
|
||||
mutationsToProcess.forEach(function (mutation) {
|
||||
mutationsToProcess.forEach(function(mutation) {
|
||||
if (mutation.type === "childList") {
|
||||
mutation.addedNodes.forEach(function (node) {
|
||||
mutation.addedNodes.forEach(function(node) {
|
||||
// Skip text nodes, comments, etc. — only elements can contain media
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
scanNodeForMedia(node, node.parentNode || mutation.target, true);
|
||||
});
|
||||
mutation.removedNodes.forEach(function (node) {
|
||||
mutation.removedNodes.forEach(function(node) {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
scanNodeForMedia(node, node.parentNode || mutation.target, false);
|
||||
});
|
||||
@@ -2629,7 +2379,7 @@ function attachMutationObserver(root) {
|
||||
function attachMediaDetectionListeners(root) {
|
||||
if (root.vscMediaEventListenersAttached) return;
|
||||
|
||||
var handleDetectedMedia = function (event) {
|
||||
var handleDetectedMedia = function(event) {
|
||||
var target = event.target;
|
||||
if (!isMediaElement(target)) return;
|
||||
ensureController(target, target.parentElement || target.parentNode);
|
||||
@@ -2642,21 +2392,22 @@ function attachMediaDetectionListeners(root) {
|
||||
"canplay",
|
||||
"playing",
|
||||
"play"
|
||||
].forEach(function (eventName) {
|
||||
].forEach(function(eventName) {
|
||||
root.addEventListener(eventName, handleDetectedMedia, true);
|
||||
});
|
||||
root.vscMediaEventListenersAttached = true;
|
||||
}
|
||||
|
||||
function attachIframeListeners(doc) {
|
||||
Array.from(doc.getElementsByTagName("iframe")).forEach(function (frame) {
|
||||
Array.from(doc.getElementsByTagName("iframe")).forEach(function(frame) {
|
||||
if (!frame.vscLoadListenerAttached) {
|
||||
frame.addEventListener("load", function () {
|
||||
frame.addEventListener("load", function() {
|
||||
try {
|
||||
if (frame.contentDocument) {
|
||||
initializeWhenReady(frame.contentDocument, true);
|
||||
}
|
||||
} catch (e) { }
|
||||
} catch (e) {
|
||||
}
|
||||
});
|
||||
frame.vscLoadListenerAttached = true;
|
||||
}
|
||||
@@ -2665,24 +2416,25 @@ function attachIframeListeners(doc) {
|
||||
if (frame.contentDocument) {
|
||||
initializeWhenReady(frame.contentDocument);
|
||||
}
|
||||
} catch (e) { }
|
||||
} catch (e) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function attachNavigationListeners() {
|
||||
if (window.vscNavigationListenersAttached) return;
|
||||
|
||||
var scheduleRescan = function () {
|
||||
var scheduleRescan = function() {
|
||||
clearTimeout(window.vscNavigationRescanTimer);
|
||||
window.vscNavigationRescanTimer = setTimeout(function () {
|
||||
window.vscNavigationRescanTimer = setTimeout(function() {
|
||||
initializeWhenReady(document, true);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
["pushState", "replaceState"].forEach(function (method) {
|
||||
["pushState", "replaceState"].forEach(function(method) {
|
||||
if (typeof history[method] !== "function") return;
|
||||
var original = history[method];
|
||||
history[method] = function () {
|
||||
history[method] = function() {
|
||||
var result = original.apply(this, arguments);
|
||||
scheduleRescan();
|
||||
return result;
|
||||
@@ -2702,6 +2454,7 @@ function initializeNow(doc, forceReinit = false) {
|
||||
if ((!forceReinit && vscInitializedDocuments.has(doc)) || !doc.body) return;
|
||||
|
||||
var siteDisabled = applySiteRuleOverrides();
|
||||
if (!tc.settings.enabled || siteDisabled) return;
|
||||
|
||||
if (!doc.body.classList.contains("vsc-initialized")) {
|
||||
doc.body.classList.add("vsc-initialized");
|
||||
@@ -2713,9 +2466,7 @@ function initializeNow(doc, forceReinit = false) {
|
||||
|
||||
if (forceReinit) {
|
||||
log("Force re-initialization requested", 4);
|
||||
reapplySiteRulesAndControllerGeometry();
|
||||
} else if (!tc.settings.enabled || siteDisabled) {
|
||||
reapplySiteRulesAndControllerGeometry();
|
||||
refreshAllControllerGeometry();
|
||||
}
|
||||
|
||||
vscInitializedDocuments.add(doc);
|
||||
@@ -2830,7 +2581,7 @@ function runAction(action, value, e) {
|
||||
);
|
||||
}
|
||||
|
||||
mediaTagsToProcess.forEach(function (v) {
|
||||
mediaTagsToProcess.forEach(function(v) {
|
||||
if (!v.vsc) return; // Don't process videos without a controller
|
||||
var controller = v.vsc.div;
|
||||
const userDrivenActionsThatShowController = [
|
||||
@@ -2838,6 +2589,8 @@ function runAction(action, value, e) {
|
||||
"advance",
|
||||
"faster",
|
||||
"slower",
|
||||
"louder",
|
||||
"softer",
|
||||
"reset",
|
||||
"fast",
|
||||
"move",
|
||||
@@ -2850,7 +2603,14 @@ function runAction(action, value, e) {
|
||||
"toggleSubtitleNudge",
|
||||
"display"
|
||||
];
|
||||
if (userDrivenActionsThatShowController.includes(action) && action !== "display") {
|
||||
var subtitleNudgeActionBlocked =
|
||||
(action === "toggleSubtitleNudge" || action === "nudge") &&
|
||||
!isSubtitleNudgeAvailableForVideo(v);
|
||||
if (
|
||||
userDrivenActionsThatShowController.includes(action) &&
|
||||
action !== "display" &&
|
||||
!subtitleNudgeActionBlocked
|
||||
) {
|
||||
showController(controller, 2000, true);
|
||||
}
|
||||
if (v.classList.contains("vsc-cancelled")) return;
|
||||
@@ -2953,6 +2713,12 @@ function runAction(action, value, e) {
|
||||
case "muted":
|
||||
muted(v);
|
||||
break;
|
||||
case "louder":
|
||||
volumeUp(v, Number.isFinite(numValue) ? numValue : 0.1);
|
||||
break;
|
||||
case "softer":
|
||||
volumeDown(v, Number.isFinite(numValue) ? numValue : 0.1);
|
||||
break;
|
||||
case "mark":
|
||||
setMark(v);
|
||||
break;
|
||||
@@ -3017,17 +2783,62 @@ function resetSpeed(v, target, isFastKey = false) {
|
||||
}
|
||||
|
||||
function muted(v) {
|
||||
v.muted = !v.muted;
|
||||
var nextMuted = !v.muted;
|
||||
v.muted = nextMuted;
|
||||
if (!isOnYouTube()) return;
|
||||
var ytApi = getYouTubePlayerApi(v);
|
||||
if (!ytApi) return;
|
||||
if (nextMuted && typeof ytApi.mute === "function") ytApi.mute();
|
||||
if (!nextMuted && typeof ytApi.unMute === "function") ytApi.unMute();
|
||||
}
|
||||
|
||||
function getYouTubePlayerApi(video) {
|
||||
if (!isOnYouTube()) return null;
|
||||
var playerEl =
|
||||
(video && video.closest ? video.closest(".html5-video-player") : null) ||
|
||||
document.getElementById("movie_player") ||
|
||||
document.querySelector(".html5-video-player");
|
||||
if (!playerEl) return null;
|
||||
return playerEl.wrappedJSObject || playerEl;
|
||||
}
|
||||
|
||||
function syncYouTubePlayerVolume(video, volume) {
|
||||
var ytApi = getYouTubePlayerApi(video);
|
||||
if (!ytApi || typeof ytApi.setVolume !== "function") return;
|
||||
ytApi.setVolume(Math.round(volume * 100));
|
||||
if (volume > 0 && typeof ytApi.unMute === "function") {
|
||||
ytApi.unMute();
|
||||
}
|
||||
}
|
||||
|
||||
function setVideoVolume(video, targetVolume) {
|
||||
var nextVolume = Math.max(0, Math.min(1, Number(targetVolume.toFixed(2))));
|
||||
video.volume = nextVolume;
|
||||
if (nextVolume > 0 && video.muted) {
|
||||
video.muted = false;
|
||||
}
|
||||
syncYouTubePlayerVolume(video, nextVolume);
|
||||
}
|
||||
|
||||
function volumeUp(v, value) {
|
||||
setVideoVolume(v, v.volume + value);
|
||||
}
|
||||
|
||||
function volumeDown(v, value) {
|
||||
setVideoVolume(v, v.volume - value);
|
||||
}
|
||||
|
||||
function setMark(v) {
|
||||
v.vsc.mark = v.currentTime;
|
||||
}
|
||||
|
||||
function jumpToMark(v) {
|
||||
if (v.vsc && typeof v.vsc.mark === "number") {
|
||||
extendSpeedRestoreWindow(v);
|
||||
v.currentTime = v.vsc.mark;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDrag(video, e) {
|
||||
const c = video.vsc.div;
|
||||
const sC = convertControllerToManualPosition(video.vsc);
|
||||
@@ -3066,6 +2877,7 @@ function handleDrag(video, e) {
|
||||
pE.addEventListener("mouseleave", eD);
|
||||
pE.addEventListener("mousemove", sD);
|
||||
}
|
||||
|
||||
function showController(controller, duration = 2000, forced = false) {
|
||||
if (!controller || typeof controller.classList === "undefined") return;
|
||||
var restoreHidden =
|
||||
@@ -3083,7 +2895,7 @@ function showController(controller, duration = 2000, forced = false) {
|
||||
clearTimeout(controller.showTimeOut);
|
||||
}
|
||||
|
||||
controller.showTimeOut = setTimeout(function () {
|
||||
controller.showTimeOut = setTimeout(function() {
|
||||
controller.classList.remove("vsc-show");
|
||||
controller.classList.remove("vsc-forced-show");
|
||||
if (controller.restoreHiddenAfterShow === true) {
|
||||
|
||||
+4
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Speeder",
|
||||
"short_name": "Speeder",
|
||||
"version": "5.1.7",
|
||||
"version": "5.2.1",
|
||||
"manifest_version": 2,
|
||||
"description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts (New and improved version of \"Video Speed Controller\")",
|
||||
"homepage_url": "https://github.com/SoPat712/speeder",
|
||||
@@ -59,7 +59,9 @@
|
||||
"inject.css"
|
||||
],
|
||||
"js": [
|
||||
"settings-core.js",
|
||||
"shared/controller-utils.js",
|
||||
"shared/key-bindings.js",
|
||||
"shared/site-rules.js",
|
||||
"ui-icons.js",
|
||||
"inject.js"
|
||||
]
|
||||
|
||||
+119
-16
@@ -7,6 +7,17 @@
|
||||
--text: #17191c;
|
||||
--muted: #626b76;
|
||||
--accent: #111827;
|
||||
--switch-track-off: #c1cad6;
|
||||
--switch-track-off-border: #aeb8c5;
|
||||
--switch-track-on: #111827;
|
||||
--switch-track-on-border: #111827;
|
||||
--switch-thumb-off: #ffffff;
|
||||
--switch-thumb-on: #ffffff;
|
||||
--toggle-open-fg: #111827;
|
||||
--toggle-open-bg: #eef2f6;
|
||||
--toggle-open-border: #c5ccd5;
|
||||
--toggle-open-hover-bg: #e4eaf1;
|
||||
--toggle-open-hover-border: #b5c0cc;
|
||||
--danger: #b42318;
|
||||
}
|
||||
|
||||
@@ -210,6 +221,7 @@ button:active {
|
||||
}
|
||||
|
||||
button:focus-visible,
|
||||
input[type="checkbox"]:focus-visible,
|
||||
input[type="text"]:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
@@ -247,10 +259,49 @@ textarea:focus {
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 2px 0 0;
|
||||
accent-color: var(--accent);
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
position: relative;
|
||||
width: 46px;
|
||||
min-width: 46px;
|
||||
height: 28px;
|
||||
margin: 0;
|
||||
border: 1px solid var(--switch-track-off-border);
|
||||
border-radius: 999px;
|
||||
background: var(--switch-track-off);
|
||||
cursor: pointer;
|
||||
transition: background-color 120ms ease, border-color 120ms ease,
|
||||
box-shadow 120ms ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
input[type="checkbox"]::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: var(--switch-thumb-off);
|
||||
box-shadow: 0 1px 2px rgba(17, 24, 39, 0.18),
|
||||
inset 0 0 0 1px rgba(17, 24, 39, 0.08);
|
||||
transition: transform 120ms ease, background-color 120ms ease;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked {
|
||||
background: var(--switch-track-on);
|
||||
border-color: var(--switch-track-on-border);
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked::before {
|
||||
transform: translateX(18px);
|
||||
background: var(--switch-thumb-on);
|
||||
}
|
||||
|
||||
input[type="checkbox"]:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
label {
|
||||
@@ -322,6 +373,39 @@ label em {
|
||||
|
||||
.toggle-site-rule {
|
||||
font-weight: 400;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.toggle-site-rule:hover {
|
||||
color: var(--toggle-open-fg);
|
||||
background: var(--toggle-open-hover-bg);
|
||||
border-color: var(--toggle-open-hover-border);
|
||||
}
|
||||
|
||||
.toggle-site-rule .site-rule-toggle-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toggle-site-rule .site-rule-toggle-icon svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.site-rule:not(.collapsed) .toggle-site-rule {
|
||||
color: var(--toggle-open-fg);
|
||||
background: var(--toggle-open-bg);
|
||||
border-color: var(--toggle-open-border);
|
||||
}
|
||||
|
||||
.site-rule:not(.collapsed) .toggle-site-rule:hover {
|
||||
color: var(--toggle-open-fg);
|
||||
background: var(--toggle-open-hover-bg);
|
||||
border-color: var(--toggle-open-hover-border);
|
||||
}
|
||||
|
||||
.row {
|
||||
@@ -339,7 +423,7 @@ label em {
|
||||
}
|
||||
|
||||
.row.row-checkbox {
|
||||
grid-template-columns: minmax(0, 1fr) 24px;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.row.row-checkbox input[type="checkbox"] {
|
||||
@@ -403,9 +487,9 @@ label em {
|
||||
|
||||
.site-override-lead {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 24px;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
@@ -414,7 +498,6 @@ label em {
|
||||
|
||||
.site-override-lead input[type="checkbox"] {
|
||||
justify-self: end;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.site-override-lead span {
|
||||
@@ -427,10 +510,19 @@ label em {
|
||||
.site-rule-override-section .site-autohide-container,
|
||||
.site-rule-override-section .site-playback-container,
|
||||
.site-rule-override-section .site-opacity-container,
|
||||
.site-rule-override-section .site-subtitleNudge-container {
|
||||
.site-rule-override-section .site-subtitleNudge-container,
|
||||
.site-controlbar-container,
|
||||
.site-popup-controlbar-container,
|
||||
.site-shortcuts-container {
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.site-override-disabled {
|
||||
opacity: 0.48;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.cb-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -803,7 +895,7 @@ button.lucide-result-tile.lucide-picked {
|
||||
}
|
||||
|
||||
.site-rule-option-checkbox {
|
||||
grid-template-columns: minmax(0, 1fr) 24px;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.site-rule-option-checkbox > input[type="checkbox"] {
|
||||
@@ -833,7 +925,7 @@ button.lucide-result-tile.lucide-picked {
|
||||
|
||||
.site-rule-split-label {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 24px;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
@@ -845,7 +937,7 @@ button.lucide-result-tile.lucide-picked {
|
||||
|
||||
.site-rule-split-label input[type="checkbox"] {
|
||||
justify-self: end;
|
||||
margin-top: 3px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.site-rule-option-checkbox > .site-rule-split-label {
|
||||
@@ -889,8 +981,8 @@ button.lucide-result-tile.lucide-picked {
|
||||
|
||||
.force-label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: auto;
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
@@ -898,7 +990,7 @@ button.lucide-result-tile.lucide-picked {
|
||||
}
|
||||
|
||||
.force-label input {
|
||||
margin-top: 2px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
@@ -956,7 +1048,7 @@ button.lucide-result-tile.lucide-picked {
|
||||
}
|
||||
|
||||
.site-override-lead {
|
||||
grid-template-columns: minmax(0, 1fr) 24px;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.action-row button,
|
||||
@@ -1005,6 +1097,17 @@ button.lucide-result-tile.lucide-picked {
|
||||
--text: #f2f4f6;
|
||||
--muted: #a0a8b2;
|
||||
--accent: #f2f4f6;
|
||||
--switch-track-off: #374151;
|
||||
--switch-track-off-border: #4b5563;
|
||||
--switch-track-on: #aab7c6;
|
||||
--switch-track-on-border: #aab7c6;
|
||||
--switch-thumb-off: #f8fafc;
|
||||
--switch-thumb-on: #111315;
|
||||
--toggle-open-fg: #f2f4f6;
|
||||
--toggle-open-bg: #2b3138;
|
||||
--toggle-open-border: #4b5563;
|
||||
--toggle-open-hover-bg: #374151;
|
||||
--toggle-open-hover-border: #64748b;
|
||||
--danger: #ff8a80;
|
||||
}
|
||||
|
||||
|
||||
+24
-24
@@ -5,10 +5,13 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Speeder Settings</title>
|
||||
<link rel="stylesheet" href="options.css" />
|
||||
<script src="settings-core.js"></script>
|
||||
<script src="shared/controller-utils.js"></script>
|
||||
<script src="shared/key-bindings.js"></script>
|
||||
<script src="shared/popup-controls.js"></script>
|
||||
<script src="ui-icons.js"></script>
|
||||
<script src="lucide-client.js"></script>
|
||||
<script src="options.js"></script>
|
||||
<script src="shared/import-export.js"></script>
|
||||
<script src="importExport.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -480,7 +483,15 @@
|
||||
<template id="siteRuleTemplate">
|
||||
<div class="site-rule">
|
||||
<div class="site-rule-header">
|
||||
<button type="button" class="toggle-site-rule" title="Expand/Collapse">+</button>
|
||||
<button
|
||||
type="button"
|
||||
class="toggle-site-rule"
|
||||
title="Expand site rule"
|
||||
aria-label="Expand site rule"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="site-rule-toggle-icon" aria-hidden="true">…</span>
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
class="site-pattern"
|
||||
@@ -501,7 +512,7 @@
|
||||
<span>Override placement for this site</span>
|
||||
<input type="checkbox" class="override-placement" />
|
||||
</label>
|
||||
<div class="site-placement-container" style="display: none">
|
||||
<div class="site-placement-container">
|
||||
<div class="site-rule-option site-rule-option-field">
|
||||
<label>Default controller location:</label>
|
||||
<select class="site-controllerLocation">
|
||||
@@ -539,7 +550,7 @@
|
||||
<span>Override hide-by-default for this site</span>
|
||||
<input type="checkbox" class="override-visibility" />
|
||||
</label>
|
||||
<div class="site-visibility-container" style="display: none">
|
||||
<div class="site-visibility-container">
|
||||
<div class="site-rule-option site-rule-option-checkbox">
|
||||
<label>Hide controller by default:</label>
|
||||
<input type="checkbox" class="site-startHidden" />
|
||||
@@ -551,26 +562,15 @@
|
||||
<span>Override auto-hide for this site</span>
|
||||
<input type="checkbox" class="override-autohide" />
|
||||
</label>
|
||||
<div class="site-autohide-container" style="display: none">
|
||||
<div class="site-autohide-container">
|
||||
<div class="site-rule-option site-rule-option-checkbox">
|
||||
<label class="site-rule-split-label">
|
||||
<span
|
||||
>Hide with controls<br /><em
|
||||
>Fade the controller in and out with the video
|
||||
interface: perfect sync on YouTube, idle-based
|
||||
elsewhere.</em
|
||||
></span
|
||||
>
|
||||
<span>Hide with controls (idle-based)</span>
|
||||
<input type="checkbox" class="site-hideWithControls" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="site-rule-option site-rule-option-field">
|
||||
<label
|
||||
>Auto-hide timer (seconds)<br /><em
|
||||
>Seconds of inactivity before hiding: 0.1–15
|
||||
for non-YouTube sites.</em
|
||||
></label
|
||||
>
|
||||
<label>Auto-hide timer (0.1–15s):</label>
|
||||
<input type="text" class="site-hideWithControlsTimer" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -580,7 +580,7 @@
|
||||
<span>Override playback for this site</span>
|
||||
<input type="checkbox" class="override-playback" />
|
||||
</label>
|
||||
<div class="site-playback-container" style="display: none">
|
||||
<div class="site-playback-container">
|
||||
<div class="site-rule-option site-rule-option-checkbox">
|
||||
<label>Remember playback speed:</label>
|
||||
<input type="checkbox" class="site-rememberSpeed" />
|
||||
@@ -600,7 +600,7 @@
|
||||
<span>Override opacity for this site</span>
|
||||
<input type="checkbox" class="override-opacity" />
|
||||
</label>
|
||||
<div class="site-opacity-container" style="display: none">
|
||||
<div class="site-opacity-container">
|
||||
<div class="site-rule-option site-rule-option-field">
|
||||
<label>Controller opacity:</label>
|
||||
<input type="text" class="site-controllerOpacity" />
|
||||
@@ -612,7 +612,7 @@
|
||||
<span>Override subtitle nudge for this site</span>
|
||||
<input type="checkbox" class="override-subtitleNudge" />
|
||||
</label>
|
||||
<div class="site-subtitleNudge-container" style="display: none">
|
||||
<div class="site-subtitleNudge-container">
|
||||
<div class="site-rule-option site-rule-option-checkbox">
|
||||
<label>Enable subtitle nudge:</label>
|
||||
<input type="checkbox" class="site-enableSubtitleNudge" />
|
||||
@@ -628,7 +628,7 @@
|
||||
<span>Override in-player control bar for this site</span>
|
||||
<input type="checkbox" class="override-controlbar" />
|
||||
</label>
|
||||
<div class="site-controlbar-container" style="display: none">
|
||||
<div class="site-controlbar-container">
|
||||
<div class="cb-editor">
|
||||
<div class="cb-zone">
|
||||
<div class="cb-zone-label">Active</div>
|
||||
@@ -646,7 +646,7 @@
|
||||
<span>Override extension popup for this site</span>
|
||||
<input type="checkbox" class="override-popup-controlbar" />
|
||||
</label>
|
||||
<div class="site-popup-controlbar-container" style="display: none">
|
||||
<div class="site-popup-controlbar-container">
|
||||
<div class="site-rule-option site-rule-option-checkbox">
|
||||
<label>Show popup control bar</label>
|
||||
<input type="checkbox" class="site-showPopupControlBar" />
|
||||
@@ -668,7 +668,7 @@
|
||||
<span>Override shortcuts for this site</span>
|
||||
<input type="checkbox" class="override-shortcuts" />
|
||||
</label>
|
||||
<div class="site-shortcuts-container" style="display: none"></div>
|
||||
<div class="site-shortcuts-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+468
-501
@@ -1,106 +1,44 @@
|
||||
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 keyCodeAliases = {
|
||||
0: "null",
|
||||
null: "null",
|
||||
undefined: "null",
|
||||
32: "Space",
|
||||
37: "Left",
|
||||
38: "Up",
|
||||
39: "Right",
|
||||
40: "Down",
|
||||
96: "Num 0",
|
||||
97: "Num 1",
|
||||
98: "Num 2",
|
||||
99: "Num 3",
|
||||
100: "Num 4",
|
||||
101: "Num 5",
|
||||
102: "Num 6",
|
||||
103: "Num 7",
|
||||
104: "Num 8",
|
||||
105: "Num 9",
|
||||
106: "Num *",
|
||||
107: "Num +",
|
||||
109: "Num -",
|
||||
110: "Num .",
|
||||
111: "Num /",
|
||||
112: "F1",
|
||||
113: "F2",
|
||||
114: "F3",
|
||||
115: "F4",
|
||||
116: "F5",
|
||||
117: "F6",
|
||||
118: "F7",
|
||||
119: "F8",
|
||||
120: "F9",
|
||||
121: "F10",
|
||||
122: "F11",
|
||||
123: "F12",
|
||||
186: ";",
|
||||
188: "<",
|
||||
189: "-",
|
||||
187: "+",
|
||||
190: ">",
|
||||
191: "/",
|
||||
192: "~",
|
||||
219: "[",
|
||||
220: "\\",
|
||||
221: "]",
|
||||
222: "'",
|
||||
59: ";",
|
||||
61: "+",
|
||||
173: "-"
|
||||
};
|
||||
|
||||
var keyCodeToKey = {
|
||||
32: " ",
|
||||
37: "ArrowLeft",
|
||||
38: "ArrowUp",
|
||||
39: "ArrowRight",
|
||||
40: "ArrowDown",
|
||||
96: "0",
|
||||
97: "1",
|
||||
98: "2",
|
||||
99: "3",
|
||||
100: "4",
|
||||
101: "5",
|
||||
102: "6",
|
||||
103: "7",
|
||||
104: "8",
|
||||
105: "9",
|
||||
106: "*",
|
||||
107: "+",
|
||||
109: "-",
|
||||
110: ".",
|
||||
111: "/",
|
||||
112: "F1",
|
||||
113: "F2",
|
||||
114: "F3",
|
||||
115: "F4",
|
||||
116: "F5",
|
||||
117: "F6",
|
||||
118: "F7",
|
||||
119: "F8",
|
||||
120: "F9",
|
||||
121: "F10",
|
||||
122: "F11",
|
||||
123: "F12",
|
||||
186: ";",
|
||||
188: "<",
|
||||
189: "-",
|
||||
187: "+",
|
||||
190: ">",
|
||||
191: "/",
|
||||
192: "~",
|
||||
219: "[",
|
||||
220: "\\",
|
||||
221: "]",
|
||||
222: "'",
|
||||
59: ";",
|
||||
61: "+",
|
||||
173: "-"
|
||||
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([
|
||||
@@ -114,14 +52,9 @@ var modifierKeys = new Set([
|
||||
"Shift"
|
||||
]);
|
||||
|
||||
var displayKeyAliases = {
|
||||
" ": "Space",
|
||||
ArrowLeft: "Left",
|
||||
ArrowUp: "Up",
|
||||
ArrowRight: "Right",
|
||||
ArrowDown: "Down"
|
||||
};
|
||||
var controllerLocations = [
|
||||
var controllerLocations = Array.isArray(controllerUtils.controllerLocations)
|
||||
? controllerUtils.controllerLocations.slice()
|
||||
: [
|
||||
"top-left",
|
||||
"top-center",
|
||||
"top-right",
|
||||
@@ -130,7 +63,7 @@ var controllerLocations = [
|
||||
"bottom-center",
|
||||
"bottom-left",
|
||||
"middle-left"
|
||||
];
|
||||
];
|
||||
|
||||
var controllerButtonDefs = {
|
||||
rewind: { icon: "\u00AB", name: "Rewind" },
|
||||
@@ -141,11 +74,13 @@ var controllerButtonDefs = {
|
||||
reset: { icon: "\u21BB", name: "Reset speed" },
|
||||
fast: { icon: "\u2605", name: "Preferred speed" },
|
||||
nudge: { icon: "\u2713", name: "Subtitle nudge" },
|
||||
settings: { icon: "\u2699", name: "Settings" },
|
||||
pause: { icon: "\u23EF", name: "Pause / Play" },
|
||||
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" }
|
||||
jump: { icon: "\u21E5", name: "Jump to marker" },
|
||||
settings: { icon: "\u2699", name: "Settings" },
|
||||
};
|
||||
var popupExcludedButtonIds = new Set(["settings"]);
|
||||
|
||||
@@ -156,15 +91,11 @@ var lucideSubtitleNudgeActionLabels = {
|
||||
};
|
||||
|
||||
function sanitizePopupButtonOrder(buttonIds) {
|
||||
if (!Array.isArray(buttonIds)) return [];
|
||||
var seen = new Set();
|
||||
return buttonIds.filter(function (id) {
|
||||
if (!controllerButtonDefs[id] || popupExcludedButtonIds.has(id) || seen.has(id)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(id);
|
||||
return true;
|
||||
});
|
||||
return popupControlUtils.sanitizeButtonOrder(
|
||||
buttonIds,
|
||||
controllerButtonDefs,
|
||||
popupExcludedButtonIds
|
||||
);
|
||||
}
|
||||
|
||||
/** Cached custom Lucide SVGs (mirrors chrome.storage.local customButtonIcons). */
|
||||
@@ -221,53 +152,67 @@ function fillControlBarIconElement(icon, buttonId) {
|
||||
icon.textContent = (def && def.icon) || "?";
|
||||
}
|
||||
|
||||
function createDefaultBinding(action, key, keyCode, value) {
|
||||
function createDefaultBinding(action, code, value) {
|
||||
return {
|
||||
action: action,
|
||||
key: key,
|
||||
keyCode: keyCode,
|
||||
code: null,
|
||||
disabled: false,
|
||||
code: code,
|
||||
value: value,
|
||||
force: false,
|
||||
predefined: true
|
||||
};
|
||||
}
|
||||
|
||||
var tcDefaults = vscGetSettingsDefaults();
|
||||
var legacySyncKeys = [
|
||||
"resetSpeed",
|
||||
"speedStep",
|
||||
"fastSpeed",
|
||||
"rewindTime",
|
||||
"advanceTime",
|
||||
"resetKeyCode",
|
||||
"slowerKeyCode",
|
||||
"fasterKeyCode",
|
||||
"rewindKeyCode",
|
||||
"advanceKeyCode",
|
||||
"fastKeyCode",
|
||||
"blacklist"
|
||||
];
|
||||
|
||||
function persistManagedSyncSettings(settings, callback) {
|
||||
var nextSettings = vscBuildStoredSettingsDiff(settings);
|
||||
chrome.storage.sync.remove(vscGetManagedSyncKeys(), function () {
|
||||
if (chrome.runtime.lastError) {
|
||||
callback(chrome.runtime.lastError);
|
||||
return;
|
||||
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
|
||||
}
|
||||
|
||||
if (Object.keys(nextSettings).length === 0) {
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.sync.set(nextSettings, function () {
|
||||
callback(chrome.runtime.lastError || null);
|
||||
});
|
||||
});
|
||||
}
|
||||
],
|
||||
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",
|
||||
@@ -278,14 +223,17 @@ const actionLabels = {
|
||||
advance: "Advance",
|
||||
reset: "Reset speed",
|
||||
fast: "Preferred speed",
|
||||
muted: "Mute",
|
||||
pause: "Pause",
|
||||
toggleSubtitleNudge: "Toggle subtitle nudge",
|
||||
pause: "Play / Pause",
|
||||
muted: "Mute / Unmute",
|
||||
louder: "Increase volume",
|
||||
softer: "Decrease volume",
|
||||
mark: "Set marker",
|
||||
jump: "Jump to marker",
|
||||
toggleSubtitleNudge: "Toggle subtitle nudge"
|
||||
jump: "Jump to marker"
|
||||
};
|
||||
|
||||
const speedBindingActions = ["slower", "faster", "fast"];
|
||||
const speedBindingActions = ["slower", "faster", "fast", "softer", "louder"];
|
||||
const requiredShortcutActions = new Set(["display", "slower", "faster"]);
|
||||
|
||||
function formatSpeedBindingDisplay(action, value) {
|
||||
if (!speedBindingActions.includes(action)) {
|
||||
@@ -298,6 +246,30 @@ function formatSpeedBindingDisplay(action, 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",
|
||||
@@ -347,21 +319,91 @@ function refreshAddShortcutSelector() {
|
||||
}
|
||||
}
|
||||
|
||||
function ensureDefaultBinding(storage, action, key, keyCode, value) {
|
||||
function ensureDefaultBinding(storage, action, code, value) {
|
||||
if (storage.keyBindings.some((item) => item.action === action)) return;
|
||||
|
||||
storage.keyBindings.push(createDefaultBinding(action, key, keyCode, value));
|
||||
storage.keyBindings.push(createDefaultBinding(action, code, value));
|
||||
}
|
||||
|
||||
function normalizeControllerLocation(location) {
|
||||
if (controllerLocations.includes(location)) return location;
|
||||
return tcDefaults.controllerLocation;
|
||||
return controllerUtils.normalizeControllerLocation(
|
||||
location,
|
||||
tcDefaults.controllerLocation
|
||||
);
|
||||
}
|
||||
|
||||
function clampMarginPxInput(el, fallback) {
|
||||
var n = parseInt(el && el.value, 10);
|
||||
if (!Number.isFinite(n)) return fallback;
|
||||
return Math.min(200, Math.max(0, n));
|
||||
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) {
|
||||
@@ -381,128 +423,88 @@ function syncSiteRuleField(ruleEl, rule, key, isCheckbox) {
|
||||
}
|
||||
|
||||
function normalizeBindingKey(key) {
|
||||
if (typeof key !== "string" || key.length === 0) return null;
|
||||
if (key === "Spacebar") return " ";
|
||||
if (key === "Esc") return "Escape";
|
||||
if (key.length === 1 && /[a-z]/i.test(key)) return key.toUpperCase();
|
||||
return key;
|
||||
return keyBindingUtils.normalizeBindingKey(key);
|
||||
}
|
||||
|
||||
function getLegacyKeyCode(binding) {
|
||||
if (!binding) return null;
|
||||
if (Number.isInteger(binding.keyCode)) return binding.keyCode;
|
||||
if (typeof binding.key === "number" && Number.isInteger(binding.key)) {
|
||||
return binding.key;
|
||||
}
|
||||
return null;
|
||||
return keyBindingUtils.getLegacyKeyCode(binding);
|
||||
}
|
||||
|
||||
function legacyKeyCodeToBinding(keyCode) {
|
||||
if (!Number.isInteger(keyCode)) return null;
|
||||
var normalizedKey = keyCodeToKey[keyCode];
|
||||
if (!normalizedKey && keyCode >= 48 && keyCode <= 57) {
|
||||
normalizedKey = String.fromCharCode(keyCode);
|
||||
}
|
||||
if (!normalizedKey && keyCode >= 65 && keyCode <= 90) {
|
||||
normalizedKey = String.fromCharCode(keyCode);
|
||||
}
|
||||
return {
|
||||
key: normalizeBindingKey(normalizedKey),
|
||||
keyCode: keyCode,
|
||||
code: null,
|
||||
disabled: false
|
||||
};
|
||||
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 {
|
||||
key: null,
|
||||
keyCode: null,
|
||||
code: null,
|
||||
disabled: true
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeStoredBinding(binding, fallbackKeyCode) {
|
||||
var fallbackBinding = legacyKeyCodeToBinding(fallbackKeyCode);
|
||||
function normalizeStoredBinding(binding, fallbackCode) {
|
||||
if (!binding) {
|
||||
return fallbackBinding;
|
||||
if (!fallbackCode) return null;
|
||||
return {
|
||||
code: fallbackCode,
|
||||
disabled: false
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
binding.disabled === true ||
|
||||
(binding.key === null &&
|
||||
binding.keyCode === null &&
|
||||
binding.code === null)
|
||||
(binding.code === null &&
|
||||
binding.key === null &&
|
||||
binding.keyCode === null)
|
||||
) {
|
||||
return createDisabledBinding();
|
||||
}
|
||||
|
||||
var normalized = {
|
||||
key: null,
|
||||
keyCode: null,
|
||||
code:
|
||||
typeof binding.code === "string" && binding.code.length > 0
|
||||
? binding.code
|
||||
: null,
|
||||
disabled: false
|
||||
};
|
||||
|
||||
if (typeof binding.key === "string") {
|
||||
normalized.key = normalizeBindingKey(binding.key);
|
||||
}
|
||||
|
||||
var legacyKeyCode = getLegacyKeyCode(binding);
|
||||
if (Number.isInteger(legacyKeyCode)) {
|
||||
var legacyBinding = legacyKeyCodeToBinding(legacyKeyCode);
|
||||
if (legacyBinding) {
|
||||
normalized.key = normalized.key || legacyBinding.key;
|
||||
normalized.keyCode = legacyKeyCode;
|
||||
}
|
||||
}
|
||||
|
||||
if (Number.isInteger(binding.keyCode)) {
|
||||
normalized.keyCode = binding.keyCode;
|
||||
}
|
||||
|
||||
if (!normalized.key && fallbackBinding) {
|
||||
normalized.key = fallbackBinding.key;
|
||||
if (normalized.keyCode === null) normalized.keyCode = fallbackBinding.keyCode;
|
||||
}
|
||||
|
||||
if (!normalized.key && !normalized.code && normalized.keyCode === null) {
|
||||
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 "";
|
||||
if (binding.key) {
|
||||
return displayKeyAliases[binding.key] || binding.key;
|
||||
}
|
||||
var legacyKeyCode = getLegacyKeyCode(binding);
|
||||
if (keyCodeAliases[legacyKeyCode]) return keyCodeAliases[legacyKeyCode];
|
||||
if (Number.isInteger(legacyKeyCode)) return String.fromCharCode(legacyKeyCode);
|
||||
return "";
|
||||
return formatBindingCode(binding.code);
|
||||
}
|
||||
|
||||
function setShortcutInputBinding(input, binding) {
|
||||
input.vscBinding = binding ? Object.assign({}, binding) : null;
|
||||
input.keyCode =
|
||||
binding && Number.isInteger(binding.keyCode) ? binding.keyCode : null;
|
||||
input.value = getBindingLabel(binding);
|
||||
}
|
||||
|
||||
function captureBindingFromEvent(event) {
|
||||
var normalizedKey = normalizeBindingKey(event.key);
|
||||
if (!normalizedKey || modifierKeys.has(normalizedKey)) return null;
|
||||
if (modifierKeys.has(event.key)) return null;
|
||||
if (typeof event.code !== "string" || event.code.length === 0) return null;
|
||||
return {
|
||||
key: normalizedKey,
|
||||
keyCode: Number.isInteger(event.keyCode) ? event.keyCode : null,
|
||||
code: event.code || null,
|
||||
code: event.code,
|
||||
disabled: false
|
||||
};
|
||||
}
|
||||
@@ -533,8 +535,10 @@ function recordKeyPress(event) {
|
||||
}
|
||||
|
||||
function inputFilterNumbersOnly(event) {
|
||||
var char = String.fromCharCode(event.keyCode);
|
||||
var char = event.key;
|
||||
if (
|
||||
typeof char !== "string" ||
|
||||
char.length !== 1 ||
|
||||
!/[\d\.]$/.test(char) ||
|
||||
!/^\d+(\.\d*)?$/.test(event.target.value + char)
|
||||
) {
|
||||
@@ -561,7 +565,15 @@ function updateCustomShortcutInputText(inputItem, bindingOrKeyCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
setShortcutInputBinding(inputItem, legacyKeyCodeToBinding(bindingOrKeyCode));
|
||||
if (typeof bindingOrKeyCode === "string") {
|
||||
setShortcutInputBinding(inputItem, { code: bindingOrKeyCode, disabled: false });
|
||||
return;
|
||||
}
|
||||
|
||||
setShortcutInputBinding(
|
||||
inputItem,
|
||||
normalizeStoredBinding({ keyCode: bindingOrKeyCode })
|
||||
);
|
||||
}
|
||||
|
||||
function appendSelectOptions(select, options) {
|
||||
@@ -597,7 +609,10 @@ function add_shortcut(action, value) {
|
||||
valueInput.value = "N/A";
|
||||
valueInput.disabled = true;
|
||||
} else {
|
||||
valueInput.value = formatSpeedBindingDisplay(action, value || 0);
|
||||
valueInput.value = formatSpeedBindingDisplay(
|
||||
action,
|
||||
resolveShortcutValue(action, value)
|
||||
);
|
||||
}
|
||||
|
||||
var removeButton = document.createElement("button");
|
||||
@@ -621,23 +636,33 @@ function createKeyBindings(item) {
|
||||
var input = item.querySelector(".customKey");
|
||||
var valueInput = item.querySelector(".customValue");
|
||||
var predefined = !!item.id;
|
||||
var fallbackKeyCode =
|
||||
predefined && action === "display"
|
||||
? tcDefaults.displayKeyCode
|
||||
: undefined;
|
||||
var binding = normalizeStoredBinding(input.vscBinding, fallbackKeyCode);
|
||||
var binding = normalizeStoredBinding(input.vscBinding);
|
||||
|
||||
if (!binding) {
|
||||
if (requiredShortcutActions.has(action)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: "Error: Shortcut for " + action + " is invalid. Unable to save"
|
||||
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,
|
||||
key: binding.key,
|
||||
keyCode: binding.keyCode,
|
||||
code: binding.code,
|
||||
disabled: binding.disabled === true,
|
||||
value: customActionsNoValues.includes(action)
|
||||
@@ -717,8 +742,10 @@ function save_options() {
|
||||
document.getElementById("controllerLocation").value
|
||||
);
|
||||
settings.controllerOpacity =
|
||||
parseFloat(document.getElementById("controllerOpacity").value) ||
|
||||
tcDefaults.controllerOpacity;
|
||||
parseFiniteNumberOrFallback(
|
||||
document.getElementById("controllerOpacity").value,
|
||||
tcDefaults.controllerOpacity
|
||||
);
|
||||
|
||||
settings.controllerMarginTop = clampMarginPxInput(
|
||||
document.getElementById("controllerMarginTop"),
|
||||
@@ -806,8 +833,10 @@ function save_options() {
|
||||
|
||||
if (ruleEl.querySelector(".override-opacity").checked) {
|
||||
rule.controllerOpacity =
|
||||
parseFloat(ruleEl.querySelector(".site-controllerOpacity").value) ||
|
||||
settings.controllerOpacity;
|
||||
parseFiniteNumberOrFallback(
|
||||
ruleEl.querySelector(".site-controllerOpacity").value,
|
||||
settings.controllerOpacity
|
||||
);
|
||||
}
|
||||
|
||||
if (ruleEl.querySelector(".override-subtitleNudge").checked) {
|
||||
@@ -847,17 +876,34 @@ function save_options() {
|
||||
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 (!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,
|
||||
key: binding.key,
|
||||
keyCode: binding.keyCode,
|
||||
code: binding.code,
|
||||
disabled: binding.disabled === true,
|
||||
value: customActionsNoValues.includes(action)
|
||||
@@ -865,28 +911,23 @@ function save_options() {
|
||||
: Number(valueInput.value),
|
||||
force: forceCheckbox ? forceCheckbox.checked : false
|
||||
});
|
||||
}
|
||||
});
|
||||
if (saveError) return;
|
||||
if (shortcuts.length > 0) rule.shortcuts = shortcuts;
|
||||
}
|
||||
|
||||
settings.siteRules.push(rule);
|
||||
});
|
||||
|
||||
chrome.storage.sync.remove(legacySyncKeys, function () {
|
||||
if (chrome.runtime.lastError) {
|
||||
status.textContent =
|
||||
"Error: Failed to clear legacy settings - " +
|
||||
chrome.runtime.lastError.message;
|
||||
return;
|
||||
}
|
||||
// Legacy keys to remove
|
||||
const legacyKeys = [
|
||||
"resetSpeed", "speedStep", "fastSpeed", "rewindTime", "advanceTime",
|
||||
"resetKeyCode", "slowerKeyCode", "fasterKeyCode", "rewindKeyCode",
|
||||
"advanceKeyCode", "fastKeyCode", "blacklist"
|
||||
];
|
||||
|
||||
persistManagedSyncSettings(settings, function (error) {
|
||||
if (error) {
|
||||
status.textContent =
|
||||
"Error: Failed to save settings - " + error.message;
|
||||
return;
|
||||
}
|
||||
chrome.storage.sync.remove(legacyKeys, function () {
|
||||
chrome.storage.sync.set(settings, function () {
|
||||
status.textContent = "Options saved";
|
||||
setTimeout(function () {
|
||||
status.textContent = "";
|
||||
@@ -897,18 +938,7 @@ function save_options() {
|
||||
|
||||
function ensureAllDefaultBindings(storage) {
|
||||
tcDefaults.keyBindings.forEach((binding) => {
|
||||
// Special case for "display" to support legacy displayKeyCode
|
||||
if (binding.action === "display" && storage.displayKeyCode) {
|
||||
ensureDefaultBinding(storage, "display", "V", storage.displayKeyCode, 0);
|
||||
} else {
|
||||
ensureDefaultBinding(
|
||||
storage,
|
||||
binding.action,
|
||||
binding.key,
|
||||
binding.keyCode,
|
||||
binding.value
|
||||
);
|
||||
}
|
||||
ensureDefaultBinding(storage, binding.action, binding.code, binding.value);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -928,11 +958,13 @@ function addSiteRuleShortcut(container, action, binding, value, force) {
|
||||
advance: "Advance",
|
||||
reset: "Reset speed",
|
||||
fast: "Preferred speed",
|
||||
muted: "Mute",
|
||||
pause: "Pause",
|
||||
toggleSubtitleNudge: "Toggle subtitle nudge",
|
||||
pause: "Play / Pause",
|
||||
muted: "Mute / Unmute",
|
||||
louder: "Increase volume",
|
||||
softer: "Decrease volume",
|
||||
mark: "Set marker",
|
||||
jump: "Jump to marker",
|
||||
toggleSubtitleNudge: "Toggle subtitle nudge"
|
||||
jump: "Jump to marker"
|
||||
};
|
||||
var actionLabelText = actionLabels[action] || action;
|
||||
if (action === "toggleSubtitleNudge") {
|
||||
@@ -960,7 +992,10 @@ function addSiteRuleShortcut(container, action, binding, value, force) {
|
||||
valueInput.value = "N/A";
|
||||
valueInput.disabled = true;
|
||||
} else {
|
||||
valueInput.value = formatSpeedBindingDisplay(action, value || 0);
|
||||
valueInput.value = formatSpeedBindingDisplay(
|
||||
action,
|
||||
resolveShortcutValue(action, value)
|
||||
);
|
||||
}
|
||||
|
||||
var forceLabel = document.createElement("label");
|
||||
@@ -996,9 +1031,7 @@ function createSiteRule(rule) {
|
||||
ruleEl.querySelector(".site-pattern").value = pattern;
|
||||
|
||||
// Make the rule body collapsed by default
|
||||
var ruleBody = ruleEl.querySelector(".site-rule-body");
|
||||
ruleBody.style.display = "none";
|
||||
ruleEl.classList.add("collapsed");
|
||||
setSiteRuleExpandedState(ruleEl, false);
|
||||
|
||||
var enabledCheckbox = ruleEl.querySelector(".site-enabled");
|
||||
var contentEl = ruleEl.querySelector(".site-rule-content");
|
||||
@@ -1033,114 +1066,99 @@ function createSiteRule(rule) {
|
||||
];
|
||||
var hasPlacementOverride =
|
||||
rule && placementKeys.some(function (k) { return rule[k] !== undefined; });
|
||||
if (hasPlacementOverride) {
|
||||
ruleEl.querySelector(".override-placement").checked = true;
|
||||
ruleEl.querySelector(".site-placement-container").style.display = "block";
|
||||
}
|
||||
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");
|
||||
|
||||
if (rule && rule.startHidden !== undefined) {
|
||||
ruleEl.querySelector(".override-visibility").checked = true;
|
||||
ruleEl.querySelector(".site-visibility-container").style.display = "block";
|
||||
}
|
||||
ruleEl.querySelector(".override-visibility").checked = Boolean(
|
||||
rule && rule.startHidden !== undefined
|
||||
);
|
||||
syncSiteRuleField(ruleEl, rule, "startHidden", true);
|
||||
applySiteRuleOverrideState(ruleEl, "override-visibility", "site-visibility-container");
|
||||
|
||||
if (
|
||||
var hasAutohideOverride = Boolean(
|
||||
rule &&
|
||||
(rule.hideWithControls !== undefined ||
|
||||
rule.hideWithControlsTimer !== undefined)
|
||||
) {
|
||||
ruleEl.querySelector(".override-autohide").checked = true;
|
||||
ruleEl.querySelector(".site-autohide-container").style.display = "block";
|
||||
}
|
||||
);
|
||||
ruleEl.querySelector(".override-autohide").checked = hasAutohideOverride;
|
||||
syncSiteRuleField(ruleEl, rule, "hideWithControls", true);
|
||||
syncSiteRuleField(ruleEl, rule, "hideWithControlsTimer", false);
|
||||
applySiteRuleOverrideState(ruleEl, "override-autohide", "site-autohide-container");
|
||||
|
||||
if (
|
||||
var hasPlaybackOverride = Boolean(
|
||||
rule &&
|
||||
(rule.rememberSpeed !== undefined ||
|
||||
rule.forceLastSavedSpeed !== undefined ||
|
||||
rule.audioBoolean !== undefined)
|
||||
) {
|
||||
ruleEl.querySelector(".override-playback").checked = true;
|
||||
ruleEl.querySelector(".site-playback-container").style.display = "block";
|
||||
}
|
||||
);
|
||||
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");
|
||||
|
||||
if (rule && rule.controllerOpacity !== undefined) {
|
||||
ruleEl.querySelector(".override-opacity").checked = true;
|
||||
ruleEl.querySelector(".site-opacity-container").style.display = "block";
|
||||
}
|
||||
ruleEl.querySelector(".override-opacity").checked = Boolean(
|
||||
rule && rule.controllerOpacity !== undefined
|
||||
);
|
||||
syncSiteRuleField(ruleEl, rule, "controllerOpacity", false);
|
||||
applySiteRuleOverrideState(ruleEl, "override-opacity", "site-opacity-container");
|
||||
|
||||
if (
|
||||
var hasSubtitleNudgeOverride = Boolean(
|
||||
rule &&
|
||||
(rule.enableSubtitleNudge !== undefined ||
|
||||
rule.subtitleNudgeInterval !== undefined)
|
||||
) {
|
||||
ruleEl.querySelector(".override-subtitleNudge").checked = true;
|
||||
ruleEl.querySelector(".site-subtitleNudge-container").style.display =
|
||||
"block";
|
||||
}
|
||||
);
|
||||
ruleEl.querySelector(".override-subtitleNudge").checked = hasSubtitleNudgeOverride;
|
||||
syncSiteRuleField(ruleEl, rule, "enableSubtitleNudge", true);
|
||||
syncSiteRuleField(ruleEl, rule, "subtitleNudgeInterval", false);
|
||||
applySiteRuleOverrideState(
|
||||
ruleEl,
|
||||
"override-subtitleNudge",
|
||||
"site-subtitleNudge-container"
|
||||
);
|
||||
|
||||
if (rule && Array.isArray(rule.controllerButtons)) {
|
||||
ruleEl.querySelector(".override-controlbar").checked = true;
|
||||
var cbContainer = ruleEl.querySelector(".site-controlbar-container");
|
||||
cbContainer.style.display = "block";
|
||||
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"),
|
||||
rule.controllerButtons
|
||||
hasControlbarOverride ? rule.controllerButtons : getControlBarOrder()
|
||||
);
|
||||
}
|
||||
applySiteRuleOverrideState(ruleEl, "override-controlbar", "site-controlbar-container");
|
||||
|
||||
if (
|
||||
var hasPopupControlbarOverride = Boolean(
|
||||
rule &&
|
||||
(rule.showPopupControlBar !== undefined ||
|
||||
Array.isArray(rule.popupControllerButtons))
|
||||
) {
|
||||
ruleEl.querySelector(".override-popup-controlbar").checked = true;
|
||||
var popupCbContainer = ruleEl.querySelector(".site-popup-controlbar-container");
|
||||
popupCbContainer.style.display = "block";
|
||||
var sitePopupActive = ruleEl.querySelector(".site-popup-cb-active");
|
||||
var sitePopupAvailable = ruleEl.querySelector(".site-popup-cb-available");
|
||||
if (Array.isArray(rule.popupControllerButtons)) {
|
||||
);
|
||||
ruleEl.querySelector(".override-popup-controlbar").checked =
|
||||
hasPopupControlbarOverride;
|
||||
populateControlBarZones(
|
||||
sitePopupActive,
|
||||
sitePopupAvailable,
|
||||
sanitizePopupButtonOrder(rule.popupControllerButtons),
|
||||
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);
|
||||
}
|
||||
);
|
||||
} else if (
|
||||
sitePopupActive &&
|
||||
sitePopupAvailable &&
|
||||
sitePopupActive.children.length === 0
|
||||
) {
|
||||
populateControlBarZones(
|
||||
sitePopupActive,
|
||||
sitePopupAvailable,
|
||||
getPopupControlBarOrder(),
|
||||
function (id) {
|
||||
return !popupExcludedButtonIds.has(id);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
syncSiteRuleField(ruleEl, rule, "showPopupControlBar", true);
|
||||
applySiteRuleOverrideState(
|
||||
ruleEl,
|
||||
"override-popup-controlbar",
|
||||
"site-popup-controlbar-container"
|
||||
);
|
||||
|
||||
if (rule && Array.isArray(rule.shortcuts) && rule.shortcuts.length > 0) {
|
||||
ruleEl.querySelector(".override-shortcuts").checked = true;
|
||||
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");
|
||||
container.style.display = "block";
|
||||
|
||||
if (hasShortcutOverride) {
|
||||
rule.shortcuts.forEach((shortcut) => {
|
||||
addSiteRuleShortcut(
|
||||
container,
|
||||
@@ -1150,13 +1168,41 @@ function createSiteRule(rule) {
|
||||
shortcut.force
|
||||
);
|
||||
});
|
||||
} else {
|
||||
populateDefaultSiteShortcuts(container);
|
||||
}
|
||||
applySiteRuleOverrideState(ruleEl, "override-shortcuts", "site-shortcuts-container");
|
||||
|
||||
document.getElementById("siteRulesContainer").appendChild(ruleEl);
|
||||
}
|
||||
|
||||
function populateDefaultSiteShortcuts(container) {
|
||||
tcDefaults.keyBindings.forEach((binding) => {
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -1548,49 +1594,53 @@ function initLucideButtonIconsUI() {
|
||||
}
|
||||
|
||||
function restore_options() {
|
||||
chrome.storage.sync.get(null, function (storage) {
|
||||
var settings = vscExpandStoredSettings(storage);
|
||||
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 = settings.rememberSpeed;
|
||||
document.getElementById("rememberSpeed").checked = storage.rememberSpeed;
|
||||
document.getElementById("forceLastSavedSpeed").checked =
|
||||
settings.forceLastSavedSpeed;
|
||||
document.getElementById("audioBoolean").checked = settings.audioBoolean;
|
||||
document.getElementById("enabled").checked = settings.enabled;
|
||||
document.getElementById("startHidden").checked = settings.startHidden;
|
||||
document.getElementById("hideWithControls").checked =
|
||||
settings.hideWithControls;
|
||||
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 =
|
||||
settings.hideWithControlsTimer || tcDefaults.hideWithControlsTimer;
|
||||
storage.hideWithControlsTimer || tcDefaults.hideWithControlsTimer;
|
||||
|
||||
document.getElementById("controllerLocation").value =
|
||||
normalizeControllerLocation(settings.controllerLocation);
|
||||
normalizeControllerLocation(storage.controllerLocation);
|
||||
document.getElementById("controllerOpacity").value =
|
||||
settings.controllerOpacity;
|
||||
storage.controllerOpacity;
|
||||
document.getElementById("controllerMarginTop").value =
|
||||
settings.controllerMarginTop ?? tcDefaults.controllerMarginTop;
|
||||
storage.controllerMarginTop ?? tcDefaults.controllerMarginTop;
|
||||
document.getElementById("controllerMarginBottom").value =
|
||||
settings.controllerMarginBottom ?? tcDefaults.controllerMarginBottom;
|
||||
storage.controllerMarginBottom ?? tcDefaults.controllerMarginBottom;
|
||||
document.getElementById("showPopupControlBar").checked =
|
||||
settings.showPopupControlBar !== false;
|
||||
storage.showPopupControlBar !== false;
|
||||
document.getElementById("enableSubtitleNudge").checked =
|
||||
settings.enableSubtitleNudge;
|
||||
storage.enableSubtitleNudge;
|
||||
document.getElementById("subtitleNudgeInterval").value =
|
||||
settings.subtitleNudgeInterval;
|
||||
storage.subtitleNudgeInterval;
|
||||
|
||||
if (!Array.isArray(settings.keyBindings) || settings.keyBindings.length === 0) {
|
||||
settings.keyBindings = tcDefaults.keyBindings.slice();
|
||||
if (!Array.isArray(storage.keyBindings) || storage.keyBindings.length === 0) {
|
||||
storage.keyBindings = tcDefaults.keyBindings.slice();
|
||||
}
|
||||
|
||||
ensureAllDefaultBindings(settings);
|
||||
ensureAllDefaultBindings(storage);
|
||||
|
||||
document.querySelectorAll(".customs:not([id])").forEach((row) => row.remove());
|
||||
|
||||
settings.keyBindings.forEach((item) => {
|
||||
storage.keyBindings.forEach((item) => {
|
||||
var row = document.getElementById(item.action);
|
||||
var normalizedBinding = normalizeStoredBinding(item);
|
||||
|
||||
@@ -1619,8 +1669,10 @@ function restore_options() {
|
||||
|
||||
refreshAddShortcutSelector();
|
||||
|
||||
var siteRules = Array.isArray(settings.siteRules)
|
||||
? settings.siteRules
|
||||
// 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"));
|
||||
@@ -1632,16 +1684,16 @@ function restore_options() {
|
||||
});
|
||||
}
|
||||
|
||||
var controllerButtons = Array.isArray(settings.controllerButtons)
|
||||
? settings.controllerButtons
|
||||
var controllerButtons = Array.isArray(storage.controllerButtons)
|
||||
? storage.controllerButtons
|
||||
: tcDefaults.controllerButtons;
|
||||
populateControlBarEditor(controllerButtons);
|
||||
|
||||
document.getElementById("popupMatchHoverControls").checked =
|
||||
settings.popupMatchHoverControls !== false;
|
||||
storage.popupMatchHoverControls !== false;
|
||||
|
||||
var popupButtons = Array.isArray(settings.popupControllerButtons)
|
||||
? settings.popupControllerButtons
|
||||
var popupButtons = Array.isArray(storage.popupControllerButtons)
|
||||
? storage.popupControllerButtons
|
||||
: tcDefaults.popupControllerButtons;
|
||||
populatePopupControlBarEditor(popupButtons);
|
||||
updatePopupEditorDisabledState();
|
||||
@@ -1659,23 +1711,7 @@ function restore_defaults() {
|
||||
function () {}
|
||||
);
|
||||
|
||||
chrome.storage.sync.remove(legacySyncKeys, function () {
|
||||
if (chrome.runtime.lastError) {
|
||||
var errorStatus = document.getElementById("status");
|
||||
errorStatus.textContent =
|
||||
"Error: Failed to clear legacy settings - " +
|
||||
chrome.runtime.lastError.message;
|
||||
return;
|
||||
}
|
||||
|
||||
persistManagedSyncSettings(tcDefaults, function (error) {
|
||||
if (error) {
|
||||
var errorStatus = document.getElementById("status");
|
||||
errorStatus.textContent =
|
||||
"Error: Failed to restore defaults - " + error.message;
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.sync.set(tcDefaults, function () {
|
||||
restore_options();
|
||||
var status = document.getElementById("status");
|
||||
status.textContent = "Default options restored";
|
||||
@@ -1683,7 +1719,6 @@ function restore_defaults() {
|
||||
status.textContent = "";
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
@@ -1739,29 +1774,26 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
eventCaller(event, "customKey", recordKeyPress)
|
||||
);
|
||||
document.addEventListener("click", (event) => {
|
||||
if (event.target.classList.contains("removeParent")) {
|
||||
event.target.parentNode.remove();
|
||||
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;
|
||||
}
|
||||
if (event.target.classList.contains("remove-site-rule")) {
|
||||
event.target.closest(".site-rule").remove();
|
||||
var removeSiteRuleButton = targetEl.closest(".remove-site-rule");
|
||||
if (removeSiteRuleButton) {
|
||||
removeSiteRuleButton.closest(".site-rule").remove();
|
||||
return;
|
||||
}
|
||||
if (event.target.classList.contains("toggle-site-rule")) {
|
||||
var ruleEl = event.target.closest(".site-rule");
|
||||
var ruleBody = ruleEl.querySelector(".site-rule-body");
|
||||
var toggleButton = targetEl.closest(".toggle-site-rule");
|
||||
if (toggleButton) {
|
||||
var ruleEl = toggleButton.closest(".site-rule");
|
||||
var isCollapsed = ruleEl.classList.contains("collapsed");
|
||||
|
||||
if (isCollapsed) {
|
||||
ruleBody.style.display = "block";
|
||||
ruleEl.classList.remove("collapsed");
|
||||
event.target.textContent = "\u2212";
|
||||
} else {
|
||||
ruleBody.style.display = "none";
|
||||
ruleEl.classList.add("collapsed");
|
||||
event.target.textContent = "\u002b";
|
||||
}
|
||||
setSiteRuleExpandedState(ruleEl, isCollapsed);
|
||||
return;
|
||||
}
|
||||
});
|
||||
@@ -1783,7 +1815,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
"override-autohide": "site-autohide-container",
|
||||
"override-playback": "site-playback-container",
|
||||
"override-opacity": "site-opacity-container",
|
||||
"override-subtitleNudge": "site-subtitleNudge-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)) {
|
||||
@@ -1792,78 +1827,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
"." + siteOverrideContainers[ocb]
|
||||
);
|
||||
if (targetBox) {
|
||||
targetBox.style.display = event.target.checked ? "block" : "none";
|
||||
setSiteOverrideContainerState(targetBox, event.target.checked);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle site rule override checkboxes
|
||||
if (event.target.classList.contains("override-shortcuts")) {
|
||||
var container = event.target
|
||||
.closest(".site-rule-shortcuts")
|
||||
.querySelector(".site-shortcuts-container");
|
||||
if (event.target.checked) {
|
||||
container.style.display = "block";
|
||||
if (container.children.length === 0) {
|
||||
populateDefaultSiteShortcuts(container);
|
||||
}
|
||||
} else {
|
||||
container.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
if (event.target.classList.contains("override-controlbar")) {
|
||||
var cbContainer = event.target
|
||||
.closest(".site-rule-controlbar")
|
||||
.querySelector(".site-controlbar-container");
|
||||
if (event.target.checked) {
|
||||
cbContainer.style.display = "block";
|
||||
var activeZone = cbContainer.querySelector(".site-cb-active");
|
||||
var availableZone = cbContainer.querySelector(".site-cb-available");
|
||||
if (
|
||||
activeZone &&
|
||||
availableZone &&
|
||||
activeZone.children.length === 0 &&
|
||||
availableZone.children.length === 0
|
||||
) {
|
||||
populateControlBarZones(
|
||||
activeZone,
|
||||
availableZone,
|
||||
getControlBarOrder()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
cbContainer.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
if (event.target.classList.contains("override-popup-controlbar")) {
|
||||
var popupCbContainer = event.target
|
||||
.closest(".site-rule-controlbar")
|
||||
.querySelector(".site-popup-controlbar-container");
|
||||
if (event.target.checked) {
|
||||
popupCbContainer.style.display = "block";
|
||||
var popupActiveZone = popupCbContainer.querySelector(".site-popup-cb-active");
|
||||
var popupAvailableZone = popupCbContainer.querySelector(".site-popup-cb-available");
|
||||
if (
|
||||
popupActiveZone &&
|
||||
popupAvailableZone &&
|
||||
popupActiveZone.children.length === 0 &&
|
||||
popupAvailableZone.children.length === 0
|
||||
) {
|
||||
populateControlBarZones(
|
||||
popupActiveZone,
|
||||
popupAvailableZone,
|
||||
getPopupControlBarOrder(),
|
||||
function (id) {
|
||||
return !popupExcludedButtonIds.has(id);
|
||||
}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
popupCbContainer.style.display = "none";
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Generated
+2128
@@ -0,0 +1,2128 @@
|
||||
{
|
||||
"name": "speeder",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "speeder",
|
||||
"devDependencies": {
|
||||
"jsdom": "^26.1.0",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
|
||||
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^2.1.3",
|
||||
"@csstools/css-color-parser": "^3.0.9",
|
||||
"@csstools/css-parser-algorithms": "^3.0.4",
|
||||
"@csstools/css-tokenizer": "^3.0.3",
|
||||
"lru-cache": "^10.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-calc": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
|
||||
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-color-parser": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
|
||||
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^5.1.0",
|
||||
"@csstools/css-calc": "^2.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-parser-algorithms": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
|
||||
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-tokenizer": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
||||
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
|
||||
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
|
||||
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
|
||||
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
|
||||
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
|
||||
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
|
||||
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
|
||||
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
|
||||
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
|
||||
"integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
|
||||
"integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
|
||||
"integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
|
||||
"integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
|
||||
"integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
|
||||
"integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
|
||||
"integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
|
||||
"integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
|
||||
"integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
|
||||
"integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
|
||||
"integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
|
||||
"integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
|
||||
"integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
|
||||
"integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
|
||||
"integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
|
||||
"integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
|
||||
"integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
|
||||
"integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/deep-eql": "*",
|
||||
"assertion-error": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/deep-eql": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
|
||||
"integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "3.2.4",
|
||||
"@vitest/utils": "3.2.4",
|
||||
"chai": "^5.2.0",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
|
||||
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "3.2.4",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"msw": "^2.4.9",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"msw": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
|
||||
"integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
|
||||
"integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "3.2.4",
|
||||
"pathe": "^2.0.3",
|
||||
"strip-literal": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
|
||||
"integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.2.4",
|
||||
"magic-string": "^0.30.17",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
|
||||
"integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyspy": "^4.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
|
||||
"integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.2.4",
|
||||
"loupe": "^3.1.4",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
|
||||
"integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assertion-error": "^2.0.1",
|
||||
"check-error": "^2.1.1",
|
||||
"deep-eql": "^5.0.1",
|
||||
"loupe": "^3.1.0",
|
||||
"pathval": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/check-error": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
|
||||
"integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/cssstyle": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
|
||||
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^3.2.0",
|
||||
"rrweb-cssom": "^0.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
|
||||
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^14.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deep-eql": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
||||
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
||||
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.7",
|
||||
"@esbuild/android-arm": "0.27.7",
|
||||
"@esbuild/android-arm64": "0.27.7",
|
||||
"@esbuild/android-x64": "0.27.7",
|
||||
"@esbuild/darwin-arm64": "0.27.7",
|
||||
"@esbuild/darwin-x64": "0.27.7",
|
||||
"@esbuild/freebsd-arm64": "0.27.7",
|
||||
"@esbuild/freebsd-x64": "0.27.7",
|
||||
"@esbuild/linux-arm": "0.27.7",
|
||||
"@esbuild/linux-arm64": "0.27.7",
|
||||
"@esbuild/linux-ia32": "0.27.7",
|
||||
"@esbuild/linux-loong64": "0.27.7",
|
||||
"@esbuild/linux-mips64el": "0.27.7",
|
||||
"@esbuild/linux-ppc64": "0.27.7",
|
||||
"@esbuild/linux-riscv64": "0.27.7",
|
||||
"@esbuild/linux-s390x": "0.27.7",
|
||||
"@esbuild/linux-x64": "0.27.7",
|
||||
"@esbuild/netbsd-arm64": "0.27.7",
|
||||
"@esbuild/netbsd-x64": "0.27.7",
|
||||
"@esbuild/openbsd-arm64": "0.27.7",
|
||||
"@esbuild/openbsd-x64": "0.27.7",
|
||||
"@esbuild/openharmony-arm64": "0.27.7",
|
||||
"@esbuild/sunos-x64": "0.27.7",
|
||||
"@esbuild/win32-arm64": "0.27.7",
|
||||
"@esbuild/win32-ia32": "0.27.7",
|
||||
"@esbuild/win32-x64": "0.27.7"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
|
||||
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
||||
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-encoding": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-potential-custom-element-name": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
|
||||
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "26.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
|
||||
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
"decimal.js": "^10.5.0",
|
||||
"html-encoding-sniffer": "^4.0.0",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"nwsapi": "^2.2.16",
|
||||
"parse5": "^7.2.1",
|
||||
"rrweb-cssom": "^0.8.0",
|
||||
"saxes": "^6.0.0",
|
||||
"symbol-tree": "^3.2.4",
|
||||
"tough-cookie": "^5.1.1",
|
||||
"w3c-xmlserializer": "^5.0.0",
|
||||
"webidl-conversions": "^7.0.0",
|
||||
"whatwg-encoding": "^3.1.1",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^14.1.1",
|
||||
"ws": "^8.18.0",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"canvas": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"canvas": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/loupe": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
|
||||
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/nwsapi": {
|
||||
"version": "2.2.23",
|
||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
|
||||
"integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pathval": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
|
||||
"integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.16"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
|
||||
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.60.1",
|
||||
"@rollup/rollup-android-arm64": "4.60.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.60.1",
|
||||
"@rollup/rollup-darwin-x64": "4.60.1",
|
||||
"@rollup/rollup-freebsd-arm64": "4.60.1",
|
||||
"@rollup/rollup-freebsd-x64": "4.60.1",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.60.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.60.1",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.60.1",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.60.1",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.60.1",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.60.1",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.60.1",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.60.1",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.60.1",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.60.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.60.1",
|
||||
"@rollup/rollup-linux-x64-musl": "4.60.1",
|
||||
"@rollup/rollup-openbsd-x64": "4.60.1",
|
||||
"@rollup/rollup-openharmony-arm64": "4.60.1",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.60.1",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.60.1",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.60.1",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.60.1",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rrweb-cssom": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"xmlchars": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/siginfo": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stackback": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/strip-literal": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
|
||||
"integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^9.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-tree": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
|
||||
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinypool": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
|
||||
"integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyrainbow": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
|
||||
"integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyspy": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
|
||||
"integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
||||
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^6.1.86"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
|
||||
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
||||
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tldts": "^6.1.32"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"rollup": "^4.43.0",
|
||||
"tinyglobby": "^0.2.15"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
|
||||
"integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cac": "^6.7.14",
|
||||
"debug": "^4.4.1",
|
||||
"es-module-lexer": "^1.7.0",
|
||||
"pathe": "^2.0.3",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
||||
},
|
||||
"bin": {
|
||||
"vite-node": "vite-node.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
"@vitest/mocker": "3.2.4",
|
||||
"@vitest/pretty-format": "^3.2.4",
|
||||
"@vitest/runner": "3.2.4",
|
||||
"@vitest/snapshot": "3.2.4",
|
||||
"@vitest/spy": "3.2.4",
|
||||
"@vitest/utils": "3.2.4",
|
||||
"chai": "^5.2.0",
|
||||
"debug": "^4.4.1",
|
||||
"expect-type": "^1.2.1",
|
||||
"magic-string": "^0.30.17",
|
||||
"pathe": "^2.0.3",
|
||||
"picomatch": "^4.0.2",
|
||||
"std-env": "^3.9.0",
|
||||
"tinybench": "^2.9.0",
|
||||
"tinyexec": "^0.3.2",
|
||||
"tinyglobby": "^0.2.14",
|
||||
"tinypool": "^1.1.1",
|
||||
"tinyrainbow": "^2.0.0",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
|
||||
"vite-node": "3.2.4",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"vitest": "vitest.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"@vitest/browser": "3.2.4",
|
||||
"@vitest/ui": "3.2.4",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@edge-runtime/vm": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/debug": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/ui": {
|
||||
"optional": true
|
||||
},
|
||||
"happy-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"jsdom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "^5.1.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/why-is-node-running": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"siginfo": "^2.0.0",
|
||||
"stackback": "0.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"why-is-node-running": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "speeder",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jsdom": "^26.1.0",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -4,7 +4,8 @@
|
||||
<meta charset="utf-8" />
|
||||
<title>Speeder</title>
|
||||
<link rel="stylesheet" href="popup.css" />
|
||||
<script src="settings-core.js"></script>
|
||||
<script src="shared/site-rules.js"></script>
|
||||
<script src="shared/popup-controls.js"></script>
|
||||
<script src="ui-icons.js"></script>
|
||||
<script src="popup.js"></script>
|
||||
</head>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
|
||||
var speederShared =
|
||||
typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
|
||||
var siteRuleUtils = speederShared.siteRules || {};
|
||||
var popupControlUtils = speederShared.popupControls || {};
|
||||
|
||||
/* `label` is only used if ui-icons.js has no path for this action (fallback). */
|
||||
var controllerButtonDefs = {
|
||||
@@ -11,60 +14,43 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
reset: { label: "\u21BB", className: "" },
|
||||
fast: { label: "", className: "" },
|
||||
nudge: { label: "", className: "" },
|
||||
settings: { label: "", className: "" },
|
||||
pause: { label: "", className: "" },
|
||||
muted: { label: "", className: "" },
|
||||
louder: { label: "", className: "" },
|
||||
softer: { label: "", className: "" },
|
||||
mark: { label: "", className: "" },
|
||||
jump: { label: "", className: "" }
|
||||
jump: { label: "", className: "" },
|
||||
settings: { label: "", className: "" }
|
||||
};
|
||||
|
||||
var defaultButtons = ["rewind", "slower", "faster", "advance", "display"];
|
||||
var popupExcludedButtonIds = new Set(["settings"]);
|
||||
var storageDefaults = {
|
||||
enabled: true,
|
||||
showPopupControlBar: true,
|
||||
controllerButtons: defaultButtons,
|
||||
popupMatchHoverControls: true,
|
||||
popupControllerButtons: defaultButtons,
|
||||
siteRules: []
|
||||
};
|
||||
var renderToken = 0;
|
||||
|
||||
function matchSiteRule(url, siteRules) {
|
||||
if (!url || !Array.isArray(siteRules)) return null;
|
||||
return vscMatchSiteRule(url, siteRules);
|
||||
return siteRuleUtils.matchSiteRule(url, siteRules);
|
||||
}
|
||||
|
||||
function isSiteRuleDisabled(rule) {
|
||||
return vscIsSiteRuleDisabled(rule);
|
||||
return siteRuleUtils.isSiteRuleDisabled(rule);
|
||||
}
|
||||
|
||||
function resolvePopupButtons(storage, siteRule) {
|
||||
function sanitize(buttons) {
|
||||
if (!Array.isArray(buttons)) return [];
|
||||
var seen = new Set();
|
||||
return buttons.filter(function (id) {
|
||||
if (!controllerButtonDefs[id] || popupExcludedButtonIds.has(id) || seen.has(id)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(id);
|
||||
return true;
|
||||
return popupControlUtils.resolvePopupButtons(storage, siteRule, {
|
||||
controllerButtonDefs: controllerButtonDefs,
|
||||
defaultButtons: defaultButtons,
|
||||
excludedIds: popupExcludedButtonIds
|
||||
});
|
||||
}
|
||||
|
||||
if (siteRule && Array.isArray(siteRule.popupControllerButtons)) {
|
||||
return sanitize(siteRule.popupControllerButtons);
|
||||
}
|
||||
|
||||
if (storage.popupMatchHoverControls) {
|
||||
if (siteRule && Array.isArray(siteRule.controllerButtons)) {
|
||||
return sanitize(siteRule.controllerButtons);
|
||||
}
|
||||
|
||||
if (Array.isArray(storage.controllerButtons)) {
|
||||
return sanitize(storage.controllerButtons);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(storage.popupControllerButtons)) {
|
||||
return sanitize(storage.popupControllerButtons);
|
||||
}
|
||||
|
||||
return sanitize(defaultButtons);
|
||||
}
|
||||
|
||||
function setControlBarVisible(visible) {
|
||||
var bar = document.getElementById("popupControlBar");
|
||||
var dividers = document.querySelectorAll(".popup-divider");
|
||||
@@ -131,23 +117,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
|
||||
function pickBestFrameSpeedResult(results) {
|
||||
if (!results || !results.length) return null;
|
||||
var i;
|
||||
var r;
|
||||
var fallback = null;
|
||||
for (i = 0; i < results.length; i++) {
|
||||
r = results[i];
|
||||
if (!r || typeof r.speed !== "number") {
|
||||
continue;
|
||||
}
|
||||
if (r.preferred) {
|
||||
return { speed: r.speed };
|
||||
}
|
||||
if (!fallback) {
|
||||
fallback = { speed: r.speed };
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
return popupControlUtils.pickBestFrameSpeedResult(results);
|
||||
}
|
||||
|
||||
function querySpeed() {
|
||||
@@ -300,9 +270,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
? loc.customButtonIcons
|
||||
: {};
|
||||
|
||||
chrome.storage.sync.get(null, function (storage) {
|
||||
chrome.storage.sync.get(storageDefaults, function (storage) {
|
||||
if (currentRenderToken !== renderToken) return;
|
||||
storage = vscExpandStoredSettings(storage);
|
||||
|
||||
getActiveTabContext(function (context) {
|
||||
if (currentRenderToken !== renderToken) return;
|
||||
@@ -364,9 +333,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
changes.controllerButtons ||
|
||||
changes.popupMatchHoverControls ||
|
||||
changes.popupControllerButtons ||
|
||||
changes.siteRules ||
|
||||
changes.siteRulesMeta ||
|
||||
changes.siteRulesFormat
|
||||
changes.siteRules
|
||||
) {
|
||||
renderForActiveTab();
|
||||
}
|
||||
@@ -375,37 +342,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
renderForActiveTab();
|
||||
|
||||
function toggleEnabled(enabled, callback) {
|
||||
chrome.storage.sync.get(null, function (storage) {
|
||||
var nextSettings = vscExpandStoredSettings(storage);
|
||||
nextSettings.enabled = enabled;
|
||||
var storedSettings = vscBuildStoredSettingsDiff(nextSettings);
|
||||
|
||||
chrome.storage.sync.remove(vscGetManagedSyncKeys(), function () {
|
||||
if (chrome.runtime.lastError) {
|
||||
setStatusMessage(
|
||||
"Failed to update settings: " + chrome.runtime.lastError.message
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(storedSettings).length === 0) {
|
||||
chrome.storage.sync.set({ enabled: enabled }, function () {
|
||||
toggleEnabledUI(enabled);
|
||||
if (callback) callback(enabled);
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.sync.set(storedSettings, function () {
|
||||
if (chrome.runtime.lastError) {
|
||||
setStatusMessage(
|
||||
"Failed to update settings: " + chrome.runtime.lastError.message
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
toggleEnabledUI(enabled);
|
||||
if (callback) callback(enabled);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
(function(root, factory) {
|
||||
var exports = factory();
|
||||
|
||||
if (typeof module === "object" && module.exports) {
|
||||
module.exports = exports;
|
||||
}
|
||||
|
||||
root.SpeederShared = root.SpeederShared || {};
|
||||
root.SpeederShared.controllerUtils = exports;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function() {
|
||||
var CONTROLLER_MARGIN_MAX_PX = 200;
|
||||
var controllerLocations = [
|
||||
"top-left",
|
||||
"top-center",
|
||||
"top-right",
|
||||
"middle-right",
|
||||
"bottom-right",
|
||||
"bottom-center",
|
||||
"bottom-left",
|
||||
"middle-left"
|
||||
];
|
||||
var defaultControllerLocation = controllerLocations[0];
|
||||
|
||||
function normalizeControllerLocation(location, fallback) {
|
||||
if (controllerLocations.includes(location)) return location;
|
||||
return typeof fallback === "string"
|
||||
? fallback
|
||||
: defaultControllerLocation;
|
||||
}
|
||||
|
||||
function clampControllerMarginPx(value, fallback) {
|
||||
var numericValue = Number(value);
|
||||
if (!Number.isFinite(numericValue)) return fallback;
|
||||
|
||||
return Math.min(
|
||||
CONTROLLER_MARGIN_MAX_PX,
|
||||
Math.max(0, Math.round(numericValue))
|
||||
);
|
||||
}
|
||||
|
||||
function getNextControllerLocation(location) {
|
||||
var normalizedLocation = normalizeControllerLocation(location);
|
||||
var currentIndex = controllerLocations.indexOf(normalizedLocation);
|
||||
return controllerLocations[(currentIndex + 1) % controllerLocations.length];
|
||||
}
|
||||
|
||||
return {
|
||||
CONTROLLER_MARGIN_MAX_PX: CONTROLLER_MARGIN_MAX_PX,
|
||||
clampControllerMarginPx: clampControllerMarginPx,
|
||||
controllerLocations: controllerLocations.slice(),
|
||||
defaultControllerLocation: defaultControllerLocation,
|
||||
getNextControllerLocation: getNextControllerLocation,
|
||||
normalizeControllerLocation: normalizeControllerLocation
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
(function(root, factory) {
|
||||
var exports = factory();
|
||||
|
||||
if (typeof module === "object" && module.exports) {
|
||||
module.exports = exports;
|
||||
}
|
||||
|
||||
root.SpeederShared = root.SpeederShared || {};
|
||||
root.SpeederShared.importExport = exports;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function() {
|
||||
var rawSettingsKeys = new Set([
|
||||
"audioBoolean",
|
||||
"controllerButtons",
|
||||
"controllerLocation",
|
||||
"controllerMarginBottom",
|
||||
"controllerMarginLeft",
|
||||
"controllerMarginRight",
|
||||
"controllerMarginTop",
|
||||
"controllerOpacity",
|
||||
"enableSubtitleNudge",
|
||||
"enabled",
|
||||
"forceLastSavedSpeed",
|
||||
"hideWithControls",
|
||||
"hideWithControlsTimer",
|
||||
"hideWithYouTubeControls",
|
||||
"keyBindings",
|
||||
"lastSpeed",
|
||||
"popupControllerButtons",
|
||||
"popupMatchHoverControls",
|
||||
"rememberSpeed",
|
||||
"showPopupControlBar",
|
||||
"siteRules",
|
||||
"speed",
|
||||
"startHidden",
|
||||
"subtitleNudgeAmount",
|
||||
"subtitleNudgeInterval"
|
||||
]);
|
||||
|
||||
function isRecognizedRawSettingsObject(backup) {
|
||||
if (!backup || typeof backup !== "object" || Array.isArray(backup)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Object.keys(backup).some(function(key) {
|
||||
return rawSettingsKeys.has(key);
|
||||
});
|
||||
}
|
||||
|
||||
function generateBackupFilename(now) {
|
||||
var date = now instanceof Date ? now : new Date(now || Date.now());
|
||||
var year = date.getFullYear();
|
||||
var month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
var day = String(date.getDate()).padStart(2, "0");
|
||||
var hours = String(date.getHours()).padStart(2, "0");
|
||||
var minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
var seconds = String(date.getSeconds()).padStart(2, "0");
|
||||
|
||||
return (
|
||||
"speeder-backup_" +
|
||||
year +
|
||||
"-" +
|
||||
month +
|
||||
"-" +
|
||||
day +
|
||||
"_" +
|
||||
hours +
|
||||
"." +
|
||||
minutes +
|
||||
"." +
|
||||
seconds +
|
||||
".json"
|
||||
);
|
||||
}
|
||||
|
||||
function buildBackupPayload(settings, localSettings, now) {
|
||||
return {
|
||||
version: "1.1",
|
||||
exportDate: new Date(now || Date.now()).toISOString(),
|
||||
settings: settings,
|
||||
localSettings: localSettings || {}
|
||||
};
|
||||
}
|
||||
|
||||
function extractImportSettings(backup) {
|
||||
var settingsToImport = null;
|
||||
var isWrappedBackup = false;
|
||||
|
||||
if (backup && backup.settings && typeof backup.settings === "object") {
|
||||
settingsToImport = backup.settings;
|
||||
isWrappedBackup = true;
|
||||
} else if (
|
||||
backup &&
|
||||
typeof backup === "object" &&
|
||||
isRecognizedRawSettingsObject(backup)
|
||||
) {
|
||||
settingsToImport = backup;
|
||||
}
|
||||
|
||||
if (!settingsToImport) return null;
|
||||
|
||||
return {
|
||||
isWrappedBackup: isWrappedBackup,
|
||||
settings: settingsToImport,
|
||||
localSettings:
|
||||
backup &&
|
||||
backup.localSettings &&
|
||||
typeof backup.localSettings === "object"
|
||||
? backup.localSettings
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
function parseImportText(text) {
|
||||
return extractImportSettings(JSON.parse(text));
|
||||
}
|
||||
|
||||
return {
|
||||
buildBackupPayload: buildBackupPayload,
|
||||
extractImportSettings: extractImportSettings,
|
||||
generateBackupFilename: generateBackupFilename,
|
||||
isRecognizedRawSettingsObject: isRecognizedRawSettingsObject,
|
||||
parseImportText: parseImportText
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
(function(root, factory) {
|
||||
var exports = factory();
|
||||
|
||||
if (typeof module === "object" && module.exports) {
|
||||
module.exports = exports;
|
||||
}
|
||||
|
||||
root.SpeederShared = root.SpeederShared || {};
|
||||
root.SpeederShared.keyBindings = exports;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function() {
|
||||
function normalizeBindingKey(key) {
|
||||
if (typeof key !== "string" || key.length === 0) return null;
|
||||
if (key === "Spacebar") return " ";
|
||||
if (key === "Esc") return "Escape";
|
||||
if (key.length === 1 && /[a-z]/i.test(key)) return key.toUpperCase();
|
||||
return key;
|
||||
}
|
||||
|
||||
function getLegacyKeyCode(binding) {
|
||||
if (!binding) return null;
|
||||
if (Number.isInteger(binding.keyCode)) return binding.keyCode;
|
||||
if (typeof binding.key === "number" && Number.isInteger(binding.key)) {
|
||||
return binding.key;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function legacyBindingKeyToCode(key) {
|
||||
var normalizedKey = normalizeBindingKey(key);
|
||||
if (!normalizedKey) return null;
|
||||
if (/^[A-Z]$/.test(normalizedKey)) return "Key" + normalizedKey;
|
||||
if (/^[0-9]$/.test(normalizedKey)) return "Digit" + normalizedKey;
|
||||
if (/^F([1-9]|1[0-2])$/.test(normalizedKey)) return normalizedKey;
|
||||
|
||||
var keyMap = {
|
||||
" ": "Space",
|
||||
ArrowLeft: "ArrowLeft",
|
||||
ArrowUp: "ArrowUp",
|
||||
ArrowRight: "ArrowRight",
|
||||
ArrowDown: "ArrowDown",
|
||||
";": "Semicolon",
|
||||
"<": "Comma",
|
||||
"-": "Minus",
|
||||
"+": "Equal",
|
||||
">": "Period",
|
||||
"/": "Slash",
|
||||
"~": "Backquote",
|
||||
"[": "BracketLeft",
|
||||
"\\": "Backslash",
|
||||
"]": "BracketRight",
|
||||
"'": "Quote"
|
||||
};
|
||||
|
||||
return keyMap[normalizedKey] || null;
|
||||
}
|
||||
|
||||
function legacyKeyCodeToCode(keyCode) {
|
||||
if (!Number.isInteger(keyCode)) return null;
|
||||
if (keyCode >= 48 && keyCode <= 57) return "Digit" + String.fromCharCode(keyCode);
|
||||
if (keyCode >= 65 && keyCode <= 90) return "Key" + String.fromCharCode(keyCode);
|
||||
if (keyCode >= 96 && keyCode <= 105) return "Numpad" + (keyCode - 96);
|
||||
if (keyCode >= 112 && keyCode <= 123) return "F" + (keyCode - 111);
|
||||
|
||||
var keyCodeMap = {
|
||||
32: "Space",
|
||||
37: "ArrowLeft",
|
||||
38: "ArrowUp",
|
||||
39: "ArrowRight",
|
||||
40: "ArrowDown",
|
||||
106: "NumpadMultiply",
|
||||
107: "NumpadAdd",
|
||||
109: "NumpadSubtract",
|
||||
110: "NumpadDecimal",
|
||||
111: "NumpadDivide",
|
||||
186: "Semicolon",
|
||||
188: "Comma",
|
||||
189: "Minus",
|
||||
187: "Equal",
|
||||
190: "Period",
|
||||
191: "Slash",
|
||||
192: "Backquote",
|
||||
219: "BracketLeft",
|
||||
220: "Backslash",
|
||||
221: "BracketRight",
|
||||
222: "Quote",
|
||||
59: "Semicolon",
|
||||
61: "Equal",
|
||||
173: "Minus"
|
||||
};
|
||||
|
||||
return keyCodeMap[keyCode] || null;
|
||||
}
|
||||
|
||||
function inferBindingCode(binding, fallbackCode) {
|
||||
if (binding && typeof binding.code === "string" && binding.code.length > 0) {
|
||||
return binding.code;
|
||||
}
|
||||
|
||||
if (binding && typeof binding.key === "string") {
|
||||
var codeFromKey = legacyBindingKeyToCode(binding.key);
|
||||
if (codeFromKey) return codeFromKey;
|
||||
}
|
||||
|
||||
var legacyKeyCode = getLegacyKeyCode(binding);
|
||||
if (Number.isInteger(legacyKeyCode)) {
|
||||
var codeFromKeyCode = legacyKeyCodeToCode(legacyKeyCode);
|
||||
if (codeFromKeyCode) return codeFromKeyCode;
|
||||
}
|
||||
|
||||
return typeof fallbackCode === "string" && fallbackCode.length > 0
|
||||
? fallbackCode
|
||||
: null;
|
||||
}
|
||||
|
||||
return {
|
||||
getLegacyKeyCode: getLegacyKeyCode,
|
||||
inferBindingCode: inferBindingCode,
|
||||
legacyBindingKeyToCode: legacyBindingKeyToCode,
|
||||
legacyKeyCodeToCode: legacyKeyCodeToCode,
|
||||
normalizeBindingKey: normalizeBindingKey
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
(function(root, factory) {
|
||||
var exports = factory();
|
||||
|
||||
if (typeof module === "object" && module.exports) {
|
||||
module.exports = exports;
|
||||
}
|
||||
|
||||
root.SpeederShared = root.SpeederShared || {};
|
||||
root.SpeederShared.popupControls = exports;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function() {
|
||||
function normalizeExcludedIds(excludedIds) {
|
||||
if (excludedIds instanceof Set) return excludedIds;
|
||||
return new Set(Array.isArray(excludedIds) ? excludedIds : []);
|
||||
}
|
||||
|
||||
function sanitizeButtonOrder(buttonIds, controllerButtonDefs, excludedIds) {
|
||||
if (!Array.isArray(buttonIds)) return [];
|
||||
|
||||
var seen = new Set();
|
||||
var excluded = normalizeExcludedIds(excludedIds);
|
||||
|
||||
return buttonIds.filter(function(id) {
|
||||
if (!controllerButtonDefs[id] || excluded.has(id) || seen.has(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
seen.add(id);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function resolvePopupButtons(storage, siteRule, options) {
|
||||
var settings = storage || {};
|
||||
var config = options || {};
|
||||
var controllerButtonDefs = config.controllerButtonDefs || {};
|
||||
var defaultButtons = Array.isArray(config.defaultButtons)
|
||||
? config.defaultButtons
|
||||
: [];
|
||||
var excludedIds = config.excludedIds;
|
||||
|
||||
function sanitize(buttonIds) {
|
||||
return sanitizeButtonOrder(buttonIds, controllerButtonDefs, excludedIds);
|
||||
}
|
||||
|
||||
if (siteRule && Array.isArray(siteRule.popupControllerButtons)) {
|
||||
return sanitize(siteRule.popupControllerButtons);
|
||||
}
|
||||
|
||||
if (settings.popupMatchHoverControls) {
|
||||
if (siteRule && Array.isArray(siteRule.controllerButtons)) {
|
||||
return sanitize(siteRule.controllerButtons);
|
||||
}
|
||||
|
||||
if (Array.isArray(settings.controllerButtons)) {
|
||||
return sanitize(settings.controllerButtons);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(settings.popupControllerButtons)) {
|
||||
return sanitize(settings.popupControllerButtons);
|
||||
}
|
||||
|
||||
return sanitize(defaultButtons);
|
||||
}
|
||||
|
||||
function pickBestFrameSpeedResult(results) {
|
||||
if (!results || !results.length) return null;
|
||||
|
||||
var fallback = null;
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
var result = results[i];
|
||||
if (!result || typeof result.speed !== "number") continue;
|
||||
if (result.preferred) return { speed: result.speed };
|
||||
if (!fallback) fallback = { speed: result.speed };
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return {
|
||||
pickBestFrameSpeedResult: pickBestFrameSpeedResult,
|
||||
resolvePopupButtons: resolvePopupButtons,
|
||||
sanitizeButtonOrder: sanitizeButtonOrder
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
(function(root, factory) {
|
||||
var exports = factory();
|
||||
|
||||
if (typeof module === "object" && module.exports) {
|
||||
module.exports = exports;
|
||||
}
|
||||
|
||||
root.SpeederShared = root.SpeederShared || {};
|
||||
root.SpeederShared.siteRules = exports;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function() {
|
||||
var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
|
||||
|
||||
function escapeStringRegExp(str) {
|
||||
return String(str).replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
|
||||
}
|
||||
|
||||
function compileSiteRulePattern(pattern) {
|
||||
if (typeof pattern !== "string") return null;
|
||||
|
||||
var normalizedPattern = pattern.replace(regStrip, "");
|
||||
if (normalizedPattern.length === 0) return null;
|
||||
|
||||
if (
|
||||
normalizedPattern.startsWith("/") &&
|
||||
normalizedPattern.lastIndexOf("/") > 0
|
||||
) {
|
||||
var lastSlash = normalizedPattern.lastIndexOf("/");
|
||||
return new RegExp(
|
||||
normalizedPattern.substring(1, lastSlash),
|
||||
normalizedPattern.substring(lastSlash + 1)
|
||||
);
|
||||
}
|
||||
|
||||
return new RegExp(escapeStringRegExp(normalizedPattern));
|
||||
}
|
||||
|
||||
function matchSiteRule(url, siteRules) {
|
||||
if (!url || !Array.isArray(siteRules)) return null;
|
||||
|
||||
for (var i = 0; i < siteRules.length; i++) {
|
||||
var rule = siteRules[i];
|
||||
if (!rule || !rule.pattern) continue;
|
||||
|
||||
try {
|
||||
var re = compileSiteRulePattern(rule.pattern);
|
||||
if (re && re.test(url)) {
|
||||
return rule;
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isSiteRuleDisabled(rule) {
|
||||
return Boolean(
|
||||
rule &&
|
||||
(rule.enabled === false || rule.disableExtension === true)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
compileSiteRulePattern: compileSiteRulePattern,
|
||||
escapeStringRegExp: escapeStringRegExp,
|
||||
isSiteRuleDisabled: isSiteRuleDisabled,
|
||||
matchSiteRule: matchSiteRule
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { vi } from "vitest";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const repoRoot = path.resolve(__dirname, "..", "..");
|
||||
|
||||
function readRepoFile(relPath) {
|
||||
return fs.readFileSync(path.join(repoRoot, relPath), "utf8");
|
||||
}
|
||||
|
||||
export function loadHtml(relPath) {
|
||||
document.open();
|
||||
document.write(readRepoFile(relPath));
|
||||
document.close();
|
||||
}
|
||||
|
||||
export function loadScript(relPath) {
|
||||
window.eval(
|
||||
"var chrome = window.chrome || globalThis.chrome;\n" +
|
||||
readRepoFile(relPath) +
|
||||
"\n//# sourceURL=" +
|
||||
relPath
|
||||
);
|
||||
}
|
||||
|
||||
export async function flushAsyncWork() {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
export function triggerDomContentLoaded() {
|
||||
document.dispatchEvent(
|
||||
new window.Event("DOMContentLoaded", {
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function createEvent() {
|
||||
const listeners = [];
|
||||
return {
|
||||
addListener(listener) {
|
||||
listeners.push(listener);
|
||||
},
|
||||
trigger(...args) {
|
||||
listeners.forEach((listener) => listener(...args));
|
||||
},
|
||||
listeners
|
||||
};
|
||||
}
|
||||
|
||||
function createStorageArea(initialState = {}) {
|
||||
const state = { ...initialState };
|
||||
|
||||
function resolveGet(keys) {
|
||||
if (keys == null) return { ...state };
|
||||
if (Array.isArray(keys)) {
|
||||
return keys.reduce((acc, key) => {
|
||||
if (Object.prototype.hasOwnProperty.call(state, key)) {
|
||||
acc[key] = state[key];
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
if (typeof keys === "string") {
|
||||
return Object.prototype.hasOwnProperty.call(state, keys)
|
||||
? { [keys]: state[keys] }
|
||||
: {};
|
||||
}
|
||||
if (typeof keys === "object") {
|
||||
const result = { ...keys };
|
||||
Object.keys(state).forEach((key) => {
|
||||
result[key] = state[key];
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
__state: state,
|
||||
get: vi.fn((keys, callback) => {
|
||||
callback(resolveGet(keys));
|
||||
}),
|
||||
set: vi.fn((items, callback) => {
|
||||
Object.assign(state, items);
|
||||
if (callback) callback();
|
||||
}),
|
||||
remove: vi.fn((keys, callback) => {
|
||||
const list = Array.isArray(keys) ? keys : [keys];
|
||||
list.forEach((key) => {
|
||||
delete state[key];
|
||||
});
|
||||
if (callback) callback();
|
||||
}),
|
||||
clear: vi.fn((callback) => {
|
||||
Object.keys(state).forEach((key) => delete state[key]);
|
||||
if (callback) callback();
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
export function createChromeMock(options = {}) {
|
||||
const syncArea = createStorageArea(options.sync ?? {});
|
||||
const localArea = createStorageArea(options.local ?? {});
|
||||
const tabsOnActivated = createEvent();
|
||||
const tabsOnUpdated = createEvent();
|
||||
const storageOnChanged = createEvent();
|
||||
|
||||
const chrome = {
|
||||
runtime: {
|
||||
lastError: null,
|
||||
getManifest: vi.fn(() => ({
|
||||
version: options.manifestVersion || "9.9.9"
|
||||
})),
|
||||
getURL: vi.fn((url) => "moz-extension://speeder/" + url)
|
||||
},
|
||||
storage: {
|
||||
sync: syncArea,
|
||||
local: localArea,
|
||||
onChanged: storageOnChanged
|
||||
},
|
||||
tabs: {
|
||||
query: vi.fn((queryInfo, callback) => {
|
||||
callback(
|
||||
options.tabs ??
|
||||
[
|
||||
{
|
||||
id: 1,
|
||||
active: true,
|
||||
url: "https://example.com/watch"
|
||||
}
|
||||
]
|
||||
);
|
||||
}),
|
||||
sendMessage: vi.fn((tabId, message, callback) => {
|
||||
if (callback) {
|
||||
callback(options.sendMessageResponse ?? { speed: 1.25 });
|
||||
}
|
||||
}),
|
||||
executeScript: vi.fn((tabId, details, callback) => {
|
||||
if (callback) {
|
||||
callback(
|
||||
options.executeScriptResponse ?? [
|
||||
{ speed: 1.25, preferred: true }
|
||||
]
|
||||
);
|
||||
}
|
||||
}),
|
||||
create: vi.fn(),
|
||||
onActivated: tabsOnActivated,
|
||||
onUpdated: tabsOnUpdated
|
||||
},
|
||||
browserAction: {
|
||||
setIcon: vi.fn()
|
||||
}
|
||||
};
|
||||
|
||||
return chrome;
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { vi } = require("vitest");
|
||||
|
||||
const ROOT = path.resolve(__dirname, "..", "..");
|
||||
|
||||
function clone(value) {
|
||||
if (value === undefined) return undefined;
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function workspacePath(relPath) {
|
||||
return path.join(ROOT, relPath);
|
||||
}
|
||||
|
||||
function readWorkspaceFile(relPath) {
|
||||
return fs.readFileSync(workspacePath(relPath), "utf8");
|
||||
}
|
||||
|
||||
function loadHtmlFile(relPath) {
|
||||
document.open();
|
||||
document.write(readWorkspaceFile(relPath));
|
||||
document.close();
|
||||
}
|
||||
|
||||
function loadHtmlString(html) {
|
||||
document.open();
|
||||
document.write(html);
|
||||
document.close();
|
||||
}
|
||||
|
||||
function evaluateScript(relPath) {
|
||||
const source = readWorkspaceFile(relPath);
|
||||
window.eval(
|
||||
`${source}\n//# sourceURL=${workspacePath(relPath).replace(/\\/g, "/")}`
|
||||
);
|
||||
}
|
||||
|
||||
function fireDOMContentLoaded() {
|
||||
document.dispatchEvent(
|
||||
new window.Event("DOMContentLoaded", {
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function flushAsyncWork(turns) {
|
||||
const count = turns || 2;
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
await Promise.resolve();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
|
||||
function pickStorageValues(data, keys) {
|
||||
if (keys == null) return clone(data || {});
|
||||
|
||||
if (typeof keys === "string") {
|
||||
return { [keys]: clone(data ? data[keys] : undefined) };
|
||||
}
|
||||
|
||||
if (Array.isArray(keys)) {
|
||||
const result = {};
|
||||
keys.forEach((key) => {
|
||||
result[key] = clone(data ? data[key] : undefined);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
if (typeof keys === "object") {
|
||||
const result = clone(keys) || {};
|
||||
Object.keys(keys).forEach((key) => {
|
||||
if (data && Object.prototype.hasOwnProperty.call(data, key)) {
|
||||
result[key] = clone(data[key]);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function createChromeEvent() {
|
||||
const listeners = [];
|
||||
return {
|
||||
addListener(listener) {
|
||||
listeners.push(listener);
|
||||
},
|
||||
removeListener(listener) {
|
||||
const index = listeners.indexOf(listener);
|
||||
if (index >= 0) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
},
|
||||
hasListener(listener) {
|
||||
return listeners.includes(listener);
|
||||
},
|
||||
emit(...args) {
|
||||
listeners.slice().forEach((listener) => listener(...args));
|
||||
},
|
||||
listeners
|
||||
};
|
||||
}
|
||||
|
||||
function createStorageArea(areaName, initialData, onChangedEvent) {
|
||||
let data = clone(initialData) || {};
|
||||
|
||||
function emitChanges(changes) {
|
||||
if (changes && Object.keys(changes).length > 0) {
|
||||
onChangedEvent.emit(changes, areaName);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
get: vi.fn((keys, callback) => {
|
||||
if (callback) callback(pickStorageValues(data, keys));
|
||||
}),
|
||||
set: vi.fn((items, callback) => {
|
||||
const nextItems = items || {};
|
||||
const changes = {};
|
||||
|
||||
Object.keys(nextItems).forEach((key) => {
|
||||
const oldValue = clone(data[key]);
|
||||
const newValue = clone(nextItems[key]);
|
||||
data[key] = newValue;
|
||||
changes[key] = { oldValue, newValue };
|
||||
});
|
||||
|
||||
emitChanges(changes);
|
||||
if (callback) callback();
|
||||
}),
|
||||
remove: vi.fn((keys, callback) => {
|
||||
const list = Array.isArray(keys) ? keys : [keys];
|
||||
const changes = {};
|
||||
|
||||
list.forEach((key) => {
|
||||
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
||||
changes[key] = {
|
||||
oldValue: clone(data[key]),
|
||||
newValue: undefined
|
||||
};
|
||||
delete data[key];
|
||||
}
|
||||
});
|
||||
|
||||
emitChanges(changes);
|
||||
if (callback) callback();
|
||||
}),
|
||||
clear: vi.fn((callback) => {
|
||||
const changes = {};
|
||||
Object.keys(data).forEach((key) => {
|
||||
changes[key] = {
|
||||
oldValue: clone(data[key]),
|
||||
newValue: undefined
|
||||
};
|
||||
});
|
||||
data = {};
|
||||
emitChanges(changes);
|
||||
if (callback) callback();
|
||||
}),
|
||||
_dump() {
|
||||
return clone(data);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createChromeMock(options) {
|
||||
const config = options || {};
|
||||
const storageOnChanged = createChromeEvent();
|
||||
const tabsOnActivated = createChromeEvent();
|
||||
const tabsOnUpdated = createChromeEvent();
|
||||
const runtimeOnMessage = createChromeEvent();
|
||||
|
||||
const chrome = {
|
||||
runtime: {
|
||||
lastError: null,
|
||||
getManifest: vi.fn(() => clone(config.manifest) || { version: "0.0.0-test" }),
|
||||
getURL: vi.fn((relPath) => `moz-extension://${relPath}`),
|
||||
onMessage: runtimeOnMessage
|
||||
},
|
||||
browserAction: {
|
||||
setIcon: vi.fn()
|
||||
},
|
||||
tabs: {
|
||||
query: vi.fn((queryInfo, callback) => {
|
||||
const tabs = clone(config.tabsQueryResult) || [
|
||||
{ id: 1, active: true, url: "https://example.com/" }
|
||||
];
|
||||
if (callback) callback(tabs);
|
||||
}),
|
||||
sendMessage: vi.fn((tabId, message, callback) => {
|
||||
if (callback) callback(null);
|
||||
}),
|
||||
executeScript: vi.fn((tabId, details, callback) => {
|
||||
if (callback) callback([]);
|
||||
}),
|
||||
create: vi.fn(),
|
||||
onActivated: tabsOnActivated,
|
||||
onUpdated: tabsOnUpdated
|
||||
},
|
||||
storage: {
|
||||
onChanged: storageOnChanged,
|
||||
sync: null,
|
||||
local: null
|
||||
}
|
||||
};
|
||||
|
||||
chrome.storage.sync = createStorageArea(
|
||||
"sync",
|
||||
config.syncData,
|
||||
storageOnChanged
|
||||
);
|
||||
chrome.storage.local = createStorageArea(
|
||||
"local",
|
||||
config.localData,
|
||||
storageOnChanged
|
||||
);
|
||||
|
||||
return chrome;
|
||||
}
|
||||
|
||||
function installCommonWindowMocks() {
|
||||
window.open = vi.fn();
|
||||
window.close = vi.fn();
|
||||
window.requestAnimationFrame = vi.fn((callback) => setTimeout(callback, 0));
|
||||
window.cancelAnimationFrame = vi.fn((id) => clearTimeout(id));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createChromeMock,
|
||||
evaluateScript,
|
||||
fireDOMContentLoaded,
|
||||
flushAsyncWork,
|
||||
installCommonWindowMocks,
|
||||
loadHtmlFile,
|
||||
loadHtmlString,
|
||||
readWorkspaceFile,
|
||||
workspacePath
|
||||
};
|
||||
@@ -0,0 +1,276 @@
|
||||
import {
|
||||
createChromeMock,
|
||||
flushAsyncWork,
|
||||
loadHtml,
|
||||
loadScript
|
||||
} from "./helpers/browser.js";
|
||||
|
||||
async function setupImportExport(overrides = {}) {
|
||||
loadHtml("options.html");
|
||||
globalThis.chrome = createChromeMock(overrides);
|
||||
window.chrome = globalThis.chrome;
|
||||
globalThis.restore_options = vi.fn();
|
||||
loadScript("shared/import-export.js");
|
||||
loadScript("importExport.js");
|
||||
await flushAsyncWork();
|
||||
return globalThis.chrome;
|
||||
}
|
||||
|
||||
describe("import/export flows", () => {
|
||||
it("exports sync and local settings as a JSON download", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(2026, 3, 4, 8, 9, 10));
|
||||
const chrome = await setupImportExport({
|
||||
sync: { rememberSpeed: true },
|
||||
local: { customButtonIcons: { faster: { slug: "rocket" } } }
|
||||
});
|
||||
const OriginalBlob = globalThis.Blob;
|
||||
globalThis.Blob = class TestBlob {
|
||||
constructor(parts, options) {
|
||||
this.parts = parts;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
async text() {
|
||||
return this.parts.join("");
|
||||
}
|
||||
};
|
||||
|
||||
let capturedBlob = null;
|
||||
let clickedDownload = null;
|
||||
Object.defineProperty(URL, "createObjectURL", {
|
||||
configurable: true,
|
||||
value: vi.fn((blob) => {
|
||||
capturedBlob = blob;
|
||||
return "blob:test";
|
||||
})
|
||||
});
|
||||
Object.defineProperty(URL, "revokeObjectURL", {
|
||||
configurable: true,
|
||||
value: vi.fn(() => {})
|
||||
});
|
||||
vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(function () {
|
||||
clickedDownload = this.download;
|
||||
});
|
||||
|
||||
document.getElementById("exportSettings").click();
|
||||
|
||||
expect(clickedDownload).toBe("speeder-backup_2026-04-04_08.09.10.json");
|
||||
expect(capturedBlob).not.toBeNull();
|
||||
const blobText = await capturedBlob.text();
|
||||
expect(JSON.parse(blobText)).toEqual({
|
||||
version: "1.1",
|
||||
exportDate: "2026-04-04T12:09:10.000Z",
|
||||
settings: { rememberSpeed: true },
|
||||
localSettings: { customButtonIcons: { faster: { slug: "rocket" } } }
|
||||
});
|
||||
expect(document.getElementById("status").textContent).toBe(
|
||||
"Settings exported successfully"
|
||||
);
|
||||
expect(chrome.storage.sync.get).toHaveBeenCalled();
|
||||
expect(chrome.storage.local.get).toHaveBeenCalled();
|
||||
globalThis.Blob = OriginalBlob;
|
||||
});
|
||||
|
||||
it("imports wrapped backup payloads and refreshes options", async () => {
|
||||
vi.useFakeTimers();
|
||||
const chrome = await setupImportExport();
|
||||
|
||||
const originalCreateElement = document.createElement.bind(document);
|
||||
let createdInput = null;
|
||||
vi.spyOn(document, "createElement").mockImplementation((tagName) => {
|
||||
const el = originalCreateElement(tagName);
|
||||
if (tagName === "input") {
|
||||
createdInput = el;
|
||||
el.click = vi.fn();
|
||||
}
|
||||
return el;
|
||||
});
|
||||
|
||||
globalThis.FileReader = class MockFileReader {
|
||||
readAsText(file) {
|
||||
this.onload({
|
||||
target: {
|
||||
result: file.__text
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
globalThis.importSettings();
|
||||
createdInput.onchange({
|
||||
target: {
|
||||
files: [
|
||||
{
|
||||
__text: JSON.stringify({
|
||||
settings: { rememberSpeed: true },
|
||||
localSettings: { customButtonIcons: { faster: { slug: "rocket" } } }
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
expect(chrome.storage.local.set).toHaveBeenCalledWith(
|
||||
{ customButtonIcons: { faster: { slug: "rocket" } } },
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(chrome.storage.sync.clear).toHaveBeenCalled();
|
||||
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
|
||||
{ rememberSpeed: true },
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(document.getElementById("status").textContent).toBe(
|
||||
"Settings imported successfully. Reloading..."
|
||||
);
|
||||
|
||||
vi.advanceTimersByTime(500);
|
||||
expect(globalThis.restore_options).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("imports raw settings objects without touching local storage", async () => {
|
||||
vi.useFakeTimers();
|
||||
const chrome = await setupImportExport({
|
||||
local: { customButtonIcons: { faster: { slug: "rocket" } } }
|
||||
});
|
||||
|
||||
const originalCreateElement = document.createElement.bind(document);
|
||||
let createdInput = null;
|
||||
vi.spyOn(document, "createElement").mockImplementation((tagName) => {
|
||||
const el = originalCreateElement(tagName);
|
||||
if (tagName === "input") {
|
||||
createdInput = el;
|
||||
el.click = vi.fn();
|
||||
}
|
||||
return el;
|
||||
});
|
||||
|
||||
globalThis.FileReader = class MockFileReader {
|
||||
readAsText(file) {
|
||||
this.onload({
|
||||
target: {
|
||||
result: file.__text
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
globalThis.importSettings();
|
||||
createdInput.onchange({
|
||||
target: {
|
||||
files: [
|
||||
{
|
||||
__text: JSON.stringify({
|
||||
enabled: false,
|
||||
siteRules: [{ pattern: "example.com", enabled: false }]
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
expect(chrome.storage.local.clear).not.toHaveBeenCalled();
|
||||
expect(chrome.storage.local.set).not.toHaveBeenCalled();
|
||||
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
|
||||
{
|
||||
enabled: false,
|
||||
siteRules: [{ pattern: "example.com", enabled: false }]
|
||||
},
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("clears stale local data when a wrapped backup has empty local settings", async () => {
|
||||
vi.useFakeTimers();
|
||||
const chrome = await setupImportExport({
|
||||
local: {
|
||||
customButtonIcons: { faster: { slug: "rocket" } },
|
||||
lucideTagsCacheV1: { stale: true }
|
||||
}
|
||||
});
|
||||
|
||||
const originalCreateElement = document.createElement.bind(document);
|
||||
let createdInput = null;
|
||||
vi.spyOn(document, "createElement").mockImplementation((tagName) => {
|
||||
const el = originalCreateElement(tagName);
|
||||
if (tagName === "input") {
|
||||
createdInput = el;
|
||||
el.click = vi.fn();
|
||||
}
|
||||
return el;
|
||||
});
|
||||
|
||||
globalThis.FileReader = class MockFileReader {
|
||||
readAsText(file) {
|
||||
this.onload({
|
||||
target: {
|
||||
result: file.__text
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
globalThis.importSettings();
|
||||
createdInput.onchange({
|
||||
target: {
|
||||
files: [
|
||||
{
|
||||
__text: JSON.stringify({
|
||||
settings: { rememberSpeed: true },
|
||||
localSettings: {}
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
expect(chrome.storage.local.clear).toHaveBeenCalled();
|
||||
expect(chrome.storage.local.set).not.toHaveBeenCalled();
|
||||
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
|
||||
{ rememberSpeed: true },
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("shows an error for invalid backup files", async () => {
|
||||
vi.useFakeTimers();
|
||||
const chrome = await setupImportExport();
|
||||
|
||||
const originalCreateElement = document.createElement.bind(document);
|
||||
let createdInput = null;
|
||||
vi.spyOn(document, "createElement").mockImplementation((tagName) => {
|
||||
const el = originalCreateElement(tagName);
|
||||
if (tagName === "input") {
|
||||
createdInput = el;
|
||||
el.click = vi.fn();
|
||||
}
|
||||
return el;
|
||||
});
|
||||
|
||||
globalThis.FileReader = class MockFileReader {
|
||||
readAsText(file) {
|
||||
this.onload({
|
||||
target: {
|
||||
result: file.__text
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
globalThis.importSettings();
|
||||
createdInput.onchange({
|
||||
target: {
|
||||
files: [
|
||||
{
|
||||
__text: JSON.stringify({ wat: true })
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
expect(document.getElementById("status").textContent).toBe(
|
||||
"Error: Invalid backup file format"
|
||||
);
|
||||
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,189 @@
|
||||
const { afterEach, beforeEach, describe, expect, it, vi } = require("vitest");
|
||||
const {
|
||||
createChromeMock,
|
||||
evaluateScript,
|
||||
flushAsyncWork,
|
||||
installCommonWindowMocks,
|
||||
loadHtmlString
|
||||
} = require("./helpers/extension-test-utils");
|
||||
|
||||
function bootImportExport(options) {
|
||||
const config = options || {};
|
||||
|
||||
loadHtmlString(`<!doctype html><html><body>
|
||||
<button id="exportSettings">Export</button>
|
||||
<button id="importSettings">Import</button>
|
||||
<div id="status"></div>
|
||||
</body></html>`);
|
||||
|
||||
installCommonWindowMocks();
|
||||
|
||||
const chrome = createChromeMock({
|
||||
syncData: config.syncData,
|
||||
localData: config.localData
|
||||
});
|
||||
|
||||
global.chrome = chrome;
|
||||
window.chrome = chrome;
|
||||
|
||||
const createObjectURL = vi.fn(() => "blob:test");
|
||||
const revokeObjectURL = vi.fn();
|
||||
vi.stubGlobal("URL", {
|
||||
createObjectURL,
|
||||
revokeObjectURL
|
||||
});
|
||||
|
||||
evaluateScript("importExport.js");
|
||||
return { chrome, createObjectURL, revokeObjectURL };
|
||||
}
|
||||
|
||||
describe("importExport.js", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
delete global.chrome;
|
||||
});
|
||||
|
||||
it("generates timestamped backup filenames", () => {
|
||||
vi.setSystemTime(new Date("2026-04-04T13:14:15Z"));
|
||||
bootImportExport();
|
||||
|
||||
expect(window.generateBackupFilename()).toBe(
|
||||
"speeder-backup_2026-04-04_13.14.15.json"
|
||||
);
|
||||
});
|
||||
|
||||
it("exports sync and local settings into a downloadable backup", async () => {
|
||||
const clickSpy = vi
|
||||
.spyOn(window.HTMLAnchorElement.prototype, "click")
|
||||
.mockImplementation(() => {});
|
||||
const { createObjectURL, revokeObjectURL } = bootImportExport({
|
||||
syncData: {
|
||||
rememberSpeed: true,
|
||||
keyBindings: [{ action: "faster", code: "KeyD", value: 0.1 }]
|
||||
},
|
||||
localData: {
|
||||
customButtonIcons: {
|
||||
faster: { slug: "rocket", svg: "<svg></svg>" }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector("#exportSettings").click();
|
||||
await flushAsyncWork();
|
||||
|
||||
expect(createObjectURL).toHaveBeenCalledTimes(1);
|
||||
const blob = createObjectURL.mock.calls[0][0];
|
||||
const backup = JSON.parse(await blob.text());
|
||||
|
||||
expect(backup.settings.rememberSpeed).toBe(true);
|
||||
expect(backup.localSettings.customButtonIcons.faster.slug).toBe("rocket");
|
||||
expect(clickSpy).toHaveBeenCalledTimes(1);
|
||||
expect(revokeObjectURL).toHaveBeenCalledWith("blob:test");
|
||||
expect(document.querySelector("#status").textContent).toContain("exported");
|
||||
});
|
||||
|
||||
it("imports wrapped backups, restores local data, and refreshes the options page", async () => {
|
||||
const { chrome } = bootImportExport();
|
||||
window.restore_options = vi.fn();
|
||||
|
||||
const realCreateElement = document.createElement.bind(document);
|
||||
const fakeInput = realCreateElement("input");
|
||||
Object.defineProperty(fakeInput, "files", {
|
||||
configurable: true,
|
||||
value: [
|
||||
{
|
||||
__contents: JSON.stringify({
|
||||
settings: {
|
||||
rememberSpeed: true,
|
||||
enabled: false
|
||||
},
|
||||
localSettings: {
|
||||
customButtonIcons: {
|
||||
faster: { slug: "rocket", svg: "<svg></svg>" }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
fakeInput.click = vi.fn(() => {
|
||||
fakeInput.onchange({ target: fakeInput });
|
||||
});
|
||||
|
||||
vi.spyOn(document, "createElement").mockImplementation((tagName) => {
|
||||
if (String(tagName).toLowerCase() === "input") {
|
||||
return fakeInput;
|
||||
}
|
||||
return realCreateElement(tagName);
|
||||
});
|
||||
|
||||
class FakeFileReader {
|
||||
readAsText(file) {
|
||||
this.onload({ target: { result: file.__contents } });
|
||||
}
|
||||
}
|
||||
|
||||
vi.stubGlobal("FileReader", FakeFileReader);
|
||||
|
||||
document.querySelector("#importSettings").click();
|
||||
await flushAsyncWork();
|
||||
|
||||
expect(chrome.storage.local.set).toHaveBeenCalledWith(
|
||||
{
|
||||
customButtonIcons: {
|
||||
faster: { slug: "rocket", svg: "<svg></svg>" }
|
||||
}
|
||||
},
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(chrome.storage.sync.clear).toHaveBeenCalled();
|
||||
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
|
||||
{ rememberSpeed: true, enabled: false },
|
||||
expect.any(Function)
|
||||
);
|
||||
|
||||
vi.advanceTimersByTime(500);
|
||||
expect(window.restore_options).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows an error for malformed backups", async () => {
|
||||
bootImportExport();
|
||||
|
||||
const realCreateElement = document.createElement.bind(document);
|
||||
const fakeInput = realCreateElement("input");
|
||||
Object.defineProperty(fakeInput, "files", {
|
||||
configurable: true,
|
||||
value: [{ __contents: "{bad json" }]
|
||||
});
|
||||
fakeInput.click = vi.fn(() => {
|
||||
fakeInput.onchange({ target: fakeInput });
|
||||
});
|
||||
|
||||
vi.spyOn(document, "createElement").mockImplementation((tagName) => {
|
||||
if (String(tagName).toLowerCase() === "input") {
|
||||
return fakeInput;
|
||||
}
|
||||
return realCreateElement(tagName);
|
||||
});
|
||||
|
||||
class FakeFileReader {
|
||||
readAsText(file) {
|
||||
this.onload({ target: { result: file.__contents } });
|
||||
}
|
||||
}
|
||||
|
||||
vi.stubGlobal("FileReader", FakeFileReader);
|
||||
|
||||
document.querySelector("#importSettings").click();
|
||||
await flushAsyncWork();
|
||||
|
||||
expect(document.querySelector("#status").textContent).toContain(
|
||||
"Failed to parse backup file"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
const { afterEach, describe, expect, it, vi } = require("vitest");
|
||||
const {
|
||||
createChromeMock,
|
||||
evaluateScript,
|
||||
flushAsyncWork,
|
||||
loadHtmlString
|
||||
} = require("./helpers/extension-test-utils");
|
||||
|
||||
function bootInject(options) {
|
||||
const config = options || {};
|
||||
|
||||
loadHtmlString("<!doctype html><html><body></body></html>");
|
||||
|
||||
const chrome = createChromeMock({
|
||||
syncData: config.syncData,
|
||||
localData: config.localData
|
||||
});
|
||||
|
||||
global.chrome = chrome;
|
||||
window.chrome = chrome;
|
||||
window.requestIdleCallback = (callback, opts) =>
|
||||
setTimeout(
|
||||
() =>
|
||||
callback({
|
||||
didTimeout: false,
|
||||
timeRemaining() {
|
||||
return 1;
|
||||
}
|
||||
}),
|
||||
(opts && opts.timeout) || 0
|
||||
);
|
||||
window.cancelIdleCallback = (id) => clearTimeout(id);
|
||||
|
||||
evaluateScript("ui-icons.js");
|
||||
evaluateScript("inject.js");
|
||||
|
||||
return chrome;
|
||||
}
|
||||
|
||||
describe("inject.js helper logic", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
delete global.chrome;
|
||||
});
|
||||
|
||||
it("normalizes bindings from legacy formats", async () => {
|
||||
bootInject();
|
||||
await flushAsyncWork(3);
|
||||
|
||||
expect(
|
||||
window.normalizeStoredBinding({
|
||||
action: "faster",
|
||||
key: "g",
|
||||
value: 1.8,
|
||||
force: false
|
||||
}).code
|
||||
).toBe("KeyG");
|
||||
|
||||
expect(
|
||||
window.normalizeStoredBinding({
|
||||
action: "pause",
|
||||
code: null,
|
||||
key: null,
|
||||
keyCode: null,
|
||||
value: 0
|
||||
})
|
||||
).toEqual({
|
||||
action: "pause",
|
||||
code: null,
|
||||
disabled: true,
|
||||
value: 0,
|
||||
force: "false",
|
||||
predefined: false
|
||||
});
|
||||
|
||||
expect(window.defaultKeyBindings({ speedStep: 0.25, rewindTime: 5 })[0]).toEqual(
|
||||
{
|
||||
action: "slower",
|
||||
code: "KeyS",
|
||||
value: 0.25,
|
||||
force: false,
|
||||
predefined: true
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("clamps controller margins and ignores stale source-specific target speeds", async () => {
|
||||
bootInject();
|
||||
await flushAsyncWork(3);
|
||||
|
||||
expect(window.normalizeControllerMarginPx(250, 0)).toBe(200);
|
||||
expect(window.normalizeControllerMarginPx(-5, 65)).toBe(0);
|
||||
expect(window.normalizeControllerMarginPx("bad", 65)).toBe(65);
|
||||
|
||||
const staleVideo = {
|
||||
currentSrc: "fresh.mp4",
|
||||
vsc: {
|
||||
targetSpeed: 1.75,
|
||||
targetSpeedSourceKey: "old.mp4"
|
||||
}
|
||||
};
|
||||
|
||||
expect(window.getControllerTargetSpeed(staleVideo)).toBeNull();
|
||||
|
||||
window.tc.settings.rememberSpeed = true;
|
||||
window.tc.settings.forceLastSavedSpeed = false;
|
||||
window.tc.settings.lastSpeed = 1.3;
|
||||
window.tc.settings.speeds = { "fresh.mp4": 1.6 };
|
||||
|
||||
expect(window.getRememberedSpeed({ currentSrc: "fresh.mp4" })).toBe(1.6);
|
||||
expect(window.getDesiredSpeed(staleVideo)).toBe(1.6);
|
||||
});
|
||||
|
||||
it("applies site rule overrides and detects disabled sites", async () => {
|
||||
bootInject();
|
||||
await flushAsyncWork(3);
|
||||
|
||||
window.tc.settings.siteRules = [{ pattern: "localhost", enabled: false }];
|
||||
window.captureSiteRuleBase();
|
||||
expect(window.applySiteRuleOverrides()).toBe(true);
|
||||
|
||||
window.resetSettingsFromSiteRuleBase();
|
||||
window.tc.settings.siteRules = [
|
||||
{
|
||||
pattern: "localhost",
|
||||
controllerLocation: "bottom-left",
|
||||
controllerMarginTop: 300,
|
||||
controllerMarginBottom: -10,
|
||||
rememberSpeed: true
|
||||
}
|
||||
];
|
||||
window.captureSiteRuleBase();
|
||||
|
||||
expect(window.applySiteRuleOverrides()).toBe(false);
|
||||
expect(window.tc.settings.controllerLocation).toBe("bottom-left");
|
||||
expect(window.tc.settings.controllerMarginTop).toBe(200);
|
||||
expect(window.tc.settings.controllerMarginBottom).toBe(0);
|
||||
expect(window.tc.settings.rememberSpeed).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createChromeMock, flushAsyncWork, loadScript } from "./helpers/browser.js";
|
||||
|
||||
function loadBlankDocument() {
|
||||
document.open();
|
||||
document.write("<!doctype html><html><body></body></html>");
|
||||
document.close();
|
||||
}
|
||||
|
||||
async function bootInject({ sync = {}, local = {} } = {}) {
|
||||
loadBlankDocument();
|
||||
globalThis.chrome = createChromeMock({ sync, local });
|
||||
window.chrome = globalThis.chrome;
|
||||
globalThis.chrome.runtime.onMessage = {
|
||||
addListener: vi.fn()
|
||||
};
|
||||
const originalSyncGet = globalThis.chrome.storage.sync.get;
|
||||
const originalLocalGet = globalThis.chrome.storage.local.get;
|
||||
globalThis.chrome.storage.sync.get = vi.fn((keys, callback) => {
|
||||
Promise.resolve().then(() => originalSyncGet(keys, callback));
|
||||
});
|
||||
globalThis.chrome.storage.local.get = vi.fn((keys, callback) => {
|
||||
Promise.resolve().then(() => originalLocalGet(keys, callback));
|
||||
});
|
||||
globalThis.requestIdleCallback = (callback, options) =>
|
||||
setTimeout(
|
||||
() =>
|
||||
callback({
|
||||
didTimeout: false,
|
||||
timeRemaining() {
|
||||
return 1;
|
||||
}
|
||||
}),
|
||||
(options && options.timeout) || 0
|
||||
);
|
||||
globalThis.cancelIdleCallback = (id) => clearTimeout(id);
|
||||
|
||||
loadScript("shared/controller-utils.js");
|
||||
loadScript("shared/key-bindings.js");
|
||||
loadScript("shared/site-rules.js");
|
||||
loadScript("ui-icons.js");
|
||||
loadScript("inject.js");
|
||||
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
await flushAsyncWork();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
|
||||
describe("inject runtime", () => {
|
||||
it("keeps subtitle nudge disabled when the effective setting is off", async () => {
|
||||
await bootInject({
|
||||
sync: {
|
||||
enableSubtitleNudge: false
|
||||
}
|
||||
});
|
||||
|
||||
const stopSubtitleNudge = vi.fn();
|
||||
const startSubtitleNudge = vi.fn();
|
||||
const flashEl = document.createElement("span");
|
||||
const video = {
|
||||
paused: false,
|
||||
playbackRate: 1.5,
|
||||
vsc: {
|
||||
stopSubtitleNudge,
|
||||
startSubtitleNudge,
|
||||
subtitleNudgeEnabledOverride: null,
|
||||
subtitleNudgeIndicator: null,
|
||||
nudgeFlashIndicator: flashEl
|
||||
}
|
||||
};
|
||||
|
||||
expect(window.tc.settings.enableSubtitleNudge).toBe(false);
|
||||
expect(window.isSubtitleNudgeEnabledForVideo(video)).toBe(false);
|
||||
expect(window.setSubtitleNudgeEnabledForVideo(video, true)).toBe(false);
|
||||
expect(video.vsc.subtitleNudgeEnabledOverride).toBeNull();
|
||||
expect(stopSubtitleNudge).toHaveBeenCalledTimes(1);
|
||||
expect(startSubtitleNudge).not.toHaveBeenCalled();
|
||||
expect(flashEl.classList.contains("visible")).toBe(false);
|
||||
|
||||
window.tc.settings.enableSubtitleNudge = true;
|
||||
expect(window.setSubtitleNudgeEnabledForVideo(video, true)).toBe(true);
|
||||
expect(window.isSubtitleNudgeEnabledForVideo(video)).toBe(true);
|
||||
|
||||
window.tc.settings.enableSubtitleNudge = false;
|
||||
expect(window.isSubtitleNudgeEnabledForVideo(video)).toBe(false);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
const { afterEach, describe, expect, it } = require("vitest");
|
||||
const {
|
||||
evaluateScript,
|
||||
loadHtmlString
|
||||
} = require("./helpers/extension-test-utils");
|
||||
|
||||
describe("lucide-client.js", () => {
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
it("builds icon URLs and rejects invalid slugs", () => {
|
||||
loadHtmlString("<!doctype html><html><body></body></html>");
|
||||
evaluateScript("ui-icons.js");
|
||||
evaluateScript("lucide-client.js");
|
||||
|
||||
expect(window.lucideIconSvgUrl("alarm-clock")).toContain(
|
||||
"/icons/alarm-clock.svg"
|
||||
);
|
||||
expect(window.lucideIconSvgUrl("bad slug!!")).toBe("");
|
||||
expect(window.lucideTagsJsonUrl()).toContain("/tags.json");
|
||||
});
|
||||
|
||||
it("sanitizes SVG before persisting a Lucide icon", () => {
|
||||
loadHtmlString("<!doctype html><html><body></body></html>");
|
||||
evaluateScript("ui-icons.js");
|
||||
evaluateScript("lucide-client.js");
|
||||
|
||||
const sanitized = window.sanitizeLucideSvg(`
|
||||
<svg width="10" height="10" onclick="evil()">
|
||||
<script>alert(1)</script>
|
||||
<foreignObject>bad</foreignObject>
|
||||
<path d="M0 0h10v10"></path>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
expect(sanitized).toContain("<svg");
|
||||
expect(sanitized).not.toContain("onclick");
|
||||
expect(sanitized).not.toContain("<script");
|
||||
expect(sanitized).not.toContain("foreignObject");
|
||||
expect(sanitized).toContain('width="100%"');
|
||||
});
|
||||
|
||||
it("searches and ranks icon slugs by query", () => {
|
||||
loadHtmlString("<!doctype html><html><body></body></html>");
|
||||
evaluateScript("ui-icons.js");
|
||||
evaluateScript("lucide-client.js");
|
||||
|
||||
const results = window.searchLucideSlugs(
|
||||
{
|
||||
alarm: ["clock", "time"],
|
||||
"badge-alert": ["alert", "warning"],
|
||||
calendar: ["date", "time"]
|
||||
},
|
||||
"al",
|
||||
10
|
||||
);
|
||||
|
||||
expect(results).toEqual(["alarm", "badge-alert", "calendar"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,194 @@
|
||||
import {
|
||||
createChromeMock,
|
||||
flushAsyncWork,
|
||||
loadHtml,
|
||||
loadScript,
|
||||
triggerDomContentLoaded
|
||||
} from "./helpers/browser.js";
|
||||
|
||||
async function setupOptions(overrides = {}) {
|
||||
loadHtml("options.html");
|
||||
globalThis.chrome = createChromeMock(overrides);
|
||||
window.chrome = globalThis.chrome;
|
||||
globalThis.fetch = vi.fn();
|
||||
loadScript("shared/controller-utils.js");
|
||||
loadScript("shared/key-bindings.js");
|
||||
loadScript("shared/popup-controls.js");
|
||||
loadScript("ui-icons.js");
|
||||
loadScript("lucide-client.js");
|
||||
loadScript("options.js");
|
||||
triggerDomContentLoaded();
|
||||
await flushAsyncWork();
|
||||
return globalThis.chrome;
|
||||
}
|
||||
|
||||
describe("options page", () => {
|
||||
it("restores stored settings, custom shortcuts, and site rules", async () => {
|
||||
await setupOptions({
|
||||
manifestVersion: "5.1.7.0",
|
||||
sync: {
|
||||
rememberSpeed: true,
|
||||
enabled: false,
|
||||
popupMatchHoverControls: false,
|
||||
popupControllerButtons: ["rewind", "settings", "advance", "advance"],
|
||||
keyBindings: [
|
||||
{ action: "display", code: "KeyV", value: 0, predefined: true },
|
||||
{ action: "pause", code: "KeyQ", value: 0, predefined: false }
|
||||
],
|
||||
siteRules: [
|
||||
{
|
||||
pattern: "youtube.com",
|
||||
enabled: true,
|
||||
showPopupControlBar: false,
|
||||
popupControllerButtons: ["advance", "settings", "advance"]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
expect(document.getElementById("app-version").textContent).toBe("5.1.7.0");
|
||||
expect(document.getElementById("rememberSpeed").checked).toBe(true);
|
||||
expect(document.getElementById("enabled").checked).toBe(false);
|
||||
expect(document.querySelector('.shortcut-row[data-action="pause"]')).not.toBe(
|
||||
null
|
||||
);
|
||||
expect(document.getElementById("siteRulesContainer").children.length).toBe(
|
||||
1
|
||||
);
|
||||
expect(globalThis.getPopupControlBarOrder()).toEqual(["rewind", "advance"]);
|
||||
});
|
||||
|
||||
it("validates site rule regexes before saving", async () => {
|
||||
const chrome = await setupOptions();
|
||||
chrome.storage.sync.set.mockClear();
|
||||
globalThis.createSiteRule(null);
|
||||
const rule = document.querySelector(".site-rule");
|
||||
rule.querySelector(".site-pattern").value = "/(/";
|
||||
|
||||
globalThis.save_options();
|
||||
|
||||
expect(document.getElementById("status").textContent).toContain(
|
||||
"Invalid site rule regex"
|
||||
);
|
||||
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows a more-menu trigger for collapsed site rules and a collapse trigger when open", async () => {
|
||||
await setupOptions({ sync: { siteRules: [] } });
|
||||
|
||||
globalThis.createSiteRule({ pattern: "youtube.com" });
|
||||
|
||||
const rule = document.getElementById("siteRulesContainer").lastElementChild;
|
||||
const toggle = rule.querySelector(".toggle-site-rule");
|
||||
const body = rule.querySelector(".site-rule-body");
|
||||
|
||||
expect(rule.classList.contains("collapsed")).toBe(true);
|
||||
expect(body.style.display).toBe("none");
|
||||
expect(toggle.getAttribute("aria-expanded")).toBe("false");
|
||||
expect(toggle.getAttribute("aria-label")).toBe("Expand site rule");
|
||||
expect(toggle.querySelector("svg")).not.toBeNull();
|
||||
|
||||
globalThis.setSiteRuleExpandedState(rule, true);
|
||||
|
||||
expect(rule.classList.contains("collapsed")).toBe(false);
|
||||
expect(body.style.display).toBe("block");
|
||||
expect(toggle.getAttribute("aria-expanded")).toBe("true");
|
||||
expect(toggle.getAttribute("aria-label")).toBe("Collapse site rule");
|
||||
});
|
||||
|
||||
it("keeps site override settings visible but disabled until enabled", async () => {
|
||||
await setupOptions({ sync: { siteRules: [] } });
|
||||
|
||||
globalThis.createSiteRule({ pattern: "youtube.com" });
|
||||
|
||||
const rule = document.getElementById("siteRulesContainer").lastElementChild;
|
||||
const playbackOverride = rule.querySelector(".override-playback");
|
||||
const playbackContainer = rule.querySelector(".site-playback-container");
|
||||
const rememberSpeed = rule.querySelector(".site-rememberSpeed");
|
||||
|
||||
expect(playbackContainer.classList.contains("site-override-disabled")).toBe(
|
||||
true
|
||||
);
|
||||
expect(rememberSpeed.disabled).toBe(true);
|
||||
|
||||
playbackOverride.checked = true;
|
||||
playbackOverride.dispatchEvent(
|
||||
new Event("change", {
|
||||
bubbles: true
|
||||
})
|
||||
);
|
||||
|
||||
expect(playbackContainer.classList.contains("site-override-disabled")).toBe(
|
||||
false
|
||||
);
|
||||
expect(rememberSpeed.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("saves normalized settings and sanitized popup/site-rule controls", async () => {
|
||||
const chrome = await setupOptions();
|
||||
|
||||
document.getElementById("rememberSpeed").checked = true;
|
||||
document.getElementById("hideWithControlsTimer").value = "20";
|
||||
document.getElementById("controllerOpacity").value = "0";
|
||||
document.getElementById("controllerMarginTop").value = "250";
|
||||
document.getElementById("controllerMarginBottom").value = "-4";
|
||||
document.getElementById("enableSubtitleNudge").checked = true;
|
||||
document.getElementById("subtitleNudgeInterval").value = "5";
|
||||
document.getElementById("popupMatchHoverControls").checked = false;
|
||||
document.getElementById("showPopupControlBar").checked = false;
|
||||
|
||||
globalThis.populatePopupControlBarEditor([
|
||||
"rewind",
|
||||
"settings",
|
||||
"faster",
|
||||
"faster"
|
||||
]);
|
||||
|
||||
globalThis.createSiteRule(null);
|
||||
const rule = document.querySelector(".site-rule");
|
||||
rule.querySelector(".site-pattern").value = "youtube.com";
|
||||
rule.querySelector(".override-playback").checked = true;
|
||||
rule.querySelector(".site-rememberSpeed").checked = true;
|
||||
rule.querySelector(".override-opacity").checked = true;
|
||||
rule.querySelector(".site-controllerOpacity").value = "0";
|
||||
rule.querySelector(".override-popup-controlbar").checked = true;
|
||||
rule.querySelector(".site-showPopupControlBar").checked = false;
|
||||
globalThis.populateControlBarZones(
|
||||
rule.querySelector(".site-popup-cb-active"),
|
||||
rule.querySelector(".site-popup-cb-available"),
|
||||
["advance", "settings", "advance"],
|
||||
function (id) {
|
||||
return id !== "settings";
|
||||
}
|
||||
);
|
||||
|
||||
globalThis.save_options();
|
||||
|
||||
expect(chrome.storage.sync.remove).toHaveBeenCalled();
|
||||
const savedSettings =
|
||||
chrome.storage.sync.set.mock.calls[
|
||||
chrome.storage.sync.set.mock.calls.length - 1
|
||||
][0];
|
||||
|
||||
expect(savedSettings.rememberSpeed).toBe(true);
|
||||
expect(savedSettings.hideWithControlsTimer).toBe(15);
|
||||
expect(savedSettings.controllerOpacity).toBe(0);
|
||||
expect(savedSettings.controllerMarginTop).toBe(200);
|
||||
expect(savedSettings.controllerMarginBottom).toBe(0);
|
||||
expect(savedSettings.subtitleNudgeInterval).toBe(10);
|
||||
expect(savedSettings.showPopupControlBar).toBe(false);
|
||||
expect(savedSettings.popupMatchHoverControls).toBe(false);
|
||||
expect(savedSettings.popupControllerButtons).toEqual(["rewind", "faster"]);
|
||||
expect(savedSettings.siteRules).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pattern: "youtube.com",
|
||||
rememberSpeed: true,
|
||||
controllerOpacity: 0,
|
||||
showPopupControlBar: false,
|
||||
popupControllerButtons: ["advance"]
|
||||
})
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,230 @@
|
||||
const { afterEach, beforeEach, describe, expect, it, vi } = require("vitest");
|
||||
const {
|
||||
createChromeMock,
|
||||
evaluateScript,
|
||||
fireDOMContentLoaded,
|
||||
flushAsyncWork,
|
||||
installCommonWindowMocks,
|
||||
loadHtmlFile
|
||||
} = require("./helpers/extension-test-utils");
|
||||
|
||||
function bootOptions(options) {
|
||||
const config = options || {};
|
||||
|
||||
loadHtmlFile("options.html");
|
||||
installCommonWindowMocks();
|
||||
|
||||
const chrome = createChromeMock({
|
||||
manifest: { version: "5.1.7.0" },
|
||||
syncData: config.syncData,
|
||||
localData: config.localData
|
||||
});
|
||||
|
||||
global.chrome = chrome;
|
||||
window.chrome = chrome;
|
||||
global.fetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
text: () => Promise.resolve("<svg></svg>")
|
||||
})
|
||||
);
|
||||
window.fetch = global.fetch;
|
||||
|
||||
evaluateScript("ui-icons.js");
|
||||
evaluateScript("lucide-client.js");
|
||||
evaluateScript("options.js");
|
||||
fireDOMContentLoaded();
|
||||
|
||||
return chrome;
|
||||
}
|
||||
|
||||
describe("options.js", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
delete global.chrome;
|
||||
delete global.fetch;
|
||||
});
|
||||
|
||||
it("restores saved settings, bindings, site rules, and popup bar order", async () => {
|
||||
bootOptions({
|
||||
syncData: {
|
||||
rememberSpeed: true,
|
||||
forceLastSavedSpeed: true,
|
||||
controllerLocation: "middle-right",
|
||||
controllerOpacity: 0.75,
|
||||
controllerMarginTop: 22,
|
||||
controllerMarginBottom: 14,
|
||||
popupMatchHoverControls: false,
|
||||
controllerButtons: ["rewind", "fast", "display"],
|
||||
popupControllerButtons: ["advance", "settings", "rewind", "advance"],
|
||||
keyBindings: [
|
||||
{ action: "display", code: "KeyV", value: 0, predefined: true },
|
||||
{ action: "pause", code: "KeyQ", value: 0, predefined: false }
|
||||
],
|
||||
siteRules: [
|
||||
{
|
||||
pattern: "youtube.com",
|
||||
enabled: false,
|
||||
controllerMarginTop: 12,
|
||||
popupControllerButtons: ["advance", "settings", "rewind"]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
await flushAsyncWork(3);
|
||||
|
||||
expect(document.getElementById("app-version").textContent).toBe("5.1.7.0");
|
||||
expect(document.getElementById("rememberSpeed").checked).toBe(true);
|
||||
expect(document.getElementById("forceLastSavedSpeed").checked).toBe(true);
|
||||
expect(document.getElementById("controllerLocation").value).toBe(
|
||||
"middle-right"
|
||||
);
|
||||
expect(document.getElementById("controllerOpacity").value).toBe("0.75");
|
||||
expect(document.getElementById("controllerMarginTop").value).toBe("22");
|
||||
expect(document.getElementById("popupMatchHoverControls").checked).toBe(false);
|
||||
expect(
|
||||
document.getElementById("popupCbEditorWrap").classList.contains(
|
||||
"cb-editor-disabled"
|
||||
)
|
||||
).toBe(false);
|
||||
|
||||
const popupButtons = Array.from(
|
||||
document.querySelectorAll("#popupControlBarActive .cb-block")
|
||||
).map((block) => block.dataset.buttonId);
|
||||
expect(popupButtons).toEqual(["advance", "rewind"]);
|
||||
|
||||
expect(
|
||||
document.querySelector('.shortcut-row.customs[data-action="pause"]')
|
||||
).not.toBeNull();
|
||||
expect(document.querySelectorAll(".site-rule")).toHaveLength(1);
|
||||
expect(document.querySelector(".site-rule .site-enabled").checked).toBe(false);
|
||||
});
|
||||
|
||||
it("saves normalized settings and site rule overrides", async () => {
|
||||
const chrome = bootOptions();
|
||||
await flushAsyncWork(3);
|
||||
|
||||
document.getElementById("rememberSpeed").checked = true;
|
||||
document.getElementById("hideWithControlsTimer").value = "99";
|
||||
document.getElementById("controllerLocation").value = "bottom-left";
|
||||
document.getElementById("controllerOpacity").value = "0.65";
|
||||
document.getElementById("controllerMarginTop").value = "250";
|
||||
document.getElementById("controllerMarginBottom").value = "-5";
|
||||
document.getElementById("popupMatchHoverControls").checked = false;
|
||||
document.getElementById("showPopupControlBar").checked = true;
|
||||
|
||||
window.populatePopupControlBarEditor(["advance", "settings", "rewind"]);
|
||||
|
||||
window.createSiteRule({ pattern: "youtube.com" });
|
||||
const ruleEl = document.querySelector(".site-rule");
|
||||
ruleEl.querySelector(".override-placement").checked = true;
|
||||
ruleEl.querySelector(".site-controllerLocation").value = "top-right";
|
||||
ruleEl.querySelector(".site-controllerMarginTop").value = "300";
|
||||
ruleEl.querySelector(".site-controllerMarginBottom").value = "-10";
|
||||
|
||||
ruleEl.querySelector(".override-autohide").checked = true;
|
||||
ruleEl.querySelector(".site-hideWithControls").checked = true;
|
||||
ruleEl.querySelector(".site-hideWithControlsTimer").value = "0";
|
||||
|
||||
ruleEl.querySelector(".override-popup-controlbar").checked = true;
|
||||
ruleEl.querySelector(".site-showPopupControlBar").checked = false;
|
||||
window.populateControlBarZones(
|
||||
ruleEl.querySelector(".site-popup-cb-active"),
|
||||
ruleEl.querySelector(".site-popup-cb-available"),
|
||||
["advance", "settings", "rewind"],
|
||||
function (id) {
|
||||
return id !== "settings";
|
||||
}
|
||||
);
|
||||
|
||||
window.save_options();
|
||||
|
||||
expect(chrome.storage.sync.remove).toHaveBeenCalledWith(
|
||||
[
|
||||
"resetSpeed",
|
||||
"speedStep",
|
||||
"fastSpeed",
|
||||
"rewindTime",
|
||||
"advanceTime",
|
||||
"resetKeyCode",
|
||||
"slowerKeyCode",
|
||||
"fasterKeyCode",
|
||||
"rewindKeyCode",
|
||||
"advanceKeyCode",
|
||||
"fastKeyCode",
|
||||
"blacklist"
|
||||
],
|
||||
expect.any(Function)
|
||||
);
|
||||
|
||||
const savedSettings = chrome.storage.sync.set.mock.calls.at(-1)[0];
|
||||
expect(savedSettings.rememberSpeed).toBe(true);
|
||||
expect(savedSettings.hideWithControlsTimer).toBe(15);
|
||||
expect(savedSettings.controllerLocation).toBe("bottom-left");
|
||||
expect(savedSettings.controllerMarginTop).toBe(200);
|
||||
expect(savedSettings.controllerMarginBottom).toBe(0);
|
||||
expect(savedSettings.popupControllerButtons).toEqual(["advance", "rewind"]);
|
||||
expect(savedSettings.siteRules).toEqual([
|
||||
{
|
||||
pattern: "youtube.com",
|
||||
enabled: true,
|
||||
controllerLocation: "top-right",
|
||||
controllerMarginTop: 200,
|
||||
controllerMarginBottom: 0,
|
||||
hideWithControls: true,
|
||||
hideWithControlsTimer: 0.1,
|
||||
showPopupControlBar: false,
|
||||
popupControllerButtons: ["advance", "rewind"]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("blocks save when a site rule regex is invalid", async () => {
|
||||
const chrome = bootOptions();
|
||||
await flushAsyncWork(3);
|
||||
|
||||
window.createSiteRule({ pattern: "/[abc/" });
|
||||
window.save_options();
|
||||
|
||||
expect(document.getElementById("status").textContent).toContain(
|
||||
"Invalid site rule regex"
|
||||
);
|
||||
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("adds shortcuts from the selector and records key input states", async () => {
|
||||
bootOptions();
|
||||
await flushAsyncWork(3);
|
||||
|
||||
const selector = document.getElementById("addShortcutSelector");
|
||||
selector.value = "pause";
|
||||
selector.dispatchEvent(new window.Event("change", { bubbles: true }));
|
||||
|
||||
const row = document.querySelector('.shortcut-row.customs[data-action="pause"]');
|
||||
expect(row).not.toBeNull();
|
||||
|
||||
const keyInput = row.querySelector(".customKey");
|
||||
keyInput.dispatchEvent(
|
||||
new window.KeyboardEvent("keydown", {
|
||||
key: "q",
|
||||
code: "KeyQ",
|
||||
bubbles: true
|
||||
})
|
||||
);
|
||||
expect(keyInput.vscBinding.code).toBe("KeyQ");
|
||||
expect(keyInput.value).toBe("Q");
|
||||
|
||||
keyInput.dispatchEvent(
|
||||
new window.KeyboardEvent("keydown", {
|
||||
key: "Escape",
|
||||
code: "Escape",
|
||||
bubbles: true
|
||||
})
|
||||
);
|
||||
expect(keyInput.vscBinding.disabled).toBe(true);
|
||||
expect(selector.disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
createChromeMock,
|
||||
flushAsyncWork,
|
||||
loadHtml,
|
||||
loadScript,
|
||||
triggerDomContentLoaded
|
||||
} from "./helpers/browser.js";
|
||||
|
||||
async function setupPopup(overrides = {}) {
|
||||
loadHtml("popup.html");
|
||||
globalThis.chrome = createChromeMock(overrides);
|
||||
window.chrome = globalThis.chrome;
|
||||
loadScript("shared/site-rules.js");
|
||||
loadScript("shared/popup-controls.js");
|
||||
loadScript("ui-icons.js");
|
||||
loadScript("popup.js");
|
||||
triggerDomContentLoaded();
|
||||
await flushAsyncWork();
|
||||
return globalThis.chrome;
|
||||
}
|
||||
|
||||
describe("popup UI", () => {
|
||||
it("renders version, builds controls, and prefers the active frame speed", async () => {
|
||||
await setupPopup({
|
||||
manifestVersion: "5.1.7.0",
|
||||
executeScriptResponse: [
|
||||
{ speed: 1.1, preferred: false },
|
||||
{ speed: 1.75, preferred: true }
|
||||
]
|
||||
});
|
||||
|
||||
expect(document.getElementById("app-version").innerText).toBe("5.1.7.0");
|
||||
expect(document.getElementById("popupSpeed").textContent).toBe("1.75");
|
||||
expect(
|
||||
document.querySelectorAll("#popupControlBar button").length
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("shows disabled state for a matching site rule", async () => {
|
||||
await setupPopup({
|
||||
sync: {
|
||||
enabled: true,
|
||||
siteRules: [{ pattern: "example.com", enabled: false }]
|
||||
}
|
||||
});
|
||||
|
||||
expect(document.getElementById("status").innerText).toBe(
|
||||
"Speeder is disabled for this site."
|
||||
);
|
||||
expect(document.getElementById("popupSpeed").textContent).toBe("1.00");
|
||||
expect(document.getElementById("popupControlBar").style.display).toBe(
|
||||
"none"
|
||||
);
|
||||
});
|
||||
|
||||
it("toggles enabled state and updates the browser action icons", async () => {
|
||||
const chrome = await setupPopup();
|
||||
chrome.storage.sync.set.mockClear();
|
||||
chrome.browserAction.setIcon.mockClear();
|
||||
|
||||
document.getElementById("disable").click();
|
||||
|
||||
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
|
||||
{ enabled: false },
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(document.getElementById("enable").classList.contains("hide")).toBe(
|
||||
false
|
||||
);
|
||||
expect(chrome.browserAction.setIcon).toHaveBeenCalledWith({
|
||||
path: {
|
||||
19: "icons/icon19_disabled.png",
|
||||
38: "icons/icon38_disabled.png",
|
||||
48: "icons/icon48_disabled.png"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("handles refresh responses for unsupported and successful pages", async () => {
|
||||
vi.useFakeTimers();
|
||||
const chrome = await setupPopup();
|
||||
let response = null;
|
||||
chrome.tabs.sendMessage.mockImplementation((tabId, message, callback) => {
|
||||
if (message.action === "rescan_page") {
|
||||
callback(response);
|
||||
return;
|
||||
}
|
||||
callback({ speed: 1.25 });
|
||||
});
|
||||
|
||||
document.getElementById("refresh").click();
|
||||
expect(document.getElementById("status").innerText).toBe(
|
||||
"Cannot run on this page."
|
||||
);
|
||||
|
||||
response = { status: "complete" };
|
||||
document.getElementById("refresh").click();
|
||||
expect(document.getElementById("status").innerText).toBe(
|
||||
"Scan complete. Closing..."
|
||||
);
|
||||
vi.advanceTimersByTime(500);
|
||||
expect(window.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("dispatches popup control bar actions back to the active tab", async () => {
|
||||
const chrome = await setupPopup();
|
||||
chrome.tabs.sendMessage.mockClear();
|
||||
chrome.tabs.executeScript.mockClear();
|
||||
|
||||
document.querySelector("#popupControlBar button").click();
|
||||
|
||||
expect(chrome.tabs.sendMessage).toHaveBeenCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
action: "run_action"
|
||||
}),
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(chrome.tabs.executeScript).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
const { afterEach, beforeEach, describe, expect, it, vi } = require("vitest");
|
||||
const {
|
||||
createChromeMock,
|
||||
evaluateScript,
|
||||
fireDOMContentLoaded,
|
||||
flushAsyncWork,
|
||||
installCommonWindowMocks,
|
||||
loadHtmlFile
|
||||
} = require("./helpers/extension-test-utils");
|
||||
|
||||
function bootPopup(options) {
|
||||
const config = options || {};
|
||||
|
||||
loadHtmlFile("popup.html");
|
||||
installCommonWindowMocks();
|
||||
|
||||
const chrome = createChromeMock({
|
||||
manifest: { version: "9.9.9-test" },
|
||||
syncData: config.syncData,
|
||||
localData: config.localData,
|
||||
tabsQueryResult: [
|
||||
config.activeTab || { id: 99, active: true, url: "https://example.com/" }
|
||||
]
|
||||
});
|
||||
|
||||
chrome.tabs.executeScript.mockImplementation(
|
||||
config.executeScriptImpl ||
|
||||
((tabId, details, callback) => {
|
||||
if (callback) callback([{ speed: 1.0, preferred: true }]);
|
||||
})
|
||||
);
|
||||
chrome.tabs.sendMessage.mockImplementation(
|
||||
config.sendMessageImpl ||
|
||||
((tabId, message, callback) => {
|
||||
if (message.action === "get_speed") {
|
||||
callback({ speed: 1.0 });
|
||||
return;
|
||||
}
|
||||
if (message.action === "rescan_page") {
|
||||
callback({ status: "complete" });
|
||||
return;
|
||||
}
|
||||
callback({ speed: 1.0 });
|
||||
})
|
||||
);
|
||||
|
||||
global.chrome = chrome;
|
||||
window.chrome = chrome;
|
||||
|
||||
evaluateScript("ui-icons.js");
|
||||
evaluateScript("popup.js");
|
||||
fireDOMContentLoaded();
|
||||
|
||||
return chrome;
|
||||
}
|
||||
|
||||
describe("popup.js", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
delete global.chrome;
|
||||
});
|
||||
|
||||
it("renders the popup disabled state when a site rule disables Speeder", async () => {
|
||||
bootPopup({
|
||||
syncData: {
|
||||
enabled: true,
|
||||
siteRules: [
|
||||
{
|
||||
pattern: "youtube.com",
|
||||
enabled: false
|
||||
}
|
||||
]
|
||||
},
|
||||
activeTab: {
|
||||
id: 10,
|
||||
active: true,
|
||||
url: "https://www.youtube.com/watch?v=abc123"
|
||||
}
|
||||
});
|
||||
|
||||
await flushAsyncWork();
|
||||
|
||||
expect(document.querySelector("#app-version").textContent).toBe("9.9.9-test");
|
||||
expect(document.querySelector("#status").textContent).toContain(
|
||||
"disabled for this site"
|
||||
);
|
||||
expect(document.querySelector("#popupSpeed").textContent).toBe("1.00");
|
||||
expect(document.querySelector("#popupControlBar").style.display).toBe("none");
|
||||
});
|
||||
|
||||
it("builds sanitized popup buttons and refreshes speed after an action", async () => {
|
||||
const chrome = bootPopup({
|
||||
syncData: {
|
||||
enabled: true,
|
||||
controllerButtons: ["faster", "settings", "rewind", "faster"],
|
||||
popupMatchHoverControls: true
|
||||
}
|
||||
});
|
||||
|
||||
chrome.tabs.executeScript
|
||||
.mockImplementationOnce((tabId, details, callback) => {
|
||||
callback([
|
||||
{ speed: 1.25, preferred: false },
|
||||
{ speed: 1.5, preferred: true }
|
||||
]);
|
||||
})
|
||||
.mockImplementationOnce((tabId, details, callback) => {
|
||||
callback([{ speed: 1.75, preferred: true }]);
|
||||
});
|
||||
|
||||
chrome.tabs.sendMessage.mockImplementation((tabId, message, callback) => {
|
||||
if (message.action === "run_action") {
|
||||
callback({ speed: 1.75 });
|
||||
return;
|
||||
}
|
||||
callback({ speed: 1.0 });
|
||||
});
|
||||
|
||||
document.dispatchEvent(new window.Event("DOMContentLoaded"));
|
||||
await flushAsyncWork();
|
||||
|
||||
const buttons = Array.from(
|
||||
document.querySelectorAll("#popupControlBar button[data-action]")
|
||||
).map((button) => button.dataset.action);
|
||||
expect(buttons).toEqual(["faster", "rewind"]);
|
||||
expect(document.querySelector("#popupSpeed").textContent).toBe("1.50");
|
||||
|
||||
document.querySelector('#popupControlBar button[data-action="faster"]').click();
|
||||
await flushAsyncWork();
|
||||
|
||||
expect(chrome.tabs.sendMessage).toHaveBeenCalledWith(
|
||||
99,
|
||||
{ action: "run_action", actionName: "faster" },
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(document.querySelector("#popupSpeed").textContent).toBe("1.75");
|
||||
});
|
||||
|
||||
it("toggles enablement and closes after a successful refresh", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const chrome = bootPopup({
|
||||
syncData: {
|
||||
enabled: false
|
||||
}
|
||||
});
|
||||
|
||||
await flushAsyncWork();
|
||||
|
||||
expect(document.querySelector("#enable").classList.contains("hide")).toBe(false);
|
||||
expect(document.querySelector("#disable").classList.contains("hide")).toBe(true);
|
||||
|
||||
document.querySelector("#enable").click();
|
||||
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
|
||||
{ enabled: true },
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(chrome.browserAction.setIcon).toHaveBeenCalledWith({
|
||||
path: {
|
||||
19: "icons/icon19.png",
|
||||
38: "icons/icon38.png",
|
||||
48: "icons/icon48.png"
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector("#refresh").click();
|
||||
expect(document.querySelector("#status").textContent).toContain("Closing");
|
||||
|
||||
vi.advanceTimersByTime(500);
|
||||
expect(window.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { afterEach, beforeEach, vi } from "vitest";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.open = vi.fn();
|
||||
window.close = vi.fn();
|
||||
}
|
||||
|
||||
globalThis.requestAnimationFrame = (callback) => setTimeout(callback, 0);
|
||||
globalThis.cancelAnimationFrame = (id) => clearTimeout(id);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
delete globalThis.SpeederShared;
|
||||
delete globalThis.restore_options;
|
||||
if (typeof document !== "undefined") {
|
||||
document.head.innerHTML = "";
|
||||
document.body.innerHTML = "";
|
||||
}
|
||||
delete globalThis.chrome;
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
import controllerUtils from "../shared/controller-utils.js";
|
||||
import importExportUtils from "../shared/import-export.js";
|
||||
import keyBindingUtils from "../shared/key-bindings.js";
|
||||
import popupControls from "../shared/popup-controls.js";
|
||||
import siteRules from "../shared/site-rules.js";
|
||||
|
||||
describe("shared helpers", () => {
|
||||
it("matches site rules and skips invalid regex patterns", () => {
|
||||
const literalRule = { pattern: "example.com/watch" };
|
||||
const regexRule = { pattern: "/youtube\\.com\\/watch/i" };
|
||||
|
||||
expect(
|
||||
siteRules.matchSiteRule("https://example.com/watch?v=1", [literalRule])
|
||||
).toBe(literalRule);
|
||||
expect(
|
||||
siteRules.matchSiteRule("https://www.youtube.com/watch?v=2", [regexRule])
|
||||
).toBe(regexRule);
|
||||
expect(
|
||||
siteRules.matchSiteRule("https://www.youtube.com/shorts/3", [
|
||||
{ pattern: "/(/" },
|
||||
regexRule
|
||||
])
|
||||
).toBeNull();
|
||||
expect(siteRules.isSiteRuleDisabled({ enabled: false })).toBe(true);
|
||||
});
|
||||
|
||||
it("sanitizes and resolves popup button orders", () => {
|
||||
const controllerButtonDefs = {
|
||||
rewind: {},
|
||||
faster: {},
|
||||
advance: {},
|
||||
display: {},
|
||||
settings: {}
|
||||
};
|
||||
|
||||
expect(
|
||||
popupControls.sanitizeButtonOrder(
|
||||
["rewind", "settings", "rewind", "faster", "missing"],
|
||||
controllerButtonDefs,
|
||||
new Set(["settings"])
|
||||
)
|
||||
).toEqual(["rewind", "faster"]);
|
||||
|
||||
expect(
|
||||
popupControls.resolvePopupButtons(
|
||||
{
|
||||
popupMatchHoverControls: true,
|
||||
controllerButtons: ["advance", "display"],
|
||||
popupControllerButtons: ["rewind"]
|
||||
},
|
||||
{ controllerButtons: ["faster", "advance"] },
|
||||
{
|
||||
controllerButtonDefs,
|
||||
defaultButtons: ["rewind", "display"],
|
||||
excludedIds: ["settings"]
|
||||
}
|
||||
)
|
||||
).toEqual(["faster", "advance"]);
|
||||
|
||||
expect(
|
||||
popupControls.resolvePopupButtons(
|
||||
{
|
||||
popupMatchHoverControls: false,
|
||||
popupControllerButtons: ["rewind", "display"]
|
||||
},
|
||||
{ popupControllerButtons: ["advance", "settings", "advance"] },
|
||||
{
|
||||
controllerButtonDefs,
|
||||
defaultButtons: ["rewind", "display"],
|
||||
excludedIds: ["settings"]
|
||||
}
|
||||
)
|
||||
).toEqual(["advance"]);
|
||||
});
|
||||
|
||||
it("normalizes controller locations and margins", () => {
|
||||
expect(controllerUtils.normalizeControllerLocation("top-right")).toBe(
|
||||
"top-right"
|
||||
);
|
||||
expect(controllerUtils.normalizeControllerLocation("bogus")).toBe(
|
||||
controllerUtils.defaultControllerLocation
|
||||
);
|
||||
expect(controllerUtils.clampControllerMarginPx(300, 65)).toBe(200);
|
||||
expect(controllerUtils.clampControllerMarginPx(-5, 65)).toBe(0);
|
||||
expect(controllerUtils.getNextControllerLocation("top-left")).toBe(
|
||||
"top-center"
|
||||
);
|
||||
});
|
||||
|
||||
it("infers key binding codes from legacy formats", () => {
|
||||
expect(keyBindingUtils.normalizeBindingKey("a")).toBe("A");
|
||||
expect(keyBindingUtils.normalizeBindingKey("Esc")).toBe("Escape");
|
||||
expect(keyBindingUtils.legacyBindingKeyToCode(" ")).toBe("Space");
|
||||
expect(keyBindingUtils.legacyKeyCodeToCode(90)).toBe("KeyZ");
|
||||
expect(keyBindingUtils.inferBindingCode({ key: "x" }, null)).toBe("KeyX");
|
||||
expect(keyBindingUtils.inferBindingCode({ keyCode: 107 }, null)).toBe(
|
||||
"NumpadAdd"
|
||||
);
|
||||
expect(keyBindingUtils.getLegacyKeyCode({ key: 65 })).toBe(65);
|
||||
});
|
||||
|
||||
it("builds and parses import/export payloads", () => {
|
||||
expect(
|
||||
importExportUtils.generateBackupFilename(new Date(2026, 3, 4, 8, 9, 10))
|
||||
).toBe("speeder-backup_2026-04-04_08.09.10.json");
|
||||
|
||||
expect(
|
||||
importExportUtils.buildBackupPayload(
|
||||
{ rememberSpeed: true },
|
||||
{ customButtonIcons: {} },
|
||||
"2026-04-04T08:09:10Z"
|
||||
)
|
||||
).toEqual({
|
||||
version: "1.1",
|
||||
exportDate: "2026-04-04T08:09:10.000Z",
|
||||
settings: { rememberSpeed: true },
|
||||
localSettings: { customButtonIcons: {} }
|
||||
});
|
||||
|
||||
expect(
|
||||
importExportUtils.extractImportSettings({
|
||||
settings: { rememberSpeed: true },
|
||||
localSettings: { customButtonIcons: {} }
|
||||
})
|
||||
).toEqual({
|
||||
isWrappedBackup: true,
|
||||
settings: { rememberSpeed: true },
|
||||
localSettings: { customButtonIcons: {} }
|
||||
});
|
||||
|
||||
expect(
|
||||
importExportUtils.parseImportText(
|
||||
JSON.stringify({ rememberSpeed: true, keyBindings: [] })
|
||||
)
|
||||
).toEqual({
|
||||
isWrappedBackup: false,
|
||||
settings: { rememberSpeed: true, keyBindings: [] },
|
||||
localSettings: null
|
||||
});
|
||||
|
||||
expect(
|
||||
importExportUtils.extractImportSettings({ enabled: true })
|
||||
).toEqual({
|
||||
isWrappedBackup: false,
|
||||
settings: { enabled: true },
|
||||
localSettings: null
|
||||
});
|
||||
|
||||
expect(importExportUtils.isRecognizedRawSettingsObject({ wat: true })).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,13 @@ var vscUiIconPaths = {
|
||||
slower: '<line x1="5" y1="12" x2="19" y2="12"/>',
|
||||
faster:
|
||||
'<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>',
|
||||
softer:
|
||||
'<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="16" y1="12" x2="22" y2="12"/>',
|
||||
louder:
|
||||
'<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="17" y1="9" x2="17" y2="15"/><line x1="14" y1="12" x2="20" y2="12"/>',
|
||||
moreHorizontal:
|
||||
'<circle cx="6" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="18" cy="12" r="1.5"/>',
|
||||
chevronUp: '<path d="m18 15-6-6-6 6"/>',
|
||||
display:
|
||||
'<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
|
||||
fast: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>',
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
const { defineConfig } = require("vitest/config");
|
||||
|
||||
module.exports = defineConfig({
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
clearMocks: true,
|
||||
globals: true,
|
||||
restoreMocks: true,
|
||||
include: ["tests/**/*.test.js"],
|
||||
setupFiles: ["./tests/setup.js"]
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user