fead.桌面组件

This commit is contained in:
lincube
2026-04-03 11:42:00 +08:00
parent 35976c3f3d
commit 44b87ba12e
12 changed files with 752 additions and 300 deletions

View File

@@ -149,6 +149,11 @@ public partial class App : Application
LinuxDesktopEntryInstaller.EnsureInstalled();
DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell);
if (!Design.IsDesignMode && OperatingSystem.IsWindows())
{
FusedDesktopManagerServiceFactory.GetOrCreate().Initialize();
}
base.OnFrameworkInitializationCompleted();
}
@@ -226,6 +231,9 @@ public partial class App : Application
AppLogger.Warn("FusedDesktop", "Fused desktop is only supported on Windows.");
return;
}
// 切换进入编辑模式,隐藏常态零散的小部件
FusedDesktopManagerServiceFactory.GetOrCreate().EnterEditMode();
// 确保透明覆盖层窗口存在并显示
EnsureTransparentOverlayWindow();
@@ -248,6 +256,19 @@ public partial class App : Application
window.SetOverlayWindow(_transparentOverlayWindow);
}
// 当组件库关闭时,退出编辑态
window.Closed += (s, ev) =>
{
if (_transparentOverlayWindow is not null)
{
// 触发画布保存,并隐藏画布
_transparentOverlayWindow.SaveLayoutAndHide();
}
// 让管理器根据已存储的最新快照重建生成所有实体小组件
FusedDesktopManagerServiceFactory.GetOrCreate().ExitEditMode();
};
window.Show();
window.Activate();
}

View File

