Merge pull request #1105 from flowln/better_resource_packs
Add basic resource pack parsing and fix issues
This commit is contained in:
commit
333dbca01e
@ -322,6 +322,8 @@ set(MINECRAFT_SOURCES
|
|||||||
minecraft/mod/Resource.cpp
|
minecraft/mod/Resource.cpp
|
||||||
minecraft/mod/ResourceFolderModel.h
|
minecraft/mod/ResourceFolderModel.h
|
||||||
minecraft/mod/ResourceFolderModel.cpp
|
minecraft/mod/ResourceFolderModel.cpp
|
||||||
|
minecraft/mod/ResourcePack.h
|
||||||
|
minecraft/mod/ResourcePack.cpp
|
||||||
minecraft/mod/ResourcePackFolderModel.h
|
minecraft/mod/ResourcePackFolderModel.h
|
||||||
minecraft/mod/ResourcePackFolderModel.cpp
|
minecraft/mod/ResourcePackFolderModel.cpp
|
||||||
minecraft/mod/TexturePackFolderModel.h
|
minecraft/mod/TexturePackFolderModel.h
|
||||||
@ -334,6 +336,8 @@ set(MINECRAFT_SOURCES
|
|||||||
minecraft/mod/tasks/LocalModParseTask.cpp
|
minecraft/mod/tasks/LocalModParseTask.cpp
|
||||||
minecraft/mod/tasks/LocalModUpdateTask.h
|
minecraft/mod/tasks/LocalModUpdateTask.h
|
||||||
minecraft/mod/tasks/LocalModUpdateTask.cpp
|
minecraft/mod/tasks/LocalModUpdateTask.cpp
|
||||||
|
minecraft/mod/tasks/LocalResourcePackParseTask.h
|
||||||
|
minecraft/mod/tasks/LocalResourcePackParseTask.cpp
|
||||||
|
|
||||||
# Assets
|
# Assets
|
||||||
minecraft/AssetsUtils.h
|
minecraft/AssetsUtils.h
|
||||||
@ -384,6 +388,10 @@ ecm_add_test(minecraft/Library_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VER
|
|||||||
ecm_add_test(minecraft/mod/ResourceFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
|
ecm_add_test(minecraft/mod/ResourceFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
|
||||||
TEST_NAME ResourceFolderModel)
|
TEST_NAME ResourceFolderModel)
|
||||||
|
|
||||||
|
ecm_add_test(minecraft/mod/ResourcePackParse_test.cpp
|
||||||
|
LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
|
||||||
|
TEST_NAME ResourcePackParse)
|
||||||
|
|
||||||
ecm_add_test(minecraft/ParseUtils_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
|
ecm_add_test(minecraft/ParseUtils_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
|
||||||
TEST_NAME ParseUtils)
|
TEST_NAME ParseUtils)
|
||||||
|
|
||||||
|
@ -151,12 +151,12 @@ int ModFolderModel::columnCount(const QModelIndex &parent) const
|
|||||||
Task* ModFolderModel::createUpdateTask()
|
Task* ModFolderModel::createUpdateTask()
|
||||||
{
|
{
|
||||||
auto index_dir = indexDir();
|
auto index_dir = indexDir();
|
||||||
auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load, this);
|
auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load);
|
||||||
m_first_folder_load = false;
|
m_first_folder_load = false;
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
Task* ModFolderModel::createParseTask(Resource const& resource)
|
Task* ModFolderModel::createParseTask(Resource& resource)
|
||||||
{
|
{
|
||||||
return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo());
|
return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo());
|
||||||
}
|
}
|
||||||
@ -259,15 +259,6 @@ void ModFolderModel::onUpdateSucceeded()
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
applyUpdates(current_set, new_set, new_mods);
|
applyUpdates(current_set, new_set, new_mods);
|
||||||
|
|
||||||
m_current_update_task.reset();
|
|
||||||
|
|
||||||
if (m_scheduled_update) {
|
|
||||||
m_scheduled_update = false;
|
|
||||||
update();
|
|
||||||
} else {
|
|
||||||
emit updateFinished();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ModFolderModel::onParseSucceeded(int ticket, QString mod_id)
|
void ModFolderModel::onParseSucceeded(int ticket, QString mod_id)
|
||||||
|
@ -82,7 +82,7 @@ public:
|
|||||||
int columnCount(const QModelIndex &parent) const override;
|
int columnCount(const QModelIndex &parent) const override;
|
||||||
|
|
||||||
[[nodiscard]] Task* createUpdateTask() override;
|
[[nodiscard]] Task* createUpdateTask() override;
|
||||||
[[nodiscard]] Task* createParseTask(Resource const&) override;
|
[[nodiscard]] Task* createParseTask(Resource&) override;
|
||||||
|
|
||||||
bool installMod(QString file_path) { return ResourceFolderModel::installResource(file_path); }
|
bool installMod(QString file_path) { return ResourceFolderModel::installResource(file_path); }
|
||||||
bool uninstallMod(const QString& filename, bool preserve_metadata = false);
|
bool uninstallMod(const QString& filename, bool preserve_metadata = false);
|
||||||
|
@ -20,6 +20,7 @@ enum class SortType {
|
|||||||
DATE,
|
DATE,
|
||||||
VERSION,
|
VERSION,
|
||||||
ENABLED,
|
ENABLED,
|
||||||
|
PACK_FORMAT
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class EnableAction {
|
enum class EnableAction {
|
||||||
@ -80,6 +81,7 @@ class Resource : public QObject {
|
|||||||
|
|
||||||
[[nodiscard]] auto shouldResolve() const -> bool { return !m_is_resolving && !m_is_resolved; }
|
[[nodiscard]] auto shouldResolve() const -> bool { return !m_is_resolving && !m_is_resolved; }
|
||||||
[[nodiscard]] auto isResolving() const -> bool { return m_is_resolving; }
|
[[nodiscard]] auto isResolving() const -> bool { return m_is_resolving; }
|
||||||
|
[[nodiscard]] auto isResolved() const -> bool { return m_is_resolved; }
|
||||||
[[nodiscard]] auto resolutionTicket() const -> int { return m_resolution_ticket; }
|
[[nodiscard]] auto resolutionTicket() const -> int { return m_resolution_ticket; }
|
||||||
|
|
||||||
void setResolving(bool resolving, int resolutionTicket)
|
void setResolving(bool resolving, int resolutionTicket)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#include "ResourceFolderModel.h"
|
#include "ResourceFolderModel.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QMimeData>
|
#include <QMimeData>
|
||||||
#include <QThreadPool>
|
#include <QThreadPool>
|
||||||
@ -19,6 +20,12 @@ ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent) : QAbstractL
|
|||||||
connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged);
|
connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ResourceFolderModel::~ResourceFolderModel()
|
||||||
|
{
|
||||||
|
while (!QThreadPool::globalInstance()->waitForDone(100))
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
}
|
||||||
|
|
||||||
bool ResourceFolderModel::startWatching(const QStringList paths)
|
bool ResourceFolderModel::startWatching(const QStringList paths)
|
||||||
{
|
{
|
||||||
if (m_is_watching)
|
if (m_is_watching)
|
||||||
@ -229,9 +236,17 @@ bool ResourceFolderModel::update()
|
|||||||
connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded,
|
connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded,
|
||||||
Qt::ConnectionType::QueuedConnection);
|
Qt::ConnectionType::QueuedConnection);
|
||||||
connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection);
|
connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection);
|
||||||
|
connect(m_current_update_task.get(), &Task::finished, this, [=] {
|
||||||
|
m_current_update_task.reset();
|
||||||
|
if (m_scheduled_update) {
|
||||||
|
m_scheduled_update = false;
|
||||||
|
update();
|
||||||
|
} else {
|
||||||
|
emit updateFinished();
|
||||||
|
}
|
||||||
|
}, Qt::ConnectionType::QueuedConnection);
|
||||||
|
|
||||||
auto* thread_pool = QThreadPool::globalInstance();
|
QThreadPool::globalInstance()->start(m_current_update_task.get());
|
||||||
thread_pool->start(m_current_update_task.get());
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -246,10 +261,7 @@ void ResourceFolderModel::resolveResource(Resource::Ptr res)
|
|||||||
if (!task)
|
if (!task)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
m_ticket_mutex.lock();
|
int ticket = m_next_resolution_ticket.fetch_add(1);
|
||||||
int ticket = m_next_resolution_ticket;
|
|
||||||
m_next_resolution_ticket += 1;
|
|
||||||
m_ticket_mutex.unlock();
|
|
||||||
|
|
||||||
res->setResolving(true, ticket);
|
res->setResolving(true, ticket);
|
||||||
m_active_parse_tasks.insert(ticket, task);
|
m_active_parse_tasks.insert(ticket, task);
|
||||||
@ -261,8 +273,7 @@ void ResourceFolderModel::resolveResource(Resource::Ptr res)
|
|||||||
connect(
|
connect(
|
||||||
task, &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection);
|
task, &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection);
|
||||||
|
|
||||||
auto* thread_pool = QThreadPool::globalInstance();
|
QThreadPool::globalInstance()->start(task);
|
||||||
thread_pool->start(task);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ResourceFolderModel::onUpdateSucceeded()
|
void ResourceFolderModel::onUpdateSucceeded()
|
||||||
@ -283,15 +294,6 @@ void ResourceFolderModel::onUpdateSucceeded()
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
applyUpdates(current_set, new_set, new_resources);
|
applyUpdates(current_set, new_set, new_resources);
|
||||||
|
|
||||||
m_current_update_task.reset();
|
|
||||||
|
|
||||||
if (m_scheduled_update) {
|
|
||||||
m_scheduled_update = false;
|
|
||||||
update();
|
|
||||||
} else {
|
|
||||||
emit updateFinished();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id)
|
void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id)
|
||||||
|
@ -24,6 +24,7 @@ class ResourceFolderModel : public QAbstractListModel {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
ResourceFolderModel(QDir, QObject* parent = nullptr);
|
ResourceFolderModel(QDir, QObject* parent = nullptr);
|
||||||
|
~ResourceFolderModel() override;
|
||||||
|
|
||||||
/** Starts watching the paths for changes.
|
/** Starts watching the paths for changes.
|
||||||
*
|
*
|
||||||
@ -145,7 +146,7 @@ class ResourceFolderModel : public QAbstractListModel {
|
|||||||
* This task should load and parse all heavy info needed by a resource, such as parsing a manifest. It gets executed
|
* This task should load and parse all heavy info needed by a resource, such as parsing a manifest. It gets executed
|
||||||
* in the background, so it slowly updates the UI as tasks get done.
|
* in the background, so it slowly updates the UI as tasks get done.
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] virtual Task* createParseTask(Resource const&) { return nullptr; };
|
[[nodiscard]] virtual Task* createParseTask(Resource&) { return nullptr; };
|
||||||
|
|
||||||
/** Standard implementation of the model update logic.
|
/** Standard implementation of the model update logic.
|
||||||
*
|
*
|
||||||
@ -197,8 +198,7 @@ class ResourceFolderModel : public QAbstractListModel {
|
|||||||
QMap<QString, int> m_resources_index;
|
QMap<QString, int> m_resources_index;
|
||||||
|
|
||||||
QMap<int, Task::Ptr> m_active_parse_tasks;
|
QMap<int, Task::Ptr> m_active_parse_tasks;
|
||||||
int m_next_resolution_ticket = 0;
|
std::atomic<int> m_next_resolution_ticket = 0;
|
||||||
QMutex m_ticket_mutex;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */
|
/* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */
|
||||||
@ -257,8 +257,11 @@ void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>
|
|||||||
// If the resource is resolving, but something about it changed, we don't want to
|
// If the resource is resolving, but something about it changed, we don't want to
|
||||||
// continue the resolving.
|
// continue the resolving.
|
||||||
if (current_resource->isResolving()) {
|
if (current_resource->isResolving()) {
|
||||||
auto task = (*m_active_parse_tasks.find(current_resource->resolutionTicket())).get();
|
auto ticket = current_resource->resolutionTicket();
|
||||||
task->abort();
|
if (m_active_parse_tasks.contains(ticket)) {
|
||||||
|
auto task = (*m_active_parse_tasks.find(ticket)).get();
|
||||||
|
task->abort();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_resources[row].reset(new_resource);
|
m_resources[row].reset(new_resource);
|
||||||
@ -285,8 +288,11 @@ void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>
|
|||||||
Q_ASSERT(removed_set.contains(removed_it->get()->internal_id()));
|
Q_ASSERT(removed_set.contains(removed_it->get()->internal_id()));
|
||||||
|
|
||||||
if ((*removed_it)->isResolving()) {
|
if ((*removed_it)->isResolving()) {
|
||||||
auto task = (*m_active_parse_tasks.find((*removed_it)->resolutionTicket())).get();
|
auto ticket = (*removed_it)->resolutionTicket();
|
||||||
task->abort();
|
if (m_active_parse_tasks.contains(ticket)) {
|
||||||
|
auto task = (*m_active_parse_tasks.find(ticket)).get();
|
||||||
|
task->abort();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
beginRemoveRows(QModelIndex(), removed_index, removed_index);
|
beginRemoveRows(QModelIndex(), removed_index, removed_index);
|
||||||
|
@ -58,7 +58,7 @@
|
|||||||
QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); \
|
QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); \
|
||||||
expire_timer.stop(); \
|
expire_timer.stop(); \
|
||||||
\
|
\
|
||||||
disconnect(&model, nullptr, nullptr, nullptr);
|
disconnect(&model, nullptr, &loop, nullptr);
|
||||||
|
|
||||||
class ResourceFolderModelTest : public QObject
|
class ResourceFolderModelTest : public QObject
|
||||||
{
|
{
|
||||||
@ -146,14 +146,10 @@ slots:
|
|||||||
for (auto mod : model.allMods())
|
for (auto mod : model.allMods())
|
||||||
qDebug() << mod->name();
|
qDebug() << mod->name();
|
||||||
|
|
||||||
QCOMPARE(model.size(), 2);
|
// FIXME: It considers every file in the directory as a mod, but we should probably filter that out somehow.
|
||||||
|
QCOMPARE(model.size(), 4);
|
||||||
|
|
||||||
model.stopWatching();
|
model.stopWatching();
|
||||||
|
|
||||||
while (model.hasPendingParseTasks()) {
|
|
||||||
QTest::qSleep(20);
|
|
||||||
QCoreApplication::processEvents();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_removeResource()
|
void test_removeResource()
|
||||||
@ -206,11 +202,6 @@ slots:
|
|||||||
qDebug() << "Removed second mod.";
|
qDebug() << "Removed second mod.";
|
||||||
|
|
||||||
model.stopWatching();
|
model.stopWatching();
|
||||||
|
|
||||||
while (model.hasPendingParseTasks()) {
|
|
||||||
QTest::qSleep(20);
|
|
||||||
QCoreApplication::processEvents();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_enable_disable()
|
void test_enable_disable()
|
||||||
@ -262,11 +253,6 @@ slots:
|
|||||||
QVERIFY(!res_2.enable(initial_enabled_res_2 ? EnableAction::ENABLE : EnableAction::DISABLE));
|
QVERIFY(!res_2.enable(initial_enabled_res_2 ? EnableAction::ENABLE : EnableAction::DISABLE));
|
||||||
QVERIFY(res_2.enabled() == initial_enabled_res_2);
|
QVERIFY(res_2.enabled() == initial_enabled_res_2);
|
||||||
QVERIFY(res_2.internal_id() == id_2);
|
QVERIFY(res_2.internal_id() == id_2);
|
||||||
|
|
||||||
while (model.hasPendingParseTasks()) {
|
|
||||||
QTest::qSleep(20);
|
|
||||||
QCoreApplication::processEvents();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
116
launcher/minecraft/mod/ResourcePack.cpp
Normal file
116
launcher/minecraft/mod/ResourcePack.cpp
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
#include "ResourcePack.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
|
#include "Version.h"
|
||||||
|
|
||||||
|
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
|
||||||
|
|
||||||
|
// Values taken from:
|
||||||
|
// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
|
||||||
|
static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
|
||||||
|
{ 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } },
|
||||||
|
{ 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } },
|
||||||
|
{ 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } },
|
||||||
|
{ 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } },
|
||||||
|
{ 9, { Version("1.19"), Version("1.19.2") } },
|
||||||
|
};
|
||||||
|
|
||||||
|
void ResourcePack::setPackFormat(int new_format_id)
|
||||||
|
{
|
||||||
|
QMutexLocker locker(&m_data_lock);
|
||||||
|
|
||||||
|
if (!s_pack_format_versions.contains(new_format_id)) {
|
||||||
|
qWarning() << "Pack format '%1' is not a recognized resource pack id!";
|
||||||
|
}
|
||||||
|
|
||||||
|
m_pack_format = new_format_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResourcePack::setDescription(QString new_description)
|
||||||
|
{
|
||||||
|
QMutexLocker locker(&m_data_lock);
|
||||||
|
|
||||||
|
m_description = new_description;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResourcePack::setImage(QImage new_image)
|
||||||
|
{
|
||||||
|
QMutexLocker locker(&m_data_lock);
|
||||||
|
|
||||||
|
Q_ASSERT(!new_image.isNull());
|
||||||
|
|
||||||
|
if (m_pack_image_cache_key.key.isValid())
|
||||||
|
QPixmapCache::remove(m_pack_image_cache_key.key);
|
||||||
|
|
||||||
|
m_pack_image_cache_key.key = QPixmapCache::insert(QPixmap::fromImage(new_image));
|
||||||
|
m_pack_image_cache_key.was_ever_used = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap ResourcePack::image(QSize size)
|
||||||
|
{
|
||||||
|
QPixmap cached_image;
|
||||||
|
if (QPixmapCache::find(m_pack_image_cache_key.key, &cached_image)) {
|
||||||
|
if (size.isNull())
|
||||||
|
return cached_image;
|
||||||
|
return cached_image.scaled(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No valid image we can get
|
||||||
|
if (!m_pack_image_cache_key.was_ever_used)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
// Imaged got evicted from the cache. Re-process it and retry.
|
||||||
|
ResourcePackUtils::process(*this);
|
||||||
|
return image(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<Version, Version> ResourcePack::compatibleVersions() const
|
||||||
|
{
|
||||||
|
if (!s_pack_format_versions.contains(m_pack_format)) {
|
||||||
|
return { {}, {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
return s_pack_format_versions.constFind(m_pack_format).value();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<int, bool> ResourcePack::compare(const Resource& other, SortType type) const
|
||||||
|
{
|
||||||
|
auto const& cast_other = static_cast<ResourcePack const&>(other);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
default: {
|
||||||
|
auto res = Resource::compare(other, type);
|
||||||
|
if (res.first != 0)
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
case SortType::PACK_FORMAT: {
|
||||||
|
auto this_ver = packFormat();
|
||||||
|
auto other_ver = cast_other.packFormat();
|
||||||
|
|
||||||
|
if (this_ver > other_ver)
|
||||||
|
return { 1, type == SortType::PACK_FORMAT };
|
||||||
|
if (this_ver < other_ver)
|
||||||
|
return { -1, type == SortType::PACK_FORMAT };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { 0, false };
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ResourcePack::applyFilter(QRegularExpression filter) const
|
||||||
|
{
|
||||||
|
if (filter.match(description()).hasMatch())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (filter.match(QString::number(packFormat())).hasMatch())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (filter.match(compatibleVersions().first.toString()).hasMatch())
|
||||||
|
return true;
|
||||||
|
if (filter.match(compatibleVersions().second.toString()).hasMatch())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return Resource::applyFilter(filter);
|
||||||
|
}
|
@ -2,12 +2,68 @@
|
|||||||
|
|
||||||
#include "Resource.h"
|
#include "Resource.h"
|
||||||
|
|
||||||
|
#include <QImage>
|
||||||
|
#include <QMutex>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QPixmapCache>
|
||||||
|
|
||||||
|
class Version;
|
||||||
|
|
||||||
|
/* TODO:
|
||||||
|
*
|
||||||
|
* Store localized descriptions
|
||||||
|
* */
|
||||||
|
|
||||||
class ResourcePack : public Resource {
|
class ResourcePack : public Resource {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
using Ptr = shared_qobject_ptr<Resource>;
|
using Ptr = shared_qobject_ptr<Resource>;
|
||||||
|
|
||||||
ResourcePack(QObject* parent = nullptr) : Resource(parent) {}
|
ResourcePack(QObject* parent = nullptr) : Resource(parent) {}
|
||||||
ResourcePack(QFileInfo file_info) : Resource(file_info) {}
|
ResourcePack(QFileInfo file_info) : Resource(file_info) {}
|
||||||
|
|
||||||
|
/** Gets the numerical ID of the pack format. */
|
||||||
|
[[nodiscard]] int packFormat() const { return m_pack_format; }
|
||||||
|
/** Gets, respectively, the lower and upper versions supported by the set pack format. */
|
||||||
|
[[nodiscard]] std::pair<Version, Version> compatibleVersions() const;
|
||||||
|
|
||||||
|
/** Gets the description of the resource pack. */
|
||||||
|
[[nodiscard]] QString description() const { return m_description; }
|
||||||
|
|
||||||
|
/** Gets the image of the resource pack, converted to a QPixmap for drawing, and scaled to size. */
|
||||||
|
[[nodiscard]] QPixmap image(QSize size);
|
||||||
|
|
||||||
|
/** Thread-safe. */
|
||||||
|
void setPackFormat(int new_format_id);
|
||||||
|
|
||||||
|
/** Thread-safe. */
|
||||||
|
void setDescription(QString new_description);
|
||||||
|
|
||||||
|
/** Thread-safe. */
|
||||||
|
void setImage(QImage new_image);
|
||||||
|
|
||||||
|
[[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override;
|
||||||
|
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
mutable QMutex m_data_lock;
|
||||||
|
|
||||||
|
/* The 'version' of a resource pack, as defined in the pack.mcmeta file.
|
||||||
|
* See https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
|
||||||
|
*/
|
||||||
|
int m_pack_format = 0;
|
||||||
|
|
||||||
|
/** The resource pack's description, as defined in the pack.mcmeta file.
|
||||||
|
*/
|
||||||
|
QString m_description;
|
||||||
|
|
||||||
|
/** The resource pack's image file cache key, for access in the QPixmapCache global instance.
|
||||||
|
*
|
||||||
|
* The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true),
|
||||||
|
* so as to tell whether a cache entry is inexistent or if it was just evicted from the cache.
|
||||||
|
*/
|
||||||
|
struct {
|
||||||
|
QPixmapCache::Key key;
|
||||||
|
bool was_ever_used = false;
|
||||||
|
} m_pack_image_cache_key;
|
||||||
};
|
};
|
||||||
|
@ -1,38 +1,151 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
/*
|
/*
|
||||||
* PolyMC - Minecraft Launcher
|
* PolyMC - Minecraft Launcher
|
||||||
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
|
||||||
*
|
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
||||||
* This program is free software: you can redistribute it and/or modify
|
*
|
||||||
* it under the terms of the GNU General Public License as published by
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* the Free Software Foundation, version 3.
|
* it under the terms of the GNU General Public License as published by
|
||||||
*
|
* the Free Software Foundation, version 3.
|
||||||
* This program is distributed in the hope that it will be useful,
|
*
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* This program is distributed in the hope that it will be useful,
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* GNU General Public License for more details.
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
*
|
* GNU General Public License for more details.
|
||||||
* You should have received a copy of the GNU General Public License
|
*
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* You should have received a copy of the GNU General Public License
|
||||||
*
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
* This file incorporates work covered by the following copyright and
|
*
|
||||||
* permission notice:
|
* This file incorporates work covered by the following copyright and
|
||||||
*
|
* permission notice:
|
||||||
* Copyright 2013-2021 MultiMC Contributors
|
*
|
||||||
*
|
* Copyright 2013-2021 MultiMC Contributors
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
*
|
||||||
* you may not use this file except in compliance with the License.
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* You may obtain a copy of the License at
|
* you may not use this file except in compliance with the License.
|
||||||
*
|
* You may obtain a copy of the License at
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
||||||
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
*
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* See the License for the specific language governing permissions and
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
* limitations under the License.
|
* See the License for the specific language governing permissions and
|
||||||
*/
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
#include "ResourcePackFolderModel.h"
|
#include "ResourcePackFolderModel.h"
|
||||||
|
|
||||||
ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {}
|
#include "Version.h"
|
||||||
|
|
||||||
|
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
|
||||||
|
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
|
||||||
|
|
||||||
|
ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir) : ResourceFolderModel(QDir(dir))
|
||||||
|
{
|
||||||
|
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE };
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
|
||||||
|
{
|
||||||
|
if (!validateIndex(index))
|
||||||
|
return {};
|
||||||
|
|
||||||
|
int row = index.row();
|
||||||
|
int column = index.column();
|
||||||
|
|
||||||
|
switch (role) {
|
||||||
|
case Qt::DisplayRole:
|
||||||
|
switch (column) {
|
||||||
|
case NameColumn:
|
||||||
|
return m_resources[row]->name();
|
||||||
|
case PackFormatColumn: {
|
||||||
|
auto resource = at(row);
|
||||||
|
auto pack_format = resource->packFormat();
|
||||||
|
if (pack_format == 0)
|
||||||
|
return tr("Unrecognized");
|
||||||
|
|
||||||
|
auto version_bounds = resource->compatibleVersions();
|
||||||
|
if (version_bounds.first.toString().isEmpty())
|
||||||
|
return QString::number(pack_format);
|
||||||
|
|
||||||
|
return QString("%1 (%2 - %3)")
|
||||||
|
.arg(QString::number(pack_format), version_bounds.first.toString(), version_bounds.second.toString());
|
||||||
|
}
|
||||||
|
case DateColumn:
|
||||||
|
return m_resources[row]->dateTimeChanged();
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
case Qt::ToolTipRole: {
|
||||||
|
if (column == PackFormatColumn) {
|
||||||
|
//: The string being explained by this is in the format: ID (Lower version - Upper version)
|
||||||
|
return tr("The resource pack format ID, as well as the Minecraft versions it was designed for.");
|
||||||
|
}
|
||||||
|
return m_resources[row]->internal_id();
|
||||||
|
}
|
||||||
|
case Qt::CheckStateRole:
|
||||||
|
switch (column) {
|
||||||
|
case ActiveColumn:
|
||||||
|
return at(row)->enabled() ? Qt::Checked : Qt::Unchecked;
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||||
|
{
|
||||||
|
switch (role) {
|
||||||
|
case Qt::DisplayRole:
|
||||||
|
switch (section) {
|
||||||
|
case ActiveColumn:
|
||||||
|
return QString();
|
||||||
|
case NameColumn:
|
||||||
|
return tr("Name");
|
||||||
|
case PackFormatColumn:
|
||||||
|
return tr("Pack Format");
|
||||||
|
case DateColumn:
|
||||||
|
return tr("Last changed");
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
case Qt::ToolTipRole:
|
||||||
|
switch (section) {
|
||||||
|
case ActiveColumn:
|
||||||
|
return tr("Is the resource pack enabled? (Only valid for ZIPs)");
|
||||||
|
case NameColumn:
|
||||||
|
return tr("The name of the resource pack.");
|
||||||
|
case PackFormatColumn:
|
||||||
|
//: The string being explained by this is in the format: ID (Lower version - Upper version)
|
||||||
|
return tr("The resource pack format ID, as well as the Minecraft versions it was designed for.");
|
||||||
|
case DateColumn:
|
||||||
|
return tr("The date and time this resource pack was last changed (or added).");
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const
|
||||||
|
{
|
||||||
|
return NUM_COLUMNS;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task* ResourcePackFolderModel::createUpdateTask()
|
||||||
|
{
|
||||||
|
return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return new ResourcePack(entry); });
|
||||||
|
}
|
||||||
|
|
||||||
|
Task* ResourcePackFolderModel::createParseTask(Resource& resource)
|
||||||
|
{
|
||||||
|
return new LocalResourcePackParseTask(m_next_resolution_ticket, static_cast<ResourcePack&>(resource));
|
||||||
|
}
|
||||||
|
@ -8,7 +8,24 @@ class ResourcePackFolderModel : public ResourceFolderModel
|
|||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
|
enum Columns
|
||||||
|
{
|
||||||
|
ActiveColumn = 0,
|
||||||
|
NameColumn,
|
||||||
|
PackFormatColumn,
|
||||||
|
DateColumn,
|
||||||
|
NUM_COLUMNS
|
||||||
|
};
|
||||||
|
|
||||||
explicit ResourcePackFolderModel(const QString &dir);
|
explicit ResourcePackFolderModel(const QString &dir);
|
||||||
|
|
||||||
|
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
[[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||||
|
[[nodiscard]] int columnCount(const QModelIndex &parent) const override;
|
||||||
|
|
||||||
|
[[nodiscard]] Task* createUpdateTask() override;
|
||||||
|
[[nodiscard]] Task* createParseTask(Resource&) override;
|
||||||
|
|
||||||
RESOURCE_HELPERS(ResourcePack)
|
RESOURCE_HELPERS(ResourcePack)
|
||||||
};
|
};
|
||||||
|
73
launcher/minecraft/mod/ResourcePackParse_test.cpp
Normal file
73
launcher/minecraft/mod/ResourcePackParse_test.cpp
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
/*
|
||||||
|
* PolyMC - Minecraft Launcher
|
||||||
|
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <QTest>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#include "FileSystem.h"
|
||||||
|
|
||||||
|
#include "ResourcePack.h"
|
||||||
|
#include "tasks/LocalResourcePackParseTask.h"
|
||||||
|
|
||||||
|
class ResourcePackParseTest : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void test_parseZIP()
|
||||||
|
{
|
||||||
|
QString source = QFINDTESTDATA("testdata");
|
||||||
|
|
||||||
|
QString zip_rp = FS::PathCombine(source, "test_resource_pack_idk.zip");
|
||||||
|
ResourcePack pack { QFileInfo(zip_rp) };
|
||||||
|
|
||||||
|
ResourcePackUtils::processZIP(pack);
|
||||||
|
|
||||||
|
QVERIFY(pack.packFormat() == 3);
|
||||||
|
QVERIFY(pack.description() == "um dois, feijão com arroz, três quatro, feijão no prato, cinco seis, café inglês, sete oito, comer biscoito, nove dez comer pastéis!!");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_parseFolder()
|
||||||
|
{
|
||||||
|
QString source = QFINDTESTDATA("testdata");
|
||||||
|
|
||||||
|
QString folder_rp = FS::PathCombine(source, "test_folder");
|
||||||
|
ResourcePack pack { QFileInfo(folder_rp) };
|
||||||
|
|
||||||
|
ResourcePackUtils::processFolder(pack);
|
||||||
|
|
||||||
|
QVERIFY(pack.packFormat() == 1);
|
||||||
|
QVERIFY(pack.description() == "Some resource pack maybe");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_parseFolder2()
|
||||||
|
{
|
||||||
|
QString source = QFINDTESTDATA("testdata");
|
||||||
|
|
||||||
|
QString folder_rp = FS::PathCombine(source, "another_test_folder");
|
||||||
|
ResourcePack pack { QFileInfo(folder_rp) };
|
||||||
|
|
||||||
|
ResourcePackUtils::process(pack);
|
||||||
|
|
||||||
|
QVERIFY(pack.packFormat() == 6);
|
||||||
|
QVERIFY(pack.description() == "o quartel pegou fogo, policia deu sinal, acode acode acode a bandeira nacional");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QTEST_GUILESS_MAIN(ResourcePackParseTest)
|
||||||
|
|
||||||
|
#include "ResourcePackParse_test.moc"
|
@ -10,37 +10,46 @@
|
|||||||
|
|
||||||
#include "tasks/Task.h"
|
#include "tasks/Task.h"
|
||||||
|
|
||||||
/** Very simple task that just loads a folder's contents directly.
|
/** Very simple task that just loads a folder's contents directly.
|
||||||
*/
|
*/
|
||||||
class BasicFolderLoadTask : public Task
|
class BasicFolderLoadTask : public Task {
|
||||||
{
|
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
struct Result {
|
struct Result {
|
||||||
QMap<QString, Resource::Ptr> resources;
|
QMap<QString, Resource::Ptr> resources;
|
||||||
};
|
};
|
||||||
using ResultPtr = std::shared_ptr<Result>;
|
using ResultPtr = std::shared_ptr<Result>;
|
||||||
|
|
||||||
[[nodiscard]] ResultPtr result() const {
|
[[nodiscard]] ResultPtr result() const { return m_result; }
|
||||||
return m_result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result) {}
|
BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result)
|
||||||
|
{
|
||||||
|
m_create_func = [](QFileInfo const& entry) -> Resource* {
|
||||||
|
return new Resource(entry);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
BasicFolderLoadTask(QDir dir, std::function<Resource*(QFileInfo const&)> create_function)
|
||||||
|
: Task(nullptr, false), m_dir(dir), m_result(new Result), m_create_func(std::move(create_function))
|
||||||
|
{}
|
||||||
|
|
||||||
[[nodiscard]] bool canAbort() const override { return true; }
|
[[nodiscard]] bool canAbort() const override { return true; }
|
||||||
bool abort() override { m_aborted = true; return true; }
|
bool abort() override
|
||||||
|
{
|
||||||
|
m_aborted.store(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void executeTask() override
|
void executeTask() override
|
||||||
{
|
{
|
||||||
m_dir.refresh();
|
m_dir.refresh();
|
||||||
for (auto entry : m_dir.entryInfoList()) {
|
for (auto entry : m_dir.entryInfoList()) {
|
||||||
auto resource = new Resource(entry);
|
auto resource = m_create_func(entry);
|
||||||
m_result->resources.insert(resource->internal_id(), resource);
|
m_result->resources.insert(resource->internal_id(), resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_aborted)
|
if (m_aborted)
|
||||||
emitAborted();
|
emit finished();
|
||||||
else
|
else
|
||||||
emitSucceeded();
|
emitSucceeded();
|
||||||
}
|
}
|
||||||
@ -49,5 +58,7 @@ private:
|
|||||||
QDir m_dir;
|
QDir m_dir;
|
||||||
ResultPtr m_result;
|
ResultPtr m_result;
|
||||||
|
|
||||||
bool m_aborted = false;
|
std::atomic<bool> m_aborted = false;
|
||||||
|
|
||||||
|
std::function<Resource*(QFileInfo const&)> m_create_func;
|
||||||
};
|
};
|
||||||
|
@ -499,7 +499,7 @@ void LocalModParseTask::processAsLitemod()
|
|||||||
|
|
||||||
bool LocalModParseTask::abort()
|
bool LocalModParseTask::abort()
|
||||||
{
|
{
|
||||||
m_aborted = true;
|
m_aborted.store(true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -521,7 +521,7 @@ void LocalModParseTask::executeTask()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (m_aborted)
|
if (m_aborted)
|
||||||
emitAborted();
|
emit finished();
|
||||||
else
|
else
|
||||||
emitSucceeded();
|
emitSucceeded();
|
||||||
}
|
}
|
||||||
|
@ -39,5 +39,5 @@ private:
|
|||||||
QFileInfo m_modFile;
|
QFileInfo m_modFile;
|
||||||
ResultPtr m_result;
|
ResultPtr m_result;
|
||||||
|
|
||||||
bool m_aborted = false;
|
std::atomic<bool> m_aborted = false;
|
||||||
};
|
};
|
||||||
|
164
launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp
Normal file
164
launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
/*
|
||||||
|
* PolyMC - Minecraft Launcher
|
||||||
|
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "LocalResourcePackParseTask.h"
|
||||||
|
|
||||||
|
#include "FileSystem.h"
|
||||||
|
#include "Json.h"
|
||||||
|
|
||||||
|
#include <quazip/quazip.h>
|
||||||
|
#include <quazip/quazipfile.h>
|
||||||
|
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
|
||||||
|
namespace ResourcePackUtils {
|
||||||
|
|
||||||
|
bool process(ResourcePack& pack)
|
||||||
|
{
|
||||||
|
switch (pack.type()) {
|
||||||
|
case ResourceType::FOLDER:
|
||||||
|
ResourcePackUtils::processFolder(pack);
|
||||||
|
return true;
|
||||||
|
case ResourceType::ZIPFILE:
|
||||||
|
ResourcePackUtils::processZIP(pack);
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
qWarning() << "Invalid type for resource pack parse task!";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void processFolder(ResourcePack& pack)
|
||||||
|
{
|
||||||
|
Q_ASSERT(pack.type() == ResourceType::FOLDER);
|
||||||
|
|
||||||
|
QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta"));
|
||||||
|
if (mcmeta_file_info.isFile()) {
|
||||||
|
QFile mcmeta_file(mcmeta_file_info.filePath());
|
||||||
|
if (!mcmeta_file.open(QIODevice::ReadOnly))
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto data = mcmeta_file.readAll();
|
||||||
|
|
||||||
|
ResourcePackUtils::processMCMeta(pack, std::move(data));
|
||||||
|
|
||||||
|
mcmeta_file.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png"));
|
||||||
|
if (image_file_info.isFile()) {
|
||||||
|
QFile mcmeta_file(image_file_info.filePath());
|
||||||
|
if (!mcmeta_file.open(QIODevice::ReadOnly))
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto data = mcmeta_file.readAll();
|
||||||
|
|
||||||
|
ResourcePackUtils::processPackPNG(pack, std::move(data));
|
||||||
|
|
||||||
|
mcmeta_file.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void processZIP(ResourcePack& pack)
|
||||||
|
{
|
||||||
|
Q_ASSERT(pack.type() == ResourceType::ZIPFILE);
|
||||||
|
|
||||||
|
QuaZip zip(pack.fileinfo().filePath());
|
||||||
|
if (!zip.open(QuaZip::mdUnzip))
|
||||||
|
return;
|
||||||
|
|
||||||
|
QuaZipFile file(&zip);
|
||||||
|
|
||||||
|
if (zip.setCurrentFile("pack.mcmeta")) {
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
qCritical() << "Failed to open file in zip.";
|
||||||
|
zip.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto data = file.readAll();
|
||||||
|
|
||||||
|
ResourcePackUtils::processMCMeta(pack, std::move(data));
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zip.setCurrentFile("pack.png")) {
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
qCritical() << "Failed to open file in zip.";
|
||||||
|
zip.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto data = file.readAll();
|
||||||
|
|
||||||
|
ResourcePackUtils::processPackPNG(pack, std::move(data));
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
zip.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
|
||||||
|
void processMCMeta(ResourcePack& pack, QByteArray&& raw_data)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
auto json_doc = QJsonDocument::fromJson(raw_data);
|
||||||
|
auto pack_obj = Json::requireObject(json_doc.object(), "pack", {});
|
||||||
|
|
||||||
|
pack.setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0));
|
||||||
|
pack.setDescription(Json::ensureString(pack_obj, "description", ""));
|
||||||
|
} catch (Json::JsonException& e) {
|
||||||
|
qWarning() << "JsonException: " << e.what() << e.cause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void processPackPNG(ResourcePack& pack, QByteArray&& raw_data)
|
||||||
|
{
|
||||||
|
auto img = QImage::fromData(raw_data);
|
||||||
|
if (!img.isNull()) {
|
||||||
|
pack.setImage(img);
|
||||||
|
} else {
|
||||||
|
qWarning() << "Failed to parse pack.png.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace ResourcePackUtils
|
||||||
|
|
||||||
|
LocalResourcePackParseTask::LocalResourcePackParseTask(int token, ResourcePack& rp)
|
||||||
|
: Task(nullptr, false), m_token(token), m_resource_pack(rp)
|
||||||
|
{}
|
||||||
|
|
||||||
|
bool LocalResourcePackParseTask::abort()
|
||||||
|
{
|
||||||
|
m_aborted = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalResourcePackParseTask::executeTask()
|
||||||
|
{
|
||||||
|
Q_ASSERT(m_resource_pack.valid());
|
||||||
|
|
||||||
|
if (!ResourcePackUtils::process(m_resource_pack))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (m_aborted)
|
||||||
|
emitAborted();
|
||||||
|
else
|
||||||
|
emitSucceeded();
|
||||||
|
}
|
56
launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h
Normal file
56
launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
/*
|
||||||
|
* PolyMC - Minecraft Launcher
|
||||||
|
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include "minecraft/mod/ResourcePack.h"
|
||||||
|
|
||||||
|
#include "tasks/Task.h"
|
||||||
|
|
||||||
|
namespace ResourcePackUtils {
|
||||||
|
bool process(ResourcePack& pack);
|
||||||
|
|
||||||
|
void processZIP(ResourcePack& pack);
|
||||||
|
void processFolder(ResourcePack& pack);
|
||||||
|
|
||||||
|
void processMCMeta(ResourcePack& pack, QByteArray&& raw_data);
|
||||||
|
void processPackPNG(ResourcePack& pack, QByteArray&& raw_data);
|
||||||
|
} // namespace ResourcePackUtils
|
||||||
|
|
||||||
|
class LocalResourcePackParseTask : public Task {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
LocalResourcePackParseTask(int token, ResourcePack& rp);
|
||||||
|
|
||||||
|
[[nodiscard]] bool canAbort() const override { return true; }
|
||||||
|
bool abort() override;
|
||||||
|
|
||||||
|
void executeTask() override;
|
||||||
|
|
||||||
|
[[nodiscard]] int token() const { return m_token; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
int m_token;
|
||||||
|
|
||||||
|
ResourcePack& m_resource_pack;
|
||||||
|
|
||||||
|
bool m_aborted = false;
|
||||||
|
};
|
@ -38,8 +38,8 @@
|
|||||||
|
|
||||||
#include "minecraft/mod/MetadataHandler.h"
|
#include "minecraft/mod/MetadataHandler.h"
|
||||||
|
|
||||||
ModFolderLoadTask::ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan, QObject* parent)
|
ModFolderLoadTask::ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan)
|
||||||
: Task(parent, false), m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_clean_orphan(clean_orphan), m_result(new Result())
|
: Task(nullptr, false), m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_clean_orphan(clean_orphan), m_result(new Result())
|
||||||
{}
|
{}
|
||||||
|
|
||||||
void ModFolderLoadTask::executeTask()
|
void ModFolderLoadTask::executeTask()
|
||||||
@ -96,7 +96,10 @@ void ModFolderLoadTask::executeTask()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emitSucceeded();
|
if (m_aborted)
|
||||||
|
emit finished();
|
||||||
|
else
|
||||||
|
emitSucceeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ModFolderLoadTask::getFromMetadata()
|
void ModFolderLoadTask::getFromMetadata()
|
||||||
|
@ -57,7 +57,15 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan = false, QObject* parent = nullptr);
|
ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan = false);
|
||||||
|
|
||||||
|
[[nodiscard]] bool canAbort() const override { return true; }
|
||||||
|
bool abort() override
|
||||||
|
{
|
||||||
|
m_aborted.store(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void executeTask() override;
|
void executeTask() override;
|
||||||
|
|
||||||
@ -69,4 +77,6 @@ private:
|
|||||||
bool m_is_indexed;
|
bool m_is_indexed;
|
||||||
bool m_clean_orphan;
|
bool m_clean_orphan;
|
||||||
ResultPtr m_result;
|
ResultPtr m_result;
|
||||||
|
|
||||||
|
std::atomic<bool> m_aborted = false;
|
||||||
};
|
};
|
||||||
|
BIN
launcher/minecraft/mod/testdata/another_test_folder/pack.mcmeta
vendored
Normal file
BIN
launcher/minecraft/mod/testdata/another_test_folder/pack.mcmeta
vendored
Normal file
Binary file not shown.
BIN
launcher/minecraft/mod/testdata/test_resource_pack_idk.zip
vendored
Normal file
BIN
launcher/minecraft/mod/testdata/test_resource_pack_idk.zip
vendored
Normal file
Binary file not shown.
@ -135,6 +135,16 @@ bool ModFolderPage::onSelectionChanged(const QModelIndex& current, const QModelI
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ModFolderPage::removeItem()
|
||||||
|
{
|
||||||
|
|
||||||
|
if (!m_controlsEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
|
||||||
|
m_model->deleteMods(selection.indexes());
|
||||||
|
}
|
||||||
|
|
||||||
void ModFolderPage::installMods()
|
void ModFolderPage::installMods()
|
||||||
{
|
{
|
||||||
if (!m_controlsEnabled)
|
if (!m_controlsEnabled)
|
||||||
|
@ -59,6 +59,8 @@ class ModFolderPage : public ExternalResourcesPage {
|
|||||||
bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override;
|
bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
void removeItem() override;
|
||||||
|
|
||||||
void installMods();
|
void installMods();
|
||||||
void updateMods();
|
void updateMods();
|
||||||
|
|
||||||
|
@ -61,4 +61,15 @@ public:
|
|||||||
return !m_instance->traits().contains("no-texturepacks") &&
|
return !m_instance->traits().contains("no-texturepacks") &&
|
||||||
!m_instance->traits().contains("texturepacks");
|
!m_instance->traits().contains("texturepacks");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override
|
||||||
|
{
|
||||||
|
auto sourceCurrent = m_filterModel->mapToSource(current);
|
||||||
|
int row = sourceCurrent.row();
|
||||||
|
auto& rp = static_cast<ResourcePack&>(m_model->at(row));
|
||||||
|
ui->frame->updateWithResourcePack(rp);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,17 +1,37 @@
|
|||||||
/* Copyright 2013-2021 MultiMC Contributors
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
*
|
/*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* PolyMC - Minecraft Launcher
|
||||||
* you may not use this file except in compliance with the License.
|
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
|
||||||
* You may obtain a copy of the License at
|
*
|
||||||
*
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* it under the terms of the GNU General Public License as published by
|
||||||
*
|
* the Free Software Foundation, version 3.
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
*
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* This program is distributed in the hope that it will be useful,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* See the License for the specific language governing permissions and
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* limitations under the License.
|
* GNU General Public License for more details.
|
||||||
*/
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* This file incorporates work covered by the following copyright and
|
||||||
|
* permission notice:
|
||||||
|
*
|
||||||
|
* Copyright 2013-2021 MultiMC Contributors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
|
||||||
@ -67,17 +87,76 @@ void InfoFrame::updateWithMod(Mod const& m)
|
|||||||
{
|
{
|
||||||
setDescription(m.description());
|
setDescription(m.description());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
void InfoFrame::updateWithResource(const Resource& resource)
|
void InfoFrame::updateWithResource(const Resource& resource)
|
||||||
{
|
{
|
||||||
setName(resource.name());
|
setName(resource.name());
|
||||||
|
setImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.sportskeeda.com/minecraft-wiki/color-codes
|
||||||
|
static const QMap<QChar, QString> s_value_to_color = {
|
||||||
|
{'0', "#000000"}, {'1', "#0000AA"}, {'2', "#00AA00"}, {'3', "#00AAAA"}, {'4', "#AA0000"},
|
||||||
|
{'5', "#AA00AA"}, {'6', "#FFAA00"}, {'7', "#AAAAAA"}, {'8', "#555555"}, {'9', "#5555FF"},
|
||||||
|
{'a', "#55FF55"}, {'b', "#55FFFF"}, {'c', "#FF5555"}, {'d', "#FF55FF"}, {'e', "#FFFF55"},
|
||||||
|
{'f', "#FFFFFF"}
|
||||||
|
};
|
||||||
|
|
||||||
|
void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack)
|
||||||
|
{
|
||||||
|
setName(resource_pack.name());
|
||||||
|
|
||||||
|
// We have to manually set the colors for use.
|
||||||
|
//
|
||||||
|
// A color is set using §x, with x = a hex number from 0 to f.
|
||||||
|
//
|
||||||
|
// We traverse the description and, when one of those is found, we create
|
||||||
|
// a span element with that color set.
|
||||||
|
//
|
||||||
|
// TODO: Make the same logic for font formatting too.
|
||||||
|
// TODO: Wrap links inside <a> tags
|
||||||
|
|
||||||
|
auto description = resource_pack.description();
|
||||||
|
|
||||||
|
QString description_parsed("<html>");
|
||||||
|
bool in_div = false;
|
||||||
|
|
||||||
|
auto desc_it = description.constBegin();
|
||||||
|
while (desc_it != description.constEnd()) {
|
||||||
|
if (*desc_it == u'§') {
|
||||||
|
if (in_div)
|
||||||
|
description_parsed += "</span>";
|
||||||
|
|
||||||
|
auto const& num = *(++desc_it);
|
||||||
|
description_parsed += QString("<span style=\"color: %1;\">").arg(s_value_to_color.constFind(num).value());
|
||||||
|
|
||||||
|
in_div = true;
|
||||||
|
|
||||||
|
desc_it++;
|
||||||
|
}
|
||||||
|
|
||||||
|
description_parsed += *desc_it;
|
||||||
|
desc_it++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_div)
|
||||||
|
description_parsed += "</span>";
|
||||||
|
description_parsed += "</html>";
|
||||||
|
|
||||||
|
description_parsed.replace("\n", "<br>");
|
||||||
|
|
||||||
|
setDescription(description_parsed);
|
||||||
|
setImage(resource_pack.image({64, 64}));
|
||||||
}
|
}
|
||||||
|
|
||||||
void InfoFrame::clear()
|
void InfoFrame::clear()
|
||||||
{
|
{
|
||||||
setName();
|
setName();
|
||||||
setDescription();
|
setDescription();
|
||||||
|
setImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
void InfoFrame::updateHiddenState()
|
void InfoFrame::updateHiddenState()
|
||||||
@ -146,12 +225,22 @@ void InfoFrame::setDescription(QString text)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ui->descriptionLabel->setTextFormat(Qt::TextFormat::PlainText);
|
ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText);
|
||||||
labeltext.append(finaltext);
|
labeltext.append(finaltext);
|
||||||
}
|
}
|
||||||
ui->descriptionLabel->setText(labeltext);
|
ui->descriptionLabel->setText(labeltext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void InfoFrame::setImage(QPixmap img)
|
||||||
|
{
|
||||||
|
if (img.isNull()) {
|
||||||
|
ui->iconLabel->setHidden(true);
|
||||||
|
} else {
|
||||||
|
ui->iconLabel->setHidden(false);
|
||||||
|
ui->iconLabel->setPixmap(img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void InfoFrame::descriptionEllipsisHandler(QString link)
|
void InfoFrame::descriptionEllipsisHandler(QString link)
|
||||||
{
|
{
|
||||||
if(!m_current_box)
|
if(!m_current_box)
|
||||||
|
@ -34,11 +34,13 @@ class InfoFrame : public QFrame {
|
|||||||
|
|
||||||
void setName(QString text = {});
|
void setName(QString text = {});
|
||||||
void setDescription(QString text = {});
|
void setDescription(QString text = {});
|
||||||
|
void setImage(QPixmap img = {});
|
||||||
|
|
||||||
void clear();
|
void clear();
|
||||||
|
|
||||||
void updateWithMod(Mod const& m);
|
void updateWithMod(Mod const& m);
|
||||||
void updateWithResource(Resource const& resource);
|
void updateWithResource(Resource const& resource);
|
||||||
|
void updateWithResourcePack(ResourcePack& rp);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void descriptionEllipsisHandler(QString link);
|
void descriptionEllipsisHandler(QString link);
|
||||||
|
@ -22,10 +22,7 @@
|
|||||||
<height>120</height>
|
<height>120</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
<property name="spacing">
|
|
||||||
<number>6</number>
|
|
||||||
</property>
|
|
||||||
<property name="leftMargin">
|
<property name="leftMargin">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
@ -38,7 +35,7 @@
|
|||||||
<property name="bottomMargin">
|
<property name="bottomMargin">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item row="0" column="1">
|
||||||
<widget class="QLabel" name="nameLabel">
|
<widget class="QLabel" name="nameLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string notr="true"/>
|
<string notr="true"/>
|
||||||
@ -60,7 +57,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="1" column="1">
|
||||||
<widget class="QLabel" name="descriptionLabel">
|
<widget class="QLabel" name="descriptionLabel">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string notr="true"/>
|
<string notr="true"/>
|
||||||
@ -85,6 +82,31 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="0" column="0" rowspan="2">
|
||||||
|
<widget class="QLabel" name="iconLabel">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>64</width>
|
||||||
|
<height>64</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string notr="true"/>
|
||||||
|
</property>
|
||||||
|
<property name="scaledContents">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="margin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<resources/>
|
<resources/>
|
||||||
|
Loading…
Reference in New Issue
Block a user