Merge branch 'develop' of https://github.com/PrismLauncher/PrismLauncher into disablemods

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
This commit is contained in:
Trial97 2024-07-24 11:47:45 +03:00
commit d5c5f5d4fc
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
108 changed files with 1549 additions and 574 deletions

View File

@ -79,6 +79,14 @@
<string>curseforge</string> <string>curseforge</string>
</array> </array>
</dict> </dict>
<dict>
<key>CFBundleURLName</key>
<string>Prismlauncher</string>
<key>CFBundleURLSchemes</key>
<array>
<string>prismlauncher</string>
</array>
</dict>
</array> </array>
</dict> </dict>
</plist> </plist>

18
flake.lock generated
View File

@ -23,11 +23,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1717285511, "lastModified": 1719994518,
"narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=", "narHash": "sha256-pQMhCCHyQGRzdfAkdJ4cIWiw+JNuWsTX7f0ZYSyz0VY=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8", "rev": "9227223f6d922fee3c7b190b2cc238a99527bbb7",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -75,11 +75,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1718276985, "lastModified": 1720768451,
"narHash": "sha256-u1fA0DYQYdeG+5kDm1bOoGcHtX0rtC7qs2YA2N1X++I=", "narHash": "sha256-EYekUHJE2gxeo2pM/zM9Wlqw1Uw2XTJXOSAO79ksc4Y=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "3f84a279f1a6290ce154c5531378acc827836fbb", "rev": "7e7c39ea35c5cdd002cd4588b03a3fb9ece6fad9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -103,11 +103,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1717664902, "lastModified": 1720524665,
"narHash": "sha256-7XfBuLULizXjXfBYy/VV+SpYMHreNRHk9nKMsm1bgb4=", "narHash": "sha256-ni/87oHPZm6Gv0ECYxr1f6uxB0UKBWJ6HvS7lwLU6oY=",
"owner": "cachix", "owner": "cachix",
"repo": "pre-commit-hooks.nix", "repo": "pre-commit-hooks.nix",
"rev": "cc4d466cb1254af050ff7bdf47f6d404a7c646d1", "rev": "8d6a17d0cdf411c55f12602624df6368ad86fac1",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -292,12 +292,17 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
QString adjustedBy; QString adjustedBy;
QString dataPath; QString dataPath;
// change folder // change folder
QString dataDirEnv;
QString dirParam = parser.value("dir"); QString dirParam = parser.value("dir");
if (!dirParam.isEmpty()) { if (!dirParam.isEmpty()) {
// the dir param. it makes multimc data path point to whatever the user specified // the dir param. it makes multimc data path point to whatever the user specified
// on command line // on command line
adjustedBy = "Command line"; adjustedBy = "Command line";
dataPath = dirParam; dataPath = dirParam;
} else if (dataDirEnv = QProcessEnvironment::systemEnvironment().value(QString("%1_DATA_DIR").arg(BuildConfig.LAUNCHER_NAME.toUpper()));
!dataDirEnv.isEmpty()) {
adjustedBy = "System environment";
dataPath = dataDirEnv;
} else { } else {
QDir foo; QDir foo;
if (DesktopServices::isSnap()) { if (DesktopServices::isSnap()) {
@ -443,7 +448,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// search the dataPath() // search the dataPath()
// seach app data standard path // seach app data standard path
if (!foundLoggingRules && !isPortable() && dirParam.isEmpty()) { if (!foundLoggingRules && !isPortable() && dirParam.isEmpty() && dataDirEnv.isEmpty()) {
logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile)); logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile));
if (!logRulesPath.isEmpty()) { if (!logRulesPath.isEmpty()) {
qDebug() << "Found" << logRulesPath << "..."; qDebug() << "Found" << logRulesPath << "...";
@ -554,6 +559,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("NumberOfConcurrentTasks", 10); m_settings->registerSetting("NumberOfConcurrentTasks", 10);
m_settings->registerSetting("NumberOfConcurrentDownloads", 6); m_settings->registerSetting("NumberOfConcurrentDownloads", 6);
m_settings->registerSetting("NumberOfManualRetries", 1);
m_settings->registerSetting("RequestTimeout", 60);
QString defaultMonospace; QString defaultMonospace;
int defaultSize = 11; int defaultSize = 11;
@ -656,6 +663,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// Minecraft mods // Minecraft mods
m_settings->registerSetting("ModMetadataDisabled", false); m_settings->registerSetting("ModMetadataDisabled", false);
m_settings->registerSetting("ModDependenciesDisabled", false); m_settings->registerSetting("ModDependenciesDisabled", false);
m_settings->registerSetting("SkipModpackUpdatePrompt", false);
// Minecraft offline player name // Minecraft offline player name
m_settings->registerSetting("LastOfflinePlayerName", ""); m_settings->registerSetting("LastOfflinePlayerName", "");
@ -810,7 +818,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_icons.reset(new IconList(instFolders, setting->get().toString())); m_icons.reset(new IconList(instFolders, setting->get().toString()));
connect(setting.get(), &Setting::SettingChanged, connect(setting.get(), &Setting::SettingChanged,
[&](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); }); [&](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); });
qDebug() << "<> Instance icons intialized."; qDebug() << "<> Instance icons initialized.";
} }
// Themes // Themes

View File

@ -48,7 +48,6 @@
#include <BaseInstance.h> #include <BaseInstance.h>
#include "minecraft/launch/MinecraftServerTarget.h" #include "minecraft/launch/MinecraftServerTarget.h"
#include "ui/themes/CatPack.h"
class LaunchController; class LaunchController;
class LocalPeer; class LocalPeer;
@ -193,6 +192,8 @@ class Application : public QApplication {
void globalSettingsClosed(); void globalSettingsClosed();
int currentCatChanged(int index); int currentCatChanged(int index);
void oauthReplyRecieved(QVariantMap);
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
void clickedOnDock(); void clickedOnDock();
#endif #endif

View File

@ -1691,4 +1691,30 @@ QString getPathNameInLocal8bit(const QString& file)
} }
#endif #endif
QString getUniqueResourceName(const QString& filePath)
{
auto newFileName = filePath;
if (!newFileName.endsWith(".disabled")) {
return newFileName; // prioritize enabled mods
}
newFileName.chop(9);
if (!QFile::exists(newFileName)) {
return filePath;
}
QFileInfo fileInfo(filePath);
auto baseName = fileInfo.completeBaseName();
auto path = fileInfo.absolutePath();
int counter = 1;
do {
if (counter == 1) {
newFileName = FS::PathCombine(path, baseName + ".duplicate");
} else {
newFileName = FS::PathCombine(path, baseName + ".duplicate" + QString::number(counter));
}
counter++;
} while (QFile::exists(newFileName));
return newFileName;
}
} // namespace FS } // namespace FS

View File

@ -560,4 +560,6 @@ uintmax_t hardLinkCount(const QString& path);
QString getPathNameInLocal8bit(const QString& file); QString getPathNameInLocal8bit(const QString& file);
#endif #endif
QString getUniqueResourceName(const QString& filePath);
} // namespace FS } // namespace FS

View File

@ -372,13 +372,13 @@ void InstanceList::undoTrashInstance()
auto top = m_trashHistory.pop(); auto top = m_trashHistory.pop();
while (QDir(top.polyPath).exists()) { while (QDir(top.path).exists()) {
top.id += "1"; top.id += "1";
top.polyPath += "1"; top.path += "1";
} }
qDebug() << "Moving" << top.trashPath << "back to" << top.polyPath; qDebug() << "Moving" << top.trashPath << "back to" << top.path;
QFile(top.trashPath).rename(top.polyPath); QFile(top.trashPath).rename(top.path);
m_instanceGroupIndex[top.id] = top.groupName; m_instanceGroupIndex[top.id] = top.groupName;
increaseGroupCount(top.groupName); increaseGroupCount(top.groupName);
@ -635,8 +635,8 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id)
QString inst_type = instanceSettings->get("InstanceType").toString(); QString inst_type = instanceSettings->get("InstanceType").toString();
// NOTE: Some PolyMC versions didn't save the InstanceType properly. We will just bank on the probability that this is probably a OneSix // NOTE: Some launcher versions didn't save the InstanceType properly. We will just bank on the probability that this is probably a
// instance // OneSix instance
if (inst_type == "OneSix" || inst_type.isEmpty()) { if (inst_type == "OneSix" || inst_type.isEmpty()) {
inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot)); inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot));
} else { } else {
@ -710,6 +710,12 @@ void InstanceList::saveGroupList()
groupsArr.insert(name, groupObj); groupsArr.insert(name, groupObj);
} }
toplevel.insert("groups", groupsArr); toplevel.insert("groups", groupsArr);
// empty string represents ungrouped "group"
if (m_collapsedGroups.contains("")) {
QJsonObject ungrouped;
ungrouped.insert("hidden", QJsonValue(true));
toplevel.insert("ungrouped", ungrouped);
}
QJsonDocument doc(toplevel); QJsonDocument doc(toplevel);
try { try {
FS::write(groupFileName, doc.toJson()); FS::write(groupFileName, doc.toJson());
@ -805,6 +811,16 @@ void InstanceList::loadGroupList()
increaseGroupCount(groupName); increaseGroupCount(groupName);
} }
} }
bool ungroupedHidden = false;
if (rootObj.value("ungrouped").isObject()) {
QJsonObject ungrouped = rootObj.value("ungrouped").toObject();
ungroupedHidden = ungrouped.value("hidden").toBool(false);
}
if (ungroupedHidden) {
// empty string represents ungrouped "group"
m_collapsedGroups.insert("");
}
m_groupsLoaded = true; m_groupsLoaded = true;
qDebug() << "Group list loaded."; qDebug() << "Group list loaded.";
} }

View File

@ -58,7 +58,7 @@ enum class GroupsState { NotLoaded, Steady, Dirty };
struct TrashHistoryItem { struct TrashHistoryItem {
QString id; QString id;
QString polyPath; QString path;
QString trashPath; QString trashPath;
QString groupName; QString groupName;
}; };

View File

