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:
@@ -138,6 +138,35 @@ public sealed class Loc
|
||||
["addr.change"] = ["change", "change", "cambio", "monnaie", "troco", "Wechselgeld"],
|
||||
["addr.info.title"] = ["Informazioni indirizzo", "Address information", "Información de dirección", "Informations sur l'adresse", "Informações do endereço", "Adressinformationen"],
|
||||
["addr.close"] = ["Chiudi", "Close", "Cerrar", "Fermer", "Fechar", "Schließen"],
|
||||
|
||||
// Storico → dettaglio transazione
|
||||
["history.hint"] = ["Doppio click su una transazione per i dettagli.", "Double-click a transaction for details.", "Doble clic en una transacción para ver los detalles.", "Double-cliquez sur une transaction pour les détails.", "Clique duplo numa transação para ver os detalhes.", "Doppelklick auf eine Transaktion für Details."],
|
||||
["tx.title"] = ["Dettagli transazione", "Transaction details", "Detalles de la transacción", "Détails de la transaction", "Detalhes da transação", "Transaktionsdetails"],
|
||||
["tx.close"] = ["Chiudi", "Close", "Cerrar", "Fermer", "Fechar", "Schließen"],
|
||||
["tx.loading"] = ["Carico i dati della transazione dal server…", "Loading transaction data from the server…", "Cargando los datos de la transacción desde el servidor…", "Chargement des données de la transaction depuis le serveur…", "Carregando os dados da transação do servidor…", "Lade Transaktionsdaten vom Server…"],
|
||||
["tx.status"] = ["Stato", "Status", "Estado", "Statut", "Estado", "Status"],
|
||||
["tx.status.mempool"] = ["0 conferme · in mempool", "0 confirmations · in mempool", "0 confirmaciones · en mempool", "0 confirmation · dans le mempool", "0 confirmações · no mempool", "0 Bestätigungen · im Mempool"],
|
||||
["tx.status.confirmations"] = ["conferme", "confirmations", "confirmaciones", "confirmations", "confirmações", "Bestätigungen"],
|
||||
["tx.status.block"] = ["blocco", "block", "bloque", "bloc", "bloco", "Block"],
|
||||
["tx.mempool"] = ["in mempool", "in mempool", "en mempool", "dans le mempool", "no mempool", "im Mempool"],
|
||||
["tx.date"] = ["Data", "Date", "Fecha", "Date", "Data", "Datum"],
|
||||
["tx.to"] = ["A", "To", "Para", "À", "Para", "An"],
|
||||
["tx.from"] = ["Da", "From", "De", "De", "De", "Von"],
|
||||
["tx.debit"] = ["Debito", "Debit", "Débito", "Débit", "Débito", "Soll"],
|
||||
["tx.credit"] = ["Credito", "Credit", "Crédito", "Crédit", "Crédito", "Haben"],
|
||||
["tx.fee"] = ["Fee transazione", "Transaction fee", "Comisión de transacción", "Frais de transaction", "Taxa da transação", "Transaktionsgebühr"],
|
||||
["tx.feerate"] = ["Fee per vByte", "Fee per vByte", "Comisión por vByte", "Frais par vOctet", "Taxa por vByte", "Gebühr pro vByte"],
|
||||
["tx.net"] = ["Importo netto", "Net amount", "Importe neto", "Montant net", "Valor líquido", "Nettobetrag"],
|
||||
["tx.id"] = ["ID transazione", "Transaction ID", "ID de transacción", "ID de transaction", "ID da transação", "Transaktions-ID"],
|
||||
["tx.size.total"] = ["Dimensione totale", "Total size", "Tamaño total", "Taille totale", "Tamanho total", "Gesamtgröße"],
|
||||
["tx.size.virtual"] = ["Dimensione virtuale", "Virtual size", "Tamaño virtual", "Taille virtuelle", "Tamanho virtual", "Virtuelle Größe"],
|
||||
["tx.rbf"] = ["Sostituibile (RBF)", "Replaceable (RBF)", "Reemplazable (RBF)", "Remplaçable (RBF)", "Substituível (RBF)", "Ersetzbar (RBF)"],
|
||||
["tx.verified"] = ["Verifica SPV", "SPV verification", "Verificación SPV", "Vérification SPV", "Verificação SPV", "SPV-Prüfung"],
|
||||
["tx.inputs"] = ["Input", "Inputs", "Entradas", "Entrées", "Entradas", "Eingänge"],
|
||||
["tx.outputs"] = ["Output", "Outputs", "Salidas", "Sorties", "Saídas", "Ausgänge"],
|
||||
["tx.yes"] = ["Sì", "Yes", "Sí", "Oui", "Sim", "Ja"],
|
||||
["tx.no"] = ["No", "No", "No", "Non", "Não", "Nein"],
|
||||
["tx.needconnection"] = ["Connettiti al server per vedere i dettagli della transazione.", "Connect to the server to view transaction details.", "Conéctate al servidor para ver los detalles de la transacción.", "Connectez-vous au serveur pour voir les détails de la transaction.", "Conecte-se ao servidor para ver os detalhes da transação.", "Mit dem Server verbinden, um die Transaktionsdetails zu sehen."],
|
||||
["send.from.contact"] = ["Da contatti:", "From contacts:", "De contactos:", "Depuis les contacts :", "De contatos:", "Aus Kontakten:"],
|
||||
["send.contact.hint"] = ["seleziona per riempire l'indirizzo", "select to fill address", "selecciona para rellenar la dirección", "sélectionner pour remplir l'adresse", "selecione para preencher o endereço", "auswählen um Adresse einzufügen"],
|
||||
["send.to"] = ["Indirizzo destinatario", "Recipient address", "Dirección destinataria", "Adresse du destinataire", "Endereço do destinatário", "Empfängeradresse"],
|
||||
|
||||
@@ -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; } = [];
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace PalladiumWallet.App.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// bool → pennello: gli indirizzi del wallet (input/output "nostri") sono
|
||||
/// evidenziati in verde, gli altri usano il colore di testo predefinito.
|
||||
/// </summary>
|
||||
public sealed class MineColorConverter : IValueConverter
|
||||
{
|
||||
public static readonly MineColorConverter Instance = new();
|
||||
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
||||
value is true ? Brushes.MediumSeaGreen : AvaloniaProperty.UnsetValue;
|
||||
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using PalladiumWallet.App.Localization;
|
||||
using PalladiumWallet.Core.Wallet;
|
||||
|
||||
namespace PalladiumWallet.App.ViewModels;
|
||||
|
||||
/// <summary>Riga input/output per le tabelle della finestra di dettaglio.</summary>
|
||||
public sealed record TxIoRow(string Position, string Address, string Amount, bool IsMine);
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel della finestra di dettaglio transazione: prende un
|
||||
/// <see cref="TransactionDetails"/> (assemblato dal server) e ne ricava tutte
|
||||
/// le stringhe già formattate e localizzate per la vista. È di sola lettura.
|
||||
/// </summary>
|
||||
public sealed class TransactionDetailsViewModel
|
||||
{
|
||||
private readonly string _unit;
|
||||
|
||||
public Loc Loc { get; }
|
||||
|
||||
public TransactionDetailsViewModel(TransactionDetails d, Loc loc, string unit)
|
||||
{
|
||||
Loc = loc;
|
||||
_unit = unit;
|
||||
|
||||
Txid = d.Txid;
|
||||
StatusText = BuildStatus(d, loc);
|
||||
DateText = d.BlockTime is { } t
|
||||
? t.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
|
||||
: loc["tx.mempool"];
|
||||
|
||||
var counterparties = d.CounterpartyAddresses;
|
||||
CounterpartyHeader = d.IsIncoming ? loc["tx.from"] : loc["tx.to"];
|
||||
CounterpartyText = counterparties.Count > 0
|
||||
? string.Join(Environment.NewLine, counterparties)
|
||||
: "—";
|
||||
|
||||
// Debito (uscita verso terzi) o Credito (entrata netta sui nostri output).
|
||||
AmountHeader = d.IsIncoming ? loc["tx.credit"] : loc["tx.debit"];
|
||||
AmountText = d.IsIncoming
|
||||
? Signed(d.ReceivedSats)
|
||||
: Signed(-d.SentToOthersSats);
|
||||
|
||||
FeeText = d.FeeSats is { } fee
|
||||
? (d.IsIncoming ? Abs(fee) : Signed(-fee))
|
||||
: "—";
|
||||
|
||||
NetText = Signed(d.NetSats);
|
||||
|
||||
TotalSizeText = $"{d.TotalSize} byte";
|
||||
VirtualSizeText = $"{d.VirtualSize} byte";
|
||||
FeeRateText = d.FeeRateSatPerVb is { } r
|
||||
? r.ToString("0.0", System.Globalization.CultureInfo.InvariantCulture) + " sat/vB"
|
||||
: "—";
|
||||
VersionText = d.Version.ToString();
|
||||
LockTimeText = d.LockTime.ToString();
|
||||
RbfText = loc[d.RbfSignaled ? "tx.yes" : "tx.no"];
|
||||
VerifiedText = d.Verified ? "✓ SPV" : "—";
|
||||
|
||||
Inputs = new ObservableCollection<TxIoRow>(d.Inputs.Select((i, n) => new TxIoRow(
|
||||
i.IsCoinbase ? "coinbase" : $"{Shorten(i.PrevTxid)}:{i.PrevIndex}",
|
||||
i.Address ?? "—",
|
||||
i.AmountSats is { } a ? CoinAmount.FormatIn(a, unit) : "—",
|
||||
i.IsMine)));
|
||||
|
||||
Outputs = new ObservableCollection<TxIoRow>(d.Outputs.Select(o => new TxIoRow(
|
||||
$"#{o.Index}",
|
||||
o.Address ?? $"({o.ScriptType})",
|
||||
CoinAmount.FormatIn(o.AmountSats, unit),
|
||||
o.IsMine)));
|
||||
}
|
||||
|
||||
public string Txid { get; }
|
||||
public string StatusText { get; }
|
||||
public string DateText { get; }
|
||||
public string CounterpartyHeader { get; }
|
||||
public string CounterpartyText { get; }
|
||||
public string AmountHeader { get; }
|
||||
public string AmountText { get; }
|
||||
public string FeeText { get; }
|
||||
public string NetText { get; }
|
||||
public string TotalSizeText { get; }
|
||||
public string VirtualSizeText { get; }
|
||||
public string FeeRateText { get; }
|
||||
public string VersionText { get; }
|
||||
public string LockTimeText { get; }
|
||||
public string RbfText { get; }
|
||||
public string VerifiedText { get; }
|
||||
public ObservableCollection<TxIoRow> Inputs { get; }
|
||||
public ObservableCollection<TxIoRow> Outputs { get; }
|
||||
|
||||
private static string BuildStatus(TransactionDetails d, Loc loc)
|
||||
{
|
||||
if (d.Confirmations <= 0)
|
||||
return loc["tx.status.mempool"];
|
||||
return $"{d.Confirmations} {loc["tx.status.confirmations"]} ({loc["tx.status.block"]} {d.Height})";
|
||||
}
|
||||
|
||||
private string Signed(long sats)
|
||||
{
|
||||
var sign = sats > 0 ? "+" : sats < 0 ? "-" : "";
|
||||
return sign + CoinAmount.FormatIn(Math.Abs(sats), _unit);
|
||||
}
|
||||
|
||||
private string Abs(long sats) => CoinAmount.FormatIn(Math.Abs(sats), _unit);
|
||||
|
||||
private static string Shorten(string txid) =>
|
||||
txid.Length > 16 ? $"{txid[..8]}…{txid[^4..]}" : txid;
|
||||
}
|
||||
+143
-13
@@ -192,19 +192,25 @@
|
||||
|
||||
<!-- 1. Storico -->
|
||||
<TabItem Header="{Binding Loc[tab.history]}">
|
||||
<ListBox ItemsSource="{Binding History}" Margin="4">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:HistoryRow">
|
||||
<Grid ColumnDefinitions="90,160,*,70">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Conferma}" Foreground="Gray"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Importo}" FontFamily="monospace"/>
|
||||
<SelectableTextBlock Grid.Column="2" Text="{Binding Txid}"
|
||||
FontFamily="monospace" FontSize="12"/>
|
||||
<TextBlock Grid.Column="3" Text="{Binding Verificata}" Foreground="Green"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
<Grid RowDefinitions="Auto,*" Margin="4">
|
||||
<TextBlock Grid.Row="0" Text="{Binding Loc[history.hint]}"
|
||||
Foreground="Gray" FontSize="11" Margin="6,2"/>
|
||||
<ListBox Grid.Row="1" ItemsSource="{Binding History}"
|
||||
DoubleTapped="OnHistoryRowDoubleTapped">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:HistoryRow">
|
||||
<Grid ColumnDefinitions="90,160,*,70" Cursor="Hand">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Conferma}" Foreground="Gray"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Importo}" FontFamily="monospace"/>
|
||||
<TextBlock Grid.Column="2" Text="{Binding Txid}"
|
||||
FontFamily="monospace" FontSize="12"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Grid.Column="3" Text="{Binding Verificata}" Foreground="Green"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<!-- 2. Invia -->
|
||||
@@ -425,6 +431,130 @@
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
<!-- ============ OVERLAY DETTAGLIO TRANSAZIONE ============ -->
|
||||
<!-- Overlay in-app (come indirizzo): appare subito con lo spinner, i dati
|
||||
arrivano dal server in background; chiusura istantanea. -->
|
||||
<Border Grid.Row="0" Grid.RowSpan="3"
|
||||
Background="#99000000"
|
||||
Tapped="OnTxDetailsOverlayBackdropTapped"
|
||||
IsVisible="{Binding IsTxDetailsOpen}">
|
||||
<Border Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}"
|
||||
BorderBrush="Gray" BorderThickness="1" CornerRadius="8"
|
||||
Width="640" MaxHeight="600" Padding="0"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Panel Margin="24">
|
||||
|
||||
<!-- Caricamento -->
|
||||
<StackPanel IsVisible="{Binding IsTxDetailsLoading}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="14">
|
||||
<ProgressBar IsIndeterminate="True" Width="220"/>
|
||||
<TextBlock Text="{Binding Loc[tx.loading]}" Foreground="Gray"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Contenuto (IsVisible sul contenitore esterno, così resta
|
||||
legato al VM principale; il DataContext interno è TxDetails) -->
|
||||
<Grid IsVisible="{Binding !IsTxDetailsLoading}">
|
||||
<DockPanel DataContext="{Binding TxDetails}">
|
||||
<TextBlock DockPanel.Dock="Top" Text="{Binding Loc[tx.title]}"
|
||||
FontSize="18" FontWeight="Bold" Margin="0,0,0,12"/>
|
||||
|
||||
<Button DockPanel.Dock="Bottom" Content="{Binding Loc[tx.close]}"
|
||||
HorizontalAlignment="Right" Margin="0,12,0,0"
|
||||
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).CloseTransactionDetailsCommand}"/>
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="14">
|
||||
|
||||
<Grid ColumnDefinitions="170,*" RowSpacing="8">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Foreground="Gray" FontSize="12" Text="{Binding Loc[tx.status]}"/>
|
||||
<SelectableTextBlock Grid.Row="0" Grid.Column="1" FontSize="13" TextWrapping="Wrap" Text="{Binding StatusText}"/>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Foreground="Gray" FontSize="12" Text="{Binding Loc[tx.date]}"/>
|
||||
<SelectableTextBlock Grid.Row="1" Grid.Column="1" FontSize="13" Text="{Binding DateText}"/>
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Foreground="Gray" FontSize="12" Text="{Binding CounterpartyHeader}"/>
|
||||
<SelectableTextBlock Grid.Row="2" Grid.Column="1" FontSize="13" FontFamily="monospace" TextWrapping="Wrap" Text="{Binding CounterpartyText}"/>
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Foreground="Gray" FontSize="12" Text="{Binding AmountHeader}"/>
|
||||
<SelectableTextBlock Grid.Row="3" Grid.Column="1" FontSize="13" FontFamily="monospace" Text="{Binding AmountText}"/>
|
||||
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" Foreground="Gray" FontSize="12" Text="{Binding Loc[tx.fee]}"/>
|
||||
<SelectableTextBlock Grid.Row="4" Grid.Column="1" FontSize="13" FontFamily="monospace" Text="{Binding FeeText}"/>
|
||||
|
||||
<TextBlock Grid.Row="5" Grid.Column="0" Foreground="Gray" FontSize="12" Text="{Binding Loc[tx.feerate]}"/>
|
||||
<SelectableTextBlock Grid.Row="5" Grid.Column="1" FontSize="13" FontFamily="monospace" Text="{Binding FeeRateText}"/>
|
||||
|
||||
<TextBlock Grid.Row="6" Grid.Column="0" Foreground="Gray" FontSize="12" Text="{Binding Loc[tx.net]}"/>
|
||||
<SelectableTextBlock Grid.Row="6" Grid.Column="1" FontSize="13" FontFamily="monospace" FontWeight="Bold" Text="{Binding NetText}"/>
|
||||
|
||||
<TextBlock Grid.Row="7" Grid.Column="0" Foreground="Gray" FontSize="12" Text="{Binding Loc[tx.id]}"/>
|
||||
<SelectableTextBlock Grid.Row="7" Grid.Column="1" FontSize="12" FontFamily="monospace" TextWrapping="Wrap" Text="{Binding Txid}"/>
|
||||
|
||||
<TextBlock Grid.Row="8" Grid.Column="0" Foreground="Gray" FontSize="12" Text="{Binding Loc[tx.size.total]}"/>
|
||||
<SelectableTextBlock Grid.Row="8" Grid.Column="1" FontSize="13" Text="{Binding TotalSizeText}"/>
|
||||
|
||||
<TextBlock Grid.Row="9" Grid.Column="0" Foreground="Gray" FontSize="12" Text="{Binding Loc[tx.size.virtual]}"/>
|
||||
<SelectableTextBlock Grid.Row="9" Grid.Column="1" FontSize="13" Text="{Binding VirtualSizeText}"/>
|
||||
|
||||
<TextBlock Grid.Row="10" Grid.Column="0" Foreground="Gray" FontSize="12" Text="{Binding Loc[tx.rbf]}"/>
|
||||
<SelectableTextBlock Grid.Row="10" Grid.Column="1" FontSize="13" Text="{Binding RbfText}"/>
|
||||
|
||||
<TextBlock Grid.Row="11" Grid.Column="0" Foreground="Gray" FontSize="12" Text="{Binding Loc[tx.verified]}"/>
|
||||
<SelectableTextBlock Grid.Row="11" Grid.Column="1" FontSize="13" Foreground="Green" Text="{Binding VerifiedText}"/>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="{Binding Loc[tx.inputs]}" FontWeight="Bold" Margin="0,4,0,0"/>
|
||||
<ItemsControl ItemsSource="{Binding Inputs}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TxIoRow">
|
||||
<Grid ColumnDefinitions="170,*,Auto" Margin="0,2">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Position}"
|
||||
FontFamily="monospace" FontSize="11" Foreground="Gray"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Address}"
|
||||
FontFamily="monospace" FontSize="11" TextTrimming="CharacterEllipsis"
|
||||
Foreground="{Binding IsMine, Converter={x:Static vm:MineColorConverter.Instance}}"/>
|
||||
<TextBlock Grid.Column="2" Text="{Binding Amount}"
|
||||
FontFamily="monospace" FontSize="11" Margin="8,0,0,0"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Text="{Binding Loc[tx.outputs]}" FontWeight="Bold" Margin="0,4,0,0"/>
|
||||
<ItemsControl ItemsSource="{Binding Outputs}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TxIoRow">
|
||||
<Grid ColumnDefinitions="60,*,Auto" Margin="0,2">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Position}"
|
||||
FontFamily="monospace" FontSize="11" Foreground="Gray"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Address}"
|
||||
FontFamily="monospace" FontSize="11" TextTrimming="CharacterEllipsis"
|
||||
Foreground="{Binding IsMine, Converter={x:Static vm:MineColorConverter.Instance}}"/>
|
||||
<TextBlock Grid.Column="2" Text="{Binding Amount}"
|
||||
FontFamily="monospace" FontSize="11" Margin="8,0,0,0"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</Panel>
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
<!-- ============ OVERLAY IMPOSTAZIONI SERVER ============ -->
|
||||
<!-- Stesso pattern dell'overlay indirizzo: apertura/chiusura istantanee.
|
||||
Accessibile da Impostazioni → Server. -->
|
||||
|
||||
@@ -37,6 +37,23 @@ public partial class MainWindow : Window
|
||||
vm.OpenFromPath(path);
|
||||
}
|
||||
|
||||
private void OnHistoryRowDoubleTapped(object? sender, TappedEventArgs e)
|
||||
{
|
||||
if (sender is not ListBox lb || DataContext is not MainWindowViewModel vm) return;
|
||||
if (lb.SelectedItem is not HistoryRow row) return;
|
||||
|
||||
// Overlay in-app: appare subito con lo spinner, i dati arrivano dal
|
||||
// server in background. Niente top-level window (lenta da aprire/chiudere).
|
||||
_ = vm.ShowTransactionDetailsAsync(row.Txid);
|
||||
}
|
||||
|
||||
private void OnTxDetailsOverlayBackdropTapped(object? sender, TappedEventArgs e)
|
||||
{
|
||||
if (!ReferenceEquals(e.Source, sender)) return;
|
||||
if (DataContext is MainWindowViewModel vm)
|
||||
vm.CloseTransactionDetailsCommand.Execute(null);
|
||||
}
|
||||
|
||||
private void OnAddressListTapped(object? sender, TappedEventArgs e)
|
||||
{
|
||||
if (DataContext is not MainWindowViewModel vm || vm.SelectedAddressRow is not { } row)
|
||||
@@ -115,6 +132,7 @@ public partial class MainWindow : Window
|
||||
{
|
||||
if (e.Key == Key.Escape && DataContext is MainWindowViewModel vm)
|
||||
{
|
||||
if (vm.IsTxDetailsOpen) { vm.CloseTransactionDetailsCommand.Execute(null); e.Handled = true; return; }
|
||||
if (vm.AddressInfo is not null) { vm.AddressInfo = null; e.Handled = true; return; }
|
||||
if (vm.IsServerSettingsOpen) { vm.IsServerSettingsOpen = false; e.Handled = true; return; }
|
||||
if (vm.IsSettingsOpen) { vm.IsSettingsOpen = false; e.Handled = true; return; }
|
||||
|
||||
Reference in New Issue
Block a user