Compare commits

..

13 Commits

7 changed files with 219 additions and 50 deletions
+33 -11
View File
@@ -121,7 +121,7 @@ var controllerButtonDefs = {
faster: { label: "", className: "" }, faster: { label: "", className: "" },
advance: { label: "", className: "rw" }, advance: { label: "", className: "rw" },
display: { label: "", className: "hideButton" }, display: { label: "", className: "hideButton" },
reset: { label: "", className: "" }, reset: { label: "\u21BB", className: "" },
fast: { label: "", className: "" }, fast: { label: "", className: "" },
settings: { label: "", className: "" }, settings: { label: "", className: "" },
pause: { label: "", className: "" }, pause: { label: "", className: "" },
@@ -778,6 +778,15 @@ function setSubtitleNudgeEnabledForVideo(video, enabled) {
function subtitleNudgeIconMarkup(isEnabled) { function subtitleNudgeIconMarkup(isEnabled) {
var action = isEnabled ? "subtitleNudgeOn" : "subtitleNudgeOff"; var action = isEnabled ? "subtitleNudgeOn" : "subtitleNudgeOff";
var custom =
tc.settings.customButtonIcons &&
tc.settings.customButtonIcons[action] &&
tc.settings.customButtonIcons[action].svg;
if (custom) {
return (
'<span class="vsc-btn-icon" aria-hidden="true">' + custom + "</span>"
);
}
if (typeof vscIconSvgString !== "function") { if (typeof vscIconSvgString !== "function") {
return isEnabled ? "✓" : "×"; return isEnabled ? "✓" : "×";
} }
@@ -1367,6 +1376,7 @@ chrome.storage.sync.get(tc.settings, function (storage) {
btn.textContent = (cdf2 && cdf2.label) || "?"; btn.textContent = (cdf2 && cdf2.label) || "?";
} }
}); });
updateSubtitleNudgeIndicator(video);
}); });
} }
}); });
@@ -1939,14 +1949,20 @@ function defineVideoController() {
if (subtitleNudgeIndicator) { if (subtitleNudgeIndicator) {
updateSubtitleNudgeIndicator(this.video); updateSubtitleNudgeIndicator(this.video);
} }
function blurAfterPointerTap(target, e) {
if (!target || typeof target.blur !== "function") return;
var pt = e.pointerType;
if (pt === "mouse" || pt === "touch" || (!pt && e.detail > 0)) {
requestAnimationFrame(function () {
target.blur();
});
}
}
dragHandle.addEventListener( dragHandle.addEventListener(
"mousedown", "mousedown",
(e) => { (e) => {
runAction( var dragAction = dragHandle.dataset.action;
e.target.dataset["action"], runAction(dragAction, getKeyBindings(dragAction, "value"), e);
getKeyBindings(e.target.dataset["action"], "value"),
e
);
e.stopPropagation(); e.stopPropagation();
}, },
true true
@@ -1955,11 +1971,9 @@ function defineVideoController() {
button.addEventListener( button.addEventListener(
"click", "click",
(e) => { (e) => {
runAction( var action = button.dataset.action;
e.target.dataset["action"], runAction(action, getKeyBindings(action), e);
getKeyBindings(e.target.dataset["action"]), blurAfterPointerTap(button, e);
e
);
e.stopPropagation(); e.stopPropagation();
}, },
true true
@@ -1974,6 +1988,7 @@ function defineVideoController() {
var newState = !isSubtitleNudgeEnabledForVideo(video); var newState = !isSubtitleNudgeEnabledForVideo(video);
setSubtitleNudgeEnabledForVideo(video, newState); setSubtitleNudgeEnabledForVideo(video, newState);
} }
blurAfterPointerTap(subtitleNudgeIndicator, e);
e.stopPropagation(); e.stopPropagation();
}, },
true true
@@ -2667,6 +2682,7 @@ function runAction(action, value, e) {
"mark", "mark",
"jump", "jump",
"drag", "drag",
"nudge",
"toggleSubtitleNudge", "toggleSubtitleNudge",
"display" "display"
]; ];
@@ -2782,6 +2798,12 @@ function runAction(action, value, e) {
case "toggleSubtitleNudge": case "toggleSubtitleNudge":
setSubtitleNudgeEnabledForVideo(v, subtitleNudgeToggleValue); setSubtitleNudgeEnabledForVideo(v, subtitleNudgeToggleValue);
break; break;
case "nudge":
setSubtitleNudgeEnabledForVideo(
v,
!isSubtitleNudgeEnabledForVideo(v)
);
break;
} }
}); });
log("runAction End", 5); log("runAction End", 5);
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "Speeder", "name": "Speeder",
"short_name": "Speeder", "short_name": "Speeder",
"version": "5.1.1", "version": "5.1.4",
"manifest_version": 2, "manifest_version": 2,
"description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts (New and improved version of \"Video Speed Controller\")", "description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts (New and improved version of \"Video Speed Controller\")",
"homepage_url": "https://github.com/SoPat712/speeder", "homepage_url": "https://github.com/SoPat712/speeder",
+45
View File
@@ -545,6 +545,51 @@ label em {
flex-shrink: 0; flex-shrink: 0;
} }
.cb-icon.cb-icon-nudge-pair {
width: auto;
min-width: 0;
padding: 0 4px;
gap: 4px;
background: transparent;
border: none;
}
.cb-nudge-chip {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 6px;
flex-shrink: 0;
color: #fff;
}
.cb-nudge-chip[data-nudge-state="on"] {
background: #4b9135;
border: 1px solid #6ec754;
}
.cb-nudge-chip[data-nudge-state="off"] {
background: #943e3e;
border: 1px solid #c06060;
}
.cb-nudge-chip .vsc-btn-icon svg,
.cb-nudge-chip svg {
width: 14px;
height: 14px;
flex-shrink: 0;
}
.cb-nudge-sep {
font-size: 11px;
font-weight: 600;
opacity: 0.45;
color: var(--text);
flex-shrink: 0;
}
.row-lucide-pair select { .row-lucide-pair select {
justify-self: end; justify-self: end;
} }
+21 -20
View File
@@ -272,11 +272,6 @@
</label> </label>
<input id="hideWithControlsTimer" type="text" placeholder="2" /> <input id="hideWithControlsTimer" type="text" placeholder="2" />
</div> </div>
<div class="row row-checkbox">
<label for="showPopupControlBar">Show popup control bar</label>
<input id="showPopupControlBar" type="checkbox" />
</div>
<div class="defaults-divider"></div> <div class="defaults-divider"></div>
<h4 class="defaults-sub-heading">Subtitle sync</h4> <h4 class="defaults-sub-heading">Subtitle sync</h4>
@@ -350,7 +345,11 @@
Configure which buttons appear in the browser popup control bar. Configure which buttons appear in the browser popup control bar.
</p> </p>
</div> </div>
<div class="row"> <div class="row row-checkbox">
<label for="showPopupControlBar">Show popup control bar</label>
<input id="showPopupControlBar" type="checkbox" />
</div>
<div class="row row-checkbox">
<label for="popupMatchHoverControls">Match hover controls</label> <label for="popupMatchHoverControls">Match hover controls</label>
<input id="popupMatchHoverControls" type="checkbox" /> <input id="popupMatchHoverControls" type="checkbox" />
</div> </div>
@@ -383,9 +382,11 @@
rel="noopener noreferrer" rel="noopener noreferrer"
>Lucide</a >Lucide</a
> >
set (fetched from jsDelivr). Chosen SVGs are cached in local set (fetched from jsDelivr). Custom icons are cached in local
storage and included in settings export. storage and included when you export settings. Subtitle nudge
<strong>Reset speed</strong> stays numeric text only. icons use two menu entries (enabled and disabled), not the bar
block id
<code>nudge</code>.
</p> </p>
</div> </div>
<div class="row row-lucide-pair"> <div class="row row-lucide-pair">
@@ -677,6 +678,17 @@
<div id="status" role="status" aria-live="polite"></div> <div id="status" role="status" aria-live="polite"></div>
</section> </section>
<section id="faq" class="settings-card info-card">
<h4>Extension controls not appearing?</h4>
<p>
This extension only works with HTML5 audio and video. If the
controls never appear, you may be looking at Flash content instead.
Right-click the player to check: if the menu mentions Flash, that
is the issue. Most sites will fall back to HTML5 when Flash is not
available, so disabling Flash in the browser can help.
</p>
</section>
<footer class="support-footer settings-card"> <footer class="support-footer settings-card">
<p> <p>
If Speeder has been useful, consider supporting its development via If Speeder has been useful, consider supporting its development via
@@ -695,17 +707,6 @@
>. >.
</p> </p>
</footer> </footer>
<section id="faq" class="settings-card info-card">
<h4>Extension controls not appearing?</h4>
<p>
This extension only works with HTML5 audio and video. If the
controls never appear, you may be looking at Flash content instead.
Right-click the player to check: if the menu mentions Flash, that
is the issue. Most sites will fall back to HTML5 when Flash is not
available, so disabling Flash in the browser can help.
</p>
</section>
</main> </main>
</div> </div>
</body> </body>
+95 -10
View File
@@ -138,7 +138,7 @@ var controllerButtonDefs = {
faster: { icon: "+", name: "Increase speed" }, faster: { icon: "+", name: "Increase speed" },
advance: { icon: "\u00BB", name: "Advance" }, advance: { icon: "\u00BB", name: "Advance" },
display: { icon: "\u00D7", name: "Close controller" }, display: { icon: "\u00D7", name: "Close controller" },
reset: { icon: "", name: "Reset speed" }, reset: { icon: "\u21BB", name: "Reset speed" },
fast: { icon: "\u2605", name: "Preferred speed" }, fast: { icon: "\u2605", name: "Preferred speed" },
nudge: { icon: "\u2713", name: "Subtitle nudge" }, nudge: { icon: "\u2713", name: "Subtitle nudge" },
settings: { icon: "\u2699", name: "Settings" }, settings: { icon: "\u2699", name: "Settings" },
@@ -147,12 +147,64 @@ var controllerButtonDefs = {
mark: { icon: "\u2691", name: "Set marker" }, mark: { icon: "\u2691", name: "Set marker" },
jump: { icon: "\u21E5", name: "Jump to marker" } jump: { icon: "\u21E5", name: "Jump to marker" }
}; };
var popupExcludedButtonIds = new Set(["settings"]);
/** Lucide picker only — not control-bar blocks (chip uses subtitleNudgeOn/Off). */
var lucideSubtitleNudgeActionLabels = {
subtitleNudgeOn: "Subtitle nudge — enabled",
subtitleNudgeOff: "Subtitle nudge — disabled"
};
function sanitizePopupButtonOrder(buttonIds) {
if (!Array.isArray(buttonIds)) return [];
var seen = new Set();
return buttonIds.filter(function (id) {
if (!controllerButtonDefs[id] || popupExcludedButtonIds.has(id) || seen.has(id)) {
return false;
}
seen.add(id);
return true;
});
}
/** Cached custom Lucide SVGs (mirrors chrome.storage.local customButtonIcons). */ /** Cached custom Lucide SVGs (mirrors chrome.storage.local customButtonIcons). */
var customButtonIconsLive = {}; var customButtonIconsLive = {};
function fillControlBarIconElement(icon, buttonId) { function fillControlBarIconElement(icon, buttonId) {
if (!icon || !buttonId) return; if (!icon || !buttonId) return;
if (buttonId === "nudge") {
icon.innerHTML = "";
icon.className = "cb-icon cb-icon-nudge-pair";
function nudgeChipMarkup(action) {
var c = customButtonIconsLive[action];
if (c && c.svg) return c.svg;
if (typeof vscIconSvgString === "function") {
return vscIconSvgString(action, 14) || "";
}
return "";
}
function appendChip(action, stateKey) {
var sp = document.createElement("span");
sp.className = "cb-nudge-chip";
sp.setAttribute("data-nudge-state", stateKey);
var inner = nudgeChipMarkup(action);
if (inner) {
var wrap = document.createElement("span");
wrap.className = "vsc-btn-icon";
wrap.innerHTML = inner;
sp.appendChild(wrap);
}
icon.appendChild(sp);
}
appendChip("subtitleNudgeOn", "on");
var sep = document.createElement("span");
sep.className = "cb-nudge-sep";
sep.textContent = "/";
icon.appendChild(sep);
appendChip("subtitleNudgeOff", "off");
return;
}
icon.className = "cb-icon";
var custom = customButtonIconsLive[buttonId]; var custom = customButtonIconsLive[buttonId];
if (custom && custom.svg) { if (custom && custom.svg) {
icon.innerHTML = custom.svg; icon.innerHTML = custom.svg;
@@ -713,7 +765,7 @@ function save_options() {
document.getElementById("showPopupControlBar").checked; document.getElementById("showPopupControlBar").checked;
settings.popupMatchHoverControls = settings.popupMatchHoverControls =
document.getElementById("popupMatchHoverControls").checked; document.getElementById("popupMatchHoverControls").checked;
settings.popupControllerButtons = getPopupControlBarOrder(); settings.popupControllerButtons = sanitizePopupButtonOrder(getPopupControlBarOrder());
// Collect site rules // Collect site rules
settings.siteRules = []; settings.siteRules = [];
@@ -802,7 +854,9 @@ function save_options() {
ruleEl.querySelector(".site-showPopupControlBar").checked; ruleEl.querySelector(".site-showPopupControlBar").checked;
var popupActiveZone = ruleEl.querySelector(".site-popup-cb-active"); var popupActiveZone = ruleEl.querySelector(".site-popup-cb-active");
if (popupActiveZone) { if (popupActiveZone) {
rule.popupControllerButtons = readControlBarOrder(popupActiveZone); rule.popupControllerButtons = sanitizePopupButtonOrder(
readControlBarOrder(popupActiveZone)
);
} }
} }
@@ -1071,7 +1125,10 @@ function createSiteRule(rule) {
populateControlBarZones( populateControlBarZones(
sitePopupActive, sitePopupActive,
sitePopupAvailable, sitePopupAvailable,
rule.popupControllerButtons sanitizePopupButtonOrder(rule.popupControllerButtons),
function (id) {
return !popupExcludedButtonIds.has(id);
}
); );
} else if ( } else if (
sitePopupActive && sitePopupActive &&
@@ -1081,7 +1138,10 @@ function createSiteRule(rule) {
populateControlBarZones( populateControlBarZones(
sitePopupActive, sitePopupActive,
sitePopupAvailable, sitePopupAvailable,
getPopupControlBarOrder() getPopupControlBarOrder(),
function (id) {
return !popupExcludedButtonIds.has(id);
}
); );
} }
} }
@@ -1139,16 +1199,23 @@ function createControlBarBlock(buttonId) {
return block; return block;
} }
function populateControlBarZones(activeZone, availableZone, activeIds) { function populateControlBarZones(activeZone, availableZone, activeIds, allowButtonId) {
activeZone.innerHTML = ""; activeZone.innerHTML = "";
availableZone.innerHTML = ""; availableZone.innerHTML = "";
var allowed = function (id) {
if (!controllerButtonDefs[id]) return false;
return typeof allowButtonId === "function" ? Boolean(allowButtonId(id)) : true;
};
activeIds.forEach(function (id) { activeIds.forEach(function (id) {
if (!allowed(id)) return;
var block = createControlBarBlock(id); var block = createControlBarBlock(id);
if (block) activeZone.appendChild(block); if (block) activeZone.appendChild(block);
}); });
Object.keys(controllerButtonDefs).forEach(function (id) { Object.keys(controllerButtonDefs).forEach(function (id) {
if (!allowed(id)) return;
if (!activeIds.includes(id)) { if (!activeIds.includes(id)) {
var block = createControlBarBlock(id); var block = createControlBarBlock(id);
if (block) availableZone.appendChild(block); if (block) availableZone.appendChild(block);
@@ -1176,15 +1243,21 @@ function getControlBarOrder() {
} }
function populatePopupControlBarEditor(activeIds) { function populatePopupControlBarEditor(activeIds) {
var popupActiveIds = sanitizePopupButtonOrder(activeIds);
populateControlBarZones( populateControlBarZones(
document.getElementById("popupControlBarActive"), document.getElementById("popupControlBarActive"),
document.getElementById("popupControlBarAvailable"), document.getElementById("popupControlBarAvailable"),
activeIds popupActiveIds,
function (id) {
return !popupExcludedButtonIds.has(id);
}
); );
} }
function getPopupControlBarOrder() { function getPopupControlBarOrder() {
return readControlBarOrder(document.getElementById("popupControlBarActive")); return sanitizePopupButtonOrder(
readControlBarOrder(document.getElementById("popupControlBarActive"))
);
} }
function updatePopupEditorDisabledState() { function updatePopupEditorDisabledState() {
@@ -1323,7 +1396,16 @@ function initLucideButtonIconsUI() {
actionSel.dataset.lucideInit = "1"; actionSel.dataset.lucideInit = "1";
actionSel.innerHTML = ""; actionSel.innerHTML = "";
Object.keys(controllerButtonDefs).forEach(function (aid) { Object.keys(controllerButtonDefs).forEach(function (aid) {
if (aid === "reset") return; if (aid === "nudge") {
Object.keys(lucideSubtitleNudgeActionLabels).forEach(function (subId) {
var o2 = document.createElement("option");
o2.value = subId;
o2.textContent =
lucideSubtitleNudgeActionLabels[subId] + " (" + subId + ")";
actionSel.appendChild(o2);
});
return;
}
var o = document.createElement("option"); var o = document.createElement("option");
o.value = aid; o.value = aid;
o.textContent = o.textContent =
@@ -1771,7 +1853,10 @@ document.addEventListener("DOMContentLoaded", function () {
populateControlBarZones( populateControlBarZones(
popupActiveZone, popupActiveZone,
popupAvailableZone, popupAvailableZone,
getPopupControlBarOrder() getPopupControlBarOrder(),
function (id) {
return !popupExcludedButtonIds.has(id);
}
); );
} }
} else { } else {
+20 -7
View File
@@ -8,8 +8,9 @@ document.addEventListener("DOMContentLoaded", function () {
faster: { label: "", className: "" }, faster: { label: "", className: "" },
advance: { label: "", className: "rw" }, advance: { label: "", className: "rw" },
display: { label: "", className: "hideButton" }, display: { label: "", className: "hideButton" },
reset: { label: "", className: "" }, reset: { label: "\u21BB", className: "" },
fast: { label: "", className: "" }, fast: { label: "", className: "" },
nudge: { label: "", className: "" },
settings: { label: "", className: "" }, settings: { label: "", className: "" },
pause: { label: "", className: "" }, pause: { label: "", className: "" },
muted: { label: "", className: "" }, muted: { label: "", className: "" },
@@ -18,6 +19,7 @@ document.addEventListener("DOMContentLoaded", function () {
}; };
var defaultButtons = ["rewind", "slower", "faster", "advance", "display"]; var defaultButtons = ["rewind", "slower", "faster", "advance", "display"];
var popupExcludedButtonIds = new Set(["settings"]);
var storageDefaults = { var storageDefaults = {
enabled: true, enabled: true,
showPopupControlBar: true, showPopupControlBar: true,
@@ -64,25 +66,37 @@ document.addEventListener("DOMContentLoaded", function () {
} }
function resolvePopupButtons(storage, siteRule) { function resolvePopupButtons(storage, siteRule) {
function sanitize(buttons) {
if (!Array.isArray(buttons)) return [];
var seen = new Set();
return buttons.filter(function (id) {
if (!controllerButtonDefs[id] || popupExcludedButtonIds.has(id) || seen.has(id)) {
return false;
}
seen.add(id);
return true;
});
}
if (siteRule && Array.isArray(siteRule.popupControllerButtons)) { if (siteRule && Array.isArray(siteRule.popupControllerButtons)) {
return siteRule.popupControllerButtons; return sanitize(siteRule.popupControllerButtons);
} }
if (storage.popupMatchHoverControls) { if (storage.popupMatchHoverControls) {
if (siteRule && Array.isArray(siteRule.controllerButtons)) { if (siteRule && Array.isArray(siteRule.controllerButtons)) {
return siteRule.controllerButtons; return sanitize(siteRule.controllerButtons);
} }
if (Array.isArray(storage.controllerButtons)) { if (Array.isArray(storage.controllerButtons)) {
return storage.controllerButtons; return sanitize(storage.controllerButtons);
} }
} }
if (Array.isArray(storage.popupControllerButtons)) { if (Array.isArray(storage.popupControllerButtons)) {
return storage.popupControllerButtons; return sanitize(storage.popupControllerButtons);
} }
return defaultButtons; return sanitize(defaultButtons);
} }
function setControlBarVisible(visible) { function setControlBarVisible(visible) {
@@ -209,7 +223,6 @@ document.addEventListener("DOMContentLoaded", function () {
var customMap = customIconsMap || {}; var customMap = customIconsMap || {};
buttons.forEach(function (btnId) { buttons.forEach(function (btnId) {
if (btnId === "nudge") return;
var def = controllerButtonDefs[btnId]; var def = controllerButtonDefs[btnId];
if (!def) return; if (!def) return;
+4 -1
View File
@@ -10,8 +10,11 @@
line-height: 1; line-height: 1;
} }
/* Show extra buttons on hover or keyboard :focus-visible only. Plain :focus-within
after a mouse click kept #controls visible while hover-only rules (e.g. draggable
margin) turned off when the pointer left the bar. */
#controller:hover #controls, #controller:hover #controls,
#controller:focus-within #controls, #controller:focus-within:has(:focus-visible) #controls,
:host(:hover) #controls { :host(:hover) #controls {
display: inline-flex; display: inline-flex;
vertical-align: middle; vertical-align: middle;