Merge pull request #3385 from Morph1984/batch-install

frontend: Add support to batch install files to NAND
This commit is contained in:
bunnei 2020-07-12 12:20:56 -04:00 committed by GitHub
commit f1aabc21ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 345 additions and 148 deletions

View file

@ -98,11 +98,13 @@ add_executable(yuzu
game_list_p.h game_list_p.h
game_list_worker.cpp game_list_worker.cpp
game_list_worker.h game_list_worker.h
hotkeys.cpp
hotkeys.h
install_dialog.cpp
install_dialog.h
loading_screen.cpp loading_screen.cpp
loading_screen.h loading_screen.h
loading_screen.ui loading_screen.ui
hotkeys.cpp
hotkeys.h
main.cpp main.cpp
main.h main.h
main.ui 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) * 11) / 10);
vbox_layout = new QVBoxLayout;
hbox_layout = new QHBoxLayout;
description = new QLabel(tr("Please confirm these are the files you wish to install."));
update_description =
new QLabel(tr("Installing an Update or DLC will overwrite the previously installed one."));
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(buttons);
vbox_layout->addWidget(description);
vbox_layout->addWidget(update_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::GetFiles() const {
QStringList files;
for (int i = 0; i < file_list->count(); ++i) {
const QListWidgetItem* item = file_list->item(i);
if (item->checkState() == Qt::Checked) {
files.append(item->data(Qt::UserRole).toString());
}
}
return files;
}
int InstallDialog::GetMinimumWidth() const {
return file_list->width();
}

36
src/yuzu/install_dialog.h Normal file
View file

@ -0,0 +1,36 @@
// 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 GetFiles() const;
bool ShouldOverwriteFiles() const;
int GetMinimumWidth() const;
private:
QListWidget* file_list;
QVBoxLayout* vbox_layout;
QHBoxLayout* hbox_layout;
QLabel* description;
QLabel* update_description;
QDialogButtonBox* buttons;
};

View file

