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();