Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73e5487d71 | ||
|
|
02296ba13e | ||
|
|
2e615df2ff | ||
|
|
fba506d9d8 | ||
|
|
43fdcc8b0a | ||
|
|
a4fe4fdc83 | ||
|
|
35dc998448 | ||
|
|
110032f3c9 | ||
|
|
36200e7c50 | ||
|
|
4b5cde4361 | ||
|
|
6d17f638e7 | ||
|
|
9803c59e76 | ||
|
|
8e7b945a7c | ||
|
|
91bc3174a0 | ||
|
|
c614df5fbe | ||
|
|
42028d93bd | ||
|
|
af7ae6b334 | ||
|
|
e53981f828 | ||
|
|
9f11b32b5e | ||
|
|
b9b5232d5f | ||
|
|
7f7ab2d737 | ||
|
|
c4246cd122 | ||
|
|
539442ef48 |
@@ -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
@@ -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)
|
||||||
@@ -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 代理工作指引
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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 流量超限通知
|
||||||
|
|
||||||
|
**第三优先级**:其余可选增强按需开发
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,469 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Jokul
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
// 网络速度监控弹窗内容组件
|
||||||
|
// 展示实时速度、总量统计、网卡切换 chip 列表;
|
||||||
|
// 由 networkview.qml 的 PanelPopup 实例化,传入 applet 属性;
|
||||||
|
// 颜色与格式化函数统一取自同目录 NetCommon.qml(common.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
|
||||||
|
// 高度需容纳 chip(28px)+ 水平滚动条(~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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 检测任务栏深浅后传入),
|
||||||
|
// 默认浅色,保证组件独立预览时与原版视觉一致
|
||||||
// 对外依赖:applet(C++ 后端对象)、networkInterfaces(接口列表)、activeInterface(当前接口)、
|
// 对外依赖:applet(C++ 后端对象)、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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.applet(C++ 后端对象),
|
// - 数据通过属性注入:applet 即 networkview.qml 的 root.applet(C++ 后端对象),
|
||||||
// 为 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 时序列化为 #AARRGGBB,Canvas 无法正确解析,故不用 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 -> 20000,80000 -> 100000),
|
// 设计原因:使刻度值为易读的整数(如 12345 -> 20000,80000 -> 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
@@ -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.accentRed,version 取自 C++ 后端 applet.version
|
// 依赖通过属性传入:accentColor = root.accentRed,version 取自 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.accentRed,applet = root.applet
|
// 依赖通过属性传入:accentColor = root.accentRed,applet = 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
@@ -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") {
|
// 设计原因:原仅过滤 lo,docker/veth/br- 接口会短暂出现在列表中,
|
||||||
|
// 随后被 readNetworkStats() 覆盖,造成 UI 闪烁
|
||||||
|
if (entry != "lo" && !entry.startsWith("veth") &&
|
||||||
|
!entry.startsWith("docker") && !entry.startsWith("br-")) {
|
||||||
m_interfaceList << entry;
|
m_interfaceList << entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_INVOKABLE(QML 不应直接调用,
|
||||||
|
// 否则会打破 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;
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user