@ -1,5 +1,7 @@
#include "InstanceTask.h" #include "InstanceTask.h"
#include "Application.h"
#include "settings/SettingsObject.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include <QPushButton> #include <QPushButton>
@ -22,6 +24,9 @@ InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& ol
ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name) ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name)
{ {
if (APPLICATION->settings()->get("SkipModpackUpdatePrompt").toBool())
return ShouldUpdate::SkipUpdating;
auto info = CustomMessageBox::selectable( auto info = CustomMessageBox::selectable(
parent, QObject::tr("Similar modpack was found!"), parent, QObject::tr("Similar modpack was found!"),
QObject::tr( QObject::tr(

View File

@ -194,7 +194,8 @@ void LaunchController::login()
bool tryagain = true; bool tryagain = true;
unsigned int tries = 0; unsigned int tries = 0;
if (m_accountToUse->accountType() != AccountType::Offline && m_accountToUse->accountState() == AccountState::Offline) { if ((m_accountToUse->accountType() != AccountType::Offline && m_accountToUse->accountState() == AccountState::Offline) ||
m_accountToUse->shouldRefresh()) {
// Force account refresh on the account used to launch the instance updating the AccountState // Force account refresh on the account used to launch the instance updating the AccountState
// only on first try and if it is not meant to be offline // only on first try and if it is not meant to be offline
auto accounts = APPLICATION->accounts(); auto accounts = APPLICATION->accounts();

View File

@ -394,7 +394,7 @@ QList<QString> JavaUtils::FindJavaPaths()
return javas; return javas;
} }
#elif defined(Q_OS_LINUX) #elif defined(Q_OS_LINUX) || defined(Q_OS_OPENBSD) || defined(Q_OS_FREEBSD)
QList<QString> JavaUtils::FindJavaPaths() QList<QString> JavaUtils::FindJavaPaths()
{ {
QList<QString> javas; QList<QString> javas;
@ -419,6 +419,7 @@ QList<QString> JavaUtils::FindJavaPaths()
scanJavaDir(snap + dirPath); scanJavaDir(snap + dirPath);
} }
}; };
#if defined(Q_OS_LINUX)
// oracle RPMs // oracle RPMs
scanJavaDirs("/usr/java"); scanJavaDirs("/usr/java");
// general locations used by distro packaging // general locations used by distro packaging
@ -437,7 +438,10 @@ QList<QString> JavaUtils::FindJavaPaths()
scanJavaDirs("/opt/ibm"); // IBM Semeru Certified Edition scanJavaDirs("/opt/ibm"); // IBM Semeru Certified Edition
// flatpak // flatpak
scanJavaDirs("/app/jdk"); scanJavaDirs("/app/jdk");
#elif defined(Q_OS_OPENBSD) || defined(Q_OS_FREEBSD)
// ports install to /usr/local on OpenBSD & FreeBSD
scanJavaDirs("/usr/local");
#endif
auto home = qEnvironmentVariable("HOME"); auto home = qEnvironmentVariable("HOME");
// javas downloaded by IntelliJ // javas downloaded by IntelliJ

View File

@ -30,13 +30,21 @@ class ParsingValidator : public Net::Validator {
virtual ~ParsingValidator() {}; virtual ~ParsingValidator() {};
public: /* methods */ public: /* methods */
bool init(QNetworkRequest&) override { return true; } bool init(QNetworkRequest&) override
{
m_data.clear();
return true;
}
bool write(QByteArray& data) override bool write(QByteArray& data) override
{ {
this->m_data.append(data); this->m_data.append(data);
return true; return true;
} }
bool abort() override { return true; } bool abort() override
{
m_data.clear();
return true;
}
bool validate(QNetworkReply&) override bool validate(QNetworkReply&) override
{ {
auto fname = m_entity->localFilename(); auto fname = m_entity->localFilename();

View File

@ -42,6 +42,20 @@
#include <net/ApiDownload.h> #include <net/ApiDownload.h>
#include <net/ChecksumValidator.h> #include <net/ChecksumValidator.h>
/**
* @brief Collect applicable files for the library.
*
* Depending on whether the library is native or not, it adds paths to the
* appropriate lists for jar files, native libraries for 32-bit, and native
* libraries for 64-bit.
*
* @param runtimeContext The current runtime context.
* @param jar List to store paths for jar files.
* @param native List to store paths for native libraries.
* @param native32 List to store paths for 32-bit native libraries.
* @param native64 List to store paths for 64-bit native libraries.
* @param overridePath Optional path to override the default storage path.
*/
void Library::getApplicableFiles(const RuntimeContext& runtimeContext, void Library::getApplicableFiles(const RuntimeContext& runtimeContext,
QStringList& jar, QStringList& jar,
QStringList& native, QStringList& native,
@ -50,6 +64,7 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext,
const QString& overridePath) const const QString& overridePath) const
{ {
bool local = isLocal(); bool local = isLocal();
// Lambda function to get the absolute file path
auto actualPath = [&](QString relPath) { auto actualPath = [&](QString relPath) {
relPath = FS::RemoveInvalidPathChars(relPath); relPath = FS::RemoveInvalidPathChars(relPath);
QFileInfo out(FS::PathCombine(storagePrefix(), relPath)); QFileInfo out(FS::PathCombine(storagePrefix(), relPath));
@ -59,6 +74,7 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext,
} }
return out.absoluteFilePath(); return out.absoluteFilePath();
}; };
QString raw_storage = storageSuffix(runtimeContext); QString raw_storage = storageSuffix(runtimeContext);
if (isNative()) { if (isNative()) {
if (raw_storage.contains("${arch}")) { if (raw_storage.contains("${arch}")) {
@ -76,6 +92,19 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext,
} }
} }
/**
* @brief Get download requests for the library files.
*
* Depending on whether the library is native or not, and the current runtime context,
* this function prepares download requests for the necessary files. It handles both local
* and remote files, checks for stale cache entries, and adds checksummed downloads.
*
* @param runtimeContext The current runtime context.
* @param cache Pointer to the HTTP meta cache.
* @param failedLocalFiles List to store paths for failed local files.
* @param overridePath Optional path to override the default storage path.
* @return QList<Net::NetRequest::Ptr> List of download requests.
*/
QList<Net::NetRequest::Ptr> Library::getDownloads(const RuntimeContext& runtimeContext, QList<Net::NetRequest::Ptr> Library::getDownloads(const RuntimeContext& runtimeContext,
class HttpMetaCache* cache, class HttpMetaCache* cache,
QStringList& failedLocalFiles, QStringList& failedLocalFiles,
@ -85,6 +114,7 @@ QList<Net::NetRequest::Ptr> Library::getDownloads(const RuntimeContext& runtimeC
bool stale = isAlwaysStale(); bool stale = isAlwaysStale();
bool local = isLocal(); bool local = isLocal();
// Lambda function to check if a local file exists
auto check_local_file = [&](QString storage) { auto check_local_file = [&](QString storage) {
QFileInfo fileinfo(storage); QFileInfo fileinfo(storage);
QString fileName = fileinfo.fileName(); QString fileName = fileinfo.fileName();
@ -97,6 +127,7 @@ QList<Net::NetRequest::Ptr> Library::getDownloads(const RuntimeContext& runtimeC
return true; return true;
}; };
// Lambda function to add a download request
auto add_download = [&](QString storage, QString url, QString sha1) { auto add_download = [&](QString storage, QString url, QString sha1) {
if (local) { if (local) {
return check_local_file(storage); return check_local_file(storage);
@ -197,6 +228,15 @@ QList<Net::NetRequest::Ptr> Library::getDownloads(const RuntimeContext& runtimeC
return out; return out;
} }
/**
* @brief Check if the library is active in the given runtime context.
*
* This function evaluates rules to determine if the library should be active,
* considering both general rules and native compatibility.
*
* @param runtimeContext The current runtime context.
* @return bool True if the library is active, false otherwise.
*/
bool Library::isActive(const RuntimeContext& runtimeContext) const bool Library::isActive(const RuntimeContext& runtimeContext) const
{ {
bool result = true; bool result = true;
@ -217,16 +257,35 @@ bool Library::isActive(const RuntimeContext& runtimeContext) const
return result; return result;
} }
/**
* @brief Check if the library is considered local.
*
* @return bool True if the library is local, false otherwise.
*/
bool Library::isLocal() const bool Library::isLocal() const
{ {
return m_hint == "local"; return m_hint == "local";
} }
/**
* @brief Check if the library is always considered stale.
*
* @return bool True if the library is always stale, false otherwise.
*/
bool Library::isAlwaysStale() const bool Library::isAlwaysStale() const
{ {
return m_hint == "always-stale"; return m_hint == "always-stale";
} }
/**
* @brief Get the compatible native classifier for the current runtime context.
*
* This function attempts to match the current runtime context with the appropriate
* native classifier.
*
* @param runtimeContext The current runtime context.
* @return QString The compatible native classifier, or an empty string if none is found.
*/
QString Library::getCompatibleNative(const RuntimeContext& runtimeContext) const QString Library::getCompatibleNative(const RuntimeContext& runtimeContext) const
{ {
// try to match precise classifier "[os]-[arch]" // try to match precise classifier "[os]-[arch]"
@ -241,16 +300,31 @@ QString Library::getCompatibleNative(const RuntimeContext& runtimeContext) const
return entry.value(); return entry.value();
} }
/**
* @brief Set the storage prefix for the library.
*
* @param prefix The storage prefix to set.
*/
void Library::setStoragePrefix(QString prefix) void Library::setStoragePrefix(QString prefix)
{ {
m_storagePrefix = prefix; m_storagePrefix = prefix;
} }
/**
* @brief Get the default storage prefix for libraries.
*
* @return QString The default storage prefix.
*/
QString Library::defaultStoragePrefix() QString Library::defaultStoragePrefix()
{ {
return "libraries/"; return "libraries/";
} }
/**
* @brief Get the current storage prefix for the library.
*
* @return QString The current storage prefix.
*/
QString Library::storagePrefix() const QString Library::storagePrefix() const
{ {
if (m_storagePrefix.isEmpty()) { if (m_storagePrefix.isEmpty()) {
@ -259,6 +333,15 @@ QString Library::storagePrefix() const
return m_storagePrefix; return m_storagePrefix;
} }
/**
* @brief Get the filename for the library in the current runtime context.
*
* This function determines the appropriate filename for the library, taking into
* account native classifiers if applicable.
*
* @param runtimeContext The current runtime context.
* @return QString The filename of the library.
*/
QString Library::filename(const RuntimeContext& runtimeContext) const QString Library::filename(const RuntimeContext& runtimeContext) const
{ {
if (!m_filename.isEmpty()) { if (!m_filename.isEmpty()) {
@ -280,6 +363,15 @@ QString Library::filename(const RuntimeContext& runtimeContext) const
return nativeSpec.getFileName(); return nativeSpec.getFileName();
} }
/**
* @brief Get the display name for the library in the current runtime context.
*
* This function returns the display name for the library, defaulting to the filename
* if no display name is set.
*
* @param runtimeContext The current runtime context.
* @return QString The display name of the library.
*/
QString Library::displayName(const RuntimeContext& runtimeContext) const QString Library::displayName(const RuntimeContext& runtimeContext) const
{ {
if (!m_displayname.isEmpty()) if (!m_displayname.isEmpty())
@ -287,6 +379,15 @@ QString Library::displayName(const RuntimeContext& runtimeContext) const
return filename(runtimeContext); return filename(runtimeContext);
} }
/**
* @brief Get the storage suffix for the library in the current runtime context.
*
* This function determines the appropriate storage suffix for the library, taking into
* account native classifiers if applicable.
*
* @param runtimeContext The current runtime context.
* @return QString The storage suffix of the library.
*/
QString Library::storageSuffix(const RuntimeContext& runtimeContext) const QString Library::storageSuffix(const RuntimeContext& runtimeContext) const
{ {
// non-native? use only the gradle specifier // non-native? use only the gradle specifier

View File

@ -58,7 +58,6 @@
#include "ComponentUpdateTask.h" #include "ComponentUpdateTask.h"
#include "PackProfile.h" #include "PackProfile.h"
#include "PackProfile_p.h" #include "PackProfile_p.h"
#include "minecraft/mod/Mod.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
static const QMap<QString, ModPlatform::ModLoaderType> modloaderMapping{ { "net.neoforged", ModPlatform::NeoForge }, static const QMap<QString, ModPlatform::ModLoaderType> modloaderMapping{ { "net.neoforged", ModPlatform::NeoForge },
@ -181,7 +180,7 @@ static bool loadPackProfile(PackProfile* parent,
} }
if (!componentsFile.open(QFile::ReadOnly)) { if (!componentsFile.open(QFile::ReadOnly)) {
qCritical() << "Couldn't open" << componentsFile.fileName() << " for reading:" << componentsFile.errorString(); qCritical() << "Couldn't open" << componentsFile.fileName() << " for reading:" << componentsFile.errorString();
qWarning() << "Ignoring overriden order"; qWarning() << "Ignoring overridden order";
return false; return false;
} }
@ -190,7 +189,7 @@ static bool loadPackProfile(PackProfile* parent,
QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error); QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error);
if (error.error != QJsonParseError::NoError) { if (error.error != QJsonParseError::NoError) {
qCritical() << "Couldn't parse" << componentsFile.fileName() << ":" << error.errorString(); qCritical() << "Couldn't parse" << componentsFile.fileName() << ":" << error.errorString();
qWarning() << "Ignoring overriden order"; qWarning() << "Ignoring overridden order";
return false; return false;
} }
@ -1022,3 +1021,23 @@ std::optional<ModPlatform::ModLoaderTypes> PackProfile::getSupportedModLoaders()
loaders |= ModPlatform::Forge; loaders |= ModPlatform::Forge;
return loaders; return loaders;
} }
QList<ModPlatform::ModLoaderType> PackProfile::getModLoadersList()
{
QList<ModPlatform::ModLoaderType> result;
for (auto c : d->components) {
if (c->isEnabled() && modloaderMapping.contains(c->getID())) {
result.append(modloaderMapping[c->getID()]);
}
}
// TODO: remove this or add version condition once Quilt drops official Fabric support
if (result.contains(ModPlatform::Quilt) && !result.contains(ModPlatform::Fabric)) {
result.append(ModPlatform::Fabric);
}
if (getComponentVersion("net.minecraft") == "1.20.1" && result.contains(ModPlatform::NeoForge) &&
!result.contains(ModPlatform::Forge)) {
result.append(ModPlatform::Forge);
}
return result;
}

View File

@ -146,6 +146,7 @@ class PackProfile : public QAbstractListModel {
std::optional<ModPlatform::ModLoaderTypes> getModLoaders(); std::optional<ModPlatform::ModLoaderTypes> getModLoaders();
// this returns aditional loaders(Quilt supports fabric and NeoForge supports Forge) // this returns aditional loaders(Quilt supports fabric and NeoForge supports Forge)
std::optional<ModPlatform::ModLoaderTypes> getSupportedModLoaders(); std::optional<ModPlatform::ModLoaderTypes> getSupportedModLoaders();
QList<ModPlatform::ModLoaderType> getModLoadersList();
private: private:
void scheduleSave(); void scheduleSave();

View File

@ -57,7 +57,7 @@ bool readOverrideOrders(QString path, PatchOrder& order)
} }
if (!orderFile.open(QFile::ReadOnly)) { if (!orderFile.open(QFile::ReadOnly)) {
qCritical() << "Couldn't open" << orderFile.fileName() << " for reading:" << orderFile.errorString(); qCritical() << "Couldn't open" << orderFile.fileName() << " for reading:" << orderFile.errorString();
qWarning() << "Ignoring overriden order"; qWarning() << "Ignoring overridden order";
return false; return false;
} }
@ -66,7 +66,7 @@ bool readOverrideOrders(QString path, PatchOrder& order)
QJsonDocument doc = QJsonDocument::fromJson(orderFile.readAll(), &error); QJsonDocument doc = QJsonDocument::fromJson(orderFile.readAll(), &error);
if (error.error != QJsonParseError::NoError) { if (error.error != QJsonParseError::NoError) {
qCritical() << "Couldn't parse" << orderFile.fileName() << ":" << error.errorString(); qCritical() << "Couldn't parse" << orderFile.fileName() << ":" << error.errorString();
qWarning() << "Ignoring overriden order"; qWarning() << "Ignoring overridden order";
return false; return false;
} }
@ -84,7 +84,7 @@ bool readOverrideOrders(QString path, PatchOrder& order)
} }
} catch ([[maybe_unused]] const JSONValidationError& err) { } catch ([[maybe_unused]] const JSONValidationError& err) {
qCritical() << "Couldn't parse" << orderFile.fileName() << ": bad file format"; qCritical() << "Couldn't parse" << orderFile.fileName() << ": bad file format";
qWarning() << "Ignoring overriden order"; qWarning() << "Ignoring overridden order";
order.clear(); order.clear();
return false; return false;
} }

View File

@ -59,6 +59,9 @@ void AuthFlow::executeTask()
void AuthFlow::nextStep() void AuthFlow::nextStep()
{ {
if (!Task::isRunning()) {
return;
}
if (m_steps.size() == 0) { if (m_steps.size() == 0) {
// we got to the end without an incident... assume this is all. // we got to the end without an incident... assume this is all.
m_currentStep.reset(); m_currentStep.reset();
@ -144,3 +147,10 @@ bool AuthFlow::changeState(AccountTaskState newState, QString reason)
} }
} }
} }
bool AuthFlow::abort()
{
emitAborted();
if (m_currentStep)
m_currentStep->abort();
return true;
}

View File

@ -24,6 +24,9 @@ class AuthFlow : public Task {
AccountTaskState taskState() { return m_taskState; } AccountTaskState taskState() { return m_taskState; }
public slots:
bool abort() override;
signals: signals:
void authorizeWithBrowser(const QUrl& url); void authorizeWithBrowser(const QUrl& url);
void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn); void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn);

View File

@ -34,6 +34,7 @@ class AuthStep : public QObject {
public slots: public slots:
virtual void perform() = 0; virtual void perform() = 0;
virtual void abort() {}
signals: signals:
void finished(AccountTaskState resultingState, QString message); void finished(AccountTaskState resultingState, QString message);

View File

@ -10,6 +10,7 @@
#include "Logging.h" #include "Logging.h"
#include "minecraft/auth/Parsers.h" #include "minecraft/auth/Parsers.h"
#include "net/Download.h" #include "net/Download.h"
#include "net/NetJob.h"
#include "net/StaticHeaderProxy.h" #include "net/StaticHeaderProxy.h"
#include "tasks/Task.h" #include "tasks/Task.h"
@ -31,12 +32,15 @@ void EntitlementsStep::perform()
{ "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } }; { "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } };
m_response.reset(new QByteArray()); m_response.reset(new QByteArray());
m_task = Net::Download::makeByteArray(url, m_response); m_request = Net::Download::makeByteArray(url, m_response);
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers)); m_request->addHeaderProxy(new Net::StaticHeaderProxy(headers));
m_task.reset(new NetJob("EntitlementsStep", APPLICATION->network()));
m_task->setAskRetry(false);
m_task->addNetAction(m_request);
connect(m_task.get(), &Task::finished, this, &EntitlementsStep::onRequestDone); connect(m_task.get(), &Task::finished, this, &EntitlementsStep::onRequestDone);
m_task->setNetwork(APPLICATION->network());
m_task->start(); m_task->start();
qDebug() << "Getting entitlements..."; qDebug() << "Getting entitlements...";
} }

View File

@ -4,6 +4,7 @@
#include "minecraft/auth/AuthStep.h" #include "minecraft/auth/AuthStep.h"
#include "net/Download.h" #include "net/Download.h"
#include "net/NetJob.h"
class EntitlementsStep : public AuthStep { class EntitlementsStep : public AuthStep {
Q_OBJECT Q_OBJECT
@ -22,5 +23,6 @@ class EntitlementsStep : public AuthStep {
private: private:
QString m_entitlements_request_id; QString m_entitlements_request_id;
std::shared_ptr<QByteArray> m_response; std::shared_ptr<QByteArray> m_response;
Net::Download::Ptr m_task; Net::Download::Ptr m_request;
NetJob::Ptr m_task;
}; };

View File

@ -17,17 +17,20 @@ void GetSkinStep::perform()
QUrl url(m_data->minecraftProfile.skin.url); QUrl url(m_data->minecraftProfile.skin.url);
m_response.reset(new QByteArray()); m_response.reset(new QByteArray());
m_task = Net::Download::makeByteArray(url, m_response); m_request = Net::Download::makeByteArray(url, m_response);
m_task.reset(new NetJob("GetSkinStep", APPLICATION->network()));
m_task->setAskRetry(false);
m_task->addNetAction(m_request);
connect(m_task.get(), &Task::finished, this, &GetSkinStep::onRequestDone); connect(m_task.get(), &Task::finished, this, &GetSkinStep::onRequestDone);
m_task->setNetwork(APPLICATION->network());
m_task->start(); m_task->start();
} }
void GetSkinStep::onRequestDone() void GetSkinStep::onRequestDone()
{ {
if (m_task->error() == QNetworkReply::NoError) if (m_request->error() == QNetworkReply::NoError)
m_data->minecraftProfile.skin.data = *m_response; m_data->minecraftProfile.skin.data = *m_response;
emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Got skin")); emit finished(AccountTaskState::STATE_WORKING, tr("Got skin"));
} }

View File

@ -4,6 +4,7 @@
#include "minecraft/auth/AuthStep.h" #include "minecraft/auth/AuthStep.h"
#include "net/Download.h" #include "net/Download.h"
#include "net/NetJob.h"
class GetSkinStep : public AuthStep { class GetSkinStep : public AuthStep {
Q_OBJECT Q_OBJECT
@ -21,5 +22,6 @@ class GetSkinStep : public AuthStep {
private: private:
std::shared_ptr<QByteArray> m_response; std::shared_ptr<QByteArray> m_response;
Net::Download::Ptr m_task; Net::Download::Ptr m_request;
NetJob::Ptr m_task;
}; };

View File

@ -37,12 +37,15 @@ void LauncherLoginStep::perform()
}; };
m_response.reset(new QByteArray()); m_response.reset(new QByteArray());
m_task = Net::Upload::makeByteArray(url, m_response, requestBody.toUtf8()); m_request = Net::Upload::makeByteArray(url, m_response, requestBody.toUtf8());
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers)); m_request->addHeaderProxy(new Net::StaticHeaderProxy(headers));
m_task.reset(new NetJob("LauncherLoginStep", APPLICATION->network()));
m_task->setAskRetry(false);
m_task->addNetAction(m_request);
connect(m_task.get(), &Task::finished, this, &LauncherLoginStep::onRequestDone); connect(m_task.get(), &Task::finished, this, &LauncherLoginStep::onRequestDone);
m_task->setNetwork(APPLICATION->network());
m_task->start(); m_task->start();
qDebug() << "Getting Minecraft access token..."; qDebug() << "Getting Minecraft access token...";
} }
@ -50,12 +53,13 @@ void LauncherLoginStep::perform()
void LauncherLoginStep::onRequestDone() void LauncherLoginStep::onRequestDone()
{ {
qCDebug(authCredentials()) << *m_response; qCDebug(authCredentials()) << *m_response;
if (m_task->error() != QNetworkReply::NoError) { if (m_request->error() != QNetworkReply::NoError) {
qWarning() << "Reply error:" << m_task->error(); qWarning() << "Reply error:" << m_request->error();
if (Net::isApplicationError(m_task->error())) { if (Net::isApplicationError(m_request->error())) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to get Minecraft access token: %1").arg(m_task->errorString())); emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to get Minecraft access token: %1").arg(m_request->errorString()));
} else { } else {
emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to get Minecraft access token: %1").arg(m_task->errorString())); emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to get Minecraft access token: %1").arg(m_request->errorString()));
} }
return; return;
} }

View File

