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/ModFolderModel.h
|
||||
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.cpp
|
||||
minecraft/mod/TexturePackFolderModel.h
|
||||
minecraft/mod/TexturePackFolderModel.cpp
|
||||
minecraft/mod/ShaderPackFolderModel.h
|
||||
minecraft/mod/tasks/BasicFolderLoadTask.h
|
||||
minecraft/mod/tasks/ModFolderLoadTask.h
|
||||
minecraft/mod/tasks/ModFolderLoadTask.cpp
|
||||
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
|
||||
# TODO: needs testdata
|
||||
ecm_add_test(minecraft/mod/ModFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
|
||||
TEST_NAME ModFolderModel)
|
||||
ecm_add_test(minecraft/mod/ResourceFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
|
||||
TEST_NAME ResourceFolderModel)
|
||||
|
||||
ecm_add_test(minecraft/ParseUtils_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
|
||||
TEST_NAME ParseUtils)
|
||||
@ -881,8 +887,8 @@ SET(LAUNCHER_SOURCES
|
||||
ui/widgets/LineSeparator.h
|
||||
ui/widgets/LogView.cpp
|
||||
ui/widgets/LogView.h
|
||||
ui/widgets/MCModInfoFrame.cpp
|
||||
ui/widgets/MCModInfoFrame.h
|
||||
ui/widgets/InfoFrame.cpp
|
||||
ui/widgets/InfoFrame.h
|
||||
ui/widgets/ModFilterWidget.cpp
|
||||
ui/widgets/ModFilterWidget.h
|
||||
ui/widgets/ModListView.cpp
|
||||
@ -944,7 +950,7 @@ qt_wrap_ui(LAUNCHER_UI
|
||||
ui/pages/modplatform/technic/TechnicPage.ui
|
||||
ui/widgets/InstanceCardWidget.ui
|
||||
ui/widgets/CustomCommands.ui
|
||||
ui/widgets/MCModInfoFrame.ui
|
||||
ui/widgets/InfoFrame.ui
|
||||
ui/widgets/ModFilterWidget.ui
|
||||
ui/dialogs/CopyInstanceDialog.ui
|
||||
ui/dialogs/ProfileSetupDialog.ui
|
||||
|
@ -37,9 +37,9 @@ public:
|
||||
modsPage->setFilter("%1 (*.zip *.jar *.litemod)");
|
||||
values.append(modsPage);
|
||||
values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList()));
|
||||
values.append(new ResourcePackPage(onesix.get()));
|
||||
values.append(new TexturePackPage(onesix.get()));
|
||||
values.append(new ShaderPackPage(onesix.get()));
|
||||
values.append(new ResourcePackPage(onesix.get(), onesix->resourcePackList()));
|
||||
values.append(new TexturePackPage(onesix.get(), onesix->texturePackList()));
|
||||
values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList()));
|
||||
values.append(new NotesPage(onesix.get()));
|
||||
values.append(new WorldListPage(onesix.get(), onesix->worldList()));
|
||||
values.append(new ServersPage(onesix));
|
||||
|
@ -148,7 +148,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
|
||||
// do not merge disabled mods.
|
||||
if (!mod->enabled())
|
||||
continue;
|
||||
if (mod->type() == Mod::MOD_ZIPFILE)
|
||||
if (mod->type() == ResourceType::ZIPFILE)
|
||||
{
|
||||
if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles))
|
||||
{
|
||||
@ -158,7 +158,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (mod->type() == Mod::MOD_SINGLEFILE)
|
||||
else if (mod->type() == ResourceType::SINGLEFILE)
|
||||
{
|
||||
// FIXME: buggy - does not work with addedFiles
|
||||
auto filename = mod->fileinfo();
|
||||
@ -171,7 +171,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
|
||||
}
|
||||
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
|
||||
// FIXME: buggy - does not work with addedFiles
|
||||
|
@ -76,6 +76,7 @@
|
||||
|
||||
#include "mod/ModFolderModel.h"
|
||||
#include "mod/ResourcePackFolderModel.h"
|
||||
#include "mod/ShaderPackFolderModel.h"
|
||||
#include "mod/TexturePackFolderModel.h"
|
||||
|
||||
#include "WorldList.h"
|
||||
@ -714,7 +715,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
|
||||
});
|
||||
for(auto mod: modList)
|
||||
{
|
||||
if(mod->type() == Mod::MOD_FOLDER)
|
||||
if(mod->type() == ResourceType::FOLDER)
|
||||
{
|
||||
out << u8" [🖿] " + mod->fileinfo().completeBaseName() + " (folder)";
|
||||
continue;
|
||||
@ -1092,18 +1093,18 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const
|
||||
return m_core_mod_list;
|
||||
}
|
||||
|
||||
std::shared_ptr<ModFolderModel> MinecraftInstance::resourcePackList() const
|
||||
std::shared_ptr<ResourcePackFolderModel> MinecraftInstance::resourcePackList() const
|
||||
{
|
||||
if (!m_resource_pack_list)
|
||||
{
|
||||
m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir()));
|
||||
m_resource_pack_list->disableInteraction(isRunning());
|
||||
connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ModFolderModel::disableInteraction);
|
||||
m_resource_pack_list->enableInteraction(!isRunning());
|
||||
connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ResourcePackFolderModel::disableInteraction);
|
||||
}
|
||||
return m_resource_pack_list;
|
||||
}
|
||||
|
||||
std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const
|
||||
std::shared_ptr<TexturePackFolderModel> MinecraftInstance::texturePackList() const
|
||||
{
|
||||
if (!m_texture_pack_list)
|
||||
{
|
||||
@ -1114,11 +1115,11 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const
|
||||
return m_texture_pack_list;
|
||||
}
|
||||
|
||||
std::shared_ptr<ModFolderModel> MinecraftInstance::shaderPackList() const
|
||||
std::shared_ptr<ShaderPackFolderModel> MinecraftInstance::shaderPackList() const
|
||||
{
|
||||
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());
|
||||
connect(this, &BaseInstance::runningStatusChanged, m_shader_pack_list.get(), &ModFolderModel::disableInteraction);
|
||||
}
|
||||
|
@ -7,6 +7,10 @@
|
||||
#include "minecraft/launch/MinecraftServerTarget.h"
|
||||
|
||||
class ModFolderModel;
|
||||
class ResourceFolderModel;
|
||||
class ResourcePackFolderModel;
|
||||
class ShaderPackFolderModel;
|
||||
class TexturePackFolderModel;
|
||||
class WorldList;
|
||||
class GameOptions;
|
||||
class LaunchStep;
|
||||
@ -72,9 +76,9 @@ public:
|
||||
////// Mod Lists //////
|
||||
std::shared_ptr<ModFolderModel> loaderModList() const;
|
||||
std::shared_ptr<ModFolderModel> coreModList() const;
|
||||
std::shared_ptr<ModFolderModel> resourcePackList() const;
|
||||
std::shared_ptr<ModFolderModel> texturePackList() const;
|
||||
std::shared_ptr<ModFolderModel> shaderPackList() const;
|
||||
std::shared_ptr<ResourcePackFolderModel> resourcePackList() const;
|
||||
std::shared_ptr<TexturePackFolderModel> texturePackList() const;
|
||||
std::shared_ptr<ShaderPackFolderModel> shaderPackList() const;
|
||||
std::shared_ptr<WorldList> worldList() const;
|
||||
std::shared_ptr<GameOptions> gameOptionsModel() const;
|
||||
|
||||
@ -125,9 +129,9 @@ protected: // data
|
||||
std::shared_ptr<PackProfile> m_components;
|
||||
mutable std::shared_ptr<ModFolderModel> m_loader_mod_list;
|
||||
mutable std::shared_ptr<ModFolderModel> m_core_mod_list;
|
||||
mutable std::shared_ptr<ModFolderModel> m_resource_pack_list;
|
||||
mutable std::shared_ptr<ModFolderModel> m_shader_pack_list;
|
||||
mutable std::shared_ptr<ModFolderModel> m_texture_pack_list;
|
||||
mutable std::shared_ptr<ResourcePackFolderModel> m_resource_pack_list;
|
||||
mutable std::shared_ptr<ShaderPackFolderModel> m_shader_pack_list;
|
||||
mutable std::shared_ptr<TexturePackFolderModel> m_texture_pack_list;
|
||||
mutable std::shared_ptr<WorldList> m_world_list;
|
||||
mutable std::shared_ptr<GameOptions> m_game_options;
|
||||
};
|
||||
|
@ -36,130 +36,77 @@
|
||||
|
||||
#include "Mod.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QString>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <FileSystem.h>
|
||||
#include <QDebug>
|
||||
|
||||
#include "Application.h"
|
||||
#include "MetadataHandler.h"
|
||||
#include "Version.h"
|
||||
|
||||
namespace {
|
||||
|
||||
ModDetails invalidDetails;
|
||||
|
||||
}
|
||||
|
||||
Mod::Mod(const QFileInfo& file)
|
||||
Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details()
|
||||
{
|
||||
repath(file);
|
||||
m_changedDateTime = file.lastModified();
|
||||
m_enabled = (file.suffix() != "disabled");
|
||||
}
|
||||
|
||||
Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata)
|
||||
: m_file(mods_dir.absoluteFilePath(metadata.filename))
|
||||
, m_internal_id(metadata.filename)
|
||||
, m_name(metadata.name)
|
||||
: Mod(mods_dir.absoluteFilePath(metadata.filename))
|
||||
{
|
||||
if (m_file.isDir()) {
|
||||
m_type = MOD_FOLDER;
|
||||
} 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;
|
||||
m_name = metadata.name;
|
||||
m_local_details.metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
|
||||
}
|
||||
|
||||
void Mod::setStatus(ModStatus status)
|
||||
{
|
||||
if (m_localDetails) {
|
||||
m_localDetails->status = status;
|
||||
} else {
|
||||
m_temp_status = status;
|
||||
}
|
||||
m_local_details.status = status;
|
||||
}
|
||||
void Mod::setMetadata(const Metadata::ModStruct& metadata)
|
||||
void Mod::setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata)
|
||||
{
|
||||
if (status() == ModStatus::NoMetadata)
|
||||
setStatus(ModStatus::Installed);
|
||||
|
||||
if (m_localDetails) {
|
||||
m_localDetails->metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
|
||||
} else {
|
||||
m_temp_metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
|
||||
m_local_details.metadata = 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
|
||||
@ -175,13 +122,12 @@ auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
|
||||
}
|
||||
}
|
||||
|
||||
m_type = MOD_UNKNOWN;
|
||||
return FS::deletePath(m_file.filePath());
|
||||
return Resource::destroy();
|
||||
}
|
||||
|
||||
auto Mod::details() const -> const ModDetails&
|
||||
{
|
||||
return m_localDetails ? *m_localDetails : invalidDetails;
|
||||
return m_local_details;
|
||||
}
|
||||
|
||||
auto Mod::name() const -> QString
|
||||
@ -218,35 +164,29 @@ auto Mod::authors() const -> QStringList
|
||||
|
||||
auto Mod::status() const -> ModStatus
|
||||
{
|
||||
if (!m_localDetails)
|
||||
return m_temp_status;
|
||||
return details().status;
|
||||
}
|
||||
|
||||
auto Mod::metadata() -> std::shared_ptr<Metadata::ModStruct>
|
||||
{
|
||||
if (m_localDetails)
|
||||
return m_localDetails->metadata;
|
||||
return m_temp_metadata;
|
||||
return m_local_details.metadata;
|
||||
}
|
||||
|
||||
auto Mod::metadata() const -> const std::shared_ptr<Metadata::ModStruct>
|
||||
{
|
||||
if (m_localDetails)
|
||||
return m_localDetails->metadata;
|
||||
return m_temp_metadata;
|
||||
return m_local_details.metadata;
|
||||
}
|
||||
|
||||
void Mod::finishResolvingWithDetails(std::shared_ptr<ModDetails> details)
|
||||
void Mod::finishResolvingWithDetails(ModDetails&& details)
|
||||
{
|
||||
m_resolving = false;
|
||||
m_resolved = true;
|
||||
m_localDetails = details;
|
||||
m_is_resolving = false;
|
||||
m_is_resolved = true;
|
||||
|
||||
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()) {
|
||||
setMetadata(*m_temp_metadata);
|
||||
m_temp_metadata.reset();
|
||||
}
|
||||
m_local_details = std::move(details);
|
||||
if (metadata)
|
||||
setMetadata(std::move(metadata));
|
||||
}
|
||||
|
@ -39,38 +39,23 @@
|
||||
#include <QFileInfo>
|
||||
#include <QList>
|
||||
|
||||
#include "QObjectPtr.h"
|
||||
#include "Resource.h"
|
||||
#include "ModDetails.h"
|
||||
|
||||
class Mod : public QObject
|
||||
class Mod : public Resource
|
||||
{
|
||||
Q_OBJECT
|
||||
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 WeakPtr = QPointer<Mod>;
|
||||
|
||||
Mod() = default;
|
||||
Mod(const QFileInfo &file);
|
||||
explicit Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata);
|
||||
|
||||
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; }
|
||||
Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata);
|
||||
Mod(QString file_path) : Mod(QFileInfo(file_path)) {}
|
||||
|
||||
auto details() const -> const ModDetails&;
|
||||
auto name() const -> QString;
|
||||
auto name() const -> QString override;
|
||||
auto version() const -> QString;
|
||||
auto homeurl() const -> QString;
|
||||
auto description() const -> QString;
|
||||
@ -81,46 +66,17 @@ public:
|
||||
auto metadata() const -> const std::shared_ptr<Metadata::ModStruct>;
|
||||
|
||||
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;
|
||||
|
||||
// change the mod's filesystem path (used by mod lists for *MAGIC* purposes)
|
||||
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);
|
||||
void finishResolvingWithDetails(ModDetails&& details);
|
||||
|
||||
protected:
|
||||
QFileInfo m_file;
|
||||
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;
|
||||
ModDetails m_local_details;
|
||||
};
|
||||
|
@ -46,34 +46,77 @@ enum class ModStatus {
|
||||
Installed, // Both JAR and Metadata are present
|
||||
NotInstalled, // Only the Metadata is present
|
||||
NoMetadata, // Only the JAR is present
|
||||
Unknown, // Default status
|
||||
};
|
||||
|
||||
struct ModDetails
|
||||
{
|
||||
/* Mod ID as defined in the ModLoader-specific metadata */
|
||||
QString mod_id;
|
||||
QString mod_id = {};
|
||||
|
||||
/* Human-readable name */
|
||||
QString name;
|
||||
QString name = {};
|
||||
|
||||
/* Human-readable mod version */
|
||||
QString version;
|
||||
QString version = {};
|
||||
|
||||
/* Human-readable minecraft version */
|
||||
QString mcversion;
|
||||
QString mcversion = {};
|
||||
|
||||
/* URL for mod's home page */
|
||||
QString homeurl;
|
||||
QString homeurl = {};
|
||||
|
||||
/* Human-readable description */
|
||||
QString description;
|
||||
QString description = {};
|
||||
|
||||
/* List of the author's names */
|
||||
QStringList authors;
|
||||
QStringList authors = {};
|
||||
|
||||
/* Installation status of the mod */
|
||||
ModStatus status;
|
||||
ModStatus status = ModStatus::Unknown;
|
||||
|
||||
/* 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/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());
|
||||
m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
|
||||
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;
|
||||
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE };
|
||||
}
|
||||
|
||||
QVariant ModFolderModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return QVariant();
|
||||
if (!validateIndex(index))
|
||||
return {};
|
||||
|
||||
int row = index.row();
|
||||
int column = index.column();
|
||||
|
||||
if (row < 0 || row >= mods.size())
|
||||
return QVariant();
|
||||
|
||||
switch (role)
|
||||
{
|
||||
case Qt::DisplayRole:
|
||||
switch (column)
|
||||
{
|
||||
case NameColumn:
|
||||
return mods[row]->name();
|
||||
return m_resources[row]->name();
|
||||
case VersionColumn: {
|
||||
switch(mods[row]->type()) {
|
||||
case Mod::MOD_FOLDER:
|
||||
switch(m_resources[row]->type()) {
|
||||
case ResourceType::FOLDER:
|
||||
return tr("Folder");
|
||||
case Mod::MOD_SINGLEFILE:
|
||||
case ResourceType::SINGLEFILE:
|
||||
return tr("File");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return mods[row]->version();
|
||||
return at(row)->version();
|
||||
}
|
||||
case DateColumn:
|
||||
return mods[row]->dateTimeChanged();
|
||||
return m_resources[row]->dateTimeChanged();
|
||||
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
case Qt::ToolTipRole:
|
||||
return mods[row]->internal_id();
|
||||
return m_resources[row]->internal_id();
|
||||
|
||||
case Qt::CheckStateRole:
|
||||
switch (column)
|
||||
{
|
||||
case ActiveColumn:
|
||||
return mods[row]->enabled() ? Qt::Checked : Qt::Unchecked;
|
||||
return at(row)->enabled() ? Qt::Checked : Qt::Unchecked;
|
||||
default:
|
||||
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
|
||||
{
|
||||
switch (role)
|
||||
@ -577,65 +143,151 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
Qt::ItemFlags ModFolderModel::flags(const QModelIndex &index) const
|
||||
int ModFolderModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
|
||||
auto flags = defaultFlags;
|
||||
if(interaction_disabled) {
|
||||
flags &= ~Qt::ItemIsDropEnabled;
|
||||
}
|
||||
else
|
||||
{
|
||||
flags |= Qt::ItemIsDropEnabled;
|
||||
if(index.isValid()) {
|
||||
flags |= Qt::ItemIsUserCheckable;
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
return NUM_COLUMNS;
|
||||
}
|
||||
|
||||
Qt::DropActions ModFolderModel::supportedDropActions() const
|
||||
Task* ModFolderModel::createUpdateTask()
|
||||
{
|
||||
// copy from outside, move from within and other mod lists
|
||||
return Qt::CopyAction | Qt::MoveAction;
|
||||
auto index_dir = indexDir();
|
||||
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;
|
||||
types << "text/uri-list";
|
||||
return types;
|
||||
return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo());
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// check if the action is supported
|
||||
if (!data || !(action & supportedDropActions()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
|
||||
{
|
||||
if(!m_can_interact) {
|
||||
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())
|
||||
if(indexes.isEmpty())
|
||||
return true;
|
||||
|
||||
for (auto i: indexes)
|
||||
{
|
||||
if(i.column() != 0) {
|
||||
continue;
|
||||
}
|
||||
// TODO: implement not only copy, but also move
|
||||
// FIXME: handle errors here
|
||||
installMod(url.toLocalFile());
|
||||
auto m = at(i.row());
|
||||
auto index_dir = indexDir();
|
||||
m->destroy(index_dir);
|
||||
}
|
||||
|
||||
update();
|
||||
|
||||
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 "Mod.h"
|
||||
#include "ResourceFolderModel.h"
|
||||
|
||||
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
|
||||
#include "minecraft/mod/tasks/LocalModParseTask.h"
|
||||
@ -56,7 +57,7 @@ class QFileSystemWatcher;
|
||||
* A legacy mod list.
|
||||
* Backed by a folder.
|
||||
*/
|
||||
class ModFolderModel : public QAbstractListModel
|
||||
class ModFolderModel : public ResourceFolderModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
@ -75,106 +76,38 @@ public:
|
||||
};
|
||||
ModFolderModel(const QString &dir, bool is_indexed = false);
|
||||
|
||||
virtual 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;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/// flags, mostly to support drag&drop
|
||||
virtual Qt::ItemFlags flags(const QModelIndex &index) const override;
|
||||
QStringList mimeTypes() const override;
|
||||
bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||
int columnCount(const QModelIndex &parent) const override;
|
||||
|
||||
virtual int rowCount(const QModelIndex &) 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);
|
||||
[[nodiscard]] Task* createUpdateTask() override;
|
||||
[[nodiscard]] Task* createParseTask(Resource const&) override;
|
||||
|
||||
bool installMod(QString file_path) { return ResourceFolderModel::installResource(file_path); }
|
||||
bool uninstallMod(const QString& filename, bool preserve_metadata = false);
|
||||
|
||||
/// Deletes all the selected mods
|
||||
bool deleteMods(const QModelIndexList &indexes);
|
||||
|
||||
/// Enable or disable listed mods
|
||||
bool setModStatus(const QModelIndexList &indexes, ModStatusAction action);
|
||||
|
||||
void startWatching();
|
||||
void stopWatching();
|
||||
|
||||
bool isValid();
|
||||
|
||||
QDir& dir()
|
||||
{
|
||||
return m_dir;
|
||||
}
|
||||
bool startWatching() override;
|
||||
bool stopWatching() override;
|
||||
|
||||
QDir indexDir()
|
||||
{
|
||||
return { QString("%1/.index").arg(dir().absolutePath()) };
|
||||
}
|
||||
QDir indexDir() { return { QString("%1/.index").arg(dir().absolutePath()) }; }
|
||||
|
||||
const QList<Mod::Ptr>& allMods()
|
||||
{
|
||||
return mods;
|
||||
}
|
||||
auto selectedMods(QModelIndexList& indexes) -> QList<Mod*>;
|
||||
auto allMods() -> QList<Mod*>;
|
||||
|
||||
auto selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>;
|
||||
|
||||
public slots:
|
||||
void disableInteraction(bool disabled);
|
||||
RESOURCE_HELPERS(Mod)
|
||||
|
||||
private
|
||||
slots:
|
||||
void directoryChanged(QString path);
|
||||
void finishUpdate();
|
||||
void finishModParse(int token);
|
||||
|
||||
signals:
|
||||
void updateFinished();
|
||||
|
||||
private:
|
||||
void resolveMod(Mod::Ptr m);
|
||||
bool setModStatus(int index, ModStatusAction action);
|
||||
void onUpdateSucceeded() override;
|
||||
void onParseSucceeded(int ticket, QString resource_id) override;
|
||||
|
||||
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_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"
|
||||
|
||||
ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(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);
|
||||
}
|
||||
ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {}
|
||||
|
@ -1,13 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "ModFolderModel.h"
|
||||
#include "ResourceFolderModel.h"
|
||||
|
||||
class ResourcePackFolderModel : public ModFolderModel
|
||||
#include "ResourcePack.h"
|
||||
|
||||
class ResourcePackFolderModel : public ResourceFolderModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
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"
|
||||
|
||||
TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(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);
|
||||
}
|
||||
TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {}
|
||||
|
@ -1,13 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "ModFolderModel.h"
|
||||
#include "ResourceFolderModel.h"
|
||||
|
||||
class TexturePackFolderModel : public ModFolderModel
|
||||
class TexturePackFolderModel : public ResourceFolderModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
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:
|
||||
// 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()) {
|
||||
return nullptr;
|
||||
return {};
|
||||
}
|
||||
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
|
||||
ModDetails details;
|
||||
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();
|
||||
// NOTE: ignore stupid example mods copies where the author didn't even bother to change the name
|
||||
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();
|
||||
if(!homeurl.isEmpty())
|
||||
{
|
||||
@ -45,8 +45,8 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
|
||||
homeurl.prepend("http://");
|
||||
}
|
||||
}
|
||||
details->homeurl = homeurl;
|
||||
details->description = firstObj.value("description").toString();
|
||||
details.homeurl = homeurl;
|
||||
details.description = firstObj.value("description").toString();
|
||||
QJsonArray authors = firstObj.value("authorList").toArray();
|
||||
if (authors.size() == 0) {
|
||||
// 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)
|
||||
{
|
||||
details->authors.append(author.toString());
|
||||
details.authors.append(author.toString());
|
||||
}
|
||||
return details;
|
||||
};
|
||||
@ -83,7 +83,7 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
|
||||
{
|
||||
qCritical() << "BAD stuff happened to mod json:";
|
||||
qCritical() << contents;
|
||||
return nullptr;
|
||||
return {};
|
||||
}
|
||||
auto arrVal = jsonDoc.object().value("modlist");
|
||||
if(arrVal.isUndefined()) {
|
||||
@ -94,13 +94,13 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
|
||||
return getInfoFromArray(arrVal.toArray());
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
return {};
|
||||
}
|
||||
|
||||
// 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];
|
||||
// top-level table
|
||||
@ -108,7 +108,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
|
||||
|
||||
if(!tomlData)
|
||||
{
|
||||
return nullptr;
|
||||
return {};
|
||||
}
|
||||
|
||||
// array defined by [[mods]]
|
||||
@ -116,7 +116,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
|
||||
if(!tomlModsArr)
|
||||
{
|
||||
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
|
||||
@ -124,33 +124,33 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
|
||||
if(!tomlModsTable0)
|
||||
{
|
||||
qWarning() << "Corrupted mods.toml? [[mods]] didn't have an element at index 0!";
|
||||
return nullptr;
|
||||
return {};
|
||||
}
|
||||
|
||||
// mandatory properties - always in [[mods]]
|
||||
toml_datum_t modIdDatum = toml_string_in(tomlModsTable0, "modId");
|
||||
if(modIdDatum.ok)
|
||||
{
|
||||
details->mod_id = modIdDatum.u.s;
|
||||
details.mod_id = modIdDatum.u.s;
|
||||
// library says this is required for strings
|
||||
free(modIdDatum.u.s);
|
||||
}
|
||||
toml_datum_t versionDatum = toml_string_in(tomlModsTable0, "version");
|
||||
if(versionDatum.ok)
|
||||
{
|
||||
details->version = versionDatum.u.s;
|
||||
details.version = versionDatum.u.s;
|
||||
free(versionDatum.u.s);
|
||||
}
|
||||
toml_datum_t displayNameDatum = toml_string_in(tomlModsTable0, "displayName");
|
||||
if(displayNameDatum.ok)
|
||||
{
|
||||
details->name = displayNameDatum.u.s;
|
||||
details.name = displayNameDatum.u.s;
|
||||
free(displayNameDatum.u.s);
|
||||
}
|
||||
toml_datum_t descriptionDatum = toml_string_in(tomlModsTable0, "description");
|
||||
if(descriptionDatum.ok)
|
||||
{
|
||||
details->description = descriptionDatum.u.s;
|
||||
details.description = descriptionDatum.u.s;
|
||||
free(descriptionDatum.u.s);
|
||||
}
|
||||
|
||||
@ -173,7 +173,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
|
||||
}
|
||||
if(!authors.isEmpty())
|
||||
{
|
||||
details->authors.append(authors);
|
||||
details.authors.append(authors);
|
||||
}
|
||||
|
||||
toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL");
|
||||
@ -200,7 +200,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
|
||||
homeurl.prepend("http://");
|
||||
}
|
||||
}
|
||||
details->homeurl = homeurl;
|
||||
details.homeurl = homeurl;
|
||||
|
||||
// this seems to be recursive, so it should free everything
|
||||
toml_free(tomlData);
|
||||
@ -209,20 +209,20 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
|
||||
}
|
||||
|
||||
// https://fabricmc.net/wiki/documentation:fabric_mod_json
|
||||
std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
|
||||
ModDetails ReadFabricModInfo(QByteArray contents)
|
||||
{
|
||||
QJsonParseError jsonError;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
|
||||
auto object = jsonDoc.object();
|
||||
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->version = object.value("version").toString();
|
||||
details.mod_id = object.value("id").toString();
|
||||
details.version = object.value("version").toString();
|
||||
|
||||
details->name = object.contains("name") ? object.value("name").toString() : details->mod_id;
|
||||
details->description = object.value("description").toString();
|
||||
details.name = object.contains("name") ? object.value("name").toString() : details.mod_id;
|
||||
details.description = object.value("description").toString();
|
||||
|
||||
if (schemaVersion >= 1)
|
||||
{
|
||||
@ -230,10 +230,10 @@ std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
|
||||
for (auto author: authors)
|
||||
{
|
||||
if(author.isObject()) {
|
||||
details->authors.append(author.toObject().value("name").toString());
|
||||
details.authors.append(author.toObject().value("name").toString());
|
||||
}
|
||||
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"))
|
||||
{
|
||||
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
|
||||
std::shared_ptr<ModDetails> ReadQuiltModInfo(QByteArray contents)
|
||||
ModDetails ReadQuiltModInfo(QByteArray contents)
|
||||
{
|
||||
QJsonParseError jsonError;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
|
||||
auto object = Json::requireObject(jsonDoc, "quilt.mod.json");
|
||||
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
|
||||
if (schemaVersion == 1)
|
||||
{
|
||||
auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info");
|
||||
|
||||
details->mod_id = Json::requireString(modInfo.value("id"), "Mod ID");
|
||||
details->version = Json::requireString(modInfo.value("version"), "Mod version");
|
||||
details.mod_id = Json::requireString(modInfo.value("id"), "Mod ID");
|
||||
details.version = Json::requireString(modInfo.value("version"), "Mod version");
|
||||
|
||||
auto modMetadata = Json::ensureObject(modInfo.value("metadata"));
|
||||
|
||||
details->name = Json::ensureString(modMetadata.value("name"), details->mod_id);
|
||||
details->description = Json::ensureString(modMetadata.value("description"));
|
||||
details.name = Json::ensureString(modMetadata.value("name"), details.mod_id);
|
||||
details.description = Json::ensureString(modMetadata.value("description"));
|
||||
|
||||
auto modContributors = Json::ensureObject(modMetadata.value("contributors"));
|
||||
|
||||
// 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"));
|
||||
|
||||
if (modContact.contains("homepage"))
|
||||
{
|
||||
details->homeurl = Json::requireString(modContact.value("homepage"));
|
||||
details.homeurl = Json::requireString(modContact.value("homepage"));
|
||||
}
|
||||
}
|
||||
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
|
||||
details->name = "Minecraft Forge";
|
||||
details->mod_id = "Forge";
|
||||
details->homeurl = "http://www.minecraftforge.net/forum/";
|
||||
details.name = "Minecraft Forge";
|
||||
details.mod_id = "Forge";
|
||||
details.homeurl = "http://www.minecraftforge.net/forum/";
|
||||
INIFile ini;
|
||||
if (!ini.loadFile(contents))
|
||||
return details;
|
||||
@ -304,47 +304,47 @@ std::shared_ptr<ModDetails> ReadForgeInfo(QByteArray contents)
|
||||
QString revision = ini.get("forge.revision.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;
|
||||
}
|
||||
|
||||
std::shared_ptr<ModDetails> ReadLiteModInfo(QByteArray contents)
|
||||
ModDetails ReadLiteModInfo(QByteArray contents)
|
||||
{
|
||||
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
|
||||
ModDetails details;
|
||||
QJsonParseError jsonError;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
|
||||
auto object = jsonDoc.object();
|
||||
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"))
|
||||
{
|
||||
details->version = object.value("version").toString("");
|
||||
details.version = object.value("version").toString("");
|
||||
}
|
||||
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();
|
||||
if(!author.isEmpty()) {
|
||||
details->authors.append(author);
|
||||
details.authors.append(author);
|
||||
}
|
||||
details->description = object.value("description").toString();
|
||||
details->homeurl = object.value("url").toString();
|
||||
details.description = object.value("description").toString();
|
||||
details.homeurl = object.value("url").toString();
|
||||
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_type(type),
|
||||
m_modFile(modFile),
|
||||
m_result(new Result())
|
||||
{
|
||||
}
|
||||
{}
|
||||
|
||||
void LocalModParseTask::processAsZip()
|
||||
{
|
||||
@ -366,7 +366,7 @@ void LocalModParseTask::processAsZip()
|
||||
file.close();
|
||||
|
||||
// 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"))
|
||||
{
|
||||
@ -395,7 +395,7 @@ void LocalModParseTask::processAsZip()
|
||||
manifestVersion = "NONE";
|
||||
}
|
||||
|
||||
m_result->details->version = manifestVersion;
|
||||
m_result->details.version = manifestVersion;
|
||||
|
||||
file.close();
|
||||
}
|
||||
@ -497,21 +497,31 @@ void LocalModParseTask::processAsLitemod()
|
||||
zip.close();
|
||||
}
|
||||
|
||||
void LocalModParseTask::run()
|
||||
bool LocalModParseTask::abort()
|
||||
{
|
||||
m_aborted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void LocalModParseTask::executeTask()
|
||||
{
|
||||
switch(m_type)
|
||||
{
|
||||
case Mod::MOD_ZIPFILE:
|
||||
case ResourceType::ZIPFILE:
|
||||
processAsZip();
|
||||
break;
|
||||
case Mod::MOD_FOLDER:
|
||||
case ResourceType::FOLDER:
|
||||
processAsFolder();
|
||||
break;
|
||||
case Mod::MOD_LITEMOD:
|
||||
case ResourceType::LITEMOD:
|
||||
processAsLitemod();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
emit finished(m_token);
|
||||
|
||||
if (m_aborted)
|
||||
emitAborted();
|
||||
else
|
||||
emitSucceeded();
|
||||
}
|
||||
|
@ -2,29 +2,31 @@
|
||||
|
||||
#include <QDebug>
|
||||
#include <QObject>
|
||||
#include <QRunnable>
|
||||
|
||||
#include "minecraft/mod/Mod.h"
|
||||
#include "minecraft/mod/ModDetails.h"
|
||||
|
||||
class LocalModParseTask : public QObject, public QRunnable
|
||||
#include "tasks/Task.h"
|
||||
|
||||
class LocalModParseTask : public Task
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
struct Result {
|
||||
QString id;
|
||||
std::shared_ptr<ModDetails> details;
|
||||
ModDetails details;
|
||||
};
|
||||
using ResultPtr = std::shared_ptr<Result>;
|
||||
ResultPtr result() const {
|
||||
return m_result;
|
||||
}
|
||||
|
||||
LocalModParseTask(int token, Mod::ModType type, const QFileInfo & modFile);
|
||||
void run();
|
||||
[[nodiscard]] bool canAbort() const override { return true; }
|
||||
bool abort() override;
|
||||
|
||||
signals:
|
||||
void finished(int token);
|
||||
LocalModParseTask(int token, ResourceType type, const QFileInfo & modFile);
|
||||
void executeTask() override;
|
||||
|
||||
[[nodiscard]] int token() const { return m_token; }
|
||||
|
||||
private:
|
||||
void processAsZip();
|
||||
@ -33,7 +35,9 @@ private:
|
||||
|
||||
private:
|
||||
int m_token;
|
||||
Mod::ModType m_type;
|
||||
ResourceType m_type;
|
||||
QFileInfo m_modFile;
|
||||
ResultPtr m_result;
|
||||
|
||||
bool m_aborted = false;
|
||||
};
|
||||
|
@ -38,11 +38,11 @@
|
||||
|
||||
#include "minecraft/mod/MetadataHandler.h"
|
||||
|
||||
ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed, bool clean_orphan)
|
||||
: m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_clean_orphan(clean_orphan), m_result(new Result())
|
||||
ModFolderLoadTask::ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan, QObject* parent)
|
||||
: 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) {
|
||||
// Read metadata first
|
||||
@ -52,7 +52,7 @@ void ModFolderLoadTask::run()
|
||||
// Read JAR files that don't have metadata
|
||||
m_mods_dir.refresh();
|
||||
for (auto entry : m_mods_dir.entryInfoList()) {
|
||||
Mod::Ptr mod(new Mod(entry));
|
||||
Mod* mod(new Mod(entry));
|
||||
|
||||
if (mod->enabled()) {
|
||||
if (m_result->mods.contains(mod->internal_id())) {
|
||||
@ -96,7 +96,7 @@ void ModFolderLoadTask::run()
|
||||
}
|
||||
}
|
||||
|
||||
emit succeeded();
|
||||
emitSucceeded();
|
||||
}
|
||||
|
||||
void ModFolderLoadTask::getFromMetadata()
|
||||
|
@ -42,8 +42,9 @@
|
||||
#include <QRunnable>
|
||||
#include <memory>
|
||||
#include "minecraft/mod/Mod.h"
|
||||
#include "tasks/Task.h"
|
||||
|
||||
class ModFolderLoadTask : public QObject, public QRunnable
|
||||
class ModFolderLoadTask : public Task
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
@ -56,16 +57,15 @@ public:
|
||||
}
|
||||
|
||||
public:
|
||||
ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed, bool clean_orphan = false);
|
||||
void run();
|
||||
signals:
|
||||
void succeeded();
|
||||
ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan = false, QObject* parent = nullptr);
|
||||
|
||||
void executeTask() override;
|
||||
|
||||
private:
|
||||
void getFromMetadata();
|
||||
|
||||
private:
|
||||
QDir& m_mods_dir, m_index_dir;
|
||||
QDir m_mods_dir, m_index_dir;
|
||||
bool m_is_indexed;
|
||||
bool m_clean_orphan;
|
||||
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
|
||||
if (mod->type() == Mod::MOD_FOLDER) {
|
||||
if (mod->type() == ResourceType::FOLDER) {
|
||||
emitReady(mod);
|
||||
}
|
||||
}
|
||||
|
@ -37,8 +37,9 @@
|
||||
|
||||
#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)
|
||||
@ -63,26 +64,31 @@ void Task::start()
|
||||
{
|
||||
case State::Inactive:
|
||||
{
|
||||
if (m_show_debug)
|
||||
qDebug() << "Task" << describe() << "starting for the first time";
|
||||
break;
|
||||
}
|
||||
case State::AbortedByUser:
|
||||
{
|
||||
if (m_show_debug)
|
||||
qDebug() << "Task" << describe() << "restarting for after being aborted by user";
|
||||
break;
|
||||
}
|
||||
case State::Failed:
|
||||
{
|
||||
if (m_show_debug)
|
||||
qDebug() << "Task" << describe() << "restarting for after failing at first";
|
||||
break;
|
||||
}
|
||||
case State::Succeeded:
|
||||
{
|
||||
if (m_show_debug)
|
||||
qDebug() << "Task" << describe() << "restarting for after succeeding at first";
|
||||
break;
|
||||
}
|
||||
case State::Running:
|
||||
{
|
||||
if (m_show_debug)
|
||||
qWarning() << "The launcher tried to start task" << describe() << "while it was already running!";
|
||||
return;
|
||||
}
|
||||
@ -118,6 +124,7 @@ void Task::emitAborted()
|
||||
}
|
||||
m_state = State::AbortedByUser;
|
||||
m_failReason = "Aborted.";
|
||||
if (m_show_debug)
|
||||
qDebug() << "Task" << describe() << "aborted.";
|
||||
emit aborted();
|
||||
emit finished();
|
||||
@ -132,6 +139,7 @@ void Task::emitSucceeded()
|
||||
return;
|
||||
}
|
||||
m_state = State::Succeeded;
|
||||
if (m_show_debug)
|
||||
qDebug() << "Task" << describe() << "succeeded";
|
||||
emit succeeded();
|
||||
emit finished();
|
||||
|
@ -35,9 +35,11 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QRunnable>
|
||||
|
||||
#include "QObjectPtr.h"
|
||||
|
||||
class Task : public QObject {
|
||||
class Task : public QObject, public QRunnable {
|
||||
Q_OBJECT
|
||||
public:
|
||||
using Ptr = shared_qobject_ptr<Task>;
|
||||
@ -45,7 +47,7 @@ class Task : public QObject {
|
||||
enum class State { Inactive, Running, Succeeded, Failed, AbortedByUser };
|
||||
|
||||
public:
|
||||
explicit Task(QObject* parent = 0);
|
||||
explicit Task(QObject* parent = 0, bool show_debug_log = true);
|
||||
virtual ~Task() = default;
|
||||
|
||||
bool isRunning() const;
|
||||
@ -95,6 +97,9 @@ class Task : public QObject {
|
||||
void stepStatus(QString status);
|
||||
|
||||
public slots:
|
||||
// QRunnable's interface
|
||||
void run() override { start(); }
|
||||
|
||||
virtual void start();
|
||||
virtual bool abort() { if(canAbort()) emitAborted(); return canAbort(); };
|
||||
|
||||
@ -117,4 +122,7 @@ class Task : public QObject {
|
||||
QString m_status;
|
||||
int m_progress = 0;
|
||||
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,
|
||||
BaseInstance* instance,
|
||||
const std::shared_ptr<ModFolderModel> mods,
|
||||
QList<Mod::Ptr>& search_for)
|
||||
QList<Mod*>& search_for)
|
||||
: ReviewMessageBox(parent, tr("Confirm mods to update"), "")
|
||||
, m_parent(parent)
|
||||
, m_mod_model(mods)
|
||||
@ -226,9 +226,8 @@ auto ModUpdateDialog::ensureMetadata() -> bool
|
||||
};
|
||||
|
||||
for (auto candidate : m_candidates) {
|
||||
auto* candidate_ptr = candidate.get();
|
||||
if (candidate->status() != ModStatus::NoMetadata) {
|
||||
onMetadataEnsured(candidate_ptr);
|
||||
onMetadataEnsured(candidate);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -236,7 +235,7 @@ auto ModUpdateDialog::ensureMetadata() -> bool
|
||||
continue;
|
||||
|
||||
if (confirm_rest) {
|
||||
addToTmp(candidate_ptr, provider_rest);
|
||||
addToTmp(candidate, provider_rest);
|
||||
should_try_others.insert(candidate->internal_id(), try_others_rest);
|
||||
continue;
|
||||
}
|
||||
@ -261,7 +260,7 @@ auto ModUpdateDialog::ensureMetadata() -> bool
|
||||
should_try_others.insert(candidate->internal_id(), response.try_others);
|
||||
|
||||
if (confirmed)
|
||||
addToTmp(candidate_ptr, response.chosen);
|
||||
addToTmp(candidate, response.chosen);
|
||||
}
|
||||
|
||||
if (!modrinth_tmp.empty()) {
|
||||
|
@ -19,7 +19,7 @@ class ModUpdateDialog final : public ReviewMessageBox {
|
||||
explicit ModUpdateDialog(QWidget* parent,
|
||||
BaseInstance* instance,
|
||||
const std::shared_ptr<ModFolderModel> mod_model,
|
||||
QList<Mod::Ptr>& search_for);
|
||||
QList<Mod*>& search_for);
|
||||
|
||||
void checkCandidates();
|
||||
|
||||
@ -46,7 +46,7 @@ class ModUpdateDialog final : public ReviewMessageBox {
|
||||
|
||||
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_flame_to_update;
|
||||
|
||||
|
@ -3,100 +3,13 @@
|
||||
|
||||
#include "DesktopServices.h"
|
||||
#include "Version.h"
|
||||
#include "minecraft/mod/ModFolderModel.h"
|
||||
#include "minecraft/mod/ResourceFolderModel.h"
|
||||
#include "ui/GuiUtil.h"
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <QMenu>
|
||||
|
||||
namespace {
|
||||
// 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)
|
||||
ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ResourceFolderModel> model, QWidget* parent)
|
||||
: QMainWindow(parent), m_instance(instance), ui(new Ui::ExternalResourcesPage), m_model(model)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
@ -105,7 +18,7 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared
|
||||
|
||||
ui->actionsToolbar->insertSpacer(ui->actionViewConfigs);
|
||||
|
||||
m_filterModel = new SortProxy(this);
|
||||
m_filterModel = model->createFilterProxyModel(this);
|
||||
m_filterModel->setDynamicSortFilter(true);
|
||||
m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
|
||||
@ -137,19 +50,9 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared
|
||||
|
||||
ExternalResourcesPage::~ExternalResourcesPage()
|
||||
{
|
||||
m_model->stopWatching();
|
||||
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* filteredMenu = QMainWindow::createPopupMenu();
|
||||
@ -179,6 +82,15 @@ void ExternalResourcesPage::retranslate()
|
||||
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)
|
||||
{
|
||||
m_viewFilter = newContents;
|
||||
@ -241,7 +153,7 @@ void ExternalResourcesPage::addItem()
|
||||
|
||||
if (!list.isEmpty()) {
|
||||
for (auto filename : list) {
|
||||
m_model->installMod(filename);
|
||||
m_model->installResource(filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -252,7 +164,7 @@ void ExternalResourcesPage::removeItem()
|
||||
return;
|
||||
|
||||
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
|
||||
m_model->deleteMods(selection.indexes());
|
||||
m_model->deleteResources(selection.indexes());
|
||||
}
|
||||
|
||||
void ExternalResourcesPage::enableItem()
|
||||
@ -261,7 +173,7 @@ void ExternalResourcesPage::enableItem()
|
||||
return;
|
||||
|
||||
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()
|
||||
@ -270,7 +182,7 @@ void ExternalResourcesPage::disableItem()
|
||||
return;
|
||||
|
||||
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()
|
||||
@ -283,15 +195,23 @@ void ExternalResourcesPage::viewFolder()
|
||||
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()) {
|
||||
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);
|
||||
int row = sourceCurrent.row();
|
||||
Mod& m = m_model->operator[](row);
|
||||
ui->frame->updateWithMod(m);
|
||||
Resource const& resource = m_model->at(row);
|
||||
ui->frame->updateWithResource(resource);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
#include "minecraft/MinecraftInstance.h"
|
||||
#include "ui/pages/BasePage.h"
|
||||
|
||||
class ModFolderModel;
|
||||
class ResourceFolderModel;
|
||||
|
||||
namespace Ui {
|
||||
class ExternalResourcesPage;
|
||||
@ -19,8 +19,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// FIXME: Switch to different model (or change the name of this one)
|
||||
explicit ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ModFolderModel> model, QWidget* parent = nullptr);
|
||||
explicit ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ResourceFolderModel> model, QWidget* parent = nullptr);
|
||||
virtual ~ExternalResourcesPage();
|
||||
|
||||
virtual QString displayName() const override = 0;
|
||||
@ -41,7 +40,9 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
|
||||
QMenu* createPopupMenu() override;
|
||||
|
||||
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:
|
||||
void itemActivated(const QModelIndex& index);
|
||||
@ -63,7 +64,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
|
||||
BaseInstance* m_instance = nullptr;
|
||||
|
||||
Ui::ExternalResourcesPage* ui = nullptr;
|
||||
std::shared_ptr<ModFolderModel> m_model;
|
||||
std::shared_ptr<ResourceFolderModel> m_model;
|
||||
QSortFilterProxyModel* m_filterModel = nullptr;
|
||||
|
||||
QString m_fileSelectionFilter;
|
||||
|
@ -43,7 +43,7 @@
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="1" colspan="3">
|
||||
<widget class="MCModInfoFrame" name="frame">
|
||||
<widget class="InfoFrame" name="frame">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
@ -166,9 +166,9 @@
|
||||
<header>ui/widgets/ModListView.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>MCModInfoFrame</class>
|
||||
<class>InfoFrame</class>
|
||||
<extends>QFrame</extends>
|
||||
<header>ui/widgets/MCModInfoFrame.h</header>
|
||||
<header>ui/widgets/InfoFrame.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
|
@ -65,7 +65,7 @@
|
||||
#include "ui/dialogs/ProgressDialog.h"
|
||||
|
||||
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
|
||||
// do not affect the Resource pack and Shader pack tabs
|
||||
@ -124,6 +124,17 @@ bool ModFolderPage::shouldDisplay() const
|
||||
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()
|
||||
{
|
||||
if (!m_controlsEnabled)
|
||||
|
@ -55,9 +55,15 @@ class ModFolderPage : public ExternalResourcesPage {
|
||||
virtual bool shouldDisplay() const override;
|
||||
void runningStateChanged(bool running) override;
|
||||
|
||||
public slots:
|
||||
bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override;
|
||||
|
||||
private slots:
|
||||
void installMods();
|
||||
void updateMods();
|
||||
|
||||
protected:
|
||||
std::shared_ptr<ModFolderModel> m_model;
|
||||
};
|
||||
|
||||
class CoreModFolderPage : public ModFolderPage {
|
||||
|
@ -38,12 +38,14 @@
|
||||
#include "ExternalResourcesPage.h"
|
||||
#include "ui_ExternalResourcesPage.h"
|
||||
|
||||
#include "minecraft/mod/ResourcePackFolderModel.h"
|
||||
|
||||
class ResourcePackPage : public ExternalResourcesPage
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ResourcePackPage(MinecraftInstance *instance, QWidget *parent = 0)
|
||||
: ExternalResourcesPage(instance, instance->resourcePackList(), parent)
|
||||
explicit ResourcePackPage(MinecraftInstance *instance, std::shared_ptr<ResourcePackFolderModel> model, QWidget *parent = 0)
|
||||
: ExternalResourcesPage(instance, model, parent)
|
||||
{
|
||||
ui->actionViewConfigs->setVisible(false);
|
||||
}
|
||||
|
@ -38,12 +38,14 @@
|
||||
#include "ExternalResourcesPage.h"
|
||||
#include "ui_ExternalResourcesPage.h"
|
||||
|
||||
#include "minecraft/mod/ShaderPackFolderModel.h"
|
||||
|
||||
class ShaderPackPage : public ExternalResourcesPage
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ShaderPackPage(MinecraftInstance *instance, QWidget *parent = 0)
|
||||
: ExternalResourcesPage(instance, instance->shaderPackList(), parent)
|
||||
explicit ShaderPackPage(MinecraftInstance *instance, std::shared_ptr<ShaderPackFolderModel> model, QWidget *parent = 0)
|
||||
: ExternalResourcesPage(instance, model, parent)
|
||||
{
|
||||
ui->actionViewConfigs->setVisible(false);
|
||||
}
|
||||
|
@ -38,12 +38,14 @@
|
||||
#include "ExternalResourcesPage.h"
|
||||
#include "ui_ExternalResourcesPage.h"
|
||||
|
||||
#include "minecraft/mod/TexturePackFolderModel.h"
|
||||
|
||||
class TexturePackPage : public ExternalResourcesPage
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TexturePackPage(MinecraftInstance *instance, QWidget *parent = 0)
|
||||
: ExternalResourcesPage(instance, instance->texturePackList(), parent)
|
||||
explicit TexturePackPage(MinecraftInstance *instance, std::shared_ptr<TexturePackFolderModel> model, QWidget *parent = 0)
|
||||
: ExternalResourcesPage(instance, model, parent)
|
||||
{
|
||||
ui->actionViewConfigs->setVisible(false);
|
||||
}
|
||||
|
@ -196,10 +196,10 @@ void VersionPage::packageCurrent(const QModelIndex ¤t, const QModelIndex &
|
||||
switch(severity)
|
||||
{
|
||||
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;
|
||||
case ProblemSeverity::Error:
|
||||
ui->frame->setModText(tr("%1 has issues!").arg(patch->getName()));
|
||||
ui->frame->setName(tr("%1 has issues!").arg(patch->getName()));
|
||||
break;
|
||||
default:
|
||||
case ProblemSeverity::None:
|
||||
@ -222,7 +222,7 @@ void VersionPage::packageCurrent(const QModelIndex ¤t, const QModelIndex &
|
||||
problemOut += problem.m_description;
|
||||
problemOut += "\n";
|
||||
}
|
||||
ui->frame->setModDescription(problemOut);
|
||||
ui->frame->setDescription(problemOut);
|
||||
}
|
||||
|
||||
void VersionPage::updateRunningStatus(bool running)
|
||||
|
@ -64,7 +64,7 @@
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="MCModInfoFrame" name="frame">
|
||||
<widget class="InfoFrame" name="frame">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
@ -278,9 +278,9 @@
|
||||
<header>ui/widgets/ModListView.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>MCModInfoFrame</class>
|
||||
<class>InfoFrame</class>
|
||||
<extends>QFrame</extends>
|
||||
<header>ui/widgets/MCModInfoFrame.h</header>
|
||||
<header>ui/widgets/InfoFrame.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
|
@ -14,16 +14,30 @@
|
||||
*/
|
||||
|
||||
#include <QMessageBox>
|
||||
#include <QtGui>
|
||||
|
||||
#include "MCModInfoFrame.h"
|
||||
#include "ui_MCModInfoFrame.h"
|
||||
#include "InfoFrame.h"
|
||||
#include "ui_InfoFrame.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();
|
||||
return;
|
||||
@ -43,42 +57,32 @@ void MCModInfoFrame::updateWithMod(Mod &m)
|
||||
if (!m.authors().isEmpty())
|
||||
text += " by " + m.authors().join(", ");
|
||||
|
||||
setModText(text);
|
||||
setName(text);
|
||||
|
||||
if (m.description().isEmpty())
|
||||
{
|
||||
setModDescription(QString());
|
||||
setDescription(QString());
|
||||
}
|
||||
else
|
||||
{
|
||||
setModDescription(m.description());
|
||||
setDescription(m.description());
|
||||
}
|
||||
}
|
||||
|
||||
void MCModInfoFrame::clear()
|
||||
void InfoFrame::updateWithResource(const Resource& resource)
|
||||
{
|
||||
setModText(QString());
|
||||
setModDescription(QString());
|
||||
setName(resource.name());
|
||||
}
|
||||
|
||||
MCModInfoFrame::MCModInfoFrame(QWidget *parent) :
|
||||
QFrame(parent),
|
||||
ui(new Ui::MCModInfoFrame)
|
||||
void InfoFrame::clear()
|
||||
{
|
||||
ui->setupUi(this);
|
||||
ui->label_ModDescription->setHidden(true);
|
||||
ui->label_ModText->setHidden(true);
|
||||
updateHiddenState();
|
||||
setName();
|
||||
setDescription();
|
||||
}
|
||||
|
||||
MCModInfoFrame::~MCModInfoFrame()
|
||||
void InfoFrame::updateHiddenState()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void MCModInfoFrame::updateHiddenState()
|
||||
{
|
||||
if(ui->label_ModDescription->isHidden() && ui->label_ModText->isHidden())
|
||||
if(ui->descriptionLabel->isHidden() && ui->nameLabel->isHidden())
|
||||
{
|
||||
setHidden(true);
|
||||
}
|
||||
@ -88,34 +92,34 @@ void MCModInfoFrame::updateHiddenState()
|
||||
}
|
||||
}
|
||||
|
||||
void MCModInfoFrame::setModText(QString text)
|
||||
void InfoFrame::setName(QString text)
|
||||
{
|
||||
if(text.isEmpty())
|
||||
{
|
||||
ui->label_ModText->setHidden(true);
|
||||
ui->nameLabel->setHidden(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
ui->label_ModText->setText(text);
|
||||
ui->label_ModText->setHidden(false);
|
||||
ui->nameLabel->setText(text);
|
||||
ui->nameLabel->setHidden(false);
|
||||
}
|
||||
updateHiddenState();
|
||||
}
|
||||
|
||||
void MCModInfoFrame::setModDescription(QString text)
|
||||
void InfoFrame::setDescription(QString text)
|
||||
{
|
||||
if(text.isEmpty())
|
||||
{
|
||||
ui->label_ModDescription->setHidden(true);
|
||||
ui->descriptionLabel->setHidden(true);
|
||||
updateHiddenState();
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
ui->label_ModDescription->setHidden(false);
|
||||
ui->descriptionLabel->setHidden(false);
|
||||
updateHiddenState();
|
||||
}
|
||||
ui->label_ModDescription->setToolTip("");
|
||||
ui->descriptionLabel->setToolTip("");
|
||||
QString intermediatetext = text.trimmed();
|
||||
bool prev(false);
|
||||
QChar rem('\n');
|
||||
@ -133,36 +137,36 @@ void MCModInfoFrame::setModDescription(QString text)
|
||||
labeltext.reserve(300);
|
||||
if(finaltext.length() > 290)
|
||||
{
|
||||
ui->label_ModDescription->setOpenExternalLinks(false);
|
||||
ui->label_ModDescription->setTextFormat(Qt::TextFormat::RichText);
|
||||
desc = text;
|
||||
ui->descriptionLabel->setOpenExternalLinks(false);
|
||||
ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText);
|
||||
m_description = text;
|
||||
// This allows injecting HTML here.
|
||||
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
|
||||
{
|
||||
ui->label_ModDescription->setTextFormat(Qt::TextFormat::PlainText);
|
||||
ui->descriptionLabel->setTextFormat(Qt::TextFormat::PlainText);
|
||||
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);
|
||||
connect(currentBox, &QMessageBox::finished, this, &MCModInfoFrame::boxClosed);
|
||||
currentBox->show();
|
||||
m_current_box = CustomMessageBox::selectable(this, "", m_description);
|
||||
connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed);
|
||||
m_current_box->show();
|
||||
}
|
||||
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
|
||||
|
||||
#include <QFrame>
|
||||
|
||||
#include "minecraft/mod/Mod.h"
|
||||
#include "minecraft/mod/ResourcePack.h"
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class MCModInfoFrame;
|
||||
class InfoFrame;
|
||||
}
|
||||
|
||||
class MCModInfoFrame : public QFrame
|
||||
{
|
||||
class InfoFrame : public QFrame {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MCModInfoFrame(QWidget *parent = 0);
|
||||
~MCModInfoFrame();
|
||||
public:
|
||||
InfoFrame(QWidget* parent = nullptr);
|
||||
~InfoFrame() override;
|
||||
|
||||
void setModText(QString text);
|
||||
void setModDescription(QString text);
|
||||
void setName(QString text = {});
|
||||
void setDescription(QString text = {});
|
||||
|
||||
void updateWithMod(Mod &m);
|
||||
void clear();
|
||||
|
||||
public slots:
|
||||
void modDescEllipsisHandler(const QString& link );
|
||||
void updateWithMod(Mod const& m);
|
||||
void updateWithResource(Resource const& resource);
|
||||
|
||||
public slots:
|
||||
void descriptionEllipsisHandler(QString link);
|
||||
void boxClosed(int result);
|
||||
|
||||
private:
|
||||
private:
|
||||
void updateHiddenState();
|
||||
|
||||
private:
|
||||
Ui::MCModInfoFrame *ui;
|
||||
QString desc;
|
||||
class QMessageBox * currentBox = nullptr;
|
||||
private:
|
||||
Ui::InfoFrame* ui;
|
||||
QString m_description;
|
||||
class QMessageBox* m_current_box = nullptr;
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MCModInfoFrame</class>
|
||||
<widget class="QFrame" name="MCModInfoFrame">
|
||||
<class>InfoFrame</class>
|
||||
<widget class="QFrame" name="InfoFrame">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
@ -39,7 +39,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_ModText">
|
||||
<widget class="QLabel" name="nameLabel">
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
@ -61,7 +61,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_ModDescription">
|
||||
<widget class="QLabel" name="descriptionLabel">
|
||||
<property name="toolTip">
|
||||
<string notr="true"/>
|
||||
</property>
|
Loading…
Reference in New Issue
Block a user