Progressive web app (#367)

* Basic PWA support

* Theme color

* Removed manifest reference added by angular cli

* Added update notification

* Basic install widget

* Fixed annoying scrolling bug on mobile

* Minor fixes

* Minor tweaks

* Better platform detection

* iOS instructions

* Success notification and viewport styling

* Styling

* Dark bg color

* Another color change

* Yet another color change

* Small tweaks to update/install UX

* Styling

* Bugfix

* Several Install widget fixes

* Updated angular service-worker

* Changed install widget

* Caching strategies

* Linting

* Styling

* PR comments
This commit is contained in:
Aleksander Rem 2021-05-18 19:41:00 +02:00 committed by GitHub
commit 5cca259ae7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 391 additions and 39 deletions

View file

@ -20,7 +20,8 @@
"assets": [
"src/assets",
"src/pow.wasm",
"src/404.html"
"src/404.html",
"src/manifest.webmanifest"
],
"styles": [
"src/styles.less"
@ -71,7 +72,9 @@
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
],
"serviceWorker": true,
"ngswConfigPath": "ngsw-config.json"
},
"desktop": {
"budgets": [
@ -131,7 +134,8 @@
],
"assets": [
"src/assets",
"src/pow.wasm"
"src/pow.wasm",
"src/manifest.webmanifest"
]
}
},

50
ngsw-config.json Normal file
View file

@ -0,0 +1,50 @@
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/manifest.webmanifest",
"/*.css",
"/*.js"
]
}
},
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**",
"/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
]
}
}
],
"dataGroups": [
{
"name": "coingecko",
"urls": ["https://api.coingecko.com/**"],
"cacheConfig": {
"maxSize": 3,
"maxAge": "60d",
"strategy": "freshness"
}
},
{
"name": "ninja",
"urls": ["https://mynano.ninja/api/**"],
"cacheConfig": {
"maxSize": 50,
"maxAge": "60d",
"strategy": "freshness"
}
}
]
}

64
package-lock.json generated
View file

