feature (PWA): turn into a progressive web app
|
|
@ -54,14 +54,18 @@ export function http_get(url, cache_expire = 0, type = 'json'){
|
|||
}
|
||||
}else{
|
||||
done(xhr.responseText);
|
||||
}
|
||||
}
|
||||
}else{
|
||||
err({status: xhr.status, message: xhr.responseText || 'Oups something went wrong'});
|
||||
if(navigator.onLine === false){
|
||||
err({status: xhr.status, code: "CONNECTION_LOST", message: 'Connection Lost'});
|
||||
}else{
|
||||
err({status: xhr.status, message: xhr.responseText || 'Oups something went wrong'});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr.open('GET', url, true);
|
||||
xhr.send(null);
|
||||
xhr.send(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -78,7 +82,7 @@ export function http_post(url, data, type = 'json'){
|
|||
}
|
||||
xhr.send(data);
|
||||
xhr.onload = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if(xhr.status === 200){
|
||||
try{
|
||||
let data = JSON.parse(xhr.responseText);
|
||||
|
|
@ -91,11 +95,15 @@ export function http_post(url, data, type = 'json'){
|
|||
}
|
||||
}catch(error){
|
||||
err({message: 'oups', trace: error});
|
||||
}
|
||||
}
|
||||
}else{
|
||||
err({status: xhr.status, message: xhr.responseText || 'Oups something went wrong'});
|
||||
if(navigator.onLine === false){
|
||||
err({status: xhr.status, code: "CONNECTION_LOST", message: 'Connection Lost'});
|
||||
}else{
|
||||
err({status: xhr.status, message: xhr.responseText || 'Oups something went wrong'});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -106,7 +114,7 @@ export function http_delete(url){
|
|||
xhr.open("DELETE", url, true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.onload = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if(xhr.status === 200){
|
||||
try{
|
||||
let data = JSON.parse(xhr.responseText);
|
||||
|
|
@ -119,11 +127,15 @@ export function http_delete(url){
|
|||
}
|
||||
}catch(error){
|
||||
err({message: 'oups', trace: error});
|
||||
}
|
||||
}
|
||||
}else{
|
||||
err({status: xhr.status, message: xhr.responseText || 'Oups something went wrong'});
|
||||
if(navigator.onLine === false){
|
||||
err({status: xhr.status, code: "CONNECTION_LOST", message: 'Connection Lost'});
|
||||
}else{
|
||||
err({status: xhr.status, message: xhr.responseText || 'Oups something went wrong'});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr.send(null);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,23 +1,42 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||
<meta content="yes" name="apple-mobile-web-app-capable">
|
||||
<meta content="Nuage" name="apple-mobile-web-app-title">
|
||||
<meta content="black-translucent" name="apple-mobile-web-app-status-bar-style">
|
||||
<link rel="apple-touch-icon" href="/img/logo.png">
|
||||
|
||||
<title>Nuage</title>
|
||||
<meta name="description" content="browse your files in the cloud">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
<link rel="stylesheet" href="/css/codemirror.css">
|
||||
<link rel="stylesheet" href="/css/codemirror-foldgutter.css">
|
||||
<link rel="stylesheet" href="/css/videojs-sublime-skin.css">
|
||||
<link rel="stylesheet" href="/css/video-js.css">
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Nuage</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta content="yes" name="apple-mobile-web-app-capable">
|
||||
<meta content="Nuage" name="apple-mobile-web-app-title">
|
||||
<meta content="black-translucent" name="apple-mobile-web-app-status-bar-style">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/logo/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/logo/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/logo/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/logo/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/logo/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/logo/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/logo/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/logo/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/logo/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/logo/android-icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/logo/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/logo/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/logo/favicon-16x16.png">
|
||||
<link rel="icon" href="/logo/favicon.ico" type="image/x-icon" />
|
||||
|
||||
<meta name="msapplication-TileColor" content="#f2f2f2">
|
||||
<meta name="msapplication-TileImage" content="/logo/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#f2f2f2">
|
||||
|
||||
<meta name="description" content="browse your files in the cloud">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
<link rel="stylesheet" href="/css/codemirror.css">
|
||||
<link rel="stylesheet" href="/css/codemirror-foldgutter.css">
|
||||
<link rel="stylesheet" href="/css/videojs-sublime-skin.css">
|
||||
<link rel="stylesheet" href="/css/video-js.css">
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
import Router from './router';
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/cache.js').then(function(registration) {
|
||||
}).catch(function(error) {
|
||||
console.log('ServiceWorker registration failed:', error);
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = () => {
|
||||
ReactDOM.render(<Router/>, document.getElementById('main'));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import Path from 'path';
|
|||
|
||||
@EventReceiver
|
||||
@DragDropContext(('ontouchstart' in window)? HTML5Backend : HTML5Backend)
|
||||
export class FilesPage extends React.Component {
|
||||
export class FilesPage extends React.Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
|
|
@ -37,6 +37,7 @@ export class FilesPage extends React.Component {
|
|||
|
||||
|
||||
componentWillMount(){
|
||||
this.setState({error: false});
|
||||
this.onPathUpdate(this.state.path, 'directory', true)
|
||||
}
|
||||
|
||||
|
|
@ -58,9 +59,13 @@ export class FilesPage extends React.Component {
|
|||
|
||||
|
||||
onRefresh(path = this.state.path){
|
||||
this.setState({error: false})
|
||||
return Files.ls(path).then((files) => {
|
||||
this.setState({files: files, loading: false})
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({error: error});
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
onPathUpdate(path, type = 'directory', withLoader = true){
|
||||
|
|
@ -104,7 +109,7 @@ export class FilesPage extends React.Component {
|
|||
icon: 'loading',
|
||||
virtual: true
|
||||
}
|
||||
});
|
||||
});
|
||||
const files = JSON.parse(JSON.stringify(this.state.files));
|
||||
this.setState({files: [].concat(newfiles, files)});
|
||||
return Promise.resolve(_files);
|
||||
|
|
@ -150,7 +155,7 @@ export class FilesPage extends React.Component {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function job(it){
|
||||
let file = it.next();
|
||||
if(file){
|
||||
|
|
@ -168,12 +173,12 @@ export class FilesPage extends React.Component {
|
|||
return job(it);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
const poolSize = 10;
|
||||
return createFilesInUI(files)
|
||||
.then((files) => Promise.resolve(generator(files)))
|
||||
.then((it) => process(it, poolSize))
|
||||
.then((res) => Promise.resolve('ok'));
|
||||
.then((res) => Promise.resolve('ok'));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -183,9 +188,9 @@ export class FilesPage extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
return (
|
||||
<div>
|
||||
<BreadCrumb className="breadcrumb" path={this.state.path} />
|
||||
<div style={{height: this.state.height+'px'}} className="scroll-y">
|
||||
|
|
@ -196,8 +201,8 @@ export class FilesPage extends React.Component {
|
|||
<NgIf cond={this.state.loading}>
|
||||
<NgIf cond={this.state.error === false}>
|
||||
<Loader/>
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.error !== false}>
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.error !== false} onClick={this.componentWillMount.bind(this)} style={{cursor: 'pointer'}}>
|
||||
<Error err={this.state.error}/>
|
||||
</NgIf>
|
||||
</NgIf>
|
||||
|
|
@ -206,6 +211,3 @@ export class FilesPage extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const IDE = (props) => (
|
|||
|
||||
@EventReceiver
|
||||
export class ViewerPage extends React.Component {
|
||||
constructor(props){
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
path: props.match.url.replace('/view', ''),
|
||||
|
|
@ -37,6 +37,34 @@ export class ViewerPage extends React.Component {
|
|||
this.props.subscribe('file.select', this.onPathUpdate.bind(this));
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
this.setState({loading: true, error: false});
|
||||
let app = opener(this.state.path);
|
||||
if(app === 'editor'){
|
||||
Files.cat(this.state.path).then((content) => {
|
||||
this.setState({data: content, loading: false, opener: app});
|
||||
}).catch(err => {
|
||||
if(err && err.code === 'CANCELLED'){ return }
|
||||
if(err.code === 'BINARY_FILE'){
|
||||
Files.url(this.state.path).then((url) => {
|
||||
this.setState({data: url, loading: false, opener: 'download'});
|
||||
}).catch(err => {
|
||||
this.setState({error: err});
|
||||
});
|
||||
}else{
|
||||
this.setState({error: err});
|
||||
}
|
||||
});
|
||||
}else{
|
||||
Files.url(this.state.path).then((url) => {
|
||||
this.setState({data: url, loading: false, opener: app});
|
||||
}).catch(err => {
|
||||
if(err && err.code === 'CANCELLED'){ return }
|
||||
this.setState({error: err});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.unsubscribe('file.select')
|
||||
window.removeEventListener("resize", this.resetHeight);
|
||||
|
|
@ -72,32 +100,6 @@ export class ViewerPage extends React.Component {
|
|||
componentDidMount(){
|
||||
this.resetHeight();
|
||||
window.addEventListener("resize", this.resetHeight);
|
||||
this.setState({loading: true});
|
||||
let app = opener(this.state.path);
|
||||
if(app === 'editor'){
|
||||
Files.cat(this.state.path).then((content) => {
|
||||
this.setState({data: content, loading: false, opener: app});
|
||||
}).catch(err => {
|
||||
if(err && err.code === 'CANCELLED'){ return }
|
||||
if(err.code === 'BINARY_FILE'){
|
||||
Files.url(this.state.path).then((url) => {
|
||||
this.setState({data: url, loading: false, opener: 'download'});
|
||||
}).catch(err => {
|
||||
this.setState({error: err});
|
||||
});
|
||||
}else{
|
||||
this.setState({error: err});
|
||||
}
|
||||
});
|
||||
}else{
|
||||
Files.url(this.state.path).then((url) => {
|
||||
this.setState({data: url, loading: false, opener: app});
|
||||
}).catch(err => {
|
||||
console.log("ERROR", err)
|
||||
if(err && err.code === 'CANCELLED'){ return }
|
||||
this.setState({error: err});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
resetHeight(){
|
||||
|
|
@ -140,13 +142,12 @@ export class ViewerPage extends React.Component {
|
|||
<NgIf cond={this.state.error === false}>
|
||||
<Loader/>
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.error !== false}>
|
||||
<NgIf cond={this.state.error !== false} onClick={this.componentWillMount.bind(this)} style={{cursor: 'pointer'}}>
|
||||
<Error err={this.state.error}/>
|
||||
</NgIf>
|
||||
</NgIf>
|
||||
</NgIf>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
160
server/public/cache.js
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
const CACHE_NAME = 'v0.0';
|
||||
const URLS_TO_CACHE = ['/', '/index.html'];
|
||||
|
||||
self.addEventListener('fetch', function(event){
|
||||
if(is_a_ressource(event.request)){
|
||||
//console.log("> FETCH RESSOURCE", event.request.url)
|
||||
return fetchRessource(event);
|
||||
}else if(is_an_api_call(event.request)){
|
||||
//console.log("> FETCH API", event.request.url)
|
||||
return fetchApi(event);
|
||||
}else if(is_an_index(event.request)){
|
||||
//console.log("> FETCH INDEX", event.request.url)
|
||||
return fetchIndex(event);
|
||||
}else{
|
||||
//console.log("> FETCH FALLBACK", event.request.url)
|
||||
return cacheFallback(event);
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener('activate', function(event){
|
||||
//console.log("> ACTIVATE")
|
||||
vacuum(event)
|
||||
});
|
||||
self.addEventListener('install', function(event){
|
||||
//console.log("> INSTALL SERVICE WORKER", navigator)
|
||||
if (self.skipWaiting) { self.skipWaiting(); }
|
||||
})
|
||||
|
||||
////////////////////////////////////////
|
||||
// ASSETS AND RESSOURCES
|
||||
////////////////////////////////////////
|
||||
|
||||
function is_a_ressource(request){
|
||||
return ['css', 'js', 'img', 'logo', 'manifest.json', 'favicon.ico'].indexOf(pathname(request)[0]) >= 0 ? true : false;
|
||||
}
|
||||
|
||||
/*
|
||||
* cache agressively but refresh the cache if possible
|
||||
*/
|
||||
function fetchRessource(event){
|
||||
event.respondWith(
|
||||
caches.open(CACHE_NAME).then(function(cache){
|
||||
return cache.match(event.request)
|
||||
.then(function(response){
|
||||
if(response){
|
||||
fetchAndCache(event).catch(nil)
|
||||
return response;
|
||||
}else{
|
||||
return Promise.reject("OUPS");
|
||||
}
|
||||
})
|
||||
.catch(function(err){
|
||||
return fetchAndCache(event);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
function fetchAndCache(event){
|
||||
// A request is a stream and can only be consumed once. Since we are consuming this
|
||||
// once by cache and once by the browser for fetch, we need to clone the response as
|
||||
// seen on https://developers.google.com/web/fundamentals/getting-started/primers/service-workers
|
||||
const request = event.request.clone();
|
||||
|
||||
return fetch(request)
|
||||
.then(function(response){
|
||||
if(!response){ return response; }
|
||||
|
||||
// A response is a stream and can only because we want the browser to consume the
|
||||
// response as well as the cache consuming the response, we need to clone it
|
||||
const responseToCache = response.clone();
|
||||
caches.open(CACHE_NAME).then(function(cache){
|
||||
cache.put(event.request, responseToCache);
|
||||
});
|
||||
return response;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////
|
||||
// API CALL
|
||||
////////////////////////////////////////
|
||||
function is_an_api_call(request){
|
||||
return pathname(request)[0] === 'api' ? true : false;
|
||||
}
|
||||
|
||||
function fetchApi(event){
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////
|
||||
// INDEX CALL
|
||||
////////////////////////////////////////
|
||||
function is_an_index(request){
|
||||
return ['login', 'files', 'view', 'logout'].indexOf(pathname(request)[0]) >= 0? true : false;
|
||||
}
|
||||
function fetchIndex(event){
|
||||
event.request.url = host(event.request);
|
||||
event.respondWith(
|
||||
caches.open(CACHE_NAME).then(function(cache){
|
||||
return cache.match('/').then(function(response){
|
||||
return response || fetch('/').then(function(response) {
|
||||
if(response && response.status === 200){
|
||||
cache.put('/', response.clone());
|
||||
}
|
||||
return response;
|
||||
})
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////
|
||||
// OTHER STUFF
|
||||
////////////////////////////////////////
|
||||
function cacheFallback(event){
|
||||
event.respondWith(
|
||||
caches.open(CACHE_NAME).then(function(cache){
|
||||
return cache.match(event.request).then(function(response){
|
||||
if(response){
|
||||
return response;
|
||||
}else{
|
||||
return fetch(event.request.clone())
|
||||
.then(function(response){
|
||||
cache.put(event.request, response.clone())
|
||||
return response;
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////
|
||||
// HELPERS
|
||||
////////////////////////////////////////
|
||||
|
||||
function vacuum(event){
|
||||
return event.waitUntil(
|
||||
caches.keys().then(function(cachesName){
|
||||
return Promise.all(cachesName.map(function(cacheName){
|
||||
if(cacheName !== CACHE_NAME){
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function host(request){
|
||||
return request.url.replace(/(http[s]?\:\/\/[^\/]*\/).*/, '$1');
|
||||
}
|
||||
function pathname(request){
|
||||
return request.url.replace(/^http[s]?:\/\/[^\/]*\//, '').split('/')
|
||||
}
|
||||
function nil(e){}
|
||||
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 2.8 KiB |
BIN
server/public/logo/android-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
server/public/logo/android-icon-192x192.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
server/public/logo/android-icon-36x36.png
Normal file
|
After Width: | Height: | Size: 1 KiB |
BIN
server/public/logo/android-icon-48x48.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
server/public/logo/android-icon-72x72.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
server/public/logo/android-icon-96x96.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
server/public/logo/apple-icon-114x114.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
server/public/logo/apple-icon-120x120.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
server/public/logo/apple-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
server/public/logo/apple-icon-152x152.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
server/public/logo/apple-icon-180x180.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
server/public/logo/apple-icon-57x57.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
server/public/logo/apple-icon-60x60.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
server/public/logo/apple-icon-72x72.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
server/public/logo/apple-icon-76x76.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
server/public/logo/apple-icon-precomposed.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
server/public/logo/apple-icon.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
server/public/logo/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 1,012 B |
BIN
server/public/logo/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
server/public/logo/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
server/public/logo/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
server/public/logo/logo_large.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
server/public/logo/ms-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
server/public/logo/ms-icon-150x150.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
server/public/logo/ms-icon-310x310.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
server/public/logo/ms-icon-70x70.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
47
server/public/manifest.json
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"name": "Manage your files in the cloud with Nuage",
|
||||
"short_name": "Nuage",
|
||||
"icons": [
|
||||
{
|
||||
"src": "logo/android-icon-36x36.png",
|
||||
"sizes": "36x36",
|
||||
"type": "image\/png",
|
||||
"density": "0.75"
|
||||
},
|
||||
{
|
||||
"src": "logo/android-icon-48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image\/png",
|
||||
"density": "1.0"
|
||||
},
|
||||
{
|
||||
"src": "logo/android-icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image\/png",
|
||||
"density": "1.5"
|
||||
},
|
||||
{
|
||||
"src": "logo/android-icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image\/png",
|
||||
"density": "2.0"
|
||||
},
|
||||
{
|
||||
"src": "logo/android-icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image\/png",
|
||||
"density": "3.0"
|
||||
},
|
||||
{
|
||||
"src": "logo/android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image\/png",
|
||||
"density": "4.0"
|
||||
}
|
||||
],
|
||||
"theme_color": "#9AD1ED",
|
||||
"background_color": "#f2f2f2",
|
||||
"orientation": "any",
|
||||
"display": "standalone",
|
||||
"start_url": "/"
|
||||
}
|
||||