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 = ` + + + `; + + 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);