mirror of
https://github.com/SoPat712/videospeed.git
synced 2026-04-23 05:12:37 -04:00
Compare commits
32 Commits
v5.1.7.0-beta.1
...
beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
e652850dfa
|
|||
|
bfe20a84dd
|
|||
|
46a8a62110
|
|||
|
de582ae157
|
|||
|
eb64de6ea3
|
|||
|
eab6d10a19
|
|||
|
76f6bd1a1e
|
|||
|
29a9a1d07f
|
|||
|
4d37b4f570
|
|||
|
9d41e6ceeb
|
|||
|
6d10a569d9
|
|||
|
5f63718d62
|
|||
|
eaffab1f27
|
|||
|
d01c01a216
|
|||
|
0b6bc5d0a0
|
|||
|
3f5983685f
|
|||
|
41d89e0993
|
|||
|
a424dea5ca
|
|||
|
ab7ae99807
|
|||
|
5d47c511be
|
|||
| 7713ba5bad | |||
|
ce0b28de4f
|
|||
|
a2b041e225
|
|||
|
4f87aadfd7
|
|||
|
8456111bf0
|
|||
|
6efe92a036
|
|||
|
0cb13905ff
|
|||
|
f32d1b3f71
|
|||
|
e6c56bcecb
|
|||
|
a7a0aafd68
|
|||
|
3cf1a4acd1
|
|||
|
a9956831c4
|
+1
-1
@@ -10,6 +10,6 @@ liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
buy_me_a_coffee: treeman183
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<svg width="241" height="194" viewBox="0 0 241 194" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="kofiSymbolMask" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="-1" y="0" width="242" height="194">
|
||||
<path d="M240.469 0.958984H-0.00585938V193.918H240.469V0.958984Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#kofiSymbolMask)">
|
||||
<path d="M96.1344 193.911C61.1312 193.911 32.6597 178.256 15.9721 149.829C1.19788 124.912 -0.00585938 97.9229 -0.00585938 67.7662C-0.00585938 49.8876 5.37293 34.3215 15.5413 22.7466C24.8861 12.1157 38.1271 5.22907 52.8317 3.35378C70.2858 1.14271 91.9848 0.958984 114.545 0.958984C151.259 0.958984 161.63 1.4088 176.075 2.85328C195.29 4.76026 211.458 11.932 222.824 23.5955C234.368 35.4428 240.469 51.2624 240.469 69.3627V72.9994C240.469 103.885 219.821 129.733 191.046 136.759C188.898 141.827 186.237 146.871 183.089 151.837L183.006 151.964C172.869 167.632 149.042 193.918 103.401 193.918H96.1281L96.1344 193.911Z" fill="white"/>
|
||||
<path d="M174.568 17.9772C160.927 16.6151 151.38 16.1589 114.552 16.1589C90.908 16.1589 70.9008 16.387 54.7644 18.4334C33.3949 21.164 15.2058 37.5285 15.2058 67.7674C15.2058 98.0066 16.796 121.422 29.0741 142.107C42.9425 165.751 66.1302 178.707 96.1412 178.707H103.414C140.242 178.707 160.25 159.156 170.253 143.698C174.574 136.874 177.754 130.058 179.801 123.234C205.947 120.96 225.27 99.3624 225.27 72.9941V69.3577C225.27 40.9432 206.631 21.164 174.574 17.9772H174.568Z" fill="white"/>
|
||||
<path d="M15.1975 67.7674C15.1975 37.5285 33.3866 21.164 54.7559 18.4334C70.8987 16.387 90.906 16.1589 114.544 16.1589C151.372 16.1589 160.919 16.6151 174.559 17.9772C206.617 21.1576 225.255 40.937 225.255 69.3577V72.9941C225.255 99.3687 205.932 120.966 179.786 123.234C177.74 130.058 174.559 136.874 170.238 143.698C160.235 159.156 140.228 178.707 103.4 178.707H96.1264C66.1155 178.707 42.9277 165.751 29.0595 142.107C16.7814 121.422 15.1912 98.4563 15.1912 67.7674" fill="#202020"/>
|
||||
<path d="M32.2469 67.9899C32.2469 97.3168 34.0654 116.184 43.6127 133.689C54.5225 153.924 74.3018 161.653 96.8117 161.653H103.857C133.411 161.653 147.736 147.329 155.693 134.829C159.558 128.462 162.966 121.417 164.784 112.547L166.147 106.864H174.332C192.521 106.864 208.208 92.09 208.208 73.2166V69.8082C208.208 48.6669 195.024 37.5228 172.058 34.7987C159.102 33.6646 151.372 33.2084 114.538 33.2084C89.7602 33.2084 72.0272 33.4364 58.6152 35.4828C39.7483 38.2134 32.2407 48.8951 32.2407 67.9899" fill="white"/>
|
||||
<path d="M166.158 83.6801C166.158 86.4107 168.204 88.4572 171.841 88.4572C183.435 88.4572 189.802 81.8619 189.802 70.9523C189.802 60.0427 183.435 53.2195 171.841 53.2195C168.204 53.2195 166.158 55.2657 166.158 57.9963V83.6866V83.6801Z" fill="#202020"/>
|
||||
<path d="M54.5321 82.3198C54.5321 95.732 62.0332 107.326 71.5807 116.424C77.9478 122.562 87.9515 128.93 94.7685 133.022C96.8147 134.157 98.8611 134.841 101.136 134.841C103.866 134.841 106.134 134.157 107.959 133.022C114.782 128.93 124.779 122.562 130.919 116.424C140.694 107.332 148.195 95.7383 148.195 82.3198C148.195 67.7673 137.286 54.8115 121.599 54.8115C112.28 54.8115 105.912 59.5882 101.136 66.1772C96.8147 59.582 90.2259 54.8115 80.9001 54.8115C64.9855 54.8115 54.5256 67.7673 54.5256 82.3198" fill="#FF5A16"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
+77
-140
@@ -1,129 +1,35 @@
|
||||
// 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
|
||||
};
|
||||
chrome.storage.local.get(null, function (localStorage) {
|
||||
const backup = importExportUtils.buildBackupPayload(
|
||||
storage,
|
||||
importExportUtils.filterLocalSettingsForExport(localStorage),
|
||||
new Date()
|
||||
);
|
||||
|
||||
if (Object.keys(localSettings).length > 0) {
|
||||
backup.localSettings = localSettings;
|
||||
}
|
||||
const dataStr = JSON.stringify(backup, null, 2);
|
||||
const blob = new Blob([dataStr], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const dataStr = JSON.stringify(backup, null, 2);
|
||||
const blob = new Blob([dataStr], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = generateBackupFilename();
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = generateBackupFilename();
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
showStatus("Settings exported successfully");
|
||||
}
|
||||
);
|
||||
showStatus("Settings exported successfully");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -139,44 +45,75 @@ 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 afterLocalImport() {
|
||||
persistManagedSyncSettings(settingsToImport, function (error) {
|
||||
if (error) {
|
||||
function importLocalSettings(callback) {
|
||||
if (parsedBackup.isWrappedBackup !== true) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.local.clear(function () {
|
||||
if (chrome.runtime.lastError) {
|
||||
showStatus(
|
||||
"Error: Failed to save imported settings - " + error.message,
|
||||
"Error: Failed to clear local extension data - " +
|
||||
chrome.runtime.lastError.message,
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
showStatus("Settings imported successfully. Reloading...");
|
||||
setTimeout(function () {
|
||||
if (typeof restore_options === "function") {
|
||||
restore_options();
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
}, 500);
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
replaceImportableLocalSettings(localToImport, afterLocalImport);
|
||||
function afterLocalImport() {
|
||||
chrome.storage.sync.clear(function () {
|
||||
chrome.storage.sync.set(settingsToImport, function () {
|
||||
if (chrome.runtime.lastError) {
|
||||
showStatus(
|
||||
"Error: Failed to save imported settings - " +
|
||||
chrome.runtime.lastError.message,
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
showStatus("Settings imported successfully. Reloading...");
|
||||
setTimeout(function () {
|
||||
if (typeof restore_options === "function") {
|
||||
restore_options();
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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,20 +61,23 @@ 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 = [
|
||||
"top-left",
|
||||
"top-center",
|
||||
"top-right",
|
||||
"middle-right",
|
||||
"bottom-right",
|
||||
"bottom-center",
|
||||
"bottom-left",
|
||||
"middle-left"
|
||||
];
|
||||
var defaultControllerLocation = controllerLocations[0];
|
||||
var controllerLocations = Array.isArray(controllerUtils.controllerLocations)
|
||||
? controllerUtils.controllerLocations.slice()
|
||||
: [
|
||||
"top-left",
|
||||
"top-center",
|
||||
"top-right",
|
||||
"middle-right",
|
||||
"bottom-right",
|
||||
"bottom-center",
|
||||
"bottom-left",
|
||||
"middle-left"
|
||||
];
|
||||
var defaultControllerLocation =
|
||||
controllerUtils.defaultControllerLocation || controllerLocations[0];
|
||||
var controllerLocationStyles = {
|
||||
"top-left": {
|
||||
top: "10px",
|
||||
@@ -123,76 +123,27 @@ var controllerLocationStyles = {
|
||||
|
||||
/* `label` fallback only when ui-icons has no path for the action. */
|
||||
var controllerButtonDefs = {
|
||||
rewind: { label: "", className: "rw" },
|
||||
slower: { label: "", className: "" },
|
||||
faster: { label: "", className: "" },
|
||||
advance: { label: "", className: "rw" },
|
||||
display: { label: "", className: "hideButton" },
|
||||
reset: { label: "\u21BB", className: "" },
|
||||
fast: { label: "", className: "" },
|
||||
settings: { label: "", className: "" },
|
||||
pause: { label: "", className: "" },
|
||||
muted: { label: "", className: "" },
|
||||
mark: { label: "", className: "" },
|
||||
jump: { label: "", className: "" }
|
||||
rewind: { label: "", className: "rw" },
|
||||
slower: { label: "", className: "" },
|
||||
faster: { label: "", className: "" },
|
||||
advance: { label: "", className: "rw" },
|
||||
display: { label: "", className: "hideButton" },
|
||||
reset: { label: "\u21BB", className: "" },
|
||||
fast: { label: "", className: "" },
|
||||
nudge: { label: "", className: "" },
|
||||
pause: { label: "", className: "" },
|
||||
muted: { label: "", className: "" },
|
||||
louder: { label: "", className: "" },
|
||||
softer: { label: "", className: "" },
|
||||
mark: { 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);
|
||||
}
|
||||
return {
|
||||
key: normalizeBindingKey(key),
|
||||
keyCode: keyCode,
|
||||
code: null,
|
||||
disabled: false
|
||||
};
|
||||
function legacyBindingKeyToCode(key) {
|
||||
return keyBindingUtils.legacyBindingKeyToCode(key);
|
||||
}
|
||||
|
||||
function normalizeStoredBinding(binding, fallbackKeyCode) {
|
||||
var fallbackBinding = legacyKeyCodeToBinding(fallbackKeyCode);
|
||||
if (!binding) return fallbackBinding;
|
||||
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 {
|
||||
code: fallbackCode,
|
||||
disabled: false,
|
||||
value: 0,
|
||||
force: "false",
|
||||
predefined: false
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1217,15 +969,11 @@ 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);
|
||||
applySiteRuleOverrides();
|
||||
if (!siteRuleUtils.isSpeederActiveForSite(tc.settings.enabled, tc.activeSiteRule)) {
|
||||
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;
|
||||
@@ -1952,7 +1696,7 @@ function defineVideoController() {
|
||||
// The vsc-hidden class (from V key) takes precedence via CSS specificity
|
||||
if (ytPlayer.classList.contains("ytp-autohide")) {
|
||||
wrapper.classList.add("ytp-autohide");
|
||||
|
||||
|
||||
// Immediately end any temporary "vsc-show" state to hide with YouTube
|
||||
// UNLESS it was forced by a shortcut (vsc-forced-show)
|
||||
if (!wrapper.classList.contains("vsc-forced-show")) {
|
||||
@@ -1962,7 +1706,7 @@ function defineVideoController() {
|
||||
wrapper.showTimeOut = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
log("YouTube controls hidden, hiding controller", 5);
|
||||
} else {
|
||||
wrapper.classList.remove("ytp-autohide");
|
||||
@@ -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,13 +1942,13 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var fragment = doc.createDocumentFragment();
|
||||
fragment.appendChild(wrapper);
|
||||
const parentEl = this.parent || this.video.parentElement;
|
||||
@@ -2278,15 +2023,17 @@ function applySiteRuleOverrides() {
|
||||
}
|
||||
|
||||
var currentUrl = location.href;
|
||||
var matchedRule = vscMatchSiteRule(currentUrl, tc.settings.siteRules);
|
||||
var matchedRule = siteRuleUtils.matchSiteRule(currentUrl, tc.settings.siteRules);
|
||||
|
||||
if (!matchedRule) return false;
|
||||
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 +2064,7 @@ function applySiteRuleOverrides() {
|
||||
[
|
||||
"controllerMarginTop",
|
||||
"controllerMarginBottom"
|
||||
].forEach(function (key) {
|
||||
].forEach(function(key) {
|
||||
tc.settings[key] = normalizeControllerMarginPx(tc.settings[key], 0);
|
||||
});
|
||||
|
||||
@@ -2346,22 +2093,25 @@ function applySiteRuleOverrides() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Re-match site rules for current URL and refresh or rebuild every controller. */
|
||||
function reapplySiteRulesAndControllerGeometry() {
|
||||
var siteDisabled = applySiteRuleOverrides();
|
||||
var videos = tc.mediaElements.slice();
|
||||
/** 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!tc.settings.enabled || siteDisabled) {
|
||||
videos.forEach(function (video) {
|
||||
removeController(video);
|
||||
});
|
||||
/** Re-match site rules for current URL and refresh controller position/opacity on every video. */
|
||||
function reapplySiteRulesAndControllerGeometry() {
|
||||
applySiteRuleOverrides();
|
||||
if (!siteRuleUtils.isSpeederActiveForSite(tc.settings.enabled, tc.activeSiteRule)) {
|
||||
return;
|
||||
}
|
||||
|
||||
videos.forEach(function (video) {
|
||||
if (!video) return;
|
||||
ensureController(video, video.parentElement || video.parentNode);
|
||||
});
|
||||
refreshAllControllerGeometry();
|
||||
}
|
||||
|
||||
function shouldPreserveDesiredSpeed(video, speed) {
|
||||
@@ -2381,6 +2131,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 +2152,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 +2199,7 @@ function setupListener(root) {
|
||||
}
|
||||
|
||||
var vscInitializedDocuments = new Set();
|
||||
|
||||
function clearPendingInitialization(doc) {
|
||||
if (!doc || !doc.vscPendingInitializeHandler) return;
|
||||
|
||||
@@ -2483,7 +2236,7 @@ function initializeWhenReady(doc, forceReinit = false) {
|
||||
|
||||
if (doc.vscPendingInitializeHandler) return;
|
||||
|
||||
var pendingInitializeHandler = function () {
|
||||
var pendingInitializeHandler = function() {
|
||||
tryInitializeDocument(doc, doc.vscPendingForceReinit === true);
|
||||
};
|
||||
|
||||
@@ -2498,6 +2251,7 @@ function initializeWhenReady(doc, forceReinit = false) {
|
||||
setTimeout(pendingInitializeHandler, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function inIframe() {
|
||||
try {
|
||||
return window.self !== window.top;
|
||||
@@ -2510,13 +2264,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 +2294,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 +2318,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 +2384,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 +2397,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 +2421,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;
|
||||
@@ -2701,7 +2458,10 @@ function attachNavigationListeners() {
|
||||
function initializeNow(doc, forceReinit = false) {
|
||||
if ((!forceReinit && vscInitializedDocuments.has(doc)) || !doc.body) return;
|
||||
|
||||
var siteDisabled = applySiteRuleOverrides();
|
||||
applySiteRuleOverrides();
|
||||
if (!siteRuleUtils.isSpeederActiveForSite(tc.settings.enabled, tc.activeSiteRule)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doc.body.classList.contains("vsc-initialized")) {
|
||||
doc.body.classList.add("vsc-initialized");
|
||||
@@ -2713,9 +2473,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 +2588,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 +2596,8 @@ function runAction(action, value, e) {
|
||||
"advance",
|
||||
"faster",
|
||||
"slower",
|
||||
"louder",
|
||||
"softer",
|
||||
"reset",
|
||||
"fast",
|
||||
"move",
|
||||
@@ -2850,7 +2610,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 +2720,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 +2790,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);
|
||||
@@ -3037,7 +2855,7 @@ function handleDrag(video, e) {
|
||||
pE.parentNode &&
|
||||
pE.parentNode.offsetHeight === pE.offsetHeight &&
|
||||
pE.parentNode.offsetWidth === pE.offsetWidth
|
||||
)
|
||||
)
|
||||
pE = pE.parentNode;
|
||||
video.classList.add("vcs-dragging");
|
||||
sC.classList.add("dragging");
|
||||
@@ -3066,6 +2884,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 +2902,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.0",
|
||||
"version": "5.2.7.0",
|
||||
"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"
|
||||
]
|
||||
|
||||
+264
-25
@@ -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 {
|
||||
@@ -292,11 +343,40 @@ label em {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.shortcut-label em {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: var(--muted);
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.customKey,
|
||||
.customValue {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Chevron: native menu indicator is often missing with themed controls */
|
||||
#addShortcutSelector,
|
||||
.site-add-shortcut-selector {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-color: var(--panel);
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%234b5563' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
background-size: 16px 16px;
|
||||
padding-right: 38px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#addShortcutSelector:disabled,
|
||||
.site-add-shortcut-selector:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
#addShortcutSelector {
|
||||
width: min(220px, 100%);
|
||||
margin-top: 12px;
|
||||
@@ -322,6 +402,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 +452,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 +516,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: start;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
@@ -414,11 +527,15 @@ label em {
|
||||
|
||||
.site-override-lead input[type="checkbox"] {
|
||||
justify-self: end;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.site-override-lead span {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.site-override-lead span em {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.site-rule-override-section .site-override-fields,
|
||||
@@ -427,10 +544,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 +929,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 +959,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%;
|
||||
@@ -843,9 +969,13 @@ button.lucide-result-tile.lucide-picked {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.site-rule-split-label span em {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.site-rule-split-label input[type="checkbox"] {
|
||||
justify-self: end;
|
||||
margin-top: 3px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.site-rule-option-checkbox > .site-rule-split-label {
|
||||
@@ -877,20 +1007,26 @@ button.lucide-result-tile.lucide-picked {
|
||||
}
|
||||
|
||||
.site-shortcuts-container .shortcut-row {
|
||||
grid-template-columns: minmax(0, 1fr) 110px 110px minmax(0, 1fr);
|
||||
grid-template-columns: minmax(0, 1fr) 110px 110px minmax(0, 1fr) 38px;
|
||||
padding: 8px 0;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.site-shortcuts-container .shortcut-row:first-child {
|
||||
.site-shortcuts-rows .shortcut-row:first-child {
|
||||
padding-top: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.site-add-shortcut-selector {
|
||||
width: min(220px, 100%);
|
||||
align-self: flex-start;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.force-label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: auto;
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
@@ -898,7 +1034,7 @@ button.lucide-result-tile.lucide-picked {
|
||||
}
|
||||
|
||||
.force-label input {
|
||||
margin-top: 2px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
@@ -918,17 +1054,85 @@ button.lucide-result-tile.lucide-picked {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.support-footer {
|
||||
padding: 16px 20px;
|
||||
.support-cta {
|
||||
margin-top: 14px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
background: var(--panel-subtle);
|
||||
}
|
||||
|
||||
.support-cta-text {
|
||||
margin: 0 0 12px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.support-footer p {
|
||||
margin: 0;
|
||||
.support-cta-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.support-footer a {
|
||||
font-weight: 600;
|
||||
.support-cta-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--panel);
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.support-cta-link:hover {
|
||||
background: var(--toggle-open-bg);
|
||||
border-color: var(--toggle-open-border);
|
||||
}
|
||||
|
||||
.support-cta-link:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.support-cta-link svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.support-cta-link--kofi {
|
||||
background: #fff4ef;
|
||||
}
|
||||
|
||||
.support-cta-link--kofi:hover {
|
||||
background: #ffe8de;
|
||||
}
|
||||
|
||||
.support-cta-kofi-img {
|
||||
display: block;
|
||||
height: 26px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.support-cta-link--bmc {
|
||||
color: #0d0c22;
|
||||
background: #ffdd00;
|
||||
}
|
||||
|
||||
.support-cta-link--bmc:hover {
|
||||
background: #f7d500;
|
||||
}
|
||||
|
||||
.support-cta-link--bmc svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
@@ -956,11 +1160,12 @@ 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,
|
||||
#addShortcutSelector {
|
||||
#addShortcutSelector,
|
||||
.site-add-shortcut-selector {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1005,6 +1210,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;
|
||||
}
|
||||
|
||||
@@ -1033,6 +1249,12 @@ button.lucide-result-tile.lucide-picked {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
#addShortcutSelector,
|
||||
.site-add-shortcut-selector {
|
||||
background-color: var(--panel);
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
@@ -1059,4 +1281,21 @@ button.lucide-result-tile.lucide-picked {
|
||||
filter: brightness(0) invert(1);
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.support-cta-link--kofi {
|
||||
background: #2c241f;
|
||||
}
|
||||
|
||||
.support-cta-link--kofi:hover {
|
||||
background: #3a312a;
|
||||
}
|
||||
|
||||
.support-cta-link--bmc {
|
||||
color: #ffdd00;
|
||||
background: #2a2618;
|
||||
}
|
||||
|
||||
.support-cta-link--bmc:hover {
|
||||
background: #3d3510;
|
||||
}
|
||||
}
|
||||
|
||||
+305
-80
@@ -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>
|
||||
@@ -21,13 +24,83 @@
|
||||
</div>
|
||||
<div class="version">v<span id="app-version"></span></div>
|
||||
</div>
|
||||
<div class="support-cta" role="region" aria-label="Support Speeder">
|
||||
<p class="support-cta-text">
|
||||
If Speeder has been useful, please consider supporting its development!
|
||||
</p>
|
||||
<div class="support-cta-links">
|
||||
<a
|
||||
class="support-cta-link support-cta-link--kofi"
|
||||
href="https://ko-fi.com/joshpatra"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Support on Ko-fi (opens in new tab)"
|
||||
>
|
||||
<img
|
||||
class="support-cta-kofi-img"
|
||||
src="images/kofi_symbol.svg"
|
||||
width="241"
|
||||
height="194"
|
||||
alt=""
|
||||
decoding="async"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
class="support-cta-link support-cta-link--github"
|
||||
href="https://github.com/sponsors/SoPat712"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Sponsor on GitHub (opens in new tab)"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class="support-cta-link support-cta-link--bmc"
|
||||
href="https://buymeacoffee.com/treeman183"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Support on Buy Me a Coffee (opens in new tab)"
|
||||
>
|
||||
<svg
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M20.216 6.415l-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 00-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 00-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 01-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 013.426-.12c.674.019 1.347.067 2.017.144l.228.031c.267.04.533.088.798.145.392.085.895.113 1.07.542.055.137.08.288.111.431l.319 1.484a.237.237 0 01-.199.284h-.003c-.037.006-.075.01-.112.015a36.704 36.704 0 01-4.743.295 37.059 37.059 0 01-4.699-.304c-.14-.017-.293-.042-.417-.06-.326-.048-.649-.108-.973-.161-.393-.065-.768-.032-1.123.161-.29.16-.527.404-.675.701-.154.316-.199.66-.267 1-.069.34-.176.707-.135 1.056.087.753.613 1.365 1.37 1.502a39.69 39.69 0 0011.343.376.483.483 0 01.535.53l-.071.697-1.018 9.907c-.041.41-.047.832-.125 1.237-.122.637-.553 1.028-1.182 1.171-.577.131-1.165.2-1.756.205-.656.004-1.31-.025-1.966-.022-.699.004-1.556-.06-2.095-.58-.475-.458-.54-1.174-.605-1.793l-.731-7.013-.322-3.094c-.037-.351-.286-.695-.678-.678-.336.015-.718.3-.678.679l.228 2.185.949 9.112c.147 1.344 1.174 2.068 2.446 2.272.742.12 1.503.144 2.257.156.966.016 1.942.053 2.892-.122 1.408-.258 2.465-1.198 2.616-2.657.34-3.332.683-6.663 1.024-9.995l.215-2.087a.484.484 0 01.39-.426c.402-.078.787-.212 1.074-.518.455-.488.546-1.124.385-1.766zm-1.478.772c-.145.137-.363.201-.578.233-2.416.359-4.866.54-7.308.46-1.748-.06-3.477-.254-5.207-.498-.17-.024-.353-.055-.47-.18-.22-.236-.111-.71-.054-.995.052-.26.152-.609.463-.646.484-.057 1.046.148 1.526.22.577.088 1.156.159 1.737.212 2.48.226 5.002.19 7.472-.14.45-.06.899-.13 1.345-.21.399-.072.84-.206 1.08.206.166.281.188.657.162.974a.544.544 0 01-.169.364zm-6.159 3.9c-.862.37-1.84.788-3.109.788a5.884 5.884 0 01-1.569-.217l.877 9.004c.065.78.717 1.38 1.5 1.38 0 0 1.243.065 1.658.065.447 0 1.786-.065 1.786-.065.783 0 1.434-.6 1.499-1.38l.94-9.95a3.996 3.996 0 00-1.322-.238c-.826 0-1.491.284-2.26.613z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="settings-stack">
|
||||
<section id="customs" class="settings-card">
|
||||
<div class="section-heading">
|
||||
<h3>Shortcuts</h3>
|
||||
<p class="section-intro">Backspace clears a shortcut. Escape disables it.</p>
|
||||
<p class="section-intro">
|
||||
Backspace clears a key. Escape disables optional shortcuts. If a site
|
||||
steals a shortcut, use a site rule with Override shortcuts (and
|
||||
per-key blocking) for that URL.
|
||||
</p>
|
||||
</div>
|
||||
<div class="shortcuts-grid">
|
||||
<div class="shortcut-row" id="display" data-action="display">
|
||||
@@ -63,7 +136,10 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="shortcut-row" id="slower" data-action="slower">
|
||||
<div class="shortcut-label">Decrease speed</div>
|
||||
<div class="shortcut-label">
|
||||
Decrease speed
|
||||
<em>Required: Speeder needs a key for this action.</em>
|
||||
</div>
|
||||
<input
|
||||
class="customKey"
|
||||
type="text"
|
||||
@@ -77,7 +153,10 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="shortcut-row" id="faster" data-action="faster">
|
||||
<div class="shortcut-label">Increase speed</div>
|
||||
<div class="shortcut-label">
|
||||
Increase speed
|
||||
<em>Required: Speeder needs a key for this action.</em>
|
||||
</div>
|
||||
<input
|
||||
class="customKey"
|
||||
type="text"
|
||||
@@ -184,11 +263,22 @@
|
||||
<h4 class="defaults-sub-heading">General</h4>
|
||||
|
||||
<div class="row row-checkbox">
|
||||
<label for="enabled">Enable</label>
|
||||
<label for="enabled"
|
||||
>Enable<br />
|
||||
<em
|
||||
>On: site rules can block sites (blacklist). Off: only matched rules keep Speeder on (whitelist).</em
|
||||
>
|
||||
</label>
|
||||
<input id="enabled" type="checkbox" />
|
||||
</div>
|
||||
<div class="row row-checkbox">
|
||||
<label for="audioBoolean">Work on audio</label>
|
||||
<label for="audioBoolean"
|
||||
>Work on audio<br />
|
||||
<em
|
||||
>Also controls plain HTML5 audio (not just video). Turn off if
|
||||
you only want Speeder on video players.</em
|
||||
>
|
||||
</label>
|
||||
<input id="audioBoolean" type="checkbox" />
|
||||
</div>
|
||||
|
||||
@@ -196,7 +286,14 @@
|
||||
<h4 class="defaults-sub-heading">Playback</h4>
|
||||
|
||||
<div class="row row-checkbox">
|
||||
<label for="rememberSpeed">Remember playback speed</label>
|
||||
<label for="rememberSpeed"
|
||||
>Remember playback speed<br />
|
||||
<em
|
||||
>Stores speed per source so revisiting the same media can restore
|
||||
it. Separate from “Force last saved speed,” which
|
||||
fights players that reset rate.</em
|
||||
>
|
||||
</label>
|
||||
<input id="rememberSpeed" type="checkbox" />
|
||||
</div>
|
||||
<div class="row row-checkbox">
|
||||
@@ -214,11 +311,23 @@
|
||||
<h4 class="defaults-sub-heading">Controller</h4>
|
||||
|
||||
<div class="row row-checkbox">
|
||||
<label for="startHidden">Hide controller by default</label>
|
||||
<label for="startHidden"
|
||||
>Hide controller by default<br />
|
||||
<em
|
||||
>Starts with the overlay hidden; use shortcuts (show/hide,
|
||||
move) or site behavior to reveal it.</em
|
||||
>
|
||||
</label>
|
||||
<input id="startHidden" type="checkbox" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="controllerLocation">Default controller location</label>
|
||||
<label for="controllerLocation"
|
||||
>Default controller location<br />
|
||||
<em
|
||||
>Corner or edge anchor for the hover bar. Site rules can override
|
||||
this for specific URLs.</em
|
||||
>
|
||||
</label>
|
||||
<select id="controllerLocation">
|
||||
<option value="top-left">Top left</option>
|
||||
<option value="top-center">Top center</option>
|
||||
@@ -231,7 +340,13 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="controllerOpacity">Controller opacity</label>
|
||||
<label for="controllerOpacity"
|
||||
>Controller opacity<br />
|
||||
<em
|
||||
>0–1 (decimals). Lower is more transparent. Applies to the
|
||||
in-page controller only.</em
|
||||
>
|
||||
</label>
|
||||
<input id="controllerOpacity" type="text" value="" />
|
||||
</div>
|
||||
<div class="row row-controller-margin">
|
||||
@@ -347,11 +462,23 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="row row-checkbox">
|
||||
<label for="showPopupControlBar">Show popup control bar</label>
|
||||
<label for="showPopupControlBar"
|
||||
>Show popup control bar<br />
|
||||
<em
|
||||
>Shows buttons in the extension popup (toolbar icon). Can be
|
||||
overridden per site in site rules.</em
|
||||
>
|
||||
</label>
|
||||
<input id="showPopupControlBar" type="checkbox" />
|
||||
</div>
|
||||
<div class="row row-checkbox">
|
||||
<label for="popupMatchHoverControls">Match hover controls</label>
|
||||
<label for="popupMatchHoverControls"
|
||||
>Match hover controls<br />
|
||||
<em
|
||||
>When on, the popup copies the hover bar’s buttons and
|
||||
order. When off, customize the popup layout below.</em
|
||||
>
|
||||
</label>
|
||||
<input id="popupMatchHoverControls" type="checkbox" />
|
||||
</div>
|
||||
<div id="popupCbEditorWrap" class="cb-editor cb-editor-disabled">
|
||||
@@ -480,7 +607,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"
|
||||
@@ -491,19 +626,36 @@
|
||||
<div class="site-rule-body">
|
||||
<div class="site-rule-option site-rule-option-checkbox">
|
||||
<label class="site-rule-split-label">
|
||||
<span>Enable Speeder on this site</span>
|
||||
<span
|
||||
>Enable Speeder on this site<br /><em
|
||||
>For this URL pattern only: off blocks when global Speeder
|
||||
is on (blacklist); on allows when global is off
|
||||
(whitelist)—same pairing as Defaults →
|
||||
Enable.</em
|
||||
></span
|
||||
>
|
||||
<input type="checkbox" class="site-enabled" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="site-rule-content">
|
||||
<div class="site-rule-override-section">
|
||||
<label class="site-override-lead">
|
||||
<span>Override placement for this site</span>
|
||||
<span
|
||||
>Override placement for this site<br /><em
|
||||
>When on, location and margin below replace general
|
||||
defaults for matching URLs.</em
|
||||
></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>
|
||||
<label
|
||||
>Default controller location:<br /><em
|
||||
>Corner or edge anchor for the hover bar. Replaces the
|
||||
general default for matching URLs only.</em
|
||||
></label
|
||||
>
|
||||
<select class="site-controllerLocation">
|
||||
<option value="top-left">Top left</option>
|
||||
<option value="top-center">Top center</option>
|
||||
@@ -518,7 +670,8 @@
|
||||
<div class="site-rule-option site-rule-margin-option">
|
||||
<label
|
||||
>Controller margin (px):<br /><em
|
||||
>Shifts the whole control. 0–200.</em
|
||||
>Shifts the whole control from its preset position (CSS
|
||||
margins). Top and bottom. 0–200.</em
|
||||
></label
|
||||
>
|
||||
<div class="controller-margin-inputs">
|
||||
@@ -536,99 +689,166 @@
|
||||
</div>
|
||||
<div class="site-rule-override-section">
|
||||
<label class="site-override-lead">
|
||||
<span>Override hide-by-default for this site</span>
|
||||
<span
|
||||
>Override hide-by-default for this site<br /><em
|
||||
>When on, the hide-by-default toggle below replaces the
|
||||
general default for matching URLs.</em
|
||||
></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>
|
||||
<label
|
||||
>Hide controller by default:<br /><em
|
||||
>Starts with the overlay hidden; use shortcuts
|
||||
(show/hide, move) or site behavior to reveal it.</em
|
||||
></label
|
||||
>
|
||||
<input type="checkbox" class="site-startHidden" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-rule-override-section">
|
||||
<label class="site-override-lead">
|
||||
<span>Override auto-hide for this site</span>
|
||||
<span
|
||||
>Override auto-hide for this site<br /><em
|
||||
>When on, hide-with-controls and timer below replace
|
||||
general defaults for matching URLs.</em
|
||||
></span
|
||||
>
|
||||
<input type="checkbox" class="override-autohide" />
|
||||
</label>
|
||||
<div class="site-autohide-container" style="display: none">
|
||||
<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
|
||||
>
|
||||
<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
|
||||
<div class="site-autohide-container">
|
||||
<div class="site-rule-option site-rule-option-checkbox">
|
||||
<label class="site-rule-split-label">
|
||||
<span
|
||||
>Hide with controls (idle-based)<br /><em
|
||||
>Fade the controller in and out with the video
|
||||
interface: perfect sync on YouTube, idle-based
|
||||
elsewhere.</em
|
||||
></span
|
||||
>
|
||||
<input type="text" class="site-hideWithControlsTimer" />
|
||||
</div>
|
||||
<input type="checkbox" class="site-hideWithControls" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="site-rule-option site-rule-option-field">
|
||||
<label
|
||||
>Auto-hide timer (0.1–15s):<br /><em
|
||||
>Seconds of inactivity before hiding: 0.1–15 for
|
||||
non-YouTube sites.</em
|
||||
></label
|
||||
>
|
||||
<input type="text" class="site-hideWithControlsTimer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-rule-override-section">
|
||||
<label class="site-override-lead">
|
||||
<span>Override playback for this site</span>
|
||||
<span
|
||||
>Override playback for this site<br /><em
|
||||
>When on, remember speed / force / audio below replace
|
||||
general defaults for matching URLs.</em
|
||||
></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>
|
||||
<label
|
||||
>Remember playback speed:<br /><em
|
||||
>Stores speed per source so revisiting the same media
|
||||
can restore it. Separate from “Force last saved
|
||||
speed,” which fights players that reset
|
||||
rate.</em
|
||||
></label
|
||||
>
|
||||
<input type="checkbox" class="site-rememberSpeed" />
|
||||
</div>
|
||||
<div class="site-rule-option site-rule-option-checkbox">
|
||||
<label>Force last saved speed:</label>
|
||||
<label
|
||||
>Force last saved speed:<br /><em
|
||||
>Useful when a video player tries to override the speed
|
||||
you set in Speeder.</em
|
||||
></label
|
||||
>
|
||||
<input type="checkbox" class="site-forceLastSavedSpeed" />
|
||||
</div>
|
||||
<div class="site-rule-option site-rule-option-checkbox">
|
||||
<label>Work on audio:</label>
|
||||
<label
|
||||
>Work on audio:<br /><em
|
||||
>Also controls plain HTML5 audio (not just video). Turn
|
||||
off if you only want Speeder on video players.</em
|
||||
></label
|
||||
>
|
||||
<input type="checkbox" class="site-audioBoolean" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-rule-override-section">
|
||||
<label class="site-override-lead">
|
||||
<span>Override opacity for this site</span>
|
||||
<span
|
||||
>Override opacity for this site<br /><em
|
||||
>When on, opacity below replaces the general default for
|
||||
matching URLs.</em
|
||||
></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>
|
||||
<label
|
||||
>Controller opacity:<br /><em
|
||||
>0–1 (decimals). Lower is more transparent.
|
||||
Applies to the in-page controller only.</em
|
||||
></label
|
||||
>
|
||||
<input type="text" class="site-controllerOpacity" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-rule-override-section">
|
||||
<label class="site-override-lead">
|
||||
<span>Override subtitle nudge for this site</span>
|
||||
<span
|
||||
>Override subtitle nudge for this site<br /><em
|
||||
>When on, nudge options below replace general defaults for
|
||||
matching URLs.</em
|
||||
></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>
|
||||
<label
|
||||
>Enable subtitle nudge:<br /><em
|
||||
>Makes tiny playback changes to help keep subtitles
|
||||
aligned.</em
|
||||
></label
|
||||
>
|
||||
<input type="checkbox" class="site-enableSubtitleNudge" />
|
||||
</div>
|
||||
<div class="site-rule-option site-rule-option-field">
|
||||
<label>Nudge interval (10–1000ms):</label>
|
||||
<label
|
||||
>Nudge interval (10–1000ms):<br /><em
|
||||
>How often to nudge: 10–1000. Smaller values are
|
||||
more frequent. Default: 50.</em
|
||||
></label
|
||||
>
|
||||
<input type="text" class="site-subtitleNudgeInterval" placeholder="50" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-rule-controlbar">
|
||||
<label class="site-override-lead">
|
||||
<span>Override in-player control bar for this site</span>
|
||||
<span
|
||||
>Override in-player control bar for this site<br /><em
|
||||
>Same idea as Hover control bar: drag blocks between
|
||||
Active and Available for matching URLs only.</em
|
||||
></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>
|
||||
@@ -643,12 +863,22 @@
|
||||
</div>
|
||||
<div class="site-rule-controlbar">
|
||||
<label class="site-override-lead">
|
||||
<span>Override extension popup for this site</span>
|
||||
<span
|
||||
>Override extension popup for this site<br /><em
|
||||
>Popup layout for matching URLs; mirrors the global Popup
|
||||
control bar when you customize it here.</em
|
||||
></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>
|
||||
<label
|
||||
>Show popup control bar<br /><em
|
||||
>Shows buttons in the extension popup (toolbar icon).
|
||||
Replaces the general default for this pattern.</em
|
||||
></label
|
||||
>
|
||||
<input type="checkbox" class="site-showPopupControlBar" />
|
||||
</div>
|
||||
<div class="cb-editor">
|
||||
@@ -665,10 +895,23 @@
|
||||
</div>
|
||||
<div class="site-rule-shortcuts">
|
||||
<label class="site-override-lead">
|
||||
<span>Override shortcuts for this site</span>
|
||||
<span
|
||||
>Override shortcuts for this site<br /><em
|
||||
>Add shortcuts from the menu; none by default. Leave off
|
||||
to use global Shortcuts.</em
|
||||
></span
|
||||
>
|
||||
<input type="checkbox" class="override-shortcuts" />
|
||||
</label>
|
||||
<div class="site-shortcuts-container" style="display: none"></div>
|
||||
<div class="site-shortcuts-container">
|
||||
<div class="site-shortcuts-rows"></div>
|
||||
<select
|
||||
class="site-add-shortcut-selector"
|
||||
aria-label="Add shortcut for this site"
|
||||
>
|
||||
<option value="">Add shortcut…</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -701,24 +944,6 @@
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<footer class="support-footer settings-card">
|
||||
<p>
|
||||
If Speeder has been useful, consider supporting its development via
|
||||
<a
|
||||
href="https://github.com/sponsors/SoPat712"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>GitHub Sponsor</a
|
||||
>
|
||||
or
|
||||
<a
|
||||
href="https://ko-fi.com/joshpatra"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Ko-Fi</a
|
||||
>.
|
||||
</p>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
+575
-559
@@ -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,23 +52,18 @@ var modifierKeys = new Set([
|
||||
"Shift"
|
||||
]);
|
||||
|
||||
var displayKeyAliases = {
|
||||
" ": "Space",
|
||||
ArrowLeft: "Left",
|
||||
ArrowUp: "Up",
|
||||
ArrowRight: "Right",
|
||||
ArrowDown: "Down"
|
||||
};
|
||||
var controllerLocations = [
|
||||
"top-left",
|
||||
"top-center",
|
||||
"top-right",
|
||||
"middle-right",
|
||||
"bottom-right",
|
||||
"bottom-center",
|
||||
"bottom-left",
|
||||
"middle-left"
|
||||
];
|
||||
var controllerLocations = Array.isArray(controllerUtils.controllerLocations)
|
||||
? controllerUtils.controllerLocations.slice()
|
||||
: [
|
||||
"top-left",
|
||||
"top-center",
|
||||
"top-right",
|
||||
"middle-right",
|
||||
"bottom-right",
|
||||
"bottom-center",
|
||||
"bottom-left",
|
||||
"middle-left"
|
||||
];
|
||||
|
||||
var controllerButtonDefs = {
|
||||
rewind: { icon: "\u00AB", name: "Rewind" },
|
||||
@@ -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(["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,155 @@ function refreshAddShortcutSelector() {
|
||||
}
|
||||
}
|
||||
|
||||
function ensureDefaultBinding(storage, action, key, keyCode, value) {
|
||||
function refreshSiteRuleAddShortcutSelector(ruleEl) {
|
||||
if (!ruleEl) return;
|
||||
var selector = ruleEl.querySelector(".site-add-shortcut-selector");
|
||||
if (!selector) return;
|
||||
|
||||
while (selector.options.length > 1) {
|
||||
selector.remove(1);
|
||||
}
|
||||
|
||||
var usedActions = new Set();
|
||||
ruleEl.querySelectorAll(".site-shortcuts-rows .shortcut-row.customs").forEach(function (row) {
|
||||
var action = row.dataset.action;
|
||||
if (action) usedActions.add(action);
|
||||
});
|
||||
|
||||
Object.keys(actionLabels).forEach(function (action) {
|
||||
if (!usedActions.has(action)) {
|
||||
var option = document.createElement("option");
|
||||
option.value = action;
|
||||
option.textContent = actionLabels[action];
|
||||
selector.appendChild(option);
|
||||
}
|
||||
});
|
||||
|
||||
var overrideShortcutsOn =
|
||||
ruleEl.querySelector(".override-shortcuts") &&
|
||||
ruleEl.querySelector(".override-shortcuts").checked;
|
||||
|
||||
if (selector.options.length === 1) {
|
||||
selector.disabled = true;
|
||||
selector.options[0].text = "All shortcuts added";
|
||||
} else {
|
||||
selector.disabled = !overrideShortcutsOn;
|
||||
selector.options[0].text = "Add shortcut\u2026";
|
||||
}
|
||||
}
|
||||
|
||||
function getGlobalBindingSnapshotForSiteShortcut(action) {
|
||||
var row = document.querySelector(
|
||||
'#customs .shortcut-row[data-action="' + action + '"]'
|
||||
);
|
||||
if (row) {
|
||||
var keyInput = row.querySelector(".customKey");
|
||||
var binding = normalizeStoredBinding(keyInput && keyInput.vscBinding);
|
||||
if (binding) {
|
||||
var valueInput = row.querySelector(".customValue");
|
||||
var value = customActionsNoValues.includes(action)
|
||||
? 0
|
||||
: Number(valueInput && valueInput.value);
|
||||
return { binding: binding, value: value };
|
||||
}
|
||||
}
|
||||
var def = tcDefaults.keyBindings.find(function (b) {
|
||||
return b.action === action;
|
||||
});
|
||||
if (def) {
|
||||
return {
|
||||
binding: normalizeStoredBinding(def),
|
||||
value: def.value
|
||||
};
|
||||
}
|
||||
return { binding: null, value: undefined };
|
||||
}
|
||||
|
||||
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 +487,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 +599,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 +629,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 +673,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 +700,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 " +
|
||||
(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 " + action + " is invalid. Unable to save"
|
||||
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 +806,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 +897,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,46 +940,58 @@ 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) {
|
||||
shortcuts.push({
|
||||
action: action,
|
||||
key: binding.key,
|
||||
keyCode: binding.keyCode,
|
||||
code: binding.code,
|
||||
disabled: binding.disabled === true,
|
||||
value: customActionsNoValues.includes(action)
|
||||
? 0
|
||||
: Number(valueInput.value),
|
||||
force: forceCheckbox ? forceCheckbox.checked : false
|
||||
});
|
||||
if (!binding) {
|
||||
if (requiredShortcutActions.has(action)) {
|
||||
saveError =
|
||||
"Error: Site rule shortcut for " +
|
||||
(actionLabels[action] || action) +
|
||||
" cannot be empty. Unable to save";
|
||||
return;
|
||||
}
|
||||
binding = createDisabledBinding();
|
||||
}
|
||||
|
||||
if (binding.disabled === true && requiredShortcutActions.has(action)) {
|
||||
saveError =
|
||||
"Error: Site rule shortcut for " +
|
||||
(actionLabels[action] || action) +
|
||||
" cannot be empty. Unable to save";
|
||||
return;
|
||||
}
|
||||
|
||||
shortcuts.push({
|
||||
action: action,
|
||||
code: binding.code,
|
||||
disabled: binding.disabled === true,
|
||||
value: customActionsNoValues.includes(action)
|
||||
? 0
|
||||
: Number(valueInput.value),
|
||||
force: forceCheckbox ? forceCheckbox.checked : false
|
||||
});
|
||||
});
|
||||
if (saveError) return;
|
||||
if (shortcuts.length > 0) rule.shortcuts = shortcuts;
|
||||
}
|
||||
|
||||
settings.siteRules.push(rule);
|
||||
});
|
||||
|
||||
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,48 +1002,22 @@ 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);
|
||||
});
|
||||
}
|
||||
|
||||
function addSiteRuleShortcut(container, action, binding, value, force) {
|
||||
function addSiteRuleShortcut(rowsEl, action, binding, value, force) {
|
||||
if (!rowsEl) return;
|
||||
|
||||
var div = document.createElement("div");
|
||||
div.setAttribute("class", "shortcut-row customs");
|
||||
div.dataset.action = action;
|
||||
|
||||
var actionLabel = document.createElement("div");
|
||||
actionLabel.className = "shortcut-label";
|
||||
var actionLabels = {
|
||||
display: "Show/hide controller",
|
||||
move: "Move controller",
|
||||
slower: "Decrease speed",
|
||||
faster: "Increase speed",
|
||||
rewind: "Rewind",
|
||||
advance: "Advance",
|
||||
reset: "Reset speed",
|
||||
fast: "Preferred speed",
|
||||
muted: "Mute",
|
||||
pause: "Pause",
|
||||
mark: "Set marker",
|
||||
jump: "Jump to marker",
|
||||
toggleSubtitleNudge: "Toggle subtitle nudge"
|
||||
};
|
||||
var actionLabelText = actionLabels[action] || action;
|
||||
if (action === "toggleSubtitleNudge") {
|
||||
// Check if the site rule is for YouTube.
|
||||
// We look up the pattern from the site rule element this container belongs to.
|
||||
var ruleEl = container.closest(".site-rule");
|
||||
var ruleEl = rowsEl.closest(".site-rule");
|
||||
var pattern = ruleEl ? ruleEl.querySelector(".site-pattern").value : "";
|
||||
if (!pattern.toLowerCase().includes("youtube.com")) {
|
||||
actionLabelText += " (only for YouTube embeds)";
|
||||
@@ -960,7 +1039,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");
|
||||
@@ -979,12 +1061,18 @@ function addSiteRuleShortcut(container, action, binding, value, force) {
|
||||
forceLabel.appendChild(forceCheckbox);
|
||||
forceLabel.appendChild(forceText);
|
||||
|
||||
var removeButton = document.createElement("button");
|
||||
removeButton.className = "removeParent";
|
||||
removeButton.type = "button";
|
||||
removeButton.textContent = "\u00d7";
|
||||
|
||||
div.appendChild(actionLabel);
|
||||
div.appendChild(keyInput);
|
||||
div.appendChild(valueInput);
|
||||
div.appendChild(forceLabel);
|
||||
div.appendChild(removeButton);
|
||||
|
||||
container.appendChild(div);
|
||||
rowsEl.appendChild(div);
|
||||
}
|
||||
|
||||
function createSiteRule(rule) {
|
||||
@@ -996,9 +1084,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,117 +1119,102 @@ 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";
|
||||
populateControlBarZones(
|
||||
ruleEl.querySelector(".site-cb-active"),
|
||||
ruleEl.querySelector(".site-cb-available"),
|
||||
rule.controllerButtons
|
||||
);
|
||||
}
|
||||
var hasControlbarOverride = Boolean(rule && Array.isArray(rule.controllerButtons));
|
||||
ruleEl.querySelector(".override-controlbar").checked = hasControlbarOverride;
|
||||
populateControlBarZones(
|
||||
ruleEl.querySelector(".site-cb-active"),
|
||||
ruleEl.querySelector(".site-cb-available"),
|
||||
hasControlbarOverride ? rule.controllerButtons : getControlBarOrder()
|
||||
);
|
||||
applySiteRuleOverrideState(ruleEl, "override-controlbar", "site-controlbar-container");
|
||||
|
||||
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)) {
|
||||
populateControlBarZones(
|
||||
sitePopupActive,
|
||||
sitePopupAvailable,
|
||||
sanitizePopupButtonOrder(rule.popupControllerButtons),
|
||||
function (id) {
|
||||
return !popupExcludedButtonIds.has(id);
|
||||
}
|
||||
);
|
||||
} else if (
|
||||
sitePopupActive &&
|
||||
sitePopupAvailable &&
|
||||
sitePopupActive.children.length === 0
|
||||
) {
|
||||
populateControlBarZones(
|
||||
sitePopupActive,
|
||||
sitePopupAvailable,
|
||||
getPopupControlBarOrder(),
|
||||
function (id) {
|
||||
return !popupExcludedButtonIds.has(id);
|
||||
}
|
||||
);
|
||||
);
|
||||
ruleEl.querySelector(".override-popup-controlbar").checked =
|
||||
hasPopupControlbarOverride;
|
||||
populateControlBarZones(
|
||||
ruleEl.querySelector(".site-popup-cb-active"),
|
||||
ruleEl.querySelector(".site-popup-cb-available"),
|
||||
hasPopupControlbarOverride && Array.isArray(rule.popupControllerButtons)
|
||||
? sanitizePopupButtonOrder(rule.popupControllerButtons)
|
||||
: getPopupControlBarOrder(),
|
||||
function (id) {
|
||||
return !popupExcludedButtonIds.has(id);
|
||||
}
|
||||
}
|
||||
);
|
||||
syncSiteRuleField(ruleEl, rule, "showPopupControlBar", true);
|
||||
applySiteRuleOverrideState(
|
||||
ruleEl,
|
||||
"override-popup-controlbar",
|
||||
"site-popup-controlbar-container"
|
||||
);
|
||||
|
||||
if (rule && Array.isArray(rule.shortcuts) && rule.shortcuts.length > 0) {
|
||||
ruleEl.querySelector(".override-shortcuts").checked = true;
|
||||
var container = ruleEl.querySelector(".site-shortcuts-container");
|
||||
container.style.display = "block";
|
||||
|
||||
var hasShortcutOverride = Boolean(
|
||||
rule && Array.isArray(rule.shortcuts) && rule.shortcuts.length > 0
|
||||
);
|
||||
ruleEl.querySelector(".override-shortcuts").checked = hasShortcutOverride;
|
||||
var rowsEl = ruleEl.querySelector(".site-shortcuts-rows");
|
||||
if (hasShortcutOverride) {
|
||||
rule.shortcuts.forEach((shortcut) => {
|
||||
addSiteRuleShortcut(
|
||||
container,
|
||||
rowsEl,
|
||||
shortcut.action,
|
||||
shortcut,
|
||||
shortcut.value,
|
||||
@@ -1151,16 +1222,12 @@ function createSiteRule(rule) {
|
||||
);
|
||||
});
|
||||
}
|
||||
applySiteRuleOverrideState(ruleEl, "override-shortcuts", "site-shortcuts-container");
|
||||
refreshSiteRuleAddShortcutSelector(ruleEl);
|
||||
|
||||
document.getElementById("siteRulesContainer").appendChild(ruleEl);
|
||||
}
|
||||
|
||||
function populateDefaultSiteShortcuts(container) {
|
||||
tcDefaults.keyBindings.forEach((binding) => {
|
||||
addSiteRuleShortcut(container, binding.action, binding, binding.value, false);
|
||||
});
|
||||
}
|
||||
|
||||
function createControlBarBlock(buttonId) {
|
||||
var def = controllerButtonDefs[buttonId];
|
||||
if (!def) return null;
|
||||
@@ -1548,49 +1615,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,9 +1690,11 @@ function restore_options() {
|
||||
|
||||
refreshAddShortcutSelector();
|
||||
|
||||
var siteRules = Array.isArray(settings.siteRules)
|
||||
? settings.siteRules
|
||||
: tcDefaults.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"));
|
||||
if (siteRules.length > 0) {
|
||||
@@ -1632,16 +1705,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,30 +1732,13 @@ 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;
|
||||
}
|
||||
|
||||
restore_options();
|
||||
var status = document.getElementById("status");
|
||||
status.textContent = "Default options restored";
|
||||
setTimeout(function () {
|
||||
status.textContent = "";
|
||||
}, 1000);
|
||||
});
|
||||
chrome.storage.sync.set(tcDefaults, function () {
|
||||
restore_options();
|
||||
var status = document.getElementById("status");
|
||||
status.textContent = "Default options restored";
|
||||
setTimeout(function () {
|
||||
status.textContent = "";
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1739,31 +1795,33 @@ 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) {
|
||||
var removedRow = removeParentButton.parentNode;
|
||||
var siteRuleForShortcut = removedRow.closest(".site-rule");
|
||||
removedRow.remove();
|
||||
refreshAddShortcutSelector();
|
||||
return;
|
||||
}
|
||||
if (event.target.classList.contains("remove-site-rule")) {
|
||||
event.target.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 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";
|
||||
if (siteRuleForShortcut) {
|
||||
refreshSiteRuleAddShortcutSelector(siteRuleForShortcut);
|
||||
}
|
||||
return;
|
||||
}
|
||||
var removeSiteRuleButton = targetEl.closest(".remove-site-rule");
|
||||
if (removeSiteRuleButton) {
|
||||
removeSiteRuleButton.closest(".site-rule").remove();
|
||||
return;
|
||||
}
|
||||
var toggleButton = targetEl.closest(".toggle-site-rule");
|
||||
if (toggleButton) {
|
||||
var ruleEl = toggleButton.closest(".site-rule");
|
||||
var isCollapsed = ruleEl.classList.contains("collapsed");
|
||||
setSiteRuleExpandedState(ruleEl, isCollapsed);
|
||||
return;
|
||||
}
|
||||
});
|
||||
document.addEventListener("change", (event) => {
|
||||
if (event.target.classList.contains("customDo")) {
|
||||
@@ -1776,6 +1834,26 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
}
|
||||
|
||||
if (event.target.classList.contains("site-add-shortcut-selector")) {
|
||||
var action = event.target.value;
|
||||
if (!action) return;
|
||||
var siteRuleRoot = event.target.closest(".site-rule");
|
||||
var rows = siteRuleRoot && siteRuleRoot.querySelector(".site-shortcuts-rows");
|
||||
if (rows) {
|
||||
var snap = getGlobalBindingSnapshotForSiteShortcut(action);
|
||||
addSiteRuleShortcut(
|
||||
rows,
|
||||
action,
|
||||
snap.binding,
|
||||
snap.value,
|
||||
false
|
||||
);
|
||||
refreshSiteRuleAddShortcutSelector(siteRuleRoot);
|
||||
}
|
||||
event.target.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Site rule: show/hide optional override sections
|
||||
var siteOverrideContainers = {
|
||||
"override-placement": "site-placement-container",
|
||||
@@ -1783,7 +1861,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 +1873,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
"." + siteOverrideContainers[ocb]
|
||||
);
|
||||
if (targetBox) {
|
||||
targetBox.style.display = event.target.checked ? "block" : "none";
|
||||
setSiteOverrideContainerState(targetBox, event.target.checked);
|
||||
}
|
||||
if (ocb === "override-shortcuts") {
|
||||
refreshSiteRuleAddShortcutSelector(siteRuleRoot);
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -210,21 +210,63 @@ button:focus-visible {
|
||||
|
||||
.donate-split {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.donate-split button {
|
||||
.donate-icon-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 32px;
|
||||
padding: 6px 4px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border-strong);
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease, border-color 120ms ease;
|
||||
}
|
||||
|
||||
.donate-icon-btn:hover {
|
||||
background: #f8f9fb;
|
||||
border-color: #c5ccd5;
|
||||
}
|
||||
|
||||
.donate-icon-btn:active {
|
||||
background: #f1f3f5;
|
||||
}
|
||||
|
||||
.donate-icon-btn:focus-visible {
|
||||
outline: 2px solid rgba(17, 24, 39, 0.14);
|
||||
outline-offset: 2px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.donate-icon-btn svg {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.donate-icon-btn--kofi img {
|
||||
display: block;
|
||||
height: 22px;
|
||||
width: auto;
|
||||
border-radius: 0;
|
||||
min-height: 28px;
|
||||
max-width: 40px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.donate-split button:first-child {
|
||||
.donate-icon-btn:first-child {
|
||||
border-radius: 8px 0 0 8px;
|
||||
border-right: 0;
|
||||
border-right-width: 0;
|
||||
}
|
||||
|
||||
.donate-split button:last-child {
|
||||
.donate-icon-btn:nth-child(2) {
|
||||
border-radius: 0;
|
||||
border-right-width: 0;
|
||||
}
|
||||
|
||||
.donate-icon-btn:last-child {
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
@@ -266,4 +308,13 @@ button:focus-visible {
|
||||
background: #dfe3e8;
|
||||
border-color: #dfe3e8;
|
||||
}
|
||||
|
||||
.donate-icon-btn:hover {
|
||||
background: #1f2226;
|
||||
border-color: #4a515a;
|
||||
}
|
||||
|
||||
.donate-icon-btn:active {
|
||||
background: #252a2f;
|
||||
}
|
||||
}
|
||||
|
||||
+63
-3
@@ -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>
|
||||
@@ -34,8 +35,67 @@
|
||||
<div id="donateWrap" class="donate-wrap">
|
||||
<button id="donate" class="secondary">Donate</button>
|
||||
<div id="donateOptions" class="donate-split hide">
|
||||
<button id="donateKofi" class="secondary">Ko-fi</button>
|
||||
<button id="donateGithub" class="secondary">Sponsors</button>
|
||||
<a
|
||||
id="donateGithub"
|
||||
class="donate-icon-btn donate-icon-btn--github"
|
||||
href="https://github.com/sponsors/SoPat712"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Sponsor on GitHub (opens in new tab)"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="22"
|
||||
height="22"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
id="donateKofi"
|
||||
class="donate-icon-btn donate-icon-btn--kofi"
|
||||
href="https://ko-fi.com/joshpatra"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Support on Ko-fi (opens in new tab)"
|
||||
>
|
||||
<img
|
||||
src="images/kofi_symbol.svg"
|
||||
width="28"
|
||||
height="22"
|
||||
alt=""
|
||||
decoding="async"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
id="donateBmc"
|
||||
class="donate-icon-btn donate-icon-btn--bmc"
|
||||
href="https://buymeacoffee.com/treeman183"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Support on Buy Me a Coffee (opens in new tab)"
|
||||
>
|
||||
<svg
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="22"
|
||||
height="22"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M20.216 6.415l-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 00-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 00-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 01-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 013.426-.12c.674.019 1.347.067 2.017.144l.228.031c.267.04.533.088.798.145.392.085.895.113 1.07.542.055.137.08.288.111.431l.319 1.484a.237.237 0 01-.199.284h-.003c-.037.006-.075.01-.112.015a36.704 36.704 0 01-4.743.295 37.059 37.059 0 01-4.699-.304c-.14-.017-.293-.042-.417-.06-.326-.048-.649-.108-.973-.161-.393-.065-.768-.032-1.123.161-.29.16-.527.404-.675.701-.154.316-.199.66-.267 1-.069.34-.176.707-.135 1.056.087.753.613 1.365 1.37 1.502a39.69 39.69 0 0011.343.376.483.483 0 01.535.53l-.071.697-1.018 9.907c-.041.41-.047.832-.125 1.237-.122.637-.553 1.028-1.182 1.171-.577.131-1.165.2-1.756.205-.656.004-1.31-.025-1.966-.022-.699.004-1.556-.06-2.095-.58-.475-.458-.54-1.174-.605-1.793l-.731-7.013-.322-3.094c-.037-.351-.286-.695-.678-.678-.336.015-.718.3-.678.679l.228 2.185.949 9.112c.147 1.344 1.174 2.068 2.446 2.272.742.12 1.503.144 2.257.156.966.016 1.942.053 2.892-.122 1.408-.258 2.465-1.198 2.616-2.657.34-3.332.683-6.663 1.024-9.995l.215-2.087a.484.484 0 01.39-.426c.402-.078.787-.212 1.074-.518.455-.488.546-1.124.385-1.766zm-1.478.772c-.145.137-.363.201-.578.233-2.416.359-4.866.54-7.308.46-1.748-.06-3.477-.254-5.207-.498-.17-.024-.353-.055-.47-.18-.22-.236-.111-.71-.054-.995.052-.26.152-.609.463-.646.484-.057 1.046.148 1.526.22.577.088 1.156.159 1.737.212 2.48.226 5.002.19 7.472-.14.45-.06.899-.13 1.345-.21.399-.072.84-.206 1.08.206.166.281.188.657.162.974a.544.544 0 01-.169.364zm-6.159 3.9c-.862.37-1.84.788-3.109.788a5.884 5.884 0 01-1.569-.217l.877 9.004c.065.78.717 1.38 1.5 1.38 0 0 1.243.065 1.658.065.447 0 1.786-.065 1.786-.065.783 0 1.434-.6 1.499-1.38l.94-9.95a3.996 3.996 0 00-1.322-.238c-.826 0-1.491.284-2.26.613z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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,58 +14,41 @@ 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;
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
return popupControlUtils.resolvePopupButtons(storage, siteRule, {
|
||||
controllerButtonDefs: controllerButtonDefs,
|
||||
defaultButtons: defaultButtons,
|
||||
excludedIds: popupExcludedButtonIds
|
||||
});
|
||||
}
|
||||
|
||||
function setControlBarVisible(visible) {
|
||||
@@ -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() {
|
||||
@@ -260,14 +230,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
document.querySelector("#donateOptions").classList.remove("hide");
|
||||
});
|
||||
|
||||
document.querySelector("#donateKofi").addEventListener("click", function () {
|
||||
window.open("https://ko-fi.com/joshpatra");
|
||||
});
|
||||
|
||||
document.querySelector("#donateGithub").addEventListener("click", function () {
|
||||
window.open("https://github.com/sponsors/SoPat712");
|
||||
});
|
||||
|
||||
document.querySelector("#enable").addEventListener("click", function () {
|
||||
toggleEnabled(true, settingsSavedReloadMessage);
|
||||
});
|
||||
@@ -300,9 +262,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;
|
||||
@@ -310,7 +271,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
var url = context && context.url ? context.url : "";
|
||||
var siteRule = matchSiteRule(url, storage.siteRules);
|
||||
var siteDisabled = isSiteRuleDisabled(siteRule);
|
||||
var siteAvailable = storage.enabled !== false && !siteDisabled;
|
||||
var siteAvailable = siteRuleUtils.isSpeederActiveForSite(
|
||||
storage.enabled,
|
||||
siteRule
|
||||
);
|
||||
var showBar = storage.showPopupControlBar !== false;
|
||||
|
||||
if (siteRule && siteRule.showPopupControlBar !== undefined) {
|
||||
@@ -364,9 +328,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
changes.controllerButtons ||
|
||||
changes.popupMatchHoverControls ||
|
||||
changes.popupControllerButtons ||
|
||||
changes.siteRules ||
|
||||
changes.siteRulesMeta ||
|
||||
changes.siteRulesFormat
|
||||
changes.siteRules
|
||||
) {
|
||||
renderForActiveTab();
|
||||
}
|
||||
@@ -375,37 +337,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) {
|
||||
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);
|
||||
});
|
||||
});
|
||||
chrome.storage.sync.set({ enabled: enabled }, function () {
|
||||
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,148 @@
|
||||
(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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Local-only keys excluded from backup JSON. These are disposable caches
|
||||
* (e.g. Lucide tags.json) that bloat exports and are refetched when needed.
|
||||
* Keep in sync with lucide-client.js (LUCIDE_TAGS_CACHE_KEY + "At").
|
||||
*/
|
||||
var localSettingsKeysOmittedFromExport = [
|
||||
"lucideTagsCacheV1",
|
||||
"lucideTagsCacheV1At"
|
||||
];
|
||||
|
||||
function filterLocalSettingsForExport(local) {
|
||||
if (!local || typeof local !== "object" || Array.isArray(local)) {
|
||||
return {};
|
||||
}
|
||||
var out = {};
|
||||
for (var key in local) {
|
||||
if (!Object.prototype.hasOwnProperty.call(local, key)) continue;
|
||||
if (localSettingsKeysOmittedFromExport.indexOf(key) !== -1) continue;
|
||||
out[key] = local[key];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
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,
|
||||
filterLocalSettingsForExport: filterLocalSettingsForExport,
|
||||
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,87 @@
|
||||
(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)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether Speeder should run on this URL given global enabled and the matched rule (if any).
|
||||
* - No rule: follows global (enabled unless explicitly false).
|
||||
* - Rule with site "off" / disableExtension: always inactive (blacklist).
|
||||
* - Rule with site "on": active even when global is off (whitelist).
|
||||
*/
|
||||
function isSpeederActiveForSite(globalEnabled, siteRule) {
|
||||
var globalOn = globalEnabled !== false;
|
||||
if (!siteRule) {
|
||||
return globalOn;
|
||||
}
|
||||
if (isSiteRuleDisabled(siteRule)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return {
|
||||
compileSiteRulePattern: compileSiteRulePattern,
|
||||
escapeStringRegExp: escapeStringRegExp,
|
||||
isSiteRuleDisabled: isSiteRuleDisabled,
|
||||
isSpeederActiveForSite: isSpeederActiveForSite,
|
||||
matchSiteRule: matchSiteRule
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,220 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { JSDOM } from "jsdom";
|
||||
import { vi } from "vitest";
|
||||
import { applyJSDOMWindow } from "./jsdom-globals.js";
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse HTML into a fresh JSDOM document so tests can reload scripts without
|
||||
* top-level `const` redeclaration errors (avoids document.write).
|
||||
*/
|
||||
export function loadHtmlString(html) {
|
||||
const dom = new JSDOM(html, {
|
||||
url: "https://example.org/",
|
||||
pretendToBeVisual: true,
|
||||
runScripts: "dangerously"
|
||||
});
|
||||
applyJSDOMWindow(dom.window);
|
||||
}
|
||||
|
||||
export function loadHtml(relPath) {
|
||||
loadHtmlString(readRepoFile(relPath));
|
||||
}
|
||||
|
||||
const WINDOW_GLOBAL_SKIP = new Set([
|
||||
"alert",
|
||||
"atob",
|
||||
"blur",
|
||||
"btoa",
|
||||
"cancelAnimationFrame",
|
||||
"captureEvents",
|
||||
"clearInterval",
|
||||
"clearTimeout",
|
||||
"close",
|
||||
"confirm",
|
||||
"fetch",
|
||||
"focus",
|
||||
"getComputedStyle",
|
||||
"matchMedia",
|
||||
"open",
|
||||
"prompt",
|
||||
"queueMicrotask",
|
||||
"releaseEvents",
|
||||
"requestAnimationFrame",
|
||||
"setInterval",
|
||||
"setTimeout",
|
||||
"stop"
|
||||
]);
|
||||
|
||||
function mirrorExtensionGlobalsFromWindow(win) {
|
||||
if (!win) return;
|
||||
if (win.tc) {
|
||||
globalThis.tc = win.tc;
|
||||
}
|
||||
for (const key of Object.keys(win)) {
|
||||
if (WINDOW_GLOBAL_SKIP.has(key)) continue;
|
||||
if (/^[A-Z]/.test(key)) continue;
|
||||
const val = win[key];
|
||||
if (typeof val === "function") {
|
||||
globalThis[key] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function loadScript(relPath) {
|
||||
const source =
|
||||
"var chrome = window.chrome || globalThis.chrome;\n" +
|
||||
readRepoFile(relPath) +
|
||||
"\n//# sourceURL=" +
|
||||
relPath;
|
||||
const el = document.createElement("script");
|
||||
el.textContent = source;
|
||||
document.head.appendChild(el);
|
||||
mirrorExtensionGlobalsFromWindow(window);
|
||||
}
|
||||
|
||||
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,309 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { JSDOM } = require("jsdom");
|
||||
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) {
|
||||
loadHtmlString(readWorkspaceFile(relPath));
|
||||
}
|
||||
|
||||
function applyJSDOMWindow(win) {
|
||||
globalThis.window = win;
|
||||
globalThis.document = win.document;
|
||||
globalThis.navigator = win.navigator;
|
||||
globalThis.customElements = win.customElements;
|
||||
globalThis.HTMLElement = win.HTMLElement;
|
||||
globalThis.Element = win.Element;
|
||||
globalThis.Node = win.Node;
|
||||
globalThis.Text = win.Text;
|
||||
globalThis.DocumentFragment = win.DocumentFragment;
|
||||
globalThis.Event = win.Event;
|
||||
globalThis.MouseEvent = win.MouseEvent;
|
||||
globalThis.KeyboardEvent = win.KeyboardEvent;
|
||||
globalThis.DOMParser = win.DOMParser;
|
||||
globalThis.URL = win.URL;
|
||||
globalThis.Blob = win.Blob;
|
||||
globalThis.FileReader = win.FileReader;
|
||||
win.Date = globalThis.Date;
|
||||
win.open = vi.fn();
|
||||
win.close = vi.fn();
|
||||
}
|
||||
|
||||
function loadHtmlString(html) {
|
||||
const dom = new JSDOM(html, {
|
||||
url: "https://example.org/",
|
||||
pretendToBeVisual: true,
|
||||
runScripts: "dangerously"
|
||||
});
|
||||
applyJSDOMWindow(dom.window);
|
||||
}
|
||||
|
||||
const WINDOW_GLOBAL_SKIP = new Set([
|
||||
"alert",
|
||||
"atob",
|
||||
"blur",
|
||||
"btoa",
|
||||
"cancelAnimationFrame",
|
||||
"captureEvents",
|
||||
"clearInterval",
|
||||
"clearTimeout",
|
||||
"close",
|
||||
"confirm",
|
||||
"fetch",
|
||||
"focus",
|
||||
"getComputedStyle",
|
||||
"matchMedia",
|
||||
"open",
|
||||
"prompt",
|
||||
"queueMicrotask",
|
||||
"releaseEvents",
|
||||
"requestAnimationFrame",
|
||||
"setInterval",
|
||||
"setTimeout",
|
||||
"stop"
|
||||
]);
|
||||
|
||||
function mirrorExtensionGlobalsFromWindow(win) {
|
||||
if (!win) return;
|
||||
if (win.tc) {
|
||||
globalThis.tc = win.tc;
|
||||
}
|
||||
for (const key of Object.keys(win)) {
|
||||
if (WINDOW_GLOBAL_SKIP.has(key)) continue;
|
||||
if (/^[A-Z]/.test(key)) continue;
|
||||
const val = win[key];
|
||||
if (typeof val === "function") {
|
||||
globalThis[key] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateScript(relPath) {
|
||||
const absPath = workspacePath(relPath);
|
||||
const source =
|
||||
"var chrome = window.chrome || globalThis.chrome;\n" +
|
||||
readWorkspaceFile(relPath) +
|
||||
`\n//# sourceURL=${absPath.replace(/\\/g, "/")}`;
|
||||
const el = document.createElement("script");
|
||||
el.textContent = source;
|
||||
document.head.appendChild(el);
|
||||
mirrorExtensionGlobalsFromWindow(window);
|
||||
}
|
||||
|
||||
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,30 @@
|
||||
/**
|
||||
* Point Vitest/jsdom test globals at a new JSDOM window (no document.write).
|
||||
* Call after creating `new JSDOM(html, options).window`.
|
||||
*/
|
||||
import { vi } from "vitest";
|
||||
|
||||
export function applyJSDOMWindow(win) {
|
||||
globalThis.window = win;
|
||||
globalThis.document = win.document;
|
||||
globalThis.navigator = win.navigator;
|
||||
globalThis.customElements = win.customElements;
|
||||
globalThis.HTMLElement = win.HTMLElement;
|
||||
globalThis.Element = win.Element;
|
||||
globalThis.Node = win.Node;
|
||||
globalThis.Text = win.Text;
|
||||
globalThis.DocumentFragment = win.DocumentFragment;
|
||||
globalThis.Event = win.Event;
|
||||
globalThis.MouseEvent = win.MouseEvent;
|
||||
globalThis.KeyboardEvent = win.KeyboardEvent;
|
||||
globalThis.DOMParser = win.DOMParser;
|
||||
globalThis.URL = win.URL;
|
||||
globalThis.Blob = win.Blob;
|
||||
globalThis.FileReader = win.FileReader;
|
||||
|
||||
// Vitest fake timers patch host `Date`; jsdom’s window keeps its own otherwise.
|
||||
win.Date = globalThis.Date;
|
||||
|
||||
win.open = vi.fn();
|
||||
win.close = vi.fn();
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
import {
|
||||
createChromeMock,
|
||||
flushAsyncWork,
|
||||
loadHtml,
|
||||
loadScript
|
||||
} from "./helpers/browser.js";
|
||||
|
||||
async function setupImportExport(overrides = {}) {
|
||||
loadHtml("options.html");
|
||||
globalThis.chrome = createChromeMock(overrides);
|
||||
window.chrome = globalThis.chrome;
|
||||
const restoreSpy = vi.fn();
|
||||
globalThis.restore_options = restoreSpy;
|
||||
window.restore_options = restoreSpy;
|
||||
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 = window.Blob;
|
||||
class TestBlob {
|
||||
constructor(parts, options) {
|
||||
this.parts = parts;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
async text() {
|
||||
return this.parts.join("");
|
||||
}
|
||||
}
|
||||
globalThis.Blob = TestBlob;
|
||||
window.Blob = TestBlob;
|
||||
|
||||
let capturedBlob = null;
|
||||
let clickedDownload = null;
|
||||
Object.defineProperty(window.URL, "createObjectURL", {
|
||||
configurable: true,
|
||||
value: vi.fn((blob) => {
|
||||
capturedBlob = blob;
|
||||
return "blob:test";
|
||||
})
|
||||
});
|
||||
Object.defineProperty(window.URL, "revokeObjectURL", {
|
||||
configurable: true,
|
||||
value: vi.fn(() => {})
|
||||
});
|
||||
vi.spyOn(window.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;
|
||||
window.Blob = OriginalBlob;
|
||||
});
|
||||
|
||||
it("export strips lucideTagsCacheV1 from localSettings", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(2026, 3, 4, 8, 9, 10));
|
||||
await setupImportExport({
|
||||
sync: { rememberSpeed: true },
|
||||
local: {
|
||||
customButtonIcons: { faster: { slug: "rocket", svg: "<svg/>" } },
|
||||
lucideTagsCacheV1: { "a-arrow-down": ["letter"] },
|
||||
lucideTagsCacheV1At: 42
|
||||
}
|
||||
});
|
||||
const OriginalBlob = window.Blob;
|
||||
class TestBlob {
|
||||
constructor(parts) {
|
||||
this.parts = parts;
|
||||
}
|
||||
async text() {
|
||||
return this.parts.join("");
|
||||
}
|
||||
}
|
||||
globalThis.Blob = TestBlob;
|
||||
window.Blob = TestBlob;
|
||||
let capturedBlob = null;
|
||||
Object.defineProperty(window.URL, "createObjectURL", {
|
||||
configurable: true,
|
||||
value: vi.fn((blob) => {
|
||||
capturedBlob = blob;
|
||||
return "blob:test";
|
||||
})
|
||||
});
|
||||
Object.defineProperty(window.URL, "revokeObjectURL", {
|
||||
configurable: true,
|
||||
value: vi.fn(() => {})
|
||||
});
|
||||
vi.spyOn(window.HTMLAnchorElement.prototype, "click").mockImplementation(
|
||||
() => {}
|
||||
);
|
||||
|
||||
document.getElementById("exportSettings").click();
|
||||
await flushAsyncWork();
|
||||
|
||||
expect(JSON.parse(await capturedBlob.text()).localSettings).toEqual({
|
||||
customButtonIcons: { faster: { slug: "rocket", svg: "<svg/>" } }
|
||||
});
|
||||
globalThis.Blob = OriginalBlob;
|
||||
window.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;
|
||||
});
|
||||
|
||||
class MockFileReader {
|
||||
readAsText(file) {
|
||||
this.onload({
|
||||
target: {
|
||||
result: file.__text
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
globalThis.FileReader = MockFileReader;
|
||||
window.FileReader = MockFileReader;
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
class MockFileReader {
|
||||
readAsText(file) {
|
||||
this.onload({
|
||||
target: {
|
||||
result: file.__text
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
globalThis.FileReader = MockFileReader;
|
||||
window.FileReader = MockFileReader;
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
class MockFileReader {
|
||||
readAsText(file) {
|
||||
this.onload({
|
||||
target: {
|
||||
result: file.__text
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
globalThis.FileReader = MockFileReader;
|
||||
window.FileReader = MockFileReader;
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
class MockFileReader {
|
||||
readAsText(file) {
|
||||
this.onload({
|
||||
target: {
|
||||
result: file.__text
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
globalThis.FileReader = MockFileReader;
|
||||
window.FileReader = MockFileReader;
|
||||
|
||||
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,219 @@
|
||||
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("omits Lucide tags cache from exported localSettings", async () => {
|
||||
vi.spyOn(window.HTMLAnchorElement.prototype, "click").mockImplementation(
|
||||
() => {}
|
||||
);
|
||||
const { createObjectURL } = bootImportExport({
|
||||
syncData: { rememberSpeed: true },
|
||||
localData: {
|
||||
customButtonIcons: {
|
||||
faster: { slug: "rocket", svg: "<svg></svg>" }
|
||||
},
|
||||
lucideTagsCacheV1: { "a-arrow-down": ["letter", "text"] },
|
||||
lucideTagsCacheV1At: 999
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector("#exportSettings").click();
|
||||
await flushAsyncWork();
|
||||
|
||||
const blob = createObjectURL.mock.calls[0][0];
|
||||
const backup = JSON.parse(await blob.text());
|
||||
|
||||
expect(backup.localSettings).toEqual({
|
||||
customButtonIcons: {
|
||||
faster: { slug: "rocket", svg: "<svg></svg>" }
|
||||
}
|
||||
});
|
||||
expect(backup.localSettings.lucideTagsCacheV1).toBeUndefined();
|
||||
expect(backup.localSettings.lucideTagsCacheV1At).toBeUndefined();
|
||||
});
|
||||
|
||||
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,115 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createChromeMock,
|
||||
flushAsyncWork,
|
||||
loadHtmlString,
|
||||
loadScript
|
||||
} from "./helpers/browser.js";
|
||||
|
||||
function loadBlankDocument() {
|
||||
loadHtmlString("<!doctype html><html><body></body></html>");
|
||||
}
|
||||
|
||||
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("treats a matching site rule with site enabled as active when global enable is off", async () => {
|
||||
await bootInject({
|
||||
sync: {
|
||||
enabled: false,
|
||||
siteRules: [{ pattern: "example.org", enabled: true }]
|
||||
}
|
||||
});
|
||||
|
||||
expect(window.tc.settings.enabled).toBe(false);
|
||||
window.captureSiteRuleBase();
|
||||
window.applySiteRuleOverrides();
|
||||
expect(window.tc.activeSiteRule).toEqual(
|
||||
expect.objectContaining({ pattern: "example.org", enabled: true })
|
||||
);
|
||||
expect(
|
||||
window.SpeederShared.siteRules.isSpeederActiveForSite(
|
||||
window.tc.settings.enabled,
|
||||
window.tc.activeSiteRule
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
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,220 @@
|
||||
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("site rule shortcut override shows no rows by default and adds via selector", async () => {
|
||||
await setupOptions({ sync: { siteRules: [] } });
|
||||
|
||||
globalThis.createSiteRule({ pattern: "example.com" });
|
||||
const rule = document.getElementById("siteRulesContainer").lastElementChild;
|
||||
const rows = rule.querySelector(".site-shortcuts-rows");
|
||||
const selector = rule.querySelector(".site-add-shortcut-selector");
|
||||
|
||||
expect(rows.querySelectorAll(".shortcut-row").length).toBe(0);
|
||||
expect(selector).not.toBeNull();
|
||||
expect(selector.disabled).toBe(true);
|
||||
|
||||
rule.querySelector(".override-shortcuts").checked = true;
|
||||
rule.querySelector(".override-shortcuts").dispatchEvent(
|
||||
new Event("change", { bubbles: true })
|
||||
);
|
||||
|
||||
expect(selector.disabled).toBe(false);
|
||||
expect(selector.options.length).toBeGreaterThan(1);
|
||||
|
||||
selector.value = "pause";
|
||||
selector.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
|
||||
expect(rows.querySelectorAll('.shortcut-row[data-action="pause"]').length).toBe(1);
|
||||
});
|
||||
|
||||
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,137 @@
|
||||
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 controls when globally disabled but a whitelist site rule matches", async () => {
|
||||
await setupPopup({
|
||||
sync: {
|
||||
enabled: false,
|
||||
siteRules: [{ pattern: "example.com", enabled: true }]
|
||||
}
|
||||
});
|
||||
|
||||
expect(document.getElementById("status").classList.contains("hide")).toBe(
|
||||
true
|
||||
);
|
||||
expect(document.getElementById("popupControlBar").style.display).not.toBe(
|
||||
"none"
|
||||
);
|
||||
});
|
||||
|
||||
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,29 @@
|
||||
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;
|
||||
try {
|
||||
delete globalThis.restore_options;
|
||||
} catch {
|
||||
globalThis.restore_options = undefined;
|
||||
}
|
||||
if (typeof document !== "undefined") {
|
||||
document.head.innerHTML = "";
|
||||
document.body.innerHTML = "";
|
||||
}
|
||||
delete globalThis.chrome;
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
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("combines global enabled with matched site rules (whitelist / blacklist)", () => {
|
||||
const allowSite = { pattern: "good.test", enabled: true };
|
||||
const blockSite = { pattern: "bad.test", enabled: false };
|
||||
|
||||
expect(siteRules.isSpeederActiveForSite(true, null)).toBe(true);
|
||||
expect(siteRules.isSpeederActiveForSite(false, null)).toBe(false);
|
||||
|
||||
expect(siteRules.isSpeederActiveForSite(true, blockSite)).toBe(false);
|
||||
expect(siteRules.isSpeederActiveForSite(false, blockSite)).toBe(false);
|
||||
|
||||
expect(siteRules.isSpeederActiveForSite(true, allowSite)).toBe(true);
|
||||
expect(siteRules.isSpeederActiveForSite(false, allowSite)).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
|
||||
);
|
||||
|
||||
expect(
|
||||
importExportUtils.filterLocalSettingsForExport({
|
||||
customButtonIcons: { faster: { slug: "zap" } },
|
||||
lucideTagsCacheV1: { "a-arrow-down": ["letter"] },
|
||||
lucideTagsCacheV1At: 123
|
||||
})
|
||||
).toEqual({
|
||||
customButtonIcons: { faster: { slug: "zap" } }
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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