Compare commits
13 Commits
v1.0.0
...
09e64d4859
| Author | SHA1 | Date | |
|---|---|---|---|
| 09e64d4859 | |||
| d53409808c | |||
| 0f58dcad03 | |||
| 6325fb51cc | |||
| 6bc1a3e3fb | |||
| 903e0f1801 | |||
| 3c92359407 | |||
| 170e602945 | |||
| 02ac3faf85 | |||
| b80769c26e | |||
| 790103698e | |||
| 2beafd4cf1 | |||
| f7281a5f0c |
@@ -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
@@ -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.json,install 安装生成文件
|
||||
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})
|
||||
@@ -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
@@ -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 "完成!插件已加载。"
|
||||
|
||||
@@ -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
@@ -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;
|
||||
Reference in New Issue
Block a user