2026-08-10 14:31:44 +08:00
|
|
|
|
"""设置页在线更新配置、事件和 Qt 后台 Worker。"""
|
2026-08-10 12:32:39 +08:00
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-08-10 14:31:44 +08:00
|
|
|
|
import sqlite3
|
2026-08-10 12:32:39 +08:00
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
2026-08-10 14:31:44 +08:00
|
|
|
|
from PyQt5.QtCore import QObject, QThread, QTimer, pyqtSignal, pyqtSlot
|
2026-08-10 12:32:39 +08:00
|
|
|
|
from qfluentwidgets import MessageBox
|
|
|
|
|
|
|
|
|
|
|
|
from .settings_repository import SettingsRepository
|
|
|
|
|
|
from .update_service import (
|
2026-08-10 14:31:44 +08:00
|
|
|
|
DEFAULT_UPDATE_MANIFEST_URL,
|
|
|
|
|
|
DEFAULT_UPDATE_USERNAME,
|
2026-08-10 14:55:17 +08:00
|
|
|
|
LEGACY_DEFAULT_UPDATE_MANIFEST_URL,
|
2026-08-10 12:32:39 +08:00
|
|
|
|
UPDATE_MANIFEST_SETTING,
|
2026-08-10 14:31:44 +08:00
|
|
|
|
UPDATE_USERNAME_SETTING,
|
2026-08-10 12:32:39 +08:00
|
|
|
|
UpdateCancelled,
|
|
|
|
|
|
UpdateCheckResult,
|
2026-08-10 14:31:44 +08:00
|
|
|
|
UpdateCredentials,
|
2026-08-10 12:32:39 +08:00
|
|
|
|
UpdateInfo,
|
|
|
|
|
|
UpdateService,
|
|
|
|
|
|
validate_manifest_url,
|
|
|
|
|
|
)
|
2026-08-10 14:31:44 +08:00
|
|
|
|
from .windows_credential_store import (
|
|
|
|
|
|
CredentialStoreError,
|
|
|
|
|
|
WindowsCredentialStore,
|
|
|
|
|
|
)
|
2026-08-10 12:32:39 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UpdateCheckWorker(QObject):
|
2026-08-10 14:31:44 +08:00
|
|
|
|
"""在线程中使用内存凭据检查新版本。"""
|
2026-08-10 12:32:39 +08:00
|
|
|
|
|
|
|
|
|
|
succeeded = pyqtSignal(object)
|
|
|
|
|
|
failed = pyqtSignal(str)
|
|
|
|
|
|
completed = pyqtSignal()
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
service: UpdateService,
|
|
|
|
|
|
manifest_url: str,
|
2026-08-10 14:31:44 +08:00
|
|
|
|
credentials: UpdateCredentials,
|
2026-08-10 12:32:39 +08:00
|
|
|
|
):
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self._service = service
|
|
|
|
|
|
self._manifest_url = manifest_url
|
2026-08-10 14:31:44 +08:00
|
|
|
|
self._credentials = credentials
|
2026-08-10 12:32:39 +08:00
|
|
|
|
self._cancelled = False
|
|
|
|
|
|
|
|
|
|
|
|
def cancel(self) -> None:
|
|
|
|
|
|
self._cancelled = True
|
|
|
|
|
|
|
|
|
|
|
|
@pyqtSlot()
|
|
|
|
|
|
def run(self) -> None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = self._service.check(
|
2026-08-10 14:31:44 +08:00
|
|
|
|
self._manifest_url,
|
2026-08-10 12:32:39 +08:00
|
|
|
|
is_cancelled=lambda: self._cancelled,
|
2026-08-10 14:31:44 +08:00
|
|
|
|
credentials=self._credentials,
|
2026-08-10 12:32:39 +08:00
|
|
|
|
)
|
|
|
|
|
|
if not self._cancelled:
|
|
|
|
|
|
self.succeeded.emit(result)
|
|
|
|
|
|
except UpdateCancelled:
|
|
|
|
|
|
pass
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
if not self._cancelled:
|
|
|
|
|
|
self.failed.emit(str(exc) or "检查更新失败")
|
|
|
|
|
|
finally:
|
2026-08-10 14:31:44 +08:00
|
|
|
|
self._credentials = None
|
2026-08-10 12:32:39 +08:00
|
|
|
|
self.completed.emit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UpdateDownloadWorker(QObject):
|
2026-08-10 14:31:44 +08:00
|
|
|
|
"""在线程中认证下载、校验并安全暂存更新。"""
|
2026-08-10 12:32:39 +08:00
|
|
|
|
|
|
|
|
|
|
progressChanged = pyqtSignal(int)
|
|
|
|
|
|
succeeded = pyqtSignal(str)
|
|
|
|
|
|
failed = pyqtSignal(str)
|
|
|
|
|
|
completed = pyqtSignal()
|
|
|
|
|
|
|
2026-08-10 14:31:44 +08:00
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
service: UpdateService,
|
|
|
|
|
|
update: UpdateInfo,
|
|
|
|
|
|
credentials: UpdateCredentials,
|
|
|
|
|
|
):
|
2026-08-10 12:32:39 +08:00
|
|
|
|
super().__init__()
|
|
|
|
|
|
self._service = service
|
|
|
|
|
|
self._update = update
|
2026-08-10 14:31:44 +08:00
|
|
|
|
self._credentials = credentials
|
2026-08-10 12:32:39 +08:00
|
|
|
|
self._cancelled = False
|
|
|
|
|
|
|
|
|
|
|
|
def cancel(self) -> None:
|
|
|
|
|
|
self._cancelled = True
|
|
|
|
|
|
|
|
|
|
|
|
@pyqtSlot()
|
|
|
|
|
|
def run(self) -> None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
self._service.download_and_stage(
|
|
|
|
|
|
self._update,
|
|
|
|
|
|
is_cancelled=lambda: self._cancelled,
|
|
|
|
|
|
on_progress=self.progressChanged.emit,
|
2026-08-10 14:31:44 +08:00
|
|
|
|
credentials=self._credentials,
|
2026-08-10 12:32:39 +08:00
|
|
|
|
)
|
|
|
|
|
|
if not self._cancelled:
|
|
|
|
|
|
self.succeeded.emit(self._update.version)
|
|
|
|
|
|
except UpdateCancelled:
|
|
|
|
|
|
pass
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
if not self._cancelled:
|
|
|
|
|
|
self.failed.emit(str(exc) or "下载更新失败")
|
|
|
|
|
|
finally:
|
2026-08-10 14:31:44 +08:00
|
|
|
|
self._credentials = None
|
2026-08-10 12:32:39 +08:00
|
|
|
|
self.completed.emit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UpdateUiEventBinder(QObject):
|
2026-08-10 14:31:44 +08:00
|
|
|
|
"""管理更新配置、保存反馈和两个后台线程。"""
|
2026-08-10 12:32:39 +08:00
|
|
|
|
|
2026-08-11 11:13:16 +08:00
|
|
|
|
updateAvailabilityChanged = pyqtSignal(bool, str, str)
|
|
|
|
|
|
|
2026-08-10 12:32:39 +08:00
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
page,
|
|
|
|
|
|
repository: SettingsRepository,
|
|
|
|
|
|
service: Optional[UpdateService] = None,
|
2026-08-10 14:31:44 +08:00
|
|
|
|
credential_store=None,
|
2026-08-10 12:32:39 +08:00
|
|
|
|
parent=None,
|
|
|
|
|
|
):
|
|
|
|
|
|
super().__init__(parent or page)
|
|
|
|
|
|
self._page = page
|
|
|
|
|
|
self._repository = repository
|
|
|
|
|
|
self._service = service or UpdateService()
|
2026-08-10 14:31:44 +08:00
|
|
|
|
self._credential_store_override = credential_store
|
2026-08-10 12:32:39 +08:00
|
|
|
|
self._closing = False
|
|
|
|
|
|
self._check_thread: Optional[QThread] = None
|
|
|
|
|
|
self._check_worker: Optional[UpdateCheckWorker] = None
|
|
|
|
|
|
self._download_thread: Optional[QThread] = None
|
|
|
|
|
|
self._download_worker: Optional[UpdateDownloadWorker] = None
|
|
|
|
|
|
|
2026-08-10 14:31:44 +08:00
|
|
|
|
saved_url_record = repository.get_record(UPDATE_MANIFEST_SETTING)
|
|
|
|
|
|
saved_username_record = repository.get_record(UPDATE_USERNAME_SETTING)
|
|
|
|
|
|
saved_url = (
|
|
|
|
|
|
saved_url_record.value
|
|
|
|
|
|
if saved_url_record is not None and isinstance(saved_url_record.value, str)
|
|
|
|
|
|
else DEFAULT_UPDATE_MANIFEST_URL
|
2026-08-10 12:32:39 +08:00
|
|
|
|
)
|
2026-08-10 14:55:17 +08:00
|
|
|
|
if saved_url == LEGACY_DEFAULT_UPDATE_MANIFEST_URL:
|
|
|
|
|
|
saved_url = DEFAULT_UPDATE_MANIFEST_URL
|
|
|
|
|
|
repository.set(UPDATE_MANIFEST_SETTING, saved_url)
|
2026-08-10 14:31:44 +08:00
|
|
|
|
saved_username = (
|
|
|
|
|
|
saved_username_record.value
|
|
|
|
|
|
if saved_username_record is not None
|
|
|
|
|
|
and isinstance(saved_username_record.value, str)
|
|
|
|
|
|
else DEFAULT_UPDATE_USERNAME
|
|
|
|
|
|
)
|
|
|
|
|
|
page.updateManifestUrlInput.setText(saved_url)
|
|
|
|
|
|
page.updateUsernameInput.setText(saved_username)
|
|
|
|
|
|
page.updatePasswordInput.clear()
|
|
|
|
|
|
self._saved_url = saved_url if saved_url_record is not None else ""
|
|
|
|
|
|
self._saved_username = (
|
|
|
|
|
|
saved_username if saved_username_record is not None else ""
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
credential_ready = self._set_password_placeholder(saved_url)
|
|
|
|
|
|
page.updateSaveButton.clicked.connect(self.request_save)
|
2026-08-10 12:32:39 +08:00
|
|
|
|
page.updateCheckButton.clicked.connect(self.request_check)
|
2026-08-10 14:31:44 +08:00
|
|
|
|
self._sync_controls()
|
|
|
|
|
|
|
|
|
|
|
|
if self._saved_url and self._saved_username and credential_ready:
|
|
|
|
|
|
QTimer.singleShot(0, self.request_check)
|
|
|
|
|
|
|
|
|
|
|
|
def _credential_store_for(self, manifest_url: str):
|
|
|
|
|
|
if self._credential_store_override is not None:
|
|
|
|
|
|
return self._credential_store_override
|
|
|
|
|
|
return WindowsCredentialStore.for_url(manifest_url)
|
|
|
|
|
|
|
|
|
|
|
|
def _set_password_placeholder(self, manifest_url: str) -> bool:
|
|
|
|
|
|
try:
|
|
|
|
|
|
saved = self._credential_store_for(manifest_url).read()
|
|
|
|
|
|
except CredentialStoreError as exc:
|
|
|
|
|
|
self._page.updatePasswordInput.setPlaceholderText("凭据管理器不可用")
|
|
|
|
|
|
self._page.set_update_status(str(exc))
|
|
|
|
|
|
return False
|
|
|
|
|
|
if saved is None:
|
|
|
|
|
|
self._page.updatePasswordInput.setPlaceholderText("请输入密码")
|
|
|
|
|
|
return False
|
|
|
|
|
|
self._page.updatePasswordInput.setPlaceholderText(
|
|
|
|
|
|
"密码已安全保存;留空表示不修改"
|
|
|
|
|
|
)
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
@pyqtSlot()
|
|
|
|
|
|
def request_save(self) -> None:
|
|
|
|
|
|
"""显式保存非敏感设置和 Windows 系统凭据。"""
|
|
|
|
|
|
|
|
|
|
|
|
if self._closing or self._is_busy():
|
|
|
|
|
|
return
|
|
|
|
|
|
manifest_url = self._page.updateManifestUrlInput.text().strip()
|
|
|
|
|
|
username = self._page.updateUsernameInput.text().strip()
|
|
|
|
|
|
password = self._page.updatePasswordInput.text()
|
|
|
|
|
|
self._page.updateManifestUrlInput.setText(manifest_url)
|
|
|
|
|
|
self._page.updateUsernameInput.setText(username)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
normalized_url = validate_manifest_url(manifest_url)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
self._page.set_update_status(f"保存失败:{exc}")
|
|
|
|
|
|
self._page.updateManifestUrlInput.setFocus()
|
|
|
|
|
|
return
|
|
|
|
|
|
if not username:
|
|
|
|
|
|
self._page.set_update_status("保存失败:更新账号不能为空")
|
|
|
|
|
|
self._page.updateUsernameInput.setFocus()
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
credential_store = self._credential_store_for(normalized_url)
|
|
|
|
|
|
existing = credential_store.read()
|
|
|
|
|
|
if password:
|
|
|
|
|
|
credential_store.save(username, password)
|
|
|
|
|
|
elif existing is None:
|
|
|
|
|
|
self._page.set_update_status("保存失败:请填写更新密码")
|
|
|
|
|
|
self._page.updatePasswordInput.setFocus()
|
|
|
|
|
|
return
|
|
|
|
|
|
elif existing[0] != username:
|
|
|
|
|
|
self._page.set_update_status("账号已修改,请重新填写密码后保存")
|
|
|
|
|
|
self._page.updatePasswordInput.setFocus()
|
|
|
|
|
|
return
|
|
|
|
|
|
self._repository.set_many(
|
|
|
|
|
|
{
|
|
|
|
|
|
UPDATE_MANIFEST_SETTING: normalized_url,
|
|
|
|
|
|
UPDATE_USERNAME_SETTING: username,
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
except (CredentialStoreError, OSError, sqlite3.Error, ValueError) as exc:
|
|
|
|
|
|
self._page.set_update_status(f"保存失败:{exc}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self._saved_url = normalized_url
|
|
|
|
|
|
self._saved_username = username
|
|
|
|
|
|
self._page.updatePasswordInput.clear()
|
|
|
|
|
|
self._set_password_placeholder(normalized_url)
|
|
|
|
|
|
self._page.set_update_status("更新设置已安全保存")
|
2026-08-10 12:32:39 +08:00
|
|
|
|
|
|
|
|
|
|
@pyqtSlot()
|
|
|
|
|
|
def request_check(self) -> None:
|
2026-08-10 14:31:44 +08:00
|
|
|
|
if self._closing or self._is_busy():
|
2026-08-10 12:32:39 +08:00
|
|
|
|
return
|
|
|
|
|
|
manifest_url = self._page.updateManifestUrlInput.text().strip()
|
2026-08-10 14:31:44 +08:00
|
|
|
|
username = self._page.updateUsernameInput.text().strip()
|
|
|
|
|
|
if not self._saved_url or not self._saved_username:
|
|
|
|
|
|
self._page.set_update_status("尚未保存更新账号和密码,请填写后保存")
|
|
|
|
|
|
self._page.updatePasswordInput.setFocus()
|
|
|
|
|
|
return
|
|
|
|
|
|
if (
|
|
|
|
|
|
manifest_url != self._saved_url
|
|
|
|
|
|
or username != self._saved_username
|
|
|
|
|
|
or self._page.updatePasswordInput.text()
|
|
|
|
|
|
):
|
|
|
|
|
|
self._page.set_update_status("更新设置有修改,请先保存后再检查")
|
|
|
|
|
|
self._page.updateSaveButton.setFocus()
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
credentials = self._read_credentials(username)
|
|
|
|
|
|
if credentials is None:
|
2026-08-10 12:32:39 +08:00
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self._page.set_update_status("正在检查新版本…")
|
|
|
|
|
|
thread = QThread(self)
|
2026-08-10 14:31:44 +08:00
|
|
|
|
worker = UpdateCheckWorker(self._service, manifest_url, credentials)
|
2026-08-10 12:32:39 +08:00
|
|
|
|
worker.moveToThread(thread)
|
|
|
|
|
|
thread.started.connect(worker.run)
|
|
|
|
|
|
worker.succeeded.connect(self._on_check_succeeded)
|
|
|
|
|
|
worker.failed.connect(self._on_check_failed)
|
|
|
|
|
|
worker.completed.connect(thread.quit)
|
|
|
|
|
|
worker.completed.connect(worker.deleteLater)
|
|
|
|
|
|
thread.finished.connect(self._on_check_finished)
|
|
|
|
|
|
thread.finished.connect(thread.deleteLater)
|
|
|
|
|
|
self._check_thread = thread
|
|
|
|
|
|
self._check_worker = worker
|
2026-08-10 14:31:44 +08:00
|
|
|
|
self._sync_controls()
|
2026-08-10 12:32:39 +08:00
|
|
|
|
thread.start()
|
|
|
|
|
|
|
2026-08-10 14:31:44 +08:00
|
|
|
|
def _read_credentials(self, expected_username: str) -> Optional[UpdateCredentials]:
|
|
|
|
|
|
try:
|
|
|
|
|
|
saved = self._credential_store_for(self._saved_url).read()
|
|
|
|
|
|
except CredentialStoreError as exc:
|
|
|
|
|
|
self._page.set_update_status(f"无法读取更新密码:{exc}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
if saved is None:
|
|
|
|
|
|
self._page.set_update_status("尚未保存更新密码,请填写后保存")
|
|
|
|
|
|
self._page.updatePasswordInput.setFocus()
|
|
|
|
|
|
return None
|
|
|
|
|
|
username, password = saved
|
|
|
|
|
|
if username != expected_username:
|
|
|
|
|
|
self._page.set_update_status("保存的账号不一致,请重新填写密码并保存")
|
|
|
|
|
|
self._page.updatePasswordInput.setFocus()
|
|
|
|
|
|
return None
|
|
|
|
|
|
return UpdateCredentials(username, password)
|
|
|
|
|
|
|
2026-08-10 12:32:39 +08:00
|
|
|
|
@pyqtSlot(object)
|
|
|
|
|
|
def _on_check_succeeded(self, result: UpdateCheckResult) -> None:
|
|
|
|
|
|
if self._closing:
|
|
|
|
|
|
return
|
2026-08-11 11:13:16 +08:00
|
|
|
|
self._page.set_update_versions(
|
|
|
|
|
|
result.current_version,
|
|
|
|
|
|
result.latest_version,
|
|
|
|
|
|
)
|
2026-08-10 12:32:39 +08:00
|
|
|
|
if not result.available or result.update is None:
|
2026-08-11 11:13:16 +08:00
|
|
|
|
self.updateAvailabilityChanged.emit(
|
|
|
|
|
|
False,
|
|
|
|
|
|
result.current_version,
|
|
|
|
|
|
result.latest_version,
|
|
|
|
|
|
)
|
2026-08-10 12:32:39 +08:00
|
|
|
|
self._page.set_update_status(
|
|
|
|
|
|
f"当前已是最新版本({result.current_version})"
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
update = result.update
|
2026-08-11 11:13:16 +08:00
|
|
|
|
self.updateAvailabilityChanged.emit(
|
|
|
|
|
|
True,
|
|
|
|
|
|
result.current_version,
|
|
|
|
|
|
update.version,
|
|
|
|
|
|
)
|
2026-08-10 12:32:39 +08:00
|
|
|
|
size_mb = update.size / (1024 * 1024)
|
|
|
|
|
|
dialog = MessageBox(
|
|
|
|
|
|
f"发现新版本 {update.version}",
|
|
|
|
|
|
f"当前版本:{result.current_version}\n"
|
|
|
|
|
|
f"下载大小:{size_mb:.1f} MB\n\n"
|
|
|
|
|
|
"下载完成后不会强制关闭程序,您可以完成当前任务后再重新启动。",
|
|
|
|
|
|
self._page.window(),
|
|
|
|
|
|
)
|
|
|
|
|
|
dialog.yesButton.setText("下载更新")
|
|
|
|
|
|
dialog.cancelButton.setText("暂不下载")
|
|
|
|
|
|
dialog.cancelButton.setFocus()
|
|
|
|
|
|
if not dialog.exec():
|
2026-08-10 14:31:44 +08:00
|
|
|
|
self._page.set_update_status(f"发现新版本 {update.version},尚未下载")
|
2026-08-10 12:32:39 +08:00
|
|
|
|
return
|
|
|
|
|
|
self._start_download(update)
|
|
|
|
|
|
|
|
|
|
|
|
@pyqtSlot(str)
|
|
|
|
|
|
def _on_check_failed(self, message: str) -> None:
|
|
|
|
|
|
if not self._closing:
|
|
|
|
|
|
self._page.set_update_status(
|
2026-08-10 14:31:44 +08:00
|
|
|
|
f"检查失败:{message};设置已保留,可以重试"
|
2026-08-10 12:32:39 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@pyqtSlot()
|
|
|
|
|
|
def _on_check_finished(self) -> None:
|
|
|
|
|
|
self._check_worker = None
|
|
|
|
|
|
self._check_thread = None
|
|
|
|
|
|
if not self._closing:
|
2026-08-10 14:31:44 +08:00
|
|
|
|
self._sync_controls()
|
2026-08-10 12:32:39 +08:00
|
|
|
|
|
|
|
|
|
|
def _start_download(self, update: UpdateInfo) -> None:
|
|
|
|
|
|
if self._closing or self._download_thread is not None:
|
|
|
|
|
|
return
|
2026-08-10 14:31:44 +08:00
|
|
|
|
credentials = self._read_credentials(self._saved_username)
|
|
|
|
|
|
if credentials is None:
|
|
|
|
|
|
return
|
2026-08-10 12:32:39 +08:00
|
|
|
|
self._page.set_update_status(f"正在下载版本 {update.version}(0%)…")
|
|
|
|
|
|
thread = QThread(self)
|
2026-08-10 14:31:44 +08:00
|
|
|
|
worker = UpdateDownloadWorker(self._service, update, credentials)
|
2026-08-10 12:32:39 +08:00
|
|
|
|
worker.moveToThread(thread)
|
|
|
|
|
|
thread.started.connect(worker.run)
|
|
|
|
|
|
worker.progressChanged.connect(self._on_download_progress)
|
|
|
|
|
|
worker.succeeded.connect(self._on_download_succeeded)
|
|
|
|
|
|
worker.failed.connect(self._on_download_failed)
|
|
|
|
|
|
worker.completed.connect(thread.quit)
|
|
|
|
|
|
worker.completed.connect(worker.deleteLater)
|
|
|
|
|
|
thread.finished.connect(self._on_download_finished)
|
|
|
|
|
|
thread.finished.connect(thread.deleteLater)
|
|
|
|
|
|
self._download_thread = thread
|
|
|
|
|
|
self._download_worker = worker
|
2026-08-10 14:31:44 +08:00
|
|
|
|
self._sync_controls()
|
2026-08-10 12:32:39 +08:00
|
|
|
|
thread.start()
|
|
|
|
|
|
|
|
|
|
|
|
@pyqtSlot(int)
|
|
|
|
|
|
def _on_download_progress(self, percent: int) -> None:
|
|
|
|
|
|
if not self._closing:
|
|
|
|
|
|
self._page.set_update_status(f"正在下载并校验更新({percent}%)…")
|
|
|
|
|
|
|
|
|
|
|
|
@pyqtSlot(str)
|
|
|
|
|
|
def _on_download_succeeded(self, version: str) -> None:
|
|
|
|
|
|
if not self._closing:
|
|
|
|
|
|
self._page.set_update_status(
|
|
|
|
|
|
f"版本 {version} 已准备好;完成当前任务后关闭并重新启动程序即可更新"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@pyqtSlot(str)
|
|
|
|
|
|
def _on_download_failed(self, message: str) -> None:
|
|
|
|
|
|
if not self._closing:
|
|
|
|
|
|
self._page.set_update_status(
|
|
|
|
|
|
f"下载失败:{message};当前版本未改动,可以重试"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@pyqtSlot()
|
|
|
|
|
|
def _on_download_finished(self) -> None:
|
|
|
|
|
|
self._download_worker = None
|
|
|
|
|
|
self._download_thread = None
|
|
|
|
|
|
if not self._closing:
|
2026-08-10 14:31:44 +08:00
|
|
|
|
self._sync_controls()
|
2026-08-10 12:32:39 +08:00
|
|
|
|
|
2026-08-10 14:31:44 +08:00
|
|
|
|
def _is_busy(self) -> bool:
|
|
|
|
|
|
return self._check_thread is not None or self._download_thread is not None
|
|
|
|
|
|
|
|
|
|
|
|
def _sync_controls(self) -> None:
|
|
|
|
|
|
enabled = not self._closing and not self._is_busy()
|
|
|
|
|
|
self._page.updateSaveButton.setEnabled(enabled)
|
|
|
|
|
|
self._page.updateCheckButton.setEnabled(enabled)
|
|
|
|
|
|
self._page.updateManifestUrlInput.setEnabled(enabled)
|
|
|
|
|
|
self._page.updateUsernameInput.setEnabled(enabled)
|
|
|
|
|
|
self._page.updatePasswordInput.setEnabled(enabled)
|
2026-08-10 12:32:39 +08:00
|
|
|
|
|
|
|
|
|
|
@pyqtSlot()
|
|
|
|
|
|
def shutdown(self) -> None:
|
2026-08-10 14:31:44 +08:00
|
|
|
|
"""取消后续处理,断开业务结果并等待线程安全退出。"""
|
2026-08-10 12:32:39 +08:00
|
|
|
|
|
|
|
|
|
|
if self._closing:
|
|
|
|
|
|
return
|
|
|
|
|
|
self._closing = True
|
|
|
|
|
|
for worker, thread, signal_slots in (
|
|
|
|
|
|
(
|
|
|
|
|
|
self._check_worker,
|
|
|
|
|
|
self._check_thread,
|
|
|
|
|
|
(
|
|
|
|
|
|
("succeeded", self._on_check_succeeded),
|
|
|
|
|
|
("failed", self._on_check_failed),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
self._download_worker,
|
|
|
|
|
|
self._download_thread,
|
|
|
|
|
|
(
|
|
|
|
|
|
("progressChanged", self._on_download_progress),
|
|
|
|
|
|
("succeeded", self._on_download_succeeded),
|
|
|
|
|
|
("failed", self._on_download_failed),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
):
|
|
|
|
|
|
if worker is not None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
worker.cancel()
|
|
|
|
|
|
for signal_name, slot in signal_slots:
|
|
|
|
|
|
try:
|
|
|
|
|
|
getattr(worker, signal_name).disconnect(slot)
|
|
|
|
|
|
except (TypeError, RuntimeError):
|
|
|
|
|
|
pass
|
|
|
|
|
|
except RuntimeError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
if thread is not None and thread.isRunning():
|
|
|
|
|
|
thread.quit()
|
|
|
|
|
|
thread.wait(11000)
|
2026-08-10 14:31:44 +08:00
|
|
|
|
self._sync_controls()
|