Merge 0f5a890051d968c4b6bec9121fb3f718ed6777ad into 79b7e277f1f06f6b315e293b029423fe35e57431

This commit is contained in:
Alexandru Ionut Tripon 2025-08-01 20:16:19 +09:00 committed by GitHub
commit 290a0f1f09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 725 additions and 156 deletions

View File

@ -106,6 +106,20 @@ int Mod::compare(const Resource& other, SortType type) const
return compare_result;
break;
}
case SortType::REQUIRED_BY: {
if (requiredByCount() > cast_other->requiredByCount())
return 1;
if (requiredByCount() < cast_other->requiredByCount())
return -1;
break;
}
case SortType::REQUIRES: {
if (requiresCount() > cast_other->requiresCount())
return 1;
if (requiresCount() < cast_other->requiresCount())
return -1;
break;
}
}
return 0;
}
@ -284,3 +298,24 @@ bool Mod::valid() const
{
return !m_local_details.mod_id.isEmpty();
}
QStringList Mod::dependencies() const
{
return details().dependencies;
}
int Mod::requiredByCount() const
{
return m_requiredByCount;
}
int Mod::requiresCount() const
{
return m_requiresCount;
}
void Mod::setRequiredByCount(int value)
{
m_requiredByCount = value;
}
void Mod::setRequiresCount(int value)
{
m_requiresCount = value;
}

View File

@ -72,6 +72,13 @@ class Mod : public Resource {
auto loaders() const -> QString;
auto mcVersions() const -> QString;
auto releaseType() const -> QString;
QStringList dependencies() const;
int requiredByCount() const;
int requiresCount() const;
void setRequiredByCount(int value);
void setRequiresCount(int value);
/** Get the intneral path to the mod's icon file*/
QString iconPath() const { return m_local_details.icon_file; }
@ -104,4 +111,7 @@ class Mod : public Resource {
bool wasEverUsed = false;
bool wasReadAttempt = false;
} mutable m_packImageCacheKey;
int m_requiredByCount = 0;
int m_requiresCount = 0;
};

View File

@ -142,6 +142,8 @@ struct ModDetails {
/* Path of mod logo */
QString icon_file = {};
QStringList dependencies = {};
ModDetails() = default;
/** Metadata should be handled manually to properly set the mod status. */
@ -156,6 +158,7 @@ struct ModDetails {
, issue_tracker(other.issue_tracker)
, licenses(other.licenses)
, icon_file(other.icon_file)
, dependencies(other.dependencies)
{}
ModDetails& operator=(const ModDetails& other) = default;

View File

@ -38,6 +38,7 @@
#include "ModFolderModel.h"
#include <FileSystem.h>
#include <qabstractitemmodel.h>
#include <QDebug>
#include <QFileSystemWatcher>
#include <QHeaderView>
@ -48,25 +49,34 @@
#include <QThreadPool>
#include <QUrl>
#include <QUuid>
#include <algorithm>
#include "Application.h"
#include "minecraft/Component.h"
#include "minecraft/mod/Resource.h"
#include "minecraft/mod/ResourceFolderModel.h"
#include "minecraft/mod/tasks/LocalModParseTask.h"
#include "modplatform/ModIndex.h"
ModFolderModel::ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent)
: ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent)
{
m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider", "Size", "Side", "Loaders",
"Minecraft Versions", "Release Type" });
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider"),
tr("Size"), tr("Side"), tr("Loaders"), tr("Minecraft Versions"), tr("Release Type") });
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION,
SortType::DATE, SortType::PROVIDER, SortType::SIZE, SortType::SIDE,
SortType::LOADERS, SortType::MC_VERSIONS, SortType::RELEASE_TYPE };
"Minecraft Versions", "Release Type", "Requires", "Required by" });
m_column_names_translated =
QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider"), tr("Size"), tr("Side"),
tr("Loaders"), tr("Minecraft Versions"), tr("Release Type"), tr("Requires "), tr("Required by") });
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, SortType::DATE,
SortType::PROVIDER, SortType::SIZE, SortType::SIDE, SortType::LOADERS, SortType::MC_VERSIONS,
SortType::RELEASE_TYPE, SortType::REQUIRES, SortType::REQUIRED_BY };
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive,
QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive };
m_columnsHideable = { false, true, false, true, true, true, true, true, true, true, true };
QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive,
QHeaderView::Interactive };
m_columnsHideable = { false, true, false, true, true, true, true, true, true, true, true, true, true };
connect(this, &ModFolderModel::parseFinished, this, &ModFolderModel::onParseFinished);
}
QVariant ModFolderModel::data(const QModelIndex& index, int role) const
@ -110,8 +120,15 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const
case ReleaseTypeColumn: {
return at(row).releaseType();
}
case SizeColumn:
case SizeColumn: {
return at(row).sizeStr();
}
case RequiredByColumn: {
return at(row).requiredByCount();
}
case RequiresColumn: {
return at(row).requiresCount();
}
default:
return QVariant();
}
@ -168,6 +185,8 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio
case McVersionsColumn:
case ReleaseTypeColumn:
case SizeColumn:
case RequiredByColumn:
case RequiresColumn:
return columnNames().at(section);
default:
return QVariant();
@ -195,6 +214,10 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio
return tr("The release type.");
case SizeColumn:
return tr("The size of the mod.");
case RequiredByColumn:
return tr("Number of mods for what this is needed.");
case RequiresColumn:
return tr("Number of mods that this requires.");
default:
return QVariant();
}
@ -240,3 +263,178 @@ void ModFolderModel::onParseSucceeded(int ticket, QString mod_id)
emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
}
Mod* findById(QList<Mod*> mods, QString modId)
{
auto found = std::find_if(mods.begin(), mods.end(), [modId](Mod* m) { return m->mod_id() == modId; });
return found != mods.end() ? *found : nullptr;
}
void ModFolderModel::onParseFinished()
{
if (hasPendingParseTasks()) {
return;
}
auto mods = allMods();
auto findByProjectID = [mods](QVariant modId, ModPlatform::ResourceProvider provider) -> Mod* {
auto found = std::find_if(mods.begin(), mods.end(), [modId, provider](Mod* m) {
return m->metadata() && m->metadata()->provider == provider && m->metadata()->project_id == modId;
});
return found != mods.end() ? *found : nullptr;
};
for (auto mod : mods) {
auto id = mod->mod_id();
for (auto dep : mod->dependencies()) {
auto d = findById(mods, dep);
if (d) {
m_requires[id] << d;
m_requiredBy[d->mod_id()] << mod;
}
}
if (mod->metadata()) {
for (auto dep : mod->metadata()->dependencies) {
if (dep.type == ModPlatform::DependencyType::REQUIRED) {
auto d = findByProjectID(dep.addonId, mod->metadata()->provider);
if (d) {
m_requires[id] << d;
m_requiredBy[d->mod_id()] << mod;
}
}
}
}
}
for (auto mod : mods) {
auto id = mod->mod_id();
mod->setRequiredByCount(m_requiredBy[id].count());
mod->setRequiresCount(m_requires[id].count());
int row = m_resources_index[mod->internal_id()];
emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
}
}
QList<Mod*> collectMods(QList<Mod*> mods, QHash<QString, QSet<Mod*>> relation, std::set<QString>& seen)
{
QList<Mod*> affectedList = {};
for (auto mod : mods) {
auto id = mod->mod_id();
if (seen.count(id) == 0) {
seen.insert(id);
for (auto affected : relation[id]) {
auto affectedId = affected->mod_id();
if (findById(mods, affectedId) == nullptr && seen.count(affectedId) == 0) {
seen.insert(affectedId);
affectedList << affected;
}
}
}
}
// collect the affected mods until all of them are included in the list
if (!affectedList.isEmpty()) {
affectedList += collectMods(affectedList, relation, seen);
}
return affectedList;
}
QModelIndexList ModFolderModel::getAffectedMods(const QModelIndexList& indexes, EnableAction action)
{
if (indexes.isEmpty())
return {};
QModelIndexList affectedList = {};
auto affectedMods = selectedMods(indexes);
std::set<QString> seen;
switch (action) {
case EnableAction::ENABLE: {
affectedMods << collectMods(affectedMods, m_requires, seen);
break;
}
case EnableAction::DISABLE: {
affectedMods << collectMods(affectedMods, m_requiredBy, seen);
break;
}
case EnableAction::TOGGLE: {
return {}; // this function should not be called with TOGGLE
}
}
bool shouldBeEnabled = action == EnableAction::ENABLE;
for (auto affected : affectedMods) {
auto affectedId = affected->mod_id();
if (shouldBeEnabled != affected->enabled()) {
auto row = m_resources_index[affected->internal_id()];
affectedList << index(row, 0);
}
}
return affectedList;
}
bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action)
{
if (indexes.isEmpty())
return {};
QModelIndexList affectedList = {};
auto indexedMods = selectedMods(indexes);
QList<Mod*> toEnable = {};
QList<Mod*> toDisable = {};
std::set<QString> seen;
switch (action) {
case EnableAction::ENABLE: {
toEnable = indexedMods;
break;
}
case EnableAction::DISABLE: {
toDisable = indexedMods;
break;
}
case EnableAction::TOGGLE: {
for (auto mod : indexedMods) {
if (mod->enabled()) {
toDisable << mod;
} else {
toEnable << mod;
}
}
break;
}
}
toEnable << collectMods(toEnable, m_requires, seen);
toDisable << collectMods(toDisable, m_requiredBy, seen);
toDisable.removeIf([toEnable](Mod* m) { return toEnable.contains(m); });
auto toList = [this](QList<Mod*> mods, bool shouldBeEnabled) {
QModelIndexList list;
for (auto mod : mods) {
if (shouldBeEnabled != mod->enabled()) {
auto row = m_resources_index[mod->internal_id()];
list << index(row, 0);
}
}
return list;
};
auto disableStatus = ResourceFolderModel::setResourceEnabled(toList(toDisable, false), EnableAction::DISABLE);
auto enableStatus = ResourceFolderModel::setResourceEnabled(toList(toEnable, true), EnableAction::ENABLE);
return disableStatus && enableStatus;
}
QStringList reqToList(QSet<Mod*> l)
{
QStringList req;
for (auto m : l) {
req << m->name();
}
return req;
}
QStringList ModFolderModel::requiresList(QString id)
{
return reqToList(m_requires[id]);
}
QStringList ModFolderModel::requiredByList(QString id)
{
return reqToList(m_requiredBy[id]);
}

