From d5955bbbddae61516fc839322c298eaf627f811c Mon Sep 17 00:00:00 2001 From: HeXiangLong <3234374354@qq.com> Date: Mon, 10 Nov 2025 17:28:10 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A6=96=E6=AC=A1=E5=A1=AB=E5=85=85=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/__init__.py | 21 +++++ core/proxy_manager.py | 76 +++++++++++++++++ core/update_manager.py | 115 +++++++++++++++++++++++++ core/v2ray_manager.py | 64 ++++++++++++++ main.py | 57 +++++++++++++ requirements.txt | 5 ++ ui/__init__.py | 23 +++++ ui/main_window.py | 185 +++++++++++++++++++++++++++++++++++++++++ ui/setup_dialog.py | 85 +++++++++++++++++++ ui/traffic_widget.py | 163 ++++++++++++++++++++++++++++++++++++ ui/update_dialog.py | 143 +++++++++++++++++++++++++++++++ update_helper.py | 63 ++++++++++++++ utils/__init__.py | 56 +++++++++++++ utils/config_utils.py | 161 +++++++++++++++++++++++++++++++++++ utils/network_utils.py | 86 +++++++++++++++++++ utils/system_utils.py | 48 +++++++++++ 16 files changed, 1351 insertions(+) diff --git a/core/__init__.py b/core/__init__.py index e69de29..e839163 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -0,0 +1,21 @@ +""" +EZProxy Core Module +================== + +核心功能模块,包含v2ray管理、代理管理和更新管理 +""" + +# 导入核心组件 +from .v2ray_manager import V2RayManager +from .proxy_manager import ProxyManager +from .update_manager import UpdateManager + +# 定义包的公开接口 +__all__ = [ + 'V2RayManager', + 'ProxyManager', + 'UpdateManager' +] + +# 包版本信息 +__version__ = "1.0.0" \ No newline at end of file diff --git a/core/proxy_manager.py b/core/proxy_manager.py index e69de29..af7900a 100644 --- a/core/proxy_manager.py +++ b/core/proxy_manager.py @@ -0,0 +1,76 @@ +import os +import sys +import logging +import subprocess +from pathlib import Path +from utils.system_utils import get_architecture + +class ProxyManager: + def __init__(self): + self.is_enabled = False + self.http_proxy = "127.0.0.1:1081" + self.socks_proxy = "127.0.0.1:1080" + + def enable(self): + """启用系统代理""" + if self.is_enabled: + return True + + try: + # 获取当前网络服务 + service_cmd = "networksetup -listnetworkserviceorder | grep $(route -n get default | grep 'interface' | awk '{print $2}') -A1 | grep -o '[^ ]*$'" + service = subprocess.check_output(service_cmd, shell=True, text=True).strip() + + # 设置HTTP/HTTPS代理 + http_cmd = f"networksetup -setwebproxy '{service}' {self.http_proxy.split(':')[0]} {self.http_proxy.split(':')[1]}" + https_cmd = f"networksetup -setsecurewebproxy '{service}' {self.http_proxy.split(':')[0]} {self.http_proxy.split(':')[1]}" + + # 设置SOCKS代理 + socks_cmd = f"networksetup -setsocksfirewallproxy '{service}' {self.socks_proxy.split(':')[0]} {self.socks_proxy.split(':')[1]}" + + # 使用AppleScript请求管理员权限 + script = f""" + do shell script "{http_cmd}" with administrator privileges + do shell script "{https_cmd}" with administrator privileges + do shell script "{socks_cmd}" with administrator privileges + do shell script "networksetup -setwebproxystate '{service}' on" with administrator privileges + do shell script "networksetup -setsecurewebproxystate '{service}' on" with administrator privileges + do shell script "networksetup -setsocksfirewallproxystate '{service}' on" with administrator privileges + """ + + subprocess.run(['osascript', '-e', script], check=True) + self.is_enabled = True + logging.info("System proxy enabled successfully") + return True + except Exception as e: + logging.error(f"Failed to enable system proxy: {str(e)}") + return False + + def disable(self): + """禁用系统代理""" + if not self.is_enabled: + return True + + try: + # 获取当前网络服务 + service_cmd = "networksetup -listnetworkserviceorder | grep $(route -n get default | grep 'interface' | awk '{print $2}') -A1 | grep -o '[^ ]*$'" + service = subprocess.check_output(service_cmd, shell=True, text=True).strip() + + # 使用AppleScript请求管理员权限 + script = f""" + do shell script "networksetup -setwebproxystate '{service}' off" with administrator privileges + do shell script "networksetup -setsecurewebproxystate '{service}' off" with administrator privileges + do shell script "networksetup -setsocksfirewallproxystate '{service}' off" with administrator privileges + """ + + subprocess.run(['osascript', '-e', script], check=True) + self.is_enabled = False + logging.info("System proxy disabled successfully") + return True + except Exception as e: + logging.error(f"Failed to disable system proxy: {str(e)}") + return False + + def get_proxy_status(self): + """获取代理状态""" + return self.is_enabled \ No newline at end of file diff --git a/core/update_manager.py b/core/update_manager.py index e69de29..afd88cf 100644 --- a/core/update_manager.py +++ b/core/update_manager.py @@ -0,0 +1,115 @@ +import yaml +import requests +import zipfile +from pathlib import Path +from utils.config_utils import load_config, save_config + +class UpdateManager: + UPDATE_URL = "https://o.nmgjg.com.cn/EZProxy/update.yaml" + CONFIG_URL = "https://o.nmgjg.com.cn/EZProxy/v2ray.json" + APP_URL = "https://o.nmgjg.com.cn/EZProxy/main.zip" + + def __init__(self, app_dir): + self.app_dir = Path(app_dir) + self.local_update_yaml = self.app_dir / "conf" / "update.yaml" + self.config = load_config() + + def check_updates(self): + """检查更新""" + try: + response = requests.get(self.UPDATE_URL, timeout=10) + response.raise_for_status() + remote = yaml.safe_load(response.text) + + # 检查配置更新 + config_update = self._check_config_update(remote) + + # 检查应用更新 + app_update = self._check_app_update(remote) + + return { + 'config': config_update, + 'app': app_update, + 'changelog_url': remote.get('changelog_url', '') + } + except Exception as e: + logging.error(f"检查更新失败: {str(e)}") + return None + + def _check_config_update(self, remote): + """检查配置模板更新""" + local_version = self.config.get('config_version', '0.0') + remote_version = remote.get('config_version', '0.0') + + if remote_version != local_version: + return { + 'available': True, + 'version': remote_version, + 'force': remote.get('force_config_update', False) + } + return {'available': False} + + def update_config(self): + """更新配置模板""" + try: + response = requests.get(self.CONFIG_URL, timeout=15) + response.raise_for_status() + + template_path = self.app_dir / "conf" / "v2ray_template.json" + with open(template_path, 'w') as f: + f.write(response.text) + + # 更新本地版本号 + self.config['config_version'] = self._get_remote_version()['config_version'] + save_config(self.config) + return True + except Exception as e: + logging.error(f"更新配置失败: {str(e)}") + return False + + def _prepare_update_helper(self): + """准备更新助手""" + helper_path = self.app_dir / "update_helper.py" + temp_dir = get_temp_dir() + temp_dir.mkdir(parents=True, exist_ok=True) + + # 复制更新助手到临时目录 + temp_helper = temp_dir / "update_helper.py" + with open(helper_path, 'r') as src, open(temp_helper, 'w') as dst: + dst.write(src.read()) + + # 设置执行权限 + temp_helper.chmod(0o755) + return temp_helper + + def start_app_update(self): + """启动应用更新流程""" + temp_dir = get_temp_dir() + zip_path = temp_dir / "main.zip" + + try: + # 下载更新包 + response = requests.get(self.APP_URL, stream=True, timeout=30) + response.raise_for_status() + + with open(zip_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + + # 准备更新助手 + helper = self._prepare_update_helper() + + # 启动更新助手(在新进程中) + import subprocess + subprocess.Popen([ + sys.executable, + str(helper), + str(zip_path), + str(self.app_dir), + str(temp_dir) + ]) + + return True + except Exception as e: + logging.error(f"更新准备失败: {str(e)}") + return False \ No newline at end of file diff --git a/core/v2ray_manager.py b/core/v2ray_manager.py index e69de29..aa295a5 100644 --- a/core/v2ray_manager.py +++ b/core/v2ray_manager.py @@ -0,0 +1,64 @@ +import json +import logging +import subprocess +from pathlib import Path +from threading import Thread +from queue import Queue + +class V2RayManager: + def __init__(self, config_path, log_path): + self.config_path = Path(config_path) + self.log_path = Path(log_path) + self.process = None + self.log_queue = Queue() + self.running = False + + def start(self): + """启动v2ray核心""" + if self.running: + return True + + v2ray_bin = get_v2ray_binary_path() + cmd = [str(v2ray_bin), "-config", str(self.config_path)] + + try: + self.process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True + ) + self.running = True + + # 启动日志监听线程 + Thread(target=self._log_reader, daemon=True).start() + return True + except Exception as e: + logging.error(f"启动v2ray失败: {str(e)}") + return False + + def stop(self): + """停止v2ray核心""" + if not self.running: + return + + if self.process: + self.process.terminate() + try: + self.process.wait(timeout=5) + except subprocess.TimeoutExpired: + self.process.kill() + + self.running = False + + def _log_reader(self): + """读取v2ray日志""" + while self.running and self.process: + line = self.process.stdout.readline() + if not line: + break + self.log_queue.put(line.strip()) + with open(self.log_path, 'a') as f: + f.write(line) + + self.running = False \ No newline at end of file diff --git a/main.py b/main.py index e69de29..6318167 100644 --- a/main.py +++ b/main.py @@ -0,0 +1,57 @@ +import sys +import os +import logging +from pathlib import Path +from PyQt5.QtWidgets import QApplication +from ui.main_window import MainWindow +from utils.config_utils import init_config, load_config + +class AppContext: + def __init__(self): + self.app_dir = Path(__file__).parent.resolve() + self.conf_dir = self.app_dir / "conf" + self.log_dir = self.app_dir / "logs" + self.resources = self.app_dir / "resources" + + # 初始化目录 + self.conf_dir.mkdir(exist_ok=True) + self.log_dir.mkdir(exist_ok=True) + + # 初始化配置 + init_config(self.conf_dir) + self.config = load_config() + + # 配置日志 + logging.basicConfig( + filename=self.log_dir / "app.log", + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + def validate_config(self): + """验证配置有效性""" + return bool(self.config.get('proxy_url')) + +if __name__ == "__main__": + # 设置macOS应用属性 + if sys.platform == "darwin": + from Foundation import NSBundle + bundle = NSBundle.mainBundle() + info = bundle.localizedInfoDictionary() or bundle.infoDictionary() + info["CFBundleName"] = "EZProxy" + info["CFBundleIconFile"] = "icon.icns" + + app = QApplication(sys.argv) + app.setQuitOnLastWindowClosed(False) + + context = AppContext() + window = MainWindow(context) + + # 首次运行检查代理配置 + if not context.validate_config(): + from ui.setup_dialog import SetupDialog + setup = SetupDialog(context) + if setup.exec_() != QDialog.Accepted: + sys.exit(0) + + sys.exit(app.exec_()) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e69de29..6e0159b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,5 @@ +PyQt5==5.15.9 +PyYAML==6.0.1 +requests==2.31.0 +psutil==5.9.5 +pyobjc==9.2 # 用于macOS系统集成 diff --git a/ui/__init__.py b/ui/__init__.py index e69de29..0fba8eb 100644 --- a/ui/__init__.py +++ b/ui/__init__.py @@ -0,0 +1,23 @@ +""" +EZProxy UI Module +================ + +用户界面模块,提供所有GUI组件 +""" + +# 导入UI组件 +from .main_window import MainWindow +from .setup_dialog import SetupDialog +from .traffic_widget import TrafficWidget +from .update_dialog import UpdateDialog + +# 定义包的公开接口 +__all__ = [ + 'MainWindow', + 'SetupDialog', + 'TrafficWidget', + 'UpdateDialog' +] + +# 包版本信息 +__version__ = "1.0.0" \ No newline at end of file diff --git a/ui/main_window.py b/ui/main_window.py index e69de29..fc6d08a 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -0,0 +1,185 @@ +from PyQt5.QtWidgets import ( + QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QPushButton, QTextEdit, QLabel, QSplitter, + QMessageBox, QSystemTrayIcon, QMenu +) +from PyQt5.QtCore import Qt, QTimer +from PyQt5.QtGui import QIcon, QTextCursor +from .traffic_widget import TrafficWidget +from core.v2ray_manager import V2RayManager +from core.proxy_manager import ProxyManager + +class MainWindow(QMainWindow): + def __init__(self, app_context): + super().__init__() + self.app_context = app_context + self.v2ray = V2RayManager( + app_context.config['v2ray_config_path'], + app_context.config['v2ray_log_path'] + ) + self.proxy = ProxyManager() + self.tray_icon = None + + self.init_ui() + self.init_tray() + self.init_timers() + + def init_ui(self): + # 主窗口设置 + self.setWindowTitle("EZProxy") + self.setWindowIcon(QIcon(str(self.app_context.resources / "icon.icns"))) + self.setMinimumSize(800, 600) + + # 创建中央部件 + central_widget = QWidget() + main_layout = QVBoxLayout(central_widget) + + # 顶部控制栏 + control_layout = QHBoxLayout() + self.toggle_btn = QPushButton("启用代理") + self.toggle_btn.clicked.connect(self.toggle_proxy) + self.update_btn = QPushButton("检查更新") + self.update_btn.clicked.connect(self.check_updates) + control_layout.addWidget(self.toggle_btn) + control_layout.addWidget(self.update_btn) + control_layout.addStretch() + + # 日志显示区域 + self.log_view = QTextEdit() + self.log_view.setReadOnly(True) + self.log_view.setFontFamily("Menlo") + + # 流量监控组件 + self.traffic_widget = TrafficWidget() + + # 创建分割器 + splitter = QSplitter(Qt.Vertical) + splitter.addWidget(self.log_view) + splitter.addWidget(self.traffic_widget) + splitter.setSizes([400, 200]) + + # 添加到主布局 + main_layout.addLayout(control_layout) + main_layout.addWidget(splitter) + + self.setCentralWidget(central_widget) + + def init_tray(self): + """初始化系统托盘""" + self.tray_icon = QSystemTrayIcon(self) + self.tray_icon.setIcon(QIcon(str(self.app_context.resources / "icon.icns"))) + + tray_menu = QMenu() + toggle_action = tray_menu.addAction("启用代理") + toggle_action.triggered.connect(self.toggle_proxy) + + update_action = tray_menu.addAction("检查更新") + update_action.triggered.connect(self.check_updates) + + quit_action = tray_menu.addAction("退出") + quit_action.triggered.connect(self.confirm_exit) + + self.tray_icon.setContextMenu(tray_menu) + self.tray_icon.activated.connect(self.tray_activated) + self.tray_icon.show() + + # 默认隐藏到托盘 + self.hide() + + def init_timers(self): + """初始化定时器""" + # 日志更新定时器 + self.log_timer = QTimer() + self.log_timer.timeout.connect(self.update_logs) + self.log_timer.start(500) + + # 流量更新定时器 + self.traffic_timer = QTimer() + self.traffic_timer.timeout.connect(self.update_traffic) + self.traffic_timer.start(1000) + + # 更新检查定时器 (1小时) + self.update_timer = QTimer() + self.update_timer.timeout.connect(self.auto_check_updates) + self.update_timer.start(3600000) # 1小时 + + def toggle_proxy(self): + """切换代理状态""" + if self.v2ray.running: + self.stop_proxy() + else: + self.start_proxy() + + def start_proxy(self): + """启动代理服务""" + # 验证配置 + if not self.app_context.validate_config(): + QMessageBox.warning(self, "配置错误", "请先设置有效的代理地址") + return + + # 启动v2ray + if not self.v2ray.start(): + QMessageBox.critical(self, "启动失败", "无法启动v2ray核心") + return + + # 配置系统代理 + if not self.proxy.enable(): + QMessageBox.warning(self, "代理警告", "系统代理配置失败,但v2ray仍在运行") + + self.toggle_btn.setText("停止代理") + self.tray_icon.setToolTip("EZProxy - 已启用") + + def stop_proxy(self): + """停止代理服务""" + self.proxy.disable() + self.v2ray.stop() + + self.toggle_btn.setText("启用代理") + self.tray_icon.setToolTip("EZProxy - 已停止") + + def update_logs(self): + """更新日志显示""" + while not self.v2ray.log_queue.empty(): + log_line = self.v2ray.log_queue.get() + self.log_view.append(log_line) + + # 保持滚动到底部 + scrollbar = self.log_view.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) + + def update_traffic(self): + """更新流量统计""" + if self.v2ray.running: + stats = self.v2ray.get_traffic_stats() + self.traffic_widget.update_stats(stats) + + def closeEvent(self, event): + """关闭事件处理""" + event.ignore() + self.hide() + self.tray_icon.showMessage( + "EZProxy", + "应用已最小化到系统托盘", + QSystemTrayIcon.Information, + 2000 + ) + + def confirm_exit(self): + """确认退出""" + if self.v2ray.running: + reply = QMessageBox.question( + self, "确认退出", + "代理服务正在运行,确定要退出吗?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + if reply == QMessageBox.No: + return + + self.stop_proxy() + QApplication.quit() + + def tray_activated(self, reason): + """托盘图标激活""" + if reason == QSystemTrayIcon.DoubleClick: + self.show() \ No newline at end of file diff --git a/ui/setup_dialog.py b/ui/setup_dialog.py index e69de29..96b8024 100644 --- a/ui/setup_dialog.py +++ b/ui/setup_dialog.py @@ -0,0 +1,85 @@ +from PyQt5.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLabel, + QLineEdit, QPushButton, QMessageBox +) +from PyQt5.QtCore import Qt +from urllib.parse import urlparse + +class SetupDialog(QDialog): + def __init__(self, app_context, parent=None): + super().__init__(parent) + self.app_context = app_context + self.setWindowTitle("设置代理地址") + self.setMinimumWidth(500) + + layout = QVBoxLayout() + + # 说明标签 + desc_label = QLabel("请输入您的VLESS代理地址 (格式: vless://...)") + desc_label.setWordWrap(True) + layout.addWidget(desc_label) + + # 代理地址输入 + self.url_input = QLineEdit() + self.url_input.setPlaceholderText("vless://uuid@server:port?type=tcp&security=reality&...") + layout.addWidget(self.url_input) + + # 按钮布局 + btn_layout = QHBoxLayout() + self.test_btn = QPushButton("测试连接") + self.test_btn.clicked.connect(self.test_connection) + self.save_btn = QPushButton("保存并继续") + self.save_btn.clicked.connect(self.save_and_continue) + self.cancel_btn = QPushButton("取消") + self.cancel_btn.clicked.connect(self.reject) + + btn_layout.addWidget(self.test_btn) + btn_layout.addStretch() + btn_layout.addWidget(self.save_btn) + btn_layout.addWidget(self.cancel_btn) + + layout.addLayout(btn_layout) + self.setLayout(layout) + + def test_connection(self): + """测试代理连接""" + url = self.url_input.text().strip() + if not url: + QMessageBox.warning(self, "输入错误", "请输入代理地址") + return + + if not url.startswith('vless://'): + QMessageBox.warning(self, "格式错误", "代理地址必须以 vless:// 开头") + return + + try: + parsed = urlparse(url) + if not parsed.hostname or not parsed.port: + raise ValueError("无效的服务器地址或端口") + + QMessageBox.information(self, "测试成功", + f"代理地址解析成功:\n服务器: {parsed.hostname}\n端口: {parsed.port}") + except Exception as e: + QMessageBox.warning(self, "测试失败", f"代理地址格式错误: {str(e)}") + + def save_and_continue(self): + """保存配置并继续""" + url = self.url_input.text().strip() + if not url: + QMessageBox.warning(self, "输入错误", "请输入代理地址") + return + + if not url.startswith('vless://'): + QMessageBox.warning(self, "格式错误", "代理地址必须以 vless:// 开头") + return + + # 保存到配置 + config = self.app_context.config + config['proxy_url'] = url + + # 保存配置文件 + if not save_config(config): + QMessageBox.critical(self, "保存失败", "无法保存配置文件") + return + + self.accept() \ No newline at end of file diff --git a/ui/traffic_widget.py b/ui/traffic_widget.py index e69de29..a6609b6 100644 --- a/ui/traffic_widget.py +++ b/ui/traffic_widget.py @@ -0,0 +1,163 @@ +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QProgressBar, QGroupBox +) +from PyQt5.QtCore import Qt, QTimer +from PyQt5.QtCharts import ( + QChart, QChartView, QLineSeries, + QValueAxis, QBarSet, QBarSeries, QBarCategoryAxis +) +from PyQt5.QtGui import QPainter, QColor, QPen + +class TrafficWidget(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.init_ui() + self.init_chart() + self.reset_stats() + + # 定时器用于模拟数据 + self.timer = QTimer() + self.timer.timeout.connect(self.update_fake_data) + self.timer.start(1000) + + def init_ui(self): + """初始化UI""" + layout = QVBoxLayout() + + # 速度显示 + speed_layout = QHBoxLayout() + self.download_label = QLabel("下载: 0.00 KB/s") + self.upload_label = QLabel("上传: 0.00 KB/s") + self.total_label = QLabel("总计: 0.00 MB") + + speed_layout.addWidget(self.download_label) + speed_layout.addWidget(self.upload_label) + speed_layout.addWidget(self.total_label) + speed_layout.addStretch() + + layout.addLayout(speed_layout) + + # 进度条 + self.progress = QProgressBar() + self.progress.setRange(0, 100) + layout.addWidget(self.progress) + + # 图表 + self.chart_view = QChartView() + self.chart_view.setRenderHint(QPainter.Antialiasing) + layout.addWidget(self.chart_view) + + self.setLayout(layout) + + def init_chart(self): + """初始化图表""" + self.chart = QChart() + self.chart.setTitle("实时流量统计") + self.chart.legend().setVisible(True) + + # 创建系列 + self.download_series = QLineSeries() + self.download_series.setName("下载") + self.download_series.setColor(QColor(52, 152, 219)) + + self.upload_series = QLineSeries() + self.upload_series.setName("上传") + self.upload_series.setColor(QColor(46, 204, 113)) + + self.chart.addSeries(self.download_series) + self.chart.addSeries(self.upload_series) + + # 创建坐标轴 + self.axis_x = QValueAxis() + self.axis_x.setTitleText("时间 (秒)") + self.axis_x.setRange(0, 60) + self.axis_x.setTickCount(7) + + self.axis_y = QValueAxis() + self.axis_y.setTitleText("流量 (KB)") + self.axis_y.setRange(0, 100) + + self.chart.addAxis(self.axis_x, Qt.AlignBottom) + self.chart.addAxis(self.axis_y, Qt.AlignLeft) + + self.download_series.attachAxis(self.axis_x) + self.download_series.attachAxis(self.axis_y) + self.upload_series.attachAxis(self.axis_x) + self.upload_series.attachAxis(self.axis_y) + + self.chart_view.setChart(self.chart) + + def reset_stats(self): + """重置统计数据""" + self.download_speed = 0.0 + self.upload_speed = 0.0 + self.total_download = 0.0 + self.total_upload = 0.0 + self.time_points = [] + self.download_points = [] + self.upload_points = [] + + def update_stats(self, stats): + """更新统计数据""" + self.download_speed = stats.get('download_speed', 0.0) + self.upload_speed = stats.get('upload_speed', 0.0) + self.total_download = stats.get('total_download', 0.0) + self.total_upload = stats.get('total_upload', 0.0) + + # 更新显示 + self.download_label.setText(f"下载: {self.download_speed:.2f} KB/s") + self.upload_label.setText(f"上传: {self.upload_speed:.2f} KB/s") + self.total_label.setText(f"总计: {(self.total_download + self.total_upload) / 1024:.2f} MB") + + # 更新进度条 + total_speed = self.download_speed + self.upload_speed + progress = min(100, int(total_speed / 1024)) # 假设1MB/s为满 + self.progress.setValue(progress) + self.progress.setFormat(f"总流量: {total_speed:.1f} KB/s") + + # 更新图表 + self.add_data_point() + + def add_data_point(self): + """添加数据点到图表""" + current_time = len(self.time_points) + + # 保留最近60秒的数据 + if len(self.time_points) >= 60: + self.time_points.pop(0) + self.download_points.pop(0) + self.upload_points.pop(0) + + self.time_points.append(current_time) + self.download_points.append(self.download_speed) + self.upload_points.append(self.upload_speed) + + # 更新系列数据 + self.download_series.clear() + self.upload_series.clear() + + for i, (t, d, u) in enumerate(zip(self.time_points, self.download_points, self.upload_points)): + self.download_series.append(i, d) + self.upload_series.append(i, u) + + # 调整Y轴范围 + max_value = max(max(self.download_points), max(self.upload_points), 100) * 1.1 + self.axis_y.setRange(0, max_value) + + def update_fake_data(self): + """模拟数据更新(实际应用中应替换为真实数据)""" + import random + fake_download = random.uniform(10, 500) + fake_upload = random.uniform(5, 100) + fake_total_download = self.total_download + fake_download + fake_total_upload = self.total_upload + fake_upload + + stats = { + 'download_speed': fake_download, + 'upload_speed': fake_upload, + 'total_download': fake_total_download, + 'total_upload': fake_total_upload + } + + self.update_stats(stats) \ No newline at end of file diff --git a/ui/update_dialog.py b/ui/update_dialog.py index e69de29..ad2b0c5 100644 --- a/ui/update_dialog.py +++ b/ui/update_dialog.py @@ -0,0 +1,143 @@ +from PyQt5.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLabel, + QPushButton, QProgressBar, QTextEdit, QMessageBox +) +from PyQt5.QtCore import Qt, QThread, pyqtSignal +import webbrowser +import logging + +class UpdateThread(QThread): + progress_updated = pyqtSignal(int, str) + finished = pyqtSignal(bool, str) + + def __init__(self, update_manager, update_type): + super().__init__() + self.update_manager = update_manager + self.update_type = update_type + + def run(self): + try: + self.progress_updated.emit(10, "准备更新...") + + if self.update_type == 'config': + success = self.update_manager.update_config() + message = "配置模板更新成功" if success else "配置模板更新失败" + elif self.update_type == 'app': + success = self.update_manager.start_app_update() + message = "应用更新已启动,程序将自动重启" if success else "应用更新启动失败" + else: + success = False + message = "未知更新类型" + + self.progress_updated.emit(100, "更新完成") + self.finished.emit(success, message) + except Exception as e: + logging.error(f"Update failed: {str(e)}") + self.finished.emit(False, f"更新失败: {str(e)}") + +class UpdateDialog(QDialog): + def __init__(self, update_info, update_manager, parent=None): + super().__init__(parent) + self.update_info = update_info + self.update_manager = update_manager + self.setWindowTitle("更新可用") + self.setMinimumSize(500, 300) + + layout = QVBoxLayout() + + # 版本信息 + if update_info['config']['available']: + config_label = QLabel(f"配置模板更新: {update_info['config']['version']}") + layout.addWidget(config_label) + + if update_info['app']['available']: + app_label = QLabel(f"应用更新: {update_info['app']['version']}") + layout.addWidget(app_label) + + # 更新日志 + self.changelog_view = QTextEdit() + self.changelog_view.setReadOnly(True) + self.changelog_view.setHtml("
点击'查看详情'查看更新日志
") + layout.addWidget(QLabel("更新内容:")) + layout.addWidget(self.changelog_view) + + # 按钮布局 + btn_layout = QHBoxLayout() + + self.details_btn = QPushButton("查看详情") + self.details_btn.clicked.connect(self.show_changelog) + + self.skip_btn = QPushButton("跳过本次更新") + self.skip_btn.setEnabled(not (update_info['config'].get('force', False) or + update_info['app'].get('force', False))) + self.skip_btn.clicked.connect(self.skip_update) + + self.update_btn = QPushButton("立即更新") + self.update_btn.clicked.connect(self.start_update) + + btn_layout.addWidget(self.details_btn) + btn_layout.addStretch() + btn_layout.addWidget(self.skip_btn) + btn_layout.addWidget(self.update_btn) + + layout.addLayout(btn_layout) + + # 进度条 + self.progress = QProgressBar() + self.progress.setVisible(False) + layout.addWidget(self.progress) + + self.setLayout(layout) + + def show_changelog(self): + """在浏览器中打开更新日志""" + url = self.update_info.get('changelog_url', '') + if url: + webbrowser.open(url) + else: + QMessageBox.information(self, "无更新日志", "没有提供更新日志链接") + + def skip_update(self): + """跳过更新""" + reply = QMessageBox.question( + self, "跳过更新", + "您确定要跳过本次更新吗?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply == QMessageBox.Yes: + self.reject() + + def start_update(self): + """开始更新""" + # 禁用按钮 + self.details_btn.setEnabled(False) + self.skip_btn.setEnabled(False) + self.update_btn.setEnabled(False) + + # 显示进度条 + self.progress.setVisible(True) + + # 创建更新线程 + self.update_thread = UpdateThread(self.update_manager, 'both') + self.update_thread.progress_updated.connect(self.update_progress) + self.update_thread.finished.connect(self.update_finished) + self.update_thread.start() + + def update_progress(self, value, message): + """更新进度""" + self.progress.setValue(value) + self.progress.setFormat(message) + + def update_finished(self, success, message): + """更新完成""" + if success: + QMessageBox.information(self, "更新成功", message) + self.accept() + else: + QMessageBox.critical(self, "更新失败", message) + self.details_btn.setEnabled(True) + self.skip_btn.setEnabled(True) + self.update_btn.setEnabled(True) + self.progress.setVisible(False) \ No newline at end of file diff --git a/update_helper.py b/update_helper.py index e69de29..be67165 100644 --- a/update_helper.py +++ b/update_helper.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +import sys +import time +import shutil +import zipfile +from pathlib import Path + +def main(): + if len(sys.argv) != 4: + print("Usage: update_helper.py