Test rendering multiple blocks on one canvas
This commit is contained in:
		
							parent
							
								
									ca7221f8b7
								
							
						
					
					
						commit
						1da6123332
					
				| @ -18,6 +18,8 @@ export default class BlockScene { | ||||
|   animationOffset: number; | ||||
|   highlightingEnabled: boolean; | ||||
|   filterFlags: bigint | null = 0b00000100_00000000_00000000_00000000n; | ||||
|   x: number; | ||||
|   y: number; | ||||
|   width: number; | ||||
|   height: number; | ||||
|   gridWidth: number; | ||||
| @ -31,14 +33,16 @@ export default class BlockScene { | ||||
|   animateUntil = 0; | ||||
|   dirty: boolean; | ||||
| 
 | ||||
|   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, | ||||
|   constructor({ x = 0, y = 0, width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }: | ||||
|       { x?: number, y?: number, width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, | ||||
|         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, theme, highlighting, colorFunction }); | ||||
|     this.init({ x, y,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({ x = 0, y = 0, width = this.width, height = this.height, animate = true }: { x?: number, y?: number, width?: number, height?: number, animate: boolean }): void { | ||||
|     this.x = x; | ||||
|     this.y = y; | ||||
|     this.width = width; | ||||
|     this.height = height; | ||||
|     this.gridSize = this.width / this.gridWidth; | ||||
| @ -238,8 +242,8 @@ export default class BlockScene { | ||||
|     this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value)); | ||||
|   } | ||||
| 
 | ||||
|   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, | ||||
|   private init({ x, y, width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }: | ||||
|       { x: number, y: number, width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, | ||||
|         orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null } | ||||
|   ): void { | ||||
|     this.animationDuration = animationDuration || this.animationDuration || 1000; | ||||
| @ -264,7 +268,7 @@ export default class BlockScene { | ||||
|     this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.02, 2); | ||||
|     this.gridWidth = resolution; | ||||
|     this.gridHeight = resolution; | ||||
|     this.resize({ width, height, animate: true }); | ||||
|     this.resize({ x, y, width, height, animate: true }); | ||||
|     this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight }); | ||||
| 
 | ||||
|     this.txs = {}; | ||||
| @ -449,18 +453,18 @@ export default class BlockScene { | ||||
|           break; | ||||
|       } | ||||
|       return { | ||||
|         x: x + this.unitPadding - (slotSize / 2), | ||||
|         y: y + this.unitPadding - (slotSize / 2), | ||||
|         x: this.x + x + this.unitPadding - (slotSize / 2), | ||||
|         y: this.y + y + this.unitPadding - (slotSize / 2), | ||||
|         s: squareSize | ||||
|       }; | ||||
|     } else { | ||||
|       return { x: 0, y: 0, s: 0 }; | ||||
|       return { x: this.x, y: this.y, s: 0 }; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private screenToGrid(position: Position): Position { | ||||
|     let x = position.x; | ||||
|     let y = this.height - position.y; | ||||
|     let x = position.x - this.x; | ||||
|     let y = this.height - (position.y - this.y); | ||||
|     let t; | ||||
| 
 | ||||
|     switch (this.orientation) { | ||||
|  | ||||
| @ -0,0 +1,24 @@ | ||||
| 
 | ||||
| <div class="block-overview-graph"> | ||||
|   <canvas *browserOnly class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas> | ||||
|   @if (!disableSpinner) { | ||||
|     <div class="loader-wrapper" [class.hidden]="!isLoading && !unavailable"> | ||||
|       <div *ngIf="!unavailable" class="spinner-border ml-3 loading" role="status"></div> | ||||
|       <div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div> | ||||
|     </div> | ||||
|   } | ||||
|   <app-block-overview-tooltip | ||||
|     [tx]="selectedTx || hoverTx" | ||||
|     [cursorPosition]="tooltipPosition" | ||||
|     [clickable]="!!selectedTx" | ||||
|     [auditEnabled]="auditHighlighting" | ||||
|     [blockConversion]="blockConversion" | ||||
|     [filterFlags]="activeFilterFlags" | ||||
|     [filterMode]="filterMode" | ||||
|     [relativeTime]="relativeTime" | ||||
|   ></app-block-overview-tooltip> | ||||
|   <app-block-filters *ngIf="webGlEnabled && showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters> | ||||
|   <div *ngIf="!webGlEnabled" class="placeholder"> | ||||
|     <span i18n="webgl-disabled">Your browser does not support this feature.</span> | ||||
|   </div> | ||||
| </div> | ||||
| @ -0,0 +1,67 @@ | ||||
| .block-overview-graph { | ||||
|   position: relative; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   background: var(--stat-box-bg); | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|   grid-column: 1/-1; | ||||
| 
 | ||||
|   .placeholder { | ||||
|     display: flex; | ||||
|     position: absolute; | ||||
|     left: 0; | ||||
|     right: 0; | ||||
|     top: 0; | ||||
|     bottom: 0; | ||||
|     height: 100%; | ||||
|     width: 100%; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .graph-alignment { | ||||
|   position: relative; | ||||
|   width: 100%; | ||||
| } | ||||
| 
 | ||||
| .grid-align { | ||||
|   display: grid; | ||||
|   grid-template-columns: repeat(auto-fit, 75px); | ||||
|   justify-content: center; | ||||
| } | ||||
| 
 | ||||
| .block-overview-canvas { | ||||
|   position: absolute; | ||||
|   left: 0; | ||||
|   right: 0; | ||||
|   top: 0; | ||||
|   bottom: 0; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   overflow: hidden; | ||||
| 
 | ||||
|   &.clickable { | ||||
|     cursor: pointer; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .loader-wrapper { | ||||
|   position: absolute; | ||||
|   background: #181b2d7f; | ||||
|   left: 0; | ||||
|   right: 0; | ||||
|   top: 0; | ||||
|   bottom: 0; | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|   transition: opacity 500ms 500ms; | ||||
|   pointer-events: none; | ||||
| 
 | ||||
|   &.hidden { | ||||
|     opacity: 0; | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,647 @@ | ||||
| import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy, OnChanges } from '@angular/core'; | ||||
| import { TransactionStripped } from '../../interfaces/node-api.interface'; | ||||
| import { FastVertexArray } from '../block-overview-graph/fast-vertex-array'; | ||||
| import BlockScene from '../block-overview-graph/block-scene'; | ||||
| import TxSprite from '../block-overview-graph/tx-sprite'; | ||||
| import TxView from '../block-overview-graph/tx-view'; | ||||
| import { Color, Position } from '../block-overview-graph/sprite-types'; | ||||
| import { Price } from '../../services/price.service'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { ThemeService } from '../../services/theme.service'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| import { defaultColorFunction, setOpacity, defaultAuditColors, defaultColors, ageColorFunction, contrastColorFunction, contrastAuditColors, contrastColors } from '../block-overview-graph/utils'; | ||||
| import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils'; | ||||
| import { detectWebGL } from '../../shared/graphs.utils'; | ||||
| 
 | ||||
| const unmatchedOpacity = 0.2; | ||||
| const unmatchedAuditColors = { | ||||
|   censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity), | ||||
|   missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity), | ||||
|   added: setOpacity(defaultAuditColors.added, unmatchedOpacity), | ||||
|   added_prioritized: setOpacity(defaultAuditColors.added_prioritized, unmatchedOpacity), | ||||
|   prioritized: setOpacity(defaultAuditColors.prioritized, unmatchedOpacity), | ||||
|   accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity), | ||||
| }; | ||||
| const unmatchedContrastAuditColors = { | ||||
|   censored: setOpacity(contrastAuditColors.censored, unmatchedOpacity), | ||||
|   missing: setOpacity(contrastAuditColors.missing, unmatchedOpacity), | ||||
|   added: setOpacity(contrastAuditColors.added, unmatchedOpacity), | ||||
|   added_prioritized: setOpacity(contrastAuditColors.added_prioritized, unmatchedOpacity), | ||||
|   prioritized: setOpacity(contrastAuditColors.prioritized, unmatchedOpacity), | ||||
|   accelerated: setOpacity(contrastAuditColors.accelerated, unmatchedOpacity), | ||||
| }; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-block-overview-multi', | ||||
|   templateUrl: './block-overview-multi.component.html', | ||||
|   styleUrls: ['./block-overview-multi.component.scss'], | ||||
| }) | ||||
| export class BlockOverviewMultiComponent implements AfterViewInit, OnDestroy, OnChanges { | ||||
|   @Input() isLoading: boolean; | ||||
|   @Input() resolution: number; | ||||
|   @Input() numBlocks: number; | ||||
|   @Input() blockWidth: number = 360; | ||||
|   @Input() autofit: boolean = false; | ||||
|   @Input() blockLimit: number; | ||||
|   @Input() orientation = 'left'; | ||||
|   @Input() flip = true; | ||||
|   @Input() animationDuration: number = 1000; | ||||
|   @Input() animationOffset: number | null = null; | ||||
|   @Input() disableSpinner = false; | ||||
|   @Input() mirrorTxid: string | void; | ||||
|   @Input() unavailable: boolean = false; | ||||
|   @Input() auditHighlighting: boolean = false; | ||||
|   @Input() showFilters: boolean = false; | ||||
|   @Input() excludeFilters: string[] = []; | ||||
|   @Input() filterFlags: bigint | null = null; | ||||
|   @Input() filterMode: FilterMode = 'and'; | ||||
|   @Input() gradientMode: 'fee' | 'age' = 'fee'; | ||||
|   @Input() relativeTime: number | null; | ||||
|   @Input() blockConversion: Price; | ||||
|   @Input() overrideColors: ((tx: TxView) => Color) | null = null; | ||||
|   @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>(); | ||||
|   @Output() txHoverEvent = new EventEmitter<string>(); | ||||
|   @Output() readyEvent = new EventEmitter(); | ||||
| 
 | ||||
|   @ViewChild('blockCanvas') | ||||
|   canvas: ElementRef<HTMLCanvasElement>; | ||||
|   themeChangedSubscription: Subscription; | ||||
| 
 | ||||
|   gl: WebGLRenderingContext; | ||||
|   animationFrameRequest: number; | ||||
|   animationHeartBeat: number; | ||||
|   displayWidth: number; | ||||
|   displayHeight: number; | ||||
|   cssWidth: number; | ||||
|   cssHeight: number; | ||||
|   shaderProgram: WebGLProgram; | ||||
|   vertexArray: FastVertexArray; | ||||
|   running: boolean; | ||||
|   scenes: BlockScene[] = []; | ||||
|   hoverTx: TxView | void; | ||||
|   selectedTx: TxView | void; | ||||
|   highlightTx: TxView | void; | ||||
|   mirrorTx: TxView | void; | ||||
|   tooltipPosition: Position; | ||||
| 
 | ||||
|   readyNextFrame = false; | ||||
|   lastUpdate: number = 0; | ||||
|   pendingUpdates: { | ||||
|     count: number, | ||||
|     add: { [txid: string]: TransactionStripped }, | ||||
|     remove: { [txid: string]: string }, | ||||
|     change: { [txid: string]: { txid: string, rate: number | undefined, acc: boolean | undefined } }, | ||||
|     direction?: string, | ||||
|   }[] = []; | ||||
| 
 | ||||
|   searchText: string; | ||||
|   searchSubscription: Subscription; | ||||
|   filtersAvailable: boolean = true; | ||||
|   activeFilterFlags: bigint | null = null; | ||||
| 
 | ||||
|   webGlEnabled = true; | ||||
| 
 | ||||
|   constructor( | ||||
|     readonly ngZone: NgZone, | ||||
|     readonly elRef: ElementRef, | ||||
|     public stateService: StateService, | ||||
|     private themeService: ThemeService, | ||||
|   ) { | ||||
|     this.webGlEnabled = this.stateService.isBrowser && detectWebGL(); | ||||
|     this.vertexArray = new FastVertexArray(512, TxSprite.dataSize); | ||||
|   } | ||||
| 
 | ||||
|   ngAfterViewInit(): void { | ||||
|     if (this.canvas) { | ||||
|       this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false); | ||||
|       this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false); | ||||
|       this.gl = this.canvas.nativeElement.getContext('webgl'); | ||||
|       this.initScenes(); | ||||
| 
 | ||||
|       if (this.gl) { | ||||
|         this.initCanvas(); | ||||
|         this.resizeCanvas(); | ||||
|         this.themeChangedSubscription = this.themeService.themeChanged$.subscribe(() => { | ||||
|           for (const scene of this.scenes) { | ||||
|             scene.setColorFunction(this.getColorFunction()); | ||||
|           } | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   initScenes(): void { | ||||
|     for (const scene of this.scenes) { | ||||
|       if (scene) { | ||||
|         scene.destroy(); | ||||
|       } | ||||
|     } | ||||
|     this.scenes = []; | ||||
|     this.pendingUpdates = []; | ||||
|     for (let i = 0; i < this.numBlocks; i++) { | ||||
|       this.scenes.push(null); | ||||
|       this.pendingUpdates.push({ | ||||
|         count: 0, | ||||
|         add: {}, | ||||
|         remove: {}, | ||||
|         change: {}, | ||||
|         direction: 'left', | ||||
|       }); | ||||
|     } | ||||
|     this.resizeCanvas(); | ||||
|     this.start(); | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges(changes): void { | ||||
|     if (changes.numBlocks) { | ||||
|       this.initScenes(); | ||||
|     } | ||||
|     if (changes.orientation || changes.flip) { | ||||
|       for (const scene of this.scenes) { | ||||
|         scene?.setOrientation(this.orientation, this.flip); | ||||
|       } | ||||
|     } | ||||
|     if (changes.auditHighlighting) { | ||||
|       this.setHighlightingEnabled(this.auditHighlighting); | ||||
|     } | ||||
|     if (changes.overrideColor) { | ||||
|       for (const scene of this.scenes) { | ||||
|         scene?.setColorFunction(this.getFilterColorFunction(0n, this.gradientMode)); | ||||
|       } | ||||
|     } | ||||
|     if ((changes.filterFlags || changes.showFilters || changes.filterMode || changes.gradientMode)) { | ||||
|       this.setFilterFlags(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setFilterFlags(goggle?: ActiveFilter): void { | ||||
|     this.filterMode = goggle?.mode || this.filterMode; | ||||
|     this.gradientMode = goggle?.gradient || this.gradientMode; | ||||
|     this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags; | ||||
|     for (const scene of this.scenes) { | ||||
|       if (this.activeFilterFlags != null && this.filtersAvailable) { | ||||
|         scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags, this.gradientMode)); | ||||
|       } else { | ||||
|         scene.setColorFunction(this.getFilterColorFunction(0n, this.gradientMode)); | ||||
|       } | ||||
|     } | ||||
|     this.start(); | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|     if (this.animationFrameRequest) { | ||||
|       cancelAnimationFrame(this.animationFrameRequest); | ||||
|       clearTimeout(this.animationHeartBeat); | ||||
|     } | ||||
|     if (this.canvas) { | ||||
|       this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost); | ||||
|       this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored); | ||||
|       this.themeChangedSubscription?.unsubscribe(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   clear(block: number, direction): void { | ||||
|     this.exit(block, direction); | ||||
|     this.start(); | ||||
|   } | ||||
| 
 | ||||
|   destroy(block: number): void { | ||||
|     if (this.scenes[block]) { | ||||
|       this.scenes[block].destroy(); | ||||
|       this.clearUpdateQueue(block); | ||||
|       this.start(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // initialize the scene without any entry transition
 | ||||
|   setup(block: number, transactions: TransactionStripped[], sort: boolean = false): void { | ||||
|     const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false); | ||||
|     if (filtersAvailable !== this.filtersAvailable) { | ||||
|       this.setFilterFlags(); | ||||
|     } | ||||
|     this.filtersAvailable = filtersAvailable; | ||||
|     if (this.scenes[block]) { | ||||
|       this.clearUpdateQueue(block); | ||||
|       this.scenes[block].setup(transactions, sort); | ||||
|       this.readyNextFrame = true; | ||||
|       this.start(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   enter(block: number, transactions: TransactionStripped[], direction: string): void { | ||||
|     if (this.scenes[block]) { | ||||
|       this.clearUpdateQueue(block); | ||||
|       this.scenes[block].enter(transactions, direction); | ||||
|       this.start(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   exit(block: number, direction: string): void { | ||||
|     if (this.scenes[block]) { | ||||
|       this.clearUpdateQueue(block); | ||||
|       this.scenes[block].exit(direction); | ||||
|       this.start(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   replace(block: number, transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void { | ||||
|     if (this.scenes[block]) { | ||||
|       this.clearUpdateQueue(block); | ||||
|       this.scenes[block].replace(transactions || [], direction, sort, startTime); | ||||
|       this.start(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // collates deferred updates into a set of consistent pending changes
 | ||||
|   queueUpdate(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void { | ||||
|     for (const tx of add) { | ||||
|       this.pendingUpdates[block].add[tx.txid] = tx; | ||||
|       delete this.pendingUpdates[block].remove[tx.txid]; | ||||
|       delete this.pendingUpdates[block].change[tx.txid]; | ||||
|     } | ||||
|     for (const txid of remove) { | ||||
|       delete this.pendingUpdates[block].add[txid]; | ||||
|       this.pendingUpdates[block].remove[txid] = txid; | ||||
|       delete this.pendingUpdates[block].change[txid]; | ||||
|     } | ||||
|     for (const tx of change) { | ||||
|       if (this.pendingUpdates[block].add[tx.txid]) { | ||||
|         this.pendingUpdates[block].add[tx.txid].rate = tx.rate; | ||||
|         this.pendingUpdates[block].add[tx.txid].acc = tx.acc; | ||||
|       } else { | ||||
|         this.pendingUpdates[block].change[tx.txid] = tx; | ||||
|       } | ||||
|     } | ||||
|     this.pendingUpdates[block].direction = direction; | ||||
|     this.pendingUpdates[block].count++; | ||||
|   } | ||||
| 
 | ||||
|   deferredUpdate(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void { | ||||
|     this.queueUpdate(block, add, remove, change, direction); | ||||
|     this.applyQueuedUpdates(); | ||||
|   } | ||||
| 
 | ||||
|   applyQueuedUpdates(): void { | ||||
|     for (const [index, pendingUpdate] of this.pendingUpdates.entries()) { | ||||
|       if (pendingUpdate.count && performance.now() > (this.lastUpdate + this.animationDuration)) { | ||||
|         this.applyUpdate(index, Object.values(pendingUpdate.add), Object.values(pendingUpdate.remove), Object.values(pendingUpdate.change), pendingUpdate.direction); | ||||
|       } | ||||
|       this.clearUpdateQueue(index); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   clearUpdateQueue(block: number): void { | ||||
|     this.pendingUpdates[block] = { | ||||
|       count: 0, | ||||
|       add: {}, | ||||
|       remove: {}, | ||||
|       change: {}, | ||||
|     }; | ||||
|     this.lastUpdate = performance.now(); | ||||
|   } | ||||
| 
 | ||||
|   update(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void { | ||||
|     // merge any pending changes into this update
 | ||||
|     this.queueUpdate(block, add, remove, change, direction); | ||||
|     this.applyUpdate(block,Object.values(this.pendingUpdates[block].add), Object.values(this.pendingUpdates[block].remove), Object.values(this.pendingUpdates[block].change), direction, resetLayout); | ||||
|     this.clearUpdateQueue(block); | ||||
|   } | ||||
| 
 | ||||
|   applyUpdate(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void { | ||||
|     if (this.scenes[block]) { | ||||
|       add = add.filter(tx => !this.scenes[block].txs[tx.txid]); | ||||
|       remove = remove.filter(txid => this.scenes[block].txs[txid]); | ||||
|       change = change.filter(tx => this.scenes[block].txs[tx.txid]); | ||||
| 
 | ||||
|       if (this.gradientMode === 'age') { | ||||
|         this.scenes[block].updateAllColors(); | ||||
|       } | ||||
|       this.scenes[block].update(add, remove, change, direction, resetLayout); | ||||
|       this.start(); | ||||
|       this.lastUpdate = performance.now(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   initCanvas(): void { | ||||
|     if (!this.canvas || !this.gl) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     this.gl.clearColor(0.0, 0.0, 0.0, 0.0); | ||||
|     this.gl.clear(this.gl.COLOR_BUFFER_BIT); | ||||
| 
 | ||||
|     const shaderSet = [ | ||||
|       { | ||||
|         type: this.gl.VERTEX_SHADER, | ||||
|         src: vertShaderSrc | ||||
|       }, | ||||
|       { | ||||
|         type: this.gl.FRAGMENT_SHADER, | ||||
|         src: fragShaderSrc | ||||
|       } | ||||
|     ]; | ||||
| 
 | ||||
|     this.shaderProgram = this.buildShaderProgram(shaderSet); | ||||
| 
 | ||||
|     this.gl.useProgram(this.shaderProgram); | ||||
| 
 | ||||
|     // Set up alpha blending
 | ||||
|     this.gl.enable(this.gl.BLEND); | ||||
|     this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA); | ||||
| 
 | ||||
|     const glBuffer = this.gl.createBuffer(); | ||||
|     this.gl.bindBuffer(this.gl.ARRAY_BUFFER, glBuffer); | ||||
| 
 | ||||
|     /* SET UP SHADER ATTRIBUTES */ | ||||
|     Object.keys(attribs).forEach((key, i) => { | ||||
|       attribs[key].pointer = this.gl.getAttribLocation(this.shaderProgram, key); | ||||
|       this.gl.enableVertexAttribArray(attribs[key].pointer); | ||||
|     }); | ||||
| 
 | ||||
|     this.start(); | ||||
|   } | ||||
| 
 | ||||
|   handleContextLost(event): void { | ||||
|     event.preventDefault(); | ||||
|     cancelAnimationFrame(this.animationFrameRequest); | ||||
|     this.animationFrameRequest = null; | ||||
|     this.running = false; | ||||
|     this.gl = null; | ||||
|   } | ||||
| 
 | ||||
|   handleContextRestored(event): void { | ||||
|     if (this.canvas?.nativeElement) { | ||||
|       this.gl = this.canvas.nativeElement.getContext('webgl'); | ||||
|       if (this.gl) { | ||||
|         this.initCanvas(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('window:resize', ['$event']) | ||||
|   resizeCanvas(): void { | ||||
|     if (this.canvas) { | ||||
|       this.cssWidth = this.canvas.nativeElement.offsetParent.clientWidth; | ||||
|       this.cssHeight = this.canvas.nativeElement.offsetParent.clientHeight; | ||||
|       this.displayWidth = window.devicePixelRatio * this.cssWidth; | ||||
|       this.displayHeight = window.devicePixelRatio * this.cssHeight; | ||||
|       this.canvas.nativeElement.width = this.displayWidth; | ||||
|       this.canvas.nativeElement.height = this.displayHeight; | ||||
|       if (this.gl) { | ||||
|         this.gl.viewport(0, 0, this.displayWidth, this.displayHeight); | ||||
|       } | ||||
|       for (let i = 0; i < this.scenes.length; i++) { | ||||
|         const blocksPerRow = Math.floor(this.displayWidth / this.blockWidth); | ||||
|         const x = (i % blocksPerRow) * this.blockWidth; | ||||
|         const row = Math.floor(i / blocksPerRow); | ||||
|         const y = this.displayHeight - ((row + 1) * this.blockWidth); | ||||
|         if (this.scenes[i]) { | ||||
|           this.scenes[i].resize({ x, y, width: this.blockWidth, height: this.blockWidth, animate: false }); | ||||
|           this.start(); | ||||
|         } else { | ||||
|           this.scenes[i] = new BlockScene({ x, y, width: this.blockWidth, height: this.blockWidth, resolution: this.resolution, | ||||
|             blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, theme: this.themeService, | ||||
|             highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: 0, | ||||
|           colorFunction: this.getColorFunction() }); | ||||
|           this.start(); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   compileShader(src, type): WebGLShader { | ||||
|     if (!this.gl) { | ||||
|       return; | ||||
|     } | ||||
|     const shader = this.gl.createShader(type); | ||||
| 
 | ||||
|     this.gl.shaderSource(shader, src); | ||||
|     this.gl.compileShader(shader); | ||||
| 
 | ||||
|     if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) { | ||||
|       console.log(`Error compiling ${type === this.gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader:`); | ||||
|       console.log(this.gl.getShaderInfoLog(shader)); | ||||
|     } | ||||
|     return shader; | ||||
|   } | ||||
| 
 | ||||
|   buildShaderProgram(shaderInfo): WebGLProgram { | ||||
|     if (!this.gl) { | ||||
|       return; | ||||
|     } | ||||
|     const program = this.gl.createProgram(); | ||||
| 
 | ||||
|     shaderInfo.forEach((desc) => { | ||||
|       const shader = this.compileShader(desc.src, desc.type); | ||||
|       if (shader) { | ||||
|         this.gl.attachShader(program, shader); | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     this.gl.linkProgram(program); | ||||
| 
 | ||||
|     if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) { | ||||
|       console.log('Error linking shader program:'); | ||||
|       console.log(this.gl.getProgramInfoLog(program)); | ||||
|     } | ||||
| 
 | ||||
|     return program; | ||||
|   } | ||||
| 
 | ||||
|   start(): void { | ||||
|     this.running = true; | ||||
|     this.ngZone.runOutsideAngular(() => this.doRun()); | ||||
|   } | ||||
| 
 | ||||
|   doRun(): void { | ||||
|     if (this.animationFrameRequest) { | ||||
|       cancelAnimationFrame(this.animationFrameRequest); | ||||
|     } | ||||
|     this.animationFrameRequest = requestAnimationFrame(() => this.run()); | ||||
|   } | ||||
| 
 | ||||
|   run(now?: DOMHighResTimeStamp): void { | ||||
|     if (!now) { | ||||
|       now = performance.now(); | ||||
|     } | ||||
|     this.applyQueuedUpdates(); | ||||
|     // skip re-render if there's no change to the scene
 | ||||
|     if (this.scenes.length && this.gl) { | ||||
|       /* SET UP SHADER UNIFORMS */ | ||||
|       // screen dimensions
 | ||||
|       this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight); | ||||
|       // frame timestamp
 | ||||
|       this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, 'now'), now); | ||||
| 
 | ||||
|       if (this.vertexArray.dirty) { | ||||
|         /* SET UP SHADER ATTRIBUTES */ | ||||
|         Object.keys(attribs).forEach((key, i) => { | ||||
|           this.gl.vertexAttribPointer(attribs[key].pointer, | ||||
|           attribs[key].count,  // number of primitives in this attribute
 | ||||
|           this.gl[attribs[key].type],  // type of primitive in this attribute (e.g. gl.FLOAT)
 | ||||
|           false, // never normalised
 | ||||
|           stride,   // distance between values of the same attribute
 | ||||
|           attribs[key].offset);  // offset of the first value
 | ||||
|         }); | ||||
| 
 | ||||
|         const pointArray = this.vertexArray.getVertexData(); | ||||
| 
 | ||||
|         if (pointArray.length) { | ||||
|           this.gl.bufferData(this.gl.ARRAY_BUFFER, pointArray, this.gl.DYNAMIC_DRAW); | ||||
|           this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize); | ||||
|         } | ||||
|         this.vertexArray.dirty = false; | ||||
|       } else { | ||||
|         const pointArray = this.vertexArray.getVertexData(); | ||||
|         if (pointArray.length) { | ||||
|           this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (this.readyNextFrame) { | ||||
|         this.readyNextFrame = false; | ||||
|         this.readyEvent.emit(); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     /* LOOP */ | ||||
|     if (this.running && this.scenes.length && now <= (this.scenes.reduce((max, scene) => scene.animateUntil > max ? scene.animateUntil : max, 0) + 500)) { | ||||
|       this.doRun(); | ||||
|     } else { | ||||
|       if (this.animationHeartBeat) { | ||||
|         clearTimeout(this.animationHeartBeat); | ||||
|       } | ||||
|       this.animationHeartBeat = window.setTimeout(() => { | ||||
|         this.start(); | ||||
|       }, 1000); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setHighlightingEnabled(enabled: boolean): void { | ||||
|     for (const scene of this.scenes) { | ||||
|       scene.setHighlighting(enabled); | ||||
|     } | ||||
|     this.start(); | ||||
|   } | ||||
| 
 | ||||
|   getColorFunction(): ((tx: TxView) => Color) { | ||||
|     if (this.overrideColors) { | ||||
|       return this.overrideColors; | ||||
|     } else if (this.filterFlags) { | ||||
|       return this.getFilterColorFunction(this.filterFlags, this.gradientMode); | ||||
|     } else if (this.activeFilterFlags) { | ||||
|       return this.getFilterColorFunction(this.activeFilterFlags, this.gradientMode); | ||||
|     } else { | ||||
|       return this.getFilterColorFunction(0n, this.gradientMode); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   getFilterColorFunction(flags: bigint, gradient: 'fee' | 'age'): ((tx: TxView) => Color) { | ||||
|     return (tx: TxView) => { | ||||
|       if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) { | ||||
|         if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') { | ||||
|           return (gradient === 'age') ? ageColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000)) : defaultColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000)); | ||||
|         } else { | ||||
|           return (gradient === 'age') ? ageColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000)) : contrastColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000)); | ||||
|         } | ||||
|       } else { | ||||
|         if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') { | ||||
|           return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : defaultColorFunction( | ||||
|             tx, | ||||
|             defaultColors.unmatchedfee, | ||||
|             unmatchedAuditColors, | ||||
|             this.relativeTime || (Date.now() / 1000) | ||||
|           ); | ||||
|         } else { | ||||
|           return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : contrastColorFunction( | ||||
|             tx, | ||||
|             contrastColors.unmatchedfee, | ||||
|             unmatchedContrastAuditColors, | ||||
|             this.relativeTime || (Date.now() / 1000) | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     }; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // WebGL shader attributes
 | ||||
| const attribs = { | ||||
|   offset: { type: 'FLOAT', count: 2, pointer: null, offset: 0 }, | ||||
|   posX: { type: 'FLOAT', count: 4, pointer: null, offset: 0 }, | ||||
|   posY: { type: 'FLOAT', count: 4, pointer: null, offset: 0 }, | ||||
|   posR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 }, | ||||
|   colR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 }, | ||||
|   colG: { type: 'FLOAT', count: 4, pointer: null, offset: 0 }, | ||||
|   colB: { type: 'FLOAT', count: 4, pointer: null, offset: 0 }, | ||||
|   colA: { type: 'FLOAT', count: 4, pointer: null, offset: 0 } | ||||
| }; | ||||
| // Calculate the number of bytes per vertex based on specified attributes
 | ||||
| const stride = Object.values(attribs).reduce((total, attrib) => { | ||||
|   return total + (attrib.count * 4); | ||||
| }, 0); | ||||
| // Calculate vertex attribute offsets
 | ||||
| for (let i = 0, offset = 0; i < Object.keys(attribs).length; i++) { | ||||
|   const attrib = Object.values(attribs)[i]; | ||||
|   attrib.offset = offset; | ||||
|   offset += (attrib.count * 4); | ||||
| } | ||||
| 
 | ||||
| const vertShaderSrc = ` | ||||
| varying lowp vec4 vColor; | ||||
| 
 | ||||
| // each attribute contains [x: startValue, y: endValue, z: startTime, w: rate]
 | ||||
| // shader interpolates between start and end values at the given rate, from the given time
 | ||||
| 
 | ||||
| attribute vec2 offset; | ||||
| attribute vec4 posX; | ||||
| attribute vec4 posY; | ||||
| attribute vec4 posR; | ||||
| attribute vec4 colR; | ||||
| attribute vec4 colG; | ||||
| attribute vec4 colB; | ||||
| attribute vec4 colA; | ||||
| 
 | ||||
| uniform vec2 screenSize; | ||||
| uniform float now; | ||||
| 
 | ||||
| float smootherstep(float x) { | ||||
|   x = clamp(x, 0.0, 1.0); | ||||
|   float ix = 1.0 - x; | ||||
|   x = x * x; | ||||
|   return x / (x + ix * ix); | ||||
| } | ||||
| 
 | ||||
| float interpolateAttribute(vec4 attr) { | ||||
|   float d = (now - attr.z) * attr.w; | ||||
|   float delta = smootherstep(d); | ||||
|   return mix(attr.x, attr.y, delta); | ||||
| } | ||||
| 
 | ||||
| void main() { | ||||
|   vec4 screenTransform = vec4(2.0 / screenSize.x, 2.0 / screenSize.y, -1.0, -1.0); | ||||
|   // vec4 screenTransform = vec4(1.0 / screenSize.x, 1.0 / screenSize.y, -0.5, -0.5);
 | ||||
| 
 | ||||
|   float radius = interpolateAttribute(posR); | ||||
|   vec2 position = vec2(interpolateAttribute(posX), interpolateAttribute(posY)) + (radius * offset); | ||||
| 
 | ||||
|   gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, 1.0, 1.0); | ||||
| 
 | ||||
|   float red = interpolateAttribute(colR); | ||||
|   float green = interpolateAttribute(colG); | ||||
|   float blue = interpolateAttribute(colB); | ||||
|   float alpha = interpolateAttribute(colA); | ||||
| 
 | ||||
|   vColor = vec4(red, green, blue, alpha); | ||||
| } | ||||
| `;
 | ||||
| 
 | ||||
| const fragShaderSrc = ` | ||||
| varying lowp vec4 vColor; | ||||
| 
 | ||||
| void main() { | ||||
|   gl_FragColor = vColor; | ||||
|   // premultiply alpha
 | ||||
|   gl_FragColor.rgb *= gl_FragColor.a; | ||||
| } | ||||
| `;
 | ||||
| @ -1,10 +1,12 @@ | ||||
| <div class="blocks" [class.wrap]="wrapBlocks"> | ||||
| <!-- <div class="blocks" [class.wrap]="wrapBlocks"> | ||||
|   <ng-container *ngFor="let i of blockIndices"> | ||||
|     <div class="block-wrapper" [style]="wrapperStyle"> | ||||
|       <div class="block-container" [style]="containerStyle"> | ||||
|         <app-block-overview-graph | ||||
|       <div class="block-container" [style]="containerStyle"> --> | ||||
|         <app-block-overview-multi | ||||
|           #blockGraph | ||||
|           [isLoading]="false" | ||||
|           [numBlocks]="8" | ||||
|           [blockWidth]="blockWidth" | ||||
|           [resolution]="resolution" | ||||
|           [blockLimit]="stateService.blockVSize" | ||||
|           [orientation]="'top'" | ||||
| @ -12,14 +14,12 @@ | ||||
|           [animationDuration]="animationDuration" | ||||
|           [animationOffset]="animationOffset" | ||||
|           [disableSpinner]="true" | ||||
|           [relativeTime]="blockInfo[i]?.timestamp" | ||||
|           (txClickEvent)="onTxClick($event)" | ||||
|         ></app-block-overview-graph> | ||||
|         <div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange> | ||||
|         ></app-block-overview-multi> | ||||
|         <!-- <div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange> | ||||
|           <h1 class="height">{{ blockInfo[i].height }}</h1> | ||||
|           <h2 class="mined-by">by {{ blockInfo[i].extras.pool.name || 'Unknown' }}</h2> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </ng-container> | ||||
| </div> | ||||
| </div> --> | ||||
| @ -1,16 +1,15 @@ | ||||
| import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core'; | ||||
| import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; | ||||
| import { ActivatedRoute, Router } from '@angular/router'; | ||||
| import { catchError, startWith } from 'rxjs/operators'; | ||||
| import { catchError } from 'rxjs/operators'; | ||||
| import { Subject, Subscription, of } from 'rxjs'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { WebsocketService } from '../../services/websocket.service'; | ||||
| import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; | ||||
| import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface'; | ||||
| import { ApiService } from '../../services/api.service'; | ||||
| import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component'; | ||||
| import { detectWebGL } from '../../shared/graphs.utils'; | ||||
| import { animate, style, transition, trigger } from '@angular/animations'; | ||||
| import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe'; | ||||
| import { BlockOverviewMultiComponent } from '../block-overview-multi/block-overview-multi.component'; | ||||
| 
 | ||||
| function bestFitResolution(min, max, n): number { | ||||
|   const target = (min + max) / 2; | ||||
| @ -65,7 +64,7 @@ export class EightBlocksComponent implements OnInit, OnDestroy { | ||||
|   autofit: boolean = false; | ||||
|   padding: number = 0; | ||||
|   wrapBlocks: boolean = false; | ||||
|   blockWidth: number = 1080; | ||||
|   blockWidth: number = 360; | ||||
|   animationDuration: number = 2000; | ||||
|   animationOffset: number = 0; | ||||
|   stagger: number = 0; | ||||
| @ -85,7 +84,7 @@ export class EightBlocksComponent implements OnInit, OnDestroy { | ||||
|   containerStyle = {}; | ||||
|   resolution: number = 86; | ||||
| 
 | ||||
|   @ViewChildren('blockGraph') blockGraphs: QueryList<BlockOverviewGraphComponent>; | ||||
|   @ViewChild('blockGraph') blockGraph: BlockOverviewMultiComponent; | ||||
| 
 | ||||
|   constructor( | ||||
|     private route: ActivatedRoute, | ||||
| @ -149,9 +148,7 @@ export class EightBlocksComponent implements OnInit, OnDestroy { | ||||
|   } | ||||
| 
 | ||||
|   ngAfterViewInit(): void { | ||||
|     this.graphChangeSubscription = this.blockGraphs.changes.pipe(startWith(null)).subscribe(() => { | ||||
|       this.setupBlockGraphs(); | ||||
|     }); | ||||
|     this.setupBlockGraphs(); | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
| @ -208,10 +205,10 @@ export class EightBlocksComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|   updateBlockGraphs(blocks): void { | ||||
|     const startTime = performance.now() + 1000 - (this.stagger < 0 ? this.stagger * 8 : 0); | ||||
|     if (this.blockGraphs) { | ||||
|       this.blockGraphs.forEach((graph, index) => { | ||||
|         graph.replace(this.strippedTransactions[blocks?.[index]?.height] || [], 'right', false, startTime + (this.stagger * index)); | ||||
|       }); | ||||
|     if (this.blockGraph) { | ||||
|       for (let i = 0; i < this.numBlocks; i++) { | ||||
|         this.blockGraph.replace(i, this.strippedTransactions[blocks?.[i]?.height] || [], 'right', false, startTime + (this.stagger * i)); | ||||
|       } | ||||
|     } | ||||
|     this.showInfo = false; | ||||
|     setTimeout(() => { | ||||
| @ -226,28 +223,11 @@ export class EightBlocksComponent implements OnInit, OnDestroy { | ||||
|   } | ||||
| 
 | ||||
|   setupBlockGraphs(): void { | ||||
|     if (this.blockGraphs) { | ||||
|       this.blockGraphs.forEach((graph, index) => { | ||||
|         graph.destroy(); | ||||
|         graph.setup(this.strippedTransactions[this.latestBlocks?.[index]?.height] || []); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void { | ||||
|     const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`); | ||||
|     if (!event.keyModifier) { | ||||
|       this.router.navigate([url]); | ||||
|     } else { | ||||
|       window.open(url, '_blank'); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   onTxHover(txid: string): void { | ||||
|     if (txid && txid.length) { | ||||
|       this.hoverTx = txid; | ||||
|     } else { | ||||
|       this.hoverTx = null; | ||||
|     if (this.blockGraph) { | ||||
|       for (let i = 0; i < this.numBlocks; i++) { | ||||
|         this.blockGraph.destroy(i); | ||||
|         this.blockGraph.setup(i, this.strippedTransactions[this.latestBlocks?.[i]?.height] || []); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -48,6 +48,7 @@ import { CapAddressPipe } from './pipes/cap-address-pipe/cap-address-pipe'; | ||||
| import { StartComponent } from '../components/start/start.component'; | ||||
| import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component'; | ||||
| import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component'; | ||||
| import { BlockOverviewMultiComponent } from '../components/block-overview-multi/block-overview-multi.component'; | ||||
| import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component'; | ||||
| import { BlockFiltersComponent } from '../components/block-filters/block-filters.component'; | ||||
| import { AddressGroupComponent } from '../components/address-group/address-group.component'; | ||||
| @ -164,6 +165,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | ||||
|     PreviewTitleComponent, | ||||
|     StartComponent, | ||||
|     BlockOverviewGraphComponent, | ||||
|     BlockOverviewMultiComponent, | ||||
|     BlockOverviewTooltipComponent, | ||||
|     BlockFiltersComponent, | ||||
|     TransactionsListComponent, | ||||
| @ -308,6 +310,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | ||||
|     AmountComponent, | ||||
|     StartComponent, | ||||
|     BlockOverviewGraphComponent, | ||||
|     BlockOverviewMultiComponent, | ||||
|     BlockOverviewTooltipComponent, | ||||
|     BlockFiltersComponent, | ||||
|     TransactionsListComponent, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user