Merge pull request #1052 from flowln/resource_model
This commit is contained in:
commit
f371ec210c
@ -318,10 +318,16 @@ set(MINECRAFT_SOURCES
|
|||||||
minecraft/mod/ModDetails.h
|
minecraft/mod/ModDetails.h
|
||||||
minecraft/mod/ModFolderModel.h
|
minecraft/mod/ModFolderModel.h
|
||||||
minecraft/mod/ModFolderModel.cpp
|
minecraft/mod/ModFolderModel.cpp
|
||||||
|
minecraft/mod/Resource.h
|
||||||
|
minecraft/mod/Resource.cpp
|
||||||
|
minecraft/mod/ResourceFolderModel.h
|
||||||
|
minecraft/mod/ResourceFolderModel.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
|
||||||
minecraft/mod/TexturePackFolderModel.cpp
|
minecraft/mod/TexturePackFolderModel.cpp
|
||||||
|
minecraft/mod/ShaderPackFolderModel.h
|
||||||
|
minecraft/mod/tasks/BasicFolderLoadTask.h
|
||||||
minecraft/mod/tasks/ModFolderLoadTask.h
|
minecraft/mod/tasks/ModFolderLoadTask.h
|
||||||
minecraft/mod/tasks/ModFolderLoadTask.cpp
|
minecraft/mod/tasks/ModFolderLoadTask.cpp
|
||||||
minecraft/mod/tasks/LocalModParseTask.h
|
minecraft/mod/tasks/LocalModParseTask.h
|
||||||
@ -375,8 +381,8 @@ ecm_add_test(minecraft/Library_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VER
|
|||||||
|
|
||||||
# FIXME: shares data with FileSystem test
|
# FIXME: shares data with FileSystem test
|
||||||
# TODO: needs testdata
|
# TODO: needs testdata
|
||||||
ecm_add_test(minecraft/mod/ModFolderModel_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 ModFolderModel)
|
TEST_NAME ResourceFolderModel)
|
||||||
|
|
||||||
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)
|
||||||
@ -881,8 +887,8 @@ SET(LAUNCHER_SOURCES
|
|||||||
ui/widgets/LineSeparator.h
|
ui/widgets/LineSeparator.h
|
||||||
ui/widgets/LogView.cpp
|
ui/widgets/LogView.cpp
|
||||||
ui/widgets/LogView.h
|
ui/widgets/LogView.h
|
||||||
ui/widgets/MCModInfoFrame.cpp
|
ui/widgets/InfoFrame.cpp
|
||||||
ui/widgets/MCModInfoFrame.h
|
ui/widgets/InfoFrame.h
|
||||||
ui/widgets/ModFilterWidget.cpp
|
ui/widgets/ModFilterWidget.cpp
|
||||||
ui/widgets/ModFilterWidget.h
|
ui/widgets/ModFilterWidget.h
|
||||||
ui/widgets/ModListView.cpp
|
ui/widgets/ModListView.cpp
|
||||||
@ -944,7 +950,7 @@ qt_wrap_ui(LAUNCHER_UI
|
|||||||
ui/pages/modplatform/technic/TechnicPage.ui
|
ui/pages/modplatform/technic/TechnicPage.ui
|
||||||
ui/widgets/InstanceCardWidget.ui
|
ui/widgets/InstanceCardWidget.ui
|
||||||
ui/widgets/CustomCommands.ui
|
ui/widgets/CustomCommands.ui
|
||||||
ui/widgets/MCModInfoFrame.ui
|
ui/widgets/InfoFrame.ui
|
||||||
ui/widgets/ModFilterWidget.ui
|
ui/widgets/ModFilterWidget.ui
|
||||||
ui/dialogs/CopyInstanceDialog.ui
|
ui/dialogs/CopyInstanceDialog.ui
|
||||||
ui/dialogs/ProfileSetupDialog.ui
|
ui/dialogs/ProfileSetupDialog.ui
|
||||||
|
@ -37,9 +37,9 @@ public:
|
|||||||
modsPage->setFilter("%1 (*.zip *.jar *.litemod)");
|
modsPage->setFilter("%1 (*.zip *.jar *.litemod)");
|
||||||
values.append(modsPage);
|
values.append(modsPage);
|
||||||
values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList()));
|
values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList()));
|
||||||
values.append(new ResourcePackPage(onesix.get()));
|
values.append(new ResourcePackPage(onesix.get(), onesix->resourcePackList()));
|
||||||
values.append(new TexturePackPage(onesix.get()));
|
values.append(new TexturePackPage(onesix.get(), onesix->texturePackList()));
|
||||||
values.append(new ShaderPackPage(onesix.get()));
|
values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList()));
|
||||||
values.append(new NotesPage(onesix.get()));
|
values.append(new NotesPage(onesix.get()));
|
||||||
values.append(new WorldListPage(onesix.get(), onesix->worldList()));
|
values.append(new WorldListPage(onesix.get(), onesix->worldList()));
|
||||||
values.append(new ServersPage(onesix));
|
values.append(new ServersPage(onesix));
|
||||||
|
@ -148,7 +148,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
|
|||||||
// do not merge disabled mods.
|
// do not merge disabled mods.
|
||||||
if (!mod->enabled())
|
if (!mod->enabled())
|
||||||
continue;
|
continue;
|
||||||
if (mod->type() == Mod::MOD_ZIPFILE)
|
if (mod->type() == ResourceType::ZIPFILE)
|
||||||
{
|
{
|
||||||
if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles))
|
if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles))
|
||||||
{
|
{
|
||||||
@ -158,7 +158,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (mod->type() == Mod::MOD_SINGLEFILE)
|
else if (mod->type() == ResourceType::SINGLEFILE)
|
||||||
{
|
{
|
||||||
// FIXME: buggy - does not work with addedFiles
|
// FIXME: buggy - does not work with addedFiles
|
||||||
auto filename = mod->fileinfo();
|
auto filename = mod->fileinfo();
|
||||||
@ -171,7 +171,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
|
|||||||
}
|
}
|
||||||
addedFiles.insert(filename.fileName());
|
addedFiles.insert(filename.fileName());
|
||||||
}
|
}
|
||||||
else if (mod->type() == Mod::MOD_FOLDER)
|
else if (mod->type() == ResourceType::FOLDER)
|
||||||
{
|
{
|
||||||
// untested, but seems to be unused / not possible to reach
|
// untested, but seems to be unused / not possible to reach
|
||||||
// FIXME: buggy - does not work with addedFiles
|
// FIXME: buggy - does not work with addedFiles
|
||||||
|
@ -76,6 +76,7 @@
|
|||||||
|
|
||||||
#include "mod/ModFolderModel.h"
|
#include "mod/ModFolderModel.h"
|
||||||
#include "mod/ResourcePackFolderModel.h"
|
#include "mod/ResourcePackFolderModel.h"
|
||||||
|
#include "mod/ShaderPackFolderModel.h"
|
||||||
#include "mod/TexturePackFolderModel.h"
|
#include "mod/TexturePackFolderModel.h"
|
||||||
|
|
||||||
#include "WorldList.h"
|
#include "WorldList.h"
|
||||||
@ -714,7 +715,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
|
|||||||
});
|
});
|
||||||
for(auto mod: modList)
|
for(auto mod: modList)
|
||||||
{
|
{
|
||||||
if(mod->type() == Mod::MOD_FOLDER)
|
if(mod->type() == ResourceType::FOLDER)
|
||||||
{
|
{
|
||||||
out << u8" [🖿] " + mod->fileinfo().completeBaseName() + " (folder)";
|
out << u8" [🖿] " + mod->fileinfo().completeBaseName() + " (folder)";
|
||||||
continue;
|
continue;
|
||||||
@ -1092,18 +1093,18 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const
|
|||||||
return m_core_mod_list;
|
return m_core_mod_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<ModFolderModel> MinecraftInstance::resourcePackList() const
|
std::shared_ptr<ResourcePackFolderModel> MinecraftInstance::resourcePackList() const
|
||||||
{
|
{
|
||||||
if (!m_resource_pack_list)
|
if (!m_resource_pack_list)
|
||||||
{
|
{
|
||||||
m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir()));
|
m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir()));
|
||||||
m_resource_pack_list->disableInteraction(isRunning());
|
m_resource_pack_list->enableInteraction(!isRunning());
|
||||||
connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ModFolderModel::disableInteraction);
|
connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ResourcePackFolderModel::disableInteraction);
|
||||||
}
|
}
|
||||||
return m_resource_pack_list;
|
return m_resource_pack_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const
|
std::shared_ptr<TexturePackFolderModel> MinecraftInstance::texturePackList() const
|
||||||
{
|
{
|
||||||
if (!m_texture_pack_list)
|
if (!m_texture_pack_list)
|
||||||
{
|
{
|
||||||
@ -1114,11 +1115,11 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const
|
|||||||
return m_texture_pack_list;
|
return m_texture_pack_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<ModFolderModel> MinecraftInstance::shaderPackList() const
|
std::shared_ptr<ShaderPackFolderModel> MinecraftInstance::shaderPackList() const
|
||||||
{
|
{
|
||||||
if (!m_shader_pack_list)
|
if (!m_shader_pack_list)
|
||||||
{
|
{
|
||||||
m_shader_pack_list.reset(new ResourcePackFolderModel(shaderPacksDir()));
|
m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir()));
|
||||||
m_shader_pack_list->disableInteraction(isRunning());
|
m_shader_pack_list->disableInteraction(isRunning());
|
||||||
connect(this, &BaseInstance::runningStatusChanged, m_shader_pack_list.get(), &ModFolderModel::disableInteraction);
|
connect(this, &BaseInstance::runningStatusChanged, m_shader_pack_list.get(), &ModFolderModel::disableInteraction);
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,10 @@
|
|||||||
#include "minecraft/launch/MinecraftServerTarget.h"
|
#include "minecraft/launch/MinecraftServerTarget.h"
|
||||||
|
|
||||||
class ModFolderModel;
|
class ModFolderModel;
|
||||||
|
class ResourceFolderModel;
|
||||||
|
class ResourcePackFolderModel;
|
||||||
|
class ShaderPackFolderModel;
|
||||||
|
class TexturePackFolderModel;
|
||||||
class WorldList;
|
class WorldList;
|
||||||
class GameOptions;
|
class GameOptions;
|
||||||
class LaunchStep;
|
class LaunchStep;
|
||||||
@ -72,9 +76,9 @@ public:
|
|||||||
////// Mod Lists //////
|
////// Mod Lists //////
|
||||||
std::shared_ptr<ModFolderModel> loaderModList() const;
|
std::shared_ptr<ModFolderModel> loaderModList() const;
|
||||||
std::shared_ptr<ModFolderModel> coreModList() const;
|
std::shared_ptr<ModFolderModel> coreModList() const;
|
||||||
std::shared_ptr<ModFolderModel> resourcePackList() const;
|
std::shared_ptr<ResourcePackFolderModel> resourcePackList() const;
|
||||||
std::shared_ptr<ModFolderModel> texturePackList() const;
|
std::shared_ptr<TexturePackFolderModel> texturePackList() const;
|
||||||
std::shared_ptr<ModFolderModel> shaderPackList() const;
|
std::shared_ptr<ShaderPackFolderModel> shaderPackList() const;
|
||||||
std::shared_ptr<WorldList> worldList() const;
|
std::shared_ptr<WorldList> worldList() const;
|
||||||
std::shared_ptr<GameOptions> gameOptionsModel() const;
|
std::shared_ptr<GameOptions> gameOptionsModel() const;
|
||||||
|
|
||||||
@ -125,9 +129,9 @@ protected: // data
|
|||||||
std::shared_ptr<PackProfile> m_components;
|
std::shared_ptr<PackProfile> m_components;
|
||||||
mutable std::shared_ptr<ModFolderModel> m_loader_mod_list;
|
mutable std::shared_ptr<ModFolderModel> m_loader_mod_list;
|
||||||
mutable std::shared_ptr<ModFolderModel> m_core_mod_list;
|
mutable std::shared_ptr<ModFolderModel> m_core_mod_list;
|
||||||
mutable std::shared_ptr<ModFolderModel> m_resource_pack_list;
|
mutable std::shared_ptr<ResourcePackFolderModel> m_resource_pack_list;
|
||||||
mutable std::shared_ptr<ModFolderModel> m_shader_pack_list;
|
mutable std::shared_ptr<ShaderPackFolderModel> m_shader_pack_list;
|
||||||
mutable std::shared_ptr<ModFolderModel> m_texture_pack_list;
|
mutable std::shared_ptr<TexturePackFolderModel> m_texture_pack_list;
|
||||||
mutable std::shared_ptr<WorldList> m_world_list;
|
mutable std::shared_ptr<WorldList> m_world_list;
|
||||||
mutable std::shared_ptr<GameOptions> m_game_options;
|
mutable std::shared_ptr<GameOptions> m_game_options;
|
||||||
};
|
};
|
||||||
|
@ -36,130 +36,77 @@
|
|||||||
|
|
||||||
#include "Mod.h"
|
#include "Mod.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
#include <FileSystem.h>
|
|
||||||
#include <QDebug>
|
|
||||||
|
|
||||||
#include "Application.h"
|
|
||||||
#include "MetadataHandler.h"
|
#include "MetadataHandler.h"
|
||||||
|
#include "Version.h"
|
||||||
|
|
||||||
namespace {
|
Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details()
|
||||||
|
|
||||||
ModDetails invalidDetails;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Mod::Mod(const QFileInfo& file)
|
|
||||||
{
|
{
|
||||||
repath(file);
|
m_enabled = (file.suffix() != "disabled");
|
||||||
m_changedDateTime = file.lastModified();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata)
|
Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata)
|
||||||
: m_file(mods_dir.absoluteFilePath(metadata.filename))
|
: Mod(mods_dir.absoluteFilePath(metadata.filename))
|
||||||
, m_internal_id(metadata.filename)
|
|
||||||
, m_name(metadata.name)
|
|
||||||
{
|
{
|
||||||
if (m_file.isDir()) {
|
m_name = metadata.name;
|
||||||
m_type = MOD_FOLDER;
|
m_local_details.metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
|
||||||
} else {
|
|
||||||
if (metadata.filename.endsWith(".zip") || metadata.filename.endsWith(".jar"))
|
|
||||||
m_type = MOD_ZIPFILE;
|
|
||||||
else if (metadata.filename.endsWith(".litemod"))
|
|
||||||
m_type = MOD_LITEMOD;
|
|
||||||
else
|
|
||||||
m_type = MOD_SINGLEFILE;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_enabled = true;
|
|
||||||
m_changedDateTime = m_file.lastModified();
|
|
||||||
|
|
||||||
m_temp_metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Mod::repath(const QFileInfo& file)
|
|
||||||
{
|
|
||||||
m_file = file;
|
|
||||||
QString name_base = file.fileName();
|
|
||||||
|
|
||||||
m_type = Mod::MOD_UNKNOWN;
|
|
||||||
|
|
||||||
m_internal_id = name_base;
|
|
||||||
|
|
||||||
if (m_file.isDir()) {
|
|
||||||
m_type = MOD_FOLDER;
|
|
||||||
m_name = name_base;
|
|
||||||
} else if (m_file.isFile()) {
|
|
||||||
if (name_base.endsWith(".disabled")) {
|
|
||||||
m_enabled = false;
|
|
||||||
name_base.chop(9);
|
|
||||||
} else {
|
|
||||||
m_enabled = true;
|
|
||||||
}
|
|
||||||
if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) {
|
|
||||||
m_type = MOD_ZIPFILE;
|
|
||||||
name_base.chop(4);
|
|
||||||
} else if (name_base.endsWith(".litemod")) {
|
|
||||||
m_type = MOD_LITEMOD;
|
|
||||||
name_base.chop(8);
|
|
||||||
} else {
|
|
||||||
m_type = MOD_SINGLEFILE;
|
|
||||||
}
|
|
||||||
m_name = name_base;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto Mod::enable(bool value) -> bool
|
|
||||||
{
|
|
||||||
if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (m_enabled == value)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
QString path = m_file.absoluteFilePath();
|
|
||||||
QFile file(path);
|
|
||||||
if (value) {
|
|
||||||
if (!path.endsWith(".disabled"))
|
|
||||||
return false;
|
|
||||||
path.chop(9);
|
|
||||||
|
|
||||||
if (!file.rename(path))
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
path += ".disabled";
|
|
||||||
|
|
||||||
if (!file.rename(path))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status() == ModStatus::NoMetadata)
|
|
||||||
repath(QFileInfo(path));
|
|
||||||
|
|
||||||
m_enabled = value;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mod::setStatus(ModStatus status)
|
void Mod::setStatus(ModStatus status)
|
||||||
{
|
{
|
||||||
if (m_localDetails) {
|
m_local_details.status = status;
|
||||||
m_localDetails->status = status;
|
|
||||||
} else {
|
|
||||||
m_temp_status = status;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
void Mod::setMetadata(const Metadata::ModStruct& metadata)
|
void Mod::setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata)
|
||||||
{
|
{
|
||||||
if (status() == ModStatus::NoMetadata)
|
if (status() == ModStatus::NoMetadata)
|
||||||
setStatus(ModStatus::Installed);
|
setStatus(ModStatus::Installed);
|
||||||
|
|
||||||
if (m_localDetails) {
|
m_local_details.metadata = metadata;
|
||||||
m_localDetails->metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
|
}
|
||||||
} else {
|
|
||||||
m_temp_metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
|
std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
|
||||||
|
{
|
||||||
|
auto cast_other = dynamic_cast<Mod const*>(&other);
|
||||||
|
if (!cast_other)
|
||||||
|
return Resource::compare(other, type);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
default:
|
||||||
|
case SortType::ENABLED:
|
||||||
|
case SortType::NAME:
|
||||||
|
case SortType::DATE: {
|
||||||
|
auto res = Resource::compare(other, type);
|
||||||
|
if (res.first != 0)
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
case SortType::VERSION: {
|
||||||
|
auto this_ver = Version(version());
|
||||||
|
auto other_ver = Version(cast_other->version());
|
||||||
|
if (this_ver > other_ver)
|
||||||
|
return { 1, type == SortType::VERSION };
|
||||||
|
if (this_ver < other_ver)
|
||||||
|
return { -1, type == SortType::VERSION };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { 0, false };
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Mod::applyFilter(QRegularExpression filter) const
|
||||||
|
{
|
||||||
|
if (filter.match(description()).hasMatch())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
for (auto& author : authors()) {
|
||||||
|
if (filter.match(author).hasMatch()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Resource::applyFilter(filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
|
auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
|
||||||
@ -175,13 +122,12 @@ auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_type = MOD_UNKNOWN;
|
return Resource::destroy();
|
||||||
return FS::deletePath(m_file.filePath());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Mod::details() const -> const ModDetails&
|
auto Mod::details() const -> const ModDetails&
|
||||||
{
|
{
|
||||||
return m_localDetails ? *m_localDetails : invalidDetails;
|
return m_local_details;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Mod::name() const -> QString
|
auto Mod::name() const -> QString
|
||||||
@ -218,35 +164,29 @@ auto Mod::authors() const -> QStringList
|
|||||||
|
|
||||||
auto Mod::status() const -> ModStatus
|
auto Mod::status() const -> ModStatus
|
||||||
{
|
{
|
||||||
if (!m_localDetails)
|
|
||||||
return m_temp_status;
|
|
||||||
return details().status;
|
return details().status;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Mod::metadata() -> std::shared_ptr<Metadata::ModStruct>
|
auto Mod::metadata() -> std::shared_ptr<Metadata::ModStruct>
|
||||||
{
|
{
|
||||||
if (m_localDetails)
|
return m_local_details.metadata;
|
||||||
return m_localDetails->metadata;
|
|
||||||
return m_temp_metadata;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Mod::metadata() const -> const std::shared_ptr<Metadata::ModStruct>
|
auto Mod::metadata() const -> const std::shared_ptr<Metadata::ModStruct>
|
||||||
{
|
{
|
||||||
if (m_localDetails)
|
return m_local_details.metadata;
|
||||||
return m_localDetails->metadata;
|
|
||||||
return m_temp_metadata;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mod::finishResolvingWithDetails(std::shared_ptr<ModDetails> details)
|
void Mod::finishResolvingWithDetails(ModDetails&& details)
|
||||||
{
|
{
|
||||||
m_resolving = false;
|
m_is_resolving = false;
|
||||||
m_resolved = true;
|
m_is_resolved = true;
|
||||||
m_localDetails = details;
|
|
||||||
|
|
||||||
setStatus(m_temp_status);
|
std::shared_ptr<Metadata::ModStruct> metadata = details.metadata;
|
||||||
|
if (details.status == ModStatus::Unknown)
|
||||||
|
details.status = m_local_details.status;
|
||||||
|
|
||||||
if (m_localDetails && m_temp_metadata && m_temp_metadata->isValid()) {
|
m_local_details = std::move(details);
|
||||||
setMetadata(*m_temp_metadata);
|
if (metadata)
|
||||||
m_temp_metadata.reset();
|
setMetadata(std::move(metadata));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -39,38 +39,23 @@
|
|||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
|
|
||||||
#include "QObjectPtr.h"
|
#include "Resource.h"
|
||||||
#include "ModDetails.h"
|
#include "ModDetails.h"
|
||||||
|
|
||||||
class Mod : public QObject
|
class Mod : public Resource
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
enum ModType
|
|
||||||
{
|
|
||||||
MOD_UNKNOWN, //!< Indicates an unspecified mod type.
|
|
||||||
MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class files.
|
|
||||||
MOD_SINGLEFILE, //!< The mod is a single file (not a zip file).
|
|
||||||
MOD_FOLDER, //!< The mod is in a folder on the filesystem.
|
|
||||||
MOD_LITEMOD, //!< The mod is a litemod
|
|
||||||
};
|
|
||||||
|
|
||||||
using Ptr = shared_qobject_ptr<Mod>;
|
using Ptr = shared_qobject_ptr<Mod>;
|
||||||
|
using WeakPtr = QPointer<Mod>;
|
||||||
|
|
||||||
Mod() = default;
|
Mod() = default;
|
||||||
Mod(const QFileInfo &file);
|
Mod(const QFileInfo &file);
|
||||||
explicit Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata);
|
Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata);
|
||||||
|
Mod(QString file_path) : Mod(QFileInfo(file_path)) {}
|
||||||
auto fileinfo() const -> QFileInfo { return m_file; }
|
|
||||||
auto dateTimeChanged() const -> QDateTime { return m_changedDateTime; }
|
|
||||||
auto internal_id() const -> QString { return m_internal_id; }
|
|
||||||
auto type() const -> ModType { return m_type; }
|
|
||||||
auto enabled() const -> bool { return m_enabled; }
|
|
||||||
|
|
||||||
auto valid() const -> bool { return m_type != MOD_UNKNOWN; }
|
|
||||||
|
|
||||||
auto details() const -> const ModDetails&;
|
auto details() const -> const ModDetails&;
|
||||||
auto name() const -> QString;
|
auto name() const -> QString override;
|
||||||
auto version() const -> QString;
|
auto version() const -> QString;
|
||||||
auto homeurl() const -> QString;
|
auto homeurl() const -> QString;
|
||||||
auto description() const -> QString;
|
auto description() const -> QString;
|
||||||
@ -81,46 +66,17 @@ public:
|
|||||||
auto metadata() const -> const std::shared_ptr<Metadata::ModStruct>;
|
auto metadata() const -> const std::shared_ptr<Metadata::ModStruct>;
|
||||||
|
|
||||||
void setStatus(ModStatus status);
|
void setStatus(ModStatus status);
|
||||||
void setMetadata(const Metadata::ModStruct& metadata);
|
void setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata);
|
||||||
|
void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared<Metadata::ModStruct>(metadata)); }
|
||||||
|
|
||||||
auto enable(bool value) -> bool;
|
[[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override;
|
||||||
|
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
|
||||||
|
|
||||||
// delete all the files of this mod
|
// Delete all the files of this mod
|
||||||
auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool;
|
auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool;
|
||||||
|
|
||||||
// change the mod's filesystem path (used by mod lists for *MAGIC* purposes)
|
void finishResolvingWithDetails(ModDetails&& details);
|
||||||
void repath(const QFileInfo &file);
|
|
||||||
|
|
||||||
auto shouldResolve() const -> bool { return !m_resolving && !m_resolved; }
|
|
||||||
auto isResolving() const -> bool { return m_resolving; }
|
|
||||||
auto resolutionTicket() const -> int { return m_resolutionTicket; }
|
|
||||||
|
|
||||||
void setResolving(bool resolving, int resolutionTicket) {
|
|
||||||
m_resolving = resolving;
|
|
||||||
m_resolutionTicket = resolutionTicket;
|
|
||||||
}
|
|
||||||
void finishResolvingWithDetails(std::shared_ptr<ModDetails> details);
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QFileInfo m_file;
|
ModDetails m_local_details;
|
||||||
QDateTime m_changedDateTime;
|
|
||||||
|
|
||||||
QString m_internal_id;
|
|
||||||
/* Name as reported via the file name */
|
|
||||||
QString m_name;
|
|
||||||
ModType m_type = MOD_UNKNOWN;
|
|
||||||
|
|
||||||
/* If the mod has metadata, this will be filled in the constructor, and passed to
|
|
||||||
* the ModDetails when calling finishResolvingWithDetails */
|
|
||||||
std::shared_ptr<Metadata::ModStruct> m_temp_metadata;
|
|
||||||
|
|
||||||
/* Set the mod status while it doesn't have local details just yet */
|
|
||||||
ModStatus m_temp_status = ModStatus::NoMetadata;
|
|
||||||
|
|
||||||
std::shared_ptr<ModDetails> m_localDetails;
|
|
||||||
|
|
||||||
bool m_enabled = true;
|
|
||||||
bool m_resolving = false;
|
|
||||||
bool m_resolved = false;
|
|
||||||
int m_resolutionTicket = 0;
|
|
||||||
};
|
};
|
||||||
|
@ -46,34 +46,77 @@ enum class ModStatus {
|
|||||||
Installed, // Both JAR and Metadata are present
|
Installed, // Both JAR and Metadata are present
|
||||||
NotInstalled, // Only the Metadata is present
|
NotInstalled, // Only the Metadata is present
|
||||||
NoMetadata, // Only the JAR is present
|
NoMetadata, // Only the JAR is present
|
||||||
|
Unknown, // Default status
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ModDetails
|
struct ModDetails
|
||||||
{
|
{
|
||||||
/* Mod ID as defined in the ModLoader-specific metadata */
|
/* Mod ID as defined in the ModLoader-specific metadata */
|
||||||
QString mod_id;
|
QString mod_id = {};
|
||||||
|
|
||||||
/* Human-readable name */
|
/* Human-readable name */
|
||||||
QString name;
|
QString name = {};
|
||||||
|
|
||||||
/* Human-readable mod version */
|
/* Human-readable mod version */
|
||||||
QString version;
|
QString version = {};
|
||||||
|
|
||||||
/* Human-readable minecraft version */
|
/* Human-readable minecraft version */
|
||||||
QString mcversion;
|
QString mcversion = {};
|
||||||
|
|
||||||
/* URL for mod's home page */
|
/* URL for mod's home page */
|
||||||
QString homeurl;
|
QString homeurl = {};
|
||||||
|
|
||||||
/* Human-readable description */
|
/* Human-readable description */
|
||||||
QString description;
|
QString description = {};
|
||||||
|
|
||||||
/* List of the author's names */
|
/* List of the author's names */
|
||||||
QStringList authors;
|
QStringList authors = {};
|
||||||
|
|
||||||
/* Installation status of the mod */
|
/* Installation status of the mod */
|
||||||
ModStatus status;
|
ModStatus status = ModStatus::Unknown;
|
||||||
|
|
||||||
/* Metadata information, if any */
|
/* Metadata information, if any */
|
||||||
std::shared_ptr<Metadata::ModStruct> metadata;
|
std::shared_ptr<Metadata::ModStruct> metadata = nullptr;
|
||||||
|
|
||||||
|
ModDetails() = default;
|
||||||
|
|
||||||
|
/** Metadata should be handled manually to properly set the mod status. */
|
||||||
|
ModDetails(ModDetails& other)
|
||||||
|
: mod_id(other.mod_id)
|
||||||
|
, name(other.name)
|
||||||
|
, version(other.version)
|
||||||
|
, mcversion(other.mcversion)
|
||||||
|
, homeurl(other.homeurl)
|
||||||
|
, description(other.description)
|
||||||
|
, authors(other.authors)
|
||||||
|
, status(other.status)
|
||||||
|
{}
|
||||||
|
|
||||||
|
ModDetails& operator=(ModDetails& other)
|
||||||
|
{
|
||||||
|
this->mod_id = other.mod_id;
|
||||||
|
this->name = other.name;
|
||||||
|
this->version = other.version;
|
||||||
|
this->mcversion = other.mcversion;
|
||||||
|
this->homeurl = other.homeurl;
|
||||||
|
this->description = other.description;
|
||||||
|
this->authors = other.authors;
|
||||||
|
this->status = other.status;
|
||||||
|
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModDetails& operator=(ModDetails&& other)
|
||||||
|
{
|
||||||
|
this->mod_id = other.mod_id;
|
||||||
|
this->name = other.name;
|
||||||
|
this->version = other.version;
|
||||||
|
this->mcversion = other.mcversion;
|
||||||
|
this->homeurl = other.homeurl;
|
||||||
|
this->description = other.description;
|
||||||
|
this->authors = other.authors;
|
||||||
|
this->status = other.status;
|
||||||
|
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -49,432 +49,53 @@
|
|||||||
#include "minecraft/mod/tasks/LocalModParseTask.h"
|
#include "minecraft/mod/tasks/LocalModParseTask.h"
|
||||||
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
|
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
|
||||||
|
|
||||||
ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : QAbstractListModel(), m_dir(dir), m_is_indexed(is_indexed)
|
ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : ResourceFolderModel(QDir(dir)), m_is_indexed(is_indexed)
|
||||||
{
|
{
|
||||||
FS::ensureFolderPathExists(m_dir.absolutePath());
|
FS::ensureFolderPathExists(m_dir.absolutePath());
|
||||||
m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
|
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE };
|
||||||
m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
|
|
||||||
m_watcher = new QFileSystemWatcher(this);
|
|
||||||
connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString)));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModFolderModel::startWatching()
|
|
||||||
{
|
|
||||||
if(is_watching)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Remove orphaned metadata next time
|
|
||||||
m_first_folder_load = true;
|
|
||||||
|
|
||||||
update();
|
|
||||||
|
|
||||||
// Watch the mods folder
|
|
||||||
is_watching = m_watcher->addPath(m_dir.absolutePath());
|
|
||||||
if (is_watching) {
|
|
||||||
qDebug() << "Started watching " << m_dir.absolutePath();
|
|
||||||
} else {
|
|
||||||
qDebug() << "Failed to start watching " << m_dir.absolutePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watch the mods index folder
|
|
||||||
is_watching = m_watcher->addPath(indexDir().absolutePath());
|
|
||||||
if (is_watching) {
|
|
||||||
qDebug() << "Started watching " << indexDir().absolutePath();
|
|
||||||
} else {
|
|
||||||
qDebug() << "Failed to start watching " << indexDir().absolutePath();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModFolderModel::stopWatching()
|
|
||||||
{
|
|
||||||
if(!is_watching)
|
|
||||||
return;
|
|
||||||
|
|
||||||
is_watching = !m_watcher->removePath(m_dir.absolutePath());
|
|
||||||
if (!is_watching) {
|
|
||||||
qDebug() << "Stopped watching " << m_dir.absolutePath();
|
|
||||||
} else {
|
|
||||||
qDebug() << "Failed to stop watching " << m_dir.absolutePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
is_watching = !m_watcher->removePath(indexDir().absolutePath());
|
|
||||||
if (!is_watching) {
|
|
||||||
qDebug() << "Stopped watching " << indexDir().absolutePath();
|
|
||||||
} else {
|
|
||||||
qDebug() << "Failed to stop watching " << indexDir().absolutePath();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ModFolderModel::update()
|
|
||||||
{
|
|
||||||
if (!isValid()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if(m_update) {
|
|
||||||
scheduled_update = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto index_dir = indexDir();
|
|
||||||
auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load);
|
|
||||||
m_first_folder_load = false;
|
|
||||||
|
|
||||||
m_update = task->result();
|
|
||||||
|
|
||||||
QThreadPool *threadPool = QThreadPool::globalInstance();
|
|
||||||
connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate);
|
|
||||||
|
|
||||||
threadPool->start(task);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModFolderModel::finishUpdate()
|
|
||||||
{
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
|
||||||
auto currentList = modsIndex.keys();
|
|
||||||
QSet<QString> currentSet(currentList.begin(), currentList.end());
|
|
||||||
auto & newMods = m_update->mods;
|
|
||||||
auto newList = newMods.keys();
|
|
||||||
QSet<QString> newSet(newList.begin(), newList.end());
|
|
||||||
#else
|
|
||||||
QSet<QString> currentSet = modsIndex.keys().toSet();
|
|
||||||
auto& newMods = m_update->mods;
|
|
||||||
QSet<QString> newSet = newMods.keys().toSet();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// see if the kept mods changed in some way
|
|
||||||
{
|
|
||||||
QSet<QString> kept = currentSet;
|
|
||||||
kept.intersect(newSet);
|
|
||||||
for(auto& keptMod : kept) {
|
|
||||||
auto newMod = newMods[keptMod];
|
|
||||||
auto row = modsIndex[keptMod];
|
|
||||||
auto currentMod = mods[row];
|
|
||||||
if(newMod->dateTimeChanged() == currentMod->dateTimeChanged()) {
|
|
||||||
// no significant change, ignore...
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
auto oldMod = mods[row];
|
|
||||||
if(oldMod->isResolving()) {
|
|
||||||
activeTickets.remove(oldMod->resolutionTicket());
|
|
||||||
}
|
|
||||||
|
|
||||||
mods[row] = newMod;
|
|
||||||
resolveMod(mods[row]);
|
|
||||||
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove mods no longer present
|
|
||||||
{
|
|
||||||
QSet<QString> removed = currentSet;
|
|
||||||
QList<int> removedRows;
|
|
||||||
removed.subtract(newSet);
|
|
||||||
for(auto & removedMod: removed) {
|
|
||||||
removedRows.append(modsIndex[removedMod]);
|
|
||||||
}
|
|
||||||
std::sort(removedRows.begin(), removedRows.end(), std::greater<int>());
|
|
||||||
for(auto iter = removedRows.begin(); iter != removedRows.end(); iter++) {
|
|
||||||
int removedIndex = *iter;
|
|
||||||
beginRemoveRows(QModelIndex(), removedIndex, removedIndex);
|
|
||||||
auto removedIter = mods.begin() + removedIndex;
|
|
||||||
if((*removedIter)->isResolving()) {
|
|
||||||
activeTickets.remove((*removedIter)->resolutionTicket());
|
|
||||||
}
|
|
||||||
|
|
||||||
mods.erase(removedIter);
|
|
||||||
endRemoveRows();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add new mods to the end
|
|
||||||
{
|
|
||||||
QSet<QString> added = newSet;
|
|
||||||
added.subtract(currentSet);
|
|
||||||
|
|
||||||
// When you have a Qt build with assertions turned on, proceeding here will abort the application
|
|
||||||
if (added.size() > 0) {
|
|
||||||
beginInsertRows(QModelIndex(), mods.size(), mods.size() + added.size() - 1);
|
|
||||||
for (auto& addedMod : added) {
|
|
||||||
mods.append(newMods[addedMod]);
|
|
||||||
resolveMod(mods.last());
|
|
||||||
}
|
|
||||||
endInsertRows();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// update index
|
|
||||||
{
|
|
||||||
modsIndex.clear();
|
|
||||||
int idx = 0;
|
|
||||||
for(auto mod: mods) {
|
|
||||||
modsIndex[mod->internal_id()] = idx;
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_update.reset();
|
|
||||||
|
|
||||||
emit updateFinished();
|
|
||||||
|
|
||||||
if(scheduled_update) {
|
|
||||||
scheduled_update = false;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModFolderModel::resolveMod(Mod::Ptr m)
|
|
||||||
{
|
|
||||||
if(!m->shouldResolve()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto task = new LocalModParseTask(nextResolutionTicket, m->type(), m->fileinfo());
|
|
||||||
auto result = task->result();
|
|
||||||
result->id = m->internal_id();
|
|
||||||
activeTickets.insert(nextResolutionTicket, result);
|
|
||||||
m->setResolving(true, nextResolutionTicket);
|
|
||||||
nextResolutionTicket++;
|
|
||||||
QThreadPool *threadPool = QThreadPool::globalInstance();
|
|
||||||
connect(task, &LocalModParseTask::finished, this, &ModFolderModel::finishModParse);
|
|
||||||
threadPool->start(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModFolderModel::finishModParse(int token)
|
|
||||||
{
|
|
||||||
auto iter = activeTickets.find(token);
|
|
||||||
if(iter == activeTickets.end()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto result = *iter;
|
|
||||||
activeTickets.remove(token);
|
|
||||||
int row = modsIndex[result->id];
|
|
||||||
auto mod = mods[row];
|
|
||||||
mod->finishResolvingWithDetails(result->details);
|
|
||||||
emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModFolderModel::disableInteraction(bool disabled)
|
|
||||||
{
|
|
||||||
if (interaction_disabled == disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
interaction_disabled = disabled;
|
|
||||||
if(size()) {
|
|
||||||
emit dataChanged(index(0), index(size() - 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModFolderModel::directoryChanged(QString path)
|
|
||||||
{
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ModFolderModel::isValid()
|
|
||||||
{
|
|
||||||
return m_dir.exists() && m_dir.isReadable();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>
|
|
||||||
{
|
|
||||||
QList<Mod::Ptr> selected_mods;
|
|
||||||
for (auto i : indexes) {
|
|
||||||
if(i.column() != 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
selected_mods.push_back(mods[i.row()]);
|
|
||||||
}
|
|
||||||
return selected_mods;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: this does not take disabled mod (with extra .disable extension) into account...
|
|
||||||
bool ModFolderModel::installMod(const QString &filename)
|
|
||||||
{
|
|
||||||
if(interaction_disabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
|
|
||||||
auto originalPath = FS::NormalizePath(filename);
|
|
||||||
QFileInfo fileinfo(originalPath);
|
|
||||||
|
|
||||||
if (!fileinfo.exists() || !fileinfo.isReadable())
|
|
||||||
{
|
|
||||||
qWarning() << "Caught attempt to install non-existing file or file-like object:" << originalPath;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
qDebug() << "installing: " << fileinfo.absoluteFilePath();
|
|
||||||
|
|
||||||
Mod installedMod(fileinfo);
|
|
||||||
if (!installedMod.valid())
|
|
||||||
{
|
|
||||||
qDebug() << originalPath << "is not a valid mod. Ignoring it.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto type = installedMod.type();
|
|
||||||
if (type == Mod::MOD_UNKNOWN)
|
|
||||||
{
|
|
||||||
qDebug() << "Cannot recognize mod type of" << originalPath << ", ignoring it.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto newpath = FS::NormalizePath(FS::PathCombine(m_dir.path(), fileinfo.fileName()));
|
|
||||||
if(originalPath == newpath)
|
|
||||||
{
|
|
||||||
qDebug() << "Overwriting the mod (" << originalPath << ") with itself makes no sense...";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD)
|
|
||||||
{
|
|
||||||
if(QFile::exists(newpath) || QFile::exists(newpath + QString(".disabled")))
|
|
||||||
{
|
|
||||||
if(!QFile::remove(newpath))
|
|
||||||
{
|
|
||||||
// FIXME: report error in a user-visible way
|
|
||||||
qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
qDebug() << newpath << "has been deleted.";
|
|
||||||
}
|
|
||||||
if (!QFile::copy(fileinfo.filePath(), newpath))
|
|
||||||
{
|
|
||||||
qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed.";
|
|
||||||
// FIXME: report error in a user-visible way
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
FS::updateTimestamp(newpath);
|
|
||||||
QFileInfo newpathInfo(newpath);
|
|
||||||
installedMod.repath(newpathInfo);
|
|
||||||
update();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (type == Mod::MOD_FOLDER)
|
|
||||||
{
|
|
||||||
QString from = fileinfo.filePath();
|
|
||||||
if(QFile::exists(newpath))
|
|
||||||
{
|
|
||||||
qDebug() << "Ignoring folder " << from << ", it would merge with " << newpath;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!FS::copy(from, newpath)())
|
|
||||||
{
|
|
||||||
qWarning() << "Copy of folder from" << originalPath << "to" << newpath << "has (potentially partially) failed.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
QFileInfo newpathInfo(newpath);
|
|
||||||
installedMod.repath(newpathInfo);
|
|
||||||
update();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata)
|
|
||||||
{
|
|
||||||
|
|
||||||
for(auto mod : allMods()){
|
|
||||||
if(mod->fileinfo().fileName() == filename){
|
|
||||||
auto index_dir = indexDir();
|
|
||||||
mod->destroy(index_dir, preserve_metadata);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusAction enable)
|
|
||||||
{
|
|
||||||
if(interaction_disabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(indexes.isEmpty())
|
|
||||||
return true;
|
|
||||||
|
|
||||||
for (auto index: indexes)
|
|
||||||
{
|
|
||||||
if(index.column() != 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
setModStatus(index.row(), enable);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
|
|
||||||
{
|
|
||||||
if(interaction_disabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(indexes.isEmpty())
|
|
||||||
return true;
|
|
||||||
|
|
||||||
for (auto i: indexes)
|
|
||||||
{
|
|
||||||
if(i.column() != 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
auto m = mods[i.row()];
|
|
||||||
auto index_dir = indexDir();
|
|
||||||
m->destroy(index_dir);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ModFolderModel::columnCount(const QModelIndex &parent) const
|
|
||||||
{
|
|
||||||
return NUM_COLUMNS;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant ModFolderModel::data(const QModelIndex &index, int role) const
|
QVariant ModFolderModel::data(const QModelIndex &index, int role) const
|
||||||
{
|
{
|
||||||
if (!index.isValid())
|
if (!validateIndex(index))
|
||||||
return QVariant();
|
return {};
|
||||||
|
|
||||||
int row = index.row();
|
int row = index.row();
|
||||||
int column = index.column();
|
int column = index.column();
|
||||||
|
|
||||||
if (row < 0 || row >= mods.size())
|
|
||||||
return QVariant();
|
|
||||||
|
|
||||||
switch (role)
|
switch (role)
|
||||||
{
|
{
|
||||||
case Qt::DisplayRole:
|
case Qt::DisplayRole:
|
||||||
switch (column)
|
switch (column)
|
||||||
{
|
{
|
||||||
case NameColumn:
|
case NameColumn:
|
||||||
return mods[row]->name();
|
return m_resources[row]->name();
|
||||||
case VersionColumn: {
|
case VersionColumn: {
|
||||||
switch(mods[row]->type()) {
|
switch(m_resources[row]->type()) {
|
||||||
case Mod::MOD_FOLDER:
|
case ResourceType::FOLDER:
|
||||||
return tr("Folder");
|
return tr("Folder");
|
||||||
case Mod::MOD_SINGLEFILE:
|
case ResourceType::SINGLEFILE:
|
||||||
return tr("File");
|
return tr("File");
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return mods[row]->version();
|
return at(row)->version();
|
||||||
}
|
}
|
||||||
case DateColumn:
|
case DateColumn:
|
||||||
return mods[row]->dateTimeChanged();
|
return m_resources[row]->dateTimeChanged();
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
case Qt::ToolTipRole:
|
case Qt::ToolTipRole:
|
||||||
return mods[row]->internal_id();
|
return m_resources[row]->internal_id();
|
||||||
|
|
||||||
case Qt::CheckStateRole:
|
case Qt::CheckStateRole:
|
||||||
switch (column)
|
switch (column)
|
||||||
{
|
{
|
||||||
case ActiveColumn:
|
case ActiveColumn:
|
||||||
return mods[row]->enabled() ? Qt::Checked : Qt::Unchecked;
|
return at(row)->enabled() ? Qt::Checked : Qt::Unchecked;
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
@ -483,61 +104,6 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ModFolderModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
|
||||||
{
|
|
||||||
if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role == Qt::CheckStateRole)
|
|
||||||
{
|
|
||||||
return setModStatus(index.row(), Toggle);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction action)
|
|
||||||
{
|
|
||||||
if(row < 0 || row >= mods.size()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto &mod = mods[row];
|
|
||||||
bool desiredStatus;
|
|
||||||
switch(action) {
|
|
||||||
case Enable:
|
|
||||||
desiredStatus = true;
|
|
||||||
break;
|
|
||||||
case Disable:
|
|
||||||
desiredStatus = false;
|
|
||||||
break;
|
|
||||||
case Toggle:
|
|
||||||
default:
|
|
||||||
desiredStatus = !mod->enabled();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(desiredStatus == mod->enabled()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// preserve the row, but change its ID
|
|
||||||
auto oldId = mod->internal_id();
|
|
||||||
if(!mod->enable(!mod->enabled())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
auto newId = mod->internal_id();
|
|
||||||
if(modsIndex.contains(newId)) {
|
|
||||||
// NOTE: this could handle a corner case, where we are overwriting a file, because the same 'mod' exists both enabled and disabled
|
|
||||||
// But is it necessary?
|
|
||||||
}
|
|
||||||
modsIndex.remove(oldId);
|
|
||||||
modsIndex[newId] = row;
|
|
||||||
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
|
QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||||
{
|
{
|
||||||
switch (role)
|
switch (role)
|
||||||
@ -577,65 +143,151 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in
|
|||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
Qt::ItemFlags ModFolderModel::flags(const QModelIndex &index) const
|
int ModFolderModel::columnCount(const QModelIndex &parent) const
|
||||||
{
|
{
|
||||||
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
|
return NUM_COLUMNS;
|
||||||
auto flags = defaultFlags;
|
|
||||||
if(interaction_disabled) {
|
|
||||||
flags &= ~Qt::ItemIsDropEnabled;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
flags |= Qt::ItemIsDropEnabled;
|
|
||||||
if(index.isValid()) {
|
|
||||||
flags |= Qt::ItemIsUserCheckable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return flags;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Qt::DropActions ModFolderModel::supportedDropActions() const
|
Task* ModFolderModel::createUpdateTask()
|
||||||
{
|
{
|
||||||
// copy from outside, move from within and other mod lists
|
auto index_dir = indexDir();
|
||||||
return Qt::CopyAction | Qt::MoveAction;
|
auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load, this);
|
||||||
|
m_first_folder_load = false;
|
||||||
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList ModFolderModel::mimeTypes() const
|
Task* ModFolderModel::createParseTask(Resource const& resource)
|
||||||
{
|
{
|
||||||
QStringList types;
|
return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo());
|
||||||
types << "text/uri-list";
|
|
||||||
return types;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ModFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&)
|
bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata)
|
||||||
{
|
{
|
||||||
if (action == Qt::IgnoreAction)
|
for(auto mod : allMods()){
|
||||||
{
|
if(mod->fileinfo().fileName() == filename){
|
||||||
|
auto index_dir = indexDir();
|
||||||
|
mod->destroy(index_dir, preserve_metadata);
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// check if the action is supported
|
return false;
|
||||||
if (!data || !(action & supportedDropActions()))
|
}
|
||||||
{
|
|
||||||
|
bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
|
||||||
|
{
|
||||||
|
if(!m_can_interact) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// files dropped from outside?
|
if(indexes.isEmpty())
|
||||||
if (data->hasUrls())
|
return true;
|
||||||
{
|
|
||||||
auto urls = data->urls();
|
for (auto i: indexes)
|
||||||
for (auto url : urls)
|
|
||||||
{
|
|
||||||
// only local files may be dropped...
|
|
||||||
if (!url.isLocalFile())
|
|
||||||
{
|
{
|
||||||
|
if(i.column() != 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// TODO: implement not only copy, but also move
|
auto m = at(i.row());
|
||||||
// FIXME: handle errors here
|
auto index_dir = indexDir();
|
||||||
installMod(url.toLocalFile());
|
m->destroy(index_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
bool ModFolderModel::isValid()
|
||||||
|
{
|
||||||
|
return m_dir.exists() && m_dir.isReadable();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ModFolderModel::startWatching()
|
||||||
|
{
|
||||||
|
// Remove orphaned metadata next time
|
||||||
|
m_first_folder_load = true;
|
||||||
|
return ResourceFolderModel::startWatching({ m_dir.absolutePath(), indexDir().absolutePath() });
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ModFolderModel::stopWatching()
|
||||||
|
{
|
||||||
|
return ResourceFolderModel::stopWatching({ m_dir.absolutePath(), indexDir().absolutePath() });
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList<Mod*>
|
||||||
|
{
|
||||||
|
QList<Mod*> selected_resources;
|
||||||
|
for (auto i : indexes) {
|
||||||
|
if(i.column() != 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
selected_resources.push_back(at(i.row()));
|
||||||
|
}
|
||||||
|
return selected_resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ModFolderModel::allMods() -> QList<Mod*>
|
||||||
|
{
|
||||||
|
QList<Mod*> mods;
|
||||||
|
|
||||||
|
for (auto& res : m_resources) {
|
||||||
|
mods.append(static_cast<Mod*>(res.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mods;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModFolderModel::onUpdateSucceeded()
|
||||||
|
{
|
||||||
|
auto update_results = static_cast<ModFolderLoadTask*>(m_current_update_task.get())->result();
|
||||||
|
|
||||||
|
auto& new_mods = update_results->mods;
|
||||||
|
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||||
|
auto current_list = m_resources_index.keys();
|
||||||
|
QSet<QString> current_set(current_list.begin(), current_list.end());
|
||||||
|
|
||||||
|
auto new_list = new_mods.keys();
|
||||||
|
QSet<QString> new_set(new_list.begin(), new_list.end());
|
||||||
|
#else
|
||||||
|
QSet<QString> current_set(m_resources_index.keys().toSet());
|
||||||
|
QSet<QString> new_set(new_mods.keys().toSet());
|
||||||
|
#endif
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
auto iter = m_active_parse_tasks.constFind(ticket);
|
||||||
|
if (iter == m_active_parse_tasks.constEnd())
|
||||||
|
return;
|
||||||
|
|
||||||
|
int row = m_resources_index[mod_id];
|
||||||
|
|
||||||
|
auto parse_task = *iter;
|
||||||
|
auto cast_task = static_cast<LocalModParseTask*>(parse_task.get());
|
||||||
|
|
||||||
|
Q_ASSERT(cast_task->token() == ticket);
|
||||||
|
|
||||||
|
auto resource = find(mod_id);
|
||||||
|
|
||||||
|
auto result = cast_task->result();
|
||||||
|
if (result && resource)
|
||||||
|
resource->finishResolvingWithDetails(std::move(result->details));
|
||||||
|
|
||||||
|
emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,7 @@
|
|||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
|
|
||||||
#include "Mod.h"
|
#include "Mod.h"
|
||||||
|
#include "ResourceFolderModel.h"
|
||||||
|
|
||||||
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
|
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
|
||||||
#include "minecraft/mod/tasks/LocalModParseTask.h"
|
#include "minecraft/mod/tasks/LocalModParseTask.h"
|
||||||
@ -56,7 +57,7 @@ class QFileSystemWatcher;
|
|||||||
* A legacy mod list.
|
* A legacy mod list.
|
||||||
* Backed by a folder.
|
* Backed by a folder.
|
||||||
*/
|
*/
|
||||||
class ModFolderModel : public QAbstractListModel
|
class ModFolderModel : public ResourceFolderModel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
@ -75,106 +76,38 @@ public:
|
|||||||
};
|
};
|
||||||
ModFolderModel(const QString &dir, bool is_indexed = false);
|
ModFolderModel(const QString &dir, bool is_indexed = false);
|
||||||
|
|
||||||
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
|
|
||||||
Qt::DropActions supportedDropActions() const override;
|
|
||||||
|
|
||||||
/// flags, mostly to support drag&drop
|
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||||
virtual Qt::ItemFlags flags(const QModelIndex &index) const override;
|
int columnCount(const QModelIndex &parent) const override;
|
||||||
QStringList mimeTypes() const override;
|
|
||||||
bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override;
|
|
||||||
|
|
||||||
virtual int rowCount(const QModelIndex &) const override
|
[[nodiscard]] Task* createUpdateTask() override;
|
||||||
{
|
[[nodiscard]] Task* createParseTask(Resource const&) override;
|
||||||
return size();
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
|
||||||
virtual int columnCount(const QModelIndex &parent) const override;
|
|
||||||
|
|
||||||
size_t size() const
|
|
||||||
{
|
|
||||||
return mods.size();
|
|
||||||
}
|
|
||||||
;
|
|
||||||
bool empty() const
|
|
||||||
{
|
|
||||||
return size() == 0;
|
|
||||||
}
|
|
||||||
Mod& operator[](size_t index)
|
|
||||||
{
|
|
||||||
return *mods[index];
|
|
||||||
}
|
|
||||||
const Mod& at(size_t index) const
|
|
||||||
{
|
|
||||||
return *mods.at(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reloads the mod list and returns true if the list changed.
|
|
||||||
bool update();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the given mod to the list at the given index - if the list supports custom ordering
|
|
||||||
*/
|
|
||||||
bool installMod(const QString& filename);
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
/// Deletes all the selected mods
|
/// Deletes all the selected mods
|
||||||
bool deleteMods(const QModelIndexList &indexes);
|
bool deleteMods(const QModelIndexList &indexes);
|
||||||
|
|
||||||
/// Enable or disable listed mods
|
|
||||||
bool setModStatus(const QModelIndexList &indexes, ModStatusAction action);
|
|
||||||
|
|
||||||
void startWatching();
|
|
||||||
void stopWatching();
|
|
||||||
|
|
||||||
bool isValid();
|
bool isValid();
|
||||||
|
|
||||||
QDir& dir()
|
bool startWatching() override;
|
||||||
{
|
bool stopWatching() override;
|
||||||
return m_dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDir indexDir()
|
QDir indexDir() { return { QString("%1/.index").arg(dir().absolutePath()) }; }
|
||||||
{
|
|
||||||
return { QString("%1/.index").arg(dir().absolutePath()) };
|
|
||||||
}
|
|
||||||
|
|
||||||
const QList<Mod::Ptr>& allMods()
|
auto selectedMods(QModelIndexList& indexes) -> QList<Mod*>;
|
||||||
{
|
auto allMods() -> QList<Mod*>;
|
||||||
return mods;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>;
|
RESOURCE_HELPERS(Mod)
|
||||||
|
|
||||||
public slots:
|
|
||||||
void disableInteraction(bool disabled);
|
|
||||||
|
|
||||||
private
|
private
|
||||||
slots:
|
slots:
|
||||||
void directoryChanged(QString path);
|
void onUpdateSucceeded() override;
|
||||||
void finishUpdate();
|
void onParseSucceeded(int ticket, QString resource_id) override;
|
||||||
void finishModParse(int token);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void updateFinished();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void resolveMod(Mod::Ptr m);
|
|
||||||
bool setModStatus(int index, ModStatusAction action);
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QFileSystemWatcher *m_watcher;
|
|
||||||
bool is_watching = false;
|
|
||||||
ModFolderLoadTask::ResultPtr m_update;
|
|
||||||
bool scheduled_update = false;
|
|
||||||
bool interaction_disabled = false;
|
|
||||||
QDir m_dir;
|
|
||||||
bool m_is_indexed;
|
bool m_is_indexed;
|
||||||
bool m_first_folder_load = true;
|
bool m_first_folder_load = true;
|
||||||
QMap<QString, int> modsIndex;
|
|
||||||
QMap<int, LocalModParseTask::ResultPtr> activeTickets;
|
|
||||||
int nextResolutionTicket = 0;
|
|
||||||
QList<Mod::Ptr> mods;
|
|
||||||
};
|
};
|
||||||
|
@ -1,92 +0,0 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
/*
|
|
||||||
* PolyMC - Minecraft Launcher
|
|
||||||
* 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
|
|
||||||
* 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/>.
|
|
||||||
*
|
|
||||||
* 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 <QTest>
|
|
||||||
#include <QTemporaryDir>
|
|
||||||
|
|
||||||
#include "FileSystem.h"
|
|
||||||
#include "minecraft/mod/ModFolderModel.h"
|
|
||||||
|
|
||||||
class ModFolderModelTest : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
private
|
|
||||||
slots:
|
|
||||||
// test for GH-1178 - install a folder with files to a mod list
|
|
||||||
void test_1178()
|
|
||||||
{
|
|
||||||
// source
|
|
||||||
QString source = QFINDTESTDATA("testdata/test_folder");
|
|
||||||
|
|
||||||
// sanity check
|
|
||||||
QVERIFY(!source.endsWith('/'));
|
|
||||||
|
|
||||||
auto verify = [](QString path)
|
|
||||||
{
|
|
||||||
QDir target_dir(FS::PathCombine(path, "test_folder"));
|
|
||||||
QVERIFY(target_dir.entryList().contains("pack.mcmeta"));
|
|
||||||
QVERIFY(target_dir.entryList().contains("assets"));
|
|
||||||
};
|
|
||||||
|
|
||||||
// 1. test with no trailing /
|
|
||||||
{
|
|
||||||
QString folder = source;
|
|
||||||
QTemporaryDir tempDir;
|
|
||||||
QEventLoop loop;
|
|
||||||
ModFolderModel m(tempDir.path(), true);
|
|
||||||
connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit);
|
|
||||||
m.installMod(folder);
|
|
||||||
loop.exec();
|
|
||||||
verify(tempDir.path());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. test with trailing /
|
|
||||||
{
|
|
||||||
QString folder = source + '/';
|
|
||||||
QTemporaryDir tempDir;
|
|
||||||
QEventLoop loop;
|
|
||||||
ModFolderModel m(tempDir.path(), true);
|
|
||||||
connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit);
|
|
||||||
m.installMod(folder);
|
|
||||||
loop.exec();
|
|
||||||
verify(tempDir.path());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
QTEST_GUILESS_MAIN(ModFolderModelTest)
|
|
||||||
|
|
||||||
#include "ModFolderModel_test.moc"
|
|
147
launcher/minecraft/mod/Resource.cpp
Normal file
147
launcher/minecraft/mod/Resource.cpp
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
#include "Resource.h"
|
||||||
|
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
|
#include "FileSystem.h"
|
||||||
|
|
||||||
|
Resource::Resource(QObject* parent) : QObject(parent) {}
|
||||||
|
|
||||||
|
Resource::Resource(QFileInfo file_info) : QObject()
|
||||||
|
{
|
||||||
|
setFile(file_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Resource::setFile(QFileInfo file_info)
|
||||||
|
{
|
||||||
|
m_file_info = file_info;
|
||||||
|
parseFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Resource::parseFile()
|
||||||
|
{
|
||||||
|
QString file_name{ m_file_info.fileName() };
|
||||||
|
|
||||||
|
m_type = ResourceType::UNKNOWN;
|
||||||
|
|
||||||
|
m_internal_id = file_name;
|
||||||
|
|
||||||
|
if (m_file_info.isDir()) {
|
||||||
|
m_type = ResourceType::FOLDER;
|
||||||
|
m_name = file_name;
|
||||||
|
} else if (m_file_info.isFile()) {
|
||||||
|
if (file_name.endsWith(".disabled")) {
|
||||||
|
file_name.chop(9);
|
||||||
|
m_enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_name.endsWith(".zip") || file_name.endsWith(".jar")) {
|
||||||
|
m_type = ResourceType::ZIPFILE;
|
||||||
|
file_name.chop(4);
|
||||||
|
} else if (file_name.endsWith(".litemod")) {
|
||||||
|
m_type = ResourceType::LITEMOD;
|
||||||
|
file_name.chop(8);
|
||||||
|
} else {
|
||||||
|
m_type = ResourceType::SINGLEFILE;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_name = file_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_changed_date_time = m_file_info.lastModified();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void removeThePrefix(QString& string)
|
||||||
|
{
|
||||||
|
QRegularExpression regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption);
|
||||||
|
string.remove(regex);
|
||||||
|
string = string.trimmed();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<int, bool> Resource::compare(const Resource& other, SortType type) const
|
||||||
|
{
|
||||||
|
switch (type) {
|
||||||
|
default:
|
||||||
|
case SortType::ENABLED:
|
||||||
|
if (enabled() && !other.enabled())
|
||||||
|
return { 1, type == SortType::ENABLED };
|
||||||
|
if (!enabled() && other.enabled())
|
||||||
|
return { -1, type == SortType::ENABLED };
|
||||||
|
case SortType::NAME: {
|
||||||
|
QString this_name{ name() };
|
||||||
|
QString other_name{ other.name() };
|
||||||
|
|
||||||
|
removeThePrefix(this_name);
|
||||||
|
removeThePrefix(other_name);
|
||||||
|
|
||||||
|
auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive);
|
||||||
|
if (compare_result != 0)
|
||||||
|
return { compare_result, type == SortType::NAME };
|
||||||
|
}
|
||||||
|
case SortType::DATE:
|
||||||
|
if (dateTimeChanged() > other.dateTimeChanged())
|
||||||
|
return { 1, type == SortType::DATE };
|
||||||
|
if (dateTimeChanged() < other.dateTimeChanged())
|
||||||
|
return { -1, type == SortType::DATE };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { 0, false };
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Resource::applyFilter(QRegularExpression filter) const
|
||||||
|
{
|
||||||
|
return filter.match(name()).hasMatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Resource::enable(EnableAction action)
|
||||||
|
{
|
||||||
|
if (m_type == ResourceType::UNKNOWN || m_type == ResourceType::FOLDER)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
|
||||||
|
QString path = m_file_info.absoluteFilePath();
|
||||||
|
QFile file(path);
|
||||||
|
|
||||||
|
bool enable = true;
|
||||||
|
switch (action) {
|
||||||
|
case EnableAction::ENABLE:
|
||||||
|
enable = true;
|
||||||
|
break;
|
||||||
|
case EnableAction::DISABLE:
|
||||||
|
enable = false;
|
||||||
|
break;
|
||||||
|
case EnableAction::TOGGLE:
|
||||||
|
default:
|
||||||
|
enable = !enabled();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_enabled == enable)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (enable) {
|
||||||
|
// m_enabled is false, but there's no '.disabled' suffix.
|
||||||
|
// TODO: Report error?
|
||||||
|
if (!path.endsWith(".disabled"))
|
||||||
|
return false;
|
||||||
|
path.chop(9);
|
||||||
|
|
||||||
|
if (!file.rename(path))
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
path += ".disabled";
|
||||||
|
|
||||||
|
if (!file.rename(path))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFile(QFileInfo(path));
|
||||||
|
|
||||||
|
m_enabled = enable;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Resource::destroy()
|
||||||
|
{
|
||||||
|
m_type = ResourceType::UNKNOWN;
|
||||||
|
return FS::deletePath(m_file_info.filePath());
|
||||||
|
}
|
115
launcher/minecraft/mod/Resource.h
Normal file
115
launcher/minecraft/mod/Resource.h
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
|
||||||
|
#include "QObjectPtr.h"
|
||||||
|
|
||||||
|
enum class ResourceType {
|
||||||
|
UNKNOWN, //!< Indicates an unspecified resource type.
|
||||||
|
ZIPFILE, //!< The resource is a zip file containing the resource's class files.
|
||||||
|
SINGLEFILE, //!< The resource is a single file (not a zip file).
|
||||||
|
FOLDER, //!< The resource is in a folder on the filesystem.
|
||||||
|
LITEMOD, //!< The resource is a litemod
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class SortType {
|
||||||
|
NAME,
|
||||||
|
DATE,
|
||||||
|
VERSION,
|
||||||
|
ENABLED,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class EnableAction {
|
||||||
|
ENABLE,
|
||||||
|
DISABLE,
|
||||||
|
TOGGLE
|
||||||
|
};
|
||||||
|
|
||||||
|
/** General class for managed resources. It mirrors a file in disk, with some more info
|
||||||
|
* for display and house-keeping purposes.
|
||||||
|
*
|
||||||
|
* Subclass it to add additional data / behavior, such as Mods or Resource packs.
|
||||||
|
*/
|
||||||
|
class Resource : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
Q_DISABLE_COPY(Resource)
|
||||||
|
public:
|
||||||
|
using Ptr = shared_qobject_ptr<Resource>;
|
||||||
|
using WeakPtr = QPointer<Resource>;
|
||||||
|
|
||||||
|
Resource(QObject* parent = nullptr);
|
||||||
|
Resource(QFileInfo file_info);
|
||||||
|
Resource(QString file_path) : Resource(QFileInfo(file_path)) {}
|
||||||
|
|
||||||
|
~Resource() override = default;
|
||||||
|
|
||||||
|
void setFile(QFileInfo file_info);
|
||||||
|
void parseFile();
|
||||||
|
|
||||||
|
[[nodiscard]] auto fileinfo() const -> QFileInfo { return m_file_info; }
|
||||||
|
[[nodiscard]] auto dateTimeChanged() const -> QDateTime { return m_changed_date_time; }
|
||||||
|
[[nodiscard]] auto internal_id() const -> QString { return m_internal_id; }
|
||||||
|
[[nodiscard]] auto type() const -> ResourceType { return m_type; }
|
||||||
|
[[nodiscard]] bool enabled() const { return m_enabled; }
|
||||||
|
|
||||||
|
[[nodiscard]] virtual auto name() const -> QString { return m_name; }
|
||||||
|
[[nodiscard]] virtual bool valid() const { return m_type != ResourceType::UNKNOWN; }
|
||||||
|
|
||||||
|
/** Compares two Resources, for sorting purposes, considering a ascending order, returning:
|
||||||
|
* > 0: 'this' comes after 'other'
|
||||||
|
* = 0: 'this' is equal to 'other'
|
||||||
|
* < 0: 'this' comes before 'other'
|
||||||
|
*
|
||||||
|
* The second argument in the pair is true if the sorting type that decided which one is greater was 'type'.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] virtual auto compare(Resource const& other, SortType type = SortType::NAME) const -> std::pair<int, bool>;
|
||||||
|
|
||||||
|
/** Returns whether the given filter should filter out 'this' (false),
|
||||||
|
* or if such filter includes the Resource (true).
|
||||||
|
*/
|
||||||
|
[[nodiscard]] virtual bool applyFilter(QRegularExpression filter) const;
|
||||||
|
|
||||||
|
/** Changes the enabled property, according to 'action'.
|
||||||
|
*
|
||||||
|
* Returns whether a change was applied to the Resource's properties.
|
||||||
|
*/
|
||||||
|
bool enable(EnableAction action);
|
||||||
|
|
||||||
|
[[nodiscard]] auto shouldResolve() const -> bool { return !m_is_resolving && !m_is_resolved; }
|
||||||
|
[[nodiscard]] auto isResolving() const -> bool { return m_is_resolving; }
|
||||||
|
[[nodiscard]] auto resolutionTicket() const -> int { return m_resolution_ticket; }
|
||||||
|
|
||||||
|
void setResolving(bool resolving, int resolutionTicket)
|
||||||
|
{
|
||||||
|
m_is_resolving = resolving;
|
||||||
|
m_resolution_ticket = resolutionTicket;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all files of this resource.
|
||||||
|
bool destroy();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
/* The file corresponding to this resource. */
|
||||||
|
QFileInfo m_file_info;
|
||||||
|
/* The cached date when this file was last changed. */
|
||||||
|
QDateTime m_changed_date_time;
|
||||||
|
|
||||||
|
/* Internal ID for internal purposes. Properties such as human-readability should not be assumed. */
|
||||||
|
QString m_internal_id;
|
||||||
|
/* Name as reported via the file name. In the absence of a better name, this is shown to the user. */
|
||||||
|
QString m_name;
|
||||||
|
|
||||||
|
/* The type of file we're dealing with. */
|
||||||
|
ResourceType m_type = ResourceType::UNKNOWN;
|
||||||
|
|
||||||
|
/* Whether the resource is enabled (e.g. shows up in the game) or not. */
|
||||||
|
bool m_enabled = true;
|
||||||
|
|
||||||
|
/* Used to keep trach of pending / concluded actions on the resource. */
|
||||||
|
bool m_is_resolving = false;
|
||||||
|
bool m_is_resolved = false;
|
||||||
|
int m_resolution_ticket = 0;
|
||||||
|
};
|
522
launcher/minecraft/mod/ResourceFolderModel.cpp
Normal file
522
launcher/minecraft/mod/ResourceFolderModel.cpp
Normal file
@ -0,0 +1,522 @@
|
|||||||
|
#include "ResourceFolderModel.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QMimeData>
|
||||||
|
#include <QThreadPool>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include "FileSystem.h"
|
||||||
|
|
||||||
|
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
|
||||||
|
|
||||||
|
#include "tasks/Task.h"
|
||||||
|
|
||||||
|
ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent) : QAbstractListModel(parent), m_dir(dir), m_watcher(this)
|
||||||
|
{
|
||||||
|
m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
|
||||||
|
m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
|
||||||
|
|
||||||
|
connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ResourceFolderModel::startWatching(const QStringList paths)
|
||||||
|
{
|
||||||
|
if (m_is_watching)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto couldnt_be_watched = m_watcher.addPaths(paths);
|
||||||
|
for (auto path : paths) {
|
||||||
|
if (couldnt_be_watched.contains(path))
|
||||||
|
qDebug() << "Failed to start watching " << path;
|
||||||
|
else
|
||||||
|
qDebug() << "Started watching " << path;
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
|
m_is_watching = !m_is_watching;
|
||||||
|
return m_is_watching;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ResourceFolderModel::stopWatching(const QStringList paths)
|
||||||
|
{
|
||||||
|
if (!m_is_watching)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto couldnt_be_stopped = m_watcher.removePaths(paths);
|
||||||
|
for (auto path : paths) {
|
||||||
|
if (couldnt_be_stopped.contains(path))
|
||||||
|
qDebug() << "Failed to stop watching " << path;
|
||||||
|
else
|
||||||
|
qDebug() << "Stopped watching " << path;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_is_watching = !m_is_watching;
|
||||||
|
return !m_is_watching;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ResourceFolderModel::installResource(QString original_path)
|
||||||
|
{
|
||||||
|
if (!m_can_interact) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
|
||||||
|
original_path = FS::NormalizePath(original_path);
|
||||||
|
QFileInfo file_info(original_path);
|
||||||
|
|
||||||
|
if (!file_info.exists() || !file_info.isReadable()) {
|
||||||
|
qWarning() << "Caught attempt to install non-existing file or file-like object:" << original_path;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
qDebug() << "Installing: " << file_info.absoluteFilePath();
|
||||||
|
|
||||||
|
Resource resource(file_info);
|
||||||
|
if (!resource.valid()) {
|
||||||
|
qWarning() << original_path << "is not a valid resource. Ignoring it.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto new_path = FS::NormalizePath(m_dir.filePath(file_info.fileName()));
|
||||||
|
if (original_path == new_path) {
|
||||||
|
qWarning() << "Overwriting the mod (" << original_path << ") with itself makes no sense...";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (resource.type()) {
|
||||||
|
case ResourceType::SINGLEFILE:
|
||||||
|
case ResourceType::ZIPFILE:
|
||||||
|
case ResourceType::LITEMOD: {
|
||||||
|
if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) {
|
||||||
|
if (!QFile::remove(new_path)) {
|
||||||
|
qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
qDebug() << new_path << "has been deleted.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!QFile::copy(original_path, new_path)) {
|
||||||
|
qCritical() << "Copy from" << original_path << "to" << new_path << "has failed.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FS::updateTimestamp(new_path);
|
||||||
|
|
||||||
|
QFileInfo new_path_file_info(new_path);
|
||||||
|
resource.setFile(new_path_file_info);
|
||||||
|
|
||||||
|
if (!m_is_watching)
|
||||||
|
return update();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case ResourceType::FOLDER: {
|
||||||
|
if (QFile::exists(new_path)) {
|
||||||
|
qDebug() << "Ignoring folder '" << original_path << "', it would merge with" << new_path;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FS::copy(original_path, new_path)()) {
|
||||||
|
qWarning() << "Copy of folder from" << original_path << "to" << new_path << "has (potentially partially) failed.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QFileInfo newpathInfo(new_path);
|
||||||
|
resource.setFile(newpathInfo);
|
||||||
|
|
||||||
|
if (!m_is_watching)
|
||||||
|
return update();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ResourceFolderModel::uninstallResource(QString file_name)
|
||||||
|
{
|
||||||
|
for (auto& resource : m_resources) {
|
||||||
|
if (resource->fileinfo().fileName() == file_name) {
|
||||||
|
auto res = resource->destroy();
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes)
|
||||||
|
{
|
||||||
|
if (!m_can_interact)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (indexes.isEmpty())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
for (auto i : indexes) {
|
||||||
|
if (i.column() != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& resource = m_resources.at(i.row());
|
||||||
|
|
||||||
|
resource->destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ResourceFolderModel::setResourceEnabled(const QModelIndexList &indexes, EnableAction action)
|
||||||
|
{
|
||||||
|
if (!m_can_interact)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (indexes.isEmpty())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
bool succeeded = true;
|
||||||
|
for (auto const& idx : indexes) {
|
||||||
|
if (!validateIndex(idx) || idx.column() != 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int row = idx.row();
|
||||||
|
|
||||||
|
auto& resource = m_resources[row];
|
||||||
|
|
||||||
|
// Preserve the row, but change its ID
|
||||||
|
auto old_id = resource->internal_id();
|
||||||
|
if (!resource->enable(action)) {
|
||||||
|
succeeded = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto new_id = resource->internal_id();
|
||||||
|
if (m_resources_index.contains(new_id)) {
|
||||||
|
// FIXME: https://github.com/PolyMC/PolyMC/issues/550
|
||||||
|
}
|
||||||
|
|
||||||
|
m_resources_index.remove(old_id);
|
||||||
|
m_resources_index[new_id] = row;
|
||||||
|
|
||||||
|
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return succeeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QMutex s_update_task_mutex;
|
||||||
|
bool ResourceFolderModel::update()
|
||||||
|
{
|
||||||
|
// We hold a lock here to prevent race conditions on the m_current_update_task reset.
|
||||||
|
QMutexLocker lock(&s_update_task_mutex);
|
||||||
|
|
||||||
|
// Already updating, so we schedule a future update and return.
|
||||||
|
if (m_current_update_task) {
|
||||||
|
m_scheduled_update = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_current_update_task.reset(createUpdateTask());
|
||||||
|
if (!m_current_update_task)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded,
|
||||||
|
Qt::ConnectionType::QueuedConnection);
|
||||||
|
connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection);
|
||||||
|
|
||||||
|
auto* thread_pool = QThreadPool::globalInstance();
|
||||||
|
thread_pool->start(m_current_update_task.get());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResourceFolderModel::resolveResource(Resource::Ptr res)
|
||||||
|
{
|
||||||
|
if (!res->shouldResolve()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto task = createParseTask(*res);
|
||||||
|
if (!task)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_ticket_mutex.lock();
|
||||||
|
int ticket = m_next_resolution_ticket;
|
||||||
|
m_next_resolution_ticket += 1;
|
||||||
|
m_ticket_mutex.unlock();
|
||||||
|
|
||||||
|
res->setResolving(true, ticket);
|
||||||
|
m_active_parse_tasks.insert(ticket, task);
|
||||||
|
|
||||||
|
connect(
|
||||||
|
task, &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection);
|
||||||
|
connect(
|
||||||
|
task, &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection);
|
||||||
|
connect(
|
||||||
|
task, &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection);
|
||||||
|
|
||||||
|
auto* thread_pool = QThreadPool::globalInstance();
|
||||||
|
thread_pool->start(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResourceFolderModel::onUpdateSucceeded()
|
||||||
|
{
|
||||||
|
auto update_results = static_cast<BasicFolderLoadTask*>(m_current_update_task.get())->result();
|
||||||
|
|
||||||
|
auto& new_resources = update_results->resources;
|
||||||
|
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||||
|
auto current_list = m_resources_index.keys();
|
||||||
|
QSet<QString> current_set(current_list.begin(), current_list.end());
|
||||||
|
|
||||||
|
auto new_list = new_resources.keys();
|
||||||
|
QSet<QString> new_set(new_list.begin(), new_list.end());
|
||||||
|
#else
|
||||||
|
QSet<QString> current_set(m_resources_index.keys().toSet());
|
||||||
|
QSet<QString> new_set(new_resources.keys().toSet());
|
||||||
|
#endif
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
auto iter = m_active_parse_tasks.constFind(ticket);
|
||||||
|
if (iter == m_active_parse_tasks.constEnd())
|
||||||
|
return;
|
||||||
|
|
||||||
|
int row = m_resources_index[resource_id];
|
||||||
|
emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
Task* ResourceFolderModel::createUpdateTask()
|
||||||
|
{
|
||||||
|
return new BasicFolderLoadTask(m_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ResourceFolderModel::hasPendingParseTasks() const
|
||||||
|
{
|
||||||
|
return !m_active_parse_tasks.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResourceFolderModel::directoryChanged(QString path)
|
||||||
|
{
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
Qt::DropActions ResourceFolderModel::supportedDropActions() const
|
||||||
|
{
|
||||||
|
// copy from outside, move from within and other resource lists
|
||||||
|
return Qt::CopyAction | Qt::MoveAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const
|
||||||
|
{
|
||||||
|
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
|
||||||
|
auto flags = defaultFlags;
|
||||||
|
if (!m_can_interact) {
|
||||||
|
flags &= ~Qt::ItemIsDropEnabled;
|
||||||
|
} else {
|
||||||
|
flags |= Qt::ItemIsDropEnabled;
|
||||||
|
if (index.isValid()) {
|
||||||
|
flags |= Qt::ItemIsUserCheckable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList ResourceFolderModel::mimeTypes() const
|
||||||
|
{
|
||||||
|
QStringList types;
|
||||||
|
types << "text/uri-list";
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ResourceFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&)
|
||||||
|
{
|
||||||
|
if (action == Qt::IgnoreAction) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the action is supported
|
||||||
|
if (!data || !(action & supportedDropActions())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// files dropped from outside?
|
||||||
|
if (data->hasUrls()) {
|
||||||
|
auto urls = data->urls();
|
||||||
|
for (auto url : urls) {
|
||||||
|
// only local files may be dropped...
|
||||||
|
if (!url.isLocalFile()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// TODO: implement not only copy, but also move
|
||||||
|
// FIXME: handle errors here
|
||||||
|
installResource(url.toLocalFile());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ResourceFolderModel::validateIndex(const QModelIndex& index) const
|
||||||
|
{
|
||||||
|
if (!index.isValid())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int row = index.row();
|
||||||
|
if (row < 0 || row >= m_resources.size())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant ResourceFolderModel::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 NAME_COLUMN:
|
||||||
|
return m_resources[row]->name();
|
||||||
|
case DATE_COLUMN:
|
||||||
|
return m_resources[row]->dateTimeChanged();
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
case Qt::ToolTipRole:
|
||||||
|
return m_resources[row]->internal_id();
|
||||||
|
case Qt::CheckStateRole:
|
||||||
|
switch (column) {
|
||||||
|
case ACTIVE_COLUMN:
|
||||||
|
return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked;
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ResourceFolderModel::setData(const QModelIndex& index, const QVariant& value, int role)
|
||||||
|
{
|
||||||
|
int row = index.row();
|
||||||
|
if (row < 0 || row >= rowCount(index) || !index.isValid())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (role == Qt::CheckStateRole)
|
||||||
|
return setResourceEnabled({ index }, EnableAction::TOGGLE);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant ResourceFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||||
|
{
|
||||||
|
switch (role) {
|
||||||
|
case Qt::DisplayRole:
|
||||||
|
switch (section) {
|
||||||
|
case NAME_COLUMN:
|
||||||
|
return tr("Name");
|
||||||
|
case DATE_COLUMN:
|
||||||
|
return tr("Last modified");
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
case Qt::ToolTipRole: {
|
||||||
|
switch (section) {
|
||||||
|
case ACTIVE_COLUMN:
|
||||||
|
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
|
||||||
|
return tr("Is the resource enabled?");
|
||||||
|
case NAME_COLUMN:
|
||||||
|
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
|
||||||
|
return tr("The name of the resource.");
|
||||||
|
case DATE_COLUMN:
|
||||||
|
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
|
||||||
|
return tr("The date and time this resource was last changed (or added).");
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QSortFilterProxyModel* ResourceFolderModel::createFilterProxyModel(QObject* parent)
|
||||||
|
{
|
||||||
|
return new ProxyModel(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
SortType ResourceFolderModel::columnToSortKey(size_t column) const
|
||||||
|
{
|
||||||
|
Q_ASSERT(m_column_sort_keys.size() == columnCount());
|
||||||
|
return m_column_sort_keys.at(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResourceFolderModel::enableInteraction(bool enabled)
|
||||||
|
{
|
||||||
|
if (m_can_interact == enabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_can_interact = enabled;
|
||||||
|
if (size())
|
||||||
|
emit dataChanged(index(0), index(size() - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Standard Proxy Model for createFilterProxyModel */
|
||||||
|
[[nodiscard]] bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
|
||||||
|
{
|
||||||
|
auto* model = qobject_cast<ResourceFolderModel*>(sourceModel());
|
||||||
|
if (!model)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
const auto& resource = model->at(source_row);
|
||||||
|
|
||||||
|
return resource.applyFilter(filterRegularExpression());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool ResourceFolderModel::ProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const
|
||||||
|
{
|
||||||
|
auto* model = qobject_cast<ResourceFolderModel*>(sourceModel());
|
||||||
|
if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) {
|
||||||
|
return QSortFilterProxyModel::lessThan(source_left, source_right);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and
|
||||||
|
// proceed.
|
||||||
|
|
||||||
|
auto column_sort_key = model->columnToSortKey(source_left.column());
|
||||||
|
auto const& resource_left = model->at(source_left.row());
|
||||||
|
auto const& resource_right = model->at(source_right.row());
|
||||||
|
|
||||||
|
auto compare_result = resource_left.compare(resource_right, column_sort_key);
|
||||||
|
if (compare_result.first == 0)
|
||||||
|
return QSortFilterProxyModel::lessThan(source_left, source_right);
|
||||||
|
|
||||||
|
if (compare_result.second || sortOrder() != Qt::DescendingOrder)
|
||||||
|
return (compare_result.first < 0);
|
||||||
|
return (compare_result.first > 0);
|
||||||
|
}
|
326
launcher/minecraft/mod/ResourceFolderModel.h
Normal file
326
launcher/minecraft/mod/ResourceFolderModel.h
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileSystemWatcher>
|
||||||
|
#include <QMutex>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
|
||||||
|
#include "Resource.h"
|
||||||
|
|
||||||
|
#include "tasks/Task.h"
|
||||||
|
|
||||||
|
class QSortFilterProxyModel;
|
||||||
|
|
||||||
|
/** A basic model for external resources.
|
||||||
|
*
|
||||||
|
* This model manages a list of resources. As such, external users of such resources do not own them,
|
||||||
|
* and the resource's lifetime is contingent on the model's lifetime.
|
||||||
|
*
|
||||||
|
* TODO: Make the resources unique pointers accessible through weak pointers.
|
||||||
|
*/
|
||||||
|
class ResourceFolderModel : public QAbstractListModel {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
ResourceFolderModel(QDir, QObject* parent = nullptr);
|
||||||
|
|
||||||
|
/** Starts watching the paths for changes.
|
||||||
|
*
|
||||||
|
* Returns whether starting to watch all the paths was successful.
|
||||||
|
* If one or more fails, it returns false.
|
||||||
|
*/
|
||||||
|
bool startWatching(const QStringList paths);
|
||||||
|
|
||||||
|
/** Stops watching the paths for changes.
|
||||||
|
*
|
||||||
|
* Returns whether stopping to watch all the paths was successful.
|
||||||
|
* If one or more fails, it returns false.
|
||||||
|
*/
|
||||||
|
bool stopWatching(const QStringList paths);
|
||||||
|
|
||||||
|
/* Helper methods for subclasses, using a predetermined list of paths. */
|
||||||
|
virtual bool startWatching() { return startWatching({ m_dir.absolutePath() }); };
|
||||||
|
virtual bool stopWatching() { return stopWatching({ m_dir.absolutePath() }); };
|
||||||
|
|
||||||
|
/** Given a path in the system, install that resource, moving it to its place in the
|
||||||
|
* instance file hierarchy.
|
||||||
|
*
|
||||||
|
* Returns whether the installation was succcessful.
|
||||||
|
*/
|
||||||
|
virtual bool installResource(QString path);
|
||||||
|
|
||||||
|
/** Uninstall (i.e. remove all data about it) a resource, given its file name.
|
||||||
|
*
|
||||||
|
* Returns whether the removal was successful.
|
||||||
|
*/
|
||||||
|
virtual bool uninstallResource(QString file_name);
|
||||||
|
virtual bool deleteResources(const QModelIndexList&);
|
||||||
|
|
||||||
|
/** Applies the given 'action' to the resources in 'indexes'.
|
||||||
|
*
|
||||||
|
* Returns whether the action was successfully applied to all resources.
|
||||||
|
*/
|
||||||
|
virtual bool setResourceEnabled(const QModelIndexList& indexes, EnableAction action);
|
||||||
|
|
||||||
|
/** Creates a new update task and start it. Returns false if no update was done, like when an update is already underway. */
|
||||||
|
virtual bool update();
|
||||||
|
|
||||||
|
/** Creates a new parse task, if needed, for 'res' and start it.*/
|
||||||
|
virtual void resolveResource(Resource::Ptr res);
|
||||||
|
|
||||||
|
[[nodiscard]] size_t size() const { return m_resources.size(); };
|
||||||
|
[[nodiscard]] bool empty() const { return size() == 0; }
|
||||||
|
[[nodiscard]] Resource& at(int index) { return *m_resources.at(index); }
|
||||||
|
[[nodiscard]] Resource const& at(int index) const { return *m_resources.at(index); }
|
||||||
|
[[nodiscard]] QList<Resource::Ptr> const& all() const { return m_resources; }
|
||||||
|
|
||||||
|
[[nodiscard]] QDir const& dir() const { return m_dir; }
|
||||||
|
|
||||||
|
/** Checks whether there's any parse tasks being done.
|
||||||
|
*
|
||||||
|
* Since they can be quite expensive, and are usually done in a separate thread, if we were to destroy the model while having
|
||||||
|
* such tasks would introduce an undefined behavior, most likely resulting in a crash.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] bool hasPendingParseTasks() const;
|
||||||
|
|
||||||
|
/* Qt behavior */
|
||||||
|
|
||||||
|
/* Basic columns */
|
||||||
|
enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, NUM_COLUMNS };
|
||||||
|
|
||||||
|
[[nodiscard]] int rowCount(const QModelIndex& = {}) const override { return size(); }
|
||||||
|
[[nodiscard]] int columnCount(const QModelIndex& = {}) const override { return NUM_COLUMNS; };
|
||||||
|
|
||||||
|
[[nodiscard]] Qt::DropActions supportedDropActions() const override;
|
||||||
|
|
||||||
|
/// flags, mostly to support drag&drop
|
||||||
|
[[nodiscard]] Qt::ItemFlags flags(const QModelIndex& index) const override;
|
||||||
|
[[nodiscard]] QStringList mimeTypes() const override;
|
||||||
|
bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override;
|
||||||
|
|
||||||
|
[[nodiscard]] bool validateIndex(const QModelIndex& index) const;
|
||||||
|
|
||||||
|
[[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||||
|
bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override;
|
||||||
|
|
||||||
|
[[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
/** This creates a proxy model to filter / sort the model for a UI.
|
||||||
|
*
|
||||||
|
* The actual comparisons and filtering are done directly by the Resource, so to modify behavior go there instead!
|
||||||
|
*/
|
||||||
|
QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] SortType columnToSortKey(size_t column) const;
|
||||||
|
|
||||||
|
class ProxyModel : public QSortFilterProxyModel {
|
||||||
|
public:
|
||||||
|
explicit ProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override;
|
||||||
|
[[nodiscard]] bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void enableInteraction(bool enabled);
|
||||||
|
void disableInteraction(bool disabled) { enableInteraction(!disabled); }
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void updateFinished();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
/** This creates a new update task to be executed by update().
|
||||||
|
*
|
||||||
|
* The task should load and parse all resources necessary, and provide a way of accessing such results.
|
||||||
|
*
|
||||||
|
* This Task is normally executed when opening a page, so it shouldn't contain much heavy work.
|
||||||
|
* If such work is needed, try using it in the Task create by createParseTask() instead!
|
||||||
|
*/
|
||||||
|
[[nodiscard]] virtual Task* createUpdateTask();
|
||||||
|
|
||||||
|
/** This creates a new parse task to be executed by onUpdateSucceeded().
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] virtual Task* createParseTask(Resource const&) { return nullptr; };
|
||||||
|
|
||||||
|
/** Standard implementation of the model update logic.
|
||||||
|
*
|
||||||
|
* It uses set operations to find differences between the current state and the updated state,
|
||||||
|
* to act only on those disparities.
|
||||||
|
*
|
||||||
|
* The implementation is at the end of this header.
|
||||||
|
*/
|
||||||
|
template <typename T>
|
||||||
|
void applyUpdates(QSet<QString>& current_set, QSet<QString>& new_set, QMap<QString, T>& new_resources);
|
||||||
|
|
||||||
|
protected slots:
|
||||||
|
void directoryChanged(QString);
|
||||||
|
|
||||||
|
/** Called when the update task is successful.
|
||||||
|
*
|
||||||
|
* This usually calls static_cast on the specific Task type returned by createUpdateTask,
|
||||||
|
* so care must be taken in such cases.
|
||||||
|
* TODO: Figure out a way to express this relationship better without templated classes (Q_OBJECT macro disallows that).
|
||||||
|
*/
|
||||||
|
virtual void onUpdateSucceeded();
|
||||||
|
virtual void onUpdateFailed() {}
|
||||||
|
|
||||||
|
/** Called when the parse task with the given ticket is successful.
|
||||||
|
*
|
||||||
|
* This is just a simple reference implementation. You probably want to override it with your own logic in a subclass
|
||||||
|
* if the resource is complex and has more stuff to parse.
|
||||||
|
*/
|
||||||
|
virtual void onParseSucceeded(int ticket, QString resource_id);
|
||||||
|
virtual void onParseFailed(int ticket, QString resource_id) {}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Represents the relationship between a column's index (represented by the list index), and it's sorting key.
|
||||||
|
// As such, the order in with they appear is very important!
|
||||||
|
QList<SortType> m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE };
|
||||||
|
|
||||||
|
bool m_can_interact = true;
|
||||||
|
|
||||||
|
QDir m_dir;
|
||||||
|
QFileSystemWatcher m_watcher;
|
||||||
|
bool m_is_watching = false;
|
||||||
|
|
||||||
|
Task::Ptr m_current_update_task = nullptr;
|
||||||
|
bool m_scheduled_update = false;
|
||||||
|
|
||||||
|
QList<Resource::Ptr> m_resources;
|
||||||
|
|
||||||
|
// Represents the relationship between a resource's internal ID and it's row position on the model.
|
||||||
|
QMap<QString, int> m_resources_index;
|
||||||
|
|
||||||
|
QMap<int, Task::Ptr> m_active_parse_tasks;
|
||||||
|
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 */
|
||||||
|
#define RESOURCE_HELPERS(T) \
|
||||||
|
[[nodiscard]] T* operator[](size_t index) \
|
||||||
|
{ \
|
||||||
|
return static_cast<T*>(m_resources[index].get()); \
|
||||||
|
} \
|
||||||
|
[[nodiscard]] T* at(size_t index) \
|
||||||
|
{ \
|
||||||
|
return static_cast<T*>(m_resources[index].get()); \
|
||||||
|
} \
|
||||||
|
[[nodiscard]] const T* at(size_t index) const \
|
||||||
|
{ \
|
||||||
|
return static_cast<const T*>(m_resources.at(index).get()); \
|
||||||
|
} \
|
||||||
|
[[nodiscard]] T* first() \
|
||||||
|
{ \
|
||||||
|
return static_cast<T*>(m_resources.first().get()); \
|
||||||
|
} \
|
||||||
|
[[nodiscard]] T* last() \
|
||||||
|
{ \
|
||||||
|
return static_cast<T*>(m_resources.last().get()); \
|
||||||
|
} \
|
||||||
|
[[nodiscard]] T* find(QString id) \
|
||||||
|
{ \
|
||||||
|
auto iter = std::find_if(m_resources.constBegin(), m_resources.constEnd(), \
|
||||||
|
[&](Resource::Ptr const& r) { return r->internal_id() == id; }); \
|
||||||
|
if (iter == m_resources.constEnd()) \
|
||||||
|
return nullptr; \
|
||||||
|
return static_cast<T*>((*iter).get()); \
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Template definition to avoid some code duplication */
|
||||||
|
template <typename T>
|
||||||
|
void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>& new_set, QMap<QString, T>& new_resources)
|
||||||
|
{
|
||||||
|
// see if the kept resources changed in some way
|
||||||
|
{
|
||||||
|
QSet<QString> kept_set = current_set;
|
||||||
|
kept_set.intersect(new_set);
|
||||||
|
|
||||||
|
for (auto const& kept : kept_set) {
|
||||||
|
auto row_it = m_resources_index.constFind(kept);
|
||||||
|
Q_ASSERT(row_it != m_resources_index.constEnd());
|
||||||
|
auto row = row_it.value();
|
||||||
|
|
||||||
|
auto& new_resource = new_resources[kept];
|
||||||
|
auto const& current_resource = m_resources[row];
|
||||||
|
|
||||||
|
if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) {
|
||||||
|
// no significant change, ignore...
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the resource is resolving, but something about it changed, we don't want to
|
||||||
|
// continue the resolving.
|
||||||
|
if (current_resource->isResolving()) {
|
||||||
|
auto task = (*m_active_parse_tasks.find(current_resource->resolutionTicket())).get();
|
||||||
|
task->abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_resources[row].reset(new_resource);
|
||||||
|
resolveResource(m_resources.at(row));
|
||||||
|
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove resources no longer present
|
||||||
|
{
|
||||||
|
QSet<QString> removed_set = current_set;
|
||||||
|
removed_set.subtract(new_set);
|
||||||
|
|
||||||
|
QList<int> removed_rows;
|
||||||
|
for (auto& removed : removed_set)
|
||||||
|
removed_rows.append(m_resources_index[removed]);
|
||||||
|
|
||||||
|
std::sort(removed_rows.begin(), removed_rows.end(), std::greater<int>());
|
||||||
|
|
||||||
|
for (auto& removed_index : removed_rows) {
|
||||||
|
auto removed_it = m_resources.begin() + removed_index;
|
||||||
|
|
||||||
|
Q_ASSERT(removed_it != m_resources.end());
|
||||||
|
Q_ASSERT(removed_set.contains(removed_it->get()->internal_id()));
|
||||||
|
|
||||||
|
if ((*removed_it)->isResolving()) {
|
||||||
|
auto task = (*m_active_parse_tasks.find((*removed_it)->resolutionTicket())).get();
|
||||||
|
task->abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
beginRemoveRows(QModelIndex(), removed_index, removed_index);
|
||||||
|
m_resources.erase(removed_it);
|
||||||
|
endRemoveRows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add new resources to the end
|
||||||
|
{
|
||||||
|
QSet<QString> added_set = new_set;
|
||||||
|
added_set.subtract(current_set);
|
||||||
|
|
||||||
|
// When you have a Qt build with assertions turned on, proceeding here will abort the application
|
||||||
|
if (added_set.size() > 0) {
|
||||||
|
beginInsertRows(QModelIndex(), m_resources.size(), m_resources.size() + added_set.size() - 1);
|
||||||
|
|
||||||
|
for (auto& added : added_set) {
|
||||||
|
auto res = new_resources[added];
|
||||||
|
m_resources.append(res);
|
||||||
|
resolveResource(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
endInsertRows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update index
|
||||||
|
{
|
||||||
|
m_resources_index.clear();
|
||||||
|
int idx = 0;
|
||||||
|
for (auto const& mod : m_resources) {
|
||||||
|
m_resources_index[mod->internal_id()] = idx;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
275
launcher/minecraft/mod/ResourceFolderModel_test.cpp
Normal file
275
launcher/minecraft/mod/ResourceFolderModel_test.cpp
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
/*
|
||||||
|
* PolyMC - Minecraft Launcher
|
||||||
|
* 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
|
||||||
|
* 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/>.
|
||||||
|
*
|
||||||
|
* 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 <QTest>
|
||||||
|
#include <QTemporaryDir>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#include "FileSystem.h"
|
||||||
|
|
||||||
|
#include "minecraft/mod/ModFolderModel.h"
|
||||||
|
#include "minecraft/mod/ResourceFolderModel.h"
|
||||||
|
|
||||||
|
#define EXEC_UPDATE_TASK(EXEC, VERIFY) \
|
||||||
|
QEventLoop loop; \
|
||||||
|
\
|
||||||
|
connect(&model, &ResourceFolderModel::updateFinished, &loop, &QEventLoop::quit); \
|
||||||
|
\
|
||||||
|
QTimer expire_timer; \
|
||||||
|
expire_timer.callOnTimeout(&loop, &QEventLoop::quit); \
|
||||||
|
expire_timer.setSingleShot(true); \
|
||||||
|
expire_timer.start(4000); \
|
||||||
|
\
|
||||||
|
VERIFY(EXEC); \
|
||||||
|
loop.exec(); \
|
||||||
|
\
|
||||||
|
QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); \
|
||||||
|
expire_timer.stop(); \
|
||||||
|
\
|
||||||
|
disconnect(&model, nullptr, nullptr, nullptr);
|
||||||
|
|
||||||
|
class ResourceFolderModelTest : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private
|
||||||
|
slots:
|
||||||
|
// test for GH-1178 - install a folder with files to a mod list
|
||||||
|
void test_1178()
|
||||||
|
{
|
||||||
|
// source
|
||||||
|
QString source = QFINDTESTDATA("testdata/test_folder");
|
||||||
|
|
||||||
|
// sanity check
|
||||||
|
QVERIFY(!source.endsWith('/'));
|
||||||
|
|
||||||
|
auto verify = [](QString path)
|
||||||
|
{
|
||||||
|
QDir target_dir(FS::PathCombine(path, "test_folder"));
|
||||||
|
QVERIFY(target_dir.entryList().contains("pack.mcmeta"));
|
||||||
|
QVERIFY(target_dir.entryList().contains("assets"));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. test with no trailing /
|
||||||
|
{
|
||||||
|
QString folder = source;
|
||||||
|
QTemporaryDir tempDir;
|
||||||
|
|
||||||
|
QEventLoop loop;
|
||||||
|
|
||||||
|
ModFolderModel m(tempDir.path(), true);
|
||||||
|
|
||||||
|
connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit);
|
||||||
|
|
||||||
|
QTimer expire_timer;
|
||||||
|
expire_timer.callOnTimeout(&loop, &QEventLoop::quit);
|
||||||
|
expire_timer.setSingleShot(true);
|
||||||
|
expire_timer.start(4000);
|
||||||
|
|
||||||
|
m.installMod(folder);
|
||||||
|
|
||||||
|
loop.exec();
|
||||||
|
|
||||||
|
QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished.");
|
||||||
|
expire_timer.stop();
|
||||||
|
|
||||||
|
verify(tempDir.path());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. test with trailing /
|
||||||
|
{
|
||||||
|
QString folder = source + '/';
|
||||||
|
QTemporaryDir tempDir;
|
||||||
|
QEventLoop loop;
|
||||||
|
ModFolderModel m(tempDir.path(), true);
|
||||||
|
|
||||||
|
connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit);
|
||||||
|
|
||||||
|
QTimer expire_timer;
|
||||||
|
expire_timer.callOnTimeout(&loop, &QEventLoop::quit);
|
||||||
|
expire_timer.setSingleShot(true);
|
||||||
|
expire_timer.start(4000);
|
||||||
|
|
||||||
|
m.installMod(folder);
|
||||||
|
|
||||||
|
loop.exec();
|
||||||
|
|
||||||
|
QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished.");
|
||||||
|
expire_timer.stop();
|
||||||
|
|
||||||
|
verify(tempDir.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_addFromWatch()
|
||||||
|
{
|
||||||
|
QString source = QFINDTESTDATA("testdata");
|
||||||
|
|
||||||
|
ModFolderModel model(source);
|
||||||
|
|
||||||
|
QCOMPARE(model.size(), 0);
|
||||||
|
|
||||||
|
EXEC_UPDATE_TASK(model.startWatching(), )
|
||||||
|
|
||||||
|
for (auto mod : model.allMods())
|
||||||
|
qDebug() << mod->name();
|
||||||
|
|
||||||
|
QCOMPARE(model.size(), 2);
|
||||||
|
|
||||||
|
model.stopWatching();
|
||||||
|
|
||||||
|
while (model.hasPendingParseTasks()) {
|
||||||
|
QTest::qSleep(20);
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_removeResource()
|
||||||
|
{
|
||||||
|
QString folder_resource = QFINDTESTDATA("testdata/test_folder");
|
||||||
|
QString file_mod = QFINDTESTDATA("testdata/supercoolmod.jar");
|
||||||
|
|
||||||
|
QTemporaryDir tmp;
|
||||||
|
|
||||||
|
ResourceFolderModel model(QDir(tmp.path()));
|
||||||
|
|
||||||
|
QCOMPARE(model.size(), 0);
|
||||||
|
|
||||||
|
{
|
||||||
|
EXEC_UPDATE_TASK(model.installResource(file_mod), QVERIFY)
|
||||||
|
}
|
||||||
|
|
||||||
|
QCOMPARE(model.size(), 1);
|
||||||
|
qDebug() << "Added first mod.";
|
||||||
|
|
||||||
|
{
|
||||||
|
EXEC_UPDATE_TASK(model.startWatching(), )
|
||||||
|
}
|
||||||
|
|
||||||
|
QCOMPARE(model.size(), 1);
|
||||||
|
qDebug() << "Started watching the temp folder.";
|
||||||
|
|
||||||
|
{
|
||||||
|
EXEC_UPDATE_TASK(model.installResource(folder_resource), QVERIFY)
|
||||||
|
}
|
||||||
|
|
||||||
|
QCOMPARE(model.size(), 2);
|
||||||
|
qDebug() << "Added second mod.";
|
||||||
|
|
||||||
|
{
|
||||||
|
EXEC_UPDATE_TASK(model.uninstallResource("supercoolmod.jar"), QVERIFY);
|
||||||
|
}
|
||||||
|
|
||||||
|
QCOMPARE(model.size(), 1);
|
||||||
|
qDebug() << "Removed first mod.";
|
||||||
|
|
||||||
|
QString mod_file_name {model.at(0).fileinfo().fileName()};
|
||||||
|
QVERIFY(!mod_file_name.isEmpty());
|
||||||
|
|
||||||
|
{
|
||||||
|
EXEC_UPDATE_TASK(model.uninstallResource(mod_file_name), QVERIFY);
|
||||||
|
}
|
||||||
|
|
||||||
|
QCOMPARE(model.size(), 0);
|
||||||
|
qDebug() << "Removed second mod.";
|
||||||
|
|
||||||
|
model.stopWatching();
|
||||||
|
|
||||||
|
while (model.hasPendingParseTasks()) {
|
||||||
|
QTest::qSleep(20);
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_enable_disable()
|
||||||
|
{
|
||||||
|
QString folder_resource = QFINDTESTDATA("testdata/test_folder");
|
||||||
|
QString file_mod = QFINDTESTDATA("testdata/supercoolmod.jar");
|
||||||
|
|
||||||
|
QTemporaryDir tmp;
|
||||||
|
ResourceFolderModel model(tmp.path());
|
||||||
|
|
||||||
|
QCOMPARE(model.size(), 0);
|
||||||
|
|
||||||
|
{
|
||||||
|
EXEC_UPDATE_TASK(model.installResource(folder_resource), QVERIFY)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
EXEC_UPDATE_TASK(model.installResource(file_mod), QVERIFY)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto res : model.all())
|
||||||
|
qDebug() << res->name();
|
||||||
|
|
||||||
|
QCOMPARE(model.size(), 2);
|
||||||
|
|
||||||
|
auto& res_1 = model.at(0).type() != ResourceType::FOLDER ? model.at(0) : model.at(1);
|
||||||
|
auto& res_2 = model.at(0).type() == ResourceType::FOLDER ? model.at(0) : model.at(1);
|
||||||
|
auto id_1 = res_1.internal_id();
|
||||||
|
auto id_2 = res_2.internal_id();
|
||||||
|
bool initial_enabled_res_2 = res_2.enabled();
|
||||||
|
bool initial_enabled_res_1 = res_1.enabled();
|
||||||
|
|
||||||
|
QVERIFY(res_1.type() != ResourceType::FOLDER && res_1.type() != ResourceType::UNKNOWN);
|
||||||
|
qDebug() << "res_1 is of the correct type.";
|
||||||
|
QVERIFY(res_1.enabled());
|
||||||
|
qDebug() << "res_1 is initially enabled.";
|
||||||
|
|
||||||
|
QVERIFY(res_1.enable(EnableAction::TOGGLE));
|
||||||
|
|
||||||
|
QVERIFY(res_1.enabled() == !initial_enabled_res_1);
|
||||||
|
qDebug() << "res_1 got successfully toggled.";
|
||||||
|
|
||||||
|
QVERIFY(res_1.enable(EnableAction::TOGGLE));
|
||||||
|
qDebug() << "res_1 got successfully toggled again.";
|
||||||
|
|
||||||
|
QVERIFY(res_1.enabled() == initial_enabled_res_1);
|
||||||
|
QVERIFY(res_1.internal_id() == id_1);
|
||||||
|
qDebug() << "res_1 got back to its initial state.";
|
||||||
|
|
||||||
|
QVERIFY(!res_2.enable(initial_enabled_res_2 ? EnableAction::ENABLE : EnableAction::DISABLE));
|
||||||
|
QVERIFY(res_2.enabled() == initial_enabled_res_2);
|
||||||
|
QVERIFY(res_2.internal_id() == id_2);
|
||||||
|
|
||||||
|
while (model.hasPendingParseTasks()) {
|
||||||
|
QTest::qSleep(20);
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QTEST_GUILESS_MAIN(ResourceFolderModelTest)
|
||||||
|
|
||||||
|
#include "ResourceFolderModel_test.moc"
|
13
launcher/minecraft/mod/ResourcePack.h
Normal file
13
launcher/minecraft/mod/ResourcePack.h
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Resource.h"
|
||||||
|
|
||||||
|
class ResourcePack : public Resource {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
using Ptr = shared_qobject_ptr<Resource>;
|
||||||
|
|
||||||
|
ResourcePack(QObject* parent = nullptr) : Resource(parent) {}
|
||||||
|
ResourcePack(QFileInfo file_info) : Resource(file_info) {}
|
||||||
|
|
||||||
|
};
|
@ -35,24 +35,4 @@
|
|||||||
|
|
||||||
#include "ResourcePackFolderModel.h"
|
#include "ResourcePackFolderModel.h"
|
||||||
|
|
||||||
ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir) {
|
ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {}
|
||||||
}
|
|
||||||
|
|
||||||
QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const {
|
|
||||||
if (role == Qt::ToolTipRole) {
|
|
||||||
switch (section) {
|
|
||||||
case ActiveColumn:
|
|
||||||
return tr("Is the resource pack enabled?");
|
|
||||||
case NameColumn:
|
|
||||||
return tr("The name of the resource pack.");
|
|
||||||
case VersionColumn:
|
|
||||||
return tr("The version of the resource pack.");
|
|
||||||
case DateColumn:
|
|
||||||
return tr("The date and time this resource pack was last changed (or added).");
|
|
||||||
default:
|
|
||||||
return QVariant();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ModFolderModel::headerData(section, orientation, role);
|
|
||||||
}
|
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ModFolderModel.h"
|
#include "ResourceFolderModel.h"
|
||||||
|
|
||||||
class ResourcePackFolderModel : public ModFolderModel
|
#include "ResourcePack.h"
|
||||||
|
|
||||||
|
class ResourcePackFolderModel : public ResourceFolderModel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ResourcePackFolderModel(const QString &dir);
|
explicit ResourcePackFolderModel(const QString &dir);
|
||||||
|
|
||||||
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
|
RESOURCE_HELPERS(ResourcePack)
|
||||||
};
|
};
|
||||||
|
10
launcher/minecraft/mod/ShaderPackFolderModel.h
Normal file
10
launcher/minecraft/mod/ShaderPackFolderModel.h
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ResourceFolderModel.h"
|
||||||
|
|
||||||
|
class ShaderPackFolderModel : public ResourceFolderModel {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ShaderPackFolderModel(const QString& dir) : ResourceFolderModel(QDir(dir)) {}
|
||||||
|
};
|
@ -35,24 +35,4 @@
|
|||||||
|
|
||||||
#include "TexturePackFolderModel.h"
|
#include "TexturePackFolderModel.h"
|
||||||
|
|
||||||
TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(dir) {
|
TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {}
|
||||||
}
|
|
||||||
|
|
||||||
QVariant TexturePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const {
|
|
||||||
if (role == Qt::ToolTipRole) {
|
|
||||||
switch (section) {
|
|
||||||
case ActiveColumn:
|
|
||||||
return tr("Is the texture pack enabled?");
|
|
||||||
case NameColumn:
|
|
||||||
return tr("The name of the texture pack.");
|
|
||||||
case VersionColumn:
|
|
||||||
return tr("The version of the texture pack.");
|
|
||||||
case DateColumn:
|
|
||||||
return tr("The date and time this texture pack was last changed (or added).");
|
|
||||||
default:
|
|
||||||
return QVariant();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ModFolderModel::headerData(section, orientation, role);
|
|
||||||
}
|
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ModFolderModel.h"
|
#include "ResourceFolderModel.h"
|
||||||
|
|
||||||
class TexturePackFolderModel : public ModFolderModel
|
class TexturePackFolderModel : public ResourceFolderModel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit TexturePackFolderModel(const QString &dir);
|
explicit TexturePackFolderModel(const QString &dir);
|
||||||
|
|
||||||
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
|
|
||||||
};
|
};
|
||||||
|
53
launcher/minecraft/mod/tasks/BasicFolderLoadTask.h
Normal file
53
launcher/minecraft/mod/tasks/BasicFolderLoadTask.h
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "minecraft/mod/Resource.h"
|
||||||
|
|
||||||
|
#include "tasks/Task.h"
|
||||||
|
|
||||||
|
/** Very simple task that just loads a folder's contents directly.
|
||||||
|
*/
|
||||||
|
class BasicFolderLoadTask : public Task
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
struct Result {
|
||||||
|
QMap<QString, Resource::Ptr> resources;
|
||||||
|
};
|
||||||
|
using ResultPtr = std::shared_ptr<Result>;
|
||||||
|
|
||||||
|
[[nodiscard]] ResultPtr result() const {
|
||||||
|
return m_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result) {}
|
||||||
|
|
||||||
|
[[nodiscard]] bool canAbort() const override { return true; }
|
||||||
|
bool abort() override { m_aborted = true; return true; }
|
||||||
|
|
||||||
|
void executeTask() override
|
||||||
|
{
|
||||||
|
m_dir.refresh();
|
||||||
|
for (auto entry : m_dir.entryInfoList()) {
|
||||||
|
auto resource = new Resource(entry);
|
||||||
|
m_result->resources.insert(resource->internal_id(), resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_aborted)
|
||||||
|
emitAborted();
|
||||||
|
else
|
||||||
|
emitSucceeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QDir m_dir;
|
||||||
|
ResultPtr m_result;
|
||||||
|
|
||||||
|
bool m_aborted = false;
|
||||||
|
};
|
@ -20,22 +20,22 @@ namespace {
|
|||||||
|
|
||||||
// OLD format:
|
// OLD format:
|
||||||
// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc
|
// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc
|
||||||
std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
|
ModDetails ReadMCModInfo(QByteArray contents)
|
||||||
{
|
{
|
||||||
auto getInfoFromArray = [&](QJsonArray arr)->std::shared_ptr<ModDetails>
|
auto getInfoFromArray = [&](QJsonArray arr) -> ModDetails
|
||||||
{
|
{
|
||||||
if (!arr.at(0).isObject()) {
|
if (!arr.at(0).isObject()) {
|
||||||
return nullptr;
|
return {};
|
||||||
}
|
}
|
||||||
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
|
ModDetails details;
|
||||||
auto firstObj = arr.at(0).toObject();
|
auto firstObj = arr.at(0).toObject();
|
||||||
details->mod_id = firstObj.value("modid").toString();
|
details.mod_id = firstObj.value("modid").toString();
|
||||||
auto name = firstObj.value("name").toString();
|
auto name = firstObj.value("name").toString();
|
||||||
// NOTE: ignore stupid example mods copies where the author didn't even bother to change the name
|
// NOTE: ignore stupid example mods copies where the author didn't even bother to change the name
|
||||||
if(name != "Example Mod") {
|
if(name != "Example Mod") {
|
||||||
details->name = name;
|
details.name = name;
|
||||||
}
|
}
|
||||||
details->version = firstObj.value("version").toString();
|
details.version = firstObj.value("version").toString();
|
||||||
auto homeurl = firstObj.value("url").toString().trimmed();
|
auto homeurl = firstObj.value("url").toString().trimmed();
|
||||||
if(!homeurl.isEmpty())
|
if(!homeurl.isEmpty())
|
||||||
{
|
{
|
||||||
@ -45,8 +45,8 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
|
|||||||
homeurl.prepend("http://");
|
homeurl.prepend("http://");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
details->homeurl = homeurl;
|
details.homeurl = homeurl;
|
||||||
details->description = firstObj.value("description").toString();
|
details.description = firstObj.value("description").toString();
|
||||||
QJsonArray authors = firstObj.value("authorList").toArray();
|
QJsonArray authors = firstObj.value("authorList").toArray();
|
||||||
if (authors.size() == 0) {
|
if (authors.size() == 0) {
|
||||||
// FIXME: what is the format of this? is there any?
|
// FIXME: what is the format of this? is there any?
|
||||||
@ -55,7 +55,7 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
|
|||||||
|
|
||||||
for (auto author: authors)
|
for (auto author: authors)
|
||||||
{
|
{
|
||||||
details->authors.append(author.toString());
|
details.authors.append(author.toString());
|
||||||
}
|
}
|
||||||
return details;
|
return details;
|
||||||
};
|
};
|
||||||
@ -83,7 +83,7 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
|
|||||||
{
|
{
|
||||||
qCritical() << "BAD stuff happened to mod json:";
|
qCritical() << "BAD stuff happened to mod json:";
|
||||||
qCritical() << contents;
|
qCritical() << contents;
|
||||||
return nullptr;
|
return {};
|
||||||
}
|
}
|
||||||
auto arrVal = jsonDoc.object().value("modlist");
|
auto arrVal = jsonDoc.object().value("modlist");
|
||||||
if(arrVal.isUndefined()) {
|
if(arrVal.isUndefined()) {
|
||||||
@ -94,13 +94,13 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
|
|||||||
return getInfoFromArray(arrVal.toArray());
|
return getInfoFromArray(arrVal.toArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nullptr;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md
|
// https://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md
|
||||||
std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
|
ModDetails ReadMCModTOML(QByteArray contents)
|
||||||
{
|
{
|
||||||
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
|
ModDetails details;
|
||||||
|
|
||||||
char errbuf[200];
|
char errbuf[200];
|
||||||
// top-level table
|
// top-level table
|
||||||
@ -108,7 +108,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
|
|||||||
|
|
||||||
if(!tomlData)
|
if(!tomlData)
|
||||||
{
|
{
|
||||||
return nullptr;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// array defined by [[mods]]
|
// array defined by [[mods]]
|
||||||
@ -116,7 +116,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
|
|||||||
if(!tomlModsArr)
|
if(!tomlModsArr)
|
||||||
{
|
{
|
||||||
qWarning() << "Corrupted mods.toml? Couldn't find [[mods]] array!";
|
qWarning() << "Corrupted mods.toml? Couldn't find [[mods]] array!";
|
||||||
return nullptr;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// we only really care about the first element, since multiple mods in one file is not supported by us at the moment
|
// we only really care about the first element, since multiple mods in one file is not supported by us at the moment
|
||||||
@ -124,33 +124,33 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
|
|||||||
if(!tomlModsTable0)
|
if(!tomlModsTable0)
|
||||||
{
|
{
|
||||||
qWarning() << "Corrupted mods.toml? [[mods]] didn't have an element at index 0!";
|
qWarning() << "Corrupted mods.toml? [[mods]] didn't have an element at index 0!";
|
||||||
return nullptr;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// mandatory properties - always in [[mods]]
|
// mandatory properties - always in [[mods]]
|
||||||
toml_datum_t modIdDatum = toml_string_in(tomlModsTable0, "modId");
|
toml_datum_t modIdDatum = toml_string_in(tomlModsTable0, "modId");
|
||||||
if(modIdDatum.ok)
|
if(modIdDatum.ok)
|
||||||
{
|
{
|
||||||
details->mod_id = modIdDatum.u.s;
|
details.mod_id = modIdDatum.u.s;
|
||||||
// library says this is required for strings
|
// library says this is required for strings
|
||||||
free(modIdDatum.u.s);
|
free(modIdDatum.u.s);
|
||||||
}
|
}
|
||||||
toml_datum_t versionDatum = toml_string_in(tomlModsTable0, "version");
|
toml_datum_t versionDatum = toml_string_in(tomlModsTable0, "version");
|
||||||
if(versionDatum.ok)
|
if(versionDatum.ok)
|
||||||
{
|
{
|
||||||
details->version = versionDatum.u.s;
|
details.version = versionDatum.u.s;
|
||||||
free(versionDatum.u.s);
|
free(versionDatum.u.s);
|
||||||
}
|
}
|
||||||
toml_datum_t displayNameDatum = toml_string_in(tomlModsTable0, "displayName");
|
toml_datum_t displayNameDatum = toml_string_in(tomlModsTable0, "displayName");
|
||||||
if(displayNameDatum.ok)
|
if(displayNameDatum.ok)
|
||||||
{
|
{
|
||||||
details->name = displayNameDatum.u.s;
|
details.name = displayNameDatum.u.s;
|
||||||
free(displayNameDatum.u.s);
|
free(displayNameDatum.u.s);
|
||||||
}
|
}
|
||||||
toml_datum_t descriptionDatum = toml_string_in(tomlModsTable0, "description");
|
toml_datum_t descriptionDatum = toml_string_in(tomlModsTable0, "description");
|
||||||
if(descriptionDatum.ok)
|
if(descriptionDatum.ok)
|
||||||
{
|
{
|
||||||
details->description = descriptionDatum.u.s;
|
details.description = descriptionDatum.u.s;
|
||||||
free(descriptionDatum.u.s);
|
free(descriptionDatum.u.s);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +173,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
|
|||||||
}
|
}
|
||||||
if(!authors.isEmpty())
|
if(!authors.isEmpty())
|
||||||
{
|
{
|
||||||
details->authors.append(authors);
|
details.authors.append(authors);
|
||||||
}
|
}
|
||||||
|
|
||||||
toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL");
|
toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL");
|
||||||
@ -200,7 +200,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
|
|||||||
homeurl.prepend("http://");
|
homeurl.prepend("http://");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
details->homeurl = homeurl;
|
details.homeurl = homeurl;
|
||||||
|
|
||||||
// this seems to be recursive, so it should free everything
|
// this seems to be recursive, so it should free everything
|
||||||
toml_free(tomlData);
|
toml_free(tomlData);
|
||||||
@ -209,20 +209,20 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://fabricmc.net/wiki/documentation:fabric_mod_json
|
// https://fabricmc.net/wiki/documentation:fabric_mod_json
|
||||||
std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
|
ModDetails ReadFabricModInfo(QByteArray contents)
|
||||||
{
|
{
|
||||||
QJsonParseError jsonError;
|
QJsonParseError jsonError;
|
||||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
|
||||||
auto object = jsonDoc.object();
|
auto object = jsonDoc.object();
|
||||||
auto schemaVersion = object.contains("schemaVersion") ? object.value("schemaVersion").toInt(0) : 0;
|
auto schemaVersion = object.contains("schemaVersion") ? object.value("schemaVersion").toInt(0) : 0;
|
||||||
|
|
||||||
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
|
ModDetails details;
|
||||||
|
|
||||||
details->mod_id = object.value("id").toString();
|
details.mod_id = object.value("id").toString();
|
||||||
details->version = object.value("version").toString();
|
details.version = object.value("version").toString();
|
||||||
|
|
||||||
details->name = object.contains("name") ? object.value("name").toString() : details->mod_id;
|
details.name = object.contains("name") ? object.value("name").toString() : details.mod_id;
|
||||||
details->description = object.value("description").toString();
|
details.description = object.value("description").toString();
|
||||||
|
|
||||||
if (schemaVersion >= 1)
|
if (schemaVersion >= 1)
|
||||||
{
|
{
|
||||||
@ -230,10 +230,10 @@ std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
|
|||||||
for (auto author: authors)
|
for (auto author: authors)
|
||||||
{
|
{
|
||||||
if(author.isObject()) {
|
if(author.isObject()) {
|
||||||
details->authors.append(author.toObject().value("name").toString());
|
details.authors.append(author.toObject().value("name").toString());
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
details->authors.append(author.toString());
|
details.authors.append(author.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,7 +243,7 @@ std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
|
|||||||
|
|
||||||
if (contact.contains("homepage"))
|
if (contact.contains("homepage"))
|
||||||
{
|
{
|
||||||
details->homeurl = contact.value("homepage").toString();
|
details.homeurl = contact.value("homepage").toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -251,50 +251,50 @@ std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md
|
// https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md
|
||||||
std::shared_ptr<ModDetails> ReadQuiltModInfo(QByteArray contents)
|
ModDetails ReadQuiltModInfo(QByteArray contents)
|
||||||
{
|
{
|
||||||
QJsonParseError jsonError;
|
QJsonParseError jsonError;
|
||||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
|
||||||
auto object = Json::requireObject(jsonDoc, "quilt.mod.json");
|
auto object = Json::requireObject(jsonDoc, "quilt.mod.json");
|
||||||
auto schemaVersion = Json::ensureInteger(object.value("schema_version"), 0, "Quilt schema_version");
|
auto schemaVersion = Json::ensureInteger(object.value("schema_version"), 0, "Quilt schema_version");
|
||||||
|
|
||||||
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
|
ModDetails details;
|
||||||
|
|
||||||
// https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md
|
// https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md
|
||||||
if (schemaVersion == 1)
|
if (schemaVersion == 1)
|
||||||
{
|
{
|
||||||
auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info");
|
auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info");
|
||||||
|
|
||||||
details->mod_id = Json::requireString(modInfo.value("id"), "Mod ID");
|
details.mod_id = Json::requireString(modInfo.value("id"), "Mod ID");
|
||||||
details->version = Json::requireString(modInfo.value("version"), "Mod version");
|
details.version = Json::requireString(modInfo.value("version"), "Mod version");
|
||||||
|
|
||||||
auto modMetadata = Json::ensureObject(modInfo.value("metadata"));
|
auto modMetadata = Json::ensureObject(modInfo.value("metadata"));
|
||||||
|
|
||||||
details->name = Json::ensureString(modMetadata.value("name"), details->mod_id);
|
details.name = Json::ensureString(modMetadata.value("name"), details.mod_id);
|
||||||
details->description = Json::ensureString(modMetadata.value("description"));
|
details.description = Json::ensureString(modMetadata.value("description"));
|
||||||
|
|
||||||
auto modContributors = Json::ensureObject(modMetadata.value("contributors"));
|
auto modContributors = Json::ensureObject(modMetadata.value("contributors"));
|
||||||
|
|
||||||
// We don't really care about the role of a contributor here
|
// We don't really care about the role of a contributor here
|
||||||
details->authors += modContributors.keys();
|
details.authors += modContributors.keys();
|
||||||
|
|
||||||
auto modContact = Json::ensureObject(modMetadata.value("contact"));
|
auto modContact = Json::ensureObject(modMetadata.value("contact"));
|
||||||
|
|
||||||
if (modContact.contains("homepage"))
|
if (modContact.contains("homepage"))
|
||||||
{
|
{
|
||||||
details->homeurl = Json::requireString(modContact.value("homepage"));
|
details.homeurl = Json::requireString(modContact.value("homepage"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return details;
|
return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<ModDetails> ReadForgeInfo(QByteArray contents)
|
ModDetails ReadForgeInfo(QByteArray contents)
|
||||||
{
|
{
|
||||||
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
|
ModDetails details;
|
||||||
// Read the data
|
// Read the data
|
||||||
details->name = "Minecraft Forge";
|
details.name = "Minecraft Forge";
|
||||||
details->mod_id = "Forge";
|
details.mod_id = "Forge";
|
||||||
details->homeurl = "http://www.minecraftforge.net/forum/";
|
details.homeurl = "http://www.minecraftforge.net/forum/";
|
||||||
INIFile ini;
|
INIFile ini;
|
||||||
if (!ini.loadFile(contents))
|
if (!ini.loadFile(contents))
|
||||||
return details;
|
return details;
|
||||||
@ -304,47 +304,47 @@ std::shared_ptr<ModDetails> ReadForgeInfo(QByteArray contents)
|
|||||||
QString revision = ini.get("forge.revision.number", "0").toString();
|
QString revision = ini.get("forge.revision.number", "0").toString();
|
||||||
QString build = ini.get("forge.build.number", "0").toString();
|
QString build = ini.get("forge.build.number", "0").toString();
|
||||||
|
|
||||||
details->version = major + "." + minor + "." + revision + "." + build;
|
details.version = major + "." + minor + "." + revision + "." + build;
|
||||||
return details;
|
return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<ModDetails> ReadLiteModInfo(QByteArray contents)
|
ModDetails ReadLiteModInfo(QByteArray contents)
|
||||||
{
|
{
|
||||||
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
|
ModDetails details;
|
||||||
QJsonParseError jsonError;
|
QJsonParseError jsonError;
|
||||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
|
||||||
auto object = jsonDoc.object();
|
auto object = jsonDoc.object();
|
||||||
if (object.contains("name"))
|
if (object.contains("name"))
|
||||||
{
|
{
|
||||||
details->mod_id = details->name = object.value("name").toString();
|
details.mod_id = details.name = object.value("name").toString();
|
||||||
}
|
}
|
||||||
if (object.contains("version"))
|
if (object.contains("version"))
|
||||||
{
|
{
|
||||||
details->version = object.value("version").toString("");
|
details.version = object.value("version").toString("");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
details->version = object.value("revision").toString("");
|
details.version = object.value("revision").toString("");
|
||||||
}
|
}
|
||||||
details->mcversion = object.value("mcversion").toString();
|
details.mcversion = object.value("mcversion").toString();
|
||||||
auto author = object.value("author").toString();
|
auto author = object.value("author").toString();
|
||||||
if(!author.isEmpty()) {
|
if(!author.isEmpty()) {
|
||||||
details->authors.append(author);
|
details.authors.append(author);
|
||||||
}
|
}
|
||||||
details->description = object.value("description").toString();
|
details.description = object.value("description").toString();
|
||||||
details->homeurl = object.value("url").toString();
|
details.homeurl = object.value("url").toString();
|
||||||
return details;
|
return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalModParseTask::LocalModParseTask(int token, Mod::ModType type, const QFileInfo& modFile):
|
LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile):
|
||||||
|
Task(nullptr, false),
|
||||||
m_token(token),
|
m_token(token),
|
||||||
m_type(type),
|
m_type(type),
|
||||||
m_modFile(modFile),
|
m_modFile(modFile),
|
||||||
m_result(new Result())
|
m_result(new Result())
|
||||||
{
|
{}
|
||||||
}
|
|
||||||
|
|
||||||
void LocalModParseTask::processAsZip()
|
void LocalModParseTask::processAsZip()
|
||||||
{
|
{
|
||||||
@ -366,7 +366,7 @@ void LocalModParseTask::processAsZip()
|
|||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
// to replace ${file.jarVersion} with the actual version, as needed
|
// to replace ${file.jarVersion} with the actual version, as needed
|
||||||
if (m_result->details && m_result->details->version == "${file.jarVersion}")
|
if (m_result->details.version == "${file.jarVersion}")
|
||||||
{
|
{
|
||||||
if (zip.setCurrentFile("META-INF/MANIFEST.MF"))
|
if (zip.setCurrentFile("META-INF/MANIFEST.MF"))
|
||||||
{
|
{
|
||||||
@ -395,7 +395,7 @@ void LocalModParseTask::processAsZip()
|
|||||||
manifestVersion = "NONE";
|
manifestVersion = "NONE";
|
||||||
}
|
}
|
||||||
|
|
||||||
m_result->details->version = manifestVersion;
|
m_result->details.version = manifestVersion;
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
}
|
}
|
||||||
@ -497,21 +497,31 @@ void LocalModParseTask::processAsLitemod()
|
|||||||
zip.close();
|
zip.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
void LocalModParseTask::run()
|
bool LocalModParseTask::abort()
|
||||||
|
{
|
||||||
|
m_aborted = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocalModParseTask::executeTask()
|
||||||
{
|
{
|
||||||
switch(m_type)
|
switch(m_type)
|
||||||
{
|
{
|
||||||
case Mod::MOD_ZIPFILE:
|
case ResourceType::ZIPFILE:
|
||||||
processAsZip();
|
processAsZip();
|
||||||
break;
|
break;
|
||||||
case Mod::MOD_FOLDER:
|
case ResourceType::FOLDER:
|
||||||
processAsFolder();
|
processAsFolder();
|
||||||
break;
|
break;
|
||||||
case Mod::MOD_LITEMOD:
|
case ResourceType::LITEMOD:
|
||||||
processAsLitemod();
|
processAsLitemod();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
emit finished(m_token);
|
|
||||||
|
if (m_aborted)
|
||||||
|
emitAborted();
|
||||||
|
else
|
||||||
|
emitSucceeded();
|
||||||
}
|
}
|
||||||
|
@ -2,29 +2,31 @@
|
|||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QRunnable>
|
|
||||||
|
|
||||||
#include "minecraft/mod/Mod.h"
|
#include "minecraft/mod/Mod.h"
|
||||||
#include "minecraft/mod/ModDetails.h"
|
#include "minecraft/mod/ModDetails.h"
|
||||||
|
|
||||||
class LocalModParseTask : public QObject, public QRunnable
|
#include "tasks/Task.h"
|
||||||
|
|
||||||
|
class LocalModParseTask : public Task
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
struct Result {
|
struct Result {
|
||||||
QString id;
|
ModDetails details;
|
||||||
std::shared_ptr<ModDetails> details;
|
|
||||||
};
|
};
|
||||||
using ResultPtr = std::shared_ptr<Result>;
|
using ResultPtr = std::shared_ptr<Result>;
|
||||||
ResultPtr result() const {
|
ResultPtr result() const {
|
||||||
return m_result;
|
return m_result;
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalModParseTask(int token, Mod::ModType type, const QFileInfo & modFile);
|
[[nodiscard]] bool canAbort() const override { return true; }
|
||||||
void run();
|
bool abort() override;
|
||||||
|
|
||||||
signals:
|
LocalModParseTask(int token, ResourceType type, const QFileInfo & modFile);
|
||||||
void finished(int token);
|
void executeTask() override;
|
||||||
|
|
||||||
|
[[nodiscard]] int token() const { return m_token; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void processAsZip();
|
void processAsZip();
|
||||||
@ -33,7 +35,9 @@ private:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
int m_token;
|
int m_token;
|
||||||
Mod::ModType m_type;
|
ResourceType m_type;
|
||||||
QFileInfo m_modFile;
|
QFileInfo m_modFile;
|
||||||
ResultPtr m_result;
|
ResultPtr m_result;
|
||||||
|
|
||||||
|
bool m_aborted = false;
|
||||||
};
|
};
|
||||||
|
@ -38,11 +38,11 @@
|
|||||||
|
|
||||||
#include "minecraft/mod/MetadataHandler.h"
|
#include "minecraft/mod/MetadataHandler.h"
|
||||||
|
|
||||||
ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed, bool clean_orphan)
|
ModFolderLoadTask::ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan, QObject* parent)
|
||||||
: 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(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())
|
||||||
{}
|
{}
|
||||||
|
|
||||||
void ModFolderLoadTask::run()
|
void ModFolderLoadTask::executeTask()
|
||||||
{
|
{
|
||||||
if (m_is_indexed) {
|
if (m_is_indexed) {
|
||||||
// Read metadata first
|
// Read metadata first
|
||||||
@ -52,7 +52,7 @@ void ModFolderLoadTask::run()
|
|||||||
// Read JAR files that don't have metadata
|
// Read JAR files that don't have metadata
|
||||||
m_mods_dir.refresh();
|
m_mods_dir.refresh();
|
||||||
for (auto entry : m_mods_dir.entryInfoList()) {
|
for (auto entry : m_mods_dir.entryInfoList()) {
|
||||||
Mod::Ptr mod(new Mod(entry));
|
Mod* mod(new Mod(entry));
|
||||||
|
|
||||||
if (mod->enabled()) {
|
if (mod->enabled()) {
|
||||||
if (m_result->mods.contains(mod->internal_id())) {
|
if (m_result->mods.contains(mod->internal_id())) {
|
||||||
@ -96,7 +96,7 @@ void ModFolderLoadTask::run()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emit succeeded();
|
emitSucceeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ModFolderLoadTask::getFromMetadata()
|
void ModFolderLoadTask::getFromMetadata()
|
||||||
|
@ -42,8 +42,9 @@
|
|||||||
#include <QRunnable>
|
#include <QRunnable>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include "minecraft/mod/Mod.h"
|
#include "minecraft/mod/Mod.h"
|
||||||
|
#include "tasks/Task.h"
|
||||||
|
|
||||||
class ModFolderLoadTask : public QObject, public QRunnable
|
class ModFolderLoadTask : public Task
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
@ -56,16 +57,15 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed, bool clean_orphan = false);
|
ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan = false, QObject* parent = nullptr);
|
||||||
void run();
|
|
||||||
signals:
|
void executeTask() override;
|
||||||
void succeeded();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void getFromMetadata();
|
void getFromMetadata();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QDir& m_mods_dir, m_index_dir;
|
QDir m_mods_dir, m_index_dir;
|
||||||
bool m_is_indexed;
|
bool m_is_indexed;
|
||||||
bool m_clean_orphan;
|
bool m_clean_orphan;
|
||||||
ResultPtr m_result;
|
ResultPtr m_result;
|
||||||
|
1
launcher/minecraft/mod/testdata/supercoolmod.jar
vendored
Normal file
1
launcher/minecraft/mod/testdata/supercoolmod.jar
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
the best mod.
|
@ -102,7 +102,7 @@ void EnsureMetadataTask::executeTask()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Folders don't have metadata
|
// Folders don't have metadata
|
||||||
if (mod->type() == Mod::MOD_FOLDER) {
|
if (mod->type() == ResourceType::FOLDER) {
|
||||||
emitReady(mod);
|
emitReady(mod);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,8 +37,9 @@
|
|||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
|
||||||
Task::Task(QObject *parent) : QObject(parent)
|
Task::Task(QObject *parent, bool show_debug) : QObject(parent), m_show_debug(show_debug)
|
||||||
{
|
{
|
||||||
|
setAutoDelete(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Task::setStatus(const QString &new_status)
|
void Task::setStatus(const QString &new_status)
|
||||||
@ -63,26 +64,31 @@ void Task::start()
|
|||||||
{
|
{
|
||||||
case State::Inactive:
|
case State::Inactive:
|
||||||
{
|
{
|
||||||
|
if (m_show_debug)
|
||||||
qDebug() << "Task" << describe() << "starting for the first time";
|
qDebug() << "Task" << describe() << "starting for the first time";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case State::AbortedByUser:
|
case State::AbortedByUser:
|
||||||
{
|
{
|
||||||
|
if (m_show_debug)
|
||||||
qDebug() << "Task" << describe() << "restarting for after being aborted by user";
|
qDebug() << "Task" << describe() << "restarting for after being aborted by user";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case State::Failed:
|
case State::Failed:
|
||||||
{
|
{
|
||||||
|
if (m_show_debug)
|
||||||
qDebug() << "Task" << describe() << "restarting for after failing at first";
|
qDebug() << "Task" << describe() << "restarting for after failing at first";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case State::Succeeded:
|
case State::Succeeded:
|
||||||
{
|
{
|
||||||
|
if (m_show_debug)
|
||||||
qDebug() << "Task" << describe() << "restarting for after succeeding at first";
|
qDebug() << "Task" << describe() << "restarting for after succeeding at first";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case State::Running:
|
case State::Running:
|
||||||
{
|
{
|
||||||
|
if (m_show_debug)
|
||||||
qWarning() << "The launcher tried to start task" << describe() << "while it was already running!";
|
qWarning() << "The launcher tried to start task" << describe() << "while it was already running!";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -118,6 +124,7 @@ void Task::emitAborted()
|
|||||||
}
|
}
|
||||||
m_state = State::AbortedByUser;
|
m_state = State::AbortedByUser;
|
||||||
m_failReason = "Aborted.";
|
m_failReason = "Aborted.";
|
||||||
|
if (m_show_debug)
|
||||||
qDebug() << "Task" << describe() << "aborted.";
|
qDebug() << "Task" << describe() << "aborted.";
|
||||||
emit aborted();
|
emit aborted();
|
||||||
emit finished();
|
emit finished();
|
||||||
@ -132,6 +139,7 @@ void Task::emitSucceeded()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_state = State::Succeeded;
|
m_state = State::Succeeded;
|
||||||
|
if (m_show_debug)
|
||||||
qDebug() << "Task" << describe() << "succeeded";
|
qDebug() << "Task" << describe() << "succeeded";
|
||||||
emit succeeded();
|
emit succeeded();
|
||||||
emit finished();
|
emit finished();
|
||||||
|
@ -35,9 +35,11 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <QRunnable>
|
||||||
|
|
||||||
#include "QObjectPtr.h"
|
#include "QObjectPtr.h"
|
||||||
|
|
||||||
class Task : public QObject {
|
class Task : public QObject, public QRunnable {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
using Ptr = shared_qobject_ptr<Task>;
|
using Ptr = shared_qobject_ptr<Task>;
|
||||||
@ -45,7 +47,7 @@ class Task : public QObject {
|
|||||||
enum class State { Inactive, Running, Succeeded, Failed, AbortedByUser };
|
enum class State { Inactive, Running, Succeeded, Failed, AbortedByUser };
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Task(QObject* parent = 0);
|
explicit Task(QObject* parent = 0, bool show_debug_log = true);
|
||||||
virtual ~Task() = default;
|
virtual ~Task() = default;
|
||||||
|
|
||||||
bool isRunning() const;
|
bool isRunning() const;
|
||||||
@ -95,6 +97,9 @@ class Task : public QObject {
|
|||||||
void stepStatus(QString status);
|
void stepStatus(QString status);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
|
// QRunnable's interface
|
||||||
|
void run() override { start(); }
|
||||||
|
|
||||||
virtual void start();
|
virtual void start();
|
||||||
virtual bool abort() { if(canAbort()) emitAborted(); return canAbort(); };
|
virtual bool abort() { if(canAbort()) emitAborted(); return canAbort(); };
|
||||||
|
|
||||||
@ -117,4 +122,7 @@ class Task : public QObject {
|
|||||||
QString m_status;
|
QString m_status;
|
||||||
int m_progress = 0;
|
int m_progress = 0;
|
||||||
int m_progressTotal = 100;
|
int m_progressTotal = 100;
|
||||||
|
|
||||||
|
// TODO: Nuke in favor of QLoggingCategory
|
||||||
|
bool m_show_debug = true;
|
||||||
};
|
};
|
||||||
|
@ -36,7 +36,7 @@ static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst)
|
|||||||
ModUpdateDialog::ModUpdateDialog(QWidget* parent,
|
ModUpdateDialog::ModUpdateDialog(QWidget* parent,
|
||||||
BaseInstance* instance,
|
BaseInstance* instance,
|
||||||
const std::shared_ptr<ModFolderModel> mods,
|
const std::shared_ptr<ModFolderModel> mods,
|
||||||
QList<Mod::Ptr>& search_for)
|
QList<Mod*>& search_for)
|
||||||
: ReviewMessageBox(parent, tr("Confirm mods to update"), "")
|
: ReviewMessageBox(parent, tr("Confirm mods to update"), "")
|
||||||
, m_parent(parent)
|
, m_parent(parent)
|
||||||
, m_mod_model(mods)
|
, m_mod_model(mods)
|
||||||
@ -226,9 +226,8 @@ auto ModUpdateDialog::ensureMetadata() -> bool
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (auto candidate : m_candidates) {
|
for (auto candidate : m_candidates) {
|
||||||
auto* candidate_ptr = candidate.get();
|
|
||||||
if (candidate->status() != ModStatus::NoMetadata) {
|
if (candidate->status() != ModStatus::NoMetadata) {
|
||||||
onMetadataEnsured(candidate_ptr);
|
onMetadataEnsured(candidate);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,7 +235,7 @@ auto ModUpdateDialog::ensureMetadata() -> bool
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (confirm_rest) {
|
if (confirm_rest) {
|
||||||
addToTmp(candidate_ptr, provider_rest);
|
addToTmp(candidate, provider_rest);
|
||||||
should_try_others.insert(candidate->internal_id(), try_others_rest);
|
should_try_others.insert(candidate->internal_id(), try_others_rest);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -261,7 +260,7 @@ auto ModUpdateDialog::ensureMetadata() -> bool
|
|||||||
should_try_others.insert(candidate->internal_id(), response.try_others);
|
should_try_others.insert(candidate->internal_id(), response.try_others);
|
||||||
|
|
||||||
if (confirmed)
|
if (confirmed)
|
||||||
addToTmp(candidate_ptr, response.chosen);
|
addToTmp(candidate, response.chosen);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!modrinth_tmp.empty()) {
|
if (!modrinth_tmp.empty()) {
|
||||||
|
@ -19,7 +19,7 @@ class ModUpdateDialog final : public ReviewMessageBox {
|
|||||||
explicit ModUpdateDialog(QWidget* parent,
|
explicit ModUpdateDialog(QWidget* parent,
|
||||||
BaseInstance* instance,
|
BaseInstance* instance,
|
||||||
const std::shared_ptr<ModFolderModel> mod_model,
|
const std::shared_ptr<ModFolderModel> mod_model,
|
||||||
QList<Mod::Ptr>& search_for);
|
QList<Mod*>& search_for);
|
||||||
|
|
||||||
void checkCandidates();
|
void checkCandidates();
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ class ModUpdateDialog final : public ReviewMessageBox {
|
|||||||
|
|
||||||
const std::shared_ptr<ModFolderModel> m_mod_model;
|
const std::shared_ptr<ModFolderModel> m_mod_model;
|
||||||
|
|
||||||
QList<Mod::Ptr>& m_candidates;
|
QList<Mod*>& m_candidates;
|
||||||
QList<Mod*> m_modrinth_to_update;
|
QList<Mod*> m_modrinth_to_update;
|
||||||
QList<Mod*> m_flame_to_update;
|
QList<Mod*> m_flame_to_update;
|
||||||
|
|
||||||
|
@ -3,100 +3,13 @@
|
|||||||
|
|
||||||
#include "DesktopServices.h"
|
#include "DesktopServices.h"
|
||||||
#include "Version.h"
|
#include "Version.h"
|
||||||
#include "minecraft/mod/ModFolderModel.h"
|
#include "minecraft/mod/ResourceFolderModel.h"
|
||||||
#include "ui/GuiUtil.h"
|
#include "ui/GuiUtil.h"
|
||||||
|
|
||||||
#include <QKeyEvent>
|
#include <QKeyEvent>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
|
|
||||||
namespace {
|
ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ResourceFolderModel> model, QWidget* parent)
|
||||||
// FIXME: wasteful
|
|
||||||
void RemoveThePrefix(QString& string)
|
|
||||||
{
|
|
||||||
QRegularExpression regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption);
|
|
||||||
string.remove(regex);
|
|
||||||
string = string.trimmed();
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
class SortProxy : public QSortFilterProxyModel {
|
|
||||||
public:
|
|
||||||
explicit SortProxy(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {}
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override
|
|
||||||
{
|
|
||||||
ModFolderModel* model = qobject_cast<ModFolderModel*>(sourceModel());
|
|
||||||
if (!model)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
const auto& mod = model->at(source_row);
|
|
||||||
|
|
||||||
if (filterRegularExpression().match(mod.name()).hasMatch())
|
|
||||||
return true;
|
|
||||||
if (filterRegularExpression().match(mod.description()).hasMatch())
|
|
||||||
return true;
|
|
||||||
|
|
||||||
for (auto& author : mod.authors()) {
|
|
||||||
if (filterRegularExpression().match(author).hasMatch()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override
|
|
||||||
{
|
|
||||||
ModFolderModel* model = qobject_cast<ModFolderModel*>(sourceModel());
|
|
||||||
if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) {
|
|
||||||
return QSortFilterProxyModel::lessThan(source_left, source_right);
|
|
||||||
}
|
|
||||||
|
|
||||||
// we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and
|
|
||||||
// proceed.
|
|
||||||
|
|
||||||
auto column = (ModFolderModel::Columns) source_left.column();
|
|
||||||
bool invert = false;
|
|
||||||
switch (column) {
|
|
||||||
// GH-2550 - sort by enabled/disabled
|
|
||||||
case ModFolderModel::ActiveColumn: {
|
|
||||||
auto dataL = source_left.data(Qt::CheckStateRole).toBool();
|
|
||||||
auto dataR = source_right.data(Qt::CheckStateRole).toBool();
|
|
||||||
if (dataL != dataR)
|
|
||||||
return dataL > dataR;
|
|
||||||
|
|
||||||
// fallthrough
|
|
||||||
invert = sortOrder() == Qt::DescendingOrder;
|
|
||||||
}
|
|
||||||
// GH-2722 - sort mod names in a way that discards "The" prefixes
|
|
||||||
case ModFolderModel::NameColumn: {
|
|
||||||
auto dataL = model->data(model->index(source_left.row(), ModFolderModel::NameColumn)).toString();
|
|
||||||
RemoveThePrefix(dataL);
|
|
||||||
auto dataR = model->data(model->index(source_right.row(), ModFolderModel::NameColumn)).toString();
|
|
||||||
RemoveThePrefix(dataR);
|
|
||||||
|
|
||||||
auto less = dataL.compare(dataR, sortCaseSensitivity());
|
|
||||||
if (less != 0)
|
|
||||||
return invert ? (less > 0) : (less < 0);
|
|
||||||
|
|
||||||
// fallthrough
|
|
||||||
invert = sortOrder() == Qt::DescendingOrder;
|
|
||||||
}
|
|
||||||
// GH-2762 - sort versions by parsing them as versions
|
|
||||||
case ModFolderModel::VersionColumn: {
|
|
||||||
auto dataL = Version(model->data(model->index(source_left.row(), ModFolderModel::VersionColumn)).toString());
|
|
||||||
auto dataR = Version(model->data(model->index(source_right.row(), ModFolderModel::VersionColumn)).toString());
|
|
||||||
return invert ? (dataL > dataR) : (dataL < dataR);
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return QSortFilterProxyModel::lessThan(source_left, source_right);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ModFolderModel> model, QWidget* parent)
|
|
||||||
: QMainWindow(parent), m_instance(instance), ui(new Ui::ExternalResourcesPage), m_model(model)
|
: QMainWindow(parent), m_instance(instance), ui(new Ui::ExternalResourcesPage), m_model(model)
|
||||||
{
|
{
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
@ -105,7 +18,7 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared
|
|||||||
|
|
||||||
ui->actionsToolbar->insertSpacer(ui->actionViewConfigs);
|
ui->actionsToolbar->insertSpacer(ui->actionViewConfigs);
|
||||||
|
|
||||||
m_filterModel = new SortProxy(this);
|
m_filterModel = model->createFilterProxyModel(this);
|
||||||
m_filterModel->setDynamicSortFilter(true);
|
m_filterModel->setDynamicSortFilter(true);
|
||||||
m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||||
m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
|
m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
|
||||||
@ -137,19 +50,9 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared
|
|||||||
|
|
||||||
ExternalResourcesPage::~ExternalResourcesPage()
|
ExternalResourcesPage::~ExternalResourcesPage()
|
||||||
{
|
{
|
||||||
m_model->stopWatching();
|
|
||||||
delete ui;
|
delete ui;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ExternalResourcesPage::itemActivated(const QModelIndex&)
|
|
||||||
{
|
|
||||||
if (!m_controlsEnabled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
|
|
||||||
m_model->setModStatus(selection.indexes(), ModFolderModel::Toggle);
|
|
||||||
}
|
|
||||||
|
|
||||||
QMenu* ExternalResourcesPage::createPopupMenu()
|
QMenu* ExternalResourcesPage::createPopupMenu()
|
||||||
{
|
{
|
||||||
QMenu* filteredMenu = QMainWindow::createPopupMenu();
|
QMenu* filteredMenu = QMainWindow::createPopupMenu();
|
||||||
@ -179,6 +82,15 @@ void ExternalResourcesPage::retranslate()
|
|||||||
ui->retranslateUi(this);
|
ui->retranslateUi(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ExternalResourcesPage::itemActivated(const QModelIndex&)
|
||||||
|
{
|
||||||
|
if (!m_controlsEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
|
||||||
|
m_model->setResourceEnabled(selection.indexes(), EnableAction::TOGGLE);
|
||||||
|
}
|
||||||
|
|
||||||
void ExternalResourcesPage::filterTextChanged(const QString& newContents)
|
void ExternalResourcesPage::filterTextChanged(const QString& newContents)
|
||||||
{
|
{
|
||||||
m_viewFilter = newContents;
|
m_viewFilter = newContents;
|
||||||
@ -241,7 +153,7 @@ void ExternalResourcesPage::addItem()
|
|||||||
|
|
||||||
if (!list.isEmpty()) {
|
if (!list.isEmpty()) {
|
||||||
for (auto filename : list) {
|
for (auto filename : list) {
|
||||||
m_model->installMod(filename);
|
m_model->installResource(filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,7 +164,7 @@ void ExternalResourcesPage::removeItem()
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
|
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
|
||||||
m_model->deleteMods(selection.indexes());
|
m_model->deleteResources(selection.indexes());
|
||||||
}
|
}
|
||||||
|
|
||||||
void ExternalResourcesPage::enableItem()
|
void ExternalResourcesPage::enableItem()
|
||||||
@ -261,7 +173,7 @@ void ExternalResourcesPage::enableItem()
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
|
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
|
||||||
m_model->setModStatus(selection.indexes(), ModFolderModel::Enable);
|
m_model->setResourceEnabled(selection.indexes(), EnableAction::ENABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ExternalResourcesPage::disableItem()
|
void ExternalResourcesPage::disableItem()
|
||||||
@ -270,7 +182,7 @@ void ExternalResourcesPage::disableItem()
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
|
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
|
||||||
m_model->setModStatus(selection.indexes(), ModFolderModel::Disable);
|
m_model->setResourceEnabled(selection.indexes(), EnableAction::DISABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ExternalResourcesPage::viewConfigs()
|
void ExternalResourcesPage::viewConfigs()
|
||||||
@ -283,15 +195,23 @@ void ExternalResourcesPage::viewFolder()
|
|||||||
DesktopServices::openDirectory(m_model->dir().absolutePath(), true);
|
DesktopServices::openDirectory(m_model->dir().absolutePath(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ExternalResourcesPage::current(const QModelIndex& current, const QModelIndex& previous)
|
bool ExternalResourcesPage::current(const QModelIndex& current, const QModelIndex& previous)
|
||||||
{
|
{
|
||||||
if (!current.isValid()) {
|
if (!current.isValid()) {
|
||||||
ui->frame->clear();
|
ui->frame->clear();
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return onSelectionChanged(current, previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ExternalResourcesPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous)
|
||||||
|
{
|
||||||
auto sourceCurrent = m_filterModel->mapToSource(current);
|
auto sourceCurrent = m_filterModel->mapToSource(current);
|
||||||
int row = sourceCurrent.row();
|
int row = sourceCurrent.row();
|
||||||
Mod& m = m_model->operator[](row);
|
Resource const& resource = m_model->at(row);
|
||||||
ui->frame->updateWithMod(m);
|
ui->frame->updateWithResource(resource);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
#include "minecraft/MinecraftInstance.h"
|
#include "minecraft/MinecraftInstance.h"
|
||||||
#include "ui/pages/BasePage.h"
|
#include "ui/pages/BasePage.h"
|
||||||
|
|
||||||
class ModFolderModel;
|
class ResourceFolderModel;
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
class ExternalResourcesPage;
|
class ExternalResourcesPage;
|
||||||
@ -19,8 +19,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// FIXME: Switch to different model (or change the name of this one)
|
explicit ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ResourceFolderModel> model, QWidget* parent = nullptr);
|
||||||
explicit ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ModFolderModel> model, QWidget* parent = nullptr);
|
|
||||||
virtual ~ExternalResourcesPage();
|
virtual ~ExternalResourcesPage();
|
||||||
|
|
||||||
virtual QString displayName() const override = 0;
|
virtual QString displayName() const override = 0;
|
||||||
@ -41,7 +40,9 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
|
|||||||
QMenu* createPopupMenu() override;
|
QMenu* createPopupMenu() override;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void current(const QModelIndex& current, const QModelIndex& previous);
|
bool current(const QModelIndex& current, const QModelIndex& previous);
|
||||||
|
|
||||||
|
virtual bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous);
|
||||||
|
|
||||||
protected slots:
|
protected slots:
|
||||||
void itemActivated(const QModelIndex& index);
|
void itemActivated(const QModelIndex& index);
|
||||||
@ -63,7 +64,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
|
|||||||
BaseInstance* m_instance = nullptr;
|
BaseInstance* m_instance = nullptr;
|
||||||
|
|
||||||
Ui::ExternalResourcesPage* ui = nullptr;
|
Ui::ExternalResourcesPage* ui = nullptr;
|
||||||
std::shared_ptr<ModFolderModel> m_model;
|
std::shared_ptr<ResourceFolderModel> m_model;
|
||||||
QSortFilterProxyModel* m_filterModel = nullptr;
|
QSortFilterProxyModel* m_filterModel = nullptr;
|
||||||
|
|
||||||
QString m_fileSelectionFilter;
|
QString m_fileSelectionFilter;
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="1" colspan="3">
|
<item row="2" column="1" colspan="3">
|
||||||
<widget class="MCModInfoFrame" name="frame">
|
<widget class="InfoFrame" name="frame">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
|
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
@ -166,9 +166,9 @@
|
|||||||
<header>ui/widgets/ModListView.h</header>
|
<header>ui/widgets/ModListView.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>MCModInfoFrame</class>
|
<class>InfoFrame</class>
|
||||||
<extends>QFrame</extends>
|
<extends>QFrame</extends>
|
||||||
<header>ui/widgets/MCModInfoFrame.h</header>
|
<header>ui/widgets/InfoFrame.h</header>
|
||||||
<container>1</container>
|
<container>1</container>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
#include "ui/dialogs/ProgressDialog.h"
|
#include "ui/dialogs/ProgressDialog.h"
|
||||||
|
|
||||||
ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent)
|
ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent)
|
||||||
: ExternalResourcesPage(inst, mods, parent)
|
: ExternalResourcesPage(inst, mods, parent), m_model(mods)
|
||||||
{
|
{
|
||||||
// This is structured like that so that these changes
|
// This is structured like that so that these changes
|
||||||
// do not affect the Resource pack and Shader pack tabs
|
// do not affect the Resource pack and Shader pack tabs
|
||||||
@ -124,6 +124,17 @@ bool ModFolderPage::shouldDisplay() const
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ModFolderPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous)
|
||||||
|
{
|
||||||
|
auto sourceCurrent = m_filterModel->mapToSource(current);
|
||||||
|
int row = sourceCurrent.row();
|
||||||
|
Mod const* m = m_model->at(row);
|
||||||
|
if (m)
|
||||||
|
ui->frame->updateWithMod(*m);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void ModFolderPage::installMods()
|
void ModFolderPage::installMods()
|
||||||
{
|
{
|
||||||
if (!m_controlsEnabled)
|
if (!m_controlsEnabled)
|
||||||
|
@ -55,9 +55,15 @@ class ModFolderPage : public ExternalResourcesPage {
|
|||||||
virtual bool shouldDisplay() const override;
|
virtual bool shouldDisplay() const override;
|
||||||
void runningStateChanged(bool running) override;
|
void runningStateChanged(bool running) override;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void installMods();
|
void installMods();
|
||||||
void updateMods();
|
void updateMods();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::shared_ptr<ModFolderModel> m_model;
|
||||||
};
|
};
|
||||||
|
|
||||||
class CoreModFolderPage : public ModFolderPage {
|
class CoreModFolderPage : public ModFolderPage {
|
||||||
|
@ -38,12 +38,14 @@
|
|||||||
#include "ExternalResourcesPage.h"
|
#include "ExternalResourcesPage.h"
|
||||||
#include "ui_ExternalResourcesPage.h"
|
#include "ui_ExternalResourcesPage.h"
|
||||||
|
|
||||||
|
#include "minecraft/mod/ResourcePackFolderModel.h"
|
||||||
|
|
||||||
class ResourcePackPage : public ExternalResourcesPage
|
class ResourcePackPage : public ExternalResourcesPage
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit ResourcePackPage(MinecraftInstance *instance, QWidget *parent = 0)
|
explicit ResourcePackPage(MinecraftInstance *instance, std::shared_ptr<ResourcePackFolderModel> model, QWidget *parent = 0)
|
||||||
: ExternalResourcesPage(instance, instance->resourcePackList(), parent)
|
: ExternalResourcesPage(instance, model, parent)
|
||||||
{
|
{
|
||||||
ui->actionViewConfigs->setVisible(false);
|
ui->actionViewConfigs->setVisible(false);
|
||||||
}
|
}
|
||||||
|
@ -38,12 +38,14 @@
|
|||||||
#include "ExternalResourcesPage.h"
|
#include "ExternalResourcesPage.h"
|
||||||
#include "ui_ExternalResourcesPage.h"
|
#include "ui_ExternalResourcesPage.h"
|
||||||
|
|
||||||
|
#include "minecraft/mod/ShaderPackFolderModel.h"
|
||||||
|
|
||||||
class ShaderPackPage : public ExternalResourcesPage
|
class ShaderPackPage : public ExternalResourcesPage
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit ShaderPackPage(MinecraftInstance *instance, QWidget *parent = 0)
|
explicit ShaderPackPage(MinecraftInstance *instance, std::shared_ptr<ShaderPackFolderModel> model, QWidget *parent = 0)
|
||||||
: ExternalResourcesPage(instance, instance->shaderPackList(), parent)
|
: ExternalResourcesPage(instance, model, parent)
|
||||||
{
|
{
|
||||||
ui->actionViewConfigs->setVisible(false);
|
ui->actionViewConfigs->setVisible(false);
|
||||||
}
|
}
|
||||||
|
@ -38,12 +38,14 @@
|
|||||||
#include "ExternalResourcesPage.h"
|
#include "ExternalResourcesPage.h"
|
||||||
#include "ui_ExternalResourcesPage.h"
|
#include "ui_ExternalResourcesPage.h"
|
||||||
|
|
||||||
|
#include "minecraft/mod/TexturePackFolderModel.h"
|
||||||
|
|
||||||
class TexturePackPage : public ExternalResourcesPage
|
class TexturePackPage : public ExternalResourcesPage
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit TexturePackPage(MinecraftInstance *instance, QWidget *parent = 0)
|
explicit TexturePackPage(MinecraftInstance *instance, std::shared_ptr<TexturePackFolderModel> model, QWidget *parent = 0)
|
||||||
: ExternalResourcesPage(instance, instance->texturePackList(), parent)
|
: ExternalResourcesPage(instance, model, parent)
|
||||||
{
|
{
|
||||||
ui->actionViewConfigs->setVisible(false);
|
ui->actionViewConfigs->setVisible(false);
|
||||||
}
|
}
|
||||||
|
@ -196,10 +196,10 @@ void VersionPage::packageCurrent(const QModelIndex ¤t, const QModelIndex &
|
|||||||
switch(severity)
|
switch(severity)
|
||||||
{
|
{
|
||||||
case ProblemSeverity::Warning:
|
case ProblemSeverity::Warning:
|
||||||
ui->frame->setModText(tr("%1 possibly has issues.").arg(patch->getName()));
|
ui->frame->setName(tr("%1 possibly has issues.").arg(patch->getName()));
|
||||||
break;
|
break;
|
||||||
case ProblemSeverity::Error:
|
case ProblemSeverity::Error:
|
||||||
ui->frame->setModText(tr("%1 has issues!").arg(patch->getName()));
|
ui->frame->setName(tr("%1 has issues!").arg(patch->getName()));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
case ProblemSeverity::None:
|
case ProblemSeverity::None:
|
||||||
@ -222,7 +222,7 @@ void VersionPage::packageCurrent(const QModelIndex ¤t, const QModelIndex &
|
|||||||
problemOut += problem.m_description;
|
problemOut += problem.m_description;
|
||||||
problemOut += "\n";
|
problemOut += "\n";
|
||||||
}
|
}
|
||||||
ui->frame->setModDescription(problemOut);
|
ui->frame->setDescription(problemOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
void VersionPage::updateRunningStatus(bool running)
|
void VersionPage::updateRunningStatus(bool running)
|
||||||
|
@ -64,7 +64,7 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="MCModInfoFrame" name="frame">
|
<widget class="InfoFrame" name="frame">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
|
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
@ -278,9 +278,9 @@
|
|||||||
<header>ui/widgets/ModListView.h</header>
|
<header>ui/widgets/ModListView.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>MCModInfoFrame</class>
|
<class>InfoFrame</class>
|
||||||
<extends>QFrame</extends>
|
<extends>QFrame</extends>
|
||||||
<header>ui/widgets/MCModInfoFrame.h</header>
|
<header>ui/widgets/InfoFrame.h</header>
|
||||||
<container>1</container>
|
<container>1</container>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
|
@ -14,16 +14,30 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QtGui>
|
|
||||||
|
|
||||||
#include "MCModInfoFrame.h"
|
#include "InfoFrame.h"
|
||||||
#include "ui_MCModInfoFrame.h"
|
#include "ui_InfoFrame.h"
|
||||||
|
|
||||||
#include "ui/dialogs/CustomMessageBox.h"
|
#include "ui/dialogs/CustomMessageBox.h"
|
||||||
|
|
||||||
void MCModInfoFrame::updateWithMod(Mod &m)
|
InfoFrame::InfoFrame(QWidget *parent) :
|
||||||
|
QFrame(parent),
|
||||||
|
ui(new Ui::InfoFrame)
|
||||||
{
|
{
|
||||||
if (m.type() == m.MOD_FOLDER)
|
ui->setupUi(this);
|
||||||
|
ui->descriptionLabel->setHidden(true);
|
||||||
|
ui->nameLabel->setHidden(true);
|
||||||
|
updateHiddenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoFrame::~InfoFrame()
|
||||||
|
{
|
||||||
|
delete ui;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InfoFrame::updateWithMod(Mod const& m)
|
||||||
|
{
|
||||||
|
if (m.type() == ResourceType::FOLDER)
|
||||||
{
|
{
|
||||||
clear();
|
clear();
|
||||||
return;
|
return;
|
||||||
@ -43,42 +57,32 @@ void MCModInfoFrame::updateWithMod(Mod &m)
|
|||||||
if (!m.authors().isEmpty())
|
if (!m.authors().isEmpty())
|
||||||
text += " by " + m.authors().join(", ");
|
text += " by " + m.authors().join(", ");
|
||||||
|
|
||||||
setModText(text);
|
setName(text);
|
||||||
|
|
||||||
if (m.description().isEmpty())
|
if (m.description().isEmpty())
|
||||||
{
|
{
|
||||||
setModDescription(QString());
|
setDescription(QString());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
setModDescription(m.description());
|
setDescription(m.description());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MCModInfoFrame::clear()
|
void InfoFrame::updateWithResource(const Resource& resource)
|
||||||
{
|
{
|
||||||
setModText(QString());
|
setName(resource.name());
|
||||||
setModDescription(QString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MCModInfoFrame::MCModInfoFrame(QWidget *parent) :
|
void InfoFrame::clear()
|
||||||
QFrame(parent),
|
|
||||||
ui(new Ui::MCModInfoFrame)
|
|
||||||
{
|
{
|
||||||
ui->setupUi(this);
|
setName();
|
||||||
ui->label_ModDescription->setHidden(true);
|
setDescription();
|
||||||
ui->label_ModText->setHidden(true);
|
|
||||||
updateHiddenState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MCModInfoFrame::~MCModInfoFrame()
|
void InfoFrame::updateHiddenState()
|
||||||
{
|
{
|
||||||
delete ui;
|
if(ui->descriptionLabel->isHidden() && ui->nameLabel->isHidden())
|
||||||
}
|
|
||||||
|
|
||||||
void MCModInfoFrame::updateHiddenState()
|
|
||||||
{
|
|
||||||
if(ui->label_ModDescription->isHidden() && ui->label_ModText->isHidden())
|
|
||||||
{
|
{
|
||||||
setHidden(true);
|
setHidden(true);
|
||||||
}
|
}
|
||||||
@ -88,34 +92,34 @@ void MCModInfoFrame::updateHiddenState()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MCModInfoFrame::setModText(QString text)
|
void InfoFrame::setName(QString text)
|
||||||
{
|
{
|
||||||
if(text.isEmpty())
|
if(text.isEmpty())
|
||||||
{
|
{
|
||||||
ui->label_ModText->setHidden(true);
|
ui->nameLabel->setHidden(true);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ui->label_ModText->setText(text);
|
ui->nameLabel->setText(text);
|
||||||
ui->label_ModText->setHidden(false);
|
ui->nameLabel->setHidden(false);
|
||||||
}
|
}
|
||||||
updateHiddenState();
|
updateHiddenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MCModInfoFrame::setModDescription(QString text)
|
void InfoFrame::setDescription(QString text)
|
||||||
{
|
{
|
||||||
if(text.isEmpty())
|
if(text.isEmpty())
|
||||||
{
|
{
|
||||||
ui->label_ModDescription->setHidden(true);
|
ui->descriptionLabel->setHidden(true);
|
||||||
updateHiddenState();
|
updateHiddenState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ui->label_ModDescription->setHidden(false);
|
ui->descriptionLabel->setHidden(false);
|
||||||
updateHiddenState();
|
updateHiddenState();
|
||||||
}
|
}
|
||||||
ui->label_ModDescription->setToolTip("");
|
ui->descriptionLabel->setToolTip("");
|
||||||
QString intermediatetext = text.trimmed();
|
QString intermediatetext = text.trimmed();
|
||||||
bool prev(false);
|
bool prev(false);
|
||||||
QChar rem('\n');
|
QChar rem('\n');
|
||||||
@ -133,36 +137,36 @@ void MCModInfoFrame::setModDescription(QString text)
|
|||||||
labeltext.reserve(300);
|
labeltext.reserve(300);
|
||||||
if(finaltext.length() > 290)
|
if(finaltext.length() > 290)
|
||||||
{
|
{
|
||||||
ui->label_ModDescription->setOpenExternalLinks(false);
|
ui->descriptionLabel->setOpenExternalLinks(false);
|
||||||
ui->label_ModDescription->setTextFormat(Qt::TextFormat::RichText);
|
ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText);
|
||||||
desc = text;
|
m_description = text;
|
||||||
// This allows injecting HTML here.
|
// This allows injecting HTML here.
|
||||||
labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>");
|
labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>");
|
||||||
QObject::connect(ui->label_ModDescription, &QLabel::linkActivated, this, &MCModInfoFrame::modDescEllipsisHandler);
|
QObject::connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ui->label_ModDescription->setTextFormat(Qt::TextFormat::PlainText);
|
ui->descriptionLabel->setTextFormat(Qt::TextFormat::PlainText);
|
||||||
labeltext.append(finaltext);
|
labeltext.append(finaltext);
|
||||||
}
|
}
|
||||||
ui->label_ModDescription->setText(labeltext);
|
ui->descriptionLabel->setText(labeltext);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MCModInfoFrame::modDescEllipsisHandler(const QString &link)
|
void InfoFrame::descriptionEllipsisHandler(QString link)
|
||||||
{
|
{
|
||||||
if(!currentBox)
|
if(!m_current_box)
|
||||||
{
|
{
|
||||||
currentBox = CustomMessageBox::selectable(this, QString(), desc);
|
m_current_box = CustomMessageBox::selectable(this, "", m_description);
|
||||||
connect(currentBox, &QMessageBox::finished, this, &MCModInfoFrame::boxClosed);
|
connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed);
|
||||||
currentBox->show();
|
m_current_box->show();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
currentBox->setText(desc);
|
m_current_box->setText(m_description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MCModInfoFrame::boxClosed(int result)
|
void InfoFrame::boxClosed(int result)
|
||||||
{
|
{
|
||||||
currentBox = nullptr;
|
m_current_box = nullptr;
|
||||||
}
|
}
|
@ -16,37 +16,39 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
|
|
||||||
#include "minecraft/mod/Mod.h"
|
#include "minecraft/mod/Mod.h"
|
||||||
|
#include "minecraft/mod/ResourcePack.h"
|
||||||
|
|
||||||
namespace Ui
|
namespace Ui
|
||||||
{
|
{
|
||||||
class MCModInfoFrame;
|
class InfoFrame;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MCModInfoFrame : public QFrame
|
class InfoFrame : public QFrame {
|
||||||
{
|
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit MCModInfoFrame(QWidget *parent = 0);
|
InfoFrame(QWidget* parent = nullptr);
|
||||||
~MCModInfoFrame();
|
~InfoFrame() override;
|
||||||
|
|
||||||
void setModText(QString text);
|
void setName(QString text = {});
|
||||||
void setModDescription(QString text);
|
void setDescription(QString text = {});
|
||||||
|
|
||||||
void updateWithMod(Mod &m);
|
|
||||||
void clear();
|
void clear();
|
||||||
|
|
||||||
public slots:
|
void updateWithMod(Mod const& m);
|
||||||
void modDescEllipsisHandler(const QString& link );
|
void updateWithResource(Resource const& resource);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void descriptionEllipsisHandler(QString link);
|
||||||
void boxClosed(int result);
|
void boxClosed(int result);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateHiddenState();
|
void updateHiddenState();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Ui::MCModInfoFrame *ui;
|
Ui::InfoFrame* ui;
|
||||||
QString desc;
|
QString m_description;
|
||||||
class QMessageBox * currentBox = nullptr;
|
class QMessageBox* m_current_box = nullptr;
|
||||||
};
|
};
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<ui version="4.0">
|
<ui version="4.0">
|
||||||
<class>MCModInfoFrame</class>
|
<class>InfoFrame</class>
|
||||||
<widget class="QFrame" name="MCModInfoFrame">
|
<widget class="QFrame" name="InfoFrame">
|
||||||
<property name="geometry">
|
<property name="geometry">
|
||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
@ -39,7 +39,7 @@
|
|||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label_ModText">
|
<widget class="QLabel" name="nameLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string notr="true"/>
|
<string notr="true"/>
|
||||||
</property>
|
</property>
|
||||||
@ -61,7 +61,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label_ModDescription">
|
<widget class="QLabel" name="descriptionLabel">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string notr="true"/>
|
<string notr="true"/>
|
||||||
</property>
|
</property>
|
Loading…
Reference in New Issue
Block a user