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(); LinuxDesktopEntryInstaller.EnsureInstalled();
DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell); DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell);
if (!Design.IsDesignMode && OperatingSystem.IsWindows())
{
FusedDesktopManagerServiceFactory.GetOrCreate().Initialize();
}
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
} }
@@ -226,6 +231,9 @@ public partial class App : Application
AppLogger.Warn("FusedDesktop", "Fused desktop is only supported on Windows."); AppLogger.Warn("FusedDesktop", "Fused desktop is only supported on Windows.");
return; return;
} }
// 切换进入编辑模式,隐藏常态零散的小部件
FusedDesktopManagerServiceFactory.GetOrCreate().EnterEditMode();
// 确保透明覆盖层窗口存在并显示 // 确保透明覆盖层窗口存在并显示
EnsureTransparentOverlayWindow(); EnsureTransparentOverlayWindow();
@@ -248,6 +256,19 @@ public partial class App : Application
window.SetOverlayWindow(_transparentOverlayWindow); window.SetOverlayWindow(_transparentOverlayWindow);
} }
// 当组件库关闭时,退出编辑态
window.Closed += (s, ev) =>
{
if (_transparentOverlayWindow is not null)
{
// 触发画布保存,并隐藏画布
_transparentOverlayWindow.SaveLayoutAndHide();
}
// 让管理器根据已存储的最新快照重建生成所有实体小组件
FusedDesktopManagerServiceFactory.GetOrCreate().ExitEditMode();
};
window.Show(); window.Show();
window.Activate(); 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)) if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase))
{ {
return Symbol.Apps; return Symbol.Info;
} }
if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase)) 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" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LanMountainDesktop.ViewModels"
xmlns:fi="using:FluentIcons.Avalonia" 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" <Border Classes="surface-translucent-panel"
BorderBrush="{DynamicResource AdaptiveBorderBrush}" CornerRadius="{DynamicResource DesignCornerRadiusLg}"
BorderThickness="0,0,1,0" Padding="10">
Padding="12,0,12,12">
<Grid RowDefinitions="Auto,*"> <Grid RowDefinitions="Auto,*">
<TextBox x:Name="SearchBox" <TextBox x:Name="SearchBox"
Watermark="搜索组件..." Watermark="搜索组件..."
@@ -27,87 +29,132 @@
Grid.Row="1" Grid.Row="1"
Background="Transparent" Background="Transparent"
BorderThickness="0" BorderThickness="0"
SelectionChanged="OnCategorySelectionChanged"> SelectionChanged="OnCategorySelectionChanged"
<ListBox.Styles> ItemsSource="{Binding Categories}">
<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> <ListBox.ItemTemplate>
<DataTemplate x:DataType="local:LibraryCategoryItem"> <DataTemplate x:DataType="vm:ComponentLibraryCategoryViewModel">
<StackPanel Orientation="Horizontal" Spacing="12"> <Border Padding="10"
<fi:SymbolIcon Symbol="{Binding Icon}" FontSize="18" /> Margin="0,0,0,6"
<TextBlock Text="{Binding DisplayName}" VerticalAlignment="Center" /> CornerRadius="{DynamicResource DesignCornerRadiusSm}"
</StackPanel> 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> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
</Grid> </Grid>
</Border> </Border>
<!-- 组件网格 (右侧) --> <!-- 组件网格 (右侧) -->
<ScrollViewer Grid.Column="1" Padding="20"> <Border Grid.Column="1"
<ItemsControl x:Name="ComponentItemsControl"> Classes="surface-translucent-strong"
<ItemsControl.ItemsPanel> CornerRadius="{DynamicResource DesignCornerRadiusLg}"
<ItemsPanelTemplate> Padding="10">
<WrapPanel Orientation="Horizontal" /> <ScrollViewer VerticalScrollBarVisibility="Auto"
</ItemsPanelTemplate> HorizontalScrollBarVisibility="Disabled">
</ItemsControl.ItemsPanel> <ItemsControl x:Name="ComponentItemsControl"
<ItemsControl.ItemTemplate> ItemsSource="{Binding Components}">
<DataTemplate x:DataType="local:LibraryComponentItem"> <ItemsControl.ItemsPanel>
<Button Classes="unstyled-card" <ItemsPanelTemplate>
Width="260" <WrapPanel Orientation="Horizontal" />
Margin="0,0,16,16" </ItemsPanelTemplate>
Padding="0" </ItemsControl.ItemsPanel>
Background="Transparent"
BorderThickness="0" <ItemsControl.ItemTemplate>
Click="OnAddComponentClick" <DataTemplate x:DataType="vm:ComponentLibraryItemViewModel">
Tag="{Binding Id}"> <Border Width="240"
<Border Classes="card" Height="220"
Background="{DynamicResource AdaptiveSurfaceLowBrush}" Margin="6"
BorderBrush="{DynamicResource AdaptiveBorderBrush}" CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
BorderThickness="1" Padding="10"
CornerRadius="24" Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
ClipToBounds="True"> BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
<Grid RowDefinitions="Auto,Auto"> BorderThickness="1">
<!-- 预览区域 (动态填充预览) --> <Grid RowDefinitions="*,Auto,Auto"
<Border x:Name="PreviewHost" RowSpacing="8">
Height="150" <!-- 预览区域 -->
Background="{DynamicResource AdaptiveSurfaceNeutralBrush}" <Border CornerRadius="{DynamicResource DesignCornerRadiusSm}"
Margin="8" Background="{DynamicResource AdaptiveGlassPanelBackgroundBrush}"
CornerRadius="16" BorderThickness="1"
ClipToBounds="True"> BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
<Panel> Padding="8">
<!-- 这里将显示组件的缩放预览 --> <Grid>
<ContentPresenter Content="{Binding PreviewContent}" /> <Image Source="{Binding PreviewBitmap}"
Stretch="Uniform"
<!-- 空状态或加载中图标 --> HorizontalAlignment="Stretch"
<fi:SymbolIcon Symbol="Cube" VerticalAlignment="Stretch"
FontSize="32" RenderOptions.BitmapInterpolationMode="HighQuality"
Opacity="0.1" IsVisible="{Binding IsPreviewReady}" />
IsVisible="{Binding !HasPreview}" />
</Panel> <!-- 加载中状态 -->
<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> </Border>
<!-- 文字说明 --> <!-- 组件名称 -->
<StackPanel Grid.Row="1" Margin="16,8,16,16" Spacing="4"> <TextBlock Grid.Row="1"
<TextBlock Text="{Binding DisplayName}" HorizontalAlignment="Center"
FontWeight="SemiBold" FontWeight="SemiBold"
FontSize="15" /> Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
<TextBlock Text="{Binding Description}" Text="{Binding DisplayName}" />
Opacity="0.6"
FontSize="12" <!-- 添加按钮 -->
TextWrapping="Wrap" <Button Grid.Row="2"
MaxLines="2" /> HorizontalAlignment="Center"
</StackPanel> Padding="12,6"
Tag="{Binding ComponentId}"
Click="OnAddComponentClick">
<TextBlock Text="添加到桌面" />
</Button>
</Grid> </Grid>
</Border> </Border>
</Button> </DataTemplate>
</DataTemplate> </ItemsControl.ItemTemplate>
</ItemsControl.ItemTemplate> </ItemsControl>
</ItemsControl> </ScrollViewer>
</ScrollViewer> </Border>
</Grid> </Grid>
</UserControl> </UserControl>

View File

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

View File

@@ -269,12 +269,6 @@ public partial class MainWindow
LauncherPagePanel.MaxWidth = pageWidth - launcherMargin * 2; LauncherPagePanel.MaxWidth = pageWidth - launcherMargin * 2;
LauncherPagePanel.MaxHeight = pageHeight - 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(); 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() private void ClampSurfaceIndex()
@@ -630,8 +611,12 @@ public partial class MainWindow
foreach (var node in button.GetSelfAndVisualAncestors()) foreach (var node in button.GetSelfAndVisualAncestors())
{ {
if (node is WrapPanel panel && if (node is WrapPanel panel && panel.Name == "LauncherRootTilePanel")
(panel.Name == "LauncherRootTilePanel" || panel.Name == "LauncherFolderTilePanel")) {
return true;
}
if (node is Grid grid && grid.Name == "LauncherFolderGridPanel")
{ {
return true; return true;
} }
@@ -719,8 +704,7 @@ public partial class MainWindow
return false; return false;
} }
return scrollViewer.Name == "LauncherRootScrollViewer" || return scrollViewer.Name == "LauncherRootScrollViewer";
scrollViewer.Name == "LauncherFolderScrollViewer";
} }
private bool TryGetPointerPositionInDesktopViewport(PointerEventArgs e, out Point point) private bool TryGetPointerPositionInDesktopViewport(PointerEventArgs e, out Point point)
@@ -1561,18 +1545,17 @@ public partial class MainWindow
LauncherFolderOverlay.IsVisible = false; LauncherFolderOverlay.IsVisible = false;
} }
if (LauncherFolderTilePanel is not null) if (LauncherFolderGridPanel is not null)
{ {
LauncherFolderTilePanel.Children.Clear(); LauncherFolderGridPanel.Children.Clear();
} }
} }
private void RenderLauncherFolderFromStack() private void RenderLauncherFolderFromStack()
{ {
if (LauncherFolderOverlay is null || if (LauncherFolderOverlay is null ||
LauncherFolderTilePanel is null || LauncherFolderGridPanel is null ||
LauncherFolderTitleTextBlock is null || LauncherFolderTitleTextBlock is null)
LauncherFolderBackButton is null)
{ {
return; return;
} }
@@ -1587,38 +1570,230 @@ public partial class MainWindow
var folder = _launcherFolderStack.Peek(); var folder = _launcherFolderStack.Peek();
LauncherFolderOverlay.IsVisible = true; LauncherFolderOverlay.IsVisible = true;
LauncherFolderTitleTextBlock.Text = folder.Name; LauncherFolderTitleTextBlock.Text = folder.Name;
LauncherFolderBackButton.IsVisible = _launcherFolderStack.Count > 1;
LauncherFolderTilePanel.Children.Clear(); LauncherFolderGridPanel.Children.Clear();
foreach (var subFolder in folder.Folders)
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; continue;
} }
LauncherFolderTilePanel.Children.Add(CreateLauncherFolderTile(subFolder)); Grid.SetColumn(cell, col);
Grid.SetRow(cell, row);
LauncherFolderGridPanel.Children.Add(cell);
} }
}
foreach (var app in folder.Apps) private Button CreateLauncherFolderGridTile(StartMenuAppEntry app, Action clickAction)
{ {
if (!IsLauncherAppVisible(app)) 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( Spacing = 6,
L("launcher.empty_folder", "This folder is empty."), HorizontalAlignment = HorizontalAlignment.Stretch,
string.Empty)); 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
});
// 在图标渲染完成后,应用布局计算 var button = new Button
Dispatcher.UIThread.Post(() => UpdateLauncherTileLayout(), DispatcherPriority.Background); {
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) 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) private void OnLauncherFolderOverlayPointerPressed(object? sender, PointerPressedEventArgs e)
{ {
if (LauncherFolderPanel is null) if (LauncherFolderPanel is null)
@@ -1721,11 +1884,6 @@ public partial class MainWindow
e.Handled = true; e.Handled = true;
} }
private void OnLauncherFolderCloseClick(object? sender, RoutedEventArgs e)
{
CloseLauncherFolderOverlay();
}
private void DisposeLauncherResources() private void DisposeLauncherResources()
{ {
foreach (var bitmap in _launcherIconCache.Values) foreach (var bitmap in _launcherIconCache.Values)

View File

@@ -189,50 +189,21 @@
Classes="surface-solid-strong" Classes="surface-solid-strong"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
Margin="52" Width="464"
MaxWidth="760" Height="384"
MaxHeight="520" CornerRadius="24"
CornerRadius="36" Padding="16,14,16,12">
Padding="14"> <Grid RowDefinitions="Auto,*">
<Border.RenderTransform> <TextBlock x:Name="LauncherFolderTitleTextBlock"
<TranslateTransform Y="42" /> FontSize="15"
</Border.RenderTransform> FontWeight="SemiBold"
<Grid RowDefinitions="Auto,*" HorizontalAlignment="Center"
RowSpacing="10"> Margin="0,0,0,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>
<ScrollViewer x:Name="LauncherFolderScrollViewer" <Grid x:Name="LauncherFolderGridPanel"
Grid.Row="1" Grid.Row="1"
VerticalScrollBarVisibility="Auto" ColumnDefinitions="*,*,*,*"
HorizontalScrollBarVisibility="Disabled"> RowDefinitions="*,*,*" />
<WrapPanel x:Name="LauncherFolderTilePanel"
Orientation="Horizontal" />
</ScrollViewer>
</Grid> </Grid>
</Border> </Border>
</Grid> </Grid>

View File

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

View File

@@ -22,9 +22,6 @@ namespace LanMountainDesktop.Views;
/// </summary> /// </summary>
public partial class TransparentOverlayWindow : Window 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(); private readonly IFusedDesktopLayoutService _layoutService = FusedDesktopLayoutServiceProvider.GetOrCreate();
// 滑动状态 // 滑动状态
@@ -67,23 +64,45 @@ public partial class TransparentOverlayWindow : Window
public TransparentOverlayWindow() public TransparentOverlayWindow()
{ {
InitializeComponent(); InitializeComponent();
_weatherDataService = _settingsFacade.Weather.GetWeatherInfoService(); var facade = HostSettingsFacadeProvider.GetOrCreate();
_timeZoneService = _settingsFacade.Region.GetTimeZoneService(); _weatherDataService = facade.Weather.GetWeatherInfoService();
_timeZoneService = facade.Region.GetTimeZoneService();
_settingsFacade = facade;
}
private readonly ISettingsFacadeService _settingsFacade;
public void SaveLayoutAndHide()
{
SaveLayout();
Hide();
// 仅在 Windows 上启用置底功能 // Remove all components so that next time we open it builds fresh from snapshot
if (OperatingSystem.IsWindows()) if (Content is Canvas canvas)
{ {
_bottomMostService.SetupBottomMost(this); canvas.Children.Clear();
} }
_componentHosts.Clear();
} }
protected override void OnOpened(EventArgs e) protected override void OnOpened(EventArgs e)
{ {
base.OnOpened(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> /// </summary>
private void UpdateInteractiveRegions() 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> /// <summary>