3 Commits

Author SHA1 Message Date
b7684aad09 Update README.md 2025-05-20 03:48:10 -04:00
43dc8b773b fix appear after hiding 2025-05-19 13:15:20 -04:00
2d8a4fc25f add nudge to settings 2025-05-19 12:54:35 -04:00
6 changed files with 477 additions and 592 deletions

View File

@@ -1,3 +1,7 @@
[![Add to Firefox](https://img.shields.io/badge/Add%20to-Firefox-orange?logo=firefox&logoColor=white)](https://addons.mozilla.org/en-US/firefox/addon/video-speed-controller-v1/)
# The science of accelerated playback # The science of accelerated playback
**TL;DR: faster playback translates to better engagement and retention.** **TL;DR: faster playback translates to better engagement and retention.**
@@ -74,10 +78,10 @@ You can try manually disabling Flash from the browser.
[`igrigorik/videospeed`](https://github.com/igrigorik/videospeed) repository [`igrigorik/videospeed`](https://github.com/igrigorik/videospeed) repository
is a port of [`igrigorik`](https://github.com/igrigorik)'s videospeed Chrome is a port of [`igrigorik`](https://github.com/igrigorik)'s videospeed Chrome
add-on for Firefox. This fork modifies the Chrome add-on code so that it works add-on for Firefox. This fork modifies the Chrome add-on code so that it works
in Firefox. This repo is the code behind the [Firefox Extension](https://addons.mozilla.org/en-us/firefox/addon/videospeed/) in Firefox. This repo is the code behind the [Firefox Extension](https://addons.mozilla.org/en-US/firefox/addon/video-speed-controller-v1/)
whereas the [`igrigorik/videospeed`](https://github.com/igrigorik/videospeed) whereas the [`igrigorik/videospeed`](https://github.com/igrigorik/videospeed)
repository contains the code behind the [Chrome Extension](https://chrome.google.com/webstore/detail/video-speed-controller/nffaoalbilbmmfgbnbgppjihopabppdk). repository contains the code behind the [Chrome Extension](https://chrome.google.com/webstore/detail/video-speed-controller/nffaoalbilbmmfgbnbgppjihopabppdk).
### License ### License
(MIT License) - Copyright (c) 2014 Ilya Grigorik (MIT License) - Copyright (c) 2014 Josh Patra

705
inject.js
View File

@@ -2,16 +2,15 @@ var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
var tc = { var tc = {
settings: { settings: {
lastSpeed: 1.0, // default 1x lastSpeed: 1.0,
enabled: true, // default enabled enabled: true,
speeds: {}, // empty object to hold speed for each source speeds: {},
displayKeyCode: 86,
displayKeyCode: 86, // default: V rememberSpeed: false,
rememberSpeed: false, // default: false forceLastSavedSpeed: false,
forceLastSavedSpeed: false, //default: false audioBoolean: false,
audioBoolean: false, // default: false startHidden: false,
startHidden: false, // default: false controllerOpacity: 0.3,
controllerOpacity: 0.3, // default: 0.3
keyBindings: [], keyBindings: [],
blacklist: `\ blacklist: `\
www.instagram.com www.instagram.com
@@ -22,35 +21,25 @@ var tc = {
`.replace(regStrip, ""), `.replace(regStrip, ""),
defaultLogLevel: 4, defaultLogLevel: 4,
logLevel: 3, logLevel: 3,
// --- Nudge settings (ADDED) ---
enableSubtitleNudge: true, enableSubtitleNudge: true,
subtitleNudgeInterval: 25, subtitleNudgeInterval: 25,
subtitleNudgeAmount: 0.001 subtitleNudgeAmount: 0.001
}, },
// Holds a reference to all of the AUDIO/VIDEO DOM elements we've attached to
mediaElements: [], mediaElements: [],
isNudging: false // Flag for nudge operation (ADDED) isNudging: false
}; };
/* Log levels (depends on caller specifying the correct level) */ /* Log levels */
function log(message, level) { function log(message, level) {
verbosity = tc.settings.logLevel; verbosity = tc.settings.logLevel;
if (typeof level === "undefined") { if (typeof level === "undefined") level = tc.settings.defaultLogLevel;
level = tc.settings.defaultLogLevel;
}
if (verbosity >= level) { if (verbosity >= level) {
// MODIFIED: Added [VSC] prefix for clarity
let prefix = "[VSC] "; let prefix = "[VSC] ";
if (level === 2) { if (level === 2) console.log(prefix + "ERROR: " + message);
console.log(prefix + "ERROR: " + message); else if (level === 3) console.log(prefix + "WARNING: " + message);
} else if (level === 3) { else if (level === 4) console.log(prefix + "INFO: " + message);
console.log(prefix + "WARNING: " + message); else if (level === 5) console.log(prefix + "DEBUG: " + message);
} else if (level === 4) { else if (level === 6) {
console.log(prefix + "INFO: " + message);
} else if (level === 5) {
console.log(prefix + "DEBUG: " + message);
} else if (level === 6) {
console.log(prefix + "DEBUG (VERBOSE): " + message); console.log(prefix + "DEBUG (VERBOSE): " + message);
console.trace(); console.trace();
} }
@@ -58,7 +47,6 @@ function log(message, level) {
} }
chrome.storage.sync.get(tc.settings, function (storage) { chrome.storage.sync.get(tc.settings, function (storage) {
// MODIFIED: Robust keyBinding initialization from storage or defaults.
tc.settings.keyBindings = tc.settings.keyBindings =
Array.isArray(storage.keyBindings) && Array.isArray(storage.keyBindings) &&
storage.keyBindings.length > 0 && storage.keyBindings.length > 0 &&
@@ -99,7 +87,7 @@ chrome.storage.sync.get(tc.settings, function (storage) {
value: 1.0, value: 1.0,
force: false, force: false,
predefined: true predefined: true
}, // Default value for reset action is 1.0 },
{ {
action: "fast", action: "fast",
key: Number(storage.fastKeyCode) || 71, key: Number(storage.fastKeyCode) || 71,
@@ -114,11 +102,10 @@ chrome.storage.sync.get(tc.settings, function (storage) {
(storage.keyBindings.length > 0 && (storage.keyBindings.length > 0 &&
!storage.keyBindings[0].hasOwnProperty("predefined")) !storage.keyBindings[0].hasOwnProperty("predefined"))
) { ) {
log("Initializing/Updating keybindings in storage.", 4);
chrome.storage.sync.set({ chrome.storage.sync.set({
keyBindings: tc.settings.keyBindings, keyBindings: tc.settings.keyBindings,
version: "0.6.3.8" version: "0.6.3.13"
}); // Update version }); // Incremented
} }
tc.settings.lastSpeed = Number(storage.lastSpeed) || 1.0; tc.settings.lastSpeed = Number(storage.lastSpeed) || 1.0;
@@ -131,17 +118,14 @@ chrome.storage.sync.get(tc.settings, function (storage) {
tc.settings.startHidden = Boolean(storage.startHidden); tc.settings.startHidden = Boolean(storage.startHidden);
tc.settings.controllerOpacity = Number(storage.controllerOpacity) || 0.3; tc.settings.controllerOpacity = Number(storage.controllerOpacity) || 0.3;
tc.settings.blacklist = String(storage.blacklist || tc.settings.blacklist); tc.settings.blacklist = String(storage.blacklist || tc.settings.blacklist);
if (typeof storage.logLevel !== "undefined")
if (typeof storage.logLevel !== "undefined") {
tc.settings.logLevel = Number(storage.logLevel); tc.settings.logLevel = Number(storage.logLevel);
}
// ADDED: Load nudge settings from storage
tc.settings.enableSubtitleNudge = tc.settings.enableSubtitleNudge =
typeof storage.enableSubtitleNudge !== "undefined" typeof storage.enableSubtitleNudge !== "undefined"
? Boolean(storage.enableSubtitleNudge) ? Boolean(storage.enableSubtitleNudge)
: tc.settings.enableSubtitleNudge; : tc.settings.enableSubtitleNudge;
tc.settings.subtitleNudgeInterval = tc.settings.subtitleNudgeInterval =
Number(storage.subtitleNudgeInterval) || 25; // Using 25ms as requested Number(storage.subtitleNudgeInterval) || 25;
tc.settings.subtitleNudgeAmount = tc.settings.subtitleNudgeAmount =
Number(storage.subtitleNudgeAmount) || tc.settings.subtitleNudgeAmount; Number(storage.subtitleNudgeAmount) || tc.settings.subtitleNudgeAmount;
@@ -156,18 +140,17 @@ chrome.storage.sync.get(tc.settings, function (storage) {
predefined: true predefined: true
}); });
} }
initializeWhenReady(document); initializeWhenReady(document);
}); });
function getKeyBindings(action, what = "value") { function getKeyBindings(action, what = "value") {
if (!tc.settings.keyBindings) return false; // ADDED: Guard against undefined keyBindings /* ... Same as your provided ... */
if (!tc.settings.keyBindings) return false;
try { try {
const binding = tc.settings.keyBindings.find( const binding = tc.settings.keyBindings.find(
(item) => item.action === action (item) => item.action === action
); );
if (binding) return binding[what]; if (binding) return binding[what];
// Fallback defaults for safety
if (what === "value") { if (what === "value") {
if (action === "slower" || action === "faster") return 0.1; if (action === "slower" || action === "faster") return 0.1;
if (action === "rewind" || action === "advance") return 10; if (action === "rewind" || action === "advance") return 10;
@@ -180,66 +163,39 @@ function getKeyBindings(action, what = "value") {
return false; return false;
} }
} }
// Original setKeyBindings, used by original resetSpeed logic.
function setKeyBindings(action, value) { function setKeyBindings(action, value) {
if (!tc.settings.keyBindings) return; // ADDED: Guard /* ... Same as your provided ... */
if (!tc.settings.keyBindings) return;
const binding = tc.settings.keyBindings.find( const binding = tc.settings.keyBindings.find(
(item) => item.action === action (item) => item.action === action
); );
if (binding) { if (binding) binding["value"] = value;
binding["value"] = value;
log(
`In-memory value for keyBinding '${action}' set to ${value} by original setKeyBindings func`,
6
);
}
} }
function defineVideoController() { function defineVideoController() {
tc.videoController = function (target, parent) { tc.videoController = function (target, parent) {
if (target.vsc) { if (target.vsc) return target.vsc;
log(`VSC controller already exists for ${target.src || "video"}.`, 6);
return target.vsc;
}
log(`Creating VSC controller for ${target.src || "video"}.`, 4);
tc.mediaElements.push(target); tc.mediaElements.push(target);
target.vsc = this; target.vsc = this;
this.video = target; this.video = target;
this.parent = parent || target.parentElement; this.parent = parent || target.parentElement;
this.nudgeIntervalId = null; // ADDED: For the subtitle nudge feature this.nudgeIntervalId = null;
// Original logic for determining initial speed
let storedSpeed; let storedSpeed;
if (!tc.settings.rememberSpeed) { if (!tc.settings.rememberSpeed) {
storedSpeed = tc.settings.speeds[target.currentSrc]; storedSpeed = tc.settings.speeds[target.currentSrc];
if (!storedSpeed) { if (!storedSpeed) storedSpeed = 1.0;
log( setKeyBindings("reset", getKeyBindings("fast"));
"Overwriting stored speed to 1.0 due to rememberSpeed being disabled or no speed for src",
5
);
storedSpeed = 1.0;
}
setKeyBindings("reset", getKeyBindings("fast")); // Original logic for 'R' key toggle state
} else { } else {
log("Recalling stored speed due to rememberSpeed being enabled", 5);
storedSpeed = storedSpeed =
tc.settings.speeds[target.currentSrc] || tc.settings.lastSpeed; tc.settings.speeds[target.currentSrc] || tc.settings.lastSpeed;
} }
if (tc.settings.forceLastSavedSpeed) { if (tc.settings.forceLastSavedSpeed) storedSpeed = tc.settings.lastSpeed;
storedSpeed = tc.settings.lastSpeed;
}
this.div = this.initializeControls(); this.div = this.initializeControls();
if (Math.abs(target.playbackRate - storedSpeed) > 0.001) { if (Math.abs(target.playbackRate - storedSpeed) > 0.001) {
log( setSpeed(target, storedSpeed, true, false); // isInitialCall=true, isUserKeyPress=false
`Video current rate ${target.playbackRate.toFixed(2)} differs from VSC target ${storedSpeed.toFixed(2)}. Setting speed (initial).`,
4
);
setSpeed(target, storedSpeed, true); // MODIFIED: Pass true for isInitialSet
} else { } else {
if (this.speedIndicator) if (this.speedIndicator)
this.speedIndicator.textContent = storedSpeed.toFixed(2); this.speedIndicator.textContent = storedSpeed.toFixed(2);
@@ -251,12 +207,10 @@ function defineVideoController() {
} }
} }
// Original mediaEventAction
var mediaEventAction = function (event) { var mediaEventAction = function (event) {
const video = event.target; const video = event.target;
if (!video.vsc) return; if (!video.vsc) return;
let speedToSet = tc.settings.speeds[video.currentSrc];
let speedToSet = tc.settings.speeds[video.currentSrc]; // Use 'let'
if (!tc.settings.rememberSpeed) { if (!tc.settings.rememberSpeed) {
if (!speedToSet) speedToSet = 1.0; if (!speedToSet) speedToSet = 1.0;
setKeyBindings("reset", getKeyBindings("fast")); setKeyBindings("reset", getKeyBindings("fast"));
@@ -266,14 +220,9 @@ function defineVideoController() {
if (tc.settings.forceLastSavedSpeed) speedToSet = tc.settings.lastSpeed; if (tc.settings.forceLastSavedSpeed) speedToSet = tc.settings.lastSpeed;
if (Math.abs(video.playbackRate - speedToSet) > 0.001) { if (Math.abs(video.playbackRate - speedToSet) > 0.001) {
log( // Speed corrections from play/seek are not direct user key presses for blink
`Media event '${event.type}': rate ${video.playbackRate.toFixed(2)} vs target ${speedToSet.toFixed(2)}. Setting.`, setSpeed(video, speedToSet, false, false); // isInitialCall=false, isUserKeyPress=false
4
);
setSpeed(video, speedToSet, false); // MODIFIED: isInitialSet is false
} }
// ADDED: Manage nudge based on event type
if (event.type === "play") video.vsc.startSubtitleNudge(); if (event.type === "play") video.vsc.startSubtitleNudge();
else if (event.type === "pause" || event.type === "ended") else if (event.type === "pause" || event.type === "ended")
video.vsc.stopSubtitleNudge(); video.vsc.stopSubtitleNudge();
@@ -286,11 +235,11 @@ function defineVideoController() {
target.addEventListener( target.addEventListener(
"pause", "pause",
(this.handlePause = mediaEventAction.bind(this)) (this.handlePause = mediaEventAction.bind(this))
); // ADDED for nudge );
target.addEventListener( target.addEventListener(
"ended", "ended",
(this.handleEnded = mediaEventAction.bind(this)) (this.handleEnded = mediaEventAction.bind(this))
); // ADDED for nudge );
target.addEventListener( target.addEventListener(
"seeked", "seeked",
(this.handleSeek = mediaEventAction.bind(this)) (this.handleSeek = mediaEventAction.bind(this))
@@ -303,15 +252,13 @@ function defineVideoController() {
(mutation.attributeName === "src" || (mutation.attributeName === "src" ||
mutation.attributeName === "currentSrc") mutation.attributeName === "currentSrc")
) { ) {
if (!this.div) return; // MODIFIED: Check if div exists if (!this.div) return;
log(`Src changed to: ${mutation.target.currentSrc || "empty"}`, 4); this.stopSubtitleNudge();
this.stopSubtitleNudge(); // ADDED: Stop nudge for old src if (!mutation.target.src && !mutation.target.currentSrc)
if (!mutation.target.src && !mutation.target.currentSrc) {
this.div.classList.add("vsc-nosource"); this.div.classList.add("vsc-nosource");
} else { else {
this.div.classList.remove("vsc-nosource"); this.div.classList.remove("vsc-nosource");
let newSrcSpeed = tc.settings.speeds[mutation.target.currentSrc]; // MODIFIED: Follow original logic let newSrcSpeed = tc.settings.speeds[mutation.target.currentSrc];
if (!tc.settings.rememberSpeed) { if (!tc.settings.rememberSpeed) {
if (!newSrcSpeed) newSrcSpeed = 1.0; if (!newSrcSpeed) newSrcSpeed = 1.0;
} else { } else {
@@ -319,24 +266,22 @@ function defineVideoController() {
} }
if (tc.settings.forceLastSavedSpeed) if (tc.settings.forceLastSavedSpeed)
newSrcSpeed = tc.settings.lastSpeed; newSrcSpeed = tc.settings.lastSpeed;
setSpeed(mutation.target, newSrcSpeed, true, false); // isInitialCall=true, isUserKeyPress=false
setSpeed(mutation.target, newSrcSpeed, true); // MODIFIED: isInitialSet = true
if (!mutation.target.paused && mutation.target.playbackRate !== 1.0) if (!mutation.target.paused && mutation.target.playbackRate !== 1.0)
this.startSubtitleNudge(); // ADDED this.startSubtitleNudge();
} }
} }
}); });
}); });
srcObserver.observe(target, { attributeFilter: ["src", "currentSrc"] }); srcObserver.observe(target, { attributeFilter: ["src", "currentSrc"] });
// ADDED: Initial nudge check if (!target.paused && target.playbackRate !== 1.0)
if (!target.paused && target.playbackRate !== 1.0) {
this.startSubtitleNudge(); this.startSubtitleNudge();
}
}; };
// --- Nudge Methods (ADDED) ---
tc.videoController.prototype.startSubtitleNudge = function () { tc.videoController.prototype.startSubtitleNudge = function () {
/* ... Same as your provided ... */
if (!location.hostname.includes("youtube.com")) return;
if ( if (
!tc.settings.enableSubtitleNudge || !tc.settings.enableSubtitleNudge ||
this.nudgeIntervalId !== null || this.nudgeIntervalId !== null ||
@@ -347,10 +292,6 @@ function defineVideoController() {
this.stopSubtitleNudge(); this.stopSubtitleNudge();
return; return;
} }
log(
`Nudge: Starting for ${this.video.currentSrc || "video"} (Rate: ${this.video.playbackRate.toFixed(2)}) interval: ${tc.settings.subtitleNudgeInterval}ms.`,
5
);
this.nudgeIntervalId = setInterval(() => { this.nudgeIntervalId = setInterval(() => {
if ( if (
!this.video || !this.video ||
@@ -363,10 +304,8 @@ function defineVideoController() {
} }
const currentRate = this.video.playbackRate; const currentRate = this.video.playbackRate;
const nudgeAmount = tc.settings.subtitleNudgeAmount; const nudgeAmount = tc.settings.subtitleNudgeAmount;
tc.isNudging = true; tc.isNudging = true;
this.video.playbackRate = currentRate + nudgeAmount; this.video.playbackRate = currentRate + nudgeAmount;
requestAnimationFrame(() => { requestAnimationFrame(() => {
if ( if (
this.video && this.video &&
@@ -379,64 +318,41 @@ function defineVideoController() {
}); });
}, tc.settings.subtitleNudgeInterval); }, tc.settings.subtitleNudgeInterval);
}; };
tc.videoController.prototype.stopSubtitleNudge = function () { tc.videoController.prototype.stopSubtitleNudge = function () {
/* ... Same as your provided ... */
if (this.nudgeIntervalId !== null) { if (this.nudgeIntervalId !== null) {
log(
`Nudge: Stopping for ${this.video ? this.video.currentSrc || "video" : "detached video"}`,
5
);
clearInterval(this.nudgeIntervalId); clearInterval(this.nudgeIntervalId);
this.nudgeIntervalId = null; this.nudgeIntervalId = null;
} }
}; };
// --- End Nudge Methods ---
tc.videoController.prototype.remove = function () { tc.videoController.prototype.remove = function () {
this.stopSubtitleNudge(); // ADDED /* ... Same as your provided ... */
// Original remove logic: this.stopSubtitleNudge();
if (this.div && this.div.parentNode) this.div.remove(); if (this.div && this.div.parentNode) this.div.remove();
if (this.video) { if (this.video) {
this.video.removeEventListener("play", this.handlePlay); this.video.removeEventListener("play", this.handlePlay);
this.video.removeEventListener("pause", this.handlePause); // ADDED this.video.removeEventListener("pause", this.handlePause);
this.video.removeEventListener("ended", this.handleEnded); // ADDED this.video.removeEventListener("ended", this.handleEnded);
this.video.removeEventListener("seeked", this.handleSeek); // MODIFIED: was "seek" in original provided code this.video.removeEventListener("seeked", this.handleSeek);
delete this.video.vsc; delete this.video.vsc;
} }
let idx = tc.mediaElements.indexOf(this.video); let idx = tc.mediaElements.indexOf(this.video);
if (idx !== -1) tc.mediaElements.splice(idx, 1); if (idx !== -1) tc.mediaElements.splice(idx, 1);
}; };
// Original initializeControls
tc.videoController.prototype.initializeControls = function () { tc.videoController.prototype.initializeControls = function () {
log("initializeControls Begin", 5); /* ... Same as your provided ... */
const doc = this.video.ownerDocument; const doc = this.video.ownerDocument;
const speedForUI = this.video.playbackRate.toFixed(2); const speedForUI = this.video.playbackRate.toFixed(2);
var top = Math.max(this.video.offsetTop, 0) + "px", var top = Math.max(this.video.offsetTop, 0) + "px",
left = Math.max(this.video.offsetLeft, 0) + "px"; left = Math.max(this.video.offsetLeft, 0) + "px";
log("Speed variable for UI set to: " + speedForUI, 5);
var wrapper = doc.createElement("div"); var wrapper = doc.createElement("div");
wrapper.classList.add("vsc-controller"); wrapper.classList.add("vsc-controller");
if (!this.video.src && !this.video.currentSrc) if (!this.video.src && !this.video.currentSrc)
wrapper.classList.add("vsc-nosource"); wrapper.classList.add("vsc-nosource");
if (tc.settings.startHidden) wrapper.classList.add("vsc-hidden"); if (tc.settings.startHidden) wrapper.classList.add("vsc-hidden");
var shadow = wrapper.attachShadow({ mode: "open" }); var shadow = wrapper.attachShadow({ mode: "open" });
var shadowTemplate = ` shadow.innerHTML = `<style> @import "${chrome.runtime.getURL("shadow.css")}"; </style><div id="controller" style="top:${top}; left:${left}; opacity:${tc.settings.controllerOpacity}"><span data-action="drag" class="draggable">${speedForUI}</span><span id="controls"><button data-action="rewind" class="rw">«</button><button data-action="slower"></button><button data-action="faster">+</button><button data-action="advance" class="rw">»</button><button data-action="display" class="hideButton">×</button></span></div>`;
<style> @import "${chrome.runtime.getURL("shadow.css")}"; </style> this.speedIndicator = shadow.querySelector(".draggable");
<div id="controller" style="top:${top}; left:${left}; opacity:${tc.settings.controllerOpacity}">
<span data-action="drag" class="draggable">${speedForUI}</span>
<span id="controls">
<button data-action="rewind" class="rw">«</button>
<button data-action="slower"></button>
<button data-action="faster">+</button>
<button data-action="advance" class="rw">»</button>
<button data-action="display" class="hideButton">×</button>
</span>
</div>`;
shadow.innerHTML = shadowTemplate;
this.speedIndicator = shadow.querySelector(".draggable"); // MODIFIED: Original was "span"
// MODIFIED: Pass this.video as 4th arg to runAction
shadow.querySelector(".draggable").addEventListener( shadow.querySelector(".draggable").addEventListener(
"mousedown", "mousedown",
(e) => { (e) => {
@@ -450,8 +366,8 @@ function defineVideoController() {
}, },
true true
); );
shadow.querySelectorAll("button").forEach((button) => { shadow.querySelectorAll("button").forEach((btn) =>
button.addEventListener( btn.addEventListener(
"click", "click",
(e) => { (e) => {
runAction( runAction(
@@ -463,8 +379,8 @@ function defineVideoController() {
e.stopPropagation(); e.stopPropagation();
}, },
true true
)
); );
});
shadow shadow
.querySelector("#controller") .querySelector("#controller")
.addEventListener("click", (e) => e.stopPropagation(), false); .addEventListener("click", (e) => e.stopPropagation(), false);
@@ -473,109 +389,93 @@ function defineVideoController() {
.addEventListener("mousedown", (e) => e.stopPropagation(), false); .addEventListener("mousedown", (e) => e.stopPropagation(), false);
var fragment = doc.createDocumentFragment(); var fragment = doc.createDocumentFragment();
fragment.appendChild(wrapper); fragment.appendChild(wrapper);
const parentEl = this.parent || this.video.parentElement; const pEl = this.parent || this.video.parentElement;
if (!parentEl || !parentEl.parentNode) { if (!pEl || !pEl.parentNode) {
doc.body.appendChild(fragment); doc.body.appendChild(fragment);
return wrapper; return wrapper;
} }
// Original placement logic
switch (true) { switch (true) {
case location.hostname == "www.amazon.com": case location.hostname == "www.amazon.com":
case location.hostname == "www.reddit.com": case location.hostname == "www.reddit.com":
case /hbogo\./.test(location.hostname): case /hbogo\./.test(location.hostname):
parentEl.parentElement.insertBefore(fragment, parentEl); pEl.parentElement.insertBefore(fragment, pEl);
break; break;
case location.hostname == "www.facebook.com": case location.hostname == "www.facebook.com":
let p = let p =
parentEl.parentElement.parentElement.parentElement.parentElement pEl.parentElement.parentElement.parentElement.parentElement
.parentElement.parentElement.parentElement; .parentElement.parentElement.parentElement;
if (p && p.firstChild) p.insertBefore(fragment, p.firstChild); if (p && p.firstChild) p.insertBefore(fragment, p.firstChild);
else if (parentEl.firstChild) else if (pEl.firstChild) pEl.insertBefore(fragment, pEl.firstChild);
parentEl.insertBefore(fragment, parentEl.firstChild); else pEl.appendChild(fragment);
else parentEl.appendChild(fragment);
break; break;
case location.hostname == "tv.apple.com": case location.hostname == "tv.apple.com":
const appleRoot = parentEl.getRootNode(); const r = pEl.getRootNode();
const scrim = const s = r && r.querySelector ? r.querySelector(".scrim") : null;
appleRoot && appleRoot.querySelector if (s) s.prepend(fragment);
? appleRoot.querySelector(".scrim") else pEl.insertBefore(fragment, pEl.firstChild);
: null;
if (scrim) scrim.prepend(fragment);
else parentEl.insertBefore(fragment, parentEl.firstChild);
break; break;
default: default:
parentEl.insertBefore(fragment, parentEl.firstChild); pEl.insertBefore(fragment, pEl.firstChild);
} }
return wrapper; return wrapper;
}; };
} }
function escapeStringRegExp(str) { function escapeStringRegExp(str) {
const matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g; const m = /[|\\{}()[\]^$+*?.]/g;
return str.replace(matchOperatorsRe, "\\$&"); return str.replace(m, "\\$&");
} }
function isBlacklisted() { function isBlacklisted() {
/* ... Same as your provided ... */
let blacklisted = false; let blacklisted = false;
const blacklistLines = tc.settings.blacklist const bl = tc.settings.blacklist ? tc.settings.blacklist.split("\n") : [];
? tc.settings.blacklist.split("\n") bl.forEach((m) => {
: [];
blacklistLines.forEach((match) => {
if (blacklisted) return; if (blacklisted) return;
match = match.replace(regStrip, ""); m = m.replace(regStrip, "");
if (match.length == 0) return; if (m.length == 0) return;
let regexp; let rgx;
if (match.startsWith("/") && match.lastIndexOf("/") > 0) { if (m.startsWith("/") && m.lastIndexOf("/") > 0) {
try { try {
const ls = match.lastIndexOf("/"); const ls = m.lastIndexOf("/");
regexp = new RegExp(match.substring(1, ls), match.substring(ls + 1)); rgx = new RegExp(m.substring(1, ls), m.substring(ls + 1));
} catch (err) { } catch (e) {
log(`Invalid regex in blacklist: ${match}. Error: ${err.message}`, 2); log(`Invalid regex: ${m}. ${e.message}`, 2);
return; return;
} }
} else regexp = new RegExp(escapeStringRegExp(match)); } else rgx = new RegExp(escapeStringRegExp(m));
if (regexp && regexp.test(location.href)) blacklisted = true; if (rgx && rgx.test(location.href)) blacklisted = true;
}); });
if (blacklisted) log(`Page ${location.href} is blacklisted.`, 4); if (blacklisted) log(`Page ${location.href} blacklisted.`, 4);
return blacklisted; return blacklisted;
} }
var coolDown = false; var coolDown = false;
function refreshCoolDown() { function refreshCoolDown() {
log("Begin refreshCoolDown", 5); /* ... Same as your provided ... */
if (coolDown) clearTimeout(coolDown); if (coolDown) clearTimeout(coolDown);
coolDown = setTimeout(function () { coolDown = setTimeout(() => {
coolDown = false; coolDown = false;
}, 1000); }, 1000);
// log("End refreshCoolDown", 6); // Original log level was 5
} }
function setupListener() { function setupListener() {
if (document.vscRateListenerAttached) return; // MODIFIED: Ensure flag check if (document.vscRateListenerAttached) return;
// MODIFIED: fromUserInput parameter added // MODIFIED: updateSpeedFromEvent NO LONGER calls runAction("blink")
function updateSpeedFromEvent(video, fromUserInput = false) { function updateSpeedFromEvent(video) {
if (!video.vsc || !video.vsc.speedIndicator) return; if (!video.vsc || !video.vsc.speedIndicator) return;
var speed = Number(video.playbackRate.toFixed(2)); var speed = Number(video.playbackRate.toFixed(2));
log( log(`updateSpeedFromEvent: Rate is ${speed}.`, 4); // Removed fromUserInput from this log
`updateSpeedFromEvent: Rate is ${speed}. FromUserInput: ${fromUserInput}`,
4
);
video.vsc.speedIndicator.textContent = speed.toFixed(2); video.vsc.speedIndicator.textContent = speed.toFixed(2);
tc.settings.speeds[video.currentSrc || "unknown_src"] = speed; tc.settings.speeds[video.currentSrc || "unknown_src"] = speed;
tc.settings.lastSpeed = speed; tc.settings.lastSpeed = speed;
chrome.storage.sync.set({ lastSpeed: speed }, () => { chrome.storage.sync.set({ lastSpeed: speed }, () => {
if (chrome.runtime.lastError) /* ... */
log(`Error saving lastSpeed: ${chrome.runtime.lastError.message}`, 2);
}); });
// MODIFIED: Only "blink" (show controller) if change was from user input // runAction("blink") is now called directly from setSpeed if it's a user key press.
if (fromUserInput) {
runAction("blink", getKeyBindings("blink", "value") || 1000, null, video);
}
if (video.vsc) { if (video.vsc) {
// MODIFIED: Manage nudge based on new speed
if (speed === 1.0 || video.paused) video.vsc.stopSubtitleNudge(); if (speed === 1.0 || video.paused) video.vsc.stopSubtitleNudge();
else video.vsc.startSubtitleNudge(); else video.vsc.startSubtitleNudge();
} }
@@ -584,15 +484,10 @@ function setupListener() {
document.addEventListener( document.addEventListener(
"ratechange", "ratechange",
function (event) { function (event) {
// ADDED: Check tc.isNudging at the very start if (tc.isNudging) return; // Ignore nudge's own rate changes for VSC UI/state logic
if (tc.isNudging) {
// log("Ratechange event during nudge, VSC UI/state update skipped. Allowing propagation for YT.", 6);
return;
}
// Original coolDown logic
if (coolDown) { if (coolDown) {
log("Speed event propagation blocked by coolDown", 4); log("Blocked by coolDown", 4);
event.stopImmediatePropagation(); event.stopImmediatePropagation();
return; return;
} }
@@ -602,149 +497,96 @@ function setupListener() {
return; return;
const eventOrigin = event.detail && event.detail.origin; const eventOrigin = event.detail && event.detail.origin;
let isFromUserInputForBlink = false; // MODIFIED: Flag to control blink // The `fromUserInput` flag that was passed to updateSpeedFromEvent is removed from here.
// updateSpeedFromEvent now just updates state. Blinking is handled by setSpeed.
if (tc.settings.forceLastSavedSpeed) { if (tc.settings.forceLastSavedSpeed) {
if (eventOrigin === "videoSpeed") { if (eventOrigin === "videoSpeed") {
// This "videoSpeed" event is dispatched by setSpeed when forceLastSavedSpeed is true.
// setSpeed itself will handle blinking if it was a user key press.
if (event.detail.speed) { if (event.detail.speed) {
const detailSpeedNum = Number(event.detail.speed); const detailSpeedNum = Number(event.detail.speed);
if ( if (
!isNaN(detailSpeedNum) && !isNaN(detailSpeedNum) &&
Math.abs(video.playbackRate - detailSpeedNum) > 0.001 Math.abs(video.playbackRate - detailSpeedNum) > 0.001
) { ) {
video.playbackRate = detailSpeedNum; video.playbackRate = detailSpeedNum; // As per original forceLastSavedSpeed logic
} }
} }
isFromUserInputForBlink = event.detail.fromUserInput !== false; // Respect passed flag updateSpeedFromEvent(video); // Update state
updateSpeedFromEvent(video, isFromUserInputForBlink); event.stopImmediatePropagation(); // Original behavior
event.stopImmediatePropagation();
} else { } else {
// Native event when forceLastSavedSpeed is ON
if (Math.abs(video.playbackRate - tc.settings.lastSpeed) > 0.001) { if (Math.abs(video.playbackRate - tc.settings.lastSpeed) > 0.001) {
log(
`Ratechange (Force ON): Discrepancy. Video rate: ${video.playbackRate.toFixed(2)}, VSC wants: ${tc.settings.lastSpeed.toFixed(2)}. Forcing.`,
3
);
video.playbackRate = tc.settings.lastSpeed; video.playbackRate = tc.settings.lastSpeed;
event.stopImmediatePropagation(); event.stopImmediatePropagation();
// The next ratechange will be from VSC forcing, consider that not direct user input for blink // The next ratechange (from VSC forcing it) will call updateSpeedFromEvent.
// updateSpeedFromEvent will be called by that next event.
} else { } else {
updateSpeedFromEvent(video, false); // Not user input, just confirming forced speed updateSpeedFromEvent(video); // Just confirming, no blink needed from here
} }
} }
} else { } else {
// forceLastSavedSpeed is OFF // forceLastSavedSpeed is OFF
// Determine if it was a VSC-initiated user action (like S/D keys) updateSpeedFromEvent(video); // Update state
// The `setSpeed` function, when called by `runAction`, doesn't add a special origin detail // DO NOT stop propagation
// when forceLastSavedSpeed is off. So, a native ratechange event fires.
// We assume if forceLastSavedSpeed is off, any rate change processed here
// that isn't a nudge IS significant enough to update UI state.
// The "blink" should happen if tc.settings.lastSpeed *changed* due to this event,
// implying it wasn't just a confirmation of existing speed.
const oldLastSpeed = tc.settings.lastSpeed;
updateSpeedFromEvent(video, false); // Initially assume not user-driven for blink
if (
Math.abs(oldLastSpeed - tc.settings.lastSpeed) > 0.001 &&
oldLastSpeed !== 1.0 &&
tc.settings.lastSpeed !== 1.0
) {
// If lastSpeed actually changed due to this event, it was likely a user action via VSC
// or a significant external change. Trigger blink.
// Exception: don't blink if going to/from 1.0x as that's often a reset.
// This logic is imperfect for determining "user input" when not forcing.
// A cleaner way would be if setSpeed could flag the *next* native event.
// For now, this is a heuristic.
if (!tc.isNudging) {
// Double check not a nudge, though already filtered
runAction(
"blink",
getKeyBindings("blink", "value") || 1000,
null,
video
);
}
}
} }
}, },
true true
); );
document.vscRateChangeListenerAttached = true; // MODIFIED: Ensure flag is set document.vscRateListenerAttached = true;
} }
// MODIFIED: More robust initialization flow with unique flags
var vscInitializedDocuments = new Set(); var vscInitializedDocuments = new Set();
function initializeWhenReady(doc) { function initializeWhenReady(doc) {
if (doc.vscInitWhenReadyCalledFullUniqueFlag && doc.readyState !== "loading") /* ... Same robust init ... */
return; if (doc.vscInitWhenReadyUniqueFlag1 && doc.readyState !== "loading") return;
doc.vscInitWhenReadyCalledFullUniqueFlag = true; doc.vscInitWhenReadyUniqueFlag1 = true;
if (isBlacklisted()) { if (isBlacklisted()) return;
return; if (doc === window.document && !window.vscPageLoadListenerUniqueFlag1) {
}
log(
`initializeWhenReady for: ${doc.location ? doc.location.href : "iframe"}. ReadyState: ${doc.readyState}`,
5
);
if (doc === window.document && !window.vscPageLoadListenerFullUniqueFlag) {
window.addEventListener("load", () => initializeNow(window.document), { window.addEventListener("load", () => initializeNow(window.document), {
once: true once: true
}); });
window.vscPageLoadListenerFullUniqueFlag = true; window.vscPageLoadListenerUniqueFlag1 = true;
} }
if (doc.readyState === "complete") initializeNow(doc);
else if (!doc.vscReadyStateListenerUniqueFlag1) {
doc.addEventListener("readystatechange", function onRSChangeUnique1() {
if (doc.readyState === "complete") { if (doc.readyState === "complete") {
initializeNow(doc); doc.removeEventListener("readystatechange", onRSChangeUnique1);
} else {
if (!doc.vscReadyStateListenerFullUniqueFlag) {
doc.addEventListener(
"readystatechange",
function onRSChange_VSC_Final_Unique_CB() {
if (doc.readyState === "complete") {
doc.removeEventListener(
"readystatechange",
onRSChange_VSC_Final_Unique_CB
);
initializeNow(doc); initializeNow(doc);
} }
} });
); doc.vscReadyStateListenerUniqueFlag1 = true;
doc.vscReadyStateListenerFullUniqueFlag = true;
}
} }
} }
function inIframe() { function inIframe() {
try { /* ... Same ... */ try {
return window.self !== window.top; return window.self !== window.top;
} catch (e) { } catch (e) {
return true; return true;
} }
} }
function getShadow(parent) { function getShadow(parent) {
let result = []; /* ... Same ... */ let r = [];
function getChild(p) { function gC(p) {
if (p.firstElementChild) { if (p.firstElementChild) {
var c = p.firstElementChild; var c = p.firstElementChild;
do { do {
result.push(c); r.push(c);
getChild(c); gC(c);
if (c.shadowRoot) result.push(...getShadow(c.shadowRoot)); if (c.shadowRoot) r.push(...getShadow(c.shadowRoot));
c = c.nextElementSibling; c = c.nextElementSibling;
} while (c); } while (c);
} }
} }
getChild(parent); gC(parent);
return result; return r;
} }
function initializeNow(doc) { function initializeNow(doc) {
/* ... Same robust init, ensuring tc.videoController is defined ... */
if (vscInitializedDocuments.has(doc) || !doc.body) return; if (vscInitializedDocuments.has(doc) || !doc.body) return;
log( if (!tc.settings.enabled) return;
`initializeNow for doc: ${doc.location ? doc.location.href : "iframe"}`,
4
);
if (!tc.settings.enabled) {
log("VSC is disabled.", 4);
return;
}
if (!doc.body.classList.contains("vsc-initialized")) if (!doc.body.classList.contains("vsc-initialized"))
doc.body.classList.add("vsc-initialized"); doc.body.classList.add("vsc-initialized");
if (typeof tc.videoController === "undefined") defineVideoController(); if (typeof tc.videoController === "undefined") defineVideoController();
@@ -754,78 +596,69 @@ function initializeNow(doc) {
doc !== window.top.document && doc !== window.top.document &&
!doc.head.querySelector('link[href*="inject.css"]') !doc.head.querySelector('link[href*="inject.css"]')
) { ) {
var link = doc.createElement("link"); var l = doc.createElement("link");
link.href = chrome.runtime.getURL("inject.css"); l.href = chrome.runtime.getURL("inject.css");
link.type = "text/css"; l.type = "text/css";
link.rel = "stylesheet"; l.rel = "stylesheet";
doc.head.appendChild(link); doc.head.appendChild(l);
} }
const docsForKeydown = new Set([doc]); const dFK = new Set([doc]);
try { try {
if (inIframe() && window.top.document) if (inIframe() && window.top.document) dFK.add(window.top.document);
docsForKeydown.add(window.top.document);
} catch (e) {} } catch (e) {}
docsForKeydown.forEach((lDoc) => { dFK.forEach((lD) => {
if (!lDoc.vscKeydownListenerUniqueFlagB) { if (!lD.vscKDLFlagC) {
// Different flag lD.addEventListener(
lDoc.addEventListener(
"keydown", "keydown",
function (event) { function (evt) {
if (!tc.settings.enabled) return; if (!tc.settings.enabled) return;
const target = event.target; const tgt = evt.target;
if ( if (
target.nodeName === "INPUT" || tgt.nodeName === "INPUT" ||
target.nodeName === "TEXTAREA" || tgt.nodeName === "TEXTAREA" ||
target.isContentEditable tgt.isContentEditable
) )
return; return;
if ( if (
event.getModifierState && evt.getModifierState &&
(event.getModifierState("Alt") || (evt.getModifierState("Alt") ||
event.getModifierState("Control") || evt.getModifierState("Control") ||
event.getModifierState("Meta") || evt.getModifierState("Meta") ||
event.getModifierState("Fn") || evt.getModifierState("Fn") ||
event.getModifierState("Hyper") || evt.getModifierState("Hyper") ||
event.getModifierState("OS")) evt.getModifierState("OS"))
) )
return; return;
if ( if (tc.mediaElements.length === 0 && !lD.querySelector("video,audio"))
tc.mediaElements.length === 0 &&
!lDoc.querySelector("video,audio")
)
return; return;
var item = tc.settings.keyBindings.find( var itm = tc.settings.keyBindings.find((k) => k.key === evt.keyCode);
(kb) => kb.key === event.keyCode if (itm) {
); runAction(itm.action, itm.value, evt);
if (item) { if (itm.force === "true" || itm.force === true) {
runAction(item.action, item.value, event); evt.preventDefault();
if (item.force === "true" || item.force === true) { evt.stopPropagation();
event.preventDefault();
event.stopPropagation();
} }
} }
}, },
true true
); );
lDoc.vscKeydownListenerUniqueFlagB = true; lD.vscKDLFlagC = true;
} }
}); });
if (!doc.vscMutationObserverUniqueFlagB) { if (!doc.vscMOFlagC) {
// Different flag const o = new MutationObserver((m) => {
const obs = new MutationObserver((muts) => {
if (typeof requestIdleCallback === "function") if (typeof requestIdleCallback === "function")
requestIdleCallback(() => processMutations(muts), { timeout: 1000 }); requestIdleCallback(() => pM(m), { timeout: 1000 });
else setTimeout(() => processMutations(muts), 200); else setTimeout(() => pM(m), 200);
}); });
function processMutations(mList) { function pM(ml) {
for (const m of mList) { for (const m of ml) {
if (m.type === "childList") { if (m.type === "childList") {
m.addedNodes.forEach((n) => { m.addedNodes.forEach((n) => {
if (n instanceof Element) chkVid(n, n.parentNode || m.target, true); if (n instanceof Element) cV(n, n.parentNode || m.target, true);
}); });
m.removedNodes.forEach((n) => { m.removedNodes.forEach((n) => {
if (n instanceof Element) if (n instanceof Element) cV(n, n.parentNode || m.target, false);
chkVid(n, n.parentNode || m.target, false);
}); });
} else if ( } else if (
m.type === "attributes" && m.type === "attributes" &&
@@ -833,59 +666,59 @@ function initializeNow(doc) {
m.target instanceof Element && m.target instanceof Element &&
m.target.getAttribute("aria-hidden") === "false" m.target.getAttribute("aria-hidden") === "false"
) { ) {
const vidsInTgt = Array.from(getShadow(m.target)).filter( const vIT = Array.from(getShadow(m.target)).filter(
(el) => el.tagName === "VIDEO" (el) => el.tagName === "VIDEO"
); );
vidsInTgt.forEach((vEl) => { vIT.forEach((vE) => {
if (!vEl.vsc) chkVid(vEl, vEl.parentNode || m.target, true); if (!vE.vsc) cV(vE, vE.parentNode || m.target, true);
}); });
} }
} }
} }
function chkVid(n, p, add) { function cV(n, p, a) {
if (!add && !n.isConnected) { if (!a && !n.isConnected) {
} else if (!add && n.isConnected) return; } else if (!a && n.isConnected) return;
if ( if (
n.nodeName === "VIDEO" || n.nodeName === "VIDEO" ||
(n.nodeName === "AUDIO" && tc.settings.audioBoolean) (n.nodeName === "AUDIO" && tc.settings.audioBoolean)
) { ) {
if (add) { if (a) {
if (!n.vsc) new tc.videoController(n, p); if (!n.vsc) new tc.videoController(n, p);
} else { } else {
if (n.vsc) n.vsc.remove(); if (n.vsc) n.vsc.remove();
} }
} else if (n.children && n.children.length > 0) { } else if (n.children && n.children.length > 0) {
for (let i = 0; i < n.children.length; i++) for (let i = 0; i < n.children.length; i++)
chkVid(n.children[i], n.children[i].parentNode || p, add); cV(n.children[i], n.children[i].parentNode || p, a);
} }
} }
obs.observe(doc, { o.observe(doc, {
childList: true, childList: true,
subtree: true, subtree: true,
attributes: true, attributes: true,
attributeFilter: ["aria-hidden"] attributeFilter: ["aria-hidden"]
}); });
doc.vscMutationObserverUniqueFlagB = true; doc.vscMOFlagC = true;
} }
const q = tc.settings.audioBoolean ? "video,audio" : "video"; const q = tc.settings.audioBoolean ? "video,audio" : "video";
doc.querySelectorAll(q).forEach((vid) => { doc.querySelectorAll(q).forEach((v) => {
if (!vid.vsc) new tc.videoController(vid, vid.parentElement); if (!v.vsc) new tc.videoController(v, v.parentElement);
}); });
Array.from(doc.getElementsByTagName("iframe")).forEach((fr) => { Array.from(doc.getElementsByTagName("iframe")).forEach((f) => {
try { try {
if (fr.contentDocument) initializeWhenReady(fr.contentDocument); if (f.contentDocument) initializeWhenReady(f.contentDocument);
} catch (e) {} } catch (e) {}
}); });
vscInitializedDocuments.add(doc); vscInitializedDocuments.add(doc);
} }
// MODIFIED setSpeed to accept `isInitialCall` and use it for `fromUserInput` // MODIFIED: setSpeed now takes `isInitialCall` and `isUserKeyPress`
function setSpeed(video, speed, isInitialCall = false) { function setSpeed(video, speed, isInitialCall = false, isUserKeyPress = false) {
const numericSpeed = Number(speed); const numericSpeed = Number(speed);
if (isNaN(numericSpeed) || numericSpeed <= 0 || numericSpeed > 16) return; if (isNaN(numericSpeed) || numericSpeed <= 0 || numericSpeed > 16) return;
if (!video || !video.vsc || !video.vsc.speedIndicator) return; if (!video || !video.vsc || !video.vsc.speedIndicator) return;
log( log(
`setSpeed: Target ${numericSpeed.toFixed(2)}. Initial: ${isInitialCall}`, `setSpeed: Target ${numericSpeed.toFixed(2)}. Initial: ${isInitialCall}. UserKeyPress: ${isUserKeyPress}`,
4 4
); );
@@ -895,53 +728,56 @@ function setSpeed(video, speed, isInitialCall = false) {
if (tc.settings.forceLastSavedSpeed) { if (tc.settings.forceLastSavedSpeed) {
video.dispatchEvent( video.dispatchEvent(
new CustomEvent("ratechange", { new CustomEvent("ratechange", {
// Pass `isUserKeyPress` as `fromUserInput` for the custom event
detail: { detail: {
origin: "videoSpeed", origin: "videoSpeed",
speed: numericSpeed.toFixed(2), speed: numericSpeed.toFixed(2),
fromUserInput: !isInitialCall fromUserInput: isUserKeyPress
} }
}) })
); );
} else { } else {
if (Math.abs(video.playbackRate - numericSpeed) > 0.001) { if (Math.abs(video.playbackRate - numericSpeed) > 0.001) {
// Before changing rate, set a flag that this is VSC initiated for non-forced mode
video.vscIsSettingRate = !isInitialCall; // True if user action, false if initial
video.playbackRate = numericSpeed; video.playbackRate = numericSpeed;
// Flag will be cleared by ratechange listener after processing
} }
} }
if (!isInitialCall) refreshCoolDown();
if (!isInitialCall) refreshCoolDown(); // Original call, only for non-initial sets
// MODIFIED: Directly trigger blink here if it's a user key press and not initial setup
if (isUserKeyPress && !isInitialCall && video.vsc) {
runAction("blink", getKeyBindings("blink", "value") || 1000, null, video);
}
if (video.vsc) { if (video.vsc) {
if (numericSpeed === 1.0 || video.paused) video.vsc.stopSubtitleNudge(); if (numericSpeed === 1.0 || video.paused) video.vsc.stopSubtitleNudge();
else video.vsc.startSubtitleNudge(); else video.vsc.startSubtitleNudge();
} }
} }
// MODIFIED runAction to pass 4th arg `specificVideo` to some internal calls if needed. // MODIFIED: runAction passes `isUserKeyPress=true` to setSpeed for relevant actions
// And pass `isInitialCall=false` to setSpeed calls.
function runAction(action, value, e, specificVideo = null) { function runAction(action, value, e, specificVideo = null) {
// ... (robust mediaTagsToProcess logic from previous correct version) ... var mediaTagsToProcess = []; // ... (same robust mediaTagsToProcess logic as before) ...
var mediaTagsToProcess = [];
if (specificVideo) mediaTagsToProcess = [specificVideo]; if (specificVideo) mediaTagsToProcess = [specificVideo];
else if (e && e.target) { else if (e && e.target) {
const docContext = e.target.ownerDocument || document; const dC = e.target.ownerDocument || document;
let activeVideo = tc.mediaElements.find( let aV = tc.mediaElements.find(
(v) => (v) =>
v.ownerDocument === docContext && v.ownerDocument === dC &&
(docContext.activeElement === v || v.contains(docContext.activeElement)) (dC.activeElement === v || v.contains(dC.activeElement))
); );
if (activeVideo) mediaTagsToProcess = [activeVideo]; if (aV) mediaTagsToProcess = [aV];
else { else {
activeVideo = tc.mediaElements.find( aV = tc.mediaElements.find(
(v) => (v) =>
v.ownerDocument === docContext && v.ownerDocument === dC &&
v.offsetParent !== null && v.offsetParent !== null &&
(!v.paused || v.readyState > 0) (!v.paused || v.readyState > 0)
); );
if (activeVideo) mediaTagsToProcess = [activeVideo]; if (aV) mediaTagsToProcess = [aV];
else { else {
mediaTagsToProcess = tc.mediaElements.filter( mediaTagsToProcess = tc.mediaElements.filter(
(v) => v.ownerDocument === docContext (v) => v.ownerDocument === dC
); );
if (mediaTagsToProcess.length === 0 && tc.mediaElements.length > 0) if (mediaTagsToProcess.length === 0 && tc.mediaElements.length > 0)
mediaTagsToProcess = [tc.mediaElements[0]]; mediaTagsToProcess = [tc.mediaElements[0]];
@@ -955,8 +791,7 @@ function runAction(action, value, e, specificVideo = null) {
e && e.target && e.target.getRootNode && e.target.getRootNode().host e && e.target && e.target.getRootNode && e.target.getRootNode().host
? e.target.getRootNode().host ? e.target.getRootNode().host
: null; : null;
var originalActionForResetContext = actionBeingProcessedForReset_ctx; // Use a local context var const currentActionContext = action;
actionBeingProcessedForReset_ctx = action;
mediaTagsToProcess.forEach(function (v) { mediaTagsToProcess.forEach(function (v) {
if (!v || !v.vsc || !v.vsc.div || !v.vsc.speedIndicator) return; if (!v || !v.vsc || !v.vsc.div || !v.vsc.speedIndicator) return;
@@ -969,6 +804,7 @@ function runAction(action, value, e, specificVideo = null) {
return; return;
if (action === "blink" && specificVideo && v !== specificVideo) return; if (action === "blink" && specificVideo && v !== specificVideo) return;
// Original showController logic (not tied to `isUserKeyPress` here, runAction("blink") is separate)
const userDrivenActionsThatShowController = [ const userDrivenActionsThatShowController = [
"rewind", "rewind",
"advance", "advance",
@@ -995,6 +831,7 @@ function runAction(action, value, e, specificVideo = null) {
case "advance": case "advance":
v.currentTime += numValue; v.currentTime += numValue;
break; break;
// MODIFIED: Pass `isUserKeyPress = true`
case "faster": case "faster":
setSpeed( setSpeed(
v, v,
@@ -1002,24 +839,24 @@ function runAction(action, value, e, specificVideo = null) {
(v.playbackRate < 0.07 ? 0.07 : v.playbackRate) + numValue, (v.playbackRate < 0.07 ? 0.07 : v.playbackRate) + numValue,
16 16
), ),
false false,
true
); );
break; break;
case "slower": case "slower":
setSpeed(v, Math.max(v.playbackRate - numValue, 0.07), false); setSpeed(v, Math.max(v.playbackRate - numValue, 0.07), false, true);
break; break;
// MODIFIED: Calls new resetSpeed directly
case "reset": case "reset":
resetSpeedSimple(v, 1.0); resetSpeed(v, 1.0, currentActionContext);
break; break;
case "fast": case "fast":
resetSpeedSimple(v, numValue); resetSpeed(v, numValue, currentActionContext);
break; break;
case "display": case "display":
controllerDiv.classList.add("vsc-manual"); controllerDiv.classList.add("vsc-manual");
controllerDiv.classList.toggle("vsc-hidden"); controllerDiv.classList.toggle("vsc-hidden");
break; break;
case "blink": case "blink": // This action is now mostly called by setSpeed itself for user key presses
if ( if (
controllerDiv.classList.contains("vsc-hidden") || controllerDiv.classList.contains("vsc-hidden") ||
controllerDiv.blinkTimeOut !== undefined controllerDiv.blinkTimeOut !== undefined
@@ -1060,103 +897,99 @@ function runAction(action, value, e, specificVideo = null) {
log(`Unknown action: ${action}`, 3); log(`Unknown action: ${action}`, 3);
} }
}); });
actionBeingProcessedForReset_ctx = originalActionForResetContext;
} }
var actionBeingProcessedForReset_ctx = null; // Context for original resetSpeed
function pause(v) { function pause(v) {
if (v.paused) v.play().catch((e) => log(`Play err:${e.message}`, 2)); /* ... Same as your original ... */ if (v.paused)
v.play().catch((e) => log(`Play err:${e.message}`, 2));
else v.pause(); else v.pause();
} }
// MODIFIED: New simpler resetSpeed function // MODIFIED: `resetSpeed` now calls `setSpeed` with `isUserKeyPress = true`
function resetSpeedSimple(v, targetActionSpeed) { function resetSpeed(v, target, currentActionContext = null) {
log( log(
`resetSpeedSimple: Video current: ${v.playbackRate.toFixed(2)}, Target for this action: ${targetActionSpeed.toFixed(2)}`, `resetSpeed (original): Video current: ${v.playbackRate.toFixed(2)}, Target: ${target.toFixed(2)}, Context: ${currentActionContext}`,
4 4
); );
const fastPreferredSpeed = getKeyBindings("fast", "value") || 1.8; if (Math.abs(v.playbackRate - target) < 0.01) {
if (v.playbackRate === (getKeyBindings("reset", "value") || 1.0)) {
if (targetActionSpeed === 1.0) { if (target !== 1.0) {
// Action was "reset" (R key) setSpeed(v, 1.0, false, true); // isInitial=false, isUserKeyPress=true
if (Math.abs(v.playbackRate - 1.0) < 0.01) {
setSpeed(v, fastPreferredSpeed, false);
} else { } else {
setSpeed(v, 1.0, false); setSpeed(v, getKeyBindings("fast", "value"), false, true); // isInitial=false, isUserKeyPress=true
} }
} else { } else {
// Action was "fast" (G key), targetActionSpeed is the preferred speed setSpeed(v, getKeyBindings("reset", "value") || 1.0, false, true); // isInitial=false, isUserKeyPress=true
if (Math.abs(v.playbackRate - targetActionSpeed) < 0.01) { }
setSpeed(v, 1.0, false);
} else { } else {
setSpeed(v, targetActionSpeed, false); if (currentActionContext === "reset") {
setKeyBindings("reset", v.playbackRate);
}
setSpeed(v, target, false, true); // isInitial=false, isUserKeyPress=true
} }
} }
}
// Remove or comment out the old `resetSpeed` function that uses setKeyBindings and actionBeingProcessedForReset_global
/*
function resetSpeed(v, target) { // THIS IS THE OLD ONE TO BE REPLACED by resetSpeedSimple
// ... original complex logic ...
}
*/
function muted(v) { function muted(v) {
v.muted = !v.muted; /* ... Same as your original ... */ v.muted = !v.muted;
log(`Mute: ${v.muted}`, 5); log(`Mute: ${v.muted}`, 5);
} }
function setMark(v) { function setMark(v) {
if (!v.vsc) v.vsc = {}; /* ... Same as your original ... */ if (!v.vsc) v.vsc = {};
v.vsc.mark = v.currentTime; v.vsc.mark = v.currentTime;
log(`Mark: ${v.vsc.mark.toFixed(2)}`, 5); log(`Mark: ${v.vsc.mark.toFixed(2)}`, 5);
} }
function jumpToMark(v) { function jumpToMark(v) {
if (v.vsc && typeof v.vsc.mark === "number") v.currentTime = v.vsc.mark; /* ... Same as your original ... */ if (
v.vsc &&
typeof v.vsc.mark === "number"
)
v.currentTime = v.vsc.mark;
else log("No mark.", 4); else log("No mark.", 4);
} }
function handleDrag(video, e) { function handleDrag(video, e) {
/* ... same original ... */ /* ... Same as your original ... */
if (!video || !video.vsc || !video.vsc.div || !video.vsc.div.shadowRoot) if (!video || !video.vsc || !video.vsc.div || !video.vsc.div.shadowRoot)
return; return;
const controller = video.vsc.div; const ctl = video.vsc.div;
const shadowController = controller.shadowRoot.querySelector("#controller"); const sCtl = ctl.shadowRoot.querySelector("#controller");
if (!shadowController) return; if (!sCtl) return;
var parentElement = controller.parentElement; var pE = ctl.parentElement;
while ( while (
parentElement && pE &&
parentElement.parentNode && pE.parentNode &&
parentElement.parentNode !== document && pE.parentNode !== document &&
parentElement.parentNode.offsetHeight === parentElement.offsetHeight && pE.parentNode.offsetHeight === pE.offsetHeight &&
parentElement.parentNode.offsetWidth === parentElement.offsetWidth pE.parentNode.offsetWidth === pE.offsetWidth
) )
parentElement = parentElement.parentNode; pE = pE.parentNode;
const dragBoundary = parentElement || video.ownerDocument.body; const dB = pE || video.ownerDocument.body;
video.classList.add("vcs-dragging"); video.classList.add("vcs-dragging");
shadowController.classList.add("dragging"); sCtl.classList.add("dragging");
const iXY = [e.clientX, e.clientY], const iXY = [e.clientX, e.clientY],
iCtrlXY = [ iCtlXY = [
parseInt(shadowController.style.left, 10) || 0, parseInt(sCtl.style.left, 10) || 0,
parseInt(shadowController.style.top, 10) || 0 parseInt(sCtl.style.top, 10) || 0
]; ];
const sD = (mvE) => { const sD = (mE) => {
let s = shadowController.style; let s = sCtl.style;
s.left = iCtrlXY[0] + mvE.clientX - iXY[0] + "px"; s.left = iCtlXY[0] + mE.clientX - iXY[0] + "px";
s.top = iCtrlXY[1] + mvE.clientY - iXY[1] + "px"; s.top = iCtlXY[1] + mE.clientY - iXY[1] + "px";
mvE.preventDefault(); mE.preventDefault();
}; };
const eD = () => { const eD = () => {
dragBoundary.removeEventListener("mousemove", sD); dB.removeEventListener("mousemove", sD);
dragBoundary.removeEventListener("mouseup", eD); dB.removeEventListener("mouseup", eD);
dragBoundary.removeEventListener("mouseleave", eD); dB.removeEventListener("mouseleave", eD);
shadowController.classList.remove("dragging"); sCtl.classList.remove("dragging");
video.classList.remove("vcs-dragging"); video.classList.remove("vcs-dragging");
}; };
dragBoundary.addEventListener("mousemove", sD); dB.addEventListener("mousemove", sD);
dragBoundary.addEventListener("mouseup", eD); dB.addEventListener("mouseup", eD);
dragBoundary.addEventListener("mouseleave", eD); dB.addEventListener("mouseleave", eD);
} }
var timer = null; var timer = null;
function showController(controller) { function showController(controller) {
/* ... same original ... */ /* ... Same as your original ... */
if (!controller || typeof controller.classList === "undefined") return; if (!controller || typeof controller.classList === "undefined") return;
controller.classList.add("vcs-show"); controller.classList.add("vcs-show");
if (timer) clearTimeout(timer); if (timer) clearTimeout(timer);

View File

@@ -1,13 +1,13 @@
{ {
"name": "Video Speed Controller", "name": "Video Speed Controller",
"short_name": "videospeed", "short_name": "videospeed",
"version": "0.6.3.3", "version": "1.1.1",
"manifest_version": 2, "manifest_version": 2,
"description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts", "description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts",
"homepage_url": "https://github.com/codebicycle/videospeed", "homepage_url": "https://github.com/SoPat712/videospeed",
"browser_specific_settings": { "browser_specific_settings": {
"gecko": { "gecko": {
"id": "{7be2ba16-0f1e-4d93-9ebc-5164397477a9}" "id": "{ed860648-f54f-4dc9-9a0d-501aec4313f5}"
} }
}, },
"icons": { "icons": {

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<title>Video Speed Controller: Options</title> <title>Video Speed Controller: Options</title>
@@ -148,8 +148,13 @@
<input id="rememberSpeed" type="checkbox" /> <input id="rememberSpeed" type="checkbox" />
</div> </div>
<div class="row"> <div class="row">
<label for="forceLastSavedSpeed">Force last saved speed<br /> <label for="forceLastSavedSpeed"
<em>Useful for video players that override the speeds set by VideoSpeed</em></label> >Force last saved speed<br />
<em
>Useful for video players that override the speeds set by
VideoSpeed</em
></label
>
<input id="forceLastSavedSpeed" type="checkbox" /> <input id="forceLastSavedSpeed" type="checkbox" />
</div> </div>
<div class="row"> <div class="row">
@@ -175,6 +180,47 @@
</div> </div>
</section> </section>
<section id="nudgeSettings">
<h3>Subtitle Nudge Settings (Experimental - YouTube Only)</h3>
<div class="row">
<label for="enableSubtitleNudge"
>Enable Subtitle Nudge <br /><em
>Periodically 'nudges' video speed by a tiny amount to help keep
subtitles in sync on some sites (e.g. YouTube).</em
>
</label>
<input id="enableSubtitleNudge" type="checkbox" />
</div>
<div class="row">
<label for="subtitleNudgeInterval"
>Nudge Interval (milliseconds) <br /><em
>How often to nudge (e.g., 25-1000). Smaller values are more
frequent. Default: 25.</em
>
</label>
<input
id="subtitleNudgeInterval"
type="text"
value=""
placeholder="25"
/>
</div>
<div class="row">
<label for="subtitleNudgeAmount"
>Nudge Amount (decimal) <br /><em
>How much to change speed by (e.g., 0.001). Very small values
recommended. Default: 0.001.</em
>
</label>
<input
id="subtitleNudgeAmount"
type="text"
value=""
placeholder="0.001"
/>
</div>
</section>
<button id="save">Save</button> <button id="save">Save</button>
<button id="restore">Restore Defaults</button> <button id="restore">Restore Defaults</button>
<button id="experimental">Show Experimental Features</button> <button id="experimental">Show Experimental Features</button>
@@ -186,12 +232,12 @@
<h4>Extension controls not appearing?</h4> <h4>Extension controls not appearing?</h4>
<p> <p>
This extension is only compatible with HTML5 audio and video. If you don't This extension is only compatible with HTML5 audio and video. If you
see the controls showing up, chances are you are viewing a Flash content. don't see the controls showing up, chances are you are viewing a Flash
If you want to confirm, try right-clicking on the content and inspect the content. If you want to confirm, try right-clicking on the content and
menu: if it mentions flash, then that's the issue. That said, <b>most sites inspect the menu: if it mentions flash, then that's the issue. That
will fallback to HTML5</b> if they detect that Flash is not available. You said, <b>most sites will fallback to HTML5</b> if they detect that Flash
can try manually disabling Flash from the browser. is not available. You can try manually disabling Flash from the browser.
</p> </p>
</div> </div>
</body> </body>

View File

@@ -22,13 +22,17 @@ var tcDefaults = {
twitter.com twitter.com
imgur.com imgur.com
teams.microsoft.com teams.microsoft.com
`.replace(regStrip, "") `.replace(regStrip, ""),
// ADDED: Nudge defaults
enableSubtitleNudge: true,
subtitleNudgeInterval: 25,
subtitleNudgeAmount: 0.001
}; };
var keyBindings = []; var keyBindings = []; // This is populated during save/restore
var keyCodeAliases = { var keyCodeAliases = {
0: "null", /* ... same as your original ... */ 0: "null",
null: "null", null: "null",
undefined: "null", undefined: "null",
32: "Space", 32: "Space",
@@ -76,83 +80,53 @@ var keyCodeAliases = {
222: "'", 222: "'",
59: ";", 59: ";",
61: "+", 61: "+",
173: "-", 173: "-"
}; };
function recordKeyPress(e) { function recordKeyPress(e) {
/* ... same as your original ... */
if ( if (
(e.keyCode >= 48 && e.keyCode <= 57) || // Numbers 0-9 (e.keyCode >= 48 && e.keyCode <= 57) ||
(e.keyCode >= 65 && e.keyCode <= 90) || // Letters A-Z (e.keyCode >= 65 && e.keyCode <= 90) ||
keyCodeAliases[e.keyCode] // Other character keys keyCodeAliases[e.keyCode]
) { ) {
e.target.value = e.target.value =
keyCodeAliases[e.keyCode] || String.fromCharCode(e.keyCode); keyCodeAliases[e.keyCode] || String.fromCharCode(e.keyCode);
e.target.keyCode = e.keyCode; e.target.keyCode = e.keyCode;
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
} else if (e.keyCode === 8) { } else if (e.keyCode === 8) {
// Clear input when backspace pressed
e.target.value = ""; e.target.value = "";
} else if (e.keyCode === 27) { } else if (e.keyCode === 27) {
// When esc clicked, clear input
e.target.value = "null"; e.target.value = "null";
e.target.keyCode = null; e.target.keyCode = null;
} }
} }
function inputFilterNumbersOnly(e) { function inputFilterNumbersOnly(e) {
/* ... same as your original ... */
var char = String.fromCharCode(e.keyCode); var char = String.fromCharCode(e.keyCode);
if (!/[\d\.]$/.test(char) || !/^\d+(\.\d*)?$/.test(e.target.value + char)) { if (!/[\d\.]$/.test(char) || !/^\d+(\.\d*)?$/.test(e.target.value + char)) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
} }
} }
function inputFocus(e) { function inputFocus(e) {
e.target.value = ""; /* ... same as your original ... */ e.target.value = "";
} }
function inputBlur(e) { function inputBlur(e) {
e.target.value = /* ... same as your original ... */ e.target.value =
keyCodeAliases[e.target.keyCode] || String.fromCharCode(e.target.keyCode); keyCodeAliases[e.target.keyCode] || String.fromCharCode(e.target.keyCode);
} }
// function updateShortcutInputText(inputId, keyCode) { /* ... same as your original ... */ } // Not directly used in provided options.js logic flow
function updateShortcutInputText(inputId, keyCode) {
document.getElementById(inputId).value =
keyCodeAliases[keyCode] || String.fromCharCode(keyCode);
document.getElementById(inputId).keyCode = keyCode;
}
function updateCustomShortcutInputText(inputItem, keyCode) { function updateCustomShortcutInputText(inputItem, keyCode) {
inputItem.value = keyCodeAliases[keyCode] || String.fromCharCode(keyCode); /* ... same as your original ... */ inputItem.value =
keyCodeAliases[keyCode] || String.fromCharCode(keyCode);
inputItem.keyCode = keyCode; inputItem.keyCode = keyCode;
} }
var customActionsNoValues = ["pause", "muted", "mark", "jump", "display"]; // Original
// List of custom actions for which customValue should be disabled
var customActionsNoValues = ["pause", "muted", "mark", "jump", "display"];
function add_shortcut() { function add_shortcut() {
var html = `<select class="customDo"> /* ... same as your original ... */
<option value="slower">Decrease speed</option> var html = `<select class="customDo"><option value="slower">Decrease speed</option><option value="faster">Increase speed</option><option value="rewind">Rewind</option><option value="advance">Advance</option><option value="reset">Reset speed</option><option value="fast">Preferred speed</option><option value="muted">Mute</option><option value="pause">Pause</option><option value="mark">Set marker</option><option value="jump">Jump to marker</option><option value="display">Show/hide controller</option></select><input class="customKey" type="text" placeholder="press a key"/><input class="customValue" type="text" placeholder="value (0.10)"/><select class="customForce"><option value="false">Do not disable website key bindings</option><option value="true">Disable website key bindings</option></select><button class="removeParent">X</button>`;
<option value="faster">Increase speed</option>
<option value="rewind">Rewind</option>
<option value="advance">Advance</option>
<option value="reset">Reset speed</option>
<option value="fast">Preferred speed</option>
<option value="muted">Mute</option>
<option value="pause">Pause</option>
<option value="mark">Set marker</option>
<option value="jump">Jump to marker</option>
<option value="display">Show/hide controller</option>
</select>
<input class="customKey" type="text" placeholder="press a key"/>
<input class="customValue" type="text" placeholder="value (0.10)"/>
<select class="customForce">
<option value="false">Do not disable website key bindings</option>
<option value="true">Disable website key bindings</option>
</select>
<button class="removeParent">X</button>`;
var div = document.createElement("div"); var div = document.createElement("div");
div.setAttribute("class", "row customs"); div.setAttribute("class", "row customs");
div.innerHTML = html; div.innerHTML = html;
@@ -162,14 +136,13 @@ function add_shortcut() {
customs_element.children[customs_element.childElementCount - 1] customs_element.children[customs_element.childElementCount - 1]
); );
} }
function createKeyBindings(item) { function createKeyBindings(item) {
/* ... same as your original ... */
const action = item.querySelector(".customDo").value; const action = item.querySelector(".customDo").value;
const key = item.querySelector(".customKey").keyCode; const key = item.querySelector(".customKey").keyCode;
const value = Number(item.querySelector(".customValue").value); const value = Number(item.querySelector(".customValue").value);
const force = item.querySelector(".customForce").value; const force = item.querySelector(".customForce").value;
const predefined = !!item.id; //item.id ? true : false; const predefined = !!item.id;
keyBindings.push({ keyBindings.push({
action: action, action: action,
key: key, key: key,
@@ -178,9 +151,8 @@ function createKeyBindings(item) {
predefined: predefined predefined: predefined
}); });
} }
// Validates settings before saving
function validate() { function validate() {
/* ... same as your original ... */
var valid = true; var valid = true;
var status = document.getElementById("status"); var status = document.getElementById("status");
document document
@@ -190,7 +162,7 @@ function validate() {
match = match.replace(regStrip, ""); match = match.replace(regStrip, "");
if (match.startsWith("/")) { if (match.startsWith("/")) {
try { try {
var regexp = new RegExp(match); new RegExp(match);
} catch (err) { } catch (err) {
status.textContent = status.textContent =
"Error: Invalid blacklist regex: " + match + ". Unable to save"; "Error: Invalid blacklist regex: " + match + ". Unable to save";
@@ -202,24 +174,45 @@ function validate() {
return valid; return valid;
} }
// Saves options to chrome.storage // MODIFIED: save_options to include nudge settings
function save_options() { function save_options() {
if (validate() === false) { if (validate() === false) return;
return;
} keyBindings = []; // Reset global keyBindings before populating from DOM
keyBindings = [];
Array.from(document.querySelectorAll(".customs")).forEach((item) => Array.from(document.querySelectorAll(".customs")).forEach((item) =>
createKeyBindings(item) createKeyBindings(item)
); // Remove added shortcuts );
var rememberSpeed = document.getElementById("rememberSpeed").checked; var s = {}; // Object to hold all settings to be saved
var forceLastSavedSpeed = document.getElementById("forceLastSavedSpeed").checked; s.rememberSpeed = document.getElementById("rememberSpeed").checked;
var audioBoolean = document.getElementById("audioBoolean").checked; s.forceLastSavedSpeed = document.getElementById(
var enabled = document.getElementById("enabled").checked; "forceLastSavedSpeed"
var startHidden = document.getElementById("startHidden").checked; ).checked;
var controllerOpacity = document.getElementById("controllerOpacity").value; s.audioBoolean = document.getElementById("audioBoolean").checked;
var blacklist = document.getElementById("blacklist").value; s.enabled = document.getElementById("enabled").checked;
s.startHidden = document.getElementById("startHidden").checked;
s.controllerOpacity = document.getElementById("controllerOpacity").value;
s.blacklist = document
.getElementById("blacklist")
.value.replace(regStrip, "");
s.keyBindings = keyBindings; // Use the populated global keyBindings
// ADDED: Save nudge settings
s.enableSubtitleNudge = document.getElementById(
"enableSubtitleNudge"
).checked;
s.subtitleNudgeInterval =
parseInt(document.getElementById("subtitleNudgeInterval").value, 10) ||
tcDefaults.subtitleNudgeInterval;
s.subtitleNudgeAmount =
parseFloat(document.getElementById("subtitleNudgeAmount").value) ||
tcDefaults.subtitleNudgeAmount;
// Basic validation for nudge interval and amount
if (s.subtitleNudgeInterval < 10) s.subtitleNudgeInterval = 10; // Min 10ms
if (s.subtitleNudgeAmount <= 0 || s.subtitleNudgeAmount > 0.1)
s.subtitleNudgeAmount = tcDefaults.subtitleNudgeAmount;
// Remove old flat settings (original logic)
chrome.storage.sync.remove([ chrome.storage.sync.remove([
"resetSpeed", "resetSpeed",
"speedStep", "speedStep",
@@ -233,33 +226,22 @@ function save_options() {
"advanceKeyCode", "advanceKeyCode",
"fastKeyCode" "fastKeyCode"
]); ]);
chrome.storage.sync.set(
{ chrome.storage.sync.set(s, function () {
rememberSpeed: rememberSpeed,
forceLastSavedSpeed: forceLastSavedSpeed,
audioBoolean: audioBoolean,
enabled: enabled,
startHidden: startHidden,
controllerOpacity: controllerOpacity,
keyBindings: keyBindings,
blacklist: blacklist.replace(regStrip, "")
},
function () {
// Update status to let user know options were saved.
var status = document.getElementById("status"); var status = document.getElementById("status");
status.textContent = "Options saved"; status.textContent = "Options saved";
setTimeout(function () { setTimeout(function () {
status.textContent = ""; status.textContent = "";
}, 1000); }, 1000);
} });
);
} }
// Restores options from chrome.storage // MODIFIED: restore_options to include nudge settings
function restore_options() { function restore_options() {
chrome.storage.sync.get(tcDefaults, function (storage) { chrome.storage.sync.get(tcDefaults, function (storage) {
document.getElementById("rememberSpeed").checked = storage.rememberSpeed; document.getElementById("rememberSpeed").checked = storage.rememberSpeed;
document.getElementById("forceLastSavedSpeed").checked = storage.forceLastSavedSpeed; document.getElementById("forceLastSavedSpeed").checked =
storage.forceLastSavedSpeed;
document.getElementById("audioBoolean").checked = storage.audioBoolean; document.getElementById("audioBoolean").checked = storage.audioBoolean;
document.getElementById("enabled").checked = storage.enabled; document.getElementById("enabled").checked = storage.enabled;
document.getElementById("startHidden").checked = storage.startHidden; document.getElementById("startHidden").checked = storage.startHidden;
@@ -267,65 +249,86 @@ function restore_options() {
storage.controllerOpacity; storage.controllerOpacity;
document.getElementById("blacklist").value = storage.blacklist; document.getElementById("blacklist").value = storage.blacklist;
// ensure that there is a "display" binding for upgrades from versions that had it as a separate binding // ADDED: Restore nudge settings
document.getElementById("enableSubtitleNudge").checked =
storage.enableSubtitleNudge;
document.getElementById("subtitleNudgeInterval").value =
storage.subtitleNudgeInterval;
document.getElementById("subtitleNudgeAmount").value =
storage.subtitleNudgeAmount;
// Original key binding restoration logic
if (
!Array.isArray(storage.keyBindings) ||
storage.keyBindings.length === 0
) {
// If keyBindings missing or not an array, use defaults from tcDefaults
storage.keyBindings = tcDefaults.keyBindings;
}
if (storage.keyBindings.filter((x) => x.action == "display").length == 0) { if (storage.keyBindings.filter((x) => x.action == "display").length == 0) {
storage.keyBindings.push({ storage.keyBindings.push({
action: "display", action: "display",
value: 0, value: 0,
force: false, force: false,
predefined: true predefined: true,
key: storage.displayKeyCode || tcDefaults.displayKeyCode
}); });
} }
// Clear existing dynamic shortcuts before restoring (if any were added by mistake)
const dynamicShortcuts = document.querySelectorAll(".customs:not([id])");
dynamicShortcuts.forEach((sc) => sc.remove());
for (let i in storage.keyBindings) { for (let i in storage.keyBindings) {
var item = storage.keyBindings[i]; var item = storage.keyBindings[i];
if (item.predefined) { if (item.predefined) {
//do predefined ones because their value needed for overlay
// document.querySelector("#" + item["action"] + " .customDo").value = item["action"];
if (item["action"] == "display" && typeof item["key"] === "undefined") { if (item["action"] == "display" && typeof item["key"] === "undefined") {
item["key"] = storage.displayKeyCode || tcDefaults.displayKeyCode; // V item["key"] = storage.displayKeyCode || tcDefaults.displayKeyCode;
} }
if (customActionsNoValues.includes(item["action"])) {
if (customActionsNoValues.includes(item["action"])) const el = document.querySelector(
document.querySelector(
"#" + item["action"] + " .customValue" "#" + item["action"] + " .customValue"
).disabled = true;
updateCustomShortcutInputText(
document.querySelector("#" + item["action"] + " .customKey"),
item["key"]
); );
document.querySelector("#" + item["action"] + " .customValue").value = if (el) el.disabled = true;
item["value"]; }
document.querySelector("#" + item["action"] + " .customForce").value = const keyEl = document.querySelector(
item["force"]; "#" + item["action"] + " .customKey"
);
const valEl = document.querySelector(
"#" + item["action"] + " .customValue"
);
const forceEl = document.querySelector(
"#" + item["action"] + " .customForce"
);
if (keyEl) updateCustomShortcutInputText(keyEl, item["key"]);
if (valEl) valEl.value = item["value"];
if (forceEl) forceEl.value = String(item["force"]); // Ensure string for select value
} else { } else {
// new ones // Non-predefined, dynamically added shortcuts
add_shortcut(); add_shortcut();
const dom = document.querySelector(".customs:last-of-type"); const dom = document.querySelector(".customs:last-of-type"); // Gets the newly added one
dom.querySelector(".customDo").value = item["action"]; dom.querySelector(".customDo").value = item["action"];
if (customActionsNoValues.includes(item["action"])) {
if (customActionsNoValues.includes(item["action"]))
dom.querySelector(".customValue").disabled = true; dom.querySelector(".customValue").disabled = true;
}
updateCustomShortcutInputText( updateCustomShortcutInputText(
dom.querySelector(".customKey"), dom.querySelector(".customKey"),
item["key"] item["key"]
); );
dom.querySelector(".customValue").value = item["value"]; dom.querySelector(".customValue").value = item["value"];
dom.querySelector(".customForce").value = item["force"]; dom.querySelector(".customForce").value = String(item["force"]);
} }
} }
}); });
} }
function restore_defaults() { function restore_defaults() {
/* ... same as your original, tcDefaults now includes nudge defaults ... */
// Remove all dynamically added shortcuts first
document.querySelectorAll(".customs:not([id])").forEach((el) => el.remove());
// Then set defaults and restore options, which will re-add predefined ones correctly
chrome.storage.sync.set(tcDefaults, function () { chrome.storage.sync.set(tcDefaults, function () {
restore_options(); restore_options(); // This will populate based on tcDefaults
document
.querySelectorAll(".removeParent")
.forEach((button) => button.click()); // Remove added shortcuts
// Update status to let user know options were saved.
var status = document.getElementById("status"); var status = document.getElementById("status");
status.textContent = "Default options restored"; status.textContent = "Default options restored";
setTimeout(function () { setTimeout(function () {
@@ -335,14 +338,15 @@ function restore_defaults() {
} }
function show_experimental() { function show_experimental() {
/* ... same as your original ... */
document document
.querySelectorAll(".customForce") .querySelectorAll(".customForce")
.forEach((item) => (item.style.display = "inline-block")); .forEach((item) => (item.style.display = "inline-block"));
} }
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
/* ... same as your original event listeners setup ... */
restore_options(); restore_options();
document.getElementById("save").addEventListener("click", save_options); document.getElementById("save").addEventListener("click", save_options);
document.getElementById("add").addEventListener("click", add_shortcut); document.getElementById("add").addEventListener("click", add_shortcut);
document document
@@ -353,34 +357,32 @@ document.addEventListener("DOMContentLoaded", function () {
.addEventListener("click", show_experimental); .addEventListener("click", show_experimental);
function eventCaller(event, className, funcName) { function eventCaller(event, className, funcName) {
if (!event.target.classList || !event.target.classList.contains(className)) { if (!event.target.classList || !event.target.classList.contains(className))
return; return;
}
funcName(event); funcName(event);
} }
document.addEventListener("keypress", (event) =>
document.addEventListener("keypress", (event) => { eventCaller(event, "customValue", inputFilterNumbersOnly)
eventCaller(event, "customValue", inputFilterNumbersOnly); );
}); document.addEventListener("focus", (event) =>
document.addEventListener("focus", (event) => { eventCaller(event, "customKey", inputFocus)
eventCaller(event, "customKey", inputFocus); );
}); document.addEventListener("blur", (event) =>
document.addEventListener("blur", (event) => { eventCaller(event, "customKey", inputBlur)
eventCaller(event, "customKey", inputBlur); );
}); document.addEventListener("keydown", (event) =>
document.addEventListener("keydown", (event) => { eventCaller(event, "customKey", recordKeyPress)
eventCaller(event, "customKey", recordKeyPress); );
}); document.addEventListener("click", (event) =>
document.addEventListener("click", (event) => {
eventCaller(event, "removeParent", function () { eventCaller(event, "removeParent", function () {
event.target.parentNode.remove(); event.target.parentNode.remove();
}); })
}); );
document.addEventListener("change", (event) => { document.addEventListener("change", (event) => {
eventCaller(event, "customDo", function () { eventCaller(event, "customDo", function () {
if (customActionsNoValues.includes(event.target.value)) { if (customActionsNoValues.includes(event.target.value)) {
event.target.nextElementSibling.nextElementSibling.disabled = true; event.target.nextElementSibling.nextElementSibling.disabled = true;
event.target.nextElementSibling.nextElementSibling.value = 0; event.target.nextElementSibling.nextElementSibling.value = 0; // Or "" if placeholder is preferred
} else { } else {
event.target.nextElementSibling.nextElementSibling.disabled = false; event.target.nextElementSibling.nextElementSibling.disabled = false;
} }

BIN
videospeed.xpi Normal file

Binary file not shown.