Compare commits

...

9 Commits

Author SHA1 Message Date
lincube
0662565dca fead.为文件管理组件添加了跨平台的支持 2026-04-05 14:02:07 +08:00
lincube
12a2f6729b fead.文件管理组件加入 2026-04-04 03:28:51 +08:00
lincube
5d2449fa8f fead.加入jiangtokoto数据源 2026-04-04 02:13:26 +08:00
lincube
00339f0ed0 fix.修Rinshub,怎么不是色色就是逆天 2026-04-03 22:55:35 +08:00
lincube
021c7ff245 fix.还是在修智教Hub组件 2026-04-03 22:07:38 +08:00
lincube
675096b6c4 fead.做了状态栏加了更多的胶囊组件。然后我稍微修了一下智教Hub组件 2026-04-03 21:25:15 +08:00
lincube
1c3cc76f21 fead.做了状态栏文字组件,支持了位置放置。 2026-04-03 13:14:20 +08:00
lincube
44b87ba12e fead.桌面组件 2026-04-03 11:42:00 +08:00
lincube
35976c3f3d fead.做桌面组件ing,智教hub加了rinshub 2026-04-03 01:17:47 +08:00
47 changed files with 6083 additions and 532 deletions

View File

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

View File

@@ -44,4 +44,5 @@ public static class BuiltInComponentIds
public const string DesktopOfficeRecentDocuments = "DesktopOfficeRecentDocuments";
public const string DesktopRemovableStorage = "DesktopRemovableStorage";
public const string DesktopZhiJiaoHub = "DesktopZhiJiaoHub";
public const string DesktopFileManager = "DesktopFileManager";
}

View File

@@ -400,6 +400,16 @@ public sealed class ComponentRegistry
MinHeightCells: 2,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true,
ResizeMode: DesktopComponentResizeMode.Free),
new DesktopComponentDefinition(
BuiltInComponentIds.DesktopFileManager,
"文件管理",
"Folder",
"File",
MinWidthCells: 4,
MinHeightCells: 4,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true,
ResizeMode: DesktopComponentResizeMode.Free)
};

View File

@@ -388,6 +388,41 @@
"settings.status_bar.clock_format_label": "Clock format",
"settings.status_bar.clock_format.hm": "Hour:Minute",
"settings.status_bar.clock_format.hms": "Hour:Minute:Second",
"settings.status_bar.clock_position_label": "Clock position",
"settings.status_bar.clock_position.left": "Left",
"settings.status_bar.clock_position.center": "Center",
"settings.status_bar.clock_position.right": "Right",
"settings.status_bar.text_capsule_header": "Text Capsule",
"settings.status_bar.text_capsule_description": "Display custom text on the status bar with Markdown support.",
"settings.status_bar.text_capsule_position_label": "Text capsule position",
"settings.status_bar.text_capsule_position.left": "Left",
"settings.status_bar.text_capsule_position.center": "Center",
"settings.status_bar.text_capsule_position.right": "Right",
"settings.status_bar.text_capsule_content_label": "Text content (Markdown supported)",
"settings.status_bar.text_capsule_transparent_background_label": "Transparent background",
"settings.status_bar.network_speed_header": "Network Speed",
"settings.status_bar.network_speed_description": "Display real-time network upload and download speed on the status bar.",
"settings.status_bar.network_speed_position_label": "Network speed position",
"settings.status_bar.network_speed_position.left": "Left",
"settings.status_bar.network_speed_position.center": "Center",
"settings.status_bar.network_speed_position.right": "Right",
"settings.status_bar.network_speed_mode_label": "Display mode",
"settings.status_bar.network_speed_mode.both": "Upload + Download",
"settings.status_bar.network_speed_mode.upload": "Upload only",
"settings.status_bar.network_speed_mode.download": "Download only",
"settings.status_bar.network_speed_transparent_background_label": "Transparent background",
"settings.status_bar.show_network_type_icon_label": "Show network type icon",
"settings.status_bar.shadow_header": "Status Bar Shadow",
"settings.status_bar.shadow_desc": "Add shadow effect to the status bar for better visibility of transparent components.",
"settings.status_bar.shadow_enabled_label": "Enable shadow",
"settings.status_bar.shadow_color_label": "Shadow color",
"settings.status_bar.shadow_opacity_label": "Shadow opacity",
"settings.status_bar.theme_header": "Status Bar Theme",
"settings.status_bar.theme_desc": "Set the theme mode for the status bar independently.",
"settings.status_bar.theme_mode_label": "Theme mode",
"settings.status_bar.theme_mode.follow_global": "Follow Global",
"settings.status_bar.theme_mode.dark": "Dark",
"settings.status_bar.theme_mode.light": "Light",
"settings.components.title": "Components",
"settings.components.description": "Adjust component layout and corner design.",
"settings.components.grid_header": "Grid Settings",
@@ -1040,7 +1075,9 @@
"zhijiaohub.settings.source": "Image Source",
"zhijiaohub.settings.classisland": "ClassIsland Gallery",
"zhijiaohub.settings.sectl": "SECTL Gallery",
"zhijiaohub.settings.source_desc": "Select the image source. ClassIsland Gallery contains fun moments from the ClassIsland community, SECTL Gallery contains content from the SECTL community.",
"zhijiaohub.settings.rinlit": "Rin's Gallery",
"zhijiaohub.settings.jiangtokoto": "Jiangtokoto Memes",
"zhijiaohub.settings.source_desc": "Select the image source. ClassIsland Gallery contains fun moments from the ClassIsland community, SECTL Gallery contains content from the SECTL community, Rin's Gallery contains content from Rin's community, Jiangtokoto Memes contains rich meme resources.",
"zhijiaohub.settings.mirror_source": "Mirror Acceleration",
"zhijiaohub.settings.mirror_direct": "Direct (GitHub)",
"zhijiaohub.settings.mirror_ghproxy": "Mirror Acceleration (Recommended)",

View File

@@ -331,6 +331,41 @@
"settings.status_bar.clock_format_label": "時計の形式",
"settings.status_bar.clock_format.hm": "時:分",
"settings.status_bar.clock_format.hms": "時:分:秒",
"settings.status_bar.clock_position_label": "時計の位置",
"settings.status_bar.clock_position.left": "左",
"settings.status_bar.clock_position.center": "中央",
"settings.status_bar.clock_position.right": "右",
"settings.status_bar.text_capsule_header": "テキストカプセル",
"settings.status_bar.text_capsule_description": "ステータスバーにMarkdown形式のカスタムテキストを表示します。",
"settings.status_bar.text_capsule_position_label": "テキストカプセルの位置",
"settings.status_bar.text_capsule_position.left": "左",
"settings.status_bar.text_capsule_position.center": "中央",
"settings.status_bar.text_capsule_position.right": "右",
"settings.status_bar.text_capsule_content_label": "テキスト内容Markdown対応",
"settings.status_bar.text_capsule_transparent_background_label": "透明な背景",
"settings.status_bar.network_speed_header": "ネットワーク速度",
"settings.status_bar.network_speed_description": "ステータスバーにリアルタイムのネットワーク速度を表示します。",
"settings.status_bar.network_speed_position_label": "ネットワーク速度の位置",
"settings.status_bar.network_speed_position.left": "左",
"settings.status_bar.network_speed_position.center": "中央",
"settings.status_bar.network_speed_position.right": "右",
"settings.status_bar.network_speed_mode_label": "表示モード",
"settings.status_bar.network_speed_mode.both": "アップロード + ダウンロード",
"settings.status_bar.network_speed_mode.upload": "アップロードのみ",
"settings.status_bar.network_speed_mode.download": "ダウンロードのみ",
"settings.status_bar.network_speed_transparent_background_label": "透明な背景",
"settings.status_bar.show_network_type_icon_label": "ネットワークタイプアイコンを表示",
"settings.status_bar.shadow_header": "ステータスバーの影",
"settings.status_bar.shadow_desc": "透明なコンポーネントの視認性を高めるために、ステータスバーに影効果を追加します。",
"settings.status_bar.shadow_enabled_label": "影を有効にする",
"settings.status_bar.shadow_color_label": "影の色",
"settings.status_bar.shadow_opacity_label": "影の不透明度",
"settings.status_bar.theme_header": "ステータスバーのテーマ",
"settings.status_bar.theme_desc": "ステータスバーのテーマモードを独立して設定します。",
"settings.status_bar.theme_mode_label": "テーマモード",
"settings.status_bar.theme_mode.follow_global": "グローバルに従う",
"settings.status_bar.theme_mode.dark": "ダーク",
"settings.status_bar.theme_mode.light": "ライト",
"settings.components.title": "コンポーネント",
"settings.components.description": "コンポーネントのレイアウトとコーナーデザインを調整します。",
"settings.components.grid_header": "グリッド設定",

View File

@@ -377,6 +377,41 @@
"settings.status_bar.clock_format_label": "시계 형식",
"settings.status_bar.clock_format.hm": "시:분",
"settings.status_bar.clock_format.hms": "시:분:초",
"settings.status_bar.clock_position_label": "시계 위치",
"settings.status_bar.clock_position.left": "왼쪽",
"settings.status_bar.clock_position.center": "가욍데",
"settings.status_bar.clock_position.right": "오른쪽",
"settings.status_bar.text_capsule_header": "텍스트 캡슐",
"settings.status_bar.text_capsule_description": "Markdown 형식의 사용자 정의 텍스트를 상태 표시줄에 표시합니다.",
"settings.status_bar.text_capsule_position_label": "텍스트 캡슐 위치",
"settings.status_bar.text_capsule_position.left": "왼쪽",
"settings.status_bar.text_capsule_position.center": "가욍데",
"settings.status_bar.text_capsule_position.right": "오른쪽",
"settings.status_bar.text_capsule_content_label": "텍스트 내용 (Markdown 지원)",
"settings.status_bar.text_capsule_transparent_background_label": "투명 배경",
"settings.status_bar.network_speed_header": "네트워크 속도",
"settings.status_bar.network_speed_description": "상태 표시줄에 실시간 네트워크 속도를 표시합니다.",
"settings.status_bar.network_speed_position_label": "네트워크 속도 위치",
"settings.status_bar.network_speed_position.left": "왼쪽",
"settings.status_bar.network_speed_position.center": "가욍데",
"settings.status_bar.network_speed_position.right": "오른쪽",
"settings.status_bar.network_speed_mode_label": "표시 모드",
"settings.status_bar.network_speed_mode.both": "업로드 + 다운로드",
"settings.status_bar.network_speed_mode.upload": "업로드만",
"settings.status_bar.network_speed_mode.download": "다운로드만",
"settings.status_bar.network_speed_transparent_background_label": "투명 배경",
"settings.status_bar.show_network_type_icon_label": "네트워크 유형 아이콘 표시",
"settings.status_bar.shadow_header": "상태 표시줄 그림자",
"settings.status_bar.shadow_desc": "투명한 구성 요소의 가시성을 높이기 위해 상태 표시줄에 그림자 효과를 추가합니다.",
"settings.status_bar.shadow_enabled_label": "그림자 활성화",
"settings.status_bar.shadow_color_label": "그림자 색상",
"settings.status_bar.shadow_opacity_label": "그림자 불투명도",
"settings.status_bar.theme_header": "상태 표시줄 테마",
"settings.status_bar.theme_desc": "상태 표시줄의 테마 모드를 독립적으로 설정합니다.",
"settings.status_bar.theme_mode_label": "테마 모드",
"settings.status_bar.theme_mode.follow_global": "전역 따르기",
"settings.status_bar.theme_mode.dark": "다크",
"settings.status_bar.theme_mode.light": "라이트",
"settings.components.title": "컴포넌트",
"settings.components.description": "컴포넌트 레이아웃과 모서리 디자인을 조정합니다.",
"settings.components.grid_header": "그리드 설정",

View File

@@ -383,6 +383,41 @@
"settings.status_bar.clock_format_label": "时钟格式",
"settings.status_bar.clock_format.hm": "时:分",
"settings.status_bar.clock_format.hms": "时:分:秒",
"settings.status_bar.clock_position_label": "时钟位置",
"settings.status_bar.clock_position.left": "靠左",
"settings.status_bar.clock_position.center": "居中",
"settings.status_bar.clock_position.right": "靠右",
"settings.status_bar.text_capsule_header": "文字胶囊",
"settings.status_bar.text_capsule_description": "在状态栏显示自定义文字,支持 Markdown 格式。",
"settings.status_bar.text_capsule_position_label": "文字胶囊位置",
"settings.status_bar.text_capsule_position.left": "靠左",
"settings.status_bar.text_capsule_position.center": "居中",
"settings.status_bar.text_capsule_position.right": "靠右",
"settings.status_bar.text_capsule_content_label": "文字内容(支持 Markdown",
"settings.status_bar.text_capsule_transparent_background_label": "透明背景",
"settings.status_bar.network_speed_header": "网速显示",
"settings.status_bar.network_speed_description": "在状态栏显示实时网络上传和下载速度。",
"settings.status_bar.network_speed_position_label": "网速显示位置",
"settings.status_bar.network_speed_position.left": "靠左",
"settings.status_bar.network_speed_position.center": "居中",
"settings.status_bar.network_speed_position.right": "靠右",
"settings.status_bar.network_speed_mode_label": "显示模式",
"settings.status_bar.network_speed_mode.both": "上传 + 下载",
"settings.status_bar.network_speed_mode.upload": "仅上传",
"settings.status_bar.network_speed_mode.download": "仅下载",
"settings.status_bar.network_speed_transparent_background_label": "透明背景",
"settings.status_bar.show_network_type_icon_label": "显示网络类型图标",
"settings.status_bar.shadow_header": "状态栏阴影",
"settings.status_bar.shadow_desc": "为状态栏添加阴影效果,使透明背景的组件更清晰。",
"settings.status_bar.shadow_enabled_label": "启用阴影",
"settings.status_bar.shadow_color_label": "阴影颜色",
"settings.status_bar.shadow_opacity_label": "阴影透明度",
"settings.status_bar.theme_header": "状态栏主题",
"settings.status_bar.theme_desc": "独立设置状态栏的主题模式。",
"settings.status_bar.theme_mode_label": "主题模式",
"settings.status_bar.theme_mode.follow_global": "跟随全局",
"settings.status_bar.theme_mode.dark": "暗色",
"settings.status_bar.theme_mode.light": "浅色",
"settings.components.title": "组件",
"settings.components.description": "调整组件布局与圆角设计。",
"settings.components.grid_header": "网格设置",
@@ -1034,7 +1069,9 @@
"zhijiaohub.settings.source": "图片源",
"zhijiaohub.settings.classisland": "ClassIsland 图库",
"zhijiaohub.settings.sectl": "SECTL 图库",
"zhijiaohub.settings.source_desc": "选择图片来源。ClassIsland 图库包含 ClassIsland 社区的趣味瞬间SECTL 图库包含 SECTL 社区的内容。",
"zhijiaohub.settings.rinlit": "Rin's 图库",
"zhijiaohub.settings.jiangtokoto": "Jiangtokoto 表情包",
"zhijiaohub.settings.source_desc": "选择图片来源。ClassIsland 图库包含 ClassIsland 社区的趣味瞬间SECTL 图库包含 SECTL 社区的内容Rin's 图库包含 Rin's 社区的内容Jiangtokoto 表情包包含丰富的表情包资源。",
"zhijiaohub.settings.mirror_source": "镜像加速",
"zhijiaohub.settings.mirror_direct": "直连GitHub",
"zhijiaohub.settings.mirror_ghproxy": "镜像加速(推荐)",

View File

@@ -112,8 +112,40 @@ public sealed class AppSettingsSnapshot
public bool StatusBarClockTransparentBackground { get; set; }
public string ClockPosition { get; set; } = "Left"; // Left, Center, Right
public string ClockFontSize { get; set; } = "Medium"; // Small, Medium, Large
public bool ShowTextCapsule { get; set; } = false;
public string TextCapsuleContent { get; set; } = "**Hello** World!";
public string TextCapsulePosition { get; set; } = "Right"; // Left, Center, Right
public bool TextCapsuleTransparentBackground { get; set; } = false;
public string TextCapsuleFontSize { get; set; } = "Medium"; // Small, Medium, Large
public bool ShowNetworkSpeed { get; set; } = false;
public string NetworkSpeedPosition { get; set; } = "Right"; // Left, Center, Right
public string NetworkSpeedDisplayMode { get; set; } = "Both"; // Upload, Download, Both
public bool NetworkSpeedTransparentBackground { get; set; } = false;
public bool ShowNetworkTypeIcon { get; set; } = false;
public string NetworkSpeedFontSize { get; set; } = "Medium"; // Small, Medium, Large
public string StatusBarSpacingMode { get; set; } = "Relaxed";
public bool StatusBarShadowEnabled { get; set; } = false;
public string StatusBarShadowColor { get; set; } = "#000000";
public double StatusBarShadowOpacity { get; set; } = 0.3;
public int StatusBarCustomSpacingPercent { get; set; } = 12;
public bool EnableThreeFingerSwipe { get; set; } = false;

View File

@@ -124,15 +124,83 @@ public static class ZhiJiaoHubSources
{
public const string ClassIsland = "classisland";
public const string Sectl = "sectl";
public const string RinLit = "rinlit";
public const string Jiangtokoto = "jiangtokoto";
public static string Normalize(string? value)
{
return value?.ToLowerInvariant() switch
{
"sectl" => Sectl,
"rinlit" => RinLit,
"jiangtokoto" => Jiangtokoto,
_ => ClassIsland
};
}
public static string GetDisplayName(string source)
{
return source?.ToLowerInvariant() switch
{
Sectl => "SECTL 图库",
RinLit => "Rin's 图库",
Jiangtokoto => "Jiangtokoto 表情包",
_ => "ClassIsland 图库"
};
}
}
// 智教Hub数据源配置
public sealed class ZhiJiaoHubSourceConfig
{
public string Owner { get; init; } = string.Empty;
public string Repo { get; init; } = string.Empty;
public string Path { get; init; } = string.Empty;
public string DisplayName { get; init; } = string.Empty;
public bool UseJsonIndex { get; init; } = false;
public string? JsonIndexPath { get; init; } = null;
public string ApiUrl => $"https://api.github.com/repos/{Owner}/{Repo}/contents/{Path}";
public string RawUrlTemplate => $"https://raw.githubusercontent.com/{Owner}/{Repo}/main/{Path}/{{0}}";
public string? JsonIndexUrl => JsonIndexPath != null
? $"https://raw.githubusercontent.com/{Owner}/{Repo}/main/{JsonIndexPath}"
: null;
public static ZhiJiaoHubSourceConfig GetConfig(string source)
{
return source?.ToLowerInvariant() switch
{
ZhiJiaoHubSources.Sectl => new ZhiJiaoHubSourceConfig
{
Owner = "SECTL",
Repo = "SECTL-hub",
Path = "docs/.vuepress/public/images",
DisplayName = "SECTL 图库"
},
ZhiJiaoHubSources.RinLit => new ZhiJiaoHubSourceConfig
{
Owner = "RinLit-233-shiroko",
Repo = "Rin-sHub",
Path = "updates/images",
DisplayName = "Rin's 图库",
UseJsonIndex = true,
JsonIndexPath = "updates/images.json"
},
ZhiJiaoHubSources.Jiangtokoto => new ZhiJiaoHubSourceConfig
{
Owner = "unDefFtr",
Repo = "jiangtokoto-images",
Path = "images",
DisplayName = "Jiangtokoto 表情包"
},
_ => new ZhiJiaoHubSourceConfig
{
Owner = "ClassIsland",
Repo = "classisland-hub",
Path = "images",
DisplayName = "ClassIsland 图库"
}
};
}
}
// 智教Hub镜像加速源常量

View File

@@ -0,0 +1,87 @@
using System;
using System.IO;
namespace LanMountainDesktop.Models;
public enum FileSystemItemType
{
Drive,
Directory,
File
}
public sealed class FileSystemItem
{
public string Name { get; init; } = string.Empty;
public string FullPath { get; init; } = string.Empty;
public FileSystemItemType ItemType { get; init; }
public long? Size { get; init; }
public DateTime? LastModified { get; init; }
public string? Extension { get; init; }
public bool IsDirectory => ItemType == FileSystemItemType.Directory || ItemType == FileSystemItemType.Drive;
public static FileSystemItem FromDriveInfo(DriveInfo drive)
{
string name;
long? size = null;
try
{
var volumeLabel = drive.VolumeLabel;
name = string.IsNullOrWhiteSpace(volumeLabel)
? $"{drive.Name.TrimEnd('\\', '/')}"
: $"{volumeLabel} ({drive.Name.TrimEnd('\\', '/').ToUpperInvariant()})";
}
catch
{
name = $"{drive.Name.TrimEnd('\\', '/')}";
}
try
{
var totalSize = drive.TotalSize;
size = totalSize > 0 ? totalSize : null;
}
catch
{
size = null;
}
return new FileSystemItem
{
Name = name,
FullPath = drive.Name,
ItemType = FileSystemItemType.Drive,
Size = size,
LastModified = null,
Extension = null
};
}
public static FileSystemItem FromDirectoryInfo(DirectoryInfo directory)
{
return new FileSystemItem
{
Name = directory.Name,
FullPath = directory.FullName,
ItemType = FileSystemItemType.Directory,
Size = null,
LastModified = directory.LastWriteTime,
Extension = null
};
}
public static FileSystemItem FromFileInfo(FileInfo file)
{
return new FileSystemItem
{
Name = file.Name,
FullPath = file.FullName,
ItemType = FileSystemItemType.File,
Size = file.Length,
LastModified = file.LastWriteTime,
Extension = file.Extension
};
}
}

View File

@@ -0,0 +1,196 @@
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;
// 【修复问题3】不再隐藏窗口而是将窗口内容转移到编辑模式覆盖层
// 这样可以保持组件的运行状态(动画、输入等)
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

@@ -317,11 +317,15 @@ public sealed record RecommendationApiOptions
public string ClassIslandHubApiUrl { get; init; } = "https://api.github.com/repos/ClassIsland/classisland-hub/contents/images";
public string SectlHubApiUrl { get; init; } = "https://api.github.com/repos/SECTL/SECTL-hub/contents/images";
public string SectlHubApiUrl { get; init; } = "https://api.github.com/repos/SECTL/SECTL-hub/contents/docs/.vuepress/public/images";
public string RinLitHubApiUrl { get; init; } = "https://api.github.com/repos/RinLit-233-shiroko/Rin-sHub/contents/images";
public string ClassIslandHubRawUrlTemplate { get; init; } = "https://raw.githubusercontent.com/ClassIsland/classisland-hub/main/images/{0}";
public string SectlHubRawUrlTemplate { get; init; } = "https://raw.githubusercontent.com/SECTL/SECTL-hub/main/images/{0}";
public string SectlHubRawUrlTemplate { get; init; } = "https://raw.githubusercontent.com/SECTL/SECTL-hub/main/docs/.vuepress/public/images/{0}";
public string RinLitHubRawUrlTemplate { get; init; } = "https://raw.githubusercontent.com/RinLit-233-shiroko/Rin-sHub/main/images/{0}";
}
public interface IRecommendationInfoService

View File