@@ -0,0 +1,195 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Views;
using LanMountainDesktop.Views.Components;
namespace LanMountainDesktop.Services;
/// <summary>
/// 融合桌面中央管理器服务接口
/// </summary>
public interface IFusedDesktopManagerService
{
void Initialize();
void EnterEditMode();
void ExitEditMode();
void ReloadWidgets();
}
/// <summary>
/// 融合桌面中央管理器服务实现。用于管理常态下的各个小窗口实体。
/// </summary>
internal sealed class FusedDesktopManagerService : IFusedDesktopManagerService
{
private readonly IFusedDesktopLayoutService _layoutService;
private readonly ISettingsFacadeService _settingsFacade;
private readonly Dictionary<string, DesktopWidgetWindow> _widgetWindows = [];
// 基础服务依赖
private readonly IWeatherInfoService _weatherDataService;
private readonly TimeZoneService _timeZoneService;
private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService();
private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService();
private ComponentRegistry? _componentRegistry;
private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry;
private bool _isEditMode;
private const double DefaultCellSize = 100;
public FusedDesktopManagerService(
IFusedDesktopLayoutService layoutService,
ISettingsFacadeService settingsFacade)
{
_layoutService = layoutService;
_settingsFacade = settingsFacade;
_weatherDataService = _settingsFacade.Weather.GetWeatherInfoService();
_timeZoneService = _settingsFacade.Region.GetTimeZoneService();
}
public void Initialize()
{
if (!OperatingSystem.IsWindows()) return;
EnsureRegistries();
ReloadWidgets();
}
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);
}
public void EnterEditMode()
{
if (_isEditMode) return;
_isEditMode = true;
// 隐藏所有底层小窗口
foreach (var window in _widgetWindows.Values)
{
window.Hide();
}
}
public void ExitEditMode()
{
if (!_isEditMode) return;
_isEditMode = false;
// 编辑完成,重新加载布局(可能已发生更改)并显示
ReloadWidgets();
}
public void ReloadWidgets()
{
if (_isEditMode) return; // 编辑模式下不渲染小窗口
var layout = _layoutService.Load();
var existingIds = new HashSet<string>(_widgetWindows.Keys);
foreach (var placement in layout.ComponentPlacements)
{
existingIds.Remove(placement.PlacementId);
if (_widgetWindows.TryGetValue(placement.PlacementId, out var existingWindow))
{
// 已存在,可能只更新位置或尺寸
existingWindow.Position = new Avalonia.PixelPoint((int)placement.X, (int)placement.Y);
if (existingWindow.IsVisible == false)
{
existingWindow.Show();
}
}
else
{
// 新组件,生成窗口
try
{
var window = CreateWidgetWindow(placement);
if (window != null)
{
_widgetWindows[placement.PlacementId] = window;
window.Show();
window.Position = new Avalonia.PixelPoint((int)placement.X, (int)placement.Y);
}
}
catch (Exception ex)
{
AppLogger.Warn("FusedDesktopMgr", $"Failed to render tiny window for {placement.ComponentId}", ex);
}
}
}
// 移除被删除的组件
foreach (var id in existingIds)
{
if (_widgetWindows.Remove(id, out var windowToRemove))
{
windowToRemove.Close();
}
}
}
private DesktopWidgetWindow? CreateWidgetWindow(FusedDesktopComponentPlacementSnapshot placement)
{
EnsureRegistries();
if (_componentRuntimeRegistry is null || !_componentRuntimeRegistry.TryGetDescriptor(placement.ComponentId, out var descriptor))
{
AppLogger.Warn("FusedDesktopMgr", $"Unknown component: {placement.ComponentId}");
return null;
}
var control = descriptor.CreateControl(
DefaultCellSize,
_timeZoneService,
_weatherDataService,
_recommendationInfoService,
_calculatorDataService,
_settingsFacade,
placement.PlacementId);
// 将组件包装到一个具有准确宽高的容器内(如果组件自身没有设置宽度)
control.Width = placement.Width;
control.Height = placement.Height;
var window = new DesktopWidgetWindow(control);
return window;
}
}
/// <summary>
/// 工厂
/// </summary>
public static class FusedDesktopManagerServiceFactory
{
private static IFusedDesktopManagerService? _instance;
private static readonly object _lock = new();
public static IFusedDesktopManagerService GetOrCreate()
{
if (_instance is not null) return _instance;
lock (_lock)
{
var layoutService = FusedDesktopLayoutServiceProvider.GetOrCreate();
var settings = HostSettingsFacadeProvider.GetOrCreate();
_instance ??= new FusedDesktopManagerService(layoutService, settings);
return _instance;
}
}
}

View File

@@ -220,7 +220,7 @@ public partial class ComponentLibraryWindow : Window
if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase))
{
return Symbol.Apps;
return Symbol.Info;
}
if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase))

View File

@@ -0,0 +1,23 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="LanMountainDesktop.Views.DesktopWidgetWindow"
Title="Desktop Component"
ShowInTaskbar="False"
SystemDecorations="None"
Background="Transparent"
Topmost="False"
SizeToContent="WidthAndHeight"
TransparencyLevelHint="Transparent"
RenderOptions.BitmapInterpolationMode="HighQuality"
CanResize="False">
<Border x:Name="ComponentContainer"
Background="Transparent"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
ClipToBounds="True">
<!-- Component control will be injected here -->
</Border>
</Window>

View File

