lots of important fixes

- DPFS container is fixed
- SD Savegame is fixed
- added slot0x25KeyX load
- added regex for titile ID
- added aes_keys.txt import
- sd savegame listing is fixed (uninitialized won't be listed any more)
- error handling is improved (removed asserts and replaced with return values)
- UI is now functional
- config is now checked in main
This commit is contained in:
zhupengfei
2019-08-30 15:33:47 +08:00
parent 8acfe9f304
commit 67e6b05e87
22 changed files with 490 additions and 78 deletions
+2
View File
@@ -10,6 +10,8 @@ add_executable(threeSD
import_dialog.cpp
import_dialog.h
import_dialog.ui
import_job.cpp
import_job.h
main.cpp
main.h
main.ui
+148 -18
View File
@@ -4,12 +4,16 @@
#include <cmath>
#include <QCheckBox>
#include <QFutureWatcher>
#include <QMessageBox>
#include <QProgressDialog>
#include <QPushButton>
#include <QStorageInfo>
#include <QtConcurrent/QtConcurrentRun>
#include "common/logging/log.h"
#include "common/scope_exit.h"
#include "frontend/import_dialog.h"
#include "frontend/import_job.h"
#include "ui_import_dialog.h"
QString ReadableByteSize(qulonglong size) {
@@ -25,10 +29,30 @@ QString ReadableByteSize(qulonglong size) {
.arg(QObject::tr(units[digit_groups], "ImportDialog"));
}
static const std::map<Core::ContentType, const char*> ContentTypeMap{
{Core::ContentType::Application, QT_TR_NOOP("Application")},
{Core::ContentType::Update, QT_TR_NOOP("Update")},
{Core::ContentType::DLC, QT_TR_NOOP("DLC")},
{Core::ContentType::Savegame, QT_TR_NOOP("Save Data")},
{Core::ContentType::Extdata, QT_TR_NOOP("Extra Data")},
{Core::ContentType::Sysdata, QT_TR_NOOP("System Data")},
};
QString GetContentName(const Core::ContentSpecifier& specifier) {
return specifier.name.empty()
? QStringLiteral("0x%1 (%2)")
.arg(specifier.id, 16, 16, QLatin1Char('0'))
.arg(QObject::tr(ContentTypeMap.at(specifier.type), "ImportDialog"))
: QString::fromStdString(specifier.name);
}
ImportDialog::ImportDialog(QWidget* parent, const Core::Config& config)
: QDialog(parent), ui(std::make_unique<Ui::ImportDialog>()), user_path(config.user_path),
importer(config) {
qRegisterMetaType<u64>("u64");
qRegisterMetaType<Core::ContentSpecifier>();
ui->setupUi(this);
if (!importer.IsGood()) {
QMessageBox::critical(
@@ -37,29 +61,57 @@ ImportDialog::ImportDialog(QWidget* parent, const Core::Config& config)
reject();
}
PopulateContent();
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();
}
});
RelistContent();
UpdateSizeDisplay();
// Set up column widths
ui->main->setColumnWidth(0, width() / 8);
ui->main->setColumnWidth(1, width() / 2);
ui->main->setColumnWidth(2, width() / 6);
ui->main->setColumnWidth(3, width() / 10);
}
ImportDialog::~ImportDialog() = default;
void ImportDialog::PopulateContent() {
void ImportDialog::RelistContent() {
auto* dialog = new QProgressDialog(tr("Loading Contents..."), tr("Cancel"), 0, 0, this);
dialog->setWindowModality(Qt::WindowModal);
dialog->setCancelButton(nullptr);
dialog->setMinimumDuration(0);
dialog->setValue(0);
using FutureWatcher = QFutureWatcher<std::vector<Core::ContentSpecifier>>;
auto* future_watcher = new FutureWatcher(this);
connect(future_watcher, &FutureWatcher::finished, this, [this, dialog] {
dialog->hide();
RepopulateContent();
});
auto future =
QtConcurrent::run([& importer = this->importer] { return importer.ListContent(); });
future_watcher->setFuture(future);
}
void ImportDialog::RepopulateContent() {
total_size = 0;
contents = importer.ListContent();
ui->main->clear();
ui->main->setSortingEnabled(false);
const std::map<Core::ContentType, QString> content_type_map{
{Core::ContentType::Application, QStringLiteral("Application")},
{Core::ContentType::Update, QStringLiteral("Update")},
{Core::ContentType::DLC, QStringLiteral("DLC (Add-on Content)")},
{Core::ContentType::Savegame, QStringLiteral("Save Data")},
{Core::ContentType::Extdata, QStringLiteral("Extra Data")},
{Core::ContentType::Sysdata, QStringLiteral("System Data")},
};
for (const auto& [type, name] : content_type_map) {
for (const auto& [type, name] : ContentTypeMap) {
auto* checkBox = new QCheckBox();
checkBox->setText(name);
checkBox->setText(tr(name));
checkBox->setStyleSheet(QStringLiteral("margin-left:7px"));
checkBox->setTristate(true);
checkBox->setProperty("previousState", static_cast<int>(Qt::Unchecked));
@@ -96,15 +148,16 @@ void ImportDialog::PopulateContent() {
ui->main->setItemWidget(item, 0, checkBox);
}
for (const auto& content : contents) {
for (std::size_t i = 0; i < contents.size(); ++i) {
const auto& content = contents[i];
auto* checkBox = new QCheckBox();
checkBox->setStyleSheet(QStringLiteral("margin-left:7px"));
// HACK: The checkbox is used to record ID. Is there a better way?
checkBox->setProperty("id", i);
auto* item = new QTreeWidgetItem{
{QString{},
content.name.empty() ? QStringLiteral("0x%1").arg(content.id, 16, 16, QLatin1Char('0'))
: QString::fromStdString(content.name),
ReadableByteSize(content.maximum_size),
{QString{}, GetContentName(content), ReadableByteSize(content.maximum_size),
content.already_exists ? QStringLiteral("Yes") : QStringLiteral("No")}};
ui->main->invisibleRootItem()->child(static_cast<int>(content.type))->addChild(item);
@@ -123,6 +176,10 @@ void ImportDialog::PopulateContent() {
UpdateItemCheckState(item->parent());
}
});
if (!content.already_exists) {
checkBox->setChecked(true);
}
}
ui->main->setSortingEnabled(true);
@@ -172,3 +229,76 @@ void ImportDialog::UpdateItemCheckState(QTreeWidgetItem* item) {
}
program_trigger = false;
}
std::vector<Core::ContentSpecifier> ImportDialog::GetSelectedContentList() {
std::vector<Core::ContentSpecifier> 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) {
const auto* checkBox = static_cast<QCheckBox*>(ui->main->itemWidget(item->child(j), 0));
if (checkBox->isChecked()) {
to_import.emplace_back(contents[checkBox->property("id").toInt()]);
}
}
}
return to_import;
}
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;
}
const 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<int>::max() - 1) / std::numeric_limits<int>::max();
auto* dialog = new QProgressDialog(tr("Initializing..."), tr("Cancel"), 0,
static_cast<int>(total_size / multiplier), this);
dialog->setWindowModality(Qt::WindowModal);
dialog->setValue(0);
auto* job = new ImportJob(this, importer, std::move(to_import));
connect(job, &ImportJob::ProgressUpdated, this,
[dialog, multiplier, total_count](u64 size_imported, u64 count,
Core::ContentSpecifier next_content) {
dialog->setValue(static_cast<int>(size_imported / multiplier));
dialog->setLabelText(tr("Importing %1 (%2/%3)...")
.arg(GetContentName(next_content))
.arg(count)
.arg(total_count));
});
connect(job, &ImportJob::ErrorOccured, this,
[this, dialog](Core::ContentSpecifier current_content) {
QMessageBox::critical(
this, tr("Error"),
tr("Failed to import content %1!").arg(GetContentName(current_content)));
dialog->hide();
});
connect(job, &ImportJob::Completed, this, [this, dialog] {
dialog->setValue(dialog->maximum());
RepopulateContent();
});
connect(dialog, &QProgressDialog::canceled, this, [this, dialog, 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, &ImportJob::Completed, cancel_dialog, &QProgressDialog::hide);
job->Cancel();
});
job->start();
}
+5 -2
View File
@@ -17,16 +17,19 @@ class ImportDialog;
}
class ImportDialog : public QDialog {
Q_OBJECT;
Q_OBJECT
public:
explicit ImportDialog(QWidget* parent, const Core::Config& config);
~ImportDialog() override;
private:
void PopulateContent();
void RelistContent();
void RepopulateContent();
void UpdateSizeDisplay();
void UpdateItemCheckState(QTreeWidgetItem* item);
std::vector<Core::ContentSpecifier> GetSelectedContentList();
void StartImporting();
std::unique_ptr<Ui::ImportDialog> ui;
+2 -2
View File
@@ -33,7 +33,7 @@
</column>
<column>
<property name="text">
<string>Imported</string>
<string>Exists</string>
</property>
</column>
</widget>
@@ -66,7 +66,7 @@
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Ok|QDialogButtonBox::Cancel</set>
<set>QDialogButtonBox::Ok|QDialogButtonBox::Cancel|QDialogButtonBox::Reset</set>
</property>
</widget>
</item>
+35
View File
@@ -0,0 +1,35 @@
// Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "frontend/import_job.h"
#include "common/assert.h"
ImportJob::ImportJob(QObject* parent, Core::SDMCImporter& importer_,
std::vector<Core::ContentSpecifier> contents_)
: QThread(parent), importer(importer_), contents(std::move(contents_)) {}
ImportJob::~ImportJob() = default;
void ImportJob::run() {
u64 size_imported = 0, count = 0;
for (const auto& content : contents) {
emit ProgressUpdated(size_imported, count + 1, content);
if (!importer.ImportContent(content)) {
emit ErrorOccured(content);
return;
}
count++;
size_imported += content.maximum_size;
if (cancelled) {
break;
}
}
emit Completed();
}
void ImportJob::Cancel() {
cancelled.store(true);
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2019 threeSD Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <atomic>
#include <QThread>
#include "core/importer.h"
class ImportJob : public QThread {
Q_OBJECT
public:
explicit ImportJob(QObject* parent, Core::SDMCImporter& importer,
std::vector<Core::ContentSpecifier> contents);
~ImportJob() override;
void run() override;
void Cancel();
signals:
void ProgressUpdated(u64 size_imported, u64 count, Core::ContentSpecifier next_content);
void Completed();
void ErrorOccured(Core::ContentSpecifier current_content);
private:
std::atomic_bool cancelled{false};
Core::SDMCImporter& importer;
std::vector<Core::ContentSpecifier> contents;
};
Q_DECLARE_METATYPE(Core::ContentSpecifier)
+19 -2
View File
@@ -4,6 +4,7 @@
#include <string>
#include <QApplication>
#include <QMessageBox>
#include <QStorageInfo>
#include <qdevicewatcher.h>
#include "common/file_util.h"
@@ -16,6 +17,11 @@
#include "common/common_paths.h"
#endif
bool IsConfigGood(const Core::Config& config) {
return !config.sdmc_path.empty() && !config.user_path.empty() &&
!config.movable_sed_path.empty() && !config.bootrom_path.empty();
}
MainDialog::MainDialog(QWidget* parent) : QDialog(parent), ui(std::make_unique<Ui::MainDialog>()) {
ui->setupUi(this);
@@ -36,8 +42,7 @@ MainDialog::MainDialog(QWidget* parent) : QDialog(parent), ui(std::make_unique<U
if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Reset)) {
LoadPresetConfig();
} else if (button == ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)) {
ImportDialog dialog(this, GetCurrentConfig());
dialog.exec();
LaunchImportDialog();
}
});
@@ -145,6 +150,18 @@ Core::Config MainDialog::GetCurrentConfig() {
}
}
void MainDialog::LaunchImportDialog() {
const auto& config = GetCurrentConfig();
if (!IsConfigGood(config)) {
QMessageBox::critical(this, tr("Incomplete Config"),
tr("Your config is missing some of the required fields."));
return;
}
ImportDialog dialog(this, config);
dialog.exec();
}
int main(int argc, char* argv[]) {
// Init settings params
QCoreApplication::setOrganizationName(QStringLiteral("zhaowenlan1779"));
+2 -1
View File
@@ -13,7 +13,7 @@ class MainDialog;
}
class MainDialog : public QDialog {
Q_OBJECT;
Q_OBJECT
public:
explicit MainDialog(QWidget* parent = nullptr);
@@ -24,6 +24,7 @@ private:
void ShowAdvanced();
void HideAdvanced();
Core::Config GetCurrentConfig();
void LaunchImportDialog();
std::vector<Core::Config> preset_config_list;
std::unique_ptr<Ui::MainDialog> ui;