@ -3,6 +3,7 @@
#include <memory> #include <memory>
#include "minecraft/auth/AuthStep.h" #include "minecraft/auth/AuthStep.h"
#include "net/NetJob.h"
#include "net/Upload.h" #include "net/Upload.h"
class LauncherLoginStep : public AuthStep { class LauncherLoginStep : public AuthStep {
@ -21,5 +22,6 @@ class LauncherLoginStep : public AuthStep {
private: private:
std::shared_ptr<QByteArray> m_response; std::shared_ptr<QByteArray> m_response;
Net::Upload::Ptr m_task; Net::Upload::Ptr m_request;
NetJob::Ptr m_task;
}; };

View File

@ -46,6 +46,8 @@
MSADeviceCodeStep::MSADeviceCodeStep(AccountData* data) : AuthStep(data) MSADeviceCodeStep::MSADeviceCodeStep(AccountData* data) : AuthStep(data)
{ {
m_clientId = APPLICATION->getMSAClientID(); m_clientId = APPLICATION->getMSAClientID();
connect(&m_expiration_timer, &QTimer::timeout, this, &MSADeviceCodeStep::abort);
connect(&m_pool_timer, &QTimer::timeout, this, &MSADeviceCodeStep::authenticateUser);
} }
QString MSADeviceCodeStep::describe() QString MSADeviceCodeStep::describe()
@ -65,12 +67,15 @@ void MSADeviceCodeStep::perform()
{ "Accept", "application/json" }, { "Accept", "application/json" },
}; };
m_response.reset(new QByteArray()); m_response.reset(new QByteArray());
m_task = Net::Upload::makeByteArray(url, m_response, payload); m_request = Net::Upload::makeByteArray(url, m_response, payload);
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers)); m_request->addHeaderProxy(new Net::StaticHeaderProxy(headers));
m_task.reset(new NetJob("MSADeviceCodeStep", APPLICATION->network()));
m_task->setAskRetry(false);
m_task->addNetAction(m_request);
connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::deviceAutorizationFinished); connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::deviceAutorizationFinished);
m_task->setNetwork(APPLICATION->network());
m_task->start(); m_task->start();
} }
@ -115,7 +120,7 @@ void MSADeviceCodeStep::deviceAutorizationFinished()
tr("Device authorization failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description)); tr("Device authorization failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description));
return; return;
} }
if (!m_task->wasSuccessful() || m_task->error() != QNetworkReply::NoError) { if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) {
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Failed to retrieve device authorization")); emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Failed to retrieve device authorization"));
qDebug() << *m_response; qDebug() << *m_response;
return; return;
@ -133,9 +138,7 @@ void MSADeviceCodeStep::deviceAutorizationFinished()
m_expiration_timer.setTimerType(Qt::VeryCoarseTimer); m_expiration_timer.setTimerType(Qt::VeryCoarseTimer);
m_expiration_timer.setInterval(rsp.expires_in * 1000); m_expiration_timer.setInterval(rsp.expires_in * 1000);
m_expiration_timer.setSingleShot(true); m_expiration_timer.setSingleShot(true);
connect(&m_expiration_timer, &QTimer::timeout, this, &MSADeviceCodeStep::abort);
m_expiration_timer.start(); m_expiration_timer.start();
m_pool_timer.setTimerType(Qt::VeryCoarseTimer); m_pool_timer.setTimerType(Qt::VeryCoarseTimer);
m_pool_timer.setSingleShot(true); m_pool_timer.setSingleShot(true);
startPoolTimer(); startPoolTimer();
@ -145,8 +148,8 @@ void MSADeviceCodeStep::abort()
{ {
m_expiration_timer.stop(); m_expiration_timer.stop();
m_pool_timer.stop(); m_pool_timer.stop();
if (m_task) { if (m_request) {
m_task->abort(); m_request->abort();
} }
m_is_aborted = true; m_is_aborted = true;
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Task aborted")); emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Task aborted"));
@ -157,8 +160,12 @@ void MSADeviceCodeStep::startPoolTimer()
if (m_is_aborted) { if (m_is_aborted) {
return; return;
} }
if (m_expiration_timer.remainingTime() < interval * 1000) {
perform();
return;
}
m_pool_timer.setInterval(interval * 1000); m_pool_timer.setInterval(interval * 1000);
connect(&m_pool_timer, &QTimer::timeout, this, &MSADeviceCodeStep::authenticateUser);
m_pool_timer.start(); m_pool_timer.start();
} }
@ -175,13 +182,13 @@ void MSADeviceCodeStep::authenticateUser()
{ "Accept", "application/json" }, { "Accept", "application/json" },
}; };
m_response.reset(new QByteArray()); m_response.reset(new QByteArray());
m_task = Net::Upload::makeByteArray(url, m_response, payload); m_request = Net::Upload::makeByteArray(url, m_response, payload);
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers)); m_request->addHeaderProxy(new Net::StaticHeaderProxy(headers));
connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::authenticationFinished); connect(m_request.get(), &Task::finished, this, &MSADeviceCodeStep::authenticationFinished);
m_task->setNetwork(APPLICATION->network()); m_request->setNetwork(APPLICATION->network());
m_task->start(); m_request->start();
} }
struct AuthenticationResponse { struct AuthenticationResponse {
@ -221,7 +228,7 @@ AuthenticationResponse parseAuthenticationResponse(const QByteArray& data)
void MSADeviceCodeStep::authenticationFinished() void MSADeviceCodeStep::authenticationFinished()
{ {
if (m_task->error() == QNetworkReply::TimeoutError) { if (m_request->error() == QNetworkReply::TimeoutError) {
// rfc8628#section-3.5 // rfc8628#section-3.5
// "On encountering a connection timeout, clients MUST unilaterally // "On encountering a connection timeout, clients MUST unilaterally
// reduce their polling frequency before retrying. The use of an // reduce their polling frequency before retrying. The use of an
@ -254,7 +261,7 @@ void MSADeviceCodeStep::authenticationFinished()
tr("Device Access failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description)); tr("Device Access failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description));
return; return;
} }
if (!m_task->wasSuccessful() || m_task->error() != QNetworkReply::NoError) { if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) {
startPoolTimer(); // it failed so just try again without increasing the interval startPoolTimer(); // it failed so just try again without increasing the interval
return; return;
} }

View File

@ -38,6 +38,7 @@
#include <QTimer> #include <QTimer>
#include "minecraft/auth/AuthStep.h" #include "minecraft/auth/AuthStep.h"
#include "net/NetJob.h"
#include "net/Upload.h" #include "net/Upload.h"
class MSADeviceCodeStep : public AuthStep { class MSADeviceCodeStep : public AuthStep {
@ -51,7 +52,7 @@ class MSADeviceCodeStep : public AuthStep {
QString describe() override; QString describe() override;
public slots: public slots:
void abort(); void abort() override;
signals: signals:
void authorizeWithBrowser(QString url, QString code, int expiresIn); void authorizeWithBrowser(QString url, QString code, int expiresIn);
@ -72,5 +73,6 @@ class MSADeviceCodeStep : public AuthStep {
QTimer m_expiration_timer; QTimer m_expiration_timer;
std::shared_ptr<QByteArray> m_response; std::shared_ptr<QByteArray> m_response;
Net::Upload::Ptr m_task; Net::Upload::Ptr m_request;
NetJob::Ptr m_task;
}; };

View File

@ -35,22 +35,74 @@
#include "MSAStep.h" #include "MSAStep.h"
#include <QtNetworkAuth/qoauthhttpserverreplyhandler.h>
#include <QAbstractOAuth2> #include <QAbstractOAuth2>
#include <QNetworkRequest> #include <QNetworkRequest>
#include <QOAuthHttpServerReplyHandler>
#include <QOAuthOobReplyHandler>
#include "Application.h" #include "Application.h"
#include "BuildConfig.h"
#include "FileSystem.h"
#include <QProcess>
#include <QSettings>
#include <QStandardPaths>
bool isSchemeHandlerRegistered()
{
#ifdef Q_OS_LINUX
QProcess process;
process.start("xdg-mime", { "query", "default", "x-scheme-handler/" + BuildConfig.LAUNCHER_APP_BINARY_NAME });
process.waitForFinished();
QString output = process.readAllStandardOutput().trimmed();
return output.contains(BuildConfig.LAUNCHER_APP_BINARY_NAME);
#elif defined(Q_OS_WIN)
QString regPath = QString("HKEY_CURRENT_USER\\Software\\Classes\\%1").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME);
QSettings settings(regPath, QSettings::NativeFormat);
return settings.contains("shell/open/command/.");
#endif
return true;
}
class CustomOAuthOobReplyHandler : public QOAuthOobReplyHandler {
Q_OBJECT
public:
explicit CustomOAuthOobReplyHandler(QObject* parent = nullptr) : QOAuthOobReplyHandler(parent)
{
connect(APPLICATION, &Application::oauthReplyRecieved, this, &QOAuthOobReplyHandler::callbackReceived);
}
~CustomOAuthOobReplyHandler() override
{
disconnect(APPLICATION, &Application::oauthReplyRecieved, this, &QOAuthOobReplyHandler::callbackReceived);
}
QString callback() const override { return BuildConfig.LAUNCHER_APP_BINARY_NAME + "://oauth/microsoft"; }
};
MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(silent) MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(silent)
{ {
m_clientId = APPLICATION->getMSAClientID(); m_clientId = APPLICATION->getMSAClientID();
if (QCoreApplication::applicationFilePath().startsWith("/tmp/.mount_") ||
QFile::exists(FS::PathCombine(APPLICATION->root(), "portable.txt")) || !isSchemeHandlerRegistered())
auto replyHandler = new QOAuthHttpServerReplyHandler(1337, this); {
replyHandler->setCallbackText( auto replyHandler = new QOAuthHttpServerReplyHandler(this);
" <iframe src=\"https://prismlauncher.org/successful-login\" title=\"PrismLauncher Microsoft login\" style=\"position:fixed; " replyHandler->setCallbackText(R"XXX(
"top:0; left:0; bottom:0; right:0; width:100%; height:100%; border:none; margin:0; padding:0; overflow:hidden; " <noscript>
"z-index:999999;\"/> "); <meta http-equiv="Refresh" content="0; URL=https://prismlauncher.org/successful-login" />
oauth2.setReplyHandler(replyHandler); </noscript>
Login Successful, redirecting...
<script>
window.location.replace("https://prismlauncher.org/successful-login");
</script>
)XXX");
oauth2.setReplyHandler(replyHandler);
} else {
oauth2.setReplyHandler(new CustomOAuthOobReplyHandler(this));
}
oauth2.setAuthorizationUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize")); oauth2.setAuthorizationUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"));
oauth2.setAccessTokenUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")); oauth2.setAccessTokenUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/token"));
oauth2.setScope("XboxLive.SignIn XboxLive.offline_access"); oauth2.setScope("XboxLive.SignIn XboxLive.offline_access");
@ -67,9 +119,27 @@ MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(sile
emit finished(AccountTaskState::STATE_WORKING, tr("Got ")); emit finished(AccountTaskState::STATE_WORKING, tr("Got "));
}); });
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &MSAStep::authorizeWithBrowser); connect(&oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &MSAStep::authorizeWithBrowser);
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::requestFailed, this, [this](const QAbstractOAuth2::Error err) { connect(&oauth2, &QOAuth2AuthorizationCodeFlow::requestFailed, this, [this, silent](const QAbstractOAuth2::Error err) {
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication failed.")); auto state = AccountTaskState::STATE_FAILED_HARD;
if (oauth2.status() == QAbstractOAuth::Status::Granted || silent) {
if (err == QAbstractOAuth2::Error::NetworkError) {
state = AccountTaskState::STATE_OFFLINE;
} else {
state = AccountTaskState::STATE_FAILED_SOFT;
}
}
auto message = tr("Microsoft user authentication failed.");
if (silent) {
message = tr("Failed to refresh token.");
}
qWarning() << message;
emit finished(state, message);
}); });
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::error, this,
[this](const QString& error, const QString& errorDescription, const QUrl& uri) {
qWarning() << "Failed to login because" << error << errorDescription;
emit finished(AccountTaskState::STATE_FAILED_HARD, errorDescription);
});
connect(&oauth2, &QOAuth2AuthorizationCodeFlow::extraTokensChanged, this, connect(&oauth2, &QOAuth2AuthorizationCodeFlow::extraTokensChanged, this,
[this](const QVariantMap& tokens) { m_data->msaToken.extra = tokens; }); [this](const QVariantMap& tokens) { m_data->msaToken.extra = tokens; });
@ -89,20 +159,27 @@ void MSAStep::perform()
if (m_data->msaClientID != m_clientId) { if (m_data->msaClientID != m_clientId) {
emit finished(AccountTaskState::STATE_DISABLED, emit finished(AccountTaskState::STATE_DISABLED,
tr("Microsoft user authentication failed - client identification has changed.")); tr("Microsoft user authentication failed - client identification has changed."));
return;
}
if (m_data->msaToken.refresh_token.isEmpty()) {
emit finished(AccountTaskState::STATE_DISABLED, tr("Microsoft user authentication failed - refresh token is empty."));
return;
} }
oauth2.setRefreshToken(m_data->msaToken.refresh_token); oauth2.setRefreshToken(m_data->msaToken.refresh_token);
oauth2.refreshAccessToken(); oauth2.refreshAccessToken();
} else { } else {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) // QMultiMap param changed in 6.0 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) // QMultiMap param changed in 6.0
oauth2.setModifyParametersFunction([](QAbstractOAuth::Stage stage, QMultiMap<QString, QVariant>* map) { oauth2.setModifyParametersFunction(
[](QAbstractOAuth::Stage stage, QMultiMap<QString, QVariant>* map) { map->insert("prompt", "select_account"); });
#else #else
oauth2.setModifyParametersFunction([](QAbstractOAuth::Stage stage, QMap<QString, QVariant>* map) { oauth2.setModifyParametersFunction(
[](QAbstractOAuth::Stage stage, QMap<QString, QVariant>* map) { map->insert("prompt", "select_account"); });
#endif #endif
map->insert("prompt", "select_account");
});
*m_data = AccountData(); *m_data = AccountData();
m_data->msaClientID = m_clientId; m_data->msaClientID = m_clientId;
oauth2.grant(); oauth2.grant();
} }
} }
#include "MSAStep.moc"

View File

@ -22,37 +22,41 @@ void MinecraftProfileStep::perform()
{ "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } }; { "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } };
m_response.reset(new QByteArray()); m_response.reset(new QByteArray());
m_task = Net::Download::makeByteArray(url, m_response); m_request = Net::Download::makeByteArray(url, m_response);
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers)); m_request->addHeaderProxy(new Net::StaticHeaderProxy(headers));
m_task.reset(new NetJob("MinecraftProfileStep", APPLICATION->network()));
m_task->setAskRetry(false);
m_task->addNetAction(m_request);
connect(m_task.get(), &Task::finished, this, &MinecraftProfileStep::onRequestDone); connect(m_task.get(), &Task::finished, this, &MinecraftProfileStep::onRequestDone);
m_task->setNetwork(APPLICATION->network());
m_task->start(); m_task->start();
} }
void MinecraftProfileStep::onRequestDone() void MinecraftProfileStep::onRequestDone()
{ {
if (m_task->error() == QNetworkReply::ContentNotFoundError) { if (m_request->error() == QNetworkReply::ContentNotFoundError) {
// NOTE: Succeed even if we do not have a profile. This is a valid account state. // NOTE: Succeed even if we do not have a profile. This is a valid account state.
m_data->minecraftProfile = MinecraftProfile(); m_data->minecraftProfile = MinecraftProfile();
emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Account has no Minecraft profile.")); emit finished(AccountTaskState::STATE_WORKING, tr("Account has no Minecraft profile."));
return; return;
} }
if (m_task->error() != QNetworkReply::NoError) { if (m_request->error() != QNetworkReply::NoError) {
qWarning() << "Error getting profile:"; qWarning() << "Error getting profile:";
qWarning() << " HTTP Status: " << m_task->replyStatusCode(); qWarning() << " HTTP Status: " << m_request->replyStatusCode();
qWarning() << " Internal error no.: " << m_task->error(); qWarning() << " Internal error no.: " << m_request->error();
qWarning() << " Error string: " << m_task->errorString(); qWarning() << " Error string: " << m_request->errorString();
qWarning() << " Response:"; qWarning() << " Response:";
qWarning() << QString::fromUtf8(*m_response); qWarning() << QString::fromUtf8(*m_response);
if (Net::isApplicationError(m_task->error())) { if (Net::isApplicationError(m_request->error())) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("Minecraft Java profile acquisition failed: %1").arg(m_task->errorString())); tr("Minecraft Java profile acquisition failed: %1").arg(m_request->errorString()));
} else { } else {
emit finished(AccountTaskState::STATE_OFFLINE, tr("Minecraft Java profile acquisition failed: %1").arg(m_task->errorString())); emit finished(AccountTaskState::STATE_OFFLINE,
tr("Minecraft Java profile acquisition failed: %1").arg(m_request->errorString()));
} }
return; return;
} }

View File

@ -4,6 +4,7 @@
#include "minecraft/auth/AuthStep.h" #include "minecraft/auth/AuthStep.h"
#include "net/Download.h" #include "net/Download.h"
#include "net/NetJob.h"
class MinecraftProfileStep : public AuthStep { class MinecraftProfileStep : public AuthStep {
Q_OBJECT Q_OBJECT
@ -21,5 +22,6 @@ class MinecraftProfileStep : public AuthStep {
private: private:
std::shared_ptr<QByteArray> m_response; std::shared_ptr<QByteArray> m_response;
Net::Download::Ptr m_task; Net::Download::Ptr m_request;
NetJob::Ptr m_task;
}; };

View File

@ -42,12 +42,15 @@ void XboxAuthorizationStep::perform()
{ "Accept", "application/json" }, { "Accept", "application/json" },
}; };
m_response.reset(new QByteArray()); m_response.reset(new QByteArray());
m_task = Net::Upload::makeByteArray(url, m_response, xbox_auth_data.toUtf8()); m_request = Net::Upload::makeByteArray(url, m_response, xbox_auth_data.toUtf8());
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers)); m_request->addHeaderProxy(new Net::StaticHeaderProxy(headers));
m_task.reset(new NetJob("XboxAuthorizationStep", APPLICATION->network()));
m_task->setAskRetry(false);
m_task->addNetAction(m_request);
connect(m_task.get(), &Task::finished, this, &XboxAuthorizationStep::onRequestDone); connect(m_task.get(), &Task::finished, this, &XboxAuthorizationStep::onRequestDone);
m_task->setNetwork(APPLICATION->network());
m_task->start(); m_task->start();
qDebug() << "Getting authorization token for " << m_relyingParty; qDebug() << "Getting authorization token for " << m_relyingParty;
} }
@ -55,19 +58,19 @@ void XboxAuthorizationStep::perform()
void XboxAuthorizationStep::onRequestDone() void XboxAuthorizationStep::onRequestDone()
{ {
qCDebug(authCredentials()) << *m_response; qCDebug(authCredentials()) << *m_response;
if (m_task->error() != QNetworkReply::NoError) { if (m_request->error() != QNetworkReply::NoError) {
qWarning() << "Reply error:" << m_task->error(); qWarning() << "Reply error:" << m_request->error();
if (Net::isApplicationError(m_task->error())) { if (Net::isApplicationError(m_request->error())) {
if (!processSTSError()) { if (!processSTSError()) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, m_task->error())); tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, m_request->error()));
} else { } else {
emit finished(AccountTaskState::STATE_FAILED_SOFT, emit finished(AccountTaskState::STATE_FAILED_SOFT,
tr("Unknown STS error for %1 services: %2").arg(m_authorizationKind, m_task->errorString())); tr("Unknown STS error for %1 services: %2").arg(m_authorizationKind, m_request->errorString()));
} }
} else { } else {
emit finished(AccountTaskState::STATE_OFFLINE, emit finished(AccountTaskState::STATE_OFFLINE,
tr("Failed to get authorization for %1 services: %2").arg(m_authorizationKind, m_task->errorString())); tr("Failed to get authorization for %1 services: %2").arg(m_authorizationKind, m_request->errorString()));
} }
return; return;
} }
@ -92,7 +95,7 @@ void XboxAuthorizationStep::onRequestDone()
bool XboxAuthorizationStep::processSTSError() bool XboxAuthorizationStep::processSTSError()
{ {
if (m_task->error() == QNetworkReply::AuthenticationRequiredError) { if (m_request->error() == QNetworkReply::AuthenticationRequiredError) {
QJsonParseError jsonError; QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(*m_response, &jsonError); QJsonDocument doc = QJsonDocument::fromJson(*m_response, &jsonError);
if (jsonError.error) { if (jsonError.error) {

View File

@ -3,6 +3,7 @@
#include <memory> #include <memory>
#include "minecraft/auth/AuthStep.h" #include "minecraft/auth/AuthStep.h"
#include "net/NetJob.h"
#include "net/Upload.h" #include "net/Upload.h"
class XboxAuthorizationStep : public AuthStep { class XboxAuthorizationStep : public AuthStep {
@ -28,5 +29,6 @@ class XboxAuthorizationStep : public AuthStep {
QString m_authorizationKind; QString m_authorizationKind;
std::shared_ptr<QByteArray> m_response; std::shared_ptr<QByteArray> m_response;
Net::Upload::Ptr m_task; Net::Upload::Ptr m_request;
NetJob::Ptr m_task;
}; };

View File

@ -34,25 +34,28 @@ void XboxProfileStep::perform()
}; };
m_response.reset(new QByteArray()); m_response.reset(new QByteArray());
m_task = Net::Download::makeByteArray(url, m_response); m_request = Net::Download::makeByteArray(url, m_response);
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers)); m_request->addHeaderProxy(new Net::StaticHeaderProxy(headers));
m_task.reset(new NetJob("XboxProfileStep", APPLICATION->network()));
m_task->setAskRetry(false);
m_task->addNetAction(m_request);
connect(m_task.get(), &Task::finished, this, &XboxProfileStep::onRequestDone); connect(m_task.get(), &Task::finished, this, &XboxProfileStep::onRequestDone);
m_task->setNetwork(APPLICATION->network());
m_task->start(); m_task->start();
qDebug() << "Getting Xbox profile..."; qDebug() << "Getting Xbox profile...";
} }
void XboxProfileStep::onRequestDone() void XboxProfileStep::onRequestDone()
{ {
if (m_task->error() != QNetworkReply::NoError) { if (m_request->error() != QNetworkReply::NoError) {
qWarning() << "Reply error:" << m_task->error(); qWarning() << "Reply error:" << m_request->error();
qCDebug(authCredentials()) << *m_response; qCDebug(authCredentials()) << *m_response;
if (Net::isApplicationError(m_task->error())) { if (Net::isApplicationError(m_request->error())) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to retrieve the Xbox profile: %1").arg(m_task->errorString())); emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to retrieve the Xbox profile: %1").arg(m_request->errorString()));
} else { } else {
emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to retrieve the Xbox profile: %1").arg(m_task->errorString())); emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to retrieve the Xbox profile: %1").arg(m_request->errorString()));
} }
return; return;
} }

