mirror of
https://github.com/SoPat712/videospeed.git
synced 2026-04-23 05:12:37 -04:00
Compare commits
53 Commits
main
...
v5.1.9.0-beta.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
ab7ae99807
|
|||
|
5d47c511be
|
|||
| 7713ba5bad | |||
|
ce0b28de4f
|
|||
|
a2b041e225
|
|||
|
4f87aadfd7
|
|||
|
8456111bf0
|
|||
|
6efe92a036
|
|||
|
0cb13905ff
|
|||
|
f32d1b3f71
|
|||
|
e6c56bcecb
|
|||
|
a7a0aafd68
|
|||
|
3cf1a4acd1
|
|||
|
a9956831c4
|
|||
|
25d3acf576
|
|||
|
7b8b4324af
|
|||
|
8b9e4bea1d
|
|||
|
8c94cc2088
|
|||
|
19d3af02a2
|
|||
|
306e0e3ea0
|
|||
|
1536c13c3e
|
|||
|
6bd319c8cc
|
|||
|
3aee8c8f9a
|
|||
|
939ee08466
|
|||
|
5a175c3cf8
|
|||
|
805e5a82e5
|
|||
|
df34b1fee9
|
|||
|
0741c6e535
|
|||
|
fad0c49e65
|
|||
|
66075fb6f3
|
|||
|
bf4025dcb4
|
|||
|
76a7b933bb
|
|||
|
1cd533fc5c
|
|||
|
8c5bd68d39
|
|||
|
9c257af446
|
|||
|
64a9b85587
|
|||
|
edd997037a
|
|||
|
f85a1f9f29
|
|||
|
97366b76b6
|
|||
|
8269875bb1
|
|||
|
e34ec17f33
|
|||
|
8d3905b654
|
|||
|
7fd8a931d8
|
|||
|
17319c1e25
|
|||
|
841c1a246e
|
|||
|
ed0f63e8bc
|
|||
|
53f66f1eeb
|
|||
|
f106ab490a
|
|||
|
5a38121e09
|
|||
|
36ed922b5c
|
|||
|
3275d1f322
|
|||
|
f6d706f096
|
|||
|
04292a8018
|
@@ -10,6 +10,8 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
WEB_EXT_IGNORE_FILES: scripts/**
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -46,7 +48,7 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ github.ref_name }}
|
tag_name: ${{ github.ref_name }}
|
||||||
name: Beta ${{ github.ref_name }}
|
name: ${{ github.ref_name }}
|
||||||
files: ${{ steps.xpi.outputs.file }}
|
files: ${{ steps.xpi.outputs.file }}
|
||||||
prerelease: true
|
prerelease: true
|
||||||
body: |
|
body: |
|
||||||
@@ -61,7 +63,9 @@ jobs:
|
|||||||
|
|
||||||
# Stable tag (v* without -beta) → Sign & submit to public AMO listing
|
# Stable tag (v* without -beta) → Sign & submit to public AMO listing
|
||||||
- name: Sign & Submit to AMO (stable)
|
- name: Sign & Submit to AMO (stable)
|
||||||
if: startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name, '-beta')
|
if:
|
||||||
|
startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name,
|
||||||
|
'-beta')
|
||||||
run: |
|
run: |
|
||||||
web-ext sign \
|
web-ext sign \
|
||||||
--api-key ${{ secrets.FIREFOX_API_KEY }} \
|
--api-key ${{ secrets.FIREFOX_API_KEY }} \
|
||||||
|
|||||||
Vendored
-3
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"kiroAgent.configureMCP": "Disabled"
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# Available for Firefox
|
# Available for Firefox
|
||||||
[](https://addons.mozilla.org//firefox/addon/speeder/)
|
[](https://addons.mozilla.org/firefox/addon/speeder/)
|
||||||
|
|
||||||
|
|
||||||
# The science of accelerated playback
|
# 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
|
\*\* 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
|
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
|
your most commonly used speeds. To add a new shortcut, open extension settings
|
||||||
and click "Add New".
|
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 —
|
Some sites may assign other functionality to one of the assigned shortcut keys —
|
||||||
these collisions are inevitable, unfortunately. As a workaround, the extension
|
these collisions are inevitable, unfortunately. As a workaround, the extension
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/* Runs via chrome.tabs.executeScript(allFrames) in the same isolated world as inject.js */
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
if (typeof getPrimaryVideoElement !== "function") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var v = getPrimaryVideoElement();
|
||||||
|
if (!v) return null;
|
||||||
|
return {
|
||||||
|
speed: v.playbackRate,
|
||||||
|
preferred: !v.paused
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
+82
-48
@@ -1,37 +1,35 @@
|
|||||||
// Import/Export functionality for Video Speed Controller settings
|
// Import/Export functionality for Video Speed Controller settings
|
||||||
|
var speederShared =
|
||||||
|
typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
|
||||||
|
var importExportUtils = speederShared.importExport || {};
|
||||||
|
|
||||||
function generateBackupFilename() {
|
function generateBackupFilename() {
|
||||||
const now = new Date();
|
return importExportUtils.generateBackupFilename(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 exportSettings() {
|
function exportSettings() {
|
||||||
chrome.storage.sync.get(null, function (storage) {
|
chrome.storage.sync.get(null, function (storage) {
|
||||||
const backup = {
|
chrome.storage.local.get(null, function (localStorage) {
|
||||||
version: "1.0",
|
const backup = importExportUtils.buildBackupPayload(
|
||||||
exportDate: new Date().toISOString(),
|
storage,
|
||||||
settings: storage
|
localStorage,
|
||||||
};
|
new Date()
|
||||||
|
);
|
||||||
|
|
||||||
const dataStr = JSON.stringify(backup, null, 2);
|
const dataStr = JSON.stringify(backup, null, 2);
|
||||||
const blob = new Blob([dataStr], { type: "application/json" });
|
const blob = new Blob([dataStr], { type: "application/json" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = generateBackupFilename();
|
link.download = generateBackupFilename();
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
showStatus("Settings exported successfully");
|
showStatus("Settings exported successfully");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,39 +45,75 @@ function importSettings() {
|
|||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function (e) {
|
reader.onload = function (e) {
|
||||||
try {
|
try {
|
||||||
const backup = JSON.parse(e.target.result);
|
const parsedBackup = importExportUtils.parseImportText(e.target.result);
|
||||||
let settingsToImport = null;
|
|
||||||
|
|
||||||
// Detect backup format: check for 'settings' wrapper or raw storage keys
|
if (!parsedBackup) {
|
||||||
if (backup.settings && typeof backup.settings === "object") {
|
|
||||||
settingsToImport = backup.settings;
|
|
||||||
} else if (typeof backup === "object" && (backup.keyBindings || backup.rememberSpeed !== undefined)) {
|
|
||||||
settingsToImport = backup; // Raw storage object
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!settingsToImport) {
|
|
||||||
showStatus("Error: Invalid backup file format", true);
|
showStatus("Error: Invalid backup file format", true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import all settings
|
var settingsToImport = parsedBackup.settings;
|
||||||
chrome.storage.sync.clear(function () {
|
var localToImport = parsedBackup.localSettings;
|
||||||
// If clear fails, we still try to set
|
|
||||||
chrome.storage.sync.set(settingsToImport, function () {
|
function importLocalSettings(callback) {
|
||||||
|
if (parsedBackup.isWrappedBackup !== true) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.storage.local.clear(function () {
|
||||||
if (chrome.runtime.lastError) {
|
if (chrome.runtime.lastError) {
|
||||||
showStatus("Error: Failed to save imported settings - " + chrome.runtime.lastError.message, true);
|
showStatus(
|
||||||
|
"Error: Failed to clear local extension data - " +
|
||||||
|
chrome.runtime.lastError.message,
|
||||||
|
true
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showStatus("Settings imported successfully. Reloading...");
|
|
||||||
setTimeout(function () {
|
if (localToImport && Object.keys(localToImport).length > 0) {
|
||||||
if (typeof restore_options === "function") {
|
chrome.storage.local.set(localToImport, function () {
|
||||||
restore_options();
|
if (chrome.runtime.lastError) {
|
||||||
} else {
|
showStatus(
|
||||||
location.reload();
|
"Error: Failed to save local extension data - " +
|
||||||
}
|
chrome.runtime.lastError.message,
|
||||||
}, 500);
|
true
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback();
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
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) {
|
} catch (err) {
|
||||||
showStatus("Error: Failed to parse backup file - " + err.message, true);
|
showStatus("Error: Failed to parse backup file - " + err.message, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
|
|
||||||
|
|
||||||
var isUserSeek = false; // Track if seek was user-initiated
|
var isUserSeek = false; // Track if seek was user-initiated
|
||||||
var lastToggleSpeed = {}; // Store last toggle speeds per video
|
var lastToggleSpeed = {}; // Store last toggle speeds per video
|
||||||
|
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;
|
||||||
|
for (var i = 0; i < tc.mediaElements.length; i++) {
|
||||||
|
var el = tc.mediaElements[i];
|
||||||
|
if (el && !el.paused) return el;
|
||||||
|
}
|
||||||
|
return tc.mediaElements[0];
|
||||||
|
}
|
||||||
|
|
||||||
var tc = {
|
var tc = {
|
||||||
settings: {
|
settings: {
|
||||||
lastSpeed: 1.0,
|
lastSpeed: 1.0,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
speeds: {},
|
speeds: {},
|
||||||
displayKeyCode: 86,
|
|
||||||
rememberSpeed: false,
|
rememberSpeed: false,
|
||||||
forceLastSavedSpeed: false,
|
forceLastSavedSpeed: false,
|
||||||
audioBoolean: false,
|
audioBoolean: false,
|
||||||
@@ -27,9 +38,10 @@ var tc = {
|
|||||||
controllerButtons: ["rewind", "slower", "faster", "advance", "display"],
|
controllerButtons: ["rewind", "slower", "faster", "advance", "display"],
|
||||||
defaultLogLevel: 3,
|
defaultLogLevel: 3,
|
||||||
logLevel: 3,
|
logLevel: 3,
|
||||||
enableSubtitleNudge: true, // Enabled by default, but only activates on YouTube
|
enableSubtitleNudge: false,
|
||||||
subtitleNudgeInterval: 50, // Default 50ms balances subtitle tracking with CPU cost
|
subtitleNudgeInterval: 50, // Default 50ms balances subtitle tracking with CPU cost
|
||||||
subtitleNudgeAmount: 0.001
|
subtitleNudgeAmount: 0.001,
|
||||||
|
customButtonIcons: {}
|
||||||
},
|
},
|
||||||
mediaElements: [],
|
mediaElements: [],
|
||||||
isNudging: false,
|
isNudging: false,
|
||||||
@@ -49,20 +61,23 @@ var vscObservedRoots = new WeakSet();
|
|||||||
var requestIdle =
|
var requestIdle =
|
||||||
typeof window.requestIdleCallback === "function"
|
typeof window.requestIdleCallback === "function"
|
||||||
? window.requestIdleCallback.bind(window)
|
? window.requestIdleCallback.bind(window)
|
||||||
: function (callback, options) {
|
: function(callback, options) {
|
||||||
return setTimeout(callback, (options && options.timeout) || 1);
|
return setTimeout(callback, (options && options.timeout) || 1);
|
||||||
};
|
};
|
||||||
var controllerLocations = [
|
var controllerLocations = Array.isArray(controllerUtils.controllerLocations)
|
||||||
"top-left",
|
? controllerUtils.controllerLocations.slice()
|
||||||
"top-center",
|
: [
|
||||||
"top-right",
|
"top-left",
|
||||||
"middle-right",
|
"top-center",
|
||||||
"bottom-right",
|
"top-right",
|
||||||
"bottom-center",
|
"middle-right",
|
||||||
"bottom-left",
|
"bottom-right",
|
||||||
"middle-left"
|
"bottom-center",
|
||||||
];
|
"bottom-left",
|
||||||
var defaultControllerLocation = controllerLocations[0];
|
"middle-left"
|
||||||
|
];
|
||||||
|
var defaultControllerLocation =
|
||||||
|
controllerUtils.defaultControllerLocation || controllerLocations[0];
|
||||||
var controllerLocationStyles = {
|
var controllerLocationStyles = {
|
||||||
"top-left": {
|
"top-left": {
|
||||||
top: "10px",
|
top: "10px",
|
||||||
@@ -106,75 +121,26 @@ var controllerLocationStyles = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* `label` fallback only when ui-icons has no path for the action. */
|
||||||
var controllerButtonDefs = {
|
var controllerButtonDefs = {
|
||||||
rewind: { label: "\u00AB", className: "rw" },
|
rewind: { label: "", className: "rw" },
|
||||||
slower: { label: "\u2212", className: "" },
|
slower: { label: "", className: "" },
|
||||||
faster: { label: "+", className: "" },
|
faster: { label: "", className: "" },
|
||||||
advance: { label: "\u00BB", className: "rw" },
|
advance: { label: "", className: "rw" },
|
||||||
display: { label: "\u00D7", className: "hideButton" },
|
display: { label: "", className: "hideButton" },
|
||||||
reset: { label: "\u21BA", className: "" },
|
reset: { label: "\u21BB", className: "" },
|
||||||
fast: { label: "\u2605", className: "" },
|
fast: { label: "", className: "" },
|
||||||
settings: { label: "\u2699", className: "" },
|
settings: { label: "", className: "" },
|
||||||
pause: { label: "\u23EF", className: "" },
|
pause: { label: "", className: "" },
|
||||||
muted: { label: "M", className: "" },
|
muted: { label: "", className: "" },
|
||||||
mark: { label: "\u2691", className: "" },
|
mark: { label: "", className: "" },
|
||||||
jump: { label: "\u21E5", className: "" }
|
jump: { label: "", className: "" }
|
||||||
};
|
};
|
||||||
|
|
||||||
var keyCodeToEventKey = {
|
function createDefaultBinding(action, code, value) {
|
||||||
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) {
|
|
||||||
return {
|
return {
|
||||||
action: action,
|
action: action,
|
||||||
key: key,
|
code: code,
|
||||||
keyCode: keyCode,
|
|
||||||
value: value,
|
value: value,
|
||||||
force: false,
|
force: false,
|
||||||
predefined: true
|
predefined: true
|
||||||
@@ -185,89 +151,73 @@ function defaultKeyBindings(storage) {
|
|||||||
return [
|
return [
|
||||||
createDefaultBinding(
|
createDefaultBinding(
|
||||||
"slower",
|
"slower",
|
||||||
"S",
|
"KeyS",
|
||||||
Number(storage.slowerKeyCode) || 83,
|
|
||||||
Number(storage.speedStep) || 0.1
|
Number(storage.speedStep) || 0.1
|
||||||
),
|
),
|
||||||
createDefaultBinding(
|
createDefaultBinding(
|
||||||
"faster",
|
"faster",
|
||||||
"D",
|
"KeyD",
|
||||||
Number(storage.fasterKeyCode) || 68,
|
|
||||||
Number(storage.speedStep) || 0.1
|
Number(storage.speedStep) || 0.1
|
||||||
),
|
),
|
||||||
createDefaultBinding(
|
createDefaultBinding(
|
||||||
"rewind",
|
"rewind",
|
||||||
"Z",
|
"KeyZ",
|
||||||
Number(storage.rewindKeyCode) || 90,
|
|
||||||
Number(storage.rewindTime) || 10
|
Number(storage.rewindTime) || 10
|
||||||
),
|
),
|
||||||
createDefaultBinding(
|
createDefaultBinding(
|
||||||
"advance",
|
"advance",
|
||||||
"X",
|
"KeyX",
|
||||||
Number(storage.advanceKeyCode) || 88,
|
|
||||||
Number(storage.advanceTime) || 10
|
Number(storage.advanceTime) || 10
|
||||||
),
|
),
|
||||||
createDefaultBinding(
|
createDefaultBinding(
|
||||||
"reset",
|
"reset",
|
||||||
"R",
|
"KeyR",
|
||||||
Number(storage.resetKeyCode) || 82,
|
|
||||||
1.0
|
1.0
|
||||||
),
|
),
|
||||||
createDefaultBinding(
|
createDefaultBinding(
|
||||||
"fast",
|
"fast",
|
||||||
"G",
|
"KeyG",
|
||||||
Number(storage.fastKeyCode) || 71,
|
|
||||||
Number(storage.fastSpeed) || 1.8
|
Number(storage.fastSpeed) || 1.8
|
||||||
),
|
),
|
||||||
createDefaultBinding(
|
createDefaultBinding(
|
||||||
"move",
|
"move",
|
||||||
"P",
|
"KeyP",
|
||||||
80,
|
|
||||||
0
|
0
|
||||||
),
|
),
|
||||||
createDefaultBinding(
|
createDefaultBinding(
|
||||||
"toggleSubtitleNudge",
|
"toggleSubtitleNudge",
|
||||||
"N",
|
"KeyN",
|
||||||
78,
|
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureDefaultKeyBinding(action, key, keyCode, value) {
|
function ensureDefaultKeyBinding(action, code, value) {
|
||||||
if (tc.settings.keyBindings.some((binding) => binding.action === action)) {
|
if (tc.settings.keyBindings.some((binding) => binding.action === action)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
tc.settings.keyBindings.push(
|
tc.settings.keyBindings.push(
|
||||||
createDefaultBinding(action, key, keyCode, value)
|
createDefaultBinding(action, code, value)
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLegacyKeyCode(binding) {
|
function getLegacyKeyCode(binding) {
|
||||||
if (!binding) return null;
|
return keyBindingUtils.getLegacyKeyCode(binding);
|
||||||
if (Number.isInteger(binding.keyCode)) return binding.keyCode;
|
|
||||||
if (typeof binding.key === "number" && Number.isInteger(binding.key)) {
|
|
||||||
return binding.key;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeControllerLocation(location) {
|
function normalizeControllerLocation(location) {
|
||||||
if (controllerLocations.includes(location)) return location;
|
return controllerUtils.normalizeControllerLocation(
|
||||||
return defaultControllerLocation;
|
location,
|
||||||
|
defaultControllerLocation
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var CONTROLLER_MARGIN_MAX_PX = 200;
|
var CONTROLLER_MARGIN_MAX_PX = 200;
|
||||||
|
|
||||||
function normalizeControllerMarginPx(value, fallback) {
|
function normalizeControllerMarginPx(value, fallback) {
|
||||||
var n = Number(value);
|
return controllerUtils.clampControllerMarginPx(value, fallback);
|
||||||
if (!Number.isFinite(n)) return fallback;
|
|
||||||
return Math.min(
|
|
||||||
CONTROLLER_MARGIN_MAX_PX,
|
|
||||||
Math.max(0, Math.round(n))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyControllerMargins(controller) {
|
function applyControllerMargins(controller) {
|
||||||
@@ -306,9 +256,7 @@ function applyControllerMargins(controller) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getNextControllerLocation(location) {
|
function getNextControllerLocation(location) {
|
||||||
var normalizedLocation = normalizeControllerLocation(location);
|
return controllerUtils.getNextControllerLocation(location);
|
||||||
var currentIndex = controllerLocations.indexOf(normalizedLocation);
|
|
||||||
return controllerLocations[(currentIndex + 1) % controllerLocations.length];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getControllerElement(videoOrController) {
|
function getControllerElement(videoOrController) {
|
||||||
@@ -438,7 +386,7 @@ function captureSiteRuleBase() {
|
|||||||
? tc.settings.controllerButtons.slice()
|
? tc.settings.controllerButtons.slice()
|
||||||
: tc.settings.controllerButtons,
|
: tc.settings.controllerButtons,
|
||||||
keyBindings: Array.isArray(tc.settings.keyBindings)
|
keyBindings: Array.isArray(tc.settings.keyBindings)
|
||||||
? tc.settings.keyBindings.map(function (binding) {
|
? tc.settings.keyBindings.map(function(binding) {
|
||||||
return Object.assign({}, binding);
|
return Object.assign({}, binding);
|
||||||
})
|
})
|
||||||
: tc.settings.keyBindings
|
: tc.settings.keyBindings
|
||||||
@@ -464,7 +412,7 @@ function resetSettingsFromSiteRuleBase() {
|
|||||||
? base.controllerButtons.slice()
|
? base.controllerButtons.slice()
|
||||||
: base.controllerButtons;
|
: base.controllerButtons;
|
||||||
tc.settings.keyBindings = Array.isArray(base.keyBindings)
|
tc.settings.keyBindings = Array.isArray(base.keyBindings)
|
||||||
? base.keyBindings.map(function (binding) {
|
? base.keyBindings.map(function(binding) {
|
||||||
return Object.assign({}, binding);
|
return Object.assign({}, binding);
|
||||||
})
|
})
|
||||||
: base.keyBindings;
|
: base.keyBindings;
|
||||||
@@ -528,44 +476,41 @@ function cycleControllerLocation(video) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeBindingKey(key) {
|
function normalizeBindingKey(key) {
|
||||||
if (typeof key !== "string" || key.length === 0) return null;
|
return keyBindingUtils.normalizeBindingKey(key);
|
||||||
if (key === "Spacebar") return " ";
|
|
||||||
if (key === "Esc") return "Escape";
|
|
||||||
if (key.length === 1 && /[a-z]/i.test(key)) return key.toUpperCase();
|
|
||||||
return key;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function legacyKeyCodeToBinding(keyCode) {
|
function legacyBindingKeyToCode(key) {
|
||||||
if (!Number.isInteger(keyCode)) return null;
|
return keyBindingUtils.legacyBindingKeyToCode(key);
|
||||||
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 normalizeStoredBinding(binding, fallbackKeyCode) {
|
function legacyKeyCodeToCode(keyCode) {
|
||||||
var fallbackBinding = legacyKeyCodeToBinding(fallbackKeyCode);
|
return keyBindingUtils.legacyKeyCodeToCode(keyCode);
|
||||||
if (!binding) return fallbackBinding;
|
}
|
||||||
|
|
||||||
|
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 (
|
if (
|
||||||
binding.disabled === true ||
|
binding.disabled === true ||
|
||||||
(binding.key === null &&
|
(binding.code === null &&
|
||||||
binding.keyCode === null &&
|
binding.key === null &&
|
||||||
binding.code === null)
|
binding.keyCode === null)
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
action: binding.action,
|
action: binding.action,
|
||||||
key: null,
|
|
||||||
keyCode: null,
|
|
||||||
code: null,
|
code: null,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
value: Number(binding.value),
|
value: Number(binding.value),
|
||||||
@@ -574,46 +519,20 @@ function normalizeStoredBinding(binding, fallbackKeyCode) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var normalizedCode = inferBindingCode(binding, fallbackCode);
|
||||||
|
if (!normalizedCode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var normalized = {
|
var normalized = {
|
||||||
action: binding.action,
|
action: binding.action,
|
||||||
key: null,
|
code: normalizedCode,
|
||||||
keyCode: null,
|
|
||||||
code:
|
|
||||||
typeof binding.code === "string" && binding.code.length > 0
|
|
||||||
? binding.code
|
|
||||||
: null,
|
|
||||||
disabled: false,
|
disabled: false,
|
||||||
value: Number(binding.value),
|
value: Number(binding.value),
|
||||||
force: String(binding.force) === "true" ? "true" : "false",
|
force: String(binding.force) === "true" ? "true" : "false",
|
||||||
predefined: Boolean(binding.predefined)
|
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;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -730,19 +649,32 @@ function isSubtitleNudgeSupported(video) {
|
|||||||
return Boolean(video);
|
return Boolean(video);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSubtitleNudgeAvailableForVideo(video) {
|
||||||
|
return isSubtitleNudgeSupported(video) && Boolean(tc.settings.enableSubtitleNudge);
|
||||||
|
}
|
||||||
|
|
||||||
function isSubtitleNudgeEnabledForVideo(video) {
|
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") {
|
if (typeof video.vsc.subtitleNudgeEnabledOverride === "boolean") {
|
||||||
return video.vsc.subtitleNudgeEnabledOverride;
|
return video.vsc.subtitleNudgeEnabledOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
return tc.settings.enableSubtitleNudge;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSubtitleNudgeEnabledForVideo(video, enabled) {
|
function setSubtitleNudgeEnabledForVideo(video, enabled) {
|
||||||
if (!video || !video.vsc) return false;
|
if (!video || !video.vsc) return false;
|
||||||
|
|
||||||
|
if (!isSubtitleNudgeAvailableForVideo(video)) {
|
||||||
|
video.vsc.subtitleNudgeEnabledOverride = null;
|
||||||
|
video.vsc.stopSubtitleNudge();
|
||||||
|
updateSubtitleNudgeIndicator(video);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var normalizedEnabled = Boolean(enabled);
|
var normalizedEnabled = Boolean(enabled);
|
||||||
video.vsc.subtitleNudgeEnabledOverride = normalizedEnabled;
|
video.vsc.subtitleNudgeEnabledOverride = normalizedEnabled;
|
||||||
|
|
||||||
@@ -759,7 +691,7 @@ function setSubtitleNudgeEnabledForVideo(video, enabled) {
|
|||||||
if (flashEl) {
|
if (flashEl) {
|
||||||
flashEl.classList.add("visible");
|
flashEl.classList.add("visible");
|
||||||
clearTimeout(flashEl._flashTimer);
|
clearTimeout(flashEl._flashTimer);
|
||||||
flashEl._flashTimer = setTimeout(function () {
|
flashEl._flashTimer = setTimeout(function() {
|
||||||
flashEl.classList.remove("visible");
|
flashEl.classList.remove("visible");
|
||||||
}, 1500);
|
}, 1500);
|
||||||
}
|
}
|
||||||
@@ -767,27 +699,68 @@ function setSubtitleNudgeEnabledForVideo(video, enabled) {
|
|||||||
return normalizedEnabled;
|
return normalizedEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSubtitleNudgeIndicatorContent(target, isEnabled) {
|
||||||
|
if (!target) return;
|
||||||
|
var action = isEnabled ? "subtitleNudgeOn" : "subtitleNudgeOff";
|
||||||
|
var custom =
|
||||||
|
tc.settings.customButtonIcons &&
|
||||||
|
tc.settings.customButtonIcons[action] &&
|
||||||
|
tc.settings.customButtonIcons[action].svg;
|
||||||
|
vscClearElement(target);
|
||||||
|
if (custom) {
|
||||||
|
var customWrap = vscCreateSvgWrap(
|
||||||
|
target.ownerDocument || document,
|
||||||
|
custom,
|
||||||
|
"vsc-btn-icon"
|
||||||
|
);
|
||||||
|
if (customWrap) {
|
||||||
|
target.appendChild(customWrap);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof vscIconSvgString !== "function") {
|
||||||
|
target.textContent = isEnabled ? "✓" : "×";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var svg = vscIconSvgString(action, 14);
|
||||||
|
if (!svg) {
|
||||||
|
target.textContent = isEnabled ? "✓" : "×";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var wrap = vscCreateSvgWrap(target.ownerDocument || document, svg, "vsc-btn-icon");
|
||||||
|
if (wrap) {
|
||||||
|
target.appendChild(wrap);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
target.textContent = isEnabled ? "✓" : "×";
|
||||||
|
}
|
||||||
|
|
||||||
function updateSubtitleNudgeIndicator(video) {
|
function updateSubtitleNudgeIndicator(video) {
|
||||||
if (!video || !video.vsc) return;
|
if (!video || !video.vsc) return;
|
||||||
|
|
||||||
|
var isAvailable = isSubtitleNudgeAvailableForVideo(video);
|
||||||
var isEnabled = isSubtitleNudgeEnabledForVideo(video);
|
var isEnabled = isSubtitleNudgeEnabledForVideo(video);
|
||||||
var label = isEnabled ? "✓" : "×";
|
var title = !isAvailable
|
||||||
var title = isEnabled ? "Subtitle nudge enabled" : "Subtitle nudge disabled";
|
? "Subtitle nudge unavailable on this site"
|
||||||
|
: isEnabled
|
||||||
|
? "Subtitle nudge enabled"
|
||||||
|
: "Subtitle nudge disabled";
|
||||||
|
|
||||||
var indicator = video.vsc.subtitleNudgeIndicator;
|
var indicator = video.vsc.subtitleNudgeIndicator;
|
||||||
if (indicator) {
|
if (indicator) {
|
||||||
indicator.textContent = label;
|
renderSubtitleNudgeIndicatorContent(indicator, isEnabled);
|
||||||
indicator.dataset.enabled = isEnabled ? "true" : "false";
|
indicator.dataset.enabled = isEnabled ? "true" : "false";
|
||||||
indicator.dataset.supported = "true";
|
indicator.dataset.supported = isAvailable ? "true" : "false";
|
||||||
indicator.title = title;
|
indicator.title = title;
|
||||||
indicator.setAttribute("aria-label", title);
|
indicator.setAttribute("aria-label", title);
|
||||||
}
|
}
|
||||||
|
|
||||||
var flashEl = video.vsc.nudgeFlashIndicator;
|
var flashEl = video.vsc.nudgeFlashIndicator;
|
||||||
if (flashEl) {
|
if (flashEl) {
|
||||||
flashEl.textContent = label;
|
renderSubtitleNudgeIndicatorContent(flashEl, isEnabled);
|
||||||
flashEl.dataset.enabled = isEnabled ? "true" : "false";
|
flashEl.dataset.enabled = isEnabled ? "true" : "false";
|
||||||
flashEl.dataset.supported = "true";
|
flashEl.dataset.supported = isAvailable ? "true" : "false";
|
||||||
|
flashEl.setAttribute("aria-label", title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,7 +770,7 @@ function schedulePersistLastSpeed(speed) {
|
|||||||
tc.pendingLastSpeedValue = speed;
|
tc.pendingLastSpeedValue = speed;
|
||||||
if (tc.pendingLastSpeedSave !== null) return;
|
if (tc.pendingLastSpeedSave !== null) return;
|
||||||
|
|
||||||
tc.pendingLastSpeedSave = setTimeout(function () {
|
tc.pendingLastSpeedSave = setTimeout(function() {
|
||||||
var speedToPersist = tc.pendingLastSpeedValue;
|
var speedToPersist = tc.pendingLastSpeedValue;
|
||||||
tc.pendingLastSpeedSave = null;
|
tc.pendingLastSpeedSave = null;
|
||||||
|
|
||||||
@@ -805,7 +778,8 @@ function schedulePersistLastSpeed(speed) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.storage.sync.set({ lastSpeed: speedToPersist }, function () { });
|
chrome.storage.sync.set({ lastSpeed: speedToPersist }, function() {
|
||||||
|
});
|
||||||
tc.persistedLastSpeed = speedToPersist;
|
tc.persistedLastSpeed = speedToPersist;
|
||||||
}, 250);
|
}, 250);
|
||||||
}
|
}
|
||||||
@@ -864,6 +838,10 @@ function applySourceTransitionPolicy(video, forceUpdate) {
|
|||||||
if (Math.abs(video.playbackRate - desiredSpeed) > 0.01) {
|
if (Math.abs(video.playbackRate - desiredSpeed) > 0.01) {
|
||||||
setSpeed(video, desiredSpeed, false, false);
|
setSpeed(video, desiredSpeed, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
function extendSpeedRestoreWindow(video, duration) {
|
function extendSpeedRestoreWindow(video, duration) {
|
||||||
@@ -883,7 +861,7 @@ function scheduleSpeedRestore(video, desiredSpeed, reason) {
|
|||||||
clearTimeout(video.vsc.restoreSpeedTimer);
|
clearTimeout(video.vsc.restoreSpeedTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
video.vsc.restoreSpeedTimer = setTimeout(function () {
|
video.vsc.restoreSpeedTimer = setTimeout(function() {
|
||||||
if (!video.vsc) return;
|
if (!video.vsc) return;
|
||||||
|
|
||||||
if (Math.abs(video.playbackRate - desiredSpeed) > 0.01) {
|
if (Math.abs(video.playbackRate - desiredSpeed) > 0.01) {
|
||||||
@@ -930,19 +908,13 @@ function takePendingRateChange(video, currentSpeed) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function matchesKeyBinding(binding, event) {
|
function matchesKeyBinding(binding, event) {
|
||||||
if (!binding || binding.disabled) return false;
|
return Boolean(
|
||||||
|
binding &&
|
||||||
var normalizedEventKey = normalizeBindingKey(event.key);
|
binding.disabled !== true &&
|
||||||
if (binding.key && normalizedEventKey) {
|
typeof binding.code === "string" &&
|
||||||
return binding.key === normalizedEventKey;
|
binding.code.length > 0 &&
|
||||||
}
|
binding.code === event.code
|
||||||
|
);
|
||||||
if (binding.code && event.code) {
|
|
||||||
return binding.code === event.code;
|
|
||||||
}
|
|
||||||
|
|
||||||
var legacyKeyCode = getLegacyKeyCode(binding);
|
|
||||||
return Number.isInteger(legacyKeyCode) && legacyKeyCode === event.keyCode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mediaSelector() {
|
function mediaSelector() {
|
||||||
@@ -972,7 +944,7 @@ function hasUsableMediaSource(node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node.querySelectorAll) {
|
if (node.querySelectorAll) {
|
||||||
return Array.from(node.querySelectorAll("source[src]")).some(function (
|
return Array.from(node.querySelectorAll("source[src]")).some(function(
|
||||||
source
|
source
|
||||||
) {
|
) {
|
||||||
var src = source.getAttribute("src");
|
var src = source.getAttribute("src");
|
||||||
@@ -992,6 +964,14 @@ function ensureController(node, parent) {
|
|||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
refreshAllControllerGeometry();
|
||||||
|
|
||||||
log(
|
log(
|
||||||
`Creating controller for ${node.tagName}: ${node.src || node.currentSrc || "no src"}`,
|
`Creating controller for ${node.tagName}: ${node.src || node.currentSrc || "no src"}`,
|
||||||
4
|
4
|
||||||
@@ -1103,7 +1083,7 @@ function patchAttachShadow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var originalAttachShadow = Element.prototype.attachShadow;
|
var originalAttachShadow = Element.prototype.attachShadow;
|
||||||
Element.prototype.attachShadow = function () {
|
Element.prototype.attachShadow = function() {
|
||||||
var shadowRoot = originalAttachShadow.apply(this, arguments);
|
var shadowRoot = originalAttachShadow.apply(this, arguments);
|
||||||
try {
|
try {
|
||||||
if (shadowRoot) {
|
if (shadowRoot) {
|
||||||
@@ -1134,7 +1114,7 @@ function log(message, level) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.storage.sync.get(tc.settings, function (storage) {
|
chrome.storage.sync.get(tc.settings, function(storage) {
|
||||||
var storedBindings = Array.isArray(storage.keyBindings)
|
var storedBindings = Array.isArray(storage.keyBindings)
|
||||||
? storage.keyBindings
|
? storage.keyBindings
|
||||||
: [];
|
: [];
|
||||||
@@ -1149,7 +1129,6 @@ chrome.storage.sync.get(tc.settings, function (storage) {
|
|||||||
chrome.storage.sync.set({
|
chrome.storage.sync.set({
|
||||||
keyBindings: tc.settings.keyBindings,
|
keyBindings: tc.settings.keyBindings,
|
||||||
version: tc.settings.version,
|
version: tc.settings.version,
|
||||||
displayKeyCode: tc.settings.displayKeyCode,
|
|
||||||
rememberSpeed: tc.settings.rememberSpeed,
|
rememberSpeed: tc.settings.rememberSpeed,
|
||||||
forceLastSavedSpeed: tc.settings.forceLastSavedSpeed,
|
forceLastSavedSpeed: tc.settings.forceLastSavedSpeed,
|
||||||
audioBoolean: tc.settings.audioBoolean,
|
audioBoolean: tc.settings.audioBoolean,
|
||||||
@@ -1169,7 +1148,6 @@ chrome.storage.sync.get(tc.settings, function (storage) {
|
|||||||
tc.settings.lastSpeed = 1.0;
|
tc.settings.lastSpeed = 1.0;
|
||||||
}
|
}
|
||||||
tc.persistedLastSpeed = tc.settings.lastSpeed;
|
tc.persistedLastSpeed = tc.settings.lastSpeed;
|
||||||
tc.settings.displayKeyCode = Number(storage.displayKeyCode);
|
|
||||||
tc.settings.rememberSpeed = Boolean(storage.rememberSpeed);
|
tc.settings.rememberSpeed = Boolean(storage.rememberSpeed);
|
||||||
tc.settings.forceLastSavedSpeed = Boolean(storage.forceLastSavedSpeed);
|
tc.settings.forceLastSavedSpeed = Boolean(storage.forceLastSavedSpeed);
|
||||||
tc.settings.audioBoolean = Boolean(storage.audioBoolean);
|
tc.settings.audioBoolean = Boolean(storage.audioBoolean);
|
||||||
@@ -1210,25 +1188,6 @@ chrome.storage.sync.get(tc.settings, function (storage) {
|
|||||||
? storage.controllerButtons
|
? storage.controllerButtons
|
||||||
: tc.settings.controllerButtons;
|
: tc.settings.controllerButtons;
|
||||||
|
|
||||||
// Migrate legacy blacklist if present
|
|
||||||
if (storage.blacklist && typeof storage.blacklist === "string" && tc.settings.siteRules.length === 0) {
|
|
||||||
var lines = storage.blacklist.split("\n");
|
|
||||||
lines.forEach((line) => {
|
|
||||||
var pattern = line.replace(regStrip, "");
|
|
||||||
if (pattern.length > 0) {
|
|
||||||
tc.settings.siteRules.push({
|
|
||||||
pattern: pattern,
|
|
||||||
disableExtension: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (tc.settings.siteRules.length > 0) {
|
|
||||||
chrome.storage.sync.set({ siteRules: tc.settings.siteRules });
|
|
||||||
chrome.storage.sync.remove(["blacklist"]);
|
|
||||||
log("Migrated legacy blacklist to site rules", 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tc.settings.enableSubtitleNudge =
|
tc.settings.enableSubtitleNudge =
|
||||||
typeof storage.enableSubtitleNudge !== "undefined"
|
typeof storage.enableSubtitleNudge !== "undefined"
|
||||||
? Boolean(storage.enableSubtitleNudge)
|
? Boolean(storage.enableSubtitleNudge)
|
||||||
@@ -1243,14 +1202,13 @@ chrome.storage.sync.get(tc.settings, function (storage) {
|
|||||||
addedDefaultBinding =
|
addedDefaultBinding =
|
||||||
ensureDefaultKeyBinding(
|
ensureDefaultKeyBinding(
|
||||||
"display",
|
"display",
|
||||||
"V",
|
"KeyV",
|
||||||
Number(storage.displayKeyCode) || 86,
|
|
||||||
0
|
0
|
||||||
) || addedDefaultBinding;
|
) || addedDefaultBinding;
|
||||||
addedDefaultBinding =
|
addedDefaultBinding =
|
||||||
ensureDefaultKeyBinding("move", "P", 80, 0) || addedDefaultBinding;
|
ensureDefaultKeyBinding("move", "KeyP", 0) || addedDefaultBinding;
|
||||||
addedDefaultBinding =
|
addedDefaultBinding =
|
||||||
ensureDefaultKeyBinding("toggleSubtitleNudge", "N", 78, 0) ||
|
ensureDefaultKeyBinding("toggleSubtitleNudge", "KeyN", 0) ||
|
||||||
addedDefaultBinding;
|
addedDefaultBinding;
|
||||||
|
|
||||||
if (addedDefaultBinding) {
|
if (addedDefaultBinding) {
|
||||||
@@ -1262,48 +1220,104 @@ chrome.storage.sync.get(tc.settings, function (storage) {
|
|||||||
// We use a global flag to ensure the listener is only attached once.
|
// We use a global flag to ensure the listener is only attached once.
|
||||||
if (!window.vscMessageListener) {
|
if (!window.vscMessageListener) {
|
||||||
chrome.runtime.onMessage.addListener(
|
chrome.runtime.onMessage.addListener(
|
||||||
function (request, sender, sendResponse) {
|
function(request, sender, sendResponse) {
|
||||||
if (request.action === "rescan_page") {
|
if (request.action === "rescan_page") {
|
||||||
log("Re-scan command received from popup.", 4);
|
log("Re-scan command received from popup.", 4);
|
||||||
initializeWhenReady(document, true);
|
initializeWhenReady(document, true);
|
||||||
sendResponse({ status: "complete" });
|
sendResponse({ status: "complete" });
|
||||||
} else if (request.action === "get_speed") {
|
return false;
|
||||||
var speed = 1.0;
|
}
|
||||||
if (tc.mediaElements && tc.mediaElements.length > 0) {
|
if (request.action === "get_speed") {
|
||||||
for (var i = 0; i < tc.mediaElements.length; i++) {
|
// Do not sendResponse in frames with no media — only one response is
|
||||||
if (tc.mediaElements[i] && !tc.mediaElements[i].paused) {
|
// accepted tab-wide, and the top frame often wins before an iframe.
|
||||||
speed = tc.mediaElements[i].playbackRate;
|
var videoGs = getPrimaryVideoElement();
|
||||||
break;
|
if (!videoGs) return false;
|
||||||
}
|
sendResponse({
|
||||||
}
|
speed: videoGs.playbackRate
|
||||||
if (speed === 1.0 && tc.mediaElements[0]) {
|
});
|
||||||
speed = tc.mediaElements[0].playbackRate;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
if (request.action === "get_page_context") {
|
||||||
sendResponse({ speed: speed });
|
|
||||||
} else if (request.action === "get_page_context") {
|
|
||||||
sendResponse({ url: location.href });
|
sendResponse({ url: location.href });
|
||||||
} else if (request.action === "run_action") {
|
return false;
|
||||||
|
}
|
||||||
|
if (request.action === "run_action") {
|
||||||
var value = request.value;
|
var value = request.value;
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
value = getKeyBindings(request.actionName, "value");
|
value = getKeyBindings(request.actionName, "value");
|
||||||
}
|
}
|
||||||
runAction(request.actionName, value);
|
runAction(request.actionName, value);
|
||||||
var newSpeed = 1.0;
|
var videoAfter = getPrimaryVideoElement();
|
||||||
if (tc.mediaElements && tc.mediaElements.length > 0) {
|
if (!videoAfter) return false;
|
||||||
newSpeed = tc.mediaElements[0].playbackRate;
|
sendResponse({
|
||||||
}
|
speed: videoAfter.playbackRate
|
||||||
sendResponse({ speed: newSpeed });
|
});
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set the flag to prevent adding the listener again.
|
// Set the flag to prevent adding the listener again.
|
||||||
window.vscMessageListener = true;
|
window.vscMessageListener = true;
|
||||||
}
|
}
|
||||||
initializeWhenReady(document);
|
chrome.storage.local.get(["customButtonIcons"], function(loc) {
|
||||||
|
tc.settings.customButtonIcons =
|
||||||
|
loc &&
|
||||||
|
loc.customButtonIcons &&
|
||||||
|
typeof loc.customButtonIcons === "object"
|
||||||
|
? loc.customButtonIcons
|
||||||
|
: {};
|
||||||
|
|
||||||
|
if (!window.vscCustomIconListener) {
|
||||||
|
window.vscCustomIconListener = true;
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
var act = btn.dataset.action;
|
||||||
|
if (!act) return;
|
||||||
|
var svg =
|
||||||
|
tc.settings.customButtonIcons &&
|
||||||
|
tc.settings.customButtonIcons[act] &&
|
||||||
|
tc.settings.customButtonIcons[act].svg;
|
||||||
|
vscClearElement(btn);
|
||||||
|
if (svg) {
|
||||||
|
var cw = vscCreateSvgWrap(doc, svg, "vsc-btn-icon");
|
||||||
|
if (cw) {
|
||||||
|
btn.appendChild(cw);
|
||||||
|
} else {
|
||||||
|
var cdf = controllerButtonDefs[act];
|
||||||
|
btn.textContent = (cdf && cdf.label) || "?";
|
||||||
|
}
|
||||||
|
} else if (typeof vscIconWrap === "function") {
|
||||||
|
var wrap = vscIconWrap(doc, act, 14);
|
||||||
|
if (wrap) {
|
||||||
|
btn.appendChild(wrap);
|
||||||
|
} else {
|
||||||
|
var cdf = controllerButtonDefs[act];
|
||||||
|
btn.textContent = (cdf && cdf.label) || "?";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var cdf2 = controllerButtonDefs[act];
|
||||||
|
btn.textContent = (cdf2 && cdf2.label) || "?";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updateSubtitleNudgeIndicator(video);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
initializeWhenReady(document);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function getKeyBindings(action, what = "value") {
|
function getKeyBindings(action, what = "value") {
|
||||||
@@ -1313,6 +1327,7 @@ function getKeyBindings(action, what = "value") {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setKeyBindings(action, value) {
|
function setKeyBindings(action, value) {
|
||||||
tc.settings.keyBindings.find((item) => item.action === action)["value"] =
|
tc.settings.keyBindings.find((item) => item.action === action)["value"] =
|
||||||
value;
|
value;
|
||||||
@@ -1321,7 +1336,27 @@ function setKeyBindings(action, value) {
|
|||||||
function createControllerButton(doc, action, label, className) {
|
function createControllerButton(doc, action, label, className) {
|
||||||
var button = doc.createElement("button");
|
var button = doc.createElement("button");
|
||||||
button.dataset.action = action;
|
button.dataset.action = action;
|
||||||
button.textContent = label;
|
var custom =
|
||||||
|
tc.settings.customButtonIcons &&
|
||||||
|
tc.settings.customButtonIcons[action] &&
|
||||||
|
tc.settings.customButtonIcons[action].svg;
|
||||||
|
if (custom) {
|
||||||
|
var customWrap = vscCreateSvgWrap(doc, custom, "vsc-btn-icon");
|
||||||
|
if (customWrap) {
|
||||||
|
button.appendChild(customWrap);
|
||||||
|
} else {
|
||||||
|
button.textContent = label || "?";
|
||||||
|
}
|
||||||
|
} else if (typeof vscIconWrap === "function") {
|
||||||
|
var wrap = vscIconWrap(doc, action, 14);
|
||||||
|
if (wrap) {
|
||||||
|
button.appendChild(wrap);
|
||||||
|
} else {
|
||||||
|
button.textContent = label || "?";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
button.textContent = label || "?";
|
||||||
|
}
|
||||||
if (className) {
|
if (className) {
|
||||||
button.className = className;
|
button.className = className;
|
||||||
}
|
}
|
||||||
@@ -1329,7 +1364,7 @@ function createControllerButton(doc, action, label, className) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function defineVideoController() {
|
function defineVideoController() {
|
||||||
tc.videoController = function (target, parent) {
|
tc.videoController = function(target, parent) {
|
||||||
if (target.vsc) return target.vsc;
|
if (target.vsc) return target.vsc;
|
||||||
tc.mediaElements.push(target);
|
tc.mediaElements.push(target);
|
||||||
target.vsc = this;
|
target.vsc = this;
|
||||||
@@ -1343,11 +1378,13 @@ function defineVideoController() {
|
|||||||
this.suppressedRateChangeCount = 0;
|
this.suppressedRateChangeCount = 0;
|
||||||
this.suppressedRateChangeUntil = 0;
|
this.suppressedRateChangeUntil = 0;
|
||||||
this.visibilityResumeHandler = null;
|
this.visibilityResumeHandler = null;
|
||||||
|
this.resetToggleArmed = false;
|
||||||
|
this.resetButtonEl = null;
|
||||||
this.controllerLocation = normalizeControllerLocation(
|
this.controllerLocation = normalizeControllerLocation(
|
||||||
tc.settings.controllerLocation
|
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);
|
let storedSpeed = sanitizeSpeed(resolveTargetSpeed(target), 1.0);
|
||||||
this.targetSpeed = storedSpeed;
|
this.targetSpeed = storedSpeed;
|
||||||
@@ -1367,9 +1404,9 @@ function defineVideoController() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
if (
|
||||||
event.type === "loadedmetadata" ||
|
event.type === "loadedmetadata" ||
|
||||||
event.type === "loadeddata" ||
|
event.type === "loadeddata" ||
|
||||||
@@ -1487,7 +1524,7 @@ function defineVideoController() {
|
|||||||
this.startSubtitleNudge();
|
this.startSubtitleNudge();
|
||||||
};
|
};
|
||||||
|
|
||||||
tc.videoController.prototype.remove = function () {
|
tc.videoController.prototype.remove = function() {
|
||||||
this.stopSubtitleNudge();
|
this.stopSubtitleNudge();
|
||||||
if (this.youTubeAutoHideObserver) {
|
if (this.youTubeAutoHideObserver) {
|
||||||
this.youTubeAutoHideObserver.disconnect();
|
this.youTubeAutoHideObserver.disconnect();
|
||||||
@@ -1519,7 +1556,7 @@ function defineVideoController() {
|
|||||||
if (idx != -1) tc.mediaElements.splice(idx, 1);
|
if (idx != -1) tc.mediaElements.splice(idx, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
tc.videoController.prototype.startSubtitleNudge = function () {
|
tc.videoController.prototype.startSubtitleNudge = function() {
|
||||||
if (
|
if (
|
||||||
!isSubtitleNudgeSupported(this.video) ||
|
!isSubtitleNudgeSupported(this.video) ||
|
||||||
!isSubtitleNudgeEnabledForVideo(this.video) ||
|
!isSubtitleNudgeEnabledForVideo(this.video) ||
|
||||||
@@ -1592,7 +1629,7 @@ function defineVideoController() {
|
|||||||
log(`Nudge: Starting with interval ${tc.settings.subtitleNudgeInterval}ms.`, 5);
|
log(`Nudge: Starting with interval ${tc.settings.subtitleNudgeInterval}ms.`, 5);
|
||||||
};
|
};
|
||||||
|
|
||||||
tc.videoController.prototype.stopSubtitleNudge = function () {
|
tc.videoController.prototype.stopSubtitleNudge = function() {
|
||||||
if (this.nudgeAnimationId !== null) {
|
if (this.nudgeAnimationId !== null) {
|
||||||
clearTimeout(this.nudgeAnimationId);
|
clearTimeout(this.nudgeAnimationId);
|
||||||
this.nudgeAnimationId = null;
|
this.nudgeAnimationId = null;
|
||||||
@@ -1610,7 +1647,7 @@ function defineVideoController() {
|
|||||||
// doesn't lose the user's intended speed if the site hijacks it.
|
// doesn't lose the user's intended speed if the site hijacks it.
|
||||||
};
|
};
|
||||||
|
|
||||||
tc.videoController.prototype.performImmediateNudge = function () {
|
tc.videoController.prototype.performImmediateNudge = function() {
|
||||||
if (
|
if (
|
||||||
!isSubtitleNudgeSupported(this.video) ||
|
!isSubtitleNudgeSupported(this.video) ||
|
||||||
!isSubtitleNudgeEnabledForVideo(this.video) ||
|
!isSubtitleNudgeEnabledForVideo(this.video) ||
|
||||||
@@ -1640,7 +1677,7 @@ function defineVideoController() {
|
|||||||
log(`Immediate nudge performed at rate ${targetRate.toFixed(2)}`, 5);
|
log(`Immediate nudge performed at rate ${targetRate.toFixed(2)}`, 5);
|
||||||
};
|
};
|
||||||
|
|
||||||
tc.videoController.prototype.setupYouTubeAutoHide = function (wrapper) {
|
tc.videoController.prototype.setupYouTubeAutoHide = function(wrapper) {
|
||||||
if (!wrapper || !isOnYouTube()) return;
|
if (!wrapper || !isOnYouTube()) return;
|
||||||
|
|
||||||
const video = this.video;
|
const video = this.video;
|
||||||
@@ -1656,7 +1693,7 @@ function defineVideoController() {
|
|||||||
// The vsc-hidden class (from V key) takes precedence via CSS specificity
|
// The vsc-hidden class (from V key) takes precedence via CSS specificity
|
||||||
if (ytPlayer.classList.contains("ytp-autohide")) {
|
if (ytPlayer.classList.contains("ytp-autohide")) {
|
||||||
wrapper.classList.add("ytp-autohide");
|
wrapper.classList.add("ytp-autohide");
|
||||||
|
|
||||||
// Immediately end any temporary "vsc-show" state to hide with YouTube
|
// Immediately end any temporary "vsc-show" state to hide with YouTube
|
||||||
// UNLESS it was forced by a shortcut (vsc-forced-show)
|
// UNLESS it was forced by a shortcut (vsc-forced-show)
|
||||||
if (!wrapper.classList.contains("vsc-forced-show")) {
|
if (!wrapper.classList.contains("vsc-forced-show")) {
|
||||||
@@ -1666,7 +1703,7 @@ function defineVideoController() {
|
|||||||
wrapper.showTimeOut = undefined;
|
wrapper.showTimeOut = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log("YouTube controls hidden, hiding controller", 5);
|
log("YouTube controls hidden, hiding controller", 5);
|
||||||
} else {
|
} else {
|
||||||
wrapper.classList.remove("ytp-autohide");
|
wrapper.classList.remove("ytp-autohide");
|
||||||
@@ -1716,7 +1753,7 @@ function defineVideoController() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
tc.videoController.prototype.setupGenericAutoHide = function (wrapper) {
|
tc.videoController.prototype.setupGenericAutoHide = function(wrapper) {
|
||||||
if (!wrapper) return;
|
if (!wrapper) return;
|
||||||
|
|
||||||
const video = this.video;
|
const video = this.video;
|
||||||
@@ -1775,7 +1812,7 @@ function defineVideoController() {
|
|||||||
log(`Generic auto-hide setup complete with ${tc.settings.hideWithControlsTimer}s timer`, 4);
|
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 doc = this.video.ownerDocument;
|
||||||
const speed = this.video.playbackRate.toFixed(2);
|
const speed = this.video.playbackRate.toFixed(2);
|
||||||
var wrapper = doc.createElement("div");
|
var wrapper = doc.createElement("div");
|
||||||
@@ -1813,7 +1850,7 @@ function defineVideoController() {
|
|||||||
|
|
||||||
var subtitleNudgeIndicator = null;
|
var subtitleNudgeIndicator = null;
|
||||||
|
|
||||||
buttonConfig.forEach(function (btnId) {
|
buttonConfig.forEach(function(btnId) {
|
||||||
if (btnId === "nudge") {
|
if (btnId === "nudge") {
|
||||||
subtitleNudgeIndicator = doc.createElement("span");
|
subtitleNudgeIndicator = doc.createElement("span");
|
||||||
subtitleNudgeIndicator.id = "nudge-indicator";
|
subtitleNudgeIndicator.id = "nudge-indicator";
|
||||||
@@ -1836,24 +1873,36 @@ function defineVideoController() {
|
|||||||
nudgeFlashIndicator.setAttribute("aria-hidden", "true");
|
nudgeFlashIndicator.setAttribute("aria-hidden", "true");
|
||||||
|
|
||||||
controller.appendChild(dragHandle);
|
controller.appendChild(dragHandle);
|
||||||
controller.appendChild(nudgeFlashIndicator);
|
|
||||||
controller.appendChild(controls);
|
controller.appendChild(controls);
|
||||||
|
/* Flash sits after #controls so it never inserts space between speed and buttons. */
|
||||||
|
controller.appendChild(nudgeFlashIndicator);
|
||||||
shadow.appendChild(controller);
|
shadow.appendChild(controller);
|
||||||
|
|
||||||
this.speedIndicator = dragHandle;
|
this.speedIndicator = dragHandle;
|
||||||
this.subtitleNudgeIndicator = subtitleNudgeIndicator;
|
this.subtitleNudgeIndicator = subtitleNudgeIndicator;
|
||||||
this.nudgeFlashIndicator = nudgeFlashIndicator;
|
this.nudgeFlashIndicator = nudgeFlashIndicator;
|
||||||
|
this.resetButtonEl =
|
||||||
|
shadow.querySelector("button[data-action=\"reset\"]") || null;
|
||||||
|
this.resetToggleArmed = false;
|
||||||
if (subtitleNudgeIndicator) {
|
if (subtitleNudgeIndicator) {
|
||||||
updateSubtitleNudgeIndicator(this.video);
|
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() {
|
||||||
|
target.blur();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dragHandle.addEventListener(
|
dragHandle.addEventListener(
|
||||||
"mousedown",
|
"mousedown",
|
||||||
(e) => {
|
(e) => {
|
||||||
runAction(
|
var dragAction = dragHandle.dataset.action;
|
||||||
e.target.dataset["action"],
|
runAction(dragAction, getKeyBindings(dragAction, "value"), e);
|
||||||
getKeyBindings(e.target.dataset["action"], "value"),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
@@ -1862,11 +1911,9 @@ function defineVideoController() {
|
|||||||
button.addEventListener(
|
button.addEventListener(
|
||||||
"click",
|
"click",
|
||||||
(e) => {
|
(e) => {
|
||||||
runAction(
|
var action = button.dataset.action;
|
||||||
e.target.dataset["action"],
|
runAction(action, getKeyBindings(action), e);
|
||||||
getKeyBindings(e.target.dataset["action"]),
|
blurAfterPointerTap(button, e);
|
||||||
e
|
|
||||||
);
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
@@ -1881,6 +1928,7 @@ function defineVideoController() {
|
|||||||
var newState = !isSubtitleNudgeEnabledForVideo(video);
|
var newState = !isSubtitleNudgeEnabledForVideo(video);
|
||||||
setSubtitleNudgeEnabledForVideo(video, newState);
|
setSubtitleNudgeEnabledForVideo(video, newState);
|
||||||
}
|
}
|
||||||
|
blurAfterPointerTap(subtitleNudgeIndicator, e);
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
@@ -1897,7 +1945,7 @@ function defineVideoController() {
|
|||||||
this.setupGenericAutoHide(wrapper);
|
this.setupGenericAutoHide(wrapper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var fragment = doc.createDocumentFragment();
|
var fragment = doc.createDocumentFragment();
|
||||||
fragment.appendChild(wrapper);
|
fragment.appendChild(wrapper);
|
||||||
const parentEl = this.parent || this.video.parentElement;
|
const parentEl = this.parent || this.video.parentElement;
|
||||||
@@ -1963,10 +2011,6 @@ function defineVideoController() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeStringRegExp(str) {
|
|
||||||
const m = /[|\\{}()[\]^$+*?.]/g;
|
|
||||||
return str.replace(m, "\\$&");
|
|
||||||
}
|
|
||||||
function applySiteRuleOverrides() {
|
function applySiteRuleOverrides() {
|
||||||
resetSettingsFromSiteRuleBase();
|
resetSettingsFromSiteRuleBase();
|
||||||
|
|
||||||
@@ -1975,34 +2019,7 @@ function applySiteRuleOverrides() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentUrl = location.href;
|
var currentUrl = location.href;
|
||||||
var matchedRule = null;
|
var matchedRule = siteRuleUtils.matchSiteRule(currentUrl, tc.settings.siteRules);
|
||||||
|
|
||||||
for (var i = 0; i < tc.settings.siteRules.length; i++) {
|
|
||||||
var rule = tc.settings.siteRules[i];
|
|
||||||
var pattern = rule.pattern;
|
|
||||||
if (!pattern || pattern.length === 0) continue;
|
|
||||||
|
|
||||||
var regex;
|
|
||||||
if (pattern.startsWith("/") && pattern.lastIndexOf("/") > 0) {
|
|
||||||
try {
|
|
||||||
var lastSlash = pattern.lastIndexOf("/");
|
|
||||||
regex = new RegExp(
|
|
||||||
pattern.substring(1, lastSlash),
|
|
||||||
pattern.substring(lastSlash + 1)
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
log(`Invalid site rule regex: ${pattern}. ${e.message}`, 2);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
regex = new RegExp(escapeStringRegExp(pattern));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (regex && regex.test(currentUrl)) {
|
|
||||||
matchedRule = rule;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!matchedRule) return false;
|
if (!matchedRule) return false;
|
||||||
|
|
||||||
@@ -2010,13 +2027,9 @@ function applySiteRuleOverrides() {
|
|||||||
log(`Matched site rule: ${matchedRule.pattern}`, 4);
|
log(`Matched site rule: ${matchedRule.pattern}`, 4);
|
||||||
|
|
||||||
// Check if extension should be enabled/disabled on this site
|
// Check if extension should be enabled/disabled on this site
|
||||||
if (matchedRule.enabled === false) {
|
if (siteRuleUtils.isSiteRuleDisabled(matchedRule)) {
|
||||||
log(`Extension disabled for site: ${currentUrl}`, 4);
|
log(`Extension disabled for site: ${currentUrl}`, 4);
|
||||||
return true;
|
return true;
|
||||||
} else if (matchedRule.disableExtension === true) {
|
|
||||||
// Handle old format
|
|
||||||
log(`Extension disabled (legacy) for site: ${currentUrl}`, 4);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override general settings with site-specific overrides
|
// Override general settings with site-specific overrides
|
||||||
@@ -2045,7 +2058,7 @@ function applySiteRuleOverrides() {
|
|||||||
[
|
[
|
||||||
"controllerMarginTop",
|
"controllerMarginTop",
|
||||||
"controllerMarginBottom"
|
"controllerMarginBottom"
|
||||||
].forEach(function (key) {
|
].forEach(function(key) {
|
||||||
tc.settings[key] = normalizeControllerMarginPx(tc.settings[key], 0);
|
tc.settings[key] = normalizeControllerMarginPx(tc.settings[key], 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2074,6 +2087,25 @@ function applySiteRuleOverrides() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Apply current tc.settings controller layout/opacity to every attached controller (after site rules). */
|
||||||
|
function refreshAllControllerGeometry() {
|
||||||
|
tc.mediaElements.forEach(function(video) {
|
||||||
|
if (!video || !video.vsc) return;
|
||||||
|
applyControllerLocation(video.vsc, tc.settings.controllerLocation);
|
||||||
|
var controllerEl = getControllerElement(video.vsc);
|
||||||
|
if (controllerEl) {
|
||||||
|
controllerEl.style.opacity = String(tc.settings.controllerOpacity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Re-match site rules for current URL and refresh controller position/opacity on every video. */
|
||||||
|
function reapplySiteRulesAndControllerGeometry() {
|
||||||
|
var siteDisabled = applySiteRuleOverrides();
|
||||||
|
if (!tc.settings.enabled || siteDisabled) return;
|
||||||
|
refreshAllControllerGeometry();
|
||||||
|
}
|
||||||
|
|
||||||
function shouldPreserveDesiredSpeed(video, speed) {
|
function shouldPreserveDesiredSpeed(video, speed) {
|
||||||
if (!video || !video.vsc) return false;
|
if (!video || !video.vsc) return false;
|
||||||
var desiredSpeed = getDesiredSpeed(video);
|
var desiredSpeed = getDesiredSpeed(video);
|
||||||
@@ -2091,8 +2123,12 @@ function shouldPreserveDesiredSpeed(video, speed) {
|
|||||||
function setupListener(root) {
|
function setupListener(root) {
|
||||||
root = root || document;
|
root = root || document;
|
||||||
if (root.vscRateListenerAttached) return;
|
if (root.vscRateListenerAttached) return;
|
||||||
function updateSpeedFromEvent(video) {
|
|
||||||
|
function updateSpeedFromEvent(video, skipResetDisarm) {
|
||||||
if (!video.vsc || !video.vsc.speedIndicator) return;
|
if (!video.vsc || !video.vsc.speedIndicator) return;
|
||||||
|
if (!skipResetDisarm) {
|
||||||
|
video.vsc.resetToggleArmed = false;
|
||||||
|
}
|
||||||
var speed = video.playbackRate; // Preserve full precision (e.g. 0.0625)
|
var speed = video.playbackRate; // Preserve full precision (e.g. 0.0625)
|
||||||
video.vsc.speedIndicator.textContent = speed.toFixed(2);
|
video.vsc.speedIndicator.textContent = speed.toFixed(2);
|
||||||
video.vsc.targetSpeed = speed;
|
video.vsc.targetSpeed = speed;
|
||||||
@@ -2108,9 +2144,10 @@ function setupListener(root) {
|
|||||||
else video.vsc.startSubtitleNudge();
|
else video.vsc.startSubtitleNudge();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
root.addEventListener(
|
root.addEventListener(
|
||||||
"ratechange",
|
"ratechange",
|
||||||
function (event) {
|
function(event) {
|
||||||
if (tc.isNudging) return;
|
if (tc.isNudging) return;
|
||||||
var video = event.target;
|
var video = event.target;
|
||||||
if (!video || typeof video.playbackRate === "undefined" || !video.vsc)
|
if (!video || typeof video.playbackRate === "undefined" || !video.vsc)
|
||||||
@@ -2119,7 +2156,7 @@ function setupListener(root) {
|
|||||||
if (tc.settings.forceLastSavedSpeed) {
|
if (tc.settings.forceLastSavedSpeed) {
|
||||||
if (event.detail && event.detail.origin === "videoSpeed") {
|
if (event.detail && event.detail.origin === "videoSpeed") {
|
||||||
video.playbackRate = event.detail.speed;
|
video.playbackRate = event.detail.speed;
|
||||||
updateSpeedFromEvent(video);
|
updateSpeedFromEvent(video, true);
|
||||||
} else {
|
} else {
|
||||||
video.playbackRate = sanitizeSpeed(tc.settings.lastSpeed, 1.0);
|
video.playbackRate = sanitizeSpeed(tc.settings.lastSpeed, 1.0);
|
||||||
}
|
}
|
||||||
@@ -2130,7 +2167,7 @@ function setupListener(root) {
|
|||||||
var pendingRateChange = takePendingRateChange(video, currentSpeed);
|
var pendingRateChange = takePendingRateChange(video, currentSpeed);
|
||||||
|
|
||||||
if (pendingRateChange) {
|
if (pendingRateChange) {
|
||||||
updateSpeedFromEvent(video);
|
updateSpeedFromEvent(video, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2139,6 +2176,7 @@ function setupListener(root) {
|
|||||||
`Ignoring external rate change to ${currentSpeed.toFixed(4)} while preserving ${desiredSpeed.toFixed(4)}`,
|
`Ignoring external rate change to ${currentSpeed.toFixed(4)} while preserving ${desiredSpeed.toFixed(4)}`,
|
||||||
4
|
4
|
||||||
);
|
);
|
||||||
|
video.vsc.resetToggleArmed = false;
|
||||||
video.vsc.speedIndicator.textContent = desiredSpeed.toFixed(2);
|
video.vsc.speedIndicator.textContent = desiredSpeed.toFixed(2);
|
||||||
scheduleSpeedRestore(video, desiredSpeed, "pause/play or seek");
|
scheduleSpeedRestore(video, desiredSpeed, "pause/play or seek");
|
||||||
return;
|
return;
|
||||||
@@ -2153,6 +2191,7 @@ function setupListener(root) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var vscInitializedDocuments = new Set();
|
var vscInitializedDocuments = new Set();
|
||||||
|
|
||||||
function clearPendingInitialization(doc) {
|
function clearPendingInitialization(doc) {
|
||||||
if (!doc || !doc.vscPendingInitializeHandler) return;
|
if (!doc || !doc.vscPendingInitializeHandler) return;
|
||||||
|
|
||||||
@@ -2189,7 +2228,7 @@ function initializeWhenReady(doc, forceReinit = false) {
|
|||||||
|
|
||||||
if (doc.vscPendingInitializeHandler) return;
|
if (doc.vscPendingInitializeHandler) return;
|
||||||
|
|
||||||
var pendingInitializeHandler = function () {
|
var pendingInitializeHandler = function() {
|
||||||
tryInitializeDocument(doc, doc.vscPendingForceReinit === true);
|
tryInitializeDocument(doc, doc.vscPendingForceReinit === true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2204,6 +2243,7 @@ function initializeWhenReady(doc, forceReinit = false) {
|
|||||||
setTimeout(pendingInitializeHandler, 0);
|
setTimeout(pendingInitializeHandler, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function inIframe() {
|
function inIframe() {
|
||||||
try {
|
try {
|
||||||
return window.self !== window.top;
|
return window.self !== window.top;
|
||||||
@@ -2216,13 +2256,14 @@ function attachKeydownListeners(doc) {
|
|||||||
var docs = [doc];
|
var docs = [doc];
|
||||||
try {
|
try {
|
||||||
if (inIframe() && window.top.document !== doc) docs.push(window.top.document);
|
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;
|
if (keyDoc.vscKeydownListenerAttached) return;
|
||||||
keyDoc.addEventListener(
|
keyDoc.addEventListener(
|
||||||
"keydown",
|
"keydown",
|
||||||
function (event) {
|
function(event) {
|
||||||
if (
|
if (
|
||||||
!event.getModifierState ||
|
!event.getModifierState ||
|
||||||
event.getModifierState("Alt") ||
|
event.getModifierState("Alt") ||
|
||||||
@@ -2245,7 +2286,7 @@ function attachKeydownListeners(doc) {
|
|||||||
|
|
||||||
if (!tc.mediaElements.length) return;
|
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);
|
return matchesKeyBinding(binding, event);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2269,24 +2310,24 @@ function attachMutationObserver(root) {
|
|||||||
|
|
||||||
var pendingMutations = [];
|
var pendingMutations = [];
|
||||||
var mutationProcessingScheduled = false;
|
var mutationProcessingScheduled = false;
|
||||||
var observer = new MutationObserver(function (mutations) {
|
var observer = new MutationObserver(function(mutations) {
|
||||||
pendingMutations.push(...mutations);
|
pendingMutations.push(...mutations);
|
||||||
if (mutationProcessingScheduled) return;
|
if (mutationProcessingScheduled) return;
|
||||||
|
|
||||||
mutationProcessingScheduled = true;
|
mutationProcessingScheduled = true;
|
||||||
requestIdle(
|
requestIdle(
|
||||||
function () {
|
function() {
|
||||||
var mutationsToProcess = pendingMutations.splice(0);
|
var mutationsToProcess = pendingMutations.splice(0);
|
||||||
mutationProcessingScheduled = false;
|
mutationProcessingScheduled = false;
|
||||||
|
|
||||||
mutationsToProcess.forEach(function (mutation) {
|
mutationsToProcess.forEach(function(mutation) {
|
||||||
if (mutation.type === "childList") {
|
if (mutation.type === "childList") {
|
||||||
mutation.addedNodes.forEach(function (node) {
|
mutation.addedNodes.forEach(function(node) {
|
||||||
// Skip text nodes, comments, etc. — only elements can contain media
|
// Skip text nodes, comments, etc. — only elements can contain media
|
||||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||||
scanNodeForMedia(node, node.parentNode || mutation.target, true);
|
scanNodeForMedia(node, node.parentNode || mutation.target, true);
|
||||||
});
|
});
|
||||||
mutation.removedNodes.forEach(function (node) {
|
mutation.removedNodes.forEach(function(node) {
|
||||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||||
scanNodeForMedia(node, node.parentNode || mutation.target, false);
|
scanNodeForMedia(node, node.parentNode || mutation.target, false);
|
||||||
});
|
});
|
||||||
@@ -2335,7 +2376,7 @@ function attachMutationObserver(root) {
|
|||||||
function attachMediaDetectionListeners(root) {
|
function attachMediaDetectionListeners(root) {
|
||||||
if (root.vscMediaEventListenersAttached) return;
|
if (root.vscMediaEventListenersAttached) return;
|
||||||
|
|
||||||
var handleDetectedMedia = function (event) {
|
var handleDetectedMedia = function(event) {
|
||||||
var target = event.target;
|
var target = event.target;
|
||||||
if (!isMediaElement(target)) return;
|
if (!isMediaElement(target)) return;
|
||||||
ensureController(target, target.parentElement || target.parentNode);
|
ensureController(target, target.parentElement || target.parentNode);
|
||||||
@@ -2348,21 +2389,22 @@ function attachMediaDetectionListeners(root) {
|
|||||||
"canplay",
|
"canplay",
|
||||||
"playing",
|
"playing",
|
||||||
"play"
|
"play"
|
||||||
].forEach(function (eventName) {
|
].forEach(function(eventName) {
|
||||||
root.addEventListener(eventName, handleDetectedMedia, true);
|
root.addEventListener(eventName, handleDetectedMedia, true);
|
||||||
});
|
});
|
||||||
root.vscMediaEventListenersAttached = true;
|
root.vscMediaEventListenersAttached = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachIframeListeners(doc) {
|
function attachIframeListeners(doc) {
|
||||||
Array.from(doc.getElementsByTagName("iframe")).forEach(function (frame) {
|
Array.from(doc.getElementsByTagName("iframe")).forEach(function(frame) {
|
||||||
if (!frame.vscLoadListenerAttached) {
|
if (!frame.vscLoadListenerAttached) {
|
||||||
frame.addEventListener("load", function () {
|
frame.addEventListener("load", function() {
|
||||||
try {
|
try {
|
||||||
if (frame.contentDocument) {
|
if (frame.contentDocument) {
|
||||||
initializeWhenReady(frame.contentDocument, true);
|
initializeWhenReady(frame.contentDocument, true);
|
||||||
}
|
}
|
||||||
} catch (e) { }
|
} catch (e) {
|
||||||
|
}
|
||||||
});
|
});
|
||||||
frame.vscLoadListenerAttached = true;
|
frame.vscLoadListenerAttached = true;
|
||||||
}
|
}
|
||||||
@@ -2371,24 +2413,25 @@ function attachIframeListeners(doc) {
|
|||||||
if (frame.contentDocument) {
|
if (frame.contentDocument) {
|
||||||
initializeWhenReady(frame.contentDocument);
|
initializeWhenReady(frame.contentDocument);
|
||||||
}
|
}
|
||||||
} catch (e) { }
|
} catch (e) {
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachNavigationListeners() {
|
function attachNavigationListeners() {
|
||||||
if (window.vscNavigationListenersAttached) return;
|
if (window.vscNavigationListenersAttached) return;
|
||||||
|
|
||||||
var scheduleRescan = function () {
|
var scheduleRescan = function() {
|
||||||
clearTimeout(window.vscNavigationRescanTimer);
|
clearTimeout(window.vscNavigationRescanTimer);
|
||||||
window.vscNavigationRescanTimer = setTimeout(function () {
|
window.vscNavigationRescanTimer = setTimeout(function() {
|
||||||
initializeWhenReady(document, true);
|
initializeWhenReady(document, true);
|
||||||
}, 300);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
["pushState", "replaceState"].forEach(function (method) {
|
["pushState", "replaceState"].forEach(function(method) {
|
||||||
if (typeof history[method] !== "function") return;
|
if (typeof history[method] !== "function") return;
|
||||||
var original = history[method];
|
var original = history[method];
|
||||||
history[method] = function () {
|
history[method] = function() {
|
||||||
var result = original.apply(this, arguments);
|
var result = original.apply(this, arguments);
|
||||||
scheduleRescan();
|
scheduleRescan();
|
||||||
return result;
|
return result;
|
||||||
@@ -2397,6 +2440,10 @@ function attachNavigationListeners() {
|
|||||||
|
|
||||||
window.addEventListener("popstate", scheduleRescan);
|
window.addEventListener("popstate", scheduleRescan);
|
||||||
window.addEventListener("hashchange", scheduleRescan);
|
window.addEventListener("hashchange", scheduleRescan);
|
||||||
|
/* YouTube often navigates without a history API call the extension can see first */
|
||||||
|
if (typeof document !== "undefined" && isOnYouTube()) {
|
||||||
|
document.addEventListener("yt-navigate-finish", scheduleRescan);
|
||||||
|
}
|
||||||
window.vscNavigationListenersAttached = true;
|
window.vscNavigationListenersAttached = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2416,20 +2463,19 @@ function initializeNow(doc, forceReinit = false) {
|
|||||||
|
|
||||||
if (forceReinit) {
|
if (forceReinit) {
|
||||||
log("Force re-initialization requested", 4);
|
log("Force re-initialization requested", 4);
|
||||||
tc.mediaElements.forEach(function (video) {
|
refreshAllControllerGeometry();
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vscInitializedDocuments.add(doc);
|
vscInitializedDocuments.add(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSpeed(video, speed, isInitialCall = false, isUserKeyPress = false) {
|
function setSpeed(
|
||||||
|
video,
|
||||||
|
speed,
|
||||||
|
isInitialCall = false,
|
||||||
|
isUserKeyPress = false,
|
||||||
|
fromResetSpeedToggle = false
|
||||||
|
) {
|
||||||
const numericSpeed = Number(speed);
|
const numericSpeed = Number(speed);
|
||||||
|
|
||||||
if (!isValidSpeed(numericSpeed)) {
|
if (!isValidSpeed(numericSpeed)) {
|
||||||
@@ -2442,6 +2488,10 @@ function setSpeed(video, speed, isInitialCall = false, isUserKeyPress = false) {
|
|||||||
|
|
||||||
if (!video || !video.vsc || !video.vsc.speedIndicator) return;
|
if (!video || !video.vsc || !video.vsc.speedIndicator) return;
|
||||||
|
|
||||||
|
if (isUserKeyPress && !fromResetSpeedToggle) {
|
||||||
|
video.vsc.resetToggleArmed = false;
|
||||||
|
}
|
||||||
|
|
||||||
log(
|
log(
|
||||||
`setSpeed: Target ${numericSpeed.toFixed(2)}. Initial: ${isInitialCall}. UserKeyPress: ${isUserKeyPress}`,
|
`setSpeed: Target ${numericSpeed.toFixed(2)}. Initial: ${isInitialCall}. UserKeyPress: ${isUserKeyPress}`,
|
||||||
4
|
4
|
||||||
@@ -2528,7 +2578,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
|
if (!v.vsc) return; // Don't process videos without a controller
|
||||||
var controller = v.vsc.div;
|
var controller = v.vsc.div;
|
||||||
const userDrivenActionsThatShowController = [
|
const userDrivenActionsThatShowController = [
|
||||||
@@ -2544,10 +2594,18 @@ function runAction(action, value, e) {
|
|||||||
"mark",
|
"mark",
|
||||||
"jump",
|
"jump",
|
||||||
"drag",
|
"drag",
|
||||||
|
"nudge",
|
||||||
"toggleSubtitleNudge",
|
"toggleSubtitleNudge",
|
||||||
"display"
|
"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);
|
showController(controller, 2000, true);
|
||||||
}
|
}
|
||||||
if (v.classList.contains("vsc-cancelled")) return;
|
if (v.classList.contains("vsc-cancelled")) return;
|
||||||
@@ -2659,6 +2717,12 @@ function runAction(action, value, e) {
|
|||||||
case "toggleSubtitleNudge":
|
case "toggleSubtitleNudge":
|
||||||
setSubtitleNudgeEnabledForVideo(v, subtitleNudgeToggleValue);
|
setSubtitleNudgeEnabledForVideo(v, subtitleNudgeToggleValue);
|
||||||
break;
|
break;
|
||||||
|
case "nudge":
|
||||||
|
setSubtitleNudgeEnabledForVideo(
|
||||||
|
v,
|
||||||
|
!isSubtitleNudgeEnabledForVideo(v)
|
||||||
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
log("runAction End", 5);
|
log("runAction End", 5);
|
||||||
@@ -2697,11 +2761,12 @@ function resetSpeed(v, target, isFastKey = false) {
|
|||||||
Math.abs(lastToggle - 1.0) < 0.01
|
Math.abs(lastToggle - 1.0) < 0.01
|
||||||
? getKeyBindings("fast") || 1.8
|
? getKeyBindings("fast") || 1.8
|
||||||
: lastToggle;
|
: lastToggle;
|
||||||
setSpeed(v, speedToRestore, false, true);
|
setSpeed(v, speedToRestore, false, true, true);
|
||||||
} else {
|
} else {
|
||||||
// Not at 1.0, save current as toggle speed and go to 1.0
|
// Not at 1.0, save current as toggle speed and go to 1.0
|
||||||
lastToggleSpeed[videoId] = currentSpeed;
|
lastToggleSpeed[videoId] = currentSpeed;
|
||||||
setSpeed(v, resetSpeedValue, false, true);
|
v.vsc.resetToggleArmed = true;
|
||||||
|
setSpeed(v, resetSpeedValue, false, true, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2709,15 +2774,18 @@ function resetSpeed(v, target, isFastKey = false) {
|
|||||||
function muted(v) {
|
function muted(v) {
|
||||||
v.muted = !v.muted;
|
v.muted = !v.muted;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMark(v) {
|
function setMark(v) {
|
||||||
v.vsc.mark = v.currentTime;
|
v.vsc.mark = v.currentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
function jumpToMark(v) {
|
function jumpToMark(v) {
|
||||||
if (v.vsc && typeof v.vsc.mark === "number") {
|
if (v.vsc && typeof v.vsc.mark === "number") {
|
||||||
extendSpeedRestoreWindow(v);
|
extendSpeedRestoreWindow(v);
|
||||||
v.currentTime = v.vsc.mark;
|
v.currentTime = v.vsc.mark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDrag(video, e) {
|
function handleDrag(video, e) {
|
||||||
const c = video.vsc.div;
|
const c = video.vsc.div;
|
||||||
const sC = convertControllerToManualPosition(video.vsc);
|
const sC = convertControllerToManualPosition(video.vsc);
|
||||||
@@ -2727,7 +2795,7 @@ function handleDrag(video, e) {
|
|||||||
pE.parentNode &&
|
pE.parentNode &&
|
||||||
pE.parentNode.offsetHeight === pE.offsetHeight &&
|
pE.parentNode.offsetHeight === pE.offsetHeight &&
|
||||||
pE.parentNode.offsetWidth === pE.offsetWidth
|
pE.parentNode.offsetWidth === pE.offsetWidth
|
||||||
)
|
)
|
||||||
pE = pE.parentNode;
|
pE = pE.parentNode;
|
||||||
video.classList.add("vcs-dragging");
|
video.classList.add("vcs-dragging");
|
||||||
sC.classList.add("dragging");
|
sC.classList.add("dragging");
|
||||||
@@ -2756,6 +2824,7 @@ function handleDrag(video, e) {
|
|||||||
pE.addEventListener("mouseleave", eD);
|
pE.addEventListener("mouseleave", eD);
|
||||||
pE.addEventListener("mousemove", sD);
|
pE.addEventListener("mousemove", sD);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showController(controller, duration = 2000, forced = false) {
|
function showController(controller, duration = 2000, forced = false) {
|
||||||
if (!controller || typeof controller.classList === "undefined") return;
|
if (!controller || typeof controller.classList === "undefined") return;
|
||||||
var restoreHidden =
|
var restoreHidden =
|
||||||
@@ -2773,7 +2842,7 @@ function showController(controller, duration = 2000, forced = false) {
|
|||||||
clearTimeout(controller.showTimeOut);
|
clearTimeout(controller.showTimeOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.showTimeOut = setTimeout(function () {
|
controller.showTimeOut = setTimeout(function() {
|
||||||
controller.classList.remove("vsc-show");
|
controller.classList.remove("vsc-show");
|
||||||
controller.classList.remove("vsc-forced-show");
|
controller.classList.remove("vsc-forced-show");
|
||||||
if (controller.restoreHiddenAfterShow === true) {
|
if (controller.restoreHiddenAfterShow === true) {
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
/**
|
||||||
|
* Lucide static icons via jsDelivr (same registry as lucide.dev).
|
||||||
|
* ISC License — https://lucide.dev
|
||||||
|
*/
|
||||||
|
var LUCIDE_STATIC_VERSION = "1.7.0";
|
||||||
|
var LUCIDE_CDN_BASE =
|
||||||
|
"https://cdn.jsdelivr.net/npm/lucide-static@" +
|
||||||
|
LUCIDE_STATIC_VERSION;
|
||||||
|
|
||||||
|
var LUCIDE_TAGS_CACHE_KEY = "lucideTagsCacheV1";
|
||||||
|
var LUCIDE_TAGS_MAX_AGE_MS = 1000 * 60 * 60 * 24 * 7; /* 7 days */
|
||||||
|
|
||||||
|
function lucideIconSvgUrl(slug) {
|
||||||
|
if (!slug || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/i.test(slug)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return LUCIDE_CDN_BASE + "/icons/" + slug.toLowerCase() + ".svg";
|
||||||
|
}
|
||||||
|
|
||||||
|
function lucideTagsJsonUrl() {
|
||||||
|
return LUCIDE_CDN_BASE + "/tags.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Collapse whitespace for smaller storage. */
|
||||||
|
function lucideMinifySvg(s) {
|
||||||
|
return String(s).replace(/\s+/g, " ").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeLucideSvg(svgText) {
|
||||||
|
if (!svgText || typeof svgText !== "string") return null;
|
||||||
|
var t = String(svgText).replace(/\0/g, "").trim();
|
||||||
|
if (!/<svg[\s>]/i.test(t)) return null;
|
||||||
|
var doc = new DOMParser().parseFromString(t, "image/svg+xml");
|
||||||
|
if (doc.querySelector("parsererror")) return null;
|
||||||
|
var svg = vscSanitizeSvgTree(doc.querySelector("svg"));
|
||||||
|
if (!svg) return null;
|
||||||
|
svg.removeAttribute("width");
|
||||||
|
svg.removeAttribute("height");
|
||||||
|
svg.setAttribute("width", "100%");
|
||||||
|
svg.setAttribute("height", "100%");
|
||||||
|
svg.setAttribute("aria-hidden", "true");
|
||||||
|
return lucideMinifySvg(svg.outerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchLucideSvg(slug) {
|
||||||
|
var url = lucideIconSvgUrl(slug);
|
||||||
|
if (!url) {
|
||||||
|
return Promise.reject(new Error("Invalid icon name"));
|
||||||
|
}
|
||||||
|
return fetch(url, { cache: "force-cache" }).then(function (r) {
|
||||||
|
if (!r.ok) {
|
||||||
|
throw new Error("Icon not found: " + slug);
|
||||||
|
}
|
||||||
|
return r.text();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchAndCacheLucideTags(chromeLocal, resolve, reject) {
|
||||||
|
fetch(lucideTagsJsonUrl(), { cache: "force-cache" })
|
||||||
|
.then(function (r) {
|
||||||
|
if (!r.ok) throw new Error("tags.json HTTP " + r.status);
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
|
.then(function (obj) {
|
||||||
|
var payload = {};
|
||||||
|
payload[LUCIDE_TAGS_CACHE_KEY] = obj;
|
||||||
|
payload[LUCIDE_TAGS_CACHE_KEY + "At"] = Date.now();
|
||||||
|
if (chromeLocal && chromeLocal.set) {
|
||||||
|
chromeLocal.set(payload, function () {
|
||||||
|
resolve(obj);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(obj);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {Promise<Object<string, string[]>>} slug -> tags */
|
||||||
|
function getLucideTagsMap(chromeLocal, bypassCache) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
if (!chromeLocal || !chromeLocal.get) {
|
||||||
|
fetch(lucideTagsJsonUrl(), { cache: "force-cache" })
|
||||||
|
.then(function (r) {
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bypassCache) {
|
||||||
|
fetchAndCacheLucideTags(chromeLocal, resolve, reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chromeLocal.get(
|
||||||
|
[LUCIDE_TAGS_CACHE_KEY, LUCIDE_TAGS_CACHE_KEY + "At"],
|
||||||
|
function (stored) {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
fetchAndCacheLucideTags(chromeLocal, resolve, reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var data = stored[LUCIDE_TAGS_CACHE_KEY];
|
||||||
|
var at = stored[LUCIDE_TAGS_CACHE_KEY + "At"];
|
||||||
|
if (
|
||||||
|
data &&
|
||||||
|
typeof data === "object" &&
|
||||||
|
at &&
|
||||||
|
Date.now() - at < LUCIDE_TAGS_MAX_AGE_MS
|
||||||
|
) {
|
||||||
|
resolve(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchAndCacheLucideTags(chromeLocal, resolve, reject);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object<string,string[]>} tagsMap
|
||||||
|
* @param {string} query
|
||||||
|
* @param {number} limit
|
||||||
|
* @returns {string[]} slugs
|
||||||
|
*/
|
||||||
|
function searchLucideSlugs(tagsMap, query, limit) {
|
||||||
|
var lim = limit != null ? limit : 60;
|
||||||
|
var q = String(query || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.trim();
|
||||||
|
if (!tagsMap || !q) return [];
|
||||||
|
var matches = [];
|
||||||
|
for (var slug in tagsMap) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(tagsMap, slug)) continue;
|
||||||
|
var hay =
|
||||||
|
slug +
|
||||||
|
" " +
|
||||||
|
(Array.isArray(tagsMap[slug]) ? tagsMap[slug].join(" ") : "");
|
||||||
|
if (hay.toLowerCase().indexOf(q) === -1) continue;
|
||||||
|
matches.push(slug);
|
||||||
|
}
|
||||||
|
matches.sort(function (a, b) {
|
||||||
|
var al = a.toLowerCase();
|
||||||
|
var bl = b.toLowerCase();
|
||||||
|
var ap = al.indexOf(q) === 0 ? 0 : 1;
|
||||||
|
var bp = bl.indexOf(q) === 0 ? 0 : 1;
|
||||||
|
if (ap !== bp) return ap - bp;
|
||||||
|
return al.localeCompare(bl);
|
||||||
|
});
|
||||||
|
return matches.slice(0, lim);
|
||||||
|
}
|
||||||
+31
-9
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Speeder",
|
"name": "Speeder",
|
||||||
"short_name": "Speeder",
|
"short_name": "Speeder",
|
||||||
"version": "5.0.2",
|
"version": "5.1.9.0",
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts (New and improved version of \"Video Speed Controller\")",
|
"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",
|
"homepage_url": "https://github.com/SoPat712/speeder",
|
||||||
@@ -9,7 +9,9 @@
|
|||||||
"gecko": {
|
"gecko": {
|
||||||
"id": "{ed860648-f54f-4dc9-9a0d-501aec4313f5}",
|
"id": "{ed860648-f54f-4dc9-9a0d-501aec4313f5}",
|
||||||
"data_collection_permissions": {
|
"data_collection_permissions": {
|
||||||
"required": ["none"]
|
"required": [
|
||||||
|
"none"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -19,12 +21,17 @@
|
|||||||
"128": "icons/icon128.png"
|
"128": "icons/icon128.png"
|
||||||
},
|
},
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": ["background.js"]
|
"scripts": [
|
||||||
|
"background.js"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"permissions": ["storage"],
|
"permissions": [
|
||||||
|
"storage",
|
||||||
|
"https://cdn.jsdelivr.net/*"
|
||||||
|
],
|
||||||
"options_ui": {
|
"options_ui": {
|
||||||
"page": "options.html",
|
"page": "options.html",
|
||||||
"open_in_tab": false
|
"open_in_tab": true
|
||||||
},
|
},
|
||||||
"browser_action": {
|
"browser_action": {
|
||||||
"default_icon": {
|
"default_icon": {
|
||||||
@@ -37,16 +44,31 @@
|
|||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"all_frames": true,
|
"all_frames": true,
|
||||||
"matches": ["http://*/*", "https://*/*", "file:///*"],
|
"matches": [
|
||||||
|
"http://*/*",
|
||||||
|
"https://*/*",
|
||||||
|
"file:///*"
|
||||||
|
],
|
||||||
"match_about_blank": true,
|
"match_about_blank": true,
|
||||||
"exclude_matches": [
|
"exclude_matches": [
|
||||||
"https://plus.google.com/hangouts/*",
|
"https://plus.google.com/hangouts/*",
|
||||||
"https://hangouts.google.com/*",
|
"https://hangouts.google.com/*",
|
||||||
"https://meet.google.com/*"
|
"https://meet.google.com/*"
|
||||||
],
|
],
|
||||||
"css": ["inject.css"],
|
"css": [
|
||||||
"js": ["inject.js"]
|
"inject.css"
|
||||||
|
],
|
||||||
|
"js": [
|
||||||
|
"shared/controller-utils.js",
|
||||||
|
"shared/key-bindings.js",
|
||||||
|
"shared/site-rules.js",
|
||||||
|
"ui-icons.js",
|
||||||
|
"inject.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"web_accessible_resources": ["inject.css", "shadow.css"]
|
"web_accessible_resources": [
|
||||||
|
"inject.css",
|
||||||
|
"shadow.css"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+482
-39
@@ -7,6 +7,17 @@
|
|||||||
--text: #17191c;
|
--text: #17191c;
|
||||||
--muted: #626b76;
|
--muted: #626b76;
|
||||||
--accent: #111827;
|
--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;
|
--danger: #b42318;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,12 +26,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
min-height: 100%;
|
/* Avoid coupling to the browser viewport: embedded options (e.g. Add-ons
|
||||||
|
* Manager iframe) must size to content, not 100vh, or a large empty band
|
||||||
|
* appears below the page. */
|
||||||
|
height: auto;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100vh;
|
min-height: 0;
|
||||||
padding: 24px 16px 40px;
|
padding: 24px 16px 40px;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
@@ -49,6 +64,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
|
h2,
|
||||||
h3,
|
h3,
|
||||||
h4 {
|
h4 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -104,6 +120,35 @@ h4 {
|
|||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.control-bars-group {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-bars-inner {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card-nested {
|
||||||
|
background: var(--panel-subtle);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading-major {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading-major h2 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 650;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading-major .section-intro {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.section-heading {
|
.section-heading {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
@@ -176,6 +221,7 @@ button:active {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button:focus-visible,
|
button:focus-visible,
|
||||||
|
input[type="checkbox"]:focus-visible,
|
||||||
input[type="text"]:focus,
|
input[type="text"]:focus,
|
||||||
select:focus,
|
select:focus,
|
||||||
textarea:focus {
|
textarea:focus {
|
||||||
@@ -213,10 +259,49 @@ textarea:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"] {
|
input[type="checkbox"] {
|
||||||
width: 16px;
|
appearance: none;
|
||||||
height: 16px;
|
-webkit-appearance: none;
|
||||||
margin: 2px 0 0;
|
position: relative;
|
||||||
accent-color: var(--accent);
|
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 {
|
label {
|
||||||
@@ -288,6 +373,39 @@ label em {
|
|||||||
|
|
||||||
.toggle-site-rule {
|
.toggle-site-rule {
|
||||||
font-weight: 400;
|
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 {
|
.row {
|
||||||
@@ -299,6 +417,19 @@ label em {
|
|||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row input[type="text"],
|
||||||
|
.row select {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-checkbox {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-checkbox input[type="checkbox"] {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-card .row:first-of-type {
|
.settings-card .row:first-of-type {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
@@ -310,16 +441,17 @@ label em {
|
|||||||
|
|
||||||
.controller-margin-inputs {
|
.controller-margin-inputs {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 116px));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: 100%;
|
width: max-content;
|
||||||
|
justify-self: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.margin-pad-cell {
|
.margin-pad-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
min-width: 0;
|
min-width: 116px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.margin-pad-mini {
|
.margin-pad-mini {
|
||||||
@@ -332,12 +464,13 @@ label em {
|
|||||||
|
|
||||||
.controller-margin-inputs input[type="text"] {
|
.controller-margin-inputs input[type="text"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 116px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-rule-option.site-rule-margin-option {
|
.site-rule-option.site-rule-margin-option {
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(0, 220px);
|
grid-template-columns: minmax(0, 1fr) minmax(0, 260px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-rule-override-section {
|
.site-rule-override-section {
|
||||||
@@ -353,17 +486,22 @@ label em {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.site-override-lead {
|
.site-override-lead {
|
||||||
display: flex;
|
display: grid;
|
||||||
align-items: flex-start;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
gap: 10px;
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: auto;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-override-lead input {
|
.site-override-lead input[type="checkbox"] {
|
||||||
margin-top: 3px;
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-override-lead span {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-rule-override-section .site-override-fields,
|
.site-rule-override-section .site-override-fields,
|
||||||
@@ -372,10 +510,19 @@ label em {
|
|||||||
.site-rule-override-section .site-autohide-container,
|
.site-rule-override-section .site-autohide-container,
|
||||||
.site-rule-override-section .site-playback-container,
|
.site-rule-override-section .site-playback-container,
|
||||||
.site-rule-override-section .site-opacity-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;
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.site-override-disabled {
|
||||||
|
opacity: 0.48;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
.cb-editor {
|
.cb-editor {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -481,6 +628,221 @@ label em {
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-icon svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-icon.cb-icon-nudge-pair {
|
||||||
|
width: auto;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0 4px;
|
||||||
|
gap: 4px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-nudge-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-nudge-chip[data-nudge-state="on"] {
|
||||||
|
background: #4b9135;
|
||||||
|
border: 1px solid #6ec754;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-nudge-chip[data-nudge-state="off"] {
|
||||||
|
background: #943e3e;
|
||||||
|
border: 1px solid #c06060;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-nudge-chip .vsc-btn-icon svg,
|
||||||
|
.cb-nudge-chip svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb-nudge-sep {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
opacity: 0.45;
|
||||||
|
color: var(--text);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-lucide-pair select {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-lucide-search-row {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-lucide-search-row .lucide-search-label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-search-field {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 14px 0 12px;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||||
|
transition: border-color 150ms ease, box-shadow 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-search-field:focus-within {
|
||||||
|
border-color: #9ca3af;
|
||||||
|
box-shadow: 0 0 0 3px rgba(17, 24, 39, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-search-icon {
|
||||||
|
display: flex;
|
||||||
|
color: var(--muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-search-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 8px 0;
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-search-input::placeholder {
|
||||||
|
color: var(--muted);
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-search-input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-results {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 220px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.lucide-result-tile {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--panel-subtle);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.lucide-result-tile:hover {
|
||||||
|
background: var(--panel);
|
||||||
|
border-color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.lucide-result-tile .lucide-result-thumb {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
object-fit: contain;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.lucide-result-tile.lucide-picked {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 2px rgba(17, 24, 39, 0.12);
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-status {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
min-height: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-preview-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 72px 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: start;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-preview {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--panel-subtle);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-preview svg {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-actions .lucide-apply {
|
||||||
|
background: #ffffff !important;
|
||||||
|
color: #111827 !important;
|
||||||
|
border: 1px solid var(--border-strong) !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-actions .lucide-apply:hover {
|
||||||
|
background: #f3f4f6 !important;
|
||||||
|
border-color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-actions .secondary {
|
||||||
|
background: var(--panel-subtle);
|
||||||
|
color: var(--text);
|
||||||
|
border-color: var(--border-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cb-label {
|
.cb-label {
|
||||||
@@ -525,24 +887,61 @@ label em {
|
|||||||
|
|
||||||
.site-rule-option {
|
.site-rule-option {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) 150px;
|
grid-template-columns: minmax(0, 1fr) 160px;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.site-rule-option-checkbox {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-option-checkbox > input[type="checkbox"] {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-option > input[type="text"],
|
||||||
|
.site-rule-option > select {
|
||||||
|
justify-self: end;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-option.site-rule-margin-option .controller-margin-inputs {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
.site-rule-body > .site-rule-option:first-child,
|
.site-rule-body > .site-rule-option:first-child,
|
||||||
.site-rule-content > .site-rule-option:first-child {
|
.site-rule-content > .site-rule-option:first-child {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-rule-option label {
|
.site-rule-option > label:not(.site-rule-split-label) {
|
||||||
display: flex;
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-split-label {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 16px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 10px;
|
width: 100%;
|
||||||
width: auto;
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-split-label input[type="checkbox"] {
|
||||||
|
justify-self: end;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-option-checkbox > .site-rule-split-label {
|
||||||
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-rule-controlbar,
|
.site-rule-controlbar,
|
||||||
@@ -552,12 +951,8 @@ label em {
|
|||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-rule-controlbar > label,
|
.site-rule-controlbar > label.site-override-lead,
|
||||||
.site-rule-shortcuts > label {
|
.site-rule-shortcuts > label.site-override-lead {
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
width: auto;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,8 +981,8 @@ label em {
|
|||||||
|
|
||||||
.force-label {
|
.force-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
width: auto;
|
width: auto;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@@ -595,7 +990,7 @@ label em {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.force-label input {
|
.force-label input {
|
||||||
margin-top: 2px;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-row {
|
.action-row {
|
||||||
@@ -615,13 +1010,6 @@ label em {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#faq hr {
|
|
||||||
height: 1px;
|
|
||||||
margin: 0 0 14px;
|
|
||||||
border: 0;
|
|
||||||
background: var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.support-footer {
|
.support-footer {
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@@ -636,14 +1024,33 @@ label em {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
|
.lucide-icon-preview-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.shortcut-row,
|
.shortcut-row,
|
||||||
.shortcut-row.customs,
|
.shortcut-row.customs,
|
||||||
.row,
|
.row,
|
||||||
|
.row.row-checkbox,
|
||||||
.site-rule-option,
|
.site-rule-option,
|
||||||
.site-shortcuts-container .shortcut-row {
|
.site-shortcuts-container .shortcut-row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row input[type="text"],
|
||||||
|
.row select {
|
||||||
|
justify-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-rule-option > input[type="text"],
|
||||||
|
.site-rule-option > select {
|
||||||
|
justify-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-override-lead {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
.action-row button,
|
.action-row button,
|
||||||
#addShortcutSelector {
|
#addShortcutSelector {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -671,6 +1078,10 @@ label em {
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.control-bars-group {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.site-rule-header {
|
.site-rule-header {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@@ -686,6 +1097,17 @@ label em {
|
|||||||
--text: #f2f4f6;
|
--text: #f2f4f6;
|
||||||
--muted: #a0a8b2;
|
--muted: #a0a8b2;
|
||||||
--accent: #f2f4f6;
|
--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;
|
--danger: #ff8a80;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -719,4 +1141,25 @@ label em {
|
|||||||
textarea:focus {
|
textarea:focus {
|
||||||
border-color: #6b7280;
|
border-color: #6b7280;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lucide-search-field:focus-within {
|
||||||
|
border-color: #6b7280;
|
||||||
|
box-shadow: 0 0 0 3px rgba(242, 244, 246, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-actions .lucide-apply {
|
||||||
|
background: #ffffff !important;
|
||||||
|
color: #111315 !important;
|
||||||
|
border-color: #e5e7eb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-icon-actions .lucide-apply:hover {
|
||||||
|
background: #f3f4f6 !important;
|
||||||
|
border-color: #d1d5db !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.lucide-result-tile .lucide-result-thumb {
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+204
-95
@@ -5,7 +5,13 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Speeder Settings</title>
|
<title>Speeder Settings</title>
|
||||||
<link rel="stylesheet" href="options.css" />
|
<link rel="stylesheet" href="options.css" />
|
||||||
|
<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="options.js"></script>
|
||||||
|
<script src="shared/import-export.js"></script>
|
||||||
<script src="importExport.js"></script>
|
<script src="importExport.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -180,11 +186,11 @@
|
|||||||
|
|
||||||
<h4 class="defaults-sub-heading">General</h4>
|
<h4 class="defaults-sub-heading">General</h4>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row row-checkbox">
|
||||||
<label for="enabled">Enable</label>
|
<label for="enabled">Enable</label>
|
||||||
<input id="enabled" type="checkbox" />
|
<input id="enabled" type="checkbox" />
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row row-checkbox">
|
||||||
<label for="audioBoolean">Work on audio</label>
|
<label for="audioBoolean">Work on audio</label>
|
||||||
<input id="audioBoolean" type="checkbox" />
|
<input id="audioBoolean" type="checkbox" />
|
||||||
</div>
|
</div>
|
||||||
@@ -192,11 +198,11 @@
|
|||||||
<div class="defaults-divider"></div>
|
<div class="defaults-divider"></div>
|
||||||
<h4 class="defaults-sub-heading">Playback</h4>
|
<h4 class="defaults-sub-heading">Playback</h4>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row row-checkbox">
|
||||||
<label for="rememberSpeed">Remember playback speed</label>
|
<label for="rememberSpeed">Remember playback speed</label>
|
||||||
<input id="rememberSpeed" type="checkbox" />
|
<input id="rememberSpeed" type="checkbox" />
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row row-checkbox">
|
||||||
<label for="forceLastSavedSpeed"
|
<label for="forceLastSavedSpeed"
|
||||||
>Force last saved speed<br />
|
>Force last saved speed<br />
|
||||||
<em
|
<em
|
||||||
@@ -210,7 +216,7 @@
|
|||||||
<div class="defaults-divider"></div>
|
<div class="defaults-divider"></div>
|
||||||
<h4 class="defaults-sub-heading">Controller</h4>
|
<h4 class="defaults-sub-heading">Controller</h4>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row row-checkbox">
|
||||||
<label for="startHidden">Hide controller by default</label>
|
<label for="startHidden">Hide controller by default</label>
|
||||||
<input id="startHidden" type="checkbox" />
|
<input id="startHidden" type="checkbox" />
|
||||||
</div>
|
</div>
|
||||||
@@ -250,7 +256,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row row-checkbox">
|
||||||
<label for="hideWithControls"
|
<label for="hideWithControls"
|
||||||
>Hide with controls<br />
|
>Hide with controls<br />
|
||||||
<em
|
<em
|
||||||
@@ -270,15 +276,10 @@
|
|||||||
</label>
|
</label>
|
||||||
<input id="hideWithControlsTimer" type="text" placeholder="2" />
|
<input id="hideWithControlsTimer" type="text" placeholder="2" />
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
|
||||||
<label for="showPopupControlBar">Show popup control bar</label>
|
|
||||||
<input id="showPopupControlBar" type="checkbox" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="defaults-divider"></div>
|
<div class="defaults-divider"></div>
|
||||||
<h4 class="defaults-sub-heading">Subtitle sync</h4>
|
<h4 class="defaults-sub-heading">Subtitle sync</h4>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row row-checkbox">
|
||||||
<label for="enableSubtitleNudge"
|
<label for="enableSubtitleNudge"
|
||||||
>Enable subtitle nudge<br /><em
|
>Enable subtitle nudge<br /><em
|
||||||
>Makes tiny playback changes to help keep subtitles aligned.</em
|
>Makes tiny playback changes to help keep subtitles aligned.</em
|
||||||
@@ -302,58 +303,160 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="controlBarSettings" class="settings-card">
|
<section
|
||||||
<div class="section-heading">
|
id="controlBarsGroup"
|
||||||
<h3>Control bar</h3>
|
class="settings-card control-bars-group"
|
||||||
|
aria-labelledby="controlBarsGroupHeading"
|
||||||
|
>
|
||||||
|
<div class="section-heading section-heading-major">
|
||||||
|
<h2 id="controlBarsGroupHeading">Control bars</h2>
|
||||||
<p class="section-intro">
|
<p class="section-intro">
|
||||||
Drag blocks to reorder. Move between Active and Available to show
|
In-page hover bar, extension popup bar, and Lucide icons for
|
||||||
or hide buttons.
|
buttons.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="cb-editor">
|
<div class="control-bars-inner">
|
||||||
<div class="cb-zone">
|
<section id="controlBarSettings" class="settings-card settings-card-nested">
|
||||||
<div class="cb-zone-label">Active</div>
|
<div class="section-heading">
|
||||||
<div
|
<h3>Hover control bar</h3>
|
||||||
id="controlBarActive"
|
<p class="section-intro">
|
||||||
class="cb-dropzone cb-active-zone"
|
Drag blocks to reorder. Move between Active and Available to
|
||||||
></div>
|
show or hide buttons.
|
||||||
</div>
|
</p>
|
||||||
<div class="cb-zone">
|
</div>
|
||||||
<div class="cb-zone-label">Available</div>
|
<div class="cb-editor">
|
||||||
<div
|
<div class="cb-zone">
|
||||||
id="controlBarAvailable"
|
<div class="cb-zone-label">Active</div>
|
||||||
class="cb-dropzone cb-available-zone"
|
<div
|
||||||
></div>
|
id="controlBarActive"
|
||||||
</div>
|
class="cb-dropzone cb-active-zone"
|
||||||
</div>
|
></div>
|
||||||
</section>
|
</div>
|
||||||
|
<div class="cb-zone">
|
||||||
|
<div class="cb-zone-label">Available</div>
|
||||||
|
<div
|
||||||
|
id="controlBarAvailable"
|
||||||
|
class="cb-dropzone cb-available-zone"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="popupControlBarSettings" class="settings-card">
|
<section id="popupControlBarSettings" class="settings-card settings-card-nested">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<h3>Popup control bar</h3>
|
<h3>Popup control bar</h3>
|
||||||
<p class="section-intro">
|
<p class="section-intro">
|
||||||
Configure which buttons appear in the browser popup control bar.
|
Configure which buttons appear in the browser popup control bar.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row row-checkbox">
|
||||||
<label for="popupMatchHoverControls">Match hover controls</label>
|
<label for="showPopupControlBar">Show popup control bar</label>
|
||||||
<input id="popupMatchHoverControls" type="checkbox" />
|
<input id="showPopupControlBar" type="checkbox" />
|
||||||
</div>
|
</div>
|
||||||
<div id="popupCbEditorWrap" class="cb-editor cb-editor-disabled">
|
<div class="row row-checkbox">
|
||||||
<div class="cb-zone">
|
<label for="popupMatchHoverControls">Match hover controls</label>
|
||||||
<div class="cb-zone-label">Active</div>
|
<input id="popupMatchHoverControls" type="checkbox" />
|
||||||
|
</div>
|
||||||
|
<div id="popupCbEditorWrap" class="cb-editor cb-editor-disabled">
|
||||||
|
<div class="cb-zone">
|
||||||
|
<div class="cb-zone-label">Active</div>
|
||||||
|
<div
|
||||||
|
id="popupControlBarActive"
|
||||||
|
class="cb-dropzone cb-active-zone"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="cb-zone">
|
||||||
|
<div class="cb-zone-label">Available</div>
|
||||||
|
<div
|
||||||
|
id="popupControlBarAvailable"
|
||||||
|
class="cb-dropzone cb-available-zone"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="lucideIconSettings" class="settings-card settings-card-nested">
|
||||||
|
<div class="section-heading">
|
||||||
|
<h3>Button icons (Lucide)</h3>
|
||||||
|
<p class="section-intro">
|
||||||
|
Search icons from the
|
||||||
|
<a
|
||||||
|
href="https://lucide.dev"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>Lucide</a
|
||||||
|
>
|
||||||
|
set (fetched from jsDelivr). Custom icons are cached in local
|
||||||
|
storage and included when you export settings. Subtitle nudge
|
||||||
|
icons use two menu entries (enabled and disabled), not the bar
|
||||||
|
block id
|
||||||
|
<code>nudge</code>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="row row-lucide-pair">
|
||||||
|
<label for="lucideIconActionSelect">Controller action</label>
|
||||||
|
<select id="lucideIconActionSelect"></select>
|
||||||
|
</div>
|
||||||
|
<div class="row row-lucide-search-row">
|
||||||
|
<label for="lucideIconSearch" class="lucide-search-label"
|
||||||
|
>Search icons</label
|
||||||
|
>
|
||||||
|
<div class="lucide-search-field">
|
||||||
|
<span class="lucide-search-icon" aria-hidden="true">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="11" cy="11" r="8" />
|
||||||
|
<path d="m21 21-4.3-4.3" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
id="lucideIconSearch"
|
||||||
|
class="lucide-search-input"
|
||||||
|
placeholder="Search by name or tag (e.g. star, arrow, media)…"
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
id="popupControlBarActive"
|
id="lucideIconResults"
|
||||||
class="cb-dropzone cb-active-zone"
|
class="lucide-icon-results"
|
||||||
|
role="listbox"
|
||||||
|
aria-label="Matching Lucide icons"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
<p id="lucideIconStatus" class="lucide-icon-status" aria-live="polite"></p>
|
||||||
<div class="cb-zone">
|
<div class="lucide-icon-preview-row">
|
||||||
<div class="cb-zone-label">Available</div>
|
<div
|
||||||
<div
|
id="lucideIconPreview"
|
||||||
id="popupControlBarAvailable"
|
class="lucide-icon-preview"
|
||||||
class="cb-dropzone cb-available-zone"
|
aria-live="polite"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
<div class="lucide-icon-actions">
|
||||||
|
<button type="button" id="lucideIconApply" class="lucide-apply">
|
||||||
|
Apply to action
|
||||||
|
</button>
|
||||||
|
<button type="button" id="lucideIconClearAction" class="secondary">
|
||||||
|
Clear this action
|
||||||
|
</button>
|
||||||
|
<button type="button" id="lucideIconClearAll" class="secondary">
|
||||||
|
Clear all custom icons
|
||||||
|
</button>
|
||||||
|
<button type="button" id="lucideIconReloadTags" class="secondary">
|
||||||
|
Refresh icon list from network
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -380,7 +483,15 @@
|
|||||||
<template id="siteRuleTemplate">
|
<template id="siteRuleTemplate">
|
||||||
<div class="site-rule">
|
<div class="site-rule">
|
||||||
<div class="site-rule-header">
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="site-pattern"
|
class="site-pattern"
|
||||||
@@ -389,20 +500,20 @@
|
|||||||
<button type="button" class="remove-site-rule">Remove</button>
|
<button type="button" class="remove-site-rule">Remove</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="site-rule-body">
|
<div class="site-rule-body">
|
||||||
<div class="site-rule-option">
|
<div class="site-rule-option site-rule-option-checkbox">
|
||||||
<label>
|
<label class="site-rule-split-label">
|
||||||
|
<span>Enable Speeder on this site</span>
|
||||||
<input type="checkbox" class="site-enabled" />
|
<input type="checkbox" class="site-enabled" />
|
||||||
Enable Speeder on this site
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="site-rule-content">
|
<div class="site-rule-content">
|
||||||
<div class="site-rule-override-section">
|
<div class="site-rule-override-section">
|
||||||
<label class="site-override-lead">
|
<label class="site-override-lead">
|
||||||
|
<span>Override placement for this site</span>
|
||||||
<input type="checkbox" class="override-placement" />
|
<input type="checkbox" class="override-placement" />
|
||||||
Override placement for this site
|
|
||||||
</label>
|
</label>
|
||||||
<div class="site-placement-container" style="display: none">
|
<div class="site-placement-container">
|
||||||
<div class="site-rule-option">
|
<div class="site-rule-option site-rule-option-field">
|
||||||
<label>Default controller location:</label>
|
<label>Default controller location:</label>
|
||||||
<select class="site-controllerLocation">
|
<select class="site-controllerLocation">
|
||||||
<option value="top-left">Top left</option>
|
<option value="top-left">Top left</option>
|
||||||
@@ -436,11 +547,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="site-rule-override-section">
|
<div class="site-rule-override-section">
|
||||||
<label class="site-override-lead">
|
<label class="site-override-lead">
|
||||||
|
<span>Override hide-by-default for this site</span>
|
||||||
<input type="checkbox" class="override-visibility" />
|
<input type="checkbox" class="override-visibility" />
|
||||||
Override hide-by-default for this site
|
|
||||||
</label>
|
</label>
|
||||||
<div class="site-visibility-container" style="display: none">
|
<div class="site-visibility-container">
|
||||||
<div class="site-rule-option">
|
<div class="site-rule-option site-rule-option-checkbox">
|
||||||
<label>Hide controller by default:</label>
|
<label>Hide controller by default:</label>
|
||||||
<input type="checkbox" class="site-startHidden" />
|
<input type="checkbox" class="site-startHidden" />
|
||||||
</div>
|
</div>
|
||||||
@@ -448,17 +559,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="site-rule-override-section">
|
<div class="site-rule-override-section">
|
||||||
<label class="site-override-lead">
|
<label class="site-override-lead">
|
||||||
|
<span>Override auto-hide for this site</span>
|
||||||
<input type="checkbox" class="override-autohide" />
|
<input type="checkbox" class="override-autohide" />
|
||||||
Override auto-hide for this site
|
|
||||||
</label>
|
</label>
|
||||||
<div class="site-autohide-container" style="display: none">
|
<div class="site-autohide-container">
|
||||||
<div class="site-rule-option">
|
<div class="site-rule-option site-rule-option-checkbox">
|
||||||
<label>
|
<label class="site-rule-split-label">
|
||||||
|
<span>Hide with controls (idle-based)</span>
|
||||||
<input type="checkbox" class="site-hideWithControls" />
|
<input type="checkbox" class="site-hideWithControls" />
|
||||||
Hide with controls (idle-based)
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="site-rule-option">
|
<div class="site-rule-option site-rule-option-field">
|
||||||
<label>Auto-hide timer (0.1–15s):</label>
|
<label>Auto-hide timer (0.1–15s):</label>
|
||||||
<input type="text" class="site-hideWithControlsTimer" />
|
<input type="text" class="site-hideWithControlsTimer" />
|
||||||
</div>
|
</div>
|
||||||
@@ -466,19 +577,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="site-rule-override-section">
|
<div class="site-rule-override-section">
|
||||||
<label class="site-override-lead">
|
<label class="site-override-lead">
|
||||||
|
<span>Override playback for this site</span>
|
||||||
<input type="checkbox" class="override-playback" />
|
<input type="checkbox" class="override-playback" />
|
||||||
Override playback for this site
|
|
||||||
</label>
|
</label>
|
||||||
<div class="site-playback-container" style="display: none">
|
<div class="site-playback-container">
|
||||||
<div class="site-rule-option">
|
<div class="site-rule-option site-rule-option-checkbox">
|
||||||
<label>Remember playback speed:</label>
|
<label>Remember playback speed:</label>
|
||||||
<input type="checkbox" class="site-rememberSpeed" />
|
<input type="checkbox" class="site-rememberSpeed" />
|
||||||
</div>
|
</div>
|
||||||
<div class="site-rule-option">
|
<div class="site-rule-option site-rule-option-checkbox">
|
||||||
<label>Force last saved speed:</label>
|
<label>Force last saved speed:</label>
|
||||||
<input type="checkbox" class="site-forceLastSavedSpeed" />
|
<input type="checkbox" class="site-forceLastSavedSpeed" />
|
||||||
</div>
|
</div>
|
||||||
<div class="site-rule-option">
|
<div class="site-rule-option site-rule-option-checkbox">
|
||||||
<label>Work on audio:</label>
|
<label>Work on audio:</label>
|
||||||
<input type="checkbox" class="site-audioBoolean" />
|
<input type="checkbox" class="site-audioBoolean" />
|
||||||
</div>
|
</div>
|
||||||
@@ -486,11 +597,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="site-rule-override-section">
|
<div class="site-rule-override-section">
|
||||||
<label class="site-override-lead">
|
<label class="site-override-lead">
|
||||||
|
<span>Override opacity for this site</span>
|
||||||
<input type="checkbox" class="override-opacity" />
|
<input type="checkbox" class="override-opacity" />
|
||||||
Override opacity for this site
|
|
||||||
</label>
|
</label>
|
||||||
<div class="site-opacity-container" style="display: none">
|
<div class="site-opacity-container">
|
||||||
<div class="site-rule-option">
|
<div class="site-rule-option site-rule-option-field">
|
||||||
<label>Controller opacity:</label>
|
<label>Controller opacity:</label>
|
||||||
<input type="text" class="site-controllerOpacity" />
|
<input type="text" class="site-controllerOpacity" />
|
||||||
</div>
|
</div>
|
||||||
@@ -498,15 +609,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="site-rule-override-section">
|
<div class="site-rule-override-section">
|
||||||
<label class="site-override-lead">
|
<label class="site-override-lead">
|
||||||
|
<span>Override subtitle nudge for this site</span>
|
||||||
<input type="checkbox" class="override-subtitleNudge" />
|
<input type="checkbox" class="override-subtitleNudge" />
|
||||||
Override subtitle nudge for this site
|
|
||||||
</label>
|
</label>
|
||||||
<div class="site-subtitleNudge-container" style="display: none">
|
<div class="site-subtitleNudge-container">
|
||||||
<div class="site-rule-option">
|
<div class="site-rule-option site-rule-option-checkbox">
|
||||||
<label>Enable subtitle nudge:</label>
|
<label>Enable subtitle nudge:</label>
|
||||||
<input type="checkbox" class="site-enableSubtitleNudge" />
|
<input type="checkbox" class="site-enableSubtitleNudge" />
|
||||||
</div>
|
</div>
|
||||||
<div class="site-rule-option">
|
<div class="site-rule-option site-rule-option-field">
|
||||||
<label>Nudge interval (10–1000ms):</label>
|
<label>Nudge interval (10–1000ms):</label>
|
||||||
<input type="text" class="site-subtitleNudgeInterval" placeholder="50" />
|
<input type="text" class="site-subtitleNudgeInterval" placeholder="50" />
|
||||||
</div>
|
</div>
|
||||||
@@ -514,10 +625,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="site-rule-controlbar">
|
<div class="site-rule-controlbar">
|
||||||
<label class="site-override-lead">
|
<label class="site-override-lead">
|
||||||
|
<span>Override in-player control bar for this site</span>
|
||||||
<input type="checkbox" class="override-controlbar" />
|
<input type="checkbox" class="override-controlbar" />
|
||||||
Override in-player control bar for this site
|
|
||||||
</label>
|
</label>
|
||||||
<div class="site-controlbar-container" style="display: none">
|
<div class="site-controlbar-container">
|
||||||
<div class="cb-editor">
|
<div class="cb-editor">
|
||||||
<div class="cb-zone">
|
<div class="cb-zone">
|
||||||
<div class="cb-zone-label">Active</div>
|
<div class="cb-zone-label">Active</div>
|
||||||
@@ -532,11 +643,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="site-rule-controlbar">
|
<div class="site-rule-controlbar">
|
||||||
<label class="site-override-lead">
|
<label class="site-override-lead">
|
||||||
|
<span>Override extension popup for this site</span>
|
||||||
<input type="checkbox" class="override-popup-controlbar" />
|
<input type="checkbox" class="override-popup-controlbar" />
|
||||||
Override extension popup for this site
|
|
||||||
</label>
|
</label>
|
||||||
<div class="site-popup-controlbar-container" style="display: none">
|
<div class="site-popup-controlbar-container">
|
||||||
<div class="site-rule-option">
|
<div class="site-rule-option site-rule-option-checkbox">
|
||||||
<label>Show popup control bar</label>
|
<label>Show popup control bar</label>
|
||||||
<input type="checkbox" class="site-showPopupControlBar" />
|
<input type="checkbox" class="site-showPopupControlBar" />
|
||||||
</div>
|
</div>
|
||||||
@@ -554,10 +665,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="site-rule-shortcuts">
|
<div class="site-rule-shortcuts">
|
||||||
<label class="site-override-lead">
|
<label class="site-override-lead">
|
||||||
|
<span>Override shortcuts for this site</span>
|
||||||
<input type="checkbox" class="override-shortcuts" />
|
<input type="checkbox" class="override-shortcuts" />
|
||||||
Override shortcuts for this site
|
|
||||||
</label>
|
</label>
|
||||||
<div class="site-shortcuts-container" style="display: none"></div>
|
<div class="site-shortcuts-container"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -580,8 +691,6 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="faq" class="settings-card info-card">
|
<section id="faq" class="settings-card info-card">
|
||||||
<hr />
|
|
||||||
|
|
||||||
<h4>Extension controls not appearing?</h4>
|
<h4>Extension controls not appearing?</h4>
|
||||||
<p>
|
<p>
|
||||||
This extension only works with HTML5 audio and video. If the
|
This extension only works with HTML5 audio and video. If the
|
||||||
|
|||||||
+690
-454
@@ -1,106 +1,44 @@
|
|||||||
var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
|
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 keyBindings = [];
|
||||||
|
|
||||||
var keyCodeAliases = {
|
var bindingCodeAliases = {
|
||||||
0: "null",
|
Space: "Space",
|
||||||
null: "null",
|
ArrowLeft: "Left",
|
||||||
undefined: "null",
|
ArrowUp: "Up",
|
||||||
32: "Space",
|
ArrowRight: "Right",
|
||||||
37: "Left",
|
ArrowDown: "Down",
|
||||||
38: "Up",
|
Numpad0: "Num 0",
|
||||||
39: "Right",
|
Numpad1: "Num 1",
|
||||||
40: "Down",
|
Numpad2: "Num 2",
|
||||||
96: "Num 0",
|
Numpad3: "Num 3",
|
||||||
97: "Num 1",
|
Numpad4: "Num 4",
|
||||||
98: "Num 2",
|
Numpad5: "Num 5",
|
||||||
99: "Num 3",
|
Numpad6: "Num 6",
|
||||||
100: "Num 4",
|
Numpad7: "Num 7",
|
||||||
101: "Num 5",
|
Numpad8: "Num 8",
|
||||||
102: "Num 6",
|
Numpad9: "Num 9",
|
||||||
103: "Num 7",
|
NumpadMultiply: "Num *",
|
||||||
104: "Num 8",
|
NumpadAdd: "Num +",
|
||||||
105: "Num 9",
|
NumpadSubtract: "Num -",
|
||||||
106: "Num *",
|
NumpadDecimal: "Num .",
|
||||||
107: "Num +",
|
NumpadDivide: "Num /",
|
||||||
109: "Num -",
|
Backquote: "`",
|
||||||
110: "Num .",
|
Minus: "-",
|
||||||
111: "Num /",
|
Equal: "=",
|
||||||
112: "F1",
|
BracketLeft: "[",
|
||||||
113: "F2",
|
BracketRight: "]",
|
||||||
114: "F3",
|
Backslash: "\\",
|
||||||
115: "F4",
|
Semicolon: ";",
|
||||||
116: "F5",
|
Quote: "'",
|
||||||
117: "F6",
|
Comma: ",",
|
||||||
118: "F7",
|
Period: ".",
|
||||||
119: "F8",
|
Slash: "/"
|
||||||
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 modifierKeys = new Set([
|
var modifierKeys = new Set([
|
||||||
@@ -114,23 +52,18 @@ var modifierKeys = new Set([
|
|||||||
"Shift"
|
"Shift"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
var displayKeyAliases = {
|
var controllerLocations = Array.isArray(controllerUtils.controllerLocations)
|
||||||
" ": "Space",
|
? controllerUtils.controllerLocations.slice()
|
||||||
ArrowLeft: "Left",
|
: [
|
||||||
ArrowUp: "Up",
|
"top-left",
|
||||||
ArrowRight: "Right",
|
"top-center",
|
||||||
ArrowDown: "Down"
|
"top-right",
|
||||||
};
|
"middle-right",
|
||||||
var controllerLocations = [
|
"bottom-right",
|
||||||
"top-left",
|
"bottom-center",
|
||||||
"top-center",
|
"bottom-left",
|
||||||
"top-right",
|
"middle-left"
|
||||||
"middle-right",
|
];
|
||||||
"bottom-right",
|
|
||||||
"bottom-center",
|
|
||||||
"bottom-left",
|
|
||||||
"middle-left"
|
|
||||||
];
|
|
||||||
|
|
||||||
var controllerButtonDefs = {
|
var controllerButtonDefs = {
|
||||||
rewind: { icon: "\u00AB", name: "Rewind" },
|
rewind: { icon: "\u00AB", name: "Rewind" },
|
||||||
@@ -138,7 +71,7 @@ var controllerButtonDefs = {
|
|||||||
faster: { icon: "+", name: "Increase speed" },
|
faster: { icon: "+", name: "Increase speed" },
|
||||||
advance: { icon: "\u00BB", name: "Advance" },
|
advance: { icon: "\u00BB", name: "Advance" },
|
||||||
display: { icon: "\u00D7", name: "Close controller" },
|
display: { icon: "\u00D7", name: "Close controller" },
|
||||||
reset: { icon: "\u21BA", name: "Reset speed" },
|
reset: { icon: "\u21BB", name: "Reset speed" },
|
||||||
fast: { icon: "\u2605", name: "Preferred speed" },
|
fast: { icon: "\u2605", name: "Preferred speed" },
|
||||||
nudge: { icon: "\u2713", name: "Subtitle nudge" },
|
nudge: { icon: "\u2713", name: "Subtitle nudge" },
|
||||||
settings: { icon: "\u2699", name: "Settings" },
|
settings: { icon: "\u2699", name: "Settings" },
|
||||||
@@ -147,12 +80,80 @@ var controllerButtonDefs = {
|
|||||||
mark: { icon: "\u2691", name: "Set marker" },
|
mark: { icon: "\u2691", name: "Set marker" },
|
||||||
jump: { icon: "\u21E5", name: "Jump to marker" }
|
jump: { icon: "\u21E5", name: "Jump to marker" }
|
||||||
};
|
};
|
||||||
|
var popupExcludedButtonIds = new Set(["settings"]);
|
||||||
|
|
||||||
function createDefaultBinding(action, key, keyCode, value) {
|
/** Lucide picker only — not control-bar blocks (chip uses subtitleNudgeOn/Off). */
|
||||||
|
var lucideSubtitleNudgeActionLabels = {
|
||||||
|
subtitleNudgeOn: "Subtitle nudge — enabled",
|
||||||
|
subtitleNudgeOff: "Subtitle nudge — disabled"
|
||||||
|
};
|
||||||
|
|
||||||
|
function sanitizePopupButtonOrder(buttonIds) {
|
||||||
|
return popupControlUtils.sanitizeButtonOrder(
|
||||||
|
buttonIds,
|
||||||
|
controllerButtonDefs,
|
||||||
|
popupExcludedButtonIds
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cached custom Lucide SVGs (mirrors chrome.storage.local customButtonIcons). */
|
||||||
|
var customButtonIconsLive = {};
|
||||||
|
|
||||||
|
function fillControlBarIconElement(icon, buttonId) {
|
||||||
|
if (!icon || !buttonId) return;
|
||||||
|
var doc = icon.ownerDocument || document;
|
||||||
|
if (buttonId === "nudge") {
|
||||||
|
vscClearElement(icon);
|
||||||
|
icon.className = "cb-icon cb-icon-nudge-pair";
|
||||||
|
function nudgeChipMarkup(action) {
|
||||||
|
var c = customButtonIconsLive[action];
|
||||||
|
if (c && c.svg) return c.svg;
|
||||||
|
if (typeof vscIconSvgString === "function") {
|
||||||
|
return vscIconSvgString(action, 14) || "";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
function appendChip(action, stateKey) {
|
||||||
|
var sp = document.createElement("span");
|
||||||
|
sp.className = "cb-nudge-chip";
|
||||||
|
sp.setAttribute("data-nudge-state", stateKey);
|
||||||
|
var inner = nudgeChipMarkup(action);
|
||||||
|
if (inner) {
|
||||||
|
var wrap = vscCreateSvgWrap(doc, inner, "vsc-btn-icon");
|
||||||
|
if (wrap) {
|
||||||
|
sp.appendChild(wrap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
icon.appendChild(sp);
|
||||||
|
}
|
||||||
|
appendChip("subtitleNudgeOn", "on");
|
||||||
|
var sep = document.createElement("span");
|
||||||
|
sep.className = "cb-nudge-sep";
|
||||||
|
sep.textContent = "/";
|
||||||
|
icon.appendChild(sep);
|
||||||
|
appendChip("subtitleNudgeOff", "off");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
icon.className = "cb-icon";
|
||||||
|
var custom = customButtonIconsLive[buttonId];
|
||||||
|
if (custom && custom.svg) {
|
||||||
|
if (vscSetSvgContent(icon, custom.svg)) return;
|
||||||
|
}
|
||||||
|
if (typeof vscIconSvgString === "function") {
|
||||||
|
var svgHtml = vscIconSvgString(buttonId, 16);
|
||||||
|
if (svgHtml) {
|
||||||
|
if (vscSetSvgContent(icon, svgHtml)) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vscClearElement(icon);
|
||||||
|
var def = controllerButtonDefs[buttonId];
|
||||||
|
icon.textContent = (def && def.icon) || "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDefaultBinding(action, code, value) {
|
||||||
return {
|
return {
|
||||||
action: action,
|
action: action,
|
||||||
key: key,
|
code: code,
|
||||||
keyCode: keyCode,
|
|
||||||
value: value,
|
value: value,
|
||||||
force: false,
|
force: false,
|
||||||
predefined: true
|
predefined: true
|
||||||
@@ -162,7 +163,6 @@ function createDefaultBinding(action, key, keyCode, value) {
|
|||||||
var tcDefaults = {
|
var tcDefaults = {
|
||||||
speed: 1.0,
|
speed: 1.0,
|
||||||
lastSpeed: 1.0,
|
lastSpeed: 1.0,
|
||||||
displayKeyCode: 86,
|
|
||||||
rememberSpeed: false,
|
rememberSpeed: false,
|
||||||
audioBoolean: false,
|
audioBoolean: false,
|
||||||
startHidden: false,
|
startHidden: false,
|
||||||
@@ -178,15 +178,15 @@ var tcDefaults = {
|
|||||||
controllerMarginBottom: 65,
|
controllerMarginBottom: 65,
|
||||||
controllerMarginLeft: 0,
|
controllerMarginLeft: 0,
|
||||||
keyBindings: [
|
keyBindings: [
|
||||||
createDefaultBinding("display", "V", 86, 0),
|
createDefaultBinding("display", "KeyV", 0),
|
||||||
createDefaultBinding("move", "P", 80, 0),
|
createDefaultBinding("move", "KeyP", 0),
|
||||||
createDefaultBinding("slower", "S", 83, 0.1),
|
createDefaultBinding("slower", "KeyS", 0.1),
|
||||||
createDefaultBinding("faster", "D", 68, 0.1),
|
createDefaultBinding("faster", "KeyD", 0.1),
|
||||||
createDefaultBinding("rewind", "Z", 90, 10),
|
createDefaultBinding("rewind", "KeyZ", 10),
|
||||||
createDefaultBinding("advance", "X", 88, 10),
|
createDefaultBinding("advance", "KeyX", 10),
|
||||||
createDefaultBinding("reset", "R", 82, 1),
|
createDefaultBinding("reset", "KeyR", 1),
|
||||||
createDefaultBinding("fast", "G", 71, 1.8),
|
createDefaultBinding("fast", "KeyG", 1.8),
|
||||||
createDefaultBinding("toggleSubtitleNudge", "N", 78, 0)
|
createDefaultBinding("toggleSubtitleNudge", "KeyN", 0)
|
||||||
],
|
],
|
||||||
siteRules: [
|
siteRules: [
|
||||||
{
|
{
|
||||||
@@ -198,6 +198,7 @@ var tcDefaults = {
|
|||||||
{
|
{
|
||||||
pattern: "/^https:\\/\\/(www\\.)?youtube\\.com\\/shorts\\/.*/",
|
pattern: "/^https:\\/\\/(www\\.)?youtube\\.com\\/shorts\\/.*/",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
rememberSpeed: true,
|
||||||
controllerMarginTop: 60,
|
controllerMarginTop: 60,
|
||||||
controllerMarginBottom: 85
|
controllerMarginBottom: 85
|
||||||
}
|
}
|
||||||
@@ -227,6 +228,20 @@ const actionLabels = {
|
|||||||
toggleSubtitleNudge: "Toggle subtitle nudge"
|
toggleSubtitleNudge: "Toggle subtitle nudge"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const speedBindingActions = ["slower", "faster", "fast"];
|
||||||
|
const requiredShortcutActions = new Set(["display", "slower", "faster"]);
|
||||||
|
|
||||||
|
function formatSpeedBindingDisplay(action, value) {
|
||||||
|
if (!speedBindingActions.includes(action)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
var n = Number(value);
|
||||||
|
if (!isFinite(n)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return n.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
const customActionsNoValues = [
|
const customActionsNoValues = [
|
||||||
"reset",
|
"reset",
|
||||||
"display",
|
"display",
|
||||||
@@ -276,21 +291,91 @@ function refreshAddShortcutSelector() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureDefaultBinding(storage, action, key, keyCode, value) {
|
function ensureDefaultBinding(storage, action, code, value) {
|
||||||
if (storage.keyBindings.some((item) => item.action === action)) return;
|
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) {
|
function normalizeControllerLocation(location) {
|
||||||
if (controllerLocations.includes(location)) return location;
|
return controllerUtils.normalizeControllerLocation(
|
||||||
return tcDefaults.controllerLocation;
|
location,
|
||||||
|
tcDefaults.controllerLocation
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampMarginPxInput(el, fallback) {
|
function clampMarginPxInput(el, fallback) {
|
||||||
var n = parseInt(el && el.value, 10);
|
return controllerUtils.clampControllerMarginPx(el && el.value, fallback);
|
||||||
if (!Number.isFinite(n)) return fallback;
|
}
|
||||||
return Math.min(200, Math.max(0, n));
|
|
||||||
|
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) {
|
function syncSiteRuleField(ruleEl, rule, key, isCheckbox) {
|
||||||
@@ -310,128 +395,88 @@ function syncSiteRuleField(ruleEl, rule, key, isCheckbox) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeBindingKey(key) {
|
function normalizeBindingKey(key) {
|
||||||
if (typeof key !== "string" || key.length === 0) return null;
|
return keyBindingUtils.normalizeBindingKey(key);
|
||||||
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) {
|
function getLegacyKeyCode(binding) {
|
||||||
if (!binding) return null;
|
return keyBindingUtils.getLegacyKeyCode(binding);
|
||||||
if (Number.isInteger(binding.keyCode)) return binding.keyCode;
|
|
||||||
if (typeof binding.key === "number" && Number.isInteger(binding.key)) {
|
|
||||||
return binding.key;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function legacyKeyCodeToBinding(keyCode) {
|
function legacyBindingKeyToCode(key) {
|
||||||
if (!Number.isInteger(keyCode)) return null;
|
return keyBindingUtils.legacyBindingKeyToCode(key);
|
||||||
var normalizedKey = keyCodeToKey[keyCode];
|
}
|
||||||
if (!normalizedKey && keyCode >= 48 && keyCode <= 57) {
|
|
||||||
normalizedKey = String.fromCharCode(keyCode);
|
function legacyKeyCodeToCode(keyCode) {
|
||||||
}
|
return keyBindingUtils.legacyKeyCodeToCode(keyCode);
|
||||||
if (!normalizedKey && keyCode >= 65 && keyCode <= 90) {
|
}
|
||||||
normalizedKey = String.fromCharCode(keyCode);
|
|
||||||
}
|
function inferBindingCode(binding, fallbackCode) {
|
||||||
return {
|
return keyBindingUtils.inferBindingCode(binding, fallbackCode);
|
||||||
key: normalizeBindingKey(normalizedKey),
|
|
||||||
keyCode: keyCode,
|
|
||||||
code: null,
|
|
||||||
disabled: false
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDisabledBinding() {
|
function createDisabledBinding() {
|
||||||
return {
|
return {
|
||||||
key: null,
|
|
||||||
keyCode: null,
|
|
||||||
code: null,
|
code: null,
|
||||||
disabled: true
|
disabled: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeStoredBinding(binding, fallbackKeyCode) {
|
function normalizeStoredBinding(binding, fallbackCode) {
|
||||||
var fallbackBinding = legacyKeyCodeToBinding(fallbackKeyCode);
|
|
||||||
if (!binding) {
|
if (!binding) {
|
||||||
return fallbackBinding;
|
if (!fallbackCode) return null;
|
||||||
|
return {
|
||||||
|
code: fallbackCode,
|
||||||
|
disabled: false
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
binding.disabled === true ||
|
binding.disabled === true ||
|
||||||
(binding.key === null &&
|
(binding.code === null &&
|
||||||
binding.keyCode === null &&
|
binding.key === null &&
|
||||||
binding.code === null)
|
binding.keyCode === null)
|
||||||
) {
|
) {
|
||||||
return createDisabledBinding();
|
return createDisabledBinding();
|
||||||
}
|
}
|
||||||
|
|
||||||
var normalized = {
|
var normalizedCode = inferBindingCode(binding, fallbackCode);
|
||||||
key: null,
|
if (!normalizedCode) {
|
||||||
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) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var normalized = {
|
||||||
|
code: normalizedCode,
|
||||||
|
disabled: false
|
||||||
|
};
|
||||||
|
|
||||||
return normalized;
|
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) {
|
function getBindingLabel(binding) {
|
||||||
if (!binding) return "";
|
if (!binding) return "";
|
||||||
if (binding.disabled) return "";
|
if (binding.disabled) return "";
|
||||||
if (binding.key) {
|
return formatBindingCode(binding.code);
|
||||||
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 "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setShortcutInputBinding(input, binding) {
|
function setShortcutInputBinding(input, binding) {
|
||||||
input.vscBinding = binding ? Object.assign({}, binding) : null;
|
input.vscBinding = binding ? Object.assign({}, binding) : null;
|
||||||
input.keyCode =
|
|
||||||
binding && Number.isInteger(binding.keyCode) ? binding.keyCode : null;
|
|
||||||
input.value = getBindingLabel(binding);
|
input.value = getBindingLabel(binding);
|
||||||
}
|
}
|
||||||
|
|
||||||
function captureBindingFromEvent(event) {
|
function captureBindingFromEvent(event) {
|
||||||
var normalizedKey = normalizeBindingKey(event.key);
|
if (modifierKeys.has(event.key)) return null;
|
||||||
if (!normalizedKey || modifierKeys.has(normalizedKey)) return null;
|
if (typeof event.code !== "string" || event.code.length === 0) return null;
|
||||||
return {
|
return {
|
||||||
key: normalizedKey,
|
code: event.code,
|
||||||
keyCode: Number.isInteger(event.keyCode) ? event.keyCode : null,
|
|
||||||
code: event.code || null,
|
|
||||||
disabled: false
|
disabled: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -462,8 +507,10 @@ function recordKeyPress(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function inputFilterNumbersOnly(event) {
|
function inputFilterNumbersOnly(event) {
|
||||||
var char = String.fromCharCode(event.keyCode);
|
var char = event.key;
|
||||||
if (
|
if (
|
||||||
|
typeof char !== "string" ||
|
||||||
|
char.length !== 1 ||
|
||||||
!/[\d\.]$/.test(char) ||
|
!/[\d\.]$/.test(char) ||
|
||||||
!/^\d+(\.\d*)?$/.test(event.target.value + char)
|
!/^\d+(\.\d*)?$/.test(event.target.value + char)
|
||||||
) {
|
) {
|
||||||
@@ -490,7 +537,15 @@ function updateCustomShortcutInputText(inputItem, bindingOrKeyCode) {
|
|||||||
return;
|
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) {
|
function appendSelectOptions(select, options) {
|
||||||
@@ -526,7 +581,7 @@ function add_shortcut(action, value) {
|
|||||||
valueInput.value = "N/A";
|
valueInput.value = "N/A";
|
||||||
valueInput.disabled = true;
|
valueInput.disabled = true;
|
||||||
} else {
|
} else {
|
||||||
valueInput.value = value || 0;
|
valueInput.value = formatSpeedBindingDisplay(action, value || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
var removeButton = document.createElement("button");
|
var removeButton = document.createElement("button");
|
||||||
@@ -550,23 +605,33 @@ function createKeyBindings(item) {
|
|||||||
var input = item.querySelector(".customKey");
|
var input = item.querySelector(".customKey");
|
||||||
var valueInput = item.querySelector(".customValue");
|
var valueInput = item.querySelector(".customValue");
|
||||||
var predefined = !!item.id;
|
var predefined = !!item.id;
|
||||||
var fallbackKeyCode =
|
var binding = normalizeStoredBinding(input.vscBinding);
|
||||||
predefined && action === "display"
|
|
||||||
? tcDefaults.displayKeyCode
|
|
||||||
: undefined;
|
|
||||||
var binding = normalizeStoredBinding(input.vscBinding, fallbackKeyCode);
|
|
||||||
|
|
||||||
if (!binding) {
|
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 {
|
return {
|
||||||
valid: false,
|
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({
|
keyBindings.push({
|
||||||
action: action,
|
action: action,
|
||||||
key: binding.key,
|
|
||||||
keyCode: binding.keyCode,
|
|
||||||
code: binding.code,
|
code: binding.code,
|
||||||
disabled: binding.disabled === true,
|
disabled: binding.disabled === true,
|
||||||
value: customActionsNoValues.includes(action)
|
value: customActionsNoValues.includes(action)
|
||||||
@@ -646,8 +711,10 @@ function save_options() {
|
|||||||
document.getElementById("controllerLocation").value
|
document.getElementById("controllerLocation").value
|
||||||
);
|
);
|
||||||
settings.controllerOpacity =
|
settings.controllerOpacity =
|
||||||
parseFloat(document.getElementById("controllerOpacity").value) ||
|
parseFiniteNumberOrFallback(
|
||||||
tcDefaults.controllerOpacity;
|
document.getElementById("controllerOpacity").value,
|
||||||
|
tcDefaults.controllerOpacity
|
||||||
|
);
|
||||||
|
|
||||||
settings.controllerMarginTop = clampMarginPxInput(
|
settings.controllerMarginTop = clampMarginPxInput(
|
||||||
document.getElementById("controllerMarginTop"),
|
document.getElementById("controllerMarginTop"),
|
||||||
@@ -678,7 +745,7 @@ function save_options() {
|
|||||||
document.getElementById("showPopupControlBar").checked;
|
document.getElementById("showPopupControlBar").checked;
|
||||||
settings.popupMatchHoverControls =
|
settings.popupMatchHoverControls =
|
||||||
document.getElementById("popupMatchHoverControls").checked;
|
document.getElementById("popupMatchHoverControls").checked;
|
||||||
settings.popupControllerButtons = getPopupControlBarOrder();
|
settings.popupControllerButtons = sanitizePopupButtonOrder(getPopupControlBarOrder());
|
||||||
|
|
||||||
// Collect site rules
|
// Collect site rules
|
||||||
settings.siteRules = [];
|
settings.siteRules = [];
|
||||||
@@ -735,8 +802,10 @@ function save_options() {
|
|||||||
|
|
||||||
if (ruleEl.querySelector(".override-opacity").checked) {
|
if (ruleEl.querySelector(".override-opacity").checked) {
|
||||||
rule.controllerOpacity =
|
rule.controllerOpacity =
|
||||||
parseFloat(ruleEl.querySelector(".site-controllerOpacity").value) ||
|
parseFiniteNumberOrFallback(
|
||||||
settings.controllerOpacity;
|
ruleEl.querySelector(".site-controllerOpacity").value,
|
||||||
|
settings.controllerOpacity
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ruleEl.querySelector(".override-subtitleNudge").checked) {
|
if (ruleEl.querySelector(".override-subtitleNudge").checked) {
|
||||||
@@ -767,33 +836,52 @@ function save_options() {
|
|||||||
ruleEl.querySelector(".site-showPopupControlBar").checked;
|
ruleEl.querySelector(".site-showPopupControlBar").checked;
|
||||||
var popupActiveZone = ruleEl.querySelector(".site-popup-cb-active");
|
var popupActiveZone = ruleEl.querySelector(".site-popup-cb-active");
|
||||||
if (popupActiveZone) {
|
if (popupActiveZone) {
|
||||||
rule.popupControllerButtons = readControlBarOrder(popupActiveZone);
|
rule.popupControllerButtons = sanitizePopupButtonOrder(
|
||||||
|
readControlBarOrder(popupActiveZone)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ruleEl.querySelector(".override-shortcuts").checked) {
|
if (ruleEl.querySelector(".override-shortcuts").checked) {
|
||||||
var shortcuts = [];
|
var shortcuts = [];
|
||||||
ruleEl.querySelectorAll(".site-shortcuts-container .customs").forEach((shortcutRow) => {
|
ruleEl.querySelectorAll(".site-shortcuts-container .customs").forEach((shortcutRow) => {
|
||||||
|
if (saveError) return;
|
||||||
var action = shortcutRow.dataset.action;
|
var action = shortcutRow.dataset.action;
|
||||||
var keyInput = shortcutRow.querySelector(".customKey");
|
var keyInput = shortcutRow.querySelector(".customKey");
|
||||||
var valueInput = shortcutRow.querySelector(".customValue");
|
var valueInput = shortcutRow.querySelector(".customValue");
|
||||||
var forceCheckbox = shortcutRow.querySelector(".customForce");
|
var forceCheckbox = shortcutRow.querySelector(".customForce");
|
||||||
var binding = normalizeStoredBinding(keyInput.vscBinding);
|
var binding = normalizeStoredBinding(keyInput.vscBinding);
|
||||||
|
|
||||||
if (binding) {
|
if (!binding) {
|
||||||
shortcuts.push({
|
if (requiredShortcutActions.has(action)) {
|
||||||
action: action,
|
saveError =
|
||||||
key: binding.key,
|
"Error: Site rule shortcut for " +
|
||||||
keyCode: binding.keyCode,
|
(actionLabels[action] || action) +
|
||||||
code: binding.code,
|
" cannot be empty. Unable to save";
|
||||||
disabled: binding.disabled === true,
|
return;
|
||||||
value: customActionsNoValues.includes(action)
|
}
|
||||||
? 0
|
binding = createDisabledBinding();
|
||||||
: Number(valueInput.value),
|
|
||||||
force: forceCheckbox ? forceCheckbox.checked : false
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
if (shortcuts.length > 0) rule.shortcuts = shortcuts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -819,42 +907,10 @@ function save_options() {
|
|||||||
|
|
||||||
function ensureAllDefaultBindings(storage) {
|
function ensureAllDefaultBindings(storage) {
|
||||||
tcDefaults.keyBindings.forEach((binding) => {
|
tcDefaults.keyBindings.forEach((binding) => {
|
||||||
// Special case for "display" to support legacy displayKeyCode
|
ensureDefaultBinding(storage, binding.action, binding.code, binding.value);
|
||||||
if (binding.action === "display" && storage.displayKeyCode) {
|
|
||||||
ensureDefaultBinding(storage, "display", "V", storage.displayKeyCode, 0);
|
|
||||||
} else {
|
|
||||||
ensureDefaultBinding(
|
|
||||||
storage,
|
|
||||||
binding.action,
|
|
||||||
binding.key,
|
|
||||||
binding.keyCode,
|
|
||||||
binding.value
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateLegacyBlacklist(storage) {
|
|
||||||
if (!storage.blacklist || typeof storage.blacklist !== "string") {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
var siteRules = [];
|
|
||||||
var lines = storage.blacklist.split("\n");
|
|
||||||
|
|
||||||
lines.forEach((line) => {
|
|
||||||
var pattern = line.replace(regStrip, "");
|
|
||||||
if (pattern.length === 0) return;
|
|
||||||
|
|
||||||
siteRules.push({
|
|
||||||
pattern: pattern,
|
|
||||||
disableExtension: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return siteRules;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSiteRuleShortcut(container, action, binding, value, force) {
|
function addSiteRuleShortcut(container, action, binding, value, force) {
|
||||||
var div = document.createElement("div");
|
var div = document.createElement("div");
|
||||||
div.setAttribute("class", "shortcut-row customs");
|
div.setAttribute("class", "shortcut-row customs");
|
||||||
@@ -899,9 +955,11 @@ function addSiteRuleShortcut(container, action, binding, value, force) {
|
|||||||
valueInput.className = "customValue";
|
valueInput.className = "customValue";
|
||||||
valueInput.type = "text";
|
valueInput.type = "text";
|
||||||
valueInput.placeholder = "value (0.10)";
|
valueInput.placeholder = "value (0.10)";
|
||||||
valueInput.value = value || 0;
|
|
||||||
if (customActionsNoValues.includes(action)) {
|
if (customActionsNoValues.includes(action)) {
|
||||||
|
valueInput.value = "N/A";
|
||||||
valueInput.disabled = true;
|
valueInput.disabled = true;
|
||||||
|
} else {
|
||||||
|
valueInput.value = formatSpeedBindingDisplay(action, value || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
var forceLabel = document.createElement("label");
|
var forceLabel = document.createElement("label");
|
||||||
@@ -937,9 +995,7 @@ function createSiteRule(rule) {
|
|||||||
ruleEl.querySelector(".site-pattern").value = pattern;
|
ruleEl.querySelector(".site-pattern").value = pattern;
|
||||||
|
|
||||||
// Make the rule body collapsed by default
|
// Make the rule body collapsed by default
|
||||||
var ruleBody = ruleEl.querySelector(".site-rule-body");
|
setSiteRuleExpandedState(ruleEl, false);
|
||||||
ruleBody.style.display = "none";
|
|
||||||
ruleEl.classList.add("collapsed");
|
|
||||||
|
|
||||||
var enabledCheckbox = ruleEl.querySelector(".site-enabled");
|
var enabledCheckbox = ruleEl.querySelector(".site-enabled");
|
||||||
var contentEl = ruleEl.querySelector(".site-rule-content");
|
var contentEl = ruleEl.querySelector(".site-rule-content");
|
||||||
@@ -974,108 +1030,99 @@ function createSiteRule(rule) {
|
|||||||
];
|
];
|
||||||
var hasPlacementOverride =
|
var hasPlacementOverride =
|
||||||
rule && placementKeys.some(function (k) { return rule[k] !== undefined; });
|
rule && placementKeys.some(function (k) { return rule[k] !== undefined; });
|
||||||
if (hasPlacementOverride) {
|
ruleEl.querySelector(".override-placement").checked = Boolean(hasPlacementOverride);
|
||||||
ruleEl.querySelector(".override-placement").checked = true;
|
|
||||||
ruleEl.querySelector(".site-placement-container").style.display = "block";
|
|
||||||
}
|
|
||||||
syncSiteRuleField(ruleEl, rule, "controllerLocation", false);
|
syncSiteRuleField(ruleEl, rule, "controllerLocation", false);
|
||||||
syncSiteRuleField(ruleEl, rule, "controllerMarginTop", false);
|
syncSiteRuleField(ruleEl, rule, "controllerMarginTop", false);
|
||||||
syncSiteRuleField(ruleEl, rule, "controllerMarginBottom", false);
|
syncSiteRuleField(ruleEl, rule, "controllerMarginBottom", false);
|
||||||
|
applySiteRuleOverrideState(ruleEl, "override-placement", "site-placement-container");
|
||||||
|
|
||||||
if (rule && rule.startHidden !== undefined) {
|
ruleEl.querySelector(".override-visibility").checked = Boolean(
|
||||||
ruleEl.querySelector(".override-visibility").checked = true;
|
rule && rule.startHidden !== undefined
|
||||||
ruleEl.querySelector(".site-visibility-container").style.display = "block";
|
);
|
||||||
}
|
|
||||||
syncSiteRuleField(ruleEl, rule, "startHidden", true);
|
syncSiteRuleField(ruleEl, rule, "startHidden", true);
|
||||||
|
applySiteRuleOverrideState(ruleEl, "override-visibility", "site-visibility-container");
|
||||||
|
|
||||||
if (
|
var hasAutohideOverride = Boolean(
|
||||||
rule &&
|
rule &&
|
||||||
(rule.hideWithControls !== undefined ||
|
(rule.hideWithControls !== undefined ||
|
||||||
rule.hideWithControlsTimer !== undefined)
|
rule.hideWithControlsTimer !== undefined)
|
||||||
) {
|
);
|
||||||
ruleEl.querySelector(".override-autohide").checked = true;
|
ruleEl.querySelector(".override-autohide").checked = hasAutohideOverride;
|
||||||
ruleEl.querySelector(".site-autohide-container").style.display = "block";
|
|
||||||
}
|
|
||||||
syncSiteRuleField(ruleEl, rule, "hideWithControls", true);
|
syncSiteRuleField(ruleEl, rule, "hideWithControls", true);
|
||||||
syncSiteRuleField(ruleEl, rule, "hideWithControlsTimer", false);
|
syncSiteRuleField(ruleEl, rule, "hideWithControlsTimer", false);
|
||||||
|
applySiteRuleOverrideState(ruleEl, "override-autohide", "site-autohide-container");
|
||||||
|
|
||||||
if (
|
var hasPlaybackOverride = Boolean(
|
||||||
rule &&
|
rule &&
|
||||||
(rule.rememberSpeed !== undefined ||
|
(rule.rememberSpeed !== undefined ||
|
||||||
rule.forceLastSavedSpeed !== undefined ||
|
rule.forceLastSavedSpeed !== undefined ||
|
||||||
rule.audioBoolean !== undefined)
|
rule.audioBoolean !== undefined)
|
||||||
) {
|
);
|
||||||
ruleEl.querySelector(".override-playback").checked = true;
|
ruleEl.querySelector(".override-playback").checked = hasPlaybackOverride;
|
||||||
ruleEl.querySelector(".site-playback-container").style.display = "block";
|
|
||||||
}
|
|
||||||
syncSiteRuleField(ruleEl, rule, "rememberSpeed", true);
|
syncSiteRuleField(ruleEl, rule, "rememberSpeed", true);
|
||||||
syncSiteRuleField(ruleEl, rule, "forceLastSavedSpeed", true);
|
syncSiteRuleField(ruleEl, rule, "forceLastSavedSpeed", true);
|
||||||
syncSiteRuleField(ruleEl, rule, "audioBoolean", true);
|
syncSiteRuleField(ruleEl, rule, "audioBoolean", true);
|
||||||
|
applySiteRuleOverrideState(ruleEl, "override-playback", "site-playback-container");
|
||||||
|
|
||||||
if (rule && rule.controllerOpacity !== undefined) {
|
ruleEl.querySelector(".override-opacity").checked = Boolean(
|
||||||
ruleEl.querySelector(".override-opacity").checked = true;
|
rule && rule.controllerOpacity !== undefined
|
||||||
ruleEl.querySelector(".site-opacity-container").style.display = "block";
|
);
|
||||||
}
|
|
||||||
syncSiteRuleField(ruleEl, rule, "controllerOpacity", false);
|
syncSiteRuleField(ruleEl, rule, "controllerOpacity", false);
|
||||||
|
applySiteRuleOverrideState(ruleEl, "override-opacity", "site-opacity-container");
|
||||||
|
|
||||||
if (
|
var hasSubtitleNudgeOverride = Boolean(
|
||||||
rule &&
|
rule &&
|
||||||
(rule.enableSubtitleNudge !== undefined ||
|
(rule.enableSubtitleNudge !== undefined ||
|
||||||
rule.subtitleNudgeInterval !== undefined)
|
rule.subtitleNudgeInterval !== undefined)
|
||||||
) {
|
);
|
||||||
ruleEl.querySelector(".override-subtitleNudge").checked = true;
|
ruleEl.querySelector(".override-subtitleNudge").checked = hasSubtitleNudgeOverride;
|
||||||
ruleEl.querySelector(".site-subtitleNudge-container").style.display =
|
|
||||||
"block";
|
|
||||||
}
|
|
||||||
syncSiteRuleField(ruleEl, rule, "enableSubtitleNudge", true);
|
syncSiteRuleField(ruleEl, rule, "enableSubtitleNudge", true);
|
||||||
syncSiteRuleField(ruleEl, rule, "subtitleNudgeInterval", false);
|
syncSiteRuleField(ruleEl, rule, "subtitleNudgeInterval", false);
|
||||||
|
applySiteRuleOverrideState(
|
||||||
|
ruleEl,
|
||||||
|
"override-subtitleNudge",
|
||||||
|
"site-subtitleNudge-container"
|
||||||
|
);
|
||||||
|
|
||||||
if (rule && Array.isArray(rule.controllerButtons)) {
|
var hasControlbarOverride = Boolean(rule && Array.isArray(rule.controllerButtons));
|
||||||
ruleEl.querySelector(".override-controlbar").checked = true;
|
ruleEl.querySelector(".override-controlbar").checked = hasControlbarOverride;
|
||||||
var cbContainer = ruleEl.querySelector(".site-controlbar-container");
|
populateControlBarZones(
|
||||||
cbContainer.style.display = "block";
|
ruleEl.querySelector(".site-cb-active"),
|
||||||
populateControlBarZones(
|
ruleEl.querySelector(".site-cb-available"),
|
||||||
ruleEl.querySelector(".site-cb-active"),
|
hasControlbarOverride ? rule.controllerButtons : getControlBarOrder()
|
||||||
ruleEl.querySelector(".site-cb-available"),
|
);
|
||||||
rule.controllerButtons
|
applySiteRuleOverrideState(ruleEl, "override-controlbar", "site-controlbar-container");
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
var hasPopupControlbarOverride = Boolean(
|
||||||
rule &&
|
rule &&
|
||||||
(rule.showPopupControlBar !== undefined ||
|
(rule.showPopupControlBar !== undefined ||
|
||||||
Array.isArray(rule.popupControllerButtons))
|
Array.isArray(rule.popupControllerButtons))
|
||||||
) {
|
);
|
||||||
ruleEl.querySelector(".override-popup-controlbar").checked = true;
|
ruleEl.querySelector(".override-popup-controlbar").checked =
|
||||||
var popupCbContainer = ruleEl.querySelector(".site-popup-controlbar-container");
|
hasPopupControlbarOverride;
|
||||||
popupCbContainer.style.display = "block";
|
populateControlBarZones(
|
||||||
var sitePopupActive = ruleEl.querySelector(".site-popup-cb-active");
|
ruleEl.querySelector(".site-popup-cb-active"),
|
||||||
var sitePopupAvailable = ruleEl.querySelector(".site-popup-cb-available");
|
ruleEl.querySelector(".site-popup-cb-available"),
|
||||||
if (Array.isArray(rule.popupControllerButtons)) {
|
hasPopupControlbarOverride && Array.isArray(rule.popupControllerButtons)
|
||||||
populateControlBarZones(
|
? sanitizePopupButtonOrder(rule.popupControllerButtons)
|
||||||
sitePopupActive,
|
: getPopupControlBarOrder(),
|
||||||
sitePopupAvailable,
|
function (id) {
|
||||||
rule.popupControllerButtons
|
return !popupExcludedButtonIds.has(id);
|
||||||
);
|
|
||||||
} else if (
|
|
||||||
sitePopupActive &&
|
|
||||||
sitePopupAvailable &&
|
|
||||||
sitePopupActive.children.length === 0
|
|
||||||
) {
|
|
||||||
populateControlBarZones(
|
|
||||||
sitePopupActive,
|
|
||||||
sitePopupAvailable,
|
|
||||||
getPopupControlBarOrder()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
syncSiteRuleField(ruleEl, rule, "showPopupControlBar", true);
|
syncSiteRuleField(ruleEl, rule, "showPopupControlBar", true);
|
||||||
|
applySiteRuleOverrideState(
|
||||||
|
ruleEl,
|
||||||
|
"override-popup-controlbar",
|
||||||
|
"site-popup-controlbar-container"
|
||||||
|
);
|
||||||
|
|
||||||
if (rule && Array.isArray(rule.shortcuts) && rule.shortcuts.length > 0) {
|
var hasShortcutOverride = Boolean(
|
||||||
ruleEl.querySelector(".override-shortcuts").checked = true;
|
rule && Array.isArray(rule.shortcuts) && rule.shortcuts.length > 0
|
||||||
var container = ruleEl.querySelector(".site-shortcuts-container");
|
);
|
||||||
container.style.display = "block";
|
ruleEl.querySelector(".override-shortcuts").checked = hasShortcutOverride;
|
||||||
|
var container = ruleEl.querySelector(".site-shortcuts-container");
|
||||||
|
if (hasShortcutOverride) {
|
||||||
rule.shortcuts.forEach((shortcut) => {
|
rule.shortcuts.forEach((shortcut) => {
|
||||||
addSiteRuleShortcut(
|
addSiteRuleShortcut(
|
||||||
container,
|
container,
|
||||||
@@ -1085,13 +1132,41 @@ function createSiteRule(rule) {
|
|||||||
shortcut.force
|
shortcut.force
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
populateDefaultSiteShortcuts(container);
|
||||||
}
|
}
|
||||||
|
applySiteRuleOverrideState(ruleEl, "override-shortcuts", "site-shortcuts-container");
|
||||||
|
|
||||||
document.getElementById("siteRulesContainer").appendChild(ruleEl);
|
document.getElementById("siteRulesContainer").appendChild(ruleEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateDefaultSiteShortcuts(container) {
|
function populateDefaultSiteShortcuts(container) {
|
||||||
tcDefaults.keyBindings.forEach((binding) => {
|
var bindings = [];
|
||||||
|
document.querySelectorAll("#customs .shortcut-row").forEach((row) => {
|
||||||
|
var action = row.dataset.action;
|
||||||
|
if (!action) return;
|
||||||
|
|
||||||
|
var keyInput = row.querySelector(".customKey");
|
||||||
|
var binding = normalizeStoredBinding(keyInput && keyInput.vscBinding);
|
||||||
|
if (!binding) return;
|
||||||
|
|
||||||
|
var valueInput = row.querySelector(".customValue");
|
||||||
|
bindings.push({
|
||||||
|
action: action,
|
||||||
|
code: binding.code,
|
||||||
|
disabled: binding.disabled === true,
|
||||||
|
value: customActionsNoValues.includes(action)
|
||||||
|
? 0
|
||||||
|
: Number(valueInput && valueInput.value),
|
||||||
|
force: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bindings.length === 0) {
|
||||||
|
bindings = tcDefaults.keyBindings.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings.forEach((binding) => {
|
||||||
addSiteRuleShortcut(container, binding.action, binding, binding.value, false);
|
addSiteRuleShortcut(container, binding.action, binding, binding.value, false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1110,7 +1185,7 @@ function createControlBarBlock(buttonId) {
|
|||||||
|
|
||||||
var icon = document.createElement("span");
|
var icon = document.createElement("span");
|
||||||
icon.className = "cb-icon";
|
icon.className = "cb-icon";
|
||||||
icon.textContent = def.icon;
|
fillControlBarIconElement(icon, buttonId);
|
||||||
|
|
||||||
var label = document.createElement("span");
|
var label = document.createElement("span");
|
||||||
label.className = "cb-label";
|
label.className = "cb-label";
|
||||||
@@ -1123,16 +1198,23 @@ function createControlBarBlock(buttonId) {
|
|||||||
return block;
|
return block;
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateControlBarZones(activeZone, availableZone, activeIds) {
|
function populateControlBarZones(activeZone, availableZone, activeIds, allowButtonId) {
|
||||||
activeZone.innerHTML = "";
|
vscClearElement(activeZone);
|
||||||
availableZone.innerHTML = "";
|
vscClearElement(availableZone);
|
||||||
|
|
||||||
|
var allowed = function (id) {
|
||||||
|
if (!controllerButtonDefs[id]) return false;
|
||||||
|
return typeof allowButtonId === "function" ? Boolean(allowButtonId(id)) : true;
|
||||||
|
};
|
||||||
|
|
||||||
activeIds.forEach(function (id) {
|
activeIds.forEach(function (id) {
|
||||||
|
if (!allowed(id)) return;
|
||||||
var block = createControlBarBlock(id);
|
var block = createControlBarBlock(id);
|
||||||
if (block) activeZone.appendChild(block);
|
if (block) activeZone.appendChild(block);
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(controllerButtonDefs).forEach(function (id) {
|
Object.keys(controllerButtonDefs).forEach(function (id) {
|
||||||
|
if (!allowed(id)) return;
|
||||||
if (!activeIds.includes(id)) {
|
if (!activeIds.includes(id)) {
|
||||||
var block = createControlBarBlock(id);
|
var block = createControlBarBlock(id);
|
||||||
if (block) availableZone.appendChild(block);
|
if (block) availableZone.appendChild(block);
|
||||||
@@ -1160,15 +1242,21 @@ function getControlBarOrder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function populatePopupControlBarEditor(activeIds) {
|
function populatePopupControlBarEditor(activeIds) {
|
||||||
|
var popupActiveIds = sanitizePopupButtonOrder(activeIds);
|
||||||
populateControlBarZones(
|
populateControlBarZones(
|
||||||
document.getElementById("popupControlBarActive"),
|
document.getElementById("popupControlBarActive"),
|
||||||
document.getElementById("popupControlBarAvailable"),
|
document.getElementById("popupControlBarAvailable"),
|
||||||
activeIds
|
popupActiveIds,
|
||||||
|
function (id) {
|
||||||
|
return !popupExcludedButtonIds.has(id);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPopupControlBarOrder() {
|
function getPopupControlBarOrder() {
|
||||||
return readControlBarOrder(document.getElementById("popupControlBarActive"));
|
return sanitizePopupButtonOrder(
|
||||||
|
readControlBarOrder(document.getElementById("popupControlBarActive"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePopupEditorDisabledState() {
|
function updatePopupEditorDisabledState() {
|
||||||
@@ -1265,8 +1353,218 @@ function initControlBarEditor() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lucidePickerSelectedSlug = null;
|
||||||
|
var lucideSearchTimer = null;
|
||||||
|
|
||||||
|
function setLucideStatus(msg) {
|
||||||
|
var el = document.getElementById("lucideIconStatus");
|
||||||
|
if (el) el.textContent = msg || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function repaintAllCbIconsFromCustomMap() {
|
||||||
|
document.querySelectorAll(".cb-block .cb-icon").forEach(function (icon) {
|
||||||
|
var block = icon.closest(".cb-block");
|
||||||
|
if (!block) return;
|
||||||
|
fillControlBarIconElement(icon, block.dataset.buttonId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistCustomButtonIcons(map, callback) {
|
||||||
|
chrome.storage.local.set({ customButtonIcons: map }, function () {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
setLucideStatus(
|
||||||
|
"Could not save icons: " + chrome.runtime.lastError.message
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
customButtonIconsLive = map;
|
||||||
|
if (callback) callback();
|
||||||
|
repaintAllCbIconsFromCustomMap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initLucideButtonIconsUI() {
|
||||||
|
var actionSel = document.getElementById("lucideIconActionSelect");
|
||||||
|
var searchInput = document.getElementById("lucideIconSearch");
|
||||||
|
var resultsEl = document.getElementById("lucideIconResults");
|
||||||
|
var previewEl = document.getElementById("lucideIconPreview");
|
||||||
|
if (!actionSel || !searchInput || !resultsEl || !previewEl) return;
|
||||||
|
if (typeof getLucideTagsMap !== "function") return;
|
||||||
|
|
||||||
|
if (!actionSel.dataset.lucideInit) {
|
||||||
|
actionSel.dataset.lucideInit = "1";
|
||||||
|
vscClearElement(actionSel);
|
||||||
|
Object.keys(controllerButtonDefs).forEach(function (aid) {
|
||||||
|
if (aid === "nudge") {
|
||||||
|
Object.keys(lucideSubtitleNudgeActionLabels).forEach(function (subId) {
|
||||||
|
var o2 = document.createElement("option");
|
||||||
|
o2.value = subId;
|
||||||
|
o2.textContent =
|
||||||
|
lucideSubtitleNudgeActionLabels[subId] + " (" + subId + ")";
|
||||||
|
actionSel.appendChild(o2);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var o = document.createElement("option");
|
||||||
|
o.value = aid;
|
||||||
|
o.textContent =
|
||||||
|
controllerButtonDefs[aid].name + " (" + aid + ")";
|
||||||
|
actionSel.appendChild(o);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResults(slugs) {
|
||||||
|
vscClearElement(resultsEl);
|
||||||
|
slugs.forEach(function (slug) {
|
||||||
|
var b = document.createElement("button");
|
||||||
|
b.type = "button";
|
||||||
|
b.className = "lucide-result-tile";
|
||||||
|
b.dataset.slug = slug;
|
||||||
|
b.title = slug;
|
||||||
|
b.setAttribute("aria-label", slug);
|
||||||
|
if (slug === lucidePickerSelectedSlug) {
|
||||||
|
b.classList.add("lucide-picked");
|
||||||
|
}
|
||||||
|
var url =
|
||||||
|
typeof lucideIconSvgUrl === "function" ? lucideIconSvgUrl(slug) : "";
|
||||||
|
if (url) {
|
||||||
|
var img = document.createElement("img");
|
||||||
|
img.className = "lucide-result-thumb";
|
||||||
|
img.src = url;
|
||||||
|
img.alt = "";
|
||||||
|
img.loading = "lazy";
|
||||||
|
b.appendChild(img);
|
||||||
|
} else {
|
||||||
|
b.textContent = slug.slice(0, 3);
|
||||||
|
}
|
||||||
|
b.addEventListener("click", function () {
|
||||||
|
lucidePickerSelectedSlug = slug;
|
||||||
|
Array.prototype.forEach.call(
|
||||||
|
resultsEl.querySelectorAll("button"),
|
||||||
|
function (x) {
|
||||||
|
x.classList.toggle("lucide-picked", x.dataset.slug === slug);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
fetchLucideSvg(slug)
|
||||||
|
.then(function (txt) {
|
||||||
|
var safe = sanitizeLucideSvg(txt);
|
||||||
|
if (!safe) throw new Error("Bad SVG");
|
||||||
|
if (!vscSetSvgContent(previewEl, safe)) {
|
||||||
|
throw new Error("Preview render failed");
|
||||||
|
}
|
||||||
|
setLucideStatus("Preview: " + slug);
|
||||||
|
})
|
||||||
|
.catch(function (e) {
|
||||||
|
vscClearElement(previewEl);
|
||||||
|
setLucideStatus(
|
||||||
|
"Could not load: " + slug + " — " + e.message
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
resultsEl.appendChild(b);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!searchInput.dataset.lucideBound) {
|
||||||
|
searchInput.dataset.lucideBound = "1";
|
||||||
|
searchInput.addEventListener("input", function () {
|
||||||
|
clearTimeout(lucideSearchTimer);
|
||||||
|
lucideSearchTimer = setTimeout(function () {
|
||||||
|
getLucideTagsMap(chrome.storage.local, false)
|
||||||
|
.then(function (map) {
|
||||||
|
var q = searchInput.value;
|
||||||
|
if (!q.trim()) {
|
||||||
|
vscClearElement(resultsEl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderResults(searchLucideSlugs(map, q, 48));
|
||||||
|
})
|
||||||
|
.catch(function (e) {
|
||||||
|
setLucideStatus("Icon list error: " + e.message);
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var applyBtn = document.getElementById("lucideIconApply");
|
||||||
|
if (applyBtn && !applyBtn.dataset.lucideBound) {
|
||||||
|
applyBtn.dataset.lucideBound = "1";
|
||||||
|
applyBtn.addEventListener("click", function () {
|
||||||
|
var action = actionSel.value;
|
||||||
|
var slug = lucidePickerSelectedSlug;
|
||||||
|
if (!action || !slug) {
|
||||||
|
setLucideStatus("Pick an action and click an icon first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchLucideSvg(slug)
|
||||||
|
.then(function (txt) {
|
||||||
|
var safe = sanitizeLucideSvg(txt);
|
||||||
|
if (!safe) throw new Error("Sanitize failed");
|
||||||
|
var next = Object.assign({}, customButtonIconsLive);
|
||||||
|
next[action] = { slug: slug, svg: safe };
|
||||||
|
persistCustomButtonIcons(next, function () {
|
||||||
|
setLucideStatus(
|
||||||
|
"Saved " +
|
||||||
|
slug +
|
||||||
|
" for " +
|
||||||
|
action +
|
||||||
|
". Reload pages for the hover bar."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(function (e) {
|
||||||
|
setLucideStatus("Apply failed: " + e.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var clrOne = document.getElementById("lucideIconClearAction");
|
||||||
|
if (clrOne && !clrOne.dataset.lucideBound) {
|
||||||
|
clrOne.dataset.lucideBound = "1";
|
||||||
|
clrOne.addEventListener("click", function () {
|
||||||
|
var action = actionSel.value;
|
||||||
|
if (!action) return;
|
||||||
|
var next = Object.assign({}, customButtonIconsLive);
|
||||||
|
delete next[action];
|
||||||
|
persistCustomButtonIcons(next, function () {
|
||||||
|
setLucideStatus("Cleared custom icon for " + action + ".");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var clrAll = document.getElementById("lucideIconClearAll");
|
||||||
|
if (clrAll && !clrAll.dataset.lucideBound) {
|
||||||
|
clrAll.dataset.lucideBound = "1";
|
||||||
|
clrAll.addEventListener("click", function () {
|
||||||
|
persistCustomButtonIcons({}, function () {
|
||||||
|
setLucideStatus("All custom icons cleared.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var reloadTags = document.getElementById("lucideIconReloadTags");
|
||||||
|
if (reloadTags && !reloadTags.dataset.lucideBound) {
|
||||||
|
reloadTags.dataset.lucideBound = "1";
|
||||||
|
reloadTags.addEventListener("click", function () {
|
||||||
|
getLucideTagsMap(chrome.storage.local, true)
|
||||||
|
.then(function () {
|
||||||
|
setLucideStatus("Icon name list refreshed.");
|
||||||
|
})
|
||||||
|
.catch(function (e) {
|
||||||
|
setLucideStatus("Refresh failed: " + e.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function restore_options() {
|
function restore_options() {
|
||||||
chrome.storage.sync.get(tcDefaults, function (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 = storage.rememberSpeed;
|
document.getElementById("rememberSpeed").checked = storage.rememberSpeed;
|
||||||
document.getElementById("forceLastSavedSpeed").checked =
|
document.getElementById("forceLastSavedSpeed").checked =
|
||||||
storage.forceLastSavedSpeed;
|
storage.forceLastSavedSpeed;
|
||||||
@@ -1329,24 +1627,19 @@ function restore_options() {
|
|||||||
valueInput.disabled = true;
|
valueInput.disabled = true;
|
||||||
}
|
}
|
||||||
} else if (valueInput) {
|
} else if (valueInput) {
|
||||||
valueInput.value = item.value;
|
valueInput.value = formatSpeedBindingDisplay(item.action, item.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
refreshAddShortcutSelector();
|
refreshAddShortcutSelector();
|
||||||
|
|
||||||
// Load site rules (use defaults if none in storage or if storage has empty array)
|
// Load site rules (use defaults if none in storage or empty array)
|
||||||
var siteRules = Array.isArray(storage.siteRules) && storage.siteRules.length > 0
|
var siteRules =
|
||||||
? storage.siteRules
|
Array.isArray(storage.siteRules) && storage.siteRules.length > 0
|
||||||
: (storage.blacklist ? migrateLegacyBlacklist(storage) : (tcDefaults.siteRules || []));
|
? storage.siteRules
|
||||||
|
: tcDefaults.siteRules || [];
|
||||||
|
|
||||||
// If we migrated from blacklist, save the new format
|
vscClearElement(document.getElementById("siteRulesContainer"));
|
||||||
if (storage.blacklist && siteRules.length > 0) {
|
|
||||||
chrome.storage.sync.set({ siteRules: siteRules });
|
|
||||||
chrome.storage.sync.remove(["blacklist"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("siteRulesContainer").innerHTML = "";
|
|
||||||
if (siteRules.length > 0) {
|
if (siteRules.length > 0) {
|
||||||
siteRules.forEach((rule) => {
|
siteRules.forEach((rule) => {
|
||||||
if (rule && rule.pattern) {
|
if (rule && rule.pattern) {
|
||||||
@@ -1368,12 +1661,20 @@ function restore_options() {
|
|||||||
: tcDefaults.popupControllerButtons;
|
: tcDefaults.popupControllerButtons;
|
||||||
populatePopupControlBarEditor(popupButtons);
|
populatePopupControlBarEditor(popupButtons);
|
||||||
updatePopupEditorDisabledState();
|
updatePopupEditorDisabledState();
|
||||||
|
|
||||||
|
initLucideButtonIconsUI();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function restore_defaults() {
|
function restore_defaults() {
|
||||||
document.querySelectorAll(".customs:not([id])").forEach((el) => el.remove());
|
document.querySelectorAll(".customs:not([id])").forEach((el) => el.remove());
|
||||||
|
|
||||||
|
chrome.storage.local.remove(
|
||||||
|
["customButtonIcons", "lucideTagsCacheV1", "lucideTagsCacheV1At"],
|
||||||
|
function () {}
|
||||||
|
);
|
||||||
|
|
||||||
chrome.storage.sync.set(tcDefaults, function () {
|
chrome.storage.sync.set(tcDefaults, function () {
|
||||||
restore_options();
|
restore_options();
|
||||||
var status = document.getElementById("status");
|
var status = document.getElementById("status");
|
||||||
@@ -1437,29 +1738,26 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
eventCaller(event, "customKey", recordKeyPress)
|
eventCaller(event, "customKey", recordKeyPress)
|
||||||
);
|
);
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
if (event.target.classList.contains("removeParent")) {
|
var target = event.target;
|
||||||
event.target.parentNode.remove();
|
var targetEl = target && target.closest ? target : target.parentElement;
|
||||||
|
if (!targetEl) return;
|
||||||
|
|
||||||
|
var removeParentButton = targetEl.closest(".removeParent");
|
||||||
|
if (removeParentButton) {
|
||||||
|
removeParentButton.parentNode.remove();
|
||||||
refreshAddShortcutSelector();
|
refreshAddShortcutSelector();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.target.classList.contains("remove-site-rule")) {
|
var removeSiteRuleButton = targetEl.closest(".remove-site-rule");
|
||||||
event.target.closest(".site-rule").remove();
|
if (removeSiteRuleButton) {
|
||||||
|
removeSiteRuleButton.closest(".site-rule").remove();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.target.classList.contains("toggle-site-rule")) {
|
var toggleButton = targetEl.closest(".toggle-site-rule");
|
||||||
var ruleEl = event.target.closest(".site-rule");
|
if (toggleButton) {
|
||||||
var ruleBody = ruleEl.querySelector(".site-rule-body");
|
var ruleEl = toggleButton.closest(".site-rule");
|
||||||
var isCollapsed = ruleEl.classList.contains("collapsed");
|
var isCollapsed = ruleEl.classList.contains("collapsed");
|
||||||
|
setSiteRuleExpandedState(ruleEl, isCollapsed);
|
||||||
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";
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1481,7 +1779,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
"override-autohide": "site-autohide-container",
|
"override-autohide": "site-autohide-container",
|
||||||
"override-playback": "site-playback-container",
|
"override-playback": "site-playback-container",
|
||||||
"override-opacity": "site-opacity-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) {
|
for (var ocb in siteOverrideContainers) {
|
||||||
if (event.target.classList.contains(ocb)) {
|
if (event.target.classList.contains(ocb)) {
|
||||||
@@ -1490,75 +1791,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
"." + siteOverrideContainers[ocb]
|
"." + siteOverrideContainers[ocb]
|
||||||
);
|
);
|
||||||
if (targetBox) {
|
if (targetBox) {
|
||||||
targetBox.style.display = event.target.checked ? "block" : "none";
|
setSiteOverrideContainerState(targetBox, event.target.checked);
|
||||||
}
|
}
|
||||||
return;
|
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()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -159,6 +159,22 @@ button:focus-visible {
|
|||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popup-control-bar button .vsc-btn-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
line-height: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-control-bar button .vsc-btn-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.popup-status {
|
.popup-status {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Speeder</title>
|
<title>Speeder</title>
|
||||||
<link rel="stylesheet" href="popup.css" />
|
<link rel="stylesheet" href="popup.css" />
|
||||||
|
<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>
|
<script src="popup.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,115 +1,52 @@
|
|||||||
document.addEventListener("DOMContentLoaded", function () {
|
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 = {
|
var controllerButtonDefs = {
|
||||||
rewind: { label: "\u00AB", className: "rw" },
|
rewind: { label: "", className: "rw" },
|
||||||
slower: { label: "\u2212", className: "" },
|
slower: { label: "", className: "" },
|
||||||
faster: { label: "+", className: "" },
|
faster: { label: "", className: "" },
|
||||||
advance: { label: "\u00BB", className: "rw" },
|
advance: { label: "", className: "rw" },
|
||||||
display: { label: "\u00D7", className: "hideButton" },
|
display: { label: "", className: "hideButton" },
|
||||||
reset: { label: "\u21BA", className: "" },
|
reset: { label: "\u21BB", className: "" },
|
||||||
fast: { label: "\u2605", className: "" },
|
fast: { label: "", className: "" },
|
||||||
settings: { label: "\u2699", className: "" },
|
nudge: { label: "", className: "" },
|
||||||
pause: { label: "\u23EF", className: "" },
|
settings: { label: "", className: "" },
|
||||||
muted: { label: "M", className: "" },
|
pause: { label: "", className: "" },
|
||||||
mark: { label: "\u2691", className: "" },
|
muted: { label: "", className: "" },
|
||||||
jump: { label: "\u21E5", className: "" }
|
mark: { label: "", className: "" },
|
||||||
|
jump: { label: "", className: "" }
|
||||||
};
|
};
|
||||||
|
|
||||||
var defaultButtons = ["rewind", "slower", "faster", "advance", "display"];
|
var defaultButtons = ["rewind", "slower", "faster", "advance", "display"];
|
||||||
|
var popupExcludedButtonIds = new Set(["settings"]);
|
||||||
var storageDefaults = {
|
var storageDefaults = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
showPopupControlBar: true,
|
showPopupControlBar: true,
|
||||||
controllerButtons: defaultButtons,
|
controllerButtons: defaultButtons,
|
||||||
popupMatchHoverControls: true,
|
popupMatchHoverControls: true,
|
||||||
popupControllerButtons: defaultButtons,
|
popupControllerButtons: defaultButtons,
|
||||||
siteRules: [],
|
siteRules: []
|
||||||
blacklist: `\
|
|
||||||
www.instagram.com
|
|
||||||
twitter.com
|
|
||||||
vine.co
|
|
||||||
imgur.com
|
|
||||||
teams.microsoft.com
|
|
||||||
`.replace(/^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm, "")
|
|
||||||
};
|
};
|
||||||
var renderToken = 0;
|
var renderToken = 0;
|
||||||
|
|
||||||
function escapeStringRegExp(str) {
|
|
||||||
const m = /[|\\{}()[\]^$+*?.]/g;
|
|
||||||
return str.replace(m, "\\$&");
|
|
||||||
}
|
|
||||||
|
|
||||||
function isBlacklisted(url, blacklist) {
|
|
||||||
let b = false;
|
|
||||||
const l = blacklist ? blacklist.split("\n") : [];
|
|
||||||
l.forEach((m) => {
|
|
||||||
if (b) return;
|
|
||||||
m = m.replace(regStrip, "");
|
|
||||||
if (m.length == 0) return;
|
|
||||||
let r;
|
|
||||||
if (m.startsWith("/") && m.lastIndexOf("/") > 0) {
|
|
||||||
try {
|
|
||||||
const ls = m.lastIndexOf("/");
|
|
||||||
r = new RegExp(m.substring(1, ls), m.substring(ls + 1));
|
|
||||||
} catch (e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else r = new RegExp(escapeStringRegExp(m));
|
|
||||||
if (r && r.test(url)) b = true;
|
|
||||||
});
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchSiteRule(url, siteRules) {
|
function matchSiteRule(url, siteRules) {
|
||||||
if (!url || !Array.isArray(siteRules)) return null;
|
return siteRuleUtils.matchSiteRule(url, siteRules);
|
||||||
for (var i = 0; i < siteRules.length; i++) {
|
|
||||||
var rule = siteRules[i];
|
|
||||||
if (!rule || !rule.pattern) continue;
|
|
||||||
var pattern = rule.pattern.replace(regStrip, "");
|
|
||||||
if (pattern.length === 0) continue;
|
|
||||||
var re;
|
|
||||||
if (pattern.startsWith("/") && pattern.lastIndexOf("/") > 0) {
|
|
||||||
try {
|
|
||||||
var ls = pattern.lastIndexOf("/");
|
|
||||||
re = new RegExp(pattern.substring(1, ls), pattern.substring(ls + 1));
|
|
||||||
} catch (e) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
re = new RegExp(escapeStringRegExp(pattern));
|
|
||||||
}
|
|
||||||
if (re && re.test(url)) return rule;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSiteRuleDisabled(rule) {
|
function isSiteRuleDisabled(rule) {
|
||||||
return Boolean(
|
return siteRuleUtils.isSiteRuleDisabled(rule);
|
||||||
rule &&
|
|
||||||
(rule.enabled === false || rule.disableExtension === true)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePopupButtons(storage, siteRule) {
|
function resolvePopupButtons(storage, siteRule) {
|
||||||
if (siteRule && Array.isArray(siteRule.popupControllerButtons)) {
|
return popupControlUtils.resolvePopupButtons(storage, siteRule, {
|
||||||
return siteRule.popupControllerButtons;
|
controllerButtonDefs: controllerButtonDefs,
|
||||||
}
|
defaultButtons: defaultButtons,
|
||||||
|
excludedIds: popupExcludedButtonIds
|
||||||
if (storage.popupMatchHoverControls) {
|
});
|
||||||
if (siteRule && Array.isArray(siteRule.controllerButtons)) {
|
|
||||||
return siteRule.controllerButtons;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(storage.controllerButtons)) {
|
|
||||||
return storage.controllerButtons;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(storage.popupControllerButtons)) {
|
|
||||||
return storage.popupControllerButtons;
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultButtons;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setControlBarVisible(visible) {
|
function setControlBarVisible(visible) {
|
||||||
@@ -171,29 +108,83 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
if (el) el.textContent = (speed != null ? Number(speed) : 1).toFixed(2);
|
if (el) el.textContent = (speed != null ? Number(speed) : 1).toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applySpeedAndResetFromResponse(response) {
|
||||||
|
if (response && response.speed != null) {
|
||||||
|
updateSpeedDisplay(response.speed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickBestFrameSpeedResult(results) {
|
||||||
|
return popupControlUtils.pickBestFrameSpeedResult(results);
|
||||||
|
}
|
||||||
|
|
||||||
function querySpeed() {
|
function querySpeed() {
|
||||||
sendToActiveTab({ action: "get_speed" }, function (response) {
|
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
|
||||||
if (response && response.speed != null) {
|
if (!tabs[0] || tabs[0].id == null) {
|
||||||
updateSpeedDisplay(response.speed);
|
return;
|
||||||
}
|
}
|
||||||
|
var tabId = tabs[0].id;
|
||||||
|
chrome.tabs.executeScript(
|
||||||
|
tabId,
|
||||||
|
{ allFrames: true, file: "frameSpeedSnapshot.js" },
|
||||||
|
function (results) {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
sendToActiveTab({ action: "get_speed" }, function (response) {
|
||||||
|
applySpeedAndResetFromResponse(response || { speed: 1 });
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var best = pickBestFrameSpeedResult(results);
|
||||||
|
if (best) {
|
||||||
|
applySpeedAndResetFromResponse(best);
|
||||||
|
} else {
|
||||||
|
sendToActiveTab({ action: "get_speed" }, function (response) {
|
||||||
|
applySpeedAndResetFromResponse(response || { speed: 1 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildControlBar(buttons) {
|
function buildControlBar(buttons, customIconsMap) {
|
||||||
var bar = document.getElementById("popupControlBar");
|
var bar = document.getElementById("popupControlBar");
|
||||||
if (!bar) return;
|
if (!bar) return;
|
||||||
|
|
||||||
var existing = bar.querySelectorAll("button");
|
var existing = bar.querySelectorAll("button");
|
||||||
existing.forEach(function (btn) { btn.remove(); });
|
existing.forEach(function (btn) { btn.remove(); });
|
||||||
|
|
||||||
|
var customMap = customIconsMap || {};
|
||||||
|
|
||||||
buttons.forEach(function (btnId) {
|
buttons.forEach(function (btnId) {
|
||||||
if (btnId === "nudge") return;
|
|
||||||
var def = controllerButtonDefs[btnId];
|
var def = controllerButtonDefs[btnId];
|
||||||
if (!def) return;
|
if (!def) return;
|
||||||
|
|
||||||
var btn = document.createElement("button");
|
var btn = document.createElement("button");
|
||||||
btn.dataset.action = btnId;
|
btn.dataset.action = btnId;
|
||||||
btn.textContent = def.label;
|
var customEntry = customMap[btnId];
|
||||||
|
if (customEntry && customEntry.svg) {
|
||||||
|
var customSpan = vscCreateSvgWrap(document, customEntry.svg, "vsc-btn-icon");
|
||||||
|
if (customSpan) {
|
||||||
|
btn.appendChild(customSpan);
|
||||||
|
} else {
|
||||||
|
btn.textContent = def.label || "?";
|
||||||
|
}
|
||||||
|
} else if (typeof vscIconSvgString === "function") {
|
||||||
|
var svgStr = vscIconSvgString(btnId, 16);
|
||||||
|
if (svgStr) {
|
||||||
|
var iconSpan = vscCreateSvgWrap(document, svgStr, "vsc-btn-icon");
|
||||||
|
if (iconSpan) {
|
||||||
|
btn.appendChild(iconSpan);
|
||||||
|
} else {
|
||||||
|
btn.textContent = def.label || "?";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
btn.textContent = def.label || "?";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
btn.textContent = def.label || "?";
|
||||||
|
}
|
||||||
if (def.className) btn.className = def.className;
|
if (def.className) btn.className = def.className;
|
||||||
btn.title = btnId.charAt(0).toUpperCase() + btnId.slice(1);
|
btn.title = btnId.charAt(0).toUpperCase() + btnId.slice(1);
|
||||||
|
|
||||||
@@ -204,10 +195,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
}
|
}
|
||||||
sendToActiveTab(
|
sendToActiveTab(
|
||||||
{ action: "run_action", actionName: btnId },
|
{ action: "run_action", actionName: btnId },
|
||||||
function (response) {
|
function () {
|
||||||
if (response && response.speed != null) {
|
querySpeed();
|
||||||
updateSpeedDisplay(response.speed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -272,18 +261,23 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
function renderForActiveTab() {
|
function renderForActiveTab() {
|
||||||
var currentRenderToken = ++renderToken;
|
var currentRenderToken = ++renderToken;
|
||||||
|
|
||||||
chrome.storage.sync.get(storageDefaults, function (storage) {
|
chrome.storage.local.get(["customButtonIcons"], function (loc) {
|
||||||
if (currentRenderToken !== renderToken) return;
|
if (currentRenderToken !== renderToken) return;
|
||||||
|
var customIconsMap =
|
||||||
|
loc && loc.customButtonIcons && typeof loc.customButtonIcons === "object"
|
||||||
|
? loc.customButtonIcons
|
||||||
|
: {};
|
||||||
|
|
||||||
|
chrome.storage.sync.get(storageDefaults, function (storage) {
|
||||||
|
if (currentRenderToken !== renderToken) return;
|
||||||
|
|
||||||
getActiveTabContext(function (context) {
|
getActiveTabContext(function (context) {
|
||||||
if (currentRenderToken !== renderToken) return;
|
if (currentRenderToken !== renderToken) return;
|
||||||
|
|
||||||
var url = context && context.url ? context.url : "";
|
var url = context && context.url ? context.url : "";
|
||||||
var siteRule = matchSiteRule(url, storage.siteRules);
|
var siteRule = matchSiteRule(url, storage.siteRules);
|
||||||
var blacklisted = isBlacklisted(url, storage.blacklist);
|
|
||||||
var siteDisabled = isSiteRuleDisabled(siteRule);
|
var siteDisabled = isSiteRuleDisabled(siteRule);
|
||||||
var siteAvailable =
|
var siteAvailable = storage.enabled !== false && !siteDisabled;
|
||||||
storage.enabled !== false && !blacklisted && !siteDisabled;
|
|
||||||
var showBar = storage.showPopupControlBar !== false;
|
var showBar = storage.showPopupControlBar !== false;
|
||||||
|
|
||||||
if (siteRule && siteRule.showPopupControlBar !== undefined) {
|
if (siteRule && siteRule.showPopupControlBar !== undefined) {
|
||||||
@@ -291,15 +285,12 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleEnabledUI(storage.enabled !== false);
|
toggleEnabledUI(storage.enabled !== false);
|
||||||
buildControlBar(resolvePopupButtons(storage, siteRule));
|
buildControlBar(
|
||||||
|
resolvePopupButtons(storage, siteRule),
|
||||||
|
customIconsMap
|
||||||
|
);
|
||||||
setControlBarVisible(siteAvailable && showBar);
|
setControlBarVisible(siteAvailable && showBar);
|
||||||
|
|
||||||
if (blacklisted) {
|
|
||||||
setStatusMessage("Site is blacklisted.");
|
|
||||||
updateSpeedDisplay(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (siteDisabled) {
|
if (siteDisabled) {
|
||||||
setStatusMessage("Speeder is disabled for this site.");
|
setStatusMessage("Speeder is disabled for this site.");
|
||||||
updateSpeedDisplay(1);
|
updateSpeedDisplay(1);
|
||||||
@@ -313,6 +304,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
updateSpeedDisplay(1);
|
updateSpeedDisplay(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,6 +320,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
chrome.storage.onChanged.addListener(function (changes, areaName) {
|
chrome.storage.onChanged.addListener(function (changes, areaName) {
|
||||||
|
if (areaName === "local" && changes.customButtonIcons) {
|
||||||
|
renderForActiveTab();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (areaName !== "sync") return;
|
if (areaName !== "sync") return;
|
||||||
if (
|
if (
|
||||||
changes.enabled ||
|
changes.enabled ||
|
||||||
@@ -335,8 +331,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
changes.controllerButtons ||
|
changes.controllerButtons ||
|
||||||
changes.popupMatchHoverControls ||
|
changes.popupMatchHoverControls ||
|
||||||
changes.popupControllerButtons ||
|
changes.popupControllerButtons ||
|
||||||
changes.siteRules ||
|
changes.siteRules
|
||||||
changes.blacklist
|
|
||||||
) {
|
) {
|
||||||
renderForActiveTab();
|
renderForActiveTab();
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
+98
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Squash beta onto main, set manifest version, one release commit, push stable tag (v* without -beta).
|
||||||
|
# Does not merge dev or push to beta — promote only what is already on beta.
|
||||||
|
# Triggers .github/workflows/deploy.yml: listed AMO submission.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
manifest_version() {
|
||||||
|
python3 -c 'import json; print(json.load(open("manifest.json"))["version"])'
|
||||||
|
}
|
||||||
|
|
||||||
|
bump_manifest() {
|
||||||
|
local ver="$1"
|
||||||
|
VER="$ver" python3 <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
ver = os.environ["VER"]
|
||||||
|
path = "manifest.json"
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
data["version"] = ver
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_semver() {
|
||||||
|
local s="$1"
|
||||||
|
s="${s#"${s%%[![:space:]]*}"}"
|
||||||
|
s="${s%"${s##*[![:space:]]}"}"
|
||||||
|
s="${s#v}"
|
||||||
|
s="${s#V}"
|
||||||
|
printf '%s' "$s"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_semver() {
|
||||||
|
local s="$1"
|
||||||
|
if [[ -z "$s" ]]; then
|
||||||
|
echo "Error: empty version." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ ! "$s" =~ ^[0-9]+(\.[0-9]+){0,3}(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
|
||||||
|
echo "Error: invalid version (use something like 5.0.4)." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -n "$(git status --porcelain)" ]]; then
|
||||||
|
echo "Error: working tree is not clean. Commit or stash before releasing." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git checkout beta
|
||||||
|
git pull origin beta
|
||||||
|
|
||||||
|
echo "Current version on beta (manifest.json): $(manifest_version)"
|
||||||
|
read -r -p "Release version for manifest.json + tag (e.g. 5.0.4): " SEMVER_IN
|
||||||
|
SEMVER="$(normalize_semver "$SEMVER_IN")"
|
||||||
|
validate_semver "$SEMVER"
|
||||||
|
|
||||||
|
TAG="v${SEMVER}"
|
||||||
|
if [[ "$TAG" == *-beta* ]]; then
|
||||||
|
echo "Warning: stable tags should not contain '-beta' (workflow would use unlisted + prerelease, not AMO listed)."
|
||||||
|
read -r -p "Continue anyway? [y/N] " w
|
||||||
|
[[ "${w:-}" =~ ^[yY](es)?$ ]] || { echo "Aborted."; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "This will:"
|
||||||
|
echo " 1. checkout main, merge --squash origin/beta (single release commit on main)"
|
||||||
|
echo " 2. set manifest.json to $SEMVER in that commit (if anything else changed, it is included too)"
|
||||||
|
echo " 3. push origin main, create tag $TAG, push tag (triggers listed AMO submit)"
|
||||||
|
echo " 4. checkout dev (merge main→dev yourself if you want them aligned)"
|
||||||
|
read -r -p "Continue? [y/N] " confirm
|
||||||
|
[[ "${confirm:-}" =~ ^[yY](es)?$ ]] || { echo "Aborted."; exit 1; }
|
||||||
|
|
||||||
|
echo "🚀 Releasing stable $TAG to AMO (listed)"
|
||||||
|
|
||||||
|
git checkout main
|
||||||
|
git pull origin main
|
||||||
|
git merge --squash beta
|
||||||
|
bump_manifest "$SEMVER"
|
||||||
|
git add -A
|
||||||
|
git commit -m "Release $TAG"
|
||||||
|
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
git tag -a "$TAG" -m "$TAG"
|
||||||
|
git push origin "$TAG"
|
||||||
|
|
||||||
|
git checkout dev
|
||||||
|
|
||||||
|
echo "✅ Done: main squashed from beta, tagged $TAG (manifest $SEMVER)"
|
||||||
Executable
+104
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Merge dev → beta, push beta, and push an annotated beta tag (v*-beta*).
|
||||||
|
# Triggers .github/workflows/deploy.yml: unlisted AMO sign + GitHub prerelease.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
manifest_version() {
|
||||||
|
python3 -c 'import json; print(json.load(open("manifest.json"))["version"])'
|
||||||
|
}
|
||||||
|
|
||||||
|
bump_manifest() {
|
||||||
|
local ver="$1"
|
||||||
|
VER="$ver" python3 <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
ver = os.environ["VER"]
|
||||||
|
path = "manifest.json"
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
data["version"] = ver
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_semver() {
|
||||||
|
local s="$1"
|
||||||
|
s="${s#"${s%%[![:space:]]*}"}"
|
||||||
|
s="${s%"${s##*[![:space:]]}"}"
|
||||||
|
s="${s#v}"
|
||||||
|
s="${s#V}"
|
||||||
|
printf '%s' "$s"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_semver() {
|
||||||
|
local s="$1"
|
||||||
|
if [[ -z "$s" ]]; then
|
||||||
|
echo "Error: empty version." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ ! "$s" =~ ^[0-9]+(\.[0-9]+){0,3}(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
|
||||||
|
echo "Error: invalid version (use something like 5.0.4 or 5.0.4-beta.1)." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -n "$(git status --porcelain)" ]]; then
|
||||||
|
echo "Error: working tree is not clean. Commit or stash before releasing." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git checkout dev
|
||||||
|
git pull origin dev
|
||||||
|
|
||||||
|
echo "Current version in manifest.json: $(manifest_version)"
|
||||||
|
read -r -p "New version for manifest.json (e.g. 5.0.4): " SEMVER_IN
|
||||||
|
SEMVER="$(normalize_semver "$SEMVER_IN")"
|
||||||
|
validate_semver "$SEMVER"
|
||||||
|
|
||||||
|
echo "Beta git tag will include '-beta' (required by deploy.yml)."
|
||||||
|
read -r -p "Beta tag suffix [beta.1]: " SUFFIX_IN
|
||||||
|
SUFFIX="${SUFFIX_IN#"${SUFFIX_IN%%[![:space:]]*}"}"
|
||||||
|
SUFFIX="${SUFFIX%"${SUFFIX##*[![:space:]]}"}"
|
||||||
|
SUFFIX="${SUFFIX:-beta.1}"
|
||||||
|
|
||||||
|
TAG="v${SEMVER}-${SUFFIX}"
|
||||||
|
if [[ "$TAG" != *-beta* ]]; then
|
||||||
|
echo "Error: beta tag must contain '-beta' for the workflow (got $TAG). Try suffix like beta.1." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "This will:"
|
||||||
|
echo " 1. set manifest.json version to $SEMVER, commit on dev, push origin dev"
|
||||||
|
echo " 2. checkout beta, merge dev (no-ff), push origin beta"
|
||||||
|
echo " 3. create tag $TAG and push it (triggers beta AMO + prerelease)"
|
||||||
|
echo " 4. checkout dev (main is not modified)"
|
||||||
|
read -r -p "Continue? [y/N] " confirm
|
||||||
|
[[ "${confirm:-}" =~ ^[yY](es)?$ ]] || { echo "Aborted."; exit 1; }
|
||||||
|
|
||||||
|
echo "🚀 Releasing beta $TAG"
|
||||||
|
|
||||||
|
bump_manifest "$SEMVER"
|
||||||
|
git add manifest.json
|
||||||
|
git commit -m "Bump version to $SEMVER"
|
||||||
|
git push origin dev
|
||||||
|
|
||||||
|
git checkout beta
|
||||||
|
git pull origin beta
|
||||||
|
git merge dev --no-ff -m "$TAG"
|
||||||
|
git push origin beta
|
||||||
|
|
||||||
|
git tag -a "$TAG" -m "$TAG"
|
||||||
|
git push origin "$TAG"
|
||||||
|
|
||||||
|
git checkout dev
|
||||||
|
git pull origin dev
|
||||||
|
|
||||||
|
echo "✅ Done: beta $TAG (manifest $SEMVER; dev + beta + tag pushed)"
|
||||||
@@ -0,0 +1,644 @@
|
|||||||
|
(function (global) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var SITE_RULES_DIFF_FORMAT = "defaults-diff-v1";
|
||||||
|
var DEFAULT_BUTTONS = ["rewind", "slower", "faster", "advance", "display"];
|
||||||
|
var SITE_RULE_OVERRIDE_KEYS = [
|
||||||
|
"controllerLocation",
|
||||||
|
"controllerMarginTop",
|
||||||
|
"controllerMarginBottom",
|
||||||
|
"startHidden",
|
||||||
|
"hideWithControls",
|
||||||
|
"hideWithControlsTimer",
|
||||||
|
"rememberSpeed",
|
||||||
|
"forceLastSavedSpeed",
|
||||||
|
"audioBoolean",
|
||||||
|
"controllerOpacity",
|
||||||
|
"enableSubtitleNudge",
|
||||||
|
"subtitleNudgeInterval",
|
||||||
|
"controllerButtons",
|
||||||
|
"showPopupControlBar",
|
||||||
|
"popupControllerButtons",
|
||||||
|
"shortcuts",
|
||||||
|
"preferredSpeed"
|
||||||
|
];
|
||||||
|
var DIFFABLE_OPTION_KEYS = [
|
||||||
|
"rememberSpeed",
|
||||||
|
"forceLastSavedSpeed",
|
||||||
|
"audioBoolean",
|
||||||
|
"enabled",
|
||||||
|
"startHidden",
|
||||||
|
"hideWithControls",
|
||||||
|
"hideWithControlsTimer",
|
||||||
|
"controllerLocation",
|
||||||
|
"controllerOpacity",
|
||||||
|
"controllerMarginTop",
|
||||||
|
"controllerMarginBottom",
|
||||||
|
"keyBindings",
|
||||||
|
"siteRules",
|
||||||
|
"siteRulesMeta",
|
||||||
|
"siteRulesFormat",
|
||||||
|
"controllerButtons",
|
||||||
|
"showPopupControlBar",
|
||||||
|
"popupMatchHoverControls",
|
||||||
|
"popupControllerButtons",
|
||||||
|
"enableSubtitleNudge",
|
||||||
|
"subtitleNudgeInterval",
|
||||||
|
"subtitleNudgeAmount"
|
||||||
|
];
|
||||||
|
var MANAGED_SYNC_KEYS = DIFFABLE_OPTION_KEYS.concat([
|
||||||
|
"hideWithYouTubeControls"
|
||||||
|
]);
|
||||||
|
|
||||||
|
var DEFAULT_SETTINGS = {
|
||||||
|
speed: 1.0,
|
||||||
|
lastSpeed: 1.0,
|
||||||
|
displayKeyCode: 86,
|
||||||
|
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: [
|
||||||
|
{
|
||||||
|
action: "display",
|
||||||
|
key: "V",
|
||||||
|
keyCode: 86,
|
||||||
|
code: null,
|
||||||
|
disabled: false,
|
||||||
|
value: 0,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "move",
|
||||||
|
key: "P",
|
||||||
|
keyCode: 80,
|
||||||
|
code: null,
|
||||||
|
disabled: false,
|
||||||
|
value: 0,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "slower",
|
||||||
|
key: "S",
|
||||||
|
keyCode: 83,
|
||||||
|
code: null,
|
||||||
|
disabled: false,
|
||||||
|
value: 0.1,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "faster",
|
||||||
|
key: "D",
|
||||||
|
keyCode: 68,
|
||||||
|
code: null,
|
||||||
|
disabled: false,
|
||||||
|
value: 0.1,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "rewind",
|
||||||
|
key: "Z",
|
||||||
|
keyCode: 90,
|
||||||
|
code: null,
|
||||||
|
disabled: false,
|
||||||
|
value: 10,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "advance",
|
||||||
|
key: "X",
|
||||||
|
keyCode: 88,
|
||||||
|
code: null,
|
||||||
|
disabled: false,
|
||||||
|
value: 10,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "reset",
|
||||||
|
key: "R",
|
||||||
|
keyCode: 82,
|
||||||
|
code: null,
|
||||||
|
disabled: false,
|
||||||
|
value: 0,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "fast",
|
||||||
|
key: "G",
|
||||||
|
keyCode: 71,
|
||||||
|
code: null,
|
||||||
|
disabled: false,
|
||||||
|
value: 1.8,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "toggleSubtitleNudge",
|
||||||
|
key: "N",
|
||||||
|
keyCode: 78,
|
||||||
|
code: null,
|
||||||
|
disabled: false,
|
||||||
|
value: 0,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
siteRules: [
|
||||||
|
{
|
||||||
|
pattern: "/^https:\\/\\/(www\\.)?youtube\\.com\\/(?!shorts\\/).*/",
|
||||||
|
enabled: true,
|
||||||
|
enableSubtitleNudge: true,
|
||||||
|
subtitleNudgeInterval: 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "/^https:\\/\\/(www\\.)?youtube\\.com\\/shorts\\/.*/",
|
||||||
|
enabled: true,
|
||||||
|
rememberSpeed: true,
|
||||||
|
controllerMarginTop: 60,
|
||||||
|
controllerMarginBottom: 85
|
||||||
|
}
|
||||||
|
],
|
||||||
|
controllerButtons: DEFAULT_BUTTONS.slice(),
|
||||||
|
showPopupControlBar: true,
|
||||||
|
popupMatchHoverControls: true,
|
||||||
|
popupControllerButtons: DEFAULT_BUTTONS.slice(),
|
||||||
|
enableSubtitleNudge: false,
|
||||||
|
subtitleNudgeInterval: 50,
|
||||||
|
subtitleNudgeAmount: 0.001
|
||||||
|
};
|
||||||
|
|
||||||
|
function clonePlainData(value) {
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return JSON.parse(JSON.stringify(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasOwn(obj, key) {
|
||||||
|
return Boolean(obj) && Object.prototype.hasOwnProperty.call(obj, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value) {
|
||||||
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortComparableValue(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(sortComparableValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(value)) {
|
||||||
|
var sorted = {};
|
||||||
|
Object.keys(value)
|
||||||
|
.sort()
|
||||||
|
.forEach(function (key) {
|
||||||
|
if (value[key] === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sorted[key] = sortComparableValue(value[key]);
|
||||||
|
});
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function areComparableValuesEqual(a, b) {
|
||||||
|
return (
|
||||||
|
JSON.stringify(sortComparableValue(a)) ===
|
||||||
|
JSON.stringify(sortComparableValue(b))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepMergeDefaults(defaults, overrides) {
|
||||||
|
if (Array.isArray(defaults)) {
|
||||||
|
return Array.isArray(overrides)
|
||||||
|
? clonePlainData(overrides)
|
||||||
|
: clonePlainData(defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(defaults)) {
|
||||||
|
var result = clonePlainData(defaults) || {};
|
||||||
|
if (!isPlainObject(overrides)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(overrides).forEach(function (key) {
|
||||||
|
if (overrides[key] === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasOwn(defaults, key)) {
|
||||||
|
result[key] = deepMergeDefaults(defaults[key], overrides[key]);
|
||||||
|
} else {
|
||||||
|
result[key] = clonePlainData(overrides[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return overrides === undefined
|
||||||
|
? clonePlainData(defaults)
|
||||||
|
: clonePlainData(overrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepDiff(current, defaults) {
|
||||||
|
if (current === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(current)) {
|
||||||
|
return areComparableValuesEqual(current, defaults)
|
||||||
|
? undefined
|
||||||
|
: clonePlainData(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(current)) {
|
||||||
|
var result = {};
|
||||||
|
Object.keys(current).forEach(function (key) {
|
||||||
|
var diff = deepDiff(current[key], defaults && defaults[key]);
|
||||||
|
if (diff !== undefined) {
|
||||||
|
result[key] = diff;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Object.keys(result).length > 0 ? result : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return areComparableValuesEqual(current, defaults)
|
||||||
|
? undefined
|
||||||
|
: clonePlainData(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultSiteRules() {
|
||||||
|
return clonePlainData(DEFAULT_SETTINGS.siteRules) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultSiteRulesByPattern() {
|
||||||
|
var map = Object.create(null);
|
||||||
|
getDefaultSiteRules().forEach(function (rule) {
|
||||||
|
if (!rule || typeof rule.pattern !== "string" || !rule.pattern) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
map[rule.pattern] = rule;
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSiteRuleForDiff(rule, baseSettings) {
|
||||||
|
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pattern = typeof rule.pattern === "string" ? rule.pattern.trim() : "";
|
||||||
|
if (!pattern) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = { pattern: pattern };
|
||||||
|
var baseEnabled = hasOwn(baseSettings, "enabled")
|
||||||
|
? Boolean(baseSettings.enabled)
|
||||||
|
: true;
|
||||||
|
var ruleEnabled = hasOwn(rule, "enabled")
|
||||||
|
? Boolean(rule.enabled)
|
||||||
|
: hasOwn(rule, "disableExtension")
|
||||||
|
? !Boolean(rule.disableExtension)
|
||||||
|
: baseEnabled;
|
||||||
|
|
||||||
|
if (!areComparableValuesEqual(ruleEnabled, baseEnabled)) {
|
||||||
|
normalized.enabled = ruleEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
SITE_RULE_OVERRIDE_KEYS.forEach(function (key) {
|
||||||
|
var baseValue = clonePlainData(baseSettings[key]);
|
||||||
|
var effectiveValue = hasOwn(rule, key)
|
||||||
|
? clonePlainData(rule[key])
|
||||||
|
: baseValue;
|
||||||
|
|
||||||
|
if (!areComparableValuesEqual(effectiveValue, baseValue)) {
|
||||||
|
normalized[key] = effectiveValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(rule).forEach(function (key) {
|
||||||
|
if (
|
||||||
|
key === "pattern" ||
|
||||||
|
key === "enabled" ||
|
||||||
|
key === "disableExtension" ||
|
||||||
|
SITE_RULE_OVERRIDE_KEYS.indexOf(key) !== -1 ||
|
||||||
|
rule[key] === undefined
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized[key] = clonePlainData(rule[key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compressSiteRules(siteRules, baseSettings) {
|
||||||
|
if (!Array.isArray(siteRules)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultRules = getDefaultSiteRules();
|
||||||
|
var defaultRulesByPattern = getDefaultSiteRulesByPattern();
|
||||||
|
var currentPatterns = new Set();
|
||||||
|
var exportRules = [];
|
||||||
|
|
||||||
|
siteRules.forEach(function (rule) {
|
||||||
|
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pattern = typeof rule.pattern === "string" ? rule.pattern.trim() : "";
|
||||||
|
if (pattern) {
|
||||||
|
currentPatterns.add(pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedRule = normalizeSiteRuleForDiff(rule, baseSettings);
|
||||||
|
if (!normalizedRule || Object.keys(normalizedRule).length === 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultRule = pattern ? defaultRulesByPattern[pattern] : null;
|
||||||
|
var normalizedDefaultRule = defaultRule
|
||||||
|
? normalizeSiteRuleForDiff(defaultRule, baseSettings)
|
||||||
|
: null;
|
||||||
|
if (normalizedDefaultRule) {
|
||||||
|
if (areComparableValuesEqual(normalizedRule, normalizedDefaultRule)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultRuleDiff = deepDiff(normalizedRule, normalizedDefaultRule);
|
||||||
|
if (defaultRuleDiff && Object.keys(defaultRuleDiff).length > 0) {
|
||||||
|
defaultRuleDiff.pattern = pattern;
|
||||||
|
exportRules.push(defaultRuleDiff);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
exportRules.push(normalizedRule);
|
||||||
|
});
|
||||||
|
|
||||||
|
var removedDefaultPatterns = defaultRules
|
||||||
|
.map(function (rule) {
|
||||||
|
return rule && typeof rule.pattern === "string" ? rule.pattern : "";
|
||||||
|
})
|
||||||
|
.filter(function (pattern) {
|
||||||
|
return pattern && !currentPatterns.has(pattern);
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = {};
|
||||||
|
if (exportRules.length > 0) {
|
||||||
|
result.siteRules = exportRules;
|
||||||
|
result.siteRulesFormat = SITE_RULES_DIFF_FORMAT;
|
||||||
|
}
|
||||||
|
if (removedDefaultPatterns.length > 0) {
|
||||||
|
result.siteRulesMeta = {
|
||||||
|
removedDefaultPatterns: removedDefaultPatterns
|
||||||
|
};
|
||||||
|
result.siteRulesFormat = SITE_RULES_DIFF_FORMAT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandSiteRules(siteRules, siteRulesMeta) {
|
||||||
|
var defaultRules = getDefaultSiteRules();
|
||||||
|
var defaultRulesByPattern = getDefaultSiteRulesByPattern();
|
||||||
|
if (defaultRules.length === 0) {
|
||||||
|
return Array.isArray(siteRules) ? clonePlainData(siteRules) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var removedDefaultPatterns = new Set(
|
||||||
|
siteRulesMeta && Array.isArray(siteRulesMeta.removedDefaultPatterns)
|
||||||
|
? siteRulesMeta.removedDefaultPatterns
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
var modifiedDefaultRules = Object.create(null);
|
||||||
|
var customRules = [];
|
||||||
|
|
||||||
|
if (Array.isArray(siteRules)) {
|
||||||
|
siteRules.forEach(function (rule) {
|
||||||
|
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pattern = typeof rule.pattern === "string" ? rule.pattern.trim() : "";
|
||||||
|
if (
|
||||||
|
pattern &&
|
||||||
|
Object.prototype.hasOwnProperty.call(defaultRulesByPattern, pattern)
|
||||||
|
) {
|
||||||
|
modifiedDefaultRules[pattern] = clonePlainData(rule);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
customRules.push(clonePlainData(rule));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var mergedRules = [];
|
||||||
|
|
||||||
|
defaultRules.forEach(function (rule) {
|
||||||
|
var pattern = rule && typeof rule.pattern === "string" ? rule.pattern : "";
|
||||||
|
if (!pattern || removedDefaultPatterns.has(pattern)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modifiedDefaultRules[pattern]) {
|
||||||
|
mergedRules.push(
|
||||||
|
Object.assign(
|
||||||
|
{},
|
||||||
|
clonePlainData(rule),
|
||||||
|
clonePlainData(modifiedDefaultRules[pattern])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedRules.push(clonePlainData(rule));
|
||||||
|
});
|
||||||
|
|
||||||
|
customRules.forEach(function (rule) {
|
||||||
|
mergedRules.push(rule);
|
||||||
|
});
|
||||||
|
|
||||||
|
return mergedRules;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStoredSettingsDiff(currentSettings) {
|
||||||
|
var defaults = clonePlainData(DEFAULT_SETTINGS);
|
||||||
|
var normalized = deepMergeDefaults(defaults, currentSettings || {});
|
||||||
|
var siteRuleData = compressSiteRules(normalized.siteRules, normalized);
|
||||||
|
var diffDefaults = {};
|
||||||
|
var diff = {};
|
||||||
|
|
||||||
|
delete normalized.siteRules;
|
||||||
|
delete normalized.siteRulesMeta;
|
||||||
|
delete normalized.siteRulesFormat;
|
||||||
|
delete normalized.hideWithYouTubeControls;
|
||||||
|
|
||||||
|
if (siteRuleData.siteRules) {
|
||||||
|
normalized.siteRules = siteRuleData.siteRules;
|
||||||
|
}
|
||||||
|
if (siteRuleData.siteRulesMeta) {
|
||||||
|
normalized.siteRulesMeta = siteRuleData.siteRulesMeta;
|
||||||
|
}
|
||||||
|
if (siteRuleData.siteRulesFormat) {
|
||||||
|
normalized.siteRulesFormat = siteRuleData.siteRulesFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
DIFFABLE_OPTION_KEYS.forEach(function (key) {
|
||||||
|
if (hasOwn(DEFAULT_SETTINGS, key)) {
|
||||||
|
diffDefaults[key] = clonePlainData(DEFAULT_SETTINGS[key]);
|
||||||
|
}
|
||||||
|
if (!hasOwn(normalized, key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var valueDiff = deepDiff(normalized[key], diffDefaults[key]);
|
||||||
|
if (valueDiff !== undefined) {
|
||||||
|
diff[key] = valueDiff;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandStoredSettings(storage) {
|
||||||
|
var raw = clonePlainData(storage) || {};
|
||||||
|
var expanded = deepMergeDefaults(DEFAULT_SETTINGS, raw);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!hasOwn(raw, "hideWithControls") &&
|
||||||
|
hasOwn(raw, "hideWithYouTubeControls")
|
||||||
|
) {
|
||||||
|
expanded.hideWithControls = Boolean(raw.hideWithYouTubeControls);
|
||||||
|
}
|
||||||
|
expanded.hideWithYouTubeControls = expanded.hideWithControls;
|
||||||
|
|
||||||
|
if (raw.siteRulesFormat === SITE_RULES_DIFF_FORMAT) {
|
||||||
|
expanded.siteRules = expandSiteRules(raw.siteRules, raw.siteRulesMeta);
|
||||||
|
} else if (Array.isArray(raw.siteRules)) {
|
||||||
|
expanded.siteRules = clonePlainData(raw.siteRules);
|
||||||
|
} else {
|
||||||
|
expanded.siteRules = getDefaultSiteRules();
|
||||||
|
}
|
||||||
|
|
||||||
|
return expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeStringRegExp(str) {
|
||||||
|
var matcher = /[|\\{}()[\]^$+*?.]/g;
|
||||||
|
return String(str).replace(matcher, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function siteRuleMatchesUrl(rule, currentUrl) {
|
||||||
|
if (!rule || !rule.pattern || !currentUrl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pattern = String(rule.pattern).trim();
|
||||||
|
if (!pattern) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var regex;
|
||||||
|
if (pattern.startsWith("/") && pattern.lastIndexOf("/") > 0) {
|
||||||
|
try {
|
||||||
|
var lastSlash = pattern.lastIndexOf("/");
|
||||||
|
regex = new RegExp(
|
||||||
|
pattern.substring(1, lastSlash),
|
||||||
|
pattern.substring(lastSlash + 1)
|
||||||
|
);
|
||||||
|
} catch (_error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
regex = new RegExp(escapeStringRegExp(pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Boolean(regex && regex.test(currentUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeMatchingSiteRules(currentUrl, siteRules) {
|
||||||
|
if (!currentUrl || !Array.isArray(siteRules)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var matchedRules = [];
|
||||||
|
for (var i = 0; i < siteRules.length; i++) {
|
||||||
|
if (siteRuleMatchesUrl(siteRules[i], currentUrl)) {
|
||||||
|
matchedRules.push(siteRules[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchedRules.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mergedRule = {};
|
||||||
|
matchedRules.forEach(function (rule) {
|
||||||
|
Object.keys(rule).forEach(function (key) {
|
||||||
|
var value = rule[key];
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
mergedRule[key] = clonePlainData(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isPlainObject(value)) {
|
||||||
|
mergedRule[key] = clonePlainData(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mergedRule[key] = value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return mergedRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSiteRuleDisabled(rule) {
|
||||||
|
return Boolean(
|
||||||
|
rule &&
|
||||||
|
(
|
||||||
|
rule.enabled === false ||
|
||||||
|
(typeof rule.enabled === "undefined" && rule.disableExtension === true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
global.vscClonePlainData = clonePlainData;
|
||||||
|
global.vscAreComparableValuesEqual = areComparableValuesEqual;
|
||||||
|
global.vscDeepMergeDefaults = deepMergeDefaults;
|
||||||
|
global.vscBuildStoredSettingsDiff = buildStoredSettingsDiff;
|
||||||
|
global.vscExpandStoredSettings = expandStoredSettings;
|
||||||
|
global.vscGetSettingsDefaults = function () {
|
||||||
|
return clonePlainData(DEFAULT_SETTINGS);
|
||||||
|
};
|
||||||
|
global.vscGetManagedSyncKeys = function () {
|
||||||
|
return MANAGED_SYNC_KEYS.slice();
|
||||||
|
};
|
||||||
|
global.vscGetSiteRulesDiffFormat = function () {
|
||||||
|
return SITE_RULES_DIFF_FORMAT;
|
||||||
|
};
|
||||||
|
global.vscMatchSiteRule = mergeMatchingSiteRules;
|
||||||
|
global.vscSiteRuleMatchesUrl = siteRuleMatchesUrl;
|
||||||
|
global.vscIsSiteRuleDisabled = isSiteRuleDisabled;
|
||||||
|
})(typeof globalThis !== "undefined" ? globalThis : this);
|
||||||
+130
-24
@@ -4,10 +4,20 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Global * uses 1.9em line-height; without this, every node inside #controller
|
||||||
|
(including svg) keeps a tall line box and the bar grows + content rides high. */
|
||||||
|
#controller * {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show extra buttons on hover or keyboard :focus-visible only. Plain :focus-within
|
||||||
|
after a mouse click kept #controls visible while hover-only rules (e.g. draggable
|
||||||
|
margin) turned off when the pointer left the bar. */
|
||||||
#controller:hover #controls,
|
#controller:hover #controls,
|
||||||
#controller:focus-within #controls,
|
#controller:focus-within:has(:focus-visible) #controls,
|
||||||
:host(:hover) #controls {
|
:host(:hover) #controls {
|
||||||
display: inline;
|
display: inline-flex;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
#controller {
|
#controller {
|
||||||
@@ -40,8 +50,9 @@
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Space between speed readout and hover buttons — tweak this value (px) as you like */
|
||||||
#controller:hover > .draggable {
|
#controller:hover > .draggable {
|
||||||
margin-right: 0.8em;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Center presets: midpoint between left- and right-preset inset lines; center bar on that X. */
|
/* Center presets: midpoint between left- and right-preset inset lines; center bar on that X. */
|
||||||
@@ -55,25 +66,30 @@
|
|||||||
|
|
||||||
#controls {
|
#controls {
|
||||||
display: none;
|
display: none;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#controls > * + * {
|
/* Standalone flash next to speed when N is pressed — hidden = no layout footprint */
|
||||||
margin-left: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Standalone flash indicator next to speed text — hidden by default,
|
|
||||||
briefly shown when nudge is toggled via N key or click */
|
|
||||||
#nudge-flash-indicator {
|
#nudge-flash-indicator {
|
||||||
display: none;
|
display: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
width: 0;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 0;
|
||||||
|
height: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-left: 0.3em;
|
|
||||||
padding: 3px 6px;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -81,8 +97,27 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Same 24×24 footprint as #controls button */
|
||||||
#nudge-flash-indicator.visible {
|
#nudge-flash-indicator.visible {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
min-width: 24px;
|
||||||
|
min-height: 24px;
|
||||||
|
max-width: 24px;
|
||||||
|
max-height: 24px;
|
||||||
|
margin-left: 5px;
|
||||||
|
padding: 0;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 5px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0;
|
||||||
|
line-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide flash indicator when hovering — the one in #controls is visible instead */
|
/* Hide flash indicator when hovering — the one in #controls is visible instead */
|
||||||
@@ -100,46 +135,75 @@
|
|||||||
#nudge-flash-indicator[data-enabled="true"] {
|
#nudge-flash-indicator[data-enabled="true"] {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: #4b9135;
|
background: #4b9135;
|
||||||
border: 1px solid #6ec754;
|
border-color: #6ec754;
|
||||||
}
|
}
|
||||||
|
|
||||||
#nudge-flash-indicator[data-enabled="false"] {
|
#nudge-flash-indicator[data-enabled="false"] {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: #943e3e;
|
background: #943e3e;
|
||||||
border: 1px solid #c06060;
|
border-color: #c06060;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Same 24×24 chip as control buttons (Lucide check / x inside) */
|
||||||
#nudge-indicator {
|
#nudge-indicator {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 3px 6px;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
font-family: "Lucida Console", Monaco, monospace;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
min-width: 24px;
|
||||||
|
min-height: 24px;
|
||||||
|
max-height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 0;
|
||||||
|
line-height: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-bottom: 2px;
|
margin: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#nudge-indicator[data-enabled="true"] {
|
#nudge-indicator[data-enabled="true"] {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: #4b9135;
|
background: #4b9135;
|
||||||
border: 1px solid #6ec754;
|
border-color: #6ec754;
|
||||||
}
|
}
|
||||||
|
|
||||||
#nudge-indicator[data-enabled="false"] {
|
#nudge-indicator[data-enabled="false"] {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: #943e3e;
|
background: #943e3e;
|
||||||
border: 1px solid #c06060;
|
border-color: #c06060;
|
||||||
}
|
}
|
||||||
|
|
||||||
#nudge-indicator[data-supported="false"] {
|
#nudge-indicator[data-supported="false"] {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#nudge-flash-indicator.visible .vsc-btn-icon,
|
||||||
|
#nudge-indicator .vsc-btn-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nudge-flash-indicator.visible .vsc-btn-icon svg,
|
||||||
|
#nudge-indicator .vsc-btn-icon svg {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transform: translateY(0.5px);
|
||||||
|
}
|
||||||
|
|
||||||
#controller.dragging {
|
#controller.dragging {
|
||||||
cursor: -webkit-grabbing;
|
cursor: -webkit-grabbing;
|
||||||
cursor: -moz-grabbing;
|
cursor: -moz-grabbing;
|
||||||
@@ -148,12 +212,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#controller.dragging #controls {
|
#controller.dragging #controls {
|
||||||
display: inline;
|
display: inline-flex;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggable {
|
.draggable {
|
||||||
cursor: -webkit-grab;
|
cursor: -webkit-grab;
|
||||||
cursor: -moz-grab;
|
cursor: -moz-grab;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggable:active {
|
.draggable:active {
|
||||||
@@ -175,6 +241,46 @@ button {
|
|||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Icon buttons: square targets, compact bar (no extra vertical stretch). */
|
||||||
|
#controls button {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
min-width: 24px;
|
||||||
|
min-height: 24px;
|
||||||
|
max-height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border-width: 1px;
|
||||||
|
line-height: 0;
|
||||||
|
font-size: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
button .vsc-btn-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button .vsc-btn-icon svg {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
/* Lucide 24×24 paths sit slightly high in the viewBox */
|
||||||
|
transform: translateY(0.5px);
|
||||||
|
}
|
||||||
|
|
||||||
button:focus {
|
button:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
(function(root, factory) {
|
||||||
|
var exports = factory();
|
||||||
|
|
||||||
|
if (typeof module === "object" && module.exports) {
|
||||||
|
module.exports = exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.SpeederShared = root.SpeederShared || {};
|
||||||
|
root.SpeederShared.controllerUtils = exports;
|
||||||
|
})(typeof globalThis !== "undefined" ? globalThis : this, function() {
|
||||||
|
var CONTROLLER_MARGIN_MAX_PX = 200;
|
||||||
|
var controllerLocations = [
|
||||||
|
"top-left",
|
||||||
|
"top-center",
|
||||||
|
"top-right",
|
||||||
|
"middle-right",
|
||||||
|
"bottom-right",
|
||||||
|
"bottom-center",
|
||||||
|
"bottom-left",
|
||||||
|
"middle-left"
|
||||||
|
];
|
||||||
|
var defaultControllerLocation = controllerLocations[0];
|
||||||
|
|
||||||
|
function normalizeControllerLocation(location, fallback) {
|
||||||
|
if (controllerLocations.includes(location)) return location;
|
||||||
|
return typeof fallback === "string"
|
||||||
|
? fallback
|
||||||
|
: defaultControllerLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampControllerMarginPx(value, fallback) {
|
||||||
|
var numericValue = Number(value);
|
||||||
|
if (!Number.isFinite(numericValue)) return fallback;
|
||||||
|
|
||||||
|
return Math.min(
|
||||||
|
CONTROLLER_MARGIN_MAX_PX,
|
||||||
|
Math.max(0, Math.round(numericValue))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextControllerLocation(location) {
|
||||||
|
var normalizedLocation = normalizeControllerLocation(location);
|
||||||
|
var currentIndex = controllerLocations.indexOf(normalizedLocation);
|
||||||
|
return controllerLocations[(currentIndex + 1) % controllerLocations.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
CONTROLLER_MARGIN_MAX_PX: CONTROLLER_MARGIN_MAX_PX,
|
||||||
|
clampControllerMarginPx: clampControllerMarginPx,
|
||||||
|
controllerLocations: controllerLocations.slice(),
|
||||||
|
defaultControllerLocation: defaultControllerLocation,
|
||||||
|
getNextControllerLocation: getNextControllerLocation,
|
||||||
|
normalizeControllerLocation: normalizeControllerLocation
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
(function(root, factory) {
|
||||||
|
var exports = factory();
|
||||||
|
|
||||||
|
if (typeof module === "object" && module.exports) {
|
||||||
|
module.exports = exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.SpeederShared = root.SpeederShared || {};
|
||||||
|
root.SpeederShared.importExport = exports;
|
||||||
|
})(typeof globalThis !== "undefined" ? globalThis : this, function() {
|
||||||
|
var rawSettingsKeys = new Set([
|
||||||
|
"audioBoolean",
|
||||||
|
"controllerButtons",
|
||||||
|
"controllerLocation",
|
||||||
|
"controllerMarginBottom",
|
||||||
|
"controllerMarginLeft",
|
||||||
|
"controllerMarginRight",
|
||||||
|
"controllerMarginTop",
|
||||||
|
"controllerOpacity",
|
||||||
|
"enableSubtitleNudge",
|
||||||
|
"enabled",
|
||||||
|
"forceLastSavedSpeed",
|
||||||
|
"hideWithControls",
|
||||||
|
"hideWithControlsTimer",
|
||||||
|
"hideWithYouTubeControls",
|
||||||
|
"keyBindings",
|
||||||
|
"lastSpeed",
|
||||||
|
"popupControllerButtons",
|
||||||
|
"popupMatchHoverControls",
|
||||||
|
"rememberSpeed",
|
||||||
|
"showPopupControlBar",
|
||||||
|
"siteRules",
|
||||||
|
"speed",
|
||||||
|
"startHidden",
|
||||||
|
"subtitleNudgeAmount",
|
||||||
|
"subtitleNudgeInterval"
|
||||||
|
]);
|
||||||
|
|
||||||
|
function isRecognizedRawSettingsObject(backup) {
|
||||||
|
if (!backup || typeof backup !== "object" || Array.isArray(backup)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(backup).some(function(key) {
|
||||||
|
return rawSettingsKeys.has(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateBackupFilename(now) {
|
||||||
|
var date = now instanceof Date ? now : new Date(now || Date.now());
|
||||||
|
var year = date.getFullYear();
|
||||||
|
var month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
var day = String(date.getDate()).padStart(2, "0");
|
||||||
|
var hours = String(date.getHours()).padStart(2, "0");
|
||||||
|
var minutes = String(date.getMinutes()).padStart(2, "0");
|
||||||
|
var seconds = String(date.getSeconds()).padStart(2, "0");
|
||||||
|
|
||||||
|
return (
|
||||||
|
"speeder-backup_" +
|
||||||
|
year +
|
||||||
|
"-" +
|
||||||
|
month +
|
||||||
|
"-" +
|
||||||
|
day +
|
||||||
|
"_" +
|
||||||
|
hours +
|
||||||
|
"." +
|
||||||
|
minutes +
|
||||||
|
"." +
|
||||||
|
seconds +
|
||||||
|
".json"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBackupPayload(settings, localSettings, now) {
|
||||||
|
return {
|
||||||
|
version: "1.1",
|
||||||
|
exportDate: new Date(now || Date.now()).toISOString(),
|
||||||
|
settings: settings,
|
||||||
|
localSettings: localSettings || {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractImportSettings(backup) {
|
||||||
|
var settingsToImport = null;
|
||||||
|
var isWrappedBackup = false;
|
||||||
|
|
||||||
|
if (backup && backup.settings && typeof backup.settings === "object") {
|
||||||
|
settingsToImport = backup.settings;
|
||||||
|
isWrappedBackup = true;
|
||||||
|
} else if (
|
||||||
|
backup &&
|
||||||
|
typeof backup === "object" &&
|
||||||
|
isRecognizedRawSettingsObject(backup)
|
||||||
|
) {
|
||||||
|
settingsToImport = backup;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settingsToImport) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isWrappedBackup: isWrappedBackup,
|
||||||
|
settings: settingsToImport,
|
||||||
|
localSettings:
|
||||||
|
backup &&
|
||||||
|
backup.localSettings &&
|
||||||
|
typeof backup.localSettings === "object"
|
||||||
|
? backup.localSettings
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseImportText(text) {
|
||||||
|
return extractImportSettings(JSON.parse(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
buildBackupPayload: buildBackupPayload,
|
||||||
|
extractImportSettings: extractImportSettings,
|
||||||
|
generateBackupFilename: generateBackupFilename,
|
||||||
|
isRecognizedRawSettingsObject: isRecognizedRawSettingsObject,
|
||||||
|
parseImportText: parseImportText
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
(function(root, factory) {
|
||||||
|
var exports = factory();
|
||||||
|
|
||||||
|
if (typeof module === "object" && module.exports) {
|
||||||
|
module.exports = exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.SpeederShared = root.SpeederShared || {};
|
||||||
|
root.SpeederShared.keyBindings = exports;
|
||||||
|
})(typeof globalThis !== "undefined" ? globalThis : this, function() {
|
||||||
|
function normalizeBindingKey(key) {
|
||||||
|
if (typeof key !== "string" || key.length === 0) return null;
|
||||||
|
if (key === "Spacebar") return " ";
|
||||||
|
if (key === "Esc") return "Escape";
|
||||||
|
if (key.length === 1 && /[a-z]/i.test(key)) return key.toUpperCase();
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLegacyKeyCode(binding) {
|
||||||
|
if (!binding) return null;
|
||||||
|
if (Number.isInteger(binding.keyCode)) return binding.keyCode;
|
||||||
|
if (typeof binding.key === "number" && Number.isInteger(binding.key)) {
|
||||||
|
return binding.key;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function legacyBindingKeyToCode(key) {
|
||||||
|
var normalizedKey = normalizeBindingKey(key);
|
||||||
|
if (!normalizedKey) return null;
|
||||||
|
if (/^[A-Z]$/.test(normalizedKey)) return "Key" + normalizedKey;
|
||||||
|
if (/^[0-9]$/.test(normalizedKey)) return "Digit" + normalizedKey;
|
||||||
|
if (/^F([1-9]|1[0-2])$/.test(normalizedKey)) return normalizedKey;
|
||||||
|
|
||||||
|
var keyMap = {
|
||||||
|
" ": "Space",
|
||||||
|
ArrowLeft: "ArrowLeft",
|
||||||
|
ArrowUp: "ArrowUp",
|
||||||
|
ArrowRight: "ArrowRight",
|
||||||
|
ArrowDown: "ArrowDown",
|
||||||
|
";": "Semicolon",
|
||||||
|
"<": "Comma",
|
||||||
|
"-": "Minus",
|
||||||
|
"+": "Equal",
|
||||||
|
">": "Period",
|
||||||
|
"/": "Slash",
|
||||||
|
"~": "Backquote",
|
||||||
|
"[": "BracketLeft",
|
||||||
|
"\\": "Backslash",
|
||||||
|
"]": "BracketRight",
|
||||||
|
"'": "Quote"
|
||||||
|
};
|
||||||
|
|
||||||
|
return keyMap[normalizedKey] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function legacyKeyCodeToCode(keyCode) {
|
||||||
|
if (!Number.isInteger(keyCode)) return null;
|
||||||
|
if (keyCode >= 48 && keyCode <= 57) return "Digit" + String.fromCharCode(keyCode);
|
||||||
|
if (keyCode >= 65 && keyCode <= 90) return "Key" + String.fromCharCode(keyCode);
|
||||||
|
if (keyCode >= 96 && keyCode <= 105) return "Numpad" + (keyCode - 96);
|
||||||
|
if (keyCode >= 112 && keyCode <= 123) return "F" + (keyCode - 111);
|
||||||
|
|
||||||
|
var keyCodeMap = {
|
||||||
|
32: "Space",
|
||||||
|
37: "ArrowLeft",
|
||||||
|
38: "ArrowUp",
|
||||||
|
39: "ArrowRight",
|
||||||
|
40: "ArrowDown",
|
||||||
|
106: "NumpadMultiply",
|
||||||
|
107: "NumpadAdd",
|
||||||
|
109: "NumpadSubtract",
|
||||||
|
110: "NumpadDecimal",
|
||||||
|
111: "NumpadDivide",
|
||||||
|
186: "Semicolon",
|
||||||
|
188: "Comma",
|
||||||
|
189: "Minus",
|
||||||
|
187: "Equal",
|
||||||
|
190: "Period",
|
||||||
|
191: "Slash",
|
||||||
|
192: "Backquote",
|
||||||
|
219: "BracketLeft",
|
||||||
|
220: "Backslash",
|
||||||
|
221: "BracketRight",
|
||||||
|
222: "Quote",
|
||||||
|
59: "Semicolon",
|
||||||
|
61: "Equal",
|
||||||
|
173: "Minus"
|
||||||
|
};
|
||||||
|
|
||||||
|
return keyCodeMap[keyCode] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferBindingCode(binding, fallbackCode) {
|
||||||
|
if (binding && typeof binding.code === "string" && binding.code.length > 0) {
|
||||||
|
return binding.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding && typeof binding.key === "string") {
|
||||||
|
var codeFromKey = legacyBindingKeyToCode(binding.key);
|
||||||
|
if (codeFromKey) return codeFromKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
var legacyKeyCode = getLegacyKeyCode(binding);
|
||||||
|
if (Number.isInteger(legacyKeyCode)) {
|
||||||
|
var codeFromKeyCode = legacyKeyCodeToCode(legacyKeyCode);
|
||||||
|
if (codeFromKeyCode) return codeFromKeyCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof fallbackCode === "string" && fallbackCode.length > 0
|
||||||
|
? fallbackCode
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getLegacyKeyCode: getLegacyKeyCode,
|
||||||
|
inferBindingCode: inferBindingCode,
|
||||||
|
legacyBindingKeyToCode: legacyBindingKeyToCode,
|
||||||
|
legacyKeyCodeToCode: legacyKeyCodeToCode,
|
||||||
|
normalizeBindingKey: normalizeBindingKey
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
(function(root, factory) {
|
||||||
|
var exports = factory();
|
||||||
|
|
||||||
|
if (typeof module === "object" && module.exports) {
|
||||||
|
module.exports = exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.SpeederShared = root.SpeederShared || {};
|
||||||
|
root.SpeederShared.popupControls = exports;
|
||||||
|
})(typeof globalThis !== "undefined" ? globalThis : this, function() {
|
||||||
|
function normalizeExcludedIds(excludedIds) {
|
||||||
|
if (excludedIds instanceof Set) return excludedIds;
|
||||||
|
return new Set(Array.isArray(excludedIds) ? excludedIds : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeButtonOrder(buttonIds, controllerButtonDefs, excludedIds) {
|
||||||
|
if (!Array.isArray(buttonIds)) return [];
|
||||||
|
|
||||||
|
var seen = new Set();
|
||||||
|
var excluded = normalizeExcludedIds(excludedIds);
|
||||||
|
|
||||||
|
return buttonIds.filter(function(id) {
|
||||||
|
if (!controllerButtonDefs[id] || excluded.has(id) || seen.has(id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.add(id);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePopupButtons(storage, siteRule, options) {
|
||||||
|
var settings = storage || {};
|
||||||
|
var config = options || {};
|
||||||
|
var controllerButtonDefs = config.controllerButtonDefs || {};
|
||||||
|
var defaultButtons = Array.isArray(config.defaultButtons)
|
||||||
|
? config.defaultButtons
|
||||||
|
: [];
|
||||||
|
var excludedIds = config.excludedIds;
|
||||||
|
|
||||||
|
function sanitize(buttonIds) {
|
||||||
|
return sanitizeButtonOrder(buttonIds, controllerButtonDefs, excludedIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (siteRule && Array.isArray(siteRule.popupControllerButtons)) {
|
||||||
|
return sanitize(siteRule.popupControllerButtons);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.popupMatchHoverControls) {
|
||||||
|
if (siteRule && Array.isArray(siteRule.controllerButtons)) {
|
||||||
|
return sanitize(siteRule.controllerButtons);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(settings.controllerButtons)) {
|
||||||
|
return sanitize(settings.controllerButtons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(settings.popupControllerButtons)) {
|
||||||
|
return sanitize(settings.popupControllerButtons);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitize(defaultButtons);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickBestFrameSpeedResult(results) {
|
||||||
|
if (!results || !results.length) return null;
|
||||||
|
|
||||||
|
var fallback = null;
|
||||||
|
for (var i = 0; i < results.length; i++) {
|
||||||
|
var result = results[i];
|
||||||
|
if (!result || typeof result.speed !== "number") continue;
|
||||||
|
if (result.preferred) return { speed: result.speed };
|
||||||
|
if (!fallback) fallback = { speed: result.speed };
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pickBestFrameSpeedResult: pickBestFrameSpeedResult,
|
||||||
|
resolvePopupButtons: resolvePopupButtons,
|
||||||
|
sanitizeButtonOrder: sanitizeButtonOrder
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
(function(root, factory) {
|
||||||
|
var exports = factory();
|
||||||
|
|
||||||
|
if (typeof module === "object" && module.exports) {
|
||||||
|
module.exports = exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.SpeederShared = root.SpeederShared || {};
|
||||||
|
root.SpeederShared.siteRules = exports;
|
||||||
|
})(typeof globalThis !== "undefined" ? globalThis : this, function() {
|
||||||
|
var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
|
||||||
|
|
||||||
|
function escapeStringRegExp(str) {
|
||||||
|
return String(str).replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileSiteRulePattern(pattern) {
|
||||||
|
if (typeof pattern !== "string") return null;
|
||||||
|
|
||||||
|
var normalizedPattern = pattern.replace(regStrip, "");
|
||||||
|
if (normalizedPattern.length === 0) return null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedPattern.startsWith("/") &&
|
||||||
|
normalizedPattern.lastIndexOf("/") > 0
|
||||||
|
) {
|
||||||
|
var lastSlash = normalizedPattern.lastIndexOf("/");
|
||||||
|
return new RegExp(
|
||||||
|
normalizedPattern.substring(1, lastSlash),
|
||||||
|
normalizedPattern.substring(lastSlash + 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RegExp(escapeStringRegExp(normalizedPattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchSiteRule(url, siteRules) {
|
||||||
|
if (!url || !Array.isArray(siteRules)) return null;
|
||||||
|
|
||||||
|
for (var i = 0; i < siteRules.length; i++) {
|
||||||
|
var rule = siteRules[i];
|
||||||
|
if (!rule || !rule.pattern) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var re = compileSiteRulePattern(rule.pattern);
|
||||||
|
if (re && re.test(url)) {
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSiteRuleDisabled(rule) {
|
||||||
|
return Boolean(
|
||||||
|
rule &&
|
||||||
|
(rule.enabled === false || rule.disableExtension === true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
compileSiteRulePattern: compileSiteRulePattern,
|
||||||
|
escapeStringRegExp: escapeStringRegExp,
|
||||||
|
isSiteRuleDisabled: isSiteRuleDisabled,
|
||||||
|
matchSiteRule: matchSiteRule
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const repoRoot = path.resolve(__dirname, "..", "..");
|
||||||
|
|
||||||
|
function readRepoFile(relPath) {
|
||||||
|
return fs.readFileSync(path.join(repoRoot, relPath), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadHtml(relPath) {
|
||||||
|
document.open();
|
||||||
|
document.write(readRepoFile(relPath));
|
||||||
|
document.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadScript(relPath) {
|
||||||
|
window.eval(
|
||||||
|
"var chrome = window.chrome || globalThis.chrome;\n" +
|
||||||
|
readRepoFile(relPath) +
|
||||||
|
"\n//# sourceURL=" +
|
||||||
|
relPath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function flushAsyncWork() {
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function triggerDomContentLoaded() {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new window.Event("DOMContentLoaded", {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEvent() {
|
||||||
|
const listeners = [];
|
||||||
|
return {
|
||||||
|
addListener(listener) {
|
||||||
|
listeners.push(listener);
|
||||||
|
},
|
||||||
|
trigger(...args) {
|
||||||
|
listeners.forEach((listener) => listener(...args));
|
||||||
|
},
|
||||||
|
listeners
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStorageArea(initialState = {}) {
|
||||||
|
const state = { ...initialState };
|
||||||
|
|
||||||
|
function resolveGet(keys) {
|
||||||
|
if (keys == null) return { ...state };
|
||||||
|
if (Array.isArray(keys)) {
|
||||||
|
return keys.reduce((acc, key) => {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(state, key)) {
|
||||||
|
acc[key] = state[key];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
if (typeof keys === "string") {
|
||||||
|
return Object.prototype.hasOwnProperty.call(state, keys)
|
||||||
|
? { [keys]: state[keys] }
|
||||||
|
: {};
|
||||||
|
}
|
||||||
|
if (typeof keys === "object") {
|
||||||
|
const result = { ...keys };
|
||||||
|
Object.keys(state).forEach((key) => {
|
||||||
|
result[key] = state[key];
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
__state: state,
|
||||||
|
get: vi.fn((keys, callback) => {
|
||||||
|
callback(resolveGet(keys));
|
||||||
|
}),
|
||||||
|
set: vi.fn((items, callback) => {
|
||||||
|
Object.assign(state, items);
|
||||||
|
if (callback) callback();
|
||||||
|
}),
|
||||||
|
remove: vi.fn((keys, callback) => {
|
||||||
|
const list = Array.isArray(keys) ? keys : [keys];
|
||||||
|
list.forEach((key) => {
|
||||||
|
delete state[key];
|
||||||
|
});
|
||||||
|
if (callback) callback();
|
||||||
|
}),
|
||||||
|
clear: vi.fn((callback) => {
|
||||||
|
Object.keys(state).forEach((key) => delete state[key]);
|
||||||
|
if (callback) callback();
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createChromeMock(options = {}) {
|
||||||
|
const syncArea = createStorageArea(options.sync ?? {});
|
||||||
|
const localArea = createStorageArea(options.local ?? {});
|
||||||
|
const tabsOnActivated = createEvent();
|
||||||
|
const tabsOnUpdated = createEvent();
|
||||||
|
const storageOnChanged = createEvent();
|
||||||
|
|
||||||
|
const chrome = {
|
||||||
|
runtime: {
|
||||||
|
lastError: null,
|
||||||
|
getManifest: vi.fn(() => ({
|
||||||
|
version: options.manifestVersion || "9.9.9"
|
||||||
|
})),
|
||||||
|
getURL: vi.fn((url) => "moz-extension://speeder/" + url)
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
sync: syncArea,
|
||||||
|
local: localArea,
|
||||||
|
onChanged: storageOnChanged
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
query: vi.fn((queryInfo, callback) => {
|
||||||
|
callback(
|
||||||
|
options.tabs ??
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
active: true,
|
||||||
|
url: "https://example.com/watch"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
sendMessage: vi.fn((tabId, message, callback) => {
|
||||||
|
if (callback) {
|
||||||
|
callback(options.sendMessageResponse ?? { speed: 1.25 });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
executeScript: vi.fn((tabId, details, callback) => {
|
||||||
|
if (callback) {
|
||||||
|
callback(
|
||||||
|
options.executeScriptResponse ?? [
|
||||||
|
{ speed: 1.25, preferred: true }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
create: vi.fn(),
|
||||||
|
onActivated: tabsOnActivated,
|
||||||
|
onUpdated: tabsOnUpdated
|
||||||
|
},
|
||||||
|
browserAction: {
|
||||||
|
setIcon: vi.fn()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return chrome;
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { vi } = require("vitest");
|
||||||
|
|
||||||
|
const ROOT = path.resolve(__dirname, "..", "..");
|
||||||
|
|
||||||
|
function clone(value) {
|
||||||
|
if (value === undefined) return undefined;
|
||||||
|
return JSON.parse(JSON.stringify(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function workspacePath(relPath) {
|
||||||
|
return path.join(ROOT, relPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readWorkspaceFile(relPath) {
|
||||||
|
return fs.readFileSync(workspacePath(relPath), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadHtmlFile(relPath) {
|
||||||
|
document.open();
|
||||||
|
document.write(readWorkspaceFile(relPath));
|
||||||
|
document.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadHtmlString(html) {
|
||||||
|
document.open();
|
||||||
|
document.write(html);
|
||||||
|
document.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateScript(relPath) {
|
||||||
|
const source = readWorkspaceFile(relPath);
|
||||||
|
window.eval(
|
||||||
|
`${source}\n//# sourceURL=${workspacePath(relPath).replace(/\\/g, "/")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fireDOMContentLoaded() {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new window.Event("DOMContentLoaded", {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function flushAsyncWork(turns) {
|
||||||
|
const count = turns || 2;
|
||||||
|
for (let i = 0; i < count; i += 1) {
|
||||||
|
await Promise.resolve();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickStorageValues(data, keys) {
|
||||||
|
if (keys == null) return clone(data || {});
|
||||||
|
|
||||||
|
if (typeof keys === "string") {
|
||||||
|
return { [keys]: clone(data ? data[keys] : undefined) };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(keys)) {
|
||||||
|
const result = {};
|
||||||
|
keys.forEach((key) => {
|
||||||
|
result[key] = clone(data ? data[key] : undefined);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof keys === "object") {
|
||||||
|
const result = clone(keys) || {};
|
||||||
|
Object.keys(keys).forEach((key) => {
|
||||||
|
if (data && Object.prototype.hasOwnProperty.call(data, key)) {
|
||||||
|
result[key] = clone(data[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createChromeEvent() {
|
||||||
|
const listeners = [];
|
||||||
|
return {
|
||||||
|
addListener(listener) {
|
||||||
|
listeners.push(listener);
|
||||||
|
},
|
||||||
|
removeListener(listener) {
|
||||||
|
const index = listeners.indexOf(listener);
|
||||||
|
if (index >= 0) {
|
||||||
|
listeners.splice(index, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hasListener(listener) {
|
||||||
|
return listeners.includes(listener);
|
||||||
|
},
|
||||||
|
emit(...args) {
|
||||||
|
listeners.slice().forEach((listener) => listener(...args));
|
||||||
|
},
|
||||||
|
listeners
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStorageArea(areaName, initialData, onChangedEvent) {
|
||||||
|
let data = clone(initialData) || {};
|
||||||
|
|
||||||
|
function emitChanges(changes) {
|
||||||
|
if (changes && Object.keys(changes).length > 0) {
|
||||||
|
onChangedEvent.emit(changes, areaName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
get: vi.fn((keys, callback) => {
|
||||||
|
if (callback) callback(pickStorageValues(data, keys));
|
||||||
|
}),
|
||||||
|
set: vi.fn((items, callback) => {
|
||||||
|
const nextItems = items || {};
|
||||||
|
const changes = {};
|
||||||
|
|
||||||
|
Object.keys(nextItems).forEach((key) => {
|
||||||
|
const oldValue = clone(data[key]);
|
||||||
|
const newValue = clone(nextItems[key]);
|
||||||
|
data[key] = newValue;
|
||||||
|
changes[key] = { oldValue, newValue };
|
||||||
|
});
|
||||||
|
|
||||||
|
emitChanges(changes);
|
||||||
|
if (callback) callback();
|
||||||
|
}),
|
||||||
|
remove: vi.fn((keys, callback) => {
|
||||||
|
const list = Array.isArray(keys) ? keys : [keys];
|
||||||
|
const changes = {};
|
||||||
|
|
||||||
|
list.forEach((key) => {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
||||||
|
changes[key] = {
|
||||||
|
oldValue: clone(data[key]),
|
||||||
|
newValue: undefined
|
||||||
|
};
|
||||||
|
delete data[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
emitChanges(changes);
|
||||||
|
if (callback) callback();
|
||||||
|
}),
|
||||||
|
clear: vi.fn((callback) => {
|
||||||
|
const changes = {};
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
changes[key] = {
|
||||||
|
oldValue: clone(data[key]),
|
||||||
|
newValue: undefined
|
||||||
|
};
|
||||||
|
});
|
||||||
|
data = {};
|
||||||
|
emitChanges(changes);
|
||||||
|
if (callback) callback();
|
||||||
|
}),
|
||||||
|
_dump() {
|
||||||
|
return clone(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createChromeMock(options) {
|
||||||
|
const config = options || {};
|
||||||
|
const storageOnChanged = createChromeEvent();
|
||||||
|
const tabsOnActivated = createChromeEvent();
|
||||||
|
const tabsOnUpdated = createChromeEvent();
|
||||||
|
const runtimeOnMessage = createChromeEvent();
|
||||||
|
|
||||||
|
const chrome = {
|
||||||
|
runtime: {
|
||||||
|
lastError: null,
|
||||||
|
getManifest: vi.fn(() => clone(config.manifest) || { version: "0.0.0-test" }),
|
||||||
|
getURL: vi.fn((relPath) => `moz-extension://${relPath}`),
|
||||||
|
onMessage: runtimeOnMessage
|
||||||
|
},
|
||||||
|
browserAction: {
|
||||||
|
setIcon: vi.fn()
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
query: vi.fn((queryInfo, callback) => {
|
||||||
|
const tabs = clone(config.tabsQueryResult) || [
|
||||||
|
{ id: 1, active: true, url: "https://example.com/" }
|
||||||
|
];
|
||||||
|
if (callback) callback(tabs);
|
||||||
|
}),
|
||||||
|
sendMessage: vi.fn((tabId, message, callback) => {
|
||||||
|
if (callback) callback(null);
|
||||||
|
}),
|
||||||
|
executeScript: vi.fn((tabId, details, callback) => {
|
||||||
|
if (callback) callback([]);
|
||||||
|
}),
|
||||||
|
create: vi.fn(),
|
||||||
|
onActivated: tabsOnActivated,
|
||||||
|
onUpdated: tabsOnUpdated
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
onChanged: storageOnChanged,
|
||||||
|
sync: null,
|
||||||
|
local: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
chrome.storage.sync = createStorageArea(
|
||||||
|
"sync",
|
||||||
|
config.syncData,
|
||||||
|
storageOnChanged
|
||||||
|
);
|
||||||
|
chrome.storage.local = createStorageArea(
|
||||||
|
"local",
|
||||||
|
config.localData,
|
||||||
|
storageOnChanged
|
||||||
|
);
|
||||||
|
|
||||||
|
return chrome;
|
||||||
|
}
|
||||||
|
|
||||||
|
function installCommonWindowMocks() {
|
||||||
|
window.open = vi.fn();
|
||||||
|
window.close = vi.fn();
|
||||||
|
window.requestAnimationFrame = vi.fn((callback) => setTimeout(callback, 0));
|
||||||
|
window.cancelAnimationFrame = vi.fn((id) => clearTimeout(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createChromeMock,
|
||||||
|
evaluateScript,
|
||||||
|
fireDOMContentLoaded,
|
||||||
|
flushAsyncWork,
|
||||||
|
installCommonWindowMocks,
|
||||||
|
loadHtmlFile,
|
||||||
|
loadHtmlString,
|
||||||
|
readWorkspaceFile,
|
||||||
|
workspacePath
|
||||||
|
};
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
import {
|
||||||
|
createChromeMock,
|
||||||
|
flushAsyncWork,
|
||||||
|
loadHtml,
|
||||||
|
loadScript
|
||||||
|
} from "./helpers/browser.js";
|
||||||
|
|
||||||
|
async function setupImportExport(overrides = {}) {
|
||||||
|
loadHtml("options.html");
|
||||||
|
globalThis.chrome = createChromeMock(overrides);
|
||||||
|
window.chrome = globalThis.chrome;
|
||||||
|
globalThis.restore_options = vi.fn();
|
||||||
|
loadScript("shared/import-export.js");
|
||||||
|
loadScript("importExport.js");
|
||||||
|
await flushAsyncWork();
|
||||||
|
return globalThis.chrome;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("import/export flows", () => {
|
||||||
|
it("exports sync and local settings as a JSON download", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date(2026, 3, 4, 8, 9, 10));
|
||||||
|
const chrome = await setupImportExport({
|
||||||
|
sync: { rememberSpeed: true },
|
||||||
|
local: { customButtonIcons: { faster: { slug: "rocket" } } }
|
||||||
|
});
|
||||||
|
const OriginalBlob = globalThis.Blob;
|
||||||
|
globalThis.Blob = class TestBlob {
|
||||||
|
constructor(parts, options) {
|
||||||
|
this.parts = parts;
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async text() {
|
||||||
|
return this.parts.join("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let capturedBlob = null;
|
||||||
|
let clickedDownload = null;
|
||||||
|
Object.defineProperty(URL, "createObjectURL", {
|
||||||
|
configurable: true,
|
||||||
|
value: vi.fn((blob) => {
|
||||||
|
capturedBlob = blob;
|
||||||
|
return "blob:test";
|
||||||
|
})
|
||||||
|
});
|
||||||
|
Object.defineProperty(URL, "revokeObjectURL", {
|
||||||
|
configurable: true,
|
||||||
|
value: vi.fn(() => {})
|
||||||
|
});
|
||||||
|
vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(function () {
|
||||||
|
clickedDownload = this.download;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("exportSettings").click();
|
||||||
|
|
||||||
|
expect(clickedDownload).toBe("speeder-backup_2026-04-04_08.09.10.json");
|
||||||
|
expect(capturedBlob).not.toBeNull();
|
||||||
|
const blobText = await capturedBlob.text();
|
||||||
|
expect(JSON.parse(blobText)).toEqual({
|
||||||
|
version: "1.1",
|
||||||
|
exportDate: "2026-04-04T12:09:10.000Z",
|
||||||
|
settings: { rememberSpeed: true },
|
||||||
|
localSettings: { customButtonIcons: { faster: { slug: "rocket" } } }
|
||||||
|
});
|
||||||
|
expect(document.getElementById("status").textContent).toBe(
|
||||||
|
"Settings exported successfully"
|
||||||
|
);
|
||||||
|
expect(chrome.storage.sync.get).toHaveBeenCalled();
|
||||||
|
expect(chrome.storage.local.get).toHaveBeenCalled();
|
||||||
|
globalThis.Blob = OriginalBlob;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("imports wrapped backup payloads and refreshes options", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const chrome = await setupImportExport();
|
||||||
|
|
||||||
|
const originalCreateElement = document.createElement.bind(document);
|
||||||
|
let createdInput = null;
|
||||||
|
vi.spyOn(document, "createElement").mockImplementation((tagName) => {
|
||||||
|
const el = originalCreateElement(tagName);
|
||||||
|
if (tagName === "input") {
|
||||||
|
createdInput = el;
|
||||||
|
el.click = vi.fn();
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
});
|
||||||
|
|
||||||
|
globalThis.FileReader = class MockFileReader {
|
||||||
|
readAsText(file) {
|
||||||
|
this.onload({
|
||||||
|
target: {
|
||||||
|
result: file.__text
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
globalThis.importSettings();
|
||||||
|
createdInput.onchange({
|
||||||
|
target: {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
__text: JSON.stringify({
|
||||||
|
settings: { rememberSpeed: true },
|
||||||
|
localSettings: { customButtonIcons: { faster: { slug: "rocket" } } }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(chrome.storage.local.set).toHaveBeenCalledWith(
|
||||||
|
{ customButtonIcons: { faster: { slug: "rocket" } } },
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(chrome.storage.sync.clear).toHaveBeenCalled();
|
||||||
|
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
|
||||||
|
{ rememberSpeed: true },
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(document.getElementById("status").textContent).toBe(
|
||||||
|
"Settings imported successfully. Reloading..."
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(500);
|
||||||
|
expect(globalThis.restore_options).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("imports raw settings objects without touching local storage", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const chrome = await setupImportExport({
|
||||||
|
local: { customButtonIcons: { faster: { slug: "rocket" } } }
|
||||||
|
});
|
||||||
|
|
||||||
|
const originalCreateElement = document.createElement.bind(document);
|
||||||
|
let createdInput = null;
|
||||||
|
vi.spyOn(document, "createElement").mockImplementation((tagName) => {
|
||||||
|
const el = originalCreateElement(tagName);
|
||||||
|
if (tagName === "input") {
|
||||||
|
createdInput = el;
|
||||||
|
el.click = vi.fn();
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
});
|
||||||
|
|
||||||
|
globalThis.FileReader = class MockFileReader {
|
||||||
|
readAsText(file) {
|
||||||
|
this.onload({
|
||||||
|
target: {
|
||||||
|
result: file.__text
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
globalThis.importSettings();
|
||||||
|
createdInput.onchange({
|
||||||
|
target: {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
__text: JSON.stringify({
|
||||||
|
enabled: false,
|
||||||
|
siteRules: [{ pattern: "example.com", enabled: false }]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(chrome.storage.local.clear).not.toHaveBeenCalled();
|
||||||
|
expect(chrome.storage.local.set).not.toHaveBeenCalled();
|
||||||
|
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
enabled: false,
|
||||||
|
siteRules: [{ pattern: "example.com", enabled: false }]
|
||||||
|
},
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears stale local data when a wrapped backup has empty local settings", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const chrome = await setupImportExport({
|
||||||
|
local: {
|
||||||
|
customButtonIcons: { faster: { slug: "rocket" } },
|
||||||
|
lucideTagsCacheV1: { stale: true }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const originalCreateElement = document.createElement.bind(document);
|
||||||
|
let createdInput = null;
|
||||||
|
vi.spyOn(document, "createElement").mockImplementation((tagName) => {
|
||||||
|
const el = originalCreateElement(tagName);
|
||||||
|
if (tagName === "input") {
|
||||||
|
createdInput = el;
|
||||||
|
el.click = vi.fn();
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
});
|
||||||
|
|
||||||
|
globalThis.FileReader = class MockFileReader {
|
||||||
|
readAsText(file) {
|
||||||
|
this.onload({
|
||||||
|
target: {
|
||||||
|
result: file.__text
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
globalThis.importSettings();
|
||||||
|
createdInput.onchange({
|
||||||
|
target: {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
__text: JSON.stringify({
|
||||||
|
settings: { rememberSpeed: true },
|
||||||
|
localSettings: {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(chrome.storage.local.clear).toHaveBeenCalled();
|
||||||
|
expect(chrome.storage.local.set).not.toHaveBeenCalled();
|
||||||
|
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
|
||||||
|
{ rememberSpeed: true },
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows an error for invalid backup files", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const chrome = await setupImportExport();
|
||||||
|
|
||||||
|
const originalCreateElement = document.createElement.bind(document);
|
||||||
|
let createdInput = null;
|
||||||
|
vi.spyOn(document, "createElement").mockImplementation((tagName) => {
|
||||||
|
const el = originalCreateElement(tagName);
|
||||||
|
if (tagName === "input") {
|
||||||
|
createdInput = el;
|
||||||
|
el.click = vi.fn();
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
});
|
||||||
|
|
||||||
|
globalThis.FileReader = class MockFileReader {
|
||||||
|
readAsText(file) {
|
||||||
|
this.onload({
|
||||||
|
target: {
|
||||||
|
result: file.__text
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
globalThis.importSettings();
|
||||||
|
createdInput.onchange({
|
||||||
|
target: {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
__text: JSON.stringify({ wat: true })
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.getElementById("status").textContent).toBe(
|
||||||
|
"Error: Invalid backup file format"
|
||||||
|
);
|
||||||
|
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
const { afterEach, beforeEach, describe, expect, it, vi } = require("vitest");
|
||||||
|
const {
|
||||||
|
createChromeMock,
|
||||||
|
evaluateScript,
|
||||||
|
flushAsyncWork,
|
||||||
|
installCommonWindowMocks,
|
||||||
|
loadHtmlString
|
||||||
|
} = require("./helpers/extension-test-utils");
|
||||||
|
|
||||||
|
function bootImportExport(options) {
|
||||||
|
const config = options || {};
|
||||||
|
|
||||||
|
loadHtmlString(`<!doctype html><html><body>
|
||||||
|
<button id="exportSettings">Export</button>
|
||||||
|
<button id="importSettings">Import</button>
|
||||||
|
<div id="status"></div>
|
||||||
|
</body></html>`);
|
||||||
|
|
||||||
|
installCommonWindowMocks();
|
||||||
|
|
||||||
|
const chrome = createChromeMock({
|
||||||
|
syncData: config.syncData,
|
||||||
|
localData: config.localData
|
||||||
|
});
|
||||||
|
|
||||||
|
global.chrome = chrome;
|
||||||
|
window.chrome = chrome;
|
||||||
|
|
||||||
|
const createObjectURL = vi.fn(() => "blob:test");
|
||||||
|
const revokeObjectURL = vi.fn();
|
||||||
|
vi.stubGlobal("URL", {
|
||||||
|
createObjectURL,
|
||||||
|
revokeObjectURL
|
||||||
|
});
|
||||||
|
|
||||||
|
evaluateScript("importExport.js");
|
||||||
|
return { chrome, createObjectURL, revokeObjectURL };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("importExport.js", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
delete global.chrome;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generates timestamped backup filenames", () => {
|
||||||
|
vi.setSystemTime(new Date("2026-04-04T13:14:15Z"));
|
||||||
|
bootImportExport();
|
||||||
|
|
||||||
|
expect(window.generateBackupFilename()).toBe(
|
||||||
|
"speeder-backup_2026-04-04_13.14.15.json"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exports sync and local settings into a downloadable backup", async () => {
|
||||||
|
const clickSpy = vi
|
||||||
|
.spyOn(window.HTMLAnchorElement.prototype, "click")
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
const { createObjectURL, revokeObjectURL } = bootImportExport({
|
||||||
|
syncData: {
|
||||||
|
rememberSpeed: true,
|
||||||
|
keyBindings: [{ action: "faster", code: "KeyD", value: 0.1 }]
|
||||||
|
},
|
||||||
|
localData: {
|
||||||
|
customButtonIcons: {
|
||||||
|
faster: { slug: "rocket", svg: "<svg></svg>" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector("#exportSettings").click();
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
expect(createObjectURL).toHaveBeenCalledTimes(1);
|
||||||
|
const blob = createObjectURL.mock.calls[0][0];
|
||||||
|
const backup = JSON.parse(await blob.text());
|
||||||
|
|
||||||
|
expect(backup.settings.rememberSpeed).toBe(true);
|
||||||
|
expect(backup.localSettings.customButtonIcons.faster.slug).toBe("rocket");
|
||||||
|
expect(clickSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(revokeObjectURL).toHaveBeenCalledWith("blob:test");
|
||||||
|
expect(document.querySelector("#status").textContent).toContain("exported");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("imports wrapped backups, restores local data, and refreshes the options page", async () => {
|
||||||
|
const { chrome } = bootImportExport();
|
||||||
|
window.restore_options = vi.fn();
|
||||||
|
|
||||||
|
const realCreateElement = document.createElement.bind(document);
|
||||||
|
const fakeInput = realCreateElement("input");
|
||||||
|
Object.defineProperty(fakeInput, "files", {
|
||||||
|
configurable: true,
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
__contents: JSON.stringify({
|
||||||
|
settings: {
|
||||||
|
rememberSpeed: true,
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
localSettings: {
|
||||||
|
customButtonIcons: {
|
||||||
|
faster: { slug: "rocket", svg: "<svg></svg>" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
fakeInput.click = vi.fn(() => {
|
||||||
|
fakeInput.onchange({ target: fakeInput });
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.spyOn(document, "createElement").mockImplementation((tagName) => {
|
||||||
|
if (String(tagName).toLowerCase() === "input") {
|
||||||
|
return fakeInput;
|
||||||
|
}
|
||||||
|
return realCreateElement(tagName);
|
||||||
|
});
|
||||||
|
|
||||||
|
class FakeFileReader {
|
||||||
|
readAsText(file) {
|
||||||
|
this.onload({ target: { result: file.__contents } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.stubGlobal("FileReader", FakeFileReader);
|
||||||
|
|
||||||
|
document.querySelector("#importSettings").click();
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
expect(chrome.storage.local.set).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
customButtonIcons: {
|
||||||
|
faster: { slug: "rocket", svg: "<svg></svg>" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(chrome.storage.sync.clear).toHaveBeenCalled();
|
||||||
|
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
|
||||||
|
{ rememberSpeed: true, enabled: false },
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(500);
|
||||||
|
expect(window.restore_options).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows an error for malformed backups", async () => {
|
||||||
|
bootImportExport();
|
||||||
|
|
||||||
|
const realCreateElement = document.createElement.bind(document);
|
||||||
|
const fakeInput = realCreateElement("input");
|
||||||
|
Object.defineProperty(fakeInput, "files", {
|
||||||
|
configurable: true,
|
||||||
|
value: [{ __contents: "{bad json" }]
|
||||||
|
});
|
||||||
|
fakeInput.click = vi.fn(() => {
|
||||||
|
fakeInput.onchange({ target: fakeInput });
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.spyOn(document, "createElement").mockImplementation((tagName) => {
|
||||||
|
if (String(tagName).toLowerCase() === "input") {
|
||||||
|
return fakeInput;
|
||||||
|
}
|
||||||
|
return realCreateElement(tagName);
|
||||||
|
});
|
||||||
|
|
||||||
|
class FakeFileReader {
|
||||||
|
readAsText(file) {
|
||||||
|
this.onload({ target: { result: file.__contents } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.stubGlobal("FileReader", FakeFileReader);
|
||||||
|
|
||||||
|
document.querySelector("#importSettings").click();
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
expect(document.querySelector("#status").textContent).toContain(
|
||||||
|
"Failed to parse backup file"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
const { afterEach, describe, expect, it, vi } = require("vitest");
|
||||||
|
const {
|
||||||
|
createChromeMock,
|
||||||
|
evaluateScript,
|
||||||
|
flushAsyncWork,
|
||||||
|
loadHtmlString
|
||||||
|
} = require("./helpers/extension-test-utils");
|
||||||
|
|
||||||
|
function bootInject(options) {
|
||||||
|
const config = options || {};
|
||||||
|
|
||||||
|
loadHtmlString("<!doctype html><html><body></body></html>");
|
||||||
|
|
||||||
|
const chrome = createChromeMock({
|
||||||
|
syncData: config.syncData,
|
||||||
|
localData: config.localData
|
||||||
|
});
|
||||||
|
|
||||||
|
global.chrome = chrome;
|
||||||
|
window.chrome = chrome;
|
||||||
|
window.requestIdleCallback = (callback, opts) =>
|
||||||
|
setTimeout(
|
||||||
|
() =>
|
||||||
|
callback({
|
||||||
|
didTimeout: false,
|
||||||
|
timeRemaining() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
(opts && opts.timeout) || 0
|
||||||
|
);
|
||||||
|
window.cancelIdleCallback = (id) => clearTimeout(id);
|
||||||
|
|
||||||
|
evaluateScript("ui-icons.js");
|
||||||
|
evaluateScript("inject.js");
|
||||||
|
|
||||||
|
return chrome;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("inject.js helper logic", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
delete global.chrome;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes bindings from legacy formats", async () => {
|
||||||
|
bootInject();
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
window.normalizeStoredBinding({
|
||||||
|
action: "faster",
|
||||||
|
key: "g",
|
||||||
|
value: 1.8,
|
||||||
|
force: false
|
||||||
|
}).code
|
||||||
|
).toBe("KeyG");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
window.normalizeStoredBinding({
|
||||||
|
action: "pause",
|
||||||
|
code: null,
|
||||||
|
key: null,
|
||||||
|
keyCode: null,
|
||||||
|
value: 0
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
action: "pause",
|
||||||
|
code: null,
|
||||||
|
disabled: true,
|
||||||
|
value: 0,
|
||||||
|
force: "false",
|
||||||
|
predefined: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(window.defaultKeyBindings({ speedStep: 0.25, rewindTime: 5 })[0]).toEqual(
|
||||||
|
{
|
||||||
|
action: "slower",
|
||||||
|
code: "KeyS",
|
||||||
|
value: 0.25,
|
||||||
|
force: false,
|
||||||
|
predefined: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps controller margins and ignores stale source-specific target speeds", async () => {
|
||||||
|
bootInject();
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
|
||||||
|
expect(window.normalizeControllerMarginPx(250, 0)).toBe(200);
|
||||||
|
expect(window.normalizeControllerMarginPx(-5, 65)).toBe(0);
|
||||||
|
expect(window.normalizeControllerMarginPx("bad", 65)).toBe(65);
|
||||||
|
|
||||||
|
const staleVideo = {
|
||||||
|
currentSrc: "fresh.mp4",
|
||||||
|
vsc: {
|
||||||
|
targetSpeed: 1.75,
|
||||||
|
targetSpeedSourceKey: "old.mp4"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(window.getControllerTargetSpeed(staleVideo)).toBeNull();
|
||||||
|
|
||||||
|
window.tc.settings.rememberSpeed = true;
|
||||||
|
window.tc.settings.forceLastSavedSpeed = false;
|
||||||
|
window.tc.settings.lastSpeed = 1.3;
|
||||||
|
window.tc.settings.speeds = { "fresh.mp4": 1.6 };
|
||||||
|
|
||||||
|
expect(window.getRememberedSpeed({ currentSrc: "fresh.mp4" })).toBe(1.6);
|
||||||
|
expect(window.getDesiredSpeed(staleVideo)).toBe(1.6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies site rule overrides and detects disabled sites", async () => {
|
||||||
|
bootInject();
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
|
||||||
|
window.tc.settings.siteRules = [{ pattern: "localhost", enabled: false }];
|
||||||
|
window.captureSiteRuleBase();
|
||||||
|
expect(window.applySiteRuleOverrides()).toBe(true);
|
||||||
|
|
||||||
|
window.resetSettingsFromSiteRuleBase();
|
||||||
|
window.tc.settings.siteRules = [
|
||||||
|
{
|
||||||
|
pattern: "localhost",
|
||||||
|
controllerLocation: "bottom-left",
|
||||||
|
controllerMarginTop: 300,
|
||||||
|
controllerMarginBottom: -10,
|
||||||
|
rememberSpeed: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
window.captureSiteRuleBase();
|
||||||
|
|
||||||
|
expect(window.applySiteRuleOverrides()).toBe(false);
|
||||||
|
expect(window.tc.settings.controllerLocation).toBe("bottom-left");
|
||||||
|
expect(window.tc.settings.controllerMarginTop).toBe(200);
|
||||||
|
expect(window.tc.settings.controllerMarginBottom).toBe(0);
|
||||||
|
expect(window.tc.settings.rememberSpeed).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { createChromeMock, flushAsyncWork, loadScript } from "./helpers/browser.js";
|
||||||
|
|
||||||
|
function loadBlankDocument() {
|
||||||
|
document.open();
|
||||||
|
document.write("<!doctype html><html><body></body></html>");
|
||||||
|
document.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bootInject({ sync = {}, local = {} } = {}) {
|
||||||
|
loadBlankDocument();
|
||||||
|
globalThis.chrome = createChromeMock({ sync, local });
|
||||||
|
window.chrome = globalThis.chrome;
|
||||||
|
globalThis.chrome.runtime.onMessage = {
|
||||||
|
addListener: vi.fn()
|
||||||
|
};
|
||||||
|
const originalSyncGet = globalThis.chrome.storage.sync.get;
|
||||||
|
const originalLocalGet = globalThis.chrome.storage.local.get;
|
||||||
|
globalThis.chrome.storage.sync.get = vi.fn((keys, callback) => {
|
||||||
|
Promise.resolve().then(() => originalSyncGet(keys, callback));
|
||||||
|
});
|
||||||
|
globalThis.chrome.storage.local.get = vi.fn((keys, callback) => {
|
||||||
|
Promise.resolve().then(() => originalLocalGet(keys, callback));
|
||||||
|
});
|
||||||
|
globalThis.requestIdleCallback = (callback, options) =>
|
||||||
|
setTimeout(
|
||||||
|
() =>
|
||||||
|
callback({
|
||||||
|
didTimeout: false,
|
||||||
|
timeRemaining() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
(options && options.timeout) || 0
|
||||||
|
);
|
||||||
|
globalThis.cancelIdleCallback = (id) => clearTimeout(id);
|
||||||
|
|
||||||
|
loadScript("shared/controller-utils.js");
|
||||||
|
loadScript("shared/key-bindings.js");
|
||||||
|
loadScript("shared/site-rules.js");
|
||||||
|
loadScript("ui-icons.js");
|
||||||
|
loadScript("inject.js");
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i += 1) {
|
||||||
|
await flushAsyncWork();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("inject runtime", () => {
|
||||||
|
it("keeps subtitle nudge disabled when the effective setting is off", async () => {
|
||||||
|
await bootInject({
|
||||||
|
sync: {
|
||||||
|
enableSubtitleNudge: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const stopSubtitleNudge = vi.fn();
|
||||||
|
const startSubtitleNudge = vi.fn();
|
||||||
|
const flashEl = document.createElement("span");
|
||||||
|
const video = {
|
||||||
|
paused: false,
|
||||||
|
playbackRate: 1.5,
|
||||||
|
vsc: {
|
||||||
|
stopSubtitleNudge,
|
||||||
|
startSubtitleNudge,
|
||||||
|
subtitleNudgeEnabledOverride: null,
|
||||||
|
subtitleNudgeIndicator: null,
|
||||||
|
nudgeFlashIndicator: flashEl
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(window.tc.settings.enableSubtitleNudge).toBe(false);
|
||||||
|
expect(window.isSubtitleNudgeEnabledForVideo(video)).toBe(false);
|
||||||
|
expect(window.setSubtitleNudgeEnabledForVideo(video, true)).toBe(false);
|
||||||
|
expect(video.vsc.subtitleNudgeEnabledOverride).toBeNull();
|
||||||
|
expect(stopSubtitleNudge).toHaveBeenCalledTimes(1);
|
||||||
|
expect(startSubtitleNudge).not.toHaveBeenCalled();
|
||||||
|
expect(flashEl.classList.contains("visible")).toBe(false);
|
||||||
|
|
||||||
|
window.tc.settings.enableSubtitleNudge = true;
|
||||||
|
expect(window.setSubtitleNudgeEnabledForVideo(video, true)).toBe(true);
|
||||||
|
expect(window.isSubtitleNudgeEnabledForVideo(video)).toBe(true);
|
||||||
|
|
||||||
|
window.tc.settings.enableSubtitleNudge = false;
|
||||||
|
expect(window.isSubtitleNudgeEnabledForVideo(video)).toBe(false);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
const { afterEach, describe, expect, it } = require("vitest");
|
||||||
|
const {
|
||||||
|
evaluateScript,
|
||||||
|
loadHtmlString
|
||||||
|
} = require("./helpers/extension-test-utils");
|
||||||
|
|
||||||
|
describe("lucide-client.js", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds icon URLs and rejects invalid slugs", () => {
|
||||||
|
loadHtmlString("<!doctype html><html><body></body></html>");
|
||||||
|
evaluateScript("ui-icons.js");
|
||||||
|
evaluateScript("lucide-client.js");
|
||||||
|
|
||||||
|
expect(window.lucideIconSvgUrl("alarm-clock")).toContain(
|
||||||
|
"/icons/alarm-clock.svg"
|
||||||
|
);
|
||||||
|
expect(window.lucideIconSvgUrl("bad slug!!")).toBe("");
|
||||||
|
expect(window.lucideTagsJsonUrl()).toContain("/tags.json");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sanitizes SVG before persisting a Lucide icon", () => {
|
||||||
|
loadHtmlString("<!doctype html><html><body></body></html>");
|
||||||
|
evaluateScript("ui-icons.js");
|
||||||
|
evaluateScript("lucide-client.js");
|
||||||
|
|
||||||
|
const sanitized = window.sanitizeLucideSvg(`
|
||||||
|
<svg width="10" height="10" onclick="evil()">
|
||||||
|
<script>alert(1)</script>
|
||||||
|
<foreignObject>bad</foreignObject>
|
||||||
|
<path d="M0 0h10v10"></path>
|
||||||
|
</svg>
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(sanitized).toContain("<svg");
|
||||||
|
expect(sanitized).not.toContain("onclick");
|
||||||
|
expect(sanitized).not.toContain("<script");
|
||||||
|
expect(sanitized).not.toContain("foreignObject");
|
||||||
|
expect(sanitized).toContain('width="100%"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("searches and ranks icon slugs by query", () => {
|
||||||
|
loadHtmlString("<!doctype html><html><body></body></html>");
|
||||||
|
evaluateScript("ui-icons.js");
|
||||||
|
evaluateScript("lucide-client.js");
|
||||||
|
|
||||||
|
const results = window.searchLucideSlugs(
|
||||||
|
{
|
||||||
|
alarm: ["clock", "time"],
|
||||||
|
"badge-alert": ["alert", "warning"],
|
||||||
|
calendar: ["date", "time"]
|
||||||
|
},
|
||||||
|
"al",
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(results).toEqual(["alarm", "badge-alert", "calendar"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import {
|
||||||
|
createChromeMock,
|
||||||
|
flushAsyncWork,
|
||||||
|
loadHtml,
|
||||||
|
loadScript,
|
||||||
|
triggerDomContentLoaded
|
||||||
|
} from "./helpers/browser.js";
|
||||||
|
|
||||||
|
async function setupOptions(overrides = {}) {
|
||||||
|
loadHtml("options.html");
|
||||||
|
globalThis.chrome = createChromeMock(overrides);
|
||||||
|
window.chrome = globalThis.chrome;
|
||||||
|
globalThis.fetch = vi.fn();
|
||||||
|
loadScript("shared/controller-utils.js");
|
||||||
|
loadScript("shared/key-bindings.js");
|
||||||
|
loadScript("shared/popup-controls.js");
|
||||||
|
loadScript("ui-icons.js");
|
||||||
|
loadScript("lucide-client.js");
|
||||||
|
loadScript("options.js");
|
||||||
|
triggerDomContentLoaded();
|
||||||
|
await flushAsyncWork();
|
||||||
|
return globalThis.chrome;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("options page", () => {
|
||||||
|
it("restores stored settings, custom shortcuts, and site rules", async () => {
|
||||||
|
await setupOptions({
|
||||||
|
manifestVersion: "5.1.7.0",
|
||||||
|
sync: {
|
||||||
|
rememberSpeed: true,
|
||||||
|
enabled: false,
|
||||||
|
popupMatchHoverControls: false,
|
||||||
|
popupControllerButtons: ["rewind", "settings", "advance", "advance"],
|
||||||
|
keyBindings: [
|
||||||
|
{ action: "display", code: "KeyV", value: 0, predefined: true },
|
||||||
|
{ action: "pause", code: "KeyQ", value: 0, predefined: false }
|
||||||
|
],
|
||||||
|
siteRules: [
|
||||||
|
{
|
||||||
|
pattern: "youtube.com",
|
||||||
|
enabled: true,
|
||||||
|
showPopupControlBar: false,
|
||||||
|
popupControllerButtons: ["advance", "settings", "advance"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.getElementById("app-version").textContent).toBe("5.1.7.0");
|
||||||
|
expect(document.getElementById("rememberSpeed").checked).toBe(true);
|
||||||
|
expect(document.getElementById("enabled").checked).toBe(false);
|
||||||
|
expect(document.querySelector('.shortcut-row[data-action="pause"]')).not.toBe(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
expect(document.getElementById("siteRulesContainer").children.length).toBe(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
expect(globalThis.getPopupControlBarOrder()).toEqual(["rewind", "advance"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates site rule regexes before saving", async () => {
|
||||||
|
const chrome = await setupOptions();
|
||||||
|
chrome.storage.sync.set.mockClear();
|
||||||
|
globalThis.createSiteRule(null);
|
||||||
|
const rule = document.querySelector(".site-rule");
|
||||||
|
rule.querySelector(".site-pattern").value = "/(/";
|
||||||
|
|
||||||
|
globalThis.save_options();
|
||||||
|
|
||||||
|
expect(document.getElementById("status").textContent).toContain(
|
||||||
|
"Invalid site rule regex"
|
||||||
|
);
|
||||||
|
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a more-menu trigger for collapsed site rules and a collapse trigger when open", async () => {
|
||||||
|
await setupOptions({ sync: { siteRules: [] } });
|
||||||
|
|
||||||
|
globalThis.createSiteRule({ pattern: "youtube.com" });
|
||||||
|
|
||||||
|
const rule = document.getElementById("siteRulesContainer").lastElementChild;
|
||||||
|
const toggle = rule.querySelector(".toggle-site-rule");
|
||||||
|
const body = rule.querySelector(".site-rule-body");
|
||||||
|
|
||||||
|
expect(rule.classList.contains("collapsed")).toBe(true);
|
||||||
|
expect(body.style.display).toBe("none");
|
||||||
|
expect(toggle.getAttribute("aria-expanded")).toBe("false");
|
||||||
|
expect(toggle.getAttribute("aria-label")).toBe("Expand site rule");
|
||||||
|
expect(toggle.querySelector("svg")).not.toBeNull();
|
||||||
|
|
||||||
|
globalThis.setSiteRuleExpandedState(rule, true);
|
||||||
|
|
||||||
|
expect(rule.classList.contains("collapsed")).toBe(false);
|
||||||
|
expect(body.style.display).toBe("block");
|
||||||
|
expect(toggle.getAttribute("aria-expanded")).toBe("true");
|
||||||
|
expect(toggle.getAttribute("aria-label")).toBe("Collapse site rule");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps site override settings visible but disabled until enabled", async () => {
|
||||||
|
await setupOptions({ sync: { siteRules: [] } });
|
||||||
|
|
||||||
|
globalThis.createSiteRule({ pattern: "youtube.com" });
|
||||||
|
|
||||||
|
const rule = document.getElementById("siteRulesContainer").lastElementChild;
|
||||||
|
const playbackOverride = rule.querySelector(".override-playback");
|
||||||
|
const playbackContainer = rule.querySelector(".site-playback-container");
|
||||||
|
const rememberSpeed = rule.querySelector(".site-rememberSpeed");
|
||||||
|
|
||||||
|
expect(playbackContainer.classList.contains("site-override-disabled")).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(rememberSpeed.disabled).toBe(true);
|
||||||
|
|
||||||
|
playbackOverride.checked = true;
|
||||||
|
playbackOverride.dispatchEvent(
|
||||||
|
new Event("change", {
|
||||||
|
bubbles: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(playbackContainer.classList.contains("site-override-disabled")).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(rememberSpeed.disabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("saves normalized settings and sanitized popup/site-rule controls", async () => {
|
||||||
|
const chrome = await setupOptions();
|
||||||
|
|
||||||
|
document.getElementById("rememberSpeed").checked = true;
|
||||||
|
document.getElementById("hideWithControlsTimer").value = "20";
|
||||||
|
document.getElementById("controllerOpacity").value = "0";
|
||||||
|
document.getElementById("controllerMarginTop").value = "250";
|
||||||
|
document.getElementById("controllerMarginBottom").value = "-4";
|
||||||
|
document.getElementById("enableSubtitleNudge").checked = true;
|
||||||
|
document.getElementById("subtitleNudgeInterval").value = "5";
|
||||||
|
document.getElementById("popupMatchHoverControls").checked = false;
|
||||||
|
document.getElementById("showPopupControlBar").checked = false;
|
||||||
|
|
||||||
|
globalThis.populatePopupControlBarEditor([
|
||||||
|
"rewind",
|
||||||
|
"settings",
|
||||||
|
"faster",
|
||||||
|
"faster"
|
||||||
|
]);
|
||||||
|
|
||||||
|
globalThis.createSiteRule(null);
|
||||||
|
const rule = document.querySelector(".site-rule");
|
||||||
|
rule.querySelector(".site-pattern").value = "youtube.com";
|
||||||
|
rule.querySelector(".override-playback").checked = true;
|
||||||
|
rule.querySelector(".site-rememberSpeed").checked = true;
|
||||||
|
rule.querySelector(".override-opacity").checked = true;
|
||||||
|
rule.querySelector(".site-controllerOpacity").value = "0";
|
||||||
|
rule.querySelector(".override-popup-controlbar").checked = true;
|
||||||
|
rule.querySelector(".site-showPopupControlBar").checked = false;
|
||||||
|
globalThis.populateControlBarZones(
|
||||||
|
rule.querySelector(".site-popup-cb-active"),
|
||||||
|
rule.querySelector(".site-popup-cb-available"),
|
||||||
|
["advance", "settings", "advance"],
|
||||||
|
function (id) {
|
||||||
|
return id !== "settings";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
globalThis.save_options();
|
||||||
|
|
||||||
|
expect(chrome.storage.sync.remove).toHaveBeenCalled();
|
||||||
|
const savedSettings =
|
||||||
|
chrome.storage.sync.set.mock.calls[
|
||||||
|
chrome.storage.sync.set.mock.calls.length - 1
|
||||||
|
][0];
|
||||||
|
|
||||||
|
expect(savedSettings.rememberSpeed).toBe(true);
|
||||||
|
expect(savedSettings.hideWithControlsTimer).toBe(15);
|
||||||
|
expect(savedSettings.controllerOpacity).toBe(0);
|
||||||
|
expect(savedSettings.controllerMarginTop).toBe(200);
|
||||||
|
expect(savedSettings.controllerMarginBottom).toBe(0);
|
||||||
|
expect(savedSettings.subtitleNudgeInterval).toBe(10);
|
||||||
|
expect(savedSettings.showPopupControlBar).toBe(false);
|
||||||
|
expect(savedSettings.popupMatchHoverControls).toBe(false);
|
||||||
|
expect(savedSettings.popupControllerButtons).toEqual(["rewind", "faster"]);
|
||||||
|
expect(savedSettings.siteRules).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
pattern: "youtube.com",
|
||||||
|
rememberSpeed: true,
|
||||||
|
controllerOpacity: 0,
|
||||||
|
showPopupControlBar: false,
|
||||||
|
popupControllerButtons: ["advance"]
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
const { afterEach, beforeEach, describe, expect, it, vi } = require("vitest");
|
||||||
|
const {
|
||||||
|
createChromeMock,
|
||||||
|
evaluateScript,
|
||||||
|
fireDOMContentLoaded,
|
||||||
|
flushAsyncWork,
|
||||||
|
installCommonWindowMocks,
|
||||||
|
loadHtmlFile
|
||||||
|
} = require("./helpers/extension-test-utils");
|
||||||
|
|
||||||
|
function bootOptions(options) {
|
||||||
|
const config = options || {};
|
||||||
|
|
||||||
|
loadHtmlFile("options.html");
|
||||||
|
installCommonWindowMocks();
|
||||||
|
|
||||||
|
const chrome = createChromeMock({
|
||||||
|
manifest: { version: "5.1.7.0" },
|
||||||
|
syncData: config.syncData,
|
||||||
|
localData: config.localData
|
||||||
|
});
|
||||||
|
|
||||||
|
global.chrome = chrome;
|
||||||
|
window.chrome = chrome;
|
||||||
|
global.fetch = vi.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({}),
|
||||||
|
text: () => Promise.resolve("<svg></svg>")
|
||||||
|
})
|
||||||
|
);
|
||||||
|
window.fetch = global.fetch;
|
||||||
|
|
||||||
|
evaluateScript("ui-icons.js");
|
||||||
|
evaluateScript("lucide-client.js");
|
||||||
|
evaluateScript("options.js");
|
||||||
|
fireDOMContentLoaded();
|
||||||
|
|
||||||
|
return chrome;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("options.js", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
delete global.chrome;
|
||||||
|
delete global.fetch;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("restores saved settings, bindings, site rules, and popup bar order", async () => {
|
||||||
|
bootOptions({
|
||||||
|
syncData: {
|
||||||
|
rememberSpeed: true,
|
||||||
|
forceLastSavedSpeed: true,
|
||||||
|
controllerLocation: "middle-right",
|
||||||
|
controllerOpacity: 0.75,
|
||||||
|
controllerMarginTop: 22,
|
||||||
|
controllerMarginBottom: 14,
|
||||||
|
popupMatchHoverControls: false,
|
||||||
|
controllerButtons: ["rewind", "fast", "display"],
|
||||||
|
popupControllerButtons: ["advance", "settings", "rewind", "advance"],
|
||||||
|
keyBindings: [
|
||||||
|
{ action: "display", code: "KeyV", value: 0, predefined: true },
|
||||||
|
{ action: "pause", code: "KeyQ", value: 0, predefined: false }
|
||||||
|
],
|
||||||
|
siteRules: [
|
||||||
|
{
|
||||||
|
pattern: "youtube.com",
|
||||||
|
enabled: false,
|
||||||
|
controllerMarginTop: 12,
|
||||||
|
popupControllerButtons: ["advance", "settings", "rewind"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
|
||||||
|
expect(document.getElementById("app-version").textContent).toBe("5.1.7.0");
|
||||||
|
expect(document.getElementById("rememberSpeed").checked).toBe(true);
|
||||||
|
expect(document.getElementById("forceLastSavedSpeed").checked).toBe(true);
|
||||||
|
expect(document.getElementById("controllerLocation").value).toBe(
|
||||||
|
"middle-right"
|
||||||
|
);
|
||||||
|
expect(document.getElementById("controllerOpacity").value).toBe("0.75");
|
||||||
|
expect(document.getElementById("controllerMarginTop").value).toBe("22");
|
||||||
|
expect(document.getElementById("popupMatchHoverControls").checked).toBe(false);
|
||||||
|
expect(
|
||||||
|
document.getElementById("popupCbEditorWrap").classList.contains(
|
||||||
|
"cb-editor-disabled"
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
const popupButtons = Array.from(
|
||||||
|
document.querySelectorAll("#popupControlBarActive .cb-block")
|
||||||
|
).map((block) => block.dataset.buttonId);
|
||||||
|
expect(popupButtons).toEqual(["advance", "rewind"]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
document.querySelector('.shortcut-row.customs[data-action="pause"]')
|
||||||
|
).not.toBeNull();
|
||||||
|
expect(document.querySelectorAll(".site-rule")).toHaveLength(1);
|
||||||
|
expect(document.querySelector(".site-rule .site-enabled").checked).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("saves normalized settings and site rule overrides", async () => {
|
||||||
|
const chrome = bootOptions();
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
|
||||||
|
document.getElementById("rememberSpeed").checked = true;
|
||||||
|
document.getElementById("hideWithControlsTimer").value = "99";
|
||||||
|
document.getElementById("controllerLocation").value = "bottom-left";
|
||||||
|
document.getElementById("controllerOpacity").value = "0.65";
|
||||||
|
document.getElementById("controllerMarginTop").value = "250";
|
||||||
|
document.getElementById("controllerMarginBottom").value = "-5";
|
||||||
|
document.getElementById("popupMatchHoverControls").checked = false;
|
||||||
|
document.getElementById("showPopupControlBar").checked = true;
|
||||||
|
|
||||||
|
window.populatePopupControlBarEditor(["advance", "settings", "rewind"]);
|
||||||
|
|
||||||
|
window.createSiteRule({ pattern: "youtube.com" });
|
||||||
|
const ruleEl = document.querySelector(".site-rule");
|
||||||
|
ruleEl.querySelector(".override-placement").checked = true;
|
||||||
|
ruleEl.querySelector(".site-controllerLocation").value = "top-right";
|
||||||
|
ruleEl.querySelector(".site-controllerMarginTop").value = "300";
|
||||||
|
ruleEl.querySelector(".site-controllerMarginBottom").value = "-10";
|
||||||
|
|
||||||
|
ruleEl.querySelector(".override-autohide").checked = true;
|
||||||
|
ruleEl.querySelector(".site-hideWithControls").checked = true;
|
||||||
|
ruleEl.querySelector(".site-hideWithControlsTimer").value = "0";
|
||||||
|
|
||||||
|
ruleEl.querySelector(".override-popup-controlbar").checked = true;
|
||||||
|
ruleEl.querySelector(".site-showPopupControlBar").checked = false;
|
||||||
|
window.populateControlBarZones(
|
||||||
|
ruleEl.querySelector(".site-popup-cb-active"),
|
||||||
|
ruleEl.querySelector(".site-popup-cb-available"),
|
||||||
|
["advance", "settings", "rewind"],
|
||||||
|
function (id) {
|
||||||
|
return id !== "settings";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
window.save_options();
|
||||||
|
|
||||||
|
expect(chrome.storage.sync.remove).toHaveBeenCalledWith(
|
||||||
|
[
|
||||||
|
"resetSpeed",
|
||||||
|
"speedStep",
|
||||||
|
"fastSpeed",
|
||||||
|
"rewindTime",
|
||||||
|
"advanceTime",
|
||||||
|
"resetKeyCode",
|
||||||
|
"slowerKeyCode",
|
||||||
|
"fasterKeyCode",
|
||||||
|
"rewindKeyCode",
|
||||||
|
"advanceKeyCode",
|
||||||
|
"fastKeyCode",
|
||||||
|
"blacklist"
|
||||||
|
],
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
|
||||||
|
const savedSettings = chrome.storage.sync.set.mock.calls.at(-1)[0];
|
||||||
|
expect(savedSettings.rememberSpeed).toBe(true);
|
||||||
|
expect(savedSettings.hideWithControlsTimer).toBe(15);
|
||||||
|
expect(savedSettings.controllerLocation).toBe("bottom-left");
|
||||||
|
expect(savedSettings.controllerMarginTop).toBe(200);
|
||||||
|
expect(savedSettings.controllerMarginBottom).toBe(0);
|
||||||
|
expect(savedSettings.popupControllerButtons).toEqual(["advance", "rewind"]);
|
||||||
|
expect(savedSettings.siteRules).toEqual([
|
||||||
|
{
|
||||||
|
pattern: "youtube.com",
|
||||||
|
enabled: true,
|
||||||
|
controllerLocation: "top-right",
|
||||||
|
controllerMarginTop: 200,
|
||||||
|
controllerMarginBottom: 0,
|
||||||
|
hideWithControls: true,
|
||||||
|
hideWithControlsTimer: 0.1,
|
||||||
|
showPopupControlBar: false,
|
||||||
|
popupControllerButtons: ["advance", "rewind"]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks save when a site rule regex is invalid", async () => {
|
||||||
|
const chrome = bootOptions();
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
|
||||||
|
window.createSiteRule({ pattern: "/[abc/" });
|
||||||
|
window.save_options();
|
||||||
|
|
||||||
|
expect(document.getElementById("status").textContent).toContain(
|
||||||
|
"Invalid site rule regex"
|
||||||
|
);
|
||||||
|
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds shortcuts from the selector and records key input states", async () => {
|
||||||
|
bootOptions();
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
|
||||||
|
const selector = document.getElementById("addShortcutSelector");
|
||||||
|
selector.value = "pause";
|
||||||
|
selector.dispatchEvent(new window.Event("change", { bubbles: true }));
|
||||||
|
|
||||||
|
const row = document.querySelector('.shortcut-row.customs[data-action="pause"]');
|
||||||
|
expect(row).not.toBeNull();
|
||||||
|
|
||||||
|
const keyInput = row.querySelector(".customKey");
|
||||||
|
keyInput.dispatchEvent(
|
||||||
|
new window.KeyboardEvent("keydown", {
|
||||||
|
key: "q",
|
||||||
|
code: "KeyQ",
|
||||||
|
bubbles: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(keyInput.vscBinding.code).toBe("KeyQ");
|
||||||
|
expect(keyInput.value).toBe("Q");
|
||||||
|
|
||||||
|
keyInput.dispatchEvent(
|
||||||
|
new window.KeyboardEvent("keydown", {
|
||||||
|
key: "Escape",
|
||||||
|
code: "Escape",
|
||||||
|
bubbles: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(keyInput.vscBinding.disabled).toBe(true);
|
||||||
|
expect(selector.disabled).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import {
|
||||||
|
createChromeMock,
|
||||||
|
flushAsyncWork,
|
||||||
|
loadHtml,
|
||||||
|
loadScript,
|
||||||
|
triggerDomContentLoaded
|
||||||
|
} from "./helpers/browser.js";
|
||||||
|
|
||||||
|
async function setupPopup(overrides = {}) {
|
||||||
|
loadHtml("popup.html");
|
||||||
|
globalThis.chrome = createChromeMock(overrides);
|
||||||
|
window.chrome = globalThis.chrome;
|
||||||
|
loadScript("shared/site-rules.js");
|
||||||
|
loadScript("shared/popup-controls.js");
|
||||||
|
loadScript("ui-icons.js");
|
||||||
|
loadScript("popup.js");
|
||||||
|
triggerDomContentLoaded();
|
||||||
|
await flushAsyncWork();
|
||||||
|
return globalThis.chrome;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("popup UI", () => {
|
||||||
|
it("renders version, builds controls, and prefers the active frame speed", async () => {
|
||||||
|
await setupPopup({
|
||||||
|
manifestVersion: "5.1.7.0",
|
||||||
|
executeScriptResponse: [
|
||||||
|
{ speed: 1.1, preferred: false },
|
||||||
|
{ speed: 1.75, preferred: true }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.getElementById("app-version").innerText).toBe("5.1.7.0");
|
||||||
|
expect(document.getElementById("popupSpeed").textContent).toBe("1.75");
|
||||||
|
expect(
|
||||||
|
document.querySelectorAll("#popupControlBar button").length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows disabled state for a matching site rule", async () => {
|
||||||
|
await setupPopup({
|
||||||
|
sync: {
|
||||||
|
enabled: true,
|
||||||
|
siteRules: [{ pattern: "example.com", enabled: false }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.getElementById("status").innerText).toBe(
|
||||||
|
"Speeder is disabled for this site."
|
||||||
|
);
|
||||||
|
expect(document.getElementById("popupSpeed").textContent).toBe("1.00");
|
||||||
|
expect(document.getElementById("popupControlBar").style.display).toBe(
|
||||||
|
"none"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles enabled state and updates the browser action icons", async () => {
|
||||||
|
const chrome = await setupPopup();
|
||||||
|
chrome.storage.sync.set.mockClear();
|
||||||
|
chrome.browserAction.setIcon.mockClear();
|
||||||
|
|
||||||
|
document.getElementById("disable").click();
|
||||||
|
|
||||||
|
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
|
||||||
|
{ enabled: false },
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(document.getElementById("enable").classList.contains("hide")).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(chrome.browserAction.setIcon).toHaveBeenCalledWith({
|
||||||
|
path: {
|
||||||
|
19: "icons/icon19_disabled.png",
|
||||||
|
38: "icons/icon38_disabled.png",
|
||||||
|
48: "icons/icon48_disabled.png"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles refresh responses for unsupported and successful pages", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const chrome = await setupPopup();
|
||||||
|
let response = null;
|
||||||
|
chrome.tabs.sendMessage.mockImplementation((tabId, message, callback) => {
|
||||||
|
if (message.action === "rescan_page") {
|
||||||
|
callback(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback({ speed: 1.25 });
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("refresh").click();
|
||||||
|
expect(document.getElementById("status").innerText).toBe(
|
||||||
|
"Cannot run on this page."
|
||||||
|
);
|
||||||
|
|
||||||
|
response = { status: "complete" };
|
||||||
|
document.getElementById("refresh").click();
|
||||||
|
expect(document.getElementById("status").innerText).toBe(
|
||||||
|
"Scan complete. Closing..."
|
||||||
|
);
|
||||||
|
vi.advanceTimersByTime(500);
|
||||||
|
expect(window.close).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dispatches popup control bar actions back to the active tab", async () => {
|
||||||
|
const chrome = await setupPopup();
|
||||||
|
chrome.tabs.sendMessage.mockClear();
|
||||||
|
chrome.tabs.executeScript.mockClear();
|
||||||
|
|
||||||
|
document.querySelector("#popupControlBar button").click();
|
||||||
|
|
||||||
|
expect(chrome.tabs.sendMessage).toHaveBeenCalledWith(
|
||||||
|
1,
|
||||||
|
expect.objectContaining({
|
||||||
|
action: "run_action"
|
||||||
|
}),
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(chrome.tabs.executeScript).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
const { afterEach, beforeEach, describe, expect, it, vi } = require("vitest");
|
||||||
|
const {
|
||||||
|
createChromeMock,
|
||||||
|
evaluateScript,
|
||||||
|
fireDOMContentLoaded,
|
||||||
|
flushAsyncWork,
|
||||||
|
installCommonWindowMocks,
|
||||||
|
loadHtmlFile
|
||||||
|
} = require("./helpers/extension-test-utils");
|
||||||
|
|
||||||
|
function bootPopup(options) {
|
||||||
|
const config = options || {};
|
||||||
|
|
||||||
|
loadHtmlFile("popup.html");
|
||||||
|
installCommonWindowMocks();
|
||||||
|
|
||||||
|
const chrome = createChromeMock({
|
||||||
|
manifest: { version: "9.9.9-test" },
|
||||||
|
syncData: config.syncData,
|
||||||
|
localData: config.localData,
|
||||||
|
tabsQueryResult: [
|
||||||
|
config.activeTab || { id: 99, active: true, url: "https://example.com/" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.tabs.executeScript.mockImplementation(
|
||||||
|
config.executeScriptImpl ||
|
||||||
|
((tabId, details, callback) => {
|
||||||
|
if (callback) callback([{ speed: 1.0, preferred: true }]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
chrome.tabs.sendMessage.mockImplementation(
|
||||||
|
config.sendMessageImpl ||
|
||||||
|
((tabId, message, callback) => {
|
||||||
|
if (message.action === "get_speed") {
|
||||||
|
callback({ speed: 1.0 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (message.action === "rescan_page") {
|
||||||
|
callback({ status: "complete" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback({ speed: 1.0 });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
global.chrome = chrome;
|
||||||
|
window.chrome = chrome;
|
||||||
|
|
||||||
|
evaluateScript("ui-icons.js");
|
||||||
|
evaluateScript("popup.js");
|
||||||
|
fireDOMContentLoaded();
|
||||||
|
|
||||||
|
return chrome;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("popup.js", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
delete global.chrome;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the popup disabled state when a site rule disables Speeder", async () => {
|
||||||
|
bootPopup({
|
||||||
|
syncData: {
|
||||||
|
enabled: true,
|
||||||
|
siteRules: [
|
||||||
|
{
|
||||||
|
pattern: "youtube.com",
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
activeTab: {
|
||||||
|
id: 10,
|
||||||
|
active: true,
|
||||||
|
url: "https://www.youtube.com/watch?v=abc123"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
expect(document.querySelector("#app-version").textContent).toBe("9.9.9-test");
|
||||||
|
expect(document.querySelector("#status").textContent).toContain(
|
||||||
|
"disabled for this site"
|
||||||
|
);
|
||||||
|
expect(document.querySelector("#popupSpeed").textContent).toBe("1.00");
|
||||||
|
expect(document.querySelector("#popupControlBar").style.display).toBe("none");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds sanitized popup buttons and refreshes speed after an action", async () => {
|
||||||
|
const chrome = bootPopup({
|
||||||
|
syncData: {
|
||||||
|
enabled: true,
|
||||||
|
controllerButtons: ["faster", "settings", "rewind", "faster"],
|
||||||
|
popupMatchHoverControls: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.tabs.executeScript
|
||||||
|
.mockImplementationOnce((tabId, details, callback) => {
|
||||||
|
callback([
|
||||||
|
{ speed: 1.25, preferred: false },
|
||||||
|
{ speed: 1.5, preferred: true }
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
.mockImplementationOnce((tabId, details, callback) => {
|
||||||
|
callback([{ speed: 1.75, preferred: true }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.tabs.sendMessage.mockImplementation((tabId, message, callback) => {
|
||||||
|
if (message.action === "run_action") {
|
||||||
|
callback({ speed: 1.75 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback({ speed: 1.0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
document.dispatchEvent(new window.Event("DOMContentLoaded"));
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
const buttons = Array.from(
|
||||||
|
document.querySelectorAll("#popupControlBar button[data-action]")
|
||||||
|
).map((button) => button.dataset.action);
|
||||||
|
expect(buttons).toEqual(["faster", "rewind"]);
|
||||||
|
expect(document.querySelector("#popupSpeed").textContent).toBe("1.50");
|
||||||
|
|
||||||
|
document.querySelector('#popupControlBar button[data-action="faster"]').click();
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
expect(chrome.tabs.sendMessage).toHaveBeenCalledWith(
|
||||||
|
99,
|
||||||
|
{ action: "run_action", actionName: "faster" },
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(document.querySelector("#popupSpeed").textContent).toBe("1.75");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles enablement and closes after a successful refresh", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
const chrome = bootPopup({
|
||||||
|
syncData: {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
expect(document.querySelector("#enable").classList.contains("hide")).toBe(false);
|
||||||
|
expect(document.querySelector("#disable").classList.contains("hide")).toBe(true);
|
||||||
|
|
||||||
|
document.querySelector("#enable").click();
|
||||||
|
expect(chrome.storage.sync.set).toHaveBeenCalledWith(
|
||||||
|
{ enabled: true },
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(chrome.browserAction.setIcon).toHaveBeenCalledWith({
|
||||||
|
path: {
|
||||||
|
19: "icons/icon19.png",
|
||||||
|
38: "icons/icon38.png",
|
||||||
|
48: "icons/icon48.png"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector("#refresh").click();
|
||||||
|
expect(document.querySelector("#status").textContent).toContain("Closing");
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(500);
|
||||||
|
expect(window.close).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { afterEach, beforeEach, vi } from "vitest";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.open = vi.fn();
|
||||||
|
window.close = vi.fn();
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.requestAnimationFrame = (callback) => setTimeout(callback, 0);
|
||||||
|
globalThis.cancelAnimationFrame = (id) => clearTimeout(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
delete globalThis.SpeederShared;
|
||||||
|
delete globalThis.restore_options;
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
|
document.head.innerHTML = "";
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
}
|
||||||
|
delete globalThis.chrome;
|
||||||
|
});
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import controllerUtils from "../shared/controller-utils.js";
|
||||||
|
import importExportUtils from "../shared/import-export.js";
|
||||||
|
import keyBindingUtils from "../shared/key-bindings.js";
|
||||||
|
import popupControls from "../shared/popup-controls.js";
|
||||||
|
import siteRules from "../shared/site-rules.js";
|
||||||
|
|
||||||
|
describe("shared helpers", () => {
|
||||||
|
it("matches site rules and skips invalid regex patterns", () => {
|
||||||
|
const literalRule = { pattern: "example.com/watch" };
|
||||||
|
const regexRule = { pattern: "/youtube\\.com\\/watch/i" };
|
||||||
|
|
||||||
|
expect(
|
||||||
|
siteRules.matchSiteRule("https://example.com/watch?v=1", [literalRule])
|
||||||
|
).toBe(literalRule);
|
||||||
|
expect(
|
||||||
|
siteRules.matchSiteRule("https://www.youtube.com/watch?v=2", [regexRule])
|
||||||
|
).toBe(regexRule);
|
||||||
|
expect(
|
||||||
|
siteRules.matchSiteRule("https://www.youtube.com/shorts/3", [
|
||||||
|
{ pattern: "/(/" },
|
||||||
|
regexRule
|
||||||
|
])
|
||||||
|
).toBeNull();
|
||||||
|
expect(siteRules.isSiteRuleDisabled({ enabled: false })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sanitizes and resolves popup button orders", () => {
|
||||||
|
const controllerButtonDefs = {
|
||||||
|
rewind: {},
|
||||||
|
faster: {},
|
||||||
|
advance: {},
|
||||||
|
display: {},
|
||||||
|
settings: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
popupControls.sanitizeButtonOrder(
|
||||||
|
["rewind", "settings", "rewind", "faster", "missing"],
|
||||||
|
controllerButtonDefs,
|
||||||
|
new Set(["settings"])
|
||||||
|
)
|
||||||
|
).toEqual(["rewind", "faster"]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
popupControls.resolvePopupButtons(
|
||||||
|
{
|
||||||
|
popupMatchHoverControls: true,
|
||||||
|
controllerButtons: ["advance", "display"],
|
||||||
|
popupControllerButtons: ["rewind"]
|
||||||
|
},
|
||||||
|
{ controllerButtons: ["faster", "advance"] },
|
||||||
|
{
|
||||||
|
controllerButtonDefs,
|
||||||
|
defaultButtons: ["rewind", "display"],
|
||||||
|
excludedIds: ["settings"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toEqual(["faster", "advance"]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
popupControls.resolvePopupButtons(
|
||||||
|
{
|
||||||
|
popupMatchHoverControls: false,
|
||||||
|
popupControllerButtons: ["rewind", "display"]
|
||||||
|
},
|
||||||
|
{ popupControllerButtons: ["advance", "settings", "advance"] },
|
||||||
|
{
|
||||||
|
controllerButtonDefs,
|
||||||
|
defaultButtons: ["rewind", "display"],
|
||||||
|
excludedIds: ["settings"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toEqual(["advance"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes controller locations and margins", () => {
|
||||||
|
expect(controllerUtils.normalizeControllerLocation("top-right")).toBe(
|
||||||
|
"top-right"
|
||||||
|
);
|
||||||
|
expect(controllerUtils.normalizeControllerLocation("bogus")).toBe(
|
||||||
|
controllerUtils.defaultControllerLocation
|
||||||
|
);
|
||||||
|
expect(controllerUtils.clampControllerMarginPx(300, 65)).toBe(200);
|
||||||
|
expect(controllerUtils.clampControllerMarginPx(-5, 65)).toBe(0);
|
||||||
|
expect(controllerUtils.getNextControllerLocation("top-left")).toBe(
|
||||||
|
"top-center"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("infers key binding codes from legacy formats", () => {
|
||||||
|
expect(keyBindingUtils.normalizeBindingKey("a")).toBe("A");
|
||||||
|
expect(keyBindingUtils.normalizeBindingKey("Esc")).toBe("Escape");
|
||||||
|
expect(keyBindingUtils.legacyBindingKeyToCode(" ")).toBe("Space");
|
||||||
|
expect(keyBindingUtils.legacyKeyCodeToCode(90)).toBe("KeyZ");
|
||||||
|
expect(keyBindingUtils.inferBindingCode({ key: "x" }, null)).toBe("KeyX");
|
||||||
|
expect(keyBindingUtils.inferBindingCode({ keyCode: 107 }, null)).toBe(
|
||||||
|
"NumpadAdd"
|
||||||
|
);
|
||||||
|
expect(keyBindingUtils.getLegacyKeyCode({ key: 65 })).toBe(65);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds and parses import/export payloads", () => {
|
||||||
|
expect(
|
||||||
|
importExportUtils.generateBackupFilename(new Date(2026, 3, 4, 8, 9, 10))
|
||||||
|
).toBe("speeder-backup_2026-04-04_08.09.10.json");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
importExportUtils.buildBackupPayload(
|
||||||
|
{ rememberSpeed: true },
|
||||||
|
{ customButtonIcons: {} },
|
||||||
|
"2026-04-04T08:09:10Z"
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
version: "1.1",
|
||||||
|
exportDate: "2026-04-04T08:09:10.000Z",
|
||||||
|
settings: { rememberSpeed: true },
|
||||||
|
localSettings: { customButtonIcons: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
importExportUtils.extractImportSettings({
|
||||||
|
settings: { rememberSpeed: true },
|
||||||
|
localSettings: { customButtonIcons: {} }
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
isWrappedBackup: true,
|
||||||
|
settings: { rememberSpeed: true },
|
||||||
|
localSettings: { customButtonIcons: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
importExportUtils.parseImportText(
|
||||||
|
JSON.stringify({ rememberSpeed: true, keyBindings: [] })
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
isWrappedBackup: false,
|
||||||
|
settings: { rememberSpeed: true, keyBindings: [] },
|
||||||
|
localSettings: null
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
importExportUtils.extractImportSettings({ enabled: true })
|
||||||
|
).toEqual({
|
||||||
|
isWrappedBackup: false,
|
||||||
|
settings: { enabled: true },
|
||||||
|
localSettings: null
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(importExportUtils.isRecognizedRawSettingsObject({ wat: true })).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
/**
|
||||||
|
* Inline SVG icons (Lucide-style strokes, compatible with https://lucide.dev — ISC license).
|
||||||
|
* Use stroke="currentColor" so buttons inherit foreground for monochrome UI.
|
||||||
|
*/
|
||||||
|
var VSC_ICON_SIZE_DEFAULT = 18;
|
||||||
|
var VSC_SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
|
/** Inner SVG markup only (paths / shapes inside <svg>). */
|
||||||
|
var vscUiIconPaths = {
|
||||||
|
rewind:
|
||||||
|
'<polygon points="11 19 2 12 11 5 11 19"/><polygon points="22 19 13 12 22 5 22 19"/>',
|
||||||
|
advance:
|
||||||
|
'<polygon points="13 19 22 12 13 5 13 19"/><polygon points="2 19 11 12 2 5 2 19"/>',
|
||||||
|
reset:
|
||||||
|
'<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>',
|
||||||
|
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"/>',
|
||||||
|
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"/>',
|
||||||
|
settings:
|
||||||
|
'<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
|
||||||
|
pause:
|
||||||
|
'<rect x="14" y="4" width="4" height="16" rx="1"/><rect x="6" y="4" width="4" height="16" rx="1"/>',
|
||||||
|
muted:
|
||||||
|
'<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/>',
|
||||||
|
mark: '<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>',
|
||||||
|
jump:
|
||||||
|
'<polyline points="9 10 4 15 9 20"/><path d="M20 4v7a4 4 0 0 1-4 4H4"/>',
|
||||||
|
nudge: '<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>',
|
||||||
|
/** Lucide check — subtitle nudge on */
|
||||||
|
subtitleNudgeOn: '<path d="M20 6 9 17l-5-5"/>',
|
||||||
|
/** Lucide x — subtitle nudge off */
|
||||||
|
subtitleNudgeOff:
|
||||||
|
'<path d="M18 6 6 18"/><path d="m6 6 12 12"/>'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} [size] - width/height in px
|
||||||
|
* @returns {string} full <svg>…</svg>
|
||||||
|
*/
|
||||||
|
function vscIconSvgString(action, size) {
|
||||||
|
var inner = vscUiIconPaths[action];
|
||||||
|
if (!inner) return "";
|
||||||
|
var s = size != null ? size : VSC_ICON_SIZE_DEFAULT;
|
||||||
|
return (
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="' +
|
||||||
|
s +
|
||||||
|
'" height="' +
|
||||||
|
s +
|
||||||
|
'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
|
||||||
|
inner +
|
||||||
|
"</svg>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function vscClearElement(el) {
|
||||||
|
if (!el) return;
|
||||||
|
while (el.firstChild) {
|
||||||
|
el.removeChild(el.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function vscSanitizeSvgTree(svg) {
|
||||||
|
if (!svg || String(svg.tagName).toLowerCase() !== "svg") return null;
|
||||||
|
|
||||||
|
svg.querySelectorAll("script, style, foreignObject").forEach(function (n) {
|
||||||
|
n.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.querySelectorAll("*").forEach(function (el) {
|
||||||
|
for (var i = el.attributes.length - 1; i >= 0; i--) {
|
||||||
|
var attr = el.attributes[i];
|
||||||
|
var name = attr.name.toLowerCase();
|
||||||
|
var val = attr.value;
|
||||||
|
if (name.indexOf("on") === 0) {
|
||||||
|
el.removeAttribute(attr.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(name === "href" || name === "xlink:href") &&
|
||||||
|
/^\s*javascript:/i.test(val)
|
||||||
|
) {
|
||||||
|
el.removeAttribute(attr.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.setAttribute("xmlns", VSC_SVG_NS);
|
||||||
|
svg.setAttribute("aria-hidden", "true");
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function vscCreateSvgNode(doc, svgText) {
|
||||||
|
if (!doc || !svgText || typeof svgText !== "string") return null;
|
||||||
|
var clean = String(svgText).replace(/\0/g, "").trim();
|
||||||
|
if (!clean || !/<svg[\s>]/i.test(clean)) return null;
|
||||||
|
|
||||||
|
var parsed = new DOMParser().parseFromString(clean, "image/svg+xml");
|
||||||
|
if (parsed.querySelector("parsererror")) return null;
|
||||||
|
|
||||||
|
var svg = vscSanitizeSvgTree(parsed.querySelector("svg"));
|
||||||
|
if (!svg) return null;
|
||||||
|
|
||||||
|
return doc.importNode(svg, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function vscSetSvgContent(el, svgText) {
|
||||||
|
if (!el) return false;
|
||||||
|
vscClearElement(el);
|
||||||
|
|
||||||
|
var doc = el.ownerDocument || document;
|
||||||
|
var svg = vscCreateSvgNode(doc, svgText);
|
||||||
|
if (!svg) return false;
|
||||||
|
|
||||||
|
el.appendChild(svg);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function vscCreateSvgWrap(doc, svgText, className) {
|
||||||
|
if (!doc) return null;
|
||||||
|
var span = doc.createElement("span");
|
||||||
|
span.className = className || "vsc-btn-icon";
|
||||||
|
if (!vscSetSvgContent(span, svgText)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Document} doc
|
||||||
|
* @param {string} action
|
||||||
|
* @returns {HTMLElement|null} wrapper span containing svg, or null if no icon
|
||||||
|
*/
|
||||||
|
function vscIconWrap(doc, action, size) {
|
||||||
|
var html = vscIconSvgString(action, size);
|
||||||
|
if (!html) return null;
|
||||||
|
return vscCreateSvgWrap(doc, html, "vsc-btn-icon");
|
||||||
|
}
|
||||||
@@ -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