@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
using LanMountainDesktop.Services;
using Avalonia.Threading;
namespace LanMountainDesktop.Views;
/// <summary>
/// 表示一个独立的组件挂载窗口。它不含有任何自己的边窗,仅仅负责包裹组件并将自身植入系统最底层。
/// </summary>
public partial class DesktopWidgetWindow : Window
{
private readonly IWindowBottomMostService _bottomMostService = WindowBottomMostServiceFactory.GetOrCreate();
private readonly IRegionPassthroughService _regionPassthroughService = RegionPassthroughServiceFactory.GetOrCreate();
public DesktopWidgetWindow()
{
InitializeComponent();
}
public DesktopWidgetWindow(Control componentContent) : this()
{
ComponentContainer.Child = componentContent;
}
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
if (OperatingSystem.IsWindows())
{
// 通过现有的置底服务将独立的小窗口锁定到底层
_bottomMostService.SetupBottomMost(this);
_bottomMostService.SendToBottom(this);
// 当窗口展示完毕且有了尺寸后,更新可交互区域,使得整个组件都能被点击
Dispatcher.UIThread.Post(UpdateInteractiveRegion, DispatcherPriority.Render);
}
}
protected override void OnSizeChanged(SizeChangedEventArgs e)
{
base.OnSizeChanged(e);
if (OperatingSystem.IsWindows() && IsVisible)
{
UpdateInteractiveRegion();
}
}
private void UpdateInteractiveRegion()
{
// 既然是一个完全紧贴在组件身上的小窗,它的全部都是可交互的
_regionPassthroughService.SetInteractiveRegions(this, new List<Rect>
{
new(0, 0, Bounds.Width, Bounds.Height)
});
}
}

View File

@@ -1,15 +1,17 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LanMountainDesktop.ViewModels"
xmlns:fi="using:FluentIcons.Avalonia"
xmlns:local="using:LanMountainDesktop.Views"
x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryControl">
x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryControl"
x:DataType="vm:ComponentLibraryWindowViewModel">
<Grid ColumnDefinitions="220,*">
<Grid ColumnDefinitions="240,*"
ColumnSpacing="12"
Margin="0">
<!-- 分类列表 (左侧) -->
<Border Grid.Column="0"
BorderBrush="{DynamicResource AdaptiveBorderBrush}"
BorderThickness="0,0,1,0"
Padding="12,0,12,12">
<Border Classes="surface-translucent-panel"
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
Padding="10">
<Grid RowDefinitions="Auto,*">
<TextBox x:Name="SearchBox"
Watermark="搜索组件..."
@@ -27,87 +29,132 @@
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>
SelectionChanged="OnCategorySelectionChanged"
ItemsSource="{Binding Categories}">
<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 x:DataType="vm:ComponentLibraryCategoryViewModel">
<Border Padding="10"
Margin="0,0,0,6"
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
Background="{DynamicResource AdaptiveNavItemBackgroundBrush}">
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<fi:SymbolIcon Symbol="{Binding Icon}"
IconVariant="Regular"
FontSize="16" />
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="{Binding Title}" />
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Border>
<!-- 组件网格 (右侧) -->
<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 Grid.Column="1"
Classes="surface-translucent-strong"
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
Padding="10">
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<ItemsControl x:Name="ComponentItemsControl"
ItemsSource="{Binding Components}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ComponentLibraryItemViewModel">
<Border Width="240"
Height="220"
Margin="6"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
Padding="10"
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1">
<Grid RowDefinitions="*,Auto,Auto"
RowSpacing="8">
<!-- 预览区域 -->
<Border CornerRadius="{DynamicResource DesignCornerRadiusSm}"
Background="{DynamicResource AdaptiveGlassPanelBackgroundBrush}"
BorderThickness="1"
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
Padding="8">
<Grid>
<Image Source="{Binding PreviewBitmap}"
Stretch="Uniform"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
RenderOptions.BitmapInterpolationMode="HighQuality"
IsVisible="{Binding IsPreviewReady}" />
<!-- 加载中状态 -->
<Border IsVisible="{Binding IsPreviewPending}"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8">
<ProgressBar Width="96"
IsIndeterminate="True" />
<TextBlock HorizontalAlignment="Center"
TextAlignment="Center"
FontSize="12"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="{Binding PreviewStatusText}" />
</StackPanel>
</Border>
<!-- 失败状态 -->
<Border IsVisible="{Binding IsPreviewFailed}"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8">
<TextBlock HorizontalAlignment="Center"
TextAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="{Binding PreviewStatusText}" />
<TextBlock HorizontalAlignment="Center"
TextAlignment="Center"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="{Binding PreviewErrorMessage}" />
</StackPanel>
</Border>
</Grid>
</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>
<!-- 组件名称 -->
<TextBlock Grid.Row="1"
HorizontalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="{Binding DisplayName}" />
<!-- 添加按钮 -->
<Button Grid.Row="2"
HorizontalAlignment="Center"
Padding="12,6"
Tag="{Binding ComponentId}"
Click="OnAddComponentClick">
<TextBlock Text="添加到桌面" />
</Button>
</Grid>
</Border>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</Grid>
</UserControl>

