diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1d5d2951..c13ab726 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -13,7 +13,7 @@
-
+
diff --git a/LANCommander.Launcher.Services/InstallService.cs b/LANCommander.Launcher.Services/InstallService.cs
index ddacd6d3..68f75a90 100644
--- a/LANCommander.Launcher.Services/InstallService.cs
+++ b/LANCommander.Launcher.Services/InstallService.cs
@@ -79,11 +79,13 @@ namespace LANCommander.Launcher.Services
OnProgress?.Invoke(e);
};
- _redistributableClient.OnInstallProgressUpdate += (e) =>
- {
- UpdateQueueItemFromProgress(e);
- OnProgress?.Invoke(e);
- };
+ // Note: RedistributableClient progress is intentionally NOT forwarded here.
+ // Its InstallProgress never carries a Game, so it can't be matched to a queue
+ // item, and redistributables are also installed/verified during game launch —
+ // forwarding those events would drive the queue footer and taskbar with
+ // out-of-band progress when nothing is actually queued. The game-level
+ // "Installing Redistributables" status (raised by GameClient with the owning
+ // game attached) still surfaces the redist phase of a queued install.
// New task-level progress forwarding
_gameClient.OnTaskProgress += OnSdkTaskProgress;
diff --git a/LANCommander.Launcher/App.axaml.cs b/LANCommander.Launcher/App.axaml.cs
index 24f71da0..7852e3ec 100644
--- a/LANCommander.Launcher/App.axaml.cs
+++ b/LANCommander.Launcher/App.axaml.cs
@@ -80,15 +80,25 @@ public partial class App : Application
};
desktop.MainWindow = mainWindow;
- mainWindow.Show();
- // Initialize taskbar progress service with the window handle
- mainWindow.Opened += (_, _) =>
+ // Bind the taskbar progress indicator to the main window handle. This must be
+ // wired BEFORE Show(): on Windows, Show() raises Opened synchronously, so a
+ // handler attached afterwards would never fire and the progress bar would stay
+ // bound to Notify.NET's default GetConsoleWindow() target instead of the app.
+ void BindTaskbarProgress()
{
var hwnd = mainWindow.TryGetPlatformHandle()?.Handle ?? IntPtr.Zero;
if (hwnd != IntPtr.Zero)
Services.GetRequiredService().Initialize(hwnd);
- };
+ }
+
+ mainWindow.Opened += (_, _) => BindTaskbarProgress();
+
+ mainWindow.Show();
+
+ // If Opened already fired synchronously during Show(), the handler above missed
+ // it; bind now since the handle is available once the window is shown.
+ BindTaskbarProgress();
// Single-instance pipe server: forward notification-click navigations
var singleInstance = Services.GetRequiredService();
@@ -235,6 +245,10 @@ public partial class App : Application
opts.AppName = "LANCommander";
opts.AppUserModelId = "LANCommander.Launcher";
});
+ services.AddTaskbarProgress(opts =>
+ {
+ opts.DesktopFileId = "LANCommander.Launcher";
+ });
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/LANCommander.Launcher/Services/TaskbarProgressService.cs b/LANCommander.Launcher/Services/TaskbarProgressService.cs
index 5ab8720c..b5146221 100644
--- a/LANCommander.Launcher/Services/TaskbarProgressService.cs
+++ b/LANCommander.Launcher/Services/TaskbarProgressService.cs
@@ -1,112 +1,109 @@
using System;
-using System.Runtime.InteropServices;
+using LANCommander.SDK.Enums;
+using LANCommander.SDK.Services;
using Microsoft.Extensions.Logging;
+using Notify.NET.Abstractions;
namespace LANCommander.Launcher.Services;
+///
+/// Drives the OS taskbar/Dock progress indicator for the active download,
+/// backed by Notify.NET's cross-platform .
+///
public class TaskbarProgressService
{
+ private readonly ITaskbarProgressService _taskbar;
private readonly ILogger _logger;
- private ITaskbarList3? _taskbarList;
- private IntPtr _hwnd;
- public TaskbarProgressService(ILogger logger)
+ public TaskbarProgressService(ITaskbarProgressService taskbar, ILogger logger)
{
+ _taskbar = taskbar;
_logger = logger;
}
public void Initialize(IntPtr hwnd)
{
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
+ _logger.LogInformation("TaskbarProgress.Initialize: IsSupported={IsSupported}, hwnd={Hwnd}", _taskbar.IsSupported, hwnd);
- _hwnd = hwnd;
+ if (!_taskbar.IsSupported)
+ return;
try
{
- _taskbarList = (ITaskbarList3)new TaskbarListInstance();
- _taskbarList.HrInit();
+ _taskbar.SetWindow(hwnd);
}
catch (Exception ex)
{
- _logger.LogWarning(ex, "Failed to initialize ITaskbarList3");
- _taskbarList = null;
+ _logger.LogWarning(ex, "Failed to bind taskbar progress to window handle");
}
}
- public void SetProgress(float progress)
+ ///
+ /// Reflects the current install item's progress and status on the taskbar:
+ /// indeterminate phases pulse, active transfers show a value, failures turn red.
+ ///
+ public void Report(InstallProgress progress)
{
- if (_taskbarList == null || _hwnd == IntPtr.Zero) return;
+ if (!_taskbar.IsSupported)
+ return;
try
{
- const ulong total = 100_000;
- var completed = (ulong)(progress * total);
- _taskbarList.SetProgressState(_hwnd, TBPFLAG.TBPF_NORMAL);
- _taskbarList.SetProgressValue(_hwnd, completed, total);
+ _logger.LogDebug("TaskbarProgress.Report: Status={Status}, Indeterminate={Indeterminate}, Progress={Progress}", progress.Status, progress.Indeterminate, progress.Progress);
+
+ switch (progress.Status)
+ {
+ case InstallStatus.Failed:
+ _taskbar.SetState(TaskbarProgressState.Error);
+ break;
+ case InstallStatus.Canceled:
+ case InstallStatus.Complete:
+ _taskbar.SetState(TaskbarProgressState.None);
+ break;
+ default:
+ // Progress is BytesTransferred/TotalBytes and is NaN before the total is
+ // known (division by zero). Treat that—and any non-finite value—as an
+ // indeterminate pulse rather than feeding NaN to the native indicator.
+ if (progress.Indeterminate || float.IsNaN(progress.Progress) || float.IsInfinity(progress.Progress))
+ _taskbar.SetState(TaskbarProgressState.Indeterminate);
+ else
+ _taskbar.SetProgress(progress.Progress);
+ break;
+ }
}
catch (Exception ex)
{
- _logger.LogWarning(ex, "Failed to set taskbar progress");
+ _logger.LogWarning(ex, "Failed to update taskbar progress");
}
}
- public void SetIndeterminate()
+ public void SetError()
{
- if (_taskbarList == null || _hwnd == IntPtr.Zero) return;
+ if (!_taskbar.IsSupported)
+ return;
try
{
- _taskbarList.SetProgressState(_hwnd, TBPFLAG.TBPF_INDETERMINATE);
+ _taskbar.SetState(TaskbarProgressState.Error);
}
catch (Exception ex)
{
- _logger.LogWarning(ex, "Failed to set indeterminate taskbar progress");
+ _logger.LogWarning(ex, "Failed to set taskbar error state");
}
}
public void ClearProgress()
{
- if (_taskbarList == null || _hwnd == IntPtr.Zero) return;
+ if (!_taskbar.IsSupported)
+ return;
try
{
- _taskbarList.SetProgressState(_hwnd, TBPFLAG.TBPF_NOPROGRESS);
+ _taskbar.SetState(TaskbarProgressState.None);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to clear taskbar progress");
}
}
-
- // ── COM interop ──────────────────────────────────────────────────────────
-
- [Flags]
- private enum TBPFLAG
- {
- TBPF_NOPROGRESS = 0x00,
- TBPF_INDETERMINATE = 0x01,
- TBPF_NORMAL = 0x02,
- TBPF_ERROR = 0x04,
- TBPF_PAUSED = 0x08,
- }
-
- [ComImport]
- [Guid("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf")]
- [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
- private interface ITaskbarList3
- {
- void HrInit();
- void AddTab(IntPtr hwnd);
- void DeleteTab(IntPtr hwnd);
- void ActivateTab(IntPtr hwnd);
- void SetActiveAlt(IntPtr hwnd);
- void MarkFullscreenWindow(IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fullscreen);
- void SetProgressValue(IntPtr hwnd, ulong completed, ulong total);
- void SetProgressState(IntPtr hwnd, TBPFLAG state);
- }
-
- [ComImport]
- [Guid("56fdf344-fd6d-11d0-958a-006097c9a090")]
- [ClassInterface(ClassInterfaceType.None)]
- private class TaskbarListInstance { }
}
diff --git a/LANCommander.Launcher/ViewModels/DownloadQueueViewModel.cs b/LANCommander.Launcher/ViewModels/DownloadQueueViewModel.cs
index 9869b1da..a462bc69 100644
--- a/LANCommander.Launcher/ViewModels/DownloadQueueViewModel.cs
+++ b/LANCommander.Launcher/ViewModels/DownloadQueueViewModel.cs
@@ -148,10 +148,23 @@ public partial class DownloadQueueViewModel : ViewModelBase
private Task OnProgress(InstallProgress progress)
{
- _taskbarProgressService.Report(progress);
-
Dispatcher.UIThread.Post(() =>
{
+ var item = QueueItems.FirstOrDefault(i => i.Id == progress.Game?.Id);
+
+ // Progress can arrive out-of-band when nothing is queued (e.g. a game-launch
+ // sub-operation). Treat the event as a real install only if it maps to a queue
+ // item, or if some item is already actively installing (covers addon/expansion
+ // sub-installs, whose progress carries the addon — not the base queue item).
+ // Otherwise ignore it so it can't drive the footer or taskbar.
+ if (item == null && !QueueItems.Any(i => i.IsActive))
+ {
+ _taskbarProgressService.ClearProgress();
+ return;
+ }
+
+ _taskbarProgressService.Report(progress);
+
CurrentStatus = GetDisplayName(progress.Status);
CurrentProgress = progress.Progress;
CurrentTransferSpeed = progress.TransferSpeed;
@@ -159,7 +172,7 @@ public partial class DownloadQueueViewModel : ViewModelBase
// Format progress text
var bytesDownloaded = ByteSize.FromBytes(progress.BytesTransferred);
var totalBytes = ByteSize.FromBytes(progress.TotalBytes);
-
+
CurrentProgressText = $"{bytesDownloaded} / {totalBytes} ({progress.Progress:P0})";
// Format transfer speed
@@ -189,8 +202,9 @@ public partial class DownloadQueueViewModel : ViewModelBase
TimeRemainingText = string.Empty;
}
- // Update the matching queue item
- var item = QueueItems.FirstOrDefault(i => i.Id == progress.Game?.Id);
+ // Update the matching queue item. May be null for a sub-install (e.g. an addon)
+ // whose progress carries the addon rather than the active base queue item; the
+ // footer/taskbar above still reflect it.
if (item != null)
{
item.UpdateProgress(progress.Status, progress.Progress, progress.TransferSpeed, progress.BytesTransferred, progress.TotalBytes);
@@ -249,7 +263,7 @@ public partial class DownloadQueueViewModel : ViewModelBase
{
_logger.LogError("Install failed for game {GameTitle}", game.Title);
- _taskbarProgressService.ClearProgress();
+ _taskbarProgressService.SetError();
_notificationService.NotifyInstallFailed(game.Title ?? "Game", game.Id);
Dispatcher.UIThread.Post(RefreshQueue);
@@ -747,6 +761,7 @@ public partial class InstallQueueItemViewModel : ViewModelBase
{
var member = typeof(InstallStatus).GetField(status.ToString());
var display = member?.GetCustomAttribute();
+
return display?.Name ?? status.ToString();
}
}