PWA: Support other skins and improve configurability

This commit is contained in:
Aleksander Machniak 2019-09-07 17:53:07 +02:00
parent a2b22e012f
commit 33ea85b5c3
5 changed files with 185 additions and 78 deletions

View file

@ -7,13 +7,25 @@ This is proof-of-concept.
CONFIGURATION CONFIGURATION
------------- -------------
1. Replace images in `/assets` directory to your Company logo. Note, that some 1. Default plugin configuration works with the Elastic skin. Any external skin that
images contain background and some are transpartent. This is to support various wants to use skin-specific images, app names, etc. need to contain `/pwa` sub-directory
operating systems and the way they use PWAs. 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 https://realfavicongenerator.net/ will help you with creating images automatically
and choosing some colors. 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. 3. Enable the plugin in Roundcube configuration file.

View file

@ -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."
}
}
}

View file

@ -25,10 +25,9 @@
* for the JavaScript code in this file. * for the JavaScript code in this file.
*/ */
/* SERVICE WORKER - REQUIRED */ // Service worker (required by Android/Chrome but not iOS/Safari)
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker navigator.serviceWorker.register('?PWA=sw.js')
.register('?PWA=sw.js')
.then(function(reg) { .then(function(reg) {
console.log("ServiceWorker registered", reg); console.log("ServiceWorker registered", reg);
}) })
@ -37,6 +36,7 @@ if ('serviceWorker' in navigator) {
}); });
} }
/*
function registerOneTimeSync() { function registerOneTimeSync() {
if (navigator.serviceWorker.controller) { if (navigator.serviceWorker.controller) {
navigator.serviceWorker.ready.then(function(reg) { navigator.serviceWorker.ready.then(function(reg) {
@ -58,8 +58,9 @@ function registerOneTimeSync() {
console.log("No active ServiceWorker"); console.log("No active ServiceWorker");
} }
} }
*/
/* OFFLINE BANNER */ // Offline banner
function updateOnlineStatus() { function updateOnlineStatus() {
// FIXME fill in something that makes sense in roundcube // FIXME fill in something that makes sense in roundcube
// var d = document.body; // var d = document.body;
@ -75,7 +76,8 @@ window.addEventListener('load', function() {
window.addEventListener('offline', updateOnlineStatus); 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() { function handleVisibilityChange() {
if (document.visibilityState == "hidden") { if (document.visibilityState == "hidden") {
document.title = "Hey! Come back!"; document.title = "Hey! Come back!";
@ -86,8 +88,10 @@ function handleVisibilityChange() {
var original_title = document.title; var original_title = document.title;
document.addEventListener('visibilitychange', handleVisibilityChange, false); document.addEventListener('visibilitychange', handleVisibilityChange, false);
*/
/* NOTIFICATIONS */ /* TODO: Do we need this to anything useful?
// Notifications
window.addEventListener('load', function () { window.addEventListener('load', function () {
// At first, let's check if we have permission for notification // At first, let's check if we have permission for notification
// If not, let's ask for it // If not, let's ask for it
@ -99,3 +103,4 @@ window.addEventListener('load', function () {
}); });
} }
}); });
*/

View file

@ -68,7 +68,9 @@ self.addEventListener('fetch', function(event) {
// Ignore non-get request like when accessing the admin panel // Ignore non-get request like when accessing the admin panel
// if (event.request.method !== 'GET') { return; } // if (event.request.method !== 'GET') { return; }
// Don't try to handle non-secure assets because fetch will fail // 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! // Here's where we cache all the things!
event.respondWith( event.respondWith(

View file

@ -29,8 +29,8 @@ class pwa extends rcube_plugin
/** @var string $version Plugin version */ /** @var string $version Plugin version */
public static $version = '0.1'; public static $version = '0.1';
/** @var array|null $config Plugin config (from manifest.json) */ /** @var array $config Plugin config (from manifest.json) */
private $config; private static $config;
/** /**
@ -49,49 +49,54 @@ class pwa extends rcube_plugin
*/ */
public function template_object_links($args) public function template_object_links($args)
{ {
$rcube = rcube::get_instance();
$config = $this->get_config(); $config = $this->get_config();
$content = ''; $content = '';
$links = array( $links = array(
array( array(
'rel' => 'manifest', 'rel' => 'manifest',
'href' => $this->urlbase . 'assets/manifest.json', 'href' => '?PWA=manifest.json', //$this->urlbase . 'assets/manifest.json',
), ),
array( array(
'rel' => 'apple-touch-icon', 'rel' => 'apple-touch-icon',
'sizes' => '180x180', 'sizes' => '180x180',
'href' => $this->urlbase . 'assets/apple-touch-icon.png' 'href' => 'apple-touch-icon.png'
), ),
array( array(
'rel' => 'icon', 'rel' => 'icon',
'type' => 'image/png', 'type' => 'image/png',
'sizes' => '32x32', 'sizes' => '32x32',
'href' => $this->urlbase . 'assets/favicon-32x32.png' 'href' => 'favicon-32x32.png'
), ),
array( array(
'rel' => 'icon', 'rel' => 'icon',
'type' => 'image/png', 'type' => 'image/png',
'sizes' => '16x16', 'sizes' => '16x16',
'href' => $this->urlbase . 'assets/favicon-16x16.png' 'href' => 'favicon-16x16.png'
), ),
array( array(
'rel' => 'mask-icon', 'rel' => 'mask-icon',
'href' => $this->urlbase . 'assets/safari-pinned-tab.svg', 'href' => 'safari-pinned-tab.svg',
'color' => $config['pinned_tab_color'] ?: $config['theme_color'], '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) { foreach ($links as $link) {
if ($link['href'][0] != '?') {
$link['href'] = $root_url . '/' . $link['href'];
}
$content .= html::tag('link', $link) . "\n"; $content .= html::tag('link', $link) . "\n";
} }
$args['content'] .= $content;
// replace favicon.ico // replace favicon.ico
$args['content'] = preg_replace( $icon = $root_url . '/favicon.ico';
'/(<link rel="shortcut icon" href=")([^"]+)/', $args['content'] = preg_replace('/(<link rel="shortcut icon" href=")([^"]+)/', '\\1' . $icon, $args['content']);
'\\1' . $this->urlbase . 'assets/favicon.ico',
$args['content'] $args['content'] .= $content;
);
return $args; return $args;
} }
@ -133,8 +138,9 @@ class pwa extends rcube_plugin
if ($_GET['PWA'] === 'sw.js') { if ($_GET['PWA'] === 'sw.js') {
$rcube = rcube::get_instance(); $rcube = rcube::get_instance();
$rcube->task = 'pwa'; $rcube->task = 'pwa';
$rcube->action = 'sw.js';
if ($file = $rcube->find_asset('plugins/pwa/js/sw.js')) { if ($file = $rcube->find_asset('plugins/pwa/js/sw.js')) {
// TODO: use caching headers?
header('Content-Type: application/javascript'); header('Content-Type: application/javascript');
// TODO: What assets do we want to cache? // 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'); header('HTTP/1.0 404 PWA plugin error');
exit; 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 * @return array Key-value configuration
*/ */
private function get_config() private static function get_config()
{ {
if (is_array($this->config)) { if (is_array(self::$config)) {
return $this->config; return self::$config;
} }
$rcube = rcube::get_instance();
$config = array(); $config = array();
self::$config = array();
$defaults = array( $defaults = array(
'tile_color' => '#2d89ef', 'tile_color' => '#2d89ef',
'theme_color' => '#f4f4f4', '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')) { // Load plugin config into $config var
$config = json_decode(file_get_contents(INSTALL_PATH . $file), true); $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']);
} }
} }
// Hijack HTTP requests to plugin assets e.g. service worker 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 special plugin assets e.g. sw.js, manifest.json
pwa::http_request(); pwa::http_request();