├── .gitignore ├── LICENSE.txt ├── README.md ├── certs ├── apple-intermediate.pem └── apple-root.pem ├── make.sh ├── out ├── PiPer-chrome.zip ├── PiPer-safari-legacy.safariextz └── PiPer-safari.pkg ├── promo ├── Chrome-small-tile.png ├── Icon-256.png ├── Icon-512.png ├── Screenshot-1.png ├── Screenshot-2a.png ├── Screenshot-2b.png ├── Screenshot-3.png ├── Screenshot-4a.png ├── Screenshot-4b.png └── Screenshot-5a.png └── src ├── chrome ├── install.html ├── manifest.json └── scripts │ ├── background.js │ └── install.js ├── common ├── Icon-128.png ├── images │ ├── default-exit.svg │ ├── default.svg │ ├── logo.svg │ └── warning.svg └── scripts │ ├── button.js │ ├── cache.js │ ├── captions.js │ ├── common.js │ ├── defines.js │ ├── externs.js │ ├── fix.js │ ├── localization.js │ ├── logger.js │ ├── main.js │ ├── resources │ ├── 9now.js │ ├── aktualne.js │ ├── amazon.js │ ├── apple.js │ ├── bbc.js │ ├── ceskatelevize.js │ ├── crunchyroll.js │ ├── curiositystream.js │ ├── dazn.js │ ├── disneyplus.js │ ├── espn.js │ ├── eurosportplayer.js │ ├── fubotv.js │ ├── giantbomb.js │ ├── hulu.js │ ├── littlethings.js │ ├── mashable.js │ ├── metacafe.js │ ├── mixer.js │ ├── mlb.js │ ├── netflix.js │ ├── ocs.js │ ├── openload.js │ ├── pbs.js │ ├── periscope.js │ ├── plex.js │ ├── seznam.js │ ├── streamable.js │ ├── ted.js │ ├── theonion.js │ ├── twitch.js │ ├── udemy.js │ ├── ustream.js │ ├── vevo.js │ ├── vice.js │ ├── vid.js │ ├── viervijfzes.js │ ├── vk.js │ ├── vrt.js │ ├── vrv.js │ ├── yeloplay.js │ └── youtube.js │ └── video.js ├── safari-legacy ├── Info.plist ├── global.html ├── scripts │ ├── background.js │ └── legacy.js └── update.plist └── safari ├── App ├── AppDelegate.swift ├── ConfettiView.swift ├── DonateContainerViewController.swift ├── DonateProgressViewController.swift ├── DonateViewController.swift ├── DonationManager.swift ├── Icon.icns ├── InAppPurchaseHelper.swift ├── Info.plist ├── LocalizationManager.swift ├── LocalizedButton.swift ├── LocalizedTextField.swift ├── Main.storyboard ├── PiPer_App.entitlements ├── ResourceHelper.swift └── ViewController.swift ├── Common ├── Defines.c └── Defines.h ├── Extension ├── Info.plist ├── PiPer_Extension.entitlements ├── Resources │ └── scripts │ │ └── localization-bridge.js └── SafariExtensionHandler.swift ├── PiPer.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ └── xcuserdata │ │ └── amarcus.xcuserdatad │ │ ├── UserInterfaceState.xcuserstate │ │ └── WorkspaceSettings.xcsettings └── xcuserdata │ └── amarcus.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── PiPer.xcscheme │ ├── PiPerExt.xcscheme │ └── xcschememanagement.plist └── exportOptions.plist /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | certs/privatekey.pem 3 | 4 | certs/cert.pem 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | PiPer logo 3 |

4 | 5 |

6 | PiPer 7 |

8 | 9 |

10 | PiPer is the browser extension to watch video Picture in Picture. 11 |

12 | 13 |

14 | Install · 15 | Donate · 16 | Report an issue 17 |

