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
|
||||
-------------
|
||||
|
||||
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.
|
||||
|
|
|
@ -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,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 () {
|
|||
});
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Reference in a new issue