View File

@ -39,13 +39,15 @@
#include <QAbstractListModel>
#include <QDir>
#include <QList>
#include <QHash>
#include <QMap>
#include <QSet>
#include <QString>
#include "Mod.h"
#include "ResourceFolderModel.h"
#include "minecraft/Component.h"
#include "minecraft/mod/Resource.h"
class BaseInstance;
class QFileSystemWatcher;
@ -69,6 +71,8 @@ class ModFolderModel : public ResourceFolderModel {
LoadersColumn,
McVersionsColumn,
ReleaseTypeColumn,
RequiresColumn,
RequiredByColumn,
NUM_COLUMNS
};
ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr);
@ -85,8 +89,20 @@ class ModFolderModel : public ResourceFolderModel {
bool isValid();
bool setResourceEnabled(const QModelIndexList& indexes, EnableAction action) override;
QModelIndexList getAffectedMods(const QModelIndexList& indexes, EnableAction action);
RESOURCE_HELPERS(Mod)
public:
QStringList requiresList(QString id);
QStringList requiredByList(QString id);
private slots:
void onParseSucceeded(int ticket, QString resource_id) override;
void onParseFinished();
private:
QHash<QString, QSet<Mod*>> m_requiredBy;
QHash<QString, QSet<Mod*>> m_requires;
};

View File

