Compare commits
7 Commits
64efe509fb
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 43cb5238c8 | |||
| ea2081f0a6 | |||
| ab1a5227fa | |||
| 8f5e30532d | |||
| 9b9c26d5d1 | |||
| 6142dc4769 | |||
| 009dd9578f |
+4
-1
@@ -22,4 +22,7 @@ cmake-build-*/
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# AI 工作流内部文档(不纳入版本控制)
|
||||
docs/superpowers/
|
||||
@@ -21,6 +21,8 @@ for the UI. The C++ backend reads `/proc/net/dev` to monitor network traffic.
|
||||
- Taskbar icon displays live speed (changes color at high speeds)
|
||||
- Hover tooltip shows speed summary
|
||||
- Click to open detailed popup
|
||||
- Popup shows active interface IP address, per-interface speed comparison and stats
|
||||
- One-click plugin uninstall from the popup
|
||||
|
||||
## Build & install
|
||||
|
||||
@@ -42,6 +44,14 @@ bash install.sh
|
||||
`cmake --install` needs `sudo` because the default `DDE_SHELL_PACKAGE_INSTALL_DIR`
|
||||
is `/usr/share/dde-shell` (a CMake CACHE variable on this system).
|
||||
|
||||
### Deb packaging
|
||||
|
||||
`build-deb.sh` builds a `.deb` from the version declared in `CMakeLists.txt`:
|
||||
|
||||
```sh
|
||||
bash build-deb.sh # -> jnetapplet_<version>_<arch>.deb
|
||||
```
|
||||
|
||||
After installation, restart dde-shell:
|
||||
|
||||
```sh
|
||||
@@ -57,11 +67,13 @@ 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
|
||||
├── package/
|
||||
│ ├── metadata.json # Plugin metadata
|
||||
│ └── networkview.qml # QML UI
|
||||
├── docs/ # Design docs and implementation plans
|
||||
└── AGENTS.md # This file
|
||||
```
|
||||
|
||||
@@ -90,6 +102,7 @@ Inherits from `DApplet`, provides:
|
||||
- `networkInterfaces`: List of available interfaces
|
||||
- `interfaceStats`: Per-interface statistics
|
||||
- `activeInterface`: Currently selected interface
|
||||
- `ipAddress`: IP address of the active interface
|
||||
- `refresh()`: Manually trigger stats update
|
||||
- `setActiveInterface(name)`: Switch active interface
|
||||
|
||||
|
||||
+2
-1
@@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Quick DBus)
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Quick DBus Network)
|
||||
find_package(Dtk6 REQUIRED COMPONENTS Core)
|
||||
find_package(DDEShell REQUIRED)
|
||||
|
||||
@@ -29,6 +29,7 @@ target_link_libraries(space.jokul.JNetApplet PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Quick
|
||||
Qt6::DBus
|
||||
Qt6::Network
|
||||
Dtk6::Core
|
||||
Dde::Shell
|
||||
)
|
||||
|
||||
@@ -1,2 +1,76 @@
|
||||
# JNetApplet
|
||||
|
||||
基于 dde-shell 的**深度系统任务栏网络速率监控插件**,使用 C++ 后端采集数据、
|
||||
QML 前端渲染界面,实时显示当前网卡的上行/下行速率与累计流量。
|
||||
|
||||
- 插件 ID:`space.jokul.JNetApplet`
|
||||
- 适用环境:deepin 桌面环境(dde-shell 面板)
|
||||
- 数据源:读取 `/proc/net/dev` 计算网络流量
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 实时上行/下行速率监控(1 秒刷新)
|
||||
- 累计上传/下载流量统计
|
||||
- 多网卡支持,可在弹窗中切换活动网卡
|
||||
- 任务栏图标实时显示速率,高速时变色提示
|
||||
- 悬停显示速率摘要,点击展开详情弹窗
|
||||
- 详情弹窗展示活动网卡 IP 地址、各网卡速率对比与统计图表
|
||||
- 支持从弹窗一键卸载插件
|
||||
|
||||
## 构建与安装
|
||||
|
||||
需要配置期存在 `DDEShell` CMake 包(来自 `dde-shell` 开发包)。
|
||||
|
||||
### 方式一:从源码安装
|
||||
|
||||
```sh
|
||||
cmake -B build
|
||||
cmake --build build
|
||||
sudo cmake --install build # -> /usr/share/dde-shell/space.jokul.JNetApplet/
|
||||
```
|
||||
|
||||
或使用安装脚本:
|
||||
|
||||
```sh
|
||||
bash install.sh
|
||||
```
|
||||
|
||||
`cmake --install` 需要 `sudo`,因为默认 `DDE_SHELL_PACKAGE_INSTALL_DIR`
|
||||
为 `/usr/share/dde-shell`(本机 CMake CACHE 变量)。
|
||||
|
||||
安装后重启 dde-shell 使插件生效:
|
||||
|
||||
```sh
|
||||
systemctl --user restart dde-shell@DDE
|
||||
```
|
||||
|
||||
### 方式二:打包为 deb 安装
|
||||
|
||||
仓库提供 `build-deb.sh` 一键打包脚本,会从 `CMakeLists.txt` 读取版本号并构建 deb 包:
|
||||
|
||||
```sh
|
||||
bash build-deb.sh
|
||||
```
|
||||
|
||||
产物为 `jnetapplet_<version>_<arch>.deb`,可直接用 `dpkg -i` 安装。
|
||||
|
||||
## 验证
|
||||
|
||||
项目无测试 / lint / typecheck / CI 流程,验证方式为手动:安装后重启 `dde-shell`
|
||||
(其通过扫描安装目录中的 `metadata.json` 发现插件)。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
├── CMakeLists.txt # 构建配置
|
||||
├── install.sh # 安装脚本
|
||||
├── build-deb.sh # deb 一键打包脚本
|
||||
├── networkmonitorapplet.h # C++ 后端头文件
|
||||
├── networkmonitorapplet.cpp # C++ 后端实现
|
||||
├── package/
|
||||
│ ├── metadata.json # 插件元数据
|
||||
│ └── networkview.qml # QML 界面
|
||||
├── docs/ # 设计文档与实现计划
|
||||
├── README.md # 本文件
|
||||
└── AGENTS.md # AI 代理工作指引
|
||||
```
|
||||
|
||||
Executable
+128
@@ -0,0 +1,128 @@
|
||||
#!/bin/bash
|
||||
|
||||
# JNetApplet deb 一键打包脚本
|
||||
# 用法:bash build-deb.sh
|
||||
# 产物:jnetapplet_<version>_<arch>.deb
|
||||
|
||||
set -e
|
||||
|
||||
# 项目根目录
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# 从 CMakeLists.txt 读取版本号
|
||||
VERSION=$(grep -oP 'project\([^)]*VERSION\s+\K[0-9]+\.[0-9]+\.[0-9]+' CMakeLists.txt)
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "错误:无法从 CMakeLists.txt 读取版本号"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 架构判断
|
||||
ARCH=$(dpkg --print-architecture)
|
||||
if [ -z "$ARCH" ]; then
|
||||
ARCH="amd64"
|
||||
fi
|
||||
|
||||
PKG_NAME="jnetapplet"
|
||||
DEB_FILE="${PKG_NAME}_${VERSION}_${ARCH}.deb"
|
||||
|
||||
echo "=========================================="
|
||||
echo " JNetApplet deb 打包"
|
||||
echo " 版本: $VERSION"
|
||||
echo " 架构: $ARCH"
|
||||
echo " 产物: $DEB_FILE"
|
||||
echo "=========================================="
|
||||
|
||||
# 检查依赖工具
|
||||
for cmd in cmake dpkg-deb; do
|
||||
if ! command -v "$cmd" &> /dev/null; then
|
||||
echo "错误:缺少 $cmd,请先安装"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# 清理并创建构建目录
|
||||
echo "[1/5] 构建 CMake 项目..."
|
||||
rm -rf build
|
||||
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release 2>&1 | tail -3
|
||||
cmake --build build -j"$(nproc)" 2>&1 | tail -3
|
||||
|
||||
# 创建 staging 目录(模拟安装根目录)
|
||||
STAGE_DIR="$(mktemp -d)"
|
||||
trap "rm -rf '$STAGE_DIR'" EXIT
|
||||
|
||||
echo "[2/5] 安装到 staging 目录..."
|
||||
# DESTDIR 指定安装根目录前缀,cmake 会将 /usr/... 安装到 $STAGE_DIR/usr/...
|
||||
DESTDIR="$STAGE_DIR" cmake --install build 2>&1 | tail -3
|
||||
|
||||
# 生成 DEBIAN/control
|
||||
echo "[3/5] 生成 control 文件..."
|
||||
mkdir -p "$STAGE_DIR/DEBIAN"
|
||||
|
||||
# 计算安装后大小(KB)
|
||||
INSTALLED_SIZE=$(du -sk "$STAGE_DIR/usr" | cut -f1)
|
||||
|
||||
cat > "$STAGE_DIR/DEBIAN/control" << EOF
|
||||
Package: ${PKG_NAME}
|
||||
Version: ${VERSION}
|
||||
Architecture: ${ARCH}
|
||||
Maintainer: Jokul <jokul@git.jokul.space>
|
||||
Installed-Size: ${INSTALLED_SIZE}
|
||||
Depends: dde-shell, libc6, libqt6core6, libqt6gui6, libqt6quick6, libqt6network6, libdtk6core
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Description: 网络速度监控 dde-shell 任务栏插件
|
||||
实时监控网络下载/上传速度,支持多网卡切换、IP 地址显示、
|
||||
总流量统计。适用于 deepin 桌面环境的 dde-shell 任务栏。
|
||||
Homepage: https://git.jokul.space/Jokul/JNetApplet
|
||||
EOF
|
||||
|
||||
# 生成 postinst 脚本:安装后重启桌面用户的 dde-shell
|
||||
# 注意:dpkg 以 root 运行此脚本,systemctl --user 需切换到桌面用户身份
|
||||
cat > "$STAGE_DIR/DEBIAN/postinst" << 'EOF'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
# 找到活跃桌面用户(UID >= 1000),以其身份重启 dde-shell 用户服务
|
||||
for user in $(who | awk '{print $1}' | sort -u); do
|
||||
[ "$user" = "root" ] && continue
|
||||
uid=$(id -u "$user" 2>/dev/null) || continue
|
||||
[ "$uid" -lt 1000 ] && continue
|
||||
su "$user" -c "systemctl --user restart dde-shell@DDE" 2>/dev/null || true
|
||||
done
|
||||
exit 0
|
||||
EOF
|
||||
chmod 755 "$STAGE_DIR/DEBIAN/postinst"
|
||||
|
||||
# 生成 prerm 脚本:卸载前重启桌面用户的 dde-shell 移除插件
|
||||
cat > "$STAGE_DIR/DEBIAN/prerm" << 'EOF'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
# 以桌面用户身份重启 dde-shell 以卸载插件
|
||||
for user in $(who | awk '{print $1}' | sort -u); do
|
||||
[ "$user" = "root" ] && continue
|
||||
uid=$(id -u "$user" 2>/dev/null) || continue
|
||||
[ "$uid" -lt 1000 ] && continue
|
||||
su "$user" -c "systemctl --user restart dde-shell@DDE" 2>/dev/null || true
|
||||
done
|
||||
exit 0
|
||||
EOF
|
||||
chmod 755 "$STAGE_DIR/DEBIAN/prerm"
|
||||
|
||||
# 修正文件权限(so 文件需要 755,qml/json 需要 644)
|
||||
echo "[4/5] 修正文件权限..."
|
||||
find "$STAGE_DIR/usr" -type f -name "*.so" -exec chmod 755 {} \;
|
||||
find "$STAGE_DIR/usr" -type f \( -name "*.qml" -o -name "*.json" \) -exec chmod 644 {} \;
|
||||
|
||||
# 构建 deb
|
||||
echo "[5/5] 构建 deb 包..."
|
||||
rm -f "$DEB_FILE"
|
||||
dpkg-deb --build --root-owner-group "$STAGE_DIR" "$DEB_FILE"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 打包完成!"
|
||||
echo " 产物: $DEB_FILE ($(du -h "$DEB_FILE" | cut -f1))"
|
||||
echo ""
|
||||
echo " 安装: sudo dpkg -i $DEB_FILE"
|
||||
echo " 卸载: sudo dpkg -r $PKG_NAME"
|
||||
echo "=========================================="
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QRegularExpression>
|
||||
#include <QNetworkInterface>
|
||||
|
||||
DS_BEGIN_NAMESPACE
|
||||
|
||||
@@ -103,6 +104,12 @@ QString NetworkMonitorApplet::activeInterface() const
|
||||
return m_activeInterface;
|
||||
}
|
||||
|
||||
// 返回活动接口的 IPv4 地址,供 QML 显示
|
||||
QString NetworkMonitorApplet::ipAddress() const
|
||||
{
|
||||
return m_ipAddress;
|
||||
}
|
||||
|
||||
void NetworkMonitorApplet::refresh()
|
||||
{
|
||||
readNetworkStats();
|
||||
@@ -114,6 +121,8 @@ void NetworkMonitorApplet::setActiveInterface(const QString &interface)
|
||||
m_activeInterface = interface;
|
||||
m_firstUpdate = true;
|
||||
emit activeInterfaceChanged();
|
||||
// 接口切换后立即检测新接口的 IP 地址
|
||||
detectIpAddress();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,14 +176,27 @@ void NetworkMonitorApplet::readNetworkStats()
|
||||
|
||||
// 自动选择活动接口
|
||||
if (m_activeInterface.isEmpty() && !m_interfaceList.isEmpty()) {
|
||||
// 优先选择有流量的接口
|
||||
// 优先选择物理网卡(wlp/wlan/enp/eth)中有流量的接口,
|
||||
// 避免选中 Meta/tun0 等虚拟代理接口作为默认显示
|
||||
for (const QString &name : m_interfaceList) {
|
||||
if (!isPhysicalInterface(name)) continue;
|
||||
const NetworkInterface &iface = m_interfaces[name];
|
||||
if (iface.rxBytes > 0 || iface.txBytes > 0) {
|
||||
m_activeInterface = name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 其次选择任意有流量的接口(物理网卡都无流量时的兜底)
|
||||
if (m_activeInterface.isEmpty()) {
|
||||
for (const QString &name : m_interfaceList) {
|
||||
const NetworkInterface &iface = m_interfaces[name];
|
||||
if (iface.rxBytes > 0 || iface.txBytes > 0) {
|
||||
m_activeInterface = name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 最后选第一个(无任何流量时)
|
||||
if (m_activeInterface.isEmpty()) {
|
||||
m_activeInterface = m_interfaceList.first();
|
||||
}
|
||||
@@ -190,6 +212,8 @@ void NetworkMonitorApplet::readNetworkStats()
|
||||
|
||||
calculateSpeed();
|
||||
emit statsChanged();
|
||||
// 每次刷新都检测 IP,以应对 DHCP 续约等 IP 变更场景
|
||||
detectIpAddress();
|
||||
}
|
||||
|
||||
void NetworkMonitorApplet::calculateSpeed()
|
||||
@@ -255,6 +279,40 @@ qint64 NetworkMonitorApplet::getActiveTxBytes() const
|
||||
return m_interfaces[m_activeInterface].txBytes;
|
||||
}
|
||||
|
||||
// 检测活动接口的 IPv4 地址
|
||||
// 跳过 IPv6 和 loopback,只取第一个有效 IPv4 地址
|
||||
// IP 变化时发射 ipAddressChanged 信号通知 QML 更新
|
||||
// 设计原因:DHCP 续约、网络切换等场景下 IP 可能变化,需持续检测
|
||||
void NetworkMonitorApplet::detectIpAddress()
|
||||
{
|
||||
QString newIp;
|
||||
if (!m_activeInterface.isEmpty()) {
|
||||
QNetworkInterface iface = QNetworkInterface::interfaceFromName(m_activeInterface);
|
||||
for (const QNetworkAddressEntry &entry : iface.addressEntries()) {
|
||||
// 只取 IPv4 地址,跳过 IPv6 和 loopback
|
||||
if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol
|
||||
&& !entry.ip().isLoopback()) {
|
||||
newIp = entry.ip().toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_ipAddress != newIp) {
|
||||
m_ipAddress = newIp;
|
||||
emit ipAddressChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否为物理网卡
|
||||
// 物理网卡前缀:无线 wlp/wlan/wifi,有线 enp/eth
|
||||
// 虚拟接口(Meta/tun/tap/docker/veth/br-)返回 false,
|
||||
// 用于自动选择时优先真实网卡而非代理 TUN 接口
|
||||
bool NetworkMonitorApplet::isPhysicalInterface(const QString &name) const
|
||||
{
|
||||
return name.startsWith("wlp") || name.startsWith("wlan")
|
||||
|| name.startsWith("enp") || name.startsWith("eth");
|
||||
}
|
||||
|
||||
D_APPLET_CLASS(NetworkMonitorApplet)
|
||||
|
||||
DS_END_NAMESPACE
|
||||
|
||||
@@ -38,6 +38,8 @@ class NetworkMonitorApplet : public DApplet
|
||||
Q_PROPERTY(QStringList interfaceStats READ interfaceStats NOTIFY statsChanged)
|
||||
Q_PROPERTY(bool ready READ ready NOTIFY readyChanged)
|
||||
Q_PROPERTY(QString activeInterface READ activeInterface NOTIFY activeInterfaceChanged)
|
||||
// 活动接口的 IPv4 地址,供 QML 在弹出面板和 tooltip 中显示
|
||||
Q_PROPERTY(QString ipAddress READ ipAddress NOTIFY ipAddressChanged)
|
||||
|
||||
public:
|
||||
explicit NetworkMonitorApplet(QObject *parent = nullptr);
|
||||
@@ -54,6 +56,7 @@ public:
|
||||
QStringList interfaceStats() const;
|
||||
bool ready() const;
|
||||
QString activeInterface() const;
|
||||
QString ipAddress() const;
|
||||
|
||||
Q_INVOKABLE void refresh();
|
||||
Q_INVOKABLE void setActiveInterface(const QString &interface);
|
||||
@@ -65,11 +68,17 @@ signals:
|
||||
void statsChanged();
|
||||
void readyChanged();
|
||||
void activeInterfaceChanged();
|
||||
void ipAddressChanged();
|
||||
|
||||
private:
|
||||
void readNetworkStats();
|
||||
void calculateSpeed();
|
||||
void detectInterfaces();
|
||||
// 检测活动接口的 IPv4 地址,变化时发射 ipAddressChanged
|
||||
void detectIpAddress();
|
||||
// 判断是否为物理网卡(无线 wlp/wlan,有线 enp/eth),
|
||||
// 用于自动选择时优先真实网卡而非虚拟代理接口(如 Meta/tun0)
|
||||
bool isPhysicalInterface(const QString &name) const;
|
||||
qint64 getActiveRxBytes() const;
|
||||
qint64 getActiveTxBytes() const;
|
||||
|
||||
@@ -77,6 +86,7 @@ private:
|
||||
QMap<QString, NetworkInterface> m_interfaces;
|
||||
QStringList m_interfaceList;
|
||||
QString m_activeInterface;
|
||||
QString m_ipAddress; // 活动接口的 IPv4 地址
|
||||
|
||||
// 速度计算
|
||||
qint64 m_lastRxBytes;
|
||||
|
||||
+932
-298
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user