diff --git a/.gitignore b/.gitignore index 46addde..03c6b3a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store +local # IntelliJ IDEA .idea/ diff --git a/README.md b/README.md index b7c0fc2..fdd6546 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,17 @@ Once the extension is installed simply navigate to any page that offers HTML5 vi * **S** - decrease playback speed. * **D** - increase playback speed. -* **R** - reset playback speed. +* **R** - reset playback speed to 1.0x. * **Z** - rewind video by 10 seconds. * **X** - advance video by 10 seconds. +* **G** - toggle between current and user configurable preferred speed. * **V** - show/hide the controller. -Some sites may assign other functionality to same shortcut keys — these collisions are inevitable, unfortunately. As a workaround, we listen both for lower and upper case values (i.e. you can use `Shift-`) if there is other functionality assigned to the lowercase key. This is not a perfect solution, as some sites may listen to both, but works most of the time. +You can customize and reassign the default shortcut keys in the extensions settings page, as well as add additional shortcut keys to match your preferences. For example, you can assign multiple different "preferred speed" shortcuts with different values, which will allow you to quickly toggle between your most commonly used speeds. To add a new shortcut, open extension settings and click "Add New". -_Note: you can customize these shortcut keys in the extension settings page and even make the extension remember your current playback speed._ +![settings Add New shortcut](https://user-images.githubusercontent.com/121805/50726471-50242200-1172-11e9-902f-0e5958387617.jpg) + +Some sites may assign other functionality to one of the assigned shortcut keys — these collisions are inevitable, unfortunately. As a workaround, the extension listens both for lower and upper case values (i.e. you can use `Shift-`) if there is other functionality assigned to the lowercase key. This is not a perfect solution, as some sites may listen to both, but works most of the time. ### FAQ diff --git a/icons/icon19_disabled.png b/icons/icon19_disabled.png new file mode 100644 index 0000000..acafee2 Binary files /dev/null and b/icons/icon19_disabled.png differ diff --git a/icons/icon38_disabled.png b/icons/icon38_disabled.png new file mode 100644 index 0000000..7666e21 Binary files /dev/null and b/icons/icon38_disabled.png differ diff --git a/icons/icon48_disabled.png b/icons/icon48_disabled.png new file mode 100644 index 0000000..d54e849 Binary files /dev/null and b/icons/icon48_disabled.png differ diff --git a/inject.css b/inject.css index 254c408..3487118 100644 --- a/inject.css +++ b/inject.css @@ -1,3 +1,4 @@ +.vsc-nosource { display: none !important; } .vsc-hidden { display: none !important; } .vsc-manual { visibility: visible !important; @@ -26,7 +27,13 @@ /* e.g. https://www.igvita.com/2012/09/12/web-fonts-performance-making-pretty-fast/ */ .html5-video-player:not(.ytp-hide-info-bar) .vsc-controller { position: relative; - top: 40px; + top: 60px; +} + +/* Facebook player */ +#facebook .vsc-controller { + position: relative; + top: 40px; } /* Google Photos player */ diff --git a/inject.js b/inject.js index 843516c..995d72b 100644 --- a/inject.js +++ b/inject.js @@ -1,87 +1,200 @@ -chrome.runtime.sendMessage({}, function(response) { + var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm; + var tc = { settings: { - speed: 1.0, // default 1x - resetSpeed: 1.0, // default 1x - speedStep: 0.1, // default 0.1x - fastSpeed: 1.8, // default 1.8x - rewindTime: 10, // default 10s - advanceTime: 10, // default 10s - resetKeyCode: 82, // default: R - slowerKeyCode: 83, // default: S - fasterKeyCode: 68, // default: D - rewindKeyCode: 90, // default: Z - advanceKeyCode: 88, // default: X + lastSpeed: 1.0, // default 1x + enabled: true, // default enabled + speeds: {}, // empty object to hold speed for each source + displayKeyCode: 86, // default: V - fastKeyCode: 71, // default: G rememberSpeed: false, // default: false + audioBoolean: false, // default: false startHidden: false, // default: false + controllerOpacity: 0.3, // default: 0.3 + keyBindings: [], blacklist: ` www.instagram.com twitter.com vine.co imgur.com - `.replace(/^\s+|\s+$/gm,'') + teams.microsoft.com + `.replace(regStrip,'') } }; - chrome.storage.local.get(tc.settings, function(storage) { - tc.settings.speed = Number(storage.speed); - tc.settings.resetSpeed = Number(storage.resetSpeed); - tc.settings.speedStep = Number(storage.speedStep); - tc.settings.fastSpeed = Number(storage.fastSpeed); - tc.settings.rewindTime = Number(storage.rewindTime); - tc.settings.advanceTime = Number(storage.advanceTime); - tc.settings.resetKeyCode = Number(storage.resetKeyCode); - tc.settings.rewindKeyCode = Number(storage.rewindKeyCode); - tc.settings.slowerKeyCode = Number(storage.slowerKeyCode); - tc.settings.fasterKeyCode = Number(storage.fasterKeyCode); - tc.settings.fastKeyCode = Number(storage.fastKeyCode); + chrome.storage.sync.get(tc.settings, function (storage) { + tc.settings.keyBindings = storage.keyBindings; // Array + if (storage.keyBindings.length == 0) // if first initialization of 0.5.3 + { + // UPDATE + tc.settings.keyBindings.push({ + action: "slower", + key: Number(storage.slowerKeyCode) || 83, + value: Number(storage.speedStep) || 0.1, + force: false, + predefined: true + }); // default S + tc.settings.keyBindings.push({ + action: "faster", + key: Number(storage.fasterKeyCode) || 68, + value: Number(storage.speedStep) || 0.1, + force: false, + predefined: true + }); // default: D + tc.settings.keyBindings.push({ + action: "rewind", + key: Number(storage.rewindKeyCode) || 90, + value: Number(storage.rewindTime) || 10, + force: false, + predefined: true + }); // default: Z + tc.settings.keyBindings.push({ + action: "advance", + key: Number(storage.advanceKeyCode) || 88, + value: Number(storage.advanceTime) || 10, + force: false, + predefined: true + }); // default: X + tc.settings.keyBindings.push({ + action: "reset", + key: Number(storage.resetKeyCode) || 82, + value: 1.0, + force: false, + predefined: true + }); // default: R + tc.settings.keyBindings.push({ + action: "fast", + key: Number(storage.fastKeyCode) || 71, + value: Number(storage.fastSpeed) || 1.8, + force: false, + predefined: true + }); // default: G + tc.settings.version = "0.5.3"; + + chrome.storage.sync.set({ + keyBindings: tc.settings.keyBindings, + version: tc.settings.version, + displayKeyCode: tc.settings.displayKeyCode, + rememberSpeed: tc.settings.rememberSpeed, + audioBoolean: tc.settings.audioBoolean, + startHidden: tc.settings.startHidden, + enabled: tc.settings.enabled, + controllerOpacity: tc.settings.controllerOpacity, + blacklist: tc.settings.blacklist.replace(regStrip, '') + }); + } + tc.settings.lastSpeed = Number(storage.lastSpeed); tc.settings.displayKeyCode = Number(storage.displayKeyCode); - tc.settings.advanceKeyCode = Number(storage.advanceKeyCode); tc.settings.rememberSpeed = Boolean(storage.rememberSpeed); + tc.settings.audioBoolean = Boolean(storage.audioBoolean); + tc.settings.enabled = Boolean(storage.enabled); tc.settings.startHidden = Boolean(storage.startHidden); + tc.settings.controllerOpacity = Number(storage.controllerOpacity); tc.settings.blacklist = String(storage.blacklist); + // ensure that there is a "display" binding (for upgrades from versions that had it as a separate binding) + if (tc.settings.keyBindings.filter(x => x.action == "display").length == 0) { + tc.settings.keyBindings.push({ + action: "display", + key: Number(storage.displayKeyCode) || 86, + value: 0, + force: false, + predefined: true + }); // default V + } + initializeWhenReady(document); }); var forEach = Array.prototype.forEach; + function getKeyBindings(action, what = "value") { + try { + return tc.settings.keyBindings.find(item => item.action === action)[what]; + } catch (e) { + return false; + } + } + + function setKeyBindings(action, value) { + tc.settings.keyBindings.find(item => item.action === action)["value"] = value; + } + function defineVideoController() { tc.videoController = function(target, parent) { if (target.dataset['vscid']) { - return; + return target.vsc; } this.video = target; this.parent = target.parentElement || parent; this.document = target.ownerDocument; this.id = Math.random().toString(36).substr(2, 9); + + // settings.speeds[] ensures that same source used across video tags (e.g. fullscreen on YT) retains speed setting + // this.speed is a controller level variable that retains speed setting across source switches (e.g. video quality, playlist change) + this.speed = 1.0; + if (!tc.settings.rememberSpeed) { - tc.settings.speed = 1.0; - tc.settings.resetSpeed = tc.settings.fastSpeed; + if (!tc.settings.speeds[target.currentSrc]) { + tc.settings.speeds[target.currentSrc] = this.speed; + } + setKeyBindings("reset", getKeyBindings("fast")); // resetSpeed = fastSpeed + } else { + tc.settings.speeds[target.currentSrc] = tc.settings.lastSpeed; } - this.initializeControls(); - target.addEventListener('play', function(event) { - target.playbackRate = tc.settings.speed; - }); + target.playbackRate = tc.settings.speeds[target.currentSrc]; - target.addEventListener('ratechange', function(event) { + this.div=this.initializeControls(); + + target.addEventListener('play', this.handlePlay = function(event) { + if (!tc.settings.rememberSpeed) { + if (!tc.settings.speeds[target.currentSrc]) { + tc.settings.speeds[target.currentSrc] = this.speed; + } + setKeyBindings("reset", getKeyBindings("fast")); // resetSpeed = fastSpeed + } else { + tc.settings.speeds[target.currentSrc] = tc.settings.lastSpeed; + } + target.playbackRate = tc.settings.speeds[target.currentSrc]; + }.bind(this)); + + target.addEventListener('ratechange', this.handleRatechange = function(event) { // Ignore ratechange events on unitialized videos. // 0 == No information is available about the media resource. if (event.target.readyState > 0) { var speed = this.getSpeed(); this.speedIndicator.textContent = speed; - tc.settings.speed = speed; - chrome.storage.local.set({'speed': speed}, function() { + tc.settings.speeds[this.video.currentSrc] = speed; + tc.settings.lastSpeed = speed; + this.speed = speed; + chrome.storage.sync.set({'lastSpeed': speed}, function() { console.log('Speed setting saved: ' + speed); }); + // show the controller for 1000ms if it's hidden. + runAction('blink', document, null, null); } }.bind(this)); - target.playbackRate = tc.settings.speed; + var observer=new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && (mutation.attributeName === 'src' || mutation.attributeName === 'currentSrc')){ + var controller = document.querySelector(`div[data-vscid="${this.id}"]`); + if(!controller){ + return; + } + if (!mutation.target.src && !mutation.target.currentSrc) { + controller.classList.add('vsc-nosource'); + } else { + controller.classList.remove('vsc-nosource'); + } + } + }); + }); + observer.observe(target, { + attributeFilter: ["src", "currentSrc"] + }); }; tc.videoController.prototype.getSpeed = function() { @@ -89,90 +202,56 @@ chrome.runtime.sendMessage({}, function(response) { } tc.videoController.prototype.remove = function() { - this.parentElement.removeChild(this); + this.div.remove(); + this.video.removeEventListener('play',this.handlePlay); + this.video.removeEventListener('ratechange',this.handleRatechange); + delete this.video.dataset['vscid']; + delete this.video.vsc; } tc.videoController.prototype.initializeControls = function() { var document = this.document; - var speed = parseFloat(tc.settings.speed).toFixed(2), + var speed = parseFloat(tc.settings.speeds[this.video.currentSrc]).toFixed(2), top = Math.max(this.video.offsetTop, 0) + "px", left = Math.max(this.video.offsetLeft, 0) + "px"; - var prevent = function(e) { - e.preventDefault(); - e.stopPropagation(); - } - var wrapper = document.createElement('div'); wrapper.classList.add('vsc-controller'); wrapper.dataset['vscid'] = this.id; - wrapper.addEventListener('dblclick', prevent, false); - wrapper.addEventListener('mousedown', prevent, false); - wrapper.addEventListener('click', prevent, false); + + if (!this.video.currentSrc) { + wrapper.classList.add('vsc-nosource'); + } if (tc.settings.startHidden) { wrapper.classList.add('vsc-hidden'); } - var styleElem = document.createElement('style') - var shadowCSS = chrome.runtime.getURL('shadow.css') - var textElem = document.createTextNode(`@import "${shadowCSS}";`) - styleElem.appendChild(textElem) - wrapper.appendChild(styleElem) - - var divElem = document.createElement('div') - divElem.setAttribute('id', 'controller') - divElem.setAttribute('style', `top:${top}; left:${left};`) - - var spanElem1 = document.createElement('span') - spanElem1.setAttribute('data-action', 'drag') - spanElem1.setAttribute('class', 'draggable') - spanElem1.appendChild(document.createTextNode(speed.toString())) - divElem.appendChild(spanElem1) - - var spanElem2 = document.createElement('span') - spanElem2.setAttribute('id', 'controls') - - var buttonElem1 = document.createElement('button') - buttonElem1.setAttribute('data-action', 'rewind') - buttonElem1.setAttribute('class', 'rw') - buttonElem1.appendChild(document.createTextNode('«')) - spanElem2.appendChild(buttonElem1) - - var buttonElem2 = document.createElement('button') - buttonElem2.setAttribute('data-action', 'slower') - buttonElem2.appendChild(document.createTextNode('-')) - spanElem2.appendChild(buttonElem2) - - var buttonElem3 = document.createElement('button') - buttonElem3.setAttribute('data-action', 'faster') - buttonElem3.appendChild(document.createTextNode('+')) - spanElem2.appendChild(buttonElem3) - - var buttonElem4 = document.createElement('button') - buttonElem4.setAttribute('data-action', 'advance') - buttonElem4.setAttribute('class', 'rw') - buttonElem4.appendChild(document.createTextNode('»')) - spanElem2.appendChild(buttonElem4) - - var buttonElem5 = document.createElement('button') - buttonElem5.setAttribute('data-action', 'display') - buttonElem5.setAttribute('class', 'hideButton') - buttonElem5.appendChild(document.createTextNode('x')) - spanElem2.appendChild(buttonElem5) - - divElem.appendChild(spanElem2) - wrapper.appendChild(divElem) - - var shadow = wrapper + var shadow = wrapper.attachShadow({ mode: 'open' }); + var shadowTemplate = ` + +
+ ${speed} + + + + + + + +
+ `; + shadow.innerHTML = shadowTemplate; shadow.querySelector('.draggable').addEventListener('mousedown', (e) => { runAction(e.target.dataset['action'], document, false, e); }); forEach.call(shadow.querySelectorAll('button'), function(button) { button.onclick = (e) => { - runAction(e.target.dataset['action'], document, false, e); + runAction(e.target.dataset['action'], document, getKeyBindings(e.target.dataset['action']), e); } }); @@ -180,11 +259,11 @@ chrome.runtime.sendMessage({}, function(response) { var fragment = document.createDocumentFragment(); fragment.appendChild(wrapper); - this.video.classList.add('vsc-initialized'); this.video.dataset['vscid'] = this.id; switch (true) { case (location.hostname == 'www.amazon.com'): + case (location.hostname == 'www.reddit.com'): case (/hbogo\./).test(location.hostname): // insert before parent to bypass overlay this.parent.parentElement.insertBefore(fragment, this.parent); @@ -196,6 +275,7 @@ chrome.runtime.sendMessage({}, function(response) { // the first element of the target, which may not be the parent. this.parent.insertBefore(fragment, this.parent.firstChild); } + return wrapper; } } @@ -207,12 +287,21 @@ chrome.runtime.sendMessage({}, function(response) { var blacklisted = false; tc.settings.blacklist.split("\n").forEach(match => { - match = match.replace(/^\s+|\s+$/g,'') + match = match.replace(regStrip,'') if (match.length == 0) { return; } - var regexp = new RegExp(escapeStringRegExp(match)); + if (match.startsWith('/')) { + try { + var regexp = new RegExp(match); + } catch(err) { + return; + } + } else { + var regexp = new RegExp(escapeStringRegExp(match)); + } + if (regexp.test(location.href)) { blacklisted = true; return; @@ -222,10 +311,9 @@ chrome.runtime.sendMessage({}, function(response) { if (blacklisted) return; - window.addEventListener('load', function () { - initializeNow(window.document); - }, false); - + window.onload = () => { + initializeNow(window.document) + }; if (document) { if (document.readyState === "complete") { initializeNow(document); @@ -238,8 +326,15 @@ chrome.runtime.sendMessage({}, function(response) { } } } - + function inIframe () { + try { + return window.self !== window.top; + } catch (e) { + return true; + } + } function initializeNow(document) { + if (!tc.settings.enabled) return; // enforce init-once due to redundant callers if (!document.body || document.body.classList.contains('vsc-initialized')) { return; @@ -255,61 +350,66 @@ chrome.runtime.sendMessage({}, function(response) { link.rel = 'stylesheet'; document.head.appendChild(link); } + var docs = Array(document) + try { + if (inIframe()) + docs.push(window.top.document); + } catch (e) { + } - document.addEventListener('keydown', function(event) { - var keyCode = event.keyCode; + docs.forEach(function(doc) { + doc.addEventListener('keydown', function(event) { + var keyCode = event.keyCode; - // Ignore if following modifier is active. - if (!event.getModifierState - || event.getModifierState("Alt") - || event.getModifierState("Control") - || event.getModifierState("Fn") - || event.getModifierState("Meta") - || event.getModifierState("Hyper") - || event.getModifierState("OS")) { - return; + // Ignore if following modifier is active. + if (!event.getModifierState + || event.getModifierState("Alt") + || event.getModifierState("Control") + || event.getModifierState("Fn") + || event.getModifierState("Meta") + || event.getModifierState("Hyper") + || event.getModifierState("OS")) { + return; + } + + // Ignore keydown event if typing in an input box + if (event.target.nodeName === 'INPUT' + || event.target.nodeName === 'TEXTAREA' + || event.target.isContentEditable) { + return false; + } + + // Ignore keydown event if typing in a page without vsc + if (!document.querySelector(".vsc-controller")) { + return false; + } + + var item = tc.settings.keyBindings.find(item => item.key === keyCode); + if (item) { + runAction(item.action, document, item.value); + if (item.force === "true") {// disable websites key bindings + event.preventDefault(); + event.stopPropagation(); + } } - // Ignore keydown event if typing in an input box - if ((document.activeElement.nodeName === 'INPUT' - && document.activeElement.getAttribute('type') === 'text') - || document.activeElement.nodeName === 'TEXTAREA' - || document.activeElement.isContentEditable) { return false; - } + }, true); + }); - if (keyCode == tc.settings.rewindKeyCode) { - runAction('rewind', document, true) - } else if (keyCode == tc.settings.advanceKeyCode) { - runAction('advance', document, true) - } else if (keyCode == tc.settings.fasterKeyCode) { - runAction('faster', document, true) - } else if (keyCode == tc.settings.slowerKeyCode) { - runAction('slower', document, true) - } else if (keyCode == tc.settings.resetKeyCode) { - runAction('reset', document, true) - } else if (keyCode == tc.settings.displayKeyCode) { - runAction('display', document, true) - } else if (keyCode == tc.settings.fastKeyCode) { - runAction('fast', document, true); - } - - return false; - }, true); function checkForVideo(node, parent, added) { - if (node.nodeName === 'VIDEO') { + // Only proceed with supposed removal if node is missing from DOM + if (!added && document.body.contains(node)) { + return; + } + if (node.nodeName === 'VIDEO' || (node.nodeName === 'AUDIO' && tc.settings.audioBoolean)) { if (added) { - new tc.videoController(node, parent); + node.vsc = new tc.videoController(node, parent); } else { - if (node.classList.contains('vsc-initialized')) { - let id = node.dataset['vscid']; - let ctrl = document.querySelector(`div[data-vscid="${id}"]`) - if (ctrl) { - ctrl.remove(); - } - node.classList.remove('vsc-initialized'); - delete node.dataset['vscid']; + let id = node.dataset['vscid']; + if (id) { + node.vsc.remove(); } } } else if (node.children != undefined) { @@ -321,7 +421,8 @@ chrome.runtime.sendMessage({}, function(response) { } var observer = new MutationObserver(function(mutations) { - function mutationObserverHandler() { + // Process the DOM nodes lazily + requestIdleCallback(_ => { mutations.forEach(function(mutation) { forEach.call(mutation.addedNodes, function(node) { if (typeof node === "function") @@ -334,19 +435,18 @@ chrome.runtime.sendMessage({}, function(response) { checkForVideo(node, node.parentNode || mutation.target, false); }); }); - } - // Process the DOM nodes lazily - if ('requestIdleCallback' in window) { - requestIdleCallback(mutationObserverHandler, {timeout: 1000}); - } else { - mutationObserverHandler(); - } + }, {timeout: 1000}); }); observer.observe(document, { childList: true, subtree: true }); - var videoTags = document.getElementsByTagName('video'); - forEach.call(videoTags, function(video) { - new tc.videoController(video); + if (tc.settings.audioBoolean) { + var mediaTags = document.querySelectorAll('video,audio'); + } else { + var mediaTags = document.querySelectorAll('video'); + } + + forEach.call(mediaTags, function(video) { + video.vsc = new tc.videoController(video); }); var frameTags = document.getElementsByTagName('iframe'); @@ -357,68 +457,122 @@ chrome.runtime.sendMessage({}, function(response) { }); } - function runAction(action, document, keyboard, e) { - var videoTags = document.getElementsByTagName('video'); - videoTags.forEach = Array.prototype.forEach; + function runAction(action, document, value, e) { + if (tc.settings.audioBoolean) { + var mediaTags = document.querySelectorAll('video,audio'); + } else { + var mediaTags = document.querySelectorAll('video'); + } - videoTags.forEach(function(v) { + mediaTags.forEach = Array.prototype.forEach; + + // Get the controller that was used if called from a button press event e + if (e) { + var targetController = e.target.getRootNode().host; + } + + mediaTags.forEach(function(v) { var id = v.dataset['vscid']; var controller = document.querySelector(`div[data-vscid="${id}"]`); - showController(controller); + // Don't change video speed if the video has a different controller + if (e && !(targetController == controller)) { + return; + } + + // Controller may have been (force) removed by the site, guard to prevent crashes but run the command + if (controller) { + showController(controller); + } if (!v.classList.contains('vsc-cancelled')) { if (action === 'rewind') { - v.currentTime -= tc.settings.rewindTime; + v.currentTime -= value; } else if (action === 'advance') { - v.currentTime += tc.settings.advanceTime; + v.currentTime += value; } else if (action === 'faster') { // Maximum playback speed in Chrome is set to 16: - // https://cs.chromium.org/chromium/src/third_party/WebKit/Source/core/html/media/HTMLMediaElement.cpp?l=168 - var s = Math.min( (v.playbackRate < 0.1 ? 0.0 : v.playbackRate) + tc.settings.speedStep, 16); + // https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/html/media/html_media_element.cc?gsn=kMinRate&l=166 + var s = Math.min((v.playbackRate < 0.1 ? 0.0 : v.playbackRate) + value, 16); v.playbackRate = Number(s.toFixed(2)); } else if (action === 'slower') { // Video min rate is 0.0625: - // https://cs.chromium.org/chromium/src/third_party/WebKit/Source/core/html/media/HTMLMediaElement.cpp?l=167 - var s = Math.max(v.playbackRate - tc.settings.speedStep, 0.07); + // https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/html/media/html_media_element.cc?gsn=kMinRate&l=165 + var s = Math.max(v.playbackRate - value, 0.07); v.playbackRate = Number(s.toFixed(2)); } else if (action === 'reset') { resetSpeed(v, 1.0); } else if (action === 'display') { controller.classList.add('vsc-manual'); controller.classList.toggle('vsc-hidden'); + } else if (action === 'blink') { + // if vsc is hidden, show it briefly to give the use visual feedback that the action is excuted. + if(controller.classList.contains('vsc-hidden') || controller.blinkTimeOut !== undefined){ + clearTimeout(controller.blinkTimeOut); + controller.classList.remove('vsc-hidden'); + controller.blinkTimeOut = setTimeout(()=>{ + controller.classList.add('vsc-hidden'); + controller.blinkTimeOut = undefined; + }, value ? value : 1000); + } } else if (action === 'drag') { handleDrag(v, controller, e); } else if (action === 'fast') { - resetSpeed(v, tc.settings.fastSpeed); + resetSpeed(v, value); + } else if (action === 'pause') { + pause(v); + } else if (action === 'muted') { + muted(v, value); + } else if (action === 'mark') { + setMark(v); + } else if (action === 'jump') { + jumpToMark(v); } } }); } + function pause(v) { + if (v.paused) { + v.play(); + } else { + v.pause(); + } + } + function resetSpeed(v, target) { if (v.playbackRate === target) { - if(v.playbackRate === tc.settings.resetSpeed) - { + if (v.playbackRate === getKeyBindings("reset")) { // resetSpeed if (target !== 1.0) { v.playbackRate = 1.0; } else { - v.playbackRate = tc.settings.fastSpeed; + v.playbackRate = getKeyBindings("fast"); // fastSpeed } - } - else - { - v.playbackRate = tc.settings.resetSpeed; + } else { + v.playbackRate = getKeyBindings("reset"); // resetSpeed } } else { - tc.settings.resetSpeed = v.playbackRate; - chrome.storage.local.set({'resetSpeed': v.playbackRate}); + setKeyBindings("reset", v.playbackRate); // resetSpeed v.playbackRate = target; } } + function muted(v, value) { + v.muted = v.muted !== true; + } + + function setMark(v) { + v.vsc.mark = v.currentTime; + } + + function jumpToMark(v) { + if (v.vsc.mark && typeof v.vsc.mark === "number") { + v.currentTime = v.vsc.mark; + } + } + function handleDrag(video, controller, e) { - const shadowController = controller.querySelector('#controller'); + const shadowController = controller.shadowRoot.querySelector('#controller'); // Find nearest parent of same size as video parent. var parentElement = controller.parentElement; @@ -473,4 +627,3 @@ chrome.runtime.sendMessage({}, function(response) { animation = false; }, 2000); } -}); diff --git a/manifest.json b/manifest.json index 9528c52..8de56a5 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "Video Speed Controller", "short_name": "videospeed", - "version": "0.5.1", + "version": "0.5.9", "manifest_version": 2, "description": "Speed up, slow down, advance and rewind any HTML5 video with quick shortcuts.", "homepage_url": "https://github.com/codebicycle/videospeed", @@ -26,9 +26,10 @@ "content_scripts": [{ "all_frames": true, "matches": [ "http://*/*", "https://*/*", "file:///*" ], + "match_about_blank": true, "exclude_matches": [ "https://plus.google.com/hangouts/*", - "https://hangouts.google.com/hangouts/*", + "https://hangouts.google.com/*", "https://meet.google.com/*", "https://teamtreehouse.com/*", "http://www.hitbox.tv/*" diff --git a/options.css b/options.css index 8a75b36..f77a9a0 100644 --- a/options.css +++ b/options.css @@ -83,10 +83,6 @@ label { width: 170px; } -label[for=rememberSpeed] { - width: 200px; -} - #status { color: #9D9D9D; display: inline-block; @@ -96,3 +92,17 @@ label[for=rememberSpeed] { #faq { margin-top: 2em; } + +select { + width: 170px; +} + +.customForce { + display: none; + width: 250px; +} + +.customKey { + color: transparent; + text-shadow: 0 0 0 #000000; +} \ No newline at end of file diff --git a/options.html b/options.html index a01c69d..2ee0994 100644 --- a/options.html +++ b/options.html @@ -1,81 +1,127 @@ - - Video Speed Controller: Options - - - - -
-

Video Speed Controller

-
+ + Video Speed Controller: Options + + + + +
+

Video Speed Controller

+
-
+

Shortcuts

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

Other

-
- - -
-
- - +

Other

+
+ +
- - + +
- - + +
- - + +
- - + + +
+
+ +
- + +
diff --git a/options.js b/options.js index d3fed38..8bc0719 100644 --- a/options.js +++ b/options.js @@ -1,32 +1,46 @@ +var regStrip=/^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm; + var tcDefaults = { - speed: 1.0, // default 1x - speedStep: 0.1, // default 0.1x - rewindTime: 10, // default 10s - advanceTime: 10, // default 10s - fastSpeed: 1.8, // default 1.8x - resetKeyCode: 82, // default: R - slowerKeyCode: 83, // default: S - fasterKeyCode: 68, // default: D - fastKeyCode: 71, // default: G - rewindKeyCode: 90, // default: Z - advanceKeyCode: 88, // default: X + speed: 1.0, // default: displayKeyCode: 86, // default: V rememberSpeed: false, // default: false + audioBoolean: false, // default: false startHidden: false, // default: false + enabled: true, // default enabled + controllerOpacity: 0.3, // default: 0.3 + keyBindings: [ + {action: "display", key: 86, value: 0, force: false, predefined: true }, // V + {action: "slower", key: 83, value: 0.1, force: false, predefined: true}, // S + {action: "faster", key: 68, value: 0.1, force: false, predefined: true}, // D + {action: "rewind", key: 90, value: 10, force: false, predefined: true}, // Z + {action: "advance", key: 88, value: 10, force: false, predefined: true}, // X + {action: "reset", key: 82, value: 1, force: false, predefined: true}, // R + {action: "fast", key: 71, value: 1.8, force: false, predefined: true} // G + ], blacklist: ` www.instagram.com twitter.com vine.co imgur.com - `.replace(/^\s+|\s+$/gm,'') + teams.microsoft.com + `.replace(regStrip, '') }; +var keyBindings = []; + var keyCodeAliases = { - 32: 'Space', - 96: 'Num 0', - 97: 'Num 1', - 98: 'Num 2', - 99: 'Num 3', + 0: 'null', + null: 'null', + undefined: 'null', + 32: 'Space', + 37: 'Left', + 38: 'Up', + 39: 'Right', + 40: 'Down', + 96: 'Num 0', + 97: 'Num 1', + 98: 'Num 2', + 99: 'Num 3', 100: 'Num 4', 101: 'Num 5', 102: 'Num 6', @@ -38,6 +52,18 @@ var keyCodeAliases = { 109: 'Num -', 110: 'Num .', 111: 'Num /', + 112: 'F1', + 113: 'F2', + 114: 'F3', + 115: 'F4', + 116: 'F5', + 117: 'F6', + 118: 'F7', + 119: 'F8', + 120: 'F9', + 121: 'F10', + 122: 'F11', + 123: 'F12', 186: ';', 188: '<', 189: '-', @@ -49,14 +75,8 @@ var keyCodeAliases = { 220: '\\', 221: ']', 222: '\'', - 59: ';', - 173: '-', - 61: '+', } -var whiteList = ['Backspace', 'Delete', 'Tab', 'Escape', 'Enter', 'Home', 'End', - 'ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'] - function recordKeyPress(e) { if ( (e.keyCode >= 48 && e.keyCode <= 57) // Numbers 0-9 @@ -70,14 +90,14 @@ function recordKeyPress(e) { e.stopPropagation(); } else if (e.keyCode === 8) { // Clear input when backspace pressed e.target.value = ''; + } else if (e.keyCode === 27) { // When esc clicked, clear input + e.target.value = 'null'; + e.target.keyCode = null; } }; function inputFilterNumbersOnly(e) { - var char = e.key; - if (whiteList.includes(char)) { - return - } + var char = String.fromCharCode(e.keyCode); if (!/[\d\.]$/.test(char) || !/^\d+(\.\d*)?$/.test(e.target.value + char)) { e.preventDefault(); e.stopPropagation(); @@ -97,51 +117,96 @@ function updateShortcutInputText(inputId, keyCode) { document.getElementById(inputId).keyCode = keyCode; } +function updateCustomShortcutInputText(inputItem, keyCode) { + inputItem.value = keyCodeAliases[keyCode] || String.fromCharCode(keyCode); + inputItem.keyCode = keyCode; +} + +// List of custom actions for which customValue should be disabled +var customActionsNoValues=["pause","muted","mark","jump","display"]; + +function add_shortcut() { + var html = ` + + + + `; + var div = document.createElement('div'); + div.setAttribute('class', 'row customs'); + div.innerHTML = html; + var customs_element = document.getElementById("customs"); + customs_element.insertBefore(div, customs_element.children[customs_element.childElementCount - 1]); +} + +function createKeyBindings(item) { + const action = item.querySelector(".customDo").value; + const key = item.querySelector(".customKey").keyCode; + const value = Number(item.querySelector(".customValue").value); + const force = item.querySelector(".customForce").value; + const predefined = !!item.id;//item.id ? true : false; + + keyBindings.push({action: action, key: key, value: value, force: force, predefined: predefined}); +} + +// Validates settings before saving +function validate() { + var valid = true; + var status = document.getElementById('status'); + document.getElementById('blacklist').value.split("\n").forEach(match => { + match = match.replace(regStrip,'') + if (match.startsWith('/')) { + try { + var regexp = new RegExp(match); + } catch(err) { + status.textContent = 'Error: Invalid Regex: ' + match + + '. Unable to save'; + valid = false; + return; + } + } + }) + return valid; +} + // Saves options to chrome.storage function save_options() { + if (validate() === false) { + return; + } + keyBindings = []; + Array.from(document.querySelectorAll(".customs")).forEach(item => createKeyBindings(item)); // Remove added shortcuts - var speedStep = document.getElementById('speedStep').value; - var rewindTime = document.getElementById('rewindTime').value; - var advanceTime = document.getElementById('advanceTime').value; - var fastSpeed = document.getElementById('fastSpeed').value; - var resetKeyCode = document.getElementById('resetKeyInput').keyCode; - var rewindKeyCode = document.getElementById('rewindKeyInput').keyCode; - var advanceKeyCode = document.getElementById('advanceKeyInput').keyCode; - var slowerKeyCode = document.getElementById('slowerKeyInput').keyCode; - var fasterKeyCode = document.getElementById('fasterKeyInput').keyCode; - var fastKeyCode = document.getElementById('fastKeyInput').keyCode; - var displayKeyCode = document.getElementById('displayKeyInput').keyCode; var rememberSpeed = document.getElementById('rememberSpeed').checked; + var audioBoolean = document.getElementById('audioBoolean').checked; + var enabled = document.getElementById('enabled').checked; var startHidden = document.getElementById('startHidden').checked; + var controllerOpacity = document.getElementById('controllerOpacity').value; var blacklist = document.getElementById('blacklist').value; - speedStep = isNaN(speedStep) ? tcDefaults.speedStep : Number(speedStep); - rewindTime = isNaN(rewindTime) ? tcDefaults.rewindTime : Number(rewindTime); - advanceTime = isNaN(advanceTime) ? tcDefaults.advanceTime : Number(advanceTime); - fastSpeed = isNaN(fastSpeed) ? tcDefaults.fastSpeed : Number(fastSpeed); - resetKeyCode = isNaN(resetKeyCode) ? tcDefaults.resetKeyCode : resetKeyCode; - rewindKeyCode = isNaN(rewindKeyCode) ? tcDefaults.rewindKeyCode : rewindKeyCode; - advanceKeyCode = isNaN(advanceKeyCode) ? tcDefaults.advanceKeyCode : advanceKeyCode; - slowerKeyCode = isNaN(slowerKeyCode) ? tcDefaults.slowerKeyCode : slowerKeyCode; - fasterKeyCode = isNaN(fasterKeyCode) ? tcDefaults.fasterKeyCode : fasterKeyCode; - fastKeyCode = isNaN(fastKeyCode) ? tcDefaults.fastKeyCode : fastKeyCode; - displayKeyCode = isNaN(displayKeyCode) ? tcDefaults.displayKeyCode : displayKeyCode; - - chrome.storage.local.set({ - speedStep: speedStep, - rewindTime: rewindTime, - advanceTime: advanceTime, - fastSpeed: fastSpeed, - resetKeyCode: resetKeyCode, - rewindKeyCode: rewindKeyCode, - advanceKeyCode: advanceKeyCode, - slowerKeyCode: slowerKeyCode, - fasterKeyCode: fasterKeyCode, - fastKeyCode: fastKeyCode, - displayKeyCode: displayKeyCode, + chrome.storage.sync.remove(["resetSpeed", "speedStep", "fastSpeed", "rewindTime", "advanceTime", "resetKeyCode", "slowerKeyCode", "fasterKeyCode", "rewindKeyCode", "advanceKeyCode", "fastKeyCode"]); + chrome.storage.sync.set({ rememberSpeed: rememberSpeed, + audioBoolean: audioBoolean, + enabled: enabled, startHidden: startHidden, - blacklist: blacklist.replace(/^\s+|\s+$/gm,'') + controllerOpacity: controllerOpacity, + keyBindings: keyBindings, + blacklist: blacklist.replace(regStrip,'') }, function() { // Update status to let user know options were saved. var status = document.getElementById('status'); @@ -154,27 +219,56 @@ function save_options() { // Restores options from chrome.storage function restore_options() { - chrome.storage.local.get(tcDefaults, function(storage) { - document.getElementById('speedStep').value = storage.speedStep.toFixed(2); - document.getElementById('rewindTime').value = storage.rewindTime; - document.getElementById('advanceTime').value = storage.advanceTime; - document.getElementById('fastSpeed').value = storage.fastSpeed; - updateShortcutInputText('resetKeyInput', storage.resetKeyCode); - updateShortcutInputText('rewindKeyInput', storage.rewindKeyCode); - updateShortcutInputText('advanceKeyInput', storage.advanceKeyCode); - updateShortcutInputText('slowerKeyInput', storage.slowerKeyCode); - updateShortcutInputText('fasterKeyInput', storage.fasterKeyCode); - updateShortcutInputText('fastKeyInput', storage.fastKeyCode); - updateShortcutInputText('displayKeyInput', storage.displayKeyCode); + chrome.storage.sync.get(tcDefaults, function(storage) { document.getElementById('rememberSpeed').checked = storage.rememberSpeed; + document.getElementById('audioBoolean').checked = storage.audioBoolean; + document.getElementById('enabled').checked = storage.enabled; document.getElementById('startHidden').checked = storage.startHidden; + document.getElementById('controllerOpacity').value = storage.controllerOpacity; document.getElementById('blacklist').value = storage.blacklist; + + // ensure that there is a "display" binding for upgrades from versions that had it as a separate binding + if(storage.keyBindings.filter(x => x.action == "display").length == 0){ + storage.keyBindings.push({ action: "display", value: 0, force: false, predefined: true }); + } + + for (let i in storage.keyBindings) { + var item = storage.keyBindings[i]; + if (item.predefined) { + //do predefined ones because their value needed for overlay + // document.querySelector("#" + item["action"] + " .customDo").value = item["action"]; + if (item["action"] == "display" && typeof (item["key"]) === "undefined"){ + item["key"] = storage.displayKeyCode || tcDefaults.displayKeyCode; // V + } + + if (customActionsNoValues.includes(item["action"])) + document.querySelector("#" + item["action"] + " .customValue").disabled = true; + + updateCustomShortcutInputText(document.querySelector("#" + item["action"] + " .customKey"), item["key"]); + document.querySelector("#" + item["action"] + " .customValue").value = item["value"]; + document.querySelector("#" + item["action"] + " .customForce").value = item["force"]; + } + else { + // new ones + add_shortcut(); + const dom = document.querySelector(".customs:last-of-type") + dom.querySelector(".customDo").value = item["action"]; + + if (customActionsNoValues.includes(item["action"])) + dom.querySelector(".customValue").disabled = true; + + updateCustomShortcutInputText(dom.querySelector(".customKey"), item["key"]); + dom.querySelector(".customValue").value = item["value"]; + dom.querySelector(".customForce").value = item["force"]; + } + } }); } function restore_defaults() { - chrome.storage.local.set(tcDefaults, function() { + chrome.storage.sync.set(tcDefaults, function() { restore_options(); + document.querySelectorAll(".removeParent").forEach(button => button.click()); // Remove added shortcuts // Update status to let user know options were saved. var status = document.getElementById('status'); status.textContent = 'Default options restored'; @@ -184,28 +278,50 @@ function restore_defaults() { }); } -function initShortcutInput(inputId) { - document.getElementById(inputId).addEventListener('focus', inputFocus); - document.getElementById(inputId).addEventListener('blur', inputBlur); - document.getElementById(inputId).addEventListener('keydown', recordKeyPress); +function show_experimental() { + document.querySelectorAll(".customForce").forEach(item => item.style.display = 'inline-block'); } document.addEventListener('DOMContentLoaded', function () { restore_options(); document.getElementById('save').addEventListener('click', save_options); + document.getElementById('add').addEventListener('click', add_shortcut); document.getElementById('restore').addEventListener('click', restore_defaults); + document.getElementById('experimental').addEventListener('click', show_experimental); - initShortcutInput('resetKeyInput'); - initShortcutInput('rewindKeyInput'); - initShortcutInput('advanceKeyInput'); - initShortcutInput('slowerKeyInput'); - initShortcutInput('fasterKeyInput'); - initShortcutInput('fastKeyInput'); - initShortcutInput('displayKeyInput'); + function eventCaller(event, className, funcName) { + if (!event.target.classList.contains(className)) { + return + } + funcName(event); + } - document.getElementById('rewindTime').addEventListener('keypress', inputFilterNumbersOnly); - document.getElementById('advanceTime').addEventListener('keypress', inputFilterNumbersOnly); - document.getElementById('speedStep').addEventListener('keypress', inputFilterNumbersOnly); - document.getElementById('fastSpeed').addEventListener('keypress', inputFilterNumbersOnly); + document.addEventListener('keypress', (event) => { + eventCaller(event, "customValue", inputFilterNumbersOnly) + }); + document.addEventListener('focus', (event) => { + eventCaller(event, "customKey", inputFocus) + }); + document.addEventListener('blur', (event) => { + eventCaller(event, "customKey", inputBlur) + }); + document.addEventListener('keydown', (event) => { + eventCaller(event, "customKey", recordKeyPress) + }); + document.addEventListener('click', (event) => { + eventCaller(event, "removeParent", function () { + event.target.parentNode.remove() + }) + }); + document.addEventListener('change', (event) => { + eventCaller(event, "customDo", function () { + if (customActionsNoValues.includes(event.target.value)) { + event.target.nextElementSibling.nextElementSibling.disabled = true; + event.target.nextElementSibling.nextElementSibling.value = 0; + } else { + event.target.nextElementSibling.nextElementSibling.disabled = false; + } + }) + }); }) diff --git a/popup.css b/popup.css index 74bfe99..f8b9a92 100644 --- a/popup.css +++ b/popup.css @@ -27,3 +27,7 @@ button { font-size: 0.95em; margin: 0.15em 0; } + +.hide { + display: none; +} diff --git a/popup.html b/popup.html index 9fd9ae0..51536d5 100644 --- a/popup.html +++ b/popup.html @@ -6,6 +6,10 @@ + + + +

diff --git a/popup.js b/popup.js index d9e4448..995f78e 100644 --- a/popup.js +++ b/popup.js @@ -10,4 +10,49 @@ document.addEventListener('DOMContentLoaded', function () { document.querySelector('#feedback').addEventListener('click', function() { window.open("https://github.com/codebicycle/videospeed/issues"); }); + + document.querySelector('#enable').addEventListener('click', function() { + toggleEnabled(true, settingsSavedReloadMessage); + }); + + document.querySelector('#disable').addEventListener('click', function() { + toggleEnabled(false, settingsSavedReloadMessage); + }); + + chrome.storage.sync.get({enabled: true}, function(storage) { + toggleEnabledUI(storage.enabled); + }); + + function toggleEnabled(enabled, callback){ + chrome.storage.sync.set({ + enabled: enabled, + }, function() { + toggleEnabledUI(enabled); + if(callback) callback(enabled); + }); + } + + function toggleEnabledUI(enabled){ + document.querySelector('#enable').classList.toggle("hide", enabled); + document.querySelector('#disable').classList.toggle("hide", !enabled); + + const suffix = `${(enabled ? "" : "_disabled")}.png` + chrome.browserAction.setIcon({ + "path": { + "19": "icons/icon19" + suffix, + "38": "icons/icon38" + suffix, + "48": "icons/icon48" + suffix + } + }); + } + + function settingsSavedReloadMessage(enabled){ + setStatusMessage(`${enabled ? "Enabled" : "Disabled"}. Reload page to see changes`); + } + + function setStatusMessage(str){ + const status_element = document.querySelector('#status') + status_element.classList.toggle("hide", false); + status_element.innerText = str; + } }); diff --git a/shadow.css b/shadow.css index 3b51bb8..ad48e6a 100644 --- a/shadow.css +++ b/shadow.css @@ -22,7 +22,6 @@ cursor: default; z-index: 9999999; - opacity: 0.3; } .vsc-controller #controller:hover {