feat: 添加应用详情窗口和更新检查功能

- 实现全新的应用详情窗口,包含统计信息、基本信息和描述展示
- 添加应用更新检查功能到CLI工具
- 优化版本列表页面的文件路径处理逻辑
- 升级GUI版本至Beta 0.4
- 增强公告详情页面的链接处理能力
This commit is contained in:
2025-09-24 21:58:25 +08:00
parent c72b25cd0b
commit e553768c1d
6 changed files with 778 additions and 315 deletions

View File

@@ -1,10 +1,60 @@
from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton)
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QFrame, QLabel, QGridLayout)
from PyQt5.QtCore import Qt, QSize, pyqtSignal, QThread
from PyQt5.QtGui import QPixmap, QFont
from qfluentwidgets import (InfoBar, InfoBarPosition, TitleLabel, SubtitleLabel,
PrimaryPushButton, PushButton, ScrollArea, CardWidget,
FluentIcon, SimpleCardWidget, BodyLabel)
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):
def __init__(self, api_client, app_id, parent=None):
@@ -15,13 +65,29 @@ class AppDetailWindow(QMainWindow):
# 设置窗口属性
self.setWindowTitle("应用详情")
self.resize(800, 600)
self.resize(850, 650)
self.setObjectName("AppDetailWindow")
# 添加全局样式
# 添加全局样式 - 现代扁平化设计
self.setStyleSheet("""
#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.setContentsMargins(20, 20, 20, 20)
self.main_layout.setSpacing(12)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.main_layout.setSpacing(0)
# 创建标题区域
self.header_layout = QHBoxLayout()
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.create_custom_header()
self.header_layout.addWidget(self.app_title)
self.header_layout.addStretch()
self.header_layout.addWidget(self.close_button)
# 添加分隔线
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setFrameShadow(QFrame.Sunken)
separator.setStyleSheet("background-color: #E5E6EB;")
self.main_layout.addWidget(separator)
# 创建滚动区域 - 使用QFluentWidgets的ScrollArea
self.scroll_area = ScrollArea()
@@ -55,20 +119,26 @@ class AppDetailWindow(QMainWindow):
# 设置滚动区域样式
self.scroll_area.setStyleSheet("""
ScrollArea {
background-color: transparent;
background-color: #F5F7FA;
border: none;
}
QScrollBar:vertical {
width: 8px;
width: 10px;
background: transparent;
margin-right: 5px;
}
QScrollBar::handle:vertical {
background: rgba(142, 142, 147, 0.3);
border-radius: 4px;
min-height: 40px;
background: rgba(142, 142, 147, 0.2);
border-radius: 5px;
min-height: 50px;
}
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.setObjectName("ScrollContent")
self.scroll_layout = QVBoxLayout(self.scroll_content)
self.scroll_layout.setContentsMargins(0, 0, 0, 20)
self.scroll_layout.setSpacing(16)
self.scroll_layout.setContentsMargins(20, 20, 20, 30)
self.scroll_layout.setSpacing(20)
# 添加滚动区域到主布局
self.scroll_area.setWidget(self.scroll_content)
# 将标题区域和滚动区域添加到主布局
self.main_layout.addLayout(self.header_layout)
self.main_layout.addWidget(self.scroll_area)
# 加载应用详情
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):
"""加载应用详情"""
try:
# 这里应该调用API获取应用详情
# 暂时使用模拟数据
app_data = {
"id": self.app_id,
"name": "示例应用",
"description": "这是一个示例应用,用于展示应用详情页面",
"developer_id": "1",
"developer_name": "示例开发者",
"status": "approved",
"version": "1.0.0",
"created_at": "2023-01-01 10:00:00"
}
# 使用API客户端调用getappinfo端点获取应用详情
result = self.api_client.make_request('getappinfo', {'id': self.app_id})
# 检查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,
"name": "未知应用",
"description": "无法加载应用详情",
"developer_id": "0",
"developer_name": "未知开发者",
"status": "unknown",
"version": "0.0.0",
"created_at": "-",
"downloads": "0",
"rating": "0.0",
"category": "未知"
}
app_data = self.app_data
# 更新窗口标题
self.setWindowTitle(f"应用详情 - {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())):
@@ -116,6 +307,9 @@ class AppDetailWindow(QMainWindow):
widget.setParent(None)
widget.deleteLater()
# 显示统计信息卡片
self.display_stats_card(app_data)
# 显示应用基本信息卡片
self.display_app_info_card(app_data)
@@ -136,39 +330,132 @@ class AppDetailWindow(QMainWindow):
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):
"""显示应用基本信息卡片"""
info_card = CardWidget()
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.setContentsMargins(16, 16, 16, 16)
card_layout.setSpacing(12)
card_layout.setContentsMargins(20, 20, 20, 20)
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_layout.addWidget(card_title)
card_title.setStyleSheet("font-size: 16px; font-weight: 600;")
# 信息网格布局
info_grid_layout = QVBoxLayout()
info_grid_layout.setSpacing(6)
title_layout.addWidget(title_icon)
title_layout.addWidget(card_title)
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 = [
("应用ID", app_data.get("id", "--")),
("开发者", app_data.get("developer_name", "--")),
("状态", app_data.get("status", "--")),
("开发者", developer_name),
("当前版本", app_data.get("version", "--")),
("创建时间", app_data.get("created_at", "--"))
]
row = 0
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)
info_row.addWidget(BodyLabel(f"{value_text}"), 3)
info_grid_layout.addLayout(info_row)
# 布局
item_layout = QVBoxLayout()
item_layout.addWidget(label)
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)
self.scroll_layout.addWidget(info_card)
@@ -177,53 +464,338 @@ class AppDetailWindow(QMainWindow):
"""显示应用描述卡片"""
description_card = CardWidget()
description_card.setObjectName("DescriptionCard")
card_layout = QVBoxLayout(description_card)
card_layout.setContentsMargins(16, 16, 16, 16)
card_layout.setSpacing(12)
# 卡片标题
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;
description_card.setStyleSheet("""
#DescriptionCard {
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
""")
# 设置描述内容
description_text.setPlainText(app_data.get("description", "无描述信息"))
card_layout = QVBoxLayout(description_card)
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)
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):
"""显示操作按钮区域"""
button_card = SimpleCardWidget()
button_card = CardWidget()
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.setContentsMargins(16, 16, 16, 16)
button_layout.setContentsMargins(20, 20, 20, 20)
button_layout.setSpacing(12)
button_layout.addStretch()
# 刷新按钮
refresh_button = PushButton("刷新")
refresh_button.setIcon(FluentIcon.SYNC)
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(install_button)
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):
"""关闭窗口事件"""

