mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 15:44:25 +08:00
0.7.1
This commit is contained in:
27
.github/workflows/airappmarket-validate.yml
vendored
27
.github/workflows/airappmarket-validate.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: AirAppMarket Validate
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "airappmarket/**"
|
||||
- ".github/workflows/airappmarket-validate.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "airappmarket/**"
|
||||
- ".github/workflows/airappmarket-validate.yml"
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: "10.0.x"
|
||||
|
||||
- name: Validate AirAppMarket index
|
||||
run: dotnet run --project airappmarket/tools/AirAppMarket.Validator -- airappmarket/index.json airappmarket/schema/airappmarket-index.schema.json
|
||||
@@ -1,33 +0,0 @@
|
||||
# LanAirApp (Mirror)
|
||||
|
||||
## 中文
|
||||
|
||||
这里的 `LanAirApp/` 是放在宿主仓库里的镜像副本,只用于本地联调和工作区构建,不是插件市场或插件开发资料的最终权威来源。
|
||||
|
||||
### 这份镜像的角色
|
||||
|
||||
- 提供本地工作区里的 `airappmarket` 索引副本
|
||||
- 提供插件文档、工具和样例镜像,便于和宿主一起联调
|
||||
- 不承担宿主运行时职责
|
||||
|
||||
### 权威来源
|
||||
|
||||
- 插件市场与开发文档:独立 `LanAirApp` 仓库
|
||||
- 权威示例插件:独立 `LanMountainDesktop.SamplePlugin`
|
||||
- 本目录中的 `samples/LanMountainDesktop.SamplePlugin` 只是镜像模板副本
|
||||
|
||||
## English
|
||||
|
||||
This `LanAirApp/` directory is a mirror that lives inside the host repository. It exists for local workspace integration and build convenience only. It is not the final authority for the plugin market or developer-facing plugin materials.
|
||||
|
||||
### Role of this mirror
|
||||
|
||||
- keep a local copy of the `airappmarket` index for workspace integration
|
||||
- keep mirrored docs, tools, and sample templates for local development
|
||||
- avoid duplicating host runtime responsibilities
|
||||
|
||||
### Sources of truth
|
||||
|
||||
- Plugin market and developer docs: standalone `LanAirApp`
|
||||
- Authoritative sample plugin: standalone `LanMountainDesktop.SamplePlugin`
|
||||
- `samples/LanMountainDesktop.SamplePlugin` in this mirror is template/mirror content only
|
||||
@@ -1,16 +0,0 @@
|
||||
# 插件开发指南
|
||||
|
||||
## 中文
|
||||
|
||||
使用 `LanMountainDesktop.PluginSdk` 开发插件时,至少需要准备:
|
||||
|
||||
- `plugin.json`
|
||||
- 插件入口程序集
|
||||
- 入口类
|
||||
- 本地化资源
|
||||
|
||||
推荐从示例插件开始,先完成清单、入口、设置页和桌面组件,再逐步扩展业务逻辑。
|
||||
|
||||
## English
|
||||
|
||||
To build a plugin with `LanMountainDesktop.PluginSdk`, prepare the manifest, plugin assembly, entrance class, and localization resources first.
|
||||
@@ -1,14 +0,0 @@
|
||||
# 插件打包指南
|
||||
|
||||
## 中文
|
||||
|
||||
阑山桌面插件的标准安装格式为 `.laapp`。插件项目应在仓库根目录提供:
|
||||
|
||||
- `.laapp` 安装包
|
||||
- `README.md`
|
||||
|
||||
官方市场索引只负责记录链接和校验信息。
|
||||
|
||||
## English
|
||||
|
||||
The standard package format is `.laapp`. Plugin repositories should keep the package and `README.md` in the repository root, while the official market index stores metadata and validation data.
|
||||
@@ -1,30 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>1.0.0</Version>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<OutputPath>bin\$(Configuration)\$(TargetFramework)\content\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<PluginPackageOutputDirectory>$(MSBuildThisFileDirectory)artifacts\Packages\</PluginPackageOutputDirectory>
|
||||
<PluginPackagePath>$(PluginPackageOutputDirectory)$(AssemblyName).$(Version).laapp</PluginPackagePath>
|
||||
<LegacyLoosePluginOutputDirectory>$(MSBuildThisFileDirectory)artifacts\Loose\</LegacyLoosePluginOutputDirectory>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" Private="false" />
|
||||
<None Include="plugin.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Include="Localization\*.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CreateLaappPackage" AfterTargets="Build">
|
||||
<MakeDir Directories="$(PluginPackageOutputDirectory)" />
|
||||
<RemoveDir Directories="$(LegacyLoosePluginOutputDirectory)" />
|
||||
<Delete Files="$(PluginPackagePath)" TreatErrorsAsWarnings="true" />
|
||||
<ZipDirectory SourceDirectory="$(OutputPath)" DestinationFile="$(PluginPackagePath)" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -1,84 +0,0 @@
|
||||
{
|
||||
"settings.page_title": "Plugin Status",
|
||||
"plugin.name": "LanMountain Sample Plugin",
|
||||
"plugin.description": "Example plugin used to validate PluginSdk loading, services, communication, and localization.",
|
||||
"widget.display_name": "Sample Plugin Status Clock",
|
||||
"widget.category": "Plugins",
|
||||
"settings.header.title": "Sample Plugin Capability Inspector",
|
||||
"settings.section.info": "Plugin Info",
|
||||
"settings.section.capabilities": "Accessible Capabilities",
|
||||
"settings.section.status": "Live Runtime Status",
|
||||
"settings.info.plugin_name": "Plugin Name",
|
||||
"settings.info.plugin_id": "Plugin Id",
|
||||
"settings.info.version": "Version",
|
||||
"settings.info.author": "Author",
|
||||
"settings.info.description": "Description",
|
||||
"settings.info.plugin_directory": "Plugin Directory",
|
||||
"settings.info.data_directory": "Data Directory",
|
||||
"settings.info.host_application": "Host Application",
|
||||
"settings.info.host_version": "Host Version",
|
||||
"settings.info.sdk_api_version": "SDK API Version",
|
||||
"settings.info.state_service_resolved": "State Service Resolved",
|
||||
"settings.info.clock_service_resolved": "Clock Service Resolved",
|
||||
"settings.info.message_bus_resolved": "Message Bus Resolved",
|
||||
"settings.info.component_placed": "Component Placed",
|
||||
"settings.info.placed_count": "Placed Count",
|
||||
"settings.info.preview_count": "Preview Count",
|
||||
"settings.info.placement_ids": "Placement Ids",
|
||||
"settings.info.last_component_id": "Last Component Id",
|
||||
"settings.info.last_cell_size": "Last Cell Size",
|
||||
"settings.info.clock_service_time": "Clock Service Time",
|
||||
"settings.status.updated_at": "Updated: {0}",
|
||||
"status.frontend.title": "Frontend Status",
|
||||
"status.component.title": "Component Status",
|
||||
"status.backend.title": "Backend Status",
|
||||
"status.service.title": "Clock Service",
|
||||
"status.summary.pending": "Pending",
|
||||
"status.summary.attached": "Attached",
|
||||
"status.summary.healthy": "Healthy",
|
||||
"status.summary.faulted": "Faulted",
|
||||
"status.summary.placed": "Placed",
|
||||
"status.summary.preview": "Preview",
|
||||
"status.frontend.detail.pending": "Waiting for a plugin UI surface to connect.",
|
||||
"status.frontend.detail.settings_connected": "Settings page is connected to plugin services and communication.",
|
||||
"status.frontend.detail.widget_connected": "Widget surface is connected to plugin services and communication.",
|
||||
"status.component.detail.pending": "No component instance has been created yet.",
|
||||
"status.component.detail.none": "No component instance is active.",
|
||||
"status.component.detail.preview": "Preview instances: {0}; no placed desktop instance is active yet.",
|
||||
"status.component.detail.placed": "Placed count: {0}; preview count: {1}; placements: {2}",
|
||||
"status.backend.detail.pending": "Plugin initialization is in progress.",
|
||||
"status.backend.detail.log_written": "Initialization log written to: {0}",
|
||||
"status.backend.detail.log_write_failed": "Initialization log write failed: {0}",
|
||||
"status.service.detail.pending": "Clock service is not attached yet.",
|
||||
"status.service.detail.attached": "Clock service was attached and is waiting for the first tick.",
|
||||
"status.service.detail.running": "Clock service is running. Current service time: {0}",
|
||||
"status.service.detail.write_failed": "Clock state write failed: {0}",
|
||||
"capability.manifest.title": "IPluginContext.Manifest",
|
||||
"capability.manifest.detail": "Readable. Current plugin id: {0}; version: {1}.",
|
||||
"capability.directories.title": "IPluginContext.PluginDirectory / DataDirectory",
|
||||
"capability.directories.detail": "Readable. Plugin directory: {0}; data directory: {1}.",
|
||||
"capability.properties.title": "IPluginContext.Properties",
|
||||
"capability.properties.detail": "Readable. Host properties currently exposed: {0}.",
|
||||
"capability.get_service.title": "IPluginContext.GetService<T>()",
|
||||
"capability.get_service.detail": "Callable. State service resolved: {0}; clock service resolved: {1}; message bus resolved: {2}.",
|
||||
"capability.register_service.title": "IPluginContext.RegisterService<TService>()",
|
||||
"capability.register_service.detail": "Callable during plugin initialization. This sample plugin registers SamplePluginRuntimeStateService and SamplePluginClockService into the plugin service container.",
|
||||
"capability.message_bus.title": "Plugin Communication Bus",
|
||||
"capability.message_bus.detail": "This sample plugin uses IPluginMessageBus to push clock ticks and state change notifications into plugin UI surfaces.",
|
||||
"capability.widget_context.title": "PluginDesktopComponentContext",
|
||||
"capability.widget_context.detail": "Widgets can read ComponentId, PlacementId, CellSize, and call GetService<T>() against the same plugin service container.",
|
||||
"widget.close_desktop.display_name": "Close Desktop",
|
||||
"widget.close_desktop.text": "Close Desktop",
|
||||
"widget.close_desktop.hint": "Exit LanMountainDesktop on click",
|
||||
"widget.close_desktop.unavailable": "Host lifecycle API is unavailable",
|
||||
"widget.close_desktop.failed": "Host rejected the exit request",
|
||||
"widget.subtitle.preview": "Preview surface | placed: {0}",
|
||||
"widget.subtitle.placement": "Placement {0} | placed: {1}",
|
||||
"common.dev": "dev",
|
||||
"common.none": "(none)",
|
||||
"common.unknown": "(unknown)",
|
||||
"common.true": "true",
|
||||
"common.false": "false",
|
||||
"common.yes": "Yes",
|
||||
"common.no": "No"
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
{
|
||||
"settings.page_title": "插件状态",
|
||||
"plugin.name": "阑山示例插件",
|
||||
"plugin.description": "用于验证 PluginSdk 加载、服务、通信与本地化能力的示例插件。",
|
||||
"widget.display_name": "示例插件状态时钟",
|
||||
"widget.category": "插件",
|
||||
"settings.header.title": "示例插件能力检查器",
|
||||
"settings.section.info": "插件信息",
|
||||
"settings.section.capabilities": "可访问能力",
|
||||
"settings.section.status": "实时运行状态",
|
||||
"settings.info.plugin_name": "插件名称",
|
||||
"settings.info.plugin_id": "插件 Id",
|
||||
"settings.info.version": "版本",
|
||||
"settings.info.author": "作者",
|
||||
"settings.info.description": "描述",
|
||||
"settings.info.plugin_directory": "插件目录",
|
||||
"settings.info.data_directory": "数据目录",
|
||||
"settings.info.host_application": "宿主应用",
|
||||
"settings.info.host_version": "宿主版本",
|
||||
"settings.info.sdk_api_version": "SDK API 版本",
|
||||
"settings.info.state_service_resolved": "状态服务已解析",
|
||||
"settings.info.clock_service_resolved": "时钟服务已解析",
|
||||
"settings.info.message_bus_resolved": "消息总线已解析",
|
||||
"settings.info.component_placed": "组件是否已放置",
|
||||
"settings.info.placed_count": "已放置数量",
|
||||
"settings.info.preview_count": "预览数量",
|
||||
"settings.info.placement_ids": "放置位置 Id",
|
||||
"settings.info.last_component_id": "最近组件 Id",
|
||||
"settings.info.last_cell_size": "最近单元尺寸",
|
||||
"settings.info.clock_service_time": "时钟服务时间",
|
||||
"settings.status.updated_at": "更新时间:{0}",
|
||||
"status.frontend.title": "前端状态",
|
||||
"status.component.title": "组件状态",
|
||||
"status.backend.title": "后端状态",
|
||||
"status.service.title": "时钟服务",
|
||||
"status.summary.pending": "等待中",
|
||||
"status.summary.attached": "已挂接",
|
||||
"status.summary.healthy": "正常",
|
||||
"status.summary.faulted": "异常",
|
||||
"status.summary.placed": "已放置",
|
||||
"status.summary.preview": "预览中",
|
||||
"status.frontend.detail.pending": "等待插件界面接入。",
|
||||
"status.frontend.detail.settings_connected": "设置页已接入插件服务与通信。",
|
||||
"status.frontend.detail.widget_connected": "组件界面已接入插件服务与通信。",
|
||||
"status.component.detail.pending": "当前还没有创建组件实例。",
|
||||
"status.component.detail.none": "当前没有活动中的组件实例。",
|
||||
"status.component.detail.preview": "当前预览实例数量:{0};尚未有已放置的桌面实例。",
|
||||
"status.component.detail.placed": "已放置数量:{0};预览数量:{1};放置位置:{2}",
|
||||
"status.backend.detail.pending": "插件初始化进行中。",
|
||||
"status.backend.detail.log_written": "初始化日志已写入:{0}",
|
||||
"status.backend.detail.log_write_failed": "初始化日志写入失败:{0}",
|
||||
"status.service.detail.pending": "时钟服务尚未挂接。",
|
||||
"status.service.detail.attached": "时钟服务已挂接,正在等待第一次心跳。",
|
||||
"status.service.detail.running": "时钟服务运行中,当前服务时间:{0}",
|
||||
"status.service.detail.write_failed": "时钟状态写入失败:{0}",
|
||||
"capability.manifest.title": "IPluginContext.Manifest",
|
||||
"capability.manifest.detail": "可读取。当前插件 id:{0};版本:{1}。",
|
||||
"capability.directories.title": "IPluginContext.PluginDirectory / DataDirectory",
|
||||
"capability.directories.detail": "可读取。插件目录:{0};数据目录:{1}。",
|
||||
"capability.properties.title": "IPluginContext.Properties",
|
||||
"capability.properties.detail": "可读取。宿主当前暴露的属性:{0}。",
|
||||
"capability.get_service.title": "IPluginContext.GetService<T>()",
|
||||
"capability.get_service.detail": "可调用。状态服务已解析:{0};时钟服务已解析:{1};消息总线已解析:{2}。",
|
||||
"capability.register_service.title": "IPluginContext.RegisterService<TService>()",
|
||||
"capability.register_service.detail": "可在插件初始化阶段调用。这个示例插件会把 SamplePluginRuntimeStateService 和 SamplePluginClockService 注册进插件服务容器。",
|
||||
"capability.message_bus.title": "插件通信总线",
|
||||
"capability.message_bus.detail": "这个示例插件通过 IPluginMessageBus 向插件 UI 推送时钟心跳和状态变化通知。",
|
||||
"capability.widget_context.title": "PluginDesktopComponentContext",
|
||||
"capability.widget_context.detail": "组件可以读取 ComponentId、PlacementId、CellSize,并能在同一个插件服务容器上调用 GetService<T>()。",
|
||||
"widget.subtitle.preview": "预览界面 | 已放置:{0}",
|
||||
"widget.subtitle.placement": "位置 {0} | 已放置:{1}",
|
||||
"common.dev": "开发版",
|
||||
"common.none": "(无)",
|
||||
"common.unknown": "(未知)",
|
||||
"common.true": "是",
|
||||
"common.false": "否",
|
||||
"common.yes": "是",
|
||||
"common.no": "否"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# LanMountainDesktop.SamplePlugin
|
||||
|
||||
## 中文
|
||||
|
||||
这是阑山桌面的标准示例插件,用于演示插件清单、设置页、桌面组件、服务注册、本地化和 `.laapp` 打包流程。
|
||||
|
||||
## English
|
||||
|
||||
This is the standard sample plugin used to demonstrate manifests, settings pages, desktop components, service registration, localization, and `.laapp` packaging.
|
||||
@@ -1,100 +0,0 @@
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
namespace LanMountainDesktop.SamplePlugin;
|
||||
|
||||
[PluginEntrance]
|
||||
public sealed class SamplePlugin : PluginBase, IDisposable
|
||||
{
|
||||
private SamplePluginRuntimeStateService? _stateService;
|
||||
private SamplePluginClockService? _clockService;
|
||||
|
||||
public override void Initialize(IPluginContext context)
|
||||
{
|
||||
Directory.CreateDirectory(context.DataDirectory);
|
||||
var localizer = PluginLocalizer.Create(context);
|
||||
|
||||
var hostName = GetHostProperty(context, PluginHostPropertyKeys.HostApplicationName, "UnknownHost");
|
||||
var hostVersion = GetHostProperty(context, PluginHostPropertyKeys.HostVersion, "UnknownVersion");
|
||||
var sdkApiVersion = GetHostProperty(context, PluginHostPropertyKeys.PluginSdkApiVersion, "UnknownApiVersion");
|
||||
var messageBus = context.GetService<IPluginMessageBus>()
|
||||
?? throw new InvalidOperationException("Plugin message bus is not available.");
|
||||
|
||||
_stateService = new SamplePluginRuntimeStateService(
|
||||
context.Manifest,
|
||||
context.PluginDirectory,
|
||||
context.DataDirectory,
|
||||
hostName,
|
||||
hostVersion,
|
||||
sdkApiVersion,
|
||||
messageBus,
|
||||
localizer);
|
||||
context.RegisterService(_stateService);
|
||||
|
||||
_clockService = new SamplePluginClockService(context.DataDirectory, _stateService, messageBus, localizer);
|
||||
context.RegisterService(_clockService);
|
||||
_stateService.AttachClockService(_clockService);
|
||||
|
||||
var logPath = Path.Combine(context.DataDirectory, "sample-plugin.log");
|
||||
var initMessage =
|
||||
$"[{DateTimeOffset.UtcNow:O}] {context.Manifest.Name} initialized in {hostName} (plugin version {context.Manifest.Version ?? "dev"}).";
|
||||
|
||||
try
|
||||
{
|
||||
File.AppendAllText(logPath, initMessage + Environment.NewLine);
|
||||
_stateService.MarkBackendReady(localizer.Format(
|
||||
"status.backend.detail.log_written",
|
||||
"Initialization log written: {0}",
|
||||
logPath));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_stateService.MarkBackendFaulted(localizer.Format(
|
||||
"status.backend.detail.log_write_failed",
|
||||
"Initialization log failed: {0}",
|
||||
ex.Message));
|
||||
throw;
|
||||
}
|
||||
|
||||
_clockService.Start();
|
||||
|
||||
context.RegisterDesktopComponent(new PluginDesktopComponentRegistration(
|
||||
"LanMountainDesktop.SamplePlugin.StatusClock",
|
||||
localizer.GetString("widget.display_name", "Sample Plugin Status Clock"),
|
||||
widgetContext => new SamplePluginStatusClockWidget(widgetContext),
|
||||
iconKey: "PuzzlePiece",
|
||||
category: localizer.GetString("widget.category", "Plugins"),
|
||||
minWidthCells: 4,
|
||||
minHeightCells: 4,
|
||||
allowDesktopPlacement: true,
|
||||
allowStatusBarPlacement: false,
|
||||
resizeMode: PluginDesktopComponentResizeMode.Proportional,
|
||||
cornerRadiusResolver: cellSize => Math.Clamp(cellSize * 0.34, 18, 34)));
|
||||
|
||||
context.RegisterDesktopComponent(new PluginDesktopComponentRegistration(
|
||||
"LanMountainDesktop.SamplePlugin.CloseDesktop",
|
||||
localizer.GetString("widget.close_desktop.display_name", "Close Desktop"),
|
||||
widgetContext => new SamplePluginCloseDesktopWidget(widgetContext),
|
||||
iconKey: "DismissCircle",
|
||||
category: localizer.GetString("widget.category", "Plugins"),
|
||||
minWidthCells: 2,
|
||||
minHeightCells: 1,
|
||||
allowDesktopPlacement: true,
|
||||
allowStatusBarPlacement: false,
|
||||
resizeMode: PluginDesktopComponentResizeMode.Free,
|
||||
cornerRadiusResolver: cellSize => Math.Clamp(cellSize * 0.28, 14, 22)));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_clockService?.Dispose();
|
||||
_clockService = null;
|
||||
_stateService = null;
|
||||
}
|
||||
|
||||
private static string GetHostProperty(IPluginContext context, string key, string fallback)
|
||||
{
|
||||
return context.TryGetProperty<string>(key, out var value) && !string.IsNullOrWhiteSpace(value)
|
||||
? value
|
||||
: fallback;
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
namespace LanMountainDesktop.SamplePlugin;
|
||||
|
||||
internal sealed class SamplePluginCloseDesktopWidget : Border
|
||||
{
|
||||
private readonly PluginLocalizer _localizer;
|
||||
private readonly IHostApplicationLifecycle? _hostApplicationLifecycle;
|
||||
private readonly TextBlock _titleTextBlock;
|
||||
private readonly TextBlock _statusTextBlock;
|
||||
|
||||
public SamplePluginCloseDesktopWidget(PluginDesktopComponentContext context)
|
||||
{
|
||||
_localizer = PluginLocalizer.Create(context);
|
||||
_hostApplicationLifecycle = context.GetService<IHostApplicationLifecycle>();
|
||||
|
||||
_titleTextBlock = new TextBlock
|
||||
{
|
||||
Text = T("widget.close_desktop.text", "关闭桌面"),
|
||||
Foreground = Brushes.White,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
|
||||
_statusTextBlock = new TextBlock
|
||||
{
|
||||
Text = _hostApplicationLifecycle is null
|
||||
? T("widget.close_desktop.unavailable", "宿主未提供退出接口")
|
||||
: T("widget.close_desktop.hint", "点击后退出阑山桌面"),
|
||||
Foreground = new SolidColorBrush(Color.Parse("#FFD4E7F6")),
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
|
||||
var contentGrid = new Grid
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("Auto,*"),
|
||||
ColumnSpacing = 14,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Children =
|
||||
{
|
||||
CreateIconShell(),
|
||||
new StackPanel
|
||||
{
|
||||
Spacing = 2,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Children =
|
||||
{
|
||||
_titleTextBlock,
|
||||
_statusTextBlock
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Grid.SetColumn(contentGrid.Children[1], 1);
|
||||
|
||||
var actionButton = new Button
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
VerticalAlignment = VerticalAlignment.Stretch,
|
||||
HorizontalContentAlignment = HorizontalAlignment.Stretch,
|
||||
VerticalContentAlignment = VerticalAlignment.Stretch,
|
||||
Background = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
Padding = new Thickness(0),
|
||||
IsEnabled = _hostApplicationLifecycle is not null,
|
||||
Content = contentGrid
|
||||
};
|
||||
actionButton.Click += OnButtonClick;
|
||||
|
||||
Background = new LinearGradientBrush
|
||||
{
|
||||
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
|
||||
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
|
||||
GradientStops =
|
||||
[
|
||||
new GradientStop(Color.Parse("#FF0B1220"), 0),
|
||||
new GradientStop(Color.Parse("#FF172554"), 0.55),
|
||||
new GradientStop(Color.Parse("#FF7F1D1D"), 1)
|
||||
]
|
||||
};
|
||||
BorderBrush = new SolidColorBrush(Color.Parse("#66FB7185"));
|
||||
BorderThickness = new Thickness(1);
|
||||
CornerRadius = new CornerRadius(18);
|
||||
Padding = new Thickness(14, 10);
|
||||
Child = actionButton;
|
||||
|
||||
SizeChanged += OnSizeChanged;
|
||||
ApplyScale();
|
||||
}
|
||||
|
||||
private Border CreateIconShell()
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
Width = 36,
|
||||
Height = 36,
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Background = new SolidColorBrush(Color.Parse("#33F87171")),
|
||||
BorderBrush = new SolidColorBrush(Color.Parse("#88FCA5A5")),
|
||||
BorderThickness = new Thickness(1),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = "⏻",
|
||||
FontSize = 18,
|
||||
Foreground = Brushes.White,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextAlignment = TextAlignment.Center
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void OnButtonClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (_hostApplicationLifecycle?.TryExit(new HostApplicationLifecycleRequest(
|
||||
Source: "SamplePlugin.CloseDesktopWidget",
|
||||
Reason: "User invoked the sample plugin close-desktop widget.")) == true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_statusTextBlock.Text = T("widget.close_desktop.failed", "宿主未接受退出请求");
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
ApplyScale();
|
||||
}
|
||||
|
||||
private void ApplyScale()
|
||||
{
|
||||
var basis = Bounds.Height > 1 ? Bounds.Height : 72;
|
||||
Padding = new Thickness(Math.Clamp(basis * 0.18, 12, 18), Math.Clamp(basis * 0.14, 8, 14));
|
||||
CornerRadius = new CornerRadius(Math.Clamp(basis * 0.32, 16, 24));
|
||||
|
||||
if (Child is not Button actionButton || actionButton.Content is not Grid contentGrid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (contentGrid.Children[0] is Border iconShell)
|
||||
{
|
||||
var iconSize = Math.Clamp(basis * 0.58, 28, 40);
|
||||
iconShell.Width = iconSize;
|
||||
iconShell.Height = iconSize;
|
||||
if (iconShell.Child is TextBlock iconText)
|
||||
{
|
||||
iconText.FontSize = Math.Clamp(iconSize * 0.5, 14, 20);
|
||||
}
|
||||
}
|
||||
|
||||
_titleTextBlock.FontSize = Math.Clamp(basis * 0.28, 14, 20);
|
||||
_statusTextBlock.FontSize = Math.Clamp(basis * 0.18, 10, 13);
|
||||
}
|
||||
|
||||
private string T(string key, string fallback)
|
||||
{
|
||||
return _localizer.GetString(key, fallback);
|
||||
}
|
||||
}
|
||||
@@ -1,524 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
namespace LanMountainDesktop.SamplePlugin;
|
||||
|
||||
internal enum SamplePluginHealthState
|
||||
{
|
||||
Healthy,
|
||||
Pending,
|
||||
Faulted
|
||||
}
|
||||
|
||||
internal sealed record SamplePluginStatusEntry(
|
||||
string Key,
|
||||
string Title,
|
||||
SamplePluginHealthState State,
|
||||
string Summary,
|
||||
string Detail,
|
||||
DateTimeOffset UpdatedAt);
|
||||
|
||||
internal sealed record SamplePluginCapabilityItem(
|
||||
string Title,
|
||||
string Detail);
|
||||
|
||||
internal sealed record SamplePluginRuntimeSnapshot(
|
||||
PluginManifest Manifest,
|
||||
string PluginDirectory,
|
||||
string DataDirectory,
|
||||
string HostApplicationName,
|
||||
string HostVersion,
|
||||
string SdkApiVersion,
|
||||
IReadOnlyList<SamplePluginStatusEntry> StatusEntries,
|
||||
bool HasPlacedComponent,
|
||||
int PlacedCount,
|
||||
int PreviewCount,
|
||||
IReadOnlyList<string> PlacementIds,
|
||||
string? LastComponentId,
|
||||
double LastCellSize,
|
||||
DateTimeOffset? ServiceClockTime);
|
||||
|
||||
internal sealed record SamplePluginClockTickMessage(DateTimeOffset CurrentTime);
|
||||
|
||||
internal sealed record SamplePluginStateChangedMessage(string Reason);
|
||||
|
||||
internal sealed record SamplePluginComponentInstance(
|
||||
string ComponentId,
|
||||
string? PlacementId,
|
||||
double CellSize)
|
||||
{
|
||||
public bool IsPlaced => !string.IsNullOrWhiteSpace(PlacementId);
|
||||
}
|
||||
|
||||
internal sealed class SamplePluginRuntimeStateService
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly IPluginMessageBus _messageBus;
|
||||
private readonly Dictionary<string, SamplePluginComponentInstance> _componentInstances =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly PluginManifest _manifest;
|
||||
private readonly string _pluginDirectory;
|
||||
private readonly string _dataDirectory;
|
||||
private readonly string _hostApplicationName;
|
||||
private readonly string _hostVersion;
|
||||
private readonly string _sdkApiVersion;
|
||||
private readonly PluginLocalizer _localizer;
|
||||
|
||||
private SamplePluginStatusEntry _frontend;
|
||||
private SamplePluginStatusEntry _component;
|
||||
private SamplePluginStatusEntry _backend;
|
||||
private SamplePluginStatusEntry _service;
|
||||
private string? _lastComponentId;
|
||||
private double _lastCellSize;
|
||||
private DateTimeOffset? _serviceClockTime;
|
||||
|
||||
public SamplePluginRuntimeStateService(
|
||||
PluginManifest manifest,
|
||||
string pluginDirectory,
|
||||
string dataDirectory,
|
||||
string hostApplicationName,
|
||||
string hostVersion,
|
||||
string sdkApiVersion,
|
||||
IPluginMessageBus messageBus,
|
||||
PluginLocalizer localizer)
|
||||
{
|
||||
_manifest = manifest;
|
||||
_pluginDirectory = pluginDirectory;
|
||||
_dataDirectory = dataDirectory;
|
||||
_hostApplicationName = hostApplicationName;
|
||||
_hostVersion = hostVersion;
|
||||
_sdkApiVersion = sdkApiVersion;
|
||||
_messageBus = messageBus;
|
||||
_localizer = localizer;
|
||||
|
||||
_frontend = CreateEntry(
|
||||
"frontend",
|
||||
T("status.frontend.title", "前端状态"),
|
||||
SamplePluginHealthState.Pending,
|
||||
T("status.summary.pending", "等待中"),
|
||||
T("status.frontend.detail.pending", "等待插件界面接入。"));
|
||||
|
||||
_component = CreateEntry(
|
||||
"component",
|
||||
T("status.component.title", "组件状态"),
|
||||
SamplePluginHealthState.Pending,
|
||||
T("status.summary.pending", "等待中"),
|
||||
T("status.component.detail.pending", "当前还没有创建组件实例。"));
|
||||
|
||||
_backend = CreateEntry(
|
||||
"backend",
|
||||
T("status.backend.title", "后端状态"),
|
||||
SamplePluginHealthState.Pending,
|
||||
T("status.summary.pending", "等待中"),
|
||||
T("status.backend.detail.pending", "插件初始化进行中。"));
|
||||
|
||||
_service = CreateEntry(
|
||||
"service",
|
||||
T("status.service.title", "时钟服务"),
|
||||
SamplePluginHealthState.Pending,
|
||||
T("status.summary.pending", "等待中"),
|
||||
T("status.service.detail.pending", "时钟服务尚未挂接。"));
|
||||
}
|
||||
|
||||
public void AttachClockService(SamplePluginClockService clockService)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(clockService);
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_serviceClockTime = clockService.CurrentTime;
|
||||
_service = CreateEntry(
|
||||
"service",
|
||||
T("status.service.title", "时钟服务"),
|
||||
SamplePluginHealthState.Pending,
|
||||
T("status.summary.attached", "已挂接"),
|
||||
T("status.service.detail.attached", "时钟服务已挂接,正在等待第一次心跳。"));
|
||||
}
|
||||
|
||||
PublishStateChanged("Clock service attached");
|
||||
}
|
||||
|
||||
public void MarkFrontendReady(string detail)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_frontend = CreateEntry(
|
||||
"frontend",
|
||||
T("status.frontend.title", "前端状态"),
|
||||
SamplePluginHealthState.Healthy,
|
||||
T("status.summary.healthy", "正常"),
|
||||
detail);
|
||||
}
|
||||
|
||||
PublishStateChanged("Frontend updated");
|
||||
}
|
||||
|
||||
public void MarkBackendReady(string detail)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_backend = CreateEntry(
|
||||
"backend",
|
||||
T("status.backend.title", "后端状态"),
|
||||
SamplePluginHealthState.Healthy,
|
||||
T("status.summary.healthy", "正常"),
|
||||
detail);
|
||||
}
|
||||
|
||||
PublishStateChanged("Backend updated");
|
||||
}
|
||||
|
||||
public void MarkBackendFaulted(string detail)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_backend = CreateEntry(
|
||||
"backend",
|
||||
T("status.backend.title", "后端状态"),
|
||||
SamplePluginHealthState.Faulted,
|
||||
T("status.summary.faulted", "异常"),
|
||||
detail);
|
||||
}
|
||||
|
||||
PublishStateChanged("Backend faulted");
|
||||
}
|
||||
|
||||
public void MarkClockServiceTick(DateTimeOffset currentTime)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_serviceClockTime = currentTime;
|
||||
_service = CreateEntry(
|
||||
"service",
|
||||
T("status.service.title", "时钟服务"),
|
||||
SamplePluginHealthState.Healthy,
|
||||
T("status.summary.healthy", "正常"),
|
||||
Tf(
|
||||
"status.service.detail.running",
|
||||
"时钟服务运行中,当前服务时间:{0}",
|
||||
currentTime.LocalDateTime.ToString("HH:mm:ss")));
|
||||
}
|
||||
|
||||
PublishStateChanged("Clock service tick");
|
||||
}
|
||||
|
||||
public void MarkClockServiceFaulted(string detail)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_service = CreateEntry(
|
||||
"service",
|
||||
T("status.service.title", "时钟服务"),
|
||||
SamplePluginHealthState.Faulted,
|
||||
T("status.summary.faulted", "异常"),
|
||||
detail);
|
||||
}
|
||||
|
||||
PublishStateChanged("Clock service faulted");
|
||||
}
|
||||
|
||||
public string RegisterComponentInstance(string componentId, string? placementId, double cellSize)
|
||||
{
|
||||
var instanceId = Guid.NewGuid().ToString("N");
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_componentInstances[instanceId] = new SamplePluginComponentInstance(componentId, placementId, cellSize);
|
||||
_lastComponentId = componentId;
|
||||
_lastCellSize = cellSize;
|
||||
UpdateComponentStatusNoLock();
|
||||
}
|
||||
|
||||
PublishStateChanged("Component attached");
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
public void UnregisterComponentInstance(string instanceId)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(instanceId);
|
||||
|
||||
var removed = false;
|
||||
lock (_gate)
|
||||
{
|
||||
removed = _componentInstances.Remove(instanceId);
|
||||
if (removed)
|
||||
{
|
||||
UpdateComponentStatusNoLock();
|
||||
}
|
||||
}
|
||||
|
||||
if (removed)
|
||||
{
|
||||
PublishStateChanged("Component detached");
|
||||
}
|
||||
}
|
||||
|
||||
public SamplePluginRuntimeSnapshot GetSnapshot()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
var placementIds = _componentInstances.Values
|
||||
.Where(instance => instance.IsPlaced)
|
||||
.Select(instance => instance.PlacementId!)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(id => id, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
var previewCount = _componentInstances.Values.Count(instance => !instance.IsPlaced);
|
||||
|
||||
return new SamplePluginRuntimeSnapshot(
|
||||
_manifest,
|
||||
_pluginDirectory,
|
||||
_dataDirectory,
|
||||
_hostApplicationName,
|
||||
_hostVersion,
|
||||
_sdkApiVersion,
|
||||
[_frontend, _component, _backend, _service],
|
||||
placementIds.Length > 0,
|
||||
placementIds.Length,
|
||||
previewCount,
|
||||
placementIds,
|
||||
_lastComponentId,
|
||||
_lastCellSize,
|
||||
_serviceClockTime);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<SamplePluginCapabilityItem> GetCapabilities(
|
||||
IPluginContext context,
|
||||
bool hasStateService,
|
||||
bool hasClockService,
|
||||
bool hasMessageBus)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
var propertyNames = context.Properties.Count == 0
|
||||
? T("common.none", "(无)")
|
||||
: string.Join(", ", context.Properties.Keys.OrderBy(key => key, StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
return
|
||||
[
|
||||
new SamplePluginCapabilityItem(
|
||||
T("capability.manifest.title", "IPluginContext.Manifest"),
|
||||
Tf(
|
||||
"capability.manifest.detail",
|
||||
"可读取。当前插件 id:{0};版本:{1}。",
|
||||
context.Manifest.Id,
|
||||
context.Manifest.Version ?? T("common.dev", "开发版"))),
|
||||
new SamplePluginCapabilityItem(
|
||||
T("capability.directories.title", "IPluginContext.PluginDirectory / DataDirectory"),
|
||||
Tf(
|
||||
"capability.directories.detail",
|
||||
"可读取。插件目录:{0};数据目录:{1}。",
|
||||
context.PluginDirectory,
|
||||
context.DataDirectory)),
|
||||
new SamplePluginCapabilityItem(
|
||||
T("capability.properties.title", "IPluginContext.Properties"),
|
||||
Tf(
|
||||
"capability.properties.detail",
|
||||
"可读取。宿主当前暴露的属性:{0}。",
|
||||
propertyNames)),
|
||||
new SamplePluginCapabilityItem(
|
||||
T("capability.get_service.title", "IPluginContext.GetService<T>()"),
|
||||
Tf(
|
||||
"capability.get_service.detail",
|
||||
"可调用。状态服务已解析:{0};时钟服务已解析:{1};消息总线已解析:{2}。",
|
||||
FormatBoolean(hasStateService),
|
||||
FormatBoolean(hasClockService),
|
||||
FormatBoolean(hasMessageBus))),
|
||||
new SamplePluginCapabilityItem(
|
||||
T("capability.register_service.title", "IPluginContext.RegisterService<TService>()"),
|
||||
T(
|
||||
"capability.register_service.detail",
|
||||
"可在插件初始化阶段调用。这个示例插件会把 SamplePluginRuntimeStateService 和 SamplePluginClockService 注册进插件服务容器。")),
|
||||
new SamplePluginCapabilityItem(
|
||||
T("capability.message_bus.title", "插件通信总线"),
|
||||
T(
|
||||
"capability.message_bus.detail",
|
||||
"这个示例插件通过 IPluginMessageBus 向插件 UI 推送时钟心跳和状态变化通知。")),
|
||||
new SamplePluginCapabilityItem(
|
||||
T("capability.widget_context.title", "PluginDesktopComponentContext"),
|
||||
T(
|
||||
"capability.widget_context.detail",
|
||||
"组件可以读取 ComponentId、PlacementId、CellSize,并能在同一个插件服务容器上调用 GetService<T>()。"))
|
||||
];
|
||||
}
|
||||
|
||||
private void UpdateComponentStatusNoLock()
|
||||
{
|
||||
var placementIds = _componentInstances.Values
|
||||
.Where(instance => instance.IsPlaced)
|
||||
.Select(instance => instance.PlacementId!)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(id => id, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
var previewCount = _componentInstances.Values.Count(instance => !instance.IsPlaced);
|
||||
|
||||
if (placementIds.Length > 0)
|
||||
{
|
||||
_component = CreateEntry(
|
||||
"component",
|
||||
T("status.component.title", "组件状态"),
|
||||
SamplePluginHealthState.Healthy,
|
||||
T("status.summary.placed", "已放置"),
|
||||
Tf(
|
||||
"status.component.detail.placed",
|
||||
"已放置数量:{0};预览数量:{1};放置位置:{2}",
|
||||
placementIds.Length,
|
||||
previewCount,
|
||||
string.Join(", ", placementIds)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (previewCount > 0)
|
||||
{
|
||||
_component = CreateEntry(
|
||||
"component",
|
||||
T("status.component.title", "组件状态"),
|
||||
SamplePluginHealthState.Healthy,
|
||||
T("status.summary.preview", "预览中"),
|
||||
Tf(
|
||||
"status.component.detail.preview",
|
||||
"当前预览实例数量:{0};尚未有已放置的桌面实例。",
|
||||
previewCount));
|
||||
return;
|
||||
}
|
||||
|
||||
_component = CreateEntry(
|
||||
"component",
|
||||
T("status.component.title", "组件状态"),
|
||||
SamplePluginHealthState.Pending,
|
||||
T("status.summary.pending", "等待中"),
|
||||
T("status.component.detail.none", "当前没有活动中的组件实例。"));
|
||||
}
|
||||
|
||||
private void PublishStateChanged(string reason)
|
||||
{
|
||||
_messageBus.Publish(new SamplePluginStateChangedMessage(reason));
|
||||
}
|
||||
|
||||
private static SamplePluginStatusEntry CreateEntry(
|
||||
string key,
|
||||
string title,
|
||||
SamplePluginHealthState state,
|
||||
string summary,
|
||||
string detail)
|
||||
{
|
||||
return new SamplePluginStatusEntry(
|
||||
key,
|
||||
title,
|
||||
state,
|
||||
summary,
|
||||
detail,
|
||||
DateTimeOffset.Now);
|
||||
}
|
||||
|
||||
private string T(string key, string fallback)
|
||||
{
|
||||
return _localizer.GetString(key, fallback);
|
||||
}
|
||||
|
||||
private string Tf(string key, string fallback, params object[] args)
|
||||
{
|
||||
return _localizer.Format(key, fallback, args);
|
||||
}
|
||||
|
||||
private string FormatBoolean(bool value)
|
||||
{
|
||||
return value
|
||||
? T("common.true", "是")
|
||||
: T("common.false", "否");
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class SamplePluginClockService : IDisposable
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly string _clockStateFilePath;
|
||||
private readonly SamplePluginRuntimeStateService _stateService;
|
||||
private readonly IPluginMessageBus _messageBus;
|
||||
private readonly PluginLocalizer _localizer;
|
||||
private readonly Timer _timer;
|
||||
private DateTimeOffset _currentTime = DateTimeOffset.Now;
|
||||
private int _disposed;
|
||||
|
||||
public SamplePluginClockService(
|
||||
string dataDirectory,
|
||||
SamplePluginRuntimeStateService stateService,
|
||||
IPluginMessageBus messageBus,
|
||||
PluginLocalizer localizer)
|
||||
{
|
||||
_clockStateFilePath = Path.Combine(dataDirectory, "clock-service.txt");
|
||||
_stateService = stateService;
|
||||
_messageBus = messageBus;
|
||||
_localizer = localizer;
|
||||
_timer = new Timer(OnTimerTick);
|
||||
}
|
||||
|
||||
public DateTimeOffset CurrentTime
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _currentTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
PublishTick();
|
||||
_timer.Change(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_timer.Dispose();
|
||||
}
|
||||
|
||||
private void OnTimerTick(object? state)
|
||||
{
|
||||
PublishTick();
|
||||
}
|
||||
|
||||
private void PublishTick()
|
||||
{
|
||||
if (Volatile.Read(ref _disposed) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.Now;
|
||||
lock (_gate)
|
||||
{
|
||||
_currentTime = now;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(
|
||||
_clockStateFilePath,
|
||||
now.ToString("O", CultureInfo.InvariantCulture));
|
||||
_stateService.MarkClockServiceTick(now);
|
||||
_messageBus.Publish(new SamplePluginClockTickMessage(now));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_stateService.MarkClockServiceFaulted(_localizer.Format(
|
||||
"status.service.detail.write_failed",
|
||||
"时钟状态写入失败:{0}",
|
||||
ex.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,374 +0,0 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
namespace LanMountainDesktop.SamplePlugin;
|
||||
|
||||
internal sealed class SamplePluginSettingsView : UserControl
|
||||
{
|
||||
private readonly IPluginContext _context;
|
||||
private readonly PluginLocalizer _localizer;
|
||||
private readonly SamplePluginRuntimeStateService _stateService;
|
||||
private readonly SamplePluginClockService _clockService;
|
||||
private readonly IPluginMessageBus _messageBus;
|
||||
private readonly StackPanel _pluginInfoPanel = new() { Spacing = 8 };
|
||||
private readonly StackPanel _capabilityPanel = new() { Spacing = 8 };
|
||||
private readonly StackPanel _statusPanel = new() { Spacing = 10 };
|
||||
private readonly List<IDisposable> _subscriptions = [];
|
||||
|
||||
public SamplePluginSettingsView(IPluginContext context)
|
||||
{
|
||||
_context = context;
|
||||
_localizer = PluginLocalizer.Create(context);
|
||||
_stateService = context.GetService<SamplePluginRuntimeStateService>()
|
||||
?? throw new InvalidOperationException("SamplePluginRuntimeStateService is not available.");
|
||||
_clockService = context.GetService<SamplePluginClockService>()
|
||||
?? throw new InvalidOperationException("SamplePluginClockService is not available.");
|
||||
_messageBus = context.GetService<IPluginMessageBus>()
|
||||
?? throw new InvalidOperationException("IPluginMessageBus is not available.");
|
||||
|
||||
_stateService.MarkFrontendReady(T(
|
||||
"status.frontend.detail.settings_connected",
|
||||
"设置页已接入插件服务与通信。"));
|
||||
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
|
||||
Content = new Border
|
||||
{
|
||||
Background = new LinearGradientBrush
|
||||
{
|
||||
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
|
||||
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
|
||||
GradientStops =
|
||||
[
|
||||
new GradientStop(Color.Parse("#1F0B1120"), 0),
|
||||
new GradientStop(Color.Parse("#260C4A6E"), 1)
|
||||
]
|
||||
},
|
||||
BorderBrush = new SolidColorBrush(Color.Parse("#6628B2FF")),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(18),
|
||||
Padding = new Thickness(18),
|
||||
Child = new StackPanel
|
||||
{
|
||||
Spacing = 14,
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = T("settings.header.title", "示例插件能力检查器"),
|
||||
FontSize = 22,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = Brushes.White
|
||||
},
|
||||
CreateSection(T("settings.section.info", "插件信息"), _pluginInfoPanel),
|
||||
CreateSection(T("settings.section.capabilities", "可访问能力"), _capabilityPanel),
|
||||
CreateSection(T("settings.section.status", "实时运行状态"), _statusPanel)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
RefreshView();
|
||||
}
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
SubscribeToPluginBus();
|
||||
RefreshView();
|
||||
}
|
||||
|
||||
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
foreach (var subscription in _subscriptions)
|
||||
{
|
||||
subscription.Dispose();
|
||||
}
|
||||
|
||||
_subscriptions.Clear();
|
||||
}
|
||||
|
||||
private void SubscribeToPluginBus()
|
||||
{
|
||||
if (_subscriptions.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_subscriptions.Add(_messageBus.Subscribe<SamplePluginClockTickMessage>(_ =>
|
||||
Dispatcher.UIThread.Post(RefreshView)));
|
||||
|
||||
_subscriptions.Add(_messageBus.Subscribe<SamplePluginStateChangedMessage>(_ =>
|
||||
Dispatcher.UIThread.Post(RefreshView)));
|
||||
}
|
||||
|
||||
private void RefreshView()
|
||||
{
|
||||
var snapshot = _stateService.GetSnapshot();
|
||||
RefreshPluginInfo(snapshot);
|
||||
RefreshCapabilities();
|
||||
RefreshStatuses(snapshot);
|
||||
}
|
||||
|
||||
private void RefreshPluginInfo(SamplePluginRuntimeSnapshot snapshot)
|
||||
{
|
||||
_pluginInfoPanel.Children.Clear();
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.plugin_name", "插件名称"),
|
||||
T("plugin.name", snapshot.Manifest.Name)));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.plugin_id", "插件 Id"), snapshot.Manifest.Id));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.version", "版本"), snapshot.Manifest.Version ?? T("common.dev", "开发版")));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.author", "作者"), snapshot.Manifest.Author ?? T("common.none", "(无)")));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.description", "描述"),
|
||||
T("plugin.description", snapshot.Manifest.Description ?? T("common.none", "(无)"))));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.plugin_directory", "插件目录"), snapshot.PluginDirectory));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.data_directory", "数据目录"), snapshot.DataDirectory));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.host_application", "宿主应用"), snapshot.HostApplicationName));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.host_version", "宿主版本"), snapshot.HostVersion));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.sdk_api_version", "SDK API 版本"), snapshot.SdkApiVersion));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.state_service_resolved", "状态服务已解析"),
|
||||
FormatBoolean(_context.GetService<SamplePluginRuntimeStateService>() is not null)));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.clock_service_resolved", "时钟服务已解析"),
|
||||
FormatBoolean(_context.GetService<SamplePluginClockService>() is not null)));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.message_bus_resolved", "消息总线已解析"),
|
||||
FormatBoolean(_context.GetService<IPluginMessageBus>() is not null)));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.component_placed", "组件是否已放置"),
|
||||
snapshot.HasPlacedComponent ? T("common.yes", "是") : T("common.no", "否")));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.placed_count", "已放置数量"), snapshot.PlacedCount.ToString()));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(T("settings.info.preview_count", "预览数量"), snapshot.PreviewCount.ToString()));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.placement_ids", "放置位置 Id"),
|
||||
snapshot.PlacementIds.Count == 0 ? T("common.none", "(无)") : string.Join(", ", snapshot.PlacementIds)));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.last_component_id", "最近组件 Id"),
|
||||
snapshot.LastComponentId ?? T("common.none", "(无)")));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.last_cell_size", "最近单元尺寸"),
|
||||
snapshot.LastCellSize > 0 ? $"{snapshot.LastCellSize:F0}px" : T("common.unknown", "(未知)")));
|
||||
_pluginInfoPanel.Children.Add(CreateInfoLine(
|
||||
T("settings.info.clock_service_time", "时钟服务时间"),
|
||||
_clockService.CurrentTime.LocalDateTime.ToString("HH:mm:ss")));
|
||||
}
|
||||
|
||||
private void RefreshCapabilities()
|
||||
{
|
||||
var capabilities = _stateService.GetCapabilities(
|
||||
_context,
|
||||
_context.GetService<SamplePluginRuntimeStateService>() is not null,
|
||||
_context.GetService<SamplePluginClockService>() is not null,
|
||||
_context.GetService<IPluginMessageBus>() is not null);
|
||||
|
||||
_capabilityPanel.Children.Clear();
|
||||
foreach (var capability in capabilities)
|
||||
{
|
||||
_capabilityPanel.Children.Add(CreateCapabilityCard(capability));
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshStatuses(SamplePluginRuntimeSnapshot snapshot)
|
||||
{
|
||||
_statusPanel.Children.Clear();
|
||||
|
||||
foreach (var entry in snapshot.StatusEntries)
|
||||
{
|
||||
var palette = GetPalette(entry.State);
|
||||
_statusPanel.Children.Add(new Border
|
||||
{
|
||||
Background = new SolidColorBrush(palette.Background),
|
||||
BorderBrush = new SolidColorBrush(palette.Border),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Padding = new Thickness(12, 10),
|
||||
Child = new StackPanel
|
||||
{
|
||||
Spacing = 4,
|
||||
Children =
|
||||
{
|
||||
CreateStatusHeader(entry, palette),
|
||||
new TextBlock
|
||||
{
|
||||
Text = entry.Detail,
|
||||
Foreground = new SolidColorBrush(Color.Parse("#FFE0F2FE")),
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = Tf("settings.status.updated_at", "更新时间:{0}", entry.UpdatedAt.LocalDateTime.ToString("HH:mm:ss")),
|
||||
Foreground = new SolidColorBrush(Color.Parse("#FF93C5FD"))
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private Border CreateSection(string title, Control content)
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.Parse("#14000000")),
|
||||
BorderBrush = new SolidColorBrush(Color.Parse("#3328B2FF")),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(14),
|
||||
Padding = new Thickness(14),
|
||||
Child = new StackPanel
|
||||
{
|
||||
Spacing = 12,
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = title,
|
||||
FontSize = 16,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = Brushes.White
|
||||
},
|
||||
content
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Control CreateInfoLine(string label, string value)
|
||||
{
|
||||
var grid = new Grid
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("180,*"),
|
||||
ColumnSpacing = 10
|
||||
};
|
||||
|
||||
var labelText = new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
Foreground = new SolidColorBrush(Color.Parse("#FFBAE6FD")),
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
var valueText = new TextBlock
|
||||
{
|
||||
Text = value,
|
||||
Foreground = Brushes.White,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
grid.Children.Add(labelText);
|
||||
grid.Children.Add(valueText);
|
||||
Grid.SetColumn(valueText, 1);
|
||||
return grid;
|
||||
}
|
||||
|
||||
private Control CreateCapabilityCard(SamplePluginCapabilityItem item)
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.Parse("#0F082F49")),
|
||||
BorderBrush = new SolidColorBrush(Color.Parse("#3338BDF8")),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Padding = new Thickness(12, 10),
|
||||
Child = new StackPanel
|
||||
{
|
||||
Spacing = 4,
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = item.Title,
|
||||
Foreground = Brushes.White,
|
||||
FontWeight = FontWeight.SemiBold
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = item.Detail,
|
||||
Foreground = new SolidColorBrush(Color.Parse("#FFE0F2FE")),
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Control CreateStatusHeader(
|
||||
SamplePluginStatusEntry entry,
|
||||
(Color Background, Color Border, Color Dot) palette)
|
||||
{
|
||||
var grid = new Grid
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto"),
|
||||
ColumnSpacing = 8
|
||||
};
|
||||
|
||||
var dot = new Border
|
||||
{
|
||||
Width = 10,
|
||||
Height = 10,
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Background = new SolidColorBrush(palette.Dot),
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
var title = new TextBlock
|
||||
{
|
||||
Text = entry.Title,
|
||||
FontSize = 15,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = Brushes.White
|
||||
};
|
||||
var summary = new TextBlock
|
||||
{
|
||||
Text = entry.Summary,
|
||||
Foreground = new SolidColorBrush(Color.Parse("#FFD7F2FF")),
|
||||
HorizontalAlignment = HorizontalAlignment.Right
|
||||
};
|
||||
|
||||
grid.Children.Add(dot);
|
||||
grid.Children.Add(title);
|
||||
grid.Children.Add(summary);
|
||||
Grid.SetColumn(title, 1);
|
||||
Grid.SetColumn(summary, 2);
|
||||
return grid;
|
||||
}
|
||||
|
||||
private static (Color Background, Color Border, Color Dot) GetPalette(SamplePluginHealthState state)
|
||||
{
|
||||
return state switch
|
||||
{
|
||||
SamplePluginHealthState.Healthy => (
|
||||
Color.Parse("#1F115E59"),
|
||||
Color.Parse("#665EEAD4"),
|
||||
Color.Parse("#5EEAD4")),
|
||||
SamplePluginHealthState.Faulted => (
|
||||
Color.Parse("#291B1B"),
|
||||
Color.Parse("#66F87171"),
|
||||
Color.Parse("#F87171")),
|
||||
_ => (
|
||||
Color.Parse("#2B3A2A0D"),
|
||||
Color.Parse("#66FBBF24"),
|
||||
Color.Parse("#FBBF24"))
|
||||
};
|
||||
}
|
||||
|
||||
private string T(string key, string fallback)
|
||||
{
|
||||
return _localizer.GetString(key, fallback);
|
||||
}
|
||||
|
||||
private string Tf(string key, string fallback, params object[] args)
|
||||
{
|
||||
return _localizer.Format(key, fallback, args);
|
||||
}
|
||||
|
||||
private string FormatBoolean(bool value)
|
||||
{
|
||||
return value
|
||||
? T("common.true", "是")
|
||||
: T("common.false", "否");
|
||||
}
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
namespace LanMountainDesktop.SamplePlugin;
|
||||
|
||||
internal sealed class SamplePluginStatusClockWidget : Border
|
||||
{
|
||||
private readonly PluginDesktopComponentContext _context;
|
||||
private readonly PluginLocalizer _localizer;
|
||||
private readonly SamplePluginRuntimeStateService _stateService;
|
||||
private readonly SamplePluginClockService _clockService;
|
||||
private readonly IPluginMessageBus _messageBus;
|
||||
private readonly TextBlock _timeTextBlock;
|
||||
private readonly TextBlock _subtitleTextBlock;
|
||||
private readonly StackPanel _statusPanel;
|
||||
private readonly Border _statusHost;
|
||||
private readonly List<IDisposable> _subscriptions = [];
|
||||
private string? _instanceId;
|
||||
|
||||
public SamplePluginStatusClockWidget(PluginDesktopComponentContext context)
|
||||
{
|
||||
_context = context;
|
||||
_localizer = PluginLocalizer.Create(context);
|
||||
_stateService = context.GetService<SamplePluginRuntimeStateService>()
|
||||
?? throw new InvalidOperationException("SamplePluginRuntimeStateService is not available.");
|
||||
_clockService = context.GetService<SamplePluginClockService>()
|
||||
?? throw new InvalidOperationException("SamplePluginClockService is not available.");
|
||||
_messageBus = context.GetService<IPluginMessageBus>()
|
||||
?? throw new InvalidOperationException("IPluginMessageBus is not available.");
|
||||
|
||||
_timeTextBlock = new TextBlock
|
||||
{
|
||||
Foreground = Brushes.White,
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Left
|
||||
};
|
||||
_subtitleTextBlock = new TextBlock
|
||||
{
|
||||
Foreground = new SolidColorBrush(Color.Parse("#FFBFE9FF")),
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
_statusPanel = new StackPanel
|
||||
{
|
||||
Spacing = 8
|
||||
};
|
||||
_statusHost = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.Parse("#1F082F49")),
|
||||
BorderBrush = new SolidColorBrush(Color.Parse("#5538BDF8")),
|
||||
BorderThickness = new Thickness(1),
|
||||
Child = _statusPanel
|
||||
};
|
||||
|
||||
Background = new LinearGradientBrush
|
||||
{
|
||||
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
|
||||
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
|
||||
GradientStops =
|
||||
[
|
||||
new GradientStop(Color.Parse("#FF07111F"), 0),
|
||||
new GradientStop(Color.Parse("#FF0C4A6E"), 0.55),
|
||||
new GradientStop(Color.Parse("#FF0EA5E9"), 1)
|
||||
]
|
||||
};
|
||||
BorderBrush = new SolidColorBrush(Color.Parse("#6648C7FF"));
|
||||
BorderThickness = new Thickness(1);
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch;
|
||||
VerticalAlignment = VerticalAlignment.Stretch;
|
||||
Child = new Grid
|
||||
{
|
||||
RowDefinitions = new RowDefinitions("Auto,*"),
|
||||
RowSpacing = 14,
|
||||
Children =
|
||||
{
|
||||
new StackPanel
|
||||
{
|
||||
Spacing = 4,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
Children =
|
||||
{
|
||||
_timeTextBlock,
|
||||
_subtitleTextBlock
|
||||
}
|
||||
},
|
||||
_statusHost
|
||||
}
|
||||
};
|
||||
|
||||
Grid.SetRow(((Grid)Child).Children[1], 1);
|
||||
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
|
||||
RefreshClock(_clockService.CurrentTime);
|
||||
UpdateSubtitle();
|
||||
RefreshStatusPanel();
|
||||
ApplyScale();
|
||||
}
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_instanceId))
|
||||
{
|
||||
_instanceId = _stateService.RegisterComponentInstance(
|
||||
_context.ComponentId,
|
||||
_context.PlacementId,
|
||||
_context.CellSize);
|
||||
}
|
||||
|
||||
_stateService.MarkFrontendReady(T(
|
||||
"status.frontend.detail.widget_connected",
|
||||
"组件界面已接入插件服务与通信。"));
|
||||
SubscribeToPluginBus();
|
||||
|
||||
RefreshClock(_clockService.CurrentTime);
|
||||
UpdateSubtitle();
|
||||
RefreshStatusPanel();
|
||||
}
|
||||
|
||||
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
foreach (var subscription in _subscriptions)
|
||||
{
|
||||
subscription.Dispose();
|
||||
}
|
||||
|
||||
_subscriptions.Clear();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_instanceId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_stateService.UnregisterComponentInstance(_instanceId);
|
||||
_instanceId = null;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
ApplyScale();
|
||||
RefreshStatusPanel();
|
||||
}
|
||||
|
||||
private void SubscribeToPluginBus()
|
||||
{
|
||||
if (_subscriptions.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_subscriptions.Add(_messageBus.Subscribe<SamplePluginClockTickMessage>(message =>
|
||||
Dispatcher.UIThread.Post(() => RefreshClock(message.CurrentTime))));
|
||||
|
||||
_subscriptions.Add(_messageBus.Subscribe<SamplePluginStateChangedMessage>(_ =>
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
UpdateSubtitle();
|
||||
RefreshStatusPanel();
|
||||
})));
|
||||
}
|
||||
|
||||
private void RefreshClock(DateTimeOffset currentTime)
|
||||
{
|
||||
_timeTextBlock.Text = currentTime.LocalDateTime.ToString("HH:mm:ss");
|
||||
}
|
||||
|
||||
private void UpdateSubtitle()
|
||||
{
|
||||
var snapshot = _stateService.GetSnapshot();
|
||||
_subtitleTextBlock.Text = string.IsNullOrWhiteSpace(_context.PlacementId)
|
||||
? Tf("widget.subtitle.preview", "预览界面 | 已放置:{0}", snapshot.PlacedCount)
|
||||
: Tf("widget.subtitle.placement", "位置 {0} | 已放置:{1}", _context.PlacementId!, snapshot.PlacedCount);
|
||||
}
|
||||
|
||||
private void RefreshStatusPanel()
|
||||
{
|
||||
_statusPanel.Children.Clear();
|
||||
|
||||
var snapshot = _stateService.GetSnapshot();
|
||||
var basis = GetLayoutBasis();
|
||||
var titleSize = Math.Clamp(basis * 0.068, 11, 16);
|
||||
var detailSize = Math.Clamp(basis * 0.052, 9, 13);
|
||||
|
||||
foreach (var entry in snapshot.StatusEntries)
|
||||
{
|
||||
var palette = GetPalette(entry.State);
|
||||
_statusPanel.Children.Add(new Border
|
||||
{
|
||||
Background = new SolidColorBrush(palette.Background),
|
||||
BorderBrush = new SolidColorBrush(palette.Border),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Padding = new Thickness(10, 8),
|
||||
Child = new Grid
|
||||
{
|
||||
RowDefinitions = new RowDefinitions("Auto,Auto"),
|
||||
ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto"),
|
||||
ColumnSpacing = 8,
|
||||
Children =
|
||||
{
|
||||
new Border
|
||||
{
|
||||
Width = Math.Clamp(basis * 0.038, 8, 11),
|
||||
Height = Math.Clamp(basis * 0.038, 8, 11),
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Background = new SolidColorBrush(palette.Dot),
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = entry.Title,
|
||||
FontSize = titleSize,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = Brushes.White,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = entry.Summary,
|
||||
FontSize = detailSize,
|
||||
Foreground = new SolidColorBrush(Color.Parse("#FFD7F2FF")),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
TextAlignment = TextAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = entry.Detail,
|
||||
FontSize = detailSize,
|
||||
Foreground = new SolidColorBrush(Color.Parse("#FFD7F2FF")),
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var row = (Grid)((Border)_statusPanel.Children[^1]).Child!;
|
||||
Grid.SetColumn(row.Children[1], 1);
|
||||
Grid.SetColumn(row.Children[2], 2);
|
||||
Grid.SetColumnSpan(row.Children[3], 3);
|
||||
Grid.SetRow(row.Children[3], 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyScale()
|
||||
{
|
||||
var basis = GetLayoutBasis();
|
||||
Padding = new Thickness(Math.Clamp(basis * 0.09, 16, 26));
|
||||
CornerRadius = new CornerRadius(Math.Clamp(basis * 0.14, 20, 34));
|
||||
_timeTextBlock.FontSize = Math.Clamp(basis * 0.22, 30, 58);
|
||||
_subtitleTextBlock.FontSize = Math.Clamp(basis * 0.062, 11, 17);
|
||||
_statusHost.Padding = new Thickness(Math.Clamp(basis * 0.045, 10, 18));
|
||||
_statusHost.CornerRadius = new CornerRadius(Math.Clamp(basis * 0.09, 14, 22));
|
||||
_statusPanel.Spacing = Math.Clamp(basis * 0.024, 6, 10);
|
||||
}
|
||||
|
||||
private double GetLayoutBasis()
|
||||
{
|
||||
var width = Bounds.Width > 1 ? Bounds.Width : _context.CellSize * 4;
|
||||
var height = Bounds.Height > 1 ? Bounds.Height : _context.CellSize * 4;
|
||||
return Math.Max(_context.CellSize * 4, Math.Min(width, height));
|
||||
}
|
||||
|
||||
private static (Color Background, Color Border, Color Dot) GetPalette(SamplePluginHealthState state)
|
||||
{
|
||||
return state switch
|
||||
{
|
||||
SamplePluginHealthState.Healthy => (
|
||||
Color.Parse("#1F0F766E"),
|
||||
Color.Parse("#4D5EEAD4"),
|
||||
Color.Parse("#5EEAD4")),
|
||||
SamplePluginHealthState.Faulted => (
|
||||
Color.Parse("#29B91C1C"),
|
||||
Color.Parse("#66F87171"),
|
||||
Color.Parse("#F87171")),
|
||||
_ => (
|
||||
Color.Parse("#1F7C2D12"),
|
||||
Color.Parse("#66FDBA74"),
|
||||
Color.Parse("#FDBA74"))
|
||||
};
|
||||
}
|
||||
|
||||
private string T(string key, string fallback)
|
||||
{
|
||||
return _localizer.GetString(key, fallback);
|
||||
}
|
||||
|
||||
private string Tf(string key, string fallback, params object[] args)
|
||||
{
|
||||
return _localizer.Format(key, fallback, args);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"id": "LanMountainDesktop.SamplePlugin",
|
||||
"name": "LanMountain Sample Plugin",
|
||||
"description": "Example plugin used to validate PluginSdk loading and isolation.",
|
||||
"author": "LanMountainDesktop",
|
||||
"version": "1.0.0",
|
||||
"apiVersion": "1.0.0",
|
||||
"entranceAssembly": "LanMountainDesktop.SamplePlugin.dll"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
# 示例插件目录
|
||||
|
||||
## 中文
|
||||
|
||||
本目录用于存放阑山桌面的示例插件和参考实现。
|
||||
|
||||
当前标准示例为 `LanMountainDesktop.SamplePlugin`。
|
||||
|
||||
## English
|
||||
|
||||
This directory stores sample plugins and reference implementations. The current standard sample is `LanMountainDesktop.SamplePlugin`.
|
||||
@@ -1,9 +0,0 @@
|
||||
# 插件标准说明
|
||||
|
||||
## 中文
|
||||
|
||||
本目录存放插件开发需要遵循的基础约定,包括 `.laapp`、`plugin.json`、`Localization/` 以及仓库根目录 README 和安装包等要求。
|
||||
|
||||
## English
|
||||
|
||||
This directory stores the baseline conventions for plugin development, including `.laapp`, `plugin.json`, `Localization/`, and repository-root deliverables.
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"id": "LanMountainDesktop.YourPlugin",
|
||||
"name": "Your Plugin",
|
||||
"description": "Describe what your plugin adds to LanMountainDesktop.",
|
||||
"author": "Your Name",
|
||||
"version": "1.0.0",
|
||||
"apiVersion": "1.0.0",
|
||||
"entranceAssembly": "LanMountainDesktop.YourPlugin.dll"
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>1.0.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,136 +0,0 @@
|
||||
using System.IO.Compression;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
return await RunAsync(args);
|
||||
|
||||
static async Task<int> RunAsync(string[] args)
|
||||
{
|
||||
if (args.Length == 0 || args.Any(arg => string.Equals(arg, "--help", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
PrintUsage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
string? inputDirectory = null;
|
||||
string? outputPath = null;
|
||||
var overwrite = false;
|
||||
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
switch (args[i])
|
||||
{
|
||||
case "--input":
|
||||
inputDirectory = ReadValue(args, ref i, "--input");
|
||||
break;
|
||||
case "--output":
|
||||
outputPath = ReadValue(args, ref i, "--output");
|
||||
break;
|
||||
case "--overwrite":
|
||||
overwrite = true;
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown argument '{args[i]}'.");
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(inputDirectory))
|
||||
{
|
||||
throw new InvalidOperationException("Missing required argument '--input'.");
|
||||
}
|
||||
|
||||
var fullInputDirectory = Path.GetFullPath(inputDirectory);
|
||||
if (!Directory.Exists(fullInputDirectory))
|
||||
{
|
||||
throw new DirectoryNotFoundException($"Plugin build directory '{fullInputDirectory}' was not found.");
|
||||
}
|
||||
|
||||
var manifestPath = Path.Combine(fullInputDirectory, PluginSdkInfo.ManifestFileName);
|
||||
if (!File.Exists(manifestPath))
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
$"Plugin build directory '{fullInputDirectory}' does not contain '{PluginSdkInfo.ManifestFileName}'.",
|
||||
manifestPath);
|
||||
}
|
||||
|
||||
var manifest = PluginManifest.Load(manifestPath);
|
||||
var entranceAssemblyPath = manifest.ResolveEntranceAssemblyPath(manifestPath);
|
||||
if (!File.Exists(entranceAssemblyPath))
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
$"The entrance assembly declared by '{PluginSdkInfo.ManifestFileName}' was not found.",
|
||||
entranceAssemblyPath);
|
||||
}
|
||||
|
||||
outputPath ??= Path.Combine(
|
||||
Path.GetDirectoryName(fullInputDirectory) ?? fullInputDirectory,
|
||||
BuildPackageFileName(manifest.Id));
|
||||
|
||||
var fullOutputPath = Path.GetFullPath(outputPath);
|
||||
var inputDirectoryWithSeparator = EnsureTrailingSeparator(fullInputDirectory);
|
||||
if (fullOutputPath.StartsWith(inputDirectoryWithSeparator, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException("The output .laapp path cannot be placed inside the source directory.");
|
||||
}
|
||||
|
||||
var destinationDirectory = Path.GetDirectoryName(fullOutputPath);
|
||||
if (string.IsNullOrWhiteSpace(destinationDirectory))
|
||||
{
|
||||
throw new InvalidOperationException("Failed to determine the output directory for the .laapp package.");
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(destinationDirectory);
|
||||
if (File.Exists(fullOutputPath))
|
||||
{
|
||||
if (!overwrite)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The output package '{fullOutputPath}' already exists. Pass '--overwrite' to replace it.");
|
||||
}
|
||||
|
||||
File.Delete(fullOutputPath);
|
||||
}
|
||||
|
||||
await Task.Run(() => ZipFile.CreateFromDirectory(
|
||||
fullInputDirectory,
|
||||
fullOutputPath,
|
||||
CompressionLevel.Optimal,
|
||||
includeBaseDirectory: false));
|
||||
|
||||
Console.WriteLine($"Packaged '{manifest.Name}' to '{fullOutputPath}'.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static string ReadValue(IReadOnlyList<string> args, ref int index, string optionName)
|
||||
{
|
||||
var nextIndex = index + 1;
|
||||
if (nextIndex >= args.Count)
|
||||
{
|
||||
throw new InvalidOperationException($"Missing value for '{optionName}'.");
|
||||
}
|
||||
|
||||
index = nextIndex;
|
||||
return args[nextIndex];
|
||||
}
|
||||
|
||||
static string BuildPackageFileName(string pluginId)
|
||||
{
|
||||
var invalidChars = Path.GetInvalidFileNameChars();
|
||||
var safeName = new string(pluginId.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
|
||||
return safeName + PluginSdkInfo.PackageFileExtension;
|
||||
}
|
||||
|
||||
static string EnsureTrailingSeparator(string path)
|
||||
{
|
||||
return path.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)
|
||||
? path
|
||||
: path + Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
static void PrintUsage()
|
||||
{
|
||||
Console.WriteLine("LanMountainDesktop.PluginPackager");
|
||||
Console.WriteLine("Usage:");
|
||||
Console.WriteLine(" --input <plugin build directory> Required");
|
||||
Console.WriteLine(" --output <path to .laapp> Optional");
|
||||
Console.WriteLine(" --overwrite Optional");
|
||||
}
|
||||
10
LanMountainDesktop.PluginSdk/IPluginAppearanceContext.cs
Normal file
10
LanMountainDesktop.PluginSdk/IPluginAppearanceContext.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public interface IPluginAppearanceContext
|
||||
{
|
||||
PluginAppearanceSnapshot Snapshot { get; }
|
||||
|
||||
double ResolveScaledCornerRadius(double baseRadius, double? minimum = null, double? maximum = null);
|
||||
|
||||
double ResolveCornerRadius(PluginCornerRadiusPreset preset, double? minimum = null, double? maximum = null);
|
||||
}
|
||||
@@ -12,6 +12,8 @@ public interface IPluginRuntimeContext
|
||||
|
||||
IReadOnlyDictionary<string, object?> Properties { get; }
|
||||
|
||||
IPluginAppearanceContext Appearance { get; }
|
||||
|
||||
T? GetService<T>();
|
||||
|
||||
bool TryGetProperty<T>(string key, out T? value);
|
||||
|
||||
49
LanMountainDesktop.PluginSdk/PluginAppearanceContext.cs
Normal file
49
LanMountainDesktop.PluginSdk/PluginAppearanceContext.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public sealed class PluginAppearanceContext : IPluginAppearanceContext
|
||||
{
|
||||
public PluginAppearanceContext(PluginAppearanceSnapshot snapshot)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(snapshot);
|
||||
ArgumentNullException.ThrowIfNull(snapshot.CornerRadiusTokens);
|
||||
|
||||
Snapshot = snapshot with
|
||||
{
|
||||
GlobalCornerRadiusScale = Math.Max(0d, snapshot.GlobalCornerRadiusScale),
|
||||
ThemeVariant = string.IsNullOrWhiteSpace(snapshot.ThemeVariant)
|
||||
? "Unknown"
|
||||
: snapshot.ThemeVariant.Trim()
|
||||
};
|
||||
}
|
||||
|
||||
public PluginAppearanceSnapshot Snapshot { get; }
|
||||
|
||||
public double ResolveScaledCornerRadius(double baseRadius, double? minimum = null, double? maximum = null)
|
||||
{
|
||||
var scale = Snapshot.GlobalCornerRadiusScale;
|
||||
var scaled = Math.Max(0d, baseRadius) * scale;
|
||||
var scaledMin = minimum.HasValue ? minimum.Value * scale : scaled;
|
||||
var scaledMax = maximum.HasValue ? maximum.Value * scale : scaled;
|
||||
return minimum.HasValue || maximum.HasValue
|
||||
? Math.Clamp(scaled, scaledMin, scaledMax)
|
||||
: scaled;
|
||||
}
|
||||
|
||||
public double ResolveCornerRadius(PluginCornerRadiusPreset preset, double? minimum = null, double? maximum = null)
|
||||
{
|
||||
var resolved = Math.Max(0d, Snapshot.CornerRadiusTokens.Get(preset));
|
||||
if (!minimum.HasValue && !maximum.HasValue)
|
||||
{
|
||||
return resolved;
|
||||
}
|
||||
|
||||
var clampedMin = minimum ?? resolved;
|
||||
var clampedMax = maximum ?? resolved;
|
||||
if (clampedMin > clampedMax)
|
||||
{
|
||||
(clampedMin, clampedMax) = (clampedMax, clampedMin);
|
||||
}
|
||||
|
||||
return Math.Clamp(resolved, clampedMin, clampedMax);
|
||||
}
|
||||
}
|
||||
6
LanMountainDesktop.PluginSdk/PluginAppearanceSnapshot.cs
Normal file
6
LanMountainDesktop.PluginSdk/PluginAppearanceSnapshot.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public sealed record PluginAppearanceSnapshot(
|
||||
double GlobalCornerRadiusScale,
|
||||
PluginCornerRadiusTokens CornerRadiusTokens,
|
||||
string ThemeVariant);
|
||||
13
LanMountainDesktop.PluginSdk/PluginCornerRadiusPreset.cs
Normal file
13
LanMountainDesktop.PluginSdk/PluginCornerRadiusPreset.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public enum PluginCornerRadiusPreset
|
||||
{
|
||||
Default = 0,
|
||||
Micro = 1,
|
||||
Xs = 2,
|
||||
Sm = 3,
|
||||
Md = 4,
|
||||
Lg = 5,
|
||||
Xl = 6,
|
||||
Island = 7
|
||||
}
|
||||
49
LanMountainDesktop.PluginSdk/PluginCornerRadiusTokens.cs
Normal file
49
LanMountainDesktop.PluginSdk/PluginCornerRadiusTokens.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Avalonia;
|
||||
using LanMountainDesktop.Shared.Contracts;
|
||||
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public sealed record PluginCornerRadiusTokens(
|
||||
double Micro,
|
||||
double Xs,
|
||||
double Sm,
|
||||
double Md,
|
||||
double Lg,
|
||||
double Xl,
|
||||
double Island)
|
||||
{
|
||||
public double Get(PluginCornerRadiusPreset preset)
|
||||
{
|
||||
return preset switch
|
||||
{
|
||||
PluginCornerRadiusPreset.Default => Md,
|
||||
PluginCornerRadiusPreset.Micro => Micro,
|
||||
PluginCornerRadiusPreset.Xs => Xs,
|
||||
PluginCornerRadiusPreset.Sm => Sm,
|
||||
PluginCornerRadiusPreset.Md => Md,
|
||||
PluginCornerRadiusPreset.Lg => Lg,
|
||||
PluginCornerRadiusPreset.Xl => Xl,
|
||||
PluginCornerRadiusPreset.Island => Island,
|
||||
_ => Md
|
||||
};
|
||||
}
|
||||
|
||||
public CornerRadius ToCornerRadius(PluginCornerRadiusPreset preset)
|
||||
{
|
||||
return new CornerRadius(Get(preset));
|
||||
}
|
||||
|
||||
public static PluginCornerRadiusTokens FromShared(AppearanceCornerRadiusTokens tokens)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(tokens);
|
||||
|
||||
return new PluginCornerRadiusTokens(
|
||||
tokens.Micro.TopLeft,
|
||||
tokens.Xs.TopLeft,
|
||||
tokens.Sm.TopLeft,
|
||||
tokens.Md.TopLeft,
|
||||
tokens.Lg.TopLeft,
|
||||
tokens.Xl.TopLeft,
|
||||
tokens.Island.TopLeft);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
using LanMountainDesktop.Shared.Contracts;
|
||||
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public sealed class PluginDesktopComponentContext
|
||||
@@ -13,8 +11,7 @@ public sealed class PluginDesktopComponentContext
|
||||
string componentId,
|
||||
string? placementId,
|
||||
double cellSize,
|
||||
double globalCornerRadiusScale,
|
||||
AppearanceCornerRadiusTokens cornerRadiusTokens,
|
||||
IPluginAppearanceContext appearance,
|
||||
IPluginSettingsService? pluginSettings = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manifest);
|
||||
@@ -23,7 +20,7 @@ public sealed class PluginDesktopComponentContext
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(componentId);
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(properties);
|
||||
ArgumentNullException.ThrowIfNull(cornerRadiusTokens);
|
||||
ArgumentNullException.ThrowIfNull(appearance);
|
||||
|
||||
Manifest = manifest;
|
||||
PluginDirectory = pluginDirectory;
|
||||
@@ -33,8 +30,7 @@ public sealed class PluginDesktopComponentContext
|
||||
ComponentId = componentId.Trim();
|
||||
PlacementId = string.IsNullOrWhiteSpace(placementId) ? null : placementId.Trim();
|
||||
CellSize = Math.Max(1, cellSize);
|
||||
GlobalCornerRadiusScale = Math.Max(0d, globalCornerRadiusScale);
|
||||
CornerRadiusTokens = cornerRadiusTokens;
|
||||
Appearance = appearance;
|
||||
PluginSettings = pluginSettings;
|
||||
}
|
||||
|
||||
@@ -54,20 +50,22 @@ public sealed class PluginDesktopComponentContext
|
||||
|
||||
public double CellSize { get; }
|
||||
|
||||
public double GlobalCornerRadiusScale { get; }
|
||||
public IPluginAppearanceContext Appearance { get; }
|
||||
|
||||
public AppearanceCornerRadiusTokens CornerRadiusTokens { get; }
|
||||
public double GlobalCornerRadiusScale => Appearance.Snapshot.GlobalCornerRadiusScale;
|
||||
|
||||
public PluginCornerRadiusTokens CornerRadiusTokens => Appearance.Snapshot.CornerRadiusTokens;
|
||||
|
||||
public IPluginSettingsService? PluginSettings { get; }
|
||||
|
||||
public double ResolveScaledCornerRadius(double baseRadius, double? minimum = null, double? maximum = null)
|
||||
{
|
||||
var scaled = Math.Max(0d, baseRadius) * GlobalCornerRadiusScale;
|
||||
var scaledMin = minimum.HasValue ? minimum.Value * GlobalCornerRadiusScale : scaled;
|
||||
var scaledMax = maximum.HasValue ? maximum.Value * GlobalCornerRadiusScale : scaled;
|
||||
return minimum.HasValue || maximum.HasValue
|
||||
? Math.Clamp(scaled, scaledMin, scaledMax)
|
||||
: scaled;
|
||||
return Appearance.ResolveScaledCornerRadius(baseRadius, minimum, maximum);
|
||||
}
|
||||
|
||||
public double ResolveCornerRadius(PluginCornerRadiusPreset preset, double? minimum = null, double? maximum = null)
|
||||
{
|
||||
return Appearance.ResolveCornerRadius(preset, minimum, maximum);
|
||||
}
|
||||
|
||||
public T? GetService<T>()
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public sealed class PluginDesktopComponentOptions
|
||||
{
|
||||
public required string ComponentId { get; init; }
|
||||
|
||||
public required string DisplayName { get; init; }
|
||||
|
||||
public string IconKey { get; init; } = "PuzzlePiece";
|
||||
|
||||
public string Category { get; init; } = "Plugins";
|
||||
|
||||
public int MinWidthCells { get; init; } = 2;
|
||||
|
||||
public int MinHeightCells { get; init; } = 2;
|
||||
|
||||
public bool AllowDesktopPlacement { get; init; } = true;
|
||||
|
||||
public bool AllowStatusBarPlacement { get; init; }
|
||||
|
||||
public PluginDesktopComponentResizeMode ResizeMode { get; init; } = PluginDesktopComponentResizeMode.Proportional;
|
||||
|
||||
public string? DisplayNameLocalizationKey { get; init; }
|
||||
|
||||
public PluginCornerRadiusPreset CornerRadiusPreset { get; init; } = PluginCornerRadiusPreset.Default;
|
||||
|
||||
public Func<IPluginAppearanceContext, double, double>? CornerRadiusResolver { get; init; }
|
||||
}
|
||||
@@ -5,67 +5,37 @@ namespace LanMountainDesktop.PluginSdk;
|
||||
public sealed class PluginDesktopComponentRegistration
|
||||
{
|
||||
public PluginDesktopComponentRegistration(
|
||||
string componentId,
|
||||
string displayName,
|
||||
Func<IServiceProvider, PluginDesktopComponentContext, Control> controlFactory,
|
||||
string iconKey = "PuzzlePiece",
|
||||
string category = "Plugins",
|
||||
int minWidthCells = 2,
|
||||
int minHeightCells = 2,
|
||||
bool allowDesktopPlacement = true,
|
||||
bool allowStatusBarPlacement = false,
|
||||
PluginDesktopComponentResizeMode resizeMode = PluginDesktopComponentResizeMode.Proportional,
|
||||
string? displayNameLocalizationKey = null,
|
||||
Func<double, double>? cornerRadiusResolver = null)
|
||||
PluginDesktopComponentOptions options)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(componentId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(displayName);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(iconKey);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(category);
|
||||
ArgumentNullException.ThrowIfNull(controlFactory);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(options.ComponentId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(options.DisplayName);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(options.IconKey);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(options.Category);
|
||||
|
||||
ComponentId = componentId.Trim();
|
||||
DisplayName = displayName.Trim();
|
||||
DisplayNameLocalizationKey = string.IsNullOrWhiteSpace(displayNameLocalizationKey)
|
||||
ComponentId = options.ComponentId.Trim();
|
||||
DisplayName = options.DisplayName.Trim();
|
||||
DisplayNameLocalizationKey = string.IsNullOrWhiteSpace(options.DisplayNameLocalizationKey)
|
||||
? null
|
||||
: displayNameLocalizationKey.Trim();
|
||||
: options.DisplayNameLocalizationKey.Trim();
|
||||
ControlFactory = controlFactory;
|
||||
IconKey = iconKey.Trim();
|
||||
Category = category.Trim();
|
||||
MinWidthCells = Math.Max(1, minWidthCells);
|
||||
MinHeightCells = Math.Max(1, minHeightCells);
|
||||
AllowDesktopPlacement = allowDesktopPlacement;
|
||||
AllowStatusBarPlacement = allowStatusBarPlacement;
|
||||
ResizeMode = resizeMode;
|
||||
CornerRadiusResolver = cornerRadiusResolver;
|
||||
IconKey = options.IconKey.Trim();
|
||||
Category = options.Category.Trim();
|
||||
MinWidthCells = Math.Max(1, options.MinWidthCells);
|
||||
MinHeightCells = Math.Max(1, options.MinHeightCells);
|
||||
AllowDesktopPlacement = options.AllowDesktopPlacement;
|
||||
AllowStatusBarPlacement = options.AllowStatusBarPlacement;
|
||||
ResizeMode = options.ResizeMode;
|
||||
CornerRadiusPreset = options.CornerRadiusPreset;
|
||||
CornerRadiusResolver = options.CornerRadiusResolver;
|
||||
}
|
||||
|
||||
public PluginDesktopComponentRegistration(
|
||||
string componentId,
|
||||
string displayName,
|
||||
Func<PluginDesktopComponentContext, Control> controlFactory,
|
||||
string iconKey = "PuzzlePiece",
|
||||
string category = "Plugins",
|
||||
int minWidthCells = 2,
|
||||
int minHeightCells = 2,
|
||||
bool allowDesktopPlacement = true,
|
||||
bool allowStatusBarPlacement = false,
|
||||
PluginDesktopComponentResizeMode resizeMode = PluginDesktopComponentResizeMode.Proportional,
|
||||
string? displayNameLocalizationKey = null,
|
||||
Func<double, double>? cornerRadiusResolver = null)
|
||||
: this(
|
||||
componentId,
|
||||
displayName,
|
||||
(_, context) => controlFactory(context),
|
||||
iconKey,
|
||||
category,
|
||||
minWidthCells,
|
||||
minHeightCells,
|
||||
allowDesktopPlacement,
|
||||
allowStatusBarPlacement,
|
||||
resizeMode,
|
||||
displayNameLocalizationKey,
|
||||
cornerRadiusResolver)
|
||||
PluginDesktopComponentOptions options)
|
||||
: this((_, context) => controlFactory(context), options)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -91,5 +61,25 @@ public sealed class PluginDesktopComponentRegistration
|
||||
|
||||
public PluginDesktopComponentResizeMode ResizeMode { get; }
|
||||
|
||||
public Func<double, double>? CornerRadiusResolver { get; }
|
||||
public PluginCornerRadiusPreset CornerRadiusPreset { get; }
|
||||
|
||||
public Func<IPluginAppearanceContext, double, double>? CornerRadiusResolver { get; }
|
||||
|
||||
public double ResolveCornerRadius(IPluginAppearanceContext appearance, double cellSize)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(appearance);
|
||||
|
||||
var resolved = CornerRadiusResolver is not null
|
||||
? CornerRadiusResolver(appearance, Math.Max(1d, cellSize))
|
||||
: CornerRadiusPreset == PluginCornerRadiusPreset.Default
|
||||
? appearance.ResolveScaledCornerRadius(
|
||||
Math.Clamp(Math.Max(1d, cellSize) * 0.22, 8, 18),
|
||||
8,
|
||||
18)
|
||||
: appearance.ResolveCornerRadius(CornerRadiusPreset);
|
||||
|
||||
return double.IsFinite(resolved)
|
||||
? Math.Max(0d, resolved)
|
||||
: appearance.ResolveCornerRadius(PluginCornerRadiusPreset.Default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,8 +87,8 @@ public sealed record PluginManifest(
|
||||
throw new InvalidOperationException(
|
||||
$"Plugin '{normalized.Id}' targets API version '{normalized.ApiVersion}' (major {requestedVersion.Major}), " +
|
||||
$"but the host provides '{PluginSdkInfo.ApiVersion}' (major {currentVersion.Major}). " +
|
||||
$"This host only supports v{currentVersion.Major}.x plugins. " +
|
||||
$"Migrate the plugin to API {PluginSdkInfo.ApiVersion} and rebuild the package.");
|
||||
$"This host only supports v{currentVersion.Major}.x plugins and rejects v{requestedVersion.Major}.x packages by default. " +
|
||||
$"Migrate the plugin manifest and code to API {PluginSdkInfo.ApiVersion}, then rebuild and republish the package.");
|
||||
}
|
||||
|
||||
return normalized;
|
||||
|
||||
@@ -2,7 +2,7 @@ namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public static class PluginSdkInfo
|
||||
{
|
||||
public const string ApiVersion = "3.0.0";
|
||||
public const string ApiVersion = "4.0.0";
|
||||
public const string ManifestFileName = "plugin.json";
|
||||
public const string PackageFileExtension = ".laapp";
|
||||
public const string DataDirectoryName = "Data";
|
||||
|
||||
@@ -30,34 +30,15 @@ public static class PluginServiceCollectionExtensions
|
||||
|
||||
public static IServiceCollection AddPluginDesktopComponent<TControl>(
|
||||
this IServiceCollection services,
|
||||
string componentId,
|
||||
string displayName,
|
||||
string iconKey = "PuzzlePiece",
|
||||
string category = "Plugins",
|
||||
int minWidthCells = 2,
|
||||
int minHeightCells = 2,
|
||||
bool allowDesktopPlacement = true,
|
||||
bool allowStatusBarPlacement = false,
|
||||
PluginDesktopComponentResizeMode resizeMode = PluginDesktopComponentResizeMode.Proportional,
|
||||
string? displayNameLocalizationKey = null,
|
||||
Func<double, double>? cornerRadiusResolver = null)
|
||||
PluginDesktopComponentOptions options)
|
||||
where TControl : Control
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
services.AddSingleton(new PluginDesktopComponentRegistration(
|
||||
componentId,
|
||||
displayName,
|
||||
(provider, context) => ActivatorUtilities.CreateInstance<TControl>(provider, context),
|
||||
iconKey,
|
||||
category,
|
||||
minWidthCells,
|
||||
minHeightCells,
|
||||
allowDesktopPlacement,
|
||||
allowStatusBarPlacement,
|
||||
resizeMode,
|
||||
displayNameLocalizationKey,
|
||||
cornerRadiusResolver));
|
||||
options));
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using LanMountainDesktop.Appearance;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Host.Abstractions;
|
||||
using LanMountainDesktop.Views.Components;
|
||||
using Xunit;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
|
||||
public sealed class BuiltInDesktopHostCornerRadiusBaselineTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(80d, 0d)]
|
||||
[InlineData(120d, 1d)]
|
||||
[InlineData(160d, 2.5d)]
|
||||
public void BuiltInDesktopHosts_ResolveToTheUnifiedLgBaseline(double cellSize, double globalScale)
|
||||
{
|
||||
var registry = new DesktopComponentRuntimeRegistry(
|
||||
ComponentRegistry.CreateDefault(),
|
||||
DesktopComponentRuntimeRegistry.GetDefaultRegistrations());
|
||||
var expected = AppearanceCornerRadiusTokenFactory.Create(globalScale).Lg.TopLeft;
|
||||
|
||||
foreach (var descriptor in registry.GetDesktopComponents())
|
||||
{
|
||||
var resolved = descriptor.ResolveCornerRadius(CreateChromeContext(descriptor.Definition.Id, cellSize, globalScale));
|
||||
Assert.Equal(expected, resolved, 3);
|
||||
}
|
||||
}
|
||||
|
||||
private static ComponentChromeContext CreateChromeContext(
|
||||
string componentId,
|
||||
double cellSize,
|
||||
double globalScale)
|
||||
{
|
||||
return new ComponentChromeContext(
|
||||
componentId,
|
||||
null,
|
||||
cellSize,
|
||||
globalScale,
|
||||
AppearanceCornerRadiusTokenFactory.Create(globalScale));
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,18 @@ public sealed class CornerRadiusScaleTests
|
||||
[Fact]
|
||||
public void PluginDesktopComponentContext_AllowsZeroRadiusScaling()
|
||||
{
|
||||
var appearanceContext = new PluginAppearanceContext(new PluginAppearanceSnapshot(
|
||||
GlobalCornerRadiusScale: 0d,
|
||||
CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(new AppearanceCornerRadiusTokens(
|
||||
new CornerRadius(6),
|
||||
new CornerRadius(10),
|
||||
new CornerRadius(14),
|
||||
new CornerRadius(18),
|
||||
new CornerRadius(24),
|
||||
new CornerRadius(30),
|
||||
new CornerRadius(36))),
|
||||
ThemeVariant: "Unknown"));
|
||||
|
||||
var context = new PluginDesktopComponentContext(
|
||||
new PluginManifest("plugin.id", "Plugin Name", "plugin.dll"),
|
||||
"C:\\Plugins\\plugin.id",
|
||||
@@ -45,21 +57,33 @@ public sealed class CornerRadiusScaleTests
|
||||
"component-1",
|
||||
null,
|
||||
96d,
|
||||
0d,
|
||||
new AppearanceCornerRadiusTokens(
|
||||
new CornerRadius(6),
|
||||
new CornerRadius(10),
|
||||
new CornerRadius(14),
|
||||
new CornerRadius(18),
|
||||
new CornerRadius(24),
|
||||
new CornerRadius(30),
|
||||
new CornerRadius(36)));
|
||||
appearanceContext);
|
||||
|
||||
Assert.Equal(0d, context.GlobalCornerRadiusScale, 3);
|
||||
Assert.Equal(0d, context.ResolveScaledCornerRadius(12d), 3);
|
||||
Assert.Equal(0d, context.ResolveScaledCornerRadius(12d, 8d, 18d), 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PluginAppearanceContext_ResolveCornerRadius_DoesNotDoubleScalePresetTokens()
|
||||
{
|
||||
var context = new PluginAppearanceContext(new PluginAppearanceSnapshot(
|
||||
GlobalCornerRadiusScale: 2d,
|
||||
CornerRadiusTokens: new PluginCornerRadiusTokens(
|
||||
Micro: 12d,
|
||||
Xs: 20d,
|
||||
Sm: 28d,
|
||||
Md: 36d,
|
||||
Lg: 48d,
|
||||
Xl: 60d,
|
||||
Island: 72d),
|
||||
ThemeVariant: "Light"));
|
||||
|
||||
Assert.Equal(36d, context.ResolveCornerRadius(PluginCornerRadiusPreset.Md), 3);
|
||||
Assert.Equal(36d, context.ResolveCornerRadius(PluginCornerRadiusPreset.Md, maximum: 40d), 3);
|
||||
Assert.Equal(36d, context.ResolveScaledCornerRadius(18d), 3);
|
||||
}
|
||||
|
||||
private sealed class NullServiceProvider : IServiceProvider
|
||||
{
|
||||
public object? GetService(Type serviceType) => null;
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Collections.Generic;
|
||||
using LanMountainDesktop.Appearance;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Host.Abstractions;
|
||||
using LanMountainDesktop.Views.Components;
|
||||
using Xunit;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
|
||||
public sealed class InfoRecommendationHostCornerRadiusTests
|
||||
{
|
||||
private static readonly string[] WideInfoComponentIds =
|
||||
[
|
||||
BuiltInComponentIds.DesktopDailyPoetry,
|
||||
BuiltInComponentIds.DesktopDailyArtwork,
|
||||
BuiltInComponentIds.DesktopDailyWord,
|
||||
BuiltInComponentIds.DesktopCnrDailyNews,
|
||||
BuiltInComponentIds.DesktopIfengNews,
|
||||
BuiltInComponentIds.DesktopBilibiliHotSearch,
|
||||
BuiltInComponentIds.DesktopBaiduHotSearch,
|
||||
BuiltInComponentIds.DesktopStcn24Forum
|
||||
];
|
||||
|
||||
[Theory]
|
||||
[InlineData(80d)]
|
||||
[InlineData(120d)]
|
||||
[InlineData(160d)]
|
||||
public void InfoHostRegistrations_ResolveToTheUnifiedLgBaseline(double cellSize)
|
||||
{
|
||||
var registry = new DesktopComponentRuntimeRegistry(
|
||||
ComponentRegistry.CreateDefault(),
|
||||
DesktopComponentRuntimeRegistry.GetDefaultRegistrations());
|
||||
|
||||
foreach (var componentId in WideInfoComponentIds)
|
||||
{
|
||||
AssertResolved(registry, componentId, cellSize);
|
||||
}
|
||||
|
||||
AssertResolved(registry, BuiltInComponentIds.DesktopDailyWord2x2, cellSize);
|
||||
}
|
||||
|
||||
private static void AssertResolved(
|
||||
DesktopComponentRuntimeRegistry registry,
|
||||
string componentId,
|
||||
double cellSize)
|
||||
{
|
||||
Assert.True(
|
||||
registry.TryGetDescriptor(componentId, out var descriptor),
|
||||
$"Missing runtime registration for '{componentId}'.");
|
||||
|
||||
var zero = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, 0d));
|
||||
var unit = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, 1d));
|
||||
var max = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, 2.5d));
|
||||
|
||||
Assert.Equal(0d, zero, 3);
|
||||
Assert.Equal(24d, unit, 3);
|
||||
Assert.Equal(60d, max, 3);
|
||||
Assert.True(zero <= unit && unit <= max);
|
||||
}
|
||||
|
||||
private static ComponentChromeContext CreateChromeContext(
|
||||
string componentId,
|
||||
double cellSize,
|
||||
double globalScale)
|
||||
{
|
||||
return new ComponentChromeContext(
|
||||
componentId,
|
||||
null,
|
||||
cellSize,
|
||||
globalScale,
|
||||
AppearanceCornerRadiusTokenFactory.Create(globalScale));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
<Solution>
|
||||
<Project Path="LanAirApp/tools/LanMountainDesktop.PluginPackager/LanMountainDesktop.PluginPackager.csproj" />
|
||||
<Project Path="LanMountainDesktop.Host.Abstractions/LanMountainDesktop.Host.Abstractions.csproj" />
|
||||
<Project Path="LanMountainDesktop.Shared.Contracts/LanMountainDesktop.Shared.Contracts.csproj" />
|
||||
<Project Path="LanMountainDesktop.Settings.Core/LanMountainDesktop.Settings.Core.csproj" />
|
||||
|
||||
@@ -9,6 +9,7 @@ using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.ComponentSystem.Extensions;
|
||||
using LanMountainDesktop.Host.Abstractions;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.Views.Components;
|
||||
@@ -62,7 +63,11 @@ public static class DesktopComponentRegistryFactory
|
||||
registration.ComponentId,
|
||||
registration.DisplayNameLocalizationKey,
|
||||
factoryContext => CreatePluginControl(contribution, factoryContext),
|
||||
registration.CornerRadiusResolver));
|
||||
chromeContext =>
|
||||
{
|
||||
var appearanceContext = CreatePluginAppearanceContext(chromeContext);
|
||||
return registration.ResolveCornerRadius(appearanceContext, chromeContext.CellSize);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +128,10 @@ public static class DesktopComponentRegistryFactory
|
||||
contribution.Plugin.Manifest.Id,
|
||||
settingsService);
|
||||
var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent();
|
||||
var pluginAppearance = new PluginAppearanceContext(new PluginAppearanceSnapshot(
|
||||
GlobalCornerRadiusScale: appearanceSnapshot.GlobalCornerRadiusScale,
|
||||
CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(appearanceSnapshot.CornerRadiusTokens),
|
||||
ThemeVariant: appearanceSnapshot.IsNightMode ? "Dark" : "Light"));
|
||||
var pluginContext = new PluginDesktopComponentContext(
|
||||
contribution.Plugin.Manifest,
|
||||
contribution.Plugin.Context.PluginDirectory,
|
||||
@@ -132,8 +141,7 @@ public static class DesktopComponentRegistryFactory
|
||||
contribution.Registration.ComponentId,
|
||||
context.PlacementId,
|
||||
context.CellSize,
|
||||
appearanceSnapshot.GlobalCornerRadiusScale,
|
||||
appearanceSnapshot.CornerRadiusTokens,
|
||||
pluginAppearance,
|
||||
pluginSettings);
|
||||
|
||||
return contribution.Registration.ControlFactory(contribution.Plugin.Services, pluginContext);
|
||||
@@ -146,6 +154,14 @@ public static class DesktopComponentRegistryFactory
|
||||
}
|
||||
}
|
||||
|
||||
private static IPluginAppearanceContext CreatePluginAppearanceContext(ComponentChromeContext chromeContext)
|
||||
{
|
||||
return new PluginAppearanceContext(new PluginAppearanceSnapshot(
|
||||
GlobalCornerRadiusScale: chromeContext.GlobalCornerRadiusScale,
|
||||
CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(chromeContext.CornerRadiusTokens),
|
||||
ThemeVariant: "Unknown"));
|
||||
}
|
||||
|
||||
private static Control CreatePluginErrorControl(
|
||||
PluginDesktopComponentContribution contribution,
|
||||
Exception exception)
|
||||
|
||||
@@ -325,8 +325,9 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var scale = ResolveScale();
|
||||
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(42 * scale, 16, 56);
|
||||
RootBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
RootBorder.Padding = new Thickness(Math.Clamp(14 * scale, 8, 26));
|
||||
ApplyModeVisualIfNeeded();
|
||||
}
|
||||
|
||||
@@ -381,12 +381,13 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget
|
||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
||||
var unifiedMainRectangle = ResolveUnifiedMainRectangle();
|
||||
RootBorder.CornerRadius = unifiedMainRectangle;
|
||||
RootBorder.Padding = new Thickness(0);
|
||||
|
||||
var horizontalPadding = Math.Clamp(16 * softScale, 8, 24);
|
||||
var verticalPadding = Math.Clamp(14 * softScale, 7, 20);
|
||||
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
||||
CardBorder.CornerRadius = unifiedMainRectangle;
|
||||
CardBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
|
||||
|
||||
var innerWidth = Math.Max(120, totalWidth - (horizontalPadding * 2d));
|
||||
@@ -615,6 +616,11 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget
|
||||
return Math.Clamp(Math.Min(scaleX, scaleY), 0.72, 2.8);
|
||||
}
|
||||
|
||||
private CornerRadius ResolveUnifiedMainRectangle() => new(ResolveUnifiedMainRadiusValue());
|
||||
|
||||
private static double ResolveUnifiedMainRadiusValue() =>
|
||||
HostAppearanceThemeProvider.GetOrCreate().GetCurrent().CornerRadiusTokens.Lg.TopLeft;
|
||||
|
||||
private string L(string key, string fallback)
|
||||
{
|
||||
return _localizationService.GetString(_languageCode, key, fallback);
|
||||
|
||||
@@ -386,12 +386,13 @@ public partial class BilibiliHotSearchWidget : UserControl, IDesktopComponentWid
|
||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
||||
var unifiedMainRectangle = ResolveUnifiedMainRectangle();
|
||||
RootBorder.CornerRadius = unifiedMainRectangle;
|
||||
RootBorder.Padding = new Thickness(0);
|
||||
|
||||
var horizontalPadding = Math.Clamp(16 * softScale, 8, 24);
|
||||
var verticalPadding = Math.Clamp(14 * softScale, 7, 20);
|
||||
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
||||
CardBorder.CornerRadius = unifiedMainRectangle;
|
||||
CardBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
|
||||
|
||||
var innerWidth = Math.Max(120, totalWidth - (horizontalPadding * 2d));
|
||||
@@ -629,6 +630,11 @@ public partial class BilibiliHotSearchWidget : UserControl, IDesktopComponentWid
|
||||
return Math.Clamp(Math.Min(scaleX, scaleY), 0.72, 2.8);
|
||||
}
|
||||
|
||||
private CornerRadius ResolveUnifiedMainRectangle() => new(ResolveUnifiedMainRadiusValue());
|
||||
|
||||
private static double ResolveUnifiedMainRadiusValue() =>
|
||||
HostAppearanceThemeProvider.GetOrCreate().GetCurrent().CornerRadiusTokens.Lg.TopLeft;
|
||||
|
||||
private string L(string key, string fallback)
|
||||
{
|
||||
return _localizationService.GetString(_languageCode, key, fallback);
|
||||
|
||||
@@ -79,11 +79,12 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.34, 12, 28);
|
||||
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
RootBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
RootBorder.Padding = new Thickness(Math.Clamp(_currentCellSize * 0.20, 8, 18));
|
||||
|
||||
WebViewHostBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.24, 10, 22);
|
||||
AddressBarBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.22, 10, 20);
|
||||
WebViewHostBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
AddressBarBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
AddressBarBorder.Padding = new Thickness(8, 6);
|
||||
|
||||
if (RootBorder.Child is Grid rootGrid)
|
||||
|
||||
@@ -613,7 +613,7 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
? CreateBrush("#FF4FC3F7")
|
||||
: CreateBrush("#FF3250");
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.45, 24, 44);
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
RootBorder.Background = _isNightVisual
|
||||
? CreateGradientBrush("#171A21", "#0C0E14")
|
||||
: CreateGradientBrush("#F7F8FC", "#ECEFF6");
|
||||
|
||||
@@ -132,8 +132,8 @@ public partial class ClockWidget : UserControl, IDesktopComponentWidget, ITimeZo
|
||||
var targetHeight = Math.Clamp(cellSize * 0.74, 34, 74);
|
||||
RootBorder.Height = targetHeight;
|
||||
|
||||
// 2. 动态圆角:确保始终是完美的胶囊半圆
|
||||
RootBorder.CornerRadius = new CornerRadius(targetHeight / 2);
|
||||
// 2. 主矩形统一到主题主档圆角
|
||||
RootBorder.CornerRadius = ResolveUnifiedMainRectangle();
|
||||
RootBorder.VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center;
|
||||
|
||||
// 3. 核心:满盈字阶 (Filled Typography)
|
||||
@@ -189,4 +189,9 @@ public partial class ClockWidget : UserControl, IDesktopComponentWidget, ITimeZo
|
||||
RootBorder.ClearValue(Border.BorderThicknessProperty);
|
||||
RootBorder.ClearValue(Border.BoxShadowProperty);
|
||||
}
|
||||
|
||||
private CornerRadius ResolveUnifiedMainRectangle() => new(ResolveUnifiedMainRadiusValue());
|
||||
|
||||
private static double ResolveUnifiedMainRadiusValue() =>
|
||||
HostAppearanceThemeProvider.GetOrCreate().GetCurrent().CornerRadiusTokens.Lg.TopLeft;
|
||||
}
|
||||
|
||||
@@ -545,10 +545,11 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
||||
var scale = ResolveScale();
|
||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||
var unifiedMainRectangle = ResolveUnifiedMainRectangle();
|
||||
RootBorder.CornerRadius = unifiedMainRectangle;
|
||||
RootBorder.Padding = new Thickness(0);
|
||||
|
||||
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||
CardBorder.CornerRadius = unifiedMainRectangle;
|
||||
CardBorder.Padding = new Thickness(
|
||||
Math.Clamp(16 * scale, 8, 24),
|
||||
Math.Clamp(14 * scale, 7, 22),
|
||||
@@ -865,6 +866,11 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
||||
return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale)), 0.56, 2.0);
|
||||
}
|
||||
|
||||
private CornerRadius ResolveUnifiedMainRectangle() => new(ResolveUnifiedMainRadiusValue());
|
||||
|
||||
private static double ResolveUnifiedMainRadiusValue() =>
|
||||
HostAppearanceThemeProvider.GetOrCreate().GetCurrent().CornerRadiusTokens.Lg.TopLeft;
|
||||
|
||||
private static string NormalizeCompactText(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
|
||||
@@ -9,6 +9,25 @@ namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
internal static class ComponentChromeCornerRadiusHelper
|
||||
{
|
||||
public static double ResolveMainRectangleRadiusValue(ComponentChromeContext? chromeContext = null, double fallback = 24d)
|
||||
{
|
||||
if (chromeContext is not null)
|
||||
{
|
||||
return Math.Max(0d, chromeContext.CornerRadiusTokens.Lg.TopLeft);
|
||||
}
|
||||
|
||||
var snapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent();
|
||||
var resolved = snapshot.CornerRadiusTokens.Lg.TopLeft;
|
||||
return double.IsFinite(resolved)
|
||||
? Math.Max(0d, resolved)
|
||||
: Math.Max(0d, fallback * ResolveScale(chromeContext));
|
||||
}
|
||||
|
||||
public static CornerRadius ResolveMainRectangleRadius(ComponentChromeContext? chromeContext = null, double fallback = 24d)
|
||||
{
|
||||
return new CornerRadius(ResolveMainRectangleRadiusValue(chromeContext, fallback));
|
||||
}
|
||||
|
||||
public static double ResolveScale(ComponentChromeContext? chromeContext = null)
|
||||
{
|
||||
if (chromeContext is not null)
|
||||
|
||||
@@ -101,7 +101,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var scale = ResolveScale();
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||
RootBorder.CornerRadius = ResolveUnifiedMainRectangle();
|
||||
|
||||
InfoPanel.Padding = new Thickness(
|
||||
Math.Clamp(18 * scale, 10, 28),
|
||||
@@ -754,6 +754,11 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
||||
return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale)), 0.56, 2.0);
|
||||
}
|
||||
|
||||
private CornerRadius ResolveUnifiedMainRectangle() => new(ResolveUnifiedMainRadiusValue());
|
||||
|
||||
private static double ResolveUnifiedMainRadiusValue() =>
|
||||
HostAppearanceThemeProvider.GetOrCreate().GetCurrent().CornerRadiusTokens.Lg.TopLeft;
|
||||
|
||||
private static string NormalizeCompactText(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
|
||||
@@ -92,7 +92,7 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var scale = ResolveScale();
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||
RootBorder.CornerRadius = ResolveUnifiedMainRectangle();
|
||||
RootBorder.Padding = new Thickness(
|
||||
Math.Clamp(20 * scale, 10, 34),
|
||||
Math.Clamp(16 * scale, 8, 28),
|
||||
@@ -452,6 +452,11 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
||||
return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale)), 0.52, 2.2);
|
||||
}
|
||||
|
||||
private CornerRadius ResolveUnifiedMainRectangle() => new(ResolveUnifiedMainRadiusValue());
|
||||
|
||||
private static double ResolveUnifiedMainRadiusValue() =>
|
||||
HostAppearanceThemeProvider.GetOrCreate().GetCurrent().CornerRadiusTokens.Lg.TopLeft;
|
||||
|
||||
private void ApplyAdaptiveTextLayout(bool isNightMode, double scale, double totalWidth, double totalHeight)
|
||||
{
|
||||
var padding = RootBorder.Padding;
|
||||
|
||||
@@ -328,8 +328,9 @@ public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget,
|
||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * scale, 14, 40);
|
||||
CardBorder.CornerRadius = RootBorder.CornerRadius;
|
||||
var unifiedMainRectangle = ResolveUnifiedMainRectangle();
|
||||
RootBorder.CornerRadius = unifiedMainRectangle;
|
||||
CardBorder.CornerRadius = unifiedMainRectangle;
|
||||
CardBorder.Padding = new Thickness(
|
||||
Math.Clamp(12 * scale, 8, 18),
|
||||
Math.Clamp(11 * scale, 7, 16),
|
||||
@@ -482,6 +483,11 @@ public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget,
|
||||
return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale)), 0.56, 2.0);
|
||||
}
|
||||
|
||||
private CornerRadius ResolveUnifiedMainRectangle() => new(ResolveUnifiedMainRadiusValue());
|
||||
|
||||
private static double ResolveUnifiedMainRadiusValue() =>
|
||||
HostAppearanceThemeProvider.GetOrCreate().GetCurrent().CornerRadiusTokens.Lg.TopLeft;
|
||||
|
||||
private string L(string key, string fallback)
|
||||
{
|
||||
return _localizationService.GetString(_languageCode, key, fallback);
|
||||
|
||||
@@ -298,7 +298,7 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
|
||||
isFourByThree = widthRatio >= 0.9 && heightRatio >= 1.35;
|
||||
}
|
||||
|
||||
var containerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||
var containerRadius = ResolveUnifiedMainRectangle();
|
||||
RootBorder.CornerRadius = containerRadius;
|
||||
RootBorder.Padding = new Thickness(0);
|
||||
|
||||
@@ -527,6 +527,11 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
|
||||
return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale)), 0.56, 2.0);
|
||||
}
|
||||
|
||||
private CornerRadius ResolveUnifiedMainRectangle() => new(ResolveUnifiedMainRadiusValue());
|
||||
|
||||
private static double ResolveUnifiedMainRadiusValue() =>
|
||||
HostAppearanceThemeProvider.GetOrCreate().GetCurrent().CornerRadiusTokens.Lg.TopLeft;
|
||||
|
||||
private string BuildPronunciationText(DailyWordSnapshot snapshot)
|
||||
{
|
||||
var uk = NormalizeCompactText(snapshot.UkPronunciation);
|
||||
|
||||
@@ -324,8 +324,9 @@ public partial class DateWidget : UserControl, IDesktopComponentWidget, ITimeZon
|
||||
private void ApplyAdaptiveTypography()
|
||||
{
|
||||
var scale = ResolveScale();
|
||||
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(28 * scale, 16, 40);
|
||||
RootBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
RootBorder.Padding = new Thickness(Math.Clamp(11 * scale, 7, 17));
|
||||
|
||||
LayoutRoot.ColumnSpacing = Math.Clamp(10 * scale, 6, 16);
|
||||
@@ -337,7 +338,7 @@ public partial class DateWidget : UserControl, IDesktopComponentWidget, ITimeZon
|
||||
Math.Clamp(2.4 * scale, 1, 4));
|
||||
CalendarGrid.Margin = new Thickness(0, 0, 0, Math.Clamp(0.8 * scale, 0, 2));
|
||||
|
||||
LunarCardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(24 * scale, 14, 34);
|
||||
LunarCardBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
LunarCardBorder.Padding = new Thickness(Math.Clamp(14 * scale, 8, 20));
|
||||
RightPanelGrid.RowSpacing = Math.Clamp(7.5 * scale, 3.5, 11);
|
||||
DividerBorder.Margin = new Thickness(0, Math.Clamp(1 * scale, 0, 2), 0, Math.Clamp(1 * scale, 0, 2));
|
||||
|
||||
@@ -89,12 +89,7 @@ public sealed class DesktopComponentRuntimeRegistration
|
||||
public sealed class DesktopComponentRuntimeDescriptor
|
||||
{
|
||||
private static readonly Func<ComponentChromeContext, double> DefaultCornerRadiusResolver =
|
||||
chromeContext =>
|
||||
{
|
||||
var scale = Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, chromeContext.GlobalCornerRadiusScale);
|
||||
var baseRadius = Math.Clamp(chromeContext.CellSize * 0.22, 8, 18);
|
||||
return Math.Clamp(baseRadius * scale, 8 * scale, 18 * scale);
|
||||
};
|
||||
chromeContext => ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadiusValue(chromeContext);
|
||||
|
||||
private readonly Func<DesktopComponentControlFactoryContext, Control> _controlFactory;
|
||||
private readonly Func<ComponentChromeContext, double> _cornerRadiusResolver;
|
||||
@@ -324,194 +319,155 @@ public sealed class DesktopComponentRuntimeRegistry
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.Date,
|
||||
"component.date",
|
||||
() => new DateWidget(),
|
||||
_ => 16),
|
||||
() => new DateWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.MonthCalendar,
|
||||
"component.month_calendar",
|
||||
() => new MonthCalendarWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.26, 10, 22)),
|
||||
() => new MonthCalendarWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.LunarCalendar,
|
||||
"component.lunar_calendar",
|
||||
() => new LunarCalendarWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.30, 12, 26)),
|
||||
() => new LunarCalendarWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopClock,
|
||||
"component.desktop_clock",
|
||||
() => new AnalogClockWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.30, 12, 28)),
|
||||
() => new AnalogClockWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopWeatherClock,
|
||||
"component.weather_clock",
|
||||
() => new WeatherClockWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.34, 14, 30)),
|
||||
() => new WeatherClockWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopWorldClock,
|
||||
"component.world_clock",
|
||||
() => new WorldClockWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.30, 10, 24)),
|
||||
() => new WorldClockWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopTimer,
|
||||
"component.desktop_timer",
|
||||
() => new TimerWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.30, 12, 28)),
|
||||
() => new TimerWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopWeather,
|
||||
"component.desktop_weather",
|
||||
() => new WeatherWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.45, 24, 44)),
|
||||
() => new WeatherWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopHourlyWeather,
|
||||
"component.hourly_weather",
|
||||
() => new HourlyWeatherWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.45, 24, 44)),
|
||||
() => new HourlyWeatherWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopMultiDayWeather,
|
||||
"component.multiday_weather",
|
||||
() => new MultiDayWeatherWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.45, 24, 44)),
|
||||
() => new MultiDayWeatherWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopExtendedWeather,
|
||||
"component.extended_weather",
|
||||
() => new ExtendedWeatherWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.45, 24, 44)),
|
||||
() => new ExtendedWeatherWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopClassSchedule,
|
||||
"component.class_schedule",
|
||||
() => new ClassScheduleWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.45, 24, 44)),
|
||||
() => new ClassScheduleWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopMusicControl,
|
||||
"component.music_control",
|
||||
() => new MusicControlWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.34, 14, 30)),
|
||||
() => new MusicControlWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopAudioRecorder,
|
||||
"component.audio_recorder",
|
||||
() => new RecordingWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.36, 16, 34)),
|
||||
() => new RecordingWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopStudyEnvironment,
|
||||
"component.study_environment",
|
||||
() => new StudyEnvironmentWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.36, 12, 26)),
|
||||
() => new StudyEnvironmentWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopStudySessionControl,
|
||||
"component.study_session_control",
|
||||
() => new StudySessionControlWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.36, 10, 24)),
|
||||
() => new StudySessionControlWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopStudySessionHistory,
|
||||
"component.study_session_history",
|
||||
() => new StudySessionHistoryWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.34, 10, 24)),
|
||||
() => new StudySessionHistoryWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopStudyNoiseCurve,
|
||||
"component.study_noise_curve",
|
||||
() => new StudyNoiseCurveWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.34, 12, 26)),
|
||||
() => new StudyNoiseCurveWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopStudyNoiseDistribution,
|
||||
"component.study_noise_distribution",
|
||||
() => new StudyNoiseDistributionWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.34, 12, 26)),
|
||||
() => new StudyNoiseDistributionWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopStudyScoreOverview,
|
||||
"component.study_score_overview",
|
||||
() => new StudyScoreOverviewWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.34, 12, 28)),
|
||||
() => new StudyScoreOverviewWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopStudyDeductionReasons,
|
||||
"component.study_deduction_reasons",
|
||||
() => new StudyDeductionReasonsWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.34, 10, 24)),
|
||||
() => new StudyDeductionReasonsWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopStudyInterruptDensity,
|
||||
"component.study_interrupt_density",
|
||||
() => new StudyInterruptDensityWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.34, 10, 24)),
|
||||
() => new StudyInterruptDensityWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopDailyPoetry,
|
||||
"component.daily_poetry",
|
||||
() => new DailyPoetryWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.34, 14, 30)),
|
||||
() => new DailyPoetryWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopDailyArtwork,
|
||||
"component.daily_artwork",
|
||||
() => new DailyArtworkWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.34, 14, 30)),
|
||||
() => new DailyArtworkWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopDailyWord,
|
||||
"component.daily_word",
|
||||
() => new DailyWordWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.34, 14, 30)),
|
||||
() => new DailyWordWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopDailyWord2x2,
|
||||
"component.daily_word_2x2",
|
||||
() => new DailyWord2x2Widget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.34, 12, 26)),
|
||||
() => new DailyWord2x2Widget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopCnrDailyNews,
|
||||
"component.cnr_daily_news",
|
||||
() => new CnrDailyNewsWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.34, 14, 30)),
|
||||
() => new CnrDailyNewsWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopIfengNews,
|
||||
"component.ifeng_news",
|
||||
() => new IfengNewsWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.30, 12, 24)),
|
||||
() => new IfengNewsWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopBilibiliHotSearch,
|
||||
"component.bilibili_hot_search",
|
||||
() => new BilibiliHotSearchWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.34, 14, 30)),
|
||||
() => new BilibiliHotSearchWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopBaiduHotSearch,
|
||||
"component.baidu_hot_search",
|
||||
() => new BaiduHotSearchWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.34, 14, 30)),
|
||||
() => new BaiduHotSearchWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopStcn24Forum,
|
||||
"component.stcn24_forum",
|
||||
() => new Stcn24ForumWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.28, 12, 24)),
|
||||
() => new Stcn24ForumWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopExchangeRateCalculator,
|
||||
"component.exchange_rate_converter",
|
||||
() => new ExchangeRateCalculatorWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.28, 12, 26)),
|
||||
() => new ExchangeRateCalculatorWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopWhiteboard,
|
||||
"component.whiteboard",
|
||||
() => new WhiteboardWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.24, 10, 24)),
|
||||
() => new WhiteboardWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopBlackboardLandscape,
|
||||
"component.blackboard_landscape",
|
||||
() => new WhiteboardWidget(baseWidthCells: 4),
|
||||
cellSize => Math.Clamp(cellSize * 0.24, 10, 24)),
|
||||
() => new WhiteboardWidget(baseWidthCells: 4)),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopBrowser,
|
||||
"component.browser",
|
||||
() => new BrowserWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.24, 10, 24)),
|
||||
() => new BrowserWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopOfficeRecentDocuments,
|
||||
"component.office_recent_documents",
|
||||
_ => new OfficeRecentDocumentsWidget(),
|
||||
chromeContext => Math.Clamp(chromeContext.CellSize * 0.50, 10, 24) *
|
||||
Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, chromeContext.GlobalCornerRadiusScale)),
|
||||
() => new OfficeRecentDocumentsWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopRemovableStorage,
|
||||
"component.removable_storage",
|
||||
() => new RemovableStorageWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.46, 12, 26)),
|
||||
() => new RemovableStorageWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.HolidayCalendar,
|
||||
"component.holiday_calendar",
|
||||
() => new HolidayCalendarWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.32, 12, 28))
|
||||
() => new HolidayCalendarWidget())
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ public partial class ExchangeRateCalculatorWidget : UserControl, IDesktopCompone
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var scale = ResolveScale();
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 14, 48);
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
RootBorder.Padding = new Thickness(ComponentChromeCornerRadiusHelper.SafeValue(12 * scale, 6, 18));
|
||||
}
|
||||
|
||||
|
||||
@@ -124,13 +124,9 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
var metrics = HyperOS3WeatherTheme.ResolveMetrics(HyperOS3WeatherWidgetKind.Extended4x4);
|
||||
var width = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * 4;
|
||||
var height = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * 4;
|
||||
var radius = ComponentChromeCornerRadiusHelper.Scale(
|
||||
_currentCellSize * metrics.CornerRadiusScale,
|
||||
28,
|
||||
54,
|
||||
_chromeContext);
|
||||
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius(_chromeContext);
|
||||
ComponentChromeCornerRadiusHelper.Apply(
|
||||
radius,
|
||||
mainRectangleCornerRadius,
|
||||
RootBorder,
|
||||
BackgroundImageLayer,
|
||||
BackgroundMotionLayer,
|
||||
|
||||
@@ -216,7 +216,7 @@ public partial class HolidayCalendarWidget : UserControl, IDesktopComponentWidge
|
||||
var titleNeedsTwoLines = isUltraCompact || titleUnits >= (isCompact ? 13 : 17);
|
||||
var dateNeedsTwoLines = isUltraCompact || dateUnits >= (isCompact ? 15 : 20);
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(shortSide * 0.13, 10, 46);
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
var padding = ComponentChromeCornerRadiusHelper.SafeValue(shortSide * 0.05, 4.5, 21);
|
||||
RootBorder.Padding = new Thickness(padding);
|
||||
LayoutRoot.RowSpacing = Math.Clamp(shortSide * 0.028, 2.2, 12);
|
||||
|
||||
@@ -270,14 +270,10 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
var scale = ResolveScale();
|
||||
var hostWidth = Bounds.Width > 1 ? Bounds.Width : Math.Max(140, _currentCellSize * 4);
|
||||
var hostHeight = Bounds.Height > 1 ? Bounds.Height : Math.Max(78, _currentCellSize * 2);
|
||||
var cornerRadius = ComponentChromeCornerRadiusHelper.Scale(
|
||||
_currentCellSize * metrics.CornerRadiusScale,
|
||||
24,
|
||||
46,
|
||||
_chromeContext);
|
||||
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius(_chromeContext);
|
||||
|
||||
ComponentChromeCornerRadiusHelper.Apply(
|
||||
cornerRadius,
|
||||
mainRectangleCornerRadius,
|
||||
RootBorder,
|
||||
BackgroundImageLayer,
|
||||
BackgroundMotionLayer,
|
||||
|
||||
@@ -400,8 +400,9 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
|
||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(32 * softScale, 16, 46);
|
||||
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(32 * softScale, 16, 46);
|
||||
var unifiedMainRectangle = ResolveUnifiedMainRectangle();
|
||||
RootBorder.CornerRadius = unifiedMainRectangle;
|
||||
CardBorder.CornerRadius = unifiedMainRectangle;
|
||||
|
||||
var horizontalPadding = Math.Clamp(14 * softScale, 8, 20);
|
||||
var verticalPadding = Math.Clamp(14 * softScale, 8, 20);
|
||||
@@ -683,6 +684,11 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
|
||||
return Math.Clamp(Math.Min(scaleX, scaleY), 0.72, 2.4);
|
||||
}
|
||||
|
||||
private CornerRadius ResolveUnifiedMainRectangle() => new(ResolveUnifiedMainRadiusValue());
|
||||
|
||||
private static double ResolveUnifiedMainRadiusValue() =>
|
||||
HostAppearanceThemeProvider.GetOrCreate().GetCurrent().CornerRadiusTokens.Lg.TopLeft;
|
||||
|
||||
private static string NormalizeCompactText(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
|
||||
@@ -181,8 +181,9 @@ public partial class LunarCalendarWidget : UserControl, IDesktopComponentWidget,
|
||||
private void ApplyAdaptiveTypography()
|
||||
{
|
||||
var scale = ResolveScale();
|
||||
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * scale, 16, 44);
|
||||
RootBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
RootBorder.Padding = new Thickness(ComponentChromeCornerRadiusHelper.SafeValue(16 * scale, 8, 24));
|
||||
LayoutRoot.RowSpacing = Math.Clamp(10 * scale, 5, 18);
|
||||
DividerBorder.Margin = new Thickness(
|
||||
|
||||
@@ -216,8 +216,9 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget,
|
||||
private void ApplyAdaptiveTypography()
|
||||
{
|
||||
var scale = ResolveScale();
|
||||
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(28 * scale, 14, 40);
|
||||
RootBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
RootBorder.Padding = new Thickness(ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 8, 22));
|
||||
LayoutRoot.RowSpacing = Math.Clamp(10 * scale, 5, 16);
|
||||
LayoutRoot.Width = Math.Clamp(280 * scale, 220, 420);
|
||||
|
||||
@@ -268,14 +268,10 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
var scale = ResolveScale();
|
||||
var hostWidth = Bounds.Width > 1 ? Bounds.Width : Math.Max(140, _currentCellSize * 4);
|
||||
var hostHeight = Bounds.Height > 1 ? Bounds.Height : Math.Max(78, _currentCellSize * 2);
|
||||
var cornerRadius = ComponentChromeCornerRadiusHelper.Scale(
|
||||
_currentCellSize * metrics.CornerRadiusScale,
|
||||
24,
|
||||
46,
|
||||
_chromeContext);
|
||||
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius(_chromeContext);
|
||||
|
||||
ComponentChromeCornerRadiusHelper.Apply(
|
||||
cornerRadius,
|
||||
mainRectangleCornerRadius,
|
||||
RootBorder,
|
||||
BackgroundImageLayer,
|
||||
BackgroundMotionLayer,
|
||||
|
||||
@@ -63,7 +63,7 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var scale = ResolveScale();
|
||||
|
||||
var rootCornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * scale, 16, 44);
|
||||
var rootCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
|
||||
RootBorder.CornerRadius = rootCornerRadius;
|
||||
ContentPaddingBorder.Padding = new Thickness(
|
||||
@@ -84,7 +84,7 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
CoverBorder.Width = Math.Clamp(56 * scale, 38, 86);
|
||||
CoverBorder.Height = Math.Clamp(56 * scale, 38, 86);
|
||||
CoverBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(12 * scale, 8, 16);
|
||||
CoverBorder.CornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
|
||||
TitleTextBlock.FontSize = Math.Clamp(20 * scale, 12, 28);
|
||||
ArtistTextBlock.FontSize = Math.Clamp(14 * scale, 9, 18);
|
||||
|
||||
@@ -36,7 +36,7 @@ public partial class OfficeRecentDocumentsWidget : UserControl, IDesktopComponen
|
||||
return;
|
||||
}
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(cellSize * 0.50, 10, 24);
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
}
|
||||
|
||||
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
|
||||
|
||||
@@ -63,7 +63,7 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe
|
||||
var chromeScale = Math.Clamp(rawScale, 0.62, 2.0);
|
||||
var contentScale = Math.Clamp(rawScale, 0.74, 1.0);
|
||||
|
||||
var rootRadius = ComponentChromeCornerRadiusHelper.Scale(34 * chromeScale, 16, 56);
|
||||
var rootRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
RootBorder.CornerRadius = rootRadius;
|
||||
RootBorder.Padding = new Thickness(0);
|
||||
RecorderCardBorder.CornerRadius = rootRadius;
|
||||
|
||||
@@ -347,7 +347,7 @@ public partial class RemovableStorageWidget : UserControl, IDesktopComponentWidg
|
||||
var scale = ResolveScale();
|
||||
var width = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * 2;
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.44, 18, 34);
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
RootBorder.Padding = new Thickness(
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(16 * scale, 10, 24),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(15 * scale, 10, 22),
|
||||
|
||||
@@ -602,8 +602,9 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * softScale, 14, 44);
|
||||
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * softScale, 14, 44);
|
||||
var unifiedMainRectangle = ResolveUnifiedMainRectangle();
|
||||
RootBorder.CornerRadius = unifiedMainRectangle;
|
||||
CardBorder.CornerRadius = unifiedMainRectangle;
|
||||
CardBorder.Padding = new Thickness(
|
||||
Math.Clamp(12 * softScale, 8, 18),
|
||||
Math.Clamp(12 * softScale, 8, 18),
|
||||
@@ -833,6 +834,11 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
||||
return Math.Clamp(Math.Min(scaleX, scaleY), 0.62, 2.6);
|
||||
}
|
||||
|
||||
private CornerRadius ResolveUnifiedMainRectangle() => new(ResolveUnifiedMainRadiusValue());
|
||||
|
||||
private static double ResolveUnifiedMainRadiusValue() =>
|
||||
HostAppearanceThemeProvider.GetOrCreate().GetCurrent().CornerRadiusTokens.Lg.TopLeft;
|
||||
|
||||
private string L(string key, string fallback)
|
||||
{
|
||||
return _localizationService.GetString(_languageCode, key, fallback);
|
||||
|
||||
@@ -229,7 +229,8 @@ public partial class StudyDeductionReasonsWidget : UserControl, IDesktopComponen
|
||||
_isUltraCompactMode = scale < 0.72 || (Bounds.Width > 1 && Bounds.Width < 300) || (Bounds.Height > 1 && Bounds.Height < 145);
|
||||
|
||||
var compactMultiplier = _isUltraCompactMode ? 0.76 : _isCompactMode ? 0.88 : 1.0;
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.46, 12, 34);
|
||||
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
RootBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
RootBorder.Padding = new Thickness(
|
||||
Math.Clamp(12 * scale * compactMultiplier, 6, 18),
|
||||
Math.Clamp(10 * scale * compactMultiplier, 5, 16));
|
||||
@@ -276,6 +277,9 @@ public partial class StudyDeductionReasonsWidget : UserControl, IDesktopComponen
|
||||
SustainedRowBorder.Padding = rowPadding;
|
||||
TimeRowBorder.Padding = rowPadding;
|
||||
SegmentRowBorder.Padding = rowPadding;
|
||||
SustainedRowBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
TimeRowBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
SegmentRowBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
|
||||
SustainedMetricTextBlock.IsVisible = !_isUltraCompactMode;
|
||||
TimeMetricTextBlock.IsVisible = !_isUltraCompactMode;
|
||||
|
||||
@@ -52,7 +52,7 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var scale = Math.Clamp(_currentCellSize / 48d, 0.82, 2.2);
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.34, 10, 28);
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
RootBorder.Padding = new Thickness(
|
||||
Math.Clamp(14 * scale, 8, 20),
|
||||
Math.Clamp(10 * scale, 6, 16));
|
||||
|
||||
@@ -255,7 +255,8 @@ public partial class StudyInterruptDensityWidget : UserControl, IDesktopComponen
|
||||
_isUltraCompactMode = scale < 0.72 || (Bounds.Width > 1 && Bounds.Width < 295) || (Bounds.Height > 1 && Bounds.Height < 130);
|
||||
|
||||
var compactMultiplier = _isUltraCompactMode ? 0.76 : _isCompactMode ? 0.88 : 1.0;
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.46, 12, 34);
|
||||
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
RootBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
RootBorder.Padding = new Thickness(
|
||||
Math.Clamp(12 * scale * compactMultiplier, 6, 18),
|
||||
Math.Clamp(9 * scale * compactMultiplier, 5, 16));
|
||||
@@ -301,6 +302,8 @@ public partial class StudyInterruptDensityWidget : UserControl, IDesktopComponen
|
||||
Math.Clamp(6 * scale * compactMultiplier, 3, 9));
|
||||
CountCardBorder.Padding = cardPadding;
|
||||
DurationCardBorder.Padding = cardPadding;
|
||||
CountCardBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
DurationCardBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
|
||||
TitleTextBlock.IsVisible = !_isUltraCompactMode;
|
||||
ThresholdTextBlock.IsVisible = !_isUltraCompactMode;
|
||||
|
||||
@@ -105,7 +105,7 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var scale = Math.Clamp(_currentCellSize / 48d, 0.78, 2.4);
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.44, 14, 42);
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
RootBorder.Padding = new Thickness(
|
||||
Math.Clamp(14 * scale, 8, 22),
|
||||
Math.Clamp(10 * scale, 6, 16));
|
||||
|
||||
@@ -323,7 +323,7 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone
|
||||
_isUltraCompactMode = scale < 0.74 || (Bounds.Width > 1 && Bounds.Width < 300) || (Bounds.Height > 1 && Bounds.Height < 142);
|
||||
|
||||
var compactMultiplier = _isUltraCompactMode ? 0.76 : _isCompactMode ? 0.88 : 1.0;
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.44, 12, 34);
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
RootBorder.Padding = new Thickness(
|
||||
Math.Clamp(12 * scale * compactMultiplier, 6, 18),
|
||||
Math.Clamp(9 * scale * compactMultiplier, 5, 16));
|
||||
|
||||
@@ -258,7 +258,8 @@ public partial class StudyScoreOverviewWidget : UserControl, IDesktopComponentWi
|
||||
|
||||
var compactMultiplier = _isUltraCompactMode ? 0.76 : _isCompactMode ? 0.88 : 1.0;
|
||||
var expandedMultiplier = _isExpandedMode ? 1.12 : 1.0;
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.50, 14, 42);
|
||||
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
RootBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
RootBorder.Padding = new Thickness(
|
||||
Math.Clamp(16 * scale * compactMultiplier * expandedMultiplier, 8, 30),
|
||||
Math.Clamp(14 * scale * compactMultiplier * expandedMultiplier, 6, 26));
|
||||
@@ -305,7 +306,7 @@ public partial class StudyScoreOverviewWidget : UserControl, IDesktopComponentWi
|
||||
var cardPadding = new Thickness(
|
||||
Math.Clamp(10 * scale * compactMultiplier * expandedMultiplier, 6, 20),
|
||||
Math.Clamp(8 * scale * compactMultiplier * expandedMultiplier, 4, 16));
|
||||
var cardCornerRadius = ComponentChromeCornerRadiusHelper.Scale(10 * scale, 6, 18);
|
||||
var cardCornerRadius = mainRectangleCornerRadius;
|
||||
AverageCardBorder.Padding = cardPadding;
|
||||
MinimumCardBorder.Padding = cardPadding;
|
||||
MaximumCardBorder.Padding = cardPadding;
|
||||
|
||||
@@ -268,7 +268,7 @@ public partial class StudySessionControlWidget : UserControl, IDesktopComponentW
|
||||
_isUltraCompactMode = scale < 0.74 || (Bounds.Width > 1 && Bounds.Width < 180) || (Bounds.Height > 1 && Bounds.Height < 76);
|
||||
|
||||
var compactMultiplier = _isUltraCompactMode ? 0.78 : _isCompactMode ? 0.90 : 1.0;
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.34, 10, 28);
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
RootBorder.Padding = new Thickness(
|
||||
Math.Clamp(14 * scale * compactMultiplier, 7, 22),
|
||||
Math.Clamp(10 * scale * compactMultiplier, 5, 16));
|
||||
|
||||
@@ -237,7 +237,7 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
||||
|
||||
var rowBorder = new Border
|
||||
{
|
||||
CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.20, 8, 14),
|
||||
CornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius(),
|
||||
Background = new SolidColorBrush(rowBackground),
|
||||
BorderBrush = new SolidColorBrush(rowBorderColor),
|
||||
BorderThickness = new Thickness(1),
|
||||
@@ -588,7 +588,8 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
||||
_isCompactMode = scale < 0.92 || (Bounds.Width > 1 && Bounds.Width < 320) || (Bounds.Height > 1 && Bounds.Height < 145);
|
||||
_isUltraCompactMode = scale < 0.78 || (Bounds.Width > 1 && Bounds.Width < 280) || (Bounds.Height > 1 && Bounds.Height < 120);
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.44, 12, 36);
|
||||
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
RootBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
RootBorder.Padding = new Thickness(
|
||||
Math.Clamp(12 * scale, 7, 22),
|
||||
Math.Clamp(9 * scale, 5, 16));
|
||||
@@ -606,7 +607,7 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
||||
DialogOverlayBorder.Padding = new Thickness(
|
||||
Math.Clamp(12 * scale, 8, 20),
|
||||
Math.Clamp(10 * scale, 8, 18));
|
||||
DialogCardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(12 * scale, 10, 18);
|
||||
DialogCardBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
DialogCardBorder.Padding = new Thickness(
|
||||
Math.Clamp(12 * scale, 9, 20),
|
||||
Math.Clamp(11 * scale, 8, 18));
|
||||
|
||||
@@ -196,10 +196,11 @@ public partial class TimerWidget : UserControl, IDesktopComponentWidget
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var scale = ResolveScale();
|
||||
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 12, 48);
|
||||
RootBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
RootBorder.Padding = new Thickness(Math.Clamp(14 * scale, 7, 22));
|
||||
TimerPanelBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(32 * scale, 12, 42);
|
||||
TimerPanelBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
|
||||
PlayButtonBorder.Width = Math.Clamp(42 * scale, 28, 58);
|
||||
PlayButtonBorder.Height = Math.Clamp(42 * scale, 28, 58);
|
||||
|
||||
@@ -151,11 +151,7 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
|
||||
var compactness = Math.Clamp((176 - targetWidth) / 86d, 0, 1);
|
||||
var ultraCompact = targetWidth < 126 || targetHeight < 46;
|
||||
var compactFactor = Lerp(1, ultraCompact ? 0.64 : 0.72, compactness);
|
||||
var cornerRadius = ComponentChromeCornerRadiusHelper.Scale(
|
||||
targetHeight * metrics.CornerRadiusScale,
|
||||
15,
|
||||
36,
|
||||
_chromeContext);
|
||||
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius(_chromeContext);
|
||||
|
||||
var horizontalPadding = ComponentChromeCornerRadiusHelper.SafeValue(
|
||||
targetHeight * Lerp(0.18, 0.12, compactness),
|
||||
@@ -168,7 +164,7 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
|
||||
20,
|
||||
_chromeContext);
|
||||
|
||||
RootBorder.CornerRadius = cornerRadius;
|
||||
RootBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
RootBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
|
||||
|
||||
var columnSpacing = Math.Clamp(targetHeight * Lerp(0.16, 0.08, compactness), 2, 22);
|
||||
|
||||
@@ -213,16 +213,12 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
|
||||
var metrics = HyperOS3WeatherTheme.ResolveMetrics(HyperOS3WeatherWidgetKind.Realtime2x2);
|
||||
var hostWidth = Bounds.Width > 1 ? Bounds.Width : Math.Max(80, _currentCellSize * 2);
|
||||
var hostHeight = Bounds.Height > 1 ? Bounds.Height : Math.Max(80, _currentCellSize * 2);
|
||||
var cornerRadius = ComponentChromeCornerRadiusHelper.Scale(
|
||||
_currentCellSize * metrics.CornerRadiusScale,
|
||||
26,
|
||||
46,
|
||||
_chromeContext);
|
||||
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius(_chromeContext);
|
||||
var horizontalPadding = Math.Clamp(_currentCellSize * metrics.HorizontalPaddingScale, 10, 24);
|
||||
var verticalPadding = Math.Clamp(_currentCellSize * metrics.VerticalPaddingScale, 10, 24);
|
||||
|
||||
ComponentChromeCornerRadiusHelper.Apply(
|
||||
cornerRadius,
|
||||
mainRectangleCornerRadius,
|
||||
RootBorder,
|
||||
BackgroundImageLayer,
|
||||
BackgroundMotionLayer,
|
||||
|
||||
@@ -118,9 +118,10 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
|
||||
var toolbarPaddingVertical = Math.Clamp(buttonSize * 0.24, 4, 8);
|
||||
|
||||
RootBorder.Padding = new Thickness(ComponentChromeCornerRadiusHelper.SafeValue(_currentCellSize * 0.14, 6, 14));
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.34, 12, 28);
|
||||
CanvasBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.24, 10, 22);
|
||||
ToolbarBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.22, 10, 20);
|
||||
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
RootBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
CanvasBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
ToolbarBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
ToolbarBorder.Padding = new Thickness(
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(toolbarPaddingHorizontal, 6, 12),
|
||||
ComponentChromeCornerRadiusHelper.SafeValue(toolbarPaddingVertical, 4, 8));
|
||||
|
||||
@@ -166,11 +166,12 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT
|
||||
|
||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
|
||||
|
||||
var horizontalPadding = Math.Clamp(10 * scale, 4, 26);
|
||||
var verticalPadding = Math.Clamp(8 * scale, 3, 22);
|
||||
RootBorder.Padding = new Thickness(horizontalPadding, verticalPadding);
|
||||
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(24 * scale, 10, 46);
|
||||
RootBorder.CornerRadius = mainRectangleCornerRadius;
|
||||
|
||||
var usableWidth = Math.Max(48, totalWidth - horizontalPadding * 2);
|
||||
var usableHeight = Math.Max(28, totalHeight - verticalPadding * 2);
|
||||
|
||||
@@ -269,7 +269,7 @@
|
||||
Grid.ColumnSpan="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="0"
|
||||
CornerRadius="36"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
|
||||
Padding="6">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto"
|
||||
ColumnSpacing="8">
|
||||
|
||||
@@ -346,6 +346,7 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
ApplyAdaptiveThemeResources();
|
||||
_recommendedColors = snapshot.MonetPalette.RecommendedColors;
|
||||
_monetColors = snapshot.MonetPalette.MonetColors;
|
||||
ApplyUnifiedMainRectangleChrome(snapshot);
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
@@ -491,7 +492,7 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
TopStatusBarHost.Padding = new Thickness(0);
|
||||
|
||||
BottomTaskbarContainer.Margin = new Thickness(0);
|
||||
BottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(taskbarCellHeight * 0.58, 20, 44));
|
||||
ApplyUnifiedMainRectangleChrome();
|
||||
BottomTaskbarContainer.Padding = new Thickness(Math.Clamp(taskbarCellHeight * 0.16, 6, 14));
|
||||
|
||||
ClockWidget.Margin = new Thickness(0);
|
||||
@@ -527,6 +528,27 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
UpdateComponentLibraryLayout(cellSize);
|
||||
}
|
||||
|
||||
private void ApplyUnifiedMainRectangleChrome(AppearanceThemeSnapshot? snapshot = null)
|
||||
{
|
||||
var unifiedMainRectangle = new CornerRadius(ResolveUnifiedMainRadiusValue(snapshot));
|
||||
BottomTaskbarContainer.CornerRadius = unifiedMainRectangle;
|
||||
|
||||
if (_currentDesktopCellSize > 0)
|
||||
{
|
||||
ClockWidget.ApplyCellSize(_currentDesktopCellSize);
|
||||
}
|
||||
}
|
||||
|
||||
private double ResolveUnifiedMainRadiusValue(AppearanceThemeSnapshot? snapshot = null)
|
||||
{
|
||||
if (snapshot is not null)
|
||||
{
|
||||
return snapshot.CornerRadiusTokens.Lg.TopLeft;
|
||||
}
|
||||
|
||||
return _appearanceThemeService.GetCurrent().CornerRadiusTokens.Lg.TopLeft;
|
||||
}
|
||||
|
||||
private static void SetButtonContentSpacing(Button? button, double spacing)
|
||||
{
|
||||
if (button?.Content is StackPanel contentPanel)
|
||||
|
||||
@@ -156,7 +156,7 @@ public sealed class PluginLoader
|
||||
var pluginType = ResolvePluginType(assembly);
|
||||
plugin = CreatePluginInstance(pluginType);
|
||||
AppLogger.Info("PluginLoader", $"Plugin instance created. PluginId='{manifest.Id}'; PluginType='{pluginType.FullName}'.");
|
||||
runtimeContext = CreateRuntimeContext(manifest, pluginDirectory, dataDirectory, properties);
|
||||
runtimeContext = CreateRuntimeContext(manifest, pluginDirectory, dataDirectory, properties, services);
|
||||
var serviceCollection = CreateServiceCollection(runtimeContext, services);
|
||||
var hostBuilderContext = CreateHostBuilderContext(runtimeContext);
|
||||
|
||||
@@ -297,13 +297,15 @@ public sealed class PluginLoader
|
||||
PluginManifest manifest,
|
||||
string pluginDirectory,
|
||||
string dataDirectory,
|
||||
IReadOnlyDictionary<string, object?>? properties)
|
||||
IReadOnlyDictionary<string, object?>? properties,
|
||||
IServiceProvider? hostServices)
|
||||
{
|
||||
return new PluginRuntimeContext(
|
||||
manifest,
|
||||
pluginDirectory,
|
||||
dataDirectory,
|
||||
CreateReadOnlyProperties(properties));
|
||||
CreateReadOnlyProperties(properties),
|
||||
BuildAppearanceSnapshot(hostServices));
|
||||
}
|
||||
|
||||
private ServiceCollection CreateServiceCollection(
|
||||
@@ -313,6 +315,7 @@ public sealed class PluginLoader
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(runtimeContext);
|
||||
services.AddSingleton<IPluginRuntimeContext>(runtimeContext);
|
||||
services.AddSingleton<IPluginAppearanceContext>(runtimeContext.Appearance);
|
||||
services.AddSingleton(runtimeContext.Manifest);
|
||||
services.AddSingleton<IReadOnlyDictionary<string, object?>>(runtimeContext.Properties);
|
||||
services.AddSingleton<IPluginMessageBus, PluginMessageBus>();
|
||||
@@ -332,6 +335,33 @@ public sealed class PluginLoader
|
||||
return services;
|
||||
}
|
||||
|
||||
private static PluginAppearanceSnapshot BuildAppearanceSnapshot(IServiceProvider? hostServices)
|
||||
{
|
||||
var defaultSnapshot = new PluginAppearanceSnapshot(
|
||||
GlobalCornerRadiusScale: 1d,
|
||||
CornerRadiusTokens: new PluginCornerRadiusTokens(6, 10, 14, 18, 24, 30, 36),
|
||||
ThemeVariant: "Unknown");
|
||||
|
||||
if (hostServices?.GetService(typeof(IAppearanceThemeService)) is not IAppearanceThemeService appearanceThemeService)
|
||||
{
|
||||
return defaultSnapshot;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var hostSnapshot = appearanceThemeService.GetCurrent();
|
||||
return new PluginAppearanceSnapshot(
|
||||
GlobalCornerRadiusScale: Math.Max(0d, hostSnapshot.GlobalCornerRadiusScale),
|
||||
CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(hostSnapshot.CornerRadiusTokens),
|
||||
ThemeVariant: hostSnapshot.IsNightMode ? "Dark" : "Light");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("PluginLoader", "Failed to resolve host appearance snapshot for plugin runtime context.", ex);
|
||||
return defaultSnapshot;
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterHostService<TService>(IServiceCollection services, IServiceProvider? hostServices)
|
||||
where TService : class
|
||||
{
|
||||
@@ -730,12 +760,14 @@ public sealed class PluginLoader
|
||||
PluginManifest manifest,
|
||||
string pluginDirectory,
|
||||
string dataDirectory,
|
||||
IReadOnlyDictionary<string, object?> properties)
|
||||
IReadOnlyDictionary<string, object?> properties,
|
||||
PluginAppearanceSnapshot appearanceSnapshot)
|
||||
{
|
||||
Manifest = manifest;
|
||||
PluginDirectory = pluginDirectory;
|
||||
DataDirectory = dataDirectory;
|
||||
Properties = properties;
|
||||
Appearance = new PluginAppearanceContext(appearanceSnapshot);
|
||||
Services = NullServiceProvider.Instance;
|
||||
}
|
||||
|
||||
@@ -749,6 +781,8 @@ public sealed class PluginLoader
|
||||
|
||||
public IReadOnlyDictionary<string, object?> Properties { get; }
|
||||
|
||||
public IPluginAppearanceContext Appearance { get; }
|
||||
|
||||
public T? GetService<T>()
|
||||
{
|
||||
return (T?)Services.GetService(typeof(T));
|
||||
|
||||
@@ -90,16 +90,30 @@ internal static class AirAppMarketDefaults
|
||||
private static string? TryResolveWorkspacePath(string repositoryName, string relativePath)
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (current is not null)
|
||||
while (current is not null && current.Exists)
|
||||
{
|
||||
var candidate = Path.Combine(current.FullName, repositoryName);
|
||||
if (Directory.Exists(candidate))
|
||||
var solutionPath = Path.Combine(current.FullName, "LanMountainDesktop.slnx");
|
||||
if (File.Exists(solutionPath))
|
||||
{
|
||||
var candidatePath = Path.GetFullPath(Path.Combine(candidate, relativePath));
|
||||
var workspaceRoot = current.Parent;
|
||||
if (workspaceRoot is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var candidateRepositoryPath = Path.Combine(workspaceRoot.FullName, repositoryName);
|
||||
if (!Directory.Exists(candidateRepositoryPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var candidatePath = Path.GetFullPath(Path.Combine(candidateRepositoryPath, relativePath));
|
||||
if (File.Exists(candidatePath))
|
||||
{
|
||||
return candidatePath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
|
||||
@@ -1,53 +1,26 @@
|
||||
# 宿主侧插件运行时 / Host Plugin Runtime
|
||||
|
||||
## 中文
|
||||
|
||||
本目录保存阑山桌面宿主侧插件运行时实现。
|
||||
|
||||
### 主要职责
|
||||
|
||||
- 发现、安装和替换 `.laapp` 插件包
|
||||
- 加载插件程序集和共享契约
|
||||
- 接入插件设置页、桌面组件与市场界面
|
||||
- 为 `3.0.0` API 基线插件构建插件作用域的 `IServiceCollection` / `ServiceProvider`
|
||||
- 在激活前解析共享契约缓存,并暴露显式插件导出
|
||||
|
||||
### 与 LanAirApp 的分工
|
||||
|
||||
- `LanAirApp` 负责官方市场索引、开发文档、校验工具和镜像样例
|
||||
- 本目录负责宿主运行时发现、安装、加载和界面接入
|
||||
- 权威示例插件是独立仓库 `LanMountainDesktop.SamplePlugin`,`LanAirApp` 中的样例目录只是镜像模板
|
||||
|
||||
### 市场安装顺序
|
||||
|
||||
1. 宿主读取官方 `LanAirApp/airappmarket/index.json`
|
||||
2. 若条目同时包含 `releaseTag` 与 `releaseAssetName`,优先解析 GitHub Release 资产
|
||||
3. 若 Release 解析失败,则回退到仓库根目录 `.laapp`
|
||||
4. 插件详情始终读取插件仓库根目录 `README.md`
|
||||
5. 市场安装为暂存安装,重启后生效
|
||||
|
||||
## English
|
||||
# Host Plugin Runtime
|
||||
|
||||
This directory contains the host-side plugin runtime for LanMountainDesktop.
|
||||
|
||||
### Responsibilities
|
||||
## Responsibilities
|
||||
|
||||
- discover, install, and replace `.laapp` packages
|
||||
- load plugin assemblies and shared contracts
|
||||
- integrate plugin settings pages, desktop components, and market UI
|
||||
- build a plugin-scoped `IServiceCollection` / `ServiceProvider` for API `3.0.0` plugins
|
||||
- resolve shared contract caches before activation and expose explicit plugin exports
|
||||
- Discover, install, replace, and stage `.laapp` plugin packages
|
||||
- Load plugin assemblies and shared contracts
|
||||
- Integrate plugin settings sections, desktop components, and market UI
|
||||
- Build plugin-scoped `IServiceCollection` / `ServiceProvider` for API `4.x` plugins
|
||||
- Resolve shared contracts before activation and expose explicit plugin exports
|
||||
|
||||
### Relationship with LanAirApp
|
||||
## Relationship with LanAirApp
|
||||
|
||||
- `LanAirApp` owns the official market index, developer docs, validation tools, and mirrored sample templates
|
||||
- this directory owns host-side discovery, installation, loading, and UI integration
|
||||
- the authoritative sample plugin lives in the standalone `LanMountainDesktop.SamplePlugin` repository; the `LanAirApp` sample directory is only a mirror/template copy
|
||||
- `LanAirApp` is a standalone repository and owns market metadata plus developer ecosystem materials
|
||||
- This host runtime only consumes market metadata and plugin packages
|
||||
- The host no longer maintains an embedded `LanAirApp/` mirror inside this repository
|
||||
- Workspace debugging resolves market files from sibling path `..\\LanAirApp\\...`
|
||||
|
||||
### Market install order
|
||||
## Market Install Flow
|
||||
|
||||
1. The host reads the official `LanAirApp/airappmarket/index.json`
|
||||
2. If an entry contains both `releaseTag` and `releaseAssetName`, the host first resolves the exact GitHub Release asset
|
||||
3. If Release resolution fails, the host falls back to the repository-root `.laapp`
|
||||
4. Plugin details always come from the plugin repository root `README.md`
|
||||
5. Market installs are staged and take effect after restart
|
||||
1. Host reads the official market index
|
||||
2. If both `releaseTag` and `releaseAssetName` are present, host resolves the exact GitHub Release asset first
|
||||
3. If release resolution fails, host falls back to repository-root `.laapp`
|
||||
4. Plugin detail text is read from plugin repository root `README.md`
|
||||
5. Installation is staged and becomes effective after restart
|
||||
|
||||
91
PRODUCT_BRIEF.md
Normal file
91
PRODUCT_BRIEF.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# 阑山桌面 产品说明
|
||||
|
||||
## 1. 目标人群
|
||||
|
||||
- **学生群体**:大学生、研究生、备考人员
|
||||
- **办公用户**:白领、远程工作者
|
||||
- **效率爱好者**:工具控、桌面美化爱好者
|
||||
|
||||
## 2. 使用场景
|
||||
|
||||
| 场景 | 说明 |
|
||||
|-----|------|
|
||||
| 学习辅助 | 查看课程表、记录自习时长、获取每日诗词单词 |
|
||||
| 办公效率 | 查看日历日程、快速访问最近文档、获取新闻资讯 |
|
||||
| 信息聚合 | 桌面一站式查看天气、日历、热搜、新闻 |
|
||||
| 个性美化 | 自由定制桌面组件布局、主题、壁纸 |
|
||||
|
||||
## 3. 解决方案
|
||||
|
||||
**核心方案**:可编排的桌面组件系统 + 插件扩展生态
|
||||
|
||||
- **20+ 内置组件**:时钟、天气、日历、新闻、课程表、计时器等
|
||||
- **网格化布局**:自由拖拽摆放,支持多页桌面
|
||||
- **主题系统**:日夜模式、Monet 取色、玻璃效果
|
||||
- **插件市场**:支持第三方插件扩展功能
|
||||
- **跨平台**:Windows / Linux / macOS
|
||||
|
||||
## 4. 解决的问题
|
||||
|
||||
| 痛点 | 解决方案 |
|
||||
|-----|---------|
|
||||
| 信息分散,需打开多个应用 | 桌面聚合展示天气、日历、新闻等信息 |
|
||||
| 桌面单调,缺乏个性化 | 丰富的组件和主题自由定制 |
|
||||
| 学习管理不便 | 课程表、自习监测专为学生设计 |
|
||||
| 功能无法满足个性需求 | 插件系统支持无限扩展 |
|
||||
|
||||
## 5. 竞品对比分析
|
||||
|
||||
### 5.1 产品定位差异
|
||||
|
||||
| 产品 | 定位 | 主要场景 | 目标用户 |
|
||||
|-----|------|---------|---------|
|
||||
| **阑山桌面** | 个人桌面信息聚合与效率工具 | 个人学习、办公、信息获取 | 学生、办公人员、个人用户 |
|
||||
| **希沃桌面** | 教室大屏教学系统 | 课堂教学、多媒体展示 | 中小学教师、学校 |
|
||||
| **鸿合鸿U** | 交互式教学系统 | 课堂授课、教学管理 | 教师、教育机构 |
|
||||
| **鸿合 Lesson+** | AI 备授课软件 | 备课、授课、互动、评价 | 教师 |
|
||||
| **Classworks** | 教学资源与课堂管理 | 课堂互动、学情分析 | 教师、学校 |
|
||||
|
||||
### 5.2 功能对比
|
||||
|
||||
| 功能维度 | 阑山桌面 | 希沃/鸿合系列 |
|
||||
|---------|---------|--------------|
|
||||
| **核心功能** | 桌面组件、信息展示、效率工具 | 教学白板、课件展示、课堂互动 |
|
||||
| **组件/工具** | 时钟、天气、日历、新闻、课程表 | 学科工具、白板批注、思维导图 |
|
||||
| **插件扩展** | ✅ 支持第三方插件 | ❌ 封闭系统 |
|
||||
| **跨平台** | ✅ Windows/Linux/macOS | ❌ 主要 Windows |
|
||||
| **硬件依赖** | 无,纯软件 | 需配合交互大屏/白板 |
|
||||
| **AI 功能** | 暂无 | 鸿合 Lesson+ 集成教学大模型 |
|
||||
| **课堂互动** | 不支持 | 多屏互动、学生端连接 |
|
||||
| **教学资源** | 无内置 | 丰富的学科资源库 |
|
||||
| **使用场景** | 个人电脑桌面 | 教室大屏教学 |
|
||||
| **部署方式** | 个人安装 | 学校/机构批量部署 |
|
||||
|
||||
### 5.3 竞争优势
|
||||
|
||||
| 优势 | 说明 |
|
||||
|-----|------|
|
||||
| **个人用户导向** | 专注个人效率,无需专用硬件 |
|
||||
| **开放生态** | 插件系统支持功能无限扩展 |
|
||||
| **跨平台支持** | 支持三大主流操作系统 |
|
||||
| **轻量灵活** | 纯软件方案,部署成本低 |
|
||||
| **隐私保护** | 本地数据存储,不上传个人信息 |
|
||||
|
||||
### 5.4 竞争劣势
|
||||
|
||||
| 劣势 | 说明 |
|
||||
|-----|------|
|
||||
| **非教学专用** | 缺乏专业教学工具和资源 |
|
||||
| **无课堂互动** | 不支持学生端连接和课堂互动 |
|
||||
| **无 AI 功能** | 暂不具备 AI 辅助教学能力 |
|
||||
| **品牌认知** | 教育市场知名度低于希沃/鸿合 |
|
||||
|
||||
## 6. 产品进度
|
||||
|
||||
- **当前版本**:v1.0.0(插件 API 3.0.0)
|
||||
- **开发状态**:核心功能已完成,进入优化迭代阶段
|
||||
- **用户统计**:通过 PostHog 收集匿名数据(具体数据需后台查看)
|
||||
|
||||
---
|
||||
|
||||
**一句话总结**:阑山桌面是一款面向个人用户的可定制桌面工具,与希沃、鸿合等教育大屏系统不同,专注个人学习办公场景,通过组件化设计和插件生态提供轻量、开放、跨平台的桌面信息聚合方案。
|
||||
256
PRODUCT_DOCUMENT.md
Normal file
256
PRODUCT_DOCUMENT.md
Normal file
@@ -0,0 +1,256 @@
|
||||
# 阑山桌面 (LanMountainDesktop) 产品说明文档
|
||||
|
||||
**文档版本**: 1.0
|
||||
**最后更新**: 2026年3月20日
|
||||
**产品版本**: 1.0.0
|
||||
**插件 API 基线**: 3.0.0
|
||||
|
||||
---
|
||||
|
||||
## 一、产品定位
|
||||
|
||||
### 1.1 一句话介绍
|
||||
|
||||
**阑山桌面是一款可编排的桌面信息与交互空间,让用户能够自由定制个性化桌面,整合信息展示与效率工具于一体。**
|
||||
|
||||
### 1.2 核心定位
|
||||
|
||||
- **产品类型**: 跨平台桌面环境增强工具
|
||||
- **技术架构**: 基于 Avalonia UI 的 .NET 跨平台桌面应用
|
||||
- **支持平台**: Windows、Linux、macOS
|
||||
- **开发语言**: C# (.NET 10)
|
||||
|
||||
---
|
||||
|
||||
## 二、目标用户群体
|
||||
|
||||
### 2.1 核心用户画像
|
||||
|
||||
| 用户群体 | 特征描述 | 核心需求 |
|
||||
|---------|---------|---------|
|
||||
| **学生群体** | 大学生、研究生、备考人员 | 课程表管理、自习环境监测、学习计时、每日诗词/单词 |
|
||||
| **办公用户** | 白领、远程工作者、知识工作者 | 日历日程、天气信息、最近文档、资讯获取 |
|
||||
| **效率爱好者** | 工具控、桌面美化爱好者 | 高度自定义、插件扩展、个性化布局 |
|
||||
| **中文用户** | 以中文为母语的用户 | 完整的本地化体验、农历/节假日支持 |
|
||||
|
||||
### 2.2 用户场景分析
|
||||
|
||||
#### 场景一:学生学习桌面
|
||||
> 小张是一名大学生,每天需要查看课程表、记录自习时间、查看天气决定穿衣。阑山桌面的课程表组件帮他管理课表,自习监测组件记录学习时长,天气组件提供实时天气信息,让他在学习时无需切换多个应用。
|
||||
|
||||
#### 场景二:办公效率桌面
|
||||
> 李女士是一名产品经理,需要随时查看日程、关注行业资讯、快速访问最近文档。阑山桌面的日历组件展示日程安排,新闻组件聚合央广网/凤凰网资讯,最近文档组件一键打开工作文件,提升工作效率。
|
||||
|
||||
#### 场景三:个性化展示桌面
|
||||
> 小王是一名桌面美化爱好者,喜欢打造独特的桌面环境。阑山桌面提供丰富的组件库和插件系统,支持自定义布局、主题色、壁纸,让他能够打造独一无二的个性化桌面。
|
||||
|
||||
---
|
||||
|
||||
## 三、使用场景
|
||||
|
||||
### 3.1 主要使用场景
|
||||
|
||||
| 场景 | 描述 | 核心组件 |
|
||||
|-----|------|---------|
|
||||
| **学习辅助** | 课程管理、自习监测、学习计时 | 课程表、自习环境监测、计时器、每日诗词/单词 |
|
||||
| **信息聚合** | 一站式获取天气、新闻、日历信息 | 天气、新闻、日历、热搜 |
|
||||
| **效率提升** | 快速访问文档、应用启动、工具使用 | 最近文档、应用启动台、汇率换算、浏览器 |
|
||||
| **桌面美化** | 个性化桌面布局与视觉呈现 | 时钟、天气、每日名画、主题系统 |
|
||||
| **音乐控制** | 桌面音乐播放控制 | 音乐控制组件 |
|
||||
|
||||
### 3.2 典型使用流程
|
||||
|
||||
```
|
||||
1. 安装阑山桌面 → 2. 选择主题与壁纸 → 3. 添加桌面组件 → 4. 自定义布局
|
||||
↓
|
||||
5. 日常使用(查看信息、使用工具)
|
||||
↓
|
||||
6. 按需安装插件扩展功能
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、解决方案
|
||||
|
||||
### 4.1 产品架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 阑山桌面 (LanMountainDesktop) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 用户界面层 │ 桌面宿主 │ 组件系统 │ 插件系统 │ 设置中心 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 跨平台运行时 (Avalonia UI + .NET 10) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Windows │ Linux │ macOS │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 核心功能模块
|
||||
|
||||
#### 4.2.1 桌面组件系统
|
||||
阑山桌面提供丰富的内置组件,涵盖多个类别:
|
||||
|
||||
| 类别 | 组件列表 |
|
||||
|-----|---------|
|
||||
| **时钟类** | 桌面时钟、世界时钟、天气时钟、模拟时钟 |
|
||||
| **天气类** | 天气组件、小时天气、多日天气、扩展天气 |
|
||||
| **日历类** | 月历、农历、节假日日历 |
|
||||
| **信息类** | 每日诗词、每日名画、每日单词、央广网新闻、凤凰网新闻、B站热搜、百度热搜 |
|
||||
| **学习类** | 课程表、自习环境监测、录音、自习时段控制、历史数据 |
|
||||
| **工具类** | 计时器、汇率换算、浏览器、最近文档、可移动存储、音乐控制 |
|
||||
| **白板类** | 竖向小黑板、横向小黑板 |
|
||||
|
||||
#### 4.2.2 插件扩展系统
|
||||
- **插件 API 基线**: 3.0.0
|
||||
- **插件格式**: `.laapp` 插件包
|
||||
- **插件市场**: 官方插件市场 (LanAirApp)
|
||||
- **开发支持**: 完整的 PluginSdk 和开发文档
|
||||
|
||||
#### 4.2.3 主题与个性化
|
||||
- **主题系统**: 支持日夜模式切换
|
||||
- **主题色**: 支持 Monet 取色和自定义主题色
|
||||
- **玻璃效果**: 多层级玻璃视觉效果
|
||||
- **壁纸系统**: 支持图片壁纸和动态效果
|
||||
|
||||
#### 4.2.4 布局系统
|
||||
- **网格化布局**: 支持多页桌面
|
||||
- **自由拖拽**: 组件可自由摆放
|
||||
- **尺寸自适应**: 组件支持多种尺寸规格
|
||||
|
||||
### 4.3 技术亮点
|
||||
|
||||
| 特性 | 说明 |
|
||||
|-----|------|
|
||||
| **跨平台** | 基于 Avalonia UI,支持 Windows/Linux/macOS |
|
||||
| **现代化 UI** | Fluent Design + Material Design 融合 |
|
||||
| **插件化架构** | 支持第三方插件扩展,API 基线 3.0.0 |
|
||||
| **数据安全** | 本地 SQLite 存储,隐私数据不上传 |
|
||||
| **性能优化** | 组件懒加载、资源按需加载 |
|
||||
| **无障碍支持** | 对比度优化、语义化界面 |
|
||||
|
||||
---
|
||||
|
||||
## 五、解决的问题
|
||||
|
||||
### 5.1 用户痛点
|
||||
|
||||
| 痛点 | 阑山桌面解决方案 |
|
||||
|-----|----------------|
|
||||
| **信息分散** | 整合天气、日历、新闻等信息于桌面 |
|
||||
| **桌面单调** | 丰富的组件和主题让桌面个性化 |
|
||||
| **效率低下** | 常用工具和信息一触即达 |
|
||||
| **学习管理难** | 课程表、自习监测专为学生设计 |
|
||||
| **功能不足** | 插件系统支持无限扩展 |
|
||||
|
||||
### 5.2 竞品差异化
|
||||
|
||||
| 对比维度 | 传统桌面工具 | 阑山桌面 |
|
||||
|---------|-------------|---------|
|
||||
| **组件丰富度** | 有限组件 | 20+ 内置组件 + 插件扩展 |
|
||||
| **定制化** | 固定布局 | 自由拖拽、网格化布局 |
|
||||
| **跨平台** | 单一平台 | Windows/Linux/macOS |
|
||||
| **插件生态** | 不支持 | 完整插件 SDK 和市场 |
|
||||
| **本地化** | 一般 | 完整中文本地化 |
|
||||
|
||||
---
|
||||
|
||||
## 六、用户量与数据统计
|
||||
|
||||
### 6.1 数据收集说明
|
||||
|
||||
根据隐私政策,阑山桌面收集以下匿名数据用于统计:
|
||||
|
||||
- ✅ **应用启动事件**: 用于统计日活跃用户
|
||||
- ✅ **设备标识符**: 匿名生成,用于区分用户(不含个人信息)
|
||||
- ✅ **应用版本**: 用于统计版本分布
|
||||
- ✅ **崩溃报告**: 用于提升应用稳定性(可选)
|
||||
- ✅ **使用统计**: 用于功能优化(可选)
|
||||
|
||||
### 6.2 隐私承诺
|
||||
|
||||
- ❌ 不收集个人身份信息(姓名、邮箱、电话等)
|
||||
- ❌ 不收集地理位置
|
||||
- ❌ 不收集文件内容
|
||||
- ❌ 不出售用户数据
|
||||
- ❌ 不用于广告目的
|
||||
|
||||
### 6.3 当前状态
|
||||
|
||||
**当前版本**: 1.0.0
|
||||
**插件 API 基线**: 3.0.0
|
||||
**数据收集服务**: PostHog(用户分析)、Sentry(崩溃报告)
|
||||
|
||||
> **注**: 具体用户量数据需从 PostHog 后台获取,此处未展示具体数字。
|
||||
|
||||
---
|
||||
|
||||
## 七、产品开发进度
|
||||
|
||||
### 7.1 当前开发状态
|
||||
|
||||
| 模块 | 状态 | 说明 |
|
||||
|-----|------|------|
|
||||
| **核心桌面功能** | ✅ 已完成 | 网格布局、组件系统、主题系统 |
|
||||
| **内置组件** | ✅ 已完成 | 20+ 组件已上线 |
|
||||
| **插件系统** | ✅ 已完成 | API 3.0.0 已稳定 |
|
||||
| **插件市场** | ✅ 已完成 | 官方市场已运营 |
|
||||
| **多平台支持** | ✅ 已完成 | Windows/Linux/macOS |
|
||||
| **自动更新** | ✅ 已完成 | 内置更新系统 |
|
||||
| **应用启动台** | ✅ 已完成 | Windows 开始菜单集成 |
|
||||
|
||||
### 7.2 版本里程碑
|
||||
|
||||
| 版本 | 目标 | 状态 |
|
||||
|-----|------|------|
|
||||
| v1.0.0 | 核心功能完整、插件系统稳定 | ✅ 已发布 |
|
||||
| v1.x.x | 组件扩展、性能优化 | 🔄 进行中 |
|
||||
| v2.0.0 | 重大功能升级(规划中) | 📋 规划中 |
|
||||
|
||||
### 7.3 近期开发计划
|
||||
|
||||
根据 `.trae/specs` 中的规格文档,近期开发任务包括:
|
||||
|
||||
1. **设置页面 Fluent 重设计** - 提升设置界面体验
|
||||
2. **课程表功能增强** - 增加更多课程管理功能
|
||||
3. **视频壁纸功能移除** - 优化产品定位
|
||||
|
||||
### 7.4 生态建设
|
||||
|
||||
| 项目 | 状态 | 说明 |
|
||||
|-----|------|------|
|
||||
| **LanMountainDesktop** | ✅ 主仓库 | 桌面宿主、插件运行时 |
|
||||
| **LanAirApp** | ✅ 独立仓库 | 插件市场、开发文档 |
|
||||
| **SamplePlugin** | ✅ 独立仓库 | 权威示例插件 |
|
||||
| **PluginSdk** | ✅ 已发布 | 插件开发 SDK |
|
||||
|
||||
---
|
||||
|
||||
## 八、产品优势总结
|
||||
|
||||
### 8.1 核心价值
|
||||
|
||||
1. **个性化桌面**: 自由定制组件布局,打造专属桌面空间
|
||||
2. **信息聚合**: 一站式获取天气、日历、新闻等实用信息
|
||||
3. **效率提升**: 常用工具触手可及,减少应用切换
|
||||
4. **学习辅助**: 专为学生群体设计的课程表、自习监测功能
|
||||
5. **无限扩展**: 插件系统支持功能无限扩展
|
||||
|
||||
### 8.2 技术保障
|
||||
|
||||
- 跨平台架构,一次开发多端运行
|
||||
- 现代化 UI 框架,流畅的用户体验
|
||||
- 严格的隐私保护,数据安全有保障
|
||||
- 完善的插件生态,功能持续扩展
|
||||
|
||||
---
|
||||
|
||||
## 九、联系我们
|
||||
|
||||
- **GitHub**: https://github.com/wwiinnddyy/LanMountainDesktop
|
||||
- **Issues**: https://github.com/wwiinnddyy/LanMountainDesktop/issues
|
||||
- **插件市场**: LanAirApp 官方市场
|
||||
|
||||
---
|
||||
|
||||
**阑山桌面,让你的桌面更有温度。**
|
||||
64
README.md
64
README.md
@@ -1,47 +1,45 @@
|
||||
# 阑山桌面 / LanMountainDesktop
|
||||
# LanMountainDesktop
|
||||
|
||||
## 中文
|
||||
`LanMountainDesktop` is the authoritative host repository for the desktop app and the host-side Plugin SDK.
|
||||
|
||||
`LanMontainDesktop` 是阑山桌面的宿主应用权威仓库,负责应用本体、宿主侧插件运行时,以及宿主侧 `PluginSdk` API 基线。
|
||||
## Repository Ownership
|
||||
|
||||
### 本仓库负责什么
|
||||
This repository owns:
|
||||
|
||||
- `LanMountainDesktop/`:桌面宿主应用
|
||||
- `LanMountainDesktop.PluginSdk/`:宿主侧插件 API 真源
|
||||
- `LanMountainDesktop/plugins/`:插件发现、安装、加载、市场接入
|
||||
- `LanMountainDesktop.Tests/`:宿主与插件运行时测试
|
||||
- `LanAirApp/`:仅用于联调的镜像副本,权威版本仍以独立 `LanAirApp` 仓库为准
|
||||
- `LanMountainDesktop/`: desktop host app and plugin runtime
|
||||
- `LanMountainDesktop.PluginSdk/`: canonical plugin API baseline (`4.0.0`)
|
||||
- `LanMountainDesktop.Shared.Contracts/`: shared host/plugin contract types
|
||||
- `LanMountainDesktop.Appearance/`: host appearance and radius token generation
|
||||
- `LanMountainDesktop.Settings.Core/`: host settings primitives
|
||||
- `LanMountainDesktop.Tests/`: host and SDK tests
|
||||
|
||||
### 生态边界
|
||||
This repository does not own:
|
||||
|
||||
- 应用本体:`LanMontainDesktop`
|
||||
- 插件市场与开发资料:独立 `LanAirApp`
|
||||
- 权威示例插件:独立 `LanMountainDesktop.SamplePlugin`
|
||||
- plugin market metadata or developer portal content
|
||||
- official sample plugin release source
|
||||
- independent ecosystem documentation hub
|
||||
|
||||
### 当前插件 API 基线
|
||||
## Ecosystem Boundaries
|
||||
|
||||
- 宿主插件 API 基线:`3.0.0`
|
||||
- `SampleClock` 共享契约:`2.0.0`
|
||||
- Host and SDK source of truth: `LanMountainDesktop` (this repo)
|
||||
- Plugin market and developer materials: standalone `LanAirApp` repo
|
||||
- Official sample plugin source of truth: standalone `LanMountainDesktop.SamplePlugin` repo
|
||||
- `ClassIsland`: reference-only project, not part of build or release flow
|
||||
|
||||
## English
|
||||
## Plugin SDK v4 Baseline
|
||||
|
||||
`LanMontainDesktop` is the authoritative host repository for LanMountainDesktop. It owns the desktop application, the host-side plugin runtime, and the host-side `PluginSdk` API baseline.
|
||||
- API baseline: `4.0.0`
|
||||
- Manifest file: `plugin.json`
|
||||
- Package extension: `.laapp`
|
||||
- Entry model: `Initialize(HostBuilderContext, IServiceCollection)`
|
||||
- Appearance model: `IPluginAppearanceContext`, `PluginAppearanceSnapshot`, `PluginCornerRadiusTokens`, `PluginCornerRadiusPreset`
|
||||
- Component registration model: `AddPluginDesktopComponent<TControl>(PluginDesktopComponentOptions options)`
|
||||
|
||||
### What this repository owns
|
||||
## Workspace Market Resolution
|
||||
|
||||
- `LanMountainDesktop/`: the desktop host application
|
||||
- `LanMountainDesktop.PluginSdk/`: the canonical host-side plugin API
|
||||
- `LanMountainDesktop/plugins/`: plugin discovery, installation, loading, and market integration
|
||||
- `LanMountainDesktop.Tests/`: host and plugin runtime tests
|
||||
- `LanAirApp/`: a mirror kept for local workspace integration only; the standalone `LanAirApp` repository remains the source of truth
|
||||
For local market debugging, the host resolves workspace files from the sibling repository path (`..\\LanAirApp`) instead of reading the in-repo mirror folder.
|
||||
|
||||
### Ecosystem boundaries
|
||||
See:
|
||||
|
||||
- Application host: `LanMontainDesktop`
|
||||
- Plugin market and developer-facing materials: standalone `LanAirApp`
|
||||
- Authoritative sample plugin: standalone `LanMountainDesktop.SamplePlugin`
|
||||
|
||||
### Current plugin API baseline
|
||||
|
||||
- Host plugin API baseline: `3.0.0`
|
||||
- `SampleClock` shared contract: `2.0.0`
|
||||
- `docs/ECOSYSTEM_BOUNDARIES.md`
|
||||
- `docs/PLUGIN_SDK_V4_MIGRATION.md`
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# AirApp Market 目录说明
|
||||
|
||||
## 中文
|
||||
|
||||
这个目录是阑山桌面仓库里遗留的市场原型目录,只用于历史参考,不再作为官方权威市场源。
|
||||
|
||||
### 当前结论
|
||||
|
||||
- 官方市场源以独立 `LanAirApp` 仓库中的 `airappmarket/index.json` 为准
|
||||
- 阑山桌面程序应连接 `LanAirApp` 仓库,而不是以本目录为权威数据源
|
||||
- 如无特殊需要,不应继续向这里添加正式市场数据
|
||||
|
||||
## English
|
||||
|
||||
This directory is a legacy market prototype kept in the LanMountainDesktop repository for historical reference only.
|
||||
|
||||
The authoritative market source now lives in the standalone `LanAirApp` repository.
|
||||
@@ -1,10 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="Sample Plugin">
|
||||
<defs>
|
||||
<linearGradient id="sampleBg" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0%" stop-color="#F59E0B"/>
|
||||
<stop offset="100%" stop-color="#EF4444"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="8" y="8" width="112" height="112" rx="28" fill="url(#sampleBg)"/>
|
||||
<path d="M52 32c0-6.627 5.373-12 12-12s12 5.373 12 12v8h8c6.627 0 12 5.373 12 12s-5.373 12-12 12h-8v32c0 6.627-5.373 12-12 12s-12-5.373-12-12V64h-8c-6.627 0-12-5.373-12-12s5.373-12 12-12h8v-8Z" fill="#FFFFFF" fill-opacity="0.92"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 618 B |
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"sourceId": "official.lanmountaindesktop",
|
||||
"sourceName": "LanMountainDesktop Official Market",
|
||||
"generatedAt": "2026-03-10T11:10:00Z",
|
||||
"plugins": [
|
||||
{
|
||||
"id": "LanMountainDesktop.SamplePlugin",
|
||||
"name": "LanMountain Sample Plugin",
|
||||
"description": "Example plugin used to validate PluginSdk loading and isolation.",
|
||||
"author": "LanMountainDesktop",
|
||||
"version": "1.0.0",
|
||||
"apiVersion": "1.0.0",
|
||||
"minHostVersion": "1.0.0",
|
||||
"downloadUrl": "https://raw.githubusercontent.com/wwiinnddyy/LanMountainDesktop/main/LanAirApp/releases/LanMountainDesktop.SamplePlugin.1.0.0.laapp",
|
||||
"sha256": "c092f9d215ee0f1e436bc49b919dd9a75b3838e950c72c46dd7e41807557125c",
|
||||
"packageSizeBytes": 1703398,
|
||||
"iconUrl": "https://raw.githubusercontent.com/wwiinnddyy/LanMountainDesktop/main/airappmarket/assets/sample-plugin.svg",
|
||||
"homepageUrl": "https://github.com/wwiinnddyy/LanMountainDesktop/tree/main/LanAirApp/samples/LanMountainDesktop.SamplePlugin",
|
||||
"repositoryUrl": "https://github.com/wwiinnddyy/LanMountainDesktop/tree/main/LanAirApp/samples/LanMountainDesktop.SamplePlugin",
|
||||
"tags": [
|
||||
"example",
|
||||
"official",
|
||||
"sdk"
|
||||
],
|
||||
"publishedAt": "2026-03-10T01:30:00Z",
|
||||
"updatedAt": "2026-03-10T01:30:00Z",
|
||||
"releaseNotes": "Reference plugin for SDK validation. Includes a settings page, a desktop widget, localization resources, service registration, and plugin message bus usage."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://raw.githubusercontent.com/wwiinnddyy/LanMountainDesktop/main/airappmarket/schema/airappmarket-index.schema.json",
|
||||
"title": "AirAppMarket Index",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"sourceId",
|
||||
"sourceName",
|
||||
"generatedAt",
|
||||
"plugins"
|
||||
],
|
||||
"properties": {
|
||||
"schemaVersion": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
|
||||
},
|
||||
"sourceId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"sourceName": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"generatedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"plugins": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/plugin"
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"plugin": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"description",
|
||||
"author",
|
||||
"version",
|
||||
"apiVersion",
|
||||
"minHostVersion",
|
||||
"downloadUrl",
|
||||
"sha256",
|
||||
"packageSizeBytes",
|
||||
"iconUrl",
|
||||
"homepageUrl",
|
||||
"repositoryUrl",
|
||||
"tags",
|
||||
"publishedAt",
|
||||
"updatedAt",
|
||||
"releaseNotes"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"author": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:[-+ ][A-Za-z0-9.-]+)?$"
|
||||
},
|
||||
"apiVersion": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:[-+ ][A-Za-z0-9.-]+)?$"
|
||||
},
|
||||
"minHostVersion": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:[-+ ][A-Za-z0-9.-]+)?$"
|
||||
},
|
||||
"downloadUrl": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-fA-F0-9]{64}$"
|
||||
},
|
||||
"packageSizeBytes": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"iconUrl": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"homepageUrl": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"repositoryUrl": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"publishedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"releaseNotes": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>1.0.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,247 +0,0 @@
|
||||
using System.Text.Json;
|
||||
|
||||
return await RunAsync(args);
|
||||
|
||||
static Task<int> RunAsync(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var indexPath = args.Length > 0
|
||||
? Path.GetFullPath(args[0])
|
||||
: Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "index.json"));
|
||||
var schemaPath = args.Length > 1
|
||||
? Path.GetFullPath(args[1])
|
||||
: Path.GetFullPath(Path.Combine(Path.GetDirectoryName(indexPath)!, "schema", "airappmarket-index.schema.json"));
|
||||
|
||||
if (!File.Exists(indexPath))
|
||||
{
|
||||
throw new FileNotFoundException($"Market index '{indexPath}' was not found.", indexPath);
|
||||
}
|
||||
|
||||
if (!File.Exists(schemaPath))
|
||||
{
|
||||
throw new FileNotFoundException($"Market schema '{schemaPath}' was not found.", schemaPath);
|
||||
}
|
||||
|
||||
JsonDocument.Parse(File.ReadAllText(schemaPath));
|
||||
var document = MarketIndex.Load(File.ReadAllText(indexPath), indexPath);
|
||||
|
||||
Console.WriteLine($"Validated '{indexPath}'.");
|
||||
Console.WriteLine($"Source: {document.SourceName} ({document.SourceId})");
|
||||
Console.WriteLine($"Plugins: {document.Plugins.Count}");
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine(ex.Message);
|
||||
return Task.FromResult(1);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MarketIndex
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true
|
||||
};
|
||||
|
||||
public string SchemaVersion { get; init; } = string.Empty;
|
||||
public string SourceId { get; init; } = string.Empty;
|
||||
public string SourceName { get; init; } = string.Empty;
|
||||
public DateTimeOffset GeneratedAt { get; init; }
|
||||
public List<MarketPlugin> Plugins { get; init; } = [];
|
||||
|
||||
public static MarketIndex Load(string json, string sourceName)
|
||||
{
|
||||
var document = JsonSerializer.Deserialize<MarketIndex>(
|
||||
json.TrimStart('\uFEFF'),
|
||||
SerializerOptions) ?? throw new InvalidOperationException($"Failed to parse market index '{sourceName}'.");
|
||||
|
||||
return document.ValidateAndNormalize(sourceName);
|
||||
}
|
||||
|
||||
private MarketIndex ValidateAndNormalize(string sourceName)
|
||||
{
|
||||
var normalizedPlugins = new List<MarketPlugin>(Plugins.Count);
|
||||
var seenIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var plugin in Plugins)
|
||||
{
|
||||
var normalizedPlugin = plugin.ValidateAndNormalize(sourceName);
|
||||
if (!seenIds.Add(normalizedPlugin.Id))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' contains duplicate plugin id '{normalizedPlugin.Id}'.");
|
||||
}
|
||||
|
||||
normalizedPlugins.Add(normalizedPlugin);
|
||||
}
|
||||
|
||||
return new MarketIndex
|
||||
{
|
||||
SchemaVersion = RequireValue(SchemaVersion, nameof(SchemaVersion), sourceName),
|
||||
SourceId = RequireValue(SourceId, nameof(SourceId), sourceName),
|
||||
SourceName = RequireValue(SourceName, nameof(SourceName), sourceName),
|
||||
GeneratedAt = GeneratedAt == default
|
||||
? throw new InvalidOperationException($"Market index '{sourceName}' is missing a valid generatedAt timestamp.")
|
||||
: GeneratedAt,
|
||||
Plugins = normalizedPlugins
|
||||
};
|
||||
}
|
||||
|
||||
internal static string RequireValue(string? value, string propertyName, string sourceName)
|
||||
{
|
||||
var normalized = NormalizeValue(value);
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
throw new InvalidOperationException($"Market index '{sourceName}' is missing required property '{propertyName}'.");
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
internal static string? NormalizeValue(string? value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
|
||||
internal static string NormalizeVersion(string? value, string propertyName, string sourceName)
|
||||
{
|
||||
var normalized = RequireValue(value, propertyName, sourceName);
|
||||
if (!TryParseVersion(normalized, out _))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' declares invalid version '{normalized}' for '{propertyName}'.");
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
internal static bool TryParseVersion(string? value, out Version? version)
|
||||
{
|
||||
version = null;
|
||||
var normalized = NormalizeValue(value);
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (normalized.StartsWith("v", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
normalized = normalized[1..];
|
||||
}
|
||||
|
||||
var separatorIndex = normalized.IndexOfAny(['-', '+', ' ']);
|
||||
if (separatorIndex > 0)
|
||||
{
|
||||
normalized = normalized[..separatorIndex];
|
||||
}
|
||||
|
||||
if (!Version.TryParse(normalized, out var parsed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
version = new Version(
|
||||
Math.Max(0, parsed.Major),
|
||||
Math.Max(0, parsed.Minor),
|
||||
Math.Max(0, parsed.Build));
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void EnsureUrl(string? value, string propertyName, string sourceName)
|
||||
{
|
||||
var normalized = RequireValue(value, propertyName, sourceName);
|
||||
if (!Uri.TryCreate(normalized, UriKind.Absolute, out var uri) ||
|
||||
(uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' declares invalid URL '{normalized}' for '{propertyName}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MarketPlugin
|
||||
{
|
||||
public string Id { get; init; } = string.Empty;
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public string Author { get; init; } = string.Empty;
|
||||
public string Version { get; init; } = string.Empty;
|
||||
public string ApiVersion { get; init; } = string.Empty;
|
||||
public string MinHostVersion { get; init; } = string.Empty;
|
||||
public string DownloadUrl { get; init; } = string.Empty;
|
||||
public string Sha256 { get; init; } = string.Empty;
|
||||
public long PackageSizeBytes { get; init; }
|
||||
public string IconUrl { get; init; } = string.Empty;
|
||||
public string HomepageUrl { get; init; } = string.Empty;
|
||||
public string RepositoryUrl { get; init; } = string.Empty;
|
||||
public List<string> Tags { get; init; } = [];
|
||||
public DateTimeOffset PublishedAt { get; init; }
|
||||
public DateTimeOffset UpdatedAt { get; init; }
|
||||
public string ReleaseNotes { get; init; } = string.Empty;
|
||||
|
||||
public MarketPlugin ValidateAndNormalize(string sourceName)
|
||||
{
|
||||
var tagSource = Tags ?? [];
|
||||
var normalizedTags = tagSource
|
||||
.Select(MarketIndex.NormalizeValue)
|
||||
.Where(tag => !string.IsNullOrWhiteSpace(tag))
|
||||
.Select(tag => tag!)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (normalizedTags.Count != tagSource.Count(tag => !string.IsNullOrWhiteSpace(tag)))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' contains duplicate or blank tags for plugin '{Id}'.");
|
||||
}
|
||||
|
||||
var normalizedSha = MarketIndex.RequireValue(Sha256, nameof(Sha256), sourceName).ToLowerInvariant();
|
||||
if (normalizedSha.Length != 64 || normalizedSha.Any(ch => !Uri.IsHexDigit(ch)))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' declares invalid SHA-256 '{normalizedSha}' for plugin '{Id}'.");
|
||||
}
|
||||
|
||||
MarketIndex.EnsureUrl(DownloadUrl, nameof(DownloadUrl), sourceName);
|
||||
MarketIndex.EnsureUrl(IconUrl, nameof(IconUrl), sourceName);
|
||||
MarketIndex.EnsureUrl(HomepageUrl, nameof(HomepageUrl), sourceName);
|
||||
MarketIndex.EnsureUrl(RepositoryUrl, nameof(RepositoryUrl), sourceName);
|
||||
|
||||
if (PackageSizeBytes <= 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' declares invalid packageSizeBytes '{PackageSizeBytes}' for plugin '{Id}'.");
|
||||
}
|
||||
|
||||
if (PublishedAt == default || UpdatedAt == default)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Market index '{sourceName}' is missing valid publish timestamps for plugin '{Id}'.");
|
||||
}
|
||||
|
||||
return new MarketPlugin
|
||||
{
|
||||
Id = MarketIndex.RequireValue(Id, nameof(Id), sourceName),
|
||||
Name = MarketIndex.RequireValue(Name, nameof(Name), sourceName),
|
||||
Description = MarketIndex.RequireValue(Description, nameof(Description), sourceName),
|
||||
Author = MarketIndex.RequireValue(Author, nameof(Author), sourceName),
|
||||
Version = MarketIndex.NormalizeVersion(Version, nameof(Version), sourceName),
|
||||
ApiVersion = MarketIndex.NormalizeVersion(ApiVersion, nameof(ApiVersion), sourceName),
|
||||
MinHostVersion = MarketIndex.NormalizeVersion(MinHostVersion, nameof(MinHostVersion), sourceName),
|
||||
DownloadUrl = MarketIndex.RequireValue(DownloadUrl, nameof(DownloadUrl), sourceName),
|
||||
Sha256 = normalizedSha,
|
||||
PackageSizeBytes = PackageSizeBytes,
|
||||
IconUrl = MarketIndex.RequireValue(IconUrl, nameof(IconUrl), sourceName),
|
||||
HomepageUrl = MarketIndex.RequireValue(HomepageUrl, nameof(HomepageUrl), sourceName),
|
||||
RepositoryUrl = MarketIndex.RequireValue(RepositoryUrl, nameof(RepositoryUrl), sourceName),
|
||||
Tags = normalizedTags,
|
||||
PublishedAt = PublishedAt,
|
||||
UpdatedAt = UpdatedAt,
|
||||
ReleaseNotes = MarketIndex.RequireValue(ReleaseNotes, nameof(ReleaseNotes), sourceName)
|
||||
};
|
||||
}
|
||||
}
|
||||
34
docs/ECOSYSTEM_BOUNDARIES.md
Normal file
34
docs/ECOSYSTEM_BOUNDARIES.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Ecosystem Boundaries
|
||||
|
||||
This document defines ownership boundaries for the LanMountainDesktop plugin ecosystem.
|
||||
|
||||
## Source of Truth
|
||||
|
||||
- Host runtime and plugin loading: `LanMountainDesktop`
|
||||
- Plugin SDK API baseline: `LanMountainDesktop`
|
||||
- Shared contracts used by host and plugins: `LanMountainDesktop`
|
||||
- Plugin market index and ecosystem metadata: `LanAirApp`
|
||||
- Official sample plugin implementation and release artifacts: `LanMountainDesktop.SamplePlugin`
|
||||
|
||||
## What Stays in This Repository
|
||||
|
||||
- Host runtime code and desktop shell behavior
|
||||
- Plugin runtime, loader, install coordination, and host integration
|
||||
- Plugin SDK public interfaces, contracts, and registration helpers
|
||||
- Host appearance and settings infrastructure
|
||||
- Tests that validate host + SDK behavior
|
||||
|
||||
## What Should Not Be Maintained Here as Authoritative
|
||||
|
||||
- Market documentation as a canonical developer portal
|
||||
- Market publishing metadata as canonical source
|
||||
- Official sample plugin source and release pipeline
|
||||
- External reference projects (for example ClassIsland) as dependencies
|
||||
|
||||
## Local Debugging Rule
|
||||
|
||||
When running a workspace build, plugin market index and related market assets must be resolved from the sibling repository path:
|
||||
|
||||
- `..\\LanAirApp\\airappmarket\\index.json`
|
||||
|
||||
The host should not depend on an embedded `LanAirApp` mirror inside this repository for workspace market resolution.
|
||||
62
docs/PLUGIN_SDK_V4_MIGRATION.md
Normal file
62
docs/PLUGIN_SDK_V4_MIGRATION.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Plugin SDK v4 Migration Guide
|
||||
|
||||
This guide describes the breaking changes introduced by Plugin SDK `4.0.0`.
|
||||
|
||||
## Version Baseline
|
||||
|
||||
- Host plugin SDK baseline: `4.0.0`
|
||||
- Plugins targeting `3.x` are rejected by default
|
||||
- Manifest file remains `plugin.json`
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
1. `AddPluginDesktopComponent` now uses options-first registration.
|
||||
2. `PluginDesktopComponentOptions` is now the canonical component registration shape and must include `ComponentId`.
|
||||
3. Appearance and radius access are provided through strongly typed APIs:
|
||||
- `IPluginAppearanceContext`
|
||||
- `PluginAppearanceSnapshot`
|
||||
- `PluginCornerRadiusTokens`
|
||||
- `PluginCornerRadiusPreset`
|
||||
4. `PluginDesktopComponentContext` now exposes `Appearance` as the primary appearance access point.
|
||||
|
||||
## New Component Registration Pattern
|
||||
|
||||
```csharp
|
||||
services.AddPluginDesktopComponent<MyWidget>(new PluginDesktopComponentOptions
|
||||
{
|
||||
ComponentId = "YourPlugin.Widget",
|
||||
DisplayName = "My Widget",
|
||||
IconKey = "PuzzlePiece",
|
||||
Category = "Plugins",
|
||||
MinWidthCells = 4,
|
||||
MinHeightCells = 3,
|
||||
CornerRadiusPreset = PluginCornerRadiusPreset.Default
|
||||
});
|
||||
```
|
||||
|
||||
## Appearance Usage Pattern
|
||||
|
||||
```csharp
|
||||
public MyWidget(PluginDesktopComponentContext context)
|
||||
{
|
||||
var mdRadius = context.Appearance.ResolveCornerRadius(PluginCornerRadiusPreset.Md);
|
||||
var adaptiveRadius = context.Appearance.ResolveScaledCornerRadius(12, 8, 20);
|
||||
}
|
||||
```
|
||||
|
||||
## Manifest Update
|
||||
|
||||
Update plugin manifests to API `4.x`:
|
||||
|
||||
```json
|
||||
{
|
||||
"apiVersion": "4.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
- `plugin.json` declares `apiVersion` `4.0.0` (or compatible `4.x`)
|
||||
- component registration migrated to options model
|
||||
- runtime appearance access uses `IPluginAppearanceContext`
|
||||
- plugin package rebuilt and republished as `.laapp`
|
||||
Reference in New Issue
Block a user