23 Commits
Author SHA1 Message Date
Jokul 73e5487d71 chore: 版本号升至 1.2.0
1.1.0 -> 1.2.0 变更清单:

新增功能:
- 国际化支持(英文源串+中文翻译,按系统语言自动加载)
- C++ 后端单元测试(5个测试用例)
- 全接口速度历史采样(切换网卡趋势图立即有数据)

缺陷修复:
- 速度计算按真实时间间隔而非假定1秒
- 计数器回绕时负速度钳制为0
- 总量统计改为累加会话增量,切换网卡不再跳变
- 独立窗口及任务栏图标适配深色模式
- detectInterfaces过滤规则统一,消除UI闪烁
- setActiveInterface校验接口有效性
- 窗口居中适配多显示器
- version()元数据为空时回退到编译时版本号
- SPDX版权头改为2026 Jokul

重构:
- 抽取NetCommon.qml消除跨组件重复代码
- 删除interfaceStats死代码
- 移除refresh()的Q_INVOKABLE标记

文档:
- 更新README和AGENTS.md
- 新增需求报告
2026-07-24 19:16:39 +08:00
Jokul 02296ba13e docs: 重整需求报告,移除已修复缺陷,仅保留待开发需求 2026-07-24 19:06:20 +08:00
Jokul 2e615df2ff test: 新增C++后端单元测试,标记#17完成并更新审查清单
新建 tests/tst_networkmonitorapplet.cpp(Qt Test),5个测试用例覆盖
isPhysicalInterface、速度计算、负值钳制、总量累加、接口校验。

