implement theme switching service

This commit is contained in:
Mononaut 2023-01-02 12:26:10 -06:00
parent f9f8bd25f8
commit f2f6e3769a
No known key found for this signature in database
GPG Key ID: A3F058E41374C04E
8 changed files with 254 additions and 67 deletions

View File

@ -1,4 +1,4 @@
export const mempoolFeeColors = [
export const defaultMempoolFeeColors = [
'557d00',
'5d7d01',
'637d02',
@ -31,6 +31,39 @@ export const mempoolFeeColors = [
'b9254b',
];
export const contrastMempoolFeeColors = [
'83fd00',
'83f609',
'83ef12',
'83e71a',
'83e023',
'83d92c',
'83d235',
'83cb3e',
'83c446',
'83bc4f',
'83b558',
'83ae61',
'83a76a',
'83a072',
'83997b',
'839184',
'838a8d',
'838395',
'837c9e',
'8375a7',
'836eb0',
'8366b9',
'835fc1',
'8358ca',
'8351d3',
'834adc',
'8343e5',
'833bed',
'8334f6',
'832dff',
];
export const chartColors = [
"#D81B60",
"#8E24AA",

View File

@ -5,6 +5,8 @@ import BlockScene from './block-scene';
import TxSprite from './tx-sprite';
import TxView from './tx-view';
import { Position } from './sprite-types';
import { ThemeService } from 'src/app/services/theme.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-block-overview-graph',
@ -26,6 +28,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@ViewChild('blockCanvas')
canvas: ElementRef<HTMLCanvasElement>;
themeChangedSubscription: Subscription;
gl: WebGLRenderingContext;
animationFrameRequest: number;
@ -48,6 +51,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
constructor(
readonly ngZone: NgZone,
readonly elRef: ElementRef,
private themeService: ThemeService,
) {
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
}
@ -59,6 +63,11 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.initCanvas();
this.resizeCanvas();
this.themeChangedSubscription = this.themeService.themeChanged$.subscribe(() => {
// force full re-render
this.resizeCanvas();
});
}
ngOnChanges(changes): void {
@ -79,6 +88,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost);
this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored);
this.themeChangedSubscription.unsubscribe();
}
clear(direction): void {
@ -195,7 +205,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.start();
} else {
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray });
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, theme: this.themeService });
this.start();
}
}

View File

