mirror of
https://github.com/SoPat712/videospeed.git
synced 2026-04-27 06:32:52 -04:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
53f66f1eeb
|
|||
|
f106ab490a
|
|||
|
5a38121e09
|
|||
|
36ed922b5c
|
|||
|
3275d1f322
|
|||
|
f6d706f096
|
|||
|
04292a8018
|
|||
|
8eb3901121
|
|||
|
0bcca24241
|
|||
|
6bf48fa479
|
|||
|
9b4f338ebb
|
|||
|
3a583ce3b8
|
|||
|
06f40b3d6d
|
@@ -46,7 +46,7 @@ jobs:
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: Beta ${{ github.ref_name }}
|
||||
name: ${{ github.ref_name }}
|
||||
files: ${{ steps.xpi.outputs.file }}
|
||||
prerelease: true
|
||||
body: |
|
||||
@@ -61,7 +61,9 @@ jobs:
|
||||
|
||||
# Stable tag (v* without -beta) → Sign & submit to public AMO listing
|
||||
- name: Sign & Submit to AMO (stable)
|
||||
if: startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name, '-beta')
|
||||
if:
|
||||
startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name,
|
||||
'-beta')
|
||||
run: |
|
||||
web-ext sign \
|
||||
--api-key ${{ secrets.FIREFOX_API_KEY }} \
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/* Runs via chrome.tabs.executeScript(allFrames) in the same isolated world as inject.js */
|
||||
(function () {
|
||||
try {
|
||||
if (
|
||||
typeof getPrimaryVideoElement !== "function" ||
|
||||
typeof computeResetButtonLabelForVideo !== "function"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
var v = getPrimaryVideoElement();
|
||||
if (!v) return null;
|
||||
return {
|
||||
speed: v.playbackRate,
|
||||
resetLabel: computeResetButtonLabelForVideo(v),
|
||||
preferred: !v.paused
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
@@ -8,6 +8,7 @@
|
||||
pointer-events: none !important;
|
||||
z-index: 2147483646 !important;
|
||||
white-space: normal;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Use minimal z-index for non-YouTube sites to avoid overlapping modals */
|
||||
|
||||
@@ -3,6 +3,15 @@ var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
|
||||
var isUserSeek = false; // Track if seek was user-initiated
|
||||
var lastToggleSpeed = {}; // Store last toggle speeds per video
|
||||
|
||||
function getPrimaryVideoElement() {
|
||||
if (!tc.mediaElements || tc.mediaElements.length === 0) return null;
|
||||
for (var i = 0; i < tc.mediaElements.length; i++) {
|
||||
var el = tc.mediaElements[i];
|
||||
if (el && !el.paused) return el;
|
||||
}
|
||||
return tc.mediaElements[0];
|
||||
}
|
||||
|
||||
var tc = {
|
||||
settings: {
|
||||
lastSpeed: 1.0,
|
||||
@@ -18,6 +27,10 @@ var tc = {
|
||||
hideWithControlsTimer: 2.0,
|
||||
controllerLocation: "top-left",
|
||||
controllerOpacity: 0.3,
|
||||
controllerMarginTop: 0,
|
||||
controllerMarginRight: 0,
|
||||
controllerMarginBottom: 65,
|
||||
controllerMarginLeft: 0,
|
||||
keyBindings: [],
|
||||
siteRules: [],
|
||||
controllerButtons: ["rewind", "slower", "faster", "advance", "display"],
|
||||
@@ -32,7 +45,8 @@ var tc = {
|
||||
pendingLastSpeedSave: null,
|
||||
pendingLastSpeedValue: null,
|
||||
persistedLastSpeed: 1.0,
|
||||
activeSiteRule: null
|
||||
activeSiteRule: null,
|
||||
siteRuleBase: null
|
||||
};
|
||||
|
||||
var MIN_SPEED = 0.0625;
|
||||
@@ -80,17 +94,17 @@ var controllerLocationStyles = {
|
||||
transform: "translate(-100%, -50%)"
|
||||
},
|
||||
"bottom-right": {
|
||||
top: "calc(100% - 65px)",
|
||||
top: "calc(100% - 0px)",
|
||||
left: "calc(100% - 10px)",
|
||||
transform: "translate(-100%, -100%)"
|
||||
},
|
||||
"bottom-center": {
|
||||
top: "calc(100% - 65px)",
|
||||
top: "calc(100% - 0px)",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -100%)"
|
||||
},
|
||||
"bottom-left": {
|
||||
top: "calc(100% - 65px)",
|
||||
top: "calc(100% - 0px)",
|
||||
left: "15px",
|
||||
transform: "translate(0, -100%)"
|
||||
},
|
||||
@@ -107,7 +121,7 @@ var controllerButtonDefs = {
|
||||
faster: { label: "+", className: "" },
|
||||
advance: { label: "\u00BB", className: "rw" },
|
||||
display: { label: "\u00D7", className: "hideButton" },
|
||||
reset: { label: "\u21BA", className: "" },
|
||||
reset: { label: "1.00x", className: "" },
|
||||
fast: { label: "\u2605", className: "" },
|
||||
settings: { label: "\u2699", className: "" },
|
||||
pause: { label: "\u23EF", className: "" },
|
||||
@@ -254,6 +268,52 @@ function normalizeControllerLocation(location) {
|
||||
return defaultControllerLocation;
|
||||
}
|
||||
|
||||
var CONTROLLER_MARGIN_MAX_PX = 200;
|
||||
|
||||
function normalizeControllerMarginPx(value, fallback) {
|
||||
var n = Number(value);
|
||||
if (!Number.isFinite(n)) return fallback;
|
||||
return Math.min(
|
||||
CONTROLLER_MARGIN_MAX_PX,
|
||||
Math.max(0, Math.round(n))
|
||||
);
|
||||
}
|
||||
|
||||
function applyControllerMargins(controller) {
|
||||
if (!controller) return;
|
||||
var d = tc.settings;
|
||||
var loc = controller.dataset.location;
|
||||
var manual = controller.dataset.positionMode === "manual";
|
||||
var isTopAnchored =
|
||||
!manual &&
|
||||
(loc === "top-left" ||
|
||||
loc === "top-center" ||
|
||||
loc === "top-right");
|
||||
var isBottomAnchored =
|
||||
!manual &&
|
||||
(loc === "bottom-right" ||
|
||||
loc === "bottom-center" ||
|
||||
loc === "bottom-left");
|
||||
var isMiddleRow =
|
||||
!manual && (loc === "middle-left" || loc === "middle-right");
|
||||
var mt = normalizeControllerMarginPx(d.controllerMarginTop, 0);
|
||||
var mb = normalizeControllerMarginPx(d.controllerMarginBottom, 65);
|
||||
if (isTopAnchored || isBottomAnchored || isMiddleRow) {
|
||||
mt = 0;
|
||||
mb = 0;
|
||||
}
|
||||
controller.style.marginTop = mt + "px";
|
||||
var ml = normalizeControllerMarginPx(d.controllerMarginLeft, 0);
|
||||
var mr = normalizeControllerMarginPx(d.controllerMarginRight, 0);
|
||||
if (!manual) {
|
||||
ml = 0;
|
||||
mr = 0;
|
||||
}
|
||||
controller.style.marginRight = mr + "px";
|
||||
controller.style.marginBottom = mb + "px";
|
||||
controller.style.marginLeft = ml + "px";
|
||||
}
|
||||
|
||||
function getNextControllerLocation(location) {
|
||||
var normalizedLocation = normalizeControllerLocation(location);
|
||||
var currentIndex = controllerLocations.indexOf(normalizedLocation);
|
||||
@@ -290,6 +350,28 @@ function applyControllerLocationToElement(controller, location) {
|
||||
controller.dataset.positionMode = "anchored";
|
||||
|
||||
var top = styles.top;
|
||||
if (
|
||||
normalizedLocation === "top-left" ||
|
||||
normalizedLocation === "top-center" ||
|
||||
normalizedLocation === "top-right"
|
||||
) {
|
||||
var insetTop = normalizeControllerMarginPx(
|
||||
tc.settings.controllerMarginTop,
|
||||
0
|
||||
);
|
||||
top = "calc(10px + " + insetTop + "px)";
|
||||
}
|
||||
if (
|
||||
normalizedLocation === "bottom-right" ||
|
||||
normalizedLocation === "bottom-center" ||
|
||||
normalizedLocation === "bottom-left"
|
||||
) {
|
||||
var lift = normalizeControllerMarginPx(
|
||||
tc.settings.controllerMarginBottom,
|
||||
65
|
||||
);
|
||||
top = "calc(100% - " + lift + "px)";
|
||||
}
|
||||
// If in fullscreen, move the controller down to avoid overlapping video titles
|
||||
if (
|
||||
document.fullscreenElement ||
|
||||
@@ -298,14 +380,40 @@ function applyControllerLocationToElement(controller, location) {
|
||||
document.msFullscreenElement
|
||||
) {
|
||||
if (normalizedLocation.startsWith("top-")) {
|
||||
top = "63px";
|
||||
var insetTopFs = normalizeControllerMarginPx(
|
||||
tc.settings.controllerMarginTop,
|
||||
0
|
||||
);
|
||||
top = "calc(63px + " + insetTopFs + "px)";
|
||||
}
|
||||
}
|
||||
|
||||
controller.style.top = top;
|
||||
controller.style.left = styles.left;
|
||||
|
||||
var left = styles.left;
|
||||
switch (normalizedLocation) {
|
||||
case "top-left":
|
||||
case "middle-left":
|
||||
case "bottom-left":
|
||||
left = "15px";
|
||||
break;
|
||||
case "top-right":
|
||||
case "middle-right":
|
||||
case "bottom-right":
|
||||
left = "calc(100% - 10px)";
|
||||
break;
|
||||
case "top-center":
|
||||
case "bottom-center":
|
||||
left = "50%";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
controller.style.left = left;
|
||||
controller.style.transform = styles.transform;
|
||||
|
||||
applyControllerMargins(controller);
|
||||
|
||||
return normalizedLocation;
|
||||
}
|
||||
|
||||
@@ -321,6 +429,56 @@ function applyControllerLocation(videoController, location) {
|
||||
);
|
||||
}
|
||||
|
||||
function captureSiteRuleBase() {
|
||||
tc.siteRuleBase = {
|
||||
startHidden: tc.settings.startHidden,
|
||||
hideWithControls: tc.settings.hideWithControls,
|
||||
hideWithControlsTimer: tc.settings.hideWithControlsTimer,
|
||||
controllerLocation: tc.settings.controllerLocation,
|
||||
rememberSpeed: tc.settings.rememberSpeed,
|
||||
forceLastSavedSpeed: tc.settings.forceLastSavedSpeed,
|
||||
audioBoolean: tc.settings.audioBoolean,
|
||||
controllerOpacity: tc.settings.controllerOpacity,
|
||||
controllerMarginTop: tc.settings.controllerMarginTop,
|
||||
controllerMarginBottom: tc.settings.controllerMarginBottom,
|
||||
enableSubtitleNudge: tc.settings.enableSubtitleNudge,
|
||||
subtitleNudgeInterval: tc.settings.subtitleNudgeInterval,
|
||||
controllerButtons: Array.isArray(tc.settings.controllerButtons)
|
||||
? tc.settings.controllerButtons.slice()
|
||||
: tc.settings.controllerButtons,
|
||||
keyBindings: Array.isArray(tc.settings.keyBindings)
|
||||
? tc.settings.keyBindings.map(function (binding) {
|
||||
return Object.assign({}, binding);
|
||||
})
|
||||
: tc.settings.keyBindings
|
||||
};
|
||||
}
|
||||
|
||||
function resetSettingsFromSiteRuleBase() {
|
||||
if (!tc.siteRuleBase) return;
|
||||
var base = tc.siteRuleBase;
|
||||
tc.settings.startHidden = base.startHidden;
|
||||
tc.settings.hideWithControls = base.hideWithControls;
|
||||
tc.settings.hideWithControlsTimer = base.hideWithControlsTimer;
|
||||
tc.settings.controllerLocation = base.controllerLocation;
|
||||
tc.settings.rememberSpeed = base.rememberSpeed;
|
||||
tc.settings.forceLastSavedSpeed = base.forceLastSavedSpeed;
|
||||
tc.settings.audioBoolean = base.audioBoolean;
|
||||
tc.settings.controllerOpacity = base.controllerOpacity;
|
||||
tc.settings.controllerMarginTop = base.controllerMarginTop;
|
||||
tc.settings.controllerMarginBottom = base.controllerMarginBottom;
|
||||
tc.settings.enableSubtitleNudge = base.enableSubtitleNudge;
|
||||
tc.settings.subtitleNudgeInterval = base.subtitleNudgeInterval;
|
||||
tc.settings.controllerButtons = Array.isArray(base.controllerButtons)
|
||||
? base.controllerButtons.slice()
|
||||
: base.controllerButtons;
|
||||
tc.settings.keyBindings = Array.isArray(base.keyBindings)
|
||||
? base.keyBindings.map(function (binding) {
|
||||
return Object.assign({}, binding);
|
||||
})
|
||||
: base.keyBindings;
|
||||
}
|
||||
|
||||
function clearManualControllerPosition(videoController) {
|
||||
if (!videoController) return;
|
||||
applyControllerLocation(
|
||||
@@ -483,10 +641,36 @@ function getVideoSourceKey(video) {
|
||||
|
||||
function getControllerTargetSpeed(video) {
|
||||
if (!video || !video.vsc) return null;
|
||||
return isValidSpeed(video.vsc.targetSpeed) ? video.vsc.targetSpeed : null;
|
||||
if (!isValidSpeed(video.vsc.targetSpeed)) return null;
|
||||
|
||||
var currentSourceKey = getVideoSourceKey(video);
|
||||
var targetSourceKey = video.vsc.targetSpeedSourceKey;
|
||||
|
||||
// SPA sites (e.g. YouTube) can reuse the same <video> element.
|
||||
// Don't carry controller target speed across a source swap.
|
||||
if (
|
||||
targetSourceKey &&
|
||||
currentSourceKey === "unknown_src" &&
|
||||
targetSourceKey !== "unknown_src"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
targetSourceKey &&
|
||||
currentSourceKey !== "unknown_src" &&
|
||||
targetSourceKey !== currentSourceKey
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return video.vsc.targetSpeed;
|
||||
}
|
||||
|
||||
function getRememberedSpeed(video) {
|
||||
if (!tc.settings.rememberSpeed && !tc.settings.forceLastSavedSpeed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var sourceKey = getVideoSourceKey(video);
|
||||
if (sourceKey !== "unknown_src") {
|
||||
var videoSpeed = tc.settings.speeds[sourceKey];
|
||||
@@ -667,6 +851,30 @@ function resolveTargetSpeed(video) {
|
||||
return getDesiredSpeed(video);
|
||||
}
|
||||
|
||||
function applySourceTransitionPolicy(video, forceUpdate) {
|
||||
if (!video || !video.vsc) return;
|
||||
|
||||
var sourceKey = getVideoSourceKey(video);
|
||||
if (!forceUpdate && video.vsc.mediaSourceKey === sourceKey) return;
|
||||
|
||||
video.vsc.mediaSourceKey = sourceKey;
|
||||
|
||||
var desiredSpeed =
|
||||
tc.settings.rememberSpeed || tc.settings.forceLastSavedSpeed
|
||||
? sanitizeSpeed(tc.settings.lastSpeed, 1.0)
|
||||
: 1.0;
|
||||
|
||||
video.vsc.targetSpeed = desiredSpeed;
|
||||
video.vsc.targetSpeedSourceKey = sourceKey;
|
||||
if (video.vsc.speedIndicator) {
|
||||
video.vsc.speedIndicator.textContent = desiredSpeed.toFixed(2);
|
||||
}
|
||||
|
||||
if (Math.abs(video.playbackRate - desiredSpeed) > 0.01) {
|
||||
setSpeed(video, desiredSpeed, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
function extendSpeedRestoreWindow(video, duration) {
|
||||
if (!video || !video.vsc) return;
|
||||
|
||||
@@ -987,6 +1195,22 @@ chrome.storage.sync.get(tc.settings, function (storage) {
|
||||
storage.controllerLocation
|
||||
);
|
||||
tc.settings.controllerOpacity = Number(storage.controllerOpacity);
|
||||
tc.settings.controllerMarginTop = normalizeControllerMarginPx(
|
||||
storage.controllerMarginTop,
|
||||
0
|
||||
);
|
||||
tc.settings.controllerMarginRight = normalizeControllerMarginPx(
|
||||
0,
|
||||
0
|
||||
);
|
||||
tc.settings.controllerMarginBottom = normalizeControllerMarginPx(
|
||||
storage.controllerMarginBottom,
|
||||
typeof storage.controllerMarginBottom !== "undefined" ? 0 : 65
|
||||
);
|
||||
tc.settings.controllerMarginLeft = normalizeControllerMarginPx(
|
||||
0,
|
||||
0
|
||||
);
|
||||
tc.settings.siteRules = Array.isArray(storage.siteRules)
|
||||
? storage.siteRules
|
||||
: [];
|
||||
@@ -1041,6 +1265,7 @@ chrome.storage.sync.get(tc.settings, function (storage) {
|
||||
if (addedDefaultBinding) {
|
||||
chrome.storage.sync.set({ keyBindings: tc.settings.keyBindings });
|
||||
}
|
||||
captureSiteRuleBase();
|
||||
patchAttachShadow();
|
||||
// Add a listener for messages from the popup.
|
||||
// We use a global flag to ensure the listener is only attached once.
|
||||
@@ -1051,36 +1276,38 @@ chrome.storage.sync.get(tc.settings, function (storage) {
|
||||
log("Re-scan command received from popup.", 4);
|
||||
initializeWhenReady(document, true);
|
||||
sendResponse({ status: "complete" });
|
||||
} else if (request.action === "get_speed") {
|
||||
var speed = 1.0;
|
||||
if (tc.mediaElements && tc.mediaElements.length > 0) {
|
||||
for (var i = 0; i < tc.mediaElements.length; i++) {
|
||||
if (tc.mediaElements[i] && !tc.mediaElements[i].paused) {
|
||||
speed = tc.mediaElements[i].playbackRate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (speed === 1.0 && tc.mediaElements[0]) {
|
||||
speed = tc.mediaElements[0].playbackRate;
|
||||
}
|
||||
}
|
||||
sendResponse({ speed: speed });
|
||||
} else if (request.action === "get_page_context") {
|
||||
return false;
|
||||
}
|
||||
if (request.action === "get_speed") {
|
||||
// Do not sendResponse in frames with no media — only one response is
|
||||
// accepted tab-wide, and the top frame often wins before an iframe.
|
||||
var videoGs = getPrimaryVideoElement();
|
||||
if (!videoGs) return false;
|
||||
sendResponse({
|
||||
speed: videoGs.playbackRate,
|
||||
resetLabel: computeResetButtonLabelForVideo(videoGs)
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (request.action === "get_page_context") {
|
||||
sendResponse({ url: location.href });
|
||||
} else if (request.action === "run_action") {
|
||||
return false;
|
||||
}
|
||||
if (request.action === "run_action") {
|
||||
var value = request.value;
|
||||
if (value === undefined || value === null) {
|
||||
value = getKeyBindings(request.actionName, "value");
|
||||
}
|
||||
runAction(request.actionName, value);
|
||||
var newSpeed = 1.0;
|
||||
if (tc.mediaElements && tc.mediaElements.length > 0) {
|
||||
newSpeed = tc.mediaElements[0].playbackRate;
|
||||
}
|
||||
sendResponse({ speed: newSpeed });
|
||||
var videoAfter = getPrimaryVideoElement();
|
||||
if (!videoAfter) return false;
|
||||
sendResponse({
|
||||
speed: videoAfter.playbackRate,
|
||||
resetLabel: computeResetButtonLabelForVideo(videoAfter)
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1127,6 +1354,8 @@ function defineVideoController() {
|
||||
this.suppressedRateChangeCount = 0;
|
||||
this.suppressedRateChangeUntil = 0;
|
||||
this.visibilityResumeHandler = null;
|
||||
this.resetToggleArmed = false;
|
||||
this.resetButtonEl = null;
|
||||
this.controllerLocation = normalizeControllerLocation(
|
||||
tc.settings.controllerLocation
|
||||
);
|
||||
@@ -1135,6 +1364,8 @@ function defineVideoController() {
|
||||
|
||||
let storedSpeed = sanitizeSpeed(resolveTargetSpeed(target), 1.0);
|
||||
this.targetSpeed = storedSpeed;
|
||||
this.targetSpeedSourceKey = getVideoSourceKey(target);
|
||||
this.mediaSourceKey = getVideoSourceKey(target);
|
||||
if (!tc.settings.rememberSpeed && !tc.settings.forceLastSavedSpeed) {
|
||||
setKeyBindings("reset", getKeyBindings("fast"));
|
||||
}
|
||||
@@ -1152,7 +1383,16 @@ function defineVideoController() {
|
||||
log(`Controller created and attached to DOM. Hidden: ${this.div.classList.contains('vsc-hidden')}`, 4);
|
||||
|
||||
var mediaEventAction = function (event) {
|
||||
if (
|
||||
event.type === "loadedmetadata" ||
|
||||
event.type === "loadeddata" ||
|
||||
event.type === "canplay"
|
||||
) {
|
||||
applySourceTransitionPolicy(event.target, false);
|
||||
}
|
||||
|
||||
if (event.type === "play") {
|
||||
applySourceTransitionPolicy(event.target, false);
|
||||
extendSpeedRestoreWindow(event.target);
|
||||
|
||||
if (!tc.settings.rememberSpeed && !tc.settings.forceLastSavedSpeed) {
|
||||
@@ -1199,6 +1439,18 @@ function defineVideoController() {
|
||||
}
|
||||
};
|
||||
|
||||
target.addEventListener(
|
||||
"loadedmetadata",
|
||||
(this.handleLoadedMetadata = mediaEventAction.bind(this))
|
||||
);
|
||||
target.addEventListener(
|
||||
"loadeddata",
|
||||
(this.handleLoadedData = mediaEventAction.bind(this))
|
||||
);
|
||||
target.addEventListener(
|
||||
"canplay",
|
||||
(this.handleCanPlay = mediaEventAction.bind(this))
|
||||
);
|
||||
target.addEventListener(
|
||||
"play",
|
||||
(this.handlePlay = mediaEventAction.bind(this))
|
||||
@@ -1234,6 +1486,7 @@ function defineVideoController() {
|
||||
this.div.classList.add("vsc-nosource");
|
||||
} else {
|
||||
this.div.classList.remove("vsc-nosource");
|
||||
applySourceTransitionPolicy(this.video, true);
|
||||
if (!mutation.target.paused) this.startSubtitleNudge();
|
||||
}
|
||||
updateSubtitleNudgeIndicator(this.video);
|
||||
@@ -1264,6 +1517,9 @@ function defineVideoController() {
|
||||
if (this.div) this.div.remove();
|
||||
if (this.restoreSpeedTimer) clearTimeout(this.restoreSpeedTimer);
|
||||
if (this.video) {
|
||||
this.video.removeEventListener("loadedmetadata", this.handleLoadedMetadata);
|
||||
this.video.removeEventListener("loadeddata", this.handleLoadedData);
|
||||
this.video.removeEventListener("canplay", this.handleCanPlay);
|
||||
this.video.removeEventListener("play", this.handlePlay);
|
||||
this.video.removeEventListener("pause", this.handlePause);
|
||||
this.video.removeEventListener("seeking", this.handleSeeking);
|
||||
@@ -1600,6 +1856,10 @@ function defineVideoController() {
|
||||
this.speedIndicator = dragHandle;
|
||||
this.subtitleNudgeIndicator = subtitleNudgeIndicator;
|
||||
this.nudgeFlashIndicator = nudgeFlashIndicator;
|
||||
this.resetButtonEl =
|
||||
shadow.querySelector('button[data-action="reset"]') || null;
|
||||
this.resetToggleArmed = false;
|
||||
updateResetButtonLabel(this.video);
|
||||
if (subtitleNudgeIndicator) {
|
||||
updateSubtitleNudgeIndicator(this.video);
|
||||
}
|
||||
@@ -1725,6 +1985,8 @@ function escapeStringRegExp(str) {
|
||||
return str.replace(m, "\\$&");
|
||||
}
|
||||
function applySiteRuleOverrides() {
|
||||
resetSettingsFromSiteRuleBase();
|
||||
|
||||
if (!Array.isArray(tc.settings.siteRules) || tc.settings.siteRules.length === 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -1784,6 +2046,8 @@ function applySiteRuleOverrides() {
|
||||
"forceLastSavedSpeed",
|
||||
"audioBoolean",
|
||||
"controllerOpacity",
|
||||
"controllerMarginTop",
|
||||
"controllerMarginBottom",
|
||||
"enableSubtitleNudge",
|
||||
"subtitleNudgeInterval"
|
||||
];
|
||||
@@ -1795,6 +2059,13 @@ function applySiteRuleOverrides() {
|
||||
}
|
||||
});
|
||||
|
||||
[
|
||||
"controllerMarginTop",
|
||||
"controllerMarginBottom"
|
||||
].forEach(function (key) {
|
||||
tc.settings[key] = normalizeControllerMarginPx(tc.settings[key], 0);
|
||||
});
|
||||
|
||||
if (Array.isArray(matchedRule.controllerButtons)) {
|
||||
log(`Overriding controllerButtons for site`, 4);
|
||||
tc.settings.controllerButtons = matchedRule.controllerButtons;
|
||||
@@ -1837,11 +2108,15 @@ function shouldPreserveDesiredSpeed(video, speed) {
|
||||
function setupListener(root) {
|
||||
root = root || document;
|
||||
if (root.vscRateListenerAttached) return;
|
||||
function updateSpeedFromEvent(video) {
|
||||
function updateSpeedFromEvent(video, skipResetDisarm) {
|
||||
if (!video.vsc || !video.vsc.speedIndicator) return;
|
||||
if (!skipResetDisarm) {
|
||||
video.vsc.resetToggleArmed = false;
|
||||
}
|
||||
var speed = video.playbackRate; // Preserve full precision (e.g. 0.0625)
|
||||
video.vsc.speedIndicator.textContent = speed.toFixed(2);
|
||||
video.vsc.targetSpeed = speed;
|
||||
video.vsc.targetSpeedSourceKey = getVideoSourceKey(video);
|
||||
var sourceKey = getVideoSourceKey(video);
|
||||
if (sourceKey !== "unknown_src") {
|
||||
tc.settings.speeds[sourceKey] = speed;
|
||||
@@ -1852,6 +2127,7 @@ function setupListener(root) {
|
||||
if (speed === 1.0 || video.paused) video.vsc.stopSubtitleNudge();
|
||||
else video.vsc.startSubtitleNudge();
|
||||
}
|
||||
updateResetButtonLabel(video);
|
||||
}
|
||||
root.addEventListener(
|
||||
"ratechange",
|
||||
@@ -1864,7 +2140,7 @@ function setupListener(root) {
|
||||
if (tc.settings.forceLastSavedSpeed) {
|
||||
if (event.detail && event.detail.origin === "videoSpeed") {
|
||||
video.playbackRate = event.detail.speed;
|
||||
updateSpeedFromEvent(video);
|
||||
updateSpeedFromEvent(video, true);
|
||||
} else {
|
||||
video.playbackRate = sanitizeSpeed(tc.settings.lastSpeed, 1.0);
|
||||
}
|
||||
@@ -1875,7 +2151,7 @@ function setupListener(root) {
|
||||
var pendingRateChange = takePendingRateChange(video, currentSpeed);
|
||||
|
||||
if (pendingRateChange) {
|
||||
updateSpeedFromEvent(video);
|
||||
updateSpeedFromEvent(video, true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1884,8 +2160,10 @@ function setupListener(root) {
|
||||
`Ignoring external rate change to ${currentSpeed.toFixed(4)} while preserving ${desiredSpeed.toFixed(4)}`,
|
||||
4
|
||||
);
|
||||
video.vsc.resetToggleArmed = false;
|
||||
video.vsc.speedIndicator.textContent = desiredSpeed.toFixed(2);
|
||||
scheduleSpeedRestore(video, desiredSpeed, "pause/play or seek");
|
||||
updateResetButtonLabel(video);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2161,12 +2439,60 @@ function initializeNow(doc, forceReinit = false) {
|
||||
|
||||
if (forceReinit) {
|
||||
log("Force re-initialization requested", 4);
|
||||
tc.mediaElements.forEach(function (video) {
|
||||
if (!video || !video.vsc) return;
|
||||
applyControllerLocation(video.vsc, tc.settings.controllerLocation);
|
||||
var controllerEl = getControllerElement(video.vsc);
|
||||
if (controllerEl) {
|
||||
controllerEl.style.opacity = String(tc.settings.controllerOpacity);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
vscInitializedDocuments.add(doc);
|
||||
}
|
||||
|
||||
function setSpeed(video, speed, isInitialCall = false, isUserKeyPress = false) {
|
||||
function formatSpeedWithX(speed) {
|
||||
var n = Number(speed);
|
||||
if (!isFinite(n)) return "?x";
|
||||
return n.toFixed(2) + "x";
|
||||
}
|
||||
|
||||
function computeResetButtonLabelForVideo(video) {
|
||||
if (!video) return "1.00x";
|
||||
var rate = video.playbackRate;
|
||||
var atOne = Math.abs(rate - 1.0) < 0.01;
|
||||
var armed = video.vsc && video.vsc.resetToggleArmed === true;
|
||||
|
||||
if (atOne) {
|
||||
if (armed) {
|
||||
var videoId = getVideoSourceKey(video);
|
||||
var lastToggle = lastToggleSpeed[videoId];
|
||||
var pref = getKeyBindings("fast") || 1.8;
|
||||
var speedToRestore =
|
||||
lastToggle == null || Math.abs(lastToggle - 1.0) < 0.01
|
||||
? pref
|
||||
: lastToggle;
|
||||
return formatSpeedWithX(speedToRestore);
|
||||
}
|
||||
return "1.00x";
|
||||
}
|
||||
return formatSpeedWithX(1.0);
|
||||
}
|
||||
|
||||
function updateResetButtonLabel(video) {
|
||||
if (!video || !video.vsc || !video.vsc.resetButtonEl) return;
|
||||
video.vsc.resetButtonEl.textContent =
|
||||
computeResetButtonLabelForVideo(video);
|
||||
}
|
||||
|
||||
function setSpeed(
|
||||
video,
|
||||
speed,
|
||||
isInitialCall = false,
|
||||
isUserKeyPress = false,
|
||||
fromResetSpeedToggle = false
|
||||
) {
|
||||
const numericSpeed = Number(speed);
|
||||
|
||||
if (!isValidSpeed(numericSpeed)) {
|
||||
@@ -2179,6 +2505,10 @@ function setSpeed(video, speed, isInitialCall = false, isUserKeyPress = false) {
|
||||
|
||||
if (!video || !video.vsc || !video.vsc.speedIndicator) return;
|
||||
|
||||
if (isUserKeyPress && !fromResetSpeedToggle) {
|
||||
video.vsc.resetToggleArmed = false;
|
||||
}
|
||||
|
||||
log(
|
||||
`setSpeed: Target ${numericSpeed.toFixed(2)}. Initial: ${isInitialCall}. UserKeyPress: ${isUserKeyPress}`,
|
||||
4
|
||||
@@ -2188,6 +2518,7 @@ function setSpeed(video, speed, isInitialCall = false, isUserKeyPress = false) {
|
||||
|
||||
// Update the target speed for nudge so it knows what to revert to
|
||||
video.vsc.targetSpeed = numericSpeed;
|
||||
video.vsc.targetSpeedSourceKey = getVideoSourceKey(video);
|
||||
|
||||
if (isUserKeyPress && !isInitialCall && video.vsc && video.vsc.div) {
|
||||
runAction("blink", 1000, null, video); // Pass video to blink
|
||||
@@ -2225,6 +2556,7 @@ function setSpeed(video, speed, isInitialCall = false, isUserKeyPress = false) {
|
||||
video.vsc.startSubtitleNudge();
|
||||
}
|
||||
}
|
||||
updateResetButtonLabel(video);
|
||||
}
|
||||
|
||||
function runAction(action, value, e) {
|
||||
@@ -2433,11 +2765,12 @@ function resetSpeed(v, target, isFastKey = false) {
|
||||
Math.abs(lastToggle - 1.0) < 0.01
|
||||
? getKeyBindings("fast") || 1.8
|
||||
: lastToggle;
|
||||
setSpeed(v, speedToRestore, false, true);
|
||||
setSpeed(v, speedToRestore, false, true, true);
|
||||
} else {
|
||||
// Not at 1.0, save current as toggle speed and go to 1.0
|
||||
lastToggleSpeed[videoId] = currentSpeed;
|
||||
setSpeed(v, resetSpeedValue, false, true);
|
||||
v.vsc.resetToggleArmed = true;
|
||||
setSpeed(v, resetSpeedValue, false, true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Speeder",
|
||||
"short_name": "Speeder",
|
||||
"version": "5.0.0",
|
||||
"version": "5.0.4",
|
||||
"manifest_version": 2,
|
||||
"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",
|
||||
@@ -21,7 +21,9 @@
|
||||
"128": "icons/icon128.png"
|
||||
},
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
"scripts": [
|
||||
"background.js"
|
||||
]
|
||||
},
|
||||
"permissions": [
|
||||
"storage"
|
||||
@@ -64,4 +66,4 @@
|
||||
"inject.css",
|
||||
"shadow.css"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+152
-11
@@ -299,11 +299,104 @@ label em {
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.row input[type="text"],
|
||||
.row select {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.row.row-checkbox {
|
||||
grid-template-columns: minmax(0, 1fr) 24px;
|
||||
}
|
||||
|
||||
.row.row-checkbox input[type="checkbox"] {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.settings-card .row:first-of-type {
|
||||
padding-top: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.row.row-controller-margin {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 260px);
|
||||
}
|
||||
|
||||
.controller-margin-inputs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 116px));
|
||||
gap: 8px;
|
||||
width: max-content;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.margin-pad-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 116px;
|
||||
}
|
||||
|
||||
.margin-pad-mini {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.controller-margin-inputs input[type="text"] {
|
||||
width: 100%;
|
||||
min-width: 116px;
|
||||
box-sizing: border-box;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.site-rule-option.site-rule-margin-option {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 260px);
|
||||
}
|
||||
|
||||
.site-rule-override-section {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.site-rule-content > .site-rule-override-section:first-of-type {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.site-override-lead {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 24px;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.site-override-lead input[type="checkbox"] {
|
||||
justify-self: end;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.site-override-lead span {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.site-rule-override-section .site-override-fields,
|
||||
.site-rule-override-section .site-placement-container,
|
||||
.site-rule-override-section .site-visibility-container,
|
||||
.site-rule-override-section .site-autohide-container,
|
||||
.site-rule-override-section .site-playback-container,
|
||||
.site-rule-override-section .site-opacity-container,
|
||||
.site-rule-override-section .site-subtitleNudge-container {
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.cb-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -453,24 +546,61 @@ label em {
|
||||
|
||||
.site-rule-option {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 150px;
|
||||
grid-template-columns: minmax(0, 1fr) 160px;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
padding: 8px 0;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.site-rule-option-checkbox {
|
||||
grid-template-columns: minmax(0, 1fr) 24px;
|
||||
}
|
||||
|
||||
.site-rule-option-checkbox > input[type="checkbox"] {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.site-rule-option > input[type="text"],
|
||||
.site-rule-option > select {
|
||||
justify-self: end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.site-rule-option.site-rule-margin-option .controller-margin-inputs {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.site-rule-body > .site-rule-option:first-child,
|
||||
.site-rule-content > .site-rule-option:first-child {
|
||||
padding-top: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.site-rule-option label {
|
||||
display: flex;
|
||||
.site-rule-option > label:not(.site-rule-split-label) {
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.site-rule-split-label {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 24px;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
width: auto;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.site-rule-split-label input[type="checkbox"] {
|
||||
justify-self: end;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.site-rule-option-checkbox > .site-rule-split-label {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.site-rule-controlbar,
|
||||
@@ -480,12 +610,8 @@ label em {
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.site-rule-controlbar > label,
|
||||
.site-rule-shortcuts > label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
width: auto;
|
||||
.site-rule-controlbar > label.site-override-lead,
|
||||
.site-rule-shortcuts > label.site-override-lead {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -567,11 +693,26 @@ label em {
|
||||
.shortcut-row,
|
||||
.shortcut-row.customs,
|
||||
.row,
|
||||
.row.row-checkbox,
|
||||
.site-rule-option,
|
||||
.site-shortcuts-container .shortcut-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.row input[type="text"],
|
||||
.row select {
|
||||
justify-self: stretch;
|
||||
}
|
||||
|
||||
.site-rule-option > input[type="text"],
|
||||
.site-rule-option > select {
|
||||
justify-self: stretch;
|
||||
}
|
||||
|
||||
.site-override-lead {
|
||||
grid-template-columns: minmax(0, 1fr) 24px;
|
||||
}
|
||||
|
||||
.action-row button,
|
||||
#addShortcutSelector {
|
||||
width: 100%;
|
||||
|
||||
+153
-67
@@ -180,11 +180,11 @@
|
||||
|
||||
<h4 class="defaults-sub-heading">General</h4>
|
||||
|
||||
<div class="row">
|
||||
<div class="row row-checkbox">
|
||||
<label for="enabled">Enable</label>
|
||||
<input id="enabled" type="checkbox" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row row-checkbox">
|
||||
<label for="audioBoolean">Work on audio</label>
|
||||
<input id="audioBoolean" type="checkbox" />
|
||||
</div>
|
||||
@@ -192,11 +192,11 @@
|
||||
<div class="defaults-divider"></div>
|
||||
<h4 class="defaults-sub-heading">Playback</h4>
|
||||
|
||||
<div class="row">
|
||||
<div class="row row-checkbox">
|
||||
<label for="rememberSpeed">Remember playback speed</label>
|
||||
<input id="rememberSpeed" type="checkbox" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row row-checkbox">
|
||||
<label for="forceLastSavedSpeed"
|
||||
>Force last saved speed<br />
|
||||
<em
|
||||
@@ -210,7 +210,7 @@
|
||||
<div class="defaults-divider"></div>
|
||||
<h4 class="defaults-sub-heading">Controller</h4>
|
||||
|
||||
<div class="row">
|
||||
<div class="row row-checkbox">
|
||||
<label for="startHidden">Hide controller by default</label>
|
||||
<input id="startHidden" type="checkbox" />
|
||||
</div>
|
||||
@@ -231,7 +231,26 @@
|
||||
<label for="controllerOpacity">Controller opacity</label>
|
||||
<input id="controllerOpacity" type="text" value="" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row row-controller-margin">
|
||||
<label for="controllerMarginTop"
|
||||
>Controller margin (px)<br />
|
||||
<em
|
||||
>Shifts the whole control from its preset position (CSS
|
||||
margins). Top and bottom. 0–200.</em
|
||||
>
|
||||
</label>
|
||||
<div class="controller-margin-inputs" aria-label="Controller margin in pixels">
|
||||
<div class="margin-pad-cell">
|
||||
<span class="margin-pad-mini">Top</span>
|
||||
<input id="controllerMarginTop" type="text" inputmode="numeric" placeholder="0" />
|
||||
</div>
|
||||
<div class="margin-pad-cell">
|
||||
<span class="margin-pad-mini">Bottom</span>
|
||||
<input id="controllerMarginBottom" type="text" inputmode="numeric" placeholder="0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-checkbox">
|
||||
<label for="hideWithControls"
|
||||
>Hide with controls<br />
|
||||
<em
|
||||
@@ -251,7 +270,7 @@
|
||||
</label>
|
||||
<input id="hideWithControlsTimer" type="text" placeholder="2" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row row-checkbox">
|
||||
<label for="showPopupControlBar">Show popup control bar</label>
|
||||
<input id="showPopupControlBar" type="checkbox" />
|
||||
</div>
|
||||
@@ -259,7 +278,7 @@
|
||||
<div class="defaults-divider"></div>
|
||||
<h4 class="defaults-sub-heading">Subtitle sync</h4>
|
||||
|
||||
<div class="row">
|
||||
<div class="row row-checkbox">
|
||||
<label for="enableSubtitleNudge"
|
||||
>Enable subtitle nudge<br /><em
|
||||
>Makes tiny playback changes to help keep subtitles aligned.</em
|
||||
@@ -349,7 +368,9 @@
|
||||
rel="noopener noreferrer"
|
||||
>Regex</a
|
||||
>
|
||||
patterns like <code>/(.+)youtube\.com(\/*)$/gi</code>.
|
||||
patterns like <code>/(.+)youtube\.com(\/*)$/gi</code>. Turn on a
|
||||
row only when you want that group to override the general defaults
|
||||
above.
|
||||
</p>
|
||||
</div>
|
||||
<div id="siteRulesContainer"></div>
|
||||
@@ -368,72 +389,133 @@
|
||||
<button type="button" class="remove-site-rule">Remove</button>
|
||||
</div>
|
||||
<div class="site-rule-body">
|
||||
<div class="site-rule-option">
|
||||
<label>
|
||||
<div class="site-rule-option site-rule-option-checkbox">
|
||||
<label class="site-rule-split-label">
|
||||
<span>Enable Speeder on this site</span>
|
||||
<input type="checkbox" class="site-enabled" />
|
||||
Enable Speeder 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 class="site-rule-override-section">
|
||||
<label class="site-override-lead">
|
||||
<span>Override placement for this site</span>
|
||||
<input type="checkbox" class="override-placement" />
|
||||
</label>
|
||||
<div class="site-placement-container" style="display: none">
|
||||
<div class="site-rule-option site-rule-option-field">
|
||||
<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 site-rule-margin-option">
|
||||
<label
|
||||
>Controller margin (px):<br /><em
|
||||
>Shifts the whole control. 0–200.</em
|
||||
></label
|
||||
>
|
||||
<div class="controller-margin-inputs">
|
||||
<div class="margin-pad-cell">
|
||||
<span class="margin-pad-mini">T</span>
|
||||
<input type="text" class="site-controllerMarginTop" inputmode="numeric" placeholder="0" />
|
||||
</div>
|
||||
<div class="margin-pad-cell">
|
||||
<span class="margin-pad-mini">B</span>
|
||||
<input type="text" class="site-controllerMarginBottom" inputmode="numeric" placeholder="0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-rule-option">
|
||||
<label>
|
||||
<input type="checkbox" class="site-hideWithControls" />
|
||||
Hide with controls (idle-based)
|
||||
</label>
|
||||
</div>
|
||||
<div class="site-rule-option">
|
||||
<label>Auto-hide timer (0.1–15s):</label>
|
||||
<input type="text" class="site-hideWithControlsTimer" />
|
||||
</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 class="site-rule-override-section">
|
||||
<label class="site-override-lead">
|
||||
<span>Override hide-by-default for this site</span>
|
||||
<input type="checkbox" class="override-visibility" />
|
||||
</label>
|
||||
<div class="site-visibility-container" style="display: none">
|
||||
<div class="site-rule-option site-rule-option-checkbox">
|
||||
<label>Hide controller by default:</label>
|
||||
<input type="checkbox" class="site-startHidden" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-rule-option">
|
||||
<label>Remember playback speed:</label>
|
||||
<input type="checkbox" class="site-rememberSpeed" />
|
||||
<div class="site-rule-override-section">
|
||||
<label class="site-override-lead">
|
||||
<span>Override auto-hide for this site</span>
|
||||
<input type="checkbox" class="override-autohide" />
|
||||
</label>
|
||||
<div class="site-autohide-container" style="display: none">
|
||||
<div class="site-rule-option site-rule-option-checkbox">
|
||||
<label class="site-rule-split-label">
|
||||
<span>Hide with controls (idle-based)</span>
|
||||
<input type="checkbox" class="site-hideWithControls" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="site-rule-option site-rule-option-field">
|
||||
<label>Auto-hide timer (0.1–15s):</label>
|
||||
<input type="text" class="site-hideWithControlsTimer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-rule-option">
|
||||
<label>Force last saved speed:</label>
|
||||
<input type="checkbox" class="site-forceLastSavedSpeed" />
|
||||
<div class="site-rule-override-section">
|
||||
<label class="site-override-lead">
|
||||
<span>Override playback for this site</span>
|
||||
<input type="checkbox" class="override-playback" />
|
||||
</label>
|
||||
<div class="site-playback-container" style="display: none">
|
||||
<div class="site-rule-option site-rule-option-checkbox">
|
||||
<label>Remember playback speed:</label>
|
||||
<input type="checkbox" class="site-rememberSpeed" />
|
||||
</div>
|
||||
<div class="site-rule-option site-rule-option-checkbox">
|
||||
<label>Force last saved speed:</label>
|
||||
<input type="checkbox" class="site-forceLastSavedSpeed" />
|
||||
</div>
|
||||
<div class="site-rule-option site-rule-option-checkbox">
|
||||
<label>Work on audio:</label>
|
||||
<input type="checkbox" class="site-audioBoolean" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-rule-option">
|
||||
<label>Work on audio:</label>
|
||||
<input type="checkbox" class="site-audioBoolean" />
|
||||
<div class="site-rule-override-section">
|
||||
<label class="site-override-lead">
|
||||
<span>Override opacity for this site</span>
|
||||
<input type="checkbox" class="override-opacity" />
|
||||
</label>
|
||||
<div class="site-opacity-container" style="display: none">
|
||||
<div class="site-rule-option site-rule-option-field">
|
||||
<label>Controller opacity:</label>
|
||||
<input type="text" class="site-controllerOpacity" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-rule-option">
|
||||
<label>Controller opacity:</label>
|
||||
<input type="text" class="site-controllerOpacity" />
|
||||
</div>
|
||||
<div class="site-rule-option">
|
||||
<label>Show popup control bar:</label>
|
||||
<input type="checkbox" class="site-showPopupControlBar" />
|
||||
</div>
|
||||
<div class="site-rule-option">
|
||||
<label>Enable subtitle nudge:</label>
|
||||
<input type="checkbox" class="site-enableSubtitleNudge" />
|
||||
</div>
|
||||
<div class="site-rule-option">
|
||||
<label>Nudge interval (10–1000ms):</label>
|
||||
<input type="text" class="site-subtitleNudgeInterval" placeholder="50" />
|
||||
<div class="site-rule-override-section">
|
||||
<label class="site-override-lead">
|
||||
<span>Override subtitle nudge for this site</span>
|
||||
<input type="checkbox" class="override-subtitleNudge" />
|
||||
</label>
|
||||
<div class="site-subtitleNudge-container" style="display: none">
|
||||
<div class="site-rule-option site-rule-option-checkbox">
|
||||
<label>Enable subtitle nudge:</label>
|
||||
<input type="checkbox" class="site-enableSubtitleNudge" />
|
||||
</div>
|
||||
<div class="site-rule-option site-rule-option-field">
|
||||
<label>Nudge interval (10–1000ms):</label>
|
||||
<input type="text" class="site-subtitleNudgeInterval" placeholder="50" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-rule-controlbar">
|
||||
<label>
|
||||
<label class="site-override-lead">
|
||||
<span>Override in-player control bar for this site</span>
|
||||
<input type="checkbox" class="override-controlbar" />
|
||||
Custom control bar for this site
|
||||
</label>
|
||||
<div class="site-controlbar-container" style="display: none">
|
||||
<div class="cb-editor">
|
||||
@@ -449,11 +531,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-rule-controlbar">
|
||||
<label>
|
||||
<label class="site-override-lead">
|
||||
<span>Override extension popup for this site</span>
|
||||
<input type="checkbox" class="override-popup-controlbar" />
|
||||
Custom popup control bar for this site
|
||||
</label>
|
||||
<div class="site-popup-controlbar-container" style="display: none">
|
||||
<div class="site-rule-option site-rule-option-checkbox">
|
||||
<label>Show popup control bar</label>
|
||||
<input type="checkbox" class="site-showPopupControlBar" />
|
||||
</div>
|
||||
<div class="cb-editor">
|
||||
<div class="cb-zone">
|
||||
<div class="cb-zone-label">Active</div>
|
||||
@@ -467,9 +553,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-rule-shortcuts">
|
||||
<label>
|
||||
<label class="site-override-lead">
|
||||
<span>Override shortcuts for this site</span>
|
||||
<input type="checkbox" class="override-shortcuts" />
|
||||
Custom shortcuts for this site
|
||||
</label>
|
||||
<div class="site-shortcuts-container" style="display: none"></div>
|
||||
</div>
|
||||
|
||||
+240
-82
@@ -138,7 +138,7 @@ var controllerButtonDefs = {
|
||||
faster: { icon: "+", name: "Increase speed" },
|
||||
advance: { icon: "\u00BB", name: "Advance" },
|
||||
display: { icon: "\u00D7", name: "Close controller" },
|
||||
reset: { icon: "\u21BA", name: "Reset speed" },
|
||||
reset: { icon: "1.00x", name: "Reset speed" },
|
||||
fast: { icon: "\u2605", name: "Preferred speed" },
|
||||
nudge: { icon: "\u2713", name: "Subtitle nudge" },
|
||||
settings: { icon: "\u2699", name: "Settings" },
|
||||
@@ -173,6 +173,10 @@ var tcDefaults = {
|
||||
forceLastSavedSpeed: false,
|
||||
enabled: true,
|
||||
controllerOpacity: 0.3,
|
||||
controllerMarginTop: 0,
|
||||
controllerMarginRight: 0,
|
||||
controllerMarginBottom: 65,
|
||||
controllerMarginLeft: 0,
|
||||
keyBindings: [
|
||||
createDefaultBinding("display", "V", 86, 0),
|
||||
createDefaultBinding("move", "P", 80, 0),
|
||||
@@ -185,10 +189,18 @@ var tcDefaults = {
|
||||
createDefaultBinding("toggleSubtitleNudge", "N", 78, 0)
|
||||
],
|
||||
siteRules: [
|
||||
{ pattern: "youtube.com", enabled: true, enableSubtitleNudge: true },
|
||||
{ pattern: "example1.com", enabled: false },
|
||||
{ pattern: "/example2\\.com/i", enabled: false },
|
||||
{ pattern: "/(example3|sample3)\\.com/gi", enabled: false }
|
||||
{
|
||||
pattern: "/^https:\\/\\/(www\\.)?youtube\\.com\\/(?!shorts\\/).*/",
|
||||
enabled: true,
|
||||
enableSubtitleNudge: true,
|
||||
subtitleNudgeInterval: 50
|
||||
},
|
||||
{
|
||||
pattern: "/^https:\\/\\/(www\\.)?youtube\\.com\\/shorts\\/.*/",
|
||||
enabled: true,
|
||||
controllerMarginTop: 60,
|
||||
controllerMarginBottom: 85
|
||||
}
|
||||
],
|
||||
controllerButtons: ["rewind", "slower", "faster", "advance", "display"],
|
||||
showPopupControlBar: true,
|
||||
@@ -215,6 +227,19 @@ const actionLabels = {
|
||||
toggleSubtitleNudge: "Toggle subtitle nudge"
|
||||
};
|
||||
|
||||
const speedBindingActions = ["slower", "faster", "fast"];
|
||||
|
||||
function formatSpeedBindingDisplay(action, value) {
|
||||
if (!speedBindingActions.includes(action)) {
|
||||
return value;
|
||||
}
|
||||
var n = Number(value);
|
||||
if (!isFinite(n)) {
|
||||
return value;
|
||||
}
|
||||
return n.toFixed(2);
|
||||
}
|
||||
|
||||
const customActionsNoValues = [
|
||||
"reset",
|
||||
"display",
|
||||
@@ -275,6 +300,28 @@ function normalizeControllerLocation(location) {
|
||||
return tcDefaults.controllerLocation;
|
||||
}
|
||||
|
||||
function clampMarginPxInput(el, fallback) {
|
||||
var n = parseInt(el && el.value, 10);
|
||||
if (!Number.isFinite(n)) return fallback;
|
||||
return Math.min(200, Math.max(0, n));
|
||||
}
|
||||
|
||||
function syncSiteRuleField(ruleEl, rule, key, isCheckbox) {
|
||||
var input = ruleEl.querySelector(".site-" + key);
|
||||
if (!input) return;
|
||||
var globalEl = document.getElementById(key);
|
||||
var value;
|
||||
if (rule && rule[key] !== undefined) {
|
||||
value = rule[key];
|
||||
} else if (globalEl) {
|
||||
value = isCheckbox ? globalEl.checked : globalEl.value;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (isCheckbox) input.checked = Boolean(value);
|
||||
else input.value = value;
|
||||
}
|
||||
|
||||
function normalizeBindingKey(key) {
|
||||
if (typeof key !== "string" || key.length === 0) return null;
|
||||
if (key === "Spacebar") return " ";
|
||||
@@ -492,7 +539,7 @@ function add_shortcut(action, value) {
|
||||
valueInput.value = "N/A";
|
||||
valueInput.disabled = true;
|
||||
} else {
|
||||
valueInput.value = value || 0;
|
||||
valueInput.value = formatSpeedBindingDisplay(action, value || 0);
|
||||
}
|
||||
|
||||
var removeButton = document.createElement("button");
|
||||
@@ -614,6 +661,16 @@ function save_options() {
|
||||
settings.controllerOpacity =
|
||||
parseFloat(document.getElementById("controllerOpacity").value) ||
|
||||
tcDefaults.controllerOpacity;
|
||||
|
||||
settings.controllerMarginTop = clampMarginPxInput(
|
||||
document.getElementById("controllerMarginTop"),
|
||||
tcDefaults.controllerMarginTop
|
||||
);
|
||||
settings.controllerMarginBottom = clampMarginPxInput(
|
||||
document.getElementById("controllerMarginBottom"),
|
||||
tcDefaults.controllerMarginBottom
|
||||
);
|
||||
|
||||
settings.keyBindings = keyBindings;
|
||||
settings.enableSubtitleNudge =
|
||||
document.getElementById("enableSubtitleNudge").checked;
|
||||
@@ -647,40 +704,69 @@ function save_options() {
|
||||
// Handle Enable toggle
|
||||
rule.enabled = ruleEl.querySelector(".site-enabled").checked;
|
||||
|
||||
// Handle other site settings
|
||||
const siteSettings = [
|
||||
{ key: "startHidden", type: "checkbox" },
|
||||
{ key: "hideWithControls", type: "checkbox" },
|
||||
{ key: "hideWithControlsTimer", type: "text" },
|
||||
{ key: "controllerLocation", type: "select" },
|
||||
{ key: "rememberSpeed", type: "checkbox" },
|
||||
{ key: "forceLastSavedSpeed", type: "checkbox" },
|
||||
{ key: "audioBoolean", type: "checkbox" },
|
||||
{ key: "controllerOpacity", type: "text" },
|
||||
{ key: "showPopupControlBar", type: "checkbox" },
|
||||
{ key: "enableSubtitleNudge", type: "checkbox" },
|
||||
{ key: "subtitleNudgeInterval", type: "text" }
|
||||
];
|
||||
if (ruleEl.querySelector(".override-placement").checked) {
|
||||
rule.controllerLocation = normalizeControllerLocation(
|
||||
ruleEl.querySelector(".site-controllerLocation").value
|
||||
);
|
||||
rule.controllerMarginTop = clampMarginPxInput(
|
||||
ruleEl.querySelector(".site-controllerMarginTop"),
|
||||
clampMarginPxInput(
|
||||
document.getElementById("controllerMarginTop"),
|
||||
tcDefaults.controllerMarginTop
|
||||
)
|
||||
);
|
||||
rule.controllerMarginBottom = clampMarginPxInput(
|
||||
ruleEl.querySelector(".site-controllerMarginBottom"),
|
||||
clampMarginPxInput(
|
||||
document.getElementById("controllerMarginBottom"),
|
||||
tcDefaults.controllerMarginBottom
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
siteSettings.forEach((s) => {
|
||||
var input = ruleEl.querySelector(`.site-${s.key}`);
|
||||
if (!input) return;
|
||||
var siteValue;
|
||||
if (s.type === "checkbox") {
|
||||
siteValue = input.checked;
|
||||
} else {
|
||||
siteValue = input.value;
|
||||
}
|
||||
var globalInput = document.getElementById(s.key);
|
||||
if (globalInput) {
|
||||
var globalValue = s.type === "checkbox" ? globalInput.checked : globalInput.value;
|
||||
if (String(siteValue) !== String(globalValue)) {
|
||||
rule[s.key] = siteValue;
|
||||
}
|
||||
} else {
|
||||
rule[s.key] = siteValue;
|
||||
}
|
||||
});
|
||||
if (ruleEl.querySelector(".override-visibility").checked) {
|
||||
rule.startHidden = ruleEl.querySelector(".site-startHidden").checked;
|
||||
}
|
||||
|
||||
if (ruleEl.querySelector(".override-autohide").checked) {
|
||||
rule.hideWithControls = ruleEl.querySelector(".site-hideWithControls").checked;
|
||||
var st = parseFloat(
|
||||
ruleEl.querySelector(".site-hideWithControlsTimer").value
|
||||
);
|
||||
rule.hideWithControlsTimer = Math.min(
|
||||
15,
|
||||
Math.max(0.1, Number.isFinite(st) ? st : settings.hideWithControlsTimer)
|
||||
);
|
||||
}
|
||||
|
||||
if (ruleEl.querySelector(".override-playback").checked) {
|
||||
rule.rememberSpeed = ruleEl.querySelector(".site-rememberSpeed").checked;
|
||||
rule.forceLastSavedSpeed =
|
||||
ruleEl.querySelector(".site-forceLastSavedSpeed").checked;
|
||||
rule.audioBoolean = ruleEl.querySelector(".site-audioBoolean").checked;
|
||||
}
|
||||
|
||||
if (ruleEl.querySelector(".override-opacity").checked) {
|
||||
rule.controllerOpacity =
|
||||
parseFloat(ruleEl.querySelector(".site-controllerOpacity").value) ||
|
||||
settings.controllerOpacity;
|
||||
}
|
||||
|
||||
if (ruleEl.querySelector(".override-subtitleNudge").checked) {
|
||||
rule.enableSubtitleNudge =
|
||||
ruleEl.querySelector(".site-enableSubtitleNudge").checked;
|
||||
var nudgeIv = parseInt(
|
||||
ruleEl.querySelector(".site-subtitleNudgeInterval").value,
|
||||
10
|
||||
);
|
||||
rule.subtitleNudgeInterval = Math.min(
|
||||
1000,
|
||||
Math.max(
|
||||
10,
|
||||
Number.isFinite(nudgeIv) ? nudgeIv : settings.subtitleNudgeInterval
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (ruleEl.querySelector(".override-controlbar").checked) {
|
||||
var activeZone = ruleEl.querySelector(".site-cb-active");
|
||||
@@ -690,6 +776,8 @@ function save_options() {
|
||||
}
|
||||
|
||||
if (ruleEl.querySelector(".override-popup-controlbar").checked) {
|
||||
rule.showPopupControlBar =
|
||||
ruleEl.querySelector(".site-showPopupControlBar").checked;
|
||||
var popupActiveZone = ruleEl.querySelector(".site-popup-cb-active");
|
||||
if (popupActiveZone) {
|
||||
rule.popupControllerButtons = readControlBarOrder(popupActiveZone);
|
||||
@@ -824,9 +912,11 @@ function addSiteRuleShortcut(container, action, binding, value, force) {
|
||||
valueInput.className = "customValue";
|
||||
valueInput.type = "text";
|
||||
valueInput.placeholder = "value (0.10)";
|
||||
valueInput.value = value || 0;
|
||||
if (customActionsNoValues.includes(action)) {
|
||||
valueInput.value = "N/A";
|
||||
valueInput.disabled = true;
|
||||
} else {
|
||||
valueInput.value = formatSpeedBindingDisplay(action, value || 0);
|
||||
}
|
||||
|
||||
var forceLabel = document.createElement("label");
|
||||
@@ -892,45 +982,68 @@ function createSiteRule(rule) {
|
||||
}
|
||||
updateDisabledState();
|
||||
|
||||
const settings = [
|
||||
{ key: "startHidden", type: "checkbox" },
|
||||
{ key: "hideWithControls", type: "checkbox" },
|
||||
{ key: "hideWithControlsTimer", type: "text" },
|
||||
{ key: "controllerLocation", type: "select" },
|
||||
{ key: "rememberSpeed", type: "checkbox" },
|
||||
{ key: "forceLastSavedSpeed", type: "checkbox" },
|
||||
{ key: "audioBoolean", type: "checkbox" },
|
||||
{ key: "controllerOpacity", type: "text" },
|
||||
{ key: "showPopupControlBar", type: "checkbox" },
|
||||
{ key: "enableSubtitleNudge", type: "checkbox" },
|
||||
{ key: "subtitleNudgeInterval", type: "text" }
|
||||
var placementKeys = [
|
||||
"controllerLocation",
|
||||
"controllerMarginTop",
|
||||
"controllerMarginBottom"
|
||||
];
|
||||
var hasPlacementOverride =
|
||||
rule && placementKeys.some(function (k) { return rule[k] !== undefined; });
|
||||
if (hasPlacementOverride) {
|
||||
ruleEl.querySelector(".override-placement").checked = true;
|
||||
ruleEl.querySelector(".site-placement-container").style.display = "block";
|
||||
}
|
||||
syncSiteRuleField(ruleEl, rule, "controllerLocation", false);
|
||||
syncSiteRuleField(ruleEl, rule, "controllerMarginTop", false);
|
||||
syncSiteRuleField(ruleEl, rule, "controllerMarginBottom", false);
|
||||
|
||||
settings.forEach((s) => {
|
||||
var input = ruleEl.querySelector(`.site-${s.key}`);
|
||||
if (!input) return;
|
||||
if (rule && rule.startHidden !== undefined) {
|
||||
ruleEl.querySelector(".override-visibility").checked = true;
|
||||
ruleEl.querySelector(".site-visibility-container").style.display = "block";
|
||||
}
|
||||
syncSiteRuleField(ruleEl, rule, "startHidden", true);
|
||||
|
||||
var value;
|
||||
if (rule && rule[s.key] !== undefined) {
|
||||
value = rule[s.key];
|
||||
} else {
|
||||
// Initialize with current global value
|
||||
var globalInput = document.getElementById(s.key);
|
||||
if (globalInput) {
|
||||
if (s.type === "checkbox") {
|
||||
value = globalInput.checked;
|
||||
} else {
|
||||
value = globalInput.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
rule &&
|
||||
(rule.hideWithControls !== undefined ||
|
||||
rule.hideWithControlsTimer !== undefined)
|
||||
) {
|
||||
ruleEl.querySelector(".override-autohide").checked = true;
|
||||
ruleEl.querySelector(".site-autohide-container").style.display = "block";
|
||||
}
|
||||
syncSiteRuleField(ruleEl, rule, "hideWithControls", true);
|
||||
syncSiteRuleField(ruleEl, rule, "hideWithControlsTimer", false);
|
||||
|
||||
if (s.type === "checkbox") {
|
||||
input.checked = value;
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
});
|
||||
if (
|
||||
rule &&
|
||||
(rule.rememberSpeed !== undefined ||
|
||||
rule.forceLastSavedSpeed !== undefined ||
|
||||
rule.audioBoolean !== undefined)
|
||||
) {
|
||||
ruleEl.querySelector(".override-playback").checked = true;
|
||||
ruleEl.querySelector(".site-playback-container").style.display = "block";
|
||||
}
|
||||
syncSiteRuleField(ruleEl, rule, "rememberSpeed", true);
|
||||
syncSiteRuleField(ruleEl, rule, "forceLastSavedSpeed", true);
|
||||
syncSiteRuleField(ruleEl, rule, "audioBoolean", true);
|
||||
|
||||
if (rule && rule.controllerOpacity !== undefined) {
|
||||
ruleEl.querySelector(".override-opacity").checked = true;
|
||||
ruleEl.querySelector(".site-opacity-container").style.display = "block";
|
||||
}
|
||||
syncSiteRuleField(ruleEl, rule, "controllerOpacity", false);
|
||||
|
||||
if (
|
||||
rule &&
|
||||
(rule.enableSubtitleNudge !== undefined ||
|
||||
rule.subtitleNudgeInterval !== undefined)
|
||||
) {
|
||||
ruleEl.querySelector(".override-subtitleNudge").checked = true;
|
||||
ruleEl.querySelector(".site-subtitleNudge-container").style.display =
|
||||
"block";
|
||||
}
|
||||
syncSiteRuleField(ruleEl, rule, "enableSubtitleNudge", true);
|
||||
syncSiteRuleField(ruleEl, rule, "subtitleNudgeInterval", false);
|
||||
|
||||
if (rule && Array.isArray(rule.controllerButtons)) {
|
||||
ruleEl.querySelector(".override-controlbar").checked = true;
|
||||
@@ -943,16 +1056,35 @@ function createSiteRule(rule) {
|
||||
);
|
||||
}
|
||||
|
||||
if (rule && Array.isArray(rule.popupControllerButtons)) {
|
||||
if (
|
||||
rule &&
|
||||
(rule.showPopupControlBar !== undefined ||
|
||||
Array.isArray(rule.popupControllerButtons))
|
||||
) {
|
||||
ruleEl.querySelector(".override-popup-controlbar").checked = true;
|
||||
var popupCbContainer = ruleEl.querySelector(".site-popup-controlbar-container");
|
||||
popupCbContainer.style.display = "block";
|
||||
populateControlBarZones(
|
||||
ruleEl.querySelector(".site-popup-cb-active"),
|
||||
ruleEl.querySelector(".site-popup-cb-available"),
|
||||
rule.popupControllerButtons
|
||||
);
|
||||
var sitePopupActive = ruleEl.querySelector(".site-popup-cb-active");
|
||||
var sitePopupAvailable = ruleEl.querySelector(".site-popup-cb-available");
|
||||
if (Array.isArray(rule.popupControllerButtons)) {
|
||||
populateControlBarZones(
|
||||
sitePopupActive,
|
||||
sitePopupAvailable,
|
||||
rule.popupControllerButtons
|
||||
);
|
||||
} else if (
|
||||
sitePopupActive &&
|
||||
sitePopupAvailable &&
|
||||
sitePopupActive.children.length === 0
|
||||
) {
|
||||
populateControlBarZones(
|
||||
sitePopupActive,
|
||||
sitePopupAvailable,
|
||||
getPopupControlBarOrder()
|
||||
);
|
||||
}
|
||||
}
|
||||
syncSiteRuleField(ruleEl, rule, "showPopupControlBar", true);
|
||||
|
||||
if (rule && Array.isArray(rule.shortcuts) && rule.shortcuts.length > 0) {
|
||||
ruleEl.querySelector(".override-shortcuts").checked = true;
|
||||
@@ -1170,6 +1302,10 @@ function restore_options() {
|
||||
normalizeControllerLocation(storage.controllerLocation);
|
||||
document.getElementById("controllerOpacity").value =
|
||||
storage.controllerOpacity;
|
||||
document.getElementById("controllerMarginTop").value =
|
||||
storage.controllerMarginTop ?? tcDefaults.controllerMarginTop;
|
||||
document.getElementById("controllerMarginBottom").value =
|
||||
storage.controllerMarginBottom ?? tcDefaults.controllerMarginBottom;
|
||||
document.getElementById("showPopupControlBar").checked =
|
||||
storage.showPopupControlBar !== false;
|
||||
document.getElementById("enableSubtitleNudge").checked =
|
||||
@@ -1208,7 +1344,7 @@ function restore_options() {
|
||||
valueInput.disabled = true;
|
||||
}
|
||||
} else if (valueInput) {
|
||||
valueInput.value = item.value;
|
||||
valueInput.value = formatSpeedBindingDisplay(item.action, item.value);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1353,6 +1489,28 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
}
|
||||
|
||||
// Site rule: show/hide optional override sections
|
||||
var siteOverrideContainers = {
|
||||
"override-placement": "site-placement-container",
|
||||
"override-visibility": "site-visibility-container",
|
||||
"override-autohide": "site-autohide-container",
|
||||
"override-playback": "site-playback-container",
|
||||
"override-opacity": "site-opacity-container",
|
||||
"override-subtitleNudge": "site-subtitleNudge-container"
|
||||
};
|
||||
for (var ocb in siteOverrideContainers) {
|
||||
if (event.target.classList.contains(ocb)) {
|
||||
var siteRuleRoot = event.target.closest(".site-rule");
|
||||
var targetBox = siteRuleRoot.querySelector(
|
||||
"." + siteOverrideContainers[ocb]
|
||||
);
|
||||
if (targetBox) {
|
||||
targetBox.style.display = event.target.checked ? "block" : "none";
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle site rule override checkboxes
|
||||
if (event.target.classList.contains("override-shortcuts")) {
|
||||
var container = event.target
|
||||
|
||||
@@ -7,7 +7,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
faster: { label: "+", className: "" },
|
||||
advance: { label: "\u00BB", className: "rw" },
|
||||
display: { label: "\u00D7", className: "hideButton" },
|
||||
reset: { label: "\u21BA", className: "" },
|
||||
reset: { label: "1.00x", className: "" },
|
||||
fast: { label: "\u2605", className: "" },
|
||||
settings: { label: "\u2699", className: "" },
|
||||
pause: { label: "\u23EF", className: "" },
|
||||
@@ -171,11 +171,76 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
if (el) el.textContent = (speed != null ? Number(speed) : 1).toFixed(2);
|
||||
}
|
||||
|
||||
function querySpeed() {
|
||||
sendToActiveTab({ action: "get_speed" }, function (response) {
|
||||
if (response && response.speed != null) {
|
||||
updateSpeedDisplay(response.speed);
|
||||
function updatePopupResetLabel(resetLabel) {
|
||||
var bar = document.getElementById("popupControlBar");
|
||||
if (!bar || typeof resetLabel !== "string") return;
|
||||
var btn = bar.querySelector('button[data-action="reset"]');
|
||||
if (btn) btn.textContent = resetLabel;
|
||||
}
|
||||
|
||||
function applySpeedAndResetFromResponse(response) {
|
||||
if (response && response.speed != null) {
|
||||
updateSpeedDisplay(response.speed);
|
||||
}
|
||||
if (response && response.resetLabel != null) {
|
||||
updatePopupResetLabel(response.resetLabel);
|
||||
}
|
||||
}
|
||||
|
||||
function pickBestFrameSpeedResult(results) {
|
||||
if (!results || !results.length) return null;
|
||||
var i;
|
||||
var r;
|
||||
var fallback = null;
|
||||
for (i = 0; i < results.length; i++) {
|
||||
r = results[i];
|
||||
if (
|
||||
!r ||
|
||||
typeof r.speed !== "number" ||
|
||||
typeof r.resetLabel !== "string"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (r.preferred) {
|
||||
return { speed: r.speed, resetLabel: r.resetLabel };
|
||||
}
|
||||
if (!fallback) {
|
||||
fallback = { speed: r.speed, resetLabel: r.resetLabel };
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function querySpeed() {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
|
||||
if (!tabs[0] || tabs[0].id == null) {
|
||||
return;
|
||||
}
|
||||
var tabId = tabs[0].id;
|
||||
chrome.tabs.executeScript(
|
||||
tabId,
|
||||
{ allFrames: true, file: "frameSpeedSnapshot.js" },
|
||||
function (results) {
|
||||
if (chrome.runtime.lastError) {
|
||||
sendToActiveTab({ action: "get_speed" }, function (response) {
|
||||
applySpeedAndResetFromResponse(
|
||||
response || { speed: 1, resetLabel: "1.00x" }
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
var best = pickBestFrameSpeedResult(results);
|
||||
if (best) {
|
||||
applySpeedAndResetFromResponse(best);
|
||||
} else {
|
||||
sendToActiveTab({ action: "get_speed" }, function (response) {
|
||||
applySpeedAndResetFromResponse(
|
||||
response || { speed: 1, resetLabel: "1.00x" }
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -204,10 +269,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
sendToActiveTab(
|
||||
{ action: "run_action", actionName: btnId },
|
||||
function (response) {
|
||||
if (response && response.speed != null) {
|
||||
updateSpeedDisplay(response.speed);
|
||||
}
|
||||
function () {
|
||||
querySpeed();
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -297,12 +360,14 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
if (blacklisted) {
|
||||
setStatusMessage("Site is blacklisted.");
|
||||
updateSpeedDisplay(1);
|
||||
updatePopupResetLabel("1.00x");
|
||||
return;
|
||||
}
|
||||
|
||||
if (siteDisabled) {
|
||||
setStatusMessage("Speeder is disabled for this site.");
|
||||
updateSpeedDisplay(1);
|
||||
updatePopupResetLabel("1.00x");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -311,6 +376,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
querySpeed();
|
||||
} else {
|
||||
updateSpeedDisplay(1);
|
||||
updatePopupResetLabel("1.00x");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Executable
+103
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bump manifest on dev, merge dev→beta→main, push an annotated stable tag (v* without -beta).
|
||||
# Triggers .github/workflows/deploy.yml: listed AMO submission.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(git rev-parse --show-toplevel)"
|
||||
cd "$ROOT"
|
||||
|
||||
manifest_version() {
|
||||
python3 -c 'import json; print(json.load(open("manifest.json"))["version"])'
|
||||
}
|
||||
|
||||
bump_manifest() {
|
||||
local ver="$1"
|
||||
VER="$ver" python3 <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
ver = os.environ["VER"]
|
||||
path = "manifest.json"
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
data["version"] = ver
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
PY
|
||||
}
|
||||
|
||||
normalize_semver() {
|
||||
local s="$1"
|
||||
s="${s#"${s%%[![:space:]]*}"}"
|
||||
s="${s%"${s##*[![:space:]]}"}"
|
||||
s="${s#v}"
|
||||
s="${s#V}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
validate_semver() {
|
||||
local s="$1"
|
||||
if [[ -z "$s" ]]; then
|
||||
echo "Error: empty version." >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ ! "$s" =~ ^[0-9]+(\.[0-9]+){0,3}(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
|
||||
echo "Error: invalid version (use something like 5.0.4)." >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
echo "Error: working tree is not clean. Commit or stash before releasing." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git checkout dev
|
||||
git pull origin dev
|
||||
|
||||
echo "Current version in manifest.json: $(manifest_version)"
|
||||
read -r -p "New version for manifest.json (e.g. 5.0.4): " SEMVER_IN
|
||||
SEMVER="$(normalize_semver "$SEMVER_IN")"
|
||||
validate_semver "$SEMVER"
|
||||
|
||||
TAG="v${SEMVER}"
|
||||
if [[ "$TAG" == *-beta* ]]; then
|
||||
echo "Warning: stable tags should not contain '-beta' (workflow would use unlisted + prerelease, not AMO listed)."
|
||||
read -r -p "Continue anyway? [y/N] " w
|
||||
[[ "${w:-}" =~ ^[yY](es)?$ ]] || { echo "Aborted."; exit 1; }
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "This will:"
|
||||
echo " 1. set manifest.json version to $SEMVER and commit on dev"
|
||||
echo " 2. merge dev → beta and push beta"
|
||||
echo " 3. merge beta → main and push main"
|
||||
echo " 4. create tag $TAG on main and push it (triggers listed AMO submit)"
|
||||
echo " 5. checkout dev"
|
||||
read -r -p "Continue? [y/N] " confirm
|
||||
[[ "${confirm:-}" =~ ^[yY](es)?$ ]] || { echo "Aborted."; exit 1; }
|
||||
|
||||
echo "🚀 Releasing stable $TAG to AMO (listed)"
|
||||
|
||||
bump_manifest "$SEMVER"
|
||||
git add manifest.json
|
||||
git commit -m "Bump version to $SEMVER"
|
||||
|
||||
git checkout beta
|
||||
git pull origin beta
|
||||
git merge dev --no-ff -m "Merge dev ($TAG)"
|
||||
git push origin beta
|
||||
|
||||
git checkout main
|
||||
git pull origin main
|
||||
git merge beta --no-ff -m "Merge beta ($TAG)"
|
||||
git push origin main
|
||||
|
||||
git tag -a "$TAG" -m "$TAG"
|
||||
git push origin "$TAG"
|
||||
|
||||
git checkout dev
|
||||
|
||||
echo "✅ Done: stable $TAG (manifest $SEMVER, main + tag pushed)"
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env bash
|
||||
# Merge dev → beta, push beta, and push an annotated beta tag (v*-beta*).
|
||||
# Triggers .github/workflows/deploy.yml: unlisted AMO sign + GitHub prerelease.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(git rev-parse --show-toplevel)"
|
||||
cd "$ROOT"
|
||||
|
||||
manifest_version() {
|
||||
python3 -c 'import json; print(json.load(open("manifest.json"))["version"])'
|
||||
}
|
||||
|
||||
bump_manifest() {
|
||||
local ver="$1"
|
||||
VER="$ver" python3 <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
ver = os.environ["VER"]
|
||||
path = "manifest.json"
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
data["version"] = ver
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
PY
|
||||
}
|
||||
|
||||
normalize_semver() {
|
||||
local s="$1"
|
||||
s="${s#"${s%%[![:space:]]*}"}"
|
||||
s="${s%"${s##*[![:space:]]}"}"
|
||||
s="${s#v}"
|
||||
s="${s#V}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
validate_semver() {
|
||||
local s="$1"
|
||||
if [[ -z "$s" ]]; then
|
||||
echo "Error: empty version." >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ ! "$s" =~ ^[0-9]+(\.[0-9]+){0,3}(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
|
||||
echo "Error: invalid version (use something like 5.0.4 or 5.0.4-beta.1)." >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
echo "Error: working tree is not clean. Commit or stash before releasing." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git checkout dev
|
||||
git pull origin dev
|
||||
|
||||
echo "Current version in manifest.json: $(manifest_version)"
|
||||
read -r -p "New version for manifest.json (e.g. 5.0.4): " SEMVER_IN
|
||||
SEMVER="$(normalize_semver "$SEMVER_IN")"
|
||||
validate_semver "$SEMVER"
|
||||
|
||||
echo "Beta git tag will include '-beta' (required by deploy.yml)."
|
||||
read -r -p "Beta tag suffix [beta.1]: " SUFFIX_IN
|
||||
SUFFIX="${SUFFIX_IN#"${SUFFIX_IN%%[![:space:]]*}"}"
|
||||
SUFFIX="${SUFFIX%"${SUFFIX##*[![:space:]]}"}"
|
||||
SUFFIX="${SUFFIX:-beta.1}"
|
||||
|
||||
TAG="v${SEMVER}-${SUFFIX}"
|
||||
if [[ "$TAG" != *-beta* ]]; then
|
||||
echo "Error: beta tag must contain '-beta' for the workflow (got $TAG). Try suffix like beta.1." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "This will:"
|
||||
echo " 1. set manifest.json version to $SEMVER and commit on dev"
|
||||
echo " 2. checkout beta, merge dev (no-ff), push origin beta"
|
||||
echo " 3. create tag $TAG and push it (triggers beta AMO + prerelease)"
|
||||
echo " 4. checkout dev"
|
||||
read -r -p "Continue? [y/N] " confirm
|
||||
[[ "${confirm:-}" =~ ^[yY](es)?$ ]] || { echo "Aborted."; exit 1; }
|
||||
|
||||
echo "🚀 Releasing beta $TAG"
|
||||
|
||||
bump_manifest "$SEMVER"
|
||||
git add manifest.json
|
||||
git commit -m "Bump version to $SEMVER"
|
||||
|
||||
git checkout beta
|
||||
git pull origin beta
|
||||
git merge dev --no-ff -m "$TAG"
|
||||
git push origin beta
|
||||
|
||||
git tag -a "$TAG" -m "$TAG"
|
||||
git push origin "$TAG"
|
||||
|
||||
git checkout dev
|
||||
|
||||
echo "✅ Done: beta $TAG (manifest $SEMVER, merge + tag pushed)"
|
||||
+12
-7
@@ -29,6 +29,11 @@
|
||||
z-index: 1;
|
||||
transition: top 160ms ease, left 160ms ease, transform 160ms ease,
|
||||
opacity 160ms ease;
|
||||
/* Insets are baked into left/top; bar must not wrap when narrow. Hover controls
|
||||
extend into the inset area (past “logical” margin). */
|
||||
white-space: nowrap;
|
||||
overflow: visible;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
#controller:hover {
|
||||
@@ -39,20 +44,20 @@
|
||||
margin-right: 0.8em;
|
||||
}
|
||||
|
||||
/* For center positions, override transform to expand from left edge instead of center.
|
||||
Exclude manual mode so dragging can freely reposition the controller. */
|
||||
#controller[data-location="top-center"]:not([data-position-mode="manual"]),
|
||||
#controller[data-location="bottom-center"]:not([data-position-mode="manual"]) {
|
||||
transform: translate(0, 0) !important;
|
||||
left: calc(50% - 30px) !important;
|
||||
/* Center presets: midpoint between left- and right-preset inset lines; center bar on that X. */
|
||||
#controller[data-location="top-center"]:not([data-position-mode="manual"]) {
|
||||
transform: translate(-50%, 0) !important;
|
||||
}
|
||||
|
||||
#controller[data-location="bottom-center"]:not([data-position-mode="manual"]) {
|
||||
transform: translate(0, -100%) !important;
|
||||
transform: translate(-50%, -100%) !important;
|
||||
}
|
||||
|
||||
#controls {
|
||||
display: none;
|
||||
white-space: nowrap;
|
||||
overflow: visible;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
#controls > * + * {
|
||||
|
||||
Reference in New Issue
Block a user