This commit is contained in:
lincube
2026-03-04 15:22:52 +08:00
parent 2ba66893ac
commit f78a56cb2c
198 changed files with 461 additions and 461 deletions

View File

@@ -0,0 +1,28 @@
namespace LanMountainDesktop.ComponentSystem;
public static class BuiltInComponentIds
{
public const string Clock = "Clock";
public const string DesktopClock = "DesktopClock";
public const string DesktopWeatherClock = "DesktopWeatherClock";
public const string DesktopTimer = "DesktopTimer";
public const string DesktopWeather = "DesktopWeather";
public const string DesktopHourlyWeather = "DesktopHourlyWeather";
public const string DesktopMultiDayWeather = "DesktopMultiDayWeather";
public const string DesktopExtendedWeather = "DesktopExtendedWeather";
public const string DesktopClassSchedule = "DesktopClassSchedule";
public const string DesktopMusicControl = "DesktopMusicControl";
public const string DesktopAudioRecorder = "DesktopAudioRecorder";
public const string DesktopStudyEnvironment = "DesktopStudyEnvironment";
public const string DesktopStudyNoiseCurve = "DesktopStudyNoiseCurve";
public const string Blank2x4 = "Blank2x4";
public const string Date = "Date";
public const string MonthCalendar = "MonthCalendar";
public const string LunarCalendar = "LunarCalendar";
public const string HolidayCalendar = "HolidayCalendar";
public const string DesktopDailyPoetry = "DesktopDailyPoetry";
public const string DesktopDailyArtwork = "DesktopDailyArtwork";
public const string DesktopWhiteboard = "DesktopWhiteboard";
public const string DesktopBlackboardLandscape = "DesktopBlackboardLandscape";
public const string DesktopBrowser = "DesktopBrowser";
}

View File

@@ -0,0 +1,28 @@
using System;
namespace LanMountainDesktop.ComponentSystem;
public static class ComponentPlacementRules
{
public static (int WidthCells, int HeightCells) EnsureMinimumSize(
DesktopComponentDefinition definition,
int requestedWidthCells,
int requestedHeightCells)
{
var width = Math.Max(definition.MinWidthCells, requestedWidthCells);
var height = Math.Max(definition.MinHeightCells, requestedHeightCells);
return (Math.Max(1, width), Math.Max(1, height));
}
public static bool CanPlaceInStatusBar(DesktopComponentDefinition definition, int requestedHeightCells)
{
return definition.AllowStatusBarPlacement && requestedHeightCells == 1;
}
public static (int Column, int Row) ClampToGrid(int requestedColumn, int requestedRow, int maxColumns, int maxRows)
{
var clampedColumn = Math.Clamp(requestedColumn, 0, Math.Max(0, maxColumns - 1));
var clampedRow = Math.Clamp(requestedRow, 0, Math.Max(0, maxRows - 1));
return (clampedColumn, clampedRow);
}
}

View File

