diff --git a/README.md b/README.md index 1671247..6600dc0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # PiPer Adds Picture in Picture functionality to Safari for Youtube, Netflix, Amazon Video, Twitch, and more! +Screenshot of PiPer in action + ## Installation Get the extension [here](https://s3.amazonaws.com/piper-extension/PiPer.safariextz), open the downloaded file, and hit trust @@ -15,6 +17,7 @@ Get the extension [here](https://s3.amazonaws.com/piper-extension/PiPer.safariex * [CollegeHumor](http://www.collegehumor.com) * [Vevo](http://www.vevo.com) * [Vid.me](http://www.vid.me) +* [Hulu](http://www.hulu.com) ## Acknowledgements * [Pied PĂ­Per](https://github.com/JoeKuhns/PiedPiPer.safariextension) for the original inspiration and the Netflix icon diff --git a/out/PiPer.safariextz b/out/PiPer.safariextz index 1fb6de5..9bfdd10 100644 Binary files a/out/PiPer.safariextz and b/out/PiPer.safariextz differ diff --git a/promo/Icon-256.png b/promo/Icon-256.png new file mode 100644 index 0000000..3e1381c Binary files /dev/null and b/promo/Icon-256.png differ diff --git a/promo/Promo-shot.png b/promo/Promo-shot.png new file mode 100644 index 0000000..fc243a6 Binary files /dev/null and b/promo/Promo-shot.png differ diff --git a/src/Info.plist b/src/Info.plist index d5bf828..5701873 100644 --- a/src/Info.plist +++ b/src/Info.plist @@ -32,37 +32,42 @@ BQ6Q24MF9X ExtensionInfoDictionaryVersion 1.0 - Update Manifest URL - https://s3.amazonaws.com/piper-extension/update.plist - Website - https://github.com/amarcu5/PiPer/ Permissions Website Access Allowed Domains + amazon.co.uk *.amazon.co.uk + amazon.com *.amazon.com + amazon.fr *.amazon.fr + amazon.de *.amazon.de + amazon.ca *.amazon.ca - *.netflix.com - netflix.com - *.youtube.com - youtube.com - *.twitch.tv - twitch.tv - *.metacafe.com - metacafe.com - *.openload.co - openload.co - *.vevo.com - vevo.com - *.collegehumor.com collegehumor.com + *.collegehumor.com + hulu.com + *.hulu.com + metacafe.com + *.metacafe.com + netflix.com + *.netflix.com + openload.co + *.openload.co + twitch.tv + *.twitch.tv + vevo.com + *.vevo.com vid.me *.vid.me + youtu.be + *.youtu.be + youtube.com + *.youtube.com Include Secure Pages @@ -70,5 +75,9 @@ Some + Update Manifest URL + https://s3.amazonaws.com/piper-extension/update.plist + Website + https://github.com/amarcu5/PiPer/ diff --git a/src/scripts/externs.js b/src/scripts/externs.js index 07017fd..60a2a39 100644 --- a/src/scripts/externs.js +++ b/src/scripts/externs.js @@ -1,5 +1,5 @@ /** @const */ -var safari = {}; +const safari = {}; /** @const */ safari.extension = {}; @@ -11,4 +11,18 @@ safari.extension.baseURI; HTMLVideoElement.prototype.webkitPresentationMode; /** @return {undefined} */ -HTMLVideoElement.prototype.webkitSetPresentationMode = function(mode) {} \ No newline at end of file +HTMLVideoElement.prototype.webkitSetPresentationMode = function(mode) {} + +/** + * @typedef {{ + * buttonClassName: (string|undefined), + * buttonDidAppear: (function(): undefined|undefined), + * buttonElementType: (string|undefined), + * buttonImage: (string|undefined), + * buttonInsertBefore: (function(Element): ?Node|undefined), + * buttonParent: function(): ?Element, + * buttonStyle: (string|undefined), + * videoElement: function(): ?Element, + * }} + */ +let PIPResource; diff --git a/src/scripts/main.js b/src/scripts/main.js index 6c4245f..ed87b96 100644 --- a/src/scripts/main.js +++ b/src/scripts/main.js @@ -1,247 +1,249 @@ 'use strict'; -/** - * @typedef {{ - * buttonImage: (string | undefined), - * buttonElementType: (string|undefined), - * buttonStyle: (string|undefined), - * buttonClassName: (string|undefined), - * buttonParent: function(): ?Element, - * buttonInsertBefore: (function(Element): ?Node|undefined), - * videoElement: function(): ?Element, - * buttonWillAppear: (function(): undefined|undefined), - * }} - */ -var PIPResource; - - /** @define {boolean} */ const COMPILED = false; function log(/** string */ message) { - !COMPILED && console.log("PIPer: " + message); + !COMPILED && console.log('PIPer: ' + message); } const BUTTON_ID = 'PIPButton'; -var /** boolean */ buttonAdded = false; -var /** ?PIPResource */ currentResource = null; +let /** boolean */ buttonAdded = false; +let /** ?PIPResource */ currentResource = null; -const addButton = function (/** Element */ parent) { - const button = document.createElement(currentResource.buttonElementType || 'button'); +const addButton = function(/** Element */ parent) { + const button = document.createElement(currentResource.buttonElementType || 'button'); - button.id = BUTTON_ID; - button.title = 'Open Picture in Picture mode'; - if (currentResource.buttonStyle) button.style.cssText = currentResource.buttonStyle; - if (currentResource.buttonClassName) button.className = currentResource.buttonClassName; + button.id = BUTTON_ID; + button.title = 'Open Picture in Picture mode'; + if (currentResource.buttonStyle) button.style.cssText = currentResource.buttonStyle; + if (currentResource.buttonClassName) button.className = currentResource.buttonClassName; - const image = document.createElement('img'); - image.src = safari.extension.baseURI + 'images/' + (currentResource.buttonImage || 'default') + '.svg'; - image.style.cssText = 'width:100%;height:100%'; + const image = document.createElement('img'); + image.src = safari.extension.baseURI + 'images/' + (currentResource.buttonImage || 'default') + '.svg'; + image.style.cssText = 'width:100%;height:100%'; - button.appendChild(image); + button.appendChild(image); - button.addEventListener('click', function (event) { - event.preventDefault(); + button.addEventListener('click', function(event) { + event.preventDefault(); - const video = /** @type {?HTMLVideoElement} */ (currentResource.videoElement()); - if (!video) return; + const video = /** @type {?HTMLVideoElement} */ (currentResource.videoElement()); + if (!video) { + log('Unable to find video'); + return; + } - video.webkitSetPresentationMode('inline' === video.webkitPresentationMode ? 'picture-in-picture' : 'inline'); - }); + const presentationMode = 'inline' === video.webkitPresentationMode ? 'picture-in-picture' : 'inline'; + video.webkitSetPresentationMode(presentationMode); + }); - parent.insertBefore(button, currentResource.buttonInsertBefore ? currentResource.buttonInsertBefore(parent) : null); + const referenceNode = currentResource.buttonInsertBefore ? currentResource.buttonInsertBefore(parent) : null; + parent.insertBefore(button, referenceNode); } -const buttonObserver = function () { +const buttonObserver = function() { - if (buttonAdded) { - if (document.getElementById(BUTTON_ID)) return; - log("Button removed"); - buttonAdded = false; - } + if (buttonAdded) { + if (document.getElementById(BUTTON_ID)) return; + log('Button removed'); + buttonAdded = false; + } - const buttonParent = currentResource.buttonParent(); - if (buttonParent) { - if (currentResource.buttonWillAppear) currentResource.buttonWillAppear(); - addButton(buttonParent); - log("Button added"); - buttonAdded = true; - } + const buttonParent = currentResource.buttonParent(); + if (buttonParent) { + addButton(buttonParent); + if (currentResource.buttonDidAppear) currentResource.buttonDidAppear(); + log('Button added'); + buttonAdded = true; + } }; /** @type {!IObject} */ const resources = { - 'amazon': { - buttonStyle: 'border:0;padding:0;margin:0;background-color:transparent;opacity:0.8;position:relative;left:-8px;width:2vw;height:2vw;min-width:20px;min-height:20px', - buttonParent: function () { - const e = document.getElementById('dv-web-player'); - return e && e.querySelector('.hideableTopButtons'); - }, - - buttonInsertBefore: function (/** Element */ parent) { - return parent.lastChild; - }, - - videoElement: function () { - const e = document.querySelector('.rendererContainer'); - return e && e.querySelector('video[width="100%"]'); - } + 'amazon': { + buttonInsertBefore: function(/** Element */ parent) { + return parent.lastChild; }, - 'youtube': { - buttonStyle: 'transform:scale(0.7)', - - buttonClassName: 'ytp-button', - - buttonParent: function () { - const e = document.getElementById('movie_player') || document.getElementById('player'); - return e && e.querySelector('.ytp-right-controls'); - }, - - videoElement: function () { - const e = document.getElementById('movie_player') || document.getElementById('player'); - return e && e.querySelector('video.html5-main-video'); - } + buttonParent: function() { + const e = document.getElementById('dv-web-player'); + return e && e.querySelector('.hideableTopButtons'); }, - 'netflix': { - buttonImage: 'netflix', - - buttonElementType: 'span', - - buttonStyle: 'position:absolute;padding:4px;right:0;top:.4em;width:2em;height:2em;cursor:pointer;background-color:#262626', - - buttonParent: function () { - const e = document.getElementById('playerContainer'); - return e ? e.querySelector('.player-status') : null; - }, - - buttonWillAppear: function () { - resources['netflix'].buttonParent().style.paddingRight = '50px'; - }, - - videoElement: function () { - const e = document.querySelector('.player-video-wrapper'); - return e && e.querySelector('video'); - } + buttonStyle: 'border:0;padding:0;margin:0;background-color:transparent;opacity:0.8;position:relative;left:-8px;width:2vw;height:2vw;min-width:20px;min-height:20px', + videoElement: function() { + const e = document.querySelector('.rendererContainer'); + return e && e.querySelector('video[width="100%"]'); }, - 'twitch': { - buttonStyle: 'transform:scale(0.8)', + }, - buttonClassName: 'player-button', - - buttonParent: function () { - const e = document.getElementById('video-playback') || document.getElementById('player'); - return e && e.querySelector('.player-buttons-right'); - }, - - videoElement: function () { - const e = document.getElementById('video-playback') || document.getElementById('player'); - return e && e.querySelector('video'); - } + 'collegehumor': { + buttonClassName: 'vjs-control vjs-button', + buttonInsertBefore: function(/** Element */ parent) { + return parent.lastChild; }, - 'metacafe': { - buttonElementType: 'div', - - buttonStyle: 'transform:scale(0.9);left:-2px', - - buttonParent: function () { - const e = document.getElementById('player_place'); - return e && e.querySelector('.tray'); - }, - - videoElement: function () { - const e = document.getElementById('player_place'); - return e && e.querySelector('video'); - } + buttonParent: function() { + const e = document.getElementById('vjs_video_3'); + return e && e.querySelector('.vjs-control-bar'); }, - 'openload': { - buttonStyle: 'transform:scale(0.6);left:5px', - - buttonClassName: 'vjs-control vjs-button', - - buttonInsertBefore: function (/** Element */ parent) { - return parent.lastChild; - }, - - buttonParent: function () { - const e = document.getElementById('olvideo'); - return e && e.querySelector('.vjs-control-bar'); - }, - - videoElement: function () { - return document.getElementById('olvideo_html5_api'); - } + buttonStyle: 'transform:scale(0.6)', + videoElement: function() { + return document.getElementById('vjs_video_3_html5_api'); }, - 'vevo': { - buttonStyle: 'transform:scale(0.7);border:0;background:transparent', + }, - buttonClassName: 'player-control', - - buttonInsertBefore: function (/** Element */ parent) { - return parent.lastChild; - }, - - buttonParent: function () { - const e = document.getElementById('control-bar'); - return e && e.querySelector('.right-controls'); - }, - - videoElement: function () { - return document.getElementById('html5-player'); - } + 'hulu': { + buttonClassName: 'simple-button', + buttonElementType: 'div', + buttonInsertBefore: function(/** Element */ parent) { + return parent.lastChild; }, - 'collegehumor': { - buttonStyle: 'transform:scale(0.6)', - - buttonClassName: 'vjs-control vjs-button', - - buttonInsertBefore: function (/** Element */ parent) { - return parent.lastChild; - }, - - buttonParent: function () { - const e = document.getElementById('vjs_video_3'); - return e && e.querySelector('.vjs-control-bar'); - }, - - videoElement: function () { - return document.getElementById('vjs_video_3_html5_api'); - } + buttonParent: function() { + const e = document.getElementById('site-player'); + return e && e.querySelector('.main-bar'); }, - 'vid': { - buttonStyle: 'position:relative;left:9px;top:-2px;transform:scale(0.7);padding:0;margin:0', + buttonStyle: 'transform:scale(0.7)', + buttonDidAppear: function() { + resources['hulu'].buttonParent().querySelector('.progress-bar-tracker').style.width = 'calc(100% - 380px)'; + }, + videoElement: function() { + return document.getElementById('content-video-player'); + }, + }, - buttonInsertBefore: function (/** Element */ parent) { - return parent.lastChild; - }, + 'metacafe': { + buttonElementType: 'div', + buttonParent: function() { + const e = document.getElementById('player_place'); + return e && e.querySelector('.tray'); + }, + buttonStyle: 'transform:scale(0.9);left:-2px', + videoElement: function() { + const e = document.getElementById('player_place'); + return e && e.querySelector('video'); + }, + }, - buttonParent: function () { - const e = document.getElementById('video_player'); - return e && e.querySelector('.vjs-control-bar'); - }, + 'netflix': { + buttonElementType: 'span', + buttonImage: 'netflix', + buttonParent: function() { + const e = document.getElementById('playerContainer'); + return e ? e.querySelector('.player-status') : null; + }, + buttonStyle: 'position:absolute;right:0;top:0;width:2em;height:100%;cursor:pointer;background-color:#262626', + buttonDidAppear: function() { + resources['netflix'].buttonParent().style.paddingRight = '50px'; + }, + videoElement: function() { + const e = document.querySelector('.player-video-wrapper'); + return e && e.querySelector('video'); + }, + }, - videoElement: function () { - return document.getElementById('video_player_html5_api'); - } - } + 'openload': { + buttonClassName: 'vjs-control vjs-button', + buttonInsertBefore: function(/** Element */ parent) { + return parent.lastChild; + }, + buttonParent: function() { + const e = document.getElementById('olvideo'); + return e && e.querySelector('.vjs-control-bar'); + }, + buttonStyle: 'transform:scale(0.6);left:5px', + videoElement: function() { + return document.getElementById('olvideo_html5_api'); + }, + }, + + 'twitch': { + buttonClassName: 'player-button', + buttonParent: function() { + const e = document.getElementById('video-playback') || document.getElementById('player'); + return e && e.querySelector('.player-buttons-right'); + }, + buttonStyle: 'transform:scale(0.8)', + videoElement: function() { + const e = document.getElementById('video-playback') || document.getElementById('player'); + return e && e.querySelector('video'); + }, + }, + + 'vevo': { + buttonClassName: 'player-control', + buttonInsertBefore: function(/** Element */ parent) { + return parent.lastChild; + }, + buttonParent: function() { + const e = document.getElementById('control-bar'); + return e && e.querySelector('.right-controls'); + }, + buttonStyle: 'transform:scale(0.7);border:0;background:transparent', + videoElement: function() { + return document.getElementById('html5-player'); + }, + }, + + 'vid': { + buttonInsertBefore: function(/** Element */ parent) { + return parent.lastChild; + }, + buttonParent: function() { + const e = document.getElementById('video_player'); + return e && e.querySelector('.vjs-control-bar'); + }, + buttonStyle: 'position:relative;left:9px;top:-2px;transform:scale(0.7);padding:0;margin:0', + videoElement: function() { + return document.getElementById('video_player_html5_api'); + }, + }, + + 'youtube': { + buttonClassName: 'ytp-button', + buttonDidAppear: function() { + const button = document.getElementById(BUTTON_ID); + const previousButton = button.previousSibling; + const /** string */ previousTitle = previousButton.title; + button.addEventListener('mouseover', function(e){ + previousButton.title = button.title; + button.title = ''; + previousButton.dispatchEvent(new Event('mouseover')); + }); + button.addEventListener('mouseout', function(e){ + previousButton.dispatchEvent(new Event('mouseout')); + button.title = previousButton.title; + previousButton.title = previousTitle; + }); + }, + buttonParent: function() { + const e = document.getElementById('movie_player') || document.getElementById('player'); + return e && e.querySelector('.ytp-right-controls'); + }, + buttonStyle: 'transform:scale(0.7)', + videoElement: function() { + const e = document.getElementById('movie_player') || document.getElementById('player'); + return e && e.querySelector('video.html5-main-video'); + }, + }, }; +resources['youtu'] = resources['youtube']; + const domainName = location.hostname.match(/([^.]+)\.(?:co\.)?[^.]+$/)[1]; if (domainName in resources) { - log("Matched site " + domainName + " (" + location + ")"); - currentResource = resources[domainName]; + log('Matched site ' + domainName + ' (' + location + ')'); + currentResource = resources[domainName]; - const observer = new MutationObserver(buttonObserver); + const observer = new MutationObserver(buttonObserver); - observer.observe(document, { - childList:true, - subtree:true - }); + observer.observe(document, { + childList: true, + subtree: true, + }); - buttonObserver(); + buttonObserver(); } - diff --git a/update.plist b/update.plist index 625d1ea..bbb3ce2 100644 --- a/update.plist +++ b/update.plist @@ -8,9 +8,9 @@ CFBundleIdentifier com.amarcus.safari.piper CFBundleShortVersionString - 0.1.0 + 0.1.1 CFBundleVersion - 1 + 10 Developer Identifier BQ6Q24MF9X URL