View File

@@ -1,18 +1,15 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using FluentIcons.Common;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels;
using LanMountainDesktop.Views.Components;
using LanMountainDesktop.Models;
namespace LanMountainDesktop.Views;
@@ -20,10 +17,9 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
{
public event EventHandler<string>? AddComponentRequested;
private readonly ObservableCollection<LibraryCategoryItem> _categories = new();
private readonly ObservableCollection<LibraryComponentItem> _components = new();
private readonly ComponentLibraryWindowViewModel _viewModel = new();
private List<DesktopComponentDefinition> _allDefinitions = new();
private ComponentRegistry? _componentRegistry;
private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry;
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
@@ -35,18 +31,17 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
public FusedDesktopComponentLibraryControl()
{
InitializeComponent();
DataContext = _viewModel;
_weatherDataService = _settingsFacade.Weather.GetWeatherInfoService();
_timeZoneService = _settingsFacade.Region.GetTimeZoneService();
CategoryListBox.ItemsSource = _categories;
ComponentItemsControl.ItemsSource = _components;
LoadRegistry();
LoadCategories();
SearchBox.KeyUp += (s, e) => FilterComponents();
// 默认选择第一个分类
if (_categories.Count > 0)
if (_viewModel.Categories.Count > 0)
{
CategoryListBox.SelectedIndex = 0;
}
@@ -60,7 +55,7 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
_componentRegistry,
pluginRuntimeService,
_settingsFacade);
_allDefinitions = _componentRegistry.GetAll()
.Where(d => d.AllowDesktopPlacement)
.ToList();
@@ -68,18 +63,27 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
private void LoadCategories()
{
_categories.Clear();
_categories.Add(new LibraryCategoryItem("all", "全部组件", "Apps"));
var categoryMap = new Dictionary<string, (string Display, string Icon)>
_viewModel.Categories.Clear();
_viewModel.Components.Clear();
// 添加"全部组件"分类
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
"all",
"全部组件",
Symbol.Apps,
Array.Empty<ComponentLibraryItemViewModel>()));
var categoryMap = new Dictionary<string, (string Display, Symbol Icon)>
{
{ "clock", ("时钟", "Clock") },
{ "date", ("日历", "Calendar") },
{ "weather", ("天气", "WeatherCloudy") },
{ "info", ("资讯", "News") },
{ "calculator", ("工具", "Calculator") },
{ "study", ("学习", "Book") },
{ "file", ("文件", "Document") }
{ "clock", ("时钟", Symbol.Clock) },
{ "date", ("日历", Symbol.CalendarDate) },
{ "weather", ("天气", Symbol.WeatherSunny) },
{ "board", ("画板", Symbol.Edit) },
{ "media", ("媒体", Symbol.Play) },
{ "info", ("资讯", Symbol.News) },
{ "calculator", ("工具", Symbol.Calculator) },
{ "study", ("学习", Symbol.Hourglass) },
{ "file", ("文件", Symbol.Folder) }
};
var usedCategories = _allDefinitions
@@ -91,15 +95,36 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
{
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"));
var categoryComponents = _allDefinitions
.Where(d => string.Equals(d.Category, cat, StringComparison.OrdinalIgnoreCase))
.OrderBy(d => d.DisplayName)
.Select(d => CreateComponentItem(d))
.ToArray();
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
cat,
info.Display,
info.Icon,
categoryComponents));
}
}
}
private ComponentLibraryItemViewModel CreateComponentItem(DesktopComponentDefinition definition)
{
var previewKey = ComponentPreviewKey.ForComponentType(
definition.Id,
definition.MinWidthCells,
definition.MinHeightCells);
return new ComponentLibraryItemViewModel(
definition.Id,
definition.DisplayName,
previewKey,
"正在加载预览...",
"预览不可用");
}
private void OnCategorySelectionChanged(object? sender, SelectionChangedEventArgs e)
{
FilterComponents();
@@ -107,7 +132,7 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
private void FilterComponents()
{
var selectedCategory = (CategoryListBox.SelectedItem as LibraryCategoryItem)?.Id;
var selectedCategory = (CategoryListBox.SelectedItem as ComponentLibraryCategoryViewModel)?.Id;
var searchText = SearchBox.Text?.ToLower() ?? "";
var filtered = _allDefinitions.Where(d =>
@@ -117,57 +142,10 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
return matchesCategory && matchesSearch;
});
_components.Clear();
_viewModel.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 };
_viewModel.Components.Add(CreateComponentItem(def));
}
}
@@ -179,15 +157,3 @@ 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,4 +1,5 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using LanMountainDesktop.ComponentSystem;

