Files
leonapp/pyqt5fluentdesign/app_detail_window.py
Leonmmcoset 5a5f825581 fix(app_detail_window): 修正URL处理中的拼写错误并添加subprocess导入
确保下载URL处理逻辑中的注释准确性,并添加必要的subprocess模块导入以支持跨平台安装操作
2025-09-25 21:49:56 +08:00

982 lines
41 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

# 这破代码有人能修复吗
from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QFrame, QLabel, QGridLayout, QMessageBox)
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
def show_error_dialog(title, message):
"""显示错误弹窗模拟Sweet Alert风格"""
# 创建错误消息框
msg_box = QMessageBox()
msg_box.setIcon(QMessageBox.Critical)
msg_box.setWindowTitle(title)
# 设置字体大小
font = QFont()
font.setPointSize(10)
msg_box.setFont(font)
# 设置消息内容
msg_box.setText(message)
msg_box.setStandardButtons(QMessageBox.Ok)
# 显示弹窗
msg_box.exec_()
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):
super().__init__(parent)
self.api_client = api_client
self.app_id = app_id
self.parent_window = parent
# 设置窗口属性
self.setWindowTitle("应用详情")
self.resize(850, 650)
self.setObjectName("AppDetailWindow")
# 添加全局样式 - 现代扁平化设计
self.setStyleSheet("""
#AppDetailWindow {
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;
}
""")
# 创建中心部件
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
# 创建主布局
self.main_layout = QVBoxLayout(self.central_widget)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.main_layout.setSpacing(0)
# 创建自定义顶部区域(非传统顶栏)
self.create_custom_header()
# 添加分隔线
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()
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setFrameShape(0)
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
# 设置滚动区域样式
self.scroll_area.setStyleSheet("""
ScrollArea {
background-color: #F5F7FA;
border: none;
}
QScrollBar:vertical {
width: 10px;
background: transparent;
margin-right: 5px;
}
QScrollBar::handle:vertical {
background: rgba(142, 142, 147, 0.2);
border-radius: 5px;
min-height: 50px;
}
QScrollBar::handle:vertical:hover {
background: rgba(142, 142, 147, 0.4);
}
QScrollBar::add-line:vertical,
QScrollBar::sub-line:vertical {
height: 0px;
width: 0px;
}
""")
# 创建滚动内容部件
self.scroll_content = QWidget()
self.scroll_content.setObjectName("ScrollContent")
self.scroll_layout = QVBoxLayout(self.scroll_content)
self.scroll_layout.setContentsMargins(20, 20, 20, 30)
self.scroll_layout.setSpacing(20)
# 添加滚动区域到主布局
self.scroll_area.setWidget(self.scroll_content)
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客户端调用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', '未知应用'))
# 通过开发者ID查询开发者名称
developer_id = app_data.get('developer_id', '')
result2 = self.api_client.make_request('getdeveloperinfo', {'id': developer_id})
developer_name = "未知开发者"
if developer_id and developer_id != "0":
# 尝试通过API获取开发者名称
try:
developer_info = self.api_client.make_request('getdeveloperinfo', {'id': developer_id})
if isinstance(developer_info, dict) and 'success' in developer_info and developer_info['success']:
developer_name = result2['data'].get('username')
else:
# 如果API调用失败尝试从本地数据获取
if 'developer_name' in app_data and app_data['developer_name']:
developer_name = app_data['developer_name']
elif 'developer' in app_data and app_data['developer']:
developer_name = app_data['developer']
elif 'developer_email' in app_data and app_data['developer_email']:
developer_name = app_data['developer_email']
except Exception as e:
# 异常处理,使用本地数据
if 'developer_name' in app_data and app_data['developer_name']:
developer_name = app_data['developer_name']
elif 'developer' in app_data and app_data['developer']:
developer_name = app_data['developer']
elif 'developer_email' in app_data and app_data['developer_email']:
developer_name = app_data['developer_email']
else:
# 如果没有有效的开发者ID尝试从本地数据获取
if 'developer_name' in app_data and app_data['developer_name']:
developer_name = app_data['developer_name']
elif 'developer' in app_data and app_data['developer']:
developer_name = app_data['developer']
elif 'developer_email' in app_data and app_data['developer_email']:
developer_name = app_data['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())):
widget = self.scroll_layout.itemAt(i).widget()
if widget is not None:
widget.setParent(None)
widget.deleteLater()
# 显示统计信息卡片
self.display_stats_card(app_data)
# 显示应用基本信息卡片
self.display_app_info_card(app_data)
# 显示应用描述卡片
self.display_description_card(app_data)
# 显示操作按钮区域
self.display_action_buttons()
except Exception as e:
InfoBar.error(
title="错误",
content=f"加载应用详情失败: {str(e)}",
orient=Qt.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP,
duration=3000,
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(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_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)
# 信息网格布局 - 使用两列布局
info_grid_layout = QGridLayout()
info_grid_layout.setSpacing(12)
info_grid_layout.setColumnStretch(0, 1)
info_grid_layout.setColumnStretch(1, 1)
# 添加基本信息字段,对开发者信息进行特殊处理
developer_id = app_data.get('developer_id')
developer_name = "未知开发者"
if developer_id and developer_id != "0":
# 尝试通过API获取开发者名称增加错误处理
try:
result2 = self.api_client.make_request('getdeveloperinfo', {'id': developer_id})
if isinstance(result2, dict) and 'success' in result2 and result2['success'] and 'data' in result2:
developer_name = result2["data"].get("username", "未知开发者")
except Exception as e:
# API调用失败尝试从本地数据获取
if 'developer_name' in app_data and app_data['developer_name']:
developer_name = app_data['developer_name']
elif 'developer' in app_data and app_data['developer']:
developer_name = app_data['developer']
elif 'developer_email' in app_data and app_data['developer_email']:
developer_name = app_data['developer_email']
info_items = [
("应用ID", app_data.get("id", "--")),
("开发者", developer_name),
("当前版本", app_data.get("version", "--")),
("创建时间", app_data.get("created_at", "--"))
]
row = 0
for label_text, value_text in info_items:
# 创建标签和值
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)
# 布局
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)
def display_description_card(self, app_data):
"""显示应用描述卡片"""
description_card = CardWidget()
description_card.setObjectName("DescriptionCard")
description_card.setStyleSheet("""
#DescriptionCard {
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
""")
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 = CardWidget()
button_card.setObjectName("ButtonCard")
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(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'):
show_error_dialog("错误", "应用数据未加载,请刷新页面后重试。")
return
# 创建Sweet Alert风格的确认对话框
confirm_box = QMessageBox()
confirm_box.setWindowTitle("确认安装")
confirm_box.setIcon(QMessageBox.Question)
confirm_box.setText(f"您确定要安装{self.app_data.get('name', '未知应用')}吗?\n\n点击'确定'开始安装。")
# 设置字体大小
font = QFont()
font.setPointSize(10)
confirm_box.setFont(font)
# 添加按钮
confirm_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
confirm_box.setDefaultButton(QMessageBox.No)
# 显示对话框
reply = confirm_box.exec_()
if reply == QMessageBox.Yes:
# 创建Sweet Alert风格的成功提示
success_box = QMessageBox()
success_box.setWindowTitle("安装开始")
success_box.setIcon(QMessageBox.Information)
success_box.setText(f"{self.app_data.get('name', '未知应用')}安装已开始,请稍候...")
# 设置字体大小
font = QFont()
font.setPointSize(10)
success_box.setFont(font)
# 添加按钮
success_box.setStandardButtons(QMessageBox.Ok)
# 显示对话框
success_box.exec_()
# 1. 首先尝试从应用数据中获取直接下载链接
download_url = self.app_data.get('direct_download_url')
# 2. 如果没有直接下载链接尝试使用备用的download_url字段
if not download_url:
download_url = self.app_data.get('download_url')
# 3. 根据新API格式从versions数组中获取最新版本的file_path
if not download_url and 'versions' in self.app_data and isinstance(self.app_data['versions'], list) and self.app_data['versions']:
# 假设versions数组中第一个版本是最新的
latest_version = self.app_data['versions'][0]
if isinstance(latest_version, dict) and 'file_path' in latest_version:
download_url = latest_version['file_path']
# 4. 如果仍然没有下载链接尝试通过API获取下载链接
if not download_url:
try:
# 使用app_id通过API获取下载链接
app_id = self.app_data.get('id')
if app_id:
download_result = self.api_client.make_request('getdownloadlink', {'id': app_id})
if isinstance(download_result, dict):
# 检查是否是新API格式的响应
if 'data' in download_result:
if isinstance(download_result['data'], dict) and 'versions' in download_result['data'] and download_result['data']['versions']:
latest_version = download_result['data']['versions'][0]
if isinstance(latest_version, dict) and 'file_path' in latest_version:
download_url = latest_version['file_path']
# 兼容旧API格式
elif 'success' in download_result and download_result['success']:
download_url = download_result.get('data')
except Exception as e:
print(f"获取下载链接API调用失败: {str(e)}")
# 4. 如果仍然没有下载链接,显示错误信息
if not download_url:
show_error_dialog("错误", "无法获取应用的下载链接,请稍后重试。\n可能原因API返回数据中缺少下载链接字段。")
return
# 5. 更健壮的URL处理逻辑
from urllib.parse import urlparse, quote
import os
# 检查是否看起来像一个文件名(包含扩展名)
is_filename = False
if '.' in download_url:
# 获取最后一个点后的部分作为扩展名
extension = download_url.split('.')[-1].lower()
# 常见的应用安装包扩展名
common_extensions = ['exe', 'apk', 'dmg', 'pkg', 'msi', 'appx', 'deb', 'rpm']
if extension in common_extensions and len(extension) <= 5:
is_filename = True
# 如果是文件名或不包含协议处理为完整URL
if is_filename or not download_url.startswith(('http://', 'https://')):
# 使用基础域名 - 修正拼写错误
base_domain = 'leonmmcoset.jjxmm.win:8010'
# 对于所有情况统一使用直链形式不再使用download.php
# 对文件名/路径进行URL编码处理空格和特殊字符
if not download_url.startswith(('http://', 'https://')):
# 使用http协议和基础域名直接拼接文件名或路径
encoded_path = quote(download_url)
download_url = f'http://leonmmcoset.jjxmm.win:8010/{encoded_path.lstrip("/")}'
# 额外检查:确保主机名有效(包含点)
parsed_url = urlparse(download_url)
if '.' not in parsed_url.netloc:
# 如果主机名仍然无效完全重建URL
base_domain = 'leonmmcoset.jjxmm.win:8010'
path = parsed_url.path or parsed_url.netloc
# 对路径进行URL编码
encoded_path = quote(path)
# 使用http协议
download_url = f'http://leonmmcoset.jjxmm.win:8010/{encoded_path.lstrip("/")}'
# 创建进度对话框
self.progress_dialog = QProgressDialog(
f"正在下载{self.app_data.get('name', '未知应用')}...",
"取消",
0,
100,
parent=self
)
self.progress_dialog.setWindowTitle("正在安装")
self.progress_dialog.setMinimumDuration(0)
# 创建保存路径 - 使用代码文件夹所在的install目录
file_extension = ".exe" # 假设是Windows可执行文件
app_name = self.app_data.get('name', '未知应用').replace(' ', '_')
# 获取当前脚本所在目录
import os
current_dir = os.path.dirname(os.path.abspath(__file__))
# 创建install目录路径
install_dir = os.path.join(current_dir, 'install')
# 确保install目录存在
if not os.path.exists(install_dir):
try:
os.makedirs(install_dir)
except Exception as e:
# 如果创建目录失败,回退到临时目录
show_error_dialog("创建目录失败", f"无法创建安装目录: {str(e)}\n将使用临时目录")
temp_dir = tempfile.gettempdir()
save_path = os.path.join(temp_dir, f"{app_name}_installer{file_extension}")
else:
# 使用install目录
save_path = os.path.join(install_dir, f"{app_name}_installer{file_extension}")
else:
# 目录已存在,直接使用
save_path = os.path.join(install_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()
# 确保subprocess模块可用
import subprocess
# 根据操作系统执行不同的安装操作
if platform.system() == "Windows":
try:
# 在Windows上直接运行可执行文件
subprocess.Popen([file_path], shell=True)
# 创建Sweet Alert风格的下载完成提示
success_box = QMessageBox()
success_box.setWindowTitle("下载完成")
success_box.setIcon(QMessageBox.Information)
success_box.setText(f"安装程序已启动,请按照向导完成安装。\n文件位置:{file_path}")
# 设置字体大小
font = QFont()
font.setPointSize(10)
success_box.setFont(font)
# 添加按钮
success_box.setStandardButtons(QMessageBox.Ok)
# 显示对话框
success_box.exec_()
# 打开下载文件夹并选中文件
subprocess.Popen(f'explorer /select,"{file_path}"')
except Exception as e:
show_error_dialog("启动安装程序失败", f"无法启动安装程序: {str(e)}\n您可以手动运行文件: {file_path}")
# 即使启动安装程序失败,也尝试打开下载文件夹
try:
subprocess.Popen(f'explorer /select,"{file_path}"')
except Exception as ex:
show_error_dialog("打开文件夹失败", f"无法打开下载文件夹: {str(ex)}")
else:
# 其他操作系统
info_box = QMessageBox()
info_box.setWindowTitle("下载完成")
info_box.setIcon(QMessageBox.Information)
info_box.setText(f"文件已下载完成。\n\n文件位置:{file_path}\n\n请手动运行安装程序。")
# 设置字体大小
font = QFont()
font.setPointSize(10)
info_box.setFont(font)
# 添加按钮
info_box.setStandardButtons(QMessageBox.Ok)
# 显示对话框
info_box.exec_()
# 打开下载文件夹
try:
import os
import subprocess
# 获取文件所在目录
folder_path = os.path.dirname(file_path)
if platform.system() == 'Windows':
# Windows下使用explorer打开文件夹并选中文件
subprocess.Popen(f'explorer /select,"{file_path}"')
elif platform.system() == 'Darwin':
# macOS下使用open命令打开文件夹
subprocess.Popen(['open', folder_path])
else:
# Linux下使用xdg-open打开文件夹
subprocess.Popen(['xdg-open', folder_path])
except Exception as e:
show_error_dialog("打开文件夹失败", f"无法打开下载文件夹: {str(e)}")
def on_download_error(self, error_msg):
"""下载错误处理"""
self.progress_dialog.reject()
# 使用Sweet Alert风格弹窗显示错误
show_error_dialog("安装失败", error_msg)
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://leonmmcoset.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):
"""关闭窗口事件"""
# 如果需要在关闭时执行清理操作,可以在这里添加
event.accept()