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:
+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