add lot of things

This commit is contained in:
2025-09-28 17:33:42 +08:00
parent 0e54dce62a
commit abdee75504
101 changed files with 3739 additions and 234 deletions

View File

@@ -26,7 +26,7 @@ LeonApp GUI - 基于PyQt5和Fluent Design的App Store API图形界面工具
#
# APP版本号
APP_VERSION = "Prerelease 1"
APP_VERSION = "Prerelease 2"
import sys
import json
@@ -36,18 +36,19 @@ import os
import datetime
import markdown
from enum import Enum
from loguru import logger
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QTabWidget, QWidget, QVBoxLayout, QHBoxLayout,
QTableWidgetItem, QTableWidget, QTextEdit, QFrame, QHeaderView, QLabel
QTableWidgetItem, QTableWidget, QTextEdit, QFrame, QHeaderView, QLabel, QSplashScreen
)
from PyQt5.QtGui import QTextOption
from PyQt5.QtCore import Qt, pyqtSignal, QThread
from PyQt5.QtGui import QTextOption, QPixmap, QIcon
from PyQt5.QtCore import Qt, pyqtSignal, QThread, QSize
from qfluentwidgets import (
CardWidget, TitleLabel, SubtitleLabel, CaptionLabel, BodyLabel, PushButton,
PrimaryPushButton, LineEdit, ComboBox, ProgressBar, TableWidget,
ScrollArea, InfoBar, InfoBarPosition, NavigationInterface, NavigationItemPosition,
FluentWindow, MSFluentWindow, FluentIcon, SimpleCardWidget, PrimaryPushSettingCard,
OptionsSettingCard, QConfig, ConfigItem, OptionsConfigItem, BoolValidator, OptionsValidator, qconfig
OptionsSettingCard, QConfig, ConfigItem, OptionsConfigItem, BoolValidator, OptionsValidator, qconfig, SplashScreen
)
from qfluentwidgets import FluentTranslator
from app_detail_window import AppDetailWindow
@@ -67,6 +68,12 @@ class AppConfig(QConfig):
"General", "AppOpenMethod", 0,
OptionsValidator([0, 1])
)
# 云母效果
mica_effect = OptionsConfigItem(
"General", "MicaEffect", True,
OptionsValidator([True, False])
)
# 创建配置文件目录
config_dir = os.path.join(os.path.dirname(__file__), "config")
@@ -94,19 +101,38 @@ class APIClient:
# 添加API类型参数
params['t'] = endpoint_type
# 记录API请求开始
logger.info(f"API请求开始: {endpoint_type},参数: {params}")
try:
response = requests.get(self.api_base_url, params=params, timeout=30)
response.raise_for_status() # 抛出HTTP错误
logger.info(f"API请求成功: {endpoint_type},状态码: {response.status_code}")
data = response.json()
if data.get('status') == 'error':
return {'error': data.get('message', '未知错误')}
# 添加返回数据日志,限制数据量大小
import json
data_str = json.dumps(data, ensure_ascii=False)
if len(data_str) > 500:
# 如果数据太大,只记录部分内容
logger.info(f"API返回数据: {endpoint_type},数据长度: {len(data_str)} 字符前500字符: {data_str[:500]}...")
else:
logger.info(f"API返回数据: {endpoint_type},完整数据: {data_str}")
if data.get('status') == 'error':
error_msg = data.get('message', '未知错误')
logger.error(f"API返回错误: {endpoint_type} - {error_msg}")
return {'error': error_msg}
logger.info(f"API响应成功: {endpoint_type},数据获取完成")
return {'success': True, 'data': data.get('data')}
except requests.exceptions.RequestException as e:
logger.error(f"API请求异常: {endpoint_type} - {str(e)}")
return {'error': f"请求异常: {str(e)}"}
except json.JSONDecodeError:
logger.error(f"API响应解析失败: {endpoint_type}")
return {'error': "无法解析响应"}
class WorkerThread(QThread):
@@ -2656,6 +2682,33 @@ class SettingsTab(QWidget):
general_layout.addWidget(self.app_open_method_option)
# 添加云母效果选项
self.mica_effect_option = OptionsSettingCard(
icon=FluentIcon.INFO,
title="启用云母效果",
content="开启窗口的云母效果,提供更现代的视觉体验",
texts=["", ""],
configItem=app_config.mica_effect,
parent=general_card
)
# 连接信号
self.mica_effect_option.optionChanged.connect(self.on_mica_effect_changed)
general_layout.addWidget(self.mica_effect_option)
# 连接信号
self.mica_effect_option.optionChanged.connect(self.on_mica_effect_changed)
# 已经在前面添加过了,这里不需要重复添加
# 添加查看日志按钮
general_layout.addSpacing(20)
view_logs_button = PrimaryPushButton("查看应用日志", general_card)
view_logs_button.clicked.connect(self.show_logs)
general_layout.addWidget(view_logs_button)
# 将常规设置卡片添加到布局
self.scroll_layout.addWidget(general_card)
@@ -2675,6 +2728,231 @@ class SettingsTab(QWidget):
def on_app_open_method_changed(self, index):
"""应用打开方式设置变更处理"""
app_config.app_open_method = index
def on_mica_effect_changed(self, index):
"""云母效果设置变更处理"""
app_config.mica_effect = (index == 0)
# 显示提示,告知用户需要重启应用才能生效
from qfluentwidgets import InfoBar, InfoBarPosition
from PyQt5.QtWidgets import QApplication
# 创建Sweet Alert风格的提示
InfoBar.warning(
title="需要重启应用",
content="云母效果设置已更新,需要重启应用才能生效。",
orient=Qt.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=3000,
parent=self
)
def show_logs(self):
"""显示应用日志"""
from qfluentwidgets import InfoBar, InfoBarPosition
from qfluentwidgets import ScrollArea, PushButton, PrimaryPushButton
from qfluentwidgets import FluentIcon, CardWidget, TitleLabel, SubtitleLabel
from PyQt5.QtGui import QIcon
import os
import threading
import time
from PyQt5.QtWidgets import QDialog, QTextEdit, QVBoxLayout, QWidget, QHBoxLayout
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QObject
# 尝试找到日志文件
log_file_path = None
# 常见的日志文件位置
possible_log_paths = [
'./logs/leonapp_gui.log', # 主要日志文件路径从config.py中配置
'./leonapp.log',
'./logs/leonapp.log',
os.path.expanduser('~') + '/leonapp.log'
]
for path in possible_log_paths:
if os.path.exists(path):
log_file_path = path
break
# 如果找不到日志文件,显示提示
if not log_file_path:
InfoBar.warning(
title="提示",
content="未找到日志文件",
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=5000,
parent=self
)
return
# 定义日志更新信号类
class LogUpdateSignal(QObject):
log_updated = pyqtSignal(str)
log_signal = LogUpdateSignal()
# 创建Sweet Alert风格的弹窗
class LogDialog(QDialog):
def __init__(self, title, parent=None):
super().__init__(parent)
self.setWindowTitle(title)
self.setFixedSize(800, 600)
# 设置为非模态对话框,允许主窗口和日志窗口同时交互
self.setWindowModality(Qt.NonModal)
# 设置窗口样式
self.setStyleSheet("""
QDialog {
background-color: #f8f9fa;
border-radius: 12px;
border: 1px solid #e0e0e0;
}
""")
self.init_ui()
def init_ui(self):
# 创建主布局
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(20, 20, 20, 20)
# 创建标题
title_label = TitleLabel("应用日志", self)
main_layout.addWidget(title_label)
# 创建日志显示区域
self.log_text_edit = QTextEdit()
self.log_text_edit.setReadOnly(True)
self.log_text_edit.setStyleSheet("""
QTextEdit {
background-color: #2d2d2d;
color: #f8f8f2;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 12px;
border-radius: 8px;
padding: 15px;
border: none;
}
QScrollBar:vertical {
background-color: #3d3d3d;
width: 10px;
border-radius: 5px;
}
QScrollBar::handle:vertical {
background-color: #666666;
border-radius: 5px;
}
QScrollBar::add-line:vertical,
QScrollBar::sub-line:vertical {
height: 0px;
}
""")
main_layout.addWidget(self.log_text_edit, 1)
# 创建按钮区域
button_layout = QHBoxLayout()
button_layout.setContentsMargins(0, 15, 0, 0)
button_layout.setAlignment(Qt.AlignRight)
# 创建按钮
self.close_button = PrimaryPushButton("关闭", self)
self.close_button.setFixedWidth(100)
self.close_button.clicked.connect(self.accept)
button_layout.addWidget(self.close_button)
main_layout.addLayout(button_layout)
# 创建日志对话框
log_dialog = LogDialog("应用日志", self)
log_text_edit = log_dialog.log_text_edit
# 定义日志文件监听函数
def monitor_log_file():
# 初始时直接移动到文件末尾,只监控新的日志
try:
with open(log_file_path, 'r', encoding='utf-8') as f:
f.seek(0, os.SEEK_END) # 移动到文件末尾
last_position = f.tell()
except Exception:
last_position = 0
while log_dialog.isVisible():
try:
with open(log_file_path, 'r', encoding='utf-8') as f:
# 检查文件大小是否有变化
f.seek(0, os.SEEK_END)
current_size = f.tell()
if current_size > last_position:
# 文件有新增内容,读取并发送信号
f.seek(last_position)
new_logs = f.read()
if new_logs:
log_signal.log_updated.emit(new_logs)
last_position = current_size
elif current_size < last_position:
# 文件被截断(如日志轮转),从头开始读取
last_position = 0
except Exception as e:
# 如果文件被锁定或其他错误,忽略
pass
time.sleep(0.3) # 降低检查间隔,提高实时性
# 日志更新槽函数
def update_log_text(new_logs):
current_text = log_text_edit.toPlainText()
# 限制日志显示的行数,避免内存占用过大
lines = (current_text + new_logs).splitlines()
if len(lines) > 1000:
lines = lines[-1000:]
# 使用QTimer延迟设置文本确保UI响应
from PyQt5.QtCore import QTimer
def set_log_text():
log_text_edit.setPlainText('\n'.join(lines))
# 强制滚动到底部,确保显示最新日志
log_text_edit.verticalScrollBar().setValue(log_text_edit.verticalScrollBar().maximum())
# 使用QTimer的单次触发确保滚动生效
QTimer.singleShot(0, lambda: log_text_edit.verticalScrollBar().setValue(log_text_edit.verticalScrollBar().maximum()))
QTimer.singleShot(0, set_log_text)
# 连接信号和槽
log_signal.log_updated.connect(update_log_text)
# 初始加载日志,优先显示最新日志
try:
with open(log_file_path, 'r', encoding='utf-8') as f:
# 直接获取文件大小
f.seek(0, os.SEEK_END)
file_size = f.tell()
# 只读取文件末尾的内容约100KB确保优先显示最新日志
# 如果文件较小,则读取全部内容
chunk_size = min(100 * 1024, file_size)
f.seek(max(0, file_size - chunk_size))
logs = f.read()
lines = logs.splitlines()
if len(lines) > 1000:
lines = lines[-1000:] # 确保只显示最新的1000行
log_text_edit.setPlainText('\n'.join(lines))
# 确保滚动到底部,显示最新日志
# 使用QTimer确保UI渲染完成后再滚动
from PyQt5.QtCore import QTimer
QTimer.singleShot(0, lambda: log_text_edit.verticalScrollBar().setValue(log_text_edit.verticalScrollBar().maximum()))
# 再次触发滚动,确保生效
QTimer.singleShot(100, lambda: log_text_edit.verticalScrollBar().setValue(log_text_edit.verticalScrollBar().maximum()))
except Exception as e:
log_text_edit.setPlainText(f"无法读取日志文件: {str(e)}")
# 启动日志监听线程
log_thread = threading.Thread(target=monitor_log_file, daemon=True)
log_thread.start()
# 显示弹窗(非模态)
log_dialog.show()
class LeonAppGUI(MSFluentWindow):
"""主窗口类"""
@@ -2694,13 +2972,22 @@ class LeonAppGUI(MSFluentWindow):
if os.path.exists(icon_path):
self.setWindowIcon(QIcon(icon_path))
# 启用亚克力效果
# 根据配置启用云母效果
from qfluentwidgets import isDarkTheme
if hasattr(self, 'setAcrylicEffect'):
self.setAcrylicEffect(True)
elif hasattr(self, 'setWindowOpacity'):
# 如果没有直接的亚克力效果方法,设置窗口透明度作为替代
self.setWindowOpacity(0.95)
if app_config.mica_effect:
# 优先尝试设置云母效果
if hasattr(self, 'setMicaEffect'):
self.setMicaEffect(True)
# 如果没有直接的云母效果方法,尝试使用亚克力效果作为替代
elif hasattr(self, 'setAcrylicEffect'):
self.setAcrylicEffect(True)
# 如果都没有,使用窗口透明度作为最后的替代
elif hasattr(self, 'setWindowOpacity'):
self.setWindowOpacity(0.95)
else:
# 禁用效果,恢复为完全不透明
if hasattr(self, 'setWindowOpacity'):
self.setWindowOpacity(1.0)
# 初始化UI
self.init_ui()
@@ -2747,6 +3034,7 @@ class LeonAppGUI(MSFluentWindow):
def show_app_detail(self, app_id):
"""显示应用详情"""
logger.info(f"用户查看应用详情应用ID: {app_id}")
detail_window = AppDetailWindow(self.api_client, app_id, self)
detail_window.show()
@@ -2789,32 +3077,86 @@ def show_error_dialog(title, message):
msg_box.setText(message)
msg_box.setStandardButtons(QMessageBox.Ok)
# 记录到日志
logger.error(f"{title}: {message}")
# 显示弹窗
msg_box.exec_()
def log_error(error_message):
"""记录错误日志到文件"""
try:
# 创建logs目录如果不存在
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
if not os.path.exists(log_dir):
os.makedirs(log_dir)
# 生成日志文件名(使用当前日期)
today = datetime.date.today().strftime("%Y-%m-%d")
log_file = os.path.join(log_dir, f"error_{today}.log")
# 写入日志
with open(log_file, "a", encoding="utf-8") as f:
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
f.write(f"[{timestamp}]\n{error_message}\n\n")
except Exception:
# 如果日志记录失败,不影响程序运行
pass
logger.error(error_message)
def main():
"""主函数"""
try:
# 配置loguru日志系统
from config import LOG_CONFIG
import logging
import os
import sys
# 设置日志文件路径
log_dir = os.path.dirname(os.path.abspath(LOG_CONFIG['LOG_FILE']))
if not os.path.exists(log_dir):
os.makedirs(log_dir)
# 移除默认的控制台输出
logger.remove()
# 添加控制台输出
logger.add(
sys.stdout,
level=LOG_CONFIG['LOG_LEVEL'],
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>"
)
# 添加文件输出(如果配置了)
if LOG_CONFIG['LOG_TO_FILE']:
logger.add(
LOG_CONFIG['LOG_FILE'],
level=LOG_CONFIG['LOG_LEVEL'],
rotation="10 MB", # 当日志文件达到10MB时旋转
retention=LOG_CONFIG['LOG_BACKUP_COUNT'], # 保留的备份文件数量
compression="zip", # 压缩旧日志
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
encoding="utf-8"
)
# 配置PyQt5的日志转发到loguru
class LoguruHandler(logging.Handler):
def emit(self, record):
# 获取日志级别对应的loguru方法
level = getattr(logger, record.levelname.lower(), logger.info)
level(self.format(record))
# 获取PyQt5的根日志记录器
qt_logger = logging.getLogger("PyQt5")
# 设置级别
qt_logger.setLevel(getattr(logging, LOG_CONFIG['LOG_LEVEL']))
# 添加自定义处理器
handler = LoguruHandler()
handler.setFormatter(logging.Formatter("%(name)s - %(levelname)s - %(message)s"))
qt_logger.addHandler(handler)
# 确保所有子记录器也使用这个处理器
qt_logger.propagate = False
# 也处理PyQt的其他可能的日志记录器
for logger_name in ["PyQt", "Qt", "QApplication", "QMainWindow", "QWidget", "QtCore", "QtGui", "QtWidgets"]:
qt_logger = logging.getLogger(logger_name)
qt_logger.setLevel(getattr(logging, LOG_CONFIG['LOG_LEVEL']))
qt_logger.addHandler(handler)
qt_logger.propagate = False
# 设置Python标准库logging的根记录器也使用我们的处理器
root_logger = logging.getLogger()
root_logger.setLevel(getattr(logging, LOG_CONFIG['LOG_LEVEL']))
root_logger.addHandler(handler)
logger.info("日志系统配置完成PyQt5相关日志已集成到loguru")
logger.info(f"LeonApp {APP_VERSION} 启动")
# 创建应用实例
app = QApplication(sys.argv)
@@ -2822,10 +3164,44 @@ def main():
translator = FluentTranslator()
app.installTranslator(translator)
# 创建并显示主窗口
# 加载并应用QFluentWidgets主题色配置
import json
import os
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config', 'config.json')
try:
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
theme_color = config.get('QFluentWidgets', {}).get('ThemeColor', '#4169E1')
from qfluentwidgets import setThemeColor
setThemeColor(theme_color)
logger.info(f"已设置QFluentWidgets主题色: {theme_color}")
except Exception as e:
logger.warning(f"加载主题色配置失败: {e}")
# 使用默认主题色
from qfluentwidgets import setThemeColor
setThemeColor('#4169E1')
# 创建主窗口实例
window = LeonAppGUI()
# 设置窗口图标
logo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets/logo.jpeg")
if os.path.exists(logo_path):
window.setWindowIcon(QIcon(logo_path))
# 创建QFluentWidgets的启动页面
splash = SplashScreen(window.windowIcon(), window)
splash.setIconSize(QSize(102, 102))
# 在创建其他子页面前先显示主界面
window.show()
# 确保启动画面显示
app.processEvents()
# 隐藏启动页面
splash.finish()
# 运行应用
sys.exit(app.exec_())
except Exception as e: