31 Commits
Author SHA1 Message Date
Jokul fba506d9d8 feat: 国际化支持,英文源串+中文翻译
全部44处qsTr源串改为英文,新建translations/jnetapplet_zh_CN.ts
中文翻译文件。CMake增加LinguistTools+qt_add_translation编译
.ts->.qm,init()按系统语言加载.qm并安装到qApp。

中文系统显示中文翻译,英文系统显示英文源串。
2026-07-24 11:40:12 +08:00
Jokul 43fdcc8b0a docs: 更新README和AGENTS.md同步当前项目状态
功能特性补全设置窗口/字体颜色/流量波动图/IPv6/深色模式/全接口采样。
项目结构补全6个组件。C++ Backend属性列表移除interfaceStats,新增
ipv6Address/version/textColor/speedHistory等。修正卸载入口描述。
2026-07-24 11:14:42 +08:00
Jokul a4fe4fdc83 refactor: 移除refresh()的Q_INVOKABLE标记
refresh() 是 m_refreshTimer 每秒回调,QML 从不直接调用。
移除 Q_INVOKABLE 防止 QML 误调用打破定时间隔。函数本身保留。
2026-07-24 11:10:55 +08:00
Jokul 35dc998448 refactor: 删除interfaceStats死代码
删除未使用的 Q_PROPERTY interfaceStats、interfaceStats() 实现、
statsChanged 信号、QML 属性绑定。#14 随之解决(interfaceStats
整体删除,不再有不完整输出)。
2026-07-24 11:04:59 +08:00
Jokul 110032f3c9 refactor: 抽取NetCommon.qml消除跨组件重复代码
新建 package/components/NetCommon.qml 聚合公共颜色定义(主题感知色
+强调色)和工具函数(isPhysicalIf/formatSpeed/formatTotal/downloadValueColor)。

networkview.qml、NetworkPopup.qml、TrafficChartWindow.qml 删除各自
的重复定义改为引用 common.xxx,净删除91行重复代码。downloadValueColor
由属性绑定改为函数(调用方传速度值)。视觉行为零变化。
2026-07-24 11:00:02 +08:00
Jokul 36200e7c50 fix: version()元数据为空时回退到编译时版本号
CMakeLists.txt 新增 target_compile_definitions 将 project(VERSION)
暴露为 PROJECT_VERSION 宏。version() 在 pluginMetaData 返回空时
回退到该宏,避免 AboutWindow 版本行显示空白。
2026-07-24 10:07:37 +08:00
Jokul 4b5cde4361 fix: SPDX版权头从占位符改为2026 Jokul
8个源文件的 SPDX-FileCopyrightText: 2024 MyCompany 改为 2026 Jokul。
2026-07-24 10:02:46 +08:00
Jokul 6d17f638e7 fix: 窗口居中加Screen.virtualX/virtualY偏移适配多显示器
原 Screen.width/height 居中在虚拟桌面原点,多显示器下窗口可能出
现在非预期屏幕。加 virtualX/virtualY 偏移后在任务栏所在屏幕内居中。
2026-07-24 09:57:32 +08:00
Jokul 9803c59e76 fix: 为所有接口采集速度历史而非仅活动接口
原 calculateSpeed 仅给活动接口追加采样点,切换到非活动接口时趋势图
为空需等待数分钟。新增 m_lastRxBytesByIface/m_lastTxBytesByIface
跟踪所有接口字节基线,遍历全部接口采集速度历史。

切换接口时趋势图立即有数据,新增接口(如USB网卡)也自动开始采集。
2026-07-24 09:53:39 +08:00
Jokul 8e7b945a7c fix: setActiveInterface校验接口有效性
传入不存在的接口名会被持久化到配置文件且当前会话速度恒为0。
增加 m_interfaceList.contains 校验,无效接口名直接忽略。
2026-07-24 09:41:20 +08:00
Jokul 91bc3174a0 fix: detectInterfaces过滤规则与readNetworkStats统一
detectInterfaces 原仅过滤 lo,docker/veth/br- 接口会在首次刷新前
短暂出现在列表中后被 readNetworkStats 覆盖,造成 UI 闪烁。
统一过滤规则为 lo/veth*/docker*/br-*。
2026-07-24 09:37:11 +08:00
Jokul c614df5fbe fix: 独立窗口及任务栏图标适配深色模式
4 个独立窗口(AboutWindow/SettingsWindow/TextColorPicker/TrafficChartWindow)
新增 isDarkMode 属性,由 networkview.qml 检测后注入,各窗口定义深/浅双色方案。

深色模式检测从 DockPalette.iconTextPalette 改为 DTK.palette.window.hslLightness,
因 DockPalette 不随系统深色主题变化。

同时将 networkview.qml 任务栏图标和 NetworkPopup 弹窗的颜色基从
DockPalette.iconTextPalette 改为 DTK.palette.windowText,修复深色模式
下黑底黑字、浅色模式下白底白字的问题。
2026-07-24 09:30:18 +08:00
Jokul 42028d93bd fix: 总量统计改为累加会话增量而非取计数器值
原 m_totalDownload = currentRxBytes 直接取活动接口计数器值,切换
网卡时跳变到新接口计数器,网卡重启时归零,语义有误导。

改为 m_totalDownload += rxDeltaClamped 累加会话增量,总计从 0
开始增长,切换网卡不再跳变。跨重启持久化属于后续 #18 的范畴。
2026-07-23 00:11:09 +08:00
Jokul af7ae6b334 fix: 计数器回绕/重置时钳制负速度为0
网卡重启、/proc/net/dev 计数器溢出或接口重置时,字节差值为负,
QML 会显示负速度。对 rxDelta/txDelta 钳制为 0 避免此问题。
2026-07-23 00:06:43 +08:00
Jokul e53981f828 fix: 速度计算按真实时间间隔而非假定为1秒
calculateSpeed() 原直接取字节差作为 bytes/sec,假定定时器严格
1 秒触发。系统负载高时 QTimer 延迟会导致速度失真。

改为记录上次采样的毫秒时间戳 m_lastTimestampMs,按真实流逝时间
delta_bytes / elapsed_seconds 计算速度;速度存储由 qint64 改为
double 保留精度;elapsedSec > 0 守卫避免除零。

同时新增 docs/project-review.md 项目审查清单。
2026-07-23 00:01:39 +08:00
Jokul 9f11b32b5e refactor: 拆分弹窗到独立组件 NetworkPopup.qml
将 networkview.qml 中 PanelPopup 弹窗内容提取到独立组件
package/components/NetworkPopup.qml,减少主文件约 400 行。

PanelPopup 是 dock 上下文类型,不能作为独立组件根元素,因此
NetworkPopup.qml 用 Control 作为根(只含弹窗内容),由
networkview.qml 的 PanelPopup 壳包裹,定位和可见性管理在壳层处理。
2026-07-22 23:28:07 +08:00
Jokul b9b5232d5f feat: 活动接口选择持久化到配置文件
- 构造函数从 ~/.config/jnetapplet/settings.ini 读取上次保存的 activeInterface
- setActiveInterface 切换接口时写入配置,弹窗 chip 和设置窗口统一持久化
- 保存的接口已不存在时(如 USB 网卡拔出)清空回退到自动选择
- 下次启动插件自动恢复用户上次选择的网卡
2026-07-22 22:42:35 +08:00
Jokul 7f7ab2d737 feat: 设置窗口网卡列表样式增强
- 增加网卡类型图标、hover 背景高亮、选中行浅蓝底+蓝边框
- 修复 RadioButton text 在 dde-shell/DTK 下渲染异常导致接口名不可见,
  改用独立 Text 元素渲染接口名
- 行高 36->30、间距 4->2 收紧列表布局
- 网卡超过 5 行时 ScrollView 垂直滚动,滚动条样式与弹窗一致
- 行内所有元素垂直居中对齐
- 整行点击即可切换网卡,RadioButton 替换为自绘圆点指示器
- 选中态颜色加深为 #1565c0 提升图标对比度
2026-07-22 22:33:46 +08:00
Jokul c4246cd122 feat: chip列表稳定排序+自动滚动到选中网卡
- sortedInterfaces 不再将活动接口移到最前,改为物理网卡在前、虚拟网卡
  在后各自按名称排序,切换网卡时列表顺序不再跳动
- 新增 scrollToActiveChip 函数,活动接口变化时自动滚动 Flickable 使选中
  chip 完整可见,左侧截断对齐左缘、右侧截断对齐右缘
- 用 NumberAnimation(200ms OutCubic)驱动 contentX 平滑滚动,用户拖拽时
  自动停止动画避免争抢
- popup 打开时用 Qt.callLater 直接定位活动 chip(无动画)
2026-07-22 21:40:20 +08:00
Jokul 539442ef48 fix: 网卡切换chip列表溢出时显示水平滚动条
网卡数量过多或名称过长时,chip 列表溢出弹窗右边界被裁切且无滚动条。
增大 Flickable 高度(32->40px)容纳滚动条,并为 ScrollBar 添加自定义
contentItem 样式,确保深浅主题下均可见可交互。
2026-07-22 21:25:15 +08:00
Jokul 647d0878de chore: 版本号升至 1.1.0 准备封版 2026-07-20 23:49:46 +08:00
Jokul d4627f82d5 fix: 流量波动图Y轴动态适应最近活动,峰值过后自动缩小
- Y轴上限从全量5分钟缓冲改为最近60秒窗口计算
- 解决历史高峰值(如12MB/s下载)导致Y轴长期偏高、
  当前低网速曲线被压到底部无法观察波动的问题
- 峰值滑出60秒窗口后Y轴自动缩小,始终为当前活动提供合适显示比例
2026-07-20 23:23:08 +08:00
Jokul a12a106791 fix: SettingsWindow 组件加载失败修复 + 设置窗口交互优化
- 修复 import "components" 错误:SettingsWindow.qml 位于 components/ 目录,
  该 import 会找 components/components/(不存在)导致 QML 加载失败、插件消失;
  同目录组件 QML 自动解析,删除该 import
- 接口名左对齐、类型描述右对齐:RadioButton 只显示接口名,描述用独立
  Text 右对齐,替代原 modelData + " - " + 描述 的连写形式
- 窗口高度 450 -> 390,字体颜色到卸载按钮的 fillHeight 改固定 4px 间距,
  消除原大段空白
- 标题栏拖动改用 startSystemMove() 系统级窗口拖动(参考 TrafficChartWindow),
  替代手动鼠标事件模拟,解决拖动抖动/不跟手问题
2026-07-20 23:10:20 +08:00
Jokul 02c6d055ce refactor: 抽取设置窗口到独立组件 package/components/SettingsWindow.qml
- 新建 SettingsWindow.qml(411 行):Window 根元素,对外属性 applet /
  networkInterfaces / activeInterface / accentColor,内部化 interfaceDescription
  函数、clipboardHelper、statusHideTimer,完整搬移标题栏、网络接口选择区、
  字体颜色选择区、卸载按钮、卸载确认对话框
- networkview.qml 从 1126 行缩减至 751 行:删除内联设置窗口及相关辅助,
  原位置实例化 SettingsWindow 并保留 id: settingsWindow(右键菜单引用无需改动)
- 模式参考 AboutWindow.qml:对外依赖通过 property 注入,组件可独立预览
2026-07-20 22:10:55 +08:00
Jokul 9a02f10eb8 fix: 设置弹框移除底部关闭按钮、禁止拖拽缩放、标题栏可拖动
- 移除底部「关闭」按钮(保留标题栏 × 按钮关闭窗口)
- 设置 minimumWidth/maximumWidth/minimumHeight/maximumHeight
  固定窗口尺寸 340x450,禁止拖拽改变宽高
- 标题栏 MouseArea 已有 drag.target: settingsWindow 拖动能力
2026-07-20 21:44:52 +08:00
Jokul 4c6bc6f689 feat: 设置窗口网络接口列表显示类型描述
- 新增 interfaceDescription 函数:根据接口名模式返回类型说明
  (有线/无线/VPN/容器/桥接/虚拟桥接/本地回环/虚拟接口/其他)
- RadioButton 文本从 modelData 改为 modelData + 描述(如 enp3s0 — 有线网络)
2026-07-20 21:37:54 +08:00
Jokul f7157e15e8 fix: 任务栏 ↑↓ 箭头颜色跟随字体颜色设置
- 横向/竖向布局共 4 处箭头 Text 的 color 从 root.secondaryText
  改为 root.speedTextColor,与网速数值统一受 textColor 属性控制
- 弹窗详情的标签色(secondaryText)保持不变,不受字体颜色设置影响
2026-07-20 21:30:42 +08:00
Jokul 22900737b0 feat: 设置窗口新增字体颜色选择,任务栏网速数值色可自定义并持久化
- 新增 TextColorPicker.qml 颜色选择器组件:预设 8 色 + 跟随系统选项,
  2px 深色边框高亮选中态,白色圆角卡片与设置窗口其他区视觉一致
- C++ 后端新增 textColor Q_PROPERTY:QString 存储,空串=跟随系统主题,
  非空用 QColor 校验,写入 ~/.config/jnetapplet/settings.ini 持久化
- networkview.qml 新增 speedTextColor 属性回退逻辑:applet.textColor
  优先,空串时回退到 DockPalette 派生的 primaryText(深浅色自适应)
- 任务栏 4 处数值 Text(横向/竖向 × 下载/上传)color 绑定改为
  speedTextColor;设置窗口网络接口区下方新增字体颜色选择区
2026-07-20 21:17:59 +08:00
Jokul ef37fbbcbf fix: build-deb.sh 显示完整 CMake 错误并预检构建依赖
- 新增 run_step 辅助函数:成功时仅显示末尾 3 行,失败时打印完整输出,
  解决原 | tail -3 管道吞掉 find_package 关键错误、用户无法定位缺包问题
- 新增 [1/6] 构建依赖预检:用 dpkg -s 检查 qt6-base-dev /
  qt6-declarative-dev / libdtkcommon-dev / libdde-shell-dev,缺失时
  直接给出 sudo apt install 命令并退出,步骤编号顺延为 [1/6]~[6/6]
- README 新增「构建依赖」小节:完整 apt install 命令 + 依赖包与
  find_package 对照表 + 运行时无需 -dev 包说明,删除原一句话提示
2026-07-20 20:12:53 +08:00
Jokul 959371c236 chore: 版本号升级到 1.0.1 2026-07-19 21:39:34 +08:00
Jokul d0b8272ec6 feat: 新增流量波动图窗口,展示活动接口最近 5 分钟网速趋势
- C++ 后端增加 SpeedSample 采样结构与每接口环形缓冲(MAX_HISTORY_SAMPLES=300,
  即 5 分钟/1Hz),通过 speedHistoryDownload/speedHistoryUpload 暴露为
  QVariantList of QPointF 供 QML 绘制;接口切换/消失时同步清理历史避免泄漏
- 新增 package/components/TrafficChartWindow.qml:纯 QML Canvas 绘制下载/上传
  双折线,无外部依赖;支持 hover 辅助线、置顶图钉、空状态兜底
- networkview.qml 右键菜单增加"流量波动图"入口,弹出屏幕居中独立窗口
2026-07-19 21:38:54 +08:00
15 changed files with 2751 additions and 891 deletions
+27 -16
View File
@@ -15,14 +15,18 @@ for the UI. The C++ backend reads `/proc/net/dev` to monitor network traffic.
## Features
- Real-time download/upload speed monitoring (1-second refresh)
- Total data transfer statistics
- Multiple network interface support with interface switching
- Real-time download/upload speed monitoring (1-second refresh, real elapsed time)
- Session cumulative download/upload traffic statistics
- Multiple network interface support with interface switching (popup + settings)
- 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
- Customizable taskbar font color (follow system theme or pick from preset)
- Hover tooltip shows interface name + IP address
- Click to open detailed popup (speed, totals, interface chips)
- Popup shows active interface IPv4/IPv6 address
- Traffic chart window: 5-minute speed trend (download/upload dual line chart)
- Settings window: interface selection, font color picker, one-click uninstall
- Full dark mode support (taskbar icon, popup, all independent windows)
- All interfaces sampled in background; switching interfaces shows history immediately
## Build & install
@@ -75,8 +79,13 @@ then restart `dde-shell` (it discovers applets by scanning the install dir for
│ ├── metadata.json.in # Plugin metadata 模板(由 CMake 生成 metadata.json
│ ├── networkview.qml # QML UI 主入口
│ └── components/ # 拆分出的子组件
── AboutWindow.qml # 关于窗口组件
├── docs/ # Design docs and implementation plans
── NetCommon.qml # 公共颜色与格式化函数
│ ├── NetworkPopup.qml # 左键弹窗内容
│ ├── SettingsWindow.qml # 设置窗口(接口/颜色/卸载)
│ ├── TrafficChartWindow.qml # 流量波动图窗口
│ ├── TextColorPicker.qml # 字体颜色选择器
│ └── AboutWindow.qml # 关于窗口
├── docs/ # Design docs and review checklist
└── AGENTS.md # This file
```
@@ -100,14 +109,16 @@ then restart `dde-shell` (it discovers applets by scanning the install dir for
## C++ Backend (NetworkMonitorApplet)
Inherits from `DApplet`, provides:
- `downloadSpeed` / `uploadSpeed`: Current speed in bytes/sec
- `totalDownload` / `totalUpload`: Total data transferred
- `downloadSpeed` / `uploadSpeed`: Current speed in bytes/sec (real elapsed time)
- `totalDownload` / `totalUpload`: Session cumulative traffic
- `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
- `activeInterface`: Currently selected interface (persisted to `~/.config/jnetapplet/settings.ini`)
- `ipAddress` / `ipv6Address`: IP address of the active interface
- `version`: Plugin version (from metadata, fallback to `PROJECT_VERSION` macro)
- `textColor`: Taskbar font color (empty = follow system theme, persisted)
- `speedHistoryDownload` / `speedHistoryUpload`: Per-interface speed history (QPointF list, 5-min window)
- `setActiveInterface(name)`: Switch active interface (validates against interface list)
- `refresh()`: Internal timer callback (not Q_INVOKABLE, called by m_refreshTimer every second)
## Identifier correspondence (keep in sync when renaming)
+19 -2
View File
@@ -1,12 +1,12 @@
cmake_minimum_required(VERSION 3.16)
project(JNetApplet VERSION 1.0.0 LANGUAGES CXX)
project(JNetApplet VERSION 1.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Core Quick DBus Network)
find_package(Qt6 REQUIRED COMPONENTS Core Quick DBus Network LinguistTools)
find_package(Dtk6 REQUIRED COMPONENTS Core)
find_package(DDEShell REQUIRED)
@@ -20,6 +20,20 @@ add_library(space.jokul.JNetApplet SHARED
set_target_properties(space.jokul.JNetApplet PROPERTIES PREFIX "")
# 将 project(VERSION) 暴露为编译宏,供 C++ version() 兜底使用
target_compile_definitions(space.jokul.JNetApplet PRIVATE PROJECT_VERSION="${PROJECT_VERSION}")
# 翻译文件:编译 .ts -> .qm,安装到插件目录的 translations/ 子目录
# 设计原因:QML 中 qsTr() 源串为英文,中文翻译通过 .qm 文件在运行时加载
set(TS_FILES ${CMAKE_CURRENT_SOURCE_DIR}/translations/jnetapplet_zh_CN.ts)
qt_add_translation(QM_FILES ${TS_FILES})
# 翻译文件安装路径,暴露为编译宏供 C++ 加载
target_compile_definitions(space.jokul.JNetApplet PRIVATE
TRANSLATIONS_DIR="/usr/share/dde-shell/${PLUGIN_ID}/translations"
)
# 确保 .qm 在构建时生成(qt_add_translation 默认仅 install 时触发)
add_custom_target(translations ALL DEPENDS ${QM_FILES})
# 头文件搜索路径指向 src/,使 #include "networkmonitorapplet.h" 可被外部解析
target_include_directories(space.jokul.JNetApplet PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
@@ -49,3 +63,6 @@ install(FILES package/networkview.qml DESTINATION /usr/share/dde-shell/${PLUGIN_
# 设计原因:拆分出的子组件需随主 QML 一起安装到 dde-shell 插件目录,
# 否则 networkview.qml 的 import "components" 在运行时找不到类型
install(DIRECTORY package/components DESTINATION /usr/share/dde-shell/${PLUGIN_ID})
# 安装编译后的翻译文件
install(FILES ${QM_FILES} DESTINATION /usr/share/dde-shell/${PLUGIN_ID}/translations)
+35 -9
View File
@@ -9,17 +9,38 @@ QML 前端渲染界面,实时显示当前网卡的上行/下行速率与累计
## 功能特性
- 实时上行/下行速率监控(1 秒刷新)
- 累计上传/下载流量统计
- 多网卡支持,可在弹窗中切换活动网卡
- 实时上行/下行速率监控(1 秒刷新,按真实时间间隔计算
- 本次会话累计上传/下载流量统计
- 多网卡支持,可在弹窗或设置窗口中切换活动网卡
- 任务栏图标实时显示速率,高速时变色提示
- 悬停显示速率摘要,点击展开详情弹窗
- 详情弹窗展示活动网卡 IP 地址、各网卡速率对比与统计图表
- 支持从弹窗一键卸载插件
- 任务栏字体颜色可自定义,支持跟随系统主题
- 悬停显示速率摘要与网卡 IP 地址,点击展开详情弹窗
- 详情弹窗展示活动网卡 IPv4/IPv6 地址、各网卡速率对比与统计
- 流量波动图窗口:展示活动接口最近 5 分钟网速趋势(下载/上传双折线)
- 设置窗口:网络接口选择、字体颜色自定义、一键卸载插件
- 深色模式全适配(任务栏图标、弹窗、独立窗口均跟随系统主题)
- 所有接口后台持续采集速度历史,切换网卡时趋势图立即有数据
## 构建与安装
需要配置期存在 `DDEShell` CMake 包(来自 `dde-shell` 开发包)。
### 构建依赖
构建需要以下开发包:
```sh
sudo apt install build-essential cmake pkg-config qt6-base-dev qt6-declarative-dev libdtkcommon-dev libdde-shell-dev
```
各包与 CMake `find_package` 的对应关系:
| 依赖包 | 提供 | 对应 CMake find_package |
|---|---|---|
| qt6-base-dev | Qt6 Core / DBus / Network | Qt6 Core DBus Network |
| qt6-declarative-dev | Qt6 Quick | Qt6 Quick |
| libdtkcommon-dev | Dtk6 Core | Dtk6 Core |
| libdde-shell-dev | DDEShell | DDEShell |
> 运行时仅需 `dde-shell` 及 Qt6/Dtk6 运行库,上述 `-dev` 包仅构建时需要。
### 方式一:从源码安装
@@ -72,8 +93,13 @@ bash build-deb.sh
│ ├── metadata.json.in # 插件元数据模板(由 CMake 生成 metadata.json
│ ├── networkview.qml # QML 界面主入口
│ └── components/ # 拆分出的子组件
── AboutWindow.qml # 关于窗口组件
├── docs/ # 设计文档与实现计划
── NetCommon.qml # 公共颜色与格式化函数
│ ├── NetworkPopup.qml # 左键弹窗内容(速度/总量/网卡切换)
│ ├── SettingsWindow.qml # 设置窗口(接口选择/字体颜色/卸载)
│ ├── TrafficChartWindow.qml # 流量波动图窗口(5分钟趋势折线)
│ ├── TextColorPicker.qml # 字体颜色选择器
│ └── AboutWindow.qml # 关于窗口
├── docs/ # 设计文档与审查清单
├── README.md # 本文件
└── AGENTS.md # AI 代理工作指引
```
+48 -8
View File
@@ -6,6 +6,25 @@
set -e
# 运行构建步骤:成功时仅显示末尾 3 行,失败时打印完整输出后返回非零
# 设计原因:原脚本用 | tail -3 管道会吞掉 find_package 等关键错误,
# 导致缺依赖时用户看不到真正原因(CMake 只在末尾报 Configuring incomplete
run_step() {
local desc="$1"; shift
local log
log=$(mktemp)
if "$@" > "$log" 2>&1; then
tail -3 "$log"
rm -f "$log"
return 0
else
echo "错误:${desc}失败,完整输出如下:" >&2
cat "$log" >&2
rm -f "$log"
return 1
fi
}
# 项目根目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
@@ -41,22 +60,43 @@ for cmd in cmake dpkg-deb; do
fi
done
echo "[1/6] 检查构建依赖..."
# 检查构建依赖:确认开发包已安装,缺失时提示安装命令并退出
# 设计原因:用户 clone 后直接运行 build-deb.sh 经常因缺少 -dev 包导致
# CMake 配置失败,但原脚本用 tail -3 吞掉关键错误,用户无法定位问题。
# 在构建前主动预检可提前给出明确指引。
DEPS=(qt6-base-dev qt6-declarative-dev libdtkcommon-dev libdde-shell-dev)
MISSING=()
for pkg in "${DEPS[@]}"; do
if ! dpkg -s "$pkg" &> /dev/null 2>&1; then
MISSING+=("$pkg")
fi
done
if [ ${#MISSING[@]} -gt 0 ]; then
echo "错误:缺少构建依赖包:${MISSING[*]}" >&2
echo "请执行以下命令安装:" >&2
echo " sudo apt install ${MISSING[*]} build-essential cmake" >&2
exit 1
fi
echo "所有构建依赖已就绪"
# 清理并创建构建目录
echo "[1/5] 构建 CMake 项目..."
echo "[2/6] 构建 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
run_step "CMake 配置" cmake -B build -S . -DCMAKE_BUILD_TYPE=Release || exit 1
run_step "CMake 构建" cmake --build build -j"$(nproc)" || exit 1
# 创建 staging 目录(模拟安装根目录)
STAGE_DIR="$(mktemp -d)"
trap "rm -rf '$STAGE_DIR'" EXIT
echo "[2/5] 安装到 staging 目录..."
echo "[3/6] 安装到 staging 目录..."
# DESTDIR 指定安装根目录前缀,cmake 会将 /usr/... 安装到 $STAGE_DIR/usr/...
DESTDIR="$STAGE_DIR" cmake --install build 2>&1 | tail -3
run_step "安装到 staging" env DESTDIR="$STAGE_DIR" cmake --install build || exit 1
# 生成 DEBIAN/control
echo "[3/5] 生成 control 文件..."
echo "[4/6] 生成 control 文件..."
mkdir -p "$STAGE_DIR/DEBIAN"
# 计算安装后大小(KB
@@ -109,12 +149,12 @@ EOF
chmod 755 "$STAGE_DIR/DEBIAN/prerm"
# 修正文件权限(so 文件需要 755qml/json 需要 644
echo "[4/5] 修正文件权限..."
echo "[5/6] 修正文件权限..."
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 包..."
echo "[6/6] 构建 deb 包..."
rm -f "$DEB_FILE"
dpkg-deb --build --root-owner-group "$STAGE_DIR" "$DEB_FILE"
+127
View File
@@ -0,0 +1,127 @@
# JNetApplet 项目审查清单
> 审查日期:2026-07-22
> 审查范围:全部源码(C++ 后端 + QML 前端 + 构建脚本 + 文档)
---
## 一、缺陷(Bug / 逻辑错误)
### 🔴 高优先级
**1. ~~速度计算假设定时间隔严格为 1 秒~~ ✅ 已修复**
~~`calculateSpeed()` 直接用 `currentRxBytes - m_lastRxBytes` 作为 bytes/sec,未除以实际流逝时间。系统负载高或定时器抖动时,速度值会失真。~~
已改为记录上次采样的毫秒时间戳 `m_lastTimestampMs`,按真实流逝时间 `delta_bytes / elapsed_seconds` 计算速度;速度存储由 `qint64` 改为 `double` 保留精度;`elapsedSec > 0` 守卫避免除零。
**2. ~~计数器回绕/重置产生负速度~~ ✅ 已修复**
~~网卡重启、`/proc/net/dev` 计数器溢出或接口重置时,`currentRxBytes - m_lastRxBytes` 可能为负。代码无任何兜底,QML 会显示负速度。~~
已在 `calculateSpeed()` 中对 `rxDelta` / `txDelta` 钳制为 0,计数器回绕/接口重置/网卡重启时不再产生负速度。
**3. ~~"总量统计"语义有误导~~ ✅ 已修复**
~~`m_totalDownload = currentRxBytes` 只存储活动接口的当前计数器值,并非真正的累计流量。~~
已改为累加会话增量 `m_totalDownload += rxDeltaClamped`"总计"语义从"活动接口计数器值"变为"本次会话累计流量":从 0 开始增长,切换网卡不再跳变,网卡重启不归零。跨重启持久化属于 #18 的范畴。
**4. ~~独立窗口不支持深色模式~~ ✅ 已修复**
~~`AboutWindow.qml`、`SettingsWindow.qml`、`TextColorPicker.qml`、`TrafficChartWindow.qml` 全部硬编码浅色,不随系统主题适配。~~
已为 4 个独立窗口新增 `isDarkMode` 属性(由 networkview.qml 检测后注入),各窗口定义深/浅双色方案切换。深色模式检测改用 `DTK.palette.window.hslLightness < 0.5`(原 `DockPalette.iconTextPalette` 不随系统主题变化)。同时修复 networkview.qml 任务栏图标和 NetworkPopup 弹窗的颜色基:从 `DockPalette.iconTextPalette` 改为 `DTK.palette.windowText`,解决深色模式黑底黑字问题。
**5. ~~`detectInterfaces()` 过滤规则与 `readNetworkStats()` 不一致~~ ✅ 已修复**
~~`init()` 先调 `detectInterfaces()`(仅过滤 `lo`),再调 `readNetworkStats()`(过滤 `lo`/`veth`/`docker`/`br-`),首次刷新前 docker/veth 接口短暂出现造成 UI 闪烁。~~
已将 `detectInterfaces()` 的过滤规则统一为与 `readNetworkStats()` 一致(`lo`/`veth*`/`docker*`/`br-*`)。
### 🟡 中优先级
**6. ~~`setActiveInterface()` 不校验接口有效性~~ ✅ 已修复**
~~可传入不存在的接口名,会被持久化到配置文件,当前会话期间速度恒为 0。~~
已在方法入口增加 `m_interfaceList.contains(interface)` 校验,无效接口名直接忽略。
**7. ~~速度历史仅记录活动接口~~ ✅ 已修复**
~~`m_speedHistory` 每秒只给 `m_activeInterface` 追加采样点,非活动接口永不采集,切换到新接口时趋势图为空。~~
已新增 `m_lastRxBytesByIface` / `m_lastTxBytesByIface` 跟踪所有接口的字节基线,`calculateSpeed()` 遍历所有接口采集速度历史。切换接口时趋势图立即有数据。
**8. ~~窗口居中未考虑多显示器~~ ✅ 已修复**
~~`AboutWindow`、`SettingsWindow`、`TrafficChartWindow` 用 `Screen.width/height` 居中,frameless 窗口在多屏环境下可能出现在非预期屏幕。~~
已加 `Screen.virtualX` / `Screen.virtualY` 偏移,窗口在任务栏所在屏幕内居中而非虚拟桌面原点。
**9. ~~SPDX 版权归属为占位符~~ ✅ 已修复**
~~所有文件头 `SPDX-FileCopyrightText: 2024 MyCompany`"MyCompany" 是模板占位符。~~
已将 8 个源文件的版权头改为 `SPDX-FileCopyrightText: 2026 Jokul`
**10. ~~`version()` 无兜底~~ ✅ 已修复**
~~若 `pluginMetaData().value("Version")` 返回空(元数据未加载),AboutWindow 版本行显示空白。~~
已在 `version()` 中加兜底:元数据返回空时回退到编译时版本号 `PROJECT_VERSION` 宏。CMakeLists.txt 新增 `target_compile_definitions``project(VERSION)` 暴露为编译宏。
---
## 二、代码质量 / 可维护性
**11. ~~大量重复代码跨组件复制~~ ✅ 已修复**
~~`networkview.qml` 和 `NetworkPopup.qml` 中重复颜色定义、`isPhysicalIf`、`formatSpeed`、`formatTotal` 等,`formatSpeed` 还在 `TrafficChartWindow.qml` 中第三份拷贝。~~
已新建 `package/components/NetCommon.qml` 聚合所有公共颜色与函数。3 个文件删除重复定义改为引用 `common.xxx`,净删除 91 行重复代码。`downloadValueColor` 由属性绑定改为函数(调用方传速度值)。
**12. ~~死代码:`interfaceStats` 属性未被使用~~ ✅ 已修复**
~~`networkview.qml` 绑定了 `interfaceStats` 属性但从未读取,C++ 侧的 `interfaceStats()` 函数和 `Q_PROPERTY` 也是死代码。~~
已删除 `Q_PROPERTY``interfaceStats()` 实现、`statsChanged` 信号、QML 属性绑定。
**13. ~~死代码:`refresh()` Q_INVOKABLE 从未被 QML 调用~~ ✅ 已修复**
~~C++ 侧的 `Q_INVOKABLE void refresh()` 从未被 QML 调用。~~
已移除 `Q_INVOKABLE` 标记。`refresh()` 函数本身保留(是 `m_refreshTimer` 每秒回调),仅不再暴露给 QML。
**14. ~~`interfaceStats` 暴露的数据不完整~~ ✅ 随 #12 一并解决**
~~`NetworkInterface` 结构体解析了 `rxErrors`/`txErrors`/`rxDropped`/`txDropped`,但 `interfaceStats()` 输出时丢弃了这些字段。~~
`interfaceStats()` 已随 #12 整体删除,不再有不完整的输出。结构体字段保留供未来使用。
**15. ~~README.md 和 AGENTS.md 严重过时~~ ✅ 已修复**
~~项目结构只列 AboutWindow.qml,功能特性未提及设置窗口/字体颜色/流量波动图/IPv6/深色模式,README 写"从弹窗卸载"实际在设置窗口。~~
已更新 README.md 和 AGENTS.md 的功能特性列表、项目结构(补全 6 个组件)、C++ Backend 属性列表(移除 interfaceStats,新增 ipv6Address/version/textColor/speedHistory 等)。
**16. ~~`qsTr()` 源字符串为中文,无翻译基础设施~~ ✅ 已修复**
~~QML 中 `qsTr()` 以中文为源串,无 `.ts` 翻译文件、无 `lupdate`/`lrelease` 构建步骤、C++ 无翻译加载逻辑。~~
已将全部 44 处 `qsTr` 源串改为英文,新建 `translations/jnetapplet_zh_CN.ts` 中文翻译文件。CMake 增加 `LinguistTools` + `qt_add_translation` 编译 `.ts` -> `.qm``init()` 中按系统语言加载 `.qm` 并安装到 `qApp`
**17. C++ 后端无单元测试**
项目无任何测试。`calculateSpeed``readNetworkStats` 的正则解析、`isPhysicalInterface``niceCeil`(QML)等纯逻辑函数适合且应该有单元测试覆盖。
---
## 三、可开发的新需求 / 功能增强
### 🟢 推荐开发
| # | 需求 | 说明 | 价值 |
|---|------|------|------|
| 18 | **日/月流量统计持久化** | 将每日累计流量写入 `settings.ini`,重启后保留。弹窗/设置窗口展示今日/本月用量 | 解决缺陷 #3,核心功能提升 |
| 19 | **深色模式适配** | AboutWindow/SettingsWindow/TextColorPicker/TrafficChartWindow 改用 DTK 主题色或 `DockPalette` 派生色 | 解决缺陷 #4,视觉一致性 |
| 20 | **公共代码抽取** | 颜色/格式化/排序逻辑抽取为共享文件,消除 3 处重复 | 解决 #11,降低维护成本 |
| 21 | **文档同步** | 更新 README + AGENTS.md 的项目结构、功能列表、组件说明 | 解决 #15,新人/AI 接手必备 |
| 22 | **网络断连检测** | 活动接口 `operState != Up` 或 IP 丢失时,任务栏显示断连图标 + tooltip 提示 | 用户体验提升 |
| 23 | **全接口持续采样** | 后台为所有接口采集速度历史,切换接口时趋势图立即有数据 | 解决缺陷 #7 |
| 24 | **速度单位切换** | 设置中可选 KB/s(二进制)或 Mbps(十进制),或智能自动切换 | 国际化/习惯适配 |
### 🔵 可选增强
| # | 需求 | 说明 |
|---|------|------|
| 25 | **刷新间隔可配置** | 设置中可选 1s/2s/5s 刷新频率,降低低端设备 CPU 占用 |
| 26 | **流量超限通知** | 设置阈值,下载/上传超限时发送系统通知 |
| 27 | **趋势图时间窗口可选** | 流量波动图支持 5min / 30min / 1h 切换(需持久化历史数据) |
| 28 | **多接口聚合速度** | 任务栏可选显示所有接口合计速度,而非仅活动接口 |
| 29 | **HiDPI 适配** | 独立窗口使用相对尺寸而非固定像素,适配高分辨率屏幕 |
| 30 | **i18n 翻译基础设施** | 源串改英文 + 添加中文 `.ts` + `lrelease` 构建集成 |
| 31 | **DTK 原生窗口框架** | 独立窗口改用 `DWindow` / DTK 窗口装饰,获得原生 deepin 标题栏、圆角、阴影 |
| 32 | **导出流量数据** | 将流量统计导出为 CSV/JSON,便于分析 |
| 33 | **C++ 后端单元测试** | 为速度计算、正则解析、接口检测添加 Qt Test 单元测试 |
| 34 | **丢包/错误率展示** | 弹窗中展示 `rxErrors`/`rxDropped`,网络质量诊断 |
| 35 | **开机自启保障** | 确保 dde-shell 启动时插件自动加载(可能需 dde-shell 配置) |
---
## 优先级建议
**第一优先级**(影响正确性):#1 速度计算间隔、#2 负速度、#5 接口过滤不一致
**第二优先级**(影响体验):#4 深色模式、#3 总量语义、#7 历史采样、#15 文档过时
**第三优先级**(工程质量):#11 重复代码、#12-14 死代码、#17 测试缺失
**第四优先级**(功能扩展):#18 日/月统计、#22 断连检测、#26 流量通知
+49 -33
View File
@@ -1,11 +1,13 @@
// SPDX-FileCopyrightText: 2024 MyCompany
// SPDX-FileCopyrightText: 2026 Jokul
//
// SPDX-License-Identifier: LGPL-3.0-or-later
// 关于窗口:DTK 原生风格的独立顶层窗口,桌面居中弹出,展示插件信息
// 设计要点:白色圆角卡片 + 键值对信息布局,信息标签使用 deepin 蓝(#0081FF),
// 关闭按钮 hover 高亮色由父组件通过 accentColor 传入(红色)
// 对外依赖:accentColor,由 networkview.qml 实例化时传入,触发方式为 show()/raise()/requestActivate()
// 设计要点:圆角卡片 + 键值对信息布局,信息标签使用 deepin 蓝(#0081FF),
// 关闭按钮 hover 高亮色由父组件通过 accentColor 传入(红色)
// 深/浅主题由 isDarkMode 切换(networkview.qml 依据 DockPalette 检测任务栏深浅后传入),
// 默认浅色,保证组件独立预览时与原版视觉一致
// 对外依赖:accentColor、isDarkMode,由 networkview.qml 实例化时传入,触发方式为 show()/raise()/requestActivate()
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.15
@@ -21,6 +23,18 @@ Window {
// 默认值保证组件独立可用(如 qmlscene 预览时未传入)
property string version: "1.0"
// 深色模式标记:由 networkview.qml 根据 DockPalette 检测后传入;
// 默认 false(浅色)保证组件独立预览时与原版视觉一致
property bool isDarkMode: false
// ---- 主题感知颜色:isDarkMode 为 false 时取值与原浅色硬编码完全一致 ----
readonly property color winBg: isDarkMode ? "#202020" : "#FFFFFF"
readonly property color cardBg: isDarkMode ? "#2A2A2A" : "#FFFFFF"
readonly property color lineColor: isDarkMode ? "#383838" : "#E8E8E8"
readonly property color textPrimary: isDarkMode ? "#E0E0E0" : "#333333"
readonly property color textSecondary: isDarkMode ? "#AAAAAA" : "#666666"
readonly property color textTertiary: isDarkMode ? "#888888" : "#999999"
// 复制成功状态标记:true 时复制图标变绿并显示"已复制"提示,
// 2 秒后由 copyResetTimer 复位
property bool copied: false
@@ -34,18 +48,20 @@ Window {
onVisibleChanged: {
if (visible) {
x = (Screen.width - width) / 2
y = (Screen.height - height) / 2
// 居中到当前屏幕(任务栏所在屏幕),加 virtualX/virtualY 偏移
// 避免多显示器下窗口出现在非预期屏幕
x = Screen.virtualX + (Screen.width - width) / 2
y = Screen.virtualY + (Screen.height - height) / 2
}
}
// 窗口主体:白色圆角卡片,1px 浅灰边框模拟 DTK 窗口描边
// 窗口主体:圆角卡片(背景与边框随 isDarkMode 切换深浅)1px 边框模拟 DTK 窗口描边
Rectangle {
anchors.fill: parent
color: "#FFFFFF"
color: root.winBg
radius: 12
border.width: 1
border.color: "#E8E8E8"
border.color: root.lineColor
ColumnLayout {
anchors.fill: parent
@@ -67,10 +83,10 @@ Window {
anchors.left: parent.left
anchors.leftMargin: 16
anchors.verticalCenter: parent.verticalCenter
text: qsTr("关于")
text: qsTr("About")
font.pixelSize: 15
font.weight: Font.Bold
color: "#333333"
color: root.textPrimary
}
// 关闭按钮:28x28 圆形,hover 时淡红底 + 红色 ×(颜色由 accentColor 决定)
@@ -88,7 +104,7 @@ Window {
text: "×"
font.pixelSize: 20
font.weight: Font.Bold
color: aboutCloseMouse.containsMouse ? accentColor : "#666666"
color: aboutCloseMouse.containsMouse ? accentColor : root.textSecondary
}
MouseArea {
@@ -114,10 +130,10 @@ Window {
spacing: 2
Text {
text: qsTr("网络速度监控")
text: qsTr("Network Speed Monitor")
font.pixelSize: 18
font.weight: Font.Bold
color: "#333333"
color: root.textPrimary
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
}
@@ -125,7 +141,7 @@ Window {
Text {
text: "JNetApplet"
font.pixelSize: 12
color: "#999999"
color: root.textTertiary
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
}
@@ -142,7 +158,7 @@ Window {
Layout.leftMargin: 24
Layout.rightMargin: 24
Layout.preferredHeight: 1
color: "#E8E8E8"
color: root.lineColor
}
// 信息区上方间距
@@ -163,7 +179,7 @@ Window {
spacing: 0
Text {
text: qsTr("版本")
text: qsTr("Version")
font.pixelSize: 12
color: "#0081FF"
Layout.preferredWidth: 56
@@ -173,7 +189,7 @@ Window {
Text {
text: version
font.pixelSize: 12
color: "#333333"
color: root.textPrimary
Layout.fillWidth: true
}
}
@@ -184,7 +200,7 @@ Window {
spacing: 0
Text {
text: qsTr("作者")
text: qsTr("Author")
font.pixelSize: 12
color: "#0081FF"
Layout.preferredWidth: 56
@@ -194,7 +210,7 @@ Window {
Text {
text: "Jokul"
font.pixelSize: 12
color: "#333333"
color: root.textPrimary
Layout.fillWidth: true
}
}
@@ -205,7 +221,7 @@ Window {
spacing: 0
Text {
text: qsTr("描述")
text: qsTr("Description")
font.pixelSize: 12
color: "#0081FF"
Layout.preferredWidth: 56
@@ -213,9 +229,9 @@ Window {
}
Text {
text: qsTr("监控网络速度和流量")
text: qsTr("Monitor network speed and traffic")
font.pixelSize: 12
color: "#333333"
color: root.textPrimary
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
@@ -227,7 +243,7 @@ Window {
spacing: 0
Text {
text: qsTr("仓库")
text: qsTr("Repository")
font.pixelSize: 12
color: "#0081FF"
Layout.preferredWidth: 56
@@ -237,7 +253,7 @@ Window {
Text {
text: "git.jokul.space/Jokul/JNetApplet"
font.pixelSize: 11
color: "#999999"
color: root.textTertiary
Layout.fillWidth: true
wrapMode: Text.WrapAnywhere
}
@@ -248,7 +264,7 @@ Window {
Layout.preferredHeight: 24
Layout.alignment: Qt.AlignTop
// "已复制"提示:白底绿字小标签,显示在按钮左侧,
// "已复制"提示:卡片底色绿字小标签,显示在按钮左侧,
// 覆盖于 URL 文字上方,避免与值文本混排
Rectangle {
anchors.right: parent.left
@@ -258,14 +274,14 @@ Window {
width: copiedHintText.implicitWidth + 10
height: 18
radius: 4
color: "#FFFFFF"
color: root.cardBg
border.width: 1
border.color: "#22A34A"
Text {
id: copiedHintText
anchors.centerIn: parent
text: qsTr("已复制")
text: qsTr("Copied")
font.pixelSize: 10
color: "#22A34A"
}
@@ -286,7 +302,7 @@ Window {
}
// 复制图标:两个重叠的小圆角矩形
// 前层矩形白底压住后层边框重叠区,形成"堆叠纸张"的视觉效果
// 前层矩形以卡片底色压住后层边框重叠区,形成"堆叠纸张"的视觉效果
// 后层矩形(向右下偏移 2px
Rectangle {
x: 8
@@ -294,9 +310,9 @@ Window {
width: 10
height: 10
radius: 2
color: "#FFFFFF"
color: root.cardBg
border.width: 1
border.color: copyRepoMouse.containsMouse ? "#333333" : "#999999"
border.color: copyRepoMouse.containsMouse ? root.textPrimary : root.textTertiary
}
// 前层矩形(左上),复制成功时边框短暂变绿
@@ -306,9 +322,9 @@ Window {
width: 10
height: 10
radius: 2
color: "#FFFFFF"
color: root.cardBg
border.width: 1
border.color: copied ? "#22A34A" : (copyRepoMouse.containsMouse ? "#333333" : "#999999")
border.color: copied ? "#22A34A" : (copyRepoMouse.containsMouse ? root.textPrimary : root.textTertiary)
}
}
}
+82
View File
@@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: 2026 Jokul
//
// SPDX-License-Identifier: LGPL-3.0-or-later
// 网络监控插件的公共颜色与格式化函数
// 消除 networkview.qml 与 NetworkPopup.qml、TrafficChartWindow.qml 之间的重复定义:
// 以下颜色与函数此前在各组件内各持一份拷贝,改一处需同步多处;
// 现集中到本文件,各组件实例化 NetCommon { id: common } 后统一经 common.xxx 访问
// 设计原因:
// - 颜色统一派生自 DTK 系统调色板(windowText),跟随系统深浅主题自动适配。
// 不采用 DockPalette.iconTextPalette:实测其不随系统深色主题变化,
// 深色模式下返回深色文字(黑底黑字)、浅色模式下返回浅色文字(白底白字),方向相反;
// DTK.palette.windowText 则与面板/弹窗背景同步。主题切换时 DTK.paletteChanged
// 自动触发属性绑定重新求值(QtObject 内绑定 DTK.palette 与 Item 内行为一致,已实测验证)
// - 函数与颜色分离:downloadValueColor 依赖调用方的速度值,提取为函数由调用方传入,
// 消除对组件上下文的依赖;sortedInterfaces 依赖各组件自身的接口列表,仍留在各组件内
import QtQuick 2.15
import org.deepin.dtk 1.0
QtObject {
// ---- 主题感知颜色(派生自 DTK 系统调色板)----
// baseTextColor 为中间基准色,以下各级文字/卡片颜色均由它加透明度派生
readonly property color baseTextColor: DTK.palette.windowText
readonly property color primaryText: Qt.rgba(baseTextColor.r, baseTextColor.g, baseTextColor.b, 0.95)
readonly property color secondaryText: Qt.rgba(baseTextColor.r, baseTextColor.g, baseTextColor.b, 0.80)
readonly property color tertiaryText: Qt.rgba(baseTextColor.r, baseTextColor.g, baseTextColor.b, 0.65)
readonly property color cardBackground: Qt.rgba(baseTextColor.r, baseTextColor.g, baseTextColor.b, 0.06)
readonly property color cardBorder: Qt.rgba(baseTextColor.r, baseTextColor.g, baseTextColor.b, 0.10)
// ---- 强调色:下载蓝、上传绿,是面板与任务栏的主视觉区分 ----
readonly property color accentBlue: Qt.rgba(20 / 255, 80 / 255, 160 / 255, 1)
readonly property color accentBlueLight: Qt.rgba(20 / 255, 80 / 255, 160 / 255, 0.12)
readonly property color accentGreen: Qt.rgba(22 / 255, 163 / 255, 74 / 255, 1)
readonly property color accentGreenLight: Qt.rgba(22 / 255, 163 / 255, 74 / 255, 0.12)
// ---- 高速警示色:下载速度超过阈值时由蓝转橙再转红 ----
readonly property color accentOrange: Qt.rgba(245 / 255, 158 / 255, 11 / 255, 1)
readonly property color accentRed: Qt.rgba(220 / 255, 38 / 255, 38 / 255, 1)
// 下载值颜色:保留原有阈值逻辑(>10MB/s 红、>1MB/s 橙、否则蓝),仅作用于下载值
// 原为各组件内的属性绑定(依赖组件自身的 downloadSpeed),提取为公共函数后
// 由调用方传入速度值;在绑定表达式中调用时,绑定仍随速度变化自动重算,行为不变
function downloadValueColor(downloadSpeed) {
if (downloadSpeed > 10 * 1024 * 1024) return accentRed
if (downloadSpeed > 1 * 1024 * 1024) return accentOrange
return accentBlue
}
// 上传值颜色:固定绿色,不做高速警示
readonly property color uploadValueColor: accentGreen
// 判断是否为物理网卡(QML 侧排序用,与 C++ isPhysicalInterface 逻辑一致)
function isPhysicalIf(name) {
return name.startsWith("wlp") || name.startsWith("wlan")
|| name.startsWith("enp") || name.startsWith("eth")
}
// 格式化速度显示(带单位,用于弹出面板,信息更完整)
// 最小单位为 KB,与 networkview.qml 的 formatSpeedShort 保持一致;所有级别保留 2 位小数
function formatSpeed(bytesPerSec) {
if (bytesPerSec < 1024 * 1024) {
return (bytesPerSec / 1024).toFixed(2) + " KB/s"
} else if (bytesPerSec < 1024 * 1024 * 1024) {
return (bytesPerSec / (1024 * 1024)).toFixed(2) + " MB/s"
} else {
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(2) + " GB/s"
}
}
// 格式化总量显示(用于弹出面板的累计统计)
function formatTotal(bytes) {
if (bytes < 1024) {
return bytes.toFixed(0) + " B"
} else if (bytes < 1024 * 1024) {
return (bytes / 1024).toFixed(1) + " KB"
} else if (bytes < 1024 * 1024 * 1024) {
return (bytes / (1024 * 1024)).toFixed(1) + " MB"
} else {
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB"
}
}
}
+469
View File
@@ -0,0 +1,469 @@
// SPDX-FileCopyrightText: 2026 Jokul
//
// SPDX-License-Identifier: LGPL-3.0-or-later
// 网络速度监控弹窗内容组件
// 展示实时速度、总量统计、网卡切换 chip 列表;
// 由 networkview.qml 的 PanelPopup 实例化,传入 applet 属性;
// 颜色与格式化函数统一取自同目录 NetCommon.qmlcommon.xxx),数据由 applet 提供
// 设计原因:PanelPopup 是 dock 上下文类型,不能作为独立组件根元素,
// 因此本组件用 Control 作为根,由 networkview.qml 的 PanelPopup 包裹
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import org.deepin.ds.dock 1.0
import org.deepin.dtk 1.0
import "."
Control {
id: popup
// 公共颜色与格式化函数:集中定义于同目录 NetCommon.qml
// 与 networkview.qml、TrafficChartWindow 共享同一份实现,消除跨组件重复定义
NetCommon { id: common }
// C++ 后端对象,提供速度/接口/IP 等所有数据
property var applet: null
readonly property bool ready: applet ? applet.ready : false
readonly property real downloadSpeed: applet ? applet.downloadSpeed : 0
readonly property real uploadSpeed: applet ? applet.uploadSpeed : 0
readonly property real totalDownload: applet ? applet.totalDownload : 0
readonly property real totalUpload: applet ? applet.totalUpload : 0
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 || []) : []
// 弹窗颜色别名:转发 common 的公共颜色(派生自 DTK 系统调色板 windowText
// 主题适配的设计原因见 NetCommon.qml 文件头注释),保持下游 popup.xxx 引用不变
readonly property color primaryText: common.primaryText
readonly property color secondaryText: common.secondaryText
readonly property color tertiaryText: common.tertiaryText
readonly property color cardBackground: common.cardBackground
readonly property color cardBorder: common.cardBorder
// 强调色别名:下载蓝、上传绿,是面板的主视觉区分
readonly property color accentBlue: common.accentBlue
readonly property color accentBlueLight: common.accentBlueLight
readonly property color accentGreen: common.accentGreen
readonly property color accentGreenLight: common.accentGreenLight
// 上传值颜色:固定绿色,不做高速警示
// 下载值颜色带阈值逻辑(>10MB/s 红、>1MB/s 橙、否则蓝),已提取为
// common.downloadValueColor(speed) 函数,在使用处直接传入 downloadSpeed 调用
readonly property color uploadValueColor: common.uploadValueColor
// 排序后的接口列表:物理网卡在前,虚拟网卡在后,各自按名称排序
// 设计原因:保持稳定排序,活动接口不再移到最前,避免切换网卡时 chip 顺序跳动;
// 活动 chip 若被截断,由 chipFlickable 自动滚动露出完整样式;
// 物理网卡判断逻辑共用 common.isPhysicalIf(与 C++ isPhysicalInterface 一致)
readonly property var sortedInterfaces: {
if (!popup.ready || popup.networkInterfaces.length === 0) return []
var physical = []
var virtual = []
for (var i = 0; i < popup.networkInterfaces.length; i++) {
var name = popup.networkInterfaces[i]
if (common.isPhysicalIf(name)) physical.push(name)
else virtual.push(name)
}
physical.sort()
virtual.sort()
return physical.concat(virtual)
}
// 供外部调用:popup 打开时滚动到活动 chip
function scrollToActiveChip(animated) {
chipFlickable.scrollToActiveChip(animated)
}
padding: 16
contentItem: ColumnLayout {
spacing: 12
// 标题栏:标题在上,网卡名+IP 在卡片底色背景内
Item {
Layout.fillWidth: true
Layout.preferredHeight: 72
ColumnLayout {
anchors.centerIn: parent
spacing: 6
Text {
text: qsTr("Network Speed Monitor")
font.pixelSize: 15
font.weight: Font.Bold
color: popup.primaryText
Layout.alignment: Qt.AlignHCenter
}
// 网卡名 + IP 地址卡片背景
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 300
// 有 IPv6 时增高以容纳第三行;无 IPv6 时保持原 40px
Layout.preferredHeight: popup.ipv6Address ? 54 : 40
color: popup.cardBackground
radius: 8
border.width: 1
border.color: popup.cardBorder
visible: popup.ready
ColumnLayout {
anchors.centerIn: parent
spacing: 0
Text {
text: popup.activeInterface
font.pixelSize: 11
color: popup.secondaryText
Layout.alignment: Qt.AlignHCenter
}
Text {
text: popup.ipAddress ? qsTr("IPv4: ") + popup.ipAddress : ""
font.pixelSize: 13
color: popup.secondaryText
visible: popup.ipAddress !== ""
Layout.alignment: Qt.AlignHCenter
}
// IPv6 全球地址行,无 IPv6 时隐藏不占位
Text {
text: popup.ipv6Address ? qsTr("IPv6: ") + popup.ipv6Address : ""
font.pixelSize: 13
color: popup.secondaryText
visible: popup.ipv6Address !== ""
Layout.alignment: Qt.AlignHCenter
}
}
}
}
}
// 实时速度卡片:双列布局,每列内容在各自半区内水平垂直居中
// 设计原因:用显式 anchor 分两个等宽半区,比 RowLayout+fillWidth 更可靠,
// 确保下载/上传内容各自在左/右半区正中显示
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 110
color: popup.cardBackground
radius: 12
border.width: 1
border.color: popup.cardBorder
// 下载半区(左半)
Item {
id: downloadHalf
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.leftMargin: 12
width: (parent.width - 1) / 2 - 12
ColumnLayout {
anchors.centerIn: parent
spacing: 4
Rectangle {
width: 32
height: 32
radius: 16
color: popup.accentBlueLight
Layout.alignment: Qt.AlignHCenter
Text {
anchors.centerIn: parent
text: "↓"
font.pixelSize: 18
font.weight: Font.Bold
color: popup.accentBlue
}
}
Text {
text: qsTr("Download")
font.pixelSize: 11
color: popup.secondaryText
Layout.alignment: Qt.AlignHCenter
}
Text {
text: common.formatSpeed(popup.downloadSpeed)
font.pixelSize: 18
font.weight: Font.Bold
color: common.downloadValueColor(popup.downloadSpeed)
Layout.alignment: Qt.AlignHCenter
}
}
}
// 中间分隔线
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 12
anchors.bottom: parent.bottom
anchors.bottomMargin: 12
width: 1
color: popup.cardBorder
}
// 上传半区(右半)
Item {
id: uploadHalf
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.rightMargin: 12
width: (parent.width - 1) / 2 - 12
ColumnLayout {
anchors.centerIn: parent
spacing: 4
Rectangle {
width: 32
height: 32
radius: 16
color: popup.accentGreenLight
Layout.alignment: Qt.AlignHCenter
Text {
anchors.centerIn: parent
text: "↑"
font.pixelSize: 18
font.weight: Font.Bold
color: popup.accentGreen
}
}
Text {
text: qsTr("Upload")
font.pixelSize: 11
color: popup.secondaryText
Layout.alignment: Qt.AlignHCenter
}
Text {
text: common.formatSpeed(popup.uploadSpeed)
font.pixelSize: 18
font.weight: Font.Bold
color: popup.uploadValueColor
Layout.alignment: Qt.AlignHCenter
}
}
}
}
// 总量统计:单行紧凑
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 44
color: popup.cardBackground
radius: 12
border.width: 1
border.color: popup.cardBorder
visible: popup.ready
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
Item { Layout.fillWidth: true }
Text {
text: qsTr("Total")
font.pixelSize: 11
font.weight: Font.Bold
color: popup.secondaryText
}
Text {
text: "↓ " + common.formatTotal(popup.totalDownload)
font.pixelSize: 12
color: popup.primaryText
}
Text {
text: "↑ " + common.formatTotal(popup.totalUpload)
font.pixelSize: 12
color: popup.primaryText
}
Item { Layout.fillWidth: true }
}
}
// 接口切换 chip 列表:水平滚动,物理网卡在前、虚拟网卡在后,顺序稳定不随切换变化
// chip 少时分散撑满一行;chip 多超出宽度时固定宽度 + 水平滚动
// 活动 chip 不在可视区域或被截断时,自动滚动露出完整样式(见 scrollToActiveChip
Flickable {
id: chipFlickable
Layout.fillWidth: true
// 高度需容纳 chip28px)+ 水平滚动条(~6px)+ 上下间距,
// 此前 32px 仅剩 4px 给滚动条,导致滚动条无法正常显示
Layout.preferredHeight: 40
visible: popup.ready && popup.networkInterfaces.length > 1
contentWidth: Math.max(chipRow.width, width)
contentHeight: chipRow.height
flickableDirection: Flickable.HorizontalFlick
clip: true
// 平滑滚动动画:直接赋值 contentX 会被 Flickable 的拖拽/回弹行为覆盖,
// 用 NumberAnimation 驱动 contentX 实现自动滚动,平滑且不冲突
NumberAnimation {
id: chipScrollAnim
target: chipFlickable
property: "contentX"
duration: 200
easing.type: Easing.OutCubic
}
// 用户开始拖拽/滑动时停止自动滚动,避免动画与用户操作争抢
onMovementStarted: chipScrollAnim.stop()
// 自动滚动使活动接口的 chip 完整可见:
// 列表顺序稳定后活动 chip 可能位于可视区域外或被截断,
// 切换接口或 popup 打开时调用,将其滚动露出完整样式。
// animated=false 用于 popup 刚打开时立即定位,跳过滚动过程
function scrollToActiveChip(animated) {
if (!visible || width <= 0) return
var idx = popup.sortedInterfaces.indexOf(popup.activeInterface)
if (idx < 0) return
var chip = chipRepeater.itemAt(idx)
if (!chip) return
var targetX = contentX
if (chip.x < contentX) {
// chip 在可视区左侧之外:回滚使 chip 左边缘与可视区左缘对齐
targetX = chip.x
} else if (chip.x + chip.width > contentX + width) {
// chip 在可视区右侧之外或被截断:滚动使 chip 右边缘与可视区右缘对齐
targetX = chip.x + chip.width - width
}
// 钳制到合法滚动范围 [0, contentWidth - width],避免触发边界回弹
targetX = Math.max(0, Math.min(targetX, Math.max(0, contentWidth - width)))
if (Math.abs(targetX - contentX) < 1) return
chipScrollAnim.stop()
if (animated) {
chipScrollAnim.to = targetX
chipScrollAnim.start()
} else {
contentX = targetX
}
}
// 活动接口变化时(用户点击 chip 或后端自动切换)自动滚动露出活动 chip
Connections {
target: popup
function onActiveInterfaceChanged() {
chipFlickable.scrollToActiveChip(true)
}
}
// 等宽分配宽度:假设所有 chip 等宽撑满时的单个宽度
readonly property real equalChipWidth: popup.sortedInterfaces.length > 0
? (width - 6 * (popup.sortedInterfaces.length - 1)) / popup.sortedInterfaces.length
: 0
Row {
id: chipRow
spacing: 6
Repeater {
id: chipRepeater
model: popup.sortedInterfaces
Rectangle {
// 宽度取等宽和自然宽度的较大值:
// chip 少时 equalChipWidth > 自然宽度 -> 分散撑满一行
// chip 多时 自然宽度 > equalChipWidth -> 固定宽度 + 水平滚动
width: Math.max(chipFlickable.equalChipWidth, chipText.implicitWidth + 24)
height: 28
radius: 8
color: modelData === popup.activeInterface
? popup.accentBlue
: (chipMouse.containsMouse ? popup.accentBlueLight : popup.cardBackground)
border.width: 1
border.color: modelData === popup.activeInterface
? popup.accentBlue
: popup.cardBorder
Text {
id: chipText
anchors.centerIn: parent
text: modelData
font.pixelSize: 11
font.weight: modelData === popup.activeInterface ? Font.Bold : Font.Normal
color: modelData === popup.activeInterface ? "white" : popup.primaryText
}
MouseArea {
id: chipMouse
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: {
if (popup.applet) popup.applet.setActiveInterface(modelData)
}
}
}
}
}
// 自定义滚动条样式:默认 ScrollBar 在面板配色下可能不可见,
// 用 contentItem 显式绘制,颜色派生自 basePalette 适配深浅主题
ScrollBar.horizontal: ScrollBar {
id: chipScrollBar
policy: ScrollBar.AsNeeded
interactive: true
contentItem: Rectangle {
implicitHeight: 4
radius: 2
// 按压时加深、悬停时中等、默认稍淡,保证滚动条在深浅主题下均可见
color: chipScrollBar.pressed ? popup.secondaryText
: (chipScrollBar.hovered ? popup.secondaryText : popup.tertiaryText)
}
}
}
// 未检测到接口占位
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 80
color: popup.cardBackground
radius: 12
border.width: 1
border.color: popup.cardBorder
visible: !popup.ready
ColumnLayout {
anchors.centerIn: parent
spacing: 6
Text {
text: "⚠"
font.pixelSize: 24
color: popup.secondaryText
Layout.alignment: Qt.AlignHCenter
}
Text {
text: qsTr("No network interface detected")
font.pixelSize: 12
color: popup.secondaryText
Layout.alignment: Qt.AlignHCenter
}
}
}
Item { Layout.fillHeight: true }
}
}
+562
View File
@@ -0,0 +1,562 @@
// SPDX-FileCopyrightText: 2026 Jokul
//
// SPDX-License-Identifier: LGPL-3.0-or-later
// 设置窗口:独立顶层窗口,桌面居中弹出,提供网络接口选择、字体颜色选择、插件卸载功能
// 设计要点:圆角卡片 + 自定义标题栏可拖动,接口单选切换,卸载按钮带复制命令功能;
// 深/浅主题由 isDarkMode 切换(networkview.qml 依据 DockPalette 检测任务栏深浅后传入),
// 默认浅色,保证组件独立预览时与原版视觉一致
// 对外依赖:appletC++ 后端对象)、networkInterfaces(接口列表)、activeInterface(当前接口)、
// accentColor(强调色)、isDarkMode(深色模式标记),由 networkview.qml 实例化时传入,
// 触发方式为 show()/raise()/requestActivate()
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.15
Window {
id: root
// 对外依赖:C++ 后端对象,用于获取/设置接口、字体颜色等
property var applet: null
// 网络接口名称列表,由父组件从 C++ 后端 applet.networkInterfaces 传入
property var networkInterfaces: []
// 当前活动接口名称,由父组件从 C++ 后端 applet.activeInterface 传入
property string activeInterface: ""
// 强调色(红色),关闭按钮 hover 态文字高亮色,由父组件传入
// 默认值与 networkview.qml 中 root.accentRed 一致,确保独立可用
property color accentColor: Qt.rgba(220 / 255, 38 / 255, 38 / 255, 1)
// 深色模式标记:由 networkview.qml 根据 DockPalette 检测后传入;
// 默认 false(浅色)保证组件独立预览时与原版视觉一致
property bool isDarkMode: false
// ---- 主题感知颜色:isDarkMode 为 false 时取值与原浅色硬编码完全一致 ----
readonly property color winBg: isDarkMode ? "#202020" : "#F5F5F5"
readonly property color cardBg: isDarkMode ? "#2A2A2A" : "#FFFFFF"
readonly property color lineColor: isDarkMode ? "#383838" : "#E0E0E0"
readonly property color textPrimary: isDarkMode ? "#E0E0E0" : "#333333"
readonly property color textSecondary: isDarkMode ? "#AAAAAA" : "#666666"
readonly property color textTertiary: isDarkMode ? "#888888" : "#999999"
readonly property color iconGray: isDarkMode ? "#AAAAAA" : "#777777"
readonly property color hoverBg: isDarkMode ? "#353535" : "#F0F0F0"
readonly property color codeBg: isDarkMode ? "#2A2A2A" : "#E8E8E8"
// 选中态蓝色系:深色模式下背景压暗、文字改浅蓝,保证深色卡片上的对比度
readonly property color selBg: isDarkMode ? "#1A3A5C" : "#E3F2FD"
readonly property color selBgHover: isDarkMode ? "#1E4A6E" : "#D8ECFD"
readonly property color selBorder: isDarkMode ? "#2C5A8A" : "#90CAF9"
readonly property color selText: isDarkMode ? "#64B5F6" : "#1565C0"
// 卸载按钮文字:复制命令后临时显示提示,定时器到期后还原
property string uninstallButtonText: qsTr("Uninstall Plugin")
width: 350
height: 390
minimumWidth: 350
maximumWidth: 350
minimumHeight: 390
maximumHeight: 390
visible: false
flags: Qt.FramelessWindowHint | Qt.Window
// NonModal:不阻塞桌面其他区域,用户可同时操作任务栏
modality: Qt.NonModal
// 窗口透明:让圆角外的区域不显示,由内部 Rectangle 提供可见背景
color: "transparent"
// 根据接口名返回类型描述,用于设置窗口网络接口列表
// 设计原因:用户面对多个网口时难以仅凭 enp3s0/wlp3s0 等命名判断用途,
// 加一行类型说明(有线/无线/VPN 等)降低认知负担
function interfaceDescription(name) {
if (name === "lo") return qsTr("Loopback")
if (name === "Meta") return qsTr("Virtual Interface")
if (/^enp|^eth/.test(name)) return qsTr("Wired Network")
if (/^wlp|^wlan/.test(name)) return qsTr("Wireless Network")
if (/^docker|^veth/.test(name)) return qsTr("Container Network")
if (/^br/.test(name)) return qsTr("Bridge")
if (/^tun|^tap/.test(name)) return qsTr("VPN")
if (/^virbr/.test(name)) return qsTr("Virtual Bridge")
return qsTr("Other")
}
// 根据接口名返回类型图标(Unicode 符号),用于网络接口列表每行左侧
// 设计原因:项目未引入图标库,用 Text 渲染 Unicode 符号(与 networkview.qml 的 ↓↑ 一致);
// 所选符号均为 DejaVu/Noto 等常见字体覆盖的单色字形,避免彩色 emoji 破坏浅色主题观感,
// 匹配规则与 interfaceDescription 保持一致
function interfaceIcon(name) {
if (name === "lo") return "↻" // 本地回环:循环箭头
if (name === "Meta") return "▢" // 虚拟接口:空心方块
if (/^enp|^eth/.test(name)) return "⇄" // 有线网络:双向链路
if (/^wlp|^wlan/.test(name)) return "∿" // 无线网络:信号波形
if (/^docker|^veth/.test(name)) return "▣" // 容器网络:盒中盒
if (/^br/.test(name)) return "⋈" // 桥接:连接(蝴蝶结形)
if (/^tun|^tap/.test(name)) return "⚿" // VPN:钥匙
if (/^virbr/.test(name)) return "⋈" // 虚拟桥接:同桥接
return "◉" // 其他:通用网络节点
}
// 显示时居中到当前屏幕(任务栏所在屏幕)
onVisibleChanged: {
if (visible) {
// 加 virtualX/virtualY 偏移,避免多显示器下窗口出现在非预期屏幕
x = Screen.virtualX + (Screen.width - width) / 2
y = Screen.virtualY + (Screen.height - height) / 2
}
}
// 主容器:提供不透明背景(随 isDarkMode 切换深浅)+ 圆角 + 边框
Rectangle {
anchors.fill: parent
color: root.winBg
radius: 12
border.width: 1
border.color: root.lineColor
ColumnLayout {
anchors.fill: parent
anchors.margins: 0
spacing: 0
// 自定义标题栏(可拖动)
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 44
color: "transparent"
// 拖动层:声明在关闭按钮之前(位于其下方),避免遮挡按钮点击与 hover
// 用 startSystemMove() 系统级窗口拖动,由窗口管理器接管移动,
// 这是 frameless Window 的正确做法,pressed 即触发,丝滑无抖动
MouseArea {
anchors.fill: parent
cursorShape: Qt.OpenHandCursor
onPressed: root.startSystemMove()
}
// 标题文字
Text {
anchors.left: parent.left
anchors.leftMargin: 16
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Settings")
font.pixelSize: 15
font.weight: Font.Bold
color: root.textPrimary
}
// 关闭按钮
Rectangle {
id: closeButton
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
width: 28
height: 28
radius: 14
color: closeMouse.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: closeMouse.containsMouse ? accentColor : root.textSecondary
}
MouseArea {
id: closeMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.hide()
}
}
}
// 分隔线
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: root.lineColor
}
// 内容区域
Item {
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
// 网络接口选择区
Text {
text: qsTr("Network Interface")
font.pixelSize: 13
font.weight: Font.Bold
color: root.textSecondary
}
// 接口列表容器:最多显示约 5 行(160px),超出部分垂直滚动
Rectangle {
Layout.fillWidth: true
// 高度 = 上下边距 10*2 + 列表可视高度;可视高度 = min(内容高度, 160)
// 内容高度 = 行高 30*n + 行间距 2*(n-1),接口少时卡片随行数收缩
Layout.preferredHeight: networkInterfaces.length > 0
? Math.min(ifaceColumn.implicitHeight, 160) + 20
: 80
color: root.cardBg
radius: 10
border.width: 1
border.color: root.lineColor
// 用 ScrollView 而非裸 Flickable:自带滚轮滚动支持,
// 且滚动条显示时自动让出内容宽度,不会遮挡行内文字
ScrollView {
id: ifaceScrollView
anchors.fill: parent
anchors.margins: 10
visible: networkInterfaces.length > 0
clip: true
// 内容宽度跟随可视宽度(自动减去滚动条占位),禁止水平滚动
contentWidth: availableWidth
// 自定义滚动条样式:与 networkview.qml chip 滚动条一致(4px 圆角矩形),
// 颜色使用主题感知灰色系(随 isDarkMode 切换深浅);
// AsNeeded 策略保证接口少时不显示滚动条
ScrollBar.vertical: ScrollBar {
id: ifaceScrollBar
policy: ScrollBar.AsNeeded
interactive: true
contentItem: Rectangle {
implicitWidth: 4
radius: 2
// 按压/悬停加深、默认稍淡,保证滚动条在卡片背景上可见
color: ifaceScrollBar.pressed ? root.textSecondary
: (ifaceScrollBar.hovered ? root.textSecondary : root.textTertiary)
}
}
Column {
id: ifaceColumn
width: ifaceScrollView.availableWidth
// 行间距 2:紧凑列表,各行 hover 背景不粘连
spacing: 2
// 接口列表:整行点击切换活动接口
// 行样式:类型图标 + 单选圆点 + 名称 + 右侧类型描述;
// hover 底、选中底与边框颜色均随 isDarkMode 切换深浅
Repeater {
model: networkInterfaces
delegate: Rectangle {
id: ifaceRow
width: ifaceColumn.width
height: 30
radius: 8
// 当前行是否为活动接口(选中态)
readonly property bool isActive: modelData === activeInterface
readonly property bool hovered: rowMouse.containsMouse
// 选中态强调色由 root.selText 提供(浅色 #1565c0 / 深色 #64b5f6):
// 保证 ▢ 等空心图标在选中背景上仍有足够对比度
readonly property color activeColor: root.selText
// 背景优先级:选中 > hover;选中行 hover 时稍加深以保留交互反馈
color: isActive
? (hovered ? root.selBgHover : root.selBg)
: (hovered ? root.hoverBg : "transparent")
// 选中行加细边框增强视觉;未选中保持透明边框占位,避免切换时出现 1px 抖动
border.width: 1
border.color: isActive ? root.selBorder : "transparent"
// 整行 hover + 点击:行内元素均不处理鼠标事件,
// 点击行任意位置即选中该接口
MouseArea {
id: rowMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: if (applet) applet.setActiveInterface(modelData)
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
spacing: 8
// 网卡类型图标:固定宽度居中,保证不同字形的视觉对齐
Text {
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: 20
horizontalAlignment: Text.AlignHCenter
text: interfaceIcon(modelData)
font.pixelSize: 14
color: ifaceRow.isActive ? ifaceRow.activeColor : root.iconGray
}
// 自绘单选圆点:替代 RadioButton 作纯视觉指示器,
// 避免 DTK 样式渲染差异(本文件早期版本 RadioButton 标签渲染异常),
// 选中态颜色完全可控;点击由整行 MouseArea 统一处理
Rectangle {
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: 16
Layout.preferredHeight: 16
radius: 8
color: "transparent"
border.width: 2
border.color: ifaceRow.isActive ? ifaceRow.activeColor
: (ifaceRow.hovered ? root.textSecondary : root.textTertiary)
Rectangle {
anchors.centerIn: parent
width: 8
height: 8
radius: 4
visible: ifaceRow.isActive
color: ifaceRow.activeColor
}
}
// 接口名称:过长时右侧省略
Text {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
text: modelData
font.pixelSize: 13
font.weight: ifaceRow.isActive ? Font.Medium : Font.Normal
color: ifaceRow.isActive ? ifaceRow.activeColor : root.textPrimary
elide: Text.ElideRight
}
// 接口类型描述:右对齐显示,与接口名分居两侧
Text {
Layout.alignment: Qt.AlignVCenter
text: interfaceDescription(modelData)
font.pixelSize: 12
color: root.textTertiary
}
}
}
}
}
}
// 无接口提示:在卡片内居中
Text {
anchors.centerIn: parent
text: qsTr("No network interface detected")
font.pixelSize: 12
color: root.textTertiary
visible: networkInterfaces.length === 0
}
}
// 字体颜色选择区:预设色板 + 跟随系统选项
// 设计原因:用户选择的颜色通过 applet.textColor 持久化到
// ~/.config/jnetapplet/settings.ini,重启 dde-shell 后仍生效
Text {
text: qsTr("Font Color")
font.pixelSize: 13
font.weight: Font.Bold
color: root.textSecondary
}
TextColorPicker {
Layout.fillWidth: true
currentColor: applet ? applet.textColor : ""
// 深色模式标记向下传递:TextColorPicker 内部色板卡片同样主题感知
isDarkMode: root.isDarkMode
onColorSelected: if (applet) applet.textColor = color
}
Item { Layout.preferredHeight: 4 }
// 卸载插件按钮:文字可动态切换(复制命令后显示提示)
// 提示状态期间按钮不可点击,文字变绿色
Rectangle {
id: uninstallButton
Layout.fillWidth: true
Layout.preferredHeight: 40
// 提示状态时背景和边框变绿
readonly property bool isStatus: uninstallButtonText !== qsTr("Uninstall Plugin")
color: isStatus
? Qt.rgba(22/255, 163/255, 74/255, 0.08)
: (uninstallMouse.containsMouse ? Qt.rgba(220/255, 38/255, 38/255, 0.15) : Qt.rgba(220/255, 38/255, 38/255, 0.08))
radius: 10
border.width: 1
border.color: isStatus ? Qt.rgba(22/255, 163/255, 74/255, 1) : accentColor
Text {
anchors.centerIn: parent
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 8
text: uninstallButtonText
font.pixelSize: uninstallButton.isStatus ? 11 : 13
font.weight: Font.Medium
// 提示状态时文字绿色,否则红色
color: uninstallButton.isStatus ? Qt.rgba(22/255, 163/255, 74/255, 1) : accentColor
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
MouseArea {
id: uninstallMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
// 提示状态期间禁用点击
enabled: !uninstallButton.isStatus
onClicked: uninstallConfirmDialog.open()
}
}
}
}
}
}
// 卸载确认对话框:在设置窗口内水平居中
Dialog {
id: uninstallConfirmDialog
width: 320
height: 260
modal: true
visible: false
// 在设置窗口中心显示
x: (root.width - width) / 2
y: (root.height - height) / 2
background: Rectangle {
color: root.winBg
radius: 12
border.width: 1
border.color: root.lineColor
}
contentItem: ColumnLayout {
spacing: 16
anchors.fill: parent
anchors.margins: 20
Text {
text: qsTr("Warning")
font.pixelSize: 16
font.weight: Font.Bold
color: accentColor
Layout.alignment: Qt.AlignHCenter
}
Text {
text: qsTr("The following command uninstalls the plugin and restarts dde-shell. Please copy and run it in terminal:")
font.pixelSize: 12
color: root.textPrimary
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
// 命令显示区域
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 60
color: root.codeBg
radius: 8
Text {
anchors.fill: parent
anchors.margins: 10
text: "sudo rm -rf /usr/share/dde-shell/space.jokul.JNetApplet/ && systemctl --user restart dde-shell@DDE"
font.pixelSize: 10
font.family: "monospace"
color: root.textSecondary
wrapMode: Text.WordWrap
}
}
Item { Layout.fillHeight: true }
// 按钮区域
RowLayout {
Layout.fillWidth: true
spacing: 12
// 取消按钮
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
color: cancelUninstallMouse.containsMouse ? root.hoverBg : root.cardBg
radius: 8
border.width: 1
border.color: root.lineColor
Text {
anchors.centerIn: parent
text: qsTr("Cancel")
font.pixelSize: 13
color: root.textPrimary
}
MouseArea {
id: cancelUninstallMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: uninstallConfirmDialog.close()
}
}
// 复制命令按钮
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
color: confirmUninstallMouse.containsMouse ? accentColor : Qt.rgba(220/255, 38/255, 38/255, 0.8)
radius: 8
Text {
anchors.centerIn: parent
text: qsTr("Copy Command")
font.pixelSize: 13
font.weight: Font.Medium
color: "white"
}
MouseArea {
id: confirmUninstallMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// 复制卸载命令到剪贴板
clipboardHelper.selectAll()
clipboardHelper.copy()
uninstallConfirmDialog.close()
// 卸载按钮文字临时替换为复制成功提示,5 秒后还原
uninstallButtonText = qsTr("Uninstall command copied to clipboard. Please paste and run it in terminal.")
statusHideTimer.start()
}
}
}
}
}
}
// 隐藏的 TextEdit 用于复制卸载命令到剪贴板
// QML 没有直接剪贴板 API,用 TextEdit.selectAll()+copy() 实现
TextEdit {
id: clipboardHelper
visible: false
text: "sudo rm -rf /usr/share/dde-shell/space.jokul.JNetApplet/ && systemctl --user restart dde-shell@DDE"
}
// 卸载按钮文字还原定时器:复制命令后 5 秒将按钮文字从提示还原为"卸载插件"
Timer {
id: statusHideTimer
interval: 5000
onTriggered: uninstallButtonText = qsTr("Uninstall Plugin")
}
}
+134
View File
@@ -0,0 +1,134 @@
// SPDX-FileCopyrightText: 2026 Jokul
//
// SPDX-License-Identifier: LGPL-3.0-or-later
// 字体颜色选择器:用于设置窗口,提供预设色板与"跟随系统"选项
// 设计要点:圆角卡片与设置窗口其他分区视觉一致;圆形色块 22x22,
// 当前选中色加 2px 边框高亮(颜色随主题切换);点击只发射 colorSelected 信号,
// 由父层(SettingsWindow)回写 currentColor 并同步到 C++ 后端持久化;
// 深/浅主题由 isDarkMode 切换(SettingsWindow 传入),默认浅色保证独立预览可用
// 属性语义:
// currentColor - 当前选中颜色字符串,空串表示"跟随系统"(默认)
// colorSelected(string) - 用户点击色块时发射,参数为空串或 #RRGGBB
import QtQuick 2.15
import QtQuick.Layouts 1.15
Rectangle {
id: root
// 当前选中颜色:空串=跟随系统,用于显示选中态高亮
property string currentColor: ""
// 用户点击色块时发射,父层据此回写 currentColor 并持久化
signal colorSelected(string color)
// 预设色板:8 个常用色,覆盖白/黑/暖/冷,克制实用
property var presetColors: [
"#FFFFFF", "#000000", "#DC2626", "#F59E0B",
"#16A34A", "#1450A0", "#7C3AED", "#0891B2"
]
// 深色模式标记:由 SettingsWindow 传入;默认 false(浅色)保证独立预览可用
property bool isDarkMode: false
// ---- 主题感知颜色:isDarkMode 为 false 时取值与原浅色硬编码完全一致 ----
readonly property color cardBg: isDarkMode ? "#2A2A2A" : "#FFFFFF"
readonly property color lineColor: isDarkMode ? "#383838" : "#E0E0E0"
readonly property color ringColor: isDarkMode ? "#E0E0E0" : "#333333"
// "跟随系统"色块底色:深色模式下用比卡片更深的灰保持可辨识
readonly property color systemSwatchBg: isDarkMode ? "#202020" : "#F5F5F5"
Layout.fillWidth: true
Layout.preferredHeight: 52
color: root.cardBg
radius: 10
border.width: 1
border.color: root.lineColor
// Canvas 不随属性绑定自动重绘,主题切换时主动触发"跟随系统"斜线重绘
onIsDarkModeChanged: slashCanvas.requestPaint()
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
// 预设色块
Repeater {
model: root.presetColors
delegate: Rectangle {
width: 26
height: 26
radius: 13
color: "transparent"
// 当前选中态:2px 边框高亮(颜色随主题切换)
border.width: root.currentColor === modelData ? 2 : 0
border.color: root.ringColor
Rectangle {
anchors.centerIn: parent
width: 22
height: 22
radius: 11
// 白色色块在白卡上需细边框可见;深色模式下深色卡片上的黑色色块同理
color: modelData
border.width: (modelData === "#FFFFFF"
|| (root.isDarkMode && modelData === "#000000")) ? 1 : 0
border.color: root.lineColor
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.colorSelected(modelData)
}
}
}
// 跟随系统选项:斜线色块表示恢复默认
Rectangle {
width: 26
height: 26
radius: 13
color: "transparent"
// currentColor 为空串时高亮(跟随系统选中态)
border.width: root.currentColor === "" ? 2 : 0
border.color: root.ringColor
Rectangle {
anchors.centerIn: parent
width: 22
height: 22
radius: 11
color: root.systemSwatchBg
border.width: 1
border.color: root.lineColor
// 斜线:表示"无自定义色/跟随系统"
Canvas {
id: slashCanvas
anchors.fill: parent
onPaint: {
var ctx = getContext("2d")
ctx.reset()
ctx.strokeStyle = root.isDarkMode ? "#888888" : "#999999"
ctx.lineWidth = 2
ctx.beginPath()
ctx.moveTo(2, height - 2)
ctx.lineTo(width - 2, 2)
ctx.stroke()
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.colorSelected("")
}
}
// 弹性空间,让色块左对齐
Item { Layout.fillWidth: true }
}
}
+604
View File
@@ -0,0 +1,604 @@
// SPDX-FileCopyrightText: 2026 Jokul
//
// SPDX-License-Identifier: LGPL-3.0-or-later
// 流量波动图窗口:屏幕居中的独立顶层窗口,展示当前活动接口最近 5 分钟的
// 网速趋势(下载/上传双折线图),随 C++ 后端 speedHistoryChanged 信号每秒动态刷新
// 设计要点:
// - 视觉风格复用 AboutWindow.qml(圆角卡片 12px、1px 边框、44px 标题栏、
// 28x28 圆形关闭按钮 hover 淡红底);深/浅主题由 isDarkMode 切换
// networkview.qml 依据 DockPalette 检测任务栏深浅后传入),默认浅色保证独立预览可用
// - 图表使用纯 QML Canvas 绘制,无 QtCharts 等外部依赖(与项目零依赖风格一致);
// Canvas 不随属性绑定自动重绘,isDarkMode 变化时主动 requestPaint()
// - 数据通过属性注入:applet 即 networkview.qml 的 root.appletC++ 后端对象),
// 为 null 时组件可独立预览(Canvas 显示"暂无历史数据"空状态)
// 触发方式:右键菜单"流量波动图" -> show()/raise()/requestActivate()
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.15
import "."
Window {
id: root
// 公共格式化函数:集中定义于同目录 NetCommon.qml
// 与 networkview.qml、NetworkPopup 共享同一份实现,消除跨组件重复定义
NetCommon { id: common }
// 对外依赖:关闭按钮 hover 态文字高亮色,由父组件传入
// 默认值与 networkview.qml 中 root.accentRed 一致,确保独立可用
property color accentColor: Qt.rgba(220 / 255, 38 / 255, 38 / 255, 1)
// 对外依赖:C++ 后端对象(NetworkMonitorApplet),由 networkview.qml 传入
// 提供 speedHistoryDownload / speedHistoryUploadQVariantList of QPointF
// x=时间戳秒, y=速度 bytes/sec)、downloadSpeed / uploadSpeed、activeInterface
// 为 null 时组件可独立预览(显示空状态),所有数据访问处均需做空值兜底
property var applet: null
// 深色模式标记:由 networkview.qml 根据 DockPalette 检测后传入;
// 默认 false(浅色)保证组件独立预览时与原版视觉一致
property bool isDarkMode: false
// ---- 主题感知颜色:isDarkMode 为 false 时取值与原浅色硬编码完全一致 ----
readonly property color winBg: isDarkMode ? "#202020" : "#FFFFFF"
readonly property color lineColor: isDarkMode ? "#383838" : "#E8E8E8"
readonly property color textPrimary: isDarkMode ? "#E0E0E0" : "#333333"
readonly property color textSecondary: isDarkMode ? "#AAAAAA" : "#666666"
readonly property color textTertiary: isDarkMode ? "#888888" : "#999999"
// 置顶按钮 hover 底色:浅色用淡黑、深色用淡白,保证两种卡片背景上均可见
readonly property color pinHoverBg: isDarkMode ? Qt.rgba(1, 1, 1, 0.10) : Qt.rgba(0, 0, 0, 0.06)
// 图表绘制色:声明为 string 供 Canvas 2D 上下文直接使用
//QML color 类型含 alpha 时序列化为 #AARRGGBBCanvas 无法正确解析,故不用 color 类型)
readonly property string gridBaselineColor: isDarkMode ? "#383838" : "#DDDDDD"
readonly property string gridLineColor: isDarkMode ? "#2A2A2A" : "#E5E5E5"
readonly property string axisTextColor: isDarkMode ? "#888888" : "#999999"
readonly property string hoverLineColor: isDarkMode ? "#555555" : "#BBBBBB"
readonly property string dotCoreColor: isDarkMode ? "#202020" : "#FFFFFF"
// Hover 状态:当前悬停采样点在历史缓冲中的索引,-1 表示无悬停
// 由图表区 MouseArea 根据 mouseX 反查最近采样点更新;Canvas 据此绘制
// 竖直辅助线与圆点标记,底部提示条据此显示该时刻数值
property int hoverIndex: -1
// Hover 提示文本:非空时底部提示条显示该内容,为空显示默认引导语
// 格式:"14:23:05 ↓856.00 KB/s ↑120.00 KB/s"
property string hoverHint: ""
// 窗口置顶状态:true 时 flags 附加 Qt.WindowStaysOnTopHint,窗口保持在
// 所有非置顶窗口之上(便于边下载大文件边观察曲线);由标题栏图钉按钮切换
property bool pinned: false
// 置顶状态变化时重绘图钉图标(未置顶=灰色轮廓,置顶=蓝色填充)
onPinnedChanged: pinIcon.requestPaint()
// 主题切换时重绘图表与图钉图标:Canvas 不随属性绑定自动重绘,需主动触发
onIsDarkModeChanged: {
pinIcon.requestPaint()
canvas.requestPaint()
}
// 下载折线颜色(绿)与上传折线颜色(橙),与规范 §4.4 一致
readonly property color downloadLineColor: "#34C759"
readonly property color uploadLineColor: "#FF9500"
// 图表区内边距:左侧容纳 Y 轴刻度标签(如 "10 MB"),底部容纳 X 轴时间标签
// 定义为根属性,供 Canvas 绘制与 MouseArea 坐标换算共用,保证两处一致
readonly property int chartMarginLeft: 56
readonly property int chartMarginRight: 12
readonly property int chartMarginTop: 12
readonly property int chartMarginBottom: 26
width: 680
height: 420
visible: false
// flags 绑定 pinned:置顶时附加 WindowStaysOnTopHint;未置顶时该位为 0
// 与原静态 flags 行为完全一致
flags: Qt.FramelessWindowHint | Qt.Window | (pinned ? Qt.WindowStaysOnTopHint : 0)
modality: Qt.NonModal
color: "transparent"
onVisibleChanged: {
if (visible) {
// 居中到当前屏幕(任务栏所在屏幕),加 virtualX/virtualY 偏移
// 避免多显示器下窗口出现在非预期屏幕
x = Screen.virtualX + (Screen.width - width) / 2
y = Screen.virtualY + (Screen.height - height) / 2
}
}
// 格式化速度显示:统一调用 common.formatSpeed(带单位,最小 KB,保留 2 位小数),
// 与 networkview.qml / NetworkPopup 共用 NetCommon 中的同一份实现
// Y 轴上限取整:将 v 向上取整到 1/2/5 × 10^n 序列
// 设计原因:使刻度值为易读的整数(如 12345 -> 2000080000 -> 100000),
// 避免 vMax * 1.2 的余量产生零碎刻度(如 14814)
function niceCeil(v) {
if (v <= 0) return 1024
var exp = Math.floor(Math.log(v) / Math.LN10)
var base = Math.pow(10, exp)
var f = v / base
var nf
if (f <= 1) nf = 1
else if (f <= 2) nf = 2
else if (f <= 5) nf = 5
else nf = 10
return nf * base
}
// Y 轴刻度单位选择:根据上限自动选择 B/KB/MB/GB,返回 {div, suffix}
// 设计原因:让刻度数值保持在 0~1000 的易读区间(如 200 KB 而非 204800 B
function axisUnit(yMax) {
if (yMax >= 1024 * 1024 * 1024) return { div: 1024 * 1024 * 1024, suffix: "GB" }
if (yMax >= 1024 * 1024) return { div: 1024 * 1024, suffix: "MB" }
if (yMax >= 1024) return { div: 1024, suffix: "KB" }
return { div: 1, suffix: "B" }
}
// 格式化单个 Y 轴刻度值:大值取整、小值保留 1 位小数,避免 "10.00" 这类冗余
function formatAxisValue(v, unit) {
var val = v / unit.div
var s
if (val >= 100) s = val.toFixed(0)
else if (val >= 10) s = val.toFixed(1)
else s = val.toFixed(val === 0 ? 0 : 1)
return s + " " + unit.suffix
}
// 窗口主体:圆角卡片(背景与边框随 isDarkMode 切换深浅),1px 边框模拟 DTK 窗口描边(与 AboutWindow 一致)
Rectangle {
anchors.fill: parent
color: root.winBg
radius: 12
border.width: 1
border.color: root.lineColor
ColumnLayout {
anchors.fill: parent
spacing: 0
// 自定义标题栏:左侧"流量波动图"标题 + 右侧置顶/关闭按钮,整栏可拖动窗口
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 44
color: "transparent"
// 拖动层:声明在关闭/置顶按钮之前(位于其下方),避免遮挡按钮点击与 hover
// 注意:drag.target 只对 Item 生效,对 Window 无效(原实现拖动不工作);
// 改用 startSystemMove() 系统级窗口拖动,这是 frameless Window 的正确做法,
// pressed 即触发,由窗口管理器接管移动,不干扰按钮的 hover/click
MouseArea {
anchors.fill: parent
cursorShape: Qt.OpenHandCursor
onPressed: root.startSystemMove()
}
Text {
anchors.left: parent.left
anchors.leftMargin: 16
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Traffic Chart")
font.pixelSize: 15
font.weight: Font.Bold
color: root.textPrimary
}
// 置顶按钮:28x28 圆形(与关闭按钮一致),位于关闭按钮左侧
// 三态视觉:未置顶=灰色图钉轮廓;置顶=deepin 蓝填充 + 淡蓝底;
// hover=淡灰底(置顶时淡蓝底优先)
Rectangle {
anchors.right: chartCloseButton.left
anchors.rightMargin: 4
anchors.verticalCenter: parent.verticalCenter
width: 28
height: 28
radius: 14
color: root.pinned ? Qt.rgba(0, 129 / 255, 1, 0.12)
: (pinMouse.containsMouse ? root.pinHoverBg : "transparent")
// 置顶图标:Canvas 绘制"上箭头触顶"(⤒ 风格)——向上箭头指向顶部横杠,
// 是"置顶/移到顶部"最通用的视觉语言,与关闭按钮"×"形态区分明显
// 未置顶:灰色细线描边;置顶:蓝色加粗 + 箭头头部实心填充,激活态更醒目
Canvas {
id: pinIcon
anchors.centerIn: parent
width: 16
height: 16
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
var c = root.pinned ? "#0081FF" : root.axisTextColor
ctx.strokeStyle = c
ctx.fillStyle = c
ctx.lineCap = "round"
ctx.lineJoin = "round"
ctx.lineWidth = root.pinned ? 2 : 1.5
// 顶部横杠:y=3x 3..13
ctx.beginPath()
ctx.moveTo(3, 3)
ctx.lineTo(13, 3)
ctx.stroke()
// 箭头主干:自底部 (8,13) 向上至 (8,7)
// 与横杠间留 ~1.5px 间隙,保持"触顶"的意象清晰
ctx.beginPath()
ctx.moveTo(8, 13)
ctx.lineTo(8, 7)
ctx.stroke()
if (root.pinned) {
// 置顶:箭头头部实心三角填充,与描边态形成明确视觉差异
ctx.beginPath()
ctx.moveTo(8, 5.2)
ctx.lineTo(4.6, 9)
ctx.lineTo(11.4, 9)
ctx.closePath()
ctx.fill()
} else {
// 未置顶:左右两片箭头羽,自顶端 (8,5.5) 向两肩展开
ctx.beginPath()
ctx.moveTo(8, 5.5)
ctx.lineTo(5, 8.5)
ctx.moveTo(8, 5.5)
ctx.lineTo(11, 8.5)
ctx.stroke()
}
}
}
MouseArea {
id: pinMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.pinned = !root.pinned
}
}
// 关闭按钮:28x28 圆形,hover 时淡红底 + 红色 ×(颜色由 accentColor 决定)
Rectangle {
id: chartCloseButton
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
width: 28
height: 28
radius: 14
color: chartCloseMouse.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: chartCloseMouse.containsMouse ? root.accentColor : root.textSecondary
}
MouseArea {
id: chartCloseMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.hide()
}
}
}
// 顶部状态条:实时下行/上行速度(箭头颜色与折线一致,兼作图例)
// + 右侧活动接口名与时间窗口标签
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: 30
Layout.leftMargin: 16
Layout.rightMargin: 16
spacing: 6
Text {
text: "↓"
font.pixelSize: 14
font.weight: Font.Bold
color: root.downloadLineColor
}
Text {
// applet 为空时兜底为 0,保证独立预览不报错
text: common.formatSpeed(root.applet ? root.applet.downloadSpeed : 0)
font.pixelSize: 12
color: root.textPrimary
}
Item { Layout.preferredWidth: 10 }
Text {
text: "↑"
font.pixelSize: 14
font.weight: Font.Bold
color: root.uploadLineColor
}
Text {
text: common.formatSpeed(root.applet ? root.applet.uploadSpeed : 0)
font.pixelSize: 12
color: root.textPrimary
}
Item { Layout.fillWidth: true }
Text {
text: (root.applet && root.applet.activeInterface
? root.applet.activeInterface : "—") + " · 5min"
font.pixelSize: 12
color: root.textTertiary
}
}
// 图表区:Canvas 绘制双折线 + 网格 + 坐标轴 + Hover 辅助线
// visible 绑定窗口可见性:弹窗关闭时不绘制(C++ 后端仍继续采集)
Canvas {
id: canvas
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: 8
Layout.rightMargin: 8
visible: root.visible
onWidthChanged: requestPaint()
onHeightChanged: requestPaint()
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
// 绘图区几何(与 chartHover 坐标换算共用同一组边距)
var pl = root.chartMarginLeft
var pr = root.chartMarginRight
var pt = root.chartMarginTop
var pb = root.chartMarginBottom
var pw = width - pl - pr
var ph = height - pt - pb
if (pw <= 0 || ph <= 0) return
// 取历史数据;applet 为空时视为空缓冲(走空状态分支)
var dl = root.applet ? root.applet.speedHistoryDownload : []
var ul = root.applet ? root.applet.speedHistoryUpload : []
var n = dl.length
// ---- Y 轴上限计算(动态适应最近活动)----
// 使用最近 60 秒的数据计算 Y 轴上限,而非全量 5 分钟缓冲。
// 设计原因:若使用全量缓冲,当历史中存在大流量峰值(如 12MB/s 下载)时,
// 即使当前网速已降至 KB/s 级别,Y 轴仍保持高位,导致当前曲线被压到底部
// 无法观察波动。改用 60 秒窗口后,峰值滑出窗口时 Y 轴自动缩小,
// 始终为当前活动提供合适的显示比例。
var vMax = 0
var i
var recentWindow = 60 // 秒,Y 轴基于最近 1 分钟的活动计算
var recentStart = Math.max(0, n - recentWindow)
for (i = recentStart; i < n; i++) {
if (dl[i].y > vMax) vMax = dl[i].y
if (i < ul.length && ul[i].y > vMax) vMax = ul[i].y
}
var yMax = vMax <= 0 ? 1024 : root.niceCeil(vMax * 1.2)
var unit = root.axisUnit(yMax)
// ---- 水平网格线与 Y 轴刻度([0, yMax] 均分 4 段)----
ctx.font = "10px sans-serif"
ctx.textBaseline = "middle"
var g, gy
for (g = 0; g <= 4; g++) {
gy = pt + ph - (ph * g / 4)
// +0.5 让 1px 线条落在像素边界上,避免抗锯齿发虚
// 基线(g=0)略深一档;网格与刻度颜色均随 isDarkMode 切换深浅
ctx.strokeStyle = g === 0 ? root.gridBaselineColor : root.gridLineColor
ctx.lineWidth = 1
ctx.beginPath()
ctx.moveTo(pl, gy + 0.5)
ctx.lineTo(pl + pw, gy + 0.5)
ctx.stroke()
// 刻度标签右对齐于绘图区左侧
ctx.fillStyle = root.axisTextColor
ctx.textAlign = "right"
ctx.fillText(root.formatAxisValue(yMax * g / 4, unit), pl - 8, gy)
}
// ---- X 轴刻度:每分钟一个标签,均布于绘图区 ----
// 5 分钟窗口用 -5m/-4m/-3m/-2m/-1m/now 共 6 个标签,比 4 个三分点
// 的 -3m20s/-1m40s 更易读;循环按数组长度参数化,两端左/右对齐
var xLabels = ["-5m", "-4m", "-3m", "-2m", "-1m", "now"]
ctx.fillStyle = root.axisTextColor
ctx.textBaseline = "alphabetic"
for (i = 0; i < xLabels.length; i++) {
var lx = pl + pw * i / (xLabels.length - 1)
// 两端标签分别左/右对齐,避免文字越出卡片边缘被裁剪
ctx.textAlign = i === 0 ? "left" : (i === xLabels.length - 1 ? "right" : "center")
ctx.fillText(xLabels[i], lx, height - 8)
}
// ---- 空状态:画完空网格后中央提示,不画折线(规范 §5)----
if (n === 0) {
ctx.fillStyle = root.axisTextColor
ctx.font = "13px sans-serif"
ctx.textAlign = "center"
ctx.textBaseline = "middle"
ctx.fillText(qsTr("No history data"), pl + pw / 2, pt + ph / 2)
return
}
// ---- 折线绘制 ----
// X 轴固定为 5 分钟窗口 [tMax-300, tMax]:右缘始终对齐"now"
// 数据不足 5 分钟时曲线从右侧向左生长,左侧留空网格。
// 设计原因:若按实际数据范围 [dl[0].x, tMax] 映射,刚启动时仅有
// 几秒数据会被拉伸到整个绘图宽度,X 轴标签 -5m/-4m/... 与实际范围
// 严重不符,用户看到的是"几秒趋势图"而非"5 分钟趋势图"。
// 300 = 5 分钟 × 60 秒,与 C++ MAX_HISTORY_SAMPLES 一致
var tMax = dl[n - 1].x
var tMin = tMax - 300
var tRange = 300
// 单点边界:只有 1 个采样点时无法连线,画一个圆点。
// 固定窗口下单点对齐 now(右缘),而非居中
if (n === 1) {
var sx = pl + pw
var sy = pt + ph - (dl[0].y / yMax) * ph
ctx.fillStyle = "#34C759"
ctx.beginPath()
ctx.arc(sx, sy, 3, 0, 2 * Math.PI)
ctx.fill()
return
}
// 绘制一条序列折线:先填充线下淡色区域增强可读性,再描边 2px 主线
// lineCap/lineJoin 用 round 使折线端点与转角圆润
// 注意每次 stroke/fill 前必须 beginPath(),避免与上一段路径串连
function drawSeries(data, colorCss, fillCss) {
var m = data.length
if (m === 0) return
var j, px, py
// 线下淡色填充(alpha 0.08):贴到基线形成闭合区域
ctx.beginPath()
for (j = 0; j < m; j++) {
px = pl + ((data[j].x - tMin) / tRange) * pw
py = pt + ph - (data[j].y / yMax) * ph
if (j === 0) ctx.moveTo(px, py)
else ctx.lineTo(px, py)
}
ctx.lineTo(pl + ((data[m - 1].x - tMin) / tRange) * pw, pt + ph)
ctx.lineTo(pl + ((data[0].x - tMin) / tRange) * pw, pt + ph)
ctx.closePath()
ctx.fillStyle = fillCss
ctx.fill()
// 主线:2px 描边
ctx.beginPath()
for (j = 0; j < m; j++) {
px = pl + ((data[j].x - tMin) / tRange) * pw
py = pt + ph - (data[j].y / yMax) * ph
if (j === 0) ctx.moveTo(px, py)
else ctx.lineTo(px, py)
}
ctx.strokeStyle = colorCss
ctx.lineWidth = 2
ctx.lineCap = "round"
ctx.lineJoin = "round"
ctx.stroke()
}
// 先画上传(橙)再画下载(绿):下载通常是主视线,后画置于上层
drawSeries(ul, "#FF9500", "rgba(255, 149, 0, 0.08)")
drawSeries(dl, "#34C759", "rgba(52, 199, 89, 0.08)")
// ---- Hover:竖直辅助线 + 两条折线上的圆点标记 ----
// hoverIndex 由 chartHover 根据 mouseX 反查更新;此处按索引换算坐标
var hi = root.hoverIndex
if (hi >= 0 && hi < n) {
var hx = pl + ((dl[hi].x - tMin) / tRange) * pw
// 竖直辅助线:虚线,避免与实线折线混淆
ctx.strokeStyle = root.hoverLineColor
ctx.lineWidth = 1
ctx.setLineDash([4, 3])
ctx.beginPath()
ctx.moveTo(hx, pt)
ctx.lineTo(hx, pt + ph)
ctx.stroke()
ctx.setLineDash([])
// 圆点标记:窗口底色芯 + 2px 彩色描边,在折线上形成清晰锚点
function drawDot(yVal, colorCss) {
var hy = pt + ph - (yVal / yMax) * ph
ctx.beginPath()
ctx.arc(hx, hy, 4, 0, 2 * Math.PI)
ctx.fillStyle = root.dotCoreColor
ctx.fill()
ctx.strokeStyle = colorCss
ctx.lineWidth = 2
ctx.stroke()
}
drawDot(dl[hi].y, "#34C759")
if (hi < ul.length) drawDot(ul[hi].y, "#FF9500")
}
}
// Hover 交互层:横向移动时按 mouseX 反查最近采样点
// 设计原因:采样点按时间均匀分布(1Hz),先由 X 坐标线性反推时间戳,
// 再线性扫描找最近点;300 点上限下扫描开销可忽略,无需二分
MouseArea {
id: chartHover
anchors.fill: parent
hoverEnabled: true
onPositionChanged: {
if (!root.applet) return
var dl = root.applet.speedHistoryDownload
var n = dl.length
if (n === 0) return
var pl = root.chartMarginLeft
var pw = width - pl - root.chartMarginRight
if (pw <= 0) return
// X 轴用与绘制一致的固定 5 分钟窗口 [tMax-300, tMax] 反推时间戳,
// 保证 Hover 辅助线与折线上的采样点严格对齐
var tMax = dl[n - 1].x
var tMin = tMax - 300
var tRange = 300
// 将 mouseX 限制在绘图区内,再线性反推时间戳
var clampedX = Math.max(pl, Math.min(mouse.x, pl + pw))
var target = tMin + (clampedX - pl) / pw * tRange
var best = 0
var bestDist = Math.abs(dl[0].x - target)
for (var i = 1; i < n; i++) {
var d = Math.abs(dl[i].x - target)
if (d < bestDist) {
bestDist = d
best = i
}
}
root.hoverIndex = best
// 更新底部提示条:时间 + 下行/上行速度
var ul = root.applet.speedHistoryUpload
var idx = root.hoverIndex
var upVal = idx < ul.length ? ul[idx].y : 0
root.hoverHint = Qt.formatDateTime(new Date(dl[idx].x * 1000), "HH:mm:ss")
+ " ↓" + common.formatSpeed(dl[idx].y)
+ " ↑" + common.formatSpeed(upVal)
canvas.requestPaint()
}
onExited: {
root.hoverIndex = -1
root.hoverHint = ""
canvas.requestPaint()
}
}
}
// 底部提示条:默认显示操作引导;Hover 时显示该时刻的时间与上下行速度
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 30
color: "transparent"
Text {
anchors.centerIn: parent
text: root.hoverHint.length > 0
? root.hoverHint : qsTr("Hover over the curve for details")
font.pixelSize: 11
color: root.hoverHint.length > 0 ? root.textPrimary : root.textTertiary
}
}
}
}
// 动态刷新:C++ 每秒追加采样点时发射 speedHistoryChanged,触发 Canvas 重绘
// 注意:此处不重置 Hover 状态,否则用户悬停时提示会随每秒刷新闪断;
// 缓冲满后旧点滑出导致的索引漂移由 onPaint 中 hi < n 的边界判断兜底
Connections {
target: root.applet
function onSpeedHistoryChanged() {
canvas.requestPaint()
}
// 接口切换时历史整体更换,原 Hover 索引失去意义,需清除避免残留标记
function onActiveInterfaceChanged() {
root.hoverIndex = -1
root.hoverHint = ""
canvas.requestPaint()
}
}
}
+78 -774
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024 MyCompany
// SPDX-FileCopyrightText: 2026 Jokul
//
// SPDX-License-Identifier: LGPL-3.0-or-later
@@ -40,31 +40,27 @@ AppletItem {
// 活动接口的 IPv6 全球地址,由 C++ 后端 detectIpAddress 持续更新
readonly property string ipv6Address: applet ? (applet.ipv6Address || "") : ""
readonly property var networkInterfaces: applet ? (applet.networkInterfaces || []) : []
readonly property var interfaceStats: applet ? (applet.interfaceStats || []) : []
// 判断是否为物理网卡(QML 侧排序用,与 C++ isPhysicalInterface 逻辑一致)
function isPhysicalIf(name) {
return name.startsWith("wlp") || name.startsWith("wlan")
|| name.startsWith("enp") || name.startsWith("eth")
}
// 公共颜色与格式化函数:集中定义于 components/NetCommon.qml(随 import "components" 引入),
// 与 NetworkPopup、TrafficChartWindow 共享同一份实现,消除跨组件重复定义
NetCommon { id: common }
// 排序后的接口列表:活动接口最前,其次物理网卡,最后虚拟网卡
// 设计原因:用户希望启动监测的网卡在最左边,方便查看和切换
// 排序后的接口列表:物理网卡在前,虚拟网卡在后,各自按名称排序
// 设计原因:保持稳定排序,活动接口不再移到最前,避免切换网卡时 chip 顺序跳动;
// 活动 chip 若被截断,由 chipFlickable 自动滚动露出完整样式;
// 物理网卡判断逻辑共用 common.isPhysicalIf(与 C++ isPhysicalInterface 一致)
readonly property var sortedInterfaces: {
if (!root.ready || root.networkInterfaces.length === 0) return []
var physical = []
var virtual = []
for (var i = 0; i < root.networkInterfaces.length; i++) {
var name = root.networkInterfaces[i]
if (name === root.activeInterface) continue
if (root.isPhysicalIf(name)) physical.push(name)
if (common.isPhysicalIf(name)) physical.push(name)
else virtual.push(name)
}
physical.sort()
virtual.sort()
var result = []
if (root.activeInterface) result.push(root.activeInterface)
return result.concat(physical).concat(virtual)
return physical.concat(virtual)
}
// 紧凑格式化速度(用于任务栏图标和 tooltip,不带单位后缀,节省空间)
@@ -81,59 +77,27 @@ AppletItem {
}
}
// 格式化速度显示(带单位,用于弹出面板,信息更完整)
// 最小单位为 KB,与 formatSpeedShort 保持一致;所有级别保留 2 位小数
function formatSpeed(bytesPerSec) {
if (bytesPerSec < 1024 * 1024) {
return (bytesPerSec / 1024).toFixed(2) + " KB/s"
} else if (bytesPerSec < 1024 * 1024 * 1024) {
return (bytesPerSec / (1024 * 1024)).toFixed(2) + " MB/s"
} else {
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(2) + " GB/s"
}
}
// 带单位的速度/总量格式化函数(formatSpeed / formatTotal)已移至 NetCommon
// 本文件未直接使用;弹窗与图表窗口经 common.formatSpeed / common.formatTotal 调用
// 格式化总量显示(用于弹出面板的累计统计)
function formatTotal(bytes) {
if (bytes < 1024) {
return bytes.toFixed(0) + " B"
} else if (bytes < 1024 * 1024) {
return (bytes / 1024).toFixed(1) + " KB"
} else if (bytes < 1024 * 1024 * 1024) {
return (bytes / (1024 * 1024)).toFixed(1) + " MB"
} else {
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB"
}
}
// 颜色别名:转发 common 的公共颜色,保持下游 root.xxx 引用不变。
// 仅保留本文件实际使用的两个(primaryText 供 speedTextColor 回退、
// accentRed 传入各独立窗口);其余公共颜色本文件未用,不再重复定义。
// 颜色派生逻辑与主题适配的设计原因见 NetCommon.qml 文件头注释
readonly property color primaryText: common.primaryText
readonly property color accentRed: common.accentRed
// 任务栏与面板的颜色全部派生自 DockPalette.iconTextPalette
// 这样在深色/浅色任务栏下自动适配,无需手动判断主题
property Palette basePalette: DockPalette.iconTextPalette
readonly property color primaryText: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.95)
readonly property color secondaryText: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.80)
readonly property color tertiaryText: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.65)
readonly property color cardBackground: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.06)
readonly property color cardBorder: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.10)
// 深色模式检测:用 DTK 系统调色板的窗口背景亮度判定,而非 DockPalette。
// 设计原因:DockPalette.iconTextPalette 反映的是任务栏图标文字色,不随系统深色主题变化;
// DTK.palette.window 是系统主题的窗口背景色,深色模式下为深色(hslLightness < 0.5),
// 浅色模式下为浅色(hslLightness >= 0.5)。主题切换时 DTK.paletteChanged 自动触发重新求值。
// 检测结果通过属性注入各独立窗口(AboutWindow 等独立 Window 无法访问 dock 上下文的 DockPalette
readonly property bool isDarkMode: DTK.palette.window.hslLightness < 0.5
// 强调色:下载蓝、上传绿,是面板与任务栏的主视觉区分
readonly property color accentBlue: Qt.rgba(20 / 255, 80 / 255, 160 / 255, 1)
readonly property color accentBlueLight: Qt.rgba(20 / 255, 80 / 255, 160 / 255, 0.12)
readonly property color accentGreen: Qt.rgba(22 / 255, 163 / 255, 74 / 255, 1)
readonly property color accentGreenLight: Qt.rgba(22 / 255, 163 / 255, 74 / 255, 0.12)
// 高速警示色:下载速度超过阈值时由蓝转橙再转红
readonly property color accentOrange: Qt.rgba(245 / 255, 158 / 255, 11 / 255, 1)
readonly property color accentRed: Qt.rgba(220 / 255, 38 / 255, 38 / 255, 1)
// 下载值颜色:保留原有阈值逻辑(>10MB/s 红、>1MB/s 橙、否则蓝),仅作用于下载值
readonly property color downloadValueColor: {
if (downloadSpeed > 10 * 1024 * 1024) return accentRed
if (downloadSpeed > 1 * 1024 * 1024) return accentOrange
return accentBlue
}
// 上传值颜色:固定绿色,不做高速警示
readonly property color uploadValueColor: accentGreen
// 任务栏网速数值字体色:用户自定义色优先(持久化),未设置时跟随系统主题
// 设计原因:primaryText 派生自 DTK.palette.windowText,深浅色系统主题自动适配;
// 用户主动选色后覆盖,空串回退到 primaryText 保持自适应
readonly property color speedTextColor: (applet && applet.textColor.length > 0) ? applet.textColor : primaryText
// 任务栏图标区:双行紧凑数值,箭头与数值紧贴、左对齐
// 设计原因:箭头与数值作为一组固定在左侧,数值左对齐紧贴箭头;
@@ -153,7 +117,7 @@ AppletItem {
Text {
text: "↓"
font.pixelSize: root.dockSize * 0.20
color: root.secondaryText
color: root.speedTextColor
Layout.alignment: Qt.AlignVCenter
}
@@ -161,7 +125,7 @@ AppletItem {
text: root.formatSpeedShort(root.downloadSpeed)
font.pixelSize: root.dockSize * 0.25
font.weight: Font.Medium
color: root.primaryText
color: root.speedTextColor
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignLeft
}
@@ -174,7 +138,7 @@ AppletItem {
Text {
text: "↑"
font.pixelSize: root.dockSize * 0.20
color: root.secondaryText
color: root.speedTextColor
Layout.alignment: Qt.AlignVCenter
}
@@ -182,7 +146,7 @@ AppletItem {
text: root.formatSpeedShort(root.uploadSpeed)
font.pixelSize: root.dockSize * 0.25
font.weight: Font.Medium
color: root.primaryText
color: root.speedTextColor
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignLeft
}
@@ -207,7 +171,7 @@ AppletItem {
Text {
text: "↓"
font.pixelSize: root.dockSize * 0.20
color: root.secondaryText
color: root.speedTextColor
anchors.horizontalCenter: parent.horizontalCenter
}
@@ -220,7 +184,7 @@ AppletItem {
font.pixelSize: root.dockSize * 0.25
height: font.pixelSize
font.weight: Font.Medium
color: root.primaryText
color: root.speedTextColor
anchors.horizontalCenter: parent.horizontalCenter
}
}
@@ -234,7 +198,7 @@ AppletItem {
Text {
text: "↑"
font.pixelSize: root.dockSize * 0.20
color: root.secondaryText
color: root.speedTextColor
anchors.horizontalCenter: parent.horizontalCenter
}
@@ -245,7 +209,7 @@ AppletItem {
font.pixelSize: root.dockSize * 0.25
height: font.pixelSize
font.weight: Font.Medium
color: root.primaryText
color: root.speedTextColor
anchors.horizontalCenter: parent.horizontalCenter
}
}
@@ -258,9 +222,9 @@ AppletItem {
id: toolTip
text: root.ready
? (root.activeInterface
+ " · IPv4" + (root.ipAddress || qsTr(""))
+ (root.ipv6Address ? "\nIPv6" + root.ipv6Address : ""))
: qsTr("未检测到网络接口")
+ " · " + qsTr("IPv4: ") + (root.ipAddress || qsTr("None"))
+ (root.ipv6Address ? "\n" + qsTr("IPv6: ") + root.ipv6Address : ""))
: qsTr("No network interface detected")
toolTipX: DockPanelPositioner.x
toolTipY: DockPanelPositioner.y
}
@@ -269,8 +233,7 @@ AppletItem {
// C++ 后端 NetworkMonitorApplet::init() 已启动 1 秒间隔的 m_refreshTimer
// 持续调用 refresh()->readNetworkStats()->calculateSpeed() 更新速度属性。
// QML 通过属性绑定自动获取最新值,无需主动触发 refresh。
// 此前 QML 额外调用 applet.refresh() 会打破 1 秒定时间隔
// 导致 calculateSpeed() 基于不固定间隔计算字节差,速度显示接近 0。
// refresh() 已移除 Q_INVOKABLE 标记,防止 QML 误调用打破定时间隔
Timer {
id: toolTipShowTimer
@@ -297,7 +260,8 @@ AppletItem {
}
}
// 弹出窗口
// 弹出窗口PanelPopup 壳在此处(dock 上下文类型不能作为独立组件根元素),
// 弹窗内容由 NetworkPopup.qml 提供,传入 applet,内部自行派生颜色和数据
PanelPopup {
id: networkPopup
width: 360
@@ -306,335 +270,17 @@ AppletItem {
popupY: DockPanelPositioner.y
onPopupVisibleChanged: {
// 仅控制 tooltip 关闭,不触发 refresh。
// C++ 后端 m_refreshTimer 持续更新,popup 通过属性绑定自动显示最新数据。
if (popupVisible) {
toolTip.close()
// popup 打开时确保活动 chip 完整可见
Qt.callLater(function() { popupContent.scrollToActiveChip(false) })
}
}
Control {
id: popupContainer
NetworkPopup {
id: popupContent
applet: root.applet
anchors.fill: parent
padding: 16
contentItem: ColumnLayout {
spacing: 12
// 标题栏:标题在上,网卡名+IP 在卡片底色背景内
Item {
Layout.fillWidth: true
Layout.preferredHeight: 72
ColumnLayout {
anchors.centerIn: parent
spacing: 6
Text {
text: qsTr("网络速度监控")
font.pixelSize: 15
font.weight: Font.Bold
color: root.primaryText
Layout.alignment: Qt.AlignHCenter
}
// 网卡名 + IP 地址卡片背景
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 300
// 有 IPv6 时增高以容纳第三行;无 IPv6 时保持原 40px
Layout.preferredHeight: root.ipv6Address ? 54 : 40
color: root.cardBackground
radius: 8
border.width: 1
border.color: root.cardBorder
visible: root.ready
ColumnLayout {
anchors.centerIn: parent
spacing: 0
Text {
text: root.activeInterface
font.pixelSize: 11
color: root.secondaryText
Layout.alignment: Qt.AlignHCenter
}
Text {
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
}
}
}
}
}
// 实时速度卡片:双列布局,每列内容在各自半区内水平垂直居中
// 设计原因:用显式 anchor 分两个等宽半区,比 RowLayout+fillWidth 更可靠,
// 确保下载/上传内容各自在左/右半区正中显示
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 110
color: root.cardBackground
radius: 12
border.width: 1
border.color: root.cardBorder
// 下载半区(左半)
Item {
id: downloadHalf
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.leftMargin: 12
width: (parent.width - 1) / 2 - 12
ColumnLayout {
anchors.centerIn: parent
spacing: 4
Rectangle {
width: 32
height: 32
radius: 16
color: root.accentBlueLight
Layout.alignment: Qt.AlignHCenter
Text {
anchors.centerIn: parent
text: "↓"
font.pixelSize: 18
font.weight: Font.Bold
color: root.accentBlue
}
}
Text {
text: qsTr("下载")
font.pixelSize: 11
color: root.secondaryText
Layout.alignment: Qt.AlignHCenter
}
Text {
text: root.formatSpeed(root.downloadSpeed)
font.pixelSize: 18
font.weight: Font.Bold
color: root.downloadValueColor
Layout.alignment: Qt.AlignHCenter
}
}
}
// 中间分隔线
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 12
anchors.bottom: parent.bottom
anchors.bottomMargin: 12
width: 1
color: root.cardBorder
}
// 上传半区(右半)
Item {
id: uploadHalf
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.rightMargin: 12
width: (parent.width - 1) / 2 - 12
ColumnLayout {
anchors.centerIn: parent
spacing: 4
Rectangle {
width: 32
height: 32
radius: 16
color: root.accentGreenLight
Layout.alignment: Qt.AlignHCenter
Text {
anchors.centerIn: parent
text: "↑"
font.pixelSize: 18
font.weight: Font.Bold
color: root.accentGreen
}
}
Text {
text: qsTr("上传")
font.pixelSize: 11
color: root.secondaryText
Layout.alignment: Qt.AlignHCenter
}
Text {
text: root.formatSpeed(root.uploadSpeed)
font.pixelSize: 18
font.weight: Font.Bold
color: root.uploadValueColor
Layout.alignment: Qt.AlignHCenter
}
}
}
}
// 总量统计:单行紧凑
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 44
color: root.cardBackground
radius: 12
border.width: 1
border.color: root.cardBorder
visible: root.ready
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
Item { Layout.fillWidth: true }
Text {
text: qsTr("总计")
font.pixelSize: 11
font.weight: Font.Bold
color: root.secondaryText
}
Text {
text: "↓ " + root.formatTotal(root.totalDownload)
font.pixelSize: 12
color: root.primaryText
}
Text {
text: "↑ " + root.formatTotal(root.totalUpload)
font.pixelSize: 12
color: root.primaryText
}
Item { Layout.fillWidth: true }
}
}
// 接口切换 chip 列表:水平滚动,活动接口在最左,物理网卡次之,虚拟网卡在后
// chip 少时分散撑满一行;chip 多超出宽度时固定宽度 + 水平滚动
Flickable {
id: chipFlickable
Layout.fillWidth: true
Layout.preferredHeight: 32
visible: root.ready && root.networkInterfaces.length > 1
contentWidth: Math.max(chipRow.width, width)
contentHeight: chipRow.height
flickableDirection: Flickable.HorizontalFlick
clip: true
// 等宽分配宽度:假设所有 chip 等宽撑满时的单个宽度
readonly property real equalChipWidth: root.sortedInterfaces.length > 0
? (width - 6 * (root.sortedInterfaces.length - 1)) / root.sortedInterfaces.length
: 0
Row {
id: chipRow
spacing: 6
Repeater {
model: root.sortedInterfaces
Rectangle {
// 宽度取等宽和自然宽度的较大值:
// chip 少时 equalChipWidth > 自然宽度 -> 分散撑满一行
// chip 多时 自然宽度 > equalChipWidth -> 固定宽度 + 水平滚动
width: Math.max(chipFlickable.equalChipWidth, chipText.implicitWidth + 24)
height: 28
radius: 8
color: modelData === root.activeInterface
? root.accentBlue
: (chipMouse.containsMouse ? root.accentBlueLight : root.cardBackground)
border.width: 1
border.color: modelData === root.activeInterface
? root.accentBlue
: root.cardBorder
Text {
id: chipText
anchors.centerIn: parent
text: modelData
font.pixelSize: 11
font.weight: modelData === root.activeInterface ? Font.Bold : Font.Normal
color: modelData === root.activeInterface ? "white" : root.primaryText
}
MouseArea {
id: chipMouse
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: {
if (root.applet) root.applet.setActiveInterface(modelData)
}
}
}
}
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AsNeeded
}
}
// 未检测到接口占位
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 80
color: root.cardBackground
radius: 12
border.width: 1
border.color: root.cardBorder
visible: !root.ready
ColumnLayout {
anchors.centerIn: parent
spacing: 6
Text {
text: "⚠"
font.pixelSize: 24
color: root.secondaryText
Layout.alignment: Qt.AlignHCenter
}
Text {
text: qsTr("未检测到网络接口")
font.pixelSize: 12
color: root.secondaryText
Layout.alignment: Qt.AlignHCenter
}
}
}
Item { Layout.fillHeight: true }
}
}
Component.onCompleted: {
@@ -654,7 +300,7 @@ AppletItem {
id: contextMenu
Platform.MenuItem {
text: qsTr("设置")
text: qsTr("Settings")
onTriggered: {
settingsWindow.show()
settingsWindow.raise()
@@ -662,10 +308,20 @@ AppletItem {
}
}
// 流量波动图:屏幕居中独立窗口,展示活动接口最近 30 分钟网速趋势
Platform.MenuItem {
text: qsTr("Traffic Chart")
onTriggered: {
trafficChartWindow.show()
trafficChartWindow.raise()
trafficChartWindow.requestActivate()
}
}
Platform.MenuSeparator {}
Platform.MenuItem {
text: qsTr("关于")
text: qsTr("About")
onTriggered: {
aboutWindow.show()
aboutWindow.raise()
@@ -675,387 +331,35 @@ AppletItem {
}
// 关于窗口:抽取为独立组件 package/components/AboutWindow.qml
// 依赖通过属性传入:accentColor = root.accentRedversion 取自 C++ 后端 applet.version
// 依赖通过属性传入:accentColor = root.accentRedversion 取自 C++ 后端 applet.version
// isDarkMode 取自上方 DockPalette 检测结果
AboutWindow {
id: aboutWindow
accentColor: root.accentRed
version: root.applet ? root.applet.version : "1.0"
isDarkMode: root.isDarkMode
}
// 流量波动图窗口:屏幕居中独立窗口,展示活动接口最近 30 分钟网速趋势
// 依赖通过属性传入:accentColor = root.accentRedapplet = root.applet
// isDarkMode 取自上方 DockPalette 检测结果
TrafficChartWindow {
id: trafficChartWindow
accentColor: root.accentRed
applet: root.applet
isDarkMode: root.isDarkMode
}
// 设置窗口:独立顶层窗口,在桌面中间弹出
// 设计原因:Dialog 使用父窗口(dock layer-surface)的 overlay,仅覆盖任务栏区域,
// 无法在桌面中间显示。改用 Window 创建独立顶层窗口,可在桌面任意位置弹出。
Window {
SettingsWindow {
id: settingsWindow
width: 340
height: 350
visible: false
flags: Qt.FramelessWindowHint | Qt.Window
// NonModal:不阻塞桌面其他区域,用户可同时操作任务栏
modality: Qt.NonModal
// 卸载按钮文字:复制命令后临时显示提示,定时器到期后还原
property string uninstallButtonText: qsTr("卸载插件")
// 窗口透明:让圆角外的区域不显示,由内部 Rectangle 提供可见背景
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: settingsWindow
cursorShape: Qt.ArrowCursor
}
// 标题文字
Text {
anchors.left: parent.left
anchors.leftMargin: 16
anchors.verticalCenter: parent.verticalCenter
text: qsTr("设置")
font.pixelSize: 15
font.weight: Font.Bold
color: "#333333"
}
// 关闭按钮
Rectangle {
id: closeButton
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
width: 28
height: 28
radius: 14
color: closeMouse.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: closeMouse.containsMouse ? root.accentRed : "#666666"
}
MouseArea {
id: closeMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: settingsWindow.hide()
}
}
}
// 分隔线
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: "#e0e0e0"
}
// 内容区域
Item {
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
// 网络接口选择区
Text {
text: qsTr("网络接口")
font.pixelSize: 13
font.weight: Font.Bold
color: "#666666"
}
// 接口列表容器
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: root.networkInterfaces.length > 0 ? 40 + root.networkInterfaces.length * 28 : 80
color: "#ffffff"
radius: 10
border.width: 1
border.color: "#e0e0e0"
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
// 接口列表:RadioButton 单选切换活动接口
Repeater {
model: root.networkInterfaces
delegate: Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 28
color: "transparent"
radius: 6
RadioButton {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: modelData
checked: modelData === root.activeInterface
onToggled: {
if (root.applet) root.applet.setActiveInterface(modelData)
}
}
}
}
// 无接口提示
Text {
text: qsTr("未检测到网络接口")
font.pixelSize: 12
color: "#999999"
visible: root.networkInterfaces.length === 0
Layout.alignment: Qt.AlignHCenter
}
}
}
Item { Layout.fillHeight: true }
// 卸载插件按钮:文字可动态切换(复制命令后显示提示)
// 提示状态期间按钮不可点击,文字变绿色
Rectangle {
id: uninstallButton
Layout.fillWidth: true
Layout.preferredHeight: 40
// 提示状态时背景和边框变绿
readonly property bool isStatus: settingsWindow.uninstallButtonText !== qsTr("卸载插件")
color: isStatus
? Qt.rgba(22/255, 163/255, 74/255, 0.08)
: (uninstallMouse.containsMouse ? Qt.rgba(220/255, 38/255, 38/255, 0.15) : Qt.rgba(220/255, 38/255, 38/255, 0.08))
radius: 10
border.width: 1
border.color: isStatus ? Qt.rgba(22/255, 163/255, 74/255, 1) : root.accentRed
Text {
anchors.centerIn: parent
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 8
text: settingsWindow.uninstallButtonText
font.pixelSize: uninstallButton.isStatus ? 11 : 13
font.weight: Font.Medium
// 提示状态时文字绿色,否则红色
color: uninstallButton.isStatus ? Qt.rgba(22/255, 163/255, 74/255, 1) : root.accentRed
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
MouseArea {
id: uninstallMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
// 提示状态期间禁用点击
enabled: !uninstallButton.isStatus
onClicked: uninstallConfirmDialog.open()
}
}
// 关闭按钮
Rectangle {
id: bottomCloseButton
Layout.fillWidth: true
Layout.preferredHeight: 40
color: bottomCloseMouse.containsMouse ? root.accentBlueLight : "#ffffff"
radius: 10
border.width: 1
border.color: bottomCloseMouse.containsMouse ? root.accentBlue : "#e0e0e0"
Text {
anchors.centerIn: parent
text: qsTr("关闭")
font.pixelSize: 13
font.weight: Font.Medium
color: bottomCloseMouse.containsMouse ? root.accentBlue : "#333333"
}
MouseArea {
id: bottomCloseMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: settingsWindow.hide()
}
}
}
}
}
}
// 卸载确认对话框:在设置窗口内水平居中
Dialog {
id: uninstallConfirmDialog
width: 320
height: 260
modal: true
visible: false
// 在设置窗口中心显示
x: (settingsWindow.width - width) / 2
y: (settingsWindow.height - height) / 2
background: Rectangle {
color: "#f5f5f5"
radius: 12
border.width: 1
border.color: "#e0e0e0"
}
contentItem: ColumnLayout {
spacing: 16
anchors.fill: parent
anchors.margins: 20
Text {
text: qsTr("警告")
font.pixelSize: 16
font.weight: Font.Bold
color: root.accentRed
Layout.alignment: Qt.AlignHCenter
}
Text {
text: qsTr("以下命令用于卸载插件并重启 dde-shell,请复制到终端中执行:")
font.pixelSize: 12
color: "#333333"
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
// 命令显示区域
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 60
color: "#e8e8e8"
radius: 8
Text {
anchors.fill: parent
anchors.margins: 10
text: "sudo rm -rf /usr/share/dde-shell/space.jokul.JNetApplet/ && systemctl --user restart dde-shell@DDE"
font.pixelSize: 10
font.family: "monospace"
color: "#666666"
wrapMode: Text.WordWrap
}
}
Item { Layout.fillHeight: true }
// 按钮区域
RowLayout {
Layout.fillWidth: true
spacing: 12
// 取消按钮
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
color: cancelUninstallMouse.containsMouse ? "#f0f0f0" : "#ffffff"
radius: 8
border.width: 1
border.color: "#e0e0e0"
Text {
anchors.centerIn: parent
text: qsTr("取消")
font.pixelSize: 13
color: "#333333"
}
MouseArea {
id: cancelUninstallMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: uninstallConfirmDialog.close()
}
}
// 复制命令按钮
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
color: confirmUninstallMouse.containsMouse ? root.accentRed : Qt.rgba(220/255, 38/255, 38/255, 0.8)
radius: 8
Text {
anchors.centerIn: parent
text: qsTr("复制命令")
font.pixelSize: 13
font.weight: Font.Medium
color: "white"
}
MouseArea {
id: confirmUninstallMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// 复制卸载命令到剪贴板
clipboardHelper.selectAll()
clipboardHelper.copy()
uninstallConfirmDialog.close()
// 卸载按钮文字临时替换为复制成功提示,5 秒后还原
settingsWindow.uninstallButtonText = qsTr("卸载命令已复制到剪贴板,请在终端中粘贴执行")
statusHideTimer.start()
}
}
}
}
}
}
}
// 隐藏的 TextEdit 用于复制卸载命令到剪贴板
// QML 没有直接剪贴板 API,用 TextEdit.selectAll()+copy() 实现
TextEdit {
id: clipboardHelper
visible: false
text: "sudo rm -rf /usr/share/dde-shell/space.jokul.JNetApplet/ && systemctl --user restart dde-shell@DDE"
}
// 卸载按钮文字还原定时器:复制命令后 5 秒将按钮文字从提示还原为"卸载插件"
Timer {
id: statusHideTimer
interval: 5000
onTriggered: settingsWindow.uninstallButtonText = qsTr("卸载插件")
applet: root.applet
networkInterfaces: root.networkInterfaces
activeInterface: root.activeInterface
accentColor: root.accentRed
isDarkMode: root.isDarkMode
}
// 点击处理
+211 -35
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024 MyCompany
// SPDX-FileCopyrightText: 2026 Jokul
//
// SPDX-License-Identifier: LGPL-3.0-or-later
@@ -11,6 +11,12 @@
#include <QDir>
#include <QRegularExpression>
#include <QNetworkInterface>
#include <QColor>
#include <QSettings>
#include <QStandardPaths>
#include <QTranslator>
#include <QLocale>
#include <QCoreApplication>
DS_BEGIN_NAMESPACE
@@ -19,13 +25,36 @@ NetworkMonitorApplet::NetworkMonitorApplet(QObject *parent)
, m_refreshTimer(nullptr)
, m_lastRxBytes(0)
, m_lastTxBytes(0)
, m_lastTimestampMs(0)
, m_downloadSpeed(0)
, m_uploadSpeed(0)
, m_totalDownload(0)
, m_totalUpload(0)
, m_ready(false)
, m_firstUpdate(true)
, m_textColor()
{
// 从独立配置文件读取持久化的字体颜色
// 设计原因:使用独立配置文件避免污染 dde-shell 的共享配置,
// 空串表示"跟随系统",由 QML 层回退到 primaryText 实现
const QString configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)
+ QStringLiteral("/jnetapplet/settings.ini");
QSettings settings(configPath, QSettings::IniFormat);
if (settings.contains(QStringLiteral("textColor"))) {
const QString saved = settings.value(QStringLiteral("textColor")).toString();
// 空串合法(跟随系统),非空则校验是否为合法颜色值
if (saved.isEmpty() || QColor::isValidColorName(saved)) {
m_textColor = saved;
}
// 若校验失败则保持空串默认值(跟随系统),不抛出也不记录
}
// 从配置文件读取持久化的活动接口,启动时自动恢复用户上次选择
// 设计原因:用户手动选择的网卡应跨重启保持,而非每次启动重新自动检测;
// 若保存的接口已不存在(如 USB 网卡拔出),readNetworkStats 会清空并回退到自动选择
if (settings.contains(QStringLiteral("activeInterface"))) {
m_activeInterface = settings.value(QStringLiteral("activeInterface")).toString();
}
}
NetworkMonitorApplet::~NetworkMonitorApplet()
@@ -39,6 +68,17 @@ bool NetworkMonitorApplet::load()
bool NetworkMonitorApplet::init()
{
// 加载翻译文件:根据系统语言加载对应的 .qm 文件
// 设计原因:QML 源串为英文,中文翻译通过 .qm 文件在运行时加载。
// 翻译上下文为 QML 文件名(如 "networkview"、"AboutWindow"),
// 安装到 QCoreApplication 后 qsTr() 自动查找匹配。
QTranslator *translator = new QTranslator(this);
const QString locale = QLocale::system().name();
if (translator->load(QStringLiteral("jnetapplet_%1.qm").arg(locale),
QStringLiteral(TRANSLATIONS_DIR))) {
qApp->installTranslator(translator);
}
detectInterfaces();
// 启动定时刷新
@@ -54,12 +94,12 @@ bool NetworkMonitorApplet::init()
double NetworkMonitorApplet::downloadSpeed() const
{
return static_cast<double>(m_downloadSpeed);
return m_downloadSpeed;
}
double NetworkMonitorApplet::uploadSpeed() const
{
return static_cast<double>(m_uploadSpeed);
return m_uploadSpeed;
}
double NetworkMonitorApplet::totalDownload() const
@@ -77,23 +117,6 @@ QStringList NetworkMonitorApplet::networkInterfaces() const
return m_interfaceList;
}
QStringList NetworkMonitorApplet::interfaceStats() const
{
QStringList stats;
for (const QString &name : m_interfaceList) {
if (m_interfaces.contains(name)) {
const NetworkInterface &iface = m_interfaces[name];
stats << QString("%1|%2|%3|%4|%5")
.arg(iface.name)
.arg(iface.rxBytes)
.arg(iface.txBytes)
.arg(iface.rxPackets)
.arg(iface.txPackets);
}
}
return stats;
}
bool NetworkMonitorApplet::ready() const
{
return m_ready;
@@ -124,7 +147,78 @@ QString NetworkMonitorApplet::version() const
// DPluginMetaData::value() 已在 "Plugin" 层级内部查找(源码:frame/pluginmetadata.cpp
// metadata.json 的 { "Plugin": { "Version": "x.y.z" } } 被 DPluginMetaData 加载后,
// value("Version") 直接返回版本号,无需再 .value("Plugin").toMap() 展开
return pluginMetaData().value("Version").toString();
const QString ver = pluginMetaData().value("Version").toString();
// 兜底:元数据未加载或无 Version 字段时回退到编译时版本号
// 设计原因:QML 各处 version 属性兜底不一致(networkview.qml 有 "1.0"AboutWindow 无),
// 统一在后端兜底,避免 AboutWindow 版本行显示空白
return ver.isEmpty() ? QStringLiteral(PROJECT_VERSION) : ver;
}
// 返回任务栏网速字体颜色,空串表示跟随系统主题
QString NetworkMonitorApplet::textColor() const
{
return m_textColor;
}
// 设置任务栏网速字体颜色,空串表示跟随系统主题
// 设计原因:
// - 空串语义:空串作为"跟随系统"的语义值,QML 层用 textColor ? textColor : primaryText 做回退,
// 避免后端硬编码系统色
// - 校验逻辑:非空颜色值通过 QColor::isValidColor 校验,无效值直接忽略,不存储不发射信号
// - 独立配置文件:使用 ~/.config/jnetapplet/settings.ini 而非 dde-shell 共享配置,
// 避免污染其他组件的配置空间
void NetworkMonitorApplet::setTextColor(const QString &color)
{
// 空串视为合法(表示跟随系统),不校验
if (!color.isEmpty() && !QColor::isValidColorName(color)) {
return;
}
if (m_textColor == color) {
return;
}
m_textColor = color;
// 持久化到独立配置文件
const QString configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)
+ QStringLiteral("/jnetapplet/settings.ini");
QSettings settings(configPath, QSettings::IniFormat);
settings.setValue(QStringLiteral("textColor"), m_textColor);
settings.sync();
emit textColorChanged();
}
// 返回当前活动接口的下行速度历史,转换为 QVariantList of QPointF 供 QML 使用
// 设计原因:QPointF 仅携带 x/y 两个值,上传与下载分别对应两个列表
QVariantList NetworkMonitorApplet::speedHistoryDownload() const
{
QVariantList list;
if (m_activeInterface.isEmpty() || !m_speedHistory.contains(m_activeInterface)) {
return list;
}
const QVector<SpeedSample> &samples = m_speedHistory[m_activeInterface];
list.reserve(samples.size());
for (const SpeedSample &s : samples) {
list.append(QPointF(s.timestamp, s.downloadSpeed));
}
return list;
}
// 返回当前活动接口的上行速度历史,结构同 speedHistoryDownload
QVariantList NetworkMonitorApplet::speedHistoryUpload() const
{
QVariantList list;
if (m_activeInterface.isEmpty() || !m_speedHistory.contains(m_activeInterface)) {
return list;
}
const QVector<SpeedSample> &samples = m_speedHistory[m_activeInterface];
list.reserve(samples.size());
for (const SpeedSample &s : samples) {
list.append(QPointF(s.timestamp, s.uploadSpeed));
}
return list;
}
void NetworkMonitorApplet::refresh()
@@ -134,10 +228,29 @@ void NetworkMonitorApplet::refresh()
void NetworkMonitorApplet::setActiveInterface(const QString &interface)
{
// 校验接口是否存在于当前接口列表中,无效接口名直接忽略
// 设计原因:传入不存在的接口名会被持久化到配置文件,且当前会话期间
// getActiveRxBytes() 返回 0 导致速度恒为 0,用户困惑;
// 虽然下次启动 readNetworkStats() 会清空回退,但当前会话不应接受无效值
if (!m_interfaceList.contains(interface)) {
return;
}
if (m_activeInterface != interface) {
m_activeInterface = interface;
m_firstUpdate = true;
// 持久化到配置文件,下次启动自动加载用户选择的网卡
// 设计原因:弹窗 chip 和设置窗口都调用此方法,统一持久化保证两处选择一致
const QString configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)
+ QStringLiteral("/jnetapplet/settings.ini");
QSettings settings(configPath, QSettings::IniFormat);
settings.setValue(QStringLiteral("activeInterface"), m_activeInterface);
settings.sync();
emit activeInterfaceChanged();
// 接口切换后通知 QML 趋势图切换显示新接口的历史数据
emit speedHistoryChanged();
// 接口切换后立即检测新接口的 IP 地址
detectIpAddress();
}
@@ -191,6 +304,22 @@ void NetworkMonitorApplet::readNetworkStats()
m_interfaceList = newInterfaces.keys();
m_interfaces = newInterfaces;
// 清理已消失接口的历史缓冲和基线数据,避免内存泄漏
// 设计原因:USB 网卡、VPN 隧道等接口可能随时消失,其历史数据和基线不再需要
const QStringList oldIfaces = m_speedHistory.keys();
for (const QString &name : oldIfaces) {
if (!newInterfaces.contains(name)) {
m_speedHistory.remove(name);
m_lastRxBytesByIface.remove(name);
m_lastTxBytesByIface.remove(name);
}
}
// 保存的接口已不在当前列表中(如 USB 网卡拔出),清空让自动选择接管
if (!m_activeInterface.isEmpty() && !m_interfaceList.contains(m_activeInterface)) {
m_activeInterface.clear();
}
// 自动选择活动接口
if (m_activeInterface.isEmpty() && !m_interfaceList.isEmpty()) {
// 优先选择物理网卡(wlp/wlan/enp/eth)中有流量的接口,
@@ -228,33 +357,77 @@ void NetworkMonitorApplet::readNetworkStats()
}
calculateSpeed();
emit statsChanged();
// 每次刷新都检测 IP,以应对 DHCP 续约等 IP 变更场景
detectIpAddress();
}
void NetworkMonitorApplet::calculateSpeed()
{
qint64 currentRxBytes = getActiveRxBytes();
qint64 currentTxBytes = getActiveTxBytes();
// 当前采样时间戳(毫秒),用于按真实流逝时间计算速度
const qint64 nowMs = QDateTime::currentMSecsSinceEpoch();
const qint64 nowSec = QDateTime::currentSecsSinceEpoch();
const double elapsedSec = (nowMs - m_lastTimestampMs) / 1000.0;
m_lastTimestampMs = nowMs;
// ---- 活动接口速度计算(用于任务栏显示和总量累加)----
if (m_firstUpdate) {
// 首次更新或接口切换后:仅记录基线,不计算速度
m_firstUpdate = false;
m_lastRxBytes = getActiveRxBytes();
m_lastTxBytes = getActiveTxBytes();
} else if (elapsedSec > 0 && !m_activeInterface.isEmpty()) {
// 按真实流逝时间计算速度(字节/秒),避免定时器抖动失真
const qint64 currentRxBytes = getActiveRxBytes();
const qint64 currentTxBytes = getActiveTxBytes();
// 计数器回绕/接口重置时差值可能为负,钳制为 0 避免显示负速度
const qint64 rxDelta = currentRxBytes - m_lastRxBytes;
const qint64 txDelta = currentTxBytes - m_lastTxBytes;
const qint64 rxDeltaClamped = rxDelta > 0 ? rxDelta : 0;
const qint64 txDeltaClamped = txDelta > 0 ? txDelta : 0;
m_downloadSpeed = rxDeltaClamped / elapsedSec;
m_uploadSpeed = txDeltaClamped / elapsedSec;
// 累加会话总量:总量 = 本次会话期间所有活动接口的流量总和
m_totalDownload += rxDeltaClamped;
m_totalUpload += txDeltaClamped;
m_lastRxBytes = currentRxBytes;
m_lastTxBytes = currentTxBytes;
return;
}
// 计算速度(字节/秒)
m_downloadSpeed = currentRxBytes - m_lastRxBytes;
m_uploadSpeed = currentTxBytes - m_lastTxBytes;
// ---- 为所有接口采集速度历史(非仅活动接口)----
// 设计原因:原仅采集活动接口,切换到非活动接口时趋势图为空需等待数分钟。
// 改为遍历所有接口计算各自速度并追加历史,切换接口时趋势图立即有数据。
if (elapsedSec > 0) {
for (const QString &name : m_interfaceList) {
if (!m_interfaces.contains(name)) continue;
const NetworkInterface &iface = m_interfaces[name];
// 更新总量
m_totalDownload = currentRxBytes;
m_totalUpload = currentTxBytes;
// 接口尚未初始化基线(首次采集或运行中新增的接口),记录基线跳过本次
if (!m_lastRxBytesByIface.contains(name)) {
m_lastRxBytesByIface[name] = iface.rxBytes;
m_lastTxBytesByIface[name] = iface.txBytes;
continue;
}
m_lastRxBytes = currentRxBytes;
m_lastTxBytes = currentTxBytes;
const qint64 iRxDelta = iface.rxBytes - m_lastRxBytesByIface[name];
const qint64 iTxDelta = iface.txBytes - m_lastTxBytesByIface[name];
SpeedSample sample;
sample.timestamp = nowSec;
sample.downloadSpeed = (iRxDelta > 0 ? iRxDelta : 0) / elapsedSec;
sample.uploadSpeed = (iTxDelta > 0 ? iTxDelta : 0) / elapsedSec;
m_speedHistory[name].append(sample);
// 滑动窗口裁剪:超过 300 点时丢弃最旧的
QVector<SpeedSample> &samples = m_speedHistory[name];
while (samples.size() > MAX_HISTORY_SAMPLES) {
samples.removeFirst();
}
m_lastRxBytesByIface[name] = iface.rxBytes;
m_lastTxBytesByIface[name] = iface.txBytes;
}
emit speedHistoryChanged();
}
emit speedChanged();
emit totalChanged();
@@ -269,8 +442,11 @@ void NetworkMonitorApplet::detectInterfaces()
m_interfaceList.clear();
for (const QString &entry : entries) {
// 过滤回环接口
if (entry != "lo") {
// 过滤回环接口和虚拟接口,与 readNetworkStats() 保持一致
// 设计原因:原仅过滤 lodocker/veth/br- 接口会短暂出现在列表中,
// 随后被 readNetworkStats() 覆盖,造成 UI 闪烁
if (entry != "lo" && !entry.startsWith("veth") &&
!entry.startsWith("docker") && !entry.startsWith("br-")) {
m_interfaceList << entry;
}
}
+58 -7
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024 MyCompany
// SPDX-FileCopyrightText: 2026 Jokul
//
// SPDX-License-Identifier: LGPL-3.0-or-later
@@ -11,6 +11,11 @@
#include <QTimer>
#include <QVariantList>
#include <QMap>
#include <QHash>
#include <QPointF>
#include <QDateTime>
#include <QColor>
#include <QSettings>
DS_BEGIN_NAMESPACE
@@ -26,6 +31,15 @@ struct NetworkInterface {
qint64 txDropped; // 发送丢弃数
};
// 单个速度采样点:用于趋势图的环形缓冲
// timestamp: 自 epoch 的秒级时间戳,作为 X 轴定位
// downloadSpeed / uploadSpeed: 当时的瞬时速度(字节/秒),作为两条折线的 Y 值
struct SpeedSample {
qint64 timestamp;
double downloadSpeed;
double uploadSpeed;
};
class NetworkMonitorApplet : public DApplet
{
Q_OBJECT
@@ -35,8 +49,9 @@ class NetworkMonitorApplet : public DApplet
Q_PROPERTY(double totalDownload READ totalDownload NOTIFY totalChanged)
Q_PROPERTY(double totalUpload READ totalUpload NOTIFY totalChanged)
Q_PROPERTY(QStringList networkInterfaces READ networkInterfaces NOTIFY interfacesChanged)
Q_PROPERTY(QStringList interfaceStats READ interfaceStats NOTIFY statsChanged)
Q_PROPERTY(bool ready READ ready NOTIFY readyChanged)
// 当前活动接口,用户在弹窗或设置窗口选择后持久化到 ~/.config/jnetapplet/settings.ini
// 下次启动自动恢复;若保存的接口已不存在则回退到自动选择
Q_PROPERTY(QString activeInterface READ activeInterface NOTIFY activeInterfaceChanged)
// 活动接口的 IPv4 地址,供 QML 在弹出面板和 tooltip 中显示
Q_PROPERTY(QString ipAddress READ ipAddress NOTIFY ipAddressChanged)
@@ -44,6 +59,14 @@ class NetworkMonitorApplet : public DApplet
Q_PROPERTY(QString ipv6Address READ ipv6Address NOTIFY ipv6AddressChanged)
// 插件版本号,从 dde-shell 元数据读取,版本唯一源为 CMakeLists.txt project(VERSION)
Q_PROPERTY(QString version READ version CONSTANT)
// 任务栏网速数值字体颜色,空串表示跟随系统主题(由 QML 回退到 primaryText),
// 用户在设置窗口选择后持久化到 ~/.config/jnetapplet/settings.ini
Q_PROPERTY(QString textColor READ textColor WRITE setTextColor NOTIFY textColorChanged)
// 当前活动接口的下行速度历史采样点(QPointF 列表:x=时间戳秒, y=速度 bytes/sec
// 用于趋势图绘制下载折线,每次 calculateSpeed 追加一个点并发射 speedHistoryChanged
Q_PROPERTY(QVariantList speedHistoryDownload READ speedHistoryDownload NOTIFY speedHistoryChanged)
// 当前活动接口的上行速度历史采样点,结构同 speedHistoryDownload
Q_PROPERTY(QVariantList speedHistoryUpload READ speedHistoryUpload NOTIFY speedHistoryChanged)
public:
explicit NetworkMonitorApplet(QObject *parent = nullptr);
@@ -57,26 +80,37 @@ public:
double totalDownload() const;
double totalUpload() const;
QStringList networkInterfaces() const;
QStringList interfaceStats() const;
bool ready() const;
QString activeInterface() const;
QString ipAddress() const;
QString ipv6Address() const;
// 返回当前活动接口的下行速度历史(QPointF 列表),供 QML 趋势图绘制
QVariantList speedHistoryDownload() const;
// 返回当前活动接口的上行速度历史(QPointF 列表),供 QML 趋势图绘制
QVariantList speedHistoryUpload() const;
// 返回插件版本号,从 dde-shell 插件元数据(metadata.json)读取
QString version() const;
// 返回任务栏网速字体颜色,空串表示跟随系统主题
QString textColor() const;
// 设置任务栏网速字体颜色,空串表示跟随系统主题
void setTextColor(const QString &color);
Q_INVOKABLE void refresh();
// 定时器回调,由 m_refreshTimer 每秒调用。非 Q_INVOKABLEQML 不应直接调用,
// 否则会打破 1 秒定时间隔导致速度计算失真)
void refresh();
Q_INVOKABLE void setActiveInterface(const QString &interface);
signals:
void speedChanged();
void totalChanged();
void interfacesChanged();
void statsChanged();
void readyChanged();
void activeInterfaceChanged();
void ipAddressChanged();
void ipv6AddressChanged();
void speedHistoryChanged();
// 任务栏网速字体颜色变化时通知 QML 更新
void textColorChanged();
private:
void readNetworkStats();
@@ -100,8 +134,15 @@ private:
// 速度计算
qint64 m_lastRxBytes;
qint64 m_lastTxBytes;
qint64 m_downloadSpeed;
qint64 m_uploadSpeed;
// 上次采样的毫秒时间戳,用于按真实间隔计算速度(避免定时器抖动失真)
qint64 m_lastTimestampMs;
// 当前瞬时速度(字节/秒),用 double 存储以保留按真实间隔除算的小数精度
double m_downloadSpeed;
double m_uploadSpeed;
// 每个接口的上次收发字节数,用于为所有接口(非仅活动接口)计算速度并采集历史
// 设计原因:原仅跟踪活动接口,切换到非活动接口时趋势图为空需等待数分钟才有数据
QHash<QString, qint64> m_lastRxBytesByIface;
QHash<QString, qint64> m_lastTxBytesByIface;
// 总量
qint64 m_totalDownload;
@@ -109,6 +150,16 @@ private:
bool m_ready;
bool m_firstUpdate;
// 任务栏网速字体颜色,空串表示跟随系统主题
// 设计原因:空串作为"跟随系统"的语义值,QML 层用 textColor ? textColor : primaryText
// 做回退,避免后端硬编码系统色
QString m_textColor;
// 速度历史环形缓冲:每个接口独立维护一份,key 为接口名
// 设计原因:接口切换时趋势图不出现跳变,切回时仍能看到该接口历史
QHash<QString, QVector<SpeedSample>> m_speedHistory;
// 每个接口最多保留的采样点数:5 分钟 × 60 秒 = 300
static constexpr int MAX_HISTORY_SAMPLES = 300;
};
DS_END_NAMESPACE
+241
View File
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="zh_CN">
<context>
<name>AboutWindow</name>
<message>
<location filename="../package/components/AboutWindow.qml" line="86"/>
<source>About</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/AboutWindow.qml" line="133"/>
<source>Network Speed Monitor</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/AboutWindow.qml" line="182"/>
<source>Version</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/AboutWindow.qml" line="203"/>
<source>Author</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/AboutWindow.qml" line="224"/>
<source>Description</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/AboutWindow.qml" line="232"/>
<source>Monitor network speed and traffic</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/AboutWindow.qml" line="246"/>
<source>Repository</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/AboutWindow.qml" line="284"/>
<source>Copied</source>
<translation></translation>
</message>
</context>
<context>
<name>NetworkPopup</name>
<message>
<location filename="../package/components/NetworkPopup.qml" line="98"/>
<source>Network Speed Monitor</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/NetworkPopup.qml" line="129"/>
<source>IPv4: </source>
<translation>IPv4</translation>
</message>
<message>
<location filename="../package/components/NetworkPopup.qml" line="138"/>
<source>IPv6: </source>
<translation>IPv6</translation>
</message>
<message>
<location filename="../package/components/NetworkPopup.qml" line="190"/>
<source>Download</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/NetworkPopup.qml" line="247"/>
<source>Upload</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/NetworkPopup.qml" line="282"/>
<source>Total</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/NetworkPopup.qml" line="459"/>
<source>No network interface detected</source>
<translation></translation>
</message>
</context>
<context>
<name>SettingsWindow</name>
<message>
<location filename="../package/components/SettingsWindow.qml" line="54"/>
<location filename="../package/components/SettingsWindow.qml" line="383"/>
<location filename="../package/components/SettingsWindow.qml" line="560"/>
<source>Uninstall Plugin</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="73"/>
<source>Loopback</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="74"/>
<source>Virtual Interface</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="75"/>
<source>Wired Network</source>
<translation>线</translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="76"/>
<source>Wireless Network</source>
<translation>线</translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="77"/>
<source>Container Network</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="78"/>
<source>Bridge</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="79"/>
<source>VPN</source>
<translation>VPN</translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="80"/>
<source>Virtual Bridge</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="81"/>
<source>Other</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="142"/>
<source>Settings</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="196"/>
<source>Network Interface</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="349"/>
<source>No network interface detected</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="360"/>
<source>Font Color</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="446"/>
<source>Warning</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="454"/>
<source>The following command uninstalls the plugin and restarts dde-shell. Please copy and run it in terminal:</source>
<translation> dde-shell</translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="499"/>
<source>Cancel</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="522"/>
<source>Copy Command</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/SettingsWindow.qml" line="539"/>
<source>Uninstall command copied to clipboard. Please paste and run it in terminal.</source>
<translation></translation>
</message>
</context>
<context>
<name>TrafficChartWindow</name>
<message>
<location filename="../package/components/TrafficChartWindow.qml" line="180"/>
<source>Traffic Chart</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/TrafficChartWindow.qml" line="424"/>
<source>No history data</source>
<translation></translation>
</message>
<message>
<location filename="../package/components/TrafficChartWindow.qml" line="581"/>
<source>Hover over the curve for details</source>
<translation>线</translation>
</message>
</context>
<context>
<name>networkview</name>
<message>
<location filename="../package/networkview.qml" line="225"/>
<source>IPv4: </source>
<translation>IPv4</translation>
</message>
<message>
<location filename="../package/networkview.qml" line="225"/>
<source>None</source>
<translation></translation>
</message>
<message>
<location filename="../package/networkview.qml" line="226"/>
<source>IPv6: </source>
<translation>IPv6</translation>
</message>
<message>
<location filename="../package/networkview.qml" line="227"/>
<source>No network interface detected</source>
<translation></translation>
</message>
<message>
<location filename="../package/networkview.qml" line="303"/>
<source>Settings</source>
<translation></translation>
</message>
<message>
<location filename="../package/networkview.qml" line="313"/>
<source>Traffic Chart</source>
<translation></translation>
</message>
<message>
<location filename="../package/networkview.qml" line="324"/>
<source>About</source>
<translation></translation>
</message>
</context>
</TS>