Add configurable skip delay (0-30s) for Skip Intro and Skip Credits
This commit is contained in:
1 parent
0f1f2a309e
commit
b2082d0624
9 files changed
+333
-21
No files matched your search
@@ -7,7 +7,8 @@ A small Chrome extension that automatically clicks "Skip Intro" and "Skip Credit
|
|||||||
- Watches the Plex Web player and clicks "Skip Intro" / "Skip Credits" the instant either button appears.
|
- Watches the Plex Web player and clicks "Skip Intro" / "Skip Credits" the instant either button appears.
|
||||||
- Works on `app.plex.tv` by default, or point it at your own server address (local IP, custom domain, whatever you use).
|
- Works on `app.plex.tv` by default, or point it at your own server address (local IP, custom domain, whatever you use).
|
||||||
- Toggle Skip Intro and Skip Credits independently — both on by default.
|
- Toggle Skip Intro and Skip Credits independently — both on by default.
|
||||||
- Settings save instantly and apply live, no reload required for toggle changes.
|
- Optional delay (0-30s) before each is clicked, in case a show needs a few extra seconds before the credits skip feels right.
|
||||||
|
- Settings save instantly and apply live, no reload required for toggle or delay changes.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
@@ -21,12 +22,13 @@ A small Chrome extension that automatically clicks "Skip Intro" and "Skip Credit
|
|||||||
|
|
||||||
- **Plex URL** — defaults to `app.plex.tv`. Choose "Custom URL" and enter your own server address (e.g. `192.168.1.50:32400/web` or a custom domain) if you watch there instead.
|
- **Plex URL** — defaults to `app.plex.tv`. Choose "Custom URL" and enter your own server address (e.g. `192.168.1.50:32400/web` or a custom domain) if you watch there instead.
|
||||||
- **Skip Intro** / **Skip Credits** — both on by default. Uncheck either one if you'd rather click it yourself sometimes.
|
- **Skip Intro** / **Skip Credits** — both on by default. Uncheck either one if you'd rather click it yourself sometimes.
|
||||||
|
- **Delay before clicking** — a 0-30 second slider under each toggle. Leave at 0 to click the instant the button appears (the original behavior), or add a few seconds if a particular show's Skip Credits button shows up before you're actually ready for it to fire. Each slider is disabled while its skip toggle is off, since a delay for a skip that isn't happening doesn't do anything.
|
||||||
|
|
||||||
Changing the Plex URL takes effect on your next page load — refresh your Plex tab after saving a new one. Toggle changes apply immediately.
|
Changing the Plex URL takes effect on your next page load — refresh your Plex tab after saving a new one. Toggle and delay changes apply immediately, no refresh needed.
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
The extension's content script runs on every page but only activates once the page's hostname matches your configured Plex URL. From there it uses a `MutationObserver` to watch the page for any `<button>` or `[role="button"]` element whose visible text contains "skip intro" or "skip credit" (case-insensitive, so it still matches if Plex appends a countdown like "Skip Intro 0:05"). When a match appears and is actually visible, it clicks it — with a short cooldown per element so a single button can't be double-clicked in one burst, but can still be clicked again later if Plex reuses the same button for the next episode.
|
The extension's content script runs on every page but only activates once the page's hostname matches your configured Plex URL. From there it uses a `MutationObserver` to watch the page for any `<button>` or `[role="button"]` element whose visible text contains "skip intro" or "skip credit" (case-insensitive, so it still matches if Plex appends a countdown like "Skip Intro 0:05"). When a match appears and is actually visible, it schedules a click — immediately if its delay is 0, or after the configured delay otherwise. Right before a delayed click actually fires, it re-checks that the button is still present, visible, still says "Skip Intro"/"Skip Credits", and that its toggle is still on — so turning a toggle off, or Plex removing the button, cancels a pending click instead of firing it late. A short cooldown per element stops a single button from being double-clicked in one burst, but still allows a later click if Plex reuses the same button element for the next episode.
|
||||||
|
|
||||||
Settings (your Plex URL and the two toggles) are stored with `chrome.storage.sync`, so they follow you across machines if you're signed into Chrome.
|
Settings (your Plex URL and the two toggles) are stored with `chrome.storage.sync`, so they follow you across machines if you're signed into Chrome.
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ npm install
|
|||||||
npm test
|
npm test
|
||||||
```
|
```
|
||||||
|
|
||||||
The suite covers: clicking Skip Intro/Skip Credits, ignoring non-Plex pages, respecting the on/off toggles, matching a custom server URL, reacting to buttons added dynamically after page load, ignoring unrelated buttons, handling countdown suffixes in the button text, not clicking hidden buttons, correctly re-clicking a reused button after its cooldown, and the settings popup's save/validation/load behavior.
|
The suite covers: clicking Skip Intro/Skip Credits, ignoring non-Plex pages, respecting the on/off toggles, matching a custom server URL, reacting to buttons added dynamically after page load, ignoring unrelated buttons, handling countdown suffixes in the button text, not clicking hidden buttons, correctly re-clicking a reused button after its cooldown, delayed clicks (including cancellation when a toggle turns off or the button disappears mid-delay), and the settings popup's save/validation/load behavior for both the URL and the delay sliders.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
+48
-11
@@ -1,6 +1,6 @@
|
|||||||
(function () {
|
(function () {
|
||||||
let settings = DEFAULT_SETTINGS;
|
let settings = DEFAULT_SETTINGS;
|
||||||
const recentClicks = new WeakMap();
|
const elementState = new WeakMap();
|
||||||
const CLICK_COOLDOWN_MS = 1500;
|
const CLICK_COOLDOWN_MS = 1500;
|
||||||
|
|
||||||
function isSkipIntroButton(text) {
|
function isSkipIntroButton(text) {
|
||||||
@@ -16,11 +16,48 @@
|
|||||||
return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
|
return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
function canClick(el) {
|
function getDelayMs(type) {
|
||||||
const last = recentClicks.get(el);
|
const seconds = type === "intro" ? settings.introDelaySeconds : settings.creditsDelaySeconds;
|
||||||
if (last && Date.now() - last < CLICK_COOLDOWN_MS) return false;
|
return clampDelaySeconds(seconds) * 1000;
|
||||||
recentClicks.set(el, Date.now());
|
}
|
||||||
return true;
|
|
||||||
|
function isStillEligible(el, type) {
|
||||||
|
if (!document.body.contains(el)) return false;
|
||||||
|
if (!isVisible(el)) return false;
|
||||||
|
if (type === "intro" && !settings.skipIntro) return false;
|
||||||
|
if (type === "credits" && !settings.skipCredits) return false;
|
||||||
|
if (!hostMatchesConfig(window.location.hostname, settings.plexUrl)) return false;
|
||||||
|
|
||||||
|
const text = (el.innerText || el.textContent || "").trim().toLowerCase();
|
||||||
|
return type === "intro" ? isSkipIntroButton(text) : isSkipCreditsButton(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function performClick(el) {
|
||||||
|
const state = elementState.get(el) || {};
|
||||||
|
state.timer = null;
|
||||||
|
state.lastClickAt = Date.now();
|
||||||
|
elementState.set(el, state);
|
||||||
|
el.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleClick(el, type) {
|
||||||
|
const state = elementState.get(el) || {};
|
||||||
|
if (state.timer) return;
|
||||||
|
if (state.lastClickAt && Date.now() - state.lastClickAt < CLICK_COOLDOWN_MS) return;
|
||||||
|
|
||||||
|
const delayMs = getDelayMs(type);
|
||||||
|
if (delayMs <= 0) {
|
||||||
|
performClick(el);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.timer = setTimeout(() => {
|
||||||
|
const current = elementState.get(el) || {};
|
||||||
|
current.timer = null;
|
||||||
|
elementState.set(el, current);
|
||||||
|
if (isStillEligible(el, type)) performClick(el);
|
||||||
|
}, delayMs);
|
||||||
|
elementState.set(el, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scanAndClick() {
|
function scanAndClick() {
|
||||||
@@ -33,11 +70,11 @@
|
|||||||
if (!text) return;
|
if (!text) return;
|
||||||
if (!isVisible(el)) return;
|
if (!isVisible(el)) return;
|
||||||
|
|
||||||
const wantsIntroSkip = settings.skipIntro && isSkipIntroButton(text);
|
if (settings.skipIntro && isSkipIntroButton(text)) {
|
||||||
const wantsCreditsSkip = settings.skipCredits && isSkipCreditsButton(text);
|
scheduleClick(el, "intro");
|
||||||
if (!wantsIntroSkip && !wantsCreditsSkip) return;
|
} else if (settings.skipCredits && isSkipCreditsButton(text)) {
|
||||||
|
scheduleClick(el, "credits");
|
||||||
if (canClick(el)) el.click();
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+37
@@ -45,6 +45,28 @@
|
|||||||
.toggle-row span {
|
.toggle-row span {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
.delay-row {
|
||||||
|
padding: 0 0 10px;
|
||||||
|
}
|
||||||
|
.delay-row-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #777;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.delay-row-label span:last-child {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
input[type="range"] {
|
||||||
|
width: 100%;
|
||||||
|
accent-color: #e5a00d;
|
||||||
|
}
|
||||||
|
input[type="range"]:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
button.save {
|
button.save {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
@@ -85,10 +107,25 @@
|
|||||||
<span>Skip Intro</span>
|
<span>Skip Intro</span>
|
||||||
<input type="checkbox" id="skipIntro">
|
<input type="checkbox" id="skipIntro">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="delay-row">
|
||||||
|
<div class="delay-row-label">
|
||||||
|
<span>Delay before clicking</span>
|
||||||
|
<span id="introDelayValue">0s</span>
|
||||||
|
</div>
|
||||||
|
<input type="range" id="introDelay" min="0" max="30" step="1" value="0">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="toggle-row">
|
<div class="toggle-row">
|
||||||
<span>Skip Credits</span>
|
<span>Skip Credits</span>
|
||||||
<input type="checkbox" id="skipCredits">
|
<input type="checkbox" id="skipCredits">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="delay-row">
|
||||||
|
<div class="delay-row-label">
|
||||||
|
<span>Delay before clicking</span>
|
||||||
|
<span id="creditsDelayValue">0s</span>
|
||||||
|
</div>
|
||||||
|
<input type="range" id="creditsDelay" min="0" max="30" step="1" value="0">
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="save" id="saveBtn">Save</button>
|
<button class="save" id="saveBtn">Save</button>
|
||||||
<div id="status"></div>
|
<div id="status"></div>
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const customUrl = document.getElementById("customUrl");
|
const customUrl = document.getElementById("customUrl");
|
||||||
const skipIntro = document.getElementById("skipIntro");
|
const skipIntro = document.getElementById("skipIntro");
|
||||||
const skipCredits = document.getElementById("skipCredits");
|
const skipCredits = document.getElementById("skipCredits");
|
||||||
|
const introDelay = document.getElementById("introDelay");
|
||||||
|
const introDelayValue = document.getElementById("introDelayValue");
|
||||||
|
const creditsDelay = document.getElementById("creditsDelay");
|
||||||
|
const creditsDelayValue = document.getElementById("creditsDelayValue");
|
||||||
const saveBtn = document.getElementById("saveBtn");
|
const saveBtn = document.getElementById("saveBtn");
|
||||||
const status = document.getElementById("status");
|
const status = document.getElementById("status");
|
||||||
|
|
||||||
@@ -11,7 +15,24 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
customUrlRow.style.display = urlPreset.value === "custom" ? "block" : "none";
|
customUrlRow.style.display = urlPreset.value === "custom" ? "block" : "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDelay(seconds) {
|
||||||
|
return seconds + "s";
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDelayEnabled() {
|
||||||
|
introDelay.disabled = !skipIntro.checked;
|
||||||
|
creditsDelay.disabled = !skipCredits.checked;
|
||||||
|
}
|
||||||
|
|
||||||
urlPreset.addEventListener("change", toggleCustomRow);
|
urlPreset.addEventListener("change", toggleCustomRow);
|
||||||
|
skipIntro.addEventListener("change", updateDelayEnabled);
|
||||||
|
skipCredits.addEventListener("change", updateDelayEnabled);
|
||||||
|
introDelay.addEventListener("input", () => {
|
||||||
|
introDelayValue.textContent = formatDelay(introDelay.value);
|
||||||
|
});
|
||||||
|
creditsDelay.addEventListener("input", () => {
|
||||||
|
creditsDelayValue.textContent = formatDelay(creditsDelay.value);
|
||||||
|
});
|
||||||
|
|
||||||
chrome.storage.sync.get(DEFAULT_SETTINGS, (settings) => {
|
chrome.storage.sync.get(DEFAULT_SETTINGS, (settings) => {
|
||||||
if (settings.plexUrl === DEFAULT_SETTINGS.plexUrl) {
|
if (settings.plexUrl === DEFAULT_SETTINGS.plexUrl) {
|
||||||
@@ -22,7 +43,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
skipIntro.checked = settings.skipIntro;
|
skipIntro.checked = settings.skipIntro;
|
||||||
skipCredits.checked = settings.skipCredits;
|
skipCredits.checked = settings.skipCredits;
|
||||||
|
|
||||||
|
introDelay.value = clampDelaySeconds(settings.introDelaySeconds);
|
||||||
|
introDelayValue.textContent = formatDelay(introDelay.value);
|
||||||
|
creditsDelay.value = clampDelaySeconds(settings.creditsDelaySeconds);
|
||||||
|
creditsDelayValue.textContent = formatDelay(creditsDelay.value);
|
||||||
|
|
||||||
toggleCustomRow();
|
toggleCustomRow();
|
||||||
|
updateDelayEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
function showStatus(message, isError) {
|
function showStatus(message, isError) {
|
||||||
@@ -54,7 +82,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
{
|
{
|
||||||
plexUrl: plexUrl,
|
plexUrl: plexUrl,
|
||||||
skipIntro: skipIntro.checked,
|
skipIntro: skipIntro.checked,
|
||||||
skipCredits: skipCredits.checked
|
skipCredits: skipCredits.checked,
|
||||||
|
introDelaySeconds: clampDelaySeconds(introDelay.value),
|
||||||
|
creditsDelaySeconds: clampDelaySeconds(creditsDelay.value)
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
showStatus("Saved", false);
|
showStatus("Saved", false);
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { loadContentScript, wait } = require("./helpers");
|
||||||
|
|
||||||
|
test("zero delay still clicks immediately (default behavior preserved)", async () => {
|
||||||
|
const { window } = loadContentScript({
|
||||||
|
url: "https://app.plex.tv/web/index.html",
|
||||||
|
html: `<!DOCTYPE html><html><body><button>Skip Intro</button></body></html>`,
|
||||||
|
settings: { introDelaySeconds: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
await wait(50);
|
||||||
|
assert.strictEqual(window.__clickedElements.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not click immediately when a delay is configured, but clicks once it elapses", async () => {
|
||||||
|
const { window } = loadContentScript({
|
||||||
|
url: "https://app.plex.tv/web/index.html",
|
||||||
|
html: `<!DOCTYPE html><html><body><button>Skip Credits</button></body></html>`,
|
||||||
|
settings: { creditsDelaySeconds: 1 }
|
||||||
|
});
|
||||||
|
|
||||||
|
await wait(50);
|
||||||
|
assert.strictEqual(window.__clickedElements.length, 0, "should not click before the delay elapses");
|
||||||
|
|
||||||
|
await wait(1100);
|
||||||
|
assert.strictEqual(window.__clickedElements.length, 1, "should click once the delay elapses");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not schedule duplicate clicks while a delay is already pending", async () => {
|
||||||
|
const { window } = loadContentScript({
|
||||||
|
url: "https://app.plex.tv/web/index.html",
|
||||||
|
html: `<!DOCTYPE html><html><body><button id="skip">Skip Intro</button></body></html>`,
|
||||||
|
settings: { introDelaySeconds: 1 }
|
||||||
|
});
|
||||||
|
|
||||||
|
await wait(50);
|
||||||
|
window.document.body.appendChild(window.document.createElement("div"));
|
||||||
|
await wait(50);
|
||||||
|
window.document.body.appendChild(window.document.createElement("div"));
|
||||||
|
|
||||||
|
await wait(1200);
|
||||||
|
assert.strictEqual(window.__clickedElements.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("cancels a scheduled click if its toggle is turned off mid-delay", async () => {
|
||||||
|
const { window, fireStorageChange } = loadContentScript({
|
||||||
|
url: "https://app.plex.tv/web/index.html",
|
||||||
|
html: `<!DOCTYPE html><html><body><button>Skip Intro</button></body></html>`,
|
||||||
|
settings: { introDelaySeconds: 1 }
|
||||||
|
});
|
||||||
|
|
||||||
|
await wait(50);
|
||||||
|
fireStorageChange({ skipIntro: false });
|
||||||
|
|
||||||
|
await wait(1200);
|
||||||
|
assert.strictEqual(window.__clickedElements.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not click if the button is removed from the page before the delay elapses", async () => {
|
||||||
|
const { window } = loadContentScript({
|
||||||
|
url: "https://app.plex.tv/web/index.html",
|
||||||
|
html: `<!DOCTYPE html><html><body><button id="skip">Skip Intro</button></body></html>`,
|
||||||
|
settings: { introDelaySeconds: 1 }
|
||||||
|
});
|
||||||
|
|
||||||
|
await wait(50);
|
||||||
|
window.document.getElementById("skip").remove();
|
||||||
|
|
||||||
|
await wait(1200);
|
||||||
|
assert.strictEqual(window.__clickedElements.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not click if the delayed button is hidden before the delay elapses", async () => {
|
||||||
|
const { window } = loadContentScript({
|
||||||
|
url: "https://app.plex.tv/web/index.html",
|
||||||
|
html: `<!DOCTYPE html><html><body><button id="skip">Skip Intro</button></body></html>`,
|
||||||
|
settings: { introDelaySeconds: 1 }
|
||||||
|
});
|
||||||
|
|
||||||
|
await wait(50);
|
||||||
|
window.document.getElementById("skip").style.display = "none";
|
||||||
|
|
||||||
|
await wait(1200);
|
||||||
|
assert.strictEqual(window.__clickedElements.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("intro and credits delays run independently", async () => {
|
||||||
|
const { window } = loadContentScript({
|
||||||
|
url: "https://app.plex.tv/web/index.html",
|
||||||
|
html: `<!DOCTYPE html><html><body>
|
||||||
|
<button id="intro">Skip Intro</button>
|
||||||
|
<button id="credits">Skip Credits</button>
|
||||||
|
</body></html>`,
|
||||||
|
settings: { introDelaySeconds: 0, creditsDelaySeconds: 1 }
|
||||||
|
});
|
||||||
|
|
||||||
|
await wait(50);
|
||||||
|
assert.strictEqual(window.__clickedElements.length, 1, "Skip Intro should click immediately");
|
||||||
|
assert.strictEqual(window.__clickedElements[0], window.document.getElementById("intro"));
|
||||||
|
|
||||||
|
await wait(1100);
|
||||||
|
assert.strictEqual(window.__clickedElements.length, 2, "Skip Credits should click after its own delay");
|
||||||
|
});
|
||||||
+26
-3
@@ -10,7 +10,13 @@ function loadContentScript({ url, html, settings }) {
|
|||||||
|
|
||||||
const window = dom.window;
|
const window = dom.window;
|
||||||
const storedSettings = Object.assign(
|
const storedSettings = Object.assign(
|
||||||
{ plexUrl: "https://app.plex.tv", skipIntro: true, skipCredits: true },
|
{
|
||||||
|
plexUrl: "https://app.plex.tv",
|
||||||
|
skipIntro: true,
|
||||||
|
skipCredits: true,
|
||||||
|
introDelaySeconds: 0,
|
||||||
|
creditsDelaySeconds: 0
|
||||||
|
},
|
||||||
settings || {}
|
settings || {}
|
||||||
);
|
);
|
||||||
const changeListeners = [];
|
const changeListeners = [];
|
||||||
@@ -57,7 +63,13 @@ function loadPopupScript({ initialSettings } = {}) {
|
|||||||
const dom = new JSDOM(html, { url: "chrome-extension://fake/popup.html", runScripts: "dangerously" });
|
const dom = new JSDOM(html, { url: "chrome-extension://fake/popup.html", runScripts: "dangerously" });
|
||||||
const window = dom.window;
|
const window = dom.window;
|
||||||
const storedSettings = Object.assign(
|
const storedSettings = Object.assign(
|
||||||
{ plexUrl: "https://app.plex.tv", skipIntro: true, skipCredits: true },
|
{
|
||||||
|
plexUrl: "https://app.plex.tv",
|
||||||
|
skipIntro: true,
|
||||||
|
skipCredits: true,
|
||||||
|
introDelaySeconds: 0,
|
||||||
|
creditsDelaySeconds: 0
|
||||||
|
},
|
||||||
initialSettings || {}
|
initialSettings || {}
|
||||||
);
|
);
|
||||||
const savedCalls = [];
|
const savedCalls = [];
|
||||||
@@ -82,8 +94,19 @@ function loadPopupScript({ initialSettings } = {}) {
|
|||||||
return { dom, window, savedCalls, storedSettings };
|
return { dom, window, savedCalls, storedSettings };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadUtils() {
|
||||||
|
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
|
||||||
|
url: "https://example.com",
|
||||||
|
runScripts: "dangerously"
|
||||||
|
});
|
||||||
|
const window = dom.window;
|
||||||
|
const utilsCode = fs.readFileSync(path.join(__dirname, "..", "utils.js"), "utf8");
|
||||||
|
window.eval(utilsCode);
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
function wait(ms) {
|
function wait(ms) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { loadContentScript, loadPopupScript, wait };
|
module.exports = { loadContentScript, loadPopupScript, loadUtils, wait };
|
||||||
+42
-1
@@ -2,7 +2,7 @@ const test = require("node:test");
|
|||||||
const assert = require("node:assert");
|
const assert = require("node:assert");
|
||||||
const { loadPopupScript, wait } = require("./helpers");
|
const { loadPopupScript, wait } = require("./helpers");
|
||||||
|
|
||||||
test("defaults to app.plex.tv preset with both toggles on", async () => {
|
test("defaults to app.plex.tv preset with both toggles on and delays at 0s", async () => {
|
||||||
const { window } = loadPopupScript();
|
const { window } = loadPopupScript();
|
||||||
await wait(20);
|
await wait(20);
|
||||||
|
|
||||||
@@ -10,6 +10,47 @@ test("defaults to app.plex.tv preset with both toggles on", async () => {
|
|||||||
assert.strictEqual(window.document.getElementById("skipIntro").checked, true);
|
assert.strictEqual(window.document.getElementById("skipIntro").checked, true);
|
||||||
assert.strictEqual(window.document.getElementById("skipCredits").checked, true);
|
assert.strictEqual(window.document.getElementById("skipCredits").checked, true);
|
||||||
assert.strictEqual(window.document.getElementById("customUrlRow").style.display, "none");
|
assert.strictEqual(window.document.getElementById("customUrlRow").style.display, "none");
|
||||||
|
assert.strictEqual(window.document.getElementById("introDelay").value, "0");
|
||||||
|
assert.strictEqual(window.document.getElementById("creditsDelay").value, "0");
|
||||||
|
assert.strictEqual(window.document.getElementById("introDelayValue").textContent, "0s");
|
||||||
|
assert.strictEqual(window.document.getElementById("creditsDelayValue").textContent, "0s");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("loads a previously saved delay value onto the sliders", async () => {
|
||||||
|
const { window } = loadPopupScript({
|
||||||
|
initialSettings: { introDelaySeconds: 3, creditsDelaySeconds: 12 }
|
||||||
|
});
|
||||||
|
await wait(20);
|
||||||
|
|
||||||
|
assert.strictEqual(window.document.getElementById("introDelay").value, "3");
|
||||||
|
assert.strictEqual(window.document.getElementById("introDelayValue").textContent, "3s");
|
||||||
|
assert.strictEqual(window.document.getElementById("creditsDelay").value, "12");
|
||||||
|
assert.strictEqual(window.document.getElementById("creditsDelayValue").textContent, "12s");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("disables a delay slider when its skip toggle is off", async () => {
|
||||||
|
const { window } = loadPopupScript({
|
||||||
|
initialSettings: { skipCredits: false }
|
||||||
|
});
|
||||||
|
await wait(20);
|
||||||
|
|
||||||
|
assert.strictEqual(window.document.getElementById("introDelay").disabled, false);
|
||||||
|
assert.strictEqual(window.document.getElementById("creditsDelay").disabled, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("saving persists the configured delay values", async () => {
|
||||||
|
const { window, savedCalls } = loadPopupScript();
|
||||||
|
await wait(20);
|
||||||
|
|
||||||
|
window.document.getElementById("introDelay").value = "5";
|
||||||
|
window.document.getElementById("creditsDelay").value = "20";
|
||||||
|
|
||||||
|
window.document.getElementById("saveBtn").click();
|
||||||
|
await wait(20);
|
||||||
|
|
||||||
|
assert.strictEqual(savedCalls.length, 1);
|
||||||
|
assert.strictEqual(savedCalls[0].introDelaySeconds, 5);
|
||||||
|
assert.strictEqual(savedCalls[0].creditsDelaySeconds, 20);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("shows custom URL field pre-filled when a custom URL is already saved", async () => {
|
test("shows custom URL field pre-filled when a custom URL is already saved", async () => {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { loadUtils } = require("./helpers");
|
||||||
|
|
||||||
|
test("clampDelaySeconds keeps in-range integer values as-is", () => {
|
||||||
|
const window = loadUtils();
|
||||||
|
assert.strictEqual(window.clampDelaySeconds(0), 0);
|
||||||
|
assert.strictEqual(window.clampDelaySeconds(5), 5);
|
||||||
|
assert.strictEqual(window.clampDelaySeconds(30), 30);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clampDelaySeconds clamps below 0 and above 30", () => {
|
||||||
|
const window = loadUtils();
|
||||||
|
assert.strictEqual(window.clampDelaySeconds(-5), 0);
|
||||||
|
assert.strictEqual(window.clampDelaySeconds(45), 30);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clampDelaySeconds rounds fractional values", () => {
|
||||||
|
const window = loadUtils();
|
||||||
|
assert.strictEqual(window.clampDelaySeconds(15.6), 16);
|
||||||
|
assert.strictEqual(window.clampDelaySeconds(15.4), 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clampDelaySeconds falls back to 0 for non-numeric input", () => {
|
||||||
|
const window = loadUtils();
|
||||||
|
assert.strictEqual(window.clampDelaySeconds("abc"), 0);
|
||||||
|
assert.strictEqual(window.clampDelaySeconds(undefined), 0);
|
||||||
|
assert.strictEqual(window.clampDelaySeconds(null), 0);
|
||||||
|
assert.strictEqual(window.clampDelaySeconds(NaN), 0);
|
||||||
|
});
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
const DEFAULT_SETTINGS = {
|
const DEFAULT_SETTINGS = {
|
||||||
plexUrl: "https://app.plex.tv",
|
plexUrl: "https://app.plex.tv",
|
||||||
skipIntro: true,
|
skipIntro: true,
|
||||||
skipCredits: true
|
skipCredits: true,
|
||||||
|
introDelaySeconds: 0,
|
||||||
|
creditsDelaySeconds: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function clampDelaySeconds(value) {
|
||||||
|
const num = Number(value);
|
||||||
|
if (!Number.isFinite(num)) return 0;
|
||||||
|
return Math.min(30, Math.max(0, Math.round(num)));
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePlexUrl(input) {
|
function normalizePlexUrl(input) {
|
||||||
let value = (input || "").trim();
|
let value = (input || "").trim();
|
||||||
if (!value) return DEFAULT_SETTINGS.plexUrl;
|
if (!value) return DEFAULT_SETTINGS.plexUrl;
|
||||||
|
|||||||
Reference in new issue
Block a user