feat: Site-Specific Rules stash pop, cleaned up some settings

This commit is contained in:
2026-03-28 11:38:13 -04:00
parent bec32b4c0e
commit 5616778421
5 changed files with 1023 additions and 352 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"kiroAgent.configureMCP": "Disabled"
}
+160 -41
View File
@@ -16,13 +16,7 @@ var tc = {
controllerLocation: "top-left",
controllerOpacity: 0.3,
keyBindings: [],
blacklist: `\
www.instagram.com
twitter.com
vine.co
imgur.com
teams.microsoft.com
`.replace(regStrip, ""),
siteRules: [],
defaultLogLevel: 3,
logLevel: 3,
enableSubtitleNudge: true, // Enabled by default, but only activates on YouTube
@@ -33,7 +27,8 @@ var tc = {
isNudging: false,
pendingLastSpeedSave: null,
pendingLastSpeedValue: null,
persistedLastSpeed: 1.0
persistedLastSpeed: 1.0,
activeSiteRule: null
};
var MIN_SPEED = 0.0625;
@@ -46,8 +41,8 @@ var requestIdle =
typeof window.requestIdleCallback === "function"
? window.requestIdleCallback.bind(window)
: function (callback, options) {
return setTimeout(callback, (options && options.timeout) || 1);
};
return setTimeout(callback, (options && options.timeout) || 1);
};
var controllerLocations = [
"top-left",
"top-center",
@@ -274,7 +269,21 @@ function applyControllerLocationToElement(controller, location) {
controller.dataset.location = normalizedLocation;
controller.dataset.positionMode = "anchored";
controller.style.top = styles.top;
var top = styles.top;
// If in fullscreen, move the controller down to avoid overlapping video titles
if (
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement
) {
if (normalizedLocation.startsWith("top-")) {
top = "50px";
}
}
controller.style.top = top;
controller.style.left = styles.left;
controller.style.transform = styles.transform;
@@ -509,8 +518,8 @@ function tryYouTubeNativeSpeed(video, speed) {
function isSubtitleNudgeSupported(video) {
return Boolean(
video &&
((video.currentSrc && video.currentSrc.includes("googlevideo.com")) ||
isOnYouTube())
((video.currentSrc && video.currentSrc.includes("googlevideo.com")) ||
isOnYouTube())
);
}
@@ -923,8 +932,7 @@ chrome.storage.sync.get(tc.settings, function (storage) {
startHidden: tc.settings.startHidden,
enabled: tc.settings.enabled,
controllerLocation: tc.settings.controllerLocation,
controllerOpacity: tc.settings.controllerOpacity,
blacklist: tc.settings.blacklist.replace(regStrip, "")
controllerOpacity: tc.settings.controllerOpacity
});
}
tc.settings.lastSpeed = Number(storage.lastSpeed);
@@ -947,7 +955,29 @@ chrome.storage.sync.get(tc.settings, function (storage) {
storage.controllerLocation
);
tc.settings.controllerOpacity = Number(storage.controllerOpacity);
tc.settings.blacklist = String(storage.blacklist);
tc.settings.siteRules = Array.isArray(storage.siteRules)
? storage.siteRules
: [];
// Migrate legacy blacklist if present
if (storage.blacklist && typeof storage.blacklist === "string" && tc.settings.siteRules.length === 0) {
var lines = storage.blacklist.split("\n");
lines.forEach((line) => {
var pattern = line.replace(regStrip, "");
if (pattern.length > 0) {
tc.settings.siteRules.push({
pattern: pattern,
disableExtension: true
});
}
});
if (tc.settings.siteRules.length > 0) {
chrome.storage.sync.set({ siteRules: tc.settings.siteRules });
chrome.storage.sync.remove(["blacklist"]);
log("Migrated legacy blacklist to site rules", 4);
}
}
tc.settings.enableSubtitleNudge =
typeof storage.enableSubtitleNudge !== "undefined"
? Boolean(storage.enableSubtitleNudge)
@@ -1463,27 +1493,91 @@ function escapeStringRegExp(str) {
const m = /[|\\{}()[\]^$+*?.]/g;
return str.replace(m, "\\$&");
}
function isBlacklisted() {
let b = false;
const l = tc.settings.blacklist ? tc.settings.blacklist.split("\n") : [];
l.forEach((m) => {
if (b) return;
m = m.replace(regStrip, "");
if (m.length == 0) return;
let r;
if (m.startsWith("/") && m.lastIndexOf("/") > 0) {
function applySiteRuleOverrides() {
if (!Array.isArray(tc.settings.siteRules) || tc.settings.siteRules.length === 0) {
return false;
}
var currentUrl = location.href;
var matchedRule = null;
for (var i = 0; i < tc.settings.siteRules.length; i++) {
var rule = tc.settings.siteRules[i];
var pattern = rule.pattern;
if (!pattern || pattern.length === 0) continue;
var regex;
if (pattern.startsWith("/") && pattern.lastIndexOf("/") > 0) {
try {
const ls = m.lastIndexOf("/");
r = new RegExp(m.substring(1, ls), m.substring(ls + 1));
var lastSlash = pattern.lastIndexOf("/");
regex = new RegExp(
pattern.substring(1, lastSlash),
pattern.substring(lastSlash + 1)
);
} catch (e) {
log(`Invalid regex: ${m}. ${e.message}`, 2);
return;
log(`Invalid site rule regex: ${pattern}. ${e.message}`, 2);
continue;
}
} else r = new RegExp(escapeStringRegExp(m));
if (r && r.test(location.href)) b = true;
} else {
regex = new RegExp(escapeStringRegExp(pattern));
}
if (regex && regex.test(currentUrl)) {
matchedRule = rule;
break;
}
}
if (!matchedRule) return false;
tc.activeSiteRule = matchedRule;
log(`Matched site rule: ${matchedRule.pattern}`, 4);
// Check if extension should be enabled/disabled on this site
if (matchedRule.enabled === false) {
log(`Extension disabled for site: ${currentUrl}`, 4);
return true;
} else if (matchedRule.disableExtension === true) {
// Handle old format
log(`Extension disabled (legacy) for site: ${currentUrl}`, 4);
return true;
}
// Override general settings with site-specific overrides
const siteSettings = [
"startHidden",
"controllerLocation",
"rememberSpeed",
"forceLastSavedSpeed",
"audioBoolean",
"controllerOpacity"
];
siteSettings.forEach((key) => {
if (matchedRule[key] !== undefined) {
log(`Overriding ${key} for site: ${matchedRule[key]}`, 4);
tc.settings[key] = matchedRule[key];
}
});
if (b) log(`Page ${location.href} blacklisted.`, 4);
return b;
// Override key bindings with site-specific shortcuts
if (Array.isArray(matchedRule.shortcuts) && matchedRule.shortcuts.length > 0) {
var overriddenActions = new Set();
matchedRule.shortcuts.forEach((shortcut) => {
overriddenActions.add(shortcut.action);
});
// Keep global bindings that aren't overridden, add site-specific ones
tc.settings.keyBindings = tc.settings.keyBindings
.filter((binding) => !overriddenActions.has(binding.action))
.concat(
matchedRule.shortcuts.map((shortcut) =>
normalizeStoredBinding(shortcut)
).filter(Boolean)
);
}
return false;
}
function shouldPreserveDesiredSpeed(video, speed) {
@@ -1627,7 +1721,7 @@ function attachKeydownListeners(doc) {
var docs = [doc];
try {
if (inIframe() && window.top.document !== doc) docs.push(window.top.document);
} catch (e) {}
} catch (e) { }
docs.forEach(function (keyDoc) {
if (keyDoc.vscKeydownListenerAttached) return;
@@ -1662,7 +1756,16 @@ function attachKeydownListeners(doc) {
if (item) {
runAction(item.action, item.value, event);
if (item.force === "true") {
// Check if we should disable website key bindings for this specific shortcut
var shouldPreventDefault = false;
// Check if this shortcut has force enabled (from site-specific shortcuts)
if (item.force === true || item.force === "true") {
shouldPreventDefault = true;
}
if (shouldPreventDefault) {
event.preventDefault();
event.stopPropagation();
}
@@ -1772,7 +1875,7 @@ function attachIframeListeners(doc) {
if (frame.contentDocument) {
initializeWhenReady(frame.contentDocument, true);
}
} catch (e) {}
} catch (e) { }
});
frame.vscLoadListenerAttached = true;
}
@@ -1781,7 +1884,7 @@ function attachIframeListeners(doc) {
if (frame.contentDocument) {
initializeWhenReady(frame.contentDocument);
}
} catch (e) {}
} catch (e) { }
});
}
@@ -1812,7 +1915,9 @@ function attachNavigationListeners() {
function initializeNow(doc, forceReinit = false) {
if ((!forceReinit && vscInitializedDocuments.has(doc)) || !doc.body) return;
if (!tc.settings.enabled || isBlacklisted()) return;
var siteDisabled = applySiteRuleOverrides();
if (!tc.settings.enabled || siteDisabled) return;
if (!doc.body.classList.contains("vsc-initialized")) {
doc.body.classList.add("vsc-initialized");
@@ -1831,7 +1936,7 @@ function initializeNow(doc, forceReinit = false) {
function setSpeed(video, speed, isInitialCall = false, isUserKeyPress = false) {
const numericSpeed = Number(speed);
if (!isValidSpeed(numericSpeed)) {
log(
`Invalid speed rejected: ${speed}, must be between ${MIN_SPEED} and ${MAX_SPEED}`,
@@ -1839,7 +1944,7 @@ function setSpeed(video, speed, isInitialCall = false, isUserKeyPress = false) {
);
return;
}
if (!video || !video.vsc || !video.vsc.speedIndicator) return;
log(
@@ -1975,7 +2080,12 @@ function runAction(action, value, e) {
resetSpeed(v, 1.0, false); // Use enhanced resetSpeed
break;
case "fast":
resetSpeed(v, numValue, true); // Use enhanced resetSpeed
var preferredSpeed = numValue;
// Apply site-specific preferred speed override if available
if (tc.activeSiteRule && typeof tc.activeSiteRule.preferredSpeed === "number") {
preferredSpeed = tc.activeSiteRule.preferredSpeed;
}
resetSpeed(v, preferredSpeed, true);
break;
case "display":
if (controller.classList.contains("vsc-hidden")) {
@@ -2151,3 +2261,12 @@ function showController(controller, duration = 2000) {
controller.showTimeOut = undefined;
}, duration);
}
// Add global listener to handle fullscreen transitions and adjust controller positions
document.addEventListener("fullscreenchange", () => {
tc.mediaElements.forEach((video) => {
if (video.vsc) {
applyControllerLocation(video.vsc, video.vsc.controllerLocation);
}
});
});
+222 -9
View File
@@ -1,7 +1,7 @@
body {
margin: 0;
padding-left: 15px;
padding-top: 53px;
padding-top: 80px;
font-family: sans-serif;
font-size: 12px;
color: rgb(48, 57, 66);
@@ -18,10 +18,10 @@ h3 {
}
h1 {
font-size: 1.5em;
margin: 21px 0 13px;
margin: 21px 0 6px;
}
.version {
margin: 0 0 12px;
margin: 0 0 6px;
color: #6b6b6b;
font-size: 0.95em;
}
@@ -39,8 +39,10 @@ header {
top: 0;
left: 15px;
right: 0;
padding-bottom: 10px;
background: white;
z-index: 100;
border-bottom: 1px solid #eee;
background: linear-gradient(white, white 40%, rgba(255, 255, 255, 0.92));
}
header,
section {
@@ -58,6 +60,7 @@ section h3 {
button {
-webkit-appearance: none;
appearance: none;
position: relative;
margin: 0 1px 0 0;
@@ -87,6 +90,66 @@ input[type="text"] {
margin: 5px 0px;
}
.shortcuts-grid {
display: flex;
flex-direction: column;
gap: 5px;
}
.shortcut-row {
display: flex;
align-items: center;
gap: 5px;
}
.shortcut-label {
display: inline-block;
width: 170px;
padding: 4px 8px;
background-color: #f5f5f5;
border: 1px solid #ccc;
border-radius: 2px;
box-sizing: border-box;
font-size: 12px;
line-height: normal;
min-height: 26px;
}
.shortcut-row input[type="text"] {
height: 26px;
box-sizing: border-box;
}
.removeParent {
width: 20px;
height: 20px;
min-width: 20px;
border-radius: 50%;
padding: 0;
display: flex !important;
align-items: center;
justify-content: center;
font-size: 10px;
line-height: 1;
background-color: #eee;
border: 1px solid #ccc;
color: #666;
cursor: pointer;
margin-left: 5px;
}
.removeParent:hover {
background-color: #ff4444;
color: white;
border-color: #cc0000;
}
#addShortcutSelector {
margin-top: 15px;
padding: 4px;
width: 200px;
}
label {
display: inline-block;
width: 170px;
@@ -107,11 +170,6 @@ select {
width: 170px;
}
.customForce {
display: none;
width: 250px;
}
.customKey {
color: transparent;
text-shadow: 0 0 0 #000000;
@@ -177,6 +235,11 @@ select {
text-shadow: 0 0 0 #e0e0e0;
}
.shortcut-label {
background-color: #2a2a2a;
border-color: #555;
}
#status {
color: #888;
}
@@ -193,3 +256,153 @@ select {
border-color: #333;
}
}
/* Site Rules Styles */
#siteRulesContainer {
margin-bottom: 15px;
}
.site-rule {
border: 1px solid #ddd;
border-radius: 4px;
padding: 12px;
margin-bottom: 12px;
background-color: #f9f9f9;
}
.site-rule-header {
display: flex;
gap: 10px;
margin-bottom: 12px;
align-items: center;
}
.site-rule.collapsed .site-rule-header {
margin-bottom: 0;
}
.toggle-site-rule {
min-width: auto;
padding: 0 8px;
font-size: 12px;
cursor: pointer;
}
.site-pattern {
flex: 1;
min-width: 300px;
padding: 6px;
font-family: monospace;
}
.remove-site-rule {
min-width: auto;
padding: 0 12px;
}
.site-rule-body {
margin-left: 10px;
}
.site-rule-content.disabled-rule {
opacity: 0.5;
pointer-events: none;
filter: grayscale(1);
}
.site-rule-option {
margin: 10px 0;
display: flex;
align-items: center;
justify-content: space-between;
max-width: 450px;
}
.site-rule-option label {
width: auto;
line-height: normal;
display: flex;
align-items: center;
gap: 8px;
}
.site-rule-option input[type="text"],
.site-rule-option select {
width: 150px;
text-align: left;
}
.site-rule-shortcuts {
margin-top: 12px;
padding-top: 12px;
padding-bottom: 8px;
border-top: 1px solid #ddd;
}
.site-rule-shortcuts > label {
display: block;
margin-bottom: 8px;
width: auto;
}
.site-shortcuts-container {
margin-left: 20px;
margin-top: 10px;
margin-bottom: 8px;
}
.site-shortcuts-container .shortcut-row {
margin: 6px 0;
display: flex;
align-items: center;
gap: 5px;
}
.site-shortcuts-container .customKey {
width: 100px;
}
.site-shortcuts-container .customValue {
width: 80px;
}
.site-shortcuts-container .customDo {
width: 180px;
}
.site-shortcuts-container .customForce {
width: auto;
margin-left: 5px;
}
.site-shortcuts-container .force-label {
display: flex;
align-items: center;
gap: 4px;
margin-left: 8px;
width: auto;
font-size: 11px;
cursor: pointer;
}
.site-shortcuts-container .force-text {
user-select: none;
}
/* Dark mode for site rules */
@media (prefers-color-scheme: dark) {
.site-rule {
border-color: #444;
background-color: #252525;
}
.site-pattern {
background-color: #2a2a2a;
color: #e0e0e0;
border: 1px solid #555;
}
.site-rule-shortcuts {
border-top-color: #444;
}
}
+190 -188
View File
@@ -13,167 +13,120 @@
<section id="customs">
<h3>Shortcuts</h3>
<div class="row customs" id="display">
<select class="customDo">
<option value="display">Show/hide controller</option>
</select>
<input
class="customKey"
type="text"
value=""
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>
</div>
<div class="row customs" id="move">
<select class="customDo">
<option value="move">Move controller</option>
</select>
<input
class="customKey"
type="text"
value=""
placeholder="press a key"
/>
<input
class="customValue"
type="text"
placeholder="value (0)"
disabled
/>
<select class="customForce">
<option value="false">Do not disable website key bindings</option>
<option value="true">Disable website key bindings</option>
</select>
</div>
<div class="row customs" id="slower">
<select class="customDo">
<option value="slower">Decrease speed</option>
</select>
<input
class="customKey"
type="text"
value=""
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>
</div>
<div class="row customs" id="faster">
<select class="customDo">
<option value="faster">Increase speed</option>
</select>
<input
class="customKey"
type="text"
value=""
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>
</div>
<div class="row customs" id="rewind">
<select class="customDo">
<option value="rewind">Rewind</option>
</select>
<input
class="customKey"
type="text"
value=""
placeholder="press a key"
/>
<input class="customValue" type="text" placeholder="value (10)" />
<select class="customForce">
<option value="false">Do not disable website key bindings</option>
<option value="true">Disable website key bindings</option>
</select>
</div>
<div class="row customs" id="advance">
<select class="customDo">
<option value="advance">Advance</option>
</select>
<input
class="customKey"
type="text"
value=""
placeholder="press a key"
/>
<input class="customValue" type="text" placeholder="value (10)" />
<select class="customForce">
<option value="false">Do not disable website key bindings</option>
<option value="true">Disable website key bindings</option>
</select>
</div>
<div class="row customs" id="reset">
<select class="customDo">
<option value="reset">Reset speed</option>
</select>
<input
class="customKey"
type="text"
value=""
placeholder="press a key"
/>
<input
class="customValue"
type="text"
placeholder="value (1.00)"
disabled
/>
<select class="customForce">
<option value="false">Do not disable website key bindings</option>
<option value="true">Disable website key bindings</option>
</select>
</div>
<div class="row customs" id="fast">
<select class="customDo">
<option value="fast">Preferred speed</option>
</select>
<input
class="customKey"
type="text"
value=""
placeholder="press a key"
/>
<input class="customValue" type="text" placeholder="value (1.80)" />
<select class="customForce">
<option value="false">Do not disable website key bindings</option>
<option value="true">Disable website key bindings</option>
</select>
</div>
<div class="row customs" id="toggleSubtitleNudge">
<select class="customDo">
<option value="toggleSubtitleNudge">Toggle subtitle nudge</option>
</select>
<input
class="customKey"
type="text"
value=""
placeholder="press a key"
/>
<input
class="customValue"
type="text"
placeholder="value (0)"
disabled
/>
<select class="customForce">
<option value="false">Do not disable website key bindings</option>
<option value="true">Disable website key bindings</option>
</select>
<div class="shortcuts-grid">
<div class="shortcut-row" id="display" data-action="display">
<div class="shortcut-label">Show/hide controller</div>
<input
class="customKey"
type="text"
value=""
placeholder="press a key"
/>
<input class="customValue" type="text" placeholder="value" value="N/A" disabled />
</div>
<div class="shortcut-row" id="move" data-action="move">
<div class="shortcut-label">Move controller</div>
<input
class="customKey"
type="text"
value=""
placeholder="press a key"
/>
<input
class="customValue"
type="text"
placeholder="value"
value="N/A"
disabled
/>
</div>
<div class="shortcut-row" id="slower" data-action="slower">
<div class="shortcut-label">Decrease speed</div>
<input
class="customKey"
type="text"
value=""
placeholder="press a key"
/>
<input class="customValue" type="text" placeholder="value (0.10)" />
</div>
<div class="shortcut-row" id="faster" data-action="faster">
<div class="shortcut-label">Increase speed</div>
<input
class="customKey"
type="text"
value=""
placeholder="press a key"
/>
<input class="customValue" type="text" placeholder="value (0.10)" />
</div>
<div class="shortcut-row" id="rewind" data-action="rewind">
<div class="shortcut-label">Rewind</div>
<input
class="customKey"
type="text"
value=""
placeholder="press a key"
/>
<input class="customValue" type="text" placeholder="value (10)" />
</div>
<div class="shortcut-row" id="advance" data-action="advance">
<div class="shortcut-label">Advance</div>
<input
class="customKey"
type="text"
value=""
placeholder="press a key"
/>
<input class="customValue" type="text" placeholder="value (10)" />
</div>
<div class="shortcut-row" id="reset" data-action="reset">
<div class="shortcut-label">Reset speed</div>
<input
class="customKey"
type="text"
value=""
placeholder="press a key"
/>
<input
class="customValue"
type="text"
placeholder="value"
value="N/A"
disabled
/>
</div>
<div class="shortcut-row" id="fast" data-action="fast">
<div class="shortcut-label">Preferred speed</div>
<input
class="customKey"
type="text"
value=""
placeholder="press a key"
/>
<input class="customValue" type="text" placeholder="value (1.80)" />
</div>
<div class="shortcut-row" id="toggleSubtitleNudge" data-action="toggleSubtitleNudge">
<div class="shortcut-label">Toggle subtitle nudge</div>
<input
class="customKey"
type="text"
value=""
placeholder="press a key"
/>
<input
class="customValue"
type="text"
placeholder="value"
value="N/A"
disabled
/>
</div>
</div>
<button id="add">Add New</button>
<select id="addShortcutSelector">
<option value="">Add shortcut...</option>
</select>
</section>
<section>
@@ -221,23 +174,87 @@
<label for="controllerOpacity">Controller opacity</label>
<input id="controllerOpacity" type="text" value="" />
</div>
<div class="row">
<label for="blacklist"
>Sites on which extension is disabled<br />
(one per line)<br />
<br />
<em>
<a href="https://www.regexpal.com/">Regex</a> is supported.<br />
Be sure it is in "//g" format.<br />
ie: /(.+)youtube\.com(\/*)$/gi
</em>
</label>
<textarea id="blacklist" rows="10" cols="50"></textarea>
</div>
</section>
<section id="siteRules">
<h3>Site-Specific Settings</h3>
<p>
<em
>Override default settings for specific websites. Supports
<a href="https://www.regexpal.com/">Regex</a> patterns (e.g.,
/(.+)youtube\.com(\/*)$/gi).</em
>
</p>
<div id="siteRulesContainer"></div>
<button id="addSiteRule" type="button">Add Site Rule</button>
</section>
<template id="siteRuleTemplate">
<div class="site-rule">
<div class="site-rule-header">
<button type="button" class="toggle-site-rule" title="Expand/Collapse">+</button>
<input
type="text"
class="site-pattern"
placeholder="e.g., youtube.com or /regex/gi"
/>
<button type="button" class="remove-site-rule">Remove</button>
</div>
<div class="site-rule-body">
<div class="site-rule-option">
<label>
<input type="checkbox" class="site-enabled" />
Enable Video Speed Controller on this site
</label>
</div>
<div class="site-rule-content">
<div class="site-rule-option">
<label>Hide controller by default:</label>
<input type="checkbox" class="site-startHidden" />
</div>
<div class="site-rule-option">
<label>Default controller location:</label>
<select class="site-controllerLocation">
<option value="top-left">Top left</option>
<option value="top-center">Top center</option>
<option value="top-right">Top right</option>
<option value="middle-right">Middle right</option>
<option value="bottom-right">Bottom right</option>
<option value="bottom-center">Bottom center</option>
<option value="bottom-left">Bottom left</option>
<option value="middle-left">Middle left</option>
</select>
</div>
<div class="site-rule-option">
<label>Remember playback speed:</label>
<input type="checkbox" class="site-rememberSpeed" />
</div>
<div class="site-rule-option">
<label>Force last saved speed:</label>
<input type="checkbox" class="site-forceLastSavedSpeed" />
</div>
<div class="site-rule-option">
<label>Work on audio:</label>
<input type="checkbox" class="site-audioBoolean" />
</div>
<div class="site-rule-option">
<label>Controller opacity:</label>
<input type="text" class="site-controllerOpacity" />
</div>
<div class="site-rule-shortcuts">
<label>
<input type="checkbox" class="override-shortcuts" />
Custom shortcuts for this site
</label>
<div class="site-shortcuts-container" style="display: none"></div>
</div>
</div>
</div>
</div>
</template>
<section id="nudgeSettings">
<h3>Subtitle Nudge Settings (Experimental - YouTube Only)</h3>
<h3>Subtitle Nudge Settings</h3>
<div class="row">
<label for="enableSubtitleNudge"
>Enable Subtitle Nudge <br /><em
@@ -261,25 +278,10 @@
placeholder="50"
/>
</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="restore">Restore Defaults</button>
<button id="experimental">Show Experimental Features</button>
<div id="status"></div>
+448 -114
View File
@@ -165,26 +165,82 @@ var tcDefaults = {
createDefaultBinding("fast", "G", 71, 1.8),
createDefaultBinding("toggleSubtitleNudge", "N", 78, 0)
],
blacklist: `www.instagram.com
twitter.com
imgur.com
teams.microsoft.com
`.replace(regStrip, ""),
siteRules: [
{ pattern: "www.instagram.com", disableExtension: true },
{ pattern: "x.com", disableExtension: true },
{ pattern: "imgur.com", disableExtension: true },
{ pattern: "teams.microsoft.com", disableExtension: true }
],
enableSubtitleNudge: true,
subtitleNudgeInterval: 50,
subtitleNudgeAmount: 0.001
};
var customActionsNoValues = [
"pause",
"muted",
"mark",
"jump",
const actionLabels = {
display: "Show/hide controller",
move: "Move controller",
slower: "Decrease speed",
faster: "Increase speed",
rewind: "Rewind",
advance: "Advance",
reset: "Reset speed",
fast: "Preferred speed",
muted: "Mute",
pause: "Pause",
mark: "Set marker",
jump: "Jump to marker",
toggleSubtitleNudge: "Toggle subtitle nudge"
};
const customActionsNoValues = [
"reset",
"display",
"move",
"muted",
"pause",
"mark",
"jump",
"toggleSubtitleNudge"
];
function refreshAddShortcutSelector() {
const selector = document.getElementById("addShortcutSelector");
if (!selector) return;
// Clear existing options except the first one
while (selector.options.length > 1) {
selector.remove(1);
}
// Find all currently used actions
const usedActions = new Set();
document.querySelectorAll(".shortcut-row").forEach((row) => {
const action = row.dataset.action;
if (action) {
usedActions.add(action);
}
});
// Add all unused actions
Object.keys(actionLabels).forEach((action) => {
if (!usedActions.has(action)) {
const option = document.createElement("option");
option.value = action;
option.text = actionLabels[action];
selector.appendChild(option);
}
});
// If no available actions, hide or disable the selector
if (selector.options.length === 1) {
selector.disabled = true;
selector.options[0].text = "All shortcuts added";
} else {
selector.disabled = false;
selector.options[0].text = "Add shortcut...";
}
}
function ensureDefaultBinding(storage, action, key, keyCode, value) {
if (storage.keyBindings.some((item) => item.action === action)) return;
@@ -389,27 +445,16 @@ function appendSelectOptions(select, options) {
});
}
function add_shortcut() {
var div = document.createElement("div");
div.setAttribute("class", "row customs");
function add_shortcut(action, value) {
if (!action) return;
var actionSelect = document.createElement("select");
actionSelect.className = "customDo";
appendSelectOptions(actionSelect, [
{ value: "slower", label: "Decrease speed" },
{ value: "faster", label: "Increase speed" },
{ value: "rewind", label: "Rewind" },
{ value: "advance", label: "Advance" },
{ value: "reset", label: "Reset speed" },
{ value: "fast", label: "Preferred speed" },
{ value: "muted", label: "Mute" },
{ value: "pause", label: "Pause" },
{ value: "mark", label: "Set marker" },
{ value: "jump", label: "Jump to marker" },
{ value: "display", label: "Show/hide controller" },
{ value: "move", label: "Move controller" },
{ value: "toggleSubtitleNudge", label: "Toggle subtitle nudge" }
]);
var div = document.createElement("div");
div.setAttribute("class", "shortcut-row customs");
div.dataset.action = action;
var actionLabel = document.createElement("div");
actionLabel.className = "shortcut-label";
actionLabel.textContent = actionLabels[action] || action;
var keyInput = document.createElement("input");
keyInput.className = "customKey";
@@ -419,41 +464,32 @@ function add_shortcut() {
var valueInput = document.createElement("input");
valueInput.className = "customValue";
valueInput.type = "text";
valueInput.placeholder = "value (0.10)";
var forceSelect = document.createElement("select");
forceSelect.className = "customForce";
appendSelectOptions(forceSelect, [
{
value: "false",
label: "Do not disable website key bindings"
},
{
value: "true",
label: "Disable website key bindings"
}
]);
valueInput.placeholder = "value";
if (customActionsNoValues.includes(action)) {
valueInput.value = "N/A";
valueInput.disabled = true;
} else {
valueInput.value = value || 0;
}
var removeButton = document.createElement("button");
removeButton.className = "removeParent";
removeButton.type = "button";
removeButton.textContent = "X";
div.appendChild(actionSelect);
div.appendChild(actionLabel);
div.appendChild(keyInput);
div.appendChild(valueInput);
div.appendChild(forceSelect);
div.appendChild(removeButton);
var customsElement = document.getElementById("customs");
customsElement.insertBefore(
div,
customsElement.children[customsElement.childElementCount - 1]
);
var customsElement = document.querySelector(".shortcuts-grid");
customsElement.appendChild(div);
refreshAddShortcutSelector();
}
function createKeyBindings(item) {
var action = item.querySelector(".customDo").value;
var action = item.dataset.action || item.querySelector(".customDo").value;
var input = item.querySelector(".customKey");
var valueInput = item.querySelector(".customValue");
var predefined = !!item.id;
@@ -479,7 +515,7 @@ function createKeyBindings(item) {
value: customActionsNoValues.includes(action)
? 0
: Number(valueInput.value),
force: item.querySelector(".customForce").value,
force: false,
predefined: predefined
});
@@ -489,22 +525,27 @@ function createKeyBindings(item) {
function validate() {
var valid = true;
var status = document.getElementById("status");
document
.getElementById("blacklist")
.value.split("\n")
.forEach((match) => {
match = match.replace(regStrip, "");
if (match.startsWith("/")) {
try {
new RegExp(match);
} catch (err) {
status.textContent =
"Error: Invalid blacklist regex: " + match + ". Unable to save";
valid = false;
return;
// Validate site rules patterns
document.querySelectorAll(".site-rule").forEach((ruleEl) => {
var pattern = ruleEl.querySelector(".site-pattern").value.trim();
if (pattern.length === 0) return;
if (pattern.startsWith("/")) {
try {
var lastSlash = pattern.lastIndexOf("/");
if (lastSlash > 0) {
new RegExp(pattern.substring(1, lastSlash), pattern.substring(lastSlash + 1));
}
} catch (err) {
status.textContent =
"Error: Invalid site rule regex: " + pattern + ". Unable to save";
valid = false;
return;
}
});
}
});
return valid;
}
@@ -538,18 +579,13 @@ function save_options() {
);
settings.controllerOpacity =
document.getElementById("controllerOpacity").value;
settings.blacklist = document
.getElementById("blacklist")
.value.replace(regStrip, "");
settings.keyBindings = keyBindings;
settings.enableSubtitleNudge =
document.getElementById("enableSubtitleNudge").checked;
settings.subtitleNudgeInterval =
parseInt(document.getElementById("subtitleNudgeInterval").value, 10) ||
tcDefaults.subtitleNudgeInterval;
settings.subtitleNudgeAmount =
parseFloat(document.getElementById("subtitleNudgeAmount").value) ||
tcDefaults.subtitleNudgeAmount;
settings.subtitleNudgeAmount = tcDefaults.subtitleNudgeAmount;
if (settings.subtitleNudgeInterval < 10) {
settings.subtitleNudgeInterval = 10;
@@ -557,12 +593,65 @@ function save_options() {
if (settings.subtitleNudgeInterval > 1000) {
settings.subtitleNudgeInterval = 1000;
}
if (
settings.subtitleNudgeAmount <= 0 ||
settings.subtitleNudgeAmount > 0.1
) {
settings.subtitleNudgeAmount = tcDefaults.subtitleNudgeAmount;
}
// Collect site rules
settings.siteRules = [];
document.querySelectorAll(".site-rule").forEach((ruleEl) => {
var pattern = ruleEl.querySelector(".site-pattern").value.trim();
if (pattern.length === 0) return;
var rule = { pattern: pattern };
// Handle Enable toggle
rule.enabled = ruleEl.querySelector(".site-enabled").checked;
// Handle other site settings
const siteSettings = [
{ key: "startHidden", type: "checkbox" },
{ key: "controllerLocation", type: "select" },
{ key: "rememberSpeed", type: "checkbox" },
{ key: "forceLastSavedSpeed", type: "checkbox" },
{ key: "audioBoolean", type: "checkbox" },
{ key: "controllerOpacity", type: "text" }
];
siteSettings.forEach((s) => {
var input = ruleEl.querySelector(`.site-${s.key}`);
if (s.type === "checkbox") {
rule[s.key] = input.checked;
} else {
rule[s.key] = input.value;
}
});
if (ruleEl.querySelector(".override-shortcuts").checked) {
var shortcuts = [];
ruleEl.querySelectorAll(".site-shortcuts-container .customs").forEach((shortcutRow) => {
var action = shortcutRow.dataset.action;
var keyInput = shortcutRow.querySelector(".customKey");
var valueInput = shortcutRow.querySelector(".customValue");
var forceCheckbox = shortcutRow.querySelector(".customForce");
var binding = normalizeStoredBinding(keyInput.vscBinding);
if (binding) {
shortcuts.push({
action: action,
key: binding.key,
keyCode: binding.keyCode,
code: binding.code,
disabled: binding.disabled === true,
value: customActionsNoValues.includes(action)
? 0
: Number(valueInput.value),
force: forceCheckbox ? forceCheckbox.checked : false
});
}
});
if (shortcuts.length > 0) rule.shortcuts = shortcuts;
}
settings.siteRules.push(rule);
});
chrome.storage.sync.remove([
"resetSpeed",
@@ -575,7 +664,8 @@ function save_options() {
"fasterKeyCode",
"rewindKeyCode",
"advanceKeyCode",
"fastKeyCode"
"fastKeyCode",
"blacklist"
]);
chrome.storage.sync.set(settings, function () {
@@ -604,6 +694,194 @@ function ensureSubtitleNudgeBinding(storage) {
ensureDefaultBinding(storage, "toggleSubtitleNudge", "N", 78, 0);
}
function migrateLegacyBlacklist(storage) {
if (!storage.blacklist || typeof storage.blacklist !== "string") {
return [];
}
var siteRules = [];
var lines = storage.blacklist.split("\n");
lines.forEach((line) => {
var pattern = line.replace(regStrip, "");
if (pattern.length === 0) return;
siteRules.push({
pattern: pattern,
disableExtension: true
});
});
return siteRules;
}
function addSiteRuleShortcut(container, action, binding, value, force) {
var div = document.createElement("div");
div.setAttribute("class", "shortcut-row customs");
div.dataset.action = action;
var actionLabel = document.createElement("div");
actionLabel.className = "shortcut-label";
var actionLabels = {
display: "Show/hide controller",
move: "Move controller",
slower: "Decrease speed",
faster: "Increase speed",
rewind: "Rewind",
advance: "Advance",
reset: "Reset speed",
fast: "Preferred speed",
muted: "Mute",
pause: "Pause",
mark: "Set marker",
jump: "Jump to marker",
toggleSubtitleNudge: "Toggle subtitle nudge"
};
var actionLabelText = actionLabels[action] || action;
if (action === "toggleSubtitleNudge") {
// Check if the site rule is for YouTube.
// We look up the pattern from the site rule element this container belongs to.
var ruleEl = container.closest(".site-rule");
var pattern = ruleEl ? ruleEl.querySelector(".site-pattern").value : "";
if (!pattern.toLowerCase().includes("youtube.com")) {
actionLabelText += " (only for YouTube embeds)";
}
}
actionLabel.textContent = actionLabelText;
var keyInput = document.createElement("input");
keyInput.className = "customKey";
keyInput.type = "text";
keyInput.placeholder = "press a key";
updateCustomShortcutInputText(keyInput, binding || createDisabledBinding());
var valueInput = document.createElement("input");
valueInput.className = "customValue";
valueInput.type = "text";
valueInput.placeholder = "value (0.10)";
valueInput.value = value || 0;
if (customActionsNoValues.includes(action)) {
valueInput.disabled = true;
}
var forceLabel = document.createElement("label");
forceLabel.className = "force-label";
forceLabel.title = "Prevent website from capturing this key";
var forceCheckbox = document.createElement("input");
forceCheckbox.type = "checkbox";
forceCheckbox.className = "customForce";
forceCheckbox.checked = force === true || force === "true";
var forceText = document.createElement("span");
forceText.textContent = "Block site from capturing keypress";
forceText.className = "force-text";
forceLabel.appendChild(forceCheckbox);
forceLabel.appendChild(forceText);
div.appendChild(actionLabel);
div.appendChild(keyInput);
div.appendChild(valueInput);
div.appendChild(forceLabel);
container.appendChild(div);
}
function createSiteRule(rule) {
var template = document.getElementById("siteRuleTemplate");
var clone = template.content.cloneNode(true);
var ruleEl = clone.querySelector(".site-rule");
var pattern = rule && rule.pattern ? rule.pattern : "";
ruleEl.querySelector(".site-pattern").value = pattern;
// Make the rule body collapsed by default
var ruleBody = ruleEl.querySelector(".site-rule-body");
ruleBody.style.display = "none";
ruleEl.classList.add("collapsed");
var enabledCheckbox = ruleEl.querySelector(".site-enabled");
var contentEl = ruleEl.querySelector(".site-rule-content");
function updateDisabledState() {
if (enabledCheckbox.checked) {
contentEl.classList.remove("disabled-rule");
} else {
contentEl.classList.add("disabled-rule");
}
}
enabledCheckbox.addEventListener("change", updateDisabledState);
if (rule) {
if (rule.enabled !== undefined) {
enabledCheckbox.checked = rule.enabled;
} else if (rule.disableExtension !== undefined) {
enabledCheckbox.checked = !rule.disableExtension;
} else {
enabledCheckbox.checked = true;
}
} else {
enabledCheckbox.checked = true;
}
updateDisabledState();
const settings = [
{ key: "startHidden", type: "checkbox" },
{ key: "controllerLocation", type: "select" },
{ key: "rememberSpeed", type: "checkbox" },
{ key: "forceLastSavedSpeed", type: "checkbox" },
{ key: "audioBoolean", type: "checkbox" },
{ key: "controllerOpacity", type: "text" }
];
settings.forEach((s) => {
var input = ruleEl.querySelector(`.site-${s.key}`);
var value;
if (rule && rule[s.key] !== undefined) {
value = rule[s.key];
} else {
// Initialize with current global value
if (s.type === "checkbox") {
value = document.getElementById(s.key).checked;
} else {
value = document.getElementById(s.key).value;
}
}
if (s.type === "checkbox") {
input.checked = value;
} else {
input.value = value;
}
});
if (rule && Array.isArray(rule.shortcuts) && rule.shortcuts.length > 0) {
ruleEl.querySelector(".override-shortcuts").checked = true;
var container = ruleEl.querySelector(".site-shortcuts-container");
container.style.display = "block";
rule.shortcuts.forEach((shortcut) => {
addSiteRuleShortcut(
container,
shortcut.action,
shortcut,
shortcut.value,
shortcut.force
);
});
}
document.getElementById("siteRulesContainer").appendChild(ruleEl);
}
function populateDefaultSiteShortcuts(container) {
tcDefaults.keyBindings.forEach((binding) => {
addSiteRuleShortcut(container, binding.action, binding, binding.value, false);
});
}
function restore_options() {
chrome.storage.sync.get(tcDefaults, function (storage) {
document.getElementById("rememberSpeed").checked = storage.rememberSpeed;
@@ -616,13 +894,10 @@ function restore_options() {
normalizeControllerLocation(storage.controllerLocation);
document.getElementById("controllerOpacity").value =
storage.controllerOpacity;
document.getElementById("blacklist").value = storage.blacklist;
document.getElementById("enableSubtitleNudge").checked =
storage.enableSubtitleNudge;
document.getElementById("subtitleNudgeInterval").value =
storage.subtitleNudgeInterval;
document.getElementById("subtitleNudgeAmount").value =
storage.subtitleNudgeAmount;
if (!Array.isArray(storage.keyBindings) || storage.keyBindings.length === 0) {
storage.keyBindings = tcDefaults.keyBindings.slice();
@@ -639,31 +914,48 @@ function restore_options() {
item.action === "display"
? storage.displayKeyCode || tcDefaults.displayKeyCode
: undefined;
var normalizedBinding = normalizeStoredBinding(item, fallbackKeyCode);
var row;
var row = document.getElementById(item.action);
var normalizedBinding = normalizeStoredBinding(item);
if (item.predefined) {
row = document.getElementById(item.action);
} else {
add_shortcut();
row = document.querySelector(".customs:last-of-type");
row.querySelector(".customDo").value = item.action;
if (!row) {
add_shortcut(item.action, item.value);
row = document.querySelector(".shortcut-row.customs:last-of-type");
}
if (!row) return;
var valueInput = row.querySelector(".customValue");
if (customActionsNoValues.includes(item.action)) {
valueInput.disabled = true;
if (valueInput) {
valueInput.value = "N/A";
valueInput.disabled = true;
}
} else if (valueInput) {
valueInput.value = item.value;
}
updateCustomShortcutInputText(
row.querySelector(".customKey"),
normalizedBinding || createDisabledBinding()
);
valueInput.value = item.value;
row.querySelector(".customForce").value = String(item.force);
});
refreshAddShortcutSelector();
// Load site rules (use defaults if none in storage or if storage has empty array)
var siteRules = Array.isArray(storage.siteRules) && storage.siteRules.length > 0
? storage.siteRules
: (storage.blacklist ? migrateLegacyBlacklist(storage) : (tcDefaults.siteRules || []));
// If we migrated from blacklist, save the new format
if (storage.blacklist && siteRules.length > 0) {
chrome.storage.sync.set({ siteRules: siteRules });
chrome.storage.sync.remove(["blacklist"]);
}
document.getElementById("siteRulesContainer").innerHTML = "";
if (siteRules.length > 0) {
siteRules.forEach((rule) => {
if (rule && rule.pattern) {
createSiteRule(rule);
}
});
}
});
}
@@ -680,12 +972,6 @@ function restore_defaults() {
});
}
function show_experimental() {
document
.querySelectorAll(".customForce")
.forEach((item) => (item.style.display = "inline-block"));
}
document.addEventListener("DOMContentLoaded", function () {
var manifest = chrome.runtime.getManifest();
var versionElement = document.getElementById("app-version");
@@ -695,13 +981,24 @@ document.addEventListener("DOMContentLoaded", function () {
restore_options();
document.getElementById("save").addEventListener("click", save_options);
document.getElementById("add").addEventListener("click", add_shortcut);
const addSelector = document.getElementById("addShortcutSelector");
if (addSelector) {
addSelector.addEventListener("change", function (e) {
if (e.target.value) {
add_shortcut(e.target.value);
e.target.value = ""; // Reset selector
}
});
}
document
.getElementById("restore")
.addEventListener("click", restore_defaults);
document
.getElementById("experimental")
.addEventListener("click", show_experimental);
.getElementById("addSiteRule")
.addEventListener("click", function () {
createSiteRule(null);
});
function eventCaller(event, className, funcName) {
if (!event.target.classList || !event.target.classList.contains(className)) {
@@ -722,13 +1019,35 @@ document.addEventListener("DOMContentLoaded", function () {
document.addEventListener("keydown", (event) =>
eventCaller(event, "customKey", recordKeyPress)
);
document.addEventListener("click", (event) =>
eventCaller(event, "removeParent", function () {
document.addEventListener("click", (event) => {
if (event.target.classList.contains("removeParent")) {
event.target.parentNode.remove();
})
);
refreshAddShortcutSelector();
return;
}
if (event.target.classList.contains("remove-site-rule")) {
event.target.closest(".site-rule").remove();
return;
}
if (event.target.classList.contains("toggle-site-rule")) {
var ruleEl = event.target.closest(".site-rule");
var ruleBody = ruleEl.querySelector(".site-rule-body");
var isCollapsed = ruleEl.classList.contains("collapsed");
if (isCollapsed) {
ruleBody.style.display = "block";
ruleEl.classList.remove("collapsed");
event.target.textContent = "-";
} else {
ruleBody.style.display = "none";
ruleEl.classList.add("collapsed");
event.target.textContent = "+";
}
return;
}
});
document.addEventListener("change", (event) => {
eventCaller(event, "customDo", function () {
if (event.target.classList.contains("customDo")) {
var valueInput = event.target.nextElementSibling.nextElementSibling;
if (customActionsNoValues.includes(event.target.value)) {
valueInput.disabled = true;
@@ -736,6 +1055,21 @@ document.addEventListener("DOMContentLoaded", function () {
} else {
valueInput.disabled = false;
}
});
}
// Handle site rule override checkboxes
if (event.target.classList.contains("override-shortcuts")) {
var container = event.target
.closest(".site-rule-shortcuts")
.querySelector(".site-shortcuts-container");
if (event.target.checked) {
container.style.display = "block";
if (container.children.length === 0) {
populateDefaultSiteShortcuts(container);
}
} else {
container.style.display = "none";
}
}
});
});