View File

@@ -6,7 +6,7 @@ LeonApp GUI - 基于PyQt5和Fluent Design的App Store API图形界面工具
"""
# APP版本号
APP_VERSION = "Beta 0.3"
APP_VERSION = "Beta 0.4"
import sys
import json
@@ -1617,216 +1617,7 @@ class StatsTab(QWidget):
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):
"""标签下的应用列表窗口"""
@@ -2256,14 +2047,18 @@ class AnnouncementDetailWindow(QMainWindow):
content_title = SubtitleLabel("公告内容")
card_layout.addWidget(content_title)
# 添加内容文本框支持Markdown渲染
from qfluentwidgets import TextEdit
self.content_text = TextEdit()
# 添加内容文本框支持Markdown渲染和链接点击
from PyQt5.QtWidgets import QTextBrowser
self.content_text = QTextBrowser()
self.content_text.setHtml(self.content_html)
self.content_text.setReadOnly(True)
self.content_text.setMinimumHeight(250)
self.content_text.setLineWrapMode(QTextEdit.WidgetWidth)
self.content_text.setWordWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere)
self.content_text.setOpenExternalLinks(False) # 禁用自动打开外部链接,由我们自己处理
# QTextBrowser组件有anchorClicked信号可以连接到处理函数
self.content_text.anchorClicked.connect(self.handle_link_clicked)
# 设置Fluent风格样式
self.content_text.setStyleSheet("""
@@ -2278,6 +2073,41 @@ class AnnouncementDetailWindow(QMainWindow):
card_layout.addWidget(self.content_text)
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):
"""创建关闭按钮"""
button_card = SimpleCardWidget()
@@ -2288,12 +2118,28 @@ class AnnouncementDetailWindow(QMainWindow):
button_layout.setContentsMargins(16, 16, 16, 16)
button_layout.addStretch()
# 关闭按钮
close_button = PushButton("关闭")
close_button.setFixedWidth(100)
close_button.clicked.connect(self.close)
button_layout.addWidget(close_button)
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):
"""开发者信息窗口"""