refactor: 新建 Platform 平台差异层,主工程 P/Invoke 全部迁出

- 新增 Platform.Abstractions(接口+NoOp+PlatformLog 日志桥)、
  Platform.Windows、Platform.MacOS、Platform.Android 四个项目
- 迁移电源管理、原生对话框、桌面层嵌入、窗口置底/区域穿透、
  DWM 互操作、图标服务、包标识查询等全部 Windows P/Invoke 实现
- 主工程保留静态工厂门面,调用点不变;DllImport 扫描为零
- 更新 WindowPassthroughServiceTests 指向新位置(20/20 通过)
This commit is contained in:
lincube
2026-07-26 22:14:46 +09:00
parent 78cbbede9d
commit 9b7a35ddd3
35 changed files with 2157 additions and 1862 deletions

View File

@@ -0,0 +1,30 @@
using Avalonia.Controls;
namespace LanMountainDesktop.Platform.Abstractions;
/// <summary>
/// 主窗口桌面层服务接口。将主窗口嵌入系统桌面图标层(仅 Windows 支持)。
/// </summary>
public interface IMainWindowDesktopLayerService
{
bool IsSupported { get; }
void EnableOrRefresh(Window window);
void Disable(Window window);
}
/// <summary>
/// 无操作实现。用于不支持桌面层嵌入的平台Linux/macOS/移动端)。
/// </summary>
public sealed class NullMainWindowDesktopLayerService : IMainWindowDesktopLayerService
{
public bool IsSupported => false;
public void EnableOrRefresh(Window window)
{
PlatformLog.Info("MainWindowDesktopLayer", "Desktop layer requested on an unsupported platform.");
}
public void Disable(Window window)
{
}
}

View File

@@ -0,0 +1,48 @@
namespace LanMountainDesktop.Platform.Abstractions;
/// <summary>
/// 电源管理服务接口。桌面平台提供关机/重启/注销/锁定/睡眠能力;
/// 移动平台不提供此能力(使用 <see cref="NullPowerManagementService"/>)。
/// </summary>
public interface IPowerManagementService
{
bool IsShutdownSupported { get; }
bool IsRestartSupported { get; }
bool IsLogoutSupported { get; }
bool IsLockSupported { get; }
bool IsSleepSupported { get; }
Task ShutdownAsync();
Task RestartAsync();
Task LogoutAsync();
Task LockAsync();
Task SleepAsync();
void ShowNativePowerUI(PowerAction action);
}
public enum PowerAction
{
Shutdown,
Restart
}
/// <summary>
/// 无操作实现。用于不支持电源管理的平台(如移动端)。
/// </summary>
public sealed class NullPowerManagementService : IPowerManagementService
{
public bool IsShutdownSupported => false;
public bool IsRestartSupported => false;
public bool IsLogoutSupported => false;
public bool IsLockSupported => false;
public bool IsSleepSupported => false;
public Task ShutdownAsync() => Task.CompletedTask;
public Task RestartAsync() => Task.CompletedTask;
public Task LogoutAsync() => Task.CompletedTask;
public Task LockAsync() => Task.CompletedTask;
public Task SleepAsync() => Task.CompletedTask;
public void ShowNativePowerUI(PowerAction action) { }
}

View File

