mirror of
https://github.com/SoPat712/videospeed.git
synced 2026-04-21 04:42:35 -04:00
feat(ui): configurable control bar, popup controls, and settings overhaul
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
chrome.runtime.onMessage.addListener(function (request) {
|
||||
if (request.action === "openOptions") {
|
||||
chrome.tabs.create({ url: chrome.runtime.getURL("options.html") });
|
||||
}
|
||||
});
|
||||
@@ -20,6 +20,7 @@ var tc = {
|
||||
controllerOpacity: 0.3,
|
||||
keyBindings: [],
|
||||
siteRules: [],
|
||||
controllerButtons: ["rewind", "slower", "faster", "advance", "display"],
|
||||
defaultLogLevel: 3,
|
||||
logLevel: 3,
|
||||
enableSubtitleNudge: true, // Enabled by default, but only activates on YouTube
|
||||
@@ -100,6 +101,21 @@ var controllerLocationStyles = {
|
||||
}
|
||||
};
|
||||
|
||||
var controllerButtonDefs = {
|
||||
rewind: { label: "\u00AB", className: "rw" },
|
||||
slower: { label: "\u2212", className: "" },
|
||||
faster: { label: "+", className: "" },
|
||||
advance: { label: "\u00BB", className: "rw" },
|
||||
display: { label: "\u00D7", className: "hideButton" },
|
||||
reset: { label: "\u21BA", className: "" },
|
||||
fast: { label: "\u2605", className: "" },
|
||||
settings: { label: "\u2699", className: "" },
|
||||
pause: { label: "\u23EF", className: "" },
|
||||
muted: { label: "M", className: "" },
|
||||
mark: { label: "\u2691", className: "" },
|
||||
jump: { label: "\u21E5", className: "" }
|
||||
};
|
||||
|
||||
var keyCodeToEventKey = {
|
||||
32: " ",
|
||||
37: "ArrowLeft",
|
||||
@@ -519,11 +535,7 @@ function tryYouTubeNativeSpeed(video, speed) {
|
||||
}
|
||||
|
||||
function isSubtitleNudgeSupported(video) {
|
||||
return Boolean(
|
||||
video &&
|
||||
((video.currentSrc && video.currentSrc.includes("googlevideo.com")) ||
|
||||
isOnYouTube())
|
||||
);
|
||||
return Boolean(video);
|
||||
}
|
||||
|
||||
function isSubtitleNudgeEnabledForVideo(video) {
|
||||
@@ -566,31 +578,24 @@ function setSubtitleNudgeEnabledForVideo(video, enabled) {
|
||||
function updateSubtitleNudgeIndicator(video) {
|
||||
if (!video || !video.vsc) return;
|
||||
|
||||
var isSupported = isSubtitleNudgeSupported(video);
|
||||
var isEnabled = isSupported && isSubtitleNudgeEnabledForVideo(video);
|
||||
var isEnabled = isSubtitleNudgeEnabledForVideo(video);
|
||||
var label = isEnabled ? "✓" : "×";
|
||||
var title = isSupported
|
||||
? isEnabled
|
||||
? "Subtitle nudge enabled"
|
||||
: "Subtitle nudge disabled"
|
||||
: "Subtitle nudge unavailable on this site";
|
||||
var title = isEnabled ? "Subtitle nudge enabled" : "Subtitle nudge disabled";
|
||||
|
||||
// Update the hover indicator (inside #controls)
|
||||
var indicator = video.vsc.subtitleNudgeIndicator;
|
||||
if (indicator) {
|
||||
indicator.textContent = label;
|
||||
indicator.dataset.enabled = isEnabled ? "true" : "false";
|
||||
indicator.dataset.supported = isSupported ? "true" : "false";
|
||||
indicator.dataset.supported = "true";
|
||||
indicator.title = title;
|
||||
indicator.setAttribute("aria-label", title);
|
||||
}
|
||||
|
||||
// Sync the flash indicator (next to speed text)
|
||||
var flashEl = video.vsc.nudgeFlashIndicator;
|
||||
if (flashEl) {
|
||||
flashEl.textContent = label;
|
||||
flashEl.dataset.enabled = isEnabled ? "true" : "false";
|
||||
flashEl.dataset.supported = isSupported ? "true" : "false";
|
||||
flashEl.dataset.supported = "true";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -969,6 +974,10 @@ chrome.storage.sync.get(tc.settings, function (storage) {
|
||||
? storage.siteRules
|
||||
: [];
|
||||
|
||||
tc.settings.controllerButtons = Array.isArray(storage.controllerButtons)
|
||||
? storage.controllerButtons
|
||||
: tc.settings.controllerButtons;
|
||||
|
||||
// Migrate legacy blacklist if present
|
||||
if (storage.blacklist && typeof storage.blacklist === "string" && tc.settings.siteRules.length === 0) {
|
||||
var lines = storage.blacklist.split("\n");
|
||||
@@ -1021,15 +1030,37 @@ chrome.storage.sync.get(tc.settings, function (storage) {
|
||||
if (!window.vscMessageListener) {
|
||||
chrome.runtime.onMessage.addListener(
|
||||
function (request, sender, sendResponse) {
|
||||
// Check if the message is a request to re-scan the page.
|
||||
if (request.action === "rescan_page") {
|
||||
log("Re-scan command received from popup.", 4);
|
||||
initializeWhenReady(document, true);
|
||||
|
||||
sendResponse({ status: "complete" });
|
||||
} else if (request.action === "get_speed") {
|
||||
var speed = 1.0;
|
||||
if (tc.mediaElements && tc.mediaElements.length > 0) {
|
||||
for (var i = 0; i < tc.mediaElements.length; i++) {
|
||||
if (tc.mediaElements[i] && !tc.mediaElements[i].paused) {
|
||||
speed = tc.mediaElements[i].playbackRate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (speed === 1.0 && tc.mediaElements[0]) {
|
||||
speed = tc.mediaElements[0].playbackRate;
|
||||
}
|
||||
}
|
||||
sendResponse({ speed: speed });
|
||||
} else if (request.action === "run_action") {
|
||||
var value = request.value;
|
||||
if (value === undefined || value === null) {
|
||||
value = getKeyBindings(request.actionName, "value");
|
||||
}
|
||||
runAction(request.actionName, value);
|
||||
var newSpeed = 1.0;
|
||||
if (tc.mediaElements && tc.mediaElements.length > 0) {
|
||||
newSpeed = tc.mediaElements[0].playbackRate;
|
||||
}
|
||||
sendResponse({ speed: newSpeed });
|
||||
}
|
||||
|
||||
// Required to allow for asynchronous responses.
|
||||
return true;
|
||||
}
|
||||
);
|
||||
@@ -1513,36 +1544,46 @@ function defineVideoController() {
|
||||
|
||||
var controls = doc.createElement("span");
|
||||
controls.id = "controls";
|
||||
controls.appendChild(createControllerButton(doc, "rewind", "«", "rw"));
|
||||
controls.appendChild(createControllerButton(doc, "slower", "−"));
|
||||
controls.appendChild(createControllerButton(doc, "faster", "+"));
|
||||
controls.appendChild(createControllerButton(doc, "advance", "»", "rw"));
|
||||
controls.appendChild(
|
||||
createControllerButton(doc, "display", "×", "hideButton")
|
||||
);
|
||||
|
||||
var subtitleNudgeIndicator = doc.createElement("span");
|
||||
subtitleNudgeIndicator.id = "nudge-indicator";
|
||||
subtitleNudgeIndicator.setAttribute("role", "button");
|
||||
subtitleNudgeIndicator.setAttribute("aria-live", "polite");
|
||||
subtitleNudgeIndicator.setAttribute("tabindex", "0");
|
||||
var buttonConfig = Array.isArray(tc.settings.controllerButtons)
|
||||
? tc.settings.controllerButtons
|
||||
: ["rewind", "slower", "faster", "advance", "display"];
|
||||
|
||||
var subtitleNudgeIndicator = null;
|
||||
|
||||
buttonConfig.forEach(function (btnId) {
|
||||
if (btnId === "nudge") {
|
||||
subtitleNudgeIndicator = doc.createElement("span");
|
||||
subtitleNudgeIndicator.id = "nudge-indicator";
|
||||
subtitleNudgeIndicator.setAttribute("role", "button");
|
||||
subtitleNudgeIndicator.setAttribute("aria-live", "polite");
|
||||
subtitleNudgeIndicator.setAttribute("tabindex", "0");
|
||||
controls.appendChild(subtitleNudgeIndicator);
|
||||
} else {
|
||||
var def = controllerButtonDefs[btnId];
|
||||
if (def) {
|
||||
controls.appendChild(
|
||||
createControllerButton(doc, btnId, def.label, def.className)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// A second indicator that lives outside #controls, next to the speed text.
|
||||
// Hidden by default, briefly shown when N is pressed to flash the state.
|
||||
var nudgeFlashIndicator = doc.createElement("span");
|
||||
nudgeFlashIndicator.id = "nudge-flash-indicator";
|
||||
nudgeFlashIndicator.setAttribute("aria-hidden", "true");
|
||||
|
||||
controller.appendChild(dragHandle);
|
||||
controller.appendChild(nudgeFlashIndicator);
|
||||
controls.appendChild(subtitleNudgeIndicator);
|
||||
controller.appendChild(controls);
|
||||
shadow.appendChild(controller);
|
||||
|
||||
this.speedIndicator = dragHandle;
|
||||
this.subtitleNudgeIndicator = subtitleNudgeIndicator;
|
||||
this.nudgeFlashIndicator = nudgeFlashIndicator;
|
||||
updateSubtitleNudgeIndicator(this.video);
|
||||
if (subtitleNudgeIndicator) {
|
||||
updateSubtitleNudgeIndicator(this.video);
|
||||
}
|
||||
dragHandle.addEventListener(
|
||||
"mousedown",
|
||||
(e) => {
|
||||
@@ -1569,21 +1610,23 @@ function defineVideoController() {
|
||||
true
|
||||
);
|
||||
});
|
||||
subtitleNudgeIndicator.addEventListener(
|
||||
"click",
|
||||
(e) => {
|
||||
var video = this.video;
|
||||
if (video) {
|
||||
var newState = !isSubtitleNudgeEnabledForVideo(video);
|
||||
setSubtitleNudgeEnabledForVideo(video, newState);
|
||||
}
|
||||
e.stopPropagation();
|
||||
},
|
||||
true
|
||||
);
|
||||
if (subtitleNudgeIndicator) {
|
||||
subtitleNudgeIndicator.addEventListener(
|
||||
"click",
|
||||
(e) => {
|
||||
var video = this.video;
|
||||
if (video) {
|
||||
var newState = !isSubtitleNudgeEnabledForVideo(video);
|
||||
setSubtitleNudgeEnabledForVideo(video, newState);
|
||||
}
|
||||
e.stopPropagation();
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
controller.addEventListener("click", (e) => e.stopPropagation(), false);
|
||||
controller.addEventListener("mousedown", (e) => e.stopPropagation(), false);
|
||||
|
||||
|
||||
// Setup auto-hide observers if enabled
|
||||
if (tc.settings.hideWithControls) {
|
||||
if (isOnYouTube()) {
|
||||
@@ -1721,7 +1764,9 @@ function applySiteRuleOverrides() {
|
||||
"rememberSpeed",
|
||||
"forceLastSavedSpeed",
|
||||
"audioBoolean",
|
||||
"controllerOpacity"
|
||||
"controllerOpacity",
|
||||
"enableSubtitleNudge",
|
||||
"subtitleNudgeInterval"
|
||||
];
|
||||
|
||||
siteSettings.forEach((key) => {
|
||||
@@ -1731,6 +1776,11 @@ function applySiteRuleOverrides() {
|
||||
}
|
||||
});
|
||||
|
||||
if (Array.isArray(matchedRule.controllerButtons) && matchedRule.controllerButtons.length > 0) {
|
||||
log(`Overriding controllerButtons for site`, 4);
|
||||
tc.settings.controllerButtons = matchedRule.controllerButtons;
|
||||
}
|
||||
|
||||
// Override key bindings with site-specific shortcuts
|
||||
if (Array.isArray(matchedRule.shortcuts) && matchedRule.shortcuts.length > 0) {
|
||||
var overriddenActions = new Set();
|
||||
@@ -2183,6 +2233,10 @@ function runAction(action, value, e) {
|
||||
} else {
|
||||
mediaTagsToProcess = tc.mediaElements;
|
||||
}
|
||||
if (action === "settings") {
|
||||
chrome.runtime.sendMessage({ action: "openOptions" });
|
||||
return;
|
||||
}
|
||||
if (mediaTagsToProcess.length === 0 && action !== "display") return;
|
||||
|
||||
if (action === "toggleSubtitleNudge" && mediaTagsToProcess.length > 0) {
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
},
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
"permissions": [
|
||||
"storage"
|
||||
],
|
||||
|
||||
+137
@@ -108,6 +108,19 @@ h4 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.defaults-divider {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 14px 0 10px;
|
||||
}
|
||||
|
||||
.defaults-sub-heading {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.75em 0;
|
||||
}
|
||||
@@ -291,6 +304,119 @@ label em {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.cb-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.cb-editor-disabled {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.cb-zone-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.cb-dropzone {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
min-height: 52px;
|
||||
padding: 10px;
|
||||
border: 2px dashed var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--panel-subtle);
|
||||
transition: border-color 150ms ease, background 150ms ease;
|
||||
}
|
||||
|
||||
.cb-dropzone.cb-over {
|
||||
border-color: var(--accent);
|
||||
background: rgba(17, 24, 39, 0.03);
|
||||
}
|
||||
|
||||
.cb-active-zone:empty::after {
|
||||
content: "Drag buttons here";
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.cb-available-zone:empty::after {
|
||||
content: "All buttons active";
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.cb-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 12px 7px 8px;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
transition: box-shadow 150ms ease, opacity 150ms ease;
|
||||
}
|
||||
|
||||
.cb-block:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.cb-block:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.cb-block.cb-dragging {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.cb-grip {
|
||||
width: 6px;
|
||||
min-width: 6px;
|
||||
height: 14px;
|
||||
background-image: radial-gradient(circle, currentColor 1px, transparent 1px);
|
||||
background-size: 3px 3px;
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.cb-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 4px;
|
||||
background: var(--panel-subtle);
|
||||
border: 1px solid var(--border);
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.cb-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#siteRulesContainer {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
@@ -347,12 +473,14 @@ label em {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.site-rule-controlbar,
|
||||
.site-rule-shortcuts {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.site-rule-controlbar > label,
|
||||
.site-rule-shortcuts > label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -361,6 +489,11 @@ label em {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.site-controlbar-container,
|
||||
.site-popup-controlbar-container {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.site-shortcuts-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -505,6 +638,10 @@ label em {
|
||||
border-color: #dfe3e8;
|
||||
}
|
||||
|
||||
.cb-dropzone.cb-over {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
|
||||
+182
-68
@@ -177,34 +177,43 @@
|
||||
<h3>Defaults</h3>
|
||||
<p class="section-intro">Used unless a site rule overrides it.</p>
|
||||
</div>
|
||||
|
||||
<h4 class="defaults-sub-heading">General</h4>
|
||||
|
||||
<div class="row">
|
||||
<label for="enabled">Enable</label>
|
||||
<input id="enabled" type="checkbox" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="audioBoolean">Work on audio</label>
|
||||
<input id="audioBoolean" type="checkbox" />
|
||||
</div>
|
||||
|
||||
<div class="defaults-divider"></div>
|
||||
<h4 class="defaults-sub-heading">Playback</h4>
|
||||
|
||||
<div class="row">
|
||||
<label for="rememberSpeed">Remember playback speed</label>
|
||||
<input id="rememberSpeed" type="checkbox" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="forceLastSavedSpeed"
|
||||
>Force last saved speed<br />
|
||||
<em
|
||||
>Useful when a video player tries to override the speed you set
|
||||
in Speeder.</em
|
||||
>
|
||||
</label>
|
||||
<input id="forceLastSavedSpeed" type="checkbox" />
|
||||
</div>
|
||||
|
||||
<div class="defaults-divider"></div>
|
||||
<h4 class="defaults-sub-heading">Controller</h4>
|
||||
|
||||
<div class="row">
|
||||
<label for="startHidden">Hide controller by default</label>
|
||||
<input id="startHidden" type="checkbox" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="hideWithControls"
|
||||
>Hide with controls (all sites)<br />
|
||||
<em
|
||||
>Fade the controller in and out with the video interface:
|
||||
perfect sync on YouTube, idle-based elsewhere.</em
|
||||
>
|
||||
</label>
|
||||
<input id="hideWithControls" type="checkbox" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="hideWithControlsTimer"
|
||||
>Auto-hide timer (seconds)<br />
|
||||
<em
|
||||
>Seconds of inactivity before hiding: 0.1–15 for non-YouTube
|
||||
sites.</em
|
||||
>
|
||||
</label>
|
||||
<input id="hideWithControlsTimer" type="text" placeholder="2" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="controllerLocation">Default controller location</label>
|
||||
<select id="controllerLocation">
|
||||
@@ -218,28 +227,115 @@
|
||||
<option value="middle-left">Middle left</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="rememberSpeed">Remember playback speed</label>
|
||||
<input id="rememberSpeed" type="checkbox" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="forceLastSavedSpeed"
|
||||
>Force last saved speed<br />
|
||||
<em
|
||||
>Useful when a video player tries to override the speed you set
|
||||
in Speeder.</em
|
||||
>
|
||||
</label>
|
||||
<input id="forceLastSavedSpeed" type="checkbox" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="audioBoolean">Work on audio</label>
|
||||
<input id="audioBoolean" type="checkbox" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="controllerOpacity">Controller opacity</label>
|
||||
<input id="controllerOpacity" type="text" value="" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="hideWithControls"
|
||||
>Hide with controls<br />
|
||||
<em
|
||||
>Fade the controller in and out with the video interface:
|
||||
perfect sync on YouTube, idle-based elsewhere.</em
|
||||
>
|
||||
</label>
|
||||
<input id="hideWithControls" type="checkbox" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="hideWithControlsTimer"
|
||||
>Auto-hide timer (seconds)<br />
|
||||
<em
|
||||
>Seconds of inactivity before hiding: 0.1–15 for
|
||||
non-YouTube sites.</em
|
||||
>
|
||||
</label>
|
||||
<input id="hideWithControlsTimer" type="text" placeholder="2" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="showPopupControlBar">Show popup control bar</label>
|
||||
<input id="showPopupControlBar" type="checkbox" />
|
||||
</div>
|
||||
|
||||
<div class="defaults-divider"></div>
|
||||
<h4 class="defaults-sub-heading">Subtitle sync</h4>
|
||||
|
||||
<div class="row">
|
||||
<label for="enableSubtitleNudge"
|
||||
>Enable subtitle nudge<br /><em
|
||||
>Makes tiny playback changes to help keep subtitles aligned.</em
|
||||
>
|
||||
</label>
|
||||
<input id="enableSubtitleNudge" type="checkbox" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="subtitleNudgeInterval"
|
||||
>Nudge interval (milliseconds)<br /><em
|
||||
>How often to nudge: 10–1000. Smaller values are more
|
||||
frequent. Default: 50.</em
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
id="subtitleNudgeInterval"
|
||||
type="text"
|
||||
value=""
|
||||
placeholder="50"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="controlBarSettings" class="settings-card">
|
||||
<div class="section-heading">
|
||||
<h3>Control bar</h3>
|
||||
<p class="section-intro">
|
||||
Drag blocks to reorder. Move between Active and Available to show
|
||||
or hide buttons.
|
||||
</p>
|
||||
</div>
|
||||
<div class="cb-editor">
|
||||
<div class="cb-zone">
|
||||
<div class="cb-zone-label">Active</div>
|
||||
<div
|
||||
id="controlBarActive"
|
||||
class="cb-dropzone cb-active-zone"
|
||||
></div>
|
||||
</div>
|
||||
<div class="cb-zone">
|
||||
<div class="cb-zone-label">Available</div>
|
||||
<div
|
||||
id="controlBarAvailable"
|
||||
class="cb-dropzone cb-available-zone"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="popupControlBarSettings" class="settings-card">
|
||||
<div class="section-heading">
|
||||
<h3>Popup control bar</h3>
|
||||
<p class="section-intro">
|
||||
Configure which buttons appear in the browser popup control bar.
|
||||
</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="popupMatchHoverControls">Match hover controls</label>
|
||||
<input id="popupMatchHoverControls" type="checkbox" />
|
||||
</div>
|
||||
<div id="popupCbEditorWrap" class="cb-editor cb-editor-disabled">
|
||||
<div class="cb-zone">
|
||||
<div class="cb-zone-label">Active</div>
|
||||
<div
|
||||
id="popupControlBarActive"
|
||||
class="cb-dropzone cb-active-zone"
|
||||
></div>
|
||||
</div>
|
||||
<div class="cb-zone">
|
||||
<div class="cb-zone-label">Available</div>
|
||||
<div
|
||||
id="popupControlBarAvailable"
|
||||
class="cb-dropzone cb-available-zone"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="siteRules" class="settings-card">
|
||||
@@ -322,6 +418,54 @@
|
||||
<label>Controller opacity:</label>
|
||||
<input type="text" class="site-controllerOpacity" />
|
||||
</div>
|
||||
<div class="site-rule-option">
|
||||
<label>Show popup control bar:</label>
|
||||
<input type="checkbox" class="site-showPopupControlBar" />
|
||||
</div>
|
||||
<div class="site-rule-option">
|
||||
<label>Enable subtitle nudge:</label>
|
||||
<input type="checkbox" class="site-enableSubtitleNudge" />
|
||||
</div>
|
||||
<div class="site-rule-option">
|
||||
<label>Nudge interval (10–1000ms):</label>
|
||||
<input type="text" class="site-subtitleNudgeInterval" placeholder="50" />
|
||||
</div>
|
||||
<div class="site-rule-controlbar">
|
||||
<label>
|
||||
<input type="checkbox" class="override-controlbar" />
|
||||
Custom control bar for this site
|
||||
</label>
|
||||
<div class="site-controlbar-container" style="display: none">
|
||||
<div class="cb-editor">
|
||||
<div class="cb-zone">
|
||||
<div class="cb-zone-label">Active</div>
|
||||
<div class="cb-dropzone cb-active-zone site-cb-active"></div>
|
||||
</div>
|
||||
<div class="cb-zone">
|
||||
<div class="cb-zone-label">Available</div>
|
||||
<div class="cb-dropzone cb-available-zone site-cb-available"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-rule-controlbar">
|
||||
<label>
|
||||
<input type="checkbox" class="override-popup-controlbar" />
|
||||
Custom popup control bar for this site
|
||||
</label>
|
||||
<div class="site-popup-controlbar-container" style="display: none">
|
||||
<div class="cb-editor">
|
||||
<div class="cb-zone">
|
||||
<div class="cb-zone-label">Active</div>
|
||||
<div class="cb-dropzone cb-active-zone site-popup-cb-active"></div>
|
||||
</div>
|
||||
<div class="cb-zone">
|
||||
<div class="cb-zone-label">Available</div>
|
||||
<div class="cb-dropzone cb-available-zone site-popup-cb-available"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-rule-shortcuts">
|
||||
<label>
|
||||
<input type="checkbox" class="override-shortcuts" />
|
||||
@@ -334,36 +478,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<section id="nudgeSettings" class="settings-card">
|
||||
<div class="section-heading">
|
||||
<h3>Subtitle sync</h3>
|
||||
<p class="section-intro">Use small speed nudges if subtitles drift.</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="enableSubtitleNudge"
|
||||
>Enable subtitle nudge<br /><em
|
||||
>Makes tiny playback changes to help keep subtitles aligned on
|
||||
some sites, especially YouTube.</em
|
||||
>
|
||||
</label>
|
||||
<input id="enableSubtitleNudge" type="checkbox" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="subtitleNudgeInterval"
|
||||
>Nudge interval (milliseconds)<br /><em
|
||||
>How often to nudge: 10–1000. Smaller values are more frequent.
|
||||
Default: 50.</em
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
id="subtitleNudgeInterval"
|
||||
type="text"
|
||||
value=""
|
||||
placeholder="50"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-card action-card">
|
||||
<div class="section-heading">
|
||||
<h3>Actions</h3>
|
||||
|
||||
+308
-6
@@ -132,6 +132,22 @@ var controllerLocations = [
|
||||
"middle-left"
|
||||
];
|
||||
|
||||
var controllerButtonDefs = {
|
||||
rewind: { icon: "\u00AB", name: "Rewind" },
|
||||
slower: { icon: "\u2212", name: "Decrease speed" },
|
||||
faster: { icon: "+", name: "Increase speed" },
|
||||
advance: { icon: "\u00BB", name: "Advance" },
|
||||
display: { icon: "\u00D7", name: "Close controller" },
|
||||
reset: { icon: "\u21BA", name: "Reset speed" },
|
||||
fast: { icon: "\u2605", name: "Preferred speed" },
|
||||
nudge: { icon: "\u2713", name: "Subtitle nudge" },
|
||||
settings: { icon: "\u2699", name: "Settings" },
|
||||
pause: { icon: "\u23EF", name: "Pause / Play" },
|
||||
muted: { icon: "M", name: "Mute / Unmute" },
|
||||
mark: { icon: "\u2691", name: "Set marker" },
|
||||
jump: { icon: "\u21E5", name: "Jump to marker" }
|
||||
};
|
||||
|
||||
function createDefaultBinding(action, key, keyCode, value) {
|
||||
return {
|
||||
action: action,
|
||||
@@ -169,11 +185,16 @@ var tcDefaults = {
|
||||
createDefaultBinding("toggleSubtitleNudge", "N", 78, 0)
|
||||
],
|
||||
siteRules: [
|
||||
{ pattern: "youtube.com", enabled: true, enableSubtitleNudge: true },
|
||||
{ pattern: "example1.com", enabled: false },
|
||||
{ pattern: "/example2\\.com/i", enabled: false },
|
||||
{ pattern: "/(example3|sample3)\\.com/gi", enabled: false }
|
||||
],
|
||||
enableSubtitleNudge: true,
|
||||
controllerButtons: ["rewind", "slower", "faster", "advance", "display"],
|
||||
showPopupControlBar: true,
|
||||
popupMatchHoverControls: true,
|
||||
popupControllerButtons: ["rewind", "slower", "faster", "advance", "display"],
|
||||
enableSubtitleNudge: false,
|
||||
subtitleNudgeInterval: 50,
|
||||
subtitleNudgeAmount: 0.001
|
||||
};
|
||||
@@ -608,6 +629,13 @@ function save_options() {
|
||||
settings.subtitleNudgeInterval = 1000;
|
||||
}
|
||||
|
||||
settings.controllerButtons = getControlBarOrder();
|
||||
settings.showPopupControlBar =
|
||||
document.getElementById("showPopupControlBar").checked;
|
||||
settings.popupMatchHoverControls =
|
||||
document.getElementById("popupMatchHoverControls").checked;
|
||||
settings.popupControllerButtons = getPopupControlBarOrder();
|
||||
|
||||
// Collect site rules
|
||||
settings.siteRules = [];
|
||||
document.querySelectorAll(".site-rule").forEach((ruleEl) => {
|
||||
@@ -628,18 +656,46 @@ function save_options() {
|
||||
{ key: "rememberSpeed", type: "checkbox" },
|
||||
{ key: "forceLastSavedSpeed", type: "checkbox" },
|
||||
{ key: "audioBoolean", type: "checkbox" },
|
||||
{ key: "controllerOpacity", type: "text" }
|
||||
{ key: "controllerOpacity", type: "text" },
|
||||
{ key: "showPopupControlBar", type: "checkbox" },
|
||||
{ key: "enableSubtitleNudge", type: "checkbox" },
|
||||
{ key: "subtitleNudgeInterval", type: "text" }
|
||||
];
|
||||
|
||||
siteSettings.forEach((s) => {
|
||||
var input = ruleEl.querySelector(`.site-${s.key}`);
|
||||
if (!input) return;
|
||||
var siteValue;
|
||||
if (s.type === "checkbox") {
|
||||
rule[s.key] = input.checked;
|
||||
siteValue = input.checked;
|
||||
} else {
|
||||
rule[s.key] = input.value;
|
||||
siteValue = input.value;
|
||||
}
|
||||
var globalInput = document.getElementById(s.key);
|
||||
if (globalInput) {
|
||||
var globalValue = s.type === "checkbox" ? globalInput.checked : globalInput.value;
|
||||
if (String(siteValue) !== String(globalValue)) {
|
||||
rule[s.key] = siteValue;
|
||||
}
|
||||
} else {
|
||||
rule[s.key] = siteValue;
|
||||
}
|
||||
});
|
||||
|
||||
if (ruleEl.querySelector(".override-controlbar").checked) {
|
||||
var activeZone = ruleEl.querySelector(".site-cb-active");
|
||||
if (activeZone) {
|
||||
rule.controllerButtons = readControlBarOrder(activeZone);
|
||||
}
|
||||
}
|
||||
|
||||
if (ruleEl.querySelector(".override-popup-controlbar").checked) {
|
||||
var popupActiveZone = ruleEl.querySelector(".site-popup-cb-active");
|
||||
if (popupActiveZone) {
|
||||
rule.popupControllerButtons = readControlBarOrder(popupActiveZone);
|
||||
}
|
||||
}
|
||||
|
||||
if (ruleEl.querySelector(".override-shortcuts").checked) {
|
||||
var shortcuts = [];
|
||||
ruleEl.querySelectorAll(".site-shortcuts-container .customs").forEach((shortcutRow) => {
|
||||
@@ -844,7 +900,10 @@ function createSiteRule(rule) {
|
||||
{ key: "rememberSpeed", type: "checkbox" },
|
||||
{ key: "forceLastSavedSpeed", type: "checkbox" },
|
||||
{ key: "audioBoolean", type: "checkbox" },
|
||||
{ key: "controllerOpacity", type: "text" }
|
||||
{ key: "controllerOpacity", type: "text" },
|
||||
{ key: "showPopupControlBar", type: "checkbox" },
|
||||
{ key: "enableSubtitleNudge", type: "checkbox" },
|
||||
{ key: "subtitleNudgeInterval", type: "text" }
|
||||
];
|
||||
|
||||
settings.forEach((s) => {
|
||||
@@ -873,6 +932,28 @@ function createSiteRule(rule) {
|
||||
}
|
||||
});
|
||||
|
||||
if (rule && Array.isArray(rule.controllerButtons) && rule.controllerButtons.length > 0) {
|
||||
ruleEl.querySelector(".override-controlbar").checked = true;
|
||||
var cbContainer = ruleEl.querySelector(".site-controlbar-container");
|
||||
cbContainer.style.display = "block";
|
||||
populateControlBarZones(
|
||||
ruleEl.querySelector(".site-cb-active"),
|
||||
ruleEl.querySelector(".site-cb-available"),
|
||||
rule.controllerButtons
|
||||
);
|
||||
}
|
||||
|
||||
if (rule && Array.isArray(rule.popupControllerButtons) && rule.popupControllerButtons.length > 0) {
|
||||
ruleEl.querySelector(".override-popup-controlbar").checked = true;
|
||||
var popupCbContainer = ruleEl.querySelector(".site-popup-controlbar-container");
|
||||
popupCbContainer.style.display = "block";
|
||||
populateControlBarZones(
|
||||
ruleEl.querySelector(".site-popup-cb-active"),
|
||||
ruleEl.querySelector(".site-popup-cb-available"),
|
||||
rule.popupControllerButtons
|
||||
);
|
||||
}
|
||||
|
||||
if (rule && Array.isArray(rule.shortcuts) && rule.shortcuts.length > 0) {
|
||||
ruleEl.querySelector(".override-shortcuts").checked = true;
|
||||
var container = ruleEl.querySelector(".site-shortcuts-container");
|
||||
@@ -898,6 +979,164 @@ function populateDefaultSiteShortcuts(container) {
|
||||
});
|
||||
}
|
||||
|
||||
function createControlBarBlock(buttonId) {
|
||||
var def = controllerButtonDefs[buttonId];
|
||||
if (!def) return null;
|
||||
|
||||
var block = document.createElement("div");
|
||||
block.className = "cb-block";
|
||||
block.dataset.buttonId = buttonId;
|
||||
block.draggable = true;
|
||||
|
||||
var grip = document.createElement("span");
|
||||
grip.className = "cb-grip";
|
||||
|
||||
var icon = document.createElement("span");
|
||||
icon.className = "cb-icon";
|
||||
icon.textContent = def.icon;
|
||||
|
||||
var label = document.createElement("span");
|
||||
label.className = "cb-label";
|
||||
label.textContent = def.name;
|
||||
|
||||
block.appendChild(grip);
|
||||
block.appendChild(icon);
|
||||
block.appendChild(label);
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
function populateControlBarZones(activeZone, availableZone, activeIds) {
|
||||
activeZone.innerHTML = "";
|
||||
availableZone.innerHTML = "";
|
||||
|
||||
activeIds.forEach(function (id) {
|
||||
var block = createControlBarBlock(id);
|
||||
if (block) activeZone.appendChild(block);
|
||||
});
|
||||
|
||||
Object.keys(controllerButtonDefs).forEach(function (id) {
|
||||
if (!activeIds.includes(id)) {
|
||||
var block = createControlBarBlock(id);
|
||||
if (block) availableZone.appendChild(block);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function readControlBarOrder(activeZone) {
|
||||
var blocks = activeZone.querySelectorAll(".cb-block");
|
||||
return Array.from(blocks).map(function (block) {
|
||||
return block.dataset.buttonId;
|
||||
});
|
||||
}
|
||||
|
||||
function populateControlBarEditor(activeIds) {
|
||||
populateControlBarZones(
|
||||
document.getElementById("controlBarActive"),
|
||||
document.getElementById("controlBarAvailable"),
|
||||
activeIds
|
||||
);
|
||||
}
|
||||
|
||||
function getControlBarOrder() {
|
||||
return readControlBarOrder(document.getElementById("controlBarActive"));
|
||||
}
|
||||
|
||||
function populatePopupControlBarEditor(activeIds) {
|
||||
populateControlBarZones(
|
||||
document.getElementById("popupControlBarActive"),
|
||||
document.getElementById("popupControlBarAvailable"),
|
||||
activeIds
|
||||
);
|
||||
}
|
||||
|
||||
function getPopupControlBarOrder() {
|
||||
return readControlBarOrder(document.getElementById("popupControlBarActive"));
|
||||
}
|
||||
|
||||
function updatePopupEditorDisabledState() {
|
||||
var checkbox = document.getElementById("popupMatchHoverControls");
|
||||
var wrap = document.getElementById("popupCbEditorWrap");
|
||||
if (!checkbox || !wrap) return;
|
||||
if (checkbox.checked) {
|
||||
wrap.classList.add("cb-editor-disabled");
|
||||
} else {
|
||||
wrap.classList.remove("cb-editor-disabled");
|
||||
}
|
||||
}
|
||||
|
||||
function getDragAfterElement(container, x, y) {
|
||||
var elements = Array.from(
|
||||
container.querySelectorAll(".cb-block:not(.cb-dragging)")
|
||||
);
|
||||
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
var box = elements[i].getBoundingClientRect();
|
||||
var centerX = box.left + box.width / 2;
|
||||
var centerY = box.top + box.height / 2;
|
||||
var rowThresh = box.height * 0.5;
|
||||
|
||||
if (y - centerY > rowThresh) continue;
|
||||
if (centerY - y > rowThresh) return elements[i];
|
||||
if (x < centerX) return elements[i];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function initControlBarEditor() {
|
||||
var zones = document.querySelectorAll(".cb-dropzone");
|
||||
var draggedBlock = null;
|
||||
|
||||
document.addEventListener("dragstart", function (e) {
|
||||
var block = e.target.closest(".cb-block");
|
||||
if (!block) return;
|
||||
draggedBlock = block;
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
e.dataTransfer.setData("text/plain", block.dataset.buttonId);
|
||||
requestAnimationFrame(function () {
|
||||
block.classList.add("cb-dragging");
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("dragend", function (e) {
|
||||
var block = e.target.closest(".cb-block");
|
||||
if (!block) return;
|
||||
block.classList.remove("cb-dragging");
|
||||
draggedBlock = null;
|
||||
zones.forEach(function (zone) {
|
||||
zone.classList.remove("cb-over");
|
||||
});
|
||||
});
|
||||
|
||||
zones.forEach(function (zone) {
|
||||
zone.addEventListener("dragover", function (e) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
zone.classList.add("cb-over");
|
||||
|
||||
if (!draggedBlock) return;
|
||||
|
||||
var afterEl = getDragAfterElement(zone, e.clientX, e.clientY);
|
||||
if (afterEl) {
|
||||
zone.insertBefore(draggedBlock, afterEl);
|
||||
} else {
|
||||
zone.appendChild(draggedBlock);
|
||||
}
|
||||
});
|
||||
|
||||
zone.addEventListener("dragleave", function (e) {
|
||||
if (zone.contains(e.relatedTarget)) return;
|
||||
zone.classList.remove("cb-over");
|
||||
});
|
||||
|
||||
zone.addEventListener("drop", function (e) {
|
||||
e.preventDefault();
|
||||
zone.classList.remove("cb-over");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function restore_options() {
|
||||
chrome.storage.sync.get(tcDefaults, function (storage) {
|
||||
document.getElementById("rememberSpeed").checked = storage.rememberSpeed;
|
||||
@@ -906,7 +1145,7 @@ function restore_options() {
|
||||
document.getElementById("audioBoolean").checked = storage.audioBoolean;
|
||||
document.getElementById("enabled").checked = storage.enabled;
|
||||
document.getElementById("startHidden").checked = storage.startHidden;
|
||||
|
||||
|
||||
// Migration/Normalization for hideWithControls
|
||||
const hideWithControls = typeof storage.hideWithControls !== "undefined"
|
||||
? storage.hideWithControls
|
||||
@@ -920,6 +1159,8 @@ function restore_options() {
|
||||
normalizeControllerLocation(storage.controllerLocation);
|
||||
document.getElementById("controllerOpacity").value =
|
||||
storage.controllerOpacity;
|
||||
document.getElementById("showPopupControlBar").checked =
|
||||
storage.showPopupControlBar !== false;
|
||||
document.getElementById("enableSubtitleNudge").checked =
|
||||
storage.enableSubtitleNudge;
|
||||
document.getElementById("subtitleNudgeInterval").value =
|
||||
@@ -981,6 +1222,24 @@ function restore_options() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var controllerButtons =
|
||||
Array.isArray(storage.controllerButtons) &&
|
||||
storage.controllerButtons.length > 0
|
||||
? storage.controllerButtons
|
||||
: tcDefaults.controllerButtons;
|
||||
populateControlBarEditor(controllerButtons);
|
||||
|
||||
document.getElementById("popupMatchHoverControls").checked =
|
||||
storage.popupMatchHoverControls !== false;
|
||||
|
||||
var popupButtons =
|
||||
Array.isArray(storage.popupControllerButtons) &&
|
||||
storage.popupControllerButtons.length > 0
|
||||
? storage.popupControllerButtons
|
||||
: tcDefaults.popupControllerButtons;
|
||||
populatePopupControlBarEditor(popupButtons);
|
||||
updatePopupEditorDisabledState();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1005,6 +1264,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
|
||||
restore_options();
|
||||
initControlBarEditor();
|
||||
|
||||
document.getElementById("popupMatchHoverControls")
|
||||
.addEventListener("change", updatePopupEditorDisabledState);
|
||||
|
||||
document.getElementById("save").addEventListener("click", save_options);
|
||||
|
||||
const addSelector = document.getElementById("addShortcutSelector");
|
||||
@@ -1096,5 +1360,43 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
container.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
if (event.target.classList.contains("override-controlbar")) {
|
||||
var cbContainer = event.target
|
||||
.closest(".site-rule-controlbar")
|
||||
.querySelector(".site-controlbar-container");
|
||||
if (event.target.checked) {
|
||||
cbContainer.style.display = "block";
|
||||
var activeZone = cbContainer.querySelector(".site-cb-active");
|
||||
if (activeZone && activeZone.children.length === 0) {
|
||||
populateControlBarZones(
|
||||
activeZone,
|
||||
cbContainer.querySelector(".site-cb-available"),
|
||||
getControlBarOrder()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
cbContainer.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
if (event.target.classList.contains("override-popup-controlbar")) {
|
||||
var popupCbContainer = event.target
|
||||
.closest(".site-rule-controlbar")
|
||||
.querySelector(".site-popup-controlbar-container");
|
||||
if (event.target.checked) {
|
||||
popupCbContainer.style.display = "block";
|
||||
var popupActiveZone = popupCbContainer.querySelector(".site-popup-cb-active");
|
||||
if (popupActiveZone && popupActiveZone.children.length === 0) {
|
||||
populateControlBarZones(
|
||||
popupActiveZone,
|
||||
popupCbContainer.querySelector(".site-popup-cb-available"),
|
||||
getPopupControlBarOrder()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
popupCbContainer.style.display = "none";
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,79 +1,253 @@
|
||||
body {
|
||||
min-width: 8em;
|
||||
background-color: white;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.version {
|
||||
margin-top: 0.7em;
|
||||
font-size: 0.85em;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
:root {
|
||||
--bg: #f4f5f7;
|
||||
--panel: #ffffff;
|
||||
--border: #e2e5e9;
|
||||
--border-strong: #d4d9e0;
|
||||
--text: #17191c;
|
||||
--muted: #626b76;
|
||||
--accent: #111827;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
height: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.3);
|
||||
margin: 0.6em 0;
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
min-width: 220px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font: 13px/1.45 "Avenir Next", "SF Pro Text", "Segoe UI", sans-serif;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.popup-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
font-family: "Avenir Next", "SF Pro Display", "Segoe UI", sans-serif;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.popup-version {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
padding: 2px 7px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
button {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
background-image: linear-gradient(#ededed, #ededed 38%, #dedede);
|
||||
border: 1px solid rgba(0, 0, 0, 0.25);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08),
|
||||
inset 0 1px 2px rgba(255, 255, 255, 0.75);
|
||||
color: #444;
|
||||
text-shadow: 0 1px 0 rgb(240, 240, 240);
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease, border-color 120ms ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #f8f9fb;
|
||||
border-color: #c5ccd5;
|
||||
}
|
||||
|
||||
button:active {
|
||||
background: #f1f3f5;
|
||||
}
|
||||
|
||||
button:focus-visible {
|
||||
outline: 2px solid rgba(17, 24, 39, 0.14);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
#refresh {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#refresh:hover {
|
||||
background: #1f2937;
|
||||
border-color: #1f2937;
|
||||
}
|
||||
|
||||
.popup-divider {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.popup-control-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 6px 10px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.popup-speed {
|
||||
font-family: "Lucida Console", Monaco, monospace;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: var(--text);
|
||||
margin-right: 4px;
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.popup-control-bar button {
|
||||
width: auto;
|
||||
min-height: 24px;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 6px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: "Lucida Console", Monaco, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 13px;
|
||||
font-weight: bold;
|
||||
padding: 3px 7px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.popup-control-bar button:hover {
|
||||
background: var(--panel);
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.popup-control-bar button:active {
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.popup-control-bar button.rw {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.popup-control-bar button.hideButton {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.popup-status {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.popup-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.popup-secondary {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
font-size: 0.95em;
|
||||
margin: 0.15em 0;
|
||||
font-size: 12px;
|
||||
min-height: 28px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.donate-wrap {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.donate-split {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.donate-split button {
|
||||
width: auto;
|
||||
border-radius: 0;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
/* Dark mode styles */
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #111315;
|
||||
--panel: #171a1d;
|
||||
--border: #2b3138;
|
||||
--border-strong: #3a414a;
|
||||
--text: #f2f4f6;
|
||||
--muted: #a0a8b2;
|
||||
--accent: #f2f4f6;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
button {
|
||||
background-image: linear-gradient(#404040, #404040 38%, #353535);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.08),
|
||||
inset 0 1px 2px rgba(0, 0, 0, 0.75);
|
||||
color: #e0e0e0;
|
||||
text-shadow: 0 1px 0 rgb(20, 20, 20);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-image: linear-gradient(#4a4a4a, #4a4a4a 38%, #3f3f3f);
|
||||
background: #1f2226;
|
||||
border-color: #4a515a;
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-image: linear-gradient(#353535, #353535 38%, #2a2a2a);
|
||||
background: #252a2f;
|
||||
}
|
||||
|
||||
#status {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.version {
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
#refresh {
|
||||
background: #f2f4f6;
|
||||
border-color: #f2f4f6;
|
||||
color: #111315;
|
||||
}
|
||||
|
||||
#refresh:hover {
|
||||
background: #dfe3e8;
|
||||
border-color: #dfe3e8;
|
||||
}
|
||||
}
|
||||
|
||||
+33
-12
@@ -1,21 +1,42 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Video Speed Controller: Popup</title>
|
||||
<meta charset="utf-8" />
|
||||
<title>Speeder</title>
|
||||
<link rel="stylesheet" href="popup.css" />
|
||||
<script src="popup.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<button id="refresh">Re-scan Page for Videos</button>
|
||||
<hr />
|
||||
<button id="enable" class="hide">Enable</button>
|
||||
<button id="disable">Disable</button>
|
||||
<span id="status" class="hide"></span>
|
||||
<hr />
|
||||
<button id="config">Settings</button>
|
||||
<hr />
|
||||
<button id="feedback" class="secondary">Send feedback</button>
|
||||
<button id="about" class="secondary">About</button>
|
||||
<div class="version">Version <span id="app-version"></span></div>
|
||||
<div class="popup-shell">
|
||||
<div class="popup-header">
|
||||
<span class="popup-title">Speeder</span>
|
||||
<span class="popup-version">v<span id="app-version"></span></span>
|
||||
</div>
|
||||
<div class="popup-actions">
|
||||
<button id="refresh">Rescan page for videos</button>
|
||||
<div class="popup-divider"></div>
|
||||
<div id="popupControlBar" class="popup-control-bar">
|
||||
<span id="popupSpeed" class="popup-speed">1.00</span>
|
||||
</div>
|
||||
<div class="popup-divider"></div>
|
||||
<button id="enable" class="hide">Enable</button>
|
||||
<button id="disable">Disable</button>
|
||||
</div>
|
||||
<div id="status" class="popup-status hide"></div>
|
||||
<div class="popup-links">
|
||||
<button id="config">Settings</button>
|
||||
<div class="popup-secondary">
|
||||
<button id="feedback" class="secondary">Feedback</button>
|
||||
<button id="about" class="secondary">About</button>
|
||||
</div>
|
||||
<div id="donateWrap" class="donate-wrap">
|
||||
<button id="donate" class="secondary">Donate</button>
|
||||
<div id="donateOptions" class="donate-split hide">
|
||||
<button id="donateKofi" class="secondary">Ko-fi</button>
|
||||
<button id="donateGithub" class="secondary">Sponsors</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
|
||||
|
||||
var controllerButtonDefs = {
|
||||
rewind: { label: "\u00AB", className: "rw" },
|
||||
slower: { label: "\u2212", className: "" },
|
||||
faster: { label: "+", className: "" },
|
||||
advance: { label: "\u00BB", className: "rw" },
|
||||
display: { label: "\u00D7", className: "hideButton" },
|
||||
reset: { label: "\u21BA", className: "" },
|
||||
fast: { label: "\u2605", className: "" },
|
||||
settings: { label: "\u2699", className: "" },
|
||||
pause: { label: "\u23EF", className: "" },
|
||||
muted: { label: "M", className: "" },
|
||||
mark: { label: "\u2691", className: "" },
|
||||
jump: { label: "\u21E5", className: "" }
|
||||
};
|
||||
|
||||
var defaultButtons = ["rewind", "slower", "faster", "advance", "display"];
|
||||
|
||||
function escapeStringRegExp(str) {
|
||||
const m = /[|\\{}()[\]^$+*?.]/g;
|
||||
return str.replace(m, "\\$&");
|
||||
@@ -27,6 +44,102 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
return b;
|
||||
}
|
||||
|
||||
function matchSiteRule(url, siteRules) {
|
||||
if (!url || !Array.isArray(siteRules)) return null;
|
||||
for (var i = 0; i < siteRules.length; i++) {
|
||||
var rule = siteRules[i];
|
||||
if (!rule || !rule.pattern) continue;
|
||||
var pattern = rule.pattern.replace(regStrip, "");
|
||||
if (pattern.length === 0) continue;
|
||||
var re;
|
||||
if (pattern.startsWith("/") && pattern.lastIndexOf("/") > 0) {
|
||||
try {
|
||||
var ls = pattern.lastIndexOf("/");
|
||||
re = new RegExp(pattern.substring(1, ls), pattern.substring(ls + 1));
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
re = new RegExp(escapeStringRegExp(pattern));
|
||||
}
|
||||
if (re && re.test(url)) return rule;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setControlBarVisible(visible) {
|
||||
var bar = document.getElementById("popupControlBar");
|
||||
var dividers = document.querySelectorAll(".popup-divider");
|
||||
if (bar) bar.style.display = visible ? "" : "none";
|
||||
dividers.forEach(function (d) { d.style.display = visible ? "" : "none"; });
|
||||
}
|
||||
|
||||
function sendToActiveTab(message, callback) {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
|
||||
if (tabs[0] && tabs[0].id) {
|
||||
chrome.tabs.sendMessage(tabs[0].id, message, function (response) {
|
||||
if (chrome.runtime.lastError) {
|
||||
if (callback) callback(null);
|
||||
} else {
|
||||
if (callback) callback(response);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (callback) callback(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateSpeedDisplay(speed) {
|
||||
var el = document.getElementById("popupSpeed");
|
||||
if (el) el.textContent = (speed != null ? Number(speed) : 1).toFixed(2);
|
||||
}
|
||||
|
||||
function querySpeed() {
|
||||
sendToActiveTab({ action: "get_speed" }, function (response) {
|
||||
if (response && response.speed != null) {
|
||||
updateSpeedDisplay(response.speed);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildControlBar(buttons) {
|
||||
var bar = document.getElementById("popupControlBar");
|
||||
if (!bar) return;
|
||||
|
||||
var existing = bar.querySelectorAll("button");
|
||||
existing.forEach(function (btn) { btn.remove(); });
|
||||
|
||||
buttons.forEach(function (btnId) {
|
||||
if (btnId === "nudge") return;
|
||||
var def = controllerButtonDefs[btnId];
|
||||
if (!def) return;
|
||||
|
||||
var btn = document.createElement("button");
|
||||
btn.dataset.action = btnId;
|
||||
btn.textContent = def.label;
|
||||
if (def.className) btn.className = def.className;
|
||||
btn.title = btnId.charAt(0).toUpperCase() + btnId.slice(1);
|
||||
|
||||
btn.addEventListener("click", function () {
|
||||
if (btnId === "settings") {
|
||||
window.open(chrome.runtime.getURL("options.html"));
|
||||
return;
|
||||
}
|
||||
sendToActiveTab(
|
||||
{ action: "run_action", actionName: btnId },
|
||||
function (response) {
|
||||
if (response && response.speed != null) {
|
||||
updateSpeedDisplay(response.speed);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
bar.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
var manifest = chrome.runtime.getManifest();
|
||||
var versionElement = document.querySelector("#app-version");
|
||||
if (versionElement) {
|
||||
@@ -45,6 +158,19 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
window.open("https://github.com/SoPat712/Speeder/issues");
|
||||
});
|
||||
|
||||
document.querySelector("#donate").addEventListener("click", function () {
|
||||
this.classList.add("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 () {
|
||||
toggleEnabled(true, settingsSavedReloadMessage);
|
||||
});
|
||||
@@ -53,27 +179,16 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
toggleEnabled(false, settingsSavedReloadMessage);
|
||||
});
|
||||
|
||||
// --- REVISED: "Re-scan" button functionality ---
|
||||
document.querySelector("#refresh").addEventListener("click", function () {
|
||||
setStatusMessage("Re-scanning page...");
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
|
||||
if (tabs[0] && tabs[0].id) {
|
||||
// Send a message to the content script, asking it to re-initialize.
|
||||
chrome.tabs.sendMessage(
|
||||
tabs[0].id,
|
||||
{ action: "rescan_page" },
|
||||
function (response) {
|
||||
if (chrome.runtime.lastError) {
|
||||
// This error is expected on pages where content scripts cannot run.
|
||||
setStatusMessage("Cannot run on this page.");
|
||||
} else if (response && response.status === "complete") {
|
||||
setStatusMessage("Scan complete. Closing...");
|
||||
setTimeout(() => window.close(), 500); // Close popup on success.
|
||||
} else {
|
||||
setStatusMessage("Scan failed. Please reload the page.");
|
||||
}
|
||||
}
|
||||
);
|
||||
setStatusMessage("Rescanning page...");
|
||||
sendToActiveTab({ action: "rescan_page" }, function (response) {
|
||||
if (!response) {
|
||||
setStatusMessage("Cannot run on this page.");
|
||||
} else if (response.status === "complete") {
|
||||
setStatusMessage("Scan complete. Closing...");
|
||||
setTimeout(function () { window.close(); }, 500);
|
||||
} else {
|
||||
setStatusMessage("Scan failed. Please reload the page.");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -81,6 +196,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
chrome.storage.sync.get(
|
||||
{
|
||||
enabled: true,
|
||||
showPopupControlBar: true,
|
||||
controllerButtons: defaultButtons,
|
||||
popupMatchHoverControls: true,
|
||||
popupControllerButtons: defaultButtons,
|
||||
siteRules: [],
|
||||
blacklist: `\
|
||||
www.instagram.com
|
||||
twitter.com
|
||||
@@ -97,20 +217,38 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
if (blacklisted) {
|
||||
setStatusMessage("Site is blacklisted.");
|
||||
}
|
||||
|
||||
var siteRule = matchSiteRule(url, storage.siteRules);
|
||||
|
||||
var buttons = storage.popupMatchHoverControls
|
||||
? storage.controllerButtons
|
||||
: storage.popupControllerButtons;
|
||||
|
||||
if (siteRule && Array.isArray(siteRule.popupControllerButtons) && siteRule.popupControllerButtons.length > 0) {
|
||||
buttons = siteRule.popupControllerButtons;
|
||||
}
|
||||
|
||||
if (!Array.isArray(buttons) || buttons.length === 0) {
|
||||
buttons = defaultButtons;
|
||||
}
|
||||
|
||||
buildControlBar(buttons);
|
||||
querySpeed();
|
||||
|
||||
var showBar = storage.showPopupControlBar !== false;
|
||||
if (siteRule && siteRule.showPopupControlBar !== undefined) {
|
||||
showBar = siteRule.showPopupControlBar;
|
||||
}
|
||||
setControlBarVisible(showBar);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
function toggleEnabled(enabled, callback) {
|
||||
chrome.storage.sync.set(
|
||||
{
|
||||
enabled: enabled
|
||||
},
|
||||
function () {
|
||||
toggleEnabledUI(enabled);
|
||||
if (callback) callback(enabled);
|
||||
}
|
||||
);
|
||||
chrome.storage.sync.set({ enabled: enabled }, function () {
|
||||
toggleEnabledUI(enabled);
|
||||
if (callback) callback(enabled);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleEnabledUI(enabled) {
|
||||
|
||||
+24
-29
@@ -61,15 +61,13 @@
|
||||
vertical-align: middle;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 1.35em;
|
||||
height: 1.35em;
|
||||
margin-left: 0.3em;
|
||||
padding: 0 0.25em;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||
font-size: 12px;
|
||||
padding: 3px 6px;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
font-family: "Lucida Console", Monaco, monospace;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -83,49 +81,46 @@
|
||||
}
|
||||
|
||||
#nudge-flash-indicator[data-enabled="true"] {
|
||||
color: #bff3a2;
|
||||
background: rgba(75, 145, 53, 0.28);
|
||||
border-color: rgba(126, 199, 104, 0.7);
|
||||
color: #fff;
|
||||
background: #4b9135;
|
||||
border: 1px solid #6ec754;
|
||||
}
|
||||
|
||||
#nudge-flash-indicator[data-enabled="false"] {
|
||||
color: #ffb8b8;
|
||||
background: rgba(164, 73, 73, 0.24);
|
||||
border-color: rgba(214, 118, 118, 0.65);
|
||||
color: #fff;
|
||||
background: #943e3e;
|
||||
border: 1px solid #c06060;
|
||||
}
|
||||
|
||||
#nudge-indicator {
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 1.35em;
|
||||
height: 1.35em;
|
||||
margin-left: 0.45em;
|
||||
padding: 0 0.25em;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||
font-size: 12px;
|
||||
padding: 3px 6px;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
font-family: "Lucida Console", Monaco, monospace;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
#nudge-indicator[data-enabled="true"] {
|
||||
color: #bff3a2;
|
||||
background: rgba(75, 145, 53, 0.28);
|
||||
border-color: rgba(126, 199, 104, 0.7);
|
||||
color: #fff;
|
||||
background: #4b9135;
|
||||
border: 1px solid #6ec754;
|
||||
}
|
||||
|
||||
#nudge-indicator[data-enabled="false"] {
|
||||
color: #ffb8b8;
|
||||
background: rgba(164, 73, 73, 0.24);
|
||||
border-color: rgba(214, 118, 118, 0.65);
|
||||
color: #fff;
|
||||
background: #943e3e;
|
||||
border: 1px solid #c06060;
|
||||
}
|
||||
|
||||
#nudge-indicator[data-supported="false"] {
|
||||
opacity: 0.75;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
#controller.dragging {
|
||||
|
||||
Reference in New Issue
Block a user