diff --git a/client/src/components/SearchBar.svelte b/client/src/components/SearchBar.svelte new file mode 100644 index 0000000..62acf43 --- /dev/null +++ b/client/src/components/SearchBar.svelte @@ -0,0 +1,151 @@ + + + + +
+
+ +
+
+
diff --git a/client/src/components/Settings.svelte b/client/src/components/Settings.svelte index 3fd516e..ea16b82 100644 --- a/client/src/components/Settings.svelte +++ b/client/src/components/Settings.svelte @@ -52,6 +52,10 @@ let settingConfig = { falseLabel: 'age', trueLabel: 'fee rate', valueType: 'bool' + }, + showSearch: { + label: 'Search Bar', + valueType: 'bool' } } $: { diff --git a/client/src/components/Sidebar.svelte b/client/src/components/Sidebar.svelte index 09c0c35..64af8dd 100644 --- a/client/src/components/Sidebar.svelte +++ b/client/src/components/Sidebar.svelte @@ -15,7 +15,7 @@ import atIcon from '../assets/icon/cil-at.svg' import gridIcon from '../assets/icon/grid-icon.svg' import peopleIcon from '../assets/icon/cil-people.svg' import giftIcon from '../assets/icon/cil-gift.svg' -import searchIcon from '../assets/icon/cil-search.svg' +import bookmarkIcon from '../assets/icon/cil-bookmark.svg' import MempoolLegend from '../components/MempoolLegend.svelte' import ContactTab from '../components/ContactTab.svelte' import SearchTab from '../components/SearchTab.svelte' @@ -122,7 +122,7 @@ function showBlock () { {settings('search')}} tooltip="Search & Highlight" bind:this={searchTabComponent}> - +
diff --git a/client/src/components/TransactionOverlay.svelte b/client/src/components/TransactionOverlay.svelte index a80738f..735cb29 100644 --- a/client/src/components/TransactionOverlay.svelte +++ b/client/src/components/TransactionOverlay.svelte @@ -2,11 +2,12 @@ import Overlay from '../components/Overlay.svelte' import Icon from './Icon.svelte' import BookmarkIcon from '../assets/icon/cil-bookmark.svg' -import { longBtcFormat, numberFormat, feeRateFormat } from '../utils/format.js' -import { exchangeRates, settings, sidebarToggle, newHighlightQuery, highlightingFull, detailTx, pageWidth } from '../stores.js' +import { longBtcFormat, numberFormat, feeRateFormat, dateFormat } from '../utils/format.js' +import { exchangeRates, settings, sidebarToggle, newHighlightQuery, highlightingFull, detailTx, pageWidth, latestBlockHeight, highlightInOut } from '../stores.js' import { formatCurrency } from '../utils/fx.js' import { hlToHex, mixColor, teal, purple } from '../utils/color.js' import { SPKToAddress } from '../utils/encodings.js' +import { searchTx } from '../utils/search.js' function onClose () { $detailTx = null @@ -16,7 +17,7 @@ function formatBTC (sats) { return `₿ ${(sats/100000000).toFixed(8)}` } -function highlight (query) { +function addToWatchlist (query) { if (!$highlightingFull && query) { $newHighlightQuery = query $sidebarToggle = 'search' @@ -43,17 +44,26 @@ $: { } } +let confirmations = 0 +$: { + if ($detailTx && $detailTx.block && $detailTx.block.height && $latestBlockHeight != null) { + confirmations = (1 + $latestBlockHeight - $detailTx.block.height) + } +} + const midColor = hlToHex(mixColor(teal, purple, 1, 3, 2)) let feeColor $: { if ($detailTx && $detailTx.feerate != null) { feeColor = hlToHex(mixColor(teal, purple, 1, Math.log2(64), Math.log2($detailTx.feerate))) + } else { + feeColor = null } } function expandAddresses(items, truncate) { let truncated = truncate ? items.slice(0,100) : items - const expanded = truncated.map(item => { + const expanded = truncated.map((item, index) => { let address = 'unknown' let title = null if (item.script_pub_key) { @@ -68,6 +78,7 @@ function expandAddresses(items, truncate) { ...item, address, title, + index, opreturn: (address === 'OP_RETURN') } }) @@ -110,6 +121,17 @@ $: { } else outputs = [] } +let highlight = {} +$: { + if ($highlightInOut && $detailTx && $highlightInOut.txid === $detailTx.id) { + highlight = {} + if ($highlightInOut.input != null) highlight.in = $highlightInOut.input + if ($highlightInOut.output != null) highlight.out = $highlightInOut.output + } else { + highlight = {} + } +} + let sankeyLines let sankeyHeight $: { @@ -230,6 +252,8 @@ function getMiterOffset (weight, dy, dx) { function clickItem (item) { if (item.rest) { truncate = false + } else if (item.prev_txid && item.prev_vout != null) { + searchTx(item.prev_txid, null, item.prev_vout) } } @@ -270,6 +294,20 @@ function clickItem (item) { } } + .confirmation-badge { + background: var(--light-good); + padding: 4px 8px; + border-radius: 8px; + float: right; + margin: 5px; + color: white; + font-weight: bold; + + &.unconfirmed { + background: var(--light-ok); + } + } + .pane { background: var(--palette-b); padding: 16px; @@ -293,7 +331,7 @@ function clickItem (item) { } } - .fee-calc { + .fields { align-items: center; display: flex; flex-direction: row; @@ -372,6 +410,9 @@ function clickItem (item) { .entry { align-items: flex-start; padding-left: 10px; + &.highlight { + background: linear-gradient(90deg, var(--bold-a) -100%, transparent 100%); + } &:hover { background: linear-gradient(90deg, var(--palette-e), transparent); } @@ -384,6 +425,9 @@ function clickItem (item) { &.outputs { .entry { padding-right: 10px; + &.highlight { + background: linear-gradient(90deg, transparent 0%, var(--bold-a) 200%); + } &:hover { background: linear-gradient(-90deg, var(--palette-e), transparent); } @@ -403,7 +447,7 @@ function clickItem (item) { } @media (max-width: 679px) { - .fee-calc { + .fields { flex-direction: column; } } @@ -430,12 +474,34 @@ function clickItem (item) { {#if $detailTx}
-
highlight($detailTx.id)} title="Add transaction to watchlist"> +
addToWatchlist($detailTx.id)} title="Add transaction to watchlist">
- {#if $detailTx.isCoinbase } -

Coinbase { $detailTx.id }

-
+ {#if $detailTx.block && $latestBlockHeight != null} + + {numberFormat.format(confirmations)} confirmation{confirmations == 1 ? '' : 's'} + + {:else} + + unconfirmed + + {/if} +

{#if $detailTx.isCoinbase }Coinbase{:else}Transaction{/if} { $detailTx.id }

+ {#if $detailTx.block} +
+
+ confirmed + { dateFormat.format($detailTx.block.time) } +
+ +
+ block height + { numberFormat.format($detailTx.block.height) } +
+
+ {/if} + {#if $detailTx.isCoinbase} +
block subsidy { formatBTC($detailTx.coinbase.subsidy) } @@ -452,16 +518,15 @@ function clickItem (item) {
-
+
coinbase { $detailTx.coinbase.sigAscii }
{:else} -

Transaction { $detailTx.id }

{#if $detailTx.is_inflated && $detailTx.fee != null && $detailTx.feerate != null} -
+
fee { numberFormat.format($detailTx.fee) } sats @@ -478,7 +543,7 @@ function clickItem (item) {
{:else} -
+
size { numberFormat.format($detailTx.vbytes) } vbytes @@ -494,12 +559,11 @@ function clickItem (item) {
{/if} -

Inputs & Outputs

{$detailTx.inputs.length} input{$detailTx.inputs.length > 1 ? 's' : ''}

{#each inputs as input} -
clickItem(input)}> +
clickItem(input)}>

{input.address.slice(0,-6)}{input.address.slice(-6)}

{ input.value == null ? '???' : formatBTC(input.value) }

@@ -535,7 +599,7 @@ function clickItem (item) {

{$detailTx.outputs.length} output{$detailTx.outputs.length > 1 ? 's' : ''} {#if $detailTx.fee != null}+ fee{/if}

{#each outputs as output} -
clickItem(output)}> +
clickItem(output)}>

{output.address.slice(0,-6)}{output.address.slice(-6)}

{ output.value == null ? '???' : formatBTC(output.value) }

diff --git a/client/src/components/TxViz.svelte b/client/src/components/TxViz.svelte index 0606940..de6610b 100644 --- a/client/src/components/TxViz.svelte +++ b/client/src/components/TxViz.svelte @@ -3,8 +3,9 @@ import TxController from '../controllers/TxController.js' import TxRender from './TxRender.svelte' import getTxStream from '../controllers/TxStream.js' - import { settings, overlay, serverConnected, serverDelay, txCount, mempoolCount, mempoolScreenHeight, frameRate, avgFrameRate, blockVisible, tinyScreen, currentBlock, selectedTx, blockAreaSize, devEvents, devSettings, pageWidth } from '../stores.js' + import { settings, overlay, serverConnected, serverDelay, txCount, mempoolCount, mempoolScreenHeight, frameRate, avgFrameRate, blockVisible, tinyScreen, compactScreen, currentBlock, selectedTx, blockAreaSize, devEvents, devSettings, pageWidth, pageHeight } from '../stores.js' import BlockInfo from '../components/BlockInfo.svelte' + import SearchBar from '../components/SearchBar.svelte' import TxInfo from '../components/TxInfo.svelte' import Sidebar from '../components/Sidebar.svelte' import TransactionOverlay from '../components/TransactionOverlay.svelte' @@ -79,6 +80,7 @@ function resize () { $pageWidth = window.innerWidth + $pageHeight = window.innerHeight if (width !== window.innerWidth - 20 || height !== window.innerHeight - 20) { // don't force resize unless the viewport has actually changed width = window.innerWidth - 20 @@ -88,12 +90,6 @@ height }) } - const aspectRatio = window.innerWidth / window.innerHeight - if ((aspectRatio >= 1 && window.innerWidth < 480) || (aspectRatio <= 1 && window.innerHeight < 480)) { - $tinyScreen = true - } else { - $tinyScreen = false - } } function changedMode () { @@ -284,7 +280,9 @@ .status { text-align: left; padding: 1rem; - flex-shrink: 0; + width: 20em; + min-width: 7.5em; + flex-shrink: 3; box-sizing: border-box; .row { @@ -340,7 +338,19 @@ } } + .search-bar-wrapper { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + height: 3.5em; + flex-grow: 1; + } + .alert-bar-wrapper { + width: 20em; + flex-shrink: 0; + } .block-area-wrapper { height: 100%; @@ -415,6 +425,15 @@ margin: auto; } } + + @media screen and (max-width: 640px) { + .search-bar-wrapper { + position: fixed; + top: 3.5em; + left: 0; + right: 0; + } + } @@ -437,7 +456,7 @@
-
+
@@ -471,10 +490,18 @@ {/if}
-
- {#if config.messagesEnabled && $settings.showMessages && !$tinyScreen } - + {#if $settings.showSearch && !$tinyScreen && !$compactScreen } +
+ +
{/if} +
+ {#if config.messagesEnabled && $settings.showMessages && !$tinyScreen } + + {:else} +
+ {/if} +
diff --git a/client/src/components/alert/Alerts.svelte b/client/src/components/alert/Alerts.svelte index 8d577ea..25280f1 100644 --- a/client/src/components/alert/Alerts.svelte +++ b/client/src/components/alert/Alerts.svelte @@ -62,7 +62,7 @@ function processAlert (alert) { } else return null } -let activeAlerts = [{ key: 'null1' }, { key: 'null2' }] +let alert let lastIndex = -1 onMount(() => { @@ -72,9 +72,8 @@ onMount(() => { function startAlerts () { if (!rotating && processedAlerts && processedAlerts.length) { rotating = true - activeAlerts[0] = processedAlerts[0] || { key: 'null1' } - activeAlerts[1] = processedAlerts[1] || { key: 'null2' } - lastIndex = processedAlerts[1] ? 1 : 0 + alert = processedAlerts[0] || { key: 'null1' } + lastIndex = 0 if (rotateTimer) clearTimeout(rotateTimer) rotateTimer = setTimeout(rotateAlerts, config.alertDuration) } @@ -84,40 +83,21 @@ let rotateTimer function rotateAlerts () { if (rotateTimer) clearTimeout(rotateTimer) - if (processedAlerts && processedAlerts.length > 2) { + if (processedAlerts && processedAlerts.length > 1) { // find the next alert in the queue - let currentIndex = -1 - if (activeAlerts[1]) { - currentIndex = processedAlerts.findIndex(alert => { alert.key === activeAlerts[1].key}) - } - if (currentIndex < 0) currentIndex = lastIndex - currentIndex = (currentIndex + 1) % processedAlerts.length - // roll over to the next alert if there's a key clash - if (processedAlerts[currentIndex].key === activeAlerts[1].key) { - currentIndex = (currentIndex + 1) % processedAlerts.length - } + let currentIndex = (lastIndex + 1) % processedAlerts.length lastIndex = currentIndex - let nextAlert = processedAlerts[currentIndex] - if (nextAlert) - activeAlerts[0] = activeAlerts[1] - activeAlerts[1] = { key: 'temp' } - setTimeout(() => { - activeAlerts[1] = nextAlert - sequences[alert.key]++ - }, 1000) - } else if (processedAlerts) { - activeAlerts[0] = processedAlerts[0] || { key: 'null1' } - activeAlerts[1] = processedAlerts[1] || { key: 'null2' } + alert = processedAlerts[currentIndex] } rotateTimer = setTimeout(rotateAlerts, config.alertDuration) } -
- {#each activeAlerts as alert (alert.key)} -
+
+ {#key alert && alert.key} +
{#if alert && alert.component } {#if alert.href} @@ -134,15 +114,13 @@ function rotateAlerts () { {/if} {/if}
- {/each} + {/key}