gitignore update

This commit is contained in:
Josh Patra
2025-09-25 18:12:01 -04:00
parent 01b9b576eb
commit 05a8adef80
3 changed files with 284 additions and 56 deletions

1
.gitignore vendored
View File

@@ -2,5 +2,6 @@
local local
# IntelliJ IDEA # IntelliJ IDEA
*.xpi
.idea/ .idea/
node_modules node_modules

271
inject.js
View File

@@ -186,6 +186,8 @@ function defineVideoController() {
this.parent = target.parentElement || parent; this.parent = target.parentElement || parent;
this.nudgeIntervalId = null; this.nudgeIntervalId = null;
log(`Creating video controller for ${target.tagName} with src: ${target.src || target.currentSrc || 'none'}`, 4);
// Determine what speed to use // Determine what speed to use
let storedSpeed = tc.settings.speeds[target.currentSrc]; let storedSpeed = tc.settings.speeds[target.currentSrc];
if (!tc.settings.rememberSpeed) { if (!tc.settings.rememberSpeed) {
@@ -209,6 +211,13 @@ function defineVideoController() {
this.div = this.initializeControls(); this.div = this.initializeControls();
if (!this.div) {
log("ERROR: Failed to create controller div!", 2);
return;
}
log(`Controller created and attached to DOM. Hidden: ${this.div.classList.contains('vsc-hidden')}`, 4);
// Make the controller visible for 5 seconds on startup // Make the controller visible for 5 seconds on startup
runAction("blink", 5000, null, this.video); runAction("blink", 5000, null, this.video);
@@ -471,17 +480,25 @@ function defineVideoController() {
var fragment = doc.createDocumentFragment(); var fragment = doc.createDocumentFragment();
fragment.appendChild(wrapper); fragment.appendChild(wrapper);
const parentEl = this.parent || this.video.parentElement; const parentEl = this.parent || this.video.parentElement;
log(`Inserting controller: parentEl=${!!parentEl}, parentNode=${!!parentEl?.parentNode}, hostname=${location.hostname}`, 4);
if (!parentEl || !parentEl.parentNode) { if (!parentEl || !parentEl.parentNode) {
log("No suitable parent found, appending to body", 4);
doc.body.appendChild(fragment); doc.body.appendChild(fragment);
return wrapper; return wrapper;
} }
try {
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):
log("Using parentElement.parentElement insertion", 5);
parentEl.parentElement.insertBefore(fragment, parentEl); parentEl.parentElement.insertBefore(fragment, parentEl);
break; break;
case location.hostname == "www.facebook.com": case location.hostname == "www.facebook.com":
log("Using Facebook-specific insertion", 5);
let p = let p =
parentEl.parentElement.parentElement.parentElement.parentElement parentEl.parentElement.parentElement.parentElement.parentElement
.parentElement.parentElement.parentElement; .parentElement.parentElement.parentElement;
@@ -489,14 +506,23 @@ function defineVideoController() {
else parentEl.insertBefore(fragment, parentEl.firstChild); else parentEl.insertBefore(fragment, parentEl.firstChild);
break; break;
case location.hostname == "tv.apple.com": case location.hostname == "tv.apple.com":
log("Using Apple TV-specific insertion", 5);
const r = parentEl.getRootNode(); const r = parentEl.getRootNode();
const s = r && r.querySelector ? r.querySelector(".scrim") : null; const s = r && r.querySelector ? r.querySelector(".scrim") : null;
if (s) s.prepend(fragment); if (s) s.prepend(fragment);
else parentEl.insertBefore(fragment, parentEl.firstChild); else parentEl.insertBefore(fragment, parentEl.firstChild);
break; break;
default: default:
log("Using default insertion method", 5);
parentEl.insertBefore(fragment, parentEl.firstChild); parentEl.insertBefore(fragment, parentEl.firstChild);
} }
log("Controller successfully inserted into DOM", 4);
} catch (error) {
log(`Error inserting controller: ${error.message}`, 2);
// Fallback to body insertion
doc.body.appendChild(fragment);
}
return wrapper; return wrapper;
}; };
} }
@@ -543,7 +569,7 @@ function setupListener() {
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 (fromUserInput) { if (fromUserInput) {
runAction("blink", 1000, null, video); runAction("blink", 1000, null, video);
} }
@@ -583,12 +609,18 @@ function setupListener() {
} }
var vscInitializedDocuments = new Set(); var vscInitializedDocuments = new Set();
function initializeWhenReady(doc) { function initializeWhenReady(doc, forceReinit = false) {
if (vscInitializedDocuments.has(doc) || !doc.body) return; if (!forceReinit && vscInitializedDocuments.has(doc) || !doc.body) return;
// For navigation changes, we want to re-scan even if already initialized
if (forceReinit) {
log("Force re-initialization requested", 4);
}
if (doc.readyState === "complete") { if (doc.readyState === "complete") {
initializeNow(doc); initializeNow(doc, forceReinit);
} else { } else {
doc.addEventListener("DOMContentLoaded", () => initializeNow(doc), { doc.addEventListener("DOMContentLoaded", () => initializeNow(doc, forceReinit), {
once: true once: true
}); });
} }
@@ -608,7 +640,17 @@ function getShadow(parent) {
do { do {
r.push(c); r.push(c);
gC(c); gC(c);
if (c.shadowRoot) r.push(...getShadow(c.shadowRoot)); if (c.shadowRoot) {
r.push(...getShadow(c.shadowRoot));
// Also check for videos in shadow DOM
const shadowVideos = c.shadowRoot.querySelectorAll(tc.settings.audioBoolean ? "video,audio" : "video");
shadowVideos.forEach(video => {
if (!video.vsc) {
log(`Found video in shadow DOM`, 5);
checkForVideo(video, video.parentElement, true);
}
});
}
c = c.nextElementSibling; c = c.nextElementSibling;
} while (c); } while (c);
} }
@@ -617,9 +659,10 @@ function getShadow(parent) {
return r; return r;
} }
function initializeNow(doc) { function initializeNow(doc, forceReinit = false) {
if (vscInitializedDocuments.has(doc) || !doc.body) return; if (!forceReinit && (vscInitializedDocuments.has(doc) || !doc.body)) return;
if (!tc.settings.enabled) return; if (!tc.settings.enabled) 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();
@@ -628,7 +671,7 @@ function initializeNow(doc) {
var docs = Array(doc); var docs = Array(doc);
try { try {
if (inIframe()) docs.push(window.top.document); if (inIframe()) docs.push(window.top.document);
} catch (e) {} } catch (e) { }
docs.forEach(function (d) { docs.forEach(function (d) {
if (d.vscKeydownListenerAttached) return; // Prevent duplicate listeners if (d.vscKeydownListenerAttached) return; // Prevent duplicate listeners
d.addEventListener( d.addEventListener(
@@ -688,9 +731,16 @@ function initializeNow(doc) {
}); });
break; break;
case "attributes": case "attributes":
if ( // Enhanced attribute monitoring for video detection
mutation.target.attributes["aria-hidden"] && const target = mutation.target;
mutation.target.attributes["aria-hidden"].value == "false" if (target.tagName === "VIDEO" || target.tagName === "AUDIO") {
// Video/audio element had attributes changed - check if it needs controller
if (!target.vsc && (target.src || target.currentSrc)) {
checkForVideo(target, target.parentNode, true);
}
} else if (
target.attributes["aria-hidden"] &&
target.attributes["aria-hidden"].value == "false"
) { ) {
var flattenedNodes = getShadow(document.body); var flattenedNodes = getShadow(document.body);
var node = flattenedNodes.filter( var node = flattenedNodes.filter(
@@ -719,9 +769,30 @@ function initializeNow(doc) {
(node.nodeName === "AUDIO" && tc.settings.audioBoolean) (node.nodeName === "AUDIO" && tc.settings.audioBoolean)
) { ) {
if (added) { if (added) {
if (!node.vsc) node.vsc = new tc.videoController(node, parent); if (!node.vsc) {
log(`Creating controller for ${node.tagName}: ${node.src || node.currentSrc || 'no src'}`, 4);
node.vsc = new tc.videoController(node, parent);
// Verify controller was created successfully
if (!node.vsc || !node.vsc.div) {
log(`ERROR: Controller creation failed for ${node.tagName}`, 2);
} else { } else {
if (node.vsc) node.vsc.remove(); log(`Controller created successfully, div in DOM: ${document.contains(node.vsc.div)}`, 4);
}
// Add to intersection observer if available
if (doc.vscVideoIntersectionObserver) {
doc.vscVideoIntersectionObserver.observe(node);
}
}
} else {
if (node.vsc) {
node.vsc.remove();
// Remove from intersection observer if available
if (doc.vscVideoIntersectionObserver) {
doc.vscVideoIntersectionObserver.unobserve(node);
}
}
} }
} else if (node.children != undefined) { } else if (node.children != undefined) {
for (var i = 0; i < node.children.length; i++) { for (var i = 0; i < node.children.length; i++) {
@@ -731,9 +802,10 @@ function initializeNow(doc) {
} }
} }
observer.observe(doc, { observer.observe(doc, {
attributeFilter: ["aria-hidden"], attributeFilter: ["aria-hidden", "src", "currentSrc", "style", "class"],
childList: true, childList: true,
subtree: true subtree: true,
attributes: true
}); });
doc.vscMutationObserverAttached = true; doc.vscMutationObserverAttached = true;
} }
@@ -744,6 +816,45 @@ function initializeNow(doc) {
if (!v.vsc) new tc.videoController(v, v.parentElement); if (!v.vsc) new tc.videoController(v, v.parentElement);
}); });
// Enhanced video detection via media events
if (!doc.vscMediaEventListenersAttached) {
const mediaEvents = ['loadstart', 'loadedmetadata', 'canplay', 'play'];
mediaEvents.forEach(eventType => {
doc.addEventListener(eventType, function (event) {
const target = event.target;
if ((target.tagName === 'VIDEO' || (target.tagName === 'AUDIO' && tc.settings.audioBoolean)) && !target.vsc) {
log(`Media event ${eventType} detected new ${target.tagName.toLowerCase()}`, 5);
checkForVideo(target, target.parentElement, true);
}
}, true);
});
doc.vscMediaEventListenersAttached = true;
}
// Intersection Observer for lazy-loaded videos
if (!doc.vscIntersectionObserverAttached && 'IntersectionObserver' in window) {
const videoIntersectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const target = entry.target;
if ((target.tagName === 'VIDEO' || (target.tagName === 'AUDIO' && tc.settings.audioBoolean)) && !target.vsc) {
log(`Intersection observer detected visible ${target.tagName.toLowerCase()}`, 5);
checkForVideo(target, target.parentElement, true);
}
}
});
}, { threshold: 0.1 });
// Observe existing videos that might not have been processed
doc.querySelectorAll(q).forEach(video => {
videoIntersectionObserver.observe(video);
});
// Store observer to add new videos to it
doc.vscVideoIntersectionObserver = videoIntersectionObserver;
doc.vscIntersectionObserverAttached = true;
}
Array.from(doc.getElementsByTagName("iframe")).forEach((f) => { Array.from(doc.getElementsByTagName("iframe")).forEach((f) => {
if (f.vscLoadListenerAttached) return; if (f.vscLoadListenerAttached) return;
f.addEventListener("load", () => { f.addEventListener("load", () => {
@@ -764,6 +875,115 @@ function initializeNow(doc) {
// Silently ignore CORS errors // Silently ignore CORS errors
} }
}); });
// Navigation change detection for SPAs
if (!doc.vscNavigationListenersAttached) {
let currentUrl = location.href;
const handleNavigation = (source) => {
if (location.href !== currentUrl) {
const oldUrl = currentUrl;
currentUrl = location.href;
log(`Navigation detected via ${source}: ${oldUrl} -> ${currentUrl}`, 4);
// Wait a bit for the new content to load, then force re-scan
setTimeout(() => {
const q = tc.settings.audioBoolean ? "video,audio" : "video";
const videos = document.querySelectorAll(q);
log(`Post-navigation scan found ${videos.length} videos`, 4);
videos.forEach(video => {
if (!video.vsc) {
log(`Adding controller to post-navigation video`, 4);
checkForVideo(video, video.parentElement, true);
}
});
}, 500); // Increased delay for content to load
// Also do a quicker scan
setTimeout(() => {
const q = tc.settings.audioBoolean ? "video,audio" : "video";
const videos = document.querySelectorAll(q);
videos.forEach(video => {
if (!video.vsc && (video.src || video.currentSrc)) {
checkForVideo(video, video.parentElement, true);
}
});
}, 100);
}
};
// Listen for popstate (back/forward navigation)
window.addEventListener('popstate', () => handleNavigation('popstate'));
// Override pushState and replaceState to catch programmatic navigation
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function (...args) {
originalPushState.apply(this, args);
handleNavigation('pushState');
};
history.replaceState = function (...args) {
originalReplaceState.apply(this, args);
handleNavigation('replaceState');
};
// Listen for hashchange as well
window.addEventListener('hashchange', () => handleNavigation('hashchange'));
// Also intercept fetch and XMLHttpRequest for AJAX-heavy sites
const originalFetch = window.fetch;
window.fetch = function (...args) {
return originalFetch.apply(this, args).then(response => {
// After any fetch completes, check for new videos
setTimeout(() => {
const q = tc.settings.audioBoolean ? "video,audio" : "video";
const videos = document.querySelectorAll(q);
videos.forEach(video => {
if (!video.vsc && (video.src || video.currentSrc || video.readyState > 0)) {
log(`Post-fetch scan found video`, 5);
checkForVideo(video, video.parentElement, true);
}
});
}, 200);
return response;
});
};
doc.vscNavigationListenersAttached = true;
}
// Periodic fallback scan for missed videos
if (!doc.vscPeriodicScanAttached) {
const periodicScan = () => {
const q = tc.settings.audioBoolean ? "video,audio" : "video";
const allVideos = doc.querySelectorAll(q);
let foundNew = false;
allVideos.forEach(video => {
if (!video.vsc && (video.src || video.currentSrc || video.readyState > 0)) {
log(`Periodic scan found missed ${video.tagName.toLowerCase()}`, 4);
checkForVideo(video, video.parentElement, true);
foundNew = true;
}
});
if (foundNew) {
log(`Periodic scan found ${foundNew} new videos`, 4);
}
};
// Run periodic scan every 3 seconds, but only if we have videos on the page
setInterval(() => {
if (tc.mediaElements.length > 0 || doc.querySelector(tc.settings.audioBoolean ? "video,audio" : "video")) {
periodicScan();
}
}, 3000);
doc.vscPeriodicScanAttached = true;
}
vscInitializedDocuments.add(doc); vscInitializedDocuments.add(doc);
} }
@@ -889,12 +1109,17 @@ function runAction(action, value, e) {
controller.classList.toggle("vsc-hidden"); controller.classList.toggle("vsc-hidden");
break; break;
case "blink": case "blink":
if ( log(`Blink action: controller hidden=${controller.classList.contains("vsc-hidden")}, timeout=${controller.blinkTimeOut !== undefined}, duration=${numValue}`, 5);
controller.classList.contains("vsc-hidden") ||
controller.blinkTimeOut !== undefined // Always clear existing timeout and show controller
) { if (controller.blinkTimeOut !== undefined) {
clearTimeout(controller.blinkTimeOut); clearTimeout(controller.blinkTimeOut);
}
// Always show the controller
controller.classList.remove("vsc-hidden"); controller.classList.remove("vsc-hidden");
log(`Controller shown, setting timeout for ${numValue || 1000}ms`, 5);
controller.blinkTimeOut = setTimeout(() => { controller.blinkTimeOut = setTimeout(() => {
if ( if (
!( !(
@@ -903,10 +1128,12 @@ function runAction(action, value, e) {
) )
) { ) {
controller.classList.add("vsc-hidden"); controller.classList.add("vsc-hidden");
log("Controller auto-hidden after blink timeout", 5);
} else {
log("Controller kept visible (manual mode)", 5);
} }
controller.blinkTimeOut = undefined; controller.blinkTimeOut = undefined;
}, numValue || 1000); // FIXED: Use numValue for consistency }, numValue || 1000);
}
break; break;
case "drag": case "drag":
if (e) handleDrag(v, e); if (e) handleDrag(v, e);

View File

@@ -1,7 +1,7 @@
{ {
"name": "Video Speed Controller", "name": "Video Speed Controller",
"short_name": "videospeed", "short_name": "videospeed",
"version": "1.6.1", "version": "2.0.0",
"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/SoPat712/videospeed", "homepage_url": "https://github.com/SoPat712/videospeed",