fead.做桌面组件ing,智教hub加了rinshub

This commit is contained in:
lincube
2026-04-03 01:17:47 +08:00
parent 88bd92e40a
commit 35976c3f3d
12 changed files with 559 additions and 133 deletions

View File

@@ -227,7 +227,7 @@ public partial class App : Application
return;
}
// 确保透明覆盖层窗口存在
// 确保透明覆盖层窗口存在并显示
EnsureTransparentOverlayWindow();
// 打开融合桌面组件库窗口
@@ -235,6 +235,12 @@ public partial class App : Application
{
try
{
// 确保覆盖层窗口已显示(组件要渲染在上面,必须先 Show
if (_transparentOverlayWindow is not null && !_transparentOverlayWindow.IsVisible)
{
_transparentOverlayWindow.Show();
}
var window = new FusedDesktopComponentLibraryWindow();
if (_transparentOverlayWindow is not null)

View File

@@ -124,12 +124,14 @@ public static class ZhiJiaoHubSources
{
public const string ClassIsland = "classisland";
public const string Sectl = "sectl";
public const string RinLit = "rinlit";
public static string Normalize(string? value)
{
return value?.ToLowerInvariant() switch
{
"sectl" => Sectl,
"rinlit" => RinLit,
_ => ClassIsland
};
}

View File

@@ -322,6 +322,8 @@ public sealed record RecommendationApiOptions
public string ClassIslandHubRawUrlTemplate { get; init; } = "https://raw.githubusercontent.com/ClassIsland/classisland-hub/main/images/{0}";
public string SectlHubRawUrlTemplate { get; init; } = "https://raw.githubusercontent.com/SECTL/SECTL-hub/main/images/{0}";
public string RinLitHubRawUrlTemplate { get; init; } = "https://raw.githubusercontent.com/RinLit-233-shiroko/Rin-sHub/main/images/{0}";
}
public interface IRecommendationInfoService

View File

@@ -3247,6 +3247,7 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
var (owner, repo, path) = source switch
{
ZhiJiaoHubSources.Sectl => ("SECTL", "SECTL-hub", "docs/.vuepress/public/images"),
ZhiJiaoHubSources.RinLit => ("RinLit-233-shiroko", "Rin-sHub", "images"),
_ => ("ClassIsland", "classisland-hub", "images")
};

View File

@@ -100,6 +100,9 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
private static readonly Dictionary<IntPtr, bool> _bottomMostWindows = new();
private static readonly Dictionary<IntPtr, IntPtr> _originalWndProcs = new();
private static readonly Dictionary<IntPtr, List<Rect>> _interactiveRegions = new();
// 记录每个窗口的屏幕原点(窗口左上角的屏幕坐标),用于将 WM_NCHITTEST 屏幕坐标转成窗口相对坐标
private static readonly Dictionary<IntPtr, Point> _windowScreenOrigins = new();
private static readonly object _staticLock = new();
public bool IsBottomMostSupported => true;
@@ -121,11 +124,12 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
// 设置为桌面子窗口
SetAsDesktopChild(handle);
// 注册置底状态
// 注册置底状态 & 记录窗口屏幕原点
lock (_staticLock)
{
_bottomMostWindows[handle] = true;
_interactiveRegions[handle] = [];
UpdateWindowScreenOrigin(handle);
}
// 注入消息钩子
@@ -147,6 +151,7 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
_bottomMostWindows.Remove(handle);
_originalWndProcs.Remove(handle);
_interactiveRegions.Remove(handle);
_windowScreenOrigins.Remove(handle);
}
}
};
@@ -220,15 +225,20 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
// 处理 WM_NCHITTEST - 区域级穿透
if (msg == WM_NCHITTEST)
{
// 从 lParam 解析坐标(低字为 X高字为 Y
var x = (short)(wParam.ToInt32() & 0xFFFF);
var y = (short)((wParam.ToInt32() >> 16) & 0xFFFF);
var point = new Point(x, y);
// WM_NCHITTEST 的鼠标坐标在 lParam低16位=X高16位=Y且为屏幕坐标
var screenX = (short)(lParam.ToInt64() & 0xFFFF);
var screenY = (short)((lParam.ToInt64() >> 16) & 0xFFFF);
lock (_staticLock)
{
if (_interactiveRegions.TryGetValue(hWnd, out var regions))
if (_interactiveRegions.TryGetValue(hWnd, out var regions) && regions.Count > 0)
{
// 将屏幕坐标转为窗口相对坐标_interactiveRegions 存的是窗口内坐标)
_windowScreenOrigins.TryGetValue(hWnd, out var origin);
var clientX = screenX - origin.X;
var clientY = screenY - origin.Y;
var point = new Point(clientX, clientY);
foreach (var region in regions)
{
if (region.Contains(point))
@@ -265,9 +275,28 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
lock (_staticLock)
{
_interactiveRegions[handle] = regions;
// 同步刷新屏幕原点DPI 缩放可能影响坐标,每次更新区域时一并刷新)
UpdateWindowScreenOrigin(handle);
}
}
/// <summary>
/// 更新指定窗口的屏幕左上角坐标缓存(用于将 WM_NCHITTEST 屏幕坐标转为窗口相对坐标)
/// </summary>
private static void UpdateWindowScreenOrigin(IntPtr handle)
{
if (GetWindowRect(handle, out var rect))
{
_windowScreenOrigins[handle] = new Point(rect.Left, rect.Top);
}
}
[StructLayout(LayoutKind.Sequential)]
private struct RECT { public int Left, Top, Right, Bottom; }
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
private delegate IntPtr WndProcDelegate(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]

View File

@@ -21,6 +21,9 @@
<ComboBoxItem x:Name="SectlItem"
Classes="component-editor-select-item"
Tag="sectl" />
<ComboBoxItem x:Name="RinLitItem"
Classes="component-editor-select-item"
Tag="rinlit" />
</ComboBox>
<TextBlock x:Name="SourceDescriptionTextBlock"
Classes="component-editor-secondary-text"

View File

@@ -29,10 +29,11 @@ public partial class ZhiJiaoHubComponentEditor : ComponentEditorViewBase
SourceLabelTextBlock.Text = L("zhijiaohub.settings.source", "图片源");
ClassIslandItem.Content = L("zhijiaohub.settings.classisland", "ClassIsland 图库");
SectlItem.Content = L("zhijiaohub.settings.sectl", "SECTL 图库");
RinLitItem.Content = L("zhijiaohub.settings.rinlit", "Rin's 图库");
// 数据源描述
SourceDescriptionTextBlock.Text = L("zhijiaohub.settings.source_desc",
"选择图片来源。ClassIsland 图库包含 ClassIsland 社区的趣味瞬间SECTL 图库包含 SECTL 社区的内容。");
"选择图片来源。ClassIsland 图库包含 ClassIsland 社区的趣味瞬间SECTL 图库包含 SECTL 社区的内容Rin's 图库包含 Rin's 社区的内容。");
// 镜像加速源
MirrorSourceLabelTextBlock.Text = L("zhijiaohub.settings.mirror_source", "镜像加速");
@@ -65,6 +66,7 @@ public partial class ZhiJiaoHubComponentEditor : ComponentEditorViewBase
SourceComboBox.SelectedItem = source switch
{
ZhiJiaoHubSources.Sectl => SectlItem,
ZhiJiaoHubSources.RinLit => RinLitItem,
_ => ClassIslandItem
};

View File

@@ -1,30 +1,113 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryControl"
Width="400" Height="500">
<!--
融合桌面组件库 - 专门用于添加组件到系统桌面(负一屏)
注意:此窗口只能添加组件到融合桌面,不能添加到阑山桌面
-->
<Grid RowDefinitions="Auto,*">
<!-- 标题栏 -->
<Border Background="{DynamicResource SystemControlBackgroundChromeMediumBrush}"
Padding="16,12">
<StackPanel>
<TextBlock Text="融合桌面组件"
FontWeight="SemiBold"
FontSize="16" />
<TextBlock Text="选择组件添加到系统桌面"
Opacity="0.7"
FontSize="12"
Margin="0,4,0,0" />
</StackPanel>
xmlns:fi="using:FluentIcons.Avalonia"
xmlns:local="using:LanMountainDesktop.Views"
x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryControl">
<Grid ColumnDefinitions="220,*">
<!-- 分类列表 (左侧) -->
<Border Grid.Column="0"
BorderBrush="{DynamicResource AdaptiveBorderBrush}"
BorderThickness="0,0,1,0"
Padding="12,0,12,12">
<Grid RowDefinitions="Auto,*">
<TextBox x:Name="SearchBox"
Watermark="搜索组件..."
Margin="0,0,0,12"
Classes="clear"
Background="{DynamicResource AdaptiveSurfaceLowBrush}"
CornerRadius="12"
Padding="12,8">
<TextBox.InnerLeftContent>
<fi:SymbolIcon Symbol="Search" FontSize="16" Margin="10,0,0,0" Opacity="0.5" />
</TextBox.InnerLeftContent>
</TextBox>
<ListBox x:Name="CategoryListBox"
Grid.Row="1"
Background="Transparent"
BorderThickness="0"
SelectionChanged="OnCategorySelectionChanged">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="CornerRadius" Value="14" />
<Setter Property="Margin" Value="0,2" />
<Setter Property="Padding" Value="12,10" />
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate x:DataType="local:LibraryCategoryItem">
<StackPanel Orientation="Horizontal" Spacing="12">
<fi:SymbolIcon Symbol="{Binding Icon}" FontSize="18" />
<TextBlock Text="{Binding DisplayName}" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Border>
<!-- 组件列表 -->
<ScrollViewer Grid.Row="1"
Padding="12">
<WrapPanel x:Name="ComponentPanel" Orientation="Horizontal" />
<!-- 组件网格 (右侧) -->
<ScrollViewer Grid.Column="1" Padding="20">
<ItemsControl x:Name="ComponentItemsControl">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="local:LibraryComponentItem">
<Button Classes="unstyled-card"
Width="260"
Margin="0,0,16,16"
Padding="0"
Background="Transparent"
BorderThickness="0"
Click="OnAddComponentClick"
Tag="{Binding Id}">
<Border Classes="card"
Background="{DynamicResource AdaptiveSurfaceLowBrush}"
BorderBrush="{DynamicResource AdaptiveBorderBrush}"
BorderThickness="1"
CornerRadius="24"
ClipToBounds="True">
<Grid RowDefinitions="Auto,Auto">
<!-- 预览区域 (动态填充预览) -->
<Border x:Name="PreviewHost"
Height="150"
Background="{DynamicResource AdaptiveSurfaceNeutralBrush}"
Margin="8"
CornerRadius="16"
ClipToBounds="True">
<Panel>
<!-- 这里将显示组件的缩放预览 -->
<ContentPresenter Content="{Binding PreviewContent}" />
<!-- 空状态或加载中图标 -->
<fi:SymbolIcon Symbol="Cube"
FontSize="32"
Opacity="0.1"
IsVisible="{Binding !HasPreview}" />
</Panel>
</Border>
<!-- 文字说明 -->
<StackPanel Grid.Row="1" Margin="16,8,16,16" Spacing="4">
<TextBlock Text="{Binding DisplayName}"
FontWeight="SemiBold"
FontSize="15" />
<TextBlock Text="{Binding Description}"
Opacity="0.6"
FontSize="12"
TextWrapping="Wrap"
MaxLines="2" />
</StackPanel>
</Grid>
</Border>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</UserControl>

View File

@@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
@@ -7,72 +10,167 @@ using Avalonia.Layout;
using Avalonia.Media;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Views.Components;
using LanMountainDesktop.Models;
namespace LanMountainDesktop.Views;
/// <summary>
/// 融合桌面组件库控件 - 专门用于添加组件到系统桌面(负一屏)
/// </summary>
public partial class FusedDesktopComponentLibraryControl : UserControl
{
/// <summary>
/// 添加组件到融合桌面事件
/// </summary>
public event EventHandler<string>? AddComponentRequested;
private readonly ObservableCollection<LibraryCategoryItem> _categories = new();
private readonly ObservableCollection<LibraryComponentItem> _components = new();
private List<DesktopComponentDefinition> _allDefinitions = new();
private ComponentRegistry? _componentRegistry;
private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry;
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
private readonly IWeatherInfoService _weatherDataService;
private readonly TimeZoneService _timeZoneService;
private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService();
private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService();
public FusedDesktopComponentLibraryControl()
{
InitializeComponent();
LoadComponents();
}
_weatherDataService = _settingsFacade.Weather.GetWeatherInfoService();
_timeZoneService = _settingsFacade.Region.GetTimeZoneService();
/// <summary>
/// 加载可用组件列表
/// </summary>
private void LoadComponents()
{
var registry = ComponentRegistry.CreateDefault();
CategoryListBox.ItemsSource = _categories;
ComponentItemsControl.ItemsSource = _components;
foreach (var definition in registry.GetAll())
LoadRegistry();
LoadCategories();
SearchBox.KeyUp += (s, e) => FilterComponents();
// 默认选择第一个分类
if (_categories.Count > 0)
{
if (!definition.AllowDesktopPlacement)
{
continue;
}
var button = new Button
{
Width = 100,
Height = 100,
Margin = new Thickness(4),
Padding = new Thickness(8),
CornerRadius = new CornerRadius(12),
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Tag = definition.Id
};
var textBlock = new TextBlock
{
Text = definition.DisplayName,
FontSize = 11,
TextAlignment = TextAlignment.Center,
TextTrimming = TextTrimming.CharacterEllipsis,
MaxLines = 2,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
button.Content = textBlock;
button.Click += OnAddComponentClick;
ComponentPanel.Children.Add(button);
CategoryListBox.SelectedIndex = 0;
}
}
private void LoadRegistry()
{
var pluginRuntimeService = (Application.Current as App)?.PluginRuntimeService;
_componentRegistry = DesktopComponentRegistryFactory.Create(pluginRuntimeService);
_componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry(
_componentRegistry,
pluginRuntimeService,
_settingsFacade);
_allDefinitions = _componentRegistry.GetAll()
.Where(d => d.AllowDesktopPlacement)
.ToList();
}
private void LoadCategories()
{
_categories.Clear();
_categories.Add(new LibraryCategoryItem("all", "全部组件", "Apps"));
var categoryMap = new Dictionary<string, (string Display, string Icon)>
{
{ "clock", ("时钟", "Clock") },
{ "date", ("日历", "Calendar") },
{ "weather", ("天气", "WeatherCloudy") },
{ "info", ("资讯", "News") },
{ "calculator", ("工具", "Calculator") },
{ "study", ("学习", "Book") },
{ "file", ("文件", "Document") }
};
var usedCategories = _allDefinitions
.Select(d => d.Category)
.Distinct()
.Where(c => !string.IsNullOrEmpty(c));
foreach (var cat in usedCategories)
{
if (categoryMap.TryGetValue(cat.ToLower(), out var info))
{
_categories.Add(new LibraryCategoryItem(cat, info.Display, info.Icon));
}
else
{
_categories.Add(new LibraryCategoryItem(cat, cat, "Cube"));
}
}
}
private void OnCategorySelectionChanged(object? sender, SelectionChangedEventArgs e)
{
FilterComponents();
}
private void FilterComponents()
{
var selectedCategory = (CategoryListBox.SelectedItem as LibraryCategoryItem)?.Id;
var searchText = SearchBox.Text?.ToLower() ?? "";
var filtered = _allDefinitions.Where(d =>
{
var matchesCategory = selectedCategory == "all" || string.Equals(d.Category, selectedCategory, StringComparison.OrdinalIgnoreCase);
var matchesSearch = string.IsNullOrEmpty(searchText) || d.DisplayName.ToLower().Contains(searchText) || d.Id.ToLower().Contains(searchText);
return matchesCategory && matchesSearch;
});
_components.Clear();
foreach (var def in filtered)
{
_components.Add(new LibraryComponentItem
{
Id = def.Id,
DisplayName = def.DisplayName,
Description = GetDescription(def.Id),
PreviewContent = CreatePreview(def.Id)
});
}
}
private string GetDescription(string id)
{
// 简单映射描述信息
return id.Contains("clock") ? "实时显示当前时间与日期。" :
id.Contains("weather") ? "为您提供精准的天气预报。" :
"多功能桌面组件,提升您的操作效率。";
}
private Control? CreatePreview(string id)
{
if (_componentRuntimeRegistry == null || !_componentRuntimeRegistry.TryGetDescriptor(id, out var descriptor))
{
return null;
}
try
{
var control = descriptor.CreateControl(
100, // Previews assume 100px base
_timeZoneService,
_weatherDataService,
_recommendationInfoService,
_calculatorDataService,
_settingsFacade,
"preview_" + id);
control.IsHitTestVisible = false;
return new Viewbox
{
Child = control,
Stretch = Stretch.Uniform,
Margin = new Thickness(12)
};
}
catch (Exception ex)
{
return new TextBlock { Text = "无法预览", VerticalAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center };
}
}
/// <summary>
/// 添加组件按钮点击
/// </summary>
private void OnAddComponentClick(object? sender, RoutedEventArgs e)
{
if (sender is Button button && button.Tag is string componentId)
@@ -81,3 +179,15 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
}
}
}
public record LibraryCategoryItem(string Id, string DisplayName, string Icon);
public class LibraryComponentItem
{
public string Id { get; set; } = "";
public string DisplayName { get; set; } = "";
public string Description { get; set; } = "";
public Control? PreviewContent { get; set; }
public bool HasPreview => PreviewContent != null;
}

View File

@@ -1,41 +1,57 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:LanMountainDesktop.Views"
xmlns:fi="using:FluentIcons.Avalonia"
x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryWindow"
Width="420" Height="560"
MinWidth="380" MinHeight="400"
Width="860" Height="620"
MinWidth="600" MinHeight="500"
WindowStartupLocation="CenterScreen"
CanResize="True"
Title="融合桌面组件">
<Grid RowDefinitions="Auto,*">
<!-- 标题栏 -->
<Border Background="{DynamicResource SystemControlBackgroundChromeMediumBrush}"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="0,0,0,1"
Padding="16,12">
<Grid ColumnDefinitions="*,Auto">
<StackPanel>
<TextBlock Text="融合桌面设置"
FontWeight="SemiBold"
FontSize="18" />
<TextBlock Text="选择组件添加到系统桌面(负一屏)"
Opacity="0.7"
FontSize="12"
Margin="0,4,0,0" />
</StackPanel>
<Button Grid.Column="1"
VerticalAlignment="Center"
CornerRadius="20"
Padding="8"
Click="OnCloseClick">
<TextBlock Text="✕"
FontSize="16" />
</Button>
</Grid>
</Border>
SystemDecorations="Full"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="NoChrome"
ExtendClientAreaTitleBarHeightHint="-1"
Background="Transparent"
TransparencyLevelHint="Mica"
Title="融合桌面组件库">
<!-- 组件库控件 -->
<controls:FusedDesktopComponentLibraryControl x:Name="LibraryControl"
Grid.Row="1" />
</Grid>
<Panel>
<!-- 背景磨砂效果 -->
<Border Background="{DynamicResource AdaptiveSurfaceLowBrush}"
Opacity="0.85" />
<Grid RowDefinitions="Auto,*">
<!-- 自定义标题栏 -->
<Border Background="Transparent"
IsHitTestVisible="True"
Padding="20,16">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Spacing="6" VerticalAlignment="Center">
<TextBlock Text="融合桌面组件库"
FontWeight="SemiBold"
FontSize="20"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock Text="将精美组件放置在您的系统桌面上(负一屏)"
Opacity="0.6"
FontSize="13"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
</StackPanel>
<Button Grid.Column="1"
Classes="accent"
Width="36" Height="36"
Padding="0"
CornerRadius="18"
BorderThickness="0"
Background="{DynamicResource AdaptiveButtonHoverBackgroundBrush}"
Click="OnCloseClick">
<fi:SymbolIcon Symbol="Dismiss" FontSize="18" />
</Button>
</Grid>
</Border>
<!-- 组件库控件 -->
<controls:FusedDesktopComponentLibraryControl x:Name="LibraryControl"
Grid.Row="1" />
</Grid>
</Panel>
</Window>

View File

@@ -1,7 +1,9 @@
using System;
using Avalonia.Controls;
using Avalonia.Interactivity;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.Views;
@@ -13,8 +15,12 @@ namespace LanMountainDesktop.Views;
public partial class FusedDesktopComponentLibraryWindow : Window
{
private readonly IFusedDesktopLayoutService _layoutService = FusedDesktopLayoutServiceProvider.GetOrCreate();
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
private TransparentOverlayWindow? _overlayWindow;
// 与 TransparentOverlayWindow 保持一致的默认 cellSize
private const double DefaultCellSize = 100;
public FusedDesktopComponentLibraryWindow()
{
InitializeComponent();
@@ -31,7 +37,7 @@ public partial class FusedDesktopComponentLibraryWindow : Window
}
/// <summary>
/// 添加组件请求处理
/// 添加组件请求处理 - 将组件放置在屏幕(覆盖层画布)中央
/// </summary>
private void OnAddComponentRequested(object? sender, string componentId)
{
@@ -41,19 +47,52 @@ public partial class FusedDesktopComponentLibraryWindow : Window
return;
}
// 在屏幕中央添加组件
var screenBounds = _overlayWindow.Bounds;
var x = screenBounds.Width / 2 - 100; // 居中
var y = screenBounds.Height / 2 - 100;
// 计算组件的像素尺寸
var (componentWidth, componentHeight) = ResolveComponentSize(componentId);
_overlayWindow.AddComponent(componentId, x, y, 200, 200);
// 取覆盖层画布的中心点,减去组件半尺寸,使组件出现在屏幕正中央
var overlayBounds = _overlayWindow.Bounds;
var centerX = overlayBounds.Width / 2.0 - componentWidth / 2.0;
var centerY = overlayBounds.Height / 2.0 - componentHeight / 2.0;
AppLogger.Info("FusedDesktopLibrary", $"Added component {componentId} to fused desktop.");
// 边界保护:确保组件不超出屏幕边界
centerX = Math.Max(0, Math.Min(centerX, overlayBounds.Width - componentWidth));
centerY = Math.Max(0, Math.Min(centerY, overlayBounds.Height - componentHeight));
_overlayWindow.AddComponent(componentId, centerX, centerY, componentWidth, componentHeight);
AppLogger.Info("FusedDesktopLibrary",
$"Added component '{componentId}' at center ({centerX:F0}, {centerY:F0}) size ({componentWidth}x{componentHeight}).");
// 关闭窗口
Close();
}
/// <summary>
/// 解析组件的默认像素尺寸(基于组件定义的 MinCells * DefaultCellSize
/// </summary>
private (double Width, double Height) ResolveComponentSize(string componentId)
{
try
{
var pluginRuntimeService = (Application.Current as App)?.PluginRuntimeService;
var registry = DesktopComponentRegistryFactory.Create(pluginRuntimeService);
if (registry.TryGetDefinition(componentId, out var definition))
{
var w = Math.Max(1, definition.MinWidthCells) * DefaultCellSize;
var h = Math.Max(1, definition.MinHeightCells) * DefaultCellSize;
return (w, h);
}
}
catch (Exception ex)
{
AppLogger.Warn("FusedDesktopLibrary", $"Failed to resolve component size for '{componentId}'.", ex);
}
// 回退为 2×2 格子的默认尺寸
return (DefaultCellSize * 2, DefaultCellSize * 2);
}
private void OnCloseClick(object? sender, RoutedEventArgs e)
{
Close();

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
@@ -10,6 +11,8 @@ using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Views.Components;
namespace LanMountainDesktop.Views;
@@ -41,6 +44,17 @@ public partial class TransparentOverlayWindow : Window
private readonly Dictionary<string, Border> _componentHosts = [];
private readonly List<Rect> _interactiveRegions = [];
private FusedDesktopLayoutSnapshot _layout = new();
private ComponentRegistry? _componentRegistry;
private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry;
// 基础服务
private readonly IWeatherInfoService _weatherDataService;
private readonly TimeZoneService _timeZoneService;
private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService();
private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService();
// 渲染参数
private const double DefaultCellSize = 100;
// 拖拽状态
private bool _isDragging;
@@ -53,6 +67,8 @@ public partial class TransparentOverlayWindow : Window
public TransparentOverlayWindow()
{
InitializeComponent();
_weatherDataService = _settingsFacade.Weather.GetWeatherInfoService();
_timeZoneService = _settingsFacade.Region.GetTimeZoneService();
// 仅在 Windows 上启用置底功能
if (OperatingSystem.IsWindows())
@@ -70,12 +86,54 @@ public partial class TransparentOverlayWindow : Window
_bottomMostService.SendToBottom(this);
}
// 加载布局
// 确保注册表已初始化
EnsureRegistries();
// 加载布局并渲染
_layout = _layoutService.Load();
RenderAllComponents();
// TODO: 渲染组件(需要从 MainWindow 获取组件注册表)
AppLogger.Info("TransparentOverlay", $"Opened with {_layout.ComponentPlacements.Count} components.");
}
AppLogger.Info("TransparentOverlay", "Transparent overlay window opened.");
/// <summary>
/// 确保组件运行时注册表已初始化
/// </summary>
private void EnsureRegistries()
{
if (_componentRuntimeRegistry is not null) return;
var pluginRuntimeService = (Application.Current as App)?.PluginRuntimeService;
_componentRegistry = DesktopComponentRegistryFactory.Create(pluginRuntimeService);
_componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry(
_componentRegistry,
pluginRuntimeService,
_settingsFacade);
}
/// <summary>
/// 渲染所有布局中的组件
/// </summary>
private void RenderAllComponents()
{
if (Content is not Canvas canvas) return;
canvas.Children.Clear();
_componentHosts.Clear();
foreach (var placement in _layout.ComponentPlacements)
{
try
{
RenderComponentInternal(placement);
}
catch (Exception ex)
{
AppLogger.Warn("TransparentOverlay", $"Failed to render component {placement.ComponentId}", ex);
}
}
UpdateInteractiveRegions();
}
protected override void OnClosed(EventArgs e)
@@ -112,8 +170,20 @@ public partial class TransparentOverlayWindow : Window
/// <summary>
/// 添加组件(供外部调用)
/// </summary>
public void AddComponent(string componentId, double x, double y, double width = 200, double height = 200)
public void AddComponent(string componentId, double x, double y, double? width = null, double? height = null)
{
EnsureRegistries();
if (_componentRegistry == null || !_componentRegistry.TryGetDefinition(componentId, out var definition))
{
AppLogger.Warn("TransparentOverlay", $"Cannot add unknown component: {componentId}");
return;
}
// 解析尺寸:如果未提供,则使用组件定义的最小尺寸 * 100
var finalWidth = width ?? (definition.MinWidthCells * DefaultCellSize);
var finalHeight = height ?? (definition.MinHeightCells * DefaultCellSize);
var placementId = Guid.NewGuid().ToString("N");
var placement = new FusedDesktopComponentPlacementSnapshot
{
@@ -121,16 +191,49 @@ public partial class TransparentOverlayWindow : Window
ComponentId = componentId,
X = x,
Y = y,
Width = width,
Height = height,
Width = finalWidth,
Height = finalHeight,
ZIndex = _layout.ComponentPlacements.Count
};
_layout.ComponentPlacements.Add(placement);
UpdateInteractiveRegions();
SaveLayout();
AppLogger.Info("TransparentOverlay", $"Added component: {componentId} at ({x}, {y})");
// 立即渲染
try
{
RenderComponentInternal(placement);
UpdateInteractiveRegions();
SaveLayout();
AppLogger.Info("TransparentOverlay", $"Added component: {componentId} at ({x}, {y}) size ({finalWidth}x{finalHeight})");
}
catch (Exception ex)
{
AppLogger.Warn("TransparentOverlay", $"Failed to add component {componentId}", ex);
_layout.ComponentPlacements.Remove(placement);
}
}
/// <summary>
/// 内部渲染单个组件
/// </summary>
private void RenderComponentInternal(FusedDesktopComponentPlacementSnapshot placement)
{
if (_componentRuntimeRegistry is null || !_componentRuntimeRegistry.TryGetDescriptor(placement.ComponentId, out var descriptor))
{
AppLogger.Warn("TransparentOverlay", $"Unknown component: {placement.ComponentId}");
return;
}
var control = descriptor.CreateControl(
DefaultCellSize,
_timeZoneService,
_weatherDataService,
_recommendationInfoService,
_calculatorDataService,
_settingsFacade,
placement.PlacementId);
RenderComponent(placement.PlacementId, control, placement.X, placement.Y, placement.Width, placement.Height);
}
/// <summary>
@@ -176,6 +279,9 @@ public partial class TransparentOverlayWindow : Window
host.PointerMoved += OnComponentPointerMoved;
host.PointerReleased += OnComponentPointerReleased;
// 右键上下文菜单(删除组件)
host.ContextRequested += OnComponentContextRequested;
if (Content is Canvas canvas)
{
canvas.Children.Add(host);
@@ -185,6 +291,33 @@ public partial class TransparentOverlayWindow : Window
UpdateInteractiveRegions();
}
// 组件右键上下文菜单(删除)
private void OnComponentContextRequested(object? sender, ContextRequestedEventArgs e)
{
if (sender is not Border host || host.Tag is not string placementId) return;
// 构建上下文菜单
var deleteItem = new MenuItem
{
Header = "移除组件",
Icon = new Avalonia.Controls.TextBlock { Text = "🗑" }
};
deleteItem.Click += (_, _) =>
{
RemoveComponent(placementId);
AppLogger.Info("TransparentOverlay", $"Component removed via context menu: {placementId}");
};
var menu = new ContextMenu
{
Items = { deleteItem }
};
// 显示在当前控件上
menu.Open(host);
e.Handled = true;
}
// 组件拖拽处理
private void OnComponentPointerPressed(object? sender, PointerPressedEventArgs e)
{