mirror of
https://github.com/moesnow/March7thAssistant.git
synced 2026-08-02 18:24:16 +08:00
- 新增 hotkey_toggle_autoplot 配置项,默认快捷键 F9 - 新增 hotkey_toggle_autoplot_enable 配置项,默认关闭 - 持久化自动对话选项(跳过/选择/战斗检测/短信) - 工具箱自动对话卡片展开后显示快捷键提示 Closes #1110
909 lines
36 KiB
Python
909 lines
36 KiB
Python
from PySide6.QtCore import Qt, QSize, QPoint, QFileSystemWatcher, Signal, QObject
|
||
from PySide6.QtGui import QIcon, QAction
|
||
from PySide6.QtWidgets import QApplication, QSystemTrayIcon, QLabel
|
||
|
||
from qfluentwidgets import NavigationItemPosition, MSFluentWindow, SplashScreen, setThemeColor, NavigationBarPushButton, setTheme, Theme, themeColor, qconfig
|
||
from qfluentwidgets import FluentIcon as FIF
|
||
from qfluentwidgets import InfoBar, InfoBarPosition, SystemTrayMenu
|
||
from app.tools.game_starter import GameStartStatus, GameLaunchThread
|
||
|
||
from .home_interface import HomeInterface
|
||
from .help_interface import HelpInterface
|
||
# from .changelog_interface import ChangelogInterface
|
||
from .warp_interface import WarpInterface
|
||
from .tools_interface import ToolsInterface
|
||
from .workflow_interface import WorkflowInterface
|
||
from .setting_interface import SettingInterface
|
||
from .log_interface import LogInterface
|
||
from .common.signal_bus import signalBus
|
||
|
||
from .card.messagebox_custom import MessageBoxSupport
|
||
from .tools.check_update import checkUpdate
|
||
from .tools.check_theme_change import checkThemeChange
|
||
from .tools.announcement import checkAnnouncement
|
||
from .tools.disclaimer import disclaimer
|
||
|
||
from module.config import cfg
|
||
from module.logger import log
|
||
from module.game import get_game_controller
|
||
from module.localization import tr
|
||
import base64
|
||
import os
|
||
import sys
|
||
|
||
|
||
class ConfigWatcher(QObject):
|
||
"""配置文件监视器"""
|
||
config_changed = Signal()
|
||
|
||
def __init__(self, config_path, parent=None):
|
||
super().__init__(parent)
|
||
self.config_path = config_path
|
||
self.watcher = QFileSystemWatcher()
|
||
self.debounce_timer = None
|
||
|
||
# 监视配置
|
||
if os.path.exists(self.config_path):
|
||
self.watcher.addPath(self.config_path)
|
||
self.watcher.fileChanged.connect(self._on_config_changed)
|
||
|
||
def _on_config_changed(self, path):
|
||
"""检测到文件变化,延迟处理避免频繁触发"""
|
||
from PySide6.QtCore import QTimer
|
||
|
||
# 清除之前的定时器
|
||
if self.debounce_timer:
|
||
self.debounce_timer.stop()
|
||
self.debounce_timer.deleteLater()
|
||
|
||
# 创建新的定时器,延迟1秒处理(避免文件写入过程中多次触发)
|
||
self.debounce_timer = QTimer()
|
||
self.debounce_timer.setSingleShot(True)
|
||
self.debounce_timer.timeout.connect(self._emit_change)
|
||
self.debounce_timer.start(1000)
|
||
|
||
def _emit_change(self):
|
||
"""检查文件是否真的改变,然后发送信号"""
|
||
if os.path.exists(self.config_path) and cfg.is_config_changed():
|
||
self.config_changed.emit()
|
||
|
||
|
||
class ClickableLabel(QLabel):
|
||
clicked = Signal()
|
||
|
||
def mousePressEvent(self, event):
|
||
if event.button() == Qt.MouseButton.LeftButton:
|
||
self.clicked.emit()
|
||
event.accept()
|
||
return
|
||
|
||
super().mousePressEvent(event)
|
||
|
||
|
||
class MainWindow(MSFluentWindow):
|
||
def __init__(self, task=None, exit_on_complete=False):
|
||
super().__init__()
|
||
self.startup_task = task # 保存启动时要执行的任务
|
||
self.exit_on_complete = exit_on_complete # 任务完成后是否退出
|
||
self.detected_update_version = None
|
||
self.updateVersionBadge = None
|
||
qconfig.themeChanged.connect(self._on_theme_changed)
|
||
|
||
self.initWindow()
|
||
|
||
self.initInterface()
|
||
self.initNavigation()
|
||
self.initSystemTray()
|
||
|
||
# 初始化配置文件监视器
|
||
self.config_watcher = ConfigWatcher(os.path.abspath(cfg.config_path), self)
|
||
self.config_watcher.config_changed.connect(self._on_config_file_changed)
|
||
|
||
# 如果有启动任务,延迟执行
|
||
if self.startup_task:
|
||
from PySide6.QtCore import QTimer
|
||
QTimer.singleShot(1000, self._executeStartupTask)
|
||
else:
|
||
# 检查更新
|
||
checkUpdate(self, flag=True)
|
||
checkAnnouncement(self)
|
||
|
||
def _executeStartupTask(self):
|
||
"""执行启动时指定的任务"""
|
||
if self.startup_task:
|
||
from tasks.base.tasks import start_task
|
||
start_task(self.startup_task)
|
||
|
||
def initWindow(self):
|
||
# 开启 “在标题栏和窗口边框上显示强调色” 后,会导致窗口顶部出现异色横条 bug 已经修复
|
||
# https://github.com/zhiyiYo/PyQt-Frameless-Window/pull/186
|
||
# 要求 PySideSix-Frameless-Window>=0.7.0
|
||
# self.setMicaEffectEnabled(False)
|
||
|
||
setThemeColor('#f18cb9', lazy=True)
|
||
setTheme(Theme.AUTO, lazy=True)
|
||
|
||
# 禁用最大化
|
||
# self.titleBar.maxBtn.setHidden(True)
|
||
# self.titleBar.maxBtn.setDisabled(True)
|
||
# self.titleBar.setDoubleClickEnabled(False)
|
||
# self.setResizeEnabled(False)
|
||
|
||
# self.setWindowFlags(Qt.WindowCloseButtonHint)
|
||
# self.setWindowFlags(Qt.WindowMinimizeButtonHint | Qt.WindowCloseButtonHint)
|
||
|
||
# 设置最小尺寸
|
||
min_width = 960
|
||
min_height = 640
|
||
self.setMinimumWidth(min_width)
|
||
self.setMinimumHeight(min_height)
|
||
|
||
window_memory = cfg.get_value('window_memory', 'size')
|
||
# 从配置文件读取窗口尺寸,确保不低于最小值
|
||
if window_memory in ('size', 'size_and_position'):
|
||
saved_width = cfg.get_value('window_width', min_width)
|
||
saved_height = cfg.get_value('window_height', min_height)
|
||
window_width = max(saved_width, min_width)
|
||
window_height = max(saved_height, min_height)
|
||
self.resize(window_width, window_height)
|
||
else:
|
||
self.resize(min_width, min_height)
|
||
|
||
self.setWindowIcon(QIcon('./assets/logo/March7th.ico'))
|
||
self.setWindowTitle("March7th Assistant")
|
||
# 分离系统窗口标题与应用内标题栏文本
|
||
self._refreshWindowTitleBar()
|
||
|
||
# 创建启动画面
|
||
self.splashScreen = SplashScreen(self.windowIcon(), self)
|
||
self.splashScreen.setIconSize(QSize(128, 128))
|
||
self.splashScreen.titleBar.maxBtn.setHidden(True)
|
||
self.splashScreen.raise_()
|
||
|
||
primary_screen = QApplication.primaryScreen().availableGeometry()
|
||
w, h = primary_screen.width(), primary_screen.height()
|
||
|
||
saved_x = cfg.get_value('window_x', None)
|
||
saved_y = cfg.get_value('window_y', None)
|
||
|
||
if window_memory in ('position', 'size_and_position') and saved_x is not None and saved_y is not None:
|
||
# 尝试找到保存位置所在的屏幕,找不到则回退到主屏幕
|
||
target_screen = QApplication.screenAt(QPoint(int(saved_x), int(saved_y)))
|
||
screen = target_screen.availableGeometry() if target_screen else primary_screen
|
||
max_x = max(screen.left(), screen.right() - self.width())
|
||
max_y = max(screen.top(), screen.bottom() - self.height())
|
||
restored_x = max(screen.left(), min(int(saved_x), max_x))
|
||
restored_y = max(screen.top(), min(int(saved_y), max_y))
|
||
self.move(restored_x, restored_y)
|
||
else:
|
||
self.move(w // 2 - self.width() // 2, h // 2 - self.height() // 2)
|
||
|
||
# 根据配置决定窗口显示方式
|
||
if window_memory in ('size', 'size_and_position') and cfg.get_value('window_maximized', False):
|
||
self.showMaximized()
|
||
else:
|
||
self.show()
|
||
|
||
QApplication.processEvents()
|
||
|
||
def _baseTitleBarText(self):
|
||
return f"March7th Assistant {cfg.version}"
|
||
|
||
def _ensureUpdateVersionBadge(self):
|
||
if self.updateVersionBadge is not None:
|
||
return self.updateVersionBadge
|
||
|
||
if not hasattr(self, 'titleBar') or not hasattr(self.titleBar, 'hBoxLayout'):
|
||
return None
|
||
|
||
badge = ClickableLabel(self.titleBar)
|
||
badge.hide()
|
||
badge.setStyleSheet(f"color: {themeColor().name()}; font-weight: 700;")
|
||
badge.setCursor(Qt.CursorShape.PointingHandCursor)
|
||
badge.clicked.connect(self._on_update_version_badge_clicked)
|
||
try:
|
||
self.titleBar.hBoxLayout.insertWidget(4, badge, 0, Qt.AlignmentFlag.AlignVCenter)
|
||
except Exception:
|
||
self.titleBar.hBoxLayout.addWidget(badge, 0, Qt.AlignmentFlag.AlignVCenter)
|
||
self.updateVersionBadge = badge
|
||
return badge
|
||
|
||
def _refreshWindowTitleBar(self):
|
||
if hasattr(self, 'titleBar') and hasattr(self.titleBar, 'setTitle'):
|
||
self.titleBar.setTitle(self._baseTitleBarText())
|
||
|
||
badge = self._ensureUpdateVersionBadge()
|
||
if badge is None:
|
||
return
|
||
|
||
badge.setStyleSheet(f"color: {themeColor().name()}; font-weight: 700;")
|
||
if self.detected_update_version:
|
||
badge.setText(tr('检测到新版本:{version}').format(version=self.detected_update_version))
|
||
badge.show()
|
||
else:
|
||
badge.hide()
|
||
|
||
def setDetectedUpdateVersion(self, version: str | None):
|
||
self.detected_update_version = version or None
|
||
self._refreshWindowTitleBar()
|
||
if hasattr(self, 'logInterface') and self.logInterface:
|
||
try:
|
||
self.logInterface.setDetectedUpdateVersion(self.detected_update_version)
|
||
except Exception:
|
||
pass
|
||
|
||
def _on_theme_changed(self):
|
||
self._refreshWindowTitleBar()
|
||
|
||
def _on_update_version_badge_clicked(self):
|
||
if self.detected_update_version:
|
||
checkUpdate(self)
|
||
|
||
def initInterface(self):
|
||
self.homeInterface = HomeInterface(self)
|
||
self.helpInterface = HelpInterface(self)
|
||
# self.changelogInterface = ChangelogInterface(self)
|
||
self.warpInterface = WarpInterface(self)
|
||
self.toolsInterface = ToolsInterface(self)
|
||
self.workflowInterface = WorkflowInterface(self)
|
||
self.logInterface = LogInterface(self)
|
||
self.settingInterface = SettingInterface(self)
|
||
|
||
# 连接任务启动信号
|
||
signalBus.startTaskSignal.connect(self._onStartTask)
|
||
# 连接热键配置改变信号
|
||
signalBus.hotkeyChangedSignal.connect(self._onHotkeyChanged)
|
||
# 连接 UI 语言改变信号(用于提示重启生效)
|
||
signalBus.uiLanguageChanged.connect(self._on_ui_language_changed)
|
||
# 连接任务完成信号
|
||
self.logInterface.taskFinished.connect(self._onTaskFinished)
|
||
# 连接自动对话切换信号
|
||
self.logInterface.autoplotToggleRequested.connect(self._onAutoplotToggleRequested)
|
||
|
||
def initNavigation(self):
|
||
self.addSubInterface(self.homeInterface, FIF.HOME, tr('主页'))
|
||
self.addSubInterface(self.helpInterface, FIF.BOOK_SHELF, tr('帮助'))
|
||
# self.addSubInterface(self.changelogInterface, FIF.UPDATE, '更新日志')
|
||
self.addSubInterface(self.warpInterface, FIF.SHARE, tr('抽卡记录'))
|
||
self.addSubInterface(self.toolsInterface, FIF.DEVELOPER_TOOLS, tr('工具箱'))
|
||
self.addSubInterface(self.workflowInterface, FIF.CODE, tr('流程编排'))
|
||
|
||
self.navigationInterface.addWidget(
|
||
'startGameButton',
|
||
NavigationBarPushButton(FIF.PLAY, tr('启动游戏'), isSelectable=False),
|
||
self.startGame,
|
||
NavigationItemPosition.BOTTOM)
|
||
|
||
self.addSubInterface(self.logInterface, FIF.COMMAND_PROMPT, tr('日志'), position=NavigationItemPosition.BOTTOM)
|
||
|
||
# self.navigationInterface.addWidget(
|
||
# 'refreshButton',
|
||
# NavigationBarPushButton(FIF.SYNC, '刷新', isSelectable=False),
|
||
# self._on_config_file_changed,
|
||
# NavigationItemPosition.BOTTOM)
|
||
|
||
# self.navigationInterface.addWidget(
|
||
# 'themeButton',
|
||
# NavigationBarPushButton(FIF.BRUSH, '主题', isSelectable=False),
|
||
# lambda: toggleTheme(lazy=True),
|
||
# NavigationItemPosition.BOTTOM)
|
||
|
||
self.navigationInterface.addWidget(
|
||
'avatar',
|
||
NavigationBarPushButton(FIF.HEART, tr('赞赏'), isSelectable=False),
|
||
lambda: MessageBoxSupport(
|
||
tr('支持作者🥰'),
|
||
tr('此程序为免费开源项目,如果你付了钱请立刻退款\n如果喜欢本项目,可以微信赞赏送作者一杯咖啡☕\n您的支持就是作者开发和维护项目的动力🚀'),
|
||
'./assets/app/images/sponsor.jpg',
|
||
self
|
||
).exec(),
|
||
NavigationItemPosition.BOTTOM
|
||
)
|
||
|
||
self.addSubInterface(self.settingInterface, FIF.SETTING, tr('设置'), position=NavigationItemPosition.BOTTOM)
|
||
|
||
self.splashScreen.finish()
|
||
self.themeListener = checkThemeChange(self)
|
||
|
||
if not cfg.get_value(base64.b64decode("YXV0b191cGRhdGU=").decode("utf-8")):
|
||
disclaimer(self)
|
||
|
||
def initSystemTray(self):
|
||
"""初始化系统托盘"""
|
||
self.tray_icon = QSystemTrayIcon(self)
|
||
self.tray_icon.setIcon(QIcon('./assets/logo/March7th.ico'))
|
||
self.tray_icon.setToolTip('March7th Assistant')
|
||
|
||
# 创建托盘菜单
|
||
tray_menu = SystemTrayMenu(parent=self)
|
||
tray_menu.aboutToShow.connect(self._on_tray_menu_about_to_show)
|
||
|
||
# 显示主界面
|
||
show_action = QAction(tr('显示主界面'), self)
|
||
show_action.triggered.connect(self._show_main_window)
|
||
tray_menu.addAction(show_action)
|
||
|
||
# 完整运行
|
||
run_action = QAction(tr('完整运行'), self)
|
||
run_action.triggered.connect(self.startFullTask)
|
||
tray_menu.addAction(run_action)
|
||
|
||
tray_menu.addSeparator()
|
||
|
||
# 打开设置界面
|
||
setting_action = QAction(tr('设置'), self)
|
||
|
||
def _open_settings():
|
||
try:
|
||
self.showNormal()
|
||
self.activateWindow()
|
||
if hasattr(self, 'settingInterface'):
|
||
self.switchTo(self.settingInterface)
|
||
except Exception:
|
||
pass
|
||
setting_action.triggered.connect(_open_settings)
|
||
tray_menu.addAction(setting_action)
|
||
|
||
# 退出程序
|
||
quit_action = QAction(tr('退出'), self)
|
||
quit_action.triggered.connect(self.quitApp)
|
||
tray_menu.addAction(quit_action)
|
||
|
||
self.tray_icon.setContextMenu(tray_menu)
|
||
if sys.platform == 'win32':
|
||
self.tray_icon.activated.connect(self.onTrayIconActivated)
|
||
self.tray_icon.show()
|
||
|
||
def _show_main_window(self):
|
||
"""显示主界面,macOS 下确保窗口置顶"""
|
||
self.showNormal()
|
||
try:
|
||
if sys.platform == 'darwin':
|
||
self.raise_()
|
||
self.activateWindow()
|
||
QApplication.setActiveWindow(self)
|
||
else:
|
||
self.activateWindow()
|
||
except Exception:
|
||
pass
|
||
|
||
def onTrayIconActivated(self, reason):
|
||
"""托盘图标被激活时的处理"""
|
||
if reason == QSystemTrayIcon.Trigger:
|
||
if self.isVisible():
|
||
self.hide()
|
||
else:
|
||
self.showNormal()
|
||
self.activateWindow()
|
||
|
||
def handle_external_activate(self, task=None, exit_on_complete=False):
|
||
"""响应来自其他实例的激活请求:置顶窗口并根据需要启动任务或设置退出行为"""
|
||
from PySide6.QtCore import QTimer
|
||
try:
|
||
# 显示并置顶窗口
|
||
self.showNormal()
|
||
self.raise_()
|
||
self.activateWindow()
|
||
except Exception:
|
||
pass
|
||
|
||
# 如果指定了任务,延迟执行以保证界面初始化完成
|
||
if task:
|
||
self.startup_task = task
|
||
QTimer.singleShot(200, self._executeStartupTask)
|
||
|
||
# 设置任务完成后是否退出的标志
|
||
if exit_on_complete:
|
||
self.exit_on_complete = exit_on_complete
|
||
|
||
def _on_tray_menu_about_to_show(self):
|
||
"""托盘菜单即将显示时激活窗口,解决 Windows 上点击外部区域无法关闭菜单的问题"""
|
||
self.activateWindow()
|
||
|
||
def _onStartTask(self, command):
|
||
"""处理任务启动信号"""
|
||
# 检查是否有任务正在运行
|
||
if self.logInterface.isTaskRunning():
|
||
InfoBar.warning(
|
||
title=tr('任务正在运行'),
|
||
content=tr("请先停止当前任务后再启动新任务"),
|
||
orient=Qt.Horizontal,
|
||
isClosable=True,
|
||
position=InfoBarPosition.TOP,
|
||
duration=5000,
|
||
parent=self
|
||
)
|
||
# 切换到日志界面
|
||
self.switchTo(self.logInterface)
|
||
return
|
||
# 静默检查更新
|
||
checkUpdate(self, silent=True)
|
||
# 切换到日志界面
|
||
self.switchTo(self.logInterface)
|
||
# 启动任务
|
||
self.logInterface.startTask(command)
|
||
|
||
def startFullTask(self):
|
||
"""启动完整运行任务"""
|
||
from tasks.base.tasks import start_task
|
||
start_task("main")
|
||
|
||
def _onHotkeyChanged(self):
|
||
"""处理热键配置改变信号"""
|
||
if hasattr(self, 'logInterface'):
|
||
self.logInterface.updateHotkey()
|
||
if hasattr(self, 'toolsInterface'):
|
||
self.toolsInterface.automaticPlotCard.updateHotkeyHint()
|
||
|
||
def _onAutoplotToggleRequested(self):
|
||
"""处理自动对话切换请求(全局热键触发)"""
|
||
if hasattr(self, 'toolsInterface'):
|
||
self.toolsInterface.toggleAutoPlot()
|
||
|
||
def _on_ui_language_changed(self, lang_code: str):
|
||
"""热重载 UI 语言,无需重启。
|
||
流程:更新翻译字典 → 禁用导航栏 → 切到安全锚点 → 等动画结束 → 分步重建界面。
|
||
"""
|
||
from PySide6.QtCore import QTimer
|
||
try:
|
||
from module.localization import load_language, detect_lang
|
||
|
||
actual_lang = lang_code
|
||
if actual_lang == 'auto':
|
||
actual_lang = detect_lang()
|
||
|
||
cfg.ui_language_now = actual_lang
|
||
load_language(actual_lang)
|
||
self._reinstall_fluent_translator(actual_lang)
|
||
self._refreshWindowTitleBar()
|
||
if hasattr(self, 'logInterface') and self.logInterface:
|
||
self.logInterface.setDetectedUpdateVersion(self.detected_update_version)
|
||
|
||
# 禁用导航栏,防止重建期间误操作
|
||
self.navigationInterface.setEnabled(False)
|
||
|
||
# 先切到日志界面(安全锚点);等 350ms 让导航动画完全结束后再重建
|
||
self.switchTo(self.logInterface)
|
||
QTimer.singleShot(350, self._rebuild_interfaces_for_language)
|
||
except Exception as e:
|
||
self.navigationInterface.setEnabled(True)
|
||
InfoBar.warning(
|
||
title='语言切换失败',
|
||
content=str(e),
|
||
orient=Qt.Horizontal,
|
||
isClosable=True,
|
||
position=InfoBarPosition.TOP,
|
||
duration=3000,
|
||
parent=self
|
||
)
|
||
|
||
def _reinstall_fluent_translator(self, lang_code: str):
|
||
"""重新安装 FluentTranslator 以使 Qt 内置组件翻译同步更新"""
|
||
from PySide6.QtCore import QLocale
|
||
from qfluentwidgets import FluentTranslator
|
||
app = QApplication.instance()
|
||
if hasattr(self, '_fluent_translator') and self._fluent_translator:
|
||
try:
|
||
app.removeTranslator(self._fluent_translator)
|
||
except Exception:
|
||
pass
|
||
if lang_code == 'zh_TW':
|
||
self._fluent_translator = FluentTranslator(QLocale(QLocale.Language.Chinese, QLocale.Country.Taiwan))
|
||
elif lang_code == 'ja_JP':
|
||
self._fluent_translator = FluentTranslator(QLocale(QLocale.Language.Japanese, QLocale.Country.Japan))
|
||
elif lang_code == 'ko_KR':
|
||
self._fluent_translator = FluentTranslator(QLocale(QLocale.Language.Korean, QLocale.Country.SouthKorea))
|
||
elif lang_code == 'en_US':
|
||
self._fluent_translator = FluentTranslator(QLocale(QLocale.Language.English, QLocale.Country.UnitedStates))
|
||
else:
|
||
self._fluent_translator = FluentTranslator(QLocale(QLocale.Language.Chinese, QLocale.Country.China))
|
||
app.installTranslator(self._fluent_translator)
|
||
|
||
def _rebuild_interfaces_for_language(self):
|
||
"""同步重建所有子界面以应用新语言。
|
||
|
||
性能策略:
|
||
- TOP 界面(Home/Help/Warp/Tools)构造轻量,先重建完毕
|
||
- 重建完 TOP 界面后调用一次 processEvents(),让 Windows 消息队列清空,
|
||
防止在最重的 SettingInterface 构建期间出现"(不响应)"标题
|
||
- SettingInterface 构建完成后立即 switchTo 并还原光标
|
||
- 全程不重新连接信号(initInterface 中已连接且永久有效)
|
||
"""
|
||
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
|
||
QApplication.processEvents() # 立即刷新,让光标实际渲染后再开始重建
|
||
try:
|
||
# ── 轻量 TOP 界面:逐一移除旧→添加新 ───────────────────────
|
||
top_specs = [
|
||
('homeInterface', FIF.HOME, tr('主页'), HomeInterface),
|
||
('helpInterface', FIF.BOOK_SHELF, tr('帮助'), HelpInterface),
|
||
('warpInterface', FIF.SHARE, tr('抽卡记录'), WarpInterface),
|
||
('toolsInterface', FIF.DEVELOPER_TOOLS, tr('工具箱'), ToolsInterface),
|
||
('workflowInterface', FIF.CODE, tr('流程编排'), WorkflowInterface),
|
||
]
|
||
for attr, icon, label, cls in top_specs:
|
||
old = getattr(self, attr, None)
|
||
if old is not None:
|
||
try:
|
||
route_key = old.objectName()
|
||
if route_key in self.navigationInterface.items:
|
||
self.navigationInterface.items[route_key].hide()
|
||
self.removeInterface(old, isDelete=True)
|
||
except Exception:
|
||
pass
|
||
try:
|
||
new_iface = cls(self)
|
||
setattr(self, attr, new_iface)
|
||
self.addSubInterface(new_iface, icon, label)
|
||
except Exception:
|
||
pass
|
||
|
||
# ── 轻量界面完成后清空 Windows 消息队列 ──────────────────
|
||
# 调用一次 processEvents(),避免在随后最重的 SettingInterface
|
||
# 构建期间因 WM_PAINT 积压 >5s 而触发系统"(不响应)"弹窗。
|
||
# 仅此一次,不在循环中调用,不会引起多余重绘。
|
||
QApplication.processEvents()
|
||
|
||
# ── 日志界面:保留进程,只更新导航标签 ──────────────────
|
||
try:
|
||
log_key = self.logInterface.objectName()
|
||
log_item = self.navigationInterface.items.get(log_key)
|
||
if log_item is not None and hasattr(log_item, 'setText'):
|
||
log_item.setText(tr('日志'))
|
||
except Exception:
|
||
pass
|
||
|
||
# ── 设置界面(最重):移除旧→创建新 ──────────────────────
|
||
old_setting = self.settingInterface
|
||
try:
|
||
route_key = old_setting.objectName()
|
||
if route_key in self.navigationInterface.items:
|
||
self.navigationInterface.items[route_key].hide()
|
||
self.removeInterface(old_setting, isDelete=True)
|
||
except Exception:
|
||
pass
|
||
try:
|
||
self.settingInterface = SettingInterface(self)
|
||
self.addSubInterface(
|
||
self.settingInterface, FIF.SETTING, tr('设置'),
|
||
position=NavigationItemPosition.BOTTOM
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
# ── 导航栏自定义按钮文本 ──────────────────────────────────
|
||
for widget_key, text_key in [('startGameButton', '启动游戏'), ('avatar', '赞赏')]:
|
||
try:
|
||
btn = self.navigationInterface.widget(widget_key)
|
||
if btn and hasattr(btn, 'setText'):
|
||
btn.setText(tr(text_key))
|
||
except Exception:
|
||
pass
|
||
|
||
self.navigationInterface.setEnabled(True)
|
||
try:
|
||
self.switchTo(self.settingInterface)
|
||
except Exception:
|
||
pass
|
||
|
||
InfoBar.success(
|
||
title=tr('更新成功'),
|
||
content='',
|
||
orient=Qt.Horizontal,
|
||
isClosable=True,
|
||
position=InfoBarPosition.TOP,
|
||
duration=1500,
|
||
parent=self
|
||
)
|
||
except Exception as e:
|
||
import traceback
|
||
traceback.print_exc()
|
||
InfoBar.warning(
|
||
title=tr('配置加载失败'),
|
||
content=str(e),
|
||
orient=Qt.Horizontal,
|
||
isClosable=True,
|
||
position=InfoBarPosition.TOP,
|
||
duration=3000,
|
||
parent=self
|
||
)
|
||
finally:
|
||
self.navigationInterface.setEnabled(True)
|
||
QApplication.restoreOverrideCursor()
|
||
|
||
def _onTaskFinished(self, exit_code):
|
||
"""处理任务完成信号"""
|
||
# 如果是启动任务且设置了完成后退出,则在任务成功完成时退出程序
|
||
if self.exit_on_complete and self.startup_task and exit_code == 0:
|
||
from PySide6.QtCore import QTimer
|
||
# 延迟一小段时间让用户看到完成状态
|
||
QTimer.singleShot(5000, self.quitApp)
|
||
else:
|
||
# 任务失败或未指定退出时,清除自动退出标记
|
||
self.exit_on_complete = False
|
||
|
||
def quitApp(self):
|
||
"""退出应用程序"""
|
||
self._do_quit()
|
||
|
||
def _saveWindowState(self):
|
||
"""保存窗口尺寸、位置和最大化状态到配置文件"""
|
||
try:
|
||
is_maximized = self.isMaximized()
|
||
cfg.set_value('window_maximized', is_maximized)
|
||
|
||
window_memory = cfg.get_value('window_memory', 'size')
|
||
|
||
# 只在非最大化状态下保存窗口尺寸和位置
|
||
if not is_maximized:
|
||
if window_memory in ('size', 'size_and_position'):
|
||
cfg.set_value('window_width', self.width())
|
||
cfg.set_value('window_height', self.height())
|
||
if window_memory in ('position', 'size_and_position'):
|
||
cfg.set_value('window_x', self.x())
|
||
cfg.set_value('window_y', self.y())
|
||
except Exception:
|
||
pass
|
||
|
||
def _on_config_file_changed(self):
|
||
"""重新加载配置文件并刷新界面"""
|
||
try:
|
||
# 检查当前是否在设置界面
|
||
is_in_setting_interface = self.stackedWidget.currentWidget() == self.settingInterface
|
||
|
||
# 重新加载配置
|
||
cfg._load_config(None, save=False)
|
||
|
||
# 重新初始化通知器
|
||
try:
|
||
from module.notification import init_notifiers
|
||
init_notifiers()
|
||
except Exception:
|
||
pass
|
||
|
||
# 更新日志界面的热键与日志悬浮窗开关
|
||
if hasattr(self, 'logInterface'):
|
||
self.logInterface.reloadConfigState()
|
||
|
||
# 保存旧的设置界面引用
|
||
old_setting_interface = self.settingInterface
|
||
route_key = old_setting_interface.objectName()
|
||
|
||
# 创建新的设置界面
|
||
self.settingInterface = SettingInterface(self)
|
||
|
||
# 必须先把旧的导航栏隐藏,否则会导致最后的高度增加(bug)
|
||
self.navigationInterface.items[route_key].hide()
|
||
|
||
# 移除旧的设置界面
|
||
self.removeInterface(old_setting_interface, isDelete=True)
|
||
|
||
# 添加新的设置界面
|
||
self.addSubInterface(self.settingInterface, FIF.SETTING, '设置', position=NavigationItemPosition.BOTTOM)
|
||
|
||
# 只有在重新加载配置前是在设置界面时,才切换到新的设置界面
|
||
if is_in_setting_interface:
|
||
self.switchTo(self.settingInterface)
|
||
|
||
# 只有在窗口可见时才显示提示
|
||
if self.isVisible():
|
||
InfoBar.success(
|
||
title=tr('配置已更新'),
|
||
content=tr("检测到配置文件变化,已自动重新加载"),
|
||
orient=Qt.Horizontal,
|
||
isClosable=True,
|
||
position=InfoBarPosition.TOP,
|
||
duration=2000,
|
||
parent=self
|
||
)
|
||
except Exception as e:
|
||
# 只有在窗口可见时才显示提示
|
||
if self.isVisible():
|
||
InfoBar.warning(
|
||
title=tr('配置加载失败'),
|
||
content=str(e),
|
||
orient=Qt.Horizontal,
|
||
isClosable=True,
|
||
position=InfoBarPosition.TOP,
|
||
duration=3000,
|
||
parent=self
|
||
)
|
||
|
||
def _stopThemeListener(self):
|
||
"""停止主题监听线程"""
|
||
if hasattr(self, 'themeListener') and self.themeListener:
|
||
self.themeListener.stop()
|
||
self.themeListener = None
|
||
|
||
def _stopRunningTask(self):
|
||
"""停止正在运行的任务"""
|
||
if hasattr(self, 'logInterface') and self.logInterface.isTaskRunning():
|
||
self.logInterface.stopTask()
|
||
# 等待进程结束
|
||
if self.logInterface.process:
|
||
self.logInterface.process.waitForFinished(3000)
|
||
# 如果还没结束,强制结束
|
||
if self.logInterface.process.state() != 0: # QProcess.NotRunning
|
||
self.logInterface.process.kill()
|
||
self.logInterface.process.waitForFinished(1000)
|
||
|
||
def _do_quit(self, e=None):
|
||
"""执行退出前的清理并退出程序
|
||
e: 可选的 QCloseEvent,用于调用 e.accept()
|
||
"""
|
||
# 保存窗口尺寸和最大化状态
|
||
self._saveWindowState()
|
||
|
||
try:
|
||
self.hide()
|
||
self.tray_icon.hide()
|
||
QApplication.processEvents()
|
||
except Exception:
|
||
pass
|
||
|
||
# 停止运行任务和主题监听
|
||
self._stopRunningTask()
|
||
self._stopThemeListener()
|
||
|
||
# 可选地清理日志界面资源
|
||
if hasattr(self, 'logInterface'):
|
||
try:
|
||
self.logInterface.cleanup()
|
||
except Exception:
|
||
pass
|
||
|
||
# 如果传入了事件,接受它
|
||
if e is not None:
|
||
try:
|
||
e.accept()
|
||
except Exception:
|
||
pass
|
||
|
||
QApplication.quit()
|
||
|
||
def closeEvent(self, e):
|
||
"""关闭窗口时根据配置执行对应操作"""
|
||
from .card.messagebox_custom import MessageBoxCloseWindow
|
||
|
||
close_action = cfg.get_value('close_window_action', 'ask')
|
||
|
||
if close_action == 'ask':
|
||
# 弹出询问对话框
|
||
dialog = MessageBoxCloseWindow(self)
|
||
dialog.exec()
|
||
|
||
if dialog.action == 'minimize':
|
||
# 最小化到托盘
|
||
e.ignore()
|
||
self.hide()
|
||
self.tray_icon.showMessage(
|
||
'March7th Assistant',
|
||
tr('程序已最小化到托盘'),
|
||
QSystemTrayIcon.Information,
|
||
2000
|
||
)
|
||
# 若用户选择记住,则刷新设置界面以同步显示
|
||
try:
|
||
if dialog.rememberCheckBox.isChecked():
|
||
self._on_config_file_changed()
|
||
except Exception:
|
||
pass
|
||
elif dialog.action == 'close':
|
||
# 关闭程序
|
||
self._do_quit(e)
|
||
else:
|
||
# 用户取消操作(例如点击了 X 按钮)
|
||
e.ignore()
|
||
elif close_action == 'minimize':
|
||
# 直接最小化到托盘
|
||
e.ignore()
|
||
self.hide()
|
||
# self.tray_icon.showMessage(
|
||
# 'March7th Assistant',
|
||
# '程序已最小化到托盘',
|
||
# QSystemTrayIcon.Information,
|
||
# 2000
|
||
# )
|
||
elif close_action == 'close':
|
||
# 直接关闭程序
|
||
self._do_quit(e)
|
||
else:
|
||
# 默认行为:最小化到托盘
|
||
e.ignore()
|
||
self.hide()
|
||
self.tray_icon.showMessage(
|
||
'March7th Assistant',
|
||
tr('程序已最小化到托盘'),
|
||
QSystemTrayIcon.Information,
|
||
2000
|
||
)
|
||
|
||
def startGame(self):
|
||
start_game_button = self.navigationInterface.widget('startGameButton')
|
||
if start_game_button:
|
||
start_game_button.setEnabled(False)
|
||
game = get_game_controller()
|
||
if cfg.cloud_game_enable and cfg.browser_type == "integrated" and not game.is_integrated_browser_downloaded():
|
||
InfoBar.warning(
|
||
title=tr('正在下载内置浏览器(ง •̀_•́)ง'),
|
||
content=tr("下载成功后,将自动启动云·星穹铁道"),
|
||
orient=Qt.Horizontal,
|
||
isClosable=True,
|
||
position=InfoBarPosition.TOP,
|
||
duration=10000,
|
||
parent=self
|
||
)
|
||
elif cfg.cloud_game_enable:
|
||
InfoBar.warning(
|
||
title=tr('正在启动游戏(❁´◡`❁)'),
|
||
content="",
|
||
orient=Qt.Horizontal,
|
||
isClosable=True,
|
||
position=InfoBarPosition.TOP,
|
||
duration=5000,
|
||
parent=self
|
||
)
|
||
else:
|
||
from tasks.game.starrailcontroller import StarRailController
|
||
starrail = StarRailController(cfg=cfg, logger=log)
|
||
if cfg.auto_battle_detect_enable:
|
||
starrail.change_auto_battle(True)
|
||
|
||
self.game_launch_thread = GameLaunchThread(game, cfg)
|
||
self.game_launch_thread.finished_signal.connect(self.on_game_launched)
|
||
self.game_launch_thread.start()
|
||
|
||
def on_game_launched(self, result):
|
||
if result == GameStartStatus.SUCCESS:
|
||
InfoBar.success(
|
||
title=tr('启动成功(^∀^●)'),
|
||
content="",
|
||
orient=Qt.Horizontal,
|
||
isClosable=True,
|
||
position=InfoBarPosition.TOP,
|
||
duration=2000,
|
||
parent=self
|
||
)
|
||
elif result == GameStartStatus.BROWSER_DOWNLOAD_FAIL:
|
||
InfoBar.warning(
|
||
title=tr('浏览器或驱动下载失败 (╥╯﹏╰╥)'),
|
||
content=tr("请检查网络连接是否正常"),
|
||
orient=Qt.Horizontal,
|
||
isClosable=True,
|
||
position=InfoBarPosition.TOP,
|
||
duration=5000,
|
||
parent=self
|
||
)
|
||
elif result == GameStartStatus.BROWSER_LAUNCH_FAIL:
|
||
InfoBar.warning(
|
||
title=tr('云游戏启动失败(╥╯﹏╰╥)'),
|
||
content=tr("请检查所选浏览器是否存在,网络连接是否正常"),
|
||
orient=Qt.Horizontal,
|
||
isClosable=True,
|
||
position=InfoBarPosition.TOP,
|
||
duration=5000,
|
||
parent=self
|
||
)
|
||
elif result == GameStartStatus.LOCAL_LAUNCH_FAIL:
|
||
InfoBar.warning(
|
||
title=tr('游戏路径配置错误(╥╯﹏╰╥)'),
|
||
content=tr("请在“设置”-->“程序”中配置"),
|
||
orient=Qt.Horizontal,
|
||
isClosable=True,
|
||
position=InfoBarPosition.TOP,
|
||
duration=5000,
|
||
parent=self
|
||
)
|
||
else:
|
||
InfoBar.warning(
|
||
title=tr('启动失败'),
|
||
content=str(self.game_launch_thread.error_msg),
|
||
orient=Qt.Horizontal,
|
||
isClosable=True,
|
||
position=InfoBarPosition.TOP,
|
||
duration=5000,
|
||
parent=self
|
||
)
|
||
start_game_button = self.navigationInterface.widget('startGameButton')
|
||
if start_game_button:
|
||
start_game_button.setEnabled(True)
|