Merge branch 'master' into mononaut/skeleton-fee
This commit is contained in:
@@ -107,22 +107,7 @@
|
||||
<span>Blockstream</span>
|
||||
</a>
|
||||
<a href="https://unchained.com/" target="_blank" title="Unchained">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" viewBox="0 0 216 216" class="image" style="enable-background:new 0 0 216 216;">
|
||||
<style type="text/css">
|
||||
.ucst0{fill:#002248;}
|
||||
.ucst1{opacity:0.5;fill:#FFFFFF;}
|
||||
.ucst2{fill:#FFFFFF;}
|
||||
.ucst3{opacity:0.75;fill:#FFFFFF;}
|
||||
</style>
|
||||
<rect class="ucst0" width="216" height="216"/>
|
||||
<g>
|
||||
<g>
|
||||
<path class="ucst1" d="M108,39.5V108l59.3,34.2V73.8L108,39.5z M126.9,95.4c0,2,1.1,3.8,2.8,4.8l27.9,16l0,10.8L125,108.2c-4.6-2.6-7.4-7.5-7.4-12.8l-0.1-22.7c0-1.9,0.5-3.7,1.4-5.3c0.9-1.5,2.2-2.9,3.8-3.8c3.3-1.9,7.2-1.9,10.5,0l24.5,14.2l-0.2,10.7l-29-16.8c-0.5-0.3-0.9-0.2-1.2,0c-0.3,0.2-0.6,0.5-0.6,1L126.9,95.4z"/>
|
||||
<path class="ucst2" d="M108,39.5L48.7,73.8v68.5L108,108V39.5z M99.7,93.1c0,5.3-2.8,10.2-7.4,12.8l-19.6,11.4c-1.7,1-3.5,1.4-5.3,1.5c-1.8,0-3.6-0.5-5.2-1.4c-3.3-1.9-5.3-5.3-5.3-9.1V80l9.4-5.2l-0.1,33.5c0,0.6,0.3,0.9,0.6,1c0.3,0.2,0.7,0.3,1.2,0l19.6-11.4c1.7-1,2.8-2.8,2.8-4.8L90.3,61l9.4-5.4L99.7,93.1z"/>
|
||||
<path class="ucst3" d="M108,108l-59.3,34.2l59.3,34.2l59.3-34.2L108,108z M133.8,152l-24.5,14.2l-9.2-5.5l29.1-16.7c0.5-0.3,0.6-0.7,0.6-1c0-0.3-0.1-0.7-0.6-1l-19.7-11.2c-1.7-1-3.8-1-5.5,0l-27.8,16.1l-9.4-5.4l32.6-18.7c4.6-2.6,10.2-2.6,14.8,0l19.7,11.2c1.7,0.9,3,2.3,3.9,3.9c0.9,1.5,1.4,3.3,1.4,5.2C139.1,146.7,137.1,150.1,133.8,152z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68"><defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/></svg>
|
||||
<span>Unchained</span>
|
||||
</a>
|
||||
<a href="https://gemini.com/" target="_blank" title="Gemini">
|
||||
@@ -408,33 +393,14 @@
|
||||
|
||||
<div class="footer-links">
|
||||
<a href="/3rdpartylicenses.txt">Third-party Licenses</a>
|
||||
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
|
||||
<div class="social-icons">
|
||||
<a target="_blank" href="https://github.com/mempool/mempool">
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="github" class="svg-inline--fa fa-github fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path fill="currentColor" d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path></svg>
|
||||
</a>
|
||||
<a target="_blank" href="https://twitter.com/mempool">
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="twitter" class="svg-inline--fa fa-twitter fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path></svg>
|
||||
</a>
|
||||
<a target="_blank" href="https://matrix.to/#/#mempool:bitcoin.kyoto">
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="matrix" class="svg-inline--fa fa-matrix fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1536 1792"><path fill="currentColor" d="M40.467 163.152v1465.696H145.92V1664H0V128h145.92v35.152zm450.757 464.64v74.14h2.069c19.79-28.356 43.717-50.215 71.483-65.575 27.765-15.656 59.963-23.336 96-23.336 34.56 0 66.165 6.795 94.818 20.086 28.652 13.293 50.216 37.22 65.28 70.893 16.246-23.926 38.4-45.194 66.166-63.507 27.766-18.314 60.848-27.472 98.954-27.472 28.948 0 55.828 3.545 80.64 10.635 24.812 7.088 45.785 18.314 63.508 33.968 17.722 15.656 31.31 35.742 41.354 60.85 9.747 25.107 14.768 55.236 14.768 90.683v366.573h-150.35V865.28c0-18.314-.59-35.741-2.068-51.987-1.476-16.247-5.316-30.426-11.52-42.24-6.499-12.112-15.656-21.563-28.062-28.653-12.405-7.088-29.242-10.634-50.214-10.634-21.268 0-38.4 4.135-51.397 12.112-12.997 8.27-23.336 18.608-30.72 31.901-7.386 12.997-12.407 27.765-14.77 44.602-2.363 16.542-3.84 33.379-3.84 50.216v305.133H692.971v-307.2c0-16.247-.294-32.197-1.18-48.149-.591-15.95-3.84-30.424-9.157-44.011-5.317-13.293-14.178-24.223-26.585-32.197-12.406-7.976-30.425-12.112-54.646-12.112-7.088 0-16.542 1.478-28.062 4.726-11.52 3.25-23.04 9.157-33.968 18.02-10.93 8.86-20.383 21.563-28.063 38.103-7.68 16.543-11.52 38.4-11.52 65.28v317.834H349.44V627.792zm1004.309 1001.056V163.152H1390.08V128H1536v1536h-145.92v-35.152z"/></svg>
|
||||
</a>
|
||||
<a target="_blank" href="https://youtube.com/@mempool">
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="youtube" class="svg-inline--fa fa-youtube fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z"/></svg>
|
||||
</a>
|
||||
<a target="_blank" href="https://bitcointv.com/c/mempool/videos" class="bitcointv">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" focusable="false" viewBox="0 0 440 440"><path d="M225.57,2.08l-.69-.45a4.22,4.22,0,0,0-5.72,1.23L182.33,46.09a4,4,0,0,0,.88,5.81l9.38,6.38L173.48,97.49a4.22,4.22,0,0,0,2.45,4.19s3.55.7,4.53-1l41.92-40.56a3.62,3.62,0,0,0-1.51-5.1l-10.55-6.12L227.44,6.79A4.26,4.26,0,0,0,225.57,2.08Z" fill="currentColor"></path><path d="M118.52,401.83c-62.51,0-113.37-51-113.37-113.67V214.68C5.15,152,56,101,118.52,101H342.08a24.82,24.82,0,0,1,24.76,24.83V377a24.81,24.81,0,0,1-24.76,24.82Z"></path><path d="M342.08,105.18a20.65,20.65,0,0,1,20.61,20.66V377a20.66,20.66,0,0,1-20.61,20.66H118.52C58.3,397.67,9.31,348.55,9.31,288.16V214.68c0-60.38,49-109.5,109.21-109.5H342.08m0-8.34H118.52C53.62,96.84,1,149.6,1,214.68v73.48C1,353.24,53.62,406,118.52,406H342.08A29,29,0,0,0,371,377V125.84a29,29,0,0,0-28.92-29Z" fill="currentColor"></path><path fill="currentColor" d="M344.69,346.23A25.84,25.84,0,1,0,335,369.87l-10.22-10.2a11.69,11.69,0,1,1,4.77-5.12l10.31,10.28A25.84,25.84,0,0,0,344.69,346.23Z"></path><path fill="currentColor" d="M315.82,257.61a25.67,25.67,0,0,0-12.53,5.22L315,274.49a9.58,9.58,0,0,1,2.11-.73A9.72,9.72,0,1,1,309.4,283a9.4,9.4,0,0,1,.75-3.41L298.4,267.84a25.77,25.77,0,1,0,17.42-10.23Z"></path><path fill="currentColor" d="M313,214a7.76,7.76,0,1,1,1.41,10.91,7.62,7.62,0,0,1-2.19-2.69l-18.67-.14a25.94,25.94,0,1,0,.05-7l18.64.14A7.4,7.4,0,0,1,313,214Z"></path><path fill="currentColor" d="M341.2,144.08h-6.32c-1.67,0-3.61,1.87-3.61,4.29s1.94,4.29,3.61,4.29h6.32c1.67,0,3.61-1.87,3.61-4.29S342.87,144.08,341.2,144.08Z"></path><path fill="currentColor" d="M301.75,144.08h-6.44c-1.67,0-3.61,1.87-3.61,4.29s1.94,4.29,3.61,4.29h6.44c1.67,0,3.61-1.87,3.61-4.29S303.42,144.08,301.75,144.08Z"></path><path fill="currentColor" d="M321.77,144.08h-7c-1.67,0-3.62,1.87-3.62,4.29s1.95,4.29,3.62,4.29h7c1.67,0,3.62-1.87,3.62-4.29S323.44,144.08,321.77,144.08Z"></path><ellipse fill="currentColor" cx="295.97" cy="127.61" rx="4.27" ry="4.29"></ellipse><path fill="currentColor" d="M340.54,131.9a4.29,4.29,0,1,0-4.27-4.29A4.28,4.28,0,0,0,340.54,131.9Z"></path><path fill="currentColor" d="M318.26,131.9a4.29,4.29,0,1,0-4.27-4.29A4.29,4.29,0,0,0,318.26,131.9Z"></path><ellipse fill="currentColor" cx="295.97" cy="169.13" rx="4.27" ry="4.29"></ellipse><path fill="currentColor" d="M340.54,164.84a4.3,4.3,0,1,0,4.27,4.29A4.29,4.29,0,0,0,340.54,164.84Z"></path><path fill="currentColor" d="M318.26,164.84a4.3,4.3,0,1,0,4.28,4.29A4.29,4.29,0,0,0,318.26,164.84Z"></path><path d="M108.62,256.87c8.36-1,7.68-7.76,3.14-17-3.64-7.4-9.74-16.39-15.75-25.36-14.23-21.23-27.69-42.23-5.35-41.07,19.55,1,42.9,18.63,68.22,36.74,31.1,22.24,65.16,45.21,98.81,39.11a151.19,151.19,0,0,1,20-2.37V221a92,92,0,0,0-91.91-92.16H124.33A92,92,0,0,0,32.42,221v17.59c17.71,3.81,31,9.94,43.8,14.15C86.6,256.16,96.69,258.31,108.62,256.87Z"></path><path d="M273.37,310.79c-35-15.26-76.67-32.1-104-23.59-3.15,1-5,2.3-6,3.85-3.35,5.31,4.67,13.57,14.89,22.17,7.17,6,15.36,12.21,21.44,17.64,11.47,10.26,15.35,17.84-9.89,16.62-29.75-1.44-49.18-13.75-71.18-24l-.29-.14a165.84,165.84,0,0,0-22.93-8.91c-15.74-4.67-34.22-6.79-58.51-3.28A91.93,91.93,0,0,0,124.33,375h61.45A92,92,0,0,0,273.37,310.79Z"></path><path fill="currentColor" d="M257.69,249.31C224,255.41,190,232.44,158.88,210.2c-25.32-18.11-48.67-35.72-68.22-36.74C68.32,172.3,81.78,193.3,96,214.53c6,9,12.11,18,15.75,25.36,4.54,9.22,5.22,16-3.14,17-11.93,1.44-22-.71-32.4-4.13-12.8-4.21-26.09-10.34-43.8-14.15v44.26c0,1.26.14,2.48.19,3.72a91.8,91.8,0,0,0,2.9,19.62c.43,1.67.84,3.34,1.37,5,24.29-3.51,42.77-1.39,58.51,3.28a165.84,165.84,0,0,1,22.93,8.91c.39-.12.76-.26,1.14-.39l-.85.53c22,10.25,41.43,22.56,71.18,24,25.24,1.22,21.36-6.36,9.89-16.62-6.08-5.43-14.27-11.61-21.44-17.64-10.22-8.6-18.24-16.86-14.89-22.17,1-1.55,2.87-2.87,6-3.85,27.33-8.51,69,8.33,104,23.59.32-1,.56-2.05.84-3.07a92.33,92.33,0,0,0,3.48-24.87V246.94A151.19,151.19,0,0,0,257.69,249.31Z"></path><path fill="currentColor" d="M192,137a78,78,0,0,1,77.78,78v73.91a78,78,0,0,1-77.78,78H118.51a78,78,0,0,1-77.78-78V215a78,78,0,0,1,77.78-78H192m0-8.33H118.51A86.21,86.21,0,0,0,32.42,215v73.91a86.21,86.21,0,0,0,86.09,86.33H192a86.21,86.21,0,0,0,86.09-86.33V215A86.21,86.21,0,0,0,192,128.64Z"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-version" *ngIf="officialMempoolSpace">
|
||||
{{ (backendInfo$ | async)?.hostname }} (v{{ (backendInfo$ | async )?.version }}) [<a href="https://github.com/mempool/mempool/commit/{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}">{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}</a>]
|
||||
</div>
|
||||
|
||||
<br>
|
||||
</div>
|
||||
|
||||
|
||||
<ng-template #loadingSponsors>
|
||||
<br>
|
||||
<div class="spinner-border text-light"></div>
|
||||
</ng-template>
|
||||
|
||||
|
||||
@@ -82,6 +82,10 @@
|
||||
|
||||
<br />
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
<main>
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
<app-global-footer *ngIf="footerVisible"></app-global-footer>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -17,6 +17,12 @@ li.nav-item {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
footer > .container-fluid {
|
||||
padding-bottom: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.navbar {
|
||||
padding: 0rem 2rem;
|
||||
|
||||
@@ -17,6 +17,7 @@ export class BisqMasterPageComponent implements OnInit {
|
||||
isMobile = window.innerWidth <= 767.98;
|
||||
urlLanguage: string;
|
||||
networkPaths: { [network: string]: string };
|
||||
footerVisible = true;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
@@ -31,6 +32,11 @@ export class BisqMasterPageComponent implements OnInit {
|
||||
this.urlLanguage = this.languageService.getLanguageForUrl();
|
||||
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||
this.networkPaths = paths;
|
||||
if (paths.mainnet.indexOf('docs') > -1) {
|
||||
this.footerVisible = false;
|
||||
} else {
|
||||
this.footerVisible = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
@media (min-width: 465px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.main-title {
|
||||
@@ -18,17 +21,19 @@
|
||||
}
|
||||
|
||||
.full-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
min-height: 500px;
|
||||
height: calc(100% - 150px);
|
||||
@media (max-width: 992px) {
|
||||
padding-bottom: 100px;
|
||||
};
|
||||
height: calc(100vh - 250px);
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
padding-bottom: 20px;
|
||||
padding-right: 10px;
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
@media (min-width: 465px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.main-title {
|
||||
@@ -18,18 +21,20 @@
|
||||
}
|
||||
|
||||
.full-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
min-height: 500px;
|
||||
height: calc(100% - 150px);
|
||||
@media (max-width: 992px) {
|
||||
padding-bottom: 100px;
|
||||
};
|
||||
height: calc(100vh - 250px);
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-bottom: 20px;
|
||||
padding-right: 10px;
|
||||
@media (max-width: 992px) {
|
||||
|
||||
@@ -23,6 +23,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
@Input() unavailable: boolean = false;
|
||||
@Input() auditHighlighting: boolean = false;
|
||||
@Input() blockConversion: Price;
|
||||
@Input() pixelAlign: boolean = false;
|
||||
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
|
||||
@Output() txHoverEvent = new EventEmitter<string>();
|
||||
@Output() readyEvent = new EventEmitter();
|
||||
@@ -132,9 +133,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
}
|
||||
}
|
||||
|
||||
update(add: TransactionStripped[], remove: string[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||
if (this.scene) {
|
||||
this.scene.update(add, remove, direction, resetLayout);
|
||||
this.scene.update(add, remove, change, direction, resetLayout);
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
@@ -201,7 +202,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
this.start();
|
||||
} else {
|
||||
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
|
||||
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, highlighting: this.auditHighlighting });
|
||||
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
|
||||
highlighting: this.auditHighlighting, pixelAlign: this.pixelAlign });
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export default class BlockScene {
|
||||
gridWidth: number;
|
||||
gridHeight: number;
|
||||
gridSize: number;
|
||||
pixelAlign: boolean;
|
||||
vbytesPerUnit: number;
|
||||
unitPadding: number;
|
||||
unitWidth: number;
|
||||
@@ -23,19 +24,24 @@ export default class BlockScene {
|
||||
animateUntil = 0;
|
||||
dirty: boolean;
|
||||
|
||||
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
|
||||
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }:
|
||||
{ width: number, height: number, resolution: number, blockLimit: number,
|
||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
|
||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, pixelAlign: boolean }
|
||||
) {
|
||||
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting });
|
||||
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign });
|
||||
}
|
||||
|
||||
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.gridSize = this.width / this.gridWidth;
|
||||
this.unitPadding = width / 500;
|
||||
this.unitWidth = this.gridSize - (this.unitPadding * 2);
|
||||
if (this.pixelAlign) {
|
||||
this.unitPadding = Math.max(1, Math.floor(this.gridSize / 2.5));
|
||||
this.unitWidth = this.gridSize - (this.unitPadding);
|
||||
} else {
|
||||
this.unitPadding = width / 500;
|
||||
this.unitWidth = this.gridSize - (this.unitPadding * 2);
|
||||
}
|
||||
|
||||
this.dirty = true;
|
||||
if (this.initialised && this.scene) {
|
||||
@@ -150,7 +156,7 @@ export default class BlockScene {
|
||||
this.updateAll(startTime, 200, direction);
|
||||
}
|
||||
|
||||
update(add: TransactionStripped[], remove: string[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||
const startTime = performance.now();
|
||||
const removed = this.removeBatch(remove, startTime, direction);
|
||||
|
||||
@@ -172,6 +178,15 @@ export default class BlockScene {
|
||||
this.place(tx);
|
||||
});
|
||||
} else {
|
||||
// update effective rates
|
||||
change.forEach(tx => {
|
||||
if (this.txs[tx.txid]) {
|
||||
this.txs[tx.txid].feerate = tx.rate || (this.txs[tx.txid].fee / this.txs[tx.txid].vsize);
|
||||
this.txs[tx.txid].rate = tx.rate;
|
||||
this.txs[tx.txid].dirty = true;
|
||||
}
|
||||
});
|
||||
|
||||
// try to insert new txs directly
|
||||
const remaining = [];
|
||||
add.map(tx => new TxView(tx, this)).sort(feeRateDescending).forEach(tx => {
|
||||
@@ -200,14 +215,15 @@ export default class BlockScene {
|
||||
this.animateUntil = Math.max(this.animateUntil, tx.setHover(value));
|
||||
}
|
||||
|
||||
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
|
||||
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }:
|
||||
{ width: number, height: number, resolution: number, blockLimit: number,
|
||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
|
||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, pixelAlign: boolean }
|
||||
): void {
|
||||
this.orientation = orientation;
|
||||
this.flip = flip;
|
||||
this.vertexArray = vertexArray;
|
||||
this.highlightingEnabled = highlighting;
|
||||
this.pixelAlign = pixelAlign;
|
||||
|
||||
this.scene = {
|
||||
count: 0,
|
||||
@@ -333,7 +349,12 @@ export default class BlockScene {
|
||||
private gridToScreen(position: Square | void): Square {
|
||||
if (position) {
|
||||
const slotSize = (position.s * this.gridSize);
|
||||
const squareSize = slotSize - (this.unitPadding * 2);
|
||||
let squareSize;
|
||||
if (this.pixelAlign) {
|
||||
squareSize = slotSize - (this.unitPadding);
|
||||
} else {
|
||||
squareSize = slotSize - (this.unitPadding * 2);
|
||||
}
|
||||
|
||||
// The grid is laid out notionally left-to-right, bottom-to-top,
|
||||
// so we rotate and/or flip the y axis to match the target configuration.
|
||||
|
||||
@@ -36,6 +36,7 @@ export default class TxView implements TransactionStripped {
|
||||
vsize: number;
|
||||
value: number;
|
||||
feerate: number;
|
||||
rate?: number;
|
||||
status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
|
||||
context?: 'projected' | 'actual';
|
||||
scene?: BlockScene;
|
||||
@@ -58,7 +59,8 @@ export default class TxView implements TransactionStripped {
|
||||
this.fee = tx.fee;
|
||||
this.vsize = tx.vsize;
|
||||
this.value = tx.value;
|
||||
this.feerate = tx.fee / tx.vsize;
|
||||
this.feerate = tx.rate || (tx.fee / tx.vsize); // sort by effective fee rate where available
|
||||
this.rate = tx.rate;
|
||||
this.status = tx.status;
|
||||
this.initialised = false;
|
||||
this.vertexArray = scene.vertexArray;
|
||||
@@ -157,7 +159,8 @@ export default class TxView implements TransactionStripped {
|
||||
}
|
||||
|
||||
getColor(): Color {
|
||||
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, this.feerate) < feeLvl) - 1;
|
||||
const rate = this.fee / this.vsize; // color by simple single-tx fee rate
|
||||
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1;
|
||||
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
|
||||
// Normal mode
|
||||
if (!this.scene?.highlightingEnabled) {
|
||||
|
||||
@@ -28,6 +28,12 @@
|
||||
{{ feeRate | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="effectiveRate && effectiveRate !== feeRate">
|
||||
<td class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||
<td>
|
||||
{{ effectiveRate | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="td-width" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
||||
<td [innerHTML]="'‎' + (vsize | vbytes: 2)"></td>
|
||||
|
||||
@@ -20,6 +20,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
|
||||
value = 0;
|
||||
vsize = 1;
|
||||
feeRate = 0;
|
||||
effectiveRate;
|
||||
|
||||
tooltipPosition: Position = { x: 0, y: 0 };
|
||||
|
||||
@@ -51,6 +52,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
|
||||
this.value = tx.value || 0;
|
||||
this.vsize = tx.vsize || 1;
|
||||
this.feeRate = this.fee / this.vsize;
|
||||
this.effectiveRate = tx.rate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
@media (min-width: 465px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.main-title {
|
||||
@@ -18,17 +21,19 @@
|
||||
}
|
||||
|
||||
.full-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
min-height: 500px;
|
||||
height: calc(100% - 150px);
|
||||
@media (max-width: 992px) {
|
||||
padding-bottom: 100px;
|
||||
};
|
||||
height: calc(100vh - 250px);
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
padding-bottom: 20px;
|
||||
padding-right: 10px;
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
@media (min-width: 465px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.main-title {
|
||||
@@ -18,17 +21,19 @@
|
||||
}
|
||||
|
||||
.full-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
min-height: 500px;
|
||||
height: calc(100% - 150px);
|
||||
@media (max-width: 992px) {
|
||||
padding-bottom: 100px;
|
||||
};
|
||||
height: calc(100vh - 250px);
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
padding-bottom: 20px;
|
||||
padding-right: 10px;
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
@media (min-width: 465px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.main-title {
|
||||
@@ -18,17 +21,19 @@
|
||||
}
|
||||
|
||||
.full-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
min-height: 500px;
|
||||
height: calc(100% - 150px);
|
||||
@media (max-width: 992px) {
|
||||
padding-bottom: 100px;
|
||||
};
|
||||
height: calc(100vh - 250px);
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
padding-bottom: 20px;
|
||||
padding-right: 10px;
|
||||
|
||||
@@ -1,53 +1,61 @@
|
||||
<div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr"
|
||||
[style.left]="static ? (offset || 0) + 'px' : null"
|
||||
<div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr" [class.minimal]="minimal"
|
||||
[style.left]="static ? (offset || 0) + 'px' : null" [style.--block-size]="blockWidth+'px'"
|
||||
*ngIf="static || (loadingBlocks$ | async) === false; else loadingBlocksTemplate">
|
||||
<div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn">
|
||||
<ng-container *ngIf="connected && block && !block.loading && !block.placeholder; else placeholderBlock">
|
||||
<div
|
||||
*ngIf="minimal && spotlight < 0 && chainTip + spotlight + 1 === block.height"
|
||||
class="spotlight-bottom"
|
||||
[style.left]="blockStyles[i].left"
|
||||
></div>
|
||||
<div [attr.data-cy]="'bitcoin-block-offset-' + offset + '-index-' + i"
|
||||
class="text-center bitcoin-block mined-block blockchain-blocks-offset-{{ offset }}-index-{{ i }}"
|
||||
[class.offscreen]="!static && count && i >= count"
|
||||
id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"
|
||||
[class.blink-bg]="isSpecial(block.height)">
|
||||
<a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }"
|
||||
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height">
|
||||
<div *ngIf="!minimal" [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height">
|
||||
<a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height
|
||||
}}</a>
|
||||
</div>
|
||||
<div class="block-body">
|
||||
<div *ngIf="block?.extras; else emptyfees" [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
|
||||
~{{ block?.extras?.medianFee | number:feeRounding }} <ng-container
|
||||
i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
||||
</div>
|
||||
<ng-template #emptyfees>
|
||||
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
|
||||
|
||||
<ng-container *ngIf="!minimal">
|
||||
<div *ngIf="block?.extras; else emptyfees" [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
|
||||
~{{ block?.extras?.medianFee | number:feeRounding }} <ng-container
|
||||
i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-fee-span'" class="fee-span"
|
||||
*ngIf="block?.extras?.feeRange; else emptyfeespan">
|
||||
{{ block?.extras?.feeRange?.[0] | number:feeRounding }} - {{
|
||||
block?.extras?.feeRange[block?.extras?.feeRange?.length - 1] | number:feeRounding }} <ng-container
|
||||
i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
||||
</div>
|
||||
<ng-template #emptyfeespan>
|
||||
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fee-span">
|
||||
|
||||
<ng-template #emptyfees>
|
||||
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
|
||||
|
||||
</div>
|
||||
</ng-template>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-fee-span'" class="fee-span"
|
||||
*ngIf="block?.extras?.feeRange; else emptyfeespan">
|
||||
{{ block?.extras?.feeRange?.[0] | number:feeRounding }} - {{
|
||||
block?.extras?.feeRange[block?.extras?.feeRange?.length - 1] | number:feeRounding }} <ng-container
|
||||
i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-total-fees'" *ngIf="showMiningInfo"
|
||||
class="block-size">
|
||||
<app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||
</div>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + 'block-size'" *ngIf="!showMiningInfo"
|
||||
class="block-size" [innerHTML]="'‎' + (block.size | bytes: 2)"></div>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + i + '-transactions'" class="transaction-count">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
|
||||
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
|
||||
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
|
||||
</div>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-time'" class="time-difference">
|
||||
<app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></div>
|
||||
<ng-template #emptyfeespan>
|
||||
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fee-span">
|
||||
|
||||
</div>
|
||||
</ng-template>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-total-fees'" *ngIf="showMiningInfo"
|
||||
class="block-size">
|
||||
<app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||
</div>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + 'block-size'" *ngIf="!showMiningInfo"
|
||||
class="block-size" [innerHTML]="'‎' + (block.size | bytes: 2)"></div>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + i + '-transactions'" class="transaction-count">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
|
||||
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
|
||||
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
|
||||
</div>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-time'" class="time-difference">
|
||||
<app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
|
||||
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary"
|
||||
@@ -79,11 +87,11 @@
|
||||
</div>
|
||||
|
||||
<ng-template #loadingBlocksTemplate>
|
||||
<div class="blocks-container" [class.time-ltr]="timeLtr">
|
||||
<div class="blocks-container" [class.time-ltr]="timeLtr" [style.--block-size]="blockWidth+'px'">
|
||||
<div class="flashing">
|
||||
<div *ngFor="let block of emptyBlocks; let i = index; trackBy: trackByBlocksFn">
|
||||
<div class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}"
|
||||
[ngStyle]="emptyBlockStyles[i]"></div>
|
||||
[ngStyle]="emptyBlockStyles[i]" [class.offscreen]="!static && count && i >= count"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.bitcoin-block {
|
||||
width: 125px;
|
||||
height: 125px;
|
||||
width: var(--block-size);
|
||||
height: var(--block-size);
|
||||
}
|
||||
|
||||
.blockLink {
|
||||
@@ -22,7 +22,11 @@
|
||||
.mined-block {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
transition: background 2s, left 2s, transform 1s;
|
||||
transition: background 2s, left 2s, transform 1s, opacity 1s;
|
||||
}
|
||||
|
||||
.mined-block.offscreen {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.mined-block.placeholder-block {
|
||||
@@ -35,9 +39,11 @@
|
||||
}
|
||||
|
||||
.blocks-container {
|
||||
--block-size: 125px;
|
||||
--block-offset: calc(0.32 * var(--block-size));
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 40px;
|
||||
left: var(--block-offset);
|
||||
}
|
||||
|
||||
.block-body {
|
||||
@@ -77,11 +83,11 @@
|
||||
|
||||
.bitcoin-block::after {
|
||||
content: '';
|
||||
width: 125px;
|
||||
height: 24px;
|
||||
width: var(--block-size);
|
||||
height: calc(0.192 * var(--block-size));
|
||||
position:absolute;
|
||||
top: -24px;
|
||||
left: -20px;
|
||||
top: calc(-0.192 * var(--block-size));
|
||||
left: calc(-0.16 * var(--block-size));
|
||||
background-color: #232838;
|
||||
transform:skew(40deg);
|
||||
transform-origin:top;
|
||||
@@ -89,11 +95,11 @@
|
||||
|
||||
.bitcoin-block::before {
|
||||
content: '';
|
||||
width: 20px;
|
||||
height: 125px;
|
||||
width: calc(0.16 * var(--block-size));
|
||||
height: var(--block-size);
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: -20px;
|
||||
top: calc(-0.096 * var(--block-size));
|
||||
left: calc(-0.16 * var(--block-size));
|
||||
background-color: #191c27;
|
||||
|
||||
transform: skewY(50deg);
|
||||
@@ -168,4 +174,16 @@
|
||||
.bitcoin-block {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
.spotlight-bottom {
|
||||
position: absolute;
|
||||
width: calc(0.6 * var(--block-size));
|
||||
height: calc(0.25 * var(--block-size));
|
||||
border-left: solid calc(0.3 * var(--block-size)) transparent;
|
||||
border-bottom: solid calc(0.3 * var(--block-size)) white;
|
||||
border-right: solid calc(0.3 * var(--block-size)) transparent;
|
||||
transform: translate(calc(0.2 * var(--block-size)), calc(1.1 * var(--block-size)));
|
||||
border-radius: 2px;
|
||||
z-index: -1;
|
||||
}
|
||||
@@ -24,6 +24,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() count: number = 8; // number of blocks in this chunk (dynamic blocks only)
|
||||
@Input() loadingTip: boolean = false;
|
||||
@Input() connected: boolean = true;
|
||||
@Input() minimal: boolean = false;
|
||||
@Input() blockWidth: number = 125;
|
||||
@Input() spotlight: number = 0;
|
||||
|
||||
specialBlocks = specialBlocks;
|
||||
network = '';
|
||||
@@ -51,6 +54,10 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
timeLtrSubscription: Subscription;
|
||||
timeLtr: boolean;
|
||||
|
||||
blockOffset: number = 155;
|
||||
dividerBlockOffset: number = 205;
|
||||
blockPadding: number = 30;
|
||||
|
||||
gradientColors = {
|
||||
'': ['#9339f4', '#105fb0'],
|
||||
bisq: ['#9339f4', '#105fb0'],
|
||||
@@ -118,7 +125,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
this.blockStyles = [];
|
||||
if (this.blocksFilled && block.height > this.chainTip) {
|
||||
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -155 : -205)));
|
||||
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -this.blockOffset : -this.dividerBlockOffset)));
|
||||
setTimeout(() => {
|
||||
this.blockStyles = [];
|
||||
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
|
||||
@@ -159,6 +166,13 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.blockWidth && this.blockWidth) {
|
||||
this.blockPadding = 0.24 * this.blockWidth;
|
||||
this.blockOffset = this.blockWidth + this.blockPadding;
|
||||
this.dividerBlockOffset = this.blockOffset + (0.4 * this.blockWidth);
|
||||
this.blockStyles = [];
|
||||
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
|
||||
}
|
||||
if (this.static) {
|
||||
const animateSlide = changes.height && (changes.height.currentValue === changes.height.previousValue + 1);
|
||||
this.updateStaticBlocks(animateSlide);
|
||||
@@ -191,14 +205,14 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
this.arrowVisible = true;
|
||||
if (newBlockFromLeft) {
|
||||
this.arrowLeftPx = blockindex * 155 + 30 - 205;
|
||||
this.arrowLeftPx = blockindex * this.blockOffset + this.blockPadding - this.dividerBlockOffset;
|
||||
setTimeout(() => {
|
||||
this.arrowTransition = '2s';
|
||||
this.arrowLeftPx = blockindex * 155 + 30;
|
||||
this.arrowLeftPx = blockindex * this.blockOffset + this.blockPadding;
|
||||
this.cd.markForCheck();
|
||||
}, 50);
|
||||
} else {
|
||||
this.arrowLeftPx = blockindex * 155 + 30;
|
||||
this.arrowLeftPx = blockindex * this.blockOffset + this.blockPadding;
|
||||
if (!animate) {
|
||||
setTimeout(() => {
|
||||
this.arrowTransition = '2s';
|
||||
@@ -245,7 +259,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
this.blocks = this.blocks.slice(0, this.count);
|
||||
this.blockStyles = [];
|
||||
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, animateSlide ? -155 : 0)));
|
||||
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, animateSlide ? -this.blockOffset : 0)));
|
||||
this.cd.markForCheck();
|
||||
if (animateSlide) {
|
||||
// animate blocks slide right
|
||||
@@ -287,7 +301,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
return {
|
||||
left: addLeft + 155 * index + 'px',
|
||||
left: addLeft + this.blockOffset * index + 'px',
|
||||
background: `repeating-linear-gradient(
|
||||
#2d3348,
|
||||
#2d3348 ${greenBackgroundHeight}%,
|
||||
@@ -309,7 +323,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
const addLeft = animateEnterFrom || 0;
|
||||
|
||||
return {
|
||||
left: addLeft + (155 * index) + 'px',
|
||||
left: addLeft + (this.blockOffset * index) + 'px',
|
||||
background: "#2d3348",
|
||||
};
|
||||
}
|
||||
@@ -317,7 +331,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
getStyleForPlaceholderBlock(index: number, animateEnterFrom: number = 0) {
|
||||
const addLeft = animateEnterFrom || 0;
|
||||
return {
|
||||
left: addLeft + (155 * index) + 'px',
|
||||
left: addLeft + (this.blockOffset * index) + 'px',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -325,7 +339,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
const addLeft = animateEnterFrom || 0;
|
||||
|
||||
return {
|
||||
left: addLeft + 155 * this.emptyBlocks.indexOf(block) + 'px',
|
||||
left: addLeft + this.blockOffset * this.emptyBlocks.indexOf(block) + 'px',
|
||||
background: "#2d3348",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<div class="clock-face" [style]="faceStyle">
|
||||
<ng-content></ng-content>
|
||||
<svg
|
||||
class="cut-out"
|
||||
width="384"
|
||||
height="384"
|
||||
viewBox="0 0 384 384"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
class="face"
|
||||
d="M 0,0 V 384 H 384 V 0 Z M 192,15 A 177,177 0 0 1 369,192 177,177 0 0 1 192,369 177,177 0 0 1 15,192 177,177 0 0 1 192,15 Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
class="demo-dial"
|
||||
width="384"
|
||||
height="384"
|
||||
viewBox="0 0 384 384"
|
||||
>
|
||||
<defs>
|
||||
<pattern id="dial-gradient" patternUnits="userSpaceOnUse" width="384" height="384">
|
||||
<image class="dial-gradient-img" href="/resources/clock/gradient.png" x="0" y="0" width="384" height="384" [style.transform]="'rotate(' + (minutes * 6) + 'deg)'" />
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<path *ngFor="let angle of minorTicks" class="tick minor" d="M 192,27 v 10" [style.transform]="'rotate(' + angle + 'deg)'"/>
|
||||
<path *ngFor="let angle of majorTicks" class="tick major" d="M 192,27 v 18" [style.transform]="'rotate(' + angle + 'deg)'"/>
|
||||
|
||||
<ng-container *ngFor="let segment of segments; trackBy: trackBySegment">
|
||||
<path class="block-segment" [attr.d]="segment.path" />
|
||||
<!-- <circle class="segment-mark start" [attr.cx]="segment.start.x" [attr.cy]="segment.start.y" r="2" style="fill:green;stroke:white;stroke-width:1px;" />
|
||||
<circle class="segment-mark end" [attr.cx]="segment.end.x" [attr.cy]="segment.end.y" r="2" style="fill:red;stroke:white;stroke-width:1px;" /> -->
|
||||
</ng-container>
|
||||
<!-- <polyline points="468.750,82.031 468.750,35 " id="polyline322" style="fill:none;stroke:#ffffff;stroke-width:4.84839;stroke-dasharray:none;stroke-opacity:1" transform="matrix(0.41250847,0,0,0.93092534,-1.3627708,-32.692008)" /> -->
|
||||
<path class="tick very major" d="M 192,0 v 45" />
|
||||
<path id="hour" class="gnomon hour" d="M 178,3 206,3 192,40 Z" [style.transform]="'rotate(' + (hours * 30) + 'deg)'" />
|
||||
<path id="minute" class="gnomon minute" d="M 180,4 204,4 192,38 Z" [style.transform]="'rotate(' + (minutes * 6) + 'deg)'" />
|
||||
</svg>
|
||||
</div>
|
||||
@@ -0,0 +1,69 @@
|
||||
.clock-face {
|
||||
position: relative;
|
||||
height: 84.375%;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
|
||||
.cut-out, .demo-dial {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.face {
|
||||
fill: #11131f;
|
||||
}
|
||||
}
|
||||
|
||||
.gnomon {
|
||||
transform-origin: center;
|
||||
stroke-linejoin: round;
|
||||
|
||||
&.minute {
|
||||
fill:#80C2E1;
|
||||
stroke:#80C2E1;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
&.hour {
|
||||
fill: #105fb0;
|
||||
stroke: #105fb0;
|
||||
stroke-width: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.tick {
|
||||
transform-origin: center;
|
||||
fill: none;
|
||||
stroke: white;
|
||||
stroke-width: 2px;
|
||||
stroke-linecap: butt;
|
||||
|
||||
&.minor {
|
||||
stroke-opacity: 0.5;
|
||||
}
|
||||
|
||||
&.very.major {
|
||||
stroke-width: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.block-segment {
|
||||
fill: none;
|
||||
stroke: url(#dial-gradient);
|
||||
stroke-width: 18px;
|
||||
}
|
||||
|
||||
.dial-segment {
|
||||
fill: none;
|
||||
stroke: white;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.dial-gradient-img {
|
||||
transform-origin: center;
|
||||
}
|
||||
}
|
||||
147
frontend/src/app/components/clock-face/clock-face.component.ts
Normal file
147
frontend/src/app/components/clock-face/clock-face.component.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Subscription, tap, timer } from 'rxjs';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-clock-face',
|
||||
templateUrl: './clock-face.component.html',
|
||||
styleUrls: ['./clock-face.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ClockFaceComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() size: number = 300;
|
||||
|
||||
blocksSubscription: Subscription;
|
||||
timeSubscription: Subscription;
|
||||
|
||||
faceStyle;
|
||||
dialPath;
|
||||
blockTimes = [];
|
||||
segments = [];
|
||||
hours: number = 0;
|
||||
minutes: number = 0;
|
||||
minorTicks: number[] = [];
|
||||
majorTicks: number[] = [];
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private cd: ChangeDetectorRef
|
||||
) {
|
||||
this.updateTime();
|
||||
this.makeTicks();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.timeSubscription = timer(0, 250).pipe(
|
||||
tap(() => {
|
||||
this.updateTime();
|
||||
})
|
||||
).subscribe();
|
||||
this.blocksSubscription = this.stateService.blocks$
|
||||
.subscribe(([block]) => {
|
||||
if (block) {
|
||||
this.blockTimes.push([block.height, new Date(block.timestamp * 1000)]);
|
||||
// using block-reported times, so ensure they are sorted chronologically
|
||||
this.blockTimes = this.blockTimes.sort((a, b) => a[1].getTime() - b[1].getTime());
|
||||
this.updateSegments();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.faceStyle = {
|
||||
width: `${this.size}px`,
|
||||
height: `${this.size}px`,
|
||||
};
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.timeSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
updateTime(): void {
|
||||
const now = new Date();
|
||||
const seconds = now.getSeconds() + (now.getMilliseconds() / 1000);
|
||||
this.minutes = (now.getMinutes() + (seconds / 60)) % 60;
|
||||
this.hours = now.getHours() + (this.minutes / 60);
|
||||
this.updateSegments();
|
||||
}
|
||||
|
||||
updateSegments(): void {
|
||||
const now = new Date();
|
||||
this.blockTimes = this.blockTimes.filter(time => (now.getTime() - time[1].getTime()) <= 3600000);
|
||||
const tail = new Date(now.getTime() - 3600000);
|
||||
const hourStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours());
|
||||
|
||||
const times = [
|
||||
['start', tail],
|
||||
...this.blockTimes,
|
||||
['end', now],
|
||||
];
|
||||
const minuteTimes = times.map(time => {
|
||||
return [time[0], (time[1].getTime() - hourStart.getTime()) / 60000];
|
||||
});
|
||||
this.segments = [];
|
||||
const r = 174;
|
||||
const cx = 192;
|
||||
const cy = cx;
|
||||
for (let i = 1; i < minuteTimes.length; i++) {
|
||||
const arc = this.getArc(minuteTimes[i-1][1], minuteTimes[i][1], r, cx, cy);
|
||||
if (arc) {
|
||||
arc.id = minuteTimes[i][0];
|
||||
this.segments.push(arc);
|
||||
}
|
||||
}
|
||||
const arc = this.getArc(minuteTimes[0][1], minuteTimes[1][1], r, cx, cy);
|
||||
if (arc) {
|
||||
this.dialPath = arc.path;
|
||||
}
|
||||
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
getArc(startTime, endTime, r, cx, cy): any {
|
||||
const startDegrees = (startTime + 0.2) * 6;
|
||||
const endDegrees = (endTime - 0.2) * 6;
|
||||
const start = this.getPointOnCircle(startDegrees, r, cx, cy);
|
||||
const end = this.getPointOnCircle(endDegrees, r, cx, cy);
|
||||
const arcLength = endDegrees - startDegrees;
|
||||
// merge gaps and omit lines shorter than 1 degree
|
||||
if (arcLength >= 1) {
|
||||
const path = `M ${start.x} ${start.y} A ${r} ${r} 0 ${arcLength > 180 ? 1 : 0} 1 ${end.x} ${end.y}`;
|
||||
return {
|
||||
path,
|
||||
start,
|
||||
end
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getPointOnCircle(deg, r, cx, cy) {
|
||||
const modDeg = ((deg % 360) + 360) % 360;
|
||||
const rad = (modDeg * Math.PI) / 180;
|
||||
return {
|
||||
x: cx + (r * Math.sin(rad)),
|
||||
y: cy - (r * Math.cos(rad)),
|
||||
};
|
||||
}
|
||||
|
||||
makeTicks() {
|
||||
this.minorTicks = [];
|
||||
this.majorTicks = [];
|
||||
for (let i = 1; i < 60; i++) {
|
||||
if (i % 5 === 0) {
|
||||
this.majorTicks.push(i * 6);
|
||||
} else {
|
||||
this.minorTicks.push(i * 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trackBySegment(index: number, segment) {
|
||||
return segment.id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<app-clock mode="mempool"></app-clock>
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-clock-mempool',
|
||||
templateUrl: './clock-mempool.component.html',
|
||||
})
|
||||
export class ClockMempoolComponent {}
|
||||
@@ -0,0 +1 @@
|
||||
<app-clock mode="block"></app-clock>
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-clock-mined',
|
||||
templateUrl: './clock-mined.component.html',
|
||||
})
|
||||
export class ClockMinedComponent {}
|
||||
67
frontend/src/app/components/clock/clock.component.html
Normal file
67
frontend/src/app/components/clock/clock.component.html
Normal file
@@ -0,0 +1,67 @@
|
||||
<div class="clock-wrapper" [style]="wrapperStyle">
|
||||
<div class="clockchain-bar" [style.height]="chainHeight + 'px'">
|
||||
<div class="clockchain">
|
||||
<app-clockchain [width]="chainWidth" [height]="chainHeight" [mode]="mode"></app-clockchain>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clock-face">
|
||||
<app-clock-face [size]="clockSize">
|
||||
<div class="block-wrapper">
|
||||
<ng-container *ngIf="block && block.height >= 0">
|
||||
<ng-container *ngIf="mode === 'block'; else mempoolMode;">
|
||||
<div class="block-cube">
|
||||
<div class="side top"></div>
|
||||
<div class="side bottom"></div>
|
||||
<div class="side right" [style]="blockStyle"></div>
|
||||
<div class="side left" [style]="blockStyle"></div>
|
||||
<div class="side front" [style]="blockStyle"></div>
|
||||
<div class="side back" [style]="blockStyle"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #mempoolMode>
|
||||
<div class="block-sizer" [style]="blockSizerStyle">
|
||||
<app-mempool-block-overview [index]="0" [pixelAlign]="true"></app-mempool-block-overview>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="fader"></div>
|
||||
<div class="title-wrapper">
|
||||
<h1 class="block-height">{{ block.height }}</h1>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</app-clock-face>
|
||||
</div>
|
||||
<ng-container *ngIf="!hideStats">
|
||||
<div class="stats top left">
|
||||
<p class="label" i18n="clock.fiat-price">fiat price</p>
|
||||
<p>
|
||||
<app-fiat [value]="100000000" digitsInfo="1.2-2" colorClass="white-color"></app-fiat>
|
||||
</p>
|
||||
</div>
|
||||
<div class="stats top right">
|
||||
<p class="label" i18n="clock.priority-rate|priority fee rate">priority rate</p>
|
||||
<p *ngIf="recommendedFees$ | async as recommendedFees;" i18n="shared.sat-vbyte|sat/vB">{{ recommendedFees.fastestFee }} sat/vB</p>
|
||||
</div>
|
||||
<div *ngIf="mode !== 'mempool' && block" class="stats bottom left">
|
||||
<p [innerHTML]="block.size | bytes: 2"></p>
|
||||
<p class="label" i18n="clock.block-size">block size</p>
|
||||
</div>
|
||||
<div *ngIf="mode !== 'mempool' && block" class="stats bottom right">
|
||||
<p class="force-wrap">
|
||||
<ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
|
||||
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} <span class="label">transaction</span></ng-template>
|
||||
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} <span class="label">transactions</span></ng-template>
|
||||
</p>
|
||||
</div>
|
||||
<ng-container *ngIf="mempoolInfo$ | async as mempoolInfo;">
|
||||
<div *ngIf="mode === 'mempool'" class="stats bottom left">
|
||||
<p [innerHTML]="mempoolInfo.usage | bytes: 0"></p>
|
||||
<p class="label" i18n="dashboard.memory-usage|Memory usage">memory usage</p>
|
||||
</div>
|
||||
<div *ngIf="mode === 'mempool'" class="stats bottom right">
|
||||
<p>{{ mempoolInfo.size | number }}</p>
|
||||
<p class="label" i18n="dashboard.unconfirmed|Unconfirmed count">unconfirmed</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
190
frontend/src/app/components/clock/clock.component.scss
Normal file
190
frontend/src/app/components/clock/clock.component.scss
Normal file
@@ -0,0 +1,190 @@
|
||||
.clock-wrapper {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
--chain-height: 60px;
|
||||
--clock-width: 300px;
|
||||
|
||||
.clockchain-bar, .clock-face {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.clockchain-bar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 15.625%;
|
||||
z-index: 2;
|
||||
// overflow: hidden;
|
||||
// background: #1d1f31;
|
||||
// box-shadow: 0 0 15px #000;
|
||||
}
|
||||
|
||||
.clock-face {
|
||||
position: relative;
|
||||
height: 84.375%;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.stats {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: calc(0.055 * var(--clock-width));
|
||||
line-height: calc(0.05 * var(--clock-width));
|
||||
opacity: 0.8;
|
||||
|
||||
&.force-wrap {
|
||||
word-spacing: 10000px;
|
||||
}
|
||||
|
||||
::ng-deep .symbol {
|
||||
font-size: inherit;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: calc(0.04 * var(--clock-width));
|
||||
line-height: calc(0.05 * var(--clock-width));
|
||||
}
|
||||
|
||||
&.top {
|
||||
top: calc(var(--chain-height) + 2%);
|
||||
}
|
||||
&.bottom {
|
||||
bottom: 2%;
|
||||
}
|
||||
&.left {
|
||||
left: 5%;
|
||||
}
|
||||
&.right {
|
||||
right: 5%;
|
||||
text-align: end;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title-wrapper {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.block-height {
|
||||
font-size: calc(0.2 * var(--clock-width));
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: radial-gradient(rgba(0,0,0,0.5), transparent 67%);
|
||||
padding: calc(0.05 * var(--clock-width)) calc(0.15 * var(--clock-width));
|
||||
}
|
||||
}
|
||||
|
||||
.block-wrapper {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.block-sizer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fader {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: radial-gradient(transparent 0%, transparent 44%, #11131f 58%, #11131f 100%);
|
||||
}
|
||||
|
||||
.block-cube {
|
||||
--side-width: calc(0.4 * var(--clock-width));
|
||||
--half-side: calc(0.2 * var(--clock-width));
|
||||
--neg-half-side: calc(-0.2 * var(--clock-width));
|
||||
transform-style: preserve-3d;
|
||||
animation: block-spin 60s infinite linear;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: var(--side-width);
|
||||
height: var(--side-width);
|
||||
|
||||
.side {
|
||||
width: var(--side-width);
|
||||
height: var(--side-width);
|
||||
line-height: 100px;
|
||||
text-align: center;
|
||||
background: #232838;
|
||||
display: block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.side.top {
|
||||
transform: rotateX(90deg);
|
||||
margin-top: var(--neg-half-side);
|
||||
}
|
||||
|
||||
.side.bottom {
|
||||
background: #105fb0;
|
||||
transform: rotateX(-90deg);
|
||||
margin-top: var(--half-side);
|
||||
}
|
||||
|
||||
.side.right {
|
||||
transform: rotateY(90deg);
|
||||
margin-left: var(--half-side);
|
||||
}
|
||||
|
||||
.side.left {
|
||||
transform: rotateY(-90deg);
|
||||
margin-left: var(--neg-half-side);
|
||||
}
|
||||
|
||||
.side.front {
|
||||
transform: translateZ(var(--half-side));
|
||||
}
|
||||
|
||||
.side.back {
|
||||
transform: translateZ(var(--neg-half-side));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes block-spin {
|
||||
0% {transform: translate(-50%, -50%) rotateX(-20deg) rotateY(0deg);}
|
||||
100% {transform: translate(-50%, -50%) rotateX(-20deg) rotateY(-360deg);}
|
||||
}
|
||||
105
frontend/src/app/components/clock/clock.component.ts
Normal file
105
frontend/src/app/components/clock/clock.component.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Input, OnInit } from '@angular/core';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { MempoolInfo, Recommendedfees } from '../../interfaces/websocket.interface';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-clock',
|
||||
templateUrl: './clock.component.html',
|
||||
styleUrls: ['./clock.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ClockComponent implements OnInit {
|
||||
@Input() mode: 'block' | 'mempool' = 'block';
|
||||
hideStats: boolean = false;
|
||||
blocksSubscription: Subscription;
|
||||
recommendedFees$: Observable<Recommendedfees>;
|
||||
mempoolInfo$: Observable<MempoolInfo>;
|
||||
block: BlockExtended;
|
||||
clockSize: number = 300;
|
||||
chainWidth: number = 384;
|
||||
chainHeight: number = 60;
|
||||
blockStyle;
|
||||
blockSizerStyle;
|
||||
wrapperStyle;
|
||||
limitWidth: number;
|
||||
limitHeight: number;
|
||||
|
||||
gradientColors = {
|
||||
'': ['#9339f4', '#105fb0'],
|
||||
bisq: ['#9339f4', '#105fb0'],
|
||||
liquid: ['#116761', '#183550'],
|
||||
'liquidtestnet': ['#494a4a', '#272e46'],
|
||||
testnet: ['#1d486f', '#183550'],
|
||||
signet: ['#6f1d5d', '#471850'],
|
||||
};
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private websocketService: WebsocketService,
|
||||
private route: ActivatedRoute,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {
|
||||
this.route.queryParams.subscribe((params) => {
|
||||
this.hideStats = params && params.stats === 'false';
|
||||
this.limitWidth = Number.parseInt(params.width) || null;
|
||||
this.limitHeight = Number.parseInt(params.height) || null;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.resizeCanvas();
|
||||
this.websocketService.want(['blocks', 'stats', 'mempool-blocks']);
|
||||
|
||||
this.blocksSubscription = this.stateService.blocks$
|
||||
.subscribe(([block]) => {
|
||||
if (block) {
|
||||
this.block = block;
|
||||
this.blockStyle = this.getStyleForBlock(this.block);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
this.recommendedFees$ = this.stateService.recommendedFees$;
|
||||
this.mempoolInfo$ = this.stateService.mempoolInfo$;
|
||||
}
|
||||
|
||||
getStyleForBlock(block: BlockExtended) {
|
||||
const greenBackgroundHeight = 100 - (block.weight / this.stateService.env.BLOCK_WEIGHT_UNITS) * 100;
|
||||
|
||||
return {
|
||||
background: `repeating-linear-gradient(
|
||||
#2d3348,
|
||||
#2d3348 ${greenBackgroundHeight}%,
|
||||
${this.gradientColors[''][0]} ${Math.max(greenBackgroundHeight, 0)}%,
|
||||
${this.gradientColors[''][1]} 100%
|
||||
)`,
|
||||
};
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
resizeCanvas(): void {
|
||||
const windowWidth = this.limitWidth || window.innerWidth;
|
||||
const windowHeight = this.limitHeight || window.innerHeight;
|
||||
this.chainWidth = windowWidth;
|
||||
this.chainHeight = Math.max(60, windowHeight / 8);
|
||||
this.clockSize = Math.min(800, windowWidth, windowHeight - (1.4 * this.chainHeight));
|
||||
const size = Math.ceil(this.clockSize / 75) * 75;
|
||||
const margin = (this.clockSize - size) / 2;
|
||||
this.blockSizerStyle = {
|
||||
transform: `translate(${margin}px, ${margin}px)`,
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
};
|
||||
this.wrapperStyle = {
|
||||
'--clock-width': `${this.clockSize}px`,
|
||||
'--chain-height': `${this.chainHeight}px`,
|
||||
'width': this.limitWidth ? `${this.limitWidth}px` : undefined,
|
||||
'height': this.limitHeight ? `${this.limitHeight}px` : undefined,
|
||||
};
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<div
|
||||
class="text-center" class="blockchain-wrapper" [class.time-ltr]="timeLtr" #container
|
||||
[class.ltr-transition]="ltrTransitionEnabled" [style.width]="width + 'px'" [style.height]="height + 'px'"
|
||||
>
|
||||
<div class="position-container" [ngClass]="network ? network : ''" [style.top]="(height / 3) + 'px'">
|
||||
<span>
|
||||
<div class="blocks-wrapper">
|
||||
<app-mempool-blocks [minimal]="true" [count]="mempoolBlocks" [blockWidth]="blockWidth" [spotlight]="mode === 'mempool' ? 1 : 0"></app-mempool-blocks>
|
||||
<app-blockchain-blocks [minimal]="true" [count]="blockchainBlocks" [blockWidth]="blockWidth" [spotlight]="mode === 'block' ? -1 : 0"></app-blockchain-blocks>
|
||||
</div>
|
||||
<div class="divider" [style.top]="-(height / 6) + 'px'">
|
||||
<svg
|
||||
viewBox="0 0 2 175"
|
||||
[style.width]="'2px'"
|
||||
[style.height]="(5 * height / 6) + 'px'"
|
||||
>
|
||||
<line
|
||||
class="divider-line"
|
||||
x0="0"
|
||||
x1="0"
|
||||
y0="0"
|
||||
y1="175px"
|
||||
></line>
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,94 @@
|
||||
.divider {
|
||||
position: absolute;
|
||||
left: -0.5px;
|
||||
top: 0;
|
||||
.divider-line {
|
||||
stroke: white;
|
||||
stroke-width: 4px;
|
||||
stroke-linecap: butt;
|
||||
stroke-dasharray: 25px 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.blockchain-wrapper {
|
||||
height: 100%;
|
||||
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* IE10+/Edge */
|
||||
user-select: none; /* Standard */
|
||||
}
|
||||
|
||||
.position-container {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.black-background {
|
||||
background-color: #11131f;
|
||||
z-index: 100;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.scroll-spacer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loading-block {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
width: 300px;
|
||||
left: -150px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.time-toggle {
|
||||
color: white;
|
||||
font-size: 0.8rem;
|
||||
position: absolute;
|
||||
bottom: -1.8em;
|
||||
left: 1px;
|
||||
transform: translateX(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.blockchain-wrapper.ltr-transition .blocks-wrapper,
|
||||
.blockchain-wrapper.ltr-transition .position-container,
|
||||
.blockchain-wrapper.ltr-transition .time-toggle {
|
||||
transition: transform 1s;
|
||||
}
|
||||
|
||||
.blockchain-wrapper.time-ltr {
|
||||
.blocks-wrapper {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.time-toggle {
|
||||
transform: translateX(-50%) scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.ltr-layout) {
|
||||
.blockchain-wrapper.time-ltr .blocks-wrapper,
|
||||
.blockchain-wrapper .blocks-wrapper {
|
||||
direction: ltr;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.rtl-layout) {
|
||||
.blockchain-wrapper.time-ltr .blocks-wrapper,
|
||||
.blockchain-wrapper .blocks-wrapper {
|
||||
direction: rtl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, OnChanges, ChangeDetectorRef } from '@angular/core';
|
||||
import { firstValueFrom, Subscription } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-clockchain',
|
||||
templateUrl: './clockchain.component.html',
|
||||
styleUrls: ['./clockchain.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ClockchainComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() width: number = 300;
|
||||
@Input() height: number = 60;
|
||||
@Input() mode: 'mempool' | 'block';
|
||||
|
||||
mempoolBlocks: number = 3;
|
||||
blockchainBlocks: number = 6;
|
||||
blockWidth: number = 50;
|
||||
dividerStyle;
|
||||
|
||||
network: string;
|
||||
timeLtrSubscription: Subscription;
|
||||
timeLtr: boolean = this.stateService.timeLtr.value;
|
||||
ltrTransitionEnabled = false;
|
||||
connectionStateSubscription: Subscription;
|
||||
loadingTip: boolean = true;
|
||||
connected: boolean = true;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.ngOnChanges();
|
||||
|
||||
this.network = this.stateService.network;
|
||||
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
|
||||
this.timeLtr = !!ltr;
|
||||
});
|
||||
this.connectionStateSubscription = this.stateService.connectionState$.subscribe(state => {
|
||||
this.connected = (state === 2);
|
||||
});
|
||||
firstValueFrom(this.stateService.chainTip$).then(() => {
|
||||
this.loadingTip = false;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.blockWidth = Math.floor(7 * this.height / 12);
|
||||
this.mempoolBlocks = Math.floor(((this.width / 2) - (this.blockWidth * 0.32)) / (1.24 * this.blockWidth));
|
||||
this.blockchainBlocks = this.mempoolBlocks;
|
||||
this.dividerStyle = {
|
||||
width: '2px',
|
||||
height: `${this.height}px`,
|
||||
};
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.timeLtrSubscription.unsubscribe();
|
||||
this.connectionStateSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
trackByPageFn(index: number, item: { index: number }) {
|
||||
return item.index;
|
||||
}
|
||||
|
||||
toggleTimeDirection() {
|
||||
this.ltrTransitionEnabled = true;
|
||||
this.stateService.timeLtr.next(!this.timeLtr);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
<a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a>
|
||||
</td>
|
||||
<td class="date text-left">
|
||||
<app-time kind="since" [time]="diffChange.timestamp" [fastRender]="true"></app-time>
|
||||
<app-time kind="since" [time]="diffChange.timestamp" [fastRender]="true" [precision]="1"></app-time>
|
||||
</td>
|
||||
<td class="text-right">{{ diffChange.difficultyShorten }}</td>
|
||||
<td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
|
||||
</div>
|
||||
<div class="symbol"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time></div>
|
||||
<div class="symbol"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true" [precision]="1"></app-time></div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
|
||||
@@ -54,7 +54,7 @@
|
||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
|
||||
</div>
|
||||
<div class="symbol"><app-time kind="until" [time]="epochData.timeUntilHalving" [fastRender]="true"></app-time></div>
|
||||
<div class="symbol"><app-time kind="until" [time]="epochData.timeUntilHalving" [fastRender]="true" [precision]="1"></app-time></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<div class="difficulty-stats">
|
||||
<div class="item">
|
||||
<div class="card-text">
|
||||
~<app-time [time]="epochData.timeAvg / 1000" [forceFloorOnTimeIntervals]="['minute']" [fractionDigits]="1"></app-time>
|
||||
~<app-time [time]="epochData.timeAvg / 1000" [fractionDigits]="1"></app-time>
|
||||
</div>
|
||||
<div class="symbol" i18n="difficulty-box.average-block-time">Average block time</div>
|
||||
</div>
|
||||
@@ -68,7 +68,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="card-text"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time></div>
|
||||
<div class="card-text"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true" [precision]="1"></app-time></div>
|
||||
<div class="symbol">
|
||||
{{ epochData.retargetDateString }}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
@media (min-width: 465px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.main-title {
|
||||
@@ -18,16 +21,19 @@
|
||||
}
|
||||
|
||||
.full-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
min-height: 500px;
|
||||
height: calc(100% - 150px);
|
||||
@media (max-width: 992px) {
|
||||
padding-bottom: 100px;
|
||||
};
|
||||
height: calc(100vh - 250px);
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-bottom: 20px;
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
@media (min-width: 465px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.main-title {
|
||||
@@ -18,18 +21,20 @@
|
||||
}
|
||||
|
||||
.full-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
min-height: 500px;
|
||||
height: calc(100% - 150px);
|
||||
@media (max-width: 992px) {
|
||||
padding-bottom: 100px;
|
||||
};
|
||||
height: calc(100vh - 250px);
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-bottom: 20px;
|
||||
padding-right: 10px;
|
||||
@media (max-width: 992px) {
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</ng-container>
|
||||
</a>
|
||||
|
||||
<div ngbDropdown (window:resize)="onResize($event)" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
|
||||
<div ngbDropdown (window:resize)="onResize()" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
|
||||
<app-svg-images [name]="network.val === '' ? 'liquid' : network.val" width="22" height="22" viewBox="0 0 125 125" style="width: 30px; height: 30px; margin-right: 5px;"></app-svg-images>
|
||||
</button>
|
||||
@@ -92,9 +92,10 @@
|
||||
|
||||
<app-testnet-alert *ngIf="network.val === 'liquidtestnet'"></app-testnet-alert>
|
||||
|
||||
<br />
|
||||
<main>
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
<app-global-footer *ngIf="footerVisible"></app-global-footer>
|
||||
|
||||
<br>
|
||||
</ng-container>
|
||||
@@ -19,6 +19,7 @@ export class LiquidMasterPageComponent implements OnInit {
|
||||
network$: Observable<string>;
|
||||
urlLanguage: string;
|
||||
networkPaths: { [network: string]: string };
|
||||
footerVisible = true;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
@@ -27,13 +28,18 @@ export class LiquidMasterPageComponent implements OnInit {
|
||||
private navigationService: NavigationService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
ngOnInit(): void {
|
||||
this.env = this.stateService.env;
|
||||
this.connectionState$ = this.stateService.connectionState$;
|
||||
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
||||
this.urlLanguage = this.languageService.getLanguageForUrl();
|
||||
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||
this.networkPaths = paths;
|
||||
if (paths.liquid.indexOf('docs') > -1) {
|
||||
this.footerVisible = false;
|
||||
} else {
|
||||
this.footerVisible = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,7 +47,7 @@ export class LiquidMasterPageComponent implements OnInit {
|
||||
this.navCollapsed = !this.navCollapsed;
|
||||
}
|
||||
|
||||
onResize(event: any) {
|
||||
onResize(): void {
|
||||
this.isMobile = window.innerWidth <= 767.98;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</ng-container>
|
||||
</a>
|
||||
|
||||
<div (window:resize)="onResize($event)" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
|
||||
<div (window:resize)="onResize()" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
|
||||
<app-svg-images [name]="network.val === '' ? 'bitcoin' : network.val" width="20" height="20" viewBox="0 0 65 65" style="width: 30px; height: 30px; margin-right: 5px;"></app-svg-images>
|
||||
</button>
|
||||
@@ -64,10 +64,9 @@
|
||||
|
||||
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert>
|
||||
|
||||
<br />
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
<br>
|
||||
<main>
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
<app-global-footer *ngIf="footerVisible"></app-global-footer>
|
||||
</ng-container>
|
||||
|
||||
@@ -20,6 +20,8 @@ export class MasterPageComponent implements OnInit {
|
||||
urlLanguage: string;
|
||||
subdomain = '';
|
||||
networkPaths: { [network: string]: string };
|
||||
networkPaths$: Observable<Record<string, string>>;
|
||||
footerVisible = true;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
@@ -28,7 +30,7 @@ export class MasterPageComponent implements OnInit {
|
||||
private navigationService: NavigationService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
ngOnInit(): void {
|
||||
this.env = this.stateService.env;
|
||||
this.connectionState$ = this.stateService.connectionState$;
|
||||
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
||||
@@ -36,6 +38,11 @@ export class MasterPageComponent implements OnInit {
|
||||
this.subdomain = this.enterpriseService.getSubdomain();
|
||||
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||
this.networkPaths = paths;
|
||||
if (paths.mainnet.indexOf('docs') > -1) {
|
||||
this.footerVisible = false;
|
||||
} else {
|
||||
this.footerVisible = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,7 +50,7 @@ export class MasterPageComponent implements OnInit {
|
||||
this.navCollapsed = !this.navCollapsed;
|
||||
}
|
||||
|
||||
onResize(event: any) {
|
||||
onResize(): void {
|
||||
this.isMobile = window.innerWidth <= 767.98;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
[blockLimit]="stateService.blockVSize"
|
||||
[orientation]="timeLtr ? 'right' : 'left'"
|
||||
[flip]="true"
|
||||
[pixelAlign]="pixelAlign"
|
||||
(txClickEvent)="onTxClick($event)"
|
||||
></app-block-overview-graph>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Router } from '@angular/router';
|
||||
})
|
||||
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
|
||||
@Input() index: number;
|
||||
@Input() pixelAlign: boolean = false;
|
||||
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
|
||||
|
||||
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
||||
@@ -99,7 +100,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
||||
const direction = (this.blockIndex == null || this.index < this.blockIndex) ? this.poolDirection : this.chainDirection;
|
||||
this.blockGraph.replace(delta.added, direction);
|
||||
} else {
|
||||
this.blockGraph.update(delta.added, delta.removed, blockMined ? this.chainDirection : this.poolDirection, blockMined);
|
||||
this.blockGraph.update(delta.added, delta.removed, delta.changed || [], blockMined ? this.chainDirection : this.poolDirection, blockMined);
|
||||
}
|
||||
|
||||
this.lastBlockHeight = this.stateService.latestBlockHeight;
|
||||
|
||||
@@ -1,40 +1,47 @@
|
||||
<ng-container *ngIf="(loadingBlocks$ | async) === false; else loadingBlocks">
|
||||
<div class="mempool-blocks-container" [class.time-ltr]="timeLtr" *ngIf="(difficultyAdjustments$ | async) as da;">
|
||||
<ng-container *ngIf="(loadingBlocks$ | async) === false; else loadingBlocks" [class.minimal]="minimal">
|
||||
<div class="mempool-blocks-container" [class.time-ltr]="timeLtr" [style.--block-size]="blockWidth+'px'" *ngIf="(difficultyAdjustments$ | async) as da;">
|
||||
<div class="flashing">
|
||||
<ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks$ | async" let-i="index" [ngForTrackBy]="trackByFn">
|
||||
<div @blockEntryTrigger [@.disabled]="i > 0 || !animateEntry" [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink">
|
||||
<div
|
||||
*ngIf="minimal && spotlight > 0 && spotlight === i + 1"
|
||||
class="spotlight-bottom"
|
||||
[style.right]="mempoolBlockStyles[i].right"
|
||||
></div>
|
||||
<div @blockEntryTrigger [@.disabled]="i > 0 || !animateEntry" [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" [class.hide-block]="count && i >= count" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink">
|
||||
<a draggable="false" [routerLink]="['/mempool-block/' | relativeUrl, i]"
|
||||
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
||||
<div class="block-body">
|
||||
<div [attr.data-cy]="'mempool-block-' + i + '-fees'" class="fees">
|
||||
~{{ projectedBlock.medianFee | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
</div>
|
||||
<div [attr.data-cy]="'mempool-block-' + i + '-fee-span'" class="fee-span">
|
||||
{{ projectedBlock.feeRange[0] | number:feeRounding }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
</div>
|
||||
<div *ngIf="showMiningInfo" class="block-size">
|
||||
<app-amount [attr.data-cy]="'mempool-block-' + i + '-total-fees'" [satoshis]="projectedBlock.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||
</div>
|
||||
<div *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'‎' + (projectedBlock.blockSize | bytes: 2)"></div>
|
||||
<div [attr.data-cy]="'mempool-block-' + i + '-transaction-count'" class="transaction-count">
|
||||
<ng-container *ngTemplateOutlet="projectedBlock.nTx === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: projectedBlock.nTx | number}"></ng-container>
|
||||
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
|
||||
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
|
||||
</div>
|
||||
<div [attr.data-cy]="'mempool-block-' + i + '-time'" class="time-difference" *ngIf="projectedBlock.blockVSize <= stateService.blockVSize; else mergedBlock">
|
||||
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeDiffMainnet">
|
||||
<app-time kind="until" [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
</ng-template>
|
||||
<ng-template #timeDiffMainnet>
|
||||
<app-time kind="until" [time]="da.timeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time>
|
||||
</ng-template>
|
||||
</div>
|
||||
<ng-template #mergedBlock>
|
||||
<div [attr.data-cy]="'mempool-block-' + i + '-blocks'" class="time-difference">
|
||||
<b>(<ng-container *ngTemplateOutlet="blocksPlural; context: {$implicit: projectedBlock.blockVSize / stateService.blockVSize | ceil }"></ng-container>)</b>
|
||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||
<ng-container *ngIf="!minimal">
|
||||
<div [attr.data-cy]="'mempool-block-' + i + '-fees'" class="fees">
|
||||
~{{ projectedBlock.medianFee | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div [attr.data-cy]="'mempool-block-' + i + '-fee-span'" class="fee-span">
|
||||
{{ projectedBlock.feeRange[0] | number:feeRounding }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
</div>
|
||||
<div *ngIf="showMiningInfo" class="block-size">
|
||||
<app-amount [attr.data-cy]="'mempool-block-' + i + '-total-fees'" [satoshis]="projectedBlock.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||
</div>
|
||||
<div *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'‎' + (projectedBlock.blockSize | bytes: 2)"></div>
|
||||
<div [attr.data-cy]="'mempool-block-' + i + '-transaction-count'" class="transaction-count">
|
||||
<ng-container *ngTemplateOutlet="projectedBlock.nTx === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: projectedBlock.nTx | number}"></ng-container>
|
||||
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
|
||||
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
|
||||
</div>
|
||||
<div [attr.data-cy]="'mempool-block-' + i + '-time'" class="time-difference" *ngIf="projectedBlock.blockVSize <= stateService.blockVSize; else mergedBlock">
|
||||
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeDiffMainnet">
|
||||
<app-time kind="until" [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
</ng-template>
|
||||
<ng-template #timeDiffMainnet>
|
||||
<app-time kind="until" [time]="da.timeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
</ng-template>
|
||||
</div>
|
||||
<ng-template #mergedBlock>
|
||||
<div [attr.data-cy]="'mempool-block-' + i + '-blocks'" class="time-difference">
|
||||
<b>(<ng-container *ngTemplateOutlet="blocksPlural; context: {$implicit: projectedBlock.blockVSize / stateService.blockVSize | ceil }"></ng-container>)</b>
|
||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
<span class="animated-border"></span>
|
||||
</div>
|
||||
@@ -45,10 +52,10 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-template #loadingBlocks>
|
||||
<div class="mempool-blocks-container" [class.time-ltr]="timeLtr">
|
||||
<div class="mempool-blocks-container" [class.time-ltr]="timeLtr" [style.--block-size]="blockWidth+'px'">
|
||||
<div class="flashing">
|
||||
<ng-template ngFor let-projectedBlock [ngForOf]="mempoolEmptyBlocks" let-i="index" [ngForTrackBy]="trackByFn">
|
||||
<div class="bitcoin-block text-center mempool-block" id="mempool-block-{{ i }}" [ngStyle]="mempoolEmptyBlockStyles[i]"></div>
|
||||
<div class="bitcoin-block text-center mempool-block" [class.hide-block]="count && i >= count" id="mempool-block-{{ i }}" [ngStyle]="mempoolEmptyBlockStyles[i]"></div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.bitcoin-block {
|
||||
width: 125px;
|
||||
height: 125px;
|
||||
transition: background 2s, right 2s, transform 1s;
|
||||
width: var(--block-size);
|
||||
height: var(--block-size);
|
||||
transition: background 2s, right 2s, transform 1s, opacity 1s;
|
||||
}
|
||||
|
||||
.block-size {
|
||||
@@ -14,6 +14,7 @@
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
--block-size: 125px;
|
||||
}
|
||||
|
||||
.flashing {
|
||||
@@ -66,11 +67,11 @@
|
||||
|
||||
.bitcoin-block::after {
|
||||
content: '';
|
||||
width: 125px;
|
||||
height: 24px;
|
||||
width: var(--block-size);
|
||||
height: calc(0.192 * var(--block-size));
|
||||
position:absolute;
|
||||
top: -24px;
|
||||
left: -20px;
|
||||
top: calc(-0.192 * var(--block-size));
|
||||
left: calc(-0.16 * var(--block-size));
|
||||
background-color: #232838;
|
||||
transform:skew(40deg);
|
||||
transform-origin:top;
|
||||
@@ -79,11 +80,11 @@
|
||||
|
||||
.bitcoin-block::before {
|
||||
content: '';
|
||||
width: 20px;
|
||||
height: 125px;
|
||||
width: calc(0.16 * var(--block-size));
|
||||
height: var(--block-size);
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: -20px;
|
||||
top: calc(-0.096 * var(--block-size));
|
||||
left: calc(-0.16 * var(--block-size));
|
||||
background-color: #191c27;
|
||||
z-index: -1;
|
||||
|
||||
@@ -100,6 +101,10 @@
|
||||
background-color: #2d2825;
|
||||
}
|
||||
|
||||
.mempool-block.hide-block {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.black-background {
|
||||
background-color: #11131f;
|
||||
z-index: 100;
|
||||
@@ -141,7 +146,7 @@
|
||||
|
||||
.bitcoin-block::before {
|
||||
transform: skewY(-50deg);
|
||||
left: 125px;
|
||||
left: var(--block-size);
|
||||
}
|
||||
.block-body {
|
||||
transform: scaleX(-1);
|
||||
@@ -152,4 +157,16 @@
|
||||
#arrow-up {
|
||||
transform: translateX(70px);
|
||||
}
|
||||
}
|
||||
|
||||
.spotlight-bottom {
|
||||
position: absolute;
|
||||
width: calc(0.6 * var(--block-size));
|
||||
height: calc(0.25 * var(--block-size));
|
||||
border-left: solid calc(0.3 * var(--block-size)) transparent;
|
||||
border-bottom: solid calc(0.3 * var(--block-size)) white;
|
||||
border-right: solid calc(0.3 * var(--block-size)) transparent;
|
||||
transform: translate(calc(-0.2 * var(--block-size)), calc(1.1 * var(--block-size)));
|
||||
border-radius: 2px;
|
||||
z-index: -1;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs';
|
||||
import { MempoolBlock } from '../../interfaces/websocket.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
@@ -23,7 +23,12 @@ import { animate, style, transition, trigger } from '@angular/animations';
|
||||
])],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
||||
export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() minimal: boolean = false;
|
||||
@Input() blockWidth: number = 125;
|
||||
@Input() count: number = null;
|
||||
@Input() spotlight: number = 0;
|
||||
|
||||
specialBlocks = specialBlocks;
|
||||
mempoolBlocks: MempoolBlock[] = [];
|
||||
mempoolEmptyBlocks: MempoolBlock[] = this.mountEmptyBlocks();
|
||||
@@ -48,8 +53,9 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
||||
timeLtr: boolean;
|
||||
animateEntry: boolean = false;
|
||||
|
||||
blockWidth = 125;
|
||||
blockPadding = 30;
|
||||
blockOffset: number = 155;
|
||||
blockPadding: number = 30;
|
||||
containerOffset: number = 40;
|
||||
arrowVisible = false;
|
||||
tabHidden = false;
|
||||
feeRounding = '1.0-0';
|
||||
@@ -218,6 +224,14 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.blockWidth && this.blockWidth) {
|
||||
this.blockPadding = 0.24 * this.blockWidth;
|
||||
this.containerOffset = 0.32 * this.blockWidth;
|
||||
this.blockOffset = this.blockWidth + this.blockPadding;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.markBlocksSubscription.unsubscribe();
|
||||
this.blockSubscription.unsubscribe();
|
||||
@@ -238,17 +252,24 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
||||
|
||||
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
|
||||
const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2;
|
||||
const blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding)));
|
||||
let blocksAmount;
|
||||
if (this.count) {
|
||||
blocksAmount = 8;
|
||||
} else {
|
||||
blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding)));
|
||||
}
|
||||
while (blocks.length > blocksAmount) {
|
||||
const block = blocks.pop();
|
||||
const lastBlock = blocks[blocks.length - 1];
|
||||
lastBlock.blockSize += block.blockSize;
|
||||
lastBlock.blockVSize += block.blockVSize;
|
||||
lastBlock.nTx += block.nTx;
|
||||
lastBlock.feeRange = lastBlock.feeRange.concat(block.feeRange);
|
||||
lastBlock.feeRange.sort((a, b) => a - b);
|
||||
lastBlock.medianFee = this.median(lastBlock.feeRange);
|
||||
lastBlock.totalFees += block.totalFees;
|
||||
if (!this.count) {
|
||||
const lastBlock = blocks[blocks.length - 1];
|
||||
lastBlock.blockSize += block.blockSize;
|
||||
lastBlock.blockVSize += block.blockVSize;
|
||||
lastBlock.nTx += block.nTx;
|
||||
lastBlock.feeRange = lastBlock.feeRange.concat(block.feeRange);
|
||||
lastBlock.feeRange.sort((a, b) => a - b);
|
||||
lastBlock.medianFee = this.median(lastBlock.feeRange);
|
||||
lastBlock.totalFees += block.totalFees;
|
||||
}
|
||||
}
|
||||
if (blocks.length) {
|
||||
blocks[blocks.length - 1].isStack = blocks[blocks.length - 1].blockVSize > this.stateService.blockVSize;
|
||||
@@ -294,14 +315,14 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
return {
|
||||
'right': 40 + index * 155 + 'px',
|
||||
'right': this.containerOffset + index * this.blockOffset + 'px',
|
||||
'background': backgroundGradients.join(',') + ')'
|
||||
};
|
||||
}
|
||||
|
||||
getStyleForMempoolEmptyBlock(index: number) {
|
||||
return {
|
||||
'right': 40 + index * 155 + 'px',
|
||||
'right': this.containerOffset + index * this.blockOffset + 'px',
|
||||
'background': '#554b45',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,8 +23,7 @@ import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/grap
|
||||
})
|
||||
export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
@Input() data: any[];
|
||||
@Input() limitFee = 350;
|
||||
@Input() limitFilterFee = 1;
|
||||
@Input() filterSize = 100000;
|
||||
@Input() height: number | string = 200;
|
||||
@Input() top: number | string = 20;
|
||||
@Input() right: number | string = 10;
|
||||
@@ -99,16 +98,20 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
generateArray(mempoolStats: OptimizedMempoolStats[]) {
|
||||
const finalArray: number[][][] = [];
|
||||
let finalArray: number[][][] = [];
|
||||
let feesArray: number[][] = [];
|
||||
const limitFeesTemplate = this.template === 'advanced' ? 26 : 20;
|
||||
for (let index = limitFeesTemplate; index > -1; index--) {
|
||||
let maxTier = 0;
|
||||
for (let index = 37; index > -1; index--) {
|
||||
feesArray = [];
|
||||
mempoolStats.forEach((stats) => {
|
||||
if (stats.vsizes[index] >= this.filterSize) {
|
||||
maxTier = Math.max(maxTier, index);
|
||||
}
|
||||
feesArray.push([stats.added * 1000, stats.vsizes[index] ? stats.vsizes[index] : 0]);
|
||||
});
|
||||
finalArray.push(feesArray);
|
||||
}
|
||||
this.feeLimitIndex = maxTier;
|
||||
finalArray.reverse();
|
||||
return finalArray;
|
||||
}
|
||||
@@ -121,7 +124,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
const newColors = [];
|
||||
for (let index = 0; index < series.length; index++) {
|
||||
const value = series[index];
|
||||
if (index >= this.feeLimitIndex) {
|
||||
if (index < this.feeLimitIndex) {
|
||||
newColors.push(this.chartColorsOrdered[index]);
|
||||
seriesGraph.push({
|
||||
zlevel: 0,
|
||||
@@ -178,7 +181,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
alwaysShowContent: false,
|
||||
position: (pos, params, el, elRect, size) => {
|
||||
const positions = { top: (this.template === 'advanced') ? 0 : -30 };
|
||||
positions[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 60;
|
||||
positions[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 100;
|
||||
return positions;
|
||||
},
|
||||
extraCssText: `width: ${(this.template === 'advanced') ? '275px' : '200px'};
|
||||
@@ -186,10 +189,19 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
border: none;
|
||||
box-shadow: none;`,
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
type: 'cross',
|
||||
label: {
|
||||
formatter: (params: any) => {
|
||||
if (params.axisDimension === 'y') {
|
||||
return this.vbytesPipe.transform(params.value, 2, 'vB', 'MvB', true)
|
||||
} else {
|
||||
return formatterXAxis(this.locale, this.windowPreference, params.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, params[0].axisValue);
|
||||
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, params[0].axisValue);
|
||||
const { totalValue, totalValueArray } = this.getTotalValues(params);
|
||||
const itemFormatted = [];
|
||||
let totalParcial = 0;
|
||||
@@ -371,17 +383,21 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
|
||||
orderLevels() {
|
||||
this.feeLevelsOrdered = [];
|
||||
for (let i = 0; i < feeLevels.length; i++) {
|
||||
if (feeLevels[i] === this.limitFilterFee) {
|
||||
this.feeLimitIndex = i;
|
||||
}
|
||||
if (feeLevels[i] <= this.limitFee) {
|
||||
let maxIndex = Math.min(feeLevels.length, this.feeLimitIndex);
|
||||
for (let i = 0; i < maxIndex; i++) {
|
||||
if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
|
||||
this.feeLevelsOrdered.push(`${(feeLevels[i] / 10).toFixed(1)} - ${(feeLevels[i + 1] / 10).toFixed(1)}`);
|
||||
if (i === maxIndex - 1) {
|
||||
this.feeLevelsOrdered.push(`${(feeLevels[i] / 10).toFixed(1)}+`);
|
||||
} else {
|
||||
this.feeLevelsOrdered.push(`${(feeLevels[i] / 10).toFixed(1)} - ${(feeLevels[i + 1] / 10).toFixed(1)}`);
|
||||
}
|
||||
} else {
|
||||
this.feeLevelsOrdered.push(`${feeLevels[i]} - ${feeLevels[i + 1]}`);
|
||||
if (i === maxIndex - 1) {
|
||||
this.feeLevelsOrdered.push(`${feeLevels[i]}+`);
|
||||
} else {
|
||||
this.feeLevelsOrdered.push(`${feeLevels[i]} - ${feeLevels[i + 1]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.chartColorsOrdered = chartColors.slice(0, this.feeLevelsOrdered.length);
|
||||
}
|
||||
|
||||
@@ -73,24 +73,4 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="pref-selectors">
|
||||
<div class="selector">
|
||||
<app-language-selector></app-language-selector>
|
||||
</div>
|
||||
<div class="selector">
|
||||
<app-fiat-selector></app-fiat-selector>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="terms-of-service">
|
||||
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
|
||||
|
|
||||
<a [routerLink]="['/privacy-policy']" i18n="shared.privacy-policy|Privacy Policy">Privacy Policy</a>
|
||||
|
|
||||
<a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
.dashboard-container {
|
||||
padding-bottom: 60px;
|
||||
text-align: center;
|
||||
margin-top: 0.5rem;
|
||||
@media (min-width: 992px) {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
.col {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
@@ -104,22 +100,3 @@
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.terms-of-service {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.pref-selectors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
.selector {
|
||||
margin-left: .5em;
|
||||
margin-bottom: .5em;
|
||||
&:first {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div [class]="!widget ? 'bottom-padding' : 'pb-0'" class="container pb-lg-0">
|
||||
<div [class]="!widget ? '' : 'pb-0'" class="container pb-lg-0">
|
||||
<div [class]="widget ? 'chart-widget' : 'chart'" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
@@ -136,7 +136,6 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100% - 140px);
|
||||
padding-bottom: 20px;
|
||||
@media (max-width: 992px) {
|
||||
height: calc(100% - 190px);
|
||||
};
|
||||
@@ -33,15 +34,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-padding {
|
||||
@media (max-width: 992px) {
|
||||
padding-bottom: 65px
|
||||
};
|
||||
@media (max-width: 576px) {
|
||||
padding-bottom: 65px
|
||||
};
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.pools-table th,
|
||||
.pools-table td {
|
||||
|
||||
@@ -153,7 +153,7 @@ export class SearchFormComponent implements OnInit {
|
||||
const matchesBlockHeight = this.regexBlockheight.test(searchText);
|
||||
const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText);
|
||||
const matchesBlockHash = this.regexBlockhash.test(searchText);
|
||||
const matchesAddress = this.regexAddress.test(searchText);
|
||||
const matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
|
||||
|
||||
if (matchesAddress && this.network === 'bisq') {
|
||||
searchText = 'B' + searchText;
|
||||
@@ -198,7 +198,7 @@ export class SearchFormComponent implements OnInit {
|
||||
const searchText = result || this.searchForm.value.searchText.trim();
|
||||
if (searchText) {
|
||||
this.isSearching = true;
|
||||
if (this.regexAddress.test(searchText)) {
|
||||
if (!this.regexTransaction.test(searchText) && this.regexAddress.test(searchText)) {
|
||||
this.navigate('/address/', searchText);
|
||||
} else if (this.regexBlockhash.test(searchText) || this.regexBlockheight.test(searchText)) {
|
||||
this.navigate('/block/', searchText);
|
||||
|
||||
@@ -84,8 +84,7 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="incoming-transactions-graph">
|
||||
<app-mempool-graph #mempoolgraph dir="ltr" [template]="'advanced'" [limitFee]="500"
|
||||
[limitFilterFee]="filterFeeIndex" [height]="500" [left]="65" [right]="10"
|
||||
<app-mempool-graph #mempoolgraph dir="ltr" [template]="'advanced'" [height]="500" [left]="65" [right]="10"
|
||||
[data]="mempoolStats && mempoolStats.length ? mempoolStats : null"></app-mempool-graph>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.container-graph {
|
||||
padding: 0px 15px 60px;
|
||||
padding: 0px 15px 10px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<div class="chart-holder">
|
||||
<app-mempool-graph
|
||||
[template]="'advanced'"
|
||||
[limitFee]="500"
|
||||
[height]="600"
|
||||
[left]="60"
|
||||
[right]="10"
|
||||
|
||||
@@ -10,6 +10,16 @@ import { dates } from '../../shared/i18n/dates';
|
||||
export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
interval: number;
|
||||
text: string;
|
||||
units: string[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
|
||||
precisionThresholds = {
|
||||
year: 100,
|
||||
month: 18,
|
||||
week: 12,
|
||||
day: 31,
|
||||
hour: 48,
|
||||
minute: 90,
|
||||
second: 90
|
||||
};
|
||||
intervals = {};
|
||||
|
||||
@Input() time: number;
|
||||
@@ -18,7 +28,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() fastRender = false;
|
||||
@Input() fixedRender = false;
|
||||
@Input() relative = false;
|
||||
@Input() forceFloorOnTimeIntervals: string[];
|
||||
@Input() precision: number = 0;
|
||||
@Input() fractionDigits: number = 0;
|
||||
|
||||
constructor(
|
||||
@@ -83,23 +93,24 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
let counter: number;
|
||||
for (const i in this.intervals) {
|
||||
if (this.kind !== 'until' || this.forceFloorOnTimeIntervals && this.forceFloorOnTimeIntervals.indexOf(i) > -1) {
|
||||
counter = Math.floor(seconds / this.intervals[i]);
|
||||
} else {
|
||||
counter = Math.round(seconds / this.intervals[i]);
|
||||
for (const [index, unit] of this.units.entries()) {
|
||||
let precisionUnit = this.units[Math.min(this.units.length - 1, index + this.precision)];
|
||||
counter = Math.floor(seconds / this.intervals[unit]);
|
||||
const precisionCounter = Math.floor(seconds / this.intervals[precisionUnit]);
|
||||
if (precisionCounter > this.precisionThresholds[precisionUnit]) {
|
||||
precisionUnit = unit;
|
||||
}
|
||||
let rounded = counter;
|
||||
if (this.fractionDigits) {
|
||||
const roundFactor = Math.pow(10,this.fractionDigits);
|
||||
rounded = Math.round((seconds / this.intervals[i]) * roundFactor) / roundFactor;
|
||||
}
|
||||
const dateStrings = dates(rounded);
|
||||
if (counter > 0) {
|
||||
let rounded = Math.round(seconds / this.intervals[precisionUnit]);
|
||||
if (this.fractionDigits) {
|
||||
const roundFactor = Math.pow(10,this.fractionDigits);
|
||||
rounded = Math.round((seconds / this.intervals[precisionUnit]) * roundFactor) / roundFactor;
|
||||
}
|
||||
const dateStrings = dates(rounded);
|
||||
switch (this.kind) {
|
||||
case 'since':
|
||||
if (counter === 1) {
|
||||
switch (i) { // singular (1 day)
|
||||
if (rounded === 1) {
|
||||
switch (precisionUnit) { // singular (1 day)
|
||||
case 'year': return $localize`:@@time-since:${dateStrings.i18nYear}:DATE: ago`; break;
|
||||
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonth}:DATE: ago`; break;
|
||||
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeek}:DATE: ago`; break;
|
||||
@@ -109,7 +120,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
case 'second': return $localize`:@@time-since:${dateStrings.i18nSecond}:DATE: ago`; break;
|
||||
}
|
||||
} else {
|
||||
switch (i) { // plural (2 days)
|
||||
switch (precisionUnit) { // plural (2 days)
|
||||
case 'year': return $localize`:@@time-since:${dateStrings.i18nYears}:DATE: ago`; break;
|
||||
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonths}:DATE: ago`; break;
|
||||
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeeks}:DATE: ago`; break;
|
||||
@@ -121,8 +132,8 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
break;
|
||||
case 'until':
|
||||
if (counter === 1) {
|
||||
switch (i) { // singular (In ~1 day)
|
||||
if (rounded === 1) {
|
||||
switch (precisionUnit) { // singular (In ~1 day)
|
||||
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYear}:DATE:`; break;
|
||||
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonth}:DATE:`; break;
|
||||
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeek}:DATE:`; break;
|
||||
@@ -132,7 +143,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSecond}:DATE:`;
|
||||
}
|
||||
} else {
|
||||
switch (i) { // plural (In ~2 days)
|
||||
switch (precisionUnit) { // plural (In ~2 days)
|
||||
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYears}:DATE:`; break;
|
||||
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonths}:DATE:`; break;
|
||||
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeeks}:DATE:`; break;
|
||||
@@ -144,8 +155,8 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
break;
|
||||
case 'span':
|
||||
if (counter === 1) {
|
||||
switch (i) { // singular (1 day)
|
||||
if (rounded === 1) {
|
||||
switch (precisionUnit) { // singular (1 day)
|
||||
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYear}:DATE:`; break;
|
||||
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonth}:DATE:`; break;
|
||||
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeek}:DATE:`; break;
|
||||
@@ -155,7 +166,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSecond}:DATE:`; break;
|
||||
}
|
||||
} else {
|
||||
switch (i) { // plural (2 days)
|
||||
switch (precisionUnit) { // plural (2 days)
|
||||
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYears}:DATE:`; break;
|
||||
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonths}:DATE:`; break;
|
||||
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeeks}:DATE:`; break;
|
||||
@@ -167,8 +178,8 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (counter === 1) {
|
||||
switch (i) { // singular (1 day)
|
||||
if (rounded === 1) {
|
||||
switch (precisionUnit) { // singular (1 day)
|
||||
case 'year': return dateStrings.i18nYear; break;
|
||||
case 'month': return dateStrings.i18nMonth; break;
|
||||
case 'week': return dateStrings.i18nWeek; break;
|
||||
@@ -178,7 +189,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
case 'second': return dateStrings.i18nSecond; break;
|
||||
}
|
||||
} else {
|
||||
switch (i) { // plural (2 days)
|
||||
switch (precisionUnit) { // plural (2 days)
|
||||
case 'year': return dateStrings.i18nYears; break;
|
||||
case 'month': return dateStrings.i18nMonths; break;
|
||||
case 'week': return dateStrings.i18nWeeks; break;
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<ng-template [ngIf]="transactionTime !== 0 && !replaced">
|
||||
<ng-template [ngIf]="transactionTime !== 0">
|
||||
<tr *ngIf="transactionTime === -1; else firstSeenTmpl">
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
@@ -109,10 +109,10 @@
|
||||
</ng-template>
|
||||
<ng-template #belowBlockLimit>
|
||||
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeEstimateDefault">
|
||||
<app-time kind="until" [time]="(60 * 1000 * this.mempoolPosition.block) + now" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time>
|
||||
<app-time kind="until" [time]="(60 * 1000 * this.mempoolPosition.block) + now" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
</ng-template>
|
||||
<ng-template #timeEstimateDefault>
|
||||
<app-time kind="until" *ngIf="(timeAvg$ | async) as timeAvg;" [time]="(timeAvg * this.mempoolPosition.block) + now + timeAvg" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time>
|
||||
<app-time kind="until" *ngIf="(timeAvg$ | async) as timeAvg;" [time]="(timeAvg * this.mempoolPosition.block) + now + timeAvg" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
@@ -461,12 +461,8 @@
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
<br>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<ng-template #feeTable>
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
|
||||
@@ -216,7 +216,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.error = undefined;
|
||||
this.waitingForTransaction = false;
|
||||
this.graphExpanded = false;
|
||||
this.transactionTime = 0;
|
||||
this.transactionTime = tx.firstSeen || 0;
|
||||
this.setupGraph();
|
||||
|
||||
this.fetchRbfHistory$.next(this.tx.txid);
|
||||
@@ -338,10 +338,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (tx.firstSeen) {
|
||||
this.transactionTime = tx.firstSeen;
|
||||
} else {
|
||||
this.transactionTime = 0;
|
||||
this.getTransactionTime();
|
||||
}
|
||||
} else {
|
||||
this.getTransactionTime();
|
||||
this.transactionTime = 0;
|
||||
}
|
||||
|
||||
if (this.tx?.status?.confirmed) {
|
||||
@@ -471,7 +471,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.apiService
|
||||
.getTransactionTimes$([this.tx.txid])
|
||||
.subscribe((transactionTimes) => {
|
||||
this.transactionTime = transactionTimes[0];
|
||||
if (transactionTimes?.length) {
|
||||
this.transactionTime = transactionTimes[0];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -114,7 +114,12 @@
|
||||
<ng-container *ngFor="let witness of vin.witness; index as i">
|
||||
<input type="checkbox" [id]="'tx' + vindex + 'witness' + i" style="display: none;">
|
||||
<p class="witness-item" [class.accordioned]="witness.length > 1000">
|
||||
{{ witness }}
|
||||
<ng-template [ngIf]="witness" [ngIfElse]="emptyWitnessItem">
|
||||
{{ witness }}
|
||||
</ng-template>
|
||||
<ng-template #emptyWitnessItem>
|
||||
<empty>
|
||||
</ng-template>
|
||||
</p>
|
||||
<div class="witness-toggle" *ngIf="witness.length > 1000">
|
||||
<span class="ellipsis">...</span>
|
||||
|
||||
Reference in New Issue
Block a user