Compare commits
23 Commits
8f5e30532d
..
v1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 959371c236 | |||
| d0b8272ec6 | |||
| c1a535a005 | |||
| c07f17d212 | |||
| fcb43fe34f | |||
| 6b2721c821 | |||
| e83fb0e269 | |||
| 09e64d4859 | |||
| d53409808c | |||
| 0f58dcad03 | |||
| 6325fb51cc | |||
| 6bc1a3e3fb | |||
| 903e0f1801 | |||
| 3c92359407 | |||
| 170e602945 | |||
| 02ac3faf85 | |||
| b80769c26e | |||
| 790103698e | |||
| 2beafd4cf1 | |||
| f7281a5f0c | |||
| 43cb5238c8 | |||
| ea2081f0a6 | |||
| ab1a5227fa |
+4
-1
@@ -22,4 +22,7 @@ cmake-build-*/
|
|||||||
|
|
||||||
# System files
|
# System files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# AI 工作流内部文档(不纳入版本控制)
|
||||||
|
docs/superpowers/
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
Guidance for AI agents working in this repo. Verified against `CMakeLists.txt`,
|
Guidance for AI agents working in this repo. Verified against `CMakeLists.txt`,
|
||||||
`package/metadata.json`, and the installed macro file at
|
`package/metadata.json.in`, and the installed macro file at
|
||||||
`/usr/lib/x86_64-linux-gnu/cmake/DDEShell/DDEShellPackageMacros.cmake`.
|
`/usr/lib/x86_64-linux-gnu/cmake/DDEShell/DDEShellPackageMacros.cmake`.
|
||||||
|
|
||||||
## What this is
|
## What this is
|
||||||
@@ -21,6 +21,8 @@ for the UI. The C++ backend reads `/proc/net/dev` to monitor network traffic.
|
|||||||
- Taskbar icon displays live speed (changes color at high speeds)
|
- Taskbar icon displays live speed (changes color at high speeds)
|
||||||
- Hover tooltip shows speed summary
|
- Hover tooltip shows speed summary
|
||||||
- Click to open detailed popup
|
- Click to open detailed popup
|
||||||
|
- Popup shows active interface IP address, per-interface speed comparison and stats
|
||||||
|
- One-click plugin uninstall from the popup
|
||||||
|
|
||||||
## Build & install
|
## Build & install
|
||||||
|
|
||||||
@@ -42,6 +44,14 @@ bash install.sh
|
|||||||
`cmake --install` needs `sudo` because the default `DDE_SHELL_PACKAGE_INSTALL_DIR`
|
`cmake --install` needs `sudo` because the default `DDE_SHELL_PACKAGE_INSTALL_DIR`
|
||||||
is `/usr/share/dde-shell` (a CMake CACHE variable on this system).
|
is `/usr/share/dde-shell` (a CMake CACHE variable on this system).
|
||||||
|
|
||||||
|
### Deb packaging
|
||||||
|
|
||||||
|
`build-deb.sh` builds a `.deb` from the version declared in `CMakeLists.txt`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bash build-deb.sh # -> jnetapplet_<version>_<arch>.deb
|
||||||
|
```
|
||||||
|
|
||||||
After installation, restart dde-shell:
|
After installation, restart dde-shell:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -57,11 +67,16 @@ then restart `dde-shell` (it discovers applets by scanning the install dir for
|
|||||||
```
|
```
|
||||||
├── CMakeLists.txt # Build configuration
|
├── CMakeLists.txt # Build configuration
|
||||||
├── install.sh # Install script
|
├── install.sh # Install script
|
||||||
├── networkmonitorapplet.h # C++ backend header
|
├── build-deb.sh # Deb packaging script
|
||||||
├── networkmonitorapplet.cpp # C++ backend implementation
|
├── src/
|
||||||
|
│ ├── networkmonitorapplet.h # C++ backend header
|
||||||
|
│ └── networkmonitorapplet.cpp # C++ backend implementation
|
||||||
├── package/
|
├── package/
|
||||||
│ ├── metadata.json # Plugin metadata
|
│ ├── metadata.json.in # Plugin metadata 模板(由 CMake 生成 metadata.json)
|
||||||
│ └── networkview.qml # QML UI
|
│ ├── networkview.qml # QML UI 主入口
|
||||||
|
│ └── components/ # 拆分出的子组件
|
||||||
|
│ └── AboutWindow.qml # 关于窗口组件
|
||||||
|
├── docs/ # Design docs and implementation plans
|
||||||
└── AGENTS.md # This file
|
└── AGENTS.md # This file
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -90,6 +105,7 @@ Inherits from `DApplet`, provides:
|
|||||||
- `networkInterfaces`: List of available interfaces
|
- `networkInterfaces`: List of available interfaces
|
||||||
- `interfaceStats`: Per-interface statistics
|
- `interfaceStats`: Per-interface statistics
|
||||||
- `activeInterface`: Currently selected interface
|
- `activeInterface`: Currently selected interface
|
||||||
|
- `ipAddress`: IP address of the active interface
|
||||||
- `refresh()`: Manually trigger stats update
|
- `refresh()`: Manually trigger stats update
|
||||||
- `setActiveInterface(name)`: Switch active interface
|
- `setActiveInterface(name)`: Switch active interface
|
||||||
|
|
||||||
@@ -99,15 +115,26 @@ Inherits from `DApplet`, provides:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `CMakeLists.txt` | `add_library(...)` target | `space.jokul.JNetApplet` |
|
| `CMakeLists.txt` | `add_library(...)` target | `space.jokul.JNetApplet` |
|
||||||
| `CMakeLists.txt` | `PLUGIN_ID` | `space.jokul.JNetApplet` |
|
| `CMakeLists.txt` | `PLUGIN_ID` | `space.jokul.JNetApplet` |
|
||||||
| `package/metadata.json` | `Plugin.Id` | `space.jokul.JNetApplet` |
|
| `package/metadata.json.in` | `Plugin.Id` | `space.jokul.JNetApplet` |
|
||||||
|
|
||||||
`Plugin.Url` (`networkview.qml`) is a path **relative to `package/`**, not an identifier.
|
`Plugin.Url` (`networkview.qml`) is a path **relative to `package/`**, not an identifier.
|
||||||
Keep it pointing at the entry QML.
|
Keep it pointing at the entry QML.
|
||||||
|
|
||||||
## Required metadata fields (QML applet)
|
## Required metadata fields (QML applet)
|
||||||
|
|
||||||
`metadata.json` must contain `Plugin.Version`, `Plugin.Id`, `Plugin.Url`, and
|
`package/metadata.json.in` is the template; CMake's `configure_file()` generates the
|
||||||
`Plugin.Parent`. The current file is complete — don't drop any.
|
final `metadata.json` in the build directory at configure time. The template must
|
||||||
|
contain `Plugin.Version`, `Plugin.Id`, `Plugin.Url`, and `Plugin.Parent`. Don't drop any.
|
||||||
|
|
||||||
|
## Version management
|
||||||
|
|
||||||
|
**Single source of truth**: `CMakeLists.txt` line 3 `project(JNetApplet VERSION x.y.z)`.
|
||||||
|
|
||||||
|
- `package/metadata.json.in` uses `@PROJECT_VERSION@` placeholder, substituted at
|
||||||
|
configure time -> installed `metadata.json` always matches.
|
||||||
|
- `build-deb.sh` extracts the version directly from `CMakeLists.txt` via grep.
|
||||||
|
- **To bump the version**: edit only the `project(... VERSION ...)` line in
|
||||||
|
`CMakeLists.txt`. All downstream consumers (install, deb) pick it up automatically.
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
|
|||||||
+17
-6
@@ -1,6 +1,6 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
project(JNetApplet VERSION 1.0.0 LANGUAGES CXX)
|
project(JNetApplet VERSION 1.0.1 LANGUAGES CXX)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
@@ -14,14 +14,15 @@ find_package(DDEShell REQUIRED)
|
|||||||
set(PLUGIN_ID "space.jokul.JNetApplet")
|
set(PLUGIN_ID "space.jokul.JNetApplet")
|
||||||
|
|
||||||
add_library(space.jokul.JNetApplet SHARED
|
add_library(space.jokul.JNetApplet SHARED
|
||||||
networkmonitorapplet.h
|
src/networkmonitorapplet.h
|
||||||
networkmonitorapplet.cpp
|
src/networkmonitorapplet.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set_target_properties(space.jokul.JNetApplet PROPERTIES PREFIX "")
|
set_target_properties(space.jokul.JNetApplet PROPERTIES PREFIX "")
|
||||||
|
|
||||||
|
# 头文件搜索路径指向 src/,使 #include "networkmonitorapplet.h" 可被外部解析
|
||||||
target_include_directories(space.jokul.JNetApplet PRIVATE
|
target_include_directories(space.jokul.JNetApplet PRIVATE
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
/usr/include/dde-shell
|
/usr/include/dde-shell
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,5 +37,15 @@ target_link_libraries(space.jokul.JNetApplet PRIVATE
|
|||||||
|
|
||||||
# 安装到dde-shell插件目录
|
# 安装到dde-shell插件目录
|
||||||
install(TARGETS space.jokul.JNetApplet DESTINATION /usr/lib/x86_64-linux-gnu/dde-shell)
|
install(TARGETS space.jokul.JNetApplet DESTINATION /usr/lib/x86_64-linux-gnu/dde-shell)
|
||||||
install(FILES package/metadata.json DESTINATION /usr/share/dde-shell/${PLUGIN_ID})
|
|
||||||
install(FILES package/networkview.qml DESTINATION /usr/share/dde-shell/${PLUGIN_ID})
|
# 由模板生成 metadata.json,版本号从 project() 取,避免多处手动维护
|
||||||
|
# 设计原因:版本号唯一源为 CMakeLists.txt 顶部的 project(... VERSION),
|
||||||
|
# configure_file 在构建目录生成最终 metadata.json,install 安装生成文件
|
||||||
|
configure_file(package/metadata.json.in ${CMAKE_CURRENT_BINARY_DIR}/metadata.json @ONLY)
|
||||||
|
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/metadata.json DESTINATION /usr/share/dde-shell/${PLUGIN_ID})
|
||||||
|
install(FILES package/networkview.qml DESTINATION /usr/share/dde-shell/${PLUGIN_ID})
|
||||||
|
|
||||||
|
# 安装 components 子目录下所有 QML 组件
|
||||||
|
# 设计原因:拆分出的子组件需随主 QML 一起安装到 dde-shell 插件目录,
|
||||||
|
# 否则 networkview.qml 的 import "components" 在运行时找不到类型
|
||||||
|
install(DIRECTORY package/components DESTINATION /usr/share/dde-shell/${PLUGIN_ID})
|
||||||
@@ -1,2 +1,79 @@
|
|||||||
# JNetApplet
|
# JNetApplet
|
||||||
|
|
||||||
|
基于 dde-shell 的**深度系统任务栏网络速率监控插件**,使用 C++ 后端采集数据、
|
||||||
|
QML 前端渲染界面,实时显示当前网卡的上行/下行速率与累计流量。
|
||||||
|
|
||||||
|
- 插件 ID:`space.jokul.JNetApplet`
|
||||||
|
- 适用环境:deepin 桌面环境(dde-shell 面板)
|
||||||
|
- 数据源:读取 `/proc/net/dev` 计算网络流量
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 实时上行/下行速率监控(1 秒刷新)
|
||||||
|
- 累计上传/下载流量统计
|
||||||
|
- 多网卡支持,可在弹窗中切换活动网卡
|
||||||
|
- 任务栏图标实时显示速率,高速时变色提示
|
||||||
|
- 悬停显示速率摘要,点击展开详情弹窗
|
||||||
|
- 详情弹窗展示活动网卡 IP 地址、各网卡速率对比与统计图表
|
||||||
|
- 支持从弹窗一键卸载插件
|
||||||
|
|
||||||
|
## 构建与安装
|
||||||
|
|
||||||
|
需要配置期存在 `DDEShell` CMake 包(来自 `dde-shell` 开发包)。
|
||||||
|
|
||||||
|
### 方式一:从源码安装
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -B build
|
||||||
|
cmake --build build
|
||||||
|
sudo cmake --install build # -> /usr/share/dde-shell/space.jokul.JNetApplet/
|
||||||
|
```
|
||||||
|
|
||||||
|
或使用安装脚本:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bash install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
`cmake --install` 需要 `sudo`,因为默认 `DDE_SHELL_PACKAGE_INSTALL_DIR`
|
||||||
|
为 `/usr/share/dde-shell`(本机 CMake CACHE 变量)。
|
||||||
|
|
||||||
|
安装后重启 dde-shell 使插件生效:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
systemctl --user restart dde-shell@DDE
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式二:打包为 deb 安装
|
||||||
|
|
||||||
|
仓库提供 `build-deb.sh` 一键打包脚本,会从 `CMakeLists.txt` 读取版本号并构建 deb 包:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bash build-deb.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
产物为 `jnetapplet_<version>_<arch>.deb`,可直接用 `dpkg -i` 安装。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
项目无测试 / lint / typecheck / CI 流程,验证方式为手动:安装后重启 `dde-shell`
|
||||||
|
(其通过扫描安装目录中的 `metadata.json` 发现插件)。
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
├── CMakeLists.txt # 构建配置
|
||||||
|
├── install.sh # 安装脚本
|
||||||
|
├── build-deb.sh # deb 一键打包脚本
|
||||||
|
├── src/
|
||||||
|
│ ├── networkmonitorapplet.h # C++ 后端头文件
|
||||||
|
│ └── networkmonitorapplet.cpp # C++ 后端实现
|
||||||
|
├── package/
|
||||||
|
│ ├── metadata.json.in # 插件元数据模板(由 CMake 生成 metadata.json)
|
||||||
|
│ ├── networkview.qml # QML 界面主入口
|
||||||
|
│ └── components/ # 拆分出的子组件
|
||||||
|
│ └── AboutWindow.qml # 关于窗口组件
|
||||||
|
├── docs/ # 设计文档与实现计划
|
||||||
|
├── README.md # 本文件
|
||||||
|
└── AGENTS.md # AI 代理工作指引
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,841 +0,0 @@
|
|||||||
# JNetApplet UI 重新设计实现计划
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** 重写 `package/networkview.qml`,实现 deepin 原生简约风格的任务栏图标、左键弹出面板、右键菜单和 Tooltip。
|
|
||||||
|
|
||||||
**Architecture:** 单文件 QML 重写,不动 C++ 后端。颜色派生自 `DockPalette.iconTextPalette`,深/浅色自适应。新增 `formatSpeedShort`、右键 `Menu`、chip 接口切换器。
|
|
||||||
|
|
||||||
**Tech Stack:** QML 2.15、QtQuick.Controls 2.15、QtQuick.Layouts 1.15、org.deepin.ds 1.0、org.deepin.ds.dock 1.0、org.deepin.dtk 1.0。
|
|
||||||
|
|
||||||
## Global Constraints
|
|
||||||
|
|
||||||
- 仅修改 `package/networkview.qml`,不改 C++、`metadata.json`、`CMakeLists.txt`
|
|
||||||
- 保留 SPDX 文件头与所有 imports
|
|
||||||
- Root 元素为 `AppletItem`(来自 `import org.deepin.ds 1.0`)
|
|
||||||
- QML 属性类型用 `double`/`real`,不用 `qint64`
|
|
||||||
- 注释统一中文
|
|
||||||
- 项目无测试/lint/CI,验证方式:`cmake --build build` + 手动安装重启 dde-shell
|
|
||||||
- Commit 中文 conventional commits,仅在被显式请求时提交
|
|
||||||
|
|
||||||
**Spec 引用:** `docs/superpowers/specs/2026-07-18-ui-redesign-design.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
仅修改 `package/networkview.qml`(当前 521 行)。无新文件、无 C++ 改动。
|
|
||||||
|
|
||||||
重写后内部结构:
|
|
||||||
|
|
||||||
```
|
|
||||||
AppletItem (root)
|
|
||||||
├── 属性区:applet 绑定、颜色派生、formatSpeedShort
|
|
||||||
├── 任务栏图标区:Column { Row(↓值), Row(↑值) }
|
|
||||||
├── PanelToolTip
|
|
||||||
├── Timer × 2
|
|
||||||
├── HoverHandler
|
|
||||||
├── PanelPopup
|
|
||||||
│ └── Control > ColumnLayout
|
|
||||||
│ ├── 标题栏(标题+副标题+刷新按钮)
|
|
||||||
│ ├── 实时速度卡片(双列)
|
|
||||||
│ ├── 总量统计(单行)
|
|
||||||
│ ├── 接口切换 chip 列表
|
|
||||||
│ └── 未检测到接口占位
|
|
||||||
├── Menu (contextMenu) ← 新增
|
|
||||||
├── Dialog (aboutDialog) ← 新增
|
|
||||||
├── TapHandler (左键)
|
|
||||||
└── TapHandler (右键) ← 新增
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task 1: 属性区与格式化函数(基础层)
|
|
||||||
|
|
||||||
**Files:** Modify `package/networkview.qml:32-72`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Produces: `formatSpeedShort()`、`accentGreen`、`accentGreenLight`、`accentOrange`、`accentRed`、`downloadValueColor`、`uploadValueColor` - 后续任务全部依赖
|
|
||||||
|
|
||||||
**说明:** 替换 root 的函数块和颜色属性块。保留所有 `applet` 绑定属性不变。
|
|
||||||
|
|
||||||
- [ ] **Step 1: 替换函数区(原 32-56 行)**
|
|
||||||
|
|
||||||
将 `// 格式化速度显示` 注释到 `formatTotal` 函数结束替换为:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
// 紧凑格式化速度(用于任务栏图标和 tooltip,不带单位后缀,节省空间)
|
|
||||||
// 设计原因:任务栏 ~48px 空间有限,"1.2M" 比 "1.2 MB/s" 节省约一半宽度
|
|
||||||
function formatSpeedShort(bytesPerSec) {
|
|
||||||
if (bytesPerSec < 1024) {
|
|
||||||
return bytesPerSec.toFixed(0)
|
|
||||||
} else if (bytesPerSec < 1024 * 1024) {
|
|
||||||
return (bytesPerSec / 1024).toFixed(1) + "K"
|
|
||||||
} else if (bytesPerSec < 1024 * 1024 * 1024) {
|
|
||||||
return (bytesPerSec / (1024 * 1024)).toFixed(1) + "M"
|
|
||||||
} else {
|
|
||||||
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + "G"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化速度显示(带单位,用于弹出面板,信息更完整)
|
|
||||||
function formatSpeed(bytesPerSec) {
|
|
||||||
if (bytesPerSec < 1024) {
|
|
||||||
return bytesPerSec.toFixed(0) + " B/s"
|
|
||||||
} else if (bytesPerSec < 1024 * 1024) {
|
|
||||||
return (bytesPerSec / 1024).toFixed(1) + " 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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: 替换颜色属性区(原 58-72 行)**
|
|
||||||
|
|
||||||
将 `// 根据速度计算图标颜色` 注释到 `accentBlueLight` 属性结束替换为:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
// 任务栏与面板的颜色全部派生自 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
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: 验证标识符已定义**
|
|
||||||
|
|
||||||
Run: `grep -n "formatSpeedShort\|downloadValueColor\|uploadValueColor\|accentGreen " /home/Jokul/Documents/projects/JNetApplet/package/networkview.qml`
|
|
||||||
Expected: 每个标识符至少出现一次。
|
|
||||||
|
|
||||||
- [ ] **Step 4: 验证 C++ 仍可构建**
|
|
||||||
|
|
||||||
Run: `cmake --build /home/Jokul/Documents/projects/JNetApplet/build 2>&1 | tail -5`
|
|
||||||
Expected: 构建成功,无 error。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task 2: 任务栏图标区重写
|
|
||||||
|
|
||||||
**Files:** Modify `package/networkview.qml`(原 74-120 行的圆圈 `Rectangle`)
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `formatSpeedShort()`、`downloadValueColor`、`uploadValueColor`、`secondaryText`、`dockSize`
|
|
||||||
- Produces: 任务栏双行数值显示
|
|
||||||
|
|
||||||
- [ ] **Step 1: 替换图标区域**
|
|
||||||
|
|
||||||
将 `// 图标区域` 注释到对应 `Rectangle` 闭合 `}` 替换为:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
// 任务栏图标区:双行紧凑数值,移除原圆圈边框
|
|
||||||
// 上行下载、下行上传,箭头为次级色、数值为强调色
|
|
||||||
// 设计原因:圆圈边框在 ~48px dock 内压缩可用空间,去掉后数值字号可放大提升可读性
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 1
|
|
||||||
|
|
||||||
// 下载速度行
|
|
||||||
Row {
|
|
||||||
spacing: 2
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "↓"
|
|
||||||
font.pixelSize: root.dockSize * 0.16
|
|
||||||
color: root.secondaryText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: root.formatSpeedShort(root.downloadSpeed)
|
|
||||||
font.pixelSize: root.dockSize * 0.22
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: root.downloadValueColor
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上传速度行
|
|
||||||
Row {
|
|
||||||
spacing: 2
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "↑"
|
|
||||||
font.pixelSize: root.dockSize * 0.16
|
|
||||||
color: root.secondaryText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: root.formatSpeedShort(root.uploadSpeed)
|
|
||||||
font.pixelSize: root.dockSize * 0.22
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: root.uploadValueColor
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: 确认旧 `iconColor` 已无引用**
|
|
||||||
|
|
||||||
Run: `grep -n "iconColor" /home/Jokul/Documents/projects/JNetApplet/package/networkview.qml`
|
|
||||||
Expected: 无输出。
|
|
||||||
|
|
||||||
- [ ] **Step 3: 安装并目视验证**
|
|
||||||
|
|
||||||
Run: `sudo cmake --install /home/Jokul/Documents/projects/JNetApplet/build && systemctl --user restart dde-shell@DDE`
|
|
||||||
Expected: 任务栏图标显示双行「↓数值 / ↑数值」,下载蓝、上传绿,无圆圈边框。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task 3: Tooltip 文本更新
|
|
||||||
|
|
||||||
**Files:** Modify `package/networkview.qml`(`buildToolTipText` 函数)
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `formatSpeedShort()`、`activeInterface`、`ready`
|
|
||||||
|
|
||||||
**说明:** Tooltip 改用 `formatSpeedShort` 与任务栏一致。`PanelToolTip.text` 是纯文本,不支持富文本着色,箭头无法单独着色。
|
|
||||||
|
|
||||||
- [ ] **Step 1: 替换 buildToolTipText 函数体**
|
|
||||||
|
|
||||||
```qml
|
|
||||||
// 构建 tooltip 文本:单行紧凑展示下载/上传速度和活动接口
|
|
||||||
// 设计原因:tooltip 空间有限,用 formatSpeedShort 与任务栏保持视觉一致
|
|
||||||
// 用 "·" 分隔速度区与接口名,比 " | " 更轻量
|
|
||||||
function buildToolTipText() {
|
|
||||||
if (!root.ready) {
|
|
||||||
return qsTr("未检测到网络接口")
|
|
||||||
}
|
|
||||||
|
|
||||||
var lines = []
|
|
||||||
lines.push("↓ " + root.formatSpeedShort(root.downloadSpeed))
|
|
||||||
lines.push("↑ " + root.formatSpeedShort(root.uploadSpeed))
|
|
||||||
lines.push(root.activeInterface)
|
|
||||||
return lines.join(" · ")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: 确认 PanelToolTip 未就绪文案为中文**
|
|
||||||
|
|
||||||
确认 `PanelToolTip` 的 `text` 属性为:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
PanelToolTip {
|
|
||||||
id: toolTip
|
|
||||||
text: root.ready ? root.buildToolTipText() : qsTr("未检测到网络接口")
|
|
||||||
toolTipX: DockPanelPositioner.x
|
|
||||||
toolTipY: DockPanelPositioner.y
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
若为英文 `"No network interface detected"`,替换为上述中文。
|
|
||||||
|
|
||||||
- [ ] **Step 3: 安装并目视验证**
|
|
||||||
|
|
||||||
Run: `sudo cmake --install /home/Jokul/Documents/projects/JNetApplet/build && systemctl --user restart dde-shell@DDE`
|
|
||||||
Expected: 悬停时 tooltip 显示形如 `↓ 1.2M · ↑ 256K · eth0`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task 4: 左键弹出面板重写
|
|
||||||
|
|
||||||
**Files:** Modify `package/networkview.qml`(`PanelPopup` 内部,原 173-489 行)
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `formatSpeed`、`formatTotal`、`primaryText`、`secondaryText`、`tertiaryText`、`cardBackground`、`cardBorder`、`accentBlue`、`accentBlueLight`、`accentGreen`、`accentGreenLight`、`downloadValueColor`、`uploadValueColor`、`ready`、`activeInterface`、`downloadSpeed`、`uploadSpeed`、`totalDownload`、`totalUpload`、`networkInterfaces`、`applet`
|
|
||||||
- Produces: 360×320 重构面板
|
|
||||||
|
|
||||||
**说明:** 尺寸从 400×350 改为 360×320。结构:标题栏(含副标题)+ 实时速度双列卡片 + 总量单行 + chip 接口切换 + 未就绪占位。
|
|
||||||
|
|
||||||
- [ ] **Step 1: 调整 PanelPopup 尺寸**
|
|
||||||
|
|
||||||
将 `PanelPopup` 的 `width: 400` / `height: 350` 改为 `width: 360` / `height: 320`。保留 `popupX`/`popupY`/`onPopupVisibleChanged` 不变。
|
|
||||||
|
|
||||||
- [ ] **Step 2: 替换 Control 及其 contentItem 全部内容**
|
|
||||||
|
|
||||||
将 `PanelPopup` 内的 `Control { id: popupContainer ... }` 整体替换为:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
Control {
|
|
||||||
id: popupContainer
|
|
||||||
anchors.fill: parent
|
|
||||||
padding: 16
|
|
||||||
|
|
||||||
contentItem: ColumnLayout {
|
|
||||||
spacing: 12
|
|
||||||
|
|
||||||
// 标题栏:左标题+副标题,右刷新按钮
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: 0
|
|
||||||
Text {
|
|
||||||
text: qsTr("网络速度监控")
|
|
||||||
font.pixelSize: 15
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: root.primaryText
|
|
||||||
}
|
|
||||||
Text {
|
|
||||||
text: root.ready ? root.activeInterface : qsTr("未连接")
|
|
||||||
font.pixelSize: 11
|
|
||||||
color: root.secondaryText
|
|
||||||
visible: root.ready
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { Layout.fillWidth: true }
|
|
||||||
|
|
||||||
// 刷新按钮:hover 高亮 + 旋转动画
|
|
||||||
Rectangle {
|
|
||||||
width: 28
|
|
||||||
height: 28
|
|
||||||
radius: 6
|
|
||||||
color: refreshIconArea.containsMouse ? root.accentBlueLight : "transparent"
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "⟳"
|
|
||||||
font.pixelSize: 16
|
|
||||||
color: refreshIconArea.containsMouse ? root.accentBlue : root.secondaryText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: refreshIconArea
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (root.applet) root.applet.refresh()
|
|
||||||
refreshSpin.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RotationAnimator on rotation {
|
|
||||||
id: refreshSpin
|
|
||||||
running: false
|
|
||||||
from: 0
|
|
||||||
to: 360
|
|
||||||
duration: 500
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 实时速度卡片:双列布局,主视觉
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: 110
|
|
||||||
color: root.cardBackground
|
|
||||||
radius: 12
|
|
||||||
border.width: 1
|
|
||||||
border.color: root.cardBorder
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 12
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
// 下载列
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 4
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
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 {
|
|
||||||
width: 1
|
|
||||||
Layout.fillHeight: true
|
|
||||||
color: root.cardBorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上传列
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 4
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { Layout.fillWidth: true }
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "↑ " + root.formatTotal(root.totalUpload)
|
|
||||||
font.pixelSize: 12
|
|
||||||
color: root.primaryText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 接口切换 chip 列表(仅多接口时显示)
|
|
||||||
Flow {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 6
|
|
||||||
visible: root.ready && root.networkInterfaces.length > 1
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.networkInterfaces
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: chipText.implicitWidth + 20
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 未检测到接口占位
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: 确认 `Component.onCompleted` 中 `DockPanelPositioner.bounding` 绑定保留**
|
|
||||||
|
|
||||||
`PanelPopup` 末尾的 `Component.onCompleted` 块保留不变(用于定位 popup 位置)。
|
|
||||||
|
|
||||||
- [ ] **Step 4: 安装并目视验证**
|
|
||||||
|
|
||||||
Run: `sudo cmake --install /home/Jokul/Documents/projects/JNetApplet/build && systemctl --user restart dde-shell@DDE`
|
|
||||||
Expected:
|
|
||||||
- 左键点击任务栏图标,弹出 360×320 面板
|
|
||||||
- 标题栏显示「网络速度监控」+ 接口名副标题
|
|
||||||
- 实时速度卡片双列:下载蓝、上传绿,各带圆形图标底
|
|
||||||
- 总量统计单行显示「总计 ↓ X · ↑ Y」
|
|
||||||
- 多接口时显示 chip 列表,当前接口高亮蓝色
|
|
||||||
- 未检测到接口时显示占位卡片
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task 5: 右键菜单与关于对话框
|
|
||||||
|
|
||||||
**Files:** Modify `package/networkview.qml`(新增 `Menu`、`Dialog`、右键 `TapHandler`,替换原左键 `TapHandler`)
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `applet.refresh()`、`applet.setActiveInterface()`、`networkInterfaces`、`activeInterface`、`primaryText`、`secondaryText`、`accentBlue`
|
|
||||||
- Produces: 右键弹出 deepin 风格菜单
|
|
||||||
|
|
||||||
**说明:** 在 `PanelPopup` 之后、原左键 `TapHandler` 之前,新增 `Menu` 和 `Dialog`。在原左键 `TapHandler` 之后新增右键 `TapHandler`。
|
|
||||||
|
|
||||||
- [ ] **Step 1: 新增 contextMenu 和 aboutDialog**
|
|
||||||
|
|
||||||
在 `PanelPopup` 闭合 `}` 之后,原 `// 构建 tooltip 文本` 注释之前,插入:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
// 右键菜单:deepin 风格 Menu,提供刷新/切换接口/关于快捷操作
|
|
||||||
Menu {
|
|
||||||
id: contextMenu
|
|
||||||
|
|
||||||
MenuItem {
|
|
||||||
text: qsTr("刷新")
|
|
||||||
onTriggered: {
|
|
||||||
if (root.applet) root.applet.refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuSeparator {}
|
|
||||||
|
|
||||||
// 切换接口子菜单:动态构建可用接口列表
|
|
||||||
Menu {
|
|
||||||
title: qsTr("切换接口")
|
|
||||||
enabled: root.networkInterfaces.length > 1
|
|
||||||
|
|
||||||
Instantiator {
|
|
||||||
model: root.networkInterfaces
|
|
||||||
delegate: MenuItem {
|
|
||||||
text: modelData
|
|
||||||
checkable: true
|
|
||||||
checked: modelData === root.activeInterface
|
|
||||||
onTriggered: {
|
|
||||||
if (root.applet) root.applet.setActiveInterface(modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onObjectAdded: (index, object) => contextMenu.children[2].insertItem(index, object)
|
|
||||||
onObjectRemoved: (index, object) => contextMenu.children[2].removeItem(object)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuSeparator {}
|
|
||||||
|
|
||||||
MenuItem {
|
|
||||||
text: qsTr("关于")
|
|
||||||
onTriggered: aboutDialog.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关于对话框:显示插件信息
|
|
||||||
Dialog {
|
|
||||||
id: aboutDialog
|
|
||||||
anchors.centerIn: parent
|
|
||||||
modal: true
|
|
||||||
width: 280
|
|
||||||
height: 160
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: root.cardBackground
|
|
||||||
radius: 12
|
|
||||||
border.width: 1
|
|
||||||
border.color: root.cardBorder
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: ColumnLayout {
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("网络速度监控")
|
|
||||||
font.pixelSize: 16
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: root.primaryText
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("版本:1.0")
|
|
||||||
font.pixelSize: 12
|
|
||||||
color: root.secondaryText
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("监控网络速度和流量")
|
|
||||||
font.pixelSize: 11
|
|
||||||
color: root.tertiaryText
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
Layout.preferredWidth: 240
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { Layout.fillHeight: true }
|
|
||||||
|
|
||||||
Button {
|
|
||||||
text: qsTr("确定")
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
onClicked: aboutDialog.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: 新增右键 TapHandler**
|
|
||||||
|
|
||||||
在原左键 `TapHandler`(`acceptedButtons: Qt.LeftButton`)之后,文件末尾的 root 闭合 `}` 之前,插入:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
// 右键点击:弹出上下文菜单
|
|
||||||
TapHandler {
|
|
||||||
acceptedButtons: Qt.RightButton
|
|
||||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
|
||||||
|
|
||||||
onTapped: {
|
|
||||||
// 关闭可能打开的 popup,避免视觉冲突
|
|
||||||
if (networkPopup.popupVisible) networkPopup.close()
|
|
||||||
toolTip.close()
|
|
||||||
// 使用 root 中心点定位菜单
|
|
||||||
contextMenu.popup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: 确认 Instantiator 子菜单挂载点正确**
|
|
||||||
|
|
||||||
Run: `grep -n "Instantiator\|onObjectAdded" /home/Jokul/Documents/projects/JNetApplet/package/networkview.qml`
|
|
||||||
Expected: 出现一次 Instantiator 块。
|
|
||||||
|
|
||||||
> 注:Instantiator 的 `onObjectAdded`/`onObjectRemoved` 中 `contextMenu.children[2]` 是切换接口子菜单(菜单顺序:刷新 MenuItem / MenuSeparator / 切换接口 Menu / MenuSeparator / 关于 MenuItem)。若 QtQuick.Controls 版本不支持 `insertItem`/`removeItem`,可改用 `Menu` 的 `MenuItem` 直接列出(无 Instantiator),通过 `Instantiator` 的 `onObjectAdded` 调用 `parent.addItem(object)` 或直接在子 Menu 内用 `Repeater`+`MenuItem` 写法。验证时如报错,将子菜单改为:
|
|
||||||
|
|
||||||
```qml
|
|
||||||
Menu {
|
|
||||||
title: qsTr("切换接口")
|
|
||||||
enabled: root.networkInterfaces.length > 1
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.networkInterfaces
|
|
||||||
delegate: MenuItem {
|
|
||||||
text: modelData
|
|
||||||
checkable: true
|
|
||||||
checked: modelData === root.activeInterface
|
|
||||||
onTriggered: {
|
|
||||||
if (root.applet) root.applet.setActiveInterface(modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
(两种写法二选一,优先用 Repeater 版本更稳。)
|
|
||||||
|
|
||||||
- [ ] **Step 4: 安装并目视验证**
|
|
||||||
|
|
||||||
Run: `sudo cmake --install /home/Jokul/Documents/projects/JNetApplet/build && systemctl --user restart dde-shell@DDE`
|
|
||||||
Expected:
|
|
||||||
- 右键点击任务栏图标,弹出 deepin 风格菜单
|
|
||||||
- 菜单项:刷新 / 切换接口(子菜单)/ 关于
|
|
||||||
- 切换接口子菜单列出所有接口,当前接口打勾
|
|
||||||
- 接口数 ≤ 1 时「切换接口」项禁用(灰色)
|
|
||||||
- 点击「关于」弹出对话框显示版本信息
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task 6: 最终验证清单
|
|
||||||
|
|
||||||
**Files:** 无改动,仅验证
|
|
||||||
|
|
||||||
**说明:** 按 spec 验证清单逐项检查。
|
|
||||||
|
|
||||||
- [ ] **Step 1: 重新构建并安装**
|
|
||||||
|
|
||||||
Run: `cmake --build /home/Jokul/Documents/projects/JNetApplet/build && sudo cmake --install /home/Jokul/Documents/projects/JNetApplet/build && systemctl --user restart dde-shell@DDE`
|
|
||||||
Expected: 构建成功、安装成功、dde-shell 重启无报错。
|
|
||||||
|
|
||||||
- [ ] **Step 2: 任务栏图标验证**
|
|
||||||
|
|
||||||
逐项检查:
|
|
||||||
- [ ] 双行数值显示,下载在上、上传在下
|
|
||||||
- [ ] 颜色正确(下载蓝、上传绿)
|
|
||||||
- [ ] 高速时下载值变橙(>1MB/s)/红(>10MB/s)
|
|
||||||
- [ ] 深色任务栏下文字可读
|
|
||||||
- [ ] 浅色任务栏下文字可读
|
|
||||||
- [ ] 无圆圈边框
|
|
||||||
|
|
||||||
- [ ] **Step 3: Tooltip 验证**
|
|
||||||
|
|
||||||
悬停任务栏图标:
|
|
||||||
- [ ] 单行显示 `↓ X · ↑ Y · 接口名`
|
|
||||||
- [ ] 未检测到接口时显示「未检测到网络接口」
|
|
||||||
|
|
||||||
- [ ] **Step 4: 左键弹出面板验证**
|
|
||||||
|
|
||||||
左键点击任务栏图标:
|
|
||||||
- [ ] 尺寸 360×320
|
|
||||||
- [ ] 标题栏:「网络速度监控」+ 接口名副标题
|
|
||||||
- [ ] 实时速度双列卡片:下载蓝、上传绿,圆形图标底
|
|
||||||
- [ ] 总量统计单行:「总计 ↓ X · ↑ Y」
|
|
||||||
- [ ] 多接口时显示 chip 列表,当前接口高亮
|
|
||||||
- [ ] 刷新按钮 hover 高亮 + 旋转动画
|
|
||||||
- [ ] 未检测到接口时显示占位卡片
|
|
||||||
|
|
||||||
- [ ] **Step 5: 右键菜单验证**
|
|
||||||
|
|
||||||
右键点击任务栏图标:
|
|
||||||
- [ ] 弹出 deepin 风格菜单
|
|
||||||
- [ ] 刷新项可用,点击后数据更新
|
|
||||||
- [ ] 切换接口子菜单列出接口、当前项打勾
|
|
||||||
- [ ] 接口数 ≤ 1 时切换项禁用
|
|
||||||
- [ ] 关于对话框显示版本信息
|
|
||||||
|
|
||||||
- [ ] **Step 6: 检查无 QML 运行时错误**
|
|
||||||
|
|
||||||
Run: `journalctl --user -u dde-shell@DDE -n 50 --no-pager | grep -i "qml\|error" | head -20`
|
|
||||||
Expected: 无 QML 加载或运行时错误。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Self-Review
|
|
||||||
|
|
||||||
**Spec 覆盖检查:**
|
|
||||||
- §1 任务栏图标区 → Task 1 (颜色/函数) + Task 2 (布局) ✓
|
|
||||||
- §2 左键弹出面板 → Task 4 ✓
|
|
||||||
- §3 右键菜单 → Task 5 ✓
|
|
||||||
- §4 Tooltip → Task 3 ✓
|
|
||||||
- §5 视觉规范 → Task 1 (颜色表) + 各任务内尺寸 ✓
|
|
||||||
- §6 排除项 → Global Constraints 明确 ✓
|
|
||||||
|
|
||||||
**占位符扫描:** 无 TBD/TODO,所有代码块完整。
|
|
||||||
|
|
||||||
**类型一致性:**
|
|
||||||
- `formatSpeedShort` 在 Task 1 定义,Task 2/3 使用 ✓
|
|
||||||
- `downloadValueColor`/`uploadValueColor` 在 Task 1 定义,Task 2/4 使用 ✓
|
|
||||||
- `accentGreen`/`accentGreenLight` 在 Task 1 定义,Task 4 使用 ✓
|
|
||||||
- 颜色派生自 `basePalette` 全局一致 ✓
|
|
||||||
|
|
||||||
**已知风险:**
|
|
||||||
- Task 5 Step 1 的 `Instantiator`+`insertItem`/`removeItem` 写法在不同 QtQuick.Controls 版本可能有差异,提供了 Repeater 备选方案。
|
|
||||||
- 右键菜单的 `contextMenu.popup()` 默认在鼠标位置弹出,dde-shell 环境下应正常工作;如定位异常可改用 `contextMenu.popup(mouseX, mouseY)`。
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
# JNetApplet UI 重新设计规范
|
|
||||||
|
|
||||||
**日期**:2026-07-18
|
|
||||||
**范围**:`package/networkview.qml` 单文件
|
|
||||||
**方案**:A - 精简重塑(deepin 原生简约风)
|
|
||||||
|
|
||||||
## 背景与目标
|
|
||||||
|
|
||||||
当前 JNetApplet 任务栏图标在 ~48px dock 区域内塞入圆圈边框 + 4 行内容(↓/下载速度/↑/上传速度),视觉拥挤;左键弹出面板色调偏灰、层次弱、接口切换 `ComboBox` 不够直观;右键行为未实现。
|
|
||||||
|
|
||||||
本次重新设计目标:
|
|
||||||
|
|
||||||
- 任务栏展示精简、易读,跟随 deepin 任务栏视觉语言
|
|
||||||
- 左键弹出面板信息层次清晰、视觉舒适
|
|
||||||
- 新增右键菜单提供快捷操作
|
|
||||||
- 全部颜色派生自 `DockPalette`,深/浅色任务栏自适应
|
|
||||||
|
|
||||||
## 范围
|
|
||||||
|
|
||||||
**包含**:
|
|
||||||
|
|
||||||
- `package/networkview.qml` 全部重写
|
|
||||||
- 任务栏图标区、左键弹出面板、右键菜单、Tooltip 四处视觉与交互
|
|
||||||
|
|
||||||
**排除**(YAGNI):
|
|
||||||
|
|
||||||
- 不改 C++ 后端(`networkmonitorapplet.h/.cpp`)
|
|
||||||
- 不新增 `Q_PROPERTY` 或 `Q_INVOKABLE`
|
|
||||||
- 不引入设置面板(刷新间隔、单位制等)
|
|
||||||
- 不加历史趋势图表、sparkline
|
|
||||||
- 不持久化任何用户配置
|
|
||||||
- 不改 `metadata.json`、`CMakeLists.txt`
|
|
||||||
|
|
||||||
## 设计决策记录
|
|
||||||
|
|
||||||
| 决策点 | 选择 | 理由 |
|
|
||||||
|---|---|---|
|
|
||||||
| 整体风格 | deepin 原生简约风 | 跟随宿主环境视觉语言,自适应深/浅色 |
|
|
||||||
| 任务栏展示 | 双行数值(↓下载/↑上传) | 信息密集但不拥挤,无需 hover 即可读 |
|
|
||||||
| 右键行为 | deepin 风格 Menu | 符合 deepin 交互习惯,提供快捷操作 |
|
|
||||||
| 设置项 | 不做 | 控制范围,避免不持久化带来的困惑 |
|
|
||||||
| 历史图表 | 不做 | 超出"重新设计样式与交互"范围 |
|
|
||||||
|
|
||||||
## §1. 任务栏图标区(dock item)
|
|
||||||
|
|
||||||
### 布局
|
|
||||||
|
|
||||||
- 移除当前圆圈 `Rectangle` 容器
|
|
||||||
- 双行 `Column` 居中:
|
|
||||||
- 上行:小号 `↓` + 下载速度数值(如 `1.2M`)
|
|
||||||
- 下行:小号 `↑` + 上传速度数值(如 `256K`)
|
|
||||||
- 箭头与数值同行,箭头为次级色、数值为强调色
|
|
||||||
|
|
||||||
### 新格式化函数 `formatSpeedShort(bytesPerSec)`
|
|
||||||
|
|
||||||
紧凑格式,用于任务栏与 tooltip:
|
|
||||||
|
|
||||||
| 字节范围 | 输出 |
|
|
||||||
|---|---|
|
|
||||||
| `< 1024` | 整数 B/s,如 `"512"` |
|
|
||||||
| `< 1024²` | 1 位小数 + `K`,如 `"1.2K"` |
|
|
||||||
| `< 1024³` | 1 位小数 + `M`,如 `"3.5M"` |
|
|
||||||
| `≥ 1024³` | 1 位小数 + `G`,如 `"2.4G"` |
|
|
||||||
|
|
||||||
保留原 `formatSpeed` / `formatTotal` 函数供弹出面板使用(带单位更清晰)。
|
|
||||||
|
|
||||||
### 字号与字重
|
|
||||||
|
|
||||||
- 箭头:`dockSize * 0.16`
|
|
||||||
- 数值:`dockSize * 0.22`(当前 0.12 太小)
|
|
||||||
- 数值字重:`Font.Medium`
|
|
||||||
|
|
||||||
### 颜色
|
|
||||||
|
|
||||||
- 下载值:`accentBlue` (`#1450A0`)
|
|
||||||
- 上传值:`accentGreen` (`#16A34A`)
|
|
||||||
- 高速警示(保留现有阈值逻辑):
|
|
||||||
- 下载 > 10 MB/s:红 (`#DC2626`)
|
|
||||||
- 下载 > 1 MB/s:橙 (`#F59E0B`)
|
|
||||||
|
|
||||||
## §2. 左键弹出面板(popup)
|
|
||||||
|
|
||||||
### 尺寸
|
|
||||||
|
|
||||||
从 `400×350` 调整为 `360×320`(更紧凑)。
|
|
||||||
|
|
||||||
### 结构(顶到底)
|
|
||||||
|
|
||||||
1. **标题栏**
|
|
||||||
- 左:主标题「网络速度监控」+ 副标题(活动接口名,次级色 12px)
|
|
||||||
- 右:圆形刷新按钮(hover 高亮 + 旋转动画)
|
|
||||||
|
|
||||||
2. **实时速度卡片**(主视觉)
|
|
||||||
- 横向双列布局
|
|
||||||
- 左列:`↓` 圆形底(`accentBlueLight` 背景)+ 「下载」标签 + 大字号速度值(蓝)
|
|
||||||
- 右列:`↑` 圆形底(`accentGreenLight` 背景)+ 「上传」标签 + 大字号速度值(绿)
|
|
||||||
- 数值字号 22px、字重 Bold
|
|
||||||
- 使用 `formatSpeed`(带单位,更清晰)
|
|
||||||
|
|
||||||
3. **总量统计**
|
|
||||||
- 单行紧凑横向布局
|
|
||||||
- 「总计」标签 + `↓ 1.07 GB` + `↑ 74.8 MB`
|
|
||||||
- 字号 12px、次级色
|
|
||||||
|
|
||||||
4. **接口切换 chip 列表**(仅当 `networkInterfaces.length > 1` 时显示)
|
|
||||||
- 横向排列,每个接口一个圆角 chip(`radius: 8`)
|
|
||||||
- 当前活动 chip:`accentBlue` 背景 + 白字
|
|
||||||
- 其他 chip:`cardBackground` + `primaryText`
|
|
||||||
- hover:`accentBlueLight` 背景
|
|
||||||
- 点击触发 `applet.setActiveInterface(name)`
|
|
||||||
|
|
||||||
5. **未检测到接口占位**
|
|
||||||
- 保留现有卡片样式
|
|
||||||
- 文案居中:`⚠` + 「未检测到网络接口」
|
|
||||||
|
|
||||||
### 视觉
|
|
||||||
|
|
||||||
- 卡片圆角 12px
|
|
||||||
- 内边距 16px、卡片内边距 12px、元素间距 12px
|
|
||||||
- 分隔线:`cardBorder` 色、1px
|
|
||||||
|
|
||||||
## §3. 右键菜单(新功能)
|
|
||||||
|
|
||||||
### 触发
|
|
||||||
|
|
||||||
在现有 `TapHandler`(左键)旁新增 `TapHandler` 处理 `Qt.RightButton`,调用 `menu.popup()`。
|
|
||||||
|
|
||||||
### 菜单项
|
|
||||||
|
|
||||||
```
|
|
||||||
刷新
|
|
||||||
─────────────
|
|
||||||
切换接口 ▸ (子菜单)
|
|
||||||
─────────────
|
|
||||||
关于
|
|
||||||
```
|
|
||||||
|
|
||||||
### 行为
|
|
||||||
|
|
||||||
- **刷新**:调用 `applet.refresh()`
|
|
||||||
- **切换接口子菜单**:
|
|
||||||
- 动态构建 `MenuItem`,绑定 `networkInterfaces`
|
|
||||||
- 当前 `activeInterface` 项打勾(`checkable: true`、`checked: true`)
|
|
||||||
- 点击触发 `applet.setActiveInterface(name)`
|
|
||||||
- 接口数 ≤ 1 时禁用此项
|
|
||||||
- **关于**:弹出小型 `Dialog`
|
|
||||||
- 插件名:网络速度监控
|
|
||||||
- 版本:1.0(硬编码,与 `metadata.json` 一致)
|
|
||||||
- 描述:监控网络速度和流量
|
|
||||||
|
|
||||||
### 实现要点
|
|
||||||
|
|
||||||
- 使用 `QtQuick.Controls` 的 `Menu`(deepin DTK 自动适配风格)
|
|
||||||
- `Menu` 作为 root 的子元素,id 为 `contextMenu`
|
|
||||||
- 子菜单用 `Menu` 嵌套
|
|
||||||
|
|
||||||
## §4. Tooltip
|
|
||||||
|
|
||||||
单行简洁:
|
|
||||||
|
|
||||||
```
|
|
||||||
↓ 1.2M/s ↑ 256K/s · eth0
|
|
||||||
```
|
|
||||||
|
|
||||||
- 方向箭头用对应强调色(下载蓝、上传绿)
|
|
||||||
- 接口名前用 `·` 分隔
|
|
||||||
- 复用 `formatSpeedShort` 保持与任务栏一致
|
|
||||||
- 未检测到接口时显示「未检测到网络接口」
|
|
||||||
|
|
||||||
## §5. 视觉规范
|
|
||||||
|
|
||||||
### 颜色派生表
|
|
||||||
|
|
||||||
| 用途 | 计算 |
|
|
||||||
|---|---|
|
|
||||||
| 主文字 `primaryText` | `basePalette` α 0.95 |
|
|
||||||
| 次级文字 `secondaryText` | `basePalette` α 0.80 |
|
|
||||||
| 三级文字 `tertiaryText` | `basePalette` α 0.65 |
|
|
||||||
| 卡片背景 `cardBackground` | `basePalette` α 0.06 |
|
|
||||||
| 卡片边框 `cardBorder` | `basePalette` α 0.10 |
|
|
||||||
| 强调蓝 `accentBlue` | `#1450A0` |
|
|
||||||
| 强调蓝浅 `accentBlueLight` | `#1450A0` α 0.12 |
|
|
||||||
| 强调绿 `accentGreen` | `#16A34A` |
|
|
||||||
| 强调绿浅 `accentGreenLight` | `#16A34A` α 0.12 |
|
|
||||||
| 高速橙 `accentOrange` | `#F59E0B` |
|
|
||||||
| 高速红 `accentRed` | `#DC2626` |
|
|
||||||
|
|
||||||
### 尺寸规范
|
|
||||||
|
|
||||||
- 卡片圆角:12px
|
|
||||||
- chip 圆角:8px
|
|
||||||
- 按钮圆角:6px
|
|
||||||
- 面板内边距:16px
|
|
||||||
- 卡片内边距:12px
|
|
||||||
- 元素间距:12px
|
|
||||||
|
|
||||||
### 字体
|
|
||||||
|
|
||||||
跟随系统无衬线字体,不指定 family。字重使用 `Font.Medium` / `Font.Bold`。
|
|
||||||
|
|
||||||
## 实现性质
|
|
||||||
|
|
||||||
- **文件**:仅 `package/networkview.qml`
|
|
||||||
- **风险**:低(纯前端、不动后端、不改构建)
|
|
||||||
- **验证**:手动安装 + 重启 dde-shell
|
|
||||||
|
|
||||||
## 验证清单
|
|
||||||
|
|
||||||
安装后重启 dde-shell,依次检查:
|
|
||||||
|
|
||||||
- [ ] 任务栏图标:双行数值显示,下载在上、上传在下
|
|
||||||
- [ ] 任务栏图标:颜色正确(下载蓝、上传绿)
|
|
||||||
- [ ] 任务栏图标:高速时下载值变橙/红
|
|
||||||
- [ ] 任务栏图标:深色任务栏下文字可读
|
|
||||||
- [ ] 任务栏图标:浅色任务栏下文字可读
|
|
||||||
- [ ] 悬停 tooltip:单行显示 ↓↑速度 + 接口名
|
|
||||||
- [ ] 左键弹出:尺寸 360×320
|
|
||||||
- [ ] 左键弹出:标题栏 + 接口名副标题
|
|
||||||
- [ ] 左键弹出:实时速度双列卡片
|
|
||||||
- [ ] 左键弹出:总量统计单行
|
|
||||||
- [ ] 左键弹出:接口 chip 列表(多接口时)
|
|
||||||
- [ ] 左键弹出:刷新按钮 hover + 旋转动画
|
|
||||||
- [ ] 左键弹出:未检测到接口时显示占位
|
|
||||||
- [ ] 右键菜单:弹出 deepin 风格菜单
|
|
||||||
- [ ] 右键菜单:刷新项可用
|
|
||||||
- [ ] 右键菜单:切换接口子菜单列出接口、当前项打勾
|
|
||||||
- [ ] 右键菜单:接口数 ≤ 1 时切换项禁用
|
|
||||||
- [ ] 右键菜单:关于对话框显示版本信息
|
|
||||||
+38
-12
@@ -4,23 +4,49 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo "Building JNetApplet..."
|
INSTALL_DIR="/usr/share/dde-shell/space.jokul.JNetApplet"
|
||||||
|
SO_DIR="/usr/lib/x86_64-linux-gnu/dde-shell"
|
||||||
|
|
||||||
# 清理旧的构建目录
|
echo "=== JNetApplet 安装脚本 ==="
|
||||||
|
|
||||||
|
# 1. 清理旧的构建目录
|
||||||
|
echo "[1/6] 清理构建目录..."
|
||||||
rm -rf build
|
rm -rf build
|
||||||
|
|
||||||
# 配置CMake
|
# 2. 配置 + 构建
|
||||||
cmake -Bbuild
|
echo "[2/6] 配置 CMake 并构建..."
|
||||||
|
cmake -B build
|
||||||
# 构建
|
|
||||||
cmake --build build
|
cmake --build build
|
||||||
|
|
||||||
echo "Installing JNetApplet (requires sudo)..."
|
# 3. 清理安装目录中残留的旧文件(包括之前重构产生的多余 QML 文件)
|
||||||
|
echo "[3/6] 清理安装目录中的旧文件..."
|
||||||
|
if [ -d "$INSTALL_DIR" ]; then
|
||||||
|
sudo rm -f "$INSTALL_DIR"/*.qml "$INSTALL_DIR"/*.qmlc
|
||||||
|
fi
|
||||||
|
|
||||||
# 安装
|
# 4. 清理 Qt QML 磁盘缓存(关键!否则 dde-shell 可能使用旧的编译缓存)
|
||||||
|
echo "[4/6] 清理 QML 磁盘缓存..."
|
||||||
|
rm -rf ~/.cache/preloader/qmlcache/ 2>/dev/null || true
|
||||||
|
find ~/.cache -path "*/dde-shell*" -name "*.qmlc" -delete 2>/dev/null || true
|
||||||
|
|
||||||
|
# 5. 安装
|
||||||
|
echo "[5/6] 安装插件(需要 sudo)..."
|
||||||
sudo cmake --install build
|
sudo cmake --install build
|
||||||
|
|
||||||
echo "Installation complete!"
|
# 验证安装结果
|
||||||
echo "Restarting dde-shell..."
|
echo ""
|
||||||
systemctl --user restart dde-shell@DDE
|
echo "=== 安装验证 ==="
|
||||||
echo "Done! Plugin loaded."
|
echo "QML 文件:"
|
||||||
|
ls -la "$INSTALL_DIR"/*.qml 2>/dev/null || echo " [警告] 未找到 QML 文件!"
|
||||||
|
echo "动态库:"
|
||||||
|
ls -la "$SO_DIR"/space.jokul.JNetApplet.so 2>/dev/null || echo " [警告] 未找到 .so 文件!"
|
||||||
|
echo "metadata.json 版本:"
|
||||||
|
grep '"Version"' "$INSTALL_DIR/metadata.json" 2>/dev/null || echo " [警告] 未找到 metadata.json!"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 6. 重启 dde-shell
|
||||||
|
echo "[6/6] 重启 dde-shell..."
|
||||||
|
systemctl --user stop dde-shell@DDE 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
systemctl --user start dde-shell@DDE
|
||||||
|
echo "完成!插件已加载。"
|
||||||
|
|||||||
@@ -0,0 +1,344 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 MyCompany
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
// 关于窗口:DTK 原生风格的独立顶层窗口,桌面居中弹出,展示插件信息
|
||||||
|
// 设计要点:白色圆角卡片 + 键值对信息布局,信息标签使用 deepin 蓝(#0081FF),
|
||||||
|
// 关闭按钮 hover 高亮色由父组件通过 accentColor 传入(红色)
|
||||||
|
// 对外依赖:accentColor,由 networkview.qml 实例化时传入,触发方式为 show()/raise()/requestActivate()
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
import QtQuick.Window 2.15
|
||||||
|
|
||||||
|
Window {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// 对外依赖:关闭按钮 hover 态文字高亮色,由父组件传入
|
||||||
|
// 默认值与 networkview.qml 中 root.accentRed 一致,确保独立可用
|
||||||
|
property color accentColor: Qt.rgba(220 / 255, 38 / 255, 38 / 255, 1)
|
||||||
|
|
||||||
|
// 插件版本号,由父组件从 C++ 后端 applet.version 传入
|
||||||
|
// 默认值保证组件独立可用(如 qmlscene 预览时未传入)
|
||||||
|
property string version: "1.0"
|
||||||
|
|
||||||
|
// 复制成功状态标记:true 时复制图标变绿并显示"已复制"提示,
|
||||||
|
// 2 秒后由 copyResetTimer 复位
|
||||||
|
property bool copied: false
|
||||||
|
|
||||||
|
width: 320
|
||||||
|
height: 260
|
||||||
|
visible: false
|
||||||
|
flags: Qt.FramelessWindowHint | Qt.Window
|
||||||
|
modality: Qt.NonModal
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) {
|
||||||
|
x = (Screen.width - width) / 2
|
||||||
|
y = (Screen.height - height) / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 窗口主体:白色圆角卡片,1px 浅灰边框模拟 DTK 窗口描边
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "#FFFFFF"
|
||||||
|
radius: 12
|
||||||
|
border.width: 1
|
||||||
|
border.color: "#E8E8E8"
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
// 自定义标题栏:左侧"关于"标题 + 右侧圆形关闭按钮,整栏可拖动窗口
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 44
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
// 拖动层:声明在关闭按钮之前(位于其下方),避免遮挡按钮点击与 hover
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
drag.target: root
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 16
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: qsTr("关于")
|
||||||
|
font.pixelSize: 15
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: "#333333"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭按钮:28x28 圆形,hover 时淡红底 + 红色 ×(颜色由 accentColor 决定)
|
||||||
|
Rectangle {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: 14
|
||||||
|
color: aboutCloseMouse.containsMouse ? Qt.rgba(220 / 255, 38 / 255, 38 / 255, 0.15) : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "×"
|
||||||
|
font.pixelSize: 20
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: aboutCloseMouse.containsMouse ? accentColor : "#666666"
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: aboutCloseMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标题区上方间距
|
||||||
|
Item {
|
||||||
|
Layout.preferredHeight: 12
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标题区:插件中文名 + 英文名,整体居中
|
||||||
|
// 注意:用 Layout.fillWidth + horizontalAlignment 而非 Layout.alignment,
|
||||||
|
// 后者在 ColumnLayout 内对 Text 不可靠(实测会左对齐贴边)
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("网络速度监控")
|
||||||
|
font.pixelSize: 18
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: "#333333"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "JNetApplet"
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#999999"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标题区下方间距
|
||||||
|
Item {
|
||||||
|
Layout.preferredHeight: 12
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分隔线:左右留 24px 边距,与信息区对齐
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: 24
|
||||||
|
Layout.rightMargin: 24
|
||||||
|
Layout.preferredHeight: 1
|
||||||
|
color: "#E8E8E8"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 信息区上方间距
|
||||||
|
Item {
|
||||||
|
Layout.preferredHeight: 12
|
||||||
|
}
|
||||||
|
|
||||||
|
// 信息区:键值对布局,左侧标签固定 56px 宽(deepin 蓝),右侧为值
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: 24
|
||||||
|
Layout.rightMargin: 24
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
// 版本行
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("版本")
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#0081FF"
|
||||||
|
Layout.preferredWidth: 56
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: version
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#333333"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 作者行
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("作者")
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#0081FF"
|
||||||
|
Layout.preferredWidth: 56
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Jokul"
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#333333"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 描述行
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("描述")
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#0081FF"
|
||||||
|
Layout.preferredWidth: 56
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("监控网络速度和流量")
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#333333"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仓库行:URL 无空格,需 WrapAnywhere 才能在窄宽度下正确折行
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("仓库")
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#0081FF"
|
||||||
|
Layout.preferredWidth: 56
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "git.jokul.space/Jokul/JNetApplet"
|
||||||
|
font.pixelSize: 11
|
||||||
|
color: "#999999"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
wrapMode: Text.WrapAnywhere
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制按钮:点击复制仓库地址到剪贴板,成功后图标变绿并显示"已复制"提示 2 秒
|
||||||
|
Item {
|
||||||
|
Layout.preferredWidth: 24
|
||||||
|
Layout.preferredHeight: 24
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
|
||||||
|
// "已复制"提示:白底绿字小标签,显示在按钮左侧,
|
||||||
|
// 覆盖于 URL 文字上方,避免与值文本混排
|
||||||
|
Rectangle {
|
||||||
|
anchors.right: parent.left
|
||||||
|
anchors.rightMargin: 4
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: copied
|
||||||
|
width: copiedHintText.implicitWidth + 10
|
||||||
|
height: 18
|
||||||
|
radius: 4
|
||||||
|
color: "#FFFFFF"
|
||||||
|
border.width: 1
|
||||||
|
border.color: "#22A34A"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: copiedHintText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: qsTr("已复制")
|
||||||
|
font.pixelSize: 10
|
||||||
|
color: "#22A34A"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: copyRepoMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
clipboardHelper.text = "git.jokul.space/Jokul/JNetApplet"
|
||||||
|
clipboardHelper.selectAll()
|
||||||
|
clipboardHelper.copy()
|
||||||
|
copied = true
|
||||||
|
copyResetTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制图标:两个重叠的小圆角矩形
|
||||||
|
// 前层矩形白底压住后层边框重叠区,形成"堆叠纸张"的视觉效果
|
||||||
|
// 后层矩形(向右下偏移 2px)
|
||||||
|
Rectangle {
|
||||||
|
x: 8
|
||||||
|
y: 8
|
||||||
|
width: 10
|
||||||
|
height: 10
|
||||||
|
radius: 2
|
||||||
|
color: "#FFFFFF"
|
||||||
|
border.width: 1
|
||||||
|
border.color: copyRepoMouse.containsMouse ? "#333333" : "#999999"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前层矩形(左上),复制成功时边框短暂变绿
|
||||||
|
Rectangle {
|
||||||
|
x: 6
|
||||||
|
y: 6
|
||||||
|
width: 10
|
||||||
|
height: 10
|
||||||
|
radius: 2
|
||||||
|
color: "#FFFFFF"
|
||||||
|
border.width: 1
|
||||||
|
border.color: copied ? "#22A34A" : (copyRepoMouse.containsMouse ? "#333333" : "#999999")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹性空间:吸收多余高度,保证底部内边距固定为 16px
|
||||||
|
Item {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部内边距
|
||||||
|
Item {
|
||||||
|
Layout.preferredHeight: 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏的 TextEdit 用于复制仓库地址到剪贴板
|
||||||
|
// QML 没有直接剪贴板 API,用 TextEdit.selectAll()+copy() 实现
|
||||||
|
//(与 networkview.qml 中 clipboardHelper 同模式)
|
||||||
|
TextEdit {
|
||||||
|
id: clipboardHelper
|
||||||
|
visible: false
|
||||||
|
text: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制状态还原定时器:2 秒后将 copied 复位,图标与"已复制"提示恢复原状
|
||||||
|
Timer {
|
||||||
|
id: copyResetTimer
|
||||||
|
interval: 2000
|
||||||
|
onTriggered: root.copied = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,573 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 MyCompany
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
// 流量波动图窗口:屏幕居中的独立顶层窗口,展示当前活动接口最近 5 分钟的
|
||||||
|
// 网速趋势(下载/上传双折线图),随 C++ 后端 speedHistoryChanged 信号每秒动态刷新
|
||||||
|
// 设计要点:
|
||||||
|
// - 视觉风格复用 AboutWindow.qml(白色圆角卡片 12px、1px #E8E8E8 边框、
|
||||||
|
// 44px 标题栏、28x28 圆形关闭按钮 hover 淡红底)
|
||||||
|
// - 图表使用纯 QML Canvas 绘制,无 QtCharts 等外部依赖(与项目零依赖风格一致)
|
||||||
|
// - 数据通过属性注入:applet 即 networkview.qml 的 root.applet(C++ 后端对象),
|
||||||
|
// 为 null 时组件可独立预览(Canvas 显示"暂无历史数据"空状态)
|
||||||
|
// 触发方式:右键菜单"流量波动图" -> show()/raise()/requestActivate()
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
import QtQuick.Window 2.15
|
||||||
|
|
||||||
|
Window {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// 对外依赖:关闭按钮 hover 态文字高亮色,由父组件传入
|
||||||
|
// 默认值与 networkview.qml 中 root.accentRed 一致,确保独立可用
|
||||||
|
property color accentColor: Qt.rgba(220 / 255, 38 / 255, 38 / 255, 1)
|
||||||
|
|
||||||
|
// 对外依赖:C++ 后端对象(NetworkMonitorApplet),由 networkview.qml 传入
|
||||||
|
// 提供 speedHistoryDownload / speedHistoryUpload(QVariantList of QPointF,
|
||||||
|
// x=时间戳秒, y=速度 bytes/sec)、downloadSpeed / uploadSpeed、activeInterface
|
||||||
|
// 为 null 时组件可独立预览(显示空状态),所有数据访问处均需做空值兜底
|
||||||
|
property var applet: null
|
||||||
|
|
||||||
|
// Hover 状态:当前悬停采样点在历史缓冲中的索引,-1 表示无悬停
|
||||||
|
// 由图表区 MouseArea 根据 mouseX 反查最近采样点更新;Canvas 据此绘制
|
||||||
|
// 竖直辅助线与圆点标记,底部提示条据此显示该时刻数值
|
||||||
|
property int hoverIndex: -1
|
||||||
|
|
||||||
|
// Hover 提示文本:非空时底部提示条显示该内容,为空显示默认引导语
|
||||||
|
// 格式:"14:23:05 ↓856.00 KB/s ↑120.00 KB/s"
|
||||||
|
property string hoverHint: ""
|
||||||
|
|
||||||
|
// 窗口置顶状态:true 时 flags 附加 Qt.WindowStaysOnTopHint,窗口保持在
|
||||||
|
// 所有非置顶窗口之上(便于边下载大文件边观察曲线);由标题栏图钉按钮切换
|
||||||
|
property bool pinned: false
|
||||||
|
|
||||||
|
// 置顶状态变化时重绘图钉图标(未置顶=灰色轮廓,置顶=蓝色填充)
|
||||||
|
onPinnedChanged: pinIcon.requestPaint()
|
||||||
|
|
||||||
|
// 下载折线颜色(绿)与上传折线颜色(橙),与规范 §4.4 一致
|
||||||
|
readonly property color downloadLineColor: "#34C759"
|
||||||
|
readonly property color uploadLineColor: "#FF9500"
|
||||||
|
|
||||||
|
// 图表区内边距:左侧容纳 Y 轴刻度标签(如 "10 MB"),底部容纳 X 轴时间标签
|
||||||
|
// 定义为根属性,供 Canvas 绘制与 MouseArea 坐标换算共用,保证两处一致
|
||||||
|
readonly property int chartMarginLeft: 56
|
||||||
|
readonly property int chartMarginRight: 12
|
||||||
|
readonly property int chartMarginTop: 12
|
||||||
|
readonly property int chartMarginBottom: 26
|
||||||
|
|
||||||
|
width: 680
|
||||||
|
height: 420
|
||||||
|
visible: false
|
||||||
|
// flags 绑定 pinned:置顶时附加 WindowStaysOnTopHint;未置顶时该位为 0,
|
||||||
|
// 与原静态 flags 行为完全一致
|
||||||
|
flags: Qt.FramelessWindowHint | Qt.Window | (pinned ? Qt.WindowStaysOnTopHint : 0)
|
||||||
|
modality: Qt.NonModal
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) {
|
||||||
|
x = (Screen.width - width) / 2
|
||||||
|
y = (Screen.height - height) / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化速度显示(带单位,最小 KB,保留 2 位小数)
|
||||||
|
// 与 networkview.qml 的 formatSpeed 同实现;组件内自带一份保证独立可用
|
||||||
|
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 序列
|
||||||
|
// 设计原因:使刻度值为易读的整数(如 12345 -> 20000,80000 -> 100000),
|
||||||
|
// 避免 vMax * 1.2 的余量产生零碎刻度(如 14814)
|
||||||
|
function niceCeil(v) {
|
||||||
|
if (v <= 0) return 1024
|
||||||
|
var exp = Math.floor(Math.log(v) / Math.LN10)
|
||||||
|
var base = Math.pow(10, exp)
|
||||||
|
var f = v / base
|
||||||
|
var nf
|
||||||
|
if (f <= 1) nf = 1
|
||||||
|
else if (f <= 2) nf = 2
|
||||||
|
else if (f <= 5) nf = 5
|
||||||
|
else nf = 10
|
||||||
|
return nf * base
|
||||||
|
}
|
||||||
|
|
||||||
|
// Y 轴刻度单位选择:根据上限自动选择 B/KB/MB/GB,返回 {div, suffix}
|
||||||
|
// 设计原因:让刻度数值保持在 0~1000 的易读区间(如 200 KB 而非 204800 B)
|
||||||
|
function axisUnit(yMax) {
|
||||||
|
if (yMax >= 1024 * 1024 * 1024) return { div: 1024 * 1024 * 1024, suffix: "GB" }
|
||||||
|
if (yMax >= 1024 * 1024) return { div: 1024 * 1024, suffix: "MB" }
|
||||||
|
if (yMax >= 1024) return { div: 1024, suffix: "KB" }
|
||||||
|
return { div: 1, suffix: "B" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化单个 Y 轴刻度值:大值取整、小值保留 1 位小数,避免 "10.00" 这类冗余
|
||||||
|
function formatAxisValue(v, unit) {
|
||||||
|
var val = v / unit.div
|
||||||
|
var s
|
||||||
|
if (val >= 100) s = val.toFixed(0)
|
||||||
|
else if (val >= 10) s = val.toFixed(1)
|
||||||
|
else s = val.toFixed(val === 0 ? 0 : 1)
|
||||||
|
return s + " " + unit.suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
// 窗口主体:白色圆角卡片,1px 浅灰边框模拟 DTK 窗口描边(与 AboutWindow 一致)
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "#FFFFFF"
|
||||||
|
radius: 12
|
||||||
|
border.width: 1
|
||||||
|
border.color: "#E8E8E8"
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
// 自定义标题栏:左侧"流量波动图"标题 + 右侧置顶/关闭按钮,整栏可拖动窗口
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 44
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
// 拖动层:声明在关闭/置顶按钮之前(位于其下方),避免遮挡按钮点击与 hover
|
||||||
|
// 注意:drag.target 只对 Item 生效,对 Window 无效(原实现拖动不工作);
|
||||||
|
// 改用 startSystemMove() 系统级窗口拖动,这是 frameless Window 的正确做法,
|
||||||
|
// pressed 即触发,由窗口管理器接管移动,不干扰按钮的 hover/click
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.OpenHandCursor
|
||||||
|
onPressed: root.startSystemMove()
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 16
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: qsTr("流量波动图")
|
||||||
|
font.pixelSize: 15
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: "#333333"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 置顶按钮:28x28 圆形(与关闭按钮一致),位于关闭按钮左侧
|
||||||
|
// 三态视觉:未置顶=灰色图钉轮廓;置顶=deepin 蓝填充 + 淡蓝底;
|
||||||
|
// hover=淡灰底(置顶时淡蓝底优先)
|
||||||
|
Rectangle {
|
||||||
|
anchors.right: chartCloseButton.left
|
||||||
|
anchors.rightMargin: 4
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: 14
|
||||||
|
color: root.pinned ? Qt.rgba(0, 129 / 255, 1, 0.12)
|
||||||
|
: (pinMouse.containsMouse ? Qt.rgba(0, 0, 0, 0.06) : "transparent")
|
||||||
|
|
||||||
|
// 置顶图标:Canvas 绘制"上箭头触顶"(⤒ 风格)——向上箭头指向顶部横杠,
|
||||||
|
// 是"置顶/移到顶部"最通用的视觉语言,与关闭按钮"×"形态区分明显
|
||||||
|
// 未置顶:灰色细线描边;置顶:蓝色加粗 + 箭头头部实心填充,激活态更醒目
|
||||||
|
Canvas {
|
||||||
|
id: pinIcon
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 16
|
||||||
|
height: 16
|
||||||
|
|
||||||
|
onPaint: {
|
||||||
|
var ctx = getContext("2d")
|
||||||
|
ctx.clearRect(0, 0, width, height)
|
||||||
|
var c = root.pinned ? "#0081FF" : "#999999"
|
||||||
|
ctx.strokeStyle = c
|
||||||
|
ctx.fillStyle = c
|
||||||
|
ctx.lineCap = "round"
|
||||||
|
ctx.lineJoin = "round"
|
||||||
|
ctx.lineWidth = root.pinned ? 2 : 1.5
|
||||||
|
|
||||||
|
// 顶部横杠:y=3,x 3..13
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(3, 3)
|
||||||
|
ctx.lineTo(13, 3)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// 箭头主干:自底部 (8,13) 向上至 (8,7),
|
||||||
|
// 与横杠间留 ~1.5px 间隙,保持"触顶"的意象清晰
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(8, 13)
|
||||||
|
ctx.lineTo(8, 7)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
if (root.pinned) {
|
||||||
|
// 置顶:箭头头部实心三角填充,与描边态形成明确视觉差异
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(8, 5.2)
|
||||||
|
ctx.lineTo(4.6, 9)
|
||||||
|
ctx.lineTo(11.4, 9)
|
||||||
|
ctx.closePath()
|
||||||
|
ctx.fill()
|
||||||
|
} else {
|
||||||
|
// 未置顶:左右两片箭头羽,自顶端 (8,5.5) 向两肩展开
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(8, 5.5)
|
||||||
|
ctx.lineTo(5, 8.5)
|
||||||
|
ctx.moveTo(8, 5.5)
|
||||||
|
ctx.lineTo(11, 8.5)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: pinMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.pinned = !root.pinned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭按钮:28x28 圆形,hover 时淡红底 + 红色 ×(颜色由 accentColor 决定)
|
||||||
|
Rectangle {
|
||||||
|
id: chartCloseButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: 14
|
||||||
|
color: chartCloseMouse.containsMouse ? Qt.rgba(220 / 255, 38 / 255, 38 / 255, 0.15) : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "×"
|
||||||
|
font.pixelSize: 20
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: chartCloseMouse.containsMouse ? root.accentColor : "#666666"
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: chartCloseMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 顶部状态条:实时下行/上行速度(箭头颜色与折线一致,兼作图例)
|
||||||
|
// + 右侧活动接口名与时间窗口标签
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 30
|
||||||
|
Layout.leftMargin: 16
|
||||||
|
Layout.rightMargin: 16
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "↓"
|
||||||
|
font.pixelSize: 14
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: root.downloadLineColor
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
// applet 为空时兜底为 0,保证独立预览不报错
|
||||||
|
text: root.formatSpeed(root.applet ? root.applet.downloadSpeed : 0)
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#333333"
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.preferredWidth: 10 }
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "↑"
|
||||||
|
font.pixelSize: 14
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: root.uploadLineColor
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.formatSpeed(root.applet ? root.applet.uploadSpeed : 0)
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#333333"
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: (root.applet && root.applet.activeInterface
|
||||||
|
? root.applet.activeInterface : "—") + " · 5min"
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: "#999999"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图表区:Canvas 绘制双折线 + 网格 + 坐标轴 + Hover 辅助线
|
||||||
|
// visible 绑定窗口可见性:弹窗关闭时不绘制(C++ 后端仍继续采集)
|
||||||
|
Canvas {
|
||||||
|
id: canvas
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.leftMargin: 8
|
||||||
|
Layout.rightMargin: 8
|
||||||
|
visible: root.visible
|
||||||
|
|
||||||
|
onWidthChanged: requestPaint()
|
||||||
|
onHeightChanged: requestPaint()
|
||||||
|
|
||||||
|
onPaint: {
|
||||||
|
var ctx = getContext("2d")
|
||||||
|
ctx.clearRect(0, 0, width, height)
|
||||||
|
|
||||||
|
// 绘图区几何(与 chartHover 坐标换算共用同一组边距)
|
||||||
|
var pl = root.chartMarginLeft
|
||||||
|
var pr = root.chartMarginRight
|
||||||
|
var pt = root.chartMarginTop
|
||||||
|
var pb = root.chartMarginBottom
|
||||||
|
var pw = width - pl - pr
|
||||||
|
var ph = height - pt - pb
|
||||||
|
if (pw <= 0 || ph <= 0) return
|
||||||
|
|
||||||
|
// 取历史数据;applet 为空时视为空缓冲(走空状态分支)
|
||||||
|
var dl = root.applet ? root.applet.speedHistoryDownload : []
|
||||||
|
var ul = root.applet ? root.applet.speedHistoryUpload : []
|
||||||
|
var n = dl.length
|
||||||
|
|
||||||
|
// ---- Y 轴上限计算(规范 §4.4)----
|
||||||
|
// 1. vMax = 所有采样点 max(download, upload)
|
||||||
|
// 2. vMax == 0 时上限取 1024(1 KB/s)避免除零
|
||||||
|
// 3. 否则取 vMax * 1.2 向上取整到 1/2/5 × 10^n 序列
|
||||||
|
var vMax = 0
|
||||||
|
var i
|
||||||
|
for (i = 0; i < n; i++) {
|
||||||
|
if (dl[i].y > vMax) vMax = dl[i].y
|
||||||
|
if (i < ul.length && ul[i].y > vMax) vMax = ul[i].y
|
||||||
|
}
|
||||||
|
var yMax = vMax <= 0 ? 1024 : root.niceCeil(vMax * 1.2)
|
||||||
|
var unit = root.axisUnit(yMax)
|
||||||
|
|
||||||
|
// ---- 水平网格线与 Y 轴刻度([0, yMax] 均分 4 段)----
|
||||||
|
ctx.font = "10px sans-serif"
|
||||||
|
ctx.textBaseline = "middle"
|
||||||
|
var g, gy
|
||||||
|
for (g = 0; g <= 4; g++) {
|
||||||
|
gy = pt + ph - (ph * g / 4)
|
||||||
|
// +0.5 让 1px 线条落在像素边界上,避免抗锯齿发虚
|
||||||
|
// 基线(g=0)略深一档,其余 4 条网格线用规范色 #E5E5E5
|
||||||
|
ctx.strokeStyle = g === 0 ? "#DDDDDD" : "#E5E5E5"
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(pl, gy + 0.5)
|
||||||
|
ctx.lineTo(pl + pw, gy + 0.5)
|
||||||
|
ctx.stroke()
|
||||||
|
// 刻度标签右对齐于绘图区左侧
|
||||||
|
ctx.fillStyle = "#999999"
|
||||||
|
ctx.textAlign = "right"
|
||||||
|
ctx.fillText(root.formatAxisValue(yMax * g / 4, unit), pl - 8, gy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- X 轴刻度:每分钟一个标签,均布于绘图区 ----
|
||||||
|
// 5 分钟窗口用 -5m/-4m/-3m/-2m/-1m/now 共 6 个标签,比 4 个三分点
|
||||||
|
// 的 -3m20s/-1m40s 更易读;循环按数组长度参数化,两端左/右对齐
|
||||||
|
var xLabels = ["-5m", "-4m", "-3m", "-2m", "-1m", "now"]
|
||||||
|
ctx.fillStyle = "#999999"
|
||||||
|
ctx.textBaseline = "alphabetic"
|
||||||
|
for (i = 0; i < xLabels.length; i++) {
|
||||||
|
var lx = pl + pw * i / (xLabels.length - 1)
|
||||||
|
// 两端标签分别左/右对齐,避免文字越出卡片边缘被裁剪
|
||||||
|
ctx.textAlign = i === 0 ? "left" : (i === xLabels.length - 1 ? "right" : "center")
|
||||||
|
ctx.fillText(xLabels[i], lx, height - 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 空状态:画完空网格后中央提示,不画折线(规范 §5)----
|
||||||
|
if (n === 0) {
|
||||||
|
ctx.fillStyle = "#999999"
|
||||||
|
ctx.font = "13px sans-serif"
|
||||||
|
ctx.textAlign = "center"
|
||||||
|
ctx.textBaseline = "middle"
|
||||||
|
ctx.fillText(qsTr("暂无历史数据"), pl + pw / 2, pt + ph / 2)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 折线绘制 ----
|
||||||
|
// X 轴固定为 5 分钟窗口 [tMax-300, tMax]:右缘始终对齐"now",
|
||||||
|
// 数据不足 5 分钟时曲线从右侧向左生长,左侧留空网格。
|
||||||
|
// 设计原因:若按实际数据范围 [dl[0].x, tMax] 映射,刚启动时仅有
|
||||||
|
// 几秒数据会被拉伸到整个绘图宽度,X 轴标签 -5m/-4m/... 与实际范围
|
||||||
|
// 严重不符,用户看到的是"几秒趋势图"而非"5 分钟趋势图"。
|
||||||
|
// 300 = 5 分钟 × 60 秒,与 C++ MAX_HISTORY_SAMPLES 一致
|
||||||
|
var tMax = dl[n - 1].x
|
||||||
|
var tMin = tMax - 300
|
||||||
|
var tRange = 300
|
||||||
|
|
||||||
|
// 单点边界:只有 1 个采样点时无法连线,画一个圆点。
|
||||||
|
// 固定窗口下单点对齐 now(右缘),而非居中
|
||||||
|
if (n === 1) {
|
||||||
|
var sx = pl + pw
|
||||||
|
var sy = pt + ph - (dl[0].y / yMax) * ph
|
||||||
|
ctx.fillStyle = "#34C759"
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(sx, sy, 3, 0, 2 * Math.PI)
|
||||||
|
ctx.fill()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制一条序列折线:先填充线下淡色区域增强可读性,再描边 2px 主线
|
||||||
|
// lineCap/lineJoin 用 round 使折线端点与转角圆润
|
||||||
|
// 注意每次 stroke/fill 前必须 beginPath(),避免与上一段路径串连
|
||||||
|
function drawSeries(data, colorCss, fillCss) {
|
||||||
|
var m = data.length
|
||||||
|
if (m === 0) return
|
||||||
|
var j, px, py
|
||||||
|
// 线下淡色填充(alpha 0.08):贴到基线形成闭合区域
|
||||||
|
ctx.beginPath()
|
||||||
|
for (j = 0; j < m; j++) {
|
||||||
|
px = pl + ((data[j].x - tMin) / tRange) * pw
|
||||||
|
py = pt + ph - (data[j].y / yMax) * ph
|
||||||
|
if (j === 0) ctx.moveTo(px, py)
|
||||||
|
else ctx.lineTo(px, py)
|
||||||
|
}
|
||||||
|
ctx.lineTo(pl + ((data[m - 1].x - tMin) / tRange) * pw, pt + ph)
|
||||||
|
ctx.lineTo(pl + ((data[0].x - tMin) / tRange) * pw, pt + ph)
|
||||||
|
ctx.closePath()
|
||||||
|
ctx.fillStyle = fillCss
|
||||||
|
ctx.fill()
|
||||||
|
// 主线:2px 描边
|
||||||
|
ctx.beginPath()
|
||||||
|
for (j = 0; j < m; j++) {
|
||||||
|
px = pl + ((data[j].x - tMin) / tRange) * pw
|
||||||
|
py = pt + ph - (data[j].y / yMax) * ph
|
||||||
|
if (j === 0) ctx.moveTo(px, py)
|
||||||
|
else ctx.lineTo(px, py)
|
||||||
|
}
|
||||||
|
ctx.strokeStyle = colorCss
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
ctx.lineCap = "round"
|
||||||
|
ctx.lineJoin = "round"
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先画上传(橙)再画下载(绿):下载通常是主视线,后画置于上层
|
||||||
|
drawSeries(ul, "#FF9500", "rgba(255, 149, 0, 0.08)")
|
||||||
|
drawSeries(dl, "#34C759", "rgba(52, 199, 89, 0.08)")
|
||||||
|
|
||||||
|
// ---- Hover:竖直辅助线 + 两条折线上的圆点标记 ----
|
||||||
|
// hoverIndex 由 chartHover 根据 mouseX 反查更新;此处按索引换算坐标
|
||||||
|
var hi = root.hoverIndex
|
||||||
|
if (hi >= 0 && hi < n) {
|
||||||
|
var hx = pl + ((dl[hi].x - tMin) / tRange) * pw
|
||||||
|
// 竖直辅助线:虚线,避免与实线折线混淆
|
||||||
|
ctx.strokeStyle = "#BBBBBB"
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.setLineDash([4, 3])
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(hx, pt)
|
||||||
|
ctx.lineTo(hx, pt + ph)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.setLineDash([])
|
||||||
|
// 圆点标记:白芯 + 2px 彩色描边,在折线上形成清晰锚点
|
||||||
|
function drawDot(yVal, colorCss) {
|
||||||
|
var hy = pt + ph - (yVal / yMax) * ph
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(hx, hy, 4, 0, 2 * Math.PI)
|
||||||
|
ctx.fillStyle = "#FFFFFF"
|
||||||
|
ctx.fill()
|
||||||
|
ctx.strokeStyle = colorCss
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
drawDot(dl[hi].y, "#34C759")
|
||||||
|
if (hi < ul.length) drawDot(ul[hi].y, "#FF9500")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hover 交互层:横向移动时按 mouseX 反查最近采样点
|
||||||
|
// 设计原因:采样点按时间均匀分布(1Hz),先由 X 坐标线性反推时间戳,
|
||||||
|
// 再线性扫描找最近点;300 点上限下扫描开销可忽略,无需二分
|
||||||
|
MouseArea {
|
||||||
|
id: chartHover
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
|
||||||
|
onPositionChanged: {
|
||||||
|
if (!root.applet) return
|
||||||
|
var dl = root.applet.speedHistoryDownload
|
||||||
|
var n = dl.length
|
||||||
|
if (n === 0) return
|
||||||
|
var pl = root.chartMarginLeft
|
||||||
|
var pw = width - pl - root.chartMarginRight
|
||||||
|
if (pw <= 0) return
|
||||||
|
// X 轴用与绘制一致的固定 5 分钟窗口 [tMax-300, tMax] 反推时间戳,
|
||||||
|
// 保证 Hover 辅助线与折线上的采样点严格对齐
|
||||||
|
var tMax = dl[n - 1].x
|
||||||
|
var tMin = tMax - 300
|
||||||
|
var tRange = 300
|
||||||
|
// 将 mouseX 限制在绘图区内,再线性反推时间戳
|
||||||
|
var clampedX = Math.max(pl, Math.min(mouse.x, pl + pw))
|
||||||
|
var target = tMin + (clampedX - pl) / pw * tRange
|
||||||
|
var best = 0
|
||||||
|
var bestDist = Math.abs(dl[0].x - target)
|
||||||
|
for (var i = 1; i < n; i++) {
|
||||||
|
var d = Math.abs(dl[i].x - target)
|
||||||
|
if (d < bestDist) {
|
||||||
|
bestDist = d
|
||||||
|
best = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root.hoverIndex = best
|
||||||
|
// 更新底部提示条:时间 + 下行/上行速度
|
||||||
|
var ul = root.applet.speedHistoryUpload
|
||||||
|
var idx = root.hoverIndex
|
||||||
|
var upVal = idx < ul.length ? ul[idx].y : 0
|
||||||
|
root.hoverHint = Qt.formatDateTime(new Date(dl[idx].x * 1000), "HH:mm:ss")
|
||||||
|
+ " ↓" + root.formatSpeed(dl[idx].y)
|
||||||
|
+ " ↑" + root.formatSpeed(upVal)
|
||||||
|
canvas.requestPaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: {
|
||||||
|
root.hoverIndex = -1
|
||||||
|
root.hoverHint = ""
|
||||||
|
canvas.requestPaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部提示条:默认显示操作引导;Hover 时显示该时刻的时间与上下行速度
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 30
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: root.hoverHint.length > 0
|
||||||
|
? root.hoverHint : qsTr("鼠标移到曲线上查看详情")
|
||||||
|
font.pixelSize: 11
|
||||||
|
color: root.hoverHint.length > 0 ? "#333333" : "#999999"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态刷新:C++ 每秒追加采样点时发射 speedHistoryChanged,触发 Canvas 重绘
|
||||||
|
// 注意:此处不重置 Hover 状态,否则用户悬停时提示会随每秒刷新闪断;
|
||||||
|
// 缓冲满后旧点滑出导致的索引漂移由 onPaint 中 hi < n 的边界判断兜底
|
||||||
|
Connections {
|
||||||
|
target: root.applet
|
||||||
|
function onSpeedHistoryChanged() {
|
||||||
|
canvas.requestPaint()
|
||||||
|
}
|
||||||
|
// 接口切换时历史整体更换,原 Hover 索引失去意义,需清除避免残留标记
|
||||||
|
function onActiveInterfaceChanged() {
|
||||||
|
root.hoverIndex = -1
|
||||||
|
root.hoverHint = ""
|
||||||
|
canvas.requestPaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"Plugin": {
|
"Plugin": {
|
||||||
"Version": "1.0",
|
"Version": "@PROJECT_VERSION@",
|
||||||
"Id": "space.jokul.JNetApplet",
|
"Id": "space.jokul.JNetApplet",
|
||||||
"Url": "networkview.qml",
|
"Url": "networkview.qml",
|
||||||
"Parent": "org.deepin.ds.dock",
|
"Parent": "org.deepin.ds.dock",
|
||||||
+122
-166
@@ -10,17 +10,23 @@ import Qt.labs.platform as Platform
|
|||||||
import org.deepin.ds 1.0
|
import org.deepin.ds 1.0
|
||||||
import org.deepin.ds.dock 1.0
|
import org.deepin.ds.dock 1.0
|
||||||
import org.deepin.dtk 1.0
|
import org.deepin.dtk 1.0
|
||||||
|
import "components"
|
||||||
|
|
||||||
AppletItem {
|
AppletItem {
|
||||||
id: root
|
id: root
|
||||||
objectName: "network monitor applet"
|
objectName: "network monitor applet"
|
||||||
property int dockOrder: 21
|
property int dockOrder: 21
|
||||||
property int dockSize: Panel.rootObject.dockItemMaxSize || 48
|
property int dockSize: Panel.rootObject.dockItemMaxSize || 48
|
||||||
|
// 任务栏方向:0=Top, 1=Right, 2=Bottom, 3=Left;% 2 为 0 表示水平,1 表示竖向
|
||||||
|
// 设计原因:dde-shell 规范用法,org.deepin.ds.dock 已在 line 11 导入
|
||||||
|
property bool isVerticalDock: Panel.position % 2 === 1
|
||||||
|
|
||||||
// 任务栏宽度稍宽于标准 dock 尺寸,容纳双行速度数值
|
// 水平任务栏:宽度稍宽容纳双行数值,高度匹配 dock 尺寸
|
||||||
// 设计原因:固定宽度避免数值长度变化导致插件宽度抖动、箭头位置漂移
|
// 竖向任务栏:宽度匹配 dock 尺寸(~40px),高度加大容纳竖排字符
|
||||||
implicitWidth: Math.round(dockSize * 1.35)
|
// 2.4 倍:dockSize=40 时约 96px,可容纳最多 8 字符的速度字符串(如 "1023.99K")
|
||||||
implicitHeight: dockSize
|
// 增大字号后需更高以容纳竖排字符
|
||||||
|
implicitWidth: isVerticalDock ? dockSize : Math.round(dockSize * 1.35)
|
||||||
|
implicitHeight: isVerticalDock ? Math.round(dockSize * 2.4) : dockSize
|
||||||
|
|
||||||
readonly property var applet: Applet
|
readonly property var applet: Applet
|
||||||
readonly property bool ready: applet ? applet.ready : false
|
readonly property bool ready: applet ? applet.ready : false
|
||||||
@@ -31,6 +37,8 @@ AppletItem {
|
|||||||
readonly property string activeInterface: applet ? (applet.activeInterface || "") : ""
|
readonly property string activeInterface: applet ? (applet.activeInterface || "") : ""
|
||||||
// 活动接口的 IPv4 地址,由 C++ 后端 detectIpAddress 持续更新
|
// 活动接口的 IPv4 地址,由 C++ 后端 detectIpAddress 持续更新
|
||||||
readonly property string ipAddress: applet ? (applet.ipAddress || "") : ""
|
readonly property string ipAddress: applet ? (applet.ipAddress || "") : ""
|
||||||
|
// 活动接口的 IPv6 全球地址,由 C++ 后端 detectIpAddress 持续更新
|
||||||
|
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 || []) : []
|
readonly property var interfaceStats: applet ? (applet.interfaceStats || []) : []
|
||||||
|
|
||||||
@@ -132,6 +140,7 @@ AppletItem {
|
|||||||
// 不给每行固定高度,让 RowLayout 按内容自然高度排列,
|
// 不给每行固定高度,让 RowLayout 按内容自然高度排列,
|
||||||
// 整组垂直居中于 dock 区域,避免两行间出现过大间隙
|
// 整组垂直居中于 dock 区域,避免两行间出现过大间隙
|
||||||
Column {
|
Column {
|
||||||
|
visible: !root.isVerticalDock
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: 4
|
anchors.leftMargin: 4
|
||||||
@@ -143,16 +152,16 @@ AppletItem {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "↓"
|
text: "↓"
|
||||||
font.pixelSize: root.dockSize * 0.18
|
font.pixelSize: root.dockSize * 0.20
|
||||||
color: root.secondaryText
|
color: root.secondaryText
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: root.formatSpeedShort(root.downloadSpeed)
|
text: root.formatSpeedShort(root.downloadSpeed)
|
||||||
font.pixelSize: root.dockSize * 0.22
|
font.pixelSize: root.dockSize * 0.25
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
color: root.downloadValueColor
|
color: root.primaryText
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
horizontalAlignment: Text.AlignLeft
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
@@ -164,28 +173,93 @@ AppletItem {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "↑"
|
text: "↑"
|
||||||
font.pixelSize: root.dockSize * 0.18
|
font.pixelSize: root.dockSize * 0.20
|
||||||
color: root.secondaryText
|
color: root.secondaryText
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: root.formatSpeedShort(root.uploadSpeed)
|
text: root.formatSpeedShort(root.uploadSpeed)
|
||||||
font.pixelSize: root.dockSize * 0.22
|
font.pixelSize: root.dockSize * 0.25
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
color: root.uploadValueColor
|
color: root.primaryText
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
horizontalAlignment: Text.AlignLeft
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 悬停提示:网卡名 + IP 地址一行展示
|
// 竖向任务栏布局:双列字符竖排,每列一个方向(下载|上传)
|
||||||
// PanelToolTip 不支持 contentItem 覆盖和文本对齐,用默认左对齐 text 属性
|
// 设计原因:竖向任务栏宽度仅 ~40px,水平布局的 "↓6.64K" 约 35px 勉强容纳但
|
||||||
|
// 数值长度变化时易裁切;逐字符竖排每列仅 ~10px,双列 ~25px,舒适容纳且视觉对称
|
||||||
|
RowLayout {
|
||||||
|
visible: root.isVerticalDock
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.topMargin: 4
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
// 下载列:箭头在上,数值字符从上往下竖排
|
||||||
|
Column {
|
||||||
|
spacing: 0
|
||||||
|
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "↓"
|
||||||
|
font.pixelSize: root.dockSize * 0.20
|
||||||
|
color: root.secondaryText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
// 将 "6.64K" 拆为 ["6", ".", "6", "4", "K"] 逐字符渲染
|
||||||
|
// 属性绑定:downloadSpeed 变化时 formatSpeedShort 重算,split 自动刷新 model
|
||||||
|
model: root.formatSpeedShort(root.downloadSpeed).split('')
|
||||||
|
Text {
|
||||||
|
text: modelData
|
||||||
|
font.pixelSize: root.dockSize * 0.25
|
||||||
|
height: font.pixelSize
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: root.primaryText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传列:结构与下载列对称,颜色用 uploadValueColor
|
||||||
|
Column {
|
||||||
|
spacing: 0
|
||||||
|
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "↑"
|
||||||
|
font.pixelSize: root.dockSize * 0.20
|
||||||
|
color: root.secondaryText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.formatSpeedShort(root.uploadSpeed).split('')
|
||||||
|
Text {
|
||||||
|
text: modelData
|
||||||
|
font.pixelSize: root.dockSize * 0.25
|
||||||
|
height: font.pixelSize
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: root.primaryText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 悬停提示:网卡名 + IPv4 单行,IPv6 在第二行(仅有 IPv6 时追加)
|
||||||
|
// 设计原因:IPv6 地址较长,单行容纳不下;无 IPv6 时保持单行与原视觉一致
|
||||||
PanelToolTip {
|
PanelToolTip {
|
||||||
id: toolTip
|
id: toolTip
|
||||||
text: root.ready
|
text: root.ready
|
||||||
? (root.activeInterface + " · " + qsTr("IP地址:") + (root.ipAddress || qsTr("无")))
|
? (root.activeInterface
|
||||||
|
+ " · IPv4:" + (root.ipAddress || qsTr("无"))
|
||||||
|
+ (root.ipv6Address ? "\nIPv6:" + root.ipv6Address : ""))
|
||||||
: qsTr("未检测到网络接口")
|
: qsTr("未检测到网络接口")
|
||||||
toolTipX: DockPanelPositioner.x
|
toolTipX: DockPanelPositioner.x
|
||||||
toolTipY: DockPanelPositioner.y
|
toolTipY: DockPanelPositioner.y
|
||||||
@@ -267,8 +341,9 @@ AppletItem {
|
|||||||
// 网卡名 + IP 地址卡片背景
|
// 网卡名 + IP 地址卡片背景
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
Layout.preferredWidth: 180
|
Layout.preferredWidth: 300
|
||||||
Layout.preferredHeight: 40
|
// 有 IPv6 时增高以容纳第三行;无 IPv6 时保持原 40px
|
||||||
|
Layout.preferredHeight: root.ipv6Address ? 54 : 40
|
||||||
color: root.cardBackground
|
color: root.cardBackground
|
||||||
radius: 8
|
radius: 8
|
||||||
border.width: 1
|
border.width: 1
|
||||||
@@ -287,12 +362,21 @@ AppletItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: root.ipAddress ? qsTr("IP地址:") + root.ipAddress : ""
|
text: root.ipAddress ? qsTr("IPv4:") + root.ipAddress : ""
|
||||||
font.pixelSize: 13
|
font.pixelSize: 13
|
||||||
color: root.secondaryText
|
color: root.secondaryText
|
||||||
visible: root.ipAddress !== ""
|
visible: root.ipAddress !== ""
|
||||||
Layout.alignment: Qt.AlignHCenter
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -578,6 +662,16 @@ AppletItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 流量波动图:屏幕居中独立窗口,展示活动接口最近 30 分钟网速趋势
|
||||||
|
Platform.MenuItem {
|
||||||
|
text: qsTr("流量波动图")
|
||||||
|
onTriggered: {
|
||||||
|
trafficChartWindow.show()
|
||||||
|
trafficChartWindow.raise()
|
||||||
|
trafficChartWindow.requestActivate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Platform.MenuSeparator {}
|
Platform.MenuSeparator {}
|
||||||
|
|
||||||
Platform.MenuItem {
|
Platform.MenuItem {
|
||||||
@@ -590,158 +684,20 @@ AppletItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关于窗口:独立顶层窗口,在桌面中间弹出,展示作者信息
|
// 关于窗口:抽取为独立组件 package/components/AboutWindow.qml
|
||||||
// 与设置窗口同样的 frameless + 自定义标题栏模式
|
// 依赖通过属性传入:accentColor = root.accentRed,version 取自 C++ 后端 applet.version
|
||||||
Window {
|
AboutWindow {
|
||||||
id: aboutWindow
|
id: aboutWindow
|
||||||
width: 320
|
accentColor: root.accentRed
|
||||||
height: 230
|
version: root.applet ? root.applet.version : "1.0"
|
||||||
visible: false
|
}
|
||||||
flags: Qt.FramelessWindowHint | Qt.Window
|
|
||||||
modality: Qt.NonModal
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
// 流量波动图窗口:屏幕居中独立窗口,展示活动接口最近 30 分钟网速趋势
|
||||||
if (visible) {
|
// 依赖通过属性传入:accentColor = root.accentRed,applet = root.applet
|
||||||
x = (Screen.width - width) / 2
|
TrafficChartWindow {
|
||||||
y = (Screen.height - height) / 2
|
id: trafficChartWindow
|
||||||
}
|
accentColor: root.accentRed
|
||||||
}
|
applet: root.applet
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: "#f5f5f5"
|
|
||||||
radius: 12
|
|
||||||
border.width: 1
|
|
||||||
border.color: "#e0e0e0"
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 0
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
// 自定义标题栏(可拖动)
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: 44
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
drag.target: aboutWindow
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 16
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: qsTr("关于")
|
|
||||||
font.pixelSize: 15
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: "#333333"
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: 12
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 28
|
|
||||||
height: 28
|
|
||||||
radius: 14
|
|
||||||
color: aboutCloseMouse.containsMouse ? Qt.rgba(220/255, 38/255, 38/255, 0.15) : "transparent"
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "×"
|
|
||||||
font.pixelSize: 20
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: aboutCloseMouse.containsMouse ? root.accentRed : "#666666"
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: aboutCloseMouse
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: aboutWindow.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 内容区
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
anchors.margins: 0
|
|
||||||
spacing: 10
|
|
||||||
Layout.leftMargin: 24
|
|
||||||
Layout.rightMargin: 24
|
|
||||||
Layout.topMargin: 4
|
|
||||||
|
|
||||||
// 标题组:插件名 + 英文名紧凑排列
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: 2
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("网络速度监控")
|
|
||||||
font.pixelSize: 18
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: "#333333"
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "JNetApplet"
|
|
||||||
font.pixelSize: 12
|
|
||||||
color: "#999999"
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分隔线
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: 1
|
|
||||||
color: "#e0e0e0"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 信息行
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: 6
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("版本:1.0")
|
|
||||||
font.pixelSize: 12
|
|
||||||
color: "#666666"
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("作者:Jokul")
|
|
||||||
font.pixelSize: 12
|
|
||||||
color: "#666666"
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("描述:监控网络速度和流量")
|
|
||||||
font.pixelSize: 12
|
|
||||||
color: "#666666"
|
|
||||||
Layout.fillWidth: true
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("仓库:git.jokul.space/Jokul/JNetApplet")
|
|
||||||
font.pixelSize: 11
|
|
||||||
color: "#999999"
|
|
||||||
Layout.fillWidth: true
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置窗口:独立顶层窗口,在桌面中间弹出
|
// 设置窗口:独立顶层窗口,在桌面中间弹出
|
||||||
|
|||||||
@@ -110,6 +110,54 @@ QString NetworkMonitorApplet::ipAddress() const
|
|||||||
return m_ipAddress;
|
return m_ipAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 返回活动接口的 IPv6 全球地址,供 QML 显示
|
||||||
|
QString NetworkMonitorApplet::ipv6Address() const
|
||||||
|
{
|
||||||
|
return m_ipv6Address;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回插件版本号,从 dde-shell 插件元数据(metadata.json)读取
|
||||||
|
// 版本唯一源为 CMakeLists.txt 的 project(VERSION),经 configure_file 写入 metadata.json,
|
||||||
|
// dde-shell 加载时存入 DPluginMetaData,此处通过继承自 DApplet 的 pluginMetaData() 读取
|
||||||
|
QString NetworkMonitorApplet::version() const
|
||||||
|
{
|
||||||
|
// DPluginMetaData::value() 已在 "Plugin" 层级内部查找(源码:frame/pluginmetadata.cpp)
|
||||||
|
// metadata.json 的 { "Plugin": { "Version": "x.y.z" } } 被 DPluginMetaData 加载后,
|
||||||
|
// value("Version") 直接返回版本号,无需再 .value("Plugin").toMap() 展开
|
||||||
|
return pluginMetaData().value("Version").toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回当前活动接口的下行速度历史,转换为 QVariantList of QPointF 供 QML 使用
|
||||||
|
// 设计原因:QPointF 仅携带 x/y 两个值,上传与下载分别对应两个列表
|
||||||
|
QVariantList NetworkMonitorApplet::speedHistoryDownload() const
|
||||||
|
{
|
||||||
|
QVariantList list;
|
||||||
|
if (m_activeInterface.isEmpty() || !m_speedHistory.contains(m_activeInterface)) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
const QVector<SpeedSample> &samples = m_speedHistory[m_activeInterface];
|
||||||
|
list.reserve(samples.size());
|
||||||
|
for (const SpeedSample &s : samples) {
|
||||||
|
list.append(QPointF(s.timestamp, s.downloadSpeed));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回当前活动接口的上行速度历史,结构同 speedHistoryDownload
|
||||||
|
QVariantList NetworkMonitorApplet::speedHistoryUpload() const
|
||||||
|
{
|
||||||
|
QVariantList list;
|
||||||
|
if (m_activeInterface.isEmpty() || !m_speedHistory.contains(m_activeInterface)) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
const QVector<SpeedSample> &samples = m_speedHistory[m_activeInterface];
|
||||||
|
list.reserve(samples.size());
|
||||||
|
for (const SpeedSample &s : samples) {
|
||||||
|
list.append(QPointF(s.timestamp, s.uploadSpeed));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
void NetworkMonitorApplet::refresh()
|
void NetworkMonitorApplet::refresh()
|
||||||
{
|
{
|
||||||
readNetworkStats();
|
readNetworkStats();
|
||||||
@@ -121,6 +169,8 @@ void NetworkMonitorApplet::setActiveInterface(const QString &interface)
|
|||||||
m_activeInterface = interface;
|
m_activeInterface = interface;
|
||||||
m_firstUpdate = true;
|
m_firstUpdate = true;
|
||||||
emit activeInterfaceChanged();
|
emit activeInterfaceChanged();
|
||||||
|
// 接口切换后通知 QML 趋势图切换显示新接口的历史数据
|
||||||
|
emit speedHistoryChanged();
|
||||||
// 接口切换后立即检测新接口的 IP 地址
|
// 接口切换后立即检测新接口的 IP 地址
|
||||||
detectIpAddress();
|
detectIpAddress();
|
||||||
}
|
}
|
||||||
@@ -173,7 +223,16 @@ void NetworkMonitorApplet::readNetworkStats()
|
|||||||
if (newInterfaces.keys() != m_interfaces.keys()) {
|
if (newInterfaces.keys() != m_interfaces.keys()) {
|
||||||
m_interfaceList = newInterfaces.keys();
|
m_interfaceList = newInterfaces.keys();
|
||||||
m_interfaces = newInterfaces;
|
m_interfaces = newInterfaces;
|
||||||
|
|
||||||
|
// 清理已消失接口的历史缓冲,避免内存泄漏
|
||||||
|
// 设计原因:USB 网卡、VPN 隧道等接口可能随时消失,其历史数据不再需要
|
||||||
|
const QStringList oldIfaces = m_speedHistory.keys();
|
||||||
|
for (const QString &name : oldIfaces) {
|
||||||
|
if (!newInterfaces.contains(name)) {
|
||||||
|
m_speedHistory.remove(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 自动选择活动接口
|
// 自动选择活动接口
|
||||||
if (m_activeInterface.isEmpty() && !m_interfaceList.isEmpty()) {
|
if (m_activeInterface.isEmpty() && !m_interfaceList.isEmpty()) {
|
||||||
// 优先选择物理网卡(wlp/wlan/enp/eth)中有流量的接口,
|
// 优先选择物理网卡(wlp/wlan/enp/eth)中有流量的接口,
|
||||||
@@ -238,7 +297,26 @@ void NetworkMonitorApplet::calculateSpeed()
|
|||||||
|
|
||||||
m_lastRxBytes = currentRxBytes;
|
m_lastRxBytes = currentRxBytes;
|
||||||
m_lastTxBytes = currentTxBytes;
|
m_lastTxBytes = currentTxBytes;
|
||||||
|
|
||||||
|
// 追加采样点到当前活动接口的环形缓冲
|
||||||
|
// 设计原因:applet 启动即持续采集,用户随时打开趋势图都能看到完整 5 分钟数据
|
||||||
|
if (!m_activeInterface.isEmpty()) {
|
||||||
|
SpeedSample sample;
|
||||||
|
sample.timestamp = QDateTime::currentSecsSinceEpoch();
|
||||||
|
sample.downloadSpeed = static_cast<double>(m_downloadSpeed);
|
||||||
|
sample.uploadSpeed = static_cast<double>(m_uploadSpeed);
|
||||||
|
m_speedHistory[m_activeInterface].append(sample);
|
||||||
|
|
||||||
|
// 滑动窗口裁剪:超过 300 点时丢弃最旧的
|
||||||
|
// removeFirst 是 O(n),但 300 点每秒一次开销可忽略,无需复杂环形索引
|
||||||
|
QVector<SpeedSample> &samples = m_speedHistory[m_activeInterface];
|
||||||
|
while (samples.size() > MAX_HISTORY_SAMPLES) {
|
||||||
|
samples.removeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
emit speedHistoryChanged();
|
||||||
|
}
|
||||||
|
|
||||||
emit speedChanged();
|
emit speedChanged();
|
||||||
emit totalChanged();
|
emit totalChanged();
|
||||||
}
|
}
|
||||||
@@ -279,21 +357,30 @@ qint64 NetworkMonitorApplet::getActiveTxBytes() const
|
|||||||
return m_interfaces[m_activeInterface].txBytes;
|
return m_interfaces[m_activeInterface].txBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检测活动接口的 IPv4 地址
|
// 检测活动接口的 IPv4 与 IPv6 地址
|
||||||
// 跳过 IPv6 和 loopback,只取第一个有效 IPv4 地址
|
// IPv4:跳过 loopback,取第一个
|
||||||
// IP 变化时发射 ipAddressChanged 信号通知 QML 更新
|
// IPv6:跳过 loopback 和 link-local (fe80::),取第一个全球地址
|
||||||
// 设计原因:DHCP 续约、网络切换等场景下 IP 可能变化,需持续检测
|
// 任一地址变化时分别 emit 对应信号通知 QML 更新
|
||||||
|
// 设计原因:DHCP 续约、网络切换、IPv6 SLAAC 等场景下地址可能变化,需持续检测
|
||||||
void NetworkMonitorApplet::detectIpAddress()
|
void NetworkMonitorApplet::detectIpAddress()
|
||||||
{
|
{
|
||||||
QString newIp;
|
QString newIp;
|
||||||
|
QString newIpv6;
|
||||||
if (!m_activeInterface.isEmpty()) {
|
if (!m_activeInterface.isEmpty()) {
|
||||||
QNetworkInterface iface = QNetworkInterface::interfaceFromName(m_activeInterface);
|
QNetworkInterface iface = QNetworkInterface::interfaceFromName(m_activeInterface);
|
||||||
for (const QNetworkAddressEntry &entry : iface.addressEntries()) {
|
for (const QNetworkAddressEntry &entry : iface.addressEntries()) {
|
||||||
// 只取 IPv4 地址,跳过 IPv6 和 loopback
|
// IPv4:跳过 loopback,取第一个
|
||||||
if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol
|
if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol
|
||||||
&& !entry.ip().isLoopback()) {
|
&& !entry.ip().isLoopback()
|
||||||
|
&& newIp.isEmpty()) {
|
||||||
newIp = entry.ip().toString();
|
newIp = entry.ip().toString();
|
||||||
break;
|
}
|
||||||
|
// IPv6:跳过 loopback 和 link-local (fe80::),取第一个全球地址
|
||||||
|
if (entry.ip().protocol() == QAbstractSocket::IPv6Protocol
|
||||||
|
&& !entry.ip().isLoopback()
|
||||||
|
&& !entry.ip().isLinkLocal()
|
||||||
|
&& newIpv6.isEmpty()) {
|
||||||
|
newIpv6 = entry.ip().toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,6 +388,10 @@ void NetworkMonitorApplet::detectIpAddress()
|
|||||||
m_ipAddress = newIp;
|
m_ipAddress = newIp;
|
||||||
emit ipAddressChanged();
|
emit ipAddressChanged();
|
||||||
}
|
}
|
||||||
|
if (m_ipv6Address != newIpv6) {
|
||||||
|
m_ipv6Address = newIpv6;
|
||||||
|
emit ipv6AddressChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否为物理网卡
|
// 判断是否为物理网卡
|
||||||
@@ -11,6 +11,9 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QVariantList>
|
#include <QVariantList>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QPointF>
|
||||||
|
#include <QDateTime>
|
||||||
|
|
||||||
DS_BEGIN_NAMESPACE
|
DS_BEGIN_NAMESPACE
|
||||||
|
|
||||||
@@ -26,6 +29,15 @@ struct NetworkInterface {
|
|||||||
qint64 txDropped; // 发送丢弃数
|
qint64 txDropped; // 发送丢弃数
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 单个速度采样点:用于趋势图的环形缓冲
|
||||||
|
// timestamp: 自 epoch 的秒级时间戳,作为 X 轴定位
|
||||||
|
// downloadSpeed / uploadSpeed: 当时的瞬时速度(字节/秒),作为两条折线的 Y 值
|
||||||
|
struct SpeedSample {
|
||||||
|
qint64 timestamp;
|
||||||
|
double downloadSpeed;
|
||||||
|
double uploadSpeed;
|
||||||
|
};
|
||||||
|
|
||||||
class NetworkMonitorApplet : public DApplet
|
class NetworkMonitorApplet : public DApplet
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -40,6 +52,15 @@ class NetworkMonitorApplet : public DApplet
|
|||||||
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)
|
||||||
|
// 活动接口的 IPv6 全球地址(已过滤 link-local 和 loopback),供 QML 显示
|
||||||
|
Q_PROPERTY(QString ipv6Address READ ipv6Address NOTIFY ipv6AddressChanged)
|
||||||
|
// 插件版本号,从 dde-shell 元数据读取,版本唯一源为 CMakeLists.txt project(VERSION)
|
||||||
|
Q_PROPERTY(QString version READ version CONSTANT)
|
||||||
|
// 当前活动接口的下行速度历史采样点(QPointF 列表:x=时间戳秒, y=速度 bytes/sec)
|
||||||
|
// 用于趋势图绘制下载折线,每次 calculateSpeed 追加一个点并发射 speedHistoryChanged
|
||||||
|
Q_PROPERTY(QVariantList speedHistoryDownload READ speedHistoryDownload NOTIFY speedHistoryChanged)
|
||||||
|
// 当前活动接口的上行速度历史采样点,结构同 speedHistoryDownload
|
||||||
|
Q_PROPERTY(QVariantList speedHistoryUpload READ speedHistoryUpload NOTIFY speedHistoryChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit NetworkMonitorApplet(QObject *parent = nullptr);
|
explicit NetworkMonitorApplet(QObject *parent = nullptr);
|
||||||
@@ -57,6 +78,13 @@ public:
|
|||||||
bool ready() const;
|
bool ready() const;
|
||||||
QString activeInterface() const;
|
QString activeInterface() const;
|
||||||
QString ipAddress() const;
|
QString ipAddress() const;
|
||||||
|
QString ipv6Address() const;
|
||||||
|
// 返回当前活动接口的下行速度历史(QPointF 列表),供 QML 趋势图绘制
|
||||||
|
QVariantList speedHistoryDownload() const;
|
||||||
|
// 返回当前活动接口的上行速度历史(QPointF 列表),供 QML 趋势图绘制
|
||||||
|
QVariantList speedHistoryUpload() const;
|
||||||
|
// 返回插件版本号,从 dde-shell 插件元数据(metadata.json)读取
|
||||||
|
QString version() const;
|
||||||
|
|
||||||
Q_INVOKABLE void refresh();
|
Q_INVOKABLE void refresh();
|
||||||
Q_INVOKABLE void setActiveInterface(const QString &interface);
|
Q_INVOKABLE void setActiveInterface(const QString &interface);
|
||||||
@@ -69,12 +97,14 @@ signals:
|
|||||||
void readyChanged();
|
void readyChanged();
|
||||||
void activeInterfaceChanged();
|
void activeInterfaceChanged();
|
||||||
void ipAddressChanged();
|
void ipAddressChanged();
|
||||||
|
void ipv6AddressChanged();
|
||||||
|
void speedHistoryChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void readNetworkStats();
|
void readNetworkStats();
|
||||||
void calculateSpeed();
|
void calculateSpeed();
|
||||||
void detectInterfaces();
|
void detectInterfaces();
|
||||||
// 检测活动接口的 IPv4 地址,变化时发射 ipAddressChanged
|
// 检测活动接口的 IPv4 与 IPv6 地址,变化时分别发射对应信号
|
||||||
void detectIpAddress();
|
void detectIpAddress();
|
||||||
// 判断是否为物理网卡(无线 wlp/wlan,有线 enp/eth),
|
// 判断是否为物理网卡(无线 wlp/wlan,有线 enp/eth),
|
||||||
// 用于自动选择时优先真实网卡而非虚拟代理接口(如 Meta/tun0)
|
// 用于自动选择时优先真实网卡而非虚拟代理接口(如 Meta/tun0)
|
||||||
@@ -87,6 +117,7 @@ private:
|
|||||||
QStringList m_interfaceList;
|
QStringList m_interfaceList;
|
||||||
QString m_activeInterface;
|
QString m_activeInterface;
|
||||||
QString m_ipAddress; // 活动接口的 IPv4 地址
|
QString m_ipAddress; // 活动接口的 IPv4 地址
|
||||||
|
QString m_ipv6Address; // 活动接口的 IPv6 全球地址(过滤 link-local)
|
||||||
|
|
||||||
// 速度计算
|
// 速度计算
|
||||||
qint64 m_lastRxBytes;
|
qint64 m_lastRxBytes;
|
||||||
@@ -100,6 +131,12 @@ private:
|
|||||||
|
|
||||||
bool m_ready;
|
bool m_ready;
|
||||||
bool m_firstUpdate;
|
bool m_firstUpdate;
|
||||||
|
|
||||||
|
// 速度历史环形缓冲:每个接口独立维护一份,key 为接口名
|
||||||
|
// 设计原因:接口切换时趋势图不出现跳变,切回时仍能看到该接口历史
|
||||||
|
QHash<QString, QVector<SpeedSample>> m_speedHistory;
|
||||||
|
// 每个接口最多保留的采样点数:5 分钟 × 60 秒 = 300
|
||||||
|
static constexpr int MAX_HISTORY_SAMPLES = 300;
|
||||||
};
|
};
|
||||||
|
|
||||||
DS_END_NAMESPACE
|
DS_END_NAMESPACE
|
||||||
Reference in New Issue
Block a user