@ -2,11 +2,13 @@ import { FastVertexArray } from './fast-vertex-array';
import TxView from './tx-view';
import { TransactionStripped } from '../../interfaces/websocket.interface';
import { Position, Square, ViewUpdateParams } from './sprite-types';
import { ThemeService } from 'src/app/services/theme.service';
export default class BlockScene {
scene: { count: number, offset: { x: number, y: number}};
vertexArray: FastVertexArray;
txs: { [key: string]: TxView };
theme: ThemeService;
orientation: string;
flip: boolean;
width: number;
@ -22,11 +24,11 @@ export default class BlockScene {
animateUntil = 0;
dirty: boolean;
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray }:
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, theme }:
{ width: number, height: number, resolution: number, blockLimit: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray }
orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService }
) {
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray });
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, theme });
}
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
@ -67,7 +69,7 @@ export default class BlockScene {
});
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
txs.forEach(tx => {
const txView = new TxView(tx, this.vertexArray);
const txView = new TxView(tx, this.vertexArray, this.theme);
this.txs[tx.txid] = txView;
this.place(txView);
this.saveGridToScreenPosition(txView);
@ -114,7 +116,7 @@ export default class BlockScene {
});
txs.forEach(tx => {
if (!this.txs[tx.txid]) {
this.txs[tx.txid] = new TxView(tx, this.vertexArray);
this.txs[tx.txid] = new TxView(tx, this.vertexArray, this.theme);
}
});
@ -156,7 +158,7 @@ export default class BlockScene {
if (resetLayout) {
add.forEach(tx => {
if (!this.txs[tx.txid]) {
this.txs[tx.txid] = new TxView(tx, this.vertexArray);
this.txs[tx.txid] = new TxView(tx, this.vertexArray, this.theme);
}
});
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
@ -166,7 +168,7 @@ export default class BlockScene {
} else {
// try to insert new txs directly
const remaining = [];
add.map(tx => new TxView(tx, this.vertexArray)).sort(feeRateDescending).forEach(tx => {
add.map(tx => new TxView(tx, this.vertexArray, this.theme)).sort(feeRateDescending).forEach(tx => {
if (!this.tryInsertByFee(tx)) {
remaining.push(tx);
}
@ -192,13 +194,14 @@ export default class BlockScene {
this.animateUntil = Math.max(this.animateUntil, tx.setHover(value));
}
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray }:
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, theme }:
{ width: number, height: number, resolution: number, blockLimit: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray }
orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService }
): void {
this.orientation = orientation;
this.flip = flip;
this.vertexArray = vertexArray;
this.theme = theme;
this.scene = {
count: 0,

View File

@ -2,20 +2,10 @@ import TxSprite from './tx-sprite';
import { FastVertexArray } from './fast-vertex-array';
import { TransactionStripped } from '../../interfaces/websocket.interface';
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types';
import { feeLevels, mempoolFeeColors } from '../../app.constants';
import BlockScene from './block-scene';
import { feeLevels } from '../../app.constants';
import { ThemeService } from 'src/app/services/theme.service';
const hoverTransitionTime = 300;
const defaultHoverColor = hexToColor('1bd8f4');
const feeColors = mempoolFeeColors.map(hexToColor);
const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
const auditColors = {
censored: hexToColor('f344df'),
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
added: hexToColor('0099ff'),
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
}
// convert from this class's update format to TxSprite's update format
function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams {
@ -37,6 +27,7 @@ export default class TxView implements TransactionStripped {
feerate: number;
status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
context?: 'projected' | 'actual';
theme: ThemeService;
initialised: boolean;
vertexArray: FastVertexArray;
@ -49,7 +40,7 @@ export default class TxView implements TransactionStripped {
dirty: boolean;
constructor(tx: TransactionStripped, vertexArray: FastVertexArray) {
constructor(tx: TransactionStripped, vertexArray: FastVertexArray, theme: ThemeService) {
this.context = tx.context;
this.txid = tx.txid;
this.fee = tx.fee;
@ -59,6 +50,7 @@ export default class TxView implements TransactionStripped {
this.status = tx.status;
this.initialised = false;
this.vertexArray = vertexArray;
this.theme = theme;
this.hover = false;
@ -131,10 +123,10 @@ export default class TxView implements TransactionStripped {
// Temporarily override the tx color
// returns minimum transition end time
setHover(hoverOn: boolean, color: Color | void = defaultHoverColor): number {
setHover(hoverOn: boolean, color: Color | void): number {
if (hoverOn) {
this.hover = true;
this.hoverColor = color;
this.hoverColor = color || this.theme.defaultHoverColor;
this.sprite.update({
...this.hoverColor,
@ -155,22 +147,22 @@ export default class TxView implements TransactionStripped {
getColor(): Color {
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, this.feerate) < feeLvl) - 1;
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
const feeLevelColor = this.theme.feeColors[feeLevelIndex] || this.theme.feeColors[this.theme.mempoolFeeColors.length - 1];
// Block audit
switch(this.status) {
case 'censored':
return auditColors.censored;
return this.theme.auditColors.censored;
case 'missing':
return auditColors.missing;
return this.theme.auditColors.missing;
case 'fresh':
return auditColors.missing;
return this.theme.auditColors.missing;
case 'added':
return auditColors.added;
return this.theme.auditColors.added;
case 'selected':
return auditColors.selected;
return this.theme.auditColors.selected;
case 'found':
if (this.context === 'projected') {
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
return this.theme.auditFeeColors[feeLevelIndex] || this.theme.auditFeeColors[this.theme.mempoolFeeColors.length - 1];
} else {
return feeLevelColor;
}
@ -179,31 +171,3 @@ export default class TxView implements TransactionStripped {
}
}
}
function hexToColor(hex: string): Color {
return {
r: parseInt(hex.slice(0, 2), 16) / 255,
g: parseInt(hex.slice(2, 4), 16) / 255,
b: parseInt(hex.slice(4, 6), 16) / 255,
a: 1
};
}
function desaturate(color: Color, amount: number): Color {
const gray = (color.r + color.g + color.b) / 6;
return {
r: color.r + ((gray - color.r) * amount),
g: color.g + ((gray - color.g) * amount),
b: color.b + ((gray - color.b) * amount),
a: color.a,
};
}
function darken(color: Color, amount: number): Color {
return {
r: color.r * amount,
g: color.g * amount,
b: color.b * amount,
a: color.a,
}
}

View File

@ -2,8 +2,9 @@ import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Observable } from 'rxjs';
import { Recommendedfees } from '../../interfaces/websocket.interface';
import { feeLevels, mempoolFeeColors } from '../../app.constants';
import { feeLevels } from '../../app.constants';
import { tap } from 'rxjs/operators';
import { ThemeService } from 'src/app/services/theme.service';
@Component({
selector: 'app-fees-box',
@ -18,7 +19,8 @@ export class FeesBoxComponent implements OnInit {
noPriority = '#2e324e';
constructor(
private stateService: StateService
private stateService: StateService,
private themeService: ThemeService,
) { }
ngOnInit(): void {
@ -28,11 +30,11 @@ export class FeesBoxComponent implements OnInit {
tap((fees) => {
let feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.minimumFee >= feeLvl);
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
const startColor = '#' + (mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
const startColor = '#' + (this.themeService.mempoolFeeColors[feeLevelIndex - 1] || this.themeService.mempoolFeeColors[this.themeService.mempoolFeeColors.length - 1]);
feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.fastestFee >= feeLvl);
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
const endColor = '#' + (mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
const endColor = '#' + (this.themeService.mempoolFeeColors[feeLevelIndex - 1] || this.themeService.mempoolFeeColors[this.themeService.mempoolFeeColors.length - 1]);
this.gradient = `linear-gradient(to right, ${startColor}, ${endColor})`;
this.noPriority = startColor;

View File

@ -4,11 +4,12 @@ import { MempoolBlock } from '../../interfaces/websocket.interface';
import { StateService } from '../../services/state.service';
import { Router } from '@angular/router';
import { take, map, switchMap } from 'rxjs/operators';
import { feeLevels, mempoolFeeColors } from '../../app.constants';
import { feeLevels } from '../../app.constants';
import { specialBlocks } from '../../app.constants';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { Location } from '@angular/common';
import { DifficultyAdjustment } from '../../interfaces/node-api.interface';
import { ThemeService } from 'src/app/services/theme.service';
@Component({
selector: 'app-mempool-blocks',
@ -58,6 +59,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
constructor(
private router: Router,
public stateService: StateService,
private themeService: ThemeService,
private cd: ChangeDetectorRef,
private relativeUrlPipe: RelativeUrlPipe,
private location: Location
@ -245,7 +247,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
trimmedFeeRange.forEach((fee: number) => {
let feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fee >= feeLvl);
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
gradientColors.push(mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
gradientColors.push(this.themeService.mempoolFeeColors[feeLevelIndex - 1] || this.themeService.mempoolFeeColors[this.themeService.mempoolFeeColors.length - 1]);
});
gradientColors.forEach((color, i, gc) => {

View File

@ -0,0 +1,103 @@
import { Injectable } from '@angular/core';
import { audit, Subject } from 'rxjs';
import { Color } from '../components/block-overview-graph/sprite-types';
import { defaultMempoolFeeColors, contrastMempoolFeeColors } from '../app.constants';
import { StorageService } from './storage.service';
const defaultAuditColors = {
censored: hexToColor('f344df'),
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
added: hexToColor('0099ff'),
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
};
const contrastAuditColors = {
censored: hexToColor('ffa8ff'),
missing: darken(desaturate(hexToColor('ffa8ff'), 0.3), 0.7),
added: hexToColor('00bb98'),
selected: darken(desaturate(hexToColor('00bb98'), 0.3), 0.7),
};
@Injectable({
providedIn: 'root'
})
export class ThemeService {
style: HTMLLinkElement;
theme: string = 'default';
themeChanged$: Subject<string> = new Subject();
mempoolFeeColors: string[] = defaultMempoolFeeColors;
/* block visualization colors */
defaultHoverColor: Color;
feeColors: Color[];
auditFeeColors: Color[];
auditColors: { [category: string]: Color } = defaultAuditColors;
constructor(
private storageService: StorageService,
) {
const theme = this.storageService.getValue('theme-preference') || 'default';
this.apply(theme);
}
apply(theme) {
this.theme = theme;
if (theme !== 'default') {
if (theme === 'contrast') {
this.mempoolFeeColors = contrastMempoolFeeColors;
this.auditColors = contrastAuditColors;
}
if (!this.style) {
this.style = document.createElement('link');
this.style.rel = 'stylesheet';
this.style.href = `theme-${theme}.css`;
document.head.appendChild(this.style);
} else {
this.style.href = `theme-${theme}.css`;
}
} else {
this.mempoolFeeColors = defaultMempoolFeeColors;
this.auditColors = defaultAuditColors;
if (this.style) {
this.style.remove();
this.style = null;
}
}
this.updateFeeColors();
this.storageService.setValue('theme-preference', theme);
this.themeChanged$.next(this.theme);
}
updateFeeColors() {
this.defaultHoverColor = hexToColor('1bd8f4');
this.feeColors = this.mempoolFeeColors.map(hexToColor);
this.auditFeeColors = this.feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
}
}
export function hexToColor(hex: string): Color {
return {
r: parseInt(hex.slice(0, 2), 16) / 255,
g: parseInt(hex.slice(2, 4), 16) / 255,
b: parseInt(hex.slice(4, 6), 16) / 255,
a: 1
};
}
export function desaturate(color: Color, amount: number): Color {
const gray = (color.r + color.g + color.b) / 6;
return {
r: color.r + ((gray - color.r) * amount),
g: color.g + ((gray - color.g) * amount),
b: color.b + ((gray - color.b) * amount),
a: color.a,
};
}
export function darken(color: Color, amount: number): Color {
return {
r: color.r * amount,
g: color.g * amount,
b: color.b * amount,
a: color.a,
}
}

View File

@ -0,0 +1,70 @@
/* Theme */
$bg: #ff1f31;
$active-bg: #ff131f;
$hover-bg: #ff131e;
$fg: #ff0;
/* Bootstrap */
$body-bg: $bg;
$body-color: $fg;
$gray-800: $bg;
$gray-700: $fg;
$nav-tabs-link-active-bg: $active-bg;
$primary: #105fb0;
$secondary: #2d3348;
$tertiary: #653b9c;
$success: #1a9436;
$info: #1bd8f4;
$h5-font-size: 1.15rem !default;
$pagination-bg: $body-bg;
$pagination-border-color: $gray-800;
$pagination-disabled-bg: $fg;
$pagination-disabled-border-color: $bg;
$pagination-active-color: $fg;
$pagination-active-bg: $tertiary;
$pagination-hover-bg: $hover-bg;
$pagination-hover-border-color: $bg;
$pagination-disabled-bg: $bg;
$custom-select-indicator-color: $fg;
.input-group-text {
background-color: #1c2031 !important;
border: 1px solid #20263e !important;
}
$link-color: $info;
$link-decoration: none !default;
$link-hover-color: darken($link-color, 15%) !default;
$link-hover-decoration: underline !default;
$dropdown-bg: $bg;
$dropdown-link-color: $fg;
$dropdown-link-hover-color: $fg;
$dropdown-link-hover-bg: $active-bg;
$dropdown-link-active-color: $fg;
$dropdown-link-active-bg: $active-bg;
@import "~bootstrap/scss/bootstrap";
:root {
--bg: #{$bg};
--active-bg: #{$active-bg};
--hover-bg: #{$hover-bg};
--fg: #{$fg};
--primary: #{$primary};
--secondary: #{$secondary};
--tertiary: #{$tertiary};
--success: #{$success};
--info: #{$info};
--box-bg: var(--box-bg);
}