@ -58,7 +58,21 @@ enum class ResourceStatus {
UNKNOWN, // Default status
};
enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER, SIZE, SIDE, MC_VERSIONS, LOADERS, RELEASE_TYPE };
enum class SortType {
NAME,
DATE,
VERSION,
ENABLED,
PACK_FORMAT,
PROVIDER,
SIZE,
SIDE,
MC_VERSIONS,
LOADERS,
RELEASE_TYPE,
REQUIRES,
REQUIRED_BY,
};
enum class EnableAction { ENABLE, DISABLE, TOGGLE };
@ -152,9 +166,6 @@ class Resource : public QObject {
bool isMoreThanOneHardLink() const;
auto mod_id() const -> QString { return m_mod_id; }
void setModId(const QString& modId) { m_mod_id = modId; }
protected:
/* The file corresponding to this resource. */
QFileInfo m_file_info;
@ -165,7 +176,6 @@ class Resource : public QObject {
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;
QString m_mod_id;
/* The type of file we're dealing with. */
ResourceType m_type = ResourceType::UNKNOWN;

View File

@ -882,6 +882,7 @@ QList<Resource*> ResourceFolderModel::allResources()
result.append((resource.get()));
return result;
}
QList<Resource*> ResourceFolderModel::selectedResources(const QModelIndexList& indexes)
{
QList<Resource*> result;

View File

@ -224,7 +224,6 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen
pDep->version.is_currently_selected = true;
pDep->pack->versions = { pDep->version };
pDep->pack->versionsLoaded = true;
} catch (const JSONValidationError& e) {
removePack(dep.addonId);
qDebug() << doc;

View File

@ -62,6 +62,36 @@ ModDetails ReadMCModInfo(QByteArray contents)
for (auto author : authors) {
details.authors.append(author.toString());
}
if (details.mod_id.startsWith("mod_")) {
details.mod_id = details.mod_id.mid(4);
}
auto addDep = [&details](QString dep) {
if (dep == "mod_MinecraftForge" || dep == "Forge")
return;
if (dep.contains(":")) {
dep = dep.section(":", 1);
}
if (dep.contains("@")) {
dep = dep.section("@", 0, 0);
}
if (dep.startsWith("mod_")) {
dep = dep.mid(4);
}
details.dependencies.append(dep);
};
if (firstObj.contains("requiredMods")) {
for (auto dep : firstObj.value("requiredMods").toArray()) {
addDep(dep.toString());
}
} else if (firstObj.contains("dependencies")) {
for (auto dep : firstObj.value("dependencies").toArray()) {
addDep(dep.toString());
}
}
return details;
};
QJsonParseError jsonError;
@ -199,6 +229,52 @@ ModDetails ReadMCModTOML(QByteArray contents)
}
details.icon_file = logoFile;
auto parseDep = [&details](toml::array* dependencies) {
static const QStringList ignoreModIds = { "", "forge", "neoforge", "minecraft" };
if (!dependencies) {
return;
}
auto isNeoForgeDep = [](toml::table* t) {
auto type = (*t)["type"].as_string();
return type && type->get() == "required";
};
auto isForgeDep = [](toml::table* t) {
auto mandatory = (*t)["mandatory"].as_boolean();
return mandatory && mandatory->get();
};
for (auto& dep : *dependencies) {
auto dep_table = dep.as_table();
if (!dep_table) {
continue;
}
auto modId = (*dep_table)["modId"].as_string();
if (!modId || ignoreModIds.contains(modId->get())) {
continue;
}
if (isNeoForgeDep(dep_table) || isForgeDep(dep_table)) {
details.dependencies.append(QString::fromStdString(modId->get()));
}
}
};
if (tomlData.contains("dependencies")) {
auto depValue = tomlData["dependencies"];
if (auto array = depValue.as_array()) {
parseDep(array);
} else if (auto depTable = depValue.as_table()) {
auto expectedKey = details.mod_id.toStdString();
if (!depTable->contains(expectedKey)) {
for (auto [k, v] : *depTable) {
expectedKey = k;
break;
}
}
if (auto array = (*depTable)[expectedKey].as_array()) {
parseDep(array);
}
}
}
return details;
}
@ -286,6 +362,18 @@ ModDetails ReadFabricModInfo(QByteArray contents)
details.icon_file = icon.toString();
}
}
if (object.contains("depends")) {
auto depends = object.value("depends");
if (depends.isObject()) {
auto obj = depends.toObject();
for (auto key : obj.keys()) {
if (key != "fabricloader" && key != "minecraft" && !key.startsWith("fabric-")) {
details.dependencies.append(key);
}
}
}
}
}
return details;
}
@ -373,6 +461,29 @@ ModDetails ReadQuiltModInfo(QByteArray contents)
details.icon_file = icon.toString();
}
}
if (object.contains("depends")) {
auto depends = object.value("depends");
if (depends.isArray()) {
auto array = depends.toArray();
for (auto obj : array) {
QString modId;
if (obj.isString()) {
modId = obj.toString();
} else if (obj.isObject()) {
auto objValue = obj.toObject();
modId = objValue.value("id").toString();
if (objValue.contains("optional") && objValue.value("optional").toBool()) {
continue;
}
} else {
continue;
}
if (modId != "minecraft" && !modId.startsWith("quilt_")) {
details.dependencies.append(modId);
}
}
}
}
}
} catch (const Exception& e) {

View File

@ -122,7 +122,7 @@ auto getModLoaderAsString(ModLoaderType type) -> const QString
case Cauldron:
return "cauldron";
case LiteLoader:
return "liteloader";
return "liteloader";
case Fabric:
return "fabric";
case Quilt:
@ -185,4 +185,40 @@ Side SideUtils::fromString(QString side)
return Side::UniversalSide;
return Side::UniversalSide;
}
QString DependencyTypeUtils::toString(DependencyType type)
{
switch (type) {
case DependencyType::REQUIRED:
return "REQUIRED";
case DependencyType::OPTIONAL:
return "OPTIONAL";
case DependencyType::INCOMPATIBLE:
return "INCOMPATIBLE";
case DependencyType::EMBEDDED:
return "EMBEDDED";
case DependencyType::TOOL:
return "TOOL";
case DependencyType::INCLUDE:
return "INCLUDE";
case DependencyType::UNKNOWN:
return "UNKNOWN";
}
return "UNKNOWN";
}
DependencyType DependencyTypeUtils::fromString(const QString& str)
{
static const QHash<QString, DependencyType> map = {
{ "REQUIRED", DependencyType::REQUIRED },
{ "OPTIONAL", DependencyType::OPTIONAL },
{ "INCOMPATIBLE", DependencyType::INCOMPATIBLE },
{ "EMBEDDED", DependencyType::EMBEDDED },
{ "TOOL", DependencyType::TOOL },
{ "INCLUDE", DependencyType::INCLUDE },
{ "UNKNOWN", DependencyType::UNKNOWN },
};
return map.value(str.toUpper(), DependencyType::UNKNOWN);
}
} // namespace ModPlatform

