Refactor batch installing files

Key issues fixed:
- Progress dialog showing up as white/hanging/getting stuck/unresponsive.

Key changes:
- Progress dialog now shows progress as a function of all files instead of per nca within a file.
- Overwrite existing files will overwrite all files in the selection.
This commit is contained in:
Morph 2020-07-01 16:15:57 -04:00
parent 4c269e5ced
commit 7f4d96d873
4 changed files with 242 additions and 200 deletions

View file

@ -22,7 +22,7 @@ InstallDialog::InstallDialog(QWidget* parent, const QStringList& files) : QDialo
item->setCheckState(Qt::Checked); item->setCheckState(Qt::Checked);
} }
file_list->setMinimumWidth((file_list->sizeHintForColumn(0) * 6) / 5); file_list->setMinimumWidth((file_list->sizeHintForColumn(0) * 10) / 9);
vbox_layout = new QVBoxLayout; vbox_layout = new QVBoxLayout;
@ -54,19 +54,23 @@ InstallDialog::InstallDialog(QWidget* parent, const QStringList& files) : QDialo
InstallDialog::~InstallDialog() = default; InstallDialog::~InstallDialog() = default;
QStringList InstallDialog::GetFilenames() const { QStringList InstallDialog::GetFiles() const {
QStringList filenames; QStringList files;
for (int i = 0; i < file_list->count(); ++i) { for (int i = 0; i < file_list->count(); ++i) {
const QListWidgetItem* item = file_list->item(i); const QListWidgetItem* item = file_list->item(i);
if (item->checkState() == Qt::Checked) { if (item->checkState() == Qt::Checked) {
filenames.append(item->data(Qt::UserRole).toString()); files.append(item->data(Qt::UserRole).toString());
} }
} }
return filenames; return files;
} }
bool InstallDialog::ShouldOverwriteFiles() const { bool InstallDialog::ShouldOverwriteFiles() const {
return overwrite_files->isChecked(); return overwrite_files->isChecked();
} }
int InstallDialog::GetMinimumWidth() const {
return file_list->width();
}

View file

@ -20,8 +20,9 @@ public:
explicit InstallDialog(QWidget* parent, const QStringList& files); explicit InstallDialog(QWidget* parent, const QStringList& files);
~InstallDialog() override; ~InstallDialog() override;
QStringList GetFilenames() const; QStringList GetFiles() const;
bool ShouldOverwriteFiles() const; bool ShouldOverwriteFiles() const;
int GetMinimumWidth() const;
private: private:
QListWidget* file_list; QListWidget* file_list;

View file

