Add support for batch install to NAND

This adds support to batch install files to NAND
This commit is contained in:
Morph 2020-04-16 23:27:38 -04:00
parent 0974533c96
commit 4c269e5ced
6 changed files with 292 additions and 126 deletions

View File

@ -98,11 +98,13 @@ add_executable(yuzu
game_list_p.h
game_list_worker.cpp
game_list_worker.h
hotkeys.cpp
hotkeys.h
install_dialog.cpp
install_dialog.h
loading_screen.cpp
loading_screen.h
loading_screen.ui
hotkeys.cpp
hotkeys.h
main.cpp
main.h
main.ui

View File

@ -0,0 +1,72 @@
// Copyright 2020 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <QCheckBox>
#include <QDialogButtonBox>
#include <QFileInfo>
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QVBoxLayout>
#include "yuzu/install_dialog.h"
#include "yuzu/uisettings.h"
InstallDialog::InstallDialog(QWidget* parent, const QStringList& files) : QDialog(parent) {
file_list = new QListWidget(this);
for (const QString& file : files) {
QListWidgetItem* item = new QListWidgetItem(QFileInfo(file).fileName(), file_list);
item->setData(Qt::UserRole, file);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(Qt::Checked);
}
file_list->setMinimumWidth((file_list->sizeHintForColumn(0) * 6) / 5);
vbox_layout = new QVBoxLayout;
hbox_layout = new QHBoxLayout;
description = new QLabel(tr("Please confirm these are the files you wish to install."));
overwrite_files = new QCheckBox(tr("Overwrite Existing Files"));
overwrite_files->setCheckState(Qt::Unchecked);
buttons = new QDialogButtonBox;
buttons->addButton(QDialogButtonBox::Cancel);
buttons->addButton(tr("Install"), QDialogButtonBox::AcceptRole);
connect(buttons, &QDialogButtonBox::accepted, this, &InstallDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &InstallDialog::reject);
hbox_layout->addWidget(overwrite_files);
hbox_layout->addWidget(buttons);
vbox_layout->addWidget(description);
vbox_layout->addWidget(file_list);
vbox_layout->addLayout(hbox_layout);
setLayout(vbox_layout);
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setWindowTitle(tr("Install Files to NAND"));
}
InstallDialog::~InstallDialog() = default;
QStringList InstallDialog::GetFilenames() const {
QStringList filenames;
for (int i = 0; i < file_list->count(); ++i) {
const QListWidgetItem* item = file_list->item(i);
if (item->checkState() == Qt::Checked) {
filenames.append(item->data(Qt::UserRole).toString());
}
}
return filenames;
}
bool InstallDialog::ShouldOverwriteFiles() const {
return overwrite_files->isChecked();
}

35
src/yuzu/install_dialog.h Normal file
View File

@ -0,0 +1,35 @@
// Copyright 2020 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <QDialog>
class QCheckBox;
class QDialogButtonBox;
class QHBoxLayout;
class QLabel;
class QListWidget;
class QVBoxLayout;
class InstallDialog : public QDialog {
Q_OBJECT
public:
explicit InstallDialog(QWidget* parent, const QStringList& files);
~InstallDialog() override;
QStringList GetFilenames() const;
bool ShouldOverwriteFiles() const;
private:
QListWidget* file_list;
QVBoxLayout* vbox_layout;
QHBoxLayout* hbox_layout;
QLabel* description;
QCheckBox* overwrite_files;
QDialogButtonBox* buttons;
};

View File

@ -107,6 +107,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "yuzu/game_list.h"
#include "yuzu/game_list_p.h"
#include "yuzu/hotkeys.h"
#include "yuzu/install_dialog.h"
#include "yuzu/loading_screen.h"
#include "yuzu/main.h"
#include "yuzu/uisettings.h"
@ -1596,38 +1597,67 @@ void GMainWindow::OnMenuLoadFolder() {
void GMainWindow::OnMenuInstallToNAND() {
const QString file_filter =
tr("Installable Switch File (*.nca *.nsp *.xci);;Nintendo Content Archive "
"(*.nca);;Nintendo Submissions Package (*.nsp);;NX Cartridge "
"(*.nca);;Nintendo Submission Package (*.nsp);;NX Cartridge "
"Image (*.xci)");
QString filename = QFileDialog::getOpenFileName(this, tr("Install File"),
UISettings::values.roms_path, file_filter);
QStringList files = QFileDialog::getOpenFileNames(this, tr("Install Files"),
UISettings::values.roms_path, file_filter);
if (filename.isEmpty()) {
if (files.isEmpty()) {
return;
}
const auto qt_raw_copy = [this](const FileSys::VirtualFile& src,
const FileSys::VirtualFile& dest, std::size_t block_size) {
if (src == nullptr || dest == nullptr)
InstallDialog installDialog(this, files);
if (installDialog.exec() == QDialog::Rejected) {
return;
}
const QStringList filenames = installDialog.GetFilenames();
const bool overwrite_files = installDialog.ShouldOverwriteFiles();
int count = 0;
int total_count = filenames.size();
bool is_progressdialog_created = false;
const auto qt_raw_copy = [this, &count, &total_count, &is_progressdialog_created](
const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
std::size_t block_size) {
if (src == nullptr || dest == nullptr) {
return false;
if (!dest->Resize(src->GetSize()))
}
if (!dest->Resize(src->GetSize())) {
return false;
}
std::array<u8, 0x1000> buffer{};
const int progress_maximum = static_cast<int>(src->GetSize() / buffer.size());
QProgressDialog progress(
tr("Installing file \"%1\"...").arg(QString::fromStdString(src->GetName())),
tr("Cancel"), 0, progress_maximum, this);
progress.setWindowModality(Qt::WindowModal);
if (!is_progressdialog_created) {
ui.action_Install_File_NAND->setEnabled(false);
install_progress = new QProgressDialog(
tr("Installing file \"%1\"...").arg(QString::fromStdString(src->GetName())),
tr("Cancel"), 0, progress_maximum, this);
install_progress->setWindowTitle(
tr("%n file(s) remaining", "", total_count - count - 1));
install_progress->setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint &
~Qt::WindowMaximizeButtonHint);
install_progress->setAutoClose(false);
is_progressdialog_created = true;
} else {
install_progress->setWindowTitle(
tr("%n file(s) remaining", "", total_count - count - 1));
install_progress->setLabelText(
tr("Installing file \"%1\"...").arg(QString::fromStdString(src->GetName())));
install_progress->setMaximum(progress_maximum);
}
for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
if (progress.wasCanceled()) {
if (install_progress->wasCanceled()) {
dest->Resize(0);
return false;
}
const int progress_value = static_cast<int>(i / buffer.size());
progress.setValue(progress_value);
install_progress->setValue(progress_value);
const auto read = src->Read(buffer.data(), buffer.size(), i);
dest->Write(buffer.data(), read, i);
@ -1636,143 +1666,166 @@ void GMainWindow::OnMenuInstallToNAND() {
return true;
};
const auto success = [this]() {
const auto success = [this, &count, &is_progressdialog_created]() {
if (is_progressdialog_created) {
install_progress->close();
}
QMessageBox::information(this, tr("Successfully Installed"),
tr("The file was successfully installed."));
tr("%n file(s) successfully installed", "", count));
game_list->PopulateAsync(UISettings::values.game_dirs);
FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) +
DIR_SEP + "game_list");
ui.action_Install_File_NAND->setEnabled(true);
};
const auto failed = [this]() {
const auto failed = [this, &is_progressdialog_created](const QString& file) {
if (is_progressdialog_created) {
install_progress->close();
}
QMessageBox::warning(
this, tr("Failed to Install"),
this, tr("Failed to Install %1").arg(QFileInfo(file).fileName()),
tr("There was an error while attempting to install the provided file. It "
"could have an incorrect format or be missing metadata. Please "
"double-check your file and try again."));
game_list->PopulateAsync(UISettings::values.game_dirs);
ui.action_Install_File_NAND->setEnabled(true);
};
const auto overwrite = [this]() {
return QMessageBox::question(this, tr("Failed to Install"),
tr("The file you are attempting to install already exists "
"in the cache. Would you like to overwrite it?")) ==
QMessageBox::Yes;
const auto overwrite = [this](const QString& file) {
return QMessageBox::question(
this, tr("Failed to Install %1").arg(QFileInfo(file).fileName()),
tr("The file you are attempting to install already exists "
"in the cache. Would you like to overwrite it?")) == QMessageBox::Yes;
};
if (filename.endsWith(QStringLiteral("xci"), Qt::CaseInsensitive) ||
filename.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
std::shared_ptr<FileSys::NSP> nsp;
if (filename.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
nsp = std::make_shared<FileSys::NSP>(
vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
if (nsp->IsExtractedType())
failed();
} else {
const auto xci = std::make_shared<FileSys::XCI>(
vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
nsp = xci->GetSecurePartitionNSP();
}
for (const QString& filename : filenames) {
if (filename.endsWith(QStringLiteral("xci"), Qt::CaseInsensitive) ||
filename.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
std::shared_ptr<FileSys::NSP> nsp;
if (filename.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
nsp = std::make_shared<FileSys::NSP>(
vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
if (nsp->IsExtractedType()) {
failed(filename);
break;
}
} else {
const auto xci = std::make_shared<FileSys::XCI>(
vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
nsp = xci->GetSecurePartitionNSP();
}
if (nsp->GetStatus() != Loader::ResultStatus::Success) {
failed();
return;
}
const auto res = Core::System::GetInstance()
.GetFileSystemController()
.GetUserNANDContents()
->InstallEntry(*nsp, false, qt_raw_copy);
if (res == FileSys::InstallResult::Success) {
success();
} else {
if (res == FileSys::InstallResult::ErrorAlreadyExists) {
if (overwrite()) {
if (nsp->GetStatus() != Loader::ResultStatus::Success) {
failed(filename);
break;
}
const auto res = Core::System::GetInstance()
.GetFileSystemController()
.GetUserNANDContents()
->InstallEntry(*nsp, false, qt_raw_copy);
if (res == FileSys::InstallResult::Success) {
++count;
} else if (res == FileSys::InstallResult::ErrorAlreadyExists) {
if (overwrite_files && overwrite(filename)) {
const auto res2 = Core::System::GetInstance()
.GetFileSystemController()
.GetUserNANDContents()
->InstallEntry(*nsp, true, qt_raw_copy);
if (res2 == FileSys::InstallResult::Success) {
success();
} else {
failed();
if (res2 != FileSys::InstallResult::Success) {
failed(filename);
break;
}
++count;
} else {
--total_count;
}
} else {
failed();
failed(filename);
break;
}
}
} else {
const auto nca = std::make_shared<FileSys::NCA>(
vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
const auto id = nca->GetStatus();
// Game updates necessary are missing base RomFS
if (id != Loader::ResultStatus::Success &&
id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
failed();
return;
}
const QStringList tt_options{tr("System Application"),
tr("System Archive"),
tr("System Application Update"),
tr("Firmware Package (Type A)"),
tr("Firmware Package (Type B)"),
tr("Game"),
tr("Game Update"),
tr("Game DLC"),
tr("Delta Title")};
bool ok;
const auto item = QInputDialog::getItem(
this, tr("Select NCA Install Type..."),
tr("Please select the type of title you would like to install this NCA as:\n(In "
"most instances, the default 'Game' is fine.)"),
tt_options, 5, false, &ok);
auto index = tt_options.indexOf(item);
if (!ok || index == -1) {
QMessageBox::warning(this, tr("Failed to Install"),
tr("The title type you selected for the NCA is invalid."));
return;
}
// If index is equal to or past Game, add the jump in TitleType.
if (index >= 5) {
index += static_cast<size_t>(FileSys::TitleType::Application) -
static_cast<size_t>(FileSys::TitleType::FirmwarePackageB);
}
FileSys::InstallResult res;
if (index >= static_cast<s32>(FileSys::TitleType::Application)) {
res = Core::System::GetInstance()
.GetFileSystemController()
.GetUserNANDContents()
->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), false,
qt_raw_copy);
} else {
res = Core::System::GetInstance()
.GetFileSystemController()
.GetSystemNANDContents()
->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), false,
qt_raw_copy);
}
const auto nca = std::make_shared<FileSys::NCA>(
vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
const auto id = nca->GetStatus();
if (res == FileSys::InstallResult::Success) {
success();
} else if (res == FileSys::InstallResult::ErrorAlreadyExists) {
if (overwrite()) {
const auto res2 = Core::System::GetInstance()
.GetFileSystemController()
.GetUserNANDContents()
->InstallEntry(*nca, static_cast<FileSys::TitleType>(index),
true, qt_raw_copy);
if (res2 == FileSys::InstallResult::Success) {
success();
// Game updates necessary are missing base RomFS
if (id != Loader::ResultStatus::Success &&
id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
failed(filename);
break;
}
const QStringList tt_options{tr("System Application"),
tr("System Archive"),
tr("System Application Update"),
tr("Firmware Package (Type A)"),
tr("Firmware Package (Type B)"),
tr("Game"),
tr("Game Update"),
tr("Game DLC"),
tr("Delta Title")};
bool ok;
const auto item = QInputDialog::getItem(
this, tr("Select NCA Install Type..."),
tr("Please select the type of title you would like to install this NCA as:\n(In "
"most instances, the default 'Game' is fine.)"),
tt_options, 5, false, &ok);
auto index = tt_options.indexOf(item);
if (!ok || index == -1) {
QMessageBox::warning(this, tr("Failed to Install"),
tr("The title type you selected for the NCA is invalid."));
break;
}
// If index is equal to or past Game, add the jump in TitleType.
if (index >= 5) {
index += static_cast<size_t>(FileSys::TitleType::Application) -
static_cast<size_t>(FileSys::TitleType::FirmwarePackageB);
}
FileSys::InstallResult res;
if (index >= static_cast<s32>(FileSys::TitleType::Application)) {
res = Core::System::GetInstance()
.GetFileSystemController()
.GetUserNANDContents()
->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), false,
qt_raw_copy);
} else {
res = Core::System::GetInstance()
.GetFileSystemController()
.GetSystemNANDContents()
->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), false,
qt_raw_copy);
}
if (res == FileSys::InstallResult::Success) {
++count;
} else if (res == FileSys::InstallResult::ErrorAlreadyExists) {
if (overwrite_files && overwrite(filename)) {
const auto res2 =
Core::System::GetInstance()
.GetFileSystemController()
.GetUserNANDContents()
->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), true,
qt_raw_copy);
if (res2 != FileSys::InstallResult::Success) {
failed(filename);
break;
}
++count;
} else {
failed();
--total_count;
}
} else {
failed(filename);
break;
}
} else {
failed();
}
// Return success only on the last file
if (filename == filenames.last()) {
success();
}
}
}

View File

@ -28,6 +28,7 @@ class MicroProfileDialog;
class ProfilerWidget;
class QLabel;
class QPushButton;
class QProgressDialog;
class WaitTreeWidget;
enum class GameListOpenTarget;
class GameListPlaceholder;
@ -272,6 +273,9 @@ private:
HotkeyRegistry hotkey_registry;
// Install to NAND progress dialog
QProgressDialog* install_progress;
protected:
void dropEvent(QDropEvent* event) override;
void dragEnterEvent(QDragEnterEvent* event) override;

View File

@ -130,7 +130,7 @@
<bool>true</bool>
</property>
<property name="text">
<string>Install File to NAND...</string>
<string>Install Files to NAND...</string>
</property>
</action>
<action name="action_Load_File">