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
-------------
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.

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,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 () {
});
}
});
*/

View file

@ -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');
}
});
});

View file

@ -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(
'/(<link rel="shortcut icon" href=")([^"]+)/',
'\\1' . $this->urlbase . 'assets/favicon.ico',
$args['content']
);
$icon = $root_url . '/favicon.ico';
$args['content'] = preg_replace('/(<link rel="shortcut icon" href=")([^"]+)/', '\\1' . $icon, $args['content']);
$args['content'] .= $content;
return $args;
}
@ -131,10 +136,11 @@ class pwa extends rcube_plugin
// as ?PWA=sw.js. This way we don't need to put it in Roundcube root
// and we can set some javascript variables like cache version, etc.
if ($_GET['PWA'] === 'sw.js') {
$rcube = rcube::get_instance();
$rcube->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();