diff --git a/installer/PSLauncher.iss b/installer/PSLauncher.iss
index 54aeff3..c1ae1a8 100644
--- a/installer/PSLauncher.iss
+++ b/installer/PSLauncher.iss
@@ -11,7 +11,7 @@
#define MyAppName "PROSERVE Launcher"
#define MyAppShortName "PS_Launcher"
-#define MyAppVersion "0.19.0"
+#define MyAppVersion "0.20.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 054698d..8cb1f06 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.19.0
- 0.19.0.0
- 0.19.0.0
+ 0.20.0
+ 0.20.0.0
+ 0.20.0.0
true
diff --git a/src/PSLauncher.App/ViewModels/MainViewModel.cs b/src/PSLauncher.App/ViewModels/MainViewModel.cs
index 2ccad32..7168365 100644
--- a/src/PSLauncher.App/ViewModels/MainViewModel.cs
+++ b/src/PSLauncher.App/ViewModels/MainViewModel.cs
@@ -53,6 +53,13 @@ public sealed partial class MainViewModel : ObservableObject
private RemoteManifest? _lastManifest;
private CancellationTokenSource? _activeDownloadCts;
private VersionRowViewModel? _activeRow;
+ ///
+ /// Référence au Task de l'install/DL en cours, exposée aux commandes externes
+ /// (ex. RestartFromZero) qui ont besoin d'attendre la fin réelle des écritures
+ /// disque avant de supprimer le partial. Sinon les segments continuent à écrire
+ /// pendant 1-2 sec après le Cancel() et recréent le fichier qu'on vient de delete.
+ ///
+ private Task? _activeInstallTask;
public ObservableCollection OtherVersions { get; } = new();
@@ -333,7 +340,7 @@ public sealed partial class MainViewModel : ObservableObject
private void WireRowHandlers(VersionRowViewModel row)
{
row.LaunchHandler = LaunchVersion;
- row.InstallHandler = r => _ = InstallVersionAsync(r);
+ row.InstallHandler = r => _activeInstallTask = InstallVersionAsync(r);
row.UninstallHandler = r => _ = UninstallVersionAsync(r);
row.ShowReleaseNotesHandler = r => _ = ShowReleaseNotesAsync(r);
row.OpenFolderHandler = OpenVersionFolder;
@@ -545,12 +552,26 @@ public sealed partial class MainViewModel : ObservableObject
MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No);
if (confirm != MessageBoxResult.Yes) return;
- // Cancel le DL actif si c'est cette row
+ // Cancel le DL actif si c'est cette row, puis attend la propagation complète
+ // (les 16 segments doivent chacun stopper leurs écritures et fermer leur
+ // FileStream sinon DiscardResumableState échouera/sera réécrasé).
if (_activeRow is not null && _activeRow == row && _activeDownloadCts is not null)
{
_activeDownloadCts.Cancel();
- // Attend la fin de la tâche en cours pour libérer les handles fichier
- try { await Task.Delay(200); } catch { }
+ // Attend que InstallVersionAsync termine son catch OperationCanceledException
+ // — y compris le flush + state save + libération des FileStream segment.
+ // Timeout de sécurité 10s au cas où une connexion HTTPS bloque sur close.
+ if (_activeInstallTask is { } running && !running.IsCompleted)
+ {
+ StatusMessage = "Annulation en cours…";
+ try
+ {
+ var awaitTask = await Task.WhenAny(running, Task.Delay(TimeSpan.FromSeconds(10)));
+ if (!ReferenceEquals(awaitTask, running))
+ _logger.LogWarning("Install task did not finish cancelling within 10s, proceeding anyway");
+ }
+ catch (Exception ex) { _logger.LogDebug(ex, "Awaiting install task during restart"); }
+ }
}
_downloadManager.DiscardResumableState(row.Version);
@@ -682,9 +703,13 @@ public sealed partial class MainViewModel : ObservableObject
row.ProgressPercent = 0;
var dlProgress = new Progress(p => UpdateDlProgress(row, p));
// Reporter dédié pour la phase verify SHA-256 (peut prendre 30s-3min sur 14 Go).
- // On bascule le StatusMessage à ce moment-là pour que l'utilisateur sache ce qui se passe.
+ // On bascule row.State sur Verifying à la première remontée, ce qui change
+ // le badge de la row de "⬇ Téléchargement…" vers "🔍 Vérification…" — les
+ // deux phases sont distinctes côté UX bien que `DownloadAsync` les enchaîne.
var hashProgress = new Progress(pct =>
{
+ if (row.State == VersionRowState.Downloading)
+ row.State = VersionRowState.Verifying;
var percent = (int)Math.Round(pct * 100.0);
ProgressPercent = percent;
row.ProgressPercent = percent;
@@ -879,6 +904,7 @@ public sealed partial class MainViewModel : ObservableObject
_activeDownloadCts?.Dispose();
_activeDownloadCts = null;
_activeRow = null;
+ _activeInstallTask = null;
IsBusy = false;
}
}
diff --git a/src/PSLauncher.App/ViewModels/VersionRowViewModel.cs b/src/PSLauncher.App/ViewModels/VersionRowViewModel.cs
index ec756ef..2fbe606 100644
--- a/src/PSLauncher.App/ViewModels/VersionRowViewModel.cs
+++ b/src/PSLauncher.App/ViewModels/VersionRowViewModel.cs
@@ -10,6 +10,7 @@ public enum VersionRowState
InstalledIdle, // Installée localement, pas d'action en cours
AvailableIdle, // Disponible côté serveur, pas installée
Downloading,
+ Verifying, // SHA-256 du ZIP téléchargé en cours
Installing,
Uninstalling,
}
@@ -77,13 +78,17 @@ public sealed partial class VersionRowViewModel : ObservableObject
public bool IsInstalled => State is VersionRowState.InstalledIdle or VersionRowState.Uninstalling;
public bool IsRemoteOnly => State is VersionRowState.AvailableIdle;
- public bool IsBusy => State is VersionRowState.Downloading or VersionRowState.Installing or VersionRowState.Uninstalling;
+ public bool IsBusy => State is VersionRowState.Downloading
+ or VersionRowState.Verifying
+ or VersionRowState.Installing
+ or VersionRowState.Uninstalling;
public string StateLabel => State switch
{
VersionRowState.InstalledIdle => Strings.StatusInstalled,
VersionRowState.AvailableIdle => Strings.StatusAvailable,
VersionRowState.Downloading => Strings.StatusDownloading,
+ VersionRowState.Verifying => Strings.StatusVerifying,
VersionRowState.Installing => Strings.StatusInstalling,
VersionRowState.Uninstalling => Strings.StatusUninstalling,
_ => string.Empty
diff --git a/src/PSLauncher.Core/Localization/Strings.cs b/src/PSLauncher.Core/Localization/Strings.cs
index 913181e..a9f653f 100644
--- a/src/PSLauncher.Core/Localization/Strings.cs
+++ b/src/PSLauncher.Core/Localization/Strings.cs
@@ -95,6 +95,7 @@ public static class Strings
public static string StatusDownloading => T("⬇ Téléchargement…", "⬇ Downloading…", "⬇ 下载中…", "⬇ กำลังดาวน์โหลด…", "⬇ جارٍ التنزيل…");
public static string StatusInstalling => T("📦 Installation…", "📦 Installing…", "📦 安装中…", "📦 กำลังติดตั้ง…", "📦 جارٍ التثبيت…");
public static string StatusUninstalling => T("🗑 Suppression…", "🗑 Uninstalling…", "🗑 卸载中…", "🗑 กำลังถอนการติดตั้ง…", "🗑 جارٍ الإزالة…");
+ public static string StatusVerifying => T("🔍 Vérification…", "🔍 Verifying…", "🔍 校验中…", "🔍 กำลังตรวจสอบ…", "🔍 جارٍ التحقق…");
// ==================== ACTIONS ====================
public static string ActionLaunch => T("▶ Lancer", "▶ Launch", "▶ 启动", "▶ เปิด", "▶ تشغيل");
diff --git a/src/PSLauncher.Updater/PSLauncher.Updater.csproj b/src/PSLauncher.Updater/PSLauncher.Updater.csproj
index f9f6501..4adacda 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.19.0
+ 0.20.0
..\PSLauncher.App\Resources\favicon.ico
ASTERION VR
© 2026 ASTERION VR — All rights reserved