Unceremoniously steals material.angular.io's theme picker. Very cool stuff from them!

This commit is contained in:
GloriousCode
2017-10-19 16:21:37 +11:00
parent be8776bb1b
commit 2969c62759
15 changed files with 387 additions and 6 deletions

View File

@@ -33,16 +33,20 @@ import { AboutComponent } from './pages/about/about.component';
import { SettingsComponent } from './pages/settings/settings.component';
import { DashboardComponent } from './pages/dashboard/dashboard.component';
import { WalletComponent } from './pages/wallet/wallet.component';
//Shared
import { NavbarComponent } from './shared/navbar/navbar.component';
import { SidebarComponent } from './shared/sidebar/sidebar.component';
import { ExchangeCurrencyTickerComponent } from './shared/exchange-currency-ticker/exchange-currency-ticker.component';
import { AllEnabledCurrencyTickersComponent } from './shared/all-enabled-currency-tickers/all-enabled-currency-tickers.component';
import { ThemePickerComponent } from './shared/theme-picker/theme-picker';
//services
import { WebsocketService } from './services/websocket/websocket.service';
import { WebsocketHandlerService } from './services/websocket-handler/websocket-handler.service';
import { SidebarService } from './services/sidebar/sidebar.service';
import { ElectronService } from './providers/electron.service';
import { StyleManagerService } from './services/style-manager/style-manager.service';
import { ThemeStorageService } from './services/theme-storage/theme-storage.service';
//Routing
import { AppRoutingModule } from './app-routing.module';
@@ -63,7 +67,8 @@ import * as Rx from 'rxjs/Rx';
ExchangeCurrencyTickerComponent,
AllEnabledCurrencyTickersComponent,
SidebarComponent,
WalletComponent
WalletComponent,
ThemePickerComponent,
],
imports: [
BrowserModule,
@@ -87,7 +92,14 @@ import * as Rx from 'rxjs/Rx';
MatExpansionModule,
MatLineModule,
],
providers: [ElectronService,WebsocketService,WebsocketHandlerService, SidebarService],
providers: [
ElectronService,
WebsocketService,
WebsocketHandlerService,
SidebarService,
StyleManagerService,
ThemeStorageService,
],
bootstrap: [AppComponent]
})
export class AppModule {

View File

@@ -7,7 +7,7 @@
<mat-accordion>
<mat-expansion-panel *ngIf="settings.SMSGlobal != null">
<mat-expansion-panel-header>
<mat-expansion-panel-header >
<mat-panel-title>
SMS Global
</mat-panel-title>
@@ -41,13 +41,13 @@
<mat-expansion-panel *ngFor="let exchange of settings?.Exchanges">
<mat-expansion-panel-header>
<mat-expansion-panel-header color="secondary">
<mat-panel-title>
{{exchange.Name}}
</mat-panel-title>
<mat-panel-description>
Exchange Settings
<mat-icon>trending_up</mat-icon>
<mat-icon>attach_money</mat-icon>
</mat-panel-description>
</mat-expansion-panel-header>
<form class="form-content">

View File

@@ -0,0 +1,54 @@
import {inject, TestBed} from '@angular/core/testing';
import {HttpModule} from '@angular/http';
import {StyleManagerComponent} from './style-manager.component';
describe('StyleManager', () => {
let styleManager: StyleManagerComponent;
beforeEach(() => TestBed.configureTestingModule({
imports: [HttpModule],
providers: [StyleManagerComponent]
}));
beforeEach(inject([StyleManagerComponent], (sm: StyleManagerComponent) => {
styleManager = sm;
}));
afterEach(() => {
let links = document.head.querySelectorAll('link');
for (let link of Array.prototype.slice.call(links)) {
if (link.className.includes('style-manager-')) {
document.head.removeChild(link);
}
}
});
it('should add stylesheet to head', () => {
styleManager.setStyle('test', 'test.css');
let styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement;
expect(styleEl).not.toBeNull();
expect(styleEl.href.endsWith('test.css')).toBe(true);
});
it('should change existing stylesheet', () => {
styleManager.setStyle('test', 'test.css');
let styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement;
expect(styleEl).not.toBeNull();
expect(styleEl.href.endsWith('test.css')).toBe(true);
styleManager.setStyle('test', 'new.css');
expect(styleEl.href.endsWith('new.css')).toBe(true);
});
it('should remove existing stylesheet', () => {
styleManager.setStyle('test', 'test.css');
let styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement;
expect(styleEl).not.toBeNull();
expect(styleEl.href.endsWith('test.css')).toBe(true);
styleManager.removeStyle('test');
styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement;
expect(styleEl).toBeNull();
});
});

View File

@@ -0,0 +1,44 @@
import {Injectable} from '@angular/core';
/**
* Class for managing stylesheets. Stylesheets are loaded into named slots so that they can be
* removed or changed later.
*/
@Injectable()
export class StyleManagerService {
/**
* Set the stylesheet with the specified key.
*/
setStyle(key: string, href: string) {
getLinkElementForKey(key).setAttribute('href', href);
}
/**
* Remove the stylesheet with the specified key.
*/
removeStyle(key: string) {
const existingLinkElement = getExistingLinkElementByKey(key);
if (existingLinkElement) {
document.head.removeChild(existingLinkElement);
}
}
}
function getLinkElementForKey(key: string) {
return getExistingLinkElementByKey(key) || createLinkElementWithKey(key);
}
function getExistingLinkElementByKey(key: string) {
return document.head.querySelector(`link[rel="stylesheet"].${getClassNameForKey(key)}`);
}
function createLinkElementWithKey(key: string) {
const linkEl = document.createElement('link');
linkEl.setAttribute('rel', 'stylesheet');
linkEl.classList.add(getClassNameForKey(key));
document.head.appendChild(linkEl);
return linkEl;
}
function getClassNameForKey(key: string) {
return `style-manager-${key}`;
}

View File

@@ -0,0 +1,52 @@
import {ThemeStorageService} from './theme-storage.service';
const testStorageKey = ThemeStorageService.storageKey;
const testTheme = {
primary: '#000000',
accent: '#ffffff',
href: 'test/path/to/theme'
};
const createTestData = () => {
window.localStorage[testStorageKey] = JSON.stringify(testTheme);
};
const clearTestData = () => {
window.localStorage.clear();
};
describe('ThemeStorage Service', () => {
const service = new ThemeStorageService();
const getCurrTheme = () => JSON.parse(window.localStorage.getItem(testStorageKey));
const secondTestTheme = {
primary: '#666666',
accent: '#333333',
href: 'some/cool/path'
};
beforeEach(createTestData);
afterEach(clearTestData);
it('should set the current theme', () => {
expect(getCurrTheme()).toEqual(testTheme);
service.storeTheme(secondTestTheme);
expect(getCurrTheme()).toEqual(secondTestTheme);
});
it('should get the current theme', () => {
const theme = service.getStoredTheme();
expect(theme).toEqual(testTheme);
});
it('should clear the stored theme data', () => {
expect(getCurrTheme()).not.toBeNull();
service.clearStorage();
expect(getCurrTheme()).toBeNull();
});
it('should emit an event when setTheme is called', () => {
spyOn(service.onThemeUpdate, 'emit');
service.storeTheme(secondTestTheme);
expect(service.onThemeUpdate.emit).toHaveBeenCalled();
expect(service.onThemeUpdate.emit).toHaveBeenCalledWith(secondTestTheme);
});
});

View File

@@ -0,0 +1,39 @@
import {Injectable, EventEmitter} from '@angular/core';
export interface DocsSiteTheme {
href: string;
accent: string;
primary: string;
isDark?: boolean;
isDefault?: boolean;
}
@Injectable()
export class ThemeStorageService {
static storageKey = 'docs-theme-storage-current';
public onThemeUpdate: EventEmitter<DocsSiteTheme> = new EventEmitter<DocsSiteTheme>();
public storeTheme(theme: DocsSiteTheme) {
try {
window.localStorage[ThemeStorageService.storageKey] = JSON.stringify(theme);
} catch (e) { }
this.onThemeUpdate.emit(theme);
}
public getStoredTheme(): DocsSiteTheme {
try {
return JSON.parse(window.localStorage[ThemeStorageService.storageKey] || null);
} catch (e) {
return null;
}
}
public clearStorage() {
try {
window.localStorage.removeItem(ThemeStorageService.storageKey);
} catch (e) { }
}
}

View File

@@ -6,5 +6,6 @@
<span>GoCryptoTrader</span>
</a>
<div class="flex-spacer"></div>
<theme-picker></theme-picker>
</mat-toolbar>
</nav>

View File

@@ -0,0 +1,18 @@
<button mat-icon-button [mat-menu-trigger-for]="themeMenu" matTooltip="Select a theme!"
tabindex="-1">
<mat-icon>format_color_fill</mat-icon>
</button>
<!-- TODO: replace use of `mat-menu` here with a custom overlay -->
<mat-menu class="docs-theme-picker-menu" #themeMenu="matMenu" x-position="before">
<mat-grid-list cols="2">
<mat-grid-tile *ngFor="let theme of themes">
<div mat-menu-item (click)="installTheme(theme)">
<div class="docs-theme-picker-swatch">
<mat-icon class="docs-theme-chosen-icon" *ngIf="currentTheme === theme">check_circle</mat-icon>
<div class="docs-theme-picker-primary" [style.background]="theme.primary"></div>
</div>
</div>
</mat-grid-tile>
</mat-grid-list>
</mat-menu>

View File

@@ -0,0 +1,58 @@
$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;
.docs-theme-picker-menu {
.mat-menu-content {
padding: $theme-picker-menu-padding;
}
[mat-menu-item] {
flex: 0 0 auto;
padding: 0;
overflow: hidden;
}
.docs-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;
.docs-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%;
}
}
.docs-theme-picker-primary {
width: 100%;
height: 100%;
}
.docs-theme-picker-accent {
position: absolute;
bottom: $theme-picker-accent-stripe-size;
width: 100%;
height: $theme-picker-accent-stripe-size;
}
}

