mirror of
https://github.com/SoPat712/Speeder.git
synced 2026-08-20 12:06:19 -04:00
Compare commits
7
Commits
aa4f1d15e7
...
f8cfbf05c8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8cfbf05c8
|
||
|
|
b4234e0387
|
||
|
|
d55006dd0a
|
||
|
|
745af2f1b9
|
||
|
|
7e9ce436d3
|
||
|
|
98546adf41
|
||
|
|
daeff63d2a
|
@@ -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) {
|
chrome.storage.sync.get(["enabled"], function(storage) {
|
||||||
if (!chrome.runtime.lastError) setToolbarIcon(storage.enabled !== false);
|
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") {
|
if (request.action === "openOptions") {
|
||||||
chrome.tabs.create({ url: chrome.runtime.getURL("options/options.html") });
|
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"
|
typeof tc === "object" && typeof tc.frameToken === "string"
|
||||||
? tc.frameToken
|
? tc.frameToken
|
||||||
: null,
|
: null,
|
||||||
|
diagnostics:
|
||||||
|
typeof getDiagnosticsSnapshot === "function"
|
||||||
|
? getDiagnosticsSnapshot(v)
|
||||||
|
: null,
|
||||||
preferred: !v.paused,
|
preferred: !v.paused,
|
||||||
forceLastSavedSpeed: Boolean(
|
forceLastSavedSpeed: Boolean(
|
||||||
typeof tc === "object" && tc.settings && tc.settings.forceLastSavedSpeed
|
typeof tc === "object" && tc.settings && tc.settings.forceLastSavedSpeed
|
||||||
|
|||||||
+123
-23
@@ -90,6 +90,81 @@ function getPrimaryVideoElement(mediaElements) {
|
|||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDiagnosticsSnapshot(media) {
|
||||||
|
var primary = media || getPrimaryVideoElement();
|
||||||
|
if (!primary) return null;
|
||||||
|
|
||||||
|
var controller = primary.vsc || null;
|
||||||
|
var wrapper = controller && controller.div;
|
||||||
|
var fullscreenElement = getFullscreenElement(primary.ownerDocument);
|
||||||
|
var rect = null;
|
||||||
|
try {
|
||||||
|
rect = primary.getBoundingClientRect();
|
||||||
|
} catch (_error) {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
mediaCount: tc.mediaElements.filter(function(item) {
|
||||||
|
return item && item.isConnected;
|
||||||
|
}).length,
|
||||||
|
mediaType: String(primary.nodeName || "media").toLowerCase(),
|
||||||
|
playbackRate: Number(primary.playbackRate),
|
||||||
|
paused: primary.paused === true,
|
||||||
|
ended: primary.ended === true,
|
||||||
|
readyState: Number(primary.readyState) || 0,
|
||||||
|
muted: primary.muted === true,
|
||||||
|
volume: Number(primary.volume),
|
||||||
|
dimensions: rect
|
||||||
|
? [Math.round(Number(rect.width) || 0), Math.round(Number(rect.height) || 0)]
|
||||||
|
: [0, 0],
|
||||||
|
fullscreen: {
|
||||||
|
active: Boolean(fullscreenElement),
|
||||||
|
element: fullscreenElement
|
||||||
|
? String(fullscreenElement.nodeName || "element").toLowerCase()
|
||||||
|
: null,
|
||||||
|
ownsMedia: Boolean(
|
||||||
|
fullscreenElement &&
|
||||||
|
(fullscreenElement === primary ||
|
||||||
|
isComposedDescendant(primary, fullscreenElement))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
controller: {
|
||||||
|
present: Boolean(controller && wrapper),
|
||||||
|
connected: Boolean(wrapper && wrapper.isConnected),
|
||||||
|
hidden: Boolean(wrapper && wrapper.classList.contains("vsc-hidden")),
|
||||||
|
geometryHidden: Boolean(
|
||||||
|
wrapper && wrapper.classList.contains("vsc-geometry-hidden")
|
||||||
|
),
|
||||||
|
fullscreenPopover: Boolean(
|
||||||
|
wrapper && wrapper.classList.contains("vsc-fullscreen-popover")
|
||||||
|
),
|
||||||
|
location: controller ? controller.controllerLocation : null
|
||||||
|
},
|
||||||
|
effectiveSettings: {
|
||||||
|
enabled: tc.settings.enabled !== false,
|
||||||
|
tabPaused: tc.tabPaused === true,
|
||||||
|
startHidden: tc.settings.startHidden === true,
|
||||||
|
hideWithControls: tc.settings.hideWithControls === true,
|
||||||
|
rememberSpeed: tc.settings.rememberSpeed === true,
|
||||||
|
forceLastSavedSpeed: tc.settings.forceLastSavedSpeed === true,
|
||||||
|
shortcutTargetMode: tc.settings.shortcutTargetMode
|
||||||
|
},
|
||||||
|
siteRule: {
|
||||||
|
matched: Boolean(tc.activeSiteRule),
|
||||||
|
disabled: Boolean(
|
||||||
|
tc.activeSiteRule &&
|
||||||
|
siteRuleUtils.isSiteRuleDisabled(tc.activeSiteRule)
|
||||||
|
),
|
||||||
|
overrideKeys: tc.activeSiteRule
|
||||||
|
? Object.keys(tc.activeSiteRule)
|
||||||
|
.filter(function(key) {
|
||||||
|
return key !== "pattern" && key !== "title";
|
||||||
|
})
|
||||||
|
.sort()
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var tc = {
|
var tc = {
|
||||||
settings: {
|
settings: {
|
||||||
lastSpeed: getSharedDefault("lastSpeed", 1.0),
|
lastSpeed: getSharedDefault("lastSpeed", 1.0),
|
||||||
@@ -151,6 +226,7 @@ var tc = {
|
|||||||
speedAccessTimes: {},
|
speedAccessTimes: {},
|
||||||
persistedLastSpeed: 1.0,
|
persistedLastSpeed: 1.0,
|
||||||
activeSiteRule: null,
|
activeSiteRule: null,
|
||||||
|
tabPaused: false,
|
||||||
siteRuleBase: null,
|
siteRuleBase: null,
|
||||||
runtimeSettingsHydrated: false,
|
runtimeSettingsHydrated: false,
|
||||||
pendingMediaCandidates: [],
|
pendingMediaCandidates: [],
|
||||||
@@ -163,6 +239,29 @@ var tc = {
|
|||||||
: String(Date.now()) + "-" + Math.random().toString(36).slice(2)
|
: 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 MIN_SPEED = Number(keyBindingUtils.MIN_SPEED) || 0.1;
|
||||||
var MAX_SPEED = Number(keyBindingUtils.MAX_SPEED) || 16;
|
var MAX_SPEED = Number(keyBindingUtils.MAX_SPEED) || 16;
|
||||||
var YT_NATIVE_MIN = 0.25;
|
var YT_NATIVE_MIN = 0.25;
|
||||||
@@ -1516,7 +1615,7 @@ function ensureController(node, parent) {
|
|||||||
// href selects site rules; re-run on every new/usable media so all runtime
|
// href selects site rules; re-run on every new/usable media so all runtime
|
||||||
// paths agree on activation and effective settings.
|
// paths agree on activation and effective settings.
|
||||||
applySiteRuleOverrides();
|
applySiteRuleOverrides();
|
||||||
if (!siteRuleUtils.isSpeederActiveForSite(tc.settings.enabled, tc.activeSiteRule)) {
|
if (!isSpeederActiveForCurrentPage()) {
|
||||||
if (node.vsc) removeController(node);
|
if (node.vsc) removeController(node);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -2324,12 +2423,24 @@ function loadInitialRuntimeSettings(attempt) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hydrateRuntimeSettings(rawStorage || {});
|
hydrateRuntimeSettings(rawStorage || {});
|
||||||
|
if (chrome.runtime && typeof chrome.runtime.sendMessage === "function") {
|
||||||
|
chrome.runtime.sendMessage({ action: "get_tab_pause_state" }, function(response) {
|
||||||
|
if (!chrome.runtime.lastError && response) {
|
||||||
|
applyTabPausedState(response.paused === true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
// patchAttachShadow() is now called at top-level before this callback
|
// patchAttachShadow() is now called at top-level before this callback
|
||||||
// Add a listener for messages from the popup.
|
// Add a listener for messages from the popup.
|
||||||
// We use a global flag to ensure the listener is only attached once.
|
// We use a global flag to ensure the listener is only attached once.
|
||||||
if (!window.vscMessageListener) {
|
if (!window.vscMessageListener) {
|
||||||
chrome.runtime.onMessage.addListener(
|
chrome.runtime.onMessage.addListener(
|
||||||
function(request, sender, sendResponse) {
|
function(request, sender, sendResponse) {
|
||||||
|
if (request.action === "set_tab_paused") {
|
||||||
|
applyTabPausedState(request.paused === true);
|
||||||
|
sendResponse({ paused: tc.tabPaused });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (request.action === "rescan_page") {
|
if (request.action === "rescan_page") {
|
||||||
log("Re-scan command received from popup.", 4);
|
log("Re-scan command received from popup.", 4);
|
||||||
initializeWhenReady(document, true);
|
initializeWhenReady(document, true);
|
||||||
@@ -2344,6 +2455,7 @@ function loadInitialRuntimeSettings(attempt) {
|
|||||||
sendResponse({
|
sendResponse({
|
||||||
speed: videoGs.playbackRate,
|
speed: videoGs.playbackRate,
|
||||||
frameToken: tc.frameToken,
|
frameToken: tc.frameToken,
|
||||||
|
diagnostics: getDiagnosticsSnapshot(videoGs),
|
||||||
forceLastSavedSpeed: tc.settings.forceLastSavedSpeed === true,
|
forceLastSavedSpeed: tc.settings.forceLastSavedSpeed === true,
|
||||||
forceLastSavedSpeedControlledBySiteRule: Boolean(
|
forceLastSavedSpeedControlledBySiteRule: Boolean(
|
||||||
tc.activeSiteRule &&
|
tc.activeSiteRule &&
|
||||||
@@ -2387,10 +2499,7 @@ function loadInitialRuntimeSettings(attempt) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!siteRuleUtils.isSpeederActiveForSite(
|
!isSpeederActiveForCurrentPage()
|
||||||
tc.settings.enabled,
|
|
||||||
tc.activeSiteRule
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -3168,14 +3277,14 @@ function disableDirectFullscreenPopover(videoController) {
|
|||||||
wrapper.removeAttribute("popover");
|
wrapper.removeAttribute("popover");
|
||||||
}
|
}
|
||||||
|
|
||||||
function enableFullscreenPopover(videoController, preferredMount) {
|
function enableDirectFullscreenPopover(videoController) {
|
||||||
if (!videoController || !videoController.video || !videoController.div) {
|
if (!videoController || !videoController.video || !videoController.div) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var wrapper = videoController.div;
|
var wrapper = videoController.div;
|
||||||
if (typeof wrapper.showPopover !== "function") return false;
|
if (typeof wrapper.showPopover !== "function") return false;
|
||||||
|
|
||||||
var normalMount = preferredMount || videoController.normalControllerMount;
|
var normalMount = videoController.normalControllerMount;
|
||||||
var normalMountIsConnected = Boolean(
|
var normalMountIsConnected = Boolean(
|
||||||
normalMount &&
|
normalMount &&
|
||||||
(normalMount.isConnected ||
|
(normalMount.isConnected ||
|
||||||
@@ -3230,13 +3339,10 @@ function syncControllerFullscreenMount(videoController) {
|
|||||||
|
|
||||||
if (!targetMount) return false;
|
if (!targetMount) return false;
|
||||||
|
|
||||||
if (ownsFullscreen) {
|
if (fullscreenElement === video) {
|
||||||
// Fullscreen elements and popovers both participate in the browser's top
|
// A replaced <video> cannot paint author children, so direct-media
|
||||||
// layer. Showing Speeder's host after the player enters fullscreen keeps it
|
// fullscreen is the only case that needs a separate top-layer popover.
|
||||||
// above provider-owned surfaces even when the provider clips descendants or
|
if (enableDirectFullscreenPopover(videoController)) return true;
|
||||||
// creates a new fullscreen stacking context (notably Firefox + YouTube).
|
|
||||||
// Browsers without the Popover API retain the player-local remount fallback.
|
|
||||||
if (enableFullscreenPopover(videoController, targetMount)) return true;
|
|
||||||
} else {
|
} else {
|
||||||
disableDirectFullscreenPopover(videoController);
|
disableDirectFullscreenPopover(videoController);
|
||||||
}
|
}
|
||||||
@@ -4163,7 +4269,7 @@ function refreshAllControllerGeometry() {
|
|||||||
/** Re-match site rules for current URL and refresh controller position/opacity on every video. */
|
/** Re-match site rules for current URL and refresh controller position/opacity on every video. */
|
||||||
function reapplySiteRulesAndControllerGeometry() {
|
function reapplySiteRulesAndControllerGeometry() {
|
||||||
applySiteRuleOverrides();
|
applySiteRuleOverrides();
|
||||||
if (!siteRuleUtils.isSpeederActiveForSite(tc.settings.enabled, tc.activeSiteRule)) {
|
if (!isSpeederActiveForCurrentPage()) {
|
||||||
tc.mediaElements.slice().forEach(function(video) {
|
tc.mediaElements.slice().forEach(function(video) {
|
||||||
removeController(video);
|
removeController(video);
|
||||||
});
|
});
|
||||||
@@ -4421,10 +4527,7 @@ function attachKeydownListeners(doc) {
|
|||||||
if (isEditableShortcutTarget(event)) return;
|
if (isEditableShortcutTarget(event)) return;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!siteRuleUtils.isSpeederActiveForSite(
|
!isSpeederActiveForCurrentPage()
|
||||||
tc.settings.enabled,
|
|
||||||
tc.activeSiteRule
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4649,10 +4752,7 @@ function initializeNow(doc, forceReinit = false) {
|
|||||||
attachNavigationListeners();
|
attachNavigationListeners();
|
||||||
if (typeof tc.videoController === "undefined") defineVideoController();
|
if (typeof tc.videoController === "undefined") defineVideoController();
|
||||||
applySiteRuleOverrides();
|
applySiteRuleOverrides();
|
||||||
var isActive = siteRuleUtils.isSpeederActiveForSite(
|
var isActive = isSpeederActiveForCurrentPage();
|
||||||
tc.settings.enabled,
|
|
||||||
tc.activeSiteRule
|
|
||||||
);
|
|
||||||
|
|
||||||
// Keep observing while inactive so dynamically-created media/shadow roots
|
// Keep observing while inactive so dynamically-created media/shadow roots
|
||||||
// are available to the next forced SPA rescan, but remove stale controls.
|
// are available to the next forced SPA rescan, but remove stale controls.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Speeder",
|
"name": "Speeder",
|
||||||
"short_name": "Speeder",
|
"short_name": "Speeder",
|
||||||
"version": "6.0.6.0",
|
"version": "6.0.8.0",
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts (New and improved version of \"Video Speed Controller\")",
|
"description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts (New and improved version of \"Video Speed Controller\")",
|
||||||
"homepage_url": "https://github.com/SoPat712/speeder",
|
"homepage_url": "https://github.com/SoPat712/speeder",
|
||||||
|
|||||||
@@ -1029,6 +1029,7 @@
|
|||||||
<button id="restore">Restore Defaults</button>
|
<button id="restore">Restore Defaults</button>
|
||||||
<button id="exportSettings">Export Settings</button>
|
<button id="exportSettings">Export Settings</button>
|
||||||
<button id="importSettings">Import Settings</button>
|
<button id="importSettings">Import Settings</button>
|
||||||
|
<button id="copyDiagnostics">Copy Diagnostics</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="status" role="status" aria-live="polite"></div>
|
<div id="status" role="status" aria-live="polite"></div>
|
||||||
|
|||||||
@@ -2104,6 +2104,41 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
.addEventListener("change", updatePopupEditorDisabledState);
|
.addEventListener("change", updatePopupEditorDisabledState);
|
||||||
|
|
||||||
document.getElementById("save").addEventListener("click", save_options);
|
document.getElementById("save").addEventListener("click", save_options);
|
||||||
|
document.getElementById("copyDiagnostics").addEventListener("click", function () {
|
||||||
|
var status = document.getElementById("status");
|
||||||
|
if (
|
||||||
|
!navigator.clipboard ||
|
||||||
|
typeof navigator.clipboard.writeText !== "function"
|
||||||
|
) {
|
||||||
|
status.textContent = "Clipboard access is unavailable.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var report = popupControlUtils.buildDiagnosticReport({
|
||||||
|
speederVersion: manifest.version,
|
||||||
|
browser: navigator.userAgent,
|
||||||
|
platform: navigator.platform || null,
|
||||||
|
storage: {
|
||||||
|
enabled: document.getElementById("enabled").checked,
|
||||||
|
rememberSpeed: document.getElementById("rememberSpeed").checked,
|
||||||
|
forceLastSavedSpeed:
|
||||||
|
document.getElementById("forceLastSavedSpeed").checked,
|
||||||
|
audioBoolean: document.getElementById("audioBoolean").checked,
|
||||||
|
startHidden: document.getElementById("startHidden").checked,
|
||||||
|
hideWithControls: document.getElementById("hideWithControls").checked,
|
||||||
|
controllerLocation: document.getElementById("controllerLocation").value,
|
||||||
|
shortcutTargetMode:
|
||||||
|
document.getElementById("shortcutTargetMode").value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
navigator.clipboard.writeText(report).then(
|
||||||
|
function() {
|
||||||
|
status.textContent = "Diagnostics copied. Review before sharing.";
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
status.textContent = "Could not copy diagnostics.";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const addSelector = document.getElementById("addShortcutSelector");
|
const addSelector = document.getElementById("addShortcutSelector");
|
||||||
if (addSelector) {
|
if (addSelector) {
|
||||||
|
|||||||
@@ -86,8 +86,13 @@ button:active {
|
|||||||
|
|
||||||
button:focus-visible {
|
button:focus-visible {
|
||||||
outline: 2px solid var(--focus-ring);
|
outline: 2px solid var(--focus-ring);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
#refresh {
|
#refresh {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
@@ -113,6 +118,12 @@ button:focus-visible {
|
|||||||
border-color: #9fbd98;
|
border-color: #9fbd98;
|
||||||
color: #285d21;
|
color: #285d21;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pauseTab[aria-pressed="true"] {
|
||||||
|
background: #fff3d6;
|
||||||
|
border-color: #d8b66b;
|
||||||
|
color: #704f0d;
|
||||||
|
}
|
||||||
|
|
||||||
.popup-divider {
|
.popup-divider {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
|
|||||||
@@ -24,6 +24,12 @@
|
|||||||
type="button"
|
type="button"
|
||||||
aria-pressed="false"
|
aria-pressed="false"
|
||||||
>Force last saved speed</button>
|
>Force last saved speed</button>
|
||||||
|
<button
|
||||||
|
id="pauseTab"
|
||||||
|
class="popup-compact-action"
|
||||||
|
type="button"
|
||||||
|
aria-pressed="false"
|
||||||
|
>Pause on this tab</button>
|
||||||
<div class="popup-divider"></div>
|
<div class="popup-divider"></div>
|
||||||
<div id="popupControlBar" class="popup-control-bar">
|
<div id="popupControlBar" class="popup-control-bar">
|
||||||
<span id="popupSpeed" class="popup-speed">1.00</span>
|
<span id="popupSpeed" class="popup-speed">1.00</span>
|
||||||
@@ -41,6 +47,8 @@
|
|||||||
<div class="popup-links">
|
<div class="popup-links">
|
||||||
<button id="config">Settings</button>
|
<button id="config">Settings</button>
|
||||||
<div class="popup-secondary">
|
<div class="popup-secondary">
|
||||||
|
<button id="addSiteRule" class="secondary">Add current site</button>
|
||||||
|
<button id="copyDiagnostics" class="secondary">Copy diagnostics</button>
|
||||||
<button id="feedback" class="secondary">Feedback</button>
|
<button id="feedback" class="secondary">Feedback</button>
|
||||||
<button id="about" class="secondary">About</button>
|
<button id="about" class="secondary">About</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+190
-5
@@ -1,4 +1,6 @@
|
|||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
if (window.vscPopupInitialized) return;
|
||||||
|
window.vscPopupInitialized = true;
|
||||||
var speederShared =
|
var speederShared =
|
||||||
typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
|
typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
|
||||||
var siteRuleUtils = speederShared.siteRules || {};
|
var siteRuleUtils = speederShared.siteRules || {};
|
||||||
@@ -29,6 +31,61 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
var forceLastSavedSpeedControlledBySiteRule = null;
|
var forceLastSavedSpeedControlledBySiteRule = null;
|
||||||
var selectedFrameToken = null;
|
var selectedFrameToken = null;
|
||||||
var shortcutTargetMode = "closest";
|
var shortcutTargetMode = "closest";
|
||||||
|
var diagnosticContext = null;
|
||||||
|
|
||||||
|
function updateTabPauseButton(paused) {
|
||||||
|
var button = document.querySelector("#pauseTab");
|
||||||
|
button.setAttribute("aria-pressed", paused ? "true" : "false");
|
||||||
|
button.textContent = paused ? "Resume on this tab" : "Pause on this tab";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTabPauseState(tab, callback) {
|
||||||
|
if (!tab || !Number.isInteger(tab.id)) {
|
||||||
|
callback(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
{ action: "get_tab_pause_state", tabId: tab.id },
|
||||||
|
function(response) {
|
||||||
|
callback(
|
||||||
|
!chrome.runtime.lastError && response && response.paused === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentSiteRuleDetails(url) {
|
||||||
|
try {
|
||||||
|
var parsed = new URL(url);
|
||||||
|
if (
|
||||||
|
(parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
|
||||||
|
!parsed.host
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
title: parsed.host,
|
||||||
|
pattern: parsed.protocol + "//" + parsed.host
|
||||||
|
};
|
||||||
|
} catch (_error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDiagnosticReport() {
|
||||||
|
if (!diagnosticContext) return null;
|
||||||
|
return popupControlUtils.buildDiagnosticReport({
|
||||||
|
speederVersion: chrome.runtime.getManifest().version,
|
||||||
|
browser: navigator.userAgent,
|
||||||
|
platform: navigator.platform || null,
|
||||||
|
url: diagnosticContext.url,
|
||||||
|
storage: diagnosticContext.storage,
|
||||||
|
siteRule: diagnosticContext.siteRule,
|
||||||
|
siteRuleDisabled: isSiteRuleDisabled(diagnosticContext.siteRule),
|
||||||
|
tabPaused: diagnosticContext.tabPaused,
|
||||||
|
frame: diagnosticContext.frame
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function persistExpandedSettings(rawStorage, settings, callback) {
|
function persistExpandedSettings(rawStorage, settings, callback) {
|
||||||
var mutation = vscBuildManagedStorageMutation(rawStorage, settings);
|
var mutation = vscBuildManagedStorageMutation(rawStorage, settings);
|
||||||
@@ -125,16 +182,23 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getActiveTabContext(callback) {
|
function getActiveTabContext(callback) {
|
||||||
|
function finish(context) {
|
||||||
|
getTabPauseState(context.tab, function(paused) {
|
||||||
|
context.tabPaused = paused;
|
||||||
|
if (callback) callback(context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
|
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
|
||||||
var activeTab = tabs && tabs[0] ? tabs[0] : null;
|
var activeTab = tabs && tabs[0] ? tabs[0] : null;
|
||||||
if (!activeTab || !activeTab.id) {
|
if (!activeTab || !activeTab.id) {
|
||||||
if (callback) callback({ tab: null, url: "" });
|
finish({ tab: null, url: "" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tabUrl = typeof activeTab.url === "string" ? activeTab.url : "";
|
var tabUrl = typeof activeTab.url === "string" ? activeTab.url : "";
|
||||||
if (tabUrl.length > 0) {
|
if (tabUrl.length > 0) {
|
||||||
if (callback) callback({ tab: activeTab, url: tabUrl });
|
finish({ tab: activeTab, url: tabUrl });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,13 +207,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
{ action: "get_page_context" },
|
{ action: "get_page_context" },
|
||||||
function (response) {
|
function (response) {
|
||||||
if (chrome.runtime.lastError) {
|
if (chrome.runtime.lastError) {
|
||||||
if (callback) callback({ tab: activeTab, url: "" });
|
finish({ tab: activeTab, url: "" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var pageUrl =
|
var pageUrl =
|
||||||
response && typeof response.url === "string" ? response.url : "";
|
response && typeof response.url === "string" ? response.url : "";
|
||||||
if (callback) callback({ tab: activeTab, url: pageUrl });
|
finish({ tab: activeTab, url: pageUrl });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -288,6 +352,104 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
window.open("https://github.com/SoPat712/Speeder/issues");
|
window.open("https://github.com/SoPat712/Speeder/issues");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.querySelector("#copyDiagnostics").addEventListener("click", function () {
|
||||||
|
var report = buildDiagnosticReport();
|
||||||
|
if (!report) {
|
||||||
|
setStatusMessage("Diagnostics are still loading.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!navigator.clipboard || typeof navigator.clipboard.writeText !== "function") {
|
||||||
|
setStatusMessage("Clipboard access is unavailable.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigator.clipboard.writeText(report).then(
|
||||||
|
function () {
|
||||||
|
setStatusMessage("Diagnostics copied. Review before sharing.");
|
||||||
|
},
|
||||||
|
function () {
|
||||||
|
setStatusMessage("Could not copy diagnostics.");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector("#addSiteRule").addEventListener("click", function () {
|
||||||
|
var details = diagnosticContext
|
||||||
|
? getCurrentSiteRuleDetails(diagnosticContext.url)
|
||||||
|
: null;
|
||||||
|
if (!details) {
|
||||||
|
setStatusMessage("A site rule cannot be created for this page.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var added = false;
|
||||||
|
|
||||||
|
updateStoredSettings(
|
||||||
|
function (settings) {
|
||||||
|
var rules = Array.isArray(settings.siteRules) ? settings.siteRules : [];
|
||||||
|
if (
|
||||||
|
rules.some(function(rule) {
|
||||||
|
return rule && rule.pattern === details.pattern;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settings.siteRules = rules.concat([
|
||||||
|
{
|
||||||
|
title: details.title,
|
||||||
|
pattern: details.pattern,
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
added = true;
|
||||||
|
},
|
||||||
|
function (error) {
|
||||||
|
if (error) {
|
||||||
|
setStatusMessage("Could not add this site: " + error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStatusMessage(
|
||||||
|
added
|
||||||
|
? "Site rule added. Opening settings..."
|
||||||
|
: "Site rule already exists. Opening settings..."
|
||||||
|
);
|
||||||
|
window.open(chrome.runtime.getURL("options/options.html"));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector("#pauseTab").addEventListener("click", function () {
|
||||||
|
var tab = diagnosticContext && diagnosticContext.tab;
|
||||||
|
if (!tab || !Number.isInteger(tab.id)) {
|
||||||
|
setStatusMessage("This page cannot be paused.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var button = this;
|
||||||
|
var paused = button.getAttribute("aria-pressed") !== "true";
|
||||||
|
button.disabled = true;
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
{ action: "set_tab_paused", tabId: tab.id, paused: paused },
|
||||||
|
function(response) {
|
||||||
|
button.disabled = false;
|
||||||
|
if (chrome.runtime.lastError || !response) {
|
||||||
|
setStatusMessage("Could not update this tab.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
diagnosticContext.tabPaused = response.paused === true;
|
||||||
|
updateTabPauseButton(diagnosticContext.tabPaused);
|
||||||
|
setControlBarVisible(
|
||||||
|
!diagnosticContext.tabPaused &&
|
||||||
|
diagnosticContext.siteAvailable &&
|
||||||
|
diagnosticContext.showBar
|
||||||
|
);
|
||||||
|
setForceButtonLoading(diagnosticContext.tabPaused);
|
||||||
|
setStatusMessage(
|
||||||
|
diagnosticContext.tabPaused
|
||||||
|
? "Speeder is paused for this tab."
|
||||||
|
: "Speeder resumed for this tab."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
document.querySelector("#donate").addEventListener("click", function () {
|
document.querySelector("#donate").addEventListener("click", function () {
|
||||||
this.classList.add("hide");
|
this.classList.add("hide");
|
||||||
document.querySelector("#donateOptions").classList.remove("hide");
|
document.querySelector("#donateOptions").classList.remove("hide");
|
||||||
@@ -400,6 +562,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
getActiveTabContext(function (context) {
|
getActiveTabContext(function (context) {
|
||||||
if (currentRenderToken !== renderToken) return;
|
if (currentRenderToken !== renderToken) return;
|
||||||
|
|
||||||
|
var tabPaused = context && context.tabPaused === true;
|
||||||
|
updateTabPauseButton(tabPaused);
|
||||||
|
|
||||||
var url = context && context.url ? context.url : "";
|
var url = context && context.url ? context.url : "";
|
||||||
var siteRule = matchSiteRule(url, storage.siteRules);
|
var siteRule = matchSiteRule(url, storage.siteRules);
|
||||||
var siteDisabled = isSiteRuleDisabled(siteRule);
|
var siteDisabled = isSiteRuleDisabled(siteRule);
|
||||||
@@ -419,9 +584,22 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
forceLastSavedSpeedControlledBySiteRule
|
forceLastSavedSpeedControlledBySiteRule
|
||||||
? siteRule.forceLastSavedSpeed === true
|
? siteRule.forceLastSavedSpeed === true
|
||||||
: storage.forceLastSavedSpeed === true;
|
: storage.forceLastSavedSpeed === true;
|
||||||
|
diagnosticContext = {
|
||||||
|
tab: context && context.tab,
|
||||||
|
url: url,
|
||||||
|
storage: storage,
|
||||||
|
siteRule: siteRule,
|
||||||
|
frame: null,
|
||||||
|
tabPaused: tabPaused,
|
||||||
|
siteAvailable: siteAvailable,
|
||||||
|
showBar: showBar
|
||||||
|
};
|
||||||
|
document.querySelector("#addSiteRule").disabled =
|
||||||
|
!getCurrentSiteRuleDetails(url);
|
||||||
|
|
||||||
if (siteRule && siteRule.showPopupControlBar !== undefined) {
|
if (siteRule && siteRule.showPopupControlBar !== undefined) {
|
||||||
showBar = siteRule.showPopupControlBar;
|
showBar = siteRule.showPopupControlBar;
|
||||||
|
diagnosticContext.showBar = showBar;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleEnabledUI(storage.enabled !== false);
|
toggleEnabledUI(storage.enabled !== false);
|
||||||
@@ -430,7 +608,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
resolvePopupButtons(storage, siteRule),
|
resolvePopupButtons(storage, siteRule),
|
||||||
customIconsMap
|
customIconsMap
|
||||||
);
|
);
|
||||||
setControlBarVisible(siteAvailable && showBar);
|
setControlBarVisible(!tabPaused && siteAvailable && showBar);
|
||||||
|
|
||||||
|
if (tabPaused) {
|
||||||
|
setForceButtonLoading(true);
|
||||||
|
setStatusMessage("Speeder is paused for this tab.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (siteDisabled) {
|
if (siteDisabled) {
|
||||||
setForceButtonLoading(false);
|
setForceButtonLoading(false);
|
||||||
@@ -443,6 +627,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
if (siteAvailable) {
|
if (siteAvailable) {
|
||||||
querySpeed(function(frameContext) {
|
querySpeed(function(frameContext) {
|
||||||
if (currentRenderToken !== renderToken) return;
|
if (currentRenderToken !== renderToken) return;
|
||||||
|
diagnosticContext.frame = frameContext || null;
|
||||||
if (
|
if (
|
||||||
frameContext &&
|
frameContext &&
|
||||||
typeof frameContext.forceLastSavedSpeed === "boolean"
|
typeof frameContext.forceLastSavedSpeed === "boolean"
|
||||||
|
|||||||
@@ -71,6 +71,9 @@
|
|||||||
if (typeof result.frameToken === "string") {
|
if (typeof result.frameToken === "string") {
|
||||||
normalized.frameToken = result.frameToken;
|
normalized.frameToken = result.frameToken;
|
||||||
}
|
}
|
||||||
|
if (result.diagnostics && typeof result.diagnostics === "object") {
|
||||||
|
normalized.diagnostics = result.diagnostics;
|
||||||
|
}
|
||||||
if (typeof result.forceLastSavedSpeed === "boolean") {
|
if (typeof result.forceLastSavedSpeed === "boolean") {
|
||||||
normalized.forceLastSavedSpeed = result.forceLastSavedSpeed;
|
normalized.forceLastSavedSpeed = result.forceLastSavedSpeed;
|
||||||
}
|
}
|
||||||
@@ -94,7 +97,61 @@
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSafePageDetails(url) {
|
||||||
|
try {
|
||||||
|
var parsed = new URL(url);
|
||||||
|
return {
|
||||||
|
protocol: parsed.protocol,
|
||||||
|
hostname: parsed.hostname || null
|
||||||
|
};
|
||||||
|
} catch (_error) {
|
||||||
|
return { protocol: null, hostname: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDiagnosticReport(context) {
|
||||||
|
var config = context || {};
|
||||||
|
var storage = config.storage || {};
|
||||||
|
var siteRule = config.siteRule || null;
|
||||||
|
var frame = config.frame || null;
|
||||||
|
|
||||||
|
return JSON.stringify(
|
||||||
|
{
|
||||||
|
speederVersion: config.speederVersion || null,
|
||||||
|
browser: config.browser || null,
|
||||||
|
platform: config.platform || null,
|
||||||
|
page: getSafePageDetails(config.url),
|
||||||
|
globalSettings: {
|
||||||
|
enabled: storage.enabled !== false,
|
||||||
|
rememberSpeed: storage.rememberSpeed === true,
|
||||||
|
forceLastSavedSpeed: storage.forceLastSavedSpeed === true,
|
||||||
|
audioEnabled: storage.audioBoolean === true,
|
||||||
|
startHidden: storage.startHidden === true,
|
||||||
|
hideWithControls: storage.hideWithControls === true,
|
||||||
|
controllerLocation: storage.controllerLocation,
|
||||||
|
shortcutTargetMode: storage.shortcutTargetMode
|
||||||
|
},
|
||||||
|
matchedSiteRule: {
|
||||||
|
matched: Boolean(siteRule),
|
||||||
|
disabled: config.siteRuleDisabled === true,
|
||||||
|
overrideKeys: siteRule
|
||||||
|
? Object.keys(siteRule)
|
||||||
|
.filter(function(key) {
|
||||||
|
return key !== "pattern" && key !== "title";
|
||||||
|
})
|
||||||
|
.sort()
|
||||||
|
: []
|
||||||
|
},
|
||||||
|
tabPaused: config.tabPaused === true,
|
||||||
|
frame: frame && frame.diagnostics ? frame.diagnostics : null
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
buildDiagnosticReport: buildDiagnosticReport,
|
||||||
pickBestFrameSpeedResult: pickBestFrameSpeedResult,
|
pickBestFrameSpeedResult: pickBestFrameSpeedResult,
|
||||||
resolvePopupButtons: resolvePopupButtons,
|
resolvePopupButtons: resolvePopupButtons,
|
||||||
sanitizeButtonOrder: sanitizeButtonOrder
|
sanitizeButtonOrder: sanitizeButtonOrder
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -173,7 +173,13 @@ export function createChromeMock(options = {}) {
|
|||||||
getManifest: vi.fn(() => ({
|
getManifest: vi.fn(() => ({
|
||||||
version: options.manifestVersion || "9.9.9"
|
version: options.manifestVersion || "9.9.9"
|
||||||
})),
|
})),
|
||||||
getURL: vi.fn((url) => "moz-extension://speeder/" + url)
|
getURL: vi.fn((url) => "moz-extension://speeder/" + url),
|
||||||
|
sendMessage: vi.fn((message, callback) => {
|
||||||
|
if (options.runtimeSendMessageImpl) {
|
||||||
|
return options.runtimeSendMessageImpl(message, callback);
|
||||||
|
}
|
||||||
|
if (callback) callback({ paused: false });
|
||||||
|
})
|
||||||
},
|
},
|
||||||
storage: {
|
storage: {
|
||||||
sync: syncArea,
|
sync: syncArea,
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ function createChromeMock(options) {
|
|||||||
const storageOnChanged = createChromeEvent();
|
const storageOnChanged = createChromeEvent();
|
||||||
const tabsOnActivated = createChromeEvent();
|
const tabsOnActivated = createChromeEvent();
|
||||||
const tabsOnUpdated = createChromeEvent();
|
const tabsOnUpdated = createChromeEvent();
|
||||||
|
const tabsOnRemoved = createChromeEvent();
|
||||||
const runtimeOnMessage = createChromeEvent();
|
const runtimeOnMessage = createChromeEvent();
|
||||||
|
|
||||||
const chrome = {
|
const chrome = {
|
||||||
@@ -252,6 +253,12 @@ function createChromeMock(options) {
|
|||||||
lastError: null,
|
lastError: null,
|
||||||
getManifest: vi.fn(() => clone(config.manifest) || { version: "0.0.0-test" }),
|
getManifest: vi.fn(() => clone(config.manifest) || { version: "0.0.0-test" }),
|
||||||
getURL: vi.fn((relPath) => `moz-extension://${relPath}`),
|
getURL: vi.fn((relPath) => `moz-extension://${relPath}`),
|
||||||
|
sendMessage: vi.fn((message, callback) => {
|
||||||
|
if (config.runtimeSendMessageImpl) {
|
||||||
|
return config.runtimeSendMessageImpl(message, callback);
|
||||||
|
}
|
||||||
|
if (callback) callback({ paused: false });
|
||||||
|
}),
|
||||||
onMessage: runtimeOnMessage
|
onMessage: runtimeOnMessage
|
||||||
},
|
},
|
||||||
browserAction: {
|
browserAction: {
|
||||||
@@ -272,7 +279,8 @@ function createChromeMock(options) {
|
|||||||
}),
|
}),
|
||||||
create: vi.fn(),
|
create: vi.fn(),
|
||||||
onActivated: tabsOnActivated,
|
onActivated: tabsOnActivated,
|
||||||
onUpdated: tabsOnUpdated
|
onUpdated: tabsOnUpdated,
|
||||||
|
onRemoved: tabsOnRemoved
|
||||||
},
|
},
|
||||||
storage: {
|
storage: {
|
||||||
onChanged: storageOnChanged,
|
onChanged: storageOnChanged,
|
||||||
|
|||||||
@@ -329,6 +329,31 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
|||||||
expect(video.playbackRate).toBe(initialSpeed);
|
expect(video.playbackRate).toBe(initialSpeed);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("removes and restores controls when this tab is paused", async () => {
|
||||||
|
const chrome = bootInject();
|
||||||
|
await settleLifecycle();
|
||||||
|
const { video } = createControlledVideo();
|
||||||
|
const listener = chrome.runtime.onMessage.listeners[0];
|
||||||
|
const playbackRate = video.playbackRate;
|
||||||
|
|
||||||
|
listener(
|
||||||
|
{ action: "set_tab_paused", paused: true },
|
||||||
|
{},
|
||||||
|
vi.fn()
|
||||||
|
);
|
||||||
|
expect(window.tc.tabPaused).toBe(true);
|
||||||
|
expect(video.vsc).toBeUndefined();
|
||||||
|
expect(video.playbackRate).toBe(playbackRate);
|
||||||
|
|
||||||
|
listener(
|
||||||
|
{ action: "set_tab_paused", paused: false },
|
||||||
|
{},
|
||||||
|
vi.fn()
|
||||||
|
);
|
||||||
|
expect(window.tc.tabPaused).toBe(false);
|
||||||
|
expect(video.vsc).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it("drops a stale hover-preview shortcut target after SPA navigation", async () => {
|
it("drops a stale hover-preview shortcut target after SPA navigation", async () => {
|
||||||
bootInject({
|
bootInject({
|
||||||
url: "https://www.youtube.com/",
|
url: "https://www.youtube.com/",
|
||||||
@@ -590,6 +615,7 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
|||||||
div: wrapper,
|
div: wrapper,
|
||||||
normalControllerMount: normalMount
|
normalControllerMount: normalMount
|
||||||
};
|
};
|
||||||
|
wrapper.showPopover = vi.fn();
|
||||||
window.setupControllerHostTracking(controller, wrapper, normalMount);
|
window.setupControllerHostTracking(controller, wrapper, normalMount);
|
||||||
Object.defineProperty(document, "fullscreenElement", {
|
Object.defineProperty(document, "fullscreenElement", {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
@@ -598,6 +624,8 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
|||||||
|
|
||||||
window.syncControllerFullscreenMount(controller);
|
window.syncControllerFullscreenMount(controller);
|
||||||
expect(fullscreenPlayer.contains(wrapper)).toBe(true);
|
expect(fullscreenPlayer.contains(wrapper)).toBe(true);
|
||||||
|
expect(wrapper.showPopover).not.toHaveBeenCalled();
|
||||||
|
expect(wrapper.hasAttribute("popover")).toBe(false);
|
||||||
|
|
||||||
Object.defineProperty(document, "fullscreenElement", {
|
Object.defineProperty(document, "fullscreenElement", {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
@@ -610,53 +638,7 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
|||||||
controller.controllerHostCleanup();
|
controller.controllerHostCleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("promotes an ancestor-fullscreen controller into the browser top layer", async () => {
|
it("only promotes the directly-fullscreen video's controller", 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);
|
|
||||||
Object.defineProperty(document, "fullscreenElement", {
|
|
||||||
configurable: true,
|
|
||||||
value: fullscreenPlayer
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(window.syncControllerFullscreenMount(controller)).toBe(true);
|
|
||||||
expect(wrapper.parentNode).toBe(fullscreenPlayer);
|
|
||||||
expect(wrapper.showPopover).toHaveBeenCalledOnce();
|
|
||||||
expect(wrapper.getAttribute("popover")).toBe("manual");
|
|
||||||
expect(wrapper.classList.contains("vsc-fullscreen-popover")).toBe(true);
|
|
||||||
|
|
||||||
Object.defineProperty(document, "fullscreenElement", {
|
|
||||||
configurable: true,
|
|
||||||
value: null
|
|
||||||
});
|
|
||||||
window.syncControllerFullscreenMount(controller);
|
|
||||||
expect(wrapper.hidePopover).toHaveBeenCalledOnce();
|
|
||||||
|
|
||||||
wrapper.remove();
|
|
||||||
controller.controllerHostCleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("only promotes the controller owned by the fullscreen player", async () => {
|
|
||||||
bootInject();
|
bootInject();
|
||||||
await settleLifecycle();
|
await settleLifecycle();
|
||||||
|
|
||||||
@@ -680,7 +662,7 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
|||||||
|
|
||||||
Object.defineProperty(document, "fullscreenElement", {
|
Object.defineProperty(document, "fullscreenElement", {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: fullscreenPlayer
|
value: fullscreenVideo
|
||||||
});
|
});
|
||||||
window.syncControllerFullscreenMount(fullscreenVideo.vsc);
|
window.syncControllerFullscreenMount(fullscreenVideo.vsc);
|
||||||
window.syncControllerFullscreenMount(otherVideo.vsc);
|
window.syncControllerFullscreenMount(otherVideo.vsc);
|
||||||
|
|||||||
@@ -84,6 +84,42 @@ describe("options.js", () => {
|
|||||||
expect(document.getElementById("status").textContent).toBe("Auto-saved");
|
expect(document.getElementById("status").textContent).toBe("Auto-saved");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("copies privacy-safe diagnostics from settings", async () => {
|
||||||
|
bootOptions({
|
||||||
|
syncData: {
|
||||||
|
enabled: false,
|
||||||
|
rememberSpeed: true,
|
||||||
|
siteRules: [
|
||||||
|
{
|
||||||
|
title: "Private account",
|
||||||
|
pattern: "secret.example/private-token",
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const writeText = vi.fn(() => Promise.resolve());
|
||||||
|
Object.defineProperty(navigator, "clipboard", {
|
||||||
|
configurable: true,
|
||||||
|
value: { writeText }
|
||||||
|
});
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
|
||||||
|
document.getElementById("copyDiagnostics").click();
|
||||||
|
await flushAsyncWork(3);
|
||||||
|
|
||||||
|
const reportText = writeText.mock.calls[0][0];
|
||||||
|
const report = JSON.parse(reportText);
|
||||||
|
expect(report.globalSettings.enabled).toBe(false);
|
||||||
|
expect(report.globalSettings.rememberSpeed).toBe(true);
|
||||||
|
expect(report.page).toEqual({ protocol: null, hostname: null });
|
||||||
|
expect(report.matchedSiteRule.matched).toBe(false);
|
||||||
|
expect(report.frame).toBeNull();
|
||||||
|
expect(reportText).not.toContain("Private account");
|
||||||
|
expect(reportText).not.toContain("private-token");
|
||||||
|
expect(document.getElementById("status").textContent).toContain("copied");
|
||||||
|
});
|
||||||
|
|
||||||
it("does not partially save options when a required site shortcut is invalid", async () => {
|
it("does not partially save options when a required site shortcut is invalid", async () => {
|
||||||
const chrome = bootOptions({ syncData: { rememberSpeed: false } });
|
const chrome = bootOptions({ syncData: { rememberSpeed: false } });
|
||||||
await flushAsyncWork(3);
|
await flushAsyncWork(3);
|
||||||
|
|||||||
+103
-1
@@ -18,6 +18,7 @@ function bootPopup(options) {
|
|||||||
manifest: { version: "9.9.9-test" },
|
manifest: { version: "9.9.9-test" },
|
||||||
syncData: config.syncData,
|
syncData: config.syncData,
|
||||||
localData: config.localData,
|
localData: config.localData,
|
||||||
|
runtimeSendMessageImpl: config.runtimeSendMessageImpl,
|
||||||
tabsQueryResult: [
|
tabsQueryResult: [
|
||||||
config.activeTab || { id: 99, active: true, url: "https://example.com/" }
|
config.activeTab || { id: 99, active: true, url: "https://example.com/" }
|
||||||
]
|
]
|
||||||
@@ -107,7 +108,7 @@ describe("popup.js", () => {
|
|||||||
executeScriptImpl: (tabId, details, callback) => {
|
executeScriptImpl: (tabId, details, callback) => {
|
||||||
speedQueryCount += 1;
|
speedQueryCount += 1;
|
||||||
callback(
|
callback(
|
||||||
speedQueryCount <= 2
|
speedQueryCount === 1
|
||||||
? [
|
? [
|
||||||
{ speed: 1.25, preferred: false },
|
{ speed: 1.25, preferred: false },
|
||||||
{ speed: 1.5, frameToken: "playing-frame", preferred: true }
|
{ 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 () => {
|
it("toggles enablement and closes after a successful refresh", async () => {
|
||||||
const chrome = bootPopup({
|
const chrome = bootPopup({
|
||||||
syncData: {
|
syncData: {
|
||||||
|
|||||||
Reference in New Issue
Block a user