@@ -1,214 +1,265 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Runtime.Versioning;
namespace LanMountainDesktop.Services;
[SupportedOSPlatform("linux")]
internal static class LinuxIconService
{
private static readonly string[] SupportedRasterExtensions =
[
".png",
".ico"
];
private static readonly string[] IconThemePaths = {
"/usr/share/icons",
"/usr/share/pixmaps",
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local/share/icons"),
"/var/lib/snapd/desktop/icons"
};
private static readonly Regex SizeDirectoryRegex =
new(@"(?<size>\d{1,4})x\d{1,4}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly string[] IconSizes = { "512x512", "256x256", "128x128", "96x96", "64x64", "48x48", "32x32", "24x24", "16x16" };
private static readonly ConcurrentDictionary<string, string?> IconPathCache = new(StringComparer.OrdinalIgnoreCase);
private static readonly string[] FolderIconNames = { "folder", "inode-directory", "folder-default" };
private static readonly string[] DriveIconNames = { "drive-harddisk", "drive-removable-media", "media-removable" };
public static byte[]? TryGetIconPngBytes(string? iconKey, string? desktopFileDirectory = null)
public static byte[]? TryGetIconPngBytes(string filePath)
{
if (!OperatingSystem.IsLinux() || string.IsNullOrWhiteSpace(iconKey))
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
{
return null;
}
foreach (var candidatePath in ResolveIconCandidates(iconKey.Trim(), desktopFileDirectory))
try
{
if (TryReadIconBytes(candidatePath, out var bytes))
var extension = Path.GetExtension(filePath).ToLowerInvariant();
var iconName = GetIconNameForExtension(extension);
return TryGetThemeIcon(iconName);
}
catch
{
return null;
}
}
public static byte[]? TryGetIconPngBytes(string iconName, string? searchDirectory)
{
if (string.IsNullOrWhiteSpace(iconName))
{
return null;
}
try
{
if (Path.IsPathRooted(iconName) && File.Exists(iconName))
{
return bytes;
if (iconName.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
{
return File.ReadAllBytes(iconName);
}
if (iconName.EndsWith(".svg", StringComparison.OrdinalIgnoreCase))
{
return null;
}
if (iconName.EndsWith(".xpm", StringComparison.OrdinalIgnoreCase))
{
return null;
}
}
var pngBytes = TryGetThemeIcon(iconName);
if (pngBytes is not null)
{
return pngBytes;
}
if (!string.IsNullOrWhiteSpace(searchDirectory))
{
var localIconPath = Path.Combine(searchDirectory, "icons", iconName + ".png");
if (File.Exists(localIconPath))
{
return File.ReadAllBytes(localIconPath);
}
localIconPath = Path.Combine(searchDirectory, iconName + ".png");
if (File.Exists(localIconPath))
{
return File.ReadAllBytes(localIconPath);
}
}
return null;
}
catch
{
return null;
}
}
public static byte[]? TryGetSystemFolderIconPngBytes()
{
foreach (var iconName in FolderIconNames)
{
var iconBytes = TryGetThemeIcon(iconName);
if (iconBytes is not null)
{
return iconBytes;
}
}
return null;
}
private static IEnumerable<string> ResolveIconCandidates(string iconKey, string? desktopFileDirectory)
public static byte[]? TryGetDriveIconPngBytes()
{
if (Path.HasExtension(iconKey))
foreach (var iconName in DriveIconNames)
{
var directPath = ExpandHome(iconKey);
if (Path.IsPathRooted(directPath))
var iconBytes = TryGetThemeIcon(iconName);
if (iconBytes is not null)
{
yield return directPath;
return iconBytes;
}
else if (!string.IsNullOrWhiteSpace(desktopFileDirectory))
{
yield return Path.GetFullPath(Path.Combine(desktopFileDirectory, directPath));
}
yield break;
}
var resolvedThemePath = ResolveThemedIconPath(iconKey);
if (!string.IsNullOrWhiteSpace(resolvedThemePath))
return null;
}
private static string GetIconNameForExtension(string extension)
{
return extension switch
{
yield return resolvedThemePath;
".txt" => "text-x-generic",
".md" => "text-x-markdown",
".pdf" => "application-pdf",
".doc" or ".docx" => "application-msword",
".xls" or ".xlsx" => "application-vnd.ms-excel",
".ppt" or ".pptx" => "application-vnd.ms-powerpoint",
".zip" or ".rar" or ".7z" or ".tar" or ".gz" => "application-x-archive",
".mp3" or ".wav" or ".flac" or ".aac" or ".ogg" => "audio-x-generic",
".mp4" or ".avi" or ".mkv" or ".mov" or ".wmv" => "video-x-generic",
".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".svg" => "image-x-generic",
".cs" => "text-x-csharp",
".js" or ".ts" => "text-x-javascript",
".py" => "text-x-python",
".java" => "text-x-java",
".cpp" or ".c" or ".h" => "text-x-c++",
".json" => "application-json",
".xml" => "text-xml",
".html" or ".htm" => "text-html",
".css" => "text-css",
".sh" or ".bash" => "text-x-script",
".exe" or ".msi" => "application-x-executable",
".deb" or ".rpm" => "application-x-package",
".iso" or ".img" => "application-x-cd-image",
_ => "text-x-generic"
};
}
private static byte[]? TryGetThemeIcon(string iconName)
{
if (string.IsNullOrWhiteSpace(iconName))
{
return null;
}
}
private static string? ResolveThemedIconPath(string iconName)
{
return IconPathCache.GetOrAdd(iconName, static key => FindBestMatchingIconPath(key));
}
private static string? FindBestMatchingIconPath(string iconName)
{
var candidates = new List<(string Path, int Score)>();
foreach (var iconRoot in EnumerateIconRoots())
foreach (var themePath in IconThemePaths)
{
foreach (var extension in SupportedRasterExtensions)
if (!Directory.Exists(themePath))
{
foreach (var candidatePath in EnumerateFilesSafe(iconRoot, iconName + extension))
continue;
}
var iconBytes = TryFindIconInTheme(themePath, iconName);
if (iconBytes is not null)
{
return iconBytes;
}
}
return TryGetIconFromGtkTheme(iconName);
}
private static byte[]? TryFindIconInTheme(string themePath, string iconName)
{
try
{
foreach (var sizeDir in IconSizes)
{
var iconPath = Path.Combine(themePath, "Adwaita", sizeDir, "mimetypes", $"{iconName}.png");
if (File.Exists(iconPath))
{
candidates.Add((candidatePath, ScoreIconPath(candidatePath)));
return File.ReadAllBytes(iconPath);
}
iconPath = Path.Combine(themePath, "Adwaita", sizeDir, "places", $"{iconName}.png");
if (File.Exists(iconPath))
{
return File.ReadAllBytes(iconPath);
}
iconPath = Path.Combine(themePath, "Adwaita", sizeDir, "devices", $"{iconName}.png");
if (File.Exists(iconPath))
{
return File.ReadAllBytes(iconPath);
}
}
}
return candidates
.OrderByDescending(candidate => candidate.Score)
.ThenBy(candidate => candidate.Path.Length)
.Select(candidate => candidate.Path)
.FirstOrDefault();
}
private static IEnumerable<string> EnumerateIconRoots()
{
var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var dataHome = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
if (string.IsNullOrWhiteSpace(dataHome) && !string.IsNullOrWhiteSpace(homeDirectory))
{
dataHome = Path.Combine(homeDirectory, ".local", "share");
}
var dataDirs = (Environment.GetEnvironmentVariable("XDG_DATA_DIRS") ?? "/usr/local/share:/usr/share")
.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var candidates = new List<string>();
if (!string.IsNullOrWhiteSpace(dataHome))
{
candidates.Add(Path.Combine(dataHome, "icons"));
candidates.Add(Path.Combine(dataHome, "pixmaps"));
}
foreach (var dataDir in dataDirs)
{
candidates.Add(Path.Combine(dataDir, "icons"));
candidates.Add(Path.Combine(dataDir, "pixmaps"));
}
if (!string.IsNullOrWhiteSpace(homeDirectory))
{
candidates.Add(Path.Combine(homeDirectory, ".icons"));
candidates.Add(Path.Combine(homeDirectory, ".local", "share", "flatpak", "exports", "share", "icons"));
}
candidates.Add("/var/lib/flatpak/exports/share/icons");
candidates.Add("/var/lib/snapd/desktop/icons");
return candidates
.Where(path => !string.IsNullOrWhiteSpace(path) && Directory.Exists(path))
.Distinct(StringComparer.OrdinalIgnoreCase);
}
private static IEnumerable<string> EnumerateFilesSafe(string rootPath, string fileName)
{
try
{
return Directory.EnumerateFiles(rootPath, fileName, SearchOption.AllDirectories);
}
catch
{
return Array.Empty<string>();
}
}
private static bool TryReadIconBytes(string filePath, out byte[] bytes)
{
bytes = [];
try
{
var extension = Path.GetExtension(filePath);
if (!SupportedRasterExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase) ||
!File.Exists(filePath))
foreach (var sizeDir in IconSizes)
{
return false;
var iconPath = Path.Combine(themePath, "hicolor", sizeDir, "mimetypes", $"{iconName}.png");
if (File.Exists(iconPath))
{
return File.ReadAllBytes(iconPath);
}
iconPath = Path.Combine(themePath, "hicolor", sizeDir, "places", $"{iconName}.png");
if (File.Exists(iconPath))
{
return File.ReadAllBytes(iconPath);
}
iconPath = Path.Combine(themePath, "hicolor", sizeDir, "devices", $"{iconName}.png");
if (File.Exists(iconPath))
{
return File.ReadAllBytes(iconPath);
}
}
bytes = File.ReadAllBytes(filePath);
return bytes.Length > 0;
var directPath = Path.Combine(themePath, $"{iconName}.png");
if (File.Exists(directPath))
{
return File.ReadAllBytes(directPath);
}
}
catch
{
return false;
}
return null;
}
private static int ScoreIconPath(string filePath)
private static byte[]? TryGetIconFromGtkTheme(string iconName)
{
var score = 0;
var extension = Path.GetExtension(filePath);
if (extension.Equals(".png", StringComparison.OrdinalIgnoreCase))
try
{
score += 4_000;
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "gtk3-icon-browser",
Arguments = $"--icon={iconName}",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
return null;
}
else if (extension.Equals(".ico", StringComparison.OrdinalIgnoreCase))
catch
{
score += 2_000;
return null;
}
if (filePath.Contains($"{Path.DirectorySeparatorChar}hicolor{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase))
{
score += 8_000;
}
if (filePath.Contains($"{Path.DirectorySeparatorChar}apps{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase))
{
score += 1_000;
}
var match = SizeDirectoryRegex.Match(filePath);
if (match.Success &&
int.TryParse(match.Groups["size"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var size))
{
score += Math.Min(size, 512);
}
return score;
}
private static string ExpandHome(string path)
{
if (!path.StartsWith("~", StringComparison.Ordinal))
{
return path;
}
var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (string.IsNullOrWhiteSpace(homeDirectory))
{
return path;
}
return path.Length == 1
? homeDirectory
: Path.Combine(homeDirectory, path[2..]);
}
}

View File

@@ -0,0 +1,296 @@
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace LanMountainDesktop.Services;
[SupportedOSPlatform("macos")]
internal static class MacIconService
{
private const int IconSize = 256;
[DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
private static extern IntPtr NSWorkspace_sharedWorkspace();
[DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
private static extern IntPtr NSWorkspace_iconForFile(IntPtr workspace, IntPtr filePath);
[DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
private static extern IntPtr NSImage_initWithContentsOfFile(IntPtr path);
[DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")]
private static extern IntPtr CGImageDestinationCreateWithURL(IntPtr url, IntPtr type, uint count, IntPtr options);
[DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")]
private static extern void CGImageDestinationAddImage(IntPtr dest, IntPtr image, IntPtr properties);
[DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")]
private static extern bool CGImageDestinationFinalize(IntPtr dest);
[DllImport("/System/Library/Frameworks/Foundation.framework/Foundation")]
private static extern IntPtr NSString_stringWithUTF8String(string str);
[DllImport("/System/Library/Frameworks/Foundation.framework/Foundation")]
private static extern IntPtr NSURL_fileURLWithPath(IntPtr path);
[DllImport("/System/Library/Frameworks/Foundation.framework/Foundation")]
private static extern void CFRelease(IntPtr handle);
[DllImport("/System/Library/Frameworks/Foundation.framework/Foundation")]
private static extern IntPtr NSTemporaryDirectory();
private static readonly string[] SystemFolderPaths =
{
"/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources",
"/System/Library/Extensions",
"/System/Library/PrivateFrameworks"
};
private static readonly string[] FolderIconNames = { "GenericFolderIcon.icns", "SidebarDownloadsFolder.icns", "SidebarDocumentsFolder.icns" };
private static readonly string[] DriveIconNames = { "GenericHardDiskIcon.icns", "ExternalDiskIcon.icns", "RemovableDiskIcon.icns" };
public static byte[]? TryGetIconPngBytes(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
{
return null;
}
try
{
return TryGetIconUsingNSWorkspace(filePath);
}
catch
{
}
try
{
var extension = Path.GetExtension(filePath).ToLowerInvariant();
return TryGetIconForExtension(extension);
}
catch
{
return null;
}
}
public static byte[]? TryGetSystemFolderIconPngBytes()
{
foreach (var folderPath in SystemFolderPaths)
{
if (!Directory.Exists(folderPath))
{
continue;
}
foreach (var iconName in FolderIconNames)
{
var iconPath = Path.Combine(folderPath, iconName);
if (File.Exists(iconPath))
{
var pngBytes = TryConvertIcnsToPng(iconPath);
if (pngBytes is not null)
{
return pngBytes;
}
}
}
}
return TryGetIconUsingNSWorkspace("/System/Library/CoreServices");
}
public static byte[]? TryGetDriveIconPngBytes()
{
foreach (var folderPath in SystemFolderPaths)
{
if (!Directory.Exists(folderPath))
{
continue;
}
foreach (var iconName in DriveIconNames)
{
var iconPath = Path.Combine(folderPath, iconName);
if (File.Exists(iconPath))
{
var pngBytes = TryConvertIcnsToPng(iconPath);
if (pngBytes is not null)
{
return pngBytes;
}
}
}
}
return TryGetIconUsingNSWorkspace("/");
}
private static byte[]? TryGetIconUsingNSWorkspace(string filePath)
{
try
{
var tempPath = Path.Combine(Path.GetTempPath(), $"icon_{Guid.NewGuid():N}.png");
var script = $@"
tell application ""System Events""
set theIcon to icon of file ""{filePath}""
end tell
";
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "osascript",
Arguments = $"-e 'tell application \"Finder\" to get icon of file \"{filePath}\"'",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
return TryGetIconUsingSips(filePath);
}
catch
{
return null;
}
}
private static byte[]? TryGetIconUsingSips(string filePath)
{
try
{
var tempPath = Path.Combine(Path.GetTempPath(), $"icon_{Guid.NewGuid():N}.png");
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "sips",
Arguments = $"-s format png -z {IconSize} {IconSize} \"{filePath}\" --out \"{tempPath}\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
process.WaitForExit(5000);
if (File.Exists(tempPath))
{
var bytes = File.ReadAllBytes(tempPath);
File.Delete(tempPath);
return bytes;
}
}
catch
{
}
return null;
}
private static byte[]? TryGetIconForExtension(string extension)
{
var iconName = GetIconNameForExtension(extension);
foreach (var folderPath in SystemFolderPaths)
{
if (!Directory.Exists(folderPath))
{
continue;
}
var iconPath = Path.Combine(folderPath, iconName);
if (File.Exists(iconPath))
{
var pngBytes = TryConvertIcnsToPng(iconPath);
if (pngBytes is not null)
{
return pngBytes;
}
}
}
return null;
}
private static string GetIconNameForExtension(string extension)
{
return extension switch
{
".txt" => "TextEdit.icns",
".md" => "TextEdit.icns",
".pdf" => "Preview.icns",
".doc" or ".docx" => "Microsoft Word.icns",
".xls" or ".xlsx" => "Microsoft Excel.icns",
".ppt" or ".pptx" => "Microsoft PowerPoint.icns",
".zip" or ".rar" or ".7z" => "Archive Utility.icns",
".mp3" or ".wav" or ".flac" or ".aac" => "Music.icns",
".mp4" or ".avi" or ".mkv" or ".mov" => "QuickTime Player.icns",
".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" => "Preview.icns",
".cs" => "Visual Studio.icns",
".js" or ".ts" => "Visual Studio Code.icns",
".py" => "IDLE.icns",
".json" => "TextEdit.icns",
".xml" => "TextEdit.icns",
".html" or ".htm" => "Safari.icns",
".css" => "TextEdit.icns",
".sh" => "Terminal.icns",
".app" => "AppIcon.icns",
".dmg" => "DiskImage.icns",
_ => "GenericDocumentIcon.icns"
};
}
private static byte[]? TryConvertIcnsToPng(string icnsPath)
{
if (!File.Exists(icnsPath))
{
return null;
}
try
{
var tempPath = Path.Combine(Path.GetTempPath(), $"icon_{Guid.NewGuid():N}.png");
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "sips",
Arguments = $"-s format png \"{icnsPath}\" --out \"{tempPath}\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
process.WaitForExit(5000);
if (File.Exists(tempPath))
{
var bytes = File.ReadAllBytes(tempPath);
File.Delete(tempPath);
return bytes;
}
}
catch
{
}
return null;
}
}

View File

@@ -3244,34 +3244,38 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
private async Task<ZhiJiaoHubSnapshot> FetchZhiJiaoHubSnapshotAsync(string source, string mirrorSource, CancellationToken cancellationToken)
{
var (owner, repo, path) = source switch
{
ZhiJiaoHubSources.Sectl => ("SECTL", "SECTL-hub", "docs/.vuepress/public/images"),
_ => ("ClassIsland", "classisland-hub", "images")
};
var contentsUrl = $"https://api.github.com/repos/{owner}/{repo}/contents/{path}";
// 如果使用镜像加速,代理 GitHub API 请求
if (string.Equals(mirrorSource, ZhiJiaoHubMirrorSources.GhProxy, StringComparison.OrdinalIgnoreCase))
{
contentsUrl = ZhiJiaoHubMirrorSources.GhProxyBaseUrl.TrimEnd('/') + "/" + contentsUrl;
}
var config = ZhiJiaoHubSourceConfig.GetConfig(source);
try
{
var images = await FetchImagesFromContentsApi(owner, repo, path, contentsUrl, mirrorSource, cancellationToken);
List<ZhiJiaoHubImageItem> images;
// 如果使用JSON索引模式Rin's Hub
if (config.UseJsonIndex && !string.IsNullOrEmpty(config.JsonIndexUrl))
{
images = await FetchImagesFromJsonIndex(config, mirrorSource, cancellationToken);
}
else
{
// 标准模式ClassIsland/SECTL
var contentsUrl = config.ApiUrl;
if (string.Equals(mirrorSource, ZhiJiaoHubMirrorSources.GhProxy, StringComparison.OrdinalIgnoreCase))
{
contentsUrl = ZhiJiaoHubMirrorSources.GhProxyBaseUrl.TrimEnd('/') + "/" + contentsUrl;
}
images = await FetchImagesFromContentsApi(config, contentsUrl, mirrorSource, cancellationToken);
}
if (images.Count == 0)
{
throw new InvalidOperationException("未找到图片文件");
throw new InvalidOperationException($"在 {config.DisplayName} 中未找到图片文件");
}
// 随机打乱图片顺序
var random = new Random();
var shuffled = images.OrderBy(_ => random.Next()).ToList();
// 重新设置索引
for (int i = 0; i < shuffled.Count; i++)
{
var item = shuffled[i];
@@ -3286,11 +3290,15 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
}
catch (Exception ex)
{
throw new HttpRequestException($"获取图片列表失败: {ex.Message}");
throw new HttpRequestException($"从 {config.DisplayName} 获取图片列表失败: {ex.Message}");
}
}
private async Task<List<ZhiJiaoHubImageItem>> FetchImagesFromContentsApi(string owner, string repo, string path, string contentsUrl, string mirrorSource, CancellationToken cancellationToken)
private async Task<List<ZhiJiaoHubImageItem>> FetchImagesFromContentsApi(
ZhiJiaoHubSourceConfig config,
string contentsUrl,
string mirrorSource,
CancellationToken cancellationToken)
{
var images = new List<ZhiJiaoHubImageItem>();
@@ -3308,7 +3316,17 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
{
throw new HttpRequestException("GitHub API 速率限制,请稍后重试");
}
throw new HttpRequestException($"API 返回错误: {(int)response.StatusCode} - {Truncate(errorText, 200)}");
if ((int)response.StatusCode == 404)
{
throw new HttpRequestException(
$"在 {config.DisplayName} 中找不到图片目录。请检查仓库结构和路径配置。\n" +
$"仓库: {config.Owner}/{config.Repo}\n" +
$"路径: {config.Path}");
}
throw new HttpRequestException(
$"从 {config.DisplayName} 获取数据失败: {(int)response.StatusCode} - {Truncate(errorText, 200)}");
}
var responseText = await response.Content.ReadAsStringAsync(cancellationToken);
@@ -3320,9 +3338,9 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty("message", out var messageNode))
{
var errorMessage = messageNode.GetString();
throw new InvalidOperationException($"GitHub API 错误: {errorMessage}");
throw new InvalidOperationException($"GitHub API 错误 ({config.DisplayName}): {errorMessage}");
}
throw new InvalidOperationException("Invalid response format from GitHub API.");
throw new InvalidOperationException($"从 {config.DisplayName} 返回的数据格式无效");
}
int index = 0;
@@ -3342,18 +3360,15 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
continue;
}
// 只处理图片文件
var extension = Path.GetExtension(name).ToLowerInvariant();
if (extension != ".png" && extension != ".jpg" && extension != ".jpeg" && extension != ".gif" && extension != ".webp")
{
continue;
}
// 解码文件名
var decodedName = Uri.UnescapeDataString(name);
decodedName = Path.GetFileNameWithoutExtension(decodedName);
// 构造图片 URL
string imageUrl;
if (!string.IsNullOrWhiteSpace(downloadUrl))
{
@@ -3361,10 +3376,12 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
}
else
{
imageUrl = $"https://raw.githubusercontent.com/{owner}/{repo}/main/{path}/{Uri.EscapeDataString(name)}";
imageUrl = string.Format(
CultureInfo.InvariantCulture,
config.RawUrlTemplate,
Uri.EscapeDataString(name));
}
// 应用镜像加速到图片 URL
imageUrl = ZhiJiaoHubMirrorSources.ApplyMirror(imageUrl, mirrorSource);
images.Add(new ZhiJiaoHubImageItem(decodedName, imageUrl, index));
@@ -3374,6 +3391,85 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
return images;
}
/// <summary>
/// 从JSON索引文件获取图片列表Rin's Hub专用
/// </summary>
private async Task<List<ZhiJiaoHubImageItem>> FetchImagesFromJsonIndex(
ZhiJiaoHubSourceConfig config,
string mirrorSource,
CancellationToken cancellationToken)
{
var images = new List<ZhiJiaoHubImageItem>();
// 下载JSON索引文件
var jsonUrl = config.JsonIndexUrl!;
if (string.Equals(mirrorSource, ZhiJiaoHubMirrorSources.GhProxy, StringComparison.OrdinalIgnoreCase))
{
jsonUrl = ZhiJiaoHubMirrorSources.GhProxyBaseUrl.TrimEnd('/') + "/" + jsonUrl;
}
using var request = new HttpRequestMessage(HttpMethod.Get, jsonUrl);
request.Headers.TryAddWithoutValidation("User-Agent", "LanMountainDesktop/1.0");
using var response = await _httpClient.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
var jsonText = await response.Content.ReadAsStringAsync(cancellationToken);
using var document = JsonDocument.Parse(jsonText);
var root = document.RootElement;
// 解析 hub_items 数组
if (!root.TryGetProperty("hub_items", out var hubItems) || hubItems.ValueKind != JsonValueKind.Array)
{
throw new InvalidOperationException($"JSON索引文件格式无效缺少 hub_items 数组");
}
int index = 0;
foreach (var item in hubItems.EnumerateArray())
{
// 获取图片路径
if (!item.TryGetProperty("image", out var imageProp))
{
continue;
}
var imagePath = imageProp.GetString();
if (string.IsNullOrWhiteSpace(imagePath))
{
continue;
}
// 获取标题(用于显示名称)
string title = string.Empty;
if (item.TryGetProperty("title", out var titleProp))
{
title = titleProp.GetString() ?? string.Empty;
}
// 如果没有标题,使用文件名
if (string.IsNullOrWhiteSpace(title))
{
title = Path.GetFileNameWithoutExtension(imagePath);
}
// 构建完整的图片URL
// imagePath 格式如: "Discord/姐姐好香.png"
// 需要拼接为: https://raw.githubusercontent.com/.../updates/images/Discord/姐姐好香.png
// 并对路径中的每个部分进行URL编码
var pathParts = imagePath.Split('/');
var encodedPath = string.Join("/", pathParts.Select(part => Uri.EscapeDataString(part)));
var imageUrl = $"https://raw.githubusercontent.com/{config.Owner}/{config.Repo}/main/{config.Path}/{encodedPath}";
// 应用镜像加速
imageUrl = ZhiJiaoHubMirrorSources.ApplyMirror(imageUrl, mirrorSource);
images.Add(new ZhiJiaoHubImageItem(title, imageUrl, index));
index++;
}
return images;
}
private bool TryGetZhiJiaoHubFromCache(string cacheKey, out ZhiJiaoHubSnapshot snapshot)
{
lock (_cacheGate)

View File

@@ -41,8 +41,31 @@ public sealed record StatusBarSettingsState(
string TaskbarLayoutMode,
string ClockDisplayFormat,
bool ClockTransparentBackground,
string ClockPosition,
string ClockFontSize,
bool ShowTextCapsule,
string TextCapsuleContent,
string TextCapsulePosition,
bool TextCapsuleTransparentBackground,
string TextCapsuleFontSize,
bool ShowNetworkSpeed,
string NetworkSpeedPosition,
string NetworkSpeedDisplayMode,
bool NetworkSpeedTransparentBackground,
bool ShowNetworkTypeIcon,
string NetworkSpeedFontSize,
string SpacingMode,
int CustomSpacingPercent);
int CustomSpacingPercent,
bool ShadowEnabled,
string ShadowColor,
double ShadowOpacity);
public sealed record TextCapsuleSettingsState(
bool ShowTextCapsule,
string Content,
string Position,
bool TransparentBackground);
public sealed record WeatherSettingsState(
string LocationMode,
string LocationKey,
@@ -274,6 +297,12 @@ public interface IStatusBarSettingsService
void Save(StatusBarSettingsState state);
}
public interface ITextCapsuleSettingsService
{
TextCapsuleSettingsState Get();
void Save(TextCapsuleSettingsState state);
}
public interface IWeatherProvider
{
Task<WeatherQueryResult<IReadOnlyList<WeatherLocation>>> SearchLocationsAsync(
@@ -385,6 +414,7 @@ public interface ISettingsFacadeService
IWallpaperMediaService WallpaperMedia { get; }
IThemeAppearanceService Theme { get; }
IStatusBarSettingsService StatusBar { get; }
ITextCapsuleSettingsService TextCapsule { get; }
IWeatherSettingsService Weather { get; }
IRegionSettingsService Region { get; }
IPrivacySettingsService Privacy { get; }

View File

@@ -386,8 +386,24 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService
snapshot.TaskbarLayoutMode,
snapshot.ClockDisplayFormat,
snapshot.StatusBarClockTransparentBackground,
snapshot.ClockPosition,
snapshot.ClockFontSize,
snapshot.ShowTextCapsule,
snapshot.TextCapsuleContent,
snapshot.TextCapsulePosition,
snapshot.TextCapsuleTransparentBackground,
snapshot.TextCapsuleFontSize,
snapshot.ShowNetworkSpeed,
snapshot.NetworkSpeedPosition,
snapshot.NetworkSpeedDisplayMode,
snapshot.NetworkSpeedTransparentBackground,
snapshot.ShowNetworkTypeIcon,
snapshot.NetworkSpeedFontSize,
snapshot.StatusBarSpacingMode,
snapshot.StatusBarCustomSpacingPercent);
snapshot.StatusBarCustomSpacingPercent,
snapshot.StatusBarShadowEnabled,
snapshot.StatusBarShadowColor,
snapshot.StatusBarShadowOpacity);
}
public void Save(StatusBarSettingsState state)
@@ -399,8 +415,24 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService
snapshot.TaskbarLayoutMode = state.TaskbarLayoutMode;
snapshot.ClockDisplayFormat = state.ClockDisplayFormat;
snapshot.StatusBarClockTransparentBackground = state.ClockTransparentBackground;
snapshot.ClockPosition = state.ClockPosition;
snapshot.ClockFontSize = state.ClockFontSize;
snapshot.ShowTextCapsule = state.ShowTextCapsule;
snapshot.TextCapsuleContent = state.TextCapsuleContent;
snapshot.TextCapsulePosition = state.TextCapsulePosition;
snapshot.TextCapsuleTransparentBackground = state.TextCapsuleTransparentBackground;
snapshot.TextCapsuleFontSize = state.TextCapsuleFontSize;
snapshot.ShowNetworkSpeed = state.ShowNetworkSpeed;
snapshot.NetworkSpeedPosition = state.NetworkSpeedPosition;
snapshot.NetworkSpeedDisplayMode = state.NetworkSpeedDisplayMode;
snapshot.NetworkSpeedTransparentBackground = state.NetworkSpeedTransparentBackground;
snapshot.ShowNetworkTypeIcon = state.ShowNetworkTypeIcon;
snapshot.NetworkSpeedFontSize = state.NetworkSpeedFontSize;
snapshot.StatusBarSpacingMode = state.SpacingMode;
snapshot.StatusBarCustomSpacingPercent = state.CustomSpacingPercent;
snapshot.StatusBarShadowEnabled = state.ShadowEnabled;
snapshot.StatusBarShadowColor = state.ShadowColor;
snapshot.StatusBarShadowOpacity = state.ShadowOpacity;
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
@@ -412,8 +444,63 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService
nameof(AppSettingsSnapshot.TaskbarLayoutMode),
nameof(AppSettingsSnapshot.ClockDisplayFormat),
nameof(AppSettingsSnapshot.StatusBarClockTransparentBackground),
nameof(AppSettingsSnapshot.ClockPosition),
nameof(AppSettingsSnapshot.ClockFontSize),
nameof(AppSettingsSnapshot.ShowTextCapsule),
nameof(AppSettingsSnapshot.TextCapsuleContent),
nameof(AppSettingsSnapshot.TextCapsulePosition),
nameof(AppSettingsSnapshot.TextCapsuleTransparentBackground),
nameof(AppSettingsSnapshot.TextCapsuleFontSize),
nameof(AppSettingsSnapshot.ShowNetworkSpeed),
nameof(AppSettingsSnapshot.NetworkSpeedPosition),
nameof(AppSettingsSnapshot.NetworkSpeedDisplayMode),
nameof(AppSettingsSnapshot.NetworkSpeedTransparentBackground),
nameof(AppSettingsSnapshot.ShowNetworkTypeIcon),
nameof(AppSettingsSnapshot.NetworkSpeedFontSize),
nameof(AppSettingsSnapshot.StatusBarSpacingMode),
nameof(AppSettingsSnapshot.StatusBarCustomSpacingPercent)
nameof(AppSettingsSnapshot.StatusBarCustomSpacingPercent),
nameof(AppSettingsSnapshot.StatusBarShadowEnabled),
nameof(AppSettingsSnapshot.StatusBarShadowColor),
nameof(AppSettingsSnapshot.StatusBarShadowOpacity)
]);
}
}
internal sealed class TextCapsuleSettingsService : ITextCapsuleSettingsService
{
private readonly ISettingsService _settingsService;
public TextCapsuleSettingsService(ISettingsService settingsService)
{
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
}
public TextCapsuleSettingsState Get()
{
var snapshot = _settingsService.Load();
return new TextCapsuleSettingsState(
snapshot.ShowTextCapsule,
snapshot.TextCapsuleContent,
snapshot.TextCapsulePosition,
snapshot.TextCapsuleTransparentBackground);
}
public void Save(TextCapsuleSettingsState state)
{
var snapshot = _settingsService.Load();
snapshot.ShowTextCapsule = state.ShowTextCapsule;
snapshot.TextCapsuleContent = state.Content;
snapshot.TextCapsulePosition = state.Position;
snapshot.TextCapsuleTransparentBackground = state.TransparentBackground;
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys:
[
nameof(AppSettingsSnapshot.ShowTextCapsule),
nameof(AppSettingsSnapshot.TextCapsuleContent),
nameof(AppSettingsSnapshot.TextCapsulePosition),
nameof(AppSettingsSnapshot.TextCapsuleTransparentBackground)
]);
}
}
@@ -1198,6 +1285,7 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
WallpaperMedia = new WallpaperMediaService();
Theme = new ThemeAppearanceService(Settings);
StatusBar = new StatusBarSettingsService(Settings);
TextCapsule = new TextCapsuleSettingsService(Settings);
_weatherSettingsService = new WeatherSettingsService(Settings);
Weather = _weatherSettingsService;
Region = new RegionSettingsService(Settings);
@@ -1227,6 +1315,8 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
public IStatusBarSettingsService StatusBar { get; }
public ITextCapsuleSettingsService TextCapsule { get; }
public IWeatherSettingsService Weather { get; }
public IRegionSettingsService Region { get; }

View File

@@ -93,6 +93,7 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
private const uint SWP_NOACTIVATE = 0x0010;
private const int WM_WINDOWPOSCHANGING = 0x0046;
private const int WM_NCHITTEST = 0x0084;
private const int WM_ACTIVATEAPP = 0x001C; // 【新增】应用激活消息
private const int HTTRANSPARENT = -1;
private const int HTCLIENT = 1;
@@ -100,8 +101,25 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
private static readonly Dictionary<IntPtr, bool> _bottomMostWindows = new();
private static readonly Dictionary<IntPtr, IntPtr> _originalWndProcs = new();
private static readonly Dictionary<IntPtr, List<Rect>> _interactiveRegions = new();
// 记录每个窗口的屏幕原点(窗口左上角的屏幕坐标),用于将 WM_NCHITTEST 屏幕坐标转成窗口相对坐标
private static readonly Dictionary<IntPtr, Point> _windowScreenOrigins = new();
private static readonly object _staticLock = new();
// 【修复问题1】静态持有委托引用防止 GC 回收导致 CallbackOnCollectedDelegate 崩溃
private static WndProcDelegate? _wndProcDelegate;
// 【修复问题2】记录每个窗口的 DPI 缩放比例
private static readonly Dictionary<IntPtr, double> _windowDpiScales = new();
// 【修复问题5】Z 轴竞争优化 - 记录上次置底时间,避免频繁操作
private static readonly Dictionary<IntPtr, long> _lastSendToBottomTime = new();
private const long MinSendToBottomIntervalMs = 100; // 【修复置底问题】降低到 100ms提高响应速度
// 【新增】定时器定期强制置底
private static System.Timers.Timer? _keepBottomTimer;
private static readonly object _timerLock = new();
public bool IsBottomMostSupported => true;
public void SetupBottomMost(Window window)
@@ -121,11 +139,13 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
// 设置为桌面子窗口
SetAsDesktopChild(handle);
// 注册置底状态
// 注册置底状态 & 记录窗口屏幕原点
lock (_staticLock)
{
_bottomMostWindows[handle] = true;
_interactiveRegions[handle] = [];
UpdateWindowScreenOrigin(handle);
UpdateWindowDpiScale(handle); // 【修复问题2】初始化 DPI 缩放
}
// 注入消息钩子
@@ -134,6 +154,9 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
// 初始置底
SendToBottomInternal(handle);
// 【新增】启动定时器定期强制置底
StartKeepBottomTimer();
AppLogger.Info("WindowBottomMost", $"Window setup as bottom-most: {handle}");
};
@@ -147,6 +170,8 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
_bottomMostWindows.Remove(handle);
_originalWndProcs.Remove(handle);
_interactiveRegions.Remove(handle);
_windowScreenOrigins.Remove(handle);
_windowDpiScales.Remove(handle); // 【修复问题2】清理 DPI 缩放记录
}
}
};
@@ -169,21 +194,113 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
SetWindowPos(handle, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE);
}
/// <summary>
/// 【新增】启动定时器定期强制置底所有窗口
/// </summary>
private static void StartKeepBottomTimer()
{
lock (_timerLock)
{
if (_keepBottomTimer != null) return;
_keepBottomTimer = new System.Timers.Timer(200); // 每 200ms 检查一次
_keepBottomTimer.Elapsed += (s, e) =>
{
try
{
lock (_staticLock)
{
foreach (var kvp in _bottomMostWindows)
{
if (kvp.Value) // 如果标记为置底
{
SendToBottomInternal(kvp.Key);
}
}
}
}
catch
{
// 忽略定时器错误
}
};
_keepBottomTimer.Start();
}
}
/// <summary>
/// 【新增】停止定时器
/// </summary>
private static void StopKeepBottomTimer()
{
lock (_timerLock)
{
_keepBottomTimer?.Stop();
_keepBottomTimer?.Dispose();
_keepBottomTimer = null;
}
}
private static void SetAsDesktopChild(IntPtr handle)
{
// 【修复问题4】增强桌面挂载逻辑支持 Wallpaper Engine 等动态壁纸软件
// 方案1: 尝试找到 WorkerW 层Wallpaper Engine 创建的层)
var workerW = IntPtr.Zero;
var hDefView = IntPtr.Zero;
// 枚举所有顶层窗口
var windowHandles = new ArrayList();
EnumWindows(EnumWindowsCallback, windowHandles);
foreach (IntPtr h in windowHandles)
{
var hDefView = FindWindowEx(h, IntPtr.Zero, "SHELLDLL_DefView", null);
// 查找 WorkerW 窗口Wallpaper Engine 创建)
var className = GetWindowClassName(h);
if (className == "WorkerW")
{
// 在 WorkerW 下查找 SHELLDLL_DefView
var defView = FindWindowEx(h, IntPtr.Zero, "SHELLDLL_DefView", null);
if (defView != IntPtr.Zero)
{
workerW = h;
hDefView = defView;
break;
}
}
}
// 如果找到了 WorkerW 层,使用它作为父窗口
if (workerW != IntPtr.Zero && hDefView != IntPtr.Zero)
{
SetWindowLong(handle, GWL_HWNDPARENT, hDefView.ToInt32());
AppLogger.Info("WindowBottomMost", "Mounted to WorkerW layer (Wallpaper Engine detected)");
return;
}
// 方案2: 回退到传统方式,查找 Progman 下的 SHELLDLL_DefView
foreach (IntPtr h in windowHandles)
{
hDefView = FindWindowEx(h, IntPtr.Zero, "SHELLDLL_DefView", null);
if (hDefView != IntPtr.Zero)
{
SetWindowLong(handle, GWL_HWNDPARENT, hDefView.ToInt32());
AppLogger.Info("WindowBottomMost", "Mounted to traditional desktop layer");
break;
}
}
}
/// <summary>
/// 【修复问题4】获取窗口类名
/// </summary>
private static string GetWindowClassName(IntPtr hWnd)
{
var buffer = new char[256];
var length = GetClassName(hWnd, buffer, buffer.Length);
return length > 0 ? new string(buffer, 0, length) : string.Empty;
}
private static bool EnumWindowsCallback(IntPtr handle, ArrayList handles)
{
handles.Add(handle);
@@ -198,13 +315,29 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
lock (_staticLock)
{
_originalWndProcs[handle] = originalWndProc;
// 【修复问题1】确保委托实例被静态引用持有防止 GC 回收
_wndProcDelegate ??= SubclassWndProc;
}
SetWindowLongPtr(handle, GWLP_WNDPROC, Marshal.GetFunctionPointerForDelegate<WndProcDelegate>(SubclassWndProc));
SetWindowLongPtr(handle, GWLP_WNDPROC, Marshal.GetFunctionPointerForDelegate(_wndProcDelegate));
}
private static IntPtr SubclassWndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam)
{
// 【新增】处理应用激活消息 - 当其他应用激活时立即置底
if (msg == WM_ACTIVATEAPP)
{
lock (_staticLock)
{
if (_bottomMostWindows.TryGetValue(hWnd, out var isBottomMost) && isBottomMost)
{
// 立即置底,不进行频率限制
SendToBottomInternal(hWnd);
}
}
}
// 处理 WM_WINDOWPOSCHANGING - 保持置底
if (msg == WM_WINDOWPOSCHANGING)
{
@@ -212,7 +345,19 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
{
if (_bottomMostWindows.TryGetValue(hWnd, out var isBottomMost) && isBottomMost)
{
// 【修复问题5】优化 Z 轴竞争 - 限制置底操作频率
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if (_lastSendToBottomTime.TryGetValue(hWnd, out var lastTime))
{
if (now - lastTime < MinSendToBottomIntervalMs)
{
// 跳过过于频繁的置底操作
goto CallOriginal;
}
}
SendToBottomInternal(hWnd);
_lastSendToBottomTime[hWnd] = now;
}
}
}
@@ -220,15 +365,29 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
// 处理 WM_NCHITTEST - 区域级穿透
if (msg == WM_NCHITTEST)
{
// 从 lParam 解析坐标(低字为 X高字为 Y
var x = (short)(wParam.ToInt32() & 0xFFFF);
var y = (short)((wParam.ToInt32() >> 16) & 0xFFFF);
var point = new Point(x, y);
// WM_NCHITTEST 的鼠标坐标在 lParam低16位=X高16位=Y且为屏幕坐标
var screenX = (short)(lParam.ToInt64() & 0xFFFF);
var screenY = (short)((lParam.ToInt64() >> 16) & 0xFFFF);
lock (_staticLock)
{
if (_interactiveRegions.TryGetValue(hWnd, out var regions))
if (_interactiveRegions.TryGetValue(hWnd, out var regions) && regions.Count > 0)
{
// 【修复问题2】获取窗口原点和 DPI 缩放比例
_windowScreenOrigins.TryGetValue(hWnd, out var origin);
_windowDpiScales.TryGetValue(hWnd, out var dpiScale);
if (dpiScale <= 0) dpiScale = 1.0; // 默认缩放为 1.0
// 将屏幕物理像素坐标转为窗口相对坐标
var clientX = screenX - origin.X;
var clientY = screenY - origin.Y;
// 【修复问题2】将物理像素坐标转换为逻辑 DIP 坐标
// _interactiveRegions 存储的是 Avalonia UI 的逻辑 DIP 坐标
var logicalX = clientX / dpiScale;
var logicalY = clientY / dpiScale;
var point = new Point(logicalX, logicalY);
foreach (var region in regions)
{
if (region.Contains(point))
@@ -245,6 +404,7 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
}
// 调用原始窗口过程
CallOriginal:
IntPtr originalWndProc;
lock (_staticLock)
{
@@ -265,9 +425,54 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
lock (_staticLock)
{
_interactiveRegions[handle] = regions;
// 同步刷新屏幕原点DPI 缩放可能影响坐标,每次更新区域时一并刷新)
UpdateWindowScreenOrigin(handle);
UpdateWindowDpiScale(handle); // 【修复问题2】同步更新 DPI 缩放
}
}
/// <summary>
/// 更新指定窗口的屏幕左上角坐标缓存(用于将 WM_NCHITTEST 屏幕坐标转为窗口相对坐标)
/// </summary>
private static void UpdateWindowScreenOrigin(IntPtr handle)
{
if (GetWindowRect(handle, out var rect))
{
_windowScreenOrigins[handle] = new Point(rect.Left, rect.Top);
}
}
/// <summary>
/// 【修复问题2】更新指定窗口的 DPI 缩放比例
/// </summary>
private static void UpdateWindowDpiScale(IntPtr handle)
{
try
{
// 获取窗口所在的显示器 DPI
var monitor = MonitorFromWindow(handle, MONITOR_DEFAULTTONEAREST);
if (monitor != IntPtr.Zero)
{
if (GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, out var dpiX, out var _) == 0)
{
// DPI 缩放比例 = 当前 DPI / 96 (标准 DPI)
_windowDpiScales[handle] = dpiX / 96.0;
}
}
}
catch
{
// 如果获取失败,使用默认缩放 1.0
_windowDpiScales[handle] = 1.0;
}
}
[StructLayout(LayoutKind.Sequential)]
private struct RECT { public int Left, Top, Right, Bottom; }
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
private delegate IntPtr WndProcDelegate(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
@@ -299,6 +504,20 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
[DllImport("user32.dll")]
private static extern IntPtr DefWindowProc(IntPtr hWnd, int uMsg, IntPtr wParam, IntPtr lParam);
// 【修复问题2】DPI 相关的 P/Invoke 声明
private const int MONITOR_DEFAULTTONEAREST = 2;
private const int MDT_EFFECTIVE_DPI = 0;
[DllImport("user32.dll")]
private static extern IntPtr MonitorFromWindow(IntPtr hWnd, int dwFlags);
[DllImport("shcore.dll")]
private static extern int GetDpiForMonitor(IntPtr hmonitor, int dpiType, out uint dpiX, out uint dpiY);
// 【修复问题4】获取窗口类名的 P/Invoke
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int GetClassName(IntPtr hWnd, char[] lpClassName, int nMaxCount);
}
/// <summary>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
@@ -696,18 +696,23 @@ internal static class WindowsIconService
try
{
using var source = Image.FromHbitmap(bitmapHandle);
using var bitmap = new Bitmap(source.Width, source.Height, PixelFormat.Format32bppArgb);
var width = source.Width;
var height = source.Height;
using var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);
using (var graphics = Graphics.FromImage(bitmap))
{
graphics.Clear(Color.Transparent);
graphics.CompositingMode = CompositingMode.SourceOver;
graphics.CompositingMode = CompositingMode.SourceCopy;
graphics.CompositingQuality = CompositingQuality.HighQuality;
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = SmoothingMode.HighQuality;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
graphics.DrawImage(source, 0, 0, source.Width, source.Height);
graphics.DrawImage(source, 0, 0, width, height);
}
FixBitmapAlpha(bitmap);
using var stream = new MemoryStream();
bitmap.Save(stream, ImageFormat.Png);
return stream.ToArray();
@@ -718,6 +723,47 @@ internal static class WindowsIconService
}
}
private static void FixBitmapAlpha(Bitmap bitmap)
{
var width = bitmap.Width;
var height = bitmap.Height;
var rect = new Rectangle(0, 0, width, height);
var data = bitmap.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
try
{
var bytes = Math.Abs(data.Stride) * height;
var buffer = new byte[bytes];
Marshal.Copy(data.Scan0, buffer, 0, bytes);
for (var i = 0; i < bytes; i += 4)
{
var b = buffer[i];
var g = buffer[i + 1];
var r = buffer[i + 2];
var a = buffer[i + 3];
if (a == 0 && (r != 0 || g != 0 || b != 0))
{
a = (byte)Math.Max(r, Math.Max(g, b));
buffer[i + 3] = a;
}
else if (a > 0 && a < 255)
{
buffer[i] = (byte)(b * 255 / a);
buffer[i + 1] = (byte)(g * 255 / a);
buffer[i + 2] = (byte)(r * 255 / a);
}
}
Marshal.Copy(buffer, 0, data.Scan0, bytes);
}
finally
{
bitmap.UnlockBits(data);
}
}
private static bool TryInitializeCom(out bool shouldUninitialize)
{
shouldUninitialize = false;

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Services;
@@ -21,6 +22,12 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
ClockFormats = CreateClockFormats();
ClockPositions = CreateClockPositions();
ClockFontSizes = CreateFontSizes();
TextCapsulePositions = CreateTextCapsulePositions();
NetworkSpeedPositions = CreateNetworkSpeedPositions();
NetworkSpeedDisplayModes = CreateNetworkSpeedDisplayModes();
NetworkSpeedFontSizes = CreateFontSizes();
SpacingModes = CreateSpacingModes();
RefreshLocalizedText();
@@ -31,8 +38,20 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
public IReadOnlyList<SelectionOption> ClockFormats { get; }
public IReadOnlyList<SelectionOption> ClockPositions { get; }
public IReadOnlyList<SelectionOption> TextCapsulePositions { get; }
public IReadOnlyList<SelectionOption> NetworkSpeedPositions { get; }
public IReadOnlyList<SelectionOption> NetworkSpeedDisplayModes { get; }
public IReadOnlyList<SelectionOption> SpacingModes { get; }
public IReadOnlyList<SelectionOption> ClockFontSizes { get; }
public IReadOnlyList<SelectionOption> NetworkSpeedFontSizes { get; }
[ObservableProperty]
private bool _showClock = true;
@@ -42,6 +61,9 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
[ObservableProperty]
private bool _clockTransparentBackground;
[ObservableProperty]
private SelectionOption _selectedClockPosition = new("Left", "Left");
[ObservableProperty]
private SelectionOption _selectedSpacingMode = new("Relaxed", "Relaxed");
@@ -75,6 +97,81 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
[ObservableProperty]
private string _clockTransparentBackgroundDescription = string.Empty;
[ObservableProperty]
private string _clockPositionLabel = string.Empty;
[ObservableProperty]
private SelectionOption _selectedClockFontSize = new("Medium", "Medium");
[ObservableProperty]
private string _clockFontSizeLabel = string.Empty;
[ObservableProperty]
private string _textCapsuleHeader = string.Empty;
[ObservableProperty]
private string _textCapsuleDescription = string.Empty;
[ObservableProperty]
private bool _showTextCapsule;
[ObservableProperty]
private string _textCapsuleContent = "**Hello** World!";
[ObservableProperty]
private SelectionOption _selectedTextCapsulePosition = new("Right", "Right");
[ObservableProperty]
private bool _textCapsuleTransparentBackground;
[ObservableProperty]
private string _textCapsulePositionLabel = string.Empty;
[ObservableProperty]
private string _textCapsuleContentLabel = string.Empty;
[ObservableProperty]
private string _textCapsuleTransparentBackgroundLabel = string.Empty;
[ObservableProperty]
private string _networkSpeedHeader = string.Empty;
[ObservableProperty]
private string _networkSpeedDescription = string.Empty;
[ObservableProperty]
private bool _showNetworkSpeed;
[ObservableProperty]
private SelectionOption _selectedNetworkSpeedPosition = new("Right", "Right");
[ObservableProperty]
private SelectionOption _selectedNetworkSpeedDisplayMode = new("Both", "Both");
[ObservableProperty]
private bool _networkSpeedTransparentBackground;
[ObservableProperty]
private string _networkSpeedPositionLabel = string.Empty;
[ObservableProperty]
private string _networkSpeedDisplayModeLabel = string.Empty;
[ObservableProperty]
private string _networkSpeedTransparentBackgroundLabel = string.Empty;
[ObservableProperty]
private bool _showNetworkTypeIcon;
[ObservableProperty]
private string _showNetworkTypeIconLabel = string.Empty;
[ObservableProperty]
private SelectionOption _selectedNetworkSpeedFontSize = new("Medium", "Medium");
[ObservableProperty]
private string _networkSpeedFontSizeLabel = string.Empty;
[ObservableProperty]
private string _spacingHeader = string.Empty;
@@ -84,6 +181,32 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
[ObservableProperty]
private string _customSpacingLabel = string.Empty;
[ObservableProperty]
private bool _statusBarShadowEnabled;
[ObservableProperty]
private Color _statusBarShadowColor = Colors.Black;
[ObservableProperty]
private double _statusBarShadowOpacity = 30;
public IBrush StatusBarShadowColorBrush => new SolidColorBrush(StatusBarShadowColor);
[ObservableProperty]
private string _statusBarShadowHeader = string.Empty;
[ObservableProperty]
private string _statusBarShadowDescription = string.Empty;
[ObservableProperty]
private string _statusBarShadowEnabledLabel = string.Empty;
[ObservableProperty]
private string _statusBarShadowColorLabel = string.Empty;
[ObservableProperty]
private string _statusBarShadowOpacityLabel = string.Empty;
public void Load()
{
var state = _settingsFacade.StatusBar.Get();
@@ -99,12 +222,59 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
?? ClockFormats[1];
ClockTransparentBackground = state.ClockTransparentBackground;
var clockPosition = NormalizeClockPosition(state.ClockPosition);
SelectedClockPosition = ClockPositions.FirstOrDefault(option =>
string.Equals(option.Value, clockPosition, StringComparison.OrdinalIgnoreCase))
?? ClockPositions[0];
// 时钟字体大小设置
var clockFontSize = NormalizeFontSize(state.ClockFontSize);
SelectedClockFontSize = ClockFontSizes.FirstOrDefault(option =>
string.Equals(option.Value, clockFontSize, StringComparison.OrdinalIgnoreCase))
?? ClockFontSizes[1]; // 默认中等
// 文字胶囊设置
ShowTextCapsule = state.ShowTextCapsule;
TextCapsuleContent = state.TextCapsuleContent ?? "**Hello** World!";
var textCapsulePosition = NormalizeTextCapsulePosition(state.TextCapsulePosition);
SelectedTextCapsulePosition = TextCapsulePositions.FirstOrDefault(option =>
string.Equals(option.Value, textCapsulePosition, StringComparison.OrdinalIgnoreCase))
?? TextCapsulePositions[2]; // 默认靠右
TextCapsuleTransparentBackground = state.TextCapsuleTransparentBackground;
// 网速设置
ShowNetworkSpeed = state.ShowNetworkSpeed;
var networkSpeedPosition = NormalizeNetworkSpeedPosition(state.NetworkSpeedPosition);
SelectedNetworkSpeedPosition = NetworkSpeedPositions.FirstOrDefault(option =>
string.Equals(option.Value, networkSpeedPosition, StringComparison.OrdinalIgnoreCase))
?? NetworkSpeedPositions[2]; // 默认靠右
var networkSpeedDisplayMode = NormalizeNetworkSpeedDisplayMode(state.NetworkSpeedDisplayMode);
SelectedNetworkSpeedDisplayMode = NetworkSpeedDisplayModes.FirstOrDefault(option =>
string.Equals(option.Value, networkSpeedDisplayMode, StringComparison.OrdinalIgnoreCase))
?? NetworkSpeedDisplayModes[0]; // 默认双向
NetworkSpeedTransparentBackground = state.NetworkSpeedTransparentBackground;
ShowNetworkTypeIcon = state.ShowNetworkTypeIcon;
// 网速字体大小设置
var networkSpeedFontSize = NormalizeFontSize(state.NetworkSpeedFontSize);
SelectedNetworkSpeedFontSize = NetworkSpeedFontSizes.FirstOrDefault(option =>
string.Equals(option.Value, networkSpeedFontSize, StringComparison.OrdinalIgnoreCase))
?? NetworkSpeedFontSizes[1]; // 默认中等
var spacingMode = NormalizeSpacingMode(state.SpacingMode);
SelectedSpacingMode = SpacingModes.FirstOrDefault(option =>
string.Equals(option.Value, spacingMode, StringComparison.OrdinalIgnoreCase))
?? SpacingModes[1];
CustomSpacingPercent = Math.Clamp(state.CustomSpacingPercent, 0, 30);
IsCustomSpacingVisible = string.Equals(SelectedSpacingMode.Value, "Custom", StringComparison.OrdinalIgnoreCase);
// 状态栏阴影设置
StatusBarShadowEnabled = state.ShadowEnabled;
if (Color.TryParse(state.ShadowColor, out var shadowColor))
{
StatusBarShadowColor = shadowColor;
}
StatusBarShadowOpacity = Math.Clamp(state.ShadowOpacity * 100, 0, 100);
}
partial void OnShowClockChanged(bool value)
@@ -137,6 +307,126 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
Save();
}
partial void OnSelectedClockPositionChanged(SelectionOption value)
{
if (_isInitializing || value is null)
{
return;
}
Save();
}
partial void OnSelectedClockFontSizeChanged(SelectionOption value)
{
if (_isInitializing || value is null)
{
return;
}
Save();
}
partial void OnShowTextCapsuleChanged(bool value)
{
if (_isInitializing)
{
return;
}
Save();
}
partial void OnTextCapsuleContentChanged(string value)
{
if (_isInitializing)
{
return;
}
Save();
}
partial void OnSelectedTextCapsulePositionChanged(SelectionOption value)
{
if (_isInitializing || value is null)
{
return;
}
Save();
}
partial void OnTextCapsuleTransparentBackgroundChanged(bool value)
{
if (_isInitializing)
{
return;
}
Save();
}
partial void OnShowNetworkSpeedChanged(bool value)
{
if (_isInitializing)
{
return;
}
Save();
}
partial void OnSelectedNetworkSpeedPositionChanged(SelectionOption value)
{
if (_isInitializing || value is null)
{
return;
}
Save();
}
partial void OnSelectedNetworkSpeedDisplayModeChanged(SelectionOption value)
{
if (_isInitializing || value is null)
{
return;
}
Save();
}
partial void OnNetworkSpeedTransparentBackgroundChanged(bool value)
{
if (_isInitializing)
{
return;
}
Save();
}
partial void OnShowNetworkTypeIconChanged(bool value)
{
if (_isInitializing)
{
return;
}
Save();
}
partial void OnSelectedNetworkSpeedFontSizeChanged(SelectionOption value)
{
if (_isInitializing || value is null)
{
return;
}
Save();
}
partial void OnSelectedSpacingModeChanged(SelectionOption value)
{
IsCustomSpacingVisible = string.Equals(value?.Value, "Custom", StringComparison.OrdinalIgnoreCase);
@@ -165,6 +455,37 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
Save();
}
partial void OnStatusBarShadowEnabledChanged(bool value)
{
if (_isInitializing)
{
return;
}
Save();
}
partial void OnStatusBarShadowColorChanged(Color value)
{
OnPropertyChanged(nameof(StatusBarShadowColorBrush));
if (_isInitializing)
{
return;
}
Save();
}
partial void OnStatusBarShadowOpacityChanged(double value)
{
if (_isInitializing)
{
return;
}
Save();
}
private void Save()
{
var state = _settingsFacade.StatusBar.Get();
@@ -184,8 +505,24 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
state.TaskbarLayoutMode,
SelectedClockFormat.Value,
ClockTransparentBackground,
SelectedClockPosition.Value,
SelectedClockFontSize?.Value ?? "Medium",
ShowTextCapsule,
TextCapsuleContent ?? "**Hello** World!",
SelectedTextCapsulePosition?.Value ?? "Right",
TextCapsuleTransparentBackground,
"Medium", // TextCapsuleFontSize - 暂时使用默认值
ShowNetworkSpeed,
SelectedNetworkSpeedPosition?.Value ?? "Right",
SelectedNetworkSpeedDisplayMode?.Value ?? "Both",
NetworkSpeedTransparentBackground,
ShowNetworkTypeIcon,
SelectedNetworkSpeedFontSize?.Value ?? "Medium",
NormalizeSpacingMode(SelectedSpacingMode.Value),
Math.Clamp(CustomSpacingPercent, 0, 30)));
Math.Clamp(CustomSpacingPercent, 0, 30),
StatusBarShadowEnabled,
StatusBarShadowColor.ToString(),
StatusBarShadowOpacity / 100.0));
}
private IReadOnlyList<SelectionOption> CreateClockFormats()
@@ -197,6 +534,46 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
];
}
private IReadOnlyList<SelectionOption> CreateClockPositions()
{
return
[
new SelectionOption("Left", L("settings.status_bar.clock_position.left", "Left")),
new SelectionOption("Center", L("settings.status_bar.clock_position.center", "Center")),
new SelectionOption("Right", L("settings.status_bar.clock_position.right", "Right"))
];
}
private IReadOnlyList<SelectionOption> CreateTextCapsulePositions()
{
return
[
new SelectionOption("Left", L("settings.status_bar.text_capsule_position.left", "Left")),
new SelectionOption("Center", L("settings.status_bar.text_capsule_position.center", "Center")),
new SelectionOption("Right", L("settings.status_bar.text_capsule_position.right", "Right"))
];
}
private IReadOnlyList<SelectionOption> CreateNetworkSpeedPositions()
{
return
[
new SelectionOption("Left", L("settings.status_bar.network_speed_position.left", "Left")),
new SelectionOption("Center", L("settings.status_bar.network_speed_position.center", "Center")),
new SelectionOption("Right", L("settings.status_bar.network_speed_position.right", "Right"))
];
}
private IReadOnlyList<SelectionOption> CreateNetworkSpeedDisplayModes()
{
return
[
new SelectionOption("Both", L("settings.status_bar.network_speed_mode.both", "Upload + Download")),
new SelectionOption("Upload", L("settings.status_bar.network_speed_mode.upload", "Upload only")),
new SelectionOption("Download", L("settings.status_bar.network_speed_mode.download", "Download only"))
];
}
private IReadOnlyList<SelectionOption> CreateSpacingModes()
{
return
@@ -217,9 +594,28 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
ClockFormatLabel = L("settings.status_bar.clock_format_label", "Clock format");
ClockTransparentBackgroundLabel = L("settings.status_bar.clock_transparent_background_label", "Transparent background");
ClockTransparentBackgroundDescription = L("settings.status_bar.clock_transparent_background_desc", "Remove the capsule background and keep only the clock text.");
ClockPositionLabel = L("settings.status_bar.clock_position_label", "Clock position");
ClockFontSizeLabel = L("settings.status_bar.clock_font_size_label", "Font size");
TextCapsuleHeader = L("settings.status_bar.text_capsule_header", "Text Capsule");
TextCapsuleDescription = L("settings.status_bar.text_capsule_description", "Display custom text with Markdown support on the status bar.");
TextCapsulePositionLabel = L("settings.status_bar.text_capsule_position_label", "Text capsule position");
TextCapsuleContentLabel = L("settings.status_bar.text_capsule_content_label", "Text content (Markdown supported)");
TextCapsuleTransparentBackgroundLabel = L("settings.status_bar.text_capsule_transparent_background_label", "Transparent background");
NetworkSpeedHeader = L("settings.status_bar.network_speed_header", "Network Speed");
NetworkSpeedDescription = L("settings.status_bar.network_speed_description", "Display real-time network upload and download speed.");
NetworkSpeedPositionLabel = L("settings.status_bar.network_speed_position_label", "Network speed position");
NetworkSpeedDisplayModeLabel = L("settings.status_bar.network_speed_mode_label", "Display mode");
NetworkSpeedTransparentBackgroundLabel = L("settings.status_bar.network_speed_transparent_background_label", "Transparent background");
ShowNetworkTypeIconLabel = L("settings.status_bar.show_network_type_icon_label", "Show network type icon");
NetworkSpeedFontSizeLabel = L("settings.status_bar.network_speed_font_size_label", "Font size");
SpacingHeader = L("settings.status_bar.spacing_header", "Component Spacing");
SpacingDescription = L("settings.status_bar.spacing_desc", "Adjust spacing between status bar components.");
CustomSpacingLabel = L("settings.status_bar.spacing_custom_label", "Custom spacing (%)");
StatusBarShadowHeader = L("settings.status_bar.shadow_header", "Status Bar Shadow");
StatusBarShadowDescription = L("settings.status_bar.shadow_desc", "Add shadow effect to the status bar for better visibility.");
StatusBarShadowEnabledLabel = L("settings.status_bar.shadow_enabled_label", "Enable shadow");
StatusBarShadowColorLabel = L("settings.status_bar.shadow_color_label", "Shadow color");
StatusBarShadowOpacityLabel = L("settings.status_bar.shadow_opacity_label", "Shadow opacity");
}
private string NormalizeSpacingMode(string? value)
@@ -232,6 +628,66 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
};
}
private static string NormalizeClockPosition(string? value)
{
return value switch
{
_ when string.Equals(value, "Center", StringComparison.OrdinalIgnoreCase) => "Center",
_ when string.Equals(value, "Right", StringComparison.OrdinalIgnoreCase) => "Right",
_ => "Left"
};
}
private static string NormalizeTextCapsulePosition(string? value)
{
return value switch
{
_ when string.Equals(value, "Left", StringComparison.OrdinalIgnoreCase) => "Left",
_ when string.Equals(value, "Center", StringComparison.OrdinalIgnoreCase) => "Center",
_ => "Right"
};
}
private static string NormalizeNetworkSpeedPosition(string? value)
{
return value switch
{
_ when string.Equals(value, "Left", StringComparison.OrdinalIgnoreCase) => "Left",
_ when string.Equals(value, "Center", StringComparison.OrdinalIgnoreCase) => "Center",
_ => "Right"
};
}
private static string NormalizeNetworkSpeedDisplayMode(string? value)
{
return value switch
{
_ when string.Equals(value, "Upload", StringComparison.OrdinalIgnoreCase) => "Upload",
_ when string.Equals(value, "Download", StringComparison.OrdinalIgnoreCase) => "Download",
_ => "Both"
};
}
private static string NormalizeFontSize(string? value)
{
return value switch
{
_ when string.Equals(value, "Small", StringComparison.OrdinalIgnoreCase) => "Small",
_ when string.Equals(value, "Large", StringComparison.OrdinalIgnoreCase) => "Large",
_ => "Medium"
};
}
private IReadOnlyList<SelectionOption> CreateFontSizes()
{
return
[
new SelectionOption("Small", L("settings.status_bar.font_size.small", "Small")),
new SelectionOption("Medium", L("settings.status_bar.font_size.medium", "Medium")),
new SelectionOption("Large", L("settings.status_bar.font_size.large", "Large"))
];
}
private string L(string key, string fallback)
=> _localizationService.GetString(_languageCode, key, fallback);
}

View File

@@ -21,6 +21,12 @@
<ComboBoxItem x:Name="SectlItem"
Classes="component-editor-select-item"
Tag="sectl" />
<ComboBoxItem x:Name="RinLitItem"
Classes="component-editor-select-item"
Tag="rinlit" />
<ComboBoxItem x:Name="JiangtokotoItem"
Classes="component-editor-select-item"
Tag="jiangtokoto" />
</ComboBox>
<TextBlock x:Name="SourceDescriptionTextBlock"
Classes="component-editor-secondary-text"

View File

@@ -29,10 +29,12 @@ public partial class ZhiJiaoHubComponentEditor : ComponentEditorViewBase
SourceLabelTextBlock.Text = L("zhijiaohub.settings.source", "图片源");
ClassIslandItem.Content = L("zhijiaohub.settings.classisland", "ClassIsland 图库");
SectlItem.Content = L("zhijiaohub.settings.sectl", "SECTL 图库");
RinLitItem.Content = L("zhijiaohub.settings.rinlit", "Rin's 图库");
JiangtokotoItem.Content = L("zhijiaohub.settings.jiangtokoto", "Jiangtokoto 表情包");
// 数据源描述
SourceDescriptionTextBlock.Text = L("zhijiaohub.settings.source_desc",
"选择图片来源。ClassIsland 图库包含 ClassIsland 社区的趣味瞬间SECTL 图库包含 SECTL 社区的内容。");
"选择图片来源。ClassIsland 图库包含 ClassIsland 社区的趣味瞬间SECTL 图库包含 SECTL 社区的内容Rin's 图库包含 Rin's 社区的内容Jiangtokoto 表情包包含丰富的表情包资源。");
// 镜像加速源
MirrorSourceLabelTextBlock.Text = L("zhijiaohub.settings.mirror_source", "镜像加速");
@@ -65,6 +67,8 @@ public partial class ZhiJiaoHubComponentEditor : ComponentEditorViewBase
SourceComboBox.SelectedItem = source switch
{
ZhiJiaoHubSources.Sectl => SectlItem,
ZhiJiaoHubSources.RinLit => RinLitItem,
ZhiJiaoHubSources.Jiangtokoto => JiangtokotoItem,
_ => ClassIslandItem
};

