Compare commits
8 Commits
fe320584eb
...
58b86ad1af
| Author | SHA1 | Date | |
|---|---|---|---|
| 58b86ad1af | |||
| 7f2759b2fc | |||
| a8a97f09b7 | |||
| f3bf4cf94a | |||
| 87e1c82610 | |||
| 51c87a7dc9 | |||
| 28cb4ce6ae | |||
| cf6e2d7654 |
@@ -27,7 +27,7 @@ public sealed class Loc
|
||||
/// </summary>
|
||||
internal static Loc SwitchTo(string language)
|
||||
{
|
||||
if (System.Array.IndexOf(Languages, language) < 0) language = "it";
|
||||
if (System.Array.IndexOf(Languages, language) < 0) language = "en";
|
||||
var loc = new Loc(language);
|
||||
Instance = loc;
|
||||
return loc;
|
||||
@@ -54,6 +54,18 @@ public sealed class Loc
|
||||
["settings.unit.short"] = ["Unità", "Unit", "Unidad", "Unité", "Unidade", "Einheit"],
|
||||
|
||||
// Wizard
|
||||
["wiz.data.title"] = ["Dove salvare i dati", "Where to store data", "Dónde guardar los datos", "Où enregistrer les données", "Onde salvar os dados", "Wo Daten gespeichert werden"],
|
||||
["wiz.data.info"] = [
|
||||
"Scegli la cartella in cui salvare wallet, configurazione e certificati. Puoi usare il percorso predefinito o sceglierne uno tuo.",
|
||||
"Choose the folder where wallets, configuration and certificates are stored. Use the default path or pick your own.",
|
||||
"Elige la carpeta donde se guardarán wallets, configuración y certificados. Usa la ruta predeterminada o elige la tuya.",
|
||||
"Choisissez le dossier où enregistrer les wallets, la configuration et les certificats. Utilisez le chemin par défaut ou le vôtre.",
|
||||
"Escolha a pasta onde salvar carteiras, configuração e certificados. Use o caminho padrão ou escolha o seu.",
|
||||
"Wählen Sie den Ordner für Wallets, Konfiguration und Zertifikate. Nutzen Sie den Standardpfad oder einen eigenen."],
|
||||
["wiz.data.default"] = ["Percorso predefinito:", "Default path:", "Ruta predeterminada:", "Chemin par défaut :", "Caminho padrão:", "Standardpfad:"],
|
||||
["wiz.data.usedefault"] = ["Usa il percorso predefinito", "Use the default path", "Usar la ruta predeterminada", "Utiliser le chemin par défaut", "Usar o caminho padrão", "Standardpfad verwenden"],
|
||||
["wiz.data.choose"] = ["Scegli una cartella…", "Choose a folder…", "Elegir una carpeta…", "Choisir un dossier…", "Escolher uma pasta…", "Ordner wählen…"],
|
||||
["wiz.choose.title"] = ["Scegli il wallet da aprire", "Choose the wallet to open", "Elige el wallet a abrir", "Choisissez le wallet à ouvrir", "Escolha a carteira a abrir", "Wallet zum Öffnen wählen"],
|
||||
["wiz.net"] = ["Rete:", "Network:", "Red:", "Réseau :", "Rede:", "Netzwerk:"],
|
||||
["wiz.open.btn"] = ["Apri il wallet esistente", "Open existing wallet", "Abrir wallet existente", "Ouvrir le wallet existant", "Abrir carteira existente", "Vorhandenes Wallet öffnen"],
|
||||
["wiz.new.btn"] = ["Crea un nuovo wallet", "Create a new wallet", "Crear nuevo wallet", "Créer un nouveau wallet", "Criar nova carteira", "Neues Wallet erstellen"],
|
||||
@@ -79,6 +91,15 @@ public sealed class Loc
|
||||
["wiz.password.title"] = ["Password del file wallet", "Wallet file password", "Contraseña del archivo wallet", "Mot de passe du fichier wallet", "Senha do arquivo da carteira", "Wallet-Dateipasswort"],
|
||||
["wiz.password.placeholder"] = ["Consigliata (vuoto = file in chiaro su disco)", "Recommended (empty = plaintext file on disk)", "Recomendada (vacío = archivo en texto claro en disco)", "Recommandé (vide = fichier en texte clair sur disque)", "Recomendada (vazio = arquivo em texto simples no disco)", "Empfohlen (leer = Klartextdatei auf Disk)"],
|
||||
["wiz.password.create"] = ["Crea il wallet", "Create wallet", "Crear wallet", "Créer le wallet", "Criar carteira", "Wallet erstellen"],
|
||||
["wiz.password.confirm"] = ["Ripeti la password", "Repeat the password", "Repite la contraseña", "Répétez le mot de passe", "Repita a senha", "Passwort wiederholen"],
|
||||
["wiz.password.encrypt"] = ["Cifra il file wallet con la password", "Encrypt the wallet file with the password", "Cifrar el archivo wallet con la contraseña", "Chiffrer le fichier wallet avec le mot de passe", "Criptografar o arquivo da carteira com a senha", "Wallet-Datei mit dem Passwort verschlüsseln"],
|
||||
["wiz.password.encrypt.hint"] = [
|
||||
"Attenzione: senza cifratura il seed resta in chiaro sul disco.",
|
||||
"Warning: without encryption the seed stays in plaintext on disk.",
|
||||
"Atención: sin cifrado la semilla queda en texto claro en el disco.",
|
||||
"Attention : sans chiffrement, la graine reste en clair sur le disque.",
|
||||
"Atenção: sem criptografia a semente fica em texto simples no disco.",
|
||||
"Achtung: ohne Verschlüsselung bleibt der Seed im Klartext auf der Festplatte."],
|
||||
["wiz.back"] = ["Indietro", "Back", "Atrás", "Retour", "Voltar", "Zurück"],
|
||||
["wiz.next"] = ["Avanti", "Next", "Siguiente", "Suivant", "Próximo", "Weiter"],
|
||||
|
||||
@@ -205,6 +226,21 @@ public sealed class Loc
|
||||
"Mot de passe de chiffrement pour le fichier wallet sur disque (recommandé). Ne remplace pas la graine : protège uniquement le fichier.",
|
||||
"Senha de criptografia para o arquivo da carteira no disco (recomendada). Não substitui a semente: apenas protege o arquivo.",
|
||||
"Verschlüsselungspasswort für die Wallet-Datei auf der Festplatte (empfohlen). Ersetzt nicht den Seed: schützt nur die Datei."],
|
||||
["msg.choose.wallet"] = ["Più wallet disponibili: scegline uno.", "Multiple wallets available: pick one.", "Varios wallets disponibles: elige uno.", "Plusieurs wallets disponibles : choisissez-en un.", "Várias carteiras disponíveis: escolha uma.", "Mehrere Wallets verfügbar: wählen Sie eines."],
|
||||
["msg.password.required"] = [
|
||||
"Inserisci una password per cifrare il wallet (o togli la spunta «Cifra il file wallet»).",
|
||||
"Enter a password to encrypt the wallet (or uncheck “Encrypt the wallet file”).",
|
||||
"Ingresa una contraseña para cifrar el wallet (o desmarca «Cifrar el archivo wallet»).",
|
||||
"Entrez un mot de passe pour chiffrer le wallet (ou décochez « Chiffrer le fichier wallet »).",
|
||||
"Digite uma senha para criptografar a carteira (ou desmarque «Criptografar o arquivo da carteira»).",
|
||||
"Geben Sie ein Passwort zum Verschlüsseln ein (oder deaktivieren Sie „Wallet-Datei verschlüsseln“)."],
|
||||
["msg.password.mismatch"] = [
|
||||
"Le due password non coincidono.",
|
||||
"The two passwords do not match.",
|
||||
"Las dos contraseñas no coinciden.",
|
||||
"Les deux mots de passe ne correspondent pas.",
|
||||
"As duas senhas não coincidem.",
|
||||
"Die beiden Passwörter stimmen nicht überein."],
|
||||
["msg.wrongpassword"] = ["Password errata.", "Wrong password.", "Contraseña incorrecta.", "Mot de passe incorrect.", "Senha incorreta.", "Falsches Passwort."],
|
||||
["msg.opened"] = ["Wallet aperto: connessione al server…", "Wallet opened: connecting to server…", "Wallet abierto: conectando al servidor…", "Wallet ouvert : connexion au serveur…", "Carteira aberta: conectando ao servidor…", "Wallet geöffnet: Verbindung zum Server…"],
|
||||
["msg.synced"] = ["Sincronizzato", "Synchronized", "Sincronizado", "Synchronisé", "Sincronizado", "Synchronisiert"],
|
||||
@@ -243,5 +279,13 @@ public sealed class Loc
|
||||
["settings.unit"] = ["Unità degli importi", "Amount unit", "Unidad de importes", "Unité des montants", "Unidade dos valores", "Betrageinheit"],
|
||||
["settings.ok"] = ["Salva", "Save", "Guardar", "Enregistrer", "Salvar", "Speichern"],
|
||||
["settings.cancel"] = ["Annulla", "Cancel", "Cancelar", "Annuler", "Cancelar", "Abbrechen"],
|
||||
["settings.server"] = ["Server di indicizzazione…", "Indexing server…", "Servidor de indexación…", "Serveur d'indexation…", "Servidor de indexação…", "Indexierungsserver…"],
|
||||
|
||||
// Finestra server
|
||||
["server.title"] = ["Server di indicizzazione", "Indexing server", "Servidor de indexación", "Serveur d'indexation", "Servidor de indexação", "Indexierungsserver"],
|
||||
["server.host"] = ["Host", "Host", "Host", "Hôte", "Host", "Host"],
|
||||
["server.port"] = ["Porta", "Port", "Puerto", "Port", "Porta", "Port"],
|
||||
["server.known"] = ["Server conosciuti (clicca per usarlo):", "Known servers (click to use):", "Servidores conocidos (clic para usar):", "Serveurs connus (cliquez pour utiliser) :", "Servidores conhecidos (clique para usar):", "Bekannte Server (zum Verwenden anklicken):"],
|
||||
["server.empty"] = ["Nessun server conosciuto. Usa «Cerca altri server» dopo esserti connesso.", "No known servers. Use “Discover servers” after connecting.", "No hay servidores conocidos. Usa «Buscar otros servidores» tras conectar.", "Aucun serveur connu. Utilisez « Rechercher des serveurs » après connexion.", "Nenhum servidor conhecido. Use «Procurar servidores» após conectar.", "Keine bekannten Server. Nutzen Sie „Server suchen“ nach dem Verbinden."],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
|
||||
<PackageReference Include="QRCoder" Version="1.8.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
@@ -15,6 +16,7 @@ using PalladiumWallet.Core.Net;
|
||||
using PalladiumWallet.Core.Spv;
|
||||
using PalladiumWallet.Core.Storage;
|
||||
using PalladiumWallet.Core.Wallet;
|
||||
using QRCoder;
|
||||
|
||||
namespace PalladiumWallet.App.ViewModels;
|
||||
|
||||
@@ -40,6 +42,9 @@ public sealed record AddressInfo(
|
||||
/// <summary>Contatto in rubrica: nome + indirizzo blockchain.</summary>
|
||||
public sealed record ContactEntry(string Name, string Address);
|
||||
|
||||
/// <summary>Voce della lista di scelta wallet: nome file + percorso completo.</summary>
|
||||
public sealed record WalletFileEntry(string Name, string Path);
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel unico dell'applicazione (wizard §15 ridotto + dashboard):
|
||||
/// pannello di setup (crea/ripristina/apri) e pannello wallet
|
||||
@@ -165,7 +170,9 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
|
||||
// ---- wizard di setup (§15): un passo alla volta ----
|
||||
|
||||
public const string StepDataLocation = "data-location";
|
||||
public const string StepStart = "start";
|
||||
public const string StepChooseWallet = "choose-wallet";
|
||||
public const string StepOpen = "open";
|
||||
public const string StepShowSeed = "show-seed";
|
||||
public const string StepConfirmSeed = "confirm-seed";
|
||||
@@ -174,7 +181,9 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
public const string StepPassword = "password";
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsStepDataLocation))]
|
||||
[NotifyPropertyChangedFor(nameof(IsStepStart))]
|
||||
[NotifyPropertyChangedFor(nameof(IsStepChooseWallet))]
|
||||
[NotifyPropertyChangedFor(nameof(IsStepOpen))]
|
||||
[NotifyPropertyChangedFor(nameof(IsStepShowSeed))]
|
||||
[NotifyPropertyChangedFor(nameof(IsStepConfirmSeed))]
|
||||
@@ -183,7 +192,9 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
[NotifyPropertyChangedFor(nameof(IsStepPassword))]
|
||||
private string setupStep = StepStart;
|
||||
|
||||
public bool IsStepDataLocation => SetupStep == StepDataLocation;
|
||||
public bool IsStepStart => SetupStep == StepStart;
|
||||
public bool IsStepChooseWallet => SetupStep == StepChooseWallet;
|
||||
public bool IsStepOpen => SetupStep == StepOpen;
|
||||
public bool IsStepShowSeed => SetupStep == StepShowSeed;
|
||||
public bool IsStepConfirmSeed => SetupStep == StepConfirmSeed;
|
||||
@@ -191,6 +202,9 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
public bool IsStepPassphrase => SetupStep == StepPassphrase;
|
||||
public bool IsStepPassword => SetupStep == StepPassword;
|
||||
|
||||
/// <summary>Percorso dati predefinito, mostrato al primo avvio.</summary>
|
||||
public string DefaultDataPath => AppPaths.DefaultDataRoot();
|
||||
|
||||
/// <summary>True quando il flusso è "ripristina" (parole inserite dall'utente).</summary>
|
||||
private bool _isRestoreFlow;
|
||||
|
||||
@@ -206,6 +220,14 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
[ObservableProperty]
|
||||
private string passwordInput = "";
|
||||
|
||||
/// <summary>Conferma password alla creazione (stile Electrum: digitata due volte).</summary>
|
||||
[ObservableProperty]
|
||||
private string confirmPasswordInput = "";
|
||||
|
||||
/// <summary>Se true il file wallet viene cifrato con la password (default, stile Electrum).</summary>
|
||||
[ObservableProperty]
|
||||
private bool encryptWallet = true;
|
||||
|
||||
// ---- pannello wallet ----
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -220,11 +242,68 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
[ObservableProperty]
|
||||
private string receiveAddress = "";
|
||||
|
||||
/// <summary>QR code dell'indirizzo di ricezione corrente (PNG in-memory).</summary>
|
||||
[ObservableProperty]
|
||||
private string serverInput = "";
|
||||
private Bitmap? receiveQr;
|
||||
|
||||
// Rigenera il QR ogni volta che l'indirizzo di ricezione cambia.
|
||||
partial void OnReceiveAddressChanged(string value)
|
||||
{
|
||||
var previous = ReceiveQr;
|
||||
ReceiveQr = string.IsNullOrEmpty(value) ? null : GenerateQr(value);
|
||||
previous?.Dispose();
|
||||
}
|
||||
|
||||
private static Bitmap? GenerateQr(string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var generator = new QRCodeGenerator();
|
||||
using var data = generator.CreateQrCode(text, QRCodeGenerator.ECCLevel.M);
|
||||
var png = new PngByteQRCode(data).GetGraphic(8);
|
||||
return new Bitmap(new MemoryStream(png));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private bool useSsl;
|
||||
private string serverHost = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string serverPort = "";
|
||||
|
||||
/// <summary>Si predilige TLS: la connessione automatica all'apertura del
|
||||
/// wallet usa la porta SSL del server. L'utente può disattivarlo.</summary>
|
||||
[ObservableProperty]
|
||||
private bool useSsl = true;
|
||||
|
||||
/// <summary>Overlay impostazioni server: aperto dall'overlay Impostazioni.</summary>
|
||||
[ObservableProperty]
|
||||
private bool isServerSettingsOpen;
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenServerSettings()
|
||||
{
|
||||
IsSettingsOpen = false;
|
||||
IsServerSettingsOpen = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CloseServerSettings() => IsServerSettingsOpen = false;
|
||||
|
||||
/// <summary>Overlay impostazioni (lingua, unità, server): in-app per evitare
|
||||
/// i popup di menu annidati, lenti su WSLg.</summary>
|
||||
[ObservableProperty]
|
||||
private bool isSettingsOpen;
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenSettings() => IsSettingsOpen = true;
|
||||
|
||||
[RelayCommand]
|
||||
private void CloseSettings() => IsSettingsOpen = false;
|
||||
|
||||
[ObservableProperty]
|
||||
private string connectionStatus = Loc.Tr("conn.none");
|
||||
@@ -244,11 +323,25 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
|
||||
public ObservableCollection<AddressRow> Addresses { get; } = [];
|
||||
|
||||
/// <summary>Wallet disponibili nella cartella della rete, per la schermata di scelta.</summary>
|
||||
public ObservableCollection<WalletFileEntry> WalletList { get; } = [];
|
||||
|
||||
// ---- tab indirizzi ----
|
||||
|
||||
[ObservableProperty]
|
||||
private AddressRow? selectedAddressRow;
|
||||
|
||||
/// <summary>Dettaglio indirizzo mostrato nell'overlay in-app; null = nascosto.</summary>
|
||||
[ObservableProperty]
|
||||
private AddressInfo? addressInfo;
|
||||
|
||||
/// <summary>Apre l'overlay con i dati dell'indirizzo passato.</summary>
|
||||
public void ShowAddressInfo(AddressRow row) =>
|
||||
AddressInfo = new AddressInfo(Loc, row.Indirizzo, row.DerivPath, row.PubKey, row.PrivKey);
|
||||
|
||||
[RelayCommand]
|
||||
private void CloseAddressInfo() => AddressInfo = null;
|
||||
|
||||
// ---- rubrica contatti ----
|
||||
|
||||
public ObservableCollection<ContactEntry> Contacts { get; } = [];
|
||||
@@ -334,7 +427,12 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
public MainWindowViewModel()
|
||||
{
|
||||
_loc = Loc.SwitchTo(_config.Language);
|
||||
RefreshSetupState();
|
||||
// Primo avvio: se la posizione dei dati non è ancora determinata,
|
||||
// chiediamola prima di tutto il resto del wizard.
|
||||
if (!AppPaths.IsDataLocationConfigured())
|
||||
SetupStep = StepDataLocation;
|
||||
else
|
||||
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.
|
||||
@@ -345,7 +443,9 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
|
||||
private async Task KeepAliveTickAsync()
|
||||
{
|
||||
if (!IsWalletOpen || IsSyncing)
|
||||
// Vale anche senza wallet aperto: se l'utente si è connesso dalle
|
||||
// impostazioni server, la connessione va mantenuta e riconnessa.
|
||||
if (IsSyncing)
|
||||
return;
|
||||
if (_client is { IsConnected: true })
|
||||
{
|
||||
@@ -372,15 +472,46 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
|
||||
private ServerRegistry Registry => new(Profile, AppPaths.ServersPath(Net));
|
||||
|
||||
/// <summary>Primo avvio: usa il percorso dati predefinito di piattaforma.</summary>
|
||||
[RelayCommand]
|
||||
private void UseDefaultDataLocation() => ApplyDataLocation(AppPaths.DefaultDataRoot());
|
||||
|
||||
/// <summary>
|
||||
/// Memorizza la radice dati scelta (default o personalizzata), ricarica la
|
||||
/// configurazione dalla nuova posizione e prosegue col wizard. Chiamata anche
|
||||
/// dalla View dopo la scelta di una cartella.
|
||||
/// </summary>
|
||||
public void ApplyDataLocation(string root)
|
||||
{
|
||||
try
|
||||
{
|
||||
AppPaths.ConfigureDataLocation(root);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"{Loc.Tr("msg.error")}: {ex.Message}";
|
||||
return;
|
||||
}
|
||||
// La config globale vive nella radice dati: ricaricala da lì.
|
||||
_config = AppConfig.Load();
|
||||
_loc = Loc.SwitchTo(_config.Language);
|
||||
OnPropertyChanged(nameof(Loc));
|
||||
OnPropertyChanged(nameof(UnitLabel));
|
||||
RefreshSetupState();
|
||||
}
|
||||
|
||||
private void RefreshSetupState()
|
||||
{
|
||||
SetupStep = StepStart;
|
||||
MnemonicInput = ConfirmMnemonicInput = PassphraseInput = PasswordInput = "";
|
||||
WalletFileExists = WalletStore.Exists(AppPaths.DefaultWalletPath(Net));
|
||||
MnemonicInput = ConfirmMnemonicInput = PassphraseInput = PasswordInput = ConfirmPasswordInput = "";
|
||||
WalletFileExists = AppPaths.WalletFiles(Net).Count > 0;
|
||||
StatusMessage = WalletFileExists
|
||||
? Loc.Tr("msg.welcome.existing")
|
||||
: Loc.Tr("msg.welcome.new");
|
||||
RefreshServers();
|
||||
// Come Electrum: ci si connette al server già durante il setup, senza
|
||||
// aspettare l'apertura di un wallet (la sync parte poi all'apertura).
|
||||
_ = ConnectAndSync();
|
||||
}
|
||||
|
||||
private void RefreshServers()
|
||||
@@ -390,19 +521,59 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
KnownServers.Add(server);
|
||||
SelectedKnownServer = KnownServers.FirstOrDefault();
|
||||
if (SelectedKnownServer is null)
|
||||
ServerInput = $"127.0.0.1:{Profile.DefaultTcpPort}";
|
||||
{
|
||||
ServerHost = "127.0.0.1";
|
||||
ServerPort = (UseSsl ? Profile.DefaultSslPort : Profile.DefaultTcpPort).ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Evita la ricorsione tra OnUseSslChanged e OnServerPortChanged
|
||||
/// mentre tengono coerenti porta e flag TLS.</summary>
|
||||
private bool _syncingServerFields;
|
||||
|
||||
partial void OnSelectedKnownServerChanged(KnownServer? value)
|
||||
{
|
||||
if (value is not null)
|
||||
ServerInput = $"{value.Host}:{value.PortFor(UseSsl)}";
|
||||
if (value is null)
|
||||
return;
|
||||
_syncingServerFields = true;
|
||||
ServerHost = value.Host;
|
||||
ServerPort = value.PortFor(UseSsl).ToString();
|
||||
_syncingServerFields = false;
|
||||
}
|
||||
|
||||
partial void OnUseSslChanged(bool value)
|
||||
{
|
||||
if (SelectedKnownServer is { } server)
|
||||
ServerInput = $"{server.Host}:{server.PortFor(value)}";
|
||||
if (_syncingServerFields)
|
||||
return;
|
||||
// Attivare/disattivare TLS allinea la porta: porta SSL del server
|
||||
// selezionato (o default di profilo) quando TLS è on, porta TCP quando off.
|
||||
_syncingServerFields = true;
|
||||
ServerPort = SelectedKnownServer is { } server
|
||||
? server.PortFor(value).ToString()
|
||||
: (value ? Profile.DefaultSslPort : Profile.DefaultTcpPort).ToString();
|
||||
_syncingServerFields = false;
|
||||
}
|
||||
|
||||
partial void OnServerPortChanged(string value)
|
||||
{
|
||||
if (_syncingServerFields)
|
||||
return;
|
||||
if (!int.TryParse(value.Trim(), out var port))
|
||||
return;
|
||||
// Scegliere una porta nota allinea il flag TLS, così non si tenta mai
|
||||
// una connessione in chiaro sulla porta SSL (causa di "connection error").
|
||||
bool? wantSsl =
|
||||
SelectedKnownServer is { } s && port == s.SslPort ? true :
|
||||
SelectedKnownServer is { } t && port == t.TcpPort ? false :
|
||||
port == Profile.DefaultSslPort ? true :
|
||||
port == Profile.DefaultTcpPort ? false :
|
||||
null;
|
||||
if (wantSsl is bool b && b != UseSsl)
|
||||
{
|
||||
_syncingServerFields = true;
|
||||
UseSsl = b;
|
||||
_syncingServerFields = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- comandi del wizard (§15): un passo alla volta ----------
|
||||
@@ -410,6 +581,31 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
[RelayCommand]
|
||||
private void WizardStartOpen()
|
||||
{
|
||||
// Con più wallet nella cartella della rete si sceglie quale aprire;
|
||||
// con uno solo si va dritti alla password (multi-wallet §8).
|
||||
var files = AppPaths.WalletFiles(Net);
|
||||
if (files.Count > 1)
|
||||
{
|
||||
WalletList.Clear();
|
||||
foreach (var path in files)
|
||||
WalletList.Add(new WalletFileEntry(Path.GetFileName(path), path));
|
||||
SetupStep = StepChooseWallet;
|
||||
StatusMessage = Loc.Tr("msg.choose.wallet");
|
||||
return;
|
||||
}
|
||||
_pendingOpenPath = files.Count == 1 ? files[0] : AppPaths.DefaultWalletPath(Net);
|
||||
PasswordInput = "";
|
||||
SetupStep = StepOpen;
|
||||
StatusMessage = Loc.Tr("msg.open.password");
|
||||
}
|
||||
|
||||
/// <summary>Sceglie quale wallet aprire dalla lista e passa alla password.</summary>
|
||||
[RelayCommand]
|
||||
private void ChooseWallet(WalletFileEntry? entry)
|
||||
{
|
||||
if (entry is null)
|
||||
return;
|
||||
_pendingOpenPath = entry.Path;
|
||||
PasswordInput = "";
|
||||
SetupStep = StepOpen;
|
||||
StatusMessage = Loc.Tr("msg.open.password");
|
||||
@@ -475,7 +671,8 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
[RelayCommand]
|
||||
private void WizardNextFromPassphrase()
|
||||
{
|
||||
PasswordInput = "";
|
||||
PasswordInput = ConfirmPasswordInput = "";
|
||||
EncryptWallet = true;
|
||||
SetupStep = StepPassword;
|
||||
StatusMessage = Loc.Tr("msg.password.info");
|
||||
}
|
||||
@@ -485,7 +682,8 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
SetupStep = SetupStep switch
|
||||
{
|
||||
StepOpen or StepShowSeed or StepWords => StepStart,
|
||||
StepOpen => WalletList.Count > 1 ? StepChooseWallet : StepStart,
|
||||
StepChooseWallet or StepShowSeed or StepWords => StepStart,
|
||||
StepConfirmSeed => StepShowSeed,
|
||||
StepPassphrase => _isRestoreFlow ? StepWords : StepConfirmSeed,
|
||||
StepPassword => StepPassphrase,
|
||||
@@ -498,6 +696,29 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
[RelayCommand]
|
||||
private void CreateOrRestore()
|
||||
{
|
||||
// Scelta cifratura stile Electrum: con cifratura attiva serve una
|
||||
// password non vuota, digitata due volte e coincidente. Solo se la
|
||||
// cifratura è disattivata il file resta in chiaro (avviso esplicito).
|
||||
string? password;
|
||||
if (EncryptWallet)
|
||||
{
|
||||
if (string.IsNullOrEmpty(PasswordInput))
|
||||
{
|
||||
StatusMessage = Loc.Tr("msg.password.required");
|
||||
return;
|
||||
}
|
||||
if (PasswordInput != ConfirmPasswordInput)
|
||||
{
|
||||
StatusMessage = Loc.Tr("msg.password.mismatch");
|
||||
return;
|
||||
}
|
||||
password = PasswordInput;
|
||||
}
|
||||
else
|
||||
{
|
||||
password = null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var (doc, account) = WalletLoader.NewFromMnemonic(
|
||||
@@ -509,7 +730,6 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
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;
|
||||
WalletStore.Save(doc, path, password);
|
||||
OpenLoaded(doc, account, path, password);
|
||||
}
|
||||
@@ -585,7 +805,7 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
_account = account;
|
||||
_walletPath = path;
|
||||
_password = password;
|
||||
MnemonicInput = ConfirmMnemonicInput = PassphraseInput = PasswordInput = "";
|
||||
MnemonicInput = ConfirmMnemonicInput = PassphraseInput = PasswordInput = ConfirmPasswordInput = "";
|
||||
SetupStep = StepStart;
|
||||
|
||||
NetworkInfo = $"{doc.Network} · {doc.ScriptKind} · m/{doc.AccountPath}"
|
||||
@@ -656,8 +876,6 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
[RelayCommand]
|
||||
private async Task ConnectAndSync()
|
||||
{
|
||||
if (_account is null || _doc is null)
|
||||
return;
|
||||
if (IsSyncing)
|
||||
{
|
||||
_resyncRequested = true;
|
||||
@@ -667,9 +885,19 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
StatusMessage = "";
|
||||
try
|
||||
{
|
||||
var (host, port) = ParseServer();
|
||||
|
||||
// Se l'endpoint richiesto (host/porta/TLS) è diverso da quello della
|
||||
// connessione attiva, la chiudo: l'utente ha cambiato server o ha
|
||||
// attivato TLS e si aspetta che la nuova scelta abbia effetto.
|
||||
if (_client is { } current &&
|
||||
(current.Host != host || current.Port != port || current.UseSsl != UseSsl))
|
||||
{
|
||||
await DisconnectAsync();
|
||||
}
|
||||
|
||||
if (_client is null || !_client.IsConnected)
|
||||
{
|
||||
var (host, port) = ParseServer();
|
||||
ConnectionStatus = $"{Loc.Tr("conn.connectingto")} {host}:{port}…";
|
||||
var pins = new CertificatePinStore(AppPaths.CertificatePinsPath(Net));
|
||||
_client = await ElectrumClient.ConnectAsync(host, port, UseSsl, pins);
|
||||
@@ -682,8 +910,18 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
IsConnected = true;
|
||||
_autoReconnect = true;
|
||||
ConnectionStatus = $"{Loc.Tr("conn.connectedto")} {host}:{port}{(UseSsl ? " (TLS)" : "")}";
|
||||
// Synchronizer per connessione: conserva la cache di tx e
|
||||
// prove verificate, così le risincronizzazioni sono incrementali.
|
||||
}
|
||||
|
||||
// Senza un wallet aperto ci si limita a stabilire/verificare la
|
||||
// connessione: la sincronizzazione richiede un account.
|
||||
if (_account is null || _doc is null)
|
||||
return;
|
||||
|
||||
// Synchronizer legato all'account corrente, creato alla prima sync
|
||||
// dopo la connessione: conserva la cache di tx e prove verificate,
|
||||
// così le risincronizzazioni sono incrementali.
|
||||
if (_synchronizer is null)
|
||||
{
|
||||
_synchronizer = new WalletSynchronizer(_account, _client, _doc.GapLimit);
|
||||
_synchronizer.Progress += msg => Dispatcher.UIThread.Post(() => StatusMessage = msg);
|
||||
}
|
||||
@@ -693,7 +931,7 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
do
|
||||
{
|
||||
_resyncRequested = false;
|
||||
var result = await _synchronizer!.SyncOnceAsync();
|
||||
var result = await _synchronizer.SyncOnceAsync();
|
||||
_lastTransactions = result.Transactions;
|
||||
|
||||
_doc.Cache = new SyncCache
|
||||
@@ -847,6 +1085,22 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chiude la connessione corrente lasciando il wallet aperto: usata quando
|
||||
/// l'utente cambia server o attiva TLS e serve riconnettersi al nuovo
|
||||
/// endpoint. Attende la chiusura del socket prima di tornare.
|
||||
/// </summary>
|
||||
private async Task DisconnectAsync()
|
||||
{
|
||||
if (_client is { } client)
|
||||
{
|
||||
_client = null;
|
||||
_synchronizer = null;
|
||||
try { await client.DisposeAsync(); } catch { /* in chiusura */ }
|
||||
}
|
||||
IsConnected = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CloseWallet()
|
||||
{
|
||||
@@ -873,9 +1127,8 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
|
||||
private (string Host, int Port) ParseServer()
|
||||
{
|
||||
var parts = ServerInput.Trim().Split(':');
|
||||
var host = parts[0];
|
||||
var port = parts.Length > 1 && int.TryParse(parts[1], out var p)
|
||||
var host = ServerHost.Trim();
|
||||
var port = int.TryParse(ServerPort.Trim(), out var p)
|
||||
? p
|
||||
: UseSsl ? Profile.DefaultSslPort : Profile.DefaultTcpPort;
|
||||
return (host, port);
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<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>
|
||||
@@ -1,20 +0,0 @@
|
||||
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();
|
||||
}
|
||||
+294
-91
@@ -1,6 +1,7 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:PalladiumWallet.App.ViewModels"
|
||||
xmlns:net="using:PalladiumWallet.Core.Net"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="620"
|
||||
@@ -25,46 +26,8 @@
|
||||
<MenuItem Header="{Binding Loc[menu.file.close]}" Command="{Binding CloseWalletCommand}"
|
||||
IsEnabled="{Binding IsWalletOpen}"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="{Binding Loc[menu.net]}">
|
||||
<MenuItem Header="{Binding Loc[menu.net.discover]}" Command="{Binding DiscoverServersCommand}"/>
|
||||
<MenuItem Header="{Binding Loc[menu.net.resetcerts]}" Command="{Binding ResetCertificatesCommand}"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="{Binding Loc[menu.settings]}">
|
||||
<MenuItem Header="{Binding Loc[settings.language]}">
|
||||
<MenuItem Header="Italiano" ToggleType="Radio"
|
||||
IsChecked="{Binding IsLangIt, Mode=OneWay}"
|
||||
Command="{Binding SetLanguageCommand}" CommandParameter="it"/>
|
||||
<MenuItem Header="English" ToggleType="Radio"
|
||||
IsChecked="{Binding IsLangEn, Mode=OneWay}"
|
||||
Command="{Binding SetLanguageCommand}" CommandParameter="en"/>
|
||||
<MenuItem Header="Español" ToggleType="Radio"
|
||||
IsChecked="{Binding IsLangEs, Mode=OneWay}"
|
||||
Command="{Binding SetLanguageCommand}" CommandParameter="es"/>
|
||||
<MenuItem Header="Français" ToggleType="Radio"
|
||||
IsChecked="{Binding IsLangFr, Mode=OneWay}"
|
||||
Command="{Binding SetLanguageCommand}" CommandParameter="fr"/>
|
||||
<MenuItem Header="Português" ToggleType="Radio"
|
||||
IsChecked="{Binding IsLangPt, Mode=OneWay}"
|
||||
Command="{Binding SetLanguageCommand}" CommandParameter="pt"/>
|
||||
<MenuItem Header="Deutsch" ToggleType="Radio"
|
||||
IsChecked="{Binding IsLangDe, Mode=OneWay}"
|
||||
Command="{Binding SetLanguageCommand}" CommandParameter="de"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="{Binding Loc[settings.unit.short]}">
|
||||
<MenuItem Header="PLM" ToggleType="Radio"
|
||||
IsChecked="{Binding IsUnitPlm, Mode=OneWay}"
|
||||
Command="{Binding SetUnitCommand}" CommandParameter="PLM"/>
|
||||
<MenuItem Header="mPLM" ToggleType="Radio"
|
||||
IsChecked="{Binding IsUnitMilli, Mode=OneWay}"
|
||||
Command="{Binding SetUnitCommand}" CommandParameter="mPLM"/>
|
||||
<MenuItem Header="µPLM" ToggleType="Radio"
|
||||
IsChecked="{Binding IsUnitMicro, Mode=OneWay}"
|
||||
Command="{Binding SetUnitCommand}" CommandParameter="µPLM"/>
|
||||
<MenuItem Header="sat" ToggleType="Radio"
|
||||
IsChecked="{Binding IsUnitSat, Mode=OneWay}"
|
||||
Command="{Binding SetUnitCommand}" CommandParameter="sat"/>
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
<MenuItem Header="{Binding Loc[menu.settings]}"
|
||||
Command="{Binding OpenSettingsCommand}"/>
|
||||
</Menu>
|
||||
|
||||
<!-- ============ WIZARD DI SETUP (§15): un passo alla volta ============ -->
|
||||
@@ -74,6 +37,23 @@
|
||||
<TextBlock Text="Palladium Wallet" FontSize="28" FontWeight="Bold"
|
||||
HorizontalAlignment="Center"/>
|
||||
|
||||
<!-- Passo 0 (primo avvio): dove salvare i dati -->
|
||||
<StackPanel IsVisible="{Binding IsStepDataLocation}" Spacing="12">
|
||||
<TextBlock Text="{Binding Loc[wiz.data.title]}" FontSize="18" FontWeight="Bold"/>
|
||||
<TextBlock Text="{Binding Loc[wiz.data.info]}" TextWrapping="Wrap" Foreground="Gray"/>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="{Binding Loc[wiz.data.default]}" FontSize="11" Foreground="Gray"/>
|
||||
<SelectableTextBlock Text="{Binding DefaultDataPath}"
|
||||
FontFamily="monospace" FontSize="13" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
<Button Content="{Binding Loc[wiz.data.usedefault]}" FontSize="16" Classes="accent"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
|
||||
Command="{Binding UseDefaultDataLocationCommand}"/>
|
||||
<Button Content="{Binding Loc[wiz.data.choose]}" FontSize="16"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
|
||||
Click="OnChooseDataFolderClick"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Passo 1: scelta iniziale -->
|
||||
<StackPanel IsVisible="{Binding IsStepStart}" Spacing="12">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Center">
|
||||
@@ -93,6 +73,23 @@
|
||||
Command="{Binding WizardStartRestoreCommand}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Passo: scelta del wallet (più file presenti) -->
|
||||
<StackPanel IsVisible="{Binding IsStepChooseWallet}" Spacing="12">
|
||||
<TextBlock Text="{Binding Loc[wiz.choose.title]}" FontSize="18" FontWeight="Bold"/>
|
||||
<ItemsControl ItemsSource="{Binding WalletList}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:WalletFileEntry">
|
||||
<Button Content="{Binding Name}" FontFamily="monospace"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||
Margin="0,0,0,6"
|
||||
Command="{Binding $parent[ItemsControl].((vm:MainWindowViewModel)DataContext).ChooseWalletCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<Button Content="{Binding Loc[wiz.back]}" Command="{Binding WizardBackCommand}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Passo: password del wallet esistente -->
|
||||
<StackPanel IsVisible="{Binding IsStepOpen}" Spacing="12">
|
||||
<TextBlock Text="{Binding Loc[wiz.open.title]}" FontSize="18" FontWeight="Bold"/>
|
||||
@@ -158,11 +155,18 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Passo finale: password del file -->
|
||||
<!-- Passo finale: password del file (cifratura, stile Electrum) -->
|
||||
<StackPanel IsVisible="{Binding IsStepPassword}" Spacing="12">
|
||||
<TextBlock Text="{Binding Loc[wiz.password.title]}" FontSize="18" FontWeight="Bold"/>
|
||||
<TextBox PlaceholderText="{Binding Loc[wiz.password.placeholder]}"
|
||||
PasswordChar="●" Text="{Binding PasswordInput}"/>
|
||||
<TextBox PlaceholderText="{Binding Loc[wiz.password.confirm]}"
|
||||
PasswordChar="●" Text="{Binding ConfirmPasswordInput}"/>
|
||||
<CheckBox Content="{Binding Loc[wiz.password.encrypt]}"
|
||||
IsChecked="{Binding EncryptWallet}"/>
|
||||
<TextBlock Text="{Binding Loc[wiz.password.encrypt.hint]}"
|
||||
Foreground="Gray" FontSize="12" TextWrapping="Wrap"
|
||||
IsVisible="{Binding !EncryptWallet}"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<Button Content="{Binding Loc[wiz.back]}" Command="{Binding WizardBackCommand}"/>
|
||||
<Button Content="{Binding Loc[wiz.password.create]}" Classes="accent"
|
||||
@@ -173,57 +177,18 @@
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ============ PANNELLO WALLET ============ -->
|
||||
<Grid Grid.Row="1" RowDefinitions="Auto,Auto,*" IsVisible="{Binding IsWalletOpen}" Margin="16">
|
||||
<Grid Grid.Row="1" RowDefinitions="Auto,*" IsVisible="{Binding IsWalletOpen}" Margin="16">
|
||||
|
||||
<!-- Testata: saldo + rete + chiudi -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="2">
|
||||
<TextBlock Text="{Binding BalanceText}" FontSize="30" FontWeight="Bold"/>
|
||||
<TextBlock Text="{Binding UnconfirmedText}" Foreground="Orange"/>
|
||||
<TextBlock Text="{Binding NetworkInfo}" FontSize="12" Foreground="Gray"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Content="{Binding Loc[wallet.close]}" VerticalAlignment="Top"
|
||||
Command="{Binding CloseWalletCommand}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Server -->
|
||||
<Border Grid.Row="1" Margin="0,12,0,12" Padding="10"
|
||||
BorderBrush="Gray" BorderThickness="1" CornerRadius="6">
|
||||
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*,Auto,Auto">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Loc[wallet.server]}"
|
||||
VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<ComboBox Grid.Row="0" Grid.Column="1"
|
||||
ItemsSource="{Binding KnownServers}"
|
||||
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="{Binding Loc[wallet.connect]}"
|
||||
Command="{Binding ConnectAndSyncCommand}" IsEnabled="{Binding !IsSyncing}"/>
|
||||
|
||||
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal"
|
||||
Spacing="8" Margin="0,8,0,0">
|
||||
<TextBox Text="{Binding ServerInput}" MinWidth="220"
|
||||
PlaceholderText="{Binding Loc[wallet.manual]}"/>
|
||||
<Button Content="{Binding Loc[wallet.discover]}"
|
||||
Command="{Binding DiscoverServersCommand}"/>
|
||||
<Button Content="{Binding Loc[wallet.resetcert]}"
|
||||
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>
|
||||
</Border>
|
||||
<!-- Testata: saldo + rete. Connetti/sincronizza è automatico; chiudi
|
||||
wallet è in File. Lo stato connessione è nella barra in basso. -->
|
||||
<StackPanel Grid.Row="0" Spacing="2" Margin="0,0,0,12">
|
||||
<TextBlock Text="{Binding BalanceText}" FontSize="30" FontWeight="Bold"/>
|
||||
<TextBlock Text="{Binding UnconfirmedText}" Foreground="Orange"/>
|
||||
<TextBlock Text="{Binding NetworkInfo}" FontSize="12" Foreground="Gray"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Tab: Storico / Invia / Ricevi / Indirizzi / Contatti -->
|
||||
<TabControl Grid.Row="2">
|
||||
<TabControl Grid.Row="1">
|
||||
|
||||
<!-- 1. Storico -->
|
||||
<TabItem Header="{Binding Loc[tab.history]}">
|
||||
@@ -290,6 +255,12 @@
|
||||
<TextBlock Text="{Binding Loc[receive.next]}"/>
|
||||
<SelectableTextBlock Text="{Binding ReceiveAddress}"
|
||||
FontFamily="monospace" FontSize="16"/>
|
||||
<Border Background="White" Padding="12" CornerRadius="6"
|
||||
HorizontalAlignment="Left"
|
||||
IsVisible="{Binding ReceiveQr, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<Image Source="{Binding ReceiveQr}" Width="220" Height="220"
|
||||
RenderOptions.BitmapInterpolationMode="None"/>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding Loc[receive.hint]}"
|
||||
Foreground="Gray" FontSize="12" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
@@ -370,10 +341,242 @@
|
||||
</TabControl>
|
||||
</Grid>
|
||||
|
||||
<!-- Barra di stato -->
|
||||
<!-- Barra di stato: messaggio a sinistra, stato connessione a destra.
|
||||
Lo stato connessione è cliccabile e apre le impostazioni del server. -->
|
||||
<Border Grid.Row="2" Background="{DynamicResource SystemControlBackgroundChromeMediumBrush}"
|
||||
Padding="10,6">
|
||||
<TextBlock Text="{Binding StatusMessage}" FontSize="12" TextWrapping="Wrap"/>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding StatusMessage}" FontSize="12"
|
||||
TextWrapping="Wrap" VerticalAlignment="Center"/>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6"
|
||||
Margin="12,0,0,0" VerticalAlignment="Center"
|
||||
Cursor="Hand" Background="Transparent"
|
||||
ToolTip.Tip="{Binding Loc[server.title]}"
|
||||
Tapped="OnConnectionStatusTapped">
|
||||
<Ellipse Width="10" Height="10" Fill="LimeGreen" VerticalAlignment="Center"
|
||||
IsVisible="{Binding IsConnected}"/>
|
||||
<Ellipse Width="10" Height="10" Fill="IndianRed" VerticalAlignment="Center"
|
||||
IsVisible="{Binding !IsConnected}"/>
|
||||
<TextBlock Text="{Binding ConnectionStatus}" FontSize="12"
|
||||
Foreground="{DynamicResource SystemAccentColor}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ============ OVERLAY DETTAGLIO INDIRIZZO ============ -->
|
||||
<!-- Overlay in-app invece di una Window separata: apertura/chiusura
|
||||
istantanee (niente create/destroy di una top-level window). -->
|
||||
<Border Grid.Row="0" Grid.RowSpan="3"
|
||||
Background="#99000000"
|
||||
Tapped="OnAddressOverlayBackdropTapped"
|
||||
IsVisible="{Binding AddressInfo, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<Border Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}"
|
||||
BorderBrush="Gray" BorderThickness="1" CornerRadius="8"
|
||||
Width="560" Padding="0"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
DataContext="{Binding AddressInfo}">
|
||||
<StackPanel Margin="24" Spacing="16">
|
||||
<TextBlock Text="{Binding Loc[addr.info.title]}"
|
||||
FontSize="18" FontWeight="Bold"/>
|
||||
|
||||
<!-- 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"
|
||||
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).CloseAddressInfoCommand}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
<!-- ============ OVERLAY IMPOSTAZIONI SERVER ============ -->
|
||||
<!-- Stesso pattern dell'overlay indirizzo: apertura/chiusura istantanee.
|
||||
Accessibile da Impostazioni → Server. -->
|
||||
<Border Grid.Row="0" Grid.RowSpan="3"
|
||||
Background="#99000000"
|
||||
Tapped="OnServerOverlayBackdropTapped"
|
||||
IsVisible="{Binding IsServerSettingsOpen}">
|
||||
<Border Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}"
|
||||
BorderBrush="Gray" BorderThickness="1" CornerRadius="8"
|
||||
Width="560" MaxHeight="540"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<StackPanel Margin="24" Spacing="14">
|
||||
<TextBlock Text="{Binding Loc[server.title]}"
|
||||
FontSize="18" FontWeight="Bold"/>
|
||||
|
||||
<!-- Host + porta separati -->
|
||||
<Grid ColumnDefinitions="*,Auto,Auto" RowDefinitions="Auto,Auto">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Loc[server.host]}"
|
||||
FontSize="11" Foreground="Gray"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Loc[server.port]}"
|
||||
FontSize="11" Foreground="Gray" Margin="10,0,0,0" Width="90"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="2" Text="TLS"
|
||||
FontSize="11" Foreground="Gray" Margin="10,0,0,0"/>
|
||||
<TextBox Grid.Row="1" Grid.Column="0" Text="{Binding ServerHost}"
|
||||
FontFamily="monospace" Margin="0,2,0,0"/>
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding ServerPort}"
|
||||
FontFamily="monospace" Width="90" Margin="10,2,0,0"/>
|
||||
<CheckBox Grid.Row="1" Grid.Column="2" IsChecked="{Binding UseSsl}"
|
||||
Margin="10,2,0,0" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Azioni -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Content="{Binding Loc[wallet.connect]}" Classes="accent"
|
||||
Command="{Binding ConnectAndSyncCommand}" IsEnabled="{Binding !IsSyncing}"/>
|
||||
<Button Content="{Binding Loc[wallet.discover]}"
|
||||
Command="{Binding DiscoverServersCommand}"/>
|
||||
<Button Content="{Binding Loc[wallet.resetcert]}"
|
||||
Command="{Binding ResetCertificatesCommand}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Stato connessione -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Ellipse Width="10" Height="10" Fill="LimeGreen" VerticalAlignment="Center"
|
||||
IsVisible="{Binding IsConnected}"/>
|
||||
<Ellipse Width="10" Height="10" Fill="IndianRed" VerticalAlignment="Center"
|
||||
IsVisible="{Binding !IsConnected}"/>
|
||||
<TextBlock Text="{Binding ConnectionStatus}" Foreground="Gray"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Server conosciuti: clicca per riempire host/porta -->
|
||||
<TextBlock Text="{Binding Loc[server.known]}" FontSize="11" Foreground="Gray"/>
|
||||
<ListBox ItemsSource="{Binding KnownServers}"
|
||||
SelectedItem="{Binding SelectedKnownServer}"
|
||||
MaxHeight="200">
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBox:empty">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<TextBlock Text="{Binding Loc[server.empty]}"
|
||||
Foreground="Gray" FontSize="12"
|
||||
TextWrapping="Wrap" Margin="8"/>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="net:KnownServer">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Host}"
|
||||
FontFamily="monospace" FontSize="13"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="1" FontSize="11" Foreground="Gray"
|
||||
VerticalAlignment="Center">
|
||||
<Run Text="tcp"/><Run Text="{Binding TcpPort}"/>
|
||||
<Run Text=" / ssl"/><Run Text="{Binding SslPort}"/>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<Button Content="{Binding Loc[addr.close]}"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{Binding CloseServerSettingsCommand}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
<!-- ============ OVERLAY IMPOSTAZIONI ============ -->
|
||||
<!-- In-app invece dei sottomenu annidati: niente popup OS lenti su WSLg. -->
|
||||
<Border Grid.Row="0" Grid.RowSpan="3"
|
||||
Background="#99000000"
|
||||
Tapped="OnSettingsOverlayBackdropTapped"
|
||||
IsVisible="{Binding IsSettingsOpen}">
|
||||
<Border Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}"
|
||||
BorderBrush="Gray" BorderThickness="1" CornerRadius="8"
|
||||
Width="460"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<StackPanel Margin="24" Spacing="16">
|
||||
<TextBlock Text="{Binding Loc[settings.title]}"
|
||||
FontSize="18" FontWeight="Bold"/>
|
||||
|
||||
<!-- Lingua -->
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="{Binding Loc[settings.language]}" FontSize="11" Foreground="Gray"/>
|
||||
<WrapPanel>
|
||||
<RadioButton GroupName="lang" Content="Italiano" Margin="0,0,14,4"
|
||||
IsChecked="{Binding IsLangIt, Mode=OneWay}"
|
||||
Command="{Binding SetLanguageCommand}" CommandParameter="it"/>
|
||||
<RadioButton GroupName="lang" Content="English" Margin="0,0,14,4"
|
||||
IsChecked="{Binding IsLangEn, Mode=OneWay}"
|
||||
Command="{Binding SetLanguageCommand}" CommandParameter="en"/>
|
||||
<RadioButton GroupName="lang" Content="Español" Margin="0,0,14,4"
|
||||
IsChecked="{Binding IsLangEs, Mode=OneWay}"
|
||||
Command="{Binding SetLanguageCommand}" CommandParameter="es"/>
|
||||
<RadioButton GroupName="lang" Content="Français" Margin="0,0,14,4"
|
||||
IsChecked="{Binding IsLangFr, Mode=OneWay}"
|
||||
Command="{Binding SetLanguageCommand}" CommandParameter="fr"/>
|
||||
<RadioButton GroupName="lang" Content="Português" Margin="0,0,14,4"
|
||||
IsChecked="{Binding IsLangPt, Mode=OneWay}"
|
||||
Command="{Binding SetLanguageCommand}" CommandParameter="pt"/>
|
||||
<RadioButton GroupName="lang" Content="Deutsch" Margin="0,0,14,4"
|
||||
IsChecked="{Binding IsLangDe, Mode=OneWay}"
|
||||
Command="{Binding SetLanguageCommand}" CommandParameter="de"/>
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Unità -->
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="{Binding Loc[settings.unit]}" FontSize="11" Foreground="Gray"/>
|
||||
<WrapPanel>
|
||||
<RadioButton GroupName="unit" Content="PLM" Margin="0,0,14,4"
|
||||
IsChecked="{Binding IsUnitPlm, Mode=OneWay}"
|
||||
Command="{Binding SetUnitCommand}" CommandParameter="PLM"/>
|
||||
<RadioButton GroupName="unit" Content="mPLM" Margin="0,0,14,4"
|
||||
IsChecked="{Binding IsUnitMilli, Mode=OneWay}"
|
||||
Command="{Binding SetUnitCommand}" CommandParameter="mPLM"/>
|
||||
<RadioButton GroupName="unit" Content="µPLM" Margin="0,0,14,4"
|
||||
IsChecked="{Binding IsUnitMicro, Mode=OneWay}"
|
||||
Command="{Binding SetUnitCommand}" CommandParameter="µPLM"/>
|
||||
<RadioButton GroupName="unit" Content="sat" Margin="0,0,14,4"
|
||||
IsChecked="{Binding IsUnitSat, Mode=OneWay}"
|
||||
Command="{Binding SetUnitCommand}" CommandParameter="sat"/>
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Server di indicizzazione (configurabile anche prima di aprire un wallet) -->
|
||||
<Button Content="{Binding Loc[settings.server]}"
|
||||
HorizontalAlignment="Left"
|
||||
Command="{Binding OpenServerSettingsCommand}"/>
|
||||
|
||||
<Button Content="{Binding Loc[addr.close]}"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{Binding CloseSettingsCommand}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -37,15 +37,14 @@ public partial class MainWindow : Window
|
||||
vm.OpenFromPath(path);
|
||||
}
|
||||
|
||||
private async void OnAddressListTapped(object? sender, TappedEventArgs e)
|
||||
private 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);
|
||||
vm.ShowAddressInfo(row);
|
||||
}
|
||||
|
||||
private async void OnAddressListPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
private 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;
|
||||
@@ -54,8 +53,61 @@ public partial class MainWindow : Window
|
||||
if (item is not { DataContext: AddressRow row }) return;
|
||||
|
||||
lb.SelectedItem = row;
|
||||
vm.ShowAddressInfo(row);
|
||||
}
|
||||
|
||||
var info = new AddressInfo(vm.Loc, row.Indirizzo, row.DerivPath, row.PubKey, row.PrivKey);
|
||||
await new AddressInfoWindow(info).ShowDialog(this);
|
||||
// Chiusura dell'overlay dettaglio indirizzo: click sullo sfondo scuro
|
||||
// (solo sullo sfondo, non sulla scheda) o tasto Esc.
|
||||
private void OnAddressOverlayBackdropTapped(object? sender, TappedEventArgs e)
|
||||
{
|
||||
if (!ReferenceEquals(e.Source, sender)) return;
|
||||
if (DataContext is MainWindowViewModel vm)
|
||||
vm.AddressInfo = null;
|
||||
}
|
||||
|
||||
private void OnServerOverlayBackdropTapped(object? sender, TappedEventArgs e)
|
||||
{
|
||||
if (!ReferenceEquals(e.Source, sender)) return;
|
||||
if (DataContext is MainWindowViewModel vm)
|
||||
vm.IsServerSettingsOpen = false;
|
||||
}
|
||||
|
||||
private async void OnChooseDataFolderClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not MainWindowViewModel vm)
|
||||
return;
|
||||
|
||||
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = "Cartella dati Palladium Wallet",
|
||||
AllowMultiple = false,
|
||||
});
|
||||
|
||||
if (folders.FirstOrDefault()?.TryGetLocalPath() is { } path)
|
||||
vm.ApplyDataLocation(path);
|
||||
}
|
||||
|
||||
private void OnConnectionStatusTapped(object? sender, TappedEventArgs e)
|
||||
{
|
||||
if (DataContext is MainWindowViewModel vm)
|
||||
vm.IsServerSettingsOpen = true;
|
||||
}
|
||||
|
||||
private void OnSettingsOverlayBackdropTapped(object? sender, TappedEventArgs e)
|
||||
{
|
||||
if (!ReferenceEquals(e.Source, sender)) return;
|
||||
if (DataContext is MainWindowViewModel vm)
|
||||
vm.IsSettingsOpen = false;
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape && DataContext is MainWindowViewModel vm)
|
||||
{
|
||||
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; }
|
||||
}
|
||||
base.OnKeyDown(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,27 +3,97 @@ using PalladiumWallet.Core.Chain;
|
||||
namespace PalladiumWallet.Core.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// Percorsi dati per piattaforma (blueprint §8): ~/.palladium-wallet (Linux) o
|
||||
/// %APPDATA%/PalladiumWallet (Windows), con sottocartella per rete. La modalità
|
||||
/// portable (dati accanto all'eseguibile) si attiva se accanto all'eseguibile
|
||||
/// esiste una cartella "palladium-data".
|
||||
/// Percorsi dati per piattaforma (blueprint §8). La radice dati può essere:
|
||||
/// 1. <b>portable</b>: cartella "palladium-data" accanto all'eseguibile;
|
||||
/// 2. <b>personalizzata</b>: scelta dall'utente al primo avvio e memorizzata in
|
||||
/// un piccolo file "puntatore" in una posizione di bootstrap fissa;
|
||||
/// 3. <b>legacy</b>: vecchia posizione (%APPDATA%/PalladiumWallet) se contiene già dati;
|
||||
/// 4. <b>default</b>: ~/.PalladiumWallet (Linux/macOS) o %ProgramFiles%\PalladiumWallet (Windows).
|
||||
/// Sotto la radice c'è una sottocartella per rete (config, wallet, header, certificati).
|
||||
/// </summary>
|
||||
public static class AppPaths
|
||||
{
|
||||
public const string PortableDirName = "palladium-data";
|
||||
|
||||
/// <summary>Nome cartella applicazione, usato nei vari percorsi.</summary>
|
||||
public const string AppDirName = "PalladiumWallet";
|
||||
|
||||
/// <summary>Override esplicito della radice dati (es. CLI --data-dir). Ha priorità su tutto.</summary>
|
||||
public static string? OverrideDataRoot { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Radice dati predefinita, secondo la convenzione di ogni piattaforma:
|
||||
/// Windows → %APPDATA%\PalladiumWallet (PascalCase, come Electrum/Bitcoin);
|
||||
/// Linux/macOS → ~/.palladium-wallet (dotfolder minuscolo, come ~/.bitcoin).
|
||||
/// Per-utente e sempre scrivibile, senza privilegi di amministratore.
|
||||
/// </summary>
|
||||
public static string DefaultDataRoot()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
return Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
AppDirName);
|
||||
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
return Path.Combine(home, ".palladium-wallet");
|
||||
}
|
||||
|
||||
/// <summary>File puntatore alla radice dati scelta dall'utente. Vive in una
|
||||
/// posizione di bootstrap sempre scrivibile e indipendente dalla radice dati.</summary>
|
||||
private static string LocationPointerPath() =>
|
||||
Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
AppDirName, "data-location");
|
||||
|
||||
private static string PortableRoot() =>
|
||||
Path.Combine(AppContext.BaseDirectory, PortableDirName);
|
||||
|
||||
private static bool HasData(string root) =>
|
||||
Directory.Exists(root) && Directory.EnumerateFileSystemEntries(root).Any();
|
||||
|
||||
/// <summary>Radice dati effettiva, secondo l'ordine di precedenza documentato in classe.</summary>
|
||||
public static string DataRoot()
|
||||
{
|
||||
var portable = Path.Combine(AppContext.BaseDirectory, PortableDirName);
|
||||
if (!string.IsNullOrEmpty(OverrideDataRoot))
|
||||
return OverrideDataRoot;
|
||||
|
||||
var portable = PortableRoot();
|
||||
if (Directory.Exists(portable))
|
||||
return portable;
|
||||
|
||||
// Windows → %APPDATA%\PalladiumWallet
|
||||
// Linux → $XDG_CONFIG_HOME/PalladiumWallet (default ~/.config/PalladiumWallet)
|
||||
// macOS → ~/Library/Application Support/PalladiumWallet
|
||||
return Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"PalladiumWallet");
|
||||
if (ReadPointer() is { } custom)
|
||||
return custom;
|
||||
|
||||
return DefaultDataRoot();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// true se la posizione dei dati è già determinata e non serve chiederla
|
||||
/// all'utente: modalità portable, override, puntatore già scritto, oppure
|
||||
/// dati già presenti nella posizione predefinita.
|
||||
/// </summary>
|
||||
public static bool IsDataLocationConfigured() =>
|
||||
!string.IsNullOrEmpty(OverrideDataRoot)
|
||||
|| Directory.Exists(PortableRoot())
|
||||
|| ReadPointer() is not null
|
||||
|| HasData(DefaultDataRoot());
|
||||
|
||||
/// <summary>Memorizza la radice dati scelta dall'utente e la crea su disco.</summary>
|
||||
public static void ConfigureDataLocation(string root)
|
||||
{
|
||||
root = Path.GetFullPath(root.Trim());
|
||||
Directory.CreateDirectory(root);
|
||||
var pointer = LocationPointerPath();
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(pointer)!);
|
||||
File.WriteAllText(pointer, root);
|
||||
}
|
||||
|
||||
private static string? ReadPointer()
|
||||
{
|
||||
var pointer = LocationPointerPath();
|
||||
if (!File.Exists(pointer))
|
||||
return null;
|
||||
var path = File.ReadAllText(pointer).Trim();
|
||||
return string.IsNullOrEmpty(path) ? null : path;
|
||||
}
|
||||
|
||||
/// <summary>Cartella dati della rete (config, wallet, header, certificati).</summary>
|
||||
@@ -44,6 +114,15 @@ public static class AppPaths
|
||||
public static string DefaultWalletPath(NetKind net) =>
|
||||
Path.Combine(WalletsDir(net), "default.wallet.json");
|
||||
|
||||
/// <summary>Tutti i file wallet della rete, ordinati per nome (multi-wallet §8).</summary>
|
||||
public static IReadOnlyList<string> WalletFiles(NetKind net)
|
||||
{
|
||||
var dir = WalletsDir(net);
|
||||
return Directory.EnumerateFiles(dir, "*.wallet.json")
|
||||
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static string CertificatePinsPath(NetKind net) =>
|
||||
Path.Combine(ForNetwork(net), "server-certs.json");
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ public class TransactionFactoryTests
|
||||
{
|
||||
File.WriteAllText(path, "{ rotto ");
|
||||
var loaded = PalladiumWallet.Core.Storage.AppConfig.Load(path);
|
||||
Assert.Equal("it", loaded.Language);
|
||||
Assert.Equal("en", loaded.Language);
|
||||
Assert.Equal("PLM", loaded.Unit);
|
||||
}
|
||||
finally
|
||||
|
||||
Reference in New Issue
Block a user