View File

@ -54,6 +54,11 @@ QString toString(Side side);
Side fromString(QString side);
} // namespace SideUtils
namespace DependencyTypeUtils {
QString toString(DependencyType type);
DependencyType fromString(const QString& str);
} // namespace DependencyTypeUtils
namespace ProviderCapabilities {
const char* name(ResourceProvider);
QString readableName(ResourceProvider);

View File

@ -122,6 +122,7 @@ auto V1::createModFormat([[maybe_unused]] const QDir& index_dir,
if (mod.version_number.isNull()) // on CurseForge, there is only a version name - not a version number
mod.version_number = mod_version.version;
mod.dependencies = mod_version.dependencies;
return mod;
}
@ -190,6 +191,16 @@ void V1::updateModIndex(const QDir& index_dir, Mod& mod)
return;
}
toml::array deps;
for (auto dep : mod.dependencies) {
auto tbl = toml::table{ { "addonId", dep.addonId.toString().toStdString() },
{ "type", ModPlatform::DependencyTypeUtils::toString(dep.type).toStdString() } };
if (!dep.version.isEmpty()) {
tbl.emplace("version", dep.version.toStdString());
}
deps.push_back(tbl);
}
// Put TOML data into the file
QTextStream in_stream(&index_file);
{
@ -200,6 +211,7 @@ void V1::updateModIndex(const QDir& index_dir, Mod& mod)
{ "x-prismlauncher-mc-versions", mcVersions },
{ "x-prismlauncher-release-type", mod.releaseType.toString().toStdString() },
{ "x-prismlauncher-version-number", mod.version_number.toStdString() },
{ "x-prismlauncher-dependencies", deps },
{ "download",
toml::table{
{ "mode", mod.mode.toStdString() },
@ -330,6 +342,21 @@ auto V1::getIndexForMod(const QDir& index_dir, QString slug) -> Mod
return {};
}
}
{ // dependencies
auto deps = table["x-prismlauncher-dependencies"].as_array();
if (deps) {
for (auto&& depNode : *deps) {
auto dep = depNode.as_table();
if (dep) {
ModPlatform::Dependency d;
d.addonId = stringEntry(*dep, "addonId");
d.version = stringEntry(*dep, "version");
d.type = ModPlatform::DependencyTypeUtils::fromString(stringEntry(*dep, "type"));
mod.dependencies << d;
}
}
}
}
return mod;
}

View File

