├── .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 |
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/%3C/g')
414 | browser_flag="BROWSER$\$module${defines_processed_path}=${browser}"
415 | logging_flag="LOGGING_LEVEL$\$module${defines_processed_path}=${logging_level}"
416 |
417 |
418 | if [[ "$optimize_strings" -eq 1 ]]; then
419 | localization_path="${scripts_path}/localization.js"
420 | localization_source=$(<"$localization_path")
421 | fi
422 |
423 | for entry in "${SOURCE_FILES[@]}"; do
424 | files=()
425 |
426 | absolute_entry="${scripts_path}/${entry}"
427 | [[ ! -f "$absolute_entry" ]] && continue
428 |
429 | add_element "$absolute_entry"
430 | process_file "$absolute_entry"
431 |
432 |
433 | # Statically analyze javascript and remove unused localized strings
434 | if [[ "$optimize_strings" -eq 1 ]]; then
435 | locale_keys=()
436 | regex="(=[ \t"$'\n'"]*localizedString(WithReplacements)?[ \t"$'\n'";,])|(localizedString(WithReplacements)?\([ \t"$'\n'"]*(\"([^\"]+)\"|'([^']+)'|([^'\",)]+))[ \t"$'\n'"]*[,)])"
437 | dynamic_access=0
438 | for path in "${files[@]}"; do
439 | [[ "$path" == "$localization_path" ]] && continue
440 | source=$(<"$path")
441 | while true; do
442 | if [[ "$source" =~ $regex ]]; then
443 | source="${source##*${BASH_REMATCH[0]}}"
444 | locale_key="${BASH_REMATCH[6]:-${BASH_REMATCH[7]}}"
445 | if [[ ! -z "$locale_key" ]]; then
446 | locale_keys+=("$locale_key")
447 | else
448 | dynamic_access=1
449 | break
450 | fi
451 | else
452 | break
453 | fi
454 | done
455 | done
456 | source="$localization_source"
457 | if [[ "$dynamic_access" -eq 0 ]]; then
458 | processing="$localization_source"
459 | regex="localizations\[[ \t"$'\n'"]*('([^']+)'|\"([^\"]+)\")[ \t"$'\n'"]*\][^=]+=[ "$'\n'"]*{([^}'\"]*('([^'\\]|\\.)*'|\"([^\"\\]|\\.)*\")?)*}[ \t"$'\n'"]*;?"
460 | while true; do
461 | if [[ "$processing" =~ $regex ]]; then
462 | processing=${processing##*"${BASH_REMATCH[0]}"}
463 | locale_key="${BASH_REMATCH[2]:-${BASH_REMATCH[3]}}"
464 | found=0
465 | for key in "${locale_keys[@]}"; do
466 | if [[ "$key" == "$locale_key" ]]; then
467 | found=1
468 | break
469 | fi
470 | done
471 | if [[ "$found" -eq 0 ]]; then
472 | source=${source/"${BASH_REMATCH[0]}"/}
473 | fi
474 | else
475 | break
476 | fi
477 | done
478 | fi
479 | echo "$source" > "${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 |

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 |
21 |
--------------------------------------------------------------------------------
/src/common/images/default.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
--------------------------------------------------------------------------------