diff --git a/src/citron/mod_manager/mod_downloader_dialog.cpp b/src/citron/mod_manager/mod_downloader_dialog.cpp index 34ada95a9..590a0845c 100644 --- a/src/citron/mod_manager/mod_downloader_dialog.cpp +++ b/src/citron/mod_manager/mod_downloader_dialog.cpp @@ -1,21 +1,20 @@ // SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include -#include -#include #include #include #include +#include +#include +#include #include +#include +#include #include #include #include "citron/mod_manager/mod_downloader_dialog.h" - -// Generated header from uic #include "ui_mod_downloader_dialog.h" - #include "common/fs/path_util.h" #include "common/logging/log.h" @@ -23,45 +22,31 @@ namespace ModManager { ModDownloaderDialog::ModDownloaderDialog(const ModUpdateInfo& info, QWidget* parent) : QDialog(parent), mod_info(info), current_reply(nullptr) { - ui = new ::Ui::ModDownloaderDialog(); ui->setupUi(this); - network_manager = new QNetworkAccessManager(this); - SetupModList(); - connect(ui->buttonDownload, &QPushButton::clicked, this, &ModDownloaderDialog::OnDownloadClicked); connect(ui->buttonCancel, &QPushButton::clicked, this, &ModDownloaderDialog::OnCancelClicked); } -ModDownloaderDialog::~ModDownloaderDialog() { - delete ui; -} +ModDownloaderDialog::~ModDownloaderDialog() { delete ui; } void ModDownloaderDialog::SetupModList() { ui->treeWidget->setHeaderLabel(QStringLiteral("Version / Mod Name")); - for (auto const& [version, patches] : mod_info.version_patches) { QTreeWidgetItem* version_item = new QTreeWidgetItem(ui->treeWidget); version_item->setText(0, QStringLiteral("Update %1").arg(version)); version_item->setCheckState(0, Qt::Unchecked); version_item->setFlags(version_item->flags() | Qt::ItemIsUserCheckable | Qt::ItemIsAutoTristate); - - // Track names we've already added to this version to avoid duplicates in the UI - std::set seen_names; - + std::set seen; for (const auto& patch : patches) { - if (seen_names.find(patch.name) != seen_names.end()) { - continue; // Skip if we already listed this mod name - } - + if (seen.count(patch.name)) continue; QTreeWidgetItem* mod_item = new QTreeWidgetItem(version_item); mod_item->setText(0, patch.name); mod_item->setCheckState(0, Qt::Unchecked); mod_item->setFlags(mod_item->flags() | Qt::ItemIsUserCheckable); - - seen_names.insert(patch.name); + seen.insert(patch.name); } } ui->treeWidget->expandAll(); @@ -69,34 +54,46 @@ void ModDownloaderDialog::SetupModList() { void ModDownloaderDialog::OnDownloadClicked() { pending_downloads.clear(); + QString os_target; +#ifdef _WIN32 + os_target = QStringLiteral("exe"); +#elif __APPLE__ + os_target = QStringLiteral("zip"); +#else + os_target = QStringLiteral("AppImage"); +#endif for (int i = 0; i < ui->treeWidget->topLevelItemCount(); ++i) { QTreeWidgetItem* version_node = ui->treeWidget->topLevelItem(i); - QString version_str = version_node->text(0).replace(QStringLiteral("Update "), QStringLiteral("")); - + QString v_str = version_node->text(0).replace(QStringLiteral("Update "), QStringLiteral("")); for (int j = 0; j < version_node->childCount(); ++j) { QTreeWidgetItem* mod_node = version_node->child(j); - if (mod_node->checkState(0) == Qt::Checked) { - QString mod_name = mod_node->text(0); - const auto& patches = mod_info.version_patches[version_str]; - - // Find EVERY patch entry that matches this name - // (e.g., grabs both the 'exefs' and 'cheats' entries for "30FPS") - for (const auto& p : patches) { - if (p.name == mod_name) { - pending_downloads.push_back({p, version_str}); + if (mod_node->checkState(0) != Qt::Checked) continue; + const auto& patches = mod_info.version_patches[v_str]; + for (auto p : patches) { + if (p.name == mod_node->text(0)) { + if (p.type == QStringLiteral("tool")) { + QStringList filtered; + for (const QString& f : p.files) { + if (f.endsWith(os_target, Qt::CaseInsensitive)) filtered << f; + } + if (filtered.size() > 1) { + bool ok; + QString choice = QInputDialog::getItem(this, QStringLiteral("Select Architecture"), + QStringLiteral("Choose your system type:"), filtered, 0, false, &ok); + if (ok && !choice.isEmpty()) p.files = {choice}; + else continue; + } else { p.files = filtered; } } + pending_downloads.push_back({p, v_str}); } } } } - if (pending_downloads.empty()) return; - ui->buttonDownload->setEnabled(false); ui->treeWidget->setEnabled(false); ui->progressBar->setVisible(true); - current_download_index = 0; current_file_index = 0; StartNextDownload(); @@ -104,51 +101,53 @@ void ModDownloaderDialog::OnDownloadClicked() { void ModDownloaderDialog::StartNextDownload() { if (current_download_index >= static_cast(pending_downloads.size())) { - QMessageBox::information(this, QStringLiteral("Success"), QStringLiteral("All selected mods have been installed.")); + QMessageBox::information(this, QStringLiteral("Success"), QStringLiteral("All items installed.")); accept(); return; } - const auto& task = pending_downloads[current_download_index]; - const ModPatch& patch = task.patch; - QString version_str = task.version; // We use this to create the folder structure - - if (current_file_index >= patch.files.size()) { + if (current_file_index >= task.patch.files.size()) { current_download_index++; current_file_index = 0; StartNextDownload(); return; } + QString file_val = task.patch.files[current_file_index]; + QUrl url = (task.patch.type == QStringLiteral("tool")) ? QUrl(file_val) : + QUrl(QStringLiteral("https://raw.githubusercontent.com/CollectingW/Citron-Mods/main/%1/%2") + .arg(task.patch.rel_path).arg(file_val)); - QString fileName = patch.files[current_file_index]; - QString urlBase = QStringLiteral("https://raw.githubusercontent.com/CollectingW/Citron-Mods/main/%1/%2"); - QString finalUrl = urlBase.arg(patch.rel_path).arg(fileName); - QUrl url(finalUrl); + QString fileName = file_val.contains(u'/') ? file_val.split(u'/').last() : file_val; + current_reply = network_manager->get(QNetworkRequest(url)); - LOG_INFO(Frontend, "Downloading: {}", url.toString().toStdString()); - - QNetworkRequest request(url); - current_reply = network_manager->get(request); - - connect(current_reply, &QNetworkReply::finished, this, [this, patch, version_str, fileName]() { + connect(current_reply, &QNetworkReply::finished, this, [this, task, fileName]() { if (current_reply->error() == QNetworkReply::NoError) { - std::filesystem::path load_dir = Common::FS::GetCitronPath(Common::FS::CitronPath::LoadDir); - std::filesystem::path tid_path = load_dir / mod_info.title_id.toStdString(); + std::filesystem::path base = Common::FS::GetCitronPath(Common::FS::CitronPath::LoadDir); + std::filesystem::path final_path = base / mod_info.title_id.toStdString(); - // This creates the hierarchy: load / [TID] / [Version] / [ModName] / [exefs/romfs] - std::filesystem::path final_path = tid_path / version_str.toStdString() / - patch.name.toStdString() / patch.type.toStdString(); + if (task.patch.type == QStringLiteral("tool")) { + final_path = Common::FS::GetCitronPath(Common::FS::CitronPath::ConfigDir) / "tools"; + } else { + final_path /= task.version.toStdString(); + final_path /= task.patch.name.toStdString(); + final_path /= task.patch.type.toStdString(); + } - std::error_code ec; - std::filesystem::create_directories(final_path, ec); - - QFile file(QString::fromStdString((final_path / fileName.toStdString()).string())); + std::filesystem::create_directories(final_path); + QString full_save_path = QString::fromStdString((final_path / fileName.toStdString()).string()); + QFile file(full_save_path); if (file.open(QIODevice::WriteOnly)) { file.write(current_reply->readAll()); file.close(); + if (fileName.endsWith(QStringLiteral(".zip"))) { + QProcess::execute(QStringLiteral("unzip"), {full_save_path, QStringLiteral("-d"), QString::fromStdString(final_path.string())}); + } +#ifndef _WIN32 + std::filesystem::permissions(final_path / fileName.toStdString(), + std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec, std::filesystem::perm_options::add); +#endif } } - current_reply->deleteLater(); current_file_index++; ui->progressBar->setValue(((current_download_index + 1) * 100) / pending_downloads.size()); @@ -156,9 +155,6 @@ void ModDownloaderDialog::StartNextDownload() { }); } -void ModDownloaderDialog::OnCancelClicked() { - if (current_reply) current_reply->abort(); - reject(); -} +void ModDownloaderDialog::OnCancelClicked() { if (current_reply) current_reply->abort(); reject(); } } // namespace ModManager