View File

@ -4,6 +4,7 @@
#include "minecraft/auth/AuthStep.h" #include "minecraft/auth/AuthStep.h"
#include "net/Download.h" #include "net/Download.h"
#include "net/NetJob.h"
class XboxProfileStep : public AuthStep { class XboxProfileStep : public AuthStep {
Q_OBJECT Q_OBJECT
@ -21,5 +22,6 @@ class XboxProfileStep : public AuthStep {
private: private:
std::shared_ptr<QByteArray> m_response; std::shared_ptr<QByteArray> m_response;
Net::Download::Ptr m_task; Net::Download::Ptr m_request;
NetJob::Ptr m_task;
}; };

View File

@ -38,24 +38,27 @@ void XboxUserStep::perform()
{ "x-xbl-contract-version", "1" } { "x-xbl-contract-version", "1" }
}; };
m_response.reset(new QByteArray()); m_response.reset(new QByteArray());
m_task = Net::Upload::makeByteArray(url, m_response, xbox_auth_data.toUtf8()); m_request = Net::Upload::makeByteArray(url, m_response, xbox_auth_data.toUtf8());
m_task->addHeaderProxy(new Net::StaticHeaderProxy(headers)); m_request->addHeaderProxy(new Net::StaticHeaderProxy(headers));
m_task.reset(new NetJob("XboxUserStep", APPLICATION->network()));
m_task->setAskRetry(false);
m_task->addNetAction(m_request);
connect(m_task.get(), &Task::finished, this, &XboxUserStep::onRequestDone); connect(m_task.get(), &Task::finished, this, &XboxUserStep::onRequestDone);
m_task->setNetwork(APPLICATION->network());
m_task->start(); m_task->start();
qDebug() << "First layer of XBox auth ... commencing."; qDebug() << "First layer of XBox auth ... commencing.";
} }
void XboxUserStep::onRequestDone() void XboxUserStep::onRequestDone()
{ {
if (m_task->error() != QNetworkReply::NoError) { if (m_request->error() != QNetworkReply::NoError) {
qWarning() << "Reply error:" << m_task->error(); qWarning() << "Reply error:" << m_request->error();
if (Net::isApplicationError(m_task->error())) { if (Net::isApplicationError(m_request->error())) {
emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication failed: %1").arg(m_task->errorString())); emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication failed: %1").arg(m_request->errorString()));
} else { } else {
emit finished(AccountTaskState::STATE_OFFLINE, tr("XBox user authentication failed: %1").arg(m_task->errorString())); emit finished(AccountTaskState::STATE_OFFLINE, tr("XBox user authentication failed: %1").arg(m_request->errorString()));
} }
return; return;
} }

View File

@ -3,6 +3,7 @@
#include <memory> #include <memory>
#include "minecraft/auth/AuthStep.h" #include "minecraft/auth/AuthStep.h"
#include "net/NetJob.h"
#include "net/Upload.h" #include "net/Upload.h"
class XboxUserStep : public AuthStep { class XboxUserStep : public AuthStep {
@ -21,5 +22,6 @@ class XboxUserStep : public AuthStep {
private: private:
std::shared_ptr<QByteArray> m_response; std::shared_ptr<QByteArray> m_response;
Net::Upload::Ptr m_task; Net::Upload::Ptr m_request;
NetJob::Ptr m_task;
}; };

View File

@ -159,15 +159,14 @@ bool Resource::enable(EnableAction action)
if (!path.endsWith(".disabled")) if (!path.endsWith(".disabled"))
return false; return false;
path.chop(9); path.chop(9);
if (!file.rename(path))
return false;
} else { } else {
path += ".disabled"; path += ".disabled";
if (QFile::exists(path)) {
if (!file.rename(path)) path = FS::getUniqueResourceName(path);
return false; }
} }
if (!file.rename(path))
return false;
setFile(QFileInfo(path)); setFile(QFileInfo(path));

View File

@ -215,9 +215,6 @@ bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, Ena
} }
auto new_id = resource->internal_id(); auto new_id = resource->internal_id();
if (m_resources_index.contains(new_id)) {
// FIXME: https://github.com/PolyMC/PolyMC/issues/550
}
m_resources_index.remove(old_id); m_resources_index.remove(old_id);
m_resources_index[new_id] = row; m_resources_index[new_id] = row;

View File

@ -7,6 +7,7 @@
#include <memory> #include <memory>
#include "FileSystem.h"
#include "minecraft/mod/Resource.h" #include "minecraft/mod/Resource.h"
#include "tasks/Task.h" #include "tasks/Task.h"
@ -50,6 +51,12 @@ class BasicFolderLoadTask : public Task {
m_dir.refresh(); m_dir.refresh();
for (auto entry : m_dir.entryInfoList()) { for (auto entry : m_dir.entryInfoList()) {
auto filePath = entry.absoluteFilePath();
auto newFilePath = FS::getUniqueResourceName(filePath);
if (newFilePath != filePath) {
FS::move(filePath, newFilePath);
entry = QFileInfo(newFilePath);
}
auto resource = m_create_func(entry); auto resource = m_create_func(entry);
resource->moveToThread(m_thread_to_spawn_into); resource->moveToThread(m_thread_to_spawn_into);
m_result->resources.insert(resource->internal_id(), resource); m_result->resources.insert(resource->internal_id(), resource);

View File

@ -29,7 +29,6 @@
#include "modplatform/ResourceAPI.h" #include "modplatform/ResourceAPI.h"
#include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameAPI.h"
#include "modplatform/modrinth/ModrinthAPI.h" #include "modplatform/modrinth/ModrinthAPI.h"
#include "tasks/ConcurrentTask.h"
#include "tasks/SequentialTask.h" #include "tasks/SequentialTask.h"
#include "ui/pages/modplatform/ModModel.h" #include "ui/pages/modplatform/ModModel.h"
#include "ui/pages/modplatform/flame/FlameResourceModels.h" #include "ui/pages/modplatform/flame/FlameResourceModels.h"

View File

@ -36,6 +36,7 @@
#include "ModFolderLoadTask.h" #include "ModFolderLoadTask.h"
#include "FileSystem.h"
#include "minecraft/mod/MetadataHandler.h" #include "minecraft/mod/MetadataHandler.h"
#include <QThread> #include <QThread>
@ -63,6 +64,12 @@ void ModFolderLoadTask::executeTask()
// Read JAR files that don't have metadata // Read JAR files that don't have metadata
m_mods_dir.refresh(); m_mods_dir.refresh();
for (auto entry : m_mods_dir.entryInfoList()) { for (auto entry : m_mods_dir.entryInfoList()) {
auto filePath = entry.absoluteFilePath();
auto newFilePath = FS::getUniqueResourceName(filePath);
if (newFilePath != filePath) {
FS::move(filePath, newFilePath);
entry = QFileInfo(newFilePath);
}
Mod* mod(new Mod(entry)); Mod* mod(new Mod(entry));
if (mod->enabled()) { if (mod->enabled()) {

View File

@ -3,7 +3,6 @@
#include "minecraft/mod/Mod.h" #include "minecraft/mod/Mod.h"
#include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
#include "tasks/Task.h" #include "tasks/Task.h"
class ResourceDownloadTask; class ResourceDownloadTask;
@ -15,9 +14,9 @@ class CheckUpdateTask : public Task {
public: public:
CheckUpdateTask(QList<Mod*>& mods, CheckUpdateTask(QList<Mod*>& mods,
std::list<Version>& mcVersions, std::list<Version>& mcVersions,
std::optional<ModPlatform::ModLoaderTypes> loaders, QList<ModPlatform::ModLoaderType> loadersList,
std::shared_ptr<ModFolderModel> mods_folder) std::shared_ptr<ModFolderModel> mods_folder)
: Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder) {}; : Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders_list(loadersList), m_mods_folder(mods_folder) {};
struct UpdatableMod { struct UpdatableMod {
QString name; QString name;
@ -67,7 +66,7 @@ class CheckUpdateTask : public Task {
protected: protected:
QList<Mod*>& m_mods; QList<Mod*>& m_mods;
std::list<Version>& m_game_versions; std::list<Version>& m_game_versions;
std::optional<ModPlatform::ModLoaderTypes> m_loaders; QList<ModPlatform::ModLoaderType> m_loaders_list;
std::shared_ptr<ModFolderModel> m_mods_folder; std::shared_ptr<ModFolderModel> m_mods_folder;
std::vector<UpdatableMod> m_updatable; std::vector<UpdatableMod> m_updatable;

View File

@ -25,7 +25,6 @@
#include <QVariant> #include <QVariant>
#include <QVector> #include <QVector>
#include <memory> #include <memory>
#include <optional>
class QIODevice; class QIODevice;
@ -44,7 +43,7 @@ namespace ProviderCapabilities {
const char* name(ResourceProvider); const char* name(ResourceProvider);
QString readableName(ResourceProvider); QString readableName(ResourceProvider);
QStringList hashType(ResourceProvider); QStringList hashType(ResourceProvider);
}; // namespace ProviderCapabilities } // namespace ProviderCapabilities
struct ModpackAuthor { struct ModpackAuthor {
QString name; QString name;

View File

@ -74,6 +74,7 @@ void Flame::FileResolvingTask::netJobFinished()
setProgress(1, 3); setProgress(1, 3);
// job to check modrinth for blocked projects // job to check modrinth for blocked projects
m_checkJob.reset(new NetJob("Modrinth check", m_network)); m_checkJob.reset(new NetJob("Modrinth check", m_network));
m_checkJob->setAskRetry(false);
blockedProjects = QMap<File*, std::shared_ptr<QByteArray>>(); blockedProjects = QMap<File*, std::shared_ptr<QByteArray>>();
QJsonDocument doc; QJsonDocument doc;

View File

@ -4,6 +4,7 @@
#include "FlameAPI.h" #include "FlameAPI.h"
#include <memory> #include <memory>
#include <optional>
#include "FlameModIndex.h" #include "FlameModIndex.h"
#include "Application.h" #include "Application.h"
@ -12,7 +13,6 @@
#include "net/ApiDownload.h" #include "net/ApiDownload.h"
#include "net/ApiUpload.h" #include "net/ApiUpload.h"
#include "net/NetJob.h" #include "net/NetJob.h"
#include "net/Upload.h"
Task::Ptr FlameAPI::matchFingerprints(const QList<uint>& fingerprints, std::shared_ptr<QByteArray> response) Task::Ptr FlameAPI::matchFingerprints(const QList<uint>& fingerprints, std::shared_ptr<QByteArray> response)
{ {
@ -34,7 +34,7 @@ Task::Ptr FlameAPI::matchFingerprints(const QList<uint>& fingerprints, std::shar
return netJob; return netJob;
} }
auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString QString FlameAPI::getModFileChangelog(int modId, int fileId)
{ {
QEventLoop lock; QEventLoop lock;
QString changelog; QString changelog;
@ -69,7 +69,7 @@ auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString
return changelog; return changelog;
} }
auto FlameAPI::getModDescription(int modId) -> QString QString FlameAPI::getModDescription(int modId)
{ {
QEventLoop lock; QEventLoop lock;
QString description; QString description;
@ -102,7 +102,7 @@ auto FlameAPI::getModDescription(int modId) -> QString
return description; return description;
} }
auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion QList<ModPlatform::IndexedVersion> FlameAPI::getLatestVersions(VersionSearchArgs&& args)
{ {
auto versions_url_optional = getVersionsURL(args); auto versions_url_optional = getVersionsURL(args);
if (!versions_url_optional.has_value()) if (!versions_url_optional.has_value())
@ -114,7 +114,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
auto netJob = makeShared<NetJob>(QString("Flame::GetLatestVersion(%1)").arg(args.pack.name), APPLICATION->network()); auto netJob = makeShared<NetJob>(QString("Flame::GetLatestVersion(%1)").arg(args.pack.name), APPLICATION->network());
auto response = std::make_shared<QByteArray>(); auto response = std::make_shared<QByteArray>();
ModPlatform::IndexedVersion ver; QList<ModPlatform::IndexedVersion> ver;
netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response)); netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response));
@ -134,9 +134,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
for (auto file : arr) { for (auto file : arr) {
auto file_obj = Json::requireObject(file); auto file_obj = Json::requireObject(file);
auto file_tmp = FlameMod::loadIndexedPackVersion(file_obj); ver.append(FlameMod::loadIndexedPackVersion(file_obj));
if (file_tmp.date > ver.date && (!args.loaders.has_value() || !file_tmp.loaders || args.loaders.value() & file_tmp.loaders))
ver = file_tmp;
} }
} catch (Json::JsonException& e) { } catch (Json::JsonException& e) {
@ -146,7 +144,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe
} }
}); });
QObject::connect(netJob.get(), &NetJob::finished, [&loop] { loop.quit(); }); QObject::connect(netJob.get(), &NetJob::finished, &loop, &QEventLoop::quit);
netJob->start(); netJob->start();
@ -261,3 +259,26 @@ QList<ModPlatform::Category> FlameAPI::loadModCategories(std::shared_ptr<QByteAr
} }
return categories; return categories;
}; };
std::optional<ModPlatform::IndexedVersion> FlameAPI::getLatestVersion(QList<ModPlatform::IndexedVersion> versions,
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;
}
}
return ver;
};
for (auto l : instanceLoaders) {
auto ver = bestVersion(l);
if (ver.has_value()) {
return ver;
}
}
return bestVersion(modLoaders);
}

View File

