Compare commits

...

7 Commits

10 changed files with 569 additions and 106 deletions
+4 -2
View File
@@ -46,7 +46,7 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: ${{ github.ref_name }} tag_name: ${{ github.ref_name }}
name: Beta ${{ github.ref_name }} name: ${{ github.ref_name }}
files: ${{ steps.xpi.outputs.file }} files: ${{ steps.xpi.outputs.file }}
prerelease: true prerelease: true
body: | body: |
@@ -61,7 +61,9 @@ jobs:
# Stable tag (v* without -beta) → Sign & submit to public AMO listing # Stable tag (v* without -beta) → Sign & submit to public AMO listing
- name: Sign & Submit to AMO (stable) - 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: | run: |
web-ext sign \ web-ext sign \
--api-key ${{ secrets.FIREFOX_API_KEY }} \ --api-key ${{ secrets.FIREFOX_API_KEY }} \
+20
View File
@@ -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;
}
})();
+97 -28
View File
@@ -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 isUserSeek = false; // Track if seek was user-initiated
var lastToggleSpeed = {}; // Store last toggle speeds per video 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 = { var tc = {
settings: { settings: {
lastSpeed: 1.0, lastSpeed: 1.0,
@@ -112,7 +121,7 @@ var controllerButtonDefs = {
faster: { label: "+", className: "" }, faster: { label: "+", className: "" },
advance: { label: "\u00BB", className: "rw" }, advance: { label: "\u00BB", className: "rw" },
display: { label: "\u00D7", className: "hideButton" }, display: { label: "\u00D7", className: "hideButton" },
reset: { label: "\u21BA", className: "" }, reset: { label: "1.00x", className: "" },
fast: { label: "\u2605", className: "" }, fast: { label: "\u2605", className: "" },
settings: { label: "\u2699", className: "" }, settings: { label: "\u2699", className: "" },
pause: { label: "\u23EF", className: "" }, pause: { label: "\u23EF", className: "" },
@@ -1267,36 +1276,38 @@ chrome.storage.sync.get(tc.settings, function (storage) {
log("Re-scan command received from popup.", 4); log("Re-scan command received from popup.", 4);
initializeWhenReady(document, true); initializeWhenReady(document, true);
sendResponse({ status: "complete" }); sendResponse({ status: "complete" });
} else if (request.action === "get_speed") { return false;
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 (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 (speed === 1.0 && tc.mediaElements[0]) { if (request.action === "get_page_context") {
speed = tc.mediaElements[0].playbackRate;
}
}
sendResponse({ speed: speed });
} else if (request.action === "get_page_context") {
sendResponse({ url: location.href }); sendResponse({ url: location.href });
} else if (request.action === "run_action") { return false;
}
if (request.action === "run_action") {
var value = request.value; var value = request.value;
if (value === undefined || value === null) { if (value === undefined || value === null) {
value = getKeyBindings(request.actionName, "value"); value = getKeyBindings(request.actionName, "value");
} }
runAction(request.actionName, value); runAction(request.actionName, value);
var newSpeed = 1.0; var videoAfter = getPrimaryVideoElement();
if (tc.mediaElements && tc.mediaElements.length > 0) { if (!videoAfter) return false;
newSpeed = tc.mediaElements[0].playbackRate; sendResponse({
speed: videoAfter.playbackRate,
resetLabel: computeResetButtonLabelForVideo(videoAfter)
});
return false;
} }
sendResponse({ speed: newSpeed }); return false;
}
return true;
} }
); );
@@ -1343,6 +1354,8 @@ function defineVideoController() {
this.suppressedRateChangeCount = 0; this.suppressedRateChangeCount = 0;
this.suppressedRateChangeUntil = 0; this.suppressedRateChangeUntil = 0;
this.visibilityResumeHandler = null; this.visibilityResumeHandler = null;
this.resetToggleArmed = false;
this.resetButtonEl = null;
this.controllerLocation = normalizeControllerLocation( this.controllerLocation = normalizeControllerLocation(
tc.settings.controllerLocation tc.settings.controllerLocation
); );
@@ -1843,6 +1856,10 @@ function defineVideoController() {
this.speedIndicator = dragHandle; this.speedIndicator = dragHandle;
this.subtitleNudgeIndicator = subtitleNudgeIndicator; this.subtitleNudgeIndicator = subtitleNudgeIndicator;
this.nudgeFlashIndicator = nudgeFlashIndicator; this.nudgeFlashIndicator = nudgeFlashIndicator;
this.resetButtonEl =
shadow.querySelector('button[data-action="reset"]') || null;
this.resetToggleArmed = false;
updateResetButtonLabel(this.video);
if (subtitleNudgeIndicator) { if (subtitleNudgeIndicator) {
updateSubtitleNudgeIndicator(this.video); updateSubtitleNudgeIndicator(this.video);
} }
@@ -2091,8 +2108,11 @@ function shouldPreserveDesiredSpeed(video, speed) {
function setupListener(root) { function setupListener(root) {
root = root || document; root = root || document;
if (root.vscRateListenerAttached) return; if (root.vscRateListenerAttached) return;
function updateSpeedFromEvent(video) { function updateSpeedFromEvent(video, skipResetDisarm) {
if (!video.vsc || !video.vsc.speedIndicator) return; if (!video.vsc || !video.vsc.speedIndicator) return;
if (!skipResetDisarm) {
video.vsc.resetToggleArmed = false;
}
var speed = video.playbackRate; // Preserve full precision (e.g. 0.0625) var speed = video.playbackRate; // Preserve full precision (e.g. 0.0625)
video.vsc.speedIndicator.textContent = speed.toFixed(2); video.vsc.speedIndicator.textContent = speed.toFixed(2);
video.vsc.targetSpeed = speed; video.vsc.targetSpeed = speed;
@@ -2107,6 +2127,7 @@ function setupListener(root) {
if (speed === 1.0 || video.paused) video.vsc.stopSubtitleNudge(); if (speed === 1.0 || video.paused) video.vsc.stopSubtitleNudge();
else video.vsc.startSubtitleNudge(); else video.vsc.startSubtitleNudge();
} }
updateResetButtonLabel(video);
} }
root.addEventListener( root.addEventListener(
"ratechange", "ratechange",
@@ -2119,7 +2140,7 @@ function setupListener(root) {
if (tc.settings.forceLastSavedSpeed) { if (tc.settings.forceLastSavedSpeed) {
if (event.detail && event.detail.origin === "videoSpeed") { if (event.detail && event.detail.origin === "videoSpeed") {
video.playbackRate = event.detail.speed; video.playbackRate = event.detail.speed;
updateSpeedFromEvent(video); updateSpeedFromEvent(video, true);
} else { } else {
video.playbackRate = sanitizeSpeed(tc.settings.lastSpeed, 1.0); video.playbackRate = sanitizeSpeed(tc.settings.lastSpeed, 1.0);
} }
@@ -2130,7 +2151,7 @@ function setupListener(root) {
var pendingRateChange = takePendingRateChange(video, currentSpeed); var pendingRateChange = takePendingRateChange(video, currentSpeed);
if (pendingRateChange) { if (pendingRateChange) {
updateSpeedFromEvent(video); updateSpeedFromEvent(video, true);
return; return;
} }
@@ -2139,8 +2160,10 @@ function setupListener(root) {
`Ignoring external rate change to ${currentSpeed.toFixed(4)} while preserving ${desiredSpeed.toFixed(4)}`, `Ignoring external rate change to ${currentSpeed.toFixed(4)} while preserving ${desiredSpeed.toFixed(4)}`,
4 4
); );
video.vsc.resetToggleArmed = false;
video.vsc.speedIndicator.textContent = desiredSpeed.toFixed(2); video.vsc.speedIndicator.textContent = desiredSpeed.toFixed(2);
scheduleSpeedRestore(video, desiredSpeed, "pause/play or seek"); scheduleSpeedRestore(video, desiredSpeed, "pause/play or seek");
updateResetButtonLabel(video);
return; return;
} }
@@ -2429,7 +2452,47 @@ function initializeNow(doc, forceReinit = false) {
vscInitializedDocuments.add(doc); 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); const numericSpeed = Number(speed);
if (!isValidSpeed(numericSpeed)) { if (!isValidSpeed(numericSpeed)) {
@@ -2442,6 +2505,10 @@ function setSpeed(video, speed, isInitialCall = false, isUserKeyPress = false) {
if (!video || !video.vsc || !video.vsc.speedIndicator) return; if (!video || !video.vsc || !video.vsc.speedIndicator) return;
if (isUserKeyPress && !fromResetSpeedToggle) {
video.vsc.resetToggleArmed = false;
}
log( log(
`setSpeed: Target ${numericSpeed.toFixed(2)}. Initial: ${isInitialCall}. UserKeyPress: ${isUserKeyPress}`, `setSpeed: Target ${numericSpeed.toFixed(2)}. Initial: ${isInitialCall}. UserKeyPress: ${isUserKeyPress}`,
4 4
@@ -2489,6 +2556,7 @@ function setSpeed(video, speed, isInitialCall = false, isUserKeyPress = false) {
video.vsc.startSubtitleNudge(); video.vsc.startSubtitleNudge();
} }
} }
updateResetButtonLabel(video);
} }
function runAction(action, value, e) { function runAction(action, value, e) {
@@ -2697,11 +2765,12 @@ function resetSpeed(v, target, isFastKey = false) {
Math.abs(lastToggle - 1.0) < 0.01 Math.abs(lastToggle - 1.0) < 0.01
? getKeyBindings("fast") || 1.8 ? getKeyBindings("fast") || 1.8
: lastToggle; : lastToggle;
setSpeed(v, speedToRestore, false, true); setSpeed(v, speedToRestore, false, true, true);
} else { } else {
// Not at 1.0, save current as toggle speed and go to 1.0 // Not at 1.0, save current as toggle speed and go to 1.0
lastToggleSpeed[videoId] = currentSpeed; lastToggleSpeed[videoId] = currentSpeed;
setSpeed(v, resetSpeedValue, false, true); v.vsc.resetToggleArmed = true;
setSpeed(v, resetSpeedValue, false, true, true);
} }
} }
} }
+25 -8
View File
@@ -1,7 +1,7 @@
{ {
"name": "Speeder", "name": "Speeder",
"short_name": "Speeder", "short_name": "Speeder",
"version": "5.0.2", "version": "5.0.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",
@@ -9,7 +9,9 @@
"gecko": { "gecko": {
"id": "{ed860648-f54f-4dc9-9a0d-501aec4313f5}", "id": "{ed860648-f54f-4dc9-9a0d-501aec4313f5}",
"data_collection_permissions": { "data_collection_permissions": {
"required": ["none"] "required": [
"none"
]
} }
} }
}, },
@@ -19,9 +21,13 @@
"128": "icons/icon128.png" "128": "icons/icon128.png"
}, },
"background": { "background": {
"scripts": ["background.js"] "scripts": [
"background.js"
]
}, },
"permissions": ["storage"], "permissions": [
"storage"
],
"options_ui": { "options_ui": {
"page": "options.html", "page": "options.html",
"open_in_tab": false "open_in_tab": false
@@ -37,16 +43,27 @@
"content_scripts": [ "content_scripts": [
{ {
"all_frames": true, "all_frames": true,
"matches": ["http://*/*", "https://*/*", "file:///*"], "matches": [
"http://*/*",
"https://*/*",
"file:///*"
],
"match_about_blank": true, "match_about_blank": true,
"exclude_matches": [ "exclude_matches": [
"https://plus.google.com/hangouts/*", "https://plus.google.com/hangouts/*",
"https://hangouts.google.com/*", "https://hangouts.google.com/*",
"https://meet.google.com/*" "https://meet.google.com/*"
], ],
"css": ["inject.css"], "css": [
"js": ["inject.js"] "inject.css"
],
"js": [
"inject.js"
]
} }
], ],
"web_accessible_resources": ["inject.css", "shadow.css"] "web_accessible_resources": [
"inject.css",
"shadow.css"
]
} }
+89 -20
View File
@@ -299,6 +299,19 @@ label em {
border-top: 1px solid var(--border); 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 { .settings-card .row:first-of-type {
padding-top: 0; padding-top: 0;
border-top: 0; border-top: 0;
@@ -310,16 +323,17 @@ label em {
.controller-margin-inputs { .controller-margin-inputs {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 116px));
gap: 8px; gap: 8px;
width: 100%; width: max-content;
justify-self: end;
} }
.margin-pad-cell { .margin-pad-cell {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
min-width: 0; min-width: 116px;
} }
.margin-pad-mini { .margin-pad-mini {
@@ -332,12 +346,13 @@ label em {
.controller-margin-inputs input[type="text"] { .controller-margin-inputs input[type="text"] {
width: 100%; width: 100%;
min-width: 0; min-width: 116px;
box-sizing: border-box; box-sizing: border-box;
text-align: right;
} }
.site-rule-option.site-rule-margin-option { .site-rule-option.site-rule-margin-option {
grid-template-columns: minmax(0, 1fr) minmax(0, 220px); grid-template-columns: minmax(0, 1fr) minmax(0, 260px);
} }
.site-rule-override-section { .site-rule-override-section {
@@ -353,19 +368,25 @@ label em {
} }
.site-override-lead { .site-override-lead {
display: flex; display: grid;
grid-template-columns: minmax(0, 1fr) 24px;
gap: 16px;
align-items: flex-start; align-items: flex-start;
gap: 10px;
font-weight: 600; font-weight: 600;
margin-bottom: 8px; margin-bottom: 8px;
cursor: pointer; cursor: pointer;
width: auto; width: 100%;
} }
.site-override-lead input { .site-override-lead input[type="checkbox"] {
justify-self: end;
margin-top: 3px; margin-top: 3px;
} }
.site-override-lead span {
margin: 0;
}
.site-rule-override-section .site-override-fields, .site-rule-override-section .site-override-fields,
.site-rule-override-section .site-placement-container, .site-rule-override-section .site-placement-container,
.site-rule-override-section .site-visibility-container, .site-rule-override-section .site-visibility-container,
@@ -525,24 +546,61 @@ label em {
.site-rule-option { .site-rule-option {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) 150px; grid-template-columns: minmax(0, 1fr) 160px;
gap: 16px; gap: 16px;
align-items: start; align-items: start;
padding: 8px 0; padding: 8px 0;
border-top: 1px solid var(--border); 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-body > .site-rule-option:first-child,
.site-rule-content > .site-rule-option:first-child { .site-rule-content > .site-rule-option:first-child {
padding-top: 0; padding-top: 0;
border-top: 0; border-top: 0;
} }
.site-rule-option label { .site-rule-option > label:not(.site-rule-split-label) {
display: flex; display: block;
margin: 0;
}
.site-rule-split-label {
display: grid;
grid-template-columns: minmax(0, 1fr) 24px;
gap: 16px;
align-items: flex-start; align-items: flex-start;
gap: 10px; width: 100%;
width: auto; 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, .site-rule-controlbar,
@@ -552,12 +610,8 @@ label em {
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
} }
.site-rule-controlbar > label, .site-rule-controlbar > label.site-override-lead,
.site-rule-shortcuts > label { .site-rule-shortcuts > label.site-override-lead {
display: flex;
align-items: flex-start;
gap: 10px;
width: auto;
margin: 0; margin: 0;
} }
@@ -639,11 +693,26 @@ label em {
.shortcut-row, .shortcut-row,
.shortcut-row.customs, .shortcut-row.customs,
.row, .row,
.row.row-checkbox,
.site-rule-option, .site-rule-option,
.site-shortcuts-container .shortcut-row { .site-shortcuts-container .shortcut-row {
grid-template-columns: 1fr; 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, .action-row button,
#addShortcutSelector { #addShortcutSelector {
width: 100%; width: 100%;
+33 -33
View File
@@ -180,11 +180,11 @@
<h4 class="defaults-sub-heading">General</h4> <h4 class="defaults-sub-heading">General</h4>
<div class="row"> <div class="row row-checkbox">
<label for="enabled">Enable</label> <label for="enabled">Enable</label>
<input id="enabled" type="checkbox" /> <input id="enabled" type="checkbox" />
</div> </div>
<div class="row"> <div class="row row-checkbox">
<label for="audioBoolean">Work on audio</label> <label for="audioBoolean">Work on audio</label>
<input id="audioBoolean" type="checkbox" /> <input id="audioBoolean" type="checkbox" />
</div> </div>
@@ -192,11 +192,11 @@
<div class="defaults-divider"></div> <div class="defaults-divider"></div>
<h4 class="defaults-sub-heading">Playback</h4> <h4 class="defaults-sub-heading">Playback</h4>
<div class="row"> <div class="row row-checkbox">
<label for="rememberSpeed">Remember playback speed</label> <label for="rememberSpeed">Remember playback speed</label>
<input id="rememberSpeed" type="checkbox" /> <input id="rememberSpeed" type="checkbox" />
</div> </div>
<div class="row"> <div class="row row-checkbox">
<label for="forceLastSavedSpeed" <label for="forceLastSavedSpeed"
>Force last saved speed<br /> >Force last saved speed<br />
<em <em
@@ -210,7 +210,7 @@
<div class="defaults-divider"></div> <div class="defaults-divider"></div>
<h4 class="defaults-sub-heading">Controller</h4> <h4 class="defaults-sub-heading">Controller</h4>
<div class="row"> <div class="row row-checkbox">
<label for="startHidden">Hide controller by default</label> <label for="startHidden">Hide controller by default</label>
<input id="startHidden" type="checkbox" /> <input id="startHidden" type="checkbox" />
</div> </div>
@@ -250,7 +250,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row row-checkbox">
<label for="hideWithControls" <label for="hideWithControls"
>Hide with controls<br /> >Hide with controls<br />
<em <em
@@ -270,7 +270,7 @@
</label> </label>
<input id="hideWithControlsTimer" type="text" placeholder="2" /> <input id="hideWithControlsTimer" type="text" placeholder="2" />
</div> </div>
<div class="row"> <div class="row row-checkbox">
<label for="showPopupControlBar">Show popup control bar</label> <label for="showPopupControlBar">Show popup control bar</label>
<input id="showPopupControlBar" type="checkbox" /> <input id="showPopupControlBar" type="checkbox" />
</div> </div>
@@ -278,7 +278,7 @@
<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>
<div class="row"> <div class="row row-checkbox">
<label for="enableSubtitleNudge" <label for="enableSubtitleNudge"
>Enable subtitle nudge<br /><em >Enable subtitle nudge<br /><em
>Makes tiny playback changes to help keep subtitles aligned.</em >Makes tiny playback changes to help keep subtitles aligned.</em
@@ -389,20 +389,20 @@
<button type="button" class="remove-site-rule">Remove</button> <button type="button" class="remove-site-rule">Remove</button>
</div> </div>
<div class="site-rule-body"> <div class="site-rule-body">
<div class="site-rule-option"> <div class="site-rule-option site-rule-option-checkbox">
<label> <label class="site-rule-split-label">
<span>Enable Speeder on this site</span>
<input type="checkbox" class="site-enabled" /> <input type="checkbox" class="site-enabled" />
Enable Speeder on this site
</label> </label>
</div> </div>
<div class="site-rule-content"> <div class="site-rule-content">
<div class="site-rule-override-section"> <div class="site-rule-override-section">
<label class="site-override-lead"> <label class="site-override-lead">
<span>Override placement for this site</span>
<input type="checkbox" class="override-placement" /> <input type="checkbox" class="override-placement" />
Override placement for this site
</label> </label>
<div class="site-placement-container" style="display: none"> <div class="site-placement-container" style="display: none">
<div class="site-rule-option"> <div class="site-rule-option site-rule-option-field">
<label>Default controller location:</label> <label>Default controller location:</label>
<select class="site-controllerLocation"> <select class="site-controllerLocation">
<option value="top-left">Top left</option> <option value="top-left">Top left</option>
@@ -436,11 +436,11 @@
</div> </div>
<div class="site-rule-override-section"> <div class="site-rule-override-section">
<label class="site-override-lead"> <label class="site-override-lead">
<span>Override hide-by-default for this site</span>
<input type="checkbox" class="override-visibility" /> <input type="checkbox" class="override-visibility" />
Override hide-by-default for this site
</label> </label>
<div class="site-visibility-container" style="display: none"> <div class="site-visibility-container" style="display: none">
<div class="site-rule-option"> <div class="site-rule-option site-rule-option-checkbox">
<label>Hide controller by default:</label> <label>Hide controller by default:</label>
<input type="checkbox" class="site-startHidden" /> <input type="checkbox" class="site-startHidden" />
</div> </div>
@@ -448,17 +448,17 @@
</div> </div>
<div class="site-rule-override-section"> <div class="site-rule-override-section">
<label class="site-override-lead"> <label class="site-override-lead">
<span>Override auto-hide for this site</span>
<input type="checkbox" class="override-autohide" /> <input type="checkbox" class="override-autohide" />
Override auto-hide for this site
</label> </label>
<div class="site-autohide-container" style="display: none"> <div class="site-autohide-container" style="display: none">
<div class="site-rule-option"> <div class="site-rule-option site-rule-option-checkbox">
<label> <label class="site-rule-split-label">
<span>Hide with controls (idle-based)</span>
<input type="checkbox" class="site-hideWithControls" /> <input type="checkbox" class="site-hideWithControls" />
Hide with controls (idle-based)
</label> </label>
</div> </div>
<div class="site-rule-option"> <div class="site-rule-option site-rule-option-field">
<label>Auto-hide timer (0.1&ndash;15s):</label> <label>Auto-hide timer (0.1&ndash;15s):</label>
<input type="text" class="site-hideWithControlsTimer" /> <input type="text" class="site-hideWithControlsTimer" />
</div> </div>
@@ -466,19 +466,19 @@
</div> </div>
<div class="site-rule-override-section"> <div class="site-rule-override-section">
<label class="site-override-lead"> <label class="site-override-lead">
<span>Override playback for this site</span>
<input type="checkbox" class="override-playback" /> <input type="checkbox" class="override-playback" />
Override playback for this site
</label> </label>
<div class="site-playback-container" style="display: none"> <div class="site-playback-container" style="display: none">
<div class="site-rule-option"> <div class="site-rule-option site-rule-option-checkbox">
<label>Remember playback speed:</label> <label>Remember playback speed:</label>
<input type="checkbox" class="site-rememberSpeed" /> <input type="checkbox" class="site-rememberSpeed" />
</div> </div>
<div class="site-rule-option"> <div class="site-rule-option site-rule-option-checkbox">
<label>Force last saved speed:</label> <label>Force last saved speed:</label>
<input type="checkbox" class="site-forceLastSavedSpeed" /> <input type="checkbox" class="site-forceLastSavedSpeed" />
</div> </div>
<div class="site-rule-option"> <div class="site-rule-option site-rule-option-checkbox">
<label>Work on audio:</label> <label>Work on audio:</label>
<input type="checkbox" class="site-audioBoolean" /> <input type="checkbox" class="site-audioBoolean" />
</div> </div>
@@ -486,11 +486,11 @@
</div> </div>
<div class="site-rule-override-section"> <div class="site-rule-override-section">
<label class="site-override-lead"> <label class="site-override-lead">
<span>Override opacity for this site</span>
<input type="checkbox" class="override-opacity" /> <input type="checkbox" class="override-opacity" />
Override opacity for this site
</label> </label>
<div class="site-opacity-container" style="display: none"> <div class="site-opacity-container" style="display: none">
<div class="site-rule-option"> <div class="site-rule-option site-rule-option-field">
<label>Controller opacity:</label> <label>Controller opacity:</label>
<input type="text" class="site-controllerOpacity" /> <input type="text" class="site-controllerOpacity" />
</div> </div>
@@ -498,15 +498,15 @@
</div> </div>
<div class="site-rule-override-section"> <div class="site-rule-override-section">
<label class="site-override-lead"> <label class="site-override-lead">
<span>Override subtitle nudge for this site</span>
<input type="checkbox" class="override-subtitleNudge" /> <input type="checkbox" class="override-subtitleNudge" />
Override subtitle nudge for this site
</label> </label>
<div class="site-subtitleNudge-container" style="display: none"> <div class="site-subtitleNudge-container" style="display: none">
<div class="site-rule-option"> <div class="site-rule-option site-rule-option-checkbox">
<label>Enable subtitle nudge:</label> <label>Enable subtitle nudge:</label>
<input type="checkbox" class="site-enableSubtitleNudge" /> <input type="checkbox" class="site-enableSubtitleNudge" />
</div> </div>
<div class="site-rule-option"> <div class="site-rule-option site-rule-option-field">
<label>Nudge interval (10&ndash;1000ms):</label> <label>Nudge interval (10&ndash;1000ms):</label>
<input type="text" class="site-subtitleNudgeInterval" placeholder="50" /> <input type="text" class="site-subtitleNudgeInterval" placeholder="50" />
</div> </div>
@@ -514,8 +514,8 @@
</div> </div>
<div class="site-rule-controlbar"> <div class="site-rule-controlbar">
<label class="site-override-lead"> <label class="site-override-lead">
<span>Override in-player control bar for this site</span>
<input type="checkbox" class="override-controlbar" /> <input type="checkbox" class="override-controlbar" />
Override in-player control bar for this site
</label> </label>
<div class="site-controlbar-container" style="display: none"> <div class="site-controlbar-container" style="display: none">
<div class="cb-editor"> <div class="cb-editor">
@@ -532,11 +532,11 @@
</div> </div>
<div class="site-rule-controlbar"> <div class="site-rule-controlbar">
<label class="site-override-lead"> <label class="site-override-lead">
<span>Override extension popup for this site</span>
<input type="checkbox" class="override-popup-controlbar" /> <input type="checkbox" class="override-popup-controlbar" />
Override extension popup for this site
</label> </label>
<div class="site-popup-controlbar-container" style="display: none"> <div class="site-popup-controlbar-container" style="display: none">
<div class="site-rule-option"> <div class="site-rule-option site-rule-option-checkbox">
<label>Show popup control bar</label> <label>Show popup control bar</label>
<input type="checkbox" class="site-showPopupControlBar" /> <input type="checkbox" class="site-showPopupControlBar" />
</div> </div>
@@ -554,8 +554,8 @@
</div> </div>
<div class="site-rule-shortcuts"> <div class="site-rule-shortcuts">
<label class="site-override-lead"> <label class="site-override-lead">
<span>Override shortcuts for this site</span>
<input type="checkbox" class="override-shortcuts" /> <input type="checkbox" class="override-shortcuts" />
Override shortcuts for this site
</label> </label>
<div class="site-shortcuts-container" style="display: none"></div> <div class="site-shortcuts-container" style="display: none"></div>
</div> </div>
+19 -4
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: "\u21BA", name: "Reset speed" }, reset: { icon: "1.00x", 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" },
@@ -227,6 +227,19 @@ const actionLabels = {
toggleSubtitleNudge: "Toggle subtitle nudge" 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 = [ const customActionsNoValues = [
"reset", "reset",
"display", "display",
@@ -526,7 +539,7 @@ function add_shortcut(action, value) {
valueInput.value = "N/A"; valueInput.value = "N/A";
valueInput.disabled = true; valueInput.disabled = true;
} else { } else {
valueInput.value = value || 0; valueInput.value = formatSpeedBindingDisplay(action, value || 0);
} }
var removeButton = document.createElement("button"); var removeButton = document.createElement("button");
@@ -899,9 +912,11 @@ function addSiteRuleShortcut(container, action, binding, value, force) {
valueInput.className = "customValue"; valueInput.className = "customValue";
valueInput.type = "text"; valueInput.type = "text";
valueInput.placeholder = "value (0.10)"; valueInput.placeholder = "value (0.10)";
valueInput.value = value || 0;
if (customActionsNoValues.includes(action)) { if (customActionsNoValues.includes(action)) {
valueInput.value = "N/A";
valueInput.disabled = true; valueInput.disabled = true;
} else {
valueInput.value = formatSpeedBindingDisplay(action, value || 0);
} }
var forceLabel = document.createElement("label"); var forceLabel = document.createElement("label");
@@ -1329,7 +1344,7 @@ function restore_options() {
valueInput.disabled = true; valueInput.disabled = true;
} }
} else if (valueInput) { } else if (valueInput) {
valueInput.value = item.value; valueInput.value = formatSpeedBindingDisplay(item.action, item.value);
} }
}); });
+73 -7
View File
@@ -7,7 +7,7 @@ document.addEventListener("DOMContentLoaded", function () {
faster: { label: "+", className: "" }, faster: { label: "+", className: "" },
advance: { label: "\u00BB", className: "rw" }, advance: { label: "\u00BB", className: "rw" },
display: { label: "\u00D7", className: "hideButton" }, display: { label: "\u00D7", className: "hideButton" },
reset: { label: "\u21BA", className: "" }, reset: { label: "1.00x", className: "" },
fast: { label: "\u2605", className: "" }, fast: { label: "\u2605", className: "" },
settings: { label: "\u2699", className: "" }, settings: { label: "\u2699", className: "" },
pause: { label: "\u23EF", className: "" }, pause: { label: "\u23EF", className: "" },
@@ -171,11 +171,76 @@ document.addEventListener("DOMContentLoaded", function () {
if (el) el.textContent = (speed != null ? Number(speed) : 1).toFixed(2); if (el) el.textContent = (speed != null ? Number(speed) : 1).toFixed(2);
} }
function querySpeed() { function updatePopupResetLabel(resetLabel) {
sendToActiveTab({ action: "get_speed" }, function (response) { 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) { if (response && response.speed != null) {
updateSpeedDisplay(response.speed); 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( sendToActiveTab(
{ action: "run_action", actionName: btnId }, { action: "run_action", actionName: btnId },
function (response) { function () {
if (response && response.speed != null) { querySpeed();
updateSpeedDisplay(response.speed);
}
} }
); );
}); });
@@ -297,12 +360,14 @@ document.addEventListener("DOMContentLoaded", function () {
if (blacklisted) { if (blacklisted) {
setStatusMessage("Site is blacklisted."); setStatusMessage("Site is blacklisted.");
updateSpeedDisplay(1); updateSpeedDisplay(1);
updatePopupResetLabel("1.00x");
return; return;
} }
if (siteDisabled) { if (siteDisabled) {
setStatusMessage("Speeder is disabled for this site."); setStatusMessage("Speeder is disabled for this site.");
updateSpeedDisplay(1); updateSpeedDisplay(1);
updatePopupResetLabel("1.00x");
return; return;
} }
@@ -311,6 +376,7 @@ document.addEventListener("DOMContentLoaded", function () {
querySpeed(); querySpeed();
} else { } else {
updateSpeedDisplay(1); updateSpeedDisplay(1);
updatePopupResetLabel("1.00x");
} }
}); });
}); });
+103
View File
@@ -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)"
+102
View File
@@ -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)"