feat.AirApp重构工程

This commit is contained in:
lincube
2026-08-11 23:03:55 +08:00
parent 2e57ae04b7
commit b6fde8188d
394 changed files with 4484 additions and 5120 deletions

View File

@@ -227,9 +227,9 @@ jobs:
if-no-files-found: warn
retention-days: 7
pack-plugin-packages:
pack-airapp-packages:
runs-on: ubuntu-latest
name: Pack_Plugin_Packages
name: Pack_AirApp_Packages
steps:
- name: Checkout
@@ -256,7 +256,7 @@ jobs:
shell: pwsh
run: .\scripts\Pack-PluginPackages.ps1 -Configuration Release -OutputPath .\artifacts\nuget
- name: Upload plugin package artifacts
- name: Upload AirApp package artifacts
uses: actions/upload-artifact@v7
with:
name: plugin-packages

View File

@@ -4,10 +4,10 @@
## 1. 项目目标与仓库边界
- 本仓库是阑山桌面桌面宿主、宿主侧插件运行时、Plugin SDK、共享契约与基础外观/设置能力的权威来源。
- 不要把插件市场元数据、开发者门户或官方示例插件实现当作本仓库内容维护。
- 本仓库是阑山桌面桌面宿主、宿主侧 AirApp 运行时、AirApp SDK、共享契约与基础外观/设置能力的权威来源。
- 不要把轻应用市场元数据、开发者门户或官方示例轻应用实现当作本仓库内容维护。
- 市场和生态材料属于兄弟仓库 `LanAirApp`
- 官方示例插件属于独立仓库 `LanMountainDesktop.SamplePlugin`
- 官方示例轻应用属于独立仓库 `LanMountainDesktop.SamplePlugin`
边界详情看:
@@ -16,14 +16,14 @@
## 2. 关键目录地图
- `desktop/LanMountainDesktop/`: 主宿主应用,包含 UI、服务、组件系统、主题与插件运行时接入
- `desktop/LanMountainDesktop/`: 主宿主应用,包含 UI、服务、组件系统、主题与 AirApp 运行时接入
- `desktop/LanMountainDesktop/ComponentSystem/`: 内置组件定义、注册、扩展加载
- `desktop/LanMountainDesktop/plugins/`: 宿主侧插件运行时、安装与 market 集成
- `desktop/LanMountainDesktop/plugins/`: 宿主侧 AirApp 运行时、安装与 market 集成
- `desktop/LanMountainDesktop/Views/` and `ViewModels/`: UI 页面、窗口与视图模型
- `desktop/LanMountainDesktop/Services/`: 设置、遥测、启动、持久化、业务服务
- `airapp/LanMountainDesktop.PluginSdk/`: 插件 SDK 公共接口和默认打包行为(含 PluginIsolation、Host.Abstractions、Settings.Core、Appearance
- `airapp/LanMountainDesktop.AirAppSdk/`: AirApp SDK 公共接口和默认打包行为(含隔离层与宿主桥接契约
- `platform/LanMountainDesktop.Platform/`: 平台差异层(接口 + Windows/macOS 实现)
- `core/LanMountainDesktop.Core/`: 宿主/插件共享契约、IPC 基础设施与插件打包
- `core/LanMountainDesktop.Core/`: 宿主/AirApp 共享契约、IPC 基础设施与打包
- `mobile/LanMountainDesktop.Mobile/`: 共享移动 UI 壳(组件面板)
- `mobile/LanMountainDesktop.Mobile.Android/`: Android head入口
- `tests/LanMountainDesktop.Tests/`: 宿主与 SDK 测试
@@ -40,7 +40,7 @@ dotnet run --project desktop/LanMountainDesktop/LanMountainDesktop.csproj
dotnet test LanMountainDesktop.slnx -c Debug
```
插件本地包生成:
AirApp 本地包生成:
```powershell
./scripts/Pack-PluginPackages.ps1
@@ -72,17 +72,17 @@ dotnet test LanMountainDesktop.slnx -c Debug
- 设置页相关改动通常同时落在 `Views/``ViewModels/``Services/``.trae/specs/`
- UI 启动与窗口生命周期主线在 `Program.cs``App.axaml.cs`
### 插件
### AirApp
- SDK 公共 API 以 `airapp/LanMountainDesktop.PluginSdk/` 为准
- SDK 公共 API 以 `airapp/LanMountainDesktop.AirAppSdk/` 为准
- 共享契约以 `core/LanMountainDesktop.Core/` 为准
- market 数据来源默认是兄弟仓库 `..\\LanAirApp`
- 迁移或 breaking change 优先同步 `docs/PLUGIN_SDK_V5_MIGRATION.md`
### 设置与主题
- 设置持久化和 scope 变化优先检查 `airapp/LanMountainDesktop.PluginSdk/`(合并自 `LanMountainDesktop.Settings.Core`
- 外观、圆角、主题资源优先检查 `airapp/LanMountainDesktop.PluginSdk/`(合并自 `LanMountainDesktop.Appearance`)与专题规范
- 设置持久化和 scope 变化优先检查 `airapp/LanMountainDesktop.AirAppSdk/`(合并自 `LanMountainDesktop.Settings.Core`
- 外观、圆角、主题资源优先检查 `airapp/LanMountainDesktop.AirAppSdk/`(合并自 `LanMountainDesktop.Appearance`)与专题规范
- **圆角统一**桌面组件Widget必须统一使用动态资源 `DesignCornerRadiusComponent`。严禁在组件根容器使用硬编码数值或非组件级令牌(如 `Xs`, `Md` 等),以确保全局圆角缩放设置能正确应用到所有组件。
## 6. 权威来源

View File

@@ -1,10 +1,11 @@
<Solution>
<Project Path="core/LanMountainDesktop.Core/LanMountainDesktop.Core.csproj" />
<Project Path="platform/LanMountainDesktop.Platform/LanMountainDesktop.Platform.csproj" />
<Project Path="airapp/LanMountainDesktop.PluginSdk/LanMountainDesktop.PluginSdk.csproj" />
<Project Path="airapp/LanMountainDesktop.AirAppSdk/LanMountainDesktop.AirAppSdk.csproj" />
<Project Path="airapp/LanMountainDesktop.AirAppRuntime/LanMountainDesktop.AirAppRuntime.csproj" />
<Project Path="airapp/LanMountainDesktop.AirAppHost/LanMountainDesktop.AirAppHost.csproj" />
<Project Path="airapp/LanMountainDesktop.PluginTemplate/LanMountainDesktop.PluginTemplate.csproj" />
<Project Path="airapp/LanMountainDesktop.AirAppDevServer/LanMountainDesktop.AirAppDevServer.csproj" />
<Project Path="airapp/LanMountainDesktop.AirAppTemplate/LanMountainDesktop.AirAppTemplate.csproj" />
<Project Path="desktop/LanMountainDesktop/LanMountainDesktop.csproj" />
<Project Path="desktop/LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj" />
<Project Path="install/LanDesktopPLONDS.installer/LanDesktopPLONDS.installer.csproj" />

View File

@@ -37,10 +37,10 @@
- 多页桌面,工作学习场景分离
- 主题切换、玻璃效果、圆角风格
### 🔌 插件生态
- 通过 `.laapp` 插件扩展功能
- 官方 Plugin SDK 支持自定义组件
- 设置页、组件、集成功能一站式接入
### 🔌 轻应用生态
- 通过 `.laapp` 轻应用扩展功能
- 官方 AirApp SDK 支持桌面组件与窗口轻应用
- 设置页、组件、窗口、集成功能一站式接入
## 为谁而设计
@@ -48,7 +48,7 @@
|---------|---------|
| 🎓 学生用户 | 课程表、自习监测、计时、天气和日常信息聚合 |
| 💼 办公用户 | 日历、资讯、最近文档、常用工具入口 |
| 🎨 效率爱好者 | 自由布局、主题切换、插件扩展 |
| 🎨 效率爱好者 | 自由布局、主题切换、轻应用扩展 |
| 🇨🇳 中文用户 | 本地化界面、农历和节假日等本地语境支持 |
## 快速开始
@@ -84,37 +84,36 @@ dotnet run --project LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csp
dotnet test LanMountainDesktop.slnx -c Debug
```
## 插件开发
## 轻应用开发
阑山桌面支持通过 Plugin SDK 开发自定义插件
阑山桌面通过统一的 AirApp SDK 支持轻应用扩展(桌面组件 + 窗口轻应用)
```bash
# 安装插件模板
dotnet new install LanMountainDesktop.PluginTemplate
# 安装轻应用模板
dotnet new install LanMountainDesktop.AirAppTemplate
# 创建新插件
dotnet new lmd-plugin -n MyPlugin
# 创建新轻应用
dotnet new lmd-airapp -n MyAirApp
```
- **Plugin SDK**: `LanMountainDesktop.PluginSdk` (API 6.0.0)
- **AirApp SDK**: `LanMountainDesktop.AirAppSdk` (API 1.0.0)
- **共享契约**: `LanMountainDesktop.Core`
- **迁移指南**: [PLUGIN_SDK_V5_MIGRATION.md](docs/PLUGIN_SDK_V5_MIGRATION.md)
- **迁移指南**: [AIRAPP_SDK_V1_MIGRATION.md](docs/AIRAPP_SDK_V1_MIGRATION.md)
## 项目结构
```
LanMountainDesktop/
├── core/ # 共享基础契约、IPC、插件打包
├── core/ # 共享基础契约、IPC、打包
│ └── LanMountainDesktop.Core/
├── platform/ # 平台差异层(接口 + Windows/macOS 实现)
│ └── LanMountainDesktop.Platform/
├── airappsdk/ # AirApp 第三方 SDK 与工具
── LanMountainDesktop.AirAppSdk/ (+ AirAppDevServer / AirAppTemplate)
├── airapp/ # 插件 SDK 与 AirApp 运行时
│ ├── LanMountainDesktop.PluginSdk/ # 官方插件 SDK
│ ├── LanMountainDesktop.AirAppRuntime/
── LanMountainDesktop.AirAppHost/
│ └── LanMountainDesktop.PluginTemplate/
├── airapp/ # AirApp 统一扩展系统
── LanMountainDesktop.AirAppSdk/ # 统一 SDK组件 + 窗口轻应用)
│ ├── LanMountainDesktop.AirAppRuntime/ # 轻应用运行时进程
│ ├── LanMountainDesktop.AirAppHost/ # 轻应用窗口宿主进程
│ ├── LanMountainDesktop.AirAppTemplate/ # dotnet new lmd-airapp 模板
── LanMountainDesktop.AirAppDevServer/ # 轻应用开发工具
├── desktop/ # 桌面宿主
│ ├── LanMountainDesktop/ # 桌面宿主应用
│ └── LanMountainDesktop.Launcher/ # 启动器 (OOBE、Splash、版本管理、更新)
@@ -131,15 +130,17 @@ LanMountainDesktop/
| 项目 | 职责 |
|-----|------|
| **本仓库** | 桌面宿主、插件运行时、Plugin SDK、共享契约 |
| [LanAirApp](https://github.com/yourorg/LanAirApp) | 插件市场元数据、开发者生态材料 |
| [LanMountainDesktop.SamplePlugin](https://github.com/yourorg/LanMountainDesktop.SamplePlugin) | 官方示例插件 |
| **本仓库** | 桌面宿主、AirApp 运行时、AirApp SDK、共享契约 |
| [LanAirApp](https://github.com/yourorg/LanAirApp) | 轻应用市场元数据、开发者生态材料 |
| [LanMountainDesktop.SamplePlugin](https://github.com/yourorg/LanMountainDesktop.SamplePlugin) | 官方示例轻应用 |
## 文档索引
- [产品定位](docs/PRODUCT.md) - 产品愿景与目标用户
- [架构说明](docs/ARCHITECTURE.md) - 仓库结构与运行时主线
- [开发指南](docs/DEVELOPMENT.md) - 构建、测试、调试
- [AirApp 开发指南](docs/01-AirApp开发/README.md) - 轻应用组件与窗口开发
- [AirApp 迁移指南](docs/AIRAPP_SDK_V1_MIGRATION.md) - 插件迁移到 AirApp
- [视觉规范](docs/VISUAL_SPEC.md) - 主题、颜色、玻璃层级
- [圆角规范](docs/CORNER_RADIUS_SPEC.md) - 圆角层级与动态规则
- [贡献指南](docs/CONTRIBUTING.md) - PR、spec、文档协作规则

View File

@@ -8,7 +8,8 @@ public sealed record AirAppLaunchOptions(
string? LauncherPipeName,
string? InstanceKey,
string? DataRoot,
string? TargetEntryId)
string? TargetEntryId,
string? AppPackagePath)
{
public const string WorldClockAppId = "world-clock";
public const string WhiteboardAppId = "whiteboard";
@@ -63,7 +64,8 @@ public sealed record AirAppLaunchOptions(
GetOptionalValue(values, "launcher-pipe"),
GetOptionalValue(values, "instance-key"),
GetOptionalValue(values, "data-root"),
GetOptionalValue(values, "target-entry-id"));
GetOptionalValue(values, "target-entry-id"),
GetOptionalValue(values, "app-package"));
}
private static string GetValue(IReadOnlyDictionary<string, string> values, string key, string fallback)

View File

@@ -17,6 +17,8 @@ public sealed partial class AirAppWindow : FAAppWindow
private readonly AirAppWindowDescriptor _descriptor;
private WhiteboardWidget? _whiteboardWidget;
private string _instanceKey = string.Empty;
private AirAppWindowLoader.LoadedAirAppWindow? _loadedAirAppWindow;
private LanMountainDesktop.AirAppSdk.IAirAppWindow? _thirdPartyWindow;
public AirAppWindow()
: this(AirAppLaunchOptions.Parse([]))
@@ -61,6 +63,12 @@ public sealed partial class AirAppWindow : FAAppWindow
private void ConfigureWindow()
{
if (!string.IsNullOrWhiteSpace(_options.AppPackagePath))
{
ConfigureThirdPartyWindow();
return;
}
ApplyWindowDescriptor(_descriptor);
if (string.Equals(_options.AppId, AirAppLaunchOptions.WorldClockAppId, StringComparison.OrdinalIgnoreCase))
@@ -88,6 +96,90 @@ public sealed partial class AirAppWindow : FAAppWindow
};
}
private void ConfigureThirdPartyWindow()
{
var loaded = new AirAppWindowLoader().Load(_options);
_loadedAirAppWindow = loaded;
_thirdPartyWindow = loaded.Window;
var window = loaded.Window;
ApplySdkWindowDescriptor(window.Descriptor);
var content = window.Content;
if (content is null)
{
throw new InvalidOperationException(
$"AirApp window '{_options.AppId}' exposes no Content. Set Content on the IAirAppWindow implementation.");
}
ContentHost.Content = content;
_ = window.OnWindowOpeningAsync();
AppLogger.Info(
"AirAppWindow",
$"Third-party AirApp window configured. AppId='{_options.AppId}'; TargetEntryId='{_options.TargetEntryId ?? string.Empty}'.");
}
private void ApplySdkWindowDescriptor(LanMountainDesktop.AirAppSdk.AirAppWindowDescriptor descriptor)
{
Title = string.IsNullOrWhiteSpace(descriptor.Title) ? _options.AppId : descriptor.Title;
Width = descriptor.Width;
Height = descriptor.Height;
MinWidth = descriptor.MinWidth;
MinHeight = descriptor.MinHeight;
ShowInTaskbar = descriptor.ShowInTaskbar;
CanResize = descriptor.CanResize;
ShowAsDialog = descriptor.ShowAsDialog;
WindowState = WindowState.Normal;
WindowRoot.Background = this.TryFindResource("AirAppWindowBackgroundBrush", out var brush) && brush is IBrush backgroundBrush
? backgroundBrush
: Brushes.White;
switch (descriptor.ChromeMode)
{
case LanMountainDesktop.AirAppSdk.AirAppWindowChromeMode.Standard:
WindowDecorations = WindowDecorations.Full;
TitleBar.ExtendsContentIntoTitleBar = false;
TitleBar.Height = 40;
break;
case LanMountainDesktop.AirAppSdk.AirAppWindowChromeMode.Borderless:
WindowDecorations = WindowDecorations.None;
TitleBar.ExtendsContentIntoTitleBar = true;
TitleBar.Height = 40;
break;
case LanMountainDesktop.AirAppSdk.AirAppWindowChromeMode.FullScreen:
WindowDecorations = WindowDecorations.None;
TitleBar.ExtendsContentIntoTitleBar = true;
ShowAsDialog = false;
WindowState = WindowState.FullScreen;
TitleBar.Height = 40;
break;
case LanMountainDesktop.AirAppSdk.AirAppWindowChromeMode.Tool:
WindowDecorations = WindowDecorations.Full;
TitleBar.ExtendsContentIntoTitleBar = false;
ShowInTaskbar = false;
CanResize = false;
TitleBar.Height = 36;
break;
case LanMountainDesktop.AirAppSdk.AirAppWindowChromeMode.BackgroundOnly:
// Reserved for future background-only Air APPs.
break;
}
TitleBar.BackgroundColor = Colors.Transparent;
TitleBar.ForegroundColor = Color.FromRgb(32, 32, 32);
TitleBar.InactiveBackgroundColor = Colors.Transparent;
TitleBar.InactiveForegroundColor = Color.FromRgb(96, 96, 96);
TitleBar.ButtonBackgroundColor = Colors.Transparent;
TitleBar.ButtonHoverBackgroundColor = Color.FromArgb(23, 0, 0, 0);
TitleBar.ButtonPressedBackgroundColor = Color.FromArgb(52, 0, 0, 0);
TitleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
TitleBar.ButtonInactiveForegroundColor = Colors.Gray;
}
private void ApplyWindowDescriptor(AirAppWindowDescriptor descriptor)
{
Title = descriptor.Title;
@@ -180,6 +272,7 @@ public sealed partial class AirAppWindow : FAAppWindow
{
base.OnOpened(e);
_ = RegisterWithLauncherAsync();
_thirdPartyWindow?.OnWindowOpened();
AppLogger.Info(
"AirAppWindow",
$"Opened. WindowRole=AirApp; AppId='{_options.AppId}'; ForegroundActivationRequested=True.");
@@ -192,16 +285,47 @@ public sealed partial class AirAppWindow : FAAppWindow
protected override void OnClosing(WindowClosingEventArgs e)
{
SaveWhiteboard();
_thirdPartyWindow?.OnWindowClosing(e);
base.OnClosing(e);
}
protected override void OnClosed(EventArgs e)
{
SaveAndDisposeWhiteboard();
DisposeLoadedAirAppWindow();
_ = UnregisterWithLauncherAsync();
base.OnClosed(e);
}
private void DisposeLoadedAirAppWindow()
{
var loaded = _loadedAirAppWindow;
if (loaded is null)
{
return;
}
_loadedAirAppWindow = null;
_thirdPartyWindow = null;
try
{
loaded.Window.OnWindowClosed();
}
catch (Exception ex)
{
AppLogger.Warn("AirAppWindow", "Failed to notify AirApp window closed.", ex);
}
try
{
loaded.LoadedAirApp.Dispose();
}
catch (Exception ex)
{
AppLogger.Warn("AirAppWindow", "Failed to dispose loaded AirApp window.", ex);
}
}
private void SaveAndDisposeWhiteboard()
{
var widget = _whiteboardWidget;

View File

@@ -0,0 +1,130 @@
using System;
using System.IO;
using System.Linq;
using LanMountainDesktop.AirAppSdk;
using LanMountainDesktop.AirApps;
using LanMountainDesktop.Services;
using Microsoft.Extensions.DependencyInjection;
namespace LanMountainDesktop.AirAppHost;
/// <summary>
/// Loads a third-party AirApp window into the AirAppHost process.
/// Reads the AirApp package manifest, loads the entrance assembly with the desktop
/// <see cref="AirAppLoader"/>, resolves the requested window by id, and produces the
/// window "blueprint" (content + descriptor) to be hosted inside the FAAppWindow shell.
/// </summary>
internal sealed class AirAppWindowLoader
{
/// <summary>
/// Result of loading a third-party AirApp window.
/// </summary>
internal sealed record LoadedAirAppWindow(
IAirAppWindow Window,
LoadedAirApp LoadedAirApp);
public LoadedAirAppWindow Load(AirAppLaunchOptions options)
{
ArgumentNullException.ThrowIfNull(options);
if (string.IsNullOrWhiteSpace(options.AppPackagePath))
{
throw new InvalidOperationException("AirAppWindowLoader requires an --app-package path.");
}
var packageDirectory = Path.GetFullPath(options.AppPackagePath);
if (!Directory.Exists(packageDirectory))
{
throw new InvalidOperationException($"AirApp package directory was not found: '{packageDirectory}'.");
}
var manifestPath = Path.Combine(packageDirectory, AirAppSdkInfo.ManifestFileName);
if (!File.Exists(manifestPath))
{
throw new InvalidOperationException(
$"AirApp package directory does not contain '{AirAppSdkInfo.ManifestFileName}': '{packageDirectory}'.");
}
var manifest = AirAppManifest.Load(manifestPath);
var assemblyPath = manifest.ResolveEntranceAssemblyPath(manifestPath);
if (!File.Exists(assemblyPath))
{
throw new InvalidOperationException($"AirApp entrance assembly was not found: '{assemblyPath}'.");
}
var loader = new AirAppLoader();
var loadResult = loader.LoadFromAssembly(assemblyPath, manifest);
if (!loadResult.IsSuccess || loadResult.LoadedAirApp is null)
{
throw new InvalidOperationException(
$"Failed to load AirApp '{manifest.Id}': {loadResult.Error?.Message ?? "unknown error"}",
loadResult.Error);
}
var loadedAirApp = loadResult.LoadedAirApp;
try
{
var windowId = ResolveWindowId(options, manifest, loadedAirApp);
var windowRegistration = loadedAirApp.Services
.GetServices<AirAppWindowRegistration>()
.FirstOrDefault(registration => string.Equals(registration.WindowId, windowId, StringComparison.OrdinalIgnoreCase));
if (windowRegistration is null)
{
throw new InvalidOperationException(
$"AirApp '{manifest.Id}' does not declare a window with id '{windowId}'. " +
"Register windows with AddAirAppWindow<TWindow> in Initialize().");
}
var window = CreateWindow(loadedAirApp, windowRegistration);
AppLogger.Info(
"AirAppWindow",
$"Loaded third-party AirApp window. AirAppId='{manifest.Id}'; WindowId='{windowId}'; WindowType='{windowRegistration.WindowType.FullName}'.");
return new LoadedAirAppWindow(window, loadedAirApp);
}
catch
{
loadedAirApp.Dispose();
throw;
}
}
private static string ResolveWindowId(
AirAppLaunchOptions options,
AirAppManifest manifest,
LoadedAirApp loadedAirApp)
{
if (!string.IsNullOrWhiteSpace(options.TargetEntryId))
{
return options.TargetEntryId.Trim();
}
var declaredWindows = loadedAirApp.Services.GetServices<AirAppWindowRegistration>().ToArray();
if (declaredWindows.Length == 1)
{
return declaredWindows[0].WindowId;
}
if (manifest.Windows is { Count: > 0 })
{
return manifest.Windows[0].Id;
}
throw new InvalidOperationException(
$"AirApp '{manifest.Id}' exposes no windows and no --target-entry-id was provided.");
}
private static IAirAppWindow CreateWindow(LoadedAirApp loadedAirApp, AirAppWindowRegistration registration)
{
var instance = loadedAirApp.Services.GetService(registration.WindowType);
instance ??= ActivatorUtilities.CreateInstance(loadedAirApp.Services, registration.WindowType);
if (instance is not IAirAppWindow window)
{
throw new InvalidOperationException(
$"Window type '{registration.WindowType.FullName}' does not implement IAirAppWindow.");
}
return window;
}
}

View File

@@ -43,7 +43,8 @@ internal sealed class AirAppLifecycleService : IAirAppLifecycleService
instanceKey,
request.SourceComponentId,
request.SourcePlacementId,
request.TargetEntryId);
request.TargetEntryId,
request.AppPackagePath);
if (process is null)
{
return Task.FromResult(BuildResult(false, "start_failed", "AirAppHost process was not created.", null));

View File

@@ -5,7 +5,7 @@ namespace LanMountainDesktop.AirAppRuntime;
internal interface IAirAppProcessStarter
{
Process? Start(string appId, string sessionId, string instanceKey, string? sourceComponentId, string? sourcePlacementId, string? targetEntryId = null);
Process? Start(string appId, string sessionId, string instanceKey, string? sourceComponentId, string? sourcePlacementId, string? targetEntryId = null, string? appPackagePath = null);
}
internal sealed class AirAppProcessStarter : IAirAppProcessStarter
@@ -33,7 +33,8 @@ internal sealed class AirAppProcessStarter : IAirAppProcessStarter
string instanceKey,
string? sourceComponentId,
string? sourcePlacementId,
string? targetEntryId = null)
string? targetEntryId = null,
string? appPackagePath = null)
{
var hostPath = _locator.Resolve(_packageRootProvider(), _hostPathProvider());
var startInfo = CreateStartInfo(hostPath);
@@ -63,6 +64,11 @@ internal sealed class AirAppProcessStarter : IAirAppProcessStarter
AddArgument(startInfo, "--target-entry-id", targetEntryId.Trim());
}
if (!string.IsNullOrWhiteSpace(appPackagePath))
{
AddArgument(startInfo, "--app-package", Path.GetFullPath(appPackagePath));
}
AirAppRuntimeLogger.Info(
$"Starting AirAppHost. AppId='{appId}'; InstanceKey='{instanceKey}'; HostPath='{hostPath}'; DataRoot='{dataRoot ?? string.Empty}'.");
var process = Process.Start(startInfo);

View File

@@ -1,17 +1,17 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
/// <summary>
/// 插件外观上下文实现,提供主题、圆角等外观资源的访问和变更通知。
/// </summary>
public sealed class PluginAppearanceContext : IPluginAppearanceContext
public sealed class AirAppAppearanceContext : IAirAppAppearanceContext
{
private PluginAppearanceSnapshot _snapshot;
private AirAppAppearanceSnapshot _snapshot;
/// <summary>
/// 创建插件外观上下文实例。
/// </summary>
/// <param name="snapshot">初始外观快照</param>
public PluginAppearanceContext(PluginAppearanceSnapshot snapshot)
public AirAppAppearanceContext(AirAppAppearanceSnapshot snapshot)
{
ArgumentNullException.ThrowIfNull(snapshot);
ArgumentNullException.ThrowIfNull(snapshot.CornerRadiusTokens);
@@ -25,7 +25,7 @@ public sealed class PluginAppearanceContext : IPluginAppearanceContext
}
/// <inheritdoc />
public PluginAppearanceSnapshot Snapshot => _snapshot;
public AirAppAppearanceSnapshot Snapshot => _snapshot;
/// <inheritdoc />
public event EventHandler<AppearanceChangedEvent>? Changed;
@@ -36,7 +36,7 @@ public sealed class PluginAppearanceContext : IPluginAppearanceContext
/// </summary>
/// <param name="newSnapshot">新的外观快照</param>
/// <param name="changedProperties">变更的属性集合</param>
public void UpdateSnapshot(PluginAppearanceSnapshot newSnapshot, IReadOnlyCollection<AppearanceProperty> changedProperties)
public void UpdateSnapshot(AirAppAppearanceSnapshot newSnapshot, IReadOnlyCollection<AppearanceProperty> changedProperties)
{
ArgumentNullException.ThrowIfNull(newSnapshot);
ArgumentNullException.ThrowIfNull(changedProperties);
@@ -69,7 +69,7 @@ public sealed class PluginAppearanceContext : IPluginAppearanceContext
}
/// <inheritdoc />
public double ResolveCornerRadius(PluginCornerRadiusPreset preset, double? minimum = null, double? maximum = null)
public double ResolveCornerRadius(AirAppCornerRadiusPreset preset, double? minimum = null, double? maximum = null)
{
var resolved = Math.Max(0d, _snapshot.CornerRadiusTokens.Get(preset));
if (!minimum.HasValue && !maximum.HasValue)

View File

@@ -1,12 +1,12 @@
using Avalonia;
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public static class PluginAppearanceExtensions
public static class AirAppAppearanceExtensions
{
public static CornerRadius ResolveCornerRadius(
this PluginAppearanceSnapshot snapshot,
PluginCornerRadiusPreset preset)
this AirAppAppearanceSnapshot snapshot,
AirAppCornerRadiusPreset preset)
{
ArgumentNullException.ThrowIfNull(snapshot);
var value = snapshot.CornerRadiusTokens.Get(preset);
@@ -14,8 +14,8 @@ public static class PluginAppearanceExtensions
}
public static CornerRadius ResolveCornerRadius(
this PluginAppearanceSnapshot snapshot,
PluginCornerRadiusPreset preset,
this AirAppAppearanceSnapshot snapshot,
AirAppCornerRadiusPreset preset,
CornerRadius fallback)
{
ArgumentNullException.ThrowIfNull(snapshot);
@@ -28,8 +28,8 @@ public static class PluginAppearanceExtensions
}
public static CornerRadius ResolveCornerRadius(
this IPluginAppearanceContext context,
PluginCornerRadiusPreset preset)
this IAirAppAppearanceContext context,
AirAppCornerRadiusPreset preset)
{
ArgumentNullException.ThrowIfNull(context);
var value = context.ResolveCornerRadius(preset);
@@ -37,8 +37,8 @@ public static class PluginAppearanceExtensions
}
public static CornerRadius ResolveCornerRadius(
this IPluginAppearanceContext context,
PluginCornerRadiusPreset preset,
this IAirAppAppearanceContext context,
AirAppCornerRadiusPreset preset,
double minimum,
double maximum)
{
@@ -48,7 +48,7 @@ public static class PluginAppearanceExtensions
}
public static CornerRadius ResolveScaledCornerRadius(
this IPluginAppearanceContext context,
this IAirAppAppearanceContext context,
double baseRadius)
{
ArgumentNullException.ThrowIfNull(context);
@@ -57,7 +57,7 @@ public static class PluginAppearanceExtensions
}
public static CornerRadius ResolveScaledCornerRadius(
this IPluginAppearanceContext context,
this IAirAppAppearanceContext context,
double baseRadius,
double minimum,
double maximum)
@@ -68,8 +68,8 @@ public static class PluginAppearanceExtensions
}
public static CornerRadius ResolveCornerRadius(
this PluginDesktopComponentContext context,
PluginCornerRadiusPreset preset,
this AirAppComponentContext context,
AirAppCornerRadiusPreset preset,
double minimum,
double maximum)
{
@@ -78,8 +78,8 @@ public static class PluginAppearanceExtensions
return new CornerRadius(Math.Max(0d, value));
}
public static PluginAppearanceSnapshot GetAppearanceSnapshot(
this PluginDesktopComponentContext context)
public static AirAppAppearanceSnapshot GetAppearanceSnapshot(
this AirAppComponentContext context)
{
ArgumentNullException.ThrowIfNull(context);
return context.Appearance.Snapshot;

View File

@@ -1,11 +1,11 @@
using Avalonia;
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
/// <summary>
/// 插件外观辅助方法,提供统一的圆角和主题资源访问。
/// </summary>
public static class PluginAppearanceHelper
public static class AirAppAppearanceHelper
{
/// <summary>
/// 获取桌面组件主外壳圆角半径。
@@ -13,10 +13,10 @@ public static class PluginAppearanceHelper
/// </summary>
/// <param name="context">外观上下文</param>
/// <returns>主外壳圆角半径(像素)</returns>
public static double GetShellCornerRadius(this IPluginAppearanceContext context)
public static double GetShellCornerRadius(this IAirAppAppearanceContext context)
{
ArgumentNullException.ThrowIfNull(context);
return context.ResolveCornerRadius(PluginCornerRadiusPreset.Component);
return context.ResolveCornerRadius(AirAppCornerRadiusPreset.Component);
}
/// <summary>
@@ -25,10 +25,10 @@ public static class PluginAppearanceHelper
/// </summary>
/// <param name="context">外观上下文</param>
/// <returns>内部卡片圆角半径(像素)</returns>
public static double GetCardCornerRadius(this IPluginAppearanceContext context)
public static double GetCardCornerRadius(this IAirAppAppearanceContext context)
{
ArgumentNullException.ThrowIfNull(context);
return context.ResolveCornerRadius(PluginCornerRadiusPreset.Sm);
return context.ResolveCornerRadius(AirAppCornerRadiusPreset.Sm);
}
/// <summary>
@@ -37,10 +37,10 @@ public static class PluginAppearanceHelper
/// </summary>
/// <param name="context">外观上下文</param>
/// <returns>控件圆角半径(像素)</returns>
public static double GetControlCornerRadius(this IPluginAppearanceContext context)
public static double GetControlCornerRadius(this IAirAppAppearanceContext context)
{
ArgumentNullException.ThrowIfNull(context);
return context.ResolveCornerRadius(PluginCornerRadiusPreset.Xs);
return context.ResolveCornerRadius(AirAppCornerRadiusPreset.Xs);
}
/// <summary>
@@ -49,10 +49,10 @@ public static class PluginAppearanceHelper
/// </summary>
/// <param name="context">外观上下文</param>
/// <returns>徽章圆角半径(像素)</returns>
public static double GetBadgeCornerRadius(this IPluginAppearanceContext context)
public static double GetBadgeCornerRadius(this IAirAppAppearanceContext context)
{
ArgumentNullException.ThrowIfNull(context);
return context.ResolveCornerRadius(PluginCornerRadiusPreset.Micro);
return context.ResolveCornerRadius(AirAppCornerRadiusPreset.Micro);
}
/// <summary>
@@ -61,10 +61,10 @@ public static class PluginAppearanceHelper
/// </summary>
/// <param name="context">外观上下文</param>
/// <returns>中等面板圆角半径(像素)</returns>
public static double GetMediumPanelCornerRadius(this IPluginAppearanceContext context)
public static double GetMediumPanelCornerRadius(this IAirAppAppearanceContext context)
{
ArgumentNullException.ThrowIfNull(context);
return context.ResolveCornerRadius(PluginCornerRadiusPreset.Md);
return context.ResolveCornerRadius(AirAppCornerRadiusPreset.Md);
}
/// <summary>
@@ -73,10 +73,10 @@ public static class PluginAppearanceHelper
/// </summary>
/// <param name="context">外观上下文</param>
/// <returns>大面板圆角半径(像素)</returns>
public static double GetLargePanelCornerRadius(this IPluginAppearanceContext context)
public static double GetLargePanelCornerRadius(this IAirAppAppearanceContext context)
{
ArgumentNullException.ThrowIfNull(context);
return context.ResolveCornerRadius(PluginCornerRadiusPreset.Lg);
return context.ResolveCornerRadius(AirAppCornerRadiusPreset.Lg);
}
/// <summary>
@@ -85,7 +85,7 @@ public static class PluginAppearanceHelper
/// <param name="context">外观上下文</param>
/// <param name="preset">圆角预设</param>
/// <returns>Avalonia CornerRadius 结构</returns>
public static CornerRadius ToCornerRadius(this IPluginAppearanceContext context, PluginCornerRadiusPreset preset)
public static CornerRadius ToCornerRadius(this IAirAppAppearanceContext context, AirAppCornerRadiusPreset preset)
{
ArgumentNullException.ThrowIfNull(context);
var radius = context.ResolveCornerRadius(preset);
@@ -97,7 +97,7 @@ public static class PluginAppearanceHelper
/// </summary>
/// <param name="context">外观上下文</param>
/// <returns>是否为暗色主题</returns>
public static bool IsDarkTheme(this IPluginAppearanceContext context)
public static bool IsDarkTheme(this IAirAppAppearanceContext context)
{
ArgumentNullException.ThrowIfNull(context);
return string.Equals(context.Snapshot.ThemeVariant, "Dark", StringComparison.OrdinalIgnoreCase);
@@ -108,7 +108,7 @@ public static class PluginAppearanceHelper
/// </summary>
/// <param name="context">外观上下文</param>
/// <returns>主题变体字符串("Light" 或 "Dark"</returns>
public static string GetThemeVariant(this IPluginAppearanceContext context)
public static string GetThemeVariant(this IAirAppAppearanceContext context)
{
ArgumentNullException.ThrowIfNull(context);
return context.Snapshot.ThemeVariant;

View File

@@ -1,11 +1,11 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
/// <remarks>
/// This is the runtime snapshot shape consumed by plugins inside the host process.
/// It is intentionally distinct from the wire DTO with the same name in
/// <c>LanMountainDesktop.PluginIsolation.Contracts.PluginAppearanceSnapshot</c>.
/// <c>LanMountainDesktop.AirAppIsolation.Contracts.AirAppAppearanceSnapshot</c>.
/// </remarks>
public sealed record PluginMaterialSurfaceSnapshot(
public sealed record AirAppMaterialSurfaceSnapshot(
string BackgroundColor,
string BorderColor,
double BlurRadius,
@@ -13,16 +13,16 @@ public sealed record PluginMaterialSurfaceSnapshot(
/// <remarks>
/// Runtime-facing appearance snapshot for plugins. This is not the same contract as the
/// wire-format snapshot in <c>LanMountainDesktop.PluginIsolation.Contracts</c>, even though the
/// wire-format snapshot in <c>LanMountainDesktop.AirAppIsolation.Contracts</c>, even though the
/// type name matches.
/// </remarks>
public sealed record PluginAppearanceSnapshot(
PluginCornerRadiusTokens CornerRadiusTokens,
public sealed record AirAppAppearanceSnapshot(
AirAppCornerRadiusTokens CornerRadiusTokens,
string ThemeVariant,
string? AccentColor = null,
string? SeedColor = null,
string? ColorSource = null,
string? SystemMaterialMode = null,
IReadOnlyDictionary<string, string>? ColorRoles = null,
IReadOnlyDictionary<string, PluginMaterialSurfaceSnapshot>? MaterialSurfaces = null,
IReadOnlyDictionary<string, AirAppMaterialSurfaceSnapshot>? MaterialSurfaces = null,
IReadOnlyList<string>? WallpaperSeedCandidates = null);

View File

@@ -5,13 +5,13 @@ namespace LanMountainDesktop.AirAppSdk;
/// <summary>
/// Base class for AirApp implementations.
/// Inherit from this class and apply the [AirAppEntrance] attribute.
/// Inherit from this class and apply the <see cref="AirAppEntranceAttribute"/> attribute.
/// </summary>
public abstract class AirAppBase : IAirApp
{
/// <summary>
/// Gets the runtime context after the AirApp has started.
/// Available after OnStartedAsync is called.
/// Available after <see cref="OnStartedAsync"/> is called.
/// </summary>
protected IAirAppRuntimeContext? RuntimeContext { get; private set; }
@@ -69,9 +69,8 @@ public abstract class AirAppBase : IAirApp
var options = new AirAppComponentOptions
{
Id = id,
Name = name,
WidgetType = typeof(TWidget)
ComponentId = id,
DisplayName = name
};
configure?.Invoke(options);

View File

@@ -1,11 +1,11 @@
namespace LanMountainDesktop.PluginIsolation.Contracts;
namespace LanMountainDesktop.AirAppIsolation.Contracts;
public sealed record PluginCapabilityDeclaration(
public sealed record AirAppCapabilityDeclaration(
string Name,
string Version,
string? Description = null);
public static class PluginCapabilityNames
public static class AirAppCapabilityNames
{
public const string Settings = "settings";
public const string Appearance = "appearance";

View File

@@ -0,0 +1,11 @@
using LanMountainDesktop.AirAppSdk;
using LanMountainDesktop.Shared.Contracts;
namespace LanMountainDesktop.AirAppSdk;
public sealed record AirAppComponentChromeContext(
string ComponentId,
string? PlacementId,
double CellSize,
AppearanceCornerRadiusTokens CornerRadiusTokens,
AirAppSettingsScope Scope = AirAppSettingsScope.App);

View File

@@ -1,9 +1,9 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public sealed class PluginDesktopComponentContext
public sealed class AirAppComponentContext
{
public PluginDesktopComponentContext(
PluginManifest manifest,
public AirAppComponentContext(
AirAppManifest manifest,
string pluginDirectory,
string dataDirectory,
IServiceProvider services,
@@ -11,8 +11,8 @@ public sealed class PluginDesktopComponentContext
string componentId,
string? placementId,
double cellSize,
IPluginAppearanceContext appearance,
IPluginSettingsService? pluginSettings = null)
IAirAppAppearanceContext appearance,
IAirAppSettingsService? pluginSettings = null)
{
ArgumentNullException.ThrowIfNull(manifest);
ArgumentException.ThrowIfNullOrWhiteSpace(pluginDirectory);
@@ -23,7 +23,7 @@ public sealed class PluginDesktopComponentContext
ArgumentNullException.ThrowIfNull(appearance);
Manifest = manifest;
PluginDirectory = pluginDirectory;
AirAppDirectory = pluginDirectory;
DataDirectory = dataDirectory;
Services = services;
Properties = properties;
@@ -31,12 +31,12 @@ public sealed class PluginDesktopComponentContext
PlacementId = string.IsNullOrWhiteSpace(placementId) ? null : placementId.Trim();
CellSize = Math.Max(1, cellSize);
Appearance = appearance;
PluginSettings = pluginSettings;
AirAppSettings = pluginSettings;
}
public PluginManifest Manifest { get; }
public AirAppManifest Manifest { get; }
public string PluginDirectory { get; }
public string AirAppDirectory { get; }
public string DataDirectory { get; }
@@ -50,22 +50,45 @@ public sealed class PluginDesktopComponentContext
public double CellSize { get; }
public IPluginAppearanceContext Appearance { get; }
public IAirAppAppearanceContext Appearance { get; }
public PluginCornerRadiusTokens CornerRadiusTokens => Appearance.Snapshot.CornerRadiusTokens;
public AirAppCornerRadiusTokens CornerRadiusTokens => Appearance.Snapshot.CornerRadiusTokens;
public IPluginSettingsService? PluginSettings { get; }
public IAirAppSettingsService? AirAppSettings { get; }
/// <summary>
/// 由宿主注入的窗口打开处理器。组件通过 <see cref="OpenWindowAsync"/> 打开本 AirApp 的窗口轻应用。
/// </summary>
public Func<string, Task>? OpenWindowHandler { get; set; }
public double ResolveScaledCornerRadius(double baseRadius, double? minimum = null, double? maximum = null)
{
return Appearance.ResolveScaledCornerRadius(baseRadius, minimum, maximum);
}
public double ResolveCornerRadius(PluginCornerRadiusPreset preset, double? minimum = null, double? maximum = null)
public double ResolveCornerRadius(AirAppCornerRadiusPreset preset, double? minimum = null, double? maximum = null)
{
return Appearance.ResolveCornerRadius(preset, minimum, maximum);
}
/// <summary>
/// 打开本 AirApp 声明的一个窗口轻应用。
/// </summary>
/// <param name="windowId">窗口标识(与 <c>AddAirAppWindow</c> 注册的 id 一致)。</param>
public Task OpenWindowAsync(string windowId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(windowId);
var handler = OpenWindowHandler;
if (handler is null)
{
throw new NotSupportedException(
$"AirApp '{Manifest.Id}' component '{ComponentId}' requested to open window '{windowId}', but the host does not support window opening for this component.");
}
return handler(windowId);
}
public T? GetService<T>()
{
return (T?)Services.GetService(typeof(T));

View File

@@ -1,16 +1,16 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public sealed class PluginDesktopComponentEditorContext
public sealed class AirAppComponentEditorContext
{
public PluginDesktopComponentEditorContext(
PluginManifest manifest,
public AirAppComponentEditorContext(
AirAppManifest manifest,
string pluginDirectory,
string dataDirectory,
IServiceProvider services,
IReadOnlyDictionary<string, object?> properties,
string componentId,
string? placementId,
IPluginSettingsService? pluginSettings,
IAirAppSettingsService? pluginSettings,
IComponentEditorHostContext hostContext)
{
ArgumentNullException.ThrowIfNull(manifest);
@@ -22,19 +22,19 @@ public sealed class PluginDesktopComponentEditorContext
ArgumentNullException.ThrowIfNull(hostContext);
Manifest = manifest;
PluginDirectory = pluginDirectory;
AirAppDirectory = pluginDirectory;
DataDirectory = dataDirectory;
Services = services;
Properties = properties;
ComponentId = componentId.Trim();
PlacementId = string.IsNullOrWhiteSpace(placementId) ? null : placementId.Trim();
PluginSettings = pluginSettings;
AirAppSettings = pluginSettings;
HostContext = hostContext;
}
public PluginManifest Manifest { get; }
public AirAppManifest Manifest { get; }
public string PluginDirectory { get; }
public string AirAppDirectory { get; }
public string DataDirectory { get; }
@@ -46,7 +46,7 @@ public sealed class PluginDesktopComponentEditorContext
public string? PlacementId { get; }
public IPluginSettingsService? PluginSettings { get; }
public IAirAppSettingsService? AirAppSettings { get; }
public IComponentEditorHostContext HostContext { get; }

View File

@@ -1,12 +1,12 @@
using Avalonia.Controls;
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public sealed class PluginDesktopComponentEditorRegistration
public sealed class AirAppComponentEditorRegistration
{
public PluginDesktopComponentEditorRegistration(
public AirAppComponentEditorRegistration(
string componentId,
Func<IServiceProvider, PluginDesktopComponentEditorContext, Control> editorFactory,
Func<IServiceProvider, AirAppComponentEditorContext, Control> editorFactory,
double preferredWidth = 720d,
double preferredHeight = 540d,
double minScale = 0.85d,
@@ -44,9 +44,9 @@ public sealed class PluginDesktopComponentEditorRegistration
AspectRatio = preferredWidth / preferredHeight;
}
public PluginDesktopComponentEditorRegistration(
public AirAppComponentEditorRegistration(
string componentId,
Func<PluginDesktopComponentEditorContext, Control> editorFactory,
Func<AirAppComponentEditorContext, Control> editorFactory,
double preferredWidth = 720d,
double preferredHeight = 540d,
double minScale = 0.85d,
@@ -63,7 +63,7 @@ public sealed class PluginDesktopComponentEditorRegistration
public string ComponentId { get; }
public Func<IServiceProvider, PluginDesktopComponentEditorContext, Control> EditorFactory { get; }
public Func<IServiceProvider, AirAppComponentEditorContext, Control> EditorFactory { get; }
public double PreferredWidth { get; }

View File

@@ -1,6 +1,6 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public sealed class PluginDesktopComponentOptions
public sealed class AirAppComponentOptions
{
public required string ComponentId { get; init; }
@@ -8,7 +8,7 @@ public sealed class PluginDesktopComponentOptions
public string IconKey { get; init; } = "PuzzlePiece";
public string Category { get; init; } = "Plugins";
public string Category { get; init; } = "AirApps";
public int MinWidthCells { get; init; } = 2;
@@ -18,7 +18,7 @@ public sealed class PluginDesktopComponentOptions
public bool AllowStatusBarPlacement { get; init; }
public PluginDesktopComponentResizeMode ResizeMode { get; init; } = PluginDesktopComponentResizeMode.Proportional;
public AirAppComponentResizeMode ResizeMode { get; init; } = AirAppComponentResizeMode.Proportional;
public string? DisplayNameLocalizationKey { get; init; }
@@ -26,7 +26,7 @@ public sealed class PluginDesktopComponentOptions
public string? DescriptionLocalizationKey { get; init; }
public PluginCornerRadiusPreset CornerRadiusPreset { get; init; } = PluginCornerRadiusPreset.Default;
public AirAppCornerRadiusPreset CornerRadiusPreset { get; init; } = AirAppCornerRadiusPreset.Default;
public Func<IPluginAppearanceContext, double, double>? CornerRadiusResolver { get; init; }
public Func<IAirAppAppearanceContext, double, double>? CornerRadiusResolver { get; init; }
}

View File

@@ -1,12 +1,12 @@
using Avalonia.Controls;
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public sealed class PluginDesktopComponentRegistration
public sealed class AirAppComponentRegistration
{
public PluginDesktopComponentRegistration(
Func<IServiceProvider, PluginDesktopComponentContext, Control> controlFactory,
PluginDesktopComponentOptions options)
public AirAppComponentRegistration(
Func<IServiceProvider, AirAppComponentContext, Control> controlFactory,
AirAppComponentOptions options)
{
ArgumentNullException.ThrowIfNull(controlFactory);
ArgumentNullException.ThrowIfNull(options);
@@ -38,9 +38,9 @@ public sealed class PluginDesktopComponentRegistration
CornerRadiusResolver = options.CornerRadiusResolver;
}
public PluginDesktopComponentRegistration(
Func<PluginDesktopComponentContext, Control> controlFactory,
PluginDesktopComponentOptions options)
public AirAppComponentRegistration(
Func<AirAppComponentContext, Control> controlFactory,
AirAppComponentOptions options)
: this((_, context) => controlFactory(context), options)
{
}
@@ -55,7 +55,7 @@ public sealed class PluginDesktopComponentRegistration
public string? DescriptionLocalizationKey { get; }
public Func<IServiceProvider, PluginDesktopComponentContext, Control> ControlFactory { get; }
public Func<IServiceProvider, AirAppComponentContext, Control> ControlFactory { get; }
public string IconKey { get; }
@@ -69,24 +69,24 @@ public sealed class PluginDesktopComponentRegistration
public bool AllowStatusBarPlacement { get; }
public PluginDesktopComponentResizeMode ResizeMode { get; }
public AirAppComponentResizeMode ResizeMode { get; }
public PluginCornerRadiusPreset CornerRadiusPreset { get; }
public AirAppCornerRadiusPreset CornerRadiusPreset { get; }
public Func<IPluginAppearanceContext, double, double>? CornerRadiusResolver { get; }
public Func<IAirAppAppearanceContext, double, double>? CornerRadiusResolver { get; }
public double ResolveCornerRadius(IPluginAppearanceContext appearance, double cellSize)
public double ResolveCornerRadius(IAirAppAppearanceContext appearance, double cellSize)
{
ArgumentNullException.ThrowIfNull(appearance);
var resolved = CornerRadiusResolver is not null
? CornerRadiusResolver(appearance, Math.Max(1d, cellSize))
: CornerRadiusPreset == PluginCornerRadiusPreset.Default
? appearance.ResolveCornerRadius(PluginCornerRadiusPreset.Component)
: CornerRadiusPreset == AirAppCornerRadiusPreset.Default
? appearance.ResolveCornerRadius(AirAppCornerRadiusPreset.Component)
: appearance.ResolveCornerRadius(CornerRadiusPreset);
return double.IsFinite(resolved)
? Math.Max(0d, resolved)
: appearance.ResolveCornerRadius(PluginCornerRadiusPreset.Component);
: appearance.ResolveCornerRadius(AirAppCornerRadiusPreset.Component);
}
}

View File

@@ -0,0 +1,7 @@
namespace LanMountainDesktop.AirAppSdk;
public enum AirAppComponentResizeMode
{
Proportional = 0,
Free = 1
}

View File

@@ -1,6 +1,6 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public enum PluginCornerRadiusPreset
public enum AirAppCornerRadiusPreset
{
Default = 0,
Micro = 1,

View File

@@ -0,0 +1,52 @@
using Avalonia;
using LanMountainDesktop.Shared.Contracts;
namespace LanMountainDesktop.AirAppSdk;
public sealed record AirAppCornerRadiusTokens(
double Micro,
double Xs,
double Sm,
double Md,
double Lg,
double Xl,
double Island,
double Component)
{
public double Get(AirAppCornerRadiusPreset preset)
{
return preset switch
{
AirAppCornerRadiusPreset.Default => Component,
AirAppCornerRadiusPreset.Micro => Micro,
AirAppCornerRadiusPreset.Xs => Xs,
AirAppCornerRadiusPreset.Sm => Sm,
AirAppCornerRadiusPreset.Md => Md,
AirAppCornerRadiusPreset.Lg => Lg,
AirAppCornerRadiusPreset.Xl => Xl,
AirAppCornerRadiusPreset.Island => Island,
AirAppCornerRadiusPreset.Component => Component,
_ => Component
};
}
public CornerRadius ToCornerRadius(AirAppCornerRadiusPreset preset)
{
return new CornerRadius(Get(preset));
}
public static AirAppCornerRadiusTokens FromShared(AppearanceCornerRadiusTokens tokens)
{
ArgumentNullException.ThrowIfNull(tokens);
return new AirAppCornerRadiusTokens(
tokens.Micro.TopLeft,
tokens.Xs.TopLeft,
tokens.Sm.TopLeft,
tokens.Md.TopLeft,
tokens.Lg.TopLeft,
tokens.Xl.TopLeft,
tokens.Island.TopLeft,
tokens.Component.TopLeft);
}
}

View File

@@ -1,9 +1,5 @@
namespace LanMountainDesktop.AirAppSdk;
/// <summary>
/// Marks a class as the entry point for an AirApp.
/// The marked class must inherit from AirAppBase or implement IAirApp.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class AirAppEntranceAttribute : Attribute
{

View File

@@ -1,9 +1,9 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public static class PluginHostPropertyKeys
public static class AirAppHostPropertyKeys
{
public const string HostApplicationName = "HostApplicationName";
public const string HostVersion = "HostVersion";
public const string PluginSdkApiVersion = "PluginSdkApiVersion";
public const string AirAppSdkApiVersion = "AirAppSdkApiVersion";
public const string HostLanguageCode = "HostLanguageCode";
}

View File

@@ -0,0 +1,8 @@
namespace LanMountainDesktop.AirAppSdk;
public sealed record AirAppInstalledInfo(
AirAppManifest Manifest,
bool IsEnabled,
bool IsLoaded,
bool IsPackage,
string? ErrorMessage);

View File

@@ -1,26 +1,26 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace LanMountainDesktop.PluginIsolation.Ipc;
namespace LanMountainDesktop.AirAppIsolation.Ipc;
public sealed class PluginIpcClient
public sealed class AirAppIpcClient
{
public PluginIpcClient(PluginIpcClientOptions options)
public AirAppIpcClient(AirAppIpcClientOptions options)
{
Options = options ?? throw new ArgumentNullException(nameof(options));
SerializerContext = options.SerializerContext ?? throw new ArgumentNullException(nameof(options.SerializerContext));
SerializerOptions = SerializerContext.Options;
}
public PluginIpcClientOptions Options { get; }
public AirAppIpcClientOptions Options { get; }
public JsonSerializerContext SerializerContext { get; }
public JsonSerializerOptions SerializerOptions { get; }
public PluginIpcRequestDispatcher? RequestDispatcher { get; set; }
public AirAppIpcRequestDispatcher? RequestDispatcher { get; set; }
public PluginIpcNotificationDispatcher? NotificationDispatcher { get; set; }
public AirAppIpcNotificationDispatcher? NotificationDispatcher { get; set; }
public Task<TResponse?> RequestAsync<TRequest, TResponse>(
string route,
@@ -48,7 +48,7 @@ public sealed class PluginIpcClient
if (RequestDispatcher is null)
{
throw new NotSupportedException(
"PluginIpcClient is not yet bound to a dotnetCampus.Ipc transport dispatcher. " +
"AirAppIpcClient is not yet bound to a dotnetCampus.Ipc transport dispatcher. " +
"Wire RequestDispatcher during host/worker transport integration.");
}
@@ -66,7 +66,7 @@ public sealed class PluginIpcClient
if (NotificationDispatcher is null)
{
throw new NotSupportedException(
"PluginIpcClient is not yet bound to a dotnetCampus.Ipc transport dispatcher. " +
"AirAppIpcClient is not yet bound to a dotnetCampus.Ipc transport dispatcher. " +
"Wire NotificationDispatcher during host/worker transport integration.");
}

View File

@@ -0,0 +1,17 @@
using System.Text.Json.Serialization;
using LanMountainDesktop.AirAppIsolation.Contracts;
namespace LanMountainDesktop.AirAppIsolation.Ipc;
public sealed record AirAppIpcClientOptions
{
public required string PipeName { get; init; }
public string ProtocolVersion { get; init; } = AirAppIsolationProtocolVersion.Current;
public TimeSpan ConnectTimeout { get; init; } = AirAppIpcConstants.DefaultConnectTimeout;
public TimeSpan RequestTimeout { get; init; } = AirAppIpcConstants.DefaultRequestTimeout;
public JsonSerializerContext SerializerContext { get; init; } = AirAppIsolationJsonContext.Default;
}

View File

@@ -1,16 +1,16 @@
using LanMountainDesktop.PluginIsolation.Contracts;
using LanMountainDesktop.AirAppIsolation.Contracts;
namespace LanMountainDesktop.PluginIsolation.Ipc;
namespace LanMountainDesktop.AirAppIsolation.Ipc;
public static class PluginIpcConstants
public static class AirAppIpcConstants
{
public const string EnvironmentPluginId = "LANMOUNTAIN_PLUGIN_ID";
public const string EnvironmentAirAppId = "LANMOUNTAIN_PLUGIN_ID";
public const string EnvironmentSessionId = "LANMOUNTAIN_PLUGIN_SESSION_ID";
public const string EnvironmentHostPipeName = "LANMOUNTAIN_PLUGIN_HOST_PIPE";
public const string EnvironmentProtocolVersion = "LANMOUNTAIN_PLUGIN_PROTOCOL_VERSION";
public const string EnvironmentRuntimeMode = "LANMOUNTAIN_PLUGIN_RUNTIME_MODE";
public const string CommandLinePluginId = "--plugin-id";
public const string CommandLineAirAppId = "--plugin-id";
public const string CommandLineSessionId = "--session-id";
public const string CommandLineHostPipeName = "--host-pipe-name";
public const string CommandLineProtocolVersion = "--protocol-version";
@@ -21,5 +21,5 @@ public static class PluginIpcConstants
public static readonly TimeSpan DefaultHeartbeatInterval = TimeSpan.FromSeconds(5);
public static readonly TimeSpan DefaultHeartbeatTimeout = TimeSpan.FromSeconds(15);
public const string DefaultProtocolVersion = PluginIsolationProtocolVersion.Current;
public const string DefaultProtocolVersion = AirAppIsolationProtocolVersion.Current;
}

View File

@@ -1,13 +1,13 @@
using System.Text.Json;
namespace LanMountainDesktop.PluginIsolation.Ipc;
namespace LanMountainDesktop.AirAppIsolation.Ipc;
public delegate Task<JsonElement?> PluginIpcRequestDispatcher(
public delegate Task<JsonElement?> AirAppIpcRequestDispatcher(
string route,
JsonElement? payload,
CancellationToken cancellationToken);
public delegate Task PluginIpcNotificationDispatcher(
public delegate Task AirAppIpcNotificationDispatcher(
string route,
JsonElement? payload,
CancellationToken cancellationToken);

View File

@@ -1,6 +1,6 @@
namespace LanMountainDesktop.PluginIsolation.Contracts;
namespace LanMountainDesktop.AirAppIsolation.Contracts;
public static class PluginIpcErrorCodes
public static class AirAppIpcErrorCodes
{
public const string ProtocolMismatch = "protocol_mismatch";
public const string SessionRejected = "session_rejected";

View File

@@ -0,0 +1,17 @@
using LanMountainDesktop.AirAppIsolation.Contracts;
namespace LanMountainDesktop.AirAppIsolation.Ipc;
public static class AirAppIpcRoutedNotifyIds
{
public const string SessionReady = AirAppIpcRoutes.Session.Ready;
public const string LifecycleStateChanged = AirAppIpcRoutes.Lifecycle.StateChanged;
public const string SettingsChanged = AirAppIpcRoutes.Settings.Changed;
public const string AppearanceChanged = AirAppIpcRoutes.Appearance.Changed;
public const string UiDetach = AirAppIpcRoutes.Ui.Detach;
public const string UiStateChanged = AirAppIpcRoutes.Ui.StateChanged;
public const string HeartbeatPing = AirAppIpcRoutes.Heartbeat.Ping;
public const string HeartbeatPong = AirAppIpcRoutes.Heartbeat.Pong;
public const string LogWrite = AirAppIpcRoutes.Log.Write;
public const string FaultReport = AirAppIpcRoutes.Fault.Report;
}

View File

@@ -1,6 +1,6 @@
namespace LanMountainDesktop.PluginIsolation.Contracts;
namespace LanMountainDesktop.AirAppIsolation.Contracts;
public static class PluginIpcRoutes
public static class AirAppIpcRoutes
{
public static class Session
{

View File

@@ -1,9 +1,9 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace LanMountainDesktop.PluginIsolation.Ipc;
namespace LanMountainDesktop.AirAppIsolation.Ipc;
public sealed class PluginIpcServer
public sealed class AirAppIpcServer
{
private readonly Dictionary<string, Func<JsonElement?, CancellationToken, Task<JsonElement?>>> _requestHandlers =
new(StringComparer.OrdinalIgnoreCase);
@@ -11,14 +11,14 @@ public sealed class PluginIpcServer
private readonly Dictionary<string, Func<JsonElement?, CancellationToken, Task>> _notificationHandlers =
new(StringComparer.OrdinalIgnoreCase);
public PluginIpcServer(PluginIpcServerOptions options)
public AirAppIpcServer(AirAppIpcServerOptions options)
{
Options = options ?? throw new ArgumentNullException(nameof(options));
SerializerContext = options.SerializerContext ?? throw new ArgumentNullException(nameof(options.SerializerContext));
SerializerOptions = SerializerContext.Options;
}
public PluginIpcServerOptions Options { get; }
public AirAppIpcServerOptions Options { get; }
public JsonSerializerContext SerializerContext { get; }

View File

@@ -0,0 +1,17 @@
using System.Text.Json.Serialization;
using LanMountainDesktop.AirAppIsolation.Contracts;
namespace LanMountainDesktop.AirAppIsolation.Ipc;
public sealed record AirAppIpcServerOptions
{
public required string PipeName { get; init; }
public string ProtocolVersion { get; init; } = AirAppIsolationProtocolVersion.Current;
public TimeSpan HeartbeatInterval { get; init; } = AirAppIpcConstants.DefaultHeartbeatInterval;
public TimeSpan HeartbeatTimeout { get; init; } = AirAppIpcConstants.DefaultHeartbeatTimeout;
public JsonSerializerContext SerializerContext { get; init; } = AirAppIsolationJsonContext.Default;
}

View File

@@ -0,0 +1,40 @@
using System.Text.Json.Serialization;
namespace LanMountainDesktop.AirAppIsolation.Contracts;
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
[JsonSerializable(typeof(AirAppCapabilityDeclaration))]
[JsonSerializable(typeof(List<AirAppCapabilityDeclaration>))]
[JsonSerializable(typeof(AirAppSessionHandshakeRequest))]
[JsonSerializable(typeof(AirAppSessionHandshakeResponse))]
[JsonSerializable(typeof(AirAppReadyNotification))]
[JsonSerializable(typeof(AirAppInitializeRequest))]
[JsonSerializable(typeof(AirAppInitializeResponse))]
[JsonSerializable(typeof(AirAppStopRequest))]
[JsonSerializable(typeof(AirAppRestartRequest))]
[JsonSerializable(typeof(AirAppLifecycleStateChanged))]
[JsonSerializable(typeof(AirAppSettingsSnapshotRequest))]
[JsonSerializable(typeof(AirAppSettingsSnapshotResponse))]
[JsonSerializable(typeof(AirAppSettingsWriteRequest))]
[JsonSerializable(typeof(AirAppSettingsWriteResponse))]
[JsonSerializable(typeof(AirAppSettingsChangedNotification))]
[JsonSerializable(typeof(AirAppAppearanceSnapshotRequest))]
[JsonSerializable(typeof(AirAppMaterialSurfaceSnapshot))]
[JsonSerializable(typeof(AirAppAppearanceSnapshot))]
[JsonSerializable(typeof(AirAppAppearanceChangedNotification))]
[JsonSerializable(typeof(AirAppUiSurfaceDescriptor))]
[JsonSerializable(typeof(List<AirAppUiSurfaceDescriptor>))]
[JsonSerializable(typeof(AirAppUiAttachRequest))]
[JsonSerializable(typeof(AirAppUiAttachResponse))]
[JsonSerializable(typeof(AirAppUiDetachNotification))]
[JsonSerializable(typeof(AirAppUiCommandRequest))]
[JsonSerializable(typeof(AirAppUiCommandResponse))]
[JsonSerializable(typeof(AirAppUiStateChangedNotification))]
[JsonSerializable(typeof(AirAppHeartbeatPing))]
[JsonSerializable(typeof(AirAppHeartbeatPong))]
[JsonSerializable(typeof(AirAppLogEntry))]
[JsonSerializable(typeof(AirAppFaultReport))]
public partial class AirAppIsolationJsonContext : JsonSerializerContext;

View File

@@ -0,0 +1,6 @@
namespace LanMountainDesktop.AirAppIsolation.Contracts;
public static class AirAppIsolationProtocolVersion
{
public const string Current = "1.0";
}

View File

@@ -1,9 +1,9 @@
using System.Globalization;
using System.Text.Json;
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public sealed class PluginLocalizer
public sealed class AirAppLocalizer
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
@@ -14,28 +14,28 @@ public sealed class PluginLocalizer
private readonly Dictionary<string, Dictionary<string, string>> _cache =
new(StringComparer.OrdinalIgnoreCase);
public PluginLocalizer(string pluginDirectory, string? languageCode)
public AirAppLocalizer(string pluginDirectory, string? languageCode)
{
ArgumentException.ThrowIfNullOrWhiteSpace(pluginDirectory);
PluginDirectory = pluginDirectory;
AirAppDirectory = pluginDirectory;
LanguageCode = NormalizeLanguageCode(languageCode);
}
public string PluginDirectory { get; }
public string AirAppDirectory { get; }
public string LanguageCode { get; }
public static PluginLocalizer Create(IPluginRuntimeContext context)
public static AirAppLocalizer Create(IAirAppRuntimeContext context)
{
ArgumentNullException.ThrowIfNull(context);
return new PluginLocalizer(context.PluginDirectory, ResolveLanguageCode(context.Properties));
return new AirAppLocalizer(context.AirAppDirectory, ResolveLanguageCode(context.Properties));
}
public static PluginLocalizer Create(PluginDesktopComponentContext context)
public static AirAppLocalizer Create(AirAppComponentContext context)
{
ArgumentNullException.ThrowIfNull(context);
return new PluginLocalizer(context.PluginDirectory, ResolveLanguageCode(context.Properties));
return new AirAppLocalizer(context.AirAppDirectory, ResolveLanguageCode(context.Properties));
}
public string GetString(string key, string fallback)
@@ -76,7 +76,7 @@ public sealed class PluginLocalizer
{
ArgumentNullException.ThrowIfNull(properties);
return properties.TryGetValue(PluginHostPropertyKeys.HostLanguageCode, out var rawValue) &&
return properties.TryGetValue(AirAppHostPropertyKeys.HostLanguageCode, out var rawValue) &&
rawValue is string languageCode
? NormalizeLanguageCode(languageCode)
: NormalizeLanguageCode(CultureInfo.CurrentUICulture.Name);
@@ -92,7 +92,7 @@ public sealed class PluginLocalizer
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
try
{
var filePath = Path.Combine(PluginDirectory, "Localization", $"{languageCode}.json");
var filePath = Path.Combine(AirAppDirectory, "Localization", $"{languageCode}.json");
if (File.Exists(filePath))
{
var json = File.ReadAllText(filePath).TrimStart('\uFEFF');

View File

@@ -2,9 +2,6 @@ using System.Text.Json;
namespace LanMountainDesktop.AirAppSdk;
/// <summary>
/// AirApp manifest (airapp.json).
/// </summary>
public sealed record AirAppManifest(
string Id,
string Name,
@@ -13,11 +10,11 @@ public sealed record AirAppManifest(
string? Author = null,
string? Version = null,
string? ApiVersion = null,
IReadOnlyList<AirAppSharedContractReference>? SharedContracts = null,
AirAppRuntimeConfiguration? Runtime = null,
IReadOnlyList<AirAppComponentManifest>? Components = null,
IReadOnlyList<AirAppWindowManifest>? Windows = null,
IReadOnlyList<string>? Permissions = null,
IReadOnlyList<AirAppSharedContractReference>? SharedContracts = null)
IReadOnlyList<string>? Permissions = null)
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
@@ -26,9 +23,6 @@ public sealed record AirAppManifest(
AllowTrailingCommas = true
};
/// <summary>
/// Load manifest from file.
/// </summary>
public static AirAppManifest Load(string manifestPath)
{
ArgumentException.ThrowIfNullOrWhiteSpace(manifestPath);
@@ -37,9 +31,6 @@ public sealed record AirAppManifest(
return Load(stream, manifestPath);
}
/// <summary>
/// Load manifest from stream.
/// </summary>
public static AirAppManifest Load(Stream stream, string sourceName)
{
ArgumentNullException.ThrowIfNull(stream);
@@ -48,15 +39,12 @@ public sealed record AirAppManifest(
var manifest = JsonSerializer.Deserialize<AirAppManifest>(stream, SerializerOptions);
if (manifest is null)
{
throw new InvalidOperationException($"Failed to deserialize AirApp manifest '{sourceName}'.");
throw new InvalidOperationException($"Failed to deserialize plugin manifest '{sourceName}'.");
}
return manifest.NormalizeAndValidate(sourceName);
}
/// <summary>
/// Resolve entrance assembly path.
/// </summary>
public string ResolveEntranceAssemblyPath(string manifestPath)
{
ArgumentException.ThrowIfNullOrWhiteSpace(manifestPath);
@@ -67,21 +55,18 @@ public sealed record AirAppManifest(
}
var manifestDirectory = Path.GetDirectoryName(Path.GetFullPath(manifestPath))
?? throw new InvalidOperationException($"Failed to determine directory of '{manifestPath}'.");
?? throw new InvalidOperationException($"Failed to determine the directory of '{manifestPath}'.");
return Path.GetFullPath(Path.Combine(manifestDirectory, EntranceAssembly));
}
/// <summary>
/// Get runtime mode.
/// </summary>
public AirAppRuntimeMode RuntimeMode =>
AirAppRuntimeModes.TryParse(Runtime?.Mode, out var mode) ? mode : AirAppRuntimeMode.InProcess;
private AirAppManifest NormalizeAndValidate(string manifestPath)
{
var normalizedSharedContracts = NormalizeSharedContracts(manifestPath, SharedContracts);
var normalizedRuntime = (Runtime ?? new AirAppRuntimeConfiguration()).NormalizeAndValidate(manifestPath);
var normalized = this with
{
Id = RequireValue(Id, nameof(Id), manifestPath),
@@ -91,18 +76,17 @@ public sealed record AirAppManifest(
Author = NormalizeOptionalValue(Author),
Version = NormalizeOptionalValue(Version),
ApiVersion = NormalizeOptionalValue(ApiVersion) ?? AirAppSdkInfo.ApiVersion,
SharedContracts = normalizedSharedContracts,
Runtime = normalizedRuntime,
Components = Components ?? Array.Empty<AirAppComponentManifest>(),
Windows = Windows ?? Array.Empty<AirAppWindowManifest>(),
Permissions = Permissions ?? Array.Empty<string>(),
SharedContracts = SharedContracts ?? Array.Empty<AirAppSharedContractReference>()
Permissions = Permissions ?? Array.Empty<string>()
};
// Validate API version
if (!System.Version.TryParse(normalized.ApiVersion, out var requestedVersion))
{
throw new InvalidOperationException(
$"AirApp manifest '{manifestPath}' declares invalid API version '{normalized.ApiVersion}'.");
$"AirApp manifest '{manifestPath}' declares an invalid API version '{normalized.ApiVersion}'.");
}
if (!System.Version.TryParse(AirAppSdkInfo.ApiVersion, out var currentVersion))
@@ -113,10 +97,43 @@ public sealed record AirAppManifest(
if (requestedVersion.Major != currentVersion.Major)
{
throw new InvalidOperationException(
$"AirApp '{normalized.Id}' targets API version '{normalized.ApiVersion}' (major {requestedVersion.Major}), " +
$"but the host provides '{AirAppSdkInfo.ApiVersion}' (major {currentVersion.Major}). " +
$"This host only supports v{currentVersion.Major}.x AirApps and rejects v{requestedVersion.Major}.x packages. " +
$"Migrate the AirApp manifest and code to API {AirAppSdkInfo.ApiVersion}, then rebuild and republish.");
$"AirApp '{normalized.Id}' targets API version '{normalized.ApiVersion}', " +
$"but the host provides '{AirAppSdkInfo.ApiVersion}'. " +
$"This host only supports API {AirAppSdkInfo.ApiVersion} plugins.");
}
return normalized;
}
private static IReadOnlyList<AirAppSharedContractReference> NormalizeSharedContracts(
string manifestPath,
IReadOnlyList<AirAppSharedContractReference>? sharedContracts)
{
if (sharedContracts is null || sharedContracts.Count == 0)
{
return Array.Empty<AirAppSharedContractReference>();
}
var normalized = new List<AirAppSharedContractReference>(sharedContracts.Count);
var seenIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var contract in sharedContracts)
{
if (contract is null)
{
throw new InvalidOperationException(
$"AirApp manifest '{manifestPath}' contains a null shared contract declaration.");
}
var normalizedContract = contract.NormalizeAndValidate(manifestPath);
var contractKey = $"{normalizedContract.Id}@{normalizedContract.Version}";
if (!seenIds.Add(contractKey))
{
throw new InvalidOperationException(
$"AirApp manifest '{manifestPath}' declares duplicate shared contract '{contractKey}'.");
}
normalized.Add(normalizedContract);
}
return normalized;
@@ -141,7 +158,7 @@ public sealed record AirAppManifest(
}
/// <summary>
/// Component declaration in manifest.
/// Component declaration in the AirApp manifest.
/// </summary>
public sealed record AirAppComponentManifest(
string Id,
@@ -153,7 +170,7 @@ public sealed record AirAppComponentManifest(
string? IconKey = null);
/// <summary>
/// Window declaration in manifest.
/// Window declaration in the AirApp manifest.
/// </summary>
public sealed record AirAppWindowManifest(
string Id,
@@ -161,28 +178,3 @@ public sealed record AirAppWindowManifest(
double DefaultWidth = 800,
double DefaultHeight = 600,
string? Description = null);
/// <summary>
/// Shared contract reference.
/// </summary>
public sealed record AirAppSharedContractReference(
string Id,
string Version);
/// <summary>
/// Runtime configuration.
/// </summary>
public sealed record AirAppRuntimeConfiguration
{
public string? Mode { get; init; }
public IReadOnlyList<string>? Capabilities { get; init; }
internal AirAppRuntimeConfiguration NormalizeAndValidate(string manifestPath)
{
return this with
{
Mode = string.IsNullOrWhiteSpace(Mode) ? "in-process" : Mode.Trim().ToLowerInvariant(),
Capabilities = Capabilities ?? Array.Empty<string>()
};
}
}

View File

@@ -0,0 +1,6 @@
namespace LanMountainDesktop.AirAppSdk;
public sealed record AirAppPackageInstallResult(
AirAppManifest Manifest,
bool ReplacedExisting,
bool RestartRequired);

View File

@@ -1,6 +1,6 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public sealed record PluginPublicIpcServiceDescriptor(
public sealed record AirAppPublicIpcServiceDescriptor(
Type ContractType,
object Implementation,
string? ObjectId,

View File

@@ -0,0 +1,6 @@
namespace LanMountainDesktop.AirAppSdk;
public sealed record AirAppPublicIpcServiceRegistration(
Type ContractType,
string? ObjectId,
string[] NotifyIds);

View File

@@ -0,0 +1,15 @@
namespace LanMountainDesktop.AirAppSdk;
public sealed record AirAppRuntimeConfiguration(string Mode = AirAppRuntimeModes.InProcess)
{
public AirAppRuntimeMode RuntimeMode =>
AirAppRuntimeModes.TryParse(Mode, out var mode) ? mode : AirAppRuntimeMode.InProcess;
internal AirAppRuntimeConfiguration NormalizeAndValidate(string manifestPath)
{
return this with
{
Mode = AirAppRuntimeModes.NormalizeManifestValue(Mode, manifestPath)
};
}
}

View File

@@ -1,6 +1,6 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public enum PluginRuntimeMode
public enum AirAppRuntimeMode
{
InProcess = 0,
IsolatedBackground = 1,

View File

@@ -1,25 +1,27 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public static class PluginRuntimeModes
public static class AirAppRuntimeModes
{
public const string InProcess = "in-proc";
public const string InProcessAlt = "in-process";
public const string IsolatedBackground = "isolated-background";
public const string IsolatedWindow = "isolated-window";
public static bool TryParse(string? value, out PluginRuntimeMode mode)
public static bool TryParse(string? value, out AirAppRuntimeMode mode)
{
switch (value?.Trim().ToLowerInvariant())
{
case null:
case "":
case InProcess:
mode = PluginRuntimeMode.InProcess;
case InProcessAlt:
mode = AirAppRuntimeMode.InProcess;
return true;
case IsolatedBackground:
mode = PluginRuntimeMode.IsolatedBackground;
mode = AirAppRuntimeMode.IsolatedBackground;
return true;
case IsolatedWindow:
mode = PluginRuntimeMode.IsolatedWindow;
mode = AirAppRuntimeMode.IsolatedWindow;
return true;
default:
mode = default;
@@ -27,7 +29,7 @@ public static class PluginRuntimeModes
}
}
public static PluginRuntimeMode Parse(string? value, string sourceName, string propertyName = "runtime.mode")
public static AirAppRuntimeMode Parse(string? value, string sourceName, string propertyName = "runtime.mode")
{
if (TryParse(value, out var mode))
{
@@ -36,7 +38,7 @@ public static class PluginRuntimeModes
var candidate = string.IsNullOrWhiteSpace(value) ? "<empty>" : value.Trim();
throw new InvalidOperationException(
$"Plugin manifest '{sourceName}' declares unsupported runtime mode '{candidate}' in '{propertyName}'. " +
$"AirApp manifest '{sourceName}' declares unsupported runtime mode '{candidate}' in '{propertyName}'. " +
$"Supported values: '{InProcess}', '{IsolatedBackground}', '{IsolatedWindow}'.");
}
@@ -45,13 +47,13 @@ public static class PluginRuntimeModes
return ToManifestValue(Parse(value, sourceName, propertyName));
}
public static string ToManifestValue(PluginRuntimeMode mode)
public static string ToManifestValue(AirAppRuntimeMode mode)
{
return mode switch
{
PluginRuntimeMode.InProcess => InProcess,
PluginRuntimeMode.IsolatedBackground => IsolatedBackground,
PluginRuntimeMode.IsolatedWindow => IsolatedWindow,
AirAppRuntimeMode.InProcess => InProcess,
AirAppRuntimeMode.IsolatedBackground => IsolatedBackground,
AirAppRuntimeMode.IsolatedWindow => IsolatedWindow,
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported plugin runtime mode.")
};
}

View File

@@ -1,9 +1,10 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public static class PluginSdkInfo
public static class AirAppSdkInfo
{
public const string ApiVersion = "5.0.0";
public const string ManifestFileName = "plugin.json";
public const string SdkVersion = "1.0.0";
public const string ApiVersion = "1.0.0";
public const string ManifestFileName = "airapp.json";
public const string PackageFileExtension = ".laapp";
public const string DataDirectoryName = "Data";
public const string RuntimeDirectoryName = ".runtime";

View File

@@ -2,15 +2,15 @@ using Avalonia.Controls;
using dotnetCampus.Ipc.CompilerServices.Attributes;
using Microsoft.Extensions.DependencyInjection;
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public static class PluginServiceCollectionExtensions
public static class AirAppServiceCollectionExtensions
{
public static IServiceCollection AddPluginSettingsSection(
public static IServiceCollection AddAirAppSettingsSection(
this IServiceCollection services,
string id,
string titleLocalizationKey,
Action<PluginSettingsSectionBuilder> configure,
Action<AirAppSettingsSectionBuilder> configure,
string? descriptionLocalizationKey = null,
string iconKey = "PuzzlePiece",
int sortOrder = 0)
@@ -18,7 +18,7 @@ public static class PluginServiceCollectionExtensions
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configure);
var builder = new PluginSettingsSectionBuilder(
var builder = new AirAppSettingsSectionBuilder(
id,
titleLocalizationKey,
descriptionLocalizationKey,
@@ -35,19 +35,19 @@ public static class PluginServiceCollectionExtensions
/// in the settings window, allowing the plugin to use any Fluent Avalonia controls
/// and custom layouts — just like built-in settings pages.
/// </summary>
/// <typeparam name="TView">A <see cref="SettingsPageBase"/> subclass that defines the settings UI using AXAML.</typeparam>
public static IServiceCollection AddPluginSettingsSection<TView>(
/// <typeparam name="TView">A <see cref="AirAppSettingsPageBase"/> subclass that defines the settings UI using AXAML.</typeparam>
public static IServiceCollection AddAirAppSettingsSection<TView>(
this IServiceCollection services,
string id,
string titleLocalizationKey,
string? descriptionLocalizationKey = null,
string iconKey = "PuzzlePiece",
int sortOrder = 0)
where TView : SettingsPageBase
where TView : AirAppSettingsPageBase
{
ArgumentNullException.ThrowIfNull(services);
var builder = new PluginSettingsSectionBuilder(
var builder = new AirAppSettingsSectionBuilder(
id,
titleLocalizationKey,
descriptionLocalizationKey,
@@ -58,21 +58,21 @@ public static class PluginServiceCollectionExtensions
return services;
}
public static IServiceCollection AddPluginDesktopComponent<TControl>(
public static IServiceCollection AddAirAppComponent<TControl>(
this IServiceCollection services,
PluginDesktopComponentOptions options)
AirAppComponentOptions options)
where TControl : Control
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(options);
services.AddSingleton(new PluginDesktopComponentRegistration(
services.AddSingleton(new AirAppComponentRegistration(
(provider, context) => ActivatorUtilities.CreateInstance<TControl>(provider, context),
options));
return services;
}
public static IServiceCollection AddPluginDesktopComponentEditor<TControl>(
public static IServiceCollection AddAirAppComponentEditor<TControl>(
this IServiceCollection services,
string componentId,
double preferredWidth = 720d,
@@ -83,7 +83,7 @@ public static class PluginServiceCollectionExtensions
{
ArgumentNullException.ThrowIfNull(services);
services.AddSingleton(new PluginDesktopComponentEditorRegistration(
services.AddSingleton(new AirAppComponentEditorRegistration(
componentId,
(provider, context) => ActivatorUtilities.CreateInstance<TControl>(provider, context),
preferredWidth,
@@ -93,7 +93,28 @@ public static class PluginServiceCollectionExtensions
return services;
}
public static IServiceCollection AddPluginExport<TContract, TImplementation>(this IServiceCollection services)
/// <summary>
/// Registers a window AirApp entry.
/// The host resolves the window by <paramref name="id"/> and hosts its
/// <see cref="IAirAppWindow"/> content inside the AirAppHost window shell.
/// </summary>
/// <typeparam name="TWindow">A class implementing <see cref="IAirAppWindow"/>.</typeparam>
/// <param name="id">Unique window identifier.</param>
/// <param name="name">Display name.</param>
public static IServiceCollection AddAirAppWindow<TWindow>(
this IServiceCollection services,
string id,
string name)
where TWindow : class, IAirAppWindow
{
ArgumentNullException.ThrowIfNull(services);
services.AddTransient<TWindow>();
services.AddSingleton(new AirAppWindowRegistration(id, name, typeof(TWindow)));
return services;
}
public static IServiceCollection AddAirAppExport<TContract, TImplementation>(this IServiceCollection services)
where TContract : class
where TImplementation : class, TContract
{
@@ -102,18 +123,18 @@ public static class PluginServiceCollectionExtensions
EnsureSingletonRegistration<TContract, TImplementation>(services);
if (!services.Any(descriptor =>
descriptor.ServiceType == typeof(PluginServiceExportRegistration) &&
descriptor.ImplementationInstance is PluginServiceExportRegistration existing &&
descriptor.ServiceType == typeof(AirAppServiceExportRegistration) &&
descriptor.ImplementationInstance is AirAppServiceExportRegistration existing &&
existing.ContractType == typeof(TContract) &&
existing.ImplementationType == typeof(TImplementation)))
{
services.AddSingleton(new PluginServiceExportRegistration(typeof(TContract), typeof(TImplementation)));
services.AddSingleton(new AirAppServiceExportRegistration(typeof(TContract), typeof(TImplementation)));
}
return services;
}
public static IServiceCollection AddPluginPublicIpc<TContract, TImplementation>(
public static IServiceCollection AddAirAppPublicIpc<TContract, TImplementation>(
this IServiceCollection services,
string? objectId = null,
params string[] notifyIds)
@@ -125,12 +146,12 @@ public static class PluginServiceCollectionExtensions
EnsureSingletonRegistration<TContract, TImplementation>(services);
if (!services.Any(descriptor =>
descriptor.ServiceType == typeof(PluginPublicIpcServiceRegistration) &&
descriptor.ImplementationInstance is PluginPublicIpcServiceRegistration existing &&
descriptor.ServiceType == typeof(AirAppPublicIpcServiceRegistration) &&
descriptor.ImplementationInstance is AirAppPublicIpcServiceRegistration existing &&
existing.ContractType == typeof(TContract) &&
string.Equals(existing.ObjectId, objectId, StringComparison.Ordinal)))
{
services.AddSingleton(new PluginPublicIpcServiceRegistration(
services.AddSingleton(new AirAppPublicIpcServiceRegistration(
typeof(TContract),
objectId,
notifyIds ?? []));
@@ -139,11 +160,11 @@ public static class PluginServiceCollectionExtensions
return services;
}
public static IServiceCollection AddPluginPublicIpcContributor<TContributor>(this IServiceCollection services)
where TContributor : class, IPluginPublicIpcContributor
public static IServiceCollection AddAirAppPublicIpcContributor<TContributor>(this IServiceCollection services)
where TContributor : class, IAirAppPublicIpcContributor
{
ArgumentNullException.ThrowIfNull(services);
services.AddSingleton<IPluginPublicIpcContributor, TContributor>();
services.AddSingleton<IAirAppPublicIpcContributor, TContributor>();
return services;
}

View File

@@ -0,0 +1,6 @@
namespace LanMountainDesktop.AirAppSdk;
public sealed record AirAppServiceExportDescriptor(
string ProviderAirAppId,
Type ContractType,
object ServiceInstance);

View File

@@ -1,8 +1,8 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public sealed class PluginServiceExportRegistration
public sealed class AirAppServiceExportRegistration
{
public PluginServiceExportRegistration(Type contractType, Type implementationType)
public AirAppServiceExportRegistration(Type contractType, Type implementationType)
{
ArgumentNullException.ThrowIfNull(contractType);
ArgumentNullException.ThrowIfNull(implementationType);

View File

@@ -1,8 +1,8 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public sealed class SettingsOptionChoice
public sealed class AirAppSettingOptionChoice
{
public SettingsOptionChoice(string value, string titleLocalizationKey)
public AirAppSettingOptionChoice(string value, string titleLocalizationKey)
{
ArgumentException.ThrowIfNullOrWhiteSpace(value);
ArgumentException.ThrowIfNullOrWhiteSpace(titleLocalizationKey);

View File

@@ -1,16 +1,16 @@
using System.Collections.Generic;
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public sealed class SettingsOptionDefinition
public sealed class AirAppSettingOptionDefinition
{
public SettingsOptionDefinition(
public AirAppSettingOptionDefinition(
string key,
SettingsOptionType optionType,
AirAppSettingOptionType optionType,
string titleLocalizationKey,
string? descriptionLocalizationKey = null,
object? defaultValue = null,
IReadOnlyList<SettingsOptionChoice>? choices = null,
IReadOnlyList<AirAppSettingOptionChoice>? choices = null,
double? minimum = null,
double? maximum = null,
string? validationPattern = null)
@@ -35,7 +35,7 @@ public sealed class SettingsOptionDefinition
public string Key { get; }
public SettingsOptionType OptionType { get; }
public AirAppSettingOptionType OptionType { get; }
public string TitleLocalizationKey { get; }
@@ -43,7 +43,7 @@ public sealed class SettingsOptionDefinition
public object? DefaultValue { get; }
public IReadOnlyList<SettingsOptionChoice> Choices { get; }
public IReadOnlyList<AirAppSettingOptionChoice> Choices { get; }
public double? Minimum { get; }

View File

@@ -1,6 +1,6 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public enum SettingsOptionType
public enum AirAppSettingOptionType
{
Toggle = 0,
Select = 1,

View File

@@ -1,14 +1,14 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public static class SettingsCategories
public static class AirAppSettingsCategories
{
public const string General = "General";
public const string Appearance = "Appearance";
public const string Components = "Components";
public const string Plugins = "Plugins";
public const string PluginCatalog = "PluginCatalog";
[Obsolete("Use PluginCatalog instead.")]
public const string PluginMarket = PluginCatalog;
public const string AirApps = "AirApps";
public const string AirAppCatalog = "AirAppCatalog";
[Obsolete("Use AirAppCatalog instead.")]
public const string AirAppMarket = AirAppCatalog;
public const string Update = "Update";
public const string About = "About";
public const string Advanced = "Advanced";

View File

@@ -1,9 +1,9 @@
using System;
using Avalonia.Controls;
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public abstract class SettingsPageBase : UserControl
public abstract class AirAppSettingsPageBase : UserControl
{
public static readonly string DialogHostIdentifier = "LanMountainDesktop.SettingsWindow";

View File

@@ -0,0 +1,14 @@
namespace LanMountainDesktop.AirAppSdk;
public enum AirAppSettingsPageCategory
{
General = 0,
Appearance = 10,
Components = 20,
AirApps = 30,
AirAppCatalog = 35,
[Obsolete("Use AirAppCatalog instead.")]
AirAppMarket = 35,
About = 40,
Dev = 50
}

View File

@@ -1,14 +1,14 @@
using System;
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class SettingsPageInfoAttribute : Attribute
public sealed class AirAppSettingsPageInfoAttribute : Attribute
{
public SettingsPageInfoAttribute(
public AirAppSettingsPageInfoAttribute(
string id,
string name,
SettingsPageCategory category)
AirAppSettingsPageCategory category)
{
ArgumentException.ThrowIfNullOrWhiteSpace(id);
ArgumentException.ThrowIfNullOrWhiteSpace(name);
@@ -22,7 +22,7 @@ public sealed class SettingsPageInfoAttribute : Attribute
public string Name { get; }
public SettingsPageCategory Category { get; }
public AirAppSettingsPageCategory Category { get; }
public string? TitleLocalizationKey { get; init; }
@@ -42,5 +42,5 @@ public sealed class SettingsPageInfoAttribute : Attribute
public string? GroupId { get; init; }
public SettingsScope Scope { get; init; } = SettingsScope.App;
public AirAppSettingsScope Scope { get; init; } = AirAppSettingsScope.App;
}

View File

@@ -0,0 +1,9 @@
namespace LanMountainDesktop.AirAppSdk;
public enum AirAppSettingsScope
{
App = 0,
Launcher = 1,
AirApp = 2,
ComponentInstance = 3
}

View File

@@ -2,14 +2,14 @@ using System;
using System.Collections.Generic;
using Avalonia.Controls;
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public sealed class PluginSettingsSectionBuilder
public sealed class AirAppSettingsSectionBuilder
{
private readonly List<SettingsOptionDefinition> _options = [];
private readonly List<AirAppSettingOptionDefinition> _options = [];
private Type? _customViewType;
internal PluginSettingsSectionBuilder(
internal AirAppSettingsSectionBuilder(
string id,
string titleLocalizationKey,
string? descriptionLocalizationKey,
@@ -35,17 +35,17 @@ public sealed class PluginSettingsSectionBuilder
public Type? CustomViewType => _customViewType;
public IReadOnlyList<SettingsOptionDefinition> Options => _options;
public IReadOnlyList<AirAppSettingOptionDefinition> Options => _options;
/// <summary>
/// Sets a custom AXAML view for this settings section.
/// The view type must be a subclass of <see cref="SettingsPageBase"/>.
/// The view type must be a subclass of <see cref="AirAppSettingsPageBase"/>.
/// When a custom view is provided, the host application will use it directly
/// instead of generating a page from the declared options, allowing the plugin
/// to use any Fluent Avalonia controls and custom layouts.
/// </summary>
/// <typeparam name="TView">A <see cref="SettingsPageBase"/> subclass that defines the settings UI.</typeparam>
public PluginSettingsSectionBuilder SetCustomView<TView>() where TView : SettingsPageBase
/// <typeparam name="TView">A <see cref="AirAppSettingsPageBase"/> subclass that defines the settings UI.</typeparam>
public AirAppSettingsSectionBuilder SetCustomView<TView>() where TView : AirAppSettingsPageBase
{
_customViewType = typeof(TView);
return this;
@@ -53,19 +53,19 @@ public sealed class PluginSettingsSectionBuilder
/// <summary>
/// Sets a custom AXAML view for this settings section.
/// The view type must be a subclass of <see cref="SettingsPageBase"/>.
/// The view type must be a subclass of <see cref="AirAppSettingsPageBase"/>.
/// When a custom view is provided, the host application will use it directly
/// instead of generating a page from the declared options.
/// </summary>
/// <param name="viewType">A <see cref="SettingsPageBase"/> subclass type that defines the settings UI.</param>
public PluginSettingsSectionBuilder SetCustomView(Type viewType)
/// <param name="viewType">A <see cref="AirAppSettingsPageBase"/> subclass type that defines the settings UI.</param>
public AirAppSettingsSectionBuilder SetCustomView(Type viewType)
{
ArgumentNullException.ThrowIfNull(viewType);
if (!typeof(SettingsPageBase).IsAssignableFrom(viewType))
if (!typeof(AirAppSettingsPageBase).IsAssignableFrom(viewType))
{
throw new ArgumentException(
$"Custom view type must be a subclass of {nameof(SettingsPageBase)}.",
$"Custom view type must be a subclass of {nameof(AirAppSettingsPageBase)}.",
nameof(viewType));
}
@@ -73,44 +73,44 @@ public sealed class PluginSettingsSectionBuilder
return this;
}
public PluginSettingsSectionBuilder AddOption(SettingsOptionDefinition option)
public AirAppSettingsSectionBuilder AddOption(AirAppSettingOptionDefinition option)
{
ArgumentNullException.ThrowIfNull(option);
_options.Add(option);
return this;
}
public PluginSettingsSectionBuilder AddToggle(
public AirAppSettingsSectionBuilder AddToggle(
string key,
string titleLocalizationKey,
string? descriptionLocalizationKey = null,
bool defaultValue = false)
{
return AddOption(new SettingsOptionDefinition(
return AddOption(new AirAppSettingOptionDefinition(
key,
SettingsOptionType.Toggle,
AirAppSettingOptionType.Toggle,
titleLocalizationKey,
descriptionLocalizationKey,
defaultValue));
}
public PluginSettingsSectionBuilder AddText(
public AirAppSettingsSectionBuilder AddText(
string key,
string titleLocalizationKey,
string? descriptionLocalizationKey = null,
string defaultValue = "",
string? validationPattern = null)
{
return AddOption(new SettingsOptionDefinition(
return AddOption(new AirAppSettingOptionDefinition(
key,
SettingsOptionType.Text,
AirAppSettingOptionType.Text,
titleLocalizationKey,
descriptionLocalizationKey,
defaultValue,
validationPattern: validationPattern));
}
public PluginSettingsSectionBuilder AddNumber(
public AirAppSettingsSectionBuilder AddNumber(
string key,
string titleLocalizationKey,
string? descriptionLocalizationKey = null,
@@ -118,9 +118,9 @@ public sealed class PluginSettingsSectionBuilder
double? minimum = null,
double? maximum = null)
{
return AddOption(new SettingsOptionDefinition(
return AddOption(new AirAppSettingOptionDefinition(
key,
SettingsOptionType.Number,
AirAppSettingOptionType.Number,
titleLocalizationKey,
descriptionLocalizationKey,
defaultValue,
@@ -128,56 +128,56 @@ public sealed class PluginSettingsSectionBuilder
maximum: maximum));
}
public PluginSettingsSectionBuilder AddSelect(
public AirAppSettingsSectionBuilder AddSelect(
string key,
string titleLocalizationKey,
IEnumerable<SettingsOptionChoice> choices,
IEnumerable<AirAppSettingOptionChoice> choices,
string? descriptionLocalizationKey = null,
string? defaultValue = null)
{
ArgumentNullException.ThrowIfNull(choices);
var normalizedChoices = choices.ToArray();
return AddOption(new SettingsOptionDefinition(
return AddOption(new AirAppSettingOptionDefinition(
key,
SettingsOptionType.Select,
AirAppSettingOptionType.Select,
titleLocalizationKey,
descriptionLocalizationKey,
defaultValue,
normalizedChoices));
}
public PluginSettingsSectionBuilder AddPath(
public AirAppSettingsSectionBuilder AddPath(
string key,
string titleLocalizationKey,
string? descriptionLocalizationKey = null,
string defaultValue = "")
{
return AddOption(new SettingsOptionDefinition(
return AddOption(new AirAppSettingOptionDefinition(
key,
SettingsOptionType.Path,
AirAppSettingOptionType.Path,
titleLocalizationKey,
descriptionLocalizationKey,
defaultValue));
}
public PluginSettingsSectionBuilder AddList(
public AirAppSettingsSectionBuilder AddList(
string key,
string titleLocalizationKey,
string? descriptionLocalizationKey = null,
IReadOnlyList<string>? defaultValue = null)
{
return AddOption(new SettingsOptionDefinition(
return AddOption(new AirAppSettingOptionDefinition(
key,
SettingsOptionType.List,
AirAppSettingOptionType.List,
titleLocalizationKey,
descriptionLocalizationKey,
defaultValue ?? Array.Empty<string>()));
}
internal PluginSettingsSectionRegistration Build()
internal AirAppSettingsSectionRegistration Build()
{
return new PluginSettingsSectionRegistration(
return new AirAppSettingsSectionRegistration(
Id,
TitleLocalizationKey,
_options.ToArray(),

View File

@@ -1,19 +1,19 @@
using System.Collections.Generic;
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public sealed class SettingsSectionDefinition
public sealed class AirAppSettingsSectionDefinition
{
public SettingsSectionDefinition(
public AirAppSettingsSectionDefinition(
string id,
string category,
SettingsScope scope,
AirAppSettingsScope scope,
string titleLocalizationKey,
string? descriptionLocalizationKey = null,
string iconKey = "Settings",
int sortOrder = 0,
string? subjectId = null,
IReadOnlyList<SettingsOptionDefinition>? options = null)
IReadOnlyList<AirAppSettingOptionDefinition>? options = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(id);
ArgumentException.ThrowIfNullOrWhiteSpace(category);
@@ -37,7 +37,7 @@ public sealed class SettingsSectionDefinition
public string Category { get; }
public SettingsScope Scope { get; }
public AirAppSettingsScope Scope { get; }
public string TitleLocalizationKey { get; }
@@ -49,5 +49,5 @@ public sealed class SettingsSectionDefinition
public string? SubjectId { get; }
public IReadOnlyList<SettingsOptionDefinition> Options { get; }
public IReadOnlyList<AirAppSettingOptionDefinition> Options { get; }
}

View File

@@ -1,14 +1,14 @@
using System;
using System.Collections.Generic;
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public sealed class PluginSettingsSectionRegistration
public sealed class AirAppSettingsSectionRegistration
{
public PluginSettingsSectionRegistration(
public AirAppSettingsSectionRegistration(
string id,
string titleLocalizationKey,
IReadOnlyList<SettingsOptionDefinition> options,
IReadOnlyList<AirAppSettingOptionDefinition> options,
string? descriptionLocalizationKey = null,
string iconKey = "PuzzlePiece",
int sortOrder = 0,
@@ -27,10 +27,10 @@ public sealed class PluginSettingsSectionRegistration
SortOrder = sortOrder;
Options = options ?? [];
if (customViewType is not null && !typeof(SettingsPageBase).IsAssignableFrom(customViewType))
if (customViewType is not null && !typeof(AirAppSettingsPageBase).IsAssignableFrom(customViewType))
{
throw new ArgumentException(
$"Custom view type must be a subclass of {nameof(SettingsPageBase)}.",
$"Custom view type must be a subclass of {nameof(AirAppSettingsPageBase)}.",
nameof(customViewType));
}
@@ -47,10 +47,10 @@ public sealed class PluginSettingsSectionRegistration
public int SortOrder { get; }
public IReadOnlyList<SettingsOptionDefinition> Options { get; }
public IReadOnlyList<AirAppSettingOptionDefinition> Options { get; }
/// <summary>
/// When set, the host application will instantiate this <see cref="SettingsPageBase"/> subclass
/// When set, the host application will instantiate this <see cref="AirAppSettingsPageBase"/> subclass
/// instead of generating a page from <see cref="Options"/>.
/// This allows plugins to provide fully custom AXAML views with any Fluent Avalonia controls.
/// </summary>

View File

@@ -1,8 +1,8 @@
using System.Text.Json.Serialization;
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public sealed record PluginSharedContractReference(
public sealed record AirAppSharedContractReference(
string Id,
string Version,
string AssemblyName)
@@ -16,7 +16,7 @@ public sealed record PluginSharedContractReference(
[JsonIgnore]
public string NormalizedAssemblyName => AssemblyName.Trim();
internal PluginSharedContractReference NormalizeAndValidate(string manifestPath)
internal AirAppSharedContractReference NormalizeAndValidate(string manifestPath)
{
var normalized = this with
{
@@ -28,7 +28,7 @@ public sealed record PluginSharedContractReference(
if (!System.Version.TryParse(normalized.Version, out _))
{
throw new InvalidOperationException(
$"Plugin manifest '{manifestPath}' declares invalid shared contract version '{normalized.Version}' for '{normalized.Id}'.");
$"AirApp manifest '{manifestPath}' declares invalid shared contract version '{normalized.Version}' for '{normalized.Id}'.");
}
return normalized;
@@ -39,7 +39,7 @@ public sealed record PluginSharedContractReference(
if (string.IsNullOrWhiteSpace(value))
{
throw new InvalidOperationException(
$"Plugin manifest '{manifestPath}' is missing required shared contract property '{propertyName}'.");
$"AirApp manifest '{manifestPath}' is missing required shared contract property '{propertyName}'.");
}
return value.Trim();

View File

@@ -0,0 +1,11 @@
namespace LanMountainDesktop.AirAppSdk;
/// <summary>
/// Describes a window entry contributed by an AirApp.
/// Produced by <c>AddAirAppWindow&lt;TWindow&gt;</c> and resolved by the AirAppHost
/// window loader to open the window content inside the host window shell.
/// </summary>
public sealed record AirAppWindowRegistration(
string WindowId,
string DisplayName,
Type WindowType);

View File

@@ -1,14 +1,14 @@
using Microsoft.Extensions.DependencyInjection;
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public abstract class PluginWorkerBase : IPluginWorker
public abstract class AirAppWorkerBase : IAirAppWorker
{
public virtual void ConfigureServices(IPluginWorkerContext context, IServiceCollection services)
public virtual void ConfigureServices(IAirAppWorkerContext context, IServiceCollection services)
{
}
public virtual Task StartAsync(IPluginWorkerContext context, IServiceProvider services, CancellationToken cancellationToken = default)
public virtual Task StartAsync(IAirAppWorkerContext context, IServiceProvider services, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}

View File

@@ -0,0 +1,6 @@
namespace LanMountainDesktop.AirAppSdk;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class AirAppWorkerEntranceAttribute : Attribute
{
}

View File

@@ -1,4 +1,4 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
/// <summary>
/// Provides the latest read-only appearance snapshot when host appearance values change.
@@ -6,7 +6,7 @@ namespace LanMountainDesktop.PluginSdk;
public sealed class AppearanceChangedEvent : EventArgs
{
public AppearanceChangedEvent(
PluginAppearanceSnapshot snapshot,
AirAppAppearanceSnapshot snapshot,
IReadOnlyCollection<AppearanceProperty> changedProperties)
{
ArgumentNullException.ThrowIfNull(snapshot);
@@ -16,7 +16,7 @@ public sealed class AppearanceChangedEvent : EventArgs
ChangedProperties = changedProperties;
}
public PluginAppearanceSnapshot Snapshot { get; }
public AirAppAppearanceSnapshot Snapshot { get; }
public IReadOnlyCollection<AppearanceProperty> ChangedProperties { get; }

View File

@@ -1,12 +1,12 @@
namespace LanMountainDesktop.PluginIsolation.Contracts;
namespace LanMountainDesktop.AirAppIsolation.Contracts;
/// <summary>
/// Wire request for the IPC appearance snapshot payload. This request targets the
/// isolation-contract DTOs, not the runtime SDK snapshot with the same type name.
/// </summary>
public sealed record PluginAppearanceSnapshotRequest(string SessionId);
public sealed record AirAppAppearanceSnapshotRequest(string SessionId);
public sealed record PluginMaterialSurfaceSnapshot(
public sealed record AirAppMaterialSurfaceSnapshot(
string BackgroundColor,
string BorderColor,
double BlurRadius,
@@ -14,9 +14,9 @@ public sealed record PluginMaterialSurfaceSnapshot(
/// <summary>
/// Wire-format appearance snapshot exchanged over IPC.
/// Do not treat this as the same type as <c>LanMountainDesktop.PluginSdk.PluginAppearanceSnapshot</c>.
/// Do not treat this as the same type as <c>LanMountainDesktop.AirAppSdk.AirAppAppearanceSnapshot</c>.
/// </summary>
public sealed record PluginAppearanceSnapshot(
public sealed record AirAppAppearanceSnapshot(
string ThemeVariant,
string? AccentColor = null,
double CornerRadiusScale = 1.0,
@@ -26,10 +26,10 @@ public sealed record PluginAppearanceSnapshot(
string? ColorSource = null,
string? SystemMaterialMode = null,
IReadOnlyDictionary<string, string>? ColorRoles = null,
IReadOnlyDictionary<string, PluginMaterialSurfaceSnapshot>? MaterialSurfaces = null,
IReadOnlyDictionary<string, AirAppMaterialSurfaceSnapshot>? MaterialSurfaces = null,
IReadOnlyList<string>? WallpaperSeedCandidates = null);
/// <summary>
/// Wire notification carrying the IPC appearance snapshot.
/// </summary>
public sealed record PluginAppearanceChangedNotification(PluginAppearanceSnapshot Snapshot);
public sealed record AirAppAppearanceChangedNotification(AirAppAppearanceSnapshot Snapshot);

View File

@@ -1,8 +1,8 @@
using Avalonia;
using LanMountainDesktop.Settings.Core;
using LanMountainDesktop.AirAppSdk;
using LanMountainDesktop.Shared.Contracts;
namespace LanMountainDesktop.Appearance;
namespace LanMountainDesktop.AirAppSdk;
public static class AppearanceCornerRadiusTokenFactory
{

View File

@@ -1,4 +1,4 @@
using Avalonia.Metadata;
[assembly: XmlnsPrefix("http://lanmountain.tech/schemas/xaml/sdk", "lmd")]
[assembly: XmlnsDefinition("http://lanmountain.tech/schemas/xaml/sdk", "LanMountainDesktop.PluginSdk")]
[assembly: XmlnsDefinition("http://lanmountain.tech/schemas/xaml/sdk", "LanMountainDesktop.AirAppSdk")]

View File

@@ -1,21 +1,21 @@
namespace LanMountainDesktop.PluginIsolation.Contracts;
namespace LanMountainDesktop.AirAppIsolation.Contracts;
public sealed record PluginHeartbeatPing(
public sealed record AirAppHeartbeatPing(
string SessionId,
DateTimeOffset SentAtUtc);
public sealed record PluginHeartbeatPong(
public sealed record AirAppHeartbeatPong(
string SessionId,
DateTimeOffset ReceivedAtUtc);
public sealed record PluginLogEntry(
public sealed record AirAppLogEntry(
string Level,
string Category,
string Message,
DateTimeOffset TimestampUtc,
string? Exception = null);
public static class PluginLogLevels
public static class AirAppLogLevels
{
public const string Trace = "trace";
public const string Debug = "debug";
@@ -25,7 +25,7 @@ public static class PluginLogLevels
public const string Critical = "critical";
}
public sealed record PluginFaultReport(
public sealed record AirAppFaultReport(
string SessionId,
string FaultKind,
bool IsFatal,
@@ -35,7 +35,7 @@ public sealed record PluginFaultReport(
int? ExitCode = null,
DateTimeOffset? OccurredAtUtc = null);
public static class PluginFaultKinds
public static class AirAppFaultKinds
{
public const string ManagedException = "managed-exception";
public const string NativeCrash = "native-crash";

View File

@@ -1,4 +1,4 @@
namespace LanMountainDesktop.Settings.Core;
namespace LanMountainDesktop.AirAppSdk;
public static class GlobalAppearanceSettings
{

View File

@@ -4,7 +4,10 @@ using Microsoft.Extensions.Hosting;
namespace LanMountainDesktop.AirAppSdk;
/// <summary>
/// Core interface for AirApp entry point.
/// Core interface for the AirApp entry point.
/// Implement this interface (or derive from <see cref="AirAppBase"/>) and mark the class
/// with <see cref="AirAppEntranceAttribute"/> to expose a desktop component or window
/// AirApp to the LanMountainDesktop host.
/// </summary>
public interface IAirApp
{

View File

@@ -1,14 +1,14 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
/// <summary>
/// 插件外观上下文接口,提供主题、圆角等外观资源的访问和变更通知。
/// </summary>
public interface IPluginAppearanceContext
public interface IAirAppAppearanceContext
{
/// <summary>
/// 当前外观快照。
/// </summary>
PluginAppearanceSnapshot Snapshot { get; }
AirAppAppearanceSnapshot Snapshot { get; }
/// <summary>
/// 外观变更事件。当主题、圆角或其他外观属性发生变化时触发。
@@ -31,5 +31,5 @@ public interface IPluginAppearanceContext
/// <param name="minimum">最小值(可选)</param>
/// <param name="maximum">最大值(可选)</param>
/// <returns>解析后的圆角半径</returns>
double ResolveCornerRadius(PluginCornerRadiusPreset preset, double? minimum = null, double? maximum = null);
double ResolveCornerRadius(AirAppCornerRadiusPreset preset, double? minimum = null, double? maximum = null);
}

View File

@@ -0,0 +1,13 @@
namespace LanMountainDesktop.AirAppSdk;
public interface IAirAppExportRegistry
{
IReadOnlyList<AirAppServiceExportDescriptor> GetExports();
IReadOnlyList<AirAppServiceExportDescriptor> GetExports(Type contractType);
AirAppServiceExportDescriptor? GetExport(Type contractType, string providerAirAppId);
TContract? GetExport<TContract>(string providerAirAppId)
where TContract : class;
}

View File

@@ -2,9 +2,20 @@ namespace LanMountainDesktop.AirAppSdk;
/// <summary>
/// Message bus for inter-AirApp communication.
/// Supports both strong-typed messages and topic-based routing.
/// </summary>
public interface IAirAppMessageBus
{
/// <summary>
/// Subscribe to a strong-typed message.
/// </summary>
IDisposable Subscribe<TMessage>(Action<TMessage> handler);
/// <summary>
/// Publish a strong-typed message.
/// </summary>
void Publish<TMessage>(TMessage message);
/// <summary>
/// Publish a message to a topic.
/// </summary>
@@ -21,7 +32,7 @@ public interface IAirAppMessageBus
IDisposable Subscribe(string topic, Action<object?> handler);
/// <summary>
/// Subscribe to a topic with typed payload.
/// Subscribe to a topic with a typed payload.
/// </summary>
/// <typeparam name="T">Payload type</typeparam>
/// <param name="topic">Message topic</param>

View File

@@ -0,0 +1,8 @@
namespace LanMountainDesktop.AirAppSdk;
public interface IAirAppPackageManager
{
IReadOnlyList<AirAppInstalledInfo> GetInstalledAirApps();
AirAppPackageInstallResult InstallPackage(string packagePath);
}

View File

@@ -1,13 +1,13 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public interface IPluginPublicIpcBuilder
public interface IAirAppPublicIpcBuilder
{
IPluginPublicIpcBuilder AddService<TContract>(
IAirAppPublicIpcBuilder AddService<TContract>(
string? objectId = null,
IEnumerable<string>? notifyIds = null)
where TContract : class;
IPluginPublicIpcBuilder AddService(
IAirAppPublicIpcBuilder AddService(
Type contractType,
object implementation,
string? objectId = null,

View File

@@ -0,0 +1,6 @@
namespace LanMountainDesktop.AirAppSdk;
public interface IAirAppPublicIpcContributor
{
void ConfigurePublicIpc(IAirAppPublicIpcBuilder builder);
}

View File

@@ -4,23 +4,20 @@ namespace LanMountainDesktop.AirAppSdk;
/// <summary>
/// Provides runtime context and services for an AirApp.
/// The host hands an implementation to <see cref="IAirApp.OnStartedAsync"/> and exposes it
/// through the per-AirApp service container.
/// </summary>
public interface IAirAppRuntimeContext
{
/// <summary>
/// Gets the unique identifier of this AirApp.
/// Gets the manifest of this AirApp.
/// </summary>
string AirAppId { get; }
AirAppManifest Manifest { get; }
/// <summary>
/// Gets the display name of this AirApp.
/// Gets the directory that contains the loaded AirApp package.
/// </summary>
string AirAppName { get; }
/// <summary>
/// Gets the AirApp version.
/// </summary>
string AirAppVersion { get; }
string AirAppDirectory { get; }
/// <summary>
/// Gets the data directory for this AirApp.
@@ -39,6 +36,11 @@ public interface IAirAppRuntimeContext
/// </summary>
IServiceProvider Services { get; }
/// <summary>
/// Gets a snapshot of the host-provided properties for this AirApp.
/// </summary>
IReadOnlyDictionary<string, object?> Properties { get; }
/// <summary>
/// Gets the host application lifetime manager.
/// </summary>
@@ -59,6 +61,16 @@ public interface IAirAppRuntimeContext
/// </summary>
IAirAppLogger Logger { get; }
/// <summary>
/// Resolve a service from the AirApp service provider.
/// </summary>
T? GetService<T>();
/// <summary>
/// Try to read a host-provided property.
/// </summary>
bool TryGetProperty<T>(string key, out T? value);
/// <summary>
/// Opens a window defined by this AirApp.
/// </summary>
@@ -73,17 +85,17 @@ public interface IAirAppRuntimeContext
void CloseWindow(string windowId);
/// <summary>
/// Register a desktop component (internal use by AirAppBase).
/// Register a desktop component (internal use by <see cref="AirAppBase"/>).
/// </summary>
void RegisterComponent(AirAppComponentOptions options);
/// <summary>
/// Register a window (internal use by AirAppBase).
/// Register a window (internal use by <see cref="AirAppBase"/>).
/// </summary>
void RegisterWindow(string id, string name, Type windowType);
/// <summary>
/// Register a service (internal use by AirAppBase).
/// Register a service (internal use by <see cref="AirAppBase"/>).
/// </summary>
void RegisterService<TService, TImplementation>()
where TService : class

View File

@@ -1,10 +1,10 @@
using System.Collections.Generic;
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public interface IPluginSettingsService
public interface IAirAppSettingsService
{
string PluginId { get; }
string AirAppId { get; }
IComponentSettingsAccessor GetComponentAccessor(string componentId, string? placementId);

View File

@@ -12,6 +12,13 @@ public interface IAirAppWindow
/// </summary>
AirAppWindowDescriptor Descriptor { get; }
/// <summary>
/// Gets the root content hosted inside the AirAppHost window shell.
/// <see cref="AirAppWindowBase"/> inherits from Avalonia <c>Window</c> and satisfies this
/// through <c>Window.Content</c>.
/// </summary>
object? Content { get; }
/// <summary>
/// Called before the window is opened.
/// Use this for async initialization.

View File

@@ -1,12 +1,12 @@
using Microsoft.Extensions.DependencyInjection;
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public interface IPluginWorker
public interface IAirAppWorker
{
void ConfigureServices(IPluginWorkerContext context, IServiceCollection services);
void ConfigureServices(IAirAppWorkerContext context, IServiceCollection services);
Task StartAsync(IPluginWorkerContext context, IServiceProvider services, CancellationToken cancellationToken = default);
Task StartAsync(IAirAppWorkerContext context, IServiceProvider services, CancellationToken cancellationToken = default);
Task StopAsync(CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,26 @@
using LanMountainDesktop.AirAppIsolation.Contracts;
namespace LanMountainDesktop.AirAppSdk;
public interface IAirAppWorkerContext
{
string AirAppId { get; }
AirAppManifest Manifest { get; }
AirAppRuntimeMode RuntimeMode { get; }
string SessionId { get; }
string HostPipeName { get; }
string ProtocolVersion { get; }
string AirAppDirectory { get; }
string DataDirectory { get; }
IReadOnlyList<AirAppCapabilityDeclaration> GrantedCapabilities { get; }
IReadOnlyDictionary<string, string> StartupProperties { get; }
}

View File

@@ -1,4 +1,4 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public interface IComponentEditorHostContext
{

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public interface IComponentSettingsAccessor
{

View File

@@ -1,4 +1,4 @@
namespace LanMountainDesktop.Host.Abstractions;
namespace LanMountainDesktop.AirAppSdk;
public interface IDesktopShellHost
{

View File

@@ -1,4 +1,4 @@
namespace LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.AirAppSdk;
public sealed record HostApplicationLifecycleRequest(
string? Source = null,

View File

@@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace LanMountainDesktop.AirAppSdk;
public interface ISettingsCatalog
{
IReadOnlyList<AirAppSettingsSectionDefinition> GetSections();
IReadOnlyList<AirAppSettingsSectionDefinition> GetSections(AirAppSettingsScope scope);
}

Some files were not shown because too many files have changed in this diff Show More