@ -816,6 +816,14 @@
"tslib": "^2.0.0"
}
},
"@angular/service-worker": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-10.2.5.tgz",
"integrity": "sha512-8gsI+sg84Oor83w2iyQ99Tmf3sEXXiLT9R/kJ0NuOzRkPW0GV//Hi0y1emTpnp+INzYlCgqRBdp45HlDnynYOA==",
"requires": {
"tslib": "^2.0.0"
}
},
"@babel/code-frame": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
@ -2367,7 +2375,7 @@
"@types/jasmine": {
"version": "2.5.54",
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.5.54.tgz",
"integrity": "sha512-B9YofFbUljs19g5gBKUYeLIulsh31U5AK70F41BImQRHEZQGm4GcN922UvnYwkduMqbC/NH+9fruWa/zrqvHIg==",
"integrity": "sha1-prXyrir7bgMHd06MfGCOA31JHGM=",
"dev": true
},
"@types/jasminewd2": {
@ -2415,7 +2423,7 @@
"@types/qrcode": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-0.8.1.tgz",
"integrity": "sha512-Z3Z5S+DS9vBGwH0wBN4544se8ogM5Y0+t7flmPxvhR4nj7zTvOy3PkNaCX5vwKXOgr6KeLfDEX5lW4fTZ+chIg==",
"integrity": "sha1-V3bLXaKZz6fO4mEPWZl6ytkK040=",
"dev": true,
"requires": {
"@types/node": "*"
@ -2933,7 +2941,7 @@
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=",
"requires": {
"color-convert": "^1.9.0"
}
@ -3020,12 +3028,12 @@
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
"integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo="
},
"are-we-there-yet": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
"integrity": "sha1-SzXClE8GKov82mZBB2A1D+nd/CE=",
"requires": {
"delegates": "^1.0.0",
"readable-stream": "^2.0.6"
@ -3034,7 +3042,7 @@
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"integrity": "sha1-vNZ5HqWuCXJeF+WtmIE0zUCz2RE=",
"requires": {
"sprintf-js": "~1.0.2"
}
@ -3188,7 +3196,7 @@
"async-exit-hook": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz",
"integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==",
"integrity": "sha1-i9iwJLDsmxwBzMua+dspvXF9+vM=",
"dev": true
},
"async-limiter": {
@ -3463,7 +3471,7 @@
"bignumber.js": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-5.0.0.tgz",
"integrity": "sha512-KWTu6ZMVk9sxlDJQh2YH1UOnfDP8O8TpxUxgQG/vKASoSnEjK9aVuOueFaPcQEYQ5fyNXNTOYwYw3099RYebWg=="
"integrity": "sha1-+85j8Jd2swAKgxhbrc3lJdrzSDM="
},
"binary-extensions": {
"version": "2.1.0",
@ -3754,7 +3762,7 @@
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -3908,7 +3916,7 @@
"buffer-alloc": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
"integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
"integrity": "sha1-iQ3ZDZI6hz4I4Q5f1RpX5bfM4Ow=",
"requires": {
"buffer-alloc-unsafe": "^1.1.0",
"buffer-fill": "^1.0.0"
@ -3917,7 +3925,7 @@
"buffer-alloc-unsafe": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="
"integrity": "sha1-vX3CauKXLQ7aJTvgYdupkjScGfA="
},
"buffer-crc32": {
"version": "0.2.13",
@ -4301,7 +4309,7 @@
"cipher-base": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
"integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
"integrity": "sha1-h2Dk7MJy9MNjUy+SbYdKriwTl94=",
"requires": {
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
@ -4774,7 +4782,7 @@
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
"integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=",
"dev": true
},
"convert-source-map": {
@ -5411,7 +5419,7 @@
"deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
"integrity": "sha1-xPp8lUBKF6nD6Mp+FTcxK3NjMKw="
},
"default-gateway": {
"version": "4.2.0",
@ -5627,7 +5635,7 @@
"diff": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
"integrity": "sha1-gAwN0eCov7yVg1wgKtIg/jF+WhI=",
"dev": true
},
"diffie-hellman": {
@ -7152,7 +7160,7 @@
"fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
"integrity": "sha1-a+Dem+mYzhavivwkSXue6bfM2a0="
},
"fs-extra": {
"version": "9.0.1",
@ -8107,7 +8115,7 @@
"ini": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
"integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc="
},
"inquirer": {
"version": "7.1.0",
@ -9742,7 +9750,7 @@
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
"requires": {
"brace-expansion": "^1.1.7"
}
@ -10474,7 +10482,7 @@
"npmlog": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=",
"requires": {
"are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0",
@ -12550,7 +12558,7 @@
"rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"integrity": "sha1-zZJL9SAKB1uDwYjNa54hG3/A0+0=",
"requires": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
@ -13017,7 +13025,7 @@
"ripemd160": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
"integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
"integrity": "sha1-ocGm9iR1FXe6XQeRTLyShQWFiQw=",
"requires": {
"hash-base": "^3.0.0",
"inherits": "^2.0.1"
@ -13100,7 +13108,7 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
"integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0="
},
"safe-regex": {
"version": "1.1.0",
@ -13114,7 +13122,7 @@
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
"integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo="
},
"sanitize-filename": {
"version": "1.6.3",
@ -13187,7 +13195,7 @@
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
"integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk="
},
"schema-utils": {
"version": "2.7.0",
@ -14319,7 +14327,7 @@
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=",
"requires": {
"safe-buffer": "~5.1.0"
}
@ -15279,7 +15287,7 @@
"uri-js": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
"integrity": "sha1-lMVA4f93KVbiKZUHwBCupsiDjrA=",
"requires": {
"punycode": "^2.1.0"
}
@ -16439,7 +16447,7 @@
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"integrity": "sha1-pFBD1U9YBTFtqNYvn1CRjT2nCwo=",
"dev": true,
"requires": {
"isexe": "^2.0.0"
@ -16458,7 +16466,7 @@
"wide-align": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"integrity": "sha1-rgdOa9wMFKQx6ATmJFScYzsABFc=",
"requires": {
"string-width": "^1.0.2 || 2"
}

View file

@ -38,6 +38,7 @@
"@angular/platform-browser": "^10.0.7",
"@angular/platform-browser-dynamic": "^10.0.7",
"@angular/router": "^10.0.7",
"@angular/service-worker": "^10.2.5",
"@ledgerhq/hw-transport": "^5.39.0",
"@ledgerhq/hw-transport-node-ble": "^5.39.0",
"@ledgerhq/hw-transport-node-hid": "^5.39.0",

View file

@ -224,6 +224,8 @@
</li>
</ul>
<app-install-widget></app-install-widget>
<div class="nav-search">
<form class="uk-search uk-search-default uk-width-1-1">
<a href="javascript:void(0)" (click)="performSearch()" class="uk-search-icon-flip" uk-search-icon></a>

View file

@ -7,12 +7,14 @@ import {PriceService} from './services/price.service';
import {NotificationService} from './services/notification.service';
import {WorkPoolService} from './services/work-pool.service';
import {Router} from '@angular/router';
import {SwUpdate} from '@angular/service-worker';
import {RepresentativeService} from './services/representative.service';
import {NodeService} from './services/node.service';
import { DesktopService, LedgerService } from './services';
import { environment } from 'environments/environment';
import { DeeplinkService } from './services/deeplink.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
@ -29,6 +31,7 @@ export class AppComponent implements OnInit {
public nodeService: NodeService,
private representative: RepresentativeService,
private router: Router,
public updates: SwUpdate,
private workPool: WorkPoolService,
public price: PriceService,
private desktop: DesktopService,
@ -48,7 +51,6 @@ export class AppComponent implements OnInit {
nanoPrice = this.price.price;
fiatTimeout = 5 * 60 * 1000; // Update fiat prices every 5 minutes
inactiveSeconds = 0;
windowHeight = 1000;
navExpanded = false;
showAccountsDropdown = false;
canToggleLightMode = true;
@ -144,6 +146,21 @@ export class AppComponent implements OnInit {
});
this.desktop.send('deeplink-ready');
// Notify user if service worker update is available
this.updates.available.subscribe((event) => {
console.log(`SW update available. Current: ${event.current.hash}. New: ${event.available.hash}`);
this.notifications.sendInfo(
'An update was installed in the background and will be applied on next launch. <a href="#" (click)="applySwUpdate()">Apply immediately</a>',
{ length: 10000 }
);
});
// Notify user after service worker was updated
this.updates.activated.subscribe((event) => {
console.log(`SW update successful. Current: ${event.current.hash}`);
this.notifications.sendSuccess('Nault was updated successfully.');
});
// Check how long the wallet has been inactive, and lock it if it's been too long
setInterval(() => {
this.inactiveSeconds += 1;
@ -180,6 +197,10 @@ export class AppComponent implements OnInit {
this.settings.setAppSetting('walletVersion', 2); // Update wallet version so we do not patch in the future.
}
applySwUpdate() {
this.updates.activateUpdate();
}
toggleNav() {
this.navExpanded = !this.navExpanded;
}

View file

@ -53,6 +53,7 @@ import { QrScanComponent } from './components/qr-scan/qr-scan.component';
import {SignComponent} from './components/sign/sign.component';
import {RemoteSigningComponent} from './components/remote-signing/remote-signing.component';
import {RemoteSignService} from './services/remote-sign.service';
import { InstallWidgetComponent } from './components/install-widget/install-widget.component';
import { QrModalComponent } from './components/qr-modal/qr-modal.component';
import { QrModalService } from './services/qr-modal.service';
import { NgbModule, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
@ -64,6 +65,8 @@ import { ZXingScannerModule } from '@zxing/ngx-scanner';
import { DeeplinkService, NinjaService } from './services';
import { ConverterComponent } from './components/converter/converter.component';
import { QrGeneratorComponent } from './components/qr-generator/qr-generator.component';
import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
import { MultisigComponent } from './components/multisig/multisig.component';
import { KeygeneratorComponent } from './components/keygenerator/keygenerator.component';
@ -102,6 +105,7 @@ import { KeygeneratorComponent } from './components/keygenerator/keygenerator.co
QrModalComponent,
ConverterComponent,
QrGeneratorComponent,
InstallWidgetComponent,
MultisigComponent,
KeygeneratorComponent,
],
@ -115,6 +119,7 @@ import { KeygeneratorComponent } from './components/keygenerator/keygenerator.co
ZXingScannerModule,
NgbModule,
PasswordStrengthMeterModule,
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
],
providers: [
UtilService,

View file

@ -0,0 +1,12 @@
<div (click)="install()" [tabindex]="isIosInstallable() ? -1 : 0" class="nav-install-row half-muted" [class.interactable]="!isIosInstallable()" [class.large]="isIosInstallable()" [class.visible]="showInstallPromotion" [class.uk-hidden]="!isPromotable()">
<span uk-icon="icon: download; ratio: 1.2" class="icon"></span>
<div>
<div class="label">Install Nault for {{ platform }}</div>
<div class="description" *ngIf="isIosInstallable()">
1. Tap the <span uk-icon="icon: push; ratio: 0.8"></span> Share button
<span *ngIf="platform === 'iOS'">below</span>
<span *ngIf="platform === 'iPadOS'">in the top right</span> <br/>
2. Select <span uk-icon="icon: plus-circle; ratio: 0.8"></span> Add to home screen
</div>
</div>
</div>

View file

@ -0,0 +1,50 @@
.nav-install-row {
width: 100%;
height: 65px;
border-bottom: 1px solid #16161b;
display: flex;
align-items: center;
box-sizing: border-box;
padding: 0 20px;
color: white;
transition: all 1s ease-in-out;
font-family: Montserrat, Arial, Helvetica, sans-serif;
outline: none;
overflow: hidden;
&.half-muted {
color: #b6bed5;
&.interactable:hover .label {
border-color: #b6bed5;
}
}
&.interactable {
text-decoration: none;
cursor: pointer;
}
&.large {
height: 90px;
}
.label {
border-bottom: 2px solid transparent;
transition: border-color .1s ease-in-out;
}
.description {
color: #8f8fab;
font-size: 0.875rem;
}
.icon {
margin-right: 8px;
}
&:not(.visible) {
height: 0;
opacity: 0;
}
}

View file

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { InstallWidgetComponent } from './install-widget.component';
describe('InstallWidgetComponent', () => {
let component: InstallWidgetComponent;
let fixture: ComponentFixture<InstallWidgetComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ InstallWidgetComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(InstallWidgetComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,105 @@
import { Component, OnInit } from '@angular/core';
import { NotificationService } from 'app/services/notification.service';
interface InstallEvent extends Event {
userChoice: Promise<{ outcome: 'accepted' | 'dismissed', platform: string }>;
prompt(): void;
}
@Component({
selector: 'app-install-widget',
templateUrl: './install-widget.component.html',
styleUrls: ['./install-widget.component.less'],
})
export class InstallWidgetComponent implements OnInit {
installEvent: InstallEvent;
showInstallPromotion = false;
platform = this.getPlatform();
promotablePlatforms = ['Android', 'iOS', 'iPadOS', 'Chrome OS'];
constructor(
private notifications: NotificationService,
) { }
ngOnInit() {
if (!this.isPromotable()) {
return;
}
// Show clickable installation banner (Chrome / Edge only)
window.addEventListener('beforeinstallprompt', (event: InstallEvent) => {
// Prevent the mini-infobar from appearing on mobile
event.preventDefault();
this.installEvent = event;
this.showInstallPromotion = true;
});
// Fallback for iOS family
if (this.isIosInstallable()) {
setTimeout(() => {
this.showInstallPromotion = true;
});
}
}
install() {
if (!this.installEvent) {
return;
}
this.installEvent.prompt();
this.installEvent.userChoice.then((result) => {
if (result.outcome === 'accepted') {
this.notifications.sendSuccess('Nault was successfully installed to the device.');
this.dismiss();
}
});
}
dismiss() {
this.showInstallPromotion = false;
}
getPlatform() {
const platform = window.navigator.platform;
const userAgent = window.navigator.userAgent;
if (platform.includes('Win')) {
return 'Windows';
} else if (platform.includes('Mac')) {
return 'Mac';
} else if (userAgent.includes('Android')) {
return 'Android';
} else if (userAgent.includes('CrOS')) {
return 'Chrome OS';
} else if (platform.includes('Linux')) {
return 'Linux';
} else if (platform.includes('iPhone')) {
return 'iOS';
} else if (platform.includes('iPad')) {
return 'iPadOS';
}
}
isIosInstallable() {
if (!this.isPromotable() || this.isInstalled()) {
return false;
}
return this.platform === 'iOS' || this.platform === 'iPadOS' && 'standalone' in window.navigator;
}
isPromotable() {
if (this.isInstalled()) {
return false;
}
return this.promotablePlatforms.includes(this.platform);
}
isInstalled() {
return window.matchMedia('(display-mode: standalone)').matches // Chrome & Edge
|| (window.navigator as any).standalone === true // Safari
|| window.navigator.userAgent.includes('Electron'); // Electron
}
}

View file

@ -13,7 +13,8 @@
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
"theme_color": "#4a90e2",
"background_color": "#1f1f24",
"display": "standalone",
"start_url": "/"
}

View file

@ -37,7 +37,7 @@
<meta name="application-name" content="Nault">
<meta name="msapplication-TileColor" content="#4a90e2">
<meta name="msapplication-config" content="assets/favicon/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<meta name="theme-color" content="#4a90e2">
</head>
<body class="body" style="margin: 0;">
<app-root>
@ -48,7 +48,8 @@
</div>
</div>
</app-root>
<script type="text/javascript" src="assets/lib/pow/startThreads.js" async></script>
<script type="text/javascript" src="assets/lib/pow/nano-webgl-pow.js" async></script>
<script type="text/javascript" src="assets/lib/pow/startThreads.js" async=""></script>
<script type="text/javascript" src="assets/lib/pow/nano-webgl-pow.js" async=""></script>
<noscript>Please enable JavaScript to continue using this application.</noscript>
</body>
</html>

View file

@ -32,6 +32,8 @@
}
.nav-container {
display: flex;
flex-direction: column;
background: @nav-background-color;
color: #868AB0;
padding-left: 0;

View file

@ -22,6 +22,10 @@ h1, h2, h3, h4, h5, .uk-text-lead, .uk-button, .uk-alert, .uk-description-list d
border-radius: @global-border-radius-medium;
}
* {
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
@media (max-width: 939px) {
.nlt-button-group button {
margin-bottom: @nlt-intro-margin / 2 !important;
@ -195,12 +199,14 @@ input[type=number] {
}
.app-grid {
// position: relative;
margin-top: 0;
overflow: hidden;
margin-left: 0;
height: 100vh;
}
.content-container {
background: #FFF;
padding: 15px 20px;
@ -217,7 +223,7 @@ input[type=number] {
margin-top: @mobile-top-bar-height;
height: calc(100vh - @mobile-top-bar-height);
padding: 15px 15px;
transition: transform 0.5s ease;
transition: transform 0.3s ease;
&.nav-expanded {
transform: translateX(360px);

59
src/manifest.webmanifest Normal file
View file

@ -0,0 +1,59 @@
{
"name": "nault",
"short_name": "nault",
"theme_color": "#1976d2",
"background_color": "#1f1f24",
"display": "standalone",
"scope": "./",
"start_url": "./",
"icons": [
{
"src": "assets/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
]
}