mirror of
https://github.com/SoPat712/Speeder.git
synced 2026-08-21 20:32:07 -04:00
Compare commits
46
Commits
v6.0.2-beta.1
...
beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5d697b7ea
|
||
|
|
7c9259e5ea
|
||
|
|
0872b66686
|
||
|
|
eede171ad9
|
||
|
|
15673b0a38
|
||
|
|
06b23b3a53
|
||
|
|
807bd0a29d
|
||
|
|
6d17f15bc0
|
||
|
|
8073014bce
|
||
|
|
f8cfbf05c8
|
||
|
|
b4234e0387
|
||
|
|
d55006dd0a
|
||
|
|
745af2f1b9
|
||
|
|
7e9ce436d3
|
||
|
|
7e3054f6fa
|
||
|
|
98546adf41
|
||
|
|
daeff63d2a
|
||
|
|
80aa00670c
|
||
|
|
aa4f1d15e7
|
||
|
|
5af0200008
|
||
|
|
1d9f6f7e13
|
||
|
|
0f64fd5ab1
|
||
|
|
e555c4c116
|
||
|
|
91d530e29b
|
||
|
|
34f2561b50
|
||
|
|
fd29883d84
|
||
|
|
e083fdcafe
|
||
|
|
2fa7d8963a
|
||
|
|
e4edf10440
|
||
|
|
d5c2d0e343
|
||
|
|
5cd559b883
|
||
|
|
0c9a6a1a1b
|
||
|
|
3ee970a54b
|
||
|
|
e12375f2d2
|
||
|
|
8d7d31a4db
|
||
|
|
ed21281fa3
|
||
|
|
13350e5cea
|
||
|
|
39b7ba27fb
|
||
|
|
ee346ee9d8
|
||
|
|
461c8c448f
|
||
|
|
bff65230bb
|
||
|
|
0afb56abe0
|
||
|
|
a02b7b57f7
|
||
|
|
39c1c76db1
|
||
|
|
9b6d1ccd3c
|
||
|
|
a2d7f0d9d6
|
@@ -12,10 +12,21 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Install deps
|
- uses: actions/setup-node@v7
|
||||||
run: npm install -g web-ext
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install project dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
- name: Install web-ext
|
||||||
|
run: npm install -g web-ext@10.6.0
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: web-ext lint --source-dir extension
|
run: web-ext lint --source-dir extension
|
||||||
@@ -43,7 +54,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create GitHub Prerelease (beta)
|
- name: Create GitHub Prerelease (beta)
|
||||||
if: contains(github.ref_name, '-beta')
|
if: contains(github.ref_name, '-beta')
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v3
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ github.ref_name }}
|
tag_name: ${{ github.ref_name }}
|
||||||
name: ${{ github.ref_name }}
|
name: ${{ github.ref_name }}
|
||||||
|
|||||||
@@ -56,16 +56,14 @@ settings page, as well as add additional shortcut keys to match your
|
|||||||
preferences. For example, you can assign multiple different "preferred speed"
|
preferences. For example, you can assign multiple different "preferred speed"
|
||||||
shortcuts with different values, which will allow you to quickly toggle between
|
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 choose an action from "Add shortcut…".
|
||||||
|
|
||||||
<img width="1760" height="1330" alt="image" src="https://github.com/user-attachments/assets/32e814dd-93ea-4943-8ec9-3eca735447ac" />
|
<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
|
You can record `Shift+<shortcut>` as a separate, exact binding, or use a site
|
||||||
listens both for lower and upper case values (i.e. you can use
|
rule to block the site from capturing a particular Speeder shortcut. Shift is
|
||||||
`Shift-<shortcut>`) if there is other functionality assigned to the lowercase
|
not applied automatically to an unshifted binding.
|
||||||
key. This is not a perfect solution, as some sites may listen to both, but works
|
|
||||||
most of the time.
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -74,19 +72,17 @@ The unpacked extension root is `extension/`. Load that directory in
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm test
|
npm test
|
||||||
npx --yes web-ext lint --source-dir extension
|
npx --yes web-ext@10.6.0 lint --source-dir extension
|
||||||
```
|
```
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
### The video controls are not showing up?
|
### The video controls are not showing up?
|
||||||
|
|
||||||
This extension is only compatible
|
Speeder works with HTML5 video and, when enabled in settings, HTML5 audio. Check
|
||||||
with HTML5 video. If you don't see the controls showing up, chances are you are
|
that Speeder is enabled for the current site, then use the popup's "Rescan page
|
||||||
viewing a Flash video. If you want to confirm, try right-clicking on the video
|
for videos" action after a player loads dynamically. Browser-internal pages and
|
||||||
and inspect the menu: if it mentions flash, then that's the issue. That said,
|
players that do not expose HTML5 media to extensions cannot be controlled.
|
||||||
most sites will fallback to HTML5 if they detect that Flash is not available.
|
|
||||||
You can try manually disabling Flash from the browser.
|
|
||||||
|
|
||||||
### What is this fork all about?
|
### What is this fork all about?
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,67 @@
|
|||||||
chrome.runtime.onMessage.addListener(function (request) {
|
function setToolbarIcon(enabled) {
|
||||||
if (request.action === "openOptions") {
|
var suffix = enabled === false ? "_disabled" : "";
|
||||||
chrome.tabs.create({ url: chrome.runtime.getURL("options/options.html") });
|
chrome.browserAction.setIcon({
|
||||||
|
path: {
|
||||||
|
19: "assets/icons/icon19" + suffix + ".png",
|
||||||
|
38: "assets/icons/icon38" + suffix + ".png",
|
||||||
|
48: "assets/icons/icon48" + suffix + ".png"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var pausedTabIds = new Set();
|
||||||
|
|
||||||
|
function getMessageTabId(request, sender) {
|
||||||
|
if (request && Number.isInteger(request.tabId)) return request.tabId;
|
||||||
|
return sender && sender.tab && Number.isInteger(sender.tab.id)
|
||||||
|
? sender.tab.id
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.storage.sync.get(["enabled"], function(storage) {
|
||||||
|
if (!chrome.runtime.lastError) setToolbarIcon(storage.enabled !== false);
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.storage.onChanged.addListener(function(changes, areaName) {
|
||||||
|
if (areaName === "sync" && changes.enabled) {
|
||||||
|
setToolbarIcon(changes.enabled.newValue !== false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
|
||||||
|
if (request.action === "openOptions") {
|
||||||
|
chrome.tabs.create({ url: chrome.runtime.getURL("options/options.html") });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (request.action === "get_tab_pause_state") {
|
||||||
|
var queriedTabId = getMessageTabId(request, sender);
|
||||||
|
sendResponse({
|
||||||
|
paused: queriedTabId !== null && pausedTabIds.has(queriedTabId)
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (request.action === "set_tab_paused") {
|
||||||
|
var tabId = getMessageTabId(request, sender);
|
||||||
|
if (tabId === null) {
|
||||||
|
sendResponse({ paused: false });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var paused = request.paused === true;
|
||||||
|
if (paused) pausedTabIds.add(tabId);
|
||||||
|
else pausedTabIds.delete(tabId);
|
||||||
|
chrome.tabs.sendMessage(
|
||||||
|
tabId,
|
||||||
|
{ action: "set_tab_paused", paused: paused },
|
||||||
|
function () {
|
||||||
|
void chrome.runtime.lastError;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
sendResponse({ paused: paused });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.tabs.onRemoved.addListener(function(tabId) {
|
||||||
|
pausedTabIds.delete(tabId);
|
||||||
|
});
|
||||||
|
|||||||
@@ -8,6 +8,14 @@
|
|||||||
if (!v) return null;
|
if (!v) return null;
|
||||||
return {
|
return {
|
||||||
speed: v.playbackRate,
|
speed: v.playbackRate,
|
||||||
|
frameToken:
|
||||||
|
typeof tc === "object" && typeof tc.frameToken === "string"
|
||||||
|
? tc.frameToken
|
||||||
|
: null,
|
||||||
|
diagnostics:
|
||||||
|
typeof getDiagnosticsSnapshot === "function"
|
||||||
|
? getDiagnosticsSnapshot(v)
|
||||||
|
: null,
|
||||||
preferred: !v.paused,
|
preferred: !v.paused,
|
||||||
forceLastSavedSpeed: Boolean(
|
forceLastSavedSpeed: Boolean(
|
||||||
typeof tc === "object" && tc.settings && tc.settings.forceLastSavedSpeed
|
typeof tc === "object" && tc.settings && tc.settings.forceLastSavedSpeed
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
/* Base styles for the controller wrapper (the shadow host) */
|
/* Base styles for the controller wrapper (the shadow host) */
|
||||||
.vsc-controller {
|
.vsc-controller {
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
/* Keep the interactive controller above player-owned click/pause panes. */
|
/* Keep the interactive controller above player-owned click/pause panes. */
|
||||||
z-index: 2147483647 !important;
|
z-index: 2147483647 !important;
|
||||||
@@ -17,7 +13,6 @@
|
|||||||
controller in the browser top layer without nesting it inside <video>. */
|
controller in the browser top layer without nesting it inside <video>. */
|
||||||
.vsc-controller.vsc-fullscreen-popover {
|
.vsc-controller.vsc-fullscreen-popover {
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
inset: auto !important;
|
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
|
|||||||
+287
-131
@@ -1,4 +1,3 @@
|
|||||||
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 =
|
var speederShared =
|
||||||
typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
|
typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
|
||||||
@@ -28,6 +27,11 @@ function getSharedDefault(key, fallback) {
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCachedVideoRect(video) {
|
||||||
|
var wrapper = video && video.vsc && video.vsc.div;
|
||||||
|
return (wrapper && wrapper.vscVideoRect) || null;
|
||||||
|
}
|
||||||
|
|
||||||
function getPrimaryVideoElement(mediaElements) {
|
function getPrimaryVideoElement(mediaElements) {
|
||||||
var candidates = Array.isArray(mediaElements)
|
var candidates = Array.isArray(mediaElements)
|
||||||
? mediaElements
|
? mediaElements
|
||||||
@@ -39,10 +43,13 @@ function getPrimaryVideoElement(mediaElements) {
|
|||||||
candidates.forEach(function(el, index) {
|
candidates.forEach(function(el, index) {
|
||||||
if (!el || !el.vsc || !el.isConnected) return;
|
if (!el || !el.vsc || !el.isConnected) return;
|
||||||
|
|
||||||
var rect = null;
|
var rect = getCachedVideoRect(el);
|
||||||
try {
|
var hasCachedRect = Boolean(rect);
|
||||||
rect = el.getBoundingClientRect();
|
if (!rect) {
|
||||||
} catch (_error) {}
|
try {
|
||||||
|
rect = el.getBoundingClientRect();
|
||||||
|
} catch (_error) {}
|
||||||
|
}
|
||||||
|
|
||||||
var width = rect && Number(rect.width) > 0 ? Number(rect.width) : 0;
|
var width = rect && Number(rect.width) > 0 ? Number(rect.width) : 0;
|
||||||
var height = rect && Number(rect.height) > 0 ? Number(rect.height) : 0;
|
var height = rect && Number(rect.height) > 0 ? Number(rect.height) : 0;
|
||||||
@@ -57,17 +64,24 @@ function getPrimaryVideoElement(mediaElements) {
|
|||||||
: 0;
|
: 0;
|
||||||
var visibleArea = visibleWidth * visibleHeight;
|
var visibleArea = visibleWidth * visibleHeight;
|
||||||
var visuallyAvailable = visibleArea > 0;
|
var visuallyAvailable = visibleArea > 0;
|
||||||
try {
|
if (
|
||||||
var computed = win && win.getComputedStyle(el);
|
el.vsc.div &&
|
||||||
if (
|
el.vsc.div.classList.contains("vsc-geometry-hidden")
|
||||||
computed &&
|
) {
|
||||||
(computed.display === "none" ||
|
visuallyAvailable = false;
|
||||||
computed.visibility === "hidden" ||
|
} else if (!hasCachedRect) {
|
||||||
Number(computed.opacity) === 0)
|
try {
|
||||||
) {
|
var computed = win && win.getComputedStyle(el);
|
||||||
visuallyAvailable = false;
|
if (
|
||||||
}
|
computed &&
|
||||||
} catch (_error) {}
|
(computed.display === "none" ||
|
||||||
|
computed.visibility === "hidden" ||
|
||||||
|
Number(computed.opacity) === 0)
|
||||||
|
) {
|
||||||
|
visuallyAvailable = false;
|
||||||
|
}
|
||||||
|
} catch (_error) {}
|
||||||
|
}
|
||||||
|
|
||||||
var score = visibleArea;
|
var score = visibleArea;
|
||||||
if (visuallyAvailable) score += 1e12;
|
if (visuallyAvailable) score += 1e12;
|
||||||
@@ -90,6 +104,81 @@ function getPrimaryVideoElement(mediaElements) {
|
|||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDiagnosticsSnapshot(media) {
|
||||||
|
var primary = media || getPrimaryVideoElement();
|
||||||
|
if (!primary) return null;
|
||||||
|
|
||||||
|
var controller = primary.vsc || null;
|
||||||
|
var wrapper = controller && controller.div;
|
||||||
|
var fullscreenElement = getFullscreenElement(primary.ownerDocument);
|
||||||
|
var rect = null;
|
||||||
|
try {
|
||||||
|
rect = primary.getBoundingClientRect();
|
||||||
|
} catch (_error) {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
mediaCount: tc.mediaElements.filter(function(item) {
|
||||||
|
return item && item.isConnected;
|
||||||
|
}).length,
|
||||||
|
mediaType: String(primary.nodeName || "media").toLowerCase(),
|
||||||
|
playbackRate: Number(primary.playbackRate),
|
||||||
|
paused: primary.paused === true,
|
||||||
|
ended: primary.ended === true,
|
||||||
|
readyState: Number(primary.readyState) || 0,
|
||||||
|
muted: primary.muted === true,
|
||||||
|
volume: Number(primary.volume),
|
||||||
|
dimensions: rect
|
||||||
|
? [Math.round(Number(rect.width) || 0), Math.round(Number(rect.height) || 0)]
|
||||||
|
: [0, 0],
|
||||||
|
fullscreen: {
|
||||||
|
active: Boolean(fullscreenElement),
|
||||||
|
element: fullscreenElement
|
||||||
|
? String(fullscreenElement.nodeName || "element").toLowerCase()
|
||||||
|
: null,
|
||||||
|
ownsMedia: Boolean(
|
||||||
|
fullscreenElement &&
|
||||||
|
(fullscreenElement === primary ||
|
||||||
|
isComposedDescendant(primary, fullscreenElement))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
controller: {
|
||||||
|
present: Boolean(controller && wrapper),
|
||||||
|
connected: Boolean(wrapper && wrapper.isConnected),
|
||||||
|
hidden: Boolean(wrapper && wrapper.classList.contains("vsc-hidden")),
|
||||||
|
geometryHidden: Boolean(
|
||||||
|
wrapper && wrapper.classList.contains("vsc-geometry-hidden")
|
||||||
|
),
|
||||||
|
fullscreenPopover: Boolean(
|
||||||
|
wrapper && wrapper.classList.contains("vsc-fullscreen-popover")
|
||||||
|
),
|
||||||
|
location: controller ? controller.controllerLocation : null
|
||||||
|
},
|
||||||
|
effectiveSettings: {
|
||||||
|
enabled: tc.settings.enabled !== false,
|
||||||
|
tabPaused: tc.tabPaused === true,
|
||||||
|
startHidden: tc.settings.startHidden === true,
|
||||||
|
hideWithControls: tc.settings.hideWithControls === true,
|
||||||
|
rememberSpeed: tc.settings.rememberSpeed === true,
|
||||||
|
forceLastSavedSpeed: tc.settings.forceLastSavedSpeed === true,
|
||||||
|
shortcutTargetMode: tc.settings.shortcutTargetMode
|
||||||
|
},
|
||||||
|
siteRule: {
|
||||||
|
matched: Boolean(tc.activeSiteRule),
|
||||||
|
disabled: Boolean(
|
||||||
|
tc.activeSiteRule &&
|
||||||
|
siteRuleUtils.isSiteRuleDisabled(tc.activeSiteRule)
|
||||||
|
),
|
||||||
|
overrideKeys: tc.activeSiteRule
|
||||||
|
? Object.keys(tc.activeSiteRule)
|
||||||
|
.filter(function(key) {
|
||||||
|
return key !== "pattern" && key !== "title";
|
||||||
|
})
|
||||||
|
.sort()
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var tc = {
|
var tc = {
|
||||||
settings: {
|
settings: {
|
||||||
lastSpeed: getSharedDefault("lastSpeed", 1.0),
|
lastSpeed: getSharedDefault("lastSpeed", 1.0),
|
||||||
@@ -151,16 +240,44 @@ var tc = {
|
|||||||
speedAccessTimes: {},
|
speedAccessTimes: {},
|
||||||
persistedLastSpeed: 1.0,
|
persistedLastSpeed: 1.0,
|
||||||
activeSiteRule: null,
|
activeSiteRule: null,
|
||||||
|
tabPaused: false,
|
||||||
siteRuleBase: null,
|
siteRuleBase: null,
|
||||||
runtimeSettingsHydrated: false,
|
runtimeSettingsHydrated: false,
|
||||||
pendingMediaCandidates: [],
|
pendingMediaCandidates: [],
|
||||||
settingsReloadRetries: 0,
|
settingsReloadRetries: 0,
|
||||||
lastPointerPosition: null,
|
lastPointerPosition: null,
|
||||||
lastInteractedMedia: null
|
lastInteractedMedia: null,
|
||||||
|
frameToken:
|
||||||
|
window.crypto && typeof window.crypto.randomUUID === "function"
|
||||||
|
? window.crypto.randomUUID()
|
||||||
|
: String(Date.now()) + "-" + Math.random().toString(36).slice(2)
|
||||||
};
|
};
|
||||||
|
|
||||||
var MIN_SPEED = 0.0625;
|
function isSpeederActiveForCurrentPage() {
|
||||||
var MAX_SPEED = 16;
|
return (
|
||||||
|
tc.tabPaused !== true &&
|
||||||
|
siteRuleUtils.isSpeederActiveForSite(
|
||||||
|
tc.settings.enabled,
|
||||||
|
tc.activeSiteRule
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTabPausedState(paused) {
|
||||||
|
tc.tabPaused = paused === true;
|
||||||
|
if (!tc.runtimeSettingsHydrated) return;
|
||||||
|
if (tc.tabPaused) {
|
||||||
|
clearAllSpeedRestoreEnforcement();
|
||||||
|
tc.mediaElements.slice().forEach(function(media) {
|
||||||
|
removeController(media);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
initializeWhenReady(document, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var MIN_SPEED = Number(keyBindingUtils.MIN_SPEED) || 0.1;
|
||||||
|
var MAX_SPEED = Number(keyBindingUtils.MAX_SPEED) || 16;
|
||||||
var YT_NATIVE_MIN = 0.25;
|
var YT_NATIVE_MIN = 0.25;
|
||||||
var YT_NATIVE_MAX = 2.0;
|
var YT_NATIVE_MAX = 2.0;
|
||||||
var YT_NATIVE_STEP = 0.05;
|
var YT_NATIVE_STEP = 0.05;
|
||||||
@@ -235,21 +352,25 @@ var controllerLocationStyles = {
|
|||||||
|
|
||||||
/* `label` fallback only when ui-icons has no path for the action. */
|
/* `label` fallback only when ui-icons has no path for the action. */
|
||||||
var controllerButtonDefs = {
|
var controllerButtonDefs = {
|
||||||
rewind: { label: "", className: "rw" },
|
rewind: { label: "", name: "Rewind", className: "rw" },
|
||||||
slower: { label: "", className: "" },
|
slower: { label: "", name: "Decrease speed", className: "" },
|
||||||
faster: { label: "", className: "" },
|
faster: { label: "", name: "Increase speed", className: "" },
|
||||||
advance: { label: "", className: "rw" },
|
advance: { label: "", name: "Advance", className: "rw" },
|
||||||
display: { label: "", className: "hideButton" },
|
display: {
|
||||||
reset: { label: "\u21BB", className: "" },
|
label: "",
|
||||||
fast: { label: "", className: "" },
|
name: "Show or hide controller",
|
||||||
nudge: { label: "", className: "" },
|
className: "hideButton"
|
||||||
pause: { label: "", className: "" },
|
},
|
||||||
muted: { label: "", className: "" },
|
reset: { label: "\u21BB", name: "Reset speed", className: "" },
|
||||||
louder: { label: "", className: "" },
|
fast: { label: "", name: "Toggle preferred speed", className: "" },
|
||||||
softer: { label: "", className: "" },
|
nudge: { label: "", name: "Toggle subtitle nudge", className: "" },
|
||||||
mark: { label: "", className: "" },
|
pause: { label: "", name: "Play or pause", className: "" },
|
||||||
jump: { label: "", className: "" },
|
muted: { label: "", name: "Mute or unmute", className: "" },
|
||||||
settings: { label: "", className: "" }
|
louder: { label: "", name: "Increase volume", className: "" },
|
||||||
|
softer: { label: "", name: "Decrease volume", className: "" },
|
||||||
|
mark: { label: "", name: "Mark position", className: "" },
|
||||||
|
jump: { label: "", name: "Jump to marked position", className: "" },
|
||||||
|
settings: { label: "", name: "Open Speeder settings", className: "" }
|
||||||
};
|
};
|
||||||
|
|
||||||
function createDefaultBinding(action, code, value) {
|
function createDefaultBinding(action, code, value) {
|
||||||
@@ -652,6 +773,7 @@ function normalizeStoredBinding(binding, fallbackCode) {
|
|||||||
var normalized = {
|
var normalized = {
|
||||||
action: binding.action,
|
action: binding.action,
|
||||||
code: normalizedCode,
|
code: normalizedCode,
|
||||||
|
shiftKey: binding.shiftKey === true,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
value: keyBindingUtils.sanitizeActionValue(
|
value: keyBindingUtils.sanitizeActionValue(
|
||||||
binding.action,
|
binding.action,
|
||||||
@@ -1360,7 +1482,8 @@ function matchesKeyBinding(binding, event) {
|
|||||||
binding.disabled !== true &&
|
binding.disabled !== true &&
|
||||||
typeof binding.code === "string" &&
|
typeof binding.code === "string" &&
|
||||||
binding.code.length > 0 &&
|
binding.code.length > 0 &&
|
||||||
binding.code === event.code
|
binding.code === event.code &&
|
||||||
|
(binding.shiftKey === true) === event.shiftKey
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1506,7 +1629,7 @@ function ensureController(node, parent) {
|
|||||||
// href selects site rules; re-run on every new/usable media so all runtime
|
// href selects site rules; re-run on every new/usable media so all runtime
|
||||||
// paths agree on activation and effective settings.
|
// paths agree on activation and effective settings.
|
||||||
applySiteRuleOverrides();
|
applySiteRuleOverrides();
|
||||||
if (!siteRuleUtils.isSpeederActiveForSite(tc.settings.enabled, tc.activeSiteRule)) {
|
if (!isSpeederActiveForCurrentPage()) {
|
||||||
if (node.vsc) removeController(node);
|
if (node.vsc) removeController(node);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -2314,12 +2437,24 @@ function loadInitialRuntimeSettings(attempt) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hydrateRuntimeSettings(rawStorage || {});
|
hydrateRuntimeSettings(rawStorage || {});
|
||||||
|
if (chrome.runtime && typeof chrome.runtime.sendMessage === "function") {
|
||||||
|
chrome.runtime.sendMessage({ action: "get_tab_pause_state" }, function(response) {
|
||||||
|
if (!chrome.runtime.lastError && response) {
|
||||||
|
applyTabPausedState(response.paused === true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
// patchAttachShadow() is now called at top-level before this callback
|
// patchAttachShadow() is now called at top-level before this callback
|
||||||
// Add a listener for messages from the popup.
|
// Add a listener for messages from the popup.
|
||||||
// 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 === "set_tab_paused") {
|
||||||
|
applyTabPausedState(request.paused === true);
|
||||||
|
sendResponse({ paused: tc.tabPaused });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
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);
|
||||||
@@ -2333,6 +2468,8 @@ function loadInitialRuntimeSettings(attempt) {
|
|||||||
if (!videoGs) return false;
|
if (!videoGs) return false;
|
||||||
sendResponse({
|
sendResponse({
|
||||||
speed: videoGs.playbackRate,
|
speed: videoGs.playbackRate,
|
||||||
|
frameToken: tc.frameToken,
|
||||||
|
diagnostics: getDiagnosticsSnapshot(videoGs),
|
||||||
forceLastSavedSpeed: tc.settings.forceLastSavedSpeed === true,
|
forceLastSavedSpeed: tc.settings.forceLastSavedSpeed === true,
|
||||||
forceLastSavedSpeedControlledBySiteRule: Boolean(
|
forceLastSavedSpeedControlledBySiteRule: Boolean(
|
||||||
tc.activeSiteRule &&
|
tc.activeSiteRule &&
|
||||||
@@ -2370,10 +2507,13 @@ function loadInitialRuntimeSettings(attempt) {
|
|||||||
}
|
}
|
||||||
if (request.action === "run_action") {
|
if (request.action === "run_action") {
|
||||||
if (
|
if (
|
||||||
!siteRuleUtils.isSpeederActiveForSite(
|
request.targetFrameToken &&
|
||||||
tc.settings.enabled,
|
request.targetFrameToken !== tc.frameToken
|
||||||
tc.activeSiteRule
|
) {
|
||||||
)
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!isSpeederActiveForCurrentPage()
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -2564,6 +2704,9 @@ function loadInitialRuntimeSettings(attempt) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Install before async settings hydration so SPA-owned window capture handlers
|
||||||
|
// cannot hide later key events from Speeder.
|
||||||
|
attachKeydownListeners(document);
|
||||||
loadInitialRuntimeSettings(0);
|
loadInitialRuntimeSettings(0);
|
||||||
|
|
||||||
function getKeyBindings(action, what = "value") {
|
function getKeyBindings(action, what = "value") {
|
||||||
@@ -2585,7 +2728,11 @@ 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");
|
||||||
|
var name = controllerButtonDefs[action] && controllerButtonDefs[action].name;
|
||||||
|
button.type = "button";
|
||||||
button.dataset.action = action;
|
button.dataset.action = action;
|
||||||
|
button.setAttribute("aria-label", name || action);
|
||||||
|
button.title = name || action;
|
||||||
var custom =
|
var custom =
|
||||||
tc.settings.customButtonIcons &&
|
tc.settings.customButtonIcons &&
|
||||||
tc.settings.customButtonIcons[action] &&
|
tc.settings.customButtonIcons[action] &&
|
||||||
@@ -2761,8 +2908,6 @@ function getControllerMount(video, boundary) {
|
|||||||
return isShadowRootNode(directRoot) ? directRoot : null;
|
return isShadowRootNode(directRoot) ? directRoot : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var mountBoundary = null;
|
|
||||||
|
|
||||||
var videoRect = video.getBoundingClientRect();
|
var videoRect = video.getBoundingClientRect();
|
||||||
var mount = video.parentElement;
|
var mount = video.parentElement;
|
||||||
var candidate = mount;
|
var candidate = mount;
|
||||||
@@ -2780,15 +2925,7 @@ function getControllerMount(video, boundary) {
|
|||||||
// Climb through tightly-sized wrappers so our host shares their stacking
|
// Climb through tightly-sized wrappers so our host shares their stacking
|
||||||
// context, but stop before broad page-layout containers.
|
// context, but stop before broad page-layout containers.
|
||||||
while (candidate && candidate.parentElement && depth < 5) {
|
while (candidate && candidate.parentElement && depth < 5) {
|
||||||
if (mountBoundary && candidate === mountBoundary) break;
|
|
||||||
var next = candidate.parentElement;
|
var next = candidate.parentElement;
|
||||||
if (
|
|
||||||
mountBoundary &&
|
|
||||||
next !== mountBoundary &&
|
|
||||||
!mountBoundary.contains(next)
|
|
||||||
) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
var nextRect = next.getBoundingClientRect();
|
var nextRect = next.getBoundingClientRect();
|
||||||
var widthLimit = Math.max(videoRect.width * 1.35, videoRect.width + 80);
|
var widthLimit = Math.max(videoRect.width * 1.35, videoRect.width + 80);
|
||||||
var heightLimit = Math.max(videoRect.height * 1.35, videoRect.height + 80);
|
var heightLimit = Math.max(videoRect.height * 1.35, videoRect.height + 80);
|
||||||
@@ -2812,10 +2949,6 @@ function getControllerMount(video, boundary) {
|
|||||||
candidate = next;
|
candidate = next;
|
||||||
depth += 1;
|
depth += 1;
|
||||||
|
|
||||||
// In fullscreen, the wrapper must remain inside the exact subtree the
|
|
||||||
// browser promotes to its top layer.
|
|
||||||
if (mountBoundary && next === mountBoundary) break;
|
|
||||||
|
|
||||||
// Never climb out of a player-owned stacking context. Doing so lets the
|
// Never climb out of a player-owned stacking context. Doing so lets the
|
||||||
// controller's high local z-index escape above sticky page headers.
|
// controller's high local z-index escape above sticky page headers.
|
||||||
if (createsControllerStackingContext(next)) break;
|
if (createsControllerStackingContext(next)) break;
|
||||||
@@ -2843,6 +2976,14 @@ function positionControllerHost(wrapper, video, mount) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var videoRect = video.getBoundingClientRect();
|
var videoRect = video.getBoundingClientRect();
|
||||||
|
wrapper.vscVideoRect = {
|
||||||
|
left: Number(videoRect.left) || 0,
|
||||||
|
top: Number(videoRect.top) || 0,
|
||||||
|
right: Number(videoRect.right) || 0,
|
||||||
|
bottom: Number(videoRect.bottom) || 0,
|
||||||
|
width: Number(videoRect.width) || 0,
|
||||||
|
height: Number(videoRect.height) || 0
|
||||||
|
};
|
||||||
if (wrapper.classList.contains("vsc-fullscreen-popover")) {
|
if (wrapper.classList.contains("vsc-fullscreen-popover")) {
|
||||||
if (videoRect.width <= 0 || videoRect.height <= 0) {
|
if (videoRect.width <= 0 || videoRect.height <= 0) {
|
||||||
wrapper.classList.add("vsc-geometry-hidden");
|
wrapper.classList.add("vsc-geometry-hidden");
|
||||||
@@ -3011,6 +3152,12 @@ function setupControllerHostTracking(videoController, wrapper, mount) {
|
|||||||
resizeObserver.observe(geometryMount);
|
resizeObserver.observe(geometryMount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mediaGeometryEvents = ["loadedmetadata", "play", "playing"];
|
||||||
|
mediaGeometryEvents.forEach(function(eventName) {
|
||||||
|
videoController.video.addEventListener(eventName, schedule, {
|
||||||
|
passive: true
|
||||||
|
});
|
||||||
|
});
|
||||||
win.addEventListener("resize", schedule, { passive: true });
|
win.addEventListener("resize", schedule, { passive: true });
|
||||||
doc.addEventListener("fullscreenchange", schedule, { passive: true });
|
doc.addEventListener("fullscreenchange", schedule, { passive: true });
|
||||||
geometryMount.addEventListener("scroll", schedule, { passive: true });
|
geometryMount.addEventListener("scroll", schedule, { passive: true });
|
||||||
@@ -3021,6 +3168,9 @@ function setupControllerHostTracking(videoController, wrapper, mount) {
|
|||||||
if (resizeObserver) resizeObserver.disconnect();
|
if (resizeObserver) resizeObserver.disconnect();
|
||||||
if (frameId !== null) win.cancelAnimationFrame(frameId);
|
if (frameId !== null) win.cancelAnimationFrame(frameId);
|
||||||
if (geometryRetryTimer !== null) win.clearTimeout(geometryRetryTimer);
|
if (geometryRetryTimer !== null) win.clearTimeout(geometryRetryTimer);
|
||||||
|
mediaGeometryEvents.forEach(function(eventName) {
|
||||||
|
videoController.video.removeEventListener(eventName, schedule);
|
||||||
|
});
|
||||||
win.removeEventListener("resize", schedule);
|
win.removeEventListener("resize", schedule);
|
||||||
doc.removeEventListener("fullscreenchange", schedule);
|
doc.removeEventListener("fullscreenchange", schedule);
|
||||||
geometryMount.removeEventListener("scroll", schedule);
|
geometryMount.removeEventListener("scroll", schedule);
|
||||||
@@ -3144,14 +3294,14 @@ function disableDirectFullscreenPopover(videoController) {
|
|||||||
wrapper.removeAttribute("popover");
|
wrapper.removeAttribute("popover");
|
||||||
}
|
}
|
||||||
|
|
||||||
function enableFullscreenPopover(videoController, preferredMount) {
|
function enableDirectFullscreenPopover(videoController) {
|
||||||
if (!videoController || !videoController.video || !videoController.div) {
|
if (!videoController || !videoController.video || !videoController.div) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var wrapper = videoController.div;
|
var wrapper = videoController.div;
|
||||||
if (typeof wrapper.showPopover !== "function") return false;
|
if (typeof wrapper.showPopover !== "function") return false;
|
||||||
|
|
||||||
var normalMount = preferredMount || videoController.normalControllerMount;
|
var normalMount = videoController.normalControllerMount;
|
||||||
var normalMountIsConnected = Boolean(
|
var normalMountIsConnected = Boolean(
|
||||||
normalMount &&
|
normalMount &&
|
||||||
(normalMount.isConnected ||
|
(normalMount.isConnected ||
|
||||||
@@ -3191,12 +3341,20 @@ function syncControllerFullscreenMount(videoController) {
|
|||||||
var doc = video.ownerDocument;
|
var doc = video.ownerDocument;
|
||||||
var fullscreenElement = getFullscreenElement(doc);
|
var fullscreenElement = getFullscreenElement(doc);
|
||||||
var targetMount = videoController.normalControllerMount;
|
var targetMount = videoController.normalControllerMount;
|
||||||
|
var ownsFullscreen = Boolean(
|
||||||
if (
|
|
||||||
fullscreenElement &&
|
fullscreenElement &&
|
||||||
(fullscreenElement === video ||
|
(fullscreenElement === video ||
|
||||||
isComposedDescendant(video, fullscreenElement))
|
isComposedDescendant(video, fullscreenElement))
|
||||||
) {
|
);
|
||||||
|
var normalGeometryMount = getControllerGeometryMount(targetMount);
|
||||||
|
var normalMountIsAlreadyFullscreenVisible = Boolean(
|
||||||
|
fullscreenElement &&
|
||||||
|
fullscreenElement !== video &&
|
||||||
|
normalGeometryMount &&
|
||||||
|
isComposedDescendant(normalGeometryMount, fullscreenElement)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ownsFullscreen && !normalMountIsAlreadyFullscreenVisible) {
|
||||||
targetMount = getControllerMount(video, fullscreenElement);
|
targetMount = getControllerMount(video, fullscreenElement);
|
||||||
} else if (!fullscreenElement && (!targetMount || !targetMount.isConnected)) {
|
} else if (!fullscreenElement && (!targetMount || !targetMount.isConnected)) {
|
||||||
targetMount = getControllerMount(video);
|
targetMount = getControllerMount(video);
|
||||||
@@ -3205,13 +3363,10 @@ function syncControllerFullscreenMount(videoController) {
|
|||||||
|
|
||||||
if (!targetMount) return false;
|
if (!targetMount) return false;
|
||||||
|
|
||||||
if (fullscreenElement) {
|
if (fullscreenElement === video) {
|
||||||
// Fullscreen elements and popovers both participate in the browser's top
|
// A replaced <video> cannot paint author children, so direct-media
|
||||||
// layer. Showing Speeder's host after the player enters fullscreen keeps it
|
// fullscreen is the only case that needs a separate top-layer popover.
|
||||||
// above provider-owned surfaces even when the provider clips descendants or
|
if (enableDirectFullscreenPopover(videoController)) return true;
|
||||||
// creates a new fullscreen stacking context (notably Firefox + YouTube).
|
|
||||||
// Browsers without the Popover API retain the player-local remount fallback.
|
|
||||||
if (enableFullscreenPopover(videoController, targetMount)) return true;
|
|
||||||
} else {
|
} else {
|
||||||
disableDirectFullscreenPopover(videoController);
|
disableDirectFullscreenPopover(videoController);
|
||||||
}
|
}
|
||||||
@@ -3353,9 +3508,6 @@ function defineVideoController() {
|
|||||||
setSpeed(event.target, expectedSpeed, false, false);
|
setSpeed(event.target, expectedSpeed, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUserSeek) {
|
|
||||||
isUserSeek = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3749,11 +3901,6 @@ function defineVideoController() {
|
|||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
timer = null;
|
timer = null;
|
||||||
if (this.controllerInteractionActive) return;
|
if (this.controllerInteractionActive) return;
|
||||||
// Only hide if the video is not paused
|
|
||||||
// (Many players keep controls visible while paused)
|
|
||||||
// However, the user said "Reveal on every mouse and keyboard input"
|
|
||||||
// and "auto-hidden after timespan".
|
|
||||||
// We'll follow the timer strictly.
|
|
||||||
wrapper.classList.add("vsc-idle-hidden");
|
wrapper.classList.add("vsc-idle-hidden");
|
||||||
log("Generic hide: controller hidden due to inactivity", 5);
|
log("Generic hide: controller hidden due to inactivity", 5);
|
||||||
}, tc.settings.hideWithControlsTimer * 1000);
|
}, tc.settings.hideWithControlsTimer * 1000);
|
||||||
@@ -3776,8 +3923,8 @@ function defineVideoController() {
|
|||||||
// Initial show/timer
|
// Initial show/timer
|
||||||
resetTimer();
|
resetTimer();
|
||||||
|
|
||||||
// The wrapper covers the player area on most sites due to inject.css styles,
|
// Players dispatch activity at different layers, so observe the media,
|
||||||
// but we listen on both the video and the wrapper for maximum coverage.
|
// aligned controller host, and player mount.
|
||||||
const activityEvents = ["mousemove", "mousedown", "keydown", "touchstart"];
|
const activityEvents = ["mousemove", "mousedown", "keydown", "touchstart"];
|
||||||
const parentEl =
|
const parentEl =
|
||||||
getControllerGeometryMount(this.controllerHostMount) ||
|
getControllerGeometryMount(this.controllerHostMount) ||
|
||||||
@@ -3819,6 +3966,9 @@ function defineVideoController() {
|
|||||||
const speed = this.video.playbackRate.toFixed(2);
|
const speed = this.video.playbackRate.toFixed(2);
|
||||||
var wrapper = doc.createElement("div");
|
var wrapper = doc.createElement("div");
|
||||||
wrapper.classList.add("vsc-controller");
|
wrapper.classList.add("vsc-controller");
|
||||||
|
// Keep the host out of player layout while its shadow stylesheet loads.
|
||||||
|
wrapper.style.position = "absolute";
|
||||||
|
wrapper.style.pointerEvents = "none";
|
||||||
if (!hasUsableMediaSource(this.video))
|
if (!hasUsableMediaSource(this.video))
|
||||||
wrapper.classList.add("vsc-nosource");
|
wrapper.classList.add("vsc-nosource");
|
||||||
if (tc.settings.startHidden) wrapper.classList.add("vsc-hidden");
|
if (tc.settings.startHidden) wrapper.classList.add("vsc-hidden");
|
||||||
@@ -3856,11 +4006,14 @@ function defineVideoController() {
|
|||||||
|
|
||||||
buttonConfig.forEach(function(btnId) {
|
buttonConfig.forEach(function(btnId) {
|
||||||
if (btnId === "nudge") {
|
if (btnId === "nudge") {
|
||||||
subtitleNudgeIndicator = doc.createElement("span");
|
subtitleNudgeIndicator = createControllerButton(
|
||||||
|
doc,
|
||||||
|
btnId,
|
||||||
|
controllerButtonDefs.nudge.label,
|
||||||
|
controllerButtonDefs.nudge.className
|
||||||
|
);
|
||||||
subtitleNudgeIndicator.id = "nudge-indicator";
|
subtitleNudgeIndicator.id = "nudge-indicator";
|
||||||
subtitleNudgeIndicator.setAttribute("role", "button");
|
|
||||||
subtitleNudgeIndicator.setAttribute("aria-live", "polite");
|
subtitleNudgeIndicator.setAttribute("aria-live", "polite");
|
||||||
subtitleNudgeIndicator.setAttribute("tabindex", "0");
|
|
||||||
controls.appendChild(subtitleNudgeIndicator);
|
controls.appendChild(subtitleNudgeIndicator);
|
||||||
} else {
|
} else {
|
||||||
var def = controllerButtonDefs[btnId];
|
var def = controllerButtonDefs[btnId];
|
||||||
@@ -3944,21 +4097,6 @@ function defineVideoController() {
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
if (subtitleNudgeIndicator) {
|
|
||||||
subtitleNudgeIndicator.addEventListener(
|
|
||||||
"click",
|
|
||||||
(e) => {
|
|
||||||
var video = this.video;
|
|
||||||
if (video) {
|
|
||||||
var newState = !isSubtitleNudgeEnabledForVideo(video);
|
|
||||||
setSubtitleNudgeEnabledForVideo(video, newState);
|
|
||||||
}
|
|
||||||
blurAfterPointerTap(subtitleNudgeIndicator, e);
|
|
||||||
e.stopPropagation();
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
controller.addEventListener("click", (e) => e.stopPropagation(), false);
|
controller.addEventListener("click", (e) => e.stopPropagation(), false);
|
||||||
controller.addEventListener("mousedown", (e) => e.stopPropagation(), false);
|
controller.addEventListener("mousedown", (e) => e.stopPropagation(), false);
|
||||||
|
|
||||||
@@ -4150,7 +4288,7 @@ function refreshAllControllerGeometry() {
|
|||||||
/** Re-match site rules for current URL and refresh controller position/opacity on every video. */
|
/** Re-match site rules for current URL and refresh controller position/opacity on every video. */
|
||||||
function reapplySiteRulesAndControllerGeometry() {
|
function reapplySiteRulesAndControllerGeometry() {
|
||||||
applySiteRuleOverrides();
|
applySiteRuleOverrides();
|
||||||
if (!siteRuleUtils.isSpeederActiveForSite(tc.settings.enabled, tc.activeSiteRule)) {
|
if (!isSpeederActiveForCurrentPage()) {
|
||||||
tc.mediaElements.slice().forEach(function(video) {
|
tc.mediaElements.slice().forEach(function(video) {
|
||||||
removeController(video);
|
removeController(video);
|
||||||
});
|
});
|
||||||
@@ -4208,7 +4346,7 @@ function setupListener(root) {
|
|||||||
if (config.skipResetDisarm !== true) {
|
if (config.skipResetDisarm !== true) {
|
||||||
video.vsc.resetToggleArmed = false;
|
video.vsc.resetToggleArmed = false;
|
||||||
}
|
}
|
||||||
var speed = video.playbackRate; // Preserve full precision (e.g. 0.0625)
|
var speed = video.playbackRate; // Preserve full precision (e.g. 0.01)
|
||||||
video.vsc.speedIndicator.textContent = speed.toFixed(2);
|
video.vsc.speedIndicator.textContent = speed.toFixed(2);
|
||||||
video.vsc.targetSpeed = speed;
|
video.vsc.targetSpeed = speed;
|
||||||
video.vsc.targetSpeedSourceKey = getVideoSourceKey(video);
|
video.vsc.targetSpeedSourceKey = getVideoSourceKey(video);
|
||||||
@@ -4240,7 +4378,7 @@ function setupListener(root) {
|
|||||||
if (!video || typeof video.playbackRate === "undefined" || !video.vsc)
|
if (!video || typeof video.playbackRate === "undefined" || !video.vsc)
|
||||||
return;
|
return;
|
||||||
if (shouldIgnoreSuppressedRateChange(video)) return;
|
if (shouldIgnoreSuppressedRateChange(video)) return;
|
||||||
var currentSpeed = video.playbackRate; // Preserve full precision (e.g. 0.0625)
|
var currentSpeed = video.playbackRate; // Preserve full precision (e.g. 0.01)
|
||||||
var pendingRateChange = takePendingRateChange(video, currentSpeed);
|
var pendingRateChange = takePendingRateChange(video, currentSpeed);
|
||||||
if (tc.settings.forceLastSavedSpeed) {
|
if (tc.settings.forceLastSavedSpeed) {
|
||||||
if (pendingRateChange) {
|
if (pendingRateChange) {
|
||||||
@@ -4309,7 +4447,11 @@ function clearPendingInitialization(doc) {
|
|||||||
|
|
||||||
function tryInitializeDocument(doc, forceReinit) {
|
function tryInitializeDocument(doc, forceReinit) {
|
||||||
if (!doc) return false;
|
if (!doc) return false;
|
||||||
if ((!forceReinit && vscInitializedDocuments.has(doc)) || !doc.body) {
|
if (
|
||||||
|
!tc.runtimeSettingsHydrated ||
|
||||||
|
(!forceReinit && vscInitializedDocuments.has(doc)) ||
|
||||||
|
!doc.body
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4352,14 +4494,41 @@ function inIframe() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isEditableShortcutTarget(event) {
|
||||||
|
var path =
|
||||||
|
event && typeof event.composedPath === "function"
|
||||||
|
? event.composedPath()
|
||||||
|
: [event && event.target];
|
||||||
|
|
||||||
|
return path.some(function(target) {
|
||||||
|
if (!target || target.nodeType !== 1) return false;
|
||||||
|
var nodeName = target.nodeName;
|
||||||
|
var role = target.getAttribute && target.getAttribute("role");
|
||||||
|
var contentEditable =
|
||||||
|
target.getAttribute && target.getAttribute("contenteditable");
|
||||||
|
return (
|
||||||
|
nodeName === "INPUT" ||
|
||||||
|
nodeName === "TEXTAREA" ||
|
||||||
|
nodeName === "SELECT" ||
|
||||||
|
target.isContentEditable ||
|
||||||
|
(contentEditable !== null && contentEditable !== "false") ||
|
||||||
|
role === "textbox" ||
|
||||||
|
role === "searchbox" ||
|
||||||
|
role === "combobox" ||
|
||||||
|
role === "spinbutton"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function attachKeydownListeners(doc) {
|
function attachKeydownListeners(doc) {
|
||||||
// Content scripts already run in every frame. Keeping each listener scoped
|
// Content scripts already run in every frame. Keeping each listener scoped
|
||||||
// to its own document avoids duplicate shortcuts and stale iframe ownership.
|
// to its own frame avoids duplicate shortcuts and stale iframe ownership.
|
||||||
var docs = [doc];
|
var docs = [doc];
|
||||||
|
|
||||||
docs.forEach(function(keyDoc) {
|
docs.forEach(function(keyDoc) {
|
||||||
if (keyDoc.vscKeydownListenerAttached) return;
|
var keyTarget = keyDoc.defaultView || keyDoc;
|
||||||
keyDoc.addEventListener(
|
if (keyTarget.vscKeydownListenerAttached) return;
|
||||||
|
keyTarget.addEventListener(
|
||||||
"keydown",
|
"keydown",
|
||||||
function(event) {
|
function(event) {
|
||||||
if (
|
if (
|
||||||
@@ -4374,19 +4543,10 @@ function attachKeydownListeners(doc) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (isEditableShortcutTarget(event)) return;
|
||||||
event.target.nodeName === "INPUT" ||
|
|
||||||
event.target.nodeName === "TEXTAREA" ||
|
|
||||||
event.target.isContentEditable
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!siteRuleUtils.isSpeederActiveForSite(
|
!isSpeederActiveForCurrentPage()
|
||||||
tc.settings.enabled,
|
|
||||||
tc.activeSiteRule
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4411,7 +4571,7 @@ function attachKeydownListeners(doc) {
|
|||||||
},
|
},
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
keyDoc.vscKeydownListenerAttached = true;
|
keyTarget.vscKeydownListenerAttached = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4611,10 +4771,7 @@ function initializeNow(doc, forceReinit = false) {
|
|||||||
attachNavigationListeners();
|
attachNavigationListeners();
|
||||||
if (typeof tc.videoController === "undefined") defineVideoController();
|
if (typeof tc.videoController === "undefined") defineVideoController();
|
||||||
applySiteRuleOverrides();
|
applySiteRuleOverrides();
|
||||||
var isActive = siteRuleUtils.isSpeederActiveForSite(
|
var isActive = isSpeederActiveForCurrentPage();
|
||||||
tc.settings.enabled,
|
|
||||||
tc.activeSiteRule
|
|
||||||
);
|
|
||||||
|
|
||||||
// Keep observing while inactive so dynamically-created media/shadow roots
|
// Keep observing while inactive so dynamically-created media/shadow roots
|
||||||
// are available to the next forced SPA rescan, but remove stale controls.
|
// are available to the next forced SPA rescan, but remove stale controls.
|
||||||
@@ -4766,16 +4923,7 @@ function getClosestMediaToPointer(candidates, pointerPosition) {
|
|||||||
|
|
||||||
candidates.forEach(function(video) {
|
candidates.forEach(function(video) {
|
||||||
if (!video || !video.vsc || !video.isConnected) return;
|
if (!video || !video.vsc || !video.isConnected) return;
|
||||||
var target = getControllerElement(video.vsc) || video;
|
var rect = getCachedVideoRect(video);
|
||||||
var rect = null;
|
|
||||||
try {
|
|
||||||
rect = target.getBoundingClientRect();
|
|
||||||
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
|
||||||
rect = video.getBoundingClientRect();
|
|
||||||
}
|
|
||||||
} catch (_error) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!rect || rect.width <= 0 || rect.height <= 0) return;
|
if (!rect || rect.width <= 0 || rect.height <= 0) return;
|
||||||
var distance = distanceSquaredToRect(
|
var distance = distanceSquaredToRect(
|
||||||
pointerPosition.x,
|
pointerPosition.x,
|
||||||
@@ -4816,6 +4964,16 @@ function resolveActionMediaTargets(event, specificVideo) {
|
|||||||
|
|
||||||
if (tc.settings.shortcutTargetMode === "all") return candidates;
|
if (tc.settings.shortcutTargetMode === "all") return candidates;
|
||||||
|
|
||||||
|
if (
|
||||||
|
isOnYouTube() &&
|
||||||
|
/^\/(watch|live)(\/|$)/.test(location.pathname)
|
||||||
|
) {
|
||||||
|
var youtubeMain = candidates.find(function(video) {
|
||||||
|
return video.closest && video.closest("#movie_player");
|
||||||
|
});
|
||||||
|
if (youtubeMain) return [youtubeMain];
|
||||||
|
}
|
||||||
|
|
||||||
var pointer =
|
var pointer =
|
||||||
tc.lastPointerPosition && tc.lastPointerPosition.document === docContext
|
tc.lastPointerPosition && tc.lastPointerPosition.document === docContext
|
||||||
? tc.lastPointerPosition
|
? tc.lastPointerPosition
|
||||||
@@ -4894,12 +5052,10 @@ function runAction(action, value, e) {
|
|||||||
);
|
);
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "rewind":
|
case "rewind":
|
||||||
isUserSeek = true;
|
|
||||||
extendSpeedRestoreWindow(v);
|
extendSpeedRestoreWindow(v);
|
||||||
v.currentTime -= numValue;
|
v.currentTime -= numValue;
|
||||||
break;
|
break;
|
||||||
case "advance":
|
case "advance":
|
||||||
isUserSeek = true;
|
|
||||||
extendSpeedRestoreWindow(v);
|
extendSpeedRestoreWindow(v);
|
||||||
v.currentTime += numValue;
|
v.currentTime += numValue;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,15 +1,28 @@
|
|||||||
:host {
|
:host {
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
z-index: 2147483647 !important;
|
z-index: 2147483647 !important;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Important declarations inside a shadow tree outrank important page styles.
|
||||||
|
Repeat the top-layer positioning here so fullscreen cannot fall back to the
|
||||||
|
base absolute host rule. */
|
||||||
|
:host(.vsc-fullscreen-popover) {
|
||||||
|
position: fixed !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border: 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.vsc-fullscreen-popover)::backdrop {
|
||||||
|
pointer-events: none !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
:host(.vsc-nosource),
|
:host(.vsc-nosource),
|
||||||
:host(.vsc-hidden) {
|
:host(.vsc-hidden) {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
@@ -316,10 +329,15 @@ button .vsc-btn-icon svg {
|
|||||||
transform: translateY(0.5px);
|
transform: translateY(0.5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:focus {
|
button:focus:not(:focus-visible) {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 2px solid #fff;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Speeder",
|
"name": "Speeder",
|
||||||
"short_name": "Speeder",
|
"short_name": "Speeder",
|
||||||
"version": "6.0.2",
|
"version": "6.0.8.2",
|
||||||
"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",
|
||||||
@@ -71,7 +71,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
"content/inject.css",
|
|
||||||
"content/shadow-bridge.js",
|
"content/shadow-bridge.js",
|
||||||
"content/shadow.css"
|
"content/shadow.css"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -54,11 +54,7 @@ function normalizedSettingsForExport(rawStorage) {
|
|||||||
// lastSpeed is useful user data, but it is intentionally outside the
|
// lastSpeed is useful user data, but it is intentionally outside the
|
||||||
// managed options diff because content scripts update it at runtime.
|
// managed options diff because content scripts update it at runtime.
|
||||||
if (Object.prototype.hasOwnProperty.call(raw, "lastSpeed")) {
|
if (Object.prototype.hasOwnProperty.call(raw, "lastSpeed")) {
|
||||||
if (
|
if (Number.isFinite(expanded.lastSpeed)) {
|
||||||
Number.isFinite(expanded.lastSpeed) &&
|
|
||||||
expanded.lastSpeed >= 0.0625 &&
|
|
||||||
expanded.lastSpeed <= 16
|
|
||||||
) {
|
|
||||||
normalized.lastSpeed = expanded.lastSpeed;
|
normalized.lastSpeed = expanded.lastSpeed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,11 +113,7 @@ function persistImportedSyncSettings(currentRaw, importedRaw, callback) {
|
|||||||
importedRaw &&
|
importedRaw &&
|
||||||
Object.prototype.hasOwnProperty.call(importedRaw, "lastSpeed")
|
Object.prototype.hasOwnProperty.call(importedRaw, "lastSpeed")
|
||||||
) {
|
) {
|
||||||
if (
|
if (Number.isFinite(expanded.lastSpeed)) {
|
||||||
Number.isFinite(expanded.lastSpeed) &&
|
|
||||||
expanded.lastSpeed >= 0.0625 &&
|
|
||||||
expanded.lastSpeed <= 16
|
|
||||||
) {
|
|
||||||
mutation.set.lastSpeed = expanded.lastSpeed;
|
mutation.set.lastSpeed = expanded.lastSpeed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,14 +215,15 @@ function importSettings() {
|
|||||||
importLocalSettings(scopedLocalSettings, function (localError) {
|
importLocalSettings(scopedLocalSettings, function (localError) {
|
||||||
if (localError) {
|
if (localError) {
|
||||||
showStatus(
|
showStatus(
|
||||||
"Error: Failed to save local extension data - " +
|
"Settings imported, but custom icons could not be updated - " +
|
||||||
localError.message,
|
localError.message +
|
||||||
|
". Reloading...",
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
return;
|
} else {
|
||||||
|
showStatus("Settings imported successfully. Reloading...");
|
||||||
}
|
}
|
||||||
|
|
||||||
showStatus("Settings imported successfully. Reloading...");
|
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
if (typeof restore_options === "function") {
|
if (typeof restore_options === "function") {
|
||||||
restore_options();
|
restore_options();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
--text: #17191c;
|
--text: #17191c;
|
||||||
--muted: #626b76;
|
--muted: #626b76;
|
||||||
--accent: #111827;
|
--accent: #111827;
|
||||||
|
--focus-ring: #2563eb;
|
||||||
--switch-track-off: #c1cad6;
|
--switch-track-off: #c1cad6;
|
||||||
--switch-track-off-border: #aeb8c5;
|
--switch-track-off-border: #aeb8c5;
|
||||||
--switch-track-on: #111827;
|
--switch-track-on: #111827;
|
||||||
@@ -177,9 +178,14 @@ a:visited {
|
|||||||
text-underline-offset: 0.14em;
|
text-underline-offset: 0.14em;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover,
|
a:hover {
|
||||||
a:focus {
|
color: var(--text);
|
||||||
color: #000;
|
text-decoration-thickness: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:focus-visible {
|
||||||
|
outline: 2px solid var(--focus-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
@@ -225,7 +231,7 @@ input[type="checkbox"]:focus-visible,
|
|||||||
input[type="text"]:focus,
|
input[type="text"]:focus,
|
||||||
select:focus,
|
select:focus,
|
||||||
textarea:focus {
|
textarea:focus {
|
||||||
outline: 2px solid rgba(17, 24, 39, 0.14);
|
outline: 2px solid var(--focus-ring);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,7 +328,7 @@ label em {
|
|||||||
|
|
||||||
.shortcut-row {
|
.shortcut-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) 120px 120px;
|
grid-template-columns: minmax(0, 1fr) 120px 120px 38px;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
@@ -334,10 +340,6 @@ label em {
|
|||||||
border-top: 0;
|
border-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shortcut-row.customs {
|
|
||||||
grid-template-columns: minmax(0, 1fr) 120px 120px 38px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcut-label {
|
.shortcut-label {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -628,6 +630,9 @@ label em {
|
|||||||
cursor: grab;
|
cursor: grab;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: box-shadow 150ms ease, opacity 150ms ease;
|
transition: box-shadow 150ms ease, opacity 150ms ease;
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cb-block:hover {
|
.cb-block:hover {
|
||||||
@@ -1061,6 +1066,20 @@ button.lucide-result-tile.lucide-picked {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#status:not(:empty) {
|
||||||
|
position: fixed;
|
||||||
|
right: 20px;
|
||||||
|
bottom: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
min-height: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: 0 8px 24px rgba(17, 24, 39, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
#status:empty {
|
#status:empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -1229,6 +1248,7 @@ button.lucide-result-tile.lucide-picked {
|
|||||||
--text: #f2f4f6;
|
--text: #f2f4f6;
|
||||||
--muted: #a0a8b2;
|
--muted: #a0a8b2;
|
||||||
--accent: #f2f4f6;
|
--accent: #f2f4f6;
|
||||||
|
--focus-ring: #93c5fd;
|
||||||
--switch-track-off: #374151;
|
--switch-track-off: #374151;
|
||||||
--switch-track-off-border: #4b5563;
|
--switch-track-off-border: #4b5563;
|
||||||
--switch-track-on: #aab7c6;
|
--switch-track-on: #aab7c6;
|
||||||
|
|||||||
@@ -250,7 +250,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<select id="addShortcutSelector">
|
<select id="addShortcutSelector" aria-label="Add another shortcut">
|
||||||
<option value="">Add shortcut…</option>
|
<option value="">Add shortcut…</option>
|
||||||
</select>
|
</select>
|
||||||
</section>
|
</section>
|
||||||
@@ -468,8 +468,8 @@
|
|||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<h3>Hover control bar</h3>
|
<h3>Hover control bar</h3>
|
||||||
<p class="section-intro">
|
<p class="section-intro">
|
||||||
Drag blocks to reorder. Move between Active and Available to
|
Drag blocks or use arrow keys to reorder. Press a block to
|
||||||
show or hide buttons.
|
move it between Active and Available.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="cb-editor">
|
<div class="cb-editor">
|
||||||
@@ -591,7 +591,7 @@
|
|||||||
<div
|
<div
|
||||||
id="lucideIconResults"
|
id="lucideIconResults"
|
||||||
class="lucide-icon-results"
|
class="lucide-icon-results"
|
||||||
role="listbox"
|
role="group"
|
||||||
aria-label="Matching Lucide icons"
|
aria-label="Matching Lucide icons"
|
||||||
></div>
|
></div>
|
||||||
<p id="lucideIconStatus" class="lucide-icon-status" aria-live="polite"></p>
|
<p id="lucideIconStatus" class="lucide-icon-status" aria-live="polite"></p>
|
||||||
@@ -843,7 +843,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="site-rule-option site-rule-option-field">
|
<div class="site-rule-option site-rule-option-field">
|
||||||
<label
|
<label
|
||||||
>Preferred speed (0.0625–16):<br /><em
|
>Preferred speed (0.1–16):<br /><em
|
||||||
>Overrides the Preferred speed action for matching
|
>Overrides the Preferred speed action for matching
|
||||||
URLs. It does not force every video to start at this
|
URLs. It does not force every video to start at this
|
||||||
speed.</em
|
speed.</em
|
||||||
@@ -943,8 +943,8 @@
|
|||||||
<label class="site-override-lead">
|
<label class="site-override-lead">
|
||||||
<span
|
<span
|
||||||
>Override in-player control bar for this site<br /><em
|
>Override in-player control bar for this site<br /><em
|
||||||
>Same idea as Hover control bar: drag blocks between
|
>Drag blocks or use the keyboard to arrange Active and
|
||||||
Active and Available for matching URLs only.</em
|
Available buttons for matching URLs only.</em
|
||||||
></span
|
></span
|
||||||
>
|
>
|
||||||
<input type="checkbox" class="override-controlbar" />
|
<input type="checkbox" class="override-controlbar" />
|
||||||
@@ -998,8 +998,8 @@
|
|||||||
<label class="site-override-lead">
|
<label class="site-override-lead">
|
||||||
<span
|
<span
|
||||||
>Override shortcuts for this site<br /><em
|
>Override shortcuts for this site<br /><em
|
||||||
>Add shortcuts from the menu; none by default. Leave off
|
>A site shortcut replaces global bindings for the same
|
||||||
to use global Shortcuts.</em
|
action; other global shortcuts remain active.</em
|
||||||
></span
|
></span
|
||||||
>
|
>
|
||||||
<input type="checkbox" class="override-shortcuts" />
|
<input type="checkbox" class="override-shortcuts" />
|
||||||
@@ -1022,13 +1022,14 @@
|
|||||||
<section class="settings-card action-card">
|
<section class="settings-card action-card">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<h3>Actions</h3>
|
<h3>Actions</h3>
|
||||||
<p class="section-intro">Save, restore, export, or import settings.</p>
|
<p class="section-intro">Changes save automatically.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-row">
|
<div class="action-row">
|
||||||
<button id="save">Save Changes</button>
|
<button id="save">Save Now</button>
|
||||||
<button id="restore">Restore Defaults</button>
|
<button id="restore">Restore Defaults</button>
|
||||||
<button id="exportSettings">Export Settings</button>
|
<button id="exportSettings">Export Settings</button>
|
||||||
<button id="importSettings">Import Settings</button>
|
<button id="importSettings">Import Settings</button>
|
||||||
|
<button id="copyDiagnostics">Copy Diagnostics</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="status" role="status" aria-live="polite"></div>
|
<div id="status" role="status" aria-live="polite"></div>
|
||||||
@@ -1037,11 +1038,11 @@
|
|||||||
<section id="faq" class="settings-card info-card">
|
<section id="faq" class="settings-card info-card">
|
||||||
<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
|
Speeder works with HTML5 video and, when enabled above, HTML5 audio.
|
||||||
controls never appear, you may be looking at Flash content instead.
|
Check that the current site is enabled, then use the popup’s
|
||||||
Right-click the player to check: if the menu mentions Flash, that
|
“Rescan page for videos” action after a player loads
|
||||||
is the issue. Most sites will fall back to HTML5 when Flash is not
|
dynamically. Browser-internal pages and players that do not expose
|
||||||
available, so disabling Flash in the browser can help.
|
HTML5 media to extensions cannot be controlled.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
+240
-69
@@ -164,9 +164,33 @@ function createDefaultBinding(action, code, value) {
|
|||||||
|
|
||||||
var tcDefaults = vscGetSettingsDefaults();
|
var tcDefaults = vscGetSettingsDefaults();
|
||||||
var optionsSyncSettingsLoaded = false;
|
var optionsSyncSettingsLoaded = false;
|
||||||
|
var autoSaveTimer = null;
|
||||||
|
var saveStatusTimer = null;
|
||||||
|
|
||||||
|
function scheduleAutoSave() {
|
||||||
|
if (!optionsSyncSettingsLoaded) return;
|
||||||
|
clearTimeout(autoSaveTimer);
|
||||||
|
var sourceSaveButton = document.getElementById("save");
|
||||||
|
var scheduledTimer = setTimeout(function () {
|
||||||
|
if (
|
||||||
|
autoSaveTimer !== scheduledTimer ||
|
||||||
|
!sourceSaveButton ||
|
||||||
|
!sourceSaveButton.isConnected
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
autoSaveTimer = null;
|
||||||
|
save_options(true);
|
||||||
|
}, 300);
|
||||||
|
autoSaveTimer = scheduledTimer;
|
||||||
|
}
|
||||||
|
|
||||||
function setOptionsSyncSettingsLoaded(loaded) {
|
function setOptionsSyncSettingsLoaded(loaded) {
|
||||||
optionsSyncSettingsLoaded = loaded === true;
|
optionsSyncSettingsLoaded = loaded === true;
|
||||||
|
if (!optionsSyncSettingsLoaded) {
|
||||||
|
clearTimeout(autoSaveTimer);
|
||||||
|
autoSaveTimer = null;
|
||||||
|
}
|
||||||
var saveButton = document.getElementById("save");
|
var saveButton = document.getElementById("save");
|
||||||
if (saveButton) {
|
if (saveButton) {
|
||||||
saveButton.disabled = !optionsSyncSettingsLoaded;
|
saveButton.disabled = !optionsSyncSettingsLoaded;
|
||||||
@@ -302,33 +326,15 @@ function refreshAddShortcutSelector() {
|
|||||||
selector.remove(1);
|
selector.remove(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all currently used actions
|
|
||||||
const usedActions = new Set();
|
|
||||||
document.querySelectorAll(".shortcut-row").forEach((row) => {
|
|
||||||
const action = row.dataset.action;
|
|
||||||
if (action) {
|
|
||||||
usedActions.add(action);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add all unused actions
|
|
||||||
Object.keys(actionLabels).forEach((action) => {
|
Object.keys(actionLabels).forEach((action) => {
|
||||||
if (!usedActions.has(action)) {
|
const option = document.createElement("option");
|
||||||
const option = document.createElement("option");
|
option.value = action;
|
||||||
option.value = action;
|
option.text = actionLabels[action];
|
||||||
option.text = actionLabels[action];
|
selector.appendChild(option);
|
||||||
selector.appendChild(option);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// If no available actions, hide or disable the selector
|
selector.disabled = false;
|
||||||
if (selector.options.length === 1) {
|
selector.options[0].text = "Add shortcut\u2026";
|
||||||
selector.disabled = true;
|
|
||||||
selector.options[0].text = "All shortcuts added";
|
|
||||||
} else {
|
|
||||||
selector.disabled = false;
|
|
||||||
selector.options[0].text = "Add shortcut\u2026";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshSiteRuleAddShortcutSelector(ruleEl) {
|
function refreshSiteRuleAddShortcutSelector(ruleEl) {
|
||||||
@@ -340,32 +346,19 @@ function refreshSiteRuleAddShortcutSelector(ruleEl) {
|
|||||||
selector.remove(1);
|
selector.remove(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
var usedActions = new Set();
|
|
||||||
ruleEl.querySelectorAll(".site-shortcuts-rows .shortcut-row.customs").forEach(function (row) {
|
|
||||||
var action = row.dataset.action;
|
|
||||||
if (action) usedActions.add(action);
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.keys(actionLabels).forEach(function (action) {
|
Object.keys(actionLabels).forEach(function (action) {
|
||||||
if (!usedActions.has(action)) {
|
var option = document.createElement("option");
|
||||||
var option = document.createElement("option");
|
option.value = action;
|
||||||
option.value = action;
|
option.textContent = actionLabels[action];
|
||||||
option.textContent = actionLabels[action];
|
selector.appendChild(option);
|
||||||
selector.appendChild(option);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var overrideShortcutsOn =
|
var overrideShortcutsOn =
|
||||||
ruleEl.querySelector(".override-shortcuts") &&
|
ruleEl.querySelector(".override-shortcuts") &&
|
||||||
ruleEl.querySelector(".override-shortcuts").checked;
|
ruleEl.querySelector(".override-shortcuts").checked;
|
||||||
|
|
||||||
if (selector.options.length === 1) {
|
selector.disabled = !overrideShortcutsOn;
|
||||||
selector.disabled = true;
|
selector.options[0].text = "Add shortcut\u2026";
|
||||||
selector.options[0].text = "All shortcuts added";
|
|
||||||
} else {
|
|
||||||
selector.disabled = !overrideShortcutsOn;
|
|
||||||
selector.options[0].text = "Add shortcut\u2026";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGlobalBindingSnapshotForSiteShortcut(action) {
|
function getGlobalBindingSnapshotForSiteShortcut(action) {
|
||||||
@@ -424,7 +417,10 @@ function readOptionalPreferredSpeedInput(input) {
|
|||||||
if (!rawValue) return undefined;
|
if (!rawValue) return undefined;
|
||||||
var parsed = Number(rawValue);
|
var parsed = Number(rawValue);
|
||||||
if (!Number.isFinite(parsed)) return undefined;
|
if (!Number.isFinite(parsed)) return undefined;
|
||||||
return Math.min(16, Math.max(0.0625, parsed));
|
return Math.min(
|
||||||
|
keyBindingUtils.MAX_SPEED,
|
||||||
|
Math.max(keyBindingUtils.MIN_SPEED, parsed)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSiteRuleToggleIcon(toggleButton, action) {
|
function updateSiteRuleToggleIcon(toggleButton, action) {
|
||||||
@@ -560,6 +556,7 @@ function normalizeStoredBinding(binding, fallbackCode) {
|
|||||||
|
|
||||||
var normalized = {
|
var normalized = {
|
||||||
code: normalizedCode,
|
code: normalizedCode,
|
||||||
|
shiftKey: binding.shiftKey === true,
|
||||||
disabled: false
|
disabled: false
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -578,7 +575,7 @@ function formatBindingCode(code) {
|
|||||||
function getBindingLabel(binding) {
|
function getBindingLabel(binding) {
|
||||||
if (!binding) return "";
|
if (!binding) return "";
|
||||||
if (binding.disabled) return "";
|
if (binding.disabled) return "";
|
||||||
return formatBindingCode(binding.code);
|
return (binding.shiftKey ? "Shift+" : "") + formatBindingCode(binding.code);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setShortcutInputBinding(input, binding) {
|
function setShortcutInputBinding(input, binding) {
|
||||||
@@ -591,6 +588,7 @@ function captureBindingFromEvent(event) {
|
|||||||
if (typeof event.code !== "string" || event.code.length === 0) return null;
|
if (typeof event.code !== "string" || event.code.length === 0) return null;
|
||||||
return {
|
return {
|
||||||
code: event.code,
|
code: event.code,
|
||||||
|
shiftKey: event.shiftKey === true,
|
||||||
disabled: false
|
disabled: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -600,6 +598,7 @@ function recordKeyPress(event) {
|
|||||||
|
|
||||||
if (event.key === "Backspace") {
|
if (event.key === "Backspace") {
|
||||||
setShortcutInputBinding(event.target, null);
|
setShortcutInputBinding(event.target, null);
|
||||||
|
scheduleAutoSave();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
return;
|
return;
|
||||||
@@ -607,6 +606,7 @@ function recordKeyPress(event) {
|
|||||||
|
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
setShortcutInputBinding(event.target, createDisabledBinding());
|
setShortcutInputBinding(event.target, createDisabledBinding());
|
||||||
|
scheduleAutoSave();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
return;
|
return;
|
||||||
@@ -616,6 +616,7 @@ function recordKeyPress(event) {
|
|||||||
if (!binding) return;
|
if (!binding) return;
|
||||||
|
|
||||||
setShortcutInputBinding(event.target, binding);
|
setShortcutInputBinding(event.target, binding);
|
||||||
|
scheduleAutoSave();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
@@ -671,6 +672,20 @@ function appendSelectOptions(select, options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function labelShortcutRow(row) {
|
||||||
|
if (!row) return;
|
||||||
|
var action = row.dataset.action;
|
||||||
|
var name = actionLabels[action] || action || "Shortcut";
|
||||||
|
var keyInput = row.querySelector(".customKey");
|
||||||
|
var valueInput = row.querySelector(".customValue");
|
||||||
|
var removeButton = row.querySelector(".removeParent");
|
||||||
|
if (keyInput) keyInput.setAttribute("aria-label", name + " key");
|
||||||
|
if (valueInput) valueInput.setAttribute("aria-label", name + " value");
|
||||||
|
if (removeButton) {
|
||||||
|
removeButton.setAttribute("aria-label", "Remove " + name + " shortcut");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function add_shortcut(action, value) {
|
function add_shortcut(action, value) {
|
||||||
if (!action) return;
|
if (!action) return;
|
||||||
|
|
||||||
@@ -710,6 +725,7 @@ function add_shortcut(action, value) {
|
|||||||
div.appendChild(keyInput);
|
div.appendChild(keyInput);
|
||||||
div.appendChild(valueInput);
|
div.appendChild(valueInput);
|
||||||
div.appendChild(removeButton);
|
div.appendChild(removeButton);
|
||||||
|
labelShortcutRow(div);
|
||||||
|
|
||||||
var customsElement = document.querySelector(".shortcuts-grid");
|
var customsElement = document.querySelector(".shortcuts-grid");
|
||||||
customsElement.appendChild(div);
|
customsElement.appendChild(div);
|
||||||
@@ -770,6 +786,7 @@ function createKeyBindings(item) {
|
|||||||
keyBindings.push({
|
keyBindings.push({
|
||||||
action: action,
|
action: action,
|
||||||
code: binding.code,
|
code: binding.code,
|
||||||
|
shiftKey: binding.shiftKey === true,
|
||||||
disabled: binding.disabled === true,
|
disabled: binding.disabled === true,
|
||||||
value: bindingValue,
|
value: bindingValue,
|
||||||
force: false,
|
force: false,
|
||||||
@@ -791,9 +808,8 @@ function validate() {
|
|||||||
if (pattern.startsWith("/")) {
|
if (pattern.startsWith("/")) {
|
||||||
try {
|
try {
|
||||||
var lastSlash = pattern.lastIndexOf("/");
|
var lastSlash = pattern.lastIndexOf("/");
|
||||||
if (lastSlash > 0) {
|
if (lastSlash === 0) throw new Error("Missing closing slash");
|
||||||
new RegExp(pattern.substring(1, lastSlash), pattern.substring(lastSlash + 1));
|
new RegExp(pattern.substring(1, lastSlash), pattern.substring(lastSlash + 1));
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
status.textContent =
|
status.textContent =
|
||||||
"Error: Invalid site rule regex: " + pattern + ". Unable to save";
|
"Error: Invalid site rule regex: " + pattern + ". Unable to save";
|
||||||
@@ -806,7 +822,10 @@ function validate() {
|
|||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
function save_options() {
|
function save_options(isAutoSave) {
|
||||||
|
clearTimeout(autoSaveTimer);
|
||||||
|
clearTimeout(saveStatusTimer);
|
||||||
|
autoSaveTimer = null;
|
||||||
var status = document.getElementById("status");
|
var status = document.getElementById("status");
|
||||||
if (!optionsSyncSettingsLoaded) {
|
if (!optionsSyncSettingsLoaded) {
|
||||||
status.textContent =
|
status.textContent =
|
||||||
@@ -1092,6 +1111,7 @@ function save_options() {
|
|||||||
shortcuts.push({
|
shortcuts.push({
|
||||||
action: action,
|
action: action,
|
||||||
code: binding.code,
|
code: binding.code,
|
||||||
|
shiftKey: binding.shiftKey === true,
|
||||||
disabled: binding.disabled === true,
|
disabled: binding.disabled === true,
|
||||||
value: shortcutValue,
|
value: shortcutValue,
|
||||||
force: forceCheckbox ? forceCheckbox.checked : false
|
force: forceCheckbox ? forceCheckbox.checked : false
|
||||||
@@ -1117,8 +1137,8 @@ function save_options() {
|
|||||||
status.textContent = "Error: Unable to save options - " + error.message;
|
status.textContent = "Error: Unable to save options - " + error.message;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
status.textContent = "Options saved";
|
status.textContent = isAutoSave === true ? "Auto-saved" : "Options saved";
|
||||||
setTimeout(function () {
|
saveStatusTimer = setTimeout(function () {
|
||||||
status.textContent = "";
|
status.textContent = "";
|
||||||
}, 1000);
|
}, 1000);
|
||||||
});
|
});
|
||||||
@@ -1139,15 +1159,7 @@ function addSiteRuleShortcut(rowsEl, action, binding, value, force) {
|
|||||||
|
|
||||||
var actionLabel = document.createElement("div");
|
var actionLabel = document.createElement("div");
|
||||||
actionLabel.className = "shortcut-label";
|
actionLabel.className = "shortcut-label";
|
||||||
var actionLabelText = actionLabels[action] || action;
|
actionLabel.textContent = actionLabels[action] || action;
|
||||||
if (action === "toggleSubtitleNudge") {
|
|
||||||
var ruleEl = rowsEl.closest(".site-rule");
|
|
||||||
var pattern = ruleEl ? ruleEl.querySelector(".site-pattern").value : "";
|
|
||||||
if (!pattern.toLowerCase().includes("youtube.com")) {
|
|
||||||
actionLabelText += " (only for YouTube embeds)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
actionLabel.textContent = actionLabelText;
|
|
||||||
|
|
||||||
var keyInput = document.createElement("input");
|
var keyInput = document.createElement("input");
|
||||||
keyInput.className = "customKey";
|
keyInput.className = "customKey";
|
||||||
@@ -1195,10 +1207,40 @@ function addSiteRuleShortcut(rowsEl, action, binding, value, force) {
|
|||||||
div.appendChild(valueInput);
|
div.appendChild(valueInput);
|
||||||
div.appendChild(forceLabel);
|
div.appendChild(forceLabel);
|
||||||
div.appendChild(removeButton);
|
div.appendChild(removeButton);
|
||||||
|
labelShortcutRow(div);
|
||||||
|
|
||||||
rowsEl.appendChild(div);
|
rowsEl.appendChild(div);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var siteRuleControlId = 0;
|
||||||
|
|
||||||
|
function associateSiteRuleLabels(ruleEl) {
|
||||||
|
ruleEl.querySelectorAll(".site-rule-option").forEach(function(option) {
|
||||||
|
var label = Array.from(option.children).find(function(child) {
|
||||||
|
return child.tagName === "LABEL";
|
||||||
|
});
|
||||||
|
var controls = option.querySelectorAll("input, select, textarea");
|
||||||
|
if (!label || controls.length !== 1 || label.contains(controls[0])) return;
|
||||||
|
controls[0].id = "site-rule-control-" + ++siteRuleControlId;
|
||||||
|
label.htmlFor = controls[0].id;
|
||||||
|
});
|
||||||
|
|
||||||
|
ruleEl.querySelectorAll(".margin-pad-cell").forEach(function(cell) {
|
||||||
|
var input = cell.querySelector("input");
|
||||||
|
var miniLabel = cell.querySelector(".margin-pad-mini");
|
||||||
|
if (input && miniLabel) {
|
||||||
|
input.setAttribute(
|
||||||
|
"aria-label",
|
||||||
|
"Controller margin " +
|
||||||
|
(miniLabel.textContent === "T" ? "top" : "bottom")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var removeRule = ruleEl.querySelector(".remove-site-rule");
|
||||||
|
if (removeRule) removeRule.setAttribute("aria-label", "Remove site rule");
|
||||||
|
}
|
||||||
|
|
||||||
function createSiteRule(rule) {
|
function createSiteRule(rule) {
|
||||||
var template = document.getElementById("siteRuleTemplate");
|
var template = document.getElementById("siteRuleTemplate");
|
||||||
var clone = template.content.cloneNode(true);
|
var clone = template.content.cloneNode(true);
|
||||||
@@ -1371,6 +1413,7 @@ function createSiteRule(rule) {
|
|||||||
}
|
}
|
||||||
applySiteRuleOverrideState(ruleEl, "override-shortcuts", "site-shortcuts-container");
|
applySiteRuleOverrideState(ruleEl, "override-shortcuts", "site-shortcuts-container");
|
||||||
refreshSiteRuleAddShortcutSelector(ruleEl);
|
refreshSiteRuleAddShortcutSelector(ruleEl);
|
||||||
|
associateSiteRuleLabels(ruleEl);
|
||||||
|
|
||||||
document.getElementById("siteRulesContainer").appendChild(ruleEl);
|
document.getElementById("siteRulesContainer").appendChild(ruleEl);
|
||||||
}
|
}
|
||||||
@@ -1379,7 +1422,8 @@ function createControlBarBlock(buttonId) {
|
|||||||
var def = controllerButtonDefs[buttonId];
|
var def = controllerButtonDefs[buttonId];
|
||||||
if (!def) return null;
|
if (!def) return null;
|
||||||
|
|
||||||
var block = document.createElement("div");
|
var block = document.createElement("button");
|
||||||
|
block.type = "button";
|
||||||
block.className = "cb-block";
|
block.className = "cb-block";
|
||||||
block.dataset.buttonId = buttonId;
|
block.dataset.buttonId = buttonId;
|
||||||
block.draggable = true;
|
block.draggable = true;
|
||||||
@@ -1402,6 +1446,25 @@ function createControlBarBlock(buttonId) {
|
|||||||
return block;
|
return block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateControlBarBlockLabels(editor) {
|
||||||
|
if (!editor) return;
|
||||||
|
editor.querySelectorAll(".cb-dropzone").forEach(function(zone) {
|
||||||
|
var state = zone.classList.contains("cb-active-zone")
|
||||||
|
? "active"
|
||||||
|
: "available";
|
||||||
|
zone.querySelectorAll(".cb-block").forEach(function(block) {
|
||||||
|
var def = controllerButtonDefs[block.dataset.buttonId];
|
||||||
|
block.setAttribute(
|
||||||
|
"aria-label",
|
||||||
|
(def ? def.name : block.dataset.buttonId) +
|
||||||
|
", " +
|
||||||
|
state +
|
||||||
|
". Press Enter to move; use arrow keys to reorder."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function populateControlBarZones(activeZone, availableZone, activeIds, allowButtonId) {
|
function populateControlBarZones(activeZone, availableZone, activeIds, allowButtonId) {
|
||||||
vscClearElement(activeZone);
|
vscClearElement(activeZone);
|
||||||
vscClearElement(availableZone);
|
vscClearElement(availableZone);
|
||||||
@@ -1424,6 +1487,8 @@ function populateControlBarZones(activeZone, availableZone, activeIds, allowButt
|
|||||||
if (block) availableZone.appendChild(block);
|
if (block) availableZone.appendChild(block);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateControlBarBlockLabels(activeZone.closest(".cb-editor"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function readControlBarOrder(activeZone) {
|
function readControlBarOrder(activeZone) {
|
||||||
@@ -1467,11 +1532,11 @@ function updatePopupEditorDisabledState() {
|
|||||||
var checkbox = document.getElementById("popupMatchHoverControls");
|
var checkbox = document.getElementById("popupMatchHoverControls");
|
||||||
var wrap = document.getElementById("popupCbEditorWrap");
|
var wrap = document.getElementById("popupCbEditorWrap");
|
||||||
if (!checkbox || !wrap) return;
|
if (!checkbox || !wrap) return;
|
||||||
if (checkbox.checked) {
|
wrap.classList.toggle("cb-editor-disabled", checkbox.checked);
|
||||||
wrap.classList.add("cb-editor-disabled");
|
wrap.querySelectorAll(".cb-block").forEach(function(block) {
|
||||||
} else {
|
block.disabled = checkbox.checked;
|
||||||
wrap.classList.remove("cb-editor-disabled");
|
block.draggable = !checkbox.checked;
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDragAfterElement(container, x, y) {
|
function getDragAfterElement(container, x, y) {
|
||||||
@@ -1494,6 +1559,8 @@ function getDragAfterElement(container, x, y) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initControlBarEditor() {
|
function initControlBarEditor() {
|
||||||
|
if (document.vscControlBarEditorInitialized) return;
|
||||||
|
document.vscControlBarEditorInitialized = true;
|
||||||
var draggedBlock = null;
|
var draggedBlock = null;
|
||||||
|
|
||||||
function clearControlBarDropTargets(activeZone) {
|
function clearControlBarDropTargets(activeZone) {
|
||||||
@@ -1506,7 +1573,7 @@ function initControlBarEditor() {
|
|||||||
|
|
||||||
document.addEventListener("dragstart", function (e) {
|
document.addEventListener("dragstart", function (e) {
|
||||||
var block = e.target.closest(".cb-block");
|
var block = e.target.closest(".cb-block");
|
||||||
if (!block) return;
|
if (!block || block.disabled) return;
|
||||||
draggedBlock = block;
|
draggedBlock = block;
|
||||||
e.dataTransfer.effectAllowed = "move";
|
e.dataTransfer.effectAllowed = "move";
|
||||||
e.dataTransfer.setData("text/plain", block.dataset.buttonId);
|
e.dataTransfer.setData("text/plain", block.dataset.buttonId);
|
||||||
@@ -1551,10 +1618,49 @@ function initControlBarEditor() {
|
|||||||
var zone = e.target.closest(".cb-dropzone");
|
var zone = e.target.closest(".cb-dropzone");
|
||||||
if (zone) {
|
if (zone) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
updateControlBarBlockLabels(zone.closest(".cb-editor"));
|
||||||
|
scheduleAutoSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearControlBarDropTargets(null);
|
clearControlBarDropTargets(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener("click", function(e) {
|
||||||
|
var block = e.target.closest ? e.target.closest(".cb-block") : null;
|
||||||
|
if (!block || block.disabled) return;
|
||||||
|
var editor = block.closest(".cb-editor");
|
||||||
|
var currentZone = block.closest(".cb-dropzone");
|
||||||
|
if (!editor || !currentZone) return;
|
||||||
|
var otherZone = editor.querySelector(
|
||||||
|
currentZone.classList.contains("cb-active-zone")
|
||||||
|
? ".cb-available-zone"
|
||||||
|
: ".cb-active-zone"
|
||||||
|
);
|
||||||
|
if (!otherZone) return;
|
||||||
|
otherZone.appendChild(block);
|
||||||
|
updateControlBarBlockLabels(editor);
|
||||||
|
block.focus();
|
||||||
|
scheduleAutoSave();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", function(e) {
|
||||||
|
var block = e.target.closest ? e.target.closest(".cb-block") : null;
|
||||||
|
if (!block || block.disabled) return;
|
||||||
|
var previous = e.key === "ArrowLeft" || e.key === "ArrowUp";
|
||||||
|
var next = e.key === "ArrowRight" || e.key === "ArrowDown";
|
||||||
|
if (!previous && !next) return;
|
||||||
|
var sibling = previous ? block.previousElementSibling : block.nextElementSibling;
|
||||||
|
if (!sibling) return;
|
||||||
|
e.preventDefault();
|
||||||
|
if (previous) {
|
||||||
|
block.parentNode.insertBefore(block, sibling);
|
||||||
|
} else {
|
||||||
|
block.parentNode.insertBefore(block, sibling.nextElementSibling);
|
||||||
|
}
|
||||||
|
updateControlBarBlockLabels(block.closest(".cb-editor"));
|
||||||
|
block.focus();
|
||||||
|
scheduleAutoSave();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var lucidePickerSelectedSlug = null;
|
var lucidePickerSelectedSlug = null;
|
||||||
@@ -1626,6 +1732,10 @@ function initLucideButtonIconsUI() {
|
|||||||
b.dataset.slug = slug;
|
b.dataset.slug = slug;
|
||||||
b.title = slug;
|
b.title = slug;
|
||||||
b.setAttribute("aria-label", slug);
|
b.setAttribute("aria-label", slug);
|
||||||
|
b.setAttribute(
|
||||||
|
"aria-pressed",
|
||||||
|
slug === lucidePickerSelectedSlug ? "true" : "false"
|
||||||
|
);
|
||||||
if (slug === lucidePickerSelectedSlug) {
|
if (slug === lucidePickerSelectedSlug) {
|
||||||
b.classList.add("lucide-picked");
|
b.classList.add("lucide-picked");
|
||||||
}
|
}
|
||||||
@@ -1647,6 +1757,10 @@ function initLucideButtonIconsUI() {
|
|||||||
resultsEl.querySelectorAll("button"),
|
resultsEl.querySelectorAll("button"),
|
||||||
function (x) {
|
function (x) {
|
||||||
x.classList.toggle("lucide-picked", x.dataset.slug === slug);
|
x.classList.toggle("lucide-picked", x.dataset.slug === slug);
|
||||||
|
x.setAttribute(
|
||||||
|
"aria-pressed",
|
||||||
|
x.dataset.slug === slug ? "true" : "false"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
fetchLucideSvg(slug)
|
fetchLucideSvg(slug)
|
||||||
@@ -1712,7 +1826,7 @@ function initLucideButtonIconsUI() {
|
|||||||
slug +
|
slug +
|
||||||
" for " +
|
" for " +
|
||||||
action +
|
action +
|
||||||
". Reload pages for the hover bar."
|
". Open pages update automatically."
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -1825,13 +1939,16 @@ function restore_options(callback) {
|
|||||||
|
|
||||||
document.querySelectorAll(".customs:not([id])").forEach((row) => row.remove());
|
document.querySelectorAll(".customs:not([id])").forEach((row) => row.remove());
|
||||||
|
|
||||||
|
var usedDefaultShortcutRows = new Set();
|
||||||
storage.keyBindings.forEach((item) => {
|
storage.keyBindings.forEach((item) => {
|
||||||
var row = document.getElementById(item.action);
|
var row = document.getElementById(item.action);
|
||||||
var normalizedBinding = normalizeStoredBinding(item);
|
var normalizedBinding = normalizeStoredBinding(item);
|
||||||
|
|
||||||
if (!row) {
|
if (!row || usedDefaultShortcutRows.has(item.action)) {
|
||||||
add_shortcut(item.action, item.value);
|
add_shortcut(item.action, item.value);
|
||||||
row = document.querySelector(".shortcut-row.customs:last-of-type");
|
row = document.querySelector(".shortcut-row.customs:last-of-type");
|
||||||
|
} else {
|
||||||
|
usedDefaultShortcutRows.add(item.action);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!row) return;
|
if (!row) return;
|
||||||
@@ -1892,6 +2009,13 @@ function restore_options(callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function restore_defaults() {
|
function restore_defaults() {
|
||||||
|
if (
|
||||||
|
!window.confirm(
|
||||||
|
"Restore all defaults? This removes saved preferences, remembered speeds, and custom icons."
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
var status = document.getElementById("status");
|
var status = document.getElementById("status");
|
||||||
var restoreButton = document.getElementById("restore");
|
var restoreButton = document.getElementById("restore");
|
||||||
setOptionsSyncSettingsLoaded(false);
|
setOptionsSyncSettingsLoaded(false);
|
||||||
@@ -1971,6 +2095,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
versionElement.textContent = manifest.version;
|
versionElement.textContent = manifest.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll("#customs .shortcut-row").forEach(labelShortcutRow);
|
||||||
|
|
||||||
restore_options();
|
restore_options();
|
||||||
initControlBarEditor();
|
initControlBarEditor();
|
||||||
|
|
||||||
@@ -1978,6 +2104,41 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
.addEventListener("change", updatePopupEditorDisabledState);
|
.addEventListener("change", updatePopupEditorDisabledState);
|
||||||
|
|
||||||
document.getElementById("save").addEventListener("click", save_options);
|
document.getElementById("save").addEventListener("click", save_options);
|
||||||
|
document.getElementById("copyDiagnostics").addEventListener("click", function () {
|
||||||
|
var status = document.getElementById("status");
|
||||||
|
if (
|
||||||
|
!navigator.clipboard ||
|
||||||
|
typeof navigator.clipboard.writeText !== "function"
|
||||||
|
) {
|
||||||
|
status.textContent = "Clipboard access is unavailable.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var report = popupControlUtils.buildDiagnosticReport({
|
||||||
|
speederVersion: manifest.version,
|
||||||
|
browser: navigator.userAgent,
|
||||||
|
platform: navigator.platform || null,
|
||||||
|
storage: {
|
||||||
|
enabled: document.getElementById("enabled").checked,
|
||||||
|
rememberSpeed: document.getElementById("rememberSpeed").checked,
|
||||||
|
forceLastSavedSpeed:
|
||||||
|
document.getElementById("forceLastSavedSpeed").checked,
|
||||||
|
audioBoolean: document.getElementById("audioBoolean").checked,
|
||||||
|
startHidden: document.getElementById("startHidden").checked,
|
||||||
|
hideWithControls: document.getElementById("hideWithControls").checked,
|
||||||
|
controllerLocation: document.getElementById("controllerLocation").value,
|
||||||
|
shortcutTargetMode:
|
||||||
|
document.getElementById("shortcutTargetMode").value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
navigator.clipboard.writeText(report).then(
|
||||||
|
function() {
|
||||||
|
status.textContent = "Diagnostics copied. Review before sharing.";
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
status.textContent = "Could not copy diagnostics.";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const addSelector = document.getElementById("addShortcutSelector");
|
const addSelector = document.getElementById("addShortcutSelector");
|
||||||
if (addSelector) {
|
if (addSelector) {
|
||||||
@@ -2030,11 +2191,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
if (siteRuleForShortcut) {
|
if (siteRuleForShortcut) {
|
||||||
refreshSiteRuleAddShortcutSelector(siteRuleForShortcut);
|
refreshSiteRuleAddShortcutSelector(siteRuleForShortcut);
|
||||||
}
|
}
|
||||||
|
scheduleAutoSave();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var removeSiteRuleButton = targetEl.closest(".remove-site-rule");
|
var removeSiteRuleButton = targetEl.closest(".remove-site-rule");
|
||||||
if (removeSiteRuleButton) {
|
if (removeSiteRuleButton) {
|
||||||
removeSiteRuleButton.closest(".site-rule").remove();
|
removeSiteRuleButton.closest(".site-rule").remove();
|
||||||
|
scheduleAutoSave();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var toggleButton = targetEl.closest(".toggle-site-rule");
|
var toggleButton = targetEl.closest(".toggle-site-rule");
|
||||||
@@ -2104,4 +2267,12 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
document.addEventListener("change", (event) => {
|
||||||
|
if (
|
||||||
|
event.target.id !== "addShortcutSelector" &&
|
||||||
|
!event.target.closest("#lucideIconSettings")
|
||||||
|
) {
|
||||||
|
scheduleAutoSave();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+23
-10
@@ -4,8 +4,9 @@
|
|||||||
--border: #e2e5e9;
|
--border: #e2e5e9;
|
||||||
--border-strong: #d4d9e0;
|
--border-strong: #d4d9e0;
|
||||||
--text: #17191c;
|
--text: #17191c;
|
||||||
--muted: #626b76;
|
--muted: #626b76;
|
||||||
--accent: #111827;
|
--accent: #111827;
|
||||||
|
--focus-ring: #2563eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -83,10 +84,15 @@ button:active {
|
|||||||
background: #f1f3f5;
|
background: #f1f3f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:focus-visible {
|
button:focus-visible {
|
||||||
outline: 2px solid rgba(17, 24, 39, 0.14);
|
outline: 2px solid var(--focus-ring);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
#refresh {
|
#refresh {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
@@ -112,6 +118,12 @@ button:focus-visible {
|
|||||||
border-color: #9fbd98;
|
border-color: #9fbd98;
|
||||||
color: #285d21;
|
color: #285d21;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pauseTab[aria-pressed="true"] {
|
||||||
|
background: #fff3d6;
|
||||||
|
border-color: #d8b66b;
|
||||||
|
color: #704f0d;
|
||||||
|
}
|
||||||
|
|
||||||
.popup-divider {
|
.popup-divider {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
@@ -250,8 +262,8 @@ button:focus-visible {
|
|||||||
background: #f1f3f5;
|
background: #f1f3f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.donate-icon-btn:focus-visible {
|
.donate-icon-btn:focus-visible {
|
||||||
outline: 2px solid rgba(17, 24, 39, 0.14);
|
outline: 2px solid var(--focus-ring);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -295,8 +307,9 @@ button:focus-visible {
|
|||||||
--border: #2b3138;
|
--border: #2b3138;
|
||||||
--border-strong: #3a414a;
|
--border-strong: #3a414a;
|
||||||
--text: #f2f4f6;
|
--text: #f2f4f6;
|
||||||
--muted: #a0a8b2;
|
--muted: #a0a8b2;
|
||||||
--accent: #f2f4f6;
|
--accent: #f2f4f6;
|
||||||
|
--focus-ring: #93c5fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|||||||
@@ -24,6 +24,12 @@
|
|||||||
type="button"
|
type="button"
|
||||||
aria-pressed="false"
|
aria-pressed="false"
|
||||||
>Force last saved speed</button>
|
>Force last saved speed</button>
|
||||||
|
<button
|
||||||
|
id="pauseTab"
|
||||||
|
class="popup-compact-action"
|
||||||
|
type="button"
|
||||||
|
aria-pressed="false"
|
||||||
|
>Pause on this tab</button>
|
||||||
<div class="popup-divider"></div>
|
<div class="popup-divider"></div>
|
||||||
<div id="popupControlBar" class="popup-control-bar">
|
<div id="popupControlBar" class="popup-control-bar">
|
||||||
<span id="popupSpeed" class="popup-speed">1.00</span>
|
<span id="popupSpeed" class="popup-speed">1.00</span>
|
||||||
@@ -32,10 +38,17 @@
|
|||||||
<button id="enable" class="hide">Enable</button>
|
<button id="enable" class="hide">Enable</button>
|
||||||
<button id="disable">Disable</button>
|
<button id="disable">Disable</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="status" class="popup-status hide"></div>
|
<div
|
||||||
|
id="status"
|
||||||
|
class="popup-status hide"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
></div>
|
||||||
<div class="popup-links">
|
<div class="popup-links">
|
||||||
<button id="config">Settings</button>
|
<button id="config">Settings</button>
|
||||||
<div class="popup-secondary">
|
<div class="popup-secondary">
|
||||||
|
<button id="addSiteRule" class="secondary">Add current site</button>
|
||||||
|
<button id="copyDiagnostics" class="secondary">Copy diagnostics</button>
|
||||||
<button id="feedback" class="secondary">Feedback</button>
|
<button id="feedback" class="secondary">Feedback</button>
|
||||||
<button id="about" class="secondary">About</button>
|
<button id="about" class="secondary">About</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+208
-16
@@ -1,4 +1,6 @@
|
|||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
if (window.vscPopupInitialized) return;
|
||||||
|
window.vscPopupInitialized = true;
|
||||||
var speederShared =
|
var speederShared =
|
||||||
typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
|
typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
|
||||||
var siteRuleUtils = speederShared.siteRules || {};
|
var siteRuleUtils = speederShared.siteRules || {};
|
||||||
@@ -27,6 +29,63 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
var popupExcludedButtonIds = new Set(["settings"]);
|
var popupExcludedButtonIds = new Set(["settings"]);
|
||||||
var renderToken = 0;
|
var renderToken = 0;
|
||||||
var forceLastSavedSpeedControlledBySiteRule = null;
|
var forceLastSavedSpeedControlledBySiteRule = null;
|
||||||
|
var selectedFrameToken = null;
|
||||||
|
var shortcutTargetMode = "closest";
|
||||||
|
var diagnosticContext = null;
|
||||||
|
|
||||||
|
function updateTabPauseButton(paused) {
|
||||||
|
var button = document.querySelector("#pauseTab");
|
||||||
|
button.setAttribute("aria-pressed", paused ? "true" : "false");
|
||||||
|
button.textContent = paused ? "Resume on this tab" : "Pause on this tab";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTabPauseState(tab, callback) {
|
||||||
|
if (!tab || !Number.isInteger(tab.id)) {
|
||||||
|
callback(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
{ action: "get_tab_pause_state", tabId: tab.id },
|
||||||
|
function(response) {
|
||||||
|
callback(
|
||||||
|
!chrome.runtime.lastError && response && response.paused === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentSiteRuleDetails(url) {
|
||||||
|
try {
|
||||||
|
var parsed = new URL(url);
|
||||||
|
if (
|
||||||
|
(parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
|
||||||
|
!parsed.host
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
title: parsed.host,
|
||||||
|
pattern: parsed.protocol + "//" + parsed.host
|
||||||
|
};
|
||||||
|
} catch (_error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDiagnosticReport() {
|
||||||
|
if (!diagnosticContext) return null;
|
||||||
|
return popupControlUtils.buildDiagnosticReport({
|
||||||
|
speederVersion: chrome.runtime.getManifest().version,
|
||||||
|
browser: navigator.userAgent,
|
||||||
|
platform: navigator.platform || null,
|
||||||
|
url: diagnosticContext.url,
|
||||||
|
storage: diagnosticContext.storage,
|
||||||
|
siteRule: diagnosticContext.siteRule,
|
||||||
|
siteRuleDisabled: isSiteRuleDisabled(diagnosticContext.siteRule),
|
||||||
|
tabPaused: diagnosticContext.tabPaused,
|
||||||
|
frame: diagnosticContext.frame
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function persistExpandedSettings(rawStorage, settings, callback) {
|
function persistExpandedSettings(rawStorage, settings, callback) {
|
||||||
var mutation = vscBuildManagedStorageMutation(rawStorage, settings);
|
var mutation = vscBuildManagedStorageMutation(rawStorage, settings);
|
||||||
@@ -123,16 +182,23 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getActiveTabContext(callback) {
|
function getActiveTabContext(callback) {
|
||||||
|
function finish(context) {
|
||||||
|
getTabPauseState(context.tab, function(paused) {
|
||||||
|
context.tabPaused = paused;
|
||||||
|
if (callback) callback(context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
|
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
|
||||||
var activeTab = tabs && tabs[0] ? tabs[0] : null;
|
var activeTab = tabs && tabs[0] ? tabs[0] : null;
|
||||||
if (!activeTab || !activeTab.id) {
|
if (!activeTab || !activeTab.id) {
|
||||||
if (callback) callback({ tab: null, url: "" });
|
finish({ tab: null, url: "" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tabUrl = typeof activeTab.url === "string" ? activeTab.url : "";
|
var tabUrl = typeof activeTab.url === "string" ? activeTab.url : "";
|
||||||
if (tabUrl.length > 0) {
|
if (tabUrl.length > 0) {
|
||||||
if (callback) callback({ tab: activeTab, url: tabUrl });
|
finish({ tab: activeTab, url: tabUrl });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,13 +207,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
{ action: "get_page_context" },
|
{ action: "get_page_context" },
|
||||||
function (response) {
|
function (response) {
|
||||||
if (chrome.runtime.lastError) {
|
if (chrome.runtime.lastError) {
|
||||||
if (callback) callback({ tab: activeTab, url: "" });
|
finish({ tab: activeTab, url: "" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var pageUrl =
|
var pageUrl =
|
||||||
response && typeof response.url === "string" ? response.url : "";
|
response && typeof response.url === "string" ? response.url : "";
|
||||||
if (callback) callback({ tab: activeTab, url: pageUrl });
|
finish({ tab: activeTab, url: pageUrl });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -162,6 +228,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
if (response && response.speed != null) {
|
if (response && response.speed != null) {
|
||||||
updateSpeedDisplay(response.speed);
|
updateSpeedDisplay(response.speed);
|
||||||
}
|
}
|
||||||
|
selectedFrameToken =
|
||||||
|
response && typeof response.frameToken === "string"
|
||||||
|
? response.frameToken
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickBestFrameSpeedResult(results) {
|
function pickBestFrameSpeedResult(results) {
|
||||||
@@ -241,14 +311,19 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
}
|
}
|
||||||
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);
|
||||||
|
btn.setAttribute("aria-label", btn.title);
|
||||||
|
|
||||||
btn.addEventListener("click", function () {
|
btn.addEventListener("click", function () {
|
||||||
if (btnId === "settings") {
|
if (btnId === "settings") {
|
||||||
window.open(chrome.runtime.getURL("options/options.html"));
|
window.open(chrome.runtime.getURL("options/options.html"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var message = { action: "run_action", actionName: btnId };
|
||||||
|
if (shortcutTargetMode !== "all" && selectedFrameToken) {
|
||||||
|
message.targetFrameToken = selectedFrameToken;
|
||||||
|
}
|
||||||
sendToActiveTab(
|
sendToActiveTab(
|
||||||
{ action: "run_action", actionName: btnId },
|
message,
|
||||||
function () {
|
function () {
|
||||||
querySpeed();
|
querySpeed();
|
||||||
}
|
}
|
||||||
@@ -277,6 +352,104 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
window.open("https://github.com/SoPat712/Speeder/issues");
|
window.open("https://github.com/SoPat712/Speeder/issues");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.querySelector("#copyDiagnostics").addEventListener("click", function () {
|
||||||
|
var report = buildDiagnosticReport();
|
||||||
|
if (!report) {
|
||||||
|
setStatusMessage("Diagnostics are still loading.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!navigator.clipboard || typeof navigator.clipboard.writeText !== "function") {
|
||||||
|
setStatusMessage("Clipboard access is unavailable.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigator.clipboard.writeText(report).then(
|
||||||
|
function () {
|
||||||
|
setStatusMessage("Diagnostics copied. Review before sharing.");
|
||||||
|
},
|
||||||
|
function () {
|
||||||
|
setStatusMessage("Could not copy diagnostics.");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector("#addSiteRule").addEventListener("click", function () {
|
||||||
|
var details = diagnosticContext
|
||||||
|
? getCurrentSiteRuleDetails(diagnosticContext.url)
|
||||||
|
: null;
|
||||||
|
if (!details) {
|
||||||
|
setStatusMessage("A site rule cannot be created for this page.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var added = false;
|
||||||
|
|
||||||
|
updateStoredSettings(
|
||||||
|
function (settings) {
|
||||||
|
var rules = Array.isArray(settings.siteRules) ? settings.siteRules : [];
|
||||||
|
if (
|
||||||
|
rules.some(function(rule) {
|
||||||
|
return rule && rule.pattern === details.pattern;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settings.siteRules = rules.concat([
|
||||||
|
{
|
||||||
|
title: details.title,
|
||||||
|
pattern: details.pattern,
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
added = true;
|
||||||
|
},
|
||||||
|
function (error) {
|
||||||
|
if (error) {
|
||||||
|
setStatusMessage("Could not add this site: " + error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStatusMessage(
|
||||||
|
added
|
||||||
|
? "Site rule added. Opening settings..."
|
||||||
|
: "Site rule already exists. Opening settings..."
|
||||||
|
);
|
||||||
|
window.open(chrome.runtime.getURL("options/options.html"));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector("#pauseTab").addEventListener("click", function () {
|
||||||
|
var tab = diagnosticContext && diagnosticContext.tab;
|
||||||
|
if (!tab || !Number.isInteger(tab.id)) {
|
||||||
|
setStatusMessage("This page cannot be paused.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var button = this;
|
||||||
|
var paused = button.getAttribute("aria-pressed") !== "true";
|
||||||
|
button.disabled = true;
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
{ action: "set_tab_paused", tabId: tab.id, paused: paused },
|
||||||
|
function(response) {
|
||||||
|
button.disabled = false;
|
||||||
|
if (chrome.runtime.lastError || !response) {
|
||||||
|
setStatusMessage("Could not update this tab.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
diagnosticContext.tabPaused = response.paused === true;
|
||||||
|
updateTabPauseButton(diagnosticContext.tabPaused);
|
||||||
|
setControlBarVisible(
|
||||||
|
!diagnosticContext.tabPaused &&
|
||||||
|
diagnosticContext.siteAvailable &&
|
||||||
|
diagnosticContext.showBar
|
||||||
|
);
|
||||||
|
setForceButtonLoading(diagnosticContext.tabPaused);
|
||||||
|
setStatusMessage(
|
||||||
|
diagnosticContext.tabPaused
|
||||||
|
? "Speeder is paused for this tab."
|
||||||
|
: "Speeder resumed for this tab."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
document.querySelector("#donate").addEventListener("click", function () {
|
document.querySelector("#donate").addEventListener("click", function () {
|
||||||
this.classList.add("hide");
|
this.classList.add("hide");
|
||||||
document.querySelector("#donateOptions").classList.remove("hide");
|
document.querySelector("#donateOptions").classList.remove("hide");
|
||||||
@@ -372,6 +545,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
function renderForActiveTab() {
|
function renderForActiveTab() {
|
||||||
var currentRenderToken = ++renderToken;
|
var currentRenderToken = ++renderToken;
|
||||||
forceLastSavedSpeedControlledBySiteRule = null;
|
forceLastSavedSpeedControlledBySiteRule = null;
|
||||||
|
selectedFrameToken = null;
|
||||||
setForceButtonLoading(true);
|
setForceButtonLoading(true);
|
||||||
|
|
||||||
chrome.storage.local.get(["customButtonIcons"], function (loc) {
|
chrome.storage.local.get(["customButtonIcons"], function (loc) {
|
||||||
@@ -388,6 +562,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
getActiveTabContext(function (context) {
|
getActiveTabContext(function (context) {
|
||||||
if (currentRenderToken !== renderToken) return;
|
if (currentRenderToken !== renderToken) return;
|
||||||
|
|
||||||
|
var tabPaused = context && context.tabPaused === true;
|
||||||
|
updateTabPauseButton(tabPaused);
|
||||||
|
|
||||||
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 siteDisabled = isSiteRuleDisabled(siteRule);
|
var siteDisabled = isSiteRuleDisabled(siteRule);
|
||||||
@@ -395,6 +572,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
storage.enabled,
|
storage.enabled,
|
||||||
siteRule
|
siteRule
|
||||||
);
|
);
|
||||||
|
shortcutTargetMode =
|
||||||
|
siteRule && siteRule.shortcutTargetMode !== undefined
|
||||||
|
? siteRule.shortcutTargetMode
|
||||||
|
: storage.shortcutTargetMode;
|
||||||
var showBar = storage.showPopupControlBar !== false;
|
var showBar = storage.showPopupControlBar !== false;
|
||||||
forceLastSavedSpeedControlledBySiteRule = Boolean(
|
forceLastSavedSpeedControlledBySiteRule = Boolean(
|
||||||
siteRule && siteRule.forceLastSavedSpeed !== undefined
|
siteRule && siteRule.forceLastSavedSpeed !== undefined
|
||||||
@@ -403,9 +584,22 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
forceLastSavedSpeedControlledBySiteRule
|
forceLastSavedSpeedControlledBySiteRule
|
||||||
? siteRule.forceLastSavedSpeed === true
|
? siteRule.forceLastSavedSpeed === true
|
||||||
: storage.forceLastSavedSpeed === true;
|
: storage.forceLastSavedSpeed === true;
|
||||||
|
diagnosticContext = {
|
||||||
|
tab: context && context.tab,
|
||||||
|
url: url,
|
||||||
|
storage: storage,
|
||||||
|
siteRule: siteRule,
|
||||||
|
frame: null,
|
||||||
|
tabPaused: tabPaused,
|
||||||
|
siteAvailable: siteAvailable,
|
||||||
|
showBar: showBar
|
||||||
|
};
|
||||||
|
document.querySelector("#addSiteRule").disabled =
|
||||||
|
!getCurrentSiteRuleDetails(url);
|
||||||
|
|
||||||
if (siteRule && siteRule.showPopupControlBar !== undefined) {
|
if (siteRule && siteRule.showPopupControlBar !== undefined) {
|
||||||
showBar = siteRule.showPopupControlBar;
|
showBar = siteRule.showPopupControlBar;
|
||||||
|
diagnosticContext.showBar = showBar;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleEnabledUI(storage.enabled !== false);
|
toggleEnabledUI(storage.enabled !== false);
|
||||||
@@ -414,7 +608,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
resolvePopupButtons(storage, siteRule),
|
resolvePopupButtons(storage, siteRule),
|
||||||
customIconsMap
|
customIconsMap
|
||||||
);
|
);
|
||||||
setControlBarVisible(siteAvailable && showBar);
|
setControlBarVisible(!tabPaused && siteAvailable && showBar);
|
||||||
|
|
||||||
|
if (tabPaused) {
|
||||||
|
setForceButtonLoading(true);
|
||||||
|
setStatusMessage("Speeder is paused for this tab.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (siteDisabled) {
|
if (siteDisabled) {
|
||||||
setForceButtonLoading(false);
|
setForceButtonLoading(false);
|
||||||
@@ -427,6 +627,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
if (siteAvailable) {
|
if (siteAvailable) {
|
||||||
querySpeed(function(frameContext) {
|
querySpeed(function(frameContext) {
|
||||||
if (currentRenderToken !== renderToken) return;
|
if (currentRenderToken !== renderToken) return;
|
||||||
|
diagnosticContext.frame = frameContext || null;
|
||||||
if (
|
if (
|
||||||
frameContext &&
|
frameContext &&
|
||||||
typeof frameContext.forceLastSavedSpeed === "boolean"
|
typeof frameContext.forceLastSavedSpeed === "boolean"
|
||||||
@@ -507,20 +708,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
function toggleEnabledUI(enabled) {
|
function toggleEnabledUI(enabled) {
|
||||||
document.querySelector("#enable").classList.toggle("hide", enabled);
|
document.querySelector("#enable").classList.toggle("hide", enabled);
|
||||||
document.querySelector("#disable").classList.toggle("hide", !enabled);
|
document.querySelector("#disable").classList.toggle("hide", !enabled);
|
||||||
|
|
||||||
const suffix = `${enabled ? "" : "_disabled"}.png`;
|
|
||||||
chrome.browserAction.setIcon({
|
|
||||||
path: {
|
|
||||||
19: "assets/icons/icon19" + suffix,
|
|
||||||
38: "assets/icons/icon38" + suffix,
|
|
||||||
48: "assets/icons/icon48" + suffix
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function settingsSavedReloadMessage(enabled) {
|
function settingsSavedReloadMessage(enabled) {
|
||||||
setStatusMessage(
|
setStatusMessage(
|
||||||
`${enabled ? "Enabled" : "Disabled"}. Reload page to see changes`
|
`${enabled ? "Enabled" : "Disabled"}. Open pages update automatically.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
"popupControllerButtons",
|
"popupControllerButtons",
|
||||||
"popupMatchHoverControls",
|
"popupMatchHoverControls",
|
||||||
"rememberSpeed",
|
"rememberSpeed",
|
||||||
|
"shortcutTargetMode",
|
||||||
|
"showAmbientLoopControls",
|
||||||
"showPopupControlBar",
|
"showPopupControlBar",
|
||||||
"siteRules",
|
"siteRules",
|
||||||
"siteRulesFormat",
|
"siteRulesFormat",
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
root.SpeederShared = root.SpeederShared || {};
|
root.SpeederShared = root.SpeederShared || {};
|
||||||
root.SpeederShared.keyBindings = exports;
|
root.SpeederShared.keyBindings = exports;
|
||||||
})(typeof globalThis !== "undefined" ? globalThis : this, function() {
|
})(typeof globalThis !== "undefined" ? globalThis : this, function() {
|
||||||
|
var MIN_SPEED = 0.1;
|
||||||
|
var MAX_SPEED = 16;
|
||||||
|
|
||||||
function normalizeBindingKey(key) {
|
function normalizeBindingKey(key) {
|
||||||
if (typeof key !== "string" || key.length === 0) return null;
|
if (typeof key !== "string" || key.length === 0) return null;
|
||||||
if (key === "Spacebar") return " ";
|
if (key === "Spacebar") return " ";
|
||||||
@@ -127,15 +130,15 @@
|
|||||||
if (!Number.isFinite(numericValue)) return "must be a finite number";
|
if (!Number.isFinite(numericValue)) return "must be a finite number";
|
||||||
if (
|
if (
|
||||||
(action === "slower" || action === "faster") &&
|
(action === "slower" || action === "faster") &&
|
||||||
(numericValue <= 0 || numericValue > 16)
|
(numericValue <= 0 || numericValue > MAX_SPEED)
|
||||||
) {
|
) {
|
||||||
return "must be greater than 0 and no more than 16";
|
return "must be greater than 0 and no more than " + MAX_SPEED;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
action === "fast" &&
|
action === "fast" &&
|
||||||
(numericValue < 0.0625 || numericValue > 16)
|
(numericValue < MIN_SPEED || numericValue > MAX_SPEED)
|
||||||
) {
|
) {
|
||||||
return "must be between 0.0625 and 16";
|
return "must be between " + MIN_SPEED + " and " + MAX_SPEED;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
(action === "rewind" || action === "advance") &&
|
(action === "rewind" || action === "advance") &&
|
||||||
@@ -162,6 +165,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
MAX_SPEED: MAX_SPEED,
|
||||||
|
MIN_SPEED: MIN_SPEED,
|
||||||
getActionValueError: getActionValueError,
|
getActionValueError: getActionValueError,
|
||||||
getLegacyKeyCode: getLegacyKeyCode,
|
getLegacyKeyCode: getLegacyKeyCode,
|
||||||
inferBindingCode: inferBindingCode,
|
inferBindingCode: inferBindingCode,
|
||||||
|
|||||||
@@ -68,6 +68,12 @@
|
|||||||
|
|
||||||
function normalizeResult(result) {
|
function normalizeResult(result) {
|
||||||
var normalized = { speed: result.speed };
|
var normalized = { speed: result.speed };
|
||||||
|
if (typeof result.frameToken === "string") {
|
||||||
|
normalized.frameToken = result.frameToken;
|
||||||
|
}
|
||||||
|
if (result.diagnostics && typeof result.diagnostics === "object") {
|
||||||
|
normalized.diagnostics = result.diagnostics;
|
||||||
|
}
|
||||||
if (typeof result.forceLastSavedSpeed === "boolean") {
|
if (typeof result.forceLastSavedSpeed === "boolean") {
|
||||||
normalized.forceLastSavedSpeed = result.forceLastSavedSpeed;
|
normalized.forceLastSavedSpeed = result.forceLastSavedSpeed;
|
||||||
}
|
}
|
||||||
@@ -91,7 +97,61 @@
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSafePageDetails(url) {
|
||||||
|
try {
|
||||||
|
var parsed = new URL(url);
|
||||||
|
return {
|
||||||
|
protocol: parsed.protocol,
|
||||||
|
hostname: parsed.hostname || null
|
||||||
|
};
|
||||||
|
} catch (_error) {
|
||||||
|
return { protocol: null, hostname: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDiagnosticReport(context) {
|
||||||
|
var config = context || {};
|
||||||
|
var storage = config.storage || {};
|
||||||
|
var siteRule = config.siteRule || null;
|
||||||
|
var frame = config.frame || null;
|
||||||
|
|
||||||
|
return JSON.stringify(
|
||||||
|
{
|
||||||
|
speederVersion: config.speederVersion || null,
|
||||||
|
browser: config.browser || null,
|
||||||
|
platform: config.platform || null,
|
||||||
|
page: getSafePageDetails(config.url),
|
||||||
|
globalSettings: {
|
||||||
|
enabled: storage.enabled !== false,
|
||||||
|
rememberSpeed: storage.rememberSpeed === true,
|
||||||
|
forceLastSavedSpeed: storage.forceLastSavedSpeed === true,
|
||||||
|
audioEnabled: storage.audioBoolean === true,
|
||||||
|
startHidden: storage.startHidden === true,
|
||||||
|
hideWithControls: storage.hideWithControls === true,
|
||||||
|
controllerLocation: storage.controllerLocation,
|
||||||
|
shortcutTargetMode: storage.shortcutTargetMode
|
||||||
|
},
|
||||||
|
matchedSiteRule: {
|
||||||
|
matched: Boolean(siteRule),
|
||||||
|
disabled: config.siteRuleDisabled === true,
|
||||||
|
overrideKeys: siteRule
|
||||||
|
? Object.keys(siteRule)
|
||||||
|
.filter(function(key) {
|
||||||
|
return key !== "pattern" && key !== "title";
|
||||||
|
})
|
||||||
|
.sort()
|
||||||
|
: []
|
||||||
|
},
|
||||||
|
tabPaused: config.tabPaused === true,
|
||||||
|
frame: frame && frame.diagnostics ? frame.diagnostics : null
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
buildDiagnosticReport: buildDiagnosticReport,
|
||||||
pickBestFrameSpeedResult: pickBestFrameSpeedResult,
|
pickBestFrameSpeedResult: pickBestFrameSpeedResult,
|
||||||
resolvePopupButtons: resolvePopupButtons,
|
resolvePopupButtons: resolvePopupButtons,
|
||||||
sanitizeButtonOrder: sanitizeButtonOrder
|
sanitizeButtonOrder: sanitizeButtonOrder
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
var LEGACY_SITE_RULES_DIFF_FORMAT = "defaults-diff-v1";
|
var LEGACY_SITE_RULES_DIFF_FORMAT = "defaults-diff-v1";
|
||||||
var SITE_RULES_DIFF_FORMAT = "defaults-diff-v2";
|
var SITE_RULES_DIFF_FORMAT = "defaults-diff-v2";
|
||||||
var REMOVED_DEFAULT_RULE_KEYS_META = "removedDefaultRuleKeys";
|
var REMOVED_DEFAULT_RULE_KEYS_META = "removedDefaultRuleKeys";
|
||||||
|
var MIN_SPEED = 0.1;
|
||||||
|
var MAX_SPEED = 16;
|
||||||
var DEFAULT_BUTTONS = ["rewind", "slower", "faster", "advance", "display"];
|
var DEFAULT_BUTTONS = ["rewind", "slower", "faster", "advance", "display"];
|
||||||
var LEGACY_SYNC_KEYS = [
|
var LEGACY_SYNC_KEYS = [
|
||||||
"resetSpeed",
|
"resetSpeed",
|
||||||
@@ -363,7 +365,7 @@
|
|||||||
return clampFiniteNumber(value, 250, 1000, undefined);
|
return clampFiniteNumber(value, 250, 1000, undefined);
|
||||||
}
|
}
|
||||||
if (key === "preferredSpeed") {
|
if (key === "preferredSpeed") {
|
||||||
return clampFiniteNumber(value, 0.0625, 16, undefined);
|
return clampFiniteNumber(value, MIN_SPEED, MAX_SPEED, undefined);
|
||||||
}
|
}
|
||||||
if (key === "shortcuts" && Array.isArray(value)) {
|
if (key === "shortcuts" && Array.isArray(value)) {
|
||||||
return sanitizeStoredBindingValues(value).map(function(shortcut) {
|
return sanitizeStoredBindingValues(value).map(function(shortcut) {
|
||||||
@@ -749,9 +751,9 @@
|
|||||||
var value = Number(normalized.value);
|
var value = Number(normalized.value);
|
||||||
var invalid = !Number.isFinite(value);
|
var invalid = !Number.isFinite(value);
|
||||||
if (action === "slower" || action === "faster") {
|
if (action === "slower" || action === "faster") {
|
||||||
invalid = invalid || value <= 0 || value > 16;
|
invalid = invalid || value <= 0 || value > MAX_SPEED;
|
||||||
} else if (action === "fast") {
|
} else if (action === "fast") {
|
||||||
invalid = invalid || value < 0.0625 || value > 16;
|
invalid = invalid || value < MIN_SPEED || value > MAX_SPEED;
|
||||||
} else if (action === "rewind" || action === "advance") {
|
} else if (action === "rewind" || action === "advance") {
|
||||||
invalid = invalid || value < 0;
|
invalid = invalid || value < 0;
|
||||||
} else if (action === "louder" || action === "softer") {
|
} else if (action === "louder" || action === "softer") {
|
||||||
@@ -885,8 +887,8 @@
|
|||||||
|
|
||||||
expanded.lastSpeed = clampFiniteNumber(
|
expanded.lastSpeed = clampFiniteNumber(
|
||||||
expanded.lastSpeed,
|
expanded.lastSpeed,
|
||||||
0.0625,
|
MIN_SPEED,
|
||||||
16,
|
MAX_SPEED,
|
||||||
DEFAULT_SETTINGS.lastSpeed
|
DEFAULT_SETTINGS.lastSpeed
|
||||||
);
|
);
|
||||||
expanded.hideWithControlsTimer = clampFiniteNumber(
|
expanded.hideWithControlsTimer = clampFiniteNumber(
|
||||||
@@ -983,6 +985,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var regex;
|
var regex;
|
||||||
|
if (pattern.startsWith("/") && pattern.lastIndexOf("/") === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (pattern.startsWith("/") && pattern.lastIndexOf("/") > 0) {
|
if (pattern.startsWith("/") && pattern.lastIndexOf("/") > 0) {
|
||||||
try {
|
try {
|
||||||
var lastSlash = pattern.lastIndexOf("/");
|
var lastSlash = pattern.lastIndexOf("/");
|
||||||
|
|||||||
@@ -197,6 +197,13 @@
|
|||||||
var normalizedPattern = pattern.replace(regStrip, "");
|
var normalizedPattern = pattern.replace(regStrip, "");
|
||||||
if (normalizedPattern.length === 0) return null;
|
if (normalizedPattern.length === 0) return null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedPattern.startsWith("/") &&
|
||||||
|
normalizedPattern.lastIndexOf("/") === 0
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
normalizedPattern.startsWith("/") &&
|
normalizedPattern.startsWith("/") &&
|
||||||
normalizedPattern.lastIndexOf("/") > 0
|
normalizedPattern.lastIndexOf("/") > 0
|
||||||
|
|||||||
@@ -72,22 +72,26 @@ function vscClearElement(el) {
|
|||||||
function vscSanitizeSvgTree(svg) {
|
function vscSanitizeSvgTree(svg) {
|
||||||
if (!svg || String(svg.tagName).toLowerCase() !== "svg") return null;
|
if (!svg || String(svg.tagName).toLowerCase() !== "svg") return null;
|
||||||
|
|
||||||
svg.querySelectorAll("script, style, foreignObject").forEach(function (n) {
|
svg
|
||||||
n.remove();
|
.querySelectorAll(
|
||||||
});
|
"script, style, foreignObject, iframe, object, embed, image, use, a, " +
|
||||||
|
"animate, animateMotion, animateTransform, set"
|
||||||
|
)
|
||||||
|
.forEach(function (n) {
|
||||||
|
n.remove();
|
||||||
|
});
|
||||||
|
|
||||||
[svg].concat(Array.from(svg.querySelectorAll("*"))).forEach(function (el) {
|
[svg].concat(Array.from(svg.querySelectorAll("*"))).forEach(function (el) {
|
||||||
for (var i = el.attributes.length - 1; i >= 0; i--) {
|
for (var i = el.attributes.length - 1; i >= 0; i--) {
|
||||||
var attr = el.attributes[i];
|
var attr = el.attributes[i];
|
||||||
var name = attr.name.toLowerCase();
|
var name = attr.name.toLowerCase();
|
||||||
var val = attr.value;
|
var val = attr.value;
|
||||||
if (name.indexOf("on") === 0) {
|
|
||||||
el.removeAttribute(attr.name);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
(name === "href" || name === "xlink:href") &&
|
name.indexOf("on") === 0 ||
|
||||||
/^\s*javascript:/i.test(val)
|
name === "style" ||
|
||||||
|
name === "href" ||
|
||||||
|
name === "xlink:href" ||
|
||||||
|
/url\s*\(/i.test(val)
|
||||||
) {
|
) {
|
||||||
el.removeAttribute(attr.name);
|
el.removeAttribute(attr.name);
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+161
-200
@@ -7,7 +7,7 @@
|
|||||||
"name": "speeder",
|
"name": "speeder",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@asamuzakjp/css-color": {
|
"node_modules/@asamuzakjp/css-color": {
|
||||||
@@ -140,9 +140,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
|
||||||
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
|
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -157,9 +157,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
|
||||||
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
|
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -174,9 +174,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm64": {
|
"node_modules/@esbuild/android-arm64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
|
||||||
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
|
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -191,9 +191,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-x64": {
|
"node_modules/@esbuild/android-x64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
|
||||||
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
|
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -208,9 +208,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
|
||||||
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
|
"integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -225,9 +225,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
|
||||||
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
|
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -242,9 +242,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
|
||||||
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
|
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -259,9 +259,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
|
||||||
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
|
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -276,9 +276,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm": {
|
"node_modules/@esbuild/linux-arm": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
|
||||||
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
|
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -293,9 +293,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
|
||||||
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
|
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -310,9 +310,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
|
||||||
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
|
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -327,9 +327,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
|
||||||
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
|
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
@@ -344,9 +344,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
|
||||||
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
|
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
@@ -361,9 +361,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
|
||||||
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
|
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -378,9 +378,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
|
||||||
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
|
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -395,9 +395,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
|
||||||
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
|
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -412,9 +412,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-x64": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
|
||||||
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
|
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -429,9 +429,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-arm64": {
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
|
||||||
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
|
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -446,9 +446,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
|
||||||
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
|
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -463,9 +463,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-arm64": {
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
|
||||||
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
|
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -480,9 +480,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
|
||||||
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
|
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -497,9 +497,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openharmony-arm64": {
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
|
||||||
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
|
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -514,9 +514,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
|
||||||
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
|
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -531,9 +531,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
|
||||||
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
|
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -548,9 +548,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
|
||||||
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
|
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -565,9 +565,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-x64": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
|
||||||
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
|
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -680,9 +680,6 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -697,9 +694,6 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -714,9 +708,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -731,9 +722,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -748,9 +736,6 @@
|
|||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -765,9 +750,6 @@
|
|||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -782,9 +764,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -799,9 +778,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -816,9 +792,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -833,9 +806,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -850,9 +820,6 @@
|
|||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -867,9 +834,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -884,9 +848,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1003,15 +964,15 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
|
||||||
"integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
|
"integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/spy": "3.2.4",
|
"@vitest/spy": "3.2.7",
|
||||||
"@vitest/utils": "3.2.4",
|
"@vitest/utils": "3.2.7",
|
||||||
"chai": "^5.2.0",
|
"chai": "^5.2.0",
|
||||||
"tinyrainbow": "^2.0.0"
|
"tinyrainbow": "^2.0.0"
|
||||||
},
|
},
|
||||||
@@ -1020,13 +981,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/mocker": {
|
"node_modules/@vitest/mocker": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",
|
||||||
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
|
"integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/spy": "3.2.4",
|
"@vitest/spy": "3.2.7",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "^3.0.3",
|
||||||
"magic-string": "^0.30.17"
|
"magic-string": "^0.30.17"
|
||||||
},
|
},
|
||||||
@@ -1047,9 +1008,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/pretty-format": {
|
"node_modules/@vitest/pretty-format": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",
|
||||||
"integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
|
"integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1060,13 +1021,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/runner": {
|
"node_modules/@vitest/runner": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",
|
||||||
"integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
|
"integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/utils": "3.2.4",
|
"@vitest/utils": "3.2.7",
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
"strip-literal": "^3.0.0"
|
"strip-literal": "^3.0.0"
|
||||||
},
|
},
|
||||||
@@ -1075,13 +1036,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/snapshot": {
|
"node_modules/@vitest/snapshot": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",
|
||||||
"integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
|
"integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "3.2.4",
|
"@vitest/pretty-format": "3.2.7",
|
||||||
"magic-string": "^0.30.17",
|
"magic-string": "^0.30.17",
|
||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
},
|
},
|
||||||
@@ -1090,9 +1051,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/spy": {
|
"node_modules/@vitest/spy": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",
|
||||||
"integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
|
"integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1103,13 +1064,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/utils": {
|
"node_modules/@vitest/utils": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",
|
||||||
"integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
|
"integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "3.2.4",
|
"@vitest/pretty-format": "3.2.7",
|
||||||
"loupe": "^3.1.4",
|
"loupe": "^3.1.4",
|
||||||
"tinyrainbow": "^2.0.0"
|
"tinyrainbow": "^2.0.0"
|
||||||
},
|
},
|
||||||
@@ -1258,9 +1219,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
|
||||||
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
|
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -1271,32 +1232,32 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@esbuild/aix-ppc64": "0.27.7",
|
"@esbuild/aix-ppc64": "0.28.2",
|
||||||
"@esbuild/android-arm": "0.27.7",
|
"@esbuild/android-arm": "0.28.2",
|
||||||
"@esbuild/android-arm64": "0.27.7",
|
"@esbuild/android-arm64": "0.28.2",
|
||||||
"@esbuild/android-x64": "0.27.7",
|
"@esbuild/android-x64": "0.28.2",
|
||||||
"@esbuild/darwin-arm64": "0.27.7",
|
"@esbuild/darwin-arm64": "0.28.2",
|
||||||
"@esbuild/darwin-x64": "0.27.7",
|
"@esbuild/darwin-x64": "0.28.2",
|
||||||
"@esbuild/freebsd-arm64": "0.27.7",
|
"@esbuild/freebsd-arm64": "0.28.2",
|
||||||
"@esbuild/freebsd-x64": "0.27.7",
|
"@esbuild/freebsd-x64": "0.28.2",
|
||||||
"@esbuild/linux-arm": "0.27.7",
|
"@esbuild/linux-arm": "0.28.2",
|
||||||
"@esbuild/linux-arm64": "0.27.7",
|
"@esbuild/linux-arm64": "0.28.2",
|
||||||
"@esbuild/linux-ia32": "0.27.7",
|
"@esbuild/linux-ia32": "0.28.2",
|
||||||
"@esbuild/linux-loong64": "0.27.7",
|
"@esbuild/linux-loong64": "0.28.2",
|
||||||
"@esbuild/linux-mips64el": "0.27.7",
|
"@esbuild/linux-mips64el": "0.28.2",
|
||||||
"@esbuild/linux-ppc64": "0.27.7",
|
"@esbuild/linux-ppc64": "0.28.2",
|
||||||
"@esbuild/linux-riscv64": "0.27.7",
|
"@esbuild/linux-riscv64": "0.28.2",
|
||||||
"@esbuild/linux-s390x": "0.27.7",
|
"@esbuild/linux-s390x": "0.28.2",
|
||||||
"@esbuild/linux-x64": "0.27.7",
|
"@esbuild/linux-x64": "0.28.2",
|
||||||
"@esbuild/netbsd-arm64": "0.27.7",
|
"@esbuild/netbsd-arm64": "0.28.2",
|
||||||
"@esbuild/netbsd-x64": "0.27.7",
|
"@esbuild/netbsd-x64": "0.28.2",
|
||||||
"@esbuild/openbsd-arm64": "0.27.7",
|
"@esbuild/openbsd-arm64": "0.28.2",
|
||||||
"@esbuild/openbsd-x64": "0.27.7",
|
"@esbuild/openbsd-x64": "0.28.2",
|
||||||
"@esbuild/openharmony-arm64": "0.27.7",
|
"@esbuild/openharmony-arm64": "0.28.2",
|
||||||
"@esbuild/sunos-x64": "0.27.7",
|
"@esbuild/sunos-x64": "0.28.2",
|
||||||
"@esbuild/win32-arm64": "0.27.7",
|
"@esbuild/win32-arm64": "0.28.2",
|
||||||
"@esbuild/win32-ia32": "0.27.7",
|
"@esbuild/win32-ia32": "0.28.2",
|
||||||
"@esbuild/win32-x64": "0.27.7"
|
"@esbuild/win32-x64": "0.28.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
@@ -1492,9 +1453,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.18",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1568,9 +1529,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.8",
|
"version": "8.5.26",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
||||||
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1588,7 +1549,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.17",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -1837,13 +1798,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.3.1",
|
"version": "7.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
|
||||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
"integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0 || ^0.28.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
"picomatch": "^4.0.3",
|
"picomatch": "^4.0.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
@@ -1935,20 +1896,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vitest": {
|
"node_modules/vitest": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
|
||||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
"integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/expect": "3.2.4",
|
"@vitest/expect": "3.2.7",
|
||||||
"@vitest/mocker": "3.2.4",
|
"@vitest/mocker": "3.2.7",
|
||||||
"@vitest/pretty-format": "^3.2.4",
|
"@vitest/pretty-format": "^3.2.7",
|
||||||
"@vitest/runner": "3.2.4",
|
"@vitest/runner": "3.2.7",
|
||||||
"@vitest/snapshot": "3.2.4",
|
"@vitest/snapshot": "3.2.7",
|
||||||
"@vitest/spy": "3.2.4",
|
"@vitest/spy": "3.2.7",
|
||||||
"@vitest/utils": "3.2.4",
|
"@vitest/utils": "3.2.7",
|
||||||
"chai": "^5.2.0",
|
"chai": "^5.2.0",
|
||||||
"debug": "^4.4.1",
|
"debug": "^4.4.1",
|
||||||
"expect-type": "^1.2.1",
|
"expect-type": "^1.2.1",
|
||||||
@@ -1978,8 +1939,8 @@
|
|||||||
"@edge-runtime/vm": "*",
|
"@edge-runtime/vm": "*",
|
||||||
"@types/debug": "^4.1.12",
|
"@types/debug": "^4.1.12",
|
||||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||||
"@vitest/browser": "3.2.4",
|
"@vitest/browser": "3.2.7",
|
||||||
"@vitest/ui": "3.2.4",
|
"@vitest/ui": "3.2.7",
|
||||||
"happy-dom": "*",
|
"happy-dom": "*",
|
||||||
"jsdom": "*"
|
"jsdom": "*"
|
||||||
},
|
},
|
||||||
@@ -2086,9 +2047,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.20.0",
|
"version": "8.21.3",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
|
||||||
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
+1
-1
@@ -7,6 +7,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ validate_semver() {
|
|||||||
echo "Error: empty version." >&2
|
echo "Error: empty version." >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
if [[ ! "$s" =~ ^[0-9]+(\.[0-9]+){0,3}(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
|
if [[ ! "$s" =~ ^[0-9]+(\.[0-9]+){0,3}$ ]]; then
|
||||||
echo "Error: invalid version (use something like 5.0.4)." >&2
|
echo "Error: Firefox versions must contain only 1-4 numeric parts (for example, 6.0.8.1)." >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -59,10 +59,15 @@ fi
|
|||||||
git checkout beta
|
git checkout beta
|
||||||
git pull origin beta
|
git pull origin beta
|
||||||
|
|
||||||
echo "Current version on beta ($MANIFEST_PATH): $(manifest_version)"
|
CURRENT_VERSION="$(manifest_version)"
|
||||||
|
echo "Current version on beta ($MANIFEST_PATH): $CURRENT_VERSION"
|
||||||
read -r -p "Release version for $MANIFEST_PATH + tag (e.g. 5.0.4): " SEMVER_IN
|
read -r -p "Release version for $MANIFEST_PATH + tag (e.g. 5.0.4): " SEMVER_IN
|
||||||
SEMVER="$(normalize_semver "$SEMVER_IN")"
|
SEMVER="$(normalize_semver "$SEMVER_IN")"
|
||||||
validate_semver "$SEMVER"
|
validate_semver "$SEMVER"
|
||||||
|
if [[ "$SEMVER" == "$CURRENT_VERSION" ]]; then
|
||||||
|
echo "Error: release version must differ from the current manifest version $CURRENT_VERSION." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
TAG="v${SEMVER}"
|
TAG="v${SEMVER}"
|
||||||
if [[ "$TAG" == *-beta* ]]; then
|
if [[ "$TAG" == *-beta* ]]; then
|
||||||
@@ -70,6 +75,11 @@ if [[ "$TAG" == *-beta* ]]; then
|
|||||||
read -r -p "Continue anyway? [y/N] " w
|
read -r -p "Continue anyway? [y/N] " w
|
||||||
[[ "${w:-}" =~ ^[yY](es)?$ ]] || { echo "Aborted."; exit 1; }
|
[[ "${w:-}" =~ ^[yY](es)?$ ]] || { echo "Aborted."; exit 1; }
|
||||||
fi
|
fi
|
||||||
|
if git show-ref --verify --quiet "refs/tags/$TAG" ||
|
||||||
|
git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then
|
||||||
|
echo "Error: tag $TAG already exists." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "This will:"
|
echo "This will:"
|
||||||
@@ -87,11 +97,11 @@ git pull origin main
|
|||||||
git merge --squash beta
|
git merge --squash beta
|
||||||
bump_manifest "$SEMVER"
|
bump_manifest "$SEMVER"
|
||||||
git add -A
|
git add -A
|
||||||
git commit -m "Release $TAG"
|
git commit -m "chore(release): prepare $TAG"
|
||||||
|
|
||||||
git push origin main
|
git push origin main
|
||||||
|
|
||||||
git tag -a "$TAG" -m "$TAG"
|
git tag -s "$TAG" -m "$TAG"
|
||||||
git push origin "$TAG"
|
git push origin "$TAG"
|
||||||
|
|
||||||
git checkout dev
|
git checkout dev
|
||||||
|
|||||||
+16
-6
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Merge dev → beta, push beta, and push an annotated beta tag (v*-beta*).
|
# Merge dev → beta, push beta, and push a signed beta tag (v*-beta*).
|
||||||
# Triggers .github/workflows/deploy.yml: unlisted AMO sign + GitHub prerelease.
|
# Triggers .github/workflows/deploy.yml: unlisted AMO sign + GitHub prerelease.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -44,8 +44,8 @@ validate_semver() {
|
|||||||
echo "Error: empty version." >&2
|
echo "Error: empty version." >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
if [[ ! "$s" =~ ^[0-9]+(\.[0-9]+){0,3}(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
|
if [[ ! "$s" =~ ^[0-9]+(\.[0-9]+){0,3}$ ]]; then
|
||||||
echo "Error: invalid version (use something like 5.0.4 or 5.0.4-beta.1)." >&2
|
echo "Error: Firefox versions must contain only 1-4 numeric parts (for example, 6.0.8.1)." >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -58,10 +58,15 @@ fi
|
|||||||
git checkout dev
|
git checkout dev
|
||||||
git pull origin dev
|
git pull origin dev
|
||||||
|
|
||||||
echo "Current version in $MANIFEST_PATH: $(manifest_version)"
|
CURRENT_VERSION="$(manifest_version)"
|
||||||
|
echo "Current version in $MANIFEST_PATH: $CURRENT_VERSION"
|
||||||
read -r -p "New version for $MANIFEST_PATH (e.g. 5.0.4): " SEMVER_IN
|
read -r -p "New version for $MANIFEST_PATH (e.g. 5.0.4): " SEMVER_IN
|
||||||
SEMVER="$(normalize_semver "$SEMVER_IN")"
|
SEMVER="$(normalize_semver "$SEMVER_IN")"
|
||||||
validate_semver "$SEMVER"
|
validate_semver "$SEMVER"
|
||||||
|
if [[ "$SEMVER" == "$CURRENT_VERSION" ]]; then
|
||||||
|
echo "Error: release version must differ from the current manifest version $CURRENT_VERSION." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Beta git tag will include '-beta' (required by deploy.yml)."
|
echo "Beta git tag will include '-beta' (required by deploy.yml)."
|
||||||
read -r -p "Beta tag suffix [beta.1]: " SUFFIX_IN
|
read -r -p "Beta tag suffix [beta.1]: " SUFFIX_IN
|
||||||
@@ -74,6 +79,11 @@ if [[ "$TAG" != *-beta* ]]; then
|
|||||||
echo "Error: beta tag must contain '-beta' for the workflow (got $TAG). Try suffix like beta.1." >&2
|
echo "Error: beta tag must contain '-beta' for the workflow (got $TAG). Try suffix like beta.1." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if git show-ref --verify --quiet "refs/tags/$TAG" ||
|
||||||
|
git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then
|
||||||
|
echo "Error: tag $TAG already exists." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "This will:"
|
echo "This will:"
|
||||||
@@ -88,7 +98,7 @@ echo "🚀 Releasing beta $TAG"
|
|||||||
|
|
||||||
bump_manifest "$SEMVER"
|
bump_manifest "$SEMVER"
|
||||||
git add "$MANIFEST_PATH"
|
git add "$MANIFEST_PATH"
|
||||||
git commit -m "Bump version to $SEMVER"
|
git commit -m "chore(release): bump version to $SEMVER"
|
||||||
git push origin dev
|
git push origin dev
|
||||||
|
|
||||||
git checkout beta
|
git checkout beta
|
||||||
@@ -96,7 +106,7 @@ git pull origin beta
|
|||||||
git merge dev --no-ff -m "$TAG"
|
git merge dev --no-ff -m "$TAG"
|
||||||
git push origin beta
|
git push origin beta
|
||||||
|
|
||||||
git tag -a "$TAG" -m "$TAG"
|
git tag -s "$TAG" -m "$TAG"
|
||||||
git push origin "$TAG"
|
git push origin "$TAG"
|
||||||
|
|
||||||
git checkout dev
|
git checkout dev
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
const {
|
||||||
|
createChromeMock,
|
||||||
|
evaluateScript,
|
||||||
|
loadHtmlString
|
||||||
|
} = require("./helpers/extension-test-utils");
|
||||||
|
|
||||||
|
describe("background toolbar state", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
delete global.chrome;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("initializes and follows the enabled storage setting", () => {
|
||||||
|
loadHtmlString("<!doctype html><html><body></body></html>");
|
||||||
|
const chrome = createChromeMock({ syncData: { enabled: false } });
|
||||||
|
global.chrome = chrome;
|
||||||
|
window.chrome = chrome;
|
||||||
|
|
||||||
|
evaluateScript("extension/background/background.js");
|
||||||
|
expect(chrome.browserAction.setIcon).toHaveBeenLastCalledWith({
|
||||||
|
path: {
|
||||||
|
19: "assets/icons/icon19_disabled.png",
|
||||||
|
38: "assets/icons/icon38_disabled.png",
|
||||||
|
48: "assets/icons/icon48_disabled.png"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.storage.sync.set({ enabled: true });
|
||||||
|
expect(chrome.browserAction.setIcon).toHaveBeenLastCalledWith({
|
||||||
|
path: {
|
||||||
|
19: "assets/icons/icon19.png",
|
||||||
|
38: "assets/icons/icon38.png",
|
||||||
|
48: "assets/icons/icon48.png"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps tab pauses in memory and clears closed tabs", () => {
|
||||||
|
loadHtmlString("<!doctype html><html><body></body></html>");
|
||||||
|
const chrome = createChromeMock();
|
||||||
|
global.chrome = chrome;
|
||||||
|
window.chrome = chrome;
|
||||||
|
|
||||||
|
evaluateScript("extension/background/background.js");
|
||||||
|
const listener = chrome.runtime.onMessage.listeners[0];
|
||||||
|
const respond = vi.fn();
|
||||||
|
|
||||||
|
listener(
|
||||||
|
{ action: "set_tab_paused", tabId: 42, paused: true },
|
||||||
|
{},
|
||||||
|
respond
|
||||||
|
);
|
||||||
|
expect(respond).toHaveBeenLastCalledWith({ paused: true });
|
||||||
|
expect(chrome.tabs.sendMessage).toHaveBeenCalledWith(
|
||||||
|
42,
|
||||||
|
{ action: "set_tab_paused", paused: true },
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
|
||||||
|
listener({ action: "get_tab_pause_state" }, { tab: { id: 42 } }, respond);
|
||||||
|
expect(respond).toHaveBeenLastCalledWith({ paused: true });
|
||||||
|
|
||||||
|
chrome.tabs.onRemoved.emit(42);
|
||||||
|
listener({ action: "get_tab_pause_state", tabId: 42 }, {}, respond);
|
||||||
|
expect(respond).toHaveBeenLastCalledWith({ paused: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,6 +8,7 @@ import { applyJSDOMWindow } from "./jsdom-globals.js";
|
|||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
const repoRoot = path.resolve(__dirname, "..", "..");
|
const repoRoot = path.resolve(__dirname, "..", "..");
|
||||||
|
let activeDom = null;
|
||||||
|
|
||||||
function readRepoFile(relPath) {
|
function readRepoFile(relPath) {
|
||||||
return fs.readFileSync(path.join(repoRoot, relPath), "utf8");
|
return fs.readFileSync(path.join(repoRoot, relPath), "utf8");
|
||||||
@@ -18,11 +19,13 @@ function readRepoFile(relPath) {
|
|||||||
* top-level `const` redeclaration errors (avoids document.write).
|
* top-level `const` redeclaration errors (avoids document.write).
|
||||||
*/
|
*/
|
||||||
export function loadHtmlString(html, options = {}) {
|
export function loadHtmlString(html, options = {}) {
|
||||||
|
if (activeDom) activeDom.window.close();
|
||||||
const dom = new JSDOM(html, {
|
const dom = new JSDOM(html, {
|
||||||
url: options.url || "https://example.org/",
|
url: options.url || "https://example.org/",
|
||||||
pretendToBeVisual: true,
|
pretendToBeVisual: true,
|
||||||
runScripts: "dangerously"
|
runScripts: "dangerously"
|
||||||
});
|
});
|
||||||
|
activeDom = dom;
|
||||||
applyJSDOMWindow(dom.window);
|
applyJSDOMWindow(dom.window);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +176,13 @@ export function createChromeMock(options = {}) {
|
|||||||
getManifest: vi.fn(() => ({
|
getManifest: vi.fn(() => ({
|
||||||
version: options.manifestVersion || "9.9.9"
|
version: options.manifestVersion || "9.9.9"
|
||||||
})),
|
})),
|
||||||
getURL: vi.fn((url) => "moz-extension://speeder/" + url)
|
getURL: vi.fn((url) => "moz-extension://speeder/" + url),
|
||||||
|
sendMessage: vi.fn((message, callback) => {
|
||||||
|
if (options.runtimeSendMessageImpl) {
|
||||||
|
return options.runtimeSendMessageImpl(message, callback);
|
||||||
|
}
|
||||||
|
if (callback) callback({ paused: false });
|
||||||
|
})
|
||||||
},
|
},
|
||||||
storage: {
|
storage: {
|
||||||
sync: syncArea,
|
sync: syncArea,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const { JSDOM } = require("jsdom");
|
|||||||
const vi = globalThis.vi;
|
const vi = globalThis.vi;
|
||||||
|
|
||||||
const ROOT = path.resolve(__dirname, "..", "..");
|
const ROOT = path.resolve(__dirname, "..", "..");
|
||||||
|
let activeDom = null;
|
||||||
|
|
||||||
function clone(value) {
|
function clone(value) {
|
||||||
if (value === undefined) return undefined;
|
if (value === undefined) return undefined;
|
||||||
@@ -42,15 +43,18 @@ function applyJSDOMWindow(win) {
|
|||||||
win.Date = globalThis.Date;
|
win.Date = globalThis.Date;
|
||||||
win.open = vi.fn();
|
win.open = vi.fn();
|
||||||
win.close = vi.fn();
|
win.close = vi.fn();
|
||||||
|
win.confirm = vi.fn(() => true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadHtmlString(html, options) {
|
function loadHtmlString(html, options) {
|
||||||
const config = options || {};
|
const config = options || {};
|
||||||
|
if (activeDom) activeDom.window.close();
|
||||||
const dom = new JSDOM(html, {
|
const dom = new JSDOM(html, {
|
||||||
url: config.url || "https://example.org/",
|
url: config.url || "https://example.org/",
|
||||||
pretendToBeVisual: true,
|
pretendToBeVisual: true,
|
||||||
runScripts: "dangerously"
|
runScripts: "dangerously"
|
||||||
});
|
});
|
||||||
|
activeDom = dom;
|
||||||
applyJSDOMWindow(dom.window);
|
applyJSDOMWindow(dom.window);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,6 +248,7 @@ function createChromeMock(options) {
|
|||||||
const storageOnChanged = createChromeEvent();
|
const storageOnChanged = createChromeEvent();
|
||||||
const tabsOnActivated = createChromeEvent();
|
const tabsOnActivated = createChromeEvent();
|
||||||
const tabsOnUpdated = createChromeEvent();
|
const tabsOnUpdated = createChromeEvent();
|
||||||
|
const tabsOnRemoved = createChromeEvent();
|
||||||
const runtimeOnMessage = createChromeEvent();
|
const runtimeOnMessage = createChromeEvent();
|
||||||
|
|
||||||
const chrome = {
|
const chrome = {
|
||||||
@@ -251,6 +256,12 @@ function createChromeMock(options) {
|
|||||||
lastError: null,
|
lastError: null,
|
||||||
getManifest: vi.fn(() => clone(config.manifest) || { version: "0.0.0-test" }),
|
getManifest: vi.fn(() => clone(config.manifest) || { version: "0.0.0-test" }),
|
||||||
getURL: vi.fn((relPath) => `moz-extension://${relPath}`),
|
getURL: vi.fn((relPath) => `moz-extension://${relPath}`),
|
||||||
|
sendMessage: vi.fn((message, callback) => {
|
||||||
|
if (config.runtimeSendMessageImpl) {
|
||||||
|
return config.runtimeSendMessageImpl(message, callback);
|
||||||
|
}
|
||||||
|
if (callback) callback({ paused: false });
|
||||||
|
}),
|
||||||
onMessage: runtimeOnMessage
|
onMessage: runtimeOnMessage
|
||||||
},
|
},
|
||||||
browserAction: {
|
browserAction: {
|
||||||
@@ -271,7 +282,8 @@ function createChromeMock(options) {
|
|||||||
}),
|
}),
|
||||||
create: vi.fn(),
|
create: vi.fn(),
|
||||||
onActivated: tabsOnActivated,
|
onActivated: tabsOnActivated,
|
||||||
onUpdated: tabsOnUpdated
|
onUpdated: tabsOnUpdated,
|
||||||
|
onRemoved: tabsOnRemoved
|
||||||
},
|
},
|
||||||
storage: {
|
storage: {
|
||||||
onChanged: storageOnChanged,
|
onChanged: storageOnChanged,
|
||||||
|
|||||||
@@ -27,4 +27,5 @@ export function applyJSDOMWindow(win) {
|
|||||||
|
|
||||||
win.open = vi.fn();
|
win.open = vi.fn();
|
||||||
win.close = vi.fn();
|
win.close = vi.fn();
|
||||||
|
win.confirm = vi.fn(() => true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ describe("import/export flows", () => {
|
|||||||
|
|
||||||
it("exports sync and local settings as a JSON download", async () => {
|
it("exports sync and local settings as a JSON download", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
vi.setSystemTime(new Date(2026, 3, 4, 8, 9, 10));
|
const exportTime = new Date(2026, 3, 4, 8, 9, 10);
|
||||||
|
vi.setSystemTime(exportTime);
|
||||||
const chrome = await setupImportExport({
|
const chrome = await setupImportExport({
|
||||||
sync: { rememberSpeed: true, shortcutTargetMode: "all" },
|
sync: { rememberSpeed: true, shortcutTargetMode: "all" },
|
||||||
local: { customButtonIcons: { faster: { slug: "rocket" } } }
|
local: { customButtonIcons: { faster: { slug: "rocket" } } }
|
||||||
@@ -89,7 +90,7 @@ describe("import/export flows", () => {
|
|||||||
const blobText = await capturedBlob.text();
|
const blobText = await capturedBlob.text();
|
||||||
expect(JSON.parse(blobText)).toEqual({
|
expect(JSON.parse(blobText)).toEqual({
|
||||||
version: "1.1",
|
version: "1.1",
|
||||||
exportDate: "2026-04-04T12:09:10.000Z",
|
exportDate: exportTime.toISOString(),
|
||||||
settings: {
|
settings: {
|
||||||
rememberSpeed: true,
|
rememberSpeed: true,
|
||||||
shortcutTargetMode: "all",
|
shortcutTargetMode: "all",
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ describe("options/import-export.js", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("generates timestamped backup filenames", () => {
|
it("generates timestamped backup filenames", () => {
|
||||||
vi.setSystemTime(new Date("2026-04-04T13:14:15Z"));
|
vi.setSystemTime(new Date(2026, 3, 4, 9, 14, 15));
|
||||||
bootImportExport();
|
bootImportExport();
|
||||||
|
|
||||||
expect(window.generateBackupFilename()).toBe(
|
expect(window.generateBackupFilename()).toBe(
|
||||||
@@ -137,9 +137,14 @@ describe("options/import-export.js", () => {
|
|||||||
expect(backup.localSettings.lucideTagsCacheV1At).toBeUndefined();
|
expect(backup.localSettings.lucideTagsCacheV1At).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("imports wrapped backups, restores local data, and refreshes the options page", async () => {
|
it("reports partial success and refreshes when custom icons fail to import", async () => {
|
||||||
const { chrome } = bootImportExport();
|
const { chrome } = bootImportExport();
|
||||||
window.restore_options = vi.fn();
|
window.restore_options = vi.fn();
|
||||||
|
chrome.storage.local.set.mockImplementationOnce(function(_items, callback) {
|
||||||
|
chrome.runtime.lastError = { message: "icon quota exceeded" };
|
||||||
|
callback();
|
||||||
|
chrome.runtime.lastError = null;
|
||||||
|
});
|
||||||
|
|
||||||
const realCreateElement = document.createElement.bind(document);
|
const realCreateElement = document.createElement.bind(document);
|
||||||
const fakeInput = realCreateElement("input");
|
const fakeInput = realCreateElement("input");
|
||||||
@@ -197,6 +202,12 @@ describe("options/import-export.js", () => {
|
|||||||
{ rememberSpeed: true, enabled: false },
|
{ rememberSpeed: true, enabled: false },
|
||||||
expect.any(Function)
|
expect.any(Function)
|
||||||
);
|
);
|
||||||
|
expect(document.querySelector("#status").textContent).toContain(
|
||||||
|
"Settings imported, but custom icons could not be updated"
|
||||||
|
);
|
||||||
|
expect(document.querySelector("#status").textContent).toContain(
|
||||||
|
"icon quota exceeded"
|
||||||
|
);
|
||||||
|
|
||||||
vi.advanceTimersByTime(500);
|
vi.advanceTimersByTime(500);
|
||||||
expect(window.restore_options).toHaveBeenCalled();
|
expect(window.restore_options).toHaveBeenCalled();
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ const {
|
|||||||
createChromeMock,
|
createChromeMock,
|
||||||
evaluateScript,
|
evaluateScript,
|
||||||
flushAsyncWork,
|
flushAsyncWork,
|
||||||
loadHtmlString
|
loadHtmlString,
|
||||||
|
readWorkspaceFile
|
||||||
} = require("./helpers/extension-test-utils");
|
} = require("./helpers/extension-test-utils");
|
||||||
|
|
||||||
function bootInject(options) {
|
function bootInject(options) {
|
||||||
@@ -280,6 +281,55 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
|||||||
expect(video.vsc.div.style.getPropertyValue("height")).toBe("226px");
|
expect(video.vsc.div.style.getPropertyValue("height")).toBe("226px");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps measured geometry out of controller stylesheet defaults", () => {
|
||||||
|
[
|
||||||
|
{
|
||||||
|
css: readWorkspaceFile("extension/content/inject.css"),
|
||||||
|
host: /\.vsc-controller\s*\{([^}]*)\}/,
|
||||||
|
fullscreen:
|
||||||
|
/\.vsc-controller\.vsc-fullscreen-popover\s*\{([^}]*)\}/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
css: readWorkspaceFile("extension/content/shadow.css"),
|
||||||
|
host: /:host\s*\{([^}]*)\}/,
|
||||||
|
fullscreen: /:host\(\.vsc-fullscreen-popover\)\s*\{([^}]*)\}/
|
||||||
|
}
|
||||||
|
].forEach(({ css, host, fullscreen }) => {
|
||||||
|
expect(css.match(host)[1]).not.toMatch(
|
||||||
|
/\b(?:top|left|width|height)\s*:/
|
||||||
|
);
|
||||||
|
expect(css.match(fullscreen)[1]).not.toMatch(/\binset\s*:/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("repositions a Shorts controller when playback moves the video on-screen", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
bootInject({ url: "https://www.youtube.com/shorts/example" });
|
||||||
|
await settleLifecycle();
|
||||||
|
|
||||||
|
const player = document.createElement("div");
|
||||||
|
player.className = "html5-video-player";
|
||||||
|
const video = document.createElement("video");
|
||||||
|
const playerRect = makeRect(40, 64, 351, 624);
|
||||||
|
let videoRect = makeRect(40, -560, 351, 624);
|
||||||
|
|
||||||
|
setRect(player, playerRect);
|
||||||
|
setBoxMetrics(player, playerRect.width, playerRect.height);
|
||||||
|
video.getBoundingClientRect = () => videoRect;
|
||||||
|
video.src = "blob:https://www.youtube.com/shorts";
|
||||||
|
player.appendChild(video);
|
||||||
|
document.body.appendChild(player);
|
||||||
|
|
||||||
|
window.ensureController(video, player);
|
||||||
|
expect(video.vsc.div.style.getPropertyValue("top")).toBe("-624px");
|
||||||
|
|
||||||
|
videoRect = playerRect;
|
||||||
|
video.dispatchEvent(new Event("playing"));
|
||||||
|
await vi.advanceTimersByTimeAsync(0);
|
||||||
|
|
||||||
|
expect(video.vsc.div.style.getPropertyValue("top")).toBe("0px");
|
||||||
|
});
|
||||||
|
|
||||||
it("targets the controller nearest the pointer unless change-all is selected", async () => {
|
it("targets the controller nearest the pointer unless change-all is selected", async () => {
|
||||||
bootInject();
|
bootInject();
|
||||||
await settleLifecycle();
|
await settleLifecycle();
|
||||||
@@ -292,16 +342,27 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
|||||||
src: "https://example.org/second.mp4",
|
src: "https://example.org/second.mp4",
|
||||||
mountRect: makeRect(500, 0, 320, 180)
|
mountRect: makeRect(500, 0, 320, 180)
|
||||||
});
|
});
|
||||||
setRect(window.getControllerElement(first.controller), makeRect(10, 10, 120, 30));
|
const firstLayoutRead = vi.fn(() => first.mount.getBoundingClientRect());
|
||||||
setRect(window.getControllerElement(second.controller), makeRect(510, 10, 120, 30));
|
const secondLayoutRead = vi.fn(() => second.mount.getBoundingClientRect());
|
||||||
|
first.video.getBoundingClientRect = firstLayoutRead;
|
||||||
|
second.video.getBoundingClientRect = secondLayoutRead;
|
||||||
|
|
||||||
document.dispatchEvent(
|
document.dispatchEvent(
|
||||||
new MouseEvent("mousemove", { bubbles: true, clientX: 600, clientY: 20 })
|
new MouseEvent("mousemove", { bubbles: true, clientX: 600, clientY: 20 })
|
||||||
);
|
);
|
||||||
window.runAction("faster", 0.1);
|
document.dispatchEvent(
|
||||||
|
new KeyboardEvent("keydown", {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
code: "KeyD",
|
||||||
|
key: "d"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
expect(first.video.playbackRate).toBe(1);
|
expect(first.video.playbackRate).toBe(1);
|
||||||
expect(second.video.playbackRate).toBe(1.1);
|
expect(second.video.playbackRate).toBe(1.1);
|
||||||
|
expect(firstLayoutRead).not.toHaveBeenCalled();
|
||||||
|
expect(secondLayoutRead).not.toHaveBeenCalled();
|
||||||
|
|
||||||
window.tc.settings.shortcutTargetMode = "all";
|
window.tc.settings.shortcutTargetMode = "all";
|
||||||
window.runAction("faster", 0.1);
|
window.runAction("faster", 0.1);
|
||||||
@@ -309,6 +370,51 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
|||||||
expect(second.video.playbackRate).toBe(1.2);
|
expect(second.video.playbackRate).toBe(1.2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("ignores popup actions addressed to another frame", async () => {
|
||||||
|
const chrome = bootInject();
|
||||||
|
await settleLifecycle();
|
||||||
|
const { video } = createControlledVideo();
|
||||||
|
const listener = chrome.runtime.onMessage.listeners[0];
|
||||||
|
const initialSpeed = video.playbackRate;
|
||||||
|
|
||||||
|
listener(
|
||||||
|
{
|
||||||
|
action: "run_action",
|
||||||
|
actionName: "faster",
|
||||||
|
targetFrameToken: "another-frame"
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
vi.fn()
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(video.playbackRate).toBe(initialSpeed);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes and restores controls when this tab is paused", async () => {
|
||||||
|
const chrome = bootInject();
|
||||||
|
await settleLifecycle();
|
||||||
|
const { video } = createControlledVideo();
|
||||||
|
const listener = chrome.runtime.onMessage.listeners[0];
|
||||||
|
const playbackRate = video.playbackRate;
|
||||||
|
|
||||||
|
listener(
|
||||||
|
{ action: "set_tab_paused", paused: true },
|
||||||
|
{},
|
||||||
|
vi.fn()
|
||||||
|
);
|
||||||
|
expect(window.tc.tabPaused).toBe(true);
|
||||||
|
expect(video.vsc).toBeUndefined();
|
||||||
|
expect(video.playbackRate).toBe(playbackRate);
|
||||||
|
|
||||||
|
listener(
|
||||||
|
{ action: "set_tab_paused", paused: false },
|
||||||
|
{},
|
||||||
|
vi.fn()
|
||||||
|
);
|
||||||
|
expect(window.tc.tabPaused).toBe(false);
|
||||||
|
expect(video.vsc).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it("drops a stale hover-preview shortcut target after SPA navigation", async () => {
|
it("drops a stale hover-preview shortcut target after SPA navigation", async () => {
|
||||||
bootInject({
|
bootInject({
|
||||||
url: "https://www.youtube.com/",
|
url: "https://www.youtube.com/",
|
||||||
@@ -320,6 +426,7 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
|||||||
src: "blob:https://www.youtube.com/hover-preview",
|
src: "blob:https://www.youtube.com/hover-preview",
|
||||||
mountRect: makeRect(0, 0, 320, 180)
|
mountRect: makeRect(0, 0, 320, 180)
|
||||||
});
|
});
|
||||||
|
preview.mount.id = "inline-preview-player";
|
||||||
document.dispatchEvent(
|
document.dispatchEvent(
|
||||||
new MouseEvent("mousemove", { bubbles: true, clientX: 100, clientY: 90 })
|
new MouseEvent("mousemove", { bubbles: true, clientX: 100, clientY: 90 })
|
||||||
);
|
);
|
||||||
@@ -333,6 +440,10 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
|||||||
src: "blob:https://www.youtube.com/main-player",
|
src: "blob:https://www.youtube.com/main-player",
|
||||||
mountRect: makeRect(0, 0, 1280, 720)
|
mountRect: makeRect(0, 0, 1280, 720)
|
||||||
});
|
});
|
||||||
|
main.mount.id = "movie_player";
|
||||||
|
document.dispatchEvent(
|
||||||
|
new MouseEvent("mousemove", { bubbles: true, clientX: 100, clientY: 90 })
|
||||||
|
);
|
||||||
document.dispatchEvent(
|
document.dispatchEvent(
|
||||||
new KeyboardEvent("keydown", {
|
new KeyboardEvent("keydown", {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
@@ -342,11 +453,115 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(window.tc.lastPointerPosition).toBeNull();
|
expect(window.tc.lastPointerPosition).not.toBeNull();
|
||||||
expect(preview.video.playbackRate).toBe(1);
|
expect(preview.video.playbackRate).toBe(1);
|
||||||
expect(main.video.playbackRate).toBe(1.1);
|
expect(main.video.playbackRate).toBe(1.1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("captures SPA shortcuts before later page handlers and distinguishes Shift", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
bootInject({
|
||||||
|
syncGetDelayMs: 25,
|
||||||
|
syncData: {
|
||||||
|
keyBindings: [
|
||||||
|
{ action: "advance", code: "KeyX", value: 10 },
|
||||||
|
{ action: "advance", code: "KeyX", shiftKey: true, value: 3 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(window.vscKeydownListenerAttached).toBe(true);
|
||||||
|
|
||||||
|
window.addEventListener(
|
||||||
|
"keydown",
|
||||||
|
(event) => event.stopImmediatePropagation(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
await vi.advanceTimersByTimeAsync(25);
|
||||||
|
await settleLifecycle();
|
||||||
|
|
||||||
|
const { video } = createControlledVideo();
|
||||||
|
video.currentTime = 50;
|
||||||
|
video.dispatchEvent(
|
||||||
|
new KeyboardEvent("keydown", {
|
||||||
|
bubbles: true,
|
||||||
|
code: "KeyX",
|
||||||
|
key: "x"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
video.dispatchEvent(
|
||||||
|
new KeyboardEvent("keydown", {
|
||||||
|
bubbles: true,
|
||||||
|
code: "KeyX",
|
||||||
|
key: "X",
|
||||||
|
shiftKey: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(video.currentTime).toBe(63);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores shortcuts from selects and shadow-DOM edit fields", async () => {
|
||||||
|
bootInject();
|
||||||
|
await settleLifecycle();
|
||||||
|
const { video } = createControlledVideo();
|
||||||
|
const select = document.createElement("select");
|
||||||
|
const host = document.createElement("site-editor");
|
||||||
|
const input = document.createElement("input");
|
||||||
|
host.attachShadow({ mode: "open" }).appendChild(input);
|
||||||
|
document.body.append(select, host);
|
||||||
|
|
||||||
|
[select, input].forEach((target) => {
|
||||||
|
target.dispatchEvent(
|
||||||
|
new KeyboardEvent("keydown", {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
code: "KeyD",
|
||||||
|
key: "d"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(video.playbackRate).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("finishes a forced SPA initialization after settings hydration", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
bootInject({
|
||||||
|
url: "https://www.youtube.com/",
|
||||||
|
syncGetDelayMs: 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
const mount = document.createElement("div");
|
||||||
|
const video = document.createElement("video");
|
||||||
|
const rect = makeRect(0, 0, 1280, 720);
|
||||||
|
mount.id = "movie_player";
|
||||||
|
video.src = "blob:https://www.youtube.com/main-player";
|
||||||
|
setRect(mount, rect);
|
||||||
|
setBoxMetrics(mount, rect.width, rect.height);
|
||||||
|
setRect(video, rect);
|
||||||
|
mount.appendChild(video);
|
||||||
|
document.body.appendChild(mount);
|
||||||
|
|
||||||
|
document.dispatchEvent(new Event("speeder-location-changed"));
|
||||||
|
await vi.advanceTimersByTimeAsync(300);
|
||||||
|
expect(window.tc.runtimeSettingsHydrated).toBe(false);
|
||||||
|
expect(video.vsc).toBeUndefined();
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(700);
|
||||||
|
await settleLifecycle();
|
||||||
|
|
||||||
|
expect(window.tc.runtimeSettingsHydrated).toBe(true);
|
||||||
|
expect(video.vsc).toBeTruthy();
|
||||||
|
video.dispatchEvent(
|
||||||
|
new KeyboardEvent("keydown", {
|
||||||
|
bubbles: true,
|
||||||
|
code: "KeyD",
|
||||||
|
key: "d"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(video.playbackRate).toBe(1.1);
|
||||||
|
});
|
||||||
|
|
||||||
it("skips ambient loops by default and includes them when explicitly enabled", async () => {
|
it("skips ambient loops by default and includes them when explicitly enabled", async () => {
|
||||||
bootInject();
|
bootInject();
|
||||||
await settleLifecycle();
|
await settleLifecycle();
|
||||||
@@ -461,6 +676,7 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
|||||||
div: wrapper,
|
div: wrapper,
|
||||||
normalControllerMount: normalMount
|
normalControllerMount: normalMount
|
||||||
};
|
};
|
||||||
|
wrapper.showPopover = vi.fn();
|
||||||
window.setupControllerHostTracking(controller, wrapper, normalMount);
|
window.setupControllerHostTracking(controller, wrapper, normalMount);
|
||||||
Object.defineProperty(document, "fullscreenElement", {
|
Object.defineProperty(document, "fullscreenElement", {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
@@ -469,6 +685,8 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
|||||||
|
|
||||||
window.syncControllerFullscreenMount(controller);
|
window.syncControllerFullscreenMount(controller);
|
||||||
expect(fullscreenPlayer.contains(wrapper)).toBe(true);
|
expect(fullscreenPlayer.contains(wrapper)).toBe(true);
|
||||||
|
expect(wrapper.showPopover).not.toHaveBeenCalled();
|
||||||
|
expect(wrapper.hasAttribute("popover")).toBe(false);
|
||||||
|
|
||||||
Object.defineProperty(document, "fullscreenElement", {
|
Object.defineProperty(document, "fullscreenElement", {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
@@ -481,50 +699,59 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
|||||||
controller.controllerHostCleanup();
|
controller.controllerHostCleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("promotes an ancestor-fullscreen controller into the browser top layer", async () => {
|
it("keeps a visible player-local host when Firefox fullscreens the page root", async () => {
|
||||||
|
bootInject();
|
||||||
|
await settleLifecycle();
|
||||||
|
|
||||||
|
const { mount, controller, wrapper } = createControlledVideo();
|
||||||
|
setRect(document.documentElement, makeRect(0, 0, 0, 0));
|
||||||
|
Object.defineProperty(document, "fullscreenElement", {
|
||||||
|
configurable: true,
|
||||||
|
value: document.documentElement
|
||||||
|
});
|
||||||
|
|
||||||
|
window.syncControllerFullscreenMount(controller);
|
||||||
|
|
||||||
|
expect(wrapper.parentElement).toBe(mount);
|
||||||
|
expect(wrapper.classList.contains("vsc-geometry-hidden")).toBe(false);
|
||||||
|
expect(document.documentElement.style.position).toBe("");
|
||||||
|
expect(document.documentElement.style.isolation).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only promotes the directly-fullscreen video's controller", async () => {
|
||||||
bootInject();
|
bootInject();
|
||||||
await settleLifecycle();
|
await settleLifecycle();
|
||||||
|
|
||||||
const fullscreenPlayer = document.createElement("div");
|
const fullscreenPlayer = document.createElement("div");
|
||||||
const video = document.createElement("video");
|
const fullscreenVideo = document.createElement("video");
|
||||||
const wrapper = document.createElement("div");
|
const otherVideo = document.createElement("video");
|
||||||
const rect = makeRect(0, 0, 1280, 720);
|
const rect = makeRect(0, 0, 640, 360);
|
||||||
|
|
||||||
fullscreenPlayer.append(video, wrapper);
|
fullscreenVideo.src = "https://example.org/fullscreen.mp4";
|
||||||
document.body.appendChild(fullscreenPlayer);
|
otherVideo.src = "https://example.org/other.mp4";
|
||||||
[fullscreenPlayer, video].forEach((element) => {
|
fullscreenPlayer.appendChild(fullscreenVideo);
|
||||||
|
document.body.append(fullscreenPlayer, otherVideo);
|
||||||
|
[fullscreenPlayer, fullscreenVideo, otherVideo].forEach((element) => {
|
||||||
setRect(element, rect);
|
setRect(element, rect);
|
||||||
setBoxMetrics(element, rect.width, rect.height);
|
setBoxMetrics(element, rect.width, rect.height);
|
||||||
});
|
});
|
||||||
wrapper.showPopover = vi.fn();
|
window.ensureController(fullscreenVideo, fullscreenPlayer);
|
||||||
wrapper.hidePopover = vi.fn();
|
window.ensureController(otherVideo, document.body);
|
||||||
|
fullscreenVideo.vsc.div.showPopover = vi.fn();
|
||||||
const controller = {
|
otherVideo.vsc.div.showPopover = vi.fn();
|
||||||
video,
|
|
||||||
div: wrapper,
|
|
||||||
normalControllerMount: fullscreenPlayer
|
|
||||||
};
|
|
||||||
window.setupControllerHostTracking(controller, wrapper, fullscreenPlayer);
|
|
||||||
Object.defineProperty(document, "fullscreenElement", {
|
|
||||||
configurable: true,
|
|
||||||
value: fullscreenPlayer
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(window.syncControllerFullscreenMount(controller)).toBe(true);
|
|
||||||
expect(wrapper.parentNode).toBe(fullscreenPlayer);
|
|
||||||
expect(wrapper.showPopover).toHaveBeenCalledOnce();
|
|
||||||
expect(wrapper.getAttribute("popover")).toBe("manual");
|
|
||||||
expect(wrapper.classList.contains("vsc-fullscreen-popover")).toBe(true);
|
|
||||||
|
|
||||||
Object.defineProperty(document, "fullscreenElement", {
|
Object.defineProperty(document, "fullscreenElement", {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: null
|
value: fullscreenVideo
|
||||||
});
|
});
|
||||||
window.syncControllerFullscreenMount(controller);
|
window.syncControllerFullscreenMount(fullscreenVideo.vsc);
|
||||||
expect(wrapper.hidePopover).toHaveBeenCalledOnce();
|
window.syncControllerFullscreenMount(otherVideo.vsc);
|
||||||
|
|
||||||
wrapper.remove();
|
expect(fullscreenVideo.vsc.div.showPopover).toHaveBeenCalledOnce();
|
||||||
controller.controllerHostCleanup();
|
expect(otherVideo.vsc.div.showPopover).not.toHaveBeenCalled();
|
||||||
|
expect(
|
||||||
|
otherVideo.vsc.div.classList.contains("vsc-fullscreen-popover")
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves direct-video requestFullscreen semantics and overlays with a popover", async () => {
|
it("preserves direct-video requestFullscreen semantics and overlays with a popover", async () => {
|
||||||
@@ -731,6 +958,21 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
|||||||
expect(wrapper.classList.contains("vsc-idle-hidden")).toBe(true);
|
expect(wrapper.classList.contains("vsc-idle-hidden")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("creates named native buttons for in-player controls", async () => {
|
||||||
|
bootInject({
|
||||||
|
syncData: { controllerButtons: ["rewind", "nudge"] }
|
||||||
|
});
|
||||||
|
await settleLifecycle();
|
||||||
|
const { wrapper } = createControlledVideo();
|
||||||
|
const rewind = wrapper.shadowRoot.querySelector('[data-action="rewind"]');
|
||||||
|
const nudge = wrapper.shadowRoot.querySelector("#nudge-indicator");
|
||||||
|
|
||||||
|
expect(rewind.tagName).toBe("BUTTON");
|
||||||
|
expect(rewind.getAttribute("aria-label")).toBe("Rewind");
|
||||||
|
expect(nudge.tagName).toBe("BUTTON");
|
||||||
|
expect(nudge.getAttribute("aria-label")).toContain("Subtitle nudge");
|
||||||
|
});
|
||||||
|
|
||||||
it("does not let YouTube auto-hide collapse controls under the pointer", async () => {
|
it("does not let YouTube auto-hide collapse controls under the pointer", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
bootInject({
|
bootInject({
|
||||||
@@ -805,9 +1047,13 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
|||||||
videoRect: makeRect(40, 40, 640, 360),
|
videoRect: makeRect(40, 40, 640, 360),
|
||||||
src: "https://example.org/visible.mp4"
|
src: "https://example.org/visible.mp4"
|
||||||
}).video;
|
}).video;
|
||||||
|
const offscreenLayoutRead = vi.spyOn(offscreen, "getBoundingClientRect");
|
||||||
|
const visibleLayoutRead = vi.spyOn(visible, "getBoundingClientRect");
|
||||||
|
|
||||||
expect(window.getPrimaryVideoElement()).toBe(visible);
|
expect(window.getPrimaryVideoElement()).toBe(visible);
|
||||||
expect(window.getPrimaryVideoElement()).not.toBe(offscreen);
|
expect(window.getPrimaryVideoElement()).not.toBe(offscreen);
|
||||||
|
expect(offscreenLayoutRead).not.toHaveBeenCalled();
|
||||||
|
expect(visibleLayoutRead).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("mounts a controller locally for video directly under an open ShadowRoot", async () => {
|
it("mounts a controller locally for video directly under an open ShadowRoot", async () => {
|
||||||
|
|||||||
@@ -29,7 +29,10 @@ describe("lucide-client.js", () => {
|
|||||||
<svg width="10" height="10" onclick="evil()">
|
<svg width="10" height="10" onclick="evil()">
|
||||||
<script>alert(1)</script>
|
<script>alert(1)</script>
|
||||||
<foreignObject>bad</foreignObject>
|
<foreignObject>bad</foreignObject>
|
||||||
<path d="M0 0h10v10"></path>
|
<image href="https://tracking.example/icon.png"></image>
|
||||||
|
<use href="https://tracking.example/sprite.svg#icon"></use>
|
||||||
|
<animate attributeName="opacity" values="0;1"></animate>
|
||||||
|
<path style="fill: url(https://tracking.example/a)" fill="url(#paint)" d="M0 0h10v10"></path>
|
||||||
</svg>
|
</svg>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -37,6 +40,12 @@ describe("lucide-client.js", () => {
|
|||||||
expect(sanitized).not.toContain("onclick");
|
expect(sanitized).not.toContain("onclick");
|
||||||
expect(sanitized).not.toContain("<script");
|
expect(sanitized).not.toContain("<script");
|
||||||
expect(sanitized).not.toContain("foreignObject");
|
expect(sanitized).not.toContain("foreignObject");
|
||||||
|
expect(sanitized).not.toContain("tracking.example");
|
||||||
|
expect(sanitized).not.toContain("<image");
|
||||||
|
expect(sanitized).not.toContain("<use");
|
||||||
|
expect(sanitized).not.toContain("<animate");
|
||||||
|
expect(sanitized).not.toContain("style=");
|
||||||
|
expect(sanitized).not.toContain("url(");
|
||||||
expect(sanitized).toContain('width="100%"');
|
expect(sanitized).toContain('width="100%"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -91,12 +91,58 @@ describe("options page", () => {
|
|||||||
expect(globalThis.getPopupControlBarOrder()).toEqual(["rewind", "advance"]);
|
expect(globalThis.getPopupControlBarOrder()).toEqual(["rewind", "advance"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reorders and toggles control-bar buttons from the keyboard", async () => {
|
||||||
|
await setupOptions({
|
||||||
|
sync: { controllerButtons: ["rewind", "faster"] }
|
||||||
|
});
|
||||||
|
const active = document.getElementById("controlBarActive");
|
||||||
|
const rewind = active.querySelector('[data-button-id="rewind"]');
|
||||||
|
|
||||||
|
rewind.dispatchEvent(
|
||||||
|
new window.KeyboardEvent("keydown", {
|
||||||
|
key: "ArrowRight",
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(globalThis.getControlBarOrder()).toEqual(["faster", "rewind"]);
|
||||||
|
|
||||||
|
expect(rewind.disabled).toBe(false);
|
||||||
|
rewind.click();
|
||||||
|
expect(globalThis.getControlBarOrder()).toEqual(["faster"]);
|
||||||
|
expect(rewind.closest(".cb-available-zone")).not.toBeNull();
|
||||||
|
expect(rewind.getAttribute("aria-label")).toContain("available");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("labels shortcut and generated site-rule controls", async () => {
|
||||||
|
await setupOptions();
|
||||||
|
expect(document.getElementById("lucideIconResults").getAttribute("role")).toBe(
|
||||||
|
"group"
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
document.querySelector('#display .customKey').getAttribute("aria-label")
|
||||||
|
).toBe("Show/hide controller key");
|
||||||
|
|
||||||
|
globalThis.createSiteRule(null);
|
||||||
|
const rule = document.querySelector(".site-rule");
|
||||||
|
const location = rule.querySelector(".site-controllerLocation");
|
||||||
|
const locationLabel = location.closest(".site-rule-option").querySelector("label");
|
||||||
|
expect(location.id).not.toBe("");
|
||||||
|
expect(locationLabel.htmlFor).toBe(location.id);
|
||||||
|
expect(
|
||||||
|
rule.querySelector(".site-controllerMarginTop").getAttribute("aria-label")
|
||||||
|
).toBe("Controller margin top");
|
||||||
|
expect(
|
||||||
|
rule.querySelector(".remove-site-rule").getAttribute("aria-label")
|
||||||
|
).toBe("Remove site rule");
|
||||||
|
});
|
||||||
|
|
||||||
it("validates site rule regexes before saving", async () => {
|
it("validates site rule regexes before saving", async () => {
|
||||||
const chrome = await setupOptions();
|
const chrome = await setupOptions();
|
||||||
chrome.storage.sync.set.mockClear();
|
chrome.storage.sync.set.mockClear();
|
||||||
globalThis.createSiteRule(null);
|
globalThis.createSiteRule(null);
|
||||||
const rule = document.querySelector(".site-rule");
|
const rule = document.querySelector(".site-rule");
|
||||||
rule.querySelector(".site-pattern").value = "/(/";
|
rule.querySelector(".site-pattern").value = "/youtube";
|
||||||
|
|
||||||
globalThis.save_options();
|
globalThis.save_options();
|
||||||
|
|
||||||
@@ -106,6 +152,48 @@ describe("options page", () => {
|
|||||||
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
|
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds and restores duplicate actions with Shift bindings", async () => {
|
||||||
|
const chrome = await setupOptions();
|
||||||
|
const selector = document.getElementById("addShortcutSelector");
|
||||||
|
expect(selector.querySelector('option[value="rewind"]')).not.toBeNull();
|
||||||
|
|
||||||
|
selector.value = "rewind";
|
||||||
|
selector.dispatchEvent(new window.Event("change", { bubbles: true }));
|
||||||
|
const duplicate = document.querySelector(
|
||||||
|
'.shortcut-row.customs[data-action="rewind"]'
|
||||||
|
);
|
||||||
|
duplicate.querySelector(".customKey").dispatchEvent(
|
||||||
|
new window.KeyboardEvent("keydown", {
|
||||||
|
key: "Z",
|
||||||
|
code: "KeyZ",
|
||||||
|
shiftKey: true,
|
||||||
|
bubbles: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
duplicate.querySelector(".customValue").value = "3";
|
||||||
|
|
||||||
|
globalThis.save_options();
|
||||||
|
expect(
|
||||||
|
chrome.storage.sync.__state.keyBindings.filter(
|
||||||
|
(binding) => binding.action === "rewind"
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
expect.objectContaining({ value: 10, shiftKey: false }),
|
||||||
|
expect.objectContaining({ value: 3, shiftKey: true })
|
||||||
|
]);
|
||||||
|
|
||||||
|
globalThis.restore_options();
|
||||||
|
await flushAsyncWork();
|
||||||
|
expect(
|
||||||
|
document.querySelectorAll('.shortcut-row[data-action="rewind"]')
|
||||||
|
).toHaveLength(2);
|
||||||
|
expect(
|
||||||
|
document.querySelector(
|
||||||
|
'.shortcut-row.customs[data-action="rewind"] .customKey'
|
||||||
|
).value
|
||||||
|
).toBe("Shift+Z");
|
||||||
|
});
|
||||||
|
|
||||||
it("shows a more-menu trigger for collapsed site rules and a collapse trigger when open", async () => {
|
it("shows a more-menu trigger for collapsed site rules and a collapse trigger when open", async () => {
|
||||||
await setupOptions({ sync: { siteRules: [] } });
|
await setupOptions({ sync: { siteRules: [] } });
|
||||||
|
|
||||||
@@ -302,4 +390,22 @@ describe("options page", () => {
|
|||||||
).toEqual(expect.any(Number));
|
).toEqual(expect.any(Number));
|
||||||
expect(chrome.storage.local.__state.unrelatedLocalValue).toBe("keep");
|
expect(chrome.storage.local.__state.unrelatedLocalValue).toBe("keep");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("leaves settings untouched when restoring defaults is cancelled", async () => {
|
||||||
|
const chrome = await setupOptions({ sync: { rememberSpeed: true } });
|
||||||
|
window.confirm.mockReturnValueOnce(false);
|
||||||
|
chrome.storage.sync.set.mockClear();
|
||||||
|
chrome.storage.sync.remove.mockClear();
|
||||||
|
chrome.storage.local.set.mockClear();
|
||||||
|
chrome.storage.local.remove.mockClear();
|
||||||
|
|
||||||
|
globalThis.restore_defaults();
|
||||||
|
|
||||||
|
expect(window.confirm).toHaveBeenCalledOnce();
|
||||||
|
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
|
||||||
|
expect(chrome.storage.sync.remove).not.toHaveBeenCalled();
|
||||||
|
expect(chrome.storage.local.set).not.toHaveBeenCalled();
|
||||||
|
expect(chrome.storage.local.remove).not.toHaveBeenCalled();
|
||||||
|
expect(chrome.storage.sync.__state.rememberSpeed).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -67,6 +67,59 @@ describe("options.js", () => {
|
|||||||
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
|
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("automatically saves changed settings", async () => {
|
||||||
|
const chrome = bootOptions({ syncData: { rememberSpeed: false } });
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
vi.useFakeTimers();
|
||||||
|
chrome.storage.sync.set.mockClear();
|
||||||
|
|
||||||
|
const rememberSpeed = document.getElementById("rememberSpeed");
|
||||||
|
rememberSpeed.checked = true;
|
||||||
|
rememberSpeed.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
|
||||||
|
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
|
||||||
|
await vi.advanceTimersByTimeAsync(300);
|
||||||
|
|
||||||
|
expect(chrome.storage.sync._dump().rememberSpeed).toBe(true);
|
||||||
|
expect(document.getElementById("status").textContent).toBe("Auto-saved");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("copies privacy-safe diagnostics from settings", async () => {
|
||||||
|
bootOptions({
|
||||||
|
syncData: {
|
||||||
|
enabled: false,
|
||||||
|
rememberSpeed: true,
|
||||||
|
siteRules: [
|
||||||
|
{
|
||||||
|
title: "Private account",
|
||||||
|
pattern: "secret.example/private-token",
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const writeText = vi.fn(() => Promise.resolve());
|
||||||
|
Object.defineProperty(navigator, "clipboard", {
|
||||||
|
configurable: true,
|
||||||
|
value: { writeText }
|
||||||
|
});
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
|
||||||
|
document.getElementById("copyDiagnostics").click();
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
|
||||||
|
const reportText = writeText.mock.calls[0][0];
|
||||||
|
const report = JSON.parse(reportText);
|
||||||
|
expect(report.globalSettings.enabled).toBe(false);
|
||||||
|
expect(report.globalSettings.rememberSpeed).toBe(true);
|
||||||
|
expect(report.page).toEqual({ protocol: null, hostname: null });
|
||||||
|
expect(report.matchedSiteRule.matched).toBe(false);
|
||||||
|
expect(report.frame).toBeNull();
|
||||||
|
expect(reportText).not.toContain("Private account");
|
||||||
|
expect(reportText).not.toContain("private-token");
|
||||||
|
expect(document.getElementById("status").textContent).toContain("copied");
|
||||||
|
});
|
||||||
|
|
||||||
it("does not partially save options when a required site shortcut is invalid", async () => {
|
it("does not partially save options when a required site shortcut is invalid", async () => {
|
||||||
const chrome = bootOptions({ syncData: { rememberSpeed: false } });
|
const chrome = bootOptions({ syncData: { rememberSpeed: false } });
|
||||||
await flushAsyncWork(3);
|
await flushAsyncWork(3);
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ describe("popup UI", () => {
|
|||||||
expect(
|
expect(
|
||||||
document.querySelectorAll("#popupControlBar button").length
|
document.querySelectorAll("#popupControlBar button").length
|
||||||
).toBeGreaterThan(0);
|
).toBeGreaterThan(0);
|
||||||
|
expect(document.getElementById("status").getAttribute("role")).toBe(
|
||||||
|
"status"
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
document.querySelector("#popupControlBar button").getAttribute("aria-label")
|
||||||
|
).not.toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows controls when globally disabled but a whitelist site rule matches", async () => {
|
it("shows controls when globally disabled but a whitelist site rule matches", async () => {
|
||||||
@@ -70,7 +76,7 @@ describe("popup UI", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("toggles enabled state and updates the browser action icons", async () => {
|
it("toggles enabled state without owning background icon state", async () => {
|
||||||
const chrome = await setupPopup();
|
const chrome = await setupPopup();
|
||||||
chrome.storage.sync.set.mockClear();
|
chrome.storage.sync.set.mockClear();
|
||||||
chrome.browserAction.setIcon.mockClear();
|
chrome.browserAction.setIcon.mockClear();
|
||||||
@@ -84,13 +90,10 @@ describe("popup UI", () => {
|
|||||||
expect(document.getElementById("enable").classList.contains("hide")).toBe(
|
expect(document.getElementById("enable").classList.contains("hide")).toBe(
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
expect(chrome.browserAction.setIcon).toHaveBeenCalledWith({
|
expect(document.getElementById("status").textContent).toBe(
|
||||||
path: {
|
"Disabled. Open pages update automatically."
|
||||||
19: "assets/icons/icon19_disabled.png",
|
);
|
||||||
38: "assets/icons/icon38_disabled.png",
|
expect(chrome.browserAction.setIcon).not.toHaveBeenCalled();
|
||||||
48: "assets/icons/icon48_disabled.png"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles refresh responses for unsupported and successful pages", async () => {
|
it("handles refresh responses for unsupported and successful pages", async () => {
|
||||||
|
|||||||
+129
-10
@@ -18,6 +18,7 @@ function bootPopup(options) {
|
|||||||
manifest: { version: "9.9.9-test" },
|
manifest: { version: "9.9.9-test" },
|
||||||
syncData: config.syncData,
|
syncData: config.syncData,
|
||||||
localData: config.localData,
|
localData: config.localData,
|
||||||
|
runtimeSendMessageImpl: config.runtimeSendMessageImpl,
|
||||||
tabsQueryResult: [
|
tabsQueryResult: [
|
||||||
config.activeTab || { id: 99, active: true, url: "https://example.com/" }
|
config.activeTab || { id: 99, active: true, url: "https://example.com/" }
|
||||||
]
|
]
|
||||||
@@ -107,10 +108,10 @@ describe("popup.js", () => {
|
|||||||
executeScriptImpl: (tabId, details, callback) => {
|
executeScriptImpl: (tabId, details, callback) => {
|
||||||
speedQueryCount += 1;
|
speedQueryCount += 1;
|
||||||
callback(
|
callback(
|
||||||
speedQueryCount <= 2
|
speedQueryCount === 1
|
||||||
? [
|
? [
|
||||||
{ speed: 1.25, preferred: false },
|
{ speed: 1.25, preferred: false },
|
||||||
{ speed: 1.5, preferred: true }
|
{ speed: 1.5, frameToken: "playing-frame", preferred: true }
|
||||||
]
|
]
|
||||||
: [{ speed: 1.75, preferred: true }]
|
: [{ speed: 1.75, preferred: true }]
|
||||||
);
|
);
|
||||||
@@ -138,12 +139,136 @@ describe("popup.js", () => {
|
|||||||
|
|
||||||
expect(chrome.tabs.sendMessage).toHaveBeenCalledWith(
|
expect(chrome.tabs.sendMessage).toHaveBeenCalledWith(
|
||||||
99,
|
99,
|
||||||
{ action: "run_action", actionName: "faster" },
|
{
|
||||||
|
action: "run_action",
|
||||||
|
actionName: "faster",
|
||||||
|
targetFrameToken: "playing-frame"
|
||||||
|
},
|
||||||
expect.any(Function)
|
expect.any(Function)
|
||||||
);
|
);
|
||||||
expect(document.querySelector("#popupSpeed").textContent).toBe("1.75");
|
expect(document.querySelector("#popupSpeed").textContent).toBe("1.75");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps all-video popup actions intentionally untargeted", async () => {
|
||||||
|
const chrome = bootPopup({
|
||||||
|
syncData: { shortcutTargetMode: "all" },
|
||||||
|
executeScriptImpl: (tabId, details, callback) => {
|
||||||
|
callback([{ speed: 1.5, frameToken: "playing-frame", preferred: true }]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await flushAsyncWork();
|
||||||
|
chrome.tabs.sendMessage.mockClear();
|
||||||
|
|
||||||
|
document.querySelector("#popupControlBar button").click();
|
||||||
|
|
||||||
|
expect(chrome.tabs.sendMessage).toHaveBeenCalledWith(
|
||||||
|
99,
|
||||||
|
{ action: "run_action", actionName: "rewind" },
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("copies redacted diagnostics for the active media frame", async () => {
|
||||||
|
bootPopup({
|
||||||
|
activeTab: {
|
||||||
|
id: 12,
|
||||||
|
active: true,
|
||||||
|
url: "https://video.example/watch?private_token=secret"
|
||||||
|
},
|
||||||
|
executeScriptImpl: (tabId, details, callback) => {
|
||||||
|
callback([
|
||||||
|
{
|
||||||
|
speed: 1.5,
|
||||||
|
preferred: true,
|
||||||
|
diagnostics: {
|
||||||
|
mediaType: "video",
|
||||||
|
fullscreen: { active: true, element: "div", ownsMedia: true },
|
||||||
|
controller: { present: true, hidden: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const writeText = vi.fn(() => Promise.resolve());
|
||||||
|
Object.defineProperty(navigator, "clipboard", {
|
||||||
|
configurable: true,
|
||||||
|
value: { writeText }
|
||||||
|
});
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
document.querySelector("#copyDiagnostics").click();
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
expect(writeText).toHaveBeenCalled();
|
||||||
|
const reportText = writeText.mock.calls.at(-1)[0];
|
||||||
|
const report = JSON.parse(reportText);
|
||||||
|
expect(report.page).toEqual({
|
||||||
|
protocol: "https:",
|
||||||
|
hostname: "video.example"
|
||||||
|
});
|
||||||
|
expect(report.frame.mediaType).toBe("video");
|
||||||
|
expect(reportText).not.toContain("private_token");
|
||||||
|
expect(reportText).not.toContain("secret");
|
||||||
|
expect(document.querySelector("#status").textContent).toContain("copied");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds one safe origin rule for the current site", async () => {
|
||||||
|
const chrome = bootPopup({
|
||||||
|
activeTab: {
|
||||||
|
id: 18,
|
||||||
|
active: true,
|
||||||
|
url: "https://courses.example:8443/watch/lesson?token=private"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
document.querySelector("#addSiteRule").click();
|
||||||
|
await flushAsyncWork();
|
||||||
|
document.querySelector("#addSiteRule").click();
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
const rules = window.vscExpandStoredSettings(
|
||||||
|
chrome.storage.sync._dump()
|
||||||
|
).siteRules.filter(function(rule) {
|
||||||
|
return rule.pattern === "https://courses.example:8443";
|
||||||
|
});
|
||||||
|
expect(rules).toEqual([
|
||||||
|
{
|
||||||
|
title: "courses.example:8443",
|
||||||
|
pattern: "https://courses.example:8443",
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
expect(window.open).toHaveBeenCalledWith(
|
||||||
|
"moz-extension://options/options.html"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pauses only the active tab for the browser session", async () => {
|
||||||
|
let paused = false;
|
||||||
|
const chrome = bootPopup({
|
||||||
|
runtimeSendMessageImpl: (message, callback) => {
|
||||||
|
if (message.action === "set_tab_paused") paused = message.paused === true;
|
||||||
|
callback({ paused });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
document.querySelector("#pauseTab").click();
|
||||||
|
|
||||||
|
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith(
|
||||||
|
{ action: "set_tab_paused", tabId: 99, paused: true },
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(document.querySelector("#pauseTab").textContent).toBe(
|
||||||
|
"Resume on this tab"
|
||||||
|
);
|
||||||
|
expect(document.querySelector("#popupControlBar").style.display).toBe(
|
||||||
|
"none"
|
||||||
|
);
|
||||||
|
expect(document.querySelector("#status").textContent).toContain("paused");
|
||||||
|
});
|
||||||
|
|
||||||
it("toggles enablement and closes after a successful refresh", async () => {
|
it("toggles enablement and closes after a successful refresh", async () => {
|
||||||
const chrome = bootPopup({
|
const chrome = bootPopup({
|
||||||
syncData: {
|
syncData: {
|
||||||
@@ -162,13 +287,7 @@ describe("popup.js", () => {
|
|||||||
expect(
|
expect(
|
||||||
window.vscExpandStoredSettings(chrome.storage.sync._dump()).enabled
|
window.vscExpandStoredSettings(chrome.storage.sync._dump()).enabled
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(chrome.browserAction.setIcon).toHaveBeenCalledWith({
|
expect(chrome.browserAction.setIcon).not.toHaveBeenCalled();
|
||||||
path: {
|
|
||||||
19: "assets/icons/icon19.png",
|
|
||||||
38: "assets/icons/icon38.png",
|
|
||||||
48: "assets/icons/icon48.png"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector("#refresh").click();
|
document.querySelector("#refresh").click();
|
||||||
expect(document.querySelector("#status").textContent).toContain("Closing");
|
expect(document.querySelector("#status").textContent).toContain("Closing");
|
||||||
|
|||||||
@@ -89,6 +89,17 @@ describe("canonical settings storage", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps every managed setting recognizable in legacy raw backups", () => {
|
||||||
|
evaluateScript("extension/shared/import-export.js");
|
||||||
|
const importExport = window.SpeederShared.importExport;
|
||||||
|
|
||||||
|
window.vscGetManagedSyncKeys().forEach(function (key) {
|
||||||
|
expect(importExport.isRecognizedRawSettingsObject({ [key]: null })).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("provides titles for built-in video rules and round-trips title edits sparsely", () => {
|
it("provides titles for built-in video rules and round-trips title edits sparsely", () => {
|
||||||
const settings = window.vscExpandStoredSettings({});
|
const settings = window.vscExpandStoredSettings({});
|
||||||
expect(settings.siteRules.map((rule) => rule.title)).toEqual([
|
expect(settings.siteRules.map((rule) => rule.title)).toEqual([
|
||||||
@@ -244,7 +255,7 @@ describe("canonical settings storage", () => {
|
|||||||
forceLastSavedSpeed: "true",
|
forceLastSavedSpeed: "true",
|
||||||
controllerOpacity: 3,
|
controllerOpacity: 3,
|
||||||
subtitleNudgeInterval: -5,
|
subtitleNudgeInterval: -5,
|
||||||
preferredSpeed: 50
|
preferredSpeed: 500
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ describe("shared helpers", () => {
|
|||||||
])
|
])
|
||||||
).toBeNull();
|
).toBeNull();
|
||||||
expect(siteRules.isSiteRuleDisabled({ enabled: false })).toBe(true);
|
expect(siteRules.isSiteRuleDisabled({ enabled: false })).toBe(true);
|
||||||
|
expect(siteRules.compileSiteRulePattern("/youtube")).toBeNull();
|
||||||
|
expect(
|
||||||
|
siteRules.siteRuleMatchesUrl(
|
||||||
|
{ pattern: "/youtube" },
|
||||||
|
"https://youtube.com/watch"
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("matches plain and regex rules against safely decoded URL text", () => {
|
it("matches plain and regex rules against safely decoded URL text", () => {
|
||||||
@@ -134,6 +141,15 @@ describe("shared helpers", () => {
|
|||||||
).toEqual(["advance"]);
|
).toEqual(["advance"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps the selected frame token with the displayed popup speed", () => {
|
||||||
|
expect(
|
||||||
|
popupControls.pickBestFrameSpeedResult([
|
||||||
|
{ speed: 1.25, frameToken: "background", preferred: false },
|
||||||
|
{ speed: 1.75, frameToken: "playing", preferred: true }
|
||||||
|
])
|
||||||
|
).toEqual({ speed: 1.75, frameToken: "playing" });
|
||||||
|
});
|
||||||
|
|
||||||
it("normalizes controller locations and margins", () => {
|
it("normalizes controller locations and margins", () => {
|
||||||
expect(controllerUtils.normalizeControllerLocation("top-right")).toBe(
|
expect(controllerUtils.normalizeControllerLocation("top-right")).toBe(
|
||||||
"top-right"
|
"top-right"
|
||||||
@@ -158,6 +174,14 @@ describe("shared helpers", () => {
|
|||||||
"NumpadAdd"
|
"NumpadAdd"
|
||||||
);
|
);
|
||||||
expect(keyBindingUtils.getLegacyKeyCode({ key: 65 })).toBe(65);
|
expect(keyBindingUtils.getLegacyKeyCode({ key: 65 })).toBe(65);
|
||||||
|
expect(keyBindingUtils.getActionValueError("fast", 0.1)).toBeNull();
|
||||||
|
expect(keyBindingUtils.getActionValueError("fast", 16)).toBeNull();
|
||||||
|
expect(keyBindingUtils.getActionValueError("fast", 0.099)).toContain(
|
||||||
|
"between 0.1 and 16"
|
||||||
|
);
|
||||||
|
expect(keyBindingUtils.getActionValueError("fast", 16.001)).toContain(
|
||||||
|
"between 0.1 and 16"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds and parses import/export payloads", () => {
|
it("builds and parses import/export payloads", () => {
|
||||||
@@ -217,6 +241,24 @@ describe("shared helpers", () => {
|
|||||||
localSettings: null
|
localSettings: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
importExportUtils.extractImportSettings({
|
||||||
|
showAmbientLoopControls: true
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
isWrappedBackup: false,
|
||||||
|
settings: { showAmbientLoopControls: true },
|
||||||
|
localSettings: null
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
importExportUtils.extractImportSettings({ shortcutTargetMode: "closest" })
|
||||||
|
).toEqual({
|
||||||
|
isWrappedBackup: false,
|
||||||
|
settings: { shortcutTargetMode: "closest" },
|
||||||
|
localSettings: null
|
||||||
|
});
|
||||||
|
|
||||||
expect(importExportUtils.isRecognizedRawSettingsObject({ wat: true })).toBe(
|
expect(importExportUtils.isRecognizedRawSettingsObject({ wat: true })).toBe(
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ module.exports = defineConfig({
|
|||||||
test: {
|
test: {
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
clearMocks: true,
|
clearMocks: true,
|
||||||
|
fileParallelism: false,
|
||||||
globals: true,
|
globals: true,
|
||||||
restoreMocks: true,
|
restoreMocks: true,
|
||||||
|
testTimeout: 15000,
|
||||||
include: ["tests/**/*.test.js", "tests/**/*.spec.js"],
|
include: ["tests/**/*.test.js", "tests/**/*.spec.js"],
|
||||||
setupFiles: ["./tests/setup.js"]
|
setupFiles: ["./tests/setup.js"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user