View File

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

View File

@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<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"

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
@@ -25,6 +25,7 @@ public partial class ClockWidget : UserControl, IDesktopComponentWidget, ITimeZo
private ClockDisplayFormat _displayFormat = ClockDisplayFormat.HourMinuteSecond;
private bool _transparentBackground;
private double _lastAppliedCellSize = 100;
private string _fontSize = "Medium"; // Small, Medium, Large
public ClockWidget()
{
@@ -72,6 +73,21 @@ public partial class ClockWidget : UserControl, IDesktopComponentWidget, ITimeZo
TransparentBackground = transparentBackground;
}
public string WidgetFontSize
{
get => _fontSize;
set
{
_fontSize = value;
ApplyCellSize(_lastAppliedCellSize);
}
}
public void SetFontSize(string fontSize)
{
WidgetFontSize = fontSize;
}
public void SetTimeZoneService(TimeZoneService timeZoneService)
{
ClearTimeZoneService();
@@ -138,7 +154,14 @@ public partial class ClockWidget : UserControl, IDesktopComponentWidget, ITimeZo
// 3. 核心:满盈字阶 (Filled Typography)
// 使主时间文字占据容器高度的 ~68%,产生饱满的视觉张力
var mainFontSize = targetHeight * 0.68;
// 根据字体大小设置调整基础大小
var fontSizeMultiplier = _fontSize switch
{
"Small" => 0.55,
"Large" => 0.85,
_ => 0.68 // Medium (default)
};
var mainFontSize = targetHeight * fontSizeMultiplier;
MainTimeTextBlock.FontSize = mainFontSize;
MainTimeTextBlock.FontWeight = FontWeight.SemiBold;

View File

@@ -475,7 +475,11 @@ public sealed class DesktopComponentRuntimeRegistry
new DesktopComponentRuntimeRegistration(
BuiltInComponentIds.DesktopZhiJiaoHub,
"component.zhijiao_hub",
() => new ZhiJiaoHubWidget())
() => new ZhiJiaoHubWidget()),
new DesktopComponentRuntimeRegistration(
BuiltInComponentIds.DesktopFileManager,
"component.file_manager",
() => new FileManagerWidget())
];
}

View File

@@ -0,0 +1,138 @@
<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:fi="using:FluentIcons.Avalonia"
mc:Ignorable="d"
d:DesignWidth="320"
d:DesignHeight="320"
x:Class="LanMountainDesktop.Views.Components.FileManagerWidget">
<Border x:Name="RootBorder"
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
Padding="12,10"
ClipToBounds="True">
<Grid RowDefinitions="Auto,Auto,*">
<!-- 导航栏 -->
<Grid Grid.Row="0"
ColumnDefinitions="Auto,Auto,*,Auto"
ColumnSpacing="6">
<!-- 返回按钮 -->
<Button x:Name="BackButton"
Grid.Column="0"
Width="32"
Height="32"
Padding="0"
CornerRadius="16"
Background="{DynamicResource AdaptiveSurfaceOverlayBrush}"
BorderThickness="0"
Click="OnBackButtonClick">
<fi:SymbolIcon Symbol="ArrowLeft"
FontSize="14"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
</Button>
<!-- 主页/盘符按钮 -->
<Button x:Name="HomeButton"
Grid.Column="1"
Width="32"
Height="32"
Padding="0"
CornerRadius="16"
Background="{DynamicResource AdaptiveSurfaceOverlayBrush}"
BorderThickness="0"
Click="OnHomeButtonClick">
<fi:SymbolIcon Symbol="Home"
FontSize="14"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
</Button>
<!-- 路径显示 -->
<Border Grid.Column="2"
Background="{DynamicResource AdaptiveSurfaceOverlayBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
Padding="10,0"
VerticalAlignment="Center"
Height="32">
<TextBlock x:Name="PathTextBlock"
VerticalAlignment="Center"
FontSize="13"
FontWeight="Medium"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
TextTrimming="CharacterEllipsis"
ToolTip.Tip="{Binding $self.Text}"
Text="此电脑" />
</Border>
<!-- 刷新按钮 -->
<Button x:Name="RefreshButton"
Grid.Column="3"
Width="32"
Height="32"
Padding="0"
CornerRadius="16"
Background="{DynamicResource AdaptiveSurfaceOverlayBrush}"
BorderThickness="0"
Click="OnRefreshButtonClick">
<fi:SymbolIcon Symbol="ArrowSync"
FontSize="14"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
</Button>
</Grid>
<!-- 分隔线 -->
<Border Grid.Row="1"
Height="1"
Margin="0,10"
Background="{DynamicResource AdaptiveDividerBrush}" />
<!-- 文件列表 -->
<ScrollViewer Grid.Row="2"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="FileItemsControl">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
<!-- 空状态 -->
<StackPanel x:Name="EmptyStatePanel"
Grid.Row="2"
IsVisible="False"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8">
<fi:SymbolIcon Symbol="FolderOpen"
FontSize="40"
Foreground="{DynamicResource AdaptiveTextMutedBrush}" />
<TextBlock x:Name="EmptyStateTextBlock"
Text="文件夹为空"
FontSize="13"
Foreground="{DynamicResource AdaptiveTextMutedBrush}" />
</StackPanel>
<!-- 错误状态 -->
<StackPanel x:Name="ErrorStatePanel"
Grid.Row="2"
IsVisible="False"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8">
<fi:SymbolIcon Symbol="ErrorCircle"
FontSize="40"
Foreground="{DynamicResource AdaptiveErrorBrush}" />
<TextBlock x:Name="ErrorStateTextBlock"
Text="无法访问此文件夹"
FontSize="13"
Foreground="{DynamicResource AdaptiveErrorBrush}" />
</StackPanel>
</Grid>
</Border>
</UserControl>

View File

@@ -0,0 +1,819 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using FluentIcons.Avalonia;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class FileManagerWidget : UserControl,
IDesktopComponentWidget,
IDesktopPageVisibilityAwareComponentWidget,
IComponentPlacementContextAware,
IDisposable
{
private readonly List<string> _navigationHistory = new();
private int _currentHistoryIndex = -1;
private string _currentPath = string.Empty;
private string _componentId = BuiltInComponentIds.DesktopFileManager;
private string _placementId = string.Empty;
private double _currentCellSize = 48;
private bool _isOnActivePage;
private bool _isEditMode;
private bool _isAttached;
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,
FileSystemItem Item,
Border Border
);
public FileManagerWidget()
{
InitializeComponent();
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
ApplyCellSize(_currentCellSize);
NavigateToDrives();
}
public void ApplyCellSize(double cellSize)
{
_currentCellSize = Math.Max(1, cellSize);
var mainRectangleCornerRadius = ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadius();
RootBorder.CornerRadius = mainRectangleCornerRadius;
RootBorder.Padding = new Thickness(
Math.Clamp(_currentCellSize * 0.25, 10, 20),
Math.Clamp(_currentCellSize * 0.20, 8, 16));
ApplyLayoutMetrics();
}
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
{
_isOnActivePage = isOnActivePage;
_isEditMode = isEditMode;
if (_isOnActivePage && _isAttached && !string.IsNullOrEmpty(_currentPath))
{
RefreshCurrentDirectory();
}
}
public void SetComponentPlacementContext(string componentId, string? placementId)
{
_componentId = string.IsNullOrWhiteSpace(componentId)
? BuiltInComponentIds.DesktopFileManager
: componentId.Trim();
_placementId = placementId?.Trim() ?? string.Empty;
}
public void Dispose()
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
AttachedToVisualTree -= OnAttachedToVisualTree;
DetachedFromVisualTree -= OnDetachedFromVisualTree;
SizeChanged -= OnSizeChanged;
_gestureStates.Clear();
}
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_ = sender;
_ = e;
_isAttached = true;
if (_isOnActivePage)
{
RefreshCurrentDirectory();
}
}
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_ = sender;
_ = e;
_isAttached = false;
}
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
{
_ = sender;
_ = e;
ApplyLayoutMetrics();
}
private void ApplyLayoutMetrics()
{
var scale = ResolveScale();
var width = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * 4;
var buttonSize = Math.Clamp(32 * scale, 28, 40);
var iconSize = Math.Clamp(14 * scale, 12, 18);
var pathFontSize = Math.Clamp(13 * scale, 11, 16);
BackButton.Width = buttonSize;
BackButton.Height = buttonSize;
BackButton.CornerRadius = new CornerRadius(buttonSize / 2);
HomeButton.Width = buttonSize;
HomeButton.Height = buttonSize;
HomeButton.CornerRadius = new CornerRadius(buttonSize / 2);
RefreshButton.Width = buttonSize;
RefreshButton.Height = buttonSize;
RefreshButton.CornerRadius = new CornerRadius(buttonSize / 2);
PathTextBlock.FontSize = pathFontSize;
if (BackButton.Content is SymbolIcon backIcon)
{
backIcon.FontSize = iconSize;
}
if (HomeButton.Content is SymbolIcon homeIcon)
{
homeIcon.FontSize = iconSize;
}
if (RefreshButton.Content is SymbolIcon refreshIcon)
{
refreshIcon.FontSize = iconSize;
}
}
private double ResolveScale()
{
var cellScale = Math.Clamp(_currentCellSize / 48d, 0.72, 2.2);
var widthScale = Bounds.Width > 1 ? Math.Clamp(Bounds.Width / 280d, 0.72, 2.4) : 1;
var heightScale = Bounds.Height > 1 ? Math.Clamp(Bounds.Height / 280d, 0.72, 2.4) : 1;
return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale)), 0.72, 2.2);
}
private void OnBackButtonClick(object? sender, RoutedEventArgs e)
{
_ = sender;
_ = e;
if (_currentHistoryIndex > 0)
{
_currentHistoryIndex--;
var path = _navigationHistory[_currentHistoryIndex];
LoadDirectory(path, addToHistory: false);
}
else if (_currentHistoryIndex == 0 && _navigationHistory.Count > 0)
{
NavigateToDrives();
}
}
private void OnHomeButtonClick(object? sender, RoutedEventArgs e)
{
_ = sender;
_ = e;
NavigateToDrives();
}
private void OnRefreshButtonClick(object? sender, RoutedEventArgs e)
{
_ = sender;
_ = e;
RefreshCurrentDirectory();
}
private void OnItemPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (sender is not Border border || border.DataContext is not FileSystemItem item)
{
return;
}
var pointer = e.GetCurrentPoint(border);
var pointerId = e.Pointer.Id;
var position = pointer.Position;
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
_gestureStates[pointerId] = new PointerGestureState(position, timestamp, item, border);
e.Pointer.Capture(border);
}
private void OnItemPointerMoved(object? sender, PointerEventArgs e)
{
if (sender is not Border border)
{
return;
}
var pointerId = e.Pointer.Id;
if (!_gestureStates.TryGetValue(pointerId, out var state))
{
return;
}
var currentPoint = e.GetCurrentPoint(border);
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);
}
}
private void OnItemPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (sender is not Border border)
{
return;
}
var pointerId = e.Pointer.Id;
if (!_gestureStates.Remove(pointerId, out var state))
{
return;
}
e.Pointer.Capture(null);
var currentPoint = e.GetCurrentPoint(border);
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)
{
if (state.Item.IsDirectory)
{
LoadDirectory(state.Item.FullPath, addToHistory: true);
}
else
{
OpenFile(state.Item.FullPath);
}
}
}
private void NavigateToDrives()
{
_navigationHistory.Clear();
_currentHistoryIndex = -1;
_currentPath = string.Empty;
try
{
var drives = new List<FileSystemItem>();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
foreach (var drive in DriveInfo.GetDrives())
{
try
{
if (!drive.IsReady)
{
continue;
}
var item = FileSystemItem.FromDriveInfo(drive);
drives.Add(item);
}
catch (Exception ex)
{
AppLogger.Warn("FileManagerWidget", $"Failed to access drive: {drive?.Name}", ex);
}
}
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
drives.Add(new FileSystemItem
{
Name = "根目录",
FullPath = "/",
ItemType = FileSystemItemType.Directory
});
var homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (!string.IsNullOrEmpty(homePath) && Directory.Exists(homePath))
{
drives.Add(new FileSystemItem
{
Name = "主目录",
FullPath = homePath,
ItemType = FileSystemItemType.Directory
});
}
var linuxMountPoints = new[] { "/mnt", "/media", "/run/media" };
foreach (var mount in linuxMountPoints)
{
if (Directory.Exists(mount))
{
drives.Add(new FileSystemItem
{
Name = mount,
FullPath = mount,
ItemType = FileSystemItemType.Directory
});
}
}
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
drives.Add(new FileSystemItem
{
Name = "根目录",
FullPath = "/",
ItemType = FileSystemItemType.Directory
});
drives.Add(new FileSystemItem
{
Name = "用户",
FullPath = "/Users",
ItemType = FileSystemItemType.Directory
});
drives.Add(new FileSystemItem
{
Name = "应用程序",
FullPath = "/Applications",
ItemType = FileSystemItemType.Directory
});
var homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (!string.IsNullOrEmpty(homePath) && Directory.Exists(homePath))
{
drives.Add(new FileSystemItem
{
Name = "个人",
FullPath = homePath,
ItemType = FileSystemItemType.Directory
});
}
if (Directory.Exists("/Volumes"))
{
foreach (var volume in Directory.GetDirectories("/Volumes"))
{
drives.Add(new FileSystemItem
{
Name = Path.GetFileName(volume),
FullPath = volume,
ItemType = FileSystemItemType.Directory
});
}
}
}
RenderFileItems(drives);
PathTextBlock.Text = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "此电脑" : "文件系统";
UpdateEmptyState(drives.Count == 0, "没有可用的位置");
ErrorStatePanel.IsVisible = false;
}
catch (Exception ex)
{
AppLogger.Warn("FileManagerWidget", "Failed to load drives.", ex);
ShowError("无法加载位置列表");
}
}
private void LoadDirectory(string path, bool addToHistory)
{
if (string.IsNullOrWhiteSpace(path))
{
NavigateToDrives();
return;
}
try
{
var directoryInfo = new DirectoryInfo(path);
if (!directoryInfo.Exists)
{
ShowError("文件夹不存在");
return;
}
var items = new List<FileSystemItem>();
// 添加子文件夹
try
{
var directories = directoryInfo.GetDirectories()
.Where(d => (d.Attributes & FileAttributes.Hidden) == 0)
.OrderBy(d => d.Name)
.Select(FileSystemItem.FromDirectoryInfo);
items.AddRange(directories);
}
catch (UnauthorizedAccessException)
{
// 忽略无权限访问的文件夹
}
// 添加文件
try
{
var files = directoryInfo.GetFiles()
.Where(f => (f.Attributes & FileAttributes.Hidden) == 0)
.OrderBy(f => f.Name)
.Select(FileSystemItem.FromFileInfo);
items.AddRange(files);
}
catch (UnauthorizedAccessException)
{
// 忽略无权限访问的文件
}
RenderFileItems(items);
_currentPath = path;
PathTextBlock.Text = FormatPathForDisplay(path);
if (addToHistory)
{
// 移除当前位置之后的历史记录
if (_currentHistoryIndex < _navigationHistory.Count - 1)
{
_navigationHistory.RemoveRange(_currentHistoryIndex + 1, _navigationHistory.Count - _currentHistoryIndex - 1);
}
_navigationHistory.Add(path);
_currentHistoryIndex = _navigationHistory.Count - 1;
}
UpdateEmptyState(items.Count == 0, "文件夹为空");
ErrorStatePanel.IsVisible = false;
}
catch (UnauthorizedAccessException)
{
ShowError("没有权限访问此文件夹");
}
catch (Exception ex)
{
AppLogger.Warn("FileManagerWidget", $"Failed to load directory: {path}", ex);
ShowError("无法加载文件夹内容");
}
}
private void RenderFileItems(List<FileSystemItem> items)
{
FileItemsControl.ItemsSource = null;
FileItemsControl.Items.Clear();
foreach (var item in items)
{
var itemControl = CreateFileItemControl(item);
FileItemsControl.Items.Add(itemControl);
}
}
private Control CreateFileItemControl(FileSystemItem item)
{
var scale = ResolveScale();
var itemWidth = Math.Clamp(72 * scale, 64, 96);
var itemHeight = Math.Clamp(80 * scale, 72, 108);
var iconSize = Math.Clamp(32 * scale, 24, 40);
var fontSize = Math.Clamp(11 * scale, 10, 14);
var textBrush = this.FindResource("AdaptiveTextPrimaryBrush") as IBrush ?? new SolidColorBrush(Colors.White);
var border = new Border
{
Width = itemWidth,
Height = itemHeight,
Margin = new Thickness(4),
CornerRadius = new CornerRadius(8),
Background = new SolidColorBrush(Colors.Transparent),
Cursor = new Cursor(StandardCursorType.Hand),
DataContext = item
};
var grid = new Grid
{
RowDefinitions = new RowDefinitions("*,Auto"),
Margin = new Thickness(4)
};
var iconImage = CreateSystemIconImage(item, iconSize);
var textBlock = new TextBlock
{
Text = item.Name,
FontSize = fontSize,
TextAlignment = TextAlignment.Center,
TextTrimming = TextTrimming.CharacterEllipsis,
MaxLines = 2,
TextWrapping = TextWrapping.Wrap,
Foreground = textBrush
};
if (iconImage is not null)
{
grid.Children.Add(iconImage);
Grid.SetRow(iconImage, 0);
}
grid.Children.Add(textBlock);
Grid.SetRow(textBlock, 1);
border.Child = grid;
ToolTip.SetTip(border, item.Name);
border.PointerPressed += OnItemPointerPressed;
border.PointerMoved += OnItemPointerMoved;
border.PointerReleased += OnItemPointerReleased;
return border;
}
private Control? CreateSystemIconImage(FileSystemItem item, double iconSize)
{
byte[]? pngBytes = null;
try
{
if (OperatingSystem.IsWindows())
{
pngBytes = item.ItemType switch
{
FileSystemItemType.Drive => GetDriveIconBytes(item.FullPath),
FileSystemItemType.Directory => WindowsIconService.TryGetSystemFolderIconPngBytes(),
_ => WindowsIconService.TryGetIconPngBytes(item.FullPath)
};
}
else if (OperatingSystem.IsLinux())
{
pngBytes = item.ItemType switch
{
FileSystemItemType.Drive => LinuxIconService.TryGetDriveIconPngBytes(),
FileSystemItemType.Directory => LinuxIconService.TryGetSystemFolderIconPngBytes(),
_ => LinuxIconService.TryGetIconPngBytes(item.FullPath)
};
}
else if (OperatingSystem.IsMacOS())
{
pngBytes = item.ItemType switch
{
FileSystemItemType.Drive => MacIconService.TryGetDriveIconPngBytes(),
FileSystemItemType.Directory => MacIconService.TryGetSystemFolderIconPngBytes(),
_ => MacIconService.TryGetIconPngBytes(item.FullPath)
};
}
}
catch
{
pngBytes = null;
}
if (pngBytes is not null)
{
try
{
using var stream = new MemoryStream(pngBytes);
var bitmap = new Bitmap(stream);
return new Image
{
Source = bitmap,
Width = iconSize,
Height = iconSize,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
Stretch = Stretch.Uniform
};
}
catch
{
}
}
return CreateFallbackIconImage(item, iconSize);
}
private static byte[]? GetDriveIconBytes(string drivePath)
{
if (string.IsNullOrWhiteSpace(drivePath))
{
return null;
}
try
{
if (OperatingSystem.IsWindows())
{
if (Directory.Exists(drivePath))
{
return WindowsIconService.TryGetIconPngBytes(drivePath);
}
}
else if (OperatingSystem.IsLinux())
{
return LinuxIconService.TryGetDriveIconPngBytes();
}
else if (OperatingSystem.IsMacOS())
{
return MacIconService.TryGetDriveIconPngBytes();
}
}
catch
{
}
if (OperatingSystem.IsWindows())
{
return WindowsIconService.TryGetSystemFolderIconPngBytes();
}
else if (OperatingSystem.IsLinux())
{
return LinuxIconService.TryGetSystemFolderIconPngBytes();
}
else if (OperatingSystem.IsMacOS())
{
return MacIconService.TryGetSystemFolderIconPngBytes();
}
return null;
}
private Control CreateFallbackIconImage(FileSystemItem item, double iconSize)
{
var symbol = item.ItemType switch
{
FileSystemItemType.Drive => FluentIcons.Common.Symbol.HardDrive,
FileSystemItemType.Directory => FluentIcons.Common.Symbol.Folder,
_ => FluentIcons.Common.Symbol.Document
};
var iconBrush = item.ItemType == FileSystemItemType.File
? this.FindResource("AdaptiveTextSecondaryBrush") as IBrush ?? new SolidColorBrush(Colors.Gray)
: this.FindResource("AdaptiveAccentBrush") as IBrush ?? new SolidColorBrush(Colors.DodgerBlue);
return new SymbolIcon
{
Symbol = symbol,
FontSize = iconSize,
Foreground = iconBrush,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
};
}
private void RefreshCurrentDirectory()
{
if (string.IsNullOrEmpty(_currentPath))
{
NavigateToDrives();
}
else
{
LoadDirectory(_currentPath, addToHistory: false);
}
}
private void UpdateEmptyState(bool isEmpty, string message)
{
EmptyStatePanel.IsVisible = isEmpty;
EmptyStateTextBlock.Text = message;
FileItemsControl.IsVisible = !isEmpty;
}
private void ShowError(string message)
{
ErrorStatePanel.IsVisible = true;
ErrorStateTextBlock.Text = message;
FileItemsControl.IsVisible = false;
EmptyStatePanel.IsVisible = false;
}
private static void OpenFile(string filePath)
{
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Process.Start(new ProcessStartInfo(filePath)
{
UseShellExecute = true
});
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Process.Start("xdg-open", filePath);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
Process.Start("open", filePath);
}
}
catch (Exception ex)
{
AppLogger.Warn("FileManagerWidget", $"Failed to open file: {filePath}", ex);
}
}
private static string FormatPathForDisplay(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "此电脑" : "文件系统";
}
var separator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? '\\' : '/';
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (path.Length <= 3 && path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase))
{
try
{
var driveInfo = new DriveInfo(path.Substring(0, 1));
if (!string.IsNullOrWhiteSpace(driveInfo.VolumeLabel))
{
return $"{driveInfo.VolumeLabel} ({path.Substring(0, 2)})";
}
}
catch
{
}
return path;
}
}
else
{
if (path == "/")
{
return "根目录";
}
if (path == Environment.GetFolderPath(Environment.SpecialFolder.UserProfile))
{
return "主目录";
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
if (path == "/Applications")
{
return "应用程序";
}
if (path == "/Users")
{
return "用户";
}
if (path.StartsWith("/Volumes/"))
{
return Path.GetFileName(path);
}
}
}
var parts = path.Split(new[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length <= 3)
{
return path;
}
return $"{parts[0]}{separator}...{separator}{parts[^2]}{separator}{parts[^1]}";
}
}

View File

@@ -0,0 +1,72 @@
<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:fi="using:FluentIcons.Avalonia"
mc:Ignorable="d"
d:DesignWidth="160"
d:DesignHeight="48"
x:Class="LanMountainDesktop.Views.Components.NetworkSpeedWidget">
<Border x:Name="RootBorder"
Classes="surface-translucent-panel"
Padding="0"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="12,0">
<!-- 上传速度 -->
<StackPanel x:Name="UploadPanel"
Orientation="Horizontal"
VerticalAlignment="Center">
<TextBlock Text="↑"
FontSize="12"
Opacity="0.7"
VerticalAlignment="Center"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"/>
<TextBlock x:Name="UploadSpeedTextBlock"
FontSize="14"
FontWeight="SemiBold"
Margin="2,0,0,0"
VerticalAlignment="Center"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"/>
</StackPanel>
<!-- 分隔符 -->
<Rectangle x:Name="Separator"
Width="1"
Height="16"
Margin="8,0"
Opacity="0.3"
Fill="{DynamicResource AdaptiveTextSecondaryBrush}"/>
<!-- 下载速度 -->
<StackPanel x:Name="DownloadPanel"
Orientation="Horizontal"
VerticalAlignment="Center">
<TextBlock Text="↓"
FontSize="12"
Opacity="0.7"
VerticalAlignment="Center"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"/>
<TextBlock x:Name="DownloadSpeedTextBlock"
FontSize="14"
FontWeight="SemiBold"
Margin="2,0,0,0"
VerticalAlignment="Center"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"/>
</StackPanel>
<!-- 网络类型图标 -->
<fi:SymbolIcon x:Name="NetworkTypeIcon"
Symbol="Globe"
FontSize="14"
Margin="8,0,0,0"
Opacity="0.8"
IsVisible="False"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"/>
</StackPanel>
</Border>
</UserControl>

View File

@@ -0,0 +1,451 @@
using System;
using System.Linq;
using System.Net.NetworkInformation;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Threading;
using FluentIcons.Avalonia;
using FluentIcons.Common;
using LanMountainDesktop.Services;
using Symbol = FluentIcons.Common.Symbol;
namespace LanMountainDesktop.Views.Components;
public partial class NetworkSpeedWidget : UserControl, IDesktopComponentWidget
{
private readonly DispatcherTimer _timer = new();
private readonly DispatcherTimer _networkTypeTimer = new();
private NetworkInterface? _selectedInterface;
private long _lastBytesReceived;
private long _lastBytesSent;
private bool _isFirstUpdate = true;
private double _lastAppliedCellSize = 100;
private bool _transparentBackground;
private string _displayMode = "Both"; // "Upload", "Download", "Both"
private bool _showNetworkTypeIcon;
private string _fontSize = "Medium"; // Small, Medium, Large
public NetworkSpeedWidget()
{
InitializeComponent();
SetupTimer();
SelectBestInterface();
UpdateDisplayMode();
UpdateNetworkTypeIcon();
}
public string DisplayMode
{
get => _displayMode;
set
{
if (_displayMode == value) return;
_displayMode = value;
UpdateDisplayMode();
}
}
public bool TransparentBackground
{
get => _transparentBackground;
set
{
if (_transparentBackground == value) return;
_transparentBackground = value;
ApplyChrome();
ApplyCellSize(_lastAppliedCellSize);
}
}
public bool ShowNetworkTypeIcon
{
get => _showNetworkTypeIcon;
set
{
if (_showNetworkTypeIcon == value) return;
_showNetworkTypeIcon = value;
UpdateNetworkTypeIcon();
}
}
public void SetDisplayMode(string mode)
{
DisplayMode = mode;
}
public void SetTransparentBackground(bool transparent)
{
TransparentBackground = transparent;
}
public void SetShowNetworkTypeIcon(bool show)
{
ShowNetworkTypeIcon = show;
}
public string WidgetFontSize
{
get => _fontSize;
set
{
_fontSize = value;
ApplyCellSize(_lastAppliedCellSize);
}
}
public void SetFontSize(string fontSize)
{
WidgetFontSize = fontSize;
}
private void SetupTimer()
{
// 网速更新定时器(每秒)
_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += (_, _) => UpdateSpeed();
_timer.Start();
// 网络类型检测定时器每500ms满足响应延迟要求
_networkTypeTimer.Interval = TimeSpan.FromMilliseconds(500);
_networkTypeTimer.Tick += (_, _) => UpdateNetworkTypeIcon();
_networkTypeTimer.Start();
}
private void SelectBestInterface()
{
try
{
var interfaces = NetworkInterface.GetAllNetworkInterfaces()
.Where(ni => ni.OperationalStatus == OperationalStatus.Up)
.Where(ni => ni.NetworkInterfaceType != NetworkInterfaceType.Loopback)
.Where(ni => ni.NetworkInterfaceType != NetworkInterfaceType.Tunnel)
.Where(ni => !ni.Description.Contains("Virtual", StringComparison.OrdinalIgnoreCase))
.Where(ni => !ni.Description.Contains("VPN", StringComparison.OrdinalIgnoreCase))
.ToList();
// 优先选择有流量的物理网卡
_selectedInterface = interfaces
.OrderByDescending(ni => ni.GetIPv4Statistics().BytesReceived + ni.GetIPv4Statistics().BytesSent)
.FirstOrDefault();
// 如果没有找到,选择第一个活动的非虚拟网卡
_selectedInterface ??= interfaces.FirstOrDefault();
if (_selectedInterface != null)
{
var stats = _selectedInterface.GetIPv4Statistics();
_lastBytesReceived = stats.BytesReceived;
_lastBytesSent = stats.BytesSent;
}
}
catch
{
// 忽略错误,下次重试
}
}
private void UpdateSpeed()
{
try
{
// 如果当前网卡不可用,尝试重新选择
if (_selectedInterface == null ||
_selectedInterface.OperationalStatus != OperationalStatus.Up)
{
SelectBestInterface();
}
if (_selectedInterface == null)
{
UploadSpeedTextBlock.Text = "--";
DownloadSpeedTextBlock.Text = "--";
return;
}
var stats = _selectedInterface.GetIPv4Statistics();
var currentBytesReceived = stats.BytesReceived;
var currentBytesSent = stats.BytesSent;
if (_isFirstUpdate)
{
_lastBytesReceived = currentBytesReceived;
_lastBytesSent = currentBytesSent;
_isFirstUpdate = false;
return;
}
// 计算速度(每秒字节数)
var downloadBytes = currentBytesReceived - _lastBytesReceived;
var uploadBytes = currentBytesSent - _lastBytesSent;
// 处理计数器重置的情况
if (downloadBytes < 0) downloadBytes = 0;
if (uploadBytes < 0) uploadBytes = 0;
UploadSpeedTextBlock.Text = FormatSpeed(uploadBytes);
DownloadSpeedTextBlock.Text = FormatSpeed(downloadBytes);
_lastBytesReceived = currentBytesReceived;
_lastBytesSent = currentBytesSent;
}
catch
{
// 错误时显示 --
UploadSpeedTextBlock.Text = "--";
DownloadSpeedTextBlock.Text = "--";
}
}
private void UpdateNetworkTypeIcon()
{
try
{
if (!_showNetworkTypeIcon || NetworkTypeIcon == null)
{
if (NetworkTypeIcon != null)
NetworkTypeIcon.IsVisible = false;
return;
}
// 获取当前活动的网络接口
var activeInterface = GetActiveNetworkInterface();
if (activeInterface == null)
{
// 无网络连接
NetworkTypeIcon.Symbol = Symbol.DismissCircle;
NetworkTypeIcon.IsVisible = true;
return;
}
// 根据网络类型设置图标
switch (activeInterface.NetworkInterfaceType)
{
case NetworkInterfaceType.Wireless80211:
// WiFi
NetworkTypeIcon.Symbol = Symbol.WiFi;
break;
case NetworkInterfaceType.Ethernet:
// 有线网络 - 检查是否是移动网络热点
if (IsLikelyMobileHotspot(activeInterface))
{
NetworkTypeIcon.Symbol = Symbol.Phone;
}
else
{
NetworkTypeIcon.Symbol = Symbol.PlugConnected;
}
break;
default:
// 其他类型,尝试根据描述判断
var symbol = GetSymbolFromDescription(activeInterface.Description);
NetworkTypeIcon.Symbol = symbol;
break;
}
NetworkTypeIcon.IsVisible = true;
}
catch
{
// 错误时隐藏图标
if (NetworkTypeIcon != null)
NetworkTypeIcon.IsVisible = false;
}
}
private NetworkInterface? GetActiveNetworkInterface()
{
try
{
// 优先使用当前选中的网卡
if (_selectedInterface != null &&
_selectedInterface.OperationalStatus == OperationalStatus.Up)
{
return _selectedInterface;
}
// 否则查找最佳网卡
var interfaces = NetworkInterface.GetAllNetworkInterfaces()
.Where(ni => ni.OperationalStatus == OperationalStatus.Up)
.Where(ni => ni.NetworkInterfaceType != NetworkInterfaceType.Loopback)
.Where(ni => ni.NetworkInterfaceType != NetworkInterfaceType.Tunnel)
.ToList();
// 优先返回有流量的网卡
return interfaces
.OrderByDescending(ni => ni.GetIPv4Statistics().BytesReceived + ni.GetIPv4Statistics().BytesSent)
.FirstOrDefault();
}
catch
{
return null;
}
}
private static bool IsLikelyMobileHotspot(NetworkInterface ni)
{
// 通过描述判断是否是移动热点
var desc = ni.Description.ToLowerInvariant();
return desc.Contains("mobile") ||
desc.Contains("cellular") ||
desc.Contains("phone") ||
desc.Contains("tether");
}
private static Symbol GetSymbolFromDescription(string description)
{
var desc = description.ToLowerInvariant();
if (desc.Contains("wifi") || desc.Contains("wi-fi") || desc.Contains("wireless"))
return Symbol.WiFi;
if (desc.Contains("ethernet") || desc.Contains("lan") || desc.Contains("wired"))
return Symbol.PlugConnected;
if (desc.Contains("cellular") || desc.Contains("mobile") || desc.Contains("lte") || desc.Contains("5g") || desc.Contains("4g"))
return Symbol.Phone;
if (desc.Contains("bluetooth"))
return Symbol.Bluetooth;
// 默认使用 Globe 图标
return Symbol.Globe;
}
private static string FormatSpeed(long bytesPerSecond)
{
// 根据数值大小决定显示格式始终保持3个字符宽度
// 例如: 1.23, 12.3, 123
return bytesPerSecond switch
{
>= 1024 * 1024 * 1024 => FormatWithThreeDigits(bytesPerSecond / (1024.0 * 1024.0 * 1024.0), "G"),
>= 1024 * 1024 => FormatWithThreeDigits(bytesPerSecond / (1024.0 * 1024.0), "M"),
>= 1024 => FormatWithThreeDigits(bytesPerSecond / 1024.0, "K"),
_ => FormatWithThreeDigits(bytesPerSecond, "B")
};
}
/// <summary>
/// 格式化数字始终保持3个有效数字的显示宽度
/// </summary>
private static string FormatWithThreeDigits(double value, string unit)
{
// 根据数值大小决定小数位数,确保总宽度一致
// < 10: 显示两位小数 (如 1.23)
// 10-99: 显示一位小数 (如 12.3)
// >= 100: 显示整数 (如 123)
string formatted = value switch
{
< 10 => $"{value:F2}",
< 100 => $"{value:F1}",
_ => $"{value:F0}"
};
return formatted + unit;
}
private void UpdateDisplayMode()
{
switch (_displayMode)
{
case "Upload":
UploadPanel.IsVisible = true;
DownloadPanel.IsVisible = false;
Separator.IsVisible = false;
break;
case "Download":
UploadPanel.IsVisible = false;
DownloadPanel.IsVisible = true;
Separator.IsVisible = false;
break;
case "Both":
default:
UploadPanel.IsVisible = true;
DownloadPanel.IsVisible = true;
Separator.IsVisible = true;
break;
}
}
public void ApplyCellSize(double cellSize)
{
_lastAppliedCellSize = cellSize;
// 计算组件高度:保持与任务栏核心比例一致 (0.74x)
var targetHeight = Math.Clamp(cellSize * 0.74, 34, 74);
RootBorder.Height = targetHeight;
// 主矩形统一到主题主档圆角
RootBorder.CornerRadius = ResolveUnifiedMainRectangle();
RootBorder.VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center;
// 根据单元格大小和字体大小设置调整字体大小
var fontSizeMultiplier = _fontSize switch
{
"Small" => 0.32,
"Large" => 0.48,
_ => 0.4 // Medium (default)
};
var fontSize = Math.Clamp(targetHeight * fontSizeMultiplier, 11, 22);
UploadSpeedTextBlock.FontSize = fontSize;
DownloadSpeedTextBlock.FontSize = fontSize;
// 调整图标大小
if (NetworkTypeIcon != null)
{
NetworkTypeIcon.FontSize = Math.Clamp(targetHeight * 0.35, 10, 18);
}
// 设置最小和最大宽度
RootBorder.MinWidth = cellSize * 1.5;
RootBorder.MaxWidth = cellSize * 5;
if (_transparentBackground)
{
RootBorder.MinWidth = 0;
RootBorder.Padding = new Thickness(Math.Clamp(cellSize * 0.06, 4, 10), 0);
return;
}
// 确保清除可能存在的固定 Padding由代码控制"紧密感"
RootBorder.Padding = new Thickness(Math.Clamp(cellSize * 0.15, 12, 24), 0);
}
private void ApplyChrome()
{
if (_transparentBackground)
{
RootBorder.Classes.Remove("glass-panel");
RootBorder.Background = Brushes.Transparent;
RootBorder.BorderBrush = Brushes.Transparent;
RootBorder.BorderThickness = new Thickness(0);
RootBorder.BoxShadow = default;
return;
}
if (!RootBorder.Classes.Contains("glass-panel"))
{
RootBorder.Classes.Add("glass-panel");
}
RootBorder.ClearValue(Border.BackgroundProperty);
RootBorder.ClearValue(Border.BorderBrushProperty);
RootBorder.ClearValue(Border.BorderThicknessProperty);
RootBorder.ClearValue(Border.BoxShadowProperty);
}
private CornerRadius ResolveUnifiedMainRectangle() => new(ResolveUnifiedMainRadiusValue());
private static double ResolveUnifiedMainRadiusValue() =>
HostAppearanceThemeProvider.GetOrCreate().GetCurrent().CornerRadiusTokens.Lg.TopLeft;
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_timer?.Stop();
_networkTypeTimer?.Stop();
}
}

View File

@@ -0,0 +1,22 @@
<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:md="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia"
mc:Ignorable="d"
d:DesignWidth="200"
d:DesignHeight="48"
x:Class="LanMountainDesktop.Views.Components.TextCapsuleWidget">
<Border x:Name="RootBorder"
Classes="surface-translucent-panel"
Padding="0"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}">
<md:MarkdownScrollViewer x:Name="MarkdownViewer"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="12,6"
MaxWidth="400" />
</Border>
</UserControl>

View File

@@ -0,0 +1,167 @@
using System;
using System.Threading;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Threading;
using LanMountainDesktop.Services;
using Markdown.Avalonia;
namespace LanMountainDesktop.Views.Components;
public partial class TextCapsuleWidget : UserControl, IDesktopComponentWidget
{
private string _text = string.Empty;
private bool _transparentBackground;
private double _lastAppliedCellSize = 100;
private CancellationTokenSource? _debounceCts;
public TextCapsuleWidget()
{
InitializeComponent();
UpdateDisplay();
}
public string Text
{
get => _text;
set
{
if (_text == value)
{
return;
}
_text = value;
DebouncedUpdateDisplay();
}
}
public bool TransparentBackground
{
get => _transparentBackground;
set
{
if (_transparentBackground == value)
{
return;
}
_transparentBackground = value;
ApplyChrome();
ApplyCellSize(_lastAppliedCellSize);
}
}
public void SetText(string text)
{
Text = text;
}
public void SetTransparentBackground(bool transparentBackground)
{
TransparentBackground = transparentBackground;
}
private void DebouncedUpdateDisplay()
{
// 取消之前的延迟任务
_debounceCts?.Cancel();
_debounceCts?.Dispose();
_debounceCts = new CancellationTokenSource();
var token = _debounceCts.Token;
// 延迟 150ms 后更新显示,避免频繁输入时过度渲染
Dispatcher.UIThread.Post(async () =>
{
try
{
await System.Threading.Tasks.Task.Delay(150, token);
if (!token.IsCancellationRequested)
{
UpdateDisplay();
}
}
catch (OperationCanceledException)
{
// 忽略取消异常
}
});
}
private void UpdateDisplay()
{
try
{
if (string.IsNullOrWhiteSpace(_text))
{
MarkdownViewer.Markdown = "*Empty*";
return;
}
// 使用 Markdown 引擎渲染文本
MarkdownViewer.Markdown = _text;
}
catch (Exception ex)
{
// 错误处理:显示错误信息而不是崩溃
MarkdownViewer.Markdown = $"*Error: {ex.Message}*";
}
}
public void ApplyCellSize(double cellSize)
{
_lastAppliedCellSize = cellSize;
// 计算组件高度:保持与任务栏核心比例一致 (0.74x)
var targetHeight = Math.Clamp(cellSize * 0.74, 34, 74);
RootBorder.Height = targetHeight;
// 主矩形统一到主题主档圆角
RootBorder.CornerRadius = ResolveUnifiedMainRectangle();
RootBorder.VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center;
// 设置最小和最大宽度
RootBorder.MinWidth = cellSize * 1.5;
RootBorder.MaxWidth = cellSize * 6;
if (_transparentBackground)
{
RootBorder.MinWidth = 0;
RootBorder.Padding = new Thickness(Math.Clamp(cellSize * 0.06, 4, 10), 0);
return;
}
// 确保清除可能存在的固定 Padding由代码控制"紧密感"
RootBorder.Padding = new Thickness(Math.Clamp(cellSize * 0.15, 12, 24), 0);
}
private void ApplyChrome()
{
if (_transparentBackground)
{
RootBorder.Classes.Remove("glass-panel");
RootBorder.Background = Brushes.Transparent;
RootBorder.BorderBrush = Brushes.Transparent;
RootBorder.BorderThickness = new Thickness(0);
RootBorder.BoxShadow = default;
return;
}
if (!RootBorder.Classes.Contains("glass-panel"))
{
RootBorder.Classes.Add("glass-panel");
}
RootBorder.ClearValue(Border.BackgroundProperty);
RootBorder.ClearValue(Border.BorderBrushProperty);
RootBorder.ClearValue(Border.BorderThicknessProperty);
RootBorder.ClearValue(Border.BoxShadowProperty);
}
private CornerRadius ResolveUnifiedMainRectangle() => new(ResolveUnifiedMainRadiusValue());
private static double ResolveUnifiedMainRadiusValue() =>
HostAppearanceThemeProvider.GetOrCreate().GetCurrent().CornerRadiusTokens.Lg.TopLeft;
}

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,30 +1,160 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LanMountainDesktop.ViewModels"
xmlns:fi="using:FluentIcons.Avalonia"
x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryControl"
Width="400" Height="500">
<!--
融合桌面组件库 - 专门用于添加组件到系统桌面(负一屏)
注意:此窗口只能添加组件到融合桌面,不能添加到阑山桌面
-->
<Grid RowDefinitions="Auto,*">
<!-- 标题栏 -->
<Border Background="{DynamicResource SystemControlBackgroundChromeMediumBrush}"
Padding="16,12">
<StackPanel>
<TextBlock Text="融合桌面组件"
FontWeight="SemiBold"
FontSize="16" />
<TextBlock Text="选择组件添加到系统桌面"
Opacity="0.7"
FontSize="12"
Margin="0,4,0,0" />
</StackPanel>
x:DataType="vm:ComponentLibraryWindowViewModel">
<Grid ColumnDefinitions="240,*"
ColumnSpacing="12"
Margin="0">
<!-- 分类列表 (左侧) -->
<Border Classes="surface-translucent-panel"
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
Padding="10">
<Grid RowDefinitions="Auto,*">
<TextBox x:Name="SearchBox"
Watermark="搜索组件..."
Margin="0,0,0,12"
Classes="clear"
Background="{DynamicResource AdaptiveSurfaceLowBrush}"
CornerRadius="12"
Padding="12,8">
<TextBox.InnerLeftContent>
<fi:SymbolIcon Symbol="Search" FontSize="16" Margin="10,0,0,0" Opacity="0.5" />
</TextBox.InnerLeftContent>
</TextBox>
<ListBox x:Name="CategoryListBox"
Grid.Row="1"
Background="Transparent"
BorderThickness="0"
SelectionChanged="OnCategorySelectionChanged"
ItemsSource="{Binding Categories}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:ComponentLibraryCategoryViewModel">
<Border Padding="10"
Margin="0,0,0,6"
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
Background="{DynamicResource AdaptiveNavItemBackgroundBrush}">
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<fi:SymbolIcon Symbol="{Binding Icon}"
IconVariant="Regular"
FontSize="16" />
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="{Binding Title}" />
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Border>
<!-- 组件网格 (右侧) -->
<Border Grid.Column="1"
Classes="surface-translucent-strong"
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
Padding="10">
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<ItemsControl x:Name="ComponentItemsControl"
ItemsSource="{Binding Components}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ComponentLibraryItemViewModel">
<Border Width="240"
Height="220"
Margin="6"
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
Padding="10"
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1">
<Grid RowDefinitions="*,Auto,Auto"
RowSpacing="8">
<!-- 预览区域 -->
<Border CornerRadius="{DynamicResource DesignCornerRadiusSm}"
Background="{DynamicResource AdaptiveGlassPanelBackgroundBrush}"
BorderThickness="1"
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
Padding="8">
<Grid>
<Image Source="{Binding PreviewBitmap}"
Stretch="Uniform"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
RenderOptions.BitmapInterpolationMode="HighQuality"
IsVisible="{Binding IsPreviewReady}" />
<!-- 加载中状态 -->
<Border IsVisible="{Binding IsPreviewPending}"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8">
<ProgressBar Width="96"
IsIndeterminate="True" />
<TextBlock HorizontalAlignment="Center"
TextAlignment="Center"
FontSize="12"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="{Binding PreviewStatusText}" />
</StackPanel>
</Border>
<!-- 失败状态 -->
<Border IsVisible="{Binding IsPreviewFailed}"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8">
<TextBlock HorizontalAlignment="Center"
TextAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="{Binding PreviewStatusText}" />
<TextBlock HorizontalAlignment="Center"
TextAlignment="Center"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="{Binding PreviewErrorMessage}" />
</StackPanel>
</Border>
</Grid>
</Border>
<!-- 组件名称 -->
<TextBlock Grid.Row="1"
HorizontalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="{Binding DisplayName}" />
<!-- 添加按钮 -->
<Button Grid.Row="2"
HorizontalAlignment="Center"
Padding="12,6"
Tag="{Binding ComponentId}"
Click="OnAddComponentClick">
<TextBlock Text="添加到桌面" />
</Button>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
<!-- 组件列表 -->
<ScrollViewer Grid.Row="1"
Padding="12">
<WrapPanel x:Name="ComponentPanel" Orientation="Horizontal" />
</ScrollViewer>
</Grid>
</UserControl>

View File

@@ -1,78 +1,185 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using FluentIcons.Common;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels;
using LanMountainDesktop.Views.Components;
using Avalonia.Controls.ApplicationLifetimes;
namespace LanMountainDesktop.Views;
/// <summary>
/// 融合桌面组件库控件 - 专门用于添加组件到系统桌面(负一屏)
/// </summary>
public partial class FusedDesktopComponentLibraryControl : UserControl
{
/// <summary>
/// 添加组件到融合桌面事件
/// </summary>
public event EventHandler<string>? AddComponentRequested;
private readonly ComponentLibraryWindowViewModel _viewModel = new();
private List<DesktopComponentDefinition> _allDefinitions = new();
private ComponentRegistry? _componentRegistry;
private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry;
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
private readonly IWeatherInfoService _weatherDataService;
private readonly TimeZoneService _timeZoneService;
private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService();
private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService();
public FusedDesktopComponentLibraryControl()
{
InitializeComponent();
LoadComponents();
}
/// <summary>
/// 加载可用组件列表
/// </summary>
private void LoadComponents()
{
var registry = ComponentRegistry.CreateDefault();
foreach (var definition in registry.GetAll())
DataContext = _viewModel;
_weatherDataService = _settingsFacade.Weather.GetWeatherInfoService();
_timeZoneService = _settingsFacade.Region.GetTimeZoneService();
LoadRegistry();
LoadCategories();
SearchBox.KeyUp += (s, e) => FilterComponents();
// 默认选择第一个分类
if (_viewModel.Categories.Count > 0)
{
if (!definition.AllowDesktopPlacement)
{
continue;
}
var button = new Button
{
Width = 100,
Height = 100,
Margin = new Thickness(4),
Padding = new Thickness(8),
CornerRadius = new CornerRadius(12),
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Tag = definition.Id
};
var textBlock = new TextBlock
{
Text = definition.DisplayName,
FontSize = 11,
TextAlignment = TextAlignment.Center,
TextTrimming = TextTrimming.CharacterEllipsis,
MaxLines = 2,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
button.Content = textBlock;
button.Click += OnAddComponentClick;
ComponentPanel.Children.Add(button);
CategoryListBox.SelectedIndex = 0;
}
}
/// <summary>
/// 添加组件按钮点击
/// </summary>
private void LoadRegistry()
{
var pluginRuntimeService = (Application.Current as App)?.PluginRuntimeService;
_componentRegistry = DesktopComponentRegistryFactory.Create(pluginRuntimeService);
_componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry(
_componentRegistry,
pluginRuntimeService,
_settingsFacade);
_allDefinitions = _componentRegistry.GetAll()
.Where(d => d.AllowDesktopPlacement)
.ToList();
}
private void LoadCategories()
{
_viewModel.Categories.Clear();
_viewModel.Components.Clear();
// 添加"全部组件"分类
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
"all",
"全部组件",
Symbol.Apps,
Array.Empty<ComponentLibraryItemViewModel>()));
var categoryMap = new Dictionary<string, (string Display, Symbol Icon)>
{
{ "clock", ("时钟", Symbol.Clock) },
{ "date", ("日历", Symbol.CalendarDate) },
{ "weather", ("天气", Symbol.WeatherSunny) },
{ "board", ("画板", Symbol.Edit) },
{ "media", ("媒体", Symbol.Play) },
{ "info", ("资讯", Symbol.News) },
{ "calculator", ("工具", Symbol.Calculator) },
{ "study", ("学习", Symbol.Hourglass) },
{ "file", ("文件", Symbol.Folder) }
};
var usedCategories = _allDefinitions
.Select(d => d.Category)
.Distinct()
.Where(c => !string.IsNullOrEmpty(c));
foreach (var cat in usedCategories)
{
if (categoryMap.TryGetValue(cat.ToLower(), out var info))
{
var categoryComponents = _allDefinitions
.Where(d => string.Equals(d.Category, cat, StringComparison.OrdinalIgnoreCase))
.OrderBy(d => d.DisplayName)
.Select(d => CreateComponentItem(d))
.ToArray();
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
cat,
info.Display,
info.Icon,
categoryComponents));
}
}
}
private ComponentLibraryItemViewModel CreateComponentItem(DesktopComponentDefinition definition)
{
var previewKey = ComponentPreviewKey.ForComponentType(
definition.Id,
definition.MinWidthCells,
definition.MinHeightCells);
var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow;
ComponentPreviewImageEntry? previewEntry = null;
if (mainWindow is not null)
{
previewEntry = mainWindow.GetPreviewEntry(previewKey);
}
var item = new ComponentLibraryItemViewModel(
definition.Id,
definition.DisplayName,
previewKey,
"正在加载预览...",
"预览不可用",
previewEntry);
if (mainWindow is not null && (previewEntry is null || previewEntry.State == ComponentPreviewImageState.Pending))
{
mainWindow.RequestDetachedLibraryPreview(previewKey);
}
return item;
}
public void UpdatePreviewImage(ComponentPreviewImageEntry entry)
{
foreach (var category in _viewModel.Categories)
{
foreach (var component in category.Components)
{
if (component.PreviewKey.Equals(entry.Key))
{
component.UpdatePreviewImageEntry(entry);
}
}
}
}
private void OnCategorySelectionChanged(object? sender, SelectionChangedEventArgs e)
{
FilterComponents();
}
private void FilterComponents()
{
var selectedCategory = (CategoryListBox.SelectedItem as ComponentLibraryCategoryViewModel)?.Id;
var searchText = SearchBox.Text?.ToLower() ?? "";
var filtered = _allDefinitions.Where(d =>
{
var matchesCategory = selectedCategory == "all" || string.Equals(d.Category, selectedCategory, StringComparison.OrdinalIgnoreCase);
var matchesSearch = string.IsNullOrEmpty(searchText) || d.DisplayName.ToLower().Contains(searchText) || d.Id.ToLower().Contains(searchText);
return matchesCategory && matchesSearch;
});
_viewModel.Components.Clear();
foreach (var def in filtered)
{
_viewModel.Components.Add(CreateComponentItem(def));
}
}
private void OnAddComponentClick(object? sender, RoutedEventArgs e)
{
if (sender is Button button && button.Tag is string componentId)

View File

@@ -1,41 +1,57 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:LanMountainDesktop.Views"
xmlns:fi="using:FluentIcons.Avalonia"
x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryWindow"
Width="420" Height="560"
MinWidth="380" MinHeight="400"
Width="860" Height="620"
MinWidth="600" MinHeight="500"
WindowStartupLocation="CenterScreen"
CanResize="True"
Title="融合桌面组件">
<Grid RowDefinitions="Auto,*">
<!-- 标题栏 -->
<Border Background="{DynamicResource SystemControlBackgroundChromeMediumBrush}"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="0,0,0,1"
Padding="16,12">
<Grid ColumnDefinitions="*,Auto">
<StackPanel>
<TextBlock Text="融合桌面设置"
FontWeight="SemiBold"
FontSize="18" />
<TextBlock Text="选择组件添加到系统桌面(负一屏)"
Opacity="0.7"
FontSize="12"
Margin="0,4,0,0" />
</StackPanel>
<Button Grid.Column="1"
VerticalAlignment="Center"
CornerRadius="20"
Padding="8"
Click="OnCloseClick">
<TextBlock Text="✕"
FontSize="16" />
</Button>
</Grid>
</Border>
SystemDecorations="Full"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="NoChrome"
ExtendClientAreaTitleBarHeightHint="-1"
Background="Transparent"
TransparencyLevelHint="Mica"
Title="融合桌面组件库">
<Panel>
<!-- 背景磨砂效果 -->
<Border Background="{DynamicResource AdaptiveSurfaceLowBrush}"
Opacity="0.85" />
<!-- 组件库控件 -->
<controls:FusedDesktopComponentLibraryControl x:Name="LibraryControl"
Grid.Row="1" />
</Grid>
<Grid RowDefinitions="Auto,*">
<!-- 自定义标题栏 -->
<Border Background="Transparent"
IsHitTestVisible="True"
Padding="20,16">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Spacing="6" VerticalAlignment="Center">
<TextBlock Text="融合桌面组件库"
FontWeight="SemiBold"
FontSize="20"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock Text="将精美组件放置在您的系统桌面上(负一屏)"
Opacity="0.6"
FontSize="13"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
</StackPanel>
<Button Grid.Column="1"
Classes="accent"
Width="36" Height="36"
Padding="0"
CornerRadius="18"
BorderThickness="0"
Background="{DynamicResource AdaptiveButtonHoverBackgroundBrush}"
Click="OnCloseClick">
<fi:SymbolIcon Symbol="Dismiss" FontSize="18" />
</Button>
</Grid>
</Border>
<!-- 组件库控件 -->
<controls:FusedDesktopComponentLibraryControl x:Name="LibraryControl"
Grid.Row="1" />
</Grid>
</Panel>
</Window>

View File

@@ -1,7 +1,11 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
using Avalonia.Controls.ApplicationLifetimes;
namespace LanMountainDesktop.Views;
@@ -13,13 +17,20 @@ namespace LanMountainDesktop.Views;
public partial class FusedDesktopComponentLibraryWindow : Window
{
private readonly IFusedDesktopLayoutService _layoutService = FusedDesktopLayoutServiceProvider.GetOrCreate();
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
private TransparentOverlayWindow? _overlayWindow;
// 与 TransparentOverlayWindow 保持一致的默认 cellSize
private const double DefaultCellSize = 100;
public FusedDesktopComponentLibraryWindow()
{
InitializeComponent();
LibraryControl.AddComponentRequested += OnAddComponentRequested;
var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow;
mainWindow?.RegisterFusedLibraryWindow(this);
}
/// <summary>
@@ -31,7 +42,7 @@ public partial class FusedDesktopComponentLibraryWindow : Window
}
/// <summary>
/// 添加组件请求处理
/// 添加组件请求处理 - 将组件放置在屏幕(覆盖层画布)中央
/// </summary>
private void OnAddComponentRequested(object? sender, string componentId)
{
@@ -41,21 +52,66 @@ public partial class FusedDesktopComponentLibraryWindow : Window
return;
}
// 在屏幕中央添加组件
var screenBounds = _overlayWindow.Bounds;
var x = screenBounds.Width / 2 - 100; // 居中
var y = screenBounds.Height / 2 - 100;
// 计算组件的像素尺寸
var (componentWidth, componentHeight) = ResolveComponentSize(componentId);
_overlayWindow.AddComponent(componentId, x, y, 200, 200);
// 取覆盖层画布的中心点,减去组件半尺寸,使组件出现在屏幕正中央
var overlayBounds = _overlayWindow.Bounds;
var centerX = overlayBounds.Width / 2.0 - componentWidth / 2.0;
var centerY = overlayBounds.Height / 2.0 - componentHeight / 2.0;
AppLogger.Info("FusedDesktopLibrary", $"Added component {componentId} to fused desktop.");
// 边界保护:确保组件不超出屏幕边界
centerX = Math.Max(0, Math.Min(centerX, overlayBounds.Width - componentWidth));
centerY = Math.Max(0, Math.Min(centerY, overlayBounds.Height - componentHeight));
_overlayWindow.AddComponent(componentId, centerX, centerY, componentWidth, componentHeight);
AppLogger.Info("FusedDesktopLibrary",
$"Added component '{componentId}' at center ({centerX:F0}, {centerY:F0}) size ({componentWidth}x{componentHeight}).");
// 关闭窗口
Close();
}
/// <summary>
/// 解析组件的默认像素尺寸(基于组件定义的 MinCells * DefaultCellSize
/// </summary>
private (double Width, double Height) ResolveComponentSize(string componentId)
{
try
{
var pluginRuntimeService = (Application.Current as App)?.PluginRuntimeService;
var registry = DesktopComponentRegistryFactory.Create(pluginRuntimeService);
if (registry.TryGetDefinition(componentId, out var definition))
{
var w = Math.Max(1, definition.MinWidthCells) * DefaultCellSize;
var h = Math.Max(1, definition.MinHeightCells) * DefaultCellSize;
return (w, h);
}
}
catch (Exception ex)
{
AppLogger.Warn("FusedDesktopLibrary", $"Failed to resolve component size for '{componentId}'.", ex);
}
// 回退为 2×2 格子的默认尺寸
return (DefaultCellSize * 2, DefaultCellSize * 2);
}
private void OnCloseClick(object? sender, RoutedEventArgs e)
{
Close();
}
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow;
mainWindow?.UnregisterFusedLibraryWindow(this);
}
public void UpdatePreviewImage(ComponentPreviewImageEntry entry)
{
LibraryControl.UpdatePreviewImage(entry);
}
}

View File

@@ -24,6 +24,7 @@ public partial class MainWindow
private readonly IComponentPreviewImageService _componentPreviewImageService = new ComponentPreviewImageService();
private readonly Dictionary<ComponentPreviewKey, List<ComponentLibraryPreviewVisualTarget>> _componentLibraryPreviewVisualTargets = new(ComponentPreviewKeyComparer.Instance);
private bool _componentLibraryPreviewWarmupStarted;
private FusedDesktopComponentLibraryWindow? _fusedLibraryWindow;
private sealed record ComponentLibraryPreviewVisualTarget(Image Image, Control Fallback);
@@ -519,6 +520,7 @@ public partial class MainWindow
{
ApplyPreviewEntryToEmbeddedVisuals(entry.Key);
_detachedComponentLibraryWindow?.UpdatePreviewImage(entry);
_fusedLibraryWindow?.UpdatePreviewImage(entry);
if (entry.Key.Kind == ComponentPreviewKeyKind.PlacementInstance)
{
@@ -597,4 +599,30 @@ public partial class MainWindow
action: "DetachedLibraryRender",
forceRefresh: false);
}
// FusedDesktop 支持
public void RegisterFusedLibraryWindow(FusedDesktopComponentLibraryWindow window)
{
_fusedLibraryWindow = window;
}
public void UnregisterFusedLibraryWindow(FusedDesktopComponentLibraryWindow window)
{
if (ReferenceEquals(_fusedLibraryWindow, window))
{
_fusedLibraryWindow = null;
}
}
public ComponentPreviewImageEntry? GetPreviewEntry(ComponentPreviewKey key)
{
return ResolvePreviewEntry(key);
}
public void RequestDetachedLibraryPreview(ComponentPreviewKey key)
{
RequestDetachedLibraryPreviewWarm(key);
RequestDetachedLibraryPreviewRender(key);
}
}

View File

@@ -364,12 +364,407 @@ public partial class MainWindow
? ClockDisplayFormat.HourMinute
: ClockDisplayFormat.HourMinuteSecond;
_statusBarClockTransparentBackground = snapshot.StatusBarClockTransparentBackground;
_clockPosition = NormalizeClockPosition(snapshot.ClockPosition);
_clockFontSize = NormalizeFontSize(snapshot.ClockFontSize);
if (ClockWidget is not null)
_showTextCapsule = snapshot.ShowTextCapsule;
_textCapsuleContent = snapshot.TextCapsuleContent ?? "**Hello** World!";
_textCapsulePosition = NormalizeTextCapsulePosition(snapshot.TextCapsulePosition);
_textCapsuleTransparentBackground = snapshot.TextCapsuleTransparentBackground;
_textCapsuleFontSize = NormalizeFontSize(snapshot.TextCapsuleFontSize);
_showNetworkSpeed = snapshot.ShowNetworkSpeed;
_networkSpeedPosition = NormalizeNetworkSpeedPosition(snapshot.NetworkSpeedPosition);
_networkSpeedDisplayMode = NormalizeNetworkSpeedDisplayMode(snapshot.NetworkSpeedDisplayMode);
_networkSpeedTransparentBackground = snapshot.NetworkSpeedTransparentBackground;
_showNetworkTypeIcon = snapshot.ShowNetworkTypeIcon;
_networkSpeedFontSize = NormalizeFontSize(snapshot.NetworkSpeedFontSize);
_statusBarShadowEnabled = snapshot.StatusBarShadowEnabled;
_statusBarShadowColor = snapshot.StatusBarShadowColor ?? "#000000";
_statusBarShadowOpacity = snapshot.StatusBarShadowOpacity;
ApplyClockSettingsToAllWidgets();
ApplyTextCapsuleSettingsToAllWidgets();
ApplyNetworkSpeedSettingsToAllWidgets();
ApplyStatusBarShadow();
}
private void ApplyClockSettingsToAllWidgets()
{
if (ClockWidgetLeft is not null)
{
ClockWidget.SetDisplayFormat(_clockDisplayFormat);
ClockWidget.SetTransparentBackground(_statusBarClockTransparentBackground);
ClockWidgetLeft.SetDisplayFormat(_clockDisplayFormat);
ClockWidgetLeft.SetTransparentBackground(_statusBarClockTransparentBackground);
ClockWidgetLeft.SetFontSize(_clockFontSize);
}
if (ClockWidgetCenter is not null)
{
ClockWidgetCenter.SetDisplayFormat(_clockDisplayFormat);
ClockWidgetCenter.SetTransparentBackground(_statusBarClockTransparentBackground);
ClockWidgetCenter.SetFontSize(_clockFontSize);
}
if (ClockWidgetRight is not null)
{
ClockWidgetRight.SetDisplayFormat(_clockDisplayFormat);
ClockWidgetRight.SetTransparentBackground(_statusBarClockTransparentBackground);
ClockWidgetRight.SetFontSize(_clockFontSize);
}
}
private static string NormalizeClockPosition(string? value)
{
return value switch
{
_ when string.Equals(value, "Center", StringComparison.OrdinalIgnoreCase) => "Center",
_ when string.Equals(value, "Right", StringComparison.OrdinalIgnoreCase) => "Right",
_ => "Left"
};
}
private static string NormalizeFontSize(string? value)
{
return value switch
{
_ when string.Equals(value, "Small", StringComparison.OrdinalIgnoreCase) => "Small",
_ when string.Equals(value, "Large", StringComparison.OrdinalIgnoreCase) => "Large",
_ => "Medium"
};
}
private void ApplyTextCapsuleSettingsToAllWidgets()
{
if (TextCapsuleWidgetLeft is not null)
{
TextCapsuleWidgetLeft.SetText(_textCapsuleContent);
TextCapsuleWidgetLeft.SetTransparentBackground(_textCapsuleTransparentBackground);
}
if (TextCapsuleWidgetCenter is not null)
{
TextCapsuleWidgetCenter.SetText(_textCapsuleContent);
TextCapsuleWidgetCenter.SetTransparentBackground(_textCapsuleTransparentBackground);
}
if (TextCapsuleWidgetRight is not null)
{
TextCapsuleWidgetRight.SetText(_textCapsuleContent);
TextCapsuleWidgetRight.SetTransparentBackground(_textCapsuleTransparentBackground);
}
}
private static string NormalizeTextCapsulePosition(string? value)
{
return value switch
{
_ when string.Equals(value, "Center", StringComparison.OrdinalIgnoreCase) => "Center",
_ when string.Equals(value, "Left", StringComparison.OrdinalIgnoreCase) => "Left",
_ => "Right"
};
}
private void ApplyNetworkSpeedSettingsToAllWidgets()
{
if (NetworkSpeedWidgetLeft is not null)
{
NetworkSpeedWidgetLeft.SetDisplayMode(_networkSpeedDisplayMode);
NetworkSpeedWidgetLeft.SetTransparentBackground(_networkSpeedTransparentBackground);
NetworkSpeedWidgetLeft.SetShowNetworkTypeIcon(_showNetworkTypeIcon);
NetworkSpeedWidgetLeft.SetFontSize(_networkSpeedFontSize);
}
if (NetworkSpeedWidgetCenter is not null)
{
NetworkSpeedWidgetCenter.SetDisplayMode(_networkSpeedDisplayMode);
NetworkSpeedWidgetCenter.SetTransparentBackground(_networkSpeedTransparentBackground);
NetworkSpeedWidgetCenter.SetShowNetworkTypeIcon(_showNetworkTypeIcon);
NetworkSpeedWidgetCenter.SetFontSize(_networkSpeedFontSize);
}
if (NetworkSpeedWidgetRight is not null)
{
NetworkSpeedWidgetRight.SetDisplayMode(_networkSpeedDisplayMode);
NetworkSpeedWidgetRight.SetTransparentBackground(_networkSpeedTransparentBackground);
NetworkSpeedWidgetRight.SetShowNetworkTypeIcon(_showNetworkTypeIcon);
NetworkSpeedWidgetRight.SetFontSize(_networkSpeedFontSize);
}
}
private static string NormalizeNetworkSpeedPosition(string? value)
{
return value switch
{
_ when string.Equals(value, "Left", StringComparison.OrdinalIgnoreCase) => "Left",
_ when string.Equals(value, "Center", StringComparison.OrdinalIgnoreCase) => "Center",
_ => "Right"
};
}
private static string NormalizeNetworkSpeedDisplayMode(string? value)
{
return value switch
{
_ when string.Equals(value, "Upload", StringComparison.OrdinalIgnoreCase) => "Upload",
_ when string.Equals(value, "Download", StringComparison.OrdinalIgnoreCase) => "Download",
_ => "Both"
};
}
private void ApplyStatusBarShadow()
{
if (StatusBarOverlay is null)
{
return;
}
if (_statusBarShadowEnabled)
{
if (Color.TryParse(_statusBarShadowColor, out var shadowColor))
{
var opacity = Math.Clamp(_statusBarShadowOpacity, 0, 1);
StatusBarOverlay.IsVisible = true;
var gradientBrush = new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative)
};
var alpha1 = (byte)(shadowColor.A * opacity * 0.8);
var alpha2 = (byte)(shadowColor.A * opacity * 0.4);
var color1 = Color.FromArgb(alpha1, shadowColor.R, shadowColor.G, shadowColor.B);
var color2 = Color.FromArgb(alpha2, shadowColor.R, shadowColor.G, shadowColor.B);
gradientBrush.GradientStops.Add(new GradientStop(color1, 0.0));
gradientBrush.GradientStops.Add(new GradientStop(color2, 0.3));
gradientBrush.GradientStops.Add(new GradientStop(Colors.Transparent, 1.0));
StatusBarOverlay.Background = gradientBrush;
}
}
else
{
StatusBarOverlay.IsVisible = false;
}
}
/// <summary>
/// 检测状态栏组件是否会发生碰撞
/// </summary>
private bool WouldComponentsCollide()
{
if (TopStatusBarHost is null)
return false;
// 获取各区域当前占用的宽度
var leftWidth = GetLeftPanelOccupiedWidth();
var centerWidth = GetCenterPanelOccupiedWidth();
var rightWidth = GetRightPanelOccupiedWidth();
// 获取状态栏总宽度
var totalWidth = TopStatusBarHost.Bounds.Width;
if (totalWidth <= 0)
return false;
// 计算中间区域的实际位置
// 左列是 *, 中列是 Auto, 右列是 *
// 中间区域居中显示
var centerLeft = (totalWidth - centerWidth) / 2;
var centerRight = centerLeft + centerWidth;
// 安全间距(像素)
const double safetyMargin = 20;
// 检测左侧组件是否会与中间区域碰撞
// 左侧组件右边界 = leftWidth
// 中间区域左边界 = centerLeft
if (leftWidth + safetyMargin > centerLeft)
{
return true;
}
// 检测右侧组件是否会与中间区域碰撞
// 右侧组件左边界 = totalWidth - rightWidth
// 中间区域右边界 = centerRight
if (totalWidth - rightWidth - safetyMargin < centerRight)
{
return true;
}
// 检测中间区域是否会与左右两侧碰撞(中间区域过宽)
if (centerLeft < leftWidth + safetyMargin ||
centerRight > totalWidth - rightWidth - safetyMargin)
{
return true;
}
return false;
}
/// <summary>
/// 获取左侧面板占用的宽度(包括间距)
/// </summary>
private double GetLeftPanelOccupiedWidth()
{
if (TopStatusLeftPanel is null)
return 0;
var spacing = TopStatusLeftPanel.Spacing;
var width = 0.0;
var visibleCount = 0;
foreach (var child in TopStatusLeftPanel.Children)
{
if (child is Control control && control.IsVisible)
{
width += control.Bounds.Width;
visibleCount++;
}
}
// 添加间距
if (visibleCount > 1)
{
width += spacing * (visibleCount - 1);
}
return width;
}
/// <summary>
/// 获取中间面板占用的宽度(包括间距)
/// </summary>
private double GetCenterPanelOccupiedWidth()
{
if (TopStatusCenterPanel is null)
return 0;
var spacing = TopStatusCenterPanel.Spacing;
var width = 0.0;
var visibleCount = 0;
foreach (var child in TopStatusCenterPanel.Children)
{
if (child is Control control && control.IsVisible)
{
width += control.Bounds.Width;
visibleCount++;
}
}
// 添加间距
if (visibleCount > 1)
{
width += spacing * (visibleCount - 1);
}
return width;
}
/// <summary>
/// 获取右侧面板占用的宽度(包括间距)
/// </summary>
private double GetRightPanelOccupiedWidth()
{
if (TopStatusRightPanel is null)
return 0;
var spacing = TopStatusRightPanel.Spacing;
var width = 0.0;
var visibleCount = 0;
foreach (var child in TopStatusRightPanel.Children)
{
if (child is Control control && control.IsVisible)
{
width += control.Bounds.Width;
visibleCount++;
}
}
// 添加间距
if (visibleCount > 1)
{
width += spacing * (visibleCount - 1);
}
return width;
}
/// <summary>
/// 检查是否可以在指定位置添加组件
/// </summary>
private bool CanAddComponentAtPosition(string position)
{
// 先临时显示组件以计算宽度
var wouldCollide = WouldComponentsCollide();
if (!wouldCollide)
return true;
// 如果会发生碰撞,检查是否是因为目标位置导致的
// 获取当前各区域宽度
var leftWidth = GetLeftPanelOccupiedWidth();
var centerWidth = GetCenterPanelOccupiedWidth();
var rightWidth = GetRightPanelOccupiedWidth();
// 估算新组件的宽度(基于当前单元格大小)
var estimatedNewComponentWidth = _currentDesktopCellSize > 0 ? _currentDesktopCellSize * 2 : 120;
// 根据目标位置检查添加后是否会碰撞
return position switch
{
"Left" => CanAddToLeft(leftWidth, centerWidth, rightWidth, estimatedNewComponentWidth),
"Center" => CanAddToCenter(leftWidth, centerWidth, rightWidth, estimatedNewComponentWidth),
"Right" => CanAddToRight(leftWidth, centerWidth, rightWidth, estimatedNewComponentWidth),
_ => false
};
}
private bool CanAddToLeft(double leftWidth, double centerWidth, double rightWidth, double newWidth)
{
if (TopStatusBarHost is null)
return false;
var totalWidth = TopStatusBarHost.Bounds.Width;
if (totalWidth <= 0)
return true;
var newLeftWidth = leftWidth + newWidth + TopStatusLeftPanel?.Spacing ?? 6;
var centerLeft = (totalWidth - centerWidth) / 2;
const double safetyMargin = 20;
return newLeftWidth + safetyMargin <= centerLeft;
}
private bool CanAddToCenter(double leftWidth, double centerWidth, double rightWidth, double newWidth)
{
if (TopStatusBarHost is null)
return false;
var totalWidth = TopStatusBarHost.Bounds.Width;
if (totalWidth <= 0)
return true;
var newCenterWidth = centerWidth + newWidth + TopStatusCenterPanel?.Spacing ?? 6;
var centerLeft = (totalWidth - newCenterWidth) / 2;
var centerRight = centerLeft + newCenterWidth;
const double safetyMargin = 20;
return centerLeft >= leftWidth + safetyMargin &&
centerRight <= totalWidth - rightWidth - safetyMargin;
}
private bool CanAddToRight(double leftWidth, double centerWidth, double rightWidth, double newWidth)
{
if (TopStatusBarHost is null)
return false;
var totalWidth = TopStatusBarHost.Bounds.Width;
if (totalWidth <= 0)
return true;
var newRightWidth = rightWidth + newWidth + TopStatusRightPanel?.Spacing ?? 6;
var centerRight = (totalWidth + centerWidth) / 2;
const double safetyMargin = 20;
return totalWidth - newRightWidth - safetyMargin >= centerRight;
}
private void ApplyTopStatusComponentVisibility()
@@ -377,16 +772,168 @@ public partial class MainWindow
var showClock = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock);
var hasVisibleTopStatusComponent = false;
if (ClockWidget is not null)
// 先隐藏所有时钟控件
if (ClockWidgetLeft is not null)
ClockWidgetLeft.IsVisible = false;
if (ClockWidgetCenter is not null)
ClockWidgetCenter.IsVisible = false;
if (ClockWidgetRight is not null)
ClockWidgetRight.IsVisible = false;
// 先隐藏所有文字胶囊控件
if (TextCapsuleWidgetLeft is not null)
TextCapsuleWidgetLeft.IsVisible = false;
if (TextCapsuleWidgetCenter is not null)
TextCapsuleWidgetCenter.IsVisible = false;
if (TextCapsuleWidgetRight is not null)
TextCapsuleWidgetRight.IsVisible = false;
// 先隐藏所有网速控件
if (NetworkSpeedWidgetLeft is not null)
NetworkSpeedWidgetLeft.IsVisible = false;
if (NetworkSpeedWidgetCenter is not null)
NetworkSpeedWidgetCenter.IsVisible = false;
if (NetworkSpeedWidgetRight is not null)
NetworkSpeedWidgetRight.IsVisible = false;
// 根据位置设置显示对应的时钟控件(带碰撞检测)
if (showClock)
{
ClockWidget.IsVisible = showClock;
ClockWidget.SetTransparentBackground(_statusBarClockTransparentBackground);
if (showClock)
var targetPosition = _clockPosition;
var canAdd = CanAddComponentAtPosition(targetPosition);
if (canAdd)
{
ClockWidget.SetDisplayFormat(_clockDisplayFormat);
var columnSpan = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? 2 : 3;
Grid.SetColumnSpan(ClockWidget, columnSpan);
hasVisibleTopStatusComponent = true;
var targetClock = targetPosition switch
{
"Center" => ClockWidgetCenter,
"Right" => ClockWidgetRight,
_ => ClockWidgetLeft
};
if (targetClock is not null)
{
targetClock.IsVisible = true;
targetClock.SetTransparentBackground(_statusBarClockTransparentBackground);
targetClock.SetDisplayFormat(_clockDisplayFormat);
hasVisibleTopStatusComponent = true;
}
}
else
{
// 如果目标位置无法添加,尝试其他位置
var alternativePosition = FindAlternativePosition(targetPosition);
if (alternativePosition is not null)
{
var targetClock = alternativePosition switch
{
"Center" => ClockWidgetCenter,
"Right" => ClockWidgetRight,
_ => ClockWidgetLeft
};
if (targetClock is not null)
{
targetClock.IsVisible = true;
targetClock.SetTransparentBackground(_statusBarClockTransparentBackground);
targetClock.SetDisplayFormat(_clockDisplayFormat);
hasVisibleTopStatusComponent = true;
}
}
}
}
// 根据位置设置显示对应的文字胶囊控件(带碰撞检测)
if (_showTextCapsule)
{
var targetPosition = _textCapsulePosition;
var canAdd = CanAddComponentAtPosition(targetPosition);
if (canAdd)
{
var targetTextCapsule = targetPosition switch
{
"Left" => TextCapsuleWidgetLeft,
"Center" => TextCapsuleWidgetCenter,
_ => TextCapsuleWidgetRight
};
if (targetTextCapsule is not null)
{
targetTextCapsule.IsVisible = true;
targetTextCapsule.SetTransparentBackground(_textCapsuleTransparentBackground);
targetTextCapsule.SetText(_textCapsuleContent);
hasVisibleTopStatusComponent = true;
}
}
else
{
// 如果目标位置无法添加,尝试其他位置
var alternativePosition = FindAlternativePosition(targetPosition);
if (alternativePosition is not null)
{
var targetTextCapsule = alternativePosition switch
{
"Left" => TextCapsuleWidgetLeft,
"Center" => TextCapsuleWidgetCenter,
_ => TextCapsuleWidgetRight
};
if (targetTextCapsule is not null)
{
targetTextCapsule.IsVisible = true;
targetTextCapsule.SetTransparentBackground(_textCapsuleTransparentBackground);
targetTextCapsule.SetText(_textCapsuleContent);
hasVisibleTopStatusComponent = true;
}
}
}
}
// 根据位置设置显示对应的网速控件(带碰撞检测)
if (_showNetworkSpeed)
{
var targetPosition = _networkSpeedPosition;
var canAdd = CanAddComponentAtPosition(targetPosition);
if (canAdd)
{
var targetNetworkSpeed = targetPosition switch
{
"Left" => NetworkSpeedWidgetLeft,
"Center" => NetworkSpeedWidgetCenter,
_ => NetworkSpeedWidgetRight
};
if (targetNetworkSpeed is not null)
{
targetNetworkSpeed.IsVisible = true;
targetNetworkSpeed.SetTransparentBackground(_networkSpeedTransparentBackground);
targetNetworkSpeed.SetDisplayMode(_networkSpeedDisplayMode);
hasVisibleTopStatusComponent = true;
}
}
else
{
// 如果目标位置无法添加,尝试其他位置
var alternativePosition = FindAlternativePosition(targetPosition);
if (alternativePosition is not null)
{
var targetNetworkSpeed = alternativePosition switch
{
"Left" => NetworkSpeedWidgetLeft,
"Center" => NetworkSpeedWidgetCenter,
_ => NetworkSpeedWidgetRight
};
if (targetNetworkSpeed is not null)
{
targetNetworkSpeed.IsVisible = true;
targetNetworkSpeed.SetTransparentBackground(_networkSpeedTransparentBackground);
targetNetworkSpeed.SetDisplayMode(_networkSpeedDisplayMode);
hasVisibleTopStatusComponent = true;
}
}
}
}
@@ -394,6 +941,244 @@ public partial class MainWindow
{
TopStatusBarHost.IsVisible = hasVisibleTopStatusComponent;
}
// 延迟检查碰撞并调整
Dispatcher.UIThread.Post(async () =>
{
await System.Threading.Tasks.Task.Delay(50);
AdjustComponentsIfColliding();
});
}
/// <summary>
/// 当组件发生碰撞时,自动调整位置
/// </summary>
private void AdjustComponentsIfColliding()
{
if (!WouldComponentsCollide())
return;
// 获取当前可见的组件
var leftComponents = GetVisibleLeftComponents();
var centerComponents = GetVisibleCenterComponents();
var rightComponents = GetVisibleRightComponents();
// 优先保留时钟,调整文字胶囊位置
if (TextCapsuleWidgetLeft?.IsVisible == true && WouldComponentsCollide())
{
// 尝试将左侧文字胶囊移到中间
if (CanAddComponentAtPosition("Center"))
{
TextCapsuleWidgetLeft.IsVisible = false;
TextCapsuleWidgetCenter!.IsVisible = true;
TextCapsuleWidgetCenter.SetTransparentBackground(_textCapsuleTransparentBackground);
TextCapsuleWidgetCenter.SetText(_textCapsuleContent);
}
// 或者移到右侧
else if (CanAddComponentAtPosition("Right"))
{
TextCapsuleWidgetLeft.IsVisible = false;
TextCapsuleWidgetRight!.IsVisible = true;
TextCapsuleWidgetRight.SetTransparentBackground(_textCapsuleTransparentBackground);
TextCapsuleWidgetRight.SetText(_textCapsuleContent);
}
// 如果都无法添加,则隐藏文字胶囊
else
{
TextCapsuleWidgetLeft.IsVisible = false;
}
}
if (TextCapsuleWidgetRight?.IsVisible == true && WouldComponentsCollide())
{
// 尝试将右侧文字胶囊移到中间
if (CanAddComponentAtPosition("Center"))
{
TextCapsuleWidgetRight.IsVisible = false;
TextCapsuleWidgetCenter!.IsVisible = true;
TextCapsuleWidgetCenter.SetTransparentBackground(_textCapsuleTransparentBackground);
TextCapsuleWidgetCenter.SetText(_textCapsuleContent);
}
// 或者移到左侧
else if (CanAddComponentAtPosition("Left"))
{
TextCapsuleWidgetRight.IsVisible = false;
TextCapsuleWidgetLeft!.IsVisible = true;
TextCapsuleWidgetLeft.SetTransparentBackground(_textCapsuleTransparentBackground);
TextCapsuleWidgetLeft.SetText(_textCapsuleContent);
}
// 如果都无法添加,则隐藏文字胶囊
else
{
TextCapsuleWidgetRight.IsVisible = false;
}
}
if (TextCapsuleWidgetCenter?.IsVisible == true && WouldComponentsCollide())
{
// 尝试将中间文字胶囊移到左侧
if (CanAddComponentAtPosition("Left"))
{
TextCapsuleWidgetCenter.IsVisible = false;
TextCapsuleWidgetLeft!.IsVisible = true;
TextCapsuleWidgetLeft.SetTransparentBackground(_textCapsuleTransparentBackground);
TextCapsuleWidgetLeft.SetText(_textCapsuleContent);
}
// 或者移到右侧
else if (CanAddComponentAtPosition("Right"))
{
TextCapsuleWidgetCenter.IsVisible = false;
TextCapsuleWidgetRight!.IsVisible = true;
TextCapsuleWidgetRight.SetTransparentBackground(_textCapsuleTransparentBackground);
TextCapsuleWidgetRight.SetText(_textCapsuleContent);
}
// 如果都无法添加,则隐藏文字胶囊
else
{
TextCapsuleWidgetCenter.IsVisible = false;
}
}
// 调整网速组件位置(优先级:时钟 > 文字胶囊 > 网速)
if (NetworkSpeedWidgetLeft?.IsVisible == true && WouldComponentsCollide())
{
// 尝试将左侧网速移到中间
if (CanAddComponentAtPosition("Center"))
{
NetworkSpeedWidgetLeft.IsVisible = false;
NetworkSpeedWidgetCenter!.IsVisible = true;
NetworkSpeedWidgetCenter.SetTransparentBackground(_networkSpeedTransparentBackground);
NetworkSpeedWidgetCenter.SetDisplayMode(_networkSpeedDisplayMode);
}
// 或者移到右侧
else if (CanAddComponentAtPosition("Right"))
{
NetworkSpeedWidgetLeft.IsVisible = false;
NetworkSpeedWidgetRight!.IsVisible = true;
NetworkSpeedWidgetRight.SetTransparentBackground(_networkSpeedTransparentBackground);
NetworkSpeedWidgetRight.SetDisplayMode(_networkSpeedDisplayMode);
}
// 如果都无法添加,则隐藏网速
else
{
NetworkSpeedWidgetLeft.IsVisible = false;
}
}
if (NetworkSpeedWidgetRight?.IsVisible == true && WouldComponentsCollide())
{
// 尝试将右侧网速移到中间
if (CanAddComponentAtPosition("Center"))
{
NetworkSpeedWidgetRight.IsVisible = false;
NetworkSpeedWidgetCenter!.IsVisible = true;
NetworkSpeedWidgetCenter.SetTransparentBackground(_networkSpeedTransparentBackground);
NetworkSpeedWidgetCenter.SetDisplayMode(_networkSpeedDisplayMode);
}
// 或者移到左侧
else if (CanAddComponentAtPosition("Left"))
{
NetworkSpeedWidgetRight.IsVisible = false;
NetworkSpeedWidgetLeft!.IsVisible = true;
NetworkSpeedWidgetLeft.SetTransparentBackground(_networkSpeedTransparentBackground);
NetworkSpeedWidgetLeft.SetDisplayMode(_networkSpeedDisplayMode);
}
// 如果都无法添加,则隐藏网速
else
{
NetworkSpeedWidgetRight.IsVisible = false;
}
}
if (NetworkSpeedWidgetCenter?.IsVisible == true && WouldComponentsCollide())
{
// 尝试将中间网速移到左侧
if (CanAddComponentAtPosition("Left"))
{
NetworkSpeedWidgetCenter.IsVisible = false;
NetworkSpeedWidgetLeft!.IsVisible = true;
NetworkSpeedWidgetLeft.SetTransparentBackground(_networkSpeedTransparentBackground);
NetworkSpeedWidgetLeft.SetDisplayMode(_networkSpeedDisplayMode);
}
// 或者移到右侧
else if (CanAddComponentAtPosition("Right"))
{
NetworkSpeedWidgetCenter.IsVisible = false;
NetworkSpeedWidgetRight!.IsVisible = true;
NetworkSpeedWidgetRight.SetTransparentBackground(_networkSpeedTransparentBackground);
NetworkSpeedWidgetRight.SetDisplayMode(_networkSpeedDisplayMode);
}
// 如果都无法添加,则隐藏网速
else
{
NetworkSpeedWidgetCenter.IsVisible = false;
}
}
}
/// <summary>
/// 查找可用的替代位置
/// </summary>
private string? FindAlternativePosition(string originalPosition)
{
// 尝试所有可能的位置
var positions = new[] { "Left", "Center", "Right" };
foreach (var position in positions)
{
if (position != originalPosition && CanAddComponentAtPosition(position))
{
return position;
}
}
return null;
}
/// <summary>
/// 获取左侧可见组件列表
/// </summary>
private List<Control> GetVisibleLeftComponents()
{
var result = new List<Control>();
if (TopStatusLeftPanel is null) return result;
foreach (var child in TopStatusLeftPanel.Children)
{
if (child is Control control && control.IsVisible)
result.Add(control);
}
return result;
}
/// <summary>
/// 获取中间可见组件列表
/// </summary>
private List<Control> GetVisibleCenterComponents()
{
var result = new List<Control>();
if (TopStatusCenterPanel is null) return result;
foreach (var child in TopStatusCenterPanel.Children)
{
if (child is Control control && control.IsVisible)
result.Add(control);
}
return result;
}
/// <summary>
/// 获取右侧可见组件列表
/// </summary>
private List<Control> GetVisibleRightComponents()
{
var result = new List<Control>();
if (TopStatusRightPanel is null) return result;
foreach (var child in TopStatusRightPanel.Children)
{
if (child is Control control && control.IsVisible)
result.Add(control);
}
return result;
}
private TaskbarContext GetCurrentTaskbarContext()

View File

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

View File

@@ -650,8 +650,24 @@ public partial class MainWindow
TaskbarLayoutMode = _taskbarLayoutMode,
ClockDisplayFormat = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? "HourMinute" : "HourMinuteSecond",
StatusBarClockTransparentBackground = _statusBarClockTransparentBackground,
ClockPosition = _clockPosition,
ClockFontSize = _clockFontSize,
ShowTextCapsule = _showTextCapsule,
TextCapsuleContent = _textCapsuleContent,
TextCapsulePosition = _textCapsulePosition,
TextCapsuleTransparentBackground = _textCapsuleTransparentBackground,
TextCapsuleFontSize = _textCapsuleFontSize,
ShowNetworkSpeed = _showNetworkSpeed,
NetworkSpeedPosition = _networkSpeedPosition,
NetworkSpeedDisplayMode = _networkSpeedDisplayMode,
NetworkSpeedTransparentBackground = _networkSpeedTransparentBackground,
ShowNetworkTypeIcon = _showNetworkTypeIcon,
NetworkSpeedFontSize = _networkSpeedFontSize,
StatusBarSpacingMode = _statusBarSpacingMode,
StatusBarCustomSpacingPercent = _statusBarCustomSpacingPercent,
StatusBarShadowEnabled = _statusBarShadowEnabled,
StatusBarShadowColor = _statusBarShadowColor,
StatusBarShadowOpacity = _statusBarShadowOpacity,
DisabledPluginIds = existingSnapshot.DisabledPluginIds,
StudyFrameMs = existingSnapshot.StudyFrameMs,
StudyScoreThresholdDbfs = existingSnapshot.StudyScoreThresholdDbfs,

View File

@@ -189,50 +189,21 @@
Classes="surface-solid-strong"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="52"
MaxWidth="760"
MaxHeight="520"
CornerRadius="36"
Padding="14">
<Border.RenderTransform>
<TranslateTransform Y="42" />
</Border.RenderTransform>
<Grid RowDefinitions="Auto,*"
RowSpacing="10">
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="8">
<Button x:Name="LauncherFolderBackButton"
Grid.Column="0"
Width="38"
Height="34"
Padding="0"
Click="OnLauncherFolderBackClick">
<fi:FluentIcon Icon="ArrowLeft"
IconVariant="Regular" />
</Button>
<TextBlock x:Name="LauncherFolderTitleTextBlock"
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontWeight="SemiBold" />
<Button x:Name="LauncherFolderCloseButton"
Grid.Column="2"
Width="38"
Height="34"
Padding="0"
Click="OnLauncherFolderCloseClick">
<fi:FluentIcon Icon="Dismiss"
IconVariant="Regular" />
</Button>
</Grid>
Width="464"
Height="384"
CornerRadius="24"
Padding="16,14,16,12">
<Grid RowDefinitions="Auto,*">
<TextBlock x:Name="LauncherFolderTitleTextBlock"
FontSize="15"
FontWeight="SemiBold"
HorizontalAlignment="Center"
Margin="0,0,0,10" />
<ScrollViewer x:Name="LauncherFolderScrollViewer"
Grid.Row="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<WrapPanel x:Name="LauncherFolderTilePanel"
Orientation="Horizontal" />
</ScrollViewer>
<Grid x:Name="LauncherFolderGridPanel"
Grid.Row="1"
ColumnDefinitions="*,*,*,*"
RowDefinitions="*,*,*" />
</Grid>
</Border>
</Grid>
@@ -255,20 +226,84 @@
</Grid>
</Border>
<!-- 状态栏阴影层 - macOS 风格的完整阴影带 -->
<Border x:Name="StatusBarOverlay"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="1"
IsVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Height="24"
ZIndex="0"
Margin="0,0,0,-24">
<Border.Background>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
<GradientStop Color="#CC000000" Offset="0.0" />
<GradientStop Color="#66000000" Offset="0.3" />
<GradientStop Color="#00000000" Offset="1.0" />
</LinearGradientBrush>
</Border.Background>
</Border>
<Border x:Name="TopStatusBarHost"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="1"
Background="Transparent"
BorderThickness="0"
Padding="4">
<StackPanel x:Name="TopStatusComponentsPanel"
Orientation="Horizontal"
Spacing="6">
<comp:ClockWidget x:Name="ClockWidget"
IsVisible="False"
Margin="0" />
</StackPanel>
Padding="4"
ZIndex="2">
<Grid ColumnDefinitions="*,Auto,*">
<!-- 左侧状态栏组件 -->
<StackPanel x:Name="TopStatusLeftPanel"
Grid.Column="0"
Orientation="Horizontal"
Spacing="6"
HorizontalAlignment="Left">
<comp:ClockWidget x:Name="ClockWidgetLeft"
IsVisible="False"
Margin="0" />
<comp:TextCapsuleWidget x:Name="TextCapsuleWidgetLeft"
IsVisible="False"
Margin="0" />
<comp:NetworkSpeedWidget x:Name="NetworkSpeedWidgetLeft"
IsVisible="False"
Margin="0" />
</StackPanel>
<!-- 中间状态栏组件 -->
<StackPanel x:Name="TopStatusCenterPanel"
Grid.Column="1"
Orientation="Horizontal"
Spacing="6"
HorizontalAlignment="Center">
<comp:ClockWidget x:Name="ClockWidgetCenter"
IsVisible="False"
Margin="0" />
<comp:TextCapsuleWidget x:Name="TextCapsuleWidgetCenter"
IsVisible="False"
Margin="0" />
<comp:NetworkSpeedWidget x:Name="NetworkSpeedWidgetCenter"
IsVisible="False"
Margin="0" />
</StackPanel>
<!-- 右侧状态栏组件 -->
<StackPanel x:Name="TopStatusRightPanel"
Grid.Column="2"
Orientation="Horizontal"
Spacing="6"
HorizontalAlignment="Right">
<comp:ClockWidget x:Name="ClockWidgetRight"
IsVisible="False"
Margin="0" />
<comp:TextCapsuleWidget x:Name="TextCapsuleWidgetRight"
IsVisible="False"
Margin="0" />
<comp:NetworkSpeedWidget x:Name="NetworkSpeedWidgetRight"
IsVisible="False"
Margin="0" />
</StackPanel>
</Grid>
</Border>
<Border x:Name="BottomTaskbarContainer"

View File

@@ -135,6 +135,22 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
private string _statusBarSpacingMode = "Relaxed";
private int _statusBarCustomSpacingPercent = 12;
private bool _statusBarClockTransparentBackground;
private string _clockPosition = "Left"; // Left, Center, Right
private string _clockFontSize = "Medium"; // Small, Medium, Large
private bool _showTextCapsule;
private string _textCapsuleContent = "**Hello** World!";
private string _textCapsulePosition = "Right"; // Left, Center, Right
private bool _textCapsuleTransparentBackground;
private string _textCapsuleFontSize = "Medium"; // Small, Medium, Large
private bool _showNetworkSpeed;
private string _networkSpeedPosition = "Right"; // Left, Center, Right
private string _networkSpeedDisplayMode = "Both"; // Upload, Download, Both
private bool _networkSpeedTransparentBackground;
private bool _showNetworkTypeIcon;
private string _networkSpeedFontSize = "Medium"; // Small, Medium, Large
private bool _statusBarShadowEnabled;
private string _statusBarShadowColor = "#000000";
private double _statusBarShadowOpacity = 0.3;
private int _desktopEdgeInsetPercent = DefaultEdgeInsetPercent;
private string _taskbarLayoutMode = TaskbarLayoutBottomFullRowMacStyle;
private string _languageCode = "zh-CN";
@@ -238,9 +254,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
TaskbarProfileButton.IsEnabled = false;
TaskbarProfilePopup.IsOpen = false;
ClockWidget.IsVisible = true;
ClockWidget.SetDisplayFormat(ClockDisplayFormat.HourMinute);
ClockWidget.SetTransparentBackground(false);
ClockWidgetLeft.IsVisible = true;
ClockWidgetLeft.SetDisplayFormat(ClockDisplayFormat.HourMinute);
ClockWidgetLeft.SetTransparentBackground(false);
ConfigureDesignTimeDesktopGrid();
PopulateDesignTimeDesktopSurface();
@@ -288,7 +304,7 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
DesktopPagesHost.ColumnDefinitions.Clear();
DesktopPagesHost.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(1, GridUnitType.Star)));
ClockWidget.ApplyCellSize(72);
ClockWidgetLeft.ApplyCellSize(72);
}
private void PopulateDesignTimeDesktopSurface()
@@ -481,7 +497,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
RebuildDesktopGrid();
LoadLauncherEntriesAsync();
InitializeTimeZoneSettings();
ClockWidget.SetTimeZoneService(_timeZoneService);
ClockWidgetLeft.SetTimeZoneService(_timeZoneService);
ClockWidgetCenter.SetTimeZoneService(_timeZoneService);
ClockWidgetRight.SetTimeZoneService(_timeZoneService);
_suppressSettingsPersistence = false;
PersistSettings();
@@ -621,7 +639,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
private void ApplyDesktopStatusBarComponentSpacing()
{
ApplyStatusBarComponentSpacingForPanel(TopStatusComponentsPanel, _currentDesktopCellSize);
ApplyStatusBarComponentSpacingForPanel(TopStatusLeftPanel, _currentDesktopCellSize);
ApplyStatusBarComponentSpacingForPanel(TopStatusCenterPanel, _currentDesktopCellSize);
ApplyStatusBarComponentSpacingForPanel(TopStatusRightPanel, _currentDesktopCellSize);
}
private int ResolveStatusBarSpacingPercent()
@@ -697,8 +717,26 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
ApplyUnifiedMainRectangleChrome();
BottomTaskbarContainer.Padding = new Thickness(Math.Clamp(taskbarCellHeight * 0.16, 6, 14));
ClockWidget.Margin = new Thickness(0);
ClockWidget.ApplyCellSize(cellSize);
ClockWidgetLeft.Margin = new Thickness(0);
ClockWidgetLeft.ApplyCellSize(cellSize);
ClockWidgetCenter.Margin = new Thickness(0);
ClockWidgetCenter.ApplyCellSize(cellSize);
ClockWidgetRight.Margin = new Thickness(0);
ClockWidgetRight.ApplyCellSize(cellSize);
TextCapsuleWidgetLeft.Margin = new Thickness(0);
TextCapsuleWidgetLeft.ApplyCellSize(cellSize);
TextCapsuleWidgetCenter.Margin = new Thickness(0);
TextCapsuleWidgetCenter.ApplyCellSize(cellSize);
TextCapsuleWidgetRight.Margin = new Thickness(0);
TextCapsuleWidgetRight.ApplyCellSize(cellSize);
NetworkSpeedWidgetLeft.Margin = new Thickness(0);
NetworkSpeedWidgetLeft.ApplyCellSize(cellSize);
NetworkSpeedWidgetCenter.Margin = new Thickness(0);
NetworkSpeedWidgetCenter.ApplyCellSize(cellSize);
NetworkSpeedWidgetRight.Margin = new Thickness(0);
NetworkSpeedWidgetRight.ApplyCellSize(cellSize);
var buttonMinWidth = Math.Clamp(taskbarCellHeight * 2.35, 100, 340);
@@ -737,7 +775,15 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
if (_currentDesktopCellSize > 0)
{
ClockWidget.ApplyCellSize(_currentDesktopCellSize);
ClockWidgetLeft.ApplyCellSize(_currentDesktopCellSize);
ClockWidgetCenter.ApplyCellSize(_currentDesktopCellSize);
ClockWidgetRight.ApplyCellSize(_currentDesktopCellSize);
TextCapsuleWidgetLeft.ApplyCellSize(_currentDesktopCellSize);
TextCapsuleWidgetCenter.ApplyCellSize(_currentDesktopCellSize);
TextCapsuleWidgetRight.ApplyCellSize(_currentDesktopCellSize);
NetworkSpeedWidgetLeft.ApplyCellSize(_currentDesktopCellSize);
NetworkSpeedWidgetCenter.ApplyCellSize(_currentDesktopCellSize);
NetworkSpeedWidgetRight.ApplyCellSize(_currentDesktopCellSize);
}
}

View File

@@ -52,6 +52,182 @@
VerticalAlignment="Center" />
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="16">
<TextBlock Text="{Binding ClockPositionLabel}"
VerticalAlignment="Center" />
<ComboBox Grid.Column="1"
Width="220"
IsEnabled="{Binding ShowClock}"
ItemsSource="{Binding ClockPositions}"
SelectedItem="{Binding SelectedClockPosition}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="16">
<TextBlock Text="{Binding ClockFontSizeLabel}"
VerticalAlignment="Center" />
<ComboBox Grid.Column="1"
Width="220"
IsEnabled="{Binding ShowClock}"
ItemsSource="{Binding ClockFontSizes}"
SelectedItem="{Binding SelectedClockFontSize}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<ui:SettingsExpander Header="{Binding TextCapsuleHeader}"
Description="{Binding TextCapsuleDescription}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="TextQuote" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding ShowTextCapsule}" />
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="16">
<TextBlock Text="{Binding TextCapsuleContentLabel}"
VerticalAlignment="Top"
Margin="0,8,0,0" />
<TextBox Grid.Column="1"
AcceptsReturn="True"
TextWrapping="Wrap"
Height="100"
IsEnabled="{Binding ShowTextCapsule}"
Text="{Binding TextCapsuleContent}"
Watermark="Enter Markdown text..." />
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="16">
<TextBlock Text="{Binding TextCapsulePositionLabel}"
VerticalAlignment="Center" />
<ComboBox Grid.Column="1"
Width="220"
IsEnabled="{Binding ShowTextCapsule}"
ItemsSource="{Binding TextCapsulePositions}"
SelectedItem="{Binding SelectedTextCapsulePosition}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="*,Auto"
ColumnSpacing="16">
<TextBlock Text="{Binding TextCapsuleTransparentBackgroundLabel}"
VerticalAlignment="Center" />
<ToggleSwitch Grid.Column="1"
IsChecked="{Binding TextCapsuleTransparentBackground}"
IsEnabled="{Binding ShowTextCapsule}"
VerticalAlignment="Center" />
</Grid>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<ui:SettingsExpander Header="{Binding NetworkSpeedHeader}"
Description="{Binding NetworkSpeedDescription}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="ArrowBidirectionalUpDown" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding ShowNetworkSpeed}" />
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="16">
<TextBlock Text="{Binding NetworkSpeedPositionLabel}"
VerticalAlignment="Center" />
<ComboBox Grid.Column="1"
Width="220"
IsEnabled="{Binding ShowNetworkSpeed}"
ItemsSource="{Binding NetworkSpeedPositions}"
SelectedItem="{Binding SelectedNetworkSpeedPosition}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="16">
<TextBlock Text="{Binding NetworkSpeedDisplayModeLabel}"
VerticalAlignment="Center" />
<ComboBox Grid.Column="1"
Width="220"
IsEnabled="{Binding ShowNetworkSpeed}"
ItemsSource="{Binding NetworkSpeedDisplayModes}"
SelectedItem="{Binding SelectedNetworkSpeedDisplayMode}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="*,Auto"
ColumnSpacing="16">
<TextBlock Text="{Binding NetworkSpeedTransparentBackgroundLabel}"
VerticalAlignment="Center" />
<ToggleSwitch Grid.Column="1"
IsChecked="{Binding NetworkSpeedTransparentBackground}"
IsEnabled="{Binding ShowNetworkSpeed}"
VerticalAlignment="Center" />
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="*,Auto"
ColumnSpacing="16">
<TextBlock Text="{Binding ShowNetworkTypeIconLabel}"
VerticalAlignment="Center" />
<ToggleSwitch Grid.Column="1"
IsChecked="{Binding ShowNetworkTypeIcon}"
IsEnabled="{Binding ShowNetworkSpeed}"
VerticalAlignment="Center" />
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="16">
<TextBlock Text="{Binding NetworkSpeedFontSizeLabel}"
VerticalAlignment="Center" />
<ComboBox Grid.Column="1"
Width="220"
IsEnabled="{Binding ShowNetworkSpeed}"
ItemsSource="{Binding NetworkSpeedFontSizes}"
SelectedItem="{Binding SelectedNetworkSpeedFontSize}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<Separator Classes="settings-separator" />
@@ -92,6 +268,55 @@
</Grid>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<Separator Classes="settings-separator" />
<controls:IconText Icon="Square"
Text="{Binding StatusBarShadowHeader}"
Margin="0,0,0,4" />
<ui:SettingsExpander Header="{Binding StatusBarShadowHeader}"
Description="{Binding StatusBarShadowDescription}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Square" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding StatusBarShadowEnabled}" />
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="16">
<TextBlock Text="{Binding StatusBarShadowColorLabel}"
VerticalAlignment="Center" />
<Button Grid.Column="1"
HorizontalAlignment="Right"
IsEnabled="{Binding StatusBarShadowEnabled}">
<Border Width="32"
Height="32"
CornerRadius="4"
Background="{Binding StatusBarShadowColorBrush}" />
<Button.Flyout>
<Flyout Placement="BottomEdgeAlignedRight">
<ColorPicker Color="{Binding StatusBarShadowColor}" />
</Flyout>
</Button.Flyout>
</Button>
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="16">
<TextBlock Text="{Binding StatusBarShadowOpacityLabel}"
VerticalAlignment="Center" />
<Slider Grid.Column="1"
Minimum="0"
Maximum="100"
TickFrequency="10"
IsEnabled="{Binding StatusBarShadowEnabled}"
Value="{Binding StatusBarShadowOpacity}" />
</Grid>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

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

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
@@ -10,6 +11,8 @@ using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Views.Components;
namespace LanMountainDesktop.Views;
@@ -19,9 +22,6 @@ namespace LanMountainDesktop.Views;
/// </summary>
public partial class TransparentOverlayWindow : Window
{
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
private readonly IWindowBottomMostService _bottomMostService = WindowBottomMostServiceFactory.GetOrCreate();
private readonly IRegionPassthroughService _regionPassthroughService = RegionPassthroughServiceFactory.GetOrCreate();
private readonly IFusedDesktopLayoutService _layoutService = FusedDesktopLayoutServiceProvider.GetOrCreate();
// 滑动状态
@@ -41,41 +41,137 @@ public partial class TransparentOverlayWindow : Window
private readonly Dictionary<string, Border> _componentHosts = [];
private readonly List<Rect> _interactiveRegions = [];
private FusedDesktopLayoutSnapshot _layout = new();
private ComponentRegistry? _componentRegistry;
private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry;
// 拖拽状态
// 基础服务
private readonly IWeatherInfoService _weatherDataService;
private readonly TimeZoneService _timeZoneService;
private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService();
private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService();
// 渲染参数
private const double DefaultCellSize = 100;
private double _currentDesktopCellSize;
// 拖拽与缩放状态
private bool _isDragging;
private string? _draggingPlacementId;
private Point _dragStartPoint;
private Border? _draggingHost;
private bool _isResizing;
private string? _interactionPlacementId;
private Point _interactionStartPoint;
private double _interactionOriginalX;
private double _interactionOriginalY;
private double _interactionOriginalWidth;
private double _interactionOriginalHeight;
private Border? _interactionHost;
// 选中状态
private Border? _selectedHost;
public event EventHandler? RestoreMainWindowRequested;
public TransparentOverlayWindow()
{
InitializeComponent();
var facade = HostSettingsFacadeProvider.GetOrCreate();
_weatherDataService = facade.Weather.GetWeatherInfoService();
_timeZoneService = facade.Region.GetTimeZoneService();
_settingsFacade = facade;
}
private readonly ISettingsFacadeService _settingsFacade;
public void SaveLayoutAndHide()
{
SaveLayout();
Hide();
// 仅在 Windows 上启用置底功能
if (OperatingSystem.IsWindows())
// Remove all components so that next time we open it builds fresh from snapshot
if (Content is Canvas canvas)
{
_bottomMostService.SetupBottomMost(this);
canvas.Children.Clear();
}
_componentHosts.Clear();
}
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
if (OperatingSystem.IsWindows())
if (Screens.Primary is { } primaryScreen)
{
_bottomMostService.SendToBottom(this);
// 避开系统任务栏
var workArea = primaryScreen.WorkingArea;
var scaling = primaryScreen.Scaling;
Position = new PixelPoint(workArea.X, workArea.Y);
Width = workArea.Width / scaling;
Height = workArea.Height / scaling;
// 基于设置计算单元格尺寸
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
var shortCells = Math.Clamp(appSnapshot.GridShortSideCells > 0 ? appSnapshot.GridShortSideCells : 12, 6, 96);
_currentDesktopCellSize = Height / shortCells;
}
else
{
_currentDesktopCellSize = DefaultCellSize;
}
if (Content is Canvas canvas)
{
// 保证透明区域也能被抓取事件
canvas.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
}
// 加载布局
// 确保注册表已初始化
EnsureRegistries();
// 加载布局并渲染
_layout = _layoutService.Load();
RenderAllComponents();
// TODO: 渲染组件(需要从 MainWindow 获取组件注册表)
AppLogger.Info("TransparentOverlay", $"Opened with {_layout.ComponentPlacements.Count} components.");
}
/// <summary>
/// 确保组件运行时注册表已初始化
/// </summary>
private void EnsureRegistries()
{
if (_componentRuntimeRegistry is not null) return;
AppLogger.Info("TransparentOverlay", "Transparent overlay window opened.");
var pluginRuntimeService = (Application.Current as App)?.PluginRuntimeService;
_componentRegistry = DesktopComponentRegistryFactory.Create(pluginRuntimeService);
_componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry(
_componentRegistry,
pluginRuntimeService,
_settingsFacade);
}
/// <summary>
/// 渲染所有布局中的组件
/// </summary>
private void RenderAllComponents()
{
if (Content is not Canvas canvas) return;
canvas.Children.Clear();
_componentHosts.Clear();
_selectedHost = null;
foreach (var placement in _layout.ComponentPlacements)
{
try
{
RenderComponentInternal(placement);
}
catch (Exception ex)
{
AppLogger.Warn("TransparentOverlay", $"Failed to render component {placement.ComponentId}", ex);
}
}
UpdateInteractiveRegions();
}
protected override void OnClosed(EventArgs e)
@@ -89,16 +185,7 @@ public partial class TransparentOverlayWindow : Window
/// </summary>
private void UpdateInteractiveRegions()
{
_interactiveRegions.Clear();
foreach (var host in _componentHosts.Values)
{
var x = Canvas.GetLeft(host);
var y = Canvas.GetTop(host);
_interactiveRegions.Add(new Rect(x, y, host.Width, host.Height));
}
_regionPassthroughService.SetInteractiveRegions(this, _interactiveRegions);
// 编辑模式下不再需要底层穿透功能计算,这里留空或移除
}
/// <summary>
@@ -112,8 +199,25 @@ public partial class TransparentOverlayWindow : Window
/// <summary>
/// 添加组件(供外部调用)
/// </summary>
public void AddComponent(string componentId, double x, double y, double width = 200, double height = 200)
public void AddComponent(string componentId, double x, double y, double? width = null, double? height = null)
{
EnsureRegistries();
if (_componentRegistry == null || !_componentRegistry.TryGetDefinition(componentId, out var definition))
{
AppLogger.Warn("TransparentOverlay", $"Cannot add unknown component: {componentId}");
return;
}
var finalWidth = width ?? (definition.MinWidthCells * _currentDesktopCellSize);
var finalHeight = height ?? (definition.MinHeightCells * _currentDesktopCellSize);
// 对齐网格
x = Math.Round(x / _currentDesktopCellSize) * _currentDesktopCellSize;
y = Math.Round(y / _currentDesktopCellSize) * _currentDesktopCellSize;
finalWidth = Math.Round(finalWidth / _currentDesktopCellSize) * _currentDesktopCellSize;
finalHeight = Math.Round(finalHeight / _currentDesktopCellSize) * _currentDesktopCellSize;
var placementId = Guid.NewGuid().ToString("N");
var placement = new FusedDesktopComponentPlacementSnapshot
{
@@ -121,16 +225,74 @@ public partial class TransparentOverlayWindow : Window
ComponentId = componentId,
X = x,
Y = y,
Width = width,
Height = height,
Width = finalWidth,
Height = finalHeight,
ZIndex = _layout.ComponentPlacements.Count
};
_layout.ComponentPlacements.Add(placement);
UpdateInteractiveRegions();
SaveLayout();
AppLogger.Info("TransparentOverlay", $"Added component: {componentId} at ({x}, {y})");
// 立即渲染
try
{
RenderComponentInternal(placement);
UpdateInteractiveRegions();
SaveLayout();
AppLogger.Info("TransparentOverlay", $"Added component: {componentId} at ({x}, {y}) size ({finalWidth}x{finalHeight})");
}
catch (Exception ex)
{
AppLogger.Warn("TransparentOverlay", $"Failed to add component {componentId}", ex);
_layout.ComponentPlacements.Remove(placement);
}
}
/// <summary>
/// 内部渲染单个组件
/// </summary>
private void RenderComponentInternal(FusedDesktopComponentPlacementSnapshot placement)
{
if (_componentRuntimeRegistry is null || !_componentRuntimeRegistry.TryGetDescriptor(placement.ComponentId, out var descriptor))
{
AppLogger.Warn("TransparentOverlay", $"Unknown component: {placement.ComponentId}");
return;
}
// 【修复问题3】尝试从现有窗口中获取组件实例避免重新创建导致状态丢失
var control = TryGetExistingControl(placement.PlacementId);
if (control is null)
{
// 如果没有现有实例,才创建新的
control = descriptor.CreateControl(
_currentDesktopCellSize,
_timeZoneService,
_weatherDataService,
_recommendationInfoService,
_calculatorDataService,
_settingsFacade,
placement.PlacementId);
}
RenderComponent(placement.PlacementId, control, placement.X, placement.Y, placement.Width, placement.Height);
}
/// <summary>
/// 【修复问题3】尝试从现有的小窗口中获取组件控件实例
/// </summary>
private Control? TryGetExistingControl(string placementId)
{
try
{
var manager = FusedDesktopManagerServiceFactory.GetOrCreate();
// 通过反射或公共 API 获取现有窗口中的控件
// 这里需要 FusedDesktopManagerService 提供获取控件的方法
// 暂时返回 null后续需要扩展接口
return null;
}
catch
{
return null;
}
}
/// <summary>
@@ -157,24 +319,47 @@ public partial class TransparentOverlayWindow : Window
/// </summary>
public void RenderComponent(string placementId, Control component, double x, double y, double width, double height)
{
var grid = new Grid();
grid.Children.Add(component);
var resizeHandle = new Border
{
Width = 24,
Height = 24,
Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#3B82F6")),
CornerRadius = new Avalonia.CornerRadius(12),
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom,
Margin = new Avalonia.Thickness(0, 0, -12, -12),
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.BottomRightCorner),
Tag = "desktop-component-resize-handle",
IsVisible = false
};
grid.Children.Add(resizeHandle);
var host = new Border
{
Tag = placementId,
Width = width,
Height = height,
Background = Brushes.Transparent,
CornerRadius = new CornerRadius(12),
ClipToBounds = true,
Child = component
Background = Avalonia.Media.Brushes.Transparent,
CornerRadius = new Avalonia.CornerRadius(12),
ClipToBounds = false, // 允许把手溢出
BorderBrush = Avalonia.Media.Brushes.Transparent,
BorderThickness = new Avalonia.Thickness(3),
Child = grid,
Classes = { "desktop-component-host" }
};
Canvas.SetLeft(host, x);
Canvas.SetTop(host, y);
// 添加拖拽支持
host.PointerPressed += OnComponentPointerPressed;
host.PointerMoved += OnComponentPointerMoved;
host.PointerReleased += OnComponentPointerReleased;
host.PointerMoved += OnInteractionPointerMoved;
host.PointerReleased += OnInteractionPointerReleased;
// 右键上下文菜单(删除组件)
host.ContextRequested += OnComponentContextRequested;
if (Content is Canvas canvas)
{
@@ -185,7 +370,87 @@ public partial class TransparentOverlayWindow : Window
UpdateInteractiveRegions();
}
// 组件拖拽处理
// 组件右键上下文菜单(删除)
private void OnComponentContextRequested(object? sender, ContextRequestedEventArgs e)
{
if (sender is not Border host || host.Tag is not string placementId) return;
// 构建上下文菜单
var deleteItem = new MenuItem
{
Header = "移除组件",
Icon = new Avalonia.Controls.TextBlock { Text = "🗑" }
};
deleteItem.Click += (_, _) =>
{
RemoveComponent(placementId);
AppLogger.Info("TransparentOverlay", $"Component removed via context menu: {placementId}");
};
var menu = new ContextMenu
{
Items = { deleteItem }
};
// 显示在当前控件上
menu.Open(host);
e.Handled = true;
}
// 取消选中
private void OnCanvasPointerPressed(object? sender, PointerPressedEventArgs e)
{
DeselectComponent();
}
// 选中组件
private void SelectComponent(Border host)
{
if (_selectedHost == host) return;
DeselectComponent();
_selectedHost = host;
// 渲染选中边框和把手
host.BorderBrush = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#3B82F6"));
host.Classes.Add("desktop-component-host-selected");
if (host.Child is Grid grid)
{
foreach (var child in grid.Children)
{
if (child is Control c && c.Tag is string tg && tg == "desktop-component-resize-handle")
{
c.IsVisible = true;
break;
}
}
}
}
private void DeselectComponent()
{
if (_selectedHost != null)
{
_selectedHost.BorderBrush = Avalonia.Media.Brushes.Transparent;
_selectedHost.Classes.Remove("desktop-component-host-selected");
if (_selectedHost.Child is Grid grid)
{
foreach (var child in grid.Children)
{
if (child is Control c && c.Tag is string tg && tg == "desktop-component-resize-handle")
{
c.IsVisible = false;
break;
}
}
}
}
_selectedHost = null;
}
// 组件拖拽与缩放处理
private void OnComponentPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (sender is not Border host || host.Tag is not string placementId) return;
@@ -193,55 +458,97 @@ public partial class TransparentOverlayWindow : Window
var point = e.GetCurrentPoint(this);
if (!point.Properties.IsLeftButtonPressed) return;
_isDragging = true;
_draggingPlacementId = placementId;
_draggingHost = host;
_dragStartPoint = e.GetPosition(this);
SelectComponent(host);
_interactionPlacementId = placementId;
_interactionHost = host;
_interactionStartPoint = e.GetPosition(this);
// 这里必须用未吸附的原始屏幕位置计算 delta
_interactionOriginalX = Canvas.GetLeft(host);
_interactionOriginalY = Canvas.GetTop(host);
_interactionOriginalWidth = host.Width;
_interactionOriginalHeight = host.Height;
if (e.Source is Control sourceControl && sourceControl.Tag is string tag && tag == "desktop-component-resize-handle")
{
_isResizing = true;
_isDragging = false;
}
else
{
_isDragging = true;
_isResizing = false;
}
e.Pointer.Capture(host);
e.Handled = true;
}
private void OnComponentPointerMoved(object? sender, PointerEventArgs e)
private void OnInteractionPointerMoved(object? sender, PointerEventArgs e)
{
if (!_isDragging || _draggingHost is null) return;
if ((!_isDragging && !_isResizing) || _interactionHost is null) return;
var currentPoint = e.GetPosition(this);
var deltaX = currentPoint.X - _dragStartPoint.X;
var deltaY = currentPoint.Y - _dragStartPoint.Y;
var deltaX = currentPoint.X - _interactionStartPoint.X;
var deltaY = currentPoint.Y - _interactionStartPoint.Y;
var currentX = Canvas.GetLeft(_draggingHost);
var currentY = Canvas.GetTop(_draggingHost);
if (_isDragging)
{
var rawX = _interactionOriginalX + deltaX;
var rawY = _interactionOriginalY + deltaY;
var snapX = Math.Round(rawX / _currentDesktopCellSize) * _currentDesktopCellSize;
var snapY = Math.Round(rawY / _currentDesktopCellSize) * _currentDesktopCellSize;
Canvas.SetLeft(_interactionHost, snapX);
Canvas.SetTop(_interactionHost, snapY);
}
else if (_isResizing)
{
var rawWidth = _interactionOriginalWidth + deltaX;
var rawHeight = _interactionOriginalHeight + deltaY;
var snapWidth = Math.Round(rawWidth / _currentDesktopCellSize) * _currentDesktopCellSize;
var snapHeight = Math.Round(rawHeight / _currentDesktopCellSize) * _currentDesktopCellSize;
// 防溢出与极小值保护
snapWidth = Math.Max(_currentDesktopCellSize, snapWidth);
snapHeight = Math.Max(_currentDesktopCellSize, snapHeight);
_interactionHost.Width = snapWidth;
_interactionHost.Height = snapHeight;
}
Canvas.SetLeft(_draggingHost, currentX + deltaX);
Canvas.SetTop(_draggingHost, currentY + deltaY);
_dragStartPoint = currentPoint;
e.Handled = true;
}
private void OnComponentPointerReleased(object? sender, PointerReleasedEventArgs e)
private void OnInteractionPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (!_isDragging || _draggingHost is null || _draggingPlacementId is null)
if ((!_isDragging && !_isResizing) || _interactionHost is null || _interactionPlacementId is null)
{
_isDragging = false;
_isResizing = false;
return;
}
// 更新布局中的位置
var placement = _layout.ComponentPlacements.Find(p => p.PlacementId == _draggingPlacementId);
// 更新布局中的位置与尺寸
var placement = _layout.ComponentPlacements.Find(p => p.PlacementId == _interactionPlacementId);
if (placement is not null)
{
placement.X = Canvas.GetLeft(_draggingHost);
placement.Y = Canvas.GetTop(_draggingHost);
placement.X = Canvas.GetLeft(_interactionHost);
placement.Y = Canvas.GetTop(_interactionHost);
placement.Width = _interactionHost.Width;
placement.Height = _interactionHost.Height;
}
UpdateInteractiveRegions();
SaveLayout();
_isDragging = false;
_draggingPlacementId = null;
_draggingHost = null;
_isResizing = false;
_interactionPlacementId = null;
_interactionHost = null;
e.Pointer.Capture(null);
e.Handled = true;