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:
2026-05-01 11:35:22 +02:00
parent 1770def4d0
commit eeff3c007b
11 changed files with 597 additions and 5 deletions

View File

@@ -1,9 +1,11 @@
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using PSLauncher.App.Services;
using PSLauncher.App.ViewModels;
using PSLauncher.App.Views;
using PSLauncher.Core.Configuration;
@@ -15,6 +17,7 @@ using PSLauncher.Core.Manifests;
using PSLauncher.Core.Process;
using PSLauncher.Core.Updates;
using PSLauncher.Models;
using Serilog;
namespace PSLauncher.App;
@@ -22,18 +25,46 @@ public partial class App : Application
{
private IHost? _host;
public static string LogsDirectory { get; private set; } = string.Empty;
protected override void OnStartup(StartupEventArgs 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()
.ConfigureLogging(b => { b.ClearProviders(); b.AddDebug(); })
.UseSerilog()
.ConfigureServices((_, services) =>
{
services.AddSingleton<IConfigStore, ConfigStore>();
services.AddSingleton<LocalConfig>(sp => sp.GetRequiredService<IConfigStore>().Load());
// HttpClient partagé (timeout long pour les gros téléchargements)
services.AddSingleton(sp =>
{
var http = new HttpClient(new SocketsHttpHandler
@@ -44,7 +75,7 @@ public partial class App : Application
{
Timeout = Timeout.InfiniteTimeSpan
};
http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("PSLauncher", "0.2"));
http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("PSLauncher", "0.5"));
return http;
});
@@ -75,6 +106,9 @@ public partial class App : Application
sp.GetRequiredService<LocalConfig>(),
sp.GetRequiredService<ILogger<LicenseService>>()));
services.AddSingleton<IToastService, ToastService>();
services.AddTransient<SettingsViewModel>();
services.AddSingleton<MainViewModel>();
services.AddSingleton<MainWindow>();
})
@@ -88,6 +122,8 @@ public partial class App : Application
protected override void OnExit(ExitEventArgs e)
{
Log.Information("PSLauncher shutting down");
Log.CloseAndFlush();
_host?.Dispose();
base.OnExit(e);
}

View File

@@ -2,7 +2,9 @@
<PropertyGroup>
<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>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
@@ -27,6 +29,11 @@
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.1" />
<PackageReference Include="Markdig" Version="0.37.0" />
<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>

View 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;
}

View File

@@ -3,6 +3,9 @@
<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 -->
<SolidColorBrush x:Key="Brush.Bg.Window" Color="#1B1B1F" />
<SolidColorBrush x:Key="Brush.Bg.Sidebar" Color="#202024" />

View 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);
}

View 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");
}
}
}

View File

@@ -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
{

View 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]}";
}
}

View File

@@ -175,7 +175,11 @@
VerticalAlignment="Center" Margin="0,0,8,0" />
<Button Style="{StaticResource SecondaryButton}"
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>
</Grid>
</Border>

View 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 &amp; 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>

View 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();
}
}