diff --git a/src/PSLauncher.App/ViewModels/SettingsViewModel.cs b/src/PSLauncher.App/ViewModels/SettingsViewModel.cs index 080d82d..dbaa549 100644 --- a/src/PSLauncher.App/ViewModels/SettingsViewModel.cs +++ b/src/PSLauncher.App/ViewModels/SettingsViewModel.cs @@ -8,8 +8,10 @@ using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.Logging; using PSLauncher.Core.Configuration; using PSLauncher.Core.Downloads; +using PSLauncher.Core.Installations; using PSLauncher.Core.Licensing; using PSLauncher.Core.Localization; +using PSLauncher.Core.Migrations; using PSLauncher.Models; namespace PSLauncher.App.ViewModels; @@ -27,6 +29,8 @@ public sealed partial class SettingsViewModel : ObservableObject private readonly LocalConfig _config; private readonly ILicenseService _licenseService; private readonly IDownloadStateStore _downloadStore; + private readonly IDatabaseMigrationService _migrationService; + private readonly IInstallationRegistry _registry; private readonly HttpClient _http; private readonly ILogger _logger; @@ -39,6 +43,18 @@ public sealed partial class SettingsViewModel : ObservableObject [ObservableProperty] private string _installRoot; [ObservableProperty] private string _language; + // ----- DB Migration settings ----- + [ObservableProperty] private string _dbHost; + [ObservableProperty] private uint _dbPort; + [ObservableProperty] private string _dbUser; + [ObservableProperty] private string _dbPassword; + [ObservableProperty] private string _dbName; + [ObservableProperty] private bool _dbAutoApplyMigrations; + [ObservableProperty] private string? _dbConnectionStatus; + [ObservableProperty] private bool _isTestingDb; + [ObservableProperty] private string? _migrationsReplayStatus; + [ObservableProperty] private bool _isReplayingMigrations; + public IReadOnlyList AvailableLanguages { get; } = PSLauncher.Core.Localization.Strings.Available .Select(t => new LanguageOption(t.Code, t.Name)) @@ -105,6 +121,8 @@ public sealed partial class SettingsViewModel : ObservableObject LocalConfig config, ILicenseService licenseService, IDownloadStateStore downloadStore, + IDatabaseMigrationService migrationService, + IInstallationRegistry registry, HttpClient http, ILogger logger) { @@ -112,6 +130,8 @@ public sealed partial class SettingsViewModel : ObservableObject _config = config; _licenseService = licenseService; _downloadStore = downloadStore; + _migrationService = migrationService; + _registry = registry; _http = http; _logger = logger; @@ -119,6 +139,13 @@ public sealed partial class SettingsViewModel : ObservableObject _installRoot = config.InstallRoot; _language = string.IsNullOrWhiteSpace(config.Language) ? "auto" : config.Language; + _dbHost = config.Database.Host; + _dbPort = config.Database.Port; + _dbUser = config.Database.User; + _dbPassword = DatabasePasswordProtector.Unprotect(config.Database.EncryptedPassword) ?? string.Empty; + _dbName = config.Database.Database; + _dbAutoApplyMigrations = config.Database.AutoApplyMigrations; + LauncherVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0"; MachineId = licenseService.GetMachineId(); LogsDirectory = App.LogsDirectory; @@ -225,6 +252,17 @@ public sealed partial class SettingsViewModel : ObservableObject _config.ServerBaseUrl = ServerBaseUrl.TrimEnd('/'); _config.InstallRoot = InstallRoot; _config.Language = Language ?? "auto"; + + // DB settings + _config.Database.Host = DbHost.Trim(); + _config.Database.Port = DbPort > 0 ? DbPort : 3306; + _config.Database.User = DbUser.Trim(); + _config.Database.EncryptedPassword = string.IsNullOrEmpty(DbPassword) + ? null + : DatabasePasswordProtector.Protect(DbPassword); + _config.Database.Database = DbName.Trim(); + _config.Database.AutoApplyMigrations = DbAutoApplyMigrations; + _configStore.Save(_config); _logger.LogInformation("Settings saved (language={Lang}, changed={Changed})", _config.Language, languageChanged); @@ -289,4 +327,99 @@ public sealed partial class SettingsViewModel : ObservableObject } private static string FormatSize(long bytes) => Strings.FormatSize(bytes); + + // ===================== DB MIGRATIONS ===================== + + /// + /// Test la connexion à la DB MySQL avec les paramètres en cours de saisie. + /// Persiste temporairement les valeurs sur _config.Database avant le test + /// pour que le service les voie via son configProvider, puis remet (et le + /// Save() final écrasera quand l'utilisateur cliquera Enregistrer). + /// + [RelayCommand] + private async Task TestDbAsync() + { + DbConnectionStatus = Strings.SettingsTest + "…"; + IsTestingDb = true; + // On applique temporairement les valeurs saisies + var oldHost = _config.Database.Host; + var oldPort = _config.Database.Port; + var oldUser = _config.Database.User; + var oldPwd = _config.Database.EncryptedPassword; + var oldDb = _config.Database.Database; + try + { + _config.Database.Host = DbHost.Trim(); + _config.Database.Port = DbPort > 0 ? DbPort : 3306; + _config.Database.User = DbUser.Trim(); + _config.Database.EncryptedPassword = string.IsNullOrEmpty(DbPassword) + ? null + : DatabasePasswordProtector.Protect(DbPassword); + _config.Database.Database = DbName.Trim(); + + var err = await _migrationService.TestConnectionAsync(CancellationToken.None); + DbConnectionStatus = err is null ? "✓ OK" : $"✗ {err}"; + } + catch (Exception ex) { DbConnectionStatus = $"✗ {ex.Message}"; } + finally + { + // Restaure : seul Save() doit écrire pour de bon + _config.Database.Host = oldHost; + _config.Database.Port = oldPort; + _config.Database.User = oldUser; + _config.Database.EncryptedPassword = oldPwd; + _config.Database.Database = oldDb; + IsTestingDb = false; + } + } + + /// + /// Re-joue les migrations bundled dans le _migrations/ de la version actuellement + /// installée la plus récente. Utile si l'auto-apply post-install avait échoué (ex. + /// XAMPP éteint à ce moment-là), ou pour rejouer manuellement après ajout d'une + /// nouvelle migration côté dev. + /// + [RelayCommand] + private async Task ReplayMigrationsAsync() + { + var installed = _registry.Scan().OrderByDescending(v => v.Version).FirstOrDefault(); + if (installed is null) + { + MigrationsReplayStatus = "Aucune version installée — installe d'abord une version avec _migrations/."; + return; + } + IsReplayingMigrations = true; + MigrationsReplayStatus = $"Réapplication des migrations pour v{installed.Version}…"; + try + { + var progress = new Progress(mp => + { + if (mp.Total > 0) + MigrationsReplayStatus = $"{mp.Done + 1}/{mp.Total} : {mp.CurrentFilename}"; + }); + // Il faut persister les éventuelles modifs DB avant le test (le service lit la config courante) + Save(); + var result = await _migrationService.ApplyMigrationsAsync( + installed.FolderPath, progress, CancellationToken.None); + if (result.AllSucceeded) + { + MigrationsReplayStatus = result.AppliedCount > 0 + ? $"✓ {result.AppliedCount} migration(s) appliquée(s), {result.SkippedCount} déjà en place." + : $"✓ Tout est à jour ({result.SkippedCount} déjà appliquée(s))"; + } + else + { + MigrationsReplayStatus = $"✗ Échec : {result.FailureMessage}"; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Replay migrations failed"); + MigrationsReplayStatus = $"✗ Erreur : {ex.Message}"; + } + finally + { + IsReplayingMigrations = false; + } + } } diff --git a/src/PSLauncher.App/Views/SettingsDialog.xaml b/src/PSLauncher.App/Views/SettingsDialog.xaml index 3edb39d..0cd1a2c 100644 --- a/src/PSLauncher.App/Views/SettingsDialog.xaml +++ b/src/PSLauncher.App/Views/SettingsDialog.xaml @@ -248,6 +248,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +