mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 09:14:25 +08:00
fead.桌面组件
This commit is contained in:
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
195
LanMountainDesktop/Services/FusedDesktopManagerService.cs
Normal file
195
LanMountainDesktop/Services/FusedDesktopManagerService.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
|||||||
23
LanMountainDesktop/Views/DesktopWidgetWindow.axaml
Normal file
23
LanMountainDesktop/Views/DesktopWidgetWindow.axaml
Normal 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>
|
||||||
61
LanMountainDesktop/Views/DesktopWidgetWindow.axaml.cs
Normal file
61
LanMountainDesktop/Views/DesktopWidgetWindow.axaml.cs
Normal 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)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user