Update Notify.NET, fix progress in taskbar
This commit is contained in:
parent
d9501aea4e
commit
07255ab552
5 changed files with 99 additions and 71 deletions
|
|
@ -13,7 +13,7 @@
|
|||
<PackageVersion Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.0.0-rc5.4" />
|
||||
<PackageVersion Include="Markdig" Version="0.44.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Configuration" Version="9.0.8" />
|
||||
<PackageVersion Include="Notify.NET" Version="1.0.2" />
|
||||
<PackageVersion Include="Notify.NET" Version="1.1.0" />
|
||||
<PackageVersion Include="Svrooij.PowerShell.DI" Version="1.3.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="AutoMapper">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<TaskbarProgressService>().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<SingleInstanceService>();
|
||||
|
|
@ -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<NotificationService>();
|
||||
services.AddSingleton<TaskbarProgressService>();
|
||||
services.AddSingleton<SingleInstanceService>();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Drives the OS taskbar/Dock progress indicator for the active download,
|
||||
/// backed by Notify.NET's cross-platform <see cref="ITaskbarProgressService"/>.
|
||||
/// </summary>
|
||||
public class TaskbarProgressService
|
||||
{
|
||||
private readonly ITaskbarProgressService _taskbar;
|
||||
private readonly ILogger<TaskbarProgressService> _logger;
|
||||
private ITaskbarList3? _taskbarList;
|
||||
private IntPtr _hwnd;
|
||||
|
||||
public TaskbarProgressService(ILogger<TaskbarProgressService> logger)
|
||||
public TaskbarProgressService(ITaskbarProgressService taskbar, ILogger<TaskbarProgressService> 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)
|
||||
/// <summary>
|
||||
/// Reflects the current install item's progress and status on the taskbar:
|
||||
/// indeterminate phases pulse, active transfers show a value, failures turn red.
|
||||
/// </summary>
|
||||
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 { }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<DisplayAttribute>();
|
||||
|
||||
return display?.Name ?? status.ToString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue