diff --git a/src/frontend/helpers/multi_job.cpp b/src/frontend/helpers/multi_job.cpp index 08adf7b..29bb802 100644 --- a/src/frontend/helpers/multi_job.cpp +++ b/src/frontend/helpers/multi_job.cpp @@ -7,9 +7,10 @@ MultiJob::MultiJob(QObject* parent, Core::SDMCImporter& importer_, std::vector contents_, ExecuteFunc execute_func_, - DeleteFunc delete_func_) + DeleteFunc delete_func_, AbortFunc abort_func_) : QThread(parent), importer(importer_), contents(std::move(contents_)), - execute_func(std::move(execute_func_)), delete_func(std::move(delete_func_)) {} + execute_func(std::move(execute_func_)), delete_func(std::move(delete_func_)), + abort_func(abort_func_) {} MultiJob::~MultiJob() = default; @@ -60,7 +61,7 @@ void MultiJob::run() { void MultiJob::Cancel() { cancelled.store(true); - importer.AbortImporting(); + abort_func(importer); } std::vector MultiJob::GetFailedContents() const { diff --git a/src/frontend/helpers/multi_job.h b/src/frontend/helpers/multi_job.h index f976811..24c0546 100644 --- a/src/frontend/helpers/multi_job.h +++ b/src/frontend/helpers/multi_job.h @@ -16,10 +16,11 @@ public: using ExecuteFunc = std::function; using DeleteFunc = std::function; + using AbortFunc = std::function; explicit MultiJob(QObject* parent, Core::SDMCImporter& importer, std::vector contents, ExecuteFunc execute_func, - DeleteFunc delete_func); + DeleteFunc delete_func, AbortFunc abort_func); ~MultiJob() override; void run() override; @@ -49,6 +50,7 @@ private: std::vector failed_contents; ExecuteFunc execute_func; DeleteFunc delete_func; + AbortFunc abort_func; }; Q_DECLARE_METATYPE(Core::ContentSpecifier) diff --git a/src/frontend/import_dialog.cpp b/src/frontend/import_dialog.cpp index b277cd6..c3ad0e7 100644 --- a/src/frontend/import_dialog.cpp +++ b/src/frontend/import_dialog.cpp @@ -2,8 +2,10 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include #include +#include #include #include #include @@ -11,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -120,6 +123,7 @@ ImportDialog::ImportDialog(QWidget* parent, const Core::Config& config) }); connect(ui->title_view_button, &QRadioButton::toggled, this, &ImportDialog::RepopulateContent); + connect(ui->advanced_button, &QPushButton::clicked, this, &ImportDialog::ShowAdvancedMenu); RelistContent(); UpdateSizeDisplay(); @@ -287,7 +291,7 @@ void ImportDialog::InsertSecondLevelItem(std::size_t row, const Core::ContentSpe "you are doing.")); applet_warning_shown = true; } - total_size += size; + total_selected_size += size; } else { if (!system_warning_shown && !exists && (type == Core::ContentType::SystemArchive || @@ -301,7 +305,7 @@ void ImportDialog::InsertSecondLevelItem(std::size_t row, const Core::ContentSpe "these contents if they do not exist yet.")); system_warning_shown = true; } - total_size -= size; + total_selected_size -= size; } UpdateSizeDisplay(); @@ -317,7 +321,7 @@ void ImportDialog::InsertSecondLevelItem(std::size_t row, const Core::ContentSpe } void ImportDialog::RepopulateContent() { - total_size = 0; + total_selected_size = 0; ui->main->clear(); ui->main->setSortingEnabled(false); @@ -423,10 +427,11 @@ void ImportDialog::UpdateSizeDisplay() { ui->availableSpace->setText( tr("Available Space: %1").arg(ReadableByteSize(storage.bytesAvailable()))); - ui->totalSize->setText(tr("Total Size: %1").arg(ReadableByteSize(total_size))); + ui->totalSize->setText(tr("Total Size: %1").arg(ReadableByteSize(total_selected_size))); ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok) - ->setEnabled(total_size > 0 && total_size <= static_cast(storage.bytesAvailable())); + ->setEnabled(total_selected_size > 0 && + total_selected_size <= static_cast(storage.bytesAvailable())); } void ImportDialog::UpdateItemCheckState(QTreeWidgetItem* item) { @@ -491,98 +496,11 @@ void ImportDialog::StartImporting() { auto to_import = GetSelectedContentList(); const std::size_t total_count = to_import.size(); - // Try to map total_size to int range - // This is equal to ceil(total_size / INT_MAX) - const u64 multiplier = - (total_size + std::numeric_limits::max() - 1) / std::numeric_limits::max(); + auto* job = + new MultiJob(this, importer, std::move(to_import), &Core::SDMCImporter::ImportContent, + &Core::SDMCImporter::DeleteContent, &Core::SDMCImporter::AbortImporting); - auto* label = new QLabel(tr("Initializing...")); - label->setWordWrap(true); - label->setFixedWidth(600); - - // We need to create the bar ourselves to circumvent an issue caused by modal ProgressDialog's - // event handling. - auto* bar = new QProgressBar(this); - bar->setRange(0, static_cast(total_size / multiplier)); - bar->setValue(0); - - auto* dialog = new QProgressDialog(tr("Initializing..."), tr("Cancel"), 0, 0, this); - dialog->setBar(bar); - dialog->setLabel(label); - dialog->setWindowModality(Qt::WindowModal); - dialog->setMinimumDuration(0); - - auto* job = new MultiJob( - this, importer, std::move(to_import), - [](Core::SDMCImporter& importer, const Core::ContentSpecifier& content, - const Core::SDMCImporter::ProgressCallback& callback) { - return importer.ImportContent(content, callback); - }, - [](Core::SDMCImporter& importer, const Core::ContentSpecifier& content) { - return importer.DeleteContent(content); - }); - - connect(job, &MultiJob::NextContent, this, - [this, bar, dialog, multiplier, total_count]( - u64 size_imported, u64 count, Core::ContentSpecifier next_content, int eta) { - bar->setValue(static_cast(size_imported / multiplier)); - dialog->setLabelText( - tr("

