PWA: Support other skins and improve configurability
This commit is contained in:
parent
a2b22e012f
commit
33ea85b5c3
5 changed files with 185 additions and 78 deletions
|
@ -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.
|
||||||
|
|
|
@ -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."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 () {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
|
@ -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(
|
||||||
|
@ -91,7 +93,7 @@ self.addEventListener('fetch', function(event) {
|
||||||
|
|
||||||
self.addEventListener('beforeinstallprompt', function(e) {
|
self.addEventListener('beforeinstallprompt', function(e) {
|
||||||
e.userChoice.then(function(choiceResult) {
|
e.userChoice.then(function(choiceResult) {
|
||||||
if(choiceResult.outcome == 'dismissed') {
|
if (choiceResult.outcome == 'dismissed') {
|
||||||
alert('User cancelled home screen install');
|
alert('User cancelled home screen install');
|
||||||
} else {
|
} else {
|
||||||
alert('User added to home screen');
|
alert('User added to home screen');
|
||||||
|
|
|
@ -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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
pwa::http_request();
|
||||||
|
|
Loading…
Add table
Reference in a new issue