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!
+
+
## 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