mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-27 21:04:27 +08:00
Compare commits
4 Commits
35976c3f3d
...
v0.8.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
021c7ff245 | ||
|
|
675096b6c4 | ||
|
|
1c3cc76f21 | ||
|
|
44b87ba12e |
@@ -149,6 +149,11 @@ public partial class App : Application
|
|||||||
LinuxDesktopEntryInstaller.EnsureInstalled();
|
LinuxDesktopEntryInstaller.EnsureInstalled();
|
||||||
DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell);
|
DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell);
|
||||||
|
|
||||||
|
if (!Design.IsDesignMode && OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
FusedDesktopManagerServiceFactory.GetOrCreate().Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
base.OnFrameworkInitializationCompleted();
|
base.OnFrameworkInitializationCompleted();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +232,9 @@ public partial class App : Application
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换进入编辑模式,隐藏常态零散的小部件
|
||||||
|
FusedDesktopManagerServiceFactory.GetOrCreate().EnterEditMode();
|
||||||
|
|
||||||
// 确保透明覆盖层窗口存在并显示
|
// 确保透明覆盖层窗口存在并显示
|
||||||
EnsureTransparentOverlayWindow();
|
EnsureTransparentOverlayWindow();
|
||||||
|
|
||||||
@@ -248,6 +256,19 @@ public partial class App : Application
|
|||||||
window.SetOverlayWindow(_transparentOverlayWindow);
|
window.SetOverlayWindow(_transparentOverlayWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 当组件库关闭时,退出编辑态
|
||||||
|
window.Closed += (s, ev) =>
|
||||||
|
{
|
||||||
|
if (_transparentOverlayWindow is not null)
|
||||||
|
{
|
||||||
|
// 触发画布保存,并隐藏画布
|
||||||
|
_transparentOverlayWindow.SaveLayoutAndHide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 让管理器根据已存储的最新快照重建生成所有实体小组件
|
||||||
|
FusedDesktopManagerServiceFactory.GetOrCreate().ExitEditMode();
|
||||||
|
};
|
||||||
|
|
||||||
window.Show();
|
window.Show();
|
||||||
window.Activate();
|
window.Activate();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -388,6 +388,41 @@
|
|||||||
"settings.status_bar.clock_format_label": "Clock format",
|
"settings.status_bar.clock_format_label": "Clock format",
|
||||||
"settings.status_bar.clock_format.hm": "Hour:Minute",
|
"settings.status_bar.clock_format.hm": "Hour:Minute",
|
||||||
"settings.status_bar.clock_format.hms": "Hour:Minute:Second",
|
"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.title": "Components",
|
||||||
"settings.components.description": "Adjust component layout and corner design.",
|
"settings.components.description": "Adjust component layout and corner design.",
|
||||||
"settings.components.grid_header": "Grid Settings",
|
"settings.components.grid_header": "Grid Settings",
|
||||||
|
|||||||
@@ -331,6 +331,41 @@
|
|||||||
"settings.status_bar.clock_format_label": "時計の形式",
|
"settings.status_bar.clock_format_label": "時計の形式",
|
||||||
"settings.status_bar.clock_format.hm": "時:分",
|
"settings.status_bar.clock_format.hm": "時:分",
|
||||||
"settings.status_bar.clock_format.hms": "時:分:秒",
|
"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.title": "コンポーネント",
|
||||||
"settings.components.description": "コンポーネントのレイアウトとコーナーデザインを調整します。",
|
"settings.components.description": "コンポーネントのレイアウトとコーナーデザインを調整します。",
|
||||||
"settings.components.grid_header": "グリッド設定",
|
"settings.components.grid_header": "グリッド設定",
|
||||||
|
|||||||
@@ -377,6 +377,41 @@
|
|||||||
"settings.status_bar.clock_format_label": "시계 형식",
|
"settings.status_bar.clock_format_label": "시계 형식",
|
||||||
"settings.status_bar.clock_format.hm": "시:분",
|
"settings.status_bar.clock_format.hm": "시:분",
|
||||||
"settings.status_bar.clock_format.hms": "시:분:초",
|
"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.title": "컴포넌트",
|
||||||
"settings.components.description": "컴포넌트 레이아웃과 모서리 디자인을 조정합니다.",
|
"settings.components.description": "컴포넌트 레이아웃과 모서리 디자인을 조정합니다.",
|
||||||
"settings.components.grid_header": "그리드 설정",
|
"settings.components.grid_header": "그리드 설정",
|
||||||
|
|||||||
@@ -383,6 +383,41 @@
|
|||||||
"settings.status_bar.clock_format_label": "时钟格式",
|
"settings.status_bar.clock_format_label": "时钟格式",
|
||||||
"settings.status_bar.clock_format.hm": "时:分",
|
"settings.status_bar.clock_format.hm": "时:分",
|
||||||
"settings.status_bar.clock_format.hms": "时:分:秒",
|
"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.title": "组件",
|
||||||
"settings.components.description": "调整组件布局与圆角设计。",
|
"settings.components.description": "调整组件布局与圆角设计。",
|
||||||
"settings.components.grid_header": "网格设置",
|
"settings.components.grid_header": "网格设置",
|
||||||
|
|||||||
@@ -112,8 +112,40 @@ public sealed class AppSettingsSnapshot
|
|||||||
|
|
||||||
public bool StatusBarClockTransparentBackground { get; set; }
|
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 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 int StatusBarCustomSpacingPercent { get; set; } = 12;
|
||||||
|
|
||||||
public bool EnableThreeFingerSwipe { get; set; } = false;
|
public bool EnableThreeFingerSwipe { get; set; } = false;
|
||||||
|
|||||||
@@ -135,6 +135,55 @@ public static class ZhiJiaoHubSources
|
|||||||
_ => ClassIsland
|
_ => ClassIsland
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string GetDisplayName(string source)
|
||||||
|
{
|
||||||
|
return source?.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
Sectl => "SECTL 图库",
|
||||||
|
RinLit => "Rin's 图库",
|
||||||
|
_ => "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 string ApiUrl => $"https://api.github.com/repos/{Owner}/{Repo}/contents/{Path}";
|
||||||
|
public string RawUrlTemplate => $"https://raw.githubusercontent.com/{Owner}/{Repo}/main/{Path}/{{0}}";
|
||||||
|
|
||||||
|
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 = "assets/images",
|
||||||
|
DisplayName = "Rin's 图库"
|
||||||
|
},
|
||||||
|
_ => new ZhiJiaoHubSourceConfig
|
||||||
|
{
|
||||||
|
Owner = "ClassIsland",
|
||||||
|
Repo = "classisland-hub",
|
||||||
|
Path = "images",
|
||||||
|
DisplayName = "ClassIsland 图库"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 智教Hub镜像加速源常量
|
// 智教Hub镜像加速源常量
|
||||||
|
|||||||
195
LanMountainDesktop/Services/FusedDesktopManagerService.cs
Normal file
195
LanMountainDesktop/Services/FusedDesktopManagerService.cs
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using LanMountainDesktop.ComponentSystem;
|
||||||
|
using LanMountainDesktop.Models;
|
||||||
|
using LanMountainDesktop.PluginSdk;
|
||||||
|
using LanMountainDesktop.Services.Settings;
|
||||||
|
using LanMountainDesktop.Views;
|
||||||
|
using LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 融合桌面中央管理器服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IFusedDesktopManagerService
|
||||||
|
{
|
||||||
|
void Initialize();
|
||||||
|
void EnterEditMode();
|
||||||
|
void ExitEditMode();
|
||||||
|
void ReloadWidgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 融合桌面中央管理器服务实现。用于管理常态下的各个小窗口实体。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class FusedDesktopManagerService : IFusedDesktopManagerService
|
||||||
|
{
|
||||||
|
private readonly IFusedDesktopLayoutService _layoutService;
|
||||||
|
private readonly ISettingsFacadeService _settingsFacade;
|
||||||
|
private readonly Dictionary<string, DesktopWidgetWindow> _widgetWindows = [];
|
||||||
|
|
||||||
|
// 基础服务依赖
|
||||||
|
private readonly IWeatherInfoService _weatherDataService;
|
||||||
|
private readonly TimeZoneService _timeZoneService;
|
||||||
|
private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService();
|
||||||
|
private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService();
|
||||||
|
|
||||||
|
private ComponentRegistry? _componentRegistry;
|
||||||
|
private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry;
|
||||||
|
private bool _isEditMode;
|
||||||
|
|
||||||
|
private const double DefaultCellSize = 100;
|
||||||
|
|
||||||
|
public FusedDesktopManagerService(
|
||||||
|
IFusedDesktopLayoutService layoutService,
|
||||||
|
ISettingsFacadeService settingsFacade)
|
||||||
|
{
|
||||||
|
_layoutService = layoutService;
|
||||||
|
_settingsFacade = settingsFacade;
|
||||||
|
|
||||||
|
_weatherDataService = _settingsFacade.Weather.GetWeatherInfoService();
|
||||||
|
_timeZoneService = _settingsFacade.Region.GetTimeZoneService();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows()) return;
|
||||||
|
|
||||||
|
EnsureRegistries();
|
||||||
|
ReloadWidgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureRegistries()
|
||||||
|
{
|
||||||
|
if (_componentRuntimeRegistry is not null) return;
|
||||||
|
|
||||||
|
var pluginRuntimeService = (Application.Current as App)?.PluginRuntimeService;
|
||||||
|
_componentRegistry = DesktopComponentRegistryFactory.Create(pluginRuntimeService);
|
||||||
|
_componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry(
|
||||||
|
_componentRegistry,
|
||||||
|
pluginRuntimeService,
|
||||||
|
_settingsFacade);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnterEditMode()
|
||||||
|
{
|
||||||
|
if (_isEditMode) return;
|
||||||
|
_isEditMode = true;
|
||||||
|
|
||||||
|
// 隐藏所有底层小窗口
|
||||||
|
foreach (var window in _widgetWindows.Values)
|
||||||
|
{
|
||||||
|
window.Hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExitEditMode()
|
||||||
|
{
|
||||||
|
if (!_isEditMode) return;
|
||||||
|
_isEditMode = false;
|
||||||
|
|
||||||
|
// 编辑完成,重新加载布局(可能已发生更改)并显示
|
||||||
|
ReloadWidgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReloadWidgets()
|
||||||
|
{
|
||||||
|
if (_isEditMode) return; // 编辑模式下不渲染小窗口
|
||||||
|
|
||||||
|
var layout = _layoutService.Load();
|
||||||
|
var existingIds = new HashSet<string>(_widgetWindows.Keys);
|
||||||
|
|
||||||
|
foreach (var placement in layout.ComponentPlacements)
|
||||||
|
{
|
||||||
|
existingIds.Remove(placement.PlacementId);
|
||||||
|
|
||||||
|
if (_widgetWindows.TryGetValue(placement.PlacementId, out var existingWindow))
|
||||||
|
{
|
||||||
|
// 已存在,可能只更新位置或尺寸
|
||||||
|
existingWindow.Position = new Avalonia.PixelPoint((int)placement.X, (int)placement.Y);
|
||||||
|
if (existingWindow.IsVisible == false)
|
||||||
|
{
|
||||||
|
existingWindow.Show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 新组件,生成窗口
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var window = CreateWidgetWindow(placement);
|
||||||
|
if (window != null)
|
||||||
|
{
|
||||||
|
_widgetWindows[placement.PlacementId] = window;
|
||||||
|
window.Show();
|
||||||
|
window.Position = new Avalonia.PixelPoint((int)placement.X, (int)placement.Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn("FusedDesktopMgr", $"Failed to render tiny window for {placement.ComponentId}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除被删除的组件
|
||||||
|
foreach (var id in existingIds)
|
||||||
|
{
|
||||||
|
if (_widgetWindows.Remove(id, out var windowToRemove))
|
||||||
|
{
|
||||||
|
windowToRemove.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DesktopWidgetWindow? CreateWidgetWindow(FusedDesktopComponentPlacementSnapshot placement)
|
||||||
|
{
|
||||||
|
EnsureRegistries();
|
||||||
|
if (_componentRuntimeRegistry is null || !_componentRuntimeRegistry.TryGetDescriptor(placement.ComponentId, out var descriptor))
|
||||||
|
{
|
||||||
|
AppLogger.Warn("FusedDesktopMgr", $"Unknown component: {placement.ComponentId}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var control = descriptor.CreateControl(
|
||||||
|
DefaultCellSize,
|
||||||
|
_timeZoneService,
|
||||||
|
_weatherDataService,
|
||||||
|
_recommendationInfoService,
|
||||||
|
_calculatorDataService,
|
||||||
|
_settingsFacade,
|
||||||
|
placement.PlacementId);
|
||||||
|
|
||||||
|
// 将组件包装到一个具有准确宽高的容器内(如果组件自身没有设置宽度)
|
||||||
|
control.Width = placement.Width;
|
||||||
|
control.Height = placement.Height;
|
||||||
|
|
||||||
|
var window = new DesktopWidgetWindow(control);
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 工厂
|
||||||
|
/// </summary>
|
||||||
|
public static class FusedDesktopManagerServiceFactory
|
||||||
|
{
|
||||||
|
private static IFusedDesktopManagerService? _instance;
|
||||||
|
private static readonly object _lock = new();
|
||||||
|
|
||||||
|
public static IFusedDesktopManagerService GetOrCreate()
|
||||||
|
{
|
||||||
|
if (_instance is not null) return _instance;
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var layoutService = FusedDesktopLayoutServiceProvider.GetOrCreate();
|
||||||
|
var settings = HostSettingsFacadeProvider.GetOrCreate();
|
||||||
|
_instance ??= new FusedDesktopManagerService(layoutService, settings);
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -317,11 +317,13 @@ public sealed record RecommendationApiOptions
|
|||||||
|
|
||||||
public string ClassIslandHubApiUrl { get; init; } = "https://api.github.com/repos/ClassIsland/classisland-hub/contents/images";
|
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 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 string RinLitHubRawUrlTemplate { get; init; } = "https://raw.githubusercontent.com/RinLit-233-shiroko/Rin-sHub/main/images/{0}";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3244,16 +3244,10 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
|||||||
|
|
||||||
private async Task<ZhiJiaoHubSnapshot> FetchZhiJiaoHubSnapshotAsync(string source, string mirrorSource, CancellationToken cancellationToken)
|
private async Task<ZhiJiaoHubSnapshot> FetchZhiJiaoHubSnapshotAsync(string source, string mirrorSource, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var (owner, repo, path) = source switch
|
var config = ZhiJiaoHubSourceConfig.GetConfig(source);
|
||||||
{
|
|
||||||
ZhiJiaoHubSources.Sectl => ("SECTL", "SECTL-hub", "docs/.vuepress/public/images"),
|
|
||||||
ZhiJiaoHubSources.RinLit => ("RinLit-233-shiroko", "Rin-sHub", "images"),
|
|
||||||
_ => ("ClassIsland", "classisland-hub", "images")
|
|
||||||
};
|
|
||||||
|
|
||||||
var contentsUrl = $"https://api.github.com/repos/{owner}/{repo}/contents/{path}";
|
var contentsUrl = config.ApiUrl;
|
||||||
|
|
||||||
// 如果使用镜像加速,代理 GitHub API 请求
|
|
||||||
if (string.Equals(mirrorSource, ZhiJiaoHubMirrorSources.GhProxy, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(mirrorSource, ZhiJiaoHubMirrorSources.GhProxy, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
contentsUrl = ZhiJiaoHubMirrorSources.GhProxyBaseUrl.TrimEnd('/') + "/" + contentsUrl;
|
contentsUrl = ZhiJiaoHubMirrorSources.GhProxyBaseUrl.TrimEnd('/') + "/" + contentsUrl;
|
||||||
@@ -3261,18 +3255,16 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var images = await FetchImagesFromContentsApi(owner, repo, path, contentsUrl, mirrorSource, cancellationToken);
|
var images = await FetchImagesFromContentsApi(config, contentsUrl, mirrorSource, cancellationToken);
|
||||||
|
|
||||||
if (images.Count == 0)
|
if (images.Count == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("未找到图片文件");
|
throw new InvalidOperationException($"在 {config.DisplayName} 中未找到图片文件");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 随机打乱图片顺序
|
|
||||||
var random = new Random();
|
var random = new Random();
|
||||||
var shuffled = images.OrderBy(_ => random.Next()).ToList();
|
var shuffled = images.OrderBy(_ => random.Next()).ToList();
|
||||||
|
|
||||||
// 重新设置索引
|
|
||||||
for (int i = 0; i < shuffled.Count; i++)
|
for (int i = 0; i < shuffled.Count; i++)
|
||||||
{
|
{
|
||||||
var item = shuffled[i];
|
var item = shuffled[i];
|
||||||
@@ -3287,11 +3279,15 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
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>();
|
var images = new List<ZhiJiaoHubImageItem>();
|
||||||
|
|
||||||
@@ -3309,7 +3305,17 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
|||||||
{
|
{
|
||||||
throw new HttpRequestException("GitHub API 速率限制,请稍后重试");
|
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);
|
var responseText = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
@@ -3321,9 +3327,9 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
|||||||
if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty("message", out var messageNode))
|
if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty("message", out var messageNode))
|
||||||
{
|
{
|
||||||
var errorMessage = messageNode.GetString();
|
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;
|
int index = 0;
|
||||||
@@ -3343,18 +3349,15 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只处理图片文件
|
|
||||||
var extension = Path.GetExtension(name).ToLowerInvariant();
|
var extension = Path.GetExtension(name).ToLowerInvariant();
|
||||||
if (extension != ".png" && extension != ".jpg" && extension != ".jpeg" && extension != ".gif" && extension != ".webp")
|
if (extension != ".png" && extension != ".jpg" && extension != ".jpeg" && extension != ".gif" && extension != ".webp")
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解码文件名
|
|
||||||
var decodedName = Uri.UnescapeDataString(name);
|
var decodedName = Uri.UnescapeDataString(name);
|
||||||
decodedName = Path.GetFileNameWithoutExtension(decodedName);
|
decodedName = Path.GetFileNameWithoutExtension(decodedName);
|
||||||
|
|
||||||
// 构造图片 URL
|
|
||||||
string imageUrl;
|
string imageUrl;
|
||||||
if (!string.IsNullOrWhiteSpace(downloadUrl))
|
if (!string.IsNullOrWhiteSpace(downloadUrl))
|
||||||
{
|
{
|
||||||
@@ -3362,10 +3365,12 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
|||||||
}
|
}
|
||||||
else
|
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);
|
imageUrl = ZhiJiaoHubMirrorSources.ApplyMirror(imageUrl, mirrorSource);
|
||||||
|
|
||||||
images.Add(new ZhiJiaoHubImageItem(decodedName, imageUrl, index));
|
images.Add(new ZhiJiaoHubImageItem(decodedName, imageUrl, index));
|
||||||
|
|||||||
@@ -41,8 +41,31 @@ public sealed record StatusBarSettingsState(
|
|||||||
string TaskbarLayoutMode,
|
string TaskbarLayoutMode,
|
||||||
string ClockDisplayFormat,
|
string ClockDisplayFormat,
|
||||||
bool ClockTransparentBackground,
|
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,
|
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(
|
public sealed record WeatherSettingsState(
|
||||||
string LocationMode,
|
string LocationMode,
|
||||||
string LocationKey,
|
string LocationKey,
|
||||||
@@ -274,6 +297,12 @@ public interface IStatusBarSettingsService
|
|||||||
void Save(StatusBarSettingsState state);
|
void Save(StatusBarSettingsState state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface ITextCapsuleSettingsService
|
||||||
|
{
|
||||||
|
TextCapsuleSettingsState Get();
|
||||||
|
void Save(TextCapsuleSettingsState state);
|
||||||
|
}
|
||||||
|
|
||||||
public interface IWeatherProvider
|
public interface IWeatherProvider
|
||||||
{
|
{
|
||||||
Task<WeatherQueryResult<IReadOnlyList<WeatherLocation>>> SearchLocationsAsync(
|
Task<WeatherQueryResult<IReadOnlyList<WeatherLocation>>> SearchLocationsAsync(
|
||||||
@@ -385,6 +414,7 @@ public interface ISettingsFacadeService
|
|||||||
IWallpaperMediaService WallpaperMedia { get; }
|
IWallpaperMediaService WallpaperMedia { get; }
|
||||||
IThemeAppearanceService Theme { get; }
|
IThemeAppearanceService Theme { get; }
|
||||||
IStatusBarSettingsService StatusBar { get; }
|
IStatusBarSettingsService StatusBar { get; }
|
||||||
|
ITextCapsuleSettingsService TextCapsule { get; }
|
||||||
IWeatherSettingsService Weather { get; }
|
IWeatherSettingsService Weather { get; }
|
||||||
IRegionSettingsService Region { get; }
|
IRegionSettingsService Region { get; }
|
||||||
IPrivacySettingsService Privacy { get; }
|
IPrivacySettingsService Privacy { get; }
|
||||||
|
|||||||
@@ -386,8 +386,24 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService
|
|||||||
snapshot.TaskbarLayoutMode,
|
snapshot.TaskbarLayoutMode,
|
||||||
snapshot.ClockDisplayFormat,
|
snapshot.ClockDisplayFormat,
|
||||||
snapshot.StatusBarClockTransparentBackground,
|
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.StatusBarSpacingMode,
|
||||||
snapshot.StatusBarCustomSpacingPercent);
|
snapshot.StatusBarCustomSpacingPercent,
|
||||||
|
snapshot.StatusBarShadowEnabled,
|
||||||
|
snapshot.StatusBarShadowColor,
|
||||||
|
snapshot.StatusBarShadowOpacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Save(StatusBarSettingsState state)
|
public void Save(StatusBarSettingsState state)
|
||||||
@@ -399,8 +415,24 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService
|
|||||||
snapshot.TaskbarLayoutMode = state.TaskbarLayoutMode;
|
snapshot.TaskbarLayoutMode = state.TaskbarLayoutMode;
|
||||||
snapshot.ClockDisplayFormat = state.ClockDisplayFormat;
|
snapshot.ClockDisplayFormat = state.ClockDisplayFormat;
|
||||||
snapshot.StatusBarClockTransparentBackground = state.ClockTransparentBackground;
|
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.StatusBarSpacingMode = state.SpacingMode;
|
||||||
snapshot.StatusBarCustomSpacingPercent = state.CustomSpacingPercent;
|
snapshot.StatusBarCustomSpacingPercent = state.CustomSpacingPercent;
|
||||||
|
snapshot.StatusBarShadowEnabled = state.ShadowEnabled;
|
||||||
|
snapshot.StatusBarShadowColor = state.ShadowColor;
|
||||||
|
snapshot.StatusBarShadowOpacity = state.ShadowOpacity;
|
||||||
_settingsService.SaveSnapshot(
|
_settingsService.SaveSnapshot(
|
||||||
SettingsScope.App,
|
SettingsScope.App,
|
||||||
snapshot,
|
snapshot,
|
||||||
@@ -412,8 +444,63 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService
|
|||||||
nameof(AppSettingsSnapshot.TaskbarLayoutMode),
|
nameof(AppSettingsSnapshot.TaskbarLayoutMode),
|
||||||
nameof(AppSettingsSnapshot.ClockDisplayFormat),
|
nameof(AppSettingsSnapshot.ClockDisplayFormat),
|
||||||
nameof(AppSettingsSnapshot.StatusBarClockTransparentBackground),
|
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.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();
|
WallpaperMedia = new WallpaperMediaService();
|
||||||
Theme = new ThemeAppearanceService(Settings);
|
Theme = new ThemeAppearanceService(Settings);
|
||||||
StatusBar = new StatusBarSettingsService(Settings);
|
StatusBar = new StatusBarSettingsService(Settings);
|
||||||
|
TextCapsule = new TextCapsuleSettingsService(Settings);
|
||||||
_weatherSettingsService = new WeatherSettingsService(Settings);
|
_weatherSettingsService = new WeatherSettingsService(Settings);
|
||||||
Weather = _weatherSettingsService;
|
Weather = _weatherSettingsService;
|
||||||
Region = new RegionSettingsService(Settings);
|
Region = new RegionSettingsService(Settings);
|
||||||
@@ -1227,6 +1315,8 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
|
|||||||
|
|
||||||
public IStatusBarSettingsService StatusBar { get; }
|
public IStatusBarSettingsService StatusBar { get; }
|
||||||
|
|
||||||
|
public ITextCapsuleSettingsService TextCapsule { get; }
|
||||||
|
|
||||||
public IWeatherSettingsService Weather { get; }
|
public IWeatherSettingsService Weather { get; }
|
||||||
|
|
||||||
public IRegionSettingsService Region { get; }
|
public IRegionSettingsService Region { get; }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Avalonia.Media;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using LanMountainDesktop.ComponentSystem;
|
using LanMountainDesktop.ComponentSystem;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
@@ -21,6 +22,12 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
|
|||||||
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
|
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
|
||||||
|
|
||||||
ClockFormats = CreateClockFormats();
|
ClockFormats = CreateClockFormats();
|
||||||
|
ClockPositions = CreateClockPositions();
|
||||||
|
ClockFontSizes = CreateFontSizes();
|
||||||
|
TextCapsulePositions = CreateTextCapsulePositions();
|
||||||
|
NetworkSpeedPositions = CreateNetworkSpeedPositions();
|
||||||
|
NetworkSpeedDisplayModes = CreateNetworkSpeedDisplayModes();
|
||||||
|
NetworkSpeedFontSizes = CreateFontSizes();
|
||||||
SpacingModes = CreateSpacingModes();
|
SpacingModes = CreateSpacingModes();
|
||||||
RefreshLocalizedText();
|
RefreshLocalizedText();
|
||||||
|
|
||||||
@@ -31,8 +38,20 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
|
|||||||
|
|
||||||
public IReadOnlyList<SelectionOption> ClockFormats { get; }
|
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> SpacingModes { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<SelectionOption> ClockFontSizes { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<SelectionOption> NetworkSpeedFontSizes { get; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _showClock = true;
|
private bool _showClock = true;
|
||||||
|
|
||||||
@@ -42,6 +61,9 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _clockTransparentBackground;
|
private bool _clockTransparentBackground;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private SelectionOption _selectedClockPosition = new("Left", "Left");
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private SelectionOption _selectedSpacingMode = new("Relaxed", "Relaxed");
|
private SelectionOption _selectedSpacingMode = new("Relaxed", "Relaxed");
|
||||||
|
|
||||||
@@ -75,6 +97,81 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _clockTransparentBackgroundDescription = string.Empty;
|
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]
|
[ObservableProperty]
|
||||||
private string _spacingHeader = string.Empty;
|
private string _spacingHeader = string.Empty;
|
||||||
|
|
||||||
@@ -84,6 +181,32 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _customSpacingLabel = string.Empty;
|
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()
|
public void Load()
|
||||||
{
|
{
|
||||||
var state = _settingsFacade.StatusBar.Get();
|
var state = _settingsFacade.StatusBar.Get();
|
||||||
@@ -99,12 +222,59 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
|
|||||||
?? ClockFormats[1];
|
?? ClockFormats[1];
|
||||||
ClockTransparentBackground = state.ClockTransparentBackground;
|
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);
|
var spacingMode = NormalizeSpacingMode(state.SpacingMode);
|
||||||
SelectedSpacingMode = SpacingModes.FirstOrDefault(option =>
|
SelectedSpacingMode = SpacingModes.FirstOrDefault(option =>
|
||||||
string.Equals(option.Value, spacingMode, StringComparison.OrdinalIgnoreCase))
|
string.Equals(option.Value, spacingMode, StringComparison.OrdinalIgnoreCase))
|
||||||
?? SpacingModes[1];
|
?? SpacingModes[1];
|
||||||
CustomSpacingPercent = Math.Clamp(state.CustomSpacingPercent, 0, 30);
|
CustomSpacingPercent = Math.Clamp(state.CustomSpacingPercent, 0, 30);
|
||||||
IsCustomSpacingVisible = string.Equals(SelectedSpacingMode.Value, "Custom", StringComparison.OrdinalIgnoreCase);
|
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)
|
partial void OnShowClockChanged(bool value)
|
||||||
@@ -137,6 +307,126 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
|
|||||||
Save();
|
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)
|
partial void OnSelectedSpacingModeChanged(SelectionOption value)
|
||||||
{
|
{
|
||||||
IsCustomSpacingVisible = string.Equals(value?.Value, "Custom", StringComparison.OrdinalIgnoreCase);
|
IsCustomSpacingVisible = string.Equals(value?.Value, "Custom", StringComparison.OrdinalIgnoreCase);
|
||||||
@@ -165,6 +455,37 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
|
|||||||
Save();
|
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()
|
private void Save()
|
||||||
{
|
{
|
||||||
var state = _settingsFacade.StatusBar.Get();
|
var state = _settingsFacade.StatusBar.Get();
|
||||||
@@ -184,8 +505,24 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
|
|||||||
state.TaskbarLayoutMode,
|
state.TaskbarLayoutMode,
|
||||||
SelectedClockFormat.Value,
|
SelectedClockFormat.Value,
|
||||||
ClockTransparentBackground,
|
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),
|
NormalizeSpacingMode(SelectedSpacingMode.Value),
|
||||||
Math.Clamp(CustomSpacingPercent, 0, 30)));
|
Math.Clamp(CustomSpacingPercent, 0, 30),
|
||||||
|
StatusBarShadowEnabled,
|
||||||
|
StatusBarShadowColor.ToString(),
|
||||||
|
StatusBarShadowOpacity / 100.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private IReadOnlyList<SelectionOption> CreateClockFormats()
|
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()
|
private IReadOnlyList<SelectionOption> CreateSpacingModes()
|
||||||
{
|
{
|
||||||
return
|
return
|
||||||
@@ -217,9 +594,28 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
|
|||||||
ClockFormatLabel = L("settings.status_bar.clock_format_label", "Clock format");
|
ClockFormatLabel = L("settings.status_bar.clock_format_label", "Clock format");
|
||||||
ClockTransparentBackgroundLabel = L("settings.status_bar.clock_transparent_background_label", "Transparent background");
|
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.");
|
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");
|
SpacingHeader = L("settings.status_bar.spacing_header", "Component Spacing");
|
||||||
SpacingDescription = L("settings.status_bar.spacing_desc", "Adjust spacing between status bar components.");
|
SpacingDescription = L("settings.status_bar.spacing_desc", "Adjust spacing between status bar components.");
|
||||||
CustomSpacingLabel = L("settings.status_bar.spacing_custom_label", "Custom spacing (%)");
|
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)
|
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)
|
private string L(string key, string fallback)
|
||||||
=> _localizationService.GetString(_languageCode, key, fallback);
|
=> _localizationService.GetString(_languageCode, key, fallback);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ public partial class ComponentLibraryWindow : Window
|
|||||||
|
|
||||||
if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return Symbol.Apps;
|
return Symbol.Info;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase))
|
||||||
|
|||||||
@@ -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:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
@@ -25,6 +25,7 @@ public partial class ClockWidget : UserControl, IDesktopComponentWidget, ITimeZo
|
|||||||
private ClockDisplayFormat _displayFormat = ClockDisplayFormat.HourMinuteSecond;
|
private ClockDisplayFormat _displayFormat = ClockDisplayFormat.HourMinuteSecond;
|
||||||
private bool _transparentBackground;
|
private bool _transparentBackground;
|
||||||
private double _lastAppliedCellSize = 100;
|
private double _lastAppliedCellSize = 100;
|
||||||
|
private string _fontSize = "Medium"; // Small, Medium, Large
|
||||||
|
|
||||||
public ClockWidget()
|
public ClockWidget()
|
||||||
{
|
{
|
||||||
@@ -72,6 +73,21 @@ public partial class ClockWidget : UserControl, IDesktopComponentWidget, ITimeZo
|
|||||||
TransparentBackground = transparentBackground;
|
TransparentBackground = transparentBackground;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string WidgetFontSize
|
||||||
|
{
|
||||||
|
get => _fontSize;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_fontSize = value;
|
||||||
|
ApplyCellSize(_lastAppliedCellSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetFontSize(string fontSize)
|
||||||
|
{
|
||||||
|
WidgetFontSize = fontSize;
|
||||||
|
}
|
||||||
|
|
||||||
public void SetTimeZoneService(TimeZoneService timeZoneService)
|
public void SetTimeZoneService(TimeZoneService timeZoneService)
|
||||||
{
|
{
|
||||||
ClearTimeZoneService();
|
ClearTimeZoneService();
|
||||||
@@ -138,7 +154,14 @@ public partial class ClockWidget : UserControl, IDesktopComponentWidget, ITimeZo
|
|||||||
|
|
||||||
// 3. 核心:满盈字阶 (Filled Typography)
|
// 3. 核心:满盈字阶 (Filled Typography)
|
||||||
// 使主时间文字占据容器高度的 ~68%,产生饱满的视觉张力
|
// 使主时间文字占据容器高度的 ~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.FontSize = mainFontSize;
|
||||||
MainTimeTextBlock.FontWeight = FontWeight.SemiBold;
|
MainTimeTextBlock.FontWeight = FontWeight.SemiBold;
|
||||||
|
|
||||||
|
|||||||
72
LanMountainDesktop/Views/Components/NetworkSpeedWidget.axaml
Normal file
72
LanMountainDesktop/Views/Components/NetworkSpeedWidget.axaml
Normal 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>
|
||||||
451
LanMountainDesktop/Views/Components/NetworkSpeedWidget.axaml.cs
Normal file
451
LanMountainDesktop/Views/Components/NetworkSpeedWidget.axaml.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
22
LanMountainDesktop/Views/Components/TextCapsuleWidget.axaml
Normal file
22
LanMountainDesktop/Views/Components/TextCapsuleWidget.axaml
Normal 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>
|
||||||
167
LanMountainDesktop/Views/Components/TextCapsuleWidget.axaml.cs
Normal file
167
LanMountainDesktop/Views/Components/TextCapsuleWidget.axaml.cs
Normal 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;
|
||||||
|
}
|
||||||
23
LanMountainDesktop/Views/DesktopWidgetWindow.axaml
Normal file
23
LanMountainDesktop/Views/DesktopWidgetWindow.axaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
x:Class="LanMountainDesktop.Views.DesktopWidgetWindow"
|
||||||
|
Title="Desktop Component"
|
||||||
|
ShowInTaskbar="False"
|
||||||
|
SystemDecorations="None"
|
||||||
|
Background="Transparent"
|
||||||
|
Topmost="False"
|
||||||
|
SizeToContent="WidthAndHeight"
|
||||||
|
TransparencyLevelHint="Transparent"
|
||||||
|
RenderOptions.BitmapInterpolationMode="HighQuality"
|
||||||
|
CanResize="False">
|
||||||
|
|
||||||
|
<Border x:Name="ComponentContainer"
|
||||||
|
Background="Transparent"
|
||||||
|
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
|
||||||
|
ClipToBounds="True">
|
||||||
|
<!-- Component control will be injected here -->
|
||||||
|
</Border>
|
||||||
|
</Window>
|
||||||
61
LanMountainDesktop/Views/DesktopWidgetWindow.axaml.cs
Normal file
61
LanMountainDesktop/Views/DesktopWidgetWindow.axaml.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using LanMountainDesktop.Services;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Views;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示一个独立的组件挂载窗口。它不含有任何自己的边窗,仅仅负责包裹组件并将自身植入系统最底层。
|
||||||
|
/// </summary>
|
||||||
|
public partial class DesktopWidgetWindow : Window
|
||||||
|
{
|
||||||
|
private readonly IWindowBottomMostService _bottomMostService = WindowBottomMostServiceFactory.GetOrCreate();
|
||||||
|
private readonly IRegionPassthroughService _regionPassthroughService = RegionPassthroughServiceFactory.GetOrCreate();
|
||||||
|
|
||||||
|
public DesktopWidgetWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DesktopWidgetWindow(Control componentContent) : this()
|
||||||
|
{
|
||||||
|
ComponentContainer.Child = componentContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnOpened(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnOpened(e);
|
||||||
|
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
// 通过现有的置底服务将独立的小窗口锁定到底层
|
||||||
|
_bottomMostService.SetupBottomMost(this);
|
||||||
|
_bottomMostService.SendToBottom(this);
|
||||||
|
|
||||||
|
// 当窗口展示完毕且有了尺寸后,更新可交互区域,使得整个组件都能被点击
|
||||||
|
Dispatcher.UIThread.Post(UpdateInteractiveRegion, DispatcherPriority.Render);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnSizeChanged(SizeChangedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnSizeChanged(e);
|
||||||
|
|
||||||
|
if (OperatingSystem.IsWindows() && IsVisible)
|
||||||
|
{
|
||||||
|
UpdateInteractiveRegion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateInteractiveRegion()
|
||||||
|
{
|
||||||
|
// 既然是一个完全紧贴在组件身上的小窗,它的全部都是可交互的
|
||||||
|
_regionPassthroughService.SetInteractiveRegions(this, new List<Rect>
|
||||||
|
{
|
||||||
|
new(0, 0, Bounds.Width, Bounds.Height)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||||
xmlns:fi="using:FluentIcons.Avalonia"
|
xmlns:fi="using:FluentIcons.Avalonia"
|
||||||
xmlns:local="using:LanMountainDesktop.Views"
|
x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryControl"
|
||||||
x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryControl">
|
x:DataType="vm:ComponentLibraryWindowViewModel">
|
||||||
|
|
||||||
<Grid ColumnDefinitions="220,*">
|
<Grid ColumnDefinitions="240,*"
|
||||||
|
ColumnSpacing="12"
|
||||||
|
Margin="0">
|
||||||
<!-- 分类列表 (左侧) -->
|
<!-- 分类列表 (左侧) -->
|
||||||
<Border Grid.Column="0"
|
<Border Classes="surface-translucent-panel"
|
||||||
BorderBrush="{DynamicResource AdaptiveBorderBrush}"
|
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
|
||||||
BorderThickness="0,0,1,0"
|
Padding="10">
|
||||||
Padding="12,0,12,12">
|
|
||||||
<Grid RowDefinitions="Auto,*">
|
<Grid RowDefinitions="Auto,*">
|
||||||
<TextBox x:Name="SearchBox"
|
<TextBox x:Name="SearchBox"
|
||||||
Watermark="搜索组件..."
|
Watermark="搜索组件..."
|
||||||
@@ -27,20 +29,26 @@
|
|||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
SelectionChanged="OnCategorySelectionChanged">
|
SelectionChanged="OnCategorySelectionChanged"
|
||||||
<ListBox.Styles>
|
ItemsSource="{Binding Categories}">
|
||||||
<Style Selector="ListBoxItem">
|
|
||||||
<Setter Property="CornerRadius" Value="14" />
|
|
||||||
<Setter Property="Margin" Value="0,2" />
|
|
||||||
<Setter Property="Padding" Value="12,10" />
|
|
||||||
</Style>
|
|
||||||
</ListBox.Styles>
|
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate x:DataType="local:LibraryCategoryItem">
|
<DataTemplate x:DataType="vm:ComponentLibraryCategoryViewModel">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
<Border Padding="10"
|
||||||
<fi:SymbolIcon Symbol="{Binding Icon}" FontSize="18" />
|
Margin="0,0,0,6"
|
||||||
<TextBlock Text="{Binding DisplayName}" VerticalAlignment="Center" />
|
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
|
||||||
</StackPanel>
|
Background="{DynamicResource AdaptiveNavItemBackgroundBrush}">
|
||||||
|
<Grid ColumnDefinitions="Auto,*"
|
||||||
|
ColumnSpacing="8">
|
||||||
|
<fi:SymbolIcon Symbol="{Binding Icon}"
|
||||||
|
IconVariant="Regular"
|
||||||
|
FontSize="16" />
|
||||||
|
<TextBlock Grid.Column="1"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||||
|
Text="{Binding Title}" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
@@ -48,66 +56,105 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- 组件网格 (右侧) -->
|
<!-- 组件网格 (右侧) -->
|
||||||
<ScrollViewer Grid.Column="1" Padding="20">
|
<Border Grid.Column="1"
|
||||||
<ItemsControl x:Name="ComponentItemsControl">
|
Classes="surface-translucent-strong"
|
||||||
|
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
|
||||||
|
Padding="10">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||||
|
HorizontalScrollBarVisibility="Disabled">
|
||||||
|
<ItemsControl x:Name="ComponentItemsControl"
|
||||||
|
ItemsSource="{Binding Components}">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
<WrapPanel Orientation="Horizontal" />
|
<WrapPanel Orientation="Horizontal" />
|
||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="local:LibraryComponentItem">
|
|
||||||
<Button Classes="unstyled-card"
|
|
||||||
Width="260"
|
|
||||||
Margin="0,0,16,16"
|
|
||||||
Padding="0"
|
|
||||||
Background="Transparent"
|
|
||||||
BorderThickness="0"
|
|
||||||
Click="OnAddComponentClick"
|
|
||||||
Tag="{Binding Id}">
|
|
||||||
<Border Classes="card"
|
|
||||||
Background="{DynamicResource AdaptiveSurfaceLowBrush}"
|
|
||||||
BorderBrush="{DynamicResource AdaptiveBorderBrush}"
|
|
||||||
BorderThickness="1"
|
|
||||||
CornerRadius="24"
|
|
||||||
ClipToBounds="True">
|
|
||||||
<Grid RowDefinitions="Auto,Auto">
|
|
||||||
<!-- 预览区域 (动态填充预览) -->
|
|
||||||
<Border x:Name="PreviewHost"
|
|
||||||
Height="150"
|
|
||||||
Background="{DynamicResource AdaptiveSurfaceNeutralBrush}"
|
|
||||||
Margin="8"
|
|
||||||
CornerRadius="16"
|
|
||||||
ClipToBounds="True">
|
|
||||||
<Panel>
|
|
||||||
<!-- 这里将显示组件的缩放预览 -->
|
|
||||||
<ContentPresenter Content="{Binding PreviewContent}" />
|
|
||||||
|
|
||||||
<!-- 空状态或加载中图标 -->
|
<ItemsControl.ItemTemplate>
|
||||||
<fi:SymbolIcon Symbol="Cube"
|
<DataTemplate x:DataType="vm:ComponentLibraryItemViewModel">
|
||||||
FontSize="32"
|
<Border Width="240"
|
||||||
Opacity="0.1"
|
Height="220"
|
||||||
IsVisible="{Binding !HasPreview}" />
|
Margin="6"
|
||||||
</Panel>
|
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>
|
||||||
|
|
||||||
<!-- 文字说明 -->
|
<!-- 失败状态 -->
|
||||||
<StackPanel Grid.Row="1" Margin="16,8,16,16" Spacing="4">
|
<Border IsVisible="{Binding IsPreviewFailed}"
|
||||||
<TextBlock Text="{Binding DisplayName}"
|
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
|
||||||
|
<StackPanel HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Spacing="8">
|
||||||
|
<TextBlock HorizontalAlignment="Center"
|
||||||
|
TextAlignment="Center"
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
FontSize="15" />
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||||
<TextBlock Text="{Binding Description}"
|
Text="{Binding PreviewStatusText}" />
|
||||||
Opacity="0.6"
|
<TextBlock HorizontalAlignment="Center"
|
||||||
|
TextAlignment="Center"
|
||||||
FontSize="12"
|
FontSize="12"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
MaxLines="2" />
|
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||||
|
Text="{Binding PreviewErrorMessage}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</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>
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Layout;
|
using FluentIcons.Common;
|
||||||
using Avalonia.Media;
|
|
||||||
using LanMountainDesktop.ComponentSystem;
|
using LanMountainDesktop.ComponentSystem;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
using LanMountainDesktop.Services.Settings;
|
using LanMountainDesktop.Services.Settings;
|
||||||
|
using LanMountainDesktop.ViewModels;
|
||||||
using LanMountainDesktop.Views.Components;
|
using LanMountainDesktop.Views.Components;
|
||||||
using LanMountainDesktop.Models;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Views;
|
namespace LanMountainDesktop.Views;
|
||||||
|
|
||||||
@@ -20,8 +18,7 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
{
|
{
|
||||||
public event EventHandler<string>? AddComponentRequested;
|
public event EventHandler<string>? AddComponentRequested;
|
||||||
|
|
||||||
private readonly ObservableCollection<LibraryCategoryItem> _categories = new();
|
private readonly ComponentLibraryWindowViewModel _viewModel = new();
|
||||||
private readonly ObservableCollection<LibraryComponentItem> _components = new();
|
|
||||||
private List<DesktopComponentDefinition> _allDefinitions = new();
|
private List<DesktopComponentDefinition> _allDefinitions = new();
|
||||||
|
|
||||||
private ComponentRegistry? _componentRegistry;
|
private ComponentRegistry? _componentRegistry;
|
||||||
@@ -35,18 +32,17 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
public FusedDesktopComponentLibraryControl()
|
public FusedDesktopComponentLibraryControl()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
DataContext = _viewModel;
|
||||||
|
|
||||||
_weatherDataService = _settingsFacade.Weather.GetWeatherInfoService();
|
_weatherDataService = _settingsFacade.Weather.GetWeatherInfoService();
|
||||||
_timeZoneService = _settingsFacade.Region.GetTimeZoneService();
|
_timeZoneService = _settingsFacade.Region.GetTimeZoneService();
|
||||||
|
|
||||||
CategoryListBox.ItemsSource = _categories;
|
|
||||||
ComponentItemsControl.ItemsSource = _components;
|
|
||||||
|
|
||||||
LoadRegistry();
|
LoadRegistry();
|
||||||
LoadCategories();
|
LoadCategories();
|
||||||
SearchBox.KeyUp += (s, e) => FilterComponents();
|
SearchBox.KeyUp += (s, e) => FilterComponents();
|
||||||
|
|
||||||
// 默认选择第一个分类
|
// 默认选择第一个分类
|
||||||
if (_categories.Count > 0)
|
if (_viewModel.Categories.Count > 0)
|
||||||
{
|
{
|
||||||
CategoryListBox.SelectedIndex = 0;
|
CategoryListBox.SelectedIndex = 0;
|
||||||
}
|
}
|
||||||
@@ -68,18 +64,27 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
|
|
||||||
private void LoadCategories()
|
private void LoadCategories()
|
||||||
{
|
{
|
||||||
_categories.Clear();
|
_viewModel.Categories.Clear();
|
||||||
_categories.Add(new LibraryCategoryItem("all", "全部组件", "Apps"));
|
_viewModel.Components.Clear();
|
||||||
|
|
||||||
var categoryMap = new Dictionary<string, (string Display, string Icon)>
|
// 添加"全部组件"分类
|
||||||
|
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
|
||||||
|
"all",
|
||||||
|
"全部组件",
|
||||||
|
Symbol.Apps,
|
||||||
|
Array.Empty<ComponentLibraryItemViewModel>()));
|
||||||
|
|
||||||
|
var categoryMap = new Dictionary<string, (string Display, Symbol Icon)>
|
||||||
{
|
{
|
||||||
{ "clock", ("时钟", "Clock") },
|
{ "clock", ("时钟", Symbol.Clock) },
|
||||||
{ "date", ("日历", "Calendar") },
|
{ "date", ("日历", Symbol.CalendarDate) },
|
||||||
{ "weather", ("天气", "WeatherCloudy") },
|
{ "weather", ("天气", Symbol.WeatherSunny) },
|
||||||
{ "info", ("资讯", "News") },
|
{ "board", ("画板", Symbol.Edit) },
|
||||||
{ "calculator", ("工具", "Calculator") },
|
{ "media", ("媒体", Symbol.Play) },
|
||||||
{ "study", ("学习", "Book") },
|
{ "info", ("资讯", Symbol.News) },
|
||||||
{ "file", ("文件", "Document") }
|
{ "calculator", ("工具", Symbol.Calculator) },
|
||||||
|
{ "study", ("学习", Symbol.Hourglass) },
|
||||||
|
{ "file", ("文件", Symbol.Folder) }
|
||||||
};
|
};
|
||||||
|
|
||||||
var usedCategories = _allDefinitions
|
var usedCategories = _allDefinitions
|
||||||
@@ -91,11 +96,62 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
{
|
{
|
||||||
if (categoryMap.TryGetValue(cat.ToLower(), out var info))
|
if (categoryMap.TryGetValue(cat.ToLower(), out var info))
|
||||||
{
|
{
|
||||||
_categories.Add(new LibraryCategoryItem(cat, info.Display, info.Icon));
|
var categoryComponents = _allDefinitions
|
||||||
|
.Where(d => string.Equals(d.Category, cat, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.OrderBy(d => d.DisplayName)
|
||||||
|
.Select(d => CreateComponentItem(d))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
|
||||||
|
cat,
|
||||||
|
info.Display,
|
||||||
|
info.Icon,
|
||||||
|
categoryComponents));
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ComponentLibraryItemViewModel CreateComponentItem(DesktopComponentDefinition definition)
|
||||||
{
|
{
|
||||||
_categories.Add(new LibraryCategoryItem(cat, cat, "Cube"));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,7 +163,7 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
|
|
||||||
private void FilterComponents()
|
private void FilterComponents()
|
||||||
{
|
{
|
||||||
var selectedCategory = (CategoryListBox.SelectedItem as LibraryCategoryItem)?.Id;
|
var selectedCategory = (CategoryListBox.SelectedItem as ComponentLibraryCategoryViewModel)?.Id;
|
||||||
var searchText = SearchBox.Text?.ToLower() ?? "";
|
var searchText = SearchBox.Text?.ToLower() ?? "";
|
||||||
|
|
||||||
var filtered = _allDefinitions.Where(d =>
|
var filtered = _allDefinitions.Where(d =>
|
||||||
@@ -117,57 +173,10 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
return matchesCategory && matchesSearch;
|
return matchesCategory && matchesSearch;
|
||||||
});
|
});
|
||||||
|
|
||||||
_components.Clear();
|
_viewModel.Components.Clear();
|
||||||
foreach (var def in filtered)
|
foreach (var def in filtered)
|
||||||
{
|
{
|
||||||
_components.Add(new LibraryComponentItem
|
_viewModel.Components.Add(CreateComponentItem(def));
|
||||||
{
|
|
||||||
Id = def.Id,
|
|
||||||
DisplayName = def.DisplayName,
|
|
||||||
Description = GetDescription(def.Id),
|
|
||||||
PreviewContent = CreatePreview(def.Id)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetDescription(string id)
|
|
||||||
{
|
|
||||||
// 简单映射描述信息
|
|
||||||
return id.Contains("clock") ? "实时显示当前时间与日期。" :
|
|
||||||
id.Contains("weather") ? "为您提供精准的天气预报。" :
|
|
||||||
"多功能桌面组件,提升您的操作效率。";
|
|
||||||
}
|
|
||||||
|
|
||||||
private Control? CreatePreview(string id)
|
|
||||||
{
|
|
||||||
if (_componentRuntimeRegistry == null || !_componentRuntimeRegistry.TryGetDescriptor(id, out var descriptor))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var control = descriptor.CreateControl(
|
|
||||||
100, // Previews assume 100px base
|
|
||||||
_timeZoneService,
|
|
||||||
_weatherDataService,
|
|
||||||
_recommendationInfoService,
|
|
||||||
_calculatorDataService,
|
|
||||||
_settingsFacade,
|
|
||||||
"preview_" + id);
|
|
||||||
|
|
||||||
control.IsHitTestVisible = false;
|
|
||||||
|
|
||||||
return new Viewbox
|
|
||||||
{
|
|
||||||
Child = control,
|
|
||||||
Stretch = Stretch.Uniform,
|
|
||||||
Margin = new Thickness(12)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return new TextBlock { Text = "无法预览", VerticalAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,15 +188,3 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record LibraryCategoryItem(string Id, string DisplayName, string Icon);
|
|
||||||
|
|
||||||
public class LibraryComponentItem
|
|
||||||
{
|
|
||||||
public string Id { get; set; } = "";
|
|
||||||
public string DisplayName { get; set; } = "";
|
|
||||||
public string Description { get; set; } = "";
|
|
||||||
public Control? PreviewContent { get; set; }
|
|
||||||
public bool HasPreview => PreviewContent != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using LanMountainDesktop.ComponentSystem;
|
using LanMountainDesktop.ComponentSystem;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
using LanMountainDesktop.Services.Settings;
|
using LanMountainDesktop.Services.Settings;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Views;
|
namespace LanMountainDesktop.Views;
|
||||||
|
|
||||||
@@ -26,6 +28,9 @@ public partial class FusedDesktopComponentLibraryWindow : Window
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
LibraryControl.AddComponentRequested += OnAddComponentRequested;
|
LibraryControl.AddComponentRequested += OnAddComponentRequested;
|
||||||
|
|
||||||
|
var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow;
|
||||||
|
mainWindow?.RegisterFusedLibraryWindow(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -97,4 +102,16 @@ public partial class FusedDesktopComponentLibraryWindow : Window
|
|||||||
{
|
{
|
||||||
Close();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public partial class MainWindow
|
|||||||
private readonly IComponentPreviewImageService _componentPreviewImageService = new ComponentPreviewImageService();
|
private readonly IComponentPreviewImageService _componentPreviewImageService = new ComponentPreviewImageService();
|
||||||
private readonly Dictionary<ComponentPreviewKey, List<ComponentLibraryPreviewVisualTarget>> _componentLibraryPreviewVisualTargets = new(ComponentPreviewKeyComparer.Instance);
|
private readonly Dictionary<ComponentPreviewKey, List<ComponentLibraryPreviewVisualTarget>> _componentLibraryPreviewVisualTargets = new(ComponentPreviewKeyComparer.Instance);
|
||||||
private bool _componentLibraryPreviewWarmupStarted;
|
private bool _componentLibraryPreviewWarmupStarted;
|
||||||
|
private FusedDesktopComponentLibraryWindow? _fusedLibraryWindow;
|
||||||
|
|
||||||
private sealed record ComponentLibraryPreviewVisualTarget(Image Image, Control Fallback);
|
private sealed record ComponentLibraryPreviewVisualTarget(Image Image, Control Fallback);
|
||||||
|
|
||||||
@@ -519,6 +520,7 @@ public partial class MainWindow
|
|||||||
{
|
{
|
||||||
ApplyPreviewEntryToEmbeddedVisuals(entry.Key);
|
ApplyPreviewEntryToEmbeddedVisuals(entry.Key);
|
||||||
_detachedComponentLibraryWindow?.UpdatePreviewImage(entry);
|
_detachedComponentLibraryWindow?.UpdatePreviewImage(entry);
|
||||||
|
_fusedLibraryWindow?.UpdatePreviewImage(entry);
|
||||||
|
|
||||||
if (entry.Key.Kind == ComponentPreviewKeyKind.PlacementInstance)
|
if (entry.Key.Kind == ComponentPreviewKeyKind.PlacementInstance)
|
||||||
{
|
{
|
||||||
@@ -597,4 +599,30 @@ public partial class MainWindow
|
|||||||
action: "DetachedLibraryRender",
|
action: "DetachedLibraryRender",
|
||||||
forceRefresh: false);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -364,12 +364,407 @@ public partial class MainWindow
|
|||||||
? ClockDisplayFormat.HourMinute
|
? ClockDisplayFormat.HourMinute
|
||||||
: ClockDisplayFormat.HourMinuteSecond;
|
: ClockDisplayFormat.HourMinuteSecond;
|
||||||
_statusBarClockTransparentBackground = snapshot.StatusBarClockTransparentBackground;
|
_statusBarClockTransparentBackground = snapshot.StatusBarClockTransparentBackground;
|
||||||
|
_clockPosition = NormalizeClockPosition(snapshot.ClockPosition);
|
||||||
|
_clockFontSize = NormalizeFontSize(snapshot.ClockFontSize);
|
||||||
|
|
||||||
if (ClockWidget is not null)
|
_showTextCapsule = snapshot.ShowTextCapsule;
|
||||||
{
|
_textCapsuleContent = snapshot.TextCapsuleContent ?? "**Hello** World!";
|
||||||
ClockWidget.SetDisplayFormat(_clockDisplayFormat);
|
_textCapsulePosition = NormalizeTextCapsulePosition(snapshot.TextCapsulePosition);
|
||||||
ClockWidget.SetTransparentBackground(_statusBarClockTransparentBackground);
|
_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)
|
||||||
|
{
|
||||||
|
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()
|
private void ApplyTopStatusComponentVisibility()
|
||||||
@@ -377,23 +772,413 @@ public partial class MainWindow
|
|||||||
var showClock = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock);
|
var showClock = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock);
|
||||||
var hasVisibleTopStatusComponent = false;
|
var hasVisibleTopStatusComponent = false;
|
||||||
|
|
||||||
if (ClockWidget is not null)
|
// 先隐藏所有时钟控件
|
||||||
{
|
if (ClockWidgetLeft is not null)
|
||||||
ClockWidget.IsVisible = showClock;
|
ClockWidgetLeft.IsVisible = false;
|
||||||
ClockWidget.SetTransparentBackground(_statusBarClockTransparentBackground);
|
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)
|
if (showClock)
|
||||||
{
|
{
|
||||||
ClockWidget.SetDisplayFormat(_clockDisplayFormat);
|
var targetPosition = _clockPosition;
|
||||||
var columnSpan = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? 2 : 3;
|
var canAdd = CanAddComponentAtPosition(targetPosition);
|
||||||
Grid.SetColumnSpan(ClockWidget, columnSpan);
|
|
||||||
|
if (canAdd)
|
||||||
|
{
|
||||||
|
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;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (TopStatusBarHost is not null)
|
if (TopStatusBarHost is not null)
|
||||||
{
|
{
|
||||||
TopStatusBarHost.IsVisible = hasVisibleTopStatusComponent;
|
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()
|
private TaskbarContext GetCurrentTaskbarContext()
|
||||||
|
|||||||
@@ -269,12 +269,6 @@ public partial class MainWindow
|
|||||||
LauncherPagePanel.MaxWidth = pageWidth - launcherMargin * 2;
|
LauncherPagePanel.MaxWidth = pageWidth - launcherMargin * 2;
|
||||||
LauncherPagePanel.MaxHeight = pageHeight - launcherMargin * 2;
|
LauncherPagePanel.MaxHeight = pageHeight - launcherMargin * 2;
|
||||||
|
|
||||||
if (LauncherFolderPanel is not null)
|
|
||||||
{
|
|
||||||
LauncherFolderPanel.MaxWidth = Math.Max(320, pageWidth - 96);
|
|
||||||
LauncherFolderPanel.MaxHeight = Math.Max(220, pageHeight - 96);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新启动台图标布局
|
// 更新启动台图标布局
|
||||||
UpdateLauncherTileLayout();
|
UpdateLauncherTileLayout();
|
||||||
|
|
||||||
@@ -331,19 +325,6 @@ public partial class MainWindow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同样更新文件夹视图的图标尺寸
|
|
||||||
if (LauncherFolderTilePanel is not null)
|
|
||||||
{
|
|
||||||
LauncherFolderTilePanel.Width = availableWidth;
|
|
||||||
foreach (var child in LauncherFolderTilePanel.Children)
|
|
||||||
{
|
|
||||||
if (child is Button button)
|
|
||||||
{
|
|
||||||
button.Width = tileWidth;
|
|
||||||
button.Height = tileHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClampSurfaceIndex()
|
private void ClampSurfaceIndex()
|
||||||
@@ -630,8 +611,12 @@ public partial class MainWindow
|
|||||||
|
|
||||||
foreach (var node in button.GetSelfAndVisualAncestors())
|
foreach (var node in button.GetSelfAndVisualAncestors())
|
||||||
{
|
{
|
||||||
if (node is WrapPanel panel &&
|
if (node is WrapPanel panel && panel.Name == "LauncherRootTilePanel")
|
||||||
(panel.Name == "LauncherRootTilePanel" || panel.Name == "LauncherFolderTilePanel"))
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node is Grid grid && grid.Name == "LauncherFolderGridPanel")
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -719,8 +704,7 @@ public partial class MainWindow
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return scrollViewer.Name == "LauncherRootScrollViewer" ||
|
return scrollViewer.Name == "LauncherRootScrollViewer";
|
||||||
scrollViewer.Name == "LauncherFolderScrollViewer";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryGetPointerPositionInDesktopViewport(PointerEventArgs e, out Point point)
|
private bool TryGetPointerPositionInDesktopViewport(PointerEventArgs e, out Point point)
|
||||||
@@ -1561,18 +1545,17 @@ public partial class MainWindow
|
|||||||
LauncherFolderOverlay.IsVisible = false;
|
LauncherFolderOverlay.IsVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (LauncherFolderTilePanel is not null)
|
if (LauncherFolderGridPanel is not null)
|
||||||
{
|
{
|
||||||
LauncherFolderTilePanel.Children.Clear();
|
LauncherFolderGridPanel.Children.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RenderLauncherFolderFromStack()
|
private void RenderLauncherFolderFromStack()
|
||||||
{
|
{
|
||||||
if (LauncherFolderOverlay is null ||
|
if (LauncherFolderOverlay is null ||
|
||||||
LauncherFolderTilePanel is null ||
|
LauncherFolderGridPanel is null ||
|
||||||
LauncherFolderTitleTextBlock is null ||
|
LauncherFolderTitleTextBlock is null)
|
||||||
LauncherFolderBackButton is null)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1587,38 +1570,230 @@ public partial class MainWindow
|
|||||||
var folder = _launcherFolderStack.Peek();
|
var folder = _launcherFolderStack.Peek();
|
||||||
LauncherFolderOverlay.IsVisible = true;
|
LauncherFolderOverlay.IsVisible = true;
|
||||||
LauncherFolderTitleTextBlock.Text = folder.Name;
|
LauncherFolderTitleTextBlock.Text = folder.Name;
|
||||||
LauncherFolderBackButton.IsVisible = _launcherFolderStack.Count > 1;
|
|
||||||
|
|
||||||
LauncherFolderTilePanel.Children.Clear();
|
LauncherFolderGridPanel.Children.Clear();
|
||||||
foreach (var subFolder in folder.Folders)
|
|
||||||
|
const int maxCols = 4;
|
||||||
|
const int maxRows = 3;
|
||||||
|
const int maxItems = maxCols * maxRows;
|
||||||
|
|
||||||
|
var visibleFolders = folder.Folders.Where(IsLauncherFolderVisible).ToList();
|
||||||
|
var visibleApps = folder.Apps.Where(IsLauncherAppVisible).ToList();
|
||||||
|
|
||||||
|
if (visibleFolders.Count == 0 && visibleApps.Count == 0)
|
||||||
{
|
{
|
||||||
if (!IsLauncherFolderVisible(subFolder))
|
LauncherFolderGridPanel.Children.Add(CreateLauncherFolderGridHintCell(
|
||||||
|
L("launcher.empty_folder", "This folder is empty.")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var allItems = new List<(StartMenuFolderNode? Folder, StartMenuAppEntry? App)>();
|
||||||
|
foreach (var f in visibleFolders)
|
||||||
|
{
|
||||||
|
allItems.Add((f, null));
|
||||||
|
}
|
||||||
|
foreach (var a in visibleApps)
|
||||||
|
{
|
||||||
|
allItems.Add((null, a));
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayCount = Math.Min(allItems.Count, maxItems);
|
||||||
|
for (var i = 0; i < displayCount; i++)
|
||||||
|
{
|
||||||
|
var col = i % maxCols;
|
||||||
|
var row = i / maxCols;
|
||||||
|
var (itemFolder, itemApp) = allItems[i];
|
||||||
|
|
||||||
|
Control cell;
|
||||||
|
if (itemFolder is not null)
|
||||||
|
{
|
||||||
|
var capturedFolder = itemFolder;
|
||||||
|
cell = CreateLauncherFolderGridTile(itemFolder.Name, GetLauncherFolderIconBitmap(), () => OpenLauncherFolder(capturedFolder));
|
||||||
|
}
|
||||||
|
else if (itemApp is not null)
|
||||||
|
{
|
||||||
|
var capturedApp = itemApp;
|
||||||
|
cell = CreateLauncherFolderGridTile(capturedApp, () => LaunchStartMenuEntry(capturedApp));
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
LauncherFolderTilePanel.Children.Add(CreateLauncherFolderTile(subFolder));
|
Grid.SetColumn(cell, col);
|
||||||
|
Grid.SetRow(cell, row);
|
||||||
|
LauncherFolderGridPanel.Children.Add(cell);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var app in folder.Apps)
|
private Button CreateLauncherFolderGridTile(StartMenuAppEntry app, Action clickAction)
|
||||||
{
|
{
|
||||||
if (!IsLauncherAppVisible(app))
|
var iconBitmap = GetLauncherIconBitmap(app);
|
||||||
|
var monogram = BuildMonogram(app.DisplayName);
|
||||||
|
|
||||||
|
Control iconControl = iconBitmap is not null
|
||||||
|
? new Image
|
||||||
{
|
{
|
||||||
continue;
|
Source = iconBitmap,
|
||||||
|
Width = 32,
|
||||||
|
Height = 32,
|
||||||
|
Stretch = Stretch.Uniform
|
||||||
}
|
}
|
||||||
|
: new Border
|
||||||
LauncherFolderTilePanel.Children.Add(CreateLauncherAppTile(app));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (LauncherFolderTilePanel.Children.Count == 0)
|
|
||||||
{
|
{
|
||||||
LauncherFolderTilePanel.Children.Add(CreateLauncherHintTile(
|
Width = 32,
|
||||||
L("launcher.empty_folder", "This folder is empty."),
|
Height = 32,
|
||||||
string.Empty));
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在图标渲染完成后,应用布局计算
|
clickAction();
|
||||||
Dispatcher.UIThread.Post(() => UpdateLauncherTileLayout(), DispatcherPriority.Background);
|
};
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
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)
|
private static string BuildMonogram(string text)
|
||||||
@@ -1689,18 +1864,6 @@ public partial class MainWindow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnLauncherFolderBackClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_launcherFolderStack.Count <= 1)
|
|
||||||
{
|
|
||||||
CloseLauncherFolderOverlay();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_launcherFolderStack.Pop();
|
|
||||||
RenderLauncherFolderFromStack();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnLauncherFolderOverlayPointerPressed(object? sender, PointerPressedEventArgs e)
|
private void OnLauncherFolderOverlayPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
{
|
{
|
||||||
if (LauncherFolderPanel is null)
|
if (LauncherFolderPanel is null)
|
||||||
@@ -1721,11 +1884,6 @@ public partial class MainWindow
|
|||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnLauncherFolderCloseClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
CloseLauncherFolderOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DisposeLauncherResources()
|
private void DisposeLauncherResources()
|
||||||
{
|
{
|
||||||
foreach (var bitmap in _launcherIconCache.Values)
|
foreach (var bitmap in _launcherIconCache.Values)
|
||||||
|
|||||||
@@ -650,8 +650,24 @@ public partial class MainWindow
|
|||||||
TaskbarLayoutMode = _taskbarLayoutMode,
|
TaskbarLayoutMode = _taskbarLayoutMode,
|
||||||
ClockDisplayFormat = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? "HourMinute" : "HourMinuteSecond",
|
ClockDisplayFormat = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? "HourMinute" : "HourMinuteSecond",
|
||||||
StatusBarClockTransparentBackground = _statusBarClockTransparentBackground,
|
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,
|
StatusBarSpacingMode = _statusBarSpacingMode,
|
||||||
StatusBarCustomSpacingPercent = _statusBarCustomSpacingPercent,
|
StatusBarCustomSpacingPercent = _statusBarCustomSpacingPercent,
|
||||||
|
StatusBarShadowEnabled = _statusBarShadowEnabled,
|
||||||
|
StatusBarShadowColor = _statusBarShadowColor,
|
||||||
|
StatusBarShadowOpacity = _statusBarShadowOpacity,
|
||||||
DisabledPluginIds = existingSnapshot.DisabledPluginIds,
|
DisabledPluginIds = existingSnapshot.DisabledPluginIds,
|
||||||
StudyFrameMs = existingSnapshot.StudyFrameMs,
|
StudyFrameMs = existingSnapshot.StudyFrameMs,
|
||||||
StudyScoreThresholdDbfs = existingSnapshot.StudyScoreThresholdDbfs,
|
StudyScoreThresholdDbfs = existingSnapshot.StudyScoreThresholdDbfs,
|
||||||
|
|||||||
@@ -189,50 +189,21 @@
|
|||||||
Classes="surface-solid-strong"
|
Classes="surface-solid-strong"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Margin="52"
|
Width="464"
|
||||||
MaxWidth="760"
|
Height="384"
|
||||||
MaxHeight="520"
|
CornerRadius="24"
|
||||||
CornerRadius="36"
|
Padding="16,14,16,12">
|
||||||
Padding="14">
|
<Grid RowDefinitions="Auto,*">
|
||||||
<Border.RenderTransform>
|
|
||||||
<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"
|
<TextBlock x:Name="LauncherFolderTitleTextBlock"
|
||||||
Grid.Column="1"
|
FontSize="15"
|
||||||
|
FontWeight="SemiBold"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
Margin="0,0,0,10" />
|
||||||
FontWeight="SemiBold" />
|
|
||||||
<Button x:Name="LauncherFolderCloseButton"
|
|
||||||
Grid.Column="2"
|
|
||||||
Width="38"
|
|
||||||
Height="34"
|
|
||||||
Padding="0"
|
|
||||||
Click="OnLauncherFolderCloseClick">
|
|
||||||
<fi:FluentIcon Icon="Dismiss"
|
|
||||||
IconVariant="Regular" />
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<ScrollViewer x:Name="LauncherFolderScrollViewer"
|
<Grid x:Name="LauncherFolderGridPanel"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
VerticalScrollBarVisibility="Auto"
|
ColumnDefinitions="*,*,*,*"
|
||||||
HorizontalScrollBarVisibility="Disabled">
|
RowDefinitions="*,*,*" />
|
||||||
<WrapPanel x:Name="LauncherFolderTilePanel"
|
|
||||||
Orientation="Horizontal" />
|
|
||||||
</ScrollViewer>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -255,20 +226,84 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</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"
|
<Border x:Name="TopStatusBarHost"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.ColumnSpan="1"
|
Grid.ColumnSpan="1"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
Padding="4">
|
Padding="4"
|
||||||
<StackPanel x:Name="TopStatusComponentsPanel"
|
ZIndex="2">
|
||||||
|
<Grid ColumnDefinitions="*,Auto,*">
|
||||||
|
<!-- 左侧状态栏组件 -->
|
||||||
|
<StackPanel x:Name="TopStatusLeftPanel"
|
||||||
|
Grid.Column="0"
|
||||||
Orientation="Horizontal"
|
Orientation="Horizontal"
|
||||||
Spacing="6">
|
Spacing="6"
|
||||||
<comp:ClockWidget x:Name="ClockWidget"
|
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"
|
IsVisible="False"
|
||||||
Margin="0" />
|
Margin="0" />
|
||||||
</StackPanel>
|
</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>
|
||||||
|
|
||||||
<Border x:Name="BottomTaskbarContainer"
|
<Border x:Name="BottomTaskbarContainer"
|
||||||
|
|||||||
@@ -135,6 +135,22 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
|||||||
private string _statusBarSpacingMode = "Relaxed";
|
private string _statusBarSpacingMode = "Relaxed";
|
||||||
private int _statusBarCustomSpacingPercent = 12;
|
private int _statusBarCustomSpacingPercent = 12;
|
||||||
private bool _statusBarClockTransparentBackground;
|
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 int _desktopEdgeInsetPercent = DefaultEdgeInsetPercent;
|
||||||
private string _taskbarLayoutMode = TaskbarLayoutBottomFullRowMacStyle;
|
private string _taskbarLayoutMode = TaskbarLayoutBottomFullRowMacStyle;
|
||||||
private string _languageCode = "zh-CN";
|
private string _languageCode = "zh-CN";
|
||||||
@@ -238,9 +254,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
|||||||
TaskbarProfileButton.IsEnabled = false;
|
TaskbarProfileButton.IsEnabled = false;
|
||||||
TaskbarProfilePopup.IsOpen = false;
|
TaskbarProfilePopup.IsOpen = false;
|
||||||
|
|
||||||
ClockWidget.IsVisible = true;
|
ClockWidgetLeft.IsVisible = true;
|
||||||
ClockWidget.SetDisplayFormat(ClockDisplayFormat.HourMinute);
|
ClockWidgetLeft.SetDisplayFormat(ClockDisplayFormat.HourMinute);
|
||||||
ClockWidget.SetTransparentBackground(false);
|
ClockWidgetLeft.SetTransparentBackground(false);
|
||||||
|
|
||||||
ConfigureDesignTimeDesktopGrid();
|
ConfigureDesignTimeDesktopGrid();
|
||||||
PopulateDesignTimeDesktopSurface();
|
PopulateDesignTimeDesktopSurface();
|
||||||
@@ -288,7 +304,7 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
|||||||
DesktopPagesHost.ColumnDefinitions.Clear();
|
DesktopPagesHost.ColumnDefinitions.Clear();
|
||||||
DesktopPagesHost.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(1, GridUnitType.Star)));
|
DesktopPagesHost.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(1, GridUnitType.Star)));
|
||||||
|
|
||||||
ClockWidget.ApplyCellSize(72);
|
ClockWidgetLeft.ApplyCellSize(72);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PopulateDesignTimeDesktopSurface()
|
private void PopulateDesignTimeDesktopSurface()
|
||||||
@@ -481,7 +497,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
|||||||
RebuildDesktopGrid();
|
RebuildDesktopGrid();
|
||||||
LoadLauncherEntriesAsync();
|
LoadLauncherEntriesAsync();
|
||||||
InitializeTimeZoneSettings();
|
InitializeTimeZoneSettings();
|
||||||
ClockWidget.SetTimeZoneService(_timeZoneService);
|
ClockWidgetLeft.SetTimeZoneService(_timeZoneService);
|
||||||
|
ClockWidgetCenter.SetTimeZoneService(_timeZoneService);
|
||||||
|
ClockWidgetRight.SetTimeZoneService(_timeZoneService);
|
||||||
|
|
||||||
_suppressSettingsPersistence = false;
|
_suppressSettingsPersistence = false;
|
||||||
PersistSettings();
|
PersistSettings();
|
||||||
@@ -621,7 +639,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
|||||||
|
|
||||||
private void ApplyDesktopStatusBarComponentSpacing()
|
private void ApplyDesktopStatusBarComponentSpacing()
|
||||||
{
|
{
|
||||||
ApplyStatusBarComponentSpacingForPanel(TopStatusComponentsPanel, _currentDesktopCellSize);
|
ApplyStatusBarComponentSpacingForPanel(TopStatusLeftPanel, _currentDesktopCellSize);
|
||||||
|
ApplyStatusBarComponentSpacingForPanel(TopStatusCenterPanel, _currentDesktopCellSize);
|
||||||
|
ApplyStatusBarComponentSpacingForPanel(TopStatusRightPanel, _currentDesktopCellSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int ResolveStatusBarSpacingPercent()
|
private int ResolveStatusBarSpacingPercent()
|
||||||
@@ -697,8 +717,26 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
|||||||
ApplyUnifiedMainRectangleChrome();
|
ApplyUnifiedMainRectangleChrome();
|
||||||
BottomTaskbarContainer.Padding = new Thickness(Math.Clamp(taskbarCellHeight * 0.16, 6, 14));
|
BottomTaskbarContainer.Padding = new Thickness(Math.Clamp(taskbarCellHeight * 0.16, 6, 14));
|
||||||
|
|
||||||
ClockWidget.Margin = new Thickness(0);
|
ClockWidgetLeft.Margin = new Thickness(0);
|
||||||
ClockWidget.ApplyCellSize(cellSize);
|
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);
|
var buttonMinWidth = Math.Clamp(taskbarCellHeight * 2.35, 100, 340);
|
||||||
|
|
||||||
@@ -737,7 +775,15 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
|||||||
|
|
||||||
if (_currentDesktopCellSize > 0)
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,182 @@
|
|||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</ui:SettingsExpanderItem>
|
</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>
|
</ui:SettingsExpander>
|
||||||
|
|
||||||
<Separator Classes="settings-separator" />
|
<Separator Classes="settings-separator" />
|
||||||
@@ -92,6 +268,55 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</ui:SettingsExpanderItem>
|
</ui:SettingsExpanderItem>
|
||||||
</ui:SettingsExpander>
|
</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>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<Window xmlns="https://github.com/avaloniaui"
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
x:Class="LanMountainDesktop.Views.TransparentOverlayWindow"
|
x:Class="LanMountainDesktop.Views.TransparentOverlayWindow"
|
||||||
WindowState="FullScreen"
|
|
||||||
SystemDecorations="None"
|
SystemDecorations="None"
|
||||||
CanResize="False"
|
CanResize="False"
|
||||||
ShowInTaskbar="False"
|
ShowInTaskbar="False"
|
||||||
|
|||||||
@@ -22,9 +22,6 @@ namespace LanMountainDesktop.Views;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class TransparentOverlayWindow : Window
|
public partial class TransparentOverlayWindow : Window
|
||||||
{
|
{
|
||||||
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
|
|
||||||
private readonly IWindowBottomMostService _bottomMostService = WindowBottomMostServiceFactory.GetOrCreate();
|
|
||||||
private readonly IRegionPassthroughService _regionPassthroughService = RegionPassthroughServiceFactory.GetOrCreate();
|
|
||||||
private readonly IFusedDesktopLayoutService _layoutService = FusedDesktopLayoutServiceProvider.GetOrCreate();
|
private readonly IFusedDesktopLayoutService _layoutService = FusedDesktopLayoutServiceProvider.GetOrCreate();
|
||||||
|
|
||||||
// 滑动状态
|
// 滑动状态
|
||||||
@@ -55,35 +52,75 @@ public partial class TransparentOverlayWindow : Window
|
|||||||
|
|
||||||
// 渲染参数
|
// 渲染参数
|
||||||
private const double DefaultCellSize = 100;
|
private const double DefaultCellSize = 100;
|
||||||
|
private double _currentDesktopCellSize;
|
||||||
|
|
||||||
// 拖拽状态
|
// 拖拽与缩放状态
|
||||||
private bool _isDragging;
|
private bool _isDragging;
|
||||||
private string? _draggingPlacementId;
|
private bool _isResizing;
|
||||||
private Point _dragStartPoint;
|
private string? _interactionPlacementId;
|
||||||
private Border? _draggingHost;
|
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 event EventHandler? RestoreMainWindowRequested;
|
||||||
|
|
||||||
public TransparentOverlayWindow()
|
public TransparentOverlayWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_weatherDataService = _settingsFacade.Weather.GetWeatherInfoService();
|
var facade = HostSettingsFacadeProvider.GetOrCreate();
|
||||||
_timeZoneService = _settingsFacade.Region.GetTimeZoneService();
|
_weatherDataService = facade.Weather.GetWeatherInfoService();
|
||||||
|
_timeZoneService = facade.Region.GetTimeZoneService();
|
||||||
// 仅在 Windows 上启用置底功能
|
_settingsFacade = facade;
|
||||||
if (OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
_bottomMostService.SetupBottomMost(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly ISettingsFacadeService _settingsFacade;
|
||||||
|
|
||||||
|
public void SaveLayoutAndHide()
|
||||||
|
{
|
||||||
|
SaveLayout();
|
||||||
|
Hide();
|
||||||
|
|
||||||
|
// Remove all components so that next time we open it builds fresh from snapshot
|
||||||
|
if (Content is Canvas canvas)
|
||||||
|
{
|
||||||
|
canvas.Children.Clear();
|
||||||
|
}
|
||||||
|
_componentHosts.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnOpened(EventArgs e)
|
protected override void OnOpened(EventArgs e)
|
||||||
{
|
{
|
||||||
base.OnOpened(e);
|
base.OnOpened(e);
|
||||||
|
|
||||||
if (OperatingSystem.IsWindows())
|
if (Screens.Primary is { } primaryScreen)
|
||||||
{
|
{
|
||||||
_bottomMostService.SendToBottom(this);
|
// 避开系统任务栏
|
||||||
|
var workArea = primaryScreen.WorkingArea;
|
||||||
|
var scaling = primaryScreen.Scaling;
|
||||||
|
Position = new PixelPoint(workArea.X, workArea.Y);
|
||||||
|
Width = workArea.Width / scaling;
|
||||||
|
Height = workArea.Height / scaling;
|
||||||
|
|
||||||
|
// 基于设置计算单元格尺寸
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保注册表已初始化
|
// 确保注册表已初始化
|
||||||
@@ -120,6 +157,7 @@ public partial class TransparentOverlayWindow : Window
|
|||||||
|
|
||||||
canvas.Children.Clear();
|
canvas.Children.Clear();
|
||||||
_componentHosts.Clear();
|
_componentHosts.Clear();
|
||||||
|
_selectedHost = null;
|
||||||
|
|
||||||
foreach (var placement in _layout.ComponentPlacements)
|
foreach (var placement in _layout.ComponentPlacements)
|
||||||
{
|
{
|
||||||
@@ -147,16 +185,7 @@ public partial class TransparentOverlayWindow : Window
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void UpdateInteractiveRegions()
|
private void UpdateInteractiveRegions()
|
||||||
{
|
{
|
||||||
_interactiveRegions.Clear();
|
// 编辑模式下不再需要底层穿透功能计算,这里留空或移除
|
||||||
|
|
||||||
foreach (var host in _componentHosts.Values)
|
|
||||||
{
|
|
||||||
var x = Canvas.GetLeft(host);
|
|
||||||
var y = Canvas.GetTop(host);
|
|
||||||
_interactiveRegions.Add(new Rect(x, y, host.Width, host.Height));
|
|
||||||
}
|
|
||||||
|
|
||||||
_regionPassthroughService.SetInteractiveRegions(this, _interactiveRegions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -180,9 +209,14 @@ public partial class TransparentOverlayWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析尺寸:如果未提供,则使用组件定义的最小尺寸 * 100
|
var finalWidth = width ?? (definition.MinWidthCells * _currentDesktopCellSize);
|
||||||
var finalWidth = width ?? (definition.MinWidthCells * DefaultCellSize);
|
var finalHeight = height ?? (definition.MinHeightCells * _currentDesktopCellSize);
|
||||||
var finalHeight = height ?? (definition.MinHeightCells * DefaultCellSize);
|
|
||||||
|
// 对齐网格
|
||||||
|
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 placementId = Guid.NewGuid().ToString("N");
|
||||||
var placement = new FusedDesktopComponentPlacementSnapshot
|
var placement = new FusedDesktopComponentPlacementSnapshot
|
||||||
@@ -225,7 +259,7 @@ public partial class TransparentOverlayWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
var control = descriptor.CreateControl(
|
var control = descriptor.CreateControl(
|
||||||
DefaultCellSize,
|
_currentDesktopCellSize,
|
||||||
_timeZoneService,
|
_timeZoneService,
|
||||||
_weatherDataService,
|
_weatherDataService,
|
||||||
_recommendationInfoService,
|
_recommendationInfoService,
|
||||||
@@ -260,24 +294,44 @@ public partial class TransparentOverlayWindow : Window
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void RenderComponent(string placementId, Control component, double x, double y, double width, double height)
|
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
|
var host = new Border
|
||||||
{
|
{
|
||||||
Tag = placementId,
|
Tag = placementId,
|
||||||
Width = width,
|
Width = width,
|
||||||
Height = height,
|
Height = height,
|
||||||
Background = Brushes.Transparent,
|
Background = Avalonia.Media.Brushes.Transparent,
|
||||||
CornerRadius = new CornerRadius(12),
|
CornerRadius = new Avalonia.CornerRadius(12),
|
||||||
ClipToBounds = true,
|
ClipToBounds = false, // 允许把手溢出
|
||||||
Child = component
|
BorderBrush = Avalonia.Media.Brushes.Transparent,
|
||||||
|
BorderThickness = new Avalonia.Thickness(3),
|
||||||
|
Child = grid,
|
||||||
|
Classes = { "desktop-component-host" }
|
||||||
};
|
};
|
||||||
|
|
||||||
Canvas.SetLeft(host, x);
|
Canvas.SetLeft(host, x);
|
||||||
Canvas.SetTop(host, y);
|
Canvas.SetTop(host, y);
|
||||||
|
|
||||||
// 添加拖拽支持
|
|
||||||
host.PointerPressed += OnComponentPointerPressed;
|
host.PointerPressed += OnComponentPointerPressed;
|
||||||
host.PointerMoved += OnComponentPointerMoved;
|
host.PointerMoved += OnInteractionPointerMoved;
|
||||||
host.PointerReleased += OnComponentPointerReleased;
|
host.PointerReleased += OnInteractionPointerReleased;
|
||||||
|
|
||||||
// 右键上下文菜单(删除组件)
|
// 右键上下文菜单(删除组件)
|
||||||
host.ContextRequested += OnComponentContextRequested;
|
host.ContextRequested += OnComponentContextRequested;
|
||||||
@@ -318,7 +372,60 @@ public partial class TransparentOverlayWindow : Window
|
|||||||
e.Handled = true;
|
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)
|
private void OnComponentPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is not Border host || host.Tag is not string placementId) return;
|
if (sender is not Border host || host.Tag is not string placementId) return;
|
||||||
@@ -326,55 +433,97 @@ public partial class TransparentOverlayWindow : Window
|
|||||||
var point = e.GetCurrentPoint(this);
|
var point = e.GetCurrentPoint(this);
|
||||||
if (!point.Properties.IsLeftButtonPressed) return;
|
if (!point.Properties.IsLeftButtonPressed) return;
|
||||||
|
|
||||||
|
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;
|
_isDragging = true;
|
||||||
_draggingPlacementId = placementId;
|
_isResizing = false;
|
||||||
_draggingHost = host;
|
}
|
||||||
_dragStartPoint = e.GetPosition(this);
|
|
||||||
|
|
||||||
e.Pointer.Capture(host);
|
e.Pointer.Capture(host);
|
||||||
e.Handled = true;
|
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 currentPoint = e.GetPosition(this);
|
||||||
var deltaX = currentPoint.X - _dragStartPoint.X;
|
var deltaX = currentPoint.X - _interactionStartPoint.X;
|
||||||
var deltaY = currentPoint.Y - _dragStartPoint.Y;
|
var deltaY = currentPoint.Y - _interactionStartPoint.Y;
|
||||||
|
|
||||||
var currentX = Canvas.GetLeft(_draggingHost);
|
if (_isDragging)
|
||||||
var currentY = Canvas.GetTop(_draggingHost);
|
{
|
||||||
|
var rawX = _interactionOriginalX + deltaX;
|
||||||
|
var rawY = _interactionOriginalY + deltaY;
|
||||||
|
|
||||||
Canvas.SetLeft(_draggingHost, currentX + deltaX);
|
var snapX = Math.Round(rawX / _currentDesktopCellSize) * _currentDesktopCellSize;
|
||||||
Canvas.SetTop(_draggingHost, currentY + deltaY);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
_dragStartPoint = currentPoint;
|
|
||||||
e.Handled = true;
|
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;
|
_isDragging = false;
|
||||||
|
_isResizing = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新布局中的位置
|
// 更新布局中的位置与尺寸
|
||||||
var placement = _layout.ComponentPlacements.Find(p => p.PlacementId == _draggingPlacementId);
|
var placement = _layout.ComponentPlacements.Find(p => p.PlacementId == _interactionPlacementId);
|
||||||
if (placement is not null)
|
if (placement is not null)
|
||||||
{
|
{
|
||||||
placement.X = Canvas.GetLeft(_draggingHost);
|
placement.X = Canvas.GetLeft(_interactionHost);
|
||||||
placement.Y = Canvas.GetTop(_draggingHost);
|
placement.Y = Canvas.GetTop(_interactionHost);
|
||||||
|
placement.Width = _interactionHost.Width;
|
||||||
|
placement.Height = _interactionHost.Height;
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateInteractiveRegions();
|
UpdateInteractiveRegions();
|
||||||
SaveLayout();
|
SaveLayout();
|
||||||
|
|
||||||
_isDragging = false;
|
_isDragging = false;
|
||||||
_draggingPlacementId = null;
|
_isResizing = false;
|
||||||
_draggingHost = null;
|
_interactionPlacementId = null;
|
||||||
|
_interactionHost = null;
|
||||||
|
|
||||||
e.Pointer.Capture(null);
|
e.Pointer.Capture(null);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user