(%1/%2) Importing %3 (%4)...

 

%5

") - .arg(count) - .arg(total_count) - .arg(GetContentName(next_content)) - .arg(GetContentTypeName(next_content.type)) - .arg(FormatETA(eta))); - current_content = next_content; - current_count = count; - }); - connect(job, &MultiJob::ProgressUpdated, this, - [this, bar, dialog, multiplier, total_count](u64 total_size_imported, - u64 current_size_imported, int eta) { - bar->setValue(static_cast(total_size_imported / multiplier)); - dialog->setLabelText(tr("

(%1/%2) Importing %3 (%4)...

%5 " - "/ %6

%7

") - .arg(current_count) - .arg(total_count) - .arg(GetContentName(current_content)) - .arg(GetContentTypeName(current_content.type)) - .arg(ReadableByteSize(current_size_imported)) - .arg(ReadableByteSize(current_content.maximum_size)) - .arg(FormatETA(eta))); - }); - connect(job, &MultiJob::Completed, this, [this, dialog, job] { - dialog->setValue(dialog->maximum()); - - const auto failed_contents = job->GetFailedContents(); - if (failed_contents.empty()) { - QMessageBox::information(this, tr("Import Completed"), - tr("Successfully imported the selected contents.")); - } else { - QString list_content; - for (const auto& content : failed_contents) { - list_content.append(QStringLiteral("
  • %1 (%2)
  • ") - .arg(GetContentName(content)) - .arg(GetContentTypeName(content.type))); - } - QMessageBox::critical( - this, tr("Import Failed"), - tr("The following contents couldn't be imported:
      %1
    ").arg(list_content)); - } - - RelistContent(); - }); - connect(dialog, &QProgressDialog::canceled, this, [this, job] { - // Add yet-another-ProgressDialog to indicate cancel progress - auto* cancel_dialog = new QProgressDialog(tr("Canceling..."), tr("Cancel"), 0, 0, this); - cancel_dialog->setWindowModality(Qt::WindowModal); - cancel_dialog->setCancelButton(nullptr); - cancel_dialog->setMinimumDuration(0); - cancel_dialog->setValue(0); - connect(job, &MultiJob::Completed, cancel_dialog, &QProgressDialog::hide); - job->Cancel(); - }); - - job->start(); + RunMultiJob(job, total_count, total_selected_size); } Core::ContentSpecifier ImportDialog::SpecifierFromItem(QTreeWidgetItem* item) const { @@ -604,14 +522,14 @@ void ImportDialog::OnContextMenu(const QPoint& point) { if (specifier.type == Core::ContentType::Application) { QAction* dump_cxi = context_menu.addAction(tr("Dump CXI file")); connect(dump_cxi, &QAction::triggered, - [this, specifier] { StartDumpingCXI(specifier); }); + [this, specifier] { StartDumpingCXISingle(specifier); }); } if (specifier.type == Core::ContentType::Application || specifier.type == Core::ContentType::Update || specifier.type == Core::ContentType::DLC) { QAction* build_cia = context_menu.addAction(tr("Build CIA (standard)")); connect(build_cia, &QAction::triggered, - [this, specifier] { StartBuildingCIA(specifier); }); + [this, specifier] { StartBuildingCIASingle(specifier); }); } } else { // Top level if (!title_view) { @@ -623,24 +541,144 @@ void ImportDialog::OnContextMenu(const QPoint& point) { if (specifier.type == Core::ContentType::Application) { QAction* dump_base_cxi = context_menu.addAction(tr("Dump Base CXI file")); connect(dump_base_cxi, &QAction::triggered, - [this, specifier] { StartDumpingCXI(specifier); }); + [this, specifier] { StartDumpingCXISingle(specifier); }); QAction* build_base_cia = context_menu.addAction(tr("Build Base CIA")); connect(build_base_cia, &QAction::triggered, - [this, specifier] { StartBuildingCIA(specifier); }); + [this, specifier] { StartBuildingCIASingle(specifier); }); } else if (specifier.type == Core::ContentType::Update) { QAction* build_update_cia = context_menu.addAction(tr("Build Update CIA")); connect(build_update_cia, &QAction::triggered, - [this, specifier] { StartBuildingCIA(specifier); }); + [this, specifier] { StartBuildingCIASingle(specifier); }); } else if (specifier.type == Core::ContentType::DLC) { QAction* build_dlc_cia = context_menu.addAction(tr("Build DLC CIA")); connect(build_dlc_cia, &QAction::triggered, - [this, specifier] { StartBuildingCIA(specifier); }); + [this, specifier] { StartBuildingCIASingle(specifier); }); } } } context_menu.exec(ui->main->viewport()->mapToGlobal(point)); } +class AdvancedMenu : public QMenu { +public: + explicit AdvancedMenu(QWidget* parent) : QMenu(parent) {} + +private: + void mousePressEvent(QMouseEvent* event) override { + auto* dialog = static_cast(parentWidget()); + // Block popup menu when clicking on the Advanced button to dismiss the menu. + // With out this, it will immediately bring up the menu again. + if (dialog->childAt(dialog->mapFromGlobal(event->globalPos())) == + dialog->ui->advanced_button) { + + dialog->block_advanced_menu = true; + } + + QMenu::mousePressEvent(event); + } +}; + +void ImportDialog::ShowAdvancedMenu() { + if (block_advanced_menu) { + block_advanced_menu = false; + return; + } + + AdvancedMenu menu(this); + + QAction* batch_dump_cxi = menu.addAction(tr("Batch Dump CXI")); + connect(batch_dump_cxi, &QAction::triggered, this, &ImportDialog::StartBatchDumpingCXI); + + QAction* batch_build_cia = menu.addAction(tr("Batch Build CIA")); + connect(batch_build_cia, &QAction::triggered, this, &ImportDialog::StartBatchBuildingCIA); + + menu.exec(ui->advanced_button->mapToGlobal(ui->advanced_button->rect().bottomLeft())); +} + +// Runs the job, opening a dialog to report is progress. +void ImportDialog::RunMultiJob(MultiJob* job, std::size_t total_count, u64 total_size) { + // Try to map total_size to int range + // This is equal to ceil(total_size / INT_MAX) + const u64 multiplier = + (total_size + std::numeric_limits::max() - 1) / std::numeric_limits::max(); + + auto* label = new QLabel(tr("Initializing...")); + label->setWordWrap(true); + label->setFixedWidth(600); + + // We need to create the bar ourselves to circumvent an issue caused by modal ProgressDialog's + // event handling. + auto* bar = new QProgressBar(this); + bar->setRange(0, static_cast(total_size / multiplier)); + bar->setValue(0); + + auto* dialog = new QProgressDialog(tr("Initializing..."), tr("Cancel"), 0, 0, this); + dialog->setBar(bar); + dialog->setLabel(label); + dialog->setWindowModality(Qt::WindowModal); + dialog->setMinimumDuration(0); + + connect( + job, &MultiJob::NextContent, this, + [this, bar, dialog, multiplier, total_count](u64 size_imported, u64 count, + Core::ContentSpecifier next_content, int eta) { + bar->setValue(static_cast(size_imported / multiplier)); + dialog->setLabelText(tr("

    (%1/%2) %3 (%4)

     

    %5

    ") + .arg(count) + .arg(total_count) + .arg(GetContentName(next_content)) + .arg(GetContentTypeName(next_content.type)) + .arg(FormatETA(eta))); + current_content = next_content; + current_count = count; + }); + connect(job, &MultiJob::ProgressUpdated, this, + [this, bar, dialog, multiplier, total_count](u64 total_size_imported, + u64 current_size_imported, int eta) { + bar->setValue(static_cast(total_size_imported / multiplier)); + dialog->setLabelText(tr("

    (%1/%2) %3 (%4)

    %5 " + "/ %6

    %7

    ") + .arg(current_count) + .arg(total_count) + .arg(GetContentName(current_content)) + .arg(GetContentTypeName(current_content.type)) + .arg(ReadableByteSize(current_size_imported)) + .arg(ReadableByteSize(current_content.maximum_size)) + .arg(FormatETA(eta))); + }); + connect(job, &MultiJob::Completed, this, [this, dialog, job] { + dialog->setValue(dialog->maximum()); + + const auto failed_contents = job->GetFailedContents(); + if (failed_contents.empty()) { + QMessageBox::information(this, tr("threeSD"), tr("All contents done successfully.")); + } else { + QString list_content; + for (const auto& content : failed_contents) { + list_content.append(QStringLiteral("
  • %1 (%2)
  • ") + .arg(GetContentName(content)) + .arg(GetContentTypeName(content.type))); + } + QMessageBox::critical(this, tr("threeSD"), + tr("List of failed contents:
      %1
    ").arg(list_content)); + } + + RelistContent(); + }); + connect(dialog, &QProgressDialog::canceled, this, [this, job] { + // Add yet-another-ProgressDialog to indicate cancel progress + auto* cancel_dialog = new QProgressDialog(tr("Canceling..."), tr("Cancel"), 0, 0, this); + cancel_dialog->setWindowModality(Qt::WindowModal); + cancel_dialog->setCancelButton(nullptr); + cancel_dialog->setMinimumDuration(0); + cancel_dialog->setValue(0); + connect(job, &MultiJob::Completed, cancel_dialog, &QProgressDialog::hide); + job->Cancel(); + }); + + job->start(); +} + // Runs the job, opening a dialog to report its progress. void ImportDialog::RunSimpleJob(SimpleJob* job) { // We need to create the bar ourselves to circumvent an issue caused by modal ProgressDialog's @@ -675,7 +713,7 @@ void ImportDialog::RunSimpleJob(SimpleJob* job) { job->start(); } -void ImportDialog::StartDumpingCXI(const Core::ContentSpecifier& specifier) { +void ImportDialog::StartDumpingCXISingle(const Core::ContentSpecifier& specifier) { const QString path = QFileDialog::getSaveFileName(this, tr("Dump CXI file"), last_dump_cxi_path, tr("CTR Executable Image (*.cxi)")); if (path.isEmpty()) { @@ -696,7 +734,70 @@ void ImportDialog::StartDumpingCXI(const Core::ContentSpecifier& specifier) { RunSimpleJob(job); } -void ImportDialog::StartBuildingCIA(const Core::ContentSpecifier& specifier) { +std::string GetCXIFileName(const Core::ContentSpecifier& specifier) { + return QStringLiteral("%1 (%2).cxi") + .arg(QString::fromStdString(specifier.name)) + .arg(specifier.id, 16, 16, QLatin1Char('0')) + .toStdString(); +} + +void ImportDialog::StartBatchDumpingCXI() { + auto to_import = GetSelectedContentList(); + if (to_import.empty()) { + QMessageBox::warning(this, tr("threeSD"), + tr("Please select the contents you would like to dump as CXIs.")); + return; + } + + const auto removed_iter = std::remove_if( + to_import.begin(), to_import.end(), [](const Core::ContentSpecifier& specifier) { + return specifier.type != Core::ContentType::Application; + }); + if (removed_iter == to_import.begin()) { // No Applications selected + QMessageBox::critical(this, tr("threeSD"), + tr("The contents selected are not supported.
    You can only dump " + "Applications as CXIs.")); + return; + } + if (removed_iter != to_import.end()) { // Some non-Applications selected + QMessageBox::warning(this, tr("threeSD"), + tr("Some contents selected are not supported and will be " + "ignored.
    Only Applications will be dumped as CXIs.")); + } + + to_import.erase(removed_iter, to_import.end()); + + QString path = + QFileDialog::getExistingDirectory(this, tr("Batch Dump CXI"), last_batch_dump_cxi_path); + if (path.isEmpty()) { + return; + } + last_batch_dump_cxi_path = path; + if (!path.endsWith(QChar::fromLatin1('/')) && !path.endsWith(QChar::fromLatin1('\\'))) { + path.append(QStringLiteral("/")); + } + + const auto total_count = to_import.size(); + const auto total_size = + std::accumulate(to_import.begin(), to_import.end(), std::size_t{0}, + [](std::size_t sum, const Core::ContentSpecifier& specifier) { + return sum + specifier.maximum_size; + }); + auto* job = new MultiJob( + this, importer, std::move(to_import), + [path](Core::SDMCImporter& importer, const Core::ContentSpecifier& specifier, + const Core::SDMCImporter::ProgressCallback& callback) { + return importer.DumpCXI(specifier, path.toStdString() + GetCXIFileName(specifier), + callback); + }, + [path](Core::SDMCImporter& /*importer*/, const Core::ContentSpecifier& specifier) { + FileUtil::Delete(path.toStdString() + GetCXIFileName(specifier)); + }, + &Core::SDMCImporter::AbortDumpCXI); + RunMultiJob(job, total_count, total_size); +} + +void ImportDialog::StartBuildingCIASingle(const Core::ContentSpecifier& specifier) { const QString path = QFileDialog::getSaveFileName(this, tr("Build CIA"), last_build_cia_path, tr("CTR Importable Archive (*.cia)")); if (path.isEmpty()) { @@ -716,3 +817,69 @@ void ImportDialog::StartBuildingCIA(const Core::ContentSpecifier& specifier) { [this] { importer.AbortBuildCIA(); }); RunSimpleJob(job); } + +std::string GetCIAFileName(const Core::ContentSpecifier& specifier) { + return QStringLiteral("%1 (%2).cia") + .arg(QString::fromStdString(specifier.name)) + .arg(specifier.id, 16, 16, QLatin1Char('0')) + .toStdString(); +} + +void ImportDialog::StartBatchBuildingCIA() { + auto to_import = GetSelectedContentList(); + if (to_import.empty()) { + QMessageBox::warning(this, tr("threeSD"), + tr("Please select the contents you would like to build as CIAs.")); + return; + } + + const auto removed_iter = std::remove_if( + to_import.begin(), to_import.end(), [](const Core::ContentSpecifier& specifier) { + return specifier.type != Core::ContentType::Application && + specifier.type != Core::ContentType::Update && + specifier.type != Core::ContentType::DLC; + }); + if (removed_iter == to_import.begin()) { // No Titles selected + QMessageBox::critical(this, tr("threeSD"), + tr("The contents selected are not supported.
    You can only build " + "CIAs from Applications, Updates and DLCs.")); + return; + } + if (removed_iter != to_import.end()) { // Some non-Titles selected + QMessageBox::warning( + this, tr("threeSD"), + tr("Some contents selected are not supported and will be ignored.
    Only " + "Applications, Updates and DLCs will be built as CIAs.")); + } + + to_import.erase(removed_iter, to_import.end()); + + QString path = + QFileDialog::getExistingDirectory(this, tr("Batch Build CIA"), last_batch_build_cia_path); + if (path.isEmpty()) { + return; + } + last_batch_build_cia_path = path; + if (!path.endsWith(QChar::fromLatin1('/')) && !path.endsWith(QChar::fromLatin1('\\'))) { + path.append(QStringLiteral("/")); + } + + const auto total_count = to_import.size(); + const auto total_size = + std::accumulate(to_import.begin(), to_import.end(), std::size_t{0}, + [](std::size_t sum, const Core::ContentSpecifier& specifier) { + return sum + specifier.maximum_size; + }); + auto* job = new MultiJob( + this, importer, std::move(to_import), + [path](Core::SDMCImporter& importer, const Core::ContentSpecifier& specifier, + const Core::SDMCImporter::ProgressCallback& callback) { + return importer.BuildCIA(specifier, path.toStdString() + GetCIAFileName(specifier), + callback); + }, + [path](Core::SDMCImporter& /*importer*/, const Core::ContentSpecifier& specifier) { + FileUtil::Delete(path.toStdString() + GetCIAFileName(specifier)); + }, + &Core::SDMCImporter::AbortBuildCIA); + RunMultiJob(job, total_count, total_size); +} diff --git a/src/frontend/import_dialog.h b/src/frontend/import_dialog.h index 27eb1f1..6273505 100644 --- a/src/frontend/import_dialog.h +++ b/src/frontend/import_dialog.h @@ -12,6 +12,8 @@ #include "core/importer.h" #include "core/ncch/ncch_container.h" +class AdvancedMenu; +class MultiJob; class SimpleJob; class QTreeWidgetItem; @@ -32,8 +34,15 @@ private: void UpdateSizeDisplay(); void UpdateItemCheckState(QTreeWidgetItem* item); std::vector GetSelectedContentList(); + void StartImporting(); + void StartBatchDumpingCXI(); + QString last_batch_dump_cxi_path; // Used for recording last path in StartBatchDumpingCXI + + void StartBatchBuildingCIA(); + QString last_batch_build_cia_path; // Used for recording last path in StartBatchBuildingCIA + void InsertTopLevelItem(const QString& text, QPixmap icon = {}); // When replace_name and replace_icon are present they are used instead of those in `content`. void InsertSecondLevelItem(std::size_t row, const Core::ContentSpecifier& content, @@ -43,13 +52,16 @@ private: Core::ContentSpecifier SpecifierFromItem(QTreeWidgetItem* item) const; void OnContextMenu(const QPoint& point); + void ShowAdvancedMenu(); + + void RunMultiJob(MultiJob* job, std::size_t total_count, u64 total_size); void RunSimpleJob(SimpleJob* job); - void StartDumpingCXI(const Core::ContentSpecifier& content); - QString last_dump_cxi_path; // Used for recording last path in StartDumpingCXI + void StartDumpingCXISingle(const Core::ContentSpecifier& content); + QString last_dump_cxi_path; // Used for recording last path in StartDumpingCXISingle - void StartBuildingCIA(const Core::ContentSpecifier& content); - QString last_build_cia_path; // Used for recording last path in StartBuildingCIA + void StartBuildingCIASingle(const Core::ContentSpecifier& content); + QString last_build_cia_path; // Used for recording last path in StartBuildingCIASingle std::unique_ptr ui; @@ -57,12 +69,16 @@ private: bool has_cert_db = false; Core::SDMCImporter importer; std::vector contents; - u64 total_size = 0; + u64 total_selected_size = 0; // HACK: To tell whether the checkbox state change is a programmatic trigger // TODO: Is there a more elegant way of doing the same? bool program_trigger = false; + // HACK: Block advanced menu trigger once. + bool block_advanced_menu = false; + friend class AdvancedMenu; + // Whether the System Archive / System Data warning has been shown bool system_warning_shown = false; // Whether the Applets warning has been shown diff --git a/src/frontend/import_dialog.ui b/src/frontend/import_dialog.ui index 196dcdc..3cf2b4f 100644 --- a/src/frontend/import_dialog.ui +++ b/src/frontend/import_dialog.ui @@ -16,6 +16,13 @@ + + + + Advanced... + + +