diff --git a/externs.js b/externs.js new file mode 100644 index 0000000..8cdd475 --- /dev/null +++ b/externs.js @@ -0,0 +1,62 @@ +/* + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +/** + * @externs + */ + +var Windows; + +Windows.UI; + +Windows.UI.ViewManagement; + +Windows.UI.ViewManagement.ApplicationView; + +/** + * @return {{titleBar: ApplicationViewTitleBar}} + */ +Windows.UI.ViewManagement.ApplicationView.getForCurrentView = function() {}; + +/** + * @typedef {{ + * r: number, + * g: number, + * b: number, + * a: number, + * }} + */ +var WindowsColor; + +/** + * @typedef {{ + * foregroundColor: WindowsColor, + * backgroundColor: WindowsColor, + * buttonForegroundColor: WindowsColor, + * buttonBackgroundColor: WindowsColor, + * buttonHoverForegroundColor: WindowsColor, + * buttonHoverBackgroundColor: WindowsColor, + * inactiveForegroundColor: WindowsColor, + * inactiveBackgroundColor: WindowsColor, + * buttonInactiveForegroundColor: WindowsColor, + * buttonInactiveBackgroundColor: WindowsColor, + * buttonInactiveHoverForegroundColor: WindowsColor, + * buttonInactiveHoverBackgroundColor: WindowsColor, + * buttonPressedForegroundColor: WindowsColor, + * buttonPressedBackgroundColor: WindowsColor, + * }} + */ +var ApplicationViewTitleBar; \ No newline at end of file diff --git a/pwacompat.js b/pwacompat.js index 9c89773..450a981 100644 --- a/pwacompat.js +++ b/pwacompat.js @@ -30,6 +30,7 @@ const isSafari = (navigator.vendor && navigator.vendor.indexOf('Apple') !== -1); const isEdge = (navigator.userAgent && navigator.userAgent.indexOf('Edge') !== -1); + const isEdgePWA = (typeof Windows !== 'undefined'); function setup() { const manifestEl = document.head.querySelector('link[rel="manifest"]'); @@ -83,6 +84,7 @@ const isCapable = capableDisplayModes.indexOf(manifest['display']) !== -1; meta('mobile-web-app-capable', isCapable); + updateThemeColor(manifest['theme_color'] || 'black'); if (isEdge) { meta('msapplication-starturl', manifest['start_url'] || '/'); @@ -94,15 +96,13 @@ const backgroundIsLight = shouldUseLightForeground(manifest['background_color'] || defaultSplashColor); - const themeIsLight = shouldUseLightForeground(manifest['theme_color'] || 'black'); const title = manifest['name'] || manifest['short_name'] || document.title; // Add related iTunes app from manifest. const itunes = findAppleId(manifest['related_applications']); itunes && meta('apple-itunes-app', `app-id=${itunes}`); - // nb. Safari 11.3+ gives a deprecation warning about this meta tag. - meta('apple-mobile-web-app-status-bar-style', themeIsLight ? 'default' : 'black'); + // General iOS meta tags. meta('apple-mobile-web-app-capable', isCapable); meta('apple-mobile-web-app-title', title); @@ -211,11 +211,97 @@ return itunes; } + /** + * @param {string} color + */ + function updateThemeColor(color) { + if (!(isSafari || isEdgePWA)) { + return; + } + + const themeIsLight = shouldUseLightForeground(color); + if (isSafari) { + // nb. Safari 11.3+ gives a deprecation warning about this meta tag. + meta('apple-mobile-web-app-status-bar-style', themeIsLight ? 'default' : 'black'); + } else { + // Edge PWA + const t = getEdgeTitleBar(); + if (t === null) { + console.debug('found Windows, could not fetch titleBar') + return; + } + + const foreground = colorToWindowsRGBA(themeIsLight ? 'black' : 'white'); + const background = colorToWindowsRGBA(color); + + t.foregroundColor = foreground; + t.backgroundColor = background; + + t.buttonForegroundColor = foreground; + t.buttonBackgroundColor = background; + + t.inactiveForegroundColor = foreground; + t.inactiveBackgroundColor = background; + + t.buttonInactiveForegroundColor = foreground; + t.buttonInactiveBackgroundColor = background; + + // TODO(samthor): Left out for now to determine Windows defaults. + // t.buttonHoverForegroundColor = background; // inverted + // t.buttonHoverBackgroundColor = foreground; + // + // t.buttonPressedForegroundColor = background; // inverted + // t.buttonPressedBackgroundColor = foreground; + // + // t.buttonInactiveHoverForegroundColor = background; // inverted + // t.buttonInactiveHoverBackgroundColor = foreground; + } + } + + /** + * @return {?ApplicationViewTitleBar} + */ + function getEdgeTitleBar() { + try { + return Windows.UI.ViewManagement.ApplicationView.getForCurrentView().titleBar; + } catch (e) { + return null; + } + } + + /** + * The Windows titlebar APIs expect an object of {r, g, b, a}. + * + * @param {string} color + * @return {WindowsColor} + */ + function colorToWindowsRGBA(color) { + const data = readColor(color); + return /** @type {WindowsColor} */ ({ + 'r': data[0], + 'g': data[1], + 'b': data[2], + 'a': data[3], + }); + } + + /** + * @param {string} color + * @return {!Uint8ClampedArray} + */ + function readColor(color) { + const c = contextForCanvas(); + c.fillStyle = color; + c.fillRect(0, 0, 1, 1); + return c.getImageData(0, 0, 1, 1).data; + } + + /** + * @param {string} color + * @return {boolean} + */ function shouldUseLightForeground(color) { - const lightTestContext = contextForCanvas(); - lightTestContext.fillStyle = color; - lightTestContext.fillRect(0, 0, 1, 1); - const pixelData = lightTestContext.getImageData(0, 0, 1, 1).data; + const pixelData = readColor(color); // From https://cs.chromium.org/chromium/src/chrome/android/java/src/org/chromium/chrome/browser/util/ColorUtils.java const data = pixelData.map((v) => { diff --git a/pwacompat.min.js b/pwacompat.min.js index 4527a56..6ebf0b6 100644 --- a/pwacompat.min.js +++ b/pwacompat.min.js @@ -1,7 +1,9 @@ -(function(){function l(){var a=document.head.querySelector('link[rel="manifest"]'),b=a?a.href:"";Promise.resolve().then(function(){if(!b)throw'can\'t find \'';var a={};"use-credentials"===b.crossOrigin&&(a.credentials="include");return window.fetch(b,a)}).then(function(a){return a.json()}).then(function(a){return n(a,b)}).catch(function(a){return console.warn("pwacompat.js error",a)})}function p(a,b){a=document.createElement(a);for(var c in b)a.setAttribute(c,b[c]); -document.head.appendChild(a);return a}function h(a,b){b&&(!0===b&&(b="yes"),p("meta",{name:a,content:b}))}function n(a,b){function c(e,c,b){var d=e.width,g=e.height,f=window.devicePixelRatio;e=q({width:d*f,height:g*f});e.scale(f,f);e.fillStyle=a.background_color||"#f8f9fa";e.fillRect(0,0,d,g);e.translate(d/2,(g-32)/2);e.font="24px HelveticaNeue-CondensedBold";e.fillStyle=l?"white":"black";d=e.measureText(r).width;b&&(g=b.width/f,f=b.height/f,128a?a/12.92:Math.pow((a+.055)/1.055,2.4)});return 3\'';var a={};"use-credentials"===b.crossOrigin&&(a.credentials="include");return window.fetch(b,a)}).then(function(a){return a.json()}).then(function(a){return z(a,b)}).catch(function(a){return console.warn("pwacompat.js error",a)})}function r(a,b){a=document.createElement(a);for(var c in b)a.setAttribute(c,b[c]); +document.head.appendChild(a);return a}function g(a,b){b&&(!0===b&&(b="yes"),r("meta",{name:a,content:b}))}function z(a,b){function c(b,c,f){var h=b.width,d=b.height,e=window.devicePixelRatio;b=t({width:h*e,height:d*e});b.scale(e,e);b.fillStyle=a.background_color||"#f8f9fa";b.fillRect(0,0,h,d);b.translate(h/2,(d-32)/2);b.font="24px HelveticaNeue-CondensedBold";b.fillStyle=n?"white":"black";h=b.measureText(u).width;f&&(d=f.width/e,e=f.height/e,128a?a/12.92:Math.pow((a+.055)/1.055,2.4)});return 3