好
This commit is contained in:
parent
697104d1ce
commit
e5d5afb1a8
@ -1,65 +1,350 @@
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QMessageBox
|
||||
from PyQt5.QtCore import Qt, QUrl
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout
|
||||
from PyQt5.QtCore import Qt, QUrl, QTimer
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
|
||||
from qfluentwidgets import PrimaryPushSettingCard, FluentIcon
|
||||
from qfluentwidgets import InfoBar, InfoBarPosition, SubtitleLabel
|
||||
from qfluentwidgets import (
|
||||
PrimaryPushSettingCard, FluentIcon, InfoBar, InfoBarPosition,
|
||||
SubtitleLabel, BodyLabel, CaptionLabel, StrongBodyLabel,
|
||||
ElevatedCardWidget, PrimaryPushButton, PushButton,
|
||||
ProgressBar, TextEdit
|
||||
)
|
||||
|
||||
from .function_fit_interface import FunctionFitInterface
|
||||
from app.tools.check_update import check_update
|
||||
from app.tools.auto_updater import AutoUpdater, check_update_availability
|
||||
|
||||
__version__ = "1.0.5"
|
||||
__version__ = "1.0.2"
|
||||
|
||||
class AboutInterface(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("aboutInterface")
|
||||
|
||||
|
||||
# 初始化更新相关变量
|
||||
self.updater = None
|
||||
self.update_info = None
|
||||
|
||||
self._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""设置用户界面"""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setAlignment(Qt.AlignTop)
|
||||
layout.setContentsMargins(20, 30, 20, 20) # 添加边距
|
||||
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
layout.setContentsMargins(20, 30, 20, 20)
|
||||
|
||||
# 页面标题
|
||||
title = SubtitleLabel("MRobot 帮助页面", self)
|
||||
title.setAlignment(Qt.AlignCenter)
|
||||
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(title)
|
||||
# 添加空间隔
|
||||
layout.addSpacing(10)
|
||||
|
||||
card = PrimaryPushSettingCard(
|
||||
text="检查更新",
|
||||
icon=FluentIcon.DOWNLOAD,
|
||||
title="更新",
|
||||
content=f"MRobot_Toolbox 当前版本:{__version__}",
|
||||
)
|
||||
card.clicked.connect(self.on_check_update_clicked)
|
||||
layout.addWidget(card)
|
||||
# 版本信息卡片 - 学习AI界面风格
|
||||
version_card = ElevatedCardWidget()
|
||||
version_layout = QVBoxLayout(version_card)
|
||||
version_layout.setContentsMargins(24, 20, 24, 20)
|
||||
|
||||
version_title = StrongBodyLabel("版本信息")
|
||||
version_layout.addWidget(version_title)
|
||||
|
||||
current_version_label = BodyLabel(f"当前版本:v{__version__}")
|
||||
version_layout.addWidget(current_version_label)
|
||||
|
||||
def on_check_update_clicked(self):
|
||||
|
||||
layout.addWidget(version_card)
|
||||
|
||||
# 检查更新按钮
|
||||
self.check_update_card = PrimaryPushSettingCard(
|
||||
text="检查更新",
|
||||
icon=FluentIcon.SYNC,
|
||||
title="检查更新",
|
||||
content="检查是否有新版本可用",
|
||||
)
|
||||
self.check_update_card.clicked.connect(self.check_for_updates)
|
||||
layout.addWidget(self.check_update_card)
|
||||
|
||||
# 更新信息卡片(初始隐藏)
|
||||
self.update_info_card = ElevatedCardWidget()
|
||||
self.update_info_card.hide()
|
||||
self._setup_update_info_card()
|
||||
layout.addWidget(self.update_info_card)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
def _setup_update_info_card(self):
|
||||
"""设置更新信息卡片"""
|
||||
layout = QVBoxLayout(self.update_info_card)
|
||||
layout.setContentsMargins(24, 20, 24, 20)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# 标题
|
||||
self.update_title = StrongBodyLabel("发现新版本")
|
||||
layout.addWidget(self.update_title)
|
||||
|
||||
# 版本对比
|
||||
version_layout = QHBoxLayout()
|
||||
|
||||
current_layout = QVBoxLayout()
|
||||
current_layout.addWidget(CaptionLabel("当前版本"))
|
||||
self.current_version_label = SubtitleLabel(f"v{__version__}")
|
||||
current_layout.addWidget(self.current_version_label)
|
||||
|
||||
arrow_label = SubtitleLabel("→")
|
||||
arrow_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
arrow_label.setFixedWidth(30)
|
||||
|
||||
latest_layout = QVBoxLayout()
|
||||
latest_layout.addWidget(CaptionLabel("最新版本"))
|
||||
self.latest_version_label = SubtitleLabel("v--")
|
||||
latest_layout.addWidget(self.latest_version_label)
|
||||
|
||||
version_layout.addLayout(current_layout)
|
||||
version_layout.addWidget(arrow_label)
|
||||
version_layout.addLayout(latest_layout)
|
||||
|
||||
layout.addLayout(version_layout)
|
||||
|
||||
# 更新信息
|
||||
info_layout = QHBoxLayout()
|
||||
self.file_size_label = BodyLabel("文件大小: --")
|
||||
self.release_date_label = BodyLabel("发布时间: --")
|
||||
|
||||
info_layout.addWidget(self.file_size_label)
|
||||
info_layout.addStretch()
|
||||
info_layout.addWidget(self.release_date_label)
|
||||
|
||||
layout.addLayout(info_layout)
|
||||
|
||||
# 更新说明
|
||||
layout.addWidget(CaptionLabel("更新说明:"))
|
||||
|
||||
self.notes_display = TextEdit()
|
||||
self.notes_display.setReadOnly(True)
|
||||
self.notes_display.setFixedHeight(200)
|
||||
self.notes_display.setText("暂无更新说明")
|
||||
layout.addWidget(self.notes_display)
|
||||
|
||||
# 进度条(初始隐藏)
|
||||
self.progress_widget = QWidget()
|
||||
progress_layout = QVBoxLayout(self.progress_widget)
|
||||
|
||||
self.progress_label = BodyLabel("准备更新...")
|
||||
progress_layout.addWidget(self.progress_label)
|
||||
|
||||
self.progress_bar = ProgressBar()
|
||||
self.progress_bar.setRange(0, 100)
|
||||
self.progress_bar.setValue(0)
|
||||
progress_layout.addWidget(self.progress_bar)
|
||||
|
||||
self.progress_widget.hide()
|
||||
layout.addWidget(self.progress_widget)
|
||||
|
||||
# 按钮区域
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
self.manual_btn = PushButton("手动下载")
|
||||
self.manual_btn.setIcon(FluentIcon.LINK)
|
||||
self.manual_btn.clicked.connect(self.open_manual_download)
|
||||
|
||||
self.update_btn = PrimaryPushButton("开始更新")
|
||||
self.update_btn.setIcon(FluentIcon.DOWNLOAD)
|
||||
self.update_btn.clicked.connect(self.start_update)
|
||||
|
||||
self.cancel_btn = PushButton("取消")
|
||||
self.cancel_btn.clicked.connect(self.cancel_update)
|
||||
|
||||
button_layout.addWidget(self.manual_btn)
|
||||
button_layout.addStretch()
|
||||
button_layout.addWidget(self.update_btn)
|
||||
button_layout.addWidget(self.cancel_btn)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
def check_for_updates(self):
|
||||
"""检查更新"""
|
||||
self.check_update_card.setEnabled(False)
|
||||
self.check_update_card.setContent("正在检查更新...")
|
||||
|
||||
# 延迟执行检查,避免阻塞UI
|
||||
QTimer.singleShot(100, self._perform_check)
|
||||
|
||||
def _perform_check(self):
|
||||
"""执行更新检查"""
|
||||
try:
|
||||
latest = check_update(__version__)
|
||||
if latest:
|
||||
# 直接用浏览器打开下载链接
|
||||
QDesktopServices.openUrl(QUrl("https://github.com/goldenfishs/MRobot/releases/latest"))
|
||||
InfoBar.success(
|
||||
title="发现新版本",
|
||||
content=f"检测到新版本:{latest},已为你打开下载页面。",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=5000
|
||||
)
|
||||
elif latest is None:
|
||||
InfoBar.info(
|
||||
title="已是最新版本",
|
||||
content="当前已是最新版本,无需更新。",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=3000
|
||||
)
|
||||
except Exception:
|
||||
InfoBar.error(
|
||||
title="检查更新失败",
|
||||
content="无法获取最新版本,请检查网络连接。",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=4000
|
||||
)
|
||||
self.update_info = check_update_availability(__version__)
|
||||
|
||||
if self.update_info:
|
||||
self._show_update_available()
|
||||
else:
|
||||
self._show_no_update()
|
||||
|
||||
except Exception as e:
|
||||
self._show_error(f"检查更新失败: {str(e)}")
|
||||
|
||||
def _show_update_available(self):
|
||||
"""显示发现更新"""
|
||||
# 更新按钮状态
|
||||
self.check_update_card.setEnabled(True)
|
||||
self.check_update_card.setContent("发现新版本!")
|
||||
|
||||
# 显示更新信息卡片
|
||||
self.update_info_card.show()
|
||||
|
||||
# 设置版本信息
|
||||
if self.update_info:
|
||||
version = self.update_info.get('version', 'Unknown')
|
||||
self.latest_version_label.setText(f"v{version}")
|
||||
|
||||
# 设置文件信息
|
||||
asset_size = self.update_info.get('asset_size', 0)
|
||||
file_size = self._format_file_size(asset_size)
|
||||
self.file_size_label.setText(f"文件大小: {file_size}")
|
||||
|
||||
# 设置发布时间
|
||||
release_date = self.update_info.get('release_date', '')
|
||||
formatted_date = self._format_date(release_date)
|
||||
self.release_date_label.setText(f"发布时间: {formatted_date}")
|
||||
|
||||
# 设置更新说明
|
||||
notes = self.update_info.get('release_notes', '暂无更新说明')
|
||||
self.notes_display.setText(notes[:500] + ('...' if len(notes) > 500 else ''))
|
||||
|
||||
InfoBar.success(
|
||||
title="发现新版本",
|
||||
content=f"检测到新版本 v{version}",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=3000
|
||||
)
|
||||
|
||||
def _show_no_update(self):
|
||||
"""显示无更新"""
|
||||
self.check_update_card.setEnabled(True)
|
||||
self.check_update_card.setContent("已是最新版本")
|
||||
|
||||
InfoBar.info(
|
||||
title="已是最新版本",
|
||||
content="当前已是最新版本,无需更新。",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=3000
|
||||
)
|
||||
|
||||
def _show_error(self, error_msg: str):
|
||||
"""显示错误"""
|
||||
self.check_update_card.setEnabled(True)
|
||||
self.check_update_card.setContent("检查更新失败")
|
||||
|
||||
InfoBar.error(
|
||||
title="检查更新失败",
|
||||
content=error_msg,
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=4000
|
||||
)
|
||||
|
||||
def start_update(self):
|
||||
"""开始更新"""
|
||||
if not self.update_info:
|
||||
return
|
||||
|
||||
# 显示进度UI
|
||||
self.progress_widget.show()
|
||||
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.status_changed.connect(self.update_status)
|
||||
self.updater.signals.error_occurred.connect(self.update_error)
|
||||
self.updater.signals.update_completed.connect(self.update_completed)
|
||||
|
||||
# 开始更新流程
|
||||
self.updater.start()
|
||||
|
||||
def update_progress(self, value: int):
|
||||
"""更新进度"""
|
||||
self.progress_bar.setValue(value)
|
||||
|
||||
def update_status(self, status: str):
|
||||
"""更新状态"""
|
||||
self.progress_label.setText(status)
|
||||
|
||||
def update_error(self, error_msg: str):
|
||||
"""更新错误"""
|
||||
self.progress_widget.hide()
|
||||
self.update_btn.setEnabled(True)
|
||||
self.manual_btn.setEnabled(True)
|
||||
|
||||
InfoBar.error(
|
||||
title="更新失败",
|
||||
content=error_msg,
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=4000
|
||||
)
|
||||
|
||||
def update_completed(self):
|
||||
"""更新完成"""
|
||||
self.progress_label.setText("更新完成,准备重启...")
|
||||
self.progress_bar.setValue(100)
|
||||
|
||||
InfoBar.success(
|
||||
title="更新完成",
|
||||
content="更新安装完成,程序将在3秒后重启",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=3000
|
||||
)
|
||||
|
||||
# 延迟重启
|
||||
QTimer.singleShot(3000, self._restart_app)
|
||||
|
||||
def cancel_update(self):
|
||||
"""取消更新"""
|
||||
if self.updater and self.updater.isRunning():
|
||||
self.updater.cancel_update()
|
||||
|
||||
self.update_info_card.hide()
|
||||
self.check_update_card.setContent("检查是否有新版本可用")
|
||||
|
||||
def open_manual_download(self):
|
||||
"""打开手动下载页面"""
|
||||
QDesktopServices.openUrl(QUrl("https://github.com/goldenfishs/MRobot/releases/latest"))
|
||||
|
||||
InfoBar.info(
|
||||
title="手动下载",
|
||||
content="已为您打开下载页面",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=2000
|
||||
)
|
||||
|
||||
def _restart_app(self):
|
||||
"""重启应用程序"""
|
||||
if self.updater:
|
||||
self.updater.restart_application()
|
||||
|
||||
def _format_file_size(self, size_bytes: int) -> str:
|
||||
"""格式化文件大小"""
|
||||
if size_bytes == 0:
|
||||
return "--"
|
||||
|
||||
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 _format_date(self, date_str: str) -> str:
|
||||
"""格式化日期"""
|
||||
if not date_str:
|
||||
return "--"
|
||||
|
||||
try:
|
||||
from datetime import datetime
|
||||
date_obj = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
||||
return date_obj.strftime('%Y-%m-%d')
|
||||
except:
|
||||
return date_str[:10] if len(date_str) >= 10 else date_str
|
||||
@ -25,9 +25,8 @@ class MainWindow(FluentWindow):
|
||||
self.initInterface()
|
||||
self.initNavigation()
|
||||
|
||||
# 检查更新
|
||||
# checkUpdate(self, flag=True)
|
||||
# checkAnnouncement(self) # 检查公告
|
||||
# 后台检查更新(不弹窗,只显示通知)
|
||||
# self.check_updates_in_background()
|
||||
|
||||
def initWindow(self):
|
||||
self.setMicaEffectEnabled(False)
|
||||
@ -74,6 +73,14 @@ class MainWindow(FluentWindow):
|
||||
None,
|
||||
NavigationItemPosition.BOTTOM
|
||||
)
|
||||
|
||||
def check_updates_in_background(self):
|
||||
"""后台检查更新"""
|
||||
try:
|
||||
# 后台更新检查已移至关于页面手动触发
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"初始化完成: {e}")
|
||||
|
||||
# main_window.py 只需修改关闭事件
|
||||
def closeEvent(self, e):
|
||||
|
||||
340
app/tools/auto_updater.py
Normal file
340
app/tools/auto_updater.py
Normal file
@ -0,0 +1,340 @@
|
||||
"""
|
||||
自动更新模块
|
||||
实现软件的自动更新功能,包括下载、解压、安装等完整流程
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import tempfile
|
||||
import zipfile
|
||||
import subprocess
|
||||
import platform
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable
|
||||
from urllib.parse import urlparse
|
||||
|
||||
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)
|
||||
status_changed = pyqtSignal(str) # 状态变化信号
|
||||
error_occurred = pyqtSignal(str) # 错误信号
|
||||
update_completed = pyqtSignal() # 更新完成信号
|
||||
update_cancelled = pyqtSignal() # 更新取消信号
|
||||
|
||||
|
||||
class AutoUpdater(QThread):
|
||||
"""自动更新器类"""
|
||||
|
||||
def __init__(self, current_version: str, repo: str = "goldenfishs/MRobot"):
|
||||
super().__init__()
|
||||
self.current_version = current_version
|
||||
self.repo = repo
|
||||
self.signals = UpdaterSignals()
|
||||
self.cancelled = False
|
||||
|
||||
# 获取当前程序信息
|
||||
self.is_frozen = getattr(sys, 'frozen', False)
|
||||
self.app_dir = self._get_app_directory()
|
||||
self.temp_dir = None
|
||||
|
||||
def _get_app_directory(self) -> str:
|
||||
"""获取应用程序目录"""
|
||||
if self.is_frozen:
|
||||
# 如果是打包的exe,返回exe所在目录
|
||||
return os.path.dirname(sys.executable)
|
||||
else:
|
||||
# 如果是Python脚本,返回项目根目录
|
||||
return os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
def cancel_update(self):
|
||||
"""取消更新"""
|
||||
self.cancelled = True
|
||||
self.signals.update_cancelled.emit()
|
||||
|
||||
def check_for_updates(self) -> Optional[dict]:
|
||||
"""检查是否有新版本可用"""
|
||||
try:
|
||||
self.signals.status_changed.emit("正在检查更新...")
|
||||
|
||||
url = f"https://api.github.com/repos/{self.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(self.current_version):
|
||||
return {
|
||||
'version': latest_version,
|
||||
'download_url': self._get_download_url(release_data),
|
||||
'release_notes': release_data.get('body', ''),
|
||||
'release_date': release_data.get('published_at', ''),
|
||||
'asset_name': self._get_asset_name(release_data)
|
||||
}
|
||||
return None
|
||||
else:
|
||||
raise Exception(f"GitHub API请求失败: {response.status_code}")
|
||||
|
||||
except Exception as e:
|
||||
self.signals.error_occurred.emit(f"检查更新失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def _get_download_url(self, release_data: dict) -> Optional[str]:
|
||||
"""从release数据中获取适合当前平台的下载链接"""
|
||||
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']
|
||||
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'):
|
||||
return asset['browser_download_url']
|
||||
elif system == 'linux':
|
||||
if name.endswith('.tar.gz') or name.endswith('.zip'):
|
||||
return asset['browser_download_url']
|
||||
|
||||
# 如果没找到特定平台的,返回第一个可用文件
|
||||
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']
|
||||
|
||||
return None
|
||||
|
||||
def _get_asset_name(self, release_data: dict) -> Optional[str]:
|
||||
"""获取资源文件名"""
|
||||
download_url = self._get_download_url(release_data)
|
||||
if download_url:
|
||||
return os.path.basename(urlparse(download_url).path)
|
||||
return None
|
||||
|
||||
def download_update(self, download_url: str, filename: str) -> Optional[str]:
|
||||
"""下载更新文件"""
|
||||
try:
|
||||
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)
|
||||
response.raise_for_status()
|
||||
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
downloaded_size = 0
|
||||
|
||||
with open(file_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
if self.cancelled:
|
||||
return None
|
||||
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
|
||||
if total_size > 0:
|
||||
progress = int((downloaded_size / total_size) * 50) # 下载占50%进度
|
||||
self.signals.progress_changed.emit(progress)
|
||||
|
||||
self.signals.status_changed.emit("下载完成")
|
||||
return file_path
|
||||
|
||||
except Exception as e:
|
||||
self.signals.error_occurred.emit(f"下载失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def extract_update(self, file_path: str) -> Optional[str]:
|
||||
"""解压更新文件"""
|
||||
try:
|
||||
self.signals.status_changed.emit("正在解压文件...")
|
||||
self.signals.progress_changed.emit(50)
|
||||
|
||||
if not self.temp_dir:
|
||||
raise Exception("临时目录未初始化")
|
||||
|
||||
extract_dir = os.path.join(self.temp_dir, "extracted")
|
||||
os.makedirs(extract_dir, exist_ok=True)
|
||||
|
||||
# 根据文件扩展名选择解压方法
|
||||
if file_path.endswith('.zip'):
|
||||
with zipfile.ZipFile(file_path, 'r') as zip_ref:
|
||||
zip_ref.extractall(extract_dir)
|
||||
elif file_path.endswith('.tar.gz'):
|
||||
import tarfile
|
||||
with tarfile.open(file_path, 'r:gz') as tar_ref:
|
||||
tar_ref.extractall(extract_dir)
|
||||
else:
|
||||
raise Exception(f"不支持的文件格式: {file_path}")
|
||||
|
||||
self.signals.progress_changed.emit(70)
|
||||
self.signals.status_changed.emit("解压完成")
|
||||
return extract_dir
|
||||
|
||||
except Exception as e:
|
||||
self.signals.error_occurred.emit(f"解压失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def install_update(self, extract_dir: str) -> bool:
|
||||
"""安装更新"""
|
||||
try:
|
||||
self.signals.status_changed.emit("正在安装更新...")
|
||||
self.signals.progress_changed.emit(80)
|
||||
|
||||
if not self.temp_dir:
|
||||
raise Exception("临时目录未初始化")
|
||||
|
||||
# 创建备份目录
|
||||
backup_dir = os.path.join(self.temp_dir, "backup")
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
|
||||
# 备份当前程序文件
|
||||
self._backup_current_files(backup_dir)
|
||||
|
||||
# 复制新文件
|
||||
self._copy_update_files(extract_dir)
|
||||
|
||||
self.signals.progress_changed.emit(95)
|
||||
self.signals.status_changed.emit("安装完成")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.signals.error_occurred.emit(f"安装失败: {str(e)}")
|
||||
# 尝试恢复备份
|
||||
self._restore_backup(backup_dir)
|
||||
return False
|
||||
|
||||
def _backup_current_files(self, backup_dir: str):
|
||||
"""备份当前程序文件"""
|
||||
important_files = ['MRobot.py', 'MRobot.exe', 'app/', 'assets/']
|
||||
|
||||
for item in important_files:
|
||||
src_path = os.path.join(self.app_dir, item)
|
||||
if os.path.exists(src_path):
|
||||
dst_path = os.path.join(backup_dir, item)
|
||||
if os.path.isdir(src_path):
|
||||
shutil.copytree(src_path, dst_path, dirs_exist_ok=True)
|
||||
else:
|
||||
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
|
||||
shutil.copy2(src_path, dst_path)
|
||||
|
||||
def _copy_update_files(self, extract_dir: str):
|
||||
"""复制更新文件到应用程序目录"""
|
||||
# 查找解压目录中的主要文件/文件夹
|
||||
extract_contents = os.listdir(extract_dir)
|
||||
|
||||
# 如果解压后只有一个文件夹,进入该文件夹
|
||||
if len(extract_contents) == 1 and os.path.isdir(os.path.join(extract_dir, extract_contents[0])):
|
||||
extract_dir = os.path.join(extract_dir, extract_contents[0])
|
||||
|
||||
# 复制文件到应用程序目录
|
||||
for item in os.listdir(extract_dir):
|
||||
src_path = os.path.join(extract_dir, item)
|
||||
dst_path = os.path.join(self.app_dir, item)
|
||||
|
||||
if os.path.isdir(src_path):
|
||||
if os.path.exists(dst_path):
|
||||
shutil.rmtree(dst_path)
|
||||
shutil.copytree(src_path, dst_path)
|
||||
else:
|
||||
shutil.copy2(src_path, dst_path)
|
||||
|
||||
def _restore_backup(self, backup_dir: str):
|
||||
"""恢复备份文件"""
|
||||
try:
|
||||
for item in os.listdir(backup_dir):
|
||||
src_path = os.path.join(backup_dir, item)
|
||||
dst_path = os.path.join(self.app_dir, item)
|
||||
|
||||
if os.path.isdir(src_path):
|
||||
if os.path.exists(dst_path):
|
||||
shutil.rmtree(dst_path)
|
||||
shutil.copytree(src_path, dst_path)
|
||||
else:
|
||||
shutil.copy2(src_path, dst_path)
|
||||
except Exception as e:
|
||||
print(f"恢复备份失败: {e}")
|
||||
|
||||
def restart_application(self):
|
||||
"""重启应用程序"""
|
||||
try:
|
||||
self.signals.status_changed.emit("正在重启应用程序...")
|
||||
|
||||
if self.is_frozen:
|
||||
# 如果是打包的exe
|
||||
executable = sys.executable
|
||||
else:
|
||||
# 如果是Python脚本
|
||||
executable = sys.executable
|
||||
script_path = os.path.join(self.app_dir, "MRobot.py")
|
||||
|
||||
# 启动新进程
|
||||
if platform.system() == 'Windows':
|
||||
subprocess.Popen([executable] + ([script_path] if not self.is_frozen else []))
|
||||
else:
|
||||
subprocess.Popen([executable] + ([script_path] if not self.is_frozen else []))
|
||||
|
||||
# 退出当前进程
|
||||
sys.exit(0)
|
||||
|
||||
except Exception as e:
|
||||
self.signals.error_occurred.emit(f"重启失败: {str(e)}")
|
||||
|
||||
def cleanup(self):
|
||||
"""清理临时文件"""
|
||||
if self.temp_dir and os.path.exists(self.temp_dir):
|
||||
try:
|
||||
shutil.rmtree(self.temp_dir)
|
||||
except Exception as e:
|
||||
print(f"清理临时文件失败: {e}")
|
||||
|
||||
def run(self):
|
||||
"""执行更新流程"""
|
||||
try:
|
||||
# 检查更新
|
||||
update_info = self.check_for_updates()
|
||||
if not update_info or self.cancelled:
|
||||
return
|
||||
|
||||
# 下载更新
|
||||
downloaded_file = self.download_update(
|
||||
update_info['download_url'],
|
||||
update_info['asset_name']
|
||||
)
|
||||
if not downloaded_file or self.cancelled:
|
||||
return
|
||||
|
||||
# 解压更新
|
||||
extract_dir = self.extract_update(downloaded_file)
|
||||
if not extract_dir or self.cancelled:
|
||||
return
|
||||
|
||||
# 安装更新
|
||||
if self.install_update(extract_dir) and not self.cancelled:
|
||||
self.signals.progress_changed.emit(100)
|
||||
self.signals.update_completed.emit()
|
||||
|
||||
except Exception as e:
|
||||
self.signals.error_occurred.emit(f"更新过程中发生错误: {str(e)}")
|
||||
finally:
|
||||
# 清理临时文件
|
||||
self.cleanup()
|
||||
|
||||
|
||||
def check_update_availability(current_version: str, repo: str = "goldenfishs/MRobot") -> Optional[dict]:
|
||||
"""快速检查是否有新版本可用(不下载)"""
|
||||
updater = AutoUpdater(current_version, repo)
|
||||
return updater.check_for_updates()
|
||||
48
check_releases.py
Normal file
48
check_releases.py
Normal file
@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
检查GitHub Releases API响应结构
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
|
||||
def check_releases_structure():
|
||||
"""检查GitHub releases的API响应结构"""
|
||||
try:
|
||||
url = "https://api.github.com/repos/goldenfishs/MRobot/releases/latest"
|
||||
response = requests.get(url, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
|
||||
print("Release信息:")
|
||||
print(f"标签: {data.get('tag_name')}")
|
||||
print(f"名称: {data.get('name')}")
|
||||
print(f"发布时间: {data.get('published_at')}")
|
||||
print(f"是否为预发布: {data.get('prerelease')}")
|
||||
print(f"是否为草稿: {data.get('draft')}")
|
||||
|
||||
print("\n可用的资源文件:")
|
||||
assets = data.get('assets', [])
|
||||
|
||||
if not assets:
|
||||
print("❌ 没有找到任何资源文件")
|
||||
print("建议在GitHub Release中上传安装包文件")
|
||||
else:
|
||||
for i, asset in enumerate(assets):
|
||||
print(f" {i+1}. {asset['name']}")
|
||||
print(f" 大小: {asset['size']} 字节")
|
||||
print(f" 下载链接: {asset['browser_download_url']}")
|
||||
print(f" 内容类型: {asset.get('content_type', 'unknown')}")
|
||||
print()
|
||||
|
||||
print(f"\n更新说明:\n{data.get('body', '无')}")
|
||||
|
||||
else:
|
||||
print(f"❌ API请求失败,状态码: {response.status_code}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 检查失败: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_releases_structure()
|
||||
114
demo_auto_update.py
Normal file
114
demo_auto_update.py
Normal file
@ -0,0 +1,114 @@
|
||||
#!/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演示完成!")
|
||||
123
test_auto_update.py
Normal file
123
test_auto_update.py
Normal file
@ -0,0 +1,123 @@
|
||||
#!/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)
|
||||
90
test_fix_verification.py
Normal file
90
test_fix_verification.py
Normal file
@ -0,0 +1,90 @@
|
||||
#!/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()
|
||||
182
test_fluent_design.py
Normal file
182
test_fluent_design.py
Normal file
@ -0,0 +1,182 @@
|
||||
#!/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()
|
||||
71
test_fluent_update.py
Normal file
71
test_fluent_update.py
Normal file
@ -0,0 +1,71 @@
|
||||
#!/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()
|
||||
116
test_modern_update.py
Normal file
116
test_modern_update.py
Normal file
@ -0,0 +1,116 @@
|
||||
#!/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()
|
||||
21
test_new_ui.py
Normal file
21
test_new_ui.py
Normal file
@ -0,0 +1,21 @@
|
||||
#!/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()
|
||||
89
test_simple_update.py
Normal file
89
test_simple_update.py
Normal file
@ -0,0 +1,89 @@
|
||||
#!/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("测试完成!")
|
||||
94
test_stable_update.py
Normal file
94
test_stable_update.py
Normal file
@ -0,0 +1,94 @@
|
||||
#!/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()
|
||||
111
test_update_fix.py
Normal file
111
test_update_fix.py
Normal file
@ -0,0 +1,111 @@
|
||||
#!/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)
|
||||
44
test_version_check.py
Normal file
44
test_version_check.py
Normal file
@ -0,0 +1,44 @@
|
||||
#!/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")
|
||||
224
自动更新功能说明.md
Normal file
224
自动更新功能说明.md
Normal file
@ -0,0 +1,224 @@
|
||||
# 自动更新功能使用说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
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+
|
||||
Loading…
Reference in New Issue
Block a user