core, frontend: Group contents by title / game instead of category

I may look to introduce a option to select in the future
This commit is contained in:
zhupengfei
2019-09-27 23:33:39 +08:00
parent 895cbb272c
commit 479dd327df
4 changed files with 67 additions and 19 deletions
+8 -3
View File
@@ -214,7 +214,7 @@ std::vector<ContentSpecifier> SDMCImporter::ListContent() const {
// Regex for half Title IDs
static const std::regex title_regex{"[0-9a-f]{8}"};
std::string SDMCImporter::LoadTitleName(const std::string& path) const {
std::pair<std::string, u64> SDMCImporter::LoadTitleData(const std::string& path) const {
// Remove trailing '/'
const auto sdmc_path = config.sdmc_path.substr(0, config.sdmc_path.size() - 1);
@@ -271,7 +271,11 @@ std::string SDMCImporter::LoadTitleName(const std::string& path) const {
SMDH smdh;
std::memcpy(&smdh, smdh_buffer.data(), smdh_buffer.size());
return Common::UTF16BufferToUTF8(smdh.GetShortTitle(SMDH::TitleLanguage::English));
u64 extdata_id{};
ncch.ReadExtdataId(extdata_id);
return {Common::UTF16BufferToUTF8(smdh.GetShortTitle(SMDH::TitleLanguage::English)),
extdata_id};
}
void SDMCImporter::ListTitle(std::vector<ContentSpecifier>& out) const {
@@ -299,10 +303,11 @@ void SDMCImporter::ListTitle(std::vector<ContentSpecifier>& out) const {
if (FileUtil::Exists(directory + virtual_name + "/content/")) {
const auto content_path =
fmt::format("/title/{:08x}/{}/content/", high_id, virtual_name);
const auto& [name, extdata_id] = LoadTitleData(content_path);
out.push_back(
{type, id, FileUtil::Exists(citra_path + "content/"),
FileUtil::GetDirectoryTreeSize(directory + virtual_name + "/content/"),
LoadTitleName(content_path)});
name, extdata_id});
}
if (type != ContentType::Application) {
+3 -2
View File
@@ -35,6 +35,7 @@ struct ContentSpecifier {
bool already_exists; ///< Tells whether a file already exists in target path.
u64 maximum_size; ///< The maximum size of the content. May be slightly bigger than real size.
std::string name; ///< Optional. The content's preferred display name.
u64 extdata_id; ///< Extdata ID for Applications.
};
/**
@@ -119,11 +120,11 @@ private:
void DeleteSysdata(u64 id) const;
/**
* Loads the English short title name of a title.
* Loads the English short title name and extdata id of a title.
* @param path Path of the 'content' folder relative to the SDMC root folder.
* Required to end with '/'.
*/
std::string LoadTitleName(const std::string& path) const;
std::pair<std::string, u64> LoadTitleData(const std::string& path) const;
bool is_good{};
Config config;
+55 -13
View File
@@ -4,6 +4,7 @@
#include <array>
#include <cmath>
#include <unordered_map>
#include <QCheckBox>
#include <QFutureWatcher>
#include <QMessageBox>
@@ -41,9 +42,7 @@ static const std::map<Core::ContentType, const char*> ContentTypeMap{
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"))
? QStringLiteral("0x%1").arg(specifier.id, 16, 16, QLatin1Char('0'))
: QString::fromStdString(specifier.name);
}
@@ -110,9 +109,20 @@ void ImportDialog::RepopulateContent() {
ui->main->clear();
ui->main->setSortingEnabled(false);
for (const auto& [type, name] : ContentTypeMap) {
std::map<u64, QString> title_name_map; // title ID -> title name
std::unordered_map<u64, u64> extdata_id_map; // extdata ID -> title ID
for (const auto& content : contents) {
if (content.type == Core::ContentType::Application) {
title_name_map.emplace(content.id, GetContentName(content));
extdata_id_map.emplace(content.extdata_id, content.id);
}
}
title_name_map.insert_or_assign(0, tr("Ungrouped"));
std::unordered_map<u64, u64> title_row_map;
for (const auto& [id, name] : title_name_map) {
auto* checkBox = new QCheckBox();
checkBox->setText(tr(name));
checkBox->setText(name);
checkBox->setStyleSheet(QStringLiteral("margin-left:7px"));
checkBox->setTristate(true);
checkBox->setProperty("previousState", static_cast<int>(Qt::Unchecked));
@@ -120,6 +130,7 @@ void ImportDialog::RepopulateContent() {
auto* item = new QTreeWidgetItem;
item->setFirstColumnSpanned(true);
ui->main->invisibleRootItem()->addChild(item);
title_row_map[id] = ui->main->invisibleRootItem()->childCount() - 1;
connect(checkBox, &QCheckBox::stateChanged, [this, checkBox, item](int state) {
SCOPE_EXIT({ checkBox->setProperty("previousState", state); });
@@ -157,11 +168,39 @@ void ImportDialog::RepopulateContent() {
// HACK: The checkbox is used to record ID. Is there a better way?
checkBox->setProperty("id", i);
std::size_t row = title_row_map.at(0);
switch (content.type) {
case Core::ContentType::Application:
case Core::ContentType::Update:
case Core::ContentType::DLC:
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) : title_row_map.at(0);
break;
}
case Core::ContentType::Extdata: {
const auto real_id =
extdata_id_map.count(content.id) ? extdata_id_map.at(content.id) : 0;
row = title_row_map.at(real_id);
break;
}
case Core::ContentType::Sysdata: {
row = title_row_map.at(0);
break;
}
}
const QString name = (row == 0 ? QStringLiteral("%1 (%2)")
.arg(GetContentName(content))
.arg(tr(ContentTypeMap.at(content.type)))
: tr(ContentTypeMap.at(content.type)));
auto* item = new QTreeWidgetItem{
{QString{}, GetContentName(content), ReadableByteSize(content.maximum_size),
{QString{}, name, ReadableByteSize(content.maximum_size),
content.already_exists ? QStringLiteral("Yes") : QStringLiteral("No")}};
ui->main->invisibleRootItem()->child(static_cast<int>(content.type))->addChild(item);
ui->main->invisibleRootItem()->child(row)->addChild(item);
ui->main->setItemWidget(item, 0, checkBox);
connect(checkBox, &QCheckBox::stateChanged,
@@ -275,10 +314,11 @@ void ImportDialog::StartImporting() {
[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("(%1/%2) Importing %3...")
dialog->setLabelText(tr("(%1/%2) Importing %3 (%4)...")
.arg(count)
.arg(total_count)
.arg(GetContentName(next_content)));
.arg(GetContentName(next_content))
.arg(tr(ContentTypeMap.at(next_content.type))));
current_content = next_content;
current_count = count;
});
@@ -286,18 +326,20 @@ void ImportDialog::StartImporting() {
[this, dialog, multiplier, total_count](u64 total_size_imported,
u64 current_size_imported) {
dialog->setValue(static_cast<int>(total_size_imported / multiplier));
dialog->setLabelText(tr("(%1/%2) Importing %3 (%4/%5)...")
dialog->setLabelText(tr("(%1/%2) Importing %3 (%4) (%5/%6)...")
.arg(current_count)
.arg(total_count)
.arg(GetContentName(current_content))
.arg(tr(ContentTypeMap.at(current_content.type)))
.arg(ReadableByteSize(current_size_imported))
.arg(ReadableByteSize(current_content.maximum_size)));
});
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)));
QMessageBox::critical(this, tr("Error"),
tr("Failed to import content %1 (%2)!")
.arg(GetContentName(current_content))
.arg(tr(ContentTypeMap.at(current_content.type))));
dialog->hide();
});
connect(job, &ImportJob::Completed, this, [this, dialog] {