diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a663453 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Build directories +build/ +CMakeFiles/ +cmake-build-*/ + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Compiled files +*.o +*.so +*.so.* +*.dylib + +# Qt Creator +*.pro.user +*.pro.user.* + +# System files +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e94d9d..cf7f540 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,39 @@ cmake_minimum_required(VERSION 3.16) -project(JNetApplet) +project(JNetApplet VERSION 1.0.0 LANGUAGES CXX) -find_package(DDEShell REQUIRED NO_MODULE) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTOMOC ON) -ds_install_package(PACKAGE space.jokul.JNetApplet) +find_package(Qt6 REQUIRED COMPONENTS Core Quick DBus) +find_package(Dtk6 REQUIRED COMPONENTS Core) +find_package(DDEShell REQUIRED) + +# 插件 ID +set(PLUGIN_ID "space.jokul.JNetApplet") + +add_library(space.jokul.JNetApplet SHARED + networkmonitorapplet.h + networkmonitorapplet.cpp +) + +set_target_properties(space.jokul.JNetApplet PROPERTIES PREFIX "") + +target_include_directories(space.jokul.JNetApplet PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + /usr/include/dde-shell +) + +target_link_libraries(space.jokul.JNetApplet PRIVATE + Qt6::Core + Qt6::Quick + Qt6::DBus + Dtk6::Core + Dde::Shell +) + +# 安装到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}) \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..5a7572c --- /dev/null +++ b/install.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# 网络速度监控任务栏插件安装脚本 + +set -e + +echo "Building JNetApplet..." + +# 清理旧的构建目录 +rm -rf build + +# 配置CMake +cmake -Bbuild + +# 构建 +cmake --build build + +echo "Installing JNetApplet (requires sudo)..." + +# 安装 +sudo cmake --install build + +echo "Installation complete!" +echo "Please restart dde-shell to load the plugin:" +echo " systemctl --user restart dde-shell@DDE" \ No newline at end of file diff --git a/networkmonitorapplet.cpp b/networkmonitorapplet.cpp new file mode 100644 index 0000000..fb6fb29 --- /dev/null +++ b/networkmonitorapplet.cpp @@ -0,0 +1,262 @@ +// SPDX-FileCopyrightText: 2024 MyCompany +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "networkmonitorapplet.h" +#include + +#include +#include +#include +#include +#include + +DS_BEGIN_NAMESPACE + +NetworkMonitorApplet::NetworkMonitorApplet(QObject *parent) + : DApplet(parent) + , m_refreshTimer(nullptr) + , m_lastRxBytes(0) + , m_lastTxBytes(0) + , m_downloadSpeed(0) + , m_uploadSpeed(0) + , m_totalDownload(0) + , m_totalUpload(0) + , m_ready(false) + , m_firstUpdate(true) +{ +} + +NetworkMonitorApplet::~NetworkMonitorApplet() +{ +} + +bool NetworkMonitorApplet::load() +{ + return DApplet::load(); +} + +bool NetworkMonitorApplet::init() +{ + detectInterfaces(); + + // 启动定时刷新 + m_refreshTimer = new QTimer(this); + connect(m_refreshTimer, &QTimer::timeout, this, &NetworkMonitorApplet::refresh); + m_refreshTimer->start(1000); // 每秒刷新一次 + + // 初始读取 + readNetworkStats(); + + return DApplet::init(); +} + +qint64 NetworkMonitorApplet::downloadSpeed() const +{ + return m_downloadSpeed; +} + +qint64 NetworkMonitorApplet::uploadSpeed() const +{ + return m_uploadSpeed; +} + +qint64 NetworkMonitorApplet::totalDownload() const +{ + return m_totalDownload; +} + +qint64 NetworkMonitorApplet::totalUpload() const +{ + return m_totalUpload; +} + +QStringList NetworkMonitorApplet::networkInterfaces() const +{ + return m_interfaceList; +} + +QStringList NetworkMonitorApplet::interfaceStats() const +{ + QStringList stats; + for (const QString &name : m_interfaceList) { + if (m_interfaces.contains(name)) { + const NetworkInterface &iface = m_interfaces[name]; + stats << QString("%1|%2|%3|%4|%5") + .arg(iface.name) + .arg(iface.rxBytes) + .arg(iface.txBytes) + .arg(iface.rxPackets) + .arg(iface.txPackets); + } + } + return stats; +} + +bool NetworkMonitorApplet::ready() const +{ + return m_ready; +} + +QString NetworkMonitorApplet::activeInterface() const +{ + return m_activeInterface; +} + +void NetworkMonitorApplet::refresh() +{ + readNetworkStats(); +} + +void NetworkMonitorApplet::setActiveInterface(const QString &interface) +{ + if (m_activeInterface != interface) { + m_activeInterface = interface; + m_firstUpdate = true; + emit activeInterfaceChanged(); + } +} + +void NetworkMonitorApplet::readNetworkStats() +{ + QFile file("/proc/net/dev"); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qDebug() << "Failed to open /proc/net/dev"; + return; + } + + QTextStream stream(&file); + QStringList lines = stream.readAll().split('\n', Qt::SkipEmptyParts); + file.close(); + + QMap newInterfaces; + + // 跳过前两行标题 + for (int i = 2; i < lines.size(); ++i) { + QString line = lines[i].trimmed(); + if (line.isEmpty()) continue; + + // 格式: "interface: rx_bytes rx_packets rx_errors rx_dropped ... tx_bytes tx_packets tx_errors tx_dropped ..." + QRegularExpression re("^([^:]+):\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+\\d+\\s+\\d+\\s+\\d+\\s+\\d+\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)"); + QRegularExpressionMatch match = re.match(line); + + if (match.hasMatch()) { + NetworkInterface iface; + iface.name = match.captured(1); + iface.rxBytes = match.captured(2).toLongLong(); + iface.rxPackets = match.captured(3).toLongLong(); + iface.rxErrors = match.captured(4).toLongLong(); + iface.rxDropped = match.captured(5).toLongLong(); + iface.txBytes = match.captured(6).toLongLong(); + iface.txPackets = match.captured(7).toLongLong(); + iface.txErrors = match.captured(8).toLongLong(); + iface.txDropped = match.captured(9).toLongLong(); + + // 过滤掉回环接口和虚拟接口 + if (iface.name != "lo" && !iface.name.startsWith("veth") && + !iface.name.startsWith("docker") && !iface.name.startsWith("br-")) { + newInterfaces[iface.name] = iface; + } + } + } + + // 检测接口变化 + if (newInterfaces.keys() != m_interfaces.keys()) { + m_interfaceList = newInterfaces.keys(); + m_interfaces = newInterfaces; + + // 自动选择活动接口 + if (m_activeInterface.isEmpty() && !m_interfaceList.isEmpty()) { + // 优先选择有流量的接口 + for (const QString &name : m_interfaceList) { + const NetworkInterface &iface = m_interfaces[name]; + if (iface.rxBytes > 0 || iface.txBytes > 0) { + m_activeInterface = name; + break; + } + } + if (m_activeInterface.isEmpty()) { + m_activeInterface = m_interfaceList.first(); + } + emit activeInterfaceChanged(); + } + + m_ready = !m_interfaceList.isEmpty(); + emit interfacesChanged(); + emit readyChanged(); + } else { + m_interfaces = newInterfaces; + } + + calculateSpeed(); + emit statsChanged(); +} + +void NetworkMonitorApplet::calculateSpeed() +{ + qint64 currentRxBytes = getActiveRxBytes(); + qint64 currentTxBytes = getActiveTxBytes(); + + if (m_firstUpdate) { + m_firstUpdate = false; + m_lastRxBytes = currentRxBytes; + m_lastTxBytes = currentTxBytes; + return; + } + + // 计算速度(字节/秒) + m_downloadSpeed = currentRxBytes - m_lastRxBytes; + m_uploadSpeed = currentTxBytes - m_lastTxBytes; + + // 更新总量 + m_totalDownload = currentRxBytes; + m_totalUpload = currentTxBytes; + + m_lastRxBytes = currentRxBytes; + m_lastTxBytes = currentTxBytes; + + emit speedChanged(); + emit totalChanged(); +} + +void NetworkMonitorApplet::detectInterfaces() +{ + QDir sysClass("/sys/class/net"); + if (!sysClass.exists()) return; + + QStringList entries = sysClass.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + m_interfaceList.clear(); + + for (const QString &entry : entries) { + // 过滤回环接口 + if (entry != "lo") { + m_interfaceList << entry; + } + } + + m_ready = !m_interfaceList.isEmpty(); + emit interfacesChanged(); + emit readyChanged(); +} + +qint64 NetworkMonitorApplet::getActiveRxBytes() const +{ + if (m_activeInterface.isEmpty() || !m_interfaces.contains(m_activeInterface)) { + return 0; + } + return m_interfaces[m_activeInterface].rxBytes; +} + +qint64 NetworkMonitorApplet::getActiveTxBytes() const +{ + if (m_activeInterface.isEmpty() || !m_interfaces.contains(m_activeInterface)) { + return 0; + } + return m_interfaces[m_activeInterface].txBytes; +} + +D_APPLET_CLASS(NetworkMonitorApplet) + +DS_END_NAMESPACE + +#include "networkmonitorapplet.moc" \ No newline at end of file diff --git a/networkmonitorapplet.h b/networkmonitorapplet.h new file mode 100644 index 0000000..86cf0fa --- /dev/null +++ b/networkmonitorapplet.h @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2024 MyCompany +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include + +#include +#include +#include +#include +#include + +DS_BEGIN_NAMESPACE + +struct NetworkInterface { + QString name; // 接口名称,如 "eth0", "wlan0" + qint64 rxBytes; // 接收字节数 + qint64 txBytes; // 发送字节数 + qint64 rxPackets; // 接收包数 + qint64 txPackets; // 发送包数 + qint64 rxErrors; // 接收错误数 + qint64 txErrors; // 发送错误数 + qint64 rxDropped; // 接收丢弃数 + qint64 txDropped; // 发送丢弃数 +}; + +class NetworkMonitorApplet : public DApplet +{ + Q_OBJECT + + Q_PROPERTY(qint64 downloadSpeed READ downloadSpeed NOTIFY speedChanged) + Q_PROPERTY(qint64 uploadSpeed READ uploadSpeed NOTIFY speedChanged) + Q_PROPERTY(qint64 totalDownload READ totalDownload NOTIFY totalChanged) + Q_PROPERTY(qint64 totalUpload READ totalUpload NOTIFY totalChanged) + Q_PROPERTY(QStringList networkInterfaces READ networkInterfaces NOTIFY interfacesChanged) + Q_PROPERTY(QStringList interfaceStats READ interfaceStats NOTIFY statsChanged) + Q_PROPERTY(bool ready READ ready NOTIFY readyChanged) + Q_PROPERTY(QString activeInterface READ activeInterface NOTIFY activeInterfaceChanged) + +public: + explicit NetworkMonitorApplet(QObject *parent = nullptr); + ~NetworkMonitorApplet(); + + virtual bool load() override; + virtual bool init() override; + + qint64 downloadSpeed() const; + qint64 uploadSpeed() const; + qint64 totalDownload() const; + qint64 totalUpload() const; + QStringList networkInterfaces() const; + QStringList interfaceStats() const; + bool ready() const; + QString activeInterface() const; + + Q_INVOKABLE void refresh(); + Q_INVOKABLE void setActiveInterface(const QString &interface); + +signals: + void speedChanged(); + void totalChanged(); + void interfacesChanged(); + void statsChanged(); + void readyChanged(); + void activeInterfaceChanged(); + +private: + void readNetworkStats(); + void calculateSpeed(); + void detectInterfaces(); + qint64 getActiveRxBytes() const; + qint64 getActiveTxBytes() const; + + QTimer *m_refreshTimer; + QMap m_interfaces; + QStringList m_interfaceList; + QString m_activeInterface; + + // 速度计算 + qint64 m_lastRxBytes; + qint64 m_lastTxBytes; + qint64 m_downloadSpeed; + qint64 m_uploadSpeed; + + // 总量 + qint64 m_totalDownload; + qint64 m_totalUpload; + + bool m_ready; + bool m_firstUpdate; +}; + +DS_END_NAMESPACE \ No newline at end of file diff --git a/package/main.qml b/package/main.qml deleted file mode 100644 index 2dad1b2..0000000 --- a/package/main.qml +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: 2024 MyCompany -// SPDX-License-Identifier: LGPL-3.0-or-later - -import QtQuick 2.11 -import QtQuick.Controls 2.4 -import org.deepin.ds 1.0 - -AppletItem { - objectName: "JnetApplet" - implicitWidth: 100 - implicitHeight: 100 - - Rectangle { - anchors.fill: parent - color: "#2ecc71" - radius: 8 - - Text { - anchors.centerIn: parent - text: "你好,世界!" - font.pixelSize: 14 - color: "white" - } - } -} \ No newline at end of file diff --git a/package/metadata.json b/package/metadata.json index 0c24af9..cd5e40b 100644 --- a/package/metadata.json +++ b/package/metadata.json @@ -2,6 +2,11 @@ "Plugin": { "Version": "1.0", "Id": "space.jokul.JNetApplet", - "Url": "main.qml" + "Url": "networkview.qml", + "Parent": "org.deepin.ds.dock", + "Name": "Network Monitor", + "Name[zh_CN]": "网络速度监控", + "Description": "Monitor network speed and traffic", + "Description[zh_CN]": "监控网络速度和流量" } } \ No newline at end of file diff --git a/package/networkview.qml b/package/networkview.qml new file mode 100644 index 0000000..76767d3 --- /dev/null +++ b/package/networkview.qml @@ -0,0 +1,521 @@ +// SPDX-FileCopyrightText: 2024 MyCompany +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Window 2.15 +import org.deepin.ds 1.0 +import org.deepin.ds.dock 1.0 +import org.deepin.dtk 1.0 + +AppletItem { + id: root + objectName: "network monitor applet" + property int dockOrder: 21 + property int dockSize: Panel.rootObject.dockItemMaxSize || 48 + + implicitWidth: dockSize + implicitHeight: dockSize + + readonly property var applet: Applet + readonly property bool ready: applet ? applet.ready : false + readonly property qint64 downloadSpeed: applet ? applet.downloadSpeed : 0 + readonly property qint64 uploadSpeed: applet ? applet.uploadSpeed : 0 + readonly property qint64 totalDownload: applet ? applet.totalDownload : 0 + readonly property qint64 totalUpload: applet ? applet.totalUpload : 0 + readonly property string activeInterface: applet ? (applet.activeInterface || "") : "" + readonly property var networkInterfaces: applet ? (applet.networkInterfaces || []) : [] + readonly property var interfaceStats: applet ? (applet.interfaceStats || []) : [] + + // 格式化速度显示 + 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" + } + } + + // 根据速度计算图标颜色 + readonly property color iconColor: { + if (downloadSpeed > 10 * 1024 * 1024) return Qt.rgba(220/255, 38/255, 38/255, 1) // 高速红色 + if (downloadSpeed > 1 * 1024 * 1024) return Qt.rgba(245/255, 158/255, 11/255, 1) // 中速橙色 + return root.primaryText // 正常颜色 + } + + 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.8) + 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.1) + 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) + + // 图标区域 + Rectangle { + anchors.centerIn: parent + width: dockSize * 0.7 + height: dockSize * 0.7 + color: "transparent" + radius: width * 0.45 + border.width: 1 + border.color: root.iconColor + + Column { + anchors.centerIn: parent + spacing: 2 + + // 下载速度 + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: "↓" + font.pixelSize: root.dockSize * 0.18 + color: root.iconColor + } + + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: formatSpeed(root.downloadSpeed) + font.pixelSize: root.dockSize * 0.12 + font.bold: true + color: root.iconColor + } + + // 上传速度 + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: "↑" + font.pixelSize: root.dockSize * 0.18 + color: root.iconColor + } + + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: formatSpeed(root.uploadSpeed) + font.pixelSize: root.dockSize * 0.12 + font.bold: true + color: root.iconColor + } + } + } + + // 悬停提示 + PanelToolTip { + id: toolTip + text: root.ready ? buildToolTipText() : qsTr("No network interface detected") + toolTipX: DockPanelPositioner.x + toolTipY: DockPanelPositioner.y + } + + // 统计数据刷新定时器 + Timer { + id: statsRefreshTimer + interval: 1000 + repeat: true + onTriggered: { + if (root.applet) { + root.applet.refresh() + } + } + } + + Timer { + id: toolTipShowTimer + interval: 50 + onTriggered: { + const point = root.mapToItem(null, root.width / 2, root.height / 2) + toolTip.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, toolTip.width, toolTip.height) + toolTip.open() + } + } + + HoverHandler { + onHoveredChanged: { + if (hovered && !networkPopup.popupVisible) { + toolTipShowTimer.start() + if (root.applet) { + root.applet.refresh() + } + statsRefreshTimer.start() + } else { + if (toolTipShowTimer.running) { + toolTipShowTimer.stop() + } + toolTip.close() + if (!networkPopup.popupVisible) { + statsRefreshTimer.stop() + } + } + } + } + + // 弹出窗口 + PanelPopup { + id: networkPopup + width: 400 + height: 350 + popupX: DockPanelPositioner.x + popupY: DockPanelPositioner.y + + onPopupVisibleChanged: { + if (popupVisible) { + toolTip.close() + statsRefreshTimer.start() + if (root.applet) { + root.applet.refresh() + } + } else { + statsRefreshTimer.stop() + } + } + + Control { + id: popupContainer + anchors.fill: parent + padding: 16 + + contentItem: ColumnLayout { + spacing: 14 + + // 标题区域 + RowLayout { + Layout.fillWidth: true + spacing: 10 + + Item { Layout.fillWidth: true } + + Rectangle { + width: 28 + height: 28 + color: accentBlueLight + radius: 8 + + Text { + anchors.centerIn: parent + text: "NET" + font.pixelSize: 12 + font.bold: true + color: accentBlue + } + } + + Text { + text: qsTr("Network Monitor") + font.pixelSize: 16 + font.bold: true + color: root.primaryText + } + + Item { Layout.fillWidth: true } + + // 刷新图标 + Rectangle { + width: 28 + height: 28 + radius: 6 + color: refreshIconArea.containsMouse ? root.accentBlueLight : "transparent" + + Text { + anchors.centerIn: parent + text: "⟳" + font.pixelSize: 18 + color: refreshIconArea.containsMouse ? root.accentBlue : root.secondaryText + } + + MouseArea { + id: refreshIconArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (root.applet) { + root.applet.refresh() + } + } + } + + // 旋转动画 + RotationAnimator on rotation { + id: refreshSpin + running: false + from: 0 + to: 360 + duration: 500 + } + + Connections { + target: refreshIconArea + function onClicked() { refreshSpin.start() } + } + } + } + + // 分隔线 + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: root.cardBorder + } + + // 当前接口信息 + RowLayout { + Layout.fillWidth: true + spacing: 8 + visible: root.ready + + Text { + text: qsTr("Interface:") + font.pixelSize: 12 + color: root.secondaryText + } + + Text { + text: root.activeInterface + font.pixelSize: 12 + font.bold: true + color: root.primaryText + Layout.fillWidth: true + elide: Text.ElideRight + } + } + + // 速度统计卡片 + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 120 + color: root.cardBackground + radius: 10 + border.width: 1 + border.color: root.cardBorder + + ColumnLayout { + anchors.fill: parent + anchors.margins: 12 + spacing: 12 + + // 下载速度 + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Text { + text: "↓ " + qsTr("Download") + font.pixelSize: 13 + color: root.secondaryText + } + + Item { Layout.fillWidth: true } + + Text { + text: formatSpeed(root.downloadSpeed) + font.pixelSize: 18 + font.bold: true + color: root.accentBlue + } + } + + // 上传速度 + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Text { + text: "↑ " + qsTr("Upload") + font.pixelSize: 13 + color: root.secondaryText + } + + Item { Layout.fillWidth: true } + + Text { + text: formatSpeed(root.uploadSpeed) + font.pixelSize: 18 + font.bold: true + color: Qt.rgba(22/255, 163/255, 74/255, 1) + } + } + } + } + + // 总量统计卡片 + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 80 + color: root.cardBackground + radius: 10 + border.width: 1 + border.color: root.cardBorder + + ColumnLayout { + anchors.fill: parent + anchors.margins: 12 + spacing: 8 + + Text { + text: qsTr("Total Data") + font.pixelSize: 12 + font.bold: true + color: root.secondaryText + } + + RowLayout { + Layout.fillWidth: true + spacing: 20 + + // 下载总量 + ColumnLayout { + spacing: 2 + Text { + text: "↓ " + qsTr("Download") + font.pixelSize: 11 + color: root.tertiaryText + } + Text { + text: formatTotal(root.totalDownload) + font.pixelSize: 14 + font.bold: true + color: root.primaryText + } + } + + // 上传总量 + ColumnLayout { + spacing: 2 + Text { + text: "↑ " + qsTr("Upload") + font.pixelSize: 11 + color: root.tertiaryText + } + Text { + text: formatTotal(root.totalUpload) + font.pixelSize: 14 + font.bold: true + color: root.primaryText + } + } + } + } + } + + // 接口切换下拉框 + ComboBox { + id: interfaceCombo + Layout.fillWidth: true + Layout.preferredHeight: 32 + visible: root.networkInterfaces.length > 1 + model: root.networkInterfaces + currentIndex: { + var idx = root.networkInterfaces.indexOf(root.activeInterface) + return idx >= 0 ? idx : 0 + } + onActivated: { + if (root.applet && root.networkInterfaces[index]) { + root.applet.setActiveInterface(root.networkInterfaces[index]) + } + } + + background: Rectangle { + color: interfaceCombo.hovered ? root.accentBlueLight : root.cardBackground + radius: 6 + border.width: 1 + border.color: root.cardBorder + } + + contentItem: Text { + text: interfaceCombo.displayText + font.pixelSize: 12 + color: root.primaryText + verticalAlignment: Text.AlignVCenter + leftPadding: 10 + } + } + + // 未检测到网络接口提示 + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 60 + color: root.cardBackground + radius: 10 + border.width: 1 + border.color: root.cardBorder + visible: !root.ready + + ColumnLayout { + anchors.centerIn: parent + spacing: 6 + + Text { + text: "⚠" + font.pixelSize: 20 + } + + Text { + text: qsTr("No network interface detected") + font.pixelSize: 12 + color: root.secondaryText + } + } + } + + Item { Layout.fillHeight: true } + } + } + + Component.onCompleted: { + DockPanelPositioner.bounding = Qt.binding(function () { + const point = root.mapToItem(null, root.width / 2, root.height / 2) + return Qt.rect(point.x, point.y, networkPopup.width, networkPopup.height) + }) + } + } + + // 构建 tooltip 文本 + function buildToolTipText() { + if (!root.ready) { + return qsTr("No network interface detected") + } + + var lines = [] + lines.push("↓ " + formatSpeed(root.downloadSpeed)) + lines.push("↑ " + formatSpeed(root.uploadSpeed)) + lines.push(root.activeInterface) + return lines.join(" | ") + } + + // 点击处理 + TapHandler { + acceptedButtons: Qt.LeftButton + gesturePolicy: TapHandler.ReleaseWithinBounds + + onTapped: { + if (networkPopup.popupVisible) { + networkPopup.close() + } else { + Panel.requestClosePopup() + const point = root.mapToItem(null, root.width / 2, root.height / 2) + networkPopup.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, networkPopup.width, networkPopup.height) + networkPopup.open() + } + toolTip.close() + } + } +} \ No newline at end of file