diff --git a/plugins/pwa/README.md b/plugins/pwa/README.md index 9fb904ba..652e72ab 100644 --- a/plugins/pwa/README.md +++ b/plugins/pwa/README.md @@ -7,13 +7,25 @@ This is proof-of-concept. CONFIGURATION ------------- -1. Replace images in `/assets` directory to your Company logo. Note, that some -images contain background and some are transpartent. This is to support various -operating systems and the way they use PWAs. +1. Default plugin configuration works with the Elastic skin. Any external skin that +wants to use skin-specific images, app names, etc. need to contain `/pwa` sub-directory +with images as in plugin's `/assets` directory. + +Colors and other metadata is configurable via skin's meta.json file. For example: +``` + "config": { + "pwa_name": "Roundcube", + "pwa_theme_color": "#f4f4f4" + } +``` https://realfavicongenerator.net/ will help you with creating images automatically and choosing some colors. -2. You can also modify `/assets/manifest.json` to your liking. +2. Configure the plugin. + +Copy config.inc.php.dist to config.inc.php and change config options to your liking. +Any option set in the plugin config will have a preference over skin configuration +described above. 3. Enable the plugin in Roundcube configuration file. diff --git a/plugins/pwa/assets/manifest.json b/plugins/pwa/assets/manifest.json deleted file mode 100644 index 762c9a31..00000000 --- a/plugins/pwa/assets/manifest.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "Roundcube", - "short_name": "Roundcube", - "description": "Free and Open Source Webmail.", - "lang": "en-US", - "start_url": ".", - "display": "standalone", - "theme_color": "#f4f4f4", - "background_color": "#ffffff", - "pinned_tab_color": "#37beff", - "scope": "/", - "icons": [ - { - "src": "android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "permissions": { - "desktop-notification": { - "description": "Needed for notifying you of any changes to your account." - } - } -} diff --git a/plugins/pwa/js/pwa.js b/plugins/pwa/js/pwa.js index 21b858be..1dff6705 100644 --- a/plugins/pwa/js/pwa.js +++ b/plugins/pwa/js/pwa.js @@ -25,18 +25,18 @@ * for the JavaScript code in this file. */ -/* SERVICE WORKER - REQUIRED */ +// Service worker (required by Android/Chrome but not iOS/Safari) if ('serviceWorker' in navigator) { - navigator.serviceWorker - .register('?PWA=sw.js') - .then(function(reg) { - console.log("ServiceWorker registered", reg); - }) - .catch(function(error) { - console.log("Failed to register ServiceWorker", error); - }); + navigator.serviceWorker.register('?PWA=sw.js') + .then(function(reg) { + console.log("ServiceWorker registered", reg); + }) + .catch(function(error) { + console.log("Failed to register ServiceWorker", error); + }); } +/* function registerOneTimeSync() { if (navigator.serviceWorker.controller) { navigator.serviceWorker.ready.then(function(reg) { @@ -58,8 +58,9 @@ function registerOneTimeSync() { console.log("No active ServiceWorker"); } } +*/ -/* OFFLINE BANNER */ +// Offline banner function updateOnlineStatus() { // FIXME fill in something that makes sense in roundcube // var d = document.body; @@ -75,7 +76,8 @@ window.addEventListener('load', function() { window.addEventListener('offline', updateOnlineStatus); }); -/* CHANGE PAGE TITLE BASED ON PAGE VISIBILITY */ +/* TODO: Do we need this to anything useful? +// Change page title depending on document visibility function handleVisibilityChange() { if (document.visibilityState == "hidden") { document.title = "Hey! Come back!"; @@ -86,8 +88,10 @@ function handleVisibilityChange() { var original_title = document.title; document.addEventListener('visibilitychange', handleVisibilityChange, false); +*/ -/* NOTIFICATIONS */ +/* TODO: Do we need this to anything useful? +// Notifications window.addEventListener('load', function () { // At first, let's check if we have permission for notification // If not, let's ask for it @@ -99,3 +103,4 @@ window.addEventListener('load', function () { }); } }); +*/ diff --git a/plugins/pwa/js/sw.js b/plugins/pwa/js/sw.js index add433a8..c0468c42 100644 --- a/plugins/pwa/js/sw.js +++ b/plugins/pwa/js/sw.js @@ -68,7 +68,9 @@ self.addEventListener('fetch', function(event) { // Ignore non-get request like when accessing the admin panel // if (event.request.method !== 'GET') { return; } // Don't try to handle non-secure assets because fetch will fail - if (/http:/.test(event.request.url)) { return; } + if (/http:/.test(event.request.url)) { + return; + } // Here's where we cache all the things! event.respondWith( @@ -91,10 +93,10 @@ self.addEventListener('fetch', function(event) { self.addEventListener('beforeinstallprompt', function(e) { e.userChoice.then(function(choiceResult) { - if(choiceResult.outcome == 'dismissed') { - alert('User cancelled home screen install'); + if (choiceResult.outcome == 'dismissed') { + alert('User cancelled home screen install'); } else { - alert('User added to home screen'); + alert('User added to home screen'); } }); }); diff --git a/plugins/pwa/pwa.php b/plugins/pwa/pwa.php index 7e4382e6..1c9d3fbe 100644 --- a/plugins/pwa/pwa.php +++ b/plugins/pwa/pwa.php @@ -29,8 +29,8 @@ class pwa extends rcube_plugin /** @var string $version Plugin version */ public static $version = '0.1'; - /** @var array|null $config Plugin config (from manifest.json) */ - private $config; + /** @var array $config Plugin config (from manifest.json) */ + private static $config; /** @@ -49,49 +49,54 @@ class pwa extends rcube_plugin */ public function template_object_links($args) { + $rcube = rcube::get_instance(); $config = $this->get_config(); $content = ''; $links = array( array( 'rel' => 'manifest', - 'href' => $this->urlbase . 'assets/manifest.json', + 'href' => '?PWA=manifest.json', //$this->urlbase . 'assets/manifest.json', ), array( 'rel' => 'apple-touch-icon', 'sizes' => '180x180', - 'href' => $this->urlbase . 'assets/apple-touch-icon.png' + 'href' => 'apple-touch-icon.png' ), array( 'rel' => 'icon', 'type' => 'image/png', 'sizes' => '32x32', - 'href' => $this->urlbase . 'assets/favicon-32x32.png' + 'href' => 'favicon-32x32.png' ), array( 'rel' => 'icon', 'type' => 'image/png', 'sizes' => '16x16', - 'href' => $this->urlbase . 'assets/favicon-16x16.png' + 'href' => 'favicon-16x16.png' ), array( 'rel' => 'mask-icon', - 'href' => $this->urlbase . 'assets/safari-pinned-tab.svg', + 'href' => 'safari-pinned-tab.svg', 'color' => $config['pinned_tab_color'] ?: $config['theme_color'], ), ); + // Check if the skin contains /pwa directory + $root_url = $rcube->find_asset('skins/' . $config['skin'] . '/pwa') ?: ($this->urlbase . 'assets'); + foreach ($links as $link) { + if ($link['href'][0] != '?') { + $link['href'] = $root_url . '/' . $link['href']; + } + $content .= html::tag('link', $link) . "\n"; } - $args['content'] .= $content; - // replace favicon.ico - $args['content'] = preg_replace( - '/(urlbase . 'assets/favicon.ico', - $args['content'] - ); + $icon = $root_url . '/favicon.ico'; + $args['content'] = preg_replace('/(task = 'pwa'; - + $rcube = rcube::get_instance(); + $rcube->task = 'pwa'; + $rcube->action = 'sw.js'; if ($file = $rcube->find_asset('plugins/pwa/js/sw.js')) { + // TODO: use caching headers? header('Content-Type: application/javascript'); // TODO: What assets do we want to cache? @@ -153,32 +159,143 @@ class pwa extends rcube_plugin header('HTTP/1.0 404 PWA plugin error'); exit; } + + // We genarate manifest.json file from skin/plugin config + if ($_GET['PWA'] === 'manifest.json') { + $rcube = rcube::get_instance(); + $rcube->task = 'pwa'; + $rcube->action = 'manifest.json'; + + // Read skin/plugin config + $config = self::get_config(); + + // HTTP scope + $scope = preg_replace('|/*\?.*$|', '', $_SERVER['REQUEST_URI']); + $scope = strlen($scope) ? $scope : ''; + + // Check if the skin contains /pwa directory + $root_url = $rcube->find_asset('skins/' . $config['skin'] . '/pwa') ?: ('plugins/pwa/assets'); + + // Manifest defaults + $defaults = array( + 'name' => null, + 'short_name' => $config['name'], + 'description' => 'Free and Open Source Webmail', + 'lang' => 'en-US', + 'theme_color' => null, + 'background_color' => null, + 'pinned_tab_color' => null, + 'icons' => array( + array( + 'src' => $root_url . '/android-chrome-192x192.png', + 'sizes' => '192x192', + 'type' => 'image/png', + ), + array( + 'src' => $root_url . '/android-chrome-512x512.png', + 'sizes' => '512x512', + 'type' => 'image/png', + ), + ), + ); + + $manifest = array( + 'scope' => $scope, + 'start_url' => '?PWAMODE=1', + 'display' => 'standalone', +/* + 'permissions' => array( + 'desktop-notification' => array( + 'description' => "Needed for notifying you of any changes to your account." + ), + ), +*/ + ); + + // Build manifest data from config and defaults + foreach ($defaults as $name => $value) { + if (isset($config[$name])) { + $value = $config[$name]; + } + + $manifest[$name] = $value; + } + + // Send manifest.json to the browser + // TODO: use caching headers? + header('Content-Type: application/json'); + echo rcube_output::json_serialize($manifest, (bool) $rcube->config->get('devel_mode')); + exit; + } } /** - * Read configuration from manifest.json + * Load plugin and skin configuration * * @return array Key-value configuration */ - private function get_config() + private static function get_config() { - if (is_array($this->config)) { - return $this->config; + if (is_array(self::$config)) { + return self::$config; } - $config = array(); - $defaults = array( - 'tile_color' => '#2d89ef', - 'theme_color' => '#f4f4f4', + $rcube = rcube::get_instance(); + $config = array(); + self::$config = array(); + $defaults = array( + 'tile_color' => '#2d89ef', + 'theme_color' => '#f4f4f4', + 'pinned_tab_color' => '#37beff', + 'background_color' => '#ffffff', + 'skin' => $rcube->config->get('skin') ?: 'elastic', + 'name' => $rcube->config->get('product_name') ?: 'Roundcube', ); - if ($file = rcube::get_instance()->find_asset('plugins/pwa/assets/manifest.json')) { - $config = json_decode(file_get_contents(INSTALL_PATH . $file), true); + // Load plugin config into $config var + $fpath = __DIR__ . '/config.inc.php'; + if (is_file($fpath) && is_readable($fpath)) { + ob_start(); + include($fpath); + ob_end_clean(); } - return $this->config = array_merge($defaults, $config); + // Load skin config + $meta = @file_get_contents(RCUBE_INSTALL_PATH . '/skins/' . $defaults['skin'] . '/meta.json'); + $meta = @json_decode($meta, true); + + if ($meta && $meta['extends']) { + // Merge with parent skin config + $root_meta = @file_get_contents(RCUBE_INSTALL_PATH . '/skins/' . $meta['extends'] . '/meta.json'); + $root_meta = @json_decode($meta, true); + + if ($root_meta && !empty($root_meta['config'])) { + $meta['config'] = array_merge((array) $root_meta['config'], (array) $meta['config']); + } + } + + foreach ((array) $meta['config'] as $name => $value) { + if (strpos($name, 'pwa_') === 0 && !isset($config[$name])) { + $config[$name] = $value; + } + } + + foreach ($config as $name => $value) { + $name = preg_replace('/^pwa_/', '', $name); + if ($value !== null) { + self::$config[$name] = $value; + } + } + + foreach ($defaults as $name => $value) { + if (!array_key_exists($name, self::$config)) { + self::$config[$name] = $value; + } + } + + return self::$config; } } -// Hijack HTTP requests to plugin assets e.g. service worker +// Hijack HTTP requests to special plugin assets e.g. sw.js, manifest.json pwa::http_request();