7 Commits

Author SHA1 Message Date
Jokul 43cb5238c8 chore: 忽略 docs/superpowers 内部文档目录 2026-07-18 19:53:17 +08:00
Jokul ea2081f0a6 remove 2026-07-18 19:51:24 +08:00
Jokul ab1a5227fa docs: 更新 README 与 AGENTS 说明,补充 deb 打包与最新功能 2026-07-18 19:48:31 +08:00
Jokul 8f5e30532d Merge pull request 'Feature/ui redesign' (#1) from feature/ui-redesign into master
Reviewed-on: #1
2026-07-18 19:39:03 +08:00
Jokul 9b9c26d5d1 feat: 增加 deb 一键打包脚本
- build-deb.sh:自动构建、staging 安装、生成 control、打包 deb
- 从 CMakeLists.txt 读取版本号,自动检测架构
- postinst/prerm 脚本以桌面用户身份重启 dde-shell
- 依赖声明:dde-shell, Qt6 Core/Gui/Quick/Network, DTK6 Core
2026-07-18 19:36:10 +08:00
Jokul 6142dc4769 feat: 优化设置/关于窗口与交互体验
- 设置窗口:frameless 自定义标题栏,网卡接口选择,卸载插件功能
- 卸载流程:复制命令到剪贴板,按钮文字临时显示提示并禁用点击
- 关于窗口:独立 Window 在桌面居中,展示作者信息
- 右键菜单:删除刷新项,精简为设置/关于
- 网卡列表:活动接口优先排序,超出宽度水平滚动
- 弹出面板:网卡名+IP 卡片背景,双列速度居中
- 修复 tooltip 数值变 0、右键菜单不展示等问题
2026-07-18 19:25:07 +08:00
Jokul 009dd9578f feat: 重新设计插件 UI 样式与交互
- 任务栏图标:双行数值显示(下载蓝/上传绿),固定宽度避免抖动
- 左键弹出面板:360×320 居中布局,双列速度卡片,网卡名+IP 卡片背景
- 右键菜单:deepin 风格 Platform.Menu + MenuHelper(刷新/设置/关于)
- 设置窗口:frameless 自定义标题栏,网卡接口选择,卸载插件功能
- Tooltip:网卡名 + IP 地址单行展示
- C++ 后端:新增 ipAddress 属性(QNetworkInterface),优先选择物理网卡
- 速度格式化:最小单位 KB,2 位小数
- 移除 QML 侧多余 refresh 调用,修复 tooltip 数值变 0
- 新增设计文档与实现计划
2026-07-18 18:44:12 +08:00
8 changed files with 1222 additions and 301 deletions
+4 -1
View File
@@ -22,4 +22,7 @@ cmake-build-*/
# System files
.DS_Store
Thumbs.db
Thumbs.db
# AI 工作流内部文档(不纳入版本控制)
docs/superpowers/
+13
View File
@@ -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
View File
@@ -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
)
+74
View File
@@ -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
View File
@@ -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 文件需要 755qml/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 "=========================================="
+59 -1
View File
@@ -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
+10
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff