implement theme switching service
This commit is contained in:
		
							parent
							
								
									ee92f6639a
								
							
						
					
					
						commit
						4c205eb09d
					
				| @ -1,4 +1,4 @@ | |||||||
| export const mempoolFeeColors = [ | export const defaultMempoolFeeColors = [ | ||||||
|   '557d00', |   '557d00', | ||||||
|   '5d7d01', |   '5d7d01', | ||||||
|   '637d02', |   '637d02', | ||||||
| @ -39,6 +39,47 @@ export const mempoolFeeColors = [ | |||||||
|   'ae005b', |   'ae005b', | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
|  | 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', | ||||||
|  |   '832dff', | ||||||
|  |   '832dff', | ||||||
|  |   '832dff', | ||||||
|  |   '832dff', | ||||||
|  |   '832dff', | ||||||
|  |   '832dff', | ||||||
|  |   '832dff', | ||||||
|  |   '832dff', | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
| export const chartColors = [ | export const chartColors = [ | ||||||
|   "#D81B60", |   "#D81B60", | ||||||
|   "#8E24AA", |   "#8E24AA", | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ import TxView from './tx-view'; | |||||||
| import { Color, Position } from './sprite-types'; | import { Color, Position } from './sprite-types'; | ||||||
| import { Price } from '../../services/price.service'; | import { Price } from '../../services/price.service'; | ||||||
| import { StateService } from '../../services/state.service'; | import { StateService } from '../../services/state.service'; | ||||||
|  | import { ThemeService } from 'src/app/services/theme.service'; | ||||||
| import { Subscription } from 'rxjs'; | import { Subscription } from 'rxjs'; | ||||||
| import { defaultColorFunction, setOpacity, defaultFeeColors, defaultAuditFeeColors, defaultMarginalFeeColors, defaultAuditColors } from './utils'; | import { defaultColorFunction, setOpacity, defaultFeeColors, defaultAuditFeeColors, defaultMarginalFeeColors, defaultAuditColors } from './utils'; | ||||||
| import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils'; | import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils'; | ||||||
| @ -55,6 +56,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | |||||||
| 
 | 
 | ||||||
|   @ViewChild('blockCanvas') |   @ViewChild('blockCanvas') | ||||||
|   canvas: ElementRef<HTMLCanvasElement>; |   canvas: ElementRef<HTMLCanvasElement>; | ||||||
|  |   themeChangedSubscription: Subscription; | ||||||
| 
 | 
 | ||||||
|   gl: WebGLRenderingContext; |   gl: WebGLRenderingContext; | ||||||
|   animationFrameRequest: number; |   animationFrameRequest: number; | ||||||
| @ -86,6 +88,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | |||||||
|     readonly ngZone: NgZone, |     readonly ngZone: NgZone, | ||||||
|     readonly elRef: ElementRef, |     readonly elRef: ElementRef, | ||||||
|     public stateService: StateService, |     public stateService: StateService, | ||||||
|  |     private themeService: ThemeService, | ||||||
|   ) { |   ) { | ||||||
|     this.webGlEnabled = this.stateService.isBrowser && detectWebGL(); |     this.webGlEnabled = this.stateService.isBrowser && detectWebGL(); | ||||||
|     this.vertexArray = new FastVertexArray(512, TxSprite.dataSize); |     this.vertexArray = new FastVertexArray(512, TxSprite.dataSize); | ||||||
| @ -104,6 +107,10 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | |||||||
|       if (this.gl) { |       if (this.gl) { | ||||||
|         this.initCanvas(); |         this.initCanvas(); | ||||||
|         this.resizeCanvas(); |         this.resizeCanvas(); | ||||||
|  |         this.themeChangedSubscription = this.themeService.themeChanged$.subscribe(() => { | ||||||
|  |           // force full re-render
 | ||||||
|  |           this.resizeCanvas(); | ||||||
|  |         }); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @ -149,6 +156,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | |||||||
|     if (this.canvas) { |     if (this.canvas) { | ||||||
|       this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost); |       this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost); | ||||||
|       this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored); |       this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored); | ||||||
|  |       this.themeChangedSubscription?.unsubscribe(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -291,7 +299,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | |||||||
|         this.start(); |         this.start(); | ||||||
|       } else { |       } else { | ||||||
|         this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution, |         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, | ||||||
|           highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset, |           highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset, | ||||||
|         colorFunction: this.getColorFunction() }); |         colorFunction: this.getColorFunction() }); | ||||||
|         this.start(); |         this.start(); | ||||||
|  | |||||||
| @ -3,12 +3,14 @@ import TxView from './tx-view'; | |||||||
| import { TransactionStripped } from '../../interfaces/node-api.interface'; | import { TransactionStripped } from '../../interfaces/node-api.interface'; | ||||||
| import { Color, Position, Square, ViewUpdateParams } from './sprite-types'; | import { Color, Position, Square, ViewUpdateParams } from './sprite-types'; | ||||||
| import { defaultColorFunction } from './utils'; | import { defaultColorFunction } from './utils'; | ||||||
|  | import { ThemeService } from 'src/app/services/theme.service'; | ||||||
| 
 | 
 | ||||||
| export default class BlockScene { | export default class BlockScene { | ||||||
|   scene: { count: number, offset: { x: number, y: number}}; |   scene: { count: number, offset: { x: number, y: number}}; | ||||||
|   vertexArray: FastVertexArray; |   vertexArray: FastVertexArray; | ||||||
|   txs: { [key: string]: TxView }; |   txs: { [key: string]: TxView }; | ||||||
|   getColor: ((tx: TxView) => Color) = defaultColorFunction; |   getColor: ((tx: TxView) => Color) = defaultColorFunction; | ||||||
|  |   theme: ThemeService; | ||||||
|   orientation: string; |   orientation: string; | ||||||
|   flip: boolean; |   flip: boolean; | ||||||
|   animationDuration: number = 900; |   animationDuration: number = 900; | ||||||
| @ -29,11 +31,11 @@ export default class BlockScene { | |||||||
|   animateUntil = 0; |   animateUntil = 0; | ||||||
|   dirty: boolean; |   dirty: boolean; | ||||||
| 
 | 
 | ||||||
|   constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting, colorFunction }: |   constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }: | ||||||
|       { width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, |       { width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, | ||||||
|         orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null } |         orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null } | ||||||
|   ) { |   ) { | ||||||
|     this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting, colorFunction }); |     this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void { |   resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void { | ||||||
| @ -90,7 +92,7 @@ export default class BlockScene { | |||||||
|     }); |     }); | ||||||
|     this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight }); |     this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight }); | ||||||
|     txs.forEach(tx => { |     txs.forEach(tx => { | ||||||
|       const txView = new TxView(tx, this); |       const txView = new TxView(tx, this, this.theme); | ||||||
|       this.txs[tx.txid] = txView; |       this.txs[tx.txid] = txView; | ||||||
|       this.place(txView); |       this.place(txView); | ||||||
|       this.saveGridToScreenPosition(txView); |       this.saveGridToScreenPosition(txView); | ||||||
| @ -136,7 +138,7 @@ export default class BlockScene { | |||||||
|     }); |     }); | ||||||
|     txs.forEach(tx => { |     txs.forEach(tx => { | ||||||
|       if (!this.txs[tx.txid]) { |       if (!this.txs[tx.txid]) { | ||||||
|         this.txs[tx.txid] = new TxView(tx, this); |         this.txs[tx.txid] = new TxView(tx, this, this.theme); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| @ -178,7 +180,7 @@ export default class BlockScene { | |||||||
|     if (resetLayout) { |     if (resetLayout) { | ||||||
|       add.forEach(tx => { |       add.forEach(tx => { | ||||||
|         if (!this.txs[tx.txid]) { |         if (!this.txs[tx.txid]) { | ||||||
|           this.txs[tx.txid] = new TxView(tx, this); |           this.txs[tx.txid] = new TxView(tx, this, this.theme); | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|       this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight }); |       this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight }); | ||||||
| @ -198,7 +200,7 @@ export default class BlockScene { | |||||||
| 
 | 
 | ||||||
|       // try to insert new txs directly
 |       // try to insert new txs directly
 | ||||||
|       const remaining = []; |       const remaining = []; | ||||||
|       add.map(tx => new TxView(tx, this)).sort(feeRateDescending).forEach(tx => { |       add.map(tx => new TxView(tx, this, this.theme)).sort(feeRateDescending).forEach(tx => { | ||||||
|         if (!this.tryInsertByFee(tx)) { |         if (!this.tryInsertByFee(tx)) { | ||||||
|           remaining.push(tx); |           remaining.push(tx); | ||||||
|         } |         } | ||||||
| @ -228,9 +230,9 @@ export default class BlockScene { | |||||||
|     this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value)); |     this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting, colorFunction }: |   private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }: | ||||||
|       { width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, |       { width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, | ||||||
|         orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null } |         orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null } | ||||||
|   ): void { |   ): void { | ||||||
|     this.animationDuration = animationDuration || 1000; |     this.animationDuration = animationDuration || 1000; | ||||||
|     this.configAnimationOffset = animationOffset; |     this.configAnimationOffset = animationOffset; | ||||||
| @ -240,6 +242,7 @@ export default class BlockScene { | |||||||
|     this.vertexArray = vertexArray; |     this.vertexArray = vertexArray; | ||||||
|     this.highlightingEnabled = highlighting; |     this.highlightingEnabled = highlighting; | ||||||
|     this.getColor = colorFunction || defaultColorFunction; |     this.getColor = colorFunction || defaultColorFunction; | ||||||
|  |     this.theme = theme; | ||||||
| 
 | 
 | ||||||
|     this.scene = { |     this.scene = { | ||||||
|       count: 0, |       count: 0, | ||||||
|  | |||||||
| @ -5,6 +5,8 @@ import { hexToColor } from './utils'; | |||||||
| import BlockScene from './block-scene'; | import BlockScene from './block-scene'; | ||||||
| import { TransactionStripped } from '../../interfaces/node-api.interface'; | import { TransactionStripped } from '../../interfaces/node-api.interface'; | ||||||
| import { TransactionFlags } from '../../shared/filters.utils'; | import { TransactionFlags } from '../../shared/filters.utils'; | ||||||
|  | import { feeLevels } from '../../app.constants'; | ||||||
|  | import { ThemeService } from 'src/app/services/theme.service'; | ||||||
| 
 | 
 | ||||||
| const hoverTransitionTime = 300; | const hoverTransitionTime = 300; | ||||||
| const defaultHoverColor = hexToColor('1bd8f4'); | const defaultHoverColor = hexToColor('1bd8f4'); | ||||||
| @ -36,6 +38,7 @@ export default class TxView implements TransactionStripped { | |||||||
|   status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated'; |   status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated'; | ||||||
|   context?: 'projected' | 'actual'; |   context?: 'projected' | 'actual'; | ||||||
|   scene?: BlockScene; |   scene?: BlockScene; | ||||||
|  |   theme: ThemeService; | ||||||
| 
 | 
 | ||||||
|   initialised: boolean; |   initialised: boolean; | ||||||
|   vertexArray: FastVertexArray; |   vertexArray: FastVertexArray; | ||||||
| @ -50,7 +53,7 @@ export default class TxView implements TransactionStripped { | |||||||
| 
 | 
 | ||||||
|   dirty: boolean; |   dirty: boolean; | ||||||
| 
 | 
 | ||||||
|   constructor(tx: TransactionStripped, scene: BlockScene) { |   constructor(tx: TransactionStripped, scene: BlockScene, theme: ThemeService) { | ||||||
|     this.scene = scene; |     this.scene = scene; | ||||||
|     this.context = tx.context; |     this.context = tx.context; | ||||||
|     this.txid = tx.txid; |     this.txid = tx.txid; | ||||||
| @ -66,6 +69,7 @@ export default class TxView implements TransactionStripped { | |||||||
|     this.bigintFlags = tx.flags ? (BigInt(tx.flags) | (this.acc ? TransactionFlags.acceleration : 0n)): 0n; |     this.bigintFlags = tx.flags ? (BigInt(tx.flags) | (this.acc ? TransactionFlags.acceleration : 0n)): 0n; | ||||||
|     this.initialised = false; |     this.initialised = false; | ||||||
|     this.vertexArray = scene.vertexArray; |     this.vertexArray = scene.vertexArray; | ||||||
|  |     this.theme = theme; | ||||||
| 
 | 
 | ||||||
|     this.hover = false; |     this.hover = false; | ||||||
| 
 | 
 | ||||||
| @ -138,10 +142,10 @@ export default class TxView implements TransactionStripped { | |||||||
| 
 | 
 | ||||||
|   // Temporarily override the tx color
 |   // Temporarily override the tx color
 | ||||||
|   // returns minimum transition end time
 |   // returns minimum transition end time
 | ||||||
|   setHover(hoverOn: boolean, color: Color | void = defaultHoverColor): number { |   setHover(hoverOn: boolean, color: Color | void): number { | ||||||
|     if (hoverOn) { |     if (hoverOn) { | ||||||
|       this.hover = true; |       this.hover = true; | ||||||
|       this.hoverColor = color; |       this.hoverColor = color || this.theme.defaultHoverColor; | ||||||
| 
 | 
 | ||||||
|       this.sprite.update({ |       this.sprite.update({ | ||||||
|         ...this.hoverColor, |         ...this.hoverColor, | ||||||
| @ -191,4 +195,30 @@ export default class TxView implements TransactionStripped { | |||||||
|     this.dirty = false; |     this.dirty = false; | ||||||
|     return performance.now() + hoverTransitionTime; |     return performance.now() + hoverTransitionTime; | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   getColor(): Color { | ||||||
|  |     const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, this.feerate) < feeLvl) - 1; | ||||||
|  |     const feeLevelColor = this.theme.feeColors[feeLevelIndex] || this.theme.feeColors[this.theme.mempoolFeeColors.length - 1]; | ||||||
|  |     // Block audit
 | ||||||
|  |     switch(this.status) { | ||||||
|  |       case 'censored': | ||||||
|  |         return this.theme.auditColors.censored; | ||||||
|  |       case 'missing': | ||||||
|  |         return this.theme.auditColors.missing; | ||||||
|  |       case 'fresh': | ||||||
|  |         return this.theme.auditColors.missing; | ||||||
|  |       case 'added': | ||||||
|  |         return this.theme.auditColors.added; | ||||||
|  |       case 'selected': | ||||||
|  |         return this.theme.auditColors.selected; | ||||||
|  |       case 'found': | ||||||
|  |         if (this.context === 'projected') { | ||||||
|  |           return this.theme.auditFeeColors[feeLevelIndex] || this.theme.auditFeeColors[this.theme.mempoolFeeColors.length - 1]; | ||||||
|  |         } else { | ||||||
|  |           return feeLevelColor; | ||||||
|  |         } | ||||||
|  |       default: | ||||||
|  |         return feeLevelColor; | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -2,8 +2,9 @@ import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; | |||||||
| import { StateService } from '../../services/state.service'; | import { StateService } from '../../services/state.service'; | ||||||
| import { Observable, combineLatest } from 'rxjs'; | import { Observable, combineLatest } from 'rxjs'; | ||||||
| import { Recommendedfees } from '../../interfaces/websocket.interface'; | import { Recommendedfees } from '../../interfaces/websocket.interface'; | ||||||
| import { feeLevels, mempoolFeeColors } from '../../app.constants'; | import { feeLevels } from '../../app.constants'; | ||||||
| import { map, startWith, tap } from 'rxjs/operators'; | import { map, startWith, tap } from 'rxjs/operators'; | ||||||
|  | import { ThemeService } from 'src/app/services/theme.service'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-fees-box', |   selector: 'app-fees-box', | ||||||
| @ -18,7 +19,8 @@ export class FeesBoxComponent implements OnInit { | |||||||
|   noPriority = '#2e324e'; |   noPriority = '#2e324e'; | ||||||
| 
 | 
 | ||||||
|   constructor( |   constructor( | ||||||
|     private stateService: StateService |     private stateService: StateService, | ||||||
|  |     private themeService: ThemeService, | ||||||
|   ) { } |   ) { } | ||||||
| 
 | 
 | ||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
| @ -33,11 +35,11 @@ export class FeesBoxComponent implements OnInit { | |||||||
|         tap((fees) => { |         tap((fees) => { | ||||||
|           let feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.minimumFee >= feeLvl); |           let feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.minimumFee >= feeLvl); | ||||||
|           feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex; |           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 = feeLevels.slice().reverse().findIndex((feeLvl) => fees.fastestFee >= feeLvl); | ||||||
|           feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex; |           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.gradient = `linear-gradient(to right, ${startColor}, ${endColor})`; | ||||||
|           this.noPriority = startColor; |           this.noPriority = startColor; | ||||||
|  | |||||||
| @ -4,12 +4,13 @@ import { MempoolBlock } from '../../interfaces/websocket.interface'; | |||||||
| import { StateService } from '../../services/state.service'; | import { StateService } from '../../services/state.service'; | ||||||
| import { Router } from '@angular/router'; | import { Router } from '@angular/router'; | ||||||
| import { map, switchMap, tap } from 'rxjs/operators'; | import { map, switchMap, tap } from 'rxjs/operators'; | ||||||
| import { feeLevels, mempoolFeeColors } from '../../app.constants'; | import { feeLevels } from '../../app.constants'; | ||||||
| import { specialBlocks } from '../../app.constants'; | import { specialBlocks } from '../../app.constants'; | ||||||
| import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; | import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; | ||||||
| import { Location } from '@angular/common'; | import { Location } from '@angular/common'; | ||||||
| import { DifficultyAdjustment, MempoolPosition } from '../../interfaces/node-api.interface'; | import { DifficultyAdjustment, MempoolPosition } from '../../interfaces/node-api.interface'; | ||||||
| import { animate, style, transition, trigger } from '@angular/animations'; | import { animate, style, transition, trigger } from '@angular/animations'; | ||||||
|  | import { ThemeService } from 'src/app/services/theme.service'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-mempool-blocks', |   selector: 'app-mempool-blocks', | ||||||
| @ -84,6 +85,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|   constructor( |   constructor( | ||||||
|     private router: Router, |     private router: Router, | ||||||
|     public stateService: StateService, |     public stateService: StateService, | ||||||
|  |     private themeService: ThemeService, | ||||||
|     private cd: ChangeDetectorRef, |     private cd: ChangeDetectorRef, | ||||||
|     private relativeUrlPipe: RelativeUrlPipe, |     private relativeUrlPipe: RelativeUrlPipe, | ||||||
|     private location: Location, |     private location: Location, | ||||||
| @ -354,7 +356,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|     trimmedFeeRange.forEach((fee: number) => { |     trimmedFeeRange.forEach((fee: number) => { | ||||||
|       let feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fee >= feeLvl); |       let feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fee >= feeLvl); | ||||||
|       feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex; |       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) => { |     gradientColors.forEach((color, i, gc) => { | ||||||
|  | |||||||
							
								
								
									
										103
									
								
								frontend/src/app/services/theme.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								frontend/src/app/services/theme.service.ts
									
									
									
									
									
										Normal 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, | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										70
									
								
								frontend/src/theme-contrast.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								frontend/src/theme-contrast.scss
									
									
									
									
									
										Normal 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); | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user