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