This commit is contained in:
lincube
2026-02-28 12:30:16 +08:00
parent 310c0f224f
commit 473a84e47b
13 changed files with 750 additions and 327 deletions

View File

@@ -0,0 +1,6 @@
namespace LanMontainDesktop.ComponentSystem;
public static class BuiltInComponentIds
{
public const string Clock = "Clock";
}

View File

@@ -0,0 +1,28 @@
using System;
namespace LanMontainDesktop.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,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using LanMontainDesktop.ComponentSystem.Extensions;
namespace LanMontainDesktop.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: 1,
MinHeightCells: 1,
AllowStatusBarPlacement: true,
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,11 @@
namespace LanMontainDesktop.ComponentSystem;
public sealed record DesktopComponentDefinition(
string Id,
string DisplayName,
string IconKey,
string Category,
int MinWidthCells,
int MinHeightCells,
bool AllowStatusBarPlacement,
bool AllowDesktopPlacement);

View File

@@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace LanMontainDesktop.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 LanMontainDesktop.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.

View File

@@ -0,0 +1,16 @@
在此目录放置第三方组件清单文件(*.json
Place third-party component manifest files (*.json) in this directory.
运行时加载路径:
Runtime load path:
Extensions/Components/*.json
最小示例文件建议:
Minimal example file suggestion:
Extensions/Components/weather.json
命名建议:
Naming suggestions:
- weather.json
- stock.json
- calendar.json

View File

@@ -12,6 +12,7 @@
<Folder Include="Models\" />
<AvaloniaResource Include="Assets\**" />
<None Include="Localization\*.json" CopyToOutputDirectory="PreserveNewest" />
<None Include="Extensions\Components\*.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,347 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Threading;
using LanMontainDesktop.ComponentSystem;
using LanMontainDesktop.Models;
namespace LanMontainDesktop.Views;
public partial class MainWindow
{
private void OnOpenComponentLibraryClick(object? sender, RoutedEventArgs e)
{
if (_isComponentLibraryOpen)
{
return;
}
_reopenSettingsAfterComponentLibraryClose = _isSettingsOpen;
if (_isSettingsOpen)
{
CloseSettingsPage(immediate: true);
}
OpenComponentLibraryWindow();
}
private void OnCloseComponentLibraryClick(object? sender, RoutedEventArgs e)
{
CloseComponentLibraryWindow(reopenSettings: true);
}
private void OnStatusBarClockChecked(object? sender, RoutedEventArgs e)
{
if (_suppressStatusBarToggleEvents)
{
return;
}
_topStatusComponentIds.Add(BuiltInComponentIds.Clock);
ApplyTopStatusComponentVisibility();
UpdateWallpaperPreviewLayout();
PersistSettings();
}
private void OnStatusBarClockUnchecked(object? sender, RoutedEventArgs e)
{
if (_suppressStatusBarToggleEvents)
{
return;
}
_topStatusComponentIds.Remove(BuiltInComponentIds.Clock);
ApplyTopStatusComponentVisibility();
UpdateWallpaperPreviewLayout();
PersistSettings();
}
private void ApplyTaskbarSettings(AppSettingsSnapshot snapshot)
{
_topStatusComponentIds.Clear();
if (snapshot.TopStatusComponentIds is not null)
{
foreach (var componentId in snapshot.TopStatusComponentIds)
{
if (string.IsNullOrWhiteSpace(componentId))
{
continue;
}
var normalizedId = componentId.Trim();
if (_componentRegistry.IsKnownComponent(normalizedId) &&
_componentRegistry.AllowsStatusBarPlacement(normalizedId))
{
_topStatusComponentIds.Add(normalizedId);
}
}
}
_pinnedTaskbarActions.Clear();
if (snapshot.PinnedTaskbarActions is not null)
{
foreach (var actionText in snapshot.PinnedTaskbarActions)
{
if (Enum.TryParse<TaskbarActionId>(actionText, ignoreCase: true, out var action))
{
_pinnedTaskbarActions.Add(action);
}
}
}
if (_pinnedTaskbarActions.Count == 0)
{
foreach (var action in DefaultPinnedTaskbarActions)
{
_pinnedTaskbarActions.Add(action);
}
}
_enableDynamicTaskbarActions = snapshot.EnableDynamicTaskbarActions;
_taskbarLayoutMode = string.IsNullOrWhiteSpace(snapshot.TaskbarLayoutMode)
? TaskbarLayoutBottomFullRowMacStyle
: snapshot.TaskbarLayoutMode;
}
private void ApplyTopStatusComponentVisibility()
{
var showClock = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock);
if (ClockWidget is not null)
{
ClockWidget.IsVisible = showClock;
}
if (WallpaperPreviewClockContainer is not null)
{
WallpaperPreviewClockContainer.IsVisible = showClock;
}
if (WallpaperPreviewClockTextBlock is not null && showClock)
{
WallpaperPreviewClockTextBlock.Text = DateTime.Now.ToString("HH:mm");
}
}
private TaskbarContext GetCurrentTaskbarContext()
{
if (!_isSettingsOpen)
{
return TaskbarContext.Desktop;
}
return SettingsNavListBox?.SelectedIndex switch
{
0 => TaskbarContext.SettingsWallpaper,
1 => TaskbarContext.SettingsGrid,
2 => TaskbarContext.SettingsColor,
3 => TaskbarContext.SettingsStatusBar,
4 => TaskbarContext.SettingsRegion,
_ => TaskbarContext.Desktop
};
}
private void ApplyTaskbarActionVisibility(TaskbarContext context)
{
if (BackToWindowsButton is null ||
OpenComponentLibraryButton is null ||
OpenSettingsButton is null ||
WallpaperPreviewBackButtonVisual is null ||
WallpaperPreviewComponentLibraryVisual is null ||
WallpaperPreviewSettingsButtonIcon is null)
{
return;
}
var showMinimize = _pinnedTaskbarActions.Contains(TaskbarActionId.MinimizeToWindows);
var showSettings = _pinnedTaskbarActions.Contains(TaskbarActionId.OpenSettings);
var showComponentLibrary = _isSettingsOpen || _isComponentLibraryOpen;
BackToWindowsButton.IsVisible = showMinimize;
OpenComponentLibraryButton.IsVisible = showComponentLibrary;
OpenSettingsButton.IsVisible = showSettings;
WallpaperPreviewBackButtonVisual.IsVisible = showMinimize;
WallpaperPreviewComponentLibraryVisual.IsVisible = showComponentLibrary;
WallpaperPreviewSettingsButtonIcon.IsVisible = showSettings;
if (TaskbarFixedActionsHost is not null)
{
TaskbarFixedActionsHost.IsVisible = showMinimize;
}
if (TaskbarSettingsActionHost is not null)
{
TaskbarSettingsActionHost.IsVisible = showSettings || showComponentLibrary;
}
if (WallpaperPreviewTaskbarFixedActionsHost is not null)
{
WallpaperPreviewTaskbarFixedActionsHost.IsVisible = showMinimize;
}
if (WallpaperPreviewTaskbarSettingsActionHost is not null)
{
WallpaperPreviewTaskbarSettingsActionHost.IsVisible = showSettings || showComponentLibrary;
}
var dynamicActions = ResolveDynamicTaskbarActions(context);
var hasDynamicActions = dynamicActions.Count > 0;
BuildDynamicTaskbarVisuals(dynamicActions);
if (TaskbarDynamicActionsHost is not null)
{
TaskbarDynamicActionsHost.IsVisible = hasDynamicActions;
}
if (WallpaperPreviewTaskbarDynamicActionsHost is not null)
{
WallpaperPreviewTaskbarDynamicActionsHost.IsVisible = hasDynamicActions;
}
UpdateOpenSettingsActionVisualState();
}
private void UpdateOpenSettingsActionVisualState()
{
if (OpenSettingsButtonTextBlock is null || OpenSettingsButton is null)
{
return;
}
var showBackToDesktop = _isSettingsOpen;
OpenSettingsButtonTextBlock.IsVisible = showBackToDesktop;
OpenSettingsButtonTextBlock.Text = L("settings.back_to_desktop", "Back to Desktop");
ToolTip.SetTip(
OpenSettingsButton,
showBackToDesktop
? L("settings.back_to_desktop", "Back to Desktop")
: L("tooltip.open_settings", "Settings"));
var effectiveCellSize = _currentDesktopCellSize > 0
? _currentDesktopCellSize
: Math.Max(32, Math.Min(Bounds.Width, Bounds.Height) / Math.Max(1, _targetShortSideCells));
ApplyWidgetSizing(effectiveCellSize);
}
private void OpenComponentLibraryWindow()
{
if (ComponentLibraryWindow is null)
{
return;
}
_isComponentLibraryOpen = true;
ComponentLibraryWindow.IsVisible = true;
ComponentLibraryWindow.Opacity = 0;
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
Dispatcher.UIThread.Post(() =>
{
if (!_isComponentLibraryOpen || ComponentLibraryWindow is null)
{
return;
}
ComponentLibraryWindow.Opacity = 1;
}, DispatcherPriority.Background);
}
private void CloseComponentLibraryWindow(bool reopenSettings)
{
if (!_isComponentLibraryOpen || ComponentLibraryWindow is null)
{
return;
}
_isComponentLibraryOpen = false;
ComponentLibraryWindow.Opacity = 0;
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
DispatcherTimer.RunOnce(() =>
{
if (_isComponentLibraryOpen || ComponentLibraryWindow is null)
{
return;
}
ComponentLibraryWindow.IsVisible = false;
var shouldReopenSettings = reopenSettings && _reopenSettingsAfterComponentLibraryClose;
_reopenSettingsAfterComponentLibraryClose = false;
if (shouldReopenSettings)
{
OpenSettingsPage();
}
}, TimeSpan.FromMilliseconds(200));
}
private IReadOnlyList<TaskbarActionItem> ResolveDynamicTaskbarActions(TaskbarContext context)
{
if (!_enableDynamicTaskbarActions)
{
return Array.Empty<TaskbarActionItem>();
}
// Reserved for page-specific actions. Disabled by default in this phase.
_ = context;
return Array.Empty<TaskbarActionItem>();
}
private void BuildDynamicTaskbarVisuals(IReadOnlyList<TaskbarActionItem> actions)
{
if (TaskbarDynamicActionsPanel is not null)
{
TaskbarDynamicActionsPanel.Children.Clear();
}
if (WallpaperPreviewTaskbarDynamicActionsPanel is not null)
{
WallpaperPreviewTaskbarDynamicActionsPanel.Children.Clear();
}
if (actions.Count == 0 ||
TaskbarDynamicActionsPanel is null ||
WallpaperPreviewTaskbarDynamicActionsPanel is null)
{
return;
}
foreach (var action in actions)
{
if (!action.IsVisible)
{
continue;
}
var button = new Button
{
Content = action.Title,
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
Padding = new Thickness(12, 6),
Foreground = Foreground
};
TaskbarDynamicActionsPanel.Children.Add(button);
var previewText = new TextBlock
{
Text = action.Title,
Foreground = Foreground,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
};
var previewBorder = new Border
{
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
Child = previewText
};
WallpaperPreviewTaskbarDynamicActionsPanel.Children.Add(previewBorder);
}
}
}

View File

@@ -38,27 +38,6 @@ public partial class MainWindow
OpenSettingsPage();
}
private void OnOpenComponentLibraryClick(object? sender, RoutedEventArgs e)
{
if (_isComponentLibraryOpen)
{
return;
}
_reopenSettingsAfterComponentLibraryClose = _isSettingsOpen;
if (_isSettingsOpen)
{
CloseSettingsPage(immediate: true);
}
OpenComponentLibraryWindow();
}
private void OnCloseComponentLibraryClick(object? sender, RoutedEventArgs e)
{
CloseComponentLibraryWindow(reopenSettings: true);
}
private void OnCloseSettingsClick(object? sender, RoutedEventArgs e)
{
CloseSettingsPage();
@@ -92,32 +71,6 @@ public partial class MainWindow
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
}
private void OnStatusBarClockChecked(object? sender, RoutedEventArgs e)
{
if (_suppressStatusBarToggleEvents)
{
return;
}
_topStatusComponentIds.Add(ClockStatusComponentId);
ApplyTopStatusComponentVisibility();
UpdateWallpaperPreviewLayout();
PersistSettings();
}
private void OnStatusBarClockUnchecked(object? sender, RoutedEventArgs e)
{
if (_suppressStatusBarToggleEvents)
{
return;
}
_topStatusComponentIds.Remove(ClockStatusComponentId);
ApplyTopStatusComponentVisibility();
UpdateWallpaperPreviewLayout();
PersistSettings();
}
private void OnNightModeChecked(object? sender, RoutedEventArgs e)
{
if (_suppressThemeToggleEvents)
@@ -646,284 +599,6 @@ public partial class MainWindow
_previewVideoWallpaperMedia = null;
}
private void ApplyTaskbarSettings(AppSettingsSnapshot snapshot)
{
_topStatusComponentIds.Clear();
if (snapshot.TopStatusComponentIds is not null)
{
foreach (var componentId in snapshot.TopStatusComponentIds)
{
if (!string.IsNullOrWhiteSpace(componentId))
{
_topStatusComponentIds.Add(componentId.Trim());
}
}
}
_pinnedTaskbarActions.Clear();
if (snapshot.PinnedTaskbarActions is not null)
{
foreach (var actionText in snapshot.PinnedTaskbarActions)
{
if (Enum.TryParse<TaskbarActionId>(actionText, ignoreCase: true, out var action))
{
_pinnedTaskbarActions.Add(action);
}
}
}
if (_pinnedTaskbarActions.Count == 0)
{
foreach (var action in DefaultPinnedTaskbarActions)
{
_pinnedTaskbarActions.Add(action);
}
}
_enableDynamicTaskbarActions = snapshot.EnableDynamicTaskbarActions;
_taskbarLayoutMode = string.IsNullOrWhiteSpace(snapshot.TaskbarLayoutMode)
? TaskbarLayoutBottomFullRowMacStyle
: snapshot.TaskbarLayoutMode;
}
private void ApplyTopStatusComponentVisibility()
{
var showClock = _topStatusComponentIds.Contains(ClockStatusComponentId);
if (ClockWidget is not null)
{
ClockWidget.IsVisible = showClock;
}
if (WallpaperPreviewClockContainer is not null)
{
WallpaperPreviewClockContainer.IsVisible = showClock;
}
if (WallpaperPreviewClockTextBlock is not null && showClock)
{
WallpaperPreviewClockTextBlock.Text = DateTime.Now.ToString("HH:mm");
}
}
private TaskbarContext GetCurrentTaskbarContext()
{
if (!_isSettingsOpen)
{
return TaskbarContext.Desktop;
}
return SettingsNavListBox?.SelectedIndex switch
{
0 => TaskbarContext.SettingsWallpaper,
1 => TaskbarContext.SettingsGrid,
2 => TaskbarContext.SettingsColor,
3 => TaskbarContext.SettingsStatusBar,
4 => TaskbarContext.SettingsRegion,
_ => TaskbarContext.Desktop
};
}
private void ApplyTaskbarActionVisibility(TaskbarContext context)
{
if (BackToWindowsButton is null ||
OpenComponentLibraryButton is null ||
OpenSettingsButton is null ||
WallpaperPreviewBackButtonVisual is null ||
WallpaperPreviewComponentLibraryVisual is null ||
WallpaperPreviewSettingsButtonIcon is null)
{
return;
}
var showMinimize = _pinnedTaskbarActions.Contains(TaskbarActionId.MinimizeToWindows);
var showSettings = _pinnedTaskbarActions.Contains(TaskbarActionId.OpenSettings);
var showComponentLibrary = _isSettingsOpen || _isComponentLibraryOpen;
BackToWindowsButton.IsVisible = showMinimize;
OpenComponentLibraryButton.IsVisible = showComponentLibrary;
OpenSettingsButton.IsVisible = showSettings;
WallpaperPreviewBackButtonVisual.IsVisible = showMinimize;
WallpaperPreviewComponentLibraryVisual.IsVisible = showComponentLibrary;
WallpaperPreviewSettingsButtonIcon.IsVisible = showSettings;
if (TaskbarFixedActionsHost is not null)
{
TaskbarFixedActionsHost.IsVisible = showMinimize;
}
if (TaskbarSettingsActionHost is not null)
{
TaskbarSettingsActionHost.IsVisible = showSettings || showComponentLibrary;
}
if (WallpaperPreviewTaskbarFixedActionsHost is not null)
{
WallpaperPreviewTaskbarFixedActionsHost.IsVisible = showMinimize;
}
if (WallpaperPreviewTaskbarSettingsActionHost is not null)
{
WallpaperPreviewTaskbarSettingsActionHost.IsVisible = showSettings || showComponentLibrary;
}
var dynamicActions = ResolveDynamicTaskbarActions(context);
var hasDynamicActions = dynamicActions.Count > 0;
BuildDynamicTaskbarVisuals(dynamicActions);
if (TaskbarDynamicActionsHost is not null)
{
TaskbarDynamicActionsHost.IsVisible = hasDynamicActions;
}
if (WallpaperPreviewTaskbarDynamicActionsHost is not null)
{
WallpaperPreviewTaskbarDynamicActionsHost.IsVisible = hasDynamicActions;
}
UpdateOpenSettingsActionVisualState();
}
private void UpdateOpenSettingsActionVisualState()
{
if (OpenSettingsButtonTextBlock is null || OpenSettingsButton is null)
{
return;
}
var showBackToDesktop = _isSettingsOpen;
OpenSettingsButtonTextBlock.IsVisible = showBackToDesktop;
OpenSettingsButtonTextBlock.Text = L("settings.back_to_desktop", "Back to Desktop");
ToolTip.SetTip(
OpenSettingsButton,
showBackToDesktop
? L("settings.back_to_desktop", "Back to Desktop")
: L("tooltip.open_settings", "Settings"));
var effectiveCellSize = _currentDesktopCellSize > 0
? _currentDesktopCellSize
: Math.Max(32, Math.Min(Bounds.Width, Bounds.Height) / Math.Max(1, _targetShortSideCells));
ApplyWidgetSizing(effectiveCellSize);
}
private void OpenComponentLibraryWindow()
{
if (ComponentLibraryWindow is null)
{
return;
}
_isComponentLibraryOpen = true;
ComponentLibraryWindow.IsVisible = true;
ComponentLibraryWindow.Opacity = 0;
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
Dispatcher.UIThread.Post(() =>
{
if (!_isComponentLibraryOpen || ComponentLibraryWindow is null)
{
return;
}
ComponentLibraryWindow.Opacity = 1;
}, DispatcherPriority.Background);
}
private void CloseComponentLibraryWindow(bool reopenSettings)
{
if (!_isComponentLibraryOpen || ComponentLibraryWindow is null)
{
return;
}
_isComponentLibraryOpen = false;
ComponentLibraryWindow.Opacity = 0;
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
DispatcherTimer.RunOnce(() =>
{
if (_isComponentLibraryOpen || ComponentLibraryWindow is null)
{
return;
}
ComponentLibraryWindow.IsVisible = false;
var shouldReopenSettings = reopenSettings && _reopenSettingsAfterComponentLibraryClose;
_reopenSettingsAfterComponentLibraryClose = false;
if (shouldReopenSettings)
{
OpenSettingsPage();
}
}, TimeSpan.FromMilliseconds(200));
}
private IReadOnlyList<TaskbarActionItem> ResolveDynamicTaskbarActions(TaskbarContext context)
{
if (!_enableDynamicTaskbarActions)
{
return Array.Empty<TaskbarActionItem>();
}
// Reserved for page-specific actions. Disabled by default in this phase.
_ = context;
return Array.Empty<TaskbarActionItem>();
}
private void BuildDynamicTaskbarVisuals(IReadOnlyList<TaskbarActionItem> actions)
{
if (TaskbarDynamicActionsPanel is not null)
{
TaskbarDynamicActionsPanel.Children.Clear();
}
if (WallpaperPreviewTaskbarDynamicActionsPanel is not null)
{
WallpaperPreviewTaskbarDynamicActionsPanel.Children.Clear();
}
if (actions.Count == 0 ||
TaskbarDynamicActionsPanel is null ||
WallpaperPreviewTaskbarDynamicActionsPanel is null)
{
return;
}
foreach (var action in actions)
{
if (!action.IsVisible)
{
continue;
}
var button = new Button
{
Content = action.Title,
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
Padding = new Thickness(12, 6),
Foreground = Foreground
};
TaskbarDynamicActionsPanel.Children.Add(button);
var previewText = new TextBlock
{
Text = action.Title,
Foreground = Foreground,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
};
var previewBorder = new Border
{
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
Child = previewText
};
WallpaperPreviewTaskbarDynamicActionsPanel.Children.Add(previewBorder);
}
}
private void PersistSettings()
{
if (_suppressSettingsPersistence)

View File

@@ -14,6 +14,8 @@ using Avalonia.Platform.Storage;
using Avalonia.Styling;
using Avalonia.Threading;
using FluentAvalonia.Styling;
using LanMontainDesktop.ComponentSystem;
using LanMontainDesktop.ComponentSystem.Extensions;
using LanMontainDesktop.Models;
using LanMontainDesktop.Services;
using LanMontainDesktop.Theme;
@@ -45,7 +47,6 @@ public partial class MainWindow : Window
private const int SettingsTransitionDurationMs = 240;
private const double WallpaperPreviewMaxWidth = 520;
private const double LightBackgroundLuminanceThreshold = 0.57;
private const string ClockStatusComponentId = "Clock";
private const string TaskbarLayoutBottomFullRowMacStyle = "BottomFullRowMacStyle";
private static readonly HashSet<string> SupportedImageExtensions = new(StringComparer.OrdinalIgnoreCase)
{
@@ -64,6 +65,11 @@ public partial class MainWindow : Window
private readonly MonetColorService _monetColorService = new();
private readonly AppSettingsService _appSettingsService = new();
private readonly LocalizationService _localizationService = new();
private readonly ComponentRegistry _componentRegistry = ComponentRegistry
.CreateDefault()
.RegisterExtensions(
JsonComponentExtensionProvider.LoadProvidersFromDirectory(
Path.Combine(AppContext.BaseDirectory, "Extensions", "Components")));
private readonly FluentAvaloniaTheme? _fluentAvaloniaTheme;
private readonly HashSet<string> _topStatusComponentIds = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<TaskbarActionId> _pinnedTaskbarActions = [];
@@ -137,7 +143,7 @@ public partial class MainWindow : Window
_isNightMode = snapshot.IsNightMode ?? (CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold);
ApplyNightModeState(_isNightMode, refreshPalettes: true);
_suppressStatusBarToggleEvents = true;
StatusBarClockToggleSwitch.IsChecked = _topStatusComponentIds.Contains(ClockStatusComponentId);
StatusBarClockToggleSwitch.IsChecked = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock);
_suppressStatusBarToggleEvents = false;
ApplyLocalization();
ThemeColorStatusTextBlock.Text = Lf("settings.color.theme_ready_format", "Theme color ready: {0}.", _selectedThemeColor);

75
README.md Normal file
View File

@@ -0,0 +1,75 @@
# LanMontainDesktop
## 项目简介 / Project Overview
`LanMontainDesktop` 是一个基于 Avalonia 的桌面壳层应用原型,聚焦于网格化桌面布局、毛玻璃视觉、主题色系统与可扩展组件体系。
`LanMontainDesktop` is an Avalonia-based desktop shell prototype focused on grid layout, glass visuals, theme system, and extensible components.
## 主要功能 / Key Features
- 网格化桌面:顶部状态栏 + 底部任务栏Dock 风格容器)。
Grid-based desktop with top status bar and bottom taskbar (dock-like container).
- 设置中心:壁纸、网格、颜色、状态栏、地区(语言)选项。
Settings center with wallpaper, grid, color, status bar, and region (language) tabs.
- 壁纸系统:支持图片与视频壁纸,并提供设置页预览。
Wallpaper system supporting image/video wallpapers with in-settings preview.
- 主题系统日夜模式、主题色、Monet 调色联动。
Theme system with day/night mode, accent color, and Monet palette integration.
- 组件系统基础:内置组件注册 + 第三方扩展入口JSON manifest
Component system foundation with built-in registry and third-party JSON extension entry.
## 技术栈 / Tech Stack
- .NET 10 (`net10.0`)
- Avalonia 11
- FluentAvalonia + FluentIcons.Avalonia
- LibVLCSharp + VideoLAN.LibVLC.Windows视频壁纸
## 环境要求 / Prerequisites
- .NET SDK `10.0`
- Windows当前项目引用 `VideoLAN.LibVLC.Windows`,视频能力以 Windows 为主)
Windows is the primary platform for current video capability due to `VideoLAN.LibVLC.Windows`.
## 快速启动 / Quick Start
```bash
dotnet restore
dotnet build LanMontainDesktop/LanMontainDesktop.csproj
dotnet run --project LanMontainDesktop/LanMontainDesktop.csproj
```
## 配置与持久化 / Configuration & Persistence
应用设置通过 `AppSettingsSnapshot` 持久化到本地:
App settings are persisted from `AppSettingsSnapshot` to local storage:
- 路径 / Path: `%LOCALAPPDATA%\LanMontainDesktop\settings.json`
核心字段(简表)/ Key fields (summary):
- `GridShortSideCells`: 网格短边格子数 / short-side grid cells
- `IsNightMode`: 日夜模式 / day-night mode
- `ThemeColor`: 主题色 / accent color
- `WallpaperPath` + `WallpaperPlacement`: 壁纸路径与显示模式 / wallpaper path and placement
- `SettingsTabIndex`: 设置页当前选项卡 / active settings tab index
- `LanguageCode`: 语言代码(`zh-CN` / `en-US`
- `TopStatusComponentIds`: 顶部状态栏组件 ID 列表 / status bar component IDs
- `PinnedTaskbarActions`: 任务栏固定动作 / pinned taskbar actions
## 组件扩展入口 / Component Extension Entry
- 运行时会扫描:`Extensions/Components/*.json`(相对应用输出目录)
Runtime scan target: `Extensions/Components/*.json` (relative to app output).
- 扩展加载器:`JsonComponentExtensionProvider`
- 详细契约与 schema 见:`LanMontainDesktop/ComponentSystem/README.md`
## 国际化 / Localization
- 语言资源文件:
Localization files:
- `LanMontainDesktop/Localization/zh-CN.json`
- `LanMontainDesktop/Localization/en-US.json`
- 当前支持简体中文、English
## 已知限制(快速版)/ Known Limitations
- 视频壁纸能力当前以 Windows 运行环境为主。
Video wallpaper support is currently Windows-first.
- `docs/VISUAL_SPEC.md` 存在历史编码问题,本次未纳入修复范围。
`docs/VISUAL_SPEC.md` has historical encoding issues and is not updated in this round.
## 许可证与贡献(占位)/ License & Contributing (Placeholder)
- License: TBD
- Contributing guide: TBD