diff options
Diffstat (limited to 'launcher')
22 files changed, 403 insertions, 399 deletions
diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 6e37e3d8..847d897e 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -38,6 +38,10 @@ #include "ExponentialSeries.h" #include "WatchLock.h" +#ifdef Q_OS_WIN32 +#include <Windows.h> +#endif + const static int GROUP_FILE_FORMAT_VERSION = 1; InstanceList::InstanceList(SettingsObjectPtr settings, const QString & instDir, QObject *parent) @@ -851,13 +855,18 @@ Task * InstanceList::wrapInstanceTask(InstanceTask * task) QString InstanceList::getStagedInstancePath() { QString key = QUuid::createUuid().toString(); - QString relPath = FS::PathCombine("_LAUNCHER_TEMP/" , key); + QString tempDir = ".LAUNCHER_TEMP/"; + QString relPath = FS::PathCombine(tempDir, key); QDir rootPath(m_instDir); auto path = FS::PathCombine(m_instDir, relPath); if(!rootPath.mkpath(relPath)) { return QString(); } +#ifdef Q_OS_WIN32 + auto tempPath = FS::PathCombine(m_instDir, tempDir); + SetFileAttributesA(tempPath.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); +#endif return path; } diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 4cb62e69..002c08b9 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -93,7 +93,7 @@ void LaunchController::decideAccount() auto reply = CustomMessageBox::selectable( m_parentWidget, tr("No Accounts"), - tr("In order to play Minecraft, you must have at least one Mojang or Minecraft " + tr("In order to play Minecraft, you must have at least one Mojang or Microsoft " "account logged in. " "Would you like to open the account manager to add an account now?"), QMessageBox::Information, diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index 584edd69..c269d10a 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -36,7 +36,7 @@ IconList::IconList(const QStringList &builtinPaths, QString path, QObject *paren auto file_info_list = instance_icons.entryInfoList(QDir::Files, QDir::Name); for (auto file_info : file_info_list) { - builtinNames.insert(file_info.baseName()); + builtinNames.insert(file_info.completeBaseName()); } } for(auto & builtinName : builtinNames) @@ -51,6 +51,9 @@ IconList::IconList(const QStringList &builtinPaths, QString path, QObject *paren connect(m_watcher.get(), SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString))); directoryChanged(path); + + // Forces the UI to update, so that lengthy icon names are shown properly from the start + emit iconUpdated({}); } void IconList::directoryChanged(const QString &path) @@ -94,7 +97,13 @@ void IconList::directoryChanged(const QString &path) { qDebug() << "Removing " << remove; QFileInfo rmfile(remove); - QString key = rmfile.baseName(); + QString key = rmfile.completeBaseName(); + + QString suffix = rmfile.suffix(); + // The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well + if (suffix != "jpeg" && suffix != "png" && suffix != "jpg" && suffix != "ico" && suffix != "svg" && suffix != "gif") + key = rmfile.fileName(); + int idx = getIconIndex(key); if (idx == -1) continue; @@ -117,8 +126,15 @@ void IconList::directoryChanged(const QString &path) for (auto add : to_add) { qDebug() << "Adding " << add; + QFileInfo addfile(add); - QString key = addfile.baseName(); + QString key = addfile.completeBaseName(); + + QString suffix = addfile.suffix(); + // The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well + if (suffix != "jpeg" && suffix != "png" && suffix != "jpg" && suffix != "ico" && suffix != "svg" && suffix != "gif") + key = addfile.fileName(); + if (addIcon(key, QString(), addfile.filePath(), IconType::FileBased)) { m_watcher->addPath(add); @@ -133,7 +149,7 @@ void IconList::fileChanged(const QString &path) QFileInfo checkfile(path); if (!checkfile.exists()) return; - QString key = checkfile.baseName(); + QString key = checkfile.completeBaseName(); int idx = getIconIndex(key); if (idx == -1) return; diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 3ba79178..e20dc24c 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -162,6 +162,11 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO m_settings->registerSetting("JoinServerOnLaunch", false); m_settings->registerSetting("JoinServerOnLaunchAddress", ""); + // Miscellaneous + auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false); + m_settings->registerOverride(globalSettings->getSetting("CloseAfterLaunch"), miscellaneousOverride); + m_settings->registerOverride(globalSettings->getSetting("QuitAfterGameStop"), miscellaneousOverride); + m_components.reset(new PackProfile(this)); } @@ -984,7 +989,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt { process->setCensorFilter(createCensorFilterFromSession(session)); } - if(APPLICATION->settings()->get("QuitAfterGameStop").toBool()) + if(m_settings->get("QuitAfterGameStop").toBool()) { auto step = new QuitAfterGameStop(pptr); process->appendStep(step); diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index 173f29b5..d7010355 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -25,7 +25,8 @@ LauncherPartLaunch::LauncherPartLaunch(LaunchTask *parent) : LaunchStep(parent) { - if (APPLICATION->settings()->get("CloseAfterLaunch").toBool()) + auto instance = parent->instance(); + if (instance->settings()->get("CloseAfterLaunch").toBool()) { std::shared_ptr<QMetaObject::Connection> connection{new QMetaObject::Connection}; *connection = connect(&m_process, &LoggedProcess::log, this, [=](QStringList lines, MessageLevel::Enum level) { @@ -168,7 +169,8 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) } case LoggedProcess::Finished: { - if (APPLICATION->settings()->get("CloseAfterLaunch").toBool()) + auto instance = m_parent->instance(); + if (instance->settings()->get("CloseAfterLaunch").toBool()) APPLICATION->showMainWindow(); m_parent->setPid(-1); diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 3889a935..95924a68 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -1,14 +1,9 @@ #include "FileResolvingTask.h" #include "Json.h" -namespace { - const char * metabase = "https://cursemeta.dries007.net"; -} - Flame::FileResolvingTask::FileResolvingTask(shared_qobject_ptr<QNetworkAccessManager> network, Flame::Manifest& toProcess) : m_network(network), m_toProcess(toProcess) -{ -} +{} void Flame::FileResolvingTask::executeTask() { @@ -17,14 +12,13 @@ void Flame::FileResolvingTask::executeTask() m_dljob = new NetJob("Mod id resolver", m_network); results.resize(m_toProcess.files.size()); int index = 0; - for(auto & file: m_toProcess.files) - { + for (auto& file : m_toProcess.files) { auto projectIdStr = QString::number(file.projectId); auto fileIdStr = QString::number(file.fileId); - QString metaurl = QString("%1/%2/%3.json").arg(metabase, projectIdStr, fileIdStr); + QString metaurl = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(projectIdStr, fileIdStr); auto dl = Net::Download::makeByteArray(QUrl(metaurl), &results[index]); m_dljob->addNetAction(dl); - index ++; + index++; } connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished); m_dljob->start(); @@ -34,16 +28,11 @@ void Flame::FileResolvingTask::netJobFinished() { bool failed = false; int index = 0; - for(auto & bytes: results) - { - auto & out = m_toProcess.files[index]; - try - { + for (auto& bytes : results) { + auto& out = m_toProcess.files[index]; + try { failed &= (!out.parseFromBytes(bytes)); - } - catch (const JSONValidationError &e) - { - + } catch (const JSONValidationError& e) { qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:"; qCritical() << e.cause(); qCritical() << "JSON:"; @@ -52,12 +41,9 @@ void Flame::FileResolvingTask::netJobFinished() } index++; } - if(!failed) - { + if (!failed) { emitSucceeded(); - } - else - { + } else { emitFailed(tr("Some mod ID resolving tasks failed.")); } } diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index ce02df65..61628e60 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -4,32 +4,52 @@ class FlameAPI : public NetworkModAPI { private: + inline auto getSortFieldInt(QString sortString) const -> int + { + return sortString == "Featured" ? 1 + : sortString == "Popularity" ? 2 + : sortString == "LastUpdated" ? 3 + : sortString == "Name" ? 4 + : sortString == "Author" ? 5 + : sortString == "TotalDownloads" ? 6 + : sortString == "Category" ? 7 + : sortString == "GameVersion" ? 8 + : 1; + } + + private: inline auto getModSearchURL(SearchArgs& args) const -> QString override { auto gameVersionStr = args.versions.size() != 0 ? QString("gameVersion=%1").arg(args.versions.front().toString()) : QString(); return QString( - "https://addons-ecs.forgesvc.net/api/v2/addon/search?" + "https://api.curseforge.com/v1/mods/search?" "gameId=432&" - "categoryId=0&" - "sectionId=6&" + "classId=6&" "index=%1&" "pageSize=25&" "searchFilter=%2&" - "sort=%3&" + "sortField=%3&" + "sortOrder=desc&" "modLoaderType=%4&" "%5") .arg(args.offset) .arg(args.search) - .arg(args.sorting) + .arg(getSortFieldInt(args.sorting)) .arg(getMappedModLoader(args.mod_loader)) .arg(gameVersionStr); }; inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override { - return QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(args.addonId); + QString gameVersionQuery = args.mcVersions.size() == 1 ? QString("gameVersion=%1&").arg(args.mcVersions.front().toString()) : ""; + QString modLoaderQuery = QString("modLoaderType=%1&").arg(getMappedModLoader(args.loader)); + + return QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&%2%3") + .arg(args.addonId) + .arg(gameVersionQuery) + .arg(modLoaderQuery); }; public: diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index c7b86b5c..ba0824cf 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -10,23 +10,12 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireInteger(obj, "id"); pack.name = Json::requireString(obj, "name"); - pack.websiteUrl = Json::ensureString(obj, "websiteUrl", ""); + pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", ""); pack.description = Json::ensureString(obj, "summary", ""); - bool thumbnailFound = false; - auto attachments = Json::requireArray(obj, "attachments"); - for (auto attachmentRaw : attachments) { - auto attachmentObj = Json::requireObject(attachmentRaw); - bool isDefault = attachmentObj.value("isDefault").toBool(false); - if (isDefault) { - thumbnailFound = true; - pack.logoName = Json::requireString(attachmentObj, "title"); - pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl"); - break; - } - } - - if (!thumbnailFound) { throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name)); } + QJsonObject logo = Json::requireObject(obj, "logo"); + pack.logoName = Json::requireString(logo, "title"); + pack.logoUrl = Json::requireString(logo, "thumbnailUrl"); auto authors = Json::requireArray(obj, "authors"); for (auto authorIter : authors) { @@ -45,18 +34,22 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, { QVector<ModPlatform::IndexedVersion> unsortedVersions; auto profile = (dynamic_cast<MinecraftInstance*>(inst))->getPackProfile(); - bool hasFabric = FlameAPI::getMappedModLoader(profile->getModLoader()) == ModAPI::Fabric; QString mcVersion = profile->getComponentVersion("net.minecraft"); for (auto versionIter : arr) { auto obj = versionIter.toObject(); - auto versionArray = Json::requireArray(obj, "gameVersion"); - if (versionArray.isEmpty()) { continue; } + auto versionArray = Json::requireArray(obj, "gameVersions"); + if (versionArray.isEmpty()) { + continue; + } ModPlatform::IndexedVersion file; for (auto mcVer : versionArray) { - file.mcVersion.append(mcVer.toString()); + auto str = mcVer.toString(); + + if (str.contains('.')) + file.mcVersion.append(str); } file.addonId = pack.addonId; @@ -66,28 +59,9 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, file.downloadUrl = Json::requireString(obj, "downloadUrl"); file.fileName = Json::requireString(obj, "fileName"); - auto modules = Json::requireArray(obj, "modules"); - bool is_valid_fabric_version = false; - for (auto m : modules) { - auto fname = Json::requireString(m.toObject(), "foldername"); - // FIXME: This does not work properly when a mod supports more than one mod loader, since - // FIXME: This also doesn't deal with Quilt mods at the moment - // they bundle the meta files for all of them in the same arquive, even when that version - // doesn't support the given mod loader. - if (hasFabric) { - if (fname == "fabric.mod.json") { - is_valid_fabric_version = true; - break; - } - } else - break; - // NOTE: Since we're not validating forge versions, we can just skip this loop. - } - - if (hasFabric && !is_valid_fabric_version) continue; - unsortedVersions.append(file); } + auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { // dates are in RFC 3339 format return a.date > b.date; diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp index 3d8ea22a..ac24c647 100644 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -2,76 +2,63 @@ #include "Json.h" -void Flame::loadIndexedPack(Flame::IndexedPack & pack, QJsonObject & obj) +void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireInteger(obj, "id"); pack.name = Json::requireString(obj, "name"); - pack.websiteUrl = Json::ensureString(obj, "websiteUrl", ""); + pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", ""); pack.description = Json::ensureString(obj, "summary", ""); - bool thumbnailFound = false; - auto attachments = Json::requireArray(obj, "attachments"); - for(auto attachmentRaw: attachments) { - auto attachmentObj = Json::requireObject(attachmentRaw); - bool isDefault = attachmentObj.value("isDefault").toBool(false); - if(isDefault) { - thumbnailFound = true; - pack.logoName = Json::requireString(attachmentObj, "title"); - pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl"); - break; - } - } - - if(!thumbnailFound) { - throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name)); - } + auto logo = Json::requireObject(obj, "logo"); + pack.logoName = Json::requireString(logo, "title"); + pack.logoUrl = Json::requireString(logo, "thumbnailUrl"); auto authors = Json::requireArray(obj, "authors"); - for(auto authorIter: authors) { + for (auto authorIter : authors) { auto author = Json::requireObject(authorIter); Flame::ModpackAuthor packAuthor; packAuthor.name = Json::requireString(author, "name"); packAuthor.url = Json::requireString(author, "url"); pack.authors.append(packAuthor); } - int defaultFileId = Json::requireInteger(obj, "defaultFileId"); + int defaultFileId = Json::requireInteger(obj, "mainFileId"); bool found = false; // check if there are some files before adding the pack auto files = Json::requireArray(obj, "latestFiles"); - for(auto fileIter: files) { + for (auto fileIter : files) { auto file = Json::requireObject(fileIter); int id = Json::requireInteger(file, "id"); // NOTE: for now, ignore everything that's not the default... - if(id != defaultFileId) { + if (id != defaultFileId) { continue; } - auto versionArray = Json::requireArray(file, "gameVersion"); - if(versionArray.size() < 1) { + auto versionArray = Json::requireArray(file, "gameVersions"); + if (versionArray.size() < 1) { continue; } found = true; break; } - if(!found) { + if (!found) { throw JSONValidationError(QString("Pack with no good file, skipping: %1").arg(pack.name)); } } -void Flame::loadIndexedPackVersions(Flame::IndexedPack & pack, QJsonArray & arr) +void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr) { QVector<Flame::IndexedVersion> unsortedVersions; - for(auto versionIter: arr) { + for (auto versionIter : arr) { auto version = Json::requireObject(versionIter); - Flame::IndexedVersion file; + Flame::IndexedVersion file; file.addonId = pack.addonId; file.fileId = Json::requireInteger(version, "id"); - auto versionArray = Json::requireArray(version, "gameVersion"); - if(versionArray.size() < 1) { + auto versionArray = Json::requireArray(version, "gameVersions"); + if (versionArray.size() < 1) { continue; } @@ -82,10 +69,7 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack & pack, QJsonArray & arr) unsortedVersions.append(file); } - auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool - { - return a.fileId > b.fileId; - }; + auto orderSortPredicate = [](const IndexedVersion& a, const IndexedVersion& b) -> bool { return a.fileId > b.fileId; }; std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); pack.versions = unsortedVersions; pack.versionsLoaded = true; diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp index b928fd16..e4f90c1a 100644 --- a/launcher/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -1,28 +1,27 @@ #include "PackManifest.h" #include "Json.h" -static void loadFileV1(Flame::File & f, QJsonObject & file) +static void loadFileV1(Flame::File& f, QJsonObject& file) { f.projectId = Json::requireInteger(file, "projectID"); f.fileId = Json::requireInteger(file, "fileID"); f.required = Json::ensureBoolean(file, QString("required"), true); } -static void loadModloaderV1(Flame::Modloader & m, QJsonObject & modLoader) +static void loadModloaderV1(Flame::Modloader& m, QJsonObject& modLoader) { m.id = Json::requireString(modLoader, "id"); m.primary = Json::ensureBoolean(modLoader, QString("primary"), false); } -static void loadMinecraftV1(Flame::Minecraft & m, QJsonObject & minecraft) +static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft) { m.version = Json::requireString(minecraft, "version"); // extra libraries... apparently only used for a custom Minecraft launcher in the 1.2.5 FTB retro pack // intended use is likely hardcoded in the 'Flame' client, the manifest says nothing m.libraries = Json::ensureString(minecraft, QString("libraries"), QString()); auto arr = Json::ensureArray(minecraft, "modLoaders", QJsonArray()); - for (QJsonValueRef item : arr) - { + for (QJsonValueRef item : arr) { auto obj = Json::requireObject(item); Flame::Modloader loader; loadModloaderV1(loader, obj); @@ -30,16 +29,15 @@ static void loadMinecraftV1(Flame::Minecraft & m, QJsonObject & minecraft) } } -static void loadManifestV1(Flame::Manifest & m, QJsonObject & manifest) +static void loadManifestV1(Flame::Manifest& m, QJsonObject& manifest) { auto mc = Json::requireObject(manifest, "minecraft"); loadMinecraftV1(m.minecraft, mc); m.name = Json::ensureString(manifest, QString("name"), "Unnamed"); m.version = Json::ensureString(manifest, QString("version"), QString()); - m.author = Json::ensureString(manifest, QString("author"), "Anonymous Coward"); + m.author = Json::ensureString(manifest, QString("author"), "Anonymous"); auto arr = Json::ensureArray(manifest, "files", QJsonArray()); - for (QJsonValueRef item : arr) - { + for (QJsonValueRef item : arr) { auto obj = Json::requireObject(item); Flame::File file; loadFileV1(file, obj); @@ -48,18 +46,16 @@ static void loadManifestV1(Flame::Manifest & m, QJsonObject & manifest) m.overrides = Json::ensureString(manifest, "overrides", "overrides"); } -void Flame::loadManifest(Flame::Manifest & m, const QString &filepath) +void Flame::loadManifest(Flame::Manifest& m, const QString& filepath) { auto doc = Json::requireDocument(filepath); auto obj = Json::requireObject(doc); m.manifestType = Json::requireString(obj, "manifestType"); - if(m.manifestType != "minecraftModpack") - { + if (m.manifestType != "minecraftModpack") { throw JSONValidationError("Not a modpack manifest!"); } m.manifestVersion = Json::requireInteger(obj, "manifestVersion"); - if(m.manifestVersion != 1) - { + if (m.manifestVersion != 1) { throw JSONValidationError(QString("Unknown manifest version (%1)").arg(m.manifestVersion)); } loadManifestV1(m, obj); @@ -68,59 +64,30 @@ void Flame::loadManifest(Flame::Manifest & m, const QString &filepath) bool Flame::File::parseFromBytes(const QByteArray& bytes) { auto doc = Json::requireDocument(bytes); - auto obj = Json::requireObject(doc); - // result code signifies true failure. - if(obj.contains("code")) - { - qCritical() << "Resolving of" << projectId << fileId << "failed because of a negative result:"; - qCritical() << bytes; - return false; + if (!doc.isObject()) { + throw JSONValidationError(QString("data is not an object? that's not supposed to happen")); } - fileName = Json::requireString(obj, "FileNameOnDisk"); - QString rawUrl = Json::requireString(obj, "DownloadURL"); + auto obj = Json::ensureObject(doc.object(), "data"); + + fileName = Json::requireString(obj, "fileName"); + + QString rawUrl = Json::requireString(obj, "downloadUrl"); url = QUrl(rawUrl, QUrl::TolerantMode); - if(!url.isValid()) - { + if (!url.isValid()) { throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); } // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience // It is also optional - QJsonObject projObj = Json::ensureObject(obj, "_Project", {}); - if(!projObj.isEmpty()) - { - QString strType = Json::ensureString(projObj, "PackageType", "mod").toLower(); - if(strType == "singlefile") - { - type = File::Type::SingleFile; - } - else if(strType == "ctoc") - { - type = File::Type::Ctoc; - } - else if(strType == "cmod2") - { - type = File::Type::Cmod2; - } - else if(strType == "mod") - { - type = File::Type::Mod; - } - else if(strType == "folder") - { - type = File::Type::Folder; - } - else if(strType == "modpack") - { - type = File::Type::Modpack; - } - else - { - qCritical() << "Resolving of" << projectId << fileId << "failed because of unknown file type:" << strType; - type = File::Type::Unknown; - return false; - } - targetFolder = Json::ensureString(projObj, "Path", "mods"); + type = File::Type::SingleFile; + + if (fileName.endsWith(".zip")) { + // this is probably a resource pack + targetFolder = "resourcepacks"; + } else { + // this is probably a mod, dunno what else could modpacks download + targetFolder = "mods"; } + resolved = true; return true; } diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index b314573f..65cc8f67 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -15,27 +15,27 @@ #include "Download.h" -#include <QFileInfo> #include <QDateTime> #include <QDebug> +#include <QFileInfo> -#include "FileSystem.h" +#include "ByteArraySink.h" #include "ChecksumValidator.h" +#include "FileSystem.h" #include "MetaCacheSink.h" -#include "ByteArraySink.h" #include "BuildConfig.h" namespace Net { -Download::Download():NetAction() +Download::Download() : NetAction() { m_status = Job_NotStarted; } Download::Ptr Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) { - Download * dl = new Download(); + Download* dl = new Download(); dl->m_url = url; dl->m_options = options; auto md5Node = new ChecksumValidator(QCryptographicHash::Md5); @@ -45,9 +45,9 @@ Download::Ptr Download::makeCached(QUrl url, MetaEntryPtr entry, Options options return dl; } -Download::Ptr Download::makeByteArray(QUrl url, QByteArray *output, Options options) +Download::Ptr Download::makeByteArray(QUrl url, QByteArray* output, Options options) { - Download * dl = new Download(); + Download* dl = new Download(); dl->m_url = url; dl->m_options = options; dl->m_sink.reset(new ByteArraySink(output)); @@ -56,30 +56,28 @@ Download::Ptr Download::makeByteArray(QUrl url, QByteArray *output, Options opti Download::Ptr Download::makeFile(QUrl url, QString path, Options options) { - Download * dl = new Download(); + Download* dl = new Download(); dl->m_url = url; dl->m_options = options; dl->m_sink.reset(new FileSink(path)); return dl; } -void Download::addValidator(Validator * v) +void Download::addValidator(Validator* v) { m_sink->addValidator(v); } void Download::startImpl() { - if(m_status == Job_Aborted) - { + if (m_status == Job_Aborted) { qWarning() << "Attempt to start an aborted Download:" << m_url.toString(); emit aborted(m_index_within_job); return; } QNetworkRequest request(m_url); m_status = m_sink->init(request); - switch(m_status) - { + switch (m_status) { case Job_Finished: emit succeeded(m_index_within_job); qDebug() << "Download cache hit " << m_url.toString(); @@ -87,7 +85,7 @@ void Download::startImpl() case Job_InProgress: qDebug() << "Downloading " << m_url.toString(); break; - case Job_Failed_Proceed: // this is meaningless in this context. We do need a sink. + case Job_Failed_Proceed: // this is meaningless in this context. We do need a sink. case Job_NotStarted: case Job_Failed: emit failed(m_index_within_job); @@ -97,8 +95,11 @@ void Download::startImpl() } request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT); + if (request.url().host().contains("api.curseforge.com")) { + request.setRawHeader("x-api-key", BuildConfig.CURSEFORGE_API_KEY.toUtf8()); + }; - QNetworkReply *rep = m_network->get(request); + QNetworkReply* rep = m_network->get(request); m_reply.reset(rep); connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64))); @@ -117,17 +118,12 @@ void Download::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) void Download::downloadError(QNetworkReply::NetworkError error) { - if(error == QNetworkReply::OperationCanceledError) - { + if (error == QNetworkReply::OperationCanceledError) { qCritical() << "Aborted " << m_url.toString(); m_status = Job_Aborted; - } - else - { - if(m_options & Option::AcceptLocalFiles) - { - if(m_sink->hasLocalData()) - { + } else { + if (m_options & Option::AcceptLocalFiles) { + if (m_sink->hasLocalData()) { m_status = Job_Failed_Proceed; return; } @@ -138,11 +134,10 @@ void Download::downloadError(QNetworkReply::NetworkError error) } } -void Download::sslErrors(const QList<QSslError> & errors) +void Download::sslErrors(const QList<QSslError>& errors) { int i = 1; - for (auto error : errors) - { + for (auto error : errors) { qCritical() << "Download" << m_url.toString() << "SSL Error #" << i << " : " << error.errorString(); auto cert = error.certificate(); qCritical() << "Certificate in question:\n" << cert.toText(); @@ -153,33 +148,27 @@ void Download::sslErrors(const QList<QSslError> & errors) bool Download::handleRedirect() { QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl(); - if(!redirect.isValid()) - { - if(!m_reply->hasRawHeader("Location")) - { + if (!redirect.isValid()) { + if (!m_reply->hasRawHeader("Location")) { // no redirect -> it's fine to continue return false; } // there is a Location header, but it's not correct. we need to apply some workarounds... QByteArray redirectBA = m_reply->rawHeader("Location"); - if(redirectBA.size() == 0) - { + if (redirectBA.size() == 0) { // empty, yet present redirect header? WTF? return false; } QString redirectStr = QString::fromUtf8(redirectBA); - if(redirectStr.startsWith("//")) - { + if (redirectStr.startsWith("//")) { /* * IF the URL begins with //, we need to insert the URL scheme. * See: https://bugreports.qt.io/browse/QTBUG-41061 * See: http://tools.ietf.org/html/rfc3986#section-4.2 */ redirectStr = m_reply->url().scheme() + ":" + redirectStr; - } - else if(redirectStr.startsWith("/")) - { + } else if (redirectStr.startsWith("/")) { /* * IF the URL begins with /, we need to process it as a relative URL */ @@ -193,16 +182,13 @@ bool Download::handleRedirect() * FIXME: report Qt bug for this */ redirect = QUrl(redirectStr, QUrl::TolerantMode); - if(!redirect.isValid()) - { + if (!redirect.isValid()) { qWarning() << "Failed to parse redirect URL:" << redirectStr; downloadError(QNetworkReply::ProtocolFailure); return false; } qDebug() << "Fixed location header:" << redirect; - } - else - { + } else { qDebug() << "Location header:" << redirect; } @@ -212,35 +198,28 @@ bool Download::handleRedirect() return true; } - void Download::downloadFinished() { // handle HTTP redirection first - if(handleRedirect()) - { + if (handleRedirect()) { qDebug() << "Download redirected:" << m_url.toString(); return; } // if the download failed before this point ... - if (m_status == Job_Failed_Proceed) - { + if (m_status == Job_Failed_Proceed) { qDebug() << "Download failed but we are allowed to proceed:" << m_url.toString(); m_sink->abort(); m_reply.reset(); emit succeeded(m_index_within_job); return; - } - else if (m_status == Job_Failed) - { + } else if (m_status == Job_Failed) { qDebug() << "Download failed in previous step:" << m_url.toString(); m_sink->abort(); m_reply.reset(); emit failed(m_index_within_job); return; - } - else if(m_status == Job_Aborted) - { + } else if (m_status == Job_Aborted) { qDebug() << "Download aborted in previous step:" << m_url.toString(); m_sink->abort(); m_reply.reset(); @@ -250,16 +229,14 @@ void Download::downloadFinished() // make sure we got all the remaining data, if any auto data = m_reply->readAll(); - if(data.size()) - { + if (data.size()) { qDebug() << "Writing extra" << data.size() << "bytes to" << m_target_path; m_status = m_sink->write(data); } // otherwise, finalize the whole graph m_status = m_sink->finalize(*m_reply.get()); - if (m_status != Job_Finished) - { + if (m_status != Job_Finished) { qDebug() << "Download failed to finalize:" << m_url.toString(); m_sink->abort(); m_reply.reset(); @@ -273,32 +250,25 @@ void Download::downloadFinished() void Download::downloadReadyRead() { - if(m_status == Job_InProgress) - { + if (m_status == Job_InProgress) { auto data = m_reply->readAll(); m_status = m_sink->write(data); - if(m_status == Job_Failed) - { + if (m_status == Job_Failed) { qCritical() << "Failed to process response chunk for " << m_target_path; } // qDebug() << "Download" << m_url.toString() << "gained" << data.size() << "bytes"; - } - else - { + } else { qCritical() << "Cannot write to " << m_target_path << ", illegal status" << m_status; } } -} +} // namespace Net bool Net::Download::abort() { - if(m_reply) - { + if (m_reply) { m_reply->abort(); - } - else - { + } else { m_status = Job_Aborted; } return true; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 44eba369..f016dc76 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -238,6 +238,9 @@ public: TranslatedAction actionREDDIT; TranslatedAction actionAbout; + TranslatedAction actionNoAccountsAdded; + TranslatedAction actionNoDefaultAccount; + QVector<TranslatedToolButton *> all_toolbuttons; QWidget *centralWidget = nullptr; @@ -831,7 +834,7 @@ public: QMetaObject::connectSlotsByName(MainWindow); } // setupUi - void retranslateUi(QMainWindow *MainWindow) + void retranslateUi(MainWindow *MainWindow) { QString winTitle = tr("%1 - Version %2", "Launcher - Version X").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString()); MainWindow->setWindowTitle(winTitle); @@ -851,6 +854,12 @@ public: // submenu buttons foldersMenuButton->setText(tr("Folders")); helpMenuButton->setText(tr("Help")); + + // playtime counter + if (MainWindow->m_statusCenter) + { + MainWindow->updateStatusCenter(); + } } // retranslateUi }; @@ -953,6 +962,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow ui->mainToolBar->addWidget(spacer); accountMenu = new QMenu(this); + // Use undocumented property... https://stackoverflow.com/questions/7121718/create-a-scrollbar-in-a-submenu-qt + accountMenu->setStyleSheet("QMenu { menu-scrollable: 1; }"); repopulateAccountsMenu(); @@ -1255,10 +1266,14 @@ void MainWindow::repopulateAccountsMenu() if (accounts->count() <= 0) { - QAction *action = new QAction(tr("No accounts added!"), this); - action->setEnabled(false); - accountMenu->addAction(action); - ui->profileMenu->addAction(action); + ui->all_actions.removeAll(&ui->actionNoAccountsAdded); + ui->actionNoAccountsAdded = TranslatedAction(this); + ui->actionNoAccountsAdded->setObjectName(QStringLiteral("actionNoAccountsAdded")); + ui->actionNoAccountsAdded.setTextId(QT_TRANSLATE_NOOP("MainWindow", "No accounts added!")); + ui->actionNoAccountsAdded->setEnabled(false); + accountMenu->addAction(ui->actionNoAccountsAdded); + ui->profileMenu->addAction(ui->actionNoAccountsAdded); + ui->all_actions.append(&ui->actionNoAccountsAdded); } else { @@ -1298,18 +1313,23 @@ void MainWindow::repopulateAccountsMenu() accountMenu->addSeparator(); ui->profileMenu->addSeparator(); - QAction *action = new QAction(tr("No Default Account"), this); - action->setCheckable(true); - action->setIcon(APPLICATION->getThemedIcon("noaccount")); - action->setData(-1); - action->setShortcut(QKeySequence(tr("Ctrl+0"))); + ui->all_actions.removeAll(&ui->actionNoDefaultAccount); + ui->actionNoDefaultAccount = TranslatedAction(this); + ui->actionNoDefaultAccount->setObjectName(QStringLiteral("actionNoDefaultAccount")); + ui->actionNoDefaultAccount.setTextId(QT_TRANSLATE_NOOP("MainWindow", "No Default Account")); + ui->actionNoDefaultAccount->setCheckable(true); + ui->actionNoDefaultAccount->setIcon(APPLICATION->getThemedIcon("noaccount")); + ui->actionNoDefaultAccount->setData(-1); + ui->actionNoDefaultAccount->setShortcut(QKeySequence(tr("Ctrl+0"))); if (!defaultAccount) { - action->setChecked(true); + ui->actionNoDefaultAccount->setChecked(true); } - accountMenu->addAction(action); - ui->profileMenu->addAction(action); - connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); + accountMenu->addAction(ui->actionNoDefaultAccount); + ui->profileMenu->addAction(ui->actionNoDefaultAccount); + connect(ui->actionNoDefaultAccount, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); + ui->all_actions.append(&ui->actionNoDefaultAccount); + ui->actionNoDefaultAccount.retranslate(); accountMenu->addSeparator(); ui->profileMenu->addSeparator(); diff --git a/launcher/ui/dialogs/SkinUploadDialog.cpp b/launcher/ui/dialogs/SkinUploadDialog.cpp index 6a5a324f..8d137afc 100644 --- a/launcher/ui/dialogs/SkinUploadDialog.cpp +++ b/launcher/ui/dialogs/SkinUploadDialog.cpp @@ -100,7 +100,8 @@ void SkinUploadDialog::on_buttonBox_accepted() void SkinUploadDialog::on_skinBrowseBtn_clicked() { - QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), "*.png"); + auto filter = QMimeDatabase().mimeTypeForName("image/png").filterString(); + QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), filter); if (raw_path.isEmpty() || !QFileInfo::exists(raw_path)) { return; diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index a48c4d69..b4562843 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -101,6 +101,20 @@ void InstanceSettingsPage::applySettings() { SettingsObject::Lock lock(m_settings); + // Miscellaneous + bool miscellaneous = ui->miscellaneousSettingsBox->isChecked(); + m_settings->set("OverrideMiscellaneous", miscellaneous); + if (miscellaneous) + { + m_settings->set("CloseAfterLaunch", ui->closeAfterLaunchCheck->isChecked()); + m_settings->set("QuitAfterGameStop", ui->quitAfterGameStopCheck->isChecked()); + } + else + { + m_settings->reset("CloseAfterLaunch"); + m_settings->reset("QuitAfterGameStop"); + } + // Console bool console = ui->consoleSettingsBox->isChecked(); m_settings->set("OverrideConsole", console); @@ -247,6 +261,11 @@ void InstanceSettingsPage::applySettings() void InstanceSettingsPage::loadSettings() { + // Miscellaneous + ui->miscellaneousSettingsBox->setChecked(m_settings->get("OverrideMiscellaneous").toBool()); + ui->closeAfterLaunchCheck->setChecked(m_settings->get("CloseAfterLaunch").toBool()); + ui->quitAfterGameStopCheck->setChecked(m_settings->get("QuitAfterGameStop").toBool()); + // Console ui->consoleSettingsBox->setChecked(m_settings->get("OverrideConsole").toBool()); ui->showConsoleCheck->setChecked(m_settings->get("ShowConsole").toBool()); diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index 5db2d147..cb66b3ce 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -350,6 +350,35 @@ </widget> </item> <item> + <widget class="QGroupBox" name="miscellaneousSettingsBox"> + <property name="title"> + <string>Miscellaneous</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_12"> + <item> + <widget class="QCheckBox" name="closeAfterLaunchCheck"> + <property name="text"> + <string>Close the launcher after game window opens</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="quitAfterGameStopCheck"> + <property name="text"> + <string>Quit the launcher after game window closes</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> <spacer name="verticalSpacerMinecraft_2"> <property name="orientation"> <enum>Qt::Vertical</enum> diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index e694ebe3..2cf17b32 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -251,7 +251,7 @@ ScreenshotsPage::ScreenshotsPage(QString path, QWidget *parent) m_model.reset(new QFileSystemModel()); m_filterModel.reset(new FilterModel()); m_filterModel->setSourceModel(m_model.get()); - m_model->setFilter(QDir::Files | QDir::Writable | QDir::Readable); + m_model->setFilter(QDir::Files); m_model->setReadOnly(false); m_model->setNameFilters({"*.png"}); m_model->setNameFilterDisables(false); @@ -343,6 +343,29 @@ void ScreenshotsPage::onItemActivated(QModelIndex index) DesktopServices::openFile(info.absoluteFilePath()); } +void ScreenshotsPage::onCurrentSelectionChanged(const QItemSelection &selected) +{ + bool allReadable = !selected.isEmpty(); + bool allWritable = !selected.isEmpty(); + + for (auto index : selected.indexes()) + { + if (!index.isValid()) + break; + auto info = m_model->fileInfo(index); + if (!info.isReadable()) + allReadable = false; + if (!info.isWritable()) + allWritable = false; + } + + ui->actionUpload->setEnabled(allReadable); + ui->actionCopy_Image->setEnabled(allReadable); + ui->actionCopy_File_s->setEnabled(allReadable); + ui->actionDelete->setEnabled(allWritable); + ui->actionRename->setEnabled(allWritable); +} + void ScreenshotsPage::on_actionView_Folder_triggered() { DesktopServices::openDirectory(m_folder, true); @@ -503,6 +526,8 @@ void ScreenshotsPage::openedImpl() if(idx.isValid()) { ui->listView->setModel(m_filterModel.get()); + connect(ui->listView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ScreenshotsPage::onCurrentSelectionChanged); + onCurrentSelectionChanged(ui->listView->selectionModel()->selection()); // set initial button enable states ui->listView->setRootIndex(m_filterModel->mapFromSource(idx)); } else diff --git a/launcher/ui/pages/instance/ScreenshotsPage.h b/launcher/ui/pages/instance/ScreenshotsPage.h index 50cf1a17..c22706af 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.h +++ b/launcher/ui/pages/instance/ScreenshotsPage.h @@ -100,6 +100,7 @@ private slots: void on_actionRename_triggered(); void on_actionView_Folder_triggered(); void onItemActivated(QModelIndex); + void onCurrentSelectionChanged(const QItemSelection &selected); void ShowContextMenu(const QPoint &pos); private: diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index 487bf77b..1b53dd40 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -143,7 +143,8 @@ void ImportPage::setUrl(const QString& url) void ImportPage::on_modpackBtn_clicked() { - const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), tr("Zip (*.zip)")); + auto filter = QMimeDatabase().mimeTypeForName("application/zip").filterString(); + const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), filter); if (url.isValid()) { if (url.isLocalFile()) diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index e82e1cdb..540ee2fd 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -21,7 +21,8 @@ auto ListModel::debugName() const -> QString void ListModel::fetchMore(const QModelIndex& parent) { - if (parent.isValid()) return; + if (parent.isValid()) + return; if (nextSearchOffset == 0) { qWarning() << "fetchMore with 0 offset is wrong..."; return; @@ -32,7 +33,9 @@ void ListModel::fetchMore(const QModelIndex& parent) auto ListModel::data(const QModelIndex& index, int role) const -> QVariant { int pos = index.row(); - if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } + if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { + return QString("INVALID INDEX %1").arg(pos); + } ModPlatform::IndexedPack pack = modpacks.at(pos); if (role == Qt::DisplayRole) { @@ -46,7 +49,9 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant } return pack.description; } else if (role == Qt::DecorationRole) { - if (m_logoMap.contains(pack.logoName)) { return (m_logoMap.value(pack.logoName)); } + if (m_logoMap.contains(pack.logoName)) { + return (m_logoMap.value(pack.logoName)); + } QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); return icon; @@ -63,16 +68,15 @@ void ListModel::requestModVersions(ModPlatform::IndexedPack const& current) { auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile(); - m_parent->apiProvider()->getVersions(this, - { current.addonId.toString(), getMineVersions(), profile->getModLoader() }); + m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoader() }); } void ListModel::performPaginatedSearch() { auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile(); - m_parent->apiProvider()->searchMods(this, - { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoader(), getMineVersions() }); + m_parent->apiProvider()->searchMods( + this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoader(), getMineVersions() }); } void ListModel::refresh() @@ -93,11 +97,9 @@ void ListModel::refresh() void ListModel::searchWithTerm(const QString& term, const int sort, const bool filter_changed) { - if (currentSearchTerm == term - && currentSearchTerm.isNull() == term.isNull() - && currentSort == sort - && !filter_changed) - { return; } + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort && !filter_changed) { + return; + } currentSearchTerm = term; currentSort = sort; @@ -118,7 +120,9 @@ void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallbac void ListModel::requestLogo(QString logo, QString url) { - if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { return; } + if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { + return; + } MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))); @@ -129,7 +133,9 @@ void ListModel::requestLogo(QString logo, QString url) QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { job->deleteLater(); emit logoLoaded(logo, QIcon(fullPath)); - if (waitingCallbacks.contains(logo)) { waitingCallbacks.value(logo)(fullPath); } + if (waitingCallbacks.contains(logo)) { + waitingCallbacks.value(logo)(fullPath); + } }); QObject::connect(job, &NetJob::failed, this, [this, logo, job] { @@ -148,7 +154,9 @@ void ListModel::logoLoaded(QString logo, QIcon out) m_loadingLogos.removeAll(logo); m_logoMap.insert(logo, out); for (int i = 0; i < modpacks.size(); i++) { - if (modpacks[i].logoName == logo) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } + if (modpacks[i].logoName == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); + } } } @@ -199,7 +207,9 @@ void ListModel::searchRequestFailed(QString reason) // 409 Gone, notify user to update QMessageBox::critical(nullptr, tr("Error"), //: %1 refers to the launcher itself - QString("%1 %2").arg(m_parent->displayName()).arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_NAME))); + QString("%1 %2") + .arg(m_parent->displayName()) + .arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_NAME))); } jobPtr.reset(); @@ -218,9 +228,12 @@ void ListModel::searchRequestFailed(QString reason) void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId) { auto& current = m_parent->getCurrent(); - if (addonId != current.addonId) { return; } + if (addonId != current.addonId) { + return; + } + + auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); - QJsonArray arr = doc.array(); try { loadIndexedPackVersions(current, arr); } catch (const JSONValidationError& e) { diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp index 905fb2dd..8de2e545 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp @@ -1,5 +1,5 @@ #include "FlameModModel.h" - +#include "Json.h" #include "modplatform/flame/FlameModIndex.h" namespace FlameMod { @@ -19,7 +19,7 @@ void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { - return obj.array(); + return Json::ensureArray(obj.object(), "data"); } } // namespace FlameMod diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index fe163cae..f97536e8 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -1,6 +1,6 @@ #include "FlameModel.h" -#include "Application.h" #include <Json.h> +#include "Application.h" #include <MMCStrings.h> #include <Version.h> @@ -9,61 +9,46 @@ namespace Flame { -ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) -{ -} +ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {} -ListModel::~ListModel() -{ -} +ListModel::~ListModel() {} -int ListModel::rowCount(const QModelIndex &parent) const +int ListModel::rowCount(const QModelIndex& parent) const { return modpacks.size(); } -int ListModel::columnCount(const QModelIndex &parent) const +int ListModel::columnCount(const QModelIndex& parent) const { return 1; } -QVariant ListModel::data(const QModelIndex &index, int role) const +QVariant ListModel::data(const QModelIndex& index, int role) const { int pos = index.row(); - if(pos >= modpacks.size() || pos < 0 || !index.isValid()) - { + if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } IndexedPack pack = modpacks.at(pos); - if(role == Qt::DisplayRole) - { + if (role == Qt::DisplayRole) { return pack.name; - } - else if (role == Qt::ToolTipRole) - { - if(pack.description.length() > 100) - { - //some magic to prevent to long tooltips and replace html linebreaks + } else if (role == Qt::ToolTipRole) { + if (pack.description.length() > 100) { + // some magic to prevent to long tooltips and replace html linebreaks QString edit = pack.description.left(97); edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("..."); return edit; - } return pack.description; - } - else if(role == Qt::DecorationRole) - { - if(m_logoMap.contains(pack.logoName)) - { + } else if (role == Qt::DecorationRole) { + if (m_logoMap.contains(pack.logoName)) { return (m_logoMap.value(pack.logoName)); } QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl); + ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); return icon; - } - else if(role == Qt::UserRole) - { + } else if (role == Qt::UserRole) { QVariant v; v.setValue(pack); return v; @@ -76,9 +61,9 @@ void ListModel::logoLoaded(QString logo, QIcon out) { m_loadingLogos.removeAll(logo); m_logoMap.insert(logo, out); - for(int i = 0; i < modpacks.size(); i++) { - if(modpacks[i].logoName == logo) { - emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + for (int i = 0; i < modpacks.size(); i++) { + if (modpacks[i].logoName == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } } } @@ -91,8 +76,7 @@ void ListModel::logoFailed(QString logo) void ListModel::requestLogo(QString logo, QString url) { - if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) - { + if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { return; } @@ -101,18 +85,15 @@ void ListModel::requestLogo(QString logo, QString url) job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] - { + QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { job->deleteLater(); emit logoLoaded(logo, QIcon(fullPath)); - if(waitingCallbacks.contains(logo)) - { + if (waitingCallbacks.contains(logo)) { waitingCallbacks.value(logo)(fullPath); } }); - QObject::connect(job, &NetJob::failed, this, [this, logo, job] - { + QObject::connect(job, &NetJob::failed, this, [this, logo, job] { job->deleteLater(); emit logoFailed(logo); }); @@ -122,19 +103,16 @@ void ListModel::requestLogo(QString logo, QString url) m_loadingLogos.append(logo); } -void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) +void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) { - if(m_logoMap.contains(logo)) - { + if (m_logoMap.contains(logo)) { callback(APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); - } - else - { + } else { requestLogo(logo, logoUrl); } } -Qt::ItemFlags ListModel::flags(const QModelIndex &index) const +Qt::ItemFlags ListModel::flags(const QModelIndex& index) const { return QAbstractListModel::flags(index); } @@ -148,7 +126,7 @@ void ListModel::fetchMore(const QModelIndex& parent) { if (parent.isValid()) return; - if(nextSearchOffset == 0) { + if (nextSearchOffset == 0) { qWarning() << "fetchMore with 0 offset is wrong..."; return; } @@ -157,17 +135,20 @@ void ListModel::fetchMore(const QModelIndex& parent) void ListModel::performPaginatedSearch() { - NetJob *netJob = new NetJob("Flame::Search", APPLICATION->network()); + NetJob* netJob = new NetJob("Flame::Search", APPLICATION->network()); auto searchUrl = QString( - "https://addons-ecs.forgesvc.net/api/v2/addon/search?" - "categoryId=0&" - "gameId=432&" - "index=%1&" - "pageSize=25&" - "searchFilter=%2&" - "sectionId=4471&" - "sort=%3" - ).arg(nextSearchOffset).arg(currentSearchTerm).arg(currentSort); + "https://api.curseforge.com/v1/mods/search?" + "gameId=432&" + "classId=4471&" + "index=%1&" + "pageSize=25&" + "searchFilter=%2&" + "sortField=%3&" + "sortOrder=desc") + .arg(nextSearchOffset) + .arg(currentSearchTerm) + .arg(currentSort + 1); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; jobPtr->start(); @@ -177,17 +158,16 @@ void ListModel::performPaginatedSearch() void ListModel::searchWithTerm(const QString& term, int sort) { - if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { return; } currentSearchTerm = term; currentSort = sort; - if(jobPtr) { + if (jobPtr) { jobPtr->abort(); searchState = ResetRequested; return; - } - else { + } else { beginResetModel(); modpacks.clear(); endResetModel(); @@ -203,30 +183,28 @@ void Flame::ListModel::searchRequestFinished() QJsonParseError parse_error; QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); - if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString(); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset + << " reason: " << parse_error.errorString(); qWarning() << response; return; } QList<Flame::IndexedPack> newList; - auto packs = doc.array(); - for(auto packRaw : packs) { + auto packs = Json::ensureArray(doc.object(), "data"); + for (auto packRaw : packs) { auto packObj = packRaw.toObject(); Flame::IndexedPack pack; - try - { + try { Flame::loadIndexedPack(pack, packObj); newList.append(pack); - } - catch(const JSONValidationError &e) - { + } catch (const JSONValidationError& e) { qWarning() << "Error while loading pack from CurseForge: " << e.cause(); continue; } } - if(packs.size() < 25) { + if (packs.size() < 25) { searchState = Finished; } else { nextSearchOffset += 25; @@ -241,7 +219,7 @@ void Flame::ListModel::searchRequestFailed(QString reason) { jobPtr.reset(); - if(searchState == ResetRequested) { + if (searchState == ResetRequested) { beginResetModel(); modpacks.clear(); endResetModel(); @@ -253,5 +231,4 @@ void Flame::ListModel::searchRequestFailed(QString reason) } } -} - +} // namespace Flame diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index c90294ce..ec774621 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -39,13 +39,12 @@ #include <QKeyEvent> #include "Application.h" +#include "FlameModel.h" +#include "InstanceImportTask.h" #include "Json.h" #include "ui/dialogs/NewInstanceDialog.h" -#include "InstanceImportTask.h" -#include "FlameModel.h" -FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget *parent) - : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog) +FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog) { ui->setupUi(this); connect(ui->searchButton, &QPushButton::clicked, this, &FlamePage::triggerSearch); @@ -112,10 +111,8 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) { ui->versionSelectionBox->clear(); - if(!first.isValid()) - { - if(isOpened) - { + if (!first.isValid()) { + if (isOpened) { dialog->setSuggestedPack(); } return; @@ -130,14 +127,14 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) else text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>"; if (!current.authors.empty()) { - auto authorToStr = [](Flame::ModpackAuthor & author) { - if(author.url.isEmpty()) { + auto authorToStr = [](Flame::ModpackAuthor& author) { + if (author.url.isEmpty()) { return author.name; } return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name); }; QStringList authorStrs; - for(auto & author: current.authors) { + for (auto& author : current.authors) { authorStrs.push_back(authorToStr(author)); } text += "<br>" + tr(" by ") + authorStrs.join(", "); @@ -146,53 +143,46 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) ui->packDescription->setHtml(text + current.description); - if (current.versionsLoaded == false) - { + if (current.versionsLoaded == false) { qDebug() << "Loading flame modpack versions"; auto netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network()); auto response = new QByteArray(); int addonId = current.addonId; - netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response)); + netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/files").arg(addonId), response)); - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] - { - if(addonId != current.addonId){ - return; //wrong request + QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] { + if (addonId != current.addonId) { + return; // wrong request } QJsonParseError parse_error; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString(); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset + << " reason: " << parse_error.errorString(); qWarning() << *response; return; } - QJsonArray arr = doc.array(); - try - { + auto arr = Json::ensureArray(doc.object(), "data"); + try { Flame::loadIndexedPackVersions(current, arr); - } - catch(const JSONValidationError &e) - { + } catch (const JSONValidationError& e) { qDebug() << *response; qWarning() << "Error while reading flame modpack version: " << e.cause(); } - for(auto version : current.versions) { + for (auto version : current.versions) { ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); } suggestCurrent(); }); - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] - { + QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); delete response; }); netJob->start(); - } - else - { - for(auto version : current.versions) { + } else { + for (auto version : current.versions) { ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); } @@ -202,13 +192,11 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) void FlamePage::suggestCurrent() { - if(!isOpened) - { + if (!isOpened) { return; } - if (selectedVersion.isEmpty()) - { + if (selectedVersion.isEmpty()) { dialog->setSuggestedPack(); return; } @@ -216,16 +204,13 @@ void FlamePage::suggestCurrent() dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion)); QString editedLogoName; editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0); - listModel->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo) - { - dialog->setSuggestedIconFromFile(logo, editedLogoName); - }); + listModel->getLogo(current.logoName, current.logoUrl, + [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); }); } void FlamePage::onVersionSelectionChanged(QString data) { - if(data.isNull() || data.isEmpty()) - { + if (data.isNull() || data.isEmpty()) { selectedVersion = ""; return; } |