feat: 添加应用详情窗口和更新检查功能
- 实现全新的应用详情窗口,包含统计信息、基本信息和描述展示 - 添加应用更新检查功能到CLI工具 - 优化版本列表页面的文件路径处理逻辑 - 升级GUI版本至Beta 0.4 - 增强公告详情页面的链接处理能力
This commit is contained in:
BIN
APP Store.zip
BIN
APP Store.zip
Binary file not shown.
@@ -14,6 +14,9 @@ from datetime import datetime
|
|||||||
import requests
|
import requests
|
||||||
from colorama import init, Fore, Style
|
from colorama import init, Fore, Style
|
||||||
|
|
||||||
|
# 当前版本
|
||||||
|
APP_VERSION = "Beta 0.3"
|
||||||
|
|
||||||
# 初始化colorama
|
# 初始化colorama
|
||||||
def init_colorama():
|
def init_colorama():
|
||||||
"""初始化colorama,确保在Windows和其他平台上都能正确显示彩色文本"""
|
"""初始化colorama,确保在Windows和其他平台上都能正确显示彩色文本"""
|
||||||
@@ -327,10 +330,10 @@ class LeonAppCLI:
|
|||||||
|
|
||||||
elif command == 'stats':
|
elif command == 'stats':
|
||||||
self.get_count_info()
|
self.get_count_info()
|
||||||
|
elif command == 'check-update':
|
||||||
|
self.check_update()
|
||||||
else:
|
else:
|
||||||
self.print_error("未知命令,请输入 'help' 获取帮助")
|
self.print_error("未知命令,请输入 'help' 获取帮助")
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print()
|
print()
|
||||||
self.print_info("按 'exit' 退出")
|
self.print_info("按 'exit' 退出")
|
||||||
@@ -350,6 +353,7 @@ class LeonAppCLI:
|
|||||||
print(Fore.CYAN + "developer info [id]" + Fore.WHITE + " - 查看开发者信息")
|
print(Fore.CYAN + "developer info [id]" + Fore.WHITE + " - 查看开发者信息")
|
||||||
print(Fore.CYAN + "list announcements" + Fore.WHITE + " - 列出所有公告")
|
print(Fore.CYAN + "list announcements" + Fore.WHITE + " - 列出所有公告")
|
||||||
print(Fore.CYAN + "stats" + Fore.WHITE + " - 查看统计信息")
|
print(Fore.CYAN + "stats" + Fore.WHITE + " - 查看统计信息")
|
||||||
|
print(Fore.CYAN + "check-update" + Fore.WHITE + " - 检查更新")
|
||||||
|
|
||||||
def parse_arguments(self):
|
def parse_arguments(self):
|
||||||
"""解析命令行参数"""
|
"""解析命令行参数"""
|
||||||
@@ -399,6 +403,46 @@ class LeonAppCLI:
|
|||||||
|
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
def check_update(self):
|
||||||
|
"""检查更新功能"""
|
||||||
|
self.print_header("检查更新")
|
||||||
|
self.print_info("正在检查更新...")
|
||||||
|
|
||||||
|
# 获取LeonAPP的版本信息(App ID为15)
|
||||||
|
data = self.make_api_request('getappversions', {'id': 15})
|
||||||
|
|
||||||
|
if data:
|
||||||
|
try:
|
||||||
|
# 检查数据格式
|
||||||
|
if not isinstance(data, dict) or 'versions' not in data:
|
||||||
|
self.print_error("获取版本信息失败:数据格式不正确")
|
||||||
|
return
|
||||||
|
|
||||||
|
versions = data.get('versions', [])
|
||||||
|
if not versions:
|
||||||
|
self.print_error("未找到任何版本信息")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 最新版本通常在列表的第一个
|
||||||
|
latest_version = versions[0].get('version', '未知')
|
||||||
|
current_version = APP_VERSION
|
||||||
|
|
||||||
|
# 比较版本号
|
||||||
|
if latest_version != current_version:
|
||||||
|
# 版本不一致,显示更新提示
|
||||||
|
self.print_separator()
|
||||||
|
self.print_success(f"发现新版本:{latest_version}")
|
||||||
|
self.print_info(f"当前版本:{current_version}")
|
||||||
|
self.print_info("建议前往应用商店下载最新版本。")
|
||||||
|
self.print_separator()
|
||||||
|
else:
|
||||||
|
# 已是最新版本
|
||||||
|
self.print_success(f"已是最新版本:{current_version}")
|
||||||
|
except Exception as e:
|
||||||
|
self.print_error(f"处理版本信息时发生错误:{str(e)}")
|
||||||
|
else:
|
||||||
|
self.print_error("检查更新失败,请稍后重试。")
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""运行CLI"""
|
"""运行CLI"""
|
||||||
# 解析命令行参数
|
# 解析命令行参数
|
||||||
@@ -421,6 +465,8 @@ class LeonAppCLI:
|
|||||||
self.list_all_announcements(args.page, args.limit)
|
self.list_all_announcements(args.page, args.limit)
|
||||||
elif args.command == 'stats':
|
elif args.command == 'stats':
|
||||||
self.get_count_info()
|
self.get_count_info()
|
||||||
|
elif args.command == 'check-update':
|
||||||
|
self.check_update()
|
||||||
elif args.command == 'interactive':
|
elif args.command == 'interactive':
|
||||||
self.interactive_mode()
|
self.interactive_mode()
|
||||||
else:
|
else:
|
||||||
|
|||||||
Binary file not shown.
@@ -1,10 +1,60 @@
|
|||||||
from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
QPushButton)
|
QPushButton, QFrame, QLabel, QGridLayout)
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt, QSize, pyqtSignal, QThread
|
||||||
|
from PyQt5.QtGui import QPixmap, QFont
|
||||||
from qfluentwidgets import (InfoBar, InfoBarPosition, TitleLabel, SubtitleLabel,
|
from qfluentwidgets import (InfoBar, InfoBarPosition, TitleLabel, SubtitleLabel,
|
||||||
PrimaryPushButton, PushButton, ScrollArea, CardWidget,
|
PrimaryPushButton, PushButton, ScrollArea, CardWidget,
|
||||||
FluentIcon, SimpleCardWidget, BodyLabel)
|
FluentIcon, SimpleCardWidget, BodyLabel)
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import subprocess
|
||||||
|
import platform
|
||||||
|
|
||||||
|
class DownloadThread(QThread):
|
||||||
|
"""下载线程,用于在后台下载文件"""
|
||||||
|
progress = pyqtSignal(int)
|
||||||
|
finished = pyqtSignal(str)
|
||||||
|
error = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, download_url, save_path):
|
||||||
|
super().__init__()
|
||||||
|
self.download_url = download_url
|
||||||
|
self.save_path = save_path
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""线程运行函数"""
|
||||||
|
try:
|
||||||
|
# 发送请求
|
||||||
|
with requests.get(self.download_url, stream=True, timeout=30) as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# 获取文件大小
|
||||||
|
total_size = int(response.headers.get('content-length', 0))
|
||||||
|
downloaded_size = 0
|
||||||
|
|
||||||
|
# 创建保存目录
|
||||||
|
os.makedirs(os.path.dirname(self.save_path), exist_ok=True)
|
||||||
|
|
||||||
|
# 下载文件
|
||||||
|
with open(self.save_path, 'wb') as file:
|
||||||
|
for chunk in response.iter_content(chunk_size=8192):
|
||||||
|
if chunk:
|
||||||
|
file.write(chunk)
|
||||||
|
downloaded_size += len(chunk)
|
||||||
|
|
||||||
|
# 计算进度
|
||||||
|
if total_size > 0:
|
||||||
|
progress = int(downloaded_size / total_size * 100)
|
||||||
|
self.progress.emit(progress)
|
||||||
|
|
||||||
|
# 下载完成
|
||||||
|
self.finished.emit(self.save_path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.error.emit(f"下载失败: {str(e)}")
|
||||||
|
|
||||||
class AppDetailWindow(QMainWindow):
|
class AppDetailWindow(QMainWindow):
|
||||||
def __init__(self, api_client, app_id, parent=None):
|
def __init__(self, api_client, app_id, parent=None):
|
||||||
@@ -15,13 +65,29 @@ class AppDetailWindow(QMainWindow):
|
|||||||
|
|
||||||
# 设置窗口属性
|
# 设置窗口属性
|
||||||
self.setWindowTitle("应用详情")
|
self.setWindowTitle("应用详情")
|
||||||
self.resize(800, 600)
|
self.resize(850, 650)
|
||||||
self.setObjectName("AppDetailWindow")
|
self.setObjectName("AppDetailWindow")
|
||||||
|
|
||||||
# 添加全局样式
|
# 添加全局样式 - 现代扁平化设计
|
||||||
self.setStyleSheet("""
|
self.setStyleSheet("""
|
||||||
#AppDetailWindow {
|
#AppDetailWindow {
|
||||||
background-color: #F2F3F5;
|
background-color: #F5F7FA;
|
||||||
|
}
|
||||||
|
QLabel {
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
#AppTitle {
|
||||||
|
color: #1A1A1A;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
#StatusBadge {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
#DeveloperLabel {
|
||||||
|
color: #666666;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
@@ -31,20 +97,18 @@ class AppDetailWindow(QMainWindow):
|
|||||||
|
|
||||||
# 创建主布局
|
# 创建主布局
|
||||||
self.main_layout = QVBoxLayout(self.central_widget)
|
self.main_layout = QVBoxLayout(self.central_widget)
|
||||||
self.main_layout.setContentsMargins(20, 20, 20, 20)
|
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
self.main_layout.setSpacing(12)
|
self.main_layout.setSpacing(0)
|
||||||
|
|
||||||
# 创建标题区域
|
# 创建自定义顶部区域(非传统顶栏)
|
||||||
self.header_layout = QHBoxLayout()
|
self.create_custom_header()
|
||||||
self.app_title = TitleLabel("加载中...")
|
|
||||||
self.app_title.setObjectName("AppTitle")
|
|
||||||
self.close_button = PushButton("关闭")
|
|
||||||
self.close_button.clicked.connect(self.close)
|
|
||||||
self.close_button.setFixedWidth(80)
|
|
||||||
|
|
||||||
self.header_layout.addWidget(self.app_title)
|
# 添加分隔线
|
||||||
self.header_layout.addStretch()
|
separator = QFrame()
|
||||||
self.header_layout.addWidget(self.close_button)
|
separator.setFrameShape(QFrame.HLine)
|
||||||
|
separator.setFrameShadow(QFrame.Sunken)
|
||||||
|
separator.setStyleSheet("background-color: #E5E6EB;")
|
||||||
|
self.main_layout.addWidget(separator)
|
||||||
|
|
||||||
# 创建滚动区域 - 使用QFluentWidgets的ScrollArea
|
# 创建滚动区域 - 使用QFluentWidgets的ScrollArea
|
||||||
self.scroll_area = ScrollArea()
|
self.scroll_area = ScrollArea()
|
||||||
@@ -55,20 +119,26 @@ class AppDetailWindow(QMainWindow):
|
|||||||
# 设置滚动区域样式
|
# 设置滚动区域样式
|
||||||
self.scroll_area.setStyleSheet("""
|
self.scroll_area.setStyleSheet("""
|
||||||
ScrollArea {
|
ScrollArea {
|
||||||
background-color: transparent;
|
background-color: #F5F7FA;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
QScrollBar:vertical {
|
QScrollBar:vertical {
|
||||||
width: 8px;
|
width: 10px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
QScrollBar::handle:vertical {
|
QScrollBar::handle:vertical {
|
||||||
background: rgba(142, 142, 147, 0.3);
|
background: rgba(142, 142, 147, 0.2);
|
||||||
border-radius: 4px;
|
border-radius: 5px;
|
||||||
min-height: 40px;
|
min-height: 50px;
|
||||||
}
|
}
|
||||||
QScrollBar::handle:vertical:hover {
|
QScrollBar::handle:vertical:hover {
|
||||||
background: rgba(142, 142, 147, 0.5);
|
background: rgba(142, 142, 147, 0.4);
|
||||||
|
}
|
||||||
|
QScrollBar::add-line:vertical,
|
||||||
|
QScrollBar::sub-line:vertical {
|
||||||
|
height: 0px;
|
||||||
|
width: 0px;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
@@ -76,38 +146,159 @@ class AppDetailWindow(QMainWindow):
|
|||||||
self.scroll_content = QWidget()
|
self.scroll_content = QWidget()
|
||||||
self.scroll_content.setObjectName("ScrollContent")
|
self.scroll_content.setObjectName("ScrollContent")
|
||||||
self.scroll_layout = QVBoxLayout(self.scroll_content)
|
self.scroll_layout = QVBoxLayout(self.scroll_content)
|
||||||
self.scroll_layout.setContentsMargins(0, 0, 0, 20)
|
self.scroll_layout.setContentsMargins(20, 20, 20, 30)
|
||||||
self.scroll_layout.setSpacing(16)
|
self.scroll_layout.setSpacing(20)
|
||||||
|
|
||||||
# 添加滚动区域到主布局
|
# 添加滚动区域到主布局
|
||||||
self.scroll_area.setWidget(self.scroll_content)
|
self.scroll_area.setWidget(self.scroll_content)
|
||||||
|
|
||||||
# 将标题区域和滚动区域添加到主布局
|
|
||||||
self.main_layout.addLayout(self.header_layout)
|
|
||||||
self.main_layout.addWidget(self.scroll_area)
|
self.main_layout.addWidget(self.scroll_area)
|
||||||
|
|
||||||
# 加载应用详情
|
# 加载应用详情
|
||||||
self.load_app_detail()
|
self.load_app_detail()
|
||||||
|
|
||||||
|
def create_custom_header(self):
|
||||||
|
"""创建自定义顶部区域"""
|
||||||
|
self.header_widget = QWidget()
|
||||||
|
self.header_widget.setObjectName("HeaderWidget")
|
||||||
|
self.header_widget.setStyleSheet("""
|
||||||
|
#HeaderWidget {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
padding: 15px 20px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.header_layout = QHBoxLayout(self.header_widget)
|
||||||
|
self.header_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.header_layout.setSpacing(15)
|
||||||
|
|
||||||
|
# 应用图标占位 - 使用QLabel替换Avatar
|
||||||
|
self.app_icon = QLabel()
|
||||||
|
self.app_icon.setFixedSize(60, 60)
|
||||||
|
self.app_icon.setStyleSheet("background-color: #4CAF50; border-radius: 12px;")
|
||||||
|
|
||||||
|
# 应用信息布局
|
||||||
|
self.app_info_layout = QVBoxLayout()
|
||||||
|
self.app_info_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.app_info_layout.setSpacing(5)
|
||||||
|
|
||||||
|
# 标题和状态布局
|
||||||
|
self.title_status_layout = QHBoxLayout()
|
||||||
|
self.title_status_layout.setSpacing(10)
|
||||||
|
|
||||||
|
# 应用标题
|
||||||
|
self.app_title = TitleLabel("加载中...")
|
||||||
|
self.app_title.setObjectName("AppTitle")
|
||||||
|
self.title_status_layout.addWidget(self.app_title)
|
||||||
|
|
||||||
|
# 应用状态标签
|
||||||
|
self.status_badge = QLabel("- ")
|
||||||
|
self.status_badge.setObjectName("StatusBadge")
|
||||||
|
self.status_badge.setStyleSheet("background-color: #E5E6EB; color: #666666;")
|
||||||
|
self.status_badge.setAlignment(Qt.AlignCenter)
|
||||||
|
self.status_badge.setFixedHeight(24)
|
||||||
|
self.title_status_layout.addWidget(self.status_badge)
|
||||||
|
self.title_status_layout.addStretch()
|
||||||
|
|
||||||
|
# 开发者标签
|
||||||
|
self.developer_label = QLabel("开发者: 加载中...")
|
||||||
|
self.developer_label.setObjectName("DeveloperLabel")
|
||||||
|
|
||||||
|
# 添加到应用信息布局
|
||||||
|
self.app_info_layout.addLayout(self.title_status_layout)
|
||||||
|
self.app_info_layout.addWidget(self.developer_label)
|
||||||
|
|
||||||
|
# 右侧按钮布局
|
||||||
|
self.button_layout = QVBoxLayout()
|
||||||
|
self.button_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.button_layout.setSpacing(8)
|
||||||
|
|
||||||
|
# 关闭按钮
|
||||||
|
self.close_button = PushButton("关闭")
|
||||||
|
self.close_button.clicked.connect(self.close)
|
||||||
|
self.close_button.setFixedSize(90, 32)
|
||||||
|
self.button_layout.addWidget(self.close_button)
|
||||||
|
self.button_layout.addStretch()
|
||||||
|
|
||||||
|
# 添加到主头部布局
|
||||||
|
self.header_layout.addWidget(self.app_icon)
|
||||||
|
self.header_layout.addLayout(self.app_info_layout, 1)
|
||||||
|
self.header_layout.addLayout(self.button_layout)
|
||||||
|
|
||||||
|
# 添加到主布局
|
||||||
|
self.main_layout.addWidget(self.header_widget)
|
||||||
|
|
||||||
def load_app_detail(self):
|
def load_app_detail(self):
|
||||||
"""加载应用详情"""
|
"""加载应用详情"""
|
||||||
try:
|
try:
|
||||||
# 这里应该调用API获取应用详情
|
# 使用API客户端调用getappinfo端点获取应用详情
|
||||||
# 暂时使用模拟数据
|
result = self.api_client.make_request('getappinfo', {'id': self.app_id})
|
||||||
app_data = {
|
|
||||||
|
# 检查API调用是否成功
|
||||||
|
if isinstance(result, dict) and 'success' in result and result['success']:
|
||||||
|
# 保存应用数据到实例属性
|
||||||
|
self.app_data = result['data']
|
||||||
|
|
||||||
|
# 为了保持向后兼容性,也保留局部变量
|
||||||
|
app_data = self.app_data
|
||||||
|
else:
|
||||||
|
# API调用失败,显示错误信息
|
||||||
|
error_msg = result.get('error', '未知错误') if isinstance(result, dict) else 'API调用失败'
|
||||||
|
InfoBar.error(
|
||||||
|
title="错误",
|
||||||
|
content=f"加载应用详情失败: {error_msg}",
|
||||||
|
orient=Qt.Horizontal,
|
||||||
|
isClosable=True,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
parent=self
|
||||||
|
)
|
||||||
|
# 使用默认数据
|
||||||
|
self.app_data = {
|
||||||
"id": self.app_id,
|
"id": self.app_id,
|
||||||
"name": "示例应用",
|
"name": "未知应用",
|
||||||
"description": "这是一个示例应用,用于展示应用详情页面",
|
"description": "无法加载应用详情",
|
||||||
"developer_id": "1",
|
"developer_id": "0",
|
||||||
"developer_name": "示例开发者",
|
"developer_name": "未知开发者",
|
||||||
"status": "approved",
|
"status": "unknown",
|
||||||
"version": "1.0.0",
|
"version": "0.0.0",
|
||||||
"created_at": "2023-01-01 10:00:00"
|
"created_at": "-",
|
||||||
|
"downloads": "0",
|
||||||
|
"rating": "0.0",
|
||||||
|
"category": "未知"
|
||||||
}
|
}
|
||||||
|
app_data = self.app_data
|
||||||
|
|
||||||
# 更新窗口标题
|
# 更新窗口标题
|
||||||
self.setWindowTitle(f"应用详情 - {app_data.get('name', '未知应用')}")
|
self.setWindowTitle(f"应用详情 - {app_data.get('name', '未知应用')}")
|
||||||
|
|
||||||
|
# 更新头部信息
|
||||||
self.app_title.setText(app_data.get('name', '未知应用'))
|
self.app_title.setText(app_data.get('name', '未知应用'))
|
||||||
|
# 改进开发者信息处理,确保能正确显示开发者名称
|
||||||
|
developer_name = app_data.get('developer_name', '')
|
||||||
|
# 如果developer_name为空,尝试从其他可能的字段获取
|
||||||
|
if not developer_name:
|
||||||
|
developer_name = app_data.get('developer', '')
|
||||||
|
# 如果developer也为空,尝试使用developer_email
|
||||||
|
if not developer_name:
|
||||||
|
developer_name = app_data.get('developer_email', '未知开发者')
|
||||||
|
self.developer_label.setText(f"开发者: {developer_name}")
|
||||||
|
|
||||||
|
# 根据应用状态设置状态标签样式
|
||||||
|
status = app_data.get('status', 'unknown')
|
||||||
|
status_text = {}
|
||||||
|
status_styles = {}
|
||||||
|
|
||||||
|
status_text['approved'] = '已通过'
|
||||||
|
status_text['pending'] = '审核中'
|
||||||
|
status_text['rejected'] = '已拒绝'
|
||||||
|
status_text['unknown'] = '未知状态'
|
||||||
|
|
||||||
|
status_styles['approved'] = 'background-color: #E8F5E9; color: #2E7D32;'
|
||||||
|
status_styles['pending'] = 'background-color: #FFF3E0; color: #E65100;'
|
||||||
|
status_styles['rejected'] = 'background-color: #FFEBEE; color: #C62828;'
|
||||||
|
status_styles['unknown'] = 'background-color: #E5E6EB; color: #666666;'
|
||||||
|
|
||||||
|
self.status_badge.setText(status_text.get(status, '未知状态'))
|
||||||
|
self.status_badge.setStyleSheet(status_styles.get(status, status_styles['unknown']))
|
||||||
|
|
||||||
# 清空滚动区域
|
# 清空滚动区域
|
||||||
for i in reversed(range(self.scroll_layout.count())):
|
for i in reversed(range(self.scroll_layout.count())):
|
||||||
@@ -116,6 +307,9 @@ class AppDetailWindow(QMainWindow):
|
|||||||
widget.setParent(None)
|
widget.setParent(None)
|
||||||
widget.deleteLater()
|
widget.deleteLater()
|
||||||
|
|
||||||
|
# 显示统计信息卡片
|
||||||
|
self.display_stats_card(app_data)
|
||||||
|
|
||||||
# 显示应用基本信息卡片
|
# 显示应用基本信息卡片
|
||||||
self.display_app_info_card(app_data)
|
self.display_app_info_card(app_data)
|
||||||
|
|
||||||
@@ -136,39 +330,132 @@ class AppDetailWindow(QMainWindow):
|
|||||||
parent=self
|
parent=self
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def display_stats_card(self, app_data):
|
||||||
|
"""显示应用统计信息卡片"""
|
||||||
|
stats_card = CardWidget()
|
||||||
|
stats_card.setObjectName("StatsCard")
|
||||||
|
stats_card.setStyleSheet("""
|
||||||
|
#StatsCard {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
#StatsCard QLabel {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
card_layout = QHBoxLayout(stats_card)
|
||||||
|
card_layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
card_layout.setSpacing(25)
|
||||||
|
|
||||||
|
# 下载量统计项
|
||||||
|
downloads_item = QVBoxLayout()
|
||||||
|
downloads_label = BodyLabel("下载量")
|
||||||
|
downloads_label.setStyleSheet("font-size: 13px; opacity: 0.9;")
|
||||||
|
downloads_value = TitleLabel(app_data.get("downloads", "0"))
|
||||||
|
downloads_value.setStyleSheet("font-size: 24px; font-weight: bold;")
|
||||||
|
downloads_item.addWidget(downloads_label)
|
||||||
|
downloads_item.addWidget(downloads_value)
|
||||||
|
downloads_item.setAlignment(Qt.AlignCenter)
|
||||||
|
|
||||||
|
# 评分统计项
|
||||||
|
rating_item = QVBoxLayout()
|
||||||
|
rating_label = BodyLabel("评分")
|
||||||
|
rating_label.setStyleSheet("font-size: 13px; opacity: 0.9;")
|
||||||
|
rating_value = TitleLabel(app_data.get("rating", "0.0"))
|
||||||
|
rating_value.setStyleSheet("font-size: 24px; font-weight: bold;")
|
||||||
|
rating_item.addWidget(rating_label)
|
||||||
|
rating_item.addWidget(rating_value)
|
||||||
|
rating_item.setAlignment(Qt.AlignCenter)
|
||||||
|
|
||||||
|
# 分类统计项
|
||||||
|
category_item = QVBoxLayout()
|
||||||
|
category_label = BodyLabel("分类")
|
||||||
|
category_label.setStyleSheet("font-size: 13px; opacity: 0.9;")
|
||||||
|
category_value = TitleLabel(app_data.get("category", "未分类"))
|
||||||
|
category_value.setStyleSheet("font-size: 24px; font-weight: bold;")
|
||||||
|
category_item.addWidget(category_label)
|
||||||
|
category_item.addWidget(category_value)
|
||||||
|
category_item.setAlignment(Qt.AlignCenter)
|
||||||
|
|
||||||
|
# 添加到卡片布局
|
||||||
|
card_layout.addLayout(downloads_item)
|
||||||
|
card_layout.addLayout(rating_item)
|
||||||
|
card_layout.addLayout(category_item)
|
||||||
|
|
||||||
|
self.scroll_layout.addWidget(stats_card)
|
||||||
|
|
||||||
def display_app_info_card(self, app_data):
|
def display_app_info_card(self, app_data):
|
||||||
"""显示应用基本信息卡片"""
|
"""显示应用基本信息卡片"""
|
||||||
info_card = CardWidget()
|
info_card = CardWidget()
|
||||||
info_card.setObjectName("InfoCard")
|
info_card.setObjectName("InfoCard")
|
||||||
|
info_card.setStyleSheet("""
|
||||||
|
#InfoCard {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
card_layout = QVBoxLayout(info_card)
|
card_layout = QVBoxLayout(info_card)
|
||||||
card_layout.setContentsMargins(16, 16, 16, 16)
|
card_layout.setContentsMargins(20, 20, 20, 20)
|
||||||
card_layout.setSpacing(12)
|
card_layout.setSpacing(15)
|
||||||
|
|
||||||
|
# 卡片标题和图标
|
||||||
|
title_layout = QHBoxLayout()
|
||||||
|
title_icon = QLabel()
|
||||||
|
title_icon.setPixmap(FluentIcon.INFO.icon().pixmap(18, 18))
|
||||||
|
title_icon.setStyleSheet("color: #4CAF50;")
|
||||||
|
|
||||||
# 卡片标题
|
|
||||||
card_title = SubtitleLabel("基本信息")
|
card_title = SubtitleLabel("基本信息")
|
||||||
card_layout.addWidget(card_title)
|
card_title.setStyleSheet("font-size: 16px; font-weight: 600;")
|
||||||
|
|
||||||
# 信息网格布局
|
title_layout.addWidget(title_icon)
|
||||||
info_grid_layout = QVBoxLayout()
|
title_layout.addWidget(card_title)
|
||||||
info_grid_layout.setSpacing(6)
|
title_layout.addStretch()
|
||||||
|
card_layout.addLayout(title_layout)
|
||||||
|
|
||||||
|
# 信息网格布局 - 使用两列布局
|
||||||
|
info_grid_layout = QGridLayout()
|
||||||
|
info_grid_layout.setSpacing(12)
|
||||||
|
info_grid_layout.setColumnStretch(0, 1)
|
||||||
|
info_grid_layout.setColumnStretch(1, 1)
|
||||||
|
|
||||||
|
# 添加基本信息字段,对开发者信息进行特殊处理
|
||||||
|
developer_name = app_data.get("developer_name", "")
|
||||||
|
if not developer_name:
|
||||||
|
developer_name = app_data.get("developer", "--")
|
||||||
|
|
||||||
# 添加基本信息字段
|
|
||||||
info_items = [
|
info_items = [
|
||||||
("应用ID", app_data.get("id", "--")),
|
("应用ID", app_data.get("id", "--")),
|
||||||
("开发者", app_data.get("developer_name", "--")),
|
("开发者", developer_name),
|
||||||
("状态", app_data.get("status", "--")),
|
|
||||||
("当前版本", app_data.get("version", "--")),
|
("当前版本", app_data.get("version", "--")),
|
||||||
("创建时间", app_data.get("created_at", "--"))
|
("创建时间", app_data.get("created_at", "--"))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
row = 0
|
||||||
for label_text, value_text in info_items:
|
for label_text, value_text in info_items:
|
||||||
info_row = QHBoxLayout()
|
# 创建标签和值
|
||||||
|
label = QLabel(f"{label_text}:")
|
||||||
|
label.setStyleSheet("color: #666666; font-size: 14px;")
|
||||||
|
# 确保value_text是字符串类型
|
||||||
|
value = QLabel(str(value_text))
|
||||||
|
value.setStyleSheet("color: #333333; font-size: 14px; font-weight: 500;")
|
||||||
|
value.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
||||||
|
|
||||||
# 标签和值之间的比例
|
# 布局
|
||||||
info_row.addWidget(BodyLabel(f"{label_text}:"), 1)
|
item_layout = QVBoxLayout()
|
||||||
info_row.addWidget(BodyLabel(f"{value_text}"), 3)
|
item_layout.addWidget(label)
|
||||||
info_grid_layout.addLayout(info_row)
|
item_layout.addWidget(value)
|
||||||
|
item_layout.setSpacing(4)
|
||||||
|
|
||||||
|
# 添加到网格
|
||||||
|
col = row % 2
|
||||||
|
actual_row = row // 2
|
||||||
|
info_grid_layout.addLayout(item_layout, actual_row, col)
|
||||||
|
row += 1
|
||||||
|
|
||||||
card_layout.addLayout(info_grid_layout)
|
card_layout.addLayout(info_grid_layout)
|
||||||
self.scroll_layout.addWidget(info_card)
|
self.scroll_layout.addWidget(info_card)
|
||||||
@@ -177,53 +464,338 @@ class AppDetailWindow(QMainWindow):
|
|||||||
"""显示应用描述卡片"""
|
"""显示应用描述卡片"""
|
||||||
description_card = CardWidget()
|
description_card = CardWidget()
|
||||||
description_card.setObjectName("DescriptionCard")
|
description_card.setObjectName("DescriptionCard")
|
||||||
|
description_card.setStyleSheet("""
|
||||||
card_layout = QVBoxLayout(description_card)
|
#DescriptionCard {
|
||||||
card_layout.setContentsMargins(16, 16, 16, 16)
|
background-color: white;
|
||||||
card_layout.setSpacing(12)
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
# 卡片标题
|
|
||||||
card_title = SubtitleLabel("应用描述")
|
|
||||||
card_layout.addWidget(card_title)
|
|
||||||
|
|
||||||
# 描述文本
|
|
||||||
from qfluentwidgets import TextEdit
|
|
||||||
description_text = TextEdit()
|
|
||||||
description_text.setReadOnly(True)
|
|
||||||
description_text.setWordWrapMode(3) # QTextOption.WrapAtWordBoundaryOrAnywhere
|
|
||||||
description_text.setMinimumHeight(150)
|
|
||||||
description_text.setStyleSheet("""
|
|
||||||
TextEdit {
|
|
||||||
background-color: transparent;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# 设置描述内容
|
card_layout = QVBoxLayout(description_card)
|
||||||
description_text.setPlainText(app_data.get("description", "无描述信息"))
|
card_layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
card_layout.setSpacing(15)
|
||||||
|
|
||||||
|
# 卡片标题和图标
|
||||||
|
title_layout = QHBoxLayout()
|
||||||
|
title_icon = QLabel()
|
||||||
|
title_icon.setPixmap(FluentIcon.DOCUMENT.icon().pixmap(18, 18))
|
||||||
|
title_icon.setStyleSheet("color: #2196F3;")
|
||||||
|
|
||||||
|
card_title = SubtitleLabel("应用描述")
|
||||||
|
card_title.setStyleSheet("font-size: 16px; font-weight: 600;")
|
||||||
|
|
||||||
|
title_layout.addWidget(title_icon)
|
||||||
|
title_layout.addWidget(card_title)
|
||||||
|
title_layout.addStretch()
|
||||||
|
card_layout.addLayout(title_layout)
|
||||||
|
|
||||||
|
# 描述文本
|
||||||
|
from PyQt5.QtWidgets import QTextBrowser
|
||||||
|
description_text = QTextBrowser()
|
||||||
|
description_text.setReadOnly(True)
|
||||||
|
description_text.setWordWrapMode(3) # QTextOption.WrapAtWordBoundaryOrAnywhere
|
||||||
|
description_text.setMinimumHeight(200)
|
||||||
|
description_text.setOpenExternalLinks(False) # 禁用自动打开外部链接,由我们自己处理
|
||||||
|
description_text.setStyleSheet("""
|
||||||
|
QTextBrowser {
|
||||||
|
background-color: #FAFAFA;
|
||||||
|
border: 1px solid #E5E6EB;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: 'Microsoft YaHei', 'SimHei';
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 设置描述内容 - 支持Markdown
|
||||||
|
description_text.setHtml(self.convert_markdown_to_html(app_data.get("description", "无描述信息")))
|
||||||
|
|
||||||
|
# 连接链接点击信号,处理外部链接打开
|
||||||
|
# QTextBrowser组件有anchorClicked信号,可以连接到处理函数
|
||||||
|
description_text.anchorClicked.connect(self.handle_link_clicked)
|
||||||
|
|
||||||
card_layout.addWidget(description_text)
|
card_layout.addWidget(description_text)
|
||||||
self.scroll_layout.addWidget(description_card)
|
self.scroll_layout.addWidget(description_card)
|
||||||
|
|
||||||
|
def convert_markdown_to_html(self, markdown_text):
|
||||||
|
"""将Markdown文本转换为HTML"""
|
||||||
|
try:
|
||||||
|
import markdown
|
||||||
|
return markdown.markdown(markdown_text)
|
||||||
|
except Exception as e:
|
||||||
|
# 如果转换失败,使用原始文本并进行HTML转义
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtGui import QTextDocument
|
||||||
|
|
||||||
|
# 创建QTextDocument进行HTML转义
|
||||||
|
doc = QTextDocument()
|
||||||
|
doc.setPlainText(markdown_text)
|
||||||
|
return doc.toHtml()
|
||||||
|
|
||||||
|
def handle_link_clicked(self, url):
|
||||||
|
"""处理链接点击事件,使用系统默认浏览器打开外部链接"""
|
||||||
|
from PyQt5.QtCore import QUrl
|
||||||
|
from PyQt5.QtGui import QDesktopServices
|
||||||
|
from qfluentwidgets import InfoBar
|
||||||
|
|
||||||
|
# 检查URL是否为http或https协议
|
||||||
|
if url.scheme() in ['http', 'https']:
|
||||||
|
# 使用系统默认浏览器打开链接
|
||||||
|
QDesktopServices.openUrl(url)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 处理特殊的leonapp链接
|
||||||
|
elif url.toString().startswith('leonapp://'):
|
||||||
|
# 提取应用ID或其他参数
|
||||||
|
# 示例: leonapp://app/123
|
||||||
|
path = url.toString()[10:] # 移除 'leonapp://'
|
||||||
|
if path.startswith('app/'):
|
||||||
|
app_id = path[4:] # 提取应用ID
|
||||||
|
# 实现打开特定应用详情的逻辑
|
||||||
|
self.app_detail_window = AppDetailWindow(self.api_client, app_id, parent=self.parent())
|
||||||
|
self.app_detail_window.show()
|
||||||
|
return
|
||||||
|
|
||||||
|
# 显示不支持的链接类型提示
|
||||||
|
InfoBar.warning(
|
||||||
|
title="不支持的链接类型",
|
||||||
|
content=f"无法打开链接: {url.toString()}",
|
||||||
|
orient=Qt.Horizontal,
|
||||||
|
isClosable=True,
|
||||||
|
position=InfoBarPosition.BOTTOM_RIGHT,
|
||||||
|
duration=3000,
|
||||||
|
parent=self
|
||||||
|
)
|
||||||
|
|
||||||
def display_action_buttons(self):
|
def display_action_buttons(self):
|
||||||
"""显示操作按钮区域"""
|
"""显示操作按钮区域"""
|
||||||
button_card = SimpleCardWidget()
|
button_card = CardWidget()
|
||||||
button_card.setObjectName("ButtonCard")
|
button_card.setObjectName("ButtonCard")
|
||||||
button_card.setMinimumHeight(80)
|
button_card.setStyleSheet("""
|
||||||
|
#ButtonCard {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
button_layout = QHBoxLayout(button_card)
|
button_layout = QHBoxLayout(button_card)
|
||||||
button_layout.setContentsMargins(16, 16, 16, 16)
|
button_layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
button_layout.setSpacing(12)
|
||||||
button_layout.addStretch()
|
button_layout.addStretch()
|
||||||
|
|
||||||
# 刷新按钮
|
# 刷新按钮
|
||||||
refresh_button = PushButton("刷新")
|
refresh_button = PushButton("刷新")
|
||||||
refresh_button.setIcon(FluentIcon.SYNC)
|
refresh_button.setIcon(FluentIcon.SYNC)
|
||||||
refresh_button.clicked.connect(self.load_app_detail)
|
refresh_button.clicked.connect(self.load_app_detail)
|
||||||
|
refresh_button.setFixedSize(100, 36)
|
||||||
|
|
||||||
|
# 安装按钮
|
||||||
|
install_button = PrimaryPushButton("安装")
|
||||||
|
install_button.setIcon(FluentIcon.DOWNLOAD)
|
||||||
|
install_button.setFixedSize(100, 36)
|
||||||
|
install_button.clicked.connect(self.install_app)
|
||||||
|
|
||||||
button_layout.addWidget(refresh_button)
|
button_layout.addWidget(refresh_button)
|
||||||
|
button_layout.addWidget(install_button)
|
||||||
self.scroll_layout.addWidget(button_card)
|
self.scroll_layout.addWidget(button_card)
|
||||||
|
def install_app(self):
|
||||||
|
"""安装应用"""
|
||||||
|
from qfluentwidgets import InfoBar
|
||||||
|
from PyQt5.QtWidgets import QMessageBox, QProgressDialog
|
||||||
|
|
||||||
|
# 检查是否有应用数据
|
||||||
|
if not hasattr(self, 'app_data'):
|
||||||
|
InfoBar.error(
|
||||||
|
title="错误",
|
||||||
|
content="应用数据未加载,请刷新页面后重试。",
|
||||||
|
orient=Qt.Horizontal,
|
||||||
|
isClosable=True,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
parent=self
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 创建确认对话框
|
||||||
|
reply = QMessageBox.question(self,
|
||||||
|
"确认安装",
|
||||||
|
f"您确定要安装{self.app_data.get('name', '未知应用')}吗?\n\n点击'确定'开始安装。",
|
||||||
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
|
QMessageBox.No)
|
||||||
|
|
||||||
|
if reply == QMessageBox.Yes:
|
||||||
|
# 显示开始安装的提示
|
||||||
|
InfoBar.success(
|
||||||
|
title="安装开始",
|
||||||
|
content=f"{self.app_data.get('name', '未知应用')}安装已开始,请稍候...",
|
||||||
|
orient=Qt.Horizontal,
|
||||||
|
isClosable=True,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
duration=3000,
|
||||||
|
parent=self
|
||||||
|
)
|
||||||
|
|
||||||
|
# 获取下载链接(使用应用数据中的直链下载地址)
|
||||||
|
download_url = self.app_data.get('direct_download_url')
|
||||||
|
|
||||||
|
# 如果没有直接下载链接,则尝试使用备用的download_url字段
|
||||||
|
if not download_url:
|
||||||
|
download_url = self.app_data.get('download_url')
|
||||||
|
|
||||||
|
# 如果仍然没有下载链接,显示错误信息
|
||||||
|
if not download_url:
|
||||||
|
InfoBar.error(
|
||||||
|
title="错误",
|
||||||
|
content="无法获取应用的下载链接,请稍后重试。",
|
||||||
|
orient=Qt.Horizontal,
|
||||||
|
isClosable=True,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
parent=self
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 创建进度对话框
|
||||||
|
self.progress_dialog = QProgressDialog(
|
||||||
|
f"正在下载{self.app_data.get('name', '未知应用')}...",
|
||||||
|
"取消",
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
parent=self
|
||||||
|
)
|
||||||
|
self.progress_dialog.setWindowTitle("正在安装")
|
||||||
|
self.progress_dialog.setMinimumDuration(0)
|
||||||
|
|
||||||
|
# 创建临时保存路径
|
||||||
|
file_extension = ".exe" # 假设是Windows可执行文件
|
||||||
|
app_name = self.app_data.get('name', '未知应用').replace(' ', '_')
|
||||||
|
temp_dir = tempfile.gettempdir()
|
||||||
|
save_path = os.path.join(temp_dir, f"{app_name}_installer{file_extension}")
|
||||||
|
|
||||||
|
# 创建下载线程
|
||||||
|
self.download_thread = DownloadThread(download_url, save_path)
|
||||||
|
self.download_thread.progress.connect(self.progress_dialog.setValue)
|
||||||
|
self.download_thread.finished.connect(self.on_download_finished)
|
||||||
|
self.download_thread.error.connect(self.on_download_error)
|
||||||
|
|
||||||
|
# 开始下载
|
||||||
|
self.download_thread.start()
|
||||||
|
self.progress_dialog.exec_()
|
||||||
|
|
||||||
|
def on_download_finished(self, file_path):
|
||||||
|
"""下载完成处理"""
|
||||||
|
self.progress_dialog.accept()
|
||||||
|
|
||||||
|
# 根据操作系统执行不同的安装操作
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
try:
|
||||||
|
# 在Windows上,直接运行可执行文件
|
||||||
|
subprocess.Popen([file_path], shell=True)
|
||||||
|
|
||||||
|
# 显示安装成功提示
|
||||||
|
InfoBar.success(
|
||||||
|
title="下载完成",
|
||||||
|
content=f"安装程序已启动,请按照向导完成安装。\n文件位置:{file_path}",
|
||||||
|
orient=Qt.Horizontal,
|
||||||
|
isClosable=True,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
duration=5000,
|
||||||
|
parent=self
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
InfoBar.error(
|
||||||
|
title="启动安装程序失败",
|
||||||
|
content=f"无法启动安装程序: {str(e)}\n您可以手动运行文件: {file_path}",
|
||||||
|
orient=Qt.Horizontal,
|
||||||
|
isClosable=True,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
parent=self
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 其他操作系统
|
||||||
|
InfoBar.information(
|
||||||
|
title="下载完成",
|
||||||
|
content=f"文件已下载完成。\n\n文件位置:{file_path}\n\n请手动运行安装程序。",
|
||||||
|
orient=Qt.Horizontal,
|
||||||
|
isClosable=True,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
duration=5000,
|
||||||
|
parent=self
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_download_error(self, error_msg):
|
||||||
|
"""下载错误处理"""
|
||||||
|
self.progress_dialog.reject()
|
||||||
|
|
||||||
|
InfoBar.error(
|
||||||
|
title="安装失败",
|
||||||
|
content=error_msg,
|
||||||
|
orient=Qt.Horizontal,
|
||||||
|
isClosable=True,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
parent=self
|
||||||
|
)
|
||||||
|
|
||||||
|
def share_app(self):
|
||||||
|
"""分享应用"""
|
||||||
|
from qfluentwidgets import InfoBar, PushButton
|
||||||
|
from PyQt5.QtGui import QClipboard
|
||||||
|
from PyQt5.QtWidgets import QApplication, QMessageBox
|
||||||
|
import webbrowser
|
||||||
|
|
||||||
|
# 创建分享链接
|
||||||
|
app_share_url = f"leonapp://app/{self.app_id}"
|
||||||
|
web_share_url = f"http://leonmcoset.jjxmm.win:8010/app?id={self.app_id}"
|
||||||
|
|
||||||
|
# 创建自定义对话框
|
||||||
|
dialog = QMessageBox(self)
|
||||||
|
dialog.setWindowTitle("分享应用")
|
||||||
|
dialog.setText(f"请选择分享方式:\n\n{self.app_data.get('name', '未知应用')}")
|
||||||
|
dialog.setIcon(QMessageBox.Information)
|
||||||
|
|
||||||
|
# 添加按钮
|
||||||
|
copy_web_url_btn = dialog.addButton("复制网页链接", QMessageBox.AcceptRole)
|
||||||
|
share_wechat_btn = dialog.addButton("分享到微信", QMessageBox.RejectRole)
|
||||||
|
copy_app_url_btn = dialog.addButton("复制应用内链接", QMessageBox.ActionRole)
|
||||||
|
|
||||||
|
# 显示对话框并处理结果
|
||||||
|
dialog.exec_()
|
||||||
|
|
||||||
|
clicked_button = dialog.clickedButton()
|
||||||
|
if clicked_button == copy_web_url_btn:
|
||||||
|
# 复制网页链接
|
||||||
|
self.copy_to_clipboard(web_share_url)
|
||||||
|
elif clicked_button == share_wechat_btn:
|
||||||
|
# 分享到微信(模拟功能)
|
||||||
|
InfoBar.info(
|
||||||
|
title="分享到微信",
|
||||||
|
content="请将以下链接分享给微信好友:\n" + web_share_url,
|
||||||
|
orient=Qt.Horizontal,
|
||||||
|
isClosable=True,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
duration=5000,
|
||||||
|
parent=self
|
||||||
|
)
|
||||||
|
self.copy_to_clipboard(web_share_url)
|
||||||
|
elif clicked_button == copy_app_url_btn:
|
||||||
|
# 复制应用内链接
|
||||||
|
self.copy_to_clipboard(app_share_url)
|
||||||
|
|
||||||
|
def copy_to_clipboard(self, text):
|
||||||
|
"""复制文本到剪贴板"""
|
||||||
|
from qfluentwidgets import InfoBar
|
||||||
|
clipboard = QApplication.clipboard()
|
||||||
|
clipboard.setText(text)
|
||||||
|
InfoBar.success(
|
||||||
|
title="复制成功",
|
||||||
|
content="链接已复制到剪贴板!",
|
||||||
|
orient=Qt.Horizontal,
|
||||||
|
isClosable=True,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
duration=2000,
|
||||||
|
parent=self
|
||||||
|
)
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
"""关闭窗口事件"""
|
"""关闭窗口事件"""
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ LeonApp GUI - 基于PyQt5和Fluent Design的App Store API图形界面工具
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# APP版本号
|
# APP版本号
|
||||||
APP_VERSION = "Beta 0.3"
|
APP_VERSION = "Beta 0.4"
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
@@ -1617,216 +1617,7 @@ class StatsTab(QWidget):
|
|||||||
parent=self
|
parent=self
|
||||||
)
|
)
|
||||||
|
|
||||||
class AppDetailWindow(QMainWindow):
|
|
||||||
"""应用详情窗口"""
|
|
||||||
def __init__(self, api_client, app_id, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.api_client = api_client
|
|
||||||
self.app_id = app_id
|
|
||||||
self.setWindowTitle("应用详情")
|
|
||||||
self.resize(800, 600)
|
|
||||||
self.init_ui()
|
|
||||||
self.load_app_detail()
|
|
||||||
|
|
||||||
def init_ui(self):
|
|
||||||
"""初始化界面"""
|
|
||||||
# 创建中心部件
|
|
||||||
central_widget = QWidget()
|
|
||||||
self.setCentralWidget(central_widget)
|
|
||||||
|
|
||||||
# 创建主布局
|
|
||||||
main_layout = QVBoxLayout(central_widget)
|
|
||||||
main_layout.setContentsMargins(20, 20, 20, 20)
|
|
||||||
|
|
||||||
# 创建标题
|
|
||||||
self.app_title = TitleLabel("加载中...")
|
|
||||||
main_layout.addWidget(self.app_title)
|
|
||||||
|
|
||||||
# 创建应用信息区域
|
|
||||||
self.info_card = CardWidget()
|
|
||||||
self.info_layout = QVBoxLayout(self.info_card)
|
|
||||||
main_layout.addWidget(self.info_card)
|
|
||||||
|
|
||||||
# 创建进度条
|
|
||||||
self.progress_bar = ProgressBar()
|
|
||||||
self.progress_bar.setVisible(False)
|
|
||||||
main_layout.addWidget(self.progress_bar)
|
|
||||||
|
|
||||||
def load_app_detail(self):
|
|
||||||
"""加载应用详情"""
|
|
||||||
self.show_progress()
|
|
||||||
self.worker = WorkerThread(self.api_client, 'getappinfo', {'id': self.app_id})
|
|
||||||
self.worker.finished.connect(self.on_app_detail_loaded)
|
|
||||||
self.worker.progress.connect(self.update_progress)
|
|
||||||
self.worker.error.connect(self.show_error)
|
|
||||||
self.worker.start()
|
|
||||||
|
|
||||||
def on_app_detail_loaded(self, data):
|
|
||||||
"""应用详情加载完成处理"""
|
|
||||||
self.hide_progress()
|
|
||||||
|
|
||||||
if not data:
|
|
||||||
self.show_error("应用详情加载失败")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 更新标题
|
|
||||||
self.app_title.setText(data.get('name', '未知应用'))
|
|
||||||
|
|
||||||
# 清空之前的信息
|
|
||||||
while self.info_layout.count():
|
|
||||||
item = self.info_layout.takeAt(0)
|
|
||||||
widget = item.widget()
|
|
||||||
if widget:
|
|
||||||
widget.deleteLater()
|
|
||||||
|
|
||||||
# 添加应用信息
|
|
||||||
info_text = f"""
|
|
||||||
版本: {data.get('version', '未知')}
|
|
||||||
年龄分级: {data.get('age_rating', '未知')}
|
|
||||||
评分: {data.get('avg_rating', '暂无')}
|
|
||||||
下载量: {data.get('total_downloads', 0)}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 添加描述
|
|
||||||
description_label = SubtitleLabel("描述")
|
|
||||||
self.info_layout.addWidget(description_label)
|
|
||||||
|
|
||||||
# 使用QFluentWidgets的TextEdit支持Markdown显示
|
|
||||||
from qfluentwidgets import TextEdit
|
|
||||||
description_text = TextEdit()
|
|
||||||
description_text.setReadOnly(True)
|
|
||||||
description_text.setWordWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere)
|
|
||||||
description_text.setLineWrapMode(QTextEdit.WidgetWidth)
|
|
||||||
description_text.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
|
||||||
# 设置Fluent风格
|
|
||||||
description_text.setStyleSheet("""
|
|
||||||
TextEdit {
|
|
||||||
background-color: transparent;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
# 尝试将markdown格式的描述转换为HTML
|
|
||||||
app_description = data.get('description', '暂无描述')
|
|
||||||
try:
|
|
||||||
html_description = markdown.markdown(app_description)
|
|
||||||
description_text.setHtml(html_description)
|
|
||||||
except Exception:
|
|
||||||
# 如果markdown解析失败,使用纯文本
|
|
||||||
description_text.setPlainText(app_description)
|
|
||||||
|
|
||||||
# 设置文本框的高度
|
|
||||||
description_text.setMinimumHeight(120)
|
|
||||||
self.info_layout.addWidget(description_text)
|
|
||||||
|
|
||||||
# 添加标签信息
|
|
||||||
if 'tags' in data and data['tags']:
|
|
||||||
tags_label = SubtitleLabel("标签")
|
|
||||||
self.info_layout.addWidget(tags_label)
|
|
||||||
|
|
||||||
tags_text = ', '.join([tag['name'] for tag in data['tags']])
|
|
||||||
self.info_layout.addWidget(CaptionLabel(tags_text))
|
|
||||||
|
|
||||||
# 添加版本信息
|
|
||||||
if 'versions' in data and data['versions']:
|
|
||||||
versions_label = SubtitleLabel("版本历史")
|
|
||||||
self.info_layout.addWidget(versions_label)
|
|
||||||
|
|
||||||
versions_text = "\n".join([f"- 版本 {v['version']} ({v['download_count']} 下载)" for v in data['versions'][:3]])
|
|
||||||
self.info_layout.addWidget(CaptionLabel(versions_text))
|
|
||||||
|
|
||||||
# 添加"查看全部版本"按钮
|
|
||||||
if len(data['versions']) > 3:
|
|
||||||
self.view_all_versions_button = PushButton("查看全部版本")
|
|
||||||
self.view_all_versions_button.clicked.connect(lambda: self.view_all_versions())
|
|
||||||
self.info_layout.addWidget(self.view_all_versions_button)
|
|
||||||
|
|
||||||
# 添加下载最新版本按钮
|
|
||||||
if data['versions']:
|
|
||||||
# 保存最新版本信息用于下载
|
|
||||||
self.latest_version = data['versions'][0]
|
|
||||||
download_layout = QHBoxLayout()
|
|
||||||
download_layout.addStretch(1)
|
|
||||||
|
|
||||||
self.download_button = PushButton("下载最新版本")
|
|
||||||
self.download_button.setIcon(FluentIcon.DOWNLOAD)
|
|
||||||
self.download_button.clicked.connect(self.download_latest_version)
|
|
||||||
download_layout.addWidget(self.download_button)
|
|
||||||
|
|
||||||
self.info_layout.addLayout(download_layout)
|
|
||||||
|
|
||||||
def view_all_versions(self):
|
|
||||||
"""查看全部版本"""
|
|
||||||
versions_window = AppVersionsWindow(self.api_client, self.app_id, self.app_title.text(), self)
|
|
||||||
versions_window.show()
|
|
||||||
|
|
||||||
def download_latest_version(self):
|
|
||||||
"""下载最新版本"""
|
|
||||||
if hasattr(self, 'latest_version'):
|
|
||||||
version_id = self.latest_version.get('id', '')
|
|
||||||
if version_id:
|
|
||||||
self.perform_download(version_id)
|
|
||||||
else:
|
|
||||||
self.show_error("无法获取版本ID")
|
|
||||||
else:
|
|
||||||
self.show_error("没有可下载的版本")
|
|
||||||
|
|
||||||
def perform_download(self, version_id):
|
|
||||||
"""执行下载操作"""
|
|
||||||
import webbrowser
|
|
||||||
|
|
||||||
# 直接使用latest_version中的file_path进行下载
|
|
||||||
if hasattr(self, 'latest_version') and 'file_path' in self.latest_version:
|
|
||||||
file_path = self.latest_version['file_path']
|
|
||||||
# 构建直接的文件URL
|
|
||||||
download_url = f"http://leonmmcoset.jjxmm.win:8010/{file_path}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 使用系统默认浏览器打开下载链接
|
|
||||||
webbrowser.open(download_url)
|
|
||||||
|
|
||||||
# 显示下载成功提示
|
|
||||||
InfoBar.success(
|
|
||||||
title="下载开始",
|
|
||||||
content=f"下载已开始,请稍候...",
|
|
||||||
orient=Qt.Horizontal,
|
|
||||||
isClosable=True,
|
|
||||||
position=InfoBarPosition.BOTTOM_RIGHT,
|
|
||||||
duration=3000,
|
|
||||||
parent=self
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
self.show_error(f"下载失败: {str(e)}")
|
|
||||||
else:
|
|
||||||
self.show_error("无法获取下载文件路径")
|
|
||||||
|
|
||||||
def show_progress(self):
|
|
||||||
"""显示进度条"""
|
|
||||||
self.progress_bar.setVisible(True)
|
|
||||||
self.progress_bar.setValue(0)
|
|
||||||
|
|
||||||
def update_progress(self, value):
|
|
||||||
"""更新进度条"""
|
|
||||||
self.progress_bar.setValue(value)
|
|
||||||
|
|
||||||
def hide_progress(self):
|
|
||||||
"""隐藏进度条"""
|
|
||||||
self.progress_bar.setVisible(False)
|
|
||||||
|
|
||||||
def show_error(self, message):
|
|
||||||
"""显示错误消息"""
|
|
||||||
self.hide_progress()
|
|
||||||
InfoBar.error(
|
|
||||||
title="错误",
|
|
||||||
content=message,
|
|
||||||
orient=Qt.Horizontal,
|
|
||||||
isClosable=True,
|
|
||||||
position=InfoBarPosition.BOTTOM_RIGHT,
|
|
||||||
duration=5000,
|
|
||||||
parent=self
|
|
||||||
)
|
|
||||||
|
|
||||||
class TagAppsWindow(QMainWindow):
|
class TagAppsWindow(QMainWindow):
|
||||||
"""标签下的应用列表窗口"""
|
"""标签下的应用列表窗口"""
|
||||||
@@ -2256,14 +2047,18 @@ class AnnouncementDetailWindow(QMainWindow):
|
|||||||
content_title = SubtitleLabel("公告内容")
|
content_title = SubtitleLabel("公告内容")
|
||||||
card_layout.addWidget(content_title)
|
card_layout.addWidget(content_title)
|
||||||
|
|
||||||
# 添加内容文本框,支持Markdown渲染
|
# 添加内容文本框,支持Markdown渲染和链接点击
|
||||||
from qfluentwidgets import TextEdit
|
from PyQt5.QtWidgets import QTextBrowser
|
||||||
self.content_text = TextEdit()
|
self.content_text = QTextBrowser()
|
||||||
self.content_text.setHtml(self.content_html)
|
self.content_text.setHtml(self.content_html)
|
||||||
self.content_text.setReadOnly(True)
|
self.content_text.setReadOnly(True)
|
||||||
self.content_text.setMinimumHeight(250)
|
self.content_text.setMinimumHeight(250)
|
||||||
self.content_text.setLineWrapMode(QTextEdit.WidgetWidth)
|
self.content_text.setLineWrapMode(QTextEdit.WidgetWidth)
|
||||||
self.content_text.setWordWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere)
|
self.content_text.setWordWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere)
|
||||||
|
self.content_text.setOpenExternalLinks(False) # 禁用自动打开外部链接,由我们自己处理
|
||||||
|
|
||||||
|
# QTextBrowser组件有anchorClicked信号,可以连接到处理函数
|
||||||
|
self.content_text.anchorClicked.connect(self.handle_link_clicked)
|
||||||
|
|
||||||
# 设置Fluent风格样式
|
# 设置Fluent风格样式
|
||||||
self.content_text.setStyleSheet("""
|
self.content_text.setStyleSheet("""
|
||||||
@@ -2278,6 +2073,41 @@ class AnnouncementDetailWindow(QMainWindow):
|
|||||||
card_layout.addWidget(self.content_text)
|
card_layout.addWidget(self.content_text)
|
||||||
self.scroll_layout.addWidget(content_card)
|
self.scroll_layout.addWidget(content_card)
|
||||||
|
|
||||||
|
def handle_link_clicked(self, url):
|
||||||
|
"""处理链接点击事件,使用系统默认浏览器打开外部链接"""
|
||||||
|
from PyQt5.QtCore import QUrl
|
||||||
|
from PyQt5.QtGui import QDesktopServices
|
||||||
|
|
||||||
|
# 检查URL是否为http或https协议
|
||||||
|
if url.scheme() in ['http', 'https']:
|
||||||
|
# 使用系统默认浏览器打开链接
|
||||||
|
QDesktopServices.openUrl(url)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 处理特殊的leonapp链接
|
||||||
|
elif url.toString().startswith('leonapp://'):
|
||||||
|
# 提取应用ID或其他参数
|
||||||
|
# 示例: leonapp://app/123
|
||||||
|
path = url.toString()[10:] # 移除 'leonapp://'
|
||||||
|
if path.startswith('app/'):
|
||||||
|
app_id = path[4:] # 提取应用ID
|
||||||
|
# 这里可以实现打开特定应用详情的逻辑
|
||||||
|
from app_detail_window import AppDetailWindow
|
||||||
|
self.app_detail_window = AppDetailWindow(self.api_client, app_id, parent=self)
|
||||||
|
self.app_detail_window.show()
|
||||||
|
return
|
||||||
|
|
||||||
|
# 显示不支持的链接类型提示
|
||||||
|
InfoBar.warning(
|
||||||
|
title="不支持的链接类型",
|
||||||
|
content=f"无法打开链接: {url.toString()}",
|
||||||
|
orient=Qt.Horizontal,
|
||||||
|
isClosable=True,
|
||||||
|
position=InfoBarPosition.BOTTOM_RIGHT,
|
||||||
|
duration=3000,
|
||||||
|
parent=self
|
||||||
|
)
|
||||||
|
|
||||||
def create_close_button(self, parent_layout):
|
def create_close_button(self, parent_layout):
|
||||||
"""创建关闭按钮"""
|
"""创建关闭按钮"""
|
||||||
button_card = SimpleCardWidget()
|
button_card = SimpleCardWidget()
|
||||||
@@ -2288,6 +2118,7 @@ class AnnouncementDetailWindow(QMainWindow):
|
|||||||
button_layout.setContentsMargins(16, 16, 16, 16)
|
button_layout.setContentsMargins(16, 16, 16, 16)
|
||||||
button_layout.addStretch()
|
button_layout.addStretch()
|
||||||
|
|
||||||
|
# 关闭按钮
|
||||||
close_button = PushButton("关闭")
|
close_button = PushButton("关闭")
|
||||||
close_button.setFixedWidth(100)
|
close_button.setFixedWidth(100)
|
||||||
close_button.clicked.connect(self.close)
|
close_button.clicked.connect(self.close)
|
||||||
@@ -2295,6 +2126,21 @@ class AnnouncementDetailWindow(QMainWindow):
|
|||||||
|
|
||||||
parent_layout.addWidget(button_card)
|
parent_layout.addWidget(button_card)
|
||||||
|
|
||||||
|
def copy_to_clipboard(self, text):
|
||||||
|
"""复制文本到剪贴板"""
|
||||||
|
from qfluentwidgets import InfoBar
|
||||||
|
clipboard = QApplication.clipboard()
|
||||||
|
clipboard.setText(text)
|
||||||
|
InfoBar.success(
|
||||||
|
title="复制成功",
|
||||||
|
content="链接已复制到剪贴板!",
|
||||||
|
orient=Qt.Horizontal,
|
||||||
|
isClosable=True,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
duration=2000,
|
||||||
|
parent=self
|
||||||
|
)
|
||||||
|
|
||||||
class DeveloperInfoWindow(QMainWindow):
|
class DeveloperInfoWindow(QMainWindow):
|
||||||
"""开发者信息窗口"""
|
"""开发者信息窗口"""
|
||||||
def __init__(self, api_client, developer_id, parent=None):
|
def __init__(self, api_client, developer_id, parent=None):
|
||||||
|
|||||||
@@ -141,29 +141,28 @@ while ($row = $result->fetch_assoc()) {
|
|||||||
<h5 class="card-title">版本 <?php echo htmlspecialchars($version['version']); ?></h5>
|
<h5 class="card-title">版本 <?php echo htmlspecialchars($version['version']); ?></h5>
|
||||||
<h6 class="card-subtitle mb-2 text-muted">发布日期: <?php echo date('Y-m-d', strtotime($version['created_at'])); ?></h6>
|
<h6 class="card-subtitle mb-2 text-muted">发布日期: <?php echo date('Y-m-d', strtotime($version['created_at'])); ?></h6>
|
||||||
<div class="card-text markdown-content"><?php echo htmlspecialchars($version['changelog']); ?></div>
|
<div class="card-text markdown-content"><?php echo htmlspecialchars($version['changelog']); ?></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer bg-transparent d-flex justify-content-between align-items-center">
|
<div class="card-footer bg-transparent d-flex justify-content-between align-items-center">
|
||||||
<?php
|
<?php
|
||||||
// 安全地处理文件大小和路径,避免open_basedir限制问题
|
// 安全地处理文件大小和路径
|
||||||
$fileName = trim($version['file_path']);
|
$filePath = trim($version['file_path']);
|
||||||
$fileSize = false;
|
$fileSize = false;
|
||||||
|
|
||||||
// 使用__DIR__构建安全的绝对路径,与version_control.php保持一致
|
// 使用__DIR__构建安全的绝对路径,与version_control.php保持一致
|
||||||
$uploadDir = __DIR__ . '/../files/';
|
$absoluteFilePath = __DIR__ . '/' . $filePath;
|
||||||
$absoluteFilePath = $uploadDir . $fileName;
|
|
||||||
|
|
||||||
// 尝试安全地获取文件大小,处理open_basedir限制
|
// 尝试安全地获取文件大小
|
||||||
if (file_exists($absoluteFilePath)) {
|
if (file_exists($absoluteFilePath)) {
|
||||||
$fileSize = filesize($absoluteFilePath);
|
$fileSize = filesize($absoluteFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
$sizeText = formatFileSize($fileSize);
|
$sizeText = formatFileSize($fileSize);
|
||||||
|
|
||||||
// 构建安全的下载链接
|
// 构建安全的下载链接
|
||||||
$downloadUrl = '/files/' . $fileName;
|
$downloadUrl = '/' . $filePath;
|
||||||
?>
|
?>
|
||||||
<a href="<?php echo htmlspecialchars($downloadUrl); ?>" class="btn btn-primary" download>下载(大小:<?php echo $sizeText; ?>)</a>
|
<a href="<?php echo htmlspecialchars($downloadUrl); ?>" class="btn btn-primary" download>下载(大小:<?php echo $sizeText; ?>)</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user