feat: 优化设置布局并支持管理端地址切换 (#130)
This commit is contained in:
@@ -11,6 +11,7 @@ from .settings_repository import SettingsRepository
|
|||||||
|
|
||||||
CLIENT_ID_KEY = "admin.client_id"
|
CLIENT_ID_KEY = "admin.client_id"
|
||||||
CLIENT_NAME_KEY = "admin.client_name"
|
CLIENT_NAME_KEY = "admin.client_name"
|
||||||
|
ADMIN_BASE_URL_KEY = "admin.base_url"
|
||||||
|
|
||||||
|
|
||||||
def generate_client_device_id() -> str:
|
def generate_client_device_id() -> str:
|
||||||
@@ -56,8 +57,12 @@ class CurrentClientService:
|
|||||||
)
|
)
|
||||||
return CurrentClientSettings(client_id, client_name)
|
return CurrentClientSettings(client_id, client_name)
|
||||||
|
|
||||||
def save(self, client_name: str) -> CurrentClientSettings:
|
def save(
|
||||||
"""保存名称;设备号不存在时生成,存在时保持不变。"""
|
self,
|
||||||
|
client_name: str,
|
||||||
|
admin_base_url: Optional[str] = None,
|
||||||
|
) -> CurrentClientSettings:
|
||||||
|
"""原子保存身份;传入管理端地址时一并保存。"""
|
||||||
|
|
||||||
normalized_name = self._clean_text(client_name)
|
normalized_name = self._clean_text(client_name)
|
||||||
if len(normalized_name) > 50:
|
if len(normalized_name) > 50:
|
||||||
@@ -70,12 +75,13 @@ class CurrentClientService:
|
|||||||
if not client_id:
|
if not client_id:
|
||||||
raise ValueError("生成的设备号不能为空")
|
raise ValueError("生成的设备号不能为空")
|
||||||
|
|
||||||
self._repository.set_many(
|
values = {
|
||||||
{
|
CLIENT_ID_KEY: client_id,
|
||||||
CLIENT_ID_KEY: client_id,
|
CLIENT_NAME_KEY: normalized_name,
|
||||||
CLIENT_NAME_KEY: normalized_name,
|
}
|
||||||
}
|
if admin_base_url is not None:
|
||||||
)
|
values[ADMIN_BASE_URL_KEY] = self._clean_text(admin_base_url)
|
||||||
|
self._repository.set_many(values)
|
||||||
return CurrentClientSettings(client_id, normalized_name)
|
return CurrentClientSettings(client_id, normalized_name)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -40,6 +40,15 @@ class HttpAdminGateway(AdminGateway):
|
|||||||
parsed = urlparse(normalized_url)
|
parsed = urlparse(normalized_url)
|
||||||
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
||||||
raise ValueError("Admin 服务地址必须是有效的 http 或 https 地址")
|
raise ValueError("Admin 服务地址必须是有效的 http 或 https 地址")
|
||||||
|
if (
|
||||||
|
parsed.username is not None
|
||||||
|
or parsed.password is not None
|
||||||
|
or parsed.query
|
||||||
|
or parsed.fragment
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"Admin 服务地址不能包含账号、密码、查询参数或片段"
|
||||||
|
)
|
||||||
if timeout_seconds <= 0:
|
if timeout_seconds <= 0:
|
||||||
raise ValueError("请求超时必须大于 0 秒")
|
raise ValueError("请求超时必须大于 0 秒")
|
||||||
|
|
||||||
|
|||||||
@@ -173,6 +173,12 @@ class ClaimTaskWorker(QObject):
|
|||||||
|
|
||||||
self._cancelled = True
|
self._cancelled = True
|
||||||
|
|
||||||
|
@pyqtSlot(object)
|
||||||
|
def replace_gateway(self, gateway: AdminGateway) -> None:
|
||||||
|
"""当前命令结束后,为后续领取和提交切换 Admin Gateway。"""
|
||||||
|
|
||||||
|
self._gateway = gateway
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
"""兼容旧调用;新代码通过 ``run_selected`` 提交命令。"""
|
"""兼容旧调用;新代码通过 ``run_selected`` 提交命令。"""
|
||||||
@@ -416,6 +422,7 @@ class PDDTaskPageEvent(QObject):
|
|||||||
_claimRunRequested = pyqtSignal(str)
|
_claimRunRequested = pyqtSignal(str)
|
||||||
_claimReleaseRequested = pyqtSignal()
|
_claimReleaseRequested = pyqtSignal()
|
||||||
_claimShutdownRequested = pyqtSignal()
|
_claimShutdownRequested = pyqtSignal()
|
||||||
|
_claimGatewayChanged = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -486,11 +493,13 @@ class PDDTaskPageEvent(QObject):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
settings = settings_repository or SettingsRepository()
|
settings = settings_repository or SettingsRepository()
|
||||||
|
self._settings_repository = settings
|
||||||
self._client_service = CurrentClientService(settings)
|
self._client_service = CurrentClientService(settings)
|
||||||
self._selected_android_device_service = SelectedAndroidDeviceService(
|
self._selected_android_device_service = SelectedAndroidDeviceService(
|
||||||
settings
|
settings
|
||||||
)
|
)
|
||||||
self._claim_gateway = claim_gateway
|
self._claim_gateway = claim_gateway
|
||||||
|
self._owns_claim_gateway = claim_gateway is None
|
||||||
self._claim_gateway_error = ""
|
self._claim_gateway_error = ""
|
||||||
if self._claim_gateway is None:
|
if self._claim_gateway is None:
|
||||||
base_url = settings.get("admin.base_url", DEFAULT_ADMIN_BASE_URL)
|
base_url = settings.get("admin.base_url", DEFAULT_ADMIN_BASE_URL)
|
||||||
@@ -520,6 +529,31 @@ class PDDTaskPageEvent(QObject):
|
|||||||
if application is not None:
|
if application is not None:
|
||||||
application.aboutToQuit.connect(self.shutdown)
|
application.aboutToQuit.connect(self.shutdown)
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def update_admin_base_url(self, base_url: str) -> None:
|
||||||
|
"""让下一次领取、执行和上报统一使用新的 Admin 地址。"""
|
||||||
|
|
||||||
|
if self._closing or not self._owns_claim_gateway:
|
||||||
|
return
|
||||||
|
timeout_value = self._settings_repository.get(
|
||||||
|
"admin.request_timeout_seconds", 3.0
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
timeout_seconds = float(timeout_value)
|
||||||
|
gateway = HttpAdminGateway(
|
||||||
|
base_url,
|
||||||
|
timeout_seconds=timeout_seconds,
|
||||||
|
client_id=self._client_service.load().client_id,
|
||||||
|
)
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
self._claim_gateway_error = str(exc)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._claim_gateway = gateway
|
||||||
|
self._claim_gateway_error = ""
|
||||||
|
if self._claim_worker is not None:
|
||||||
|
self._claimGatewayChanged.emit(gateway)
|
||||||
|
|
||||||
def load_initial_tasks(self) -> None:
|
def load_initial_tasks(self) -> None:
|
||||||
"""应用启动后读取第一页本地任务。"""
|
"""应用启动后读取第一页本地任务。"""
|
||||||
|
|
||||||
@@ -1018,6 +1052,7 @@ class PDDTaskPageEvent(QObject):
|
|||||||
self._claimRunRequested.connect(worker.run_selected)
|
self._claimRunRequested.connect(worker.run_selected)
|
||||||
self._claimReleaseRequested.connect(worker.release_device)
|
self._claimReleaseRequested.connect(worker.release_device)
|
||||||
self._claimShutdownRequested.connect(worker.shutdown_worker)
|
self._claimShutdownRequested.connect(worker.shutdown_worker)
|
||||||
|
self._claimGatewayChanged.connect(worker.replace_gateway)
|
||||||
worker.taskSaved.connect(self._on_claimed_task_saved)
|
worker.taskSaved.connect(self._on_claimed_task_saved)
|
||||||
worker.retryableFailed.connect(self._route_claim_retryable_failed)
|
worker.retryableFailed.connect(self._route_claim_retryable_failed)
|
||||||
worker.failed.connect(self._route_claim_failed)
|
worker.failed.connect(self._route_claim_failed)
|
||||||
|
|||||||
+55
-26
@@ -13,7 +13,7 @@ from typing import Iterable, Optional
|
|||||||
from PyQt5.QtCore import QAbstractTableModel, QEvent, QModelIndex, Qt, pyqtSignal
|
from PyQt5.QtCore import QAbstractTableModel, QEvent, QModelIndex, Qt, pyqtSignal
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
QFormLayout,
|
QGridLayout,
|
||||||
QHeaderView,
|
QHeaderView,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QLineEdit,
|
QLineEdit,
|
||||||
@@ -243,6 +243,11 @@ class SettingsPage(QWidget):
|
|||||||
self.deviceNameInput.setMaxLength(50)
|
self.deviceNameInput.setMaxLength(50)
|
||||||
self.deviceNameInput.setAccessibleName("当前客户端设备名")
|
self.deviceNameInput.setAccessibleName("当前客户端设备名")
|
||||||
|
|
||||||
|
self.adminBaseUrlInput = LineEdit(self)
|
||||||
|
self.adminBaseUrlInput.setPlaceholderText("http://127.0.0.1:8080")
|
||||||
|
self.adminBaseUrlInput.setClearButtonEnabled(True)
|
||||||
|
self.adminBaseUrlInput.setAccessibleName("Admin 管理端地址")
|
||||||
|
|
||||||
self.currentDeviceSaveButton = PushButton(FIF.SAVE, "保存", self)
|
self.currentDeviceSaveButton = PushButton(FIF.SAVE, "保存", self)
|
||||||
self.currentDeviceSaveButton.setAccessibleName("保存当前设备信息")
|
self.currentDeviceSaveButton.setAccessibleName("保存当前设备信息")
|
||||||
self.currentDeviceStatusLabel = CaptionLabel(
|
self.currentDeviceStatusLabel = CaptionLabel(
|
||||||
@@ -373,16 +378,26 @@ class SettingsPage(QWidget):
|
|||||||
titleLayout.addWidget(self.currentDeviceStatusLabel, 1)
|
titleLayout.addWidget(self.currentDeviceStatusLabel, 1)
|
||||||
layout.addLayout(titleLayout)
|
layout.addLayout(titleLayout)
|
||||||
|
|
||||||
form = QFormLayout()
|
self.currentDeviceFormLayout = QGridLayout()
|
||||||
form.setHorizontalSpacing(16)
|
self.currentDeviceFormLayout.setHorizontalSpacing(12)
|
||||||
form.setVerticalSpacing(12)
|
self.currentDeviceFormLayout.setVerticalSpacing(12)
|
||||||
deviceIdLabel = CaptionLabel("设备号", card)
|
self.currentDeviceFormLayout.setColumnStretch(1, 1)
|
||||||
deviceIdLabel.setBuddy(self.deviceIdInput)
|
self.currentDeviceFormLayout.setColumnStretch(3, 1)
|
||||||
deviceNameLabel = CaptionLabel("设备名", card)
|
self.deviceIdLabel = CaptionLabel("设备号", card)
|
||||||
deviceNameLabel.setBuddy(self.deviceNameInput)
|
self.deviceIdLabel.setBuddy(self.deviceIdInput)
|
||||||
form.addRow(deviceIdLabel, self.deviceIdInput)
|
self.deviceNameLabel = CaptionLabel("设备名", card)
|
||||||
form.addRow(deviceNameLabel, self.deviceNameInput)
|
self.deviceNameLabel.setBuddy(self.deviceNameInput)
|
||||||
layout.addLayout(form)
|
self.adminBaseUrlLabel = CaptionLabel("管理端地址", card)
|
||||||
|
self.adminBaseUrlLabel.setBuddy(self.adminBaseUrlInput)
|
||||||
|
self.currentDeviceFormLayout.addWidget(self.deviceIdLabel, 0, 0)
|
||||||
|
self.currentDeviceFormLayout.addWidget(self.deviceIdInput, 0, 1)
|
||||||
|
self.currentDeviceFormLayout.addWidget(self.deviceNameLabel, 0, 2)
|
||||||
|
self.currentDeviceFormLayout.addWidget(self.deviceNameInput, 0, 3)
|
||||||
|
self.currentDeviceFormLayout.addWidget(self.adminBaseUrlLabel, 1, 0)
|
||||||
|
self.currentDeviceFormLayout.addWidget(
|
||||||
|
self.adminBaseUrlInput, 1, 1, 1, 3
|
||||||
|
)
|
||||||
|
layout.addLayout(self.currentDeviceFormLayout)
|
||||||
|
|
||||||
commandLayout = QHBoxLayout()
|
commandLayout = QHBoxLayout()
|
||||||
commandLayout.addStretch(1)
|
commandLayout.addStretch(1)
|
||||||
@@ -434,21 +449,30 @@ class SettingsPage(QWidget):
|
|||||||
titleLayout.addWidget(self.updateStatusLabel, 1)
|
titleLayout.addWidget(self.updateStatusLabel, 1)
|
||||||
layout.addLayout(titleLayout)
|
layout.addLayout(titleLayout)
|
||||||
|
|
||||||
form = QFormLayout()
|
self.updateFormLayout = QGridLayout()
|
||||||
form.setHorizontalSpacing(16)
|
self.updateFormLayout.setHorizontalSpacing(12)
|
||||||
form.setVerticalSpacing(12)
|
self.updateFormLayout.setVerticalSpacing(12)
|
||||||
versionLabel = CaptionLabel("当前版本", card)
|
# 输入区按约 45:55 分配,标签列只占自身需要的宽度。
|
||||||
manifestLabel = CaptionLabel("清单地址", card)
|
self.updateFormLayout.setColumnStretch(1, 45)
|
||||||
usernameLabel = CaptionLabel("账号", card)
|
self.updateFormLayout.setColumnStretch(3, 55)
|
||||||
passwordLabel = CaptionLabel("密码", card)
|
self.versionLabel = CaptionLabel("当前版本", card)
|
||||||
manifestLabel.setBuddy(self.updateManifestUrlInput)
|
self.manifestLabel = CaptionLabel("清单地址", card)
|
||||||
usernameLabel.setBuddy(self.updateUsernameInput)
|
self.usernameLabel = CaptionLabel("账号", card)
|
||||||
passwordLabel.setBuddy(self.updatePasswordInput)
|
self.passwordLabel = CaptionLabel("密码", card)
|
||||||
form.addRow(versionLabel, self.currentVersionLabel)
|
self.manifestLabel.setBuddy(self.updateManifestUrlInput)
|
||||||
form.addRow(manifestLabel, self.updateManifestUrlInput)
|
self.usernameLabel.setBuddy(self.updateUsernameInput)
|
||||||
form.addRow(usernameLabel, self.updateUsernameInput)
|
self.passwordLabel.setBuddy(self.updatePasswordInput)
|
||||||
form.addRow(passwordLabel, self.updatePasswordInput)
|
self.updateFormLayout.addWidget(self.versionLabel, 0, 0)
|
||||||
layout.addLayout(form)
|
self.updateFormLayout.addWidget(self.currentVersionLabel, 0, 1, 1, 3)
|
||||||
|
self.updateFormLayout.addWidget(self.manifestLabel, 1, 0)
|
||||||
|
self.updateFormLayout.addWidget(
|
||||||
|
self.updateManifestUrlInput, 1, 1, 1, 3
|
||||||
|
)
|
||||||
|
self.updateFormLayout.addWidget(self.usernameLabel, 2, 0)
|
||||||
|
self.updateFormLayout.addWidget(self.updateUsernameInput, 2, 1)
|
||||||
|
self.updateFormLayout.addWidget(self.passwordLabel, 2, 2)
|
||||||
|
self.updateFormLayout.addWidget(self.updatePasswordInput, 2, 3)
|
||||||
|
layout.addLayout(self.updateFormLayout)
|
||||||
|
|
||||||
commandLayout = QHBoxLayout()
|
commandLayout = QHBoxLayout()
|
||||||
commandLayout.addStretch(1)
|
commandLayout.addStretch(1)
|
||||||
@@ -468,6 +492,11 @@ class SettingsPage(QWidget):
|
|||||||
|
|
||||||
self.currentDeviceStatusLabel.setText(message)
|
self.currentDeviceStatusLabel.setText(message)
|
||||||
|
|
||||||
|
def set_admin_base_url(self, base_url: str) -> None:
|
||||||
|
"""显示当前保存的 Admin 管理端地址。"""
|
||||||
|
|
||||||
|
self.adminBaseUrlInput.setText(base_url)
|
||||||
|
|
||||||
def set_android_devices(self, devices: Iterable[AndroidDeviceRow]) -> None:
|
def set_android_devices(self, devices: Iterable[AndroidDeviceRow]) -> None:
|
||||||
"""显示后续 ADB 服务返回的设备列表。"""
|
"""显示后续 ADB 服务返回的设备列表。"""
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from .admin_gateway import (
|
|||||||
ClientRegistrationGateway,
|
ClientRegistrationGateway,
|
||||||
)
|
)
|
||||||
from .current_client_service import (
|
from .current_client_service import (
|
||||||
|
ADMIN_BASE_URL_KEY,
|
||||||
CurrentClientService,
|
CurrentClientService,
|
||||||
generate_client_device_id,
|
generate_client_device_id,
|
||||||
)
|
)
|
||||||
@@ -46,7 +47,7 @@ DEVICE_ID_PLACEHOLDER = "待生成"
|
|||||||
class CurrentClientSaveWorker(QObject):
|
class CurrentClientSaveWorker(QObject):
|
||||||
"""在线程中先保存本地身份,再登记到 Admin。"""
|
"""在线程中先保存本地身份,再登记到 Admin。"""
|
||||||
|
|
||||||
localSaved = pyqtSignal(str, str)
|
localSaved = pyqtSignal(str, str, str)
|
||||||
localSaveFailed = pyqtSignal(str)
|
localSaveFailed = pyqtSignal(str)
|
||||||
registrationSucceeded = pyqtSignal(str)
|
registrationSucceeded = pyqtSignal(str)
|
||||||
registrationFailed = pyqtSignal(str)
|
registrationFailed = pyqtSignal(str)
|
||||||
@@ -58,6 +59,7 @@ class CurrentClientSaveWorker(QObject):
|
|||||||
gateway: Optional[ClientRegistrationGateway],
|
gateway: Optional[ClientRegistrationGateway],
|
||||||
client_name: str,
|
client_name: str,
|
||||||
capabilities: ClaimCapabilities,
|
capabilities: ClaimCapabilities,
|
||||||
|
admin_base_url: str,
|
||||||
gateway_error: str = "",
|
gateway_error: str = "",
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -65,6 +67,7 @@ class CurrentClientSaveWorker(QObject):
|
|||||||
self._gateway = gateway
|
self._gateway = gateway
|
||||||
self._client_name = client_name
|
self._client_name = client_name
|
||||||
self._capabilities = capabilities
|
self._capabilities = capabilities
|
||||||
|
self._admin_base_url = admin_base_url
|
||||||
self._gateway_error = gateway_error
|
self._gateway_error = gateway_error
|
||||||
self._cancelled = False
|
self._cancelled = False
|
||||||
|
|
||||||
@@ -77,12 +80,19 @@ class CurrentClientSaveWorker(QObject):
|
|||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
saved = self._service.save(self._client_name)
|
saved = self._service.save(
|
||||||
|
self._client_name,
|
||||||
|
self._admin_base_url,
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.localSaveFailed.emit(str(exc) or "无法写入本地数据库")
|
self.localSaveFailed.emit(str(exc) or "无法写入本地数据库")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.localSaved.emit(saved.client_id, saved.client_name)
|
self.localSaved.emit(
|
||||||
|
saved.client_id,
|
||||||
|
saved.client_name,
|
||||||
|
self._admin_base_url,
|
||||||
|
)
|
||||||
if self._cancelled:
|
if self._cancelled:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -300,10 +310,11 @@ class SettingsPageEventBinder(QObject):
|
|||||||
|
|
||||||
searchRequested = pyqtSignal()
|
searchRequested = pyqtSignal()
|
||||||
convertWifiRequested = pyqtSignal(str)
|
convertWifiRequested = pyqtSignal(str)
|
||||||
currentDeviceSaveRequested = pyqtSignal(str, str)
|
currentDeviceSaveRequested = pyqtSignal(str, str, str)
|
||||||
saveRequested = pyqtSignal(str, str)
|
saveRequested = pyqtSignal(str, str)
|
||||||
deleteRequested = pyqtSignal(str)
|
deleteRequested = pyqtSignal(str)
|
||||||
androidDeviceConfigurationChanged = pyqtSignal(str)
|
androidDeviceConfigurationChanged = pyqtSignal(str)
|
||||||
|
adminBaseUrlChanged = pyqtSignal(str)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -353,6 +364,7 @@ class SettingsPageEventBinder(QObject):
|
|||||||
)
|
)
|
||||||
|
|
||||||
repository = settings_repository or SettingsRepository()
|
repository = settings_repository or SettingsRepository()
|
||||||
|
self._settings_repository = repository
|
||||||
self.updateEventBinder = UpdateUiEventBinder(
|
self.updateEventBinder = UpdateUiEventBinder(
|
||||||
page,
|
page,
|
||||||
repository,
|
repository,
|
||||||
@@ -365,6 +377,8 @@ class SettingsPageEventBinder(QObject):
|
|||||||
repository
|
repository
|
||||||
)
|
)
|
||||||
self._admin_gateway = admin_gateway
|
self._admin_gateway = admin_gateway
|
||||||
|
self._owns_admin_gateway = admin_gateway is None
|
||||||
|
self._pending_admin_gateway = None
|
||||||
self._gateway_error = ""
|
self._gateway_error = ""
|
||||||
if self._admin_gateway is None:
|
if self._admin_gateway is None:
|
||||||
base_url = repository.get("admin.base_url", DEFAULT_ADMIN_BASE_URL)
|
base_url = repository.get("admin.base_url", DEFAULT_ADMIN_BASE_URL)
|
||||||
@@ -419,12 +433,20 @@ class SettingsPageEventBinder(QObject):
|
|||||||
|
|
||||||
device_id = self._page.deviceIdInput.text().strip()
|
device_id = self._page.deviceIdInput.text().strip()
|
||||||
device_name = self._page.deviceNameInput.text().strip()
|
device_name = self._page.deviceNameInput.text().strip()
|
||||||
|
admin_base_url = self._page.adminBaseUrlInput.text().strip()
|
||||||
self._page.deviceNameInput.setText(device_name)
|
self._page.deviceNameInput.setText(device_name)
|
||||||
self.currentDeviceSaveRequested.emit(device_id, device_name)
|
self.currentDeviceSaveRequested.emit(
|
||||||
|
device_id,
|
||||||
|
device_name,
|
||||||
|
admin_base_url,
|
||||||
|
)
|
||||||
|
|
||||||
@pyqtSlot(str, str)
|
@pyqtSlot(str, str, str)
|
||||||
def _start_save_current_device(
|
def _start_save_current_device(
|
||||||
self, _displayed_device_id: str, device_name: str
|
self,
|
||||||
|
_displayed_device_id: str,
|
||||||
|
device_name: str,
|
||||||
|
admin_base_url: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
if (
|
if (
|
||||||
self._closing
|
self._closing
|
||||||
@@ -436,6 +458,15 @@ class SettingsPageEventBinder(QObject):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
timeout_value = self._settings_repository.get(
|
||||||
|
"admin.request_timeout_seconds", 3.0
|
||||||
|
)
|
||||||
|
timeout_seconds = float(timeout_value)
|
||||||
|
validated_gateway = HttpAdminGateway(
|
||||||
|
admin_base_url,
|
||||||
|
timeout_seconds=timeout_seconds,
|
||||||
|
)
|
||||||
|
normalized_base_url = admin_base_url.strip().rstrip("/")
|
||||||
device = self._selected_android_device()
|
device = self._selected_android_device()
|
||||||
purchase_mode = (
|
purchase_mode = (
|
||||||
"live"
|
"live"
|
||||||
@@ -452,10 +483,19 @@ class SettingsPageEventBinder(QObject):
|
|||||||
supported_types=supported_types,
|
supported_types=supported_types,
|
||||||
purchase_mode=purchase_mode,
|
purchase_mode=purchase_mode,
|
||||||
)
|
)
|
||||||
except ValueError as exc:
|
except (TypeError, ValueError) as exc:
|
||||||
self._page.set_current_device_status(f"保存失败:{exc}")
|
self._page.set_current_device_status(f"保存失败:{exc}")
|
||||||
|
self._page.adminBaseUrlInput.setFocus()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
pending_gateway = (
|
||||||
|
validated_gateway
|
||||||
|
if self._owns_admin_gateway
|
||||||
|
else self._admin_gateway
|
||||||
|
)
|
||||||
|
self._pending_admin_gateway = pending_gateway
|
||||||
|
self._page.set_admin_base_url(normalized_base_url)
|
||||||
|
|
||||||
self._current_device_busy = True
|
self._current_device_busy = True
|
||||||
self._sync_button_state()
|
self._sync_button_state()
|
||||||
self._page.set_current_device_status("正在保存本地设备信息…")
|
self._page.set_current_device_status("正在保存本地设备信息…")
|
||||||
@@ -463,10 +503,11 @@ class SettingsPageEventBinder(QObject):
|
|||||||
thread = QThread(self)
|
thread = QThread(self)
|
||||||
worker = CurrentClientSaveWorker(
|
worker = CurrentClientSaveWorker(
|
||||||
self._client_service,
|
self._client_service,
|
||||||
self._admin_gateway,
|
pending_gateway,
|
||||||
device_name,
|
device_name,
|
||||||
capabilities,
|
capabilities,
|
||||||
self._gateway_error,
|
normalized_base_url,
|
||||||
|
"",
|
||||||
)
|
)
|
||||||
worker.moveToThread(thread)
|
worker.moveToThread(thread)
|
||||||
|
|
||||||
@@ -905,6 +946,15 @@ class SettingsPageEventBinder(QObject):
|
|||||||
"""页面创建时恢复本地 Client 信息。"""
|
"""页面创建时恢复本地 Client 信息。"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
base_url = self._settings_repository.get(
|
||||||
|
ADMIN_BASE_URL_KEY,
|
||||||
|
DEFAULT_ADMIN_BASE_URL,
|
||||||
|
)
|
||||||
|
displayed_base_url = (
|
||||||
|
base_url.strip()
|
||||||
|
if isinstance(base_url, str) and base_url.strip()
|
||||||
|
else DEFAULT_ADMIN_BASE_URL
|
||||||
|
)
|
||||||
saved = self._client_service.load()
|
saved = self._client_service.load()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._page.set_current_device_status(
|
self._page.set_current_device_status(
|
||||||
@@ -912,6 +962,7 @@ class SettingsPageEventBinder(QObject):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self._page.set_admin_base_url(displayed_base_url)
|
||||||
self._page.set_client_info(saved.client_id, saved.client_name)
|
self._page.set_client_info(saved.client_id, saved.client_name)
|
||||||
if saved.client_id:
|
if saved.client_id:
|
||||||
self._page.set_current_device_status("本地设备信息已加载")
|
self._page.set_current_device_status("本地设备信息已加载")
|
||||||
@@ -1144,11 +1195,20 @@ class SettingsPageEventBinder(QObject):
|
|||||||
self._refresh_registration_after_android_setting = False
|
self._refresh_registration_after_android_setting = False
|
||||||
QTimer.singleShot(0, self._request_save_current_device)
|
QTimer.singleShot(0, self._request_save_current_device)
|
||||||
|
|
||||||
@pyqtSlot(str, str)
|
@pyqtSlot(str, str, str)
|
||||||
def _on_local_saved(self, client_id: str, client_name: str) -> None:
|
def _on_local_saved(
|
||||||
|
self,
|
||||||
|
client_id: str,
|
||||||
|
client_name: str,
|
||||||
|
admin_base_url: str,
|
||||||
|
) -> None:
|
||||||
if self._closing:
|
if self._closing:
|
||||||
return
|
return
|
||||||
self._page.set_client_info(client_id, client_name)
|
self._page.set_client_info(client_id, client_name)
|
||||||
|
self._page.set_admin_base_url(admin_base_url)
|
||||||
|
self._admin_gateway = self._pending_admin_gateway
|
||||||
|
self._gateway_error = ""
|
||||||
|
self.adminBaseUrlChanged.emit(admin_base_url)
|
||||||
self._page.set_current_device_status(
|
self._page.set_current_device_status(
|
||||||
"本地已保存,正在登记到 Admin…"
|
"本地已保存,正在登记到 Admin…"
|
||||||
)
|
)
|
||||||
@@ -1158,6 +1218,7 @@ class SettingsPageEventBinder(QObject):
|
|||||||
if self._closing:
|
if self._closing:
|
||||||
return
|
return
|
||||||
self._page.set_current_device_status(f"本地保存失败:{message}")
|
self._page.set_current_device_status(f"本地保存失败:{message}")
|
||||||
|
self._pending_admin_gateway = None
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def _on_registration_succeeded(self, _registered_at: str) -> None:
|
def _on_registration_succeeded(self, _registered_at: str) -> None:
|
||||||
@@ -1178,6 +1239,7 @@ class SettingsPageEventBinder(QObject):
|
|||||||
self._worker = None
|
self._worker = None
|
||||||
self._thread = None
|
self._thread = None
|
||||||
self._current_device_busy = False
|
self._current_device_busy = False
|
||||||
|
self._pending_admin_gateway = None
|
||||||
if not self._closing:
|
if not self._closing:
|
||||||
self._sync_button_state()
|
self._sync_button_state()
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ class MainWindow(FluentWindow):
|
|||||||
self.pddTaskPage,
|
self.pddTaskPage,
|
||||||
task_repository or TaskRepository(),
|
task_repository or TaskRepository(),
|
||||||
self,
|
self,
|
||||||
|
claim_gateway=admin_gateway,
|
||||||
|
settings_repository=settings_repository,
|
||||||
purchase_adapter_factory=create_u2_purchase_adapter,
|
purchase_adapter_factory=create_u2_purchase_adapter,
|
||||||
live_purchase_adapter_factory=create_u2_live_purchase_adapter,
|
live_purchase_adapter_factory=create_u2_live_purchase_adapter,
|
||||||
purchase_reconcile_factory=create_u2_purchase_reconcile_adapter,
|
purchase_reconcile_factory=create_u2_purchase_reconcile_adapter,
|
||||||
@@ -74,6 +76,9 @@ class MainWindow(FluentWindow):
|
|||||||
self.settingsPage.eventBinder.androidDeviceConfigurationChanged.connect(
|
self.settingsPage.eventBinder.androidDeviceConfigurationChanged.connect(
|
||||||
self.pddTaskPageEvent.android_device_configuration_changed
|
self.pddTaskPageEvent.android_device_configuration_changed
|
||||||
)
|
)
|
||||||
|
self.settingsPage.eventBinder.adminBaseUrlChanged.connect(
|
||||||
|
self.pddTaskPageEvent.update_admin_base_url
|
||||||
|
)
|
||||||
self.addSubInterface(self.pddTaskPage, FIF.HOME, "pdd")
|
self.addSubInterface(self.pddTaskPage, FIF.HOME, "pdd")
|
||||||
self.addSubInterface(
|
self.addSubInterface(
|
||||||
self.settingsPage,
|
self.settingsPage,
|
||||||
|
|||||||
@@ -1110,6 +1110,31 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
|||||||
window.close()
|
window.close()
|
||||||
window.deleteLater()
|
window.deleteLater()
|
||||||
|
|
||||||
|
def test_admin_base_url_change_reaches_persistent_claim_worker(self):
|
||||||
|
settings = self._saved_settings()
|
||||||
|
page = PDDTaskPage()
|
||||||
|
events = PDDTaskPageEvent(
|
||||||
|
page,
|
||||||
|
self.repository,
|
||||||
|
settings_repository=settings,
|
||||||
|
)
|
||||||
|
worker = events._ensure_claim_executor()
|
||||||
|
|
||||||
|
events.update_admin_base_url("http://192.168.0.9:8080/")
|
||||||
|
self.assertTrue(
|
||||||
|
wait_until(
|
||||||
|
self.app,
|
||||||
|
lambda: getattr(worker._gateway, "_base_url", "")
|
||||||
|
== "http://192.168.0.9:8080",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
getattr(events._claim_gateway, "_base_url", ""),
|
||||||
|
"http://192.168.0.9:8080",
|
||||||
|
)
|
||||||
|
events.shutdown()
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
def test_admin_task_mapping_uses_explicit_real_field_names(self):
|
def test_admin_task_mapping_uses_explicit_real_field_names(self):
|
||||||
local_task = admin_task_to_new_claimed_task(
|
local_task = admin_task_to_new_claimed_task(
|
||||||
collect_admin_task("COL-8020a8729f111c15")
|
collect_admin_task("COL-8020a8729f111c15")
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from src.android_device_service import (
|
|||||||
AndroidWifiConversionResult,
|
AndroidWifiConversionResult,
|
||||||
)
|
)
|
||||||
from src.mock_admin_gateway import MockAdminGateway
|
from src.mock_admin_gateway import MockAdminGateway
|
||||||
|
from src.http_admin_gateway import DEFAULT_ADMIN_BASE_URL
|
||||||
from src.selected_android_device_service import SELECTED_ANDROID_SERIAL_KEY
|
from src.selected_android_device_service import SELECTED_ANDROID_SERIAL_KEY
|
||||||
from src.settings_repository import SettingsRepository
|
from src.settings_repository import SettingsRepository
|
||||||
from src.settings_ui import AndroidDeviceRow, SettingsPage
|
from src.settings_ui import AndroidDeviceRow, SettingsPage
|
||||||
@@ -212,10 +213,129 @@ class SettingsPageEventTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(page.deviceIdInput.text(), "CLIENT-EXISTING")
|
self.assertEqual(page.deviceIdInput.text(), "CLIENT-EXISTING")
|
||||||
self.assertEqual(page.deviceNameInput.text(), "办公室电脑")
|
self.assertEqual(page.deviceNameInput.text(), "办公室电脑")
|
||||||
|
self.assertEqual(page.adminBaseUrlInput.text(), DEFAULT_ADMIN_BASE_URL)
|
||||||
self.assertEqual(page.currentDeviceStatusLabel.text(), "本地设备信息已加载")
|
self.assertEqual(page.currentDeviceStatusLabel.text(), "本地设备信息已加载")
|
||||||
page.eventBinder.shutdown()
|
page.eventBinder.shutdown()
|
||||||
page.deleteLater()
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_compact_form_layout_keeps_related_fields_on_same_row(self):
|
||||||
|
page = SettingsPage(
|
||||||
|
settings_repository=self.repository,
|
||||||
|
admin_gateway=MockAdminGateway(),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
page.currentDeviceFormLayout.getItemPosition(
|
||||||
|
page.currentDeviceFormLayout.indexOf(page.deviceIdInput)
|
||||||
|
),
|
||||||
|
(0, 1, 1, 1),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
page.currentDeviceFormLayout.getItemPosition(
|
||||||
|
page.currentDeviceFormLayout.indexOf(page.deviceNameInput)
|
||||||
|
),
|
||||||
|
(0, 3, 1, 1),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
page.currentDeviceFormLayout.getItemPosition(
|
||||||
|
page.currentDeviceFormLayout.indexOf(page.adminBaseUrlInput)
|
||||||
|
),
|
||||||
|
(1, 1, 1, 3),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
page.updateFormLayout.getItemPosition(
|
||||||
|
page.updateFormLayout.indexOf(page.updateUsernameInput)
|
||||||
|
),
|
||||||
|
(2, 1, 1, 1),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
page.updateFormLayout.getItemPosition(
|
||||||
|
page.updateFormLayout.indexOf(page.updatePasswordInput)
|
||||||
|
),
|
||||||
|
(2, 3, 1, 1),
|
||||||
|
)
|
||||||
|
self.assertEqual(page.updateFormLayout.columnStretch(1), 45)
|
||||||
|
self.assertEqual(page.updateFormLayout.columnStretch(3), 55)
|
||||||
|
self.assertIs(page.deviceIdLabel.buddy(), page.deviceIdInput)
|
||||||
|
self.assertIs(page.adminBaseUrlLabel.buddy(), page.adminBaseUrlInput)
|
||||||
|
self.assertIs(page.usernameLabel.buddy(), page.updateUsernameInput)
|
||||||
|
self.assertIs(page.passwordLabel.buddy(), page.updatePasswordInput)
|
||||||
|
page.eventBinder.shutdown()
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_save_persists_normalized_admin_base_url_and_emits_change(self):
|
||||||
|
gateway = MockAdminGateway()
|
||||||
|
page = SettingsPage(
|
||||||
|
settings_repository=self.repository,
|
||||||
|
admin_gateway=gateway,
|
||||||
|
)
|
||||||
|
changed_urls = []
|
||||||
|
page.eventBinder.adminBaseUrlChanged.connect(changed_urls.append)
|
||||||
|
page.deviceNameInput.setText("仓库电脑")
|
||||||
|
page.adminBaseUrlInput.setText(" http://192.168.0.8:8080/ ")
|
||||||
|
|
||||||
|
page.currentDeviceSaveButton.click()
|
||||||
|
self._wait_until(lambda: page.eventBinder._thread is None)
|
||||||
|
|
||||||
|
expected = "http://192.168.0.8:8080"
|
||||||
|
self.assertEqual(self.repository.get("admin.base_url"), expected)
|
||||||
|
self.assertEqual(page.adminBaseUrlInput.text(), expected)
|
||||||
|
self.assertEqual(changed_urls, [expected])
|
||||||
|
self.assertEqual(gateway.registration_count, 1)
|
||||||
|
page.eventBinder.shutdown()
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_invalid_admin_base_url_does_not_save_any_current_device_field(self):
|
||||||
|
self.repository.set_many(
|
||||||
|
{
|
||||||
|
"admin.client_id": "CLIENT-EXISTING",
|
||||||
|
"admin.client_name": "原设备名",
|
||||||
|
"admin.base_url": "http://127.0.0.1:8080",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
gateway = MockAdminGateway()
|
||||||
|
page = SettingsPage(
|
||||||
|
settings_repository=self.repository,
|
||||||
|
admin_gateway=gateway,
|
||||||
|
)
|
||||||
|
page.show()
|
||||||
|
self.app.processEvents()
|
||||||
|
page.deviceNameInput.setText("不应保存的新名称")
|
||||||
|
page.adminBaseUrlInput.setText("不是地址")
|
||||||
|
|
||||||
|
page.currentDeviceSaveButton.click()
|
||||||
|
self.app.processEvents()
|
||||||
|
|
||||||
|
self.assertEqual(self.repository.get("admin.client_name"), "原设备名")
|
||||||
|
self.assertEqual(
|
||||||
|
self.repository.get("admin.base_url"),
|
||||||
|
"http://127.0.0.1:8080",
|
||||||
|
)
|
||||||
|
self.assertEqual(gateway.registration_count, 0)
|
||||||
|
self.assertIn("Admin 服务地址必须", page.currentDeviceStatusLabel.text())
|
||||||
|
self.assertIs(self.app.focusWidget(), page.adminBaseUrlInput)
|
||||||
|
page.eventBinder.shutdown()
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_admin_base_url_with_credentials_is_not_saved(self):
|
||||||
|
gateway = MockAdminGateway()
|
||||||
|
page = SettingsPage(
|
||||||
|
settings_repository=self.repository,
|
||||||
|
admin_gateway=gateway,
|
||||||
|
)
|
||||||
|
page.adminBaseUrlInput.setText(
|
||||||
|
"http://operator@127.0.0.1:8080"
|
||||||
|
)
|
||||||
|
|
||||||
|
page.currentDeviceSaveButton.click()
|
||||||
|
self.app.processEvents()
|
||||||
|
|
||||||
|
self.assertIsNone(self.repository.get("admin.base_url"))
|
||||||
|
self.assertEqual(gateway.registration_count, 0)
|
||||||
|
self.assertIn("不能包含账号、密码", page.currentDeviceStatusLabel.text())
|
||||||
|
page.eventBinder.shutdown()
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
def test_settings_page_has_no_manual_live_purchase_authorization(self):
|
def test_settings_page_has_no_manual_live_purchase_authorization(self):
|
||||||
page = SettingsPage(
|
page = SettingsPage(
|
||||||
settings_repository=self.repository,
|
settings_repository=self.repository,
|
||||||
|
|||||||
@@ -266,6 +266,14 @@ class TaskTableModel(QAbstractTableModel):
|
|||||||
|
|
||||||
设置页使用可滚动布局和 Fluent 设置卡片,分组如下。
|
设置页使用可滚动布局和 Fluent 设置卡片,分组如下。
|
||||||
|
|
||||||
|
### 当前设备
|
||||||
|
|
||||||
|
- 第一行依次显示设备号和设备名;设备号只读,设备名可以修改,两组输入随窗口宽度平均伸缩;
|
||||||
|
- 第二行显示“管理端地址”,对应 SQLite 中已有的 `admin.base_url`,没有保存值时显示默认地址;
|
||||||
|
- 点击“保存”前先验证地址;只接受有效的 HTTP/HTTPS 地址,不能包含账号、密码、查询参数或片段。验证失败时不保存设备号、设备名或地址,并把焦点放回地址输入框;
|
||||||
|
- 保存成功后立即用新地址登记 Client,后续任务领取、结果提交和失败提交也统一切换到新地址;已经发出的请求等待自身结束,不强制中断;
|
||||||
|
- 保存不新增数据库表,不增加心跳,也不连接或操作 Android 设备。
|
||||||
|
|
||||||
### Admin
|
### Admin
|
||||||
|
|
||||||
- 服务地址;
|
- 服务地址;
|
||||||
@@ -311,7 +319,8 @@ class TaskTableModel(QAbstractTableModel):
|
|||||||
### 软件更新
|
### 软件更新
|
||||||
|
|
||||||
- 显示只读的当前版本;
|
- 显示只读的当前版本;
|
||||||
- 清单地址、账号和密码分别使用带可见标签的单行输入框,密码框默认隐藏且不回填;
|
- 清单地址独占一行;账号和密码在下一行并排显示,输入区域按约 45:55 自适应伸缩;
|
||||||
|
- 三个字段都有可见标签,密码框默认隐藏且不回填;
|
||||||
- “保存”把 URL/账号写入 SQLite,把密码写入 Windows 凭据管理器;已有密码时留空
|
- “保存”把 URL/账号写入 SQLite,把密码写入 Windows 凭据管理器;已有密码时留空
|
||||||
表示不修改,修改账号时必须重新输入密码;
|
表示不修改,修改账号时必须重新输入密码;
|
||||||
- “检查更新”只使用已经保存的配置;存在未保存修改时先提示保存;检查和下载期间
|
- “检查更新”只使用已经保存的配置;存在未保存修改时先提示保存;检查和下载期间
|
||||||
|
|||||||
Reference in New Issue
Block a user