@ -107,6 +107,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "yuzu/game_list.h" #include "yuzu/game_list.h"
#include "yuzu/game_list_p.h" #include "yuzu/game_list_p.h"
#include "yuzu/hotkeys.h" #include "yuzu/hotkeys.h"
#include "yuzu/install_dialog.h"
#include "yuzu/loading_screen.h" #include "yuzu/loading_screen.h"
#include "yuzu/main.h" #include "yuzu/main.h"
#include "yuzu/uisettings.h" #include "yuzu/uisettings.h"
@ -847,6 +848,9 @@ void GMainWindow::ConnectWidgetEvents() {
connect(game_list, &GameList::OpenPerGameGeneralRequested, this, connect(game_list, &GameList::OpenPerGameGeneralRequested, this,
&GMainWindow::OnGameListOpenPerGameProperties); &GMainWindow::OnGameListOpenPerGameProperties);
connect(this, &GMainWindow::UpdateInstallProgress, this,
&GMainWindow::IncrementInstallProgress);
connect(this, &GMainWindow::EmulationStarting, render_window, connect(this, &GMainWindow::EmulationStarting, render_window,
&GRenderWindow::OnEmulationStarting); &GRenderWindow::OnEmulationStarting);
connect(this, &GMainWindow::EmulationStopping, render_window, connect(this, &GMainWindow::EmulationStopping, render_window,
@ -1593,80 +1597,150 @@ void GMainWindow::OnMenuLoadFolder() {
} }
} }
void GMainWindow::IncrementInstallProgress() {
install_progress->setValue(install_progress->value() + 1);
}
void GMainWindow::OnMenuInstallToNAND() { void GMainWindow::OnMenuInstallToNAND() {
const QString file_filter = const QString file_filter =
tr("Installable Switch File (*.nca *.nsp *.xci);;Nintendo Content Archive " 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)"); "Image (*.xci)");
QString filename = QFileDialog::getOpenFileName(this, tr("Install File"),
UISettings::values.roms_path, file_filter);
if (filename.isEmpty()) { QStringList filenames = QFileDialog::getOpenFileNames(
this, tr("Install Files"), UISettings::values.roms_path, file_filter);
if (filenames.isEmpty()) {
return; return;
} }
InstallDialog installDialog(this, filenames);
if (installDialog.exec() == QDialog::Rejected) {
return;
}
const QStringList files = installDialog.GetFiles();
if (files.isEmpty()) {
return;
}
int remaining = filenames.size();
// This would only overflow above 2^43 bytes (8.796 TB)
int total_size = 0;
for (const QString& file : files) {
total_size += static_cast<int>(QFile(file).size() / 0x1000);
}
if (total_size < 0) {
LOG_CRITICAL(Frontend, "Attempting to install too many files, aborting.");
return;
}
QStringList new_files{}; // Newly installed files that do not yet exist in the NAND
QStringList overwritten_files{}; // Files that overwrote those existing in the NAND
QStringList failed_files{}; // Files that failed to install due to errors
ui.action_Install_File_NAND->setEnabled(false);
install_progress = new QProgressDialog(QStringLiteral(""), tr("Cancel"), 0, total_size, this);
install_progress->setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint &
~Qt::WindowMaximizeButtonHint);
install_progress->setAttribute(Qt::WA_DeleteOnClose, true);
install_progress->setFixedWidth(installDialog.GetMinimumWidth() + 40);
install_progress->show();
for (const QString& file : files) {
install_progress->setWindowTitle(tr("%n file(s) remaining", "", remaining));
install_progress->setLabelText(
tr("Installing file \"%1\"...").arg(QFileInfo(file).fileName()));
QFuture<InstallResult> future;
InstallResult result;
if (file.endsWith(QStringLiteral("xci"), Qt::CaseInsensitive) ||
file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
future = QtConcurrent::run([this, &file] { return InstallNSPXCI(file); });
while (!future.isFinished()) {
QCoreApplication::processEvents();
}
result = future.result();
} else {
result = InstallNCA(file);
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
switch (result) {
case InstallResult::Success:
new_files.append(QFileInfo(file).fileName());
break;
case InstallResult::Overwrite:
overwritten_files.append(QFileInfo(file).fileName());
break;
case InstallResult::Failure:
failed_files.append(QFileInfo(file).fileName());
break;
}
--remaining;
}
install_progress->close();
const QString install_results =
(new_files.isEmpty() ? QStringLiteral("")
: tr("%n file(s) were newly installed\n", "", new_files.size())) +
(overwritten_files.isEmpty()
? QStringLiteral("")
: tr("%n file(s) were overwritten\n", "", overwritten_files.size())) +
(failed_files.isEmpty() ? QStringLiteral("")
: tr("%n file(s) failed to install\n", "", failed_files.size()));
QMessageBox::information(this, tr("Install Results"), install_results);
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);
}
InstallResult GMainWindow::InstallNSPXCI(const QString& filename) {
const auto qt_raw_copy = [this](const FileSys::VirtualFile& src, const auto qt_raw_copy = [this](const FileSys::VirtualFile& src,
const FileSys::VirtualFile& dest, std::size_t block_size) { const FileSys::VirtualFile& dest, std::size_t block_size) {
if (src == nullptr || dest == nullptr) if (src == nullptr || dest == nullptr) {
return false; return false;
if (!dest->Resize(src->GetSize())) }
if (!dest->Resize(src->GetSize())) {
return false; return false;
}
std::array<u8, 0x1000> buffer{}; 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);
for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) { for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
if (progress.wasCanceled()) { if (install_progress->wasCanceled()) {
dest->Resize(0); dest->Resize(0);
return false; return false;
} }
const int progress_value = static_cast<int>(i / buffer.size()); emit UpdateInstallProgress();
progress.setValue(progress_value);
const auto read = src->Read(buffer.data(), buffer.size(), i); const auto read = src->Read(buffer.data(), buffer.size(), i);
dest->Write(buffer.data(), read, i); dest->Write(buffer.data(), read, i);
} }
return true; return true;
}; };
const auto success = [this]() {
QMessageBox::information(this, tr("Successfully Installed"),
tr("The file was successfully installed."));
game_list->PopulateAsync(UISettings::values.game_dirs);
FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) +
DIR_SEP + "game_list");
};
const auto failed = [this]() {
QMessageBox::warning(
this, tr("Failed to Install"),
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."));
};
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;
};
if (filename.endsWith(QStringLiteral("xci"), Qt::CaseInsensitive) ||
filename.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
std::shared_ptr<FileSys::NSP> nsp; std::shared_ptr<FileSys::NSP> nsp;
if (filename.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) { if (filename.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
nsp = std::make_shared<FileSys::NSP>( nsp = std::make_shared<FileSys::NSP>(
vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read)); vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
if (nsp->IsExtractedType()) if (nsp->IsExtractedType()) {
failed(); return InstallResult::Failure;
}
} else { } else {
const auto xci = std::make_shared<FileSys::XCI>( const auto xci = std::make_shared<FileSys::XCI>(
vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read)); vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
@ -1674,42 +1748,54 @@ void GMainWindow::OnMenuInstallToNAND() {
} }
if (nsp->GetStatus() != Loader::ResultStatus::Success) { if (nsp->GetStatus() != Loader::ResultStatus::Success) {
failed(); return InstallResult::Failure;
return;
} }
const auto res = Core::System::GetInstance() const auto res =
.GetFileSystemController() Core::System::GetInstance().GetFileSystemController().GetUserNANDContents()->InstallEntry(
.GetUserNANDContents() *nsp, true, qt_raw_copy);
->InstallEntry(*nsp, false, qt_raw_copy);
if (res == FileSys::InstallResult::Success) { if (res == FileSys::InstallResult::Success) {
success(); return InstallResult::Success;
} else if (res == FileSys::InstallResult::ErrorAlreadyExists) {
return InstallResult::Overwrite;
} else { } else {
if (res == FileSys::InstallResult::ErrorAlreadyExists) { return InstallResult::Failure;
if (overwrite()) {
const auto res2 = Core::System::GetInstance()
.GetFileSystemController()
.GetUserNANDContents()
->InstallEntry(*nsp, true, qt_raw_copy);
if (res2 == FileSys::InstallResult::Success) {
success();
} else {
failed();
} }
}
InstallResult GMainWindow::InstallNCA(const QString& filename) {
const auto qt_raw_copy = [this](const FileSys::VirtualFile& src,
const FileSys::VirtualFile& dest, std::size_t block_size) {
if (src == nullptr || dest == nullptr) {
return false;
} }
} else { if (!dest->Resize(src->GetSize())) {
failed(); return false;
} }
std::array<u8, 0x1000> buffer{};
for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
if (install_progress->wasCanceled()) {
dest->Resize(0);
return false;
} }
} else {
const auto nca = std::make_shared<FileSys::NCA>( emit UpdateInstallProgress();
vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
const auto read = src->Read(buffer.data(), buffer.size(), i);
dest->Write(buffer.data(), read, i);
}
return true;
};
const auto nca =
std::make_shared<FileSys::NCA>(vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
const auto id = nca->GetStatus(); const auto id = nca->GetStatus();
// Game updates necessary are missing base RomFS // Game updates necessary are missing base RomFS
if (id != Loader::ResultStatus::Success && if (id != Loader::ResultStatus::Success &&
id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) { id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
failed(); return InstallResult::Failure;
return;
} }
const QStringList tt_options{tr("System Application"), const QStringList tt_options{tr("System Application"),
@ -1732,7 +1818,7 @@ void GMainWindow::OnMenuInstallToNAND() {
if (!ok || index == -1) { if (!ok || index == -1) {
QMessageBox::warning(this, tr("Failed to Install"), QMessageBox::warning(this, tr("Failed to Install"),
tr("The title type you selected for the NCA is invalid.")); tr("The title type you selected for the NCA is invalid."));
return; return InstallResult::Failure;
} }
// If index is equal to or past Game, add the jump in TitleType. // If index is equal to or past Game, add the jump in TitleType.
@ -1746,34 +1832,20 @@ void GMainWindow::OnMenuInstallToNAND() {
res = Core::System::GetInstance() res = Core::System::GetInstance()
.GetFileSystemController() .GetFileSystemController()
.GetUserNANDContents() .GetUserNANDContents()
->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), false, ->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), true, qt_raw_copy);
qt_raw_copy);
} else { } else {
res = Core::System::GetInstance() res = Core::System::GetInstance()
.GetFileSystemController() .GetFileSystemController()
.GetSystemNANDContents() .GetSystemNANDContents()
->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), false, ->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), true, qt_raw_copy);
qt_raw_copy);
} }
if (res == FileSys::InstallResult::Success) { if (res == FileSys::InstallResult::Success) {
success(); return InstallResult::Success;
} else if (res == FileSys::InstallResult::ErrorAlreadyExists) { } else if (res == FileSys::InstallResult::ErrorAlreadyExists) {
if (overwrite()) { return InstallResult::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();
} else { } else {
failed(); return InstallResult::Failure;
}
}
} else {
failed();
}
} }
} }

