feat(app): transaction detail overlay with full on-chain data
Double-clicking a history row opens an in-app overlay (same pattern as address/server/settings overlays) that shows all available data for a transaction. The overlay appears immediately with a spinner; data is fetched from the server in the background and rendered once ready. Backdrop tap and Esc close it; a pending fetch is cancelled. New Core type — TransactionInspector (Core/Wallet/): - FetchAsync() downloads the raw tx, all parent txs (parallel requests, one round-trip instead of N sequential), and the block header - Reconstructs inputs (amounts, addresses, is-mine), outputs, fee, virtual size, RBF flag, block timestamp, confirmations New App types: - TransactionDetails record: full parsed data (fee, net, I/O lists, counterparty addresses, coinbase detection…) - TransactionDetailsViewModel: pre-formats all strings in the current locale and unit (status, date, signed amounts, sat/vB rate…) - TxIoRow record: one row in the input/output tables - MineColorConverter: bool → brush (MediumSeaGreen for own addresses) ViewModel changes: - ShowTransactionDetailsAsync / CloseTransactionDetailsCommand - BuildTransactionDetailsAsync (also public for future RBF/CPFP use) - CancellationTokenSource so opening a new tx cancels the previous fetch - All server work runs on Task.Run to keep the UI thread free XAML: history rows get DoubleTapped handler, Hand cursor, hint label, txid now uses TextTrimming instead of SelectableTextBlock. i18n: 24 new tx.* / history.hint keys in all 6 languages.
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
@@ -345,6 +346,122 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
[RelayCommand]
|
||||
private void CloseAddressInfo() => AddressInfo = null;
|
||||
|
||||
// ---- overlay dettaglio transazione ----
|
||||
|
||||
/// <summary>Overlay dettaglio transazione aperto.</summary>
|
||||
[ObservableProperty]
|
||||
private bool isTxDetailsOpen;
|
||||
|
||||
/// <summary>Caricamento dei dati in corso (mostra lo spinner nell'overlay).</summary>
|
||||
[ObservableProperty]
|
||||
private bool isTxDetailsLoading;
|
||||
|
||||
/// <summary>Dati della transazione mostrata; null finché non sono pronti.</summary>
|
||||
[ObservableProperty]
|
||||
private TransactionDetailsViewModel? txDetails;
|
||||
|
||||
private CancellationTokenSource? _txDetailsCts;
|
||||
|
||||
/// <summary>
|
||||
/// Apre l'overlay dettaglio transazione: appare subito con lo spinner e i
|
||||
/// dati arrivano dal server in background. Overlay in-app (come indirizzo e
|
||||
/// impostazioni) per apertura/chiusura istantanee, senza una top-level window.
|
||||
/// </summary>
|
||||
public async Task ShowTransactionDetailsAsync(string txid)
|
||||
{
|
||||
if (_client is null || !_client.IsConnected)
|
||||
{
|
||||
StatusMessage = Loc.Tr("tx.needconnection");
|
||||
return;
|
||||
}
|
||||
|
||||
_txDetailsCts?.Cancel();
|
||||
_txDetailsCts = new CancellationTokenSource();
|
||||
var ct = _txDetailsCts.Token;
|
||||
|
||||
TxDetails = null;
|
||||
IsTxDetailsLoading = true;
|
||||
IsTxDetailsOpen = true;
|
||||
|
||||
var details = await BuildTransactionDetailsAsync(txid, ct);
|
||||
|
||||
// L'utente potrebbe aver chiuso l'overlay (o aperto un'altra tx) mentre
|
||||
// caricava: non sovrascrivere lo stato in quel caso.
|
||||
if (ct.IsCancellationRequested || !IsTxDetailsOpen)
|
||||
return;
|
||||
|
||||
if (details is null)
|
||||
{
|
||||
IsTxDetailsOpen = false;
|
||||
IsTxDetailsLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
TxDetails = details;
|
||||
IsTxDetailsLoading = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CloseTransactionDetails()
|
||||
{
|
||||
_txDetailsCts?.Cancel();
|
||||
IsTxDetailsOpen = false;
|
||||
IsTxDetailsLoading = false;
|
||||
TxDetails = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recupera dal server tutti i dati della transazione <paramref name="txid"/>
|
||||
/// (importi, fee, indirizzi, dimensioni, data) e li impacchetta per la
|
||||
/// finestra di dettaglio. Restituisce null se non c'è connessione o in errore.
|
||||
/// </summary>
|
||||
public async Task<TransactionDetailsViewModel?> BuildTransactionDetailsAsync(
|
||||
string txid, CancellationToken ct = default)
|
||||
{
|
||||
if (_client is null || !_client.IsConnected)
|
||||
{
|
||||
StatusMessage = Loc.Tr("tx.needconnection");
|
||||
return null;
|
||||
}
|
||||
if (_doc?.Cache is not { } cache)
|
||||
return null;
|
||||
|
||||
// Snapshot dei dati sull'UI thread, poi tutto il lavoro (round-trip al
|
||||
// server + parsing delle transazioni) va su un thread pool: altrimenti i
|
||||
// continuation dei fetch riprendono sull'UI thread e bloccano la finestra
|
||||
// (spinner fermo, chiusura ritardata).
|
||||
var client = _client;
|
||||
var network = PalladiumNetworks.For(Net);
|
||||
var row = cache.History.FirstOrDefault(t => t.Txid == txid);
|
||||
var owned = cache.Addresses.Select(a => a.Address).ToHashSet();
|
||||
var tipHeight = cache.TipHeight;
|
||||
var height = row?.Height ?? 0;
|
||||
var delta = row?.DeltaSats ?? 0;
|
||||
var verified = row?.Verified ?? false;
|
||||
var transactions = _lastTransactions;
|
||||
var loc = _loc;
|
||||
var unit = _config.Unit;
|
||||
|
||||
try
|
||||
{
|
||||
return await Task.Run(async () =>
|
||||
{
|
||||
var details = await TransactionInspector.FetchAsync(
|
||||
client, network, txid, tipHeight, height, owned, delta, verified, transactions, ct);
|
||||
return new TransactionDetailsViewModel(details, loc, unit);
|
||||
}, ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"{Loc.Tr("msg.error")}: {ex.Message}";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- rubrica contatti ----
|
||||
|
||||
public ObservableCollection<ContactEntry> Contacts { get; } = [];
|
||||
|
||||
Reference in New Issue
Block a user