同时更新审查清单:#17标记完成,推荐开发中#19/#20/#21/#23/#30/#33
标记为随缺陷修复一并完成。
2026-07-24 19:03:52 +08:00
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
15 changed files with 1672 additions and 663 deletions
+27 -16
View File
@@ -15,14 +15,18 @@ for the UI. The C++ backend reads `/proc/net/dev` to monitor network traffic.
## Features ## Features
- Real-time download/upload speed monitoring (1-second refresh) - Real-time download/upload speed monitoring (1-second refresh, real elapsed time)
- Total data transfer statistics - Session cumulative download/upload traffic statistics
- Multiple network interface support with interface switching - Multiple network interface support with interface switching (popup + settings)
- Taskbar icon displays live speed (changes color at high speeds) - Taskbar icon displays live speed (changes color at high speeds)
- Hover tooltip shows speed summary - Customizable taskbar font color (follow system theme or pick from preset)
- Click to open detailed popup - Hover tooltip shows interface name + IP address
- Popup shows active interface IP address, per-interface speed comparison and stats - Click to open detailed popup (speed, totals, interface chips)
- One-click plugin uninstall from the popup - 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 ## 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 │ ├── metadata.json.in # Plugin metadata 模板(由 CMake 生成 metadata.json
│ ├── networkview.qml # QML UI 主入口 │ ├── networkview.qml # QML UI 主入口
│ └── components/ # 拆分出的子组件 │ └── components/ # 拆分出的子组件
── AboutWindow.qml # 关于窗口组件 ── NetCommon.qml # 公共颜色与格式化函数
├── docs/ # Design docs and implementation plans │ ├── NetworkPopup.qml # 左键弹窗内容
│ ├── SettingsWindow.qml # 设置窗口(接口/颜色/卸载)
│ ├── TrafficChartWindow.qml # 流量波动图窗口
│ ├── TextColorPicker.qml # 字体颜色选择器
│ └── AboutWindow.qml # 关于窗口
├── docs/ # Design docs and review checklist
└── AGENTS.md # This file └── AGENTS.md # This file
``` ```
@@ -100,14 +109,16 @@ then restart `dde-shell` (it discovers applets by scanning the install dir for
## C++ Backend (NetworkMonitorApplet) ## C++ Backend (NetworkMonitorApplet)
Inherits from `DApplet`, provides: Inherits from `DApplet`, provides:
- `downloadSpeed` / `uploadSpeed`: Current speed in bytes/sec - `downloadSpeed` / `uploadSpeed`: Current speed in bytes/sec (real elapsed time)
- `totalDownload` / `totalUpload`: Total data transferred - `totalDownload` / `totalUpload`: Session cumulative traffic
- `networkInterfaces`: List of available interfaces - `networkInterfaces`: List of available interfaces
- `interfaceStats`: Per-interface statistics - `activeInterface`: Currently selected interface (persisted to `~/.config/jnetapplet/settings.ini`)
- `activeInterface`: Currently selected interface - `ipAddress` / `ipv6Address`: IP address of the active interface
- `ipAddress`: IP address of the active interface - `version`: Plugin version (from metadata, fallback to `PROJECT_VERSION` macro)
- `refresh()`: Manually trigger stats update - `textColor`: Taskbar font color (empty = follow system theme, persisted)
- `setActiveInterface(name)`: Switch active interface - `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) ## Identifier correspondence (keep in sync when renaming)
+46 -2
View File
@@ -1,12 +1,12 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(JNetApplet VERSION 1.1.0 LANGUAGES CXX) project(JNetApplet VERSION 1.2.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Core Quick DBus Network) find_package(Qt6 REQUIRED COMPONENTS Core Quick DBus Network LinguistTools Test)
find_package(Dtk6 REQUIRED COMPONENTS Core) find_package(Dtk6 REQUIRED COMPONENTS Core)
find_package(DDEShell REQUIRED) find_package(DDEShell REQUIRED)
@@ -20,6 +20,20 @@ add_library(space.jokul.JNetApplet SHARED
set_target_properties(space.jokul.JNetApplet PROPERTIES PREFIX "") 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" 可被外部解析 # 头文件搜索路径指向 src/,使 #include "networkmonitorapplet.h" 可被外部解析
target_include_directories(space.jokul.JNetApplet PRIVATE target_include_directories(space.jokul.JNetApplet PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/src
@@ -49,3 +63,33 @@ install(FILES package/networkview.qml DESTINATION /usr/share/dde-shell/${PLUGIN_
# 设计原因:拆分出的子组件需随主 QML 一起安装到 dde-shell 插件目录, # 设计原因:拆分出的子组件需随主 QML 一起安装到 dde-shell 插件目录,
# 否则 networkview.qml 的 import "components" 在运行时找不到类型 # 否则 networkview.qml 的 import "components" 在运行时找不到类型
install(DIRECTORY package/components DESTINATION /usr/share/dde-shell/${PLUGIN_ID}) install(DIRECTORY package/components DESTINATION /usr/share/dde-shell/${PLUGIN_ID})
# 安装编译后的翻译文件
install(FILES ${QM_FILES} DESTINATION /usr/share/dde-shell/${PLUGIN_ID}/translations)
# ---- 单元测试 ----
# 直接编译源文件到测试可执行文件,避免共享库依赖问题
enable_testing()
add_executable(tst_networkmonitorapplet
tests/tst_networkmonitorapplet.cpp
src/networkmonitorapplet.cpp
src/networkmonitorapplet.h
)
target_link_libraries(tst_networkmonitorapplet PRIVATE
Qt6::Test
Qt6::Core
Qt6::Quick
Qt6::DBus
Qt6::Network
Dtk6::Core
Dde::Shell
)
target_include_directories(tst_networkmonitorapplet PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
/usr/include/dde-shell
)
target_compile_definitions(tst_networkmonitorapplet PRIVATE
PROJECT_VERSION="${PROJECT_VERSION}"
TRANSLATIONS_DIR="${CMAKE_CURRENT_SOURCE_DIR}/translations"
)
add_test(NAME tst_networkmonitorapplet COMMAND tst_networkmonitorapplet)
+17 -8
View File
@@ -9,13 +9,17 @@ QML 前端渲染界面,实时显示当前网卡的上行/下行速率与累计
## 功能特性 ## 功能特性
- 实时上行/下行速率监控(1 秒刷新) - 实时上行/下行速率监控(1 秒刷新,按真实时间间隔计算
- 累计上传/下载流量统计 - 本次会话累计上传/下载流量统计
- 多网卡支持,可在弹窗中切换活动网卡 - 多网卡支持,可在弹窗或设置窗口中切换活动网卡
- 任务栏图标实时显示速率,高速时变色提示 - 任务栏图标实时显示速率,高速时变色提示
- 悬停显示速率摘要,点击展开详情弹窗 - 任务栏字体颜色可自定义,支持跟随系统主题
- 详情弹窗展示活动网卡 IP 地址、各网卡速率对比与统计图表 - 悬停显示速率摘要与网卡 IP 地址,点击展开详情弹窗
- 支持从弹窗一键卸载插件 - 详情弹窗展示活动网卡 IPv4/IPv6 地址、各网卡速率对比与统计
- 流量波动图窗口:展示活动接口最近 5 分钟网速趋势(下载/上传双折线)
- 设置窗口:网络接口选择、字体颜色自定义、一键卸载插件
- 深色模式全适配(任务栏图标、弹窗、独立窗口均跟随系统主题)
- 所有接口后台持续采集速度历史,切换网卡时趋势图立即有数据
## 构建与安装 ## 构建与安装
@@ -89,8 +93,13 @@ bash build-deb.sh
│ ├── metadata.json.in # 插件元数据模板(由 CMake 生成 metadata.json │ ├── metadata.json.in # 插件元数据模板(由 CMake 生成 metadata.json
│ ├── networkview.qml # QML 界面主入口 │ ├── networkview.qml # QML 界面主入口
│ └── components/ # 拆分出的子组件 │ └── components/ # 拆分出的子组件
── AboutWindow.qml # 关于窗口组件 ── NetCommon.qml # 公共颜色与格式化函数
├── docs/ # 设计文档与实现计划 │ ├── NetworkPopup.qml # 左键弹窗内容(速度/总量/网卡切换)
│ ├── SettingsWindow.qml # 设置窗口(接口选择/字体颜色/卸载)
│ ├── TrafficChartWindow.qml # 流量波动图窗口(5分钟趋势折线)
│ ├── TextColorPicker.qml # 字体颜色选择器
│ └── AboutWindow.qml # 关于窗口
├── docs/ # 设计文档与审查清单
├── README.md # 本文件 ├── README.md # 本文件
└── AGENTS.md # AI 代理工作指引 └── AGENTS.md # AI 代理工作指引
``` ```
+38
View File
@@ -0,0 +1,38 @@
# JNetApplet 需求报告
> 生成日期:2026-07-24
> 基于项目全面审查,已修复的缺陷与代码质量问题已移除,本文件仅保留待开发的新需求。
---
## 推荐开发
| # | 需求 | 说明 | 价值 |
|---|------|------|------|
| 1 | **日/月流量统计持久化** | 将每日累计流量写入 `settings.ini`,重启后保留。弹窗/设置窗口展示今日/本月用量 | 核心功能提升 |
| 2 | **网络断连检测** | 活动接口 `operState != Up` 或 IP 丢失时,任务栏显示断连图标 + tooltip 提示 | 用户体验提升 |
| 3 | **速度单位切换** | 设置中可选 KB/s(二进制)或 Mbps(十进制),或智能自动切换 | 国际化/习惯适配 |
## 可选增强
| # | 需求 | 说明 |
|---|------|------|
| 4 | **刷新间隔可配置** | 设置中可选 1s/2s/5s 刷新频率,降低低端设备 CPU 占用 |
| 5 | **流量超限通知** | 设置阈值,下载/上传超限时发送系统通知 |
| 6 | **趋势图时间窗口可选** | 流量波动图支持 5min / 30min / 1h 切换(需持久化历史数据) |
| 7 | **多接口聚合速度** | 任务栏可选显示所有接口合计速度,而非仅活动接口 |
| 8 | **HiDPI 适配** | 独立窗口使用相对尺寸而非固定像素,适配高分辨率屏幕 |
| 9 | **DTK 原生窗口框架** | 独立窗口改用 `DWindow` / DTK 窗口装饰,获得原生 deepin 标题栏、圆角、阴影 |
| 10 | **导出流量数据** | 将流量统计导出为 CSV/JSON,便于分析 |
| 11 | **丢包/错误率展示** | 弹窗中展示 `rxErrors`/`rxDropped`,网络质量诊断 |
| 12 | **开机自启保障** | 确保 dde-shell 启动时插件自动加载(可能需 dde-shell 配置) |
---
## 优先级建议
**第一优先级**#1 日/月流量统计持久化、#2 网络断连检测
**第二优先级**#3 速度单位切换、#5 流量超限通知
**第三优先级**:其余可选增强按需开发
+49 -33
View File
@@ -1,11 +1,13 @@
// SPDX-FileCopyrightText: 2024 MyCompany // SPDX-FileCopyrightText: 2026 Jokul
// //
// SPDX-License-Identifier: LGPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later
// 关于窗口:DTK 原生风格的独立顶层窗口,桌面居中弹出,展示插件信息 // 关于窗口:DTK 原生风格的独立顶层窗口,桌面居中弹出,展示插件信息
// 设计要点:白色圆角卡片 + 键值对信息布局,信息标签使用 deepin 蓝(#0081FF), // 设计要点:圆角卡片 + 键值对信息布局,信息标签使用 deepin 蓝(#0081FF),
// 关闭按钮 hover 高亮色由父组件通过 accentColor 传入(红色) // 关闭按钮 hover 高亮色由父组件通过 accentColor 传入(红色)
// 对外依赖:accentColor,由 networkview.qml 实例化时传入,触发方式为 show()/raise()/requestActivate() // 深/浅主题由 isDarkMode 切换(networkview.qml 依据 DockPalette 检测任务栏深浅后传入),
// 默认浅色,保证组件独立预览时与原版视觉一致
// 对外依赖:accentColor、isDarkMode,由 networkview.qml 实例化时传入,触发方式为 show()/raise()/requestActivate()
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import QtQuick.Window 2.15 import QtQuick.Window 2.15
@@ -21,6 +23,18 @@ Window {
// 默认值保证组件独立可用(如 qmlscene 预览时未传入) // 默认值保证组件独立可用(如 qmlscene 预览时未传入)
property string version: "1.0" 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 时复制图标变绿并显示"已复制"提示, // 复制成功状态标记:true 时复制图标变绿并显示"已复制"提示,
// 2 秒后由 copyResetTimer 复位 // 2 秒后由 copyResetTimer 复位
property bool copied: false property bool copied: false
@@ -34,18 +48,20 @@ Window {
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
x = (Screen.width - width) / 2 // 居中到当前屏幕(任务栏所在屏幕),加 virtualX/virtualY 偏移
y = (Screen.height - height) / 2 // 避免多显示器下窗口出现在非预期屏幕
x = Screen.virtualX + (Screen.width - width) / 2
y = Screen.virtualY + (Screen.height - height) / 2
} }
} }
// 窗口主体:白色圆角卡片,1px 浅灰边框模拟 DTK 窗口描边 // 窗口主体:圆角卡片(背景与边框随 isDarkMode 切换深浅)1px 边框模拟 DTK 窗口描边
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: "#FFFFFF" color: root.winBg
radius: 12 radius: 12
border.width: 1 border.width: 1
border.color: "#E8E8E8" border.color: root.lineColor
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
@@ -67,10 +83,10 @@ Window {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 16 anchors.leftMargin: 16
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: qsTr("关于") text: qsTr("About")
font.pixelSize: 15 font.pixelSize: 15
font.weight: Font.Bold font.weight: Font.Bold
color: "#333333" color: root.textPrimary
} }
// 关闭按钮:28x28 圆形,hover 时淡红底 + 红色 ×(颜色由 accentColor 决定) // 关闭按钮:28x28 圆形,hover 时淡红底 + 红色 ×(颜色由 accentColor 决定)
@@ -88,7 +104,7 @@ Window {
text: "×" text: "×"
font.pixelSize: 20 font.pixelSize: 20
font.weight: Font.Bold font.weight: Font.Bold
color: aboutCloseMouse.containsMouse ? accentColor : "#666666" color: aboutCloseMouse.containsMouse ? accentColor : root.textSecondary
} }
MouseArea { MouseArea {
@@ -114,10 +130,10 @@ Window {
spacing: 2 spacing: 2
Text { Text {
text: qsTr("网络速度监控") text: qsTr("Network Speed Monitor")
font.pixelSize: 18 font.pixelSize: 18
font.weight: Font.Bold font.weight: Font.Bold
color: "#333333" color: root.textPrimary
Layout.fillWidth: true Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
@@ -125,7 +141,7 @@ Window {
Text { Text {
text: "JNetApplet" text: "JNetApplet"
font.pixelSize: 12 font.pixelSize: 12
color: "#999999" color: root.textTertiary
Layout.fillWidth: true Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
@@ -142,7 +158,7 @@ Window {
Layout.leftMargin: 24 Layout.leftMargin: 24
Layout.rightMargin: 24 Layout.rightMargin: 24
Layout.preferredHeight: 1 Layout.preferredHeight: 1
color: "#E8E8E8" color: root.lineColor
} }
// 信息区上方间距 // 信息区上方间距
@@ -163,7 +179,7 @@ Window {
spacing: 0 spacing: 0
Text { Text {
text: qsTr("版本") text: qsTr("Version")
font.pixelSize: 12 font.pixelSize: 12
color: "#0081FF" color: "#0081FF"
Layout.preferredWidth: 56 Layout.preferredWidth: 56
@@ -173,7 +189,7 @@ Window {
Text { Text {
text: version text: version
font.pixelSize: 12 font.pixelSize: 12
color: "#333333" color: root.textPrimary
Layout.fillWidth: true Layout.fillWidth: true
} }
} }
@@ -184,7 +200,7 @@ Window {
spacing: 0 spacing: 0
Text { Text {
text: qsTr("作者") text: qsTr("Author")
font.pixelSize: 12 font.pixelSize: 12
color: "#0081FF" color: "#0081FF"
Layout.preferredWidth: 56 Layout.preferredWidth: 56
@@ -194,7 +210,7 @@ Window {
Text { Text {
text: "Jokul" text: "Jokul"
font.pixelSize: 12 font.pixelSize: 12
color: "#333333" color: root.textPrimary
Layout.fillWidth: true Layout.fillWidth: true
} }
} }
@@ -205,7 +221,7 @@ Window {
spacing: 0 spacing: 0
Text { Text {
text: qsTr("描述") text: qsTr("Description")
font.pixelSize: 12 font.pixelSize: 12
color: "#0081FF" color: "#0081FF"
Layout.preferredWidth: 56 Layout.preferredWidth: 56
@@ -213,9 +229,9 @@ Window {
} }
Text { Text {
text: qsTr("监控网络速度和流量") text: qsTr("Monitor network speed and traffic")
font.pixelSize: 12 font.pixelSize: 12
color: "#333333" color: root.textPrimary
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
@@ -227,7 +243,7 @@ Window {
spacing: 0 spacing: 0
Text { Text {
text: qsTr("仓库") text: qsTr("Repository")
font.pixelSize: 12 font.pixelSize: 12
color: "#0081FF" color: "#0081FF"
Layout.preferredWidth: 56 Layout.preferredWidth: 56
@@ -237,7 +253,7 @@ Window {
Text { Text {
text: "git.jokul.space/Jokul/JNetApplet" text: "git.jokul.space/Jokul/JNetApplet"
font.pixelSize: 11 font.pixelSize: 11
color: "#999999" color: root.textTertiary
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Text.WrapAnywhere wrapMode: Text.WrapAnywhere
} }
@@ -248,7 +264,7 @@ Window {
Layout.preferredHeight: 24 Layout.preferredHeight: 24
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
// "已复制"提示:白底绿字小标签,显示在按钮左侧, // "已复制"提示:卡片底色绿字小标签,显示在按钮左侧,
// 覆盖于 URL 文字上方,避免与值文本混排 // 覆盖于 URL 文字上方,避免与值文本混排
Rectangle { Rectangle {
anchors.right: parent.left anchors.right: parent.left
@@ -258,14 +274,14 @@ Window {
width: copiedHintText.implicitWidth + 10 width: copiedHintText.implicitWidth + 10
height: 18 height: 18
radius: 4 radius: 4
color: "#FFFFFF" color: root.cardBg
border.width: 1 border.width: 1
border.color: "#22A34A" border.color: "#22A34A"
Text { Text {
id: copiedHintText id: copiedHintText
anchors.centerIn: parent anchors.centerIn: parent
text: qsTr("已复制") text: qsTr("Copied")
font.pixelSize: 10 font.pixelSize: 10
color: "#22A34A" color: "#22A34A"
} }
@@ -286,7 +302,7 @@ Window {
} }
// 复制图标:两个重叠的小圆角矩形 // 复制图标:两个重叠的小圆角矩形
// 前层矩形白底压住后层边框重叠区,形成"堆叠纸张"的视觉效果 // 前层矩形以卡片底色压住后层边框重叠区,形成"堆叠纸张"的视觉效果
// 后层矩形(向右下偏移 2px // 后层矩形(向右下偏移 2px
Rectangle { Rectangle {
x: 8 x: 8
@@ -294,9 +310,9 @@ Window {
width: 10 width: 10
height: 10 height: 10
radius: 2 radius: 2
color: "#FFFFFF" color: root.cardBg
border.width: 1 border.width: 1
border.color: copyRepoMouse.containsMouse ? "#333333" : "#999999" border.color: copyRepoMouse.containsMouse ? root.textPrimary : root.textTertiary
} }
// 前层矩形(左上),复制成功时边框短暂变绿 // 前层矩形(左上),复制成功时边框短暂变绿
@@ -306,9 +322,9 @@ Window {
width: 10 width: 10
height: 10 height: 10
radius: 2 radius: 2
color: "#FFFFFF" color: root.cardBg
border.width: 1 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 }
}
}
+209 -69
View File
@@ -1,11 +1,14 @@
// SPDX-FileCopyrightText: 2024 MyCompany // SPDX-FileCopyrightText: 2026 Jokul
// //
// SPDX-License-Identifier: LGPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later
// 设置窗口:独立顶层窗口,桌面居中弹出,提供网络接口选择、字体颜色选择、插件卸载功能 // 设置窗口:独立顶层窗口,桌面居中弹出,提供网络接口选择、字体颜色选择、插件卸载功能
// 设计要点:白色圆角卡片 + 自定义标题栏可拖动,接口单选切换,卸载按钮带复制命令功能 // 设计要点:圆角卡片 + 自定义标题栏可拖动,接口单选切换,卸载按钮带复制命令功能
// 深/浅主题由 isDarkMode 切换(networkview.qml 依据 DockPalette 检测任务栏深浅后传入),
// 默认浅色,保证组件独立预览时与原版视觉一致
// 对外依赖:appletC++ 后端对象)、networkInterfaces(接口列表)、activeInterface(当前接口)、 // 对外依赖:appletC++ 后端对象)、networkInterfaces(接口列表)、activeInterface(当前接口)、
// accentColor(强调色),由 networkview.qml 实例化时传入,触发方式为 show()/raise()/requestActivate() // accentColor(强调色)、isDarkMode(深色模式标记),由 networkview.qml 实例化时传入,
// 触发方式为 show()/raise()/requestActivate()
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
@@ -27,8 +30,28 @@ Window {
// 默认值与 networkview.qml 中 root.accentRed 一致,确保独立可用 // 默认值与 networkview.qml 中 root.accentRed 一致,确保独立可用
property color accentColor: Qt.rgba(220 / 255, 38 / 255, 38 / 255, 1) 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("卸载插件") property string uninstallButtonText: qsTr("Uninstall Plugin")
width: 350 width: 350
height: 390 height: 390
@@ -47,32 +70,49 @@ Window {
// 设计原因:用户面对多个网口时难以仅凭 enp3s0/wlp3s0 等命名判断用途, // 设计原因:用户面对多个网口时难以仅凭 enp3s0/wlp3s0 等命名判断用途,
// 加一行类型说明(有线/无线/VPN 等)降低认知负担 // 加一行类型说明(有线/无线/VPN 等)降低认知负担
function interfaceDescription(name) { function interfaceDescription(name) {
if (name === "lo") return qsTr("本地回环") if (name === "lo") return qsTr("Loopback")
if (name === "Meta") return qsTr("虚拟接口") if (name === "Meta") return qsTr("Virtual Interface")
if (/^enp|^eth/.test(name)) return qsTr("有线网络") if (/^enp|^eth/.test(name)) return qsTr("Wired Network")
if (/^wlp|^wlan/.test(name)) return qsTr("无线网络") if (/^wlp|^wlan/.test(name)) return qsTr("Wireless Network")
if (/^docker|^veth/.test(name)) return qsTr("容器网络") if (/^docker|^veth/.test(name)) return qsTr("Container Network")
if (/^br/.test(name)) return qsTr("桥接") if (/^br/.test(name)) return qsTr("Bridge")
if (/^tun|^tap/.test(name)) return qsTr("VPN") if (/^tun|^tap/.test(name)) return qsTr("VPN")
if (/^virbr/.test(name)) return qsTr("虚拟桥接") if (/^virbr/.test(name)) return qsTr("Virtual Bridge")
return qsTr("其他") 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: { onVisibleChanged: {
if (visible) { if (visible) {
x = (Screen.width - width) / 2 // 加 virtualX/virtualY 偏移,避免多显示器下窗口出现在非预期屏幕
y = (Screen.height - height) / 2 x = Screen.virtualX + (Screen.width - width) / 2
y = Screen.virtualY + (Screen.height - height) / 2
} }
} }
// 主容器:提供不透明背景 + 圆角 + 边框 // 主容器:提供不透明背景(随 isDarkMode 切换深浅)+ 圆角 + 边框
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: "#f5f5f5" color: root.winBg
radius: 12 radius: 12
border.width: 1 border.width: 1
border.color: "#e0e0e0" border.color: root.lineColor
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
@@ -99,10 +139,10 @@ Window {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 16 anchors.leftMargin: 16
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: qsTr("设置") text: qsTr("Settings")
font.pixelSize: 15 font.pixelSize: 15
font.weight: Font.Bold font.weight: Font.Bold
color: "#333333" color: root.textPrimary
} }
// 关闭按钮 // 关闭按钮
@@ -121,7 +161,7 @@ Window {
text: "×" text: "×"
font.pixelSize: 20 font.pixelSize: 20
font.weight: Font.Bold font.weight: Font.Bold
color: closeMouse.containsMouse ? accentColor : "#666666" color: closeMouse.containsMouse ? accentColor : root.textSecondary
} }
MouseArea { MouseArea {
@@ -138,7 +178,7 @@ Window {
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 1 Layout.preferredHeight: 1
color: "#e0e0e0" color: root.lineColor
} }
// 内容区域 // 内容区域
@@ -153,65 +193,163 @@ Window {
// 网络接口选择区 // 网络接口选择区
Text { Text {
text: qsTr("网络接口") text: qsTr("Network Interface")
font.pixelSize: 13 font.pixelSize: 13
font.weight: Font.Bold font.weight: Font.Bold
color: "#666666" color: root.textSecondary
} }
// 接口列表容器 // 接口列表容器:最多显示约 5 行(160px),超出部分垂直滚动
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: networkInterfaces.length > 0 ? 40 + networkInterfaces.length * 28 : 80 // 高度 = 上下边距 10*2 + 列表可视高度;可视高度 = min(内容高度, 160)
color: "#ffffff" // 内容高度 = 行高 30*n + 行间距 2*(n-1),接口少时卡片随行数收缩
Layout.preferredHeight: networkInterfaces.length > 0
? Math.min(ifaceColumn.implicitHeight, 160) + 20
: 80
color: root.cardBg
radius: 10 radius: 10
border.width: 1 border.width: 1
border.color: "#e0e0e0" border.color: root.lineColor
ColumnLayout { // 用 ScrollView 而非裸 Flickable:自带滚轮滚动支持,
// 且滚动条显示时自动让出内容宽度,不会遮挡行内文字
ScrollView {
id: ifaceScrollView
anchors.fill: parent anchors.fill: parent
anchors.margins: 12 anchors.margins: 10
spacing: 8 visible: networkInterfaces.length > 0
clip: true
// 内容宽度跟随可视宽度(自动减去滚动条占位),禁止水平滚动
contentWidth: availableWidth
// 接口列表:RadioButton 单选切换活动接口 // 自定义滚动条样式:与 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 { Repeater {
model: networkInterfaces model: networkInterfaces
delegate: Rectangle { delegate: Rectangle {
Layout.fillWidth: true id: ifaceRow
Layout.preferredHeight: 28 width: ifaceColumn.width
color: "transparent" height: 30
radius: 6 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"
RadioButton { // 整行 hover + 点击:行内元素均不处理鼠标事件,
anchors.left: parent.left // 点击行任意位置即选中该接口
anchors.verticalCenter: parent.verticalCenter MouseArea {
text: modelData id: rowMouse
checked: modelData === activeInterface anchors.fill: parent
onToggled: { hoverEnabled: true
if (applet) applet.setActiveInterface(modelData) 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 { Text {
anchors.right: parent.right Layout.alignment: Qt.AlignVCenter
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: interfaceDescription(modelData) text: interfaceDescription(modelData)
font.pixelSize: 12 font.pixelSize: 12
color: "#999999" color: root.textTertiary
}
}
}
} }
} }
} }
// 无接口提示 // 无接口提示:在卡片内居中
Text { Text {
text: qsTr("未检测到网络接口") anchors.centerIn: parent
text: qsTr("No network interface detected")
font.pixelSize: 12 font.pixelSize: 12
color: "#999999" color: root.textTertiary
visible: networkInterfaces.length === 0 visible: networkInterfaces.length === 0
Layout.alignment: Qt.AlignHCenter
}
} }
} }
@@ -219,15 +357,17 @@ Window {
// 设计原因:用户选择的颜色通过 applet.textColor 持久化到 // 设计原因:用户选择的颜色通过 applet.textColor 持久化到
// ~/.config/jnetapplet/settings.ini,重启 dde-shell 后仍生效 // ~/.config/jnetapplet/settings.ini,重启 dde-shell 后仍生效
Text { Text {
text: qsTr("字体颜色") text: qsTr("Font Color")
font.pixelSize: 13 font.pixelSize: 13
font.weight: Font.Bold font.weight: Font.Bold
color: "#666666" color: root.textSecondary
} }
TextColorPicker { TextColorPicker {
Layout.fillWidth: true Layout.fillWidth: true
currentColor: applet ? applet.textColor : "" currentColor: applet ? applet.textColor : ""
// 深色模式标记向下传递:TextColorPicker 内部色板卡片同样主题感知
isDarkMode: root.isDarkMode
onColorSelected: if (applet) applet.textColor = color onColorSelected: if (applet) applet.textColor = color
} }
@@ -240,7 +380,7 @@ Window {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 40 Layout.preferredHeight: 40
// 提示状态时背景和边框变绿 // 提示状态时背景和边框变绿
readonly property bool isStatus: uninstallButtonText !== qsTr("卸载插件") readonly property bool isStatus: uninstallButtonText !== qsTr("Uninstall Plugin")
color: isStatus color: isStatus
? Qt.rgba(22/255, 163/255, 74/255, 0.08) ? 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)) : (uninstallMouse.containsMouse ? Qt.rgba(220/255, 38/255, 38/255, 0.15) : Qt.rgba(220/255, 38/255, 38/255, 0.08))
@@ -291,10 +431,10 @@ Window {
y: (root.height - height) / 2 y: (root.height - height) / 2
background: Rectangle { background: Rectangle {
color: "#f5f5f5" color: root.winBg
radius: 12 radius: 12
border.width: 1 border.width: 1
border.color: "#e0e0e0" border.color: root.lineColor
} }
contentItem: ColumnLayout { contentItem: ColumnLayout {
@@ -303,7 +443,7 @@ Window {
anchors.margins: 20 anchors.margins: 20
Text { Text {
text: qsTr("警告") text: qsTr("Warning")
font.pixelSize: 16 font.pixelSize: 16
font.weight: Font.Bold font.weight: Font.Bold
color: accentColor color: accentColor
@@ -311,9 +451,9 @@ Window {
} }
Text { Text {
text: qsTr("以下命令用于卸载插件并重启 dde-shell,请复制到终端中执行:") text: qsTr("The following command uninstalls the plugin and restarts dde-shell. Please copy and run it in terminal:")
font.pixelSize: 12 font.pixelSize: 12
color: "#333333" color: root.textPrimary
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -324,7 +464,7 @@ Window {
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 60 Layout.preferredHeight: 60
color: "#e8e8e8" color: root.codeBg
radius: 8 radius: 8
Text { Text {
@@ -333,7 +473,7 @@ Window {
text: "sudo rm -rf /usr/share/dde-shell/space.jokul.JNetApplet/ && systemctl --user restart dde-shell@DDE" text: "sudo rm -rf /usr/share/dde-shell/space.jokul.JNetApplet/ && systemctl --user restart dde-shell@DDE"
font.pixelSize: 10 font.pixelSize: 10
font.family: "monospace" font.family: "monospace"
color: "#666666" color: root.textSecondary
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
} }
@@ -349,16 +489,16 @@ Window {
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 36 Layout.preferredHeight: 36
color: cancelUninstallMouse.containsMouse ? "#f0f0f0" : "#ffffff" color: cancelUninstallMouse.containsMouse ? root.hoverBg : root.cardBg
radius: 8 radius: 8
border.width: 1 border.width: 1
border.color: "#e0e0e0" border.color: root.lineColor
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: qsTr("取消") text: qsTr("Cancel")
font.pixelSize: 13 font.pixelSize: 13
color: "#333333" color: root.textPrimary
} }
MouseArea { MouseArea {
@@ -379,7 +519,7 @@ Window {
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: qsTr("复制命令") text: qsTr("Copy Command")
font.pixelSize: 13 font.pixelSize: 13
font.weight: Font.Medium font.weight: Font.Medium
color: "white" color: "white"
@@ -396,7 +536,7 @@ Window {
clipboardHelper.copy() clipboardHelper.copy()
uninstallConfirmDialog.close() uninstallConfirmDialog.close()
// 卸载按钮文字临时替换为复制成功提示,5 秒后还原 // 卸载按钮文字临时替换为复制成功提示,5 秒后还原
uninstallButtonText = qsTr("卸载命令已复制到剪贴板,请在终端中粘贴执行") uninstallButtonText = qsTr("Uninstall command copied to clipboard. Please paste and run it in terminal.")
statusHideTimer.start() statusHideTimer.start()
} }
} }
@@ -417,6 +557,6 @@ Window {
Timer { Timer {
id: statusHideTimer id: statusHideTimer
interval: 5000 interval: 5000
onTriggered: uninstallButtonText = qsTr("卸载插件") onTriggered: uninstallButtonText = qsTr("Uninstall Plugin")
} }
} }
+31 -15
View File
@@ -1,11 +1,12 @@
// SPDX-FileCopyrightText: 2024 MyCompany // SPDX-FileCopyrightText: 2026 Jokul
// //
// SPDX-License-Identifier: LGPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later
// 字体颜色选择器:用于设置窗口,提供预设色板与"跟随系统"选项 // 字体颜色选择器:用于设置窗口,提供预设色板与"跟随系统"选项
// 设计要点:白色圆角卡片与设置窗口其他分区视觉一致;圆形色块 22x22, // 设计要点:圆角卡片与设置窗口其他分区视觉一致;圆形色块 22x22,
// 当前选中色加 2px 深色边框高亮;点击只发射 colorSelected 信号, // 当前选中色加 2px 边框高亮(颜色随主题切换);点击只发射 colorSelected 信号,
// 由父层(networkview.qml)回写 currentColor 并同步到 C++ 后端持久化 // 由父层(SettingsWindow)回写 currentColor 并同步到 C++ 后端持久化
// 深/浅主题由 isDarkMode 切换(SettingsWindow 传入),默认浅色保证独立预览可用
// 属性语义: // 属性语义:
// currentColor - 当前选中颜色字符串,空串表示"跟随系统"(默认) // currentColor - 当前选中颜色字符串,空串表示"跟随系统"(默认)
// colorSelected(string) - 用户点击色块时发射,参数为空串或 #RRGGBB // colorSelected(string) - 用户点击色块时发射,参数为空串或 #RRGGBB
@@ -27,12 +28,25 @@ Rectangle {
"#16A34A", "#1450A0", "#7C3AED", "#0891B2" "#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.fillWidth: true
Layout.preferredHeight: 52 Layout.preferredHeight: 52
color: "#ffffff" color: root.cardBg
radius: 10 radius: 10
border.width: 1 border.width: 1
border.color: "#e0e0e0" border.color: root.lineColor
// Canvas 不随属性绑定自动重绘,主题切换时主动触发"跟随系统"斜线重绘
onIsDarkModeChanged: slashCanvas.requestPaint()
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
@@ -47,19 +61,20 @@ Rectangle {
height: 26 height: 26
radius: 13 radius: 13
color: "transparent" color: "transparent"
// 当前选中态:2px 深色边框 // 当前选中态:2px 边框高亮(颜色随主题切换)
border.width: root.currentColor === modelData ? 2 : 0 border.width: root.currentColor === modelData ? 2 : 0
border.color: "#333333" border.color: root.ringColor
Rectangle { Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
width: 22 width: 22
height: 22 height: 22
radius: 11 radius: 11
// 白色色块在白卡上需细边框可见 // 白色色块在白卡上需细边框可见;深色模式下深色卡片上的黑色色块同理
color: modelData color: modelData
border.width: modelData === "#FFFFFF" ? 1 : 0 border.width: (modelData === "#FFFFFF"
border.color: "#e0e0e0" || (root.isDarkMode && modelData === "#000000")) ? 1 : 0
border.color: root.lineColor
} }
MouseArea { MouseArea {
@@ -78,24 +93,25 @@ Rectangle {
color: "transparent" color: "transparent"
// currentColor 为空串时高亮(跟随系统选中态) // currentColor 为空串时高亮(跟随系统选中态)
border.width: root.currentColor === "" ? 2 : 0 border.width: root.currentColor === "" ? 2 : 0
border.color: "#333333" border.color: root.ringColor
Rectangle { Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
width: 22 width: 22
height: 22 height: 22
radius: 11 radius: 11
color: "#f5f5f5" color: root.systemSwatchBg
border.width: 1 border.width: 1
border.color: "#e0e0e0" border.color: root.lineColor
// 斜线:表示"无自定义色/跟随系统" // 斜线:表示"无自定义色/跟随系统"
Canvas { Canvas {
id: slashCanvas
anchors.fill: parent anchors.fill: parent
onPaint: { onPaint: {
var ctx = getContext("2d") var ctx = getContext("2d")
ctx.reset() ctx.reset()
ctx.strokeStyle = "#999999" ctx.strokeStyle = root.isDarkMode ? "#888888" : "#999999"
ctx.lineWidth = 2 ctx.lineWidth = 2
ctx.beginPath() ctx.beginPath()
ctx.moveTo(2, height - 2) ctx.moveTo(2, height - 2)
+70 -43
View File
@@ -1,23 +1,30 @@
// SPDX-FileCopyrightText: 2024 MyCompany // SPDX-FileCopyrightText: 2026 Jokul
// //
// SPDX-License-Identifier: LGPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later
// 流量波动图窗口:屏幕居中的独立顶层窗口,展示当前活动接口最近 5 分钟的 // 流量波动图窗口:屏幕居中的独立顶层窗口,展示当前活动接口最近 5 分钟的
// 网速趋势(下载/上传双折线图),随 C++ 后端 speedHistoryChanged 信号每秒动态刷新 // 网速趋势(下载/上传双折线图),随 C++ 后端 speedHistoryChanged 信号每秒动态刷新
// 设计要点: // 设计要点:
// - 视觉风格复用 AboutWindow.qml白色圆角卡片 12px、1px #E8E8E8 边框、 // - 视觉风格复用 AboutWindow.qml(圆角卡片 12px、1px 边框、44px 标题栏
// 44px 标题栏、28x28 圆形关闭按钮 hover 淡红底) // 28x28 圆形关闭按钮 hover 淡红底);深/浅主题由 isDarkMode 切换
// - 图表使用纯 QML Canvas 绘制,无 QtCharts 等外部依赖(与项目零依赖风格一致) // networkview.qml 依据 DockPalette 检测任务栏深浅后传入),默认浅色保证独立预览可用
// - 图表使用纯 QML Canvas 绘制,无 QtCharts 等外部依赖(与项目零依赖风格一致);
// Canvas 不随属性绑定自动重绘,isDarkMode 变化时主动 requestPaint()
// - 数据通过属性注入:applet 即 networkview.qml 的 root.appletC++ 后端对象), // - 数据通过属性注入:applet 即 networkview.qml 的 root.appletC++ 后端对象),
// 为 null 时组件可独立预览(Canvas 显示"暂无历史数据"空状态) // 为 null 时组件可独立预览(Canvas 显示"暂无历史数据"空状态)
// 触发方式:右键菜单"流量波动图" -> show()/raise()/requestActivate() // 触发方式:右键菜单"流量波动图" -> show()/raise()/requestActivate()
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import QtQuick.Window 2.15 import QtQuick.Window 2.15
import "."
Window { Window {
id: root id: root
// 公共格式化函数:集中定义于同目录 NetCommon.qml
// 与 networkview.qml、NetworkPopup 共享同一份实现,消除跨组件重复定义
NetCommon { id: common }
// 对外依赖:关闭按钮 hover 态文字高亮色,由父组件传入 // 对外依赖:关闭按钮 hover 态文字高亮色,由父组件传入
// 默认值与 networkview.qml 中 root.accentRed 一致,确保独立可用 // 默认值与 networkview.qml 中 root.accentRed 一致,确保独立可用
property color accentColor: Qt.rgba(220 / 255, 38 / 255, 38 / 255, 1) property color accentColor: Qt.rgba(220 / 255, 38 / 255, 38 / 255, 1)
@@ -28,6 +35,27 @@ Window {
// 为 null 时组件可独立预览(显示空状态),所有数据访问处均需做空值兜底 // 为 null 时组件可独立预览(显示空状态),所有数据访问处均需做空值兜底
property var applet: 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 表示无悬停 // Hover 状态:当前悬停采样点在历史缓冲中的索引,-1 表示无悬停
// 由图表区 MouseArea 根据 mouseX 反查最近采样点更新;Canvas 据此绘制 // 由图表区 MouseArea 根据 mouseX 反查最近采样点更新;Canvas 据此绘制
// 竖直辅助线与圆点标记,底部提示条据此显示该时刻数值 // 竖直辅助线与圆点标记,底部提示条据此显示该时刻数值
@@ -44,6 +72,12 @@ Window {
// 置顶状态变化时重绘图钉图标(未置顶=灰色轮廓,置顶=蓝色填充) // 置顶状态变化时重绘图钉图标(未置顶=灰色轮廓,置顶=蓝色填充)
onPinnedChanged: pinIcon.requestPaint() onPinnedChanged: pinIcon.requestPaint()
// 主题切换时重绘图表与图钉图标:Canvas 不随属性绑定自动重绘,需主动触发
onIsDarkModeChanged: {
pinIcon.requestPaint()
canvas.requestPaint()
}
// 下载折线颜色(绿)与上传折线颜色(橙),与规范 §4.4 一致 // 下载折线颜色(绿)与上传折线颜色(橙),与规范 §4.4 一致
readonly property color downloadLineColor: "#34C759" readonly property color downloadLineColor: "#34C759"
readonly property color uploadLineColor: "#FF9500" readonly property color uploadLineColor: "#FF9500"
@@ -66,22 +100,15 @@ Window {
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
x = (Screen.width - width) / 2 // 居中到当前屏幕(任务栏所在屏幕),加 virtualX/virtualY 偏移
y = (Screen.height - height) / 2 // 避免多显示器下窗口出现在非预期屏幕
x = Screen.virtualX + (Screen.width - width) / 2
y = Screen.virtualY + (Screen.height - height) / 2
} }
} }
// 格式化速度显示(带单位,最小 KB,保留 2 位小数) // 格式化速度显示:统一调用 common.formatSpeed(带单位,最小 KB,保留 2 位小数)
// 与 networkview.qml 的 formatSpeed 同实现;组件内自带一份保证独立可用 // 与 networkview.qml / NetworkPopup 共用 NetCommon 中的同一份实现
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"
}
}
// Y 轴上限取整:将 v 向上取整到 1/2/5 × 10^n 序列 // Y 轴上限取整:将 v 向上取整到 1/2/5 × 10^n 序列
// 设计原因:使刻度值为易读的整数(如 12345 -> 2000080000 -> 100000), // 设计原因:使刻度值为易读的整数(如 12345 -> 2000080000 -> 100000),
@@ -118,13 +145,13 @@ Window {
return s + " " + unit.suffix return s + " " + unit.suffix
} }
// 窗口主体:白色圆角卡片,1px 浅灰边框模拟 DTK 窗口描边(与 AboutWindow 一致) // 窗口主体:圆角卡片(背景与边框随 isDarkMode 切换深浅),1px 边框模拟 DTK 窗口描边(与 AboutWindow 一致)
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: "#FFFFFF" color: root.winBg
radius: 12 radius: 12
border.width: 1 border.width: 1
border.color: "#E8E8E8" border.color: root.lineColor
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
@@ -150,10 +177,10 @@ Window {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 16 anchors.leftMargin: 16
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: qsTr("流量波动图") text: qsTr("Traffic Chart")
font.pixelSize: 15 font.pixelSize: 15
font.weight: Font.Bold font.weight: Font.Bold
color: "#333333" color: root.textPrimary
} }
// 置顶按钮:28x28 圆形(与关闭按钮一致),位于关闭按钮左侧 // 置顶按钮:28x28 圆形(与关闭按钮一致),位于关闭按钮左侧
@@ -167,7 +194,7 @@ Window {
height: 28 height: 28
radius: 14 radius: 14
color: root.pinned ? Qt.rgba(0, 129 / 255, 1, 0.12) color: root.pinned ? Qt.rgba(0, 129 / 255, 1, 0.12)
: (pinMouse.containsMouse ? Qt.rgba(0, 0, 0, 0.06) : "transparent") : (pinMouse.containsMouse ? root.pinHoverBg : "transparent")
// 置顶图标:Canvas 绘制"上箭头触顶"(⤒ 风格)——向上箭头指向顶部横杠, // 置顶图标:Canvas 绘制"上箭头触顶"(⤒ 风格)——向上箭头指向顶部横杠,
// 是"置顶/移到顶部"最通用的视觉语言,与关闭按钮"×"形态区分明显 // 是"置顶/移到顶部"最通用的视觉语言,与关闭按钮"×"形态区分明显
@@ -181,7 +208,7 @@ Window {
onPaint: { onPaint: {
var ctx = getContext("2d") var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height) ctx.clearRect(0, 0, width, height)
var c = root.pinned ? "#0081FF" : "#999999" var c = root.pinned ? "#0081FF" : root.axisTextColor
ctx.strokeStyle = c ctx.strokeStyle = c
ctx.fillStyle = c ctx.fillStyle = c
ctx.lineCap = "round" ctx.lineCap = "round"
@@ -246,7 +273,7 @@ Window {
text: "×" text: "×"
font.pixelSize: 20 font.pixelSize: 20
font.weight: Font.Bold font.weight: Font.Bold
color: chartCloseMouse.containsMouse ? root.accentColor : "#666666" color: chartCloseMouse.containsMouse ? root.accentColor : root.textSecondary
} }
MouseArea { MouseArea {
@@ -277,9 +304,9 @@ Window {
Text { Text {
// applet 为空时兜底为 0,保证独立预览不报错 // applet 为空时兜底为 0,保证独立预览不报错
text: root.formatSpeed(root.applet ? root.applet.downloadSpeed : 0) text: common.formatSpeed(root.applet ? root.applet.downloadSpeed : 0)
font.pixelSize: 12 font.pixelSize: 12
color: "#333333" color: root.textPrimary
} }
Item { Layout.preferredWidth: 10 } Item { Layout.preferredWidth: 10 }
@@ -292,9 +319,9 @@ Window {
} }
Text { Text {
text: root.formatSpeed(root.applet ? root.applet.uploadSpeed : 0) text: common.formatSpeed(root.applet ? root.applet.uploadSpeed : 0)
font.pixelSize: 12 font.pixelSize: 12
color: "#333333" color: root.textPrimary
} }
Item { Layout.fillWidth: true } Item { Layout.fillWidth: true }
@@ -303,7 +330,7 @@ Window {
text: (root.applet && root.applet.activeInterface text: (root.applet && root.applet.activeInterface
? root.applet.activeInterface : "—") + " · 5min" ? root.applet.activeInterface : "—") + " · 5min"
font.pixelSize: 12 font.pixelSize: 12
color: "#999999" color: root.textTertiary
} }
} }
@@ -362,15 +389,15 @@ Window {
for (g = 0; g <= 4; g++) { for (g = 0; g <= 4; g++) {
gy = pt + ph - (ph * g / 4) gy = pt + ph - (ph * g / 4)
// +0.5 让 1px 线条落在像素边界上,避免抗锯齿发虚 // +0.5 让 1px 线条落在像素边界上,避免抗锯齿发虚
// 基线(g=0)略深一档,其余 4 条网格线用规范色 #E5E5E5 // 基线(g=0)略深一档;网格与刻度颜色均随 isDarkMode 切换深浅
ctx.strokeStyle = g === 0 ? "#DDDDDD" : "#E5E5E5" ctx.strokeStyle = g === 0 ? root.gridBaselineColor : root.gridLineColor
ctx.lineWidth = 1 ctx.lineWidth = 1
ctx.beginPath() ctx.beginPath()
ctx.moveTo(pl, gy + 0.5) ctx.moveTo(pl, gy + 0.5)
ctx.lineTo(pl + pw, gy + 0.5) ctx.lineTo(pl + pw, gy + 0.5)
ctx.stroke() ctx.stroke()
// 刻度标签右对齐于绘图区左侧 // 刻度标签右对齐于绘图区左侧
ctx.fillStyle = "#999999" ctx.fillStyle = root.axisTextColor
ctx.textAlign = "right" ctx.textAlign = "right"
ctx.fillText(root.formatAxisValue(yMax * g / 4, unit), pl - 8, gy) ctx.fillText(root.formatAxisValue(yMax * g / 4, unit), pl - 8, gy)
} }
@@ -379,7 +406,7 @@ Window {
// 5 分钟窗口用 -5m/-4m/-3m/-2m/-1m/now 共 6 个标签,比 4 个三分点 // 5 分钟窗口用 -5m/-4m/-3m/-2m/-1m/now 共 6 个标签,比 4 个三分点
// 的 -3m20s/-1m40s 更易读;循环按数组长度参数化,两端左/右对齐 // 的 -3m20s/-1m40s 更易读;循环按数组长度参数化,两端左/右对齐
var xLabels = ["-5m", "-4m", "-3m", "-2m", "-1m", "now"] var xLabels = ["-5m", "-4m", "-3m", "-2m", "-1m", "now"]
ctx.fillStyle = "#999999" ctx.fillStyle = root.axisTextColor
ctx.textBaseline = "alphabetic" ctx.textBaseline = "alphabetic"
for (i = 0; i < xLabels.length; i++) { for (i = 0; i < xLabels.length; i++) {
var lx = pl + pw * i / (xLabels.length - 1) var lx = pl + pw * i / (xLabels.length - 1)
@@ -390,11 +417,11 @@ Window {
// ---- 空状态:画完空网格后中央提示,不画折线(规范 §5)---- // ---- 空状态:画完空网格后中央提示,不画折线(规范 §5)----
if (n === 0) { if (n === 0) {
ctx.fillStyle = "#999999" ctx.fillStyle = root.axisTextColor
ctx.font = "13px sans-serif" ctx.font = "13px sans-serif"
ctx.textAlign = "center" ctx.textAlign = "center"
ctx.textBaseline = "middle" ctx.textBaseline = "middle"
ctx.fillText(qsTr("暂无历史数据"), pl + pw / 2, pt + ph / 2) ctx.fillText(qsTr("No history data"), pl + pw / 2, pt + ph / 2)
return return
} }
@@ -466,7 +493,7 @@ Window {
if (hi >= 0 && hi < n) { if (hi >= 0 && hi < n) {
var hx = pl + ((dl[hi].x - tMin) / tRange) * pw var hx = pl + ((dl[hi].x - tMin) / tRange) * pw
// 竖直辅助线:虚线,避免与实线折线混淆 // 竖直辅助线:虚线,避免与实线折线混淆
ctx.strokeStyle = "#BBBBBB" ctx.strokeStyle = root.hoverLineColor
ctx.lineWidth = 1 ctx.lineWidth = 1
ctx.setLineDash([4, 3]) ctx.setLineDash([4, 3])
ctx.beginPath() ctx.beginPath()
@@ -474,12 +501,12 @@ Window {
ctx.lineTo(hx, pt + ph) ctx.lineTo(hx, pt + ph)
ctx.stroke() ctx.stroke()
ctx.setLineDash([]) ctx.setLineDash([])
// 圆点标记:芯 + 2px 彩色描边,在折线上形成清晰锚点 // 圆点标记:窗口底色芯 + 2px 彩色描边,在折线上形成清晰锚点
function drawDot(yVal, colorCss) { function drawDot(yVal, colorCss) {
var hy = pt + ph - (yVal / yMax) * ph var hy = pt + ph - (yVal / yMax) * ph
ctx.beginPath() ctx.beginPath()
ctx.arc(hx, hy, 4, 0, 2 * Math.PI) ctx.arc(hx, hy, 4, 0, 2 * Math.PI)
ctx.fillStyle = "#FFFFFF" ctx.fillStyle = root.dotCoreColor
ctx.fill() ctx.fill()
ctx.strokeStyle = colorCss ctx.strokeStyle = colorCss
ctx.lineWidth = 2 ctx.lineWidth = 2
@@ -529,8 +556,8 @@ Window {
var idx = root.hoverIndex var idx = root.hoverIndex
var upVal = idx < ul.length ? ul[idx].y : 0 var upVal = idx < ul.length ? ul[idx].y : 0
root.hoverHint = Qt.formatDateTime(new Date(dl[idx].x * 1000), "HH:mm:ss") root.hoverHint = Qt.formatDateTime(new Date(dl[idx].x * 1000), "HH:mm:ss")
+ " ↓" + root.formatSpeed(dl[idx].y) + " ↓" + common.formatSpeed(dl[idx].y)
+ " ↑" + root.formatSpeed(upVal) + " ↑" + common.formatSpeed(upVal)
canvas.requestPaint() canvas.requestPaint()
} }
@@ -551,9 +578,9 @@ Window {
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: root.hoverHint.length > 0 text: root.hoverHint.length > 0
? root.hoverHint : qsTr("鼠标移到曲线上查看详情") ? root.hoverHint : qsTr("Hover over the curve for details")
font.pixelSize: 11 font.pixelSize: 11
color: root.hoverHint.length > 0 ? "#333333" : "#999999" color: root.hoverHint.length > 0 ? root.textPrimary : root.textTertiary
} }
} }
} }
+46 -401
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024 MyCompany // SPDX-FileCopyrightText: 2026 Jokul
// //
// SPDX-License-Identifier: LGPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later
@@ -40,31 +40,27 @@ AppletItem {
// 活动接口的 IPv6 全球地址,由 C++ 后端 detectIpAddress 持续更新 // 活动接口的 IPv6 全球地址,由 C++ 后端 detectIpAddress 持续更新
readonly property string ipv6Address: applet ? (applet.ipv6Address || "") : "" readonly property string ipv6Address: applet ? (applet.ipv6Address || "") : ""
readonly property var networkInterfaces: applet ? (applet.networkInterfaces || []) : [] readonly property var networkInterfaces: applet ? (applet.networkInterfaces || []) : []
readonly property var interfaceStats: applet ? (applet.interfaceStats || []) : []
// 判断是否为物理网卡(QML 侧排序用,与 C++ isPhysicalInterface 逻辑一致) // 公共颜色与格式化函数:集中定义于 components/NetCommon.qml(随 import "components" 引入),
function isPhysicalIf(name) { // 与 NetworkPopup、TrafficChartWindow 共享同一份实现,消除跨组件重复定义
return name.startsWith("wlp") || name.startsWith("wlan") NetCommon { id: common }
|| name.startsWith("enp") || name.startsWith("eth")
}
// 排序后的接口列表:活动接口最前,其次物理网卡,最后虚拟网卡 // 排序后的接口列表:物理网卡在前,虚拟网卡在后,各自按名称排序
// 设计原因:用户希望启动监测的网卡在最左边,方便查看和切换 // 设计原因:保持稳定排序,活动接口不再移到最前,避免切换网卡时 chip 顺序跳动;
// 活动 chip 若被截断,由 chipFlickable 自动滚动露出完整样式;
// 物理网卡判断逻辑共用 common.isPhysicalIf(与 C++ isPhysicalInterface 一致)
readonly property var sortedInterfaces: { readonly property var sortedInterfaces: {
if (!root.ready || root.networkInterfaces.length === 0) return [] if (!root.ready || root.networkInterfaces.length === 0) return []
var physical = [] var physical = []
var virtual = [] var virtual = []
for (var i = 0; i < root.networkInterfaces.length; i++) { for (var i = 0; i < root.networkInterfaces.length; i++) {
var name = root.networkInterfaces[i] var name = root.networkInterfaces[i]
if (name === root.activeInterface) continue if (common.isPhysicalIf(name)) physical.push(name)
if (root.isPhysicalIf(name)) physical.push(name)
else virtual.push(name) else virtual.push(name)
} }
physical.sort() physical.sort()
virtual.sort() virtual.sort()
var result = [] return physical.concat(virtual)
if (root.activeInterface) result.push(root.activeInterface)
return result.concat(physical).concat(virtual)
} }
// 紧凑格式化速度(用于任务栏图标和 tooltip,不带单位后缀,节省空间) // 紧凑格式化速度(用于任务栏图标和 tooltip,不带单位后缀,节省空间)
@@ -81,63 +77,25 @@ AppletItem {
} }
} }
// 带单位的速度/总量格式化函数(formatSpeed / formatTotal)已移至 NetCommon
// 本文件未直接使用;弹窗与图表窗口经 common.formatSpeed / common.formatTotal 调用
// 格式化速度显示(带单位,用于弹出面板,信息更完整) // 颜色别名:转发 common 的公共颜色,保持下游 root.xxx 引用不变。
// 最小单位为 KB,与 formatSpeedShort 保持一致;所有级别保留 2 位小数 // 仅保留本文件实际使用的两个(primaryText 供 speedTextColor 回退、
function formatSpeed(bytesPerSec) { // accentRed 传入各独立窗口);其余公共颜色本文件未用,不再重复定义。
if (bytesPerSec < 1024 * 1024) { // 颜色派生逻辑与主题适配的设计原因见 NetCommon.qml 文件头注释
return (bytesPerSec / 1024).toFixed(2) + " KB/s" readonly property color primaryText: common.primaryText
} else if (bytesPerSec < 1024 * 1024 * 1024) { readonly property color accentRed: common.accentRed
return (bytesPerSec / (1024 * 1024)).toFixed(2) + " MB/s"
} else {
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(2) + " GB/s"
}
}
// 格式化总量显示(用于弹出面板的累计统计) // 深色模式检测:用 DTK 系统调色板的窗口背景亮度判定,而非 DockPalette。
function formatTotal(bytes) { // 设计原因:DockPalette.iconTextPalette 反映的是任务栏图标文字色,不随系统深色主题变化;
if (bytes < 1024) { // DTK.palette.window 是系统主题的窗口背景色,深色模式下为深色(hslLightness < 0.5),
return bytes.toFixed(0) + " B" // 浅色模式下为浅色(hslLightness >= 0.5)。主题切换时 DTK.paletteChanged 自动触发重新求值。
} else if (bytes < 1024 * 1024) { // 检测结果通过属性注入各独立窗口(AboutWindow 等独立 Window 无法访问 dock 上下文的 DockPalette
return (bytes / 1024).toFixed(1) + " KB" readonly property bool isDarkMode: DTK.palette.window.hslLightness < 0.5
} else if (bytes < 1024 * 1024 * 1024) {
return (bytes / (1024 * 1024)).toFixed(1) + " MB"
} else {
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB"
}
}
// 任务栏与面板的颜色全部派生自 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)
// 强调色:下载蓝、上传绿,是面板与任务栏的主视觉区分
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 派生自 DockPalette,深浅色任务栏自动适配; // 设计原因:primaryText 派生自 DTK.palette.windowText,深浅色系统主题自动适配;
// 用户主动选色后覆盖,空串回退到 primaryText 保持自适应 // 用户主动选色后覆盖,空串回退到 primaryText 保持自适应
readonly property color speedTextColor: (applet && applet.textColor.length > 0) ? applet.textColor : primaryText readonly property color speedTextColor: (applet && applet.textColor.length > 0) ? applet.textColor : primaryText
@@ -264,9 +222,9 @@ AppletItem {
id: toolTip id: toolTip
text: root.ready text: root.ready
? (root.activeInterface ? (root.activeInterface
+ " · IPv4" + (root.ipAddress || qsTr("")) + " · " + qsTr("IPv4: ") + (root.ipAddress || qsTr("None"))
+ (root.ipv6Address ? "\nIPv6" + root.ipv6Address : "")) + (root.ipv6Address ? "\n" + qsTr("IPv6: ") + root.ipv6Address : ""))
: qsTr("未检测到网络接口") : qsTr("No network interface detected")
toolTipX: DockPanelPositioner.x toolTipX: DockPanelPositioner.x
toolTipY: DockPanelPositioner.y toolTipY: DockPanelPositioner.y
} }
@@ -275,8 +233,7 @@ AppletItem {
// C++ 后端 NetworkMonitorApplet::init() 已启动 1 秒间隔的 m_refreshTimer // C++ 后端 NetworkMonitorApplet::init() 已启动 1 秒间隔的 m_refreshTimer
// 持续调用 refresh()->readNetworkStats()->calculateSpeed() 更新速度属性。 // 持续调用 refresh()->readNetworkStats()->calculateSpeed() 更新速度属性。
// QML 通过属性绑定自动获取最新值,无需主动触发 refresh。 // QML 通过属性绑定自动获取最新值,无需主动触发 refresh。
// 此前 QML 额外调用 applet.refresh() 会打破 1 秒定时间隔 // refresh() 已移除 Q_INVOKABLE 标记,防止 QML 误调用打破定时间隔
// 导致 calculateSpeed() 基于不固定间隔计算字节差,速度显示接近 0。
Timer { Timer {
id: toolTipShowTimer id: toolTipShowTimer
@@ -303,7 +260,8 @@ AppletItem {
} }
} }
// 弹出窗口 // 弹出窗口PanelPopup 壳在此处(dock 上下文类型不能作为独立组件根元素),
// 弹窗内容由 NetworkPopup.qml 提供,传入 applet,内部自行派生颜色和数据
PanelPopup { PanelPopup {
id: networkPopup id: networkPopup
width: 360 width: 360
@@ -312,335 +270,17 @@ AppletItem {
popupY: DockPanelPositioner.y popupY: DockPanelPositioner.y
onPopupVisibleChanged: { onPopupVisibleChanged: {
// 仅控制 tooltip 关闭,不触发 refresh。
// C++ 后端 m_refreshTimer 持续更新,popup 通过属性绑定自动显示最新数据。
if (popupVisible) { if (popupVisible) {
toolTip.close() toolTip.close()
// popup 打开时确保活动 chip 完整可见
Qt.callLater(function() { popupContent.scrollToActiveChip(false) })
} }
} }
Control { NetworkPopup {
id: popupContainer id: popupContent
applet: root.applet
anchors.fill: parent 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: { Component.onCompleted: {
@@ -660,7 +300,7 @@ AppletItem {
id: contextMenu id: contextMenu
Platform.MenuItem { Platform.MenuItem {
text: qsTr("设置") text: qsTr("Settings")
onTriggered: { onTriggered: {
settingsWindow.show() settingsWindow.show()
settingsWindow.raise() settingsWindow.raise()
@@ -670,7 +310,7 @@ AppletItem {
// 流量波动图:屏幕居中独立窗口,展示活动接口最近 30 分钟网速趋势 // 流量波动图:屏幕居中独立窗口,展示活动接口最近 30 分钟网速趋势
Platform.MenuItem { Platform.MenuItem {
text: qsTr("流量波动图") text: qsTr("Traffic Chart")
onTriggered: { onTriggered: {
trafficChartWindow.show() trafficChartWindow.show()
trafficChartWindow.raise() trafficChartWindow.raise()
@@ -681,7 +321,7 @@ AppletItem {
Platform.MenuSeparator {} Platform.MenuSeparator {}
Platform.MenuItem { Platform.MenuItem {
text: qsTr("关于") text: qsTr("About")
onTriggered: { onTriggered: {
aboutWindow.show() aboutWindow.show()
aboutWindow.raise() aboutWindow.raise()
@@ -691,19 +331,23 @@ AppletItem {
} }
// 关于窗口:抽取为独立组件 package/components/AboutWindow.qml // 关于窗口:抽取为独立组件 package/components/AboutWindow.qml
// 依赖通过属性传入:accentColor = root.accentRedversion 取自 C++ 后端 applet.version // 依赖通过属性传入:accentColor = root.accentRedversion 取自 C++ 后端 applet.version
// isDarkMode 取自上方 DockPalette 检测结果
AboutWindow { AboutWindow {
id: aboutWindow id: aboutWindow
accentColor: root.accentRed accentColor: root.accentRed
version: root.applet ? root.applet.version : "1.0" version: root.applet ? root.applet.version : "1.0"
isDarkMode: root.isDarkMode
} }
// 流量波动图窗口:屏幕居中独立窗口,展示活动接口最近 30 分钟网速趋势 // 流量波动图窗口:屏幕居中独立窗口,展示活动接口最近 30 分钟网速趋势
// 依赖通过属性传入:accentColor = root.accentRedapplet = root.applet // 依赖通过属性传入:accentColor = root.accentRedapplet = root.applet
// isDarkMode 取自上方 DockPalette 检测结果
TrafficChartWindow { TrafficChartWindow {
id: trafficChartWindow id: trafficChartWindow
accentColor: root.accentRed accentColor: root.accentRed
applet: root.applet applet: root.applet
isDarkMode: root.isDarkMode
} }
// 设置窗口:独立顶层窗口,在桌面中间弹出 // 设置窗口:独立顶层窗口,在桌面中间弹出
@@ -715,6 +359,7 @@ AppletItem {
networkInterfaces: root.networkInterfaces networkInterfaces: root.networkInterfaces
activeInterface: root.activeInterface activeInterface: root.activeInterface
accentColor: root.accentRed accentColor: root.accentRed
isDarkMode: root.isDarkMode
} }
// 点击处理 // 点击处理
+107 -46
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024 MyCompany // SPDX-FileCopyrightText: 2026 Jokul
// //
// SPDX-License-Identifier: LGPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later
@@ -14,6 +14,9 @@
#include <QColor> #include <QColor>
#include <QSettings> #include <QSettings>
#include <QStandardPaths> #include <QStandardPaths>
#include <QTranslator>
#include <QLocale>
#include <QCoreApplication>
DS_BEGIN_NAMESPACE DS_BEGIN_NAMESPACE
@@ -22,6 +25,7 @@ NetworkMonitorApplet::NetworkMonitorApplet(QObject *parent)
, m_refreshTimer(nullptr) , m_refreshTimer(nullptr)
, m_lastRxBytes(0) , m_lastRxBytes(0)
, m_lastTxBytes(0) , m_lastTxBytes(0)
, m_lastTimestampMs(0)
, m_downloadSpeed(0) , m_downloadSpeed(0)
, m_uploadSpeed(0) , m_uploadSpeed(0)
, m_totalDownload(0) , m_totalDownload(0)
@@ -44,6 +48,13 @@ NetworkMonitorApplet::NetworkMonitorApplet(QObject *parent)
} }
// 若校验失败则保持空串默认值(跟随系统),不抛出也不记录 // 若校验失败则保持空串默认值(跟随系统),不抛出也不记录
} }
// 从配置文件读取持久化的活动接口,启动时自动恢复用户上次选择
// 设计原因:用户手动选择的网卡应跨重启保持,而非每次启动重新自动检测;
// 若保存的接口已不存在(如 USB 网卡拔出),readNetworkStats 会清空并回退到自动选择
if (settings.contains(QStringLiteral("activeInterface"))) {
m_activeInterface = settings.value(QStringLiteral("activeInterface")).toString();
}
} }
NetworkMonitorApplet::~NetworkMonitorApplet() NetworkMonitorApplet::~NetworkMonitorApplet()
@@ -57,6 +68,17 @@ bool NetworkMonitorApplet::load()
bool NetworkMonitorApplet::init() 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(); detectInterfaces();
// 启动定时刷新 // 启动定时刷新
@@ -72,12 +94,12 @@ bool NetworkMonitorApplet::init()
double NetworkMonitorApplet::downloadSpeed() const double NetworkMonitorApplet::downloadSpeed() const
{ {
return static_cast<double>(m_downloadSpeed); return m_downloadSpeed;
} }
double NetworkMonitorApplet::uploadSpeed() const double NetworkMonitorApplet::uploadSpeed() const
{ {
return static_cast<double>(m_uploadSpeed); return m_uploadSpeed;
} }
double NetworkMonitorApplet::totalDownload() const double NetworkMonitorApplet::totalDownload() const
@@ -95,23 +117,6 @@ QStringList NetworkMonitorApplet::networkInterfaces() const
return m_interfaceList; 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 bool NetworkMonitorApplet::ready() const
{ {
return m_ready; return m_ready;
@@ -142,7 +147,11 @@ QString NetworkMonitorApplet::version() const
// DPluginMetaData::value() 已在 "Plugin" 层级内部查找(源码:frame/pluginmetadata.cpp // DPluginMetaData::value() 已在 "Plugin" 层级内部查找(源码:frame/pluginmetadata.cpp
// metadata.json 的 { "Plugin": { "Version": "x.y.z" } } 被 DPluginMetaData 加载后, // metadata.json 的 { "Plugin": { "Version": "x.y.z" } } 被 DPluginMetaData 加载后,
// value("Version") 直接返回版本号,无需再 .value("Plugin").toMap() 展开 // 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;
} }
// 返回任务栏网速字体颜色,空串表示跟随系统主题 // 返回任务栏网速字体颜色,空串表示跟随系统主题
@@ -219,9 +228,26 @@ void NetworkMonitorApplet::refresh()
void NetworkMonitorApplet::setActiveInterface(const QString &interface) void NetworkMonitorApplet::setActiveInterface(const QString &interface)
{ {
// 校验接口是否存在于当前接口列表中,无效接口名直接忽略
// 设计原因:传入不存在的接口名会被持久化到配置文件,且当前会话期间
// getActiveRxBytes() 返回 0 导致速度恒为 0,用户困惑;
// 虽然下次启动 readNetworkStats() 会清空回退,但当前会话不应接受无效值
if (!m_interfaceList.contains(interface)) {
return;
}
if (m_activeInterface != interface) { if (m_activeInterface != interface) {
m_activeInterface = interface; m_activeInterface = interface;
m_firstUpdate = true; 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(); emit activeInterfaceChanged();
// 接口切换后通知 QML 趋势图切换显示新接口的历史数据 // 接口切换后通知 QML 趋势图切换显示新接口的历史数据
emit speedHistoryChanged(); emit speedHistoryChanged();
@@ -278,15 +304,22 @@ void NetworkMonitorApplet::readNetworkStats()
m_interfaceList = newInterfaces.keys(); m_interfaceList = newInterfaces.keys();
m_interfaces = newInterfaces; m_interfaces = newInterfaces;
// 清理已消失接口的历史缓冲,避免内存泄漏 // 清理已消失接口的历史缓冲和基线数据,避免内存泄漏
// 设计原因:USB 网卡、VPN 隧道等接口可能随时消失,其历史数据不再需要 // 设计原因:USB 网卡、VPN 隧道等接口可能随时消失,其历史数据和基线不再需要
const QStringList oldIfaces = m_speedHistory.keys(); const QStringList oldIfaces = m_speedHistory.keys();
for (const QString &name : oldIfaces) { for (const QString &name : oldIfaces) {
if (!newInterfaces.contains(name)) { if (!newInterfaces.contains(name)) {
m_speedHistory.remove(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()) { if (m_activeInterface.isEmpty() && !m_interfaceList.isEmpty()) {
// 优先选择物理网卡(wlp/wlan/enp/eth)中有流量的接口, // 优先选择物理网卡(wlp/wlan/enp/eth)中有流量的接口,
@@ -324,50 +357,75 @@ void NetworkMonitorApplet::readNetworkStats()
} }
calculateSpeed(); calculateSpeed();
emit statsChanged();
// 每次刷新都检测 IP,以应对 DHCP 续约等 IP 变更场景 // 每次刷新都检测 IP,以应对 DHCP 续约等 IP 变更场景
detectIpAddress(); detectIpAddress();
} }
void NetworkMonitorApplet::calculateSpeed() 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) { if (m_firstUpdate) {
// 首次更新或接口切换后:仅记录基线,不计算速度
m_firstUpdate = false; 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_lastRxBytes = currentRxBytes;
m_lastTxBytes = currentTxBytes; 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; if (!m_lastRxBytesByIface.contains(name)) {
m_totalUpload = currentTxBytes; m_lastRxBytesByIface[name] = iface.rxBytes;
m_lastTxBytesByIface[name] = iface.txBytes;
continue;
}
m_lastRxBytes = currentRxBytes; const qint64 iRxDelta = iface.rxBytes - m_lastRxBytesByIface[name];
m_lastTxBytes = currentTxBytes; const qint64 iTxDelta = iface.txBytes - m_lastTxBytesByIface[name];
// 追加采样点到当前活动接口的环形缓冲
// 设计原因:applet 启动即持续采集,用户随时打开趋势图都能看到完整 5 分钟数据
if (!m_activeInterface.isEmpty()) {
SpeedSample sample; SpeedSample sample;
sample.timestamp = QDateTime::currentSecsSinceEpoch(); sample.timestamp = nowSec;
sample.downloadSpeed = static_cast<double>(m_downloadSpeed); sample.downloadSpeed = (iRxDelta > 0 ? iRxDelta : 0) / elapsedSec;
sample.uploadSpeed = static_cast<double>(m_uploadSpeed); sample.uploadSpeed = (iTxDelta > 0 ? iTxDelta : 0) / elapsedSec;
m_speedHistory[m_activeInterface].append(sample); m_speedHistory[name].append(sample);
// 滑动窗口裁剪:超过 300 点时丢弃最旧的 // 滑动窗口裁剪:超过 300 点时丢弃最旧的
// removeFirst 是 O(n),但 300 点每秒一次开销可忽略,无需复杂环形索引 QVector<SpeedSample> &samples = m_speedHistory[name];
QVector<SpeedSample> &samples = m_speedHistory[m_activeInterface];
while (samples.size() > MAX_HISTORY_SAMPLES) { while (samples.size() > MAX_HISTORY_SAMPLES) {
samples.removeFirst(); samples.removeFirst();
} }
m_lastRxBytesByIface[name] = iface.rxBytes;
m_lastTxBytesByIface[name] = iface.txBytes;
}
emit speedHistoryChanged(); emit speedHistoryChanged();
} }
@@ -384,8 +442,11 @@ void NetworkMonitorApplet::detectInterfaces()
m_interfaceList.clear(); m_interfaceList.clear();
for (const QString &entry : entries) { for (const QString &entry : entries) {
// 过滤回环接口 // 过滤回环接口和虚拟接口,与 readNetworkStats() 保持一致
if (entry != "lo") { // 设计原因:原仅过滤 lodocker/veth/br- 接口会短暂出现在列表中,
// 随后被 readNetworkStats() 覆盖,造成 UI 闪烁
if (entry != "lo" && !entry.startsWith("veth") &&
!entry.startsWith("docker") && !entry.startsWith("br-")) {
m_interfaceList << entry; m_interfaceList << entry;
} }
} }
+15 -7
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024 MyCompany // SPDX-FileCopyrightText: 2026 Jokul
// //
// SPDX-License-Identifier: LGPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later
@@ -49,8 +49,9 @@ class NetworkMonitorApplet : public DApplet
Q_PROPERTY(double totalDownload READ totalDownload NOTIFY totalChanged) Q_PROPERTY(double totalDownload READ totalDownload NOTIFY totalChanged)
Q_PROPERTY(double totalUpload READ totalUpload NOTIFY totalChanged) Q_PROPERTY(double totalUpload READ totalUpload NOTIFY totalChanged)
Q_PROPERTY(QStringList networkInterfaces READ networkInterfaces NOTIFY interfacesChanged) Q_PROPERTY(QStringList networkInterfaces READ networkInterfaces NOTIFY interfacesChanged)
Q_PROPERTY(QStringList interfaceStats READ interfaceStats NOTIFY statsChanged)
Q_PROPERTY(bool ready READ ready NOTIFY readyChanged) Q_PROPERTY(bool ready READ ready NOTIFY readyChanged)
// 当前活动接口,用户在弹窗或设置窗口选择后持久化到 ~/.config/jnetapplet/settings.ini
// 下次启动自动恢复;若保存的接口已不存在则回退到自动选择
Q_PROPERTY(QString activeInterface READ activeInterface NOTIFY activeInterfaceChanged) Q_PROPERTY(QString activeInterface READ activeInterface NOTIFY activeInterfaceChanged)
// 活动接口的 IPv4 地址,供 QML 在弹出面板和 tooltip 中显示 // 活动接口的 IPv4 地址,供 QML 在弹出面板和 tooltip 中显示
Q_PROPERTY(QString ipAddress READ ipAddress NOTIFY ipAddressChanged) Q_PROPERTY(QString ipAddress READ ipAddress NOTIFY ipAddressChanged)
@@ -79,7 +80,6 @@ public:
double totalDownload() const; double totalDownload() const;
double totalUpload() const; double totalUpload() const;
QStringList networkInterfaces() const; QStringList networkInterfaces() const;
QStringList interfaceStats() const;
bool ready() const; bool ready() const;
QString activeInterface() const; QString activeInterface() const;
QString ipAddress() const; QString ipAddress() const;
@@ -95,14 +95,15 @@ public:
// 设置任务栏网速字体颜色,空串表示跟随系统主题 // 设置任务栏网速字体颜色,空串表示跟随系统主题
void setTextColor(const QString &color); void setTextColor(const QString &color);
Q_INVOKABLE void refresh(); // 定时器回调,由 m_refreshTimer 每秒调用。非 Q_INVOKABLEQML 不应直接调用,
// 否则会打破 1 秒定时间隔导致速度计算失真)
void refresh();
Q_INVOKABLE void setActiveInterface(const QString &interface); Q_INVOKABLE void setActiveInterface(const QString &interface);
signals: signals:
void speedChanged(); void speedChanged();
void totalChanged(); void totalChanged();
void interfacesChanged(); void interfacesChanged();
void statsChanged();
void readyChanged(); void readyChanged();
void activeInterfaceChanged(); void activeInterfaceChanged();
void ipAddressChanged(); void ipAddressChanged();
@@ -133,8 +134,15 @@ private:
// 速度计算 // 速度计算
qint64 m_lastRxBytes; qint64 m_lastRxBytes;
qint64 m_lastTxBytes; 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; qint64 m_totalDownload;
+202
View File
@@ -0,0 +1,202 @@
// SPDX-FileCopyrightText: 2026 Jokul
//
// SPDX-License-Identifier: LGPL-3.0-or-later
// NetworkMonitorApplet 单元测试
// 覆盖核心纯逻辑函数:isPhysicalInterface、速度计算(含真实间隔/负值钳制/总量累加)、
// setActiveInterface 接口校验
// 用 #define private public 技巧访问私有成员(friend 声明因 DS_BEGIN_NAMESPACE
// 命名空间与 Q_OBJECT 宏交互而无法正确解析,此为 C++ 测试常用替代方案)
// 必须在 include 头文件之前定义,使 private 成员在测试编译单元中可访问
#define private public
#include "networkmonitorapplet.h"
#undef private
#include <QtTest/QtTest>
#include <QDateTime>
// dde-shell 的 DS 命名空间(DS_BEGIN_NAMESPACE = namespace ds {
DS_USE_NAMESPACE
class NetworkMonitorAppletTest : public QObject
{
Q_OBJECT
private:
// 创建 applet 实例的辅助方法
// 构造时不调用 init()(会启动定时器),仅用于测试纯逻辑方法
NetworkMonitorApplet *createApplet()
{
return new NetworkMonitorApplet();
}
private slots:
// isPhysicalInterface:物理网卡前缀匹配
void testIsPhysicalInterface_data();
void testIsPhysicalInterface();
// 速度计算:按真实时间间隔计算
void testCalculateSpeed();
// 计数器回绕时负值钳制为 0
void testNegativeDeltaClamped();
// 总量累加而非取计数器值
void testTotalAccumulation();
// setActiveInterface 拒绝无效接口名
void testSetActiveInterfaceValidation();
};
void NetworkMonitorAppletTest::testIsPhysicalInterface_data()
{
QTest::addColumn<QString>("name");
QTest::addColumn<bool>("expected");
// 物理网卡
QTest::newRow("wlp3s0") << "wlp3s0" << true;
QTest::newRow("wlan0") << "wlan0" << true;
QTest::newRow("enp3s0") << "enp3s0" << true;
QTest::newRow("eth0") << "eth0" << true;
// 非物理接口
QTest::newRow("lo") << "lo" << false;
QTest::newRow("docker0") << "docker0" << false;
QTest::newRow("veth0") << "veth0" << false;
QTest::newRow("br-xxx") << "br-xxx" << false;
QTest::newRow("tun0") << "tun0" << false;
QTest::newRow("Meta") << "Meta" << false;
QTest::newRow("empty") << "" << false;
}
void NetworkMonitorAppletTest::testIsPhysicalInterface()
{
NetworkMonitorApplet *applet = createApplet();
QFETCH(QString, name);
QFETCH(bool, expected);
QCOMPARE(applet->isPhysicalInterface(name), expected);
delete applet;
}
void NetworkMonitorAppletTest::testCalculateSpeed()
{
NetworkMonitorApplet *applet = createApplet();
// 设置活动接口和接口数据
applet->m_interfaceList = QStringList() << "eth0";
NetworkInterface iface;
iface.name = "eth0";
iface.rxBytes = 1000;
iface.txBytes = 500;
applet->m_interfaces["eth0"] = iface;
applet->m_activeInterface = "eth0";
// 模拟首次更新:记录基线
applet->m_firstUpdate = true;
applet->calculateSpeed();
QVERIFY(!applet->m_firstUpdate);
// 第二次更新:字节增加了 1000 rx / 500 tx,间隔约 1 秒
iface.rxBytes = 2000;
iface.txBytes = 1000;
applet->m_interfaces["eth0"] = iface;
applet->m_lastTimestampMs = QDateTime::currentMSecsSinceEpoch() - 1000;
applet->calculateSpeed();
// 速度应约为 1000 bytes/sec(允许误差因真实时间戳)
QVERIFY(applet->m_downloadSpeed > 900 && applet->m_downloadSpeed < 1100);
QVERIFY(applet->m_uploadSpeed > 450 && applet->m_uploadSpeed < 550);
delete applet;
}
void NetworkMonitorAppletTest::testNegativeDeltaClamped()
{
NetworkMonitorApplet *applet = createApplet();
applet->m_interfaceList = QStringList() << "eth0";
NetworkInterface iface;
iface.name = "eth0";
iface.rxBytes = 2000;
iface.txBytes = 1000;
applet->m_interfaces["eth0"] = iface;
applet->m_activeInterface = "eth0";
// 首次更新记录基线
applet->m_firstUpdate = true;
applet->calculateSpeed();
// 模拟计数器回绕:当前值小于上次值
iface.rxBytes = 500; // 回绕!
iface.txBytes = 200; // 回绕!
applet->m_interfaces["eth0"] = iface;
applet->m_lastTimestampMs = QDateTime::currentMSecsSinceEpoch() - 1000;
applet->calculateSpeed();
// 速度应为 0,不应为负
QVERIFY(applet->m_downloadSpeed >= 0);
QVERIFY(applet->m_uploadSpeed >= 0);
delete applet;
}
void NetworkMonitorAppletTest::testTotalAccumulation()
{
NetworkMonitorApplet *applet = createApplet();
applet->m_interfaceList = QStringList() << "eth0";
NetworkInterface iface;
iface.name = "eth0";
iface.rxBytes = 0;
iface.txBytes = 0;
applet->m_interfaces["eth0"] = iface;
applet->m_activeInterface = "eth0";
applet->m_totalDownload = 0;
applet->m_totalUpload = 0;
// 首次更新记录基线
applet->m_firstUpdate = true;
applet->calculateSpeed();
// 第一次增量:rx +1000, tx +500
iface.rxBytes = 1000;
iface.txBytes = 500;
applet->m_interfaces["eth0"] = iface;
applet->m_lastTimestampMs = QDateTime::currentMSecsSinceEpoch() - 1000;
applet->calculateSpeed();
QCOMPARE(applet->m_totalDownload, qint64(1000));
QCOMPARE(applet->m_totalUpload, qint64(500));
// 第二次增量:rx +2000, tx +1000
iface.rxBytes = 3000;
iface.txBytes = 1500;
applet->m_interfaces["eth0"] = iface;
applet->m_lastTimestampMs = QDateTime::currentMSecsSinceEpoch() - 1000;
applet->calculateSpeed();
// 总量应累加,而非取计数器值
QCOMPARE(applet->m_totalDownload, qint64(3000));
QCOMPARE(applet->m_totalUpload, qint64(1500));
delete applet;
}
void NetworkMonitorAppletTest::testSetActiveInterfaceValidation()
{
NetworkMonitorApplet *applet = createApplet();
applet->m_interfaceList = QStringList() << "eth0" << "wlan0";
applet->m_activeInterface = "eth0";
// 无效接口名应被忽略
applet->setActiveInterface("nonexistent");
QCOMPARE(applet->m_activeInterface, QString("eth0"));
// 有效接口名应被设置
applet->setActiveInterface("wlan0");
QCOMPARE(applet->m_activeInterface, QString("wlan0"));
delete applet;
}
QTEST_MAIN(NetworkMonitorAppletTest)
#include "tst_networkmonitorapplet.moc"
+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>