@ -5,7 +5,6 @@
#pragma once #pragma once
#include <QList> #include <QList>
#include <algorithm>
#include <memory> #include <memory>
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h" #include "modplatform/ResourceAPI.h"
@ -13,10 +12,13 @@
class FlameAPI : public NetworkResourceAPI { class FlameAPI : public NetworkResourceAPI {
public: public:
auto getModFileChangelog(int modId, int fileId) -> QString; QString getModFileChangelog(int modId, int fileId);
auto getModDescription(int modId) -> QString; QString getModDescription(int modId);
auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion; QList<ModPlatform::IndexedVersion> getLatestVersions(VersionSearchArgs&& args);
std::optional<ModPlatform::IndexedVersion> getLatestVersion(QList<ModPlatform::IndexedVersion> versions,
QList<ModPlatform::ModLoaderType> instanceLoaders,
ModPlatform::ModLoaderTypes fallback);
Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const override; Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const override;
Task::Ptr matchFingerprints(const QList<uint>& fingerprints, std::shared_ptr<QByteArray> response); Task::Ptr matchFingerprints(const QList<uint>& fingerprints, std::shared_ptr<QByteArray> response);
@ -26,9 +28,9 @@ class FlameAPI : public NetworkResourceAPI {
static Task::Ptr getModCategories(std::shared_ptr<QByteArray> response); static Task::Ptr getModCategories(std::shared_ptr<QByteArray> response);
static QList<ModPlatform::Category> loadModCategories(std::shared_ptr<QByteArray> response); static QList<ModPlatform::Category> loadModCategories(std::shared_ptr<QByteArray> response);
[[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override; [[nodiscard]] QList<ResourceAPI::SortingMethod> getSortingMethods() const override;
static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool static inline bool validateModLoaders(ModPlatform::ModLoaderTypes loaders)
{ {
return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt); return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt);
} }
@ -67,7 +69,7 @@ class FlameAPI : public NetworkResourceAPI {
return 0; return 0;
} }
static auto getModLoaderStrings(const ModPlatform::ModLoaderTypes types) -> const QStringList static const QStringList getModLoaderStrings(const ModPlatform::ModLoaderTypes types)
{ {
QStringList l; QStringList l;
for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt }) { for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt }) {
@ -78,10 +80,7 @@ class FlameAPI : public NetworkResourceAPI {
return l; return l;
} }
static auto getModLoaderFilters(ModPlatform::ModLoaderTypes types) -> const QString static const QString getModLoaderFilters(ModPlatform::ModLoaderTypes types) { return "[" + getModLoaderStrings(types).join(',') + "]"; }
{
return "[" + getModLoaderStrings(types).join(',') + "]";
}
private: private:
[[nodiscard]] std::optional<QString> getSearchURL(SearchArgs const& args) const override [[nodiscard]] std::optional<QString> getSearchURL(SearchArgs const& args) const override

View File

@ -127,25 +127,26 @@ void FlameCheckUpdate::executeTask()
setStatus(tr("Getting API response from CurseForge for '%1'...").arg(mod->name())); setStatus(tr("Getting API response from CurseForge for '%1'...").arg(mod->name()));
setProgress(i++, m_mods.size()); setProgress(i++, m_mods.size());
auto latest_ver = api.getLatestVersion({ { mod->metadata()->project_id.toString() }, m_game_versions, m_loaders }); auto latest_vers = api.getLatestVersions({ { mod->metadata()->project_id.toString() }, m_game_versions });
// Check if we were aborted while getting the latest version // Check if we were aborted while getting the latest version
if (m_was_aborted) { if (m_was_aborted) {
aborted(); aborted();
return; return;
} }
auto latest_ver = api.getLatestVersion(latest_vers, m_loaders_list, mod->loaders());
setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(mod->name())); setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(mod->name()));
if (!latest_ver.addonId.isValid()) { if (!latest_ver.has_value() || !latest_ver->addonId.isValid()) {
emit checkFailed(mod, tr("No valid version found for this mod. It's probably unavailable for the current game " emit checkFailed(mod, tr("No valid version found for this mod. It's probably unavailable for the current game "
"version / mod loader.")); "version / mod loader."));
continue; continue;
} }
if (latest_ver.downloadUrl.isEmpty() && latest_ver.fileId != mod->metadata()->file_id) { if (latest_ver->downloadUrl.isEmpty() && latest_ver->fileId != mod->metadata()->file_id) {
auto pack = getProjectInfo(latest_ver); auto pack = getProjectInfo(latest_ver.value());
auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver.fileId.toString()); auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver->fileId.toString());
emit checkFailed(mod, tr("Mod has a new update available, but is not downloadable using CurseForge."), recover_url); emit checkFailed(mod, tr("Mod has a new update available, but is not downloadable using CurseForge."), recover_url);
continue; continue;
@ -161,19 +162,19 @@ void FlameCheckUpdate::executeTask()
pack->authors.append({ author }); pack->authors.append({ author });
pack->description = mod->description(); pack->description = mod->description();
pack->provider = ModPlatform::ResourceProvider::FLAME; pack->provider = ModPlatform::ResourceProvider::FLAME;
if (!latest_ver.hash.isEmpty() && (mod->metadata()->hash != latest_ver.hash || mod->status() == ModStatus::NotInstalled)) { if (!latest_ver->hash.isEmpty() && (mod->metadata()->hash != latest_ver->hash || mod->status() == ModStatus::NotInstalled)) {
auto old_version = mod->version(); auto old_version = mod->version();
if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) { if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) {
auto current_ver = getFileInfo(latest_ver.addonId.toInt(), mod->metadata()->file_id.toInt()); auto current_ver = getFileInfo(latest_ver->addonId.toInt(), mod->metadata()->file_id.toInt());
old_version = current_ver.version; old_version = current_ver.version;
} }
auto download_task = makeShared<ResourceDownloadTask>(pack, latest_ver, m_mods_folder); auto download_task = makeShared<ResourceDownloadTask>(pack, latest_ver.value(), m_mods_folder);
m_updatable.emplace_back(pack->name, mod->metadata()->hash, old_version, latest_ver.version, latest_ver.version_type, m_updatable.emplace_back(pack->name, mod->metadata()->hash, old_version, latest_ver->version, latest_ver->version_type,
api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()), api.getModFileChangelog(latest_ver->addonId.toInt(), latest_ver->fileId.toInt()),
ModPlatform::ResourceProvider::FLAME, download_task, mod->enabled()); ModPlatform::ResourceProvider::FLAME, download_task, mod->enabled());
} }
m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, latest_ver)); m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, latest_ver.value()));
} }
emitSucceeded(); emitSucceeded();

View File

@ -1,6 +1,5 @@
#pragma once #pragma once
#include "Application.h"
#include "modplatform/CheckUpdateTask.h" #include "modplatform/CheckUpdateTask.h"
#include "net/NetJob.h" #include "net/NetJob.h"
@ -10,9 +9,9 @@ class FlameCheckUpdate : public CheckUpdateTask {
public: public:
FlameCheckUpdate(QList<Mod*>& mods, FlameCheckUpdate(QList<Mod*>& mods,
std::list<Version>& mcVersions, std::list<Version>& mcVersions,
std::optional<ModPlatform::ModLoaderTypes> loaders, QList<ModPlatform::ModLoaderType> loadersList,
std::shared_ptr<ModFolderModel> mods_folder) std::shared_ptr<ModFolderModel> mods_folder)
: CheckUpdateTask(mods, mcVersions, loaders, mods_folder) : CheckUpdateTask(mods, mcVersions, loadersList, mods_folder)
{} {}
public slots: public slots:

View File

@ -18,8 +18,7 @@ Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provid
case ModPlatform::ResourceProvider::FLAME: case ModPlatform::ResourceProvider::FLAME:
return makeShared<Hasher>(file_path, Algorithm::Murmur2); return makeShared<Hasher>(file_path, Algorithm::Murmur2);
default: default:
qCritical() << "[Hashing]" qCritical() << "[Hashing]" << "Unrecognized mod platform!";
<< "Unrecognized mod platform!";
return nullptr; return nullptr;
} }
} }
@ -138,7 +137,8 @@ QString hash(QByteArray data, Algorithm type)
void Hasher::executeTask() void Hasher::executeTask()
{ {
m_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return hash(m_path, m_alg); }); m_future = QtConcurrent::run(
QThreadPool::globalInstance(), [](QString fileName, Algorithm type) { return hash(fileName, type); }, m_path, m_alg);
connect(&m_watcher, &QFutureWatcher<QString>::finished, this, [this] { connect(&m_watcher, &QFutureWatcher<QString>::finished, this, [this] {
if (m_future.isCanceled()) { if (m_future.isCanceled()) {
emitAborted(); emitAborted();

View File

@ -1,23 +1,31 @@
#include "ModrinthCheckUpdate.h" #include "ModrinthCheckUpdate.h"
#include "Application.h"
#include "ModrinthAPI.h" #include "ModrinthAPI.h"
#include "ModrinthPackIndex.h" #include "ModrinthPackIndex.h"
#include "Json.h" #include "Json.h"
#include "QObjectPtr.h"
#include "ResourceDownloadTask.h" #include "ResourceDownloadTask.h"
#include "modplatform/helpers/HashUtils.h" #include "modplatform/helpers/HashUtils.h"
#include "tasks/ConcurrentTask.h" #include "tasks/ConcurrentTask.h"
#include "minecraft/mod/ModFolderModel.h"
static ModrinthAPI api; static ModrinthAPI api;
ModrinthCheckUpdate::ModrinthCheckUpdate(QList<Mod*>& mods,
std::list<Version>& mcVersions,
QList<ModPlatform::ModLoaderType> loadersList,
std::shared_ptr<ModFolderModel> mods_folder)
: CheckUpdateTask(mods, mcVersions, loadersList, mods_folder)
, m_hash_type(ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first())
{}
bool ModrinthCheckUpdate::abort() bool ModrinthCheckUpdate::abort()
{ {
if (m_net_job) if (m_job)
return m_net_job->abort(); return m_job->abort();
return true; return true;
} }
@ -29,147 +37,185 @@ bool ModrinthCheckUpdate::abort()
void ModrinthCheckUpdate::executeTask() void ModrinthCheckUpdate::executeTask()
{ {
setStatus(tr("Preparing mods for Modrinth...")); setStatus(tr("Preparing mods for Modrinth..."));
setProgress(0, 3); setProgress(0, 9);
QHash<QString, Mod*> mappings; auto hashing_task =
makeShared<ConcurrentTask>(this, "MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
// Create all hashes
QStringList hashes;
auto best_hash_type = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first();
ConcurrentTask hashing_task(this, "MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
for (auto* mod : m_mods) { for (auto* mod : m_mods) {
auto hash = mod->metadata()->hash; auto hash = mod->metadata()->hash;
// Sadly the API can only handle one hash type per call, se we // Sadly the API can only handle one hash type per call, se we
// need to generate a new hash if the current one is innadequate // need to generate a new hash if the current one is innadequate
// (though it will rarely happen, if at all) // (though it will rarely happen, if at all)
if (mod->metadata()->hash_format != best_hash_type) { if (mod->metadata()->hash_format != m_hash_type) {
auto hash_task = Hashing::createHasher(mod->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::MODRINTH); auto hash_task = Hashing::createHasher(mod->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::MODRINTH);
connect(hash_task.get(), &Hashing::Hasher::resultsReady, [&hashes, &mappings, mod](QString hash) { connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { m_mappings.insert(hash, mod); });
hashes.append(hash);
mappings.insert(hash, mod);
});
connect(hash_task.get(), &Task::failed, [this] { failed("Failed to generate hash"); }); connect(hash_task.get(), &Task::failed, [this] { failed("Failed to generate hash"); });
hashing_task.addTask(hash_task); hashing_task->addTask(hash_task);
} else { } else {
hashes.append(hash); m_mappings.insert(hash, mod);
mappings.insert(hash, mod);
} }
} }
QEventLoop loop; connect(hashing_task.get(), &Task::finished, this, &ModrinthCheckUpdate::checkNextLoader);
connect(&hashing_task, &Task::finished, [&loop] { loop.quit(); }); m_job = hashing_task;
hashing_task.start(); hashing_task->start();
loop.exec(); }
auto response = std::make_shared<QByteArray>(); void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr<QByteArray> response,
auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response); ModPlatform::ModLoaderTypes loader,
bool forceModLoaderCheck)
{
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
connect(job.get(), &Task::succeeded, this, [this, response, mappings, best_hash_type, job] { emitFailed(parse_error.errorString());
QJsonParseError parse_error{}; return;
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); }
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
emitFailed(parse_error.errorString()); setStatus(tr("Parsing the API response from Modrinth..."));
return; setProgress(m_next_loader_idx * 2, 9);
}
setStatus(tr("Parsing the API response from Modrinth...")); try {
setProgress(2, 3); for (auto hash : m_mappings.keys()) {
if (forceModLoaderCheck && !(m_mappings[hash]->loaders() & loader)) {
try { continue;
for (auto hash : mappings.keys()) {
auto project_obj = doc[hash].toObject();
// If the returned project is empty, but we have Modrinth metadata,
// it means this specific version is not available
if (project_obj.isEmpty()) {
qDebug() << "Mod " << mappings.find(hash).value()->name() << " got an empty response.";
qDebug() << "Hash: " << hash;
emit checkFailed(
mappings.find(hash).value(),
tr("No valid version found for this mod. It's probably unavailable for the current game version / mod loader."));
continue;
}
// Sometimes a version may have multiple files, one with "forge" and one with "fabric",
// so we may want to filter it
QString loader_filter;
if (m_loaders.has_value()) {
static auto flags = { ModPlatform::ModLoaderType::NeoForge, ModPlatform::ModLoaderType::Forge,
ModPlatform::ModLoaderType::Fabric, ModPlatform::ModLoaderType::Quilt };
for (auto flag : flags) {
if (m_loaders.value().testFlag(flag)) {
loader_filter = ModPlatform::getModLoaderAsString(flag);
break;
}
}
}
// Currently, we rely on a couple heuristics to determine whether an update is actually available or not:
// - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the
// loader_filter
// - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case)
// Such is the pain of having arbitrary files for a given version .-.
auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, best_hash_type, loader_filter);
if (project_ver.downloadUrl.isEmpty()) {
qCritical() << "Modrinth mod without download url!";
qCritical() << project_ver.fileName;
emit checkFailed(mappings.find(hash).value(), tr("Mod has an empty download URL"));
continue;
}
auto mod_iter = mappings.find(hash);
if (mod_iter == mappings.end()) {
qCritical() << "Failed to remap mod from Modrinth!";
continue;
}
auto mod = *mod_iter;
auto key = project_ver.hash;
// Fake pack with the necessary info to pass to the download task :)
auto pack = std::make_shared<ModPlatform::IndexedPack>();
pack->name = mod->name();
pack->slug = mod->metadata()->slug;
pack->addonId = mod->metadata()->project_id;
pack->websiteUrl = mod->homeurl();
for (auto& author : mod->authors())
pack->authors.append({ author });
pack->description = mod->description();
pack->provider = ModPlatform::ResourceProvider::MODRINTH;
if ((key != hash && project_ver.is_preferred) || (mod->status() == ModStatus::NotInstalled)) {
if (mod->version() == project_ver.version_number)
continue;
auto download_task = makeShared<ResourceDownloadTask>(pack, project_ver, m_mods_folder);
m_updatable.emplace_back(pack->name, hash, mod->version(), project_ver.version_number, project_ver.version_type,
project_ver.changelog, ModPlatform::ResourceProvider::MODRINTH, download_task, mod->enabled());
}
m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, project_ver));
} }
} catch (Json::JsonException& e) { auto project_obj = doc[hash].toObject();
emitFailed(e.cause() + " : " + e.what());
return;
}
emitSucceeded();
});
connect(job.get(), &Task::failed, this, &ModrinthCheckUpdate::emitFailed); // If the returned project is empty, but we have Modrinth metadata,
// it means this specific version is not available
if (project_obj.isEmpty()) {
qDebug() << "Mod " << m_mappings.find(hash).value()->name() << " got an empty response." << "Hash: " << hash;
continue;
}
// 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.testFlag(flag)) {
loader_filter = ModPlatform::getModLoaderAsString(flag);
break;
}
}
// Currently, we rely on a couple heuristics to determine whether an update is actually available or not:
// - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the
// loader_filter
// - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case)
// Such is the pain of having arbitrary files for a given version .-.
auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, m_hash_type, loader_filter);
if (project_ver.downloadUrl.isEmpty()) {
qCritical() << "Modrinth mod without download url!" << project_ver.fileName;
continue;
}
auto mod_iter = m_mappings.find(hash);
if (mod_iter == m_mappings.end()) {
qCritical() << "Failed to remap mod from Modrinth!";
continue;
}
auto mod = *mod_iter;
m_mappings.remove(hash);
auto key = project_ver.hash;
// Fake pack with the necessary info to pass to the download task :)
auto pack = std::make_shared<ModPlatform::IndexedPack>();
pack->name = mod->name();
pack->slug = mod->metadata()->slug;
pack->addonId = mod->metadata()->project_id;
pack->websiteUrl = mod->homeurl();
for (auto& author : mod->authors())
pack->authors.append({ author });
pack->description = mod->description();
pack->provider = ModPlatform::ResourceProvider::MODRINTH;
if ((key != hash && project_ver.is_preferred) || (mod->status() == ModStatus::NotInstalled)) {
if (mod->version() == project_ver.version_number)
continue;
auto download_task = makeShared<ResourceDownloadTask>(pack, project_ver, m_mods_folder);
m_updatable.emplace_back(pack->name, hash, mod->version(), project_ver.version_number, project_ver.version_type,
project_ver.changelog, ModPlatform::ResourceProvider::MODRINTH, download_task, mod->enabled());
}
m_deps.append(std::make_shared<GetModDependenciesTask::PackDependency>(pack, project_ver));
}
} catch (Json::JsonException& e) {
emitFailed(e.cause() + " : " + e.what());
return;
}
checkNextLoader();
}
void ModrinthCheckUpdate::getUpdateModsForLoader(ModPlatform::ModLoaderTypes loader, bool forceModLoaderCheck)
{
auto response = std::make_shared<QByteArray>();
QStringList hashes;
if (forceModLoaderCheck) {
for (auto hash : m_mappings.keys()) {
if (m_mappings[hash]->loaders() & loader) {
hashes.append(hash);
}
}
} else {
hashes = m_mappings.keys();
}
auto job = api.latestVersions(hashes, m_hash_type, m_game_versions, loader, response);
connect(job.get(), &Task::succeeded, this,
[this, response, loader, forceModLoaderCheck] { checkVersionsResponse(response, loader, forceModLoaderCheck); });
connect(job.get(), &Task::failed, this, &ModrinthCheckUpdate::checkNextLoader);
setStatus(tr("Waiting for the API response from Modrinth...")); setStatus(tr("Waiting for the API response from Modrinth..."));
setProgress(1, 3); setProgress(m_next_loader_idx * 2 - 1, 9);
m_net_job = qSharedPointerObjectCast<NetJob, Task>(job); m_job = job;
job->start(); job->start();
} }
void ModrinthCheckUpdate::checkNextLoader()
{
if (m_mappings.isEmpty()) {
emitSucceeded();
return;
}
if (m_next_loader_idx < m_loaders_list.size()) {
getUpdateModsForLoader(m_loaders_list.at(m_next_loader_idx));
m_next_loader_idx++;
return;
}
static auto flags = { ModPlatform::ModLoaderType::NeoForge, ModPlatform::ModLoaderType::Forge, ModPlatform::ModLoaderType::Quilt,
ModPlatform::ModLoaderType::Fabric };
for (auto flag : flags) {
if (!m_loaders_list.contains(flag)) {
m_loaders_list.append(flag);
m_next_loader_idx++;
setProgress(m_next_loader_idx * 2 - 1, 9);
for (auto m : m_mappings) {
if (m->loaders() & flag) {
getUpdateModsForLoader(flag, true);
return;
}
}
setProgress(m_next_loader_idx * 2, 9);
}
}
for (auto m : m_mappings) {
emit checkFailed(m,
tr("No valid version found for this mod. It's probably unavailable for the current game version / mod loader."));
}
emitSucceeded();
return;
}

View File

