Fix block info transitions

This commit is contained in:
Mononaut 2022-04-26 15:59:46 -06:00
parent c4c32dfa89
commit 5cad68a2a7
6 changed files with 148 additions and 81 deletions

View File

@ -62,18 +62,24 @@
} }
} }
let transitionDirection
let flyIn let flyIn
let flyOut let flyOut
$: { $: {
if ($blockTransitionDirection && $blockTransitionDirection === 'right') { if (!$blockTransitionDirection || !visible || !block || !$blocksEnabled) {
transitionDirection = 'up'
flyIn = { y: (restoring ? -50 : 50), duration: (restoring ? 500 : 1000), easing: linear, delay: (restoring ? 0 : newBlockDelay) }
flyOut = { y: -50, duration: 2000, easing: linear }
} else if ($blockTransitionDirection && $blockTransitionDirection === 'right') {
transitionDirection = 'right'
flyIn = { x: 100, easing: linear, delay: 1000, duration: 1000 } flyIn = { x: 100, easing: linear, delay: 1000, duration: 1000 }
flyOut = { x: -100, easing: linear, delay: 0, duration: 1000 } flyOut = { x: -100, easing: linear, delay: 0, duration: 1000 }
} else if ($blockTransitionDirection && $blockTransitionDirection === 'left') { } else if ($blockTransitionDirection && $blockTransitionDirection === 'left') {
transitionDirection = 'left'
flyIn = { x: -100, easing: linear, delay: 1000, duration: 1000 } flyIn = { x: -100, easing: linear, delay: 1000, duration: 1000 }
flyOut = { x: 100, easing: linear, delay: 0, duration: 1000 } flyOut = { x: 100, easing: linear, delay: 0, duration: 1000 }
} else { } else {
flyIn = { y: (restoring ? -50 : 50), duration: (restoring ? 500 : 1000), easing: linear, delay: (restoring ? 0 : newBlockDelay) } transitionDirection = 'down'
flyOut = { y: -50, duration: 2000, easing: linear }
} }
} }
@ -155,6 +161,14 @@
} }
} }
.block-info-container {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.block-info { .block-info {
position: absolute; position: absolute;
bottom: calc(100% + 0.25rem); bottom: calc(100% + 0.25rem);
@ -319,68 +333,71 @@
} }
</style> </style>
{#key block} {#key transitionDirection}
{#if block != null && visible && $blocksEnabled } {#each ((block != null && visible && $blocksEnabled) ? [block] : []) as block (block.id)}
<div class="block-info" out:fly={flyOut} in:fly={flyIn}> <div class="block-info-container" out:fly|local={flyOut} in:fly|local={flyIn}>
<!-- <span class="data-field">Hash: { block.id }</span> --> <div class="block-info">
<div class="full-size"> <!-- <span class="data-field">Hash: { block.id }</span> -->
<div class="data-row"> <div class="full-size">
<span class="data-field title-field" title="{block.miner_sig}"><b>{#if block.height == $latestBlockHeight}Latest {/if}Block: </b>{ numberFormat.format(block.height) }</span> <div class="data-row">
<button class="data-field close-button" on:click={hideBlock}><Icon icon={closeIcon} color="var(--palette-x)" /></button> <span class="data-field title-field" title="{block.miner_sig}"><b>{#if block.height == $latestBlockHeight}Latest {/if}Block: </b>{ numberFormat.format(block.height) }</span>
</div> <button class="data-field close-button" on:click={hideBlock}><Icon icon={closeIcon} color="var(--palette-x)" /></button>
<div class="data-row"> </div>
<span class="data-field" title="block timestamp">{ formatDateTime(block.time) }</span> <div class="data-row">
<span class="data-field">{ formattedBlockValue }</span> <span class="data-field" title="block timestamp">{ formatDateTime(block.time) }</span>
</div> <span class="data-field">{ formattedBlockValue }</span>
<div class="data-row"> </div>
<span class="data-field">{ formatBytes(block.bytes) }</span> <div class="data-row">
<span class="data-field">{ formatCount(block.txnCount) } transactions</span>
</div>
<div class="data-row spacer">&nbsp;</div>
<div class="data-row">
<span class="data-field">Avg fee rate</span>
{#if block.fees != null}
<span class="data-field">{ formatFee(block.avgFeerate) } sats/vbyte</span>
{:else}
<span class="data-field">unavailable</span>
{/if}
</div>
</div>
<div class="compact">
<div class="data-row">
<span class="data-field title-field" title="{block.miner_sig}"><b>Latest Block: </b>{ numberFormat.format(block.height) }</span>
<button class="data-field close-button" on:click={hideBlock}><Icon icon={closeIcon} color="var(--palette-x)" /></button>
</div>
<div class="data-row">
<span class="data-field">{ formatDateTime(block.time) }</span>
<span class="data-field">{ formattedBlockValue }</span>
</div>
<div class="data-row">
<span class="data-field">{ formatCount(block.txnCount) } transactions</span>
{#if block.fees != null}
<span class="data-field">{ formatFee(block.avgFeerate) } sats/vb</span>
{:else}
<span class="data-field">{ formatBytes(block.bytes) }</span> <span class="data-field">{ formatBytes(block.bytes) }</span>
{/if} <span class="data-field">{ formatCount(block.txnCount) } transaction{block.txnCount == 1 ? '' : 's'}</span>
</div>
<div class="data-row spacer">&nbsp;</div>
<div class="data-row">
<span class="data-field">Avg fee rate</span>
{#if block.fees != null}
<span class="data-field">{ formatFee(block.avgFeerate) } sats/vbyte</span>
{:else}
<span class="data-field">unavailable</span>
{/if}
</div>
</div> </div>
</div> <div class="compact">
<div class="data-row">
<span class="data-field title-field" title="{block.miner_sig}"><b>Latest Block: </b>{ numberFormat.format(block.height) }</span>
<button class="data-field close-button" on:click={hideBlock}><Icon icon={closeIcon} color="var(--palette-x)" /></button>
</div>
<div class="data-row">
<span class="data-field">{ formatDateTime(block.time) }</span>
<span class="data-field">{ formattedBlockValue }</span>
</div>
<div class="data-row">
<span class="data-field">{ formatCount(block.txnCount) } transactions</span>
{#if block.fees != null}
<span class="data-field">{ formatFee(block.avgFeerate) } sats/vb</span>
{:else}
<span class="data-field">{ formatBytes(block.bytes) }</span>
{/if}
</div>
</div>
</div>
{#if hasPrevBlock }
<a href="/block/height/{block.height - 1}" on:click={explorePrevBlock} class="explore-button prev">
<svg class="chevron left" height="1.5em" width="1.5em" viewBox="0 0 512 512">
<path d="M 107.628,257.54 327.095,38.078 404,114.989 261.506,257.483 404,399.978 327.086,476.89 Z" class="outline" />
</svg>
</a>
{/if}
{#if hasNextBlock }
<a href="/block/height/{block.height + 1}" on:click={exploreNextBlock} class="explore-button next">
<svg class="chevron right" height="1.5em" width="1.5em" viewBox="0 0 512 512">
<path d="M 107.628,257.54 327.095,38.078 404,114.989 261.506,257.483 404,399.978 327.086,476.89 Z" class="outline" />
</svg>
</a>
{/if}
<button class="close-button standalone" on:click={hideBlock}>
<Icon icon={closeIcon} color="var(--palette-x)" />
</button>
</div> </div>
{#if hasPrevBlock } {/each}
<a href="/block/height/{block.height - 1}" on:click={explorePrevBlock} class="explore-button prev" out:fly={flyOut} in:fly={flyIn}>
<svg class="chevron left" height="1.5em" width="1.5em" viewBox="0 0 512 512">
<path d="M 107.628,257.54 327.095,38.078 404,114.989 261.506,257.483 404,399.978 327.086,476.89 Z" class="outline" />
</svg>
</a>
{/if}
{#if hasNextBlock }
<a href="/block/height/{block.height + 1}" on:click={exploreNextBlock} class="explore-button next" out:fly={flyOut} in:fly={flyIn}>
<svg class="chevron right" height="1.5em" width="1.5em" viewBox="0 0 512 512">
<path d="M 107.628,257.54 327.095,38.078 404,114.989 261.506,257.483 404,399.978 327.086,476.89 Z" class="outline" />
</svg>
</a>
{/if}
<button class="close-button standalone" on:click={hideBlock} out:fly={flyOut} in:fly={flyIn} >
<Icon icon={closeIcon} color="var(--palette-x)" />
</button>
{/if}
{/key} {/key}

View File

@ -219,7 +219,7 @@ async function searchSubmit (e) {
<div class="input-wrapper" transition:fly={{ y: -25 }}> <div class="input-wrapper" transition:fly={{ y: -25 }}>
<form class="search-form" action="" on:submit={searchSubmit}> <form class="search-form" action="" on:submit={searchSubmit}>
<input class="search-input" type="text" bind:value={query} placeholder="Enter a txid"> <input class="search-input" type="text" bind:value={query} placeholder="txid, block id or block height">
<div class="clear-button" class:disabled={query == null || query === ''} on:click={clearInput} title="Clear"> <div class="clear-button" class:disabled={query == null || query === ''} on:click={clearInput} title="Clear">
<Icon icon={CrossIcon}/> <Icon icon={CrossIcon}/>
</div> </div>

View File

@ -3,12 +3,12 @@ import Overlay from '../components/Overlay.svelte'
import Icon from './Icon.svelte' import Icon from './Icon.svelte'
import BookmarkIcon from '../assets/icon/cil-bookmark.svg' import BookmarkIcon from '../assets/icon/cil-bookmark.svg'
import { longBtcFormat, numberFormat, feeRateFormat, dateFormat } from '../utils/format.js' import { longBtcFormat, numberFormat, feeRateFormat, dateFormat } from '../utils/format.js'
import { exchangeRates, settings, sidebarToggle, newHighlightQuery, highlightingFull, detailTx, pageWidth, latestBlockHeight, highlightInOut, loading, urlPath } from '../stores.js' import { exchangeRates, settings, sidebarToggle, newHighlightQuery, highlightingFull, detailTx, pageWidth, latestBlockHeight, highlightInOut, loading, urlPath, currentBlock, overlay, explorerBlockData } from '../stores.js'
import { formatCurrency } from '../utils/fx.js' import { formatCurrency } from '../utils/fx.js'
import { hlToHex, mixColor, teal, purple } from '../utils/color.js' import { hlToHex, mixColor, teal, purple } from '../utils/color.js'
import { SPKToAddress } from '../utils/encodings.js' import { SPKToAddress } from '../utils/encodings.js'
import api from '../utils/api.js' import api from '../utils/api.js'
import { searchTx } from '../utils/search.js' import { searchTx, searchBlockHash, searchBlockHeight } from '../utils/search.js'
import { fade } from 'svelte/transition' import { fade } from 'svelte/transition'
function onClose () { function onClose () {
@ -259,14 +259,6 @@ function getMiterOffset (weight, dy, dx) {
async function clickItem (item) { async function clickItem (item) {
if (item.rest) { if (item.rest) {
truncate = false truncate = false
} else if (item.prev_txid && item.prev_vout != null) {
// $loading++
// await searchTx(item.prev_txid, null, item.prev_vout)
// $loading--
} else if (item.spend) {
// $loading++
// await searchTx(item.spend.txid, item.spend.vin)
// $loading--
} }
} }
@ -283,6 +275,26 @@ async function goToOutput(e, output) {
await searchTx(output.spend.txid, output.spend.vin) await searchTx(output.spend.txid, output.spend.vin)
loading.decrement() loading.decrement()
} }
async function goToBlock(e) {
e.preventDefault()
// ignore click if it was triggered while selecting text, or if we don't have a block to go to
if (!$detailTx || !$detailTx.block || !!window.getSelection().toString()) return
let hash = $detailTx.block.hash || $detailTx.block.id
let height = $detailTx.block.height
if (hash === $currentBlock.id) {
$overlay = null
} else if (height == $latestBlockHeight) {
$explorerBlockData = null
$overlay = null
} else if (hash) {
loading.increment()
await searchBlockHash($detailTx.block.hash || $detailTx.block.id)
loading.decrement()
}
}
</script> </script>
<style type="text/scss"> <style type="text/scss">
@ -362,6 +374,14 @@ async function goToOutput(e, output) {
word-break: break-all; word-break: break-all;
} }
} }
&.clickable {
cursor: pointer;
user-select: text;
&:hover {
background: var(--palette-a);
}
}
} }
.fields { .fields {
@ -562,7 +582,7 @@ async function goToOutput(e, output) {
{/if} {/if}
<h2><span class="title">{#if $detailTx.isCoinbase }Coinbase{:else}Transaction{/if}</span> <span class="tx-id">{ $detailTx.id }</span></h2> <h2><span class="title">{#if $detailTx.isCoinbase }Coinbase{:else}Transaction{/if}</span> <span class="tx-id">{ $detailTx.id }</span></h2>
{#if $detailTx.block} {#if $detailTx.block}
<div class="pane fields"> <a class="pane fields clickable" href="/block/{$detailTx.block.hash || $detailTx.block.id}" draggable="false" on:click={goToBlock}>
<div class="field"> <div class="field">
<span class="label">confirmed</span> <span class="label">confirmed</span>
<span class="value" style="color: {feeColor};">{ dateFormat.format($detailTx.block.time) }</span> <span class="value" style="color: {feeColor};">{ dateFormat.format($detailTx.block.time) }</span>
@ -572,7 +592,7 @@ async function goToOutput(e, output) {
<span class="label">block height</span> <span class="label">block height</span>
<span class="value" style="color: {feeColor};">{ numberFormat.format($detailTx.block.height) }</span> <span class="value" style="color: {feeColor};">{ numberFormat.format($detailTx.block.height) }</span>
</div> </div>
</div> </a>
{/if} {/if}
{#if $detailTx.isCoinbase} {#if $detailTx.isCoinbase}
<div class="pane fields"> <div class="pane fields">

View File

@ -8,6 +8,7 @@ import { FastVertexArray } from '../utils/memory.js'
import { searchTx, fetchSpends, addSpends } from '../utils/search.js' import { searchTx, fetchSpends, addSpends } from '../utils/search.js'
import { overlay, txCount, mempoolCount, mempoolScreenHeight, blockVisible, currentBlock, selectedTx, detailTx, blockAreaSize, highlight, colorMode, blocksEnabled, latestBlockHeight, explorerBlockData, blockTransitionDirection, loading, urlPath } from '../stores.js' import { overlay, txCount, mempoolCount, mempoolScreenHeight, blockVisible, currentBlock, selectedTx, detailTx, blockAreaSize, highlight, colorMode, blocksEnabled, latestBlockHeight, explorerBlockData, blockTransitionDirection, loading, urlPath } from '../stores.js'
import config from "../config.js" import config from "../config.js"
import { tick } from 'svelte';
export default class TxController { export default class TxController {
constructor ({ width, height }) { constructor ({ width, height }) {
@ -188,6 +189,22 @@ export default class TxController {
this.expiredTxs = {} this.expiredTxs = {}
if (this.explorerBlockScene && this.explorerBlock && this.explorerBlock.id === block.id) {
this.block = this.explorerBlock
this.blockScene = this.explorerBlockScene
this.explorerBlockScene = null
this.explorerBlock = null
urlPath.set("/")
for (let i = 0; i < block.txns.length; i++) {
this.txs[block.txns[i].id].setData(block.txns[i])
this.poolScene.remove(block.txns[i].id)
}
this.poolScene.layoutAll()
return
}
if (!this.explorerBlockScene) this.clearBlock() if (!this.explorerBlockScene) this.clearBlock()
if (this.blocksEnabled) { if (this.blocksEnabled) {
@ -268,8 +285,14 @@ export default class TxController {
return block return block
} }
exploreBlock (blockData) { async exploreBlock (blockData) {
const block = blockData.isBlock ? blockData : new BitcoinBlock(blockData) const block = blockData.isBlock ? blockData : new BitcoinBlock(blockData)
if (this.block && this.block.id === block.id) {
this.showBlock()
return
}
let enterFromRight = false let enterFromRight = false
// clean up previous block // clean up previous block
@ -311,10 +334,11 @@ export default class TxController {
} }
blockVisible.set(true) blockVisible.set(true)
await tick()
currentBlock.set(block) currentBlock.set(block)
} }
resumeLatest () { async resumeLatest () {
if (this.explorerBlock && this.explorerBlockScene) { if (this.explorerBlock && this.explorerBlockScene) {
const prevBlock = this.explorerBlock const prevBlock = this.explorerBlock
const prevBlockScene = this.explorerBlockScene const prevBlockScene = this.explorerBlockScene
@ -326,14 +350,16 @@ export default class TxController {
} }
if (this.blockScene && this.block) { if (this.blockScene && this.block) {
blockTransitionDirection.set('right') blockTransitionDirection.set('right')
await tick()
this.blockScene.enterRight() this.blockScene.enterRight()
currentBlock.set(this.block) currentBlock.set(this.block)
} }
} }
hideBlock () { async hideBlock () {
if (this.blockScene && !this.explorerBlockScene) { if (this.blockScene && !this.explorerBlockScene) {
blockTransitionDirection.set(null) blockTransitionDirection.set(null)
await tick()
this.blockScene.hide() this.blockScene.hide()
} }
} }

View File

@ -77,7 +77,7 @@ export default class TxBlockScene extends TxMondrianPoolScene {
}) })
} }
this.savePixelsToScreenPosition(tx, 0, this.hidden ? 50 : 0) this.savePixelsToScreenPosition(tx, 0, (this.hidden && !this.exited) ? 50 : 0)
if (this.hidden) { if (this.hidden) {
tx.view.update({ tx.view.update({
display: { display: {
@ -164,6 +164,7 @@ export default class TxBlockScene extends TxMondrianPoolScene {
enter (right) { enter (right) {
this.hidden = false this.hidden = false
this.exited = false
const ids = this.getActiveTxList() const ids = this.getActiveTxList()
for (let i = 0; i < ids.length; i++) { for (let i = 0; i < ids.length; i++) {
this.enterTx(this.txs[ids[i]], right) this.enterTx(this.txs[ids[i]], right)
@ -198,6 +199,7 @@ export default class TxBlockScene extends TxMondrianPoolScene {
exit (right) { exit (right) {
this.hidden = true this.hidden = true
this.exited = true
const ids = this.getActiveTxList() const ids = this.getActiveTxList()
for (let i = 0; i < ids.length; i++) { for (let i = 0; i < ids.length; i++) {
this.exitTx(this.txs[ids[i]], right) this.exitTx(this.txs[ids[i]], right)

View File

@ -226,6 +226,7 @@ export async function searchTx (txid, input, output) {
export async function searchBlockHash (hash) { export async function searchBlockHash (hash) {
urlPath.set(`/block/${hash}`) urlPath.set(`/block/${hash}`)
overlay.set(null)
try { try {
const searchResult = await fetchBlockByHash(hash) const searchResult = await fetchBlockByHash(hash)
if (searchResult) { if (searchResult) {
@ -244,6 +245,7 @@ export async function searchBlockHash (hash) {
export async function searchBlockHeight (height) { export async function searchBlockHeight (height) {
urlPath.set(`/block/height/${height}`) urlPath.set(`/block/height/${height}`)
overlay.set(null)
try { try {
const searchResult = await fetchBlockByHeight(height) const searchResult = await fetchBlockByHeight(height)
if (searchResult) { if (searchResult) {