diff --git a/.github/workflows/airappmarket-validate.yml b/.github/workflows/airappmarket-validate.yml
deleted file mode 100644
index 2451843..0000000
--- a/.github/workflows/airappmarket-validate.yml
+++ /dev/null
@@ -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
diff --git a/LanAirApp/README.md b/LanAirApp/README.md
deleted file mode 100644
index b79d19c..0000000
--- a/LanAirApp/README.md
+++ /dev/null
@@ -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
diff --git a/LanAirApp/docs/PLUGIN_DEVELOPMENT.md b/LanAirApp/docs/PLUGIN_DEVELOPMENT.md
deleted file mode 100644
index 9a3ee4e..0000000
--- a/LanAirApp/docs/PLUGIN_DEVELOPMENT.md
+++ /dev/null
@@ -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.
diff --git a/LanAirApp/docs/PLUGIN_PACKAGING.md b/LanAirApp/docs/PLUGIN_PACKAGING.md
deleted file mode 100644
index d99e132..0000000
--- a/LanAirApp/docs/PLUGIN_PACKAGING.md
+++ /dev/null
@@ -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.
diff --git a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/LanMountainDesktop.SamplePlugin.csproj b/LanAirApp/samples/LanMountainDesktop.SamplePlugin/LanMountainDesktop.SamplePlugin.csproj
deleted file mode 100644
index f7cc095..0000000
--- a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/LanMountainDesktop.SamplePlugin.csproj
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
- net10.0
- enable
- enable
- 1.0.0
- true
- bin\$(Configuration)\$(TargetFramework)\content\
- false
- false
- $(MSBuildThisFileDirectory)artifacts\Packages\
- $(PluginPackageOutputDirectory)$(AssemblyName).$(Version).laapp
- $(MSBuildThisFileDirectory)artifacts\Loose\
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/Localization/en-US.json b/LanAirApp/samples/LanMountainDesktop.SamplePlugin/Localization/en-US.json
deleted file mode 100644
index 09f91b9..0000000
--- a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/Localization/en-US.json
+++ /dev/null
@@ -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()",
- "capability.get_service.detail": "Callable. State service resolved: {0}; clock service resolved: {1}; message bus resolved: {2}.",
- "capability.register_service.title": "IPluginContext.RegisterService()",
- "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() 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"
-}
diff --git a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/Localization/zh-CN.json b/LanAirApp/samples/LanMountainDesktop.SamplePlugin/Localization/zh-CN.json
deleted file mode 100644
index 82d3796..0000000
--- a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/Localization/zh-CN.json
+++ /dev/null
@@ -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()",
- "capability.get_service.detail": "可调用。状态服务已解析:{0};时钟服务已解析:{1};消息总线已解析:{2}。",
- "capability.register_service.title": "IPluginContext.RegisterService()",
- "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()。",
- "widget.subtitle.preview": "预览界面 | 已放置:{0}",
- "widget.subtitle.placement": "位置 {0} | 已放置:{1}",
- "common.dev": "开发版",
- "common.none": "(无)",
- "common.unknown": "(未知)",
- "common.true": "是",
- "common.false": "否",
- "common.yes": "是",
- "common.no": "否"
-}
diff --git a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/README.md b/LanAirApp/samples/LanMountainDesktop.SamplePlugin/README.md
deleted file mode 100644
index 1dd7ce5..0000000
--- a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/README.md
+++ /dev/null
@@ -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.
diff --git a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePlugin.cs b/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePlugin.cs
deleted file mode 100644
index 58e11f0..0000000
--- a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePlugin.cs
+++ /dev/null
@@ -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()
- ?? 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(key, out var value) && !string.IsNullOrWhiteSpace(value)
- ? value
- : fallback;
- }
-}
diff --git a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePluginCloseDesktopWidget.cs b/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePluginCloseDesktopWidget.cs
deleted file mode 100644
index a297025..0000000
--- a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePluginCloseDesktopWidget.cs
+++ /dev/null
@@ -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();
-
- _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);
- }
-}
diff --git a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePluginRuntimeStatus.cs b/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePluginRuntimeStatus.cs
deleted file mode 100644
index 29a82d3..0000000
--- a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePluginRuntimeStatus.cs
+++ /dev/null
@@ -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 StatusEntries,
- bool HasPlacedComponent,
- int PlacedCount,
- int PreviewCount,
- IReadOnlyList 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 _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 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()"),
- Tf(
- "capability.get_service.detail",
- "可调用。状态服务已解析:{0};时钟服务已解析:{1};消息总线已解析:{2}。",
- FormatBoolean(hasStateService),
- FormatBoolean(hasClockService),
- FormatBoolean(hasMessageBus))),
- new SamplePluginCapabilityItem(
- T("capability.register_service.title", "IPluginContext.RegisterService()"),
- 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()。"))
- ];
- }
-
- 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));
- }
- }
-}
diff --git a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePluginSettingsView.cs b/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePluginSettingsView.cs
deleted file mode 100644
index c1450e4..0000000
--- a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePluginSettingsView.cs
+++ /dev/null
@@ -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 _subscriptions = [];
-
- public SamplePluginSettingsView(IPluginContext context)
- {
- _context = context;
- _localizer = PluginLocalizer.Create(context);
- _stateService = context.GetService()
- ?? throw new InvalidOperationException("SamplePluginRuntimeStateService is not available.");
- _clockService = context.GetService()
- ?? throw new InvalidOperationException("SamplePluginClockService is not available.");
- _messageBus = context.GetService()
- ?? 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(_ =>
- Dispatcher.UIThread.Post(RefreshView)));
-
- _subscriptions.Add(_messageBus.Subscribe(_ =>
- 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() is not null)));
- _pluginInfoPanel.Children.Add(CreateInfoLine(
- T("settings.info.clock_service_resolved", "时钟服务已解析"),
- FormatBoolean(_context.GetService() is not null)));
- _pluginInfoPanel.Children.Add(CreateInfoLine(
- T("settings.info.message_bus_resolved", "消息总线已解析"),
- FormatBoolean(_context.GetService() 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() is not null,
- _context.GetService() is not null,
- _context.GetService() 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", "否");
- }
-}
diff --git a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePluginStatusClockWidget.cs b/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePluginStatusClockWidget.cs
deleted file mode 100644
index 5793fda..0000000
--- a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePluginStatusClockWidget.cs
+++ /dev/null
@@ -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 _subscriptions = [];
- private string? _instanceId;
-
- public SamplePluginStatusClockWidget(PluginDesktopComponentContext context)
- {
- _context = context;
- _localizer = PluginLocalizer.Create(context);
- _stateService = context.GetService()
- ?? throw new InvalidOperationException("SamplePluginRuntimeStateService is not available.");
- _clockService = context.GetService()
- ?? throw new InvalidOperationException("SamplePluginClockService is not available.");
- _messageBus = context.GetService()
- ?? 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(message =>
- Dispatcher.UIThread.Post(() => RefreshClock(message.CurrentTime))));
-
- _subscriptions.Add(_messageBus.Subscribe(_ =>
- 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);
- }
-}
diff --git a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/plugin.json b/LanAirApp/samples/LanMountainDesktop.SamplePlugin/plugin.json
deleted file mode 100644
index fa139a0..0000000
--- a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/plugin.json
+++ /dev/null
@@ -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"
-}
diff --git a/LanAirApp/samples/README.md b/LanAirApp/samples/README.md
deleted file mode 100644
index f6c55fb..0000000
--- a/LanAirApp/samples/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# 示例插件目录
-
-## 中文
-
-本目录用于存放阑山桌面的示例插件和参考实现。
-
-当前标准示例为 `LanMountainDesktop.SamplePlugin`。
-
-## English
-
-This directory stores sample plugins and reference implementations. The current standard sample is `LanMountainDesktop.SamplePlugin`.
diff --git a/LanAirApp/standards/README.md b/LanAirApp/standards/README.md
deleted file mode 100644
index 2e0bcfe..0000000
--- a/LanAirApp/standards/README.md
+++ /dev/null
@@ -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.
diff --git a/LanAirApp/standards/plugin.template.json b/LanAirApp/standards/plugin.template.json
deleted file mode 100644
index 1852860..0000000
--- a/LanAirApp/standards/plugin.template.json
+++ /dev/null
@@ -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"
-}
diff --git a/LanAirApp/tools/LanMountainDesktop.PluginPackager/LanMountainDesktop.PluginPackager.csproj b/LanAirApp/tools/LanMountainDesktop.PluginPackager/LanMountainDesktop.PluginPackager.csproj
deleted file mode 100644
index 4735444..0000000
--- a/LanAirApp/tools/LanMountainDesktop.PluginPackager/LanMountainDesktop.PluginPackager.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- Exe
- net10.0
- enable
- enable
- 1.0.0
-
-
-
-
-
-
-
diff --git a/LanAirApp/tools/LanMountainDesktop.PluginPackager/Program.cs b/LanAirApp/tools/LanMountainDesktop.PluginPackager/Program.cs
deleted file mode 100644
index 7e3d171..0000000
--- a/LanAirApp/tools/LanMountainDesktop.PluginPackager/Program.cs
+++ /dev/null
@@ -1,136 +0,0 @@
-using System.IO.Compression;
-using LanMountainDesktop.PluginSdk;
-
-return await RunAsync(args);
-
-static async Task 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 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 Required");
- Console.WriteLine(" --output Optional");
- Console.WriteLine(" --overwrite Optional");
-}
diff --git a/LanMountainDesktop.PluginSdk/IPluginAppearanceContext.cs b/LanMountainDesktop.PluginSdk/IPluginAppearanceContext.cs
new file mode 100644
index 0000000..eb93784
--- /dev/null
+++ b/LanMountainDesktop.PluginSdk/IPluginAppearanceContext.cs
@@ -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);
+}
diff --git a/LanMountainDesktop.PluginSdk/IPluginRuntimeContext.cs b/LanMountainDesktop.PluginSdk/IPluginRuntimeContext.cs
index e55a758..c7345ca 100644
--- a/LanMountainDesktop.PluginSdk/IPluginRuntimeContext.cs
+++ b/LanMountainDesktop.PluginSdk/IPluginRuntimeContext.cs
@@ -12,6 +12,8 @@ public interface IPluginRuntimeContext
IReadOnlyDictionary Properties { get; }
+ IPluginAppearanceContext Appearance { get; }
+
T? GetService();
bool TryGetProperty(string key, out T? value);
diff --git a/LanMountainDesktop.PluginSdk/PluginAppearanceContext.cs b/LanMountainDesktop.PluginSdk/PluginAppearanceContext.cs
new file mode 100644
index 0000000..d7fdfad
--- /dev/null
+++ b/LanMountainDesktop.PluginSdk/PluginAppearanceContext.cs
@@ -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);
+ }
+}
diff --git a/LanMountainDesktop.PluginSdk/PluginAppearanceSnapshot.cs b/LanMountainDesktop.PluginSdk/PluginAppearanceSnapshot.cs
new file mode 100644
index 0000000..fba0f99
--- /dev/null
+++ b/LanMountainDesktop.PluginSdk/PluginAppearanceSnapshot.cs
@@ -0,0 +1,6 @@
+namespace LanMountainDesktop.PluginSdk;
+
+public sealed record PluginAppearanceSnapshot(
+ double GlobalCornerRadiusScale,
+ PluginCornerRadiusTokens CornerRadiusTokens,
+ string ThemeVariant);
diff --git a/LanMountainDesktop.PluginSdk/PluginCornerRadiusPreset.cs b/LanMountainDesktop.PluginSdk/PluginCornerRadiusPreset.cs
new file mode 100644
index 0000000..d206714
--- /dev/null
+++ b/LanMountainDesktop.PluginSdk/PluginCornerRadiusPreset.cs
@@ -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
+}
diff --git a/LanMountainDesktop.PluginSdk/PluginCornerRadiusTokens.cs b/LanMountainDesktop.PluginSdk/PluginCornerRadiusTokens.cs
new file mode 100644
index 0000000..8bdfa89
--- /dev/null
+++ b/LanMountainDesktop.PluginSdk/PluginCornerRadiusTokens.cs
@@ -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);
+ }
+}
diff --git a/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs b/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs
index 07b85ae..6f592dc 100644
--- a/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs
+++ b/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs
@@ -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()
diff --git a/LanMountainDesktop.PluginSdk/PluginDesktopComponentOptions.cs b/LanMountainDesktop.PluginSdk/PluginDesktopComponentOptions.cs
new file mode 100644
index 0000000..d5172a4
--- /dev/null
+++ b/LanMountainDesktop.PluginSdk/PluginDesktopComponentOptions.cs
@@ -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? CornerRadiusResolver { get; init; }
+}
diff --git a/LanMountainDesktop.PluginSdk/PluginDesktopComponentRegistration.cs b/LanMountainDesktop.PluginSdk/PluginDesktopComponentRegistration.cs
index a4213e0..88708f8 100644
--- a/LanMountainDesktop.PluginSdk/PluginDesktopComponentRegistration.cs
+++ b/LanMountainDesktop.PluginSdk/PluginDesktopComponentRegistration.cs
@@ -5,67 +5,37 @@ namespace LanMountainDesktop.PluginSdk;
public sealed class PluginDesktopComponentRegistration
{
public PluginDesktopComponentRegistration(
- string componentId,
- string displayName,
Func 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? 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 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? 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? CornerRadiusResolver { get; }
+ public PluginCornerRadiusPreset CornerRadiusPreset { get; }
+
+ public Func? 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);
+ }
}
diff --git a/LanMountainDesktop.PluginSdk/PluginManifest.cs b/LanMountainDesktop.PluginSdk/PluginManifest.cs
index ffa4717..56d1ac7 100644
--- a/LanMountainDesktop.PluginSdk/PluginManifest.cs
+++ b/LanMountainDesktop.PluginSdk/PluginManifest.cs
@@ -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;
diff --git a/LanMountainDesktop.PluginSdk/PluginSdkInfo.cs b/LanMountainDesktop.PluginSdk/PluginSdkInfo.cs
index b65e669..0a41024 100644
--- a/LanMountainDesktop.PluginSdk/PluginSdkInfo.cs
+++ b/LanMountainDesktop.PluginSdk/PluginSdkInfo.cs
@@ -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";
diff --git a/LanMountainDesktop.PluginSdk/PluginServiceCollectionExtensions.cs b/LanMountainDesktop.PluginSdk/PluginServiceCollectionExtensions.cs
index a53e4f6..48148d2 100644
--- a/LanMountainDesktop.PluginSdk/PluginServiceCollectionExtensions.cs
+++ b/LanMountainDesktop.PluginSdk/PluginServiceCollectionExtensions.cs
@@ -30,34 +30,15 @@ public static class PluginServiceCollectionExtensions
public static IServiceCollection AddPluginDesktopComponent(
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? cornerRadiusResolver = null)
+ PluginDesktopComponentOptions options)
where TControl : Control
{
ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(options);
services.AddSingleton(new PluginDesktopComponentRegistration(
- componentId,
- displayName,
(provider, context) => ActivatorUtilities.CreateInstance(provider, context),
- iconKey,
- category,
- minWidthCells,
- minHeightCells,
- allowDesktopPlacement,
- allowStatusBarPlacement,
- resizeMode,
- displayNameLocalizationKey,
- cornerRadiusResolver));
+ options));
return services;
}
diff --git a/LanMountainDesktop.Tests/BuiltInDesktopHostCornerRadiusBaselineTests.cs b/LanMountainDesktop.Tests/BuiltInDesktopHostCornerRadiusBaselineTests.cs
new file mode 100644
index 0000000..cb8b0db
--- /dev/null
+++ b/LanMountainDesktop.Tests/BuiltInDesktopHostCornerRadiusBaselineTests.cs
@@ -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));
+ }
+}
diff --git a/LanMountainDesktop.Tests/CornerRadiusScaleTests.cs b/LanMountainDesktop.Tests/CornerRadiusScaleTests.cs
index 4532b89..63a0b2e 100644
--- a/LanMountainDesktop.Tests/CornerRadiusScaleTests.cs
+++ b/LanMountainDesktop.Tests/CornerRadiusScaleTests.cs
@@ -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;
diff --git a/LanMountainDesktop.Tests/InfoRecommendationHostCornerRadiusTests.cs b/LanMountainDesktop.Tests/InfoRecommendationHostCornerRadiusTests.cs
new file mode 100644
index 0000000..a07e4d3
--- /dev/null
+++ b/LanMountainDesktop.Tests/InfoRecommendationHostCornerRadiusTests.cs
@@ -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));
+ }
+}
diff --git a/LanMountainDesktop.slnx b/LanMountainDesktop.slnx
index 77087f6..61bb691 100644
--- a/LanMountainDesktop.slnx
+++ b/LanMountainDesktop.slnx
@@ -1,5 +1,4 @@
-
diff --git a/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs b/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs
index 20198ef..a883628 100644
--- a/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs
+++ b/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs
@@ -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)
diff --git a/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs b/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs
index 27a2137..caff4b8 100644
--- a/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs
@@ -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();
}
diff --git a/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs b/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs
index e9187e8..bfadd25 100644
--- a/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs
@@ -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);
diff --git a/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs b/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs
index c0f800f..b30c71e 100644
--- a/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs
@@ -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);
diff --git a/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs b/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs
index 591f2d9..81163f9 100644
--- a/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs
@@ -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)
diff --git a/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs b/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs
index 06d24a3..bea6a2e 100644
--- a/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs
@@ -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");
diff --git a/LanMountainDesktop/Views/Components/ClockWidget.axaml.cs b/LanMountainDesktop/Views/Components/ClockWidget.axaml.cs
index bbfb2e1..604a7b3 100644
--- a/LanMountainDesktop/Views/Components/ClockWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/ClockWidget.axaml.cs
@@ -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;
}
diff --git a/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs b/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs
index d16b1c1..2ccc644 100644
--- a/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs
@@ -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))
diff --git a/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs b/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs
index 258a108..4351090 100644
--- a/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs
+++ b/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs
@@ -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)
diff --git a/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs b/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs
index e1bb3d3..259820e 100644
--- a/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs
@@ -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))
diff --git a/LanMountainDesktop/Views/Components/DailyPoetryWidget.axaml.cs b/LanMountainDesktop/Views/Components/DailyPoetryWidget.axaml.cs
index b03930a..12dbe82 100644
--- a/LanMountainDesktop/Views/Components/DailyPoetryWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/DailyPoetryWidget.axaml.cs
@@ -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;
diff --git a/LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml.cs b/LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml.cs
index ac45737..59a3206 100644
--- a/LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml.cs
@@ -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);
diff --git a/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs b/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs
index 9d6389d..26663f1 100644
--- a/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs
@@ -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);
diff --git a/LanMountainDesktop/Views/Components/DateWidget.axaml.cs b/LanMountainDesktop/Views/Components/DateWidget.axaml.cs
index 1315699..43d5651 100644
--- a/LanMountainDesktop/Views/Components/DateWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/DateWidget.axaml.cs
@@ -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));
diff --git a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs
index b5ef729..9eb35a3 100644
--- a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs
+++ b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs
@@ -89,12 +89,7 @@ public sealed class DesktopComponentRuntimeRegistration
public sealed class DesktopComponentRuntimeDescriptor
{
private static readonly Func 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 _controlFactory;
private readonly Func _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())
];
}
diff --git a/LanMountainDesktop/Views/Components/ExchangeRateCalculatorWidget.axaml.cs b/LanMountainDesktop/Views/Components/ExchangeRateCalculatorWidget.axaml.cs
index b91e8a0..3cba134 100644
--- a/LanMountainDesktop/Views/Components/ExchangeRateCalculatorWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/ExchangeRateCalculatorWidget.axaml.cs
@@ -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));
}
diff --git a/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs
index 4a6e358..290ab73 100644
--- a/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs
@@ -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,
diff --git a/LanMountainDesktop/Views/Components/HolidayCalendarWidget.axaml.cs b/LanMountainDesktop/Views/Components/HolidayCalendarWidget.axaml.cs
index b31e5ce..49409d5 100644
--- a/LanMountainDesktop/Views/Components/HolidayCalendarWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/HolidayCalendarWidget.axaml.cs
@@ -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);
diff --git a/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs
index 2873a8b..5b5ab68 100644
--- a/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs
@@ -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,
diff --git a/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs b/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs
index 46194a5..9716c05 100644
--- a/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs
@@ -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))
diff --git a/LanMountainDesktop/Views/Components/LunarCalendarWidget.axaml.cs b/LanMountainDesktop/Views/Components/LunarCalendarWidget.axaml.cs
index b43b45e..b5debff 100644
--- a/LanMountainDesktop/Views/Components/LunarCalendarWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/LunarCalendarWidget.axaml.cs
@@ -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(
diff --git a/LanMountainDesktop/Views/Components/MonthCalendarWidget.axaml.cs b/LanMountainDesktop/Views/Components/MonthCalendarWidget.axaml.cs
index db854ea..9924c97 100644
--- a/LanMountainDesktop/Views/Components/MonthCalendarWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/MonthCalendarWidget.axaml.cs
@@ -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);
diff --git a/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs
index d8e8476..c30b968 100644
--- a/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs
@@ -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,
diff --git a/LanMountainDesktop/Views/Components/MusicControlWidget.axaml.cs b/LanMountainDesktop/Views/Components/MusicControlWidget.axaml.cs
index 716acad..3964c26 100644
--- a/LanMountainDesktop/Views/Components/MusicControlWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/MusicControlWidget.axaml.cs
@@ -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);
diff --git a/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml.cs b/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml.cs
index c6b124a..d6f92f1 100644
--- a/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml.cs
@@ -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)
diff --git a/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs b/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs
index 26ebe58..940297d 100644
--- a/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs
@@ -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;
diff --git a/LanMountainDesktop/Views/Components/RemovableStorageWidget.axaml.cs b/LanMountainDesktop/Views/Components/RemovableStorageWidget.axaml.cs
index 3660e17..50f10cb 100644
--- a/LanMountainDesktop/Views/Components/RemovableStorageWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/RemovableStorageWidget.axaml.cs
@@ -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),
diff --git a/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs b/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs
index 2c5868b..49efd2f 100644
--- a/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs
@@ -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);
diff --git a/LanMountainDesktop/Views/Components/StudyDeductionReasonsWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyDeductionReasonsWidget.axaml.cs
index bd68a19..a15f35b 100644
--- a/LanMountainDesktop/Views/Components/StudyDeductionReasonsWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/StudyDeductionReasonsWidget.axaml.cs
@@ -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;
diff --git a/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs
index 173cb7f..5381d80 100644
--- a/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs
@@ -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));
diff --git a/LanMountainDesktop/Views/Components/StudyInterruptDensityWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyInterruptDensityWidget.axaml.cs
index 568061f..9d01ef1 100644
--- a/LanMountainDesktop/Views/Components/StudyInterruptDensityWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/StudyInterruptDensityWidget.axaml.cs
@@ -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;
diff --git a/LanMountainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs
index c85c4ef..8dfe619 100644
--- a/LanMountainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs
@@ -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));
diff --git a/LanMountainDesktop/Views/Components/StudyNoiseDistributionWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyNoiseDistributionWidget.axaml.cs
index 717f0f5..d35016a 100644
--- a/LanMountainDesktop/Views/Components/StudyNoiseDistributionWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/StudyNoiseDistributionWidget.axaml.cs
@@ -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));
diff --git a/LanMountainDesktop/Views/Components/StudyScoreOverviewWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyScoreOverviewWidget.axaml.cs
index 74c3bcb..d579724 100644
--- a/LanMountainDesktop/Views/Components/StudyScoreOverviewWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/StudyScoreOverviewWidget.axaml.cs
@@ -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;
diff --git a/LanMountainDesktop/Views/Components/StudySessionControlWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudySessionControlWidget.axaml.cs
index 422af01..613f70e 100644
--- a/LanMountainDesktop/Views/Components/StudySessionControlWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/StudySessionControlWidget.axaml.cs
@@ -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));
diff --git a/LanMountainDesktop/Views/Components/StudySessionHistoryWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudySessionHistoryWidget.axaml.cs
index db56fcf..244a6db 100644
--- a/LanMountainDesktop/Views/Components/StudySessionHistoryWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/StudySessionHistoryWidget.axaml.cs
@@ -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));
diff --git a/LanMountainDesktop/Views/Components/TimerWidget.axaml.cs b/LanMountainDesktop/Views/Components/TimerWidget.axaml.cs
index 76b3220..4fd8256 100644
--- a/LanMountainDesktop/Views/Components/TimerWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/TimerWidget.axaml.cs
@@ -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);
diff --git a/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs b/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs
index 8946da4..b72a8a2 100644
--- a/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs
@@ -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);
diff --git a/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs
index 156d497..1371990 100644
--- a/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs
@@ -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,
diff --git a/LanMountainDesktop/Views/Components/WhiteboardWidget.axaml.cs b/LanMountainDesktop/Views/Components/WhiteboardWidget.axaml.cs
index 8a600fa..79960cb 100644
--- a/LanMountainDesktop/Views/Components/WhiteboardWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/WhiteboardWidget.axaml.cs
@@ -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));
diff --git a/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs b/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs
index 5621dac..d95de69 100644
--- a/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs
@@ -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);
diff --git a/LanMountainDesktop/Views/MainWindow.axaml b/LanMountainDesktop/Views/MainWindow.axaml
index a7efd3d..e75ca27 100644
--- a/LanMountainDesktop/Views/MainWindow.axaml
+++ b/LanMountainDesktop/Views/MainWindow.axaml
@@ -269,7 +269,7 @@
Grid.ColumnSpan="1"
HorizontalAlignment="Stretch"
Margin="0"
- CornerRadius="36"
+ CornerRadius="{DynamicResource DesignCornerRadiusLg}"
Padding="6">
diff --git a/LanMountainDesktop/Views/MainWindow.axaml.cs b/LanMountainDesktop/Views/MainWindow.axaml.cs
index 6189fe0..5fd1af3 100644
--- a/LanMountainDesktop/Views/MainWindow.axaml.cs
+++ b/LanMountainDesktop/Views/MainWindow.axaml.cs
@@ -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)
diff --git a/LanMountainDesktop/plugins/PluginLoader.cs b/LanMountainDesktop/plugins/PluginLoader.cs
index a747545..aebd557 100644
--- a/LanMountainDesktop/plugins/PluginLoader.cs
+++ b/LanMountainDesktop/plugins/PluginLoader.cs
@@ -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? properties)
+ IReadOnlyDictionary? 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(runtimeContext);
+ services.AddSingleton(runtimeContext.Appearance);
services.AddSingleton(runtimeContext.Manifest);
services.AddSingleton>(runtimeContext.Properties);
services.AddSingleton();
@@ -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(IServiceCollection services, IServiceProvider? hostServices)
where TService : class
{
@@ -730,12 +760,14 @@ public sealed class PluginLoader
PluginManifest manifest,
string pluginDirectory,
string dataDirectory,
- IReadOnlyDictionary properties)
+ IReadOnlyDictionary 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 Properties { get; }
+ public IPluginAppearanceContext Appearance { get; }
+
public T? GetService()
{
return (T?)Services.GetService(typeof(T));
diff --git a/LanMountainDesktop/plugins/PluginMarketModels.cs b/LanMountainDesktop/plugins/PluginMarketModels.cs
index ad05c13..6ca2277 100644
--- a/LanMountainDesktop/plugins/PluginMarketModels.cs
+++ b/LanMountainDesktop/plugins/PluginMarketModels.cs
@@ -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;
diff --git a/LanMountainDesktop/plugins/README.md b/LanMountainDesktop/plugins/README.md
index 72f6eb9..f17cb4e 100644
--- a/LanMountainDesktop/plugins/README.md
+++ b/LanMountainDesktop/plugins/README.md
@@ -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
diff --git a/PRODUCT_BRIEF.md b/PRODUCT_BRIEF.md
new file mode 100644
index 0000000..cb33428
--- /dev/null
+++ b/PRODUCT_BRIEF.md
@@ -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 收集匿名数据(具体数据需后台查看)
+
+---
+
+**一句话总结**:阑山桌面是一款面向个人用户的可定制桌面工具,与希沃、鸿合等教育大屏系统不同,专注个人学习办公场景,通过组件化设计和插件生态提供轻量、开放、跨平台的桌面信息聚合方案。
diff --git a/PRODUCT_DOCUMENT.md b/PRODUCT_DOCUMENT.md
new file mode 100644
index 0000000..cc2dab5
--- /dev/null
+++ b/PRODUCT_DOCUMENT.md
@@ -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 官方市场
+
+---
+
+**阑山桌面,让你的桌面更有温度。**
diff --git a/README.md b/README.md
index eb7619f..8be2891 100644
--- a/README.md
+++ b/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(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`
diff --git a/airappmarket/README.md b/airappmarket/README.md
deleted file mode 100644
index be92491..0000000
--- a/airappmarket/README.md
+++ /dev/null
@@ -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.
diff --git a/airappmarket/assets/sample-plugin.svg b/airappmarket/assets/sample-plugin.svg
deleted file mode 100644
index 6c5c75c..0000000
--- a/airappmarket/assets/sample-plugin.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
diff --git a/airappmarket/index.json b/airappmarket/index.json
deleted file mode 100644
index 9b1b828..0000000
--- a/airappmarket/index.json
+++ /dev/null
@@ -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."
- }
- ]
-}
diff --git a/airappmarket/schema/airappmarket-index.schema.json b/airappmarket/schema/airappmarket-index.schema.json
deleted file mode 100644
index cb23803..0000000
--- a/airappmarket/schema/airappmarket-index.schema.json
+++ /dev/null
@@ -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
- }
- }
- }
- }
-}
diff --git a/airappmarket/tools/AirAppMarket.Validator/AirAppMarket.Validator.csproj b/airappmarket/tools/AirAppMarket.Validator/AirAppMarket.Validator.csproj
deleted file mode 100644
index d0870c2..0000000
--- a/airappmarket/tools/AirAppMarket.Validator/AirAppMarket.Validator.csproj
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
- Exe
- net10.0
- enable
- enable
- 1.0.0
-
-
-
diff --git a/airappmarket/tools/AirAppMarket.Validator/Program.cs b/airappmarket/tools/AirAppMarket.Validator/Program.cs
deleted file mode 100644
index 4edbd60..0000000
--- a/airappmarket/tools/AirAppMarket.Validator/Program.cs
+++ /dev/null
@@ -1,247 +0,0 @@
-using System.Text.Json;
-
-return await RunAsync(args);
-
-static Task 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 Plugins { get; init; } = [];
-
- public static MarketIndex Load(string json, string sourceName)
- {
- var document = JsonSerializer.Deserialize(
- 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(Plugins.Count);
- var seenIds = new HashSet(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 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)
- };
- }
-}
diff --git a/docs/ECOSYSTEM_BOUNDARIES.md b/docs/ECOSYSTEM_BOUNDARIES.md
new file mode 100644
index 0000000..79aac5e
--- /dev/null
+++ b/docs/ECOSYSTEM_BOUNDARIES.md
@@ -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.
diff --git a/docs/PLUGIN_SDK_V4_MIGRATION.md b/docs/PLUGIN_SDK_V4_MIGRATION.md
new file mode 100644
index 0000000..0fd63ff
--- /dev/null
+++ b/docs/PLUGIN_SDK_V4_MIGRATION.md
@@ -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(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`