@ -1,8 +1,6 @@
#pragma once #pragma once
#include "Application.h"
#include "modplatform/CheckUpdateTask.h" #include "modplatform/CheckUpdateTask.h"
#include "net/NetJob.h"
class ModrinthCheckUpdate : public CheckUpdateTask { class ModrinthCheckUpdate : public CheckUpdateTask {
Q_OBJECT Q_OBJECT
@ -10,17 +8,21 @@ class ModrinthCheckUpdate : public CheckUpdateTask {
public: public:
ModrinthCheckUpdate(QList<Mod*>& mods, ModrinthCheckUpdate(QList<Mod*>& mods,
std::list<Version>& mcVersions, std::list<Version>& mcVersions,
std::optional<ModPlatform::ModLoaderTypes> loaders, QList<ModPlatform::ModLoaderType> loadersList,
std::shared_ptr<ModFolderModel> mods_folder) std::shared_ptr<ModFolderModel> mods_folder);
: CheckUpdateTask(mods, mcVersions, loaders, mods_folder)
{}
public slots: public slots:
bool abort() override; bool abort() override;
protected slots: protected slots:
void executeTask() override; void executeTask() override;
void getUpdateModsForLoader(ModPlatform::ModLoaderTypes loader, bool forceModLoaderCheck = false);
void checkVersionsResponse(std::shared_ptr<QByteArray> response, ModPlatform::ModLoaderTypes loader, bool forceModLoaderCheck = false);
void checkNextLoader();
private: private:
NetJob::Ptr m_net_job = nullptr; Task::Ptr m_job = nullptr;
QHash<QString, Mod*> m_mappings;
QString m_hash_type;
int m_next_loader_idx = 0;
}; };

View File

@ -58,7 +58,6 @@ auto ApiDownload::makeFile(QUrl url, QString path, Options options) -> Download:
void ApiDownload::init() void ApiDownload::init()
{ {
qDebug() << "Setting up api download";
auto api_headers = new ApiHeaderProxy(); auto api_headers = new ApiHeaderProxy();
addHeaderProxy(api_headers); addHeaderProxy(api_headers);
} }

View File

@ -33,7 +33,6 @@ Upload::Ptr ApiUpload::makeByteArray(QUrl url, std::shared_ptr<QByteArray> outpu
void ApiUpload::init() void ApiUpload::init()
{ {
qDebug() << "Setting up api upload";
auto api_headers = new ApiHeaderProxy(); auto api_headers = new ApiHeaderProxy();
addHeaderProxy(api_headers); addHeaderProxy(api_headers);
} }

View File

@ -60,7 +60,11 @@ class ChecksumValidator : public Validator {
return true; return true;
} }
auto abort() -> bool override { return true; } auto abort() -> bool override
{
m_checksum.reset();
return true;
}
auto validate(QNetworkReply&) -> bool override auto validate(QNetworkReply&) -> bool override
{ {

View File

@ -36,6 +36,7 @@
*/ */
#include "NetJob.h" #include "NetJob.h"
#include <QNetworkReply>
#include "net/NetRequest.h" #include "net/NetRequest.h"
#include "tasks/ConcurrentTask.h" #include "tasks/ConcurrentTask.h"
#if defined(LAUNCHER_APPLICATION) #if defined(LAUNCHER_APPLICATION)
@ -145,10 +146,23 @@ void NetJob::updateState()
.arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(totalSize()))); .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(totalSize())));
} }
bool NetJob::isOnline()
{
// check some errors that are ussually associated with the lack of internet
for (auto job : getFailedActions()) {
auto err = job->error();
if (err != QNetworkReply::HostNotFoundError && err != QNetworkReply::NetworkSessionFailedError) {
return true;
}
}
return false;
};
void NetJob::emitFailed(QString reason) void NetJob::emitFailed(QString reason)
{ {
#if defined(LAUNCHER_APPLICATION) #if defined(LAUNCHER_APPLICATION)
if (m_ask_retry) { if (m_ask_retry && m_manual_try < APPLICATION->settings()->get("NumberOfManualRetries").toInt() && isOnline()) {
m_manual_try++;
auto response = CustomMessageBox::selectable(nullptr, "Confirm retry", auto response = CustomMessageBox::selectable(nullptr, "Confirm retry",
"The tasks failed.\n" "The tasks failed.\n"
"Failed urls\n" + "Failed urls\n" +

View File

@ -74,10 +74,12 @@ class NetJob : public ConcurrentTask {
protected: protected:
void updateState() override; void updateState() override;
bool isOnline();
private: private:
shared_qobject_ptr<QNetworkAccessManager> m_network; shared_qobject_ptr<QNetworkAccessManager> m_network;
int m_try = 1; int m_try = 1;
bool m_ask_retry = true; bool m_ask_retry = true;
int m_manual_try = 0;
}; };

View File

@ -86,7 +86,7 @@ void NetRequest::executeTask()
break; break;
case State::Inactive: case State::Inactive:
case State::Failed: case State::Failed:
emit failed("Failed to initilize sink"); emit failed("Failed to initialize sink");
emit finished(); emit finished();
return; return;
case State::AbortedByUser: case State::AbortedByUser:
@ -107,7 +107,11 @@ void NetRequest::executeTask()
} }
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
#if defined(LAUNCHER_APPLICATION)
request.setTransferTimeout(APPLICATION->settings()->get("RequestTimeout").toInt() * 1000);
#else
request.setTransferTimeout(); request.setTransferTimeout();
#endif
#endif #endif
m_last_progress_time = m_clock.now(); m_last_progress_time = m_clock.now();

View File

@ -58,6 +58,7 @@ void NewsChecker::reloadNews()
NetJob::Ptr job{ new NetJob("News RSS Feed", m_network) }; NetJob::Ptr job{ new NetJob("News RSS Feed", m_network) };
job->addNetAction(Net::Download::makeByteArray(m_feedUrl, newsData)); job->addNetAction(Net::Download::makeByteArray(m_feedUrl, newsData));
job->setAskRetry(false);
QObject::connect(job.get(), &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished); QObject::connect(job.get(), &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished);
QObject::connect(job.get(), &NetJob::failed, this, &NewsChecker::rssDownloadFailed); QObject::connect(job.get(), &NetJob::failed, this, &NewsChecker::rssDownloadFailed);
m_newsNetJob.reset(job); m_newsNetJob.reset(job);

View File

@ -2,6 +2,7 @@
<RCC version="1.0"> <RCC version="1.0">
<qresource prefix="/documents"> <qresource prefix="/documents">
<file>../../../COPYING.md</file> <file>../../../COPYING.md</file>
<file>login-qr.svg</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -0,0 +1,8 @@
<svg width="33" height="33" version="1.1" viewBox="0 0 8.7312 8.7312" xmlns="http://www.w3.org/2000/svg">
<g transform="scale(.26458)">
<rect width="33" height="33" fill="#fff"/>
<g>
<path d="m29 29h-6v-1h1v-3h-1v2h-1v-1h-1v1h-2v-1h1v-2h-1v-1h1v-1h-1v-1h1v-1h1v-1h1v1h1v-1h1v-1h-5v3h-2v-1h1v-1h-1v1h-2v-1h1v-1h1v-1h-1v1h-1v1h-1v-2h1v-2h2v-2h1v2h2v1h-1v1h2v-2h-1v-3h-1v-1h1v-2h-2v-1h-1v-1h-1v2h-2v1h-1v-1h-1v-3h1v1h1v1h1v-2h2v-2h1v3h3v5h1v1h-1v1h2v-1h1v1h1v1h2v1h2v2h-1v-1h-2v-1h-1v4h3v-1h1v7h-1v1h-1v1h2zm-10 0h-5v-1h3v-1h1v1h1zm-6 0h-1v-4h3v-1h1v2h1v1h-2v-1h-2zm-2 0h-7v-7h7zm15-1v-2h-1v2zm-16 0v-5h-5v5zm-1-1h-3v-3h3zm9-1h-1v-1h1zm10-1v-1h-1v-1h-1v-2h-1v3h1v1zm-9 0h-1v-1h1zm5-1v-3h-3v3zm-7 0h-1v-2h-1v1h-2v-1h-1v-1h-3v-1h6v1h2v1h2v1h-2zm6-1h-1v-1h1zm5-1v-1h-1v1zm-21-1h-1v-2h1zm-2-1h-1v-1h1zm23-1h-2v-1h2zm-17 0h-4v-1h1v-1h1v1h2zm-5 0h-1v-3h1v-1h1v-1h-1v1h-2v-3h4v1h2v1h-2v1h2v1h2v-1h1v1h1v1h-5v-1h-1v1h-2zm18-2v-2h-2v1h1v1zm-13-2h-1v-1h1zm17-1h-1v-1h1zm-14 0h-1v-1h1zm-2 0h-1v-1h1zm17-1h-1v-1h1zm-2 0h-3v-1h3zm-8 0h-1v-1h1zm-6 0h-1v-1h1zm-2 0h-1v-1h1zm5-1h-1v-1h1zm-2 0h-1v-1h1zm15-1h-7v-7h7zm-10 0h-1v-1h1zm-2 0h-1v-2h1zm-2 0h-1v-1h1zm-2 0h-1v-1h1zm-2 0h-7v-7h7zm17-1v-5h-5v5zm-18 0v-5h-5v5zm17-1h-3v-3h3zm-18 0h-3v-3h3zm5-4h-2v-1h2z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -65,7 +65,7 @@ QNetworkReply* ImgurAlbumCreation::getReply(QNetworkRequest& request)
} }
const QByteArray data = "deletehashes=" + hashes.join(',').toUtf8() + "&title=Minecraft%20Screenshots&privacy=hidden"; const QByteArray data = "deletehashes=" + hashes.join(',').toUtf8() + "&title=Minecraft%20Screenshots&privacy=hidden";
return m_network->post(request, data); return m_network->post(request, data);
}; }
void ImgurAlbumCreation::init() void ImgurAlbumCreation::init()
{ {
@ -81,7 +81,7 @@ auto ImgurAlbumCreation::Sink::init(QNetworkRequest& request) -> Task::State
{ {
m_output.clear(); m_output.clear();
return Task::State::Running; return Task::State::Running;
}; }
auto ImgurAlbumCreation::Sink::write(QByteArray& data) -> Task::State auto ImgurAlbumCreation::Sink::write(QByteArray& data) -> Task::State
{ {

View File

@ -81,13 +81,13 @@ QNetworkReply* ImgurUpload::getReply(QNetworkRequest& request)
multipart->append(namePart); multipart->append(namePart);
return m_network->post(request, multipart); return m_network->post(request, multipart);
}; }
auto ImgurUpload::Sink::init(QNetworkRequest& request) -> Task::State auto ImgurUpload::Sink::init(QNetworkRequest& request) -> Task::State
{ {
m_output.clear(); m_output.clear();
return Task::State::Running; return Task::State::Running;
}; }
auto ImgurUpload::Sink::write(QByteArray& data) -> Task::State auto ImgurUpload::Sink::write(QByteArray& data) -> Task::State
{ {

View File

@ -29,7 +29,7 @@
class OverrideSetting : public Setting { class OverrideSetting : public Setting {
Q_OBJECT Q_OBJECT
public: public:
explicit OverrideSetting(std::shared_ptr<Setting> overriden, std::shared_ptr<Setting> gate); explicit OverrideSetting(std::shared_ptr<Setting> overridden, std::shared_ptr<Setting> gate);
virtual QVariant defValue() const; virtual QVariant defValue() const;
virtual QVariant get() const; virtual QVariant get() const;

View File

@ -28,7 +28,7 @@
class PassthroughSetting : public Setting { class PassthroughSetting : public Setting {
Q_OBJECT Q_OBJECT
public: public:
explicit PassthroughSetting(std::shared_ptr<Setting> overriden, std::shared_ptr<Setting> gate); explicit PassthroughSetting(std::shared_ptr<Setting> overridden, std::shared_ptr<Setting> gate);
virtual QVariant defValue() const; virtual QVariant defValue() const;
virtual QVariant get() const; virtual QVariant get() const;

View File

@ -553,6 +553,7 @@ void TranslationsModel::downloadIndex()
auto task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + "index_v2.json"), entry); auto task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + "index_v2.json"), entry);
d->m_index_task = task.get(); d->m_index_task = task.get();
d->m_index_job->addNetAction(task); d->m_index_job->addNetAction(task);
d->m_index_job->setAskRetry(false);
connect(d->m_index_job.get(), &NetJob::failed, this, &TranslationsModel::indexFailed); connect(d->m_index_job.get(), &NetJob::failed, this, &TranslationsModel::indexFailed);
connect(d->m_index_job.get(), &NetJob::succeeded, this, &TranslationsModel::indexReceived); connect(d->m_index_job.get(), &NetJob::succeeded, this, &TranslationsModel::indexReceived);
d->m_index_job->start(); d->m_index_job->start();

View File

@ -972,6 +972,14 @@ void MainWindow::processURLs(QList<QUrl> urls)
dlUrlDialod.execWithTask(job.get()); dlUrlDialod.execWithTask(job.get());
} }
} else if (url.scheme() == BuildConfig.LAUNCHER_APP_BINARY_NAME) {
QVariantMap receivedData;
const QUrlQuery query(url.query());
const auto items = query.queryItems();
for (auto it = items.begin(), end = items.end(); it != end; ++it)
receivedData.insert(it->first, it->second);
emit APPLICATION->oauthReplyRecieved(receivedData);
continue;
} else { } else {
dl_url = url; dl_url = url;
} }

View File

@ -34,6 +34,8 @@
*/ */
#include "MSALoginDialog.h" #include "MSALoginDialog.h"
#include "Application.h"
#include "ui_MSALoginDialog.h" #include "ui_MSALoginDialog.h"
#include "DesktopServices.h" #include "DesktopServices.h"
@ -41,6 +43,7 @@
#include <QApplication> #include <QApplication>
#include <QClipboard> #include <QClipboard>
#include <QPixmap>
#include <QUrl> #include <QUrl>
#include <QtWidgets/QPushButton> #include <QtWidgets/QPushButton>
@ -48,28 +51,48 @@ MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MS
{ {
ui->setupUi(this); ui->setupUi(this);
ui->cancel->setEnabled(false); // make font monospace
ui->link->setVisible(false); QFont font;
ui->copy->setVisible(false); font.setPixelSize(ui->code->fontInfo().pixelSize());
ui->progressBar->setVisible(false); font.setFamily(APPLICATION->settings()->get("ConsoleFont").toString());
font.setStyleHint(QFont::Monospace);
font.setFixedPitch(true);
ui->code->setFont(font);
connect(ui->cancel, &QPushButton::pressed, this, &QDialog::reject); connect(ui->copyCode, &QPushButton::clicked, this, [this] { QApplication::clipboard()->setText(ui->code->text()); });
connect(ui->copy, &QPushButton::pressed, this, &MSALoginDialog::copyUrl); ui->qr->setPixmap(QIcon((":/documents/login-qr.svg")).pixmap(QSize(150, 150)));
connect(ui->loginButton, &QPushButton::clicked, this, [this] {
if (m_url.isValid()) {
if (!DesktopServices::openUrl(m_url)) {
QApplication::clipboard()->setText(m_url.toString());
}
}
});
} }
int MSALoginDialog::exec() int MSALoginDialog::exec()
{ {
// Setup the login task and start it // Setup the login task and start it
m_account = MinecraftAccount::createBlankMSA(); m_account = MinecraftAccount::createBlankMSA();
m_task = m_account->login(m_using_device_code); m_authflow_task = m_account->login(false);
connect(m_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed); connect(m_authflow_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed);
connect(m_task.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded); connect(m_authflow_task.get(), &Task::succeeded, this, &QDialog::accept);
connect(m_task.get(), &Task::status, this, &MSALoginDialog::onTaskStatus); connect(m_authflow_task.get(), &Task::aborted, this, &MSALoginDialog::reject);
connect(m_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser); connect(m_authflow_task.get(), &Task::status, this, &MSALoginDialog::onTaskStatus);
connect(m_task.get(), &AuthFlow::authorizeWithBrowserWithExtra, this, &MSALoginDialog::authorizeWithBrowserWithExtra); connect(m_authflow_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser);
connect(ui->cancel, &QPushButton::pressed, m_task.get(), &Task::abort); connect(m_authflow_task.get(), &AuthFlow::authorizeWithBrowserWithExtra, this, &MSALoginDialog::authorizeWithBrowserWithExtra);
connect(&m_external_timer, &QTimer::timeout, this, &MSALoginDialog::externalLoginTick); connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_authflow_task.get(), &Task::abort);
m_task->start();
m_devicecode_task.reset(new AuthFlow(m_account->accountData(), AuthFlow::Action::DeviceCode, this));
connect(m_devicecode_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed);
connect(m_devicecode_task.get(), &Task::succeeded, this, &QDialog::accept);
connect(m_devicecode_task.get(), &Task::aborted, this, &MSALoginDialog::reject);
connect(m_devicecode_task.get(), &Task::status, this, &MSALoginDialog::onTaskStatus);
connect(m_devicecode_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser);
connect(m_devicecode_task.get(), &AuthFlow::authorizeWithBrowserWithExtra, this, &MSALoginDialog::authorizeWithBrowserWithExtra);
connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_devicecode_task.get(), &Task::abort);
QMetaObject::invokeMethod(m_authflow_task.get(), &Task::start, Qt::QueuedConnection);
QMetaObject::invokeMethod(m_devicecode_task.get(), &Task::start, Qt::QueuedConnection);
return QDialog::exec(); return QDialog::exec();
} }
@ -79,9 +102,12 @@ MSALoginDialog::~MSALoginDialog()
delete ui; delete ui;
} }
void MSALoginDialog::onTaskFailed(const QString& reason) void MSALoginDialog::onTaskFailed(QString reason)
{ {
// Set message // Set message
m_authflow_task->disconnect();
m_devicecode_task->disconnect();
ui->stackedWidget->setCurrentIndex(0);
auto lines = reason.split('\n'); auto lines = reason.split('\n');
QString processed; QString processed;
for (auto line : lines) { for (auto line : lines) {
@ -91,91 +117,53 @@ void MSALoginDialog::onTaskFailed(const QString& reason)
processed += "<br />"; processed += "<br />";
} }
} }
ui->message->setText(processed); ui->status->setText(processed);
auto task = m_authflow_task;
if (task->failReason().isEmpty()) {
task = m_devicecode_task;
}
if (task) {
ui->loadingLabel->setText(task->getStatus());
}
disconnect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_authflow_task.get(), &Task::abort);
disconnect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_devicecode_task.get(), &Task::abort);
connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &MSALoginDialog::reject);
} }
void MSALoginDialog::onTaskSucceeded() void MSALoginDialog::authorizeWithBrowser(const QUrl& url)
{ {
QDialog::accept(); ui->stackedWidget->setCurrentIndex(1);
ui->loginButton->setToolTip(QString("<div style='width: 200px;'>%1</div>").arg(url.toString()));
m_url = url;
} }
void MSALoginDialog::onTaskStatus(const QString& status) void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn)
{ {
ui->message->setText(status); ui->stackedWidget->setCurrentIndex(1);
ui->cancel->setEnabled(false);
ui->link->setVisible(false); const auto linkString = QString("<a href=\"%1\">%2</a>").arg(url, url);
ui->copy->setVisible(false); ui->code->setText(code);
ui->progressBar->setVisible(false); auto isDefaultUrl = url == "https://www.microsoft.com/link";
ui->qr->setVisible(isDefaultUrl);
if (isDefaultUrl) {
ui->qrMessage->setText(tr("Open %1 or scan the QR and enter the above code.").arg(linkString));
} else {
ui->qrMessage->setText(tr("Open %1 and enter the above code.").arg(linkString));
}
}
void MSALoginDialog::onTaskStatus(QString status)
{
ui->stackedWidget->setCurrentIndex(0);
ui->status->setText(status);
} }
// Public interface // Public interface
MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent, QString msg, bool usingDeviceCode) MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent)
{ {
MSALoginDialog dlg(parent); MSALoginDialog dlg(parent);
dlg.m_using_device_code = usingDeviceCode;
dlg.ui->message->setText(msg);
if (dlg.exec() == QDialog::Accepted) { if (dlg.exec() == QDialog::Accepted) {
return dlg.m_account; return dlg.m_account;
} }
return nullptr; return nullptr;
} }
void MSALoginDialog::authorizeWithBrowser(const QUrl& url)
{
ui->cancel->setEnabled(true);
ui->link->setVisible(true);
ui->copy->setVisible(true);
DesktopServices::openUrl(url);
ui->link->setText(url.toDisplayString());
ui->message->setText(
tr("Browser opened to complete the login process."
"<br /><br />"
"If your browser hasn't opened, please manually open the below link in your browser:"));
}
void MSALoginDialog::copyUrl()
{
QClipboard* cb = QApplication::clipboard();
cb->setText(ui->link->text());
}
void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn)
{
m_external_elapsed = 0;
m_external_timeout = expiresIn;
m_external_timer.setInterval(1000);
m_external_timer.setSingleShot(false);
m_external_timer.start();
ui->progressBar->setMaximum(expiresIn);
ui->progressBar->setValue(m_external_elapsed);
QString linkString = QString("<a href=\"%1\">%2</a>").arg(url, url);
if (url == "https://www.microsoft.com/link" && !code.isEmpty()) {
url += QString("?otc=%1").arg(code);
ui->message->setText(tr("<p>Please login in the opened browser. If no browser was opened, please open up %1 in "
"a browser and put in the code <b>%2</b> to proceed with login.</p>")
.arg(linkString, code));
} else {
ui->message->setText(
tr("<p>Please open up %1 in a browser and put in the code <b>%2</b> to proceed with login.</p>").arg(linkString, code));
}
ui->cancel->setEnabled(true);
ui->link->setVisible(true);
ui->copy->setVisible(true);
ui->progressBar->setVisible(true);
DesktopServices::openUrl(url);
ui->link->setText(code);
}
void MSALoginDialog::externalLoginTick()
{
m_external_elapsed++;
ui->progressBar->setValue(m_external_elapsed);
ui->progressBar->repaint();
if (m_external_elapsed >= m_external_timeout) {
m_external_timer.stop();
}
}