@@ -0,0 +1,268 @@
using System;
using System.Collections.Generic;
using System.Linq;
using LanMountainDesktop.ComponentSystem.Extensions;
namespace LanMountainDesktop.ComponentSystem;
public sealed class ComponentRegistry
{
private readonly Dictionary<string, DesktopComponentDefinition> _definitions;
public ComponentRegistry(IEnumerable<DesktopComponentDefinition> definitions)
{
_definitions = definitions
.Where(d => !string.IsNullOrWhiteSpace(d.Id))
.GroupBy(d => d.Id, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.Last(), StringComparer.OrdinalIgnoreCase);
}
public static ComponentRegistry CreateDefault()
{
var builtIn = new[]
{
new DesktopComponentDefinition(
BuiltInComponentIds.Clock,
"Clock",
"Clock",
"Status",
MinWidthCells: 3,
MinHeightCells: 1,
AllowStatusBarPlacement: true,
AllowDesktopPlacement: false),
new DesktopComponentDefinition(
BuiltInComponentIds.DesktopClock,
"Clock",
"Clock",
"Clock",
MinWidthCells: 2,
MinHeightCells: 2,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true),
new DesktopComponentDefinition(
BuiltInComponentIds.DesktopWeatherClock,
"Weather Clock",
"Clock",
"Clock",
MinWidthCells: 2,
MinHeightCells: 1,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true),
new DesktopComponentDefinition(
BuiltInComponentIds.DesktopTimer,
"Timer",
"Timer",
"Clock",
MinWidthCells: 2,
MinHeightCells: 2,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true),
new DesktopComponentDefinition(
BuiltInComponentIds.DesktopWeather,
"Weather",
"WeatherSunny",
"Weather",
MinWidthCells: 2,
MinHeightCells: 2,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true),
new DesktopComponentDefinition(
BuiltInComponentIds.DesktopHourlyWeather,
"Hourly Weather",
"WeatherSunny",
"Weather",
MinWidthCells: 4,
MinHeightCells: 2,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true),
new DesktopComponentDefinition(
BuiltInComponentIds.DesktopMultiDayWeather,
"Multi-day Weather",
"WeatherSunny",
"Weather",
MinWidthCells: 4,
MinHeightCells: 2,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true),
new DesktopComponentDefinition(
BuiltInComponentIds.DesktopExtendedWeather,
"Extended Weather",
"WeatherSunny",
"Weather",
MinWidthCells: 4,
MinHeightCells: 4,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true),
new DesktopComponentDefinition(
BuiltInComponentIds.DesktopClassSchedule,
"Class Schedule",
"CalendarDate",
"Date",
MinWidthCells: 2,
MinHeightCells: 4,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true,
ResizeMode: DesktopComponentResizeMode.Free),
new DesktopComponentDefinition(
BuiltInComponentIds.DesktopMusicControl,
"Music Control",
"Play",
"Media",
MinWidthCells: 4,
MinHeightCells: 2,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true),
new DesktopComponentDefinition(
BuiltInComponentIds.DesktopAudioRecorder,
"Recorder",
"MicOn",
"Media",
MinWidthCells: 2,
MinHeightCells: 2,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true),
new DesktopComponentDefinition(
BuiltInComponentIds.DesktopStudyEnvironment,
"Study Environment",
"MicOn",
"Study",
MinWidthCells: 2,
MinHeightCells: 1,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true,
ResizeMode: DesktopComponentResizeMode.Free),
new DesktopComponentDefinition(
BuiltInComponentIds.DesktopStudyNoiseCurve,
"Noise Curve",
"DataLine",
"Study",
MinWidthCells: 4,
MinHeightCells: 2,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true),
new DesktopComponentDefinition(
BuiltInComponentIds.DesktopDailyPoetry,
"Daily Poetry",
"Book",
"Info",
MinWidthCells: 4,
MinHeightCells: 2,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true),
new DesktopComponentDefinition(
BuiltInComponentIds.DesktopDailyArtwork,
"Daily Artwork",
"Image",
"Info",
MinWidthCells: 4,
MinHeightCells: 2,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true),
new DesktopComponentDefinition(
BuiltInComponentIds.DesktopWhiteboard,
"Blackboard Portrait",
"Edit",
"Board",
MinWidthCells: 2,
MinHeightCells: 4,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true,
ResizeMode: DesktopComponentResizeMode.Free),
new DesktopComponentDefinition(
BuiltInComponentIds.DesktopBlackboardLandscape,
"Blackboard Landscape",
"Edit",
"Board",
MinWidthCells: 4,
MinHeightCells: 2,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true,
ResizeMode: DesktopComponentResizeMode.Free),
new DesktopComponentDefinition(
BuiltInComponentIds.DesktopBrowser,
"Browser",
"Globe",
"Board",
MinWidthCells: 4,
MinHeightCells: 4,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true,
ResizeMode: DesktopComponentResizeMode.Free),
new DesktopComponentDefinition(
BuiltInComponentIds.Date,
"Calendar",
"Calendar",
"Date",
MinWidthCells: 4,
MinHeightCells: 2,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true),
new DesktopComponentDefinition(
BuiltInComponentIds.MonthCalendar,
"Month Calendar",
"CalendarMonth",
"Date",
MinWidthCells: 2,
MinHeightCells: 2,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true),
new DesktopComponentDefinition(
BuiltInComponentIds.LunarCalendar,
"Lunar Calendar",
"Calendar",
"Date",
MinWidthCells: 2,
MinHeightCells: 2,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true),
new DesktopComponentDefinition(
BuiltInComponentIds.HolidayCalendar,
"Holiday Countdown",
"Calendar",
"Date",
MinWidthCells: 2,
MinHeightCells: 2,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true)
};
return new ComponentRegistry(builtIn);
}
public ComponentRegistry RegisterExtensions(IEnumerable<IComponentExtensionProvider> providers)
{
var merged = _definitions.Values.ToList();
foreach (var provider in providers)
{
var externalDefinitions = provider.GetComponents();
if (externalDefinitions is null)
{
continue;
}
merged.AddRange(externalDefinitions);
}
return new ComponentRegistry(merged);
}
public bool TryGetDefinition(string componentId, out DesktopComponentDefinition definition)
{
return _definitions.TryGetValue(componentId, out definition!);
}
public bool IsKnownComponent(string componentId)
{
return _definitions.ContainsKey(componentId);
}
public bool AllowsStatusBarPlacement(string componentId)
{
return _definitions.TryGetValue(componentId, out var definition) && definition.AllowStatusBarPlacement;
}
public IReadOnlyList<DesktopComponentDefinition> GetAll()
{
return _definitions.Values.OrderBy(d => d.Category).ThenBy(d => d.DisplayName).ToList();
}
}

