Skip to content
Snippets Groups Projects
Commit 9aa58e3d authored by Benoit Lavenier's avatar Benoit Lavenier
Browse files

[enh] transfer: fix when wot modal is opened

[enh] account pipes: auto-update value when account data changed (balance, name, etc.)
parent 07998442
Branches
Tags
No related merge requests found
Showing
with 729 additions and 206 deletions
......@@ -33,7 +33,8 @@
"bn.js",
"ip-regexp",
"eventemitter3",
"qrious"
"qrious",
"localforage-cordovasqlitedriver"
],
"assets": [
{
......@@ -78,6 +79,18 @@
"with": "src/environments/environment.prod.ts"
}
],
"budgets": [
{
"type": "initial",
"maximumWarning": "6mb",
"maximumError": "8mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
......@@ -85,14 +98,7 @@
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
"buildOptimizer": true
},
"ci": {
"progress": false
......@@ -107,15 +113,10 @@
"configurations": {
"production": {
"browserTarget": "app:build:production"
},
"ci": {
}
"ci": {
"progress": false
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "app:build"
}
},
"test": {
......
......@@ -12,7 +12,10 @@
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"start.android": "ionic capacitor run android -l --external",
"build": "ng build --configuration production",
"build.android": "ionic capacitor build android --configuration production",
"sync.android": "npx cap sync android",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
......@@ -30,20 +33,21 @@
"@angular/platform-browser-dynamic": "^14.2.1",
"@angular/router": "^14.2.1",
"@capacitor-community/barcode-scanner": "3.0.0",
"@capacitor-community/sqlite": "^4.0.1",
"@capacitor/android": "4.1.0",
"@capacitor/app": "4.0.1",
"@capacitor/clipboard": "^4.0.1",
"@capacitor/core": "4.1.0",
"@capacitor/haptics": "4.0.1",
"@capacitor/keyboard": "4.0.1",
"@capacitor/splash-screen": "4.0.1",
"@capacitor/status-bar": "4.0.1",
"@capacitor/android": "~4.2.0",
"@capacitor/app": "~4.0.1",
"@capacitor/browser": "~4.0.1",
"@capacitor/camera": "~4.1.1",
"@capacitor/clipboard": "~4.0.1",
"@capacitor/core": "~4.1.0",
"@capacitor/haptics": "~4.0.1",
"@capacitor/keyboard": "~4.0.1",
"@capacitor/splash-screen": "~4.0.1",
"@capacitor/status-bar": "~4.0.1",
"@ionic/angular": "^6.2.6",
"@ionic/pwa-elements": "^3.1.1",
"@ionic/storage-angular": "^3.0.6",
"@ngx-translate/core": "^14.0.0",
"@ngx-translate/http-loader": "^7.0.0",
"@ionic/pwa-elements": "~3.1.1",
"@polkadot/api": "^9.2.4",
"@polkadot/keyring": "^10.1.6",
"@polkadot/networks": "^10.1.6",
......@@ -54,6 +58,8 @@
"angular2-qrcode": "^2.0.3",
"crypto-browserify": "^3.12.0",
"jdenticon": "^3.1.1",
"localforage": "~1.10.0",
"localforage-cordovasqlitedriver": "~1.8.0",
"moment": "^2.29.4",
"moment-timezone": "^0.5.37",
"ngx-jdenticon": "^1.0.4",
......@@ -63,11 +69,6 @@
"zone.js": "~0.11.8"
},
"devDependencies": {
"ngx-color-picker": "^12.0.1",
"ngx-jdenticon": "^1.0.4",
"ngx-markdown": "^14.0.1",
"ngx-material-timepicker": "5.5.3",
"ngx-quicklink": "^0.3.0",
"@angular-devkit/build-angular": "^14.2.2",
"@angular-eslint/builder": "~13.5.0",
"@angular-eslint/eslint-plugin": "~13.5.0",
......@@ -78,13 +79,13 @@
"@angular/compiler-cli": "^14.2.1",
"@angular/language-service": "^14.2.1",
"@capacitor/cli": "4.1.0",
"@ionic/cli": "^6.20.1",
"@ionic/angular-toolkit": "^6.1.0",
"@ionic/cli": "^6.20.1",
"@polkadot/typegen": "^9.2.4",
"@polkadot/types": "^9.2.4",
"@types/jasmine": "~4.0.3",
"@types/jasminewd2": "~2.0.10",
"@types/node": "^12.20.55",
"@types/node": "^14.18.28",
"@typescript-eslint/eslint-plugin": "4.33.0",
"@typescript-eslint/parser": "4.33.0",
"eslint": "^7.32.0",
......@@ -99,8 +100,13 @@
"karma-coverage-istanbul-reporter": "~3.0.3",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.0.0",
"ngx-color-picker": "^12.0.1",
"ngx-jdenticon": "^1.0.4",
"ngx-markdown": "^14.0.1",
"ngx-material-timepicker": "5.5.3",
"ngx-quicklink": "^0.3.0",
"protractor": "~7.0.0",
"ts-node": "^8.6.2",
"ts-node": "^8.10.2",
"typescript": "~4.6.4"
},
"engines": {
......
#!/bin/bash
curl -H "Content-Type: application/json" -d '{"id":"1", "jsonrpc":"2.0", "method": "state_getMetadata", "params":[]}' http://localhost:9933 > ../src/interfaces/duniter-types.json
NODE=http://localhost:9933
curl -H "Content-Type: application/json" -d '{"id":"1", "jsonrpc":"2.0", "method": "state_getMetadata", "params":[]}' ${NODE} > ../src/interfaces/types.json
......@@ -2,9 +2,10 @@
<ion-split-pane contentId="main-content">
<ion-menu contentId="main-content" type="overlay">
<ion-content>
<ion-list id="inbox-list">
<ion-list-header [innerHTML]="appName"></ion-list-header>
<ion-note>profile name</ion-note>
<ion-list id="main-menu-list">
<ion-list-header [innerHTML]="'COMMON.APP_NAME'|translate"></ion-list-header>
<ion-note></ion-note>
<ion-menu-toggle auto-hide="false" *ngFor="let p of appPages; let i = index">
<ion-item routerDirection="root" *ngIf="p.url"
......@@ -13,7 +14,7 @@
<ion-label [color]="p.color">{{ p.title | translate }}</ion-label>
</ion-item>
<ion-item routerDirection="root" *ngIf="p.handle && p.enable()"
(click)="p.handle()" lines="none" detail="false" routerLinkActive="selected">
(click)="p.handle($event)" lines="none" detail="false" routerLinkActive="selected">
<ion-icon slot="start" [color]="p.color" [ios]="p.icon + '-outline'" [md]="p.icon + '-sharp'"></ion-icon>
<ion-label [color]="p.color">{{ p.title | translate }}</ion-label>
</ion-item>
......
......@@ -43,11 +43,11 @@ ion-menu.md ion-note {
padding-left: 10px;
}
ion-menu.md ion-list#inbox-list {
ion-menu.md ion-list#main-menu-list {
border-bottom: 1px solid var(--ion-color-step-150, #d7d8da);
}
ion-menu.md ion-list#inbox-list ion-list-header {
ion-menu.md ion-list#main-menu-list ion-list-header {
font-size: 22px;
font-weight: 600;
......
......@@ -3,6 +3,8 @@ import {PlatformService} from "./shared/services/platform.service";
import {environment} from "@environments/environment";
import {AccountService} from "@app/wallet/account.service";
import {Router} from "@angular/router";
import {App} from "@capacitor/app";
import {isNotNilOrBlank} from "@app/shared/functions";
@Component({
selector: 'app-root',
......@@ -11,7 +13,7 @@ import {Router} from "@angular/router";
})
export class AppComponent {
appName = environment.name;
appName = 'COMMON.APP_NAME';
appPages = [
{ title: 'MENU.HOME', url: '/home', icon: 'home' },
......@@ -24,10 +26,10 @@ export class AppComponent {
{ title: 'MENU.SETTINGS', url: '/settings', icon: 'settings' },
{ title: 'HOME.BTN_CHANGE_ACCOUNT', icon: 'log-out', color: 'danger',
{ title: 'COMMON.BTN_LOGOUT', icon: 'log-out', color: 'danger',
handle: (event) => this.logout(event),
enable: () => this.accountService.isLogin
enable: () => this.accountService.isLogin && this.platform.mobile
},
];
......@@ -41,9 +43,13 @@ export class AppComponent {
var now = Date.now();
console.info('[app] Starting...');
// Start all stuff (services, plugins, etc.)
await this.platform.start();
console.info(`[app] Starting [OK] in ${Date.now()-now}ms`);
// Detecting deep link
await this.detectDeepLink();
}
async logout(event) {
......@@ -52,4 +58,28 @@ export class AppComponent {
replaceUrl: true
});
}
async detectDeepLink(){
try {
const {url} = await App.getLaunchUrl();
if (isNotNilOrBlank(url)) {
const slashIndex = url.indexOf('/');
if (slashIndex !== -1) {
const relativeUrl = url.substring(slashIndex+1);
console.info('[app] Detected a deep link: ' + relativeUrl);
// TODO: call the router ?
await this.router.navigateByUrl(relativeUrl);
}
else {
console.warn(`[app] Detected a INVALID deep link: ${url} - missing slash`);
}
}
}
catch(err) {
console.error(`[platform] Cannot get launch URL: ${err.message||err}\n${err?.originalStack || JSON.stringify(err)}`);
// Continue
}
}
}
import {
ActionSheetButton,
ActionSheetController,
ActionSheetOptions,
IonModal,
ModalController,
PopoverController
} from "@ionic/angular";
import {Injectable} from "@angular/core";
import {PlatformService} from "@app/shared/services/platform.service";
import {PopoverOptions} from "@ionic/core";
import {ListItem, ListPopover, ListPopoverOptions} from "@app/shared/popover/list.popover";
import {TranslateService} from "@ngx-translate/core";
import {AuthModal, AuthModalOptions} from "@app/auth/auth.modal";
import {Router} from "@angular/router";
import {RegisterModal, RegisterModalOptions} from "@app/register/register.modal";
export declare type LoginMethodType = 'v1' | 'v2' | 'keyfile-v1';
export const LoginMethods: ListItem[] = [
{value: 'v1', label: 'Compte Duniter v1'},
{value: 'v2', label: 'Phrase de restauration'},
{value: 'keyfile-v1', label: 'Fichier de clef Duniter v1', disabled: true}
];
@Injectable()
export class AuthController {
private _mobile = this.platform.mobile;
protected actionSheetOptions: Partial<ActionSheetOptions> = {
backdropDismiss: true,
cssClass: 'select-login-action-sheet'
};
protected popoverOptions: Partial<PopoverOptions> = {
backdropDismiss: true,
cssClass: 'select-login-popover',
reference: 'event'
};
constructor(
private platform: PlatformService,
private translate: TranslateService,
private popoverCtrl: PopoverController,
private actionSheetCtrl: ActionSheetController,
private modalCtrl: ModalController,
private router: Router
) {
}
async login(event, opts?: {
loginMethod?: LoginMethodType,
auth?: boolean,
redirectToWalletPage?: boolean
}) {
let loginMethod = opts?.loginMethod;
// Ask login method
if (!loginMethod) {
// ...using popover
if (!this._mobile) {
const popover = await this.popoverCtrl.create(<PopoverOptions>{
event,
backdropDismiss: true,
component: ListPopover,
componentProps: <ListPopoverOptions>{
title: 'LOGIN.METHOD_POPOVER_TITLE',
items: LoginMethods
}
})
await popover.present(event);
const {data} = await popover.onWillDismiss();
loginMethod = data;
}
else {
const actionSheet = await this.actionSheetCtrl.create({
...this.actionSheetOptions,
header: this.translate.instant('LOGIN.METHOD_POPOVER_TITLE'),
buttons: LoginMethods.map(method => {
return <ActionSheetButton>{
id: method.value,
data: method.value,
text: this.translate.instant(method.label)
}
})
});
await actionSheet.present();
const {data} = await actionSheet.onWillDismiss();
loginMethod = data;
}
}
if (!loginMethod) return undefined; // User cancelled
console.info('[auth] Selected login method: ' + loginMethod);
let modal: HTMLIonModalElement;
switch (loginMethod) {
case 'v1':
modal = await this.modalCtrl.create({
component: AuthModal,
componentProps: <AuthModalOptions>{
auth: opts?.auth,
scrollY: false // TODO remove this !
}
});
break;
default:
console.warn('[home] Unknown login method: ' + loginMethod);
}
if (!modal) return; // User cancelled of method not found
await modal.present();
const {data} = await modal.onWillDismiss();
if (data?.address && opts?.redirectToWalletPage === true) {
setTimeout(() => this.router.navigate(['/wallet', data.address]));
}
return data;
}
async register(opts?: { redirectToWalletPage?: boolean; }) {
const modal = await this.modalCtrl.create({
component: RegisterModal,
componentProps: <RegisterModalOptions>{
scrollY: false // TODO remove this !
}
});
await modal.present();
const {data} = await modal.onWillDismiss();
if (data?.address && opts.redirectToWalletPage === true) {
setTimeout(() => this.router.navigate(['/wallet', data.address]));
}
return data;
}
}
......@@ -7,7 +7,7 @@ import {AppForm} from "@app/shared/form.class";
import {AuthData} from "@app/auth/auth.model";
import {SettingsService} from "@app/settings/settings.service";
import {NetworkService} from "@app/network/network.service";
import {environment} from "@duniter/core-types/environments/environment";
import {environment} from "@environments/environment";
import {FormUtils} from "@app/shared/forms";
......@@ -64,7 +64,7 @@ export class AuthForm extends AppForm<AuthData> implements OnInit {
this.onCancel.emit();
}
async doSubmit(event?: UIEvent) {
async doSubmit(event?: Event) {
if (event) {
event.preventDefault();
event.stopPropagation();
......
......@@ -6,7 +6,7 @@
</ion-button>
</ion-buttons>
<ion-title [innerHTML]="auth?'AUTH.TITLE': 'LOGIN.TITLE'|translate">
<ion-title [innerHTML]="title|translate">
</ion-title>
<ion-buttons slot="end">
......@@ -20,7 +20,7 @@
</ion-toolbar>
</ion-header>
<ion-content style="height: 100%">
<ion-content style="height: 100%" [scrollY]="scrollY">
<app-auth-form #form
(onSubmit)="doSubmit($event)"
(onCancel)="cancel()">
......
......@@ -5,15 +5,19 @@ import {AuthForm} from './auth.form';
import {firstNotNilPromise} from '@app/shared/observables';
import {AuthData} from "@app/auth/auth.model";
export interface AuthModalOptions {
auth?: boolean;
scrollY?: boolean;
title?: string;
}
@Component({
selector: 'app-auth-modal',
templateUrl: 'auth.modal.html',
styleUrls: ['./auth.modal.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AuthModal implements OnInit {
export class AuthModal implements OnInit, AuthModalOptions {
title: string = null;
get loading() {
return this.form?.loading;
}
......@@ -22,7 +26,9 @@ export class AuthModal implements OnInit {
return this.form?.mobile;
}
@Input() auth = false;
@Input() auth = false; // false for login, true for auth
@Input() scrollY = false;
@Input() title: string = null;
@ViewChild('form', { static: true }) private form: AuthForm;
......@@ -34,7 +40,7 @@ export class AuthModal implements OnInit {
ngOnInit() {
this.title = this.auth ? 'AUTH.TITLE' : 'LOGIN.TITLE';
this.title = this.title || (this.auth ? 'AUTH.TITLE' : 'LOGIN.TITLE');
this.form.markAsReady({emitEvent: false});
this.form.markAsLoaded();
......
......@@ -7,6 +7,8 @@ import {AuthForm} from "./auth.form";
import {AuthModal} from "./auth.modal";
import {AppSharedModule} from "@app/shared/shared.module";
import {TranslateModule} from "@ngx-translate/core";
import {AuthController} from "@app/auth/auth.controller";
import {AppRegisterModule} from "@app/register/register.module";
@NgModule({
imports: [
......@@ -14,12 +16,22 @@ import {TranslateModule} from "@ngx-translate/core";
FormsModule,
ReactiveFormsModule,
IonicModule,
TranslateModule,
// App modules
AppSharedModule,
TranslateModule
AppRegisterModule
],
exports: [
declarations: [
AuthForm, AuthModal
],
declarations: [AuthForm, AuthModal]
exports: [
AuthForm,
AuthModal,
TranslateModule
],
providers: [
AuthController
]
})
export class AppAuthModule {}
......@@ -90,41 +90,6 @@
</div>
</ion-content>
<ion-popover #loginMethodPopover>
<ng-template>
<ion-content>
<ion-list>
<ion-item *ngFor="let item of loginMethods"
[disabled]="item.disabled"
(click)="loginMethodPopover.dismiss(item.value)"
tappable>
{{item.label|translate}}
</ion-item>
</ion-list>
</ion-content>
</ng-template>
</ion-popover>
<ion-modal
#loginModal
[backdropDismiss]="false">
<ng-template>
<ion-content scrollY="false">
<app-auth-modal></app-auth-modal>
</ion-content>
</ng-template>
</ion-modal>
<ion-modal
#registerModal
[backdropDismiss]="false">
<ng-template>
<ion-content scrollY="false">
<app-register-modal></app-register-modal>
</ion-content>
</ng-template>
</ion-modal>
<ng-template #localeButton let-buttonColor>
<!-- locale button -->
......@@ -140,7 +105,7 @@
<ion-content>
<ion-list>
<ion-item *ngFor="let item of locales"
(click)="changeLocale(item.key) || popover.dismiss()"
(click)="changeLocale(item.key) && popover.dismiss()"
tappable>
<ion-label>{{item.value}}</ion-label>
<ion-icon slot="end" name="checkmark" *ngIf="data.locale===item.key"></ion-icon>
......
import {Component, Inject, Injector, OnInit, ViewChild} from '@angular/core';
import {SettingsService} from "@app/settings/settings.service";
import {Component, Inject, Injector, OnInit} from '@angular/core';
import {APP_LOCALES, LocaleConfig, Settings} from "@app/settings/settings.model";
import {BasePage} from "@app/shared/pages/base.page";
import {NetworkService} from "@app/network/network.service";
import {AbbreviatePipe} from "@app/shared/pipes/string.pipes";
import {AccountService} from "@app/wallet/account.service";
import {Account} from "@app/wallet/account.model";
import {fadeInAnimation} from "@app/shared/animations";
import {AuthModal} from "@app/auth/auth.modal";
import {RegisterModal} from "@app/register/register.modal";
import {
ActionSheetButton,
ActionSheetController,
ActionSheetOptions,
IonModal,
IonPopover,
PopoverOptions
} from "@ionic/angular";
import {Router} from "@angular/router";
import {AuthController} from "@app/auth/auth.controller";
export interface LoginMethod {
value: string;
label: string;
disabled?: boolean;
}
@Component({
selector: 'app-home',
templateUrl: './home.page.html',
......@@ -32,22 +16,6 @@ export interface LoginMethod {
})
export class HomePage extends BasePage<Settings> implements OnInit {
protected actionSheetOptions: Partial<ActionSheetOptions> = {
backdropDismiss: true,
cssClass: 'select-login-action-sheet'
};
protected popoverOptions: Partial<PopoverOptions> = {
backdropDismiss: true,
cssClass: 'select-login-popover',
reference: 'event'
};
protected loginMethods: LoginMethod[] = [
{value: 'v1', label: 'Compte Duniter v1'},
{value: 'v2', label: 'Phrase de restauration'},
{value: 'keyfile-v1', label: 'Fichier de clef Duniter v1', disabled: true}
];
currency: string = null;
defaultAccount: Account = null;
......@@ -55,16 +23,12 @@ export class HomePage extends BasePage<Settings> implements OnInit {
return this.accountService.isLogin
}
@ViewChild('loginModal') loginModal: IonModal;
@ViewChild('registerModal') registerModal: IonModal;
@ViewChild('loginMethodPopover') loginMethodPopover: IonPopover;
constructor(
injector: Injector,
public networkService: NetworkService,
public accountService: AccountService,
public authController: AuthController,
public router: Router,
public actionSheetCtrl: ActionSheetController,
@Inject(APP_LOCALES) public locales: LocaleConfig[]
) {
super(injector, {name: 'home'})
......@@ -89,62 +53,25 @@ export class HomePage extends BasePage<Settings> implements OnInit {
}
changeLocale(locale: string) {
changeLocale(locale: string): boolean {
this.settings.patchValue({locale});
this.data.locale = locale;
this.markForCheck();
return true;
}
async login(event) {
let loginMethod: string;
if (!this.mobile) {
await this.loginMethodPopover.present(event);
const {data} = await this.loginMethodPopover.onWillDismiss();
loginMethod = data;
}
else {
const actionSheet = await this.actionSheetCtrl.create({
...this.actionSheetOptions,
header: this.translate.instant('Select login method'),
buttons: this.loginMethods.map(method => {
return <ActionSheetButton>{
data: method.value,
text: this.translate.instant(method.label),
id: method.value
}
})
async login(event: UIEvent) {
const data = await this.authController.login(event, {
auth: true
});
await actionSheet.present();
const {data} = await actionSheet.onWillDismiss();
loginMethod = data;
}
if (!loginMethod) return;
console.info('[home] Selected login method: ' + loginMethod);
let modal: IonModal;
switch (loginMethod) {
case 'v1':
modal = this.loginModal;
break;
default:
console.warn('[home] Unknown login method: ' + loginMethod);
}
if (!modal) return; // User cancelled of method not found
await modal.present();
const {data} = await modal.onWillDismiss();
if (data?.address) {
this.defaultAccount = data;
setTimeout(() => this.router.navigate(['/wallet', data.address]));
}
}
async register(event) {
await this.registerModal.present();
const {data} = await this.registerModal.onWillDismiss();
async register() {
const data = await this.authController.register();
if (data?.address) {
this.defaultAccount = data;
setTimeout(() => this.router.navigate(['/wallet', data.address]));
......
......@@ -5,7 +5,6 @@ import {Peer, Peers} from "./peer.model";
import {StartableService} from "@app/shared/services/startable-service.class";
import {abbreviate} from "@app/shared/currencies";
import {Currency} from "@app/network/currency.model";
//import * as definitions from '@duniter/core-types/interfaces'
const WELL_KNOWN_CURRENCIES = Object.freeze({
'Ğdev': <Partial<Currency>>{
......
......@@ -26,12 +26,12 @@
</ion-toolbar>
</ion-header>
<ion-content >
<ion-content [scrollY]="scrollY">
<app-register-form (onSubmit)="doSubmit()" (onCancel)="cancel()"
[class]="mobile ? '': 'has-footer'"
#form>
<ion-row *ngIf="form.debug" codeConfirmation>
<ion-text color="primary" class="ion-padding">
<small>loading: {{loading}}<br/>
......
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, ViewChild} from '@angular/core';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit, ViewChild} from '@angular/core';
import {ModalController} from '@ionic/angular';
import {REGISTER_FORM_SLIDES, RegisterForm} from "@app/register/register.form";
import {AccountService} from "@app/wallet/account.service";
import {FormUtils} from "@app/shared/forms";
import {RegisterData} from "@app/register/register.model";
import {environment} from "@environments/environment";
export interface RegisterModalOptions {
scrollY?: boolean;
}
@Component({
selector: 'app-register-modal',
templateUrl: 'register.modal.html',
styleUrls: ['./register.modal.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RegisterModal implements OnInit{
export class RegisterModal implements OnInit, RegisterModalOptions {
@ViewChild('form', { static: true }) private form: RegisterForm;
get loading() {
return this.form.loading;
......@@ -25,6 +25,9 @@ export class RegisterModal implements OnInit{
return this.form.mobile;
}
@Input() scrollY = false;
@ViewChild('form', { static: true }) private form: RegisterForm;
constructor(
private accountService: AccountService,
public viewCtrl: ModalController,
......
......@@ -18,9 +18,14 @@ import {AppUnlockModule} from "@app/unlock/unlock.module";
AppSharedModule,
AppUnlockModule
],
exports: [
RegisterForm, RegisterModal
declarations: [
RegisterForm,
RegisterModal
],
declarations: [RegisterForm, RegisterModal]
exports: [
RegisterForm,
RegisterModal,
TranslateModule
]
})
export class AppRegisterModule {}
export type PredefinedColors =
| 'primary'
| 'secondary'
| 'tertiary'
| 'success'
| 'warning'
| 'danger'
| 'light'
| 'medium'
| 'dark';
export function rgbToHex(r: number, g: number, b: number): string {
return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b);
}
export function rgbArrayToHex(rgb: number[]): string {
return '#' + componentToHex(rgb[0]) + componentToHex(rgb[1]) + componentToHex(rgb[2]);
}
export function hexToRgbArray(hex: string): number[] {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? [
parseInt(result[1], 16),
parseInt(result[2], 16),
parseInt(result[3], 16)
] : null;
}
export function hexToRgb(hex: string): {r: number; g: number; b: number} {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function componentToHex(c: number): string {
const hex = c.toString(16);
return hex.length === 1 ? ('0' + hex) : hex;
}
// See mix in file ionic.functions.color.scss
export function mixHex(color1: string, color2: string, weight?: number) {
weight = weight ? (weight / 100) : 0.5;
const rgb1 = hexToRgbArray(color1);
if (!rgb1) throw Error('Invalid hex color:' + color1);
const rgb2 = hexToRgbArray(color2);
if (!rgb2) throw Error('Invalid hex color:' + color2);
const rgbAverage = rgb1.map((v, index) => Math.round((v * weight + rgb2[index] * (1 - weight))) );
return rgbArrayToHex(rgbAverage);
}
// 12% darker version of the base color (mix with black)
export function getColorShade(color: string) {
return mixHex('#000000', color, 12);
}
// 10% lighter version of the base color (mix with white)
export function getColorTint(color: string) {
return mixHex('#ffffff', color, 10);
}
/**
*
* @param color
* @param bw if true, will use black or white color, instead of the exact inverse
*/
export function getColorContrast(color: string, bw?: boolean) {
const rgb = hexToRgbArray(color);
if (!rgb) throw Error('Invalid hex color:' + color);
if (bw === true) {
// http://stackoverflow.com/a/3943023/112731
return (rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114) > 186
? '#000000'
: '#FFFFFF';
}
return rgbArrayToHex(rgb.map(v => 255 - v));
}
import {PredefinedColors} from '@ionic/core';
import {isNil, isNotNil} from '../functions';
export declare type ColorName = PredefinedColors |
'white'
| 'red'
| 'green'
| 'blue';
/**
* Define here theme colors
*/
const rgbArrayMap = {
white: [255, 255, 255],
primary: [20, 67, 145], // ok
secondary: [117, 196, 253], // ok
tertiary: [91, 94, 244], // ok
danger: [245, 61, 61], // ok
light: [244, 245, 248], // ok
medium: [152, 154, 162], // ok
dark: [34, 36, 40], // ok
red: [255, 0, 0],
green: [0, 255, 0],
blue: [0, 0, 255]
};
// Fill a map of Color objects
const colorsMap: { [key: string]: Color } = {};
/**
* Useful class for color conversion
*/
// @dynamic
export class Color {
// Helper method, to retrieve a color
static get(name: ColorName): Color {
return colorsMap[name] as Color;
}
static parseRgba(rgba: string): Color|null{
if (!rgba || (!rgba.startsWith('rgb(') && !rgba.startsWith('rgba('))) return null;
// Parse parts
const parts = rgba
.replace('rgb(', '')
.replace('rgba(', '')
.replace(')', '')
.split(',');
if (parts.length !== 3 && parts.length !== 4) return null;
return new Color([+parts[0], +parts[1], +parts[2]],
parts.length === 4 && +parts[3] || 1,
'custom');
}
static transparent = function() {
return new Color([0,0,0], 0, 'translucent');
};
constructor(
private _rgbArray: number[],
private _opacity?: number,
private _name: string = 'custom'
) {
}
get name(): string {
return this._name;
}
get opacity(): number{
return isNotNil(this._opacity) ? this._opacity : 1;
}
get rgb(): number[] {
return this._rgbArray;
}
get r(): number {
return this._rgbArray[0];
}
get g(): number {
return this._rgbArray[1];
}
get b(): number {
return this._rgbArray[2];
}
rgba(opacity?: number): string {
opacity = isNotNil(opacity) ? opacity : this._opacity;
if (isNil(opacity) || opacity < 0 || opacity > 1) {
return 'rgb(' + this._rgbArray.join(',') + ')';
}
return 'rgba(' + this._rgbArray.join(',') + ',' + opacity + ')';
}
}
export declare interface ColorGradientOptions {
opacity?: number;
startColor?: number[];
mainColor?: number[];
mainColorIndex?: number;
endColor?: number[];
format?: 'rgb'|'hex'|'array';
}
export declare interface ColorScaleOptions extends ColorGradientOptions{
min?: number;
max?: number;
upperMax?: boolean;
}
export declare interface ColorScaleLegendItem {
label: string;
color: string;
}
export declare interface ColorScaleLegend {
items: ColorScaleLegendItem[];
}
/**
* Helper class for colors scale
*/
// @dynamic
export class ColorScale {
static custom = (count: number, options?: ColorScaleOptions) => {
options = options || {};
return new ColorScale(
linearColorGradientWithIntermediate(count, {
opacity: options.opacity,
startColor: options.startColor || undefined,
mainColor: options.mainColor || undefined,
mainColorIndex: isNotNil(options.mainColorIndex) ? options.mainColorIndex : undefined,
endColor: options.endColor || undefined,
format: options.format
}) as string[],
options
);
};
static default() {
return ColorScale.custom(25);
}
/**
* Create a array with the given color
**/
static fix(length?: number, colorName?: ColorName): any[] {
return Array.apply(null, Array(length || 25))
.map(String.prototype.valueOf, Color.get(colorName || 'primary').rgba(0.5));
}
private _min: number;
private _max: number;
private _rangeSize: number;
private _legendItems: ColorScaleLegendItem[];
get min(): number {
return this._min;
}
get max(): number {
return this._max;
}
constructor(private colorArray: string[], options?: ColorScaleOptions) {
options = options || {};
// reserved last colors for value > max
const nbIntervalBeforeUpper = !options.upperMax ? colorArray.length : (colorArray.length - 1);
this._min = options.min || 0;
this._max = options.max || nbIntervalBeforeUpper;
this._rangeSize = Math.round((this._max - this._min) / nbIntervalBeforeUpper);
this._legendItems = this.computeLegend();
}
get legend(): ColorScaleLegend {
return {
items: this._legendItems
};
}
getValueColor(value: number): string {
const index = Math.floor(value * (this.colorArray.length - 1) / this._max);
return this.colorArray[index];
}
protected computeLegend(): ColorScaleLegendItem[] {
return this.colorArray.map((color, index) => {
const start = index * this._rangeSize;
const end = start + this._rangeSize;
return {
color,
label: (end < this._max) ? `${start.toLocaleString()} - ${end.toLocaleString()}` : ` >= ${start}`
};
});
}
}
// Fill colorsMap
Object.getOwnPropertyNames(rgbArrayMap)
.forEach((key) => {
colorsMap[key] = new Color(rgbArrayMap[key], 1, key);
});
// Internal function
function state2side(state) {
switch (state) {
case 0:
return 0;
case 1:
return -1;
case 2:
return 0;
case 3:
return 1;
}
}
const SCALE_OPTIONS_DEFAULT = {
startColor: rgbArrayMap.red,
startStates: [0, 2, 3], // R=keep, V=keep, B=increase
startStepsFn: (defaultStateSize: number) => [
Math.round((rgbArrayMap.red[0] - 50) / defaultStateSize),
Math.round((255 - rgbArrayMap.red[1]) / defaultStateSize),
Math.round((255 - rgbArrayMap.red[2]) / defaultStateSize)
]
};
/**
* Internal function, that create a colors scale, using iteration
*
* @param count
* @param opacity
* @param startColor
* @param startState
* @returns
*/
function linearColorGradientWithIntermediate(count: number,
options?: ColorGradientOptions): any[] {
options = options || {};
// From [0,1]
options.opacity = (options.opacity > 0 && options.opacity < 1) ? options.opacity : 1;
options.startColor = options.startColor || [255, 255, 190]; // default start = creme
options.mainColorIndex = options.mainColorIndex && options.mainColorIndex < count - 1 ? options.mainColorIndex : count - 1;
options.endColor = options.endColor || [255, 0, 0]; // default main = red
options.format = options.format || 'rgb';
if (!options.mainColor) {
return linearColorGradient(count, {
opacity: options.opacity,
startColor: options.startColor,
endColor: options.endColor,
format: options.format
});
}
else {
// Step 1: startColor -> mainColor
const result = linearColorGradient(options.mainColorIndex + 1, {
opacity: options.opacity,
startColor: options.startColor,
endColor: options.mainColor,
format: options.format
});
// Step 2: mainColor -> endColor
if (options.mainColorIndex < count - 1) {
return result.concat(
linearColorGradient(count - options.mainColorIndex, {
opacity: options.opacity,
startColor: options.mainColor,
endColor: options.endColor,
format: options.format
}));
}
else {
return result;
}
}
}
function linearColorGradient(count: number,
options?: ColorGradientOptions): any[] {
options = options || {};
// From [0,1]
options.opacity = (options.opacity > 0 && options.opacity < 1) ? options.opacity : 1;
options.startColor = options.startColor || [255, 255, 255]; // default start = white
options.endColor = options.endColor || [255, 0, 0]; // default end = red
options.format = options.format || 'rgb';
const result: number[][] = [];
const color = options.startColor.slice(); // copy the start color
const delta = [
Math.round((options.endColor[0] - options.startColor[0]) / count),
Math.round((options.endColor[1] - options.startColor[1]) / count),
Math.round((options.endColor[2] - options.startColor[2]) / count)
];
for (let i = 0; i < count - 1; i++) {
for (let j = 0; j < 3; j++) {
color[j] += delta[j];
}
result.push(color.slice());
}
// Force last color = end color
result.push(options.endColor.slice());
// Output as array
if (options.format === 'array') {
return result;
}
// Output as rgb(r,g,b) string
if (options.format === 'rgb') {
if (options.opacity >= 1) {
return result.map(color => 'rgb(' + color.join(',') + ')');
} else {
return result.map(color => 'rgba(' + color.concat(options.opacity).join(',') + ')');
}
}
// Output as hex
// TODO
// return result.map(color => {
// return "rgb(" + color.join(',') + ")";
// });
return result;
}
......@@ -27,7 +27,7 @@ export function abbreviate(currency: string): string {
return currency;
}
export function formatAddress(value: string, withChecksum?: boolean ): string {
export function formatAddress(value: string): string {
if (!value) return '';
if (value.length < 12) return '?';
return value.substring(0,6) + '\u2026' + value.substring(value.length - 6);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment