mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
feat.加入快捷方式组件
This commit is contained in:
@@ -26,7 +26,11 @@
|
|||||||
## [0.8.3.1] - 2026-04-08
|
## [0.8.3.1] - 2026-04-08
|
||||||
|
|
||||||
### 新增 (Added)
|
### 新增 (Added)
|
||||||
- 初始化更新日志文档,为后续版本发布建立基础
|
- ✨ **快捷方式组件**: 新增快捷方式组件,可在阑山桌面内便捷打开系统应用与文件
|
||||||
|
- 支持创建快捷方式,统一管理应用和文件
|
||||||
|
- 提供单击打开和双击打开两种交互模式
|
||||||
|
- 支持配置是否显示背景
|
||||||
|
- 📝 初始化更新日志文档,为后续版本发布建立基础
|
||||||
|
|
||||||
### 变更 (Changed)
|
### 变更 (Changed)
|
||||||
- 无
|
- 无
|
||||||
|
|||||||
@@ -46,4 +46,5 @@ public static class BuiltInComponentIds
|
|||||||
public const string DesktopZhiJiaoHub = "DesktopZhiJiaoHub";
|
public const string DesktopZhiJiaoHub = "DesktopZhiJiaoHub";
|
||||||
public const string DesktopFileManager = "DesktopFileManager";
|
public const string DesktopFileManager = "DesktopFileManager";
|
||||||
public const string DesktopNotificationBox = "DesktopNotificationBox";
|
public const string DesktopNotificationBox = "DesktopNotificationBox";
|
||||||
|
public const string DesktopShortcut = "DesktopShortcut";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -420,6 +420,16 @@ public sealed class ComponentRegistry
|
|||||||
MinHeightCells: 2,
|
MinHeightCells: 2,
|
||||||
AllowStatusBarPlacement: false,
|
AllowStatusBarPlacement: false,
|
||||||
AllowDesktopPlacement: true,
|
AllowDesktopPlacement: true,
|
||||||
|
ResizeMode: DesktopComponentResizeMode.Free),
|
||||||
|
new DesktopComponentDefinition(
|
||||||
|
BuiltInComponentIds.DesktopShortcut,
|
||||||
|
"快捷方式",
|
||||||
|
"App",
|
||||||
|
"Launcher",
|
||||||
|
MinWidthCells: 1,
|
||||||
|
MinHeightCells: 1,
|
||||||
|
AllowStatusBarPlacement: false,
|
||||||
|
AllowDesktopPlacement: true,
|
||||||
ResizeMode: DesktopComponentResizeMode.Free)
|
ResizeMode: DesktopComponentResizeMode.Free)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,25 @@ public sealed class ComponentSettingsSnapshot
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Shortcut Component Settings (快捷方式组件设置)
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 快捷方式目标路径
|
||||||
|
/// </summary>
|
||||||
|
public string? ShortcutTargetPath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 点击模式:Single(单击打开) 或 Double(双击打开)
|
||||||
|
/// </summary>
|
||||||
|
public string ShortcutClickMode { get; set; } = "Double";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否透明背景
|
||||||
|
/// </summary>
|
||||||
|
public bool ShortcutTransparentBackground { get; set; } = false;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
public ComponentSettingsSnapshot Clone()
|
public ComponentSettingsSnapshot Clone()
|
||||||
{
|
{
|
||||||
var clone = (ComponentSettingsSnapshot)MemberwiseClone();
|
var clone = (ComponentSettingsSnapshot)MemberwiseClone();
|
||||||
|
|||||||
@@ -272,7 +272,12 @@ public static class DesktopComponentEditorRegistryFactory
|
|||||||
BuiltInComponentIds.DesktopNotificationBox,
|
BuiltInComponentIds.DesktopNotificationBox,
|
||||||
context => new NotificationBoxComponentEditor(context),
|
context => new NotificationBoxComponentEditor(context),
|
||||||
preferredWidth: 480d,
|
preferredWidth: 480d,
|
||||||
preferredHeight: 520d)
|
preferredHeight: 520d),
|
||||||
|
[BuiltInComponentIds.DesktopShortcut] = new(
|
||||||
|
BuiltInComponentIds.DesktopShortcut,
|
||||||
|
context => new ShortcutComponentEditor(context),
|
||||||
|
preferredWidth: 420d,
|
||||||
|
preferredHeight: 400d)
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var componentId in GetBuiltInDesktopComponentIds(componentRegistry))
|
foreach (var componentId in GetBuiltInDesktopComponentIds(componentRegistry))
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<UserControl 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"
|
||||||
|
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||||
|
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignWidth="360"
|
||||||
|
d:DesignHeight="400"
|
||||||
|
x:Class="LanMountainDesktop.Views.ComponentEditors.ShortcutComponentEditor">
|
||||||
|
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Spacing="16" Margin="20">
|
||||||
|
|
||||||
|
<TextBlock x:Name="HeadlineTextBlock"
|
||||||
|
FontSize="18"
|
||||||
|
FontWeight="SemiBold" />
|
||||||
|
|
||||||
|
<TextBlock x:Name="DescriptionTextBlock"
|
||||||
|
Opacity="0.75"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
|
||||||
|
<ui:SettingsExpander x:Name="TargetPathExpander">
|
||||||
|
<ui:SettingsExpander.IconSource>
|
||||||
|
<fi:SymbolIconSource Symbol="Folder" />
|
||||||
|
</ui:SettingsExpander.IconSource>
|
||||||
|
<ui:SettingsExpander.Footer>
|
||||||
|
<Button x:Name="BrowseButton"
|
||||||
|
Content="浏览..."
|
||||||
|
Click="OnBrowseClick" />
|
||||||
|
</ui:SettingsExpander.Footer>
|
||||||
|
<ui:SettingsExpanderItem>
|
||||||
|
<TextBox x:Name="TargetPathTextBox"
|
||||||
|
IsReadOnly="True"
|
||||||
|
Watermark="未选择目标"
|
||||||
|
MinWidth="200" />
|
||||||
|
</ui:SettingsExpanderItem>
|
||||||
|
<ui:SettingsExpanderItem>
|
||||||
|
<Button x:Name="ClearButton"
|
||||||
|
Content="清除"
|
||||||
|
Click="OnClearClick"
|
||||||
|
HorizontalAlignment="Stretch" />
|
||||||
|
</ui:SettingsExpanderItem>
|
||||||
|
</ui:SettingsExpander>
|
||||||
|
|
||||||
|
<ui:SettingsExpander>
|
||||||
|
<ui:SettingsExpander.IconSource>
|
||||||
|
<fi:SymbolIconSource Symbol="CursorClick" />
|
||||||
|
</ui:SettingsExpander.IconSource>
|
||||||
|
<ui:SettingsExpanderItem>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||||
|
<RadioButton x:Name="SingleClickRadio"
|
||||||
|
Content="单击打开"
|
||||||
|
GroupName="ClickModeGroup" />
|
||||||
|
<RadioButton x:Name="DoubleClickRadio"
|
||||||
|
Content="双击打开"
|
||||||
|
GroupName="ClickModeGroup"
|
||||||
|
IsChecked="True" />
|
||||||
|
</StackPanel>
|
||||||
|
</ui:SettingsExpanderItem>
|
||||||
|
</ui:SettingsExpander>
|
||||||
|
|
||||||
|
<ui:SettingsExpander>
|
||||||
|
<ui:SettingsExpander.IconSource>
|
||||||
|
<fi:SymbolIconSource Symbol="ColorBackground" />
|
||||||
|
</ui:SettingsExpander.IconSource>
|
||||||
|
<ui:SettingsExpander.Footer>
|
||||||
|
<ToggleSwitch x:Name="BackgroundToggle"
|
||||||
|
OnContent=""
|
||||||
|
OffContent=""
|
||||||
|
IsChecked="True" />
|
||||||
|
</ui:SettingsExpander.Footer>
|
||||||
|
<ui:SettingsExpanderItem>
|
||||||
|
<StackPanel Spacing="2">
|
||||||
|
<TextBlock x:Name="BackgroundLabel" />
|
||||||
|
<TextBlock x:Name="BackgroundDescription"
|
||||||
|
Opacity="0.75"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</ui:SettingsExpanderItem>
|
||||||
|
</ui:SettingsExpander>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
using LanMountainDesktop.ComponentSystem;
|
||||||
|
using LanMountainDesktop.Models;
|
||||||
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Views.ComponentEditors;
|
||||||
|
|
||||||
|
public partial class ShortcutComponentEditor : ComponentEditorViewBase
|
||||||
|
{
|
||||||
|
private bool _suppressEvents;
|
||||||
|
|
||||||
|
public ShortcutComponentEditor()
|
||||||
|
: this(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShortcutComponentEditor(DesktopComponentEditorContext? context)
|
||||||
|
: base(context)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
ApplyLocalizedText();
|
||||||
|
ApplyState();
|
||||||
|
AttachEventHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyLocalizedText()
|
||||||
|
{
|
||||||
|
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "快捷方式";
|
||||||
|
DescriptionTextBlock.Text = L(
|
||||||
|
"shortcut.settings.desc",
|
||||||
|
"配置快捷方式的目标路径和打开方式。");
|
||||||
|
|
||||||
|
BackgroundLabel.Text = L("shortcut.settings.show_background", "显示背景");
|
||||||
|
BackgroundDescription.Text = L(
|
||||||
|
"shortcut.settings.show_background.desc",
|
||||||
|
"关闭后组件背景将变为透明。");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyState()
|
||||||
|
{
|
||||||
|
var snapshot = LoadSnapshot();
|
||||||
|
var targetPath = snapshot.ShortcutTargetPath;
|
||||||
|
var clickMode = snapshot.ShortcutClickMode;
|
||||||
|
var transparentBackground = snapshot.ShortcutTransparentBackground;
|
||||||
|
|
||||||
|
_suppressEvents = true;
|
||||||
|
TargetPathTextBox.Text = targetPath ?? string.Empty;
|
||||||
|
SingleClickRadio.IsChecked = string.Equals(clickMode, "Single", StringComparison.OrdinalIgnoreCase);
|
||||||
|
DoubleClickRadio.IsChecked = !SingleClickRadio.IsChecked;
|
||||||
|
BackgroundToggle.IsChecked = !transparentBackground;
|
||||||
|
_suppressEvents = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AttachEventHandlers()
|
||||||
|
{
|
||||||
|
BackgroundToggle.IsCheckedChanged += OnBackgroundToggleChanged;
|
||||||
|
SingleClickRadio.IsCheckedChanged += OnClickModeChanged;
|
||||||
|
DoubleClickRadio.IsCheckedChanged += OnClickModeChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnBrowseClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var topLevel = TopLevel.GetTopLevel(this);
|
||||||
|
if (topLevel?.StorageProvider is not { } storageProvider)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = new FilePickerOpenOptions
|
||||||
|
{
|
||||||
|
Title = L("shortcut.settings.picker_title", "选择目标文件或文件夹"),
|
||||||
|
AllowMultiple = false,
|
||||||
|
FileTypeFilter =
|
||||||
|
[
|
||||||
|
new FilePickerFileType(L("shortcut.settings.picker_type.executable", "可执行文件"))
|
||||||
|
{
|
||||||
|
Patterns = ["*.exe", "*.lnk", "*.bat", "*.cmd"]
|
||||||
|
},
|
||||||
|
new FilePickerFileType(L("shortcut.settings.picker_type.all", "所有文件"))
|
||||||
|
{
|
||||||
|
Patterns = ["*.*"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var files = await storageProvider.OpenFilePickerAsync(options);
|
||||||
|
var file = files.FirstOrDefault();
|
||||||
|
var localPath = file?.TryGetLocalPath();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(localPath))
|
||||||
|
{
|
||||||
|
var folderOptions = new FolderPickerOpenOptions
|
||||||
|
{
|
||||||
|
Title = L("shortcut.settings.picker_title_folder", "选择目标文件夹"),
|
||||||
|
AllowMultiple = false
|
||||||
|
};
|
||||||
|
|
||||||
|
var folders = await storageProvider.OpenFolderPickerAsync(folderOptions);
|
||||||
|
localPath = folders.FirstOrDefault()?.TryGetLocalPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(localPath))
|
||||||
|
{
|
||||||
|
TargetPathTextBox.Text = localPath;
|
||||||
|
var snapshot = LoadSnapshot();
|
||||||
|
snapshot.ShortcutTargetPath = localPath;
|
||||||
|
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.ShortcutTargetPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClearClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
TargetPathTextBox.Text = string.Empty;
|
||||||
|
var snapshot = LoadSnapshot();
|
||||||
|
snapshot.ShortcutTargetPath = null;
|
||||||
|
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.ShortcutTargetPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClickModeChanged(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_suppressEvents)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var clickMode = SingleClickRadio.IsChecked == true ? "Single" : "Double";
|
||||||
|
var snapshot = LoadSnapshot();
|
||||||
|
snapshot.ShortcutClickMode = clickMode;
|
||||||
|
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.ShortcutClickMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBackgroundToggleChanged(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_suppressEvents)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var transparentBackground = BackgroundToggle.IsChecked != true;
|
||||||
|
var snapshot = LoadSnapshot();
|
||||||
|
snapshot.ShortcutTransparentBackground = transparentBackground;
|
||||||
|
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.ShortcutTransparentBackground));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -479,7 +479,11 @@ public sealed class DesktopComponentRuntimeRegistry
|
|||||||
new DesktopComponentRuntimeRegistration(
|
new DesktopComponentRuntimeRegistration(
|
||||||
BuiltInComponentIds.DesktopNotificationBox,
|
BuiltInComponentIds.DesktopNotificationBox,
|
||||||
"component.notification_box",
|
"component.notification_box",
|
||||||
() => new NotificationBoxWidget())
|
() => new NotificationBoxWidget()),
|
||||||
|
new DesktopComponentRuntimeRegistration(
|
||||||
|
BuiltInComponentIds.DesktopShortcut,
|
||||||
|
"component.shortcut",
|
||||||
|
() => new ShortcutWidget())
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
38
LanMountainDesktop/Views/Components/ShortcutWidget.axaml
Normal file
38
LanMountainDesktop/Views/Components/ShortcutWidget.axaml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<UserControl 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"
|
||||||
|
d:DesignWidth="96"
|
||||||
|
d:DesignHeight="96"
|
||||||
|
x:Class="LanMountainDesktop.Views.Components.ShortcutWidget">
|
||||||
|
|
||||||
|
<Border x:Name="RootBorder"
|
||||||
|
Classes="glass-panel"
|
||||||
|
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
|
||||||
|
ClipToBounds="True">
|
||||||
|
<Grid RowDefinitions="*,Auto"
|
||||||
|
x:Name="ContentGrid">
|
||||||
|
|
||||||
|
<Border x:Name="IconHost"
|
||||||
|
Grid.Row="0"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Image x:Name="IconImage"
|
||||||
|
Stretch="Uniform" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock x:Name="NameTextBlock"
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
TextAlignment="Center"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
|
MaxLines="2"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Margin="4,0,4,4"
|
||||||
|
FontSize="11" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</UserControl>
|
||||||
369
LanMountainDesktop/Views/Components/ShortcutWidget.axaml.cs
Normal file
369
LanMountainDesktop/Views/Components/ShortcutWidget.axaml.cs
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
using FluentIcons.Avalonia;
|
||||||
|
using LanMountainDesktop.ComponentSystem;
|
||||||
|
using LanMountainDesktop.Models;
|
||||||
|
using LanMountainDesktop.PluginSdk;
|
||||||
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
|
public partial class ShortcutWidget : UserControl, IDesktopComponentWidget, IComponentPlacementContextAware, IDisposable
|
||||||
|
{
|
||||||
|
private string _componentId = BuiltInComponentIds.DesktopShortcut;
|
||||||
|
private string _placementId = string.Empty;
|
||||||
|
private string? _targetPath;
|
||||||
|
private string _clickMode = "Double";
|
||||||
|
private bool _transparentBackground;
|
||||||
|
private double _currentCellSize = 48;
|
||||||
|
private bool _isDisposed;
|
||||||
|
|
||||||
|
private const double TapMovementThreshold = 10;
|
||||||
|
private const long TapTimeThresholdMs = 500;
|
||||||
|
|
||||||
|
private readonly Dictionary<int, PointerGestureState> _gestureStates = new();
|
||||||
|
|
||||||
|
private record PointerGestureState(
|
||||||
|
Point StartPosition,
|
||||||
|
long StartTime
|
||||||
|
);
|
||||||
|
|
||||||
|
public ShortcutWidget()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DoubleTapped += OnDoubleTapped;
|
||||||
|
UpdateDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetComponentPlacementContext(string componentId, string? placementId)
|
||||||
|
{
|
||||||
|
_componentId = string.IsNullOrWhiteSpace(componentId)
|
||||||
|
? BuiltInComponentIds.DesktopShortcut
|
||||||
|
: componentId.Trim();
|
||||||
|
_placementId = placementId?.Trim() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplySettings(ComponentSettingsSnapshot snapshot)
|
||||||
|
{
|
||||||
|
_targetPath = snapshot.ShortcutTargetPath;
|
||||||
|
_clickMode = string.Equals(snapshot.ShortcutClickMode, "Single", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? "Single"
|
||||||
|
: "Double";
|
||||||
|
_transparentBackground = snapshot.ShortcutTransparentBackground;
|
||||||
|
UpdateDisplay();
|
||||||
|
ApplyChrome();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyCellSize(double cellSize)
|
||||||
|
{
|
||||||
|
_currentCellSize = cellSize;
|
||||||
|
var iconSize = Math.Clamp(cellSize * 0.5, 24, 64);
|
||||||
|
IconImage.Width = iconSize;
|
||||||
|
IconImage.Height = iconSize;
|
||||||
|
|
||||||
|
var fontSize = Math.Clamp(cellSize * 0.18, 10, 16);
|
||||||
|
NameTextBlock.FontSize = fontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateDisplay()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_targetPath))
|
||||||
|
{
|
||||||
|
ShowEmptyState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var name = GetDisplayName(_targetPath);
|
||||||
|
NameTextBlock.Text = name;
|
||||||
|
NameTextBlock.Foreground = this.FindResource("AdaptiveTextPrimaryBrush") as IBrush ?? new SolidColorBrush(Colors.White);
|
||||||
|
|
||||||
|
LoadIcon(_targetPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
ShowEmptyState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowEmptyState()
|
||||||
|
{
|
||||||
|
NameTextBlock.Text = "添加快捷方式";
|
||||||
|
NameTextBlock.Foreground = this.FindResource("AdaptiveTextSecondaryBrush") as IBrush ?? new SolidColorBrush(Colors.Gray);
|
||||||
|
|
||||||
|
var iconBrush = this.FindResource("AdaptiveTextSecondaryBrush") as IBrush ?? new SolidColorBrush(Colors.Gray);
|
||||||
|
IconImage.Source = null;
|
||||||
|
|
||||||
|
var iconHostContent = new SymbolIcon
|
||||||
|
{
|
||||||
|
Symbol = FluentIcons.Common.Symbol.Add,
|
||||||
|
FontSize = 32,
|
||||||
|
Foreground = iconBrush,
|
||||||
|
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
||||||
|
};
|
||||||
|
IconHost.Child = iconHostContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetDisplayName(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
return "快捷方式";
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
return Path.GetFileName(path.TrimEnd('\\', '/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileName = Path.GetFileNameWithoutExtension(path);
|
||||||
|
return string.IsNullOrWhiteSpace(fileName) ? path : fileName;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadIcon(string path)
|
||||||
|
{
|
||||||
|
byte[]? pngBytes = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
pngBytes = WindowsIconService.TryGetSystemFolderIconPngBytes();
|
||||||
|
}
|
||||||
|
else if (File.Exists(path))
|
||||||
|
{
|
||||||
|
pngBytes = WindowsIconService.TryGetIconPngBytes(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
pngBytes = LinuxIconService.TryGetSystemFolderIconPngBytes();
|
||||||
|
}
|
||||||
|
else if (File.Exists(path))
|
||||||
|
{
|
||||||
|
pngBytes = LinuxIconService.TryGetIconPngBytes(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
pngBytes = MacIconService.TryGetSystemFolderIconPngBytes();
|
||||||
|
}
|
||||||
|
else if (File.Exists(path))
|
||||||
|
{
|
||||||
|
pngBytes = MacIconService.TryGetIconPngBytes(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
pngBytes = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pngBytes is not null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(pngBytes);
|
||||||
|
IconImage.Source = new Bitmap(stream);
|
||||||
|
IconHost.Child = IconImage;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadFallbackIcon(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadFallbackIcon(string path)
|
||||||
|
{
|
||||||
|
var symbol = Directory.Exists(path)
|
||||||
|
? FluentIcons.Common.Symbol.Folder
|
||||||
|
: FluentIcons.Common.Symbol.Document;
|
||||||
|
|
||||||
|
var iconBrush = this.FindResource("AdaptiveAccentBrush") as IBrush ?? new SolidColorBrush(Colors.DodgerBlue);
|
||||||
|
|
||||||
|
IconImage.Source = null;
|
||||||
|
var iconHostContent = new SymbolIcon
|
||||||
|
{
|
||||||
|
Symbol = symbol,
|
||||||
|
FontSize = 32,
|
||||||
|
Foreground = iconBrush,
|
||||||
|
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
||||||
|
};
|
||||||
|
IconHost.Child = iconHostContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyChrome()
|
||||||
|
{
|
||||||
|
if (_transparentBackground)
|
||||||
|
{
|
||||||
|
RootBorder.Classes.Remove("glass-panel");
|
||||||
|
RootBorder.Background = Brushes.Transparent;
|
||||||
|
RootBorder.BorderBrush = Brushes.Transparent;
|
||||||
|
RootBorder.BorderThickness = new Thickness(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!RootBorder.Classes.Contains("glass-panel"))
|
||||||
|
{
|
||||||
|
RootBorder.Classes.Add("glass-panel");
|
||||||
|
}
|
||||||
|
|
||||||
|
RootBorder.ClearValue(Border.BackgroundProperty);
|
||||||
|
RootBorder.ClearValue(Border.BorderBrushProperty);
|
||||||
|
RootBorder.ClearValue(Border.BorderThicknessProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnPointerPressed(e);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(_targetPath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pointer = e.GetCurrentPoint(this);
|
||||||
|
var pointerId = e.Pointer.Id;
|
||||||
|
var position = pointer.Position;
|
||||||
|
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
|
||||||
|
_gestureStates[pointerId] = new PointerGestureState(position, timestamp);
|
||||||
|
e.Pointer.Capture(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPointerMoved(PointerEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnPointerMoved(e);
|
||||||
|
|
||||||
|
var pointerId = e.Pointer.Id;
|
||||||
|
if (!_gestureStates.TryGetValue(pointerId, out var state))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentPoint = e.GetCurrentPoint(this);
|
||||||
|
var distance = Math.Sqrt(
|
||||||
|
Math.Pow(currentPoint.Position.X - state.StartPosition.X, 2) +
|
||||||
|
Math.Pow(currentPoint.Position.Y - state.StartPosition.Y, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distance > TapMovementThreshold)
|
||||||
|
{
|
||||||
|
_gestureStates.Remove(pointerId);
|
||||||
|
e.Pointer.Capture(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnPointerReleased(e);
|
||||||
|
|
||||||
|
var pointerId = e.Pointer.Id;
|
||||||
|
if (!_gestureStates.Remove(pointerId, out var state))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Pointer.Capture(null);
|
||||||
|
|
||||||
|
var currentPoint = e.GetCurrentPoint(this);
|
||||||
|
var distance = Math.Sqrt(
|
||||||
|
Math.Pow(currentPoint.Position.X - state.StartPosition.X, 2) +
|
||||||
|
Math.Pow(currentPoint.Position.Y - state.StartPosition.Y, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
var elapsed = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - state.StartTime;
|
||||||
|
|
||||||
|
if (distance > TapMovementThreshold || elapsed > TapTimeThresholdMs)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_clickMode == "Single")
|
||||||
|
{
|
||||||
|
OpenTarget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDoubleTapped(object? sender, TappedEventArgs e)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_targetPath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_clickMode == "Double")
|
||||||
|
{
|
||||||
|
OpenTarget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenTarget()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_targetPath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo(_targetPath)
|
||||||
|
{
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
{
|
||||||
|
Process.Start("xdg-open", _targetPath);
|
||||||
|
}
|
||||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
|
{
|
||||||
|
Process.Start("open", _targetPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn("ShortcutWidget", $"Failed to open target: {_targetPath}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
_gestureStates.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user