feat: 保存并登记当前 Client (#11)

This commit is contained in:
chengma
2026-08-06 17:57:49 +08:00
parent 262f48c778
commit fab5778049
17 changed files with 1078 additions and 43 deletions
+27 -5
View File
@@ -1,6 +1,6 @@
"""Client 访问 Admin 的稳定边界和简单数据对象。
AdminGateway 只有领取任务、提交成功结果、提交失败结果三个业务方法。
AdminGateway 只有登记、领取任务、提交成功结果、提交失败结果四个业务方法。
业务层不应直接依赖 HTTP 请求或 Mock 的内部实现。
"""
@@ -13,13 +13,16 @@ from .task_models import TaskType
@dataclass(frozen=True)
class ClientInfo:
"""发起领取请求的 Client 身份,不保存访问令牌。"""
"""Client 身份和可选名称,不保存访问令牌。"""
client_id: str
name: str = ""
def __post_init__(self) -> None:
if not self.client_id.strip():
raise ValueError("client_id 不能为空")
if len(self.name.strip()) > 50:
raise ValueError("client.name 最多 50 个字")
@dataclass(frozen=True)
@@ -43,7 +46,7 @@ class AndroidDeviceInfo:
class ClaimCapabilities:
"""Client 领取任务时声明的设备与执行能力。"""
device: AndroidDeviceInfo
device: Optional[AndroidDeviceInfo] = None
supported_types: Tuple[TaskType, ...] = (
TaskType.COLLECT,
TaskType.PURCHASE,
@@ -96,6 +99,15 @@ class SubmissionReceipt:
accepted_at: str
@dataclass(frozen=True)
class RegistrationReceipt:
"""Admin 已登记当前 Client 的确认。"""
registered: bool
client_id: str
registered_at: str
class AdminGatewayError(RuntimeError):
"""带稳定错误代码和可重试标志的 Admin 边界错误。"""
@@ -114,8 +126,18 @@ class AdminGatewayError(RuntimeError):
self.details = dict(details or {})
class AdminGateway(ABC):
"""Admin 边界;不得增加状态查询、心跳或租约方法。"""
class ClientRegistrationGateway(ABC):
"""设置页只依赖登记能力,不依赖任务领取和提交。"""
@abstractmethod
def register_client(
self, client: ClientInfo, capabilities: ClaimCapabilities
) -> RegistrationReceipt:
"""幂等登记或更新当前 Client。"""
class AdminGateway(ClientRegistrationGateway):
"""完整 Admin 边界;不得增加状态查询、心跳或租约方法。"""
@abstractmethod
def claim_next(
+83
View File
@@ -0,0 +1,83 @@
"""当前 Windows Client 身份的本地保存与读取服务。"""
import hashlib
import platform
import uuid
from dataclasses import dataclass
from typing import Callable, Optional
from .settings_repository import SettingsRepository
CLIENT_ID_KEY = "admin.client_id"
CLIENT_NAME_KEY = "admin.client_name"
def generate_client_device_id() -> str:
"""根据本机特征生成稳定设备号,只保留不可逆哈希。"""
fingerprint = "|".join(
(
platform.system(),
platform.node(),
platform.machine(),
f"{uuid.getnode():012x}",
)
)
digest = hashlib.sha256(fingerprint.encode("utf-8")).hexdigest()[:16]
return f"CLIENT-{digest.upper()}"
@dataclass(frozen=True)
class CurrentClientSettings:
"""本地已经确认保存的当前 Client 信息。"""
client_id: str
client_name: str
class CurrentClientService:
"""保证设备号只生成一次,并与名称一起原子保存。"""
def __init__(
self,
repository: SettingsRepository,
device_id_factory: Callable[[], str] = generate_client_device_id,
):
self._repository = repository
self._device_id_factory = device_id_factory
def load(self) -> CurrentClientSettings:
"""读取已保存的信息;缺少的字段返回空字符串。"""
client_id = self._clean_text(self._repository.get(CLIENT_ID_KEY, ""))
client_name = self._clean_text(
self._repository.get(CLIENT_NAME_KEY, "")
)
return CurrentClientSettings(client_id, client_name)
def save(self, client_name: str) -> CurrentClientSettings:
"""保存名称;设备号不存在时生成,存在时保持不变。"""
normalized_name = self._clean_text(client_name)
if len(normalized_name) > 50:
raise ValueError("设备名最多 50 个字")
existing = self.load()
client_id = existing.client_id
if not client_id:
client_id = self._clean_text(self._device_id_factory())
if not client_id:
raise ValueError("生成的设备号不能为空")
self._repository.set_many(
{
CLIENT_ID_KEY: client_id,
CLIENT_NAME_KEY: normalized_name,
}
)
return CurrentClientSettings(client_id, normalized_name)
@staticmethod
def _clean_text(value: object) -> str:
return value.strip() if isinstance(value, str) else ""
+190
View File
@@ -0,0 +1,190 @@
"""使用 Python 标准库调用 Admin 登记接口。"""
import json
import socket
from http.client import RemoteDisconnected
from typing import Callable, Optional
from urllib.error import HTTPError, URLError
from urllib.parse import urlparse
from urllib.request import ProxyHandler, Request, build_opener
from uuid import uuid4
from .admin_gateway import (
AdminGatewayError,
ClaimCapabilities,
ClientInfo,
ClientRegistrationGateway,
RegistrationReceipt,
)
DEFAULT_ADMIN_BASE_URL = "http://127.0.0.1:8080"
class HttpAdminGateway(ClientRegistrationGateway):
"""通过 HTTP 登记 Client;访问令牌只保存在内存。"""
def __init__(
self,
base_url: str = DEFAULT_ADMIN_BASE_URL,
token: str = "",
timeout_seconds: float = 3.0,
opener: Optional[Callable] = None,
):
normalized_url = base_url.strip().rstrip("/")
parsed = urlparse(normalized_url)
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
raise ValueError("Admin 服务地址必须是有效的 http 或 https 地址")
if timeout_seconds <= 0:
raise ValueError("请求超时必须大于 0 秒")
self._base_url = normalized_url
self._token = token.strip()
self._timeout_seconds = timeout_seconds
# Admin 通常运行在本机或局域网。明确禁用环境代理,避免
# HTTP_PROXY 把 127.0.0.1 请求错误转发到代理服务器。
self._opener = opener or build_opener(ProxyHandler({})).open
def register_client(
self, client: ClientInfo, capabilities: ClaimCapabilities
) -> RegistrationReceipt:
"""调用独立登记接口,不领取或修改任务。"""
request_id = str(uuid4())
payload = {
"client": {"name": client.name.strip()},
"supported_types": [
task_type.value for task_type in capabilities.supported_types
],
"capabilities": {
"purchase_mode": capabilities.purchase_mode,
"schema_versions": list(capabilities.schema_versions),
},
}
if capabilities.device is not None:
payload["device"] = {
"address": capabilities.device.address.strip(),
"platform": capabilities.device.platform,
"pdd_package": capabilities.device.pdd_package.strip(),
}
headers = {
"Content-Type": "application/json; charset=utf-8",
"Accept": "application/json",
"X-Client-Id": client.client_id.strip(),
"X-Request-Id": request_id,
}
if self._token:
headers["Authorization"] = f"Bearer {self._token}"
request = Request(
f"{self._base_url}/api/v1/client/registration",
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
headers=headers,
method="PUT",
)
try:
with self._opener(request, timeout=self._timeout_seconds) as response:
status = getattr(response, "status", None)
if status is None:
status = response.getcode()
body = response.read()
except HTTPError as exc:
self._raise_http_error(exc, request_id)
except (
URLError,
RemoteDisconnected,
ConnectionError,
socket.timeout,
TimeoutError,
) as exc:
reason = getattr(exc, "reason", exc)
if isinstance(reason, (socket.timeout, TimeoutError)):
raise AdminGatewayError(
"ADMIN_TIMEOUT", "Admin 请求超时,请稍后重试", True, request_id
) from exc
raise AdminGatewayError(
"ADMIN_UNAVAILABLE", "无法连接 Admin,请检查服务地址", True, request_id
) from exc
if status != 200:
raise AdminGatewayError(
"ADMIN_UNEXPECTED_RESPONSE",
f"Admin 返回了未预期的状态码 {status}",
status >= 500,
request_id,
)
data = self._decode_json(body, request_id)
if (
data.get("registered") is not True
or data.get("client_id") != client.client_id.strip()
or not isinstance(data.get("registered_at"), str)
or not data["registered_at"].strip()
):
raise AdminGatewayError(
"ADMIN_INVALID_RESPONSE",
"Admin 登记响应缺少必要字段",
False,
request_id,
)
return RegistrationReceipt(
registered=True,
client_id=data["client_id"],
registered_at=data["registered_at"],
)
@staticmethod
def _decode_json(body: bytes, request_id: str) -> dict:
try:
data = json.loads(body.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise AdminGatewayError(
"ADMIN_INVALID_RESPONSE",
"Admin 返回的不是有效 JSON",
False,
request_id,
) from exc
if not isinstance(data, dict):
raise AdminGatewayError(
"ADMIN_INVALID_RESPONSE",
"Admin 返回的 JSON 不是对象",
False,
request_id,
)
return data
@classmethod
def _raise_http_error(cls, error: HTTPError, request_id: str) -> None:
try:
body = error.read()
except OSError:
body = b""
try:
data = cls._decode_json(body, request_id)
details = data.get("error", {})
if not isinstance(details, dict):
raise ValueError
code = str(details.get("code") or "ADMIN_HTTP_ERROR")
message = str(details.get("message") or "Admin 拒绝了登记请求")
retryable = bool(details.get("retryable", error.code >= 500))
response_request_id = str(details.get("request_id") or request_id)
extra = details.get("details")
if not isinstance(extra, dict):
extra = {}
except (AdminGatewayError, ValueError):
code = "ADMIN_HTTP_ERROR"
message = f"Admin 登记失败,状态码 {error.code}"
retryable = error.code >= 500
response_request_id = request_id
extra = {}
raise AdminGatewayError(
code,
message,
retryable,
response_request_id,
extra,
) from error
+34
View File
@@ -15,6 +15,7 @@ from .admin_gateway import (
AdminTask,
ClaimCapabilities,
ClientInfo,
RegistrationReceipt,
SubmissionReceipt,
)
@@ -49,8 +50,41 @@ class MockAdminGateway(AdminGateway):
] = {}
self._next_error: Optional[AdminGatewayError] = None
self._reject_next_submission = False
self._registrations: Dict[str, Tuple[ClientInfo, ClaimCapabilities]] = {}
self._lock = Lock()
def register_client(
self, client: ClientInfo, capabilities: ClaimCapabilities
) -> RegistrationReceipt:
"""幂等登记 Client,并保留最后一次上报内容供测试检查。"""
with self._lock:
self._raise_forced_error()
normalized = ClientInfo(client.client_id.strip(), client.name.strip())
self._registrations[normalized.client_id] = (
normalized,
deepcopy(capabilities),
)
return RegistrationReceipt(
registered=True,
client_id=normalized.client_id,
registered_at=utc_now_iso(),
)
@property
def registration_count(self) -> int:
"""返回不同 Client ID 的登记数量。"""
return len(self._registrations)
def registered_client(
self, client_id: str
) -> Optional[Tuple[ClientInfo, ClaimCapabilities]]:
"""测试辅助:返回某个 Client 最后一次登记的资料。"""
value = self._registrations.get(client_id)
return deepcopy(value) if value is not None else None
def enqueue_task(self, task: AdminTask, assigned_client_id: str) -> None:
"""测试辅助:加入一条分配给指定 Client 的任务。"""
+30 -1
View File
@@ -3,7 +3,7 @@
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional, Union
from typing import Any, Mapping, Optional, Union
from .db import initialize_database, open_database
from .task_models import AppSettingRecord
@@ -74,6 +74,35 @@ class SettingsRepository:
connection.close()
return AppSettingRecord(key, value, now)
def set_many(
self, values: Mapping[str, Any], updated_at: Optional[str] = None
) -> list[AppSettingRecord]:
"""在同一事务中保存多个非敏感设置。"""
if not values:
return []
normalized = [
(self._validate_key(key), value) for key, value in values.items()
]
now = updated_at or _utc_now_iso()
connection = open_database(self._db_path)
try:
with connection:
for key, value in normalized:
connection.execute(
"INSERT INTO app_settings"
" (setting_key, value_json, updated_at)"
" VALUES (?, ?, ?)"
" ON CONFLICT(setting_key) DO UPDATE SET"
" value_json = excluded.value_json,"
" updated_at = excluded.updated_at",
(key, json.dumps(value, ensure_ascii=False), now),
)
finally:
connection.close()
return [AppSettingRecord(key, value, now) for key, value in normalized]
def delete(self, setting_key: str) -> bool:
"""删除设置;确实删除了一条记录时返回 True。"""
+21 -2
View File
@@ -201,7 +201,12 @@ class AndroidDeviceTableModel(QAbstractTableModel):
class SettingsPage(QWidget):
"""设备管理与后续应用设置的统一页面。"""
def __init__(self, parent=None):
def __init__(
self,
parent=None,
settings_repository=None,
admin_gateway=None,
):
super().__init__(parent)
self.setObjectName("settingsPage")
self._build_ui()
@@ -209,7 +214,11 @@ class SettingsPage(QWidget):
# 延迟到控件创建完成后绑定,避免事件层在构造过程中访问半成品页面。
from .settings_ui_event import SettingsPageEventBinder
self.eventBinder = SettingsPageEventBinder(self)
self.eventBinder = SettingsPageEventBinder(
self,
settings_repository=settings_repository,
admin_gateway=admin_gateway,
)
def _build_ui(self) -> None:
self.deviceIdInput = LineEdit(self)
@@ -225,6 +234,10 @@ class SettingsPage(QWidget):
self.currentDeviceSaveButton = PushButton(FIF.SAVE, "保存", self)
self.currentDeviceSaveButton.setAccessibleName("保存当前设备信息")
self.currentDeviceStatusLabel = CaptionLabel(
"设备信息尚未保存", self
)
self.currentDeviceStatusLabel.setAccessibleName("当前设备保存状态")
self.currentDeviceCard = self._build_current_device_card()
@@ -322,6 +335,7 @@ class SettingsPage(QWidget):
commandLayout.addStretch(1)
commandLayout.addWidget(self.currentDeviceSaveButton)
layout.addLayout(commandLayout)
layout.addWidget(self.currentDeviceStatusLabel)
return card
def _build_android_device_card(self) -> CardWidget:
@@ -357,6 +371,11 @@ class SettingsPage(QWidget):
self.deviceIdInput.setText(device_id or "待生成")
self.deviceNameInput.setText(device_name)
def set_current_device_status(self, message: str) -> None:
"""显示本地保存和 Admin 登记两个阶段的结果。"""
self.currentDeviceStatusLabel.setText(message)
def set_android_devices(self, devices: Iterable[AndroidDeviceRow]) -> None:
"""显示后续 ADB 服务返回的设备列表。"""
+243 -26
View File
@@ -1,32 +1,92 @@
"""设置页的事件绑定和当前客户端设备号生成。
"""设置页事件、当前 Client 本地保存和后台登记。
本文件把界面操作转换为轻量信号,不直接执行 ADB 或 SQLite。
后续应用服务应在后台线程处理阻塞操作,再通过页面更新方法返回结果。
本文件不直接写 SQL。SQLite 保存和 Admin HTTP 请求由 Worker 在线程中执行,
后台结果只通过信号返回主线程更新页面。
"""
import hashlib
import platform
import uuid
from typing import Optional
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot
from PyQt5.QtCore import QCoreApplication, QObject, QThread, pyqtSignal, pyqtSlot
from .admin_gateway import (
AdminGatewayError,
AndroidDeviceInfo,
ClaimCapabilities,
ClientInfo,
ClientRegistrationGateway,
)
from .current_client_service import (
CurrentClientService,
generate_client_device_id,
)
from .http_admin_gateway import DEFAULT_ADMIN_BASE_URL, HttpAdminGateway
from .settings_repository import SettingsRepository
DEVICE_ID_PLACEHOLDER = "待生成"
def generate_client_device_id() -> str:
"""根据本机特征生成设备号,不暴露原始系统和硬件信息。"""
class CurrentClientSaveWorker(QObject):
"""在线程中先保存本地身份,再登记到 Admin。"""
fingerprint = "|".join(
(
platform.system(),
platform.node(),
platform.machine(),
f"{uuid.getnode():012x}",
)
)
digest = hashlib.sha256(fingerprint.encode("utf-8")).hexdigest()[:16].upper()
return f"CLIENT-{digest}"
localSaved = pyqtSignal(str, str)
localSaveFailed = pyqtSignal(str)
registrationSucceeded = pyqtSignal(str)
registrationFailed = pyqtSignal(str)
completed = pyqtSignal()
def __init__(
self,
service: CurrentClientService,
gateway: Optional[ClientRegistrationGateway],
client_name: str,
capabilities: ClaimCapabilities,
gateway_error: str = "",
):
super().__init__()
self._service = service
self._gateway = gateway
self._client_name = client_name
self._capabilities = capabilities
self._gateway_error = gateway_error
self._cancelled = False
def cancel(self) -> None:
"""阻止尚未开始的远端登记;已经发出的 HTTP 等超时返回。"""
self._cancelled = True
@pyqtSlot()
def run(self) -> None:
try:
try:
saved = self._service.save(self._client_name)
except Exception as exc:
self.localSaveFailed.emit(str(exc) or "无法写入本地数据库")
return
self.localSaved.emit(saved.client_id, saved.client_name)
if self._cancelled:
return
if self._gateway is None:
self.registrationFailed.emit(
self._gateway_error or "Admin Gateway 尚未配置"
)
return
try:
receipt = self._gateway.register_client(
ClientInfo(saved.client_id, saved.client_name),
self._capabilities,
)
except AdminGatewayError as exc:
self.registrationFailed.emit(str(exc))
except Exception as exc:
self.registrationFailed.emit(str(exc) or "Admin 登记失败")
else:
self.registrationSucceeded.emit(receipt.registered_at)
finally:
self.completed.emit()
class SettingsPageEventBinder(QObject):
@@ -38,36 +98,108 @@ class SettingsPageEventBinder(QObject):
saveRequested = pyqtSignal(str, str)
deleteRequested = pyqtSignal(str)
def __init__(self, page):
def __init__(
self,
page,
settings_repository: Optional[SettingsRepository] = None,
admin_gateway: Optional[ClientRegistrationGateway] = None,
):
super().__init__(page)
self._page = page
self._busy = False
self._current_device_busy = False
self._closing = False
self._thread: Optional[QThread] = None
self._worker: Optional[CurrentClientSaveWorker] = None
repository = settings_repository or SettingsRepository()
self._client_service = CurrentClientService(repository)
self._admin_gateway = admin_gateway
self._gateway_error = ""
if self._admin_gateway is None:
base_url = repository.get("admin.base_url", DEFAULT_ADMIN_BASE_URL)
timeout_value = repository.get("admin.request_timeout_seconds", 3.0)
try:
timeout_seconds = float(timeout_value)
self._admin_gateway = HttpAdminGateway(
base_url if isinstance(base_url, str) else "",
timeout_seconds=timeout_seconds,
)
except (TypeError, ValueError) as exc:
self._gateway_error = str(exc)
page.currentDeviceSaveButton.clicked.connect(
self._request_save_current_device
)
self.currentDeviceSaveRequested.connect(
self._start_save_current_device
)
page.searchButton.clicked.connect(self._request_search)
page.connectButton.clicked.connect(self._request_connect)
page.saveButton.clicked.connect(self._request_save)
page.deleteButton.clicked.connect(self._request_delete)
page.addressInput.textChanged.connect(self._sync_button_state)
page.deviceTableModel.checkedDeviceChanged.connect(self._sync_button_state)
page.destroyed.connect(self.shutdown)
application = QCoreApplication.instance()
if application is not None:
application.aboutToQuit.connect(self.shutdown)
self._load_current_client()
self._sync_button_state()
@pyqtSlot()
def _request_save_current_device(self) -> None:
if self._busy:
if self._busy or self._current_device_busy:
return
device_id = self._page.deviceIdInput.text().strip()
if not device_id or device_id == DEVICE_ID_PLACEHOLDER:
device_id = generate_client_device_id()
self._page.deviceIdInput.setText(device_id)
device_name = self._page.deviceNameInput.text().strip()
self._page.deviceNameInput.setText(device_name)
self.currentDeviceSaveRequested.emit(device_id, device_name)
@pyqtSlot(str, str)
def _start_save_current_device(
self, _displayed_device_id: str, device_name: str
) -> None:
if self._closing or self._current_device_busy:
return
try:
device = self._selected_android_device()
capabilities = ClaimCapabilities(device=device)
except ValueError as exc:
self._page.set_current_device_status(f"保存失败:{exc}")
return
self._current_device_busy = True
self._sync_button_state()
self._page.set_current_device_status("正在保存本地设备信息…")
thread = QThread(self)
worker = CurrentClientSaveWorker(
self._client_service,
self._admin_gateway,
device_name,
capabilities,
self._gateway_error,
)
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.localSaved.connect(self._on_local_saved)
worker.localSaveFailed.connect(self._on_local_save_failed)
worker.registrationSucceeded.connect(self._on_registration_succeeded)
worker.registrationFailed.connect(self._on_registration_failed)
worker.completed.connect(thread.quit)
worker.completed.connect(worker.deleteLater)
thread.finished.connect(self._on_save_thread_finished)
thread.finished.connect(thread.deleteLater)
self._thread = thread
self._worker = worker
thread.start()
@pyqtSlot()
def _request_search(self) -> None:
if self._busy:
@@ -118,7 +250,9 @@ class SettingsPageEventBinder(QObject):
def _sync_button_state(self, *_args) -> None:
serial = self._page.deviceTableModel.checked_serial
has_address = bool(self._page.addressInput.text().strip())
self._page.currentDeviceSaveButton.setEnabled(not self._busy)
self._page.currentDeviceSaveButton.setEnabled(
not self._busy and not self._current_device_busy
)
self._page.searchButton.setEnabled(not self._busy)
self._page.connectButton.setEnabled(not self._busy and has_address)
self._page.saveButton.setEnabled(not self._busy and bool(serial))
@@ -131,3 +265,86 @@ class SettingsPageEventBinder(QObject):
self._sync_button_state()
if message:
self._page.deviceStatusLabel.setText(message)
def _load_current_client(self) -> None:
"""页面创建时恢复本地 Client 信息。"""
try:
saved = self._client_service.load()
except Exception as exc:
self._page.set_current_device_status(
f"读取本地设备信息失败:{str(exc) or '数据库不可用'}"
)
return
self._page.set_client_info(saved.client_id, saved.client_name)
if saved.client_id:
self._page.set_current_device_status("本地设备信息已加载")
def _selected_android_device(self) -> Optional[AndroidDeviceInfo]:
serial = self._page.deviceTableModel.checked_serial.strip()
return AndroidDeviceInfo(serial) if serial else None
@pyqtSlot(str, str)
def _on_local_saved(self, client_id: str, client_name: str) -> None:
if self._closing:
return
self._page.set_client_info(client_id, client_name)
self._page.set_current_device_status(
"本地已保存,正在登记到 Admin…"
)
@pyqtSlot(str)
def _on_local_save_failed(self, message: str) -> None:
if self._closing:
return
self._page.set_current_device_status(f"本地保存失败:{message}")
@pyqtSlot(str)
def _on_registration_succeeded(self, _registered_at: str) -> None:
if self._closing:
return
self._page.set_current_device_status("本地已保存,已登记到 Admin")
@pyqtSlot(str)
def _on_registration_failed(self, message: str) -> None:
if self._closing:
return
self._page.set_current_device_status(
f"本地已保存,Admin 登记失败:{message};可再次点击保存重试"
)
@pyqtSlot()
def _on_save_thread_finished(self) -> None:
self._worker = None
self._thread = None
self._current_device_busy = False
if not self._closing:
self._sync_button_state()
@pyqtSlot()
def shutdown(self) -> None:
"""窗口关闭时忽略迟到结果,并等待短请求安全结束。"""
if self._closing:
return
self._closing = True
worker = self._worker
thread = self._thread
if worker is not None:
worker.cancel()
for signal, slot in (
(worker.localSaved, self._on_local_saved),
(worker.localSaveFailed, self._on_local_save_failed),
(worker.registrationSucceeded, self._on_registration_succeeded),
(worker.registrationFailed, self._on_registration_failed),
):
try:
signal.disconnect(slot)
except TypeError:
pass
if thread is not None and thread.isRunning():
thread.quit()
thread.wait(4000)
+11 -2
View File
@@ -36,11 +36,20 @@ from .task_repository import TaskRepository
class MainWindow(FluentWindow):
"""应用主窗口。"""
def __init__(self, task_repository=None):
def __init__(
self,
task_repository=None,
settings_repository=None,
admin_gateway=None,
):
super().__init__()
self.pddTaskPage = PDDTaskPage(self)
self.settingsPage = SettingsPage(self)
self.settingsPage = SettingsPage(
self,
settings_repository=settings_repository,
admin_gateway=admin_gateway,
)
self.pddTaskPageEvent = PDDTaskPageEvent(
self.pddTaskPage,
task_repository or TaskRepository(),