@@ -0,0 +1,68 @@
using Avalonia;
using Avalonia.Controls;
namespace LanMountainDesktop.Platform.Abstractions;
/// <summary>
/// 窗口置底服务接口。使组件窗口保持在桌面层Z 序最底)。
/// </summary>
public interface IWindowBottomMostService
{
void SetupBottomMost(Window window);
void SendToBottom(Window window);
PixelPoint GetScreenPosition(Window window);
bool SetScreenPosition(Window window, PixelPoint position, bool queueOnFailure = false);
bool IsBottomMostSupported { get; }
}
/// <summary>
/// 窗口交互区域定义。区域外的点击穿透到桌面。
/// </summary>
public readonly record struct WindowInteractiveRegion(
Rect Bounds,
double CornerRadius,
Matrix? ClientToRegionTransform = null,
Rect? ClientClipBounds = null,
double ClientClipCornerRadius = 0d);
/// <summary>
/// 区域点击穿透服务接口。
/// </summary>
public interface IRegionPassthroughService
{
void SetInteractiveRegions(Window window, IReadOnlyList<WindowInteractiveRegion> interactiveRegions);
void ClearInteractiveRegions(Window window);
bool IsRegionPassthroughSupported { get; }
}
/// <summary>
/// 无操作实现:非 Windows 平台窗口置底不可用。
/// </summary>
public sealed class NullWindowBottomMostService : IWindowBottomMostService
{
public bool IsBottomMostSupported => false;
public void SetupBottomMost(Window window) { }
public void SendToBottom(Window window) { }
public PixelPoint GetScreenPosition(Window window) => window.Position;
public bool SetScreenPosition(Window window, PixelPoint position, bool queueOnFailure = false)
{
window.Position = position;
return true;
}
}
/// <summary>
/// 无操作实现:非 Windows 平台区域穿透不可用。
/// </summary>
public sealed class NullRegionPassthroughService : IRegionPassthroughService
{
public bool IsRegionPassthroughSupported => false;
public void SetInteractiveRegions(Window window, IReadOnlyList<WindowInteractiveRegion> interactiveRegions) { }
public void ClearInteractiveRegions(Window window) { }
}

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,35 @@
namespace LanMountainDesktop.Platform.Abstractions;
/// <summary>
/// 平台层日志桥。平台实现项目不引用宿主,
/// 宿主在启动时通过 <see cref="SetSink"/> 接入自己的日志系统AppLogger
/// 未接入时日志静默丢弃。
/// </summary>
public static class PlatformLog
{
private static IPlatformLogSink? _sink;
public static void SetSink(IPlatformLogSink sink)
{
ArgumentNullException.ThrowIfNull(sink);
_sink = sink;
}
public static void Info(string category, string message) => _sink?.Info(category, message);
public static void Warn(string category, string message, Exception? exception = null) =>
_sink?.Warn(category, message, exception);
public static void Error(string category, string message, Exception? exception = null) =>
_sink?.Error(category, message, exception);
}
/// <summary>
/// 平台层日志输出目标。
/// </summary>
public interface IPlatformLogSink
{
void Info(string category, string message);
void Warn(string category, string message, Exception? exception = null);
void Error(string category, string message, Exception? exception = null);
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-android</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<SupportedOSPlatformVersion>24</SupportedOSPlatformVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\LanMountainDesktop.Platform.Abstractions\LanMountainDesktop.Platform.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\LanMountainDesktop.Platform.Abstractions\LanMountainDesktop.Platform.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Drawing.Common" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
@@ -6,10 +6,10 @@ using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace LanMountainDesktop.Services;
namespace LanMountainDesktop.Platform.MacOS;
[SupportedOSPlatform("macos")]
internal static class MacIconService
public static class MacIconService
{
private const int IconSize = 256;

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\LanMountainDesktop.Platform.Abstractions\LanMountainDesktop.Platform.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="System.Drawing.Common" />
</ItemGroup>
</Project>

View File

@@ -10,10 +10,10 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Linq;
namespace LanMountainDesktop.Services;
namespace LanMountainDesktop.Platform.Windows;
[SupportedOSPlatform("windows")]
internal static class UwpManifestIconResolver
public static class UwpManifestIconResolver
{
private const int ErrorSuccess = 0;
private const int ErrorInsufficientBuffer = 122;

View File

@@ -0,0 +1,45 @@
using System.Runtime.InteropServices;
namespace LanMountainDesktop.Platform.Windows;
/// <summary>
/// DWM桌面窗口管理器互操作封装。
/// </summary>
public static class WindowsDwmInterop
{
public const int WindowAttributeBorderColor = 34;
public const uint ColorNone = 0xFFFFFFFE;
/// <summary>
/// 移除窗口原生边框颜色Windows 11 22000+)。
/// 失败静默忽略DWM 属性为尽力而为语义)。
/// </summary>
public static void TryDisableWindowBorder(IntPtr windowHandle)
{
if (windowHandle == IntPtr.Zero || !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000))
{
return;
}
try
{
var borderColor = ColorNone;
_ = DwmSetWindowAttribute(
windowHandle,
WindowAttributeBorderColor,
ref borderColor,
sizeof(uint));
}
catch
{
// DWM attributes are best-effort and unavailable on older/unsupported Windows builds.
}
}
[DllImport("dwmapi.dll")]
private static extern int DwmSetWindowAttribute(
IntPtr windowHandle,
int attribute,
ref uint attributeValue,
int attributeSize);
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
@@ -9,10 +9,10 @@ using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
namespace LanMountainDesktop.Services;
namespace LanMountainDesktop.Platform.Windows;
[SupportedOSPlatform("windows")]
internal static class WindowsIconService
public static class WindowsIconService
{
private const int HighResolutionIconSize = 256;
private const int MaxShellPath = 1024;

View File

@@ -0,0 +1,238 @@
using System.Runtime.InteropServices;
using Avalonia.Controls;
using LanMountainDesktop.Platform.Abstractions;
namespace LanMountainDesktop.Platform.Windows;
/// <summary>
/// Windows 桌面层实现将窗口设为桌面图标宿主SHELLDLL_DefView的子窗口。
/// 自 LanMountainDesktop.Services.MainWindowDesktopLayerService 迁移,行为不变。
/// </summary>
public sealed class WindowsMainWindowDesktopLayerService : IMainWindowDesktopLayerService
{
private const int GWL_STYLE = -16;
private const int GWL_EXSTYLE = -20;
private const long WS_CHILD = 0x40000000L;
private const long WS_POPUP = 0x80000000L;
private const long WS_CAPTION = 0x00C00000L;
private const long WS_THICKFRAME = 0x00040000L;
private const long WS_MINIMIZEBOX = 0x00020000L;
private const long WS_MAXIMIZEBOX = 0x00010000L;
private const long WS_SYSMENU = 0x00080000L;
private const uint SWP_NOSIZE = 0x0001;
private const uint SWP_NOMOVE = 0x0002;
private const uint SWP_NOACTIVATE = 0x0010;
private const uint SWP_SHOWWINDOW = 0x0040;
private const uint SWP_FRAMECHANGED = 0x0020;
private static readonly IntPtr HWND_TOP = IntPtr.Zero;
private static readonly IntPtr HWND_BOTTOM = new(1);
private readonly object _gate = new();
private readonly Dictionary<IntPtr, WindowRestoreState> _restoreStates = [];
public bool IsSupported => true;
public void EnableOrRefresh(Window window)
{
ArgumentNullException.ThrowIfNull(window);
var handle = GetWindowHandle(window);
if (handle == IntPtr.Zero)
{
window.Opened -= OnDeferredOpened;
window.Opened += OnDeferredOpened;
return;
}
EnableOrRefresh(handle);
}
public void Disable(Window window)
{
ArgumentNullException.ThrowIfNull(window);
window.Opened -= OnDeferredOpened;
var handle = GetWindowHandle(window);
if (handle == IntPtr.Zero)
{
return;
}
WindowRestoreState? restoreState;
lock (_gate)
{
if (!_restoreStates.Remove(handle, out restoreState))
{
return;
}
}
try
{
_ = SetParent(handle, restoreState.Parent);
SetWindowLongPtr(handle, GWL_STYLE, restoreState.Style);
SetWindowLongPtr(handle, GWL_EXSTYLE, restoreState.ExStyle);
_ = SetWindowPos(
handle,
HWND_TOP,
0,
0,
0,
0,
SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_SHOWWINDOW);
PlatformLog.Info("MainWindowDesktopLayer", $"Disabled desktop layer. Window={handle}.");
}
catch (Exception ex)
{
PlatformLog.Warn("MainWindowDesktopLayer", $"Failed to disable desktop layer. Window={handle}.", ex);
}
}
private void OnDeferredOpened(object? sender, EventArgs e)
{
if (sender is not Window window)
{
return;
}
window.Opened -= OnDeferredOpened;
EnableOrRefresh(window);
}
private void EnableOrRefresh(IntPtr handle)
{
if (handle == IntPtr.Zero || !IsWindow(handle))
{
return;
}
SaveRestoreStateIfNeeded(handle);
var desktopHost = ResolveDesktopIconHost();
if (desktopHost != IntPtr.Zero && IsWindow(desktopHost))
{
ApplyDesktopChildStyle(handle);
if (GetParent(handle) != desktopHost)
{
_ = SetParent(handle, desktopHost);
}
_ = SetWindowPos(
handle,
HWND_TOP,
0,
0,
0,
0,
SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_SHOWWINDOW);
PlatformLog.Info("MainWindowDesktopLayer", $"Enabled desktop layer. Window={handle}; Host={desktopHost}.");
return;
}
_ = SetWindowPos(handle, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
PlatformLog.Warn("MainWindowDesktopLayer", $"Desktop icon host not found. Falling back to HWND_BOTTOM. Window={handle}.");
}
private void SaveRestoreStateIfNeeded(IntPtr handle)
{
lock (_gate)
{
if (_restoreStates.ContainsKey(handle))
{
return;
}
_restoreStates[handle] = new WindowRestoreState(
GetParent(handle),
GetWindowLongPtr(handle, GWL_STYLE),
GetWindowLongPtr(handle, GWL_EXSTYLE));
}
}
private static void ApplyDesktopChildStyle(IntPtr handle)
{
var style = GetWindowLongPtr(handle, GWL_STYLE).ToInt64();
style |= WS_CHILD;
style &= ~(WS_POPUP | WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU);
SetWindowLongPtr(handle, GWL_STYLE, new IntPtr(style));
}
private static IntPtr ResolveDesktopIconHost()
{
var topLevelWindows = new List<IntPtr>();
EnumWindows((handle, _) =>
{
topLevelWindows.Add(handle);
return true;
}, IntPtr.Zero);
foreach (var topLevelWindow in topLevelWindows)
{
var worker = FindWindowEx(topLevelWindow, IntPtr.Zero, "WorkerW", null);
if (worker == IntPtr.Zero)
{
continue;
}
var defView = FindWindowEx(worker, IntPtr.Zero, "SHELLDLL_DefView", null);
if (defView != IntPtr.Zero)
{
return defView;
}
}
foreach (var topLevelWindow in topLevelWindows)
{
var defView = FindWindowEx(topLevelWindow, IntPtr.Zero, "SHELLDLL_DefView", null);
if (defView != IntPtr.Zero)
{
return defView;
}
}
return IntPtr.Zero;
}
private static IntPtr GetWindowHandle(Window window)
{
try
{
return window.TryGetPlatformHandle()?.Handle ?? IntPtr.Zero;
}
catch
{
return IntPtr.Zero;
}
}
private sealed record WindowRestoreState(IntPtr Parent, IntPtr Style, IntPtr ExStyle);
private delegate bool EnumWindowsProc(IntPtr handle, IntPtr lParam);
[DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")]
private static extern IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex);
[DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
[DllImport("user32.dll")]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint flags);
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
[DllImport("user32.dll")]
private static extern IntPtr GetParent(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool IsWindow(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr FindWindowEx(IntPtr hParent, IntPtr hChildAfter, string? lpszClass, string? lpszWindow);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
}

View File

@@ -0,0 +1,30 @@
using System.Runtime.InteropServices;
namespace LanMountainDesktop.Platform.Windows;
/// <summary>
/// Windows 原生消息框MessageBoxW封装。
/// 供宿主在 UI 框架尚不可用(启动早期)时显示诊断信息。
/// </summary>
public static class WindowsNativeDialogs
{
public const uint Ok = 0x00000000;
public const uint IconInformation = 0x00000040;
public const uint IconWarning = 0x00000030;
/// <summary>
/// 显示原生消息框。仅在 Windows 上有效,其他平台为空操作。
/// </summary>
public static void Show(string caption, string message, uint type)
{
if (!OperatingSystem.IsWindows())
{
return;
}
_ = MessageBoxW(IntPtr.Zero, message, caption, type);
}
[DllImport("user32.dll", EntryPoint = "MessageBoxW", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int MessageBoxW(IntPtr hWnd, string text, string caption, uint type);
}

View File

@@ -0,0 +1,43 @@
using System.Runtime.InteropServices;
using System.Text;
namespace LanMountainDesktop.Platform.Windows;
/// <summary>
/// Windows 包标识查询MSIX/UWP 打包检测)。
/// </summary>
public static class WindowsPackageIdentity
{
private const int AppmodelErrorNoPackage = 15700;
/// <summary>
/// 检测当前进程是否具有包标识(以 MSIX 打包运行)。
/// 非 Windows 平台返回 false。
/// </summary>
public static bool HasPackageIdentity()
{
if (!OperatingSystem.IsWindows())
{
return false;
}
var length = 0;
var hr = GetCurrentPackageFullName(ref length, null);
if (hr == AppmodelErrorNoPackage)
{
return false;
}
if (length <= 0)
{
return hr == 0;
}
var builder = new StringBuilder(length);
hr = GetCurrentPackageFullName(ref length, builder);
return hr == 0;
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern int GetCurrentPackageFullName(ref int packageFullNameLength, StringBuilder? packageFullName);
}

View File

@@ -0,0 +1,106 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using LanMountainDesktop.Platform.Abstractions;
namespace LanMountainDesktop.Platform.Windows;
/// <summary>
/// Windows 电源管理实现P/Invoke user32/powrprof
/// 自 LanMountainDesktop.Services.PowerManagementService 迁移,行为不变。
/// </summary>
public sealed class WindowsPowerManagementService : IPowerManagementService
{
public bool IsShutdownSupported => true;
public bool IsRestartSupported => true;
public bool IsLogoutSupported => true;
public bool IsLockSupported => true;
public bool IsSleepSupported => true;
public async Task ShutdownAsync()
{
await Task.Run(() =>
{
Process.Start(new ProcessStartInfo
{
FileName = "shutdown",
Arguments = "/s /t 0",
UseShellExecute = true,
WindowStyle = ProcessWindowStyle.Hidden
});
});
}
public async Task RestartAsync()
{
await Task.Run(() =>
{
Process.Start(new ProcessStartInfo
{
FileName = "shutdown",
Arguments = "/r /t 0",
UseShellExecute = true,
WindowStyle = ProcessWindowStyle.Hidden
});
});
}
public async Task LogoutAsync()
{
await Task.Run(() =>
{
ExitWindowsEx(0, 0);
});
}
public async Task LockAsync()
{
await Task.Run(() =>
{
LockWorkStation();
});
}
public async Task SleepAsync()
{
await Task.Run(() =>
{
SetSuspendState(false, false, false);
});
}
public void ShowNativePowerUI(PowerAction action)
{
// SlideToShutDown.exe 只支持关机,不支持重启
// 重启操作应该通过 RestartAsync() 使用 shutdown /r 命令
if (action != PowerAction.Shutdown)
return;
var slideToShutDownPath = Environment.ExpandEnvironmentVariables(@"%windir%\System32\SlideToShutDown.exe");
if (File.Exists(slideToShutDownPath))
{
Process.Start(new ProcessStartInfo
{
FileName = slideToShutDownPath,
UseShellExecute = true
});
return;
}
// 回退到标准关机命令
Process.Start(new ProcessStartInfo
{
FileName = "shutdown",
Arguments = "/s /t 5 /c \"LanMountainDesktop: Shutting down...\"",
UseShellExecute = true
});
}
[DllImport("user32.dll", SetLastError = true)]
private static extern bool ExitWindowsEx(uint uFlags, uint dwReason);
[DllImport("user32.dll")]
private static extern void LockWorkStation();
[DllImport("powrprof.dll", SetLastError = true)]
private static extern bool SetSuspendState(bool hibernate, bool forceCritical, bool disableWakeEvent);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
using System.Reflection;
using System.Reflection;
using Avalonia;
using LanMountainDesktop.Services;
using LanMountainDesktop.Platform.Abstractions;
using LanMountainDesktop.Platform.Windows;
using Xunit;
namespace LanMountainDesktop.Tests;
@@ -52,7 +53,7 @@ public sealed class WindowPassthroughServiceTests
[Fact]
public void NativeIntegration_UsesAvaloniaCallbacksWithoutManualWndProcSubclassing()
{
var source = ReadRepositoryFile("LanMountainDesktop", "Services", "WindowPassthroughService.cs");
var source = ReadRepositoryFile("LanMountainDesktop.Platform.Windows", "WindowsWindowPassthroughServices.cs");
Assert.Contains("Win32Properties.AddWindowStylesCallback", source);
Assert.Contains("Win32Properties.AddWndProcHookCallback", source);
@@ -158,7 +159,7 @@ public sealed class WindowPassthroughServiceTests
Assert.False(Invoke<bool>("OriginalWindowUsesParentClientCoordinates", WsPopup));
Assert.False(Invoke<bool>("OriginalWindowUsesParentClientCoordinates", 0U));
var source = ReadRepositoryFile("LanMountainDesktop", "Services", "WindowPassthroughService.cs");
var source = ReadRepositoryFile("LanMountainDesktop.Platform.Windows", "WindowsWindowPassthroughServices.cs");
Assert.Contains("GWLP_HWNDPARENT", source);
Assert.Contains("restore the owner through GWLP_HWNDPARENT", source);
}
@@ -213,7 +214,7 @@ public sealed class WindowPassthroughServiceTests
[Fact]
public void HitTestCoordinates_AreResolvedFromTheLiveWindowState()
{
var source = ReadRepositoryFile("LanMountainDesktop", "Services", "WindowPassthroughService.cs");
var source = ReadRepositoryFile("LanMountainDesktop.Platform.Windows", "WindowsWindowPassthroughServices.cs");
Assert.Contains("ScreenToClient(hWnd, ref screenPoint)", source);
Assert.Contains("GetDpiForWindow(handle)", source);
@@ -229,9 +230,8 @@ public sealed class WindowPassthroughServiceTests
private static T Invoke<T>(string methodName, params object[] arguments)
{
var serviceType = typeof(IWindowBottomMostService).Assembly.GetType(
"LanMountainDesktop.Services.WindowsWindowBottomMostService",
throwOnError: true)!;
// Windows 实现已迁移至 Platform.Windows跨平台重构类型现为 public。
var serviceType = typeof(WindowsWindowBottomMostService);
var method = serviceType.GetMethod(
methodName,
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);

View File

@@ -1,4 +1,8 @@
<Solution>
<Project Path="LanMountainDesktop.Platform.Abstractions/LanMountainDesktop.Platform.Abstractions.csproj" />
<Project Path="LanMountainDesktop.Platform.Windows/LanMountainDesktop.Platform.Windows.csproj" />
<Project Path="LanMountainDesktop.Platform.Android/LanMountainDesktop.Platform.Android.csproj" />
<Project Path="LanMountainDesktop.Platform.MacOS/LanMountainDesktop.Platform.MacOS.csproj" />
<Project Path="LanMountainDesktop.Host.Abstractions/LanMountainDesktop.Host.Abstractions.csproj" />
<Project Path="LanMountainDesktop.Shared.Contracts/LanMountainDesktop.Shared.Contracts.csproj" />
<Project Path="LanMountainDesktop.Shared.IPC/LanMountainDesktop.Shared.IPC.csproj" />

View File

@@ -18,6 +18,7 @@ using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.DesktopHost;
using LanMountainDesktop.Models;
using LanMountainDesktop.Platform.Abstractions;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.ExternalIpc;

View File

@@ -30,6 +30,9 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LanMountainDesktop.Platform.Abstractions\LanMountainDesktop.Platform.Abstractions.csproj" />
<ProjectReference Include="..\LanMountainDesktop.Platform.Windows\LanMountainDesktop.Platform.Windows.csproj" />
<ProjectReference Include="..\LanMountainDesktop.Platform.MacOS\LanMountainDesktop.Platform.MacOS.csproj" />
<ProjectReference Include="..\LanMountainDesktop.Host.Abstractions\LanMountainDesktop.Host.Abstractions.csproj" />
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
<ProjectReference Include="..\LanMountainDesktop.Shared.IPC\LanMountainDesktop.Shared.IPC.csproj" />

View File

@@ -10,6 +10,7 @@ using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.DesktopEditing;
using LanMountainDesktop.Host.Abstractions;
using LanMountainDesktop.Models;
using LanMountainDesktop.Platform.Abstractions;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Views;

View File

@@ -1,17 +1,14 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using LanMountainDesktop.Platform.Abstractions;
using LanMountainDesktop.Platform.Windows;
namespace LanMountainDesktop.Services;
public interface IMainWindowDesktopLayerService
{
bool IsSupported { get; }
void EnableOrRefresh(Window window);
void Disable(Window window);
}
/// <summary>
/// 桌面层服务工厂。接口与实现位于平台层:
/// 接口 <see cref="IMainWindowDesktopLayerService"/> 在 Platform.Abstractions
/// Windows 实现P/Invoke在 Platform.Windows。
/// </summary>
public static class MainWindowDesktopLayerServiceFactory
{
private static readonly object Gate = new();
@@ -21,252 +18,15 @@ public static class MainWindowDesktopLayerServiceFactory
{
lock (Gate)
{
return _instance ??= OperatingSystem.IsWindows()
if (_instance is null)
{
PlatformLogBridge.Install();
_instance = OperatingSystem.IsWindows()
? new WindowsMainWindowDesktopLayerService()
: new NullMainWindowDesktopLayerService();
}
}
}
internal sealed class WindowsMainWindowDesktopLayerService : IMainWindowDesktopLayerService
{
private const int GWL_STYLE = -16;
private const int GWL_EXSTYLE = -20;
private const long WS_CHILD = 0x40000000L;
private const long WS_POPUP = 0x80000000L;
private const long WS_CAPTION = 0x00C00000L;
private const long WS_THICKFRAME = 0x00040000L;
private const long WS_MINIMIZEBOX = 0x00020000L;
private const long WS_MAXIMIZEBOX = 0x00010000L;
private const long WS_SYSMENU = 0x00080000L;
private const uint SWP_NOSIZE = 0x0001;
private const uint SWP_NOMOVE = 0x0002;
private const uint SWP_NOACTIVATE = 0x0010;
private const uint SWP_SHOWWINDOW = 0x0040;
private const uint SWP_FRAMECHANGED = 0x0020;
private static readonly IntPtr HWND_TOP = IntPtr.Zero;
private static readonly IntPtr HWND_BOTTOM = new(1);
private readonly object _gate = new();
private readonly Dictionary<IntPtr, WindowRestoreState> _restoreStates = [];
public bool IsSupported => true;
public void EnableOrRefresh(Window window)
{
ArgumentNullException.ThrowIfNull(window);
var handle = GetWindowHandle(window);
if (handle == IntPtr.Zero)
{
window.Opened -= OnDeferredOpened;
window.Opened += OnDeferredOpened;
return;
}
EnableOrRefresh(handle);
}
public void Disable(Window window)
{
ArgumentNullException.ThrowIfNull(window);
window.Opened -= OnDeferredOpened;
var handle = GetWindowHandle(window);
if (handle == IntPtr.Zero)
{
return;
}
WindowRestoreState? restoreState;
lock (_gate)
{
if (!_restoreStates.Remove(handle, out restoreState))
{
return;
}
}
try
{
_ = SetParent(handle, restoreState.Parent);
SetWindowLongPtr(handle, GWL_STYLE, restoreState.Style);
SetWindowLongPtr(handle, GWL_EXSTYLE, restoreState.ExStyle);
_ = SetWindowPos(
handle,
HWND_TOP,
0,
0,
0,
0,
SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_SHOWWINDOW);
AppLogger.Info("MainWindowDesktopLayer", $"Disabled desktop layer. Window={handle}.");
}
catch (Exception ex)
{
AppLogger.Warn("MainWindowDesktopLayer", $"Failed to disable desktop layer. Window={handle}.", ex);
}
}
private void OnDeferredOpened(object? sender, EventArgs e)
{
if (sender is not Window window)
{
return;
}
window.Opened -= OnDeferredOpened;
EnableOrRefresh(window);
}
private void EnableOrRefresh(IntPtr handle)
{
if (handle == IntPtr.Zero || !IsWindow(handle))
{
return;
}
SaveRestoreStateIfNeeded(handle);
var desktopHost = ResolveDesktopIconHost();
if (desktopHost != IntPtr.Zero && IsWindow(desktopHost))
{
ApplyDesktopChildStyle(handle);
if (GetParent(handle) != desktopHost)
{
_ = SetParent(handle, desktopHost);
}
_ = SetWindowPos(
handle,
HWND_TOP,
0,
0,
0,
0,
SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_SHOWWINDOW);
AppLogger.Info("MainWindowDesktopLayer", $"Enabled desktop layer. Window={handle}; Host={desktopHost}.");
return;
}
_ = SetWindowPos(handle, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
AppLogger.Warn("MainWindowDesktopLayer", $"Desktop icon host not found. Falling back to HWND_BOTTOM. Window={handle}.");
}
private void SaveRestoreStateIfNeeded(IntPtr handle)
{
lock (_gate)
{
if (_restoreStates.ContainsKey(handle))
{
return;
}
_restoreStates[handle] = new WindowRestoreState(
GetParent(handle),
GetWindowLongPtr(handle, GWL_STYLE),
GetWindowLongPtr(handle, GWL_EXSTYLE));
}
}
private static void ApplyDesktopChildStyle(IntPtr handle)
{
var style = GetWindowLongPtr(handle, GWL_STYLE).ToInt64();
style |= WS_CHILD;
style &= ~(WS_POPUP | WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU);
SetWindowLongPtr(handle, GWL_STYLE, new IntPtr(style));
}
private static IntPtr ResolveDesktopIconHost()
{
var topLevelWindows = new List<IntPtr>();
EnumWindows((handle, _) =>
{
topLevelWindows.Add(handle);
return true;
}, IntPtr.Zero);
foreach (var topLevelWindow in topLevelWindows)
{
var worker = FindWindowEx(topLevelWindow, IntPtr.Zero, "WorkerW", null);
if (worker == IntPtr.Zero)
{
continue;
}
var defView = FindWindowEx(worker, IntPtr.Zero, "SHELLDLL_DefView", null);
if (defView != IntPtr.Zero)
{
return defView;
}
}
foreach (var topLevelWindow in topLevelWindows)
{
var defView = FindWindowEx(topLevelWindow, IntPtr.Zero, "SHELLDLL_DefView", null);
if (defView != IntPtr.Zero)
{
return defView;
}
}
return IntPtr.Zero;
}
private static IntPtr GetWindowHandle(Window window)
{
try
{
return window.TryGetPlatformHandle()?.Handle ?? IntPtr.Zero;
}
catch
{
return IntPtr.Zero;
}
}
private sealed record WindowRestoreState(IntPtr Parent, IntPtr Style, IntPtr ExStyle);
private delegate bool EnumWindowsProc(IntPtr handle, IntPtr lParam);
[DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")]
private static extern IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex);
[DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
[DllImport("user32.dll")]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint flags);
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
[DllImport("user32.dll")]
private static extern IntPtr GetParent(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool IsWindow(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr FindWindowEx(IntPtr hParent, IntPtr hChildAfter, string? lpszClass, string? lpszWindow);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
}
internal sealed class NullMainWindowDesktopLayerService : IMainWindowDesktopLayerService
{
public bool IsSupported => false;
public void EnableOrRefresh(Window window)
{
AppLogger.Info("MainWindowDesktopLayer", "Desktop layer requested on an unsupported platform.");
}
public void Disable(Window window)
{
return _instance;
}
}
}

View File

@@ -0,0 +1,35 @@
using System;
using LanMountainDesktop.Platform.Abstractions;
namespace LanMountainDesktop.Services;
/// <summary>
/// 将平台层日志PlatformLog接入宿主 AppLogger。
/// 在应用启动早期调用 <see cref="Install"/> 一次。
/// </summary>
internal static class PlatformLogBridge
{
private static bool _installed;
public static void Install()
{
if (_installed)
{
return;
}
_installed = true;
PlatformLog.SetSink(new AppLoggerSink());
}
private sealed class AppLoggerSink : IPlatformLogSink
{
public void Info(string category, string message) => AppLogger.Info(category, message);
public void Warn(string category, string message, Exception? exception = null) =>
AppLogger.Warn(category, message, exception);
public void Error(string category, string message, Exception? exception = null) =>
AppLogger.Error(category, message, exception);
}
}

View File

@@ -1,33 +1,17 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using LanMountainDesktop.Platform.Abstractions;
using LanMountainDesktop.Platform.Windows;
namespace LanMountainDesktop.Services;
public interface IPowerManagementService
{
bool IsShutdownSupported { get; }
bool IsRestartSupported { get; }
bool IsLogoutSupported { get; }
bool IsLockSupported { get; }
bool IsSleepSupported { get; }
Task ShutdownAsync();
Task RestartAsync();
Task LogoutAsync();
Task LockAsync();
Task SleepAsync();
void ShowNativePowerUI(PowerAction action);
}
public enum PowerAction
{
Shutdown,
Restart
}
/// <summary>
/// 电源管理服务工厂。接口与实现位于平台层:
/// 接口 <see cref="IPowerManagementService"/> 在 Platform.Abstractions
/// Windows 实现P/Invoke在 Platform.Windows。
/// Linux 实现基于 systemctl/loginctl 命令行,无平台专属 API保留在宿主内。
/// </summary>
public static class PowerManagementServiceFactory
{
private static IPowerManagementService? _instance;
@@ -51,103 +35,6 @@ public static class PowerManagementServiceFactory
}
}
internal sealed class WindowsPowerManagementService : IPowerManagementService
{
public bool IsShutdownSupported => true;
public bool IsRestartSupported => true;
public bool IsLogoutSupported => true;
public bool IsLockSupported => true;
public bool IsSleepSupported => true;
public async Task ShutdownAsync()
{
await Task.Run(() =>
{
Process.Start(new ProcessStartInfo
{
FileName = "shutdown",
Arguments = "/s /t 0",
UseShellExecute = true,
WindowStyle = ProcessWindowStyle.Hidden
});
});
}
public async Task RestartAsync()
{
await Task.Run(() =>
{
Process.Start(new ProcessStartInfo
{
FileName = "shutdown",
Arguments = "/r /t 0",
UseShellExecute = true,
WindowStyle = ProcessWindowStyle.Hidden
});
});
}
public async Task LogoutAsync()
{
await Task.Run(() =>
{
ExitWindowsEx(0, 0);
});
}
public async Task LockAsync()
{
await Task.Run(() =>
{
LockWorkStation();
});
}
public async Task SleepAsync()
{
await Task.Run(() =>
{
SetSuspendState(false, false, false);
});
}
public void ShowNativePowerUI(PowerAction action)
{
// SlideToShutDown.exe 只支持关机,不支持重启
// 重启操作应该通过 RestartAsync() 使用 shutdown /r 命令
if (action != PowerAction.Shutdown)
return;
var slideToShutDownPath = Environment.ExpandEnvironmentVariables(@"%windir%\System32\SlideToShutDown.exe");
if (System.IO.File.Exists(slideToShutDownPath))
{
Process.Start(new ProcessStartInfo
{
FileName = slideToShutDownPath,
UseShellExecute = true
});
return;
}
// 回退到标准关机命令
Process.Start(new ProcessStartInfo
{
FileName = "shutdown",
Arguments = "/s /t 5 /c \"LanMountainDesktop: Shutting down...\"",
UseShellExecute = true
});
}
[DllImport("user32.dll", SetLastError = true)]
private static extern bool ExitWindowsEx(uint uFlags, uint dwReason);
[DllImport("user32.dll")]
private static extern void LockWorkStation();
[DllImport("powrprof.dll", SetLastError = true)]
private static extern bool SetSuspendState(bool hibernate, bool forceCritical, bool disableWakeEvent);
}
internal sealed class LinuxPowerManagementService : IPowerManagementService
{
public bool IsShutdownSupported => true;
@@ -226,20 +113,3 @@ internal sealed class LinuxPowerManagementService : IPowerManagementService
});
}
}
internal sealed class NullPowerManagementService : IPowerManagementService
{
public bool IsShutdownSupported => false;
public bool IsRestartSupported => false;
public bool IsLogoutSupported => false;
public bool IsLockSupported => false;
public bool IsSleepSupported => false;
public Task ShutdownAsync() => Task.CompletedTask;
public Task RestartAsync() => Task.CompletedTask;
public Task LogoutAsync() => Task.CompletedTask;
public Task LockAsync() => Task.CompletedTask;
public Task SleepAsync() => Task.CompletedTask;
public void ShowNativePowerUI(PowerAction action) { }
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,32 @@
using System;
using System.Runtime.InteropServices;
using LanMountainDesktop.Platform.Windows;
namespace LanMountainDesktop.Services;
/// <summary>
/// 原生对话框门面。P/Invoke 实现位于 Platform.WindowsWindowsNativeDialogs
/// </summary>
internal static class WindowsNativeDialogService
{
private const uint Ok = 0x00000000;
private const uint IconInformation = 0x00000040;
private const uint IconWarning = 0x00000030;
public static void ShowInformation(string caption, string message)
{
Show(caption, message, Ok | IconInformation, "NativeDialog");
Show(caption, message, WindowsNativeDialogs.Ok | WindowsNativeDialogs.IconInformation, "NativeDialog");
}
public static void ShowWarning(string caption, string message)
{
Show(caption, message, Ok | IconWarning, "StartupDiagnostics");
Show(caption, message, WindowsNativeDialogs.Ok | WindowsNativeDialogs.IconWarning, "StartupDiagnostics");
}
private static void Show(string caption, string message, uint type, string logCategory)
{
if (!OperatingSystem.IsWindows())
{
return;
}
try
{
_ = MessageBoxW(IntPtr.Zero, message, caption, type);
WindowsNativeDialogs.Show(caption, message, type);
}
catch (Exception ex)
{
AppLogger.Warn(logCategory, "Failed to show native dialog.", ex);
}
}
[DllImport("user32.dll", EntryPoint = "MessageBoxW", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int MessageBoxW(IntPtr hWnd, string text, string caption, uint type);
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -8,6 +8,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using LanMountainDesktop.Models;
using LanMountainDesktop.Platform.Windows;
namespace LanMountainDesktop.Services;
@@ -320,26 +321,7 @@ internal sealed class WindowsNotificationListener : IPlatformNotificationListene
private static bool HasPackageIdentity()
{
if (!OperatingSystem.IsWindows())
{
return false;
}
var length = 0;
var hr = GetCurrentPackageFullName(ref length, null);
if (hr == AppmodelErrorNoPackage)
{
return false;
}
if (length <= 0)
{
return hr == 0;
}
var builder = new StringBuilder(length);
hr = GetCurrentPackageFullName(ref length, builder);
return hr == 0;
return WindowsPackageIdentity.HasPackageIdentity();
}
private static string TryReadPackageFamilyName(object? appInfo)
@@ -497,8 +479,4 @@ internal sealed class WindowsNotificationListener : IPlatformNotificationListene
_cts.Dispose();
}
private const int AppmodelErrorNoPackage = 15700;
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern int GetCurrentPackageFullName(ref int packageFullNameLength, StringBuilder? packageFullName);
}

View File

@@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using LanMountainDesktop.Models;
using LanMountainDesktop.Platform.Windows;
namespace LanMountainDesktop.Services;

View File

@@ -14,6 +14,8 @@ using Avalonia.Platform;
using FluentIcons.Avalonia;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Platform.MacOS;
using LanMountainDesktop.Platform.Windows;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;

View File

@@ -11,6 +11,8 @@ using Avalonia.Media.Imaging;
using FluentIcons.Avalonia;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Platform.MacOS;
using LanMountainDesktop.Platform.Windows;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;

View File

@@ -7,6 +7,7 @@ using Avalonia.Media;
using Avalonia.Threading;
using LanMountainDesktop.DesktopEditing;
using LanMountainDesktop.Models;
using LanMountainDesktop.Platform.Abstractions;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;

View File

@@ -1,11 +1,11 @@
using System;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input;
using Avalonia.Interactivity;
using LanMountainDesktop.Appearance;
using LanMountainDesktop.Platform.Windows;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Settings.Core;
@@ -14,9 +14,6 @@ namespace LanMountainDesktop.Views;
public partial class FusedDesktopComponentLibraryWindow : Window
{
private const int DwmWindowAttributeBorderColor = 34;
private const uint DwmColorNone = 0xFFFFFFFE;
private static readonly LocalizationService LocalizationService = new();
public FusedDesktopComponentLibraryWindow()
@@ -123,31 +120,9 @@ public partial class FusedDesktopComponentLibraryWindow : Window
}
private void TryDisableNativeWindowBorder()
{
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000))
{
return;
}
try
{
var handle = TryGetPlatformHandle()?.Handle ?? IntPtr.Zero;
if (handle == IntPtr.Zero)
{
return;
}
var borderColor = DwmColorNone;
_ = DwmSetWindowAttribute(
handle,
DwmWindowAttributeBorderColor,
ref borderColor,
sizeof(uint));
}
catch
{
// DWM attributes are best-effort and unavailable on older/unsupported Windows builds.
}
WindowsDwmInterop.TryDisableWindowBorder(handle);
}
protected override void OnClosed(EventArgs e)
@@ -163,11 +138,4 @@ public partial class FusedDesktopComponentLibraryWindow : Window
var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow;
mainWindow?.UnregisterFusedLibraryWindow(this);
}
[DllImport("dwmapi.dll")]
private static extern int DwmSetWindowAttribute(
IntPtr windowHandle,
int attribute,
ref uint attributeValue,
int attributeSize);
}

View File

@@ -18,6 +18,7 @@ using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.DesktopEditing;
using LanMountainDesktop.Host.Abstractions;
using LanMountainDesktop.Models;
using LanMountainDesktop.Platform.Abstractions;
using LanMountainDesktop.Services;
using LanMountainDesktop.Settings.Core;
using LanMountainDesktop.Theme;

View File

@@ -16,6 +16,7 @@ using Avalonia.Threading;
using Avalonia.VisualTree;
using FluentAvalonia.UI.Controls;
using LanMountainDesktop.Models;
using LanMountainDesktop.Platform.Windows;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;