feat(app): navbar restructure, contacts tab and address info window

- Reorder tabs: Storico / Invia / Ricevi / Indirizzi / Contatti
- Invia: ComboBox to quick-fill recipient from saved contacts
- Contatti: new tab to add/remove contacts (name + address), persisted
  to the wallet file via WalletDocument.Contacts
- Indirizzi: clicking any address (left or right click) opens a modal
  window with derivation path, public key and private key as selectable
  text (no copy button needed — user selects and copies manually);
  AddressRow now carries PubKey, PrivKey, DerivPath pre-computed
- AddressInfoWindow: new Window with SelectableTextBlock per field,
  private key section hidden for watch-only wallets
- Loc: adds tab.contacts, addr.*, contacts.*, send.from.contact keys
  in all 6 languages
This commit is contained in:
2026-06-11 21:39:32 +02:00
parent 6fe31964e1
commit fe320584eb
6 changed files with 331 additions and 43 deletions
+49
View File
@@ -0,0 +1,49 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:PalladiumWallet.App.ViewModels"
x:Class="PalladiumWallet.App.Views.AddressInfoWindow"
x:DataType="vm:AddressInfo"
Title="{Binding Loc[addr.info.title]}"
Width="560" SizeToContent="Height"
CanResize="False"
WindowStartupLocation="CenterOwner">
<StackPanel Margin="24" Spacing="16">
<!-- Indirizzo -->
<StackPanel Spacing="4">
<TextBlock Text="{Binding Loc[addr.address]}" FontSize="11" Foreground="Gray"/>
<SelectableTextBlock Text="{Binding Address}"
FontFamily="monospace" FontSize="13"
TextWrapping="Wrap"/>
</StackPanel>
<!-- Derivation path -->
<StackPanel Spacing="4">
<TextBlock Text="{Binding Loc[addr.derivpath]}" FontSize="11" Foreground="Gray"/>
<SelectableTextBlock Text="{Binding DerivPath}"
FontFamily="monospace" FontSize="13"/>
</StackPanel>
<!-- Chiave pubblica -->
<StackPanel Spacing="4">
<TextBlock Text="{Binding Loc[addr.pubkey]}" FontSize="11" Foreground="Gray"/>
<SelectableTextBlock Text="{Binding PubKey}"
FontFamily="monospace" FontSize="12"
TextWrapping="Wrap"/>
</StackPanel>
<!-- Chiave privata (solo se disponibile) -->
<StackPanel Spacing="4" IsVisible="{Binding HasPrivKey}">
<TextBlock Text="{Binding Loc[addr.privkey]}" FontSize="11" Foreground="OrangeRed"/>
<SelectableTextBlock Text="{Binding PrivKey}"
FontFamily="monospace" FontSize="12"
Foreground="OrangeRed"
TextWrapping="Wrap"/>
</StackPanel>
<Button Content="{Binding Loc[addr.close]}"
HorizontalAlignment="Right"
Click="OnCloseClick"/>
</StackPanel>
</Window>
+20
View File
@@ -0,0 +1,20 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using PalladiumWallet.App.ViewModels;
namespace PalladiumWallet.App.Views;
public partial class AddressInfoWindow : Window
{
public AddressInfoWindow()
{
InitializeComponent();
}
public AddressInfoWindow(AddressInfo info) : this()
{
DataContext = info;
}
private void OnCloseClick(object? sender, RoutedEventArgs e) => Close();
}
+101 -36
View File
@@ -222,18 +222,10 @@
</Grid>
</Border>
<!-- Tab: Ricevi / Storico / Indirizzi / Invia -->
<!-- Tab: Storico / Invia / Ricevi / Indirizzi / Contatti -->
<TabControl Grid.Row="2">
<TabItem Header="{Binding Loc[tab.receive]}">
<StackPanel Spacing="10" Margin="8">
<TextBlock Text="{Binding Loc[receive.next]}"/>
<SelectableTextBlock Text="{Binding ReceiveAddress}"
FontFamily="monospace" FontSize="16"/>
<TextBlock Text="{Binding Loc[receive.hint]}"
Foreground="Gray" FontSize="12" TextWrapping="Wrap"/>
</StackPanel>
</TabItem>
<!-- 1. Storico -->
<TabItem Header="{Binding Loc[tab.history]}">
<ListBox ItemsSource="{Binding History}" Margin="4">
<ListBox.ItemTemplate>
@@ -250,34 +242,23 @@
</ListBox>
</TabItem>
<TabItem Header="{Binding Loc[tab.addresses]}">
<Grid RowDefinitions="Auto,*" Margin="4">
<Grid Grid.Row="0" ColumnDefinitions="90,60,*,140,60" Margin="12,4">
<TextBlock Grid.Column="0" Text="{Binding Loc[addr.type]}" FontWeight="Bold"/>
<TextBlock Grid.Column="1" Text="{Binding Loc[addr.index]}" FontWeight="Bold"/>
<TextBlock Grid.Column="2" Text="{Binding Loc[addr.address]}" FontWeight="Bold"/>
<TextBlock Grid.Column="3" Text="{Binding Loc[addr.balance]}" FontWeight="Bold"/>
<TextBlock Grid.Column="4" Text="Tx" FontWeight="Bold"/>
</Grid>
<ListBox Grid.Row="1" ItemsSource="{Binding Addresses}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:AddressRow">
<Grid ColumnDefinitions="90,60,*,140,60">
<TextBlock Grid.Column="0" Text="{Binding Tipo}" Foreground="Gray"/>
<TextBlock Grid.Column="1" Text="{Binding Indice}" Foreground="Gray"/>
<SelectableTextBlock Grid.Column="2" Text="{Binding Indirizzo}"
FontFamily="monospace" FontSize="13"/>
<TextBlock Grid.Column="3" Text="{Binding Saldo}" FontFamily="monospace"/>
<TextBlock Grid.Column="4" Text="{Binding NumTx}" Foreground="Gray"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</TabItem>
<!-- 2. Invia -->
<TabItem Header="{Binding Loc[tab.send]}">
<StackPanel Spacing="10" Margin="8" MaxWidth="640" HorizontalAlignment="Left">
<!-- Contatto rapido -->
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{Binding Loc[send.from.contact]}" VerticalAlignment="Center"/>
<ComboBox ItemsSource="{Binding Contacts}"
SelectedItem="{Binding SendToContact}"
PlaceholderText="{Binding Loc[send.contact.hint]}"
MinWidth="220">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:ContactEntry">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
<TextBox PlaceholderText="{Binding Loc[send.to]}" Text="{Binding SendTo}"
FontFamily="monospace"/>
<Grid ColumnDefinitions="*,Auto,Auto,Auto">
@@ -302,6 +283,90 @@
FontSize="13"/>
</StackPanel>
</TabItem>
<!-- 3. Ricevi -->
<TabItem Header="{Binding Loc[tab.receive]}">
<StackPanel Spacing="10" Margin="8">
<TextBlock Text="{Binding Loc[receive.next]}"/>
<SelectableTextBlock Text="{Binding ReceiveAddress}"
FontFamily="monospace" FontSize="16"/>
<TextBlock Text="{Binding Loc[receive.hint]}"
Foreground="Gray" FontSize="12" TextWrapping="Wrap"/>
</StackPanel>
</TabItem>
<!-- 4. Indirizzi -->
<TabItem Header="{Binding Loc[tab.addresses]}">
<Grid RowDefinitions="Auto,*" Margin="4">
<Grid Grid.Row="0" ColumnDefinitions="90,60,*,140,60" Margin="12,4">
<TextBlock Grid.Column="0" Text="{Binding Loc[addr.type]}" FontWeight="Bold"/>
<TextBlock Grid.Column="1" Text="{Binding Loc[addr.index]}" FontWeight="Bold"/>
<TextBlock Grid.Column="2" Text="{Binding Loc[addr.address]}" FontWeight="Bold"/>
<TextBlock Grid.Column="3" Text="{Binding Loc[addr.balance]}" FontWeight="Bold"/>
<TextBlock Grid.Column="4" Text="Tx" FontWeight="Bold"/>
</Grid>
<ListBox Grid.Row="1" ItemsSource="{Binding Addresses}"
SelectedItem="{Binding SelectedAddressRow}"
x:Name="AddressesListBox"
Tapped="OnAddressListTapped"
PointerPressed="OnAddressListPointerPressed">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:AddressRow">
<Grid ColumnDefinitions="90,60,*,140,60">
<TextBlock Grid.Column="0" Text="{Binding Tipo}" Foreground="Gray"/>
<TextBlock Grid.Column="1" Text="{Binding Indice}" Foreground="Gray"/>
<TextBlock Grid.Column="2" Text="{Binding Indirizzo}"
FontFamily="monospace" FontSize="13"/>
<TextBlock Grid.Column="3" Text="{Binding Saldo}" FontFamily="monospace"/>
<TextBlock Grid.Column="4" Text="{Binding NumTx}" Foreground="Gray"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</TabItem>
<!-- 5. Contatti -->
<TabItem Header="{Binding Loc[tab.contacts]}">
<Grid RowDefinitions="Auto,*,Auto,Auto" Margin="4">
<!-- Intestazioni colonne -->
<Grid Grid.Row="0" ColumnDefinitions="180,*" Margin="12,4">
<TextBlock Grid.Column="0" Text="{Binding Loc[contacts.name]}" FontWeight="Bold"/>
<TextBlock Grid.Column="1" Text="{Binding Loc[contacts.address]}" FontWeight="Bold"/>
</Grid>
<!-- Lista contatti -->
<ListBox Grid.Row="1" ItemsSource="{Binding Contacts}"
SelectedItem="{Binding SelectedContactInList}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:ContactEntry">
<Grid ColumnDefinitions="180,*">
<TextBlock Grid.Column="0" Text="{Binding Name}"
VerticalAlignment="Center"/>
<SelectableTextBlock Grid.Column="1" Text="{Binding Address}"
FontFamily="monospace" FontSize="12"
VerticalAlignment="Center"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- Pulsante rimuovi selezionato -->
<Button Grid.Row="2" Content="{Binding Loc[contacts.remove]}"
Command="{Binding RemoveSelectedContactCommand}"
IsEnabled="{Binding SelectedContactInList, Converter={x:Static ObjectConverters.IsNotNull}}"
Margin="0,6,0,0"/>
<!-- Form aggiungi contatto -->
<Grid Grid.Row="3" ColumnDefinitions="180,*,Auto" Margin="0,8,0,0">
<TextBox Grid.Column="0" PlaceholderText="{Binding Loc[contacts.name.ph]}"
Text="{Binding NewContactName}" Margin="0,0,6,0"/>
<TextBox Grid.Column="1" PlaceholderText="{Binding Loc[contacts.address.ph]}"
Text="{Binding NewContactAddress}" FontFamily="monospace"
Margin="0,0,6,0"/>
<Button Grid.Column="2" Content="{Binding Loc[contacts.add]}"
Command="{Binding AddContactCommand}"/>
</Grid>
</Grid>
</TabItem>
</TabControl>
</Grid>
+27 -1
View File
@@ -1,7 +1,12 @@
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using Avalonia.VisualTree;
using PalladiumWallet.App.ViewModels;
namespace PalladiumWallet.App.Views;
@@ -13,7 +18,6 @@ public partial class MainWindow : Window
InitializeComponent();
}
/// <summary>File → Apri wallet da file (il picker richiede il TopLevel, da qui).</summary>
private async void OnOpenWalletFileClick(object? sender, RoutedEventArgs e)
{
if (DataContext is not MainWindowViewModel vm)
@@ -32,4 +36,26 @@ public partial class MainWindow : Window
if (files.FirstOrDefault()?.TryGetLocalPath() is { } path)
vm.OpenFromPath(path);
}
private async void OnAddressListTapped(object? sender, TappedEventArgs e)
{
if (DataContext is not MainWindowViewModel vm || vm.SelectedAddressRow is not { } row)
return;
var info = new AddressInfo(vm.Loc, row.Indirizzo, row.DerivPath, row.PubKey, row.PrivKey);
await new AddressInfoWindow(info).ShowDialog(this);
}
private async void OnAddressListPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (!e.GetCurrentPoint(null).Properties.IsRightButtonPressed) return;
if (sender is not ListBox lb || DataContext is not MainWindowViewModel vm) return;
var item = (e.Source as Visual)?.FindAncestorOfType<ListBoxItem>();
if (item is not { DataContext: AddressRow row }) return;
lb.SelectedItem = row;
var info = new AddressInfo(vm.Loc, row.Indirizzo, row.DerivPath, row.PubKey, row.PrivKey);
await new AddressInfoWindow(info).ShowDialog(this);
}
}