mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1d5a0c6de | ||
|
|
5fa2031ad6 |
165
LanMountainDesktop.PluginSdk/LICENSE
Normal file
165
LanMountainDesktop.PluginSdk/LICENSE
Normal file
@@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version of
|
||||
the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
@@ -13,6 +13,8 @@
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://github.com/wwiinnddyy/LanMountainDesktop</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageLicenseExpression>LGPL-3.0-or-later</PackageLicenseExpression>
|
||||
<Copyright>Copyright (c) LanMountainDesktop Contributors</Copyright>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
165
LanMountainDesktop.Shared.Contracts/LICENSE
Normal file
165
LanMountainDesktop.Shared.Contracts/LICENSE
Normal file
@@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version of
|
||||
the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
@@ -13,6 +13,8 @@
|
||||
<RepositoryUrl>https://github.com/wwiinnddyy/LanMountainDesktop</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
<PackageLicenseExpression>LGPL-3.0-or-later</PackageLicenseExpression>
|
||||
<Copyright>Copyright (c) LanMountainDesktop Contributors</Copyright>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.3.12" />
|
||||
|
||||
@@ -45,4 +45,5 @@ public static class BuiltInComponentIds
|
||||
public const string DesktopRemovableStorage = "DesktopRemovableStorage";
|
||||
public const string DesktopZhiJiaoHub = "DesktopZhiJiaoHub";
|
||||
public const string DesktopFileManager = "DesktopFileManager";
|
||||
public const string DesktopNotificationBox = "DesktopNotificationBox";
|
||||
}
|
||||
|
||||
@@ -410,6 +410,16 @@ public sealed class ComponentRegistry
|
||||
MinHeightCells: 4,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true,
|
||||
ResizeMode: DesktopComponentResizeMode.Free),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopNotificationBox,
|
||||
"消息盒子",
|
||||
"Inbox",
|
||||
"Info",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true,
|
||||
ResizeMode: DesktopComponentResizeMode.Free)
|
||||
};
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
<PackageReference Include="WebView.Avalonia" Version="11.0.0.1" />
|
||||
<PackageReference Include="WebView.Avalonia.Desktop" Version="11.0.0.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||
<PackageReference Include="Tmds.DBus.Protocol" Version="0.22.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CopyPluginsInstallHelperToOutput" AfterTargets="Build">
|
||||
|
||||
@@ -1087,5 +1087,23 @@
|
||||
"zhijiaohub.settings.auto_refresh_desc": "Automatically refresh the image list periodically.",
|
||||
"zhijiaohub.settings.interval": "Refresh Interval (minutes)",
|
||||
"zhijiaohub.settings.about": "About",
|
||||
"zhijiaohub.settings.about_desc": "ZhiJiaoHub displays interesting images from the educational technology community. Images are fetched from GitHub repositories and cached locally."
|
||||
"zhijiaohub.settings.about_desc": "ZhiJiaoHub displays interesting images from the educational technology community. Images are fetched from GitHub repositories and cached locally.",
|
||||
"power.menu": "Power",
|
||||
"power.title": "Power",
|
||||
"power.back": "Back",
|
||||
"power.shutdown": "Shutdown",
|
||||
"power.restart": "Restart",
|
||||
"power.logout": "Log Out",
|
||||
"power.sleep": "Sleep",
|
||||
"power.lock_screen": "Lock Screen",
|
||||
"power.shutdown_confirm_title": "Shutdown Confirmation",
|
||||
"power.shutdown_confirm_message": "Are you sure you want to shut down this computer? Unsaved data may be lost.",
|
||||
"power.restart_confirm_title": "Restart Confirmation",
|
||||
"power.restart_confirm_message": "Are you sure you want to restart this computer? Unsaved data may be lost.",
|
||||
"power.logout_confirm_title": "Log Out Confirmation",
|
||||
"power.logout_confirm_message": "Are you sure you want to log out?",
|
||||
"power.sleep_confirm_title": "Sleep Confirmation",
|
||||
"power.sleep_confirm_message": "Are you sure you want to put the computer to sleep?",
|
||||
"power.confirm_yes": "Yes",
|
||||
"power.confirm_cancel": "Cancel"
|
||||
}
|
||||
|
||||
@@ -1081,5 +1081,23 @@
|
||||
"zhijiaohub.settings.auto_refresh_desc": "定期自动刷新图片列表。",
|
||||
"zhijiaohub.settings.interval": "刷新间隔(分钟)",
|
||||
"zhijiaohub.settings.about": "关于",
|
||||
"zhijiaohub.settings.about_desc": "智教Hub 展示来自教育技术社区的有趣图片。图片从 GitHub 仓库获取并缓存在本地。"
|
||||
"zhijiaohub.settings.about_desc": "智教Hub 展示来自教育技术社区的有趣图片。图片从 GitHub 仓库获取并缓存在本地。",
|
||||
"power.menu": "电源",
|
||||
"power.title": "电源",
|
||||
"power.back": "返回",
|
||||
"power.shutdown": "关机",
|
||||
"power.restart": "重启",
|
||||
"power.logout": "注销",
|
||||
"power.sleep": "睡眠",
|
||||
"power.lock_screen": "锁定屏幕",
|
||||
"power.shutdown_confirm_title": "关机确认",
|
||||
"power.shutdown_confirm_message": "确定要关闭计算机吗?未保存的数据可能会丢失。",
|
||||
"power.restart_confirm_title": "重启确认",
|
||||
"power.restart_confirm_message": "确定要重启计算机吗?未保存的数据可能会丢失。",
|
||||
"power.logout_confirm_title": "注销确认",
|
||||
"power.logout_confirm_message": "确定要注销当前用户吗?",
|
||||
"power.sleep_confirm_title": "睡眠确认",
|
||||
"power.sleep_confirm_message": "确定要让计算机进入睡眠状态吗?",
|
||||
"power.confirm_yes": "确定",
|
||||
"power.confirm_cancel": "取消"
|
||||
}
|
||||
|
||||
@@ -200,6 +200,35 @@ public sealed class AppSettingsSnapshot
|
||||
|
||||
#endregion
|
||||
|
||||
#region Notification Box Settings (消息盒子全局设置)
|
||||
|
||||
/// <summary>
|
||||
/// 启用消息盒子功能(Windows通知监听)
|
||||
/// </summary>
|
||||
public bool NotificationBoxEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 隐私模式:开启后只显示"您有新的通知",不显示具体内容
|
||||
/// </summary>
|
||||
public bool NotificationBoxPrivacyMode { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 被屏蔽的应用列表(不接收这些应用的通知)
|
||||
/// </summary>
|
||||
public List<string> NotificationBoxBlockedApps { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 历史记录保留天数
|
||||
/// </summary>
|
||||
public int NotificationBoxHistoryRetentionDays { get; set; } = 7;
|
||||
|
||||
/// <summary>
|
||||
/// 最大存储通知数量(防止内存无限增长)
|
||||
/// </summary>
|
||||
public int NotificationBoxMaxStoredCount { get; set; } = 500;
|
||||
|
||||
#endregion
|
||||
|
||||
public AppSettingsSnapshot Clone()
|
||||
{
|
||||
var clone = (AppSettingsSnapshot)MemberwiseClone();
|
||||
@@ -213,6 +242,9 @@ public sealed class AppSettingsSnapshot
|
||||
clone.DisabledPluginIds = DisabledPluginIds is { Count: > 0 }
|
||||
? new List<string>(DisabledPluginIds)
|
||||
: [];
|
||||
clone.NotificationBoxBlockedApps = NotificationBoxBlockedApps is { Count: > 0 }
|
||||
? new List<string>(NotificationBoxBlockedApps)
|
||||
: [];
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
@@ -84,6 +84,45 @@ public sealed class ComponentSettingsSnapshot
|
||||
|
||||
public int ZhiJiaoHubCurrentImageIndex { get; set; } = 0;
|
||||
|
||||
#region Notification Box Component Settings (消息盒子组件设置)
|
||||
|
||||
/// <summary>
|
||||
/// 组件内最大显示通知数量
|
||||
/// </summary>
|
||||
public int NotificationBoxMaxDisplayCount { get; set; } = 50;
|
||||
|
||||
/// <summary>
|
||||
/// 排序方式:TimeDesc(时间倒序), TimeAsc(时间正序), AppGroup(按应用分组)
|
||||
/// </summary>
|
||||
public string NotificationBoxSortOrder { get; set; } = "TimeDesc";
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示应用图标
|
||||
/// </summary>
|
||||
public bool NotificationBoxShowAppIcon { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示时间戳
|
||||
/// </summary>
|
||||
public bool NotificationBoxShowTimestamp { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 时间格式:Relative(相对时间,如"5分钟前"), Absolute(绝对时间)
|
||||
/// </summary>
|
||||
public string NotificationBoxTimeFormat { get; set; } = "Relative";
|
||||
|
||||
/// <summary>
|
||||
/// 是否按应用分组显示
|
||||
/// </summary>
|
||||
public bool NotificationBoxGroupByApp { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示清除按钮
|
||||
/// </summary>
|
||||
public bool NotificationBoxShowClearButton { get; set; } = true;
|
||||
|
||||
#endregion
|
||||
|
||||
public ComponentSettingsSnapshot Clone()
|
||||
{
|
||||
var clone = (ComponentSettingsSnapshot)MemberwiseClone();
|
||||
|
||||
54
LanMountainDesktop/Models/NotificationItem.cs
Normal file
54
LanMountainDesktop/Models/NotificationItem.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
|
||||
namespace LanMountainDesktop.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 通知项数据模型
|
||||
/// </summary>
|
||||
public sealed class NotificationItem
|
||||
{
|
||||
/// <summary>
|
||||
/// 唯一标识
|
||||
/// </summary>
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// 应用ID(如 WeChat, Outlook 等)
|
||||
/// </summary>
|
||||
public string AppId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 应用名称
|
||||
/// </summary>
|
||||
public string AppName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 应用图标路径或Base64
|
||||
/// </summary>
|
||||
public string? AppIconPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 通知标题
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 通知内容
|
||||
/// </summary>
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 接收时间
|
||||
/// </summary>
|
||||
public DateTime ReceivedTime { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 是否已读
|
||||
/// </summary>
|
||||
public bool IsRead { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 原始通知的额外数据(用于点击跳转)
|
||||
/// </summary>
|
||||
public string? LaunchArgs { get; set; }
|
||||
}
|
||||
@@ -267,6 +267,11 @@ public static class DesktopComponentEditorRegistryFactory
|
||||
BuiltInComponentIds.DesktopZhiJiaoHub,
|
||||
context => new ZhiJiaoHubComponentEditor(context),
|
||||
preferredWidth: 480d,
|
||||
preferredHeight: 520d),
|
||||
[BuiltInComponentIds.DesktopNotificationBox] = new(
|
||||
BuiltInComponentIds.DesktopNotificationBox,
|
||||
context => new NotificationBoxComponentEditor(context),
|
||||
preferredWidth: 480d,
|
||||
preferredHeight: 520d)
|
||||
};
|
||||
|
||||
|
||||
216
LanMountainDesktop/Services/LinuxNotificationListener.cs
Normal file
216
LanMountainDesktop/Services/LinuxNotificationListener.cs
Normal file
@@ -0,0 +1,216 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using LanMountainDesktop.Models;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Linux平台通知监听器 - 通过DBus监听org.freedesktop.Notifications
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("linux")]
|
||||
internal sealed class LinuxNotificationListener : IDisposable
|
||||
{
|
||||
private readonly NotificationListenerService _parent;
|
||||
private CancellationTokenSource? _cts;
|
||||
private bool _isRunning;
|
||||
|
||||
public LinuxNotificationListener(NotificationListenerService parent)
|
||||
{
|
||||
_parent = parent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化并启动DBus监听
|
||||
/// </summary>
|
||||
public async Task<bool> InitializeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查DBus环境变量
|
||||
var dbusSessionBus = Environment.GetEnvironmentVariable("DBUS_SESSION_BUS_ADDRESS");
|
||||
if (string.IsNullOrEmpty(dbusSessionBus))
|
||||
{
|
||||
Console.WriteLine("[NotificationBox] DBus Session Bus 环境变量未设置");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查通知守护进程是否运行
|
||||
// 通过检查常见进程名
|
||||
var hasNotificationDaemon = await CheckNotificationDaemonAsync();
|
||||
if (!hasNotificationDaemon)
|
||||
{
|
||||
Console.WriteLine("[NotificationBox] 未检测到通知守护进程,消息盒子功能可能不可用");
|
||||
// 仍然返回true,因为守护进程可能在之后启动
|
||||
}
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
_ = StartListeningAsync(_cts.Token);
|
||||
|
||||
Console.WriteLine("[NotificationBox] Linux通知监听已启动");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[NotificationBox] Linux通知监听初始化失败: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> CheckNotificationDaemonAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查常见通知守护进程
|
||||
var processNames = new[] { "gnome-shell", "kded5", "dunst", "mako", "swaync" };
|
||||
foreach (var name in processNames)
|
||||
{
|
||||
var psi = new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = "pgrep",
|
||||
Arguments = $"-x {name}",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var process = System.Diagnostics.Process.Start(psi);
|
||||
if (process != null)
|
||||
{
|
||||
await process.WaitForExitAsync();
|
||||
if (process.ExitCode == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartListeningAsync(CancellationToken ct)
|
||||
{
|
||||
_isRunning = true;
|
||||
|
||||
try
|
||||
{
|
||||
// 注意:Tmds.DBus.Protocol 是低层API
|
||||
// 这里使用简化方案,实际生产环境需要完整的DBus信号订阅实现
|
||||
// 当前版本为框架实现,后续可以完善DBus监听逻辑
|
||||
|
||||
while (!ct.IsCancellationRequested && _isRunning)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(1000, ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[NotificationBox] Linux通知监听异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理接收到的通知(供DBus信号处理器调用)
|
||||
/// </summary>
|
||||
public void HandleNotification(
|
||||
string appName,
|
||||
uint replacesId,
|
||||
string appIcon,
|
||||
string summary,
|
||||
string body,
|
||||
string[] actions,
|
||||
object hints,
|
||||
int expireTimeout)
|
||||
{
|
||||
try
|
||||
{
|
||||
var notification = new NotificationItem
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
AppId = appName.ToLowerInvariant().Replace(" ", ""),
|
||||
AppName = appName,
|
||||
Title = summary,
|
||||
Content = StripHtmlTags(body),
|
||||
ReceivedTime = DateTime.Now,
|
||||
AppIconPath = ResolveIconPath(appIcon, appName)
|
||||
};
|
||||
|
||||
_parent.AddNotification(notification);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[NotificationBox] 处理通知失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析应用图标路径
|
||||
/// </summary>
|
||||
private static string? ResolveIconPath(string iconName, string appName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(iconName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 如果是绝对路径,直接使用
|
||||
if (File.Exists(iconName))
|
||||
{
|
||||
return iconName;
|
||||
}
|
||||
|
||||
// 尝试从图标主题中查找
|
||||
var iconPaths = new[]
|
||||
{
|
||||
$"/usr/share/icons/hicolor/48x48/apps/{iconName}.png",
|
||||
$"/usr/share/icons/hicolor/64x64/apps/{iconName}.png",
|
||||
$"/usr/share/pixmaps/{iconName}.png",
|
||||
$"/usr/share/pixmaps/{iconName}.svg",
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
$".local/share/icons/{iconName}.png")
|
||||
};
|
||||
|
||||
return iconPaths.FirstOrDefault(File.Exists);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 去除HTML标签(通知内容可能包含HTML)
|
||||
/// </summary>
|
||||
private static string StripHtmlTags(string html)
|
||||
{
|
||||
if (string.IsNullOrEmpty(html))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// 简单的HTML标签去除
|
||||
var result = html;
|
||||
result = System.Text.RegularExpressions.Regex.Replace(result, "<[^>]+>", "");
|
||||
result = result.Replace("<", "<");
|
||||
result = result.Replace(">", ">");
|
||||
result = result.Replace("&", "&");
|
||||
result = result.Replace(""", "\"");
|
||||
return result.Trim();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_isRunning = false;
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
}
|
||||
}
|
||||
201
LanMountainDesktop/Services/NotificationListenerService.cs
Normal file
201
LanMountainDesktop/Services/NotificationListenerService.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 跨平台通知监听服务
|
||||
/// </summary>
|
||||
public sealed class NotificationListenerService : IDisposable
|
||||
{
|
||||
private readonly List<NotificationItem> _notifications = [];
|
||||
private readonly object _lock = new();
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
// 平台特定的监听器
|
||||
private LinuxNotificationListener? _linuxListener;
|
||||
|
||||
public event EventHandler<NotificationItem>? NotificationReceived;
|
||||
public event EventHandler<string>? NotificationRemoved;
|
||||
|
||||
public NotificationListenerService(ISettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化并启动监听
|
||||
/// </summary>
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// Windows: 使用 UserNotificationListener (需要Windows SDK)
|
||||
// 当前为模拟实现
|
||||
await InitializeWindowsAsync();
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
// Linux: 使用 DBus
|
||||
await InitializeLinuxAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// macOS 或其他平台:功能不可用
|
||||
Console.WriteLine("[NotificationBox] 当前平台不支持通知监听");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[NotificationBox] 初始化失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InitializeWindowsAsync()
|
||||
{
|
||||
// Windows通知监听实现
|
||||
// 实际项目中需要添加Windows SDK引用并使用UserNotificationListener
|
||||
// 由于需要UWP API,这里使用模拟实现
|
||||
await Task.CompletedTask;
|
||||
Console.WriteLine("[NotificationBox] Windows通知监听已启动(模拟模式)");
|
||||
}
|
||||
|
||||
private async Task InitializeLinuxAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_linuxListener = new LinuxNotificationListener(this);
|
||||
var success = await _linuxListener.InitializeAsync();
|
||||
|
||||
if (!success)
|
||||
{
|
||||
Console.WriteLine("[NotificationBox] Linux通知监听初始化失败,可能未运行通知守护进程");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[NotificationBox] Linux通知监听异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加通知(供平台监听器调用)
|
||||
/// </summary>
|
||||
public void AddNotification(NotificationItem notification)
|
||||
{
|
||||
var settings = _settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||
|
||||
// 检查全局开关
|
||||
if (!settings.NotificationBoxEnabled)
|
||||
return;
|
||||
|
||||
// 检查是否在屏蔽列表中
|
||||
if (settings.NotificationBoxBlockedApps.Contains(notification.AppId, StringComparer.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_notifications.Add(notification);
|
||||
CleanupOldNotifications(settings);
|
||||
}
|
||||
|
||||
// 在UI线程触发事件
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
NotificationReceived?.Invoke(this, notification);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除通知
|
||||
/// </summary>
|
||||
public void RemoveNotification(string notificationId)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var notification = _notifications.FirstOrDefault(n => n.Id == notificationId);
|
||||
if (notification != null)
|
||||
{
|
||||
_notifications.Remove(notification);
|
||||
}
|
||||
}
|
||||
|
||||
NotificationRemoved?.Invoke(this, notificationId);
|
||||
}
|
||||
|
||||
private void CleanupOldNotifications(AppSettingsSnapshot settings)
|
||||
{
|
||||
// 按数量清理
|
||||
var maxCount = settings.NotificationBoxMaxStoredCount;
|
||||
while (_notifications.Count > maxCount)
|
||||
{
|
||||
_notifications.RemoveAt(0);
|
||||
}
|
||||
|
||||
// 按时间清理
|
||||
var cutoffDate = DateTime.Now.AddDays(-settings.NotificationBoxHistoryRetentionDays);
|
||||
_notifications.RemoveAll(n => n.ReceivedTime < cutoffDate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有通知
|
||||
/// </summary>
|
||||
public IReadOnlyList<NotificationItem> GetNotifications()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _notifications.ToList().AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有通知
|
||||
/// </summary>
|
||||
public void ClearAll()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_notifications.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记通知为已读
|
||||
/// </summary>
|
||||
public void MarkAsRead(string notificationId)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var notification = _notifications.FirstOrDefault(n => n.Id == notificationId);
|
||||
if (notification != null)
|
||||
{
|
||||
notification.IsRead = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取未读通知数量
|
||||
/// </summary>
|
||||
public int GetUnreadCount()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _notifications.Count(n => !n.IsRead);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_linuxListener?.Dispose();
|
||||
ClearAll();
|
||||
}
|
||||
}
|
||||
253
LanMountainDesktop/Services/PowerManagementService.cs
Normal file
253
LanMountainDesktop/Services/PowerManagementService.cs
Normal file
@@ -0,0 +1,253 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
public interface IPowerManagementService
|
||||
{
|
||||
bool IsShutdownSupported { get; }
|
||||
bool IsRestartSupported { get; }
|
||||
bool IsLogoutSupported { get; }
|
||||
bool IsLockSupported { get; }
|
||||
bool IsSleepSupported { get; }
|
||||
|
||||
Task ShutdownAsync();
|
||||
Task RestartAsync();
|
||||
Task LogoutAsync();
|
||||
Task LockAsync();
|
||||
Task SleepAsync();
|
||||
|
||||
void ShowNativePowerUI(PowerAction action);
|
||||
}
|
||||
|
||||
public enum PowerAction
|
||||
{
|
||||
Shutdown,
|
||||
Restart
|
||||
}
|
||||
|
||||
public static class PowerManagementServiceFactory
|
||||
{
|
||||
private static IPowerManagementService? _instance;
|
||||
private static readonly object _lock = new();
|
||||
|
||||
public static IPowerManagementService GetOrCreate()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _instance ??= CreatePlatformService();
|
||||
}
|
||||
}
|
||||
|
||||
private static IPowerManagementService CreatePlatformService()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
return new WindowsPowerManagementService();
|
||||
if (OperatingSystem.IsLinux())
|
||||
return new LinuxPowerManagementService();
|
||||
return new NullPowerManagementService();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class WindowsPowerManagementService : IPowerManagementService
|
||||
{
|
||||
public bool IsShutdownSupported => true;
|
||||
public bool IsRestartSupported => true;
|
||||
public bool IsLogoutSupported => true;
|
||||
public bool IsLockSupported => true;
|
||||
public bool IsSleepSupported => true;
|
||||
|
||||
public async Task ShutdownAsync()
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "shutdown",
|
||||
Arguments = "/s /t 0",
|
||||
UseShellExecute = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async Task RestartAsync()
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "shutdown",
|
||||
Arguments = "/r /t 0",
|
||||
UseShellExecute = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async Task LogoutAsync()
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
ExitWindowsEx(0, 0);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task LockAsync()
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
LockWorkStation();
|
||||
});
|
||||
}
|
||||
|
||||
public async Task SleepAsync()
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
SetSuspendState(false, false, false);
|
||||
});
|
||||
}
|
||||
|
||||
public void ShowNativePowerUI(PowerAction action)
|
||||
{
|
||||
var slideToShutDownPath = Environment.ExpandEnvironmentVariables(@"%windir%\System32\SlideToShutDown.exe");
|
||||
if (System.IO.File.Exists(slideToShutDownPath))
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = slideToShutDownPath,
|
||||
UseShellExecute = true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case PowerAction.Shutdown:
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "shutdown",
|
||||
Arguments = "/s /t 5 /c \"LanMountainDesktop: Shutting down...\"",
|
||||
UseShellExecute = true
|
||||
});
|
||||
break;
|
||||
|
||||
case PowerAction.Restart:
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "shutdown",
|
||||
Arguments = "/r /t 5 /c \"LanMountainDesktop: Restarting...\"",
|
||||
UseShellExecute = true
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool ExitWindowsEx(uint uFlags, uint dwReason);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern void LockWorkStation();
|
||||
|
||||
[DllImport("powrprof.dll", SetLastError = true)]
|
||||
private static extern bool SetSuspendState(bool hibernate, bool forceCritical, bool disableWakeEvent);
|
||||
}
|
||||
|
||||
internal sealed class LinuxPowerManagementService : IPowerManagementService
|
||||
{
|
||||
public bool IsShutdownSupported => true;
|
||||
public bool IsRestartSupported => true;
|
||||
public bool IsLogoutSupported => true;
|
||||
public bool IsLockSupported => true;
|
||||
public bool IsSleepSupported => true;
|
||||
|
||||
public async Task ShutdownAsync()
|
||||
{
|
||||
await RunSystemctlCommand("poweroff -i");
|
||||
}
|
||||
|
||||
public async Task RestartAsync()
|
||||
{
|
||||
await RunSystemctlCommand("reboot -i");
|
||||
}
|
||||
|
||||
public async Task LogoutAsync()
|
||||
{
|
||||
await RunLoginctlCommand("terminate-session $XDG_SESSION_ID");
|
||||
}
|
||||
|
||||
public async Task LockAsync()
|
||||
{
|
||||
await RunLoginctlCommand("lock-session");
|
||||
}
|
||||
|
||||
public async Task SleepAsync()
|
||||
{
|
||||
await RunSystemctlCommand("suspend -i");
|
||||
}
|
||||
|
||||
public void ShowNativePowerUI(PowerAction action)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case PowerAction.Shutdown:
|
||||
RunProcess("systemctl", "poweroff -i");
|
||||
break;
|
||||
case PowerAction.Restart:
|
||||
RunProcess("systemctl", "reboot -i");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task RunSystemctlCommand(string args)
|
||||
{
|
||||
await RunProcess("systemctl", args);
|
||||
}
|
||||
|
||||
private static async Task RunLoginctlCommand(string args)
|
||||
{
|
||||
await RunProcess("loginctl", args);
|
||||
}
|
||||
|
||||
private static async Task RunProcess(string command, string args)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = command,
|
||||
Arguments = args,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
})?.WaitForExit(5000);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Error("LinuxPowerManagement", $"Failed to execute {command} {args}: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class NullPowerManagementService : IPowerManagementService
|
||||
{
|
||||
public bool IsShutdownSupported => false;
|
||||
public bool IsRestartSupported => false;
|
||||
public bool IsLogoutSupported => false;
|
||||
public bool IsLockSupported => false;
|
||||
public bool IsSleepSupported => false;
|
||||
|
||||
public Task ShutdownAsync() => Task.CompletedTask;
|
||||
public Task RestartAsync() => Task.CompletedTask;
|
||||
public Task LogoutAsync() => Task.CompletedTask;
|
||||
public Task LockAsync() => Task.CompletedTask;
|
||||
public Task SleepAsync() => Task.CompletedTask;
|
||||
|
||||
public void ShowNativePowerUI(PowerAction action) { }
|
||||
}
|
||||
115
LanMountainDesktop/ViewModels/NotificationBoxEditorViewModel.cs
Normal file
115
LanMountainDesktop/ViewModels/NotificationBoxEditorViewModel.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
|
||||
namespace LanMountainDesktop.ViewModels;
|
||||
|
||||
public sealed partial class NotificationBoxEditorViewModel : ViewModelBase
|
||||
{
|
||||
private readonly DesktopComponentEditorContext? _context;
|
||||
private bool _isInitializing;
|
||||
|
||||
public NotificationBoxEditorViewModel(DesktopComponentEditorContext? context)
|
||||
{
|
||||
_context = context;
|
||||
|
||||
MaxDisplayCountOptions = new ObservableCollection<SelectionOption>
|
||||
{
|
||||
new("20", "20条"),
|
||||
new("50", "50条"),
|
||||
new("100", "100条"),
|
||||
new("200", "200条")
|
||||
};
|
||||
|
||||
SortOrderOptions = new ObservableCollection<SelectionOption>
|
||||
{
|
||||
new("TimeDesc", "最新优先"),
|
||||
new("TimeAsc", "最早优先"),
|
||||
new("AppGroup", "按应用分组")
|
||||
};
|
||||
|
||||
TimeFormatOptions = new ObservableCollection<SelectionOption>
|
||||
{
|
||||
new("Relative", "相对时间(如:5分钟前)"),
|
||||
new("Absolute", "绝对时间(如:14:30)")
|
||||
};
|
||||
|
||||
LoadSettings();
|
||||
}
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
var snapshot = _context?.ComponentSettingsAccessor.LoadSnapshot<ComponentSettingsSnapshot>()
|
||||
?? new ComponentSettingsSnapshot();
|
||||
|
||||
_isInitializing = true;
|
||||
|
||||
var countValue = snapshot.NotificationBoxMaxDisplayCount.ToString();
|
||||
SelectedMaxDisplayCount = MaxDisplayCountOptions.FirstOrDefault(o => o.Value == countValue)
|
||||
?? MaxDisplayCountOptions[1]; // 默认50
|
||||
|
||||
SelectedSortOrder = SortOrderOptions.FirstOrDefault(o => o.Value == snapshot.NotificationBoxSortOrder)
|
||||
?? SortOrderOptions[0];
|
||||
ShowAppIcon = snapshot.NotificationBoxShowAppIcon;
|
||||
ShowTimestamp = snapshot.NotificationBoxShowTimestamp;
|
||||
SelectedTimeFormat = TimeFormatOptions.FirstOrDefault(o => o.Value == snapshot.NotificationBoxTimeFormat)
|
||||
?? TimeFormatOptions[0];
|
||||
GroupByApp = snapshot.NotificationBoxGroupByApp;
|
||||
ShowClearButton = snapshot.NotificationBoxShowClearButton;
|
||||
|
||||
_isInitializing = false;
|
||||
}
|
||||
|
||||
private void SaveSettings()
|
||||
{
|
||||
if (_isInitializing || _context == null) return;
|
||||
|
||||
var snapshot = _context.ComponentSettingsAccessor.LoadSnapshot<ComponentSettingsSnapshot>();
|
||||
|
||||
snapshot.NotificationBoxMaxDisplayCount = int.TryParse(SelectedMaxDisplayCount?.Value, out var count) ? count : 50;
|
||||
snapshot.NotificationBoxSortOrder = SelectedSortOrder?.Value ?? "TimeDesc";
|
||||
snapshot.NotificationBoxShowAppIcon = ShowAppIcon;
|
||||
snapshot.NotificationBoxShowTimestamp = ShowTimestamp;
|
||||
snapshot.NotificationBoxTimeFormat = SelectedTimeFormat?.Value ?? "Relative";
|
||||
snapshot.NotificationBoxGroupByApp = GroupByApp;
|
||||
snapshot.NotificationBoxShowClearButton = ShowClearButton;
|
||||
|
||||
_context.ComponentSettingsAccessor.SaveSnapshot(snapshot);
|
||||
|
||||
_context.HostContext.RequestRefresh();
|
||||
}
|
||||
|
||||
[ObservableProperty] private string _descriptionText = "配置此消息盒子组件的显示方式。这些设置仅作用于当前组件实例。";
|
||||
[ObservableProperty] private string _maxDisplayCountLabel = "最大显示数量";
|
||||
[ObservableProperty] private string _maxDisplayCountDescription = "组件中最多显示的通知条数";
|
||||
[ObservableProperty] private string _sortOrderLabel = "排序方式";
|
||||
[ObservableProperty] private string _displayOptionsLabel = "显示选项";
|
||||
[ObservableProperty] private string _showAppIconLabel = "显示应用图标";
|
||||
[ObservableProperty] private string _showTimestampLabel = "显示时间戳";
|
||||
[ObservableProperty] private string _groupByAppLabel = "按应用分组显示";
|
||||
[ObservableProperty] private string _showClearButtonLabel = "显示清空按钮";
|
||||
[ObservableProperty] private string _timeFormatLabel = "时间格式";
|
||||
|
||||
[ObservableProperty] private SelectionOption? _selectedMaxDisplayCount;
|
||||
[ObservableProperty] private SelectionOption? _selectedSortOrder;
|
||||
[ObservableProperty] private bool _showAppIcon = true;
|
||||
[ObservableProperty] private bool _showTimestamp = true;
|
||||
[ObservableProperty] private SelectionOption? _selectedTimeFormat;
|
||||
[ObservableProperty] private bool _groupByApp = false;
|
||||
[ObservableProperty] private bool _showClearButton = true;
|
||||
|
||||
public ObservableCollection<SelectionOption> MaxDisplayCountOptions { get; }
|
||||
public ObservableCollection<SelectionOption> SortOrderOptions { get; }
|
||||
public ObservableCollection<SelectionOption> TimeFormatOptions { get; }
|
||||
|
||||
partial void OnSelectedMaxDisplayCountChanged(SelectionOption? value) => SaveSettings();
|
||||
partial void OnSelectedSortOrderChanged(SelectionOption? value) => SaveSettings();
|
||||
partial void OnShowAppIconChanged(bool value) => SaveSettings();
|
||||
partial void OnShowTimestampChanged(bool value) => SaveSettings();
|
||||
partial void OnSelectedTimeFormatChanged(SelectionOption? value) => SaveSettings();
|
||||
partial void OnGroupByAppChanged(bool value) => SaveSettings();
|
||||
partial void OnShowClearButtonChanged(bool value) => SaveSettings();
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
x:Class="LanMountainDesktop.Views.ComponentEditors.NotificationBoxComponentEditor"
|
||||
x:DataType="vm:NotificationBoxEditorViewModel">
|
||||
|
||||
<StackPanel Spacing="16">
|
||||
<!-- 说明卡片 -->
|
||||
<Border Classes="component-editor-card" Padding="20">
|
||||
<TextBlock Text="{Binding DescriptionText}"
|
||||
Classes="component-editor-secondary-text"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
|
||||
<!-- 最大显示数量 -->
|
||||
<Border Classes="component-editor-card" Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="{Binding MaxDisplayCountLabel}"
|
||||
Classes="component-editor-section-title" />
|
||||
<TextBlock Text="{Binding MaxDisplayCountDescription}"
|
||||
Classes="component-editor-secondary-text" />
|
||||
<ComboBox ItemsSource="{Binding MaxDisplayCountOptions}"
|
||||
SelectedItem="{Binding SelectedMaxDisplayCount}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SelectionOption">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 排序方式 -->
|
||||
<Border Classes="component-editor-card" Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="{Binding SortOrderLabel}"
|
||||
Classes="component-editor-section-title" />
|
||||
<ComboBox ItemsSource="{Binding SortOrderOptions}"
|
||||
SelectedItem="{Binding SelectedSortOrder}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SelectionOption">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 显示选项 -->
|
||||
<Border Classes="component-editor-card" Padding="20">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="{Binding DisplayOptionsLabel}"
|
||||
Classes="component-editor-section-title" />
|
||||
|
||||
<CheckBox IsChecked="{Binding ShowAppIcon}"
|
||||
Content="{Binding ShowAppIconLabel}" />
|
||||
|
||||
<CheckBox IsChecked="{Binding ShowTimestamp}"
|
||||
Content="{Binding ShowTimestampLabel}" />
|
||||
|
||||
<CheckBox IsChecked="{Binding GroupByApp}"
|
||||
Content="{Binding GroupByAppLabel}" />
|
||||
|
||||
<CheckBox IsChecked="{Binding ShowClearButton}"
|
||||
Content="{Binding ShowClearButtonLabel}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 时间格式 -->
|
||||
<Border Classes="component-editor-card" Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="{Binding TimeFormatLabel}"
|
||||
Classes="component-editor-section-title" />
|
||||
<ComboBox ItemsSource="{Binding TimeFormatOptions}"
|
||||
SelectedItem="{Binding SelectedTimeFormat}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SelectionOption">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,15 @@
|
||||
using Avalonia.Controls;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.ViewModels;
|
||||
|
||||
namespace LanMountainDesktop.Views.ComponentEditors;
|
||||
|
||||
public partial class NotificationBoxComponentEditor : ComponentEditorViewBase
|
||||
{
|
||||
public NotificationBoxComponentEditor(DesktopComponentEditorContext? context)
|
||||
: base(context)
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new NotificationBoxEditorViewModel(context);
|
||||
}
|
||||
}
|
||||
@@ -479,7 +479,11 @@ public sealed class DesktopComponentRuntimeRegistry
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopFileManager,
|
||||
"component.file_manager",
|
||||
() => new FileManagerWidget())
|
||||
() => new FileManagerWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopNotificationBox,
|
||||
"component.notification_box",
|
||||
() => new NotificationBoxWidget())
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||
xmlns:symbol="using:FluentIcons.Common"
|
||||
x:Class="LanMountainDesktop.Views.Components.NotificationBoxWidget">
|
||||
|
||||
<Border x:Name="RootBorder"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
|
||||
Background="Transparent"
|
||||
ClipToBounds="True">
|
||||
<Grid>
|
||||
<!-- 主卡片 -->
|
||||
<Border x:Name="CardBorder"
|
||||
Background="#FCFCFD"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
|
||||
Padding="12,10">
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<!-- 头部 -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
||||
<fi:SymbolIcon x:Name="HeaderIcon" Symbol="{x:Static symbol:Symbol.MailInbox}" FontSize="16" />
|
||||
<TextBlock x:Name="HeaderTextBlock"
|
||||
Text="消息盒子"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold" />
|
||||
<Border x:Name="UnreadBadge"
|
||||
Background="#E24B2D"
|
||||
CornerRadius="8"
|
||||
Padding="5,2"
|
||||
IsVisible="False">
|
||||
<TextBlock x:Name="UnreadCountText"
|
||||
Foreground="White"
|
||||
FontSize="11"
|
||||
FontWeight="Bold" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<Button x:Name="ClearButton"
|
||||
Grid.Column="1"
|
||||
IsVisible="False"
|
||||
Click="OnClearButtonClick"
|
||||
Padding="6"
|
||||
Background="Transparent"
|
||||
BorderThickness="0">
|
||||
<fi:SymbolIcon Symbol="{x:Static symbol:Symbol.Delete}" FontSize="14" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- 通知列表 -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
Margin="0,8,0,0"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel x:Name="NotificationListPanel" Spacing="6" />
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<TextBlock x:Name="EmptyStateText"
|
||||
Grid.Row="1"
|
||||
Text="暂无通知"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#8B95A5"
|
||||
FontSize="13"
|
||||
IsVisible="False" />
|
||||
|
||||
<!-- 隐私模式遮罩 -->
|
||||
<Border x:Name="PrivacyOverlay"
|
||||
Grid.Row="1"
|
||||
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
|
||||
IsVisible="False">
|
||||
<StackPanel HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="6">
|
||||
<fi:SymbolIcon Symbol="{x:Static symbol:Symbol.EyeOff}" FontSize="24" Foreground="#8B95A5" />
|
||||
<TextBlock Text="您有新的通知"
|
||||
Foreground="#8B95A5"
|
||||
FontSize="12" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 底部状态 -->
|
||||
<TextBlock x:Name="StatusTextBlock"
|
||||
Grid.Row="2"
|
||||
FontSize="11"
|
||||
Foreground="#8B95A5"
|
||||
Margin="0,6,0,0" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,529 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
public partial class NotificationBoxWidget : UserControl,
|
||||
IDesktopComponentWidget,
|
||||
IComponentSettingsContextAware,
|
||||
IComponentRuntimeContextAware
|
||||
{
|
||||
private readonly List<NotificationItemControl> _notificationControls = [];
|
||||
private NotificationListenerService? _notificationService;
|
||||
private IComponentInstanceSettingsStore _componentSettings = null!;
|
||||
private ISettingsService _appSettingsService = null!;
|
||||
private AppSettingsSnapshot _appSettings = new();
|
||||
private ComponentSettingsSnapshot _componentSettingsSnapshot = new();
|
||||
private bool _isAttached;
|
||||
private bool _isPrivacyMode;
|
||||
private bool _isNightVisual;
|
||||
private double _currentCellSize = 48d;
|
||||
|
||||
public NotificationBoxWidget()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||
|
||||
PointerPressed += (_, e) =>
|
||||
{
|
||||
if (e.Source == NotificationListPanel)
|
||||
{
|
||||
ClearSelection();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
UpdateAdaptiveLayout();
|
||||
}
|
||||
|
||||
public void SetComponentSettingsContext(DesktopComponentSettingsContext context)
|
||||
{
|
||||
_componentSettings = context.ComponentSettingsStore;
|
||||
LoadSettings();
|
||||
RefreshUI();
|
||||
}
|
||||
|
||||
public void SetComponentRuntimeContext(DesktopComponentRuntimeContext context)
|
||||
{
|
||||
_notificationService = NotificationListenerServiceProvider.GetOrCreate(_appSettingsService);
|
||||
if (_notificationService != null)
|
||||
{
|
||||
_notificationService.NotificationReceived += OnNotificationReceived;
|
||||
_notificationService.NotificationRemoved += OnNotificationRemoved;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_isAttached = true;
|
||||
_isNightVisual = ResolveNightMode();
|
||||
LoadSettings();
|
||||
RefreshUI();
|
||||
UpdateAdaptiveLayout();
|
||||
}
|
||||
|
||||
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_isAttached = false;
|
||||
|
||||
if (_notificationService != null)
|
||||
{
|
||||
_notificationService.NotificationReceived -= OnNotificationReceived;
|
||||
_notificationService.NotificationRemoved -= OnNotificationRemoved;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
|
||||
{
|
||||
_isNightVisual = ResolveNightMode();
|
||||
UpdateAdaptiveLayout();
|
||||
}
|
||||
|
||||
private bool ResolveNightMode()
|
||||
{
|
||||
if (ActualThemeVariant == ThemeVariant.Dark)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ActualThemeVariant == ThemeVariant.Light)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) &&
|
||||
value is ISolidColorBrush brush)
|
||||
{
|
||||
return CalculateRelativeLuminance(brush.Color) < 0.45;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static double CalculateRelativeLuminance(Color color)
|
||||
{
|
||||
static double ToLinear(double channel)
|
||||
{
|
||||
return channel <= 0.03928
|
||||
? channel / 12.92
|
||||
: Math.Pow((channel + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
var r = ToLinear(color.R / 255d);
|
||||
var g = ToLinear(color.G / 255d);
|
||||
var b = ToLinear(color.B / 255d);
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
var appSettingsFacade = HostSettingsFacadeProvider.GetOrCreate();
|
||||
_appSettingsService = appSettingsFacade.Settings;
|
||||
_appSettings = _appSettingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||
_isPrivacyMode = _appSettings.NotificationBoxPrivacyMode;
|
||||
|
||||
_componentSettingsSnapshot = _componentSettings?.Load()
|
||||
?? new ComponentSettingsSnapshot();
|
||||
}
|
||||
|
||||
private void RefreshUI()
|
||||
{
|
||||
if (!_isAttached) return;
|
||||
|
||||
var hasNotifications = _notificationService?.GetNotifications().Count > 0;
|
||||
PrivacyOverlay.IsVisible = _isPrivacyMode && hasNotifications && _notificationService?.GetUnreadCount() > 0;
|
||||
NotificationListPanel.IsVisible = !PrivacyOverlay.IsVisible;
|
||||
|
||||
var unreadCount = _notificationService?.GetUnreadCount() ?? 0;
|
||||
UnreadBadge.IsVisible = unreadCount > 0;
|
||||
UnreadCountText.Text = unreadCount.ToString();
|
||||
|
||||
ClearButton.IsVisible = _componentSettingsSnapshot.NotificationBoxShowClearButton
|
||||
&& hasNotifications;
|
||||
|
||||
UpdateStatusText();
|
||||
RenderNotifications();
|
||||
}
|
||||
|
||||
private void RenderNotifications()
|
||||
{
|
||||
NotificationListPanel.Children.Clear();
|
||||
_notificationControls.Clear();
|
||||
|
||||
if (_notificationService == null)
|
||||
{
|
||||
EmptyStateText.IsVisible = true;
|
||||
EmptyStateText.Text = "通知服务未启动";
|
||||
return;
|
||||
}
|
||||
|
||||
var notifications = _notificationService.GetNotifications();
|
||||
|
||||
if (notifications.Count == 0)
|
||||
{
|
||||
EmptyStateText.IsVisible = true;
|
||||
EmptyStateText.Text = "暂无通知";
|
||||
return;
|
||||
}
|
||||
|
||||
EmptyStateText.IsVisible = false;
|
||||
|
||||
notifications = ApplySorting(notifications);
|
||||
|
||||
var maxCount = _componentSettingsSnapshot.NotificationBoxMaxDisplayCount;
|
||||
notifications = notifications.Take(maxCount).ToList();
|
||||
|
||||
if (_componentSettingsSnapshot.NotificationBoxGroupByApp)
|
||||
{
|
||||
RenderGroupedNotifications(notifications);
|
||||
}
|
||||
else
|
||||
{
|
||||
RenderFlatNotifications(notifications);
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<NotificationItem> ApplySorting(IReadOnlyList<NotificationItem> notifications)
|
||||
{
|
||||
return _componentSettingsSnapshot.NotificationBoxSortOrder switch
|
||||
{
|
||||
"TimeAsc" => notifications.OrderBy(n => n.ReceivedTime).ToList(),
|
||||
"AppGroup" => notifications.OrderBy(n => n.AppName).ThenByDescending(n => n.ReceivedTime).ToList(),
|
||||
_ => notifications.OrderByDescending(n => n.ReceivedTime).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private void RenderFlatNotifications(IReadOnlyList<NotificationItem> notifications)
|
||||
{
|
||||
foreach (var notification in notifications)
|
||||
{
|
||||
var control = CreateNotificationControl(notification);
|
||||
NotificationListPanel.Children.Add(control);
|
||||
_notificationControls.Add(control);
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderGroupedNotifications(IReadOnlyList<NotificationItem> notifications)
|
||||
{
|
||||
var grouped = notifications.GroupBy(n => n.AppName).ToList();
|
||||
|
||||
foreach (var group in grouped)
|
||||
{
|
||||
var groupHeader = new TextBlock
|
||||
{
|
||||
Text = group.Key,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
FontSize = 11,
|
||||
Foreground = new SolidColorBrush(Color.Parse("#8B95A5")),
|
||||
Margin = new Thickness(0, 6, 0, 3)
|
||||
};
|
||||
NotificationListPanel.Children.Add(groupHeader);
|
||||
|
||||
foreach (var notification in group)
|
||||
{
|
||||
var control = CreateNotificationControl(notification);
|
||||
NotificationListPanel.Children.Add(control);
|
||||
_notificationControls.Add(control);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private NotificationItemControl CreateNotificationControl(NotificationItem notification)
|
||||
{
|
||||
var control = new NotificationItemControl(notification, _componentSettingsSnapshot, _isNightVisual);
|
||||
control.Clicked += OnNotificationClicked;
|
||||
control.MarkAsRead += OnMarkAsRead;
|
||||
return control;
|
||||
}
|
||||
|
||||
private void OnNotificationReceived(object? sender, NotificationItem notification)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (!_isAttached) return;
|
||||
RefreshUI();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnNotificationRemoved(object? sender, string notificationId)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (!_isAttached) return;
|
||||
RefreshUI();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnNotificationClicked(object? sender, NotificationItem notification)
|
||||
{
|
||||
}
|
||||
|
||||
private void OnMarkAsRead(object? sender, NotificationItem notification)
|
||||
{
|
||||
_notificationService?.MarkAsRead(notification.Id);
|
||||
RefreshUI();
|
||||
}
|
||||
|
||||
private void OnClearButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_notificationService?.ClearAll();
|
||||
RefreshUI();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void UpdateStatusText()
|
||||
{
|
||||
var total = _notificationService?.GetNotifications().Count ?? 0;
|
||||
var max = _componentSettingsSnapshot.NotificationBoxMaxDisplayCount;
|
||||
StatusTextBlock.Text = $"共 {total} 条" + (total > max ? $"(显{max})" : "");
|
||||
}
|
||||
|
||||
private void ClearSelection()
|
||||
{
|
||||
foreach (var control in _notificationControls)
|
||||
{
|
||||
control.IsSelected = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAdaptiveLayout()
|
||||
{
|
||||
var scale = Math.Clamp(_currentCellSize / 48.0, 0.7, 1.8);
|
||||
var fontScale = Math.Clamp(scale, 0.8, 1.4);
|
||||
|
||||
var cornerRadius = ResolveUnifiedMainRadiusValue();
|
||||
RootBorder.CornerRadius = new CornerRadius(cornerRadius);
|
||||
CardBorder.CornerRadius = new CornerRadius(cornerRadius);
|
||||
|
||||
CardBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#1B2129") : Color.Parse("#FCFCFD"));
|
||||
|
||||
HeaderTextBlock.FontSize = 15 * fontScale;
|
||||
HeaderIcon.FontSize = 16 * fontScale;
|
||||
UnreadCountText.FontSize = 11 * fontScale;
|
||||
EmptyStateText.FontSize = 13 * fontScale;
|
||||
StatusTextBlock.FontSize = 11 * fontScale;
|
||||
|
||||
var padding = Math.Clamp(12 * scale, 8, 20);
|
||||
var verticalPadding = Math.Clamp(10 * scale, 6, 16);
|
||||
CardBorder.Padding = new Thickness(padding, verticalPadding);
|
||||
|
||||
foreach (var control in _notificationControls)
|
||||
{
|
||||
control.UpdateTheme(_isNightVisual, fontScale);
|
||||
}
|
||||
}
|
||||
|
||||
private static double ResolveUnifiedMainRadiusValue() =>
|
||||
HostAppearanceThemeProvider.GetOrCreate().GetCurrent().CornerRadiusTokens.Lg.TopLeft;
|
||||
}
|
||||
|
||||
public class NotificationItemControl : Border
|
||||
{
|
||||
private readonly NotificationItem _item;
|
||||
private readonly ComponentSettingsSnapshot _settings;
|
||||
private bool _isPointerPressed;
|
||||
private Point _pointerPressedPosition;
|
||||
private bool _isNightVisual;
|
||||
|
||||
public NotificationItemControl(NotificationItem item, ComponentSettingsSnapshot settings, bool isNightVisual)
|
||||
{
|
||||
_item = item;
|
||||
_settings = settings;
|
||||
_isNightVisual = isNightVisual;
|
||||
|
||||
Background = _item.IsRead
|
||||
? new SolidColorBrush(isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#F5F5F5"))
|
||||
: new SolidColorBrush(isNightVisual ? Color.Parse("#3D4250") : Color.Parse("#FFFFFF"));
|
||||
CornerRadius = new CornerRadius(6);
|
||||
Padding = new Thickness(10, 6);
|
||||
Cursor = new Cursor(StandardCursorType.Hand);
|
||||
BorderBrush = _item.IsRead
|
||||
? new SolidColorBrush(Colors.Transparent)
|
||||
: new SolidColorBrush(Color.Parse("#E24B2D"));
|
||||
BorderThickness = _item.IsRead ? new Thickness(0) : new Thickness(2, 0, 0, 0);
|
||||
|
||||
BuildUI();
|
||||
|
||||
PointerPressed += OnPointerPressed;
|
||||
PointerReleased += OnPointerReleased;
|
||||
}
|
||||
|
||||
private void BuildUI()
|
||||
{
|
||||
var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("Auto,*,Auto") };
|
||||
|
||||
if (_settings.NotificationBoxShowAppIcon)
|
||||
{
|
||||
var iconBorder = new Border
|
||||
{
|
||||
Width = 28,
|
||||
Height = 28,
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#4D5560") : Color.Parse("#E8EAED")),
|
||||
Margin = new Thickness(0, 0, 8, 0)
|
||||
};
|
||||
var iconText = new TextBlock
|
||||
{
|
||||
Text = _item.AppName.Length > 0 ? _item.AppName[0].ToString() : "?",
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327"))
|
||||
};
|
||||
iconBorder.Child = iconText;
|
||||
grid.Children.Add(iconBorder);
|
||||
}
|
||||
|
||||
var contentPanel = new StackPanel { Spacing = 1 };
|
||||
Grid.SetColumn(contentPanel, 1);
|
||||
|
||||
var titleBlock = new TextBlock
|
||||
{
|
||||
Text = _item.Title,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
FontSize = 12,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
MaxLines = 1,
|
||||
Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327"))
|
||||
};
|
||||
|
||||
var contentBlock = new TextBlock
|
||||
{
|
||||
Text = _item.Content,
|
||||
FontSize = 11,
|
||||
Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#5E6671")),
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
MaxLines = 2,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
contentPanel.Children.Add(titleBlock);
|
||||
if (!string.IsNullOrWhiteSpace(_item.Content))
|
||||
{
|
||||
contentPanel.Children.Add(contentBlock);
|
||||
}
|
||||
grid.Children.Add(contentPanel);
|
||||
|
||||
if (_settings.NotificationBoxShowTimestamp)
|
||||
{
|
||||
var timeText = _settings.NotificationBoxTimeFormat == "Relative"
|
||||
? GetRelativeTime(_item.ReceivedTime)
|
||||
: _item.ReceivedTime.ToString("HH:mm");
|
||||
|
||||
var timeBlock = new TextBlock
|
||||
{
|
||||
Text = timeText,
|
||||
FontSize = 10,
|
||||
Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#8B95A5") : Color.Parse("#8B95A5")),
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Top,
|
||||
Margin = new Thickness(6, 0, 0, 0)
|
||||
};
|
||||
Grid.SetColumn(timeBlock, 2);
|
||||
grid.Children.Add(timeBlock);
|
||||
}
|
||||
|
||||
Child = grid;
|
||||
}
|
||||
|
||||
public void UpdateTheme(bool isNightVisual, double fontScale)
|
||||
{
|
||||
_isNightVisual = isNightVisual;
|
||||
Background = _item.IsRead
|
||||
? new SolidColorBrush(isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#F5F5F5"))
|
||||
: new SolidColorBrush(isNightVisual ? Color.Parse("#3D4250") : Color.Parse("#FFFFFF"));
|
||||
|
||||
if (Child is Grid grid)
|
||||
{
|
||||
foreach (var child in grid.Children)
|
||||
{
|
||||
if (child is StackPanel panel)
|
||||
{
|
||||
foreach (var textBlock in panel.Children.OfType<TextBlock>())
|
||||
{
|
||||
textBlock.FontSize *= fontScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
_isPointerPressed = true;
|
||||
_pointerPressedPosition = e.GetPosition(this);
|
||||
IsSelected = true;
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
if (!_isPointerPressed) return;
|
||||
|
||||
_isPointerPressed = false;
|
||||
var releasePosition = e.GetPosition(this);
|
||||
var distance = Math.Sqrt(
|
||||
Math.Pow(releasePosition.X - _pointerPressedPosition.X, 2) +
|
||||
Math.Pow(releasePosition.Y - _pointerPressedPosition.Y, 2));
|
||||
|
||||
if (distance < 5)
|
||||
{
|
||||
Clicked?.Invoke(this, _item);
|
||||
MarkAsRead?.Invoke(this, _item);
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private static string GetRelativeTime(DateTime time)
|
||||
{
|
||||
var diff = DateTime.Now - time;
|
||||
|
||||
if (diff.TotalMinutes < 1) return "刚刚";
|
||||
if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes}分前";
|
||||
if (diff.TotalHours < 24) return $"{(int)diff.TotalHours}小时前";
|
||||
return $"{(int)diff.TotalDays}天前";
|
||||
}
|
||||
|
||||
public bool IsSelected { get; set; }
|
||||
|
||||
public event EventHandler<NotificationItem>? Clicked;
|
||||
public event EventHandler<NotificationItem>? MarkAsRead;
|
||||
}
|
||||
|
||||
public static class NotificationListenerServiceProvider
|
||||
{
|
||||
private static NotificationListenerService? _instance;
|
||||
|
||||
public static NotificationListenerService GetOrCreate(ISettingsService settingsService)
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = new NotificationListenerService(settingsService);
|
||||
_instance.InitializeAsync().ConfigureAwait(false);
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,10 @@ public partial class MainWindow
|
||||
Color PressedColor,
|
||||
Color DividerColor);
|
||||
|
||||
private readonly IPowerManagementService _powerService = PowerManagementServiceFactory.GetOrCreate();
|
||||
private bool _isPowerMenuOpen;
|
||||
private bool _isPowerMenuAnimating;
|
||||
|
||||
private void InitializeTaskbarProfileFlyout()
|
||||
{
|
||||
if (TaskbarProfileButton is null || TaskbarProfilePopup is null)
|
||||
@@ -98,6 +102,16 @@ public partial class MainWindow
|
||||
TaskbarProfileDisplayNameTextBlock.Text = profile.DisplayName;
|
||||
TaskbarProfileSettingsActionTextBlock.Text = L("tooltip.open_settings", "Settings");
|
||||
TaskbarProfileDesktopEditActionTextBlock.Text = L("button.component_library", "Edit Desktop");
|
||||
TaskbarProfilePowerActionTextBlock.Text = L("power.menu", "Power");
|
||||
TaskbarPowerTitleTextBlock.Text = L("power.title", "Power");
|
||||
TaskbarPowerBackTextBlock.Text = L("power.back", "Back");
|
||||
PowerShutdownTextBlock.Text = L("power.shutdown", "Shutdown");
|
||||
PowerRestartTextBlock.Text = L("power.restart", "Restart");
|
||||
PowerLogoutTextBlock.Text = L("power.logout", "Log Out");
|
||||
PowerSleepTextBlock.Text = L("power.sleep", "Sleep");
|
||||
PowerLockTextBlock.Text = L("power.lock_screen", "Lock Screen");
|
||||
|
||||
UpdatePowerMenuVisibility();
|
||||
ApplyTaskbarProfilePopupTheme(_appearanceThemeService.GetCurrent());
|
||||
|
||||
ToolTip.SetTip(TaskbarProfileButton, profile.DisplayName);
|
||||
@@ -216,6 +230,7 @@ public partial class MainWindow
|
||||
return;
|
||||
}
|
||||
|
||||
ResetPowerMenuState();
|
||||
RefreshTaskbarProfilePresentation();
|
||||
TaskbarProfilePopup.IsOpen = true;
|
||||
}
|
||||
@@ -279,6 +294,202 @@ public partial class MainWindow
|
||||
app?.OpenIndependentSettingsModule("MainWindowTaskbar");
|
||||
}
|
||||
|
||||
private void OnPowerMenuEnterClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
EnterPowerMenu();
|
||||
}
|
||||
|
||||
private void OnPowerMenuBackClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
ExitPowerMenu();
|
||||
}
|
||||
|
||||
private void ResetPowerMenuState()
|
||||
{
|
||||
_isPowerMenuOpen = false;
|
||||
_isPowerMenuAnimating = false;
|
||||
|
||||
if (TaskbarProfileMainPanel is not null)
|
||||
{
|
||||
TaskbarProfileMainPanel.IsVisible = true;
|
||||
TaskbarProfileMainPanel.Opacity = 1d;
|
||||
}
|
||||
|
||||
if (TaskbarProfilePowerPanel is not null)
|
||||
{
|
||||
TaskbarProfilePowerPanel.IsVisible = false;
|
||||
TaskbarProfilePowerPanel.Opacity = 0d;
|
||||
var transform = TaskbarProfilePowerPanel.RenderTransform as TranslateTransform;
|
||||
if (transform is not null) transform.X = 340d;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePowerMenuVisibility()
|
||||
{
|
||||
var supported = _powerService.IsShutdownSupported ||
|
||||
_powerService.IsRestartSupported ||
|
||||
_powerService.IsLogoutSupported ||
|
||||
_powerService.IsSleepSupported ||
|
||||
_powerService.IsLockSupported;
|
||||
|
||||
if (TaskbarProfilePowerActionButton is not null)
|
||||
{
|
||||
TaskbarProfilePowerActionButton.IsVisible = supported;
|
||||
}
|
||||
}
|
||||
|
||||
private async void EnterPowerMenu()
|
||||
{
|
||||
if (_isPowerMenuAnimating || _isPowerMenuOpen || TaskbarProfileMainPanel is null || TaskbarProfilePowerPanel is null)
|
||||
return;
|
||||
|
||||
_isPowerMenuAnimating = true;
|
||||
|
||||
TaskbarProfilePowerPanel.IsVisible = true;
|
||||
TaskbarProfilePowerPanel.Opacity = 0d;
|
||||
var powerTransform = TaskbarProfilePowerPanel.RenderTransform as TranslateTransform;
|
||||
if (powerTransform is not null) powerTransform.X = 340d;
|
||||
|
||||
await Task.Delay(16);
|
||||
|
||||
TaskbarProfileMainPanel.Opacity = 0d;
|
||||
TaskbarProfilePowerPanel.Opacity = 1d;
|
||||
if (powerTransform is not null) powerTransform.X = 0d;
|
||||
|
||||
await Task.Delay(280);
|
||||
|
||||
TaskbarProfileMainPanel.IsVisible = false;
|
||||
_isPowerMenuOpen = true;
|
||||
_isPowerMenuAnimating = false;
|
||||
}
|
||||
|
||||
private async void ExitPowerMenu()
|
||||
{
|
||||
if (_isPowerMenuAnimating || !_isPowerMenuOpen || TaskbarProfileMainPanel is null || TaskbarProfilePowerPanel is null)
|
||||
return;
|
||||
|
||||
_isPowerMenuAnimating = true;
|
||||
|
||||
TaskbarProfileMainPanel.IsVisible = true;
|
||||
TaskbarProfileMainPanel.Opacity = 0d;
|
||||
var powerTransform = TaskbarProfilePowerPanel.RenderTransform as TranslateTransform;
|
||||
if (powerTransform is not null) powerTransform.X = 0d;
|
||||
|
||||
await Task.Delay(16);
|
||||
|
||||
TaskbarProfileMainPanel.Opacity = 1d;
|
||||
TaskbarProfilePowerPanel.Opacity = 0d;
|
||||
if (powerTransform is not null) powerTransform.X = 340d;
|
||||
|
||||
await Task.Delay(280);
|
||||
|
||||
TaskbarProfilePowerPanel.IsVisible = false;
|
||||
_isPowerMenuOpen = false;
|
||||
_isPowerMenuAnimating = false;
|
||||
}
|
||||
|
||||
private async void OnPowerShutdownClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
ClosePopupIfOpen();
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
_powerService.ShowNativePowerUI(PowerAction.Shutdown);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ShowPowerConfirmDialogAsync(L("power.shutdown_confirm_title", "Shutdown"),
|
||||
L("power.shutdown_confirm_message", "Are you sure you want to shut down this computer?"),
|
||||
() => _powerService.ShutdownAsync());
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnPowerRestartClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
ClosePopupIfOpen();
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
_powerService.ShowNativePowerUI(PowerAction.Restart);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ShowPowerConfirmDialogAsync(L("power.restart_confirm_title", "Restart"),
|
||||
L("power.restart_confirm_message", "Are you sure you want to restart this computer?"),
|
||||
() => _powerService.RestartAsync());
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnPowerLogoutClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
ClosePopupIfOpen();
|
||||
|
||||
await ShowPowerConfirmDialogAsync(L("power.logout_confirm_title", "Log Out"),
|
||||
L("power.logout_confirm_message", "Are you sure you want to log out?"),
|
||||
() => _powerService.LogoutAsync());
|
||||
}
|
||||
|
||||
private async void OnPowerSleepClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
ClosePopupIfOpen();
|
||||
|
||||
await ShowPowerConfirmDialogAsync(L("power.sleep_confirm_title", "Sleep"),
|
||||
L("power.sleep_confirm_message", "Are you sure you want to put the computer to sleep?"),
|
||||
() => _powerService.SleepAsync());
|
||||
}
|
||||
|
||||
private async void OnPowerLockClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
ClosePopupIfOpen();
|
||||
await _powerService.LockAsync();
|
||||
}
|
||||
|
||||
private async Task ShowPowerConfirmDialogAsync(string title, string message, Func<Task> action)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Title = title,
|
||||
Content = message,
|
||||
PrimaryButtonText = L("power.confirm_yes", "Yes"),
|
||||
SecondaryButtonText = L("power.confirm_cancel", "Cancel")
|
||||
};
|
||||
|
||||
var result = await dialog.ShowAsync(this);
|
||||
if (result == ContentDialogResult.Primary)
|
||||
{
|
||||
await action();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Error("PowerMenu", $"Dialog error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ClosePopupIfOpen()
|
||||
{
|
||||
if (TaskbarProfilePopup is not null && TaskbarProfilePopup.IsOpen)
|
||||
{
|
||||
TaskbarProfilePopup.IsOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCloseComponentLibraryClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_componentLibraryWindowService.Close(this);
|
||||
|
||||
@@ -398,69 +398,215 @@
|
||||
<Border x:Name="TaskbarProfilePopupPanel"
|
||||
Classes="taskbar-profile-popup-panel"
|
||||
Margin="0,0,0,10">
|
||||
<StackPanel Width="280"
|
||||
Spacing="12">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="12">
|
||||
<Border x:Name="TaskbarProfileHeaderAvatarBorder"
|
||||
Classes="taskbar-profile-popup-avatar"
|
||||
Width="44"
|
||||
Height="44"
|
||||
ClipToBounds="True">
|
||||
<Grid>
|
||||
<Image x:Name="TaskbarProfileHeaderAvatarImage"
|
||||
Stretch="UniformToFill"
|
||||
IsVisible="False" />
|
||||
<TextBlock x:Name="TaskbarProfileHeaderAvatarFallbackText"
|
||||
Classes="taskbar-profile-popup-primary"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Text="U" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<Grid Width="340">
|
||||
<Grid x:Name="TaskbarProfileMainPanel"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid.Transitions>
|
||||
<Transitions>
|
||||
<DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Fast}" />
|
||||
</Transitions>
|
||||
</Grid.Transitions>
|
||||
<StackPanel Spacing="12">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="12">
|
||||
<Border x:Name="TaskbarProfileHeaderAvatarBorder"
|
||||
Classes="taskbar-profile-popup-avatar"
|
||||
Width="44"
|
||||
Height="44"
|
||||
ClipToBounds="True">
|
||||
<Grid>
|
||||
<Image x:Name="TaskbarProfileHeaderAvatarImage"
|
||||
Stretch="UniformToFill"
|
||||
IsVisible="False" />
|
||||
<TextBlock x:Name="TaskbarProfileHeaderAvatarFallbackText"
|
||||
Classes="taskbar-profile-popup-primary"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Text="U" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="TaskbarProfileDisplayNameTextBlock"
|
||||
Classes="taskbar-profile-popup-title"
|
||||
Text="User" />
|
||||
<StackPanel Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="TaskbarProfileDisplayNameTextBlock"
|
||||
Classes="taskbar-profile-popup-title"
|
||||
Text="User" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Border x:Name="TaskbarProfilePopupDivider"
|
||||
Height="1"
|
||||
Background="{DynamicResource TaskbarProfilePopupDividerBrush}" />
|
||||
|
||||
<Button x:Name="TaskbarProfileSettingsActionButton"
|
||||
Classes="taskbar-profile-popup-action"
|
||||
Click="OnOpenSettingsClick">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="12">
|
||||
<mi:MaterialIcon Classes="taskbar-profile-popup-action-icon"
|
||||
Kind="Settings" />
|
||||
<TextBlock x:Name="TaskbarProfileSettingsActionTextBlock"
|
||||
Grid.Column="1"
|
||||
Classes="taskbar-profile-popup-action-text"
|
||||
Text="Settings" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button x:Name="TaskbarProfileDesktopEditActionButton"
|
||||
Classes="taskbar-profile-popup-action"
|
||||
Click="OnOpenComponentLibraryClick">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="12">
|
||||
<mi:MaterialIcon Classes="taskbar-profile-popup-action-icon"
|
||||
Kind="Pencil" />
|
||||
<TextBlock x:Name="TaskbarProfileDesktopEditActionTextBlock"
|
||||
Grid.Column="1"
|
||||
Classes="taskbar-profile-popup-action-text"
|
||||
Text="Edit Desktop" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button x:Name="TaskbarProfilePowerActionButton"
|
||||
Classes="taskbar-profile-popup-action"
|
||||
Click="OnPowerMenuEnterClick">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="12">
|
||||
<mi:MaterialIcon Classes="taskbar-profile-popup-action-icon"
|
||||
Kind="Power" />
|
||||
<TextBlock x:Name="TaskbarProfilePowerActionTextBlock"
|
||||
Grid.Column="1"
|
||||
Classes="taskbar-profile-popup-action-text"
|
||||
Text="Power" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Border x:Name="TaskbarProfilePopupDivider"
|
||||
Height="1"
|
||||
Background="{DynamicResource TaskbarProfilePopupDividerBrush}" />
|
||||
<Grid x:Name="TaskbarProfilePowerPanel"
|
||||
IsVisible="False"
|
||||
Opacity="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid.Transitions>
|
||||
<Transitions>
|
||||
<DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Fast}" />
|
||||
</Transitions>
|
||||
</Grid.Transitions>
|
||||
<Grid.RenderTransform>
|
||||
<TranslateTransform>
|
||||
<TranslateTransform.Transitions>
|
||||
<Transitions>
|
||||
<DoubleTransition Property="X"
|
||||
Duration="{StaticResource FluttermotionToken.Duration.Fast}"
|
||||
Easing="0.22,1,0.36,1" />
|
||||
</Transitions>
|
||||
</TranslateTransform.Transitions>
|
||||
</TranslateTransform>
|
||||
</Grid.RenderTransform>
|
||||
<StackPanel Spacing="8">
|
||||
<Button x:Name="TaskbarPowerBackButton"
|
||||
Padding="4,6"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
HorizontalAlignment="Left"
|
||||
Click="OnPowerMenuBackClick">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<fi:SymbolIcon Classes="icon-s"
|
||||
Symbol="ArrowLeft"
|
||||
IconVariant="Regular" />
|
||||
<TextBlock x:Name="TaskbarPowerBackTextBlock"
|
||||
VerticalAlignment="Center"
|
||||
Text="Back" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button x:Name="TaskbarProfileSettingsActionButton"
|
||||
Classes="taskbar-profile-popup-action"
|
||||
Click="OnOpenSettingsClick">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="12">
|
||||
<mi:MaterialIcon Classes="taskbar-profile-popup-action-icon"
|
||||
Kind="Settings" />
|
||||
<TextBlock x:Name="TaskbarProfileSettingsActionTextBlock"
|
||||
Grid.Column="1"
|
||||
Classes="taskbar-profile-popup-action-text"
|
||||
Text="Settings" />
|
||||
</Grid>
|
||||
</Button>
|
||||
<TextBlock x:Name="TaskbarPowerTitleTextBlock"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TaskbarProfilePopupTextBrush}"
|
||||
Margin="2,6,0,0"
|
||||
Text="Power" />
|
||||
|
||||
<Button x:Name="TaskbarProfileDesktopEditActionButton"
|
||||
Classes="taskbar-profile-popup-action"
|
||||
Click="OnOpenComponentLibraryClick">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="12">
|
||||
<mi:MaterialIcon Classes="taskbar-profile-popup-action-icon"
|
||||
Kind="Pencil" />
|
||||
<TextBlock x:Name="TaskbarProfileDesktopEditActionTextBlock"
|
||||
Grid.Column="1"
|
||||
Classes="taskbar-profile-popup-action-text"
|
||||
Text="Edit Desktop" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<Border Height="1"
|
||||
Background="{DynamicResource TaskbarProfilePopupDividerBrush}"
|
||||
Margin="0,4" />
|
||||
|
||||
<Button x:Name="PowerShutdownButton"
|
||||
Classes="taskbar-profile-popup-action"
|
||||
Click="OnPowerShutdownClick">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="14">
|
||||
<mi:MaterialIcon Classes="taskbar-profile-popup-action-icon"
|
||||
Kind="Power" />
|
||||
<TextBlock x:Name="PowerShutdownTextBlock"
|
||||
Grid.Column="1"
|
||||
Classes="taskbar-profile-popup-action-text"
|
||||
Text="Shutdown" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button x:Name="PowerRestartButton"
|
||||
Classes="taskbar-profile-popup-action"
|
||||
Click="OnPowerRestartClick">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="14">
|
||||
<mi:MaterialIcon Classes="taskbar-profile-popup-action-icon"
|
||||
Kind="Refresh" />
|
||||
<TextBlock x:Name="PowerRestartTextBlock"
|
||||
Grid.Column="1"
|
||||
Classes="taskbar-profile-popup-action-text"
|
||||
Text="Restart" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button x:Name="PowerLogoutButton"
|
||||
Classes="taskbar-profile-popup-action"
|
||||
Click="OnPowerLogoutClick">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="14">
|
||||
<mi:MaterialIcon Classes="taskbar-profile-popup-action-icon"
|
||||
Kind="ExitToApp" />
|
||||
<TextBlock x:Name="PowerLogoutTextBlock"
|
||||
Grid.Column="1"
|
||||
Classes="taskbar-profile-popup-action-text"
|
||||
Text="Log Out" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button x:Name="PowerSleepButton"
|
||||
Classes="taskbar-profile-popup-action"
|
||||
Click="OnPowerSleepClick">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="14">
|
||||
<mi:MaterialIcon Classes="taskbar-profile-popup-action-icon"
|
||||
Kind="WeatherNight" />
|
||||
<TextBlock x:Name="PowerSleepTextBlock"
|
||||
Grid.Column="1"
|
||||
Classes="taskbar-profile-popup-action-text"
|
||||
Text="Sleep" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button x:Name="PowerLockButton"
|
||||
Classes="taskbar-profile-popup-action"
|
||||
Click="OnPowerLockClick">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="14">
|
||||
<mi:MaterialIcon Classes="taskbar-profile-popup-action-icon"
|
||||
Kind="Lock" />
|
||||
<TextBlock x:Name="PowerLockTextBlock"
|
||||
Grid.Column="1"
|
||||
Classes="taskbar-profile-popup-action-text"
|
||||
Text="Lock Screen" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Popup>
|
||||
</Grid>
|
||||
|
||||
Reference in New Issue
Block a user