| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,325 @@ | ||
| // Copyright 2018 Dolphin Emulator Project | ||
| // Licensed under GPLv2+ | ||
| // Refer to the license.txt file included. | ||
|
|
||
| #include "DolphinQt/ResourcePackManager.h" | ||
|
|
||
| #include <QDesktopServices> | ||
| #include <QDialogButtonBox> | ||
| #include <QGridLayout> | ||
| #include <QHeaderView> | ||
| #include <QMessageBox> | ||
| #include <QPushButton> | ||
| #include <QTableWidget> | ||
| #include <QUrl> | ||
|
|
||
| #include "Common/FileUtil.h" | ||
| #include "UICommon/ResourcePack/Manager.h" | ||
|
|
||
| ResourcePackManager::ResourcePackManager(QWidget* widget) : QDialog(widget) | ||
| { | ||
| CreateWidgets(); | ||
| ConnectWidgets(); | ||
| RepopulateTable(); | ||
|
|
||
| setWindowTitle(tr("Resource Pack Manager")); | ||
| setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); | ||
|
|
||
| resize(QSize(900, 600)); | ||
| } | ||
|
|
||
| void ResourcePackManager::CreateWidgets() | ||
| { | ||
| auto* layout = new QGridLayout; | ||
|
|
||
| m_table_widget = new QTableWidget; | ||
|
|
||
| m_open_directory_button = new QPushButton(tr("Open Directory...")); | ||
| m_change_button = new QPushButton(tr("Install")); | ||
| m_remove_button = new QPushButton(tr("Remove")); | ||
| m_refresh_button = new QPushButton(tr("Refresh")); | ||
| m_priority_up_button = new QPushButton(tr("Up")); | ||
| m_priority_down_button = new QPushButton(tr("Down")); | ||
|
|
||
| auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok); | ||
|
|
||
| connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); | ||
|
|
||
| layout->addWidget(m_table_widget, 0, 0, 7, 1); | ||
| layout->addWidget(m_open_directory_button, 0, 1); | ||
| layout->addWidget(m_change_button, 1, 1); | ||
| layout->addWidget(m_remove_button, 2, 1); | ||
| layout->addWidget(m_refresh_button, 3, 1); | ||
| layout->addWidget(m_priority_up_button, 4, 1); | ||
| layout->addWidget(m_priority_down_button, 5, 1); | ||
|
|
||
| layout->addWidget(buttons, 7, 1, Qt::AlignRight); | ||
| setLayout(layout); | ||
| setLayout(layout); | ||
| } | ||
|
|
||
| void ResourcePackManager::ConnectWidgets() | ||
| { | ||
| connect(m_open_directory_button, &QPushButton::pressed, this, | ||
| &ResourcePackManager::OpenResourcePackDir); | ||
| connect(m_refresh_button, &QPushButton::pressed, this, &ResourcePackManager::Refresh); | ||
| connect(m_change_button, &QPushButton::pressed, this, &ResourcePackManager::Change); | ||
| connect(m_remove_button, &QPushButton::pressed, this, &ResourcePackManager::Remove); | ||
| connect(m_priority_up_button, &QPushButton::pressed, this, &ResourcePackManager::PriorityUp); | ||
| connect(m_priority_down_button, &QPushButton::pressed, this, &ResourcePackManager::PriorityDown); | ||
|
|
||
| connect(m_table_widget, &QTableWidget::itemSelectionChanged, this, | ||
| &ResourcePackManager::SelectionChanged); | ||
|
|
||
| connect(m_table_widget, &QTableWidget::itemDoubleClicked, this, | ||
| &ResourcePackManager::ItemDoubleClicked); | ||
| } | ||
|
|
||
| void ResourcePackManager::OpenResourcePackDir() | ||
| { | ||
| QDesktopServices::openUrl( | ||
| QUrl::fromLocalFile(QString::fromStdString(File::GetUserPath(D_RESOURCEPACK_IDX)))); | ||
| } | ||
|
|
||
| void ResourcePackManager::RepopulateTable() | ||
| { | ||
| m_table_widget->clear(); | ||
| m_table_widget->setColumnCount(6); | ||
|
|
||
| m_table_widget->setHorizontalHeaderLabels({QStringLiteral(""), tr("Name"), tr("Version"), | ||
| tr("Description"), tr("Author"), tr("Website")}); | ||
|
|
||
| auto* header = m_table_widget->horizontalHeader(); | ||
|
|
||
| for (int i = 0; i < 4; i++) | ||
| header->setSectionResizeMode(i, QHeaderView::ResizeToContents); | ||
|
|
||
| header->setStretchLastSection(true); | ||
|
|
||
| int size = static_cast<int>(ResourcePack::GetPacks().size()); | ||
|
|
||
| m_table_widget->setSelectionBehavior(QAbstractItemView::SelectRows); | ||
| m_table_widget->setSelectionMode(QAbstractItemView::SingleSelection); | ||
|
|
||
| m_table_widget->setRowCount(size); | ||
| m_table_widget->setIconSize(QSize(32, 32)); | ||
|
|
||
| for (int i = 0; i < size; i++) | ||
| { | ||
| const auto& pack = ResourcePack::GetPacks()[size - 1 - i]; | ||
| auto* manifest = pack.GetManifest(); | ||
|
|
||
| auto* logo_item = new QTableWidgetItem; | ||
| auto* name_item = new QTableWidgetItem(QString::fromStdString(manifest->GetName())); | ||
| auto* version_item = new QTableWidgetItem(QString::fromStdString(manifest->GetVersion())); | ||
| auto* author_item = new QTableWidgetItem( | ||
| QString::fromStdString(manifest->GetAuthors().value_or("Unknown author"))); | ||
| auto* description_item = | ||
| new QTableWidgetItem(QString::fromStdString(manifest->GetDescription().value_or(""))); | ||
| auto* website_item = | ||
| new QTableWidgetItem(QString::fromStdString(manifest->GetWebsite().value_or(""))); | ||
|
|
||
| QPixmap logo; | ||
|
|
||
| logo.loadFromData(reinterpret_cast<const uchar*>(pack.GetLogo().data()), | ||
| (int)pack.GetLogo().size()); | ||
|
|
||
| logo_item->setIcon(QIcon(logo)); | ||
|
|
||
| QFont link_font = website_item->font(); | ||
|
|
||
| link_font.setUnderline(true); | ||
|
|
||
| website_item->setFont(link_font); | ||
| website_item->setForeground(QBrush(Qt::blue)); | ||
| website_item->setData(Qt::UserRole, website_item->text()); | ||
|
|
||
| for (auto* item : | ||
| {logo_item, name_item, version_item, author_item, description_item, website_item}) | ||
| { | ||
| item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); | ||
|
|
||
| if (ResourcePack::IsInstalled(pack)) | ||
| { | ||
| item->setBackgroundColor(QColor(Qt::green)); | ||
|
|
||
| auto font = item->font(); | ||
| font.setBold(true); | ||
| item->setFont(font); | ||
| } | ||
| } | ||
|
|
||
| m_table_widget->setItem(i, 0, logo_item); | ||
| m_table_widget->setItem(i, 1, name_item); | ||
| m_table_widget->setItem(i, 2, version_item); | ||
| m_table_widget->setItem(i, 3, description_item); | ||
| m_table_widget->setItem(i, 4, author_item); | ||
| m_table_widget->setItem(i, 5, website_item); | ||
| } | ||
|
|
||
| SelectionChanged(); | ||
| } | ||
|
|
||
| void ResourcePackManager::Change() | ||
| { | ||
| auto items = m_table_widget->selectedItems(); | ||
|
|
||
| if (items.empty()) | ||
| return; | ||
|
|
||
| if (ResourcePack::IsInstalled(ResourcePack::GetPacks()[items[0]->row()])) | ||
| Uninstall(); | ||
| else | ||
| Install(); | ||
| } | ||
|
|
||
| void ResourcePackManager::Install() | ||
| { | ||
| auto items = m_table_widget->selectedItems(); | ||
|
|
||
| if (items.empty()) | ||
| return; | ||
|
|
||
| auto& item = ResourcePack::GetPacks()[m_table_widget->rowCount() - 1 - items[0]->row()]; | ||
|
|
||
| bool success = item.Install(File::GetUserPath(D_USER_IDX)); | ||
|
|
||
| if (!success) | ||
| { | ||
| QMessageBox::critical( | ||
| this, tr("Error"), | ||
| tr("Failed to install pack: %1").arg(QString::fromStdString(item.GetError()))); | ||
| } | ||
|
|
||
| RepopulateTable(); | ||
| } | ||
|
|
||
| void ResourcePackManager::Uninstall() | ||
| { | ||
| auto items = m_table_widget->selectedItems(); | ||
|
|
||
| if (items.empty()) | ||
| return; | ||
|
|
||
| auto& item = ResourcePack::GetPacks()[m_table_widget->rowCount() - 1 - items[0]->row()]; | ||
|
|
||
| bool success = item.Uninstall(File::GetUserPath(D_USER_IDX)); | ||
|
|
||
| if (!success) | ||
| { | ||
| QMessageBox::critical( | ||
| this, tr("Error"), | ||
| tr("Failed to uninstall pack: %1").arg(QString::fromStdString(item.GetError()))); | ||
| } | ||
|
|
||
| RepopulateTable(); | ||
| } | ||
|
|
||
| void ResourcePackManager::Remove() | ||
| { | ||
| auto items = m_table_widget->selectedItems(); | ||
|
|
||
| if (items.empty()) | ||
| return; | ||
|
|
||
| QMessageBox box(this); | ||
| box.setWindowTitle(tr("Confirmation")); | ||
| box.setText(tr("Are you sure you want to delete this pack?")); | ||
| box.setIcon(QMessageBox::Warning); | ||
| box.setStandardButtons(QMessageBox::Yes | QMessageBox::Abort); | ||
|
|
||
| if (box.exec() != QMessageBox::Yes) | ||
| return; | ||
|
|
||
| Uninstall(); | ||
| File::Delete( | ||
| ResourcePack::GetPacks()[m_table_widget->rowCount() - 1 - items[0]->row()].GetPath()); | ||
| RepopulateTable(); | ||
| } | ||
|
|
||
| void ResourcePackManager::PriorityDown() | ||
| { | ||
| auto items = m_table_widget->selectedItems(); | ||
|
|
||
| if (items.empty()) | ||
| return; | ||
|
|
||
| int row = m_table_widget->rowCount() - 1 - items[0]->row(); | ||
|
|
||
| if (items[0]->row() >= m_table_widget->rowCount()) | ||
| return; | ||
|
|
||
| auto& pack = ResourcePack::GetPacks()[row]; | ||
| std::string path = pack.GetPath(); | ||
|
|
||
| row--; | ||
|
|
||
| ResourcePack::Remove(pack); | ||
| ResourcePack::Add(path, row); | ||
|
|
||
| RepopulateTable(); | ||
|
|
||
| m_table_widget->selectRow(row == 0 ? m_table_widget->rowCount() - 1 : row); | ||
| } | ||
|
|
||
| void ResourcePackManager::PriorityUp() | ||
| { | ||
| auto items = m_table_widget->selectedItems(); | ||
|
|
||
| if (items.empty()) | ||
| return; | ||
|
|
||
| int row = m_table_widget->rowCount() - 1 - items[0]->row(); | ||
|
|
||
| if (items[0]->row() == 0) | ||
| return; | ||
|
|
||
| auto& pack = ResourcePack::GetPacks()[row]; | ||
| std::string path = pack.GetPath(); | ||
|
|
||
| row++; | ||
|
|
||
| ResourcePack::Remove(pack); | ||
| ResourcePack::Add(path, items[0]->row() == m_table_widget->rowCount() ? -1 : row); | ||
|
|
||
| RepopulateTable(); | ||
|
|
||
| m_table_widget->selectRow(row == m_table_widget->rowCount() - 1 ? 0 : row); | ||
| } | ||
|
|
||
| void ResourcePackManager::Refresh() | ||
| { | ||
| ResourcePack::Init(); | ||
| RepopulateTable(); | ||
| } | ||
|
|
||
| void ResourcePackManager::SelectionChanged() | ||
| { | ||
| auto items = m_table_widget->selectedItems(); | ||
|
|
||
| const bool has_selection = !items.empty(); | ||
|
|
||
| if (has_selection) | ||
| { | ||
| m_change_button->setText(ResourcePack::IsInstalled(ResourcePack::GetPacks()[items[0]->row()]) ? | ||
| tr("Uninstall") : | ||
| tr("Install")); | ||
| } | ||
|
|
||
| for (auto* item : {m_change_button, m_remove_button}) | ||
| item->setEnabled(has_selection); | ||
|
|
||
| m_priority_down_button->setEnabled(has_selection && | ||
| items[0]->row() < m_table_widget->rowCount() - 1); | ||
| m_priority_up_button->setEnabled(has_selection && items[0]->row() != 0); | ||
| } | ||
|
|
||
| void ResourcePackManager::ItemDoubleClicked(QTableWidgetItem* item) | ||
| { | ||
| auto item_data = item->data(Qt::UserRole); | ||
|
|
||
| if (item_data.isNull()) | ||
| return; | ||
|
|
||
| QDesktopServices::openUrl(QUrl(item_data.toString())); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| // Copyright 2018 Dolphin Emulator Project | ||
| // Licensed under GPLv2+ | ||
| // Refer to the license.txt file included. | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <QDialog> | ||
|
|
||
| class QPushButton; | ||
| class QTableWidget; | ||
| class QTableWidgetItem; | ||
|
|
||
| class ResourcePackManager : public QDialog | ||
| { | ||
| public: | ||
| explicit ResourcePackManager(QWidget* parent = nullptr); | ||
|
|
||
| private: | ||
| void CreateWidgets(); | ||
| void ConnectWidgets(); | ||
| void OpenResourcePackDir(); | ||
| void RepopulateTable(); | ||
| void Change(); | ||
| void Install(); | ||
| void Uninstall(); | ||
| void Remove(); | ||
| void PriorityUp(); | ||
| void PriorityDown(); | ||
| void Refresh(); | ||
|
|
||
| void SelectionChanged(); | ||
| void ItemDoubleClicked(QTableWidgetItem* item); | ||
|
|
||
| QPushButton* m_open_directory_button; | ||
| QPushButton* m_change_button; | ||
| QPushButton* m_remove_button; | ||
| QPushButton* m_refresh_button; | ||
| QPushButton* m_priority_up_button; | ||
| QPushButton* m_priority_down_button; | ||
|
|
||
| QTableWidget* m_table_widget; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| // Copyright 2018 Dolphin Emulator Project | ||
| // Licensed under GPLv2+ | ||
| // Refer to the license.txt file included. | ||
|
|
||
| #include "UICommon/ResourcePack/Manager.h" | ||
|
|
||
| #include "Common/CommonTypes.h" | ||
| #include "Common/FileSearch.h" | ||
| #include "Common/FileUtil.h" | ||
| #include "Common/IniFile.h" | ||
|
|
||
| #include <algorithm> | ||
|
|
||
| namespace | ||
| { | ||
| std::vector<ResourcePack::ResourcePack> packs; | ||
|
|
||
| std::string packs_path; | ||
| } // namespace | ||
|
|
||
| namespace ResourcePack | ||
| { | ||
| IniFile GetPackConfig() | ||
| { | ||
| packs_path = File::GetUserPath(D_RESOURCEPACK_IDX) + "/Packs.ini"; | ||
|
|
||
| IniFile file; | ||
| file.Load(packs_path); | ||
|
|
||
| return file; | ||
| } | ||
|
|
||
| bool Init() | ||
| { | ||
| packs.clear(); | ||
| auto pack_list = Common::DoFileSearch({File::GetUserPath(D_RESOURCEPACK_IDX)}, {".zip"}); | ||
|
|
||
| bool error = false; | ||
|
|
||
| IniFile file = GetPackConfig(); | ||
|
|
||
| auto* order = file.GetOrCreateSection("Order"); | ||
|
|
||
| std::sort(pack_list.begin(), pack_list.end(), [order](std::string& a, std::string& b) { | ||
| std::string order_a = a, order_b = b; | ||
|
|
||
| order->Get(ResourcePack(a).GetManifest()->GetID(), &order_a); | ||
| order->Get(ResourcePack(b).GetManifest()->GetID(), &order_b); | ||
|
|
||
| return order_a < order_b; | ||
| }); | ||
|
|
||
| for (size_t i = 0; i < pack_list.size(); i++) | ||
| { | ||
| const auto& path = pack_list[i]; | ||
|
|
||
| error |= !Add(path); | ||
|
|
||
| order->Set(packs[i].GetManifest()->GetID(), static_cast<u64>(i)); | ||
| } | ||
|
|
||
| file.Save(packs_path); | ||
|
|
||
| return !error; | ||
| } | ||
|
|
||
| std::vector<ResourcePack>& GetPacks() | ||
| { | ||
| return packs; | ||
| } | ||
|
|
||
| std::vector<ResourcePack*> GetLowerPriorityPacks(ResourcePack& pack) | ||
| { | ||
| std::vector<ResourcePack*> list; | ||
| for (auto it = std::find(packs.begin(), packs.end(), pack) + 1; it != packs.end(); it++) | ||
| { | ||
| auto& entry = *it; | ||
| if (!IsInstalled(pack)) | ||
| continue; | ||
|
|
||
| list.push_back(&entry); | ||
| } | ||
|
|
||
| return list; | ||
| } | ||
|
|
||
| std::vector<ResourcePack*> GetHigherPriorityPacks(ResourcePack& pack) | ||
| { | ||
| std::vector<ResourcePack*> list; | ||
| auto end = std::find(packs.begin(), packs.end(), pack); | ||
|
|
||
| for (auto it = packs.begin(); it != end; it++) | ||
| { | ||
| auto& entry = *it; | ||
| if (!IsInstalled(entry)) | ||
| continue; | ||
| list.push_back(&entry); | ||
| } | ||
|
|
||
| return list; | ||
| } | ||
|
|
||
| bool Add(const std::string& path, int offset) | ||
| { | ||
| if (offset == -1) | ||
| offset = static_cast<int>(packs.size()); | ||
|
|
||
| ResourcePack pack(path); | ||
|
|
||
| IniFile file = GetPackConfig(); | ||
|
|
||
| auto* order = file.GetOrCreateSection("Order"); | ||
|
|
||
| order->Set(pack.GetManifest()->GetID(), offset); | ||
|
|
||
| for (int i = offset; i < static_cast<int>(packs.size()); i++) | ||
| order->Set(packs[i].GetManifest()->GetID(), i + 1); | ||
|
|
||
| file.Save(packs_path); | ||
|
|
||
| packs.insert(packs.begin() + offset, std::move(pack)); | ||
|
|
||
| return pack.IsValid(); | ||
| } | ||
|
|
||
| bool Remove(ResourcePack& pack) | ||
| { | ||
| const auto result = pack.Uninstall(File::GetUserPath(D_USER_IDX)); | ||
|
|
||
| if (!result) | ||
| return false; | ||
|
|
||
| auto pack_iterator = std::find(packs.begin(), packs.end(), pack); | ||
|
|
||
| if (pack_iterator == packs.end()) | ||
| return false; | ||
|
|
||
| std::string filename; | ||
|
|
||
| IniFile file = GetPackConfig(); | ||
|
|
||
| auto* order = file.GetOrCreateSection("Order"); | ||
|
|
||
| order->Delete(pack.GetManifest()->GetID()); | ||
|
|
||
| int offset = pack_iterator - packs.begin(); | ||
|
|
||
| for (int i = offset + 1; i < static_cast<int>(packs.size()); i++) | ||
| order->Set(packs[i].GetManifest()->GetID(), i - 1); | ||
|
|
||
| file.Save(packs_path); | ||
|
|
||
| packs.erase(pack_iterator); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| void SetInstalled(const ResourcePack& pack, bool installed) | ||
| { | ||
| IniFile file = GetPackConfig(); | ||
|
|
||
| auto* install = file.GetOrCreateSection("Installed"); | ||
|
|
||
| if (installed) | ||
| install->Set(pack.GetManifest()->GetID(), installed); | ||
| else | ||
| install->Delete(pack.GetManifest()->GetID()); | ||
|
|
||
| file.Save(packs_path); | ||
| } | ||
|
|
||
| bool IsInstalled(const ResourcePack& pack) | ||
| { | ||
| IniFile file = GetPackConfig(); | ||
|
|
||
| auto* install = file.GetOrCreateSection("Installed"); | ||
|
|
||
| bool installed; | ||
|
|
||
| install->Get(pack.GetManifest()->GetID(), &installed, false); | ||
|
|
||
| return installed; | ||
| } | ||
|
|
||
| } // namespace ResourcePack |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| // Copyright 2018 Dolphin Emulator Project | ||
| // Licensed under GPLv2+ | ||
| // Refer to the license.txt file included. | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <string> | ||
| #include <vector> | ||
|
|
||
| #include "UICommon/ResourcePack/ResourcePack.h" | ||
|
|
||
| namespace ResourcePack | ||
| { | ||
| bool Init(); | ||
|
|
||
| bool Add(const std::string& path, int offset = -1); | ||
| bool Remove(ResourcePack& pack); | ||
| void SetInstalled(const ResourcePack& pack, bool installed); | ||
| bool IsInstalled(const ResourcePack& pack); | ||
|
|
||
| std::vector<ResourcePack>& GetPacks(); | ||
|
|
||
| std::vector<ResourcePack*> GetHigherPriorityPacks(ResourcePack& pack); | ||
| std::vector<ResourcePack*> GetLowerPriorityPacks(ResourcePack& pack); | ||
| } // namespace ResourcePack |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| // Copyright 2018 Dolphin Emulator Project | ||
| // Licensed under GPLv2+ | ||
| // Refer to the license.txt file included. | ||
|
|
||
| #include "UICommon/ResourcePack/Manifest.h" | ||
|
|
||
| #include <picojson/picojson.h> | ||
|
|
||
| namespace ResourcePack | ||
| { | ||
| Manifest::Manifest(const std::string& json) | ||
| { | ||
| picojson::value out; | ||
| auto error = picojson::parse(out, json); | ||
|
|
||
| if (!error.empty()) | ||
| { | ||
| m_error = "Failed to parse manifest."; | ||
| m_valid = false; | ||
| return; | ||
| } | ||
|
|
||
| // Required fields | ||
| picojson::value& name = out.get("name"); | ||
| picojson::value& version = out.get("version"); | ||
| picojson::value& id = out.get("id"); | ||
|
|
||
| // Optional fields | ||
| picojson::value& authors = out.get("authors"); | ||
| picojson::value& description = out.get("description"); | ||
| picojson::value& website = out.get("website"); | ||
|
|
||
| if (!name.is<std::string>() || !id.is<std::string>() || !version.is<std::string>()) | ||
| { | ||
| m_error = "Some objects have a bad type."; | ||
| m_valid = false; | ||
| return; | ||
| } | ||
|
|
||
| m_name = name.to_str(); | ||
| m_version = version.to_str(); | ||
| m_id = id.to_str(); | ||
|
|
||
| if (authors.is<picojson::array>()) | ||
| { | ||
| std::string author_list; | ||
| for (const auto& o : authors.get<picojson::array>()) | ||
| { | ||
| author_list += o.to_str() + ", "; | ||
| } | ||
|
|
||
| if (!author_list.empty()) | ||
| m_authors = author_list.substr(0, author_list.size() - 2); | ||
| } | ||
|
|
||
| if (description.is<std::string>()) | ||
| m_description = description.to_str(); | ||
|
|
||
| if (website.is<std::string>()) | ||
| m_website = website.to_str(); | ||
| } | ||
|
|
||
| bool Manifest::IsValid() const | ||
| { | ||
| return m_valid; | ||
| } | ||
|
|
||
| const std::string& Manifest::GetName() const | ||
| { | ||
| return m_name; | ||
| } | ||
|
|
||
| const std::string& Manifest::GetVersion() const | ||
| { | ||
| return m_version; | ||
| } | ||
|
|
||
| const std::string& Manifest::GetID() const | ||
| { | ||
| return m_id; | ||
| } | ||
|
|
||
| const std::string& Manifest::GetError() const | ||
| { | ||
| return m_error; | ||
| } | ||
|
|
||
| const std::optional<std::string>& Manifest::GetAuthors() const | ||
| { | ||
| return m_authors; | ||
| } | ||
|
|
||
| const std::optional<std::string>& Manifest::GetDescription() const | ||
| { | ||
| return m_description; | ||
| } | ||
|
|
||
| const std::optional<std::string>& Manifest::GetWebsite() const | ||
| { | ||
| return m_website; | ||
| } | ||
| } // namespace ResourcePack |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| // Copyright 2018 Dolphin Emulator Project | ||
| // Licensed under GPLv2+ | ||
| // Refer to the license.txt file included. | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <optional> | ||
| #include <string> | ||
|
|
||
| namespace ResourcePack | ||
| { | ||
| class Manifest | ||
| { | ||
| public: | ||
| explicit Manifest(const std::string& text); | ||
|
|
||
| bool IsValid() const; | ||
|
|
||
| const std::string& GetName() const; | ||
| const std::string& GetVersion() const; | ||
| const std::string& GetID() const; | ||
| const std::string& GetError() const; | ||
|
|
||
| const std::optional<std::string>& GetAuthors() const; | ||
| const std::optional<std::string>& GetDescription() const; | ||
| const std::optional<std::string>& GetWebsite() const; | ||
|
|
||
| private: | ||
| bool m_valid = true; | ||
|
|
||
| std::string m_name; | ||
| std::string m_version; | ||
| std::string m_id; | ||
| std::string m_error; | ||
|
|
||
| std::optional<std::string> m_authors; | ||
| std::optional<std::string> m_description; | ||
| std::optional<std::string> m_website; | ||
| }; | ||
| } // namespace ResourcePack |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,326 @@ | ||
| // Copyright 2018 Dolphin Emulator Project | ||
| // Licensed under GPLv2+ | ||
| // Refer to the license.txt file included. | ||
|
|
||
| #include "UICommon/ResourcePack/ResourcePack.h" | ||
|
|
||
| #include <algorithm> | ||
|
|
||
| #include <minizip/unzip.h> | ||
|
|
||
| #include "Common/FileSearch.h" | ||
| #include "Common/FileUtil.h" | ||
| #include "Common/StringUtil.h" | ||
|
|
||
| #include "UICommon/ResourcePack/Manager.h" | ||
| #include "UICommon/ResourcePack/Manifest.h" | ||
|
|
||
| static const char* TEXTURE_PATH = "Load/Textures/"; | ||
|
|
||
| namespace ResourcePack | ||
| { | ||
| // Since minzip doesn't provide a way to unzip a file of a length > 65535, we have to implement | ||
| // this ourselves | ||
| static bool ReadCurrentFileUnlimited(unzFile file, std::vector<char>& destination) | ||
| { | ||
| const uint32_t MAX_BUFFER_SIZE = 65535; | ||
|
|
||
| if (unzOpenCurrentFile(file) != UNZ_OK) | ||
| return false; | ||
|
|
||
| uint32_t bytes_to_go = static_cast<uint32_t>(destination.size()); | ||
|
|
||
| while (bytes_to_go > 0) | ||
| { | ||
| int bytes_read = unzReadCurrentFile(file, &destination[destination.size() - bytes_to_go], | ||
| std::min(bytes_to_go, MAX_BUFFER_SIZE)); | ||
|
|
||
| if (bytes_read < 0) | ||
| { | ||
| unzCloseCurrentFile(file); | ||
| return false; | ||
| } | ||
|
|
||
| bytes_to_go -= bytes_read; | ||
| } | ||
|
|
||
| unzCloseCurrentFile(file); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| ResourcePack::ResourcePack(const std::string& path) : m_path(path) | ||
| { | ||
| auto file = unzOpen(path.c_str()); | ||
|
|
||
| if (file == nullptr) | ||
| { | ||
| m_valid = false; | ||
| m_error = "Failed to open resource pack"; | ||
| return; | ||
| } | ||
|
|
||
| if (unzLocateFile(file, "manifest.json", 0) == UNZ_END_OF_LIST_OF_FILE) | ||
| { | ||
| m_valid = false; | ||
| m_error = "Resource pack is missing a manifest."; | ||
| return; | ||
| } | ||
|
|
||
| unz_file_info manifest_info; | ||
|
|
||
| unzGetCurrentFileInfo(file, &manifest_info, nullptr, 0, nullptr, 0, nullptr, 0); | ||
|
|
||
| std::vector<char> manifest_contents; | ||
|
|
||
| manifest_contents.resize(manifest_info.uncompressed_size); | ||
|
|
||
| if (!ReadCurrentFileUnlimited(file, manifest_contents)) | ||
| { | ||
| m_valid = false; | ||
| m_error = "Failed to read manifest.json"; | ||
| return; | ||
| } | ||
|
|
||
| unzCloseCurrentFile(file); | ||
|
|
||
| m_manifest = | ||
| std::make_shared<Manifest>(std::string(manifest_contents.begin(), manifest_contents.end())); | ||
|
|
||
| if (!m_manifest->IsValid()) | ||
| { | ||
| m_valid = false; | ||
| m_error = "Manifest error: " + m_manifest->GetError(); | ||
| return; | ||
| } | ||
|
|
||
| if (unzLocateFile(file, "logo.png", 0) != UNZ_END_OF_LIST_OF_FILE) | ||
| { | ||
| unz_file_info logo_info; | ||
|
|
||
| unzGetCurrentFileInfo(file, &logo_info, nullptr, 0, nullptr, 0, nullptr, 0); | ||
|
|
||
| m_logo_data.resize(logo_info.uncompressed_size); | ||
|
|
||
| if (!ReadCurrentFileUnlimited(file, m_logo_data)) | ||
| { | ||
| m_valid = false; | ||
| m_error = "Failed to read logo.png"; | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| unzGoToFirstFile(file); | ||
|
|
||
| do | ||
| { | ||
| std::string filename; | ||
|
|
||
| filename.resize(256); | ||
|
|
||
| unz_file_info texture_info; | ||
|
|
||
| unzGetCurrentFileInfo(file, &texture_info, &filename[0], static_cast<uint16_t>(filename.size()), | ||
| nullptr, 0, nullptr, 0); | ||
|
|
||
| if (filename.compare(0, 9, "textures/") != 0 || texture_info.uncompressed_size == 0) | ||
| continue; | ||
|
|
||
| // If a texture is compressed, abort. | ||
| if (texture_info.compression_method != 0) | ||
| { | ||
| m_valid = false; | ||
| m_error = "Texture " + filename + " is compressed!"; | ||
| return; | ||
| } | ||
|
|
||
| m_textures.push_back(filename.substr(9)); | ||
| } while (unzGoToNextFile(file) != UNZ_END_OF_LIST_OF_FILE); | ||
|
|
||
| unzClose(file); | ||
| } | ||
|
|
||
| bool ResourcePack::IsValid() const | ||
| { | ||
| return m_valid; | ||
| } | ||
|
|
||
| const std::vector<char>& ResourcePack::GetLogo() const | ||
| { | ||
| return m_logo_data; | ||
| } | ||
|
|
||
| const std::string& ResourcePack::GetPath() const | ||
| { | ||
| return m_path; | ||
| } | ||
|
|
||
| const std::string& ResourcePack::GetError() const | ||
| { | ||
| return m_error; | ||
| } | ||
|
|
||
| const Manifest* ResourcePack::GetManifest() const | ||
| { | ||
| return m_manifest.get(); | ||
| } | ||
|
|
||
| const std::vector<std::string>& ResourcePack::GetTextures() const | ||
| { | ||
| return m_textures; | ||
| } | ||
|
|
||
| bool ResourcePack::Install(const std::string& path) | ||
| { | ||
| if (!IsValid()) | ||
| { | ||
| m_error = "Invalid pack"; | ||
| return false; | ||
| } | ||
|
|
||
| auto file = unzOpen(m_path.c_str()); | ||
|
|
||
| for (const auto& texture : m_textures) | ||
| { | ||
| bool provided_by_other_pack = false; | ||
|
|
||
| // Check if a higher priority pack already provides a given texture, don't overwrite it | ||
| for (const auto& pack : GetHigherPriorityPacks(*this)) | ||
| { | ||
| if (std::find(pack->GetTextures().begin(), pack->GetTextures().end(), texture) != | ||
| pack->GetTextures().end()) | ||
| { | ||
| provided_by_other_pack = true; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (provided_by_other_pack) | ||
| continue; | ||
|
|
||
| if (unzLocateFile(file, ("textures/" + texture).c_str(), 0) != UNZ_OK) | ||
| { | ||
| m_error = "Failed to locate texture " + texture; | ||
| return false; | ||
| } | ||
|
|
||
| std::string m_full_dir; | ||
|
|
||
| SplitPath(path + TEXTURE_PATH + texture, &m_full_dir, nullptr, nullptr); | ||
|
|
||
| if (!File::CreateFullPath(m_full_dir)) | ||
| { | ||
| m_error = "Failed to create full path " + m_full_dir; | ||
| return false; | ||
| } | ||
|
|
||
| unz_file_info texture_info; | ||
|
|
||
| unzGetCurrentFileInfo(file, &texture_info, nullptr, 0, nullptr, 0, nullptr, 0); | ||
|
|
||
| std::vector<char> data; | ||
| data.resize(texture_info.uncompressed_size); | ||
|
|
||
| if (!ReadCurrentFileUnlimited(file, data)) | ||
| { | ||
| m_error = "Failed to read texture " + texture; | ||
| return false; | ||
| } | ||
|
|
||
| std::ofstream out(path + TEXTURE_PATH + texture, std::ios::trunc | std::ios::binary); | ||
|
|
||
| if (!out.good()) | ||
| { | ||
| m_error = "Failed to write " + texture; | ||
| return false; | ||
| } | ||
|
|
||
| out.write(data.data(), data.size()); | ||
| out.flush(); | ||
| } | ||
|
|
||
| unzClose(file); | ||
|
|
||
| SetInstalled(*this, true); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| bool ResourcePack::Uninstall(const std::string& path) | ||
| { | ||
| if (!IsValid()) | ||
| { | ||
| m_error = "Invalid pack"; | ||
| return false; | ||
| } | ||
|
|
||
| auto lower = GetLowerPriorityPacks(*this); | ||
|
|
||
| SetInstalled(*this, false); | ||
|
|
||
| for (const auto& texture : m_textures) | ||
| { | ||
| bool provided_by_other_pack = false; | ||
|
|
||
| // Check if a higher priority pack already provides a given texture, don't delete it | ||
| for (const auto& pack : GetHigherPriorityPacks(*this)) | ||
| { | ||
| if (std::find(pack->GetTextures().begin(), pack->GetTextures().end(), texture) != | ||
| pack->GetTextures().end()) | ||
| { | ||
| provided_by_other_pack = true; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (provided_by_other_pack) | ||
| continue; | ||
|
|
||
| // Check if a lower priority pack provides a given texture - if so, install it. | ||
| for (auto& pack : lower) | ||
| { | ||
| if (std::find(pack->GetTextures().rbegin(), pack->GetTextures().rend(), texture) != | ||
| pack->GetTextures().rend()) | ||
| { | ||
| pack->Install(path); | ||
|
|
||
| provided_by_other_pack = true; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (provided_by_other_pack) | ||
| continue; | ||
|
|
||
| if (File::Exists(path + TEXTURE_PATH + texture) && !File::Delete(path + TEXTURE_PATH + texture)) | ||
| { | ||
| m_error = "Failed to delete texture " + texture; | ||
| return false; | ||
| } | ||
|
|
||
| // Recursively delete empty directories | ||
|
|
||
| std::string dir; | ||
|
|
||
| SplitPath(path + TEXTURE_PATH + texture, &dir, nullptr, nullptr); | ||
|
|
||
| while (dir.length() > (path + TEXTURE_PATH).length()) | ||
| { | ||
| auto is_empty = Common::DoFileSearch({dir}).empty(); | ||
|
|
||
| if (is_empty) | ||
| File::DeleteDir(dir); | ||
|
|
||
| SplitPath(dir.substr(0, dir.size() - 2), &dir, nullptr, nullptr); | ||
| } | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| bool ResourcePack::operator==(const ResourcePack& pack) | ||
| { | ||
| return pack.GetPath() == m_path; | ||
| } | ||
|
|
||
| } // namespace ResourcePack |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| // Copyright 2018 Dolphin Emulator Project | ||
| // Licensed under GPLv2+ | ||
| // Refer to the license.txt file included. | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <memory> | ||
| #include <string> | ||
| #include <vector> | ||
|
|
||
| #include "Common/CommonTypes.h" | ||
|
|
||
| #include "UICommon/ResourcePack/Manifest.h" | ||
|
|
||
| namespace ResourcePack | ||
| { | ||
| class ResourcePack | ||
| { | ||
| public: | ||
| explicit ResourcePack(const std::string& path); | ||
|
|
||
| bool IsValid() const; | ||
| const std::vector<char>& GetLogo() const; | ||
|
|
||
| const std::string& GetPath() const; | ||
| const std::string& GetError() const; | ||
| const Manifest* GetManifest() const; | ||
| const std::vector<std::string>& GetTextures() const; | ||
|
|
||
| bool Install(const std::string& path); | ||
| bool Uninstall(const std::string& path); | ||
|
|
||
| bool operator==(const ResourcePack& pack); | ||
|
|
||
| private: | ||
| bool m_valid = true; | ||
|
|
||
| std::string m_path; | ||
| std::string m_error; | ||
|
|
||
| std::shared_ptr<Manifest> m_manifest; | ||
| std::vector<std::string> m_textures; | ||
| std::vector<char> m_logo_data; | ||
| }; | ||
| } // namespace ResourcePack |