mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-21 16:14:28 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00747f33b2 | ||
|
|
2c48b7b846 |
@@ -1,11 +0,0 @@
|
|||||||
# External IPC Public API Checklist
|
|
||||||
|
|
||||||
- [x] Host can expose strong-typed public IPC services.
|
|
||||||
- [x] External .NET client can connect and call built-in services.
|
|
||||||
- [x] Host publishes launcher startup and loading-state notifications through routed notify.
|
|
||||||
- [x] Launcher consumes routed notify instead of the old primary custom named-pipe path.
|
|
||||||
- [x] Plugin SDK exposes public IPC contribution primitives.
|
|
||||||
- [x] Plugin runtime can discover and register plugin public IPC services.
|
|
||||||
- [x] Public catalog includes built-in and plugin-contributed services.
|
|
||||||
- [x] `catalog.changed` is emitted when new services are added after startup.
|
|
||||||
- [ ] Add example external client sample.
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# External IPC Public API Spec
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Provide a single `dotnetCampus.Ipc` based external integration layer for:
|
|
||||||
|
|
||||||
- Host public APIs
|
|
||||||
- Launcher/OOBE startup progress and loading-state notifications
|
|
||||||
- plugin-contributed public services and live event push
|
|
||||||
|
|
||||||
## Delivered
|
|
||||||
|
|
||||||
- `LanMountainDesktop.Shared.IPC` project
|
|
||||||
- `[IpcPublic]` based built-in public contracts
|
|
||||||
- `PublicIpcHostService` and `LanMountainDesktopIpcClient`
|
|
||||||
- Launcher migrated to Host public IPC notifications
|
|
||||||
- Plugin SDK public IPC contribution API
|
|
||||||
- Host runtime integration for plugin public IPC services
|
|
||||||
|
|
||||||
## Out of Scope
|
|
||||||
|
|
||||||
- plugin process isolation
|
|
||||||
- non-.NET strong-typed public IPC clients
|
|
||||||
- live plugin public service removal without restart
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# External IPC Public API Tasks
|
|
||||||
|
|
||||||
- [x] Add `LanMountainDesktop.Shared.IPC`
|
|
||||||
- [x] Expose built-in `[IpcPublic]` services
|
|
||||||
- [x] Add routed notify constants and public IPC client/host wrappers
|
|
||||||
- [x] Start Host public IPC during app startup
|
|
||||||
- [x] Move Launcher startup progress consumption to the new IPC base
|
|
||||||
- [x] Add plugin public IPC registration/contributor SDK
|
|
||||||
- [x] Register plugin-contributed public services into Host catalog
|
|
||||||
- [x] Add integration tests for strong-typed public service access and plugin registration descriptors
|
|
||||||
- [ ] Expand built-in public service surface beyond the first minimal set
|
|
||||||
- [ ] Add non-.NET bridge guidance and samples
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
- [x] 从桌面、托盘、IPC、组件库进入设置时,都会落到同一个设置窗口
|
|
||||||
- [x] 设置已打开时再次触发设置入口,只会聚焦已有窗口,不会切换成关闭
|
|
||||||
- [x] 设置窗口始终拥有独立任务栏图标,不受“桌面主窗口在任务栏显示图标”开关影响
|
|
||||||
- [x] 点击“回到 Windows”后,只隐藏或最小化桌面主窗口,设置窗口保持可见
|
|
||||||
- [x] 启用滑入滑出动画后,只有主窗口参与动画,设置窗口不参与
|
|
||||||
- [x] 点击设置窗口关闭按钮后会真实关闭;再次打开时创建新的居中窗口
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
# 独立设置窗口 Spec
|
|
||||||
|
|
||||||
## Why
|
|
||||||
|
|
||||||
- 当前设置窗口仍然带有桌面壳的 owner / anchor 语义,点击“回到 Windows”或触发桌面动画时,容易被一起隐藏或重新定位。
|
|
||||||
- 产品新增了“在任务栏显示图标”和“启用滑入滑出动画”设置,需要明确边界:它们只影响桌面主窗口,不影响设置窗口。
|
|
||||||
- 桌面底栏、托盘菜单、IPC、组件库等入口应当始终打开同一个独立设置窗口,而不是切换成附属浮窗或开关行为。
|
|
||||||
|
|
||||||
## What Changes
|
|
||||||
|
|
||||||
- 将设置窗口改为独立顶层窗口,始终使用自己的任务栏按钮和图标。
|
|
||||||
- `SettingsWindowService.Open` 改为幂等的 open-or-focus;重复打开只聚焦已有窗口,并在提供目标页时切换到对应页面。
|
|
||||||
- 移除 `Owner`、锚点定位和 `Toggle` 语义;首次打开按参考屏幕居中,关闭为真实关闭。
|
|
||||||
- 桌面壳的“回到 Windows”、最小化到托盘/任务栏、滑入滑出动画,只影响 `MainWindow`,不会影响设置窗口。
|
|
||||||
- 统一桌面、托盘、IPC、组件库等设置入口,全部走 `OpenIndependentSettingsModule`。
|
|
||||||
- 设置页文案明确“在任务栏显示图标”只控制桌面主窗口;设置窗口始终保留独立任务栏图标。
|
|
||||||
|
|
||||||
## Impact
|
|
||||||
|
|
||||||
- Affected code:
|
|
||||||
- `LanMountainDesktop/Services/Settings/SettingsWindowService.cs`
|
|
||||||
- `LanMountainDesktop/App.axaml.cs`
|
|
||||||
- `LanMountainDesktop/Views/MainWindow.axaml.cs`
|
|
||||||
- `LanMountainDesktop/Views/MainWindow.ComponentSystem.cs`
|
|
||||||
- `LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml.cs`
|
|
||||||
- `LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml`
|
|
||||||
- Affected behavior:
|
|
||||||
- 设置窗口生命周期
|
|
||||||
- 设置入口一致性
|
|
||||||
- 任务栏图标与桌面壳显示边界
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ADDED Requirements
|
|
||||||
|
|
||||||
### Requirement: 设置窗口为独立顶层窗口
|
|
||||||
|
|
||||||
系统 SHALL 将设置窗口作为独立顶层窗口显示,而不是作为桌面主窗口的附属子窗。
|
|
||||||
|
|
||||||
#### Scenario: 设置窗口拥有独立任务栏图标
|
|
||||||
- **WHEN** 用户打开设置窗口
|
|
||||||
- **THEN** 设置窗口使用独立顶层窗口方式显示
|
|
||||||
- **AND THEN** 设置窗口在任务栏中保留自己的独立按钮和图标
|
|
||||||
- **AND THEN** “在任务栏显示图标”开关不会影响设置窗口的任务栏按钮
|
|
||||||
|
|
||||||
### Requirement: 设置入口统一为 open-or-focus
|
|
||||||
|
|
||||||
系统 SHALL 让所有设置入口打开或聚焦同一个设置窗口实例。
|
|
||||||
|
|
||||||
#### Scenario: 已打开时重复触发设置入口
|
|
||||||
- **WHEN** 设置窗口已经打开,用户再次从桌面、托盘或 IPC 触发打开设置
|
|
||||||
- **THEN** 系统只聚焦现有设置窗口
|
|
||||||
- **AND THEN** 如果请求包含目标页,则导航到目标页
|
|
||||||
- **AND THEN** 不会把已打开的设置窗口当作开关关闭
|
|
||||||
|
|
||||||
### Requirement: 设置窗口不参与桌面壳可见性切换
|
|
||||||
|
|
||||||
系统 SHALL 让桌面壳的隐藏、最小化和进出场动画只作用于主窗口。
|
|
||||||
|
|
||||||
#### Scenario: 回到 Windows 时设置窗口保持可见
|
|
||||||
- **WHEN** 主窗口执行“回到 Windows”并隐藏到托盘或最小化到任务栏
|
|
||||||
- **THEN** 设置窗口保持当前可见状态
|
|
||||||
- **AND THEN** 设置窗口不会跟随主窗口一起隐藏、最小化或重定位
|
|
||||||
|
|
||||||
#### Scenario: 桌面滑入滑出动画不作用于设置窗口
|
|
||||||
- **WHEN** 启用了滑入滑出动画并触发主窗口退场或入场
|
|
||||||
- **THEN** 只有主窗口参与动画
|
|
||||||
- **AND THEN** 设置窗口不会消失,也不会跟随主窗口做进出场动画
|
|
||||||
|
|
||||||
### Requirement: 关闭设置窗口时真实销毁实例
|
|
||||||
|
|
||||||
系统 SHALL 在用户关闭设置窗口时真实关闭该窗口实例。
|
|
||||||
|
|
||||||
#### Scenario: 关闭后再次打开
|
|
||||||
- **WHEN** 用户点击设置窗口右上角关闭按钮
|
|
||||||
- **THEN** 当前设置窗口实例被关闭并销毁
|
|
||||||
- **AND THEN** 下次再次打开设置时创建新的设置窗口实例
|
|
||||||
- **AND THEN** 新窗口按参考屏幕居中显示
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Tasks
|
|
||||||
|
|
||||||
- [x] Task 1: 简化设置窗口打开契约
|
|
||||||
- [x] 将 `SettingsWindowOpenRequest` 从 owner / anchor 语义改为目标页 + 参考屏幕语义
|
|
||||||
- [x] 移除 `ISettingsWindowService.Toggle`
|
|
||||||
|
|
||||||
- [x] Task 2: 重做设置窗口服务行为
|
|
||||||
- [x] 设置窗口始终使用 `Show()` 打开
|
|
||||||
- [x] 设置窗口始终 `ShowInTaskbar = true`
|
|
||||||
- [x] 已打开时只聚焦并在需要时切页
|
|
||||||
- [x] 关闭后销毁实例,下次打开重新创建并居中
|
|
||||||
|
|
||||||
- [x] Task 3: 统一设置入口并解耦桌面壳
|
|
||||||
- [x] 桌面底栏设置按钮改为 open-or-focus
|
|
||||||
- [x] 组件库入口改为复用 `OpenIndependentSettingsModule`
|
|
||||||
- [x] 移除 `MainWindow` 上的设置窗口锚点逻辑
|
|
||||||
|
|
||||||
- [x] Task 4: 明确产品边界
|
|
||||||
- [x] 调整“在任务栏显示图标”文案,限定为桌面主窗口
|
|
||||||
- [x] 新增独立设置窗口 feature spec
|
|
||||||
- [x] 在窗口过渡动画 spec 中补充“设置窗口不参与动画”
|
|
||||||
|
|
||||||
- [x] Task 5: 验证
|
|
||||||
- [x] 运行 `dotnet build LanMountainDesktop.slnx -c Debug`
|
|
||||||
- [x] 运行与新 helper 相关的测试
|
|
||||||
@@ -113,15 +113,6 @@
|
|||||||
- **AND THEN** 过渡时长使用 `FluttermotionToken.Duration.Page`(320ms)和 `FluttermotionToken.Duration.Intro`(400ms)
|
- **AND THEN** 过渡时长使用 `FluttermotionToken.Duration.Page`(320ms)和 `FluttermotionToken.Duration.Intro`(400ms)
|
||||||
- **AND THEN** 缓动函数使用 `0.05,0.75,0.10,1.00`(DecelerateBezier)
|
- **AND THEN** 缓动函数使用 `0.05,0.75,0.10,1.00`(DecelerateBezier)
|
||||||
|
|
||||||
### Requirement: 设置窗口不参与桌面壳过渡动画
|
|
||||||
|
|
||||||
系统 SHALL 将桌面壳进出场动画限制在主窗口范围内,不影响独立设置窗口。
|
|
||||||
|
|
||||||
#### Scenario: 设置窗口在桌面动画期间保持独立
|
|
||||||
- **WHEN** 主窗口执行滑入、滑出、最小化或恢复动画
|
|
||||||
- **THEN** 设置窗口不参与该动画
|
|
||||||
- **AND THEN** 设置窗口不会跟随主窗口一起隐藏、最小化或重定位
|
|
||||||
|
|
||||||
## MODIFIED Requirements
|
## MODIFIED Requirements
|
||||||
|
|
||||||
### Requirement: OnMinimizeClick 行为
|
### Requirement: OnMinimizeClick 行为
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- 只引用 Shared.Contracts(IPC 协议) -->
|
<!-- 只引用 Shared.Contracts(IPC 协议) -->
|
||||||
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||||
<ProjectReference Include="..\LanMountainDesktop.Shared.IPC\LanMountainDesktop.Shared.IPC.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LanMountainDesktop.Launcher.Models;
|
using LanMountainDesktop.Launcher.Models;
|
||||||
|
using LanMountainDesktop.Launcher.Services.Ipc;
|
||||||
using LanMountainDesktop.Launcher.Views;
|
using LanMountainDesktop.Launcher.Views;
|
||||||
using LanMountainDesktop.Shared.Contracts.Launcher;
|
using LanMountainDesktop.Shared.Contracts.Launcher;
|
||||||
using LanMountainDesktop.Shared.IPC;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Launcher.Services;
|
namespace LanMountainDesktop.Launcher.Services;
|
||||||
|
|
||||||
@@ -83,8 +83,7 @@ internal sealed class LauncherFlowCoordinator
|
|||||||
var lastStageMessage = "launcher-started";
|
var lastStageMessage = "launcher-started";
|
||||||
|
|
||||||
var loadingState = new LoadingStateMessage();
|
var loadingState = new LoadingStateMessage();
|
||||||
using var ipcClient = new LanMountainDesktopIpcClient();
|
using var ipcServer = new LauncherIpcServer(message =>
|
||||||
ipcClient.RegisterNotifyHandler<StartupProgressMessage>(IpcRoutedNotifyIds.LauncherStartupProgress, message =>
|
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Post(() =>
|
Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
@@ -122,21 +121,7 @@ internal sealed class LauncherFlowCoordinator
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ipcClient.RegisterNotifyHandler<LoadingStateMessage>(IpcRoutedNotifyIds.LauncherLoadingState, message =>
|
ipcServer.Start();
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
loadingState = message;
|
|
||||||
loadingDetailsWindow?.UpdateLoadingState(loadingState);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.Error("IPC loading-state callback failed.", ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -189,12 +174,6 @@ internal sealed class LauncherFlowCoordinator
|
|||||||
details: MergeDetails(launcherContextDetails, launchOutcome.Details));
|
details: MergeDetails(launcherContextDetails, launchOutcome.Details));
|
||||||
}
|
}
|
||||||
|
|
||||||
var connected = await TryConnectToPublicIpcAsync(ipcClient, TimeSpan.FromSeconds(5)).ConfigureAwait(false);
|
|
||||||
if (!connected)
|
|
||||||
{
|
|
||||||
Logger.Warn("Timed out waiting for host public IPC. Launcher will continue without live startup notifications.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var processExitTask = launchOutcome.Process.WaitForExitAsync();
|
var processExitTask = launchOutcome.Process.WaitForExitAsync();
|
||||||
var completedTask = await Task.WhenAny(
|
var completedTask = await Task.WhenAny(
|
||||||
visibilityTcs.Task,
|
visibilityTcs.Task,
|
||||||
@@ -921,21 +900,6 @@ internal sealed class LauncherFlowCoordinator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<bool> TryConnectToPublicIpcAsync(
|
|
||||||
LanMountainDesktopIpcClient ipcClient,
|
|
||||||
TimeSpan timeout)
|
|
||||||
{
|
|
||||||
var connectTask = ipcClient.ConnectAsync();
|
|
||||||
var completedTask = await Task.WhenAny(connectTask, Task.Delay(timeout)).ConfigureAwait(false);
|
|
||||||
if (completedTask != connectTask)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await connectTask.ConfigureAwait(false);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum HostStartMode
|
private enum HostStartMode
|
||||||
{
|
{
|
||||||
ShellExecute,
|
ShellExecute,
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
namespace LanMountainDesktop.PluginSdk;
|
|
||||||
|
|
||||||
public interface IPluginPublicIpcBuilder
|
|
||||||
{
|
|
||||||
IPluginPublicIpcBuilder AddService<TContract>(
|
|
||||||
string? objectId = null,
|
|
||||||
IEnumerable<string>? notifyIds = null)
|
|
||||||
where TContract : class;
|
|
||||||
|
|
||||||
IPluginPublicIpcBuilder AddService(
|
|
||||||
Type contractType,
|
|
||||||
object implementation,
|
|
||||||
string? objectId = null,
|
|
||||||
IEnumerable<string>? notifyIds = null);
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace LanMountainDesktop.PluginSdk;
|
|
||||||
|
|
||||||
public interface IPluginPublicIpcContributor
|
|
||||||
{
|
|
||||||
void ConfigurePublicIpc(IPluginPublicIpcBuilder builder);
|
|
||||||
}
|
|
||||||
@@ -25,10 +25,8 @@
|
|||||||
<PackageReference Include="FluentIcons.Avalonia.Fluent" Version="2.0.320" ExcludeAssets="runtime" />
|
<PackageReference Include="FluentIcons.Avalonia.Fluent" Version="2.0.320" ExcludeAssets="runtime" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
||||||
<PackageReference Include="dotnetCampus.Ipc" Version="2.0.0-alpha434" />
|
|
||||||
<ProjectReference Include="..\LanMountainDesktop.PluginIsolation.Contracts\LanMountainDesktop.PluginIsolation.Contracts.csproj" />
|
<ProjectReference Include="..\LanMountainDesktop.PluginIsolation.Contracts\LanMountainDesktop.PluginIsolation.Contracts.csproj" />
|
||||||
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||||
<ProjectReference Include="..\LanMountainDesktop.Shared.IPC\LanMountainDesktop.Shared.IPC.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace LanMountainDesktop.PluginSdk;
|
|
||||||
|
|
||||||
public sealed record PluginPublicIpcServiceDescriptor(
|
|
||||||
Type ContractType,
|
|
||||||
object Implementation,
|
|
||||||
string? ObjectId,
|
|
||||||
string[] NotifyIds);
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace LanMountainDesktop.PluginSdk;
|
|
||||||
|
|
||||||
public sealed record PluginPublicIpcServiceRegistration(
|
|
||||||
Type ContractType,
|
|
||||||
string? ObjectId,
|
|
||||||
string[] NotifyIds);
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using dotnetCampus.Ipc.CompilerServices.Attributes;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace LanMountainDesktop.PluginSdk;
|
namespace LanMountainDesktop.PluginSdk;
|
||||||
@@ -113,55 +112,6 @@ public static class PluginServiceCollectionExtensions
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddPluginPublicIpc<TContract, TImplementation>(
|
|
||||||
this IServiceCollection services,
|
|
||||||
string? objectId = null,
|
|
||||||
params string[] notifyIds)
|
|
||||||
where TContract : class
|
|
||||||
where TImplementation : class, TContract
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(services);
|
|
||||||
EnsurePublicIpcContract(typeof(TContract));
|
|
||||||
EnsureSingletonRegistration<TContract, TImplementation>(services);
|
|
||||||
|
|
||||||
if (!services.Any(descriptor =>
|
|
||||||
descriptor.ServiceType == typeof(PluginPublicIpcServiceRegistration) &&
|
|
||||||
descriptor.ImplementationInstance is PluginPublicIpcServiceRegistration existing &&
|
|
||||||
existing.ContractType == typeof(TContract) &&
|
|
||||||
string.Equals(existing.ObjectId, objectId, StringComparison.Ordinal)))
|
|
||||||
{
|
|
||||||
services.AddSingleton(new PluginPublicIpcServiceRegistration(
|
|
||||||
typeof(TContract),
|
|
||||||
objectId,
|
|
||||||
notifyIds ?? []));
|
|
||||||
}
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IServiceCollection AddPluginPublicIpcContributor<TContributor>(this IServiceCollection services)
|
|
||||||
where TContributor : class, IPluginPublicIpcContributor
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(services);
|
|
||||||
services.AddSingleton<IPluginPublicIpcContributor, TContributor>();
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void EnsurePublicIpcContract(Type contractType)
|
|
||||||
{
|
|
||||||
if (!contractType.IsInterface)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Public IPC contract '{contractType.FullName}' must be an interface.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Attribute.IsDefined(contractType, typeof(IpcPublicAttribute), inherit: false))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Public IPC contract '{contractType.FullName}' must be marked with '{nameof(IpcPublicAttribute)}'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void EnsureSingletonRegistration<TContract, TImplementation>(IServiceCollection services)
|
private static void EnsureSingletonRegistration<TContract, TImplementation>(IServiceCollection services)
|
||||||
where TContract : class
|
where TContract : class
|
||||||
where TImplementation : class, TContract
|
where TImplementation : class, TContract
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
using dotnetCampus.Ipc.CompilerServices.Attributes;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Shared.IPC.Abstractions.Services;
|
|
||||||
|
|
||||||
[IpcPublic(IgnoresIpcException = true)]
|
|
||||||
public interface IPublicAppInfoService
|
|
||||||
{
|
|
||||||
PublicAppInfoSnapshot GetAppInfo();
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
using dotnetCampus.Ipc.CompilerServices.Attributes;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Shared.IPC.Abstractions.Services;
|
|
||||||
|
|
||||||
[IpcPublic(IgnoresIpcException = true)]
|
|
||||||
public interface IPublicPluginCatalogService
|
|
||||||
{
|
|
||||||
PublicIpcCatalogSnapshot GetCatalog();
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using dotnetCampus.Ipc.CompilerServices.Attributes;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Shared.IPC.Abstractions.Services;
|
|
||||||
|
|
||||||
[IpcPublic(IgnoresIpcException = true)]
|
|
||||||
public interface IPublicShellControlService
|
|
||||||
{
|
|
||||||
Task<bool> ActivateMainWindowAsync();
|
|
||||||
|
|
||||||
Task<bool> OpenSettingsAsync(string? pageTag = null);
|
|
||||||
|
|
||||||
Task<bool> RestartAsync();
|
|
||||||
|
|
||||||
Task<bool> ExitAsync();
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace LanMountainDesktop.Shared.IPC.DependencyInjection;
|
|
||||||
|
|
||||||
public sealed record PublicIpcServiceRegistration(
|
|
||||||
Type ContractType,
|
|
||||||
Func<IServiceProvider, object> ImplementationFactory,
|
|
||||||
string? ObjectId,
|
|
||||||
string? PluginId,
|
|
||||||
string[] NotifyIds);
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Shared.IPC.DependencyInjection;
|
|
||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
|
||||||
{
|
|
||||||
public static IServiceCollection AddLanMountainDesktopIpcHost(
|
|
||||||
this IServiceCollection services,
|
|
||||||
string? pipeName = null)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(services);
|
|
||||||
|
|
||||||
services.AddSingleton(provider =>
|
|
||||||
{
|
|
||||||
var host = new PublicIpcHostService(pipeName ?? IpcConstants.DefaultPipeName);
|
|
||||||
foreach (var registration in provider.GetServices<PublicIpcServiceRegistration>())
|
|
||||||
{
|
|
||||||
var implementation = registration.ImplementationFactory(provider);
|
|
||||||
host.RegisterPublicService(
|
|
||||||
registration.ContractType,
|
|
||||||
implementation,
|
|
||||||
registration.ObjectId,
|
|
||||||
registration.PluginId,
|
|
||||||
registration.NotifyIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
host.Start();
|
|
||||||
return host;
|
|
||||||
});
|
|
||||||
|
|
||||||
services.AddSingleton<IExternalIpcNotificationPublisher>(provider =>
|
|
||||||
provider.GetRequiredService<PublicIpcHostService>());
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IServiceCollection AddPublicIpcService<TContract, TImplementation>(
|
|
||||||
this IServiceCollection services,
|
|
||||||
string? objectId = null,
|
|
||||||
string? pluginId = null,
|
|
||||||
params string[] notifyIds)
|
|
||||||
where TContract : class
|
|
||||||
where TImplementation : class, TContract
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(services);
|
|
||||||
|
|
||||||
EnsureSingletonRegistration<TContract, TImplementation>(services);
|
|
||||||
|
|
||||||
if (!services.Any(descriptor =>
|
|
||||||
descriptor.ServiceType == typeof(PublicIpcServiceRegistration) &&
|
|
||||||
descriptor.ImplementationInstance is PublicIpcServiceRegistration existing &&
|
|
||||||
existing.ContractType == typeof(TContract) &&
|
|
||||||
string.Equals(existing.ObjectId, objectId, StringComparison.Ordinal)))
|
|
||||||
{
|
|
||||||
services.AddSingleton(new PublicIpcServiceRegistration(
|
|
||||||
typeof(TContract),
|
|
||||||
provider => provider.GetRequiredService<TContract>(),
|
|
||||||
objectId,
|
|
||||||
pluginId,
|
|
||||||
notifyIds ?? []));
|
|
||||||
}
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void EnsureSingletonRegistration<TContract, TImplementation>(IServiceCollection services)
|
|
||||||
where TContract : class
|
|
||||||
where TImplementation : class, TContract
|
|
||||||
{
|
|
||||||
var descriptor = services.LastOrDefault(item => item.ServiceType == typeof(TContract));
|
|
||||||
if (descriptor is null)
|
|
||||||
{
|
|
||||||
services.AddSingleton<TContract, TImplementation>();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (descriptor.Lifetime != ServiceLifetime.Singleton)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Public IPC contract '{typeof(TContract).FullName}' must be registered as Singleton.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace LanMountainDesktop.Shared.IPC;
|
|
||||||
|
|
||||||
public interface IExternalIpcNotificationPublisher
|
|
||||||
{
|
|
||||||
Task NotifyAsync<TPayload>(string notifyId, TPayload payload, CancellationToken cancellationToken = default)
|
|
||||||
where TPayload : class;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
namespace LanMountainDesktop.Shared.IPC;
|
|
||||||
|
|
||||||
public static class IpcConstants
|
|
||||||
{
|
|
||||||
public const string DefaultPipeName = "LanMountainDesktop.IPC.v1.Server";
|
|
||||||
|
|
||||||
public const string ProtocolVersion = "external-ipc-public-api.v1";
|
|
||||||
|
|
||||||
public static class Routes
|
|
||||||
{
|
|
||||||
public const string SessionGetInfo = "lanmountain.session.get-info";
|
|
||||||
public const string CatalogGet = "lanmountain.catalog.get";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace LanMountainDesktop.Shared.IPC;
|
|
||||||
|
|
||||||
public static class IpcRoutedNotifyIds
|
|
||||||
{
|
|
||||||
public const string CatalogChanged = "lanmountain.catalog.changed";
|
|
||||||
public const string LauncherStartupProgress = "lanmountain.launcher.startup-progress";
|
|
||||||
public const string LauncherLoadingState = "lanmountain.launcher.loading-state";
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Version>1.0.0</Version>
|
|
||||||
<PackageId>LanMountainDesktop.Shared.IPC</PackageId>
|
|
||||||
<IsPackable>true</IsPackable>
|
|
||||||
<Authors>LanMountainDesktop</Authors>
|
|
||||||
<Description>Public IPC abstractions and host/client infrastructure for LanMountainDesktop, backed by dotnetCampus.Ipc.</Description>
|
|
||||||
<PackageTags>LanMountainDesktop;IPC;dotnetCampus.Ipc;Integration</PackageTags>
|
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
|
||||||
<RepositoryUrl>https://github.com/wwiinnddyy/LanMountainDesktop</RepositoryUrl>
|
|
||||||
<RepositoryType>git</RepositoryType>
|
|
||||||
<PackageLicenseExpression>LGPL-3.0-or-later</PackageLicenseExpression>
|
|
||||||
<Copyright>Copyright (c) LanMountainDesktop Contributors</Copyright>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="dotnetCampus.Ipc" Version="2.0.0-alpha434" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
|
||||||
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
using dotnetCampus.Ipc.CompilerServices.GeneratedProxies;
|
|
||||||
using dotnetCampus.Ipc.IpcRouteds.DirectRouteds;
|
|
||||||
using dotnetCampus.Ipc.Pipes;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Shared.IPC;
|
|
||||||
|
|
||||||
public sealed class LanMountainDesktopIpcClient : IDisposable
|
|
||||||
{
|
|
||||||
private bool _started;
|
|
||||||
|
|
||||||
public LanMountainDesktopIpcClient(string? clientPipeName = null)
|
|
||||||
{
|
|
||||||
Provider = string.IsNullOrWhiteSpace(clientPipeName)
|
|
||||||
? new IpcProvider()
|
|
||||||
: new IpcProvider(clientPipeName);
|
|
||||||
RoutedProvider = new JsonIpcDirectRoutedProvider(Provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IpcProvider Provider { get; }
|
|
||||||
|
|
||||||
public JsonIpcDirectRoutedProvider RoutedProvider { get; }
|
|
||||||
|
|
||||||
public PeerProxy? Peer { get; private set; }
|
|
||||||
|
|
||||||
public bool IsConnected => Peer is not null && Peer.IsConnectedFinished;
|
|
||||||
|
|
||||||
public async Task ConnectAsync(string pipeName = IpcConstants.DefaultPipeName)
|
|
||||||
{
|
|
||||||
EnsureStarted();
|
|
||||||
Peer = await Provider.GetAndConnectToPeerAsync(pipeName).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterNotifyHandler<TPayload>(string notifyId, Action<TPayload> handler)
|
|
||||||
where TPayload : class
|
|
||||||
{
|
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(notifyId);
|
|
||||||
ArgumentNullException.ThrowIfNull(handler);
|
|
||||||
RoutedProvider.AddNotifyHandler(notifyId, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterNotifyHandler<TPayload>(string notifyId, Func<TPayload, Task> handler)
|
|
||||||
where TPayload : class
|
|
||||||
{
|
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(notifyId);
|
|
||||||
ArgumentNullException.ThrowIfNull(handler);
|
|
||||||
RoutedProvider.AddNotifyHandler(notifyId, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
public TContract CreateProxy<TContract>(string? objectId = null)
|
|
||||||
where TContract : class
|
|
||||||
{
|
|
||||||
var peer = Peer ?? throw new InvalidOperationException("IPC client is not connected.");
|
|
||||||
return Provider.CreateIpcProxy<TContract>(peer, objectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<PublicIpcCatalogSnapshot?> GetCatalogAsync()
|
|
||||||
{
|
|
||||||
var client = await GetRoutedClientAsync().ConfigureAwait(false);
|
|
||||||
return await client.GetResponseAsync<PublicIpcCatalogSnapshot>(IpcConstants.Routes.CatalogGet)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<PublicIpcSessionInfo?> GetSessionInfoAsync()
|
|
||||||
{
|
|
||||||
var client = await GetRoutedClientAsync().ConfigureAwait(false);
|
|
||||||
return await client.GetResponseAsync<PublicIpcSessionInfo>(IpcConstants.Routes.SessionGetInfo)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<JsonIpcDirectRoutedClientProxy> GetRoutedClientAsync()
|
|
||||||
{
|
|
||||||
if (Peer is null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("IPC client is not connected.");
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.CompletedTask;
|
|
||||||
return new JsonIpcDirectRoutedClientProxy(Peer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EnsureStarted()
|
|
||||||
{
|
|
||||||
if (_started)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
RoutedProvider.StartServer();
|
|
||||||
_started = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Provider.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace LanMountainDesktop.Shared.IPC;
|
|
||||||
|
|
||||||
public sealed record PublicAppInfoSnapshot(
|
|
||||||
string ApplicationName,
|
|
||||||
string Version,
|
|
||||||
string Codename,
|
|
||||||
string PipeName,
|
|
||||||
int ProcessId,
|
|
||||||
DateTimeOffset StartedAt);
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace LanMountainDesktop.Shared.IPC;
|
|
||||||
|
|
||||||
public sealed record PublicIpcCatalogSnapshot(
|
|
||||||
PublicIpcServiceDescriptor[] Services,
|
|
||||||
PublicPluginDescriptor[] Plugins,
|
|
||||||
DateTimeOffset Timestamp);
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using dotnetCampus.Ipc.Context;
|
|
||||||
using dotnetCampus.Ipc.CompilerServices.GeneratedProxies;
|
|
||||||
using dotnetCampus.Ipc.IpcRouteds.DirectRouteds;
|
|
||||||
using dotnetCampus.Ipc.Pipes;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Shared.IPC;
|
|
||||||
|
|
||||||
public sealed class PublicIpcHostService : IDisposable, IExternalIpcNotificationPublisher
|
|
||||||
{
|
|
||||||
private static readonly MethodInfo CreateIpcJointMethod = typeof(GeneratedIpcFactory)
|
|
||||||
.GetMethods(BindingFlags.Public | BindingFlags.Static)
|
|
||||||
.Single(method =>
|
|
||||||
method.Name == nameof(GeneratedIpcFactory.CreateIpcJoint) &&
|
|
||||||
method.IsGenericMethodDefinition &&
|
|
||||||
method.GetParameters().Length == 3);
|
|
||||||
|
|
||||||
private readonly Dictionary<(Type ContractType, string ObjectId), PublicServiceEntry> _services = new();
|
|
||||||
private readonly ConcurrentDictionary<string, PeerProxy> _connectedPeers = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
private readonly object _gate = new();
|
|
||||||
private bool _started;
|
|
||||||
|
|
||||||
public PublicIpcHostService(string pipeName = IpcConstants.DefaultPipeName)
|
|
||||||
{
|
|
||||||
PipeName = pipeName;
|
|
||||||
StartedAt = DateTimeOffset.UtcNow;
|
|
||||||
Provider = new IpcProvider(pipeName);
|
|
||||||
RoutedProvider = new JsonIpcDirectRoutedProvider(Provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string PipeName { get; }
|
|
||||||
|
|
||||||
public DateTimeOffset StartedAt { get; }
|
|
||||||
|
|
||||||
public IpcProvider Provider { get; }
|
|
||||||
|
|
||||||
public JsonIpcDirectRoutedProvider RoutedProvider { get; }
|
|
||||||
|
|
||||||
public Func<IReadOnlyList<PublicPluginDescriptor>> PluginDescriptorProvider { get; set; } =
|
|
||||||
static () => Array.Empty<PublicPluginDescriptor>();
|
|
||||||
|
|
||||||
public void Start()
|
|
||||||
{
|
|
||||||
if (_started)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
RoutedProvider.AddRequestHandler(IpcConstants.Routes.SessionGetInfo, () => BuildSessionInfo());
|
|
||||||
RoutedProvider.AddRequestHandler(IpcConstants.Routes.CatalogGet, () => GetCatalogSnapshot());
|
|
||||||
Provider.PeerConnected += OnPeerConnected;
|
|
||||||
RoutedProvider.StartServer();
|
|
||||||
_started = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterPublicService<TContract>(
|
|
||||||
TContract implementation,
|
|
||||||
string? objectId = null,
|
|
||||||
string? pluginId = null,
|
|
||||||
params string[] notifyIds)
|
|
||||||
where TContract : class
|
|
||||||
{
|
|
||||||
RegisterPublicService(typeof(TContract), implementation, objectId, pluginId, notifyIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterPublicService(
|
|
||||||
Type contractType,
|
|
||||||
object implementation,
|
|
||||||
string? objectId = null,
|
|
||||||
string? pluginId = null,
|
|
||||||
IEnumerable<string>? notifyIds = null)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(contractType);
|
|
||||||
ArgumentNullException.ThrowIfNull(implementation);
|
|
||||||
|
|
||||||
var normalizedObjectId = objectId ?? string.Empty;
|
|
||||||
var normalizedNotifyIds = notifyIds?
|
|
||||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToArray() ?? [];
|
|
||||||
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_services.ContainsKey((contractType, normalizedObjectId)))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Public IPC contract '{contractType.FullName}' with object id '{normalizedObjectId}' is already registered.");
|
|
||||||
}
|
|
||||||
|
|
||||||
CreateIpcJointMethod
|
|
||||||
.MakeGenericMethod(contractType)
|
|
||||||
.Invoke(null, [Provider, implementation, string.IsNullOrEmpty(normalizedObjectId) ? null : normalizedObjectId]);
|
|
||||||
|
|
||||||
_services[(contractType, normalizedObjectId)] = new PublicServiceEntry(
|
|
||||||
contractType,
|
|
||||||
implementation,
|
|
||||||
string.IsNullOrEmpty(normalizedObjectId) ? null : normalizedObjectId,
|
|
||||||
pluginId,
|
|
||||||
normalizedNotifyIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_started)
|
|
||||||
{
|
|
||||||
_ = NotifyCatalogChangedAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public PublicIpcCatalogSnapshot GetCatalogSnapshot()
|
|
||||||
{
|
|
||||||
PublicIpcServiceDescriptor[] services;
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
services = _services.Values
|
|
||||||
.Select(entry => new PublicIpcServiceDescriptor(
|
|
||||||
entry.ContractType.FullName ?? entry.ContractType.Name,
|
|
||||||
entry.ContractType.Assembly.GetName().Name ?? string.Empty,
|
|
||||||
entry.ContractType.AssemblyQualifiedName,
|
|
||||||
entry.ObjectId,
|
|
||||||
entry.PluginId,
|
|
||||||
string.IsNullOrWhiteSpace(entry.PluginId),
|
|
||||||
entry.NotifyIds))
|
|
||||||
.OrderBy(entry => entry.PluginId ?? string.Empty, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ThenBy(entry => entry.ContractTypeName, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
var plugins = PluginDescriptorProvider()?.ToArray() ?? Array.Empty<PublicPluginDescriptor>();
|
|
||||||
return new PublicIpcCatalogSnapshot(services, plugins, DateTimeOffset.UtcNow);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PublishStartupProgressAsync(
|
|
||||||
LanMountainDesktop.Shared.Contracts.Launcher.StartupProgressMessage message,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(message);
|
|
||||||
return NotifyAsync(IpcRoutedNotifyIds.LauncherStartupProgress, message, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PublishLoadingStateAsync(
|
|
||||||
LanMountainDesktop.Shared.Contracts.Launcher.LoadingStateMessage message,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(message);
|
|
||||||
return NotifyAsync(IpcRoutedNotifyIds.LauncherLoadingState, message, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task NotifyAsync<TPayload>(string notifyId, TPayload payload, CancellationToken cancellationToken = default)
|
|
||||||
where TPayload : class
|
|
||||||
{
|
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(notifyId);
|
|
||||||
ArgumentNullException.ThrowIfNull(payload);
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
foreach (var peer in _connectedPeers.Values)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var client = new JsonIpcDirectRoutedClientProxy(peer);
|
|
||||||
await client.NotifyAsync(notifyId, payload).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Keep notification fan-out best-effort. Broken peers are cleaned by dotnetCampus.Ipc.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task NotifyCatalogChangedAsync()
|
|
||||||
{
|
|
||||||
return NotifyAsync(IpcRoutedNotifyIds.CatalogChanged, GetCatalogSnapshot());
|
|
||||||
}
|
|
||||||
|
|
||||||
private PublicIpcSessionInfo BuildSessionInfo()
|
|
||||||
{
|
|
||||||
return new PublicIpcSessionInfo(
|
|
||||||
PipeName,
|
|
||||||
IpcConstants.ProtocolVersion,
|
|
||||||
[
|
|
||||||
IpcConstants.Routes.SessionGetInfo,
|
|
||||||
IpcConstants.Routes.CatalogGet,
|
|
||||||
IpcRoutedNotifyIds.CatalogChanged,
|
|
||||||
IpcRoutedNotifyIds.LauncherStartupProgress,
|
|
||||||
IpcRoutedNotifyIds.LauncherLoadingState
|
|
||||||
],
|
|
||||||
StartedAt);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Provider.PeerConnected -= OnPeerConnected;
|
|
||||||
Provider.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPeerConnected(object? sender, PeerConnectedArgs e)
|
|
||||||
{
|
|
||||||
var peer = e.Peer;
|
|
||||||
_connectedPeers[peer.PeerName] = peer;
|
|
||||||
peer.PeerConnectionBroken -= OnPeerConnectionBroken;
|
|
||||||
peer.PeerConnectionBroken += OnPeerConnectionBroken;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPeerConnectionBroken(object? sender, IPeerConnectionBrokenArgs e)
|
|
||||||
{
|
|
||||||
if (sender is PeerProxy peer)
|
|
||||||
{
|
|
||||||
_connectedPeers.TryRemove(peer.PeerName, out _);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed record PublicServiceEntry(
|
|
||||||
Type ContractType,
|
|
||||||
object Implementation,
|
|
||||||
string? ObjectId,
|
|
||||||
string? PluginId,
|
|
||||||
string[] NotifyIds);
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace LanMountainDesktop.Shared.IPC;
|
|
||||||
|
|
||||||
public sealed record PublicIpcServiceDescriptor(
|
|
||||||
string ContractTypeName,
|
|
||||||
string ContractAssemblyName,
|
|
||||||
string? ContractAssemblyQualifiedName,
|
|
||||||
string? ObjectId,
|
|
||||||
string? PluginId,
|
|
||||||
bool IsBuiltIn,
|
|
||||||
string[] NotifyIds);
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace LanMountainDesktop.Shared.IPC;
|
|
||||||
|
|
||||||
public sealed record PublicIpcSessionInfo(
|
|
||||||
string PipeName,
|
|
||||||
string ProtocolVersion,
|
|
||||||
string[] Capabilities,
|
|
||||||
DateTimeOffset StartedAt);
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace LanMountainDesktop.Shared.IPC;
|
|
||||||
|
|
||||||
public sealed record PublicPluginDescriptor(
|
|
||||||
string PluginId,
|
|
||||||
string DisplayName,
|
|
||||||
string? Version,
|
|
||||||
bool IsLoaded,
|
|
||||||
bool IsEnabled);
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# LanMountainDesktop.Shared.IPC
|
|
||||||
|
|
||||||
Public IPC abstractions and host/client helpers for LanMountainDesktop.
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
using LanMountainDesktop.PluginSdk;
|
|
||||||
using LanMountainDesktop.Shared.IPC;
|
|
||||||
using LanMountainDesktop.Shared.IPC.Abstractions.Services;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Tests;
|
|
||||||
|
|
||||||
public sealed class ExternalIpcPublicApiTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public async Task PublicIpcHost_ExposesStrongTypedServiceAndCatalog()
|
|
||||||
{
|
|
||||||
var pipeName = "LanMountainDesktop.Test." + Guid.NewGuid().ToString("N");
|
|
||||||
using var host = new PublicIpcHostService(pipeName);
|
|
||||||
host.PluginDescriptorProvider = () =>
|
|
||||||
[
|
|
||||||
new PublicPluginDescriptor("sample.plugin", "Sample Plugin", "1.0.0", true, true)
|
|
||||||
];
|
|
||||||
|
|
||||||
var appInfo = new PublicAppInfoSnapshot(
|
|
||||||
"LanMountainDesktop",
|
|
||||||
"1.2.3",
|
|
||||||
"Administrate",
|
|
||||||
pipeName,
|
|
||||||
42,
|
|
||||||
DateTimeOffset.UtcNow);
|
|
||||||
host.RegisterPublicService<IPublicAppInfoService>(new TestPublicAppInfoService(appInfo));
|
|
||||||
host.Start();
|
|
||||||
|
|
||||||
using var client = new LanMountainDesktopIpcClient();
|
|
||||||
var catalogChanged = new TaskCompletionSource<PublicIpcCatalogSnapshot>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
||||||
client.RegisterNotifyHandler<PublicIpcCatalogSnapshot>(IpcRoutedNotifyIds.CatalogChanged, snapshot =>
|
|
||||||
{
|
|
||||||
catalogChanged.TrySetResult(snapshot);
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.ConnectAsync(pipeName);
|
|
||||||
|
|
||||||
var proxy = client.CreateProxy<IPublicAppInfoService>();
|
|
||||||
var remoteInfo = proxy.GetAppInfo();
|
|
||||||
Assert.Equal(appInfo.ApplicationName, remoteInfo.ApplicationName);
|
|
||||||
Assert.Equal(appInfo.Version, remoteInfo.Version);
|
|
||||||
Assert.Equal(appInfo.Codename, remoteInfo.Codename);
|
|
||||||
|
|
||||||
var initialCatalog = await client.GetCatalogAsync();
|
|
||||||
Assert.NotNull(initialCatalog);
|
|
||||||
Assert.Contains(initialCatalog!.Services, service => service.ContractTypeName == typeof(IPublicAppInfoService).FullName);
|
|
||||||
Assert.Contains(initialCatalog.Plugins, plugin => plugin.PluginId == "sample.plugin");
|
|
||||||
|
|
||||||
host.RegisterPublicService<IPublicPluginCatalogService>(new TestPublicPluginCatalogService(initialCatalog));
|
|
||||||
var updatedCatalog = await catalogChanged.Task.WaitAsync(TimeSpan.FromSeconds(10));
|
|
||||||
Assert.Contains(updatedCatalog.Services, service => service.ContractTypeName == typeof(IPublicPluginCatalogService).FullName);
|
|
||||||
|
|
||||||
var sessionInfo = await client.GetSessionInfoAsync();
|
|
||||||
Assert.NotNull(sessionInfo);
|
|
||||||
Assert.Equal(pipeName, sessionInfo!.PipeName);
|
|
||||||
Assert.Equal(IpcConstants.ProtocolVersion, sessionInfo.ProtocolVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void AddPluginPublicIpc_RegistersServiceDescriptor()
|
|
||||||
{
|
|
||||||
var services = new ServiceCollection();
|
|
||||||
services.AddPluginPublicIpc<ITestPluginPublicService, TestPluginPublicService>(
|
|
||||||
objectId: "plugin-service",
|
|
||||||
notifyIds: ["lanmountain.plugin.sample.updated"]);
|
|
||||||
|
|
||||||
using var provider = services.BuildServiceProvider();
|
|
||||||
var registration = Assert.Single(provider.GetServices<PluginPublicIpcServiceRegistration>());
|
|
||||||
Assert.Equal(typeof(ITestPluginPublicService), registration.ContractType);
|
|
||||||
Assert.Equal("plugin-service", registration.ObjectId);
|
|
||||||
Assert.Contains("lanmountain.plugin.sample.updated", registration.NotifyIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class TestPublicAppInfoService : IPublicAppInfoService
|
|
||||||
{
|
|
||||||
private readonly PublicAppInfoSnapshot _snapshot;
|
|
||||||
|
|
||||||
public TestPublicAppInfoService(PublicAppInfoSnapshot snapshot)
|
|
||||||
{
|
|
||||||
_snapshot = snapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PublicAppInfoSnapshot GetAppInfo()
|
|
||||||
{
|
|
||||||
return _snapshot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class TestPublicPluginCatalogService : IPublicPluginCatalogService
|
|
||||||
{
|
|
||||||
private readonly PublicIpcCatalogSnapshot _snapshot;
|
|
||||||
|
|
||||||
public TestPublicPluginCatalogService(PublicIpcCatalogSnapshot snapshot)
|
|
||||||
{
|
|
||||||
_snapshot = snapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PublicIpcCatalogSnapshot GetCatalog()
|
|
||||||
{
|
|
||||||
return _snapshot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
[dotnetCampus.Ipc.CompilerServices.Attributes.IpcPublic]
|
|
||||||
public interface ITestPluginPublicService
|
|
||||||
{
|
|
||||||
string Ping();
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class TestPluginPublicService : ITestPluginPublicService
|
|
||||||
{
|
|
||||||
public string Ping()
|
|
||||||
{
|
|
||||||
return "pong";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using LanMountainDesktop.Services.Settings;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Tests;
|
|
||||||
|
|
||||||
public sealed class SettingsWindowPlacementHelperTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void ResolveWorkingArea_PrefersReferenceScreen()
|
|
||||||
{
|
|
||||||
var referenceArea = new PixelRect(1920, 0, 2560, 1440);
|
|
||||||
var primaryArea = new PixelRect(0, 0, 1920, 1080);
|
|
||||||
|
|
||||||
var result = SettingsWindowPlacementHelper.ResolveWorkingArea(
|
|
||||||
referenceArea,
|
|
||||||
primaryArea,
|
|
||||||
fallbackWindowWidth: 1120,
|
|
||||||
fallbackWindowHeight: 760);
|
|
||||||
|
|
||||||
Assert.Equal(referenceArea, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ResolveWorkingArea_FallsBackToPrimaryScreenWhenReferenceIsMissing()
|
|
||||||
{
|
|
||||||
var primaryArea = new PixelRect(0, 0, 1920, 1080);
|
|
||||||
|
|
||||||
var result = SettingsWindowPlacementHelper.ResolveWorkingArea(
|
|
||||||
referenceWorkingArea: null,
|
|
||||||
primaryWorkingArea: primaryArea,
|
|
||||||
fallbackWindowWidth: 1120,
|
|
||||||
fallbackWindowHeight: 760);
|
|
||||||
|
|
||||||
Assert.Equal(primaryArea, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CalculateCenteredPosition_ReturnsCenteredPointInsideWorkingArea()
|
|
||||||
{
|
|
||||||
var workingArea = new PixelRect(1920, 40, 2560, 1400);
|
|
||||||
|
|
||||||
var result = SettingsWindowPlacementHelper.CalculateCenteredPosition(
|
|
||||||
workingArea,
|
|
||||||
windowWidth: 1120,
|
|
||||||
windowHeight: 760);
|
|
||||||
|
|
||||||
Assert.Equal(new PixelPoint(2640, 360), result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
<Project Path="LanMountainDesktop.Host.Abstractions/LanMountainDesktop.Host.Abstractions.csproj" />
|
<Project Path="LanMountainDesktop.Host.Abstractions/LanMountainDesktop.Host.Abstractions.csproj" />
|
||||||
<Project Path="LanMountainDesktop.Shared.Contracts/LanMountainDesktop.Shared.Contracts.csproj" />
|
<Project Path="LanMountainDesktop.Shared.Contracts/LanMountainDesktop.Shared.Contracts.csproj" />
|
||||||
<Project Path="LanMountainDesktop.Shared.IPC/LanMountainDesktop.Shared.IPC.csproj" />
|
|
||||||
<Project Path="LanMountainDesktop.Settings.Core/LanMountainDesktop.Settings.Core.csproj" />
|
<Project Path="LanMountainDesktop.Settings.Core/LanMountainDesktop.Settings.Core.csproj" />
|
||||||
<Project Path="LanMountainDesktop.Appearance/LanMountainDesktop.Appearance.csproj" />
|
<Project Path="LanMountainDesktop.Appearance/LanMountainDesktop.Appearance.csproj" />
|
||||||
<Project Path="LanMountainDesktop.DesktopComponents.Runtime/LanMountainDesktop.DesktopComponents.Runtime.csproj" />
|
<Project Path="LanMountainDesktop.DesktopComponents.Runtime/LanMountainDesktop.DesktopComponents.Runtime.csproj" />
|
||||||
|
|||||||
@@ -20,13 +20,10 @@ using LanMountainDesktop.DesktopHost;
|
|||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.PluginSdk;
|
using LanMountainDesktop.PluginSdk;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
using LanMountainDesktop.Services.ExternalIpc;
|
|
||||||
using LanMountainDesktop.Services.Launcher;
|
using LanMountainDesktop.Services.Launcher;
|
||||||
using LanMountainDesktop.Services.Loading;
|
using LanMountainDesktop.Services.Loading;
|
||||||
using LanMountainDesktop.Services.Settings;
|
using LanMountainDesktop.Services.Settings;
|
||||||
using LanMountainDesktop.Shared.Contracts.Launcher;
|
using LanMountainDesktop.Shared.Contracts.Launcher;
|
||||||
using LanMountainDesktop.Shared.IPC;
|
|
||||||
using LanMountainDesktop.Shared.IPC.Abstractions.Services;
|
|
||||||
using LanMountainDesktop.Theme;
|
using LanMountainDesktop.Theme;
|
||||||
using LanMountainDesktop.ViewModels;
|
using LanMountainDesktop.ViewModels;
|
||||||
using LanMountainDesktop.Views;
|
using LanMountainDesktop.Views;
|
||||||
@@ -58,7 +55,6 @@ public partial class App : Application
|
|||||||
private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService();
|
private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService();
|
||||||
private readonly IDetachedComponentLibraryWindowService _detachedComponentLibraryWindowService = new DetachedComponentLibraryWindowService();
|
private readonly IDetachedComponentLibraryWindowService _detachedComponentLibraryWindowService = new DetachedComponentLibraryWindowService();
|
||||||
private readonly ILocationService _locationService = HostLocationServiceProvider.GetOrCreate();
|
private readonly ILocationService _locationService = HostLocationServiceProvider.GetOrCreate();
|
||||||
private readonly DateTimeOffset _startupAt = DateTimeOffset.UtcNow;
|
|
||||||
private ISettingsPageRegistry? _settingsPageRegistry;
|
private ISettingsPageRegistry? _settingsPageRegistry;
|
||||||
private ISettingsWindowService? _settingsWindowService;
|
private ISettingsWindowService? _settingsWindowService;
|
||||||
private WeatherLocationRefreshService? _weatherLocationRefreshService;
|
private WeatherLocationRefreshService? _weatherLocationRefreshService;
|
||||||
@@ -79,7 +75,7 @@ public partial class App : Application
|
|||||||
private bool _mainWindowClosed;
|
private bool _mainWindowClosed;
|
||||||
private bool _uiUnhandledExceptionHooked;
|
private bool _uiUnhandledExceptionHooked;
|
||||||
private DesktopShellHost? _desktopShellHost;
|
private DesktopShellHost? _desktopShellHost;
|
||||||
private PublicIpcHostService? _publicIpcHostService;
|
private LauncherIpcClient? _launcherIpcClient;
|
||||||
private LoadingStateManager? _loadingStateManager;
|
private LoadingStateManager? _loadingStateManager;
|
||||||
private LoadingStateReporter? _loadingStateReporter;
|
private LoadingStateReporter? _loadingStateReporter;
|
||||||
private bool _singleInstanceReleased;
|
private bool _singleInstanceReleased;
|
||||||
@@ -117,8 +113,8 @@ public partial class App : Application
|
|||||||
$"Opening settings window. Source='{source}'; PageTag='{pageTag ?? "<default>"}'.");
|
$"Opening settings window. Source='{source}'; PageTag='{pageTag ?? "<default>"}'.");
|
||||||
_settingsWindowService?.Open(new SettingsWindowOpenRequest(
|
_settingsWindowService?.Open(new SettingsWindowOpenRequest(
|
||||||
Source: source,
|
Source: source,
|
||||||
PageId: pageTag,
|
Owner: _mainWindow is { IsVisible: true } ? _mainWindow : null,
|
||||||
ScreenReferenceWindow: _mainWindow is { IsVisible: true } ? _mainWindow : null));
|
PageId: pageTag));
|
||||||
}
|
}
|
||||||
|
|
||||||
public App()
|
public App()
|
||||||
@@ -164,7 +160,6 @@ public partial class App : Application
|
|||||||
|
|
||||||
RegisterUiUnhandledExceptionGuard();
|
RegisterUiUnhandledExceptionGuard();
|
||||||
LinuxDesktopEntryInstaller.EnsureInstalled();
|
LinuxDesktopEntryInstaller.EnsureInstalled();
|
||||||
InitializePublicIpc();
|
|
||||||
_ = InitializeLauncherIpcAsync();
|
_ = InitializeLauncherIpcAsync();
|
||||||
DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell);
|
DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell);
|
||||||
|
|
||||||
@@ -178,24 +173,34 @@ public partial class App : Application
|
|||||||
|
|
||||||
private async Task InitializeLauncherIpcAsync()
|
private async Task InitializeLauncherIpcAsync()
|
||||||
{
|
{
|
||||||
if (_loadingStateManager is not null)
|
if (!LauncherIpcClient.IsLaunchedByLauncher())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
_launcherIpcClient = new LauncherIpcClient();
|
||||||
|
var connected = await _launcherIpcClient.ConnectAsync();
|
||||||
|
if (!connected)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLogger.Info("LauncherIpc", "Connected to Launcher IPC server.");
|
||||||
|
|
||||||
bool hadBufferedMessages;
|
bool hadBufferedMessages;
|
||||||
lock (_launcherProgressLock)
|
lock (_launcherProgressLock)
|
||||||
{
|
{
|
||||||
hadBufferedMessages = _pendingLauncherProgressMessages.Count > 0;
|
hadBufferedMessages = _pendingLauncherProgressMessages.Count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await FlushPendingLauncherProgressAsync();
|
||||||
|
|
||||||
_loadingStateManager = new LoadingStateManager();
|
_loadingStateManager = new LoadingStateManager();
|
||||||
_loadingStateReporter = new LoadingStateReporter(_loadingStateManager, _publicIpcHostService);
|
_loadingStateReporter = new LoadingStateReporter(_loadingStateManager, _launcherIpcClient);
|
||||||
_loadingStateReporter.Start();
|
_loadingStateReporter.Start();
|
||||||
|
|
||||||
_loadingStateManager.RegisterItem("system.init", LoadingItemType.System, "System Initialization", "Initialize core application services.");
|
_loadingStateManager.RegisterItem("system.init", LoadingItemType.System, "System Initialization", "Initialize core application services.");
|
||||||
_loadingStateManager.StartItem("system.init", "Public IPC host ready.");
|
_loadingStateManager.StartItem("system.init", "Launcher IPC connected.");
|
||||||
await FlushPendingLauncherProgressAsync();
|
|
||||||
|
|
||||||
if (!hadBufferedMessages)
|
if (!hadBufferedMessages)
|
||||||
{
|
{
|
||||||
@@ -233,8 +238,8 @@ public partial class App : Application
|
|||||||
|
|
||||||
private void QueueOrSendLauncherProgress(StartupProgressMessage message, bool logSuccess)
|
private void QueueOrSendLauncherProgress(StartupProgressMessage message, bool logSuccess)
|
||||||
{
|
{
|
||||||
var publicIpcHostService = _publicIpcHostService;
|
var ipcClient = _launcherIpcClient;
|
||||||
if (publicIpcHostService is null)
|
if (ipcClient is null || !ipcClient.IsConnected)
|
||||||
{
|
{
|
||||||
lock (_launcherProgressLock)
|
lock (_launcherProgressLock)
|
||||||
{
|
{
|
||||||
@@ -245,13 +250,13 @@ public partial class App : Application
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = SendLauncherProgressAsync(publicIpcHostService, message, logSuccess);
|
_ = SendLauncherProgressAsync(ipcClient, message, logSuccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task FlushPendingLauncherProgressAsync()
|
private async Task FlushPendingLauncherProgressAsync()
|
||||||
{
|
{
|
||||||
var publicIpcHostService = _publicIpcHostService;
|
var ipcClient = _launcherIpcClient;
|
||||||
if (publicIpcHostService is null)
|
if (ipcClient is null || !ipcClient.IsConnected)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -265,15 +270,15 @@ public partial class App : Application
|
|||||||
|
|
||||||
foreach (var pendingMessage in pendingMessages)
|
foreach (var pendingMessage in pendingMessages)
|
||||||
{
|
{
|
||||||
await SendLauncherProgressAsync(publicIpcHostService, pendingMessage, logSuccess: false);
|
await SendLauncherProgressAsync(ipcClient, pendingMessage, logSuccess: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendLauncherProgressAsync(PublicIpcHostService publicIpcHostService, StartupProgressMessage message, bool logSuccess)
|
private async Task SendLauncherProgressAsync(LauncherIpcClient ipcClient, StartupProgressMessage message, bool logSuccess)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await publicIpcHostService.PublishStartupProgressAsync(message);
|
await ipcClient.ReportProgressAsync(message);
|
||||||
if (logSuccess)
|
if (logSuccess)
|
||||||
{
|
{
|
||||||
AppLogger.Info("LauncherIpc", $"Successfully reported stage: {message.Stage}");
|
AppLogger.Info("LauncherIpc", $"Successfully reported stage: {message.Stage}");
|
||||||
@@ -458,7 +463,7 @@ public partial class App : Application
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_pluginRuntimeService?.Dispose();
|
_pluginRuntimeService?.Dispose();
|
||||||
_pluginRuntimeService = new PluginRuntimeService(_settingsFacade, _publicIpcHostService);
|
_pluginRuntimeService = new PluginRuntimeService(_settingsFacade);
|
||||||
HostSettingsFacadeProvider.BindPluginRuntime(_pluginRuntimeService);
|
HostSettingsFacadeProvider.BindPluginRuntime(_pluginRuntimeService);
|
||||||
_pluginRuntimeService.LoadInstalledPlugins();
|
_pluginRuntimeService.LoadInstalledPlugins();
|
||||||
}
|
}
|
||||||
@@ -738,7 +743,7 @@ public partial class App : Application
|
|||||||
var mainWindow = GetOrCreateMainWindow(desktop, source);
|
var mainWindow = GetOrCreateMainWindow(desktop, source);
|
||||||
mainWindow.PrepareEnterAnimation();
|
mainWindow.PrepareEnterAnimation();
|
||||||
|
|
||||||
mainWindow.ShowInTaskbar = ShouldShowMainWindowInTaskbar();
|
mainWindow.ShowInTaskbar = true;
|
||||||
|
|
||||||
if (!mainWindow.IsVisible)
|
if (!mainWindow.IsVisible)
|
||||||
{
|
{
|
||||||
@@ -1038,19 +1043,6 @@ public partial class App : Application
|
|||||||
_pluginRuntimeService = null;
|
_pluginRuntimeService = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_publicIpcHostService?.Dispose();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
AppLogger.Warn("PublicIpc", "Failed to dispose public IPC host during shutdown.", ex);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_publicIpcHostService = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_settingsWindowService?.Close();
|
_settingsWindowService?.Close();
|
||||||
if (_settingsPageRegistry is IDisposable disposableRegistry)
|
if (_settingsPageRegistry is IDisposable disposableRegistry)
|
||||||
{
|
{
|
||||||
@@ -1106,7 +1098,7 @@ public partial class App : Application
|
|||||||
var mainWindow = new MainWindow
|
var mainWindow = new MainWindow
|
||||||
{
|
{
|
||||||
DataContext = new MainWindowViewModel(),
|
DataContext = new MainWindowViewModel(),
|
||||||
ShowInTaskbar = ShouldShowMainWindowInTaskbar()
|
ShowInTaskbar = true
|
||||||
};
|
};
|
||||||
|
|
||||||
_mainWindowOpened = false;
|
_mainWindowOpened = false;
|
||||||
@@ -1296,11 +1288,6 @@ public partial class App : Application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldShowMainWindowInTaskbar()
|
|
||||||
{
|
|
||||||
return _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App).ShowInTaskbar;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetDesktopShellState(DesktopShellState state, string source)
|
private void SetDesktopShellState(DesktopShellState state, string source)
|
||||||
{
|
{
|
||||||
if (_desktopShellState == state)
|
if (_desktopShellState == state)
|
||||||
@@ -1349,56 +1336,6 @@ public partial class App : Application
|
|||||||
var languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
|
var languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
|
||||||
return _localizationService.GetString(languageCode, key, fallback);
|
return _localizationService.GetString(languageCode, key, fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool TryActivateMainWindowFromExternalIpc(string source)
|
|
||||||
{
|
|
||||||
return RestoreOrCreateMainWindowCore(showSingleInstanceNotice: false, source);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializePublicIpc()
|
|
||||||
{
|
|
||||||
if (_publicIpcHostService is not null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var version = typeof(App).Assembly.GetName().Version?.ToString() ?? "1.0.0";
|
|
||||||
_publicIpcHostService = new PublicIpcHostService();
|
|
||||||
_publicIpcHostService.PluginDescriptorProvider = BuildPublicPluginDescriptors;
|
|
||||||
_publicIpcHostService.RegisterPublicService<IPublicAppInfoService>(
|
|
||||||
new PublicAppInfoService(version, "Administrate", _startupAt));
|
|
||||||
_publicIpcHostService.RegisterPublicService<IPublicShellControlService>(
|
|
||||||
new PublicShellControlService());
|
|
||||||
_publicIpcHostService.RegisterPublicService<IPublicPluginCatalogService>(
|
|
||||||
new PublicPluginCatalogService(_publicIpcHostService));
|
|
||||||
_publicIpcHostService.Start();
|
|
||||||
AppLogger.Info("PublicIpc", $"Public IPC host started. PipeName='{IpcConstants.DefaultPipeName}'.");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
AppLogger.Warn("PublicIpc", "Failed to initialize public IPC host.", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IReadOnlyList<PublicPluginDescriptor> BuildPublicPluginDescriptors()
|
|
||||||
{
|
|
||||||
var runtime = _pluginRuntimeService;
|
|
||||||
if (runtime is null)
|
|
||||||
{
|
|
||||||
return Array.Empty<PublicPluginDescriptor>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return runtime.Catalog
|
|
||||||
.Select(entry => new PublicPluginDescriptor(
|
|
||||||
entry.Manifest.Id,
|
|
||||||
entry.Manifest.Name,
|
|
||||||
entry.Manifest.Version,
|
|
||||||
entry.IsLoaded,
|
|
||||||
entry.IsEnabled))
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\LanMountainDesktop.Host.Abstractions\LanMountainDesktop.Host.Abstractions.csproj" />
|
<ProjectReference Include="..\LanMountainDesktop.Host.Abstractions\LanMountainDesktop.Host.Abstractions.csproj" />
|
||||||
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||||
<ProjectReference Include="..\LanMountainDesktop.Shared.IPC\LanMountainDesktop.Shared.IPC.csproj" />
|
|
||||||
<ProjectReference Include="..\LanMountainDesktop.Settings.Core\LanMountainDesktop.Settings.Core.csproj" />
|
<ProjectReference Include="..\LanMountainDesktop.Settings.Core\LanMountainDesktop.Settings.Core.csproj" />
|
||||||
<ProjectReference Include="..\LanMountainDesktop.Appearance\LanMountainDesktop.Appearance.csproj" />
|
<ProjectReference Include="..\LanMountainDesktop.Appearance\LanMountainDesktop.Appearance.csproj" />
|
||||||
<ProjectReference Include="..\LanMountainDesktop.DesktopComponents.Runtime\LanMountainDesktop.DesktopComponents.Runtime.csproj" />
|
<ProjectReference Include="..\LanMountainDesktop.DesktopComponents.Runtime\LanMountainDesktop.DesktopComponents.Runtime.csproj" />
|
||||||
|
|||||||
@@ -154,8 +154,6 @@ public sealed class AppSettingsSnapshot
|
|||||||
|
|
||||||
public bool EnableSlideTransition { get; set; } = false;
|
public bool EnableSlideTransition { get; set; } = false;
|
||||||
|
|
||||||
public bool ShowInTaskbar { get; set; } = false;
|
|
||||||
|
|
||||||
public bool EnableFusedDesktop { get; set; } = false;
|
public bool EnableFusedDesktop { get; set; } = false;
|
||||||
|
|
||||||
public List<string> DisabledPluginIds { get; set; } = [];
|
public List<string> DisabledPluginIds { get; set; } = [];
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
using LanMountainDesktop.Shared.IPC;
|
|
||||||
using LanMountainDesktop.Shared.IPC.Abstractions.Services;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Services.ExternalIpc;
|
|
||||||
|
|
||||||
internal sealed class PublicAppInfoService : IPublicAppInfoService
|
|
||||||
{
|
|
||||||
private readonly string _version;
|
|
||||||
private readonly string _codename;
|
|
||||||
private readonly DateTimeOffset _startedAt;
|
|
||||||
|
|
||||||
public PublicAppInfoService(string version, string codename, DateTimeOffset startedAt)
|
|
||||||
{
|
|
||||||
_version = version;
|
|
||||||
_codename = codename;
|
|
||||||
_startedAt = startedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PublicAppInfoSnapshot GetAppInfo()
|
|
||||||
{
|
|
||||||
return new PublicAppInfoSnapshot(
|
|
||||||
"LanMountainDesktop",
|
|
||||||
_version,
|
|
||||||
_codename,
|
|
||||||
IpcConstants.DefaultPipeName,
|
|
||||||
Environment.ProcessId,
|
|
||||||
_startedAt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using LanMountainDesktop.Shared.IPC;
|
|
||||||
using LanMountainDesktop.Shared.IPC.Abstractions.Services;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Services.ExternalIpc;
|
|
||||||
|
|
||||||
internal sealed class PublicPluginCatalogService : IPublicPluginCatalogService
|
|
||||||
{
|
|
||||||
private readonly PublicIpcHostService _publicIpcHostService;
|
|
||||||
|
|
||||||
public PublicPluginCatalogService(PublicIpcHostService publicIpcHostService)
|
|
||||||
{
|
|
||||||
_publicIpcHostService = publicIpcHostService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PublicIpcCatalogSnapshot GetCatalog()
|
|
||||||
{
|
|
||||||
return _publicIpcHostService.GetCatalogSnapshot();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
using LanMountainDesktop.PluginSdk;
|
|
||||||
using LanMountainDesktop.Shared.IPC.Abstractions.Services;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Services.ExternalIpc;
|
|
||||||
|
|
||||||
internal sealed class PublicShellControlService : IPublicShellControlService
|
|
||||||
{
|
|
||||||
public Task<bool> ActivateMainWindowAsync()
|
|
||||||
{
|
|
||||||
return Dispatcher.UIThread.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
return (Application.Current as App)?.TryActivateMainWindowFromExternalIpc("PublicIpc") == true;
|
|
||||||
}).GetTask();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<bool> OpenSettingsAsync(string? pageTag = null)
|
|
||||||
{
|
|
||||||
return Dispatcher.UIThread.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
if (Application.Current is not App app)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
app.OpenIndependentSettingsModule("PublicIpc", pageTag);
|
|
||||||
return true;
|
|
||||||
}).GetTask();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<bool> RestartAsync()
|
|
||||||
{
|
|
||||||
var lifecycle = App.CurrentHostApplicationLifecycle;
|
|
||||||
return Task.FromResult(lifecycle?.TryRestart(new HostApplicationLifecycleRequest(
|
|
||||||
Source: "PublicIpc",
|
|
||||||
Reason: "External IPC requested restart.")) == true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<bool> ExitAsync()
|
|
||||||
{
|
|
||||||
var lifecycle = App.CurrentHostApplicationLifecycle;
|
|
||||||
return Task.FromResult(lifecycle?.TryExit(new HostApplicationLifecycleRequest(
|
|
||||||
Source: "PublicIpc",
|
|
||||||
Reason: "External IPC requested exit.")) == true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Timers;
|
using System.Timers;
|
||||||
|
using LanMountainDesktop.Services.Launcher;
|
||||||
using LanMountainDesktop.Shared.Contracts.Launcher;
|
using LanMountainDesktop.Shared.Contracts.Launcher;
|
||||||
using LanMountainDesktop.Shared.IPC;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Services.Loading;
|
namespace LanMountainDesktop.Services.Loading;
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ namespace LanMountainDesktop.Services.Loading;
|
|||||||
public class LoadingStateReporter : IDisposable
|
public class LoadingStateReporter : IDisposable
|
||||||
{
|
{
|
||||||
private readonly LoadingStateManager _manager;
|
private readonly LoadingStateManager _manager;
|
||||||
private readonly IExternalIpcNotificationPublisher? _notificationPublisher;
|
private readonly LauncherIpcClient? _ipcClient;
|
||||||
private readonly System.Timers.Timer _reportTimer;
|
private readonly System.Timers.Timer _reportTimer;
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
private bool _isDisposed;
|
private bool _isDisposed;
|
||||||
@@ -36,10 +36,10 @@ public class LoadingStateReporter : IDisposable
|
|||||||
|
|
||||||
public LoadingStateReporter(
|
public LoadingStateReporter(
|
||||||
LoadingStateManager manager,
|
LoadingStateManager manager,
|
||||||
IExternalIpcNotificationPublisher? notificationPublisher = null)
|
LauncherIpcClient? ipcClient = null)
|
||||||
{
|
{
|
||||||
_manager = manager ?? throw new ArgumentNullException(nameof(manager));
|
_manager = manager ?? throw new ArgumentNullException(nameof(manager));
|
||||||
_notificationPublisher = notificationPublisher;
|
_ipcClient = ipcClient;
|
||||||
|
|
||||||
// 创建定时上报定时器
|
// 创建定时上报定时器
|
||||||
_reportTimer = new System.Timers.Timer(ReportIntervalMs);
|
_reportTimer = new System.Timers.Timer(ReportIntervalMs);
|
||||||
@@ -80,7 +80,7 @@ public class LoadingStateReporter : IDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task ReportImmediatelyAsync()
|
public async Task ReportImmediatelyAsync()
|
||||||
{
|
{
|
||||||
if (_isDisposed || _notificationPublisher == null) return;
|
if (_isDisposed || _ipcClient == null) return;
|
||||||
|
|
||||||
var message = CreateDetailedProgressMessage();
|
var message = CreateDetailedProgressMessage();
|
||||||
await SendMessageAsync(message);
|
await SendMessageAsync(message);
|
||||||
@@ -91,7 +91,7 @@ public class LoadingStateReporter : IDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task ReportItemProgressAsync(string itemId, int percent, string? message = null)
|
public async Task ReportItemProgressAsync(string itemId, int percent, string? message = null)
|
||||||
{
|
{
|
||||||
if (_isDisposed || _notificationPublisher == null) return;
|
if (_isDisposed || _ipcClient == null) return;
|
||||||
|
|
||||||
var item = _manager.GetAllItems().FirstOrDefault(i => i.Id == itemId);
|
var item = _manager.GetAllItems().FirstOrDefault(i => i.Id == itemId);
|
||||||
if (item == null) return;
|
if (item == null) return;
|
||||||
@@ -121,7 +121,7 @@ public class LoadingStateReporter : IDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task ReportStageChangeAsync(StartupStage stage, string? message = null)
|
public async Task ReportStageChangeAsync(StartupStage stage, string? message = null)
|
||||||
{
|
{
|
||||||
if (_isDisposed || _notificationPublisher == null) return;
|
if (_isDisposed || _ipcClient == null) return;
|
||||||
|
|
||||||
var progressMessage = new DetailedProgressMessage
|
var progressMessage = new DetailedProgressMessage
|
||||||
{
|
{
|
||||||
@@ -140,7 +140,7 @@ public class LoadingStateReporter : IDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task ReportErrorAsync(string errorMessage, string? details = null)
|
public async Task ReportErrorAsync(string errorMessage, string? details = null)
|
||||||
{
|
{
|
||||||
if (_isDisposed || _notificationPublisher == null) return;
|
if (_isDisposed || _ipcClient == null) return;
|
||||||
|
|
||||||
var fullMessage = string.IsNullOrEmpty(details)
|
var fullMessage = string.IsNullOrEmpty(details)
|
||||||
? errorMessage
|
? errorMessage
|
||||||
@@ -280,7 +280,7 @@ public class LoadingStateReporter : IDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task SendMessageAsync(DetailedProgressMessage message)
|
private async Task SendMessageAsync(DetailedProgressMessage message)
|
||||||
{
|
{
|
||||||
if (_notificationPublisher == null) return;
|
if (_ipcClient == null) return;
|
||||||
|
|
||||||
// 检查最小上报间隔
|
// 检查最小上报间隔
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = DateTimeOffset.UtcNow;
|
||||||
@@ -293,15 +293,15 @@ public class LoadingStateReporter : IDisposable
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 转换为 StartupProgressMessage 以保持兼容性
|
// 转换为 StartupProgressMessage 以保持兼容性
|
||||||
var loadingStateMessage = _manager.GetLoadingStateMessage() with
|
var baseMessage = new StartupProgressMessage
|
||||||
{
|
{
|
||||||
Stage = message.Stage,
|
Stage = message.Stage,
|
||||||
OverallProgressPercent = message.ProgressPercent,
|
ProgressPercent = message.ProgressPercent,
|
||||||
Message = FormatMessage(message),
|
Message = FormatMessage(message),
|
||||||
Timestamp = DateTimeOffset.UtcNow
|
Timestamp = DateTimeOffset.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
await _notificationPublisher.NotifyAsync(IpcRoutedNotifyIds.LauncherLoadingState, loadingStateMessage);
|
await _ipcClient.ReportProgressAsync(baseMessage);
|
||||||
_lastReportTime = DateTimeOffset.UtcNow;
|
_lastReportTime = DateTimeOffset.UtcNow;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -14,10 +14,28 @@ using LanMountainDesktop.Views;
|
|||||||
|
|
||||||
namespace LanMountainDesktop.Services.Settings;
|
namespace LanMountainDesktop.Services.Settings;
|
||||||
|
|
||||||
|
public enum SettingsWindowAnchorTarget
|
||||||
|
{
|
||||||
|
DesktopDockTrailingEdge = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SettingsWindowFallbackMode
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
ScreenBottomRight = 1
|
||||||
|
}
|
||||||
|
|
||||||
public readonly record struct SettingsWindowOpenRequest(
|
public readonly record struct SettingsWindowOpenRequest(
|
||||||
string Source,
|
string Source,
|
||||||
|
Window? Owner = null,
|
||||||
string? PageId = null,
|
string? PageId = null,
|
||||||
Window? ScreenReferenceWindow = null);
|
SettingsWindowAnchorTarget AnchorTarget = SettingsWindowAnchorTarget.DesktopDockTrailingEdge,
|
||||||
|
SettingsWindowFallbackMode FallbackMode = SettingsWindowFallbackMode.ScreenBottomRight);
|
||||||
|
|
||||||
|
public interface ISettingsWindowAnchorProvider
|
||||||
|
{
|
||||||
|
bool TryGetSettingsWindowAnchorBounds(out PixelRect anchorBounds);
|
||||||
|
}
|
||||||
|
|
||||||
public interface ISettingsWindowService
|
public interface ISettingsWindowService
|
||||||
{
|
{
|
||||||
@@ -28,6 +46,8 @@ public interface ISettingsWindowService
|
|||||||
void Open(SettingsWindowOpenRequest request);
|
void Open(SettingsWindowOpenRequest request);
|
||||||
|
|
||||||
void Close();
|
void Close();
|
||||||
|
|
||||||
|
void Toggle(SettingsWindowOpenRequest request);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class SettingsWindowService : ISettingsWindowService
|
internal sealed class SettingsWindowService : ISettingsWindowService
|
||||||
@@ -72,25 +92,27 @@ internal sealed class SettingsWindowService : ISettingsWindowService
|
|||||||
var appearanceSnapshot = _appearanceThemeService.GetCurrent();
|
var appearanceSnapshot = _appearanceThemeService.GetCurrent();
|
||||||
_window.ApplyChromeMode(appearanceSnapshot.UseSystemChrome);
|
_window.ApplyChromeMode(appearanceSnapshot.UseSystemChrome);
|
||||||
ApplyTheme(_window);
|
ApplyTheme(_window);
|
||||||
|
_window.ReloadPages(request.PageId);
|
||||||
var targetPageId = request.PageId ?? _window.ViewModel.CurrentPageId;
|
PositionWindow(_window, request);
|
||||||
_window.ReloadPages(targetPageId);
|
|
||||||
|
|
||||||
if (!_window.IsVisible)
|
if (!_window.IsVisible)
|
||||||
{
|
{
|
||||||
CenterWindow(_window, request);
|
if (request.Owner is not null && request.Owner.IsVisible)
|
||||||
|
{
|
||||||
|
_window.Show(request.Owner);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
_window.Show();
|
_window.Show();
|
||||||
|
}
|
||||||
|
|
||||||
NotifyStateChanged();
|
NotifyStateChanged();
|
||||||
CenterWindowLater(_window, request);
|
PositionWindowLater(_window, request);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_window.WindowState == WindowState.Minimized)
|
|
||||||
{
|
|
||||||
_window.WindowState = WindowState.Normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
_window.Activate();
|
_window.Activate();
|
||||||
|
PositionWindowLater(_window, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Close()
|
public void Close()
|
||||||
@@ -98,6 +120,17 @@ internal sealed class SettingsWindowService : ISettingsWindowService
|
|||||||
_window?.Close();
|
_window?.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Toggle(SettingsWindowOpenRequest request)
|
||||||
|
{
|
||||||
|
if (IsOpen)
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Open(request);
|
||||||
|
}
|
||||||
|
|
||||||
private SettingsWindow CreateWindow()
|
private SettingsWindow CreateWindow()
|
||||||
{
|
{
|
||||||
var regionState = _settingsFacade.Region.Get();
|
var regionState = _settingsFacade.Region.Get();
|
||||||
@@ -114,7 +147,7 @@ internal sealed class SettingsWindowService : ISettingsWindowService
|
|||||||
_hostApplicationLifecycle,
|
_hostApplicationLifecycle,
|
||||||
useSystemChrome);
|
useSystemChrome);
|
||||||
ApplyTheme(window);
|
ApplyTheme(window);
|
||||||
window.ShowInTaskbar = true;
|
window.ShowInTaskbar = false;
|
||||||
window.Closed += (_, _) =>
|
window.Closed += (_, _) =>
|
||||||
{
|
{
|
||||||
_window = null;
|
_window = null;
|
||||||
@@ -123,87 +156,106 @@ internal sealed class SettingsWindowService : ISettingsWindowService
|
|||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CenterWindowLater(SettingsWindow window, SettingsWindowOpenRequest request)
|
private void PositionWindowLater(SettingsWindow window, SettingsWindowOpenRequest request)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Post(
|
Dispatcher.UIThread.Post(
|
||||||
() =>
|
() =>
|
||||||
{
|
{
|
||||||
if (!ReferenceEquals(_window, window) || !window.IsVisible)
|
if (!window.IsVisible)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CenterWindow(window, request);
|
PositionWindow(window, request);
|
||||||
},
|
},
|
||||||
DispatcherPriority.Background);
|
DispatcherPriority.Background);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CenterWindow(SettingsWindow window, SettingsWindowOpenRequest request)
|
private static void PositionWindow(SettingsWindow window, SettingsWindowOpenRequest request)
|
||||||
{
|
{
|
||||||
var referenceWorkingArea =
|
if (request.AnchorTarget == SettingsWindowAnchorTarget.DesktopDockTrailingEdge &&
|
||||||
request.ScreenReferenceWindow is { IsVisible: true } screenReferenceWindow &&
|
request.Owner is ISettingsWindowAnchorProvider anchorProvider &&
|
||||||
screenReferenceWindow.Screens?.ScreenFromWindow(screenReferenceWindow) is { } referenceScreen
|
anchorProvider.TryGetSettingsWindowAnchorBounds(out var anchorBounds))
|
||||||
? referenceScreen.WorkingArea
|
{
|
||||||
: (PixelRect?)null;
|
PositionWindowAboveAnchor(window, anchorBounds, request);
|
||||||
var width = ResolveWindowWidth(window, request.ScreenReferenceWindow);
|
return;
|
||||||
var height = ResolveWindowHeight(window, request.ScreenReferenceWindow);
|
|
||||||
var workingArea = SettingsWindowPlacementHelper.ResolveWorkingArea(
|
|
||||||
referenceWorkingArea,
|
|
||||||
window.Screens?.Primary?.WorkingArea,
|
|
||||||
width,
|
|
||||||
height);
|
|
||||||
window.Position = SettingsWindowPlacementHelper.CalculateCenteredPosition(workingArea, width, height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int ResolveWindowWidth(Window window, Window? referenceWindow)
|
if (request.FallbackMode == SettingsWindowFallbackMode.ScreenBottomRight)
|
||||||
{
|
{
|
||||||
var widthDip = ResolveWindowDimensionDip(window.Bounds.Width, window.Width, window.MinWidth, 1120d);
|
PositionWindowNearScreenBottomRight(window, request);
|
||||||
var scale = ResolveWindowScale(window, referenceWindow);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PositionWindowAboveAnchor(Window window, PixelRect anchorBounds, SettingsWindowOpenRequest request)
|
||||||
|
{
|
||||||
|
var workingArea = GetWorkingArea(window, request);
|
||||||
|
|
||||||
|
if (anchorBounds.Width <= 0 || anchorBounds.Height <= 0 ||
|
||||||
|
anchorBounds.Right < workingArea.X || anchorBounds.Y > workingArea.Bottom)
|
||||||
|
{
|
||||||
|
PositionWindowNearScreenBottomRight(window, request);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scale = window.RenderScaling > 0 ? window.RenderScaling : 1d;
|
||||||
|
var width = ResolveWindowWidth(window, scale);
|
||||||
|
var height = ResolveWindowHeight(window, scale);
|
||||||
|
var inset = (int)Math.Round(24 * scale);
|
||||||
|
var gap = (int)Math.Round(16 * scale);
|
||||||
|
|
||||||
|
var x = anchorBounds.Right - width - inset;
|
||||||
|
var y = anchorBounds.Y - height - gap;
|
||||||
|
x = Math.Clamp(x, workingArea.X + inset, Math.Max(workingArea.X + inset, workingArea.Right - width - inset));
|
||||||
|
y = Math.Clamp(y, workingArea.Y + inset, Math.Max(workingArea.Y + inset, workingArea.Bottom - height - inset));
|
||||||
|
window.Position = new PixelPoint(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PositionWindowNearScreenBottomRight(Window window, SettingsWindowOpenRequest request)
|
||||||
|
{
|
||||||
|
var workingArea = GetWorkingArea(window, request);
|
||||||
|
var scale = window.RenderScaling > 0 ? window.RenderScaling : 1d;
|
||||||
|
var width = ResolveWindowWidth(window, scale);
|
||||||
|
var height = ResolveWindowHeight(window, scale);
|
||||||
|
var inset = (int)Math.Round(24 * scale);
|
||||||
|
|
||||||
|
var x = Math.Max(workingArea.X + inset, workingArea.Right - width - inset);
|
||||||
|
var y = Math.Max(workingArea.Y + inset, workingArea.Bottom - height - inset);
|
||||||
|
window.Position = new PixelPoint(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PixelRect GetWorkingArea(Window window, SettingsWindowOpenRequest request)
|
||||||
|
{
|
||||||
|
if (request.Owner is not null && request.Owner.Screens?.ScreenFromWindow(request.Owner) is { } ownerScreen)
|
||||||
|
{
|
||||||
|
return ownerScreen.WorkingArea;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.Screens?.ScreenFromWindow(window) is { } windowScreen)
|
||||||
|
{
|
||||||
|
return windowScreen.WorkingArea;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.Screens?.Primary?.WorkingArea
|
||||||
|
?? new PixelRect(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
Math.Max(1280, ResolveWindowWidth(window, 1d) + 96),
|
||||||
|
Math.Max(720, ResolveWindowHeight(window, 1d) + 96));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ResolveWindowWidth(Window window, double scale)
|
||||||
|
{
|
||||||
|
var widthDip = window.Bounds.Width > 1 ? window.Bounds.Width : Math.Max(window.Width, window.MinWidth);
|
||||||
return Math.Max(320, (int)Math.Round(widthDip * scale));
|
return Math.Max(320, (int)Math.Round(widthDip * scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int ResolveWindowHeight(Window window, Window? referenceWindow)
|
private static int ResolveWindowHeight(Window window, double scale)
|
||||||
{
|
{
|
||||||
var heightDip = ResolveWindowDimensionDip(window.Bounds.Height, window.Height, window.MinHeight, 760d);
|
var heightDip = window.Bounds.Height > 1 ? window.Bounds.Height : Math.Max(window.Height, window.MinHeight);
|
||||||
var scale = ResolveWindowScale(window, referenceWindow);
|
|
||||||
return Math.Max(240, (int)Math.Round(heightDip * scale));
|
return Math.Max(240, (int)Math.Round(heightDip * scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double ResolveWindowScale(Window window, Window? referenceWindow)
|
|
||||||
{
|
|
||||||
if (referenceWindow is not null && referenceWindow.RenderScaling > 0)
|
|
||||||
{
|
|
||||||
return referenceWindow.RenderScaling;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.RenderScaling > 0)
|
|
||||||
{
|
|
||||||
return window.RenderScaling;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1d;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double ResolveWindowDimensionDip(double boundsDip, double configuredDip, double minimumDip, double fallbackDip)
|
|
||||||
{
|
|
||||||
if (boundsDip > 1)
|
|
||||||
{
|
|
||||||
return boundsDip;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!double.IsNaN(configuredDip) && configuredDip > 1)
|
|
||||||
{
|
|
||||||
return configuredDip;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!double.IsNaN(minimumDip) && minimumDip > 1)
|
|
||||||
{
|
|
||||||
return minimumDip;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallbackDip;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void NotifyStateChanged()
|
private void NotifyStateChanged()
|
||||||
{
|
{
|
||||||
StateChanged?.Invoke(this, EventArgs.Empty);
|
StateChanged?.Invoke(this, EventArgs.Empty);
|
||||||
@@ -311,38 +363,3 @@ internal sealed class SettingsWindowService : ISettingsWindowService
|
|||||||
}, DispatcherPriority.Background);
|
}, DispatcherPriority.Background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class SettingsWindowPlacementHelper
|
|
||||||
{
|
|
||||||
internal static PixelRect ResolveWorkingArea(
|
|
||||||
PixelRect? referenceWorkingArea,
|
|
||||||
PixelRect? primaryWorkingArea,
|
|
||||||
int fallbackWindowWidth,
|
|
||||||
int fallbackWindowHeight)
|
|
||||||
{
|
|
||||||
if (referenceWorkingArea is { } referenceArea)
|
|
||||||
{
|
|
||||||
return referenceArea;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (primaryWorkingArea is { } primaryArea)
|
|
||||||
{
|
|
||||||
return primaryArea;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new PixelRect(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
Math.Max(1280, fallbackWindowWidth + 96),
|
|
||||||
Math.Max(720, fallbackWindowHeight + 96));
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static PixelPoint CalculateCenteredPosition(PixelRect workingArea, int windowWidth, int windowHeight)
|
|
||||||
{
|
|
||||||
var horizontalOffset = Math.Max(0, (workingArea.Width - windowWidth) / 2);
|
|
||||||
var verticalOffset = Math.Max(0, (workingArea.Height - windowHeight) / 2);
|
|
||||||
return new PixelPoint(
|
|
||||||
workingArea.X + horizontalOffset,
|
|
||||||
workingArea.Y + verticalOffset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -202,7 +202,6 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
|
|||||||
string.Equals(option.Value, normalizedRenderMode, StringComparison.OrdinalIgnoreCase))
|
string.Equals(option.Value, normalizedRenderMode, StringComparison.OrdinalIgnoreCase))
|
||||||
?? RenderModes[0];
|
?? RenderModes[0];
|
||||||
EnableSlideTransition = appSnapshot.EnableSlideTransition;
|
EnableSlideTransition = appSnapshot.EnableSlideTransition;
|
||||||
ShowInTaskbar = appSnapshot.ShowInTaskbar;
|
|
||||||
_isInitializing = false;
|
_isInitializing = false;
|
||||||
|
|
||||||
RefreshPreview();
|
RefreshPreview();
|
||||||
@@ -239,11 +238,6 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
|
|||||||
{
|
{
|
||||||
EnableSlideTransition = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App).EnableSlideTransition;
|
EnableSlideTransition = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App).EnableSlideTransition;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedKeys.Contains(nameof(AppSettingsSnapshot.ShowInTaskbar)))
|
|
||||||
{
|
|
||||||
ShowInTaskbar = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App).ShowInTaskbar;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public event Action? RestartRequested;
|
public event Action? RestartRequested;
|
||||||
@@ -266,9 +260,6 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _enableSlideTransition;
|
private bool _enableSlideTransition;
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private bool _showInTaskbar;
|
|
||||||
|
|
||||||
public bool IsSlideTransitionAvailable => System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
|
public bool IsSlideTransitionAvailable => System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
@@ -376,12 +367,6 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
|
|||||||
SaveField(nameof(AppSettingsSnapshot.EnableSlideTransition), value);
|
SaveField(nameof(AppSettingsSnapshot.EnableSlideTransition), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnShowInTaskbarChanged(bool value)
|
|
||||||
{
|
|
||||||
if (_isInitializing) return;
|
|
||||||
SaveField(nameof(AppSettingsSnapshot.ShowInTaskbar), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SaveField<T>(string key, T value)
|
private void SaveField<T>(string key, T value)
|
||||||
{
|
{
|
||||||
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||||
|
|||||||
@@ -256,14 +256,18 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
private void OnFindMoreComponentsClick(object? sender, RoutedEventArgs e)
|
private void OnFindMoreComponentsClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// 打开设置窗口并导航到插件目录页面
|
// 打开设置窗口并导航到插件目录页面
|
||||||
if (Application.Current is App app)
|
if (Application.Current is App app && app.SettingsWindowService is { } settingsWindowService)
|
||||||
{
|
{
|
||||||
app.OpenIndependentSettingsModule("FusedDesktopComponentLibrary", "plugin-catalog");
|
var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow;
|
||||||
|
var request = new SettingsWindowOpenRequest(
|
||||||
|
Source: "FusedDesktopComponentLibrary",
|
||||||
|
Owner: mainWindow,
|
||||||
|
PageId: "plugin-catalog");
|
||||||
|
settingsWindowService.Open(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭所在窗口
|
// 关闭所在窗口
|
||||||
var window = this.FindAncestorOfType<Window>();
|
var window = this.FindAncestorOfType<Window>();
|
||||||
var componentLibraryWindow = this.FindAncestorOfType<Window>();
|
window?.Close();
|
||||||
componentLibraryWindow?.Close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ using LanMountainDesktop.DesktopEditing;
|
|||||||
using LanMountainDesktop.Host.Abstractions;
|
using LanMountainDesktop.Host.Abstractions;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
|
using LanMountainDesktop.Services.Settings;
|
||||||
using LanMountainDesktop.Settings.Core;
|
using LanMountainDesktop.Settings.Core;
|
||||||
using LanMountainDesktop.Theme;
|
using LanMountainDesktop.Theme;
|
||||||
using LanMountainDesktop.Views.Components;
|
using LanMountainDesktop.Views.Components;
|
||||||
@@ -281,7 +282,16 @@ public partial class MainWindow
|
|||||||
CloseComponentLibraryWindow(reopenSettings: false);
|
CloseComponentLibraryWindow(reopenSettings: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
(Application.Current as App)?.OpenIndependentSettingsModule("MainWindowTaskbar");
|
var app = Application.Current as App;
|
||||||
|
if (app?.SettingsWindowService is { } settingsWindowService)
|
||||||
|
{
|
||||||
|
settingsWindowService.Toggle(new SettingsWindowOpenRequest(
|
||||||
|
Source: "MainWindowTaskbar",
|
||||||
|
Owner: this));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app?.OpenIndependentSettingsModule("MainWindowTaskbar");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPowerMenuEnterClick(object? sender, RoutedEventArgs e)
|
private void OnPowerMenuEnterClick(object? sender, RoutedEventArgs e)
|
||||||
@@ -2851,6 +2861,34 @@ public partial class MainWindow
|
|||||||
CloseDetachedComponentLibraryWindow();
|
CloseDetachedComponentLibraryWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryGetSettingsWindowAnchorBounds(out PixelRect anchorBounds)
|
||||||
|
{
|
||||||
|
anchorBounds = default;
|
||||||
|
if (!IsVisible || BottomTaskbarContainer is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var origin = BottomTaskbarContainer.TranslatePoint(new Point(0, 0), this);
|
||||||
|
if (origin is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scale = RenderScaling > 0 ? RenderScaling : 1d;
|
||||||
|
var width = (int)Math.Round(BottomTaskbarContainer.Bounds.Width * scale);
|
||||||
|
var height = (int)Math.Round(BottomTaskbarContainer.Bounds.Height * scale);
|
||||||
|
if (width <= 0 || height <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var x = Position.X + (int)Math.Round(origin.Value.X * scale);
|
||||||
|
var y = Position.Y + (int)Math.Round(origin.Value.Y * scale);
|
||||||
|
anchorBounds = new PixelRect(x, y, width, height);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private void CollapseComponentLibraryPanel()
|
private void CollapseComponentLibraryPanel()
|
||||||
{
|
{
|
||||||
// Animate component library panel collapsing downward
|
// Animate component library panel collapsing downward
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ public partial class MainWindow
|
|||||||
string.Equals(key, nameof(AppSettingsSnapshot.UpdateDownloadSource), StringComparison.OrdinalIgnoreCase) ||
|
string.Equals(key, nameof(AppSettingsSnapshot.UpdateDownloadSource), StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(key, nameof(AppSettingsSnapshot.UpdateDownloadThreads), StringComparison.OrdinalIgnoreCase) ||
|
string.Equals(key, nameof(AppSettingsSnapshot.UpdateDownloadThreads), StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(key, nameof(AppSettingsSnapshot.EnableThreeFingerSwipe), StringComparison.OrdinalIgnoreCase) ||
|
string.Equals(key, nameof(AppSettingsSnapshot.EnableThreeFingerSwipe), StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(key, nameof(AppSettingsSnapshot.ShowInTaskbar), StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
string.Equals(key, nameof(AppSettingsSnapshot.EnableSlideTransition), StringComparison.OrdinalIgnoreCase)))
|
string.Equals(key, nameof(AppSettingsSnapshot.EnableSlideTransition), StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -689,10 +688,6 @@ public partial class MainWindow
|
|||||||
StatusBarShadowEnabled = _statusBarShadowEnabled,
|
StatusBarShadowEnabled = _statusBarShadowEnabled,
|
||||||
StatusBarShadowColor = _statusBarShadowColor,
|
StatusBarShadowColor = _statusBarShadowColor,
|
||||||
StatusBarShadowOpacity = _statusBarShadowOpacity,
|
StatusBarShadowOpacity = _statusBarShadowOpacity,
|
||||||
EnableThreeFingerSwipe = existingSnapshot.EnableThreeFingerSwipe,
|
|
||||||
EnableSlideTransition = existingSnapshot.EnableSlideTransition,
|
|
||||||
ShowInTaskbar = existingSnapshot.ShowInTaskbar,
|
|
||||||
EnableFusedDesktop = existingSnapshot.EnableFusedDesktop,
|
|
||||||
DisabledPluginIds = existingSnapshot.DisabledPluginIds,
|
DisabledPluginIds = existingSnapshot.DisabledPluginIds,
|
||||||
StudyFrameMs = existingSnapshot.StudyFrameMs,
|
StudyFrameMs = existingSnapshot.StudyFrameMs,
|
||||||
StudyScoreThresholdDbfs = existingSnapshot.StudyScoreThresholdDbfs,
|
StudyScoreThresholdDbfs = existingSnapshot.StudyScoreThresholdDbfs,
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
UseLayoutRounding="True"
|
UseLayoutRounding="True"
|
||||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
TransparencyLevelHint="Transparent"
|
|
||||||
Title="LanMountainDesktop">
|
Title="LanMountainDesktop">
|
||||||
|
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
@@ -100,17 +99,12 @@
|
|||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch">
|
VerticalAlignment="Stretch">
|
||||||
<Grid.RenderTransform>
|
<Grid.RenderTransform>
|
||||||
<TranslateTransform>
|
<TranslateTransform />
|
||||||
<TranslateTransform.Transitions>
|
|
||||||
<Transitions>
|
|
||||||
<DoubleTransition Property="X" Duration="{StaticResource FluttermotionToken.Duration.Intro}" Easing="0.05,0.75,0.10,1.00" />
|
|
||||||
</Transitions>
|
|
||||||
</TranslateTransform.Transitions>
|
|
||||||
</TranslateTransform>
|
|
||||||
</Grid.RenderTransform>
|
</Grid.RenderTransform>
|
||||||
<Grid.Transitions>
|
<Grid.Transitions>
|
||||||
<Transitions>
|
<Transitions>
|
||||||
<DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Page}" Easing="0.05,0.75,0.10,1.00" />
|
<DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Page}" Easing="0.05,0.75,0.10,1.00" />
|
||||||
|
<DoubleTransition Property="TranslateTransform.X" Duration="{StaticResource FluttermotionToken.Duration.Intro}" Easing="0.05,0.75,0.10,1.00" />
|
||||||
</Transitions>
|
</Transitions>
|
||||||
</Grid.Transitions>
|
</Grid.Transitions>
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ using LanMountainDesktop.Views.Components;
|
|||||||
|
|
||||||
namespace LanMountainDesktop.Views;
|
namespace LanMountainDesktop.Views;
|
||||||
|
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||||
{
|
{
|
||||||
private enum WallpaperMediaType
|
private enum WallpaperMediaType
|
||||||
{
|
{
|
||||||
@@ -450,8 +450,6 @@ public partial class MainWindow : Window
|
|||||||
MinShortSideCells,
|
MinShortSideCells,
|
||||||
MaxShortSideCells);
|
MaxShortSideCells);
|
||||||
|
|
||||||
ShowInTaskbar = snapshot.ShowInTaskbar;
|
|
||||||
|
|
||||||
_gridSpacingPreset = _gridSettingsService.NormalizeSpacingPreset(snapshot.GridSpacingPreset);
|
_gridSpacingPreset = _gridSettingsService.NormalizeSpacingPreset(snapshot.GridSpacingPreset);
|
||||||
|
|
||||||
_desktopEdgeInsetPercent = Math.Clamp(snapshot.DesktopEdgeInsetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
|
_desktopEdgeInsetPercent = Math.Clamp(snapshot.DesktopEdgeInsetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
|
||||||
@@ -886,19 +884,7 @@ public partial class MainWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var snapshot = _settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
|
||||||
if (snapshot.ShowInTaskbar)
|
|
||||||
{
|
|
||||||
WindowState = WindowState.Minimized;
|
WindowState = WindowState.Minimized;
|
||||||
}
|
|
||||||
else if (Application.Current is App app)
|
|
||||||
{
|
|
||||||
app.HideMainWindowToTray(this, "MinimizeAction");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
WindowState = WindowState.Minimized;
|
|
||||||
}
|
|
||||||
|
|
||||||
slideTransform.X = 0;
|
slideTransform.X = 0;
|
||||||
DesktopPage.Opacity = 1;
|
DesktopPage.Opacity = 1;
|
||||||
@@ -920,8 +906,7 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
if (useSlide)
|
if (useSlide)
|
||||||
{
|
{
|
||||||
var screenWidth = Screens.ScreenFromVisual(this)?.Bounds.Width ?? 3840;
|
slideTransform.X = Bounds.Width > 0 ? Bounds.Width : 1920;
|
||||||
slideTransform.X = Bounds.Width > 0 ? Bounds.Width : screenWidth;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DesktopPage.Transitions = savedTransitions;
|
DesktopPage.Transitions = savedTransitions;
|
||||||
@@ -956,27 +941,7 @@ public partial class MainWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var newState = (WindowState)e.NewValue!;
|
if (WindowState is WindowState.Minimized or WindowState.FullScreen)
|
||||||
var oldState = (WindowState)e.OldValue!;
|
|
||||||
|
|
||||||
if (oldState == WindowState.Minimized && newState != WindowState.Minimized)
|
|
||||||
{
|
|
||||||
PrepareEnterAnimation();
|
|
||||||
|
|
||||||
if (newState != WindowState.FullScreen)
|
|
||||||
{
|
|
||||||
WindowState = WindowState.FullScreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
PlayEnterAnimation();
|
|
||||||
}, DispatcherPriority.Background);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newState is WindowState.Minimized or WindowState.FullScreen)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,16 +117,6 @@
|
|||||||
</ui:SettingsExpander.Footer>
|
</ui:SettingsExpander.Footer>
|
||||||
</ui:SettingsExpander>
|
</ui:SettingsExpander>
|
||||||
|
|
||||||
<ui:SettingsExpander Header="桌面主窗口在任务栏显示图标"
|
|
||||||
Description="仅控制桌面主窗口在系统任务栏中的图标显示;不会影响设置窗口,设置窗口打开时始终保留独立任务栏图标">
|
|
||||||
<ui:SettingsExpander.IconSource>
|
|
||||||
<fi:SymbolIconSource Symbol="Window" />
|
|
||||||
</ui:SettingsExpander.IconSource>
|
|
||||||
<ui:SettingsExpander.Footer>
|
|
||||||
<ToggleSwitch IsChecked="{Binding ShowInTaskbar}" />
|
|
||||||
</ui:SettingsExpander.Footer>
|
|
||||||
</ui:SettingsExpander>
|
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ public sealed class LoadedPlugin : IDisposable, IAsyncDisposable
|
|||||||
IReadOnlyList<PluginDesktopComponentRegistration> desktopComponents,
|
IReadOnlyList<PluginDesktopComponentRegistration> desktopComponents,
|
||||||
IReadOnlyList<PluginDesktopComponentEditorRegistration> desktopComponentEditors,
|
IReadOnlyList<PluginDesktopComponentEditorRegistration> desktopComponentEditors,
|
||||||
IReadOnlyList<PluginServiceExportDescriptor> exportedServices,
|
IReadOnlyList<PluginServiceExportDescriptor> exportedServices,
|
||||||
IReadOnlyList<PluginPublicIpcServiceDescriptor> publicIpcServices,
|
|
||||||
IReadOnlyList<IHostedService> hostedServices,
|
IReadOnlyList<IHostedService> hostedServices,
|
||||||
PluginLoadContext loadContext)
|
PluginLoadContext loadContext)
|
||||||
{
|
{
|
||||||
@@ -40,7 +39,6 @@ public sealed class LoadedPlugin : IDisposable, IAsyncDisposable
|
|||||||
DesktopComponents = desktopComponents;
|
DesktopComponents = desktopComponents;
|
||||||
DesktopComponentEditors = desktopComponentEditors;
|
DesktopComponentEditors = desktopComponentEditors;
|
||||||
ExportedServices = exportedServices;
|
ExportedServices = exportedServices;
|
||||||
PublicIpcServices = publicIpcServices;
|
|
||||||
HostedServices = hostedServices;
|
HostedServices = hostedServices;
|
||||||
LoadContext = loadContext;
|
LoadContext = loadContext;
|
||||||
}
|
}
|
||||||
@@ -69,8 +67,6 @@ public sealed class LoadedPlugin : IDisposable, IAsyncDisposable
|
|||||||
|
|
||||||
public IReadOnlyList<PluginServiceExportDescriptor> ExportedServices { get; }
|
public IReadOnlyList<PluginServiceExportDescriptor> ExportedServices { get; }
|
||||||
|
|
||||||
public IReadOnlyList<PluginPublicIpcServiceDescriptor> PublicIpcServices { get; }
|
|
||||||
|
|
||||||
public PluginLoadContext LoadContext { get; }
|
public PluginLoadContext LoadContext { get; }
|
||||||
|
|
||||||
private IReadOnlyList<IHostedService> HostedServices { get; }
|
private IReadOnlyList<IHostedService> HostedServices { get; }
|
||||||
|
|||||||
@@ -14,10 +14,8 @@ using System.Threading.Tasks;
|
|||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
using LanMountainDesktop.Services.Settings;
|
using LanMountainDesktop.Services.Settings;
|
||||||
using LanMountainDesktop.PluginSdk;
|
using LanMountainDesktop.PluginSdk;
|
||||||
using LanMountainDesktop.Shared.IPC;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using dotnetCampus.Ipc.CompilerServices.Attributes;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Plugins;
|
namespace LanMountainDesktop.Plugins;
|
||||||
|
|
||||||
@@ -189,10 +187,9 @@ public sealed class PluginLoader
|
|||||||
.OrderBy(editor => editor.ComponentId, StringComparer.OrdinalIgnoreCase)
|
.OrderBy(editor => editor.ComponentId, StringComparer.OrdinalIgnoreCase)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
var exportedServices = ResolveExports(manifest, pluginServices);
|
var exportedServices = ResolveExports(manifest, pluginServices);
|
||||||
var publicIpcServices = ResolvePublicIpcServices(manifest, pluginServices);
|
|
||||||
AppLogger.Info(
|
AppLogger.Info(
|
||||||
"PluginLoader",
|
"PluginLoader",
|
||||||
$"Plugin contributions resolved. PluginId='{manifest.Id}'; SettingsSections={settingsSections.Length}; Widgets={desktopComponents.Length}; Editors={desktopComponentEditors.Length}; Exports={exportedServices.Count}; PublicIpcServices={publicIpcServices.Count}.");
|
$"Plugin contributions resolved. PluginId='{manifest.Id}'; SettingsSections={settingsSections.Length}; Widgets={desktopComponents.Length}; Editors={desktopComponentEditors.Length}; Exports={exportedServices.Count}.");
|
||||||
hostedServices = pluginServices.GetServices<IHostedService>().ToArray();
|
hostedServices = pluginServices.GetServices<IHostedService>().ToArray();
|
||||||
StartHostedServices(hostedServices);
|
StartHostedServices(hostedServices);
|
||||||
AppLogger.Info("PluginLoader", $"Hosted services started. PluginId='{manifest.Id}'; HostedServices={hostedServices.Count}.");
|
AppLogger.Info("PluginLoader", $"Hosted services started. PluginId='{manifest.Id}'; HostedServices={hostedServices.Count}.");
|
||||||
@@ -209,7 +206,6 @@ public sealed class PluginLoader
|
|||||||
desktopComponents,
|
desktopComponents,
|
||||||
desktopComponentEditors,
|
desktopComponentEditors,
|
||||||
exportedServices,
|
exportedServices,
|
||||||
publicIpcServices,
|
|
||||||
hostedServices,
|
hostedServices,
|
||||||
loadContext);
|
loadContext);
|
||||||
|
|
||||||
@@ -336,7 +332,6 @@ public sealed class PluginLoader
|
|||||||
RegisterHostService<ISettingsService>(services, hostServices);
|
RegisterHostService<ISettingsService>(services, hostServices);
|
||||||
RegisterHostService<ISettingsCatalog>(services, hostServices);
|
RegisterHostService<ISettingsCatalog>(services, hostServices);
|
||||||
RegisterHostService<IAppearanceThemeService>(services, hostServices);
|
RegisterHostService<IAppearanceThemeService>(services, hostServices);
|
||||||
RegisterHostService<IExternalIpcNotificationPublisher>(services, hostServices);
|
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
@@ -418,68 +413,6 @@ public sealed class PluginLoader
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<PluginPublicIpcServiceDescriptor> ResolvePublicIpcServices(
|
|
||||||
PluginManifest manifest,
|
|
||||||
IServiceProvider services)
|
|
||||||
{
|
|
||||||
var descriptors = new List<PluginPublicIpcServiceDescriptor>();
|
|
||||||
var seenKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
foreach (var registration in services.GetServices<PluginPublicIpcServiceRegistration>())
|
|
||||||
{
|
|
||||||
var implementation = services.GetService(registration.ContractType)
|
|
||||||
?? throw new InvalidOperationException(
|
|
||||||
$"Plugin '{manifest.Id}' registered public IPC contract '{registration.ContractType.FullName}', but no singleton service instance was found.");
|
|
||||||
|
|
||||||
AddDescriptor(registration.ContractType, implementation, registration.ObjectId, registration.NotifyIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
var builder = new RuntimePluginPublicIpcBuilder(services, AddDescriptor);
|
|
||||||
foreach (var contributor in services.GetServices<IPluginPublicIpcContributor>())
|
|
||||||
{
|
|
||||||
contributor.ConfigurePublicIpc(builder);
|
|
||||||
}
|
|
||||||
|
|
||||||
return descriptors;
|
|
||||||
|
|
||||||
void AddDescriptor(Type contractType, object implementation, string? objectId, IEnumerable<string>? notifyIds)
|
|
||||||
{
|
|
||||||
EnsurePublicIpcContract(manifest, contractType);
|
|
||||||
|
|
||||||
var normalizedObjectId = objectId ?? string.Empty;
|
|
||||||
var dedupeKey = $"{contractType.AssemblyQualifiedName}::{normalizedObjectId}";
|
|
||||||
if (!seenKeys.Add(dedupeKey))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Plugin '{manifest.Id}' registered duplicate public IPC contract '{contractType.FullName}' with object id '{normalizedObjectId}'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
descriptors.Add(new PluginPublicIpcServiceDescriptor(
|
|
||||||
contractType,
|
|
||||||
implementation,
|
|
||||||
string.IsNullOrEmpty(normalizedObjectId) ? null : normalizedObjectId,
|
|
||||||
notifyIds?
|
|
||||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToArray() ?? []));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void EnsurePublicIpcContract(PluginManifest manifest, Type contractType)
|
|
||||||
{
|
|
||||||
if (!contractType.IsInterface)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Plugin '{manifest.Id}' public IPC contract '{contractType.FullName}' must be an interface.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Attribute.IsDefined(contractType, typeof(IpcPublicAttribute), inherit: false))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Plugin '{manifest.Id}' public IPC contract '{contractType.FullName}' must be marked with '{nameof(IpcPublicAttribute)}'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsSupportedExportContract(PluginManifest manifest, Type contractType)
|
private static bool IsSupportedExportContract(PluginManifest manifest, Type contractType)
|
||||||
{
|
{
|
||||||
if (contractType.Assembly == typeof(IPlugin).Assembly)
|
if (contractType.Assembly == typeof(IPlugin).Assembly)
|
||||||
@@ -1141,42 +1074,4 @@ public sealed class PluginLoader
|
|||||||
string SourcePath,
|
string SourcePath,
|
||||||
PluginManifest Manifest,
|
PluginManifest Manifest,
|
||||||
PluginSourceKind SourceKind);
|
PluginSourceKind SourceKind);
|
||||||
|
|
||||||
private sealed class RuntimePluginPublicIpcBuilder : IPluginPublicIpcBuilder
|
|
||||||
{
|
|
||||||
private readonly IServiceProvider _services;
|
|
||||||
private readonly Action<Type, object, string?, IEnumerable<string>?> _register;
|
|
||||||
|
|
||||||
public RuntimePluginPublicIpcBuilder(
|
|
||||||
IServiceProvider services,
|
|
||||||
Action<Type, object, string?, IEnumerable<string>?> register)
|
|
||||||
{
|
|
||||||
_services = services;
|
|
||||||
_register = register;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IPluginPublicIpcBuilder AddService<TContract>(
|
|
||||||
string? objectId = null,
|
|
||||||
IEnumerable<string>? notifyIds = null)
|
|
||||||
where TContract : class
|
|
||||||
{
|
|
||||||
var implementation = _services.GetService(typeof(TContract))
|
|
||||||
?? throw new InvalidOperationException(
|
|
||||||
$"Plugin public IPC contributor requested contract '{typeof(TContract).FullName}', but no singleton service was registered.");
|
|
||||||
_register(typeof(TContract), implementation, objectId, notifyIds);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IPluginPublicIpcBuilder AddService(
|
|
||||||
Type contractType,
|
|
||||||
object implementation,
|
|
||||||
string? objectId = null,
|
|
||||||
IEnumerable<string>? notifyIds = null)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(contractType);
|
|
||||||
ArgumentNullException.ThrowIfNull(implementation);
|
|
||||||
_register(contractType, implementation, objectId, notifyIds);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ using LanMountainDesktop.Models;
|
|||||||
using LanMountainDesktop.Plugins;
|
using LanMountainDesktop.Plugins;
|
||||||
using LanMountainDesktop.PluginSdk;
|
using LanMountainDesktop.PluginSdk;
|
||||||
using LanMountainDesktop.Services.Settings;
|
using LanMountainDesktop.Services.Settings;
|
||||||
using LanMountainDesktop.Shared.IPC;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
@@ -32,7 +31,6 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
private readonly IPluginPackageManager _packageManager;
|
private readonly IPluginPackageManager _packageManager;
|
||||||
private readonly ISettingsFacadeService _settingsFacade;
|
private readonly ISettingsFacadeService _settingsFacade;
|
||||||
private readonly SettingsCatalogService _settingsCatalogService;
|
private readonly SettingsCatalogService _settingsCatalogService;
|
||||||
private readonly PublicIpcHostService? _publicIpcHostService;
|
|
||||||
private readonly List<LoadedPlugin> _loadedPlugins = [];
|
private readonly List<LoadedPlugin> _loadedPlugins = [];
|
||||||
private readonly List<PluginLoadResult> _loadResults = [];
|
private readonly List<PluginLoadResult> _loadResults = [];
|
||||||
private readonly List<PluginCatalogEntry> _catalog = [];
|
private readonly List<PluginCatalogEntry> _catalog = [];
|
||||||
@@ -41,16 +39,13 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
private readonly List<PluginDesktopComponentEditorContribution> _desktopComponentEditors = [];
|
private readonly List<PluginDesktopComponentEditorContribution> _desktopComponentEditors = [];
|
||||||
private readonly object _packageMutationGate = new();
|
private readonly object _packageMutationGate = new();
|
||||||
|
|
||||||
public PluginRuntimeService(
|
public PluginRuntimeService(ISettingsFacadeService? settingsFacade = null)
|
||||||
ISettingsFacadeService? settingsFacade = null,
|
|
||||||
PublicIpcHostService? publicIpcHostService = null)
|
|
||||||
{
|
{
|
||||||
PluginsDirectory = Path.Combine(GetUserDataRootDirectory(), "Extensions", "Plugins");
|
PluginsDirectory = Path.Combine(GetUserDataRootDirectory(), "Extensions", "Plugins");
|
||||||
_sharedContractManager = new PluginSharedContractManager(
|
_sharedContractManager = new PluginSharedContractManager(
|
||||||
Path.Combine(GetUserDataRootDirectory(), "PluginMarket"));
|
Path.Combine(GetUserDataRootDirectory(), "PluginMarket"));
|
||||||
_packageManager = new PluginRuntimePackageManager(this);
|
_packageManager = new PluginRuntimePackageManager(this);
|
||||||
_settingsFacade = settingsFacade ?? new SettingsFacadeService();
|
_settingsFacade = settingsFacade ?? new SettingsFacadeService();
|
||||||
_publicIpcHostService = publicIpcHostService;
|
|
||||||
_settingsCatalogService = _settingsFacade.Catalog as SettingsCatalogService
|
_settingsCatalogService = _settingsFacade.Catalog as SettingsCatalogService
|
||||||
?? new SettingsCatalogService();
|
?? new SettingsCatalogService();
|
||||||
if (_settingsFacade is SettingsFacadeService concreteFacade)
|
if (_settingsFacade is SettingsFacadeService concreteFacade)
|
||||||
@@ -63,8 +58,7 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
_exportRegistry,
|
_exportRegistry,
|
||||||
_settingsFacade,
|
_settingsFacade,
|
||||||
_settingsFacade.Settings,
|
_settingsFacade.Settings,
|
||||||
_settingsFacade.Catalog,
|
_settingsFacade.Catalog);
|
||||||
_publicIpcHostService);
|
|
||||||
_loaderOptions = CreateOptions();
|
_loaderOptions = CreateOptions();
|
||||||
_loader = new PluginLoader(_loaderOptions);
|
_loader = new PluginLoader(_loaderOptions);
|
||||||
}
|
}
|
||||||
@@ -681,8 +675,6 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
AddSharedAssembly(options, typeof(App).Assembly);
|
AddSharedAssembly(options, typeof(App).Assembly);
|
||||||
AddSharedAssembly(options, typeof(IServiceCollection).Assembly);
|
AddSharedAssembly(options, typeof(IServiceCollection).Assembly);
|
||||||
AddSharedAssembly(options, typeof(HostBuilderContext).Assembly);
|
AddSharedAssembly(options, typeof(HostBuilderContext).Assembly);
|
||||||
AddSharedAssembly(options, typeof(IExternalIpcNotificationPublisher).Assembly);
|
|
||||||
AddSharedAssembly(options, typeof(dotnetCampus.Ipc.Pipes.IpcProvider).Assembly);
|
|
||||||
|
|
||||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||||
{
|
{
|
||||||
@@ -769,19 +761,6 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
{
|
{
|
||||||
_desktopComponentEditors.Add(new PluginDesktopComponentEditorContribution(loadedPlugin, desktopComponentEditor));
|
_desktopComponentEditors.Add(new PluginDesktopComponentEditorContribution(loadedPlugin, desktopComponentEditor));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_publicIpcHostService is not null)
|
|
||||||
{
|
|
||||||
foreach (var publicIpcService in loadedPlugin.PublicIpcServices)
|
|
||||||
{
|
|
||||||
_publicIpcHostService.RegisterPublicService(
|
|
||||||
publicIpcService.ContractType,
|
|
||||||
publicIpcService.Implementation,
|
|
||||||
publicIpcService.ObjectId,
|
|
||||||
loadedPlugin.Manifest.Id,
|
|
||||||
publicIpcService.NotifyIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterSharedContractsForLoad(PluginManifest manifest)
|
private void RegisterSharedContractsForLoad(PluginManifest manifest)
|
||||||
@@ -1016,7 +995,6 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
private readonly ISettingsCatalog _settingsCatalog;
|
private readonly ISettingsCatalog _settingsCatalog;
|
||||||
private readonly IAppearanceThemeService _appearanceThemeService;
|
private readonly IAppearanceThemeService _appearanceThemeService;
|
||||||
private readonly IExternalIpcNotificationPublisher? _externalIpcNotificationPublisher;
|
|
||||||
|
|
||||||
public PluginHostServiceProvider(
|
public PluginHostServiceProvider(
|
||||||
IPluginPackageManager packageManager,
|
IPluginPackageManager packageManager,
|
||||||
@@ -1024,8 +1002,7 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
IPluginExportRegistry exportRegistry,
|
IPluginExportRegistry exportRegistry,
|
||||||
ISettingsFacadeService settingsFacade,
|
ISettingsFacadeService settingsFacade,
|
||||||
ISettingsService settingsService,
|
ISettingsService settingsService,
|
||||||
ISettingsCatalog settingsCatalog,
|
ISettingsCatalog settingsCatalog)
|
||||||
IExternalIpcNotificationPublisher? externalIpcNotificationPublisher)
|
|
||||||
{
|
{
|
||||||
_packageManager = packageManager;
|
_packageManager = packageManager;
|
||||||
_applicationLifecycle = applicationLifecycle;
|
_applicationLifecycle = applicationLifecycle;
|
||||||
@@ -1034,7 +1011,6 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
_settingsService = settingsService;
|
_settingsService = settingsService;
|
||||||
_settingsCatalog = settingsCatalog;
|
_settingsCatalog = settingsCatalog;
|
||||||
_appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate();
|
_appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate();
|
||||||
_externalIpcNotificationPublisher = externalIpcNotificationPublisher;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public object? GetService(Type serviceType)
|
public object? GetService(Type serviceType)
|
||||||
@@ -1074,11 +1050,6 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
return _appearanceThemeService;
|
return _appearanceThemeService;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serviceType == typeof(IExternalIpcNotificationPublisher))
|
|
||||||
{
|
|
||||||
return _externalIpcNotificationPublisher;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,16 +218,6 @@ Two new supporting packages define the isolation boundary:
|
|||||||
|
|
||||||
For the detailed design, migration path, UI strategy, and residual risks, see `docs/PLUGIN_PROCESS_ISOLATION_ARCHITECTURE.md`.
|
For the detailed design, migration path, UI strategy, and residual risks, see `docs/PLUGIN_PROCESS_ISOLATION_ARCHITECTURE.md`.
|
||||||
|
|
||||||
## External IPC Public API
|
|
||||||
|
|
||||||
- The current IPC mainline is external integration, not plugin process isolation.
|
|
||||||
- `LanMountainDesktop.Shared.IPC` is the unified IPC base for Host public services, Launcher/OOBE startup notifications, and plugin-contributed public services.
|
|
||||||
- Strongly typed command/query access uses `[IpcPublic]` contracts plus `dotnetCampus.Ipc` generated proxy/joint support.
|
|
||||||
- One-way events use `JsonIpcDirectRoutedProvider.NotifyAsync` with fixed top-level notify IDs.
|
|
||||||
- Host remains the single external IPC entry point even when a capability is contributed by a plugin.
|
|
||||||
|
|
||||||
See `docs/EXTERNAL_IPC_ARCHITECTURE.md` for the detailed contract and migration model.
|
|
||||||
|
|
||||||
## Launcher OOBE / Elevation Contract
|
## Launcher OOBE / Elevation Contract
|
||||||
|
|
||||||
- Launcher OOBE state is owned by a per-user JSON file under `%LOCALAPPDATA%\LanMountainDesktop\.launcher\state\oobe-state.json`.
|
- Launcher OOBE state is owned by a per-user JSON file under `%LOCALAPPDATA%\LanMountainDesktop\.launcher\state\oobe-state.json`.
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
# External IPC Architecture
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This document defines the current external integration IPC baseline for LanMountainDesktop.
|
|
||||||
|
|
||||||
- The delivery focus is external application integration, not plugin process isolation.
|
|
||||||
- `dotnetCampus.Ipc` is the single IPC foundation for Host public APIs, Launcher/OOBE startup notifications, and plugin-contributed external services.
|
|
||||||
- Process isolation remains a future track and stays documented in `docs/PLUGIN_PROCESS_ISOLATION_ARCHITECTURE.md`.
|
|
||||||
|
|
||||||
## Design Summary
|
|
||||||
|
|
||||||
The public IPC stack is split into two complementary layers:
|
|
||||||
|
|
||||||
1. Strongly typed public services
|
|
||||||
- Contracts are marked with `[IpcPublic]`.
|
|
||||||
- Host exposes service instances through `CreateIpcJoint<TContract>(instance)`.
|
|
||||||
- .NET clients connect once and obtain strong typed proxies through `CreateIpcProxy<TContract>(peer)`.
|
|
||||||
2. Routed notifications
|
|
||||||
- `JsonIpcDirectRoutedProvider.NotifyAsync` is used for one-way event delivery.
|
|
||||||
- Startup progress, loading-state updates, catalog changed events, and plugin live events all use routed notify IDs.
|
|
||||||
|
|
||||||
This keeps command/query calls explicit and strongly typed while still giving plugins and Launcher a lightweight event channel.
|
|
||||||
|
|
||||||
## Projects
|
|
||||||
|
|
||||||
- `LanMountainDesktop.Shared.IPC`
|
|
||||||
- Public IPC constants, routed notify IDs, DTOs, strong-typed public service contracts, host/client helpers, and DI registration helpers.
|
|
||||||
- `LanMountainDesktop`
|
|
||||||
- Runs `PublicIpcHostService`, exposes built-in public services, and folds plugin-contributed services into one external catalog.
|
|
||||||
- `LanMountainDesktop.Launcher`
|
|
||||||
- Connects to the Host public pipe and listens for startup and loading-state notifications instead of running a custom length-prefixed IPC server.
|
|
||||||
- `LanMountainDesktop.PluginSdk`
|
|
||||||
- Adds `IPluginPublicIpcContributor`, `IPluginPublicIpcBuilder`, and `AddPluginPublicIpc(...)`.
|
|
||||||
|
|
||||||
## Built-in Public Services
|
|
||||||
|
|
||||||
Current built-in `[IpcPublic]` contracts:
|
|
||||||
|
|
||||||
- `IPublicAppInfoService`
|
|
||||||
- Returns application metadata such as version, codename, process id, pipe name, and startup time.
|
|
||||||
- `IPublicShellControlService`
|
|
||||||
- Allows external .NET clients to activate the shell, open settings, request restart, and request exit.
|
|
||||||
- `IPublicPluginCatalogService`
|
|
||||||
- Returns the merged public IPC catalog snapshot exposed by Host.
|
|
||||||
|
|
||||||
## Routed Notify IDs
|
|
||||||
|
|
||||||
Current fixed routed notify IDs:
|
|
||||||
|
|
||||||
- `lanmountain.catalog.changed`
|
|
||||||
- `lanmountain.launcher.startup-progress`
|
|
||||||
- `lanmountain.launcher.loading-state`
|
|
||||||
|
|
||||||
The fixed routed surface is intentionally small. Runtime variation happens in the service catalog and in plugin-contributed service instances, not in ad-hoc top-level route registration after startup.
|
|
||||||
|
|
||||||
## Host Lifecycle
|
|
||||||
|
|
||||||
`PublicIpcHostService` is started during Host application startup and remains the single external IPC entry point.
|
|
||||||
|
|
||||||
Responsibilities:
|
|
||||||
|
|
||||||
- Start a named `dotnetCampus.Ipc` provider.
|
|
||||||
- Register fixed request routes before `StartServer()`.
|
|
||||||
- Expose built-in strong-typed public services.
|
|
||||||
- Maintain the merged service catalog.
|
|
||||||
- Publish startup and loading-state notifications to connected clients.
|
|
||||||
- Accept plugin-contributed public services after plugin load.
|
|
||||||
|
|
||||||
## Launcher / OOBE Migration
|
|
||||||
|
|
||||||
Launcher no longer depends on the previous custom named-pipe length-prefixed protocol as the primary path.
|
|
||||||
|
|
||||||
- Host publishes `StartupProgressMessage` through `lanmountain.launcher.startup-progress`.
|
|
||||||
- Host publishes `LoadingStateMessage` through `lanmountain.launcher.loading-state`.
|
|
||||||
- Launcher connects as a normal public IPC client and subscribes to those routed notifications.
|
|
||||||
|
|
||||||
This means Splash/OOBE is now just another IPC consumer on the same base transport used by external integrators.
|
|
||||||
|
|
||||||
## Plugin Public IPC Contribution Model
|
|
||||||
|
|
||||||
Plugins can contribute new external IPC services in two ways:
|
|
||||||
|
|
||||||
1. Declarative registration
|
|
||||||
- `services.AddPluginPublicIpc<TContract, TImplementation>(...)`
|
|
||||||
2. Advanced contributor
|
|
||||||
- Register `IPluginPublicIpcContributor`
|
|
||||||
- Use `IPluginPublicIpcBuilder` to contribute services from plugin DI
|
|
||||||
|
|
||||||
At plugin load time the Host runtime:
|
|
||||||
|
|
||||||
- discovers `PluginPublicIpcServiceRegistration`
|
|
||||||
- executes `IPluginPublicIpcContributor`
|
|
||||||
- validates that contributed contracts are `[IpcPublic]` interfaces
|
|
||||||
- registers the resolved instances into `PublicIpcHostService`
|
|
||||||
- emits `lanmountain.catalog.changed`
|
|
||||||
|
|
||||||
Plugins can also inject `IExternalIpcNotificationPublisher` and translate internal DI/message-bus events into routed notifications such as:
|
|
||||||
|
|
||||||
- `lanmountain.plugin.{pluginId}.attendance.updated`
|
|
||||||
- `lanmountain.plugin.{pluginId}.status.changed`
|
|
||||||
|
|
||||||
## Service Catalog
|
|
||||||
|
|
||||||
The public catalog is represented by `PublicIpcCatalogSnapshot` and includes:
|
|
||||||
|
|
||||||
- built-in and plugin-provided public services
|
|
||||||
- contract type metadata
|
|
||||||
- optional object id
|
|
||||||
- owning `pluginId` for plugin services
|
|
||||||
- declared notify IDs
|
|
||||||
- current loaded/enabled plugin list
|
|
||||||
|
|
||||||
This catalog is available through:
|
|
||||||
|
|
||||||
- strong-typed public service `IPublicPluginCatalogService`
|
|
||||||
- fixed request route `lanmountain.catalog.get`
|
|
||||||
- routed notify `lanmountain.catalog.changed`
|
|
||||||
|
|
||||||
## Current Limitations
|
|
||||||
|
|
||||||
- Strong-typed proxy/joint support is .NET-first.
|
|
||||||
- Plugin service removal is still restart-bound. New services can be added at runtime, but service removal is not yet modeled as a live unload contract.
|
|
||||||
- Cross-language clients still need a .NET bridge or sidecar if they want to consume `[IpcPublic]` contracts directly.
|
|
||||||
- Plugin process isolation is not part of this delivery. That remains future work.
|
|
||||||
@@ -559,13 +559,3 @@ var updateCheckService = new UpdateCheckService(
|
|||||||
- `apply-update`, `plugin-install`, and `debug-preview` must not auto-enter OOBE.
|
- `apply-update`, `plugin-install`, and `debug-preview` must not auto-enter OOBE.
|
||||||
- Allowed elevation paths are limited to the installer itself, full installer update application, and user-confirmed legacy uninstall.
|
- Allowed elevation paths are limited to the installer itself, full installer update application, and user-confirmed legacy uninstall.
|
||||||
- Default plugin installation targets the current user's LocalAppData scope and must not request elevation by default.
|
- Default plugin installation targets the current user's LocalAppData scope and must not request elevation by default.
|
||||||
|
|
||||||
## Public IPC Baseline
|
|
||||||
|
|
||||||
Launcher now consumes Host startup telemetry from the unified public IPC stack:
|
|
||||||
|
|
||||||
- Host publishes `StartupProgressMessage` via `lanmountain.launcher.startup-progress`
|
|
||||||
- Host publishes `LoadingStateMessage` via `lanmountain.launcher.loading-state`
|
|
||||||
- Launcher connects through `LanMountainDesktopIpcClient`
|
|
||||||
|
|
||||||
The previous custom length-prefixed named-pipe transport is no longer the primary startup communication path.
|
|
||||||
|
|||||||
@@ -684,34 +684,3 @@ if (!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
|||||||
- [组件开发指南](COMPONENT_DEVELOPMENT.md)
|
- [组件开发指南](COMPONENT_DEVELOPMENT.md)
|
||||||
- [API 参考](API_REFERENCE.md)
|
- [API 参考](API_REFERENCE.md)
|
||||||
- [架构文档](ARCHITECTURE.md)
|
- [架构文档](ARCHITECTURE.md)
|
||||||
## Public IPC Extension
|
|
||||||
|
|
||||||
Plugins can now contribute external IPC capabilities through the Host public IPC entry point.
|
|
||||||
|
|
||||||
Recommended registration styles:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
services.AddPluginPublicIpc<IMyPluginPublicService, MyPluginPublicService>(
|
|
||||||
objectId: "default",
|
|
||||||
notifyIds: ["lanmountain.plugin.my-plugin.status.changed"]);
|
|
||||||
```
|
|
||||||
|
|
||||||
Or use the advanced contributor model:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public sealed class MyPluginPublicIpcContributor : IPluginPublicIpcContributor
|
|
||||||
{
|
|
||||||
public void ConfigurePublicIpc(IPluginPublicIpcBuilder builder)
|
|
||||||
{
|
|
||||||
builder.AddService<IMyPluginPublicService>(
|
|
||||||
objectId: "default",
|
|
||||||
notifyIds: ["lanmountain.plugin.my-plugin.status.changed"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Additional notes:
|
|
||||||
|
|
||||||
- Public IPC contracts must be interfaces marked with `[IpcPublic]`.
|
|
||||||
- External .NET clients can reference the plugin contract assembly and create strong-typed proxies through the Host public pipe.
|
|
||||||
- Plugins can inject `IExternalIpcNotificationPublisher` to push live events outward through routed notifications.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user