diff --git a/LanMontainDesktop/ComponentSystem/BuiltInComponentIds.cs b/LanMontainDesktop/ComponentSystem/BuiltInComponentIds.cs new file mode 100644 index 0000000..d533e18 --- /dev/null +++ b/LanMontainDesktop/ComponentSystem/BuiltInComponentIds.cs @@ -0,0 +1,6 @@ +namespace LanMontainDesktop.ComponentSystem; + +public static class BuiltInComponentIds +{ + public const string Clock = "Clock"; +} diff --git a/LanMontainDesktop/ComponentSystem/ComponentPlacementRules.cs b/LanMontainDesktop/ComponentSystem/ComponentPlacementRules.cs new file mode 100644 index 0000000..3210c36 --- /dev/null +++ b/LanMontainDesktop/ComponentSystem/ComponentPlacementRules.cs @@ -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); + } +} diff --git a/LanMontainDesktop/ComponentSystem/ComponentRegistry.cs b/LanMontainDesktop/ComponentSystem/ComponentRegistry.cs new file mode 100644 index 0000000..18ffc32 --- /dev/null +++ b/LanMontainDesktop/ComponentSystem/ComponentRegistry.cs @@ -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 _definitions; + + public ComponentRegistry(IEnumerable 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 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 GetAll() + { + return _definitions.Values.OrderBy(d => d.Category).ThenBy(d => d.DisplayName).ToList(); + } +} diff --git a/LanMontainDesktop/ComponentSystem/DesktopComponentDefinition.cs b/LanMontainDesktop/ComponentSystem/DesktopComponentDefinition.cs new file mode 100644 index 0000000..526f1fc --- /dev/null +++ b/LanMontainDesktop/ComponentSystem/DesktopComponentDefinition.cs @@ -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); diff --git a/LanMontainDesktop/ComponentSystem/Extensions/IComponentExtensionProvider.cs b/LanMontainDesktop/ComponentSystem/Extensions/IComponentExtensionProvider.cs new file mode 100644 index 0000000..b76d4a7 --- /dev/null +++ b/LanMontainDesktop/ComponentSystem/Extensions/IComponentExtensionProvider.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace LanMontainDesktop.ComponentSystem.Extensions; + +public interface IComponentExtensionProvider +{ + IReadOnlyList GetComponents(); +} diff --git a/LanMontainDesktop/ComponentSystem/Extensions/JsonComponentExtensionProvider.cs b/LanMontainDesktop/ComponentSystem/Extensions/JsonComponentExtensionProvider.cs new file mode 100644 index 0000000..043a166 --- /dev/null +++ b/LanMontainDesktop/ComponentSystem/Extensions/JsonComponentExtensionProvider.cs @@ -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 _definitions; + + private JsonComponentExtensionProvider(IReadOnlyList definitions) + { + _definitions = definitions; + } + + public IReadOnlyList GetComponents() + { + return _definitions; + } + + public static IReadOnlyList LoadProvidersFromDirectory(string directoryPath) + { + if (string.IsNullOrWhiteSpace(directoryPath) || !Directory.Exists(directoryPath)) + { + return Array.Empty(); + } + + var providers = new List(); + 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>(json); + if (entries is null || entries.Count == 0) + { + return null; + } + + var definitions = new List(); + 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; + } +} diff --git a/LanMontainDesktop/ComponentSystem/README.md b/LanMontainDesktop/ComponentSystem/README.md new file mode 100644 index 0000000..1a27d83 --- /dev/null +++ b/LanMontainDesktop/ComponentSystem/README.md @@ -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. diff --git a/LanMontainDesktop/Extensions/Components/README.txt b/LanMontainDesktop/Extensions/Components/README.txt new file mode 100644 index 0000000..74fa7e9 --- /dev/null +++ b/LanMontainDesktop/Extensions/Components/README.txt @@ -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 diff --git a/LanMontainDesktop/LanMontainDesktop.csproj b/LanMontainDesktop/LanMontainDesktop.csproj index 51db0a2..99e93c0 100644 --- a/LanMontainDesktop/LanMontainDesktop.csproj +++ b/LanMontainDesktop/LanMontainDesktop.csproj @@ -12,6 +12,7 @@ + diff --git a/LanMontainDesktop/Views/MainWindow.ComponentSystem.cs b/LanMontainDesktop/Views/MainWindow.ComponentSystem.cs new file mode 100644 index 0000000..72f0f09 --- /dev/null +++ b/LanMontainDesktop/Views/MainWindow.ComponentSystem.cs @@ -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(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 ResolveDynamicTaskbarActions(TaskbarContext context) + { + if (!_enableDynamicTaskbarActions) + { + return Array.Empty(); + } + + // Reserved for page-specific actions. Disabled by default in this phase. + _ = context; + return Array.Empty(); + } + + private void BuildDynamicTaskbarVisuals(IReadOnlyList 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); + } + } +} diff --git a/LanMontainDesktop/Views/MainWindow.Settings.cs b/LanMontainDesktop/Views/MainWindow.Settings.cs index 517a24a..654a167 100644 --- a/LanMontainDesktop/Views/MainWindow.Settings.cs +++ b/LanMontainDesktop/Views/MainWindow.Settings.cs @@ -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(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 ResolveDynamicTaskbarActions(TaskbarContext context) - { - if (!_enableDynamicTaskbarActions) - { - return Array.Empty(); - } - - // Reserved for page-specific actions. Disabled by default in this phase. - _ = context; - return Array.Empty(); - } - - private void BuildDynamicTaskbarVisuals(IReadOnlyList 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) diff --git a/LanMontainDesktop/Views/MainWindow.axaml.cs b/LanMontainDesktop/Views/MainWindow.axaml.cs index 2c89147..77e8f64 100644 --- a/LanMontainDesktop/Views/MainWindow.axaml.cs +++ b/LanMontainDesktop/Views/MainWindow.axaml.cs @@ -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 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 _topStatusComponentIds = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet _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); diff --git a/README.md b/README.md new file mode 100644 index 0000000..70ba971 --- /dev/null +++ b/README.md @@ -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 +