diff --git a/installer/PSLauncher.iss b/installer/PSLauncher.iss
index ed60e8c..819da29 100644
--- a/installer/PSLauncher.iss
+++ b/installer/PSLauncher.iss
@@ -11,7 +11,7 @@
#define MyAppName "PROSERVE Launcher"
#define MyAppShortName "PS_Launcher"
-#define MyAppVersion "0.15.0"
+#define MyAppVersion "0.16.0"
#define MyAppPublisher "ASTERION VR"
#define MyAppURL "https://asterionvr.com"
#define MyAppExeName "PS_Launcher.exe"
diff --git a/src/PSLauncher.App/PSLauncher.App.csproj b/src/PSLauncher.App/PSLauncher.App.csproj
index 2e63260..987f2e2 100644
--- a/src/PSLauncher.App/PSLauncher.App.csproj
+++ b/src/PSLauncher.App/PSLauncher.App.csproj
@@ -15,9 +15,9 @@
PROSERVE Launcher
© 2026 ASTERION VR — All rights reserved
PSLauncher.App
- 0.15.0
- 0.15.0.0
- 0.15.0.0
+ 0.16.0
+ 0.16.0.0
+ 0.16.0.0
true
diff --git a/src/PSLauncher.App/ViewModels/MainViewModel.cs b/src/PSLauncher.App/ViewModels/MainViewModel.cs
index 291654e..fdca5e0 100644
--- a/src/PSLauncher.App/ViewModels/MainViewModel.cs
+++ b/src/PSLauncher.App/ViewModels/MainViewModel.cs
@@ -438,7 +438,7 @@ public sealed partial class MainViewModel : ObservableObject
catch (Exception ex)
{
_logger.LogError(ex, "Self-update failed");
- MessageBox.Show(Strings.MsgSelfUpdateFailed(ex.Message),
+ ThemedMessageBox.Show(Strings.MsgSelfUpdateFailed(ex.Message),
Strings.MsgBoxError, MessageBoxButton.OK, MessageBoxImage.Error);
StatusMessage = Strings.StatusSelfUpdateError(ex.Message);
IsBusy = false;
@@ -518,7 +518,7 @@ public sealed partial class MainViewModel : ObservableObject
private void CancelDownload()
{
if (_activeDownloadCts is null || _activeRow is null) return;
- var confirm = MessageBox.Show(
+ var confirm = ThemedMessageBox.Show(
Strings.MsgCancelDownloadConfirm(_activeRow.Version),
Strings.MsgBoxCancelDl,
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
@@ -535,7 +535,7 @@ public sealed partial class MainViewModel : ObservableObject
// Si un DL est actif sur cette même row, on doit d'abord l'annuler proprement.
var st = _downloadManager.GetResumableState(row.Version);
var sizeStr = FormatSize(st?.DownloadedBytes ?? 0);
- var confirm = MessageBox.Show(
+ var confirm = ThemedMessageBox.Show(
Strings.MsgRestartDownloadConfirm(row.Version, sizeStr),
Strings.MsgBoxRestartDl,
MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No);
@@ -605,7 +605,7 @@ public sealed partial class MainViewModel : ObservableObject
catch (Exception ex)
{
_logger.LogError(ex, "Launch failed");
- MessageBox.Show(Strings.MsgLaunchFailed(ex.Message),
+ ThemedMessageBox.Show(Strings.MsgLaunchFailed(ex.Message),
Strings.MsgBoxLaunchError, MessageBoxButton.OK, MessageBoxImage.Error);
}
}
@@ -613,7 +613,7 @@ public sealed partial class MainViewModel : ObservableObject
private async Task InstallVersionAsync(VersionRowViewModel row)
{
if (row.Remote is null) return;
- if (IsBusy) { MessageBox.Show(Strings.MsgBusy, Strings.MsgBoxPatience, MessageBoxButton.OK, MessageBoxImage.Information); return; }
+ if (IsBusy) { ThemedMessageBox.Show(Strings.MsgBusy, Strings.MsgBoxPatience, MessageBoxButton.OK, MessageBoxImage.Information); return; }
IsBusy = true;
_activeRow = row;
@@ -738,7 +738,7 @@ public sealed partial class MainViewModel : ObservableObject
// notifie l'utilisateur que la DB n'a pas été migrée — la prochaine relance
// de l'install (ou un Settings → "Rejouer migrations") tentera à nouveau.
_logger.LogError("DB migration failed for v{Version}: {Detail}", row.Version, migResult.FailureMessage);
- MessageBox.Show(
+ ThemedMessageBox.Show(
Strings.MsgMigrationFailed(migResult.FailureMessage ?? "?"),
Strings.MsgBoxMigrationFailed,
MessageBoxButton.OK, MessageBoxImage.Warning);
@@ -779,13 +779,13 @@ public sealed partial class MainViewModel : ObservableObject
_logger.LogInformation("No _report/ in v{Version} ZIP, report tool unchanged", row.Version);
break;
case DeployStatus.XamppNotFound:
- MessageBox.Show(
+ ThemedMessageBox.Show(
Strings.MsgReportXamppNotFound(_config.ReportTool.HtdocsRoot),
Strings.MsgBoxReportDeployFailed,
MessageBoxButton.OK, MessageBoxImage.Warning);
break;
case DeployStatus.Failed:
- MessageBox.Show(
+ ThemedMessageBox.Show(
Strings.MsgReportDeployFailed(rdResult.ErrorMessage ?? "?"),
Strings.MsgBoxReportDeployFailed,
MessageBoxButton.OK, MessageBoxImage.Warning);
@@ -818,7 +818,7 @@ public sealed partial class MainViewModel : ObservableObject
row.State = VersionRowState.AvailableIdle;
row.ProgressDetail = null;
row.ProgressPercent = 0;
- MessageBox.Show(
+ ThemedMessageBox.Show(
Strings.MsgInstallFailed(ex.Message),
Strings.MsgBoxError, MessageBoxButton.OK, MessageBoxImage.Error);
StatusMessage = Strings.StatusError(ex.Message);
@@ -841,14 +841,14 @@ public sealed partial class MainViewModel : ObservableObject
if (row.Installed is null) return;
var sizeStr = FormatSize(row.SizeBytes);
- var confirm = MessageBox.Show(
+ var confirm = ThemedMessageBox.Show(
Strings.MsgUninstallConfirm(row.Version, row.FolderPath, sizeStr),
Strings.MsgBoxConfirm,
MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No);
if (confirm != MessageBoxResult.Yes) return;
- if (IsBusy) { MessageBox.Show(Strings.MsgBusy, Strings.MsgBoxPatience, MessageBoxButton.OK, MessageBoxImage.Information); return; }
+ if (IsBusy) { ThemedMessageBox.Show(Strings.MsgBusy, Strings.MsgBoxPatience, MessageBoxButton.OK, MessageBoxImage.Information); return; }
IsBusy = true;
row.State = VersionRowState.Uninstalling;
@@ -863,7 +863,7 @@ public sealed partial class MainViewModel : ObservableObject
{
_logger.LogError(ex, "Uninstall failed for {Version}", row.Version);
row.State = VersionRowState.InstalledIdle;
- MessageBox.Show(Strings.MsgUninstallFailed(ex.Message), Strings.MsgBoxError,
+ ThemedMessageBox.Show(Strings.MsgUninstallFailed(ex.Message), Strings.MsgBoxError,
MessageBoxButton.OK, MessageBoxImage.Error);
StatusMessage = Strings.StatusError(ex.Message);
}
@@ -878,7 +878,7 @@ public sealed partial class MainViewModel : ObservableObject
var url = row.Remote?.ReleaseNotesUrl;
if (string.IsNullOrEmpty(url))
{
- MessageBox.Show(Strings.MsgNoReleaseNotes,
+ ThemedMessageBox.Show(Strings.MsgNoReleaseNotes,
Strings.MsgBoxReleaseNotes, MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
@@ -887,7 +887,7 @@ public sealed partial class MainViewModel : ObservableObject
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to fetch release notes");
- MessageBox.Show(Strings.MsgReleaseNotesFetchFailed(ex.Message),
+ ThemedMessageBox.Show(Strings.MsgReleaseNotesFetchFailed(ex.Message),
Strings.MsgBoxError, MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
diff --git a/src/PSLauncher.App/ViewModels/SettingsViewModel.cs b/src/PSLauncher.App/ViewModels/SettingsViewModel.cs
index eec2f37..3a9bdc9 100644
--- a/src/PSLauncher.App/ViewModels/SettingsViewModel.cs
+++ b/src/PSLauncher.App/ViewModels/SettingsViewModel.cs
@@ -6,6 +6,7 @@ using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
+using PSLauncher.App.Views;
using PSLauncher.Core.Configuration;
using PSLauncher.Core.Downloads;
using PSLauncher.Core.Installations;
@@ -239,7 +240,7 @@ public sealed partial class SettingsViewModel : ObservableObject
[RelayCommand]
private void ClearCache()
{
- var confirm = MessageBox.Show(
+ var confirm = ThemedMessageBox.Show(
Strings.MsgClearCacheConfirm,
Strings.MsgBoxConfirm,
MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No);
@@ -256,7 +257,7 @@ public sealed partial class SettingsViewModel : ObservableObject
catch (Exception ex)
{
_logger.LogError(ex, "ClearCache failed");
- MessageBox.Show(ex.Message, Strings.MsgBoxError, MessageBoxButton.OK, MessageBoxImage.Error);
+ ThemedMessageBox.Show(ex.Message, Strings.MsgBoxError, MessageBoxButton.OK, MessageBoxImage.Error);
}
}
@@ -304,7 +305,7 @@ public sealed partial class SettingsViewModel : ObservableObject
// Le launcher doit être relancé pour que les bindings x:Static reflètent
// la nouvelle langue. Plutôt que de demander à l'utilisateur de fermer/rouvrir,
// on spawne un cmd qui attend la fermeture puis relance.
- var ok = MessageBox.Show(
+ var ok = ThemedMessageBox.Show(
Strings.MsgLanguageRestart,
Strings.MsgBoxLanguageChange,
MessageBoxButton.OKCancel, MessageBoxImage.Information, MessageBoxResult.OK);
@@ -335,7 +336,7 @@ public sealed partial class SettingsViewModel : ObservableObject
[RelayCommand]
private void DeactivateLicense()
{
- var confirm = MessageBox.Show(
+ var confirm = ThemedMessageBox.Show(
Strings.MsgDeactivateLicenseConfirm,
Strings.MsgBoxConfirm,
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
@@ -526,7 +527,7 @@ public sealed partial class SettingsViewModel : ObservableObject
{
var localDate = backup.TimestampUtc.ToLocalTime();
var dateLabel = Strings.FormatLongDate(localDate);
- var confirm = MessageBox.Show(
+ var confirm = ThemedMessageBox.Show(
Strings.MsgRevertReportConfirm(dateLabel),
Strings.MsgBoxRevertReport,
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
diff --git a/src/PSLauncher.App/Views/LicenseDetailsDialog.xaml.cs b/src/PSLauncher.App/Views/LicenseDetailsDialog.xaml.cs
index ac3230a..294b0fd 100644
--- a/src/PSLauncher.App/Views/LicenseDetailsDialog.xaml.cs
+++ b/src/PSLauncher.App/Views/LicenseDetailsDialog.xaml.cs
@@ -66,7 +66,7 @@ public partial class LicenseDetailsDialog : Window
private void OnDeactivate(object sender, RoutedEventArgs e)
{
- var confirm = MessageBox.Show(
+ var confirm = ThemedMessageBox.Show(
Strings.MsgDeactivateLicenseDetailedConfirm,
Strings.MsgBoxConfirm,
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
diff --git a/src/PSLauncher.App/Views/MainWindow.xaml.cs b/src/PSLauncher.App/Views/MainWindow.xaml.cs
index 5bf59c0..c3979ed 100644
--- a/src/PSLauncher.App/Views/MainWindow.xaml.cs
+++ b/src/PSLauncher.App/Views/MainWindow.xaml.cs
@@ -42,7 +42,7 @@ public partial class MainWindow : Window
if (DataContext is MainViewModel vm && vm.HasActiveDownload)
{
var version = vm.ActiveDownloadVersion ?? "?";
- var result = MessageBox.Show(
+ var result = ThemedMessageBox.Show(
Strings.MsgQuitWhileDownloadingConfirm(version),
Strings.MsgBoxQuit,
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
diff --git a/src/PSLauncher.App/Views/ThemedMessageBox.xaml b/src/PSLauncher.App/Views/ThemedMessageBox.xaml
new file mode 100644
index 0000000..fec94ed
--- /dev/null
+++ b/src/PSLauncher.App/Views/ThemedMessageBox.xaml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/PSLauncher.App/Views/ThemedMessageBox.xaml.cs b/src/PSLauncher.App/Views/ThemedMessageBox.xaml.cs
new file mode 100644
index 0000000..b15e1b0
--- /dev/null
+++ b/src/PSLauncher.App/Views/ThemedMessageBox.xaml.cs
@@ -0,0 +1,187 @@
+using System.Windows;
+using System.Windows.Input;
+using System.Windows.Media;
+using PSLauncher.Core.Localization;
+
+namespace PSLauncher.App.Views;
+
+///
+/// Drop-in pour avec un look qui colle
+/// au reste de la UI (dark theme, accent ASTERION). API copiée :
+///
+/// ThemedMessageBox.Show("Texte", "Titre", MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
+///
+///
+/// Notes :
+/// - est mappé vers une icône emoji + couleur.
+/// - Les libellés des boutons (OK / Yes / No / Cancel) sont localisés via .
+/// - Owner = MainWindow par défaut (centrage), peut être passé explicitement.
+/// - Esc renvoie Cancel ou No selon le bouton défini comme cancel.
+///
+public partial class ThemedMessageBox : Window
+{
+ public MessageBoxResult Result { get; private set; } = MessageBoxResult.None;
+
+ private MessageBoxResult _btn1Result = MessageBoxResult.None;
+ private MessageBoxResult _btn2Result = MessageBoxResult.None;
+ private MessageBoxResult _btn3Result = MessageBoxResult.None;
+
+ private ThemedMessageBox(string message, string title, MessageBoxButton button,
+ MessageBoxImage icon, MessageBoxResult defaultResult)
+ {
+ InitializeComponent();
+
+ Title = title;
+ TitleText.Text = title;
+ MessageText.Text = message;
+ SetIcon(icon);
+ SetupButtons(button, defaultResult);
+
+ // Esc = cancel/no
+ PreviewKeyDown += (_, e) =>
+ {
+ if (e.Key == Key.Escape)
+ {
+ Result = button switch
+ {
+ MessageBoxButton.OK => MessageBoxResult.OK,
+ MessageBoxButton.OKCancel => MessageBoxResult.Cancel,
+ MessageBoxButton.YesNo => MessageBoxResult.No,
+ MessageBoxButton.YesNoCancel => MessageBoxResult.Cancel,
+ _ => MessageBoxResult.None,
+ };
+ DialogResult = false;
+ Close();
+ }
+ };
+ }
+
+ private void SetIcon(MessageBoxImage icon)
+ {
+ // NOTE : MessageBoxImage.Stop ≡ Error (16) et Asterisk ≡ Information (64)
+ // côté enum .NET, donc on ne switche que sur les noms canoniques.
+ switch (icon)
+ {
+ case MessageBoxImage.Error: // = Stop = Hand
+ IconText.Text = "⛔";
+ IconText.Foreground = new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44));
+ break;
+ case MessageBoxImage.Warning: // = Exclamation
+ IconText.Text = "⚠";
+ IconText.Foreground = new SolidColorBrush(Color.FromRgb(0xF5, 0x9E, 0x0B));
+ break;
+ case MessageBoxImage.Question:
+ IconText.Text = "❓";
+ break;
+ case MessageBoxImage.Information: // = Asterisk
+ IconText.Text = "ℹ";
+ IconText.Foreground = new SolidColorBrush(Color.FromRgb(0x3B, 0x82, 0xF6));
+ break;
+ default:
+ IconText.Visibility = Visibility.Collapsed;
+ break;
+ }
+ }
+
+ ///
+ /// Configure jusqu'à 3 boutons. L'ordre visuel est gauche → droite, avec
+ /// le bouton "principal" (default) en accent à l'extrême droite. Les autres
+ /// en secondary à gauche.
+ ///
+ private void SetupButtons(MessageBoxButton button, MessageBoxResult defaultResult)
+ {
+ // Liste (label, result) dans l'ordre où on veut les afficher
+ var btns = button switch
+ {
+ MessageBoxButton.OK => new[] { (Strings.ActionOk, MessageBoxResult.OK) },
+ MessageBoxButton.OKCancel => new[] {
+ (Strings.ActionCancel, MessageBoxResult.Cancel),
+ (Strings.ActionOk, MessageBoxResult.OK),
+ },
+ MessageBoxButton.YesNo => new[] {
+ (Strings.ActionNo, MessageBoxResult.No),
+ (Strings.ActionYes, MessageBoxResult.Yes),
+ },
+ MessageBoxButton.YesNoCancel => new[] {
+ (Strings.ActionCancel, MessageBoxResult.Cancel),
+ (Strings.ActionNo, MessageBoxResult.No),
+ (Strings.ActionYes, MessageBoxResult.Yes),
+ },
+ _ => new[] { (Strings.ActionOk, MessageBoxResult.OK) },
+ };
+
+ // Btn3 = bouton accent (le plus à droite, principal). Toujours rempli.
+ // Btn2 = secondary à sa gauche, optionnel.
+ // Btn1 = secondary tout à gauche, optionnel.
+ // On remplit en partant de la fin pour que l'accent soit toujours sur Btn3.
+ var slots = new[] { Btn1, Btn2, Btn3 };
+ var resultSetters = new System.Action[]
+ {
+ r => _btn1Result = r,
+ r => _btn2Result = r,
+ r => _btn3Result = r,
+ };
+
+ int startSlot = 3 - btns.Length;
+ for (int i = 0; i < btns.Length; i++)
+ {
+ int slotIdx = startSlot + i;
+ slots[slotIdx].Content = btns[i].Item1;
+ slots[slotIdx].Visibility = Visibility.Visible;
+ resultSetters[slotIdx](btns[i].Item2);
+ // Default = celui dont le result match defaultResult
+ if (btns[i].Item2 == defaultResult)
+ slots[slotIdx].IsDefault = true;
+ }
+
+ // Si aucun match exact pour le default, on prend le bouton principal (Btn3)
+ if (Btn3.Visibility == Visibility.Visible && !Btn1.IsDefault && !Btn2.IsDefault && !Btn3.IsDefault)
+ Btn3.IsDefault = true;
+ }
+
+ private void OnBtn1Click(object sender, RoutedEventArgs e) => Finish(_btn1Result);
+ private void OnBtn2Click(object sender, RoutedEventArgs e) => Finish(_btn2Result);
+ private void OnBtn3Click(object sender, RoutedEventArgs e) => Finish(_btn3Result);
+
+ private void Finish(MessageBoxResult result)
+ {
+ Result = result;
+ DialogResult = true;
+ Close();
+ }
+
+ // ===================== STATIC SHOW OVERLOADS =====================
+
+ public static MessageBoxResult Show(string message)
+ => Show(message, string.Empty, MessageBoxButton.OK, MessageBoxImage.None, MessageBoxResult.OK);
+
+ public static MessageBoxResult Show(string message, string title)
+ => Show(message, title, MessageBoxButton.OK, MessageBoxImage.None, MessageBoxResult.OK);
+
+ public static MessageBoxResult Show(string message, string title, MessageBoxButton button)
+ => Show(message, title, button, MessageBoxImage.None, DefaultFor(button));
+
+ public static MessageBoxResult Show(string message, string title, MessageBoxButton button, MessageBoxImage icon)
+ => Show(message, title, button, icon, DefaultFor(button));
+
+ public static MessageBoxResult Show(string message, string title, MessageBoxButton button,
+ MessageBoxImage icon, MessageBoxResult defaultResult)
+ {
+ var owner = Application.Current?.Windows.OfType().FirstOrDefault(w => w.IsActive)
+ ?? Application.Current?.MainWindow;
+ var dlg = new ThemedMessageBox(message, title, button, icon, defaultResult);
+ if (owner is not null && owner.IsVisible) dlg.Owner = owner;
+ else dlg.WindowStartupLocation = WindowStartupLocation.CenterScreen;
+ dlg.ShowDialog();
+ return dlg.Result;
+ }
+
+ private static MessageBoxResult DefaultFor(MessageBoxButton button) => button switch
+ {
+ MessageBoxButton.OK => MessageBoxResult.OK,
+ MessageBoxButton.OKCancel => MessageBoxResult.OK,
+ MessageBoxButton.YesNo => MessageBoxResult.Yes,
+ MessageBoxButton.YesNoCancel => MessageBoxResult.Yes,
+ _ => MessageBoxResult.None,
+ };
+}
diff --git a/src/PSLauncher.Core/Localization/Strings.cs b/src/PSLauncher.Core/Localization/Strings.cs
index 4164f92..1136afb 100644
--- a/src/PSLauncher.Core/Localization/Strings.cs
+++ b/src/PSLauncher.Core/Localization/Strings.cs
@@ -102,6 +102,9 @@ public static class Strings
public static string ActionInstall => T("⬇ Installer", "⬇ Install", "⬇ 安装", "⬇ ติดตั้ง", "⬇ تثبيت");
public static string ActionInstallBig => T("⬇ INSTALLER", "⬇ INSTALL", "⬇ 安装", "⬇ ติดตั้ง", "⬇ تثبيت");
public static string ActionCancel => T("Annuler", "Cancel", "取消", "ยกเลิก", "إلغاء");
+ public static string ActionOk => T("OK", "OK", "确定", "ตกลง", "موافق");
+ public static string ActionYes => T("Oui", "Yes", "是", "ใช่", "نعم");
+ public static string ActionNo => T("Non", "No", "否", "ไม่", "لا");
public static string ActionLater => T("Plus tard", "Later", "稍后", "ภายหลัง", "لاحقاً");
public static string ActionSave => T("Enregistrer", "Save", "保存", "บันทึก", "حفظ");
public static string ActionClose => T("Fermer", "Close", "关闭", "ปิด", "إغلاق");
diff --git a/src/PSLauncher.Updater/PSLauncher.Updater.csproj b/src/PSLauncher.Updater/PSLauncher.Updater.csproj
index b1cb23c..d5d5ae3 100644
--- a/src/PSLauncher.Updater/PSLauncher.Updater.csproj
+++ b/src/PSLauncher.Updater/PSLauncher.Updater.csproj
@@ -8,7 +8,7 @@
latest
PS_Launcher.Updater
PSLauncher.Updater
- 0.15.0
+ 0.16.0
..\PSLauncher.App\Resources\favicon.ico
ASTERION VR
© 2026 ASTERION VR — All rights reserved