// Copyright 2019 threeSD Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include #include #include #include #include #include #include #include #include #include #include #include #include "common/assert.h" #include "common/logging/log.h" #include "common/progress_callback.h" #include "common/scope_exit.h" #include "frontend/cia_build_dialog.h" #include "frontend/helpers/frontend_common.h" #include "frontend/helpers/multi_job.h" #include "frontend/helpers/rate_limited_progress_dialog.h" #include "frontend/helpers/simple_job.h" #include "frontend/import_dialog.h" #include "frontend/title_info_dialog.h" #include "ui_import_dialog.h" // Groups that are used in the frontend. This is organized in a slightly different way than Core. enum class DisplayGroup { Application, Update, DLC, Savegame, Extdata, Sysdata, SystemTitle, SystemApplet, }; // clang-format off // group, singular name, plural name, icon name constexpr std::array, 8> DisplayGroupMap{{ {DisplayGroup::Application, QT_TR_NOOP("Application"), QT_TR_NOOP("Applications"), "app"}, {DisplayGroup::Update, QT_TR_NOOP("Update"), QT_TR_NOOP("Updates"), "update"}, {DisplayGroup::DLC, QT_TR_NOOP("DLC"), QT_TR_NOOP("DLCs"), "dlc"}, {DisplayGroup::Savegame, QT_TR_NOOP("Save Data"), QT_TR_NOOP("Save Data"), "save_data"}, {DisplayGroup::Extdata, QT_TR_NOOP("Extra Data"), QT_TR_NOOP("Extra Data"), "save_data"}, {DisplayGroup::Sysdata, QT_TR_NOOP("System Data"), QT_TR_NOOP("System Data"), "system_data"}, {DisplayGroup::SystemTitle, QT_TR_NOOP("System Title"), QT_TR_NOOP("System Titles"), "hos"}, {DisplayGroup::SystemApplet, QT_TR_NOOP("System Applet"), QT_TR_NOOP("System Applets"), "hos"}, }}; // clang-format on static DisplayGroup GetDisplayGroup(const Core::ContentSpecifier& specifier) { if (specifier.type == Core::ContentType::Title) { switch (specifier.id >> 32) { case 0x00040000: return DisplayGroup::Application; case 0x0004000e: return DisplayGroup::Update; case 0x0004008c: return DisplayGroup::DLC; default: UNREACHABLE(); } } if (specifier.type == Core::ContentType::Savegame) { return DisplayGroup::Savegame; } if (specifier.type == Core::ContentType::Extdata) { return DisplayGroup::Extdata; } if (specifier.type == Core::ContentType::NandSavegame || specifier.type == Core::ContentType::NandExtdata || specifier.type == Core::ContentType::Sysdata) { // These are grouped together return DisplayGroup::Sysdata; } if (specifier.type == Core::ContentType::NandTitle) { return (specifier.id >> 32) == 0x00040030 ? DisplayGroup::SystemApplet : DisplayGroup::SystemTitle; } UNREACHABLE(); } static QString GetContentName(const Core::ContentSpecifier& specifier) { if (specifier.type == Core::ContentType::NandSavegame) { return QObject::tr("System Save 0x%1", "ImportDialog") .arg(specifier.id, 16, 16, QLatin1Char('0')); } if (specifier.type == Core::ContentType::NandExtdata) { return QObject::tr("System Extra 0x%1", "ImportDialog") .arg(specifier.id, 16, 16, QLatin1Char('0')); } return specifier.name.empty() ? QStringLiteral("0x%1").arg(specifier.id, 16, 16, QLatin1Char('0')) : QString::fromStdString(specifier.name); } template static QString GetDisplayGroupName(DisplayGroup group) { if constexpr (Plural) { return QObject::tr(std::get<2>(DisplayGroupMap.at(static_cast(group))), "ImportDialog"); } else { return QObject::tr(std::get<1>(DisplayGroupMap.at(static_cast(group))), "ImportDialog"); } } template static QString GetDisplayGroupName(const Core::ContentSpecifier& specifier) { return GetDisplayGroupName(GetDisplayGroup(specifier)); } static QPixmap GetDisplayGroupIcon(DisplayGroup group) { return QIcon::fromTheme( QString::fromUtf8(std::get<3>(DisplayGroupMap.at(static_cast(group))))) .pixmap(24); } static QPixmap GetContentIcon(const Core::ContentSpecifier& specifier) { if (!specifier.icon.empty()) { return QPixmap::fromImage(QImage(reinterpret_cast(specifier.icon.data()), 24, 24, QImage::Format::Format_RGB16)); } // Use a category icon to distinguish between different types of System Data if (specifier.type == Core::ContentType::NandSavegame || specifier.type == Core::ContentType::NandExtdata) { return GetDisplayGroupIcon(DisplayGroup::Savegame); } if (specifier.type == Core::ContentType::Sysdata) { return GetDisplayGroupIcon(DisplayGroup::Sysdata); } // Use a special icon for NAND non-executable archives if (specifier.type == Core::ContentType::NandTitle) { const auto id_high = specifier.id >> 32; if (id_high == 0x0004001b || id_high == 0x0004009b || id_high == 0x000400db) { return QIcon::fromTheme(QStringLiteral("system_archive")).pixmap(24); } } // Return a null icon otherwise return QIcon::fromTheme(QStringLiteral("unknown")).pixmap(24); } ImportDialog::ImportDialog(QWidget* parent, const Core::Config& config_) : DPIAwareDialog(parent, 560, 320), ui(std::make_unique()), config(config_) { qRegisterMetaType("u64"); qRegisterMetaType("std::size_t"); qRegisterMetaType(); ui->setupUi(this); setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); RelistContent(); UpdateSizeDisplay(); ui->title_view_button->setChecked(true); ui->buttonBox->button(QDialogButtonBox::StandardButton::Reset)->setText(tr("Refresh")); connect(ui->buttonBox, &QDialogButtonBox::clicked, [this](QAbstractButton* button) { if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)) { StartImporting(); } else if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Cancel)) { reject(); } else { RelistContent(); } }); connect(ui->title_view_button, &QRadioButton::toggled, this, &ImportDialog::RepopulateContent); connect(ui->advanced_button, &QPushButton::clicked, this, &ImportDialog::ShowAdvancedMenu); ui->main->sortByColumn(-1, Qt::AscendingOrder); // disable sorting by default ui->main->header()->setStretchLastSection(false); connect(ui->main, &QTreeWidget::customContextMenuRequested, this, &ImportDialog::OnContextMenu); } ImportDialog::~ImportDialog() = default; void ImportDialog::SetContentSizes(int previous_width, int previous_height) { const int current_width = width(); if (previous_width == 0) { // first time ui->main->setColumnWidth(0, current_width * 0.66); ui->main->setColumnWidth(1, current_width * 0.145); ui->main->setColumnWidth(2, current_width * 0.10); } else { // proportionally update column widths for (int i : {0, 1, 2}) { ui->main->setColumnWidth(i, ui->main->columnWidth(i) * current_width / previous_width); } } } void ImportDialog::RelistContent() { auto* dialog = new RateLimitedProgressDialog(tr("Loading Contents..."), tr("Cancel"), 0, 0, this); dialog->setCancelButton(nullptr); using FutureWatcher = QFutureWatcher; auto* future_watcher = new FutureWatcher(this); connect(future_watcher, &FutureWatcher::finished, this, [this, dialog] { dialog->hide(); if (importer->IsGood()) { RepopulateContent(); } else { QMessageBox::critical( this, tr("Importer Error"), tr("Failed to initalize the importer.\nRefer to the log for details.")); reject(); } }); auto future = QtConcurrent::run( [&importer = this->importer, &config = this->config, &contents = this->contents] { if (!importer) { importer = std::make_unique(config); } if (importer->IsGood()) { contents = importer->ListContent(); } }); future_watcher->setFuture(future); } constexpr Qt::ItemDataRole SpecifierIndexRole = Qt::UserRole; /// Supports readable size display and sorting class ContentListItem final : public QTreeWidgetItem { public: explicit ContentListItem(QString name, u64 content_size_, QString exists) : QTreeWidgetItem{{std::move(name), ReadableByteSize(content_size_), std::move(exists)}}, content_size(content_size_) {} explicit ContentListItem(QString name, u64 content_size_, QString exists, std::size_t idx) : QTreeWidgetItem{{std::move(name), ReadableByteSize(content_size_), std::move(exists)}}, content_size(content_size_) { setData(0, SpecifierIndexRole, static_cast(idx)); } ~ContentListItem() override = default; private: bool operator<(const QTreeWidgetItem& other_item) const override { const auto* other = dynamic_cast(&other_item); if (!other) { return false; } const int column = treeWidget()->sortColumn(); if (column == 1) { // size return content_size < other->content_size; } else { return text(column) < other->text(column); } } u64 content_size; }; void ImportDialog::InsertTopLevelItem(QString text, QPixmap icon) { auto* item = new QTreeWidgetItem{{text}}; item->setIcon(0, QIcon(std::move(icon))); item->setFlags(item->flags() | Qt::ItemIsAutoTristate); item->setCheckState(0, Qt::Unchecked); // required to give the item a checkbox ui->main->invisibleRootItem()->addChild(item); item->setFirstColumnSpanned(true); } void ImportDialog::InsertTopLevelItem(QString text, QPixmap icon, u64 total_size, QString exists) { auto* item = new ContentListItem{std::move(text), total_size, std::move(exists)}; item->setIcon(0, QIcon(std::move(icon))); item->setFlags(item->flags() | Qt::ItemIsAutoTristate); item->setCheckState(0, Qt::Unchecked); // required to give the item a checkbox ui->main->invisibleRootItem()->addChild(item); } // Content types that themselves form a 'Title' like entity. constexpr std::array SpecialDisplayGroupList{{ DisplayGroup::Sysdata, DisplayGroup::SystemTitle, DisplayGroup::SystemApplet, }}; void ImportDialog::InsertSecondLevelItem(std::size_t row, const Core::ContentSpecifier& content, std::size_t id, QString replace_name, QPixmap replace_icon) { const bool use_title_view = ui->title_view_button->isChecked(); const auto group = GetDisplayGroup(content); QString name; if (use_title_view) { if (row == 0) { name = QStringLiteral("%1 (%2)") .arg(GetContentName(content)) .arg(GetDisplayGroupName(group)); } else if (row <= SpecialDisplayGroupList.size()) { name = GetContentName(content); } else { name = GetDisplayGroupName(group); } } else { name = GetContentName(content); } if (!replace_name.isEmpty()) { name = replace_name; } auto* item = new ContentListItem{name, content.maximum_size, content.already_exists ? tr("Yes") : tr("No"), id}; // Set icon QPixmap icon; if (replace_icon.isNull()) { const bool in_special_group = std::find(SpecialDisplayGroupList.begin(), SpecialDisplayGroupList.end(), group) != SpecialDisplayGroupList.end(); if (use_title_view && !in_special_group) { icon = GetDisplayGroupIcon(group); } else { icon = GetContentIcon(content); } } else { icon = replace_icon; } item->setIcon(0, QIcon(icon)); // Skip System Applets, but enable everything else by default. if (!content.already_exists && group != DisplayGroup::SystemApplet) { item->setCheckState(0, Qt::Checked); total_selected_size += content.maximum_size; } else { item->setCheckState(0, Qt::Unchecked); } ui->main->invisibleRootItem()->child(row)->addChild(item); } void ImportDialog::OnItemChanged(QTreeWidgetItem* item, int column) { // Only handle second level items (with checkboxes) if (column != 0 || !item->parent()) { return; } const auto& specifier = SpecifierFromItem(item); const auto group = GetDisplayGroup(specifier); if (item->checkState(0) == Qt::Checked) { if (!applet_warning_shown && !specifier.already_exists && group == DisplayGroup::SystemApplet) { QMessageBox::warning( this, tr("Warning"), tr("You are trying to import System Applets.\nThese are known to cause problems " "with certain games.\nOnly proceed if you understand what you are doing.")); applet_warning_shown = true; } total_selected_size += specifier.maximum_size; } else { if (!system_warning_shown && !specifier.already_exists && (group == DisplayGroup::Sysdata || group == DisplayGroup::SystemTitle)) { QMessageBox::warning(this, tr("Warning"), tr("You are de-selecting important files that may be necessary " "for your imported games to run.\nIt is highly recommended to " "import these contents if they do not exist yet.")); system_warning_shown = true; } total_selected_size -= specifier.maximum_size; } UpdateSizeDisplay(); } void ImportDialog::RepopulateContent() { if (contents.empty()) { // why??? QMessageBox::warning(this, tr("threeSD"), tr("Sorry, there are no contents available.")); reject(); return; } total_selected_size = 0; ui->main->clear(); ui->main->setSortingEnabled(false); disconnect(ui->main, &QTreeWidget::itemChanged, this, &ImportDialog::OnItemChanged); struct TitleMapEntry { QString name; QPixmap icon; std::vector contents; }; std::map title_map; std::unordered_map extdata_id_map; // extdata ID -> title ID for (const auto& content : contents) { // Applications if (content.type == Core::ContentType::Title && (content.id >> 32) == 0x00040000) { title_map[content.id].name = GetContentName(content); title_map[content.id].icon = GetContentIcon(content); extdata_id_map.emplace(content.extdata_id, content.id); } } for (const auto& content : contents) { if (content.type == Core::ContentType::Extdata) { if (extdata_id_map.count(content.id)) { const u64 title_id = extdata_id_map.at(content.id); title_map[title_id].contents.emplace_back(&content); } } else if (content.type == Core::ContentType::Title || content.type == Core::ContentType::Savegame) { if (title_map.count(content.id)) { title_map[content.id].contents.emplace_back(&content); } } } const bool use_title_view = ui->title_view_button->isChecked(); if (use_title_view) { // Create 'Ungrouped' category. InsertTopLevelItem(tr("Ungrouped"), QIcon::fromTheme(QStringLiteral("unknown")).pixmap(24)); // Create categories for special display groups. for (std::size_t i = 0; i < SpecialDisplayGroupList.size(); ++i) { InsertTopLevelItem(GetDisplayGroupName(SpecialDisplayGroupList[i]), GetDisplayGroupIcon(SpecialDisplayGroupList[i])); } // Applications std::unordered_map title_row_map; for (auto& [id, entry] : title_map) { // Process the application's contents u64 total_size = 0; bool has_exist = false, has_non_exist = false; for (const auto* content : entry.contents) { total_size += content->maximum_size; if (content->already_exists) { has_exist = true; } else { has_non_exist = true; } } QString exist_text; if (!has_exist) { exist_text = tr("No"); } else if (!has_non_exist) { exist_text = tr("Yes"); } else { exist_text = tr("Part"); } InsertTopLevelItem(std::move(entry.name), std::move(entry.icon), total_size, std::move(exist_text)); title_row_map.emplace(id, ui->main->invisibleRootItem()->childCount() - 1); } for (std::size_t i = 0; i < contents.size(); ++i) { const auto& content = contents[i]; std::size_t row = 0; // 0 for ungrouped (default) switch (content.type) { case Core::ContentType::Title: case Core::ContentType::Savegame: { // Fix the id const auto real_id = content.id & 0xffffff00ffffffff; row = title_row_map.count(real_id) ? title_row_map.at(real_id) : 0; break; } case Core::ContentType::Extdata: { if (extdata_id_map.count(content.id)) { row = title_row_map.at(extdata_id_map.at(content.id)); } else { row = 0; // Ungrouped } break; } default: { const std::size_t idx = std::find(SpecialDisplayGroupList.begin(), SpecialDisplayGroupList.end(), GetDisplayGroup(content)) - SpecialDisplayGroupList.begin(); ASSERT_MSG(idx < SpecialDisplayGroupList.size(), "Display Group not handled"); row = idx + 1; break; } } InsertSecondLevelItem(row, content, i); } } else { for (const auto& [group, singular_name, plural_name, icon_name] : DisplayGroupMap) { InsertTopLevelItem(tr(plural_name), GetDisplayGroupIcon(group)); } for (std::size_t i = 0; i < contents.size(); ++i) { const auto& content = contents[i]; QString name{}; QPixmap icon{}; if (content.type == Core::ContentType::Savegame) { if (title_map.count(content.id)) { name = title_map.at(content.id).name; icon = title_map.at(content.id).icon; } } else if (content.type == Core::ContentType::Extdata) { if (extdata_id_map.count(content.id)) { u64 title_id = extdata_id_map.at(content.id); name = title_map.at(title_id).name; icon = title_map.at(title_id).icon; } } InsertSecondLevelItem(static_cast(GetDisplayGroup(content)), content, i, name, icon); } } ui->main->setSortingEnabled(true); connect(ui->main, &QTreeWidget::itemChanged, this, &ImportDialog::OnItemChanged); UpdateSizeDisplay(); } void ImportDialog::UpdateSizeDisplay() { QStorageInfo storage(QString::fromStdString(config.user_path)); if (!storage.isValid() || !storage.isReady()) { LOG_ERROR(Frontend, "Storage {} is not good", config.user_path); QMessageBox::critical( this, tr("Bad Storage"), tr("An error occured while trying to get available space for the storage.")); reject(); return; } ui->availableSpace->setText( tr("Available Space: %1").arg(ReadableByteSize(storage.bytesAvailable()))); ui->totalSize->setText(tr("Total Size: %1").arg(ReadableByteSize(total_selected_size))); ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok) ->setEnabled(total_selected_size > 0 && total_selected_size <= static_cast(storage.bytesAvailable())); } std::vector ImportDialog::GetSelectedContentList() { std::vector to_import; for (int i = 0; i < ui->main->invisibleRootItem()->childCount(); ++i) { const auto* item = ui->main->invisibleRootItem()->child(i); for (int j = 0; j < item->childCount(); ++j) { if (item->child(j)->checkState(0) == Qt::Checked) { to_import.emplace_back( contents[item->child(j)->data(0, SpecifierIndexRole).toInt()]); } } } return to_import; } Core::ContentSpecifier ImportDialog::SpecifierFromItem(QTreeWidgetItem* item) const { return contents[item->data(0, SpecifierIndexRole).toInt()]; } void ImportDialog::OnContextMenu(const QPoint& point) { QTreeWidgetItem* item = ui->main->itemAt(point.x(), point.y()); if (!item) { return; } const bool title_view = ui->title_view_button->isChecked(); QMenu context_menu(this); if (item->parent()) { // Second level const auto& specifier = SpecifierFromItem(item); const auto group = GetDisplayGroup(specifier); if (group == DisplayGroup::Application) { context_menu.addAction(tr("Dump CXI file"), [this, specifier] { StartDumpingCXISingle(specifier); }); } if (Core::IsTitle(specifier.type)) { context_menu.addAction(tr("Build CIA..."), [this, specifier] { StartBuildingCIASingle(specifier); }); context_menu.addAction(tr("Show Title Info"), [this, specifier] { TitleInfoDialog dialog(this, *importer, specifier); dialog.exec(); }); } } else { // Top level if (!title_view) { return; } for (int i = 0; i < item->childCount(); ++i) { const auto& specifier = SpecifierFromItem(item->child(i)); const auto group = GetDisplayGroup(specifier); if (group == DisplayGroup::Application) { context_menu.addAction(tr("Dump Base CXI file"), [this, specifier] { StartDumpingCXISingle(specifier); }); context_menu.addAction(tr("Build Base CIA"), [this, specifier] { StartBuildingCIASingle(specifier); }); } else if (group == DisplayGroup::Update) { context_menu.addAction(tr("Build Update CIA"), [this, specifier] { StartBuildingCIASingle(specifier); }); } else if (group == DisplayGroup::DLC) { context_menu.addAction(tr("Build DLC CIA"), [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); menu.addAction(tr("Batch Dump CXI"), this, &ImportDialog::StartBatchDumpingCXI); menu.addAction(tr("Batch Build CIA"), this, &ImportDialog::StartBatchBuildingCIA); menu.exec(ui->advanced_button->mapToGlobal(ui->advanced_button->rect().bottomLeft())); } static QString FormatETA(int eta) { if (eta < 0) { return QStringLiteral(" "); } return QCoreApplication::translate("ImportDialog", "ETA %1m%2s") .arg(eta / 60, 2, 10, QLatin1Char('0')) .arg(eta % 60, 2, 10, QLatin1Char('0')); } // 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); auto* dialog = new RateLimitedProgressDialog(tr("Initializing..."), tr("Cancel"), 0, static_cast(total_size / multiplier), this); dialog->setLabel(label); connect(job, &MultiJob::NextContent, this, [this, dialog, multiplier, total_count](std::size_t count, u64 total_imported_size, const Core::ContentSpecifier& next_content, int eta) { if (dialog->wasCanceled()) { return; } dialog->Update(static_cast(total_imported_size / multiplier), tr("

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

 

%5

") .arg(count) .arg(total_count) .arg(GetContentName(next_content)) .arg(GetDisplayGroupName(next_content)) .arg(FormatETA(eta))); current_content = next_content; current_count = count; }); connect( job, &MultiJob::ProgressUpdated, this, [this, dialog, multiplier, total_count](u64 current_imported_size, u64 total_imported_size, int eta) { if (dialog->wasCanceled()) { return; } dialog->Update( static_cast(total_imported_size / multiplier), tr("

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

%5 / %6

%7

") .arg(current_count) .arg(total_count) .arg(GetContentName(current_content)) .arg(GetDisplayGroupName(current_content)) .arg(ReadableByteSize(current_imported_size)) .arg(ReadableByteSize(current_content.maximum_size)) .arg(FormatETA(eta))); }); connect(job, &MultiJob::Completed, this, [this, dialog, job] { dialog->hide(); 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(GetDisplayGroupName(content))); } 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 RateLimitedProgressDialog(tr("Canceling..."), tr("Cancel"), 0, 0, this); cancel_dialog->setCancelButton(nullptr); connect(job, &MultiJob::Completed, cancel_dialog, &QProgressDialog::hide); job->Cancel(); }); job->start(); } void ImportDialog::StartImporting() { UpdateSizeDisplay(); if (!ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->isEnabled()) { // Space is no longer enough QMessageBox::warning(this, tr("Not Enough Space"), tr("Your disk does not have enough space to hold imported data.")); return; } auto to_import = GetSelectedContentList(); const std::size_t total_count = to_import.size(); auto* job = new MultiJob(this, *importer, std::move(to_import), &Core::SDMCImporter::ImportContent, &Core::SDMCImporter::AbortImporting); RunMultiJob(job, total_count, total_selected_size); } // CXI dumping 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()) { return; } last_dump_cxi_path = QFileInfo(path).path(); auto* job = new SimpleJob( this, [this, specifier, path](const Common::ProgressCallback& callback) { return importer->DumpCXI(specifier, path.toStdString(), callback); }, [this] { importer->AbortDumpCXI(); }); job->StartWithProgressDialog(this); } 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::Title || (specifier.id >> 32) != 0x00040000; }); 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(), u64{0}, [](u64 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 Common::ProgressCallback& callback) { return importer.DumpCXI(specifier, path.toStdString(), callback, true); }, &Core::SDMCImporter::AbortDumpCXI); RunMultiJob(job, total_count, total_size); } // CIA building void ImportDialog::StartBuildingCIASingle(const Core::ContentSpecifier& specifier) { CIABuildDialog dialog(this, /*is_dir*/ false, /*is_nand*/ specifier.type == Core::ContentType::NandTitle, /*enable_legit*/ importer->CanBuildLegitCIA(specifier), last_build_cia_path); if (dialog.exec() != QDialog::Accepted) { return; } const auto& [path, type] = dialog.GetResults(); last_build_cia_path = QFileInfo(path).path(); auto* job = new SimpleJob( this, [this, specifier, path = path, type = type](const Common::ProgressCallback& callback) { return importer->BuildCIA(type, specifier, path.toStdString(), callback); }, [this] { importer->AbortBuildCIA(); }); job->StartWithProgressDialog(this); } 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 !Core::IsTitle(specifier.type); }); 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, DLCs and System Titles.")); 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, DLCs and System Titles will be built as CIAs.")); } to_import.erase(removed_iter, to_import.end()); const bool is_nand = std::all_of(to_import.begin(), to_import.end(), [](const Core::ContentSpecifier& specifier) { return specifier.type == Core::ContentType::NandTitle; }); const bool enable_legit = std::all_of(to_import.begin(), to_import.end(), [this](const Core::ContentSpecifier& specifier) { return importer->CanBuildLegitCIA(specifier); }); CIABuildDialog dialog(this, /*is_dir*/ true, is_nand, enable_legit, last_batch_build_cia_path); if (dialog.exec() != QDialog::Accepted) { return; } auto [path, type] = dialog.GetResults(); 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(), u64{0}, [](u64 sum, const Core::ContentSpecifier& specifier) { return sum + specifier.maximum_size; }); auto* job = new MultiJob( this, *importer, std::move(to_import), [path = path, type = type](Core::SDMCImporter& importer, const Core::ContentSpecifier& specifier, const Common::ProgressCallback& callback) { return importer.BuildCIA(type, specifier, path.toStdString(), callback, true); }, &Core::SDMCImporter::AbortBuildCIA); RunMultiJob(job, total_count, total_size); }