feat(core): 添加隐私协议管理和关于菜单功能

- 在 Main.py 中初始化隐私协议管理器,并在程序启动前显示隐私协议对话框
- 在 core/__init__.py 中添加 PrivacyManager 的引用
- 在 ui_manager.py 中实现关于菜单和隐私协议相关功能,包括:
  - 创建关于按钮和菜单
  - 添加隐私协议子菜单
  - 实现撤回隐私协议同意并重启软件的功能
  - 优化菜单样式和字体加载
This commit is contained in:
hyb-oyqq
2025-07-31 10:59:42 +08:00
parent c837370470
commit cbfe0d7ff6
6 changed files with 618 additions and 62 deletions

View File

@@ -1,9 +1,21 @@
import sys
from PySide6.QtWidgets import QApplication
from main_window import MainWindow
from core.privacy_manager import PrivacyManager
if __name__ == "__main__":
app = QApplication(sys.argv)
# 初始化隐私协议管理器
privacy_manager = PrivacyManager()
# 显示隐私协议对话框(仅在首次运行或用户未同意时显示)
if not privacy_manager.show_privacy_dialog():
print("用户未同意隐私协议,程序退出")
sys.exit(0) # 如果用户不同意隐私协议,退出程序
# 用户已同意隐私协议,继续启动程序
print("隐私协议已同意,启动主程序")
window = MainWindow()
window.show()
sys.exit(app.exec())

View File

@@ -6,6 +6,7 @@ from .window_manager import WindowManager
from .game_detector import GameDetector
from .patch_manager import PatchManager
from .config_manager import ConfigManager
from .privacy_manager import PrivacyManager
__all__ = [
'MultiStageAnimations',
@@ -15,5 +16,6 @@ __all__ = [
'WindowManager',
'GameDetector',
'PatchManager',
'ConfigManager'
'ConfigManager',
'PrivacyManager'
]

View File

@@ -0,0 +1,178 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import json
from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QCheckBox, QLabel, QMessageBox
from PySide6.QtCore import Qt
from data.privacy_policy import PRIVACY_POLICY_BRIEF
from data.config import CACHE, APP_NAME
from utils import msgbox_frame
class PrivacyManager:
"""隐私协议管理器,负责显示隐私协议对话框并处理用户选择"""
def __init__(self):
"""初始化隐私协议管理器"""
# 确保缓存目录存在
os.makedirs(CACHE, exist_ok=True)
self.config_file = os.path.join(CACHE, "privacy_config.json")
self.privacy_accepted = self._load_privacy_config()
def _load_privacy_config(self):
"""加载隐私协议配置
Returns:
bool: 用户是否已同意隐私协议
"""
if os.path.exists(self.config_file):
try:
with open(self.config_file, "r", encoding="utf-8") as f:
config = json.load(f)
return config.get("privacy_accepted", False)
except (json.JSONDecodeError, IOError) as e:
print(f"读取隐私配置失败: {e}")
# 如果读取失败返回False强制显示隐私协议
return False
return False
def _save_privacy_config(self, accepted):
"""保存隐私协议配置
Args:
accepted: 用户是否同意隐私协议
Returns:
bool: 配置是否保存成功
"""
try:
# 确保目录存在
os.makedirs(os.path.dirname(self.config_file), exist_ok=True)
# 写入配置文件
with open(self.config_file, "w", encoding="utf-8") as f:
json.dump({
"privacy_accepted": accepted,
"version": "1.0" # 添加版本号,便于将来升级隐私协议时使用
}, f, indent=2)
# 更新实例变量
self.privacy_accepted = accepted
return True
except IOError as e:
print(f"保存隐私协议配置失败: {e}")
# 显示保存失败的提示
QMessageBox.warning(
None,
f"配置保存警告 - {APP_NAME}",
f"隐私设置无法保存到配置文件,下次启动时可能需要重新确认。\n\n错误信息:{e}"
)
return False
def show_privacy_dialog(self):
"""显示隐私协议对话框
Returns:
bool: 用户是否同意隐私协议
"""
# 如果用户已经同意了隐私协议直接返回True不显示对话框
if self.privacy_accepted:
print("用户已同意隐私协议,无需再次显示")
return True
print("首次运行或用户未同意隐私协议,显示隐私对话框")
# 创建隐私协议对话框
dialog = QDialog()
dialog.setWindowTitle(f"隐私政策 - {APP_NAME}")
dialog.setMinimumSize(600, 400)
dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowContextHelpButtonHint)
# 创建布局
layout = QVBoxLayout(dialog)
# 添加标题
title_label = QLabel("请阅读并同意以下隐私政策")
title_label.setStyleSheet("font-size: 14px; font-weight: bold;")
layout.addWidget(title_label)
# 添加隐私协议文本框
text_browser = QTextBrowser()
text_browser.setMarkdown(PRIVACY_POLICY_BRIEF)
text_browser.setOpenExternalLinks(True)
layout.addWidget(text_browser)
# 添加同意选择框
checkbox = QCheckBox("我已阅读并同意上述隐私政策")
layout.addWidget(checkbox)
# 添加按钮
buttons_layout = QHBoxLayout()
agree_button = QPushButton("同意并继续")
agree_button.setEnabled(False) # 初始状态为禁用
decline_button = QPushButton("不同意并退出")
buttons_layout.addWidget(agree_button)
buttons_layout.addWidget(decline_button)
layout.addLayout(buttons_layout)
# 连接选择框状态变化 - 修复勾选后按钮不亮起的问题
def on_checkbox_state_changed(state):
print(f"复选框状态变更为: {state}")
agree_button.setEnabled(state == 2) # Qt.Checked 在 PySide6 中值为 2
checkbox.stateChanged.connect(on_checkbox_state_changed)
# 连接按钮点击事件
agree_button.clicked.connect(lambda: self._on_agree(dialog))
decline_button.clicked.connect(lambda: self._on_decline(dialog))
# 显示对话框
result = dialog.exec()
# 返回用户选择结果
return self.privacy_accepted
def _on_agree(self, dialog):
"""处理用户同意隐私协议
Args:
dialog: 对话框实例
"""
# 保存配置并更新状态
self._save_privacy_config(True)
dialog.accept()
def _on_decline(self, dialog):
"""处理用户拒绝隐私协议
Args:
dialog: 对话框实例
"""
# 显示拒绝信息
msg_box = msgbox_frame(
f"退出 - {APP_NAME}",
"\n您需要同意隐私政策才能使用本软件。\n软件将立即退出。\n",
QMessageBox.Ok,
)
msg_box.exec()
# 保存拒绝状态
self._save_privacy_config(False)
dialog.reject()
def is_privacy_accepted(self):
"""检查用户是否已同意隐私协议
Returns:
bool: 用户是否已同意隐私协议
"""
return self.privacy_accepted
def reset_privacy_agreement(self):
"""重置隐私协议同意状态,用于测试或重新显示隐私协议
Returns:
bool: 重置是否成功
"""
return self._save_privacy_config(False)

View File

@@ -1,9 +1,10 @@
from PySide6.QtGui import QIcon, QAction, QFont
from PySide6.QtWidgets import QMessageBox, QMainWindow, QMenu
from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon, QAction, QFont, QCursor
from PySide6.QtWidgets import QMessageBox, QMainWindow, QMenu, QPushButton
from PySide6.QtCore import Qt, QRect
import webbrowser
import os
from utils import load_base64_image, msgbox_frame
from utils import load_base64_image, msgbox_frame, resource_path
from data.config import APP_NAME, APP_VERSION
class UIManager:
@@ -19,6 +20,9 @@ class UIManager:
self.debug_action = None
self.turbo_download_action = None
self.dev_menu = None
self.privacy_menu = None # 隐私协议菜单
self.about_menu = None # 关于菜单
self.about_btn = None # 关于按钮
def setup_ui(self):
"""设置UI元素包括窗口图标、标题和菜单"""
@@ -32,76 +36,161 @@ class UIManager:
# 设置窗口标题
self.main_window.setWindowTitle(f"{APP_NAME} v{APP_VERSION}")
# 创建关于按钮
self._create_about_button()
# 设置菜单
self._setup_help_menu()
self._setup_about_menu() # 新增关于菜单
self._setup_settings_menu()
def _create_about_button(self):
"""创建"关于"按钮"""
if not self.ui or not hasattr(self.ui, 'menu_area'):
return
# 获取菜单字体和样式
menu_font = self._get_menu_font()
# 创建关于按钮
self.about_btn = QPushButton("关于", self.ui.menu_area)
self.about_btn.setObjectName(u"about_btn")
# 获取帮助按钮的位置和样式
help_btn_x = 0
help_btn_width = 0
if hasattr(self.ui, 'help_btn'):
help_btn_x = self.ui.help_btn.x()
help_btn_width = self.ui.help_btn.width()
# 设置位置在"帮助"按钮右侧
self.about_btn.setGeometry(QRect(help_btn_x + help_btn_width + 20, 1, 80, 28))
self.about_btn.setFont(menu_font)
self.about_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
# 复制帮助按钮的样式
if hasattr(self.ui, 'help_btn'):
self.about_btn.setStyleSheet(self.ui.help_btn.styleSheet())
else:
# 默认样式
self.about_btn.setStyleSheet("""
QPushButton {
background-color: transparent;
color: white;
border: none;
text-align: left;
padding-left: 10px;
}
QPushButton:hover {
background-color: #F47A5B;
border-radius: 4px;
}
QPushButton:pressed {
background-color: #D25A3C;
border-radius: 4px;
}
""")
def _setup_help_menu(self):
"""设置"帮助"菜单"""
if not self.ui or not hasattr(self.ui, 'menu_2'):
return
# 创建菜单项
project_home_action = QAction("项目主页", self.main_window)
project_home_action.triggered.connect(self.open_project_home_page)
# 获取菜单字体
menu_font = self._get_menu_font()
# 创建菜单项 - 移除"项目主页",添加"常见问题"和"提交错误"
faq_action = QAction("常见问题", self.main_window)
faq_action.triggered.connect(self.open_faq_page)
faq_action.setFont(menu_font)
about_action = QAction("关于", self.main_window)
about_action.triggered.connect(self.show_about_dialog)
report_issue_action = QAction("提交错误", self.main_window)
report_issue_action.triggered.connect(self.open_issues_page)
report_issue_action.setFont(menu_font)
# 添加到菜单
self.ui.menu_2.addAction(project_home_action)
self.ui.menu_2.addAction(about_action)
# 清除现有菜单项并添加新的菜单
self.ui.menu_2.clear()
self.ui.menu_2.addAction(faq_action)
self.ui.menu_2.addAction(report_issue_action)
# 连接按钮点击事件,如果使用按钮式菜单
if hasattr(self.ui, 'help_btn'):
# 按钮已经连接到显示菜单,不需要额外处理
pass
def _setup_settings_menu(self):
"""设置"设置"菜单"""
if not self.ui or not hasattr(self.ui, 'menu'):
return
# 获取自定义字体和字体族名称
font_family = "Arial" # 默认字体族
menu_font = None
# 尝试从UI中获取字体和字体族
try:
# 优先从Ui_install.py中获取font_family变量
import os
from PySide6.QtGui import QFontDatabase
# 尝试直接加载字体并获取字体族
font_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fonts", "SmileySans-Oblique.ttf")
if os.path.exists(font_path):
font_id = QFontDatabase.addApplicationFont(font_path)
if font_id != -1:
font_family = QFontDatabase.applicationFontFamilies(font_id)[0]
# 创建与UI_install.py中完全相同的菜单字体
menu_font = QFont(font_family, 14) # 字体大小为14
menu_font.setBold(True) # 设置粗体
# 如果以上方法失败则尝试从ui获取字体
if not menu_font and hasattr(self.ui, 'menu') and self.ui.menu:
menu_font = self.ui.menu.font()
# 如果仍然没有获取到,使用默认字体
if not menu_font:
menu_font = QFont(font_family, 14)
menu_font.setBold(True)
except:
# 如果出错,使用默认字体
menu_font = QFont(font_family, 14)
menu_font.setBold(True)
# 创建开发者选项子菜单
self.dev_menu = QMenu("开发者选项", self.main_window)
self.dev_menu.setFont(menu_font) # 设置与UI_install.py中相同的字体
def _setup_about_menu(self):
"""设置"关于"菜单"""
# 获取菜单字体
menu_font = self._get_menu_font()
# 使用和主菜单相同的样式,直接指定字体族、字体大小和粗细
menu_style = f"""
# 创建关于菜单
self.about_menu = QMenu("关于", self.main_window)
self.about_menu.setFont(menu_font)
# 设置菜单样式
font_family = menu_font.family()
menu_style = self._get_menu_style(font_family)
self.about_menu.setStyleSheet(menu_style)
# 创建菜单项
about_project_action = QAction("关于本项目", self.main_window)
about_project_action.setFont(menu_font)
about_project_action.triggered.connect(self.show_about_dialog)
# 添加项目主页选项(从帮助菜单移动过来)
project_home_action = QAction("Github项目主页", self.main_window)
project_home_action.setFont(menu_font)
project_home_action.triggered.connect(self.open_project_home_page)
# 添加加入QQ群选项
qq_group_action = QAction("加入QQ群", self.main_window)
qq_group_action.setFont(menu_font)
qq_group_action.triggered.connect(self.open_qq_group)
# 创建隐私协议菜单
self._setup_privacy_menu()
# 添加到关于菜单
self.about_menu.addAction(about_project_action)
self.about_menu.addAction(project_home_action)
self.about_menu.addAction(qq_group_action)
self.about_menu.addSeparator()
self.about_menu.addMenu(self.privacy_menu)
# 连接按钮点击事件
if self.about_btn:
self.about_btn.clicked.connect(lambda: self.show_menu(self.about_menu, self.about_btn))
def _setup_privacy_menu(self):
"""设置"隐私协议"菜单"""
# 获取菜单字体
menu_font = self._get_menu_font()
# 创建隐私协议子菜单
self.privacy_menu = QMenu("隐私协议", self.main_window)
self.privacy_menu.setFont(menu_font)
# 设置与其他菜单一致的样式
font_family = menu_font.family()
menu_style = self._get_menu_style(font_family)
self.privacy_menu.setStyleSheet(menu_style)
# 添加子选项
view_privacy_action = QAction("查看完整隐私协议", self.main_window)
view_privacy_action.setFont(menu_font)
view_privacy_action.triggered.connect(self.open_privacy_policy)
revoke_privacy_action = QAction("撤回隐私协议", self.main_window)
revoke_privacy_action.setFont(menu_font)
revoke_privacy_action.triggered.connect(self.revoke_privacy_agreement)
# 添加到子菜单
self.privacy_menu.addAction(view_privacy_action)
self.privacy_menu.addAction(revoke_privacy_action)
def _get_menu_style(self, font_family):
"""获取统一的菜单样式"""
return f"""
QMenu {{
background-color: #E96948;
color: white;
@@ -136,6 +225,48 @@ class UIManager:
border-radius: 4px;
}}
"""
def _get_menu_font(self):
"""获取菜单字体"""
font_family = "Arial" # 默认字体族
try:
from PySide6.QtGui import QFontDatabase
# 尝试加载字体
font_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fonts", "SmileySans-Oblique.ttf")
if os.path.exists(font_path):
font_id = QFontDatabase.addApplicationFont(font_path)
if font_id != -1:
font_family = QFontDatabase.applicationFontFamilies(font_id)[0]
# 创建菜单字体
menu_font = QFont(font_family, 14)
menu_font.setBold(True)
return menu_font
except Exception as e:
print(f"加载字体失败: {e}")
# 返回默认字体
menu_font = QFont(font_family, 14)
menu_font.setBold(True)
return menu_font
def _setup_settings_menu(self):
"""设置"设置"菜单"""
if not self.ui or not hasattr(self.ui, 'menu'):
return
# 获取菜单字体
menu_font = self._get_menu_font()
font_family = menu_font.family()
# 创建开发者选项子菜单
self.dev_menu = QMenu("开发者选项", self.main_window)
self.dev_menu.setFont(menu_font) # 设置与UI_install.py中相同的字体
# 使用和主菜单相同的样式
menu_style = self._get_menu_style(font_family)
self.dev_menu.setStyleSheet(menu_style)
# 创建Debug模式选项并添加到开发者选项子菜单中
@@ -172,22 +303,119 @@ class UIManager:
self.ui.menu.addAction(self.switch_source_action)
self.ui.menu.addSeparator()
self.ui.menu.addMenu(self.dev_menu) # 添加开发者选项子菜单
def show_menu(self, menu, button):
"""显示菜单
# 连接按钮点击事件,如果使用按钮式菜单
if hasattr(self.ui, 'settings_btn'):
# 按钮已经连接到显示菜单,不需要额外处理
pass
Args:
menu: 要显示的菜单
button: 触发菜单的按钮
"""
# 检查Ui_install中是否定义了show_menu方法
if hasattr(self.ui, 'show_menu'):
# 如果存在使用UI中定义的方法
self.ui.show_menu(menu, button)
else:
# 否则,使用默认的弹出方法
global_pos = button.mapToGlobal(button.rect().bottomLeft())
menu.popup(global_pos)
def open_project_home_page(self):
"""打开项目主页"""
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT")
def open_github_page(self):
"""打开项目GitHub页面"""
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT")
def open_faq_page(self):
"""打开常见问题页面"""
import locale
# 根据系统语言选择FAQ页面
system_lang = locale.getdefaultlocale()[0]
if system_lang and system_lang.startswith('zh'):
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ.md")
else:
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/FAQ-en.md")
def open_issues_page(self):
"""打开GitHub问题页面"""
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/issues")
def open_qq_group(self):
"""打开QQ群链接"""
webbrowser.open("https://qm.qq.com/q/g9i04i5eec")
def open_privacy_policy(self):
"""打开完整隐私协议在GitHub上"""
webbrowser.open("https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/PRIVACY.md")
def revoke_privacy_agreement(self):
"""撤回隐私协议同意,并重启软件"""
# 创建确认对话框
msg_box = msgbox_frame(
f"确认操作 - {APP_NAME}",
"\n您确定要撤回隐私协议同意吗?\n\n撤回后软件将立即重启,您需要重新阅读并同意隐私协议。\n",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
reply = msg_box.exec()
if reply == QMessageBox.StandardButton.Yes:
# 用户确认撤回
try:
# 导入隐私管理器
from core.privacy_manager import PrivacyManager
import sys
import subprocess
import os
# 创建实例并重置隐私协议同意
privacy_manager = PrivacyManager()
if privacy_manager.reset_privacy_agreement():
# 显示重启提示
restart_msg = msgbox_frame(
f"操作成功 - {APP_NAME}",
"\n已成功撤回隐私协议同意。\n\n软件将立即重启。\n",
QMessageBox.StandardButton.Ok,
)
restart_msg.exec()
# 获取当前执行的Python解释器路径和脚本路径
python_executable = sys.executable
script_path = os.path.abspath(sys.argv[0])
# 构建重启命令
restart_cmd = [python_executable, script_path]
# 启动新进程
subprocess.Popen(restart_cmd)
# 退出当前进程
sys.exit(0)
else:
# 显示失败提示
fail_msg = msgbox_frame(
f"操作失败 - {APP_NAME}",
"\n撤回隐私协议同意失败。\n\n请检查应用权限或稍后再试。\n",
QMessageBox.StandardButton.Ok,
)
fail_msg.exec()
except Exception as e:
# 显示错误提示
error_msg = msgbox_frame(
f"错误 - {APP_NAME}",
f"\n撤回隐私协议同意时发生错误:\n\n{str(e)}\n",
QMessageBox.StandardButton.Ok,
)
error_msg.exec()
def show_about_dialog(self):
"""显示关于对话框"""
about_text = f"""
<p><b>{APP_NAME} v{APP_VERSION}</b></p>
<p>GitHub: <a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT">https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT</a></p>
<p>原作: <a href="https://github.com/Yanam1Anna">Yanam1Anna</a></p>
<p>此应用根据 <a href="https://github.com/hyb-oyqq/FRAISEMOE2-Installer/blob/master/LICENSE">GPL-3.0 许可证</a> 授权。</p>
<p>此应用根据 <a href="https://github.com/hyb-oyqq/FRAISEMOE-Addons-Installer-NEXT/blob/master/LICENSE">GPL-3.0 许可证</a> 授权。</p>
<br>
<p><b>感谢:</b></p>
<p>- <a href="https://github.com/HTony03">HTony03</a>:对原项目部分源码的重构、逻辑优化和功能实现提供了支持。</p>