18 | 19 | *** 20 | 21 | ## Contents 22 | - [Features](#features) 23 | - [Installation](#installation) 24 | * [Safari](#safari) 25 | * [Chrome](#chrome) 26 | - [Supported sites](#supported-sites) 27 | - [Changelog](#changelog) 28 | - [Development](#development) 29 | * [Building](#building) 30 | + [Prerequisites](#prerequisites) 31 | + [Build tools](#build-tools) 32 | + [Steps](#steps) 33 | * [Supporting a new site](#supporting-a-new-site) 34 | - [Acknowledgements](#acknowledgements) 35 | 36 | ## Features 37 | * Adds a dedicated Picture in Picture button to the video player of [supported sites](#supported-sites) 38 | * Button integrates seamlessly with the player including hover effects and tooltips 39 | * Supports closed captions in Picture and Picture mode (Safari only) 40 | * Supports Safari and Chrome 41 | * Free and open source 42 | 43 | ## Installation 44 | ### Safari 45 | Install from the [Mac App Store](https://itunes.apple.com/app/id1421915518?mt=12&ls=1) by clicking "Get" 46 | (The [Safari Extension Gallery](https://safari-extensions.apple.com/details/?id=com.amarcus.safari.piper-BQ6Q24MF9X) is now [deprecated](https://developer.apple.com/documentation/safariextensions)) 47 | ### Chrome 48 | Install from the [Chrome Web Store](https://chrome.google.com/webstore/detail/piper/jbjleapidaddpbncgofepljddfeoghkc) by clicking "Add to Chrome" 49 | 50 | ...or live life on the edge with the latest [development build](https://github.com/amarcu5/PiPer/tree/develop-1.0.x/out) (IMPORTANT: these builds do not update automatically!) 51 | 52 | ## Supported sites 53 | * [9Now](http://www.9now.com.au) 54 | * [Apple TV+](http://tv.apple.com) 55 | * [Amazon Video](http://www.amazon.com/PrimeVideo) 56 | * [Česká televize](http://www.ceskatelevize.cz) 57 | * [CollegeHumor](http://www.collegehumor.com) 58 | * [Crunchyroll](http://www.crunchyroll.com) 59 | * [CuriosityStream](http://www.curiositystream.com) 60 | * [DAZN](https://www.dazn.com) 61 | * [Disney+](http://www.disneyplus.com) 62 | * [Eurosport player](http://www.eurosportplayer.com) 63 | * [FuboTV](http://www.fubo.tv) 64 | * [Giant Bomb](http://www.giantbomb.com) 65 | * [Hulu](http://www.hulu.com) 66 | * [LittleThings](http://www.littlethings.com) 67 | * [Mashable](http://www.mashable.com) 68 | * [Metacafe](http://www.metacafe.com) 69 | * [Mixer](http://mixer.com) 70 | * [MLB](http://www.mlb.tv) 71 | * [Netflix](http://www.netflix.com) 72 | * [OCS](http://www.ocs.fr) 73 | * [Openload](http://www.openload.co) 74 | * [PBS](http://www.pbs.org) 75 | * [Periscope](http://www.periscope.tv) 76 | * [Plex](http://www.plex.tv) 77 | * [Seznam Zprávy](http://www.seznam.cz/zpravy) 78 | * [Stream.cz](http://www.stream.cz) 79 | * [Streamable](http://streamable.com) 80 | * [TED](http://www.ted.com) 81 | * [The Onion](http://www.theonion.com) 82 | * [Twitch](http://www.twitch.tv) 83 | * [Udemy](http://www.udemy.com) 84 | * [Vevo](http://www.vevo.com) 85 | * [Vice](http://www.vice.com) 86 | * [Vid.me](http://www.vid.me) 87 | * [Video Aktálně](http://video.aktualne.cz) 88 | * [Vier](http://www.vier.be) 89 | * [Vijf](http://www.vijf.be) 90 | * [VK](http://www.vk.com) 91 | * [VRV](http://www.vrv.co) 92 | * [VRT NU](http://www.vrt.be/vrtnu/) 93 | * [Yelo Play](http://www.yeloplay.be) 94 | * [YouTube](http://www.youtube.com) 95 | * [Zes](http://www.zes.be) 96 | 97 | ## Changelog 98 | You can find information about releases [here](https://github.com/amarcu5/PiPer/releases) 99 | 100 | ## Development 101 | 102 | ### Building 103 | 104 | #### Prerequisites 105 | * Operating system 106 | * macOS: 10.12 Sierra or newer (required to build Safari extension) 107 | * Windows: Vista or newer using [Cygwin](https://cygwin.com/install.html) 108 | * Linux: 64-bit Ubuntu 14.04+, Debian 8+, openSUSE 13.3+, or Fedora Linux 24+ 109 | * Software 110 | * [Node.js](https://nodejs.org) 111 | * [Java](https://www.java.com/en/download/) (Windows only) 112 | 113 | 114 | #### Build tools 115 | The following build tools are used to build the extension: 116 | * [csso](https://github.com/css/csso) for compressing CSS 117 | * [svgo](https://github.com/svg/svgo) for compressing SVG images 118 | * [xarjs](https://github.com/robertknight/xar-js) for packaging Safari legacy extension 119 | * [google-closure-compiler](https://github.com/google/closure-compiler) for compiling JavaScript 120 | 121 | These can be installed by executing the following command: 122 | ```Shell 123 | npm install -g csso-cli svgo xar-js google-closure-compiler 124 | ``` 125 | 126 | #### Steps 127 | 1. Clone the repository 128 | 2. Run `make.sh` 129 | 1. By default this builds the unoptimized and unpackaged development version for all targets into the `./out/` directory 130 | 2. Alternatively: 131 | * `./make.sh -p release` to build the optimized release versions for all targets 132 | * `./make.sh -p release -t chrome` to build the optimized release version for the Chrome browser 133 | * `./make.sh -h` to see the full list of options 134 | 135 | ### Supporting a new site 136 | If we wanted to support `example.com` with the source: 137 | ```HTML 138 |
139 | 140 |
141 | Example caption 142 |
143 |
144 | 145 | 146 |
147 |
148 | ``` 149 | We would start by adding a new file `example.js` in the [resources directory](https://github.com/amarcu5/PiPer/tree/master/src/common/scripts/resources): 150 | ```JavaScript 151 | export const domain = 'example'; 152 | 153 | export const resource = { 154 | buttonParent: function() { 155 | // Returns the element that will contain the button 156 | return document.querySelector('.video-controls'); 157 | }, 158 | videoElement: function() { 159 | // Returns the video element 160 | return document.querySelector('.video-container video'); 161 | }, 162 | 163 | // Optional 164 | captionElement: function() { 165 | // Returns the element that contains the video captions 166 | return document.querySelector('.video-captions'); 167 | }, 168 | }; 169 | ``` 170 | We might want to style the button so that it integrates with the page better: 171 | ```JavaScript 172 | export const resource = { 173 | ... 174 | // Assign a CSS class 175 | buttonClassName: 'control', 176 | // Scale the button 177 | buttonScale: 0.5, 178 | // Apply custom CSS styles 179 | buttonStyle: /** CSS */ (` 180 | /* Declaring CSS this way ensures it gets optimized when the extension is built */ 181 | cursor: pointer; 182 | opacity: 0.5; 183 | `), 184 | // Apply a custom CSS hover style 185 | buttonHoverStyle: /** CSS */ (`opacity: 1 !important`), 186 | }; 187 | ``` 188 | For more examples, please see the [source](https://github.com/amarcu5/PiPer/tree/master/src/) 189 | 190 | ## Acknowledgements 191 | * [Pied PíPer](https://github.com/JoeKuhns/PiedPiPer.safariextension) for the original inspiration 192 | -------------------------------------------------------------------------------- /certs/apple-intermediate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEIjCCAwqgAwIBAgIIAd68xDltoBAwDQYJKoZIhvcNAQEFBQAwYjELMAkGA1UE 3 | BhMCVVMxEzARBgNVBAoTCkFwcGxlIEluYy4xJjAkBgNVBAsTHUFwcGxlIENlcnRp 4 | ZmljYXRpb24gQXV0aG9yaXR5MRYwFAYDVQQDEw1BcHBsZSBSb290IENBMB4XDTEz 5 | MDIwNzIxNDg0N1oXDTIzMDIwNzIxNDg0N1owgZYxCzAJBgNVBAYTAlVTMRMwEQYD 6 | VQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBXb3JsZHdpZGUgRGV2ZWxv 7 | cGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29ybGR3aWRlIERldmVsb3Bl 8 | ciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3 9 | DQEBAQUAA4IBDwAwggEKAoIBAQDKOFSmy1aqyCQ5SOmM7uxfuH8mkbw0U3rOfGOA 10 | YXdkXqUHI7Y5/lAtFVZYcC1+xG7BSoU+L/DehBqhV8mvexj/avoVEkkVCBmsqtsq 11 | Mu2WY2hSFT2Miuy/axiV4AOsAX2XBWfODoWVN2rtCbauZ81RZJ/GXNG8V25nNYB2 12 | NqSHgW44j9grFU57Jdhav06DwY3Sk9UacbVgnJ0zTlX5ElgMhrgWDcHld0WNUEi6 13 | Ky3klIXh6MSdxmilsKP8Z35wugJZS3dCkTm59c3hTO/AO0iMpuUhXf1qarunFjVg 14 | 0uat80YpyejDi+l5wGphZxWy8P3laLxiX27Pmd3vG2P+kmWrAgMBAAGjgaYwgaMw 15 | HQYDVR0OBBYEFIgnFwmpthhgi+zruvZHWcVSVKO3MA8GA1UdEwEB/wQFMAMBAf8w 16 | HwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wLgYDVR0fBCcwJTAjoCGg 17 | H4YdaHR0cDovL2NybC5hcHBsZS5jb20vcm9vdC5jcmwwDgYDVR0PAQH/BAQDAgGG 18 | MBAGCiqGSIb3Y2QGAgEEAgUAMA0GCSqGSIb3DQEBBQUAA4IBAQBPz+9Zviz1smwv 19 | j+4ThzLoBTWobot9yWkMudkXvHcs1Gfi/ZptOllc34MBvbKuKmFysa/Nw0Uwj6OD 20 | Dc4dR7Txk4qjdJukw5hyhzs+r0ULklS5MruQGFNrCk4QttkdUGwhgAqJTleMa1s8 21 | Pab93vcNIx0LSiaHP7qRkkykGRIZbVf1eliHe2iK5IaMSuviSRSqpd1VAKmuu0sw 22 | ruGgsbwpgOYJd+W+NKIByn/c4grmO7i77LpilfMFY0GCzQ87HUyVpNur+cmV6U/k 23 | TecmmYHpvPm0KdIBembhLoz2IYrF+Hjhga6/05Cdqa3zr/04GpZnMBxRpVzscYqC 24 | tGwPDBUf 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /certs/apple-root.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzET 3 | MBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv 4 | biBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDYwNDI1MjE0 5 | MDM2WhcNMzUwMjA5MjE0MDM2WjBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBw 6 | bGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx 7 | FjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw 8 | ggEKAoIBAQDkkakJH5HbHkdQ6wXtXnmELes2oldMVeyLGYne+Uts9QerIjAC6Bg+ 9 | +FAJ039BqJj50cpmnCRrEdCju+QbKsMflZ56DKRHi1vUFjczy8QPTc4UadHJGXL1 10 | XQ7Vf1+b8iUDulWPTV0N8WQ1IxVLFVkds5T39pyez1C6wVhQZ48ItCD3y6wsIG9w 11 | tj8BMIy3Q88PnT3zK0koGsj+zrW5DtleHNbLPbU6rfQPDgCSC7EhFi501TwN22IW 12 | q6NxkkdTVcGvL0Gz+PvjcM3mo0xFfh9Ma1CWQYnEdGILEINBhzOKgbEwWOxaBDKM 13 | aLOPHd5lc/9nXmW8Sdh2nzMUZaF3lMktAgMBAAGjggF6MIIBdjAOBgNVHQ8BAf8E 14 | BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9BpR5R2Cf70a40uQKb3 15 | R01/CF4wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wggERBgNVHSAE 16 | ggEIMIIBBDCCAQAGCSqGSIb3Y2QFATCB8jAqBggrBgEFBQcCARYeaHR0cHM6Ly93 17 | d3cuYXBwbGUuY29tL2FwcGxlY2EvMIHDBggrBgEFBQcCAjCBthqBs1JlbGlhbmNl 18 | IG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0 19 | YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBj 20 | b25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZp 21 | Y2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMA0GCSqGSIb3DQEBBQUAA4IBAQBc 22 | NplMLXi37Yyb3PN3m/J20ncwT8EfhYOFG5k9RzfyqZtAjizUsZAS2L70c5vu0mQP 23 | y3lPNNiiPvl4/2vIB+x9OYOLUyDTOMSxv5pPCmv/K/xZpwUJfBdAVhEedNO3iyM7 24 | R6PVbyTi69G3cN8PReEnyvFteO3ntRcXqNx+IjXKJdXZD9Zr1KIkIxH3oayPc4Fg 25 | xhtbCS+SsvhESPBgOJ4V9T0mZyCKM2r3DYLP3uujL/lTaltkwGMzd/c6ByxW69oP 26 | IQ7aunMZT7XZNn/Bh1XZp5m5MkL72NVxnn6hUrcbvZNCJBIqxw8dtk2cXmPIS4AX 27 | UKqK1drk/NAJBzewdXUh 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Make the PiPer Safari extension 4 | 5 | EXTENSION_NAME="PiPer" 6 | 7 | SOURCE_FILES=("main.js" "fix.js" "background.js" "install.js" "localization-bridge.js" "legacy.js") 8 | 9 | # Certifcate paths 10 | LEAF_CERT_PATH="../certs/cert.pem" 11 | INTERMEDIATE_CERT_PATH="../certs/apple-intermediate.pem" 12 | ROOT_CERT_PATH="../certs/apple-root.pem" 13 | PRIVATE_KEY_PATH="../certs/privatekey.pem" 14 | 15 | 16 | # Display help then exit 17 | show_help() { 18 | cat << EOF 19 | Usage: make.sh [options] 20 | 21 | Options: 22 | -h -? --help Show this screen 23 | -t --target (all|safari|safari-legacy|chrome) Make extension for target browser [default: all] 24 | -p --profile (release|debug|distribute) Set settings according to profile [default: debug] 25 | -c --compress-css Compress CSS 26 | -j --compress-js Compress JavaScript 27 | -s --compress-svg Compress SVG 28 | -l --logging-level Set logging level (0=all 10=trace 20=info 30=warning 40=error) 29 | -o --optimize-strings Remove unused localized strings by static program analysis 30 | -i --development-team Set development team ID 31 | -a --archive-to-xcode Archive Safari extension to Xcode for Mac App Store distribution 32 | -e --package-extension Package extension for distribution (safari-legacy requires private key) 33 | -d --no-debug-js Remove JavaScript source maps to prevent debugging 34 | -v --no-version-increment Disable automatic version incrementing 35 | 36 | EOF 37 | exit 0 38 | } 39 | 40 | arguments=("$@") 41 | 42 | # First pass processing arguments 43 | while :; do 44 | case $1 in 45 | -h|-\?|--help) show_help ;; 46 | -p|--profile) [[ "$2" ]] && profile=$2 ;; 47 | --profile=?*) profile=${1#*=} ;; 48 | -l|-t|-i|--logging-level|--target|--development-team) shift ;; 49 | -?*) ;; 50 | *) break 51 | esac 52 | shift 53 | done 54 | 55 | # Set default settings as per profile 56 | case $profile in 57 | distribute) 58 | compress_svg=1 59 | compress_css=1 60 | compress_js=1 61 | debug_js=0 62 | package_ext=1 63 | logging_level=100 64 | optimize_strings=1 65 | ;; 66 | release) 67 | compress_svg=1 68 | compress_css=1 69 | compress_js=1 70 | debug_js=1 71 | package_ext=0 72 | logging_level=40 73 | optimize_strings=1 74 | ;; 75 | *) 76 | compress_svg=0 77 | compress_css=0 78 | compress_js=0 79 | debug_js=1 80 | package_ext=0 81 | logging_level=0 82 | optimize_strings=0 83 | profile="debug" 84 | ;; 85 | esac 86 | update_version=1 87 | archive_xcode=0 88 | development_team="" 89 | targets="all" 90 | 91 | set -- "${arguments[@]}" 92 | 93 | # Second pass processing arguments 94 | while :; do 95 | case $1 in 96 | -c|--compress-css) compress_css=1 ;; 97 | -j|--compress-js) compress_js=1 ;; 98 | -s|--compress-svg) compress_svg=1 ;; 99 | -e|--package-extension) package_ext=1 ;; 100 | -o|--optimize-localizations) optimize_strings=1 ;; 101 | -d|--no-debug-js) debug_js=0 ;; 102 | -v|--no-version-increment) update_version=0 ;; 103 | -t|--target) [[ "$2" ]] && targets=$2 && shift ;; 104 | --target=?*) targets=${1#*=} ;; 105 | -l|--logging-level) [[ "$2" ]] && logging_level=$2 && shift ;; 106 | --logging-level=?*) logging_level=${1#*=} ;; 107 | -i|--development-team) [[ "$2" ]] && development_team=$2 && shift ;; 108 | --development-team=?*) development_team=${1#*=} ;; 109 | -a|--archive-to-xcode) archive_xcode=1 ;; 110 | -p|--profile) shift ;; 111 | -?*) ;; 112 | *) break ;; 113 | esac 114 | shift 115 | done 116 | 117 | # Highlight selected build profile 118 | echo "Setting '${profile}' profile" 119 | 120 | # Validate targets 121 | case $targets in 122 | safari) targets=("safari") ;; 123 | safari-legacy) targets=("safari-legacy") ;; 124 | chrome) targets=("chrome") ;; 125 | *) targets=("safari" "safari-legacy" "chrome") 126 | esac 127 | 128 | # Validate logging level 129 | logging_level="${logging_level//[!0-9]/}" 130 | [[ -z "$logging_level" ]] && logging_level=0 131 | 132 | # Helper checks for build tool dependency and falls back to 'npx' if possible 133 | function get_node_command() { 134 | if type "$1" &>/dev/null; then 135 | echo "$1" 136 | elif type "npx" &>/dev/null; then 137 | npx_package=$([[ -z "$2" ]] && echo "$1" || echo "$2") 138 | echo "npx --quiet --package ${npx_package} $1" 139 | echo "Info: '$1' command not found therefore falling back to 'npx'; performance may suffer (avoid this by installing package with 'npm install ${npx_package} -g')" >&2 140 | else 141 | echo "Error: '$1' command not found and neither fallback 'npx'" >&2 142 | echo "Please install the latest version of Node.js (see https://nodejs.org/en/download/package-manager/)" >&2 143 | return 1 144 | fi 145 | return 0 146 | } 147 | 148 | # Target specific build checks 149 | for i in "${!targets[@]}"; do 150 | 151 | if [[ "${targets[$i]}" = "safari" ]]; then 152 | 153 | # Only build 'safari' extension target when running under macOS 154 | if [[ "$(uname)" != "Darwin" ]]; then 155 | echo "Warning: Building 'safari' extension skipped as requires macOS" >&2 156 | unset "targets[$i]" 157 | continue 158 | fi 159 | 160 | # Ensure with have Xcode command line tools installed 161 | if [[ -z $(xcode-select --print-path) ]]; then 162 | echo "Installing Xcode Command Line Tools (expect a GUI popup)" 163 | xcode-select --install &>/dev/null 164 | echo "Press any key after installation has completed" 165 | read -rsn1 166 | if [[ -z $(xcode-select --print-path) ]]; then 167 | echo "Unable to find Xcode Command Line Tools" 168 | exit 1 169 | fi 170 | fi 171 | 172 | elif [[ "${targets[$i]}" = "safari-legacy" ]]; then 173 | 174 | # Get 'safari-legacy' specific build tool path and exit if not found 175 | [[ "${package_ext}" -eq 1 ]] && { XARJS_PATH=$(get_node_command "xarjs" "xar-js") || exit 1; } 176 | fi 177 | 178 | done 179 | 180 | # Check for google closure compiler requirements and exit if not found 181 | CCJS_PATH=$(get_node_command "google-closure-compiler") || exit 1; 182 | if ${CCJS_PATH} --platform native --version &>/dev/null; then 183 | CCJS_PATH="${CCJS_PATH} --platform native"; 184 | elif ${CCJS_PATH} --platform java --version &>/dev/null; then 185 | CCJS_PATH="${CCJS_PATH} --platform java"; 186 | else 187 | echo "Error: Java runtime required by 'google-closure-compiler' not found" >&2 188 | echo "Please install the latest version of Java (see https://www.java.com/en/download/)" >&2 189 | exit 1 190 | fi 191 | 192 | # Check for csso and exit if not found 193 | [[ "${compress_css}" -eq 1 ]] && { CSSO_PATH=$(get_node_command "csso" "csso-cli") || exit 1; } 194 | 195 | # Check for svgo and exit if not found 196 | [[ "${compress_svg}" -eq 1 ]] && { SVGO_PATH=$(get_node_command "svgo") || exit 1; } 197 | 198 | # Check for git and exit if not found 199 | if [[ "${update_version}" -eq 1 ]] && { ! type "git" &>/dev/null; }; then 200 | echo "Error: 'git' command not found" >&2 201 | echo "Please install the latest version of git (see https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)" >&2 202 | exit 1 203 | else 204 | GIT_PATH=$(sh /etc/profile; which git) 205 | fi 206 | 207 | 208 | # 209 | # Build script 210 | # 211 | 212 | # Set working directory to project root 213 | cd "${BASH_SOURCE[0]%/*}" 214 | 215 | # Remove output folder 216 | rm -rf out 217 | 218 | # Make common output folder 219 | mkdir -p "out/${EXTENSION_NAME}" 220 | 221 | # Copy common items into common output folder 222 | cp -r "src/common"/* "out/${EXTENSION_NAME}/" 223 | 224 | # Compress all supported images with SVGO 225 | if [[ "${compress_svg}" -eq 1 ]]; then 226 | ${SVGO_PATH} -q -f "out/${EXTENSION_NAME}/images" 227 | fi 228 | 229 | # Get current version from git if automatic versioning enabled 230 | if [[ "${update_version}" -eq 1 ]]; then 231 | 232 | # Check we're inside a git work tree 233 | inside_git_repo="$(git rev-parse --is-inside-work-tree 2>/dev/null)" 234 | if [[ "${inside_git_repo}" ]]; then 235 | 236 | # Get number of commits and release version from most recent tag 237 | number_of_commits=$(($("${GIT_PATH}" rev-list HEAD --count) + 1)) 238 | git_release_version=$("${GIT_PATH}" describe --tags --always --abbrev=0) 239 | git_release_version=${git_release_version%%-*}; 240 | git_release_version=${git_release_version#*v}; 241 | 242 | # Otherwise issue warning and set blank version 243 | else 244 | echo "Warning: Unable to set version automatically as cannot find 'git' repository (ensure repository has been cloned to fix this)" >&2 245 | number_of_commits="0" 246 | git_release_version="0.0.0" 247 | fi 248 | 249 | # Helper performs multiline sed regular expression 250 | function multiline_sed_regex() { 251 | mv "$1" "$1.bak" 252 | echo -n "$(cat "$1.bak")" | tr "\n" "\f" | sed -E "$2" | tr "\f" "\n" > "$1" 253 | rm -rf "$1.bak" 254 | } 255 | fi 256 | 257 | # Make resources index file 258 | import_list="" 259 | resource_list="" 260 | alias_list="" 261 | resource_count=0 262 | for path in "out/${EXTENSION_NAME}/scripts/resources"/*.js; do 263 | path="${path##*/}" 264 | [[ $path == "index.js" ]] && continue 265 | resource_count=$((resource_count+1)) 266 | import_list="${import_list}"$'\n'"import * as r${resource_count} from \"./${path}\";" 267 | resource=$(<"out/${EXTENSION_NAME}/scripts/resources/${path}") 268 | regex_arr="(^|[ "$'\n'"])(const|let|var)[ "$'\n'"]+domain[ "$'\n'"]*=[ "$'\n'"]*\[([^]]+)\]" 269 | regex_val="(^|[ "$'\n'"])(const|let|var)[ "$'\n'"]+domain[ "$'\n'"]*=" 270 | if [[ "$resource" =~ $regex_arr ]]; then 271 | IFS=", " read -a arr <<< "${BASH_REMATCH[3]}" 272 | resource_list="${resource_list}"$'\n'"resources[${arr[0]}] = r${resource_count}.resource;" 273 | for ((i=1;i<${#arr[@]};i++)); do 274 | alias_list="${alias_list}"$'\n'"resources[${arr[$i]}] = resources[${arr[0]}];" 275 | done 276 | elif [[ "$resource" =~ $regex_val ]]; then 277 | resource_list="${resource_list}"$'\n'"resources[r${resource_count}.domain] = r${resource_count}.resource;" 278 | else 279 | echo "Warning: No domain's listed for resource '${path}'" >&2 280 | fi 281 | done 282 | 283 | { 284 | cat <"out/${EXTENSION_NAME}/scripts/resources/index.js" 293 | 294 | 295 | 296 | for target in "${targets[@]}"; do 297 | 298 | echo "Building '${target}' extension" 299 | 300 | # Set target specific flags 301 | case $target in 302 | safari) 303 | browser=1 304 | target_extension="" 305 | common_file_path="/Extension/Resources" 306 | ;; 307 | safari-legacy) 308 | browser=1 309 | target_extension=".safariextension" 310 | common_file_path="" 311 | ;; 312 | chrome) 313 | browser=2 314 | target_extension="" 315 | common_file_path="" 316 | ;; 317 | *) exit 1 318 | esac 319 | 320 | # Make target folder 321 | mkdir -p "out/${EXTENSION_NAME}-${target}${target_extension}${common_file_path}" 322 | 323 | # Copy items from common output folder to target folder 324 | cp -r "out/${EXTENSION_NAME}"/* "out/${EXTENSION_NAME}-${target}${target_extension}${common_file_path}/" 325 | 326 | # Copy target specific items to target output folder 327 | cp -r "src/${target}"/* "out/${EXTENSION_NAME}-${target}${target_extension}/" 2>/dev/null 328 | 329 | # Compress all inline CSS with CSSO 330 | if [[ "${compress_css}" -eq 1 ]]; then 331 | function minify_css() { 332 | echo "$@" | sed -e 's/\\"/"/g' -e 's/\\\$/$/g' | ${CSSO_PATH} --declaration-list 333 | } 334 | export -f minify_css 335 | export CSSO_PATH 336 | for path in "out/${EXTENSION_NAME}-${target}${target_extension}${common_file_path}/scripts"/{*,**/*}.js; do 337 | [[ ! -f "${path}" ]] && continue 338 | source=$(cat "${path}") 339 | echo "echo \"$(sed -e 's/\\/\\\\/g' -e 's/\$/\\$/g' -e 's/`/\\`/g' -e 's/\"/\\\"/g' -e 's/\\n/\\\\n/g' <<< "$source" \ 340 | | tr '\n' '\f' \ 341 | | sed -E 's/\/\*\*[[:space:]]+CSS[[:space:]]+\*\/[[:space:]]*\([[:space:]]*\\`([^`]*)\\`[[:space:]]*\)/\\`\$(minify_css '\''\1'\'')\\`/g' \ 342 | | tr '\f' '\n')\"" \ 343 | | sh > "${path}" 344 | done 345 | fi 346 | 347 | # Use closure compiler to compress javascript 348 | function remove_element() { 349 | for i in "${!files[@]}"; do 350 | if [[ ${files[$i]} = "$1" ]]; then 351 | unset "files[$i]" 352 | fi 353 | done 354 | } 355 | 356 | function add_element() { 357 | remove_element "$1" 358 | files+=("$1") 359 | } 360 | 361 | function get_absolute_path() { 362 | local dirname="${1%/*}" 363 | local basename="${1##*/}" 364 | 365 | echo "$(cd "$dirname" 2>/dev/null; pwd)/$basename" 366 | } 367 | 368 | # Convert absolute paths to platform native path on Windows 369 | function fix_absolute_path() { 370 | case "$(uname -s)" in 371 | CYGWIN*|MINGW32*|MSYS*) 372 | echo "$(cygpath -wa ${1})" 373 | ;; 374 | *) echo "$1" 375 | esac 376 | } 377 | 378 | function process_file() { 379 | local dirname="${1%/*}" 380 | local imports=() 381 | 382 | if [[ ! -f "$1" ]]; then 383 | remove_element "$1" 384 | return 385 | fi 386 | 387 | local source=$(<"$1") 388 | regex="(^| |"$'\n'")(import|export)["$'\n'" ]+(([*a-zA-Z0-9_,{}"$'\n'" $]+)from["$'\n'" ]+)?['\"]([^'\"]+)['\"][ "$'\n'";]" 389 | while true; do 390 | if [[ "$source" =~ $regex ]]; then 391 | source="${source##*${BASH_REMATCH[0]}}" 392 | imports+=("${BASH_REMATCH[5]}") 393 | else 394 | break 395 | fi 396 | done 397 | 398 | for i in "${!imports[@]}"; do 399 | imports[$i]=$(cd "$dirname"; get_absolute_path "${imports[$i]}") 400 | add_element "${imports[$i]}" 401 | done 402 | 403 | for i in "${!imports[@]}"; do 404 | process_file "${imports[$i]}" 405 | done 406 | } 407 | 408 | 409 | scripts_path=$(get_absolute_path "out/${EXTENSION_NAME}-${target}${target_extension}${common_file_path}/scripts") 410 | defines_path="${scripts_path}/defines.js" 411 | extern_path=$(fix_absolute_path "${scripts_path}/externs.js") 412 | 413 | defines_processed_path=$(echo "${defines_path%.*}" | sed -E 's|[/@\]|$|g' | sed -E 's/[-. ]/_/g' | sed -e 's/\[/%5B/g' -e 's/]/%5D/g' -e 's/>/%3E/g' -e 's/ "${localization_path}" 480 | fi 481 | 482 | absolute_entry=$(fix_absolute_path "$absolute_entry") 483 | 484 | defines=() 485 | js_code=() 486 | for path in "${files[@]}"; do 487 | path=$(fix_absolute_path "$path") 488 | js_code=("--js" "$path" "${js_code[@]}") 489 | if [[ "$path" = "$defines_path" ]]; then 490 | defines=( 491 | "--define" "$logging_flag" 492 | "--define" "$browser_flag" 493 | ) 494 | fi 495 | done 496 | 497 | if [[ "$debug_js" -eq 0 ]]; then 498 | source_map_options=() 499 | else 500 | source_map_options=( 501 | --create_source_map "${absolute_entry}.map" 502 | --source_map_location_mapping "$scripts_path|." 503 | --source_map_include_content 504 | ) 505 | fi 506 | 507 | if [[ "$compress_js" -eq 0 ]]; then 508 | compression_options=( 509 | --compilation_level WHITESPACE_ONLY \ 510 | --js_module_root "$scripts_path" \ 511 | --formatting PRETTY_PRINT \ 512 | --formatting PRINT_INPUT_DELIMITER \ 513 | ) 514 | else 515 | compression_options=( 516 | --compilation_level ADVANCED \ 517 | --use_types_for_optimization \ 518 | --assume_function_wrapper \ 519 | --jscomp_error strictCheckTypes \ 520 | --jscomp_error strictMissingProperties \ 521 | --jscomp_error checkTypes \ 522 | --jscomp_error checkVars \ 523 | --jscomp_error reportUnknownTypes \ 524 | --externs "$extern_path" \ 525 | "${defines[@]}" \ 526 | ) 527 | fi 528 | 529 | ${CCJS_PATH} \ 530 | "${compression_options[@]}" \ 531 | --warning_level VERBOSE \ 532 | --language_out ECMASCRIPT_2017 \ 533 | --output_wrapper "var a;a||(a=!0,(()=>{%output%})());" \ 534 | "${source_map_options[@]}" \ 535 | "${js_code[@]}" \ 536 | > "${absolute_entry%.*}.cjs" 537 | 538 | done 539 | 540 | # Remove uncompiled JavaScript 541 | rm -f "${scripts_path}/"{*,**/*}.js 542 | 543 | # Remove any empty folders 544 | for path in "${scripts_path}/"{*,**/*}; do 545 | if [[ -d "$path" ]] && [[ ! -f "$path"/* ]]; then 546 | rm -rf "$path" 547 | fi 548 | done 549 | 550 | # Restore '.js' extension for compiled JavaScript 551 | for entry in "${SOURCE_FILES[@]}"; do 552 | entry="${scripts_path}/"${entry%.*} 553 | [ ! -f "${entry}.cjs" ] && continue 554 | mv "${entry}.cjs" "${entry}.js" 555 | done 556 | 557 | # Embed source maps and remove map files 558 | if [[ "$debug_js" -eq 1 ]]; then 559 | for entry in "${SOURCE_FILES[@]}"; do 560 | entry="${scripts_path}/${entry}" 561 | [[ ! -f "${entry}" ]] && continue 562 | source_map=$(base64 "${entry}.map" | tr -d \\n) 563 | echo "//# sourceMappingURL=data:application/json;base64,${source_map}" >> "${entry}" 564 | rm -f "${entry}.map" 565 | done 566 | fi 567 | 568 | # Safari specific build steps 569 | if [[ "${target}" == "safari" ]]; then 570 | 571 | # Update version info from git 572 | if [[ "${update_version}" -eq 1 ]]; then 573 | multiline_sed_regex "out/${EXTENSION_NAME}-${target}/Extension/Info.plist" "s|(> *CFBundleShortVersionString *[^>]+>)[^<]+|\1${git_release_version}|g" 574 | multiline_sed_regex "out/${EXTENSION_NAME}-${target}/Extension/Info.plist" "s|(> *CFBundleVersion *[^>]+>)[^<]+|\1${number_of_commits}|g" 575 | multiline_sed_regex "out/${EXTENSION_NAME}-${target}/App/Info.plist" "s|(> *CFBundleShortVersionString *[^>]+>)[^<]+|\1${git_release_version}|g" 576 | multiline_sed_regex "out/${EXTENSION_NAME}-${target}/App/Info.plist" "s|(> *CFBundleVersion *[^>]+>)[^<]+|\1${number_of_commits}|g" 577 | fi 578 | 579 | # Get development team id automatically if needed 580 | if [[ -z "${development_team}" ]]; then 581 | 582 | # Helper maintains unique list of ids 583 | team_ids=() 584 | function add_unique_team_ids() { 585 | for i in "${!team_ids[@]}"; do 586 | [[ ${team_ids[$i]} = "$1" ]] && return 587 | done 588 | team_ids+=("$1") 589 | } 590 | 591 | # Search mobileprovision files for team identifiers 592 | regex="TeamIdentifier<\/key>[^\/]+>([A-Z0-9]{10})<\/" 593 | for path in "${HOME}/Library/MobileDevice/Provisioning Profiles"/*.mobileprovision; do 594 | source=$(cat "${path}" | iconv -f "ISO-8859-1" -t "UTF-8") 595 | if [[ "${source}" =~ $regex ]]; then 596 | add_unique_team_ids "${BASH_REMATCH[1]}" 597 | fi 598 | done 599 | 600 | # If multiple or no identifiers found then prompt the user 601 | development_team_hint="(avoid this message in future by specifying --development-team)" 602 | if (( ${#team_ids[@]} == 0 )); then 603 | echo "Unable to find development team automatically, please enter below: ${development_team_hint}" 604 | read development_team /dev/null 623 | 624 | # Copy archive to Xcode if needed 625 | if [[ "${archive_xcode}" -eq 1 ]]; then 626 | archive_time=$(date '+%d-%m-%Y, %H.%M') 627 | mv "./out/${EXTENSION_NAME}-${target}.xcarchive" "${HOME}/Library/Developer/Xcode/Archives/${EXTENSION_NAME} ${archive_time}.xcarchive" 628 | fi 629 | 630 | # Package extension 631 | if [[ "${package_ext}" -eq 1 ]]; then 632 | productbuild --quiet --component "./out/PiPer.app" "/Applications" "./out/${EXTENSION_NAME}-${target}.pkg" 633 | rm -rf "./out/PiPer.app" 634 | else 635 | mv "out/PiPer.app" "out/${EXTENSION_NAME}-${target}.app" 636 | fi 637 | 638 | # Remove everything else 639 | rm -rf "out/${EXTENSION_NAME}-${target}.xcarchive" 640 | rm -rf "out/${EXTENSION_NAME}-${target}" 641 | 642 | elif [[ "${target}" == "safari-legacy" ]]; then 643 | 644 | # Remove irrelevant target file 645 | rm -f "out/${EXTENSION_NAME}-${target}${target_extension}/update.plist" 646 | 647 | # Update version info from git 648 | if [[ "${update_version}" -eq 1 ]]; then 649 | info_plist="out/${EXTENSION_NAME}-${target}${target_extension}/Info.plist" 650 | update_plist="src/${target}/update.plist" 651 | multiline_sed_regex "${info_plist}" "s|(> *CFBundleShortVersionString *[^>]+>)[^<]+|\1${git_release_version}|g" 652 | multiline_sed_regex "${info_plist}" "s|(> *CFBundleVersion *[^>]+>)[^<]+|\1${number_of_commits}|g" 653 | multiline_sed_regex "${update_plist}" "s|(> *CFBundleShortVersionString *[^>]+>)[^<]+|\1${git_release_version}|g" 654 | multiline_sed_regex "${update_plist}" "s|(> *CFBundleVersion *[^>]+>)[^<]+|\1${number_of_commits}|g" 655 | fi 656 | 657 | # Package safari extension 658 | if [[ "${package_ext}" -eq 1 ]] && [[ -f "out/${PRIVATE_KEY_PATH}" ]]; then 659 | (cd out && ${XARJS_PATH} create "${EXTENSION_NAME}-${target}.safariextz" --cert "${LEAF_CERT_PATH}" --cert "${INTERMEDIATE_CERT_PATH}" --cert "${ROOT_CERT_PATH}" --private-key "${PRIVATE_KEY_PATH}" "${EXTENSION_NAME}-${target}${target_extension}") 660 | rm -rf "out/${EXTENSION_NAME}-${target}${target_extension}" 661 | fi 662 | 663 | elif [[ "${target}" == "chrome" ]]; then 664 | 665 | # Update manifest version information 666 | if [[ "${update_version}" -eq 1 ]]; then 667 | sed -i.bak -E "s|\"version\": *\"[^\"]+\"|\"version\": \"${git_release_version}.${number_of_commits}\"|g" "out/${EXTENSION_NAME}-${target}/manifest.json" 668 | sed -i.bak -E "s|\"version_name\": *\"[^\"]+\"|\"version_name\": \"${git_release_version}\"|g" "out/${EXTENSION_NAME}-${target}/manifest.json" 669 | rm -rf "out/${EXTENSION_NAME}-${target}/manifest.json.bak" 670 | fi 671 | 672 | # Package chrome extension 673 | if [[ "${package_ext}" -eq 1 ]]; then 674 | (cd "out/${EXTENSION_NAME}-${target}${target_extension}" && zip -rq "${EXTENSION_NAME}-${target}${target_extension}.zip" *) 675 | mv "out/${EXTENSION_NAME}-${target}${target_extension}/${EXTENSION_NAME}-${target}${target_extension}.zip" "out/" 676 | rm -rf "out/${EXTENSION_NAME}-${target}${target_extension}" 677 | fi 678 | fi 679 | 680 | done 681 | 682 | # Clean common output folder 683 | rm -rf "out/${EXTENSION_NAME}" 684 | 685 | echo "Done." 686 | -------------------------------------------------------------------------------- /out/PiPer-chrome.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amarcu5/PiPer/51d585da9cdf6bf31156fbb3f9b2df15e0e1749a/out/PiPer-chrome.zip -------------------------------------------------------------------------------- /out/PiPer-safari-legacy.safariextz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amarcu5/PiPer/51d585da9cdf6bf31156fbb3f9b2df15e0e1749a/out/PiPer-safari-legacy.safariextz -------------------------------------------------------------------------------- /out/PiPer-safari.pkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amarcu5/PiPer/51d585da9cdf6bf31156fbb3f9b2df15e0e1749a/out/PiPer-safari.pkg -------------------------------------------------------------------------------- /promo/Chrome-small-tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amarcu5/PiPer/51d585da9cdf6bf31156fbb3f9b2df15e0e1749a/promo/Chrome-small-tile.png -------------------------------------------------------------------------------- /promo/Icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amarcu5/PiPer/51d585da9cdf6bf31156fbb3f9b2df15e0e1749a/promo/Icon-256.png -------------------------------------------------------------------------------- /promo/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amarcu5/PiPer/51d585da9cdf6bf31156fbb3f9b2df15e0e1749a/promo/Icon-512.png -------------------------------------------------------------------------------- /promo/Screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amarcu5/PiPer/51d585da9cdf6bf31156fbb3f9b2df15e0e1749a/promo/Screenshot-1.png -------------------------------------------------------------------------------- /promo/Screenshot-2a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amarcu5/PiPer/51d585da9cdf6bf31156fbb3f9b2df15e0e1749a/promo/Screenshot-2a.png -------------------------------------------------------------------------------- /promo/Screenshot-2b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amarcu5/PiPer/51d585da9cdf6bf31156fbb3f9b2df15e0e1749a/promo/Screenshot-2b.png -------------------------------------------------------------------------------- /promo/Screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amarcu5/PiPer/51d585da9cdf6bf31156fbb3f9b2df15e0e1749a/promo/Screenshot-3.png -------------------------------------------------------------------------------- /promo/Screenshot-4a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amarcu5/PiPer/51d585da9cdf6bf31156fbb3f9b2df15e0e1749a/promo/Screenshot-4a.png -------------------------------------------------------------------------------- /promo/Screenshot-4b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amarcu5/PiPer/51d585da9cdf6bf31156fbb3f9b2df15e0e1749a/promo/Screenshot-4b.png -------------------------------------------------------------------------------- /promo/Screenshot-5a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amarcu5/PiPer/51d585da9cdf6bf31156fbb3f9b2df15e0e1749a/promo/Screenshot-5a.png -------------------------------------------------------------------------------- /src/chrome/install.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PiPer 5 | 6 | 7 | 94 | 95 | 96 | 100 | 101 |
102 | Warning 103 |
chrome-flags-warning
104 | 105 |
106 | 107 |
108 | report-bug 109 | donate 110 |
111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /src/chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PiPer", 3 | "description": "Adds Picture in Picture functionality to YouTube, Netflix, Amazon Video, Twitch, and more!", 4 | "version": "0.0.0.0", 5 | "version_name": "0.0.0", 6 | "icons": { 7 | "128": "Icon-128.png" 8 | }, 9 | "background": { 10 | "scripts": ["scripts/background.js"], 11 | "persistent": false 12 | }, 13 | "content_scripts": [ 14 | { 15 | "all_frames": true, 16 | "matches": ["http://*/*", "https://*/*"], 17 | "run_at": "document_idle", 18 | "js": ["scripts/main.js"] 19 | } 20 | ], 21 | "permissions": [ 22 | "activeTab", 23 | "storage" 24 | ], 25 | "web_accessible_resources": [ 26 | "images/*.svg", 27 | "scripts/*.js" 28 | ], 29 | "minimum_chrome_version": "69.0.3483.0", 30 | "manifest_version": 2 31 | } 32 | -------------------------------------------------------------------------------- /src/chrome/scripts/background.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onInstalled.addListener(function(/** {reason: string} */ details) { 2 | if (details.reason == "install") { 3 | chrome.tabs.create({url: chrome.extension.getURL("install.html")}); 4 | } 5 | }); -------------------------------------------------------------------------------- /src/chrome/scripts/install.js: -------------------------------------------------------------------------------- 1 | import { info } from './logger.js' 2 | import { localizedString, localizedStringWithReplacements } from './localization.js' 3 | 4 | // Hide page during loading 5 | const htmlTag = /** @type {HTMLElement} */ (document.getElementsByTagName("html")[0]); 6 | htmlTag.style.display = 'none'; 7 | 8 | document.addEventListener('DOMContentLoaded', function() { 9 | 10 | // Localize text elements 11 | const localizedElements = document.getElementsByClassName('localized-string'); 12 | for (let index = 0, element; element = localizedElements[index]; index++) { 13 | const key = element.textContent.trim(); 14 | 15 | let string; 16 | if (key == 'chrome-flags-warning') { 17 | string = localizedStringWithReplacements(key, [ 18 | ['emphasis', ''], 19 | ['/emphasis', ''], 20 | ]); 21 | } else { 22 | string = localizedString(key); 23 | } 24 | 25 | element.innerHTML = string; 26 | } 27 | 28 | // Make page visible 29 | htmlTag.style.removeProperty('display'); 30 | 31 | // Open required Chrome flag if warning button clicked 32 | document.getElementById('warning-button').addEventListener('click', function(event) { 33 | chrome.tabs.create({url: 'chrome://flags/#enable-surfaces-for-videos'}); 34 | }); 35 | 36 | // Test for Picture in Picture support and display warning to activate Chrome flags if needed 37 | const video = /** @type {HTMLVideoElement} */ (document.getElementById('test-video')); 38 | video.addEventListener('loadeddata', function() { 39 | video.requestPictureInPicture().catch(function(error) { 40 | const errorMessage = /** @type {Error} */ (error).message; 41 | if (~errorMessage.indexOf('Picture-in-Picture is not available')) { 42 | info('Picture-in-Picture NOT supported'); 43 | document.getElementById('warning').style.display = 'flex'; 44 | } else { 45 | info('Picture-in-Picture IS supported'); 46 | } 47 | }); 48 | }); 49 | }); -------------------------------------------------------------------------------- /src/common/Icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amarcu5/PiPer/51d585da9cdf6bf31156fbb3f9b2df15e0e1749a/src/common/Icon-128.png -------------------------------------------------------------------------------- /src/common/images/default-exit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 11 | 12 | 15 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/common/images/default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 11 | 12 | 14 | 15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/common/scripts/button.js: -------------------------------------------------------------------------------- 1 | import { info, error } from './logger.js' 2 | import { getResource, getExtensionURL } from './common.js' 3 | import { togglePictureInPicture, addPictureInPictureEventListener } from './video.js' 4 | import { localizedString } from './localization.js' 5 | 6 | const BUTTON_ID = 'PiPer_button'; 7 | 8 | let /** ?HTMLElement */ button = null; 9 | 10 | /** 11 | * Injects Picture in Picture button into webpage 12 | * 13 | * @param {Element} parent - Element button will be inserted into 14 | */ 15 | export const addButton = function(parent) { 16 | 17 | // Create button if needed 18 | if (!button) { 19 | const buttonElementType = getResource().buttonElementType || 'button'; 20 | button = /** @type {HTMLElement} */ (document.createElement(buttonElementType)); 21 | 22 | // Set button properties 23 | button.id = BUTTON_ID; 24 | button.title = localizedString('button-title'); 25 | const buttonStyle = getResource().buttonStyle; 26 | if (buttonStyle) button.style.cssText = buttonStyle; 27 | const buttonClassName = getResource().buttonClassName; 28 | if (buttonClassName) button.className = buttonClassName; 29 | 30 | // Add scaled image to button 31 | const image = /** @type {HTMLImageElement} */ (document.createElement('img')); 32 | image.style.width = image.style.height = '100%'; 33 | const buttonScale = getResource().buttonScale; 34 | if (buttonScale) image.style.transform = `scale(${buttonScale})`; 35 | button.appendChild(image); 36 | 37 | // Set image paths 38 | let buttonImage = getResource().buttonImage; 39 | let buttonExitImage = getResource().buttonExitImage; 40 | if (!buttonImage) { 41 | buttonImage = 'default'; 42 | buttonExitImage = 'default-exit'; 43 | } 44 | const buttonImageURL = getExtensionURL(`images/${buttonImage}.svg`); 45 | image.src = buttonImageURL; 46 | if (buttonExitImage) { 47 | const buttonExitImageURL = getExtensionURL(`images/${buttonExitImage}.svg`); 48 | addPictureInPictureEventListener(function(video, isPlayingPictureInPicture) { 49 | image.src = (isPlayingPictureInPicture) ? buttonExitImageURL : buttonImageURL; 50 | }); 51 | } 52 | 53 | // Add hover style to button (a nested stylesheet is used to avoid tracking another element) 54 | const buttonHoverStyle = getResource().buttonHoverStyle; 55 | if (buttonHoverStyle) { 56 | const style = document.createElement('style'); 57 | const css = `#${BUTTON_ID}:hover{${buttonHoverStyle}}`; 58 | style.appendChild(document.createTextNode(css)); 59 | button.appendChild(style); 60 | } 61 | 62 | // Toggle Picture in Picture mode when button is clicked 63 | button.addEventListener('click', function(event) { 64 | event.preventDefault(); 65 | 66 | // Get the video element and bypass caching to accomodate for the underlying video changing (e.g. pre-roll adverts) 67 | const video = /** @type {?HTMLVideoElement} */ (getResource().videoElement(true)); 68 | if (!video) { 69 | error('Unable to find video'); 70 | return; 71 | } 72 | 73 | togglePictureInPicture(video); 74 | }); 75 | 76 | info('Picture in Picture button created'); 77 | } 78 | 79 | // Inject button into correct place 80 | const referenceNode = getResource().buttonInsertBefore ? getResource().buttonInsertBefore(parent) : null; 81 | parent.insertBefore(button, referenceNode); 82 | }; 83 | 84 | /** 85 | * Returns the Picture in Picture button element 86 | * 87 | * @return {?HTMLElement} 88 | */ 89 | export const getButton = function() { 90 | return button; 91 | }; 92 | 93 | /** 94 | * Checks if Picture in Picture button is injected into page 95 | * 96 | * @return {boolean} 97 | */ 98 | export const checkButton = function() { 99 | return !!document.getElementById(BUTTON_ID); 100 | }; 101 | -------------------------------------------------------------------------------- /src/common/scripts/cache.js: -------------------------------------------------------------------------------- 1 | import { getResource } from './common.js' 2 | 3 | /** 4 | * Initialises caching for button, video, and caption elements 5 | */ 6 | export const initialiseCaches = function() { 7 | 8 | // Return a unique id 9 | let uniqueIdCounter = 0; 10 | const /** function():string */ uniqueId = function() { 11 | return 'PiPer_' + uniqueIdCounter++; 12 | }; 13 | 14 | /** 15 | * Wraps a function that returns an element to provide faster lookups by id 16 | * 17 | * @param {function(boolean=):?Element} elementFunction 18 | * @return {function(boolean=):?Element} 19 | */ 20 | const cacheElementWrapper = function(elementFunction) { 21 | let /** ?string */ cachedElementId = null; 22 | 23 | return /** function():?Element */ function(/** boolean= */ bypassCache) { 24 | 25 | // Return element by id if possible 26 | const cachedElement = cachedElementId ? 27 | document.getElementById(cachedElementId) : null; 28 | if (cachedElement && !bypassCache) return cachedElement; 29 | 30 | // Call the underlying function to get the element 31 | const uncachedElement = elementFunction(); 32 | if (uncachedElement) { 33 | 34 | // Save the native id otherwise assign a unique id 35 | if (!uncachedElement.id) uncachedElement.id = uniqueId(); 36 | cachedElementId = uncachedElement.id; 37 | } 38 | return uncachedElement; 39 | }; 40 | }; 41 | 42 | // Wrap the button, video, and caption elements 43 | const currentResource = getResource(); 44 | currentResource.buttonParent = cacheElementWrapper(currentResource.buttonParent); 45 | currentResource.videoElement = cacheElementWrapper(currentResource.videoElement); 46 | if (currentResource.captionElement) { 47 | currentResource.captionElement = cacheElementWrapper(currentResource.captionElement); 48 | } 49 | }; -------------------------------------------------------------------------------- /src/common/scripts/captions.js: -------------------------------------------------------------------------------- 1 | import { info } from './logger.js' 2 | import { Browser, getBrowser, getResource } from './common.js' 3 | import { videoPlayingPictureInPicture, addPictureInPictureEventListener, removePictureInPictureEventListener } from './video.js' 4 | 5 | const TRACK_ID = 'PiPer_track'; 6 | 7 | let /** ?TextTrack */ track = null; 8 | let /** boolean */ captionsEnabled = false; 9 | let /** boolean */ showingCaptions = false; 10 | let /** boolean */ showingEmptyCaption = false; 11 | let /** string */ lastUnprocessedCaption = ''; 12 | 13 | /** 14 | * Disable closed caption support in Picture in Picture mode 15 | */ 16 | export const disableCaptions = function() { 17 | captionsEnabled = false; 18 | showingCaptions = false; 19 | processCaptions(); 20 | removePictureInPictureEventListener(pictureInPictureEventListener); 21 | 22 | info('Closed caption support disabled'); 23 | }; 24 | 25 | /** 26 | * Enable closed caption support in Picture in Picture mode 27 | * 28 | * @param {boolean=} ignoreNowPlayingCheck - assumes video isn't already playing Picture in Picture 29 | */ 30 | export const enableCaptions = function(ignoreNowPlayingCheck) { 31 | 32 | if (!getResource().captionElement) return; 33 | 34 | captionsEnabled = true; 35 | addPictureInPictureEventListener(pictureInPictureEventListener); 36 | 37 | info('Closed caption support enabled'); 38 | 39 | if (ignoreNowPlayingCheck) return; 40 | 41 | const video = /** @type {?HTMLVideoElement} */ (getResource().videoElement(true)); 42 | if (!video) return; 43 | showingCaptions = videoPlayingPictureInPicture(video); 44 | track = getCaptionTrack(video); 45 | processCaptions(); 46 | }; 47 | 48 | /** 49 | * Checks whether processing closed captions is required 50 | * 51 | * @return {boolean} 52 | */ 53 | export const shouldProcessCaptions = function() { 54 | return captionsEnabled && showingCaptions; 55 | }; 56 | 57 | /** 58 | * Gets caption track for video (creates or returns existing track as needed) 59 | * 60 | * @param {HTMLVideoElement} video - video element that will display captions 61 | * @return {TextTrack} 62 | */ 63 | const getCaptionTrack = function(video) { 64 | 65 | // Find existing caption track 66 | const allTracks = video.textTracks; 67 | for (let trackId = allTracks.length; trackId--;) { 68 | if (allTracks[trackId].label === TRACK_ID) { 69 | info('Existing caption track found'); 70 | return allTracks[trackId]; 71 | } 72 | } 73 | 74 | // Otherwise create new caption track 75 | info('Caption track created'); 76 | return video.addTextTrack('captions', TRACK_ID, 'en'); 77 | }; 78 | 79 | /** 80 | * Adds caption tracks to all video elements 81 | */ 82 | export const addVideoCaptionTracks = function() { 83 | const elements = document.getElementsByTagName('video'); 84 | for (let index = 0, element; element = elements[index]; index++) { 85 | getCaptionTrack(/** @type {?HTMLVideoElement} */ (element)); 86 | } 87 | }; 88 | 89 | /** 90 | * Toggles captions when video enters or exits Picture in Picture mode 91 | * 92 | * @param {HTMLVideoElement} video - target video element 93 | * @param {boolean} isPlayingPictureInPicture - true if video playing Picture in Picture 94 | */ 95 | const pictureInPictureEventListener = function(video, isPlayingPictureInPicture) { 96 | 97 | // Toggle display of the captions and prepare video if needed 98 | showingCaptions = isPlayingPictureInPicture; 99 | if (showingCaptions) { 100 | track = getCaptionTrack(video); 101 | track.mode = 'showing'; 102 | } 103 | lastUnprocessedCaption = ''; 104 | processCaptions(); 105 | 106 | info(`Video presentation mode changed (showingCaptions: ${showingCaptions})`); 107 | }; 108 | 109 | /** 110 | * Removes visible Picture in Picture mode captions 111 | * 112 | * @param {HTMLVideoElement} video - video element showing captions 113 | * @param {boolean=} workaround - apply Safari bug workaround 114 | */ 115 | const removeCaptions = function(video, workaround = true) { 116 | 117 | while (track.activeCues.length) { 118 | track.removeCue(track.activeCues[0]); 119 | } 120 | 121 | // Workaround Safari bug; 'removeCue' doesn't immediately remove captions shown in Picture in Picture mode 122 | if (getBrowser() == Browser.SAFARI && workaround && video && !showingEmptyCaption) { 123 | track.addCue(new VTTCue(video.currentTime, video.currentTime + 60, '')); 124 | showingEmptyCaption = true; 125 | } 126 | }; 127 | 128 | /** 129 | * Displays Picture in Picture mode caption 130 | * 131 | * @param {HTMLVideoElement} video - video element showing captions 132 | * @param {string} caption - a caption to display 133 | */ 134 | const addCaption = function(video, caption) { 135 | 136 | info(`Showing caption '${caption}'`); 137 | track.mode = 'showing'; 138 | track.addCue(new VTTCue(video.currentTime, video.currentTime + 60, caption)); 139 | 140 | if (getBrowser() == Browser.SAFARI) { 141 | showingEmptyCaption = false; 142 | } 143 | }; 144 | 145 | /** 146 | * Updates visible captions 147 | */ 148 | export const processCaptions = function() { 149 | 150 | // Get handles to caption and video elements 151 | const captionElement = getResource().captionElement(); 152 | const video = /** @type {?HTMLVideoElement} */ (getResource().videoElement()); 153 | 154 | // Remove Picture in Picture mode captions and show native captions if no longer showing captions or encountered an error 155 | if (!showingCaptions || !captionElement) { 156 | removeCaptions(video); 157 | if (captionElement) captionElement.style.visibility = ''; 158 | return; 159 | } 160 | 161 | // Otherwise ensure native captions remain hidden 162 | captionElement.style.visibility = 'hidden'; 163 | 164 | // Check if a new native caption needs to be processed 165 | const unprocessedCaption = captionElement.textContent; 166 | if (unprocessedCaption == lastUnprocessedCaption) return; 167 | lastUnprocessedCaption = unprocessedCaption; 168 | 169 | // Remove old captions and apply Safari bug fix if caption has no content as otherwise causes flicker 170 | removeCaptions(video, !unprocessedCaption); 171 | 172 | // Performance optimisation - early exit if caption has no content 173 | if (!unprocessedCaption) return; 174 | 175 | // Show correctly spaced and formatted Picture in Picture mode caption 176 | let caption = ''; 177 | const walk = document.createTreeWalker(captionElement, NodeFilter.SHOW_TEXT, null, false); 178 | while (walk.nextNode()) { 179 | const segment = walk.currentNode.nodeValue.trim(); 180 | if (segment) { 181 | const style = window.getComputedStyle(walk.currentNode.parentElement); 182 | if (style.fontStyle == 'italic') { 183 | caption += `${segment}`; 184 | } else if (style.textDecoration == 'underline') { 185 | caption += `${segment}`; 186 | } else { 187 | caption += segment; 188 | } 189 | caption += ' '; 190 | } else if (caption.charAt(caption.length - 1) != '\n') { 191 | caption += '\n'; 192 | } 193 | } 194 | caption = caption.trim(); 195 | addCaption(video, caption); 196 | }; -------------------------------------------------------------------------------- /src/common/scripts/common.js: -------------------------------------------------------------------------------- 1 | import { BROWSER } from './defines.js' 2 | import { warn } from './logger.js' 3 | 4 | /** @enum {number} - Enum for browser */ 5 | export const Browser = { 6 | UNKNOWN: 0, 7 | SAFARI: 1, 8 | CHROME: 2, 9 | }; 10 | 11 | /** 12 | * Returns current web browser 13 | * 14 | * @return {Browser} 15 | */ 16 | export const getBrowser = function() { 17 | if (BROWSER != Browser.UNKNOWN) { 18 | return /** @type {Browser} */ (BROWSER); 19 | } 20 | if (/Safari/.test(navigator.userAgent) && /Apple/.test(navigator.vendor)) { 21 | return Browser.SAFARI; 22 | } 23 | if (/Chrome/.test(navigator.userAgent) && /Google/.test(navigator.vendor)) { 24 | return Browser.CHROME; 25 | } 26 | return Browser.UNKNOWN; 27 | }; 28 | 29 | /** 30 | * @typedef {{ 31 | * buttonClassName: (string|undefined), 32 | * buttonDidAppear: (function():undefined|undefined), 33 | * buttonElementType: (string|undefined), 34 | * buttonExitImage: (string|undefined), 35 | * buttonHoverStyle: (string|undefined), 36 | * buttonImage: (string|undefined), 37 | * buttonInsertBefore: (function(Element):?Node|undefined), 38 | * buttonParent: function(boolean=):?Element, 39 | * buttonScale: (number|undefined), 40 | * buttonStyle: (string|undefined), 41 | * captionElement: (function(boolean=):?Element|undefined), 42 | * videoElement: function(boolean=):?Element, 43 | * }} 44 | */ 45 | let PiperResource; 46 | 47 | let /** ?PiperResource */ currentResource = null; 48 | 49 | /** 50 | * Returns the current resource 51 | * 52 | * @return {?PiperResource} 53 | */ 54 | export const getResource = function() { 55 | return currentResource; 56 | }; 57 | 58 | /** 59 | * Sets the current resource 60 | * 61 | * @param {?PiperResource} resource - a resource to set as current resource 62 | */ 63 | export const setResource = function(resource) { 64 | currentResource = resource; 65 | }; 66 | 67 | /** 68 | * Converts a relative path within an extension to a fully-qualified URL 69 | * 70 | * @param {string} path - a path to a resource 71 | * @return {string} 72 | */ 73 | export const getExtensionURL = function(path) { 74 | switch (getBrowser()) { 75 | case Browser.SAFARI: 76 | return safari.extension.baseURI + path; 77 | case Browser.CHROME: 78 | return chrome.runtime.getURL(path); 79 | case Browser.UNKNOWN: 80 | default: 81 | return path; 82 | } 83 | }; 84 | 85 | /** 86 | * Applies fix to bypass background DOM timer throttling 87 | */ 88 | export const bypassBackgroundTimerThrottling = function() { 89 | 90 | // Issue warning for unnecessary use of background timer throttling 91 | if (!currentResource.captionElement) { 92 | warn('Unnecessary bypassing of background timer throttling on page without caption support'); 93 | } 94 | 95 | const request = new XMLHttpRequest(); 96 | request.open('GET', getExtensionURL('scripts/fix.js')); 97 | request.onload = function() { 98 | const script = document.createElement('script'); 99 | script.setAttribute('type', 'module'); 100 | script.appendChild(document.createTextNode(request.responseText)); 101 | document.head.appendChild(script); 102 | }; 103 | request.send(); 104 | }; -------------------------------------------------------------------------------- /src/common/scripts/defines.js: -------------------------------------------------------------------------------- 1 | /** @define {number} - Flag used by closure compiler to set logging level */ 2 | export const LOGGING_LEVEL = 0; 3 | 4 | /** @define {number} - Flag used by closure compiler to target specific browser */ 5 | export const BROWSER = 0; -------------------------------------------------------------------------------- /src/common/scripts/externs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Externs file for PiPer used by Closure Compiler 3 | * @externs 4 | */ 5 | 6 | 7 | /* Safari Extension */ 8 | 9 | /** @const */ 10 | const safari = {}; 11 | 12 | /** @const */ 13 | safari.extension = {}; 14 | 15 | /** @type {string} */ 16 | safari.extension.baseURI; 17 | 18 | /** @type {string} */ 19 | HTMLVideoElement.prototype.webkitPresentationMode; 20 | 21 | /** @return {undefined} */ 22 | HTMLVideoElement.prototype.webkitSetPresentationMode = function(mode) {}; 23 | 24 | /** @type {string} */ 25 | TextTrack.prototype.label; 26 | 27 | /** @interface */ 28 | const SafariExtensionMessageEvent = function() {}; 29 | 30 | /** @type {*} */ 31 | SafariExtensionMessageEvent.prototype.message; 32 | 33 | /** @type {string} */ 34 | SafariExtensionMessageEvent.prototype.name; 35 | 36 | /** @type {EventTarget} */ 37 | SafariExtensionMessageEvent.prototype.target; 38 | 39 | /** @interface */ 40 | const SafariEventTarget = function() {}; 41 | 42 | /** @return {function(string,function(SafariExtensionMessageEvent),boolean=):undefined} */ 43 | SafariEventTarget.prototype.addEventListener = function(type, listener, capture) {}; 44 | 45 | /** @return {function(string,function(SafariExtensionMessageEvent),boolean=):undefined} */ 46 | SafariEventTarget.prototype.removeEventListener = function(type, listener, capture) {}; 47 | 48 | /** @type {!SafariEventTarget} */ 49 | safari.self; 50 | 51 | /** @type {function(string,*=):undefined} */ 52 | safari.extension.dispatchMessage = function(message, userInfo) {}; 53 | 54 | 55 | /* Legacy Safari Extension */ 56 | 57 | /** @const */ 58 | safari.extension.settings = {}; 59 | 60 | /** @return {undefined} */ 61 | safari.extension.settings.clear = function() {}; 62 | 63 | /** @type {!SafariEventTarget} */ 64 | safari.application; 65 | 66 | /** @const */ 67 | safari.self.tab = {}; 68 | 69 | /** @type {function(string,*=):undefined} */ 70 | safari.self.tab.dispatchMessage = function(message, userInfo) {}; 71 | 72 | /** @interface */ 73 | const SafariBrowserTab = function() {}; 74 | 75 | /** @type {SafariWebPageProxy} */ 76 | SafariBrowserTab.prototype.page; 77 | 78 | /** @interface */ 79 | const SafariWebPageProxy = function() {}; 80 | 81 | /** @type {function(string,*=):undefined} */ 82 | SafariWebPageProxy.prototype.dispatchMessage = function(message, userInfo) {}; 83 | 84 | 85 | /* Chrome Extension */ 86 | 87 | /** @const */ 88 | const chrome = {}; 89 | 90 | /** @const */ 91 | chrome.runtime = {}; 92 | 93 | /** @return {string} */ 94 | chrome.runtime.getURL = function(path) {}; 95 | 96 | /** @const */ 97 | chrome.runtime.onInstalled = {}; 98 | 99 | /** @return {undefined} */ 100 | chrome.runtime.onInstalled.addListener = function(callback) {}; 101 | 102 | /** @constructor */ 103 | chrome.runtime.Manifest = function() {}; 104 | 105 | /** @type {string} */ 106 | chrome.runtime.Manifest.prototype.version; 107 | 108 | /** @return {!chrome.runtime.Manifest} */ 109 | chrome.runtime.getManifest = function() {}; 110 | 111 | /** @const */ 112 | chrome.tabs = {}; 113 | 114 | /** @return {undefined} */ 115 | chrome.tabs.create = function(properties) {}; 116 | 117 | /** @return {Promise<*>} */ 118 | HTMLVideoElement.prototype.requestPictureInPicture = function() {}; 119 | 120 | /** @const */ 121 | chrome.extension = {}; 122 | 123 | /** @return {string} */ 124 | chrome.extension.getURL = function(path) {}; 125 | 126 | /** @const */ 127 | chrome.storage = {}; 128 | 129 | /** @const */ 130 | chrome.storage.sync = {}; 131 | 132 | /** @return {undefined} */ 133 | chrome.storage.sync.get = function(keys, callback) {}; 134 | 135 | /** @return {undefined} */ 136 | chrome.storage.sync.set = function(items, callback) {}; 137 | 138 | /** @return {undefined} */ 139 | chrome.storage.sync.clear = function(callback) {}; -------------------------------------------------------------------------------- /src/common/scripts/fix.js: -------------------------------------------------------------------------------- 1 | import { info } from './logger.js' 2 | import { videoPlayingPictureInPicture } from './video.js' 3 | 4 | let activeVideo = null; 5 | let timeoutId = 0; 6 | let /** !Object */ timeouts = {}; 7 | 8 | const /** !Array */ requests = []; 9 | const /** !Array */ callbacks = []; 10 | 11 | const originalSetTimeout = window.setTimeout; 12 | const originalClearTimeout = window.clearTimeout; 13 | const originalRequestAnimationFrame = window.requestAnimationFrame; 14 | 15 | /** 16 | * Tracks animation frame requests and forwards requests when page visible 17 | * 18 | * @param {function(number): undefined} callback - a requestAnimationFrame callback 19 | */ 20 | const trackAnimationFrameRequest = function(callback) { 21 | let request = 0; 22 | 23 | if (!activeVideo) { 24 | request = originalRequestAnimationFrame(callback); 25 | requests.push(request); 26 | } 27 | 28 | callbacks.push(callback); 29 | 30 | return request; 31 | }; 32 | window.requestAnimationFrame = trackAnimationFrameRequest; 33 | 34 | /** 35 | * Clears tracked animation frame requests on new frame 36 | */ 37 | const clearAnimationFrameRequests = function() { 38 | requests.length = 0; 39 | callbacks.length = 0; 40 | 41 | originalRequestAnimationFrame(clearAnimationFrameRequests); 42 | }; 43 | clearAnimationFrameRequests(); 44 | 45 | /** 46 | * Calls tracked animation frame requests and timeouts 47 | */ 48 | const callAnimationFrameRequestsAndTimeouts = function() { 49 | 50 | // Copy animation frame callbacks before calling to prevent endless looping 51 | const callbacksCopy = callbacks.slice(); 52 | callbacks.length = 0; 53 | 54 | // Call animation frame requests 55 | const timestamp = window.performance.now(); 56 | for (let callback; callback = callbacksCopy.pop();) { 57 | callback(timestamp); 58 | } 59 | 60 | // Copy timeouts to prevent endless looping 61 | const timeoutsCopy = timeouts; 62 | timeouts = {}; 63 | 64 | // Call elapsed timeouts 65 | for (let id in timeoutsCopy) { 66 | let timeout = timeoutsCopy[id]; 67 | if (timeout[0] <= timestamp) { 68 | if (typeof timeout[1] == "function") { 69 | timeout[1](); 70 | } else { 71 | eval(/** @type {string} */ (timeout[1])); 72 | } 73 | } else { 74 | timeouts[id] = timeout; 75 | } 76 | } 77 | }; 78 | 79 | /** 80 | * Avoids background throttling by invoking timeouts with media 'timeupdate' events 81 | * 82 | * @param {Function|TrustedScript|string} callback - a setTimeout callback 83 | * @param {number=} timeout - a delay in ms 84 | * @return {number} 85 | */ 86 | const unthrottledSetTimeout = function(callback, timeout) { 87 | const id = timeoutId++; 88 | timeouts[id.toString()] = [window.performance.now() + (timeout || 0), callback]; 89 | return id; 90 | }; 91 | 92 | /** 93 | * Clears queued timeouts to be invoked with media 'timeupdate' events 94 | * 95 | * @param {?number|undefined} id - an id returned by unthrottledSetTimeout 96 | */ 97 | const unthrottledClearTimeout = function(id) { 98 | if (id) delete timeouts[id.toString()]; 99 | }; 100 | 101 | /** 102 | * Bypasses background timer throttling when video playing picture in picture 103 | */ 104 | const bypassBackgroundTimerThrottling = function() { 105 | 106 | if (document.hidden) { 107 | 108 | const allVideos = document.querySelectorAll('video'); 109 | for (let videoId = allVideos.length; videoId--;) { 110 | const video = /** @type {?HTMLVideoElement} */ (allVideos[videoId]); 111 | if (videoPlayingPictureInPicture(video)) { 112 | activeVideo = video; 113 | break; 114 | } 115 | } 116 | if (!activeVideo) return; 117 | 118 | for (let request; request = requests.pop();) { 119 | window.cancelAnimationFrame(request); 120 | } 121 | 122 | window.setTimeout = unthrottledSetTimeout; 123 | window.clearTimeout = unthrottledClearTimeout; 124 | 125 | activeVideo.addEventListener('timeupdate', callAnimationFrameRequestsAndTimeouts); 126 | 127 | info('Bypassing background timer throttling'); 128 | 129 | } else if (activeVideo) { 130 | 131 | info('Finished bypassing background timer throttling'); 132 | 133 | window.setTimeout = originalSetTimeout; 134 | window.clearTimeout = originalClearTimeout; 135 | 136 | activeVideo.removeEventListener('timeupdate', callAnimationFrameRequestsAndTimeouts); 137 | 138 | activeVideo = null; 139 | 140 | for (let callbackId = callbacks.length; callbackId--;) { 141 | let request = originalRequestAnimationFrame(callbacks[callbackId]); 142 | requests.push(request); 143 | } 144 | 145 | const timestamp = window.performance.now(); 146 | for (let id in timeouts) { 147 | let timeout = timeouts[id]; 148 | originalSetTimeout(timeout[1], timeout[0] - timestamp); 149 | } 150 | timeouts = {}; 151 | } 152 | }; 153 | document.addEventListener('visibilitychange', bypassBackgroundTimerThrottling); -------------------------------------------------------------------------------- /src/common/scripts/localization.js: -------------------------------------------------------------------------------- 1 | import { error } from './logger.js' 2 | 3 | const localizations = {}; 4 | 5 | localizations['button-title'] = { 6 | 'en': 'Open Picture in Picture mode', 7 | 'de': 'Bild-in-Bild starten', 8 | 'nl': 'Beeld in beeld starten', 9 | 'fr': 'Démarrer Image dans l’image', 10 | }; 11 | 12 | localizations['donate'] = { 13 | 'en': 'Donate', 14 | 'de': 'Spenden', 15 | }; 16 | 17 | localizations['donate-small'] = { 18 | 'en': 'Small donation', 19 | }; 20 | 21 | localizations['donate-medium'] = { 22 | 'en': 'Medium donation', 23 | }; 24 | 25 | localizations['donate-large'] = { 26 | 'en': 'Grand donation', 27 | }; 28 | 29 | localizations['total-donations'] = { 30 | 'en': 'Total donations:', 31 | }; 32 | 33 | localizations['donate-error'] = { 34 | 'en': 'In-app purchase unavailable', 35 | }; 36 | 37 | localizations['report-bug'] = { 38 | 'en': 'Report a bug', 39 | 'de': 'Einen Fehler melden', 40 | }; 41 | 42 | localizations['options'] = { 43 | 'en': 'Options', 44 | }; 45 | 46 | localizations['install-thanks'] = { 47 | 'en': 'Thanks for adding PiPer!', 48 | }; 49 | 50 | localizations['enable'] = { 51 | 'en': 'Enable', 52 | }; 53 | 54 | localizations['safari-disabled-warning'] = { 55 | 'en': 'Extension is currently disabled, enable in Safari preferences', 56 | }; 57 | 58 | localizations['chrome-flags-open'] = { 59 | 'en': 'Open Chrome Flags', 60 | }; 61 | 62 | localizations['chrome-flags-warning'] = { 63 | 'en': 'Before you get started you need to enable the chrome flag [emphasis]"SurfaceLayer objects for videos"[/emphasis]', 64 | }; 65 | 66 | // Set English as the default fallback language 67 | const defaultLanguage = 'en'; 68 | 69 | /** 70 | * Returns a localized version of the string designated by the specified key 71 | * 72 | * @param {string} key - the key for a string 73 | * @param {string=} language - two-letter ISO 639-1 language code 74 | * @return {string} 75 | */ 76 | export const localizedString = function(key, language = navigator.language.substring(0, 2)) { 77 | 78 | // Get all localizations for key 79 | const /** Object */ localizationsForKey = localizations[key]; 80 | if (localizationsForKey) { 81 | 82 | // Get the users specific localization or fallback to default language 83 | let string = localizationsForKey[language] || localizationsForKey[defaultLanguage]; 84 | if (string) return string; 85 | } 86 | 87 | error(`No localized string found for key '${key}'`); 88 | return ''; 89 | }; 90 | 91 | /** 92 | * Returns a localized version of the string designated by the specified key with tags replaced 93 | * 94 | * @param {string} key - the key for a string 95 | * @param {Array>} replacements - an array of arrays containing pairs of tags and their replacement 96 | * @param {string=} language - two-letter ISO 639-1 language code 97 | * @return {string} 98 | */ 99 | export const localizedStringWithReplacements = function(key, replacements, language) { 100 | 101 | let string = localizedString(key, language); 102 | 103 | // Replace tags of the form [XXX] with directed replacements if needed 104 | for (let index = replacements.length; index--; ) { 105 | let replacement = replacements[index]; 106 | 107 | // Ensure tags do not contain special characters (this gets optimised away as opposed to escaping the tags with the associated performance cost) 108 | if (/[^-_0-9a-zA-Z\/]/.test(replacement[0])) { 109 | error(`Invalid characters used in localized string tag '${replacement[0]}'`); 110 | } 111 | 112 | const regex = new RegExp(`\\\[${replacement[0]}\\\]`, 'g'); 113 | string = string.replace(regex, replacement[1]); 114 | } 115 | 116 | return string; 117 | }; 118 | -------------------------------------------------------------------------------- /src/common/scripts/logger.js: -------------------------------------------------------------------------------- 1 | import { LOGGING_LEVEL } from './defines.js' 2 | 3 | const loggingPrefix = '[PiPer] '; 4 | 5 | /** @enum {number} - Enum for logging level */ 6 | export const LoggingLevel = { 7 | ALL: 0, 8 | TRACE: 10, 9 | INFO: 20, 10 | WARNING: 30, 11 | ERROR: 40, 12 | }; 13 | 14 | /** 15 | * Logs stack trace to console 16 | */ 17 | export const trace = (LoggingLevel.TRACE >= LOGGING_LEVEL) ? 18 | console.trace.bind(console) : function(){}; 19 | 20 | /** 21 | * Logs informational message to console 22 | */ 23 | export const info = (LoggingLevel.INFO >= LOGGING_LEVEL) ? 24 | console.info.bind(console, loggingPrefix) : function(){}; 25 | 26 | /** 27 | * Logs warning message to console 28 | */ 29 | export const warn = (LoggingLevel.WARNING >= LOGGING_LEVEL) ? 30 | console.warn.bind(console, loggingPrefix) : function(){}; 31 | 32 | /** 33 | * Logs error message to console 34 | */ 35 | export const error = (LoggingLevel.ERROR >= LOGGING_LEVEL) ? 36 | console.error.bind(console, loggingPrefix) : function(){}; -------------------------------------------------------------------------------- /src/common/scripts/main.js: -------------------------------------------------------------------------------- 1 | import { info } from './logger.js' 2 | import { Browser, getBrowser, getResource, setResource } from './common.js' 3 | import { addVideoElementListeners } from './video.js' 4 | import { resources } from './resources/index.js'; 5 | import { checkButton, addButton } from './button.js' 6 | import { shouldProcessCaptions, enableCaptions, processCaptions, addVideoCaptionTracks } from './captions.js' 7 | import { initialiseCaches } from './cache.js' 8 | 9 | /** 10 | * Tracks injected button and captions 11 | */ 12 | const mutationObserver = function() { 13 | const currentResource = getResource(); 14 | 15 | // Process video captions if needed 16 | if (shouldProcessCaptions()) processCaptions(); 17 | 18 | // Workaround Chrome's lack of an entering Picture in Picture mode event by monitoring all video elements 19 | if (getBrowser() == Browser.CHROME) addVideoElementListeners(); 20 | 21 | // Workaround Safari bug; captions are not displayed if the track is added after the video has loaded 22 | if (getBrowser() == Browser.SAFARI && currentResource.captionElement) addVideoCaptionTracks(); 23 | 24 | // Try adding the button to the page if needed 25 | if (checkButton()) return; 26 | const buttonParent = currentResource.buttonParent(); 27 | if (buttonParent) { 28 | addButton(buttonParent); 29 | if (currentResource.buttonDidAppear) currentResource.buttonDidAppear(); 30 | info('Picture in Picture button added to webpage'); 31 | } 32 | }; 33 | 34 | /** 35 | * Returns the first non-public subdomain from the current domain name 36 | * 37 | * @return {string|undefined} 38 | */ 39 | const getCurrentDomainName = function() { 40 | 41 | // Special case for local Plex Media Server access that always uses port 32400 42 | if (location.port == 32400) { 43 | return 'plex'; 44 | } else { 45 | // Remove subdomain and public suffix (far from comprehensive as only removes .X and .co.Y) 46 | return (location.hostname.match(/([^.]+)\.(?:com?\.)?[^.]+$/) || [])[1]; 47 | } 48 | }; 49 | 50 | const domainName = getCurrentDomainName(); 51 | 52 | if (domainName in resources) { 53 | info(`Matched site ${domainName} (${location})`); 54 | setResource(resources[domainName]); 55 | 56 | initialiseCaches(); 57 | 58 | if (getBrowser() == Browser.SAFARI) { 59 | enableCaptions(true); 60 | } 61 | 62 | const observer = new MutationObserver(mutationObserver); 63 | observer.observe(document, { 64 | childList: true, 65 | subtree: true, 66 | }); 67 | mutationObserver(); 68 | } 69 | -------------------------------------------------------------------------------- /src/common/scripts/resources/9now.js: -------------------------------------------------------------------------------- 1 | import { getResource } from './../common.js' 2 | 3 | export const domain = '9now'; 4 | 5 | export const resource = { 6 | buttonClassName: 'vjs-control vjs-button', 7 | buttonHoverStyle: /** CSS */ (` 8 | filter: brightness(50%) sepia(1) hue-rotate(167deg) saturate(253%) brightness(104%); 9 | `), 10 | buttonInsertBefore: function(/** Element */ parent) { 11 | return parent.querySelector('.vjs-fullscreen-control'); 12 | }, 13 | buttonParent: function() { 14 | return document.querySelector('.vjs-control-bar'); 15 | }, 16 | buttonScale: 0.7, 17 | buttonStyle: /** CSS */ (` 18 | order: 999999; 19 | cursor: pointer; 20 | height: 44px; 21 | width: 40px; 22 | `), 23 | captionElement: function() { 24 | const e = getResource().videoElement(); 25 | return e && e.parentElement.querySelector('.vjs-text-track-display'); 26 | }, 27 | videoElement: function() { 28 | return document.querySelector('video.vjs-tech'); 29 | }, 30 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/aktualne.js: -------------------------------------------------------------------------------- 1 | export const domain = 'aktualne'; 2 | 3 | export const resource = { 4 | buttonClassName: 'jw-icon jw-icon-inline jw-button-color jw-reset jw-icon-logo', 5 | buttonElementType: 'div', 6 | buttonHoverStyle: /** CSS */ (` 7 | filter: brightness(50%) sepia(1) hue-rotate(311deg) saturate(550%) brightness(49%) !important; 8 | `), 9 | buttonInsertBefore: function(/** Element */ parent) { 10 | return parent.lastChild; 11 | }, 12 | buttonParent: function() { 13 | return document.querySelector('.jw-controlbar-right-group'); 14 | }, 15 | buttonStyle: /** CSS */ (` 16 | width: 38px; 17 | filter: brightness(80%); 18 | `), 19 | videoElement: function() { 20 | return document.querySelector('video.jw-video'); 21 | }, 22 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/amazon.js: -------------------------------------------------------------------------------- 1 | export const domain = ['amazon', 'primevideo']; 2 | 3 | export const resource = { 4 | buttonHoverStyle: /** CSS */ (`opacity: 1 !important`), 5 | buttonInsertBefore: function(/** Element */ parent) { 6 | return parent.querySelector('.fullscreenButtonWrapper'); 7 | }, 8 | buttonParent: function() { 9 | const e = document.getElementById('dv-web-player'); 10 | return e && e.querySelector('.hideableTopButtons'); 11 | }, 12 | buttonStyle: /** CSS */ (` 13 | position: relative; 14 | left: 8px; 15 | width: 3vw; 16 | height: 2vw; 17 | min-width: 35px; 18 | min-height: 24px; 19 | border: 0px; 20 | padding: 0px; 21 | background-color: transparent; 22 | opacity: 0.8; 23 | `), 24 | captionElement: function() { 25 | const e = document.getElementById('dv-web-player'); 26 | return e && e.querySelector('.captions'); 27 | }, 28 | videoElement: function() { 29 | const e = document.querySelector('.rendererContainer'); 30 | return e && e.querySelector('video[width="100%"]'); 31 | }, 32 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/apple.js: -------------------------------------------------------------------------------- 1 | export const domain = 'apple'; 2 | 3 | /** 4 | * Returns nested shadow root 5 | * 6 | * @param {!Array} selectors 7 | * @return {?ShadowRoot} 8 | */ 9 | const getNestedShadowRoot = function(selectors) { 10 | let dom = document; 11 | for (const selector of selectors) { 12 | dom = /** @type {HTMLElement} */ (dom.querySelector(selector)); 13 | dom = dom && dom.shadowRoot; 14 | if (!dom) return null; 15 | } 16 | return /** @type {ShadowRoot} */ (dom); 17 | } 18 | 19 | export const resource = { 20 | buttonClassName: 'footer__control hydrated', 21 | buttonElementType: 'div', 22 | buttonHoverStyle: /** CSS */ (`opacity: 0.8 !important`), 23 | buttonInsertBefore: function(/** Element */ parent) { 24 | return parent.lastChild; 25 | }, 26 | buttonParent: function() { 27 | const internal = getNestedShadowRoot(["apple-tv-plus-player", 28 | "amp-video-player-internal"]); 29 | if (!internal) return; 30 | const fullscreenButton = internal.querySelector("amp-playback-controls-full-screen"); 31 | if (!fullscreenButton) return; 32 | return fullscreenButton.parentElement; 33 | }, 34 | buttonStyle: /** CSS */ (` 35 | transition: opacity 0.15s; 36 | cursor: pointer; 37 | opacity: 0.9; 38 | `), 39 | videoElement: function() { 40 | const video = getNestedShadowRoot(["apple-tv-plus-player", 41 | "amp-video-player-internal", 42 | "amp-video-player"]); 43 | if (!video) return; 44 | return video.querySelector('video'); 45 | }, 46 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/bbc.js: -------------------------------------------------------------------------------- 1 | export const domain = 'bbc'; 2 | 3 | export const resource = { 4 | buttonParent: function() { 5 | return null; 6 | }, 7 | captionElement: function() { 8 | return document.querySelector('.p_subtitlesContainer'); 9 | }, 10 | videoElement: function() { 11 | return document.querySelector('#mediaContainer video[src]'); 12 | }, 13 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/ceskatelevize.js: -------------------------------------------------------------------------------- 1 | export const domain = 'ceskatelevize'; 2 | 3 | export const resource = { 4 | buttonClassName: 'videoButtonShell dontHideControls cursorPointer focusableBtn', 5 | buttonElementType: 'div', 6 | buttonHoverStyle: /** CSS */ (` 7 | filter: brightness(50%) sepia(1) hue-rotate(170deg) saturate(250%) brightness(90%); 8 | `), 9 | buttonInsertBefore: function(/** Element */ parent) { 10 | return document.getElementById('fullScreenShell'); 11 | }, 12 | buttonScale: 1.2, 13 | buttonStyle: /** CSS */ (` 14 | width: 18px; 15 | height: 18px; 16 | display: inline-block; 17 | `), 18 | buttonParent: function() { 19 | return document.getElementById('videoButtons'); 20 | }, 21 | videoElement: function() { 22 | return document.getElementById('video'); 23 | }, 24 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/crunchyroll.js: -------------------------------------------------------------------------------- 1 | export const domain = 'crunchyroll'; 2 | 3 | export const resource = { 4 | buttonClassName: 'vjs-control vjs-button', 5 | buttonHoverStyle: /** CSS */ (`opacity: 1 !important`), 6 | buttonScale: 0.6, 7 | buttonStyle: /** CSS */ (` 8 | position: absolute; 9 | right: 100px; 10 | opacity: 0.75; 11 | cursor: pointer; 12 | `), 13 | buttonParent: function() { 14 | return document.querySelector('.vjs-control-bar'); 15 | }, 16 | videoElement: function() { 17 | return document.getElementById('player_html5_api'); 18 | }, 19 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/curiositystream.js: -------------------------------------------------------------------------------- 1 | import { Browser, getBrowser, getResource } from './../common.js' 2 | 3 | export const domain = 'curiositystream'; 4 | 5 | export const resource = { 6 | buttonClassName: 'vjs-control vjs-button', 7 | buttonDidAppear: function() { 8 | if (getBrowser() != Browser.SAFARI) return; 9 | const video = /** @type {?HTMLVideoElement} */ (getResource().videoElement()); 10 | const videoContainer = video.parentElement; 11 | video.addEventListener('webkitbeginfullscreen', function() { 12 | const height = Math.floor(100 * video.videoHeight / video.videoWidth) + 'vw'; 13 | const maxHeight = video.videoHeight + 'px'; 14 | videoContainer.style.setProperty('height', height, 'important'); 15 | videoContainer.style.setProperty('max-height', maxHeight); 16 | }); 17 | video.addEventListener('webkitendfullscreen', function() { 18 | videoContainer.style.removeProperty('height'); 19 | videoContainer.style.removeProperty('max-height'); 20 | }); 21 | }, 22 | buttonHoverStyle: /** CSS */ (`opacity: 1 !important`), 23 | buttonInsertBefore: function(/** Element */ parent) { 24 | return parent.lastChild; 25 | }, 26 | buttonParent: function() { 27 | const e = document.getElementById('main-player'); 28 | return e && e.querySelector('.vjs-control-bar'); 29 | }, 30 | buttonScale: 0.7, 31 | buttonStyle: /** CSS */ (` 32 | opacity: 0.8; 33 | cursor: pointer; 34 | `), 35 | videoElement: function() { 36 | return document.getElementById('main-player_html5_api'); 37 | }, 38 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/dazn.js: -------------------------------------------------------------------------------- 1 | export const domain = 'dazn'; 2 | 3 | export const resource = { 4 | buttonStyle: (` 5 | width: 1.5rem; 6 | height: 1.5rem; 7 | color: white; 8 | background: transparent; 9 | position: relative; 10 | border: none; 11 | outline: none; 12 | border-radius: 0; 13 | cursor: pointer; 14 | -webkit-appearance: none; 15 | margin: 0.5rem; 16 | z-index: 1; 17 | `), 18 | buttonInsertBefore: function(/** Element */ parent) { 19 | // The Live indicator might move/cover the PiP button, just place the PiP button before it 20 | const liveIndicator = document.querySelector('div[data-test-id^="PLAYER_LIVE_INDICATOR"]'); 21 | if (liveIndicator) { 22 | return liveIndicator; 23 | } 24 | return parent.lastChild; 25 | }, 26 | buttonParent: function() { 27 | return document.querySelector('div[data-test-id^="PLAYER_BAR"]'); 28 | }, 29 | videoElement: function() { 30 | return document.querySelector('div[data-test-id^="PLAYER_SOLUTION"] video'); 31 | } 32 | }; 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/common/scripts/resources/disneyplus.js: -------------------------------------------------------------------------------- 1 | export const domain = 'disneyplus'; 2 | 3 | export const resource = { 4 | buttonClassName: 'control-icon-btn', 5 | buttonInsertBefore: function(/** Element */ parent) { 6 | return document.querySelector('.fullscreen-icon'); 7 | }, 8 | buttonParent: function() { 9 | return document.querySelector('.controls__right'); 10 | }, 11 | videoElement: function() { 12 | return document.querySelector('video[src]'); 13 | }, 14 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/espn.js: -------------------------------------------------------------------------------- 1 | import { getButton } from './../button.js' 2 | 3 | export const domain = 'espn'; 4 | 5 | export const resource = { 6 | buttonClassName: 'media-icon', 7 | buttonDidAppear: function() { 8 | // Get localized button title and hide default tooltip 9 | const button = getButton(); 10 | const /** string */ title = button.title; 11 | button.title = ''; 12 | 13 | // Create stylized tooltip and add to DOM 14 | const tooltip = /** @type {HTMLElement} */ (document.createElement('div')); 15 | tooltip.className = 'control-tooltip'; 16 | tooltip.style.cssText = /** CSS */ (` 17 | right: 0px; 18 | bottom: 35px; 19 | transition: bottom 0.2s ease-out; 20 | `); 21 | tooltip.textContent = title; 22 | button.appendChild(tooltip); 23 | 24 | // Display stylized tooltip on mouseover 25 | button.addEventListener('mouseover', function() { 26 | button.classList.add('displaying'); 27 | tooltip.style.bottom = '75px'; 28 | }); 29 | button.addEventListener('mouseout', function() { 30 | button.classList.remove('displaying'); 31 | tooltip.style.bottom = '35px'; 32 | }); 33 | }, 34 | buttonElementType: 'div', 35 | buttonInsertBefore: function(/** Element */ parent) { 36 | return parent.lastChild; 37 | }, 38 | buttonParent: function() { 39 | return document.querySelector('.controls-right-horizontal'); 40 | }, 41 | buttonScale: 0.7, 42 | buttonStyle: /** CSS */ (` 43 | width: 44px; 44 | height: 44px; 45 | order: 4; 46 | `), 47 | captionElement: function() { 48 | return document.querySelector('.text-track-display'); 49 | }, 50 | videoElement: function() { 51 | return document.querySelector('video.js-video-content'); 52 | }, 53 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/eurosportplayer.js: -------------------------------------------------------------------------------- 1 | export const domain = 'eurosportplayer'; 2 | 3 | export const resource = { 4 | buttonElementType: 'div', 5 | buttonHoverStyle: /** CSS */ (`opacity: 1 !important`), 6 | buttonParent: function() { 7 | return document.querySelector('.controls-bar-right-section'); 8 | }, 9 | buttonScale: 0.9, 10 | buttonStyle: /** CSS */ (` 11 | height: 100%; 12 | margin-right: 15px; 13 | opacity: 0.8; 14 | cursor: pointer; 15 | `), 16 | videoElement: function() { 17 | return document.querySelector('.video-player__screen'); 18 | }, 19 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/fubotv.js: -------------------------------------------------------------------------------- 1 | export const domain = 'fubo'; 2 | 3 | export const resource = { 4 | buttonElementType: 'div', 5 | buttonInsertBefore: function(/** Element */ parent) { 6 | return parent.lastChild; 7 | }, 8 | buttonParent: function() { 9 | return document.querySelector('.css-ja7yk7'); 10 | }, 11 | buttonScale: 1.25, 12 | buttonStyle: /** CSS */ (` 13 | height: 24px; 14 | width: 25px; 15 | margin: 8px 10px 12px; 16 | cursor: pointer; 17 | `), 18 | videoElement: function() { 19 | return document.getElementById('bitmovinplayer-video-video'); 20 | }, 21 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/giantbomb.js: -------------------------------------------------------------------------------- 1 | export const domain = 'giantbomb'; 2 | 3 | export const resource = { 4 | buttonClassName: 'av-chrome-control', 5 | buttonElementType: 'div', 6 | buttonInsertBefore: function(/** Element */ parent) { 7 | return parent.querySelector('.js-vid-pin-wrap').nextSibling; 8 | }, 9 | buttonParent: function() { 10 | return document.querySelector('.av-controls--right'); 11 | }, 12 | buttonScale: 0.7, 13 | buttonStyle: /** CSS */ (` 14 | height: 100%; 15 | width: 30px; 16 | opacity: 1.0; 17 | cursor: pointer; 18 | `), 19 | videoElement: function() { 20 | return document.querySelector('video[id^="video_js-vid-player"]'); 21 | } 22 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/hulu.js: -------------------------------------------------------------------------------- 1 | import { getButton } from './../button.js' 2 | 3 | export const domain = 'hulu'; 4 | 5 | export const resource = { 6 | buttonDidAppear: function() { 7 | 8 | // Get localized button title and hide default tooltip 9 | const button = getButton(); 10 | const /** string */ title = button.title; 11 | button.title = ''; 12 | 13 | // Create stylized tooltip and add to DOM 14 | const tooltip = /** @type {HTMLElement} */ (document.createElement('div')); 15 | tooltip.className = 'button-tool-tips'; 16 | tooltip.style.cssText = /** CSS */ (` 17 | white-space: nowrap; 18 | padding: 0 5px; 19 | right: 0; 20 | `); 21 | tooltip.textContent = title.toUpperCase(); 22 | button.appendChild(tooltip); 23 | 24 | // Display stylized tooltip on mouseover 25 | button.addEventListener('mouseover', function() { 26 | tooltip.style.display = 'block'; 27 | }); 28 | button.addEventListener('mouseout', function() { 29 | tooltip.style.display = 'none'; 30 | }); 31 | }, 32 | buttonElementType: 'div', 33 | buttonHoverStyle: /** CSS */ (`opacity: 1.0 !important`), 34 | buttonInsertBefore: function(/** Element */ parent) { 35 | return document.querySelector('.controls__view-mode-button'); 36 | }, 37 | buttonParent: function() { 38 | return document.querySelector('#dash-player-container .controls__menus-right'); 39 | }, 40 | buttonStyle: /** CSS */ (` 41 | opacity: 0.7; 42 | cursor: pointer; 43 | width: 24px; 44 | `), 45 | captionElement: function() { 46 | return document.querySelector('.closed-caption-outband'); 47 | }, 48 | videoElement: function() { 49 | return document.querySelector('.video-player'); 50 | }, 51 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/littlethings.js: -------------------------------------------------------------------------------- 1 | export const domain = 'littlethings'; 2 | 3 | export const resource = { 4 | buttonClassName: 'jw-icon jw-icon-inline jw-button-color jw-reset jw-icon-logo', 5 | buttonElementType: 'div', 6 | buttonInsertBefore: function(/** Element */ parent) { 7 | return parent.lastChild; 8 | }, 9 | buttonParent: function() { 10 | return document.querySelector('.jw-controlbar-right-group'); 11 | }, 12 | buttonStyle: /** CSS */ (`width: 38px`), 13 | videoElement: function() { 14 | return document.querySelector('video.jw-video'); 15 | }, 16 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/mashable.js: -------------------------------------------------------------------------------- 1 | export const domain = 'mashable'; 2 | 3 | export const resource = { 4 | buttonClassName: 'jw-icon jw-icon-inline jw-button-color jw-reset jw-icon-logo', 5 | buttonElementType: 'div', 6 | buttonInsertBefore: function(/** Element */ parent) { 7 | return parent.lastChild; 8 | }, 9 | buttonParent: function() { 10 | return document.querySelector('.jw-controlbar-right-group'); 11 | }, 12 | buttonStyle: /** CSS */ (` 13 | top: -2px; 14 | width: 38px; 15 | `), 16 | videoElement: function() { 17 | return document.querySelector('video.jw-video'); 18 | }, 19 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/metacafe.js: -------------------------------------------------------------------------------- 1 | export const domain = 'metacafe'; 2 | 3 | export const resource = { 4 | buttonElementType: 'div', 5 | buttonInsertBefore: function(/** Element */ parent) { 6 | return parent.lastChild; 7 | }, 8 | buttonParent: function() { 9 | return document.querySelector('#player_place .tray'); 10 | }, 11 | buttonScale: 0.85, 12 | videoElement: function() { 13 | return document.querySelector('#player_place video'); 14 | }, 15 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/mixer.js: -------------------------------------------------------------------------------- 1 | export const domain = 'mixer'; 2 | 3 | export const resource = { 4 | buttonClassName: 'control', 5 | buttonElementType: 'div', 6 | buttonHoverStyle: /** CSS */ (`background: rgba(255, 255, 255, 0.08)`), 7 | buttonInsertBefore: function(/** Element */ parent) { 8 | return parent.lastChild.previousSibling; 9 | }, 10 | buttonParent: function() { 11 | return document.querySelector('.control-container .toolbar .right'); 12 | }, 13 | buttonScale: 0.65, 14 | buttonStyle: /** CSS */ (` 15 | width: 36px; 16 | height: 36px; 17 | border-radius: 50%; 18 | cursor: pointer; 19 | `), 20 | videoElement: function() { 21 | return document.querySelector('.control-container + video'); 22 | }, 23 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/mlb.js: -------------------------------------------------------------------------------- 1 | export const domain = 'mlb'; 2 | 3 | export const resource = { 4 | buttonScale: 0.7, 5 | buttonStyle: /** CSS */ (` 6 | border: 0px; 7 | background: transparent; 8 | filter: brightness(80%); 9 | `), 10 | buttonHoverStyle: /** CSS */ (`filter: brightness(120%) !important`), 11 | buttonParent: function() { 12 | return document.querySelector('.bottom-controls-right'); 13 | }, 14 | buttonInsertBefore: function(/** Element */ parent) { 15 | return parent.lastChild; 16 | }, 17 | videoElement: function() { 18 | return document.querySelector('.mlbtv-media-player video'); 19 | }, 20 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/netflix.js: -------------------------------------------------------------------------------- 1 | import { getResource } from './../common.js' 2 | 3 | export const domain = 'netflix'; 4 | 5 | export const resource = { 6 | buttonClassName: 'touchable PlayerControls--control-element nfp-button-control default-control-button', 7 | buttonHoverStyle: /** CSS */ (`transform: scale(1.2);`), 8 | buttonInsertBefore: function(/** Element */ parent) { 9 | return parent.lastChild; 10 | }, 11 | buttonParent: function() { 12 | return document.querySelector('.PlayerControlsNeo__button-control-row'); 13 | }, 14 | buttonScale: 0.7, 15 | buttonStyle: /** CSS */ (`min-width: 2.3em`), 16 | captionElement: function() { 17 | const e = getResource().videoElement(); 18 | return e && e.parentElement.querySelector('.player-timedtext'); 19 | }, 20 | videoElement: function() { 21 | return document.querySelector('.VideoContainer video'); 22 | }, 23 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/ocs.js: -------------------------------------------------------------------------------- 1 | export const domain = 'ocs'; 2 | 3 | export const resource = { 4 | buttonClassName: 'footer-elt fltr', 5 | buttonInsertBefore: function(/** Element */ parent) { 6 | return parent.querySelector('#togglePlay'); 7 | }, 8 | buttonParent: function() { 9 | return document.querySelector('.footer-block:last-child'); 10 | }, 11 | buttonScale: 1.2, 12 | buttonStyle: /** CSS */ (` 13 | display: block; 14 | width: 25px; 15 | height: 18px; 16 | margin-right: 10px; 17 | margin-bottom: -10px; 18 | padding: 0px; 19 | border: 0px; 20 | background-color: transparent; 21 | `), 22 | videoElement: function() { 23 | return document.getElementById('LgyVideoPlayer'); 24 | }, 25 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/openload.js: -------------------------------------------------------------------------------- 1 | export const domain = ['openload', 'oload']; 2 | 3 | export const resource = { 4 | buttonClassName: 'vjs-control vjs-button', 5 | buttonInsertBefore: function(/** Element */ parent) { 6 | return parent.lastChild; 7 | }, 8 | buttonParent: function() { 9 | return document.querySelector('.vjs-control-bar'); 10 | }, 11 | buttonScale: 0.6, 12 | buttonStyle: /** CSS */ (` 13 | left: 5px; 14 | cursor: pointer; 15 | `), 16 | videoElement: function() { 17 | return document.getElementById('olvideo_html5_api'); 18 | }, 19 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/pbs.js: -------------------------------------------------------------------------------- 1 | import { getResource } from './../common.js' 2 | import { videoPlayingPictureInPicture, togglePictureInPicture } from './../video.js' 3 | 4 | export const domain = 'pbs'; 5 | 6 | export const resource = { 7 | buttonClassName: 'jw-icon jw-icon-inline jw-button-color jw-reset', 8 | buttonDidAppear: function() { 9 | const fullscreenButton = document.querySelector('.jw-icon-fullscreen'); 10 | fullscreenButton.addEventListener('click', function() { 11 | const video = /** @type {?HTMLVideoElement} */ (getResource().videoElement()); 12 | if (videoPlayingPictureInPicture(video)) togglePictureInPicture(video); 13 | }); 14 | }, 15 | buttonElementType: 'div', 16 | buttonHoverStyle: /** CSS */ (`opacity: 1 !important`), 17 | buttonInsertBefore: function(/** Element */ parent) { 18 | return parent.lastChild; 19 | }, 20 | buttonParent: function() { 21 | return document.querySelector('.jw-button-container'); 22 | }, 23 | buttonScale: 0.6, 24 | buttonStyle: /** CSS */ (`opacity: 0.8`), 25 | videoElement: function() { 26 | return document.querySelector('.jw-video'); 27 | }, 28 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/periscope.js: -------------------------------------------------------------------------------- 1 | export const domain = ['periscope', 'pscp']; 2 | 3 | export const resource = { 4 | buttonClassName: 'Pill Pill--withIcon', 5 | buttonElementType: 'span', 6 | buttonHoverStyle: /** CSS */ (` 7 | opacity: 0.8 !important; 8 | filter: brightness(125%) !important; 9 | `), 10 | buttonInsertBefore: function(/** Element */ parent) { 11 | return parent.querySelector('.ShareBroadcast').nextSibling; 12 | }, 13 | buttonParent: function() { 14 | return document.querySelector('.VideoOverlayRedesign-BottomBar-Right'); 15 | }, 16 | buttonScale: 0.6, 17 | buttonStyle: /** CSS */ (` 18 | opacity: 0.5; 19 | filter: brightness(200%); 20 | `), 21 | videoElement: function() { 22 | return document.querySelector('.Video video'); 23 | }, 24 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/plex.js: -------------------------------------------------------------------------------- 1 | import { bypassBackgroundTimerThrottling } from './../common.js' 2 | 3 | export const domain = 'plex'; 4 | 5 | export const resource = { 6 | buttonDidAppear: function() { 7 | bypassBackgroundTimerThrottling(); 8 | }, 9 | buttonHoverStyle: /** CSS */ (`opacity: 1 !important`), 10 | buttonInsertBefore: function(/** Element */ parent) { 11 | return parent.lastChild; 12 | }, 13 | buttonParent: function() { 14 | const e = document.querySelector('div[class^="FullPlayerTopControls-topControls"]'); 15 | return /** @type {?Element} */ (e && e.lastChild); 16 | }, 17 | buttonScale: 2, 18 | buttonStyle: /** CSS */ (` 19 | position: relative; 20 | top: -3px; 21 | width: 30px; 22 | padding: 10px; 23 | border: 0px; 24 | background: transparent; 25 | opacity: 0.7; 26 | outline: 0; 27 | text-shadow: 0px 0px 4px rgba(0, 0, 0, 0.45); 28 | `), 29 | captionElement: function() { 30 | return document.querySelector('.libjass-subs'); 31 | }, 32 | videoElement: function() { 33 | return document.querySelector('video[class^="HTMLMedia-mediaElement"]'); 34 | }, 35 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/seznam.js: -------------------------------------------------------------------------------- 1 | export const domain = ['seznam', 'stream']; 2 | 3 | export const resource = { 4 | buttonClassName: 'sznp-ui-widget-box', 5 | buttonElementType: 'div', 6 | buttonHoverStyle: /** CSS */ (`transform: scale(1.05)`), 7 | buttonInsertBefore: function(/** Element */ parent) { 8 | return parent.lastChild; 9 | }, 10 | buttonParent: function() { 11 | return document.querySelector('.sznp-ui-ctrl-panel-layout-wrapper'); 12 | }, 13 | buttonScale: 0.75, 14 | buttonStyle: /** CSS */ (`cursor: pointer`), 15 | videoElement: function() { 16 | return document.querySelector('.sznp-ui-tech-video-wrapper video'); 17 | }, 18 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/streamable.js: -------------------------------------------------------------------------------- 1 | import { getButton } from './../button.js' 2 | 3 | export const domain = 'streamable'; 4 | 5 | export const resource = { 6 | buttonDidAppear: function() { 7 | const progressBar = document.getElementById('player-progress'); 8 | const progressBarStyle = window.getComputedStyle(progressBar); 9 | getButton().style.right = progressBarStyle.right; 10 | progressBar.style.right = (parseInt(progressBarStyle.right, 10) + 40) + 'px'; 11 | }, 12 | buttonElementType: 'div', 13 | buttonHoverStyle: /** CSS */ (`opacity: 1 !important`), 14 | buttonParent: function() { 15 | return document.querySelector('.player-controls-right'); 16 | }, 17 | buttonStyle: /** CSS */ (` 18 | position: absolute; 19 | bottom: 10px; 20 | height: 26px; 21 | width: 26px; 22 | cursor: pointer; 23 | opacity: 0.9; 24 | filter: drop-shadow(rgba(0, 0, 0, 0.5) 0px 0px 2px); 25 | `), 26 | videoElement: function() { 27 | return document.getElementById('video-player-tag'); 28 | }, 29 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/ted.js: -------------------------------------------------------------------------------- 1 | import { getButton } from './../button.js' 2 | 3 | export const domain = 'ted'; 4 | 5 | export const resource = { 6 | buttonClassName: 'z-i:0 pos:r bottom:0 hover/bg:white.7 b-r:.1 p:1 cur:p', 7 | buttonElementType: 'div', 8 | buttonInsertBefore: function(/** Element */ parent) { 9 | return parent.lastChild; 10 | }, 11 | buttonParent: function() { 12 | const playButton = document.querySelector('[aria-controls="video1"]'); 13 | return playButton.parentElement.parentElement; 14 | }, 15 | buttonDidAppear: function() { 16 | const img = getButton().querySelector('img'); 17 | img.classList.add('w:2'); 18 | img.classList.add('h:2'); 19 | }, 20 | videoElement: function() { 21 | return document.querySelector('video[id^="ted-player-"]'); 22 | } 23 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/theonion.js: -------------------------------------------------------------------------------- 1 | export const domain = 'theonion'; 2 | 3 | export const resource = { 4 | buttonClassName: 'jw-icon jw-icon-inline jw-button-color jw-reset jw-icon-logo', 5 | buttonElementType: 'div', 6 | buttonInsertBefore: function(/** Element */ parent) { 7 | return parent.lastChild; 8 | }, 9 | buttonParent: function() { 10 | return document.querySelector('.jw-controlbar-right-group'); 11 | }, 12 | buttonStyle: /** CSS */ (` 13 | top: -2px; 14 | left: 10px; 15 | width: 38px; 16 | `), 17 | videoElement: function() { 18 | return document.querySelector('video.jw-video'); 19 | }, 20 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/twitch.js: -------------------------------------------------------------------------------- 1 | import { getResource } from './../common.js' 2 | import { getButton } from './../button.js' 3 | import { videoPlayingPictureInPicture, togglePictureInPicture } from './../video.js' 4 | 5 | export const domain = 'twitch'; 6 | 7 | export const resource = { 8 | buttonClassName: 'tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-button-icon tw-button-icon--overlay tw-core-button tw-core-button--overlay tw-inline-flex tw-relative tw-tooltip-wrapper', 9 | buttonDidAppear: function() { 10 | // Add tooltip 11 | const button = getButton(); 12 | const title = button.title; 13 | button.title = ''; 14 | const tooltip = /** @type {HTMLElement} */ (document.createElement('div')); 15 | tooltip.className = 'tw-tooltip tw-tooltip--align-right tw-tooltip--up'; 16 | tooltip.appendChild(document.createTextNode(title)); 17 | button.appendChild(tooltip); 18 | 19 | // Fix issues with fullscreen when activated while video playing Picture-in-Picture 20 | const fullscreenButton = document.querySelector("[data-a-target='player-fullscreen-button']"); 21 | if (!fullscreenButton) return; 22 | fullscreenButton.addEventListener('click', function() { 23 | const video = /** @type {?HTMLVideoElement} */ (getResource().videoElement()); 24 | if (videoPlayingPictureInPicture(video)) togglePictureInPicture(video); 25 | }); 26 | }, 27 | buttonInsertBefore: function(/** Element */ parent) { 28 | return parent.lastChild; 29 | }, 30 | buttonParent: function() { 31 | return document.querySelector('.player-controls__right-control-group,.player-buttons-right'); 32 | }, 33 | buttonScale: 0.8, 34 | captionElement: function() { 35 | return document.querySelector('.player-captions-container'); 36 | }, 37 | videoElement: function() { 38 | return document.querySelector('video[src]'); 39 | }, 40 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/udemy.js: -------------------------------------------------------------------------------- 1 | import { getButton } from './../button.js' 2 | 3 | export const domain = 'udemy'; 4 | 5 | export const resource = { 6 | buttonClassName: 'btn', 7 | buttonHoverStyle: /** CSS */ (`opacity: 1 !important`), 8 | buttonInsertBefore: function(/** Element */ parent) { 9 | return document.querySelector('button[aria-label="Fullscreen"]'); 10 | }, 11 | buttonParent: function() { 12 | return document.querySelector('div[class^="control-bar--control-bar--"]'); 13 | }, 14 | buttonScale: 0.8, 15 | buttonStyle: /** CSS */ (` 16 | width: 3em; 17 | height: 3em; 18 | padding: 0; 19 | opacity: 0.8; 20 | `), 21 | captionElement: function() { 22 | return document.querySelector('div[class^="captions-display--captions-container"]'); 23 | }, 24 | videoElement: function() { 25 | return document.querySelector('video.vjs-tech'); 26 | }, 27 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/ustream.js: -------------------------------------------------------------------------------- 1 | export const domain = 'ustream'; 2 | 3 | export const resource = { 4 | buttonClassName: 'component shown', 5 | buttonElementType: 'div', 6 | buttonHoverStyle: /** CSS */ (` 7 | opacity: 1 !important; 8 | filter: drop-shadow(0px 0px 5px rgba(255, 255, 255, 0.5)); 9 | `), 10 | buttonInsertBefore: function(/** Element */ parent) { 11 | return parent.lastChild; 12 | }, 13 | buttonScale: 0.8, 14 | buttonStyle: /** CSS */ (` 15 | opacity: 0.7; 16 | `), 17 | buttonParent: function() { 18 | return document.getElementById('controlPanelRight'); 19 | }, 20 | videoElement: function() { 21 | return document.querySelector('#ViewerContainer video'); 22 | }, 23 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/vevo.js: -------------------------------------------------------------------------------- 1 | export const domain = 'vevo'; 2 | 3 | export const resource = { 4 | buttonClassName: 'player-control', 5 | buttonInsertBefore: function(/** Element */ parent) { 6 | return parent.lastChild; 7 | }, 8 | buttonParent: function() { 9 | return document.querySelector('#control-bar .right-controls'); 10 | }, 11 | buttonScale: 0.7, 12 | buttonStyle: /** CSS */ (` 13 | border: 0px; 14 | background: transparent; 15 | `), 16 | videoElement: function() { 17 | return document.getElementById('html5-player'); 18 | }, 19 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/vice.js: -------------------------------------------------------------------------------- 1 | export const domain = 'vice'; 2 | 3 | export const resource = { 4 | buttonClassName: 'vp__controls__icon__popup__container', 5 | buttonElementType: 'div', 6 | buttonInsertBefore: function(/** Element */ parent) { 7 | return parent.lastChild; 8 | }, 9 | buttonParent: function() { 10 | return document.querySelector('.vp__controls__icons'); 11 | }, 12 | buttonScale: 0.6, 13 | buttonStyle: /** CSS */ (`top: -11px`), 14 | videoElement: function() { 15 | return document.querySelector('video.jw-video'); 16 | }, 17 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/vid.js: -------------------------------------------------------------------------------- 1 | export const domain = 'vid'; 2 | 3 | export const resource = { 4 | buttonInsertBefore: function(/** Element */ parent) { 5 | return parent.lastChild; 6 | }, 7 | buttonParent: function() { 8 | return document.querySelector('.vjs-control-bar'); 9 | }, 10 | buttonScale: 0.7, 11 | buttonStyle: /** CSS */ (` 12 | position: relative; 13 | top: -2px; 14 | left: 9px; 15 | padding: 0px; 16 | margin: 0px; 17 | `), 18 | videoElement: function() { 19 | return document.getElementById('video_player_html5_api'); 20 | }, 21 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/viervijfzes.js: -------------------------------------------------------------------------------- 1 | import { getButton } from './../button.js' 2 | 3 | export const domain = ['vijf', 'vier', 'zes']; 4 | 5 | export const resource = { 6 | buttonClassName: 'vjs-control vjs-button', 7 | buttonDidAppear: function() { 8 | // Move fullscreen button to the right so the pip button appears left of it 9 | const fullScreenButton = document.getElementsByClassName('vjs-fullscreen-control')[0]; 10 | fullScreenButton.style.order = 10; 11 | }, 12 | buttonParent: function() { 13 | return document.getElementsByClassName('vjs-control-bar')[0]; 14 | }, 15 | buttonStyle: /** CSS */ (` 16 | text-indent: 0! important; 17 | margin-left: 10px; 18 | order: 9; 19 | `), 20 | videoElement: function() { 21 | return document.querySelector('video[preload="metadata"]'); 22 | }, 23 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/vk.js: -------------------------------------------------------------------------------- 1 | export const domain = 'vk'; 2 | 3 | export const resource = { 4 | buttonClassName: 'videoplayer_btn', 5 | buttonElementType: 'div', 6 | buttonInsertBefore: function(/** Element */ parent) { 7 | return document.querySelector('div.videoplayer_btn_fullscreen'); 8 | }, 9 | buttonStyle: /** CSS */ (` 10 | width: 24px; 11 | height: 45px; 12 | padding: 0 8px; 13 | `), 14 | buttonParent: function() { 15 | return document.querySelector('div.videoplayer_controls'); 16 | }, 17 | videoElement: function() { 18 | return document.querySelector('video.videoplayer_media_provider'); 19 | }, 20 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/vrt.js: -------------------------------------------------------------------------------- 1 | export const domain = 'vrt'; 2 | 3 | export const resource = { 4 | buttonClassName: 'vuplay-control', 5 | buttonInsertBefore: function(/** Element */ parent) { 6 | return parent.lastChild; 7 | }, 8 | buttonParent: function() { 9 | return document.getElementsByClassName('vuplay-control-right')[0]; 10 | }, 11 | captionElement: function() { 12 | return document.querySelector('.theoplayer-texttracks'); 13 | }, 14 | buttonStyle: /** CSS */ (` 15 | width: 30px; 16 | height: 47px; 17 | padding: 0; 18 | position: relative; 19 | top: -9px; 20 | right: 8px; 21 | `), 22 | videoElement: function() { 23 | return document.querySelector('video[preload="metadata"]'); 24 | }, 25 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/vrv.js: -------------------------------------------------------------------------------- 1 | import { getResource, bypassBackgroundTimerThrottling } from './../common.js' 2 | import { getButton } from './../button.js' 3 | import { videoPlayingPictureInPicture, togglePictureInPicture } from './../video.js' 4 | 5 | export const domain = 'vrv'; 6 | 7 | export const resource = { 8 | buttonClassName: 'vjs-control vjs-button', 9 | buttonDidAppear: function() { 10 | const neighbourButton = getButton().nextSibling; 11 | neighbourButton.addEventListener('click', function() { 12 | const video = /** @type {?HTMLVideoElement} */ (getResource().videoElement()); 13 | if (videoPlayingPictureInPicture(video)) togglePictureInPicture(video); 14 | }); 15 | bypassBackgroundTimerThrottling(); 16 | }, 17 | buttonHoverStyle: /** CSS */ (`opacity: 1 !important`), 18 | buttonInsertBefore: function(/** Element */ parent) { 19 | return parent.lastChild; 20 | }, 21 | buttonParent: function() { 22 | return document.querySelector('.vjs-control-bar'); 23 | }, 24 | buttonScale: 0.6, 25 | buttonStyle: /** CSS */ (` 26 | position: absolute; 27 | right: 114px; 28 | width: 50px; 29 | cursor: pointer; 30 | opacity: 0.6; 31 | `), 32 | captionElement: function() { 33 | return document.querySelector('.libjass-subs'); 34 | }, 35 | videoElement: function() { 36 | return document.getElementById('player_html5_api'); 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/common/scripts/resources/yeloplay.js: -------------------------------------------------------------------------------- 1 | import { getResource } from './../common.js' 2 | 3 | export const domain = 'yeloplay'; 4 | 5 | export const resource = { 6 | buttonClassName: 'button', 7 | buttonDidAppear: function() { 8 | const parent = getResource().buttonParent(); 9 | parent.style.width = '210px'; 10 | }, 11 | buttonHoverStyle: /** CSS */ (`opacity: 1 !important`), 12 | buttonInsertBefore: function(/** Element */ parent) { 13 | return document.getElementsByTagName('player-fullscreen-button')[0]; 14 | }, 15 | buttonParent: function() { 16 | return document.getElementsByClassName('buttons')[0]; 17 | }, 18 | buttonScale: 0.8, 19 | buttonStyle: /** CSS */ (` 20 | margin-bottom: -10px; 21 | margin-left: 10px; 22 | width: 50px; 23 | cursor: pointer; 24 | opacity: 0.8; 25 | height: 40px !important; 26 | margin-bottom: 0px !important; 27 | `), 28 | videoElement: function() { 29 | return document.querySelector('video[src]'); 30 | }, 31 | }; -------------------------------------------------------------------------------- /src/common/scripts/resources/youtube.js: -------------------------------------------------------------------------------- 1 | import { Browser, getBrowser, getResource, bypassBackgroundTimerThrottling } from './../common.js' 2 | import { getButton } from './../button.js' 3 | import { enableCaptions, disableCaptions, shouldProcessCaptions } from './../captions.js' 4 | 5 | export const domain = ['youtube', 'youtu']; 6 | 7 | export const resource = { 8 | buttonClassName: 'ytp-button', 9 | buttonDidAppear: function() { 10 | const button = getButton(); 11 | const neighbourButton = /** @type {?HTMLElement} */ (button.nextSibling); 12 | const /** string */ title = button.title; 13 | const /** string */ neighbourTitle = neighbourButton.title; 14 | button.title = ''; 15 | button.addEventListener('mouseover', function() { 16 | neighbourButton.title = title; 17 | neighbourButton.dispatchEvent(new Event('mouseover')); 18 | }); 19 | button.addEventListener('mouseout', function() { 20 | neighbourButton.dispatchEvent(new Event('mouseout')); 21 | neighbourButton.title = neighbourTitle; 22 | }); 23 | bypassBackgroundTimerThrottling(); 24 | 25 | // Workaround Safari bug; old captions persist in Picture in Picture mode when MediaSource buffers change 26 | if (getBrowser() == Browser.SAFARI) { 27 | const video = /** @type {?HTMLVideoElement} */ (getResource().videoElement()); 28 | let captionsVisible = false; 29 | const navigateStart = function() { 30 | captionsVisible = shouldProcessCaptions(); 31 | if (captionsVisible) disableCaptions(); 32 | }; 33 | const navigateFinish = function() { 34 | if (captionsVisible) enableCaptions(); 35 | }; 36 | window.addEventListener('spfrequest', navigateStart); 37 | window.addEventListener('spfdone', navigateFinish); 38 | window.addEventListener('yt-navigate-start', navigateStart); 39 | window.addEventListener('yt-navigate-finish', navigateFinish); 40 | } 41 | }, 42 | buttonInsertBefore: function(/** Element */ parent) { 43 | return parent.lastChild; 44 | }, 45 | buttonParent: function() { 46 | return document.querySelector('.ytp-right-controls'); 47 | }, 48 | buttonScale: 0.68, 49 | captionElement: function() { 50 | return document.querySelector('.caption-window'); 51 | }, 52 | videoElement: function() { 53 | return document.querySelector('video.html5-main-video'); 54 | }, 55 | }; -------------------------------------------------------------------------------- /src/common/scripts/video.js: -------------------------------------------------------------------------------- 1 | import { info } from './logger.js' 2 | import { Browser, getBrowser, getResource } from './common.js' 3 | 4 | const CHROME_PLAYING_PIP_ATTRIBUTE = 'data-playing-picture-in-picture'; 5 | 6 | const /** !Array */ eventListeners = []; 7 | 8 | /** 9 | * Toggles video Picture in Picture 10 | * 11 | * @param {HTMLVideoElement} video - video element to toggle Picture in Picture mode 12 | */ 13 | export const togglePictureInPicture = function(video) { 14 | const playingPictureInPicture = videoPlayingPictureInPicture(video); 15 | switch (getBrowser()) { 16 | case Browser.SAFARI: 17 | if (playingPictureInPicture) { 18 | video.webkitSetPresentationMode('inline'); 19 | } else { 20 | video.webkitSetPresentationMode('picture-in-picture'); 21 | } 22 | break; 23 | case Browser.CHROME: 24 | if (playingPictureInPicture) { 25 | // Workaround Chrome content scripts being unable to call 'exitPictureInPicture' directly 26 | const script = document.createElement('script'); 27 | script.textContent = 'document.exitPictureInPicture()'; 28 | document.head.appendChild(script); 29 | script.remove(); 30 | } else { 31 | // Force enable Picture in Picture mode support 32 | video.removeAttribute('disablepictureinpicture'); 33 | 34 | video.requestPictureInPicture(); 35 | } 36 | break; 37 | case Browser.UNKNOWN: 38 | default: 39 | break; 40 | } 41 | }; 42 | 43 | /** 44 | * Adds a Picture in Picture event listener 45 | * 46 | * @param {function(HTMLVideoElement, boolean)} listener - an event listener to add 47 | */ 48 | export const addPictureInPictureEventListener = function(listener) { 49 | const index = eventListeners.indexOf(listener); 50 | if (index == -1) { 51 | eventListeners.push(listener); 52 | } 53 | 54 | if (getBrowser() == Browser.SAFARI) { 55 | document.addEventListener('webkitpresentationmodechanged', videoPresentationModeChanged, { 56 | capture: true, 57 | }); 58 | } 59 | }; 60 | 61 | /** 62 | * Removes a Picture in Picture event listener 63 | * 64 | * @param {function(HTMLVideoElement,boolean)} listener - an event listener to remove 65 | */ 66 | export const removePictureInPictureEventListener = function(listener) { 67 | const index = eventListeners.indexOf(listener); 68 | if (index > -1) { 69 | eventListeners.splice(index, 1); 70 | } 71 | 72 | if (getBrowser() == Browser.SAFARI && eventListeners.length == 0) { 73 | document.removeEventListener('webkitpresentationmodechanged', videoPresentationModeChanged); 74 | } 75 | }; 76 | 77 | /** 78 | * Dispatches a Picture in Picture event 79 | * 80 | * @param {HTMLVideoElement} video - target video element 81 | */ 82 | const dispatchPictureInPictureEvent = function(video) { 83 | 84 | // Ignore events from other video elements e.g. adverts 85 | const expectedVideo = getResource().videoElement(true); 86 | if (video != expectedVideo) return; 87 | 88 | const isPlayingPictureInPicture = videoPlayingPictureInPicture(video); 89 | if (isPlayingPictureInPicture) { 90 | info('Video entering Picture in Picture mode'); 91 | } else { 92 | info('Video leaving Picture in Picture mode'); 93 | } 94 | 95 | // Call event listeners using a copy to prevent possiblity of endless looping 96 | const eventListenersCopy = eventListeners.slice(); 97 | for (let listener; listener = eventListenersCopy.pop();) { 98 | listener(video, isPlayingPictureInPicture); 99 | } 100 | } 101 | 102 | /** 103 | * Dispatches a Picture in Picture event for every 'webkitpresentationmodechanged' event 104 | * 105 | * @param {!Event} event - a webkitpresentationmodechanged event 106 | */ 107 | const videoPresentationModeChanged = function(event) { 108 | const video = /** @type {HTMLVideoElement} */ (event.target); 109 | dispatchPictureInPictureEvent(video); 110 | }; 111 | 112 | /** 113 | * Returns true if video is playing Picture in Picture 114 | * 115 | * @param {HTMLVideoElement} video - video element to test 116 | * @return {boolean} 117 | */ 118 | export const videoPlayingPictureInPicture = function(video) { 119 | switch (getBrowser()) { 120 | case Browser.SAFARI: 121 | return video.webkitPresentationMode == 'picture-in-picture'; 122 | case Browser.CHROME: 123 | return video.hasAttribute(CHROME_PLAYING_PIP_ATTRIBUTE); 124 | case Browser.UNKNOWN: 125 | default: 126 | return false; 127 | } 128 | }; 129 | 130 | /** 131 | * Sets Picture in Picture attribute and toggles captions on entering Picture in Picture mode 132 | * 133 | * @param {!Event} event - an enterpictureinpicture event 134 | */ 135 | const videoDidEnterPictureInPicture = function(event) { 136 | const video = /** @type {HTMLVideoElement} */ (event.target); 137 | 138 | // Set playing in Picture in Picture mode attribute and dispatch event 139 | video.setAttribute(CHROME_PLAYING_PIP_ATTRIBUTE, true); 140 | dispatchPictureInPictureEvent(video); 141 | 142 | // Remove Picture in Picture attribute and dispatch event on leaving Picture in Picture mode 143 | video.addEventListener('leavepictureinpicture', function(event) { 144 | video.removeAttribute(CHROME_PLAYING_PIP_ATTRIBUTE); 145 | dispatchPictureInPictureEvent(video); 146 | }, { once: true }); 147 | }; 148 | 149 | /** 150 | * Adds Picture in Picture event listeners to all video elements 151 | */ 152 | export const addVideoElementListeners = function() { 153 | const elements = document.getElementsByTagName('video'); 154 | for (let index = 0, element; element = elements[index]; index++) { 155 | element.addEventListener('enterpictureinpicture', videoDidEnterPictureInPicture); 156 | } 157 | }; 158 | -------------------------------------------------------------------------------- /src/safari-legacy/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Author 6 | Adam Marcus 7 | Builder Version 8 | 12602.4.8 9 | CFBundleDisplayName 10 | PiPer 11 | CFBundleIdentifier 12 | com.amarcus.safari.piper 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleShortVersionString 16 | 0.0 17 | CFBundleVersion 18 | 0 19 | Chrome 20 | 21 | Global Page 22 | global.html 23 | 24 | Content 25 | 26 | Scripts 27 | 28 | End 29 | 30 | scripts/main.js 31 | scripts/legacy.js 32 | 33 | 34 | 35 | Description 36 | Adds Picture in Picture functionality to Youtube, Netflix, Amazon Video, Twitch, and more! 37 | DeveloperIdentifier 38 | BQ6Q24MF9X 39 | ExtensionInfoDictionaryVersion 40 | 1.0 41 | Permissions 42 | 43 | Website Access 44 | 45 | Include Secure Pages 46 | 47 | Level 48 | All 49 | 50 | 51 | Update Manifest URL 52 | https://s3.amazonaws.com/piper-extension/update.plist 53 | Website 54 | https://github.com/amarcu5/PiPer/ 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/safari-legacy/global.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PiPer 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/safari-legacy/scripts/background.js: -------------------------------------------------------------------------------- 1 | const messageHandler = function(/** SafariExtensionMessageEvent */ messageEvent) { 2 | switch (messageEvent.name) { 3 | case 'getUpgradeAlertShown': 4 | const setting = /** @type {string|undefined} */ (safari.extension.settings['upgradeAlertShown']); 5 | const target = /** @type {SafariBrowserTab} */ (messageEvent.target); 6 | target.page.dispatchMessage('upgradeAlertShownResponse', parseInt(setting || '0', 10)); 7 | break; 8 | case 'setUpgradeAlertShown': 9 | safari.extension.settings['upgradeAlertShown'] = messageEvent.message; 10 | break; 11 | } 12 | } 13 | safari.application.addEventListener('message', messageHandler, false); 14 | -------------------------------------------------------------------------------- /src/safari-legacy/scripts/legacy.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | let /** boolean */ upgradeAlertShown = false; 4 | 5 | /** 6 | * Returns localized legacy upgrade alert message 7 | * 8 | * @return {string} 9 | */ 10 | const localizedUpgradeAlertMessage = function() { 11 | const language = navigator.language.substring(0, 2); 12 | switch (language) { 13 | case 'it': 14 | return 'Apple finirà presto il supporto per questa versione di PiPer. Esegui l\'upgrade alla versione per [url]Mac App Store[/url] ora'; 15 | case 'es': 16 | return 'Apple terminará el soporte para esta versión de PiPer pronto. Por favor actualice a la versión de [url]Mac App Store[/url] ahora'; 17 | case 'de': 18 | return 'Apple wird den Support für diese Version von PiPer in Kürze beenden. Aktualisieren Sie jetzt auf die [url]Mac App Store-Version[/url]'; 19 | case 'nl': 20 | return 'Apple zal binnenkort de ondersteuning voor deze versie van PiPer beëindigen. Upgrade nu naar de [url]Mac App Store-versie[/url]'; 21 | case 'fr': 22 | return 'Apple va bientôt mettre fin au support de cette version de PiPer. Veuillez passer à la version [url]Mac App Store[/url] maintenant'; 23 | case 'pt': 24 | return 'A Apple encerrará o suporte para esta versão do PiPer em breve. Por favor, atualize para a versão [url]Mac App Store[/url] agora'; 25 | case 'en': 26 | default: 27 | return 'Apple will end support for this version of PiPer soon. Please upgrade to the [url]Mac App Store version[/url] now'; 28 | } 29 | }; 30 | 31 | /** 32 | * Shows alert 33 | * 34 | * @param {string} message - a message to display 35 | * @param {function()} callback - a function called after alert dismissed 36 | */ 37 | const showAlert = function(message, callback) { 38 | const alert = document.createElement('div'); 39 | alert.style.cssText = /** CSS */ (` 40 | position: fixed; 41 | top: 30px; 42 | left: 50%; 43 | transform: translateX(-50%); 44 | width: calc(100% - 80px); 45 | max-width: 600px; 46 | border-radius: 5px; 47 | background-color: #E66; 48 | padding: 10px; 49 | z-index: 9999; 50 | font-family: -apple-system; 51 | line-height: 1.1; 52 | color: white; 53 | `); 54 | 55 | const image = /** @type {HTMLImageElement} */ (document.createElement('img')); 56 | image.src = safari.extension.baseURI + 'images/default.svg'; 57 | image.style.cssText = /** CSS */ (` 58 | float: left; 59 | width: 25px; 60 | height: 25px; 61 | margin: 5px; 62 | `); 63 | alert.appendChild(image); 64 | 65 | const close = document.createElement('div'); 66 | close.innerHTML = '×'; 67 | close.style.cssText = /** CSS */ (` 68 | float: right; 69 | width: 25px; 70 | margin: 0px 5px; 71 | font-size: 30px; 72 | text-align: center; 73 | opacity: 0.6; 74 | cursor: pointer; 75 | `); 76 | alert.appendChild(close); 77 | 78 | const content = document.createElement('div'); 79 | content.innerHTML = `PiPer
${message}`; 80 | content.style.cssText = /** CSS */ (` 81 | font-size: 16px; 82 | margin: 0px 45px; 83 | `); 84 | alert.appendChild(content); 85 | 86 | close.addEventListener('click', function() { 87 | document.body.removeChild(alert); 88 | callback(); 89 | }); 90 | 91 | document.body.appendChild(alert); 92 | }; 93 | 94 | /** 95 | * Shows upgrade to Mac App Store alert if needed 96 | * 97 | * @param {number} dismissedTimestamp - timestamp upgrade alert was last dismissed in milliseconds 98 | */ 99 | const showUpgradeAlertIfNeeded = function(dismissedTimestamp) { 100 | const currentTimestamp = Date.now(); 101 | 102 | let /** number */ alertInterval; 103 | if (currentTimestamp >= 1556665200000) { // 2019-05-01 104 | alertInterval = 3.6e+6; // hourly 105 | } else if (currentTimestamp >= 1554073200000) { // 2019-04-01 106 | alertInterval = 8.64e+7; // daily 107 | } else if (currentTimestamp >= 1551398400000) { // 2019-03-01 108 | alertInterval = 6.048e+8; // weekly 109 | } else { 110 | alertInterval = 2.628e+9; // monthly 111 | } 112 | 113 | if (upgradeAlertShown || currentTimestamp - dismissedTimestamp < alertInterval) { 114 | return; 115 | } 116 | 117 | const message = localizedUpgradeAlertMessage() 118 | .replace('[url]', '') 119 | .replace('[/url]', ''); 120 | 121 | showAlert(message, function() { 122 | safari.self.tab.dispatchMessage('setUpgradeAlertShown', currentTimestamp); 123 | }); 124 | 125 | upgradeAlertShown = true; 126 | }; 127 | 128 | /** 129 | * Handles Safari 'upgradeAlertShownResponse' messages from global page and shows alert if needed 130 | * 131 | * @param {SafariExtensionMessageEvent} messageEvent - a Safari extension message 132 | */ 133 | const messageHandler = function(/** SafariExtensionMessageEvent */ messageEvent) { 134 | if (messageEvent.name === 'upgradeAlertShownResponse') { 135 | safari.self.removeEventListener('message', messageHandler, false); 136 | const dismissedTimestamp = /** @type {number} */ (messageEvent.message); 137 | showUpgradeAlertIfNeeded(dismissedTimestamp); 138 | } 139 | } 140 | 141 | // Listen for entering Picture in Picture mode events and request last upgrade alert shown time 142 | document.addEventListener('webkitpresentationmodechanged', function(event){ 143 | const video = /** @type {HTMLVideoElement} */ (event.target); 144 | if (video.webkitPresentationMode == 'picture-in-picture') { 145 | safari.self.addEventListener('message', messageHandler, false); 146 | safari.self.tab.dispatchMessage('getUpgradeAlertShown'); 147 | } 148 | }, { capture: true }); 149 | 150 | })(); -------------------------------------------------------------------------------- /src/safari-legacy/update.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Extension Updates 6 | 7 | 8 | CFBundleIdentifier 9 | com.amarcus.safari.piper 10 | CFBundleShortVersionString 11 | 1.0.4 12 | CFBundleVersion 13 | 234 14 | Developer Identifier 15 | BQ6Q24MF9X 16 | URL 17 | https://s3.amazonaws.com/piper-extension/PiPer.safariextz 18 | Update From Gallery 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/safari/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PiPer App 4 | // 5 | // Created by Adam Marcus on 19/07/2018. 6 | // Copyright © 2018 Adam Marcus. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | func applicationDidFinishLaunching(_ aNotification: Notification) { 15 | // Preload donation in-app purchases 16 | DonationManager.shared.getDonationProducts() 17 | } 18 | 19 | func applicationWillTerminate(_ aNotification: Notification) { 20 | } 21 | 22 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 23 | return true; 24 | } 25 | 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/safari/App/ConfettiView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfettiView.swift 3 | // PiPer 4 | // 5 | // Created by Adam Marcus on 22/11/2018. 6 | // Copyright © 2018 Adam Marcus. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ConfettiView: NSView { 12 | 13 | private static var colors = [ 14 | NSColor(red:0.95, green:0.40, blue:0.27, alpha:1.0), 15 | NSColor(red:1.00, green:0.78, blue:0.36, alpha:1.0), 16 | NSColor(red:0.48, green:0.78, blue:0.64, alpha:1.0), 17 | NSColor(red:0.30, green:0.76, blue:0.85, alpha:1.0), 18 | NSColor(red:0.58, green:0.39, blue:0.55, alpha:1.0) 19 | ] 20 | 21 | private var emitter: CAEmitterLayer! 22 | 23 | required public init?(coder aDecoder: NSCoder) { 24 | super.init(coder: aDecoder) 25 | setup() 26 | } 27 | 28 | public override init(frame: CGRect) { 29 | super.init(frame: frame) 30 | setup() 31 | } 32 | 33 | private func setup() { 34 | 35 | // Set up confetti emitter layer 36 | emitter = CAEmitterLayer() 37 | emitter.emitterShape = CAEmitterLayerEmitterShape.line 38 | emitter.birthRate = 0 39 | 40 | // Set confetti emitter layer position/size and respond to view frame changes 41 | self.postsFrameChangedNotifications = true 42 | NotificationCenter.default.addObserver( 43 | self, 44 | selector: #selector(setEmitterFrame), 45 | name: NSView.frameDidChangeNotification, 46 | object: self) 47 | setEmitterFrame() 48 | 49 | // Add emitter cells for each confetti color 50 | var cells = [CAEmitterCell]() 51 | for color in ConfettiView.colors { 52 | cells.append(confettiWithColor(color: color)) 53 | } 54 | emitter.emitterCells = cells 55 | 56 | // Add confetti emitter layer 57 | self.wantsLayer = true 58 | self.layer!.addSublayer(emitter) 59 | } 60 | 61 | @objc private func setEmitterFrame() { 62 | 63 | // Position confetti emitter offscreen and size to fit view 64 | emitter.emitterPosition = CGPoint(x: frame.size.width * 0.5, 65 | y: frame.size.height + 32) 66 | emitter.emitterSize = CGSize(width: frame.size.width, 67 | height: 1) 68 | } 69 | 70 | public func dropConfetti() { 71 | 72 | // Set confetti emitter to show particles start spawning from emitter position 73 | emitter.beginTime = CACurrentMediaTime() 74 | 75 | // Animate confetti emitter birth rate to spawn a burst of confetti 76 | let birthRateDecayAnimation = CABasicAnimation() 77 | birthRateDecayAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) 78 | birthRateDecayAnimation.duration = CFTimeInterval(1.0) 79 | birthRateDecayAnimation.fromValue = NSNumber(value: 1.0) 80 | birthRateDecayAnimation.toValue = NSNumber(value: 0.0) 81 | emitter.add(birthRateDecayAnimation, forKey: "birthRate") 82 | } 83 | 84 | // Generate a diamond CGImage representing confetti 85 | private func getDiamondImage() -> CGImage? { 86 | let image = NSImage(size: CGSize(width: 24, height: 32), flipped: false) { _ in 87 | let path = NSBezierPath() 88 | path.move(to: NSPoint(x: 12, y: 0)) 89 | path.line(to: NSPoint(x: 24, y: 16)) 90 | path.line(to: NSPoint(x: 12, y: 32)) 91 | path.line(to: NSPoint(x: 0, y: 16)) 92 | path.line(to: NSPoint(x: 12, y: 0)) 93 | NSColor.white.setFill() 94 | path.fill() 95 | return true 96 | } 97 | return image.cgImage(forProposedRect: nil, context: nil, hints: nil) 98 | } 99 | 100 | // Setup an confetti emitter cell with a specific color 101 | private func confettiWithColor(color: NSColor) -> CAEmitterCell { 102 | let confetti = CAEmitterCell() 103 | confetti.birthRate = 400.0 104 | confetti.lifetime = 10.0 105 | confetti.alphaSpeed = -1.0 / confetti.lifetime 106 | confetti.color = color.cgColor 107 | confetti.velocity = 200.0 108 | confetti.velocityRange = 200.0 109 | confetti.emissionRange = CGFloat(Double.pi * 0.5) 110 | confetti.spin = 3.0 111 | confetti.spinRange = 3.0 112 | confetti.scale = 0.15 113 | confetti.scaleRange = 0.15 114 | confetti.contents = getDiamondImage() 115 | return confetti 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/safari/App/DonateContainerViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DonateContainerViewController.swift 3 | // PiPer 4 | // 5 | // Created by Adam Marcus on 19/11/2018. 6 | // Copyright © 2018 Adam Marcus. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class DonateContainerViewController: NSTabViewController { 12 | 13 | @IBOutlet var donateProgressViewTabItem: NSTabViewItem! 14 | @IBOutlet var donateViewTabItem: NSTabViewItem! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | let donateViewController = donateViewTabItem.viewController as! DonateViewController 20 | let donateProgressViewController = donateProgressViewTabItem.viewController as! DonateProgressViewController 21 | 22 | let productsAvaliable = DonationManager.shared.donationProductsAvaliable() 23 | 24 | donateViewController.loadProducts(completionHandler: { 25 | success in 26 | if !success { 27 | donateProgressViewController.showError() 28 | } else if !productsAvaliable { 29 | self.tabView.selectTabViewItem(self.donateViewTabItem) 30 | } 31 | }) 32 | 33 | if productsAvaliable { 34 | self.tabView.selectTabViewItem(self.donateViewTabItem) 35 | } 36 | } 37 | 38 | override func tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?) { 39 | super.tabView(tabView, didSelect: tabViewItem) 40 | 41 | if let window = self.view.window, let contentSize = tabViewItem?.view?.fittingSize { 42 | let newWindowSize = window.frameRect(forContentRect: NSRect(origin: CGPoint.zero, size: contentSize)).size 43 | 44 | var frame = window.frame 45 | frame.origin.x = frame.origin.x + (frame.size.width - newWindowSize.width) * 0.5 46 | frame.origin.y = frame.origin.y + (frame.size.height - newWindowSize.height) 47 | frame.size = newWindowSize 48 | 49 | window.animator().setFrame(frame, display: false) 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/safari/App/DonateProgressViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DonateProgressViewController.swift 3 | // PiPer 4 | // 5 | // Created by Adam Marcus on 19/11/2018. 6 | // Copyright © 2018 Adam Marcus. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class DonateProgressViewController: NSViewController { 12 | 13 | @IBOutlet var progressIndicator: NSProgressIndicator! 14 | @IBOutlet var errorMessage: LocalizedTextField! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | progressIndicator.startAnimation(self) 19 | } 20 | 21 | func showError() { 22 | progressIndicator.stopAnimation(self) 23 | errorMessage.isHidden = false 24 | } 25 | 26 | @IBAction func dismissClicked(sender: NSButton) { 27 | self.parent?.dismiss(self.parent) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/safari/App/DonateViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DonateViewController.swift 3 | // PiPer 4 | // 5 | // Created by Adam Marcus on 17/11/2018. 6 | // Copyright © 2018 Adam Marcus. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class DonateViewController: NSViewController { 12 | 13 | @IBOutlet var totalDonations: NSTextField! 14 | 15 | @objc dynamic var donationProducts = [DonationProduct]() 16 | 17 | @IBOutlet var donationTableView: NSTableView! 18 | @IBOutlet var tableViewHeightConstraint: NSLayoutConstraint! 19 | @IBOutlet var tableViewWidthConstraint: NSLayoutConstraint! 20 | 21 | @IBOutlet var confettiView: ConfettiView! 22 | 23 | func loadProducts(completionHandler: @escaping (_ success: Bool) -> ()) { 24 | DonationManager.shared.getDonationProducts(completionHandler: { 25 | productsResponse, error in 26 | if let products = productsResponse { 27 | self.donationProducts = products 28 | completionHandler(true) 29 | } else { 30 | completionHandler(false) 31 | } 32 | }) 33 | } 34 | 35 | override func viewDidLoad() { 36 | super.viewDidLoad() 37 | 38 | self.donationTableView.postsFrameChangedNotifications = true 39 | NotificationCenter.default.addObserver( 40 | self, 41 | selector: #selector(sizeDonationTableViewToFitContents), 42 | name: NSView.frameDidChangeNotification, 43 | object: self.donationTableView) 44 | sizeDonationTableViewToFitContents() 45 | 46 | updateTotalDonations() 47 | } 48 | 49 | @objc private func sizeDonationTableViewToFitContents() { 50 | var computedWidth: CGFloat = 0 51 | for row in 0.. () 32 | public typealias BuyDonationProductCompletionHandler = (_ transaction: SKPaymentTransaction) -> () 33 | 34 | static let shared = DonationManager() 35 | 36 | static private let identifiers = Set([ 37 | "com.amarcus.PiPer.donation.1", 38 | "com.amarcus.PiPer.donation.3", 39 | "com.amarcus.PiPer.donation.10" 40 | ]) 41 | 42 | static private let totalDonationsKey = "com.amarcus.PiPer.totalDonations" 43 | 44 | private var donationProducts:[DonationProduct]? 45 | 46 | private var smallestDonation: NSDecimalNumber? 47 | private var priceFormatter: NumberFormatter? 48 | 49 | private init() {} 50 | 51 | func getDonationProducts(completionHandler: @escaping GetDonationProductsCompletionHandler = {_,_ in}) { 52 | 53 | if let products = self.donationProducts { 54 | completionHandler(products, .none) 55 | return 56 | } 57 | 58 | guard InAppPurchaseHelper.shared.canMakePayments() else { 59 | let error = NSError(domain: "com.amarcus.PiPer.DonationManager", 60 | code: 0, 61 | userInfo: [NSLocalizedDescriptionKey: "Payments are unavailable"]) 62 | completionHandler(nil, error) 63 | return 64 | } 65 | 66 | InAppPurchaseHelper.shared.requestProducts(identifiers:DonationManager.identifiers, completionHandler: { 67 | productResponse, error in 68 | 69 | DispatchQueue.main.async { 70 | guard error == nil else { 71 | completionHandler(nil, error) 72 | return 73 | } 74 | if let response = productResponse { 75 | let sortedProducts = response.products.sorted { (product1, product2) -> Bool in 76 | return product1.price.compare(product2.price) == .orderedAscending 77 | } 78 | 79 | if let cheapestProduct = sortedProducts.first { 80 | let priceFormatter = NumberFormatter() 81 | priceFormatter.numberStyle = .currency 82 | priceFormatter.locale = cheapestProduct.priceLocale 83 | self.priceFormatter = priceFormatter 84 | self.smallestDonation = cheapestProduct.price 85 | } 86 | 87 | self.donationProducts = sortedProducts.map { 88 | DonationProduct(name: $0.localizedTitle, 89 | price: self.localizedStringForPrice($0.price)!, 90 | emoticon: self.emoticonForPrice($0.price)!, 91 | product: $0) 92 | } 93 | completionHandler(self.donationProducts, .none) 94 | 95 | } 96 | } 97 | }) 98 | } 99 | 100 | var totalDonations: NSDecimalNumber { 101 | set { 102 | guard let priceFormatter = self.priceFormatter else { 103 | return 104 | } 105 | var totalDonationsDictionary = NSUbiquitousKeyValueStore.default.dictionary(forKey: DonationManager.totalDonationsKey) ?? [String:String]() 106 | let numberString = newValue.description(withLocale: [NSLocale.Key.decimalSeparator: "."]) 107 | totalDonationsDictionary[priceFormatter.currencyCode] = numberString 108 | NSUbiquitousKeyValueStore.default.set(totalDonationsDictionary, forKey: DonationManager.totalDonationsKey) 109 | NSUbiquitousKeyValueStore.default.synchronize() 110 | } 111 | get { 112 | guard let priceFormatter = self.priceFormatter, 113 | let totalDonationsDictionary = NSUbiquitousKeyValueStore.default.dictionary(forKey: DonationManager.totalDonationsKey), 114 | let numberString = totalDonationsDictionary[priceFormatter.currencyCode] as? String else { 115 | return NSDecimalNumber.zero 116 | } 117 | return NSDecimalNumber(string: numberString) 118 | } 119 | } 120 | 121 | func buyDonationProduct(_ donationProduct: DonationProduct, completionHandler: @escaping BuyDonationProductCompletionHandler = {_ in}) { 122 | 123 | InAppPurchaseHelper.shared.buyProduct(donationProduct.product, completionHandler: { 124 | transaction in 125 | DispatchQueue.main.async { 126 | self.totalDonations = self.totalDonations.adding(donationProduct.product.price) 127 | completionHandler(transaction) 128 | } 129 | }) 130 | } 131 | 132 | func donationProductsAvaliable() -> Bool { 133 | return self.donationProducts != nil 134 | } 135 | 136 | func localizedStringForPrice(_ price: NSDecimalNumber) -> String? { 137 | guard let priceFormatter = self.priceFormatter else { 138 | return nil 139 | } 140 | return priceFormatter.string(from:price) ?? "\(price)" 141 | } 142 | 143 | func emoticonForPrice(_ decimalPrice: NSDecimalNumber) -> String? { 144 | guard let baseUnit = self.smallestDonation?.doubleValue else { 145 | return nil 146 | } 147 | 148 | // Get seasonal emoticons 149 | let emoticons = Array({ 150 | () -> String in 151 | switch Calendar.current.dateComponents([.month, .weekdayOrdinal, .weekday, .day], from: Date()) { 152 | case let date where date.month == 1 && date.day == 1: // New Years 153 | return "🕛🥂🍾🎊" 154 | case let date where date.month == 2 && date.day == 14: // Valentine's Day 155 | return "🥀🌹💋💘" 156 | case let date where date.month == 10 && date.day == 31: // Halloween 157 | return "💀👻🧙‍♀️🎃" 158 | case let date where date.month == 11 && date.weekdayOrdinal == 4 && date.weekday == 5: // Thanksgiving 159 | return "🍂🍴🍗🇺🇸" 160 | case let date where date.month == 12 && date.day == 25: // Christmas 161 | return "🥶🎄🎁🎅" 162 | default: 163 | return "😢🙂😃😍" 164 | } 165 | }()).map { String($0) } 166 | 167 | // Select emoticon based on price 168 | let price = decimalPrice.doubleValue 169 | if price < baseUnit { 170 | return emoticons[0] 171 | } else if price < 3 * baseUnit { 172 | return emoticons[1] 173 | } else if price < 10 * baseUnit { 174 | return emoticons[2] 175 | } else { 176 | return emoticons[3] 177 | } 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /src/safari/App/Icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amarcu5/PiPer/51d585da9cdf6bf31156fbb3f9b2df15e0e1749a/src/safari/App/Icon.icns -------------------------------------------------------------------------------- /src/safari/App/InAppPurchaseHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InAppPurchaseHelper.swift 3 | // PiPer 4 | // 5 | // Created by Adam Marcus on 18/11/2018. 6 | // Copyright © 2018 Adam Marcus. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import StoreKit 11 | 12 | class InAppPurchaseHelper: NSObject { 13 | 14 | static let shared = InAppPurchaseHelper() 15 | 16 | public typealias RequestProductsCompletionHandler = (_ response: SKProductsResponse?, _ error: Error?) -> () 17 | public typealias BuyProductCompletionHandler = (_ transaction: SKPaymentTransaction) -> () 18 | 19 | private var productsRequestsInProgress = [SKRequest:RequestProductsCompletionHandler]() 20 | private var purchasesInProgress = [SKPayment:BuyProductCompletionHandler]() 21 | private let paymentQueue = SKPaymentQueue.default() 22 | 23 | private override init() { 24 | super.init() 25 | self.paymentQueue.add(self) 26 | } 27 | deinit { 28 | self.paymentQueue.remove(self) 29 | } 30 | 31 | func requestProducts(identifiers: Set, completionHandler: @escaping RequestProductsCompletionHandler) { 32 | let request = SKProductsRequest(productIdentifiers: identifiers) 33 | self.productsRequestsInProgress[request] = completionHandler 34 | request.delegate = self 35 | request.start() 36 | } 37 | 38 | func buyProduct(_ product: SKProduct, completionHandler: @escaping BuyProductCompletionHandler) { 39 | let payment = SKPayment(product: product) 40 | self.purchasesInProgress[payment] = completionHandler 41 | self.paymentQueue.add(payment) 42 | } 43 | 44 | func canMakePayments() -> Bool { 45 | return SKPaymentQueue.canMakePayments() 46 | } 47 | 48 | } 49 | 50 | extension InAppPurchaseHelper: SKProductsRequestDelegate { 51 | 52 | func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { 53 | if let completionHandler = self.productsRequestsInProgress[request] { 54 | completionHandler(response, .none) 55 | } 56 | self.productsRequestsInProgress.removeValue(forKey: request) 57 | } 58 | 59 | func request(_ request: SKRequest, didFailWithError error: Error) { 60 | if let completionHandler = self.productsRequestsInProgress[request] { 61 | completionHandler(.none, error) 62 | } 63 | self.productsRequestsInProgress.removeValue(forKey: request) 64 | } 65 | } 66 | 67 | 68 | extension InAppPurchaseHelper: SKPaymentTransactionObserver { 69 | 70 | func paymentQueue(_ queue: SKPaymentQueue, 71 | updatedTransactions transactions: [SKPaymentTransaction]) { 72 | for transaction in transactions { 73 | switch transaction.transactionState { 74 | case .purchased, .failed, .restored: 75 | if let completionHandler = self.purchasesInProgress[transaction.payment] { 76 | completionHandler(transaction) 77 | self.purchasesInProgress.removeValue(forKey: transaction.payment) 78 | } 79 | queue.finishTransaction(transaction) 80 | break 81 | case .purchasing, .deferred: 82 | break 83 | } 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/safari/App/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | PiPer 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIconFile 12 | Icon.icns 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 0.0.0 23 | CFBundleVersion 24 | 0 25 | LSApplicationCategoryType 26 | public.app-category.utilities 27 | LSMinimumSystemVersion 28 | $(MACOSX_DEPLOYMENT_TARGET) 29 | NSHumanReadableCopyright 30 | Copyright © 2018 Adam Marcus. All rights reserved. 31 | NSMainStoryboardFile 32 | Main 33 | NSPrincipalClass 34 | NSApplication 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/safari/App/LocalizationManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizationManager.swift 3 | // PiPer 4 | // 5 | // Created by Adam Marcus on 12/10/2018. 6 | // Copyright © 2018 Adam Marcus. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import JavaScriptCore 11 | 12 | class LocalizationManager { 13 | 14 | let context: JSContext = JSContext() 15 | let languageCode: String 16 | 17 | static let `default` = LocalizationManager() 18 | 19 | init(withLanguageCode languageCode: String? = Locale.current.languageCode) { 20 | 21 | self.languageCode = languageCode ?? "" 22 | 23 | #if DEBUG 24 | context.exceptionHandler = { _, value in 25 | print("Localization JavaScriptCore error: \(value!)") 26 | } 27 | #endif 28 | 29 | context.evaluateScript("const window = {};") 30 | 31 | if let extensionBundleURL = ResourceHelper.extensionBundleURL { 32 | let localizationFile = extensionBundleURL.appendingPathComponent("scripts/localization-bridge.js").path 33 | 34 | let localizationFileContents = try? String(contentsOfFile: localizationFile, 35 | encoding: String.Encoding.utf8) 36 | 37 | if let localizationScript = localizationFileContents { 38 | context.evaluateScript(localizationScript) 39 | } 40 | } 41 | } 42 | 43 | func localizedString(forKey key: String) -> String { 44 | let string = context.evaluateScript("window.localizedString('\(key)', '\(languageCode)');").toString() 45 | return string ?? "" 46 | } 47 | 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/safari/App/LocalizedButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizedButton.swift 3 | // PiPer 4 | // 5 | // Created by Adam Marcus on 13/10/2018. 6 | // Copyright © 2018 Adam Marcus. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @IBDesignable 12 | class LocalizedButton: NSButton { 13 | 14 | override init(frame frameRect: NSRect) { 15 | super.init(frame: frameRect) 16 | localizeTitle() 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | super.init(coder: aDecoder) 21 | localizeTitle() 22 | } 23 | 24 | override func prepareForInterfaceBuilder() { 25 | super.prepareForInterfaceBuilder() 26 | localizeTitle() 27 | } 28 | 29 | func localizeTitle() { 30 | self.title = LocalizationManager.default.localizedString(forKey:self.title) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/safari/App/LocalizedTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizedTextField.swift 3 | // PiPer 4 | // 5 | // Created by Adam Marcus on 13/10/2018. 6 | // Copyright © 2018 Adam Marcus. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @IBDesignable 12 | class LocalizedTextField: NSTextField { 13 | 14 | override init(frame frameRect: NSRect) { 15 | super.init(frame: frameRect) 16 | localizeValue() 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | super.init(coder: aDecoder) 21 | localizeValue() 22 | } 23 | 24 | override func prepareForInterfaceBuilder() { 25 | super.prepareForInterfaceBuilder() 26 | localizeValue() 27 | } 28 | 29 | func localizeValue() { 30 | self.stringValue = LocalizationManager.default.localizedString(forKey:self.stringValue) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/safari/App/PiPer_App.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.icloud-container-identifiers 6 | 7 | com.apple.developer.ubiquity-kvstore-identifier 8 | $(TeamIdentifierPrefix)$(CFBundleIdentifier) 9 | com.apple.security.app-sandbox 10 | 11 | com.apple.security.application-groups 12 | 13 | $(APP_GROUP_ID) 14 | 15 | com.apple.security.cs.allow-jit 16 | 17 | com.apple.security.network.client 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/safari/App/ResourceHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResourceHelper.swift 3 | // PiPer 4 | // 5 | // Created by Adam Marcus on 13/10/2018. 6 | // Copyright © 2018 Adam Marcus. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class ResourceHelper { 12 | 13 | static let extensionBundleURL: URL? = { 14 | guard let pluginURL = Bundle.main.builtInPlugInsURL else { 15 | return nil 16 | } 17 | guard let extensionBundle = Bundle(url: pluginURL.appendingPathComponent("PiPerExt.appex")) else { 18 | return nil 19 | } 20 | return extensionBundle.resourceURL 21 | }() 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/safari/App/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // PiPer App 4 | // 5 | // Created by Adam Marcus on 19/07/2018. 6 | // Copyright © 2018 Adam Marcus. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import SafariServices 11 | 12 | class ViewController: NSViewController { 13 | 14 | let extensionId = String(cString:EXTENSION_BUNDLE_ID) 15 | 16 | @IBOutlet var viewContainer: NSVisualEffectView! 17 | @IBOutlet var mainView: NSView! 18 | @IBOutlet var extensionDisabledView: NSView! 19 | 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | // Display the main view by default 25 | viewContainer.addSubview(mainView) 26 | 27 | // Poll every second for Safari extension state changes 28 | Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(ViewController.checkExtensionState), userInfo: nil, repeats: true).fire() 29 | } 30 | 31 | // Display the requested view 32 | func showView(_ newView: NSView) { 33 | let oldView = viewContainer.subviews.first 34 | if oldView == newView { 35 | return 36 | } 37 | // Avoid animating the transition due to NSButton vibrancy rendering glitch in dark mode 38 | // viewContainer.animator().replaceSubview(oldView!, with:newView) 39 | viewContainer.replaceSubview(oldView!, with:newView) 40 | } 41 | 42 | // Check extension state and show extension disabled view if necessary 43 | @objc func checkExtensionState() { 44 | SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionId) { state, error in 45 | DispatchQueue.main.async { 46 | if let status = state?.isEnabled { 47 | self.showView(status ? self.mainView : self.extensionDisabledView) 48 | } 49 | } 50 | } 51 | } 52 | 53 | @IBAction func clickedEnableExtension(sender: NSButton) { 54 | SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionId) 55 | } 56 | 57 | @IBAction func clickedReportBug(sender: NSButton) { 58 | if let url = URL(string: "https://github.com/amarcu5/PiPer/issues") { 59 | NSWorkspace.shared.open(url) 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/safari/Common/Defines.c: -------------------------------------------------------------------------------- 1 | // 2 | // Defines.c 3 | // PiPer 4 | // 5 | // Created by Adam Marcus on 10/08/2018. 6 | // Copyright © 2018 Adam Marcus. All rights reserved. 7 | // 8 | 9 | #define QUOTE(str) #str 10 | #define EXPAND_AND_QUOTE(str) QUOTE(str) 11 | 12 | const char * APP_GROUP_ID = EXPAND_AND_QUOTE(APP_GROUP_ID_CONST); 13 | 14 | const char * APP_BUNDLE_ID = EXPAND_AND_QUOTE(APP_BUNDLE_ID_CONST); 15 | const char * EXTENSION_BUNDLE_ID = EXPAND_AND_QUOTE(EXTENSION_BUNDLE_ID_CONST); 16 | -------------------------------------------------------------------------------- /src/safari/Common/Defines.h: -------------------------------------------------------------------------------- 1 | // 2 | // Defines.h 3 | // PiPer 4 | // 5 | // Created by Adam Marcus on 10/08/2018. 6 | // Copyright © 2018 Adam Marcus. All rights reserved. 7 | // 8 | 9 | extern const char * APP_GROUP_ID; 10 | 11 | extern const char * APP_BUNDLE_ID; 12 | extern const char * EXTENSION_BUNDLE_ID; 13 | -------------------------------------------------------------------------------- /src/safari/Extension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | PiPer 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | XPC! 19 | CFBundleShortVersionString 20 | 0.0.0 21 | CFBundleVersion 22 | 0 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSExtension 26 | 27 | NSExtensionPointIdentifier 28 | com.apple.Safari.extension 29 | NSExtensionPrincipalClass 30 | $(PRODUCT_MODULE_NAME).SafariExtensionHandler 31 | SFSafariContentScript 32 | 33 | 34 | Script 35 | scripts/main.js 36 | 37 | 38 | SFSafariExtensionBundleIdentifiersToUninstall 39 | 40 | com.amarcus.safari.piper 41 | 42 | SFSafariWebsiteAccess 43 | 44 | Level 45 | All 46 | 47 | 48 | NSHumanReadableCopyright 49 | Copyright © 2018 Adam Marcus. All rights reserved. 50 | NSHumanReadableDescription 51 | Adds Picture in Picture functionality to Youtube, Netflix, Amazon Video, Twitch, and more! 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/safari/Extension/PiPer_Extension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | $(APP_GROUP_ID) 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/safari/Extension/Resources/scripts/localization-bridge.js: -------------------------------------------------------------------------------- 1 | import { localizedString } from './localization.js' 2 | 3 | // Export localization functions 4 | window['localizedString'] = localizedString; -------------------------------------------------------------------------------- /src/safari/Extension/SafariExtensionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariExtensionHandler.swift 3 | // PiPer 4 | // 5 | // Created by Adam Marcus on 19/07/2018. 6 | // Copyright © 2018 Adam Marcus. All rights reserved. 7 | // 8 | 9 | import SafariServices 10 | 11 | class SafariExtensionHandler: SFSafariExtensionHandler { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/safari/PiPer.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 851545D721A1D23E002B149F /* InAppPurchaseHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851545D621A1D23E002B149F /* InAppPurchaseHelper.swift */; }; 11 | 851545D921A1D430002B149F /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 851545D821A1D430002B149F /* StoreKit.framework */; }; 12 | 851A5FA6219F971000A6E0CD /* DonateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851A5FA5219F971000A6E0CD /* DonateViewController.swift */; }; 13 | 85254C902100C6CA000CDDE0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85254C8F2100C6CA000CDDE0 /* AppDelegate.swift */; }; 14 | 85254C922100C6CA000CDDE0 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85254C912100C6CA000CDDE0 /* ViewController.swift */; }; 15 | 85254CA92100C703000CDDE0 /* SafariExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85254CA82100C703000CDDE0 /* SafariExtensionHandler.swift */; }; 16 | 85254CB72100C703000CDDE0 /* PiPerExt.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 85254CA32100C703000CDDE0 /* PiPerExt.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 17 | 854061A62171599100F60C11 /* LocalizationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854061A52171599100F60C11 /* LocalizationManager.swift */; }; 18 | 854061A8217167EA00F60C11 /* LocalizedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854061A7217167EA00F60C11 /* LocalizedButton.swift */; }; 19 | 854061AA2172234300F60C11 /* LocalizedTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854061A92172234300F60C11 /* LocalizedTextField.swift */; }; 20 | 857DA7332116237700B38873 /* Icon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 857DA7322116237700B38873 /* Icon.icns */; }; 21 | 85A0EA8021726E30000DB27C /* scripts in Resources */ = {isa = PBXBuildFile; fileRef = 85A0EA7A21726E2A000DB27C /* scripts */; }; 22 | 85A0EA8121726E30000DB27C /* images in Resources */ = {isa = PBXBuildFile; fileRef = 85A0EA7B21726E2B000DB27C /* images */; }; 23 | 85A0EA8621726F05000DB27C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85A0EA8521726F05000DB27C /* Main.storyboard */; }; 24 | 85A0EA8C2172850D000DB27C /* ResourceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85A0EA8B2172850D000DB27C /* ResourceHelper.swift */; }; 25 | 85CBAE6B21A32FF40004C5E6 /* DonateProgressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CBAE6A21A32FF40004C5E6 /* DonateProgressViewController.swift */; }; 26 | 85CBAE6E21A359520004C5E6 /* DonateContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CBAE6C21A359520004C5E6 /* DonateContainerViewController.swift */; }; 27 | 85CBAE8F21A6083A0004C5E6 /* DonationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CBAE8E21A6083A0004C5E6 /* DonationManager.swift */; }; 28 | 85CBAEB321A74BFF0004C5E6 /* ConfettiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CBAEB221A74BFF0004C5E6 /* ConfettiView.swift */; }; 29 | 85E505AB211E330F003B446B /* Defines.c in Sources */ = {isa = PBXBuildFile; fileRef = 85E505AA211E330F003B446B /* Defines.c */; }; 30 | 85E505AC211E3339003B446B /* Defines.c in Sources */ = {isa = PBXBuildFile; fileRef = 85E505AA211E330F003B446B /* Defines.c */; }; 31 | /* End PBXBuildFile section */ 32 | 33 | /* Begin PBXContainerItemProxy section */ 34 | 85254CB52100C703000CDDE0 /* PBXContainerItemProxy */ = { 35 | isa = PBXContainerItemProxy; 36 | containerPortal = 85254C842100C6C9000CDDE0 /* Project object */; 37 | proxyType = 1; 38 | remoteGlobalIDString = 85254CA22100C703000CDDE0; 39 | remoteInfo = PiPer; 40 | }; 41 | /* End PBXContainerItemProxy section */ 42 | 43 | /* Begin PBXCopyFilesBuildPhase section */ 44 | 85254CBB2100C703000CDDE0 /* Embed App Extensions */ = { 45 | isa = PBXCopyFilesBuildPhase; 46 | buildActionMask = 2147483647; 47 | dstPath = ""; 48 | dstSubfolderSpec = 13; 49 | files = ( 50 | 85254CB72100C703000CDDE0 /* PiPerExt.appex in Embed App Extensions */, 51 | ); 52 | name = "Embed App Extensions"; 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXCopyFilesBuildPhase section */ 56 | 57 | /* Begin PBXFileReference section */ 58 | 851545D621A1D23E002B149F /* InAppPurchaseHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseHelper.swift; sourceTree = ""; }; 59 | 851545D821A1D430002B149F /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; 60 | 851A5FA5219F971000A6E0CD /* DonateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DonateViewController.swift; sourceTree = ""; }; 61 | 851C3AE62113D4F90052505E /* PiPer_App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PiPer_App.entitlements; sourceTree = ""; }; 62 | 851C3AE72113D4FE0052505E /* PiPer_Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PiPer_Extension.entitlements; sourceTree = ""; }; 63 | 85254C8C2100C6CA000CDDE0 /* PiPer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PiPer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 64 | 85254C8F2100C6CA000CDDE0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 65 | 85254C912100C6CA000CDDE0 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 66 | 85254C982100C6CA000CDDE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 67 | 85254CA32100C703000CDDE0 /* PiPerExt.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PiPerExt.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 68 | 85254CA82100C703000CDDE0 /* SafariExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariExtensionHandler.swift; sourceTree = ""; }; 69 | 85254CAF2100C703000CDDE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 70 | 854061A52171599100F60C11 /* LocalizationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationManager.swift; sourceTree = ""; }; 71 | 854061A7217167EA00F60C11 /* LocalizedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedButton.swift; sourceTree = ""; }; 72 | 854061A92172234300F60C11 /* LocalizedTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedTextField.swift; sourceTree = ""; }; 73 | 857DA7322116237700B38873 /* Icon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = Icon.icns; sourceTree = ""; }; 74 | 85A0EA7A21726E2A000DB27C /* scripts */ = {isa = PBXFileReference; lastKnownFileType = folder; path = scripts; sourceTree = ""; }; 75 | 85A0EA7B21726E2B000DB27C /* images */ = {isa = PBXFileReference; lastKnownFileType = folder; path = images; sourceTree = ""; }; 76 | 85A0EA8521726F05000DB27C /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; 77 | 85A0EA8B2172850D000DB27C /* ResourceHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceHelper.swift; sourceTree = ""; }; 78 | 85CBAE6A21A32FF40004C5E6 /* DonateProgressViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DonateProgressViewController.swift; sourceTree = ""; }; 79 | 85CBAE6C21A359520004C5E6 /* DonateContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DonateContainerViewController.swift; sourceTree = ""; }; 80 | 85CBAE8E21A6083A0004C5E6 /* DonationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DonationManager.swift; sourceTree = ""; }; 81 | 85CBAEB221A74BFF0004C5E6 /* ConfettiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfettiView.swift; sourceTree = ""; }; 82 | 85E505A7211E22A4003B446B /* Defines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Defines.h; sourceTree = ""; }; 83 | 85E505AA211E330F003B446B /* Defines.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = Defines.c; sourceTree = ""; }; 84 | /* End PBXFileReference section */ 85 | 86 | /* Begin PBXFrameworksBuildPhase section */ 87 | 85254C892100C6CA000CDDE0 /* Frameworks */ = { 88 | isa = PBXFrameworksBuildPhase; 89 | buildActionMask = 2147483647; 90 | files = ( 91 | 851545D921A1D430002B149F /* StoreKit.framework in Frameworks */, 92 | ); 93 | runOnlyForDeploymentPostprocessing = 0; 94 | }; 95 | 85254CA02100C703000CDDE0 /* Frameworks */ = { 96 | isa = PBXFrameworksBuildPhase; 97 | buildActionMask = 2147483647; 98 | files = ( 99 | ); 100 | runOnlyForDeploymentPostprocessing = 0; 101 | }; 102 | /* End PBXFrameworksBuildPhase section */ 103 | 104 | /* Begin PBXGroup section */ 105 | 85254C832100C6C9000CDDE0 = { 106 | isa = PBXGroup; 107 | children = ( 108 | 85E505A9211E2D83003B446B /* Common */, 109 | 85254C8E2100C6CA000CDDE0 /* App */, 110 | 85254CA72100C703000CDDE0 /* Extension */, 111 | 85254C8D2100C6CA000CDDE0 /* Products */, 112 | 8589A341211E42FD006F9C65 /* Frameworks */, 113 | ); 114 | sourceTree = ""; 115 | }; 116 | 85254C8D2100C6CA000CDDE0 /* Products */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 85254C8C2100C6CA000CDDE0 /* PiPer.app */, 120 | 85254CA32100C703000CDDE0 /* PiPerExt.appex */, 121 | ); 122 | name = Products; 123 | sourceTree = ""; 124 | }; 125 | 85254C8E2100C6CA000CDDE0 /* App */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | 85254C8F2100C6CA000CDDE0 /* AppDelegate.swift */, 129 | 85A0EA8B2172850D000DB27C /* ResourceHelper.swift */, 130 | 851545D621A1D23E002B149F /* InAppPurchaseHelper.swift */, 131 | 85CBAE8E21A6083A0004C5E6 /* DonationManager.swift */, 132 | 85254C912100C6CA000CDDE0 /* ViewController.swift */, 133 | 854061A7217167EA00F60C11 /* LocalizedButton.swift */, 134 | 854061A92172234300F60C11 /* LocalizedTextField.swift */, 135 | 854061A52171599100F60C11 /* LocalizationManager.swift */, 136 | 85CBAE6C21A359520004C5E6 /* DonateContainerViewController.swift */, 137 | 85CBAE6A21A32FF40004C5E6 /* DonateProgressViewController.swift */, 138 | 851A5FA5219F971000A6E0CD /* DonateViewController.swift */, 139 | 85CBAEB221A74BFF0004C5E6 /* ConfettiView.swift */, 140 | 85A0EA8521726F05000DB27C /* Main.storyboard */, 141 | 857DA7322116237700B38873 /* Icon.icns */, 142 | 851C3AE62113D4F90052505E /* PiPer_App.entitlements */, 143 | 85254C982100C6CA000CDDE0 /* Info.plist */, 144 | ); 145 | path = App; 146 | sourceTree = ""; 147 | }; 148 | 85254CA72100C703000CDDE0 /* Extension */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 85A0EA7321726E12000DB27C /* Resources */, 152 | 85254CA82100C703000CDDE0 /* SafariExtensionHandler.swift */, 153 | 851C3AE72113D4FE0052505E /* PiPer_Extension.entitlements */, 154 | 85254CAF2100C703000CDDE0 /* Info.plist */, 155 | ); 156 | path = Extension; 157 | sourceTree = ""; 158 | }; 159 | 8589A341211E42FD006F9C65 /* Frameworks */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | 851545D821A1D430002B149F /* StoreKit.framework */, 163 | ); 164 | name = Frameworks; 165 | sourceTree = ""; 166 | }; 167 | 85A0EA7321726E12000DB27C /* Resources */ = { 168 | isa = PBXGroup; 169 | children = ( 170 | 85A0EA7A21726E2A000DB27C /* scripts */, 171 | 85A0EA7B21726E2B000DB27C /* images */, 172 | ); 173 | path = Resources; 174 | sourceTree = ""; 175 | }; 176 | 85E505A9211E2D83003B446B /* Common */ = { 177 | isa = PBXGroup; 178 | children = ( 179 | 85E505A7211E22A4003B446B /* Defines.h */, 180 | 85E505AA211E330F003B446B /* Defines.c */, 181 | ); 182 | path = Common; 183 | sourceTree = ""; 184 | }; 185 | /* End PBXGroup section */ 186 | 187 | /* Begin PBXNativeTarget section */ 188 | 85254C8B2100C6CA000CDDE0 /* PiPer */ = { 189 | isa = PBXNativeTarget; 190 | buildConfigurationList = 85254C9C2100C6CA000CDDE0 /* Build configuration list for PBXNativeTarget "PiPer" */; 191 | buildPhases = ( 192 | 85254C882100C6CA000CDDE0 /* Sources */, 193 | 85254C892100C6CA000CDDE0 /* Frameworks */, 194 | 85254C8A2100C6CA000CDDE0 /* Resources */, 195 | 85254CBB2100C703000CDDE0 /* Embed App Extensions */, 196 | ); 197 | buildRules = ( 198 | ); 199 | dependencies = ( 200 | 85254CB62100C703000CDDE0 /* PBXTargetDependency */, 201 | ); 202 | name = PiPer; 203 | productName = PiPer; 204 | productReference = 85254C8C2100C6CA000CDDE0 /* PiPer.app */; 205 | productType = "com.apple.product-type.application"; 206 | }; 207 | 85254CA22100C703000CDDE0 /* PiPerExt */ = { 208 | isa = PBXNativeTarget; 209 | buildConfigurationList = 85254CB82100C703000CDDE0 /* Build configuration list for PBXNativeTarget "PiPerExt" */; 210 | buildPhases = ( 211 | 85254C9F2100C703000CDDE0 /* Sources */, 212 | 85254CA02100C703000CDDE0 /* Frameworks */, 213 | 85254CA12100C703000CDDE0 /* Resources */, 214 | ); 215 | buildRules = ( 216 | ); 217 | dependencies = ( 218 | ); 219 | name = PiPerExt; 220 | productName = PiPer; 221 | productReference = 85254CA32100C703000CDDE0 /* PiPerExt.appex */; 222 | productType = "com.apple.product-type.app-extension"; 223 | }; 224 | /* End PBXNativeTarget section */ 225 | 226 | /* Begin PBXProject section */ 227 | 85254C842100C6C9000CDDE0 /* Project object */ = { 228 | isa = PBXProject; 229 | attributes = { 230 | LastSwiftUpdateCheck = 1000; 231 | LastUpgradeCheck = 1000; 232 | ORGANIZATIONNAME = "Adam Marcus"; 233 | TargetAttributes = { 234 | 85254C8B2100C6CA000CDDE0 = { 235 | CreatedOnToolsVersion = 10.0; 236 | LastSwiftMigration = 1000; 237 | SystemCapabilities = { 238 | com.apple.ApplicationGroups.Mac = { 239 | enabled = 1; 240 | }; 241 | com.apple.HardenedRuntime = { 242 | enabled = 1; 243 | }; 244 | com.apple.InAppPurchase = { 245 | enabled = 1; 246 | }; 247 | com.apple.NetworkExtensions = { 248 | enabled = 0; 249 | }; 250 | com.apple.Sandbox = { 251 | enabled = 1; 252 | }; 253 | com.apple.iCloud = { 254 | enabled = 1; 255 | }; 256 | }; 257 | }; 258 | 85254CA22100C703000CDDE0 = { 259 | CreatedOnToolsVersion = 10.0; 260 | SystemCapabilities = { 261 | com.apple.ApplicationGroups.Mac = { 262 | enabled = 1; 263 | }; 264 | com.apple.HardenedRuntime = { 265 | enabled = 1; 266 | }; 267 | com.apple.NetworkExtensions = { 268 | enabled = 0; 269 | }; 270 | }; 271 | }; 272 | }; 273 | }; 274 | buildConfigurationList = 85254C872100C6C9000CDDE0 /* Build configuration list for PBXProject "PiPer" */; 275 | compatibilityVersion = "Xcode 9.3"; 276 | developmentRegion = en; 277 | hasScannedForEncodings = 0; 278 | knownRegions = ( 279 | en, 280 | Base, 281 | ); 282 | mainGroup = 85254C832100C6C9000CDDE0; 283 | productRefGroup = 85254C8D2100C6CA000CDDE0 /* Products */; 284 | projectDirPath = ""; 285 | projectRoot = ""; 286 | targets = ( 287 | 85254C8B2100C6CA000CDDE0 /* PiPer */, 288 | 85254CA22100C703000CDDE0 /* PiPerExt */, 289 | ); 290 | }; 291 | /* End PBXProject section */ 292 | 293 | /* Begin PBXResourcesBuildPhase section */ 294 | 85254C8A2100C6CA000CDDE0 /* Resources */ = { 295 | isa = PBXResourcesBuildPhase; 296 | buildActionMask = 2147483647; 297 | files = ( 298 | 857DA7332116237700B38873 /* Icon.icns in Resources */, 299 | 85A0EA8621726F05000DB27C /* Main.storyboard in Resources */, 300 | ); 301 | runOnlyForDeploymentPostprocessing = 0; 302 | }; 303 | 85254CA12100C703000CDDE0 /* Resources */ = { 304 | isa = PBXResourcesBuildPhase; 305 | buildActionMask = 2147483647; 306 | files = ( 307 | 85A0EA8021726E30000DB27C /* scripts in Resources */, 308 | 85A0EA8121726E30000DB27C /* images in Resources */, 309 | ); 310 | runOnlyForDeploymentPostprocessing = 0; 311 | }; 312 | /* End PBXResourcesBuildPhase section */ 313 | 314 | /* Begin PBXSourcesBuildPhase section */ 315 | 85254C882100C6CA000CDDE0 /* Sources */ = { 316 | isa = PBXSourcesBuildPhase; 317 | buildActionMask = 2147483647; 318 | files = ( 319 | 854061A8217167EA00F60C11 /* LocalizedButton.swift in Sources */, 320 | 85E505AB211E330F003B446B /* Defines.c in Sources */, 321 | 85CBAE8F21A6083A0004C5E6 /* DonationManager.swift in Sources */, 322 | 85254C922100C6CA000CDDE0 /* ViewController.swift in Sources */, 323 | 851545D721A1D23E002B149F /* InAppPurchaseHelper.swift in Sources */, 324 | 85254C902100C6CA000CDDE0 /* AppDelegate.swift in Sources */, 325 | 85A0EA8C2172850D000DB27C /* ResourceHelper.swift in Sources */, 326 | 854061A62171599100F60C11 /* LocalizationManager.swift in Sources */, 327 | 854061AA2172234300F60C11 /* LocalizedTextField.swift in Sources */, 328 | 851A5FA6219F971000A6E0CD /* DonateViewController.swift in Sources */, 329 | 85CBAE6B21A32FF40004C5E6 /* DonateProgressViewController.swift in Sources */, 330 | 85CBAEB321A74BFF0004C5E6 /* ConfettiView.swift in Sources */, 331 | 85CBAE6E21A359520004C5E6 /* DonateContainerViewController.swift in Sources */, 332 | ); 333 | runOnlyForDeploymentPostprocessing = 0; 334 | }; 335 | 85254C9F2100C703000CDDE0 /* Sources */ = { 336 | isa = PBXSourcesBuildPhase; 337 | buildActionMask = 2147483647; 338 | files = ( 339 | 85254CA92100C703000CDDE0 /* SafariExtensionHandler.swift in Sources */, 340 | 85E505AC211E3339003B446B /* Defines.c in Sources */, 341 | ); 342 | runOnlyForDeploymentPostprocessing = 0; 343 | }; 344 | /* End PBXSourcesBuildPhase section */ 345 | 346 | /* Begin PBXTargetDependency section */ 347 | 85254CB62100C703000CDDE0 /* PBXTargetDependency */ = { 348 | isa = PBXTargetDependency; 349 | target = 85254CA22100C703000CDDE0 /* PiPerExt */; 350 | targetProxy = 85254CB52100C703000CDDE0 /* PBXContainerItemProxy */; 351 | }; 352 | /* End PBXTargetDependency section */ 353 | 354 | /* Begin XCBuildConfiguration section */ 355 | 85254C9A2100C6CA000CDDE0 /* Debug */ = { 356 | isa = XCBuildConfiguration; 357 | buildSettings = { 358 | ALWAYS_SEARCH_USER_PATHS = NO; 359 | APP_BUNDLE_ID = com.amarcus.PiPer; 360 | APP_GROUP_ID = "$(TeamIdentifierPrefix)group.amarcus.piper"; 361 | CLANG_ANALYZER_NONNULL = YES; 362 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 363 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 364 | CLANG_CXX_LIBRARY = "libc++"; 365 | CLANG_ENABLE_MODULES = YES; 366 | CLANG_ENABLE_OBJC_ARC = YES; 367 | CLANG_ENABLE_OBJC_WEAK = YES; 368 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 369 | CLANG_WARN_BOOL_CONVERSION = YES; 370 | CLANG_WARN_COMMA = YES; 371 | CLANG_WARN_CONSTANT_CONVERSION = YES; 372 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 373 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 374 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 375 | CLANG_WARN_EMPTY_BODY = YES; 376 | CLANG_WARN_ENUM_CONVERSION = YES; 377 | CLANG_WARN_INFINITE_RECURSION = YES; 378 | CLANG_WARN_INT_CONVERSION = YES; 379 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 380 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 381 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 382 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 383 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 384 | CLANG_WARN_STRICT_PROTOTYPES = YES; 385 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 386 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 387 | CLANG_WARN_UNREACHABLE_CODE = YES; 388 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 389 | COPY_PHASE_STRIP = NO; 390 | DEAD_CODE_STRIPPING = YES; 391 | DEBUG_INFORMATION_FORMAT = dwarf; 392 | ENABLE_STRICT_OBJC_MSGSEND = YES; 393 | ENABLE_TESTABILITY = YES; 394 | EXTENSION_BUNDLE_ID = com.amarcus.PiPer.PiPerExt; 395 | GCC_C_LANGUAGE_STANDARD = gnu11; 396 | GCC_DYNAMIC_NO_PIC = NO; 397 | GCC_NO_COMMON_BLOCKS = YES; 398 | GCC_OPTIMIZATION_LEVEL = 0; 399 | GCC_PREPROCESSOR_DEFINITIONS = ( 400 | "APP_GROUP_ID_CONST=\"$(APP_GROUP_ID)\"", 401 | "APP_BUNDLE_ID_CONST=\"$(APP_BUNDLE_ID)\"", 402 | "EXTENSION_BUNDLE_ID_CONST=\"$(EXTENSION_BUNDLE_ID)\"", 403 | "DEBUG=1", 404 | ); 405 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 406 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 407 | GCC_WARN_UNDECLARED_SELECTOR = YES; 408 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 409 | GCC_WARN_UNUSED_FUNCTION = YES; 410 | GCC_WARN_UNUSED_VARIABLE = YES; 411 | MACOSX_DEPLOYMENT_TARGET = 10.12; 412 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 413 | ONLY_ACTIVE_ARCH = YES; 414 | SDKROOT = macosx; 415 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 416 | SWIFT_OBJC_BRIDGING_HEADER = Common/Defines.h; 417 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 418 | }; 419 | name = Debug; 420 | }; 421 | 85254C9B2100C6CA000CDDE0 /* Release */ = { 422 | isa = XCBuildConfiguration; 423 | buildSettings = { 424 | ALWAYS_SEARCH_USER_PATHS = NO; 425 | APP_BUNDLE_ID = com.amarcus.PiPer; 426 | APP_GROUP_ID = "$(TeamIdentifierPrefix)group.amarcus.piper"; 427 | CLANG_ANALYZER_NONNULL = YES; 428 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 429 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 430 | CLANG_CXX_LIBRARY = "libc++"; 431 | CLANG_ENABLE_MODULES = YES; 432 | CLANG_ENABLE_OBJC_ARC = YES; 433 | CLANG_ENABLE_OBJC_WEAK = YES; 434 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 435 | CLANG_WARN_BOOL_CONVERSION = YES; 436 | CLANG_WARN_COMMA = YES; 437 | CLANG_WARN_CONSTANT_CONVERSION = YES; 438 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 439 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 440 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 441 | CLANG_WARN_EMPTY_BODY = YES; 442 | CLANG_WARN_ENUM_CONVERSION = YES; 443 | CLANG_WARN_INFINITE_RECURSION = YES; 444 | CLANG_WARN_INT_CONVERSION = YES; 445 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 446 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 447 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 448 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 449 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 450 | CLANG_WARN_STRICT_PROTOTYPES = YES; 451 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 452 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 453 | CLANG_WARN_UNREACHABLE_CODE = YES; 454 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 455 | COPY_PHASE_STRIP = NO; 456 | DEAD_CODE_STRIPPING = YES; 457 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 458 | ENABLE_NS_ASSERTIONS = NO; 459 | ENABLE_STRICT_OBJC_MSGSEND = YES; 460 | EXTENSION_BUNDLE_ID = com.amarcus.PiPer.PiPerExt; 461 | GCC_C_LANGUAGE_STANDARD = gnu11; 462 | GCC_NO_COMMON_BLOCKS = YES; 463 | GCC_PREPROCESSOR_DEFINITIONS = ( 464 | "APP_GROUP_ID_CONST=\"$(APP_GROUP_ID)\"", 465 | "APP_BUNDLE_ID_CONST=\"$(APP_BUNDLE_ID)\"", 466 | "EXTENSION_BUNDLE_ID_CONST=\"$(EXTENSION_BUNDLE_ID)\"", 467 | ); 468 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 469 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 470 | GCC_WARN_UNDECLARED_SELECTOR = YES; 471 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 472 | GCC_WARN_UNUSED_FUNCTION = YES; 473 | GCC_WARN_UNUSED_VARIABLE = YES; 474 | MACOSX_DEPLOYMENT_TARGET = 10.12; 475 | MTL_ENABLE_DEBUG_INFO = NO; 476 | SDKROOT = macosx; 477 | SWIFT_COMPILATION_MODE = wholemodule; 478 | SWIFT_OBJC_BRIDGING_HEADER = Common/Defines.h; 479 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 480 | }; 481 | name = Release; 482 | }; 483 | 85254C9D2100C6CA000CDDE0 /* Debug */ = { 484 | isa = XCBuildConfiguration; 485 | buildSettings = { 486 | CLANG_ENABLE_MODULES = YES; 487 | CODE_SIGN_ENTITLEMENTS = App/PiPer_App.entitlements; 488 | CODE_SIGN_IDENTITY = "Mac Developer"; 489 | CODE_SIGN_STYLE = Manual; 490 | COMBINE_HIDPI_IMAGES = YES; 491 | DEVELOPMENT_TEAM = ""; 492 | ENABLE_HARDENED_RUNTIME = YES; 493 | INFOPLIST_FILE = App/Info.plist; 494 | LD_RUNPATH_SEARCH_PATHS = ( 495 | "$(inherited)", 496 | "@executable_path/../Frameworks", 497 | ); 498 | MACOSX_DEPLOYMENT_TARGET = 10.12; 499 | PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_ID)"; 500 | PRODUCT_NAME = "$(TARGET_NAME)"; 501 | PROVISIONING_PROFILE_SPECIFIER = ""; 502 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 503 | SWIFT_VERSION = 4.2; 504 | }; 505 | name = Debug; 506 | }; 507 | 85254C9E2100C6CA000CDDE0 /* Release */ = { 508 | isa = XCBuildConfiguration; 509 | buildSettings = { 510 | CLANG_ENABLE_MODULES = YES; 511 | CODE_SIGN_ENTITLEMENTS = App/PiPer_App.entitlements; 512 | CODE_SIGN_IDENTITY = "Mac Developer"; 513 | CODE_SIGN_STYLE = Manual; 514 | COMBINE_HIDPI_IMAGES = YES; 515 | DEVELOPMENT_TEAM = ""; 516 | ENABLE_HARDENED_RUNTIME = YES; 517 | INFOPLIST_FILE = App/Info.plist; 518 | LD_RUNPATH_SEARCH_PATHS = ( 519 | "$(inherited)", 520 | "@executable_path/../Frameworks", 521 | ); 522 | MACOSX_DEPLOYMENT_TARGET = 10.12; 523 | PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_ID)"; 524 | PRODUCT_NAME = "$(TARGET_NAME)"; 525 | PROVISIONING_PROFILE_SPECIFIER = ""; 526 | SWIFT_VERSION = 4.2; 527 | }; 528 | name = Release; 529 | }; 530 | 85254CB92100C703000CDDE0 /* Debug */ = { 531 | isa = XCBuildConfiguration; 532 | buildSettings = { 533 | CODE_SIGN_ENTITLEMENTS = Extension/PiPer_Extension.entitlements; 534 | CODE_SIGN_IDENTITY = "Mac Developer"; 535 | CODE_SIGN_STYLE = Manual; 536 | DEVELOPMENT_TEAM = ""; 537 | ENABLE_HARDENED_RUNTIME = YES; 538 | INFOPLIST_FILE = Extension/Info.plist; 539 | LD_RUNPATH_SEARCH_PATHS = ( 540 | "$(inherited)", 541 | "@executable_path/../Frameworks", 542 | "@executable_path/../../../../Frameworks", 543 | ); 544 | PRODUCT_BUNDLE_IDENTIFIER = "$(EXTENSION_BUNDLE_ID)"; 545 | PRODUCT_NAME = "$(TARGET_NAME)"; 546 | PROVISIONING_PROFILE_SPECIFIER = ""; 547 | SKIP_INSTALL = YES; 548 | SWIFT_VERSION = 4.2; 549 | }; 550 | name = Debug; 551 | }; 552 | 85254CBA2100C703000CDDE0 /* Release */ = { 553 | isa = XCBuildConfiguration; 554 | buildSettings = { 555 | CODE_SIGN_ENTITLEMENTS = Extension/PiPer_Extension.entitlements; 556 | CODE_SIGN_IDENTITY = "Mac Developer"; 557 | CODE_SIGN_STYLE = Manual; 558 | DEVELOPMENT_TEAM = ""; 559 | ENABLE_HARDENED_RUNTIME = YES; 560 | INFOPLIST_FILE = Extension/Info.plist; 561 | LD_RUNPATH_SEARCH_PATHS = ( 562 | "$(inherited)", 563 | "@executable_path/../Frameworks", 564 | "@executable_path/../../../../Frameworks", 565 | ); 566 | PRODUCT_BUNDLE_IDENTIFIER = "$(EXTENSION_BUNDLE_ID)"; 567 | PRODUCT_NAME = "$(TARGET_NAME)"; 568 | PROVISIONING_PROFILE_SPECIFIER = ""; 569 | SKIP_INSTALL = YES; 570 | SWIFT_VERSION = 4.2; 571 | }; 572 | name = Release; 573 | }; 574 | /* End XCBuildConfiguration section */ 575 | 576 | /* Begin XCConfigurationList section */ 577 | 85254C872100C6C9000CDDE0 /* Build configuration list for PBXProject "PiPer" */ = { 578 | isa = XCConfigurationList; 579 | buildConfigurations = ( 580 | 85254C9A2100C6CA000CDDE0 /* Debug */, 581 | 85254C9B2100C6CA000CDDE0 /* Release */, 582 | ); 583 | defaultConfigurationIsVisible = 0; 584 | defaultConfigurationName = Release; 585 | }; 586 | 85254C9C2100C6CA000CDDE0 /* Build configuration list for PBXNativeTarget "PiPer" */ = { 587 | isa = XCConfigurationList; 588 | buildConfigurations = ( 589 | 85254C9D2100C6CA000CDDE0 /* Debug */, 590 | 85254C9E2100C6CA000CDDE0 /* Release */, 591 | ); 592 | defaultConfigurationIsVisible = 0; 593 | defaultConfigurationName = Release; 594 | }; 595 | 85254CB82100C703000CDDE0 /* Build configuration list for PBXNativeTarget "PiPerExt" */ = { 596 | isa = XCConfigurationList; 597 | buildConfigurations = ( 598 | 85254CB92100C703000CDDE0 /* Debug */, 599 | 85254CBA2100C703000CDDE0 /* Release */, 600 | ); 601 | defaultConfigurationIsVisible = 0; 602 | defaultConfigurationName = Release; 603 | }; 604 | /* End XCConfigurationList section */ 605 | }; 606 | rootObject = 85254C842100C6C9000CDDE0 /* Project object */; 607 | } 608 | -------------------------------------------------------------------------------- /src/safari/PiPer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/safari/PiPer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/safari/PiPer.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/safari/PiPer.xcodeproj/project.xcworkspace/xcuserdata/amarcus.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amarcu5/PiPer/51d585da9cdf6bf31156fbb3f9b2df15e0e1749a/src/safari/PiPer.xcodeproj/project.xcworkspace/xcuserdata/amarcus.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /src/safari/PiPer.xcodeproj/project.xcworkspace/xcuserdata/amarcus.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | CustomBuildLocationType 8 | RelativeToDerivedData 9 | DerivedDataLocationStyle 10 | Default 11 | EnabledFullIndexStoreVisibility 12 | 13 | IssueFilterStyle 14 | ShowActiveSchemeOnly 15 | LiveSourceIssuesEnabled 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/safari/PiPer.xcodeproj/xcuserdata/amarcus.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/safari/PiPer.xcodeproj/xcuserdata/amarcus.xcuserdatad/xcschemes/PiPer.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/safari/PiPer.xcodeproj/xcuserdata/amarcus.xcuserdatad/xcschemes/PiPerExt.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 54 | 55 | 56 | 57 | 58 | 59 | 70 | 74 | 75 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 94 | 96 | 102 | 103 | 104 | 105 | 107 | 108 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/safari/PiPer.xcodeproj/xcuserdata/amarcus.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | PiPer.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | PiPerExt.xcscheme 13 | 14 | orderHint 15 | 1 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 85254C8B2100C6CA000CDDE0 21 | 22 | primary 23 | 24 | 25 | 85254CA22100C703000CDDE0 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/safari/exportOptions.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | method 6 | mac-application 7 | 8 | 9 | --------------------------------------------------------------------------------