forked from Github/PrismLauncher
Merge remote-tracking branch 'upstream/develop' into unify-mc-settings
Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
commit
cc504f4a6c
58 changed files with 738 additions and 640 deletions
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 ?
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(", ");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "Application.h"
|
||||
#include "modplatform/CheckUpdateTask.h"
|
||||
#include "net/NetJob.h"
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 :)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ struct BlockedMod {
|
|||
bool matched;
|
||||
QString localPath;
|
||||
QString targetFolder;
|
||||
bool move = false;
|
||||
};
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)") };
|
||||
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@
|
|||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="text">
|
||||
<string><html><head/><body><p>Note: you only need to set this to access private data. Read the <a href="https://docs.modrinth.com/#section/Authentication">documentation</a> for more information.</p></body></html></string>
|
||||
<string><html><head/><body><p>Note: you only need to set this to access private data. Read the <a href="https://docs.modrinth.com/api/#authentication">documentation</a> for more information.</p></body></html></string>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue