TV full screen view.

This commit is contained in:
Simon Lindh 2019-07-27 18:43:17 +03:00
parent b561137962
commit 85c4a3480b
14 changed files with 339 additions and 130 deletions

View File

@ -3,8 +3,14 @@ import { Routes, RouterModule } from '@angular/router';
import { BlockchainComponent } from './blockchain/blockchain.component'; import { BlockchainComponent } from './blockchain/blockchain.component';
import { AboutComponent } from './about/about.component'; import { AboutComponent } from './about/about.component';
import { StatisticsComponent } from './statistics/statistics.component'; import { StatisticsComponent } from './statistics/statistics.component';
import { TelevisionComponent } from './television/television.component';
import { MasterPageComponent } from './master-page/master-page.component';
const routes: Routes = [ const routes: Routes = [
{
path: '',
component: MasterPageComponent,
children: [
{ {
path: '', path: '',
children: [], children: [],
@ -28,6 +34,12 @@ const routes: Routes = [
path: 'graphs', path: 'graphs',
component: StatisticsComponent, component: StatisticsComponent,
}, },
],
},
{
path: 'tv',
component: TelevisionComponent,
},
{ {
path: '**', path: '**',
redirectTo: '' redirectTo: ''

View File

@ -1,32 +1 @@
<header>
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" routerLink="/"><img src="/assets/mempool-space-logo.png" width="180" class="logo"> <span class="badge badge-warning" style="margin-left: 10px;" *ngIf="isOffline">Offline</span></a>
<button class="navbar-toggler" type="button" (click)="collapse()" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse" id="navbarCollapse" [ngClass]="{'show': navCollapsed}">
<ul class="navbar-nav mr-auto">
<li class="nav-item" routerLinkActive="active" [ngClass]="{'active': txActive.isActive}" [routerLinkActiveOptions]="{exact: true}">
<a class="nav-link" routerLink="/" (click)="collapse()">Blocks</a>
<a class="nav-link" routerLink="/tx" routerLinkActive #txActive="routerLinkActive" style="display: none;"></a>
</li>
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/graphs" (click)="collapse()">Graphs</a>
</li>
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/about" (click)="collapse()">About</a>
</li>
</ul>
<form [formGroup]="searchForm" class="form-inline mt-2 mt-md-0 mr-4" (submit)="searchForm.valid && search()" novalidate>
<input formControlName="txId" required style="width: 300px;" class="form-control mr-sm-2" type="text" placeholder="Track transaction (TXID)" aria-label="Search">
<button class="btn btn-primary my-2 my-sm-0" type="submit">Track</button>
</form>
</div>
</nav>
</header>
<br />
<router-outlet></router-outlet> <router-outlet></router-outlet>

View File

@ -1,28 +0,0 @@
li.nav-item.active {
background-color: #653b9c;
}
li.nav-item {
padding: 10px;
}
.navbar {
z-index: 100;
}
@media (min-width: 768px) {
.navbar {
padding: 0rem 1rem;
}
li.nav-item {
padding: 20px;
}
}
.logo {
margin-left: 40px;
}
li.nav-item a {
color: #ffffff;
}

View File

@ -1,52 +1,10 @@
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { MemPoolService } from './services/mem-pool.service';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'] styleUrls: ['./app.component.scss']
}) })
export class AppComponent implements OnInit { export class AppComponent {
navCollapsed = false; constructor() { }
isOffline = false;
searchForm: FormGroup;
constructor(
private memPoolService: MemPoolService,
private router: Router,
private formBuilder: FormBuilder,
) { }
ngOnInit() {
this.searchForm = this.formBuilder.group({
txId: ['', Validators.pattern('^[a-fA-F0-9]{64}$')],
});
this.memPoolService.isOffline$
.subscribe((state) => {
this.isOffline = state;
});
}
collapse(): void {
this.navCollapsed = !this.navCollapsed;
}
search() {
const txId = this.searchForm.value.txId;
if (txId) {
if (window.location.pathname === '/' || window.location.pathname.substr(0, 4) === '/tx/') {
window.history.pushState({}, '', `/tx/${txId}`);
} else {
this.router.navigate(['/tx/', txId]);
}
this.memPoolService.txIdSearch$.next(txId);
this.searchForm.setValue({
txId: '',
});
this.collapse();
}
}
} }

View File

@ -14,8 +14,11 @@ import { ReactiveFormsModule } from '@angular/forms';
import { BlockModalComponent } from './blockchain-blocks/block-modal/block-modal.component'; import { BlockModalComponent } from './blockchain-blocks/block-modal/block-modal.component';
import { StatisticsComponent } from './statistics/statistics.component'; import { StatisticsComponent } from './statistics/statistics.component';
import { ProjectedBlockModalComponent } from './blockchain-projected-blocks/projected-block-modal/projected-block-modal.component'; import { ProjectedBlockModalComponent } from './blockchain-projected-blocks/projected-block-modal/projected-block-modal.component';
import { TelevisionComponent } from './television/television.component';
import { BlockchainBlocksComponent } from './blockchain-blocks/blockchain-blocks.component'; import { BlockchainBlocksComponent } from './blockchain-blocks/blockchain-blocks.component';
import { BlockchainProjectedBlocksComponent } from './blockchain-projected-blocks/blockchain-projected-blocks.component'; import { BlockchainProjectedBlocksComponent } from './blockchain-projected-blocks/blockchain-projected-blocks.component';
import { ApiService } from './services/api.service';
import { MasterPageComponent } from './master-page/master-page.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -27,8 +30,10 @@ import { BlockchainProjectedBlocksComponent } from './blockchain-projected-block
TxBubbleComponent, TxBubbleComponent,
BlockModalComponent, BlockModalComponent,
ProjectedBlockModalComponent, ProjectedBlockModalComponent,
TelevisionComponent,
BlockchainBlocksComponent, BlockchainBlocksComponent,
BlockchainProjectedBlocksComponent, BlockchainProjectedBlocksComponent,
MasterPageComponent,
], ],
imports: [ imports: [
ReactiveFormsModule, ReactiveFormsModule,
@ -38,6 +43,7 @@ import { BlockchainProjectedBlocksComponent } from './blockchain-projected-block
SharedModule, SharedModule,
], ],
providers: [ providers: [
ApiService,
MemPoolService, MemPoolService,
], ],
entryComponents: [ entryComponents: [

View File

@ -21,7 +21,10 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
ngOnInit() { ngOnInit() {
this.blocksSubscription = this.memPoolService.blocks$ this.blocksSubscription = this.memPoolService.blocks$
.subscribe((block) => this.blocks.unshift(block)); .subscribe((block) => {
this.blocks.unshift(block);
this.blocks = this.blocks.slice(0, 8);
});
} }
ngOnDestroy() { ngOnDestroy() {

View File

@ -0,0 +1,32 @@
<header>
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" routerLink="/"><img src="/assets/mempool-space-logo.png" width="180" class="logo"> <span class="badge badge-warning" style="margin-left: 10px;" *ngIf="isOffline">Offline</span></a>
<button class="navbar-toggler" type="button" (click)="collapse()" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse" id="navbarCollapse" [ngClass]="{'show': navCollapsed}">
<ul class="navbar-nav mr-auto">
<li class="nav-item" routerLinkActive="active" [ngClass]="{'active': txActive.isActive}" [routerLinkActiveOptions]="{exact: true}">
<a class="nav-link" routerLink="/" (click)="collapse()">Blocks</a>
<a class="nav-link" routerLink="/tx" routerLinkActive #txActive="routerLinkActive" style="display: none;"></a>
</li>
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/graphs" (click)="collapse()">Graphs</a>
</li>
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/about" (click)="collapse()">About</a>
</li>
</ul>
<form [formGroup]="searchForm" class="form-inline mt-2 mt-md-0 mr-4" (submit)="searchForm.valid && search()" novalidate>
<input formControlName="txId" required style="width: 300px;" class="form-control mr-sm-2" type="text" placeholder="Track transaction (TXID)" aria-label="Search">
<button class="btn btn-primary my-2 my-sm-0" type="submit">Track</button>
</form>
</div>
</nav>
</header>
<br />
<router-outlet></router-outlet>

View File

@ -0,0 +1,28 @@
li.nav-item.active {
background-color: #653b9c;
}
li.nav-item {
padding: 10px;
}
.navbar {
z-index: 100;
}
@media (min-width: 768px) {
.navbar {
padding: 0rem 1rem;
}
li.nav-item {
padding: 20px;
}
}
.logo {
margin-left: 40px;
}
li.nav-item a {
color: #ffffff;
}

View File

@ -0,0 +1,54 @@
import { Component, OnInit } from '@angular/core';
import { MemPoolService } from '../services/mem-pool.service';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-master-page',
templateUrl: './master-page.component.html',
styleUrls: ['./master-page.component.scss']
})
export class MasterPageComponent implements OnInit {
navCollapsed = false;
isOffline = false;
searchForm: FormGroup;
constructor(
private memPoolService: MemPoolService,
private router: Router,
private formBuilder: FormBuilder,
) { }
ngOnInit() {
this.searchForm = this.formBuilder.group({
txId: ['', Validators.pattern('^[a-fA-F0-9]{64}$')],
});
this.memPoolService.isOffline$
.subscribe((state) => {
this.isOffline = state;
});
}
collapse(): void {
this.navCollapsed = !this.navCollapsed;
}
search() {
const txId = this.searchForm.value.txId;
if (txId) {
if (window.location.pathname === '/' || window.location.pathname.substr(0, 4) === '/tx/') {
window.history.pushState({}, '', `/tx/${txId}`);
} else {
this.router.navigate(['/tx/', txId]);
}
this.memPoolService.txIdSearch$.next(txId);
this.searchForm.setValue({
txId: '',
});
this.collapse();
}
}
}

View File

@ -215,13 +215,6 @@ export class StatisticsComponent implements OnInit {
}; };
} }
getTimeToNextTenMinutes(): number {
const now = new Date();
const nextInterval = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(),
Math.floor(now.getMinutes() / 10) * 10 + 10, 0, 0);
return nextInterval.getTime() - now.getTime();
}
generateArray(mempoolStats: IMempoolStats[]) { generateArray(mempoolStats: IMempoolStats[]) {
const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000]; 250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];

View File

@ -0,0 +1,25 @@
<div id="tv-wrapper">
<div *ngIf="loading" class="text-center">
<div class="spinner-border text-light"></div>
</div>
<div class="chart-holder" *ngIf="mempoolVsizeFeesData">
<app-chartist
[data]="mempoolVsizeFeesData"
[type]="'Line'"
[options]="mempoolVsizeFeesOptions">
</app-chartist>
</div>
<div class="text-center" class="blockchain-wrapper">
<div class="position-container">
<app-blockchain-projected-blocks></app-blockchain-projected-blocks>
<app-blockchain-blocks></app-blockchain-blocks>
<div id="divider"></div>
</div>
</div>
</div>

View File

@ -0,0 +1,35 @@
#tv-wrapper {
height: 100%;
margin: 10px;
margin-top: 20px;
}
.blockchain-wrapper {
overflow: hidden;
}
.position-container {
position: absolute;
left: 50%;
bottom: 150px;
}
#divider {
width: 3px;
height: 175px;
left: 0;
top: -40px;
background-image: url('/assets/divider-new.png');
background-repeat: repeat-y;
position: absolute;
}
#divider > img {
position: absolute;
left: -100px;
top: -28px;
}
.chart-holder {
height: calc(100% - 220px);
}

View File

@ -0,0 +1,118 @@
import { Component, OnInit, LOCALE_ID, Inject } from '@angular/core';
import { ApiService } from '../services/api.service';
import { formatDate } from '@angular/common';
import { BytesPipe } from '../shared/pipes/bytes-pipe/bytes.pipe';
import * as Chartist from 'chartist';
import { IMempoolStats } from '../blockchain/interfaces';
import { MemPoolService } from '../services/mem-pool.service';
@Component({
selector: 'app-television',
templateUrl: './television.component.html',
styleUrls: ['./television.component.scss']
})
export class TelevisionComponent implements OnInit {
loading = true;
mempoolStats: IMempoolStats[] = [];
mempoolVsizeFeesData: any;
mempoolVsizeFeesOptions: any;
constructor(
private apiService: ApiService,
@Inject(LOCALE_ID) private locale: string,
private bytesPipe: BytesPipe,
private memPoolService: MemPoolService,
) { }
ngOnInit() {
this.apiService.sendWebSocket({'action': 'want', data: ['projected-blocks', 'live-2h-chart']});
const labelInterpolationFnc = (value: any, index: any) => {
return index % 6 === 0 ? formatDate(value, 'HH:mm', this.locale) : null;
};
this.mempoolVsizeFeesOptions = {
showArea: true,
showLine: false,
fullWidth: true,
showPoint: false,
low: 0,
axisX: {
labelInterpolationFnc: labelInterpolationFnc,
offset: 40
},
axisY: {
labelInterpolationFnc: (value: number): any => {
return this.bytesPipe.transform(value);
},
offset: 50
},
plugins: [
Chartist.plugins.ctTargetLine({
value: 1000000
}),
]
};
this.apiService.list2HStatistics$()
.subscribe((mempoolStats) => {
this.mempoolStats = mempoolStats;
this.handleNewMempoolData(this.mempoolStats.concat([]));
this.loading = false;
});
this.memPoolService.live2Chart$
.subscribe((mempoolStats) => {
this.mempoolStats.unshift(mempoolStats);
this.mempoolStats = this.mempoolStats.slice(0, this.mempoolStats.length - 1);
this.handleNewMempoolData(this.mempoolStats.concat([]));
});
}
handleNewMempoolData(mempoolStats: IMempoolStats[]) {
mempoolStats.reverse();
const labels = mempoolStats.map(stats => stats.added);
const finalArrayVbyte = this.generateArray(mempoolStats);
// Remove the 0-1 fee vbyte since it's practially empty
finalArrayVbyte.shift();
this.mempoolVsizeFeesData = {
labels: labels,
series: finalArrayVbyte
};
}
generateArray(mempoolStats: IMempoolStats[]) {
const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
logFees.reverse();
const finalArray: number[][] = [];
let feesArray: number[] = [];
logFees.forEach((fee) => {
feesArray = [];
mempoolStats.forEach((stats) => {
// @ts-ignore
const theFee = stats['vsize_' + fee];
if (theFee) {
feesArray.push(parseInt(theFee, 10));
} else {
feesArray.push(0);
}
});
if (finalArray.length) {
feesArray = feesArray.map((value, i) => value + finalArray[finalArray.length - 1][i]);
}
finalArray.push(feesArray);
});
finalArray.reverse();
return finalArray;
}
}

View File

@ -66,6 +66,10 @@ body {
margin-bottom: 60px; margin-bottom: 60px;
} }
html, body {
height: 100%;
}
@media (min-width: 768px) { @media (min-width: 768px) {
body.disable-scroll { body.disable-scroll {
overflow: hidden; overflow: hidden;