View File

@@ -0,0 +1,12 @@
namespace LanMountainDesktop.ComponentSystem;
public sealed record DesktopComponentDefinition(
string Id,
string DisplayName,
string IconKey,
string Category,
int MinWidthCells,
int MinHeightCells,
bool AllowStatusBarPlacement,
bool AllowDesktopPlacement,
DesktopComponentResizeMode ResizeMode = DesktopComponentResizeMode.Proportional);

View File

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

View File

@@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace LanMountainDesktop.ComponentSystem.Extensions;
public interface IComponentExtensionProvider
{
IReadOnlyList<DesktopComponentDefinition> GetComponents();
}

View File

@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
namespace LanMountainDesktop.ComponentSystem.Extensions;
public sealed class JsonComponentExtensionProvider : IComponentExtensionProvider
{
private readonly IReadOnlyList<DesktopComponentDefinition> _definitions;
private JsonComponentExtensionProvider(IReadOnlyList<DesktopComponentDefinition> definitions)
{
_definitions = definitions;
}
public IReadOnlyList<DesktopComponentDefinition> GetComponents()
{
return _definitions;
}
public static IReadOnlyList<IComponentExtensionProvider> LoadProvidersFromDirectory(string directoryPath)
{
if (string.IsNullOrWhiteSpace(directoryPath) || !Directory.Exists(directoryPath))
{
return Array.Empty<IComponentExtensionProvider>();
}
var providers = new List<IComponentExtensionProvider>();
foreach (var filePath in Directory.GetFiles(directoryPath, "*.json", SearchOption.TopDirectoryOnly))
{
var provider = TryLoadFromFile(filePath);
if (provider is not null)
{
providers.Add(provider);
}
}
return providers;
}
private static JsonComponentExtensionProvider? TryLoadFromFile(string filePath)
{
try
{
var json = File.ReadAllText(filePath);
var entries = JsonSerializer.Deserialize<List<ComponentExtensionEntry>>(json);
if (entries is null || entries.Count == 0)
{
return null;
}
var definitions = new List<DesktopComponentDefinition>();
foreach (var entry in entries)
{
if (string.IsNullOrWhiteSpace(entry.Id) ||
string.IsNullOrWhiteSpace(entry.DisplayName))
{
continue;
}
definitions.Add(new DesktopComponentDefinition(
entry.Id.Trim(),
entry.DisplayName.Trim(),
string.IsNullOrWhiteSpace(entry.IconKey) ? "PuzzlePiece" : entry.IconKey,
string.IsNullOrWhiteSpace(entry.Category) ? "Extensions" : entry.Category,
MinWidthCells: Math.Max(1, entry.MinWidthCells),
MinHeightCells: Math.Max(1, entry.MinHeightCells),
AllowStatusBarPlacement: entry.AllowStatusBarPlacement,
AllowDesktopPlacement: entry.AllowDesktopPlacement));
}
return definitions.Count == 0 ? null : new JsonComponentExtensionProvider(definitions);
}
catch
{
return null;
}
}
private sealed class ComponentExtensionEntry
{
public string Id { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string IconKey { get; set; } = string.Empty;
public string Category { get; set; } = "Extensions";
public int MinWidthCells { get; set; } = 1;
public int MinHeightCells { get; set; } = 1;
public bool AllowStatusBarPlacement { get; set; }
public bool AllowDesktopPlacement { get; set; } = true;
}
}

View File

@@ -0,0 +1,77 @@
# 组件系统模块Component System Module
本目录提供组件系统的模块化基础,用于支持内置组件管理与第三方扩展接入。
This directory provides the modular foundation for built-in component management and third-party extension integration.
## 核心文件职责Core Files
- `BuiltInComponentIds.cs`:内置组件 ID 常量(例如 `Clock`)。
Built-in component ID constants (for example `Clock`).
- `DesktopComponentDefinition.cs`:组件元数据定义(名称、类别、最小尺寸、可放置区域等)。
Component metadata model (name, category, minimum size, placement permissions).
- `ComponentPlacementRules.cs`:组件放置规则(最小尺寸、状态栏高度限制、网格边界约束)。
Placement rules (minimum size, status-bar height rule, grid clamping).
- `ComponentRegistry.cs`:组件注册中心,负责内置组件与扩展组件合并。
Registry that merges built-in and extension components.
- `Extensions/IComponentExtensionProvider.cs`:扩展提供者接口契约。
Extension provider interface contract.
- `Extensions/JsonComponentExtensionProvider.cs`:基于 JSON 的扩展加载器。
JSON-based extension loader.
## 第三方扩展契约Extension Contract
- 第三方可通过实现 `IComponentExtensionProvider` 提供组件定义。
Third parties can provide component definitions via `IComponentExtensionProvider`.
- 当前内置了 JSON 提供者,运行时扫描目录:
Built-in JSON provider scans at runtime:
- `Extensions/Components/*.json`(相对应用输出目录)
`Extensions/Components/*.json` (relative to app output directory)
## 加载流程Load Flow
1. `ComponentRegistry.CreateDefault()` 先注册内置组件。
Register built-in components first via `ComponentRegistry.CreateDefault()`.
2. 调用 `.RegisterExtensions(...)` 合并扩展组件。
Merge extension components via `.RegisterExtensions(...)`.
3. 主窗口通过注册中心校验组件合法性与放置权限。
Main window validates component identity and placement permission through the registry.
## JSON 清单格式Manifest Schema
JSON 文件为数组,每一项代表一个组件定义。
The JSON file is an array, where each item represents one component definition.
```json
[
{
"id": "Weather",
"displayName": "Weather",
"iconKey": "WeatherSunny",
"category": "Status",
"minWidthCells": 1,
"minHeightCells": 1,
"allowStatusBarPlacement": true,
"allowDesktopPlacement": true
}
]
```
字段说明Field notes
- `id`:组件唯一 ID建议英文、稳定不变
Unique component ID (prefer stable English key).
- `displayName`:显示名。
Display name.
- `iconKey`:图标键(由上层 UI 解释)。
Icon key resolved by UI layer.
- `category`:组件分类。
Component category.
- `minWidthCells` / `minHeightCells`:最小占格,必须满足 `>= 1`
Minimum cell size, must satisfy `>= 1`.
- `allowStatusBarPlacement`:是否允许放到顶部状态栏。
Whether placing in top status bar is allowed.
- `allowDesktopPlacement`:是否允许放到桌面区域。
Whether placing in desktop area is allowed.
## 放置规则摘要Placement Rules Summary
- 最小尺寸约束:`minWidthCells >= 1``minHeightCells >= 1`
Minimum size constraint: `minWidthCells >= 1` and `minHeightCells >= 1`.
- 状态栏约束:状态栏组件高度必须为 `1` 格。
Status bar constraint: component height must be exactly `1` cell.
- 越界约束所有组件坐标会被网格边界钳制clamp
Out-of-bounds constraint: component coordinates are clamped to grid bounds.