diff --git a/.envrc b/.envrc index 3550a30f2..190b5b2b3 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,2 @@ use flake +watch_file nix/*.nix diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 08cfb56dd..c705ff7b0 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -20,7 +20,7 @@ jobs: if: github.repository_owner == 'PrismLauncher' && github.event.pull_request.merged == true && (github.event_name != 'labeled' || startsWith('backport', github.event.label.name)) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} - name: Create backport PRs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d044f4faf..059795a11 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -125,7 +125,7 @@ jobs: # PREPARE ## - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: 'true' @@ -164,7 +164,7 @@ jobs: - name: Retrieve ccache cache (Windows MinGW-w64) if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' - uses: actions/cache@v3.3.1 + uses: actions/cache@v3.3.2 with: path: '${{ github.workspace }}\.ccache' key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }} @@ -620,7 +620,7 @@ jobs: options: --privileged steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: inputs.build_type == 'Debug' with: submodules: 'true' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0cd1f6e40..a77b4ae1e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -8,7 +8,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: 'true' diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index 2a46ff5e7..bda75e354 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -26,7 +26,7 @@ jobs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: 'true' path: 'PrismLauncher-source' diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 6bad57030..6a16b0369 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -16,10 +16,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v22 + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@6a9a9e84a173d90b3ffb42c5ddaf9ea033fad011 # v23 - - uses: DeterminateSystems/update-flake-lock@v19 + - uses: DeterminateSystems/update-flake-lock@v20 with: commit-msg: "chore(nix): update lockfile" pr-title: "chore(nix): update lockfile" diff --git a/cmake/CompilerWarnings.cmake b/cmake/CompilerWarnings.cmake index 635e54289..69e57b4d5 100644 --- a/cmake/CompilerWarnings.cmake +++ b/cmake/CompilerWarnings.cmake @@ -75,7 +75,6 @@ function( set(CLANG_WARNINGS -Wall -Wextra # reasonable and standard - -Wextra-semi # Warn about semicolon after in-class function definition. -Wshadow # warn the user if a variable declaration shadows one from a parent context -Wnon-virtual-dtor # warn the user if a class with virtual functions has a non-virtual destructor. This helps # catch hard to track down memory errors diff --git a/flake.lock b/flake.lock index bfe151758..b1486ea69 100644 --- a/flake.lock +++ b/flake.lock @@ -21,11 +21,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1690933134, - "narHash": "sha256-ab989mN63fQZBFrkk4Q8bYxQCktuHmBIBqUG1jl6/FQ=", + "lastModified": 1693611461, + "narHash": "sha256-aPODl8vAgGQ0ZYFIRisxYG5MOGSkIczvu2Cd8Gb9+1Y=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "59cf3f1447cfc75087e7273b04b31e689a8599fb", + "rev": "7f53fdb7bdc5bb237da7fefef12d099e4fd611ca", "type": "github" }, "original": { @@ -91,11 +91,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1691853136, - "narHash": "sha256-wTzDsRV4HN8A2Sl0SVQY0q8ILs90CD43Ha//7gNZE+E=", + "lastModified": 1693626178, + "narHash": "sha256-Rpiy6lIOu4zny8tfGuIeN1ji9eSz9nPmm9yBhh/4IOM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "f0451844bbdf545f696f029d1448de4906c7f753", + "rev": "bfb7dfec93f3b5d7274db109f2990bc889861caf", "type": "github" }, "original": { @@ -108,11 +108,11 @@ "nixpkgs-lib": { "locked": { "dir": "lib", - "lastModified": 1690881714, - "narHash": "sha256-h/nXluEqdiQHs1oSgkOOWF+j8gcJMWhwnZ9PFabN6q0=", + "lastModified": 1693471703, + "narHash": "sha256-0l03ZBL8P1P6z8MaSDS/MvuU8E75rVxe5eE1N6gxeTo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9e1960bc196baf6881340d53dccb203a951745a2", + "rev": "3e52e76b70d5508f3cec70b882a29199f4d1ee85", "type": "github" }, "original": { @@ -138,11 +138,11 @@ ] }, "locked": { - "lastModified": 1691747570, - "narHash": "sha256-J3fnIwJtHVQ0tK2JMBv4oAmII+1mCdXdpeCxtIsrL2A=", + "lastModified": 1692274144, + "narHash": "sha256-BxTQuRUANQ81u8DJznQyPmRsg63t4Yc+0kcyq6OLz8s=", "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "c5ac3aa3324bd8aebe8622a3fc92eeb3975d317a", + "rev": "7e3517c03d46159fdbf8c0e5c97f82d5d4b0c8fa", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index c3148fe03..d45282aa6 100644 --- a/flake.nix +++ b/flake.nix @@ -23,5 +23,19 @@ outputs = inputs: inputs.flake-parts.lib.mkFlake {inherit inputs;} - {imports = [./nix];}; + { + imports = [ + inputs.pre-commit-hooks.flakeModule + + ./nix/dev.nix + ./nix/distribution.nix + ]; + + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + }; } diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 78de87470..66044d9ac 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -9,7 +9,6 @@ * Copyright (C) 2022 Tayou * Copyright (C) 2023 TheKodeToad * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> - * Copyright (C) 2023 seth * * 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 @@ -580,9 +579,6 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("IgnoreJavaCompatibility", false); m_settings->registerSetting("IgnoreJavaWizard", false); - // Mod loader settings - m_settings->registerSetting("DisableQuiltBeacon", false); - // Native library workarounds m_settings->registerSetting("UseNativeOpenAL", false); m_settings->registerSetting("CustomOpenALPath", ""); @@ -598,6 +594,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("ShowGameTime", true); m_settings->registerSetting("ShowGlobalGameTime", true); m_settings->registerSetting("RecordGameTime", true); + m_settings->registerSetting("ShowGameTimeWithoutDays", false); // Minecraft mods m_settings->registerSetting("ModMetadataDisabled", false); @@ -968,7 +965,7 @@ void Application::performMainStartupAction() qDebug() << " Launching with account" << m_profileToUse; } - launch(inst, true, false, nullptr, serverToJoin, accountToUse); + launch(inst, true, false, serverToJoin, accountToUse); return; } } @@ -1067,7 +1064,7 @@ void Application::messageReceived(const QByteArray& message) } } - launch(instance, true, false, nullptr, serverObject, accountObject); + launch(instance, true, false, serverObject, accountObject); } else { qWarning() << "Received invalid message" << message; } @@ -1108,7 +1105,6 @@ bool Application::openJsonEditor(const QString& filename) bool Application::launch(InstancePtr instance, bool online, bool demo, - BaseProfilerFactory* profiler, MinecraftServerTargetPtr serverToJoin, MinecraftAccountPtr accountToUse) { @@ -1116,7 +1112,7 @@ bool Application::launch(InstancePtr instance, qDebug() << "Cannot launch instances while an update is running. Please try again when updates are completed."; } else if (instance->canLaunch()) { auto& extras = m_instanceExtras[instance->id()]; - auto& window = extras.window; + auto window = extras.window; if (window) { if (!window->saveAll()) { return false; @@ -1127,7 +1123,7 @@ bool Application::launch(InstancePtr instance, controller->setInstance(instance); controller->setOnline(online); controller->setDemo(demo); - controller->setProfiler(profiler); + controller->setProfiler(profilers().value(instance->settings()->get("Profiler").toString(), nullptr).get()); controller->setServerToJoin(serverToJoin); controller->setAccountToUse(accountToUse); if (window) { diff --git a/launcher/Application.h b/launcher/Application.h index 29774b1c5..b227bb813 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -195,7 +195,6 @@ class Application : public QApplication { bool launch(InstancePtr instance, bool online = true, bool demo = false, - BaseProfilerFactory* profiler = nullptr, MinecraftServerTargetPtr serverToJoin = nullptr, MinecraftAccountPtr accountToUse = nullptr); bool kill(InstancePtr instance); diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 70c6da6b3..725036395 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -3,6 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2023 TheKodeToad * * 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 @@ -100,6 +101,8 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("ManagedPackName", ""); m_settings->registerSetting("ManagedPackVersionID", ""); m_settings->registerSetting("ManagedPackVersionName", ""); + + m_settings->registerSetting("Profiler", ""); } QString BaseInstance::getPreLaunchCommand() diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 38dc7c4aa..47fff5957 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -3,6 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2023 TheKodeToad * * 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 @@ -38,6 +39,7 @@ #include #include +#include #include #include #include @@ -246,6 +248,8 @@ class BaseInstance : public QObject, public std::enable_shared_from_this + * Copyright (C) 2023 TheKodeToad * * 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 @@ -361,22 +362,21 @@ void LaunchController::readyForLaunch() QString error; if (!m_profiler->check(&error)) { m_launcher->abort(); - QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't start profiler: %1").arg(error)); emitFailed("Profiler startup failed!"); + QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Profiler check for %1 failed: %2").arg(m_profiler->name(), error)); return; } BaseProfiler* profilerInstance = m_profiler->createProfiler(m_launcher->instance(), this); connect(profilerInstance, &BaseProfiler::readyToLaunch, [this](const QString& message) { - QMessageBox msg; + QMessageBox msg(m_parentWidget); msg.setText(tr("The game launch is delayed until you press the " "button. This is the right time to setup the profiler, as the " "profiler server is running now.\n\n%1") .arg(message)); msg.setWindowTitle(tr("Waiting.")); msg.setIcon(QMessageBox::Information); - msg.addButton(tr("Launch"), QMessageBox::AcceptRole); - msg.setModal(true); + msg.addButton(tr("&Launch"), QMessageBox::AcceptRole); msg.exec(); m_launcher->proceed(); }); diff --git a/launcher/MMCTime.cpp b/launcher/MMCTime.cpp index 3972dbd53..1765fd844 100644 --- a/launcher/MMCTime.cpp +++ b/launcher/MMCTime.cpp @@ -16,19 +16,20 @@ */ #include +#include #include #include #include -QString Time::prettifyDuration(int64_t duration) +QString Time::prettifyDuration(int64_t duration, bool noDays) { int seconds = (int)(duration % 60); duration /= 60; int minutes = (int)(duration % 60); duration /= 60; - int hours = (int)(duration % 24); - int days = (int)(duration / 24); + int hours = (int)(noDays ? duration : (duration % 24)); + int days = (int)(noDays ? 0 : (duration / 24)); if ((hours == 0) && (days == 0)) { return QObject::tr("%1min %2s").arg(minutes).arg(seconds); } diff --git a/launcher/MMCTime.h b/launcher/MMCTime.h index b7d34b5d8..ea6d37e7e 100644 --- a/launcher/MMCTime.h +++ b/launcher/MMCTime.h @@ -20,7 +20,7 @@ namespace Time { -QString prettifyDuration(int64_t duration); +QString prettifyDuration(int64_t duration, bool noDays = false); /** * @brief Returns a string with short form time duration ie. `2days 1h3m4s56.0ms`. diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h index ff658c431..c79600e7d 100644 --- a/launcher/NullInstance.h +++ b/launcher/NullInstance.h @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad * * 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 @@ -62,6 +63,7 @@ class NullInstance : public BaseInstance { bool canExport() const override { return false; } bool canEdit() const override { return false; } bool canLaunch() const override { return false; } + void populateLaunchMenu(QMenu* menu) override {} QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override { QStringList out; diff --git a/launcher/Version.h b/launcher/Version.h index 92bff9ba9..9c043b013 100644 --- a/launcher/Version.h +++ b/launcher/Version.h @@ -103,14 +103,8 @@ class Version { QString m_fullString; - [[nodiscard]] inline bool isAppendix() const - { - return m_stringPart.startsWith('+'); - } - [[nodiscard]] inline bool isPreRelease() const - { - return m_stringPart.startsWith('-') && m_stringPart.length() > 1; - } + [[nodiscard]] inline bool isAppendix() const { return m_stringPart.startsWith('+'); } + [[nodiscard]] inline bool isPreRelease() const { return m_stringPart.startsWith('-') && m_stringPart.length() > 1; } inline bool operator==(const Section& other) const { @@ -156,14 +150,8 @@ class Version { return m_fullString < other.m_fullString; } - inline bool operator!=(const Section& other) const - { - return !(*this == other); - } - inline bool operator>(const Section& other) const - { - return !(*this < other || *this == other); - } + inline bool operator!=(const Section& other) const { return !(*this == other); } + inline bool operator>(const Section& other) const { return !(*this < other || *this == other); } }; private: diff --git a/launcher/launch/LogModel.h b/launcher/launch/LogModel.h index ba2e4054a..18e51d7e3 100644 --- a/launcher/launch/LogModel.h +++ b/launcher/launch/LogModel.h @@ -30,7 +30,7 @@ class LogModel : public QAbstractListModel { enum Roles { LevelRole = Qt::UserRole }; - private /* types */: + private /* types */: struct entry { MessageLevel::Enum level; QString line; diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp index 0b85b81aa..bb838043a 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -2,14 +2,14 @@ #include "Component.h" #include "ComponentUpdateTask_p.h" -#include "OneSixVersionFormat.h" #include "PackProfile.h" #include "PackProfile_p.h" #include "Version.h" #include "cassert" #include "meta/Index.h" #include "meta/Version.h" -#include "meta/VersionList.h" +#include "minecraft/OneSixVersionFormat.h" +#include "minecraft/ProfileUtils.h" #include "net/Mode.h" #include "Application.h" diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 62d22019f..0e64c46d4 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -3,8 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 Jamie Mansfield - * Copyright (C) 2022 TheKodeToad - * Copyright (c) 2023 seth + * Copyright (C) 2023 TheKodeToad * * 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 @@ -88,6 +87,10 @@ #include "minecraft/gameoptions/GameOptions.h" #include "minecraft/update/FoldersTask.h" +#include "tools/BaseProfiler.h" + +#include + #ifdef Q_OS_LINUX #include "MangoHud.h" #endif @@ -181,10 +184,6 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerOverride(global_settings->getSetting("CloseAfterLaunch"), miscellaneousOverride); m_settings->registerOverride(global_settings->getSetting("QuitAfterGameStop"), miscellaneousOverride); - // Mod loader specific options - auto modLoaderSettings = m_settings->registerSetting("OverrideModLoaderSettings", false); - m_settings->registerOverride(global_settings->getSetting("DisableQuiltBeacon"), modLoaderSettings); - m_settings->set("InstanceType", "OneSix"); } @@ -196,6 +195,12 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerSetting("UseAccountForInstance", false); m_settings->registerSetting("InstanceAccountId", ""); + m_settings->registerSetting("ExportName", ""); + m_settings->registerSetting("ExportVersion", "1.0.0"); + m_settings->registerSetting("ExportSummary", ""); + m_settings->registerSetting("ExportAuthor", ""); + m_settings->registerSetting("ExportOptionalFiles", true); + qDebug() << "Instance-type specific settings were loaded!"; setSpecificSettingsLoaded(true); @@ -231,6 +236,50 @@ QSet MinecraftInstance::traits() const return profile->getTraits(); } +// FIXME: move UI code out of MinecraftInstance +void MinecraftInstance::populateLaunchMenu(QMenu* menu) +{ + QAction* normalLaunch = menu->addAction(tr("&Launch")); + normalLaunch->setShortcut(QKeySequence::Open); + QAction* normalLaunchOffline = menu->addAction(tr("Launch &Offline")); + normalLaunchOffline->setShortcut(QKeySequence(tr("Ctrl+Shift+O"))); + QAction* normalLaunchDemo = menu->addAction(tr("Launch &Demo")); + normalLaunchDemo->setShortcut(QKeySequence(tr("Ctrl+Alt+O"))); + + normalLaunchDemo->setEnabled(supportsDemo()); + + connect(normalLaunch, &QAction::triggered, [this] { APPLICATION->launch(shared_from_this()); }); + connect(normalLaunchOffline, &QAction::triggered, [this] { APPLICATION->launch(shared_from_this(), false, false); }); + connect(normalLaunchDemo, &QAction::triggered, [this] { APPLICATION->launch(shared_from_this(), false, true); }); + + QString profilersTitle = tr("Profilers"); + menu->addSeparator()->setText(profilersTitle); + + auto profilers = new QActionGroup(menu); + profilers->setExclusive(true); + connect(profilers, &QActionGroup::triggered, [this](QAction* action) { + settings()->set("Profiler", action->data()); + emit profilerChanged(); + }); + + QAction* noProfilerAction = menu->addAction(tr("&No Profiler")); + noProfilerAction->setData(""); + noProfilerAction->setCheckable(true); + noProfilerAction->setChecked(true); + profilers->addAction(noProfilerAction); + + for (auto profiler = APPLICATION->profilers().begin(); profiler != APPLICATION->profilers().end(); profiler++) { + QAction* profilerAction = menu->addAction(profiler.value()->name()); + profilers->addAction(profilerAction); + profilerAction->setData(profiler.key()); + profilerAction->setCheckable(true); + profilerAction->setChecked(settings()->get("Profiler").toString() == profiler.key()); + + QString error; + profilerAction->setEnabled(profiler.value()->check(&error)); + } +} + QString MinecraftInstance::gameRoot() const { QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft")); @@ -386,12 +435,6 @@ QStringList MinecraftInstance::extraArguments() list.append("-javaagent:" + jar[0] + (agent->argument().isEmpty() ? "" : "=" + agent->argument())); } - { - const auto loaders = version->getModLoaders(); - if (loaders.has_value() && loaders.value() & ResourceAPI::Quilt && settings()->get("DisableQuiltBeacon").toBool()) - list.append("-Dloader.disable_beacon=true"); - } - { QString openALPath; QString glfwPath; @@ -897,13 +940,16 @@ QString MinecraftInstance::getStatusbarDescription() if (m_settings->get("ShowGameTime").toBool()) { if (lastTimePlayed() > 0) { QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch()); - description.append(tr(", last played on %1 for %2") - .arg(QLocale().toString(lastLaunchTime, QLocale::ShortFormat)) - .arg(Time::prettifyDuration(lastTimePlayed()))); + description.append( + tr(", last played on %1 for %2") + .arg(QLocale().toString(lastLaunchTime, QLocale::ShortFormat)) + .arg(Time::prettifyDuration(lastTimePlayed(), APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool()))); } if (totalTimePlayed() > 0) { - description.append(tr(", total played for %1").arg(Time::prettifyDuration(totalTimePlayed()))); + description.append( + tr(", total played for %1") + .arg(Time::prettifyDuration(totalTimePlayed(), APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool()))); } } if (hasCrashed()) { diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index c331cc6f5..dabd44ba7 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -2,7 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (C) 2022 TheKodeToad + * Copyright (C) 2023 TheKodeToad * * 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 @@ -70,6 +70,8 @@ class MinecraftInstance : public BaseInstance { bool canExport() const override { return true; } + void populateLaunchMenu(QMenu* menu) override; + ////// Directories and files ////// QString jarModsDir() const; QString resourcePacksDir() const; diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index 92988808a..9e42c5dd6 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -58,15 +58,14 @@ #include "ComponentUpdateTask.h" #include "PackProfile.h" #include "PackProfile_p.h" +#include "minecraft/mod/Mod.h" +#include "modplatform/ModIndex.h" -#include "Application.h" -#include "modplatform/ResourceAPI.h" - -static const QMap modloaderMapping{ { "net.neoforged", ResourceAPI::NeoForge }, - { "net.minecraftforge", ResourceAPI::Forge }, - { "net.fabricmc.fabric-loader", ResourceAPI::Fabric }, - { "org.quiltmc.quilt-loader", ResourceAPI::Quilt }, - { "com.mumfrey.liteloader", ResourceAPI::LiteLoader } }; +static const QMap modloaderMapping{ { "net.neoforged", ModPlatform::NeoForge }, + { "net.minecraftforge", ModPlatform::Forge }, + { "net.fabricmc.fabric-loader", ModPlatform::Fabric }, + { "org.quiltmc.quilt-loader", ModPlatform::Quilt }, + { "com.mumfrey.liteloader", ModPlatform::LiteLoader } }; PackProfile::PackProfile(MinecraftInstance* instance) : QAbstractListModel() { @@ -990,12 +989,12 @@ void PackProfile::disableInteraction(bool disable) } } -std::optional PackProfile::getModLoaders() +std::optional PackProfile::getModLoaders() { - ResourceAPI::ModLoaderTypes result; + ModPlatform::ModLoaderTypes result; bool has_any_loader = false; - QMapIterator i(modloaderMapping); + QMapIterator i(modloaderMapping); while (i.hasNext()) { i.next(); @@ -1009,3 +1008,18 @@ std::optional PackProfile::getModLoaders() return {}; return result; } + +std::optional PackProfile::getSupportedModLoaders() +{ + auto loadersOpt = getModLoaders(); + if (!loadersOpt.has_value()) + return loadersOpt; + auto loaders = loadersOpt.value(); + // TODO: remove this or add version condition once Quilt drops official Fabric support + if (loaders & ModPlatform::Quilt) + loaders |= ModPlatform::Fabric; + // TODO: remove this or add version condition once NeoForge drops official Forge support + if (loaders & ModPlatform::NeoForge) + loaders |= ModPlatform::Forge; + return loaders; +} diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h index ce44fa588..e72b6ebfe 100644 --- a/launcher/minecraft/PackProfile.h +++ b/launcher/minecraft/PackProfile.h @@ -44,14 +44,11 @@ #include #include #include +#include -#include "BaseVersion.h" #include "Component.h" #include "LaunchProfile.h" -#include "Library.h" -#include "MojangDownloadInfo.h" -#include "ProfileUtils.h" -#include "modplatform/ResourceAPI.h" +#include "modplatform/ModIndex.h" #include "net/Mode.h" class MinecraftInstance; @@ -146,7 +143,9 @@ class PackProfile : public QAbstractListModel { // todo(merged): is this the best approach void appendComponent(ComponentPtr component); - std::optional getModLoaders(); + std::optional getModLoaders(); + // this returns aditional loaders(Quilt supports fabric and NeoForge supports Forge) + std::optional getSupportedModLoaders(); private: void scheduleSave(); diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index c534ea3bd..84dbd8416 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -415,7 +415,7 @@ Qt::ItemFlags AccountList::flags(const QModelIndex& index) const bool AccountList::setData(const QModelIndex& idx, const QVariant& value, int role) { - if (idx.row() < 0 || idx.row() >= rowCount(idx) || !idx.isValid()) { + if (idx.row() < 0 || idx.row() >= rowCount(idx.parent()) || !idx.isValid()) { return false; } @@ -423,7 +423,8 @@ bool AccountList::setData(const QModelIndex& idx, const QVariant& value, int rol if (value == Qt::Checked) { MinecraftAccountPtr account = at(idx.row()); setDefaultAccount(account); - } + } else if (m_defaultAccount == at(idx.row())) + setDefaultAccount(nullptr); } emit dataChanged(idx, index(idx.row(), columnCount(QModelIndex()) - 1)); diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index ae3dea8d8..310946379 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -132,17 +132,23 @@ auto Mod::destroy(QDir& index_dir, bool preserve_metadata, bool attempt_trash) - if (!preserve_metadata) { qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); - if (metadata()) { - Metadata::remove(index_dir, metadata()->slug); - } else { - auto n = name(); - Metadata::remove(index_dir, n); - } + destroyMetadata(index_dir); } return Resource::destroy(attempt_trash); } +void Mod::destroyMetadata(QDir& index_dir) +{ + if (metadata()) { + Metadata::remove(index_dir, metadata()->slug); + } else { + auto n = name(); + Metadata::remove(index_dir, n); + } + m_local_details.metadata = nullptr; +} + auto Mod::details() const -> const ModDetails& { return m_local_details; @@ -246,7 +252,8 @@ void Mod::setIcon(QImage new_image) const PixmapCache::remove(m_pack_image_cache_key.key); // scale the image to avoid flooding the pixmapcache - auto pixmap = QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding)); + auto pixmap = + QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); m_pack_image_cache_key.key = PixmapCache::insert(pixmap); m_pack_image_cache_key.was_ever_used = true; @@ -259,7 +266,7 @@ QPixmap Mod::icon(QSize size, Qt::AspectRatioMode mode) const if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { if (size.isNull()) return cached_image; - return cached_image.scaled(size, mode); + return cached_image.scaled(size, mode, Qt::SmoothTransformation); } // No valid image we can get diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 6dafecfc5..e97ee9d3b 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -93,6 +93,8 @@ class Mod : public Resource { // Delete all the files of this mod auto destroy(QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool; + // Delete the metadata only + void destroyMetadata(QDir& index_dir); void finishResolvingWithDetails(ModDetails&& details); diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 280e70d7b..a5f1489dd 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -51,8 +51,13 @@ #include "Application.h" +#include "Json.h" #include "minecraft/mod/tasks/LocalModParseTask.h" +#include "minecraft/mod/tasks/LocalModUpdateTask.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h" +#include "modplatform/ModIndex.h" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlameModIndex.h" ModFolderModel::ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir) : ResourceFolderModel(QDir(dir), instance, nullptr, create_dir), m_is_indexed(is_indexed) @@ -228,6 +233,25 @@ bool ModFolderModel::deleteMods(const QModelIndexList& indexes) return true; } +bool ModFolderModel::deleteModsMetadata(const QModelIndexList& indexes) +{ + if (indexes.isEmpty()) + return true; + + for (auto i : indexes) { + if (i.column() != 0) { + continue; + } + auto m = at(i.row()); + auto index_dir = indexDir(); + m->destroyMetadata(index_dir); + } + + update(); + + return true; +} + bool ModFolderModel::isValid() { return m_dir.exists() && m_dir.isReadable(); @@ -309,3 +333,47 @@ void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } + +static const FlameAPI flameAPI; +bool ModFolderModel::installMod(QString file_path, ModPlatform::IndexedVersion& vers) +{ + if (vers.addonId.isValid()) { + ModPlatform::IndexedPack pack{ + vers.addonId, + ModPlatform::ResourceProvider::FLAME, + }; + + QEventLoop loop; + + auto response = std::make_shared(); + auto job = flameAPI.getProject(vers.addonId.toString(), response); + + QObject::connect(job.get(), &Task::failed, [&loop] { loop.quit(); }); + QObject::connect(job.get(), &Task::aborted, &loop, &QEventLoop::quit); + QObject::connect(job.get(), &Task::succeeded, [response, this, &vers, &loop, &pack] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qDebug() << *response; + return; + } + try { + auto obj = Json::requireObject(Json::requireObject(doc), "data"); + FlameMod::loadIndexedPack(pack, obj); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading mod info: " << e.cause(); + } + LocalModUpdateTask update_metadata(indexDir(), pack, vers); + QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); + update_metadata.start(); + }); + + job->start(); + + loop.exec(); + } + return ResourceFolderModel::installResource(file_path); +} diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 06fd78149..61d840f9b 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -48,6 +48,7 @@ #include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h" +#include "modplatform/ModIndex.h" class LegacyInstance; class BaseInstance; @@ -75,10 +76,12 @@ class ModFolderModel : public ResourceFolderModel { [[nodiscard]] Task* createParseTask(Resource&) override; bool installMod(QString file_path) { return ResourceFolderModel::installResource(file_path); } + bool installMod(QString file_path, ModPlatform::IndexedVersion& vers); bool uninstallMod(const QString& filename, bool preserve_metadata = false); /// Deletes all the selected mods bool deleteMods(const QModelIndexList& indexes); + bool deleteModsMetadata(const QModelIndexList& indexes); bool isValid(); diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp index dab0f6d67..2bb51dc5b 100644 --- a/launcher/minecraft/mod/ResourcePack.cpp +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -50,7 +50,8 @@ void ResourcePack::setImage(QImage new_image) const PixmapCache::instance().remove(m_pack_image_cache_key.key); // scale the image to avoid flooding the pixmapcache - auto pixmap = QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding)); + auto pixmap = + QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); m_pack_image_cache_key.key = PixmapCache::instance().insert(pixmap); m_pack_image_cache_key.was_ever_used = true; @@ -68,7 +69,7 @@ QPixmap ResourcePack::image(QSize size, Qt::AspectRatioMode mode) const if (PixmapCache::instance().find(m_pack_image_cache_key.key, &cached_image)) { if (size.isNull()) return cached_image; - return cached_image.scaled(size, mode); + return cached_image.scaled(size, mode, Qt::SmoothTransformation); } // No valid image we can get diff --git a/launcher/minecraft/mod/TexturePack.cpp b/launcher/minecraft/mod/TexturePack.cpp index 7d8c67137..04cc36310 100644 --- a/launcher/minecraft/mod/TexturePack.cpp +++ b/launcher/minecraft/mod/TexturePack.cpp @@ -44,7 +44,8 @@ void TexturePack::setImage(QImage new_image) const PixmapCache::remove(m_pack_image_cache_key.key); // scale the image to avoid flooding the pixmapcache - auto pixmap = QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding)); + auto pixmap = + QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); m_pack_image_cache_key.key = PixmapCache::insert(pixmap); m_pack_image_cache_key.was_ever_used = true; @@ -56,7 +57,7 @@ QPixmap TexturePack::image(QSize size, Qt::AspectRatioMode mode) const if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { if (size.isNull()) return cached_image; - return cached_image.scaled(size, mode); + return cached_image.scaled(size, mode, Qt::SmoothTransformation); } // No valid image we can get diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp index 0a0f57bf3..ee84b1f5e 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp @@ -39,9 +39,9 @@ static Version mcVersion(BaseInstance* inst) return static_cast(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion(); } -static ResourceAPI::ModLoaderTypes mcLoaders(BaseInstance* inst) +static ModPlatform::ModLoaderTypes mcLoaders(BaseInstance* inst) { - return static_cast(inst)->getPackProfile()->getModLoaders().value(); + return static_cast(inst)->getPackProfile()->getSupportedModLoaders().value(); } GetModDependenciesTask::GetModDependenciesTask(QObject* parent, @@ -75,7 +75,7 @@ void GetModDependenciesTask::prepare() ModPlatform::Dependency GetModDependenciesTask::getOverride(const ModPlatform::Dependency& dep, const ModPlatform::ResourceProvider providerName) { - if (auto isQuilt = m_loaderType & ResourceAPI::Quilt; isQuilt || m_loaderType & ResourceAPI::Fabric) { + if (auto isQuilt = m_loaderType & ModPlatform::Quilt; isQuilt || m_loaderType & ModPlatform::Fabric) { auto overide = ModPlatform::getOverrideDeps(); auto over = std::find_if(overide.cbegin(), overide.cend(), [dep, providerName, isQuilt](auto o) { return o.provider == providerName && dep.addonId == (isQuilt ? o.fabric : o.quilt); @@ -191,7 +191,7 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen } pDep->version = provider.mod->loadDependencyVersions(dep, arr); if (!pDep->version.addonId.isValid()) { - if (m_loaderType & ResourceAPI::Quilt) { // falback for quilt + if (m_loaderType & ModPlatform::Quilt) { // falback for quilt auto overide = ModPlatform::getOverrideDeps(); auto over = std::find_if(overide.cbegin(), overide.cend(), [dep, provider](auto o) { return o.provider == provider.name && dep.addonId == o.quilt; }); @@ -201,6 +201,7 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen return; } } + removePack(dep.addonId); qWarning() << "Error while reading mod version empty "; qDebug() << doc; return; diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.h b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h index 50eba6afc..a8b9953d3 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.h +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h @@ -80,5 +80,5 @@ class GetModDependenciesTask : public SequentialTask { Provider m_modrinth_provider; Version m_version; - ResourceAPI::ModLoaderTypes m_loaderType; + ModPlatform::ModLoaderTypes m_loaderType; }; diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h index 6d968ea48..d125a5879 100644 --- a/launcher/modplatform/CheckUpdateTask.h +++ b/launcher/modplatform/CheckUpdateTask.h @@ -14,7 +14,7 @@ class CheckUpdateTask : public Task { public: CheckUpdateTask(QList& mods, std::list& mcVersions, - std::optional loaders, + std::optional loaders, std::shared_ptr mods_folder) : Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder){}; @@ -53,7 +53,7 @@ class CheckUpdateTask : public Task { protected: QList& m_mods; std::list& m_game_versions; - std::optional m_loaders; + std::optional m_loaders; std::shared_ptr m_mods_folder; std::vector m_updatable; diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index 350a9f10b..e8e4a38dd 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -83,4 +83,25 @@ QString getMetaURL(ResourceProvider provider, QVariant projectID) projectID.toString(); } +auto getModLoaderString(ModLoaderType type) -> const QString +{ + switch (type) { + case NeoForge: + return "neoforge"; + case Forge: + return "forge"; + case Cauldron: + return "cauldron"; + case LiteLoader: + return "liteloader"; + case Fabric: + return "fabric"; + case Quilt: + return "quilt"; + default: + break; + } + return ""; +} + } // namespace ModPlatform diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 7036241db..232c22ec9 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -30,6 +30,9 @@ class QIODevice; namespace ModPlatform { +enum ModLoaderType { NeoForge = 1 << 0, Forge = 1 << 1, Cauldron = 1 << 2, LiteLoader = 1 << 3, Fabric = 1 << 4, Quilt = 1 << 5 }; +Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) + enum class ResourceProvider { MODRINTH, FLAME }; enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK }; @@ -70,7 +73,7 @@ struct IndexedVersion { QString downloadUrl; QString date; QString fileName; - QStringList loaders = {}; + ModLoaderTypes loaders = {}; QString hash_type; QString hash; bool is_preferred = true; @@ -148,6 +151,14 @@ inline auto getOverrideDeps() -> QList QString getMetaURL(ResourceProvider provider, QVariant projectID); +auto getModLoaderString(ModLoaderType type) -> const QString; + +constexpr bool hasSingleModLoaderSelected(ModLoaderTypes l) noexcept +{ + auto x = static_cast(l); + return x && !(x & (x - 1)); +} + } // namespace ModPlatform Q_DECLARE_METATYPE(ModPlatform::IndexedPack) diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index f6ccb426d..3b1959384 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -54,9 +54,6 @@ class ResourceAPI { public: virtual ~ResourceAPI() = default; - enum ModLoaderType { NeoForge = 1 << 0, Forge = 1 << 1, Cauldron = 1 << 2, LiteLoader = 1 << 3, Fabric = 1 << 4, Quilt = 1 << 5 }; - Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) - struct SortingMethod { // The index of the sorting method. Used to allow for arbitrary ordering in the list of methods. // Used by Flame in the API request. @@ -74,7 +71,7 @@ class ResourceAPI { std::optional search; std::optional sorting; - std::optional loaders; + std::optional loaders; std::optional > versions; }; struct SearchCallbacks { @@ -87,7 +84,7 @@ class ResourceAPI { ModPlatform::IndexedPack pack; std::optional > mcVersions; - std::optional loaders; + std::optional loaders; VersionSearchArgs(VersionSearchArgs const&) = default; void operator=(VersionSearchArgs other) @@ -108,13 +105,15 @@ class ResourceAPI { void operator=(ProjectInfoArgs other) { pack = other.pack; } }; struct ProjectInfoCallbacks { - std::function on_succeed; + std::function on_succeed; + std::function on_fail; + std::function on_abort; }; struct DependencySearchArgs { ModPlatform::Dependency dependency; Version mcVersion; - ModLoaderTypes loader; + ModPlatform::ModLoaderTypes loader; }; struct DependencySearchCallbacks { @@ -161,27 +160,6 @@ class ResourceAPI { return nullptr; } - static auto getModLoaderString(ModLoaderType type) -> const QString - { - switch (type) { - case NeoForge: - return "neoforge"; - case Forge: - return "forge"; - case Cauldron: - return "cauldron"; - case LiteLoader: - return "liteloader"; - case Fabric: - return "fabric"; - case Quilt: - return "quilt"; - default: - break; - } - return ""; - } - protected: [[nodiscard]] inline QString debugName() const { return "External resource API"; } diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 860d7340f..5865bee91 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -1,6 +1,7 @@ #include "FileResolvingTask.h" #include "Json.h" +#include "modplatform/ModIndex.h" #include "net/ApiDownload.h" #include "net/ApiUpload.h" #include "net/Upload.h" @@ -102,7 +103,7 @@ void Flame::FileResolvingTask::netJobFinished() auto url = QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha1").arg(hash); auto output = std::make_shared(); auto dl = Net::ApiDownload::makeByteArray(QUrl(url), output); - QObject::connect(dl.get(), &Net::Download::succeeded, [&out]() { out.resolved = true; }); + QObject::connect(dl.get(), &Net::ApiDownload::succeeded, [&out]() { out.resolved = true; }); m_checkJob->addNetAction(dl); blockedProjects.insert(&out, output); @@ -153,7 +154,7 @@ void Flame::FileResolvingTask::modrinthCheckFinished() // If there's more than one mod loader for this version, we can't know for sure // which file is relative to each loader, so it's best to not use any one and // let the user download it manually. - if (file.loaders.size() <= 1) { + if (!file.loaders || hasSingleModLoaderSelected(file.loaders)) { out->url = file.downloadUrl; qDebug() << "Found alternative on modrinth " << out->fileName; } else { @@ -175,7 +176,7 @@ void Flame::FileResolvingTask::modrinthCheckFinished() auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(projectId); auto dl = Net::ApiDownload::makeByteArray(url, output); qDebug() << "Fetching url slug for file:" << mod->fileName; - QObject::connect(dl.get(), &Net::Download::succeeded, [block, index, output]() { + QObject::connect(dl.get(), &Net::ApiDownload::succeeded, [block, index, output]() { auto mod = block->at(index); // use the shared_ptr so it is captured and only freed when we are done auto json = QJsonDocument::fromJson(*output); auto base = diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index 74d7db975..e99ce3a56 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -6,7 +6,6 @@ #include "FlameModIndex.h" #include "Application.h" -#include "BuildConfig.h" #include "Json.h" #include "net/ApiDownload.h" #include "net/ApiUpload.h" @@ -131,19 +130,13 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe auto obj = Json::requireObject(doc); auto arr = Json::requireArray(obj, "data"); - QJsonObject latest_file_obj; - ModPlatform::IndexedVersion ver_tmp; - for (auto file : arr) { auto file_obj = Json::requireObject(file); auto file_tmp = FlameMod::loadIndexedPackVersion(file_obj); - if (file_tmp.date > ver_tmp.date) { - ver_tmp = file_tmp; - latest_file_obj = file_obj; - } + if (file_tmp.date > ver.date && (!args.loaders.has_value() || !file_tmp.loaders || args.loaders.value() & file_tmp.loaders)) + ver = file_tmp; } - ver = FlameMod::loadIndexedPackVersion(latest_file_obj); } catch (Json::JsonException& e) { qCritical() << "Failed to parse response from a version request."; qCritical() << e.what(); diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index a1256e175..47350c33e 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -24,7 +24,10 @@ class FlameAPI : public NetworkResourceAPI { [[nodiscard]] auto getSortingMethods() const -> QList override; - static inline auto validateModLoaders(ModLoaderTypes loaders) -> bool { return loaders & (NeoForge | Forge | Fabric | Quilt); } + static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool + { + return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt); + } private: static int getClassId(ModPlatform::ResourceType type) @@ -38,21 +41,42 @@ class FlameAPI : public NetworkResourceAPI { } } - static int getMappedModLoader(ModLoaderTypes loaders) + static int getMappedModLoader(ModPlatform::ModLoaderType loaders) { // https://docs.curseforge.com/?http#tocS_ModLoaderType - if (loaders & Forge) - return 1; - if (loaders & Fabric) - return 4; - // TODO: remove this once Quilt drops official Fabric support - if (loaders & Quilt) // NOTE: Most if not all Fabric mods should work *currently* - return 4; // FIXME: implement multiple loaders filter - if (loaders & NeoForge) - return 6; + switch (loaders) { + case ModPlatform::Forge: + return 1; + case ModPlatform::Cauldron: + return 2; + case ModPlatform::LiteLoader: + return 3; + case ModPlatform::Fabric: + return 4; + case ModPlatform::Quilt: + return 5; + case ModPlatform::NeoForge: + return 6; + } return 0; } + static auto getModLoaderStrings(const ModPlatform::ModLoaderTypes types) -> const QStringList + { + QStringList l; + for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt }) { + if (types & loader) { + l << QString::number(getMappedModLoader(loader)); + } + } + return l; + } + + static auto getModLoaderFilters(ModPlatform::ModLoaderTypes types) -> const QString + { + return "[" + getModLoaderStrings(types).join(',') + "]"; + } + private: [[nodiscard]] std::optional getSearchURL(SearchArgs const& args) const override { @@ -69,7 +93,7 @@ class FlameAPI : public NetworkResourceAPI { get_arguments.append(QString("sortField=%1").arg(args.sorting.value().index)); get_arguments.append("sortOrder=desc"); if (args.loaders.has_value()) - get_arguments.append(QString("modLoaderType=%1").arg(getMappedModLoader(args.loaders.value()))); + get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(args.loaders.value()))); get_arguments.append(gameVersionStr); return "https://api.curseforge.com/v1/mods/search?gameId=432&" + get_arguments.join('&'); @@ -83,47 +107,27 @@ class FlameAPI : public NetworkResourceAPI { [[nodiscard]] std::optional getVersionsURL(VersionSearchArgs const& args) const override { auto addonId = args.pack.addonId.toString(); - QString url{ QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&").arg(addonId) }; + QString url = QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000").arg(addonId); - QStringList get_parameters; if (args.mcVersions.has_value()) - get_parameters.append(QString("gameVersion=%1").arg(args.mcVersions.value().front().toString())); + url += QString("&gameVersion=%1").arg(args.mcVersions.value().front().toString()); - if (args.loaders.has_value()) { - int mappedModLoader = getMappedModLoader(args.loaders.value()); - - if (args.loaders.value() & Quilt) { - auto overide = ModPlatform::getOverrideDeps(); - auto over = std::find_if(overide.cbegin(), overide.cend(), [addonId](auto dep) { - return dep.provider == ModPlatform::ResourceProvider::FLAME && addonId == dep.quilt; - }); - if (over != overide.cend()) { - mappedModLoader = 5; - } - } - - get_parameters.append(QString("modLoaderType=%1").arg(mappedModLoader)); + if (args.loaders.has_value() && ModPlatform::hasSingleModLoaderSelected(args.loaders.value())) { + int mappedModLoader = getMappedModLoader(static_cast(static_cast(args.loaders.value()))); + url += QString("&modLoaderType=%1").arg(mappedModLoader); } - - return url + get_parameters.join('&'); + return url; }; [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override { - auto mappedModLoader = getMappedModLoader(args.loader); auto addonId = args.dependency.addonId.toString(); - if (args.loader & Quilt) { - auto overide = ModPlatform::getOverrideDeps(); - auto over = std::find_if(overide.cbegin(), overide.cend(), [addonId](auto dep) { - return dep.provider == ModPlatform::ResourceProvider::FLAME && addonId == dep.quilt; - }); - if (over != overide.cend()) { - mappedModLoader = 5; - } + auto url = + QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&gameVersion=%2").arg(addonId, args.mcVersion.toString()); + if (args.loader && ModPlatform::hasSingleModLoaderSelected(args.loader)) { + int mappedModLoader = getMappedModLoader(static_cast(static_cast(args.loader))); + url += QString("&modLoaderType=%1").arg(mappedModLoader); } - return QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&gameVersion=%2&modLoaderType=%3") - .arg(addonId) - .arg(args.mcVersion.toString()) - .arg(mappedModLoader); + return url; }; }; diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index e10fedc3c..476a4667a 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -5,13 +5,11 @@ #include #include -#include "FileSystem.h" #include "Json.h" #include "ResourceDownloadTask.h" #include "minecraft/mod/ModFolderModel.h" -#include "minecraft/mod/ResourceFolderModel.h" #include "net/ApiDownload.h" diff --git a/launcher/modplatform/flame/FlameCheckUpdate.h b/launcher/modplatform/flame/FlameCheckUpdate.h index e3465d7e2..05c619a70 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.h +++ b/launcher/modplatform/flame/FlameCheckUpdate.h @@ -10,7 +10,7 @@ class FlameCheckUpdate : public CheckUpdateTask { public: FlameCheckUpdate(QList& mods, std::list& mcVersions, - std::optional loaders, + std::optional loaders, std::shared_ptr mods_folder) : CheckUpdateTask(mods, mcVersions, loaders, mods_folder) {} diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 19803cf6d..494dc2a7e 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -81,6 +81,7 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QVector unsortedVersions; auto profile = (dynamic_cast(inst))->getPackProfile(); QString mcVersion = profile->getComponentVersion("net.minecraft"); + auto loaders = profile->getSupportedModLoaders(); for (auto versionIter : arr) { auto obj = versionIter.toObject(); @@ -89,7 +90,8 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, if (!file.addonId.isValid()) file.addonId = pack.addonId; - if (file.fileId.isValid()) // Heuristic to check if the returned value is valid + if (file.fileId.isValid() && + (!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid unsortedVersions.append(file); } @@ -115,6 +117,19 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> if (str.contains('.')) file.mcVersion.append(str); + auto loader = str.toLower(); + if (loader == "neoforge") + file.loaders |= ModPlatform::NeoForge; + if (loader == "forge") + file.loaders |= ModPlatform::Forge; + if (loader == "cauldron") + file.loaders |= ModPlatform::Cauldron; + if (loader == "liteloader") + file.loaders |= ModPlatform::LiteLoader; + if (loader == "fabric") + file.loaders |= ModPlatform::Fabric; + if (loader == "quilt") + file.loaders |= ModPlatform::Quilt; } file.addonId = Json::requireInteger(obj, "modId"); @@ -173,8 +188,11 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> return file; } -ModPlatform::IndexedVersion FlameMod::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) +ModPlatform::IndexedVersion FlameMod::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) { + auto profile = (dynamic_cast(inst))->getPackProfile(); + QString mcVersion = profile->getComponentVersion("net.minecraft"); + auto loaders = profile->getSupportedModLoaders(); QVector versions; for (auto versionIter : arr) { auto obj = versionIter.toObject(); @@ -183,7 +201,8 @@ ModPlatform::IndexedVersion FlameMod::loadDependencyVersions(const ModPlatform:: if (!file.addonId.isValid()) file.addonId = m.addonId; - if (file.fileId.isValid()) // Heuristic to check if the returned value is valid + if (file.fileId.isValid() && + (!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid versions.append(file); } @@ -192,5 +211,7 @@ ModPlatform::IndexedVersion FlameMod::loadDependencyVersions(const ModPlatform:: return a.date > b.date; }; std::sort(versions.begin(), versions.end(), orderSortPredicate); - return versions.front(); + if (versions.size() != 0) + return versions.front(); + return {}; } diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index aa0d6f812..1bcaa44ba 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -19,5 +19,5 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, const shared_qobject_ptr& network, const BaseInstance* inst); auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion; -auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion; +auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) -> ModPlatform::IndexedVersion; } // namespace FlameMod \ No newline at end of file diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp index 0863f0b2b..d86d34bf4 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.cpp +++ b/launcher/modplatform/flame/FlamePackExportTask.cpp @@ -43,12 +43,14 @@ const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" }); FlamePackExportTask::FlamePackExportTask(const QString& name, const QString& version, const QString& author, + bool optionalFiles, InstancePtr instance, const QString& output, MMCZip::FilterFunction filter) : name(name) , version(version) , author(author) + , optionalFiles(optionalFiles) , instance(instance) , mcInstance(dynamic_cast(instance.get())) , gameRoot(instance->gameRoot()) @@ -410,7 +412,7 @@ QByteArray FlamePackExportTask::generateIndex() QJsonObject file; file["projectID"] = mod.addonId; file["fileID"] = mod.version; - file["required"] = mod.enabled; + file["required"] = mod.enabled || !optionalFiles; files << file; } obj["files"] = files; diff --git a/launcher/modplatform/flame/FlamePackExportTask.h b/launcher/modplatform/flame/FlamePackExportTask.h index d3dc6281e..78b46e91f 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.h +++ b/launcher/modplatform/flame/FlamePackExportTask.h @@ -30,6 +30,7 @@ class FlamePackExportTask : public Task { FlamePackExportTask(const QString& name, const QString& version, const QString& author, + bool optionalFiles, InstancePtr instance, const QString& output, MMCZip::FilterFunction filter); @@ -44,6 +45,7 @@ class FlamePackExportTask : public Task { // inputs const QString name, version, author; + const bool optionalFiles; const InstancePtr instance; MinecraftInstance* mcInstance; const QDir gameRoot; diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.cpp b/launcher/modplatform/helpers/NetworkResourceAPI.cpp index 46b966620..dccccdc21 100644 --- a/launcher/modplatform/helpers/NetworkResourceAPI.cpp +++ b/launcher/modplatform/helpers/NetworkResourceAPI.cpp @@ -72,7 +72,8 @@ Task::Ptr NetworkResourceAPI::getProjectInfo(ProjectInfoArgs&& args, ProjectInfo callbacks.on_succeed(doc, args.pack); }); - + QObject::connect(job.get(), &NetJob::failed, [callbacks](QString reason) { callbacks.on_fail(reason); }); + QObject::connect(job.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); }); return job; } @@ -131,7 +132,7 @@ Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args, auto netJob = makeShared(QString("%1::Dependency").arg(args.dependency.addonId.toString()), APPLICATION->network()); auto response = std::make_shared(); - netJob->addNetAction(Net::Download::makeByteArray(versions_url, response)); + netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response)); QObject::connect(netJob.get(), &NetJob::succeeded, [=] { QJsonParseError parse_error{}; diff --git a/launcher/modplatform/import_ftb/PackHelpers.cpp b/launcher/modplatform/import_ftb/PackHelpers.cpp index 118bdd157..ecf973452 100644 --- a/launcher/modplatform/import_ftb/PackHelpers.cpp +++ b/launcher/modplatform/import_ftb/PackHelpers.cpp @@ -60,19 +60,19 @@ Modpack parseDirectory(QString path) auto name = Json::requireString(obj, "name", "name"); auto version = Json::requireString(obj, "version", "version"); if (name == "neoforge") { - modpack.loaderType = ResourceAPI::NeoForge; + modpack.loaderType = ModPlatform::NeoForge; modpack.version = version; break; } else if (name == "forge") { - modpack.loaderType = ResourceAPI::Forge; + modpack.loaderType = ModPlatform::Forge; modpack.version = version; break; } else if (name == "fabric") { - modpack.loaderType = ResourceAPI::Fabric; + modpack.loaderType = ModPlatform::Fabric; modpack.version = version; break; } else if (name == "quilt") { - modpack.loaderType = ResourceAPI::Quilt; + modpack.loaderType = ModPlatform::Quilt; modpack.version = version; break; } diff --git a/launcher/modplatform/import_ftb/PackHelpers.h b/launcher/modplatform/import_ftb/PackHelpers.h index 8ea4f3faf..5400252b6 100644 --- a/launcher/modplatform/import_ftb/PackHelpers.h +++ b/launcher/modplatform/import_ftb/PackHelpers.h @@ -39,7 +39,7 @@ struct Modpack { // not needed for instance creation QVariant jvmArgs; - std::optional loaderType; + std::optional loaderType; QString loaderVersion; QIcon icon; diff --git a/launcher/modplatform/import_ftb/PackInstallTask.cpp b/launcher/modplatform/import_ftb/PackInstallTask.cpp index 9e4decb0c..9a3b2595b 100644 --- a/launcher/modplatform/import_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/import_ftb/PackInstallTask.cpp @@ -68,25 +68,25 @@ void PackInstallTask::copySettings() auto modloader = m_pack.loaderType; if (modloader.has_value()) switch (modloader.value()) { - case ResourceAPI::NeoForge: { + case ModPlatform::NeoForge: { components->setComponentVersion("net.neoforged", m_pack.version, true); break; } - case ResourceAPI::Forge: { + case ModPlatform::Forge: { components->setComponentVersion("net.minecraftforge", m_pack.version, true); break; } - case ResourceAPI::Fabric: { + case ModPlatform::Fabric: { components->setComponentVersion("net.fabricmc.fabric-loader", m_pack.version, true); break; } - case ResourceAPI::Quilt: { + case ModPlatform::Quilt: { components->setComponentVersion("org.quiltmc.quilt-loader", m_pack.version, true); break; } - case ResourceAPI::Cauldron: + case ModPlatform::Cauldron: break; - case ResourceAPI::LiteLoader: + case ModPlatform::LiteLoader: break; } components->saveNow(); diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp index 466c5b102..f453f5cb9 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.cpp +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -41,7 +41,7 @@ Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_f Task::Ptr ModrinthAPI::latestVersion(QString hash, QString hash_format, std::optional> mcVersions, - std::optional loaders, + std::optional loaders, std::shared_ptr response) { auto netJob = makeShared(QString("Modrinth::GetLatestVersion"), APPLICATION->network()); @@ -71,7 +71,7 @@ Task::Ptr ModrinthAPI::latestVersion(QString hash, Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes, QString hash_format, std::optional> mcVersions, - std::optional loaders, + std::optional loaders, std::shared_ptr response) { auto netJob = makeShared(QString("Modrinth::GetLatestVersions"), APPLICATION->network()); diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 0f150e97a..d0f0811b2 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -19,13 +19,13 @@ class ModrinthAPI : public NetworkResourceAPI { auto latestVersion(QString hash, QString hash_format, std::optional> mcVersions, - std::optional loaders, + std::optional loaders, std::shared_ptr response) -> Task::Ptr; auto latestVersions(const QStringList& hashes, QString hash_format, std::optional> mcVersions, - std::optional loaders, + std::optional loaders, std::shared_ptr response) -> Task::Ptr; Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; @@ -35,20 +35,19 @@ class ModrinthAPI : public NetworkResourceAPI { inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; - static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList + static auto getModLoaderStrings(const ModPlatform::ModLoaderTypes types) -> const QStringList { QStringList l; - for (auto loader : { NeoForge, Forge, Fabric, Quilt, LiteLoader }) { + for (auto loader : + { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader }) { if (types & loader) { l << getModLoaderString(loader); } } - if ((types & Quilt) && (~types & Fabric)) // Add Fabric if Quilt is in use, if Fabric isn't already there - l << getModLoaderString(Fabric); return l; } - static auto getModLoaderFilters(ModLoaderTypes types) -> const QString + static auto getModLoaderFilters(ModPlatform::ModLoaderTypes types) -> const QString { QStringList l; for (auto loader : getModLoaderStrings(types)) { @@ -141,9 +140,9 @@ class ModrinthAPI : public NetworkResourceAPI { return s.isEmpty() ? QString() : s; } - static inline auto validateModLoaders(ModLoaderTypes loaders) -> bool + static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool { - return loaders & (NeoForge | Forge | Fabric | Quilt | LiteLoader); + return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt | ModPlatform::LiteLoader); } [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index bff8fa2fe..c65f4fa80 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -11,7 +11,6 @@ #include "tasks/ConcurrentTask.h" #include "minecraft/mod/ModFolderModel.h" -#include "minecraft/mod/ResourceFolderModel.h" static ModrinthAPI api; static ModPlatform::ProviderCapabilities ProviderCaps; @@ -111,11 +110,11 @@ void ModrinthCheckUpdate::executeTask() // so we may want to filter it QString loader_filter; if (m_loaders.has_value()) { - static auto flags = { ResourceAPI::ModLoaderType::NeoForge, ResourceAPI::ModLoaderType::Forge, - ResourceAPI::ModLoaderType::Fabric, ResourceAPI::ModLoaderType::Quilt }; + 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 = api.getModLoaderString(flag); + loader_filter = ModPlatform::getModLoaderString(flag); break; } } diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h index 4583dd6ce..f2f2c7e92 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h @@ -10,7 +10,7 @@ class ModrinthCheckUpdate : public CheckUpdateTask { public: ModrinthCheckUpdate(QList& mods, std::list& mcVersions, - std::optional loaders, + std::optional loaders, std::shared_ptr mods_folder) : CheckUpdateTask(mods, mcVersions, loaders, mods_folder) {} diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp index 72f0bd6c4..e9e8a3b75 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp @@ -34,12 +34,14 @@ const QStringList ModrinthPackExportTask::FILE_EXTENSIONS({ "jar", "litemod", "z ModrinthPackExportTask::ModrinthPackExportTask(const QString& name, const QString& version, const QString& summary, + bool optionalFiles, InstancePtr instance, const QString& output, MMCZip::FilterFunction filter) : name(name) , version(version) , summary(summary) + , optionalFiles(optionalFiles) , instance(instance) , mcInstance(dynamic_cast(instance.get())) , gameRoot(instance->gameRoot()) @@ -272,11 +274,11 @@ QByteArray ModrinthPackExportTask::generateIndex() QString path = iterator.key(); const ResolvedFile& value = iterator.value(); + QJsonObject env; + // detect disabled mod const QFileInfo pathInfo(path); - - QJsonObject env; - if (pathInfo.suffix() == "disabled") { + if (optionalFiles && pathInfo.suffix() == "disabled") { // rename it path = pathInfo.dir().filePath(pathInfo.completeBaseName()); env["client"] = "optional"; diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.h b/launcher/modplatform/modrinth/ModrinthPackExportTask.h index 388b733e5..33c42e817 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.h +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.h @@ -31,6 +31,7 @@ class ModrinthPackExportTask : public Task { ModrinthPackExportTask(const QString& name, const QString& version, const QString& summary, + bool optionalFiles, InstancePtr instance, const QString& output, MMCZip::FilterFunction filter); @@ -51,6 +52,7 @@ class ModrinthPackExportTask : public Task { // inputs const QString name, version, summary; + const bool optionalFiles; const InstancePtr instance; MinecraftInstance* mcInstance; const QDir gameRoot; diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 9cc59ac39..2e16147c0 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -109,19 +109,19 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob pack.extraDataLoaded = true; } -void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, - QJsonArray& arr, - [[maybe_unused]] const shared_qobject_ptr& network, - const BaseInstance* inst) +void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const BaseInstance* inst) { QVector unsortedVersions; - QString mcVersion = (static_cast(inst))->getPackProfile()->getComponentVersion("net.minecraft"); + auto profile = (dynamic_cast(inst))->getPackProfile(); + QString mcVersion = profile->getComponentVersion("net.minecraft"); + auto loaders = profile->getSupportedModLoaders(); for (auto versionIter : arr) { auto obj = versionIter.toObject(); auto file = loadIndexedPackVersion(obj); - if (file.fileId.isValid()) // Heuristic to check if the returned value is valid + if (file.fileId.isValid() && + (!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid unsortedVersions.append(file); } auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { @@ -150,7 +150,18 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t } auto loaders = Json::requireArray(obj, "loaders"); for (auto loader : loaders) { - file.loaders.append(loader.toString()); + if (loader == "neoforge") + file.loaders |= ModPlatform::NeoForge; + if (loader == "forge") + file.loaders |= ModPlatform::Forge; + if (loader == "cauldron") + file.loaders |= ModPlatform::Cauldron; + if (loader == "liteloader") + file.loaders |= ModPlatform::LiteLoader; + if (loader == "fabric") + file.loaders |= ModPlatform::Fabric; + if (loader == "quilt") + file.loaders |= ModPlatform::Quilt; } file.version = Json::requireString(obj, "name"); file.version_number = Json::requireString(obj, "version_number"); @@ -234,15 +245,20 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t return {}; } -auto Modrinth::loadDependencyVersions([[maybe_unused]] const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion +auto Modrinth::loadDependencyVersions([[maybe_unused]] const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) + -> ModPlatform::IndexedVersion { - QVector versions; + auto profile = (dynamic_cast(inst))->getPackProfile(); + QString mcVersion = profile->getComponentVersion("net.minecraft"); + auto loaders = profile->getSupportedModLoaders(); + QVector versions; for (auto versionIter : arr) { auto obj = versionIter.toObject(); auto file = loadIndexedPackVersion(obj); - if (file.fileId.isValid()) // Heuristic to check if the returned value is valid + if (file.fileId.isValid() && + (!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid versions.append(file); } auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index 58a0f227c..93f91eec2 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -26,11 +26,8 @@ namespace Modrinth { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj); -void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, - QJsonArray& arr, - const shared_qobject_ptr& network, - const BaseInstance* inst); +void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const BaseInstance* inst); auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion; -auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion; +auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) -> ModPlatform::IndexedVersion; } // namespace Modrinth diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp index 7809d40fc..f37bc0bf8 100644 --- a/launcher/net/HttpMetaCache.cpp +++ b/launcher/net/HttpMetaCache.cpp @@ -218,9 +218,24 @@ void HttpMetaCache::Load() if (!index.open(QIODevice::ReadOnly)) return; - QJsonDocument json = QJsonDocument::fromJson(index.readAll()); + QJsonParseError parseError; + QJsonDocument json = QJsonDocument::fromJson(index.readAll(), &parseError); - auto root = Json::requireObject(json, "HttpMetaCache root"); + // Fail if the JSON is invalid. + if (parseError.error != QJsonParseError::NoError) { + qCritical() << QString("Failed to parse HttpMetaCache file: %1 at offset %2") + .arg(parseError.errorString(), QString::number(parseError.offset)) + .toUtf8(); + return; + } + + // Make sure the root is an object. + if (!json.isObject()) { + qCritical() << "HttpMetaCache root should be an object."; + return; + } + + auto root = json.object(); // check file version first auto version_val = Json::ensureString(root, "version"); diff --git a/launcher/ui/InstanceWindow.cpp b/launcher/ui/InstanceWindow.cpp index 7025cb795..bf83a56c9 100644 --- a/launcher/ui/InstanceWindow.cpp +++ b/launcher/ui/InstanceWindow.cpp @@ -44,8 +44,6 @@ #include #include -#include "ui/dialogs/CustomMessageBox.h" -#include "ui/dialogs/ProgressDialog.h" #include "ui/widgets/PageContainer.h" #include "InstancePageProvider.h" @@ -76,40 +74,44 @@ InstanceWindow::InstanceWindow(InstancePtr instance, QWidget* parent) : QMainWin // Add custom buttons to the page container layout. { - auto horizontalLayout = new QHBoxLayout(); + auto horizontalLayout = new QHBoxLayout(this); horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); horizontalLayout->setContentsMargins(6, -1, 6, -1); - auto btnHelp = new QPushButton(); + auto btnHelp = new QPushButton(this); btnHelp->setText(tr("Help")); horizontalLayout->addWidget(btnHelp); - connect(btnHelp, SIGNAL(clicked(bool)), m_container, SLOT(help())); + connect(btnHelp, &QPushButton::clicked, m_container, &PageContainer::help); auto spacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); horizontalLayout->addSpacerItem(spacer); - m_killButton = new QPushButton(); + m_launchButton = new QToolButton(this); + m_launchButton->setText(tr("&Launch")); + m_launchButton->setToolTip(tr("Launch the instance")); + m_launchButton->setPopupMode(QToolButton::MenuButtonPopup); + m_launchButton->setMinimumWidth(80); // HACK!! + horizontalLayout->addWidget(m_launchButton); + connect(m_launchButton, &QPushButton::clicked, this, [this] { APPLICATION->launch(m_instance); }); + + m_killButton = new QPushButton(this); + m_killButton->setText(tr("&Kill")); + m_killButton->setToolTip(tr("Kill the running instance")); + m_killButton->setShortcut(QKeySequence(tr("Ctrl+K"))); horizontalLayout->addWidget(m_killButton); - connect(m_killButton, SIGNAL(clicked(bool)), SLOT(on_btnKillMinecraft_clicked())); + connect(m_killButton, &QPushButton::clicked, this, [this] { APPLICATION->kill(m_instance); }); - m_launchOfflineButton = new QPushButton(); - horizontalLayout->addWidget(m_launchOfflineButton); - m_launchOfflineButton->setText(tr("Launch Offline")); + updateButtons(); - m_launchDemoButton = new QPushButton(); - horizontalLayout->addWidget(m_launchDemoButton); - m_launchDemoButton->setText(tr("Launch Demo")); - - updateLaunchButtons(); - connect(m_launchOfflineButton, SIGNAL(clicked(bool)), SLOT(on_btnLaunchMinecraftOffline_clicked())); - connect(m_launchDemoButton, SIGNAL(clicked(bool)), SLOT(on_btnLaunchMinecraftDemo_clicked())); - - m_closeButton = new QPushButton(); + m_closeButton = new QPushButton(this); m_closeButton->setText(tr("Close")); horizontalLayout->addWidget(m_closeButton); - connect(m_closeButton, SIGNAL(clicked(bool)), SLOT(on_closeButton_clicked())); + connect(m_closeButton, &QPushButton::clicked, this, &QMainWindow::close); m_container->addButtons(horizontalLayout); + + connect(m_instance.get(), &BaseInstance::profilerChanged, this, &InstanceWindow::updateButtons); + connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceWindow::updateButtons); } // restore window state @@ -149,47 +151,18 @@ void InstanceWindow::on_instanceStatusChanged(BaseInstance::Status, BaseInstance } } -void InstanceWindow::updateLaunchButtons() +void InstanceWindow::updateButtons() { - if (m_instance->isRunning()) { - m_launchOfflineButton->setEnabled(false); - m_launchDemoButton->setEnabled(false); - m_killButton->setText(tr("Kill")); - m_killButton->setObjectName("killButton"); - m_killButton->setToolTip(tr("Kill the running instance")); - } else if (!m_instance->canLaunch()) { - m_launchOfflineButton->setEnabled(false); - m_launchDemoButton->setEnabled(false); - m_killButton->setText(tr("Launch")); - m_killButton->setObjectName("launchButton"); - m_killButton->setToolTip(tr("Launch the instance")); - m_killButton->setEnabled(false); - } else { - m_launchOfflineButton->setEnabled(true); + m_launchButton->setEnabled(m_instance->canLaunch()); + m_killButton->setEnabled(m_instance->isRunning()); - // Disable demo-mode if not available. - auto instance = dynamic_cast(m_instance.get()); - if (instance) { - m_launchDemoButton->setEnabled(instance->supportsDemo()); - } - - m_killButton->setText(tr("Launch")); - m_killButton->setObjectName("launchButton"); - m_killButton->setToolTip(tr("Launch the instance")); - } - // NOTE: this is a hack to force the button to recalculate its style - m_killButton->setStyleSheet("/* */"); - m_killButton->setStyleSheet(QString()); -} - -void InstanceWindow::on_btnLaunchMinecraftOffline_clicked() -{ - APPLICATION->launch(m_instance, false, false, nullptr); -} - -void InstanceWindow::on_btnLaunchMinecraftDemo_clicked() -{ - APPLICATION->launch(m_instance, false, true, nullptr); + QMenu* launchMenu = m_launchButton->menu(); + if (launchMenu) + launchMenu->clear(); + else + launchMenu = new QMenu(this); + m_instance->populateLaunchMenu(launchMenu); + m_launchButton->setMenu(launchMenu); } void InstanceWindow::instanceLaunchTaskChanged(shared_qobject_ptr proc) @@ -199,18 +172,13 @@ void InstanceWindow::instanceLaunchTaskChanged(shared_qobject_ptr pr void InstanceWindow::runningStateChanged(bool running) { - updateLaunchButtons(); + updateButtons(); m_container->refreshContainer(); if (running) { selectPage("log"); } } -void InstanceWindow::on_closeButton_clicked() -{ - close(); -} - void InstanceWindow::closeEvent(QCloseEvent* event) { bool proceed = true; @@ -233,15 +201,6 @@ bool InstanceWindow::saveAll() return m_container->saveAll(); } -void InstanceWindow::on_btnKillMinecraft_clicked() -{ - if (m_instance->isRunning()) { - APPLICATION->kill(m_instance); - } else { - APPLICATION->launch(m_instance, true, false, nullptr); - } -} - QString InstanceWindow::instanceId() { return m_instance->id(); @@ -252,17 +211,15 @@ bool InstanceWindow::selectPage(QString pageId) return m_container->selectPage(pageId); } -BasePage* InstanceWindow::selectedPage() const -{ - return m_container->selectedPage(); -} - void InstanceWindow::refreshContainer() { m_container->refreshContainer(); } -InstanceWindow::~InstanceWindow() {} +BasePage* InstanceWindow::selectedPage() const +{ + return m_container->selectedPage(); +} bool InstanceWindow::requestClose() { diff --git a/launcher/ui/InstanceWindow.h b/launcher/ui/InstanceWindow.h index 70f206f28..e5bc24d44 100644 --- a/launcher/ui/InstanceWindow.h +++ b/launcher/ui/InstanceWindow.h @@ -38,6 +38,7 @@ #include #include +#include #include "LaunchController.h" #include "launch/LaunchTask.h" @@ -53,7 +54,7 @@ class InstanceWindow : public QMainWindow, public BasePageContainer { public: explicit InstanceWindow(InstancePtr proc, QWidget* parent = 0); - virtual ~InstanceWindow(); + virtual ~InstanceWindow() = default; bool selectPage(QString pageId) override; BasePage* selectedPage() const override; @@ -71,11 +72,6 @@ class InstanceWindow : public QMainWindow, public BasePageContainer { void isClosing(); private slots: - void on_closeButton_clicked(); - void on_btnKillMinecraft_clicked(); - void on_btnLaunchMinecraftOffline_clicked(); - void on_btnLaunchMinecraftDemo_clicked(); - void instanceLaunchTaskChanged(shared_qobject_ptr proc); void runningStateChanged(bool running); void on_instanceStatusChanged(BaseInstance::Status, BaseInstance::Status newStatus); @@ -84,7 +80,7 @@ class InstanceWindow : public QMainWindow, public BasePageContainer { void closeEvent(QCloseEvent*) override; private: - void updateLaunchButtons(); + void updateButtons(); private: shared_qobject_ptr m_proc; @@ -92,7 +88,6 @@ class InstanceWindow : public QMainWindow, public BasePageContainer { bool m_doNotSave = false; PageContainer* m_container = nullptr; QPushButton* m_closeButton = nullptr; + QToolButton* m_launchButton = nullptr; QPushButton* m_killButton = nullptr; - QPushButton* m_launchOfflineButton = nullptr; - QPushButton* m_launchDemoButton = nullptr; }; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 067108f2d..5e55a5abb 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -43,7 +43,6 @@ #include "FileSystem.h" #include "MainWindow.h" -#include "ui/dialogs/ExportToModListDialog.h" #include "ui_MainWindow.h" #include @@ -90,17 +89,14 @@ #include #include #include -#include "InstancePageProvider.h" #include "InstanceWindow.h" -#include "JavaCommon.h" -#include "LaunchController.h" #include "ui/dialogs/AboutDialog.h" #include "ui/dialogs/CopyInstanceDialog.h" #include "ui/dialogs/CustomMessageBox.h" -#include "ui/dialogs/EditAccountDialog.h" #include "ui/dialogs/ExportInstanceDialog.h" #include "ui/dialogs/ExportPackDialog.h" +#include "ui/dialogs/ExportToModListDialog.h" #include "ui/dialogs/IconPickerDialog.h" #include "ui/dialogs/ImportResourceDialog.h" #include "ui/dialogs/NewInstanceDialog.h" @@ -113,17 +109,22 @@ #include "ui/themes/ThemeManager.h" #include "ui/widgets/LabeledToolButton.h" +#include "minecraft/PackProfile.h" +#include "minecraft/VersionFile.h" #include "minecraft/WorldList.h" #include "minecraft/mod/ModFolderModel.h" +#include "minecraft/mod/ResourcePackFolderModel.h" #include "minecraft/mod/ShaderPackFolderModel.h" +#include "minecraft/mod/TexturePackFolderModel.h" #include "minecraft/mod/tasks/LocalResourceParse.h" +#include "modplatform/ModIndex.h" #include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlameModIndex.h" #include "KonamiCode.h" #include "InstanceCopyTask.h" -#include "InstanceImportTask.h" #include "Json.h" @@ -553,71 +554,15 @@ void MainWindow::updateMainToolBar() ui->mainToolBar->setVisible(ui->menuBar->isNativeMenuBar() || !APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()); } -void MainWindow::updateToolsMenu() +void MainWindow::updateLaunchButton() { - bool currentInstanceRunning = m_selectedInstance && m_selectedInstance->isRunning(); - - ui->actionLaunchInstance->setDisabled(!m_selectedInstance || currentInstanceRunning); - ui->actionLaunchInstanceOffline->setDisabled(!m_selectedInstance || currentInstanceRunning); - ui->actionLaunchInstanceDemo->setDisabled(!m_selectedInstance || currentInstanceRunning); - QMenu* launchMenu = ui->actionLaunchInstance->menu(); - if (launchMenu) { + if (launchMenu) launchMenu->clear(); - } else { + else launchMenu = new QMenu(this); - } - QAction* normalLaunch = launchMenu->addAction(tr("Launch")); - normalLaunch->setShortcut(QKeySequence::Open); - QAction* normalLaunchOffline = launchMenu->addAction(tr("Launch Offline")); - normalLaunchOffline->setShortcut(QKeySequence(tr("Ctrl+Shift+O"))); - QAction* normalLaunchDemo = launchMenu->addAction(tr("Launch Demo")); - normalLaunchDemo->setShortcut(QKeySequence(tr("Ctrl+Alt+O"))); - if (m_selectedInstance) { - normalLaunch->setEnabled(m_selectedInstance->canLaunch()); - normalLaunchOffline->setEnabled(m_selectedInstance->canLaunch()); - normalLaunchDemo->setEnabled(m_selectedInstance->canLaunch()); - - connect(normalLaunch, &QAction::triggered, [this]() { APPLICATION->launch(m_selectedInstance, true, false); }); - connect(normalLaunchOffline, &QAction::triggered, [this]() { APPLICATION->launch(m_selectedInstance, false, false); }); - connect(normalLaunchDemo, &QAction::triggered, [this]() { APPLICATION->launch(m_selectedInstance, false, true); }); - } else { - normalLaunch->setDisabled(true); - normalLaunchOffline->setDisabled(true); - normalLaunchDemo->setDisabled(true); - } - - // Disable demo-mode if not available. - auto instance = dynamic_cast(m_selectedInstance.get()); - if (instance) { - normalLaunchDemo->setEnabled(instance->supportsDemo()); - } - - QString profilersTitle = tr("Profilers"); - launchMenu->addSeparator()->setText(profilersTitle); - for (auto profiler : APPLICATION->profilers().values()) { - QAction* profilerAction = launchMenu->addAction(profiler->name()); - QAction* profilerOfflineAction = launchMenu->addAction(tr("%1 Offline").arg(profiler->name())); - QString error; - if (!profiler->check(&error)) { - profilerAction->setDisabled(true); - profilerOfflineAction->setDisabled(true); - QString profilerToolTip = tr("Profiler not setup correctly. Go into settings, \"External Tools\"."); - profilerAction->setToolTip(profilerToolTip); - profilerOfflineAction->setToolTip(profilerToolTip); - } else if (m_selectedInstance) { - profilerAction->setEnabled(m_selectedInstance->canLaunch()); - profilerOfflineAction->setEnabled(m_selectedInstance->canLaunch()); - - connect(profilerAction, &QAction::triggered, - [this, profiler]() { APPLICATION->launch(m_selectedInstance, true, false, profiler.get()); }); - connect(profilerOfflineAction, &QAction::triggered, - [this, profiler]() { APPLICATION->launch(m_selectedInstance, false, false, profiler.get()); }); - } else { - profilerAction->setDisabled(true); - profilerOfflineAction->setDisabled(true); - } - } + if (m_selectedInstance) + m_selectedInstance->populateLaunchMenu(launchMenu); ui->actionLaunchInstance->setMenu(launchMenu); } @@ -927,7 +872,7 @@ void MainWindow::finalizeInstance(InstancePtr inst) } else { CustomMessageBox::selectable(this, tr("Error"), tr("The launcher cannot download Minecraft or update instances unless you have at least " - "one account added.\nPlease add your Mojang or Minecraft account."), + "one account added.\nPlease add your Microsoft or Mojang account."), QMessageBox::Warning) ->show(); } @@ -980,6 +925,7 @@ void MainWindow::processURLs(QList urls) if (url.scheme().isEmpty()) url.setScheme("file"); + ModPlatform::IndexedVersion version; QMap extra_info; QUrl local_url; if (!url.isLocalFile()) { // download the remote resource and identify @@ -989,6 +935,11 @@ void MainWindow::processURLs(QList urls) // format of url curseforge://install?addonId=IDHERE&fileId=IDHERE QUrlQuery query(url); + if (query.allQueryItemValues("addonId").isEmpty() || query.allQueryItemValues("fileId").isEmpty()) { + qDebug() << "Invalid curseforge link:" << url; + continue; + } + auto addonId = query.allQueryItemValues("addonId")[0]; auto fileId = query.allQueryItemValues("fileId")[0]; @@ -1000,20 +951,19 @@ void MainWindow::processURLs(QList urls) auto api = FlameAPI(); auto job = api.getFile(addonId, fileId, array); - QString resource_name; - connect(job.get(), &Task::failed, this, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); - connect(job.get(), &Task::succeeded, this, [this, array, addonId, fileId, &dl_url, &resource_name] { + connect(job.get(), &Task::succeeded, this, [this, array, addonId, fileId, &dl_url, &version] { qDebug() << "Returned CFURL Json:\n" << array->toStdString().c_str(); auto doc = Json::requireDocument(*array); auto data = Json::ensureObject(Json::ensureObject(doc.object()), "data"); // No way to find out if it's a mod or a modpack before here // And also we need to check if it ends with .zip, instead of any better way - auto fileName = Json::ensureString(data, "fileName"); + version = FlameMod::loadIndexedPackVersion(data); + auto fileName = version.fileName; // Have to use ensureString then use QUrl to get proper url encoding - dl_url = QUrl(Json::ensureString(data, "downloadUrl", "", "downloadUrl")); + dl_url = QUrl(version.downloadUrl); if (!dl_url.isValid()) { CustomMessageBox::selectable( this, tr("Error"), @@ -1024,7 +974,6 @@ void MainWindow::processURLs(QList urls) } QFileInfo dl_file(dl_url.fileName()); - resource_name = Json::ensureString(data, "displayName", dl_file.completeBaseName(), "displayName"); }); { // drop stack @@ -1099,7 +1048,7 @@ void MainWindow::processURLs(QList urls) qWarning() << "Importing of Data Packs not supported at this time. Ignoring" << localFileName; break; case PackedResourceType::Mod: - minecraftInst->loaderModList()->installMod(localFileName); + minecraftInst->loaderModList()->installMod(localFileName, version); break; case PackedResourceType::ShaderPack: minecraftInst->shaderPackList()->installResource(localFileName); @@ -1278,7 +1227,7 @@ void MainWindow::globalSettingsClosed() proxymodel->invalidate(); proxymodel->sort(0); updateMainToolBar(); - updateToolsMenu(); + updateLaunchButton(); updateThemeMenu(); updateStatusCenter(); // This needs to be done to prevent UI elements disappearing in the event the config is changed @@ -1408,10 +1357,11 @@ void MainWindow::on_actionDeleteInstance_triggered() if (APPLICATION->instances()->trashInstance(id)) { ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); - return; + } else { + APPLICATION->instances()->deleteInstance(id); } - - APPLICATION->instances()->deleteInstance(id); + APPLICATION->settings()->set("SelectedInstance", QString()); + selectionBad(); } void MainWindow::on_actionExportInstanceZip_triggered() @@ -1513,20 +1463,6 @@ void MainWindow::activateInstance(InstancePtr instance) APPLICATION->launch(instance); } -void MainWindow::on_actionLaunchInstanceOffline_triggered() -{ - if (m_selectedInstance) { - APPLICATION->launch(m_selectedInstance, false); - } -} - -void MainWindow::on_actionLaunchInstanceDemo_triggered() -{ - if (m_selectedInstance) { - APPLICATION->launch(m_selectedInstance, false, true); - } -} - void MainWindow::on_actionKillInstance_triggered() { if (m_selectedInstance && m_selectedInstance->isRunning()) { @@ -1700,6 +1636,7 @@ void MainWindow::instanceChanged(const QModelIndex& current, [[maybe_unused]] co } if (m_selectedInstance) { disconnect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance); + disconnect(m_selectedInstance.get(), &BaseInstance::profilerChanged, this, &MainWindow::refreshCurrentInstance); } QString id = current.data(InstanceList::InstanceIDRole).toString(); m_selectedInstance = APPLICATION->instances()->getInstanceById(id); @@ -1707,14 +1644,6 @@ void MainWindow::instanceChanged(const QModelIndex& current, [[maybe_unused]] co ui->instanceToolBar->setEnabled(true); setInstanceActionsEnabled(true); ui->actionLaunchInstance->setEnabled(m_selectedInstance->canLaunch()); - ui->actionLaunchInstanceOffline->setEnabled(m_selectedInstance->canLaunch()); - ui->actionLaunchInstanceDemo->setEnabled(m_selectedInstance->canLaunch()); - - // Disable demo-mode if not available. - auto instance = dynamic_cast(m_selectedInstance.get()); - if (instance) { - ui->actionLaunchInstanceDemo->setEnabled(instance->supportsDemo()); - } ui->actionKillInstance->setEnabled(m_selectedInstance->isRunning()); ui->actionExportInstance->setEnabled(m_selectedInstance->canExport()); @@ -1723,18 +1652,13 @@ void MainWindow::instanceChanged(const QModelIndex& current, [[maybe_unused]] co updateStatusCenter(); updateInstanceToolIcon(m_selectedInstance->iconKey()); - updateToolsMenu(); + updateLaunchButton(); APPLICATION->settings()->set("SelectedInstance", m_selectedInstance->id()); connect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance); + connect(m_selectedInstance.get(), &BaseInstance::profilerChanged, this, &MainWindow::refreshCurrentInstance); } else { - ui->instanceToolBar->setEnabled(false); - setInstanceActionsEnabled(false); - ui->actionLaunchInstance->setEnabled(false); - ui->actionLaunchInstanceOffline->setEnabled(false); - ui->actionLaunchInstanceDemo->setEnabled(false); - ui->actionKillInstance->setEnabled(false); APPLICATION->settings()->set("SelectedInstance", QString()); selectionBad(); return; @@ -1759,11 +1683,12 @@ void MainWindow::selectionBad() { // start by reseting everything... m_selectedInstance = nullptr; + m_statusLeft->setText(tr("No instance selected")); statusBar()->clearMessage(); ui->instanceToolBar->setEnabled(false); setInstanceActionsEnabled(false); - updateToolsMenu(); + updateLaunchButton(); renameButton->setText(tr("Rename Instance")); updateInstanceToolIcon("grass"); @@ -1810,7 +1735,9 @@ void MainWindow::updateStatusCenter() int timePlayed = APPLICATION->instances()->getTotalPlayTime(); if (timePlayed > 0) { - m_statusCenter->setText(tr("Total playtime: %1").arg(Time::prettifyDuration(timePlayed))); + m_statusCenter->setText( + tr("Total playtime: %1") + .arg(Time::prettifyDuration(timePlayed, APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool()))); } } // "Instance actions" are actions that require an instance to be selected (i.e. "new instance" is not here) @@ -1826,7 +1753,7 @@ void MainWindow::setInstanceActionsEnabled(bool enabled) ui->actionCreateInstanceShortcut->setEnabled(enabled); } -void MainWindow::refreshCurrentInstance([[maybe_unused]] bool running) +void MainWindow::refreshCurrentInstance() { auto current = view->selectionModel()->currentIndex(); instanceChanged(current, current); diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 9fd72d25d..0b6144522 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -144,10 +144,6 @@ class MainWindow : public QMainWindow { void on_actionLaunchInstance_triggered(); - void on_actionLaunchInstanceOffline_triggered(); - - void on_actionLaunchInstanceDemo_triggered(); - void on_actionKillInstance_triggered(); void on_actionDeleteInstance_triggered(); @@ -155,10 +151,7 @@ class MainWindow : public QMainWindow { void deleteGroup(); void undoTrashInstance(); - inline void on_actionExportInstance_triggered() - { - on_actionExportInstanceZip_triggered(); - } + inline void on_actionExportInstance_triggered() { on_actionExportInstanceZip_triggered(); } void on_actionExportInstanceZip_triggered(); void on_actionExportInstanceMrPack_triggered(); void on_actionExportInstanceFlamePack_triggered(); @@ -181,7 +174,7 @@ class MainWindow : public QMainWindow { void updateMainToolBar(); - void updateToolsMenu(); + void updateLaunchButton(); void updateThemeMenu(); @@ -215,7 +208,7 @@ class MainWindow : public QMainWindow { void keyReleaseEvent(QKeyEvent* event) override; #endif - void refreshCurrentInstance(bool running); + void refreshCurrentInstance(); private: void retranslateUi(); diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index 6ef32099f..91b2c2703 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -440,22 +440,6 @@ Ctrl+D - - - Launch &Offline - - - Launch the selected instance in offline mode. - - - - - Launch &Demo - - - Launch the selected instance in demo mode. - - diff --git a/launcher/ui/dialogs/ExportPackDialog.cpp b/launcher/ui/dialogs/ExportPackDialog.cpp index ad8db5ffb..5af24b1b7 100644 --- a/launcher/ui/dialogs/ExportPackDialog.cpp +++ b/launcher/ui/dialogs/ExportPackDialog.cpp @@ -37,15 +37,21 @@ ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPlatform::ResourceProvider provider) : QDialog(parent), instance(instance), ui(new Ui::ExportPackDialog), m_provider(provider) { + Q_ASSERT(m_provider == ModPlatform::ResourceProvider::MODRINTH || m_provider == ModPlatform::ResourceProvider::FLAME); + ui->setupUi(this); - ui->name->setText(instance->name()); + ui->name->setPlaceholderText(instance->name()); + ui->name->setText(instance->settings()->get("ExportName").toString()); + ui->version->setText(instance->settings()->get("ExportVersion").toString()); + ui->optionalFiles->setChecked(instance->settings()->get("ExportOptionalFiles").toBool()); + if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { - ui->summary->setText(instance->notes().split(QRegularExpression("\\r?\\n"))[0]); - setWindowTitle("Export Modrinth Pack"); + setWindowTitle(tr("Export Modrinth Pack")); + ui->summary->setText(instance->settings()->get("ExportSummary").toString()); } else { - setWindowTitle("Export CurseForge Pack"); - ui->version->setText(""); - ui->summaryLabel->setText("Author"); + setWindowTitle(tr("Export CurseForge Pack")); + ui->summaryLabel->setText(tr("&Author")); + ui->summary->setText(instance->settings()->get("ExportAuthor").toString()); } // ensure a valid pack is generated @@ -75,20 +81,19 @@ ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPla MinecraftInstance* mcInstance = dynamic_cast(instance.get()); if (mcInstance) { - mcInstance->loaderModList()->update(); const QDir index = mcInstance->loaderModList()->indexDir(); if (index.exists()) - proxy->blockedPaths().insert(root.relativeFilePath(index.absolutePath())); + proxy->ignoreFilesWithPath().insert(root.relativeFilePath(index.absolutePath())); } - ui->treeView->setModel(proxy); - ui->treeView->setRootIndex(proxy->mapFromSource(model->index(instance->gameRoot()))); - ui->treeView->sortByColumn(0, Qt::AscendingOrder); + ui->files->setModel(proxy); + ui->files->setRootIndex(proxy->mapFromSource(model->index(instance->gameRoot()))); + ui->files->sortByColumn(0, Qt::AscendingOrder); model->setFilter(filter); model->setRootPath(instance->gameRoot()); - QHeaderView* headerView = ui->treeView->header(); + QHeaderView* headerView = ui->files->header(); headerView->setSectionResizeMode(QHeaderView::ResizeToContents); headerView->setSectionResizeMode(0, QHeaderView::Stretch); } @@ -100,26 +105,41 @@ ExportPackDialog::~ExportPackDialog() void ExportPackDialog::done(int result) { - if (result == Accepted) { - const QString filename = FS::RemoveInvalidFilenameChars(ui->name->text()); - QString output; - if (m_provider == ModPlatform::ResourceProvider::MODRINTH) - output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()), - FS::PathCombine(QDir::homePath(), filename + ".mrpack"), "Modrinth pack (*.mrpack *.zip)", - nullptr); - else - output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()), - FS::PathCombine(QDir::homePath(), filename + ".zip"), "CurseForge pack (*.zip)", nullptr); + auto settings = instance->settings(); + settings->set("ExportName", ui->name->text()); + settings->set("ExportVersion", ui->version->text()); + settings->set(m_provider == ModPlatform::ResourceProvider::FLAME ? "ExportAuthor" : "ExportSummary", ui->summary->text()); + settings->set("ExportOptionalFiles", ui->optionalFiles->isChecked()); + + if (result == Accepted) { + const QString name = ui->name->text().isEmpty() ? instance->name() : ui->name->text(); + const QString filename = FS::RemoveInvalidFilenameChars(name); + + QString output; + if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { + output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + ".mrpack"), + "Modrinth pack (*.mrpack *.zip)", nullptr); + if (output.isEmpty()) + return; + if (!(output.endsWith(".zip") || output.endsWith(".mrpack"))) + output.append(".mrpack"); + } else { + output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + ".zip"), + "CurseForge pack (*.zip)", nullptr); + if (output.isEmpty()) + return; + if (!output.endsWith(".zip")) + output.append(".zip"); + } - if (output.isEmpty()) - return; Task* task; - if (m_provider == ModPlatform::ResourceProvider::MODRINTH) - task = new ModrinthPackExportTask(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output, - std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); - else - task = new FlamePackExportTask(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output, + if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { + task = new ModrinthPackExportTask(name, ui->version->text(), ui->summary->text(), ui->optionalFiles->isChecked(), instance, + output, std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); + } else { + task = new FlamePackExportTask(name, ui->version->text(), ui->summary->text(), ui->optionalFiles->isChecked(), instance, output, std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); + } connect(task, &Task::failed, [this](const QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); @@ -140,7 +160,6 @@ void ExportPackDialog::done(int result) void ExportPackDialog::validate() { - const bool invalid = - ui->name->text().isEmpty() || ((m_provider == ModPlatform::ResourceProvider::MODRINTH) && ui->version->text().isEmpty()); - ui->buttonBox->button(QDialogButtonBox::Ok)->setDisabled(invalid); + ui->buttonBox->button(QDialogButtonBox::Ok) + ->setDisabled(m_provider == ModPlatform::ResourceProvider::MODRINTH && ui->version->text().isEmpty()); } diff --git a/launcher/ui/dialogs/ExportPackDialog.ui b/launcher/ui/dialogs/ExportPackDialog.ui index 3976e28f8..09dea72a8 100644 --- a/launcher/ui/dialogs/ExportPackDialog.ui +++ b/launcher/ui/dialogs/ExportPackDialog.ui @@ -7,12 +7,9 @@ 0 0 650 - 413 + 510 - - Export Pack - true @@ -20,13 +17,16 @@ - Information + &Description - Summary + &Summary + + + summary @@ -36,14 +36,20 @@ - Name + &Name + + + name - Version + &Version + + + version @@ -57,31 +63,52 @@ - - - - Files + + + &Options - - - - - - true - - - QAbstractItemView::ExtendedSelection - - - true - - - false - + + + + + &Files + + + files + + + + + + + true + + + QAbstractItemView::ExtendedSelection + + + true + + + false + + + + + + + &Mark disabled files as optional + + + true + + + + @@ -97,7 +124,8 @@ name version summary - treeView + files + optionalFiles diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index 0af1ec59b..1f0fa7cd2 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -5,8 +5,6 @@ #include "ScrollMessageBox.h" #include "ui_ReviewMessageBox.h" -#include "FileSystem.h" -#include "Json.h" #include "Markdown.h" #include "tasks/ConcurrentTask.h" @@ -30,9 +28,9 @@ static std::list mcVersions(BaseInstance* inst) return { static_cast(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() }; } -static std::optional mcLoaders(BaseInstance* inst) +static std::optional mcLoaders(BaseInstance* inst) { - return { static_cast(inst)->getPackProfile()->getModLoaders() }; + return { static_cast(inst)->getPackProfile()->getSupportedModLoaders() }; } ModUpdateDialog::ModUpdateDialog(QWidget* parent, diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index 4b82c0c5c..9e121bb61 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -279,7 +279,7 @@ QList ModDownloadDialog::getPages() { QList pages; - auto loaders = static_cast(m_instance)->getPackProfile()->getModLoaders().value(); + auto loaders = static_cast(m_instance)->getPackProfile()->getSupportedModLoaders().value(); if (ModrinthAPI::validateModLoaders(loaders)) pages.append(ModrinthModPage::create(this, *m_instance)); diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 5c6fb092b..c95bfabdd 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -64,7 +64,8 @@ AccountListPage::AccountListPage(QWidget* parent) : QMainWindow(parent), ui(new ui->setupUi(this); ui->listView->setEmptyString( tr("Welcome!\n" - "If you're new here, you can click the \"Add\" button to add your Mojang or Minecraft account.")); + "If you're new here, you can select the \"Add Microsoft\" or \"Add Mojang\" buttons to link your Microsoft and/or Mojang " + "accounts.")); ui->listView->setEmptyMode(VersionListView::String); ui->listView->setContextMenuPolicy(Qt::CustomContextMenu); @@ -85,6 +86,8 @@ AccountListPage::AccountListPage(QWidget* parent) : QMainWindow(parent), ui(new connect(selectionModel, &QItemSelectionModel::selectionChanged, [this]([[maybe_unused]] const QItemSelection& sel, [[maybe_unused]] const QItemSelection& dsel) { updateButtonStates(); }); connect(ui->listView, &VersionListView::customContextMenuRequested, this, &AccountListPage::ShowContextMenu); + connect(ui->listView, &VersionListView::activated, this, + [this](const QModelIndex& index) { m_accounts->setDefaultAccount(m_accounts->at(index.row())); }); connect(m_accounts.get(), &AccountList::listChanged, this, &AccountListPage::listChanged); connect(m_accounts.get(), &AccountList::listActivityChanged, this, &AccountListPage::listChanged); diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index 45d8f0180..ac50319ec 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -185,6 +185,7 @@ void JavaPage::updateThresholds() { auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; unsigned int maxMem = ui->maxMemSpinBox->value(); + unsigned int minMem = ui->minMemSpinBox->value(); QString iconName; @@ -194,6 +195,9 @@ void JavaPage::updateThresholds() } else if (maxMem > (sysMiB * 0.9)) { iconName = "status-yellow"; ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); + } else if (maxMem < minMem) { + iconName = "status-yellow"; + ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value")); } else { iconName = "status-good"; ui->labelMaxMemIcon->setToolTip(""); diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp index 1c7747210..553cefd83 100644 --- a/launcher/ui/pages/global/MinecraftPage.cpp +++ b/launcher/ui/pages/global/MinecraftPage.cpp @@ -2,7 +2,6 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield - * Copyright (C) 2023 seth * * 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 @@ -115,13 +114,11 @@ void MinecraftPage::applySettings() s->set("ShowGameTime", ui->showGameTime->isChecked()); s->set("ShowGlobalGameTime", ui->showGlobalGameTime->isChecked()); s->set("RecordGameTime", ui->recordGameTime->isChecked()); + s->set("ShowGameTimeWithoutDays", ui->showGameTimeWithoutDays->isChecked()); // Miscellaneous s->set("CloseAfterLaunch", ui->closeAfterLaunchCheck->isChecked()); s->set("QuitAfterGameStop", ui->quitAfterGameStopCheck->isChecked()); - - // Mod loader settings - s->set("DisableQuiltBeacon", ui->disableQuiltBeaconCheckBox->isChecked()); } void MinecraftPage::loadSettings() @@ -169,11 +166,10 @@ void MinecraftPage::loadSettings() ui->showGameTime->setChecked(s->get("ShowGameTime").toBool()); ui->showGlobalGameTime->setChecked(s->get("ShowGlobalGameTime").toBool()); ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool()); + ui->showGameTimeWithoutDays->setChecked(s->get("ShowGameTimeWithoutDays").toBool()); ui->closeAfterLaunchCheck->setChecked(s->get("CloseAfterLaunch").toBool()); ui->quitAfterGameStopCheck->setChecked(s->get("QuitAfterGameStop").toBool()); - - ui->disableQuiltBeaconCheckBox->setChecked(s->get("DisableQuiltBeacon").toBool()); } void MinecraftPage::retranslate() diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui index 98e90e4ec..b5cfa659f 100644 --- a/launcher/ui/pages/global/MinecraftPage.ui +++ b/launcher/ui/pages/global/MinecraftPage.ui @@ -138,6 +138,13 @@ + + + + Show time spent playing in hours + + + @@ -190,25 +197,6 @@ Tweaks - - - - Mod loader settings - - - - - - Disable Quilt Loader Beacon - - - Disable Quilt loader's beacon for counting monthly active users - - - - - - diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index 719db8799..1a8fafa9b 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -152,6 +152,7 @@ void ExternalResourcesPage::retranslate() void ExternalResourcesPage::itemActivated(const QModelIndex&) { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); + m_model->setResourceEnabled(selection.indexes(), EnableAction::TOGGLE); } void ExternalResourcesPage::filterTextChanged(const QString& newContents) diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui index 3c8366917..ba703f77d 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -168,6 +168,17 @@ Go to mods home page + + + false + + + Remove metadata + + + Remove mod's metadata + + diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index 108f2c274..7aa6bd322 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -3,7 +3,6 @@ * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (C) 2023 seth * * 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 @@ -254,14 +253,6 @@ void InstanceSettingsPage::applySettings() m_settings->reset("InstanceAccountId"); } - bool overrideModLoaderSettings = ui->modLoaderSettingsGroupBox->isChecked(); - m_settings->set("OverrideModLoaderSettings", overrideModLoaderSettings); - if (overrideModLoaderSettings) { - m_settings->set("DisableQuiltBeacon", ui->disableQuiltBeaconCheckBox->isChecked()); - } else { - m_settings->reset("DisableQuiltBeacon"); - } - // FIXME: This should probably be called by a signal instead m_instance->updateRuntimeContext(); } @@ -365,10 +356,6 @@ void InstanceSettingsPage::loadSettings() ui->instanceAccountGroupBox->setChecked(m_settings->get("UseAccountForInstance").toBool()); updateAccountsMenu(); - - // Mod loader specific settings - ui->modLoaderSettingsGroupBox->setChecked(m_settings->get("OverrideModLoaderSettings").toBool()); - ui->disableQuiltBeaconCheckBox->setChecked(m_settings->get("DisableQuiltBeacon").toBool()); } void InstanceSettingsPage::on_javaDetectBtn_clicked() @@ -491,6 +478,7 @@ void InstanceSettingsPage::updateThresholds() { auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; unsigned int maxMem = ui->maxMemSpinBox->value(); + unsigned int minMem = ui->minMemSpinBox->value(); QString iconName; @@ -500,6 +488,9 @@ void InstanceSettingsPage::updateThresholds() } else if (maxMem > (sysMiB * 0.9)) { iconName = "status-yellow"; ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); + } else if (maxMem < minMem) { + iconName = "status-yellow"; + ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value")); } else { iconName = "status-good"; ui->labelMaxMemIcon->setToolTip(""); diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index fcc2a5d0a..81cf7093e 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -583,31 +583,6 @@ Miscellaneous - - - - true - - - false - - - Mod loader settings - - - - - - Disable Quilt Loader Beacon - - - Disable Quilt loader's beacon for counting monthly active users - - - - - - diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 0f5e29cb6..b42bbf466 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -92,6 +92,10 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr ui->actionsToolbar->addAction(ui->actionVisitItemPage); connect(ui->actionVisitItemPage, &QAction::triggered, this, &ModFolderPage::visitModPages); + ui->actionRemoveItemMetadata->setToolTip(tr("Remove mod's metadata")); + ui->actionsToolbar->insertActionAfter(ui->actionRemoveItem, ui->actionRemoveItemMetadata); + connect(ui->actionRemoveItemMetadata, &QAction::triggered, this, &ModFolderPage::deleteModMetadata); + auto check_allow_update = [this] { return ui->treeView->selectionModel()->hasSelection() || !m_model->empty(); }; connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this, check_allow_update] { @@ -104,11 +108,16 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr if (selected <= 1) { ui->actionVisitItemPage->setText(tr("Visit mod's page")); ui->actionVisitItemPage->setToolTip(tr("Go to mod's home page")); + + ui->actionRemoveItemMetadata->setToolTip(tr("Remove mod's metadata")); } else { ui->actionVisitItemPage->setText(tr("Visit mods' pages")); ui->actionVisitItemPage->setToolTip(tr("Go to the pages of the selected mods")); + + ui->actionRemoveItemMetadata->setToolTip(tr("Remove mods' metadata")); } ui->actionVisitItemPage->setEnabled(selected != 0); + ui->actionRemoveItemMetadata->setEnabled(selected != 0); }); connect(mods.get(), &ModFolderModel::rowsInserted, this, @@ -297,3 +306,24 @@ void ModFolderPage::visitModPages() DesktopServices::openUrl(url); } } + +void ModFolderPage::deleteModMetadata() +{ + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + auto selectionCount = m_model->selectedMods(selection).length(); + if (selectionCount == 0) + return; + if (selectionCount > 1) { + auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), + tr("You are about to remove the metadata for %1 mods.\n" + "Are you sure?") + .arg(selectionCount), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + + m_model->deleteModsMetadata(selection); +} diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index a23dcae18..0c654d0d1 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -61,6 +61,7 @@ class ModFolderPage : public ExternalResourcesPage { private slots: void removeItems(const QItemSelection& selection) override; + void deleteModMetadata(); void installMods(); void updateMods(); diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index da49f4a7d..2142e6c9f 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -3,7 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (C) 2022 TheKodeToad + * Copyright (C) 2023 TheKodeToad * * 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 @@ -354,14 +354,8 @@ class ServersModel : public QAbstractListModel { } } - virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override - { - return parent.isValid() ? 0 : m_servers.size(); - } - int columnCount(const QModelIndex& parent) const override - { - return parent.isValid() ? 0 : COLUMN_COUNT; - } + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override { return parent.isValid() ? 0 : m_servers.size(); } + int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : COLUMN_COUNT; } Server* at(int index) { @@ -445,10 +439,7 @@ class ServersModel : public QAbstractListModel { qDebug() << "Changed:" << path; load(); } - void fileChanged(const QString& path) - { - qDebug() << "Changed:" << path; - } + void fileChanged(const QString& path) { qDebug() << "Changed:" << path; } private slots: void save_internal() @@ -492,10 +483,7 @@ class ServersModel : public QAbstractListModel { m_saveTimer.stop(); } - bool saveIsScheduled() const - { - return m_dirty; - } + bool saveIsScheduled() const { return m_dirty; } void updateFSObserver() { @@ -743,7 +731,7 @@ void ServersPage::on_actionMove_Down_triggered() void ServersPage::on_actionJoin_triggered() { const auto& address = m_model->at(currentServer)->m_address; - APPLICATION->launch(m_inst, true, false, nullptr, std::make_shared(MinecraftServerTarget::parse(address))); + APPLICATION->launch(m_inst, true, false, std::make_shared(MinecraftServerTarget::parse(address))); } #include "ServersPage.moc" diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index ef029d1d9..e22c764ca 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -166,14 +166,17 @@ VersionPage::VersionPage(MinecraftInstance* inst, QWidget* parent) : QMainWindow ui->packageView->setSelectionMode(QAbstractItemView::SingleSelection); ui->packageView->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->packageView->selectionModel(), &QItemSelectionModel::currentChanged, this, &VersionPage::versionCurrent); auto smodel = ui->packageView->selectionModel(); + connect(smodel, &QItemSelectionModel::currentChanged, this, &VersionPage::versionCurrent); connect(smodel, &QItemSelectionModel::currentChanged, this, &VersionPage::packageCurrent); - connect(m_profile.get(), &PackProfile::minecraftChanged, this, &VersionPage::updateVersionControls); updateVersionControls(); preselect(0); connect(ui->packageView, &ModListView::customContextMenuRequested, this, &VersionPage::showContextMenu); + connect(ui->packageView, &QAbstractItemView::activated, this, [this](const QModelIndex& index) { + auto component = m_profile->getComponent(index.row()); + component->setEnabled(!component->isEnabled()); + }); connect(ui->filterEdit, &QLineEdit::textChanged, this, &VersionPage::onFilterTextChanged); } @@ -408,7 +411,7 @@ void VersionPage::on_actionDownload_All_triggered() if (!APPLICATION->accounts()->anyAccountIsValid()) { CustomMessageBox::selectable(this, tr("Error"), tr("Cannot download Minecraft or update instances unless you have at least " - "one account added.\nPlease add your Mojang or Minecraft account."), + "one account added.\nPlease add your Microsoft or Mojang account."), QMessageBox::Warning) ->show(); return; diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index b75378905..c628f74ac 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -33,7 +33,7 @@ ResourceAPI::SearchArgs ModModel::createSearchArguments() auto sort = getCurrentSortingMethodByIndex(); - return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, sort, profile->getModLoaders(), versions }; + return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, sort, profile->getSupportedModLoaders(), versions }; } ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& entry) @@ -48,7 +48,7 @@ ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& en if (!m_filter->versions.empty()) versions = m_filter->versions; - return { pack, versions, profile->getModLoaders() }; + return { pack, versions, profile->getSupportedModLoaders() }; } ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(QModelIndex& entry) diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 60a43128a..7eb08f6a2 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -124,8 +124,7 @@ void ModPage::updateVersionList() auto version = current_pack->versions[i]; bool valid = false; for (auto& mcVer : m_filter->versions) { - // NOTE: Flame doesn't care about loader, so passing it changes nothing. - if (validateVersion(version, mcVer.toString(), packProfile->getModLoaders())) { + if (validateVersion(version, mcVer.toString(), packProfile->getSupportedModLoaders())) { valid = true; break; } diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 5510c1911..5a43e49a6 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -55,7 +55,7 @@ class ModPage : public ResourcePage { virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, - std::optional loaders = {}) const -> bool = 0; + std::optional loaders = {}) const -> bool = 0; [[nodiscard]] bool supportsFiltering() const override { return true; }; auto getFilter() const -> const std::shared_ptr { return m_filter; } diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index 0a7edb7b7..cb8f1920f 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -132,6 +132,32 @@ void ResourceModel::search() if (hasActiveSearchJob()) return; + if (m_search_term.startsWith("#")) { + auto projectId = m_search_term.mid(1); + if (!projectId.isEmpty()) { + ResourceAPI::ProjectInfoCallbacks callbacks; + + callbacks.on_fail = [this](QString reason) { + if (!s_running_models.constFind(this).value()) + return; + searchRequestFailed(reason, -1); + }; + callbacks.on_abort = [this] { + if (!s_running_models.constFind(this).value()) + return; + searchRequestAborted(); + }; + + callbacks.on_succeed = [this](auto& doc, auto& pack) { + if (!s_running_models.constFind(this).value()) + return; + searchRequestForOneSucceeded(doc); + }; + if (auto job = m_api->getProjectInfo({ projectId }, std::move(callbacks)); job) + runSearchJob(job); + return; + } + } auto args{ createSearchArguments() }; auto callbacks{ createSearchCallbacks() }; @@ -189,11 +215,18 @@ void ResourceModel::loadEntry(QModelIndex& entry) // Use default if no callbacks are set if (!callbacks.on_succeed) - callbacks.on_succeed = [this, entry](auto& doc, auto pack) { + callbacks.on_succeed = [this, entry](auto& doc, auto& newpack) { if (!s_running_models.constFind(this).value()) return; + auto pack = newpack; infoRequestSucceeded(doc, pack, entry); }; + if (!callbacks.on_fail) + callbacks.on_fail = [this](QString reason) { + if (!s_running_models.constFind(this).value()) + return; + QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load project info:%1").arg(reason)); + }; if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job) runInfoJob(job); @@ -372,6 +405,27 @@ void ResourceModel::searchRequestSucceeded(QJsonDocument& doc) endInsertRows(); } +void ResourceModel::searchRequestForOneSucceeded(QJsonDocument& doc) +{ + ModPlatform::IndexedPack::Ptr pack = std::make_shared(); + + try { + auto obj = Json::requireObject(doc); + if (obj.contains("data")) + obj = Json::requireObject(obj, "data"); + loadIndexedPack(*pack, obj); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause(); + } + + m_search_state = SearchState::Finished; + + beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + 1); + m_packs.append(pack); + endInsertRows(); +} + void ResourceModel::searchRequestFailed([[maybe_unused]] QString reason, int network_error_code) { switch (network_error_code) { diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index cc813d6e6..ecf4f8f79 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -149,6 +149,7 @@ class ResourceModel : public QAbstractListModel { private: /* Default search request callbacks */ void searchRequestSucceeded(QJsonDocument&); + void searchRequestForOneSucceeded(QJsonDocument&); void searchRequestFailed(QString reason, int network_error_code); void searchRequestAborted(); diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index c087e2be3..fc7d64a48 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -44,9 +44,6 @@ #include #include "Markdown.h" -#include "ResourceDownloadTask.h" - -#include "minecraft/MinecraftInstance.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/pages/modplatform/ResourceModel.h" diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.cpp b/launcher/ui/pages/modplatform/ShaderPackPage.cpp index fbf94e844..586dffc55 100644 --- a/launcher/ui/pages/modplatform/ShaderPackPage.cpp +++ b/launcher/ui/pages/modplatform/ShaderPackPage.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-3.0-only #include "ShaderPackPage.h" +#include "modplatform/ModIndex.h" #include "ui_ResourcePage.h" #include "ShaderPackModel.h" @@ -48,7 +49,7 @@ void ShaderPackResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pac const std::shared_ptr base_model) { QString custom_target_folder; - if (version.loaders.contains(QStringLiteral("canvas"))) + if (version.loaders & ModPlatform::Cauldron) custom_target_folder = QStringLiteral("resourcepacks"); m_model->addPack(pack, version, base_model, false, custom_target_folder); } diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp index 9cd5eed53..dee3784e5 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp @@ -67,9 +67,10 @@ bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParen if (searchTerm.isEmpty()) { return true; } - QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); ATLauncher::IndexedPack pack = sourceModel()->data(index, Qt::UserRole).value(); + if (searchTerm.startsWith("#")) + return QString::number(pack.id) == searchTerm.mid(1); return pack.name.contains(searchTerm, Qt::CaseInsensitive); } diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp index 39f4f346a..b6fb71535 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -21,6 +21,7 @@ #include #include "net/ApiDownload.h" +#include "ui/widgets/ProjectItem.h" namespace Atl { @@ -46,27 +47,50 @@ QVariant ListModel::data(const QModelIndex& index, int role) const } ATLauncher::IndexedPack pack = modpacks.at(pos); - if (role == Qt::DisplayRole) { - return pack.name; - } else if (role == Qt::ToolTipRole) { - return pack.name; - } else if (role == Qt::DecorationRole) { - if (m_logoMap.contains(pack.safeName)) { - return (m_logoMap.value(pack.safeName)); + switch (role) { + case 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("
")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + } + return pack.description; } - auto icon = APPLICATION->getThemedIcon("atlauncher-placeholder"); + case Qt::DecorationRole: { + if (m_logoMap.contains(pack.safeName)) { + return (m_logoMap.value(pack.safeName)); + } + auto icon = APPLICATION->getThemedIcon("atlauncher-placeholder"); - auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(pack.safeName.toLower()); - ((ListModel*)this)->requestLogo(pack.safeName, url); + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(pack.safeName.toLower()); + ((ListModel*)this)->requestLogo(pack.safeName, url); - return icon; - } else if (role == Qt::UserRole) { - QVariant v; - v.setValue(pack); - return v; + return icon; + } + case Qt::UserRole: { + QVariant v; + v.setValue(pack); + return v; + } + case Qt::DisplayRole: + return pack.name; + case Qt::SizeHintRole: + return QSize(0, 58); + // Custom data + case UserDataTypes::TITLE: + return pack.name; + case UserDataTypes::DESCRIPTION: + return pack.description; + case UserDataTypes::SELECTED: + return false; + case UserDataTypes::INSTALLED: + return false; + default: + break; } - return QVariant(); + return {}; } void ListModel::request() diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index 5e3b9ecf1..c7e800278 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -35,11 +35,11 @@ */ #include "AtlPage.h" +#include "ui/widgets/ProjectItem.h" #include "ui_AtlPage.h" #include "BuildConfig.h" -#include "AtlOptionalModDialog.h" #include "AtlUserInteractionSupportImpl.h" #include "modplatform/atlauncher/ATLPackInstallTask.h" #include "ui/dialogs/NewInstanceDialog.h" @@ -71,6 +71,8 @@ AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &AtlPage::onSortingSelectionChanged); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &AtlPage::onSelectionChanged); connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &AtlPage::onVersionSelectionChanged); + + ui->packView->setItemDelegate(new ProjectItemDelegate(this)); } AtlPage::~AtlPage() diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui index 746aa6d1c..8b6747331 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui @@ -11,44 +11,7 @@
- - - - - - true - - - - 96 - 48 - - - - - - - - true - - - true - - - - - - - Warning: This is still a work in progress. If you run into issues with the imported modpack, it may be a bug. - - - true - - - - - - + @@ -68,7 +31,34 @@ - + + + + + + true + + + true + + + + + + + true + + + + 96 + 48 + + + + + + + Search and filter... @@ -78,6 +68,31 @@ + + + + Search + + + + + + + + true + + + + Warning: This is still a work in progress. If you run into issues with the imported modpack, it may be a bug. + + + Qt::AlignCenter + + + true + + +
diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index ff21d0109..8875a9452 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -1,6 +1,8 @@ #include "FlameModel.h" #include #include "Application.h" +#include "modplatform/ResourceAPI.h" +#include "modplatform/flame/FlameAPI.h" #include "ui/widgets/ProjectItem.h" #include "net/ApiDownload.h" @@ -161,6 +163,21 @@ void ListModel::fetchMore(const QModelIndex& parent) void ListModel::performPaginatedSearch() { + if (currentSearchTerm.startsWith("#")) { + auto projectId = currentSearchTerm.mid(1); + if (!projectId.isEmpty()) { + ResourceAPI::ProjectInfoCallbacks callbacks; + + callbacks.on_fail = [this](QString reason) { searchRequestFailed(reason); }; + callbacks.on_succeed = [this](auto& doc, auto& pack) { searchRequestForOneSucceeded(doc); }; + static const FlameAPI api; + if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) { + jobPtr = job; + jobPtr->start(); + } + return; + } + } auto netJob = makeShared("Flame::Search", APPLICATION->network()); auto searchUrl = QString( "https://api.curseforge.com/v1/mods/search?" @@ -189,23 +206,24 @@ void ListModel::searchWithTerm(const QString& term, int sort) } currentSearchTerm = term; currentSort = sort; - if (jobPtr) { + if (hasActiveSearchJob()) { jobPtr->abort(); searchState = ResetRequested; return; - } else { - beginResetModel(); - modpacks.clear(); - endResetModel(); - searchState = None; } + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + nextSearchOffset = 0; performPaginatedSearch(); } void Flame::ListModel::searchRequestFinished() { - jobPtr.reset(); + if (hasActiveSearchJob()) + return; QJsonParseError parse_error; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); @@ -246,6 +264,25 @@ void Flame::ListModel::searchRequestFinished() endInsertRows(); } +void Flame::ListModel::searchRequestForOneSucceeded(QJsonDocument& doc) +{ + jobPtr.reset(); + + auto packObj = Json::ensureObject(doc.object(), "data"); + + Flame::IndexedPack pack; + try { + Flame::loadIndexedPack(pack, packObj); + } catch (const JSONValidationError& e) { + qWarning() << "Error while loading pack from CurseForge: " << e.cause(); + return; + } + + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + 1); + modpacks.append({ pack }); + endInsertRows(); +} + void Flame::ListModel::searchRequestFailed(QString reason) { jobPtr.reset(); diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.h b/launcher/ui/pages/modplatform/flame/FlameModel.h index b3bc96b8c..fd8496dfb 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModel.h @@ -40,6 +40,9 @@ class ListModel : public QAbstractListModel { void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); void searchWithTerm(const QString& term, const int sort); + [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } + [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } + private slots: void performPaginatedSearch(); @@ -48,6 +51,7 @@ class ListModel : public QAbstractListModel { void searchRequestFinished(); void searchRequestFailed(QString reason); + void searchRequestForOneSucceeded(QJsonDocument&); private: void requestLogo(QString file, QString url); @@ -63,7 +67,7 @@ class ListModel : public QAbstractListModel { int currentSort = 0; int nextSearchOffset = 0; enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; - NetJob::Ptr jobPtr; + Task::Ptr jobPtr; std::shared_ptr response = std::make_shared(); }; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index 183e16f90..50656f427 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -50,7 +50,8 @@ static FlameAPI api; -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), m_fetch_progress(this, false) { ui->setupUi(this); connect(ui->searchButton, &QPushButton::clicked, this, &FlamePage::triggerSearch); @@ -61,6 +62,17 @@ FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(paren ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); + m_search_timer.setSingleShot(true); + + connect(&m_search_timer, &QTimer::timeout, this, &FlamePage::triggerSearch); + + m_fetch_progress.hideIfInactive(true); + m_fetch_progress.setFixedHeight(24); + m_fetch_progress.progressFormat(""); + + ui->gridLayout->addWidget(&m_fetch_progress, 2, 0, 1, ui->gridLayout->columnCount()); + // index is used to set the sorting with the curseforge api ui->sortByBox->addItem(tr("Sort by Featured")); ui->sortByBox->addItem(tr("Sort by Popularity")); @@ -90,6 +102,11 @@ bool FlamePage::eventFilter(QObject* watched, QEvent* event) triggerSearch(); keyEvent->accept(); return true; + } else { + if (m_search_timer.isActive()) + m_search_timer.stop(); + + m_search_timer.start(350); } } return QWidget::eventFilter(watched, event); @@ -114,6 +131,7 @@ void FlamePage::openedImpl() void FlamePage::triggerSearch() { listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); + m_fetch_progress.watch(listModel->activeSearchJob().get()); } void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index ff5c79750..d35858fbc 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -39,8 +39,9 @@ #include #include -#include "tasks/Task.h" +#include #include "ui/pages/BasePage.h" +#include "ui/widgets/ProgressWidget.h" namespace Ui { class FlamePage; @@ -86,4 +87,9 @@ class FlamePage : public QWidget, public BasePage { Flame::IndexedPack current; int m_selected_version_index = -1; + + ProgressWidget m_fetch_progress; + + // Used to do instant searching with a delay to cache quick changes + QTimer m_search_timer; }; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui index 71d195135..f9e1fe67f 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.ui +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -47,7 +47,7 @@
- + @@ -77,7 +77,7 @@ - + diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp index 2b020c48d..c80e4f999 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -31,7 +31,7 @@ void FlameModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonAr auto FlameModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion { - return FlameMod::loadDependencyVersions(m, arr); + return FlameMod::loadDependencyVersions(m, arr, &m_base_instance); } auto FlameModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index dc17e705a..1403e98f9 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -68,10 +68,10 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance) : auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, - std::optional loaders) const -> bool + std::optional loaders) const -> bool { - Q_UNUSED(loaders); - return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty(); + return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty() && + (!loaders.has_value() || !ver.loaders || loaders.value() & ver.loaders); } bool FlameModPage::optedOut(ModPlatform::IndexedVersion& ver) const diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index c6ebc1eac..035da2d5f 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -95,7 +95,7 @@ class FlameModPage : public ModPage { bool validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, - std::optional loaders = {}) const override; + std::optional loaders = {}) const override; bool optedOut(ModPlatform::IndexedVersion& ver) const override; void openUrl(const QUrl& url) override; diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp index 5c9ff63b2..d3ead0837 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp @@ -17,6 +17,7 @@ */ #include "ImportFTBPage.h" +#include "ui/widgets/ProjectItem.h" #include "ui_ImportFTBPage.h" #include @@ -32,17 +33,30 @@ ImportFTBPage::ImportFTBPage(NewInstanceDialog* dialog, QWidget* parent) : QWidg ui->setupUi(this); { + currentModel = new FilterModel(this); listModel = new ListModel(this); + currentModel->setSourceModel(listModel); - ui->modpackList->setModel(listModel); + ui->modpackList->setModel(currentModel); ui->modpackList->setSortingEnabled(true); ui->modpackList->header()->hide(); ui->modpackList->setIndentation(0); ui->modpackList->setIconSize(QSize(42, 42)); + + for (int i = 0; i < currentModel->getAvailableSortings().size(); i++) { + ui->sortByBox->addItem(currentModel->getAvailableSortings().keys().at(i)); + } + + ui->sortByBox->setCurrentText(currentModel->translateCurrentSorting()); } connect(ui->modpackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &ImportFTBPage::onPublicPackSelectionChanged); + connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &ImportFTBPage::onSortingSelectionChanged); + + connect(ui->searchEdit, &QLineEdit::textChanged, this, &ImportFTBPage::triggerSearch); + + ui->modpackList->setItemDelegate(new ProjectItemDelegate(this)); ui->modpackList->selectionModel()->reset(); } @@ -86,7 +100,7 @@ void ImportFTBPage::onPublicPackSelectionChanged(QModelIndex now, QModelIndex pr onPackSelectionChanged(); return; } - Modpack selectedPack = listModel->data(now, Qt::UserRole).value(); + Modpack selectedPack = currentModel->data(now, Qt::UserRole).value(); onPackSelectionChanged(&selectedPack); } @@ -101,4 +115,15 @@ void ImportFTBPage::onPackSelectionChanged(Modpack* pack) dialog->setSuggestedPack(); } +void ImportFTBPage::onSortingSelectionChanged(QString sort) +{ + FilterModel::Sorting toSet = currentModel->getAvailableSortings().value(sort); + currentModel->setSorting(toSet); +} + +void ImportFTBPage::triggerSearch() +{ + currentModel->setSearchTerm(ui->searchEdit->text()); +} + } // namespace FTBImportAPP diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h index 54c49f7b7..8e9661272 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h @@ -53,12 +53,15 @@ class ImportFTBPage : public QWidget, public BasePage { void suggestCurrent(); void onPackSelectionChanged(Modpack* pack = nullptr); private slots: + void onSortingSelectionChanged(QString data); void onPublicPackSelectionChanged(QModelIndex first, QModelIndex second); + void triggerSearch(); private: bool initialized = false; Modpack selected; ListModel* listModel = nullptr; + FilterModel* currentModel = nullptr; NewInstanceDialog* dialog = nullptr; Ui::ImportFTBPage* ui = nullptr; diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui index 32d548b0d..5e09fb6d1 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui @@ -10,8 +10,8 @@ 1011 - - + + @@ -21,6 +21,54 @@ + + + + + + Search and filter... + + + true + + + + + + + Search + + + + + + + + + + + + 265 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp index dc78f451c..134bdc0c3 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp @@ -23,7 +23,9 @@ #include #include #include "FileSystem.h" +#include "StringUtils.h" #include "modplatform/import_ftb/PackHelpers.h" +#include "ui/widgets/ProjectItem.h" namespace FTBImportAPP { @@ -71,18 +73,99 @@ QVariant ListModel::data(const QModelIndex& index, int role) const } auto pack = modpacks.at(pos); - if (role == Qt::DisplayRole) { - return pack.name; - } else if (role == Qt::DecorationRole) { - return pack.icon; - } else if (role == Qt::UserRole) { - QVariant v; - v.setValue(pack); - return v; - } else if (role == Qt::ToolTipRole) { - return tr("Minecraft %1").arg(pack.mcVersion); + if (role == Qt::ToolTipRole) { } - return QVariant(); + switch (role) { + case Qt::ToolTipRole: + return tr("Minecraft %1").arg(pack.mcVersion); + case Qt::DecorationRole: + return pack.icon; + case Qt::UserRole: { + QVariant v; + v.setValue(pack); + return v; + } + case Qt::DisplayRole: + return pack.name; + case Qt::SizeHintRole: + return QSize(0, 58); + // Custom data + case UserDataTypes::TITLE: + return pack.name; + case UserDataTypes::DESCRIPTION: + return tr("Minecraft %1").arg(pack.mcVersion); + case UserDataTypes::SELECTED: + return false; + case UserDataTypes::INSTALLED: + return false; + default: + break; + } + + return {}; +} + +FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent) +{ + currentSorting = Sorting::ByGameVersion; + sortings.insert(tr("Sort by Name"), Sorting::ByName); + sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion); +} + +bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const +{ + Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value(); + Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value(); + + if (currentSorting == Sorting::ByGameVersion) { + Version lv(leftPack.mcVersion); + Version rv(rightPack.mcVersion); + return lv < rv; + + } else if (currentSorting == Sorting::ByName) { + return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; + } + + // UHM, some inavlid value set?! + qWarning() << "Invalid sorting set!"; + return true; +} + +bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unused]] const QModelIndex& sourceParent) const +{ + if (searchTerm.isEmpty()) { + return true; + } + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + Modpack pack = sourceModel()->data(index, Qt::UserRole).value(); + return pack.name.contains(searchTerm, Qt::CaseInsensitive); +} + +void FilterModel::setSearchTerm(const QString term) +{ + searchTerm = term.trimmed(); + invalidate(); +} + +const QMap FilterModel::getAvailableSortings() +{ + return sortings; +} + +QString FilterModel::translateCurrentSorting() +{ + return sortings.key(currentSorting); +} + +void FilterModel::setSorting(Sorting s) +{ + currentSorting = s; + invalidate(); +} + +FilterModel::Sorting FilterModel::getCurrentSorting() +{ + return currentSorting; } } // namespace FTBImportAPP \ No newline at end of file diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.h b/launcher/ui/pages/modplatform/import_ftb/ListModel.h index c67aa8963..111928276 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.h +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.h @@ -20,11 +20,33 @@ #include #include +#include #include #include "modplatform/import_ftb/PackHelpers.h" namespace FTBImportAPP { +class FilterModel : public QSortFilterProxyModel { + Q_OBJECT + public: + FilterModel(QObject* parent = Q_NULLPTR); + enum Sorting { ByName, ByGameVersion }; + const QMap getAvailableSortings(); + QString translateCurrentSorting(); + void setSorting(Sorting sorting); + Sorting getCurrentSorting(); + void setSearchTerm(QString term); + + protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; + bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; + + private: + QMap sortings; + Sorting currentSorting; + QString searchTerm; +}; + class ListModel : public QAbstractListModel { Q_OBJECT diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 356d919d9..49666cf6e 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -41,6 +41,7 @@ #include #include "StringUtils.h" +#include "ui/widgets/ProjectItem.h" #include #include @@ -79,7 +80,20 @@ bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) co bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unused]] const QModelIndex& sourceParent) const { - return true; + if (searchTerm.isEmpty()) { + return true; + } + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + Modpack pack = sourceModel()->data(index, Qt::UserRole).value(); + if (searchTerm.startsWith("#")) + return pack.packCode == searchTerm.mid(1); + return pack.name.contains(searchTerm, Qt::CaseInsensitive); +} + +void FilterModel::setSearchTerm(const QString term) +{ + searchTerm = term.trimmed(); + invalidate(); } const QMap FilterModel::getAvailableSortings() @@ -139,39 +153,57 @@ QVariant ListModel::data(const QModelIndex& index, int role) const } Modpack pack = modpacks.at(pos); - if (role == Qt::DisplayRole) { - return pack.name + "\n" + translatePackType(pack.type); - } 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("
")).left(edit.lastIndexOf(" ")).append("..."); - return edit; + switch (role) { + case 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("
")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + } + return pack.description; } - return pack.description; - } else if (role == Qt::DecorationRole) { - if (m_logoMap.contains(pack.logo)) { - return (m_logoMap.value(pack.logo)); + case Qt::DecorationRole: { + if (m_logoMap.contains(pack.logo)) { + return (m_logoMap.value(pack.logo)); + } + QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + ((ListModel*)this)->requestLogo(pack.logo); + return icon; } - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - ((ListModel*)this)->requestLogo(pack.logo); - return icon; - } else if (role == Qt::ForegroundRole) { - if (pack.broken) { - // FIXME: Hardcoded color - return QColor(255, 0, 50); - } else if (pack.bugged) { - // FIXME: Hardcoded color - // bugged pack, currently only indicates bugged xml - return QColor(244, 229, 66); + case Qt::UserRole: { + QVariant v; + v.setValue(pack); + return v; } - } else if (role == Qt::UserRole) { - QVariant v; - v.setValue(pack); - return v; + case Qt::ForegroundRole: { + if (pack.broken) { + // FIXME: Hardcoded color + return QColor(255, 0, 50); + } else if (pack.bugged) { + // FIXME: Hardcoded color + // bugged pack, currently only indicates bugged xml + return QColor(244, 229, 66); + } + } + case Qt::DisplayRole: + return pack.name; + case Qt::SizeHintRole: + return QSize(0, 58); + // Custom data + case UserDataTypes::TITLE: + return pack.name; + case UserDataTypes::DESCRIPTION: + return pack.description; + case UserDataTypes::SELECTED: + return false; + case UserDataTypes::INSTALLED: + return false; + default: + break; } - return QVariant(); + return {}; } void ListModel::fill(ModpackList modpacks_) diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h index 51a58d991..c802a4b56 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h @@ -25,6 +25,7 @@ class FilterModel : public QSortFilterProxyModel { QString translateCurrentSorting(); void setSorting(Sorting sorting); Sorting getCurrentSorting(); + void setSearchTerm(QString term); protected: bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; @@ -33,6 +34,7 @@ class FilterModel : public QSortFilterProxyModel { private: QMap sortings; Sorting currentSorting; + QString searchTerm; }; class ListModel : public QAbstractListModel { diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp index 0103bbaa2..4104f1391 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -35,6 +35,7 @@ */ #include "Page.h" +#include "ui/widgets/ProjectItem.h" #include "ui_Page.h" #include @@ -110,6 +111,8 @@ Page::Page(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), dialog connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &Page::onSortingSelectionChanged); connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &Page::onVersionSelectionItemChanged); + connect(ui->searchEdit, &QLineEdit::textChanged, this, &Page::triggerSearch); + connect(ui->publicPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPublicPackSelectionChanged); connect(ui->thirdPartyPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onThirdPartyPackSelectionChanged); connect(ui->privatePackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPrivatePackSelectionChanged); @@ -125,6 +128,9 @@ Page::Page(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), dialog ui->thirdPartyPackList->selectionModel()->reset(); ui->privatePackList->selectionModel()->reset(); + ui->publicPackList->setItemDelegate(new ProjectItemDelegate(this)); + ui->thirdPartyPackList->setItemDelegate(new ProjectItemDelegate(this)); + ui->privatePackList->setItemDelegate(new ProjectItemDelegate(this)); onTabChanged(ui->tabWidget->currentIndex()); } @@ -319,6 +325,8 @@ void Page::onTabChanged(int tab) currentModpackInfo = ui->publicPackDescription; } + triggerSearch(); + currentList->selectionModel()->reset(); QModelIndex idx = currentList->currentIndex(); if (idx.isValid()) { @@ -358,4 +366,9 @@ void Page::onRemovePackClicked() onPackSelectionChanged(); } +void Page::triggerSearch() +{ + currentModel->setSearchTerm(ui->searchEdit->text()); +} + } // namespace LegacyFTB diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.h b/launcher/ui/pages/modplatform/legacy_ftb/Page.h index a12b0745d..4d317b7c0 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.h @@ -43,7 +43,6 @@ #include "QObjectPtr.h" #include "modplatform/legacy_ftb/PackFetchTask.h" #include "modplatform/legacy_ftb/PackHelpers.h" -#include "tasks/Task.h" #include "ui/pages/BasePage.h" class NewInstanceDialog; @@ -56,8 +55,6 @@ class Page; class ListModel; class FilterModel; -class PrivatePackListModel; -class PrivatePackFilterModel; class PrivatePackManager; class Page : public QWidget, public BasePage { @@ -98,6 +95,8 @@ class Page : public QWidget, public BasePage { void onAddPackClicked(); void onRemovePackClicked(); + void triggerSearch(); + private: FilterModel* currentModel = nullptr; QTreeView* currentList = nullptr; diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui index ad08dc255..56cba7485 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui @@ -10,8 +10,29 @@ 602 - - + + + + + + + Search and filter... + + + true + + + + + + + Search + + + + + + 0 @@ -36,9 +57,9 @@ - - true - + + true + @@ -50,10 +71,10 @@ - - true - - + + true + + @@ -104,16 +125,16 @@ - - true - - + + true + + - + diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index ebc5556c6..f691a185d 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -38,8 +38,8 @@ #include "BuildConfig.h" #include "Json.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" +#include "modplatform/modrinth/ModrinthAPI.h" +#include "net/NetJob.h" #include "ui/widgets/ProjectItem.h" #include "net/ApiDownload.h" @@ -130,7 +130,24 @@ bool ModpackListModel::setData(const QModelIndex& index, const QVariant& value, void ModpackListModel::performPaginatedSearch() { - // TODO: Move to standalone API + if (hasActiveSearchJob()) + return; + + if (currentSearchTerm.startsWith("#")) { + auto projectId = currentSearchTerm.mid(1); + if (!projectId.isEmpty()) { + ResourceAPI::ProjectInfoCallbacks callbacks; + + callbacks.on_fail = [this](QString reason) { searchRequestFailed(reason); }; + callbacks.on_succeed = [this](auto& doc, auto& pack) { searchRequestForOneSucceeded(doc); }; + static const ModrinthAPI api; + if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) { + jobPtr = job; + jobPtr->start(); + } + return; + } + } // TODO: Move to standalone API auto netJob = makeShared("Modrinth::SearchModpack", APPLICATION->network()); auto searchAllUrl = QString(BuildConfig.MODRINTH_PROD_URL + "/search?" @@ -167,16 +184,17 @@ void ModpackListModel::performPaginatedSearch() void ModpackListModel::refresh() { - if (jobPtr) { + if (hasActiveSearchJob()) { jobPtr->abort(); searchState = ResetRequested; return; - } else { - beginResetModel(); - modpacks.clear(); - endResetModel(); - searchState = None; } + + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + nextSearchOffset = 0; performPaginatedSearch(); } @@ -307,9 +325,29 @@ void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all) endInsertRows(); } +void ModpackListModel::searchRequestForOneSucceeded(QJsonDocument& doc) +{ + jobPtr.reset(); + + auto packObj = doc.object(); + + Modrinth::Modpack pack; + try { + Modrinth::loadIndexedPack(pack, packObj); + pack.id = Json::ensureString(packObj, "id", pack.id); + } catch (const JSONValidationError& e) { + qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause(); + return; + } + + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + 1); + modpacks.append({ pack }); + endInsertRows(); +} + void ModpackListModel::searchRequestFailed(QString reason) { - auto failed_action = jobPtr->getFailedActions().at(0); + auto failed_action = dynamic_cast(jobPtr.get())->getFailedActions().at(0); if (!failed_action->m_reply) { // Network error QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load modpacks.")); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 721c69f55..2a9d62261 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -73,6 +73,9 @@ class ModpackListModel : public QAbstractListModel { void refresh(); void searchWithTerm(const QString& term, const int sort); + [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } + [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } + void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); inline auto canFetchMore(const QModelIndex& parent) const -> bool override @@ -83,6 +86,7 @@ class ModpackListModel : public QAbstractListModel { public slots: void searchRequestFinished(QJsonDocument& doc_all); void searchRequestFailed(QString reason); + void searchRequestForOneSucceeded(QJsonDocument&); protected slots: @@ -111,7 +115,7 @@ class ModpackListModel : public QAbstractListModel { int nextSearchOffset = 0; enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; - NetJob::Ptr jobPtr; + Task::Ptr jobPtr; std::shared_ptr m_all_response = std::make_shared(); QByteArray m_specific_response; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 41fd5003f..f7fa8fd78 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -52,7 +52,8 @@ #include #include -ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog) +ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) + : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog), m_fetch_progress(this, false) { ui->setupUi(this); @@ -64,6 +65,17 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); + m_search_timer.setSingleShot(true); + + connect(&m_search_timer, &QTimer::timeout, this, &ModrinthPage::triggerSearch); + + m_fetch_progress.hideIfInactive(true); + m_fetch_progress.setFixedHeight(24); + m_fetch_progress.progressFormat(""); + + ui->gridLayout->addWidget(&m_fetch_progress, 2, 0, 1, ui->gridLayout->columnCount()); + ui->sortByBox->addItem(tr("Sort by Relevance")); ui->sortByBox->addItem(tr("Sort by Total Downloads")); ui->sortByBox->addItem(tr("Sort by Follows")); @@ -102,6 +114,11 @@ bool ModrinthPage::eventFilter(QObject* watched, QEvent* event) this->triggerSearch(); keyEvent->accept(); return true; + } else { + if (m_search_timer.isActive()) + m_search_timer.stop(); + + m_search_timer.start(350); } } return QObject::eventFilter(watched, event); @@ -309,6 +326,7 @@ void ModrinthPage::suggestCurrent() void ModrinthPage::triggerSearch() { m_model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); + m_fetch_progress.watch(m_model->activeSearchJob().get()); } void ModrinthPage::onVersionSelectionChanged(QString version) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index b7054c886..4240dcafb 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -41,7 +41,9 @@ #include "ui/pages/BasePage.h" #include "modplatform/modrinth/ModrinthPackManifest.h" +#include "ui/widgets/ProgressWidget.h" +#include #include namespace Ui { @@ -88,4 +90,9 @@ class ModrinthPage : public QWidget, public BasePage { Modrinth::Modpack current; QString selectedVersion; + + ProgressWidget m_fetch_progress; + + // Used to do instant searching with a delay to cache quick changes + QTimer m_search_timer; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index 6d8b2b675..78a25feae 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -10,8 +10,8 @@ 600 - - + + @@ -29,7 +29,7 @@ - + @@ -47,7 +47,7 @@ - + @@ -77,7 +77,7 @@ - + diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp index ce7475b6e..856018294 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp @@ -39,12 +39,12 @@ void ModrinthModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObjec void ModrinthModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); + ::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); } auto ModrinthModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion { - return ::Modrinth::loadDependencyVersions(m, arr); + return ::Modrinth::loadDependencyVersions(m, arr, &m_base_instance); } auto ModrinthModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray @@ -66,7 +66,7 @@ void ModrinthResourcePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, Q void ModrinthResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); + ::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); } auto ModrinthResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray @@ -88,7 +88,7 @@ void ModrinthTexturePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJ void ModrinthTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); + ::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); } auto ModrinthTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray @@ -110,7 +110,7 @@ void ModrinthShaderPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJs void ModrinthShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); + ::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); } auto ModrinthShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index 616c1a815..a4197b225 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -65,21 +65,9 @@ ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instan auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, - std::optional loaders) const -> bool + std::optional loaders) const -> bool { - auto loaderCompatible = !loaders.has_value(); - - if (!loaderCompatible) { - auto loaderStrings = ModrinthAPI::getModLoaderStrings(loaders.value()); - for (auto remoteLoader : ver.loaders) { - if (loaderStrings.contains(remoteLoader)) { - loaderCompatible = true; - break; - } - } - } - - return ver.mcVersion.contains(mineVer) && loaderCompatible; + return ver.mcVersion.contains(mineVer) && (!loaders.has_value() || !ver.loaders || loaders.value() & ver.loaders); } ModrinthResourcePackPage::ModrinthResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h index 86ba1ccb2..311bcfe32 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -93,7 +93,7 @@ class ModrinthModPage : public ModPage { [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } - auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional loaders = {}) const + auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional loaders = {}) const -> bool override; }; diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index e8c5ac922..3cd1d9a2d 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -39,6 +39,7 @@ #include "Json.h" #include "net/ApiDownload.h" +#include "ui/widgets/ProjectItem.h" #include @@ -54,21 +55,47 @@ QVariant Technic::ListModel::data(const QModelIndex& index, int role) const } Modpack pack = modpacks.at(pos); - if (role == Qt::DisplayRole) { - return pack.name; - } else if (role == Qt::DecorationRole) { - if (m_logoMap.contains(pack.logoName)) { - return (m_logoMap.value(pack.logoName)); + switch (role) { + case 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("
")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + } + return pack.description; } - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); - return icon; - } else if (role == Qt::UserRole) { - QVariant v; - v.setValue(pack); - return v; + case 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); + return icon; + } + case Qt::UserRole: { + QVariant v; + v.setValue(pack); + return v; + } + case Qt::DisplayRole: + return pack.name; + case Qt::SizeHintRole: + return QSize(0, 58); + // Custom data + case UserDataTypes::TITLE: + return pack.name; + case UserDataTypes::DESCRIPTION: + return pack.description; + case UserDataTypes::SELECTED: + return false; + case UserDataTypes::INSTALLED: + return false; + default: + break; } - return QVariant(); + + return {}; } int Technic::ListModel::columnCount(const QModelIndex& parent) const @@ -87,21 +114,25 @@ void Technic::ListModel::searchWithTerm(const QString& term) return; } currentSearchTerm = term; - if (jobPtr) { + if (hasActiveSearchJob()) { jobPtr->abort(); searchState = ResetRequested; return; - } else { - beginResetModel(); - modpacks.clear(); - endResetModel(); - searchState = None; } + + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + performSearch(); } void Technic::ListModel::performSearch() { + if (hasActiveSearchJob()) + return; + auto netJob = makeShared("Technic::Search", APPLICATION->network()); QString searchUrl = ""; if (currentSearchTerm.isEmpty()) { @@ -113,6 +144,9 @@ void Technic::ListModel::performSearch() } else if (currentSearchTerm.startsWith("https://api.technicpack.net/modpack/")) { searchUrl = QString("%1?build=%2").arg(currentSearchTerm, BuildConfig.TECHNIC_API_BUILD); searchMode = Single; + } else if (currentSearchTerm.startsWith("#")) { + searchUrl = QString("https://api.technicpack.net/modpack/%1?build=%2").arg(currentSearchTerm.mid(1), BuildConfig.TECHNIC_API_BUILD); + searchMode = Single; } else { searchUrl = QString("%1search?build=%2&q=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm); diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.h b/launcher/ui/pages/modplatform/technic/TechnicModel.h index d7a635d41..aeb4f3084 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.h +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.h @@ -58,6 +58,9 @@ class ListModel : public QAbstractListModel { void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); void searchWithTerm(const QString& term); + [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } + [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } + private slots: void searchRequestFinished(); void searchRequestFailed(); diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 54b86feba..190b7c68f 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -34,6 +34,7 @@ */ #include "TechnicPage.h" +#include "ui/widgets/ProjectItem.h" #include "ui_TechnicPage.h" #include @@ -51,7 +52,8 @@ #include "net/ApiDownload.h" -TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog) +TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget* parent) + : QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog), m_fetch_progress(this, false) { ui->setupUi(this); connect(ui->searchButton, &QPushButton::clicked, this, &TechnicPage::triggerSearch); @@ -59,8 +61,21 @@ TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(p model = new Technic::ListModel(this); ui->packView->setModel(model); + m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); + m_search_timer.setSingleShot(true); + + connect(&m_search_timer, &QTimer::timeout, this, &TechnicPage::triggerSearch); + + m_fetch_progress.hideIfInactive(true); + m_fetch_progress.setFixedHeight(24); + m_fetch_progress.progressFormat(""); + + ui->gridLayout->addWidget(&m_fetch_progress, 2, 0, 1, ui->gridLayout->columnCount()); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged); connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &TechnicPage::onVersionSelectionChanged); + + ui->packView->setItemDelegate(new ProjectItemDelegate(this)); } bool TechnicPage::eventFilter(QObject* watched, QEvent* event) @@ -71,6 +86,11 @@ bool TechnicPage::eventFilter(QObject* watched, QEvent* event) triggerSearch(); keyEvent->accept(); return true; + } else { + if (m_search_timer.isActive()) + m_search_timer.stop(); + + m_search_timer.start(350); } } return QWidget::eventFilter(watched, event); @@ -100,6 +120,7 @@ void TechnicPage::openedImpl() void TechnicPage::triggerSearch() { model->searchWithTerm(ui->searchEdit->text()); + m_fetch_progress.watch(model->activeSearchJob().get()); } void TechnicPage::onSelectionChanged(QModelIndex first, [[maybe_unused]] QModelIndex second) diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index 91b61eaf2..01439337d 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -35,13 +35,14 @@ #pragma once +#include #include #include #include "TechnicData.h" #include "net/NetJob.h" -#include "tasks/Task.h" #include "ui/pages/BasePage.h" +#include "ui/widgets/ProgressWidget.h" namespace Ui { class TechnicPage; @@ -91,4 +92,9 @@ class TechnicPage : public QWidget, public BasePage { NetJob::Ptr jobPtr; std::shared_ptr response = std::make_shared(); + + ProgressWidget m_fetch_progress; + + // Used to do instant searching with a delay to cache quick changes + QTimer m_search_timer; }; diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.ui b/launcher/ui/pages/modplatform/technic/TechnicPage.ui index 15bf645fb..b988eda2b 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.ui +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.ui @@ -11,7 +11,7 @@ - + @@ -44,7 +44,7 @@ - + diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index 42279a663..d2d921918 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -186,12 +186,20 @@ QString JavaSettingsWidget::javaPath() const int JavaSettingsWidget::maxHeapSize() const { - return m_maxMemSpinBox->value(); + auto min = m_minMemSpinBox->value(); + auto max = m_maxMemSpinBox->value(); + if (max < min) + max = min; + return max; } int JavaSettingsWidget::minHeapSize() const { - return m_minMemSpinBox->value(); + auto min = m_minMemSpinBox->value(); + auto max = m_maxMemSpinBox->value(); + if (min > max) + min = max; + return min; } bool JavaSettingsWidget::permGenEnabled() const @@ -214,17 +222,9 @@ void JavaSettingsWidget::memoryValueChanged(int) if (obj == m_minMemSpinBox && min != observedMinMemory) { observedMinMemory = min; actuallyChanged = true; - if (min > max) { - observedMaxMemory = min; - m_maxMemSpinBox->setValue(min); - } } else if (obj == m_maxMemSpinBox && max != observedMaxMemory) { observedMaxMemory = max; actuallyChanged = true; - if (min > max) { - observedMinMemory = max; - m_minMemSpinBox->setValue(max); - } } else if (obj == m_permGenSpinBox && permgen != observedPermGenMemory) { observedPermGenMemory = permgen; actuallyChanged = true; @@ -361,8 +361,8 @@ void JavaSettingsWidget::checkJavaPath(const QString& path) setJavaStatus(JavaStatus::Pending); m_checker.reset(new JavaChecker()); m_checker->m_path = path; - m_checker->m_minMem = m_minMemSpinBox->value(); - m_checker->m_maxMem = m_maxMemSpinBox->value(); + m_checker->m_minMem = minHeapSize(); + m_checker->m_maxMem = maxHeapSize(); if (m_permGenSpinBox->isVisible()) { m_checker->m_permGen = m_permGenSpinBox->value(); } @@ -415,6 +415,9 @@ void JavaSettingsWidget::updateThresholds() } else if (observedMaxMemory > (m_availableMemory * 0.9)) { iconName = "status-yellow"; m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); + } else if (observedMaxMemory < observedMinMemory) { + iconName = "status-yellow"; + m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value")); } else { iconName = "status-good"; m_labelMaxMemIcon->setToolTip(""); diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index 1481c1b6b..60b92b28b 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -34,8 +34,8 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o icon_width = icon_size.width(); icon_height = icon_size.height(); - icon_x_margin = (rect.height() - icon_width) / 2; icon_y_margin = (rect.height() - icon_height) / 2; + icon_x_margin = icon_y_margin; // use same margins for consistency } // Centralize icon with a margin to separate from the other elements diff --git a/libraries/LocalPeer/src/LocalPeer.cpp b/libraries/LocalPeer/src/LocalPeer.cpp index ab528c2a1..bd407042f 100644 --- a/libraries/LocalPeer/src/LocalPeer.cpp +++ b/libraries/LocalPeer/src/LocalPeer.cpp @@ -220,5 +220,5 @@ void LocalPeer::receiveConnection() socket->waitForBytesWritten(1000); socket->waitForDisconnected(1000); // make sure client reads ack delete socket; - emit messageReceived(uMsg); //### (might take a long time to return) + emit messageReceived(uMsg); // ### (might take a long time to return) } diff --git a/nix/default.nix b/nix/default.nix deleted file mode 100644 index 47172927a..000000000 --- a/nix/default.nix +++ /dev/null @@ -1,32 +0,0 @@ -{ - inputs, - self, - ... -}: { - imports = [ - ./dev.nix - ./distribution.nix - ]; - - _module.args = { - # User-friendly version number. - version = builtins.substring 0 8 self.lastModifiedDate; - }; - - perSystem = {system, ...}: { - # Nixpkgs instantiated for supported systems with our overlay. - _module.args.pkgs = import inputs.nixpkgs { - inherit system; - overlays = [self.overlays.default]; - }; - }; - - # Supported systems. - systems = [ - "x86_64-linux" - "aarch64-linux" - # Disabled due to our packages not supporting darwin yet. - # "x86_64-darwin" - # "aarch64-darwin" - ]; -} diff --git a/nix/dev.nix b/nix/dev.nix index c39e15653..c476ed10f 100644 --- a/nix/dev.nix +++ b/nix/dev.nix @@ -1,42 +1,33 @@ { - inputs, - self, - ... -}: { perSystem = { - system, + config, + lib, pkgs, ... }: { - checks = { - pre-commit-check = inputs.pre-commit-hooks.lib.${system}.run { - src = self; - hooks = { - markdownlint.enable = true; + pre-commit.settings = { + hooks = { + markdownlint.enable = true; - alejandra.enable = true; - deadnix.enable = true; - nil.enable = true; + alejandra.enable = true; + deadnix.enable = true; + nil.enable = true; - clang-format = { - enable = true; - types_or = ["c" "c++" "java" "json" "objective-c"]; - }; + clang-format = { + enable = true; + types_or = ["c" "c++" "java" "json" "objective-c"]; }; }; + + tools.clang-tools = lib.mkForce pkgs.clang-tools_16; }; devShells.default = pkgs.mkShell { - inherit (self.checks.${system}.pre-commit-check) shellHook; - packages = with pkgs; [ - nodePackages.markdownlint-cli - alejandra - deadnix - clang-tools - nil - ]; + shellHook = '' + ${config.pre-commit.installationScript} + ''; - inputsFrom = [self.packages.${system}.prismlauncher-unwrapped]; + inputsFrom = [config.packages.prismlauncher-unwrapped]; buildInputs = with pkgs; [ccache ninja]; }; diff --git a/nix/distribution.nix b/nix/distribution.nix index 0f2e26f3e..6b93d355f 100644 --- a/nix/distribution.nix +++ b/nix/distribution.nix @@ -1,29 +1,44 @@ { inputs, self, - version, ... }: { - perSystem = {pkgs, ...}: { - packages = { - inherit (pkgs) prismlauncher-qt5-unwrapped prismlauncher-qt5 prismlauncher-unwrapped prismlauncher; - default = pkgs.prismlauncher; + perSystem = { + lib, + pkgs, + ... + }: { + packages = let + ourPackages = lib.fix (final: self.overlays.default ({inherit (pkgs) darwin;} // final) pkgs); + in { + inherit + (ourPackages) + prismlauncher-qt5-unwrapped + prismlauncher-qt5 + prismlauncher-unwrapped + prismlauncher + ; + default = ourPackages.prismlauncher; }; }; flake = { overlays.default = final: prev: let - # Helper function to build prism against different versions of Qt. - mkPrism = qt: - qt.callPackage ./package.nix { - inherit (inputs) libnbtplusplus; - inherit self version; - }; + version = builtins.substring 0 8 self.lastModifiedDate or "dirty"; + + # common args for prismlauncher evaluations + unwrappedArgs = { + inherit (inputs) libnbtplusplus; + inherit (final.darwin.apple_sdk.frameworks) Cocoa; + inherit self version; + }; in { - prismlauncher-qt5-unwrapped = mkPrism final.libsForQt5; - prismlauncher-qt5 = prev.prismlauncher-qt5.override {prismlauncher-unwrapped = final.prismlauncher-qt5-unwrapped;}; - prismlauncher-unwrapped = mkPrism final.qt6Packages; - prismlauncher = prev.prismlauncher.override {inherit (final) prismlauncher-unwrapped;}; + prismlauncher-qt5-unwrapped = prev.libsForQt5.callPackage ./pkg unwrappedArgs; + prismlauncher-qt5 = prev.libsForQt5.callPackage ./pkg/wrapper.nix { + prismlauncher-unwrapped = final.prismlauncher-qt5-unwrapped; + }; + prismlauncher-unwrapped = prev.qt6Packages.callPackage ./pkg unwrappedArgs; + prismlauncher = prev.qt6Packages.callPackage ./pkg/wrapper.nix {inherit (final) prismlauncher-unwrapped;}; }; }; } diff --git a/nix/package.nix b/nix/package.nix deleted file mode 100644 index edc266dc4..000000000 --- a/nix/package.nix +++ /dev/null @@ -1,65 +0,0 @@ -{ - lib, - stdenv, - cmake, - ninja, - jdk17, - zlib, - qtbase, - quazip, - extra-cmake-modules, - tomlplusplus, - cmark, - ghc_filesystem, - gamemode, - msaClientID ? null, - gamemodeSupport ? true, - self, - version, - libnbtplusplus, -}: -stdenv.mkDerivation rec { - pname = "prismlauncher-unwrapped"; - inherit version; - - src = lib.cleanSource self; - - nativeBuildInputs = [extra-cmake-modules cmake jdk17 ninja]; - buildInputs = - [ - qtbase - zlib - quazip - ghc_filesystem - tomlplusplus - cmark - ] - ++ lib.optional gamemodeSupport gamemode; - - hardeningEnable = ["pie"]; - - cmakeFlags = - lib.optionals (msaClientID != null) ["-DLauncher_MSA_CLIENT_ID=${msaClientID}"] - ++ lib.optionals (lib.versionOlder qtbase.version "6") ["-DLauncher_QT_VERSION_MAJOR=5"]; - - postUnpack = '' - rm -rf source/libraries/libnbtplusplus - ln -s ${libnbtplusplus} source/libraries/libnbtplusplus - ''; - - dontWrapQtApps = true; - - meta = with lib; { - homepage = "https://prismlauncher.org/"; - description = "A free, open source launcher for Minecraft"; - longDescription = '' - Allows you to have multiple, separate instances of Minecraft (each with - their own mods, texture packs, saves, etc) and helps you manage them and - their associated options with a simple interface. - ''; - platforms = platforms.linux; - changelog = "https://github.com/PrismLauncher/PrismLauncher/releases/tag/${version}"; - license = licenses.gpl3Only; - maintainers = with maintainers; [minion3665 Scrumplex]; - }; -} diff --git a/nix/pkg/default.nix b/nix/pkg/default.nix new file mode 100644 index 000000000..074214c4b --- /dev/null +++ b/nix/pkg/default.nix @@ -0,0 +1,73 @@ +{ + lib, + stdenv, + canonicalize-jars-hook, + cmake, + cmark, + Cocoa, + ninja, + jdk17, + zlib, + qtbase, + quazip, + extra-cmake-modules, + tomlplusplus, + ghc_filesystem, + gamemode, + msaClientID ? null, + gamemodeSupport ? stdenv.isLinux, + self, + version, + libnbtplusplus, +}: +assert lib.assertMsg (stdenv.isLinux || !gamemodeSupport) "gamemodeSupport is only available on Linux"; + stdenv.mkDerivation rec { + pname = "prismlauncher-unwrapped"; + inherit version; + + src = lib.cleanSource self; + + nativeBuildInputs = [extra-cmake-modules cmake jdk17 ninja canonicalize-jars-hook]; + buildInputs = + [ + qtbase + zlib + quazip + ghc_filesystem + tomlplusplus + cmark + ] + ++ lib.optional gamemodeSupport gamemode + ++ lib.optionals stdenv.isDarwin [Cocoa]; + + hardeningEnable = lib.optionals stdenv.isLinux ["pie"]; + + cmakeFlags = + [ + "-DLauncher_BUILD_PLATFORM=nixpkgs" + ] + ++ lib.optionals (msaClientID != null) ["-DLauncher_MSA_CLIENT_ID=${msaClientID}"] + ++ lib.optionals (lib.versionOlder qtbase.version "6") ["-DLauncher_QT_VERSION_MAJOR=5"] + ++ lib.optionals stdenv.isDarwin ["-DINSTALL_BUNDLE=nodeps" "-DMACOSX_SPARKLE_UPDATE_FEED_URL=''"]; + + postUnpack = '' + rm -rf source/libraries/libnbtplusplus + ln -s ${libnbtplusplus} source/libraries/libnbtplusplus + ''; + + dontWrapQtApps = true; + + meta = with lib; { + homepage = "https://prismlauncher.org/"; + description = "A free, open source launcher for Minecraft"; + longDescription = '' + Allows you to have multiple, separate instances of Minecraft (each with + their own mods, texture packs, saves, etc) and helps you manage them and + their associated options with a simple interface. + ''; + platforms = with platforms; linux ++ darwin; + changelog = "https://github.com/PrismLauncher/PrismLauncher/releases/tag/${version}"; + license = licenses.gpl3Only; + maintainers = with maintainers; [minion3665 Scrumplex getchoo]; + }; + } diff --git a/nix/pkg/wrapper.nix b/nix/pkg/wrapper.nix new file mode 100644 index 000000000..8d160143d --- /dev/null +++ b/nix/pkg/wrapper.nix @@ -0,0 +1,91 @@ +{ + lib, + stdenv, + symlinkJoin, + prismlauncher-unwrapped, + wrapQtAppsHook, + qtbase, # needed for wrapQtAppsHook + qtsvg, + qtwayland, + xorg, + libpulseaudio, + libGL, + glfw, + openal, + jdk8, + jdk17, + gamemode, + flite, + mesa-demos, + udev, + msaClientID ? null, + gamemodeSupport ? stdenv.isLinux, + textToSpeechSupport ? stdenv.isLinux, + jdks ? [jdk17 jdk8], + additionalLibs ? [], + additionalPrograms ? [], +}: let + prismlauncherFinal = prismlauncher-unwrapped.override { + inherit msaClientID gamemodeSupport; + }; +in + symlinkJoin { + name = "prismlauncher-${prismlauncherFinal.version}"; + + paths = [prismlauncherFinal]; + + nativeBuildInputs = [ + wrapQtAppsHook + ]; + + buildInputs = + [ + qtbase + qtsvg + ] + ++ lib.optional (lib.versionAtLeast qtbase.version "6" && stdenv.isLinux) qtwayland; + + postBuild = '' + wrapQtAppsHook + ''; + + qtWrapperArgs = let + runtimeLibs = + (with xorg; [ + libX11 + libXext + libXcursor + libXrandr + libXxf86vm + ]) + ++ [ + # lwjgl + libpulseaudio + libGL + glfw + openal + stdenv.cc.cc.lib + + # oshi + udev + ] + ++ lib.optional gamemodeSupport gamemode.lib + ++ lib.optional textToSpeechSupport flite + ++ additionalLibs; + + runtimePrograms = + [ + xorg.xrandr + mesa-demos # need glxinfo + ] + ++ additionalPrograms; + in + ["--prefix PRISMLAUNCHER_JAVA_PATHS : ${lib.makeSearchPath "bin/java" jdks}"] + ++ lib.optionals stdenv.isLinux [ + "--set LD_LIBRARY_PATH /run/opengl-driver/lib:${lib.makeLibraryPath runtimeLibs}" + # xorg.xrandr needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128 + "--prefix PATH : ${lib.makeBinPath runtimePrograms}" + ]; + + inherit (prismlauncherFinal) meta; + } diff --git a/program_info/org.prismlauncher.PrismLauncher.desktop.in b/program_info/org.prismlauncher.PrismLauncher.desktop.in index 816c00595..76f4b19c0 100644 --- a/program_info/org.prismlauncher.PrismLauncher.desktop.in +++ b/program_info/org.prismlauncher.PrismLauncher.desktop.in @@ -4,7 +4,7 @@ Name=Prism Launcher Comment=A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once. Type=Application Terminal=false -Exec=@Launcher_APP_BINARY_NAME@ +Exec=@Launcher_APP_BINARY_NAME@ %U StartupNotify=true Icon=org.prismlauncher.PrismLauncher Categories=Game;ActionGame;AdventureGame;Simulation; diff --git a/renovate.json b/renovate.json index 71581e3b0..f9c2c3270 100644 --- a/renovate.json +++ b/renovate.json @@ -2,11 +2,5 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:base" - ], - "nix": { - "enabled": true - }, - "lockFileMaintenance": { - "enabled": true - } + ] } diff --git a/tests/FileSystem_test.cpp b/tests/FileSystem_test.cpp index e19410423..1d3cee85f 100644 --- a/tests/FileSystem_test.cpp +++ b/tests/FileSystem_test.cpp @@ -353,10 +353,7 @@ class FileSystemTest : public QObject { } } - void test_getDesktop() - { - QCOMPARE(FS::getDesktopDir(), QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); - } + void test_getDesktop() { QCOMPARE(FS::getDesktopDir(), QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); } void test_link() {