Merge pull request #1 from amarcu5/develop

v0.1.1
This commit is contained in:
amarcu5
2017-03-14 16:33:11 +00:00
committed by GitHub
8 changed files with 245 additions and 217 deletions

View File

@@ -1,6 +1,8 @@
# PiPer
Adds Picture in Picture functionality to Safari for Youtube, Netflix, Amazon Video, Twitch, and more!
<img src="/promo/Promo-shot.png" alt="Screenshot of PiPer in action" width="512" height="384"/>
## 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

Binary file not shown.

BIN
promo/Icon-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

BIN
promo/Promo-shot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -32,37 +32,42 @@
<string>BQ6Q24MF9X</string>
<key>ExtensionInfoDictionaryVersion</key>
<string>1.0</string>
<key>Update Manifest URL</key>
<string>https://s3.amazonaws.com/piper-extension/update.plist</string>
<key>Website</key>
<string>https://github.com/amarcu5/PiPer/</string>
<key>Permissions</key>
<dict>
<key>Website Access</key>
<dict>
<key>Allowed Domains</key>
<array>
<string>amazon.co.uk</string>
<string>*.amazon.co.uk</string>
<string>amazon.com</string>
<string>*.amazon.com</string>
<string>amazon.fr</string>
<string>*.amazon.fr</string>
<string>amazon.de</string>
<string>*.amazon.de</string>
<string>amazon.ca</string>
<string>*.amazon.ca</string>
<string>*.netflix.com</string>
<string>netflix.com</string>
<string>*.youtube.com</string>
<string>youtube.com</string>
<string>*.twitch.tv</string>
<string>twitch.tv</string>
<string>*.metacafe.com</string>
<string>metacafe.com</string>
<string>*.openload.co</string>
<string>openload.co</string>
<string>*.vevo.com</string>
<string>vevo.com</string>
<string>*.collegehumor.com</string>
<string>collegehumor.com</string>
<string>*.collegehumor.com</string>
<string>hulu.com</string>
<string>*.hulu.com</string>
<string>metacafe.com</string>
<string>*.metacafe.com</string>
<string>netflix.com</string>
<string>*.netflix.com</string>
<string>openload.co</string>
<string>*.openload.co</string>
<string>twitch.tv</string>
<string>*.twitch.tv</string>
<string>vevo.com</string>
<string>*.vevo.com</string>
<string>vid.me</string>
<string>*.vid.me</string>
<string>youtu.be</string>
<string>*.youtu.be</string>
<string>youtube.com</string>
<string>*.youtube.com</string>
</array>
<key>Include Secure Pages</key>
<true/>
@@ -70,5 +75,9 @@
<string>Some</string>
</dict>
</dict>
<key>Update Manifest URL</key>
<string>https://s3.amazonaws.com/piper-extension/update.plist</string>
<key>Website</key>
<string>https://github.com/amarcu5/PiPer/</string>
</dict>
</plist>

View File

@@ -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) {}
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;

View File

@@ -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<string, PIPResource>} */
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();
}

View File

@@ -8,9 +8,9 @@
<key>CFBundleIdentifier</key>
<string>com.amarcus.safari.piper</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<string>0.1.1</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>10</string>
<key>Developer Identifier</key>
<string>BQ6Q24MF9X</string>
<key>URL</key>