|
@ -0,0 +1 @@
|
||||||
|
from . import controllers
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Copyright 2020 Lorenzo Battistini @ TAKOBI
|
||||||
|
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Progressive web application",
|
||||||
|
"summary": "Make Odoo a PWA",
|
||||||
|
"version": "12.0.1.0.0",
|
||||||
|
"development_status": "Beta",
|
||||||
|
"category": "Website",
|
||||||
|
"website": "https://github.com/OCA/web",
|
||||||
|
"author": "TAKOBI, Odoo Community Association (OCA)",
|
||||||
|
"maintainers": ["eLBati"],
|
||||||
|
"license": "LGPL-3",
|
||||||
|
"application": True,
|
||||||
|
"installable": True,
|
||||||
|
"depends": [
|
||||||
|
'web',
|
||||||
|
'mail',
|
||||||
|
],
|
||||||
|
"data": [
|
||||||
|
"views/webclient_templates.xml",
|
||||||
|
],
|
||||||
|
'qweb': [
|
||||||
|
'static/src/xml/pwa_install.xml',
|
||||||
|
],
|
||||||
|
'images': ['static/description/pwa.png'],
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
from . import main
|
|
@ -0,0 +1,69 @@
|
||||||
|
from odoo.http import request, Controller, route
|
||||||
|
|
||||||
|
|
||||||
|
class PWA(Controller):
|
||||||
|
|
||||||
|
def get_asset_urls(self, asset_xml_id):
|
||||||
|
qweb = request.env['ir.qweb'].sudo()
|
||||||
|
assets = qweb._get_asset_nodes(asset_xml_id, {}, True, True)
|
||||||
|
urls = []
|
||||||
|
for asset in assets:
|
||||||
|
if asset[0] == 'link':
|
||||||
|
urls.append(asset[1]['href'])
|
||||||
|
if asset[0] == 'script':
|
||||||
|
urls.append(asset[1]['src'])
|
||||||
|
return urls
|
||||||
|
|
||||||
|
@route('/service-worker.js', type='http', auth="public")
|
||||||
|
def service_worker(self):
|
||||||
|
qweb = request.env['ir.qweb'].sudo()
|
||||||
|
urls = []
|
||||||
|
urls.extend(self.get_asset_urls("web.assets_common"))
|
||||||
|
urls.extend(self.get_asset_urls("web.assets_backend"))
|
||||||
|
version_list = []
|
||||||
|
for url in urls:
|
||||||
|
version_list.append(url.split('/')[3])
|
||||||
|
cache_version = '-'.join(version_list)
|
||||||
|
mimetype = 'text/javascript;charset=utf-8'
|
||||||
|
content = qweb.render('web_pwa.service_worker', {
|
||||||
|
'pwa_cache_name': cache_version,
|
||||||
|
'pwa_files_to_cache': urls,
|
||||||
|
})
|
||||||
|
return request.make_response(content, [('Content-Type', mimetype)])
|
||||||
|
|
||||||
|
@route('/web_pwa/manifest.json', type='http', auth="public")
|
||||||
|
def manifest(self):
|
||||||
|
qweb = request.env['ir.qweb'].sudo()
|
||||||
|
config_param = request.env['ir.config_parameter'].sudo()
|
||||||
|
pwa_name = config_param.get_param("pwa.manifest.name", "Odoo PWA")
|
||||||
|
pwa_short_name = config_param.get_param("pwa.manifest.short_name", "Odoo PWA")
|
||||||
|
icon128x128 = config_param.get_param(
|
||||||
|
"pwa.manifest.icon128x128", "/web_pwa/static/img/icons/icon-128x128.png")
|
||||||
|
icon144x144 = config_param.get_param(
|
||||||
|
"pwa.manifest.icon144x144", "/web_pwa/static/img/icons/icon-144x144.png")
|
||||||
|
icon152x152 = config_param.get_param(
|
||||||
|
"pwa.manifest.icon152x152", "/web_pwa/static/img/icons/icon-152x152.png")
|
||||||
|
icon192x192 = config_param.get_param(
|
||||||
|
"pwa.manifest.icon192x192", "/web_pwa/static/img/icons/icon-192x192.png")
|
||||||
|
icon256x256 = config_param.get_param(
|
||||||
|
"pwa.manifest.icon256x256", "/web_pwa/static/img/icons/icon-256x256.png")
|
||||||
|
icon512x512 = config_param.get_param(
|
||||||
|
"pwa.manifest.icon512x512", "/web_pwa/static/img/icons/icon-512x512.png")
|
||||||
|
background_color = config_param.get_param(
|
||||||
|
"pwa.manifest.background_color", "#2E69B5")
|
||||||
|
theme_color = config_param.get_param(
|
||||||
|
"pwa.manifest.theme_color", "#2E69B5")
|
||||||
|
mimetype = 'application/json;charset=utf-8'
|
||||||
|
content = qweb.render('web_pwa.manifest', {
|
||||||
|
'pwa_name': pwa_name,
|
||||||
|
'pwa_short_name': pwa_short_name,
|
||||||
|
'icon128x128': icon128x128,
|
||||||
|
'icon144x144': icon144x144,
|
||||||
|
'icon152x152': icon152x152,
|
||||||
|
'icon192x192': icon192x192,
|
||||||
|
'icon256x256': icon256x256,
|
||||||
|
'icon512x512': icon512x512,
|
||||||
|
'background_color': background_color,
|
||||||
|
'theme_color': theme_color,
|
||||||
|
})
|
||||||
|
return request.make_response(content, [('Content-Type', mimetype)])
|
|
@ -0,0 +1,10 @@
|
||||||
|
The following system parameters con be set to customize the appearance of the application
|
||||||
|
|
||||||
|
* pwa.manifest.name (defaults to "Odoo PWA")
|
||||||
|
* pwa.manifest.short_name (defaults to "Odoo PWA")
|
||||||
|
* pwa.manifest.icon128x128 (defaults to "/web_pwa/static/img/icons/icon-128x128.png")
|
||||||
|
* pwa.manifest.icon144x144 (defaults to "/web_pwa/static/img/icons/icon-144x144.png")
|
||||||
|
* pwa.manifest.icon152x152 (defaults to "/web_pwa/static/img/icons/icon-152x152.png")
|
||||||
|
* pwa.manifest.icon192x192 (defaults to "/web_pwa/static/img/icons/icon-192x192.png")
|
||||||
|
* pwa.manifest.icon256x256 (defaults to "/web_pwa/static/img/icons/icon-256x256.png")
|
||||||
|
* pwa.manifest.icon512x512 (defaults to "/web_pwa/static/img/icons/icon-512x512.png")
|
|
@ -0,0 +1,3 @@
|
||||||
|
* `TAKOBI <https://takobi.online>`_:
|
||||||
|
|
||||||
|
* Lorenzo Battistini
|
|
@ -0,0 +1,5 @@
|
||||||
|
Make Odoo an installable Progressive Web Application.
|
||||||
|
|
||||||
|
Progressive Web Apps provide an installable, app-like experience on desktop and mobile that are built and delivered directly via the web.
|
||||||
|
They're web apps that are fast and reliable. And most importantly, they're web apps that work in any browser.
|
||||||
|
If you're building a web app today, you're already on the path towards building a Progressive Web App.
|
|
@ -0,0 +1,13 @@
|
||||||
|
After having installed this module, browsing your odoo on mobile you will be able to install it as a PWA.
|
||||||
|
|
||||||
|
It is strongly recommended to use this module with a responsive layout, like the one provided by web_responsive.
|
||||||
|
|
||||||
|
This module is intended to be used by Odoo back-end users (employees).
|
||||||
|
|
||||||
|
When a Progressive Web App is installed, it looks and behaves like all of the other installed apps.
|
||||||
|
It launches from the same place that other apps launch. It runs in an app without an address bar or other browser UI.
|
||||||
|
And like all other installed apps, it's a top level app in the task switcher.
|
||||||
|
|
||||||
|
In Chrome, a Progressive Web App can either be installed through the three-dot context menu.
|
||||||
|
|
||||||
|
This module also provides a "Install PWA" link in Odoo user menu.
|
|
@ -0,0 +1,5 @@
|
||||||
|
* Evaluate to extend ``FILES_TO_CACHE``
|
||||||
|
* Evaluate to use a normal JS file for service worker and download data from a normal JSON controller
|
||||||
|
* Integrate `Notification API <https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification>`_
|
||||||
|
* Integrate `Web Share API <https://web.dev/web-share/>`_
|
||||||
|
* Create ``portal_pwa`` module, intended to be used by front-end users (customers, suppliers...)
|
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 16 KiB |
|
@ -0,0 +1,45 @@
|
||||||
|
odoo.define('web_pwa.systray.install', function (require) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var core = require('web.core');
|
||||||
|
var session = require('web.session');
|
||||||
|
var UserMenu = require('web.UserMenu');
|
||||||
|
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
navigator.serviceWorker.register('/service-worker.js')
|
||||||
|
.then(function (reg) {
|
||||||
|
console.log('Service worker registered.', reg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var deferredInstallPrompt = null;
|
||||||
|
|
||||||
|
UserMenu.include({
|
||||||
|
start: function () {
|
||||||
|
window.addEventListener('beforeinstallprompt', this.saveBeforeInstallPromptEvent);
|
||||||
|
return this._super.apply(this, arguments);
|
||||||
|
},
|
||||||
|
saveBeforeInstallPromptEvent: function(evt) {
|
||||||
|
deferredInstallPrompt = evt;
|
||||||
|
this.$.find('#pwa_install_button')[0].removeAttribute('hidden');
|
||||||
|
},
|
||||||
|
_onMenuInstallpwa: function () {
|
||||||
|
deferredInstallPrompt.prompt();
|
||||||
|
// Hide the install button, it can't be called twice.
|
||||||
|
this.el.setAttribute('hidden', true);
|
||||||
|
// Log user response to prompt.
|
||||||
|
deferredInstallPrompt.userChoice
|
||||||
|
.then(function (choice) {
|
||||||
|
if (choice.outcome === 'accepted') {
|
||||||
|
console.log('User accepted the A2HS prompt', choice);
|
||||||
|
} else {
|
||||||
|
console.log('User dismissed the A2HS prompt', choice);
|
||||||
|
}
|
||||||
|
deferredInstallPrompt = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates>
|
||||||
|
|
||||||
|
<t t-name="web_pwa.systray.install">
|
||||||
|
<a href="#" role="menuitem" id="pwa_install_button" data-menu="installpwa" class="dropdown-item" hidden="1">Install PWA</a>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-extend="UserMenu.Actions">
|
||||||
|
<t t-jquery="a[data-menu='settings']" t-operation="after">
|
||||||
|
<t t-call="web_pwa.systray.install"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
|
@ -0,0 +1,105 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<template id="web_layout_pwa" name="Web layout PWA" inherit_id="web.layout">
|
||||||
|
<xpath expr="//meta[@name='viewport']" position="after">
|
||||||
|
<!-- Add link rel manifest -->
|
||||||
|
<link rel="manifest" href="/web_pwa/manifest.json"/>
|
||||||
|
<!-- Add iOS meta tags and icons -->
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes"/>
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Odoo PWA"/>
|
||||||
|
<link rel="apple-touch-icon" href="/web_pwa/static/img/icons/icon-152x152.png"/>
|
||||||
|
<!-- Add meta theme-color -->
|
||||||
|
<meta name="theme-color" content="#2E69B5" />
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
<template id="assets_backend" name="PWA assets" inherit_id="web.assets_backend">
|
||||||
|
<xpath expr="." position="inside">
|
||||||
|
<script type="text/javascript" src="/web_pwa/static/src/js/pwa_install.js"/>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
<template id="service_worker" name="PWA service worker">
|
||||||
|
'use strict';
|
||||||
|
const CACHE_NAME = '<t t-esc="pwa_cache_name"/>';
|
||||||
|
const FILES_TO_CACHE = [
|
||||||
|
<t t-foreach="pwa_files_to_cache" t-as="file_to_cache">
|
||||||
|
'<t t-esc="file_to_cache"/>',
|
||||||
|
</t>
|
||||||
|
];
|
||||||
|
self.addEventListener('install', function (evt) {
|
||||||
|
console.log('[ServiceWorker] Install');
|
||||||
|
evt.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then(function (cache) {
|
||||||
|
console.log('[ServiceWorker] Pre-caching offline page');
|
||||||
|
return cache.addAll(FILES_TO_CACHE);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
self.addEventListener('activate', function(evt) {
|
||||||
|
console.log('[ServiceWorker] Activate');
|
||||||
|
evt.waitUntil(
|
||||||
|
caches.keys().then(function(keyList) {
|
||||||
|
return Promise.all(keyList.map(function(key) {
|
||||||
|
if (key !== CACHE_NAME) {
|
||||||
|
console.log('[ServiceWorker] Removing old cache', key);
|
||||||
|
return caches.delete(key);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
self.clients.claim();
|
||||||
|
});
|
||||||
|
self.addEventListener('fetch', function(evt) {
|
||||||
|
if (evt.request.cache === 'only-if-cached' && evt.request.mode !== 'same-origin') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('[ServiceWorker] Fetch', evt.request.url);
|
||||||
|
evt.respondWith(
|
||||||
|
caches.open(CACHE_NAME).then(function(cache) {
|
||||||
|
return cache.match(evt.request)
|
||||||
|
.then(function(response) {
|
||||||
|
return response || fetch(evt.request);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="manifest" name="PWA manifest">
|
||||||
|
{
|
||||||
|
"name": "<t t-esc="pwa_name"/>",
|
||||||
|
"short_name": "<t t-esc="pwa_short_name"/>",
|
||||||
|
"icons": [{
|
||||||
|
"src": "<t t-esc="icon128x128"/>",
|
||||||
|
"sizes": "128x128",
|
||||||
|
"type": "image/png"
|
||||||
|
}, {
|
||||||
|
"src": "<t t-esc="icon144x144"/>",
|
||||||
|
"sizes": "144x144",
|
||||||
|
"type": "image/png"
|
||||||
|
}, {
|
||||||
|
"src": "<t t-esc="icon152x152"/>",
|
||||||
|
"sizes": "152x152",
|
||||||
|
"type": "image/png"
|
||||||
|
}, {
|
||||||
|
"src": "<t t-esc="icon192x192"/>",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
}, {
|
||||||
|
"src": "<t t-esc="icon256x256"/>",
|
||||||
|
"sizes": "256x256",
|
||||||
|
"type": "image/png"
|
||||||
|
}, {
|
||||||
|
"src": "<t t-esc="icon512x512"/>",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}],
|
||||||
|
"start_url": "/web",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "<t t-esc="background_color"/>",
|
||||||
|
"theme_color": "<t t-esc="theme_color"/>"
|
||||||
|
}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</odoo>
|