@ -55,6 +55,8 @@ class V1 {
QVariant project_id{};
QString version_number{};
QList<ModPlatform::Dependency> dependencies;
public:
// This is a totally heuristic, but should work for now.
auto isValid() const -> bool { return !slug.isEmpty() && !project_id.isNull(); }

View File

@ -37,6 +37,7 @@
*/
#include "ModFolderPage.h"
#include "minecraft/mod/Resource.h"
#include "ui/dialogs/ExportToModListDialog.h"
#include "ui_ExternalResourcesPage.h"
@ -90,7 +91,7 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel>
auto depsDisabled = APPLICATION->settings()->getSetting("ModDependenciesDisabled");
ui->actionVerifyItemDependencies->setVisible(!depsDisabled->get().toBool());
connect(depsDisabled.get(), &Setting::SettingChanged, this,
[this](const Setting& setting, const QVariant& value) { ui->actionVerifyItemDependencies->setVisible(!value.toBool()); });
[this](const Setting&, const QVariant& value) { ui->actionVerifyItemDependencies->setVisible(!value.toBool()); });
updateMenu->addAction(ui->actionResetItemMetadata);
connect(ui->actionResetItemMetadata, &QAction::triggered, this, &ModFolderPage::deleteModMetadata);
@ -118,7 +119,7 @@ void ModFolderPage::updateFrame(const QModelIndex& current, [[maybe_unused]] con
auto sourceCurrent = m_filterModel->mapToSource(current);
int row = sourceCurrent.row();
const Mod& mod = m_model->at(row);
ui->frame->updateWithMod(mod);
ui->frame->updateWithMod(mod, m_model->requiresList(mod.mod_id()), m_model->requiredByList(mod.mod_id()));
}
void ModFolderPage::removeItems(const QItemSelection& selection)
@ -133,7 +134,22 @@ void ModFolderPage::removeItems(const QItemSelection& selection)
if (response != QMessageBox::Yes)
return;
}
m_model->deleteResources(selection.indexes());
auto indexes = selection.indexes();
auto affected = m_model->getAffectedMods(indexes, EnableAction::DISABLE);
if (!affected.isEmpty()) {
auto response = CustomMessageBox::selectable(this, tr("Confirm Disable"),
tr("The mods you are tring to disable are required by %1 mods.\n"
"Do you want to disable them?")
.arg(affected.length()),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (response != QMessageBox::Yes) {
m_model->setResourceEnabled(affected, EnableAction::DISABLE);
}
}
m_model->deleteResources(indexes);
}
void ModFolderPage::downloadMods()

View File