@ -1599,27 +1599,106 @@ void GMainWindow::OnMenuInstallToNAND() {
tr("Installable Switch File (*.nca *.nsp *.xci);;Nintendo Content Archive " tr("Installable Switch File (*.nca *.nsp *.xci);;Nintendo Content Archive "
"(*.nca);;Nintendo Submission Package (*.nsp);;NX Cartridge " "(*.nca);;Nintendo Submission Package (*.nsp);;NX Cartridge "
"Image (*.xci)"); "Image (*.xci)");
QStringList files = QFileDialog::getOpenFileNames(this, tr("Install Files"),
UISettings::values.roms_path, file_filter);
if (files.isEmpty()) { QStringList filenames = QFileDialog::getOpenFileNames(
this, tr("Install Files"), UISettings::values.roms_path, file_filter);
if (filenames.isEmpty()) {
return; return;
} }
InstallDialog installDialog(this, files); InstallDialog installDialog(this, filenames);
if (installDialog.exec() == QDialog::Rejected) { if (installDialog.exec() == QDialog::Rejected) {
return; return;
} }
const QStringList filenames = installDialog.GetFilenames(); const QStringList files = installDialog.GetFiles();
const bool overwrite_files = installDialog.ShouldOverwriteFiles(); const bool overwrite_files = installDialog.ShouldOverwriteFiles();
int count = 0; int count = 0;
int total_count = filenames.size(); const int total_count = filenames.size();
bool is_progressdialog_created = false;
const auto qt_raw_copy = [this, &count, &total_count, &is_progressdialog_created]( QStringList new_files{}; // Newly installed files that do not yet exist in the NAND
const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest, QStringList overwritten_files{}; // Files that overwrote those existing in the NAND
QStringList existing_files{}; // Files that were not installed as they already exist in the NAND
QStringList failed_files{}; // Files that failed to install due to errors
ui.action_Install_File_NAND->setEnabled(false);
QProgressDialog install_progress(QStringLiteral(""), tr("Cancel"), 0, total_count, this);
install_progress.setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint &
~Qt::WindowMaximizeButtonHint);
install_progress.setAutoClose(false);
install_progress.setFixedWidth(installDialog.GetMinimumWidth());
install_progress.show();
for (const QString& file : files) {
install_progress.setWindowTitle(tr("%n file(s) remaining", "", total_count - count));
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, &overwrite_files, &install_progress] {
return InstallNSPXCI(file, overwrite_files, install_progress);
});
while (!future.isFinished()) {
QCoreApplication::processEvents();
}
result = future.result();
} else {
result = InstallNCA(file, overwrite_files, install_progress);
}
switch (result) {
case InstallResult::Success:
new_files.append(QFileInfo(file).fileName());
break;
case InstallResult::Overwrite:
overwritten_files.append(QFileInfo(file).fileName());
break;
case InstallResult::AlreadyExists:
existing_files.append(QFileInfo(file).fileName());
break;
case InstallResult::Failure:
failed_files.append(QFileInfo(file).fileName());
break;
}
install_progress.setValue(++count);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
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())) +
(existing_files.isEmpty()
? QStringLiteral("")
: tr("%n file(s) already exist in NAND\n", "", existing_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, bool overwrite_files,
QProgressDialog& install_progress) {
const auto qt_raw_copy = [this, &install_progress](const FileSys::VirtualFile& src,
const FileSys::VirtualFile& dest,
std::size_t block_size) { std::size_t block_size) {
if (src == nullptr || dest == nullptr) { if (src == nullptr || dest == nullptr) {
return false; return false;
@ -1629,85 +1708,25 @@ void GMainWindow::OnMenuInstallToNAND() {
} }
std::array<u8, 0x1000> buffer{}; std::array<u8, 0x1000> buffer{};
const int progress_maximum = static_cast<int>(src->GetSize() / buffer.size());
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()) { for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
if (install_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());
install_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, &count, &is_progressdialog_created]() {
if (is_progressdialog_created) {
install_progress->close();
}
QMessageBox::information(this, tr("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, &is_progressdialog_created](const QString& file) {
if (is_progressdialog_created) {
install_progress->close();
}
QMessageBox::warning(
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](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;
};
for (const QString& filename : filenames) {
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(filename); return InstallResult::Failure;
break;
} }
} else { } else {
const auto xci = std::make_shared<FileSys::XCI>( const auto xci = std::make_shared<FileSys::XCI>(
@ -1716,43 +1735,65 @@ void GMainWindow::OnMenuInstallToNAND() {
} }
if (nsp->GetStatus() != Loader::ResultStatus::Success) { if (nsp->GetStatus() != Loader::ResultStatus::Success) {
failed(filename); return InstallResult::Failure;
break;
} }
const auto res = Core::System::GetInstance() const auto res =
.GetFileSystemController() Core::System::GetInstance().GetFileSystemController().GetUserNANDContents()->InstallEntry(
.GetUserNANDContents() *nsp, false, qt_raw_copy);
->InstallEntry(*nsp, false, qt_raw_copy);
if (res == FileSys::InstallResult::Success) { if (res == FileSys::InstallResult::Success) {
++count; return InstallResult::Success;
} else if (res == FileSys::InstallResult::ErrorAlreadyExists) { } else if (res == FileSys::InstallResult::ErrorAlreadyExists) {
if (overwrite_files && overwrite(filename)) { if (overwrite_files) {
const auto res2 = Core::System::GetInstance() const auto res2 = Core::System::GetInstance()
.GetFileSystemController() .GetFileSystemController()
.GetUserNANDContents() .GetUserNANDContents()
->InstallEntry(*nsp, true, qt_raw_copy); ->InstallEntry(*nsp, true, qt_raw_copy);
if (res2 != FileSys::InstallResult::Success) { if (res2 != FileSys::InstallResult::Success) {
failed(filename); return InstallResult::Failure;
break;
} }
++count; return InstallResult::Overwrite;
} else { } else {
--total_count; return InstallResult::AlreadyExists;
} }
} else { } else {
failed(filename); return InstallResult::Failure;
break;
} }
} else { }
const auto nca = std::make_shared<FileSys::NCA>(
vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read)); InstallResult GMainWindow::InstallNCA(const QString& filename, bool overwrite_files,
QProgressDialog& install_progress) {
const auto qt_raw_copy = [this, &install_progress](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())) {
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;
}
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(filename); return InstallResult::Failure;
break;
} }
const QStringList tt_options{tr("System Application"), const QStringList tt_options{tr("System Application"),
@ -1775,7 +1816,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."));
break; 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.
@ -1789,44 +1830,32 @@ 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), false, 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), false, qt_raw_copy);
qt_raw_copy);
} }
if (res == FileSys::InstallResult::Success) { if (res == FileSys::InstallResult::Success) {
++count; return InstallResult::Success;
} else if (res == FileSys::InstallResult::ErrorAlreadyExists) { } else if (res == FileSys::InstallResult::ErrorAlreadyExists) {
if (overwrite_files && overwrite(filename)) { if (overwrite_files) {
const auto res2 = const auto res2 =
Core::System::GetInstance() Core::System::GetInstance()
.GetFileSystemController() .GetFileSystemController()
.GetUserNANDContents() .GetUserNANDContents()
->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), true, ->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), true, qt_raw_copy);
qt_raw_copy);
if (res2 != FileSys::InstallResult::Success) { if (res2 != FileSys::InstallResult::Success) {
failed(filename); return InstallResult::Failure;
break;
} }
++count; return InstallResult::Overwrite;
} else { } else {
--total_count; return InstallResult::AlreadyExists;
} }
} else { } else {
failed(filename); return InstallResult::Failure;
break;
}
}
// Return success only on the last file
if (filename == filenames.last()) {
success();
}
} }
} }

View file

@ -48,6 +48,13 @@ enum class EmulatedDirectoryTarget {
SDMC, SDMC,
}; };
enum class InstallResult {
Success,
Overwrite,
AlreadyExists,
Failure,
};
enum class ReinitializeKeyBehavior { enum class ReinitializeKeyBehavior {
NoWarning, NoWarning,
Warning, Warning,
@ -219,6 +226,10 @@ 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, bool overwrite_files,
QProgressDialog& install_progress);
InstallResult InstallNCA(const QString& filename, bool overwrite_files,
QProgressDialog& install_progress);
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();
@ -273,9 +284,6 @@ private:
HotkeyRegistry hotkey_registry; HotkeyRegistry hotkey_registry;
// Install to NAND 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;