diff --git a/app/__pycache__/about_interface.cpython-39.pyc b/app/__pycache__/about_interface.cpython-39.pyc index ab4ca22..3490f28 100644 Binary files a/app/__pycache__/about_interface.cpython-39.pyc and b/app/__pycache__/about_interface.cpython-39.pyc differ diff --git a/app/__pycache__/ai_interface.cpython-39.pyc b/app/__pycache__/ai_interface.cpython-39.pyc index 27c46c1..aee47f8 100644 Binary files a/app/__pycache__/ai_interface.cpython-39.pyc and b/app/__pycache__/ai_interface.cpython-39.pyc differ diff --git a/app/__pycache__/code_configuration_interface.cpython-39.pyc b/app/__pycache__/code_configuration_interface.cpython-39.pyc index b341a88..42472b8 100644 Binary files a/app/__pycache__/code_configuration_interface.cpython-39.pyc and b/app/__pycache__/code_configuration_interface.cpython-39.pyc differ diff --git a/app/__pycache__/code_generate_interface.cpython-39.pyc b/app/__pycache__/code_generate_interface.cpython-39.pyc index b4cffe4..e239a45 100644 Binary files a/app/__pycache__/code_generate_interface.cpython-39.pyc and b/app/__pycache__/code_generate_interface.cpython-39.pyc differ diff --git a/app/__pycache__/data_interface.cpython-39.pyc b/app/__pycache__/data_interface.cpython-39.pyc index e389e44..7df10bd 100644 Binary files a/app/__pycache__/data_interface.cpython-39.pyc and b/app/__pycache__/data_interface.cpython-39.pyc differ diff --git a/app/__pycache__/function_fit_interface.cpython-39.pyc b/app/__pycache__/function_fit_interface.cpython-39.pyc index dd6a1b8..886cba0 100644 Binary files a/app/__pycache__/function_fit_interface.cpython-39.pyc and b/app/__pycache__/function_fit_interface.cpython-39.pyc differ diff --git a/app/__pycache__/home_interface.cpython-39.pyc b/app/__pycache__/home_interface.cpython-39.pyc index 7b34d20..311a744 100644 Binary files a/app/__pycache__/home_interface.cpython-39.pyc and b/app/__pycache__/home_interface.cpython-39.pyc differ diff --git a/app/__pycache__/main_window.cpython-39.pyc b/app/__pycache__/main_window.cpython-39.pyc index e6c11c8..8e971ac 100644 Binary files a/app/__pycache__/main_window.cpython-39.pyc and b/app/__pycache__/main_window.cpython-39.pyc differ diff --git a/app/__pycache__/mini_tool_interface.cpython-39.pyc b/app/__pycache__/mini_tool_interface.cpython-39.pyc index 5ee51b8..79f3a2a 100644 Binary files a/app/__pycache__/mini_tool_interface.cpython-39.pyc and b/app/__pycache__/mini_tool_interface.cpython-39.pyc differ diff --git a/app/__pycache__/part_library_interface.cpython-39.pyc b/app/__pycache__/part_library_interface.cpython-39.pyc index 1c409d4..74e20b5 100644 Binary files a/app/__pycache__/part_library_interface.cpython-39.pyc and b/app/__pycache__/part_library_interface.cpython-39.pyc differ diff --git a/app/__pycache__/serial_terminal_interface.cpython-39.pyc b/app/__pycache__/serial_terminal_interface.cpython-39.pyc index 52fd265..9671994 100644 Binary files a/app/__pycache__/serial_terminal_interface.cpython-39.pyc and b/app/__pycache__/serial_terminal_interface.cpython-39.pyc differ diff --git a/app/about_interface.py b/app/about_interface.py index b1ddc73..f2eb477 100644 --- a/app/about_interface.py +++ b/app/about_interface.py @@ -1,3 +1,4 @@ +import os from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout from PyQt5.QtCore import Qt, QUrl, QTimer from PyQt5.QtGui import QDesktopServices @@ -12,8 +13,9 @@ from qfluentwidgets import ( from .function_fit_interface import FunctionFitInterface from app.tools.check_update import check_update from app.tools.auto_updater import AutoUpdater, check_update_availability +from app.tools.update_check_thread import UpdateCheckThread -__version__ = "1.0.2" +__version__ = "1.0.6" class AboutInterface(QWidget): def __init__(self, parent=None): @@ -32,13 +34,7 @@ class AboutInterface(QWidget): layout.setAlignment(Qt.AlignmentFlag.AlignTop) layout.setContentsMargins(20, 30, 20, 20) - # 页面标题 - title = SubtitleLabel("MRobot 帮助页面", self) - title.setAlignment(Qt.AlignmentFlag.AlignCenter) - layout.addWidget(title) - layout.addSpacing(10) - - # 版本信息卡片 - 学习AI界面风格 + # 版本信息卡片 version_card = ElevatedCardWidget() version_layout = QVBoxLayout(version_card) version_layout.setContentsMargins(24, 20, 24, 20) @@ -57,7 +53,7 @@ class AboutInterface(QWidget): text="检查更新", icon=FluentIcon.SYNC, title="检查更新", - content="检查是否有新版本可用", + content="检查是否有新版本可用(需要能够连接github)", ) self.check_update_card.clicked.connect(self.check_for_updates) layout.addWidget(self.check_update_card) @@ -119,7 +115,8 @@ class AboutInterface(QWidget): self.notes_display = TextEdit() self.notes_display.setReadOnly(True) - self.notes_display.setFixedHeight(200) + self.notes_display.setMaximumHeight(200) + self.notes_display.setMinimumHeight(80) self.notes_display.setText("暂无更新说明") layout.addWidget(self.notes_display) @@ -135,6 +132,11 @@ class AboutInterface(QWidget): self.progress_bar.setValue(0) progress_layout.addWidget(self.progress_bar) + # 详细下载信息 + self.download_info = BodyLabel("") + self.download_info.setWordWrap(True) + progress_layout.addWidget(self.download_info) + self.progress_widget.hide() layout.addWidget(self.progress_widget) @@ -170,16 +172,58 @@ class AboutInterface(QWidget): def _perform_check(self): """执行更新检查""" try: + # 获取最新版本信息(包括当前版本的详细信息) + latest_info = self._get_latest_release_info() + + # 检查是否有可用更新 self.update_info = check_update_availability(__version__) if self.update_info: self._show_update_available() else: - self._show_no_update() + self._show_no_update(latest_info) except Exception as e: self._show_error(f"检查更新失败: {str(e)}") + def _get_latest_release_info(self): + """获取最新发布信息,不论版本是否需要更新""" + try: + import requests + from packaging.version import parse as vparse + + url = f"https://api.github.com/repos/goldenfishs/MRobot/releases/latest" + response = requests.get(url, timeout=10) + + if response.status_code == 200: + release_data = response.json() + latest_version = release_data["tag_name"].lstrip("v") + + # 获取下载URL和文件大小 + assets = release_data.get('assets', []) + asset_size = 0 + download_url = None + + if assets: + # 选择第一个资源文件 + asset = assets[0] + asset_size = asset.get('size', 0) + download_url = asset.get('browser_download_url', '') + + return { + 'version': latest_version, + 'release_notes': release_data.get('body', '暂无更新说明'), + 'release_date': release_data.get('published_at', ''), + 'asset_size': asset_size, + 'download_url': download_url + } + else: + return None + + except Exception as e: + print(f"获取发布信息失败: {e}") + return None + def _show_update_available(self): """显示发现更新""" # 更新按钮状态 @@ -216,11 +260,43 @@ class AboutInterface(QWidget): duration=3000 ) - def _show_no_update(self): - """显示无更新""" + def _show_no_update(self, latest_info=None): + """显示无更新,但展示最新版本信息""" self.check_update_card.setEnabled(True) self.check_update_card.setContent("已是最新版本") + # 如果有最新版本信息,显示详情卡片 + if latest_info: + self.update_info_card.show() + + # 显示版本信息(当前版本就是最新版本) + self.latest_version_label.setText(f"v{__version__}") + + # 设置文件信息 + asset_size = latest_info.get('asset_size', 0) + file_size = self._format_file_size(asset_size) + self.file_size_label.setText(f"文件大小: {file_size}") + + # 设置发布时间 + release_date = latest_info.get('release_date', '') + formatted_date = self._format_date(release_date) + self.release_date_label.setText(f"发布时间: {formatted_date}") + + # 设置更新说明 + notes = latest_info.get('release_notes', '暂无更新说明') + self.notes_display.setText(notes[:500] + ('...' if len(notes) > 500 else '')) + + # 修改标题和按钮 + self.update_title.setText("版本信息") + self.update_btn.setText("手动下载") + self.update_btn.setIcon(FluentIcon.DOWNLOAD) + self.update_btn.setEnabled(True) + self.manual_btn.setEnabled(True) + + # 连接手动下载功能 + self.update_btn.clicked.disconnect() + self.update_btn.clicked.connect(self.open_manual_download) + InfoBar.info( title="已是最新版本", content="当前已是最新版本,无需更新。", @@ -252,9 +328,10 @@ class AboutInterface(QWidget): self.update_btn.setEnabled(False) self.manual_btn.setEnabled(False) - # 启动更新器 + # 启动更新器(使用简化的单线程下载) self.updater = AutoUpdater(__version__) self.updater.signals.progress_changed.connect(self.update_progress) + self.updater.signals.download_progress.connect(self.update_download_progress) self.updater.signals.status_changed.connect(self.update_status) self.updater.signals.error_occurred.connect(self.update_error) self.updater.signals.update_completed.connect(self.update_completed) @@ -266,6 +343,29 @@ class AboutInterface(QWidget): """更新进度""" self.progress_bar.setValue(value) + def update_download_progress(self, downloaded: int, total: int, speed: float, remaining: float): + """更新下载进度详情""" + if total > 0: + downloaded_str = self._format_bytes(downloaded) + total_str = self._format_bytes(total) + percentage = (downloaded / total) * 100 + + info_text = f"已下载: {downloaded_str} / {total_str} ({percentage:.1f}%)" + + self.download_info.setText(info_text) + + def _format_bytes(self, size_bytes: int) -> str: + """格式化字节大小""" + if size_bytes == 0: + return "0 B" + + size = float(size_bytes) + for unit in ['B', 'KB', 'MB', 'GB']: + if size < 1024.0: + return f"{size:.1f} {unit}" + size /= 1024.0 + return f"{size:.1f} TB" + def update_status(self, status: str): """更新状态""" self.progress_label.setText(status) @@ -276,34 +376,106 @@ class AboutInterface(QWidget): self.update_btn.setEnabled(True) self.manual_btn.setEnabled(True) - InfoBar.error( - title="更新失败", - content=error_msg, - parent=self, - position=InfoBarPosition.TOP, - duration=4000 - ) + # 如果是平台兼容性问题,提供更友好的提示 + if "Windows 安装程序" in error_msg and "当前系统是" in error_msg: + InfoBar.warning( + title="平台不兼容", + content="检测到 Windows 安装程序,请点击'手动下载'获取适合 macOS 的版本", + parent=self, + position=InfoBarPosition.TOP, + duration=6000 + ) + else: + InfoBar.error( + title="更新失败", + content=error_msg, + parent=self, + position=InfoBarPosition.TOP, + duration=4000 + ) - def update_completed(self): - """更新完成""" - self.progress_label.setText("更新完成,准备重启...") + def update_completed(self, file_path=None): + """更新完成 - 显示下载文件位置""" + print(f"update_completed called with file_path: {file_path}") # 调试输出 + + self.progress_label.setText("下载完成!") self.progress_bar.setValue(100) - InfoBar.success( - title="更新完成", - content="更新安装完成,程序将在3秒后重启", - parent=self, - position=InfoBarPosition.TOP, - duration=3000 - ) + # 重新启用按钮 + self.update_btn.setEnabled(True) + self.manual_btn.setEnabled(True) - # 延迟重启 - QTimer.singleShot(3000, self._restart_app) + if file_path and os.path.exists(file_path): + print(f"File exists: {file_path}") # 调试输出 + InfoBar.success( + title="下载完成", + content="安装文件已下载完成,点击下方按钮打开文件位置", + parent=self, + position=InfoBarPosition.TOP, + duration=5000 + ) + + # 添加打开文件夹按钮 + self._add_open_folder_button(file_path) + else: + print(f"File does not exist or file_path is None: {file_path}") # 调试输出 + InfoBar.success( + title="下载完成", + content="文件下载完成", + parent=self, + position=InfoBarPosition.TOP, + duration=3000 + ) + + def _add_open_folder_button(self, file_path): + """添加打开文件夹按钮""" + def open_file_location(): + folder_path = os.path.dirname(file_path) + # 在 macOS 上使用 Finder 打开文件夹 + QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path)) + + InfoBar.info( + title="已打开文件夹", + content=f"文件位置: {folder_path}", + parent=self, + position=InfoBarPosition.TOP, + duration=3000 + ) + + # 直接替换更新按钮的文本和功能 + self.update_btn.setText("打开文件位置") + self.update_btn.setIcon(FluentIcon.FOLDER) + # 断开原有连接 + self.update_btn.clicked.disconnect() + # 连接新功能 + self.update_btn.clicked.connect(open_file_location) + + # 修改取消按钮为清理按钮 + self.cancel_btn.setText("清理临时文件") + self.cancel_btn.setIcon(FluentIcon.DELETE) + self.cancel_btn.clicked.disconnect() + + def cleanup_temp_files(): + if self.updater: + self.updater.cleanup() + InfoBar.success( + title="已清理", + content="临时文件已清理完成", + parent=self, + position=InfoBarPosition.TOP, + duration=2000 + ) + # 重置界面 + self.update_info_card.hide() + self.check_update_card.setContent("检查是否有新版本可用") + + self.cancel_btn.clicked.connect(cleanup_temp_files) def cancel_update(self): """取消更新""" - if self.updater and self.updater.isRunning(): + if hasattr(self, 'updater') and self.updater and self.updater.isRunning(): self.updater.cancel_update() + self.updater.cleanup() self.update_info_card.hide() self.check_update_card.setContent("检查是否有新版本可用") diff --git a/app/code_page/__pycache__/bsp_interface.cpython-39.pyc b/app/code_page/__pycache__/bsp_interface.cpython-39.pyc index cc9b996..397cf95 100644 Binary files a/app/code_page/__pycache__/bsp_interface.cpython-39.pyc and b/app/code_page/__pycache__/bsp_interface.cpython-39.pyc differ diff --git a/app/tools/__pycache__/analyzing_ioc.cpython-39.pyc b/app/tools/__pycache__/analyzing_ioc.cpython-39.pyc index d9bf58a..0310cdc 100644 Binary files a/app/tools/__pycache__/analyzing_ioc.cpython-39.pyc and b/app/tools/__pycache__/analyzing_ioc.cpython-39.pyc differ diff --git a/app/tools/__pycache__/auto_updater.cpython-39.pyc b/app/tools/__pycache__/auto_updater.cpython-39.pyc new file mode 100644 index 0000000..a67e931 Binary files /dev/null and b/app/tools/__pycache__/auto_updater.cpython-39.pyc differ diff --git a/app/tools/__pycache__/check_update.cpython-39.pyc b/app/tools/__pycache__/check_update.cpython-39.pyc index de74a03..8f378d9 100644 Binary files a/app/tools/__pycache__/check_update.cpython-39.pyc and b/app/tools/__pycache__/check_update.cpython-39.pyc differ diff --git a/app/tools/__pycache__/code_generator.cpython-39.pyc b/app/tools/__pycache__/code_generator.cpython-39.pyc index 4449429..390ddd7 100644 Binary files a/app/tools/__pycache__/code_generator.cpython-39.pyc and b/app/tools/__pycache__/code_generator.cpython-39.pyc differ diff --git a/app/tools/__pycache__/code_task_config.cpython-39.pyc b/app/tools/__pycache__/code_task_config.cpython-39.pyc index cf0389f..6aba332 100644 Binary files a/app/tools/__pycache__/code_task_config.cpython-39.pyc and b/app/tools/__pycache__/code_task_config.cpython-39.pyc differ diff --git a/app/tools/__pycache__/multi_thread_downloader.cpython-39.pyc b/app/tools/__pycache__/multi_thread_downloader.cpython-39.pyc new file mode 100644 index 0000000..748392c Binary files /dev/null and b/app/tools/__pycache__/multi_thread_downloader.cpython-39.pyc differ diff --git a/app/tools/__pycache__/part_download.cpython-39.pyc b/app/tools/__pycache__/part_download.cpython-39.pyc index 3edd79f..dc02841 100644 Binary files a/app/tools/__pycache__/part_download.cpython-39.pyc and b/app/tools/__pycache__/part_download.cpython-39.pyc differ diff --git a/app/tools/__pycache__/update_check_thread.cpython-39.pyc b/app/tools/__pycache__/update_check_thread.cpython-39.pyc new file mode 100644 index 0000000..a77f001 Binary files /dev/null and b/app/tools/__pycache__/update_check_thread.cpython-39.pyc differ diff --git a/app/tools/__pycache__/update_code.cpython-39.pyc b/app/tools/__pycache__/update_code.cpython-39.pyc index 9e663d2..cc982da 100644 Binary files a/app/tools/__pycache__/update_code.cpython-39.pyc and b/app/tools/__pycache__/update_code.cpython-39.pyc differ diff --git a/app/tools/auto_updater.py b/app/tools/auto_updater.py index 18f174e..96825f3 100644 --- a/app/tools/auto_updater.py +++ b/app/tools/auto_updater.py @@ -18,13 +18,16 @@ import requests from packaging.version import parse as vparse from PyQt5.QtCore import QThread, pyqtSignal, QObject +# 移除多线程下载器依赖 + class UpdaterSignals(QObject): """更新器信号类""" progress_changed = pyqtSignal(int) # 进度变化信号 (0-100) + download_progress = pyqtSignal(int, int, float, float) # 下载进度: 已下载字节, 总字节, 速度MB/s, 剩余时间秒 status_changed = pyqtSignal(str) # 状态变化信号 error_occurred = pyqtSignal(str) # 错误信号 - update_completed = pyqtSignal() # 更新完成信号 + update_completed = pyqtSignal(str) # 更新完成信号,可选包含文件路径 update_cancelled = pyqtSignal() # 更新取消信号 @@ -43,6 +46,9 @@ class AutoUpdater(QThread): self.app_dir = self._get_app_directory() self.temp_dir = None + # 多线程下载器 + self.downloader = None + def _get_app_directory(self) -> str: """获取应用程序目录""" if self.is_frozen: @@ -55,6 +61,8 @@ class AutoUpdater(QThread): def cancel_update(self): """取消更新""" self.cancelled = True + if self.downloader: + self.downloader.cancel() self.signals.update_cancelled.emit() def check_for_updates(self) -> Optional[dict]: @@ -70,12 +78,15 @@ class AutoUpdater(QThread): latest_version = release_data["tag_name"].lstrip("v") if vparse(latest_version) > vparse(self.current_version): + download_url = self._get_download_url(release_data) + asset_size = self._get_asset_size(release_data, download_url) if download_url else 0 return { 'version': latest_version, - 'download_url': self._get_download_url(release_data), + 'download_url': download_url, 'release_notes': release_data.get('body', ''), 'release_date': release_data.get('published_at', ''), - 'asset_name': self._get_asset_name(release_data) + 'asset_name': self._get_asset_name(release_data), + 'asset_size': asset_size } return None else: @@ -100,17 +111,32 @@ class AutoUpdater(QThread): if name.endswith('.exe') or name.endswith('.zip'): return asset['browser_download_url'] elif system == 'darwin': # macOS - if name.endswith('.dmg') or name.endswith('.zip'): + # 优先选择 dmg 或 zip 文件 + if name.endswith('.dmg'): + return asset['browser_download_url'] + if name.endswith('.zip') and 'macos' in name: return asset['browser_download_url'] elif system == 'linux': - if name.endswith('.tar.gz') or name.endswith('.zip'): + if name.endswith('.tar.gz') or (name.endswith('.zip') and 'linux' in name): return asset['browser_download_url'] - # 如果没找到特定平台的,返回第一个可用文件 + # 如果没找到特定平台的,在 macOS 上避免选择 .exe 文件 for asset in assets: name = asset['name'].lower() - if any(name.endswith(ext) for ext in ['.zip', '.exe', '.dmg', '.tar.gz']): - return asset['browser_download_url'] + if system == 'darwin': + # macOS 优先选择非 exe 文件 + if name.endswith('.zip') or name.endswith('.dmg') or name.endswith('.tar.gz'): + return asset['browser_download_url'] + else: + if any(name.endswith(ext) for ext in ['.zip', '.exe', '.dmg', '.tar.gz']): + return asset['browser_download_url'] + + # 最后才选择 exe 文件(如果没有其他选择) + if system == 'darwin': + for asset in assets: + name = asset['name'].lower() + if name.endswith('.exe'): + return asset['browser_download_url'] return None @@ -121,46 +147,95 @@ class AutoUpdater(QThread): return os.path.basename(urlparse(download_url).path) return None + def _get_asset_size(self, release_data: dict, download_url: str) -> int: + """获取资源文件大小""" + if not download_url: + return 0 + + assets = release_data.get('assets', []) + for asset in assets: + if asset.get('browser_download_url') == download_url: + return asset.get('size', 0) + return 0 + def download_update(self, download_url: str, filename: str) -> Optional[str]: - """下载更新文件""" + """下载更新文件 - 使用简单的单线程下载""" try: - self.signals.status_changed.emit("正在下载更新...") + self.signals.status_changed.emit("开始下载...") # 创建临时目录 self.temp_dir = tempfile.mkdtemp(prefix="MRobot_update_") file_path = os.path.join(self.temp_dir, filename) - response = requests.get(download_url, stream=True, timeout=30) + print(f"Downloading to: {file_path}") + + # 使用简单的requests下载 + import requests + headers = { + 'User-Agent': 'MRobot-Updater/1.0', + 'Accept': '*/*', + } + + response = requests.get(download_url, headers=headers, stream=True, timeout=30) response.raise_for_status() - total_size = int(response.headers.get('content-length', 0)) + # 获取文件总大小 + total_size = int(response.headers.get('Content-Length', 0)) downloaded_size = 0 + # 确保目录存在 + os.makedirs(os.path.dirname(file_path), exist_ok=True) + + # 下载文件 with open(file_path, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): if self.cancelled: + print("Download cancelled by user") return None if chunk: f.write(chunk) downloaded_size += len(chunk) + # 更新进度 if total_size > 0: - progress = int((downloaded_size / total_size) * 50) # 下载占50%进度 + progress = int((downloaded_size / total_size) * 100) self.signals.progress_changed.emit(progress) + + # 发送详细进度信息 + speed = 0 # 简化版本不计算速度 + remaining = 0 + self.signals.download_progress.emit(downloaded_size, total_size, speed, remaining) - self.signals.status_changed.emit("下载完成") - return file_path + # 验证下载的文件 + if os.path.exists(file_path) and os.path.getsize(file_path) > 0: + print(f"Download completed successfully: {file_path}") + self.signals.status_changed.emit("下载完成!") + return file_path + else: + raise Exception("下载的文件不存在或为空") except Exception as e: - self.signals.error_occurred.emit(f"下载失败: {str(e)}") + error_msg = f"下载失败: {str(e)}" + print(f"Download error: {error_msg}") + self.signals.error_occurred.emit(error_msg) return None + def _on_download_progress(self, downloaded: int, total: int, speed: float, remaining: float): + """处理下载进度""" + # 转发详细下载进度 + self.signals.download_progress.emit(downloaded, total, speed, remaining) + + # 计算总体进度 (下载占80%,其他操作占20%) + if total > 0: + download_progress = int((downloaded / total) * 80) + self.signals.progress_changed.emit(download_progress) + def extract_update(self, file_path: str) -> Optional[str]: """解压更新文件""" try: self.signals.status_changed.emit("正在解压文件...") - self.signals.progress_changed.emit(50) + self.signals.progress_changed.emit(85) if not self.temp_dir: raise Exception("临时目录未初始化") @@ -176,10 +251,19 @@ class AutoUpdater(QThread): import tarfile with tarfile.open(file_path, 'r:gz') as tar_ref: tar_ref.extractall(extract_dir) + elif file_path.endswith('.exe'): + # 对于 .exe 文件,在非 Windows 系统上提示手动安装 + current_system = platform.system() + if current_system != 'Windows': + self.signals.error_occurred.emit(f"下载的是 Windows 安装程序,当前系统是 {current_system}。\n请手动下载适合您系统的版本。") + return None + else: + # Windows 系统直接返回文件路径,由安装函数处理 + return file_path else: raise Exception(f"不支持的文件格式: {file_path}") - self.signals.progress_changed.emit(70) + self.signals.progress_changed.emit(90) self.signals.status_changed.emit("解压完成") return extract_dir @@ -191,7 +275,7 @@ class AutoUpdater(QThread): """安装更新""" try: self.signals.status_changed.emit("正在安装更新...") - self.signals.progress_changed.emit(80) + self.signals.progress_changed.emit(95) if not self.temp_dir: raise Exception("临时目录未初始化") @@ -206,7 +290,7 @@ class AutoUpdater(QThread): # 复制新文件 self._copy_update_files(extract_dir) - self.signals.progress_changed.emit(95) + self.signals.progress_changed.emit(99) self.signals.status_changed.emit("安装完成") return True @@ -295,43 +379,81 @@ class AutoUpdater(QThread): def cleanup(self): """清理临时文件""" + print(f"cleanup() called, temp_dir: {self.temp_dir}") # 调试输出 if self.temp_dir and os.path.exists(self.temp_dir): try: + print(f"Removing temp directory: {self.temp_dir}") # 调试输出 shutil.rmtree(self.temp_dir) + print("Temp directory removed successfully") # 调试输出 except Exception as e: print(f"清理临时文件失败: {e}") + else: + print("No temp directory to clean up") # 调试输出 def run(self): """执行更新流程""" try: + self.signals.status_changed.emit("开始更新流程...") + # 检查更新 + self.signals.status_changed.emit("正在获取更新信息...") update_info = self.check_for_updates() if not update_info or self.cancelled: + self.signals.status_changed.emit("未找到更新信息或已取消") return + self.signals.status_changed.emit(f"准备下载版本 {update_info['version']}") + # 下载更新 downloaded_file = self.download_update( update_info['download_url'], update_info['asset_name'] ) if not downloaded_file or self.cancelled: + self.signals.status_changed.emit("下载失败或已取消") return + self.signals.status_changed.emit("下载完成!") + self.signals.progress_changed.emit(100) + + # 检查是否为exe文件且当前系统非Windows + current_system = platform.system() + print(f"Downloaded file: {downloaded_file}") # 调试输出 + print(f"Current system: {current_system}") # 调试输出 + if downloaded_file.endswith('.exe') and current_system != 'Windows': + # 直接完成,返回文件路径 + print(f"Emitting update_completed signal with file path: {downloaded_file}") # 调试输出 + self.signals.update_completed.emit(downloaded_file) + # 不要立即清理,让用户有时间访问文件 + print("Skipping cleanup to preserve downloaded file") # 调试输出 + return + + # 对于其他情况,继续原有流程 + self.signals.status_changed.emit("下载完成,开始解压...") + # 解压更新 extract_dir = self.extract_update(downloaded_file) if not extract_dir or self.cancelled: + self.signals.status_changed.emit("解压失败或已取消") return # 安装更新 + self.signals.status_changed.emit("开始安装更新...") if self.install_update(extract_dir) and not self.cancelled: self.signals.progress_changed.emit(100) - self.signals.update_completed.emit() + self.signals.status_changed.emit("更新安装完成") + self.signals.update_completed.emit("") # 传递空字符串表示正常安装完成 + else: + self.signals.status_changed.emit("安装失败或已取消") except Exception as e: - self.signals.error_occurred.emit(f"更新过程中发生错误: {str(e)}") + error_msg = f"更新过程中发生错误: {str(e)}" + print(f"AutoUpdater error: {error_msg}") # 调试输出 + self.signals.error_occurred.emit(error_msg) finally: - # 清理临时文件 - self.cleanup() + # 对于下载完成的情况,延迟清理临时文件 + # 这样用户有时间访问下载的文件 + pass # 暂时不在这里清理 def check_update_availability(current_version: str, repo: str = "goldenfishs/MRobot") -> Optional[dict]: diff --git a/app/tools/check_update.py b/app/tools/check_update.py index 426f3ab..d59e300 100644 --- a/app/tools/check_update.py +++ b/app/tools/check_update.py @@ -1,5 +1,7 @@ import requests +import platform from packaging.version import parse as vparse +from typing import Optional def check_update(local_version, repo="goldenfishs/MRobot"): url = f"https://api.github.com/repos/{repo}/releases/latest" @@ -11,4 +13,62 @@ def check_update(local_version, repo="goldenfishs/MRobot"): else: return None else: - raise RuntimeError("GitHub API 请求失败") \ No newline at end of file + raise RuntimeError("GitHub API 请求失败") + +def check_update_availability(current_version: str, repo: str = "goldenfishs/MRobot") -> Optional[dict]: + """检查更新并返回详细信息""" + try: + url = f"https://api.github.com/repos/{repo}/releases/latest" + response = requests.get(url, timeout=10) + + if response.status_code == 200: + release_data = response.json() + latest_version = release_data["tag_name"].lstrip("v") + + if vparse(latest_version) > vparse(current_version): + # 获取适合当前平台的下载链接和文件大小 + download_url, asset_size, asset_name = _get_platform_asset(release_data) + + return { + 'version': latest_version, + 'download_url': download_url, + 'asset_size': asset_size, + 'asset_name': asset_name, + 'release_notes': release_data.get('body', ''), + 'release_date': release_data.get('published_at', ''), + } + return None + else: + raise Exception(f"GitHub API请求失败: {response.status_code}") + + except Exception as e: + raise Exception(f"检查更新失败: {str(e)}") + +def _get_platform_asset(release_data: dict) -> tuple: + """获取适合当前平台的资源文件信息""" + assets = release_data.get('assets', []) + system = platform.system().lower() + + # 根据操作系统选择合适的安装包 + for asset in assets: + name = asset['name'].lower() + + if system == 'windows': + if 'installer' in name and name.endswith('.exe'): + return asset['browser_download_url'], asset.get('size', 0), asset['name'] + if name.endswith('.exe') or name.endswith('.zip'): + return asset['browser_download_url'], asset.get('size', 0), asset['name'] + elif system == 'darwin': # macOS + if name.endswith('.dmg') or name.endswith('.zip'): + return asset['browser_download_url'], asset.get('size', 0), asset['name'] + elif system == 'linux': + if name.endswith('.tar.gz') or name.endswith('.zip'): + return asset['browser_download_url'], asset.get('size', 0), asset['name'] + + # 如果没找到特定平台的,返回第一个可用文件 + for asset in assets: + name = asset['name'].lower() + if any(name.endswith(ext) for ext in ['.zip', '.exe', '.dmg', '.tar.gz']): + return asset['browser_download_url'], asset.get('size', 0), asset['name'] + + return None, 0, None \ No newline at end of file diff --git a/app/tools/update_check_thread.py b/app/tools/update_check_thread.py new file mode 100644 index 0000000..f242144 --- /dev/null +++ b/app/tools/update_check_thread.py @@ -0,0 +1,34 @@ +""" +更新检查线程 +避免阻塞UI界面的更新检查 +""" + +from PyQt5.QtCore import QThread, pyqtSignal +from app.tools.check_update import check_update_availability + + +class UpdateCheckThread(QThread): + """更新检查线程""" + + # 信号定义 + update_found = pyqtSignal(dict) # 发现更新 + no_update = pyqtSignal() # 无更新 + error_occurred = pyqtSignal(str) # 检查出错 + + def __init__(self, current_version: str, repo: str = "goldenfishs/MRobot"): + super().__init__() + self.current_version = current_version + self.repo = repo + + def run(self): + """执行更新检查""" + try: + update_info = check_update_availability(self.current_version, self.repo) + + if update_info: + self.update_found.emit(update_info) + else: + self.no_update.emit() + + except Exception as e: + self.error_occurred.emit(str(e)) \ No newline at end of file diff --git a/assets/User_code/device/motor_rm.c b/assets/User_code/device/motor_rm.c index 9b5ea44..1e78a47 100644 --- a/assets/User_code/device/motor_rm.c +++ b/assets/User_code/device/motor_rm.c @@ -74,7 +74,7 @@ static int8_t MOTOR_RM_GetLogicalIndex(uint16_t can_id, MOTOR_RM_Module_t module static float MOTOR_RM_GetRatio(MOTOR_RM_Module_t module) { switch (module) { case MOTOR_M2006: return 36.0f; - case MOTOR_M3508: return 19.0f; + case MOTOR_M3508: return 3591.0f / 187.0f; case MOTOR_GM6020: return 1.0f; default: return 1.0f; } diff --git a/demo_auto_update.py b/demo_auto_update.py deleted file mode 100644 index f6d5b03..0000000 --- a/demo_auto_update.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env python3 -""" -自动更新功能演示 -展示如何使用自动更新功能的完整示例 -""" - -import sys -import os - -# 添加项目路径 -sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) - -from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel -from PyQt5.QtCore import Qt - -def demo_auto_update(): - """演示自动更新功能""" - from app.tools.update_dialog import UpdateDialog - - app = QApplication(sys.argv) - - # 创建主窗口 - window = QWidget() - window.setWindowTitle("自动更新演示") - window.setFixedSize(300, 200) - - layout = QVBoxLayout(window) - - # 标题 - title = QLabel("MRobot 自动更新演示") - title.setAlignment(Qt.AlignCenter) - title.setStyleSheet("font-size: 16px; font-weight: bold; margin: 20px;") - layout.addWidget(title) - - # 当前版本显示 - version_label = QLabel("当前版本: v1.0.0") - version_label.setAlignment(Qt.AlignCenter) - layout.addWidget(version_label) - - # 更新按钮 - update_btn = QPushButton("检查并更新") - update_btn.clicked.connect(lambda: show_update_dialog(window)) - layout.addWidget(update_btn) - - # 说明 - info_label = QLabel("点击按钮体验自动更新功能") - info_label.setAlignment(Qt.AlignCenter) - info_label.setStyleSheet("color: gray; font-size: 12px;") - layout.addWidget(info_label) - - window.show() - - def show_update_dialog(parent): - """显示更新对话框""" - dialog = UpdateDialog("1.0.0", parent) - dialog.exec_() - - sys.exit(app.exec_()) - -def demo_quick_check(): - """演示快速更新检查""" - from app.tools.update_dialog import QuickUpdateChecker - - print("演示快速更新检查功能...") - - # 检查更新但不显示对话框 - result = QuickUpdateChecker.check_and_notify("1.0.0", None, auto_show_dialog=False) - - if result: - print("✅ 发现更新并显示了通知") - else: - print("ℹ️ 当前已是最新版本或检查失败") - -def demo_api_usage(): - """演示API使用方法""" - from app.tools.auto_updater import check_update_availability - - print("\n演示API使用方法...") - - # 检查更新 - current_version = "1.0.0" - update_info = check_update_availability(current_version) - - if update_info: - print("📦 发现新版本!") - print(f" 版本号: {update_info['version']}") - print(f" 下载链接: {update_info['download_url']}") - print(f" 文件名: {update_info['asset_name']}") - print(f" 发布日期: {update_info['release_date']}") - print(f" 更新说明: {update_info['release_notes'][:100]}...") - else: - print("ℹ️ 当前已是最新版本") - -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser(description="MRobot 自动更新功能演示") - parser.add_argument("--mode", choices=["ui", "quick", "api"], default="ui", - help="演示模式: ui=图形界面, quick=快速检查, api=API演示") - - args = parser.parse_args() - - print("🚀 MRobot 自动更新功能演示") - print("="*40) - - if args.mode == "ui": - print("启动图形界面演示...") - demo_auto_update() - elif args.mode == "quick": - demo_quick_check() - elif args.mode == "api": - demo_api_usage() - - print("\n演示完成!") \ No newline at end of file diff --git a/test_auto_update.py b/test_auto_update.py deleted file mode 100644 index 7a81353..0000000 --- a/test_auto_update.py +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env python3 -""" -自动更新功能测试脚本 -用于测试自动更新功能的各个组件 -""" - -import sys -import os - -# 添加项目路径到 Python 路径 -sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) - -def test_update_check(): - """测试更新检查功能""" - print("测试更新检查功能...") - - try: - from app.tools.auto_updater import check_update_availability - - current_version = "1.0.0" # 使用一个较低的版本号来测试 - result = check_update_availability(current_version) - - if result: - print(f"✅ 发现新版本: {result['version']}") - print(f" 下载链接: {result['download_url']}") - print(f" 文件名: {result['asset_name']}") - return True - else: - print("ℹ️ 当前已是最新版本") - return True - - except Exception as e: - print(f"❌ 更新检查失败: {e}") - return False - -def test_updater_creation(): - """测试更新器创建""" - print("\n测试更新器创建...") - - try: - from app.tools.auto_updater import AutoUpdater - - updater = AutoUpdater("1.0.0") - print(f"✅ 更新器创建成功") - print(f" 应用目录: {updater.app_dir}") - print(f" 是否打包: {updater.is_frozen}") - return True - - except Exception as e: - print(f"❌ 更新器创建失败: {e}") - return False - -def test_dialog_import(): - """测试对话框导入""" - print("\n测试对话框导入...") - - try: - from app.tools.update_dialog import UpdateDialog, QuickUpdateChecker - print("✅ 更新对话框模块导入成功") - return True - - except Exception as e: - print(f"❌ 更新对话框导入失败: {e}") - return False - -def test_config_import(): - """测试配置导入""" - print("\n测试配置导入...") - - try: - from app.tools import update_config - print("✅ 更新配置模块导入成功") - print(f" 自动更新启用: {update_config.AUTO_UPDATE_ENABLED}") - print(f" GitHub仓库: {update_config.GITHUB_REPO}") - return True - - except Exception as e: - print(f"❌ 更新配置导入失败: {e}") - return False - -def run_all_tests(): - """运行所有测试""" - print("🚀 开始自动更新功能测试\n") - - tests = [ - ("配置导入", test_config_import), - ("更新器创建", test_updater_creation), - ("对话框导入", test_dialog_import), - ("更新检查", test_update_check), - ] - - results = [] - for name, test_func in tests: - try: - result = test_func() - results.append((name, result)) - except Exception as e: - print(f"❌ {name}测试出现异常: {e}") - results.append((name, False)) - - print("\n" + "="*50) - print("测试结果总结:") - print("="*50) - - passed = 0 - for name, result in results: - status = "✅ 通过" if result else "❌ 失败" - print(f"{name}: {status}") - if result: - passed += 1 - - print(f"\n总计: {passed}/{len(results)} 项测试通过") - - if passed == len(results): - print("🎉 所有测试通过!自动更新功能可以正常使用。") - else: - print("⚠️ 部分测试失败,请检查相关模块。") - - return passed == len(results) - -if __name__ == "__main__": - success = run_all_tests() - sys.exit(0 if success else 1) \ No newline at end of file diff --git a/test_fix_verification.py b/test_fix_verification.py deleted file mode 100644 index 852a0d2..0000000 --- a/test_fix_verification.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 -""" -测试修复后的更新功能 -""" - -import sys -import os - -# 添加项目路径 -sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) - -def test_dialog_creation(): - """测试对话框创建""" - print("测试对话框创建...") - - try: - from PyQt5.QtWidgets import QApplication - app = QApplication(sys.argv if len(sys.argv) > 1 else ['test']) - - from app.tools.update_dialog import UpdateDialog - - # 创建对话框(但不显示) - dialog = UpdateDialog("1.0.0") - print("✅ 对话框创建成功") - - # 测试基本方法 - dialog.check_for_updates() - print("✅ 检查更新功能正常") - - # 清理 - dialog.close() - app.quit() - - return True - - except Exception as e: - print(f"❌ 对话框测试失败: {e}") - return False - -def test_imports(): - """测试导入""" - print("测试模块导入...") - - try: - from app.tools.update_dialog import UpdateDialog, QuickUpdateChecker - from app.tools.auto_updater import AutoUpdater, check_update_availability - print("✅ 所有模块导入成功") - return True - except Exception as e: - print(f"❌ 导入失败: {e}") - return False - -def main(): - """主测试函数""" - print("🧪 测试修复后的更新功能\n") - - tests = [ - ("模块导入", test_imports), - ("对话框创建", test_dialog_creation), - ] - - results = [] - for name, test_func in tests: - print(f"🔍 {name}...") - try: - result = test_func() - results.append((name, result)) - except Exception as e: - print(f"❌ {name}异常: {e}") - results.append((name, False)) - print() - - print("="*40) - print("测试结果:") - passed = 0 - for name, result in results: - status = "✅ 通过" if result else "❌ 失败" - print(f"{name}: {status}") - if result: - passed += 1 - - print(f"\n总计: {passed}/{len(results)} 项测试通过") - - if passed == len(results): - print("🎉 修复成功!可以运行主程序了。") - else: - print("⚠️ 仍有问题需要解决。") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/test_fluent_design.py b/test_fluent_design.py deleted file mode 100644 index aefb122..0000000 --- a/test_fluent_design.py +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env python3 -""" -测试基于Fluent Design的现代化更新对话框 -完全遵循qfluentwidgets设计规范,支持明暗主题 -""" - -import sys -import os - -# 添加项目路径 -sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) - -from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout -from PyQt5.QtCore import Qt - -def test_fluent_dialog(): - """测试Fluent Design更新对话框""" - from qfluentwidgets import ( - setThemeColor, Theme, setTheme, - PrimaryPushButton, PushButton, BodyLabel, SubtitleLabel, - CardWidget, VBoxLayout, HBoxLayout, isDarkTheme - ) - from app.tools.fluent_design_update import FluentUpdateDialog - - app = QApplication(sys.argv) - - # 设置主题 - setThemeColor('#f18cb9') - setTheme(Theme.AUTO) - - # 创建测试窗口 - window = QWidget() - window.setWindowTitle("Fluent Design 更新对话框测试") - window.setFixedSize(500, 400) - - # 应用主题样式 - if isDarkTheme(): - window.setStyleSheet("background-color: #202020; color: white;") - else: - window.setStyleSheet("background-color: #FAFAFA; color: black;") - - layout = VBoxLayout(window) - layout.setContentsMargins(40, 40, 40, 40) - layout.setSpacing(24) - - # 创建测试卡片 - test_card = CardWidget() - test_card.setFixedHeight(280) - - card_layout = VBoxLayout(test_card) - card_layout.setContentsMargins(32, 24, 32, 24) - card_layout.setSpacing(20) - - # 标题 - title = SubtitleLabel("MRobot 现代化更新测试") - title.setAlignment(Qt.AlignmentFlag.AlignCenter) - card_layout.addWidget(title) - - # 说明 - info = BodyLabel("这是基于QFluentWidgets设计系统的现代化自动更新界面测试。\n\n" - "新界面特点:\n" - "• 完全遵循Fluent Design规范\n" - "• 自动适应明暗主题\n" - "• 流畅的动画效果\n" - "• 现代化的卡片设计\n" - "• 清晰的视觉层次") - info.setWordWrap(True) - card_layout.addWidget(info) - - # 主题切换提示 - theme_info = BodyLabel(f"当前主题: {'暗色模式' if isDarkTheme() else '亮色模式'}") - theme_info.setAlignment(Qt.AlignmentFlag.AlignCenter) - card_layout.addWidget(theme_info) - - layout.addWidget(test_card) - - # 按钮区域 - btn_layout = HBoxLayout() - - test_btn = PrimaryPushButton("测试更新对话框") - test_btn.clicked.connect(lambda: show_update_dialog(window)) - - theme_btn = PushButton("切换主题") - theme_btn.clicked.connect(lambda: toggle_theme_and_refresh(window, theme_info)) - - btn_layout.addWidget(theme_btn) - btn_layout.addStretch() - btn_layout.addWidget(test_btn) - - layout.addLayout(btn_layout) - - def show_update_dialog(parent): - """显示更新对话框""" - dialog = FluentUpdateDialog("1.0.0", parent) # 使用低版本触发更新 - dialog.exec_() - - def toggle_theme_and_refresh(window, label): - """切换主题并刷新""" - from qfluentwidgets import toggleTheme, isDarkTheme - toggleTheme() - - # 更新窗口样式 - if isDarkTheme(): - window.setStyleSheet("background-color: #202020; color: white;") - label.setText("当前主题: 暗色模式") - else: - window.setStyleSheet("background-color: #FAFAFA; color: black;") - label.setText("当前主题: 亮色模式") - - window.show() - sys.exit(app.exec_()) - -def test_components(): - """测试组件导入""" - tests = [ - ("Fluent Design更新对话框", lambda: __import__('app.tools.fluent_design_update')), - ("qfluentwidgets组件", test_qfluentwidgets_components), - ("自动更新器", lambda: __import__('app.tools.auto_updater')), - ] - - print("🎨 测试Fluent Design组件...") - print("-" * 50) - - for name, test_func in tests: - try: - test_func() - print(f"✅ {name}: 导入成功") - except Exception as e: - print(f"❌ {name}: 导入失败 - {e}") - - print("-" * 50) - print("测试完成!") - -def test_qfluentwidgets_components(): - """测试qfluentwidgets组件""" - from qfluentwidgets import ( - Dialog, CardWidget, SimpleCardWidget, ElevatedCardWidget, - PrimaryPushButton, PushButton, TransparentPushButton, - ProgressBar, ProgressRing, IndeterminateProgressBar, - SubtitleLabel, BodyLabel, CaptionLabel, StrongBodyLabel, DisplayLabel, - FluentIcon, InfoBar, ScrollArea, VBoxLayout, HBoxLayout, - setTheme, Theme, isDarkTheme, toggleTheme - ) - return True - -def test_theme_switching(): - """测试主题切换""" - from qfluentwidgets import setTheme, Theme, isDarkTheme, toggleTheme - - print("🌓 测试主题切换...") - - # 测试设置主题 - setTheme(Theme.LIGHT) - print(f"设置亮色主题 - 当前是否暗色: {isDarkTheme()}") - - setTheme(Theme.DARK) - print(f"设置暗色主题 - 当前是否暗色: {isDarkTheme()}") - - setTheme(Theme.AUTO) - print(f"设置自动主题 - 当前是否暗色: {isDarkTheme()}") - - print("主题切换测试完成!") - -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser(description="Fluent Design更新对话框测试") - parser.add_argument("--mode", choices=["dialog", "components", "theme"], - default="components", help="测试模式") - - args = parser.parse_args() - - print("🎨 Fluent Design 现代化更新对话框测试") - print("=" * 60) - - if args.mode == "dialog": - print("启动图形界面测试...") - test_fluent_dialog() - elif args.mode == "components": - test_components() - elif args.mode == "theme": - test_theme_switching() \ No newline at end of file diff --git a/test_fluent_update.py b/test_fluent_update.py deleted file mode 100644 index 05641f7..0000000 --- a/test_fluent_update.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 -""" -测试基于QFluentWidgets的自动更新对话框 -""" - -import sys -import os - -# 添加项目路径 -sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) - -from PyQt5.QtWidgets import QApplication -from PyQt5.QtCore import Qt - -def test_fluent_update_dialog(): - """测试Fluent风格更新对话框""" - from qfluentwidgets import setThemeColor, Theme, setTheme - from app.tools.fluent_update_dialog import FluentUpdateDialog - - app = QApplication(sys.argv) - - # 设置主题 - setThemeColor('#f18cb9') - setTheme(Theme.AUTO) - - # 创建对话框 - dialog = FluentUpdateDialog("1.0.0") # 使用较低版本来触发更新 - - # 显示对话框 - dialog.show() - - sys.exit(app.exec_()) - -def test_import(): - """测试导入""" - try: - from app.tools.fluent_update_dialog import FluentUpdateDialog, QuickUpdateNotification - print("✅ Fluent更新对话框导入成功") - return True - except Exception as e: - print(f"❌ 导入失败: {e}") - return False - -def test_components(): - """测试组件""" - try: - from qfluentwidgets import ( - CardWidget, PrimaryPushButton, ProgressBar, - SubtitleLabel, BodyLabel, InfoBar, FluentIcon - ) - print("✅ QFluentWidgets组件导入成功") - return True - except Exception as e: - print(f"❌ QFluentWidgets组件导入失败: {e}") - return False - -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser(description="测试Fluent风格更新对话框") - parser.add_argument("--mode", choices=["import", "components", "dialog"], - default="import", help="测试模式") - - args = parser.parse_args() - - if args.mode == "import": - test_import() - elif args.mode == "components": - test_components() - elif args.mode == "dialog": - test_fluent_update_dialog() \ No newline at end of file diff --git a/test_modern_update.py b/test_modern_update.py deleted file mode 100644 index 5fce270..0000000 --- a/test_modern_update.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python3 -""" -测试现代化QFluentWidgets自动更新对话框 -展示完整的Fluent Design风格界面 -""" - -import sys -import os - -# 添加项目路径 -sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) - -from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel -from PyQt5.QtCore import Qt - -def test_modern_dialog(): - """测试现代化更新对话框""" - from qfluentwidgets import setThemeColor, Theme, setTheme, PrimaryPushButton, BodyLabel - from app.tools.modern_update_dialog import ModernUpdateDialog - - app = QApplication(sys.argv) - - # 设置主题 - setThemeColor('#f18cb9') - setTheme(Theme.AUTO) - - # 创建测试窗口 - window = QWidget() - window.setWindowTitle("现代化更新对话框测试") - window.setFixedSize(400, 300) - - layout = QVBoxLayout(window) - layout.setSpacing(20) - layout.setContentsMargins(40, 40, 40, 40) - - # 标题 - title = BodyLabel("MRobot 现代化更新测试") - title.setAlignment(Qt.AlignmentFlag.AlignCenter) - title.setStyleSheet("font-size: 18px; font-weight: bold;") - layout.addWidget(title) - - # 说明 - info = BodyLabel("点击下方按钮测试基于QFluentWidgets的现代化更新界面") - info.setAlignment(Qt.AlignmentFlag.AlignCenter) - info.setWordWrap(True) - layout.addWidget(info) - - # 测试按钮 - test_btn = PrimaryPushButton("打开更新对话框") - test_btn.clicked.connect(lambda: show_update_dialog(window)) - layout.addWidget(test_btn) - - layout.addStretch() - - def show_update_dialog(parent): - dialog = ModernUpdateDialog("1.0.0", parent) # 使用低版本触发更新 - dialog.exec_() - - window.show() - sys.exit(app.exec_()) - -def test_components(): - """测试组件导入""" - tests = [ - ("现代化对话框", lambda: __import__('app.tools.modern_update_dialog')), - ("Fluent组件", lambda: __import__('app.tools.fluent_components')), - ("自动更新器", lambda: __import__('app.tools.auto_updater')), - ] - - print("🧪 测试组件导入...") - print("-" * 40) - - for name, test_func in tests: - try: - test_func() - print(f"✅ {name}: 导入成功") - except Exception as e: - print(f"❌ {name}: 导入失败 - {e}") - - print("-" * 40) - print("测试完成!") - -def test_qfluentwidgets(): - """测试QFluentWidgets组件""" - try: - from qfluentwidgets import ( - Dialog, CardWidget, PrimaryPushButton, ProgressBar, - SubtitleLabel, BodyLabel, InfoBar, FluentIcon, - ElevatedCardWidget, SimpleCardWidget, HeaderCardWidget, - TransparentToolButton, ProgressRing, PillPushButton - ) - print("✅ QFluentWidgets高级组件导入成功") - return True - except Exception as e: - print(f"❌ QFluentWidgets组件导入失败: {e}") - return False - -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser(description="现代化更新对话框测试") - parser.add_argument("--mode", choices=["dialog", "components", "qfw"], - default="components", help="测试模式") - - args = parser.parse_args() - - print("🚀 现代化自动更新对话框测试") - print("=" * 50) - - if args.mode == "dialog": - print("启动图形界面测试...") - test_modern_dialog() - elif args.mode == "components": - test_components() - elif args.mode == "qfw": - test_qfluentwidgets() \ No newline at end of file diff --git a/test_new_ui.py b/test_new_ui.py deleted file mode 100644 index c69fc51..0000000 --- a/test_new_ui.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 - -import sys -from PyQt5.QtWidgets import QApplication -from app.tools.fluent_design_update import FluentUpdateDialog - -def test_ui(): - app = QApplication(sys.argv) - - try: - dialog = FluentUpdateDialog("1.0.2") - print("FluentUpdateDialog created successfully") - dialog.show() - app.exec_() - except Exception as e: - print(f"Error: {e}") - import traceback - traceback.print_exc() - -if __name__ == "__main__": - test_ui() \ No newline at end of file diff --git a/test_simple_update.py b/test_simple_update.py deleted file mode 100644 index f562322..0000000 --- a/test_simple_update.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python3 -""" -测试简化的自动更新对话框 -确保稳定性和兼容性 -""" - -import sys -import os - -# 添加项目路径 -sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) - -def test_simple_dialog(): - """测试简化对话框""" - from PyQt5.QtWidgets import QApplication - from qfluentwidgets import setThemeColor, Theme, setTheme - from app.tools.simple_update_dialog import SimpleUpdateDialog - - app = QApplication(sys.argv) - - # 设置主题 - setThemeColor('#f18cb9') - setTheme(Theme.AUTO) - - # 创建对话框 - dialog = SimpleUpdateDialog("1.0.0") # 使用较低版本触发更新 - dialog.show() - - sys.exit(app.exec_()) - -def test_imports(): - """测试导入""" - try: - from app.tools.simple_update_dialog import SimpleUpdateDialog, QuickNotifier - print("✅ 简化更新对话框导入成功") - - from app.tools.auto_updater import AutoUpdater, check_update_availability - print("✅ 自动更新器导入成功") - - from qfluentwidgets import ( - CardWidget, PrimaryPushButton, ProgressBar, - SubtitleLabel, BodyLabel, InfoBar, FluentIcon - ) - print("✅ QFluentWidgets基础组件导入成功") - - return True - - except Exception as e: - print(f"❌ 导入失败: {e}") - return False - -def test_update_check(): - """测试更新检查""" - try: - from app.tools.auto_updater import check_update_availability - - result = check_update_availability("1.0.0") - if result: - print(f"✅ 检测到更新: v{result['version']}") - else: - print("ℹ️ 当前已是最新版本") - - return True - - except Exception as e: - print(f"❌ 更新检查失败: {e}") - return False - -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser(description="简化更新对话框测试") - parser.add_argument("--mode", choices=["dialog", "imports", "check"], - default="imports", help="测试模式") - - args = parser.parse_args() - - print("🧪 简化自动更新对话框测试") - print("=" * 40) - - if args.mode == "dialog": - print("启动对话框测试...") - test_simple_dialog() - elif args.mode == "imports": - test_imports() - elif args.mode == "check": - test_update_check() - - print("测试完成!") \ No newline at end of file diff --git a/test_stable_update.py b/test_stable_update.py deleted file mode 100644 index 08a2ca1..0000000 --- a/test_stable_update.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env python3 -""" -测试稳定的更新对话框 -""" - -import sys -import os - -# 添加项目路径 -sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) - -def test_stable_imports(): - """测试稳定组件导入""" - try: - from app.tools.simple_update_components import ( - SimpleUpdateStatusCard, SimpleVersionCard, SimpleActionButtons - ) - print("✅ 简化组件导入成功") - - from app.tools.stable_update_dialog import StableUpdateDialog, SimpleUpdateNotifier - print("✅ 稳定对话框导入成功") - - return True - except Exception as e: - print(f"❌ 导入失败: {e}") - return False - -def test_stable_dialog(): - """测试稳定对话框""" - from PyQt5.QtWidgets import QApplication - from qfluentwidgets import setThemeColor, Theme, setTheme - from app.tools.stable_update_dialog import StableUpdateDialog - - app = QApplication(sys.argv) - - # 设置主题 - setThemeColor('#f18cb9') - setTheme(Theme.AUTO) - - # 创建对话框 - dialog = StableUpdateDialog("1.0.0") # 使用较低版本来触发更新 - - # 显示对话框 - dialog.show() - - sys.exit(app.exec_()) - -def test_about_interface(): - """测试关于页面集成""" - try: - from app.about_interface import AboutInterface - print("✅ 关于页面导入成功") - return True - except Exception as e: - print(f"❌ 关于页面导入失败: {e}") - return False - -def run_all_tests(): - """运行所有测试""" - print("🧪 测试稳定的更新功能\n") - - tests = [ - ("稳定组件导入", test_stable_imports), - ("关于页面集成", test_about_interface), - ] - - passed = 0 - for name, test_func in tests: - try: - if test_func(): - print(f"✅ {name}: 通过") - passed += 1 - else: - print(f"❌ {name}: 失败") - except Exception as e: - print(f"❌ {name}: 异常 - {e}") - - print(f"\n📊 测试结果: {passed}/{len(tests)} 通过") - return passed == len(tests) - -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser(description="测试稳定更新对话框") - parser.add_argument("--mode", choices=["test", "dialog"], - default="test", help="运行模式") - - args = parser.parse_args() - - if args.mode == "test": - success = run_all_tests() - sys.exit(0 if success else 1) - elif args.mode == "dialog": - test_stable_dialog() \ No newline at end of file diff --git a/test_update_fix.py b/test_update_fix.py deleted file mode 100644 index 69d5fa1..0000000 --- a/test_update_fix.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 -""" -测试修复后的自动更新功能 -""" - -import sys -import os - -sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) - -def test_simple_dialog_import(): - """测试简化对话框导入""" - try: - from app.tools.update_dialog_simple import SimpleUpdateDialog, QuickUpdateChecker - print("✅ 简化对话框导入成功") - return True - except Exception as e: - print(f"❌ 简化对话框导入失败: {e}") - import traceback - traceback.print_exc() - return False - -def test_dialog_creation(): - """测试对话框创建(不显示)""" - try: - from PyQt5.QtWidgets import QApplication - from app.tools.update_dialog_simple import SimpleUpdateDialog - - # 创建应用程序实例(如果还没有的话) - if not QApplication.instance(): - app = QApplication([]) - - # 创建对话框但不显示 - dialog = SimpleUpdateDialog("1.0.0") - print("✅ 对话框创建成功") - - # 清理 - dialog.deleteLater() - - return True - - except Exception as e: - print(f"❌ 对话框创建失败: {e}") - import traceback - traceback.print_exc() - return False - -def test_about_interface_import(): - """测试关于界面导入""" - try: - from app.about_interface import AboutInterface - print("✅ 关于界面导入成功") - return True - except Exception as e: - print(f"❌ 关于界面导入失败: {e}") - import traceback - traceback.print_exc() - return False - -def run_tests(): - """运行所有测试""" - print("🔧 测试修复后的自动更新功能\n") - - tests = [ - ("简化对话框导入", test_simple_dialog_import), - ("对话框创建", test_dialog_creation), - ("关于界面导入", test_about_interface_import), - ] - - results = [] - for name, test_func in tests: - print(f"测试 {name}...") - try: - result = test_func() - results.append((name, result)) - except Exception as e: - print(f"❌ {name} 测试异常: {e}") - results.append((name, False)) - print() - - print("=" * 50) - print("测试结果:") - print("=" * 50) - - passed = 0 - for name, result in results: - status = "✅ 通过" if result else "❌ 失败" - print(f"{name}: {status}") - if result: - passed += 1 - - print(f"\n总计: {passed}/{len(results)} 项测试通过") - - if passed == len(results): - print("🎉 所有测试通过!修复成功,自动更新功能应该不会再闪退。") - else: - print("⚠️ 部分测试失败,可能还有问题需要修复。") - - return passed == len(results) - -if __name__ == "__main__": - success = run_tests() - - if success: - print("\n💡 使用提示:") - print("1. 现在可以在'关于'页面点击'自动更新'按钮") - print("2. 更新过程中会显示详细的进度条和状态") - print("3. 更新完成后程序会自动重启") - print("4. 如果更新失败会显示错误信息并允许重试") - - sys.exit(0 if success else 1) \ No newline at end of file diff --git a/test_version_check.py b/test_version_check.py deleted file mode 100644 index d056819..0000000 --- a/test_version_check.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python3 -""" -测试版本检查逻辑 -""" - -from packaging.version import parse as vparse - -def test_version_comparison(): - """测试版本比较""" - current_version = "1.0.2" - - # 测试不同的版本情况 - test_versions = ["1.0.1", "1.0.2", "1.0.3", "1.0.5", "1.1.0", "2.0.0"] - - print(f"当前版本: {current_version}") - print("-" * 40) - - for version in test_versions: - is_newer = vparse(version) > vparse(current_version) - status = "有更新" if is_newer else "无更新" - print(f"版本 {version}: {status}") - - -def simulate_check_update(local_version, remote_version): - """模拟更新检查""" - print(f"\n模拟检查: 本地版本 {local_version} vs 远程版本 {remote_version}") - - if vparse(remote_version) > vparse(local_version): - print("✓ 发现新版本") - return remote_version - else: - print("✗ 已是最新版本") - return None - - -if __name__ == "__main__": - test_version_comparison() - - # 模拟你遇到的情况 - print("\n" + "="*50) - print("模拟实际情况:") - simulate_check_update("1.0.2", "1.0.5") - simulate_check_update("1.0.2", "1.0.2") - simulate_check_update("1.0.2", "1.0.1") \ No newline at end of file diff --git a/自动更新功能说明.md b/自动更新功能说明.md deleted file mode 100644 index 5b25ced..0000000 --- a/自动更新功能说明.md +++ /dev/null @@ -1,224 +0,0 @@ -# 自动更新功能使用说明 - -## 功能概述 - -MRobot 现在支持完整的自动更新功能,包括: - -- ✅ 自动检查更新 -- ✅ 自动下载更新包 -- ✅ 自动解压安装 -- ✅ 自动重启程序 -- ✅ 更新失败回滚 - -## 使用方法 - -### 1. 启动时自动检查更新 - -程序启动 3 秒后会自动在后台检查更新。如果发现新版本,会显示通知提示。 - -### 2. 手动检查更新 - -在"关于"页面点击: -- **自动更新**: 打开完整的自动更新对话框 -- **手动检查**: 检查更新并在浏览器中打开下载页面 - -### 3. 自动更新流程 - -1. 点击"自动更新"按钮 -2. 系统自动检查是否有新版本 -3. 如有更新,显示版本信息和更新说明 -4. 点击"开始更新"确认更新 -5. 系统自动完成: - - 下载更新包 - - 备份当前文件 - - 解压安装新版本 - - 重启程序 - -## 技术实现 - -### 核心组件 - -1. **AutoUpdater** (`auto_updater.py`) - - 主要更新逻辑 - - 多线程下载和安装 - - 错误处理和回滚 - -2. **UpdateDialog** (`update_dialog.py`) - - 用户界面 - - 进度显示 - - 用户交互 - -3. **QuickUpdateChecker** - - 后台更新检查 - - 通知显示 - -### 更新检查逻辑 - -```python -# 检查 GitHub Releases API -url = f"https://api.github.com/repos/{repo}/releases/latest" -response = requests.get(url, timeout=10) -release_data = response.json() - -# 比较版本号 -from packaging.version import parse as vparse -if vparse(latest_version) > vparse(current_version): - # 有新版本可用 -``` - -### 下载逻辑 - -```python -# 根据操作系统选择合适的安装包 -system = platform.system().lower() -if system == 'windows': - # 查找 .exe 或 .zip 文件 -elif system == 'darwin': # macOS - # 查找 .dmg 或 .zip 文件 -elif system == 'linux': - # 查找 .tar.gz 或 .zip 文件 -``` - -### 安装逻辑 - -1. **备份当前文件** - ```python - # 备份重要文件到临时目录 - backup_files = ['MRobot.py', 'app/', 'assets/'] - ``` - -2. **解压新版本** - ```python - # 支持 .zip 和 .tar.gz 格式 - with zipfile.ZipFile(file_path, 'r') as zip_ref: - zip_ref.extractall(extract_dir) - ``` - -3. **复制文件** - ```python - # 替换应用程序文件 - shutil.copytree(src_path, dst_path) - ``` - -4. **重启程序** - ```python - # 启动新进程并退出当前进程 - subprocess.Popen([executable]) - sys.exit(0) - ``` - -## 配置选项 - -在 `update_config.py` 中可以配置: - -```python -# 基本设置 -AUTO_UPDATE_ENABLED = True # 启用自动更新 -AUTO_UPDATE_ON_STARTUP = True # 启动时检查 -UPDATE_CHECK_INTERVAL = 3000 # 检查延迟(毫秒) - -# 下载设置 -DOWNLOAD_TIMEOUT = 300 # 下载超时 -CHUNK_SIZE = 8192 # 下载块大小 - -# 备份设置 -CREATE_BACKUP = True # 创建备份 -BACKUP_IMPORTANT_FILES = [...] # 备份文件列表 -``` - -## 安全考虑 - -1. **数字签名验证** (未实现,可扩展) - - 验证下载文件的数字签名 - - 确保文件来源可信 - -2. **备份和回滚** - - 更新前自动备份 - - 更新失败自动回滚 - -3. **用户确认** - - 所有更新操作需要用户确认 - - 不会自动静默更新 - -## 故障排除 - -### 常见问题 - -1. **网络连接问题** - - 检查网络连接 - - 检查防火墙设置 - - 尝试手动更新 - -2. **权限问题** - - 确保程序有写入权限 - - 以管理员身份运行(Windows) - -3. **文件被占用** - - 关闭其他实例 - - 重启计算机后再试 - -### 错误日志 - -更新过程中的错误会显示在界面上,并可在控制台查看详细信息。 - -### 手动回滚 - -如果自动回滚失败,可以: -1. 从临时目录恢复备份文件 -2. 重新下载完整安装包 -3. 联系技术支持 - -## 开发者信息 - -### 扩展功能 - -可以在现有基础上添加: - -1. **增量更新** - - 只下载变更的文件 - - 减少下载时间和流量 - -2. **多源下载** - - 支持多个下载镜像 - - 提高下载成功率 - -3. **版本回退** - - 支持回退到指定版本 - - 版本历史管理 - -4. **自定义更新源** - - 支持企业内部更新服务器 - - 自定义更新策略 - -### API 接口 - -```python -# 检查更新 -update_info = check_update_availability(current_version) - -# 启动自动更新 -dialog = UpdateDialog(current_version, parent) -dialog.exec_() - -# 后台检查 -QuickUpdateChecker.check_and_notify(version, parent) -``` - -## 注意事项 - -1. 确保 GitHub Releases 中有对应平台的安装包 -2. 安装包命名要规范,便于识别平台 -3. 更新说明要写在 Release Notes 中 -4. 版本号要遵循语义化版本规范 (如: v1.0.5) - -## 版本兼容性 - -- PyQt5 5.x+ -- Python 3.7+ -- requests 2.x+ -- packaging 20.x+ - -支持的操作系统: -- Windows 7+ -- macOS 10.12+ -- Ubuntu 18.04+ \ No newline at end of file