diff --git a/inject.js b/inject.js
index 79f4e583..5bbfa005 100644
--- a/inject.js
+++ b/inject.js
@@ -28,6 +28,199 @@ var tc = {
mediaElements: []
};
+// ===== Site Preference Helpers =====
+
+// Best-effort registrable domain extraction (eTLD+1). Not perfect without PSL, but solid for common TLDs.
+function getRegistrableDomain(hostname) {
+ try {
+ hostname = (hostname || location.hostname || "").toLowerCase();
+ // strip common mobile/www prefixes
+ hostname = hostname.replace(/^(www|m|amp)\./, "");
+ const parts = hostname.split(".");
+ if (parts.length >= 2) {
+ const tld = parts.slice(-1)[0];
+ const sld = parts.slice(-2)[0];
+ // crude guard for multi-part TLDs; fallback if needed
+ if (["co", "com", "org", "net", "gov", "edu"].includes(sld) && parts.length >= 3) {
+ return parts.slice(-3).join(".");
+ }
+ return parts.slice(-2).join(".");
+ }
+ return hostname;
+ } catch (_) {
+ return location.hostname || "unknown";
+ }
+}
+
+const SITE_PREFS_KEY = "sitePrefs";
+
+async function getSitePrefs() {
+ return new Promise((resolve) => {
+ chrome.storage.local.get(SITE_PREFS_KEY, (result) => {
+ resolve(result[SITE_PREFS_KEY] || {});
+ });
+ });
+}
+
+async function setSitePrefs(prefs) {
+ return new Promise((resolve) => {
+ chrome.storage.local.set({ [SITE_PREFS_KEY]: prefs }, resolve);
+ });
+}
+
+async function getDomainPref(domain) {
+ const prefs = await getSitePrefs();
+ return prefs[domain] || null;
+}
+
+async function saveDomainPref(domain, pref) {
+ const prefs = await getSitePrefs();
+ prefs[domain] = pref;
+ await setSitePrefs(prefs);
+}
+
+function getPreferredSpeedDefault() {
+ // reuse extension's preferred speed if available on window or settings; otherwise 1.6
+ try {
+ if (window.vsc && typeof window.vsc.getPreference === "function") {
+ const s = window.vsc.getPreference("preferredSpeed");
+ if (typeof s === "number" && s > 0) return s;
+ }
+ } catch (_) {}
+ return 1.6;
+}
+
+// Create a lightweight prompt in Shadow DOM
+function buildPromptUI({ domain, speed, onChoose }) {
+ const host = document.createElement("div");
+ host.id = "vsc-site-prompt-host";
+ host.style.position = "fixed";
+ host.style.top = "16px";
+ host.style.right = "16px";
+ host.style.zIndex = "2147483647"; // max-ish
+ host.style.width = "320px";
+ host.style.pointerEvents = "auto";
+
+ const shadow = host.attachShadow({ mode: "open" });
+ const wrapper = document.createElement("div");
+
+ wrapper.innerHTML = `
+
+
+
Use preferred speed on ${domain}?
+
Apply ${speed.toFixed(2)}× automatically for all videos on this site.
+
+
+
+
+
+
Shortcuts: Enter = Always, Esc = Never
+
+ `;
+
+ shadow.appendChild(wrapper);
+ document.documentElement.appendChild(host);
+
+ const cleanup = () => host.remove();
+
+ shadow.querySelectorAll("button").forEach(btn => {
+ btn.addEventListener("click", e => {
+ const action = e.currentTarget.getAttribute("data-action");
+ onChoose(action, cleanup);
+ });
+ });
+
+ const onKey = (e) => {
+ if (e.key === "Enter") { onChoose("always", cleanup); }
+ else if (e.key === "Escape") { onChoose("never", cleanup); }
+ };
+ shadow.addEventListener("keydown", onKey);
+ // ensure focus inside shadow so Enter works
+ shadow.querySelector("button[autofocus]")?.focus();
+
+ return { cleanup };
+}
+
+async function maybePromptAndApplyForSite(firstVideoEl) {
+ const domain = getRegistrableDomain(location.hostname);
+ const existing = await getDomainPref(domain);
+ const preferredSpeed = getPreferredSpeedDefault();
+
+ // Already decided: apply if enabled and exit
+ if (existing && existing.enabled) {
+ applySpeedToAllVideos(preferredSpeed);
+ return;
+ }
+ if (existing && existing.enabled === false) {
+ // explicitly disabled: do nothing
+ return;
+ }
+
+ // Not decided yet: show prompt
+ const { cleanup } = buildPromptUI({
+ domain,
+ speed: preferredSpeed,
+ onChoose: async (action, done) => {
+ if (action === "always") {
+ await saveDomainPref(domain, { enabled: true, speed: preferredSpeed, decidedAt: Date.now() });
+ applySpeedToAllVideos(preferredSpeed);
+ } else if (action === "never") {
+ await saveDomainPref(domain, { enabled: false, speed: preferredSpeed, decidedAt: Date.now() });
+ } // "later" -> do nothing, no persistence
+ done();
+ }
+ });
+}
+
+// Apply speed to all existing and future videos on the page
+function applySpeedToAllVideos(speed) {
+ const setSpeed = (v) => {
+ try {
+ v.playbackRate = speed;
+ if (v.preservesPitch !== undefined) v.preservesPitch = true;
+ } catch (_) {}
+ };
+ document.querySelectorAll("video").forEach(setSpeed);
+
+ // Future videos
+ const mo = new MutationObserver((muts) => {
+ for (const m of muts) {
+ m.addedNodes.forEach(node => {
+ if (node && node.nodeType === 1) {
+ if (node.tagName === "VIDEO") setSpeed(node);
+ node.querySelectorAll?.("video").forEach(setSpeed);
+ }
+ });
+ }
+ });
+ mo.observe(document.documentElement, { childList: true, subtree: true });
+}
+
/* Log levels (depends on caller specifying the correct level)
1 - none
2 - error
@@ -902,3 +1095,22 @@ function showController(controller) {
log("Hiding controller", 5);
}, 2000);
}
+
+// When first playable video is detected on a page, consider prompting
+(function bootstrapSiteDecision() {
+ const tryInit = () => {
+ const vid = document.querySelector("video");
+ if (vid) {
+ maybePromptAndApplyForSite(vid);
+ return true;
+ }
+ return false;
+ };
+
+ if (!tryInit()) {
+ const mo = new MutationObserver(() => {
+ if (tryInit()) mo.disconnect();
+ });
+ mo.observe(document.documentElement, { childList: true, subtree: true });
+ }
+})();
diff --git a/options.html b/options.html
index 242c326f..f3396cd4 100644
--- a/options.html
+++ b/options.html
@@ -181,6 +181,13 @@ Other
+
+ Site Preferences
+ These decisions control whether preferred speed is auto-applied per site.
+
+
+
+
diff --git a/options.js b/options.js
index e1b8b95f..942719de 100644
--- a/options.js
+++ b/options.js
@@ -387,3 +387,30 @@ document.addEventListener("DOMContentLoaded", function () {
});
});
});
+
+const SITE_PREFS_KEY = "sitePrefs";
+
+async function renderSitePrefs() {
+ const { [SITE_PREFS_KEY]: prefs } = await chrome.storage.local.get(SITE_PREFS_KEY);
+ const container = document.getElementById("site-prefs");
+ container.innerHTML = "";
+ const list = document.createElement("ul");
+ const entries = Object.entries(prefs || {});
+ if (entries.length === 0) {
+ container.textContent = "No site decisions saved yet.";
+ return;
+ }
+ for (const [domain, data] of entries) {
+ const li = document.createElement("li");
+ li.textContent = `${domain} — ${data.enabled ? "Always" : "Never"} (${(data.speed||1.6).toFixed(2)}×)`;
+ list.appendChild(li);
+ }
+ container.appendChild(list);
+}
+
+document.getElementById("clear-site-prefs").addEventListener("click", async () => {
+ await chrome.storage.local.remove(SITE_PREFS_KEY);
+ renderSitePrefs();
+});
+
+document.addEventListener("DOMContentLoaded", renderSitePrefs);