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:
@@ -1,9 +1,11 @@
|
|||||||
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using PSLauncher.App.Services;
|
||||||
using PSLauncher.App.ViewModels;
|
using PSLauncher.App.ViewModels;
|
||||||
using PSLauncher.App.Views;
|
using PSLauncher.App.Views;
|
||||||
using PSLauncher.Core.Configuration;
|
using PSLauncher.Core.Configuration;
|
||||||
@@ -15,6 +17,7 @@ using PSLauncher.Core.Manifests;
|
|||||||
using PSLauncher.Core.Process;
|
using PSLauncher.Core.Process;
|
||||||
using PSLauncher.Core.Updates;
|
using PSLauncher.Core.Updates;
|
||||||
using PSLauncher.Models;
|
using PSLauncher.Models;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
namespace PSLauncher.App;
|
namespace PSLauncher.App;
|
||||||
|
|
||||||
@@ -22,18 +25,46 @@ public partial class App : Application
|
|||||||
{
|
{
|
||||||
private IHost? _host;
|
private IHost? _host;
|
||||||
|
|
||||||
|
public static string LogsDirectory { get; private set; } = string.Empty;
|
||||||
|
|
||||||
protected override void OnStartup(StartupEventArgs e)
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
{
|
{
|
||||||
base.OnStartup(e);
|
base.OnStartup(e);
|
||||||
|
|
||||||
|
// Logs avant tout — pour ne rien perdre des erreurs au boot
|
||||||
|
LogsDirectory = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"PSLauncher", "logs");
|
||||||
|
Directory.CreateDirectory(LogsDirectory);
|
||||||
|
|
||||||
|
Log.Logger = new LoggerConfiguration()
|
||||||
|
.MinimumLevel.Debug()
|
||||||
|
.Enrich.FromLogContext()
|
||||||
|
.WriteTo.Debug()
|
||||||
|
.WriteTo.File(
|
||||||
|
path: Path.Combine(LogsDirectory, "app-.log"),
|
||||||
|
rollingInterval: RollingInterval.Day,
|
||||||
|
retainedFileCountLimit: 10,
|
||||||
|
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}")
|
||||||
|
.CreateLogger();
|
||||||
|
|
||||||
|
Log.Information("PSLauncher starting (logs in {Path})", LogsDirectory);
|
||||||
|
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += (_, args) =>
|
||||||
|
Log.Fatal((Exception)args.ExceptionObject, "Unhandled exception (AppDomain)");
|
||||||
|
DispatcherUnhandledException += (_, args) =>
|
||||||
|
{
|
||||||
|
Log.Error(args.Exception, "Unhandled UI exception");
|
||||||
|
args.Handled = false;
|
||||||
|
};
|
||||||
|
|
||||||
_host = Host.CreateDefaultBuilder()
|
_host = Host.CreateDefaultBuilder()
|
||||||
.ConfigureLogging(b => { b.ClearProviders(); b.AddDebug(); })
|
.UseSerilog()
|
||||||
.ConfigureServices((_, services) =>
|
.ConfigureServices((_, services) =>
|
||||||
{
|
{
|
||||||
services.AddSingleton<IConfigStore, ConfigStore>();
|
services.AddSingleton<IConfigStore, ConfigStore>();
|
||||||
services.AddSingleton<LocalConfig>(sp => sp.GetRequiredService<IConfigStore>().Load());
|
services.AddSingleton<LocalConfig>(sp => sp.GetRequiredService<IConfigStore>().Load());
|
||||||
|
|
||||||
// HttpClient partagé (timeout long pour les gros téléchargements)
|
|
||||||
services.AddSingleton(sp =>
|
services.AddSingleton(sp =>
|
||||||
{
|
{
|
||||||
var http = new HttpClient(new SocketsHttpHandler
|
var http = new HttpClient(new SocketsHttpHandler
|
||||||
@@ -44,7 +75,7 @@ public partial class App : Application
|
|||||||
{
|
{
|
||||||
Timeout = Timeout.InfiniteTimeSpan
|
Timeout = Timeout.InfiniteTimeSpan
|
||||||
};
|
};
|
||||||
http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("PSLauncher", "0.2"));
|
http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("PSLauncher", "0.5"));
|
||||||
return http;
|
return http;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,6 +106,9 @@ public partial class App : Application
|
|||||||
sp.GetRequiredService<LocalConfig>(),
|
sp.GetRequiredService<LocalConfig>(),
|
||||||
sp.GetRequiredService<ILogger<LicenseService>>()));
|
sp.GetRequiredService<ILogger<LicenseService>>()));
|
||||||
|
|
||||||
|
services.AddSingleton<IToastService, ToastService>();
|
||||||
|
|
||||||
|
services.AddTransient<SettingsViewModel>();
|
||||||
services.AddSingleton<MainViewModel>();
|
services.AddSingleton<MainViewModel>();
|
||||||
services.AddSingleton<MainWindow>();
|
services.AddSingleton<MainWindow>();
|
||||||
})
|
})
|
||||||
@@ -88,6 +122,8 @@ public partial class App : Application
|
|||||||
|
|
||||||
protected override void OnExit(ExitEventArgs e)
|
protected override void OnExit(ExitEventArgs e)
|
||||||
{
|
{
|
||||||
|
Log.Information("PSLauncher shutting down");
|
||||||
|
Log.CloseAndFlush();
|
||||||
_host?.Dispose();
|
_host?.Dispose();
|
||||||
base.OnExit(e);
|
base.OnExit(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
<TargetFramework>net8.0-windows10.0.17763.0</TargetFramework>
|
||||||
|
<SupportedOSPlatformVersion>10.0.17763.0</SupportedOSPlatformVersion>
|
||||||
|
<WindowsSdkPackageVersion>10.0.17763.41</WindowsSdkPackageVersion>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
@@ -27,6 +29,11 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.1" />
|
||||||
<PackageReference Include="Markdig" Version="0.37.0" />
|
<PackageReference Include="Markdig" Version="0.37.0" />
|
||||||
<PackageReference Include="Markdig.Wpf" Version="0.5.0.1" />
|
<PackageReference Include="Markdig.Wpf" Version="0.5.0.1" />
|
||||||
|
<PackageReference Include="Serilog" Version="4.0.2" />
|
||||||
|
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
13
src/PSLauncher.App/Resources/InverseBoolConverter.cs
Normal file
13
src/PSLauncher.App/Resources/InverseBoolConverter.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Windows.Data;
|
||||||
|
|
||||||
|
namespace PSLauncher.App.Resources;
|
||||||
|
|
||||||
|
public sealed class InverseBoolConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
=> value is bool b ? !b : true;
|
||||||
|
|
||||||
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
=> value is bool b ? !b : false;
|
||||||
|
}
|
||||||
@@ -3,6 +3,9 @@
|
|||||||
|
|
||||||
<BooleanToVisibilityConverter x:Key="BoolToVisibility" />
|
<BooleanToVisibilityConverter x:Key="BoolToVisibility" />
|
||||||
|
|
||||||
|
<!-- Inverse bool : utilisé pour griser un bouton pendant qu'une opération est en cours -->
|
||||||
|
<local:InverseBoolConverter x:Key="InverseBoolToBool" xmlns:local="clr-namespace:PSLauncher.App.Resources" />
|
||||||
|
|
||||||
<!-- Epic Games inspired dark palette -->
|
<!-- Epic Games inspired dark palette -->
|
||||||
<SolidColorBrush x:Key="Brush.Bg.Window" Color="#1B1B1F" />
|
<SolidColorBrush x:Key="Brush.Bg.Window" Color="#1B1B1F" />
|
||||||
<SolidColorBrush x:Key="Brush.Bg.Sidebar" Color="#202024" />
|
<SolidColorBrush x:Key="Brush.Bg.Sidebar" Color="#202024" />
|
||||||
|
|||||||
8
src/PSLauncher.App/Services/IToastService.cs
Normal file
8
src/PSLauncher.App/Services/IToastService.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace PSLauncher.App.Services;
|
||||||
|
|
||||||
|
public interface IToastService
|
||||||
|
{
|
||||||
|
void ShowSuccess(string title, string message);
|
||||||
|
void ShowError(string title, string message);
|
||||||
|
void ShowInfo(string title, string message);
|
||||||
|
}
|
||||||
32
src/PSLauncher.App/Services/ToastService.cs
Normal file
32
src/PSLauncher.App/Services/ToastService.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Toolkit.Uwp.Notifications;
|
||||||
|
|
||||||
|
namespace PSLauncher.App.Services;
|
||||||
|
|
||||||
|
public sealed class ToastService : IToastService
|
||||||
|
{
|
||||||
|
private readonly ILogger<ToastService> _logger;
|
||||||
|
|
||||||
|
public ToastService(ILogger<ToastService> logger) => _logger = logger;
|
||||||
|
|
||||||
|
public void ShowSuccess(string title, string message) => Show(title, message, "PROSERVE");
|
||||||
|
public void ShowError(string title, string message) => Show(title, message, "PROSERVE");
|
||||||
|
public void ShowInfo(string title, string message) => Show(title, message, "PROSERVE");
|
||||||
|
|
||||||
|
private void Show(string title, string message, string attribution)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
new ToastContentBuilder()
|
||||||
|
.AddText(title)
|
||||||
|
.AddText(message)
|
||||||
|
.AddAttributionText(attribution)
|
||||||
|
.Show();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Si Action Center n'est pas dispo (ancien Windows, COM bloqué…), on log mais on ne plante pas
|
||||||
|
_logger.LogWarning(ex, "Failed to show Windows toast");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PSLauncher.App.Views;
|
using PSLauncher.App.Views;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using PSLauncher.App.Services;
|
||||||
using PSLauncher.Core.Configuration;
|
using PSLauncher.Core.Configuration;
|
||||||
using PSLauncher.Core.Downloads;
|
using PSLauncher.Core.Downloads;
|
||||||
using PSLauncher.Core.Installations;
|
using PSLauncher.Core.Installations;
|
||||||
@@ -28,6 +30,8 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
private readonly IDownloadManager _downloadManager;
|
private readonly IDownloadManager _downloadManager;
|
||||||
private readonly IZipInstaller _zipInstaller;
|
private readonly IZipInstaller _zipInstaller;
|
||||||
private readonly ILicenseService _licenseService;
|
private readonly ILicenseService _licenseService;
|
||||||
|
private readonly IToastService _toastService;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly ILogger<MainViewModel> _logger;
|
private readonly ILogger<MainViewModel> _logger;
|
||||||
|
|
||||||
private LicenseValidationResponse? _license;
|
private LicenseValidationResponse? _license;
|
||||||
@@ -101,6 +105,8 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
IDownloadManager downloadManager,
|
IDownloadManager downloadManager,
|
||||||
IZipInstaller zipInstaller,
|
IZipInstaller zipInstaller,
|
||||||
ILicenseService licenseService,
|
ILicenseService licenseService,
|
||||||
|
IToastService toastService,
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
ILogger<MainViewModel> logger)
|
ILogger<MainViewModel> logger)
|
||||||
{
|
{
|
||||||
_registry = registry;
|
_registry = registry;
|
||||||
@@ -112,6 +118,8 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
_downloadManager = downloadManager;
|
_downloadManager = downloadManager;
|
||||||
_zipInstaller = zipInstaller;
|
_zipInstaller = zipInstaller;
|
||||||
_licenseService = licenseService;
|
_licenseService = licenseService;
|
||||||
|
_toastService = toastService;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
// Charge la license depuis le cache (pas d'appel réseau au démarrage,
|
// 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;
|
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]
|
[RelayCommand]
|
||||||
private async Task ActivateLicenseAsync()
|
private async Task ActivateLicenseAsync()
|
||||||
{
|
{
|
||||||
@@ -339,6 +361,9 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
ProgressDetail = null;
|
ProgressDetail = null;
|
||||||
ProgressPercent = 0;
|
ProgressPercent = 0;
|
||||||
RebuildList();
|
RebuildList();
|
||||||
|
_toastService.ShowSuccess(
|
||||||
|
"Installation terminée",
|
||||||
|
$"Proserve v{row.Version} est prête à être lancée.");
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
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}",
|
$"Le téléchargement ou l'installation a échoué :\n\n{ex.Message}",
|
||||||
"Erreur", MessageBoxButton.OK, MessageBoxImage.Error);
|
"Erreur", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
StatusMessage = $"Erreur : {ex.Message}";
|
StatusMessage = $"Erreur : {ex.Message}";
|
||||||
|
_toastService.ShowError($"Échec v{row.Version}", ex.Message);
|
||||||
}
|
}
|
||||||
finally
|
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]}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -175,7 +175,11 @@
|
|||||||
VerticalAlignment="Center" Margin="0,0,8,0" />
|
VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||||
<Button Style="{StaticResource SecondaryButton}"
|
<Button Style="{StaticResource SecondaryButton}"
|
||||||
Content="🔑 Activer / changer"
|
Content="🔑 Activer / changer"
|
||||||
Command="{Binding ActivateLicenseCommand}" />
|
Command="{Binding ActivateLicenseCommand}"
|
||||||
|
Margin="0,0,8,0" />
|
||||||
|
<Button Style="{StaticResource SecondaryButton}"
|
||||||
|
Content="⚙ Paramètres"
|
||||||
|
Command="{Binding OpenSettingsCommand}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
232
src/PSLauncher.App/Views/SettingsDialog.xaml
Normal file
232
src/PSLauncher.App/Views/SettingsDialog.xaml
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
<Window x:Class="PSLauncher.App.Views.SettingsDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:PSLauncher.App.ViewModels"
|
||||||
|
Title="Paramètres"
|
||||||
|
Width="640" Height="720"
|
||||||
|
MinWidth="540" MinHeight="500"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
Background="{StaticResource Brush.Bg.Window}"
|
||||||
|
d:DataContext="{d:DesignInstance Type=vm:SettingsViewModel}"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<Border Grid.Row="0" Padding="32,24,32,16">
|
||||||
|
<TextBlock Text="Paramètres" FontSize="22" FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource Brush.Text.Primary}" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Padding="32,0,32,16">
|
||||||
|
<StackPanel>
|
||||||
|
|
||||||
|
<!-- Serveur -->
|
||||||
|
<Border Background="{StaticResource Brush.Bg.Card}"
|
||||||
|
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
|
||||||
|
CornerRadius="6" Padding="20" Margin="0,0,0,16">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="SERVEUR"
|
||||||
|
FontSize="11" FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
Margin="0,0,0,8" />
|
||||||
|
<TextBlock Text="URL de l'API"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
FontSize="12" Margin="0,0,0,4" />
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox Grid.Column="0"
|
||||||
|
Text="{Binding ServerBaseUrl, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Background="#1A1A20"
|
||||||
|
Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
BorderBrush="{StaticResource Brush.Border}"
|
||||||
|
BorderThickness="1" Padding="8" />
|
||||||
|
<Button Grid.Column="1" Margin="8,0,0,0"
|
||||||
|
Style="{StaticResource SecondaryButton}"
|
||||||
|
Content="Tester"
|
||||||
|
Command="{Binding TestConnectionCommand}"
|
||||||
|
IsEnabled="{Binding IsTestingConnection, Converter={StaticResource InverseBoolToBool}}" />
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Text="{Binding ConnectionStatus}"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
FontSize="12" Margin="0,8,0,0" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Installation -->
|
||||||
|
<Border Background="{StaticResource Brush.Bg.Card}"
|
||||||
|
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
|
||||||
|
CornerRadius="6" Padding="20" Margin="0,0,0,16">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="INSTALLATION"
|
||||||
|
FontSize="11" FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
Margin="0,0,0,8" />
|
||||||
|
<TextBlock Text="Dossier où sont installées les versions"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
FontSize="12" Margin="0,0,0,4" />
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox Grid.Column="0"
|
||||||
|
Text="{Binding InstallRoot, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Background="#1A1A20"
|
||||||
|
Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
BorderBrush="{StaticResource Brush.Border}"
|
||||||
|
BorderThickness="1" Padding="8"
|
||||||
|
FontFamily="Consolas" />
|
||||||
|
<Button Grid.Column="1" Margin="8,0,0,0"
|
||||||
|
Style="{StaticResource SecondaryButton}"
|
||||||
|
Content="📁 Parcourir"
|
||||||
|
Command="{Binding BrowseInstallRootCommand}" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- License -->
|
||||||
|
<Border Background="{StaticResource Brush.Bg.Card}"
|
||||||
|
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
|
||||||
|
CornerRadius="6" Padding="20" Margin="0,0,0,16">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="LICENSE"
|
||||||
|
FontSize="11" FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
Margin="0,0,0,8" />
|
||||||
|
<TextBlock Text="{Binding LicenseInfo}"
|
||||||
|
Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
FontSize="13" />
|
||||||
|
<Grid Margin="0,12,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<TextBlock Text="Identifiant machine (anonyme)"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}" />
|
||||||
|
<TextBlock Text="{Binding MachineId}"
|
||||||
|
FontFamily="Consolas" FontSize="11"
|
||||||
|
Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
Margin="0,2,0,0" TextTrimming="CharacterEllipsis" />
|
||||||
|
</StackPanel>
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Style="{StaticResource SecondaryButton}"
|
||||||
|
Content="📋 Copier"
|
||||||
|
Command="{Binding CopyMachineIdCommand}" />
|
||||||
|
</Grid>
|
||||||
|
<Button Style="{StaticResource SecondaryButton}"
|
||||||
|
Content="Désactiver la license sur cette machine"
|
||||||
|
Command="{Binding DeactivateLicenseCommand}"
|
||||||
|
Margin="0,12,0,0" HorizontalAlignment="Left" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Cache -->
|
||||||
|
<Border Background="{StaticResource Brush.Bg.Card}"
|
||||||
|
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
|
||||||
|
CornerRadius="6" Padding="20" Margin="0,0,0,16">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="CACHE DE TÉLÉCHARGEMENTS"
|
||||||
|
FontSize="11" FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
Margin="0,0,0,8" />
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<TextBlock Text="{Binding CacheDirectory}"
|
||||||
|
FontFamily="Consolas" FontSize="11"
|
||||||
|
Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
TextTrimming="CharacterEllipsis" />
|
||||||
|
<TextBlock FontSize="12"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}">
|
||||||
|
<Run Text="Taille actuelle : " />
|
||||||
|
<Run Text="{Binding CacheSizeDisplay}" FontWeight="SemiBold" />
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
<Button Grid.Column="1" Margin="8,0,0,0"
|
||||||
|
Style="{StaticResource SecondaryButton}"
|
||||||
|
Content="📁"
|
||||||
|
Command="{Binding OpenCacheFolderCommand}" />
|
||||||
|
<Button Grid.Column="2" Margin="8,0,0,0"
|
||||||
|
Style="{StaticResource SecondaryButton}"
|
||||||
|
Content="🗑 Vider"
|
||||||
|
Command="{Binding ClearCacheCommand}" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Logs / About -->
|
||||||
|
<Border Background="{StaticResource Brush.Bg.Card}"
|
||||||
|
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
|
||||||
|
CornerRadius="6" Padding="20" Margin="0,0,0,16">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="LOGS & APPLICATION"
|
||||||
|
FontSize="11" FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
Margin="0,0,0,8" />
|
||||||
|
<TextBlock FontSize="13"
|
||||||
|
Foreground="{StaticResource Brush.Text.Primary}">
|
||||||
|
<Run Text="Version : " />
|
||||||
|
<Run Text="{Binding LauncherVersion}" FontWeight="SemiBold" />
|
||||||
|
</TextBlock>
|
||||||
|
<Grid Margin="0,8,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<TextBlock Text="{Binding LogsDirectory}"
|
||||||
|
FontFamily="Consolas" FontSize="11"
|
||||||
|
Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
TextTrimming="CharacterEllipsis" />
|
||||||
|
<TextBlock Text="Logs rotation quotidienne, 10 jours conservés"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
Margin="0,2,0,0" />
|
||||||
|
</StackPanel>
|
||||||
|
<Button Grid.Column="1" Margin="8,0,0,0"
|
||||||
|
Style="{StaticResource SecondaryButton}"
|
||||||
|
Content="📁 Ouvrir"
|
||||||
|
Command="{Binding OpenLogsFolderCommand}" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<Border Grid.Row="2" Background="{StaticResource Brush.Bg.Footer}"
|
||||||
|
BorderBrush="{StaticResource Brush.Border}" BorderThickness="0,1,0,0"
|
||||||
|
Padding="20,12">
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<Button Style="{StaticResource SecondaryButton}"
|
||||||
|
Content="Annuler"
|
||||||
|
IsCancel="True"
|
||||||
|
Margin="0,0,12,0"
|
||||||
|
Click="OnCancel" />
|
||||||
|
<Button Style="{StaticResource AccentButton}"
|
||||||
|
Content="Enregistrer"
|
||||||
|
Padding="32,8"
|
||||||
|
IsDefault="True"
|
||||||
|
Click="OnSave" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
28
src/PSLauncher.App/Views/SettingsDialog.xaml.cs
Normal file
28
src/PSLauncher.App/Views/SettingsDialog.xaml.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using PSLauncher.App.ViewModels;
|
||||||
|
|
||||||
|
namespace PSLauncher.App.Views;
|
||||||
|
|
||||||
|
public partial class SettingsDialog : Window
|
||||||
|
{
|
||||||
|
private readonly SettingsViewModel _vm;
|
||||||
|
|
||||||
|
public SettingsDialog(SettingsViewModel vm)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = _vm = vm;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSave(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_vm.SaveCommand.Execute(null);
|
||||||
|
DialogResult = true;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCancel(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
DialogResult = false;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user