View File

@@ -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<ThemePickerComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ThemePickerComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ThemePickerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,74 @@
import {Component, ViewEncapsulation, ChangeDetectionStrategy, NgModule} from '@angular/core';
import { StyleManagerService } from './../../services/style-manager/style-manager.service';
import { ThemeStorageService,DocsSiteTheme } from './../../services/theme-storage/theme-storage.service';
import {CommonModule} from '@angular/common';
@Component({
selector: 'theme-picker',
templateUrl: 'theme-picker.html',
styleUrls: ['theme-picker.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: {'aria-hidden': 'true'},
})
export class ThemePickerComponent {
currentTheme;
themes = [
{
primary: '#673AB7',
accent: '#FFC107',
href: 'deeppurple-amber.css',
isDark: false,
},
{
primary: '#3F51B5',
accent: '#E91E63',
href: 'indigo-pink.css',
isDark: false,
isDefault: true,
},
{
primary: '#E91E63',
accent: '#607D8B',
href: 'pink-bluegrey.css',
isDark: true,
},
{
primary: '#9C27B0',
accent: '#4CAF50',
href: 'purple-green.css',
isDark: true,
},
];
constructor(
public styleManager: StyleManagerService,
private _themeStorage: ThemeStorageService
) {
const currentTheme = this._themeStorage.getStoredTheme();
if (currentTheme) {
this.installTheme(currentTheme);
}
}
installTheme(theme: DocsSiteTheme) {
this.currentTheme = this._getCurrentThemeFromHref(theme.href);
if (theme.isDefault) {
this.styleManager.removeStyle('theme');
} else {
this.styleManager.setStyle('theme', `assets/${theme.href}`);
}
if (this.currentTheme) {
this._themeStorage.storeTheme(this.currentTheme);
}
}
private _getCurrentThemeFromHref(href: string): DocsSiteTheme {
return this.themes.find(theme => theme.href === href);
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long