diff --git a/src/App/Localization/Loc.cs b/src/App/Localization/Loc.cs
index c1f450b..71e85cf 100644
--- a/src/App/Localization/Loc.cs
+++ b/src/App/Localization/Loc.cs
@@ -27,7 +27,7 @@ public sealed class Loc
///
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,17 @@ 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.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"],
diff --git a/src/App/ViewModels/MainWindowViewModel.cs b/src/App/ViewModels/MainWindowViewModel.cs
index 58dceb0..8c16b02 100644
--- a/src/App/ViewModels/MainWindowViewModel.cs
+++ b/src/App/ViewModels/MainWindowViewModel.cs
@@ -165,6 +165,7 @@ 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 StepOpen = "open";
public const string StepShowSeed = "show-seed";
@@ -174,6 +175,7 @@ public partial class MainWindowViewModel : ViewModelBase
public const string StepPassword = "password";
[ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(IsStepDataLocation))]
[NotifyPropertyChangedFor(nameof(IsStepStart))]
[NotifyPropertyChangedFor(nameof(IsStepOpen))]
[NotifyPropertyChangedFor(nameof(IsStepShowSeed))]
@@ -183,6 +185,7 @@ public partial class MainWindowViewModel : ViewModelBase
[NotifyPropertyChangedFor(nameof(IsStepPassword))]
private string setupStep = StepStart;
+ public bool IsStepDataLocation => SetupStep == StepDataLocation;
public bool IsStepStart => SetupStep == StepStart;
public bool IsStepOpen => SetupStep == StepOpen;
public bool IsStepShowSeed => SetupStep == StepShowSeed;
@@ -191,6 +194,9 @@ public partial class MainWindowViewModel : ViewModelBase
public bool IsStepPassphrase => SetupStep == StepPassphrase;
public bool IsStepPassword => SetupStep == StepPassword;
+ /// Percorso dati predefinito, mostrato al primo avvio.
+ public string DefaultDataPath => AppPaths.DefaultDataRoot();
+
/// True quando il flusso è "ripristina" (parole inserite dall'utente).
private bool _isRestoreFlow;
@@ -375,7 +381,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.
@@ -413,6 +424,34 @@ public partial class MainWindowViewModel : ViewModelBase
private ServerRegistry Registry => new(Profile, AppPaths.ServersPath(Net));
+ /// Primo avvio: usa il percorso dati predefinito di piattaforma.
+ [RelayCommand]
+ private void UseDefaultDataLocation() => ApplyDataLocation(AppPaths.DefaultDataRoot());
+
+ ///
+ /// 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.
+ ///
+ 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;
diff --git a/src/App/Views/MainWindow.axaml b/src/App/Views/MainWindow.axaml
index a9d82ee..91da29a 100644
--- a/src/App/Views/MainWindow.axaml
+++ b/src/App/Views/MainWindow.axaml
@@ -41,6 +41,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/App/Views/MainWindow.axaml.cs b/src/App/Views/MainWindow.axaml.cs
index 636493e..7339acb 100644
--- a/src/App/Views/MainWindow.axaml.cs
+++ b/src/App/Views/MainWindow.axaml.cs
@@ -72,6 +72,21 @@ public partial class MainWindow : Window
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)
diff --git a/src/Core/Storage/AppPaths.cs b/src/Core/Storage/AppPaths.cs
index 39f7d93..2d80819 100644
--- a/src/Core/Storage/AppPaths.cs
+++ b/src/Core/Storage/AppPaths.cs
@@ -3,27 +3,104 @@ using PalladiumWallet.Core.Chain;
namespace PalladiumWallet.Core.Storage;
///
-/// 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. portable: cartella "palladium-data" accanto all'eseguibile;
+/// 2. personalizzata: scelta dall'utente al primo avvio e memorizzata in
+/// un piccolo file "puntatore" in una posizione di bootstrap fissa;
+/// 3. legacy: vecchia posizione (%APPDATA%/PalladiumWallet) se contiene già dati;
+/// 4. default: ~/.PalladiumWallet (Linux/macOS) o %ProgramFiles%\PalladiumWallet (Windows).
+/// Sotto la radice c'è una sottocartella per rete (config, wallet, header, certificati).
///
public static class AppPaths
{
public const string PortableDirName = "palladium-data";
+ /// Nome cartella applicazione, usato nei vari percorsi.
+ public const string AppDirName = "PalladiumWallet";
+
+ /// Override esplicito della radice dati (es. CLI --data-dir). Ha priorità su tutto.
+ public static string? OverrideDataRoot { get; set; }
+
+ ///
+ /// Radice dati predefinita: cartella nascosta nella home dell'utente
+ /// (%USERPROFILE%\.PalladiumWallet su Windows, ~/.PalladiumWallet su Linux/macOS).
+ /// Per-utente e sempre scrivibile, senza privilegi di amministratore.
+ ///
+ public static string DefaultDataRoot()
+ {
+ var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ return Path.Combine(home, "." + AppDirName);
+ }
+
+ /// Vecchia radice (%APPDATA%/PalladiumWallet, ~/.config su Linux): mantenuta
+ /// per non orfanizzare i dati di installazioni precedenti.
+ private static string LegacyDataRoot() =>
+ Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ AppDirName);
+
+ /// File puntatore alla radice dati scelta dall'utente. Vive in una
+ /// posizione di bootstrap sempre scrivibile e indipendente dalla radice dati.
+ 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();
+
+ /// Radice dati effettiva, secondo l'ordine di precedenza documentato in classe.
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;
+
+ var legacy = LegacyDataRoot();
+ if (HasData(legacy))
+ return legacy;
+
+ return DefaultDataRoot();
+ }
+
+ ///
+ /// 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 legacy o nella default.
+ ///
+ public static bool IsDataLocationConfigured() =>
+ !string.IsNullOrEmpty(OverrideDataRoot)
+ || Directory.Exists(PortableRoot())
+ || ReadPointer() is not null
+ || HasData(LegacyDataRoot())
+ || HasData(DefaultDataRoot());
+
+ /// Memorizza la radice dati scelta dall'utente e la crea su disco.
+ 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;
}
/// Cartella dati della rete (config, wallet, header, certificati).
diff --git a/tests/PalladiumWallet.Tests/Wallet/TransactionFactoryTests.cs b/tests/PalladiumWallet.Tests/Wallet/TransactionFactoryTests.cs
index 8e68b13..41f86f3 100644
--- a/tests/PalladiumWallet.Tests/Wallet/TransactionFactoryTests.cs
+++ b/tests/PalladiumWallet.Tests/Wallet/TransactionFactoryTests.cs
@@ -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