821 lines
33 KiB
Python
821 lines
33 KiB
Python
"""Main GUI window."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from PySide6.QtCore import QUrl
|
||
from PySide6.QtGui import QDesktopServices
|
||
|
||
from .. import client_policy, subscription
|
||
from ..version import display_name
|
||
from .activation_dialog import MembershipActivationDialog
|
||
from .tabs.accounts import AccountsTab
|
||
from .tabs.apply import ApplyTab
|
||
from .tabs.collect import CollectTab
|
||
from .tabs.generate import GenerateTab
|
||
from .tabs.product_suite import ProductSuiteTab
|
||
from .tabs.settings import SettingsTab
|
||
from .widgets import *
|
||
from .workers import SubscriptionCheckWorker
|
||
|
||
|
||
PREFERRED_WINDOW_SIZE = (1180, 760)
|
||
MIN_WINDOW_SIZE = (960, 640)
|
||
WINDOW_SCREEN_MARGIN = 40
|
||
# 仅供直接构造 MainWindow 的兼容入口使用;正式启动以远程策略为准。
|
||
SUBSCRIPTION_CHECK_ENABLED = True
|
||
SUBSCRIPTION_ENFORCEMENT_ENABLED = True
|
||
|
||
|
||
def _screen_available_geometry():
|
||
app = QApplication.instance()
|
||
if app is None:
|
||
return None
|
||
screen = app.primaryScreen()
|
||
if screen is None:
|
||
return None
|
||
return screen.availableGeometry()
|
||
|
||
|
||
def _bounded_dimension(available_size, preferred_size, minimum_size, margin):
|
||
if available_size <= 0:
|
||
return preferred_size
|
||
if available_size > margin * 2:
|
||
maximum = available_size - margin * 2
|
||
else:
|
||
maximum = available_size
|
||
if maximum >= minimum_size:
|
||
return max(minimum_size, min(preferred_size, maximum))
|
||
return max(1, min(preferred_size, maximum))
|
||
|
||
|
||
def _fit_and_center_window(
|
||
window,
|
||
available_geometry=None,
|
||
preferred_size=PREFERRED_WINDOW_SIZE,
|
||
minimum_size=MIN_WINDOW_SIZE,
|
||
margin=WINDOW_SCREEN_MARGIN,
|
||
):
|
||
available = available_geometry if available_geometry is not None else _screen_available_geometry()
|
||
if available is None or available.width() <= 0 or available.height() <= 0:
|
||
window.resize(*preferred_size)
|
||
return
|
||
|
||
width = _bounded_dimension(
|
||
available.width(),
|
||
preferred_size[0],
|
||
minimum_size[0],
|
||
margin,
|
||
)
|
||
height = _bounded_dimension(
|
||
available.height(),
|
||
preferred_size[1],
|
||
minimum_size[1],
|
||
margin,
|
||
)
|
||
window.resize(width, height)
|
||
|
||
x = available.x() + max(0, (available.width() - width) // 2)
|
||
y = available.y() + max(0, (available.height() - height) // 2)
|
||
max_x = available.x() + max(0, available.width() - width)
|
||
max_y = available.y() + max(0, available.height() - height)
|
||
window.move(min(max(x, available.x()), max_x), min(max(y, available.y()), max_y))
|
||
|
||
|
||
class MainWindow(QMainWindow):
|
||
"""Main application window with the fixed workflow tabs."""
|
||
|
||
def __init__(
|
||
self,
|
||
db_path=None,
|
||
config=None,
|
||
config_path=None,
|
||
ai_models_path=None,
|
||
startup_status="",
|
||
subscription_policy=None,
|
||
):
|
||
super().__init__()
|
||
self._initial_window_fit_applied_after_show = False
|
||
self.config = appconfig.load_config(config_path or appconfig.CONFIG_PATH) if config is None else config
|
||
self.config_path = (
|
||
config_path
|
||
or self.config.get("config_path")
|
||
or appconfig.CONFIG_PATH
|
||
)
|
||
self.db_path = _database_path(db_path, self.config)
|
||
self.ai_models_path = (
|
||
ai_models_path
|
||
or self.config.get("ai_models_path")
|
||
or appconfig.ai_models_config_path(self.config)
|
||
)
|
||
self._client_policy = (
|
||
subscription_policy
|
||
if isinstance(subscription_policy, client_policy.ClientPolicy)
|
||
else client_policy.ClientPolicy(
|
||
policy_version=client_policy.POLICY_VERSION,
|
||
subscription_check_enabled=SUBSCRIPTION_CHECK_ENABLED,
|
||
subscription_enforcement_enabled=SUBSCRIPTION_ENFORCEMENT_ENABLED,
|
||
updated_at="",
|
||
source="compatibility",
|
||
)
|
||
)
|
||
self.setWindowTitle(display_name())
|
||
window_icon = app_icon()
|
||
if window_icon is not None:
|
||
self.setWindowIcon(window_icon)
|
||
_fit_and_center_window(self)
|
||
self.setStyleSheet(BUTTON_BASE_STYLE)
|
||
self._settings_tab_index = TAB_TITLES.index("设置")
|
||
self._last_tab_index = 0
|
||
self._reverting_tab_change = False
|
||
self._subscription_status = subscription.SubscriptionStatus(
|
||
subscription.STATUS_UNAVAILABLE
|
||
)
|
||
self._subscription_worker = None
|
||
self._subscription_thread = None
|
||
self._subscription_request_token = 0
|
||
self._subscription_closed = False
|
||
self._expired_subscription_notice_shown = False
|
||
self._subscription_notice_state = None
|
||
self._subscription_notice_states_seen = set()
|
||
self._activation_dialog = None
|
||
self._first_use_guide_shown = False
|
||
self.tabs = QTabWidget()
|
||
self.tabs.setObjectName("mainTabs")
|
||
self.tabs.setStyleSheet(TAB_STYLE)
|
||
self.tabs.currentChanged.connect(self._on_tab_changed)
|
||
for title in TAB_TITLES:
|
||
self.tabs.addTab(self._build_tab(title), title)
|
||
settings_tab = self._settings_tab()
|
||
if hasattr(settings_tab, "settingsSaved"):
|
||
settings_tab.settingsSaved.connect(self._on_settings_saved)
|
||
if hasattr(settings_tab, "subscriptionCheckRequested"):
|
||
settings_tab.subscriptionCheckRequested.connect(
|
||
self.begin_subscription_check
|
||
)
|
||
central = QWidget()
|
||
central_layout = QVBoxLayout(central)
|
||
central_layout.setContentsMargins(0, 0, 0, 0)
|
||
central_layout.setSpacing(0)
|
||
central_layout.addWidget(self.tabs, 1)
|
||
self.setCentralWidget(central)
|
||
self.show_status("就绪", level="muted")
|
||
if startup_status:
|
||
self.show_status(startup_status, level="success")
|
||
|
||
def showEvent(self, event):
|
||
super().showEvent(event)
|
||
if not self._initial_window_fit_applied_after_show:
|
||
_fit_and_center_window(self)
|
||
self._initial_window_fit_applied_after_show = True
|
||
|
||
def show_status(self, message, level="muted"):
|
||
color = _status_level_color(level)
|
||
self.statusBar().setStyleSheet(f"QStatusBar {{ color: {color}; }}")
|
||
self.statusBar().showMessage(str(message))
|
||
|
||
def _build_tab(self, title):
|
||
if title == "① 导入采集":
|
||
return CollectTab(
|
||
db_path=self.db_path,
|
||
config=self.config,
|
||
status_callback=self.show_status,
|
||
open_accounts_callback=lambda: self.open_accounts_tab(),
|
||
refresh_workflow_callback=lambda: self.refresh_task_tabs(),
|
||
)
|
||
if title == "② AI生成":
|
||
return GenerateTab(
|
||
db_path=self.db_path,
|
||
config=self.config,
|
||
config_path=self.config_path,
|
||
status_callback=self.show_status,
|
||
title_prompt_path=appconfig.title_prompt_path(self.config),
|
||
cover_prompts_dir=appconfig.cover_prompts_dir(self.config),
|
||
open_accounts_callback=lambda: self.open_accounts_tab(),
|
||
refresh_workflow_callback=lambda: self.refresh_task_tabs(),
|
||
subscription_preflight_callback=self.ensure_subscription_for_new_submit,
|
||
)
|
||
if title == "③ 更新蝦皮":
|
||
return ApplyTab(
|
||
db_path=self.db_path,
|
||
config=self.config,
|
||
status_callback=self.show_status,
|
||
open_accounts_callback=lambda: self.open_accounts_tab(),
|
||
open_settings_callback=lambda: self.open_settings_tab(),
|
||
refresh_workflow_callback=lambda: self.refresh_task_tabs(),
|
||
)
|
||
if title == "账号管理":
|
||
return AccountsTab(
|
||
db_path=self.db_path,
|
||
config=self.config,
|
||
status_callback=self.show_status,
|
||
)
|
||
if title == "设置":
|
||
tab = SettingsTab(
|
||
config=self.config,
|
||
config_path=self.config_path,
|
||
ai_models_path=self.ai_models_path,
|
||
status_callback=self.show_status,
|
||
)
|
||
if hasattr(tab, "set_subscription_policy_enabled"):
|
||
tab.set_subscription_policy_enabled(
|
||
self._client_policy.subscription_check_enabled
|
||
)
|
||
return tab
|
||
if title == "商品套图":
|
||
return ProductSuiteTab(
|
||
db_path=self.db_path,
|
||
config=self.config,
|
||
config_path=self.config_path,
|
||
status_callback=self.show_status,
|
||
subscription_preflight_callback=self.ensure_subscription_for_new_submit,
|
||
)
|
||
raise ValueError(f"未知主界面模块:{title}")
|
||
|
||
def refresh_task_tabs(self):
|
||
for index in range(self.tabs.count()):
|
||
widget = self.tabs.widget(index)
|
||
if hasattr(widget, "refresh_tasks"):
|
||
widget.refresh_tasks()
|
||
|
||
def _on_settings_saved(self, backend):
|
||
for index in range(self.tabs.count()):
|
||
widget = self.tabs.widget(index)
|
||
if hasattr(widget, "refresh_gateway_state"):
|
||
widget.refresh_gateway_state()
|
||
label = "自定义网关" if str(backend) == "direct" else "默认网关"
|
||
self.show_status("设置已保存,当前使用%s" % label, level="success")
|
||
self.begin_subscription_check()
|
||
|
||
@property
|
||
def subscription_status(self):
|
||
return self._subscription_status
|
||
|
||
@property
|
||
def subscription_policy(self):
|
||
return self._client_policy
|
||
|
||
def begin_subscription_check(self):
|
||
"""Refresh membership state without blocking the Qt GUI thread."""
|
||
|
||
self._set_membership_window_title()
|
||
if not self._client_policy.subscription_check_enabled:
|
||
self._set_product_access(True)
|
||
self._set_subscription_check_running(False)
|
||
self.show_status("服务端暂未启用会员检测", level="muted")
|
||
return
|
||
self._subscription_request_token += 1
|
||
token = self._subscription_request_token
|
||
if self._subscription_worker is not None:
|
||
self._subscription_worker.cancel()
|
||
self._set_product_access(
|
||
not self._client_policy.subscription_enforcement_enabled
|
||
)
|
||
self._set_subscription_check_running(True)
|
||
self.show_status("正在验证会员状态", level="info")
|
||
worker = SubscriptionCheckWorker(
|
||
config=self.config,
|
||
cmhub_config_path=self.config.get("cmhub_config_path"),
|
||
)
|
||
worker.finished.connect(
|
||
lambda payload, token=token: self._on_subscription_checked(
|
||
token,
|
||
payload,
|
||
)
|
||
)
|
||
worker.cancelled.connect(
|
||
lambda payload, token=token: self._on_subscription_checked(
|
||
token,
|
||
payload,
|
||
)
|
||
)
|
||
thread = run_worker(worker, thread_name="SubscriptionCheckWorker", start=False)
|
||
thread.finished.connect(lambda: self._forget_subscription_thread(thread))
|
||
self._subscription_worker = worker
|
||
self._subscription_thread = thread
|
||
thread.start()
|
||
|
||
def ensure_subscription_for_new_submit(self, action_name="生成"):
|
||
"""Return whether a new product request may start from the current UI."""
|
||
|
||
if (
|
||
not self._client_policy.subscription_check_enabled
|
||
or not self._client_policy.subscription_enforcement_enabled
|
||
):
|
||
return True
|
||
status = self._subscription_status
|
||
if status.allows_product_workflows:
|
||
return True
|
||
if self._subscription_thread is not None:
|
||
self.show_status("正在验证会员状态,请稍后再%s" % action_name, level="info")
|
||
return False
|
||
self.show_status("%s:%s" % (action_name, status.user_message), level="warning")
|
||
self.open_settings_tab()
|
||
return False
|
||
|
||
def _on_subscription_checked(self, token, payload):
|
||
if self._subscription_closed or token != self._subscription_request_token:
|
||
return
|
||
result = payload.get("subscription") if isinstance(payload, dict) else None
|
||
if not isinstance(result, subscription.SubscriptionStatus):
|
||
result = subscription.SubscriptionStatus(subscription.STATUS_UNAVAILABLE)
|
||
self._subscription_status = result
|
||
self._apply_subscription_status(result)
|
||
|
||
def _forget_subscription_thread(self, thread):
|
||
if self._subscription_thread is thread:
|
||
self._subscription_thread = None
|
||
self._subscription_worker = None
|
||
self._set_subscription_check_running(False)
|
||
|
||
def _apply_subscription_status(
|
||
self,
|
||
status,
|
||
*,
|
||
show_notice=True,
|
||
show_first_use_guide=True,
|
||
):
|
||
self._subscription_status = status
|
||
if status.allows_product_workflows:
|
||
self._expired_subscription_notice_shown = False
|
||
self._subscription_notice_state = None
|
||
self._subscription_notice_states_seen.clear()
|
||
if status.state == subscription.STATUS_ACTIVE:
|
||
expiry = subscription.format_expiry(status.expires_at)
|
||
self._set_membership_window_title(
|
||
status.account_name,
|
||
status.plan_name,
|
||
"有效至%s" % expiry,
|
||
)
|
||
elif status.state == subscription.STATUS_GRACE:
|
||
expiry = subscription.format_expiry(
|
||
status.grace_expires_at
|
||
) or subscription.format_expiry(status.expires_at)
|
||
self._set_membership_window_title(
|
||
status.account_name,
|
||
status.plan_name,
|
||
"宽限至%s" % expiry,
|
||
)
|
||
else:
|
||
self._set_membership_window_title()
|
||
|
||
access_allowed = (
|
||
status.allows_product_workflows
|
||
or not self._client_policy.subscription_enforcement_enabled
|
||
)
|
||
self._set_product_access(access_allowed)
|
||
if status.allows_product_workflows:
|
||
message = (
|
||
"会员状态已验证"
|
||
if self._client_policy.subscription_enforcement_enabled
|
||
else "会员状态已验证,当前处于观察模式"
|
||
)
|
||
self.show_status(message, level="success")
|
||
if (
|
||
self._client_policy.subscription_enforcement_enabled
|
||
and show_notice
|
||
):
|
||
self._show_subscription_notice_once(status)
|
||
if show_first_use_guide:
|
||
QTimer.singleShot(0, self._show_first_use_guide_if_pending)
|
||
return
|
||
if not self._client_policy.subscription_enforcement_enabled:
|
||
self.show_status(
|
||
"订阅观察:%s,当前不影响使用" % status.user_message,
|
||
level=self._subscription_level(status),
|
||
)
|
||
return
|
||
self.show_status(status.user_message, level=self._subscription_level(status))
|
||
if status.state == subscription.STATUS_NOT_CONFIGURED:
|
||
self.show_status("请先绑定会员账号", level="warning")
|
||
self._show_membership_activation()
|
||
return
|
||
if status.state == subscription.STATUS_KEY_INVALID:
|
||
self.open_settings_tab()
|
||
self._show_membership_activation()
|
||
if self._activation_dialog is not None:
|
||
self._activation_dialog.focus_api_key()
|
||
return
|
||
self.open_settings_tab()
|
||
if status.state == subscription.STATUS_EXPIRED:
|
||
self._show_expired_subscription_notice_once(status)
|
||
return
|
||
self._show_subscription_state_notice_once(status)
|
||
|
||
def _show_membership_activation(self):
|
||
if self._activation_dialog is not None:
|
||
self._activation_dialog.raise_()
|
||
self._activation_dialog.activateWindow()
|
||
return
|
||
dialog = MembershipActivationDialog(self)
|
||
self._activation_dialog = dialog
|
||
dialog.validationRequested.connect(self._begin_activation_validation)
|
||
dialog.memberCenterRequested.connect(self._open_activation_member_center)
|
||
dialog.finished.connect(
|
||
lambda result, current=dialog: self._on_activation_dialog_finished(
|
||
current,
|
||
result,
|
||
)
|
||
)
|
||
dialog.open()
|
||
|
||
def _begin_activation_validation(self, api_key):
|
||
dialog = self._activation_dialog
|
||
key = str(api_key or "").strip()
|
||
if dialog is None or not key:
|
||
return
|
||
self._subscription_request_token += 1
|
||
token = self._subscription_request_token
|
||
if self._subscription_worker is not None:
|
||
self._subscription_worker.cancel()
|
||
self._set_product_access(False)
|
||
self._set_subscription_check_running(True)
|
||
dialog.set_validating(True)
|
||
self.show_status("正在验证会员账号", level="info")
|
||
worker = SubscriptionCheckWorker(
|
||
config=self.config,
|
||
cmhub_config_path=self.config.get("cmhub_config_path"),
|
||
base_url=appconfig.cmhub_config(self.config).get("base_url"),
|
||
api_key=key,
|
||
)
|
||
worker.finished.connect(
|
||
lambda payload, token=token, key=key: self._on_activation_checked(
|
||
token,
|
||
key,
|
||
payload,
|
||
)
|
||
)
|
||
worker.cancelled.connect(
|
||
lambda payload, token=token, key=key: self._on_activation_checked(
|
||
token,
|
||
key,
|
||
payload,
|
||
)
|
||
)
|
||
thread = run_worker(worker, thread_name="MembershipActivationWorker", start=False)
|
||
thread.finished.connect(lambda: self._forget_subscription_thread(thread))
|
||
self._subscription_worker = worker
|
||
self._subscription_thread = thread
|
||
thread.start()
|
||
|
||
def _on_activation_checked(self, token, api_key, payload):
|
||
if self._subscription_closed or token != self._subscription_request_token:
|
||
return
|
||
dialog = self._activation_dialog
|
||
if dialog is None:
|
||
return
|
||
dialog.set_validating(False)
|
||
status = payload.get("subscription") if isinstance(payload, dict) else None
|
||
if not isinstance(status, subscription.SubscriptionStatus):
|
||
status = subscription.SubscriptionStatus(subscription.STATUS_UNAVAILABLE)
|
||
self._subscription_status = status
|
||
|
||
if not status.credentials_accepted:
|
||
self._set_product_access(False)
|
||
self._set_membership_window_title()
|
||
if status.state == subscription.STATUS_KEY_INVALID:
|
||
message = "会员 API Key 无效,请检查后重新输入。"
|
||
dialog.focus_api_key()
|
||
elif status.state == subscription.STATUS_NOT_CONFIGURED:
|
||
message = "请输入有效的会员 API Key。"
|
||
dialog.focus_api_key()
|
||
else:
|
||
message = "暂时无法验证会员账号,请检查网络后重试。"
|
||
dialog.set_status(message, level="danger")
|
||
self.show_status(message, level="warning")
|
||
return
|
||
|
||
try:
|
||
appconfig.save_cmhub_config(
|
||
{"api_key": str(api_key or "").strip()},
|
||
path=self.config.get("cmhub_config_path")
|
||
or appconfig.cmhub_config_file_path(self.config),
|
||
)
|
||
except (OSError, appconfig.ConfigError):
|
||
message = "会员账号已验证,但 API Key 保存失败,请检查数据目录后重试。"
|
||
dialog.set_status(message, level="danger")
|
||
self.show_status(message, level="danger")
|
||
return
|
||
|
||
settings_tab = self._settings_tab()
|
||
if hasattr(settings_tab, "refresh_default_gateway_credentials"):
|
||
settings_tab.refresh_default_gateway_credentials()
|
||
|
||
if not status.allows_product_workflows:
|
||
self._set_product_access(False)
|
||
self._set_membership_window_title()
|
||
message = "%s。请前往会员中心处理后重新验证。" % status.user_message
|
||
dialog.set_status(message, level="danger")
|
||
self.show_status(status.user_message, level=self._subscription_level(status))
|
||
return
|
||
|
||
self._save_first_use_state("pending")
|
||
if status.notice_id:
|
||
self._save_subscription_notice_id(status.notice_id)
|
||
self._apply_subscription_status(
|
||
status,
|
||
show_notice=False,
|
||
show_first_use_guide=False,
|
||
)
|
||
dialog.set_status("会员账号验证成功,正在进入工作区。", level="success")
|
||
dialog.accept_activation()
|
||
QTimer.singleShot(0, self._show_first_use_guide_if_pending)
|
||
|
||
def _on_activation_dialog_finished(self, dialog, result):
|
||
if self._activation_dialog is dialog:
|
||
self._activation_dialog = None
|
||
dialog.deleteLater()
|
||
if (
|
||
result != QDialog.Accepted
|
||
and not self._subscription_closed
|
||
and self._client_policy.subscription_enforcement_enabled
|
||
and not self._subscription_status.allows_product_workflows
|
||
):
|
||
self.close()
|
||
|
||
def _open_activation_member_center(self):
|
||
manage_url = str(self._subscription_status.manage_url or "").strip()
|
||
if not manage_url:
|
||
manage_url = subscription.safe_manage_url(
|
||
appconfig.DEFAULT_MEMBER_CENTER_URL,
|
||
appconfig.DEFAULT_CMHUB_BASE_URL,
|
||
)
|
||
if not manage_url:
|
||
message = "会员中心地址当前不可用,请稍后重试。"
|
||
if self._activation_dialog is not None:
|
||
self._activation_dialog.set_status(message, level="danger")
|
||
self.show_status(message, level="warning")
|
||
return
|
||
opened = QDesktopServices.openUrl(QUrl(manage_url))
|
||
if opened is False:
|
||
message = "无法打开会员中心,请检查系统默认浏览器后重试。"
|
||
if self._activation_dialog is not None:
|
||
self._activation_dialog.set_status(message, level="danger")
|
||
self.show_status(message, level="warning")
|
||
|
||
def _show_first_use_guide_if_pending(self):
|
||
if self._first_use_guide_shown:
|
||
return
|
||
if appconfig.first_use_guide_state(self.config) != "pending":
|
||
return
|
||
if not self._subscription_status.allows_product_workflows:
|
||
return
|
||
self._first_use_guide_shown = True
|
||
|
||
box = QMessageBox(self)
|
||
box.setIcon(QMessageBox.Information)
|
||
box.setWindowTitle("首次使用指引")
|
||
box.setText(
|
||
"接下来按以下顺序开始使用:\n\n"
|
||
"1. 添加蝦皮店铺账号\n"
|
||
"2. 启动 Chrome 并人工完成登录\n"
|
||
"3. 导入商品 Excel\n"
|
||
"4. 开始采集、AI 生成和更新蝦皮"
|
||
)
|
||
start_button = box.addButton("开始配置店铺", QMessageBox.AcceptRole)
|
||
later_button = box.addButton("稍后提醒", QMessageBox.RejectRole)
|
||
dismiss_button = box.addButton("不再提示", QMessageBox.ActionRole)
|
||
box.setDefaultButton(start_button)
|
||
box.exec()
|
||
clicked = box.clickedButton()
|
||
if clicked is start_button:
|
||
self._save_first_use_state("completed")
|
||
self.open_accounts_tab()
|
||
elif clicked is dismiss_button:
|
||
self._save_first_use_state("dismissed")
|
||
elif clicked is later_button:
|
||
self.show_status("可稍后从账号管理开始配置店铺", level="muted")
|
||
|
||
def _save_first_use_state(self, state):
|
||
try:
|
||
saved = appconfig.save_first_use_guide_state(
|
||
state,
|
||
path=self.config_path,
|
||
)
|
||
except (OSError, appconfig.ConfigError):
|
||
self.show_status("首次使用引导状态保存失败", level="warning")
|
||
return False
|
||
self._replace_runtime_config(saved)
|
||
return True
|
||
|
||
def _save_subscription_notice_id(self, notice_id):
|
||
try:
|
||
saved = appconfig.save_subscription_notice_id(
|
||
notice_id,
|
||
path=self.config_path,
|
||
)
|
||
except (OSError, appconfig.ConfigError):
|
||
return False
|
||
self._replace_runtime_config(saved)
|
||
return True
|
||
|
||
def _replace_runtime_config(self, saved):
|
||
self.config.clear()
|
||
self.config.update(saved)
|
||
|
||
@staticmethod
|
||
def _subscription_level(status):
|
||
if status.state in {subscription.STATUS_UNAVAILABLE, subscription.STATUS_KEY_INVALID}:
|
||
return "warning"
|
||
return "danger"
|
||
|
||
def _set_membership_window_title(
|
||
self,
|
||
account_name="",
|
||
plan_name="",
|
||
expiry_text="",
|
||
):
|
||
account = " ".join(str(account_name or "").split())
|
||
plan = " ".join(str(plan_name or "").split())
|
||
expiry = " ".join(str(expiry_text or "").split())
|
||
if account and plan and expiry:
|
||
self.setWindowTitle(
|
||
"%s | %s · %s · %s" % (display_name(), account, plan, expiry)
|
||
)
|
||
return
|
||
self.setWindowTitle(display_name())
|
||
|
||
def _set_product_access(self, enabled):
|
||
for index, title in enumerate(TAB_TITLES):
|
||
self.tabs.setTabEnabled(index, bool(enabled) or title == "设置")
|
||
|
||
def _set_subscription_check_running(self, running):
|
||
settings_tab = self._settings_tab()
|
||
if hasattr(settings_tab, "set_subscription_check_running"):
|
||
settings_tab.set_subscription_check_running(running)
|
||
|
||
def _show_expired_subscription_notice_once(self, status):
|
||
self._show_subscription_state_notice_once(status)
|
||
|
||
def _show_subscription_state_notice_once(self, status):
|
||
state = status.state
|
||
notice_state = (
|
||
subscription.STATUS_UNAVAILABLE
|
||
if state == subscription.STATUS_LEGACY
|
||
else state
|
||
)
|
||
if notice_state in self._subscription_notice_states_seen:
|
||
return
|
||
self._subscription_notice_states_seen.add(notice_state)
|
||
self._subscription_notice_state = notice_state
|
||
self._expired_subscription_notice_shown = (
|
||
notice_state == subscription.STATUS_EXPIRED
|
||
)
|
||
|
||
titles = {
|
||
subscription.STATUS_REQUIRED: "尚未开通会员套餐",
|
||
subscription.STATUS_EXPIRED: "会员套餐已过期",
|
||
subscription.STATUS_REVOKED: "会员套餐已失效",
|
||
subscription.STATUS_ACCOUNT_DISABLED: "会员账号当前不可用",
|
||
subscription.STATUS_UNAVAILABLE: "暂时无法验证会员状态",
|
||
}
|
||
messages = {
|
||
subscription.STATUS_REQUIRED: (
|
||
"当前账号尚未开通蝦皮圈会员套餐,业务功能已暂停。"
|
||
"请前往会员中心选择套餐,完成后重新检测会员状态。"
|
||
),
|
||
subscription.STATUS_EXPIRED: (
|
||
"当前账号的蝦皮圈会员已到期,业务功能已暂停。"
|
||
"请前往会员中心续费或更换套餐,完成后重新检测会员状态。"
|
||
),
|
||
subscription.STATUS_REVOKED: (
|
||
"当前账号的蝦皮圈会员套餐已失效,业务功能已暂停。"
|
||
"请前往会员中心查看套餐状态,处理后重新检测。"
|
||
),
|
||
subscription.STATUS_ACCOUNT_DISABLED: (
|
||
"当前会员账号暂时不可用,业务功能已暂停。"
|
||
"请前往会员中心查看账号状态或联系管理员处理。"
|
||
),
|
||
subscription.STATUS_UNAVAILABLE: (
|
||
"当前暂时无法验证会员状态,业务功能已暂停。"
|
||
"这不代表账号未付费,请检查网络或服务状态后重新检测。"
|
||
),
|
||
}
|
||
box = QMessageBox(self)
|
||
box.setIcon(QMessageBox.Warning)
|
||
box.setWindowTitle(titles.get(notice_state, "暂时无法验证会员状态"))
|
||
text = messages.get(
|
||
notice_state,
|
||
messages[subscription.STATUS_UNAVAILABLE],
|
||
)
|
||
|
||
manage_button = None
|
||
settings_button = None
|
||
manage_url = str(status.manage_url or "").strip()
|
||
if notice_state != subscription.STATUS_UNAVAILABLE:
|
||
manage_button = box.addButton(
|
||
"前往会员中心",
|
||
QMessageBox.ActionRole,
|
||
)
|
||
manage_button.setEnabled(bool(manage_url))
|
||
if manage_url:
|
||
box.setDefaultButton(manage_button)
|
||
else:
|
||
manage_button.setToolTip("会员中心地址当前不可用")
|
||
text += "\n\n会员中心地址当前不可用,请稍后重试。"
|
||
retry_button = box.addButton("重新检测", QMessageBox.AcceptRole)
|
||
if notice_state == subscription.STATUS_UNAVAILABLE:
|
||
settings_button = box.addButton("打开设置", QMessageBox.ActionRole)
|
||
box.setDefaultButton(retry_button)
|
||
exit_button = box.addButton("退出程序", QMessageBox.DestructiveRole)
|
||
box.setText(text)
|
||
box.exec()
|
||
|
||
clicked = box.clickedButton()
|
||
if manage_button is not None and clicked is manage_button and manage_url:
|
||
opened = QDesktopServices.openUrl(QUrl(manage_url))
|
||
if opened is False:
|
||
self.show_status(
|
||
"无法打开会员中心,请检查系统默认浏览器后重试",
|
||
level="warning",
|
||
)
|
||
elif clicked is retry_button:
|
||
self.begin_subscription_check()
|
||
elif settings_button is not None and clicked is settings_button:
|
||
self.open_settings_tab()
|
||
elif clicked is exit_button:
|
||
self.close()
|
||
|
||
def _show_subscription_notice_once(self, status):
|
||
notice_id = str(status.notice_id or "").strip()
|
||
if not notice_id or notice_id == appconfig.subscription_notice_id(self.config):
|
||
return
|
||
box = QMessageBox(self)
|
||
box.setIcon(QMessageBox.Information)
|
||
box.setWindowTitle("已启用会员订阅")
|
||
expiry = subscription.format_expiry(
|
||
status.grace_expires_at if status.state == subscription.STATUS_GRACE else status.expires_at
|
||
)
|
||
text = "已启用蝦皮圈会员订阅服务。当前账号:%s;套餐:%s。" % (
|
||
status.account_name,
|
||
status.plan_name,
|
||
)
|
||
if expiry:
|
||
text += "当前有效至%s。" % expiry
|
||
text += "请在到期前前往线上会员中心选择或续费套餐。"
|
||
box.setText(text)
|
||
manage_button = None
|
||
if status.manage_url:
|
||
manage_button = box.addButton("前往会员中心", QMessageBox.ActionRole)
|
||
close_button = box.addButton("关闭", QMessageBox.AcceptRole)
|
||
box.setDefaultButton(close_button)
|
||
box.exec()
|
||
if manage_button is not None and box.clickedButton() is manage_button:
|
||
QDesktopServices.openUrl(QUrl(status.manage_url))
|
||
self._save_subscription_notice_id(notice_id)
|
||
|
||
def _on_tab_changed(self, index):
|
||
if self._reverting_tab_change:
|
||
self._last_tab_index = index
|
||
self.show_status(f"当前:{self.tabs.tabText(index)}", level="muted")
|
||
return
|
||
previous = self._last_tab_index
|
||
if previous == self._settings_tab_index and index != previous:
|
||
if not self._confirm_leave_settings_tab():
|
||
self._reverting_tab_change = True
|
||
try:
|
||
self.tabs.setCurrentIndex(previous)
|
||
finally:
|
||
self._reverting_tab_change = False
|
||
self._last_tab_index = previous
|
||
self.show_status(f"当前:{self.tabs.tabText(previous)}", level="muted")
|
||
return
|
||
self._last_tab_index = index
|
||
self.show_status(f"当前:{self.tabs.tabText(index)}", level="muted")
|
||
|
||
def _settings_tab(self):
|
||
return self.tabs.widget(self._settings_tab_index)
|
||
|
||
def _confirm_leave_settings_tab(self):
|
||
settings_tab = self._settings_tab()
|
||
if not hasattr(settings_tab, "is_dirty") or not settings_tab.is_dirty():
|
||
return True
|
||
box = QMessageBox(self)
|
||
box.setWindowTitle("未保存更改")
|
||
box.setText("设置有未保存更改。要先保存再离开吗?")
|
||
save_button = box.addButton("保存", QMessageBox.AcceptRole)
|
||
discard_button = box.addButton("放弃", QMessageBox.DestructiveRole)
|
||
box.addButton("取消", QMessageBox.RejectRole)
|
||
box.setDefaultButton(save_button)
|
||
box.exec()
|
||
clicked = box.clickedButton()
|
||
if clicked is save_button:
|
||
return bool(settings_tab.save_app_settings())
|
||
if clicked is discard_button:
|
||
return bool(settings_tab.discard_unsaved_changes())
|
||
return False
|
||
|
||
def closeEvent(self, event):
|
||
if self._confirm_leave_settings_tab():
|
||
self._subscription_closed = True
|
||
if self._subscription_worker is not None:
|
||
self._subscription_worker.cancel()
|
||
event.accept()
|
||
return
|
||
event.ignore()
|
||
|
||
def open_accounts_tab(self):
|
||
self.tabs.setCurrentIndex(TAB_TITLES.index("账号管理"))
|
||
|
||
def open_settings_tab(self):
|
||
self.tabs.setCurrentIndex(TAB_TITLES.index("设置"))
|