Files
leonapp/pyqt5fluentdesign/app_detail_window.py
Leonmmcoset 0e54dce62a feat(配置管理): 添加配置系统和设置界面
添加基于QConfig的配置管理系统,支持自动打开安装程序和应用打开方式的设置
新增设置标签页用于管理应用配置
更新安装确认对话框以支持配置选项
将版本号更新为Prerelease 1
2025-09-27 21:53:54 +08:00

1013 lines
43 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, MessageBox
from PyQt5.QtWidgets import QProgressDialog
from leonapp_gui import get_global_settings
# 检查是否有应用数据
if not hasattr(self, 'app_data'):
show_error_dialog("错误", "应用数据未加载,请刷新页面后重试。")
return
# 创建QFluentWidgets风格的确认对话框
# 从全局设置获取默认的自动打开选项
global_settings = get_global_settings()
default_auto_open = bool(global_settings.auto_open_installer)
confirm_box = MessageBox("确认安装", f"您确定要安装{self.app_data.get('name', '未知应用')}吗?", self)
# 添加自动打开复选框
from PyQt5.QtWidgets import QCheckBox
self.auto_open_checkbox = QCheckBox("下载后自动打开安装程序", confirm_box)
self.auto_open_checkbox.setChecked(default_auto_open) # 使用全局设置作为默认值
# 将复选框添加到对话框中
layout = confirm_box.layout()
layout.addWidget(self.auto_open_checkbox)
# 显示对话框
reply = confirm_box.exec()
# 保存用户选择
self.auto_open_installer = self.auto_open_checkbox.isChecked()
if reply == 1: # QFluentWidgets的MessageBox返回1表示确认
# 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目录
# 从下载URL中提取文件扩展名
import os
_, file_extension = os.path.splitext(download_url)
# 如果没有扩展名或者扩展名不合法,使用默认扩展名.zip
if not file_extension or len(file_extension) < 2:
file_extension = ".zip"
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:
# 使用MessageBox显示下载完成提示
from qfluentwidgets import MessageBox
# 导入全局设置
from leonapp_gui import get_global_settings
global_settings = get_global_settings()
app_open_method = global_settings.app_open_method
# 根据用户选择决定是否自动打开安装程序
if hasattr(self, 'auto_open_installer') and self.auto_open_installer:
try:
# 根据配置的应用打开方式执行不同的操作
# 0: Windows直接运行应用程序, 1: 打开文件夹方式
if app_open_method == 0: # Windows直接运行应用程序
# 根据文件扩展名决定如何处理
import os
_, ext = os.path.splitext(file_path)
ext = ext.lower()
# 对于可执行文件,直接运行
if ext in [".exe", ".msi", ".bat", ".cmd"]:
subprocess.Popen([file_path], shell=True)
# 对于其他文件类型,使用系统默认程序打开
else:
os.startfile(file_path)
download_box = MessageBox("下载完成", f"安装程序已启动,请按照向导完成安装。\n文件位置:{file_path}", self)
download_box.exec()
return # 成功打开安装程序后不需要再打开文件夹
else: # 打开文件夹方式
# 打开下载文件夹并选中文件
subprocess.Popen(f'explorer /select,"{file_path}"')
download_box = MessageBox("下载完成", f"文件已下载完成,文件夹已打开。\n文件位置:{file_path}", self)
download_box.exec()
return
except Exception as e:
show_error_dialog("操作失败", f"无法执行操作: {str(e)}\n您可以手动运行文件: {file_path}")
else:
# 不自动打开,只显示下载完成信息
download_box = MessageBox("下载完成", f"文件已下载完成。\n文件位置:{file_path}", self)
download_box.exec()
# 如果没有自动打开,或者自动打开失败后,根据配置决定默认行为
if app_open_method == 0: # 默认直接运行应用程序
try:
import os
_, ext = os.path.splitext(file_path)
ext = ext.lower()
if ext in [".exe", ".msi", ".bat", ".cmd"]:
subprocess.Popen([file_path], shell=True)
else:
os.startfile(file_path)
except Exception:
# 如果运行失败,回退到打开文件夹
subprocess.Popen(f'explorer /select,"{file_path}"')
else: # 默认打开文件夹
# 打开下载文件夹并选中文件
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()