View file

@ -28,6 +28,7 @@ class MicroProfileDialog;
class ProfilerWidget; class ProfilerWidget;
class QLabel; class QLabel;
class QPushButton; class QPushButton;
class QProgressDialog;
class WaitTreeWidget; class WaitTreeWidget;
enum class GameListOpenTarget; enum class GameListOpenTarget;
class GameListPlaceholder; class GameListPlaceholder;
@ -47,6 +48,12 @@ enum class EmulatedDirectoryTarget {
SDMC, SDMC,
}; };
enum class InstallResult {
Success,
Overwrite,
Failure,
};
enum class ReinitializeKeyBehavior { enum class ReinitializeKeyBehavior {
NoWarning, NoWarning,
Warning, Warning,
@ -102,6 +109,8 @@ signals:
// Signal that tells widgets to update icons to use the current theme // Signal that tells widgets to update icons to use the current theme
void UpdateThemedIcons(); void UpdateThemedIcons();
void UpdateInstallProgress();
void ErrorDisplayFinished(); void ErrorDisplayFinished();
void ProfileSelectorFinishedSelection(std::optional<Common::UUID> uuid); void ProfileSelectorFinishedSelection(std::optional<Common::UUID> uuid);
@ -198,6 +207,7 @@ private slots:
void OnGameListOpenPerGameProperties(const std::string& file); void OnGameListOpenPerGameProperties(const std::string& file);
void OnMenuLoadFile(); void OnMenuLoadFile();
void OnMenuLoadFolder(); void OnMenuLoadFolder();
void IncrementInstallProgress();
void OnMenuInstallToNAND(); void OnMenuInstallToNAND();
void OnMenuRecentFile(); void OnMenuRecentFile();
void OnConfigure(); void OnConfigure();
@ -218,6 +228,8 @@ private slots:
private: private:
std::optional<u64> SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id); std::optional<u64> SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id);
InstallResult InstallNSPXCI(const QString& filename);
InstallResult InstallNCA(const QString& filename);
void UpdateWindowTitle(const std::string& title_name = {}, void UpdateWindowTitle(const std::string& title_name = {},
const std::string& title_version = {}); const std::string& title_version = {});
void UpdateStatusBar(); void UpdateStatusBar();
@ -272,6 +284,9 @@ private:
HotkeyRegistry hotkey_registry; HotkeyRegistry hotkey_registry;
// Install progress dialog
QProgressDialog* install_progress;
protected: protected:
void dropEvent(QDropEvent* event) override; void dropEvent(QDropEvent* event) override;
void dragEnterEvent(QDragEnterEvent* event) override; void dragEnterEvent(QDragEnterEvent* event) override;

View file

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