feat(ui): restructure transaction details + clearer coinbase identification

Transaction details overlay redesigned from a flat 12-row grid into a
scannable layout: a header with the net amount in focus plus SPV/status
badges, then three labeled cards (Overview, Amounts & fees, Technical
details), with inputs/outputs as carded rows. Adds a .section heading
style and tx.sect.* localization keys (6 languages).

Coinbase transactions are now explicitly identified: an amber "Coinbase"
badge in the header, and the misleading "From: —" / "coinbase —" replaced
with "Newly generated (mining)" (localized). Exposes IsCoinbase on the
details view model; no consensus/logic change, presentation only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 10:21:50 +02:00
parent 41eb1bb788
commit 4b82a0852c
4 changed files with 164 additions and 81 deletions
+5
View File
@@ -216,6 +216,11 @@ public sealed class Loc
["tx.yes"] = ["Sì", "Yes", "Sí", "Oui", "Sim", "Ja"], ["tx.yes"] = ["Sì", "Yes", "Sí", "Oui", "Sim", "Ja"],
["tx.no"] = ["No", "No", "No", "Non", "Não", "Nein"], ["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."], ["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."],
["tx.sect.overview"] = ["Panoramica", "Overview", "Resumen", "Aperçu", "Visão geral", "Übersicht"],
["tx.sect.amounts"] = ["Importi e commissioni", "Amounts & fees", "Importes y comisiones", "Montants et frais", "Valores e taxas", "Beträge & Gebühren"],
["tx.sect.tech"] = ["Dettagli tecnici", "Technical details", "Detalles técnicos", "Détails techniques", "Detalhes técnicos", "Technische Details"],
["tx.coinbase"] = ["Coinbase", "Coinbase", "Coinbase", "Coinbase", "Coinbase", "Coinbase"],
["tx.coinbase.newcoins"] = ["Nuova emissione (mining)", "Newly generated (mining)", "Nueva emisión (minería)", "Nouvelle émission (minage)", "Nova emissão (mineração)", "Neu erzeugt (Mining)"],
["send.from.contact"] = ["Da contatti:", "From contacts:", "De contactos:", "Depuis les contacts :", "De contatos:", "Aus Kontakten:"], ["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.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"], ["send.to"] = ["Indirizzo destinatario", "Recipient address", "Dirección destinataria", "Adresse du destinataire", "Endereço do destinatário", "Empfängeradresse"],
+7
View File
@@ -47,6 +47,13 @@
<Setter Property="FontFamily" Value="monospace"/> <Setter Property="FontFamily" Value="monospace"/>
<Setter Property="FontFeatures" Value="+tnum"/> <Setter Property="FontFeatures" Value="+tnum"/>
</Style> </Style>
<!-- Intestazione di sezione (es. dettaglio transazione) -->
<Style Selector="TextBlock.section">
<Setter Property="FontSize" Value="11"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{DynamicResource TextMutedBrush}"/>
<Setter Property="LetterSpacing" Value="0.8"/>
</Style>
<!-- ===== Bottoni ===== --> <!-- ===== Bottoni ===== -->
<Style Selector="Button"> <Style Selector="Button">
@@ -27,6 +27,7 @@ public sealed class TransactionDetailsViewModel
_unit = unit; _unit = unit;
Txid = d.Txid; Txid = d.Txid;
IsCoinbase = d.IsCoinbase;
StatusText = BuildStatus(d, loc); StatusText = BuildStatus(d, loc);
DateText = d.BlockTime is { } t DateText = d.BlockTime is { } t
? t.ToLocalTime().ToString("dd/MM/yyyy HH:mm") ? t.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
@@ -34,9 +35,12 @@ public sealed class TransactionDetailsViewModel
var counterparties = d.CounterpartyAddresses; var counterparties = d.CounterpartyAddresses;
CounterpartyHeader = d.IsIncoming ? loc["tx.from"] : loc["tx.to"]; CounterpartyHeader = d.IsIncoming ? loc["tx.from"] : loc["tx.to"];
CounterpartyText = counterparties.Count > 0 // Una coinbase non ha mittente: le monete sono generate dal mining.
? string.Join(Environment.NewLine, counterparties) CounterpartyText = d.IsCoinbase
: "—"; ? loc["tx.coinbase.newcoins"]
: counterparties.Count > 0
? string.Join(Environment.NewLine, counterparties)
: "—";
// Debito (uscita verso terzi) o Credito (entrata netta sui nostri output). // Debito (uscita verso terzi) o Credito (entrata netta sui nostri output).
AmountHeader = d.IsIncoming ? loc["tx.credit"] : loc["tx.debit"]; AmountHeader = d.IsIncoming ? loc["tx.credit"] : loc["tx.debit"];
@@ -61,8 +65,8 @@ public sealed class TransactionDetailsViewModel
VerifiedText = d.Verified ? "✓ SPV" : "—"; VerifiedText = d.Verified ? "✓ SPV" : "—";
Inputs = new ObservableCollection<TxIoRow>(d.Inputs.Select((i, n) => new TxIoRow( Inputs = new ObservableCollection<TxIoRow>(d.Inputs.Select((i, n) => new TxIoRow(
i.IsCoinbase ? "coinbase" : $"{Shorten(i.PrevTxid)}:{i.PrevIndex}", i.IsCoinbase ? loc["tx.coinbase"] : $"{Shorten(i.PrevTxid)}:{i.PrevIndex}",
i.Address ?? "—", i.IsCoinbase ? loc["tx.coinbase.newcoins"] : i.Address ?? "—",
i.AmountSats is { } a ? CoinAmount.FormatIn(a, unit) : "—", i.AmountSats is { } a ? CoinAmount.FormatIn(a, unit) : "—",
i.IsMine))); i.IsMine)));
@@ -74,6 +78,7 @@ public sealed class TransactionDetailsViewModel
} }
public string Txid { get; } public string Txid { get; }
public bool IsCoinbase { get; }
public string StatusText { get; } public string StatusText { get; }
public string DateText { get; } public string DateText { get; }
public string CounterpartyHeader { get; } public string CounterpartyHeader { get; }
+142 -76
View File
@@ -778,88 +778,154 @@
Command="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).CloseTransactionDetailsCommand}"/> Command="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).CloseTransactionDetailsCommand}"/>
<ScrollViewer> <ScrollViewer>
<StackPanel Spacing="14"> <StackPanel Spacing="18">
<Grid ColumnDefinitions="170,*" RowSpacing="8"> <!-- Header: importo netto in evidenza + badge stato/SPV -->
<Grid.RowDefinitions> <StackPanel Spacing="10">
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/> <TextBlock Text="{Binding AmountHeader}" Classes="label"/>
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/> <SelectableTextBlock Text="{Binding NetText}"
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/> FontFamily="monospace" FontFeatures="+tnum"
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/> FontSize="28" FontWeight="Bold"/>
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/> <WrapPanel>
<RowDefinition Height="Auto"/><RowDefinition Height="Auto"/> <!-- Badge coinbase: monete generate dal mining (nessun mittente) -->
</Grid.RowDefinitions> <Border Classes="chip" Margin="0,0,8,6" IsVisible="{Binding IsCoinbase}"
Background="{DynamicResource WarningBrush}">
<StackPanel Orientation="Horizontal" Spacing="6">
<Viewbox Width="14" Height="14" VerticalAlignment="Center">
<Canvas Width="24" Height="24">
<Path Fill="#FFFFFF"
Data="M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z"/>
</Canvas>
</Viewbox>
<TextBlock Text="{Binding Loc[tx.coinbase]}"
Foreground="#FFFFFF"
FontSize="12" FontWeight="SemiBold" VerticalAlignment="Center"/>
</StackPanel>
</Border>
<Border Classes="chip" Margin="0,0,8,6"
Background="{DynamicResource PrimarySoftBrush}">
<StackPanel Orientation="Horizontal" Spacing="6">
<Viewbox Width="14" Height="14" VerticalAlignment="Center">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource SuccessBrush}"
Data="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"/>
</Canvas>
</Viewbox>
<TextBlock Text="{Binding VerifiedText}"
Foreground="{DynamicResource SuccessBrush}"
FontSize="12" FontWeight="Medium" VerticalAlignment="Center"/>
</StackPanel>
</Border>
<Border Classes="chip" Margin="0,0,8,6">
<TextBlock Text="{Binding StatusText}" FontSize="12"
Foreground="{DynamicResource TextSecondaryBrush}"/>
</Border>
</WrapPanel>
</StackPanel>
<TextBlock Grid.Row="0" Grid.Column="0" Foreground="{DynamicResource TextSecondaryBrush}" FontSize="12" Text="{Binding Loc[tx.status]}"/> <!-- Sezione: Panoramica -->
<SelectableTextBlock Grid.Row="0" Grid.Column="1" FontSize="13" TextWrapping="Wrap" Text="{Binding StatusText}"/> <TextBlock Classes="section" Text="{Binding Loc[tx.sect.overview]}"/>
<Border Classes="card" Padding="16,12">
<StackPanel Spacing="10">
<Grid ColumnDefinitions="150,*">
<TextBlock Grid.Column="0" Classes="label" Text="{Binding Loc[tx.date]}"/>
<SelectableTextBlock Grid.Column="1" FontSize="13" Text="{Binding DateText}"/>
</Grid>
<Grid ColumnDefinitions="150,*">
<TextBlock Grid.Column="0" Classes="label" Text="{Binding CounterpartyHeader}"/>
<SelectableTextBlock Grid.Column="1" FontSize="13" Classes="mono"
TextWrapping="Wrap" Text="{Binding CounterpartyText}"/>
</Grid>
</StackPanel>
</Border>
<TextBlock Grid.Row="1" Grid.Column="0" Foreground="{DynamicResource TextSecondaryBrush}" FontSize="12" Text="{Binding Loc[tx.date]}"/> <!-- Sezione: Importi e commissioni -->
<SelectableTextBlock Grid.Row="1" Grid.Column="1" FontSize="13" Text="{Binding DateText}"/> <TextBlock Classes="section" Text="{Binding Loc[tx.sect.amounts]}"/>
<Border Classes="card" Padding="16,12">
<StackPanel Spacing="10">
<Grid ColumnDefinitions="150,*">
<TextBlock Grid.Column="0" Classes="label" Text="{Binding AmountHeader}"/>
<SelectableTextBlock Grid.Column="1" FontSize="13" Classes="mono" Text="{Binding AmountText}"/>
</Grid>
<Grid ColumnDefinitions="150,*">
<TextBlock Grid.Column="0" Classes="label" Text="{Binding Loc[tx.fee]}"/>
<SelectableTextBlock Grid.Column="1" FontSize="13" Classes="mono" Text="{Binding FeeText}"/>
</Grid>
<Grid ColumnDefinitions="150,*">
<TextBlock Grid.Column="0" Classes="label" Text="{Binding Loc[tx.feerate]}"/>
<SelectableTextBlock Grid.Column="1" FontSize="13" Classes="mono" Text="{Binding FeeRateText}"/>
</Grid>
<Border Height="1" Background="{DynamicResource BorderSubtleBrush}" Margin="0,2"/>
<Grid ColumnDefinitions="150,*">
<TextBlock Grid.Column="0" Classes="label" Text="{Binding Loc[tx.net]}"/>
<SelectableTextBlock Grid.Column="1" FontSize="14" Classes="mono"
FontWeight="Bold" Text="{Binding NetText}"/>
</Grid>
</StackPanel>
</Border>
<TextBlock Grid.Row="2" Grid.Column="0" Foreground="{DynamicResource TextSecondaryBrush}" FontSize="12" Text="{Binding CounterpartyHeader}"/> <!-- Sezione: Dettagli tecnici -->
<SelectableTextBlock Grid.Row="2" Grid.Column="1" FontSize="13" FontFamily="monospace" TextWrapping="Wrap" Text="{Binding CounterpartyText}"/> <TextBlock Classes="section" Text="{Binding Loc[tx.sect.tech]}"/>
<Border Classes="card" Padding="16,12">
<StackPanel Spacing="10">
<StackPanel Spacing="3">
<TextBlock Classes="label" Text="{Binding Loc[tx.id]}"/>
<SelectableTextBlock FontSize="12" Classes="mono"
TextWrapping="Wrap" Text="{Binding Txid}"/>
</StackPanel>
<Grid ColumnDefinitions="150,*">
<TextBlock Grid.Column="0" Classes="label" Text="{Binding Loc[tx.size.total]}"/>
<SelectableTextBlock Grid.Column="1" FontSize="13" Text="{Binding TotalSizeText}"/>
</Grid>
<Grid ColumnDefinitions="150,*">
<TextBlock Grid.Column="0" Classes="label" Text="{Binding Loc[tx.size.virtual]}"/>
<SelectableTextBlock Grid.Column="1" FontSize="13" Text="{Binding VirtualSizeText}"/>
</Grid>
<Grid ColumnDefinitions="150,*">
<TextBlock Grid.Column="0" Classes="label" Text="{Binding Loc[tx.rbf]}"/>
<SelectableTextBlock Grid.Column="1" FontSize="13" Text="{Binding RbfText}"/>
</Grid>
</StackPanel>
</Border>
<TextBlock Grid.Row="3" Grid.Column="0" Foreground="{DynamicResource TextSecondaryBrush}" FontSize="12" Text="{Binding AmountHeader}"/> <TextBlock Classes="section" Text="{Binding Loc[tx.inputs]}"/>
<SelectableTextBlock Grid.Row="3" Grid.Column="1" FontSize="13" FontFamily="monospace" Text="{Binding AmountText}"/> <Border Classes="card" Padding="14,6">
<ItemsControl ItemsSource="{Binding Inputs}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:TxIoRow">
<Grid ColumnDefinitions="160,*,Auto" Margin="0,4">
<TextBlock Grid.Column="0" Text="{Binding Position}"
FontFamily="monospace" FontSize="11" Foreground="{DynamicResource TextMutedBrush}"
TextTrimming="CharacterEllipsis" Margin="0,0,8,0"/>
<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" FontFeatures="+tnum" FontSize="11" Margin="8,0,0,0"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<TextBlock Grid.Row="4" Grid.Column="0" Foreground="{DynamicResource TextSecondaryBrush}" FontSize="12" Text="{Binding Loc[tx.fee]}"/> <TextBlock Classes="section" Text="{Binding Loc[tx.outputs]}"/>
<SelectableTextBlock Grid.Row="4" Grid.Column="1" FontSize="13" FontFamily="monospace" Text="{Binding FeeText}"/> <Border Classes="card" Padding="14,6">
<ItemsControl ItemsSource="{Binding Outputs}">
<TextBlock Grid.Row="5" Grid.Column="0" Foreground="{DynamicResource TextSecondaryBrush}" FontSize="12" Text="{Binding Loc[tx.feerate]}"/> <ItemsControl.ItemTemplate>
<SelectableTextBlock Grid.Row="5" Grid.Column="1" FontSize="13" FontFamily="monospace" Text="{Binding FeeRateText}"/> <DataTemplate x:DataType="vm:TxIoRow">
<Grid ColumnDefinitions="44,*,Auto" Margin="0,4">
<TextBlock Grid.Row="6" Grid.Column="0" Foreground="{DynamicResource TextSecondaryBrush}" FontSize="12" Text="{Binding Loc[tx.net]}"/> <TextBlock Grid.Column="0" Text="{Binding Position}"
<SelectableTextBlock Grid.Row="6" Grid.Column="1" FontSize="13" FontFamily="monospace" FontWeight="Bold" Text="{Binding NetText}"/> FontFamily="monospace" FontSize="11" Foreground="{DynamicResource TextMutedBrush}"/>
<TextBlock Grid.Column="1" Text="{Binding Address}"
<TextBlock Grid.Row="7" Grid.Column="0" Foreground="{DynamicResource TextSecondaryBrush}" FontSize="12" Text="{Binding Loc[tx.id]}"/> FontFamily="monospace" FontSize="11" TextTrimming="CharacterEllipsis"
<SelectableTextBlock Grid.Row="7" Grid.Column="1" FontSize="12" FontFamily="monospace" TextWrapping="Wrap" Text="{Binding Txid}"/> Foreground="{Binding IsMine, Converter={x:Static vm:MineColorConverter.Instance}}"/>
<TextBlock Grid.Column="2" Text="{Binding Amount}"
<TextBlock Grid.Row="8" Grid.Column="0" Foreground="{DynamicResource TextSecondaryBrush}" FontSize="12" Text="{Binding Loc[tx.size.total]}"/> FontFamily="monospace" FontFeatures="+tnum" FontSize="11" Margin="8,0,0,0"/>
<SelectableTextBlock Grid.Row="8" Grid.Column="1" FontSize="13" Text="{Binding TotalSizeText}"/> </Grid>
</DataTemplate>
<TextBlock Grid.Row="9" Grid.Column="0" Foreground="{DynamicResource TextSecondaryBrush}" FontSize="12" Text="{Binding Loc[tx.size.virtual]}"/> </ItemsControl.ItemTemplate>
<SelectableTextBlock Grid.Row="9" Grid.Column="1" FontSize="13" Text="{Binding VirtualSizeText}"/> </ItemsControl>
</Border>
<TextBlock Grid.Row="10" Grid.Column="0" Foreground="{DynamicResource TextSecondaryBrush}" 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="{DynamicResource TextSecondaryBrush}" FontSize="12" Text="{Binding Loc[tx.verified]}"/>
<SelectableTextBlock Grid.Row="11" Grid.Column="1" FontSize="13" Foreground="{DynamicResource SuccessBrush}" 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="{DynamicResource TextSecondaryBrush}"/>
<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="{DynamicResource TextSecondaryBrush}"/>
<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> </StackPanel>
</ScrollViewer> </ScrollViewer>