mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
fead.做桌面组件ing,智教hub加了rinshub
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
};
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载可用组件列表
|
||||
/// </summary>
|
||||
private void LoadComponents()
|
||||
{
|
||||
var registry = ComponentRegistry.CreateDefault();
|
||||
_weatherDataService = _settingsFacade.Weather.GetWeatherInfoService();
|
||||
_timeZoneService = _settingsFacade.Region.GetTimeZoneService();
|
||||
|
||||
foreach (var definition in registry.GetAll())
|
||||
CategoryListBox.ItemsSource = _categories;
|
||||
ComponentItemsControl.ItemsSource = _components;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加组件按钮点击
|
||||
/// </summary>
|
||||
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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="融合桌面组件库">
|
||||
|
||||
<Panel>
|
||||
<!-- 背景磨砂效果 -->
|
||||
<Border Background="{DynamicResource AdaptiveSurfaceLowBrush}"
|
||||
Opacity="0.85" />
|
||||
|
||||
<!-- 组件库控件 -->
|
||||
<controls:FusedDesktopComponentLibraryControl x:Name="LibraryControl"
|
||||
Grid.Row="1" />
|
||||
</Grid>
|
||||
<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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保组件运行时注册表已初始化
|
||||
/// </summary>
|
||||
private void EnsureRegistries()
|
||||
{
|
||||
if (_componentRuntimeRegistry is not null) return;
|
||||
|
||||
AppLogger.Info("TransparentOverlay", "Transparent overlay window opened.");
|
||||
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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user