View File

@ -32,29 +32,23 @@ class MSALoginDialog : public QDialog {
public: public:
~MSALoginDialog(); ~MSALoginDialog();
static MinecraftAccountPtr newAccount(QWidget* parent, QString message, bool usingDeviceCode = false); static MinecraftAccountPtr newAccount(QWidget* parent);
int exec() override; int exec() override;
private: private:
explicit MSALoginDialog(QWidget* parent = 0); explicit MSALoginDialog(QWidget* parent = 0);
protected slots: protected slots:
void onTaskFailed(const QString& reason); void onTaskFailed(QString reason);
void onTaskSucceeded(); void onTaskStatus(QString status);
void onTaskStatus(const QString& status);
void authorizeWithBrowser(const QUrl& url); void authorizeWithBrowser(const QUrl& url);
void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn); void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn);
void copyUrl();
void externalLoginTick();
private: private:
Ui::MSALoginDialog* ui; Ui::MSALoginDialog* ui;
MinecraftAccountPtr m_account; MinecraftAccountPtr m_account;
shared_qobject_ptr<AuthFlow> m_task; shared_qobject_ptr<AuthFlow> m_devicecode_task;
shared_qobject_ptr<AuthFlow> m_authflow_task;
int m_external_elapsed; QUrl m_url;
int m_external_timeout;
QTimer m_external_timer;
bool m_using_device_code = false;
}; };

View File

@ -6,87 +6,345 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>491</width> <width>440</width>
<height>208</height> <height>430</height>
</rect> </rect>
</property> </property>
<property name="sizePolicy"> <property name="minimumSize">
<sizepolicy hsizetype="Fixed" vsizetype="MinimumExpanding"> <size>
<horstretch>0</horstretch> <width>0</width>
<verstretch>0</verstretch> <height>430</height>
</sizepolicy> </size>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>Add Microsoft Account</string> <string>Add Microsoft Account</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout_6">
<item> <item>
<widget class="QLabel" name="message"> <widget class="QStackedWidget" name="stackedWidget">
<property name="sizePolicy"> <property name="currentIndex">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding"> <number>1</number>
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>500</width>
<height>500</height>
</size>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property> </property>
<widget class="QWidget" name="loadingPage">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="loadingLabel">
<property name="font">
<font>
<pointsize>16</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Please wait...</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="status">
<property name="text">
<string>Status</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="mpPage">
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,0,0,0">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="loginButton">
<property name="minimumSize">
<size>
<width>250</width>
<height>40</height>
</size>
</property>
<property name="text">
<string>Sign in with Microsoft</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="Line" name="line_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="orLabel">
<property name="font">
<font>
<pointsize>16</pointsize>
</font>
</property>
<property name="text">
<string>Or</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="qr">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>150</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>150</width>
<height>150</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="code">
<property name="font">
<font>
<pointsize>30</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="cursor">
<cursorShape>IBeamCursor</cursorShape>
</property>
<property name="text">
<string>CODE</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="copyCode">
<property name="toolTip">
<string>Copy code to clipboard</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="copy">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="qrMessage">
<property name="text">
<string>Info</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget> </widget>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="linkLayout"> <widget class="QDialogButtonBox" name="buttonBox">
<item> <property name="standardButtons">
<widget class="QLineEdit" name="link"> <set>QDialogButtonBox::Cancel</set>
<property name="readOnly">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="copy">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="copy">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>24</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancel">
<property name="text">
<string>Cancel</string>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -1,4 +1,5 @@
#include "ModUpdateDialog.h" #include "ModUpdateDialog.h"
#include "Application.h"
#include "ChooseProviderDialog.h" #include "ChooseProviderDialog.h"
#include "CustomMessageBox.h" #include "CustomMessageBox.h"
#include "ProgressDialog.h" #include "ProgressDialog.h"
@ -30,9 +31,9 @@ static std::list<Version> mcVersions(BaseInstance* inst)
return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() }; return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() };
} }
static std::optional<ModPlatform::ModLoaderTypes> mcLoaders(BaseInstance* inst) static QList<ModPlatform::ModLoaderType> mcLoadersList(BaseInstance* inst)
{ {
return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getSupportedModLoaders() }; return static_cast<MinecraftInstance*>(inst)->getPackProfile()->getModLoadersList();
} }
ModUpdateDialog::ModUpdateDialog(QWidget* parent, ModUpdateDialog::ModUpdateDialog(QWidget* parent,
@ -86,19 +87,19 @@ void ModUpdateDialog::checkCandidates()
} }
auto versions = mcVersions(m_instance); auto versions = mcVersions(m_instance);
auto loaders = mcLoaders(m_instance); auto loadersList = mcLoadersList(m_instance);
SequentialTask check_task(m_parent, tr("Checking for updates")); SequentialTask check_task(m_parent, tr("Checking for updates"));
if (!m_modrinth_to_update.empty()) { if (!m_modrinth_to_update.empty()) {
m_modrinth_check_task.reset(new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_mod_model)); m_modrinth_check_task.reset(new ModrinthCheckUpdate(m_modrinth_to_update, versions, loadersList, m_mod_model));
connect(m_modrinth_check_task.get(), &CheckUpdateTask::checkFailed, this, connect(m_modrinth_check_task.get(), &CheckUpdateTask::checkFailed, this,
[this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({ mod, reason, recover_url }); }); [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({ mod, reason, recover_url }); });
check_task.addTask(m_modrinth_check_task); check_task.addTask(m_modrinth_check_task);
} }
if (!m_flame_to_update.empty()) { if (!m_flame_to_update.empty()) {
m_flame_check_task.reset(new FlameCheckUpdate(m_flame_to_update, versions, loaders, m_mod_model)); m_flame_check_task.reset(new FlameCheckUpdate(m_flame_to_update, versions, loadersList, m_mod_model));
connect(m_flame_check_task.get(), &CheckUpdateTask::checkFailed, this, connect(m_flame_check_task.get(), &CheckUpdateTask::checkFailed, this,
[this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({ mod, reason, recover_url }); }); [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({ mod, reason, recover_url }); });
check_task.addTask(m_flame_check_task); check_task.addTask(m_flame_check_task);

View File

@ -145,6 +145,7 @@ void ResourceDownloadDialog::confirm()
confirm_dialog->retranslateUi(resourcesString()); confirm_dialog->retranslateUi(resourcesString());
QHash<QString, GetModDependenciesTask::PackDependencyExtraInfo> dependencyExtraInfo; QHash<QString, GetModDependenciesTask::PackDependencyExtraInfo> dependencyExtraInfo;
QStringList depNames;
if (auto task = getModDependenciesTask(); task) { if (auto task = getModDependenciesTask(); task) {
connect(task.get(), &Task::failed, this, connect(task.get(), &Task::failed, this,
[&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
@ -167,8 +168,10 @@ void ResourceDownloadDialog::confirm()
QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
return; return;
} else { } else {
for (auto dep : task->getDependecies()) for (auto dep : task->getDependecies()) {
addResource(dep->pack, dep->version); addResource(dep->pack, dep->version);
depNames << dep->pack->name;
}
dependencyExtraInfo = task->getExtraInfo(); dependencyExtraInfo = task->getExtraInfo();
} }
} }
@ -193,6 +196,9 @@ void ResourceDownloadDialog::confirm()
} }
this->accept(); this->accept();
} else {
for (auto name : depNames)
removeResource(name);
} }
} }

View File

@ -207,7 +207,7 @@
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="label_8"> <widget class="QLabel" name="label_8">
<property name="text"> <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/api-spec/#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/#section/Authentication&quot;&gt;documentation&lt;/a&gt; for more information.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="openExternalLinks"> <property name="openExternalLinks">
<bool>true</bool> <bool>true</bool>

View File

@ -130,20 +130,7 @@ void AccountListPage::listChanged()
void AccountListPage::on_actionAddMicrosoft_triggered() void AccountListPage::on_actionAddMicrosoft_triggered()
{ {
QMessageBox box(this); auto account = MSALoginDialog::newAccount(this);
box.setWindowTitle(tr("Add account"));
box.setText(tr("How do you want to login?"));
box.setIcon(QMessageBox::Question);
auto deviceCode = box.addButton(tr("Legacy"), QMessageBox::ButtonRole::YesRole);
auto authCode = box.addButton(tr("Recommended"), QMessageBox::ButtonRole::NoRole);
auto cancel = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::RejectRole);
box.setDefaultButton(authCode);
box.exec();
if ((box.clickedButton() != deviceCode && box.clickedButton() != authCode) || box.clickedButton() == cancel)
return;
MinecraftAccountPtr account = MSALoginDialog::newAccount(
this, tr("Please enter your Mojang account email and password to add your account."), box.clickedButton() == deviceCode);
if (account) { if (account) {
m_accounts->addAccount(account); m_accounts->addAccount(account);
if (m_accounts->count() == 1) { if (m_accounts->count() == 1) {

View File

@ -66,7 +66,7 @@ class AccountListPage : public QMainWindow, public BasePage {
return icon; return icon;
} }
QString id() const override { return "accounts"; } QString id() const override { return "accounts"; }
QString helpPage() const override { return "Getting-Started#adding-an-account"; } QString helpPage() const override { return "/getting-started/adding-an-account"; }
void retranslate() override; void retranslate() override;
public slots: public slots:

View File

@ -203,6 +203,8 @@ void LauncherPage::applySettings()
s->set("NumberOfConcurrentTasks", ui->numberOfConcurrentTasksSpinBox->value()); s->set("NumberOfConcurrentTasks", ui->numberOfConcurrentTasksSpinBox->value());
s->set("NumberOfConcurrentDownloads", ui->numberOfConcurrentDownloadsSpinBox->value()); s->set("NumberOfConcurrentDownloads", ui->numberOfConcurrentDownloadsSpinBox->value());
s->set("NumberOfManualRetries", ui->numberOfManualRetriesSpinBox->value());
s->set("RequestTimeout", ui->timeoutSecondsSpinBox->value());
// Console settings // Console settings
s->set("ShowConsole", ui->showConsoleCheck->isChecked()); s->set("ShowConsole", ui->showConsoleCheck->isChecked());
@ -240,6 +242,7 @@ void LauncherPage::applySettings()
// Mods // Mods
s->set("ModMetadataDisabled", ui->metadataDisableBtn->isChecked()); s->set("ModMetadataDisabled", ui->metadataDisableBtn->isChecked());
s->set("ModDependenciesDisabled", ui->dependenciesDisableBtn->isChecked()); s->set("ModDependenciesDisabled", ui->dependenciesDisableBtn->isChecked());
s->set("SkipModpackUpdatePrompt", ui->skipModpackUpdatePromptBtn->isChecked());
} }
void LauncherPage::loadSettings() void LauncherPage::loadSettings()
{ {
@ -259,6 +262,8 @@ void LauncherPage::loadSettings()
ui->numberOfConcurrentTasksSpinBox->setValue(s->get("NumberOfConcurrentTasks").toInt()); ui->numberOfConcurrentTasksSpinBox->setValue(s->get("NumberOfConcurrentTasks").toInt());
ui->numberOfConcurrentDownloadsSpinBox->setValue(s->get("NumberOfConcurrentDownloads").toInt()); ui->numberOfConcurrentDownloadsSpinBox->setValue(s->get("NumberOfConcurrentDownloads").toInt());
ui->numberOfManualRetriesSpinBox->setValue(s->get("NumberOfManualRetries").toInt());
ui->timeoutSecondsSpinBox->setValue(s->get("RequestTimeout").toInt());
// Console settings // Console settings
ui->showConsoleCheck->setChecked(s->get("ShowConsole").toBool()); ui->showConsoleCheck->setChecked(s->get("ShowConsole").toBool());
@ -301,6 +306,7 @@ void LauncherPage::loadSettings()
ui->metadataDisableBtn->setChecked(s->get("ModMetadataDisabled").toBool()); ui->metadataDisableBtn->setChecked(s->get("ModMetadataDisabled").toBool());
ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked());
ui->dependenciesDisableBtn->setChecked(s->get("ModDependenciesDisabled").toBool()); ui->dependenciesDisableBtn->setChecked(s->get("ModDependenciesDisabled").toBool());
ui->skipModpackUpdatePromptBtn->setChecked(s->get("SkipModpackUpdatePrompt").toBool());
} }
void LauncherPage::refreshFontPreview() void LauncherPage::refreshFontPreview()

View File

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>511</width> <width>511</width>
<height>629</height> <height>691</height>
</rect> </rect>
</property> </property>
<property name="sizePolicy"> <property name="sizePolicy">
@ -243,6 +243,16 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QCheckBox" name="skipModpackUpdatePromptBtn">
<property name="toolTip">
<string>When creating a new modpack instance, do not suggest updating existing instances instead.</string>
</property>
<property name="text">
<string>Skip modpack update prompt</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -252,6 +262,13 @@
<string>Miscellaneous</string> <string>Miscellaneous</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="1" column="1">
<widget class="QSpinBox" name="numberOfConcurrentDownloadsSpinBox">
<property name="minimum">
<number>1</number>
</property>
</widget>
</item>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="numberOfConcurrentTasksLabel"> <widget class="QLabel" name="numberOfConcurrentTasksLabel">
<property name="text"> <property name="text">
@ -273,10 +290,34 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="2" column="0">
<widget class="QSpinBox" name="numberOfConcurrentDownloadsSpinBox"> <widget class="QLabel" name="numberOfManualRetriesLabel">
<property name="text">
<string>Number of manual retries</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="numberOfManualRetriesSpinBox">
<property name="minimum"> <property name="minimum">
<number>1</number> <number>0</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="timeoutSecondsLabel">
<property name="toolTip">
<string>Seconds to wait until the requests are terminated</string>
</property>
<property name="text">
<string>Timeout for HTTP requests</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="timeoutSecondsSpinBox">
<property name="suffix">
<string>s</string>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -119,6 +119,7 @@ class ModrinthManagedPackPage final : public ManagedPackPage {
void parseManagedPack() override; void parseManagedPack() override;
[[nodiscard]] QString url() const override; [[nodiscard]] QString url() const override;
[[nodiscard]] QString helpPage() const override { return "modrinth-managed-pack"; }
public slots: public slots:
void suggestVersion() override; void suggestVersion() override;
@ -142,6 +143,7 @@ class FlameManagedPackPage final : public ManagedPackPage {
void parseManagedPack() override; void parseManagedPack() override;
[[nodiscard]] QString url() const override; [[nodiscard]] QString url() const override;
[[nodiscard]] QString helpPage() const override { return "curseforge-managed-pack"; }
public slots: public slots:
void suggestVersion() override; void suggestVersion() override;

View File

@ -57,7 +57,7 @@ class OtherLogsPage : public QWidget, public BasePage {
QString id() const override { return "logs"; } QString id() const override { return "logs"; }
QString displayName() const override { return tr("Other logs"); } QString displayName() const override { return tr("Other logs"); }
QIcon icon() const override { return APPLICATION->getThemedIcon("log"); } QIcon icon() const override { return APPLICATION->getThemedIcon("log"); }
QString helpPage() const override { return "Minecraft-Logs"; } QString helpPage() const override { return "other-Logs"; }
void retranslate() override; void retranslate() override;
void openedImpl() override; void openedImpl() override;

View File

@ -48,7 +48,7 @@ class ShaderPackPage : public ExternalResourcesPage {
QString displayName() const override { return tr("Shader packs"); } QString displayName() const override { return tr("Shader packs"); }
QIcon icon() const override { return APPLICATION->getThemedIcon("shaderpacks"); } QIcon icon() const override { return APPLICATION->getThemedIcon("shaderpacks"); }
QString id() const override { return "shaderpacks"; } QString id() const override { return "shaderpacks"; }
QString helpPage() const override { return "Resource-packs"; } QString helpPage() const override { return "shader-packs"; }
bool shouldDisplay() const override { return true; } bool shouldDisplay() const override { return true; }

View File

@ -104,7 +104,7 @@ void ImportPage::updateState()
return; return;
} }
if (ui->modpackEdit->hasAcceptableInput()) { if (ui->modpackEdit->hasAcceptableInput()) {
QString input = ui->modpackEdit->text(); QString input = ui->modpackEdit->text().trimmed();
auto url = QUrl::fromUserInput(input); auto url = QUrl::fromUserInput(input);
if (url.isLocalFile()) { if (url.isLocalFile()) {
// FIXME: actually do some validation of what's inside here... this is fake AF // FIXME: actually do some validation of what's inside here... this is fake AF

View File

@ -317,8 +317,10 @@ std::optional<QIcon> ResourceModel::getIcon(QModelIndex& index, const QUrl& url)
if (QPixmapCache::find(url.toString(), &pixmap)) if (QPixmapCache::find(url.toString(), &pixmap))
return { pixmap }; return { pixmap };
if (!m_current_icon_job) if (!m_current_icon_job) {
m_current_icon_job.reset(new NetJob("IconJob", APPLICATION->network())); m_current_icon_job.reset(new NetJob("IconJob", APPLICATION->network()));
m_current_icon_job->setAskRetry(false);
}
if (m_currently_running_icon_actions.contains(url)) if (m_currently_running_icon_actions.contains(url))
return {}; return {};

View File

@ -40,6 +40,8 @@ class ResourcePackResourcePage : public ResourcePage {
[[nodiscard]] QMap<QString, QString> urlHandlers() const override; [[nodiscard]] QMap<QString, QString> urlHandlers() const override;
[[nodiscard]] inline auto helpPage() const -> QString override { return "resourcepack-platform"; }
protected: protected:
ResourcePackResourcePage(ResourceDownloadDialog* dialog, BaseInstance& instance); ResourcePackResourcePage(ResourceDownloadDialog* dialog, BaseInstance& instance);

View File

@ -42,6 +42,8 @@ class ShaderPackResourcePage : public ResourcePage {
[[nodiscard]] QMap<QString, QString> urlHandlers() const override; [[nodiscard]] QMap<QString, QString> urlHandlers() const override;
[[nodiscard]] inline auto helpPage() const -> QString override { return "shaderpack-platform"; }
protected: protected:
ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance); ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance);

View File

@ -195,6 +195,7 @@ void ListModel::requestLogo(QString file, QString url)
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(file)); MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(file));
auto job = new NetJob(QString("ATLauncher Icon Download %1").arg(file), APPLICATION->network()); auto job = new NetJob(QString("ATLauncher Icon Download %1").arg(file), APPLICATION->network());
job->setAskRetry(false);
job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry));
auto fullPath = entry->getFullPath(); auto fullPath = entry->getFullPath();

View File

@ -110,6 +110,7 @@ void ListModel::requestLogo(QString logo, QString url)
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo)); MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo));
auto job = new NetJob(QString("Flame Icon Download %1").arg(logo), APPLICATION->network()); auto job = new NetJob(QString("Flame Icon Download %1").arg(logo), APPLICATION->network());
job->setAskRetry(false);
job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry));
auto fullPath = entry->getFullPath(); auto fullPath = entry->getFullPath();
@ -172,7 +173,7 @@ void ListModel::performPaginatedSearch()
callbacks.on_succeed = [this](auto& doc, auto& pack) { searchRequestForOneSucceeded(doc); }; callbacks.on_succeed = [this](auto& doc, auto& pack) { searchRequestForOneSucceeded(doc); };
callbacks.on_abort = [this] { callbacks.on_abort = [this] {
qCritical() << "Search task aborted by an unknown reason!"; qCritical() << "Search task aborted by an unknown reason!";
searchRequestFailed("Abborted"); searchRequestFailed("Aborted");
}; };
static const FlameAPI api; static const FlameAPI api;
if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) { if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) {

View File

@ -44,7 +44,7 @@ class ImportFTBPage : public QWidget, public BasePage {
QString displayName() const override { return tr("FTB App Import"); } QString displayName() const override { return tr("FTB App Import"); }
QIcon icon() const override { return APPLICATION->getThemedIcon("ftb_logo"); } QIcon icon() const override { return APPLICATION->getThemedIcon("ftb_logo"); }
QString id() const override { return "import_ftb"; } QString id() const override { return "import_ftb"; }
QString helpPage() const override { return "FTB-platform"; } QString helpPage() const override { return "FTB-import"; }
bool shouldDisplay() const override { return true; } bool shouldDisplay() const override { return true; }
void openedImpl() override; void openedImpl() override;
void retranslate() override; void retranslate() override;

View File

@ -264,6 +264,7 @@ void ListModel::requestLogo(QString file)
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(file)); MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(file));
NetJob* job = new NetJob(QString("FTB Icon Download for %1").arg(file), APPLICATION->network()); NetJob* job = new NetJob(QString("FTB Icon Download for %1").arg(file), APPLICATION->network());
job->setAskRetry(false);
job->addNetAction(Net::ApiDownload::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry)); job->addNetAction(Net::ApiDownload::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry));
auto fullPath = entry->getFullPath(); auto fullPath = entry->getFullPath();

