feat(app): server selector, peer discovery, address view, menu bar, keep-alive
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
@@ -18,6 +20,9 @@ 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>
|
||||||
|
public sealed record AddressRow(string Tipo, int Indice, string Indirizzo, string Saldo, string NumTx);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ViewModel unico dell'applicazione (wizard §15 ridotto + dashboard):
|
/// ViewModel unico dell'applicazione (wizard §15 ridotto + dashboard):
|
||||||
/// pannello di setup (crea/ripristina/apri) e pannello wallet
|
/// pannello di setup (crea/ripristina/apri) e pannello wallet
|
||||||
@@ -33,6 +38,14 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
private IReadOnlyDictionary<string, Transaction>? _lastTransactions;
|
private IReadOnlyDictionary<string, Transaction>? _lastTransactions;
|
||||||
private BuiltTransaction? _pendingSend;
|
private BuiltTransaction? _pendingSend;
|
||||||
|
|
||||||
|
/// <summary>File in attesa di password (apertura da menu File → Apri).</summary>
|
||||||
|
private string? _pendingOpenPath;
|
||||||
|
|
||||||
|
/// <summary>Dopo la prima connessione riuscita il timer riconnette da solo.</summary>
|
||||||
|
private bool _autoReconnect;
|
||||||
|
|
||||||
|
private readonly DispatcherTimer _keepAliveTimer;
|
||||||
|
|
||||||
// ---- selezione rete e stato pannelli ----
|
// ---- selezione rete e stato pannelli ----
|
||||||
|
|
||||||
public string[] Networks { get; } = ["mainnet", "testnet", "regtest"];
|
public string[] Networks { get; } = ["mainnet", "testnet", "regtest"];
|
||||||
@@ -86,11 +99,21 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string connectionStatus = "non connesso";
|
private string connectionStatus = "non connesso";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool isConnected;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool isSyncing;
|
private bool isSyncing;
|
||||||
|
|
||||||
|
public ObservableCollection<KnownServer> KnownServers { get; } = [];
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private KnownServer? selectedKnownServer;
|
||||||
|
|
||||||
public ObservableCollection<HistoryRow> History { get; } = [];
|
public ObservableCollection<HistoryRow> History { get; } = [];
|
||||||
|
|
||||||
|
public ObservableCollection<AddressRow> Addresses { get; } = [];
|
||||||
|
|
||||||
// ---- pannello invia ----
|
// ---- pannello invia ----
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
@@ -114,6 +137,34 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
public MainWindowViewModel()
|
public MainWindowViewModel()
|
||||||
{
|
{
|
||||||
RefreshSetupState();
|
RefreshSetupState();
|
||||||
|
// Aggiornamenti continui (§9): ping periodico per tenere viva la
|
||||||
|
// connessione e accorgersi subito della caduta; se cade, riconnette
|
||||||
|
// e risincronizza da solo. Le notifiche push restano la via principale.
|
||||||
|
_keepAliveTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(20) };
|
||||||
|
_keepAliveTimer.Tick += async (_, _) => await KeepAliveTickAsync();
|
||||||
|
_keepAliveTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task KeepAliveTickAsync()
|
||||||
|
{
|
||||||
|
if (!IsWalletOpen || IsSyncing)
|
||||||
|
return;
|
||||||
|
if (_client is { IsConnected: true })
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _client.PingAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// La caduta viene gestita dall'evento Disconnected.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (_autoReconnect)
|
||||||
|
{
|
||||||
|
ConnectionStatus = "riconnessione…";
|
||||||
|
await ConnectAndSync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private NetKind Net => Enum.Parse<NetKind>(SelectedNetwork, ignoreCase: true);
|
private NetKind Net => Enum.Parse<NetKind>(SelectedNetwork, ignoreCase: true);
|
||||||
@@ -121,13 +172,37 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
partial void OnSelectedNetworkChanged(string value) => RefreshSetupState();
|
partial void OnSelectedNetworkChanged(string value) => RefreshSetupState();
|
||||||
|
|
||||||
|
private ServerRegistry Registry => new(Profile, AppPaths.ServersPath(Net));
|
||||||
|
|
||||||
private void RefreshSetupState()
|
private void RefreshSetupState()
|
||||||
{
|
{
|
||||||
WalletFileExists = WalletStore.Exists(AppPaths.DefaultWalletPath(Net));
|
WalletFileExists = WalletStore.Exists(AppPaths.DefaultWalletPath(Net));
|
||||||
StatusMessage = WalletFileExists
|
StatusMessage = WalletFileExists
|
||||||
? "Trovato un wallet esistente: inserisci la password (se impostata) e apri."
|
? "Trovato un wallet esistente: inserisci la password (se impostata) e apri."
|
||||||
: "Nessun wallet su questa rete: creane uno nuovo o ripristina da seed.";
|
: "Nessun wallet su questa rete: creane uno nuovo o ripristina da seed.";
|
||||||
ServerInput = $"127.0.0.1:{Profile.DefaultTcpPort}";
|
RefreshServers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshServers()
|
||||||
|
{
|
||||||
|
KnownServers.Clear();
|
||||||
|
foreach (var server in Registry.All)
|
||||||
|
KnownServers.Add(server);
|
||||||
|
SelectedKnownServer = KnownServers.FirstOrDefault();
|
||||||
|
if (SelectedKnownServer is null)
|
||||||
|
ServerInput = $"127.0.0.1:{Profile.DefaultTcpPort}";
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSelectedKnownServerChanged(KnownServer? value)
|
||||||
|
{
|
||||||
|
if (value is not null)
|
||||||
|
ServerInput = $"{value.Host}:{value.PortFor(UseSsl)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnUseSslChanged(bool value)
|
||||||
|
{
|
||||||
|
if (SelectedKnownServer is { } server)
|
||||||
|
ServerInput = $"{server.Host}:{server.PortFor(value)}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- comandi setup ----------
|
// ---------- comandi setup ----------
|
||||||
@@ -149,7 +224,10 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
string.IsNullOrEmpty(PassphraseInput) ? null : PassphraseInput,
|
string.IsNullOrEmpty(PassphraseInput) ? null : PassphraseInput,
|
||||||
ScriptKind.NativeSegwit,
|
ScriptKind.NativeSegwit,
|
||||||
Profile);
|
Profile);
|
||||||
|
// Mai sovrascrivere un wallet esistente: si cerca il primo nome libero.
|
||||||
var path = AppPaths.DefaultWalletPath(Net);
|
var path = AppPaths.DefaultWalletPath(Net);
|
||||||
|
for (var n = 2; WalletStore.Exists(path); n++)
|
||||||
|
path = Path.Combine(AppPaths.WalletsDir(Net), $"wallet-{n}.wallet.json");
|
||||||
var password = string.IsNullOrEmpty(PasswordInput) ? null : PasswordInput;
|
var password = string.IsNullOrEmpty(PasswordInput) ? null : PasswordInput;
|
||||||
WalletStore.Save(doc, path, password);
|
WalletStore.Save(doc, path, password);
|
||||||
OpenLoaded(doc, account, path, password);
|
OpenLoaded(doc, account, path, password);
|
||||||
@@ -165,9 +243,10 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var path = AppPaths.DefaultWalletPath(Net);
|
var path = _pendingOpenPath ?? AppPaths.DefaultWalletPath(Net);
|
||||||
var password = string.IsNullOrEmpty(PasswordInput) ? null : PasswordInput;
|
var password = string.IsNullOrEmpty(PasswordInput) ? null : PasswordInput;
|
||||||
var doc = WalletStore.Load(path, password);
|
var doc = WalletStore.Load(path, password);
|
||||||
|
_pendingOpenPath = null;
|
||||||
OpenLoaded(doc, WalletLoader.ToAccount(doc), path, password);
|
OpenLoaded(doc, WalletLoader.ToAccount(doc), path, password);
|
||||||
}
|
}
|
||||||
catch (WrongPasswordException)
|
catch (WrongPasswordException)
|
||||||
@@ -180,8 +259,45 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Apertura di un file wallet qualunque (menu File → Apri, multi-wallet §8).</summary>
|
||||||
|
public void OpenFromPath(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (IsWalletOpen)
|
||||||
|
CloseWallet();
|
||||||
|
var doc = WalletStore.Load(path);
|
||||||
|
OpenLoaded(doc, WalletLoader.ToAccount(doc), path, password: null);
|
||||||
|
}
|
||||||
|
catch (WrongPasswordException)
|
||||||
|
{
|
||||||
|
// Cifrato: si chiede la password nel pannello di apertura.
|
||||||
|
if (IsWalletOpen)
|
||||||
|
CloseWallet();
|
||||||
|
_pendingOpenPath = path;
|
||||||
|
WalletFileExists = true;
|
||||||
|
StatusMessage = $"Il wallet \"{Path.GetFileName(path)}\" è cifrato: inserisci la password e premi Apri.";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StatusMessage = $"Errore: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Torna al pannello di setup per creare/ripristinare un altro wallet.</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void NewWallet()
|
||||||
|
{
|
||||||
|
if (IsWalletOpen)
|
||||||
|
CloseWallet();
|
||||||
|
_pendingOpenPath = null;
|
||||||
|
StatusMessage = "Crea un nuovo wallet o ripristina da seed. ATTENZIONE: sovrascrive il wallet di default della rete selezionata.";
|
||||||
|
}
|
||||||
|
|
||||||
private void OpenLoaded(WalletDocument doc, HdAccount account, string path, string? password)
|
private void OpenLoaded(WalletDocument doc, HdAccount account, string path, string? password)
|
||||||
{
|
{
|
||||||
|
// La rete del wallet comanda (registry, pin TLS, indirizzi).
|
||||||
|
SelectedNetwork = doc.Network;
|
||||||
_doc = doc;
|
_doc = doc;
|
||||||
_account = account;
|
_account = account;
|
||||||
_walletPath = path;
|
_walletPath = path;
|
||||||
@@ -207,11 +323,19 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
UnconfirmedText = "";
|
UnconfirmedText = "";
|
||||||
ReceiveAddress = _account.GetReceiveAddress(0).ToString();
|
ReceiveAddress = _account.GetReceiveAddress(0).ToString();
|
||||||
History.Clear();
|
History.Clear();
|
||||||
|
// Prima della sincronizzazione si mostrano i primi indirizzi derivati.
|
||||||
|
Addresses.Clear();
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
Addresses.Add(new AddressRow("ricezione", i,
|
||||||
|
_account.GetReceiveAddress(i).ToString(), "—", "—"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
BalanceText = CoinAmount.Format(cache.ConfirmedSats, Profile.CoinUnit);
|
BalanceText = CoinAmount.Format(cache.ConfirmedSats, Profile.CoinUnit);
|
||||||
UnconfirmedText = cache.UnconfirmedSats != 0
|
// Saldo in attesa: somma delle tx in mempool (può essere negativo per
|
||||||
? $"+ {CoinAmount.Format(cache.UnconfirmedSats)} non confermato"
|
// gli invii in uscita non ancora confermati).
|
||||||
|
var pending = cache.History.Where(t => t.Height <= 0).Sum(t => t.DeltaSats);
|
||||||
|
UnconfirmedText = pending != 0
|
||||||
|
? $"in mempool: {(pending > 0 ? "+" : "")}{CoinAmount.Format(pending)} (in attesa di conferma)"
|
||||||
: "";
|
: "";
|
||||||
ReceiveAddress = _account.GetReceiveAddress(cache.NextReceiveIndex).ToString();
|
ReceiveAddress = _account.GetReceiveAddress(cache.NextReceiveIndex).ToString();
|
||||||
History.Clear();
|
History.Clear();
|
||||||
@@ -221,6 +345,15 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
(tx.DeltaSats >= 0 ? "+" : "") + CoinAmount.Format(tx.DeltaSats),
|
(tx.DeltaSats >= 0 ? "+" : "") + CoinAmount.Format(tx.DeltaSats),
|
||||||
tx.Txid,
|
tx.Txid,
|
||||||
tx.Verified ? "✓ SPV" : "—"));
|
tx.Verified ? "✓ SPV" : "—"));
|
||||||
|
|
||||||
|
Addresses.Clear();
|
||||||
|
foreach (var a in cache.Addresses)
|
||||||
|
Addresses.Add(new AddressRow(
|
||||||
|
a.IsChange ? "change" : "ricezione",
|
||||||
|
a.Index,
|
||||||
|
a.Address,
|
||||||
|
a.BalanceSats > 0 ? CoinAmount.Format(a.BalanceSats) : (a.TxCount > 0 ? "0" : "—"),
|
||||||
|
a.TxCount > 0 ? a.TxCount.ToString() : "—"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- comandi wallet ----------
|
// ---------- comandi wallet ----------
|
||||||
@@ -242,7 +375,12 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
_client = await ElectrumClient.ConnectAsync(host, port, UseSsl, pins);
|
_client = await ElectrumClient.ConnectAsync(host, port, UseSsl, pins);
|
||||||
_client.NotificationReceived += OnServerNotification;
|
_client.NotificationReceived += OnServerNotification;
|
||||||
_client.Disconnected += _ => Dispatcher.UIThread.Post(() =>
|
_client.Disconnected += _ => Dispatcher.UIThread.Post(() =>
|
||||||
ConnectionStatus = "disconnesso");
|
{
|
||||||
|
IsConnected = false;
|
||||||
|
ConnectionStatus = "disconnesso";
|
||||||
|
});
|
||||||
|
IsConnected = true;
|
||||||
|
_autoReconnect = true;
|
||||||
ConnectionStatus = $"connesso a {host}:{port}{(UseSsl ? " (TLS)" : "")}";
|
ConnectionStatus = $"connesso a {host}:{port}{(UseSsl ? " (TLS)" : "")}";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,6 +398,7 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
NextChangeIndex = result.NextChangeIndex,
|
NextChangeIndex = result.NextChangeIndex,
|
||||||
History = [.. result.History],
|
History = [.. result.History],
|
||||||
Utxos = [.. result.Utxos],
|
Utxos = [.. result.Utxos],
|
||||||
|
Addresses = [.. result.AddressRows],
|
||||||
};
|
};
|
||||||
WalletStore.Save(_doc, _walletPath!, _password);
|
WalletStore.Save(_doc, _walletPath!, _password);
|
||||||
ApplyCache(_doc.Cache);
|
ApplyCache(_doc.Cache);
|
||||||
@@ -268,12 +407,14 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
catch (CertificatePinMismatchException ex)
|
catch (CertificatePinMismatchException ex)
|
||||||
{
|
{
|
||||||
|
IsConnected = false;
|
||||||
ConnectionStatus = "certificato cambiato";
|
ConnectionStatus = "certificato cambiato";
|
||||||
StatusMessage = ex.Message;
|
StatusMessage = ex.Message;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
ConnectionStatus = "errore";
|
IsConnected = _client?.IsConnected == true;
|
||||||
|
ConnectionStatus = IsConnected ? ConnectionStatus : "errore di connessione";
|
||||||
StatusMessage = $"Errore: {ex.Message}";
|
StatusMessage = $"Errore: {ex.Message}";
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -282,6 +423,32 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Scopre nuovi server dai peer annunciati dal server connesso (§9).</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task DiscoverServers()
|
||||||
|
{
|
||||||
|
if (_client is null || !_client.IsConnected)
|
||||||
|
{
|
||||||
|
StatusMessage = "Connettiti a un server prima di cercare i peer.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var added = await Registry.DiscoverAsync(_client);
|
||||||
|
var selected = SelectedKnownServer;
|
||||||
|
RefreshServers();
|
||||||
|
SelectedKnownServer = KnownServers.FirstOrDefault(s => s.Host == selected?.Host)
|
||||||
|
?? KnownServers.FirstOrDefault();
|
||||||
|
StatusMessage = added > 0
|
||||||
|
? $"Trovati {added} nuovi server dai peer (totale {KnownServers.Count})."
|
||||||
|
: $"Nessun nuovo server annunciato (totale {KnownServers.Count}).";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StatusMessage = $"Errore nella scoperta peer: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnServerNotification(string method, System.Text.Json.JsonElement payload)
|
private void OnServerNotification(string method, System.Text.Json.JsonElement payload)
|
||||||
{
|
{
|
||||||
// Cambiamento su un nostro indirizzo o nuovo blocco: risincronizza.
|
// Cambiamento su un nostro indirizzo o nuovo blocco: risincronizza.
|
||||||
@@ -380,6 +547,7 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
HasPendingSend = false;
|
HasPendingSend = false;
|
||||||
History.Clear();
|
History.Clear();
|
||||||
IsWalletOpen = false;
|
IsWalletOpen = false;
|
||||||
|
IsConnected = false;
|
||||||
ConnectionStatus = "non connesso";
|
ConnectionStatus = "non connesso";
|
||||||
RefreshSetupState();
|
RefreshSetupState();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,25 @@
|
|||||||
<vm:MainWindowViewModel/>
|
<vm:MainWindowViewModel/>
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
|
|
||||||
<Grid RowDefinitions="*,Auto">
|
<Grid RowDefinitions="Auto,*,Auto">
|
||||||
|
|
||||||
|
<!-- ============ MENU (stile Electrum) ============ -->
|
||||||
|
<Menu Grid.Row="0">
|
||||||
|
<MenuItem Header="_File">
|
||||||
|
<MenuItem Header="Nuovo / ripristina wallet…" Command="{Binding NewWalletCommand}"/>
|
||||||
|
<MenuItem Header="Apri wallet da file…" Click="OnOpenWalletFileClick"/>
|
||||||
|
<Separator/>
|
||||||
|
<MenuItem Header="Chiudi wallet" Command="{Binding CloseWalletCommand}"
|
||||||
|
IsEnabled="{Binding IsWalletOpen}"/>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="_Rete">
|
||||||
|
<MenuItem Header="Cerca altri server (peer)" Command="{Binding DiscoverServersCommand}"/>
|
||||||
|
<MenuItem Header="Reset certificati SSL" Command="{Binding ResetCertificatesCommand}"/>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
<!-- ============ PANNELLO SETUP: crea / ripristina / apri ============ -->
|
<!-- ============ PANNELLO SETUP: crea / ripristina / apri ============ -->
|
||||||
<ScrollViewer Grid.Row="0" IsVisible="{Binding IsSetupVisible}">
|
<ScrollViewer Grid.Row="1" IsVisible="{Binding IsSetupVisible}">
|
||||||
<StackPanel MaxWidth="560" Margin="24" Spacing="14">
|
<StackPanel MaxWidth="560" Margin="24" Spacing="14">
|
||||||
<TextBlock Text="Palladium Wallet" FontSize="28" FontWeight="Bold"/>
|
<TextBlock Text="Palladium Wallet" FontSize="28" FontWeight="Bold"/>
|
||||||
|
|
||||||
@@ -58,7 +73,7 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
<!-- ============ PANNELLO WALLET ============ -->
|
<!-- ============ PANNELLO WALLET ============ -->
|
||||||
<Grid Grid.Row="0" RowDefinitions="Auto,Auto,*" IsVisible="{Binding IsWalletOpen}" Margin="16">
|
<Grid Grid.Row="1" RowDefinitions="Auto,Auto,*" IsVisible="{Binding IsWalletOpen}" Margin="16">
|
||||||
|
|
||||||
<!-- Testata: saldo + rete + chiudi -->
|
<!-- Testata: saldo + rete + chiudi -->
|
||||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
|
||||||
@@ -74,17 +89,36 @@
|
|||||||
<!-- Server -->
|
<!-- Server -->
|
||||||
<Border Grid.Row="1" Margin="0,12,0,12" Padding="10"
|
<Border Grid.Row="1" Margin="0,12,0,12" Padding="10"
|
||||||
BorderBrush="Gray" BorderThickness="1" CornerRadius="6">
|
BorderBrush="Gray" BorderThickness="1" CornerRadius="6">
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto,Auto">
|
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*,Auto,Auto">
|
||||||
<TextBlock Grid.Column="0" Text="Server:" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Server:"
|
||||||
<TextBox Grid.Column="1" Text="{Binding ServerInput}"
|
VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||||
PlaceholderText="host:porta del server di indicizzazione"/>
|
<ComboBox Grid.Row="0" Grid.Column="1"
|
||||||
<CheckBox Grid.Column="2" Content="TLS" IsChecked="{Binding UseSsl}" Margin="8,0"/>
|
ItemsSource="{Binding KnownServers}"
|
||||||
<Button Grid.Column="3" Content="Connetti e sincronizza"
|
SelectedItem="{Binding SelectedKnownServer}"
|
||||||
|
HorizontalAlignment="Stretch"/>
|
||||||
|
<CheckBox Grid.Row="0" Grid.Column="2" Content="TLS"
|
||||||
|
IsChecked="{Binding UseSsl}" Margin="8,0"/>
|
||||||
|
<Button Grid.Row="0" Grid.Column="3" Content="Connetti e sincronizza"
|
||||||
Command="{Binding ConnectAndSyncCommand}" IsEnabled="{Binding !IsSyncing}"/>
|
Command="{Binding ConnectAndSyncCommand}" IsEnabled="{Binding !IsSyncing}"/>
|
||||||
<Button Grid.Column="4" Content="Reset cert." Margin="6,0,0,0"
|
|
||||||
Command="{Binding ResetCertificatesCommand}"/>
|
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal"
|
||||||
<TextBlock Grid.Column="5" Text="{Binding ConnectionStatus}"
|
Spacing="8" Margin="0,8,0,0">
|
||||||
VerticalAlignment="Center" Margin="10,0,0,0" Foreground="Gray"/>
|
<TextBox Text="{Binding ServerInput}" MinWidth="220"
|
||||||
|
PlaceholderText="oppure host:porta manuale"/>
|
||||||
|
<Button Content="Cerca altri server"
|
||||||
|
Command="{Binding DiscoverServersCommand}"/>
|
||||||
|
<Button Content="Reset cert."
|
||||||
|
Command="{Binding ResetCertificatesCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Row="1" Grid.Column="2" Grid.ColumnSpan="2"
|
||||||
|
Orientation="Horizontal" Spacing="6" Margin="8,8,0,0"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Ellipse Width="10" Height="10" Fill="LimeGreen"
|
||||||
|
IsVisible="{Binding IsConnected}"/>
|
||||||
|
<Ellipse Width="10" Height="10" Fill="IndianRed"
|
||||||
|
IsVisible="{Binding !IsConnected}"/>
|
||||||
|
<TextBlock Text="{Binding ConnectionStatus}" Foreground="Gray"/>
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -116,6 +150,32 @@
|
|||||||
</ListBox>
|
</ListBox>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem Header="Indirizzi">
|
||||||
|
<Grid RowDefinitions="Auto,*" Margin="4">
|
||||||
|
<Grid Grid.Row="0" ColumnDefinitions="90,60,*,140,60" Margin="12,4">
|
||||||
|
<TextBlock Grid.Column="0" Text="Tipo" FontWeight="Bold"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Indice" FontWeight="Bold"/>
|
||||||
|
<TextBlock Grid.Column="2" Text="Indirizzo" FontWeight="Bold"/>
|
||||||
|
<TextBlock Grid.Column="3" Text="Saldo" 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="Invia">
|
<TabItem Header="Invia">
|
||||||
<StackPanel Spacing="10" Margin="8" MaxWidth="640" HorizontalAlignment="Left">
|
<StackPanel Spacing="10" Margin="8" MaxWidth="640" HorizontalAlignment="Left">
|
||||||
<TextBox PlaceholderText="Indirizzo destinatario" Text="{Binding SendTo}"
|
<TextBox PlaceholderText="Indirizzo destinatario" Text="{Binding SendTo}"
|
||||||
@@ -144,7 +204,7 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Barra di stato -->
|
<!-- Barra di stato -->
|
||||||
<Border Grid.Row="1" Background="{DynamicResource SystemControlBackgroundChromeMediumBrush}"
|
<Border Grid.Row="2" Background="{DynamicResource SystemControlBackgroundChromeMediumBrush}"
|
||||||
Padding="10,6">
|
Padding="10,6">
|
||||||
<TextBlock Text="{Binding StatusMessage}" FontSize="12" TextWrapping="Wrap"/>
|
<TextBlock Text="{Binding StatusMessage}" FontSize="12" TextWrapping="Wrap"/>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -1,11 +1,35 @@
|
|||||||
using Avalonia.Controls;
|
using System.Linq;
|
||||||
|
using Avalonia.Controls;
|
||||||
namespace PalladiumWallet.App.Views;
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
public partial class MainWindow : Window
|
using PalladiumWallet.App.ViewModels;
|
||||||
{
|
|
||||||
public MainWindow()
|
namespace PalladiumWallet.App.Views;
|
||||||
{
|
|
||||||
InitializeComponent();
|
public partial class MainWindow : Window
|
||||||
}
|
{
|
||||||
}
|
public MainWindow()
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||||
|
{
|
||||||
|
Title = "Apri file wallet",
|
||||||
|
AllowMultiple = false,
|
||||||
|
FileTypeFilter =
|
||||||
|
[
|
||||||
|
new FilePickerFileType("Wallet Palladium") { Patterns = ["*.wallet.json", "*.json"] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (files.FirstOrDefault()?.TryGetLocalPath() is { } path)
|
||||||
|
vm.OpenFromPath(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user