From 6d993edf507e2dc3b268c5a6a44cb2331e95650f Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Tue, 31 Mar 2026 00:47:31 -0400 Subject: [PATCH] feat(ui): configurable control bar, popup controls, and settings overhaul --- background.js | 5 + inject.js | 152 ++++++++++++++++-------- manifest.json | 3 + options.css | 137 ++++++++++++++++++++++ options.html | 250 +++++++++++++++++++++++++++++----------- options.js | 314 +++++++++++++++++++++++++++++++++++++++++++++++++- popup.css | 284 ++++++++++++++++++++++++++++++++++++--------- popup.html | 45 ++++++-- popup.js | 196 ++++++++++++++++++++++++++----- shadow.css | 53 ++++----- 10 files changed, 1191 insertions(+), 248 deletions(-) create mode 100644 background.js diff --git a/background.js b/background.js new file mode 100644 index 0000000..544eb16 --- /dev/null +++ b/background.js @@ -0,0 +1,5 @@ +chrome.runtime.onMessage.addListener(function (request) { + if (request.action === "openOptions") { + chrome.tabs.create({ url: chrome.runtime.getURL("options.html") }); + } +}); diff --git a/inject.js b/inject.js index 70f6f45..5a1e254 100644 --- a/inject.js +++ b/inject.js @@ -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) { diff --git a/manifest.json b/manifest.json index bc66268..3589396 100644 --- a/manifest.json +++ b/manifest.json @@ -20,6 +20,9 @@ "48": "icons/icon48.png", "128": "icons/icon128.png" }, + "background": { + "scripts": ["background.js"] + }, "permissions": [ "storage" ], diff --git a/options.css b/options.css index f77e84e..ba968d2 100644 --- a/options.css +++ b/options.css @@ -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 { diff --git a/options.html b/options.html index f4423e7..00170ce 100644 --- a/options.html +++ b/options.html @@ -177,34 +177,43 @@

Defaults

Used unless a site rule overrides it.

+ +

General

+
+
+ + +
+ +
+

Playback

+ +
+ + +
+
+ + +
+ +
+

Controller

+
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
+
+ + +
+
+ + +
+
+ + +
+ +
+

Subtitle sync

+ +
+ + +
+
+ + +
+ + +
+
+

Control bar

+

+ Drag blocks to reorder. Move between Active and Available to show + or hide buttons. +

+
+
+
+
Active
+
+
+
+
Available
+
+
+
+
+ +
+
+

Popup control bar

+

+ Configure which buttons appear in the browser popup control bar. +

+
+
+ + +
+
+
+
Active
+
+
+
+
Available
+
+
+
@@ -322,6 +418,54 @@ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
-
-
-

Subtitle sync

-

Use small speed nudges if subtitles drift.

-
-
- - -
-
- - -
-
-

Actions

diff --git a/options.js b/options.js index 3cc1457..cad80f8 100644 --- a/options.js +++ b/options.js @@ -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"; + } + } }); }); diff --git a/popup.css b/popup.css index 07421fe..5a1a08f 100644 --- a/popup.css +++ b/popup.css @@ -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; + } +} diff --git a/popup.html b/popup.html index ce614b3..58537bc 100644 --- a/popup.html +++ b/popup.html @@ -1,21 +1,42 @@ - Video Speed Controller: Popup + + Speeder - -
- - - -
- -
- - -
Version
+ diff --git a/popup.js b/popup.js index 9b89347..a077ca9 100644 --- a/popup.js +++ b/popup.js @@ -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) { diff --git a/shadow.css b/shadow.css index f1112d3..0e391f7 100644 --- a/shadow.css +++ b/shadow.css @@ -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 {