v0.5: settings dialog, persistent Serilog logs, Windows toasts
Settings (⚙ button in top bar)
------------------------------
SettingsDialog opens via OpenSettings command in MainViewModel. Sections:
- Serveur: server URL field + "Tester" button that GETs /api/health
and reports status inline.
- Installation: installRoot path with Browse button
(Microsoft.Win32.OpenFolderDialog, .NET 8 native).
- License: shows status / owner / exp / machine ID (read-only,
copy-to-clipboard button), "Désactiver" wipes the cached license.
- Cache: shows download cache path + current size, opens or empties it.
- Logs & Application: launcher version + logs path with "Open" button.
Apply persists to LocalConfig via IConfigStore. After save, MainViewModel
RebuildList()s so changes to ServerBaseUrl or InstallRoot take effect
without restart.
Persistent Serilog logs
-----------------------
Logs now live in %LocalAppData%/PSLauncher/logs/app-YYYYMMDD.log, rolling
daily, 10 days kept. Output template includes source context and stack
traces. Generic Host wired up via .UseSerilog() so all
ILogger<T>-injected types share the sink. Unhandled AppDomain and
Dispatcher exceptions are routed to Serilog before propagation.
Windows toasts
--------------
IToastService + ToastService backed by Microsoft.Toolkit.Uwp.Notifications
(ToastContentBuilder.Show()). Required bumping the App TFM from
net8.0-windows to net8.0-windows10.0.17763.0 (with explicit
WindowsSdkPackageVersion=10.0.17763.41 to satisfy CommunityToolkit.Mvvm
8.3.2's MVVMTKCFG0003 check). Triggered on:
- successful install completion: "Proserve v{X} est prête à être lancée"
- install/download error: short error excerpt
Misc
----
- InverseBoolConverter for "disable button while busy" patterns.
- Added Markdig.Wpf import to ReleaseNotesViewerDialog (was implicit
before, now required explicitly).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,8 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSLauncher.App.Views;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using PSLauncher.App.Services;
|
||||
using PSLauncher.Core.Configuration;
|
||||
using PSLauncher.Core.Downloads;
|
||||
using PSLauncher.Core.Installations;
|
||||
@@ -28,6 +30,8 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
private readonly IDownloadManager _downloadManager;
|
||||
private readonly IZipInstaller _zipInstaller;
|
||||
private readonly ILicenseService _licenseService;
|
||||
private readonly IToastService _toastService;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<MainViewModel> _logger;
|
||||
|
||||
private LicenseValidationResponse? _license;
|
||||
@@ -101,6 +105,8 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
IDownloadManager downloadManager,
|
||||
IZipInstaller zipInstaller,
|
||||
ILicenseService licenseService,
|
||||
IToastService toastService,
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<MainViewModel> logger)
|
||||
{
|
||||
_registry = registry;
|
||||
@@ -112,6 +118,8 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
_downloadManager = downloadManager;
|
||||
_zipInstaller = zipInstaller;
|
||||
_licenseService = licenseService;
|
||||
_toastService = toastService;
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
|
||||
// Charge la license depuis le cache (pas d'appel réseau au démarrage,
|
||||
@@ -226,6 +234,20 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
}
|
||||
private bool CanCheckUpdates() => !IsBusy;
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenSettings()
|
||||
{
|
||||
var vm = _serviceProvider.GetRequiredService<SettingsViewModel>();
|
||||
var dialog = new Views.SettingsDialog(vm) { Owner = Application.Current.MainWindow };
|
||||
if (dialog.ShowDialog() == true)
|
||||
{
|
||||
// Au retour, l'URL serveur ou l'installRoot ont pu changer → refresh
|
||||
_license = _licenseService.GetCached();
|
||||
OnPropertyChanged(nameof(LicenseSummary));
|
||||
RebuildList();
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ActivateLicenseAsync()
|
||||
{
|
||||
@@ -339,6 +361,9 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
ProgressDetail = null;
|
||||
ProgressPercent = 0;
|
||||
RebuildList();
|
||||
_toastService.ShowSuccess(
|
||||
"Installation terminée",
|
||||
$"Proserve v{row.Version} est prête à être lancée.");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -357,6 +382,7 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
$"Le téléchargement ou l'installation a échoué :\n\n{ex.Message}",
|
||||
"Erreur", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
StatusMessage = $"Erreur : {ex.Message}";
|
||||
_toastService.ShowError($"Échec v{row.Version}", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
203
src/PSLauncher.App/ViewModels/SettingsViewModel.cs
Normal file
203
src/PSLauncher.App/ViewModels/SettingsViewModel.cs
Normal file
@@ -0,0 +1,203 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PSLauncher.Core.Configuration;
|
||||
using PSLauncher.Core.Downloads;
|
||||
using PSLauncher.Core.Licensing;
|
||||
using PSLauncher.Models;
|
||||
|
||||
namespace PSLauncher.App.ViewModels;
|
||||
|
||||
public sealed partial class SettingsViewModel : ObservableObject
|
||||
{
|
||||
private readonly IConfigStore _configStore;
|
||||
private readonly LocalConfig _config;
|
||||
private readonly ILicenseService _licenseService;
|
||||
private readonly IDownloadStateStore _downloadStore;
|
||||
private readonly HttpClient _http;
|
||||
private readonly ILogger<SettingsViewModel> _logger;
|
||||
|
||||
public string LauncherVersion { get; }
|
||||
public string MachineId { get; }
|
||||
public string LogsDirectory { get; }
|
||||
public string CacheDirectory { get; }
|
||||
|
||||
[ObservableProperty] private string _serverBaseUrl;
|
||||
[ObservableProperty] private string _installRoot;
|
||||
[ObservableProperty] private string? _connectionStatus;
|
||||
[ObservableProperty] private bool _isTestingConnection;
|
||||
[ObservableProperty] private string _cacheSizeDisplay = "—";
|
||||
|
||||
public string LicenseInfo
|
||||
{
|
||||
get
|
||||
{
|
||||
var l = _licenseService.GetCached();
|
||||
if (l is null) return "Aucune license activée";
|
||||
return $"{l.Status} • {l.OwnerName} • exp. {l.DownloadEntitlementUntil:dd/MM/yyyy}";
|
||||
}
|
||||
}
|
||||
|
||||
public SettingsViewModel(
|
||||
IConfigStore configStore,
|
||||
LocalConfig config,
|
||||
ILicenseService licenseService,
|
||||
IDownloadStateStore downloadStore,
|
||||
HttpClient http,
|
||||
ILogger<SettingsViewModel> logger)
|
||||
{
|
||||
_configStore = configStore;
|
||||
_config = config;
|
||||
_licenseService = licenseService;
|
||||
_downloadStore = downloadStore;
|
||||
_http = http;
|
||||
_logger = logger;
|
||||
|
||||
_serverBaseUrl = config.ServerBaseUrl;
|
||||
_installRoot = config.InstallRoot;
|
||||
|
||||
LauncherVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0";
|
||||
MachineId = licenseService.GetMachineId();
|
||||
LogsDirectory = App.LogsDirectory;
|
||||
CacheDirectory = downloadStore.GetDownloadsDirectory();
|
||||
RefreshCacheSize();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task TestConnectionAsync()
|
||||
{
|
||||
ConnectionStatus = "Test en cours…";
|
||||
IsTestingConnection = true;
|
||||
try
|
||||
{
|
||||
var url = ServerBaseUrl.TrimEnd('/') + "/health";
|
||||
using var resp = await _http.GetAsync(url);
|
||||
if (resp.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await resp.Content.ReadAsStringAsync();
|
||||
ConnectionStatus = $"✓ OK ({(int)resp.StatusCode}) — {body.Substring(0, Math.Min(80, body.Length))}";
|
||||
}
|
||||
else
|
||||
{
|
||||
ConnectionStatus = $"✗ HTTP {(int)resp.StatusCode}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConnectionStatus = $"✗ Erreur : {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsTestingConnection = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void BrowseInstallRoot()
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFolderDialog
|
||||
{
|
||||
Title = "Choisir le dossier d'installation",
|
||||
InitialDirectory = Directory.Exists(InstallRoot) ? InstallRoot : Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
};
|
||||
if (dlg.ShowDialog() == true)
|
||||
{
|
||||
InstallRoot = dlg.FolderName;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenLogsFolder()
|
||||
{
|
||||
if (Directory.Exists(LogsDirectory))
|
||||
Process.Start(new ProcessStartInfo { FileName = LogsDirectory, UseShellExecute = true, Verb = "open" });
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenCacheFolder()
|
||||
{
|
||||
if (Directory.Exists(CacheDirectory))
|
||||
Process.Start(new ProcessStartInfo { FileName = CacheDirectory, UseShellExecute = true, Verb = "open" });
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ClearCache()
|
||||
{
|
||||
var confirm = MessageBox.Show(
|
||||
"Vider le cache de téléchargements ? Les téléchargements en cours/pause seront perdus.",
|
||||
"Confirmer",
|
||||
MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No);
|
||||
if (confirm != MessageBoxResult.Yes) return;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var f in Directory.EnumerateFiles(CacheDirectory))
|
||||
{
|
||||
try { File.Delete(f); } catch (Exception ex) { _logger.LogWarning(ex, "Cannot delete {F}", f); }
|
||||
}
|
||||
RefreshCacheSize();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "ClearCache failed");
|
||||
MessageBox.Show("Échec : " + ex.Message, "Erreur", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CopyMachineId()
|
||||
{
|
||||
try { Clipboard.SetText(MachineId); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "Clipboard failed"); }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Save()
|
||||
{
|
||||
_config.ServerBaseUrl = ServerBaseUrl.TrimEnd('/');
|
||||
_config.InstallRoot = InstallRoot;
|
||||
_configStore.Save(_config);
|
||||
_logger.LogInformation("Settings saved");
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void DeactivateLicense()
|
||||
{
|
||||
var confirm = MessageBox.Show(
|
||||
"Désactiver la license sur cette machine ?\nTu pourras la réactiver avec ta clé.",
|
||||
"Confirmer",
|
||||
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
|
||||
if (confirm != MessageBoxResult.Yes) return;
|
||||
_licenseService.Clear();
|
||||
OnPropertyChanged(nameof(LicenseInfo));
|
||||
}
|
||||
|
||||
private void RefreshCacheSize()
|
||||
{
|
||||
try
|
||||
{
|
||||
long total = 0;
|
||||
if (Directory.Exists(CacheDirectory))
|
||||
{
|
||||
foreach (var f in Directory.EnumerateFiles(CacheDirectory))
|
||||
total += new FileInfo(f).Length;
|
||||
}
|
||||
CacheSizeDisplay = FormatSize(total);
|
||||
}
|
||||
catch { CacheSizeDisplay = "—"; }
|
||||
}
|
||||
|
||||
private static string FormatSize(long bytes)
|
||||
{
|
||||
if (bytes <= 0) return "0 o";
|
||||
string[] units = { "o", "Ko", "Mo", "Go", "To" };
|
||||
double v = bytes; int u = 0;
|
||||
while (v >= 1024 && u < units.Length - 1) { v /= 1024; u++; }
|
||||
return $"{v:0.#} {units[u]}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user