13 Commits

9 changed files with 387 additions and 205 deletions
+22 -8
View File
@@ -1,7 +1,7 @@
# AGENTS.md
Guidance for AI agents working in this repo. Verified against `CMakeLists.txt`,
`package/metadata.json`, and the installed macro file at
`package/metadata.json.in`, and the installed macro file at
`/usr/lib/x86_64-linux-gnu/cmake/DDEShell/DDEShellPackageMacros.cmake`.
## What this is
@@ -68,11 +68,14 @@ then restart `dde-shell` (it discovers applets by scanning the install dir for
├── CMakeLists.txt # Build configuration
├── install.sh # Install script
├── build-deb.sh # Deb packaging script
├── networkmonitorapplet.h # C++ backend header
├── networkmonitorapplet.cpp # C++ backend implementation
├── src/
├── networkmonitorapplet.h # C++ backend header
│ └── networkmonitorapplet.cpp # C++ backend implementation
├── package/
│ ├── metadata.json # Plugin metadata
── networkview.qml # QML UI
│ ├── metadata.json.in # Plugin metadata 模板(由 CMake 生成 metadata.json
── networkview.qml # QML UI 主入口
│ └── components/ # 拆分出的子组件
│ └── AboutWindow.qml # 关于窗口组件
├── docs/ # Design docs and implementation plans
└── AGENTS.md # This file
```
@@ -112,15 +115,26 @@ Inherits from `DApplet`, provides:
|---|---|---|
| `CMakeLists.txt` | `add_library(...)` target | `space.jokul.JNetApplet` |
| `CMakeLists.txt` | `PLUGIN_ID` | `space.jokul.JNetApplet` |
| `package/metadata.json` | `Plugin.Id` | `space.jokul.JNetApplet` |
| `package/metadata.json.in` | `Plugin.Id` | `space.jokul.JNetApplet` |
`Plugin.Url` (`networkview.qml`) is a path **relative to `package/`**, not an identifier.
Keep it pointing at the entry QML.
## Required metadata fields (QML applet)
`metadata.json` must contain `Plugin.Version`, `Plugin.Id`, `Plugin.Url`, and
`Plugin.Parent`. The current file is complete — don't drop any.
`package/metadata.json.in` is the template; CMake's `configure_file()` generates the
final `metadata.json` in the build directory at configure time. The template must
contain `Plugin.Version`, `Plugin.Id`, `Plugin.Url`, and `Plugin.Parent`. Don't drop any.
## Version management
**Single source of truth**: `CMakeLists.txt` line 3 `project(JNetApplet VERSION x.y.z)`.
- `package/metadata.json.in` uses `@PROJECT_VERSION@` placeholder, substituted at
configure time -> installed `metadata.json` always matches.
- `build-deb.sh` extracts the version directly from `CMakeLists.txt` via grep.
- **To bump the version**: edit only the `project(... VERSION ...)` line in
`CMakeLists.txt`. All downstream consumers (install, deb) pick it up automatically.
## Conventions
+16 -5
View File
@@ -14,14 +14,15 @@ find_package(DDEShell REQUIRED)
set(PLUGIN_ID "space.jokul.JNetApplet")
add_library(space.jokul.JNetApplet SHARED
networkmonitorapplet.h
networkmonitorapplet.cpp
src/networkmonitorapplet.h
src/networkmonitorapplet.cpp
)
set_target_properties(space.jokul.JNetApplet PROPERTIES PREFIX "")
# 头文件搜索路径指向 src/,使 #include "networkmonitorapplet.h" 可被外部解析
target_include_directories(space.jokul.JNetApplet PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/src
/usr/include/dde-shell
)
@@ -36,5 +37,15 @@ target_link_libraries(space.jokul.JNetApplet PRIVATE
# 安装到dde-shell插件目录
install(TARGETS space.jokul.JNetApplet DESTINATION /usr/lib/x86_64-linux-gnu/dde-shell)
install(FILES package/metadata.json DESTINATION /usr/share/dde-shell/${PLUGIN_ID})
install(FILES package/networkview.qml DESTINATION /usr/share/dde-shell/${PLUGIN_ID})
# 由模板生成 metadata.json,版本号从 project() 取,避免多处手动维护
# 设计原因:版本号唯一源为 CMakeLists.txt 顶部的 project(... VERSION)
# configure_file 在构建目录生成最终 metadata.jsoninstall 安装生成文件
configure_file(package/metadata.json.in ${CMAKE_CURRENT_BINARY_DIR}/metadata.json @ONLY)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/metadata.json DESTINATION /usr/share/dde-shell/${PLUGIN_ID})
install(FILES package/networkview.qml DESTINATION /usr/share/dde-shell/${PLUGIN_ID})
# 安装 components 子目录下所有 QML 组件
# 设计原因:拆分出的子组件需随主 QML 一起安装到 dde-shell 插件目录,
# 否则 networkview.qml 的 import "components" 在运行时找不到类型
install(DIRECTORY package/components DESTINATION /usr/share/dde-shell/${PLUGIN_ID})
+7 -4
View File
@@ -65,11 +65,14 @@ bash build-deb.sh
├── CMakeLists.txt # 构建配置
├── install.sh # 安装脚本
├── build-deb.sh # deb 一键打包脚本
├── networkmonitorapplet.h # C++ 后端头文件
├── networkmonitorapplet.cpp # C++ 后端实现
├── src/
├── networkmonitorapplet.h # C++ 后端头文件
│ └── networkmonitorapplet.cpp # C++ 后端实现
├── package/
│ ├── metadata.json # 插件元数据
── networkview.qml # QML 界面
│ ├── metadata.json.in # 插件元数据模板(由 CMake 生成 metadata.json
── networkview.qml # QML 界面主入口
│ └── components/ # 拆分出的子组件
│ └── AboutWindow.qml # 关于窗口组件
├── docs/ # 设计文档与实现计划
├── README.md # 本文件
└── AGENTS.md # AI 代理工作指引
+38 -12
View File
@@ -4,23 +4,49 @@
set -e
echo "Building JNetApplet..."
INSTALL_DIR="/usr/share/dde-shell/space.jokul.JNetApplet"
SO_DIR="/usr/lib/x86_64-linux-gnu/dde-shell"
# 清理旧的构建目录
echo "=== JNetApplet 安装脚本 ==="
# 1. 清理旧的构建目录
echo "[1/6] 清理构建目录..."
rm -rf build
# 配置CMake
cmake -Bbuild
# 构建
# 2. 配置 + 构建
echo "[2/6] 配置 CMake 并构建..."
cmake -B build
cmake --build build
echo "Installing JNetApplet (requires sudo)..."
# 3. 清理安装目录中残留的旧文件(包括之前重构产生的多余 QML 文件)
echo "[3/6] 清理安装目录中的旧文件..."
if [ -d "$INSTALL_DIR" ]; then
sudo rm -f "$INSTALL_DIR"/*.qml "$INSTALL_DIR"/*.qmlc
fi
# 安装
# 4. 清理 Qt QML 磁盘缓存(关键!否则 dde-shell 可能使用旧的编译缓存)
echo "[4/6] 清理 QML 磁盘缓存..."
rm -rf ~/.cache/preloader/qmlcache/ 2>/dev/null || true
find ~/.cache -path "*/dde-shell*" -name "*.qmlc" -delete 2>/dev/null || true
# 5. 安装
echo "[5/6] 安装插件(需要 sudo..."
sudo cmake --install build
echo "Installation complete!"
echo "Restarting dde-shell..."
systemctl --user restart dde-shell@DDE
echo "Done! Plugin loaded."
# 验证安装结果
echo ""
echo "=== 安装验证 ==="
echo "QML 文件:"
ls -la "$INSTALL_DIR"/*.qml 2>/dev/null || echo " [警告] 未找到 QML 文件!"
echo "动态库:"
ls -la "$SO_DIR"/space.jokul.JNetApplet.so 2>/dev/null || echo " [警告] 未找到 .so 文件!"
echo "metadata.json 版本:"
grep '"Version"' "$INSTALL_DIR/metadata.json" 2>/dev/null || echo " [警告] 未找到 metadata.json"
echo ""
# 6. 重启 dde-shell
echo "[6/6] 重启 dde-shell..."
systemctl --user stop dde-shell@DDE 2>/dev/null || true
sleep 1
systemctl --user start dde-shell@DDE
echo "完成!插件已加载。"
+167
View File
@@ -0,0 +1,167 @@
// SPDX-FileCopyrightText: 2024 MyCompany
//
// SPDX-License-Identifier: LGPL-3.0-or-later
// 关于窗口:独立顶层窗口,桌面居中弹出,展示插件信息
// 从 networkview.qml 抽取,由父组件实例化并调用 show() 触发
// 对外依赖:accentColor(关闭按钮 hover 高亮色),由父组件传入
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.15
Window {
id: root
// 对外依赖:关闭按钮 hover 态文字高亮色,由父组件传入
// 默认值与 networkview.qml 中 root.accentRed 一致,确保独立可用
property color accentColor: Qt.rgba(220 / 255, 38 / 255, 38 / 255, 1)
width: 320
height: 230
visible: false
flags: Qt.FramelessWindowHint | Qt.Window
modality: Qt.NonModal
color: "transparent"
onVisibleChanged: {
if (visible) {
x = (Screen.width - width) / 2
y = (Screen.height - height) / 2
}
}
Rectangle {
anchors.fill: parent
color: "#f5f5f5"
radius: 12
border.width: 1
border.color: "#e0e0e0"
ColumnLayout {
anchors.fill: parent
anchors.margins: 0
spacing: 0
// 自定义标题栏(可拖动)
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 44
color: "transparent"
MouseArea {
anchors.fill: parent
drag.target: root
}
Text {
anchors.left: parent.left
anchors.leftMargin: 16
anchors.verticalCenter: parent.verticalCenter
text: qsTr("关于")
font.pixelSize: 15
font.weight: Font.Bold
color: "#333333"
}
Rectangle {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
width: 28
height: 28
radius: 14
color: aboutCloseMouse.containsMouse ? Qt.rgba(220/255, 38/255, 38/255, 0.15) : "transparent"
Text {
anchors.centerIn: parent
text: "×"
font.pixelSize: 20
font.weight: Font.Bold
color: aboutCloseMouse.containsMouse ? accentColor : "#666666"
}
MouseArea {
id: aboutCloseMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.hide()
}
}
}
// 内容区
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
anchors.margins: 0
spacing: 10
Layout.leftMargin: 24
Layout.rightMargin: 24
Layout.topMargin: 4
// 标题组:插件名 + 英文名紧凑排列
ColumnLayout {
spacing: 2
Layout.alignment: Qt.AlignHCenter
Text {
text: qsTr("网络速度监控")
font.pixelSize: 18
font.weight: Font.Bold
color: "#333333"
Layout.alignment: Qt.AlignHCenter
}
Text {
text: "JNetApplet"
font.pixelSize: 12
color: "#999999"
Layout.alignment: Qt.AlignHCenter
}
}
// 分隔线
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: "#e0e0e0"
}
// 信息行
ColumnLayout {
spacing: 6
Layout.fillWidth: true
Text {
text: qsTr("版本:1.0")
font.pixelSize: 12
color: "#666666"
}
Text {
text: qsTr("作者:Jokul")
font.pixelSize: 12
color: "#666666"
}
Text {
text: qsTr("描述:监控网络速度和流量")
font.pixelSize: 12
color: "#666666"
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
Text {
text: qsTr("仓库:git.jokul.space/Jokul/JNetApplet")
font.pixelSize: 11
color: "#999999"
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
}
}
}
}
}
@@ -1,6 +1,6 @@
{
"Plugin": {
"Version": "1.0",
"Version": "@PROJECT_VERSION@",
"Id": "space.jokul.JNetApplet",
"Url": "networkview.qml",
"Parent": "org.deepin.ds.dock",
+104 -167
View File
@@ -10,17 +10,23 @@ import Qt.labs.platform as Platform
import org.deepin.ds 1.0
import org.deepin.ds.dock 1.0
import org.deepin.dtk 1.0
import "components"
AppletItem {
id: root
objectName: "network monitor applet"
property int dockOrder: 21
property int dockSize: Panel.rootObject.dockItemMaxSize || 48
// 任务栏方向:0=Top, 1=Right, 2=Bottom, 3=Left% 2 为 0 表示水平,1 表示竖向
// 设计原因:dde-shell 规范用法,org.deepin.ds.dock 已在 line 11 导入
property bool isVerticalDock: Panel.position % 2 === 1
// 任务栏宽度稍宽于标准 dock 尺寸,容纳双行速度数值
// 设计原因:固定宽度避免数值长度变化导致插件宽度抖动、箭头位置漂移
implicitWidth: Math.round(dockSize * 1.35)
implicitHeight: dockSize
// 水平任务栏宽度稍宽容纳双行数值,高度匹配 dock 尺寸
// 竖向任务栏:宽度匹配 dock 尺寸(~40px),高度加大容纳竖排字符
// 2.4 倍:dockSize=40 时约 96px,可容纳最多 8 字符的速度字符串(如 "1023.99K"
// 增大字号后需更高以容纳竖排字符
implicitWidth: isVerticalDock ? dockSize : Math.round(dockSize * 1.35)
implicitHeight: isVerticalDock ? Math.round(dockSize * 2.4) : dockSize
readonly property var applet: Applet
readonly property bool ready: applet ? applet.ready : false
@@ -31,6 +37,8 @@ AppletItem {
readonly property string activeInterface: applet ? (applet.activeInterface || "") : ""
// 活动接口的 IPv4 地址,由 C++ 后端 detectIpAddress 持续更新
readonly property string ipAddress: applet ? (applet.ipAddress || "") : ""
// 活动接口的 IPv6 全球地址,由 C++ 后端 detectIpAddress 持续更新
readonly property string ipv6Address: applet ? (applet.ipv6Address || "") : ""
readonly property var networkInterfaces: applet ? (applet.networkInterfaces || []) : []
readonly property var interfaceStats: applet ? (applet.interfaceStats || []) : []
@@ -132,6 +140,7 @@ AppletItem {
// 不给每行固定高度,让 RowLayout 按内容自然高度排列,
// 整组垂直居中于 dock 区域,避免两行间出现过大间隙
Column {
visible: !root.isVerticalDock
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 4
@@ -143,16 +152,16 @@ AppletItem {
Text {
text: "↓"
font.pixelSize: root.dockSize * 0.18
font.pixelSize: root.dockSize * 0.20
color: root.secondaryText
Layout.alignment: Qt.AlignVCenter
}
Text {
text: root.formatSpeedShort(root.downloadSpeed)
font.pixelSize: root.dockSize * 0.22
font.pixelSize: root.dockSize * 0.25
font.weight: Font.Medium
color: root.downloadValueColor
color: root.primaryText
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignLeft
}
@@ -164,28 +173,93 @@ AppletItem {
Text {
text: "↑"
font.pixelSize: root.dockSize * 0.18
font.pixelSize: root.dockSize * 0.20
color: root.secondaryText
Layout.alignment: Qt.AlignVCenter
}
Text {
text: root.formatSpeedShort(root.uploadSpeed)
font.pixelSize: root.dockSize * 0.22
font.pixelSize: root.dockSize * 0.25
font.weight: Font.Medium
color: root.uploadValueColor
color: root.primaryText
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignLeft
}
}
}
// 悬停提示:网卡名 + IP 地址一行展示
// PanelToolTip 不支持 contentItem 覆盖和文本对齐,用默认左对齐 text 属性
// 竖向任务栏布局:双列字符竖排,每列一个方向(下载|上传)
// 设计原因:竖向任务栏宽度仅 ~40px,水平布局的 "↓6.64K" 约 35px 勉强容纳但
// 数值长度变化时易裁切;逐字符竖排每列仅 ~10px,双列 ~25px,舒适容纳且视觉对称
RowLayout {
visible: root.isVerticalDock
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 4
spacing: 4
// 下载列:箭头在上,数值字符从上往下竖排
Column {
spacing: 0
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
Text {
text: "↓"
font.pixelSize: root.dockSize * 0.20
color: root.secondaryText
anchors.horizontalCenter: parent.horizontalCenter
}
Repeater {
// 将 "6.64K" 拆为 ["6", ".", "6", "4", "K"] 逐字符渲染
// 属性绑定:downloadSpeed 变化时 formatSpeedShort 重算,split 自动刷新 model
model: root.formatSpeedShort(root.downloadSpeed).split('')
Text {
text: modelData
font.pixelSize: root.dockSize * 0.25
height: font.pixelSize
font.weight: Font.Medium
color: root.primaryText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
// 上传列:结构与下载列对称,颜色用 uploadValueColor
Column {
spacing: 0
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
Text {
text: "↑"
font.pixelSize: root.dockSize * 0.20
color: root.secondaryText
anchors.horizontalCenter: parent.horizontalCenter
}
Repeater {
model: root.formatSpeedShort(root.uploadSpeed).split('')
Text {
text: modelData
font.pixelSize: root.dockSize * 0.25
height: font.pixelSize
font.weight: Font.Medium
color: root.primaryText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
// 悬停提示:网卡名 + IPv4 单行,IPv6 在第二行(仅有 IPv6 时追加)
// 设计原因:IPv6 地址较长,单行容纳不下;无 IPv6 时保持单行与原视觉一致
PanelToolTip {
id: toolTip
text: root.ready
? (root.activeInterface + " · " + qsTr("IP地址:") + (root.ipAddress || qsTr("无")))
? (root.activeInterface
+ " · IPv4" + (root.ipAddress || qsTr("无"))
+ (root.ipv6Address ? "\nIPv6" + root.ipv6Address : ""))
: qsTr("未检测到网络接口")
toolTipX: DockPanelPositioner.x
toolTipY: DockPanelPositioner.y
@@ -267,8 +341,9 @@ AppletItem {
// 网卡名 + IP 地址卡片背景
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 180
Layout.preferredHeight: 40
Layout.preferredWidth: 300
// 有 IPv6 时增高以容纳第三行;无 IPv6 时保持原 40px
Layout.preferredHeight: root.ipv6Address ? 54 : 40
color: root.cardBackground
radius: 8
border.width: 1
@@ -287,12 +362,21 @@ AppletItem {
}
Text {
text: root.ipAddress ? qsTr("IP地址") + root.ipAddress : ""
text: root.ipAddress ? qsTr("IPv4") + root.ipAddress : ""
font.pixelSize: 13
color: root.secondaryText
visible: root.ipAddress !== ""
Layout.alignment: Qt.AlignHCenter
}
// IPv6 全球地址行,无 IPv6 时隐藏不占位
Text {
text: root.ipv6Address ? qsTr("IPv6") + root.ipv6Address : ""
font.pixelSize: 13
color: root.secondaryText
visible: root.ipv6Address !== ""
Layout.alignment: Qt.AlignHCenter
}
}
}
}
@@ -590,158 +674,11 @@ AppletItem {
}
}
// 关于窗口:独立顶层窗口,在桌面中间弹出,展示作者信息
// 与设置窗口同样的 frameless + 自定义标题栏模式
Window {
// 关于窗口:抽取为独立组件 package/components/AboutWindow.qml
// 依赖通过属性传入:accentColor = root.accentRed
AboutWindow {
id: aboutWindow
width: 320
height: 230
visible: false
flags: Qt.FramelessWindowHint | Qt.Window
modality: Qt.NonModal
color: "transparent"
onVisibleChanged: {
if (visible) {
x = (Screen.width - width) / 2
y = (Screen.height - height) / 2
}
}
Rectangle {
anchors.fill: parent
color: "#f5f5f5"
radius: 12
border.width: 1
border.color: "#e0e0e0"
ColumnLayout {
anchors.fill: parent
anchors.margins: 0
spacing: 0
// 自定义标题栏(可拖动)
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 44
color: "transparent"
MouseArea {
anchors.fill: parent
drag.target: aboutWindow
}
Text {
anchors.left: parent.left
anchors.leftMargin: 16
anchors.verticalCenter: parent.verticalCenter
text: qsTr("关于")
font.pixelSize: 15
font.weight: Font.Bold
color: "#333333"
}
Rectangle {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
width: 28
height: 28
radius: 14
color: aboutCloseMouse.containsMouse ? Qt.rgba(220/255, 38/255, 38/255, 0.15) : "transparent"
Text {
anchors.centerIn: parent
text: "×"
font.pixelSize: 20
font.weight: Font.Bold
color: aboutCloseMouse.containsMouse ? root.accentRed : "#666666"
}
MouseArea {
id: aboutCloseMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: aboutWindow.hide()
}
}
}
// 内容区
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
anchors.margins: 0
spacing: 10
Layout.leftMargin: 24
Layout.rightMargin: 24
Layout.topMargin: 4
// 标题组:插件名 + 英文名紧凑排列
ColumnLayout {
spacing: 2
Layout.alignment: Qt.AlignHCenter
Text {
text: qsTr("网络速度监控")
font.pixelSize: 18
font.weight: Font.Bold
color: "#333333"
Layout.alignment: Qt.AlignHCenter
}
Text {
text: "JNetApplet"
font.pixelSize: 12
color: "#999999"
Layout.alignment: Qt.AlignHCenter
}
}
// 分隔线
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: "#e0e0e0"
}
// 信息行
ColumnLayout {
spacing: 6
Layout.fillWidth: true
Text {
text: qsTr("版本:1.0")
font.pixelSize: 12
color: "#666666"
}
Text {
text: qsTr("作者:Jokul")
font.pixelSize: 12
color: "#666666"
}
Text {
text: qsTr("描述:监控网络速度和流量")
font.pixelSize: 12
color: "#666666"
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
Text {
text: qsTr("仓库:git.jokul.space/Jokul/JNetApplet")
font.pixelSize: 11
color: "#999999"
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
}
}
}
}
accentColor: root.accentRed
}
// 设置窗口:独立顶层窗口,在桌面中间弹出
@@ -110,6 +110,12 @@ QString NetworkMonitorApplet::ipAddress() const
return m_ipAddress;
}
// 返回活动接口的 IPv6 全球地址,供 QML 显示
QString NetworkMonitorApplet::ipv6Address() const
{
return m_ipv6Address;
}
void NetworkMonitorApplet::refresh()
{
readNetworkStats();
@@ -279,21 +285,30 @@ qint64 NetworkMonitorApplet::getActiveTxBytes() const
return m_interfaces[m_activeInterface].txBytes;
}
// 检测活动接口的 IPv4 地址
// 跳过 IPv6 和 loopback取第一个有效 IPv4 地址
// IP 变化时发射 ipAddressChanged 信号通知 QML 更新
// 设计原因:DHCP 续约、网络切换等场景下 IP 可能变化,需持续检测
// 检测活动接口的 IPv4 与 IPv6 地址
// IPv4跳过 loopback,取第一个
// IPv6:跳过 loopback 和 link-local (fe80::),取第一个全球地址
// 任一地址变化时分别 emit 对应信号通知 QML 更新
// 设计原因:DHCP 续约、网络切换、IPv6 SLAAC 等场景下地址可能变化,需持续检测
void NetworkMonitorApplet::detectIpAddress()
{
QString newIp;
QString newIpv6;
if (!m_activeInterface.isEmpty()) {
QNetworkInterface iface = QNetworkInterface::interfaceFromName(m_activeInterface);
for (const QNetworkAddressEntry &entry : iface.addressEntries()) {
// 只取 IPv4 地址,跳过 IPv6 和 loopback
// IPv4:跳过 loopback,取第一个
if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol
&& !entry.ip().isLoopback()) {
&& !entry.ip().isLoopback()
&& newIp.isEmpty()) {
newIp = entry.ip().toString();
break;
}
// IPv6:跳过 loopback 和 link-local (fe80::),取第一个全球地址
if (entry.ip().protocol() == QAbstractSocket::IPv6Protocol
&& !entry.ip().isLoopback()
&& !entry.ip().isLinkLocal()
&& newIpv6.isEmpty()) {
newIpv6 = entry.ip().toString();
}
}
}
@@ -301,6 +316,10 @@ void NetworkMonitorApplet::detectIpAddress()
m_ipAddress = newIp;
emit ipAddressChanged();
}
if (m_ipv6Address != newIpv6) {
m_ipv6Address = newIpv6;
emit ipv6AddressChanged();
}
}
// 判断是否为物理网卡
@@ -40,6 +40,8 @@ class NetworkMonitorApplet : public DApplet
Q_PROPERTY(QString activeInterface READ activeInterface NOTIFY activeInterfaceChanged)
// 活动接口的 IPv4 地址,供 QML 在弹出面板和 tooltip 中显示
Q_PROPERTY(QString ipAddress READ ipAddress NOTIFY ipAddressChanged)
// 活动接口的 IPv6 全球地址(已过滤 link-local 和 loopback),供 QML 显示
Q_PROPERTY(QString ipv6Address READ ipv6Address NOTIFY ipv6AddressChanged)
public:
explicit NetworkMonitorApplet(QObject *parent = nullptr);
@@ -57,6 +59,7 @@ public:
bool ready() const;
QString activeInterface() const;
QString ipAddress() const;
QString ipv6Address() const;
Q_INVOKABLE void refresh();
Q_INVOKABLE void setActiveInterface(const QString &interface);
@@ -69,12 +72,13 @@ signals:
void readyChanged();
void activeInterfaceChanged();
void ipAddressChanged();
void ipv6AddressChanged();
private:
void readNetworkStats();
void calculateSpeed();
void detectInterfaces();
// 检测活动接口的 IPv4 地址,变化时发射 ipAddressChanged
// 检测活动接口的 IPv4 与 IPv6 地址,变化时分别发射对应信号
void detectIpAddress();
// 判断是否为物理网卡(无线 wlp/wlan,有线 enp/eth),
// 用于自动选择时优先真实网卡而非虚拟代理接口(如 Meta/tun0)
@@ -87,6 +91,7 @@ private:
QStringList m_interfaceList;
QString m_activeInterface;
QString m_ipAddress; // 活动接口的 IPv4 地址
QString m_ipv6Address; // 活动接口的 IPv6 全球地址(过滤 link-local
// 速度计算
qint64 m_lastRxBytes;