mirror of
https://github.com/gosticks/PiPer.git
synced 2026-07-01 22:00:00 +00:00
@@ -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
BIN
promo/Icon-256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 512 B |
BIN
promo/Promo-shot.png
Normal file
BIN
promo/Promo-shot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user