View File

@ -66,7 +66,7 @@ class Page : public QWidget, public BasePage {
QString displayName() const override { return "FTB Legacy"; } QString displayName() const override { return "FTB Legacy"; }
QIcon icon() const override { return APPLICATION->getThemedIcon("ftb_logo"); } QIcon icon() const override { return APPLICATION->getThemedIcon("ftb_logo"); }
QString id() const override { return "legacy_ftb"; } QString id() const override { return "legacy_ftb"; }
QString helpPage() const override { return "FTB-platform"; } QString helpPage() const override { return "FTB-legacy"; }
bool shouldDisplay() const override; bool shouldDisplay() const override;
void openedImpl() override; void openedImpl() override;
void retranslate() override; void retranslate() override;

View File

@ -254,6 +254,7 @@ void ModpackListModel::requestLogo(QString logo, QString url)
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo)); MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo));
auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network()); auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network());
job->setAskRetry(false);
job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry));
auto fullPath = entry->getFullPath(); auto fullPath = entry->getFullPath();

View File

@ -292,6 +292,7 @@ void Technic::ListModel::requestLogo(QString logo, QString url)
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo)); MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo));
auto job = new NetJob(QString("Technic Icon Download %1").arg(logo), APPLICATION->network()); auto job = new NetJob(QString("Technic Icon Download %1").arg(logo), APPLICATION->network());
job->setAskRetry(false);
job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry));
auto fullPath = entry->getFullPath(); auto fullPath = entry->getFullPath();

View File

@ -1,3 +1,37 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2024 Tayou <git@tayou.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "BrightTheme.h" #include "BrightTheme.h"
#include <QObject> #include <QObject>
@ -55,3 +89,7 @@ QString BrightTheme::appStyleSheet()
{ {
return QString(); return QString();
} }
QString BrightTheme::tooltip()
{
return QString();
}

View File

@ -1,3 +1,37 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2024 Tayou <git@tayou.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once #pragma once
#include "FusionTheme.h" #include "FusionTheme.h"
@ -8,6 +42,7 @@ class BrightTheme : public FusionTheme {
QString id() override; QString id() override;
QString name() override; QString name() override;
QString tooltip() override;
bool hasStyleSheet() override; bool hasStyleSheet() override;
QString appStyleSheet() override; QString appStyleSheet() override;
bool hasColorScheme() override; bool hasColorScheme() override;

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Tayou <git@tayou.org> * Copyright (C) 2024 Tayou <git@tayou.org>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -285,3 +285,7 @@ QString CustomTheme::qtTheme()
{ {
return m_widgets; return m_widgets;
} }
QString CustomTheme::tooltip()
{
return m_tooltip;
}

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Tayou <git@tayou.org> * Copyright (C) 2024 Tayou <git@tayou.org>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -44,6 +44,7 @@ class CustomTheme : public ITheme {
QString id() override; QString id() override;
QString name() override; QString name() override;
QString tooltip() override;
bool hasStyleSheet() override; bool hasStyleSheet() override;
QString appStyleSheet() override; QString appStyleSheet() override;
bool hasColorScheme() override; bool hasColorScheme() override;
@ -62,4 +63,10 @@ class CustomTheme : public ITheme {
QString m_id; QString m_id;
QString m_widgets; QString m_widgets;
QString m_qssFilePath; QString m_qssFilePath;
/**
* The tooltip could be defined in the theme json,
* or composed of other fields that could be in there.
* like author, license, etc.
*/
QString m_tooltip = "";
}; };

View File

@ -1,3 +1,37 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2024 Tayou <git@tayou.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "DarkTheme.h" #include "DarkTheme.h"
#include <QObject> #include <QObject>
@ -56,3 +90,7 @@ QString DarkTheme::appStyleSheet()
{ {
return "QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }"; return "QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }";
} }
QString DarkTheme::tooltip()
{
return "";
}

View File

@ -1,3 +1,37 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2024 Tayou <git@tayou.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once #pragma once
#include "FusionTheme.h" #include "FusionTheme.h"
@ -8,6 +42,7 @@ class DarkTheme : public FusionTheme {
QString id() override; QString id() override;
QString name() override; QString name() override;
QString tooltip() override;
bool hasStyleSheet() override; bool hasStyleSheet() override;
QString appStyleSheet() override; QString appStyleSheet() override;
bool hasColorScheme() override; bool hasColorScheme() override;

View File

@ -44,6 +44,7 @@ class ITheme {
virtual void apply(bool initial); virtual void apply(bool initial);
virtual QString id() = 0; virtual QString id() = 0;
virtual QString name() = 0; virtual QString name() = 0;
virtual QString tooltip() = 0;
virtual bool hasStyleSheet() = 0; virtual bool hasStyleSheet() = 0;
virtual QString appStyleSheet() = 0; virtual QString appStyleSheet() = 0;
virtual QString qtTheme() = 0; virtual QString qtTheme() = 0;

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Tayou <git@tayou.org> * Copyright (C) 2024 Tayou <git@tayou.org>
* Copyright (C) 2024 TheKodeToad <TheKodeToad@proton.me> * Copyright (C) 2024 TheKodeToad <TheKodeToad@proton.me>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -35,31 +35,16 @@
*/ */
#include "SystemTheme.h" #include "SystemTheme.h"
#include <QApplication> #include <QApplication>
#include <QDebug>
#include <QStyle> #include <QStyle>
#include <QStyleFactory> #include <QStyleFactory>
#include "HintOverrideProxyStyle.h" #include "HintOverrideProxyStyle.h"
#include "ThemeManager.h" #include "ThemeManager.h"
SystemTheme::SystemTheme() SystemTheme::SystemTheme(QString& styleName, bool isSystemTheme)
{ {
themeDebugLog() << "Determining System Theme..."; themeName = isSystemTheme ? "system" : styleName;
const auto& style = QApplication::style(); widgetTheme = styleName;
systemPalette = QApplication::palette(); colorPalette = QApplication::palette();
QString lowerThemeName = style->objectName();
themeDebugLog() << "System theme seems to be:" << lowerThemeName;
QStringList styles = QStyleFactory::keys();
for (auto& st : styles) {
themeDebugLog() << "Considering theme from theme factory:" << st.toLower();
if (st.toLower() == lowerThemeName) {
systemTheme = st;
themeDebugLog() << "System theme has been determined to be:" << systemTheme;
return;
}
}
// fall back to fusion if we can't find the current theme.
systemTheme = "Fusion";
themeDebugLog() << "System theme not found, defaulted to Fusion";
} }
void SystemTheme::apply(bool initial) void SystemTheme::apply(bool initial)
@ -76,22 +61,49 @@ void SystemTheme::apply(bool initial)
QString SystemTheme::id() QString SystemTheme::id()
{ {
return "system"; return themeName;
} }
QString SystemTheme::name() QString SystemTheme::name()
{ {
return QObject::tr("System"); if (themeName.toLower() == "windowsvista") {
return QObject::tr("Windows Vista");
} else if (themeName.toLower() == "windows") {
return QObject::tr("Windows 9x");
} else if (themeName.toLower() == "windows11") {
return QObject::tr("Windows 11");
} else if (themeName.toLower() == "system") {
return QObject::tr("System");
} else {
return themeName;
}
}
QString SystemTheme::tooltip()
{
if (themeName.toLower() == "windowsvista") {
return QObject::tr("Widget style trying to look like your win32 theme");
} else if (themeName.toLower() == "windows") {
return QObject::tr("Windows 9x inspired widget style");
} else if (themeName.toLower() == "windows11") {
return QObject::tr("WinUI 3 inspired Qt widget style");
} else if (themeName.toLower() == "fusion") {
return QObject::tr("The default Qt widget style");
} else if (themeName.toLower() == "system") {
return QObject::tr("Your current system theme");
} else {
return "";
}
} }
QString SystemTheme::qtTheme() QString SystemTheme::qtTheme()
{ {
return systemTheme; return widgetTheme;
} }
QPalette SystemTheme::colorScheme() QPalette SystemTheme::colorScheme()
{ {
return systemPalette; return colorPalette;
} }
QString SystemTheme::appStyleSheet() QString SystemTheme::appStyleSheet()

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Tayou <git@tayou.org> * Copyright (C) 2024 Tayou <git@tayou.org>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -38,12 +38,13 @@
class SystemTheme : public ITheme { class SystemTheme : public ITheme {
public: public:
SystemTheme(); SystemTheme(QString& themeName, bool isSystemTheme = false);
virtual ~SystemTheme() {} virtual ~SystemTheme() {}
void apply(bool initial) override; void apply(bool initial) override;
QString id() override; QString id() override;
QString name() override; QString name() override;
QString tooltip() override;
QString qtTheme() override; QString qtTheme() override;
bool hasStyleSheet() override; bool hasStyleSheet() override;
QString appStyleSheet() override; QString appStyleSheet() override;
@ -53,6 +54,7 @@ class SystemTheme : public ITheme {
QColor fadeColor() override; QColor fadeColor() override;
private: private:
QPalette systemPalette; QPalette colorPalette;
QString systemTheme; QString widgetTheme;
QString themeName;
}; };

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
/* /*
* Prism Launcher - Minecraft Launcher * Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Tayou <git@tayou.org> * Copyright (C) 2024 Tayou <git@tayou.org>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -23,6 +23,8 @@
#include <QDirIterator> #include <QDirIterator>
#include <QIcon> #include <QIcon>
#include <QImageReader> #include <QImageReader>
#include <QStyle>
#include <QStyleFactory>
#include "Exception.h" #include "Exception.h"
#include "ui/themes/BrightTheme.h" #include "ui/themes/BrightTheme.h"
#include "ui/themes/CatPack.h" #include "ui/themes/CatPack.h"
@ -119,14 +121,30 @@ void ThemeManager::initializeIcons()
void ThemeManager::initializeWidgets() void ThemeManager::initializeWidgets()
{ {
themeDebugLog() << "Determining System Widget Theme...";
const auto& style = QApplication::style();
currentlySelectedSystemTheme = style->objectName();
themeDebugLog() << "System theme seems to be:" << currentlySelectedSystemTheme;
themeDebugLog() << "<> Initializing Widget Themes"; themeDebugLog() << "<> Initializing Widget Themes";
themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique<SystemTheme>()); themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique<SystemTheme>(currentlySelectedSystemTheme, true));
auto darkThemeId = addTheme(std::make_unique<DarkTheme>()); auto darkThemeId = addTheme(std::make_unique<DarkTheme>());
themeDebugLog() << "Loading Built-in Theme:" << darkThemeId; themeDebugLog() << "Loading Built-in Theme:" << darkThemeId;
themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique<BrightTheme>()); themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique<BrightTheme>());
// TODO: need some way to differentiate same name themes in different subdirectories (maybe smaller grey text next to theme name in themeDebugLog() << "<> Initializing System Widget Themes";
// dropdown?) QStringList styles = QStyleFactory::keys();
for (auto& st : styles) {
#ifdef Q_OS_WINDOWS
if (QSysInfo::productVersion() != "11" && st == "windows11") {
continue;
}
#endif
themeDebugLog() << "Loading System Theme:" << addTheme(std::make_unique<SystemTheme>(st));
}
// TODO: need some way to differentiate same name themes in different subdirectories
// (maybe smaller grey text next to theme name in dropdown?)
if (!m_applicationThemeFolder.mkpath(".")) if (!m_applicationThemeFolder.mkpath("."))
themeWarningLog() << "Couldn't create theme folder"; themeWarningLog() << "Couldn't create theme folder";
@ -238,7 +256,11 @@ void ThemeManager::applyCurrentlySelectedTheme(bool initial)
auto settings = APPLICATION->settings(); auto settings = APPLICATION->settings();
setIconTheme(settings->get("IconTheme").toString()); setIconTheme(settings->get("IconTheme").toString());
themeDebugLog() << "<> Icon theme set."; themeDebugLog() << "<> Icon theme set.";
setApplicationTheme(settings->get("ApplicationTheme").toString(), initial); auto applicationTheme = settings->get("ApplicationTheme").toString();
if (applicationTheme == "") {
applicationTheme = currentlySelectedSystemTheme;
}
setApplicationTheme(applicationTheme, initial);
themeDebugLog() << "<> Application theme set."; themeDebugLog() << "<> Application theme set.";
} }

Some files were not shown because too many files have changed in this diff Show More