diff --git a/angular.json b/angular.json
index c6a1cd1f1..05add5dd7 100644
--- a/angular.json
+++ b/angular.json
@@ -22,6 +22,7 @@
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
"polyfills": "src/polyfills.ts",
+ "extractCss": true,
"assets": [
"src/favicon.ico",
"src/robots.txt",
@@ -29,7 +30,27 @@
"src/assets"
],
"styles": [
- "src/main.scss"
+ "src/main.scss",
+ {
+ "input": "src/theme/deeppurple-amber.scss",
+ "bundleName": "theme/deeppurple-amber",
+ "lazy": true
+ },
+ {
+ "input": "src/theme/indigo-pink.scss",
+ "bundleName": "theme/indigo-pink",
+ "lazy": true
+ },
+ {
+ "input": "src/theme/pink-bluegrey.scss",
+ "bundleName": "theme/pink-bluegrey",
+ "lazy": true
+ },
+ {
+ "input": "src/theme/purple-green.scss",
+ "bundleName": "theme/purple-green",
+ "lazy": true
+ }
],
"scripts": []
},
@@ -80,7 +101,27 @@
"tsConfig": "src/tsconfig.spec.json",
"scripts": [],
"styles": [
- "src/main.scss"
+ "src/main.scss",
+ {
+ "input": "src/theme/deeppurple-amber.scss",
+ "bundleName": "theme/deeppurple-amber",
+ "lazy": true
+ },
+ {
+ "input": "src/theme/indigo-pink.scss",
+ "bundleName": "theme/indigo-pink",
+ "lazy": true
+ },
+ {
+ "input": "src/theme/pink-bluegrey.scss",
+ "bundleName": "theme/pink-bluegrey",
+ "lazy": true
+ },
+ {
+ "input": "src/theme/purple-green.scss",
+ "bundleName": "theme/purple-green",
+ "lazy": true
+ }
],
"assets": [
"src/favicon.ico",
diff --git a/package-lock.json b/package-lock.json
index 5ed762012..5c65776ff 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -685,6 +685,11 @@
"webpack-sources": "1.1.0"
}
},
+ "@ngx-pwa/local-storage": {
+ "version": "6.0.0-rc.0",
+ "resolved": "https://registry.npmjs.org/@ngx-pwa/local-storage/-/local-storage-6.0.0-rc.0.tgz",
+ "integrity": "sha512-ks+5nSjSbErv8u22jsu4zKUDcQuXTum9k5qVptq2+mytzxXfNZLMNz3NT91Ww72axgsoDKBNEYNaLuY4ST5pLg=="
+ },
"@ngx-rocket/ascii-logo": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@ngx-rocket/ascii-logo/-/ascii-logo-1.1.0.tgz",
diff --git a/package.json b/package.json
index b9b38177c..8ecb98e4d 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
"@angular/platform-browser-dynamic": "^6.0.6",
"@angular/router": "^6.0.6",
"@angular/service-worker": "^6.0.6",
+ "@ngx-pwa/local-storage": "^6.0.0-rc.0",
"@ngx-translate/core": "^10.0.2",
"@swimlane/ngx-charts": "^8.1.0",
"angular-archwizard": "^3.0.0",
diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts
index 158f21544..be2230005 100644
--- a/src/app/core/core.module.ts
+++ b/src/app/core/core.module.ts
@@ -53,6 +53,7 @@ import { CacheInterceptor } from './http/cache.interceptor';
import { ShellComponent } from './shell/shell.component';
import { SidenavComponent } from './shell/sidenav/sidenav.component';
+import { ThemePickerComponent } from './shell/toolbar/theme-picker/theme-picker.component';
import { ToolbarComponent } from './shell/toolbar/toolbar.component';
import { ContentComponent } from './shell/content/content.component';
import { BreadcrumbComponent } from './shell/breadcrumb/breadcrumb.component';
@@ -102,6 +103,7 @@ import { BreadcrumbComponent } from './shell/breadcrumb/breadcrumb.component';
declarations: [
ShellComponent,
SidenavComponent,
+ ThemePickerComponent,
ToolbarComponent,
ContentComponent,
BreadcrumbComponent
diff --git a/src/app/core/shell/toolbar/theme-picker/theme-manager.service.spec.ts b/src/app/core/shell/toolbar/theme-picker/theme-manager.service.spec.ts
new file mode 100644
index 000000000..7bfde5b85
--- /dev/null
+++ b/src/app/core/shell/toolbar/theme-picker/theme-manager.service.spec.ts
@@ -0,0 +1,15 @@
+import { TestBed, inject } from '@angular/core/testing';
+
+import { ThemeManagerService } from './theme-manager.service';
+
+describe('ThemeManagerService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [ThemeManagerService]
+ });
+ });
+
+ it('should be created', inject([ThemeManagerService], (service: ThemeManagerService) => {
+ expect(service).toBeTruthy();
+ }));
+});
diff --git a/src/app/core/shell/toolbar/theme-picker/theme-manager.service.ts b/src/app/core/shell/toolbar/theme-picker/theme-manager.service.ts
new file mode 100644
index 000000000..cb28501ab
--- /dev/null
+++ b/src/app/core/shell/toolbar/theme-picker/theme-manager.service.ts
@@ -0,0 +1,39 @@
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ThemeManagerService {
+
+ private themeManagerClass = 'theme-manager';
+
+ constructor() { }
+
+ setTheme(href: string) {
+ this.getLinkElement().setAttribute('href', href);
+ }
+
+ removeTheme() {
+ const existingLinkElement = this.getExistingLinkElement();
+ if (existingLinkElement) {
+ document.head.removeChild(existingLinkElement);
+ }
+ }
+
+ private getLinkElement() {
+ return this.getExistingLinkElement() || this.createLinkElement();
+ }
+
+ private getExistingLinkElement() {
+ return document.head.querySelector(`link[rel="stylesheet"].${this.themeManagerClass}`);
+ }
+
+ private createLinkElement() {
+ const linkElement = document.createElement('link');
+ linkElement.setAttribute('rel', 'stylesheet');
+ linkElement.classList.add(this.themeManagerClass);
+ document.head.appendChild(linkElement);
+ return linkElement;
+ }
+
+}
diff --git a/src/app/core/shell/toolbar/theme-picker/theme-picker.component.html b/src/app/core/shell/toolbar/theme-picker/theme-picker.component.html
new file mode 100644
index 000000000..45e3b413d
--- /dev/null
+++ b/src/app/core/shell/toolbar/theme-picker/theme-picker.component.html
@@ -0,0 +1,16 @@
+
+
+
diff --git a/src/app/core/shell/toolbar/theme-picker/theme-picker.component.scss b/src/app/core/shell/toolbar/theme-picker/theme-picker.component.scss
new file mode 100644
index 000000000..b2a98433d
--- /dev/null
+++ b/src/app/core/shell/toolbar/theme-picker/theme-picker.component.scss
@@ -0,0 +1,57 @@
+$theme-picker-menu-padding: 8px;
+$theme-picker-grid-cell-size: 48px;
+$theme-picker-grid-cells-per-row: 2;
+$theme-picker-swatch-size: 36px;
+$theme-picker-accent-stripe-size: 6px;
+
+.mifosx-theme-picker-menu {
+ .mat-menu-content {
+ padding: $theme-picker-menu-padding;
+ }
+
+ [mat-menu-item] {
+ flex: 0 0 auto;
+ padding: 0;
+ overflow: hidden;
+ }
+
+ .mifosx-theme-picker-swatch {
+ position: relative;
+ width: $theme-picker-swatch-size;
+ height: $theme-picker-swatch-size;
+ margin: ($theme-picker-grid-cell-size - $theme-picker-swatch-size) / 2;
+ border-radius: 50%;
+ overflow: hidden;
+
+ .mifosx-theme-chosen-icon {
+ color: white;
+ position: absolute;
+ left: 50%; top: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+ border: 1px solid rgba(0,0,0,.2);
+ border-radius: 50%;
+ }
+ }
+
+ .mifosx-theme-picker-primary {
+ width: 100%;
+ height: 100%;
+ }
+
+ .mifosx-theme-picker-accent {
+ position: absolute;
+ bottom: $theme-picker-accent-stripe-size;
+ width: 100%;
+ height: $theme-picker-accent-stripe-size;
+ }
+}
diff --git a/src/app/core/shell/toolbar/theme-picker/theme-picker.component.spec.ts b/src/app/core/shell/toolbar/theme-picker/theme-picker.component.spec.ts
new file mode 100644
index 000000000..87b8ba21c
--- /dev/null
+++ b/src/app/core/shell/toolbar/theme-picker/theme-picker.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ThemePickerComponent } from './theme-picker.component';
+
+describe('ThemePickerComponent', () => {
+ let component: ThemePickerComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ThemePickerComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ThemePickerComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/core/shell/toolbar/theme-picker/theme-picker.component.ts b/src/app/core/shell/toolbar/theme-picker/theme-picker.component.ts
new file mode 100644
index 000000000..6d1fbf545
--- /dev/null
+++ b/src/app/core/shell/toolbar/theme-picker/theme-picker.component.ts
@@ -0,0 +1,65 @@
+import { Component, OnInit, ViewEncapsulation } from '@angular/core';
+
+import { Theme } from './theme.model';
+import { ThemeManagerService } from './theme-manager.service';
+import { ThemeStorageService } from './theme-storage.service';
+
+@Component({
+ selector: 'mifosx-theme-picker',
+ templateUrl: './theme-picker.component.html',
+ styleUrls: ['./theme-picker.component.scss'],
+ encapsulation: ViewEncapsulation.None
+})
+export class ThemePickerComponent implements OnInit {
+
+ currentTheme: Theme = {
+ href: 'indigo-pink.css',
+ primary: '#3F51B5',
+ accent: '#E91E63',
+ isDark: false,
+ isDefault: true,
+ };
+ themes = [
+ this.currentTheme,
+ {
+ href: 'deeppurple-amber.css',
+ primary: '#673AB7',
+ accent: '#FFC107',
+ isDark: false,
+ },
+ {
+ href: 'pink-bluegrey.css',
+ primary: '#E91E63',
+ accent: '#607D8B',
+ isDark: true,
+ },
+ {
+ href: 'purple-green.css',
+ primary: '#9C27B0',
+ accent: '#4CAF50',
+ isDark: true,
+ },
+ ];
+
+ constructor(public themeManagerService: ThemeManagerService,
+ private themeStorageService: ThemeStorageService) {
+ this.themeStorageService.getTheme().subscribe((theme) => {
+ this.installTheme(theme);
+ });
+ }
+
+ ngOnInit() { }
+
+ installTheme(theme: Theme) {
+ if (theme) {
+ this.currentTheme = theme;
+ }
+ if (this.currentTheme.isDefault) {
+ this.themeManagerService.removeTheme();
+ } else {
+ this.themeManagerService.setTheme(`theme/${this.currentTheme.href}`);
+ }
+ this.themeStorageService.storeTheme(this.currentTheme);
+ }
+
+}
diff --git a/src/app/core/shell/toolbar/theme-picker/theme-storage.service.spec.ts b/src/app/core/shell/toolbar/theme-picker/theme-storage.service.spec.ts
new file mode 100644
index 000000000..e18a1826a
--- /dev/null
+++ b/src/app/core/shell/toolbar/theme-picker/theme-storage.service.spec.ts
@@ -0,0 +1,15 @@
+import { TestBed, inject } from '@angular/core/testing';
+
+import { ThemeStorageService } from './theme-storage.service';
+
+describe('ThemeStorageService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [ThemeStorageService]
+ });
+ });
+
+ it('should be created', inject([ThemeStorageService], (service: ThemeStorageService) => {
+ expect(service).toBeTruthy();
+ }));
+});
diff --git a/src/app/core/shell/toolbar/theme-picker/theme-storage.service.ts b/src/app/core/shell/toolbar/theme-picker/theme-storage.service.ts
new file mode 100644
index 000000000..795b1dd43
--- /dev/null
+++ b/src/app/core/shell/toolbar/theme-picker/theme-storage.service.ts
@@ -0,0 +1,33 @@
+import {Injectable, EventEmitter} from '@angular/core';
+
+import { LocalStorage } from '@ngx-pwa/local-storage';
+import { Observable } from 'rxjs';
+
+import { Theme } from './theme.model';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ThemeStorageService {
+
+ private themeStorageKey = 'mifosXTheme';
+ onThemeUpdate: EventEmitter;
+
+ constructor(private localStorage: LocalStorage) {
+ this.onThemeUpdate = new EventEmitter();
+ }
+
+ storeTheme(mifosXTheme: Theme) {
+ this.localStorage.setItem(this.themeStorageKey, mifosXTheme)
+ .subscribe(() => this.onThemeUpdate.emit(mifosXTheme));
+ }
+
+ getTheme(): Observable {
+ return this.localStorage.getItem(this.themeStorageKey);
+ }
+
+ clearTheme() {
+ this.localStorage.removeItem(this.themeStorageKey);
+ }
+
+}
diff --git a/src/app/core/shell/toolbar/theme-picker/theme.model.ts b/src/app/core/shell/toolbar/theme-picker/theme.model.ts
new file mode 100644
index 000000000..453853a70
--- /dev/null
+++ b/src/app/core/shell/toolbar/theme-picker/theme.model.ts
@@ -0,0 +1,7 @@
+export interface Theme {
+ href: string;
+ primary: string;
+ accent: string;
+ isDark?: boolean;
+ isDefault?: boolean;
+}
diff --git a/src/app/core/shell/toolbar/toolbar.component.html b/src/app/core/shell/toolbar/toolbar.component.html
index 1654a7b7e..db9fed5d4 100644
--- a/src/app/core/shell/toolbar/toolbar.component.html
+++ b/src/app/core/shell/toolbar/toolbar.component.html
@@ -56,9 +56,7 @@
Hindi
-
+