diff --git a/README.md b/README.md index 16072ba..9a28370 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ ## Contents - [Features](#features) - [Installation](#installation) + * [Safari](#safari) + * [Chrome](#chrome) - [Supported sites](#supported-sites) - [Changelog](#changelog) - [Development](#development) @@ -34,13 +36,18 @@ ## Features * Adds a dedicated Picture in Picture button to the video player of [supported sites](#supported-sites) * Button integrates seamlessly with the player including hover effects and tooltips -* Supports closed captions in Picture and Picture mode +* Supports closed captions in Picture and Picture mode (Safari only) +* Supports Safari and Chrome * Free and open source ## Installation -Get the latest release from the [Safari Extension Gallery](https://safari-extensions.apple.com/details/?id=com.amarcus.safari.piper-BQ6Q24MF9X) - -...or live life on the edge with the latest [development build](https://rawgit.com/amarcu5/PiPer/develop/out/PiPer-safari-legacy.safariextz) (IMPORTANT: these builds do not update automatically!) +### Safari +Install from the [Mac App Store](https://itunes.apple.com/app/1421915518?mt=12&ls=1) by clicking "Get" +(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)) +### Chrome +Install from the [Chrome Web Store](https://chrome.google.com/webstore/detail/piper/jbjleapidaddpbncgofepljddfeoghkc) by clicking "Add to Chrome" + +...or live life on the edge with the latest [development build](https://github.com/amarcu5/PiPer/tree/develop/out) (IMPORTANT: these builds do not update automatically!) ## Supported sites * [Amazon Video](http://www.amazon.com/PrimeVideo) @@ -86,8 +93,14 @@ You can find information about releases [here](https://github.com/amarcu5/PiPer/ ### Building #### Prerequisites -* Mac running macOS 10.12 Sierra or later -* [Node.js](https://nodejs.org) +* Operating system + * macOS: 10.12 Sierra or newer (required to build Safari extension) + * Windows: Vista or newer using [Cygwin](https://cygwin.com/install.html) + * Linux: 64-bit Ubuntu 14.04+, Debian 8+, openSUSE 13.3+, or Fedora Linux 24+ +* Software + * [Node.js](https://nodejs.org) + * [Java](https://www.java.com/en/download/) (Windows only) + #### Build tools The following build tools are used to build the extension: @@ -107,8 +120,11 @@ npm install -g google-closure-compiler-js #### Steps 1. Clone the repository 2. Run `make.sh` - 1. By default this builds the unoptimized and unpackaged development version into the `./out/` directory that can then be installed using Safari's Extension Builder - 2. Alternatively run `./make.sh -p release` to build the optimized and packaged release version (note that packaging requires a private key) + 1. By default this builds the unoptimized and unpackaged development version for all targets into the `./out/` directory + 2. Alternatively: + * `./make.sh -p release` to build the optimized release versions for all targets + * `./make.sh -p release -t chrome` to build the optimized release version for the Chrome browser + * `./make.sh -h` to see the full list of options ### Supporting a new site If we wanted to support `example.com` with the source: diff --git a/make.sh b/make.sh index b891196..3a8e8df 100755 --- a/make.sh +++ b/make.sh @@ -4,7 +4,7 @@ EXTENSION_NAME="PiPer" -SOURCE_FILES=("main.js" "fix.js" "localization_bridge.js") +SOURCE_FILES=("main.js" "fix.js" "background.js" "install.js" "localization_bridge.js") # Certifcate paths LEAF_CERT_PATH="../certs/cert.pem" @@ -20,7 +20,7 @@ Usage: make.sh [options] Options: -h -? --help Show this screen - -t --target (all|safari|safari-legacy) Make extension for target browser [default: all] + -t --target (all|safari|safari-legacy|chrome) Make extension for target browser [default: all] -p --profile (release|debug|distribute) Set settings according to profile [default: debug] -c --compress-css Compress CSS -j --compress-js Compress JavaScript @@ -116,9 +116,13 @@ echo "Setting '${profile}' profile" case $targets in safari) targets=("safari") ;; safari-legacy) targets=("safari-legacy") ;; - *) targets=("safari" "safari-legacy") + chrome) targets=("chrome") ;; + *) targets=("safari" "safari-legacy" "chrome") esac +# Validate logging level +logging_level="${logging_level//[!0-9]/}" +[[ -z "$logging_level" ]] && logging_level=0 # Helper checks for build tool dependency and falls back to 'npx' if possible function get_node_command() { @@ -301,6 +305,7 @@ EOF } >"out/${EXTENSION_NAME}/scripts/resources/index.js" + for target in "${targets[@]}"; do echo "Building '${target}' extension" @@ -317,6 +322,11 @@ for target in "${targets[@]}"; do target_extension=".safariextension" common_file_path="" ;; + chrome) + browser=2 + target_extension="" + common_file_path="" + ;; *) exit 1 esac @@ -397,15 +407,16 @@ for target in "${targets[@]}"; do extern_path=$(fix_absolute_path "${scripts_path}/externs.js") 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/ + + + PiPer + + + + + + + +
+ Warning +
chrome-flags-warning
+ +
+ +
+ report-bug + donate +
+ + + + + diff --git a/src/chrome/manifest.json b/src/chrome/manifest.json new file mode 100755 index 0000000..cba9e32 --- /dev/null +++ b/src/chrome/manifest.json @@ -0,0 +1,30 @@ +{ + "name": "PiPer", + "description": "Adds Picture in Picture functionality to YouTube, Netflix, Amazon Video, Twitch, and more!", + "version": "0.0.0", + "icons": { + "128": "Icon-128.png" + }, + "background": { + "scripts": ["scripts/background.js"], + "persistent": false + }, + "content_scripts": [ + { + "all_frames": true, + "matches": ["http://*/*", "https://*/*"], + "run_at": "document_idle", + "js": ["scripts/main.js"] + } + ], + "permissions": [ + "activeTab", + "storage" + ], + "web_accessible_resources": [ + "images/*.svg", + "scripts/*.js" + ], + "minimum_chrome_version": "69.0.3483.0", + "manifest_version": 2 +} diff --git a/src/chrome/scripts/background.js b/src/chrome/scripts/background.js new file mode 100644 index 0000000..08cef55 --- /dev/null +++ b/src/chrome/scripts/background.js @@ -0,0 +1,5 @@ +chrome.runtime.onInstalled.addListener(function(/** {reason: string} */ details) { + if (details.reason == "install") { + chrome.tabs.create({url: chrome.extension.getURL("install.html")}); + } +}); \ No newline at end of file diff --git a/src/chrome/scripts/install.js b/src/chrome/scripts/install.js new file mode 100644 index 0000000..82008ff --- /dev/null +++ b/src/chrome/scripts/install.js @@ -0,0 +1,48 @@ +import { info } from './logger.js' +import { localizedString, localizedStringWithReplacements } from './localization.js' + +// Hide page during loading +const htmlTag = /** @type {HTMLElement} */ (document.getElementsByTagName("html")[0]); +htmlTag.style.display = 'none'; + +document.addEventListener('DOMContentLoaded', function() { + + // Localize text elements + const localizedElements = document.getElementsByClassName('localized-string'); + for (let index = 0, element; element = localizedElements[index]; index++) { + const key = element.textContent.trim(); + + let string; + if (key == 'chrome-flags-warning') { + string = localizedStringWithReplacements(key, [ + ['emphasis', ''], + ['/emphasis', ''], + ]); + } else { + string = localizedString(key); + } + + element.innerHTML = string; + } + + // Make page visible + htmlTag.style.removeProperty('display'); + + // Open required Chrome flag if warning button clicked + document.getElementById('warning-button').addEventListener('click', function(event) { + chrome.tabs.create({url: 'chrome://flags/#enable-surfaces-for-videos'}); + }); + + // Test for Picture in Picture support and display warning to activate Chrome flags if needed + const video = /** @type {HTMLVideoElement} */ (document.getElementById('test-video')); + video.addEventListener('loadeddata', function() { + video.requestPictureInPicture().catch(function(/** Error */ error) { + if (~error.message.indexOf('Picture-in-Picture is not available')) { + info('Picture-in-Picture NOT supported'); + document.getElementById('warning').style.display = 'flex'; + } else { + info('Picture-in-Picture IS supported'); + } + }); + }); +}); \ No newline at end of file diff --git a/src/common/images/logo.svg b/src/common/images/logo.svg new file mode 100644 index 0000000..f6ae917 --- /dev/null +++ b/src/common/images/logo.svg @@ -0,0 +1,848 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/common/images/warning.svg b/src/common/images/warning.svg new file mode 100644 index 0000000..0ac3628 --- /dev/null +++ b/src/common/images/warning.svg @@ -0,0 +1,380 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/common/scripts/captions.js b/src/common/scripts/captions.js index b52551e..acce5ed 100644 --- a/src/common/scripts/captions.js +++ b/src/common/scripts/captions.js @@ -1,5 +1,5 @@ import { info } from './logger.js' -import { getResource } from './common.js' +import { Browser, getBrowser, getResource } from './common.js' import { videoPlayingPictureInPicture } from './video.js' const TRACK_ID = 'PiPer_track'; @@ -117,7 +117,7 @@ const removeCaptions = function(video, workaround = true) { } // Workaround Safari bug; 'removeCue' doesn't immediately remove captions shown in Picture in Picture mode - if (workaround && video && !showingEmptyCaption) { + if (getBrowser() == Browser.SAFARI && workaround && video && !showingEmptyCaption) { track.addCue(new VTTCue(video.currentTime, video.currentTime + 60, '')); showingEmptyCaption = true; } @@ -134,7 +134,9 @@ const addCaption = function(video, caption) { info(`Showing caption '${caption}'`); track.addCue(new VTTCue(video.currentTime, video.currentTime + 60, caption)); - showingEmptyCaption = false; + if (getBrowser() == Browser.SAFARI) { + showingEmptyCaption = false; + } }; /** diff --git a/src/common/scripts/common.js b/src/common/scripts/common.js index 32e7cf4..3ff4c68 100644 --- a/src/common/scripts/common.js +++ b/src/common/scripts/common.js @@ -1,5 +1,31 @@ +import { BROWSER } from './defines.js' import { warn } from './logger.js' +/** @enum {number} - Enum for browser */ +export const Browser = { + UNKNOWN: 0, + SAFARI: 1, + CHROME: 2, +}; + +/** + * Returns current web browser + * + * @return {Browser} + */ +export const getBrowser = function() { + if (BROWSER != Browser.UNKNOWN) { + return /** @type {Browser} */ (BROWSER); + } + if (/Safari/.test(navigator.userAgent) && /Apple/.test(navigator.vendor)) { + return Browser.SAFARI; + } + if (/Chrome/.test(navigator.userAgent) && /Google/.test(navigator.vendor)) { + return Browser.CHROME; + } + return Browser.UNKNOWN; +}; + /** * @typedef {{ * buttonClassName: (string|undefined), @@ -44,7 +70,15 @@ export const setResource = function(resource) { * @return {string} */ export const getExtensionURL = function(path) { - return safari.extension.baseURI + path; + switch (getBrowser()) { + case Browser.SAFARI: + return safari.extension.baseURI + path; + case Browser.CHROME: + return chrome.runtime.getURL(path); + case Browser.UNKNOWN: + default: + return path; + } }; /** diff --git a/src/common/scripts/defines.js b/src/common/scripts/defines.js index d65e4f5..d728052 100644 --- a/src/common/scripts/defines.js +++ b/src/common/scripts/defines.js @@ -1,2 +1,5 @@ /** @define {number} - Flag used by closure compiler to set logging level */ -export const LOGGING_LEVEL = 0; \ No newline at end of file +export const LOGGING_LEVEL = 0; + +/** @define {number} - Flag used by closure compiler to target specific browser */ +export const BROWSER = 0; \ No newline at end of file diff --git a/src/common/scripts/externs.js b/src/common/scripts/externs.js index 4a56482..123dd7b 100644 --- a/src/common/scripts/externs.js +++ b/src/common/scripts/externs.js @@ -1,3 +1,11 @@ +/** + * @fileoverview Externs file for PiPer used by Closure Compiler + * @externs + */ + + +/* Safari Extension */ + /** @const */ const safari = {}; @@ -11,7 +19,91 @@ safari.extension.baseURI; HTMLVideoElement.prototype.webkitPresentationMode; /** @return {undefined} */ -HTMLVideoElement.prototype.webkitSetPresentationMode = function(mode) {} +HTMLVideoElement.prototype.webkitSetPresentationMode = function(mode) {}; /** @type {string} */ TextTrack.prototype.label; + +/** @return {undefined} */ +safari.extension.dispatchMessage = function(message, userInfo) {}; + +/** @const */ +safari.self = {}; + +/** @return {undefined} */ +safari.self.addEventListener = function(type, listener) {}; + +/** @constructor */ +const SafariExtensionMessageEvent = function() {}; + +/** @type {*} */ +SafariExtensionMessageEvent.prototype.message; + +/** @type {string} */ +SafariExtensionMessageEvent.prototype.name; + + +/* Legacy Safari Extension */ + +/** @const */ +safari.extension.settings = {}; + +/** @return {undefined} */ +safari.extension.settings.clear = function() {}; + + +/* Chrome Extension */ + +/** @const */ +const chrome = {}; + +/** @const */ +chrome.runtime = {}; + +/** @return {string} */ +chrome.runtime.getURL = function(path) {}; + +/** @const */ +chrome.runtime.onInstalled = {}; + +/** @return {undefined} */ +chrome.runtime.onInstalled.addListener = function(callback) {}; + +/** @constructor */ +chrome.runtime.Manifest = function() {}; + +/** @type {string} */ +chrome.runtime.Manifest.prototype.version; + +/** @return {!chrome.runtime.Manifest} */ +chrome.runtime.getManifest = function() {}; + +/** @const */ +chrome.tabs = {}; + +/** @return {undefined} */ +chrome.tabs.create = function(properties) {}; + +/** @return {Promise<*>} */ +HTMLVideoElement.prototype.requestPictureInPicture = function() {}; + +/** @const */ +chrome.extension = {}; + +/** @return {string} */ +chrome.extension.getURL = function(path) {}; + +/** @const */ +chrome.storage = {}; + +/** @const */ +chrome.storage.sync = {}; + +/** @return {undefined} */ +chrome.storage.sync.get = function(keys, callback) {}; + +/** @return {undefined} */ +chrome.storage.sync.set = function(items, callback) {}; + +/** @return {undefined} */ +chrome.storage.sync.clear = function(callback) {}; \ No newline at end of file diff --git a/src/common/scripts/localization.js b/src/common/scripts/localization.js index 93c73b0..3a34c3b 100644 --- a/src/common/scripts/localization.js +++ b/src/common/scripts/localization.js @@ -19,6 +19,14 @@ localizations['report-bug'] = { 'de': 'Einen Fehler melden', }; +localizations['options'] = { + 'en': 'Options', +}; + +localizations['install-thanks'] = { + 'en': 'Thanks for adding PiPer!', +}; + localizations['enable'] = { 'en': 'Enable', }; @@ -27,6 +35,14 @@ localizations['safari-disabled-warning'] = { 'en': 'Extension is currently disabled, enable in Safari preferences', }; +localizations['chrome-flags-open'] = { + 'en': 'Open Chrome Flags', +}; + +localizations['chrome-flags-warning'] = { + 'en': 'Before you get started you need to enable the chrome flag [emphasis]"SurfaceLayer objects for videos"[/emphasis]', +}; + // Set English as the default fallback language const defaultLanguage = 'en'; diff --git a/src/common/scripts/main.js b/src/common/scripts/main.js index 6dca360..5355ac3 100644 --- a/src/common/scripts/main.js +++ b/src/common/scripts/main.js @@ -1,5 +1,5 @@ import { info } from './logger.js' -import { getResource, setResource } from './common.js' +import { Browser, getBrowser, getResource, setResource } from './common.js' import { addVideoElementListeners } from './video.js' import { resources } from './resources/index.js'; import { checkButton, addButton } from './button.js' @@ -13,7 +13,10 @@ const mutationObserver = function() { // Process video captions if needed if (shouldProcessCaptions()) processCaptions(); - + + // Workaround Chrome's lack of an entering Picture in Picture mode event by monitoring all video elements + if (getBrowser() == Browser.CHROME) addVideoElementListeners(); + // Try adding the button to the page if needed if (checkButton()) return; const currentResource = getResource(); @@ -33,8 +36,10 @@ if (domainName in resources) { setResource(resources[domainName]); initialiseCaches(); - - enableCaptions(true); + + if (getBrowser() == Browser.SAFARI) { + enableCaptions(true); + } const observer = new MutationObserver(mutationObserver); observer.observe(document, { diff --git a/src/common/scripts/resources/curiositystream.js b/src/common/scripts/resources/curiositystream.js index b1c49f8..d551c34 100644 --- a/src/common/scripts/resources/curiositystream.js +++ b/src/common/scripts/resources/curiositystream.js @@ -1,10 +1,11 @@ -import { getResource } from './../common.js' +import { Browser, getBrowser, getResource } from './../common.js' export const domain = 'curiositystream'; export const resource = { buttonClassName: 'vjs-control vjs-button', buttonDidAppear: function() { + if (getBrowser() != Browser.SAFARI) return; const video = /** @type {?HTMLVideoElement} */ (getResource().videoElement()); const videoContainer = video.parentElement; video.addEventListener('webkitbeginfullscreen', function() { diff --git a/src/common/scripts/resources/youtube.js b/src/common/scripts/resources/youtube.js index 4373e06..ebb27f5 100644 --- a/src/common/scripts/resources/youtube.js +++ b/src/common/scripts/resources/youtube.js @@ -1,4 +1,4 @@ -import { getResource, bypassBackgroundTimerThrottling } from './../common.js' +import { Browser, getBrowser, getResource, bypassBackgroundTimerThrottling } from './../common.js' import { getButton } from './../button.js' import { enableCaptions, disableCaptions, shouldProcessCaptions } from './../captions.js' @@ -23,19 +23,21 @@ export const resource = { bypassBackgroundTimerThrottling(); // Workaround Safari bug; old captions persist in Picture in Picture mode when MediaSource buffers change - const video = /** @type {?HTMLVideoElement} */ (getResource().videoElement()); - let captionsVisible = false; - const navigateStart = function() { - captionsVisible = shouldProcessCaptions(); - if (captionsVisible) disableCaptions(); - }; - const navigateFinish = function() { - if (captionsVisible) enableCaptions(); - }; - window.addEventListener('spfrequest', navigateStart); - window.addEventListener('spfdone', navigateFinish); - window.addEventListener('yt-navigate-start', navigateStart); - window.addEventListener('yt-navigate-finish', navigateFinish); + if (getBrowser() == Browser.SAFARI) { + const video = /** @type {?HTMLVideoElement} */ (getResource().videoElement()); + let captionsVisible = false; + const navigateStart = function() { + captionsVisible = shouldProcessCaptions(); + if (captionsVisible) disableCaptions(); + }; + const navigateFinish = function() { + if (captionsVisible) enableCaptions(); + }; + window.addEventListener('spfrequest', navigateStart); + window.addEventListener('spfdone', navigateFinish); + window.addEventListener('yt-navigate-start', navigateStart); + window.addEventListener('yt-navigate-finish', navigateFinish); + } }, buttonInsertBefore: function(/** Element */ parent) { return parent.lastChild; diff --git a/src/common/scripts/video.js b/src/common/scripts/video.js index 45440e9..135377e 100644 --- a/src/common/scripts/video.js +++ b/src/common/scripts/video.js @@ -1,4 +1,7 @@ import { info } from './logger.js' +import { Browser, getBrowser } from './common.js' + +const CHROME_PLAYING_PIP_ATTRIBUTE = 'data-playing-picture-in-picture'; /** * Toggles video Picture in Picture @@ -7,10 +10,28 @@ import { info } from './logger.js' */ export const togglePictureInPicture = function(video) { const playingPictureInPicture = videoPlayingPictureInPicture(video); - if (playingPictureInPicture) { - video.webkitSetPresentationMode('inline'); - } else { - video.webkitSetPresentationMode('picture-in-picture'); + switch (getBrowser()) { + case Browser.SAFARI: + if (playingPictureInPicture) { + video.webkitSetPresentationMode('inline'); + } else { + video.webkitSetPresentationMode('picture-in-picture'); + } + break; + case Browser.CHROME: + if (playingPictureInPicture) { + // Workaround Chrome content scripts being unable to call 'exitPictureInPicture' directly + const script = document.createElement('script'); + script.textContent = 'document.exitPictureInPicture()'; + document.head.appendChild(script); + script.remove(); + } else { + video.requestPictureInPicture(); + } + break; + case Browser.UNKNOWN: + default: + break; } }; @@ -21,5 +42,48 @@ export const togglePictureInPicture = function(video) { * @return {boolean} */ export const videoPlayingPictureInPicture = function(video) { - return video.webkitPresentationMode == 'picture-in-picture'; + switch (getBrowser()) { + case Browser.SAFARI: + return video.webkitPresentationMode == 'picture-in-picture'; + case Browser.CHROME: + return video.hasAttribute(CHROME_PLAYING_PIP_ATTRIBUTE); + case Browser.UNKNOWN: + default: + return false; + } +}; + +/** + * Sets Picture in Picture attribute and toggles captions on entering Picture in Picture mode + * + * @param {!Event} event - an enterpictureinpicture event + */ +const videoDidEnterPictureInPicture = function(event) { + info('Video entering Picture in Picture mode'); + + const video = /** @type {HTMLVideoElement} */ (event.target); + + // Toggle captions; Avoid circular referencing by calling 'videoPresentationModeChanged' directly + const presentationEvent = new Event('webkitpresentationmodechanged', { bubbles: true }); + video.dispatchEvent(presentationEvent); + + // Set playing in Picture in Picture mode attribute + video.setAttribute(CHROME_PLAYING_PIP_ATTRIBUTE, true); + + // Remove Picture in Picture attribute and toggle captions on leaving Picture in Picture mode + video.addEventListener('leavepictureinpicture', function(event) { + info('Video leaving Picture in Picture mode'); + video.removeAttribute(CHROME_PLAYING_PIP_ATTRIBUTE); + video.dispatchEvent(presentationEvent); + }, { once: true }); +}; + +/** + * Adds Picture in Picture event listeners to all video elements + */ +export const addVideoElementListeners = function() { + const elements = document.getElementsByTagName('video'); + for (let index = 0, element; element = elements[index]; index++) { + element.addEventListener('enterpictureinpicture', videoDidEnterPictureInPicture); + } }; diff --git a/src/safari-legacy/update.plist b/src/safari-legacy/update.plist index a64852f..1639f89 100644 --- a/src/safari-legacy/update.plist +++ b/src/safari-legacy/update.plist @@ -10,7 +10,7 @@ CFBundleShortVersionString CFBundleVersion - 174 + 175 Developer Identifier BQ6Q24MF9X URL