March7thAssistant/app/tools_interface.py
moesnow 00537b74c2
feat: 添加自动对话全局快捷键切换功能
- 新增 hotkey_toggle_autoplot 配置项,默认快捷键 F9
- 新增 hotkey_toggle_autoplot_enable 配置项,默认关闭
- 持久化自动对话选项(跳过/选择/战斗检测/短信)
- 工具箱自动对话卡片展开后显示快捷键提示

Closes #1110
2026-06-08 20:54:43 +08:00

331 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from PySide6.QtCore import Qt
from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QSpacerItem, QScroller, QScrollerProperties, QFileDialog
from qfluentwidgets import FluentIcon as FIF
from qfluentwidgets import SettingCardGroup, PushSettingCard, ScrollArea, InfoBar, InfoBarPosition, MessageBox, PushButton
from .card.pushsettingcard1 import PushSettingCardCode
from .card.autoplot_setting_card import AutoPlotSettingCard
from .common.style_sheet import StyleSheet
import tasks.tool as tool
import base64
import subprocess
import pyperclip
from module.config import cfg
from tasks.base.tasks import start_task
import os
import sys
from module.localization import tr
class ToolsInterface(ScrollArea):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.parent = parent
self.scrollWidget = QWidget()
self.vBoxLayout = QVBoxLayout(self.scrollWidget)
self.toolsLabel = QLabel(tr("工具箱"), self)
self.ToolsGroup = SettingCardGroup(tr('工具箱'), self.scrollWidget)
self.automaticPlotCard = AutoPlotSettingCard(
FIF.IMAGE_EXPORT,
tr("自动对话"),
tr("进入剧情页面后自动开始运行,支持大于等于 1920*1080 的 16:9 分辨率,不支持云·星穹铁道")
)
self.gameScreenshotCard = PushSettingCard(
tr('捕获'),
FIF.CLIPPING_TOOL,
tr("游戏截图"),
tr("检查程序获取的图像是否正确支持OCR识别文字可用于自行排查异常")
)
self.unlockfpsCard = PushSettingCard(
tr('解锁'),
FIF.SPEED_HIGH,
tr("解锁帧率"),
tr("通过修改注册表解锁120帧率如已解锁再次点击将恢复60帧率支持国服和国际服")
)
self.redemptionCodeCard = PushSettingCardCode(
tr('执行'),
FIF.BOOK_SHELF,
tr("兑换码"),
"redemption_code",
self
)
self.cloudTouchCard = PushSettingCard(
tr('启动'),
FIF.CLOUD,
tr("触屏模式"),
tr("以云游戏移动端 UI 的方式启动游戏,可搭配 UU远程 平板触控模式,启动后会将命令复制到剪贴板内")
)
self.screenTestCard = PushSettingCard(
tr('测试'),
FIF.CHECKBOX,
tr("界面可切换性测试"),
tr("以最短路径遍历所有可达界面,验证界面切换是否正常")
)
self.__initWidget()
def __initWidget(self):
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setViewportMargins(0, 80, 0, 20)
self.setWidget(self.scrollWidget)
self.setWidgetResizable(True)
self.setObjectName('toolsInterface')
self.scrollWidget.setObjectName('scrollWidget')
self.toolsLabel.setObjectName('toolsLabel')
StyleSheet.TOOLS_INTERFACE.apply(self)
self.__initLayout()
self.__connectSignalToSlot()
QScroller.grabGesture(self.viewport(), QScroller.ScrollerGestureType.LeftMouseButtonGesture)
scroller = QScroller.scroller(self.viewport())
scroller_props = scroller.scrollerProperties()
scroller_props.setScrollMetric(QScrollerProperties.ScrollMetric.OvershootDragDistanceFactor, 0.05)
scroller_props.setScrollMetric(QScrollerProperties.ScrollMetric.OvershootScrollDistanceFactor, 0.05)
scroller_props.setScrollMetric(QScrollerProperties.ScrollMetric.DecelerationFactor, 0.5)
scroller.setScrollerProperties(scroller_props)
def __initLayout(self):
self.toolsLabel.move(36, 30)
self.ToolsGroup.addSettingCard(self.automaticPlotCard)
self.ToolsGroup.addSettingCard(self.gameScreenshotCard)
self.ToolsGroup.addSettingCard(self.unlockfpsCard)
self.ToolsGroup.addSettingCard(self.redemptionCodeCard)
self.ToolsGroup.addSettingCard(self.cloudTouchCard)
self.ToolsGroup.addSettingCard(self.screenTestCard)
if sys.platform != 'win32':
self.automaticPlotCard.setDisabled(True)
self.unlockfpsCard.setDisabled(True)
self.cloudTouchCard.setDisabled(True)
self.ToolsGroup.titleLabel.setHidden(True)
self.vBoxLayout.setSpacing(28)
self.vBoxLayout.setContentsMargins(36, 10, 36, 0)
self.vBoxLayout.addWidget(self.ToolsGroup)
for i in range(self.ToolsGroup.vBoxLayout.count()):
item = self.ToolsGroup.vBoxLayout.itemAt(i)
if isinstance(item, QSpacerItem):
self.ToolsGroup.vBoxLayout.removeItem(item)
break
def __onUnlockfpsCardClicked(self):
from utils.registry.star_rail_setting import get_game_fps, set_game_fps # 延迟导入,避免非 Windows 平台报错
try:
fps = get_game_fps()
if fps == 120:
set_game_fps(60)
InfoBar.info(
title=tr('恢复60成功 (^∀^●)'),
content="",
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP,
duration=1000,
parent=self
)
else:
set_game_fps(120)
InfoBar.success(
title=tr('解锁120成功 (^∀^●)'),
content="",
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP,
duration=1000,
parent=self
)
except:
InfoBar.warning(
title=tr('解锁失败'),
content=tr("请将游戏图像质量修改为自定义后重试"),
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP,
duration=5000,
parent=self
)
def __onCloudTouchCardClicked(self):
exe_path = os.path.abspath(os.path.join(cfg.genshin_starRail_fps_unlocker_path, "unlocker.exe"))
config_path = os.path.abspath(os.path.join(cfg.genshin_starRail_fps_unlocker_path, "hoyofps_config.ini"))
game_path = os.path.abspath(cfg.game_path)
try:
if cfg.genshin_starRail_fps_unlocker_allow is False:
from qfluentwidgets import MessageBox
# 依次展示多次确认对话框,每次提示更严肃,任意一次取消即返回
confirm_messages = [
(tr('此功能依赖第三方程序实现'),
tr('https://github.com/winTEuser/Genshin_StarRail_fps_unlocker\n使用本功能产生的所有问题与本项目与开发者团队无关,是否继续?')),
(tr('再次确认:可能存在风险'),
tr('工作原理是通过 WriteProcessMemory 把代码写进游戏,是否继续?')),
(tr('最终确认:请谨慎操作'),
tr('确认继续并允许启用触屏模式?')),
]
for title, message in confirm_messages:
from qfluentwidgets import MessageBox
step_confirm = MessageBox(title, message, self.window())
step_confirm.yesButton.setText(tr('确认'))
step_confirm.cancelButton.setText(tr('取消'))
if not step_confirm.exec():
return
cfg.set_value("genshin_starRail_fps_unlocker_allow", True)
if not game_path or not os.path.exists(game_path):
InfoBar.warning(
title=tr('游戏路径配置错误(╥╯﹏╰╥)'),
content=tr("请在“设置”-->“程序”中配置正确的游戏路径"),
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP,
duration=5000,
parent=self
)
return
if not os.path.exists(exe_path):
start_task("mobileui_update")
return
config_dir = os.path.dirname(config_path)
if not os.path.exists(config_dir):
os.makedirs(config_dir, exist_ok=True)
import configparser
cp = configparser.ConfigParser()
if os.path.exists(config_path):
cp.read(config_path, encoding='utf-16')
if 'Setting' not in cp:
cp['Setting'] = {}
old_path = cp['Setting'].get('HKSRPath', '')
old_fps = cp['Setting'].get('FPS', '')
if old_path != game_path or old_fps != '60':
cp['Setting']['HKSRPath'] = game_path
cp['Setting']['FPS'] = '60'
with open(config_path, 'w', encoding='utf-16') as f:
cp.write(f)
else:
cp['Setting'] = {'HKSRPath': game_path, 'FPS': '60'}
with open(config_path, 'w', encoding='utf-16') as f:
cp.write(f)
args = ["-HKSR", "-EnableMobileUI"]
subprocess.Popen([exe_path] + args, cwd=config_dir)
pyperclip.copy(f'cd "{config_dir}" && "{exe_path}" {" ".join(args)}')
InfoBar.success(
title=tr('启动成功(^∀^●)'),
content=tr("已将命令复制到剪贴板"),
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP,
duration=2000,
parent=self
)
except Exception as e:
InfoBar.warning(
title=tr('启动失败'),
content=str(e),
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP,
duration=5000,
parent=self
)
def __onGameScreenshotCardClicked(self):
"""点击截图卡片后先选择来源:游戏窗口或本地图片。"""
message_box = MessageBox(tr("截图来源"), tr("请选择截图方式"), self.window())
message_box.contentLabel.setMinimumWidth(500)
message_box.yesButton.setText(tr("捕获游戏窗口"))
message_box.cancelButton.setText(tr("取消"))
choose_file_button = PushButton(tr("手动选择图片"), message_box)
message_box.buttonLayout.insertWidget(1, choose_file_button, 1, Qt.AlignmentFlag.AlignVCenter)
action = {"value": None}
def _choose_local_file():
action["value"] = "file"
message_box.reject()
choose_file_button.clicked.connect(_choose_local_file)
accepted = message_box.exec()
if action["value"] == "file":
file_path, _ = QFileDialog.getOpenFileName(
self,
tr("选择图片"),
"",
"Image Files (*.png *.jpg *.jpeg *.bmp *.webp)"
)
if file_path:
tool.start_screenshot_from_file(file_path)
return
if accepted:
tool.start("screenshot")
def __connectSignalToSlot(self):
self.gameScreenshotCard.clicked.connect(self.__onGameScreenshotCardClicked)
self.automaticPlotCard.switchChanged.connect(self.__onAutoPlotSwitchChanged)
self.automaticPlotCard.optionsChanged.connect(self.__onAutoPlotOptionsChanged)
self.unlockfpsCard.clicked.connect(self.__onUnlockfpsCardClicked)
self.cloudTouchCard.clicked.connect(self.__onCloudTouchCardClicked)
self.screenTestCard.clicked.connect(self.__onScreenTestCardClicked)
def __onScreenTestCardClicked(self):
start_task("screen_test")
def __onAutoPlotSwitchChanged(self, isChecked: bool):
"""Handle auto plot switch state change"""
if isChecked:
# Update options first
options = self.automaticPlotCard.getOptions()
tool.update_plot_options(options)
# Start auto plot
tool.start("plot")
InfoBar.success(
title=tr('自动对话已启动'),
content="",
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP,
duration=1000,
parent=self
)
else:
# Stop auto plot
tool.stop_plot()
InfoBar.info(
title=tr('自动对话已停止'),
content="",
orient=Qt.Orientation.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP,
duration=1000,
parent=self
)
def __onAutoPlotOptionsChanged(self, options: dict):
"""Handle auto plot options change"""
# Update options if auto plot is running
if self.automaticPlotCard.getSwitchState():
tool.update_plot_options(options)
def toggleAutoPlot(self):
"""切换自动对话状态(供全局热键调用)"""
current_state = self.automaticPlotCard.getSwitchState()
new_state = not current_state
self.automaticPlotCard.setValue(new_state)