mirror of
https://github.com/SoPat712/Speeder.git
synced 2026-08-19 11:52:31 -04:00
v6.0.8.0-beta.1
This commit is contained in:
@@ -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
|
||||
|
||||
+117
-14
@@ -90,6 +90,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 +226,7 @@ var tc = {
|
||||
speedAccessTimes: {},
|
||||
persistedLastSpeed: 1.0,
|
||||
activeSiteRule: null,
|
||||
tabPaused: false,
|
||||
siteRuleBase: null,
|
||||
runtimeSettingsHydrated: false,
|
||||
pendingMediaCandidates: [],
|
||||
@@ -163,6 +239,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 +1615,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 +2423,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 +2455,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 +2499,7 @@ function loadInitialRuntimeSettings(attempt) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!siteRuleUtils.isSpeederActiveForSite(
|
||||
tc.settings.enabled,
|
||||
tc.activeSiteRule
|
||||
)
|
||||
!isSpeederActiveForCurrentPage()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -4160,7 +4269,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);
|
||||
});
|
||||
@@ -4418,10 +4527,7 @@ function attachKeydownListeners(doc) {
|
||||
if (isEditableShortcutTarget(event)) return;
|
||||
|
||||
if (
|
||||
!siteRuleUtils.isSpeederActiveForSite(
|
||||
tc.settings.enabled,
|
||||
tc.activeSiteRule
|
||||
)
|
||||
!isSpeederActiveForCurrentPage()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -4646,10 +4752,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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Speeder",
|
||||
"short_name": "Speeder",
|
||||
"version": "6.0.7.0",
|
||||
"version": "6.0.8.0",
|
||||
"manifest_version": 2,
|
||||
"description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts (New and improved version of \"Video Speed Controller\")",
|
||||
"homepage_url": "https://github.com/SoPat712/speeder",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(() => ({
|
||||
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,
|
||||
|
||||
@@ -245,6 +245,7 @@ function createChromeMock(options) {
|
||||
const storageOnChanged = createChromeEvent();
|
||||
const tabsOnActivated = createChromeEvent();
|
||||
const tabsOnUpdated = createChromeEvent();
|
||||
const tabsOnRemoved = createChromeEvent();
|
||||
const runtimeOnMessage = createChromeEvent();
|
||||
|
||||
const chrome = {
|
||||
@@ -252,6 +253,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 +279,8 @@ function createChromeMock(options) {
|
||||
}),
|
||||
create: vi.fn(),
|
||||
onActivated: tabsOnActivated,
|
||||
onUpdated: tabsOnUpdated
|
||||
onUpdated: tabsOnUpdated,
|
||||
onRemoved: tabsOnRemoved
|
||||
},
|
||||
storage: {
|
||||
onChanged: storageOnChanged,
|
||||
|
||||
@@ -329,6 +329,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/",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user