mirror of
https://github.com/SoPat712/Speeder.git
synced 2026-08-20 12:06:19 -04:00
Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5d697b7ea
|
||
|
|
7c9259e5ea
|
||
|
|
0872b66686
|
||
|
|
eede171ad9
|
||
|
|
15673b0a38
|
||
|
|
06b23b3a53
|
||
|
|
807bd0a29d
|
||
|
|
6d17f15bc0
|
||
|
|
8073014bce
|
||
|
|
f8cfbf05c8
|
||
|
|
b4234e0387
|
||
|
|
d55006dd0a
|
||
|
|
745af2f1b9
|
||
|
|
7e9ce436d3
|
||
|
|
7e3054f6fa
|
||
|
|
98546adf41
|
||
|
|
daeff63d2a
|
||
|
|
80aa00670c
|
||
|
|
5af0200008
|
||
|
|
13350e5cea
|
||
|
|
461c8c448f
|
||
|
|
39c1c76db1
|
||
|
|
f8cbcd4368
|
||
|
|
e5ae1ed4ba
|
||
|
|
1ae7bbdee3
|
||
|
|
f1e8f44f42
|
||
|
|
7fd8e5d92d
|
||
|
|
fd9911855a
|
||
|
|
5b8f2422ab
|
||
|
|
c429215626
|
||
|
|
432ed37f45
|
||
|
|
51caae3b7c
|
||
|
|
5df0bbc551
|
@@ -12,9 +12,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
run: npm test
|
||||
|
||||
- name: Install web-ext
|
||||
run: npm install -g web-ext
|
||||
run: npm install -g web-ext@10.6.0
|
||||
|
||||
- name: Lint
|
||||
run: web-ext lint --source-dir extension
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
|
||||
- name: Create GitHub Prerelease (beta)
|
||||
if: contains(github.ref_name, '-beta')
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: ${{ github.ref_name }}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
# Implementation Plan
|
||||
|
||||
Constraints: local commits only; no push; no browser testing; one commit per feature.
|
||||
|
||||
## Controller and targeting
|
||||
|
||||
- [x] Keep the controller visible for its own video in element and ancestor fullscreen.
|
||||
- [x] Target popup actions at the frame represented by the displayed speed; keep “all videos” intentional.
|
||||
- [x] Ignore shortcuts originating from editable controls, including shadow-DOM inputs.
|
||||
|
||||
## Accessibility and usability
|
||||
|
||||
- [x] Give in-player controls accessible names, keyboard behavior, and visible focus.
|
||||
- [x] Make control-bar customization operable by keyboard as well as drag and drop.
|
||||
- [x] Label generated shortcut and site-rule form controls.
|
||||
- [x] Improve popup status announcements, focus indicators, and icon-search semantics.
|
||||
|
||||
## Settings safety and validation
|
||||
|
||||
- [x] Confirm before Restore Defaults removes preferences and remembered data.
|
||||
- [x] Report partial imports accurately when custom icons cannot be restored.
|
||||
- [x] Reject malformed slash-prefixed regular expressions before saving site rules.
|
||||
|
||||
## Extension lifecycle and copy
|
||||
|
||||
- [x] Initialize and synchronize the disabled toolbar icon from background state.
|
||||
- [x] Correct shortcut, subtitle-nudge, live-update, and obsolete troubleshooting copy.
|
||||
- [x] Run automated tests in the release workflow before packaging.
|
||||
|
||||
## Verification
|
||||
|
||||
- [x] Run focused automated checks after each non-trivial change.
|
||||
- [x] Run the complete non-browser test suite and review the final local commit series.
|
||||
- [x] Leave cross-site fullscreen visual verification for reporter/user validation.
|
||||
@@ -72,7 +72,7 @@ The unpacked extension root is `extension/`. Load that directory in
|
||||
|
||||
```sh
|
||||
npm test
|
||||
npx --yes web-ext lint --source-dir extension
|
||||
npx --yes web-ext@10.6.0 lint --source-dir extension
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
@@ -9,6 +9,15 @@ function setToolbarIcon(enabled) {
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
@@ -19,8 +28,40 @@ chrome.storage.onChanged.addListener(function(changes, areaName) {
|
||||
}
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener(function (request) {
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
typeof tc === "object" && typeof tc.frameToken === "string"
|
||||
? tc.frameToken
|
||||
: null,
|
||||
diagnostics:
|
||||
typeof getDiagnosticsSnapshot === "function"
|
||||
? getDiagnosticsSnapshot(v)
|
||||
: null,
|
||||
preferred: !v.paused,
|
||||
forceLastSavedSpeed: Boolean(
|
||||
typeof tc === "object" && tc.settings && tc.settings.forceLastSavedSpeed
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
/* Base styles for the controller wrapper (the shadow host) */
|
||||
.vsc-controller {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
pointer-events: none !important;
|
||||
/* Keep the interactive controller above player-owned click/pause panes. */
|
||||
z-index: 2147483647 !important;
|
||||
@@ -17,7 +13,6 @@
|
||||
controller in the browser top layer without nesting it inside <video>. */
|
||||
.vsc-controller.vsc-fullscreen-popover {
|
||||
position: fixed !important;
|
||||
inset: auto !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
|
||||
+184
-76
@@ -1,4 +1,3 @@
|
||||
var isUserSeek = false; // Track if seek was user-initiated
|
||||
var lastToggleSpeed = {}; // Store last toggle speeds per video
|
||||
var speederShared =
|
||||
typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
|
||||
@@ -28,6 +27,11 @@ function getSharedDefault(key, fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function getCachedVideoRect(video) {
|
||||
var wrapper = video && video.vsc && video.vsc.div;
|
||||
return (wrapper && wrapper.vscVideoRect) || null;
|
||||
}
|
||||
|
||||
function getPrimaryVideoElement(mediaElements) {
|
||||
var candidates = Array.isArray(mediaElements)
|
||||
? mediaElements
|
||||
@@ -39,10 +43,13 @@ function getPrimaryVideoElement(mediaElements) {
|
||||
candidates.forEach(function(el, index) {
|
||||
if (!el || !el.vsc || !el.isConnected) return;
|
||||
|
||||
var rect = null;
|
||||
try {
|
||||
rect = el.getBoundingClientRect();
|
||||
} catch (_error) {}
|
||||
var rect = getCachedVideoRect(el);
|
||||
var hasCachedRect = Boolean(rect);
|
||||
if (!rect) {
|
||||
try {
|
||||
rect = el.getBoundingClientRect();
|
||||
} catch (_error) {}
|
||||
}
|
||||
|
||||
var width = rect && Number(rect.width) > 0 ? Number(rect.width) : 0;
|
||||
var height = rect && Number(rect.height) > 0 ? Number(rect.height) : 0;
|
||||
@@ -57,17 +64,24 @@ function getPrimaryVideoElement(mediaElements) {
|
||||
: 0;
|
||||
var visibleArea = visibleWidth * visibleHeight;
|
||||
var visuallyAvailable = visibleArea > 0;
|
||||
try {
|
||||
var computed = win && win.getComputedStyle(el);
|
||||
if (
|
||||
computed &&
|
||||
(computed.display === "none" ||
|
||||
computed.visibility === "hidden" ||
|
||||
Number(computed.opacity) === 0)
|
||||
) {
|
||||
visuallyAvailable = false;
|
||||
}
|
||||
} catch (_error) {}
|
||||
if (
|
||||
el.vsc.div &&
|
||||
el.vsc.div.classList.contains("vsc-geometry-hidden")
|
||||
) {
|
||||
visuallyAvailable = false;
|
||||
} else if (!hasCachedRect) {
|
||||
try {
|
||||
var computed = win && win.getComputedStyle(el);
|
||||
if (
|
||||
computed &&
|
||||
(computed.display === "none" ||
|
||||
computed.visibility === "hidden" ||
|
||||
Number(computed.opacity) === 0)
|
||||
) {
|
||||
visuallyAvailable = false;
|
||||
}
|
||||
} catch (_error) {}
|
||||
}
|
||||
|
||||
var score = visibleArea;
|
||||
if (visuallyAvailable) score += 1e12;
|
||||
@@ -90,6 +104,81 @@ function getPrimaryVideoElement(mediaElements) {
|
||||
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 = {
|
||||
settings: {
|
||||
lastSpeed: getSharedDefault("lastSpeed", 1.0),
|
||||
@@ -151,6 +240,7 @@ var tc = {
|
||||
speedAccessTimes: {},
|
||||
persistedLastSpeed: 1.0,
|
||||
activeSiteRule: null,
|
||||
tabPaused: false,
|
||||
siteRuleBase: null,
|
||||
runtimeSettingsHydrated: false,
|
||||
pendingMediaCandidates: [],
|
||||
@@ -163,6 +253,29 @@ var tc = {
|
||||
: String(Date.now()) + "-" + Math.random().toString(36).slice(2)
|
||||
};
|
||||
|
||||
function isSpeederActiveForCurrentPage() {
|
||||
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;
|
||||
@@ -1516,7 +1629,7 @@ function ensureController(node, parent) {
|
||||
// href selects site rules; re-run on every new/usable media so all runtime
|
||||
// paths agree on activation and effective settings.
|
||||
applySiteRuleOverrides();
|
||||
if (!siteRuleUtils.isSpeederActiveForSite(tc.settings.enabled, tc.activeSiteRule)) {
|
||||
if (!isSpeederActiveForCurrentPage()) {
|
||||
if (node.vsc) removeController(node);
|
||||
return null;
|
||||
}
|
||||
@@ -2324,12 +2437,24 @@ function loadInitialRuntimeSettings(attempt) {
|
||||
return;
|
||||
}
|
||||
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
|
||||
// Add a listener for messages from the popup.
|
||||
// We use a global flag to ensure the listener is only attached once.
|
||||
if (!window.vscMessageListener) {
|
||||
chrome.runtime.onMessage.addListener(
|
||||
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") {
|
||||
log("Re-scan command received from popup.", 4);
|
||||
initializeWhenReady(document, true);
|
||||
@@ -2344,6 +2469,7 @@ function loadInitialRuntimeSettings(attempt) {
|
||||
sendResponse({
|
||||
speed: videoGs.playbackRate,
|
||||
frameToken: tc.frameToken,
|
||||
diagnostics: getDiagnosticsSnapshot(videoGs),
|
||||
forceLastSavedSpeed: tc.settings.forceLastSavedSpeed === true,
|
||||
forceLastSavedSpeedControlledBySiteRule: Boolean(
|
||||
tc.activeSiteRule &&
|
||||
@@ -2387,10 +2513,7 @@ function loadInitialRuntimeSettings(attempt) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!siteRuleUtils.isSpeederActiveForSite(
|
||||
tc.settings.enabled,
|
||||
tc.activeSiteRule
|
||||
)
|
||||
!isSpeederActiveForCurrentPage()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -2785,8 +2908,6 @@ function getControllerMount(video, boundary) {
|
||||
return isShadowRootNode(directRoot) ? directRoot : null;
|
||||
}
|
||||
|
||||
var mountBoundary = null;
|
||||
|
||||
var videoRect = video.getBoundingClientRect();
|
||||
var mount = video.parentElement;
|
||||
var candidate = mount;
|
||||
@@ -2804,15 +2925,7 @@ function getControllerMount(video, boundary) {
|
||||
// Climb through tightly-sized wrappers so our host shares their stacking
|
||||
// context, but stop before broad page-layout containers.
|
||||
while (candidate && candidate.parentElement && depth < 5) {
|
||||
if (mountBoundary && candidate === mountBoundary) break;
|
||||
var next = candidate.parentElement;
|
||||
if (
|
||||
mountBoundary &&
|
||||
next !== mountBoundary &&
|
||||
!mountBoundary.contains(next)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
var nextRect = next.getBoundingClientRect();
|
||||
var widthLimit = Math.max(videoRect.width * 1.35, videoRect.width + 80);
|
||||
var heightLimit = Math.max(videoRect.height * 1.35, videoRect.height + 80);
|
||||
@@ -2836,10 +2949,6 @@ function getControllerMount(video, boundary) {
|
||||
candidate = next;
|
||||
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
|
||||
// controller's high local z-index escape above sticky page headers.
|
||||
if (createsControllerStackingContext(next)) break;
|
||||
@@ -2867,6 +2976,14 @@ function positionControllerHost(wrapper, video, mount) {
|
||||
return;
|
||||
}
|
||||
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 (videoRect.width <= 0 || videoRect.height <= 0) {
|
||||
wrapper.classList.add("vsc-geometry-hidden");
|
||||
@@ -3035,6 +3152,12 @@ function setupControllerHostTracking(videoController, wrapper, mount) {
|
||||
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 });
|
||||
doc.addEventListener("fullscreenchange", schedule, { passive: true });
|
||||
geometryMount.addEventListener("scroll", schedule, { passive: true });
|
||||
@@ -3045,6 +3168,9 @@ function setupControllerHostTracking(videoController, wrapper, mount) {
|
||||
if (resizeObserver) resizeObserver.disconnect();
|
||||
if (frameId !== null) win.cancelAnimationFrame(frameId);
|
||||
if (geometryRetryTimer !== null) win.clearTimeout(geometryRetryTimer);
|
||||
mediaGeometryEvents.forEach(function(eventName) {
|
||||
videoController.video.removeEventListener(eventName, schedule);
|
||||
});
|
||||
win.removeEventListener("resize", schedule);
|
||||
doc.removeEventListener("fullscreenchange", schedule);
|
||||
geometryMount.removeEventListener("scroll", schedule);
|
||||
@@ -3168,14 +3294,14 @@ function disableDirectFullscreenPopover(videoController) {
|
||||
wrapper.removeAttribute("popover");
|
||||
}
|
||||
|
||||
function enableFullscreenPopover(videoController, preferredMount) {
|
||||
function enableDirectFullscreenPopover(videoController) {
|
||||
if (!videoController || !videoController.video || !videoController.div) {
|
||||
return false;
|
||||
}
|
||||
var wrapper = videoController.div;
|
||||
if (typeof wrapper.showPopover !== "function") return false;
|
||||
|
||||
var normalMount = preferredMount || videoController.normalControllerMount;
|
||||
var normalMount = videoController.normalControllerMount;
|
||||
var normalMountIsConnected = Boolean(
|
||||
normalMount &&
|
||||
(normalMount.isConnected ||
|
||||
@@ -3220,8 +3346,15 @@ function syncControllerFullscreenMount(videoController) {
|
||||
(fullscreenElement === video ||
|
||||
isComposedDescendant(video, fullscreenElement))
|
||||
);
|
||||
var normalGeometryMount = getControllerGeometryMount(targetMount);
|
||||
var normalMountIsAlreadyFullscreenVisible = Boolean(
|
||||
fullscreenElement &&
|
||||
fullscreenElement !== video &&
|
||||
normalGeometryMount &&
|
||||
isComposedDescendant(normalGeometryMount, fullscreenElement)
|
||||
);
|
||||
|
||||
if (ownsFullscreen) {
|
||||
if (ownsFullscreen && !normalMountIsAlreadyFullscreenVisible) {
|
||||
targetMount = getControllerMount(video, fullscreenElement);
|
||||
} else if (!fullscreenElement && (!targetMount || !targetMount.isConnected)) {
|
||||
targetMount = getControllerMount(video);
|
||||
@@ -3230,13 +3363,10 @@ function syncControllerFullscreenMount(videoController) {
|
||||
|
||||
if (!targetMount) return false;
|
||||
|
||||
if (ownsFullscreen) {
|
||||
// Fullscreen elements and popovers both participate in the browser's top
|
||||
// layer. Showing Speeder's host after the player enters fullscreen keeps it
|
||||
// above provider-owned surfaces even when the provider clips descendants or
|
||||
// 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;
|
||||
if (fullscreenElement === video) {
|
||||
// A replaced <video> cannot paint author children, so direct-media
|
||||
// fullscreen is the only case that needs a separate top-layer popover.
|
||||
if (enableDirectFullscreenPopover(videoController)) return true;
|
||||
} else {
|
||||
disableDirectFullscreenPopover(videoController);
|
||||
}
|
||||
@@ -3378,9 +3508,6 @@ function defineVideoController() {
|
||||
setSpeed(event.target, expectedSpeed, false, false);
|
||||
}
|
||||
|
||||
if (isUserSeek) {
|
||||
isUserSeek = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3774,11 +3901,6 @@ function defineVideoController() {
|
||||
timer = setTimeout(() => {
|
||||
timer = null;
|
||||
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");
|
||||
log("Generic hide: controller hidden due to inactivity", 5);
|
||||
}, tc.settings.hideWithControlsTimer * 1000);
|
||||
@@ -3801,8 +3923,8 @@ function defineVideoController() {
|
||||
// Initial show/timer
|
||||
resetTimer();
|
||||
|
||||
// The wrapper covers the player area on most sites due to inject.css styles,
|
||||
// but we listen on both the video and the wrapper for maximum coverage.
|
||||
// Players dispatch activity at different layers, so observe the media,
|
||||
// aligned controller host, and player mount.
|
||||
const activityEvents = ["mousemove", "mousedown", "keydown", "touchstart"];
|
||||
const parentEl =
|
||||
getControllerGeometryMount(this.controllerHostMount) ||
|
||||
@@ -3844,6 +3966,9 @@ function defineVideoController() {
|
||||
const speed = this.video.playbackRate.toFixed(2);
|
||||
var wrapper = doc.createElement("div");
|
||||
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))
|
||||
wrapper.classList.add("vsc-nosource");
|
||||
if (tc.settings.startHidden) wrapper.classList.add("vsc-hidden");
|
||||
@@ -4163,7 +4288,7 @@ function refreshAllControllerGeometry() {
|
||||
/** Re-match site rules for current URL and refresh controller position/opacity on every video. */
|
||||
function reapplySiteRulesAndControllerGeometry() {
|
||||
applySiteRuleOverrides();
|
||||
if (!siteRuleUtils.isSpeederActiveForSite(tc.settings.enabled, tc.activeSiteRule)) {
|
||||
if (!isSpeederActiveForCurrentPage()) {
|
||||
tc.mediaElements.slice().forEach(function(video) {
|
||||
removeController(video);
|
||||
});
|
||||
@@ -4421,10 +4546,7 @@ function attachKeydownListeners(doc) {
|
||||
if (isEditableShortcutTarget(event)) return;
|
||||
|
||||
if (
|
||||
!siteRuleUtils.isSpeederActiveForSite(
|
||||
tc.settings.enabled,
|
||||
tc.activeSiteRule
|
||||
)
|
||||
!isSpeederActiveForCurrentPage()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -4649,10 +4771,7 @@ function initializeNow(doc, forceReinit = false) {
|
||||
attachNavigationListeners();
|
||||
if (typeof tc.videoController === "undefined") defineVideoController();
|
||||
applySiteRuleOverrides();
|
||||
var isActive = siteRuleUtils.isSpeederActiveForSite(
|
||||
tc.settings.enabled,
|
||||
tc.activeSiteRule
|
||||
);
|
||||
var isActive = isSpeederActiveForCurrentPage();
|
||||
|
||||
// Keep observing while inactive so dynamically-created media/shadow roots
|
||||
// are available to the next forced SPA rescan, but remove stale controls.
|
||||
@@ -4804,16 +4923,7 @@ function getClosestMediaToPointer(candidates, pointerPosition) {
|
||||
|
||||
candidates.forEach(function(video) {
|
||||
if (!video || !video.vsc || !video.isConnected) return;
|
||||
var target = getControllerElement(video.vsc) || video;
|
||||
var rect = null;
|
||||
try {
|
||||
rect = target.getBoundingClientRect();
|
||||
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
||||
rect = video.getBoundingClientRect();
|
||||
}
|
||||
} catch (_error) {
|
||||
return;
|
||||
}
|
||||
var rect = getCachedVideoRect(video);
|
||||
if (!rect || rect.width <= 0 || rect.height <= 0) return;
|
||||
var distance = distanceSquaredToRect(
|
||||
pointerPosition.x,
|
||||
@@ -4942,12 +5052,10 @@ function runAction(action, value, e) {
|
||||
);
|
||||
switch (action) {
|
||||
case "rewind":
|
||||
isUserSeek = true;
|
||||
extendSpeedRestoreWindow(v);
|
||||
v.currentTime -= numValue;
|
||||
break;
|
||||
case "advance":
|
||||
isUserSeek = true;
|
||||
extendSpeedRestoreWindow(v);
|
||||
v.currentTime += numValue;
|
||||
break;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
:host {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
pointer-events: none !important;
|
||||
z-index: 2147483647 !important;
|
||||
white-space: normal;
|
||||
@@ -15,7 +11,6 @@
|
||||
base absolute host rule. */
|
||||
:host(.vsc-fullscreen-popover) {
|
||||
position: fixed !important;
|
||||
inset: auto !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Speeder",
|
||||
"short_name": "Speeder",
|
||||
"version": "6.0.6.0",
|
||||
"version": "6.0.8.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\")",
|
||||
"homepage_url": "https://github.com/SoPat712/speeder",
|
||||
@@ -71,7 +71,6 @@
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
"content/inject.css",
|
||||
"content/shadow-bridge.js",
|
||||
"content/shadow.css"
|
||||
]
|
||||
|
||||
@@ -1029,6 +1029,7 @@
|
||||
<button id="restore">Restore Defaults</button>
|
||||
<button id="exportSettings">Export Settings</button>
|
||||
<button id="importSettings">Import Settings</button>
|
||||
<button id="copyDiagnostics">Copy Diagnostics</button>
|
||||
</div>
|
||||
|
||||
<div id="status" role="status" aria-live="polite"></div>
|
||||
|
||||
@@ -2104,6 +2104,41 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
.addEventListener("change", updatePopupEditorDisabledState);
|
||||
|
||||
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");
|
||||
if (addSelector) {
|
||||
|
||||
@@ -86,8 +86,13 @@ button:active {
|
||||
|
||||
button:focus-visible {
|
||||
outline: 2px solid var(--focus-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
#refresh {
|
||||
background: var(--accent);
|
||||
@@ -113,6 +118,12 @@ button:focus-visible {
|
||||
border-color: #9fbd98;
|
||||
color: #285d21;
|
||||
}
|
||||
|
||||
#pauseTab[aria-pressed="true"] {
|
||||
background: #fff3d6;
|
||||
border-color: #d8b66b;
|
||||
color: #704f0d;
|
||||
}
|
||||
|
||||
.popup-divider {
|
||||
height: 1px;
|
||||
|
||||
@@ -24,6 +24,12 @@
|
||||
type="button"
|
||||
aria-pressed="false"
|
||||
>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 id="popupControlBar" class="popup-control-bar">
|
||||
<span id="popupSpeed" class="popup-speed">1.00</span>
|
||||
@@ -41,6 +47,8 @@
|
||||
<div class="popup-links">
|
||||
<button id="config">Settings</button>
|
||||
<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="about" class="secondary">About</button>
|
||||
</div>
|
||||
|
||||
+190
-5
@@ -1,4 +1,6 @@
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
if (window.vscPopupInitialized) return;
|
||||
window.vscPopupInitialized = true;
|
||||
var speederShared =
|
||||
typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
|
||||
var siteRuleUtils = speederShared.siteRules || {};
|
||||
@@ -29,6 +31,61 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
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) {
|
||||
var mutation = vscBuildManagedStorageMutation(rawStorage, settings);
|
||||
@@ -125,16 +182,23 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
|
||||
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) {
|
||||
var activeTab = tabs && tabs[0] ? tabs[0] : null;
|
||||
if (!activeTab || !activeTab.id) {
|
||||
if (callback) callback({ tab: null, url: "" });
|
||||
finish({ tab: null, url: "" });
|
||||
return;
|
||||
}
|
||||
|
||||
var tabUrl = typeof activeTab.url === "string" ? activeTab.url : "";
|
||||
if (tabUrl.length > 0) {
|
||||
if (callback) callback({ tab: activeTab, url: tabUrl });
|
||||
finish({ tab: activeTab, url: tabUrl });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -143,13 +207,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
{ action: "get_page_context" },
|
||||
function (response) {
|
||||
if (chrome.runtime.lastError) {
|
||||
if (callback) callback({ tab: activeTab, url: "" });
|
||||
finish({ tab: activeTab, url: "" });
|
||||
return;
|
||||
}
|
||||
|
||||
var pageUrl =
|
||||
response && typeof response.url === "string" ? response.url : "";
|
||||
if (callback) callback({ tab: activeTab, url: pageUrl });
|
||||
finish({ tab: activeTab, url: pageUrl });
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -288,6 +352,104 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
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 () {
|
||||
this.classList.add("hide");
|
||||
document.querySelector("#donateOptions").classList.remove("hide");
|
||||
@@ -400,6 +562,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
getActiveTabContext(function (context) {
|
||||
if (currentRenderToken !== renderToken) return;
|
||||
|
||||
var tabPaused = context && context.tabPaused === true;
|
||||
updateTabPauseButton(tabPaused);
|
||||
|
||||
var url = context && context.url ? context.url : "";
|
||||
var siteRule = matchSiteRule(url, storage.siteRules);
|
||||
var siteDisabled = isSiteRuleDisabled(siteRule);
|
||||
@@ -419,9 +584,22 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
forceLastSavedSpeedControlledBySiteRule
|
||||
? siteRule.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) {
|
||||
showBar = siteRule.showPopupControlBar;
|
||||
diagnosticContext.showBar = showBar;
|
||||
}
|
||||
|
||||
toggleEnabledUI(storage.enabled !== false);
|
||||
@@ -430,7 +608,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
resolvePopupButtons(storage, siteRule),
|
||||
customIconsMap
|
||||
);
|
||||
setControlBarVisible(siteAvailable && showBar);
|
||||
setControlBarVisible(!tabPaused && siteAvailable && showBar);
|
||||
|
||||
if (tabPaused) {
|
||||
setForceButtonLoading(true);
|
||||
setStatusMessage("Speeder is paused for this tab.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (siteDisabled) {
|
||||
setForceButtonLoading(false);
|
||||
@@ -443,6 +627,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
if (siteAvailable) {
|
||||
querySpeed(function(frameContext) {
|
||||
if (currentRenderToken !== renderToken) return;
|
||||
diagnosticContext.frame = frameContext || null;
|
||||
if (
|
||||
frameContext &&
|
||||
typeof frameContext.forceLastSavedSpeed === "boolean"
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
"popupControllerButtons",
|
||||
"popupMatchHoverControls",
|
||||
"rememberSpeed",
|
||||
"shortcutTargetMode",
|
||||
"showAmbientLoopControls",
|
||||
"showPopupControlBar",
|
||||
"siteRules",
|
||||
"siteRulesFormat",
|
||||
|
||||
@@ -71,6 +71,9 @@
|
||||
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") {
|
||||
normalized.forceLastSavedSpeed = result.forceLastSavedSpeed;
|
||||
}
|
||||
@@ -94,7 +97,61 @@
|
||||
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 {
|
||||
buildDiagnosticReport: buildDiagnosticReport,
|
||||
pickBestFrameSpeedResult: pickBestFrameSpeedResult,
|
||||
resolvePopupButtons: resolvePopupButtons,
|
||||
sanitizeButtonOrder: sanitizeButtonOrder
|
||||
|
||||
@@ -72,22 +72,26 @@ function vscClearElement(el) {
|
||||
function vscSanitizeSvgTree(svg) {
|
||||
if (!svg || String(svg.tagName).toLowerCase() !== "svg") return null;
|
||||
|
||||
svg.querySelectorAll("script, style, foreignObject").forEach(function (n) {
|
||||
n.remove();
|
||||
});
|
||||
svg
|
||||
.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) {
|
||||
for (var i = el.attributes.length - 1; i >= 0; i--) {
|
||||
var attr = el.attributes[i];
|
||||
var name = attr.name.toLowerCase();
|
||||
var val = attr.value;
|
||||
if (name.indexOf("on") === 0) {
|
||||
el.removeAttribute(attr.name);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
(name === "href" || name === "xlink:href") &&
|
||||
/^\s*javascript:/i.test(val)
|
||||
name.indexOf("on") === 0 ||
|
||||
name === "style" ||
|
||||
name === "href" ||
|
||||
name === "xlink:href" ||
|
||||
/url\s*\(/i.test(val)
|
||||
) {
|
||||
el.removeAttribute(attr.name);
|
||||
}
|
||||
|
||||
Generated
+161
-200
@@ -7,7 +7,7 @@
|
||||
"name": "speeder",
|
||||
"devDependencies": {
|
||||
"jsdom": "^26.1.0",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^3.2.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
@@ -140,9 +140,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
|
||||
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
|
||||
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -157,9 +157,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
|
||||
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -174,9 +174,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -191,9 +191,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -208,9 +208,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -225,9 +225,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -242,9 +242,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -259,9 +259,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -276,9 +276,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
|
||||
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -293,9 +293,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -310,9 +310,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
|
||||
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
|
||||
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -327,9 +327,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
|
||||
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
|
||||
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -344,9 +344,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
|
||||
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
|
||||
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -361,9 +361,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
|
||||
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
|
||||
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -378,9 +378,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
|
||||
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
|
||||
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -395,9 +395,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
|
||||
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
|
||||
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -412,9 +412,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -429,9 +429,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -446,9 +446,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -463,9 +463,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -480,9 +480,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -497,9 +497,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -514,9 +514,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -531,9 +531,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -548,9 +548,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
|
||||
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
|
||||
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -565,9 +565,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -680,9 +680,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -697,9 +694,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -714,9 +708,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -731,9 +722,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -748,9 +736,6 @@
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -765,9 +750,6 @@
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -782,9 +764,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -799,9 +778,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -816,9 +792,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -833,9 +806,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -850,9 +820,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -867,9 +834,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -884,9 +848,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1003,15 +964,15 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
|
||||
"integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
|
||||
"integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "3.2.4",
|
||||
"@vitest/utils": "3.2.4",
|
||||
"@vitest/spy": "3.2.7",
|
||||
"@vitest/utils": "3.2.7",
|
||||
"chai": "^5.2.0",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
@@ -1020,13 +981,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
|
||||
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",
|
||||
"integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "3.2.4",
|
||||
"@vitest/spy": "3.2.7",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.17"
|
||||
},
|
||||
@@ -1047,9 +1008,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
|
||||
"integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",
|
||||
"integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1060,13 +1021,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
|
||||
"integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",
|
||||
"integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "3.2.4",
|
||||
"@vitest/utils": "3.2.7",
|
||||
"pathe": "^2.0.3",
|
||||
"strip-literal": "^3.0.0"
|
||||
},
|
||||
@@ -1075,13 +1036,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
|
||||
"integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",
|
||||
"integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.2.4",
|
||||
"@vitest/pretty-format": "3.2.7",
|
||||
"magic-string": "^0.30.17",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
@@ -1090,9 +1051,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
|
||||
"integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",
|
||||
"integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1103,13 +1064,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
|
||||
"integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",
|
||||
"integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.2.4",
|
||||
"@vitest/pretty-format": "3.2.7",
|
||||
"loupe": "^3.1.4",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
@@ -1258,9 +1219,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
||||
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
|
||||
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -1271,32 +1232,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.7",
|
||||
"@esbuild/android-arm": "0.27.7",
|
||||
"@esbuild/android-arm64": "0.27.7",
|
||||
"@esbuild/android-x64": "0.27.7",
|
||||
"@esbuild/darwin-arm64": "0.27.7",
|
||||
"@esbuild/darwin-x64": "0.27.7",
|
||||
"@esbuild/freebsd-arm64": "0.27.7",
|
||||
"@esbuild/freebsd-x64": "0.27.7",
|
||||
"@esbuild/linux-arm": "0.27.7",
|
||||
"@esbuild/linux-arm64": "0.27.7",
|
||||
"@esbuild/linux-ia32": "0.27.7",
|
||||
"@esbuild/linux-loong64": "0.27.7",
|
||||
"@esbuild/linux-mips64el": "0.27.7",
|
||||
"@esbuild/linux-ppc64": "0.27.7",
|
||||
"@esbuild/linux-riscv64": "0.27.7",
|
||||
"@esbuild/linux-s390x": "0.27.7",
|
||||
"@esbuild/linux-x64": "0.27.7",
|
||||
"@esbuild/netbsd-arm64": "0.27.7",
|
||||
"@esbuild/netbsd-x64": "0.27.7",
|
||||
"@esbuild/openbsd-arm64": "0.27.7",
|
||||
"@esbuild/openbsd-x64": "0.27.7",
|
||||
"@esbuild/openharmony-arm64": "0.27.7",
|
||||
"@esbuild/sunos-x64": "0.27.7",
|
||||
"@esbuild/win32-arm64": "0.27.7",
|
||||
"@esbuild/win32-ia32": "0.27.7",
|
||||
"@esbuild/win32-x64": "0.27.7"
|
||||
"@esbuild/aix-ppc64": "0.28.2",
|
||||
"@esbuild/android-arm": "0.28.2",
|
||||
"@esbuild/android-arm64": "0.28.2",
|
||||
"@esbuild/android-x64": "0.28.2",
|
||||
"@esbuild/darwin-arm64": "0.28.2",
|
||||
"@esbuild/darwin-x64": "0.28.2",
|
||||
"@esbuild/freebsd-arm64": "0.28.2",
|
||||
"@esbuild/freebsd-x64": "0.28.2",
|
||||
"@esbuild/linux-arm": "0.28.2",
|
||||
"@esbuild/linux-arm64": "0.28.2",
|
||||
"@esbuild/linux-ia32": "0.28.2",
|
||||
"@esbuild/linux-loong64": "0.28.2",
|
||||
"@esbuild/linux-mips64el": "0.28.2",
|
||||
"@esbuild/linux-ppc64": "0.28.2",
|
||||
"@esbuild/linux-riscv64": "0.28.2",
|
||||
"@esbuild/linux-s390x": "0.28.2",
|
||||
"@esbuild/linux-x64": "0.28.2",
|
||||
"@esbuild/netbsd-arm64": "0.28.2",
|
||||
"@esbuild/netbsd-x64": "0.28.2",
|
||||
"@esbuild/openbsd-arm64": "0.28.2",
|
||||
"@esbuild/openbsd-x64": "0.28.2",
|
||||
"@esbuild/openharmony-arm64": "0.28.2",
|
||||
"@esbuild/sunos-x64": "0.28.2",
|
||||
"@esbuild/win32-arm64": "0.28.2",
|
||||
"@esbuild/win32-ia32": "0.28.2",
|
||||
"@esbuild/win32-x64": "0.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
@@ -1492,9 +1453,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -1568,9 +1529,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||
"version": "8.5.26",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
||||
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -1588,7 +1549,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"nanoid": "^3.3.17",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -1837,13 +1798,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"version": "7.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
|
||||
"integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
@@ -1935,20 +1896,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
|
||||
"integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
"@vitest/mocker": "3.2.4",
|
||||
"@vitest/pretty-format": "^3.2.4",
|
||||
"@vitest/runner": "3.2.4",
|
||||
"@vitest/snapshot": "3.2.4",
|
||||
"@vitest/spy": "3.2.4",
|
||||
"@vitest/utils": "3.2.4",
|
||||
"@vitest/expect": "3.2.7",
|
||||
"@vitest/mocker": "3.2.7",
|
||||
"@vitest/pretty-format": "^3.2.7",
|
||||
"@vitest/runner": "3.2.7",
|
||||
"@vitest/snapshot": "3.2.7",
|
||||
"@vitest/spy": "3.2.7",
|
||||
"@vitest/utils": "3.2.7",
|
||||
"chai": "^5.2.0",
|
||||
"debug": "^4.4.1",
|
||||
"expect-type": "^1.2.1",
|
||||
@@ -1978,8 +1939,8 @@
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"@vitest/browser": "3.2.4",
|
||||
"@vitest/ui": "3.2.4",
|
||||
"@vitest/browser": "3.2.7",
|
||||
"@vitest/ui": "3.2.7",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
@@ -2086,9 +2047,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
|
||||
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
+1
-1
@@ -7,6 +7,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"jsdom": "^26.1.0",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^3.2.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@ validate_semver() {
|
||||
echo "Error: empty version." >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ ! "$s" =~ ^[0-9]+(\.[0-9]+){0,3}(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
|
||||
echo "Error: invalid version (use something like 5.0.4)." >&2
|
||||
if [[ ! "$s" =~ ^[0-9]+(\.[0-9]+){0,3}$ ]]; then
|
||||
echo "Error: Firefox versions must contain only 1-4 numeric parts (for example, 6.0.8.1)." >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -59,10 +59,15 @@ fi
|
||||
git checkout 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
|
||||
SEMVER="$(normalize_semver "$SEMVER_IN")"
|
||||
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}"
|
||||
if [[ "$TAG" == *-beta* ]]; then
|
||||
@@ -70,6 +75,11 @@ if [[ "$TAG" == *-beta* ]]; then
|
||||
read -r -p "Continue anyway? [y/N] " w
|
||||
[[ "${w:-}" =~ ^[yY](es)?$ ]] || { echo "Aborted."; exit 1; }
|
||||
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 "This will:"
|
||||
@@ -87,11 +97,11 @@ git pull origin main
|
||||
git merge --squash beta
|
||||
bump_manifest "$SEMVER"
|
||||
git add -A
|
||||
git commit -m "Release $TAG"
|
||||
git commit -m "chore(release): prepare $TAG"
|
||||
|
||||
git push origin main
|
||||
|
||||
git tag -a "$TAG" -m "$TAG"
|
||||
git tag -s "$TAG" -m "$TAG"
|
||||
git push origin "$TAG"
|
||||
|
||||
git checkout dev
|
||||
|
||||
+16
-6
@@ -1,5 +1,5 @@
|
||||
#!/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.
|
||||
|
||||
set -euo pipefail
|
||||
@@ -44,8 +44,8 @@ validate_semver() {
|
||||
echo "Error: empty version." >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ ! "$s" =~ ^[0-9]+(\.[0-9]+){0,3}(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
|
||||
echo "Error: invalid version (use something like 5.0.4 or 5.0.4-beta.1)." >&2
|
||||
if [[ ! "$s" =~ ^[0-9]+(\.[0-9]+){0,3}$ ]]; then
|
||||
echo "Error: Firefox versions must contain only 1-4 numeric parts (for example, 6.0.8.1)." >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -58,10 +58,15 @@ fi
|
||||
git checkout 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
|
||||
SEMVER="$(normalize_semver "$SEMVER_IN")"
|
||||
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)."
|
||||
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
|
||||
exit 1
|
||||
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 "This will:"
|
||||
@@ -88,7 +98,7 @@ echo "🚀 Releasing beta $TAG"
|
||||
|
||||
bump_manifest "$SEMVER"
|
||||
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 checkout beta
|
||||
@@ -96,7 +106,7 @@ git pull origin beta
|
||||
git merge dev --no-ff -m "$TAG"
|
||||
git push origin beta
|
||||
|
||||
git tag -a "$TAG" -m "$TAG"
|
||||
git tag -s "$TAG" -m "$TAG"
|
||||
git push origin "$TAG"
|
||||
|
||||
git checkout dev
|
||||
|
||||
@@ -33,4 +33,34 @@ describe("background toolbar state", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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 __dirname = path.dirname(__filename);
|
||||
const repoRoot = path.resolve(__dirname, "..", "..");
|
||||
let activeDom = null;
|
||||
|
||||
function readRepoFile(relPath) {
|
||||
return fs.readFileSync(path.join(repoRoot, relPath), "utf8");
|
||||
@@ -18,11 +19,13 @@ function readRepoFile(relPath) {
|
||||
* top-level `const` redeclaration errors (avoids document.write).
|
||||
*/
|
||||
export function loadHtmlString(html, options = {}) {
|
||||
if (activeDom) activeDom.window.close();
|
||||
const dom = new JSDOM(html, {
|
||||
url: options.url || "https://example.org/",
|
||||
pretendToBeVisual: true,
|
||||
runScripts: "dangerously"
|
||||
});
|
||||
activeDom = dom;
|
||||
applyJSDOMWindow(dom.window);
|
||||
}
|
||||
|
||||
@@ -173,7 +176,13 @@ export function createChromeMock(options = {}) {
|
||||
getManifest: vi.fn(() => ({
|
||||
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: {
|
||||
sync: syncArea,
|
||||
|
||||
@@ -4,6 +4,7 @@ const { JSDOM } = require("jsdom");
|
||||
const vi = globalThis.vi;
|
||||
|
||||
const ROOT = path.resolve(__dirname, "..", "..");
|
||||
let activeDom = null;
|
||||
|
||||
function clone(value) {
|
||||
if (value === undefined) return undefined;
|
||||
@@ -47,11 +48,13 @@ function applyJSDOMWindow(win) {
|
||||
|
||||
function loadHtmlString(html, options) {
|
||||
const config = options || {};
|
||||
if (activeDom) activeDom.window.close();
|
||||
const dom = new JSDOM(html, {
|
||||
url: config.url || "https://example.org/",
|
||||
pretendToBeVisual: true,
|
||||
runScripts: "dangerously"
|
||||
});
|
||||
activeDom = dom;
|
||||
applyJSDOMWindow(dom.window);
|
||||
}
|
||||
|
||||
@@ -245,6 +248,7 @@ function createChromeMock(options) {
|
||||
const storageOnChanged = createChromeEvent();
|
||||
const tabsOnActivated = createChromeEvent();
|
||||
const tabsOnUpdated = createChromeEvent();
|
||||
const tabsOnRemoved = createChromeEvent();
|
||||
const runtimeOnMessage = createChromeEvent();
|
||||
|
||||
const chrome = {
|
||||
@@ -252,6 +256,12 @@ function createChromeMock(options) {
|
||||
lastError: null,
|
||||
getManifest: vi.fn(() => clone(config.manifest) || { version: "0.0.0-test" }),
|
||||
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
|
||||
},
|
||||
browserAction: {
|
||||
@@ -272,7 +282,8 @@ function createChromeMock(options) {
|
||||
}),
|
||||
create: vi.fn(),
|
||||
onActivated: tabsOnActivated,
|
||||
onUpdated: tabsOnUpdated
|
||||
onUpdated: tabsOnUpdated,
|
||||
onRemoved: tabsOnRemoved
|
||||
},
|
||||
storage: {
|
||||
onChanged: storageOnChanged,
|
||||
|
||||
@@ -2,7 +2,8 @@ const {
|
||||
createChromeMock,
|
||||
evaluateScript,
|
||||
flushAsyncWork,
|
||||
loadHtmlString
|
||||
loadHtmlString,
|
||||
readWorkspaceFile
|
||||
} = require("./helpers/extension-test-utils");
|
||||
|
||||
function bootInject(options) {
|
||||
@@ -280,6 +281,55 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
||||
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 () => {
|
||||
bootInject();
|
||||
await settleLifecycle();
|
||||
@@ -292,16 +342,27 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
||||
src: "https://example.org/second.mp4",
|
||||
mountRect: makeRect(500, 0, 320, 180)
|
||||
});
|
||||
setRect(window.getControllerElement(first.controller), makeRect(10, 10, 120, 30));
|
||||
setRect(window.getControllerElement(second.controller), makeRect(510, 10, 120, 30));
|
||||
const firstLayoutRead = vi.fn(() => first.mount.getBoundingClientRect());
|
||||
const secondLayoutRead = vi.fn(() => second.mount.getBoundingClientRect());
|
||||
first.video.getBoundingClientRect = firstLayoutRead;
|
||||
second.video.getBoundingClientRect = secondLayoutRead;
|
||||
|
||||
document.dispatchEvent(
|
||||
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(second.video.playbackRate).toBe(1.1);
|
||||
expect(firstLayoutRead).not.toHaveBeenCalled();
|
||||
expect(secondLayoutRead).not.toHaveBeenCalled();
|
||||
|
||||
window.tc.settings.shortcutTargetMode = "all";
|
||||
window.runAction("faster", 0.1);
|
||||
@@ -329,6 +390,31 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
||||
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 () => {
|
||||
bootInject({
|
||||
url: "https://www.youtube.com/",
|
||||
@@ -590,6 +676,7 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
||||
div: wrapper,
|
||||
normalControllerMount: normalMount
|
||||
};
|
||||
wrapper.showPopover = vi.fn();
|
||||
window.setupControllerHostTracking(controller, wrapper, normalMount);
|
||||
Object.defineProperty(document, "fullscreenElement", {
|
||||
configurable: true,
|
||||
@@ -598,6 +685,8 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
||||
|
||||
window.syncControllerFullscreenMount(controller);
|
||||
expect(fullscreenPlayer.contains(wrapper)).toBe(true);
|
||||
expect(wrapper.showPopover).not.toHaveBeenCalled();
|
||||
expect(wrapper.hasAttribute("popover")).toBe(false);
|
||||
|
||||
Object.defineProperty(document, "fullscreenElement", {
|
||||
configurable: true,
|
||||
@@ -610,53 +699,26 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
||||
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 fullscreenPlayer = document.createElement("div");
|
||||
const video = document.createElement("video");
|
||||
const wrapper = document.createElement("div");
|
||||
const rect = makeRect(0, 0, 1280, 720);
|
||||
|
||||
fullscreenPlayer.append(video, wrapper);
|
||||
document.body.appendChild(fullscreenPlayer);
|
||||
[fullscreenPlayer, video].forEach((element) => {
|
||||
setRect(element, rect);
|
||||
setBoxMetrics(element, rect.width, rect.height);
|
||||
});
|
||||
wrapper.showPopover = vi.fn();
|
||||
wrapper.hidePopover = vi.fn();
|
||||
|
||||
const controller = {
|
||||
video,
|
||||
div: wrapper,
|
||||
normalControllerMount: fullscreenPlayer
|
||||
};
|
||||
window.setupControllerHostTracking(controller, wrapper, fullscreenPlayer);
|
||||
const { mount, controller, wrapper } = createControlledVideo();
|
||||
setRect(document.documentElement, makeRect(0, 0, 0, 0));
|
||||
Object.defineProperty(document, "fullscreenElement", {
|
||||
configurable: true,
|
||||
value: fullscreenPlayer
|
||||
value: document.documentElement
|
||||
});
|
||||
|
||||
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", {
|
||||
configurable: true,
|
||||
value: null
|
||||
});
|
||||
window.syncControllerFullscreenMount(controller);
|
||||
expect(wrapper.hidePopover).toHaveBeenCalledOnce();
|
||||
|
||||
wrapper.remove();
|
||||
controller.controllerHostCleanup();
|
||||
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 controller owned by the fullscreen player", async () => {
|
||||
it("only promotes the directly-fullscreen video's controller", async () => {
|
||||
bootInject();
|
||||
await settleLifecycle();
|
||||
|
||||
@@ -680,7 +742,7 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
||||
|
||||
Object.defineProperty(document, "fullscreenElement", {
|
||||
configurable: true,
|
||||
value: fullscreenPlayer
|
||||
value: fullscreenVideo
|
||||
});
|
||||
window.syncControllerFullscreenMount(fullscreenVideo.vsc);
|
||||
window.syncControllerFullscreenMount(otherVideo.vsc);
|
||||
@@ -985,9 +1047,13 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
||||
videoRect: makeRect(40, 40, 640, 360),
|
||||
src: "https://example.org/visible.mp4"
|
||||
}).video;
|
||||
const offscreenLayoutRead = vi.spyOn(offscreen, "getBoundingClientRect");
|
||||
const visibleLayoutRead = vi.spyOn(visible, "getBoundingClientRect");
|
||||
|
||||
expect(window.getPrimaryVideoElement()).toBe(visible);
|
||||
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 () => {
|
||||
|
||||
@@ -29,7 +29,10 @@ describe("lucide-client.js", () => {
|
||||
<svg width="10" height="10" onclick="evil()">
|
||||
<script>alert(1)</script>
|
||||
<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>
|
||||
`);
|
||||
|
||||
@@ -37,6 +40,12 @@ describe("lucide-client.js", () => {
|
||||
expect(sanitized).not.toContain("onclick");
|
||||
expect(sanitized).not.toContain("<script");
|
||||
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%"');
|
||||
});
|
||||
|
||||
|
||||
@@ -84,6 +84,42 @@ describe("options.js", () => {
|
||||
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 () => {
|
||||
const chrome = bootOptions({ syncData: { rememberSpeed: false } });
|
||||
await flushAsyncWork(3);
|
||||
|
||||
+103
-1
@@ -18,6 +18,7 @@ function bootPopup(options) {
|
||||
manifest: { version: "9.9.9-test" },
|
||||
syncData: config.syncData,
|
||||
localData: config.localData,
|
||||
runtimeSendMessageImpl: config.runtimeSendMessageImpl,
|
||||
tabsQueryResult: [
|
||||
config.activeTab || { id: 99, active: true, url: "https://example.com/" }
|
||||
]
|
||||
@@ -107,7 +108,7 @@ describe("popup.js", () => {
|
||||
executeScriptImpl: (tabId, details, callback) => {
|
||||
speedQueryCount += 1;
|
||||
callback(
|
||||
speedQueryCount <= 2
|
||||
speedQueryCount === 1
|
||||
? [
|
||||
{ speed: 1.25, preferred: false },
|
||||
{ speed: 1.5, frameToken: "playing-frame", preferred: true }
|
||||
@@ -167,6 +168,107 @@ describe("popup.js", () => {
|
||||
);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const chrome = bootPopup({
|
||||
syncData: {
|
||||
|
||||
@@ -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", () => {
|
||||
const settings = window.vscExpandStoredSettings({});
|
||||
expect(settings.siteRules.map((rule) => rule.title)).toEqual([
|
||||
|
||||
@@ -241,6 +241,24 @@ describe("shared helpers", () => {
|
||||
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(
|
||||
false
|
||||
);
|
||||
|
||||
@@ -4,8 +4,10 @@ module.exports = defineConfig({
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
clearMocks: true,
|
||||
fileParallelism: false,
|
||||
globals: true,
|
||||
restoreMocks: true,
|
||||
testTimeout: 15000,
|
||||
include: ["tests/**/*.test.js", "tests/**/*.spec.js"],
|
||||
setupFiles: ["./tests/setup.js"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user