2 Commits

Author SHA1 Message Date
davide fe320584eb 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
2026-06-11 21:39:36 +02:00
davide 6fe31964e1 feat(storage): add contacts list to WalletDocument
Adds StoredContact class and a Contacts list to WalletDocument so the
address book survives app restarts inside the encrypted wallet file.
2026-06-11 21:39:18 +02:00
7 changed files with 341 additions and 43 deletions
+20
View File
@@ -93,6 +93,7 @@ public sealed class Loc
["tab.history"] = ["Storico", "History", "Historial", "Historique", "Histórico", "Verlauf"], ["tab.history"] = ["Storico", "History", "Historial", "Historique", "Histórico", "Verlauf"],
["tab.addresses"] = ["Indirizzi", "Addresses", "Direcciones", "Adresses", "Endereços", "Adressen"], ["tab.addresses"] = ["Indirizzi", "Addresses", "Direcciones", "Adresses", "Endereços", "Adressen"],
["tab.send"] = ["Invia", "Send", "Enviar", "Envoyer", "Enviar", "Senden"], ["tab.send"] = ["Invia", "Send", "Enviar", "Envoyer", "Enviar", "Senden"],
["tab.contacts"] = ["Contatti", "Contacts", "Contactos", "Contacts", "Contatos", "Kontakte"],
["receive.next"] = ["Prossimo indirizzo non usato:", "Next unused address:", "Próxima dirección no usada:", "Prochaine adresse non utilisée :", "Próximo endereço não usado:", "Nächste ungenutzte Adresse:"], ["receive.next"] = ["Prossimo indirizzo non usato:", "Next unused address:", "Próxima dirección no usada:", "Prochaine adresse non utilisée :", "Próximo endereço não usado:", "Nächste ungenutzte Adresse:"],
["receive.hint"] = [ ["receive.hint"] = [
"Ogni pagamento ricevuto qui comparirà nello storico alla prossima sincronizzazione.", "Ogni pagamento ricevuto qui comparirà nello storico alla prossima sincronizzazione.",
@@ -105,8 +106,18 @@ public sealed class Loc
["addr.index"] = ["Indice", "Index", "Índice", "Index", "Índice", "Index"], ["addr.index"] = ["Indice", "Index", "Índice", "Index", "Índice", "Index"],
["addr.address"] = ["Indirizzo", "Address", "Dirección", "Adresse", "Endereço", "Adresse"], ["addr.address"] = ["Indirizzo", "Address", "Dirección", "Adresse", "Endereço", "Adresse"],
["addr.balance"] = ["Saldo", "Balance", "Saldo", "Solde", "Saldo", "Saldo"], ["addr.balance"] = ["Saldo", "Balance", "Saldo", "Solde", "Saldo", "Saldo"],
["addr.copied"] = ["Indirizzo copiato negli appunti", "Address copied to clipboard", "Dirección copiada al portapapeles", "Adresse copiée dans le presse-papiers", "Endereço copiado para a área de transferência", "Adresse in die Zwischenablage kopiert"],
["addr.derivpath"] = ["Percorso di derivazione:", "Derivation path:", "Ruta de derivación:", "Chemin de dérivation :", "Caminho de derivação:", "Ableitungspfad:"],
["addr.pubkey"] = ["Chiave pubblica:", "Public key:", "Clave pública:", "Clé publique :", "Chave pública:", "Öffentlicher Schlüssel:"],
["addr.privkey"] = ["Chiave privata (WIF):", "Private key (WIF):", "Clave privada (WIF):", "Clé privée (WIF) :", "Chave privada (WIF):", "Privater Schlüssel (WIF):"],
["addr.show.privkey"] = ["Mostra", "Show", "Mostrar", "Afficher", "Mostrar", "Anzeigen"],
["addr.hide.privkey"] = ["Nascondi", "Hide", "Ocultar", "Masquer", "Ocultar", "Ausblenden"],
["addr.receive"] = ["ricezione", "receive", "recepción", "réception", "recebimento", "Empfang"], ["addr.receive"] = ["ricezione", "receive", "recepción", "réception", "recebimento", "Empfang"],
["addr.change"] = ["change", "change", "cambio", "monnaie", "troco", "Wechselgeld"], ["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"],
["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"], ["send.to"] = ["Indirizzo destinatario", "Recipient address", "Dirección destinataria", "Adresse du destinataire", "Endereço do destinatário", "Empfängeradresse"],
["send.amount"] = ["Importo", "Amount", "Importe", "Montant", "Valor", "Betrag"], ["send.amount"] = ["Importo", "Amount", "Importe", "Montant", "Valor", "Betrag"],
["send.all"] = ["Invia tutto", "Send all", "Enviar todo", "Tout envoyer", "Enviar tudo", "Alles senden"], ["send.all"] = ["Invia tutto", "Send all", "Enviar todo", "Tout envoyer", "Enviar tudo", "Alles senden"],
@@ -217,6 +228,15 @@ public sealed class Loc
"SSL-Zertifikate zurückgesetzt: Verbindung erneut versuchen."], "SSL-Zertifikate zurückgesetzt: Verbindung erneut versuchen."],
["msg.error"] = ["Errore", "Error", "Error", "Erreur", "Erro", "Fehler"], ["msg.error"] = ["Errore", "Error", "Error", "Erreur", "Erro", "Fehler"],
// Contatti
["contacts.name"] = ["Nome", "Name", "Nombre", "Nom", "Nome", "Name"],
["contacts.address"] = ["Indirizzo", "Address", "Dirección", "Adresse", "Endereço", "Adresse"],
["contacts.name.ph"] = ["Nome contatto", "Contact name", "Nombre del contacto", "Nom du contact", "Nome do contato", "Kontaktname"],
["contacts.address.ph"] = ["Indirizzo blockchain", "Blockchain address", "Dirección blockchain", "Adresse blockchain", "Endereço blockchain", "Blockchain-Adresse"],
["contacts.add"] = ["Aggiungi", "Add", "Agregar", "Ajouter", "Adicionar", "Hinzufügen"],
["contacts.remove"] = ["Rimuovi selezionato", "Remove selected", "Eliminar seleccionado", "Supprimer la sélection", "Remover selecionado", "Auswahl entfernen"],
["contacts.empty"] = ["Nessun contatto salvato.", "No saved contacts.", "No hay contactos guardados.", "Aucun contact enregistré.", "Nenhum contato salvo.", "Keine gespeicherten Kontakte."],
// Finestra impostazioni // Finestra impostazioni
["settings.title"] = ["Impostazioni", "Settings", "Configuración", "Paramètres", "Configurações", "Einstellungen"], ["settings.title"] = ["Impostazioni", "Settings", "Configuración", "Paramètres", "Configurações", "Einstellungen"],
["settings.language"] = ["Lingua", "Language", "Idioma", "Langue", "Idioma", "Sprache"], ["settings.language"] = ["Lingua", "Language", "Idioma", "Langue", "Idioma", "Sprache"],
+114 -6
View File
@@ -21,8 +21,24 @@ namespace PalladiumWallet.App.ViewModels;
/// <summary>Riga dello storico transazioni per la vista.</summary> /// <summary>Riga dello storico transazioni per la vista.</summary>
public sealed record HistoryRow(string Conferma, string Importo, string Txid, string Verificata); public sealed record HistoryRow(string Conferma, string Importo, string Txid, string Verificata);
/// <summary>Riga della vista indirizzi (stile Electrum): saldo e uso per indirizzo.</summary> /// <summary>Riga della vista indirizzi con chiavi e derivation path pre-calcolati.</summary>
public sealed record AddressRow(string Tipo, int Indice, string Indirizzo, string Saldo, string NumTx); public sealed record AddressRow(
string Tipo, int Indice, string Indirizzo, string Saldo, string NumTx,
bool IsChange = false, string PubKey = "", string PrivKey = "", string DerivPath = "")
{
public bool HasPrivKey => !string.IsNullOrEmpty(PrivKey);
}
/// <summary>Dati completi di un indirizzo passati alla finestra di dettaglio.</summary>
public sealed record AddressInfo(
Localization.Loc Loc,
string Address, string DerivPath, string PubKey, string PrivKey)
{
public bool HasPrivKey => !string.IsNullOrEmpty(PrivKey);
}
/// <summary>Contatto in rubrica: nome + indirizzo blockchain.</summary>
public sealed record ContactEntry(string Name, string Address);
/// <summary> /// <summary>
/// ViewModel unico dell'applicazione (wizard §15 ridotto + dashboard): /// ViewModel unico dell'applicazione (wizard §15 ridotto + dashboard):
@@ -108,6 +124,18 @@ public partial class MainWindowViewModel : ViewModelBase
private string Fmt(long sats, bool withLabel = true) => private string Fmt(long sats, bool withLabel = true) =>
CoinAmount.FormatIn(sats, _config.Unit, withLabel); CoinAmount.FormatIn(sats, _config.Unit, withLabel);
/// <summary>Chiave privata WIF per un indirizzo; stringa vuota se watch-only.</summary>
private string KeyWif(bool isChange, int index)
{
if (_account is null or { IsWatchOnly: true }) return "";
try
{
return _account.GetExtPrivateKey(isChange, index)
.PrivateKey.GetWif(PalladiumNetworks.For(Net)).ToString();
}
catch { return ""; }
}
/// <summary>File in attesa di password (apertura da menu File → Apri).</summary> /// <summary>File in attesa di password (apertura da menu File → Apri).</summary>
private string? _pendingOpenPath; private string? _pendingOpenPath;
@@ -216,6 +244,73 @@ public partial class MainWindowViewModel : ViewModelBase
public ObservableCollection<AddressRow> Addresses { get; } = []; public ObservableCollection<AddressRow> Addresses { get; } = [];
// ---- tab indirizzi ----
[ObservableProperty]
private AddressRow? selectedAddressRow;
// ---- rubrica contatti ----
public ObservableCollection<ContactEntry> Contacts { get; } = [];
[ObservableProperty]
private ContactEntry? selectedContactInList;
/// <summary>Contatto selezionato nella ComboBox del pannello Invia: riempie SendTo.</summary>
[ObservableProperty]
private ContactEntry? sendToContact;
partial void OnSendToContactChanged(ContactEntry? value)
{
if (value is not null)
SendTo = value.Address;
}
[ObservableProperty]
private string newContactName = "";
[ObservableProperty]
private string newContactAddress = "";
[RelayCommand]
private void AddContact()
{
var name = NewContactName.Trim();
var addr = NewContactAddress.Trim();
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(addr)) return;
Contacts.Add(new ContactEntry(name, addr));
NewContactName = NewContactAddress = "";
PersistContacts();
}
[RelayCommand]
private void RemoveSelectedContact()
{
if (SelectedContactInList is { } c)
{
Contacts.Remove(c);
SelectedContactInList = null;
PersistContacts();
}
}
private void PersistContacts()
{
if (_doc is null || _walletPath is null) return;
_doc.Contacts = Contacts
.Select(c => new PalladiumWallet.Core.Storage.StoredContact { Name = c.Name, Address = c.Address })
.ToList();
WalletStore.Save(_doc, _walletPath, _password);
}
private void LoadContacts()
{
Contacts.Clear();
if (_doc is null) return;
foreach (var c in _doc.Contacts)
Contacts.Add(new ContactEntry(c.Name, c.Address));
}
// ---- pannello invia ---- // ---- pannello invia ----
[ObservableProperty] [ObservableProperty]
@@ -495,6 +590,7 @@ public partial class MainWindowViewModel : ViewModelBase
NetworkInfo = $"{doc.Network} · {doc.ScriptKind} · m/{doc.AccountPath}" NetworkInfo = $"{doc.Network} · {doc.ScriptKind} · m/{doc.AccountPath}"
+ (doc.IsWatchOnly ? " · watch-only" : ""); + (doc.IsWatchOnly ? " · watch-only" : "");
LoadContacts();
ApplyCache(doc.Cache); ApplyCache(doc.Cache);
IsWalletOpen = true; IsWalletOpen = true;
StatusMessage = Loc.Tr("msg.opened"); StatusMessage = Loc.Tr("msg.opened");
@@ -516,8 +612,12 @@ public partial class MainWindowViewModel : ViewModelBase
// Prima della sincronizzazione si mostrano i primi indirizzi derivati. // Prima della sincronizzazione si mostrano i primi indirizzi derivati.
Addresses.Clear(); Addresses.Clear();
for (var i = 0; i < 10; i++) for (var i = 0; i < 10; i++)
Addresses.Add(new AddressRow("ricezione", i, Addresses.Add(new AddressRow(_loc["addr.receive"], i,
_account.GetReceiveAddress(i).ToString(), "—", "—")); _account.GetReceiveAddress(i).ToString(), "—", "—",
false,
_account.GetPublicKey(false, i).ToHex(),
KeyWif(false, i),
$"m/{_doc!.AccountPath}/0/{i}"));
return; return;
} }
BalanceText = Fmt(cache.ConfirmedSats); BalanceText = Fmt(cache.ConfirmedSats);
@@ -540,11 +640,15 @@ public partial class MainWindowViewModel : ViewModelBase
Addresses.Clear(); Addresses.Clear();
foreach (var a in cache.Addresses) foreach (var a in cache.Addresses)
Addresses.Add(new AddressRow( Addresses.Add(new AddressRow(
a.IsChange ? "change" : "ricezione", a.IsChange ? _loc["addr.change"] : _loc["addr.receive"],
a.Index, a.Index,
a.Address, a.Address,
a.BalanceSats > 0 ? Fmt(a.BalanceSats, withLabel: false) : (a.TxCount > 0 ? "0" : "—"), a.BalanceSats > 0 ? Fmt(a.BalanceSats, withLabel: false) : (a.TxCount > 0 ? "0" : "—"),
a.TxCount > 0 ? a.TxCount.ToString() : "—")); a.TxCount > 0 ? a.TxCount.ToString() : "—",
a.IsChange,
_account.GetPublicKey(a.IsChange, a.Index).ToHex(),
KeyWif(a.IsChange, a.Index),
$"m/{_doc!.AccountPath}/{(a.IsChange ? 1 : 0)}/{a.Index}"));
} }
// ---------- comandi wallet ---------- // ---------- comandi wallet ----------
@@ -757,6 +861,10 @@ public partial class MainWindowViewModel : ViewModelBase
_pendingSend = null; _pendingSend = null;
HasPendingSend = false; HasPendingSend = false;
History.Clear(); History.Clear();
Contacts.Clear();
SelectedContactInList = null;
SendToContact = null;
SelectedAddressRow = null;
IsWalletOpen = false; IsWalletOpen = false;
IsConnected = false; IsConnected = false;
ConnectionStatus = Loc.Tr("conn.none"); ConnectionStatus = Loc.Tr("conn.none");
+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> </Grid>
</Border> </Border>
<!-- Tab: Ricevi / Storico / Indirizzi / Invia --> <!-- Tab: Storico / Invia / Ricevi / Indirizzi / Contatti -->
<TabControl Grid.Row="2"> <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]}"> <TabItem Header="{Binding Loc[tab.history]}">
<ListBox ItemsSource="{Binding History}" Margin="4"> <ListBox ItemsSource="{Binding History}" Margin="4">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
@@ -250,34 +242,23 @@
</ListBox> </ListBox>
</TabItem> </TabItem>
<TabItem Header="{Binding Loc[tab.addresses]}"> <!-- 2. Invia -->
<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>
<TabItem Header="{Binding Loc[tab.send]}"> <TabItem Header="{Binding Loc[tab.send]}">
<StackPanel Spacing="10" Margin="8" MaxWidth="640" HorizontalAlignment="Left"> <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}" <TextBox PlaceholderText="{Binding Loc[send.to]}" Text="{Binding SendTo}"
FontFamily="monospace"/> FontFamily="monospace"/>
<Grid ColumnDefinitions="*,Auto,Auto,Auto"> <Grid ColumnDefinitions="*,Auto,Auto,Auto">
@@ -302,6 +283,90 @@
FontSize="13"/> FontSize="13"/>
</StackPanel> </StackPanel>
</TabItem> </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> </TabControl>
</Grid> </Grid>
+27 -1
View File
@@ -1,7 +1,12 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Avalonia.VisualTree;
using PalladiumWallet.App.ViewModels; using PalladiumWallet.App.ViewModels;
namespace PalladiumWallet.App.Views; namespace PalladiumWallet.App.Views;
@@ -13,7 +18,6 @@ public partial class MainWindow : Window
InitializeComponent(); InitializeComponent();
} }
/// <summary>File → Apri wallet da file (il picker richiede il TopLevel, da qui).</summary>
private async void OnOpenWalletFileClick(object? sender, RoutedEventArgs e) private async void OnOpenWalletFileClick(object? sender, RoutedEventArgs e)
{ {
if (DataContext is not MainWindowViewModel vm) if (DataContext is not MainWindowViewModel vm)
@@ -32,4 +36,26 @@ public partial class MainWindow : Window
if (files.FirstOrDefault()?.TryGetLocalPath() is { } path) if (files.FirstOrDefault()?.TryGetLocalPath() is { } path)
vm.OpenFromPath(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);
}
} }
+10
View File
@@ -40,6 +40,9 @@ public sealed class WalletDocument
/// <summary>Etichette per indirizzo/txid (§12).</summary> /// <summary>Etichette per indirizzo/txid (§12).</summary>
public Dictionary<string, string> Labels { get; set; } = []; public Dictionary<string, string> Labels { get; set; } = [];
/// <summary>Rubrica contatti (nome + indirizzo blockchain).</summary>
public List<StoredContact> Contacts { get; set; } = [];
/// <summary>Cache dell'ultimo stato sincronizzato (saldo/storico mostrabili offline).</summary> /// <summary>Cache dell'ultimo stato sincronizzato (saldo/storico mostrabili offline).</summary>
public SyncCache? Cache { get; set; } public SyncCache? Cache { get; set; }
@@ -69,6 +72,13 @@ public sealed class WalletDocument
}; };
} }
/// <summary>Contatto in rubrica: nome leggibile + indirizzo blockchain.</summary>
public sealed class StoredContact
{
public required string Name { get; set; }
public required string Address { get; set; }
}
/// <summary>Stato sincronizzato persistito: permette di mostrare saldo/storico offline.</summary> /// <summary>Stato sincronizzato persistito: permette di mostrare saldo/storico offline.</summary>
public sealed class SyncCache public sealed class SyncCache
{ {