Compare commits

..

27 Commits

Author SHA1 Message Date
joshpatra 1a7dc3097e Release v5.2.4 2026-04-10 15:04:29 -04:00
joshpatra c626aca89c Release v5.2.1 2026-04-09 16:17:27 -04:00
joshpatra 05b8456e94 release: v5.1.7 2026-04-02 22:37:10 -04:00
joshpatra 0cbfac4b82 Release v5.1.5 2026-04-02 18:08:33 -04:00
joshpatra b3707c0803 Release v5.1.4 2026-04-02 18:07:09 -04:00
joshpatra fb25c56230 Merge beta 2026-04-01 15:33:11 -04:00
joshpatra 8eb3901121 v5.0.2-beta.1 2026-04-01 15:32:37 -04:00
joshpatra 4efc3e0acc Merge beta 2026-04-01 11:29:16 -04:00
joshpatra 6bf48fa479 v5.0.1-beta.1 2026-04-01 11:28:47 -04:00
joshpatra 7c0a188cd3 Merge beta 2026-04-01 11:19:08 -04:00
joshpatra 3a583ce3b8 v5.0.1-beta.1 2026-04-01 11:19:06 -04:00
joshpatra d7ce1fd000 Merge beta 2026-03-31 15:04:56 -04:00
joshpatra 5c20d7ae0b v5.0.0-beta.1 2026-03-31 15:03:56 -04:00
joshpatra 313832015b Merge beta 2026-03-31 15:01:18 -04:00
joshpatra b0498cd1d7 Merge dev 2026-03-31 15:01:17 -04:00
joshpatra 9f8c235cea Merge dev: update beta install instructions 2026-03-31 14:53:41 -04:00
joshpatra 85215ae54b Merge main: update beta install instructions 2026-03-31 14:53:41 -04:00
joshpatra 597c7a8af5 Merge main: fix beta signing via AMO unlisted 2026-03-31 14:48:47 -04:00
joshpatra d215ad4224 Merge dev: fix beta signing via AMO unlisted 2026-03-31 14:48:46 -04:00
joshpatra df18e638c2 Merge main: fix XPI rename glob 2026-03-31 14:44:46 -04:00
joshpatra 9ff3ce4f74 Merge dev: fix XPI rename glob 2026-03-31 14:44:39 -04:00
joshpatra 91dc47f5a8 Merge main: restructure CI/CD workflow 2026-03-31 14:39:11 -04:00
joshpatra 36a60bbe34 Merge dev: restructure CI/CD workflow 2026-03-31 14:39:04 -04:00
joshpatra 3bcb36f6d0 Merge branch 'dev' into beta 2026-03-31 14:22:24 -04:00
joshpatra 2747c15924 Test beta prerelease 2026-03-31 14:18:49 -04:00
joshpatra 5e7d5d6d21 Merge branch 'dev' into beta 2026-03-31 14:18:32 -04:00
joshpatra 38174a2a76 Test beta prerelease 2026-03-31 14:15:17 -04:00
14 changed files with 135 additions and 626 deletions
+8 -14
View File
@@ -1,8 +1,8 @@
# Available for Firefox # Available for Firefox
[![Add to Firefox](https://img.shields.io/badge/Add%20to-Firefox-orange?logo=firefox&logoColor=white)](https://addons.mozilla.org/firefox/addon/speeder/) [![Add to Firefox](https://img.shields.io/badge/Add%20to-Firefox-orange?logo=firefox&logoColor=white)](https://addons.mozilla.org/firefox/addon/speeder/)
## The science of accelerated playback
# The science of accelerated playback
**TL;DR: faster playback translates to better engagement and retention.** **TL;DR: faster playback translates to better engagement and retention.**
@@ -33,11 +33,9 @@ last point to listen to it a few more times.
![Player](https://cloud.githubusercontent.com/assets/2400185/24076745/5723e6ae-0c41-11e7-820c-1d8e814a2888.png) ![Player](https://cloud.githubusercontent.com/assets/2400185/24076745/5723e6ae-0c41-11e7-820c-1d8e814a2888.png)
## Using the extension #### *Install [Chrome](https://chrome.google.com/webstore/detail/video-speed-controller/nffaoalbilbmmfgbnbgppjihopabppdk) or [Firefox](https://addons.mozilla.org/en-us/firefox/addon/speeder/) Extension*
[![Add to Firefox](https://img.shields.io/badge/Add%20to-Firefox-orange?logo=firefox&logoColor=white)](https://addons.mozilla.org/firefox/addon/speeder/) \*\* Once the extension is installed simply navigate to any page that offers
Once the extension is installed simply navigate to any page that offers
HTML5 video ([example](https://www.youtube.com/watch?v=E9FxNzv1Tr8)), and you'll HTML5 video ([example](https://www.youtube.com/watch?v=E9FxNzv1Tr8)), and you'll
see a speed indicator in top left corner. Hover over the indicator to reveal the see a speed indicator in top left corner. Hover over the indicator to reveal the
controls to accelerate, slowdown, and quickly rewind or advance the video. Or, controls to accelerate, slowdown, and quickly rewind or advance the video. Or,
@@ -67,25 +65,21 @@ listens both for lower and upper case values (i.e. you can use
key. This is not a perfect solution, as some sites may listen to both, but works key. This is not a perfect solution, as some sites may listen to both, but works
most of the time. most of the time.
## FAQ ### FAQ
### The video controls are not showing up? **The video controls are not showing up?** This extension is only compatible
This extension is only compatible
with HTML5 video. If you don't see the controls showing up, chances are you are with HTML5 video. If you don't see the controls showing up, chances are you are
viewing a Flash video. If you want to confirm, try right-clicking on the video viewing a Flash video. If you want to confirm, try right-clicking on the video
and inspect the menu: if it mentions flash, then that's the issue. That said, and inspect the menu: if it mentions flash, then that's the issue. That said,
most sites will fallback to HTML5 if they detect that Flash is not available. most sites will fallback to HTML5 if they detect that Flash is not available.
You can try manually disabling Flash from the browser. You can try manually disabling Flash from the browser.
### What is this fork all about? **What is this fork all about?** This is a fork of
This is a fork of
[CodeBicycle's Video Speed Controller extension for Firefox](https://github.com/codebicycle/videospeed) [CodeBicycle's Video Speed Controller extension for Firefox](https://github.com/codebicycle/videospeed)
which is a fork of [Igrigorik's Video Speed Controller extension for Chromium](https://github.com/igrigorik/videospeed). which is a fork of [Igrigorik's Video Speed Controller extension for Chromium](https://github.com/igrigorik/videospeed).
The goal of this fork is fix bugs in the upstream code as well as add new features. The goal of this fork is fix bugs in the upstream code as well as add new features.
## License ### License
(GPLv3) - Copyright (c) 2025 Josh Patra (GPLv3) - Copyright (c) 2025 Josh Patra
+7 -14
View File
@@ -969,8 +969,8 @@ function ensureController(node, parent) {
} }
// href selects site rules; re-run on every new/usable media so margins/opacity match current URL. // href selects site rules; re-run on every new/usable media so margins/opacity match current URL.
applySiteRuleOverrides(); var siteDisabled = applySiteRuleOverrides();
if (!siteRuleUtils.isSpeederActiveForSite(tc.settings.enabled, tc.activeSiteRule)) { if (!tc.settings.enabled || siteDisabled) {
return null; return null;
} }
refreshAllControllerGeometry(); refreshAllControllerGeometry();
@@ -2016,7 +2016,6 @@ function defineVideoController() {
function applySiteRuleOverrides() { function applySiteRuleOverrides() {
resetSettingsFromSiteRuleBase(); resetSettingsFromSiteRuleBase();
tc.activeSiteRule = null;
if (!Array.isArray(tc.settings.siteRules) || tc.settings.siteRules.length === 0) { if (!Array.isArray(tc.settings.siteRules) || tc.settings.siteRules.length === 0) {
return false; return false;
@@ -2025,9 +2024,7 @@ function applySiteRuleOverrides() {
var currentUrl = location.href; var currentUrl = location.href;
var matchedRule = siteRuleUtils.matchSiteRule(currentUrl, tc.settings.siteRules); var matchedRule = siteRuleUtils.matchSiteRule(currentUrl, tc.settings.siteRules);
if (!matchedRule) { if (!matchedRule) return false;
return false;
}
tc.activeSiteRule = matchedRule; tc.activeSiteRule = matchedRule;
log(`Matched site rule: ${matchedRule.pattern}`, 4); log(`Matched site rule: ${matchedRule.pattern}`, 4);
@@ -2107,10 +2104,8 @@ function refreshAllControllerGeometry() {
/** Re-match site rules for current URL and refresh controller position/opacity on every video. */ /** Re-match site rules for current URL and refresh controller position/opacity on every video. */
function reapplySiteRulesAndControllerGeometry() { function reapplySiteRulesAndControllerGeometry() {
applySiteRuleOverrides(); var siteDisabled = applySiteRuleOverrides();
if (!siteRuleUtils.isSpeederActiveForSite(tc.settings.enabled, tc.activeSiteRule)) { if (!tc.settings.enabled || siteDisabled) return;
return;
}
refreshAllControllerGeometry(); refreshAllControllerGeometry();
} }
@@ -2458,10 +2453,8 @@ function attachNavigationListeners() {
function initializeNow(doc, forceReinit = false) { function initializeNow(doc, forceReinit = false) {
if ((!forceReinit && vscInitializedDocuments.has(doc)) || !doc.body) return; if ((!forceReinit && vscInitializedDocuments.has(doc)) || !doc.body) return;
applySiteRuleOverrides(); var siteDisabled = applySiteRuleOverrides();
if (!siteRuleUtils.isSpeederActiveForSite(tc.settings.enabled, tc.activeSiteRule)) { if (!tc.settings.enabled || siteDisabled) return;
return;
}
if (!doc.body.classList.contains("vsc-initialized")) { if (!doc.body.classList.contains("vsc-initialized")) {
doc.body.classList.add("vsc-initialized"); doc.body.classList.add("vsc-initialized");
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "Speeder", "name": "Speeder",
"short_name": "Speeder", "short_name": "Speeder",
"version": "5.2.7.0", "version": "5.2.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",
+4 -55
View File
@@ -343,40 +343,11 @@ label em {
font-weight: 500; font-weight: 500;
} }
.shortcut-label em {
display: block;
margin-top: 4px;
color: var(--muted);
font-style: normal;
font-weight: 400;
}
.customKey, .customKey,
.customValue { .customValue {
text-align: center; text-align: center;
} }
/* Chevron: native menu indicator is often missing with themed controls */
#addShortcutSelector,
.site-add-shortcut-selector {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-color: var(--panel);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%234b5563' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px 16px;
padding-right: 38px;
cursor: pointer;
}
#addShortcutSelector:disabled,
.site-add-shortcut-selector:disabled {
cursor: not-allowed;
opacity: 0.72;
}
#addShortcutSelector { #addShortcutSelector {
width: min(220px, 100%); width: min(220px, 100%);
margin-top: 12px; margin-top: 12px;
@@ -518,7 +489,7 @@ label em {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
gap: 16px; gap: 16px;
align-items: start; align-items: center;
font-weight: 600; font-weight: 600;
margin-bottom: 8px; margin-bottom: 8px;
cursor: pointer; cursor: pointer;
@@ -531,11 +502,6 @@ label em {
.site-override-lead span { .site-override-lead span {
margin: 0; margin: 0;
font-weight: 600;
}
.site-override-lead span em {
font-weight: 400;
} }
.site-rule-override-section .site-override-fields, .site-rule-override-section .site-override-fields,
@@ -969,10 +935,6 @@ button.lucide-result-tile.lucide-picked {
color: var(--text); color: var(--text);
} }
.site-rule-split-label span em {
font-weight: 400;
}
.site-rule-split-label input[type="checkbox"] { .site-rule-split-label input[type="checkbox"] {
justify-self: end; justify-self: end;
margin-top: 0; margin-top: 0;
@@ -1007,22 +969,16 @@ button.lucide-result-tile.lucide-picked {
} }
.site-shortcuts-container .shortcut-row { .site-shortcuts-container .shortcut-row {
grid-template-columns: minmax(0, 1fr) 110px 110px minmax(0, 1fr) 38px; grid-template-columns: minmax(0, 1fr) 110px 110px minmax(0, 1fr);
padding: 8px 0; padding: 8px 0;
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
} }
.site-shortcuts-rows .shortcut-row:first-child { .site-shortcuts-container .shortcut-row:first-child {
padding-top: 0; padding-top: 0;
border-top: 0; border-top: 0;
} }
.site-add-shortcut-selector {
width: min(220px, 100%);
align-self: flex-start;
margin-top: 0;
}
.force-label { .force-label {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1164,8 +1120,7 @@ button.lucide-result-tile.lucide-picked {
} }
.action-row button, .action-row button,
#addShortcutSelector, #addShortcutSelector {
.site-add-shortcut-selector {
width: 100%; width: 100%;
} }
@@ -1249,12 +1204,6 @@ button.lucide-result-tile.lucide-picked {
background: rgba(255, 255, 255, 0.04); background: rgba(255, 255, 255, 0.04);
} }
#addShortcutSelector,
.site-add-shortcut-selector {
background-color: var(--panel);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}
input[type="text"]:focus, input[type="text"]:focus,
select:focus, select:focus,
textarea:focus { textarea:focus {
+34 -211
View File
@@ -96,11 +96,7 @@
<section id="customs" class="settings-card"> <section id="customs" class="settings-card">
<div class="section-heading"> <div class="section-heading">
<h3>Shortcuts</h3> <h3>Shortcuts</h3>
<p class="section-intro"> <p class="section-intro">Backspace clears a shortcut. Escape disables it.</p>
Backspace clears a key. Escape disables optional shortcuts. If a site
steals a shortcut, use a site rule with Override shortcuts (and
per-key blocking) for that URL.
</p>
</div> </div>
<div class="shortcuts-grid"> <div class="shortcuts-grid">
<div class="shortcut-row" id="display" data-action="display"> <div class="shortcut-row" id="display" data-action="display">
@@ -136,10 +132,7 @@
/> />
</div> </div>
<div class="shortcut-row" id="slower" data-action="slower"> <div class="shortcut-row" id="slower" data-action="slower">
<div class="shortcut-label"> <div class="shortcut-label">Decrease speed</div>
Decrease speed
<em>Required: Speeder needs a key for this action.</em>
</div>
<input <input
class="customKey" class="customKey"
type="text" type="text"
@@ -153,10 +146,7 @@
/> />
</div> </div>
<div class="shortcut-row" id="faster" data-action="faster"> <div class="shortcut-row" id="faster" data-action="faster">
<div class="shortcut-label"> <div class="shortcut-label">Increase speed</div>
Increase speed
<em>Required: Speeder needs a key for this action.</em>
</div>
<input <input
class="customKey" class="customKey"
type="text" type="text"
@@ -263,22 +253,11 @@
<h4 class="defaults-sub-heading">General</h4> <h4 class="defaults-sub-heading">General</h4>
<div class="row row-checkbox"> <div class="row row-checkbox">
<label for="enabled" <label for="enabled">Enable</label>
>Enable<br />
<em
>On: site rules can block sites (blacklist). Off: only matched rules keep Speeder on (whitelist).</em
>
</label>
<input id="enabled" type="checkbox" /> <input id="enabled" type="checkbox" />
</div> </div>
<div class="row row-checkbox"> <div class="row row-checkbox">
<label for="audioBoolean" <label for="audioBoolean">Work on audio</label>
>Work on audio<br />
<em
>Also controls plain HTML5 audio (not just video). Turn off if
you only want Speeder on video players.</em
>
</label>
<input id="audioBoolean" type="checkbox" /> <input id="audioBoolean" type="checkbox" />
</div> </div>
@@ -286,14 +265,7 @@
<h4 class="defaults-sub-heading">Playback</h4> <h4 class="defaults-sub-heading">Playback</h4>
<div class="row row-checkbox"> <div class="row row-checkbox">
<label for="rememberSpeed" <label for="rememberSpeed">Remember playback speed</label>
>Remember playback speed<br />
<em
>Stores speed per source so revisiting the same media can restore
it. Separate from &ldquo;Force last saved speed,&rdquo; which
fights players that reset rate.</em
>
</label>
<input id="rememberSpeed" type="checkbox" /> <input id="rememberSpeed" type="checkbox" />
</div> </div>
<div class="row row-checkbox"> <div class="row row-checkbox">
@@ -311,23 +283,11 @@
<h4 class="defaults-sub-heading">Controller</h4> <h4 class="defaults-sub-heading">Controller</h4>
<div class="row row-checkbox"> <div class="row row-checkbox">
<label for="startHidden" <label for="startHidden">Hide controller by default</label>
>Hide controller by default<br />
<em
>Starts with the overlay hidden; use shortcuts (show/hide,
move) or site behavior to reveal it.</em
>
</label>
<input id="startHidden" type="checkbox" /> <input id="startHidden" type="checkbox" />
</div> </div>
<div class="row"> <div class="row">
<label for="controllerLocation" <label for="controllerLocation">Default controller location</label>
>Default controller location<br />
<em
>Corner or edge anchor for the hover bar. Site rules can override
this for specific URLs.</em
>
</label>
<select id="controllerLocation"> <select id="controllerLocation">
<option value="top-left">Top left</option> <option value="top-left">Top left</option>
<option value="top-center">Top center</option> <option value="top-center">Top center</option>
@@ -340,13 +300,7 @@
</select> </select>
</div> </div>
<div class="row"> <div class="row">
<label for="controllerOpacity" <label for="controllerOpacity">Controller opacity</label>
>Controller opacity<br />
<em
>0&ndash;1 (decimals). Lower is more transparent. Applies to the
in-page controller only.</em
>
</label>
<input id="controllerOpacity" type="text" value="" /> <input id="controllerOpacity" type="text" value="" />
</div> </div>
<div class="row row-controller-margin"> <div class="row row-controller-margin">
@@ -462,23 +416,11 @@
</p> </p>
</div> </div>
<div class="row row-checkbox"> <div class="row row-checkbox">
<label for="showPopupControlBar" <label for="showPopupControlBar">Show popup control bar</label>
>Show popup control bar<br />
<em
>Shows buttons in the extension popup (toolbar icon). Can be
overridden per site in site rules.</em
>
</label>
<input id="showPopupControlBar" type="checkbox" /> <input id="showPopupControlBar" type="checkbox" />
</div> </div>
<div class="row row-checkbox"> <div class="row row-checkbox">
<label for="popupMatchHoverControls" <label for="popupMatchHoverControls">Match hover controls</label>
>Match hover controls<br />
<em
>When on, the popup copies the hover bar&rsquo;s buttons and
order. When off, customize the popup layout below.</em
>
</label>
<input id="popupMatchHoverControls" type="checkbox" /> <input id="popupMatchHoverControls" type="checkbox" />
</div> </div>
<div id="popupCbEditorWrap" class="cb-editor cb-editor-disabled"> <div id="popupCbEditorWrap" class="cb-editor cb-editor-disabled">
@@ -626,36 +568,19 @@
<div class="site-rule-body"> <div class="site-rule-body">
<div class="site-rule-option site-rule-option-checkbox"> <div class="site-rule-option site-rule-option-checkbox">
<label class="site-rule-split-label"> <label class="site-rule-split-label">
<span <span>Enable Speeder on this site</span>
>Enable Speeder on this site<br /><em
>For this URL pattern only: off blocks when global Speeder
is on (blacklist); on allows when global is off
(whitelist)&mdash;same pairing as Defaults &rarr;
Enable.</em
></span
>
<input type="checkbox" class="site-enabled" /> <input type="checkbox" class="site-enabled" />
</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 <span>Override placement for this site</span>
>Override placement for this site<br /><em
>When on, location and margin below replace general
defaults for matching URLs.</em
></span
>
<input type="checkbox" class="override-placement" /> <input type="checkbox" class="override-placement" />
</label> </label>
<div class="site-placement-container"> <div class="site-placement-container">
<div class="site-rule-option site-rule-option-field"> <div class="site-rule-option site-rule-option-field">
<label <label>Default controller location:</label>
>Default controller location:<br /><em
>Corner or edge anchor for the hover bar. Replaces the
general default for matching URLs only.</em
></label
>
<select class="site-controllerLocation"> <select class="site-controllerLocation">
<option value="top-left">Top left</option> <option value="top-left">Top left</option>
<option value="top-center">Top center</option> <option value="top-center">Top center</option>
@@ -670,8 +595,7 @@
<div class="site-rule-option site-rule-margin-option"> <div class="site-rule-option site-rule-margin-option">
<label <label
>Controller margin (px):<br /><em >Controller margin (px):<br /><em
>Shifts the whole control from its preset position (CSS >Shifts the whole control. 0&ndash;200.</em
margins). Top and bottom. 0&ndash;200.</em
></label ></label
> >
<div class="controller-margin-inputs"> <div class="controller-margin-inputs">
@@ -689,163 +613,85 @@
</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 <span>Override hide-by-default for this site</span>
>Override hide-by-default for this site<br /><em
>When on, the hide-by-default toggle below replaces the
general default for matching URLs.</em
></span
>
<input type="checkbox" class="override-visibility" /> <input type="checkbox" class="override-visibility" />
</label> </label>
<div class="site-visibility-container"> <div class="site-visibility-container">
<div class="site-rule-option site-rule-option-checkbox"> <div class="site-rule-option site-rule-option-checkbox">
<label <label>Hide controller by default:</label>
>Hide controller by default:<br /><em
>Starts with the overlay hidden; use shortcuts
(show/hide, move) or site behavior to reveal it.</em
></label
>
<input type="checkbox" class="site-startHidden" /> <input type="checkbox" class="site-startHidden" />
</div> </div>
</div> </div>
</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 <span>Override auto-hide for this site</span>
>Override auto-hide for this site<br /><em
>When on, hide-with-controls and timer below replace
general defaults for matching URLs.</em
></span
>
<input type="checkbox" class="override-autohide" /> <input type="checkbox" class="override-autohide" />
</label> </label>
<div class="site-autohide-container"> <div class="site-autohide-container">
<div class="site-rule-option site-rule-option-checkbox"> <div class="site-rule-option site-rule-option-checkbox">
<label class="site-rule-split-label"> <label class="site-rule-split-label">
<span <span>Hide with controls (idle-based)</span>
>Hide with controls (idle-based)<br /><em
>Fade the controller in and out with the video
interface: perfect sync on YouTube, idle-based
elsewhere.</em
></span
>
<input type="checkbox" class="site-hideWithControls" /> <input type="checkbox" class="site-hideWithControls" />
</label> </label>
</div> </div>
<div class="site-rule-option site-rule-option-field"> <div class="site-rule-option site-rule-option-field">
<label <label>Auto-hide timer (0.1&ndash;15s):</label>
>Auto-hide timer (0.1&ndash;15s):<br /><em
>Seconds of inactivity before hiding: 0.1&ndash;15 for
non-YouTube sites.</em
></label
>
<input type="text" class="site-hideWithControlsTimer" /> <input type="text" class="site-hideWithControlsTimer" />
</div> </div>
</div> </div>
</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 <span>Override playback for this site</span>
>Override playback for this site<br /><em
>When on, remember speed / force / audio below replace
general defaults for matching URLs.</em
></span
>
<input type="checkbox" class="override-playback" /> <input type="checkbox" class="override-playback" />
</label> </label>
<div class="site-playback-container"> <div class="site-playback-container">
<div class="site-rule-option site-rule-option-checkbox"> <div class="site-rule-option site-rule-option-checkbox">
<label <label>Remember playback speed:</label>
>Remember playback speed:<br /><em
>Stores speed per source so revisiting the same media
can restore it. Separate from &ldquo;Force last saved
speed,&rdquo; which fights players that reset
rate.</em
></label
>
<input type="checkbox" class="site-rememberSpeed" /> <input type="checkbox" class="site-rememberSpeed" />
</div> </div>
<div class="site-rule-option site-rule-option-checkbox"> <div class="site-rule-option site-rule-option-checkbox">
<label <label>Force last saved speed:</label>
>Force last saved speed:<br /><em
>Useful when a video player tries to override the speed
you set in Speeder.</em
></label
>
<input type="checkbox" class="site-forceLastSavedSpeed" /> <input type="checkbox" class="site-forceLastSavedSpeed" />
</div> </div>
<div class="site-rule-option site-rule-option-checkbox"> <div class="site-rule-option site-rule-option-checkbox">
<label <label>Work on audio:</label>
>Work on audio:<br /><em
>Also controls plain HTML5 audio (not just video). Turn
off if you only want Speeder on video players.</em
></label
>
<input type="checkbox" class="site-audioBoolean" /> <input type="checkbox" class="site-audioBoolean" />
</div> </div>
</div> </div>
</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 <span>Override opacity for this site</span>
>Override opacity for this site<br /><em
>When on, opacity below replaces the general default for
matching URLs.</em
></span
>
<input type="checkbox" class="override-opacity" /> <input type="checkbox" class="override-opacity" />
</label> </label>
<div class="site-opacity-container"> <div class="site-opacity-container">
<div class="site-rule-option site-rule-option-field"> <div class="site-rule-option site-rule-option-field">
<label <label>Controller opacity:</label>
>Controller opacity:<br /><em
>0&ndash;1 (decimals). Lower is more transparent.
Applies to the in-page controller only.</em
></label
>
<input type="text" class="site-controllerOpacity" /> <input type="text" class="site-controllerOpacity" />
</div> </div>
</div> </div>
</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 <span>Override subtitle nudge for this site</span>
>Override subtitle nudge for this site<br /><em
>When on, nudge options below replace general defaults for
matching URLs.</em
></span
>
<input type="checkbox" class="override-subtitleNudge" /> <input type="checkbox" class="override-subtitleNudge" />
</label> </label>
<div class="site-subtitleNudge-container"> <div class="site-subtitleNudge-container">
<div class="site-rule-option site-rule-option-checkbox"> <div class="site-rule-option site-rule-option-checkbox">
<label <label>Enable subtitle nudge:</label>
>Enable subtitle nudge:<br /><em
>Makes tiny playback changes to help keep subtitles
aligned.</em
></label
>
<input type="checkbox" class="site-enableSubtitleNudge" /> <input type="checkbox" class="site-enableSubtitleNudge" />
</div> </div>
<div class="site-rule-option site-rule-option-field"> <div class="site-rule-option site-rule-option-field">
<label <label>Nudge interval (10&ndash;1000ms):</label>
>Nudge interval (10&ndash;1000ms):<br /><em
>How often to nudge: 10&ndash;1000. Smaller values are
more frequent. Default: 50.</em
></label
>
<input type="text" class="site-subtitleNudgeInterval" placeholder="50" /> <input type="text" class="site-subtitleNudgeInterval" placeholder="50" />
</div> </div>
</div> </div>
</div> </div>
<div class="site-rule-controlbar"> <div class="site-rule-controlbar">
<label class="site-override-lead"> <label class="site-override-lead">
<span <span>Override in-player control bar for this site</span>
>Override in-player control bar for this site<br /><em
>Same idea as Hover control bar: drag blocks between
Active and Available for matching URLs only.</em
></span
>
<input type="checkbox" class="override-controlbar" /> <input type="checkbox" class="override-controlbar" />
</label> </label>
<div class="site-controlbar-container"> <div class="site-controlbar-container">
@@ -863,22 +709,12 @@
</div> </div>
<div class="site-rule-controlbar"> <div class="site-rule-controlbar">
<label class="site-override-lead"> <label class="site-override-lead">
<span <span>Override extension popup for this site</span>
>Override extension popup for this site<br /><em
>Popup layout for matching URLs; mirrors the global Popup
control bar when you customize it here.</em
></span
>
<input type="checkbox" class="override-popup-controlbar" /> <input type="checkbox" class="override-popup-controlbar" />
</label> </label>
<div class="site-popup-controlbar-container"> <div class="site-popup-controlbar-container">
<div class="site-rule-option site-rule-option-checkbox"> <div class="site-rule-option site-rule-option-checkbox">
<label <label>Show popup control bar</label>
>Show popup control bar<br /><em
>Shows buttons in the extension popup (toolbar icon).
Replaces the general default for this pattern.</em
></label
>
<input type="checkbox" class="site-showPopupControlBar" /> <input type="checkbox" class="site-showPopupControlBar" />
</div> </div>
<div class="cb-editor"> <div class="cb-editor">
@@ -895,23 +731,10 @@
</div> </div>
<div class="site-rule-shortcuts"> <div class="site-rule-shortcuts">
<label class="site-override-lead"> <label class="site-override-lead">
<span <span>Override shortcuts for this site</span>
>Override shortcuts for this site<br /><em
>Add shortcuts from the menu; none by default. Leave off
to use global Shortcuts.</em
></span
>
<input type="checkbox" class="override-shortcuts" /> <input type="checkbox" class="override-shortcuts" />
</label> </label>
<div class="site-shortcuts-container"> <div class="site-shortcuts-container"></div>
<div class="site-shortcuts-rows"></div>
<select
class="site-add-shortcut-selector"
aria-label="Add shortcut for this site"
>
<option value="">Add shortcut&hellip;</option>
</select>
</div>
</div> </div>
</div> </div>
</div> </div>
+59 -108
View File
@@ -233,7 +233,7 @@ const actionLabels = {
}; };
const speedBindingActions = ["slower", "faster", "fast", "softer", "louder"]; const speedBindingActions = ["slower", "faster", "fast", "softer", "louder"];
const requiredShortcutActions = new Set(["slower", "faster"]); const requiredShortcutActions = new Set(["display", "slower", "faster"]);
function formatSpeedBindingDisplay(action, value) { function formatSpeedBindingDisplay(action, value) {
if (!speedBindingActions.includes(action)) { if (!speedBindingActions.includes(action)) {
@@ -319,70 +319,6 @@ function refreshAddShortcutSelector() {
} }
} }
function refreshSiteRuleAddShortcutSelector(ruleEl) {
if (!ruleEl) return;
var selector = ruleEl.querySelector(".site-add-shortcut-selector");
if (!selector) return;
while (selector.options.length > 1) {
selector.remove(1);
}
var usedActions = new Set();
ruleEl.querySelectorAll(".site-shortcuts-rows .shortcut-row.customs").forEach(function (row) {
var action = row.dataset.action;
if (action) usedActions.add(action);
});
Object.keys(actionLabels).forEach(function (action) {
if (!usedActions.has(action)) {
var option = document.createElement("option");
option.value = action;
option.textContent = actionLabels[action];
selector.appendChild(option);
}
});
var overrideShortcutsOn =
ruleEl.querySelector(".override-shortcuts") &&
ruleEl.querySelector(".override-shortcuts").checked;
if (selector.options.length === 1) {
selector.disabled = true;
selector.options[0].text = "All shortcuts added";
} else {
selector.disabled = !overrideShortcutsOn;
selector.options[0].text = "Add shortcut\u2026";
}
}
function getGlobalBindingSnapshotForSiteShortcut(action) {
var row = document.querySelector(
'#customs .shortcut-row[data-action="' + action + '"]'
);
if (row) {
var keyInput = row.querySelector(".customKey");
var binding = normalizeStoredBinding(keyInput && keyInput.vscBinding);
if (binding) {
var valueInput = row.querySelector(".customValue");
var value = customActionsNoValues.includes(action)
? 0
: Number(valueInput && valueInput.value);
return { binding: binding, value: value };
}
}
var def = tcDefaults.keyBindings.find(function (b) {
return b.action === action;
});
if (def) {
return {
binding: normalizeStoredBinding(def),
value: def.value
};
}
return { binding: null, value: undefined };
}
function ensureDefaultBinding(storage, action, code, value) { function ensureDefaultBinding(storage, action, code, value) {
if (storage.keyBindings.some((item) => item.action === action)) return; if (storage.keyBindings.some((item) => item.action === action)) return;
@@ -1006,18 +942,35 @@ function ensureAllDefaultBindings(storage) {
}); });
} }
function addSiteRuleShortcut(rowsEl, action, binding, value, force) { function addSiteRuleShortcut(container, action, binding, value, force) {
if (!rowsEl) return;
var div = document.createElement("div"); var div = document.createElement("div");
div.setAttribute("class", "shortcut-row customs"); div.setAttribute("class", "shortcut-row customs");
div.dataset.action = action; div.dataset.action = action;
var actionLabel = document.createElement("div"); var actionLabel = document.createElement("div");
actionLabel.className = "shortcut-label"; actionLabel.className = "shortcut-label";
var actionLabels = {
display: "Show/hide controller",
move: "Move controller",
slower: "Decrease speed",
faster: "Increase speed",
rewind: "Rewind",
advance: "Advance",
reset: "Reset speed",
fast: "Preferred speed",
toggleSubtitleNudge: "Toggle subtitle nudge",
pause: "Play / Pause",
muted: "Mute / Unmute",
louder: "Increase volume",
softer: "Decrease volume",
mark: "Set marker",
jump: "Jump to marker"
};
var actionLabelText = actionLabels[action] || action; var actionLabelText = actionLabels[action] || action;
if (action === "toggleSubtitleNudge") { if (action === "toggleSubtitleNudge") {
var ruleEl = rowsEl.closest(".site-rule"); // Check if the site rule is for YouTube.
// We look up the pattern from the site rule element this container belongs to.
var ruleEl = container.closest(".site-rule");
var pattern = ruleEl ? ruleEl.querySelector(".site-pattern").value : ""; var pattern = ruleEl ? ruleEl.querySelector(".site-pattern").value : "";
if (!pattern.toLowerCase().includes("youtube.com")) { if (!pattern.toLowerCase().includes("youtube.com")) {
actionLabelText += " (only for YouTube embeds)"; actionLabelText += " (only for YouTube embeds)";
@@ -1061,18 +1014,12 @@ function addSiteRuleShortcut(rowsEl, action, binding, value, force) {
forceLabel.appendChild(forceCheckbox); forceLabel.appendChild(forceCheckbox);
forceLabel.appendChild(forceText); forceLabel.appendChild(forceText);
var removeButton = document.createElement("button");
removeButton.className = "removeParent";
removeButton.type = "button";
removeButton.textContent = "\u00d7";
div.appendChild(actionLabel); div.appendChild(actionLabel);
div.appendChild(keyInput); div.appendChild(keyInput);
div.appendChild(valueInput); div.appendChild(valueInput);
div.appendChild(forceLabel); div.appendChild(forceLabel);
div.appendChild(removeButton);
rowsEl.appendChild(div); container.appendChild(div);
} }
function createSiteRule(rule) { function createSiteRule(rule) {
@@ -1210,24 +1157,56 @@ function createSiteRule(rule) {
rule && Array.isArray(rule.shortcuts) && rule.shortcuts.length > 0 rule && Array.isArray(rule.shortcuts) && rule.shortcuts.length > 0
); );
ruleEl.querySelector(".override-shortcuts").checked = hasShortcutOverride; ruleEl.querySelector(".override-shortcuts").checked = hasShortcutOverride;
var rowsEl = ruleEl.querySelector(".site-shortcuts-rows"); var container = ruleEl.querySelector(".site-shortcuts-container");
if (hasShortcutOverride) { if (hasShortcutOverride) {
rule.shortcuts.forEach((shortcut) => { rule.shortcuts.forEach((shortcut) => {
addSiteRuleShortcut( addSiteRuleShortcut(
rowsEl, container,
shortcut.action, shortcut.action,
shortcut, shortcut,
shortcut.value, shortcut.value,
shortcut.force shortcut.force
); );
}); });
} else {
populateDefaultSiteShortcuts(container);
} }
applySiteRuleOverrideState(ruleEl, "override-shortcuts", "site-shortcuts-container"); applySiteRuleOverrideState(ruleEl, "override-shortcuts", "site-shortcuts-container");
refreshSiteRuleAddShortcutSelector(ruleEl);
document.getElementById("siteRulesContainer").appendChild(ruleEl); document.getElementById("siteRulesContainer").appendChild(ruleEl);
} }
function populateDefaultSiteShortcuts(container) {
var bindings = [];
document.querySelectorAll("#customs .shortcut-row").forEach((row) => {
var action = row.dataset.action;
if (!action) return;
var keyInput = row.querySelector(".customKey");
var binding = normalizeStoredBinding(keyInput && keyInput.vscBinding);
if (!binding) return;
var valueInput = row.querySelector(".customValue");
bindings.push({
action: action,
code: binding.code,
disabled: binding.disabled === true,
value: customActionsNoValues.includes(action)
? 0
: Number(valueInput && valueInput.value),
force: false
});
});
if (bindings.length === 0) {
bindings = tcDefaults.keyBindings.slice();
}
bindings.forEach((binding) => {
addSiteRuleShortcut(container, binding.action, binding, binding.value, false);
});
}
function createControlBarBlock(buttonId) { function createControlBarBlock(buttonId) {
var def = controllerButtonDefs[buttonId]; var def = controllerButtonDefs[buttonId];
if (!def) return null; if (!def) return null;
@@ -1801,13 +1780,8 @@ document.addEventListener("DOMContentLoaded", function () {
var removeParentButton = targetEl.closest(".removeParent"); var removeParentButton = targetEl.closest(".removeParent");
if (removeParentButton) { if (removeParentButton) {
var removedRow = removeParentButton.parentNode; removeParentButton.parentNode.remove();
var siteRuleForShortcut = removedRow.closest(".site-rule");
removedRow.remove();
refreshAddShortcutSelector(); refreshAddShortcutSelector();
if (siteRuleForShortcut) {
refreshSiteRuleAddShortcutSelector(siteRuleForShortcut);
}
return; return;
} }
var removeSiteRuleButton = targetEl.closest(".remove-site-rule"); var removeSiteRuleButton = targetEl.closest(".remove-site-rule");
@@ -1834,26 +1808,6 @@ document.addEventListener("DOMContentLoaded", function () {
} }
} }
if (event.target.classList.contains("site-add-shortcut-selector")) {
var action = event.target.value;
if (!action) return;
var siteRuleRoot = event.target.closest(".site-rule");
var rows = siteRuleRoot && siteRuleRoot.querySelector(".site-shortcuts-rows");
if (rows) {
var snap = getGlobalBindingSnapshotForSiteShortcut(action);
addSiteRuleShortcut(
rows,
action,
snap.binding,
snap.value,
false
);
refreshSiteRuleAddShortcutSelector(siteRuleRoot);
}
event.target.value = "";
return;
}
// Site rule: show/hide optional override sections // Site rule: show/hide optional override sections
var siteOverrideContainers = { var siteOverrideContainers = {
"override-placement": "site-placement-container", "override-placement": "site-placement-container",
@@ -1875,9 +1829,6 @@ document.addEventListener("DOMContentLoaded", function () {
if (targetBox) { if (targetBox) {
setSiteOverrideContainerState(targetBox, event.target.checked); setSiteOverrideContainerState(targetBox, event.target.checked);
} }
if (ocb === "override-shortcuts") {
refreshSiteRuleAddShortcutSelector(siteRuleRoot);
}
return; return;
} }
} }
+9 -60
View File
@@ -210,63 +210,21 @@ button:focus-visible {
.donate-split { .donate-split {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: 1fr 1fr;
} }
.donate-icon-btn { .donate-split button {
display: flex;
align-items: center;
justify-content: center;
min-height: 32px;
padding: 6px 4px;
background: var(--panel);
border: 1px solid var(--border-strong);
color: var(--text);
text-decoration: none;
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease;
}
.donate-icon-btn:hover {
background: #f8f9fb;
border-color: #c5ccd5;
}
.donate-icon-btn:active {
background: #f1f3f5;
}
.donate-icon-btn:focus-visible {
outline: 2px solid rgba(17, 24, 39, 0.14);
outline-offset: 2px;
position: relative;
z-index: 1;
}
.donate-icon-btn svg {
display: block;
flex-shrink: 0;
}
.donate-icon-btn--kofi img {
display: block;
height: 22px;
width: auto; width: auto;
max-width: 40px;
object-fit: contain;
}
.donate-icon-btn:first-child {
border-radius: 8px 0 0 8px;
border-right-width: 0;
}
.donate-icon-btn:nth-child(2) {
border-radius: 0; border-radius: 0;
border-right-width: 0; min-height: 28px;
} }
.donate-icon-btn:last-child { .donate-split button:first-child {
border-radius: 8px 0 0 8px;
border-right: 0;
}
.donate-split button:last-child {
border-radius: 0 8px 8px 0; border-radius: 0 8px 8px 0;
} }
@@ -308,13 +266,4 @@ button:focus-visible {
background: #dfe3e8; background: #dfe3e8;
border-color: #dfe3e8; border-color: #dfe3e8;
} }
.donate-icon-btn:hover {
background: #1f2226;
border-color: #4a515a;
}
.donate-icon-btn:active {
background: #252a2f;
}
} }
+2 -61
View File
@@ -35,67 +35,8 @@
<div id="donateWrap" class="donate-wrap"> <div id="donateWrap" class="donate-wrap">
<button id="donate" class="secondary">Donate</button> <button id="donate" class="secondary">Donate</button>
<div id="donateOptions" class="donate-split hide"> <div id="donateOptions" class="donate-split hide">
<a <button id="donateKofi" class="secondary">Ko-fi</button>
id="donateGithub" <button id="donateGithub" class="secondary">Sponsors</button>
class="donate-icon-btn donate-icon-btn--github"
href="https://github.com/sponsors/SoPat712"
target="_blank"
rel="noopener noreferrer"
aria-label="Sponsor on GitHub (opens in new tab)"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="22"
height="22"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
/>
</svg>
</a>
<a
id="donateKofi"
class="donate-icon-btn donate-icon-btn--kofi"
href="https://ko-fi.com/joshpatra"
target="_blank"
rel="noopener noreferrer"
aria-label="Support on Ko-fi (opens in new tab)"
>
<img
src="images/kofi_symbol.svg"
width="28"
height="22"
alt=""
decoding="async"
/>
</a>
<a
id="donateBmc"
class="donate-icon-btn donate-icon-btn--bmc"
href="https://buymeacoffee.com/treeman183"
target="_blank"
rel="noopener noreferrer"
aria-label="Support on Buy Me a Coffee (opens in new tab)"
>
<svg
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="22"
height="22"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M20.216 6.415l-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 00-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 00-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 01-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 013.426-.12c.674.019 1.347.067 2.017.144l.228.031c.267.04.533.088.798.145.392.085.895.113 1.07.542.055.137.08.288.111.431l.319 1.484a.237.237 0 01-.199.284h-.003c-.037.006-.075.01-.112.015a36.704 36.704 0 01-4.743.295 37.059 37.059 0 01-4.699-.304c-.14-.017-.293-.042-.417-.06-.326-.048-.649-.108-.973-.161-.393-.065-.768-.032-1.123.161-.29.16-.527.404-.675.701-.154.316-.199.66-.267 1-.069.34-.176.707-.135 1.056.087.753.613 1.365 1.37 1.502a39.69 39.69 0 0011.343.376.483.483 0 01.535.53l-.071.697-1.018 9.907c-.041.41-.047.832-.125 1.237-.122.637-.553 1.028-1.182 1.171-.577.131-1.165.2-1.756.205-.656.004-1.31-.025-1.966-.022-.699.004-1.556-.06-2.095-.58-.475-.458-.54-1.174-.605-1.793l-.731-7.013-.322-3.094c-.037-.351-.286-.695-.678-.678-.336.015-.718.3-.678.679l.228 2.185.949 9.112c.147 1.344 1.174 2.068 2.446 2.272.742.12 1.503.144 2.257.156.966.016 1.942.053 2.892-.122 1.408-.258 2.465-1.198 2.616-2.657.34-3.332.683-6.663 1.024-9.995l.215-2.087a.484.484 0 01.39-.426c.402-.078.787-.212 1.074-.518.455-.488.546-1.124.385-1.766zm-1.478.772c-.145.137-.363.201-.578.233-2.416.359-4.866.54-7.308.46-1.748-.06-3.477-.254-5.207-.498-.17-.024-.353-.055-.47-.18-.22-.236-.111-.71-.054-.995.052-.26.152-.609.463-.646.484-.057 1.046.148 1.526.22.577.088 1.156.159 1.737.212 2.48.226 5.002.19 7.472-.14.45-.06.899-.13 1.345-.21.399-.072.84-.206 1.08.206.166.281.188.657.162.974a.544.544 0 01-.169.364zm-6.159 3.9c-.862.37-1.84.788-3.109.788a5.884 5.884 0 01-1.569-.217l.877 9.004c.065.78.717 1.38 1.5 1.38 0 0 1.243.065 1.658.065.447 0 1.786-.065 1.786-.065.783 0 1.434-.6 1.499-1.38l.94-9.95a3.996 3.996 0 00-1.322-.238c-.826 0-1.491.284-2.26.613z"
/>
</svg>
</a>
</div> </div>
</div> </div>
</div> </div>
+9 -4
View File
@@ -230,6 +230,14 @@ document.addEventListener("DOMContentLoaded", function () {
document.querySelector("#donateOptions").classList.remove("hide"); document.querySelector("#donateOptions").classList.remove("hide");
}); });
document.querySelector("#donateKofi").addEventListener("click", function () {
window.open("https://ko-fi.com/joshpatra");
});
document.querySelector("#donateGithub").addEventListener("click", function () {
window.open("https://github.com/sponsors/SoPat712");
});
document.querySelector("#enable").addEventListener("click", function () { document.querySelector("#enable").addEventListener("click", function () {
toggleEnabled(true, settingsSavedReloadMessage); toggleEnabled(true, settingsSavedReloadMessage);
}); });
@@ -271,10 +279,7 @@ document.addEventListener("DOMContentLoaded", function () {
var url = context && context.url ? context.url : ""; var url = context && context.url ? context.url : "";
var siteRule = matchSiteRule(url, storage.siteRules); var siteRule = matchSiteRule(url, storage.siteRules);
var siteDisabled = isSiteRuleDisabled(siteRule); var siteDisabled = isSiteRuleDisabled(siteRule);
var siteAvailable = siteRuleUtils.isSpeederActiveForSite( var siteAvailable = storage.enabled !== false && !siteDisabled;
storage.enabled,
siteRule
);
var showBar = storage.showPopupControlBar !== false; var showBar = storage.showPopupControlBar !== false;
if (siteRule && siteRule.showPopupControlBar !== undefined) { if (siteRule && siteRule.showPopupControlBar !== undefined) {
-18
View File
@@ -60,28 +60,10 @@
); );
} }
/**
* Whether Speeder should run on this URL given global enabled and the matched rule (if any).
* - No rule: follows global (enabled unless explicitly false).
* - Rule with site "off" / disableExtension: always inactive (blacklist).
* - Rule with site "on": active even when global is off (whitelist).
*/
function isSpeederActiveForSite(globalEnabled, siteRule) {
var globalOn = globalEnabled !== false;
if (!siteRule) {
return globalOn;
}
if (isSiteRuleDisabled(siteRule)) {
return false;
}
return true;
}
return { return {
compileSiteRulePattern: compileSiteRulePattern, compileSiteRulePattern: compileSiteRulePattern,
escapeStringRegExp: escapeStringRegExp, escapeStringRegExp: escapeStringRegExp,
isSiteRuleDisabled: isSiteRuleDisabled, isSiteRuleDisabled: isSiteRuleDisabled,
isSpeederActiveForSite: isSpeederActiveForSite,
matchSiteRule: matchSiteRule matchSiteRule: matchSiteRule
}; };
}); });
-22
View File
@@ -51,28 +51,6 @@ async function bootInject({ sync = {}, local = {} } = {}) {
} }
describe("inject runtime", () => { describe("inject runtime", () => {
it("treats a matching site rule with site enabled as active when global enable is off", async () => {
await bootInject({
sync: {
enabled: false,
siteRules: [{ pattern: "example.org", enabled: true }]
}
});
expect(window.tc.settings.enabled).toBe(false);
window.captureSiteRuleBase();
window.applySiteRuleOverrides();
expect(window.tc.activeSiteRule).toEqual(
expect.objectContaining({ pattern: "example.org", enabled: true })
);
expect(
window.SpeederShared.siteRules.isSpeederActiveForSite(
window.tc.settings.enabled,
window.tc.activeSiteRule
)
).toBe(true);
});
it("keeps subtitle nudge disabled when the effective setting is off", async () => { it("keeps subtitle nudge disabled when the effective setting is off", async () => {
await bootInject({ await bootInject({
sync: { sync: {
-26
View File
@@ -96,32 +96,6 @@ describe("options page", () => {
expect(toggle.getAttribute("aria-label")).toBe("Collapse site rule"); expect(toggle.getAttribute("aria-label")).toBe("Collapse site rule");
}); });
it("site rule shortcut override shows no rows by default and adds via selector", async () => {
await setupOptions({ sync: { siteRules: [] } });
globalThis.createSiteRule({ pattern: "example.com" });
const rule = document.getElementById("siteRulesContainer").lastElementChild;
const rows = rule.querySelector(".site-shortcuts-rows");
const selector = rule.querySelector(".site-add-shortcut-selector");
expect(rows.querySelectorAll(".shortcut-row").length).toBe(0);
expect(selector).not.toBeNull();
expect(selector.disabled).toBe(true);
rule.querySelector(".override-shortcuts").checked = true;
rule.querySelector(".override-shortcuts").dispatchEvent(
new Event("change", { bubbles: true })
);
expect(selector.disabled).toBe(false);
expect(selector.options.length).toBeGreaterThan(1);
selector.value = "pause";
selector.dispatchEvent(new Event("change", { bubbles: true }));
expect(rows.querySelectorAll('.shortcut-row[data-action="pause"]').length).toBe(1);
});
it("keeps site override settings visible but disabled until enabled", async () => { it("keeps site override settings visible but disabled until enabled", async () => {
await setupOptions({ sync: { siteRules: [] } }); await setupOptions({ sync: { siteRules: [] } });
-16
View File
@@ -36,22 +36,6 @@ describe("popup UI", () => {
).toBeGreaterThan(0); ).toBeGreaterThan(0);
}); });
it("shows controls when globally disabled but a whitelist site rule matches", async () => {
await setupPopup({
sync: {
enabled: false,
siteRules: [{ pattern: "example.com", enabled: true }]
}
});
expect(document.getElementById("status").classList.contains("hide")).toBe(
true
);
expect(document.getElementById("popupControlBar").style.display).not.toBe(
"none"
);
});
it("shows disabled state for a matching site rule", async () => { it("shows disabled state for a matching site rule", async () => {
await setupPopup({ await setupPopup({
sync: { sync: {
-14
View File
@@ -24,20 +24,6 @@ describe("shared helpers", () => {
expect(siteRules.isSiteRuleDisabled({ enabled: false })).toBe(true); expect(siteRules.isSiteRuleDisabled({ enabled: false })).toBe(true);
}); });
it("combines global enabled with matched site rules (whitelist / blacklist)", () => {
const allowSite = { pattern: "good.test", enabled: true };
const blockSite = { pattern: "bad.test", enabled: false };
expect(siteRules.isSpeederActiveForSite(true, null)).toBe(true);
expect(siteRules.isSpeederActiveForSite(false, null)).toBe(false);
expect(siteRules.isSpeederActiveForSite(true, blockSite)).toBe(false);
expect(siteRules.isSpeederActiveForSite(false, blockSite)).toBe(false);
expect(siteRules.isSpeederActiveForSite(true, allowSite)).toBe(true);
expect(siteRules.isSpeederActiveForSite(false, allowSite)).toBe(true);
});
it("sanitizes and resolves popup button orders", () => { it("sanitizes and resolves popup button orders", () => {
const controllerButtonDefs = { const controllerButtonDefs = {
rewind: {}, rewind: {},