View File

@@ -0,0 +1,46 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 隐私协议的缩略版内容
PRIVACY_POLICY_BRIEF = """
# FRAISEMOE2-Installer 隐私政策摘要
本应用在运行过程中会收集和处理以下信息:
## 收集的信息
- 系统信息:操作系统版本、程序版本号
- 网络信息IP 地址(Cloudflare 加速时)、域名解析、下载统计
- 文件信息:游戏安装路径、文件哈希值
## 系统修改
- 使用 Cloudflare 加速时会临时修改系统 hosts 文件
- 修改前会自动备份,程序退出时自动恢复
## 第三方服务
- Cloudflare 服务:用于测试和优化下载速度
- 云端配置服务:获取配置信息和下载链接
完整的隐私政策可在程序中访问github仓库查看。
"""
# 隐私协议的英文版缩略版内容
PRIVACY_POLICY_BRIEF_EN = """
# FRAISEMOE2-Installer Privacy Policy Summary
This application collects and processes the following information:
## Information Collected
- System info: OS version, application version
- Network info: IP address (for Cloudflare acceleration), DNS resolution, download statistics
- File info: Game installation paths, file hash values
## System Modifications
- Temporarily modifies system hosts file when using Cloudflare acceleration
- Automatically backs up before modification and restores upon exit
## Third-party Services
- Cloudflare services: Used for testing and optimizing download speeds
- Cloud configuration services: For obtaining configuration information and download links
The complete privacy policy can be found in the github repository of the program.
"""