View File

@@ -269,12 +269,6 @@ public partial class MainWindow
LauncherPagePanel.MaxWidth = pageWidth - launcherMargin * 2;
LauncherPagePanel.MaxHeight = pageHeight - launcherMargin * 2;
if (LauncherFolderPanel is not null)
{
LauncherFolderPanel.MaxWidth = Math.Max(320, pageWidth - 96);
LauncherFolderPanel.MaxHeight = Math.Max(220, pageHeight - 96);
}
// 更新启动台图标布局
UpdateLauncherTileLayout();
@@ -331,19 +325,6 @@ public partial class MainWindow
}
}
// 同样更新文件夹视图的图标尺寸
if (LauncherFolderTilePanel is not null)
{
LauncherFolderTilePanel.Width = availableWidth;
foreach (var child in LauncherFolderTilePanel.Children)
{
if (child is Button button)
{
button.Width = tileWidth;
button.Height = tileHeight;
}
}
}
}
private void ClampSurfaceIndex()
@@ -630,8 +611,12 @@ public partial class MainWindow
foreach (var node in button.GetSelfAndVisualAncestors())
{
if (node is WrapPanel panel &&
(panel.Name == "LauncherRootTilePanel" || panel.Name == "LauncherFolderTilePanel"))
if (node is WrapPanel panel && panel.Name == "LauncherRootTilePanel")
{
return true;
}
if (node is Grid grid && grid.Name == "LauncherFolderGridPanel")
{
return true;
}
@@ -719,8 +704,7 @@ public partial class MainWindow
return false;
}
return scrollViewer.Name == "LauncherRootScrollViewer" ||
scrollViewer.Name == "LauncherFolderScrollViewer";
return scrollViewer.Name == "LauncherRootScrollViewer";
}
private bool TryGetPointerPositionInDesktopViewport(PointerEventArgs e, out Point point)
@@ -1561,18 +1545,17 @@ public partial class MainWindow
LauncherFolderOverlay.IsVisible = false;
}
if (LauncherFolderTilePanel is not null)
if (LauncherFolderGridPanel is not null)
{
LauncherFolderTilePanel.Children.Clear();
LauncherFolderGridPanel.Children.Clear();
}
}
private void RenderLauncherFolderFromStack()
{
if (LauncherFolderOverlay is null ||
LauncherFolderTilePanel is null ||
LauncherFolderTitleTextBlock is null ||
LauncherFolderBackButton is null)
LauncherFolderGridPanel is null ||
LauncherFolderTitleTextBlock is null)
{
return;
}
@@ -1587,38 +1570,230 @@ public partial class MainWindow
var folder = _launcherFolderStack.Peek();
LauncherFolderOverlay.IsVisible = true;
LauncherFolderTitleTextBlock.Text = folder.Name;
LauncherFolderBackButton.IsVisible = _launcherFolderStack.Count > 1;
LauncherFolderTilePanel.Children.Clear();
foreach (var subFolder in folder.Folders)
LauncherFolderGridPanel.Children.Clear();
const int maxCols = 4;
const int maxRows = 3;
const int maxItems = maxCols * maxRows;
var visibleFolders = folder.Folders.Where(IsLauncherFolderVisible).ToList();
var visibleApps = folder.Apps.Where(IsLauncherAppVisible).ToList();
if (visibleFolders.Count == 0 && visibleApps.Count == 0)
{
if (!IsLauncherFolderVisible(subFolder))
LauncherFolderGridPanel.Children.Add(CreateLauncherFolderGridHintCell(
L("launcher.empty_folder", "This folder is empty.")));
return;
}
var allItems = new List<(StartMenuFolderNode? Folder, StartMenuAppEntry? App)>();
foreach (var f in visibleFolders)
{
allItems.Add((f, null));
}
foreach (var a in visibleApps)
{
allItems.Add((null, a));
}
var displayCount = Math.Min(allItems.Count, maxItems);
for (var i = 0; i < displayCount; i++)
{
var col = i % maxCols;
var row = i / maxCols;
var (itemFolder, itemApp) = allItems[i];
Control cell;
if (itemFolder is not null)
{
var capturedFolder = itemFolder;
cell = CreateLauncherFolderGridTile(itemFolder.Name, GetLauncherFolderIconBitmap(), () => OpenLauncherFolder(capturedFolder));
}
else if (itemApp is not null)
{
var capturedApp = itemApp;
cell = CreateLauncherFolderGridTile(capturedApp, () => LaunchStartMenuEntry(capturedApp));
}
else
{
continue;
}
LauncherFolderTilePanel.Children.Add(CreateLauncherFolderTile(subFolder));
Grid.SetColumn(cell, col);
Grid.SetRow(cell, row);
LauncherFolderGridPanel.Children.Add(cell);
}
}
foreach (var app in folder.Apps)
{
if (!IsLauncherAppVisible(app))
private Button CreateLauncherFolderGridTile(StartMenuAppEntry app, Action clickAction)
{
var iconBitmap = GetLauncherIconBitmap(app);
var monogram = BuildMonogram(app.DisplayName);
Control iconControl = iconBitmap is not null
? new Image
{
continue;
Source = iconBitmap,
Width = 32,
Height = 32,
Stretch = Stretch.Uniform
}
: new Border
{
Width = 32,
Height = 32,
CornerRadius = new CornerRadius(8),
Background = GetThemeBrush("AdaptiveButtonBackgroundBrush"),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Child = new TextBlock
{
Text = monogram,
FontSize = 13,
FontWeight = FontWeight.Bold,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
};
var content = new StackPanel
{
Spacing = 6,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Center
};
content.Children.Add(iconControl);
content.Children.Add(new TextBlock
{
Text = app.DisplayName,
TextTrimming = TextTrimming.CharacterEllipsis,
MaxLines = 2,
TextAlignment = TextAlignment.Center,
FontSize = 11,
HorizontalAlignment = HorizontalAlignment.Stretch
});
var button = new Button
{
Classes = { "glass-panel" },
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
BorderThickness = new Thickness(0),
CornerRadius = new CornerRadius(12),
Padding = new Thickness(8, 8, 8, 6),
Content = content
};
button.Click += (_, _) =>
{
if (_isComponentLibraryOpen)
{
return;
}
LauncherFolderTilePanel.Children.Add(CreateLauncherAppTile(app));
}
clickAction();
};
return button;
}
if (LauncherFolderTilePanel.Children.Count == 0)
private Button CreateLauncherFolderGridTile(string folderName, Bitmap? iconBitmap, Action clickAction)
{
var monogram = "DIR";
Control iconControl = iconBitmap is not null
? new Image
{
Source = iconBitmap,
Width = 32,
Height = 32,
Stretch = Stretch.Uniform
}
: new Border
{
Width = 32,
Height = 32,
CornerRadius = new CornerRadius(8),
Background = GetThemeBrush("AdaptiveButtonBackgroundBrush"),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Child = new TextBlock
{
Text = monogram,
FontSize = 11,
FontWeight = FontWeight.Bold,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
};
var content = new StackPanel
{
LauncherFolderTilePanel.Children.Add(CreateLauncherHintTile(
L("launcher.empty_folder", "This folder is empty."),
string.Empty));
}
Spacing = 6,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Center
};
content.Children.Add(iconControl);
content.Children.Add(new TextBlock
{
Text = folderName,
TextTrimming = TextTrimming.CharacterEllipsis,
MaxLines = 2,
TextAlignment = TextAlignment.Center,
FontSize = 11,
HorizontalAlignment = HorizontalAlignment.Stretch
});
// 在图标渲染完成后,应用布局计算
Dispatcher.UIThread.Post(() => UpdateLauncherTileLayout(), DispatcherPriority.Background);
var button = new Button
{
Classes = { "glass-panel" },
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
BorderThickness = new Thickness(0),
CornerRadius = new CornerRadius(12),
Padding = new Thickness(8, 8, 8, 6),
Content = content
};
button.Click += (_, _) =>
{
if (_isComponentLibraryOpen)
{
return;
}
clickAction();
};
return button;
}
private Control CreateLauncherFolderGridHintCell(string message)
{
return CreateLauncherFolderGridHintCell(message, 0, 0);
}
private Control CreateLauncherFolderGridHintCell(string message, int col, int row)
{
var textBlock = new TextBlock
{
Text = message,
FontSize = 12,
FontWeight = FontWeight.SemiBold,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Opacity = 0.6
};
var cell = new Border
{
Classes = { "glass-panel" },
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
CornerRadius = new CornerRadius(12),
Child = textBlock
};
Grid.SetColumn(cell, col);
Grid.SetRow(cell, row);
return cell;
}
private static string BuildMonogram(string text)
@@ -1689,18 +1864,6 @@ public partial class MainWindow
}
}
private void OnLauncherFolderBackClick(object? sender, RoutedEventArgs e)
{
if (_launcherFolderStack.Count <= 1)
{
CloseLauncherFolderOverlay();
return;
}
_launcherFolderStack.Pop();
RenderLauncherFolderFromStack();
}
private void OnLauncherFolderOverlayPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (LauncherFolderPanel is null)
@@ -1721,11 +1884,6 @@ public partial class MainWindow
e.Handled = true;
}
private void OnLauncherFolderCloseClick(object? sender, RoutedEventArgs e)
{
CloseLauncherFolderOverlay();
}
private void DisposeLauncherResources()
{
foreach (var bitmap in _launcherIconCache.Values)

View File

@@ -189,50 +189,21 @@
Classes="surface-solid-strong"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="52"
MaxWidth="760"
MaxHeight="520"
CornerRadius="36"
Padding="14">
<Border.RenderTransform>
<TranslateTransform Y="42" />
</Border.RenderTransform>
<Grid RowDefinitions="Auto,*"
RowSpacing="10">
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="8">
<Button x:Name="LauncherFolderBackButton"
Grid.Column="0"
Width="38"
Height="34"
Padding="0"
Click="OnLauncherFolderBackClick">
<fi:FluentIcon Icon="ArrowLeft"
IconVariant="Regular" />
</Button>
<TextBlock x:Name="LauncherFolderTitleTextBlock"
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontWeight="SemiBold" />
<Button x:Name="LauncherFolderCloseButton"
Grid.Column="2"
Width="38"
Height="34"
Padding="0"
Click="OnLauncherFolderCloseClick">
<fi:FluentIcon Icon="Dismiss"
IconVariant="Regular" />
</Button>
</Grid>
Width="464"
Height="384"
CornerRadius="24"
Padding="16,14,16,12">
<Grid RowDefinitions="Auto,*">
<TextBlock x:Name="LauncherFolderTitleTextBlock"
FontSize="15"
FontWeight="SemiBold"
HorizontalAlignment="Center"
Margin="0,0,0,10" />
<ScrollViewer x:Name="LauncherFolderScrollViewer"
Grid.Row="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<WrapPanel x:Name="LauncherFolderTilePanel"
Orientation="Horizontal" />
</ScrollViewer>
<Grid x:Name="LauncherFolderGridPanel"
Grid.Row="1"
ColumnDefinitions="*,*,*,*"
RowDefinitions="*,*,*" />
</Grid>
</Border>
</Grid>

View File

@@ -1,7 +1,6 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="LanMountainDesktop.Views.TransparentOverlayWindow"
WindowState="FullScreen"
SystemDecorations="None"
CanResize="False"
ShowInTaskbar="False"

View File

@@ -22,9 +22,6 @@ namespace LanMountainDesktop.Views;
/// </summary>
public partial class TransparentOverlayWindow : Window
{
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
private readonly IWindowBottomMostService _bottomMostService = WindowBottomMostServiceFactory.GetOrCreate();
private readonly IRegionPassthroughService _regionPassthroughService = RegionPassthroughServiceFactory.GetOrCreate();
private readonly IFusedDesktopLayoutService _layoutService = FusedDesktopLayoutServiceProvider.GetOrCreate();
// 滑动状态
@@ -67,23 +64,45 @@ public partial class TransparentOverlayWindow : Window
public TransparentOverlayWindow()
{
InitializeComponent();
_weatherDataService = _settingsFacade.Weather.GetWeatherInfoService();
_timeZoneService = _settingsFacade.Region.GetTimeZoneService();
var facade = HostSettingsFacadeProvider.GetOrCreate();
_weatherDataService = facade.Weather.GetWeatherInfoService();
_timeZoneService = facade.Region.GetTimeZoneService();
_settingsFacade = facade;
}
private readonly ISettingsFacadeService _settingsFacade;
public void SaveLayoutAndHide()
{
SaveLayout();
Hide();
// 仅在 Windows 上启用置底功能
if (OperatingSystem.IsWindows())
// Remove all components so that next time we open it builds fresh from snapshot
if (Content is Canvas canvas)
{
_bottomMostService.SetupBottomMost(this);
canvas.Children.Clear();
}
_componentHosts.Clear();
}
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
if (OperatingSystem.IsWindows())
if (Screens.Primary is { } primaryScreen)
{
_bottomMostService.SendToBottom(this);
// 避开系统任务栏
var workArea = primaryScreen.WorkingArea;
var scaling = primaryScreen.Scaling;
Position = new PixelPoint(workArea.X, workArea.Y);
Width = workArea.Width / scaling;
Height = workArea.Height / scaling;
}
if (Content is Canvas canvas)
{
// 保证透明区域也能被抓取事件
canvas.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
}
// 确保注册表已初始化
@@ -147,16 +166,7 @@ public partial class TransparentOverlayWindow : Window
/// </summary>
private void UpdateInteractiveRegions()
{
_interactiveRegions.Clear();
foreach (var host in _componentHosts.Values)
{
var x = Canvas.GetLeft(host);
var y = Canvas.GetTop(host);
_interactiveRegions.Add(new Rect(x, y, host.Width, host.Height));
}
_regionPassthroughService.SetInteractiveRegions(this, _interactiveRegions);
// 编辑模式下不再需要底层穿透功能计算,这里留空或移除
}
/// <summary>