@@ -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()));
}
@@ -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;
};
@@ -5,6 +5,9 @@ add_library(uicommon
DiscordPresence.cpp
GameFile.cpp
GameFileCache.cpp
ResourcePack/Manager.cpp
ResourcePack/Manifest.cpp
ResourcePack/ResourcePack.cpp
UICommon.cpp
USBUtils.cpp
VideoUtils.cpp
@@ -14,6 +17,7 @@ target_link_libraries(uicommon
PUBLIC
common
cpp-optparse
minizip

PRIVATE
$<$<BOOL:APPLE>:${IOK_LIBRARY}>
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -156,6 +156,7 @@ void SetLocale(std::string locale_name)

void CreateDirectories()
{
File::CreateFullPath(File::GetUserPath(D_RESOURCEPACK_IDX));
File::CreateFullPath(File::GetUserPath(D_USER_IDX));
File::CreateFullPath(File::GetUserPath(D_CACHE_IDX));
File::CreateFullPath(File::GetUserPath(D_COVERCACHE_IDX));
@@ -53,6 +53,9 @@
<ClCompile Include="AutoUpdate.cpp" />
<ClCompile Include="CommandLineParse.cpp" />
<ClCompile Include="DiscordPresence.cpp" />
<ClCompile Include="ResourcePack\Manager.cpp" />
<ClCompile Include="ResourcePack\Manifest.cpp" />
<ClCompile Include="ResourcePack\ResourcePack.cpp" />
<ClCompile Include="UICommon.cpp" />
<ClCompile Include="Disassembler.cpp" />
<ClCompile Include="USBUtils.cpp">
@@ -66,6 +69,9 @@
<ClInclude Include="AutoUpdate.h" />
<ClInclude Include="CommandLineParse.h" />
<ClInclude Include="DiscordPresence.h" />
<ClInclude Include="ResourcePack\Manager.h" />
<ClInclude Include="ResourcePack\Manifest.h" />
<ClInclude Include="ResourcePack\ResourcePack.h" />
<ClInclude Include="UICommon.h" />
<ClInclude Include="Disassembler.h" />
<ClInclude Include="USBUtils.h" />