@ -54,28 +54,34 @@ void setupLinkToolTip(QLabel* label)
});
}
InfoFrame::InfoFrame(QWidget* parent) : QFrame(parent), ui(new Ui::InfoFrame)
InfoFrame::InfoFrame(QWidget* parent) : QFrame(parent), m_ui(new Ui::InfoFrame)
{
ui->setupUi(this);
ui->descriptionLabel->setHidden(true);
ui->nameLabel->setHidden(true);
ui->licenseLabel->setHidden(true);
ui->issueTrackerLabel->setHidden(true);
m_ui->setupUi(this);
m_ui->descriptionLabel->setHidden(true);
m_ui->nameLabel->setHidden(true);
m_ui->licenseLabel->setHidden(true);
m_ui->issueTrackerLabel->setHidden(true);
setupLinkToolTip(ui->iconLabel);
setupLinkToolTip(ui->descriptionLabel);
setupLinkToolTip(ui->nameLabel);
setupLinkToolTip(ui->licenseLabel);
setupLinkToolTip(ui->issueTrackerLabel);
setupLinkToolTip(m_ui->iconLabel);
setupLinkToolTip(m_ui->descriptionLabel);
setupLinkToolTip(m_ui->nameLabel);
setupLinkToolTip(m_ui->licenseLabel);
setupLinkToolTip(m_ui->issueTrackerLabel);
updateHiddenState();
connect(m_ui->moreInfoBtn, &QPushButton::clicked, this, [this]() {
auto nextIndex = (m_ui->infoStacked->currentIndex() + 1) % 2;
m_ui->infoStacked->setCurrentIndex(nextIndex);
m_ui->moreInfoBtn->setText(nextIndex == 0 ? ">" : "<");
});
m_ui->moreInfoBtn->hide();
}
InfoFrame::~InfoFrame()
{
delete ui;
delete m_ui;
}
void InfoFrame::updateWithMod(Mod const& m)
void InfoFrame::updateWithMod(Mod const& m, QStringList requiresList, QStringList requiredByList)
{
if (m.type() == ResourceType::FOLDER) {
clear();
@ -141,6 +147,26 @@ void InfoFrame::updateWithMod(Mod const& m)
issueTracker += "<a href=\"" + m.issueTracker() + "\">" + m.issueTracker() + "</a>";
}
setIssueTracker(issueTracker);
if (requiredByList.isEmpty()) {
m_ui->requiredGB->hide();
} else {
m_ui->requiredGB->show();
m_ui->requiredView->clear();
m_ui->requiredView->addItems(requiredByList);
}
if (requiresList.isEmpty()) {
m_ui->requiresGB->hide();
} else {
m_ui->requiresGB->show();
m_ui->requiresView->clear();
m_ui->requiresView->addItems(requiresList);
}
if (requiresList.isEmpty() && requiredByList.isEmpty()) {
m_ui->infoStacked->setCurrentIndex(0);
m_ui->moreInfoBtn->setText(">");
}
m_ui->moreInfoBtn->setHidden(requiresList.isEmpty() && requiredByList.isEmpty());
}
void InfoFrame::updateWithResource(const Resource& resource)
@ -227,7 +253,8 @@ void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack)
setImage(resource_pack.image({ 64, 64 }));
}
void InfoFrame::updateWithDataPack(DataPack& data_pack) {
void InfoFrame::updateWithDataPack(DataPack& data_pack)
{
setName(renderColorCodes(data_pack.name()));
setDescription(renderColorCodes(data_pack.description()));
setImage(data_pack.image({ 64, 64 }));
@ -254,12 +281,13 @@ void InfoFrame::clear()
setImage();
setLicense();
setIssueTracker();
m_ui->moreInfoBtn->hide();
}
void InfoFrame::updateHiddenState()
{
if (ui->descriptionLabel->isHidden() && ui->nameLabel->isHidden() && ui->licenseLabel->isHidden() &&
ui->issueTrackerLabel->isHidden()) {
if (m_ui->descriptionLabel->isHidden() && m_ui->nameLabel->isHidden() && m_ui->licenseLabel->isHidden() &&
m_ui->issueTrackerLabel->isHidden()) {
setHidden(true);
} else {
setHidden(false);
@ -269,10 +297,10 @@ void InfoFrame::updateHiddenState()
void InfoFrame::setName(QString text)
{
if (text.isEmpty()) {
ui->nameLabel->setHidden(true);
m_ui->nameLabel->setHidden(true);
} else {
ui->nameLabel->setText(text);
ui->nameLabel->setHidden(false);
m_ui->nameLabel->setText(text);
m_ui->nameLabel->setHidden(false);
}
updateHiddenState();
}
@ -280,14 +308,14 @@ void InfoFrame::setName(QString text)
void InfoFrame::setDescription(QString text)
{
if (text.isEmpty()) {
ui->descriptionLabel->setHidden(true);
m_ui->descriptionLabel->setHidden(true);
updateHiddenState();
return;
} else {
ui->descriptionLabel->setHidden(false);
m_ui->descriptionLabel->setHidden(false);
updateHiddenState();
}
ui->descriptionLabel->setToolTip("");
m_ui->descriptionLabel->setToolTip("");
QString intermediatetext = text.trimmed();
bool prev(false);
QChar rem('\n');
@ -309,8 +337,8 @@ void InfoFrame::setDescription(QString text)
doc.setHtml(text);
if (doc.characterCount() > maxCharacterElide) {
ui->descriptionLabel->setOpenExternalLinks(false);
ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText); // This allows injecting HTML here.
m_ui->descriptionLabel->setOpenExternalLinks(false);
m_ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText); // This allows injecting HTML here.
m_description = text;
// move the cursor to the character elide, doesn't see html
@ -323,25 +351,25 @@ void InfoFrame::setDescription(QString text)
cursor.insertHtml("<a href=\"#mod_desc\">...</a>");
labeltext.append(doc.toHtml());
connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler);
connect(m_ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler);
} else {
ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText);
m_ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText);
labeltext.append(finaltext);
}
ui->descriptionLabel->setText(labeltext);
m_ui->descriptionLabel->setText(labeltext);
}
void InfoFrame::setLicense(QString text)
{
if (text.isEmpty()) {
ui->licenseLabel->setHidden(true);
m_ui->licenseLabel->setHidden(true);
updateHiddenState();
return;
} else {
ui->licenseLabel->setHidden(false);
m_ui->licenseLabel->setHidden(false);
updateHiddenState();
}
ui->licenseLabel->setToolTip("");
m_ui->licenseLabel->setToolTip("");
QString intermediatetext = text.trimmed();
bool prev(false);
QChar rem('\n');
@ -357,26 +385,26 @@ void InfoFrame::setLicense(QString text)
QString labeltext;
labeltext.reserve(300);
if (finaltext.length() > 290) {
ui->licenseLabel->setOpenExternalLinks(false);
ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText);
m_ui->licenseLabel->setOpenExternalLinks(false);
m_ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText);
m_license = text;
// This allows injecting HTML here.
labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>");
connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler);
connect(m_ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler);
} else {
ui->licenseLabel->setTextFormat(Qt::TextFormat::AutoText);
m_ui->licenseLabel->setTextFormat(Qt::TextFormat::AutoText);
labeltext.append(finaltext);
}
ui->licenseLabel->setText(labeltext);
m_ui->licenseLabel->setText(labeltext);
}
void InfoFrame::setIssueTracker(QString text)
{
if (text.isEmpty()) {
ui->issueTrackerLabel->setHidden(true);
m_ui->issueTrackerLabel->setHidden(true);
} else {
ui->issueTrackerLabel->setText(text);
ui->issueTrackerLabel->setHidden(false);
m_ui->issueTrackerLabel->setText(text);
m_ui->issueTrackerLabel->setHidden(false);
}
updateHiddenState();
}
@ -384,10 +412,10 @@ void InfoFrame::setIssueTracker(QString text)
void InfoFrame::setImage(QPixmap img)
{
if (img.isNull()) {
ui->iconLabel->setHidden(true);
m_ui->iconLabel->setHidden(true);
} else {
ui->iconLabel->setHidden(false);
ui->iconLabel->setPixmap(img);
m_ui->iconLabel->setHidden(false);
m_ui->iconLabel->setPixmap(img);
}
}

View File

@ -61,7 +61,7 @@ class InfoFrame : public QFrame {
void clear();
void updateWithMod(Mod const& m);
void updateWithMod(Mod const& m, QStringList requiresList = {}, QStringList requiredByList = {});
void updateWithResource(Resource const& resource);
void updateWithResourcePack(ResourcePack& rp);
void updateWithDataPack(DataPack& rp);
@ -78,7 +78,7 @@ class InfoFrame : public QFrame {
void updateHiddenState();
private:
Ui::InfoFrame* ui;
Ui::InfoFrame* m_ui;
QString m_description;
QString m_license;
class QMessageBox* m_current_box = nullptr;

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>527</width>
<height>113</height>
<height>130</height>
</rect>
</property>
<property name="sizePolicy">
@ -19,7 +19,7 @@
<property name="maximumSize">
<size>
<width>16777215</width>
<height>120</height>
<height>130</height>
</size>
</property>
<layout class="QGridLayout" name="gridLayout">
@ -35,6 +35,169 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item row="1" column="0">
<widget class="QPushButton" name="moreInfoBtn">
<property name="text">
<string>&gt;</string>
</property>
</widget>
</item>
<item row="0" column="1" rowspan="2">
<widget class="QStackedWidget" name="infoStacked">
<widget class="QWidget" name="base">
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="2">
<widget class="QLabel" name="descriptionLabel">
<property name="toolTip">
<string notr="true"/>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="nameLabel">
<property name="text">
<string notr="true"/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QLabel" name="issueTrackerLabel">
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLabel" name="licenseLabel">
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="extra">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QGroupBox" name="requiresGB">
<property name="title">
<string>Requires</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QListWidget" name="requiresView">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="resizeMode">
<enum>QListView::Adjust</enum>
</property>
<property name="spacing">
<number>10</number>
</property>
<property name="viewMode">
<enum>QListView::IconMode</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="requiredGB">
<property name="title">
<string>Required by</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QListWidget" name="requiredView">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="movement">
<enum>QListView::Static</enum>
</property>
<property name="resizeMode">
<enum>QListView::Adjust</enum>
</property>
<property name="spacing">
<number>10</number>
</property>
<property name="viewMode">
<enum>QListView::IconMode</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="0" column="0" rowspan="2">
<widget class="QLabel" name="iconLabel">
<property name="minimumSize">
@ -60,97 +223,6 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="descriptionLabel">
<property name="toolTip">
<string notr="true"/>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="nameLabel">
<property name="text">
<string notr="true"/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="licenseLabel">
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLabel" name="issueTrackerLabel">
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>