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`