Merge remote-tracking branch 'upstream/develop' into unify-mc-settings

Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
TheKodeToad 2025-01-27 17:02:50 +00:00
commit cc504f4a6c
No known key found for this signature in database
GPG key ID: 5E39D70B4C93C38E
58 changed files with 738 additions and 640 deletions

View file

@ -614,6 +614,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("IconsDir", "icons");
m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
m_settings->registerSetting("DownloadsDirWatchRecursive", false);
m_settings->registerSetting("MoveModsFromDownloadsDir", false);
m_settings->registerSetting("SkinsDir", "skins");
m_settings->registerSetting("JavaDir", "java");

View file

@ -1024,8 +1024,6 @@ SET(LAUNCHER_SOURCES
ui/dialogs/CopyInstanceDialog.h
ui/dialogs/CustomMessageBox.cpp
ui/dialogs/CustomMessageBox.h
ui/dialogs/EditAccountDialog.cpp
ui/dialogs/EditAccountDialog.h
ui/dialogs/ExportInstanceDialog.cpp
ui/dialogs/ExportInstanceDialog.h
ui/dialogs/ExportPackDialog.cpp
@ -1079,8 +1077,6 @@ SET(LAUNCHER_SOURCES
ui/widgets/CustomCommands.h
ui/widgets/EnvironmentVariables.cpp
ui/widgets/EnvironmentVariables.h
ui/widgets/DropLabel.cpp
ui/widgets/DropLabel.h
ui/widgets/FocusLineEdit.cpp
ui/widgets/FocusLineEdit.h
ui/widgets/IconLabel.cpp
@ -1215,7 +1211,6 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/MSALoginDialog.ui
ui/dialogs/OfflineLoginDialog.ui
ui/dialogs/AboutDialog.ui
ui/dialogs/EditAccountDialog.ui
ui/dialogs/ReviewMessageBox.ui
ui/dialogs/ScrollMessageBox.ui
ui/dialogs/BlockedModsDialog.ui

View file

@ -47,24 +47,24 @@
#define MAX_SIZE 1024
IconList::IconList(const QStringList& builtinPaths, QString path, QObject* parent) : QAbstractListModel(parent)
IconList::IconList(const QStringList& builtinPaths, const QString& path, QObject* parent) : QAbstractListModel(parent)
{
QSet<QString> builtinNames;
// add builtin icons
for (auto& builtinPath : builtinPaths) {
QDir instance_icons(builtinPath);
auto file_info_list = instance_icons.entryInfoList(QDir::Files, QDir::Name);
for (auto file_info : file_info_list) {
builtinNames.insert(file_info.completeBaseName());
for (const auto& builtinPath : builtinPaths) {
QDir instanceIcons(builtinPath);
auto fileInfoList = instanceIcons.entryInfoList(QDir::Files, QDir::Name);
for (const auto& fileInfo : fileInfoList) {
builtinNames.insert(fileInfo.baseName());
}
}
for (auto& builtinName : builtinNames) {
for (const auto& builtinName : builtinNames) {
addThemeIcon(builtinName);
}
m_watcher.reset(new QFileSystemWatcher());
is_watching = false;
m_isWatching = false;
connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &IconList::directoryChanged);
connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &IconList::fileChanged);
@ -77,91 +77,131 @@ IconList::IconList(const QStringList& builtinPaths, QString path, QObject* paren
void IconList::sortIconList()
{
qDebug() << "Sorting icon list...";
std::sort(icons.begin(), icons.end(), [](const MMCIcon& a, const MMCIcon& b) { return a.m_key.localeAwareCompare(b.m_key) < 0; });
std::sort(m_icons.begin(), m_icons.end(), [](const MMCIcon& a, const MMCIcon& b) {
bool aIsSubdir = a.m_key.contains(QDir::separator());
bool bIsSubdir = b.m_key.contains(QDir::separator());
if (aIsSubdir != bIsSubdir) {
return !aIsSubdir; // root-level icons come first
}
return a.m_key.localeAwareCompare(b.m_key) < 0;
});
reindex();
}
// Helper function to add directories recursively
bool IconList::addPathRecursively(const QString& path)
{
QDir dir(path);
if (!dir.exists())
return false;
// Add the directory itself
bool watching = m_watcher->addPath(path);
// Add all subdirectories
QFileInfoList entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const QFileInfo& entry : entries) {
if (addPathRecursively(entry.absoluteFilePath())) {
watching = true;
}
}
return watching;
}
QStringList IconList::getIconFilePaths() const
{
QStringList iconFiles{};
QStringList directories{ m_dir.absolutePath() };
while (!directories.isEmpty()) {
QString first = directories.takeFirst();
QDir dir(first);
for (QFileInfo& fileInfo : dir.entryInfoList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, QDir::Name)) {
if (fileInfo.isDir())
directories.push_back(fileInfo.absoluteFilePath());
else
iconFiles.push_back(fileInfo.absoluteFilePath());
}
}
return iconFiles;
}
QString formatName(const QDir& iconsDir, const QFileInfo& iconFile)
{
if (iconFile.dir() == iconsDir)
return iconFile.baseName();
constexpr auto delimiter = " » ";
QString relativePathWithoutExtension = iconsDir.relativeFilePath(iconFile.dir().path()) + QDir::separator() + iconFile.baseName();
return relativePathWithoutExtension.replace(QDir::separator(), delimiter);
}
/// Split into a separate function because the preprocessing impedes readability
QSet<QString> toStringSet(const QList<QString>& list)
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QSet<QString> set(list.begin(), list.end());
#else
QSet<QString> set = list.toSet();
#endif
return set;
}
void IconList::directoryChanged(const QString& path)
{
QDir new_dir(path);
if (m_dir.absolutePath() != new_dir.absolutePath()) {
m_dir.setPath(path);
QDir newDir(path);
if (m_dir.absolutePath() != newDir.absolutePath()) {
if (!path.startsWith(m_dir.absolutePath()))
m_dir.setPath(path);
m_dir.refresh();
if (is_watching)
if (m_isWatching)
stopWatching();
startWatching();
}
if (!m_dir.exists())
if (!FS::ensureFolderPathExists(m_dir.absolutePath()))
return;
if (!m_dir.exists() && !FS::ensureFolderPathExists(m_dir.absolutePath()))
return;
m_dir.refresh();
auto new_list = m_dir.entryList(QDir::Files, QDir::Name);
for (auto it = new_list.begin(); it != new_list.end(); it++) {
QString& foo = (*it);
foo = m_dir.filePath(foo);
}
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QSet<QString> new_set(new_list.begin(), new_list.end());
#else
auto new_set = new_list.toSet();
#endif
QList<QString> current_list;
for (auto& it : icons) {
const QStringList newFileNamesList = getIconFilePaths();
const QSet<QString> newSet = toStringSet(newFileNamesList);
QSet<QString> currentSet;
for (const MMCIcon& it : m_icons) {
if (!it.has(IconType::FileBased))
continue;
current_list.push_back(it.m_images[IconType::FileBased].filename);
currentSet.insert(it.m_images[IconType::FileBased].filename);
}
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QSet<QString> current_set(current_list.begin(), current_list.end());
#else
QSet<QString> current_set = current_list.toSet();
#endif
QSet<QString> toRemove = currentSet - newSet;
QSet<QString> toAdd = newSet - currentSet;
QSet<QString> to_remove = current_set;
to_remove -= new_set;
QSet<QString> to_add = new_set;
to_add -= current_set;
for (auto remove : to_remove) {
qDebug() << "Removing " << remove;
QFileInfo rmfile(remove);
QString key = rmfile.completeBaseName();
QString suffix = rmfile.suffix();
// The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well
if (!IconUtils::isIconSuffix(suffix))
key = rmfile.fileName();
for (const QString& removedPath : toRemove) {
qDebug() << "Removing icon " << removedPath;
QFileInfo removedFile(removedPath);
QString key = m_dir.relativeFilePath(removedFile.absoluteFilePath());
int idx = getIconIndex(key);
if (idx == -1)
continue;
icons[idx].remove(IconType::FileBased);
if (icons[idx].type() == IconType::ToBeDeleted) {
m_icons[idx].remove(FileBased);
if (m_icons[idx].type() == ToBeDeleted) {
beginRemoveRows(QModelIndex(), idx, idx);
icons.remove(idx);
m_icons.remove(idx);
reindex();
endRemoveRows();
} else {
dataChanged(index(idx), index(idx));
}
m_watcher->removePath(remove);
m_watcher->removePath(removedPath);
emit iconUpdated(key);
}
for (auto add : to_add) {
qDebug() << "Adding " << add;
for (const QString& addedPath : toAdd) {
qDebug() << "Adding icon " << addedPath;
QFileInfo addfile(add);
QString key = addfile.completeBaseName();
QFileInfo addfile(addedPath);
QString relativePath = m_dir.relativeFilePath(addfile.absoluteFilePath());
QString key = QFileInfo(relativePath).completeBaseName();
QString name = formatName(m_dir, addfile);
QString suffix = addfile.suffix();
// The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well
if (!IconUtils::isIconSuffix(suffix))
key = addfile.fileName();
if (addIcon(key, QString(), addfile.filePath(), IconType::FileBased)) {
m_watcher->addPath(add);
if (addIcon(key, name, addfile.filePath(), IconType::FileBased)) {
m_watcher->addPath(addedPath);
emit iconUpdated(key);
}
}
@ -171,24 +211,24 @@ void IconList::directoryChanged(const QString& path)
void IconList::fileChanged(const QString& path)
{
qDebug() << "Checking " << path;
qDebug() << "Checking icon " << path;
QFileInfo checkfile(path);
if (!checkfile.exists())
return;
QString key = checkfile.completeBaseName();
QString key = m_dir.relativeFilePath(checkfile.absoluteFilePath());
int idx = getIconIndex(key);
if (idx == -1)
return;
QIcon icon(path);
if (!icon.availableSizes().size())
if (icon.availableSizes().empty())
return;
icons[idx].m_images[IconType::FileBased].icon = icon;
m_icons[idx].m_images[IconType::FileBased].icon = icon;
dataChanged(index(idx), index(idx));
emit iconUpdated(key);
}
void IconList::SettingChanged(const Setting& setting, QVariant value)
void IconList::SettingChanged(const Setting& setting, const QVariant& value)
{
if (setting.id() != "IconsDir")
return;
@ -200,8 +240,8 @@ void IconList::startWatching()
{
auto abs_path = m_dir.absolutePath();
FS::ensureFolderPathExists(abs_path);
is_watching = m_watcher->addPath(abs_path);
if (is_watching) {
m_isWatching = addPathRecursively(abs_path);
if (m_isWatching) {
qDebug() << "Started watching " << abs_path;
} else {
qDebug() << "Failed to start watching " << abs_path;
@ -212,7 +252,7 @@ void IconList::stopWatching()
{
m_watcher->removePaths(m_watcher->files());
m_watcher->removePaths(m_watcher->directories());
is_watching = false;
m_isWatching = false;
}
QStringList IconList::mimeTypes() const
@ -242,7 +282,7 @@ bool IconList::dropMimeData(const QMimeData* data,
if (data->hasUrls()) {
auto urls = data->urls();
QStringList iconFiles;
for (auto url : urls) {
for (const auto& url : urls) {
// only local files may be dropped...
if (!url.isLocalFile())
continue;
@ -263,33 +303,33 @@ Qt::ItemFlags IconList::flags(const QModelIndex& index) const
QVariant IconList::data(const QModelIndex& index, int role) const
{
if (!index.isValid())
return QVariant();
return {};
int row = index.row();
if (row < 0 || row >= icons.size())
return QVariant();
if (row < 0 || row >= m_icons.size())
return {};
switch (role) {
case Qt::DecorationRole:
return icons[row].icon();
return m_icons[row].icon();
case Qt::DisplayRole:
return icons[row].name();
return m_icons[row].name();
case Qt::UserRole:
return icons[row].m_key;
return m_icons[row].m_key;
default:
return QVariant();
return {};
}
}
int IconList::rowCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : icons.size();
return parent.isValid() ? 0 : m_icons.size();
}
void IconList::installIcons(const QStringList& iconFiles)
{
for (QString file : iconFiles)
for (const QString& file : iconFiles)
installIcon(file, {});
}
@ -312,12 +352,13 @@ bool IconList::iconFileExists(const QString& key) const
return iconEntry && iconEntry->has(IconType::FileBased);
}
/// Returns the icon with the given key or nullptr if it doesn't exist.
const MMCIcon* IconList::icon(const QString& key) const
{
int iconIdx = getIconIndex(key);
if (iconIdx == -1)
return nullptr;
return &icons[iconIdx];
return &m_icons[iconIdx];
}
bool IconList::deleteIcon(const QString& key)
@ -332,22 +373,22 @@ bool IconList::trashIcon(const QString& key)
bool IconList::addThemeIcon(const QString& key)
{
auto iter = name_index.find(key);
if (iter != name_index.end()) {
auto& oldOne = icons[*iter];
auto iter = m_nameIndex.find(key);
if (iter != m_nameIndex.end()) {
auto& oldOne = m_icons[*iter];
oldOne.replace(Builtin, key);
dataChanged(index(*iter), index(*iter));
return true;
}
// add a new icon
beginInsertRows(QModelIndex(), icons.size(), icons.size());
beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size());
{
MMCIcon mmc_icon;
mmc_icon.m_name = key;
mmc_icon.m_key = key;
mmc_icon.replace(Builtin, key);
icons.push_back(mmc_icon);
name_index[key] = icons.size() - 1;
m_icons.push_back(mmc_icon);
m_nameIndex[key] = m_icons.size() - 1;
}
endInsertRows();
return true;
@ -359,22 +400,22 @@ bool IconList::addIcon(const QString& key, const QString& name, const QString& p
QIcon icon(path);
if (icon.isNull())
return false;
auto iter = name_index.find(key);
if (iter != name_index.end()) {
auto& oldOne = icons[*iter];
auto iter = m_nameIndex.find(key);
if (iter != m_nameIndex.end()) {
auto& oldOne = m_icons[*iter];
oldOne.replace(type, icon, path);
dataChanged(index(*iter), index(*iter));
return true;
}
// add a new icon
beginInsertRows(QModelIndex(), icons.size(), icons.size());
beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size());
{
MMCIcon mmc_icon;
mmc_icon.m_name = name;
mmc_icon.m_key = key;
mmc_icon.replace(type, icon, path);
icons.push_back(mmc_icon);
name_index[key] = icons.size() - 1;
m_icons.push_back(mmc_icon);
m_nameIndex[key] = m_icons.size() - 1;
}
endInsertRows();
return true;
@ -389,33 +430,32 @@ void IconList::saveIcon(const QString& key, const QString& path, const char* for
void IconList::reindex()
{
name_index.clear();
int i = 0;
for (auto& iter : icons) {
name_index[iter.m_key] = i;
i++;
m_nameIndex.clear();
for (int i = 0; i < m_icons.size(); i++) {
m_nameIndex[m_icons[i].m_key] = i;
emit iconUpdated(m_icons[i].m_key); // prevents incorrect indices with proxy model
}
}
QIcon IconList::getIcon(const QString& key) const
{
int icon_index = getIconIndex(key);
int iconIndex = getIconIndex(key);
if (icon_index != -1)
return icons[icon_index].icon();
if (iconIndex != -1)
return m_icons[iconIndex].icon();
// Fallback for icons that don't exist.
icon_index = getIconIndex("grass");
// Fallback for icons that don't exist.b
iconIndex = getIconIndex("grass");
if (icon_index != -1)
return icons[icon_index].icon();
return QIcon();
if (iconIndex != -1)
return m_icons[iconIndex].icon();
return {};
}
int IconList::getIconIndex(const QString& key) const
{
auto iter = name_index.find(key == "default" ? "grass" : key);
if (iter != name_index.end())
auto iter = m_nameIndex.find(key == "default" ? "grass" : key);
if (iter != m_nameIndex.end())
return *iter;
return -1;
@ -425,3 +465,15 @@ QString IconList::getDirectory() const
{
return m_dir.absolutePath();
}
/// Returns the directory of the icon with the given key or the default directory if it's a builtin icon.
QString IconList::iconDirectory(const QString& key) const
{
for (const auto& mmcIcon : m_icons) {
if (mmcIcon.m_key == key && mmcIcon.has(IconType::FileBased)) {
QFileInfo iconFile(mmcIcon.getFilePath());
return iconFile.dir().path();
}
}
return getDirectory();
}

View file

@ -51,7 +51,7 @@ class QFileSystemWatcher;
class IconList : public QAbstractListModel {
Q_OBJECT
public:
explicit IconList(const QStringList& builtinPaths, QString path, QObject* parent = 0);
explicit IconList(const QStringList& builtinPaths, const QString& path, QObject* parent = 0);
virtual ~IconList() {};
QIcon getIcon(const QString& key) const;
@ -72,6 +72,7 @@ class IconList : public QAbstractListModel {
bool deleteIcon(const QString& key);
bool trashIcon(const QString& key);
bool iconFileExists(const QString& key) const;
QString iconDirectory(const QString& key) const;
void installIcons(const QStringList& iconFiles);
void installIcon(const QString& file, const QString& name);
@ -91,18 +92,20 @@ class IconList : public QAbstractListModel {
IconList& operator=(const IconList&) = delete;
void reindex();
void sortIconList();
bool addPathRecursively(const QString& path);
QStringList getIconFilePaths() const;
public slots:
void directoryChanged(const QString& path);
protected slots:
void fileChanged(const QString& path);
void SettingChanged(const Setting& setting, QVariant value);
void SettingChanged(const Setting& setting, const QVariant& value);
private:
shared_qobject_ptr<QFileSystemWatcher> m_watcher;
bool is_watching;
QMap<QString, int> name_index;
QVector<MMCIcon> icons;
bool m_isWatching;
QMap<QString, int> m_nameIndex;
QVector<MMCIcon> m_icons;
QDir m_dir;
};

View file

@ -86,11 +86,10 @@ void ManifestDownloadTask::downloadJava(const QJsonDocument& doc)
if (type == "directory") {
FS::ensureFolderPathExists(file);
} else if (type == "link") {
// this is linux only !
// this is *nix only !
auto path = Json::ensureString(meta, "target");
if (!path.isEmpty()) {
auto target = FS::PathCombine(file, "../" + path);
QFile(target).link(file);
QFile::link(path, file);
}
} else if (type == "file") {
// TODO download compressed version if it exists ?

View file

@ -254,20 +254,60 @@ void LaunchTask::emitFailed(QString reason)
Task::emitFailed(reason);
}
void LaunchTask::substituteVariables(QStringList& args) const
QString expandVariables(const QString& input, QProcessEnvironment dict)
{
auto env = m_instance->createEnvironment();
QString result = input;
for (auto key : env.keys()) {
args.replaceInStrings("$" + key, env.value(key));
enum { base, maybeBrace, variable, brace } state = base;
int startIdx = -1;
for (int i = 0; i < result.length();) {
QChar c = result.at(i++);
switch (state) {
case base:
if (c == '$')
state = maybeBrace;
break;
case maybeBrace:
if (c == '{') {
state = brace;
startIdx = i;
} else if (c.isLetterOrNumber() || c == '_') {
state = variable;
startIdx = i - 1;
} else {
state = base;
}
break;
case brace:
if (c == '}') {
const auto res = dict.value(result.mid(startIdx, i - 1 - startIdx), "");
if (!res.isEmpty()) {
result.replace(startIdx - 2, i - startIdx + 2, res);
i = startIdx - 2 + res.length();
}
state = base;
}
break;
case variable:
if (!c.isLetterOrNumber() && c != '_') {
const auto res = dict.value(result.mid(startIdx, i - startIdx - 1), "");
if (!res.isEmpty()) {
result.replace(startIdx - 1, i - startIdx, res);
i = startIdx - 1 + res.length();
}
state = base;
}
break;
}
}
if (state == variable) {
if (const auto res = dict.value(result.mid(startIdx), ""); !res.isEmpty())
result.replace(startIdx - 1, result.length() - startIdx + 1, res);
}
return result;
}
void LaunchTask::substituteVariables(QString& cmd) const
QString LaunchTask::substituteVariables(QString& cmd, bool isLaunch) const
{
auto env = m_instance->createEnvironment();
for (auto key : env.keys()) {
cmd.replace("$" + key, env.value(key));
}
return expandVariables(cmd, isLaunch ? m_instance->createLaunchEnvironment() : m_instance->createEnvironment());
}

View file

@ -87,8 +87,7 @@ class LaunchTask : public Task {
shared_qobject_ptr<LogModel> getLogModel();
public:
void substituteVariables(QStringList& args) const;
void substituteVariables(QString& cmd) const;
QString substituteVariables(QString& cmd, bool isLaunch = false) const;
QString censorPrivateInfo(QString in);
protected: /* methods */

View file

@ -47,19 +47,15 @@ PostLaunchCommand::PostLaunchCommand(LaunchTask* parent) : LaunchStep(parent)
void PostLaunchCommand::executeTask()
{
// FIXME: where to put this?
auto cmd = m_parent->substituteVariables(m_command);
emit logLine(tr("Running Post-Launch command: %1").arg(cmd), MessageLevel::Launcher);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
auto args = QProcess::splitCommand(m_command);
m_parent->substituteVariables(args);
auto args = QProcess::splitCommand(cmd);
emit logLine(tr("Running Post-Launch command: %1").arg(args.join(' ')), MessageLevel::Launcher);
const QString program = args.takeFirst();
m_process.start(program, args);
#else
m_parent->substituteVariables(m_command);
emit logLine(tr("Running Post-Launch command: %1").arg(m_command), MessageLevel::Launcher);
m_process.start(m_command);
m_process.start(cmd);
#endif
}

View file

@ -47,19 +47,14 @@ PreLaunchCommand::PreLaunchCommand(LaunchTask* parent) : LaunchStep(parent)
void PreLaunchCommand::executeTask()
{
// FIXME: where to put this?
auto cmd = m_parent->substituteVariables(m_command);
emit logLine(tr("Running Pre-Launch command: %1").arg(cmd), MessageLevel::Launcher);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
auto args = QProcess::splitCommand(m_command);
m_parent->substituteVariables(args);
emit logLine(tr("Running Pre-Launch command: %1").arg(args.join(' ')), MessageLevel::Launcher);
auto args = QProcess::splitCommand(cmd);
const QString program = args.takeFirst();
m_process.start(program, args);
#else
m_parent->substituteVariables(m_command);
emit logLine(tr("Running Pre-Launch command: %1").arg(m_command), MessageLevel::Launcher);
m_process.start(m_command);
m_process.start(cmd);
#endif
}

View file

@ -594,6 +594,13 @@ QMap<QString, QString> MinecraftInstance::getVariables()
out.insert("INST_JAVA", settings()->get("JavaPath").toString());
out.insert("INST_JAVA_ARGS", javaArguments().join(' '));
out.insert("NO_COLOR", "1");
#ifdef Q_OS_MACOS
// get library for Steam overlay support
QString steamDyldInsertLibraries = qEnvironmentVariable("STEAM_DYLD_INSERT_LIBRARIES");
if (!steamDyldInsertLibraries.isEmpty()) {
out.insert("DYLD_INSERT_LIBRARIES", steamDyldInsertLibraries);
}
#endif
return out;
}
@ -1151,13 +1158,6 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
process->appendStep(step);
}
// run pre-launch command if that's needed
if (getPreLaunchCommand().size()) {
auto step = makeShared<PreLaunchCommand>(pptr);
step->setWorkingDirectory(gameRoot());
process->appendStep(step);
}
// load meta
{
auto mode = session->status != AuthSession::PlayableOffline ? Net::Mode::Online : Net::Mode::Offline;
@ -1170,6 +1170,13 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
process->appendStep(makeShared<CheckJava>(pptr));
}
// run pre-launch command if that's needed
if (getPreLaunchCommand().size()) {
auto step = makeShared<PreLaunchCommand>(pptr);
step->setWorkingDirectory(gameRoot());
process->appendStep(step);
}
// if we aren't in offline mode,.
if (session->status != AuthSession::PlayableOffline) {
if (!session->demo) {

View file

@ -8,7 +8,10 @@ void MinecraftLoadAndCheck::executeTask()
{
// add offline metadata load task
auto components = m_inst->getPackProfile();
components->reload(m_netmode);
if (auto result = components->reload(m_netmode); !result) {
emitFailed(result.error);
return;
}
m_task = components->getCurrentTask();
if (!m_task) {

View file

@ -173,29 +173,32 @@ static bool savePackProfile(const QString& filename, const ComponentContainer& c
}
// Read the given file into component containers
static bool loadPackProfile(PackProfile* parent,
const QString& filename,
const QString& componentJsonPattern,
ComponentContainer& container)
static PackProfile::Result loadPackProfile(PackProfile* parent,
const QString& filename,
const QString& componentJsonPattern,
ComponentContainer& container)
{
QFile componentsFile(filename);
if (!componentsFile.exists()) {
qCWarning(instanceProfileC) << "Components file" << filename << "doesn't exist. This should never happen.";
return false;
auto message = QObject::tr("Components file %1 doesn't exist. This should never happen.").arg(filename);
qCWarning(instanceProfileC) << message;
return PackProfile::Result::Error(message);
}
if (!componentsFile.open(QFile::ReadOnly)) {
qCCritical(instanceProfileC) << "Couldn't open" << componentsFile.fileName() << " for reading:" << componentsFile.errorString();
auto message = QObject::tr("Couldn't open %1 for reading: %2").arg(componentsFile.fileName(), componentsFile.errorString());
qCCritical(instanceProfileC) << message;
qCWarning(instanceProfileC) << "Ignoring overridden order";
return false;
return PackProfile::Result::Error(message);
}
// and it's valid JSON
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qCCritical(instanceProfileC) << "Couldn't parse" << componentsFile.fileName() << ":" << error.errorString();
auto message = QObject::tr("Couldn't parse %1 as json: %2").arg(componentsFile.fileName(), error.errorString());
qCCritical(instanceProfileC) << message;
qCWarning(instanceProfileC) << "Ignoring overridden order";
return false;
return PackProfile::Result::Error(message);
}
// and then read it and process it if all above is true.
@ -212,11 +215,13 @@ static bool loadPackProfile(PackProfile* parent,
container.append(componentFromJsonV1(parent, componentJsonPattern, comp_obj));
}
} catch ([[maybe_unused]] const JSONValidationError& err) {
qCCritical(instanceProfileC) << "Couldn't parse" << componentsFile.fileName() << ": bad file format";
auto message = QObject::tr("Couldn't parse %1 : bad file format").arg(componentsFile.fileName());
qCCritical(instanceProfileC) << message;
qCWarning(instanceProfileC) << "error:" << err.what();
container.clear();
return false;
return PackProfile::Result::Error(message);
}
return true;
return PackProfile::Result::Success();
}
// END: component file format
@ -283,44 +288,43 @@ void PackProfile::save_internal()
d->dirty = false;
}
bool PackProfile::load()
PackProfile::Result PackProfile::load()
{
auto filename = componentsFilePath();
// load the new component list and swap it with the current one...
ComponentContainer newComponents;
if (!loadPackProfile(this, filename, patchesPattern(), newComponents)) {
if (auto result = loadPackProfile(this, filename, patchesPattern(), newComponents); !result) {
qCritical() << d->m_instance->name() << "|" << "Failed to load the component config";
return false;
} else {
// FIXME: actually use fine-grained updates, not this...
beginResetModel();
// disconnect all the old components
for (auto component : d->components) {
disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
}
d->components.clear();
d->componentIndex.clear();
for (auto component : newComponents) {
if (d->componentIndex.contains(component->m_uid)) {
qWarning() << d->m_instance->name() << "|" << "Ignoring duplicate component entry" << component->m_uid;
continue;
}
connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
d->components.append(component);
d->componentIndex[component->m_uid] = component;
}
endResetModel();
d->loaded = true;
return true;
return result;
}
// FIXME: actually use fine-grained updates, not this...
beginResetModel();
// disconnect all the old components
for (auto component : d->components) {
disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
}
d->components.clear();
d->componentIndex.clear();
for (auto component : newComponents) {
if (d->componentIndex.contains(component->m_uid)) {
qWarning() << d->m_instance->name() << "|" << "Ignoring duplicate component entry" << component->m_uid;
continue;
}
connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
d->components.append(component);
d->componentIndex[component->m_uid] = component;
}
endResetModel();
d->loaded = true;
return Result::Success();
}
void PackProfile::reload(Net::Mode netmode)
PackProfile::Result PackProfile::reload(Net::Mode netmode)
{
// Do not reload when the update/resolve task is running. It is in control.
if (d->m_updateTask) {
return;
return Result::Success();
}
// flush any scheduled saves to not lose state
@ -329,9 +333,11 @@ void PackProfile::reload(Net::Mode netmode)
// FIXME: differentiate when a reapply is required by propagating state from components
invalidateLaunchProfile();
if (load()) {
resolve(netmode);
if (auto result = load(); !result) {
return result;
}
resolve(netmode);
return Result::Success();
}
Task::Ptr PackProfile::getCurrentTask()

View file

@ -62,6 +62,19 @@ class PackProfile : public QAbstractListModel {
public:
enum Columns { NameColumn = 0, VersionColumn, NUM_COLUMNS };
struct Result {
bool success;
QString error;
// Implicit conversion to bool
operator bool() const { return success; }
// Factory methods for convenience
static Result Success() { return { true, "" }; }
static Result Error(const QString& errorMessage) { return { false, errorMessage }; }
};
explicit PackProfile(MinecraftInstance* instance);
virtual ~PackProfile();
@ -102,7 +115,7 @@ class PackProfile : public QAbstractListModel {
bool revertToBase(int index);
/// reload the list, reload all components, resolve dependencies
void reload(Net::Mode netmode);
Result reload(Net::Mode netmode);
// reload all components, resolve dependencies
void resolve(Net::Mode netmode);
@ -169,7 +182,7 @@ class PackProfile : public QAbstractListModel {
void disableInteraction(bool disable);
private:
bool load();
Result load();
bool installJarMods_internal(QStringList filepaths);
bool installCustomJar_internal(QString filepath);
bool installAgents_internal(QStringList filepaths);

View file

@ -131,6 +131,7 @@ void LauncherPartLaunch::executeTask()
QString wrapperCommandStr = instance->getWrapperCommand().trimmed();
if (!wrapperCommandStr.isEmpty()) {
wrapperCommandStr = m_parent->substituteVariables(wrapperCommandStr);
auto wrapperArgs = Commandline::splitArgs(wrapperCommandStr);
auto wrapperCommand = wrapperArgs.takeFirst();
auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand);

View file

@ -48,6 +48,7 @@
#include "Version.h"
#include "minecraft/mod/ModDetails.h"
#include "minecraft/mod/tasks/LocalModParseTask.h"
#include "modplatform/ModIndex.h"
Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details()
{
@ -157,11 +158,8 @@ auto Mod::loaders() const -> QString
if (metadata()) {
QStringList loaders;
auto modLoaders = metadata()->loaders;
for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Cauldron, ModPlatform::LiteLoader, ModPlatform::Fabric,
ModPlatform::Quilt }) {
if (modLoaders & loader) {
loaders << getModLoaderAsString(loader);
}
for (auto loader : ModPlatform::modLoaderTypesToList(modLoaders)) {
loaders << getModLoaderAsString(loader);
}
return loaders.join(", ");
}

View file

@ -19,27 +19,28 @@ static ModrinthAPI modrinth_api;
static FlameAPI flame_api;
EnsureMetadataTask::EnsureMetadataTask(Resource* resource, QDir dir, ModPlatform::ResourceProvider prov)
: Task(), m_index_dir(dir), m_provider(prov), m_hashing_task(nullptr), m_current_task(nullptr)
: Task(), m_index_dir(dir), m_provider(prov), m_hashingTask(nullptr), m_current_task(nullptr)
{
auto hash_task = createNewHash(resource);
if (!hash_task)
auto hashTask = createNewHash(resource);
if (!hashTask)
return;
connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); });
connect(hash_task.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); });
hash_task->start();
connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); });
connect(hashTask.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); });
m_hashingTask = hashTask;
}
EnsureMetadataTask::EnsureMetadataTask(QList<Resource*>& resources, QDir dir, ModPlatform::ResourceProvider prov)
: Task(), m_index_dir(dir), m_provider(prov), m_current_task(nullptr)
{
m_hashing_task.reset(new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()));
auto hashTask = makeShared<ConcurrentTask>("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
m_hashingTask = hashTask;
for (auto* resource : resources) {
auto hash_task = createNewHash(resource);
if (!hash_task)
continue;
connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); });
connect(hash_task.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); });
m_hashing_task->addTask(hash_task);
hashTask->addTask(hash_task);
}
}

View file

@ -21,7 +21,7 @@ class EnsureMetadataTask : public Task {
~EnsureMetadataTask() = default;
Task::Ptr getHashingTask() { return m_hashing_task; }
Task::Ptr getHashingTask() { return m_hashingTask; }
public slots:
bool abort() override;
@ -59,6 +59,6 @@ class EnsureMetadataTask : public Task {
ModPlatform::ResourceProvider m_provider;
QHash<QString, ModPlatform::IndexedVersion> m_temp_versions;
ConcurrentTask::Ptr m_hashing_task;
Task::Ptr m_hashingTask;
Task::Ptr m_current_task;
};

View file

@ -31,6 +31,19 @@ static const QMap<QString, IndexedVersionType::VersionType> s_indexed_version_ty
{ "alpha", IndexedVersionType::VersionType::Alpha }
};
static const QList<ModLoaderType> loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric };
QList<ModLoaderType> modLoaderTypesToList(ModLoaderTypes flags)
{
QList<ModLoaderType> flagList;
for (auto flag : loaderList) {
if (flags.testFlag(flag)) {
flagList.append(flag);
}
}
return flagList;
}
IndexedVersionType::IndexedVersionType(const QString& type) : IndexedVersionType(enumFromString(type)) {}
IndexedVersionType::IndexedVersionType(const IndexedVersionType::VersionType& type)

View file

@ -32,6 +32,7 @@ namespace ModPlatform {
enum ModLoaderType { NeoForge = 1 << 0, Forge = 1 << 1, Cauldron = 1 << 2, LiteLoader = 1 << 3, Fabric = 1 << 4, Quilt = 1 << 5 };
Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType)
QList<ModLoaderType> modLoaderTypesToList(ModLoaderTypes flags);
enum class ResourceProvider { MODRINTH, FLAME };

View file

@ -87,6 +87,30 @@ void Flame::FileResolvingTask::executeTask()
m_task->start();
}
PackedResourceType getResourceType(int classId)
{
switch (classId) {
case 17: // Worlds
return PackedResourceType::WorldSave;
case 6: // Mods
return PackedResourceType::Mod;
case 12: // Resource Packs
// return PackedResourceType::ResourcePack; // not really a resourcepack
/* fallthrough */
case 4546: // Customization
// return PackedResourceType::ShaderPack; // not really a shaderPack
/* fallthrough */
case 4471: // Modpacks
/* fallthrough */
case 5: // Bukkit Plugins
/* fallthrough */
case 4559: // Addons
/* fallthrough */
default:
return PackedResourceType::UNKNOWN;
}
}
void Flame::FileResolvingTask::netJobFinished()
{
setProgress(1, 3);
@ -144,7 +168,7 @@ void Flame::FileResolvingTask::netJobFinished()
<< " reason: " << parse_error.errorString();
qWarning() << *m_result;
failed(parse_error.errorString());
getFlameProjects();
return;
}
@ -232,6 +256,10 @@ void Flame::FileResolvingTask::getFlameProjects()
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(file->version.fileName));
FlameMod::loadIndexedPack(file->pack, entry_obj);
file->resourceType = getResourceType(Json::requireInteger(entry_obj, "classId", "modClassId"));
if (file->resourceType == PackedResourceType::WorldSave) {
file->targetFolder = "saves";
}
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();

View file

@ -270,21 +270,44 @@ std::optional<ModPlatform::IndexedVersion> FlameAPI::getLatestVersion(QList<ModP
QList<ModPlatform::ModLoaderType> instanceLoaders,
ModPlatform::ModLoaderTypes modLoaders)
{
// edge case: mod has installed for forge but the instance is fabric => fabric version will be prioritizated on update
auto bestVersion = [&versions](ModPlatform::ModLoaderTypes loader) {
std::optional<ModPlatform::IndexedVersion> ver;
for (auto file_tmp : versions) {
if (file_tmp.loaders & loader && (!ver.has_value() || file_tmp.date > ver->date)) {
ver = file_tmp;
static const auto noLoader = ModPlatform::ModLoaderType(0);
QHash<ModPlatform::ModLoaderType, ModPlatform::IndexedVersion> bestMatch;
auto checkVersion = [&bestMatch](const ModPlatform::IndexedVersion& version, const ModPlatform::ModLoaderType& loader) {
if (bestMatch.contains(loader)) {
auto best = bestMatch.value(loader);
if (version.date > best.date) {
bestMatch[loader] = version;
}
} else {
bestMatch[loader] = version;
}
};
for (auto file_tmp : versions) {
auto loaders = ModPlatform::modLoaderTypesToList(file_tmp.loaders);
if (loaders.isEmpty()) {
checkVersion(file_tmp, noLoader);
} else {
for (auto loader : loaders) {
checkVersion(file_tmp, loader);
}
}
return ver;
};
for (auto l : instanceLoaders) {
auto ver = bestVersion(l);
if (ver.has_value()) {
return ver;
}
// edge case: mod has installed for forge but the instance is fabric => fabric version will be prioritizated on update
auto currentLoaders = instanceLoaders + ModPlatform::modLoaderTypesToList(modLoaders);
currentLoaders.append(noLoader); // add a fallback in case the versions do not define a loader
for (auto loader : currentLoaders) {
if (bestMatch.contains(loader)) {
auto bestForLoader = bestMatch.value(loader);
// awkward case where the mod has only two loaders and one of them is not specified
if (loader != noLoader && bestMatch.contains(noLoader) && bestMatch.size() == 2) {
auto bestForNoLoader = bestMatch.value(noLoader);
if (bestForNoLoader.date > bestForLoader.date) {
return bestForNoLoader;
}
}
return bestForLoader;
}
}
return bestVersion(modLoaders);
return {};
}

View file

@ -1,6 +1,5 @@
#pragma once
#include "Application.h"
#include "modplatform/CheckUpdateTask.h"
#include "net/NetJob.h"

View file

@ -75,12 +75,12 @@ bool FlameCreationTask::abort()
return false;
m_abort = true;
if (m_process_update_file_info_job)
m_process_update_file_info_job->abort();
if (m_files_job)
m_files_job->abort();
if (m_mod_id_resolver)
m_mod_id_resolver->abort();
if (m_processUpdateFileInfoJob)
m_processUpdateFileInfoJob->abort();
if (m_filesJob)
m_filesJob->abort();
if (m_modIdResolver)
m_modIdResolver->abort();
return Task::abort();
}
@ -232,12 +232,12 @@ bool FlameCreationTask::updateInstance()
connect(job.get(), &Task::failed, this, [](QString reason) { qCritical() << "Failed to get files: " << reason; });
connect(job.get(), &Task::finished, &loop, &QEventLoop::quit);
m_process_update_file_info_job = job;
m_processUpdateFileInfoJob = job;
job->start();
loop.exec();
m_process_update_file_info_job = nullptr;
m_processUpdateFileInfoJob = nullptr;
} else {
// We don't have an old index file, so we may duplicate stuff!
auto dialog = CustomMessageBox::selectable(m_parent, tr("No index file."),
@ -430,26 +430,26 @@ bool FlameCreationTask::createInstance()
}
// Don't add managed info to packs without an ID (most likely imported from ZIP)
if (!m_managed_id.isEmpty())
instance.setManagedPack("flame", m_managed_id, m_pack.name, m_managed_version_id, m_pack.version);
if (!m_managedId.isEmpty())
instance.setManagedPack("flame", m_managedId, m_pack.name, m_managedVersionId, m_pack.version);
else
instance.setManagedPack("flame", "", name(), "", "");
instance.setName(name());
m_mod_id_resolver.reset(new Flame::FileResolvingTask(APPLICATION->network(), m_pack));
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); });
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::failed, [this, &loop](QString reason) {
m_mod_id_resolver.reset();
m_modIdResolver.reset(new Flame::FileResolvingTask(APPLICATION->network(), m_pack));
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); });
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [this, &loop](QString reason) {
m_modIdResolver.reset();
setError(tr("Unable to resolve mod IDs:\n") + reason);
loop.quit();
});
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::aborted, &loop, &QEventLoop::quit);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propagateStepProgress);
connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails);
m_mod_id_resolver->start();
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::aborted, &loop, &QEventLoop::quit);
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress);
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus);
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propagateStepProgress);
connect(m_modIdResolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails);
m_modIdResolver->start();
loop.exec();
@ -468,14 +468,14 @@ bool FlameCreationTask::createInstance()
void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
{
auto results = m_mod_id_resolver->getResults();
auto results = m_modIdResolver->getResults();
// first check for blocked mods
QList<BlockedMod> blocked_mods;
auto anyBlocked = false;
for (const auto& result : results.files.values()) {
if (result.version.fileName.endsWith(".zip")) {
m_ZIP_resources.append(std::make_pair(result.version.fileName, result.targetFolder));
if (result.resourceType != PackedResourceType::Mod) {
m_otherResources.append(std::make_pair(result.version.fileName, result.targetFolder));
}
if (result.version.downloadUrl.isEmpty()) {
@ -507,7 +507,7 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
copyBlockedMods(blocked_mods);
setupDownloadJob(loop);
} else {
m_mod_id_resolver.reset();
m_modIdResolver.reset();
setError("Canceled");
loop.quit();
}
@ -518,8 +518,8 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
{
m_files_job.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network()));
auto results = m_mod_id_resolver->getResults().files;
m_filesJob.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network()));
auto results = m_modIdResolver->getResults().files;
QStringList optionalFiles;
for (auto& result : results) {
@ -554,26 +554,26 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
if (!result.version.downloadUrl.isEmpty()) {
qDebug() << "Will download" << result.version.downloadUrl << "to" << path;
auto dl = Net::ApiDownload::makeFile(result.version.downloadUrl, path);
m_files_job->addNetAction(dl);
m_filesJob->addNetAction(dl);
}
}
connect(m_files_job.get(), &NetJob::finished, this, [this, &loop]() {
m_files_job.reset();
validateZIPResources(loop);
connect(m_filesJob.get(), &NetJob::finished, this, [this, &loop]() {
m_filesJob.reset();
validateOtherResources(loop);
});
connect(m_files_job.get(), &NetJob::failed, [this](QString reason) {
m_files_job.reset();
connect(m_filesJob.get(), &NetJob::failed, [this](QString reason) {
m_filesJob.reset();
setError(reason);
});
connect(m_files_job.get(), &NetJob::progress, this, [this](qint64 current, qint64 total) {
connect(m_filesJob.get(), &NetJob::progress, this, [this](qint64 current, qint64 total) {
setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
setProgress(current, total);
});
connect(m_files_job.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress);
connect(m_filesJob.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress);
setStatus(tr("Downloading mods..."));
m_files_job->start();
m_filesJob->start();
}
/// @brief copy the matched blocked mods to the instance staging area
@ -597,8 +597,14 @@ void FlameCreationTask::copyBlockedMods(QList<BlockedMod> const& blocked_mods)
qDebug() << "Will try to copy" << mod.localPath << "to" << destPath;
if (!FS::copy(mod.localPath, destPath)()) {
qDebug() << "Copy of" << mod.localPath << "to" << destPath << "Failed";
if (mod.move) {
if (!FS::move(mod.localPath, destPath)) {
qDebug() << "Move of" << mod.localPath << "to" << destPath << "Failed";
}
} else {
if (!FS::copy(mod.localPath, destPath)()) {
qDebug() << "Copy of" << mod.localPath << "to" << destPath << "Failed";
}
}
i++;
@ -608,11 +614,11 @@ void FlameCreationTask::copyBlockedMods(QList<BlockedMod> const& blocked_mods)
setAbortable(true);
}
void FlameCreationTask::validateZIPResources(QEventLoop& loop)
void FlameCreationTask::validateOtherResources(QEventLoop& loop)
{
qDebug() << "Validating whether resources stored as .zip are in the right place";
qDebug() << "Validating whether other resources are in the right place";
QStringList zipMods;
for (auto [fileName, targetFolder] : m_ZIP_resources) {
for (auto [fileName, targetFolder] : m_otherResources) {
qDebug() << "Checking" << fileName << "...";
auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName);
@ -672,6 +678,7 @@ void FlameCreationTask::validateZIPResources(QEventLoop& loop)
installWorld(worldPath);
break;
case PackedResourceType::UNKNOWN:
/* fallthrough */
default:
qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is.";
break;
@ -679,7 +686,7 @@ void FlameCreationTask::validateZIPResources(QEventLoop& loop)
}
// TODO make this work with other sorts of resource
auto task = makeShared<ConcurrentTask>("CreateModMetadata", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
auto results = m_mod_id_resolver->getResults().files;
auto results = m_modIdResolver->getResults().files;
auto folder = FS::PathCombine(m_stagingPath, "minecraft", "mods", ".index");
for (auto file : results) {
if (file.targetFolder != "mods" || (file.version.fileName.endsWith(".zip") && !zipMods.contains(file.version.fileName))) {
@ -688,6 +695,6 @@ void FlameCreationTask::validateZIPResources(QEventLoop& loop)
task->addTask(makeShared<LocalResourceUpdateTask>(folder, file.pack, file.version));
}
connect(task.get(), &Task::finished, &loop, &QEventLoop::quit);
m_process_update_file_info_job = task;
m_processUpdateFileInfoJob = task;
task->start();
}

View file

@ -57,7 +57,7 @@ class FlameCreationTask final : public InstanceCreationTask {
QString id,
QString version_id,
QString original_instance_id = {})
: InstanceCreationTask(), m_parent(parent), m_managed_id(std::move(id)), m_managed_version_id(std::move(version_id))
: InstanceCreationTask(), m_parent(parent), m_managedId(std::move(id)), m_managedVersionId(std::move(version_id))
{
setStagingPath(staging_path);
setParentSettings(global_settings);
@ -74,22 +74,22 @@ class FlameCreationTask final : public InstanceCreationTask {
void idResolverSucceeded(QEventLoop&);
void setupDownloadJob(QEventLoop&);
void copyBlockedMods(QList<BlockedMod> const& blocked_mods);
void validateZIPResources(QEventLoop& loop);
void validateOtherResources(QEventLoop& loop);
QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion);
private:
QWidget* m_parent = nullptr;
shared_qobject_ptr<Flame::FileResolvingTask> m_mod_id_resolver;
shared_qobject_ptr<Flame::FileResolvingTask> m_modIdResolver;
Flame::Manifest m_pack;
// Handle to allow aborting
Task::Ptr m_process_update_file_info_job = nullptr;
NetJob::Ptr m_files_job = nullptr;
Task::Ptr m_processUpdateFileInfoJob = nullptr;
NetJob::Ptr m_filesJob = nullptr;
QString m_managed_id, m_managed_version_id;
QString m_managedId, m_managedVersionId;
QList<std::pair<QString, QString>> m_ZIP_resources;
QList<std::pair<QString, QString>> m_otherResources;
std::optional<InstancePtr> m_instance;
};

View file

@ -40,6 +40,7 @@
#include <QString>
#include <QUrl>
#include <QVector>
#include "minecraft/mod/tasks/LocalResourceParse.h"
#include "modplatform/ModIndex.h"
namespace Flame {
@ -54,6 +55,7 @@ struct File {
// our
QString targetFolder = QStringLiteral("mods");
PackedResourceType resourceType;
};
struct Modloader {

View file

@ -71,13 +71,15 @@ class ModrinthAPI : public NetworkResourceAPI {
static auto getSideFilters(QString side) -> const QString
{
if (side.isEmpty() || side == "both") {
if (side.isEmpty()) {
return {};
}
if (side == "both")
return QString("\"client_side:required\"],[\"server_side:required\"");
if (side == "client")
return QString("\"client_side:required\",\"client_side:optional\"");
return QString("\"client_side:required\",\"client_side:optional\"],[\"server_side:optional\",\"server_side:unsupported\"");
if (side == "server")
return QString("\"server_side:required\",\"server_side:optional\"");
return QString("\"server_side:required\",\"server_side:optional\"],[\"client_side:optional\",\"client_side:unsupported\"");
return {};
}

View file

@ -8,6 +8,7 @@
#include "QObjectPtr.h"
#include "ResourceDownloadTask.h"
#include "modplatform/ModIndex.h"
#include "modplatform/helpers/HashUtils.h"
#include "tasks/ConcurrentTask.h"
@ -107,10 +108,8 @@ void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr<QByteArray> resp
// Sometimes a version may have multiple files, one with "forge" and one with "fabric",
// so we may want to filter it
QString loader_filter;
static auto flags = { ModPlatform::ModLoaderType::NeoForge, ModPlatform::ModLoaderType::Forge,
ModPlatform::ModLoaderType::Quilt, ModPlatform::ModLoaderType::Fabric };
for (auto flag : flags) {
if (loader.has_value() && loader->testFlag(flag)) {
if (loader.has_value()) {
for (auto flag : ModPlatform::modLoaderTypesToList(*loader)) {
loader_filter = ModPlatform::getModLoaderAsString(flag);
break;
}

View file

@ -262,12 +262,14 @@ bool ModrinthCreationTask::createInstance()
mod->setDetails(d);
resources[file.hash.toHex()] = mod;
}
if (file.downloads.empty()) {
setError(tr("The file '%1' is missing a download link. This is invalid in the pack format.").arg(fileName));
return false;
}
qDebug() << "Will try to download" << file.downloads.front() << "to" << file_path;
auto dl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path);
dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
downloadMods->addNetAction(dl);
if (!file.downloads.empty()) {
// FIXME: This really needs to be put into a ConcurrentTask of
// MultipleOptionsTask's , once those exist :)

View file

@ -190,11 +190,8 @@ void V1::updateModIndex(const QDir& index_dir, Mod& mod)
}
toml::array loaders;
for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Cauldron, ModPlatform::LiteLoader, ModPlatform::Fabric,
ModPlatform::Quilt }) {
if (mod.loaders & loader) {
loaders.push_back(getModLoaderAsString(loader).toStdString());
}
for (auto loader : ModPlatform::modLoaderTypesToList(mod.loaders)) {
loaders.push_back(getModLoaderAsString(loader).toStdString());
}
toml::array mcVersions;
for (auto version : mod.mcVersions) {

View file

@ -1,7 +1,6 @@
[Icon Theme]
Name=Legacy
Comment=Default Icons
Inherits=default
Directories=8x8,16x16,22x22,24x24,32x32,32x32/instances,48x48,50x50/instances,64x64,128x128/instances,256x256,scalable,scalable/instances
[8x8]

View file

@ -51,11 +51,35 @@
#include <settings/SettingsObject.h>
#include "Application.h"
constexpr int MaxMclogsLines = 25000;
constexpr int InitialMclogsLines = 10000;
constexpr int FinalMclogsLines = 14900;
QString truncateLogForMclogs(const QString& logContent)
{
QStringList lines = logContent.split("\n");
if (lines.size() > MaxMclogsLines) {
QString truncatedLog = lines.mid(0, InitialMclogsLines).join("\n");
truncatedLog +=
"\n\n\n\n\n\n\n\n\n\n"
"------------------------------------------------------------\n"
"----------------------- Log truncated ----------------------\n"
"------------------------------------------------------------\n"
"----- Middle portion omitted to fit mclo.gs size limits ----\n"
"------------------------------------------------------------\n"
"\n\n\n\n\n\n\n\n\n\n";
truncatedLog += lines.mid(lines.size() - FinalMclogsLines - 1).join("\n");
return truncatedLog;
}
return logContent;
}
std::optional<QString> GuiUtil::uploadPaste(const QString& name, const QString& text, QWidget* parentWidget)
{
ProgressDialog dialog(parentWidget);
auto pasteTypeSetting = static_cast<PasteUpload::PasteType>(APPLICATION->settings()->get("PastebinType").toInt());
auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString();
bool shouldTruncate = false;
{
QUrl baseUrl;
@ -75,10 +99,36 @@ std::optional<QString> GuiUtil::uploadPaste(const QString& name, const QString&
if (response != QMessageBox::Yes)
return {};
if (baseUrl.toString() == "https://api.mclo.gs" && text.count("\n") > MaxMclogsLines) {
auto truncateResponse = CustomMessageBox::selectable(
parentWidget, QObject::tr("Confirm Truncation"),
QObject::tr("The log has %1 lines, exceeding mclo.gs' limit of %2.\n"
"The launcher can keep the first %3 and last %4 lines, trimming the middle.\n\n"
"If you choose 'No', mclo.gs will only keep the first %2 lines, cutting off "
"potentially useful info like crashes at the end.\n\n"
"Proceed with truncation?")
.arg(text.count("\n"))
.arg(MaxMclogsLines)
.arg(InitialMclogsLines)
.arg(FinalMclogsLines),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No)
->exec();
if (truncateResponse == QMessageBox::Cancel) {
return {};
}
shouldTruncate = truncateResponse == QMessageBox::Yes;
}
}
}
std::unique_ptr<PasteUpload> paste(new PasteUpload(parentWidget, text, pasteCustomAPIBaseSetting, pasteTypeSetting));
QString textToUpload = text;
if (shouldTruncate) {
textToUpload = truncateLogForMclogs(text);
}
std::unique_ptr<PasteUpload> paste(new PasteUpload(parentWidget, textToUpload, pasteCustomAPIBaseSetting, pasteTypeSetting));
dialog.execWithTask(paste.get());
if (!paste->wasSuccessful()) {

View file

@ -288,6 +288,8 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path)
qDebug() << "[Blocked Mods Dialog] Checking for match on hash: " << hash << "| From path:" << path;
auto downloadDir = QFileInfo(APPLICATION->settings()->get("DownloadsDir").toString()).absoluteFilePath();
auto moveFiles = APPLICATION->settings()->get("MoveModsFromDownloadsDir").toBool();
for (auto& mod : m_mods) {
if (mod.matched) {
continue;
@ -295,6 +297,9 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path)
if (mod.hash.compare(hash, Qt::CaseInsensitive) == 0) {
mod.matched = true;
mod.localPath = path;
if (moveFiles) {
mod.move = QFileInfo(path).absoluteFilePath().startsWith(downloadDir);
}
match = true;
qDebug() << "[Blocked Mods Dialog] Hash match found:" << mod.name << hash << "| From path:" << path;
@ -346,6 +351,8 @@ bool BlockedModsDialog::checkValidPath(QString path)
return fsName.compare(metaName) == 0;
};
auto downloadDir = QFileInfo(APPLICATION->settings()->get("DownloadsDir").toString()).absoluteFilePath();
auto moveFiles = APPLICATION->settings()->get("MoveModsFromDownloadsDir").toBool();
for (auto& mod : m_mods) {
if (compare(filename, mod.name)) {
// if the mod is not yet matched and doesn't have a hash then
@ -353,6 +360,9 @@ bool BlockedModsDialog::checkValidPath(QString path)
if (!mod.matched && mod.hash.isEmpty()) {
mod.matched = true;
mod.localPath = path;
if (moveFiles) {
mod.move = QFileInfo(path).absoluteFilePath().startsWith(downloadDir);
}
return false;
}
qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path;

View file

@ -42,6 +42,7 @@ struct BlockedMod {
bool matched;
QString localPath;
QString targetFolder;
bool move = false;
};
QT_BEGIN_NAMESPACE

View file

@ -1,64 +0,0 @@
/* 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 "EditAccountDialog.h"
#include <DesktopServices.h>
#include <QPushButton>
#include <QUrl>
#include "ui_EditAccountDialog.h"
EditAccountDialog::EditAccountDialog(const QString& text, QWidget* parent, int flags) : QDialog(parent), ui(new Ui::EditAccountDialog)
{
ui->setupUi(this);
ui->label->setText(text);
ui->label->setVisible(!text.isEmpty());
ui->userTextBox->setEnabled(flags & UsernameField);
ui->passTextBox->setEnabled(flags & PasswordField);
ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
}
EditAccountDialog::~EditAccountDialog()
{
delete ui;
}
void EditAccountDialog::on_label_linkActivated(const QString& link)
{
DesktopServices::openUrl(QUrl(link));
}
void EditAccountDialog::setUsername(const QString& user) const
{
ui->userTextBox->setText(user);
}
QString EditAccountDialog::username() const
{
return ui->userTextBox->text();
}
void EditAccountDialog::setPassword(const QString& pass) const
{
ui->passTextBox->setText(pass);
}
QString EditAccountDialog::password() const
{
return ui->passTextBox->text();
}

View file

@ -1,52 +0,0 @@
/* 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.
*/
#pragma once
#include <QDialog>
namespace Ui {
class EditAccountDialog;
}
class EditAccountDialog : public QDialog {
Q_OBJECT
public:
explicit EditAccountDialog(const QString& text = "", QWidget* parent = 0, int flags = UsernameField | PasswordField);
~EditAccountDialog();
void setUsername(const QString& user) const;
void setPassword(const QString& pass) const;
QString username() const;
QString password() const;
enum Flags {
NoFlags = 0,
//! Specifies that the dialog should have a username field.
UsernameField,
//! Specifies that the dialog should have a password field.
PasswordField,
};
private slots:
void on_label_linkActivated(const QString& link);
private:
Ui::EditAccountDialog* ui;
};

View file

@ -1,94 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EditAccountDialog</class>
<widget class="QDialog" name="EditAccountDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>148</height>
</rect>
</property>
<property name="windowTitle">
<string>Login</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string notr="true">Message label placeholder.</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="userTextBox">
<property name="placeholderText">
<string>Email</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="passTextBox">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string>Password</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>EditAccountDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>EditAccountDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View file

@ -15,7 +15,9 @@
#include <QFileDialog>
#include <QKeyEvent>
#include <QLineEdit>
#include <QPushButton>
#include <QSortFilterProxyModel>
#include "Application.h"
@ -33,6 +35,15 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui
ui->setupUi(this);
setWindowModality(Qt::WindowModal);
searchBar = new QLineEdit(this);
searchBar->setPlaceholderText(tr("Search..."));
ui->verticalLayout->insertWidget(0, searchBar);
proxyModel = new QSortFilterProxyModel(this);
proxyModel->setSourceModel(APPLICATION->icons().get());
proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
ui->iconView->setModel(proxyModel);
auto contentsWidget = ui->iconView;
contentsWidget->setViewMode(QListView::IconMode);
contentsWidget->setFlow(QListView::LeftToRight);
@ -57,7 +68,7 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui
contentsWidget->installEventFilter(this);
contentsWidget->setModel(APPLICATION->icons().get());
contentsWidget->setModel(proxyModel);
// NOTE: ResetRole forces the button to be on the left, while the OK/Cancel ones are on the right. We win.
auto buttonAdd = ui->buttonBox->addButton(tr("Add Icon"), QDialogButtonBox::ResetRole);
@ -76,6 +87,9 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui
auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), QDialogButtonBox::ResetRole);
connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder);
connect(searchBar, &QLineEdit::textChanged, this, &IconPickerDialog::filterIcons);
// Prevent incorrect indices from e.g. filesystem changes
connect(APPLICATION->icons().get(), &IconList::iconUpdated, this, [this]() { proxyModel->invalidate(); });
}
bool IconPickerDialog::eventFilter(QObject* obj, QEvent* evt)
@ -162,5 +176,10 @@ IconPickerDialog::~IconPickerDialog()
void IconPickerDialog::openFolder()
{
DesktopServices::openPath(APPLICATION->icons()->getDirectory(), true);
DesktopServices::openPath(APPLICATION->icons()->iconDirectory(selectedIconKey), true);
}
void IconPickerDialog::filterIcons(const QString& query)
{
proxyModel->setFilterFixedString(query);
}

View file

@ -16,6 +16,8 @@
#pragma once
#include <QDialog>
#include <QItemSelection>
#include <QLineEdit>
#include <QSortFilterProxyModel>
namespace Ui {
class IconPickerDialog;
@ -36,6 +38,8 @@ class IconPickerDialog : public QDialog {
private:
Ui::IconPickerDialog* ui;
QPushButton* buttonRemove;
QLineEdit* searchBar;
QSortFilterProxyModel* proxyModel;
private slots:
void selectionChanged(QItemSelection, QItemSelection);
@ -44,4 +48,5 @@ class IconPickerDialog : public QDialog {
void addNewIcon();
void removeSelectedIcon();
void openFolder();
void filterIcons(const QString& text);
};

View file

@ -30,6 +30,9 @@ Choose your name carefully:</string>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
<property name="buddy">
<cstring>nameEdit</cstring>
</property>

View file

@ -1,4 +1,5 @@
#include "ResourceUpdateDialog.h"
#include "Application.h"
#include "ChooseProviderDialog.h"
#include "CustomMessageBox.h"
#include "ProgressDialog.h"
@ -7,6 +8,7 @@
#include "minecraft/mod/tasks/GetModDependenciesTask.h"
#include "modplatform/ModIndex.h"
#include "modplatform/flame/FlameAPI.h"
#include "tasks/SequentialTask.h"
#include "ui_ReviewMessageBox.h"
#include "Markdown.h"
@ -411,8 +413,14 @@ void ResourceUpdateDialog::onMetadataFailed(Resource* resource, bool try_others,
connect(task.get(), &EnsureMetadataTask::metadataFailed, [this](Resource* candidate) { onMetadataFailed(candidate, false); });
connect(task.get(), &EnsureMetadataTask::failed,
[this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
m_second_try_metadata->addTask(task);
if (task->getHashingTask()) {
auto seq = makeShared<SequentialTask>();
seq->addTask(task->getHashingTask());
seq->addTask(task);
m_second_try_metadata->addTask(seq);
} else {
m_second_try_metadata->addTask(task);
}
} else {
QString reason{ tr("Couldn't find a valid version on the selected mod provider(s)") };

View file

@ -207,7 +207,7 @@
<item row="0" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Note: you only need to set this to access private data. Read the &lt;a href=&quot;https://docs.modrinth.com/#section/Authentication&quot;&gt;documentation&lt;/a&gt; for more information.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Note: you only need to set this to access private data. Read the &lt;a href=&quot;https://docs.modrinth.com/api/#authentication&quot;&gt;documentation&lt;/a&gt; for more information.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>

View file

@ -232,6 +232,7 @@ void LauncherPage::applySettings()
s->set("SkinsDir", ui->skinsDirTextBox->text());
s->set("JavaDir", ui->javaDirTextBox->text());
s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked());
s->set("MoveModsFromDownloadsDir", ui->downloadsDirMoveCheckBox->isChecked());
auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId();
switch (sortMode) {
@ -296,6 +297,7 @@ void LauncherPage::loadSettings()
ui->skinsDirTextBox->setText(s->get("SkinsDir").toString());
ui->javaDirTextBox->setText(s->get("JavaDir").toString());
ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool());
ui->downloadsDirMoveCheckBox->setChecked(s->get("MoveModsFromDownloadsDir").toBool());
QString sortMode = s->get("InstSortMode").toString();

View file

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>511</width>
<width>562</width>
<height>726</height>
</rect>
</property>
@ -38,7 +38,7 @@
<enum>QTabWidget::Rounded</enum>
</property>
<property name="currentIndex">
<number>0</number>
<number>2</number>
</property>
<widget class="QWidget" name="featuresTab">
<attribute name="title">
@ -48,7 +48,7 @@
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
<enum>Qt::ScrollBarAsNeeded</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
@ -58,8 +58,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>473</width>
<height>690</height>
<width>570</width>
<height>692</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_8">
@ -156,14 +156,28 @@
</widget>
</item>
<item row="9" column="1" colspan="2">
<widget class="QCheckBox" name="downloadsDirWatchRecursiveCheckBox">
<property name="toolTip">
<string>When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge).</string>
</property>
<property name="text">
<string>Check downloads folder recursively</string>
</property>
</widget>
<layout class="QHBoxLayout" name="downloadModsCheckLayout">
<item>
<widget class="QCheckBox" name="downloadsDirWatchRecursiveCheckBox">
<property name="toolTip">
<string>When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge).</string>
</property>
<property name="text">
<string>Check downloads folder recursively</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="downloadsDirMoveCheckBox">
<property name="toolTip">
<string>When enabled, it will move blocked resources instead of copying them.</string>
</property>
<property name="text">
<string>Move blocked resources</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="8" column="1">
<widget class="QLineEdit" name="downloadsDirTextBox"/>
@ -585,7 +599,7 @@
</sizepolicy>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
<enum>Qt::ScrollBarAsNeeded</enum>
</property>
<property name="undoRedoEnabled">
<bool>false</bool>
@ -637,15 +651,33 @@
</customwidgets>
<tabstops>
<tabstop>tabWidget</tabstop>
<tabstop>scrollArea</tabstop>
<tabstop>autoUpdateCheckBox</tabstop>
<tabstop>updateIntervalSpinBox</tabstop>
<tabstop>instDirTextBox</tabstop>
<tabstop>instDirBrowseBtn</tabstop>
<tabstop>modsDirTextBox</tabstop>
<tabstop>modsDirBrowseBtn</tabstop>
<tabstop>iconsDirTextBox</tabstop>
<tabstop>iconsDirBrowseBtn</tabstop>
<tabstop>javaDirTextBox</tabstop>
<tabstop>javaDirBrowseBtn</tabstop>
<tabstop>skinsDirTextBox</tabstop>
<tabstop>skinsDirBrowseBtn</tabstop>
<tabstop>downloadsDirTextBox</tabstop>
<tabstop>downloadsDirBrowseBtn</tabstop>
<tabstop>downloadsDirWatchRecursiveCheckBox</tabstop>
<tabstop>metadataDisableBtn</tabstop>
<tabstop>dependenciesDisableBtn</tabstop>
<tabstop>skipModpackUpdatePromptBtn</tabstop>
<tabstop>numberOfConcurrentTasksSpinBox</tabstop>
<tabstop>numberOfConcurrentDownloadsSpinBox</tabstop>
<tabstop>numberOfManualRetriesSpinBox</tabstop>
<tabstop>timeoutSecondsSpinBox</tabstop>
<tabstop>sortLastLaunchedBtn</tabstop>
<tabstop>sortByNameBtn</tabstop>
<tabstop>catOpacitySpinBox</tabstop>
<tabstop>preferMenuBarCheckBox</tabstop>
<tabstop>lineLimitSpinBox</tabstop>
<tabstop>checkStopLogging</tabstop>
<tabstop>consoleFont</tabstop>

View file

@ -60,7 +60,7 @@
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::NoDragDrop</enum>
<enum>QAbstractItemView::DropOnly</enum>
</property>
<property name="uniformRowHeights">
<bool>true</bool>

View file

@ -252,8 +252,11 @@ void VersionPage::updateButtons(int row)
bool VersionPage::reloadPackProfile()
{
try {
m_profile->reload(Net::Mode::Online);
return true;
auto result = m_profile->reload(Net::Mode::Online);
if (!result) {
QMessageBox::critical(this, tr("Error"), result.error);
}
return result;
} catch (const Exception& e) {
QMessageBox::critical(this, tr("Error"), e.cause());
return false;

View file

@ -13,6 +13,11 @@
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Note: If your FTB instances are not in the default location, select it using the button next to search.</string>
</property>

View file

@ -36,6 +36,9 @@
ThemeManager::ThemeManager()
{
QIcon::setFallbackThemeName(QIcon::themeName());
QIcon::setThemeSearchPaths(QIcon::themeSearchPaths() << m_iconThemeFolder.path());
themeDebugLog() << "Determining System Widget Theme...";
const auto& style = QApplication::style();
m_defaultStyle = style->objectName();
@ -93,10 +96,6 @@ void ThemeManager::initializeIcons()
// set icon theme search path!
themeDebugLog() << "<> Initializing Icon Themes";
auto searchPaths = QIcon::themeSearchPaths();
searchPaths.append(m_iconThemeFolder.path());
QIcon::setThemeSearchPaths(searchPaths);
for (const QString& id : builtinIcons) {
IconTheme theme(id, QString(":/icons/%1").arg(id));
if (!theme.load()) {
@ -348,4 +347,4 @@ void ThemeManager::refresh()
initializeThemes();
initializeCatPacks();
}
}

View file

@ -1,40 +0,0 @@
#include "DropLabel.h"
#include <QDropEvent>
#include <QMimeData>
DropLabel::DropLabel(QWidget* parent) : QLabel(parent)
{
setAcceptDrops(true);
}
void DropLabel::dragEnterEvent(QDragEnterEvent* event)
{
event->acceptProposedAction();
}
void DropLabel::dragMoveEvent(QDragMoveEvent* event)
{
event->acceptProposedAction();
}
void DropLabel::dragLeaveEvent(QDragLeaveEvent* event)
{
event->accept();
}
void DropLabel::dropEvent(QDropEvent* event)
{
const QMimeData* mimeData = event->mimeData();
if (!mimeData) {
return;
}
if (mimeData->hasUrls()) {
auto urls = mimeData->urls();
emit droppedURLs(urls);
}
event->acceptProposedAction();
}

View file

@ -1,19 +0,0 @@
#pragma once
#include <QLabel>
class DropLabel : public QLabel {
Q_OBJECT
public:
explicit DropLabel(QWidget* parent = nullptr);
signals:
void droppedURLs(QList<QUrl> urls);
protected:
void dropEvent(QDropEvent* event) override;
void dragEnterEvent(QDragEnterEvent* event) override;
void dragMoveEvent(QDragMoveEvent* event) override;
void dragLeaveEvent(QDragLeaveEvent* event) override;
};

View file

@ -279,16 +279,17 @@ void ModFilterWidget::onSideFilterChanged()
{
QString side;
if (ui->clientSide->isChecked() != ui->serverSide->isChecked()) {
if (ui->clientSide->isChecked())
side = "client";
else
side = "server";
if (ui->clientSide->isChecked() && !ui->serverSide->isChecked()) {
side = "client";
} else if (!ui->clientSide->isChecked() && ui->serverSide->isChecked()) {
side = "server";
} else if (ui->clientSide->isChecked() && ui->serverSide->isChecked()) {
side = "both";
} else {
// both are checked or none are checked; in either case no filtering will happen
side = "";
}
m_filter_changed = side != m_filter->side;
m_filter->side = side;
if (m_filter_changed)

View file

@ -47,6 +47,9 @@
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
@ -68,6 +71,9 @@
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
</layout>