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

This commit is contained in:
Trial97 2024-06-28 20:34:59 +03:00
commit 55f740453c
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
173 changed files with 2482 additions and 1171 deletions

View File

@ -79,7 +79,7 @@ jobs:
qt_ver: 6
qt_host: windows
qt_arch: ""
qt_version: "6.7.0"
qt_version: "6.7.1"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: windows-2022
@ -90,7 +90,7 @@ jobs:
qt_ver: 6
qt_host: windows
qt_arch: "win64_msvc2019_arm64"
qt_version: "6.7.0"
qt_version: "6.7.1"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: macos-12
@ -99,7 +99,7 @@ jobs:
qt_ver: 6
qt_host: mac
qt_arch: ""
qt_version: "6.7.0"
qt_version: "6.7.1"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: macos-12

View File

@ -19,7 +19,7 @@ jobs:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27
- uses: DeterminateSystems/update-flake-lock@v21
- uses: DeterminateSystems/update-flake-lock@v22
with:
commit-msg: "chore(nix): update lockfile"
pr-title: "chore(nix): update lockfile"

12
flake.lock generated
View File

@ -75,16 +75,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1717774105,
"narHash": "sha256-HV97wqUQv9wvptiHCb3Y0/YH0lJ60uZ8FYfEOIzYEqI=",
"owner": "nixos",
"lastModified": 1718276985,
"narHash": "sha256-u1fA0DYQYdeG+5kDm1bOoGcHtX0rtC7qs2YA2N1X++I=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d226935fd75012939397c83f6c385e4d6d832288",
"rev": "3f84a279f1a6290ce154c5531378acc827836fbb",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}

View File

@ -7,7 +7,7 @@
};
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-parts = {
url = "github:hercules-ci/flake-parts";
inputs.nixpkgs-lib.follows = "nixpkgs";

View File

@ -64,7 +64,8 @@ modules:
config-opts:
- -DCMAKE_BUILD_TYPE=RelWithDebInfo
- -DBUILD_SHARED_LIBS:BOOL=ON
- -DGLFW_USE_WAYLAND=ON
- -DGLFW_USE_WAYLAND:BOOL=ON
- -DGLFW_BUILD_DOCS:BOOL=OFF
sources:
- type: git
url: https://github.com/glfw/glfw.git

View File

@ -847,24 +847,17 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
{
m_metacache.reset(new HttpMetaCache("metacache"));
m_metacache->addBase("asset_indexes", QDir("assets/indexes").absolutePath());
m_metacache->addBase("asset_objects", QDir("assets/objects").absolutePath());
m_metacache->addBase("versions", QDir("versions").absolutePath());
m_metacache->addBase("libraries", QDir("libraries").absolutePath());
m_metacache->addBase("minecraftforge", QDir("mods/minecraftforge").absolutePath());
m_metacache->addBase("fmllibs", QDir("mods/minecraftforge/libs").absolutePath());
m_metacache->addBase("liteloader", QDir("mods/liteloader").absolutePath());
m_metacache->addBase("general", QDir("cache").absolutePath());
m_metacache->addBase("ATLauncherPacks", QDir("cache/ATLauncherPacks").absolutePath());
m_metacache->addBase("FTBPacks", QDir("cache/FTBPacks").absolutePath());
m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath());
m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath());
m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").absolutePath());
m_metacache->addBase("FlameMods", QDir("cache/FlameMods").absolutePath());
m_metacache->addBase("ModrinthPacks", QDir("cache/ModrinthPacks").absolutePath());
m_metacache->addBase("ModrinthModpacks", QDir("cache/ModrinthModpacks").absolutePath());
m_metacache->addBase("root", QDir::currentPath());
m_metacache->addBase("translations", QDir("translations").absolutePath());
m_metacache->addBase("icons", QDir("cache/icons").absolutePath());
m_metacache->addBase("meta", QDir("meta").absolutePath());
m_metacache->Load();
qDebug() << "<> Cache initialized.";
@ -945,8 +938,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
[[fallthrough]];
default: {
qDebug() << "Exiting because update lockfile is present";
QMetaObject::invokeMethod(
this, []() { exit(1); }, Qt::QueuedConnection);
QMetaObject::invokeMethod(this, []() { exit(1); }, Qt::QueuedConnection);
return;
}
}
@ -978,8 +970,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
[[fallthrough]];
default: {
qDebug() << "Exiting because update lockfile is present";
QMetaObject::invokeMethod(
this, []() { exit(1); }, Qt::QueuedConnection);
QMetaObject::invokeMethod(this, []() { exit(1); }, Qt::QueuedConnection);
return;
}
}
@ -991,7 +982,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
"\n"
"You are now running %1 .\n"
"Check the Prism Launcher updater log at: \n"
"%1\n"
"%2\n"
"for details.")
.arg(BuildConfig.printableVersionString())
.arg(update_log_path);
@ -1673,8 +1664,7 @@ QString Application::getJarPath(QString jarFile)
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME),
#endif
FS::PathCombine(m_rootPath, "jars"),
FS::PathCombine(applicationDirPath(), "jars"),
FS::PathCombine(m_rootPath, "jars"), FS::PathCombine(applicationDirPath(), "jars"),
FS::PathCombine(applicationDirPath(), "..", "jars") // from inside build dir, for debuging
};
for (QString p : potentialPaths) {

View File

@ -269,13 +269,18 @@ void BaseInstance::setRunning(bool running)
m_isRunning = running;
if (!m_settings->get("RecordGameTime").toBool()) {
emit runningStatusChanged(running);
}
void BaseInstance::setMinecraftRunning(bool running)
{
if (!settings()->get("RecordGameTime").toBool()) {
return;
}
if (running) {
m_timeStarted = QDateTime::currentDateTime();
setLastLaunch(m_timeStarted.toMSecsSinceEpoch());
} else {
QDateTime timeEnded = QDateTime::currentDateTime();
@ -285,8 +290,6 @@ void BaseInstance::setRunning(bool running)
emit propertiesChanged(this);
}
emit runningStatusChanged(running);
}
int64_t BaseInstance::totalTimePlayed() const

View File

@ -104,6 +104,7 @@ class BaseInstance : public QObject, public std::enable_shared_from_this<BaseIns
/// be unique.
virtual QString id() const;
void setMinecraftRunning(bool running);
void setRunning(bool running);
bool isRunning() const;
int64_t totalTimePlayed() const;

View File

@ -163,6 +163,8 @@ set(LAUNCH_SOURCES
launch/steps/Update.h
launch/steps/QuitAfterGameStop.cpp
launch/steps/QuitAfterGameStop.h
launch/steps/PrintServers.cpp
launch/steps/PrintServers.h
launch/LaunchStep.cpp
launch/LaunchStep.h
launch/LaunchTask.cpp
@ -1034,6 +1036,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/skins/SkinManageDialog.h
# GUI - widgets
ui/widgets/CheckComboBox.cpp
ui/widgets/CheckComboBox.h
ui/widgets/Common.cpp
ui/widgets/Common.h
ui/widgets/CustomCommands.cpp

View File

@ -815,25 +815,68 @@ QString NormalizePath(QString path)
}
}
static const QString BAD_PATH_CHARS = "\"?<>:;*|!+\r\n";
static const QString BAD_FILENAME_CHARS = BAD_PATH_CHARS + "\\/";
static const QString BAD_WIN_CHARS = "<>:\"|?*\r\n";
static const QString BAD_NTFS_CHARS = "<>:\"|?*";
static const QString BAD_HFS_CHARS = ":";
static const QString BAD_FILENAME_CHARS = BAD_WIN_CHARS + "\\/";
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith)
{
for (int i = 0; i < string.length(); i++)
if (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i)))
string[i] = replaceWith;
return string;
}
QString RemoveInvalidPathChars(QString string, QChar replaceWith)
QString RemoveInvalidPathChars(QString path, QChar replaceWith)
{
for (int i = 0; i < string.length(); i++)
if (string.at(i) < ' ' || BAD_PATH_CHARS.contains(string.at(i)))
string[i] = replaceWith;
QString invalidChars;
#ifdef Q_OS_WIN
invalidChars = BAD_WIN_CHARS;
#endif
return string;
// the null character is ignored in this check as it was not a problem until now
switch (statFS(path).fsType) {
case FilesystemType::FAT: // similar to NTFS
/* fallthrough */
case FilesystemType::NTFS:
/* fallthrough */
case FilesystemType::REFS: // similar to NTFS(should be available only on windows)
invalidChars += BAD_NTFS_CHARS;
break;
// case FilesystemType::EXT:
// case FilesystemType::EXT_2_OLD:
// case FilesystemType::EXT_2_3_4:
// case FilesystemType::XFS:
// case FilesystemType::BTRFS:
// case FilesystemType::NFS:
// case FilesystemType::ZFS:
case FilesystemType::APFS:
/* fallthrough */
case FilesystemType::HFS:
/* fallthrough */
case FilesystemType::HFSPLUS:
/* fallthrough */
case FilesystemType::HFSX:
invalidChars += BAD_HFS_CHARS;
break;
// case FilesystemType::FUSEBLK:
// case FilesystemType::F2FS:
// case FilesystemType::UNKNOWN:
default:
break;
}
if (invalidChars.size() != 0) {
for (int i = 0; i < path.length(); i++) {
if (path.at(i) < ' ' || invalidChars.contains(path.at(i))) {
path[i] = replaceWith;
}
}
}
return path;
}
QString DirNameFromString(QString string, QString inDir)

View File

@ -36,6 +36,7 @@
#include "LaunchController.h"
#include "Application.h"
#include "launch/steps/PrintServers.h"
#include "minecraft/auth/AccountData.h"
#include "minecraft/auth/AccountList.h"
@ -84,7 +85,7 @@ void LaunchController::decideAccount()
// Find an account to use.
auto accounts = APPLICATION->accounts();
if (accounts->count() <= 0) {
if (accounts->count() <= 0 || !accounts->anyAccountIsValid()) {
// Tell the user they need to log in at least one account in order to play.
auto reply = CustomMessageBox::selectable(m_parentWidget, tr("No Accounts"),
tr("In order to play Minecraft, you must have at least one Microsoft "
@ -128,12 +129,63 @@ void LaunchController::decideAccount()
}
}
bool LaunchController::askPlayDemo()
{
QMessageBox box(m_parentWidget);
box.setWindowTitle(tr("Play demo?"));
box.setText(
tr("This account does not own Minecraft.\nYou need to purchase the game first to play it.\n\nDo you want to play "
"the demo?"));
box.setIcon(QMessageBox::Warning);
auto demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole);
auto cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole);
box.setDefaultButton(cancelButton);
box.exec();
return box.clickedButton() == demoButton;
}
QString LaunchController::askOfflineName(QString playerName, bool demo, bool& ok)
{
// we ask the user for a player name
QString message = tr("Choose your offline mode player name.");
if (demo) {
message = tr("Choose your demo mode player name.");
}
QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString();
QString usedname = lastOfflinePlayerName.isEmpty() ? playerName : lastOfflinePlayerName;
QString name = QInputDialog::getText(m_parentWidget, tr("Player name"), message, QLineEdit::Normal, usedname, &ok);
if (!ok)
return {};
if (name.length()) {
usedname = name;
APPLICATION->settings()->set("LastOfflinePlayerName", usedname);
}
return usedname;
}
void LaunchController::login()
{
decideAccount();
// if no account is selected, we bail
if (!m_accountToUse) {
// if no account is selected, ask about demo
if (!m_demo) {
m_demo = askPlayDemo();
}
if (m_demo) {
// we ask the user for a player name
bool ok = false;
auto name = askOfflineName("Player", m_demo, ok);
if (ok) {
m_session = std::make_shared<AuthSession>();
m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString().remove(QRegularExpression("[{}-]")));
launchInstance();
return;
}
}
// if no account is selected, we bail
emitFailed(tr("No account selected for launch."));
return;
}
@ -180,24 +232,12 @@ void LaunchController::login()
if (!m_session->wants_online) {
// we ask the user for a player name
bool ok = false;
QString message = tr("Choose your offline mode player name.");
if (m_session->demo) {
message = tr("Choose your demo mode player name.");
}
QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString();
QString usedname = lastOfflinePlayerName.isEmpty() ? m_session->player_name : lastOfflinePlayerName;
QString name = QInputDialog::getText(m_parentWidget, tr("Player name"), message, QLineEdit::Normal, usedname, &ok);
auto name = askOfflineName(m_session->player_name, m_session->demo, ok);
if (!ok) {
tryagain = false;
break;
}
if (name.length()) {
usedname = name;
APPLICATION->settings()->set("LastOfflinePlayerName", usedname);
}
m_session->MakeOffline(usedname);
m_session->MakeOffline(name);
// offline flavored game from here :3
}
if (m_accountToUse->ownsMinecraft()) {
@ -217,20 +257,10 @@ void LaunchController::login()
return;
} else {
// play demo ?
QMessageBox box(m_parentWidget);
box.setWindowTitle(tr("Play demo?"));
box.setText(
tr("This account does not own Minecraft.\nYou need to purchase the game first to play it.\n\nDo you want to play "
"the demo?"));
box.setIcon(QMessageBox::Warning);
auto demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole);
auto cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole);
box.setDefaultButton(cancelButton);
box.exec();
if (box.clickedButton() == demoButton) {
// play demo here
m_session->MakeDemo();
if (!m_session->demo) {
m_session->demo = askPlayDemo();
}
if (m_session->demo) { // play demo here
launchInstance();
} else {
emitFailed(tr("Launch cancelled - account does not own Minecraft."));
@ -316,25 +346,8 @@ void LaunchController::launchInstance()
// Prepend Server Status
QStringList servers = { "login.microsoftonline.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" };
QString resolved_servers = "";
QHostInfo host_info;
for (QString server : servers) {
host_info = QHostInfo::fromName(server);
resolved_servers = resolved_servers + server + " resolves to:\n [";
if (!host_info.addresses().isEmpty()) {
for (QHostAddress address : host_info.addresses()) {
resolved_servers = resolved_servers + address.toString();
if (!host_info.addresses().endsWith(address)) {
resolved_servers = resolved_servers + ", ";
}
}
} else {
resolved_servers = resolved_servers + "N/A";
}
resolved_servers = resolved_servers + "]\n\n";
}
m_launcher->prependStep(makeShared<TextPrint>(m_launcher.get(), resolved_servers, MessageLevel::Launcher));
m_launcher->prependStep(makeShared<PrintServers>(m_launcher.get(), servers));
} else {
online_mode = m_demo ? "demo" : "offline";
}

View File

@ -74,6 +74,8 @@ class LaunchController : public Task {
void login();
void launchInstance();
void decideAccount();
bool askPlayDemo();
QString askOfflineName(QString playerName, bool demo, bool& ok);
private slots:
void readyForLaunch();

View File

@ -289,9 +289,7 @@ std::optional<QStringList> extractSubDir(QuaZip* zip, const QString& subdir, con
do {
QString file_name = zip->getCurrentFileName();
#ifdef Q_OS_WIN
file_name = FS::RemoveInvalidPathChars(file_name);
#endif
if (!file_name.startsWith(subdir))
continue;

View File

@ -213,7 +213,7 @@ class ExtractZipTask : public Task {
{}
virtual ~ExtractZipTask() = default;
typedef std::optional<QString> ZipResult;
using ZipResult = std::optional<QString>;
protected:
virtual void executeTask() override;

View File

@ -79,11 +79,9 @@ QProcessEnvironment CleanEnviroment()
QStringList stripped = {
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
"LD_LIBRARY_PATH",
"LD_PRELOAD",
"LD_LIBRARY_PATH", "LD_PRELOAD",
#endif
"QT_PLUGIN_PATH",
"QT_FONTPATH"
"QT_PLUGIN_PATH", "QT_FONTPATH"
};
for (auto key : rawenv.keys()) {
auto value = rawenv.value(key);
@ -376,7 +374,19 @@ QList<QString> JavaUtils::FindJavaPaths()
auto home = qEnvironmentVariable("HOME");
// javas downloaded by sdkman
javas.append(FS::PathCombine(home, ".sdkman/candidates/java"));
QDir sdkmanDir(FS::PathCombine(home, ".sdkman/candidates/java"));
QStringList sdkmanJavas = sdkmanDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
foreach (const QString& java, sdkmanJavas) {
javas.append(sdkmanDir.absolutePath() + "/" + java + "/bin/java");
}
// java in user library folder (like from intellij downloads)
QDir userLibraryJVMDir(FS::PathCombine(home, "Library/Java/JavaVirtualMachines/"));
QStringList userLibraryJVMJavas = userLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
foreach (const QString& java, userLibraryJVMJavas) {
javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java");
javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java");
}
javas.append(getMinecraftJavaBundle());
javas = addJavasFromEnv(javas);
@ -415,6 +425,10 @@ QList<QString> JavaUtils::FindJavaPaths()
scanJavaDirs("/usr/lib/jvm");
scanJavaDirs("/usr/lib64/jvm");
scanJavaDirs("/usr/lib32/jvm");
// Gentoo's locations for openjdk and openjdk-bin respectively
scanJavaDir("/usr/lib64");
scanJavaDir("/usr/lib");
scanJavaDir("/opt");
// javas stored in Prism Launcher's folder
scanJavaDirs("java");
// manually installed JDKs in /opt

View File

@ -0,0 +1,65 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2024 Leia uwu <leia@tutamail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "PrintServers.h"
#include "QHostInfo"
PrintServers::PrintServers(LaunchTask* parent, const QStringList& servers) : LaunchStep(parent)
{
m_servers = servers;
}
void PrintServers::executeTask()
{
for (QString server : m_servers) {
QHostInfo::lookupHost(server, this, &PrintServers::resolveServer);
}
}
void PrintServers::resolveServer(const QHostInfo& host_info)
{
QString server = host_info.hostName();
QString addresses = server + " resolves to:\n [";
if (!host_info.addresses().isEmpty()) {
for (QHostAddress address : host_info.addresses()) {
addresses += address.toString();
if (!host_info.addresses().endsWith(address)) {
addresses += ", ";
}
}
} else {
addresses += "N/A";
}
addresses += "]\n\n";
m_server_to_address.insert(server, addresses);
// print server info in order once all servers are resolved
if (m_server_to_address.size() >= m_servers.size()) {
for (QString serv : m_servers) {
emit logLine(m_server_to_address.value(serv), MessageLevel::Launcher);
}
emitSucceeded();
}
}
bool PrintServers::canAbort() const
{
return true;
}

View File

@ -0,0 +1,37 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2024 Leia uwu <leia@tutamail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <LoggedProcess.h>
#include <java/JavaChecker.h>
#include <launch/LaunchStep.h>
#include <QHostInfo>
class PrintServers : public LaunchStep {
Q_OBJECT
public:
PrintServers(LaunchTask* parent, const QStringList& servers);
virtual void executeTask();
virtual bool canAbort() const;
private:
void resolveServer(const QHostInfo& host_info);
QMap<QString, QString> m_server_to_address;
QStringList m_servers;
};

View File

@ -51,6 +51,7 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext,
{
bool local = isLocal();
auto actualPath = [&](QString relPath) {
relPath = FS::RemoveInvalidPathChars(relPath);
QFileInfo out(FS::PathCombine(storagePrefix(), relPath));
if (local && !overridePath.isEmpty()) {
QString fileName = out.fileName();

View File

@ -30,8 +30,13 @@ bool AuthSession::MakeOffline(QString offline_playername)
return true;
}
void AuthSession::MakeDemo()
void AuthSession::MakeDemo(QString name, QString u)
{
player_name = "Player";
wants_online = false;
demo = true;
}
uuid = u;
session = "-";
access_token = "0";
player_name = name;
status = PlayableOnline; // needs online to download the assets
};

View File

@ -10,7 +10,7 @@ class QNetworkAccessManager;
struct AuthSession {
bool MakeOffline(QString offline_playername);
void MakeDemo();
void MakeDemo(QString name, QString uuid);
QString serializeUserProperties();

View File

@ -83,8 +83,6 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString& username)
account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc();
account->data.yggdrasilToken.extra["userName"] = username;
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
account->data.minecraftEntitlement.ownsMinecraft = true;
account->data.minecraftEntitlement.canPlayMinecraft = true;
account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegularExpression("[{}-]"));
account->data.minecraftProfile.name = username;
account->data.minecraftProfile.validity = Validity::Certain;
@ -253,6 +251,8 @@ void MinecraftAccount::fillSession(AuthSessionPtr session)
session->player_name = data.profileName();
// profile ID
session->uuid = data.profileId();
if (session->uuid.isEmpty())
session->uuid = uuidFromUsername(session->player_name).toString().remove(QRegularExpression("[{}-]"));
// 'legacy' or 'mojang', depending on account type
session->user_type = typeString();
if (!session->access_token.isEmpty()) {

View File

@ -116,7 +116,7 @@ class MinecraftAccount : public QObject, public Usable {
[[nodiscard]] AccountType accountType() const noexcept { return data.type; }
bool ownsMinecraft() const { return data.minecraftEntitlement.ownsMinecraft; }
bool ownsMinecraft() const { return data.type != AccountType::Offline && data.minecraftEntitlement.ownsMinecraft; }
bool hasProfile() const { return data.profileId().size() != 0; }

View File

@ -178,6 +178,7 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state)
APPLICATION->showMainWindow();
m_parent->setPid(-1);
m_parent->instance()->setMinecraftRunning(false);
// if the exit code wasn't 0, report this as a crash
auto exitCode = m_process.exitCode();
if (exitCode != 0) {
@ -193,7 +194,6 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state)
case LoggedProcess::Running:
emit logLine(QString("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::Launcher);
m_parent->setPid(m_process.processId());
m_parent->instance()->setLastLaunch();
// send the launch script to the launcher part
m_process.write(m_launchScript.toUtf8());
@ -213,6 +213,7 @@ void LauncherPartLaunch::setWorkingDirectory(const QString& wd)
void LauncherPartLaunch::proceed()
{
if (mayProceed) {
m_parent->instance()->setMinecraftRunning(true);
QString launchString("launch\n");
m_process.write(launchString.toUtf8());
mayProceed = false;

View File

@ -28,15 +28,52 @@
#include "Version.h"
// Values taken from:
// https://minecraft.wiki/w/Tutorials/Creating_a_data_pack#%22pack_format%22
static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
{ 4, { Version("1.13"), Version("1.14.4") } }, { 5, { Version("1.15"), Version("1.16.1") } },
{ 6, { Version("1.16.2"), Version("1.16.5") } }, { 7, { Version("1.17"), Version("1.17.1") } },
{ 8, { Version("1.18"), Version("1.18.1") } }, { 9, { Version("1.18.2"), Version("1.18.2") } },
{ 10, { Version("1.19"), Version("1.19.3") } }, { 11, { Version("23w03a"), Version("23w05a") } },
{ 12, { Version("1.19.4"), Version("1.19.4") } }, { 13, { Version("23w12a"), Version("23w14a") } },
{ 14, { Version("23w16a"), Version("23w17a") } }, { 15, { Version("1.20"), Version("1.20") } },
};
// https://minecraft.wiki/w/Pack_format#List_of_data_pack_formats
static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = { { 4, { Version("1.13"), Version("1.14.4") } },
{ 5, { Version("1.15"), Version("1.16.1") } },
{ 6, { Version("1.16.2"), Version("1.16.5") } },
{ 7, { Version("1.17"), Version("1.17.1") } },
{ 8, { Version("1.18"), Version("1.18.1") } },
{ 9, { Version("1.18.2"), Version("1.18.2") } },
{ 10, { Version("1.19"), Version("1.19.3") } },
{ 11, { Version("23w03a"), Version("23w05a") } },
{ 12, { Version("1.19.4"), Version("1.19.4") } },
{ 13, { Version("23w12a"), Version("23w14a") } },
{ 14, { Version("23w16a"), Version("23w17a") } },
{ 15, { Version("1.20"), Version("1.20.1") } },
{ 16, { Version("23w31a"), Version("23w31a") } },
{ 17, { Version("23w32a"), Version("23w35a") } },
{ 18, { Version("1.20.2"), Version("1.20.2") } },
{ 19, { Version("23w40a"), Version("23w40a") } },
{ 20, { Version("23w41a"), Version("23w41a") } },
{ 21, { Version("23w42a"), Version("23w42a") } },
{ 22, { Version("23w43a"), Version("23w43b") } },
{ 23, { Version("23w44a"), Version("23w44a") } },
{ 24, { Version("23w45a"), Version("23w45a") } },
{ 25, { Version("23w46a"), Version("23w46a") } },
{ 26, { Version("1.20.3"), Version("1.20.4") } },
{ 27, { Version("23w51a"), Version("23w51b") } },
{ 28, { Version("24w05a"), Version("24w05b") } },
{ 29, { Version("24w04a"), Version("24w04a") } },
{ 30, { Version("24w05a"), Version("24w05b") } },
{ 31, { Version("24w06a"), Version("24w06a") } },
{ 32, { Version("24w07a"), Version("24w07a") } },
{ 33, { Version("24w09a"), Version("24w09a") } },
{ 34, { Version("24w10a"), Version("24w10a") } },
{ 35, { Version("24w11a"), Version("24w11a") } },
{ 36, { Version("24w12a"), Version("24w12a") } },
{ 37, { Version("24w13a"), Version("24w13a") } },
{ 38, { Version("24w14a"), Version("24w14a") } },
{ 39, { Version("1.20.5-pre1"), Version("1.20.5-pre1") } },
{ 40, { Version("1.20.5-pre2"), Version("1.20.5-pre2") } },
{ 41, { Version("1.20.5"), Version("1.20.6") } },
{ 42, { Version("24w18a"), Version("24w18a") } },
{ 43, { Version("24w19a"), Version("24w19b") } },
{ 44, { Version("24w20a"), Version("24w20a") } },
{ 45, { Version("21w21a"), Version("21w21b") } },
{ 46, { Version("1.21-pre1"), Version("1.21-pre1") } },
{ 47, { Version("1.21-pre2"), Version("1.21-pre2") } },
{ 48, { Version("1.21"), Version("1.21") } } };
void DataPack::setPackFormat(int new_format_id)
{

View File

@ -2,6 +2,7 @@
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* 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
@ -52,4 +53,6 @@ class Metadata {
static auto get(QDir& index_dir, QString mod_slug) -> ModStruct { return Packwiz::V1::getIndexForMod(index_dir, mod_slug); }
static auto get(QDir& index_dir, QVariant& mod_id) -> ModStruct { return Packwiz::V1::getIndexForMod(index_dir, mod_id); }
static auto modSideToString(ModSide side) -> QString { return Packwiz::V1::sideToString(side); }
};

View File

@ -3,6 +3,7 @@
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* 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
@ -47,8 +48,7 @@
#include "minecraft/mod/ModDetails.h"
#include "minecraft/mod/Resource.h"
#include "minecraft/mod/tasks/LocalModParseTask.h"
static ModPlatform::ProviderCapabilities ProviderCaps;
#include "modplatform/ModIndex.h"
Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details()
{
@ -103,6 +103,32 @@ int Mod::compare(const Resource& other, SortType type) const
case SortType::PROVIDER: {
return QString::compare(provider().value_or("Unknown"), cast_other->provider().value_or("Unknown"), Qt::CaseInsensitive);
}
case SortType::SIDE: {
if (side() > cast_other->side())
return 1;
else if (side() < cast_other->side())
return -1;
break;
}
case SortType::LOADERS: {
if (loaders() > cast_other->loaders())
return 1;
else if (loaders() < cast_other->loaders())
return -1;
break;
}
case SortType::MC_VERSIONS: {
auto thisVersion = mcVersions().join(",");
auto otherVersion = cast_other->mcVersions().join(",");
return QString::compare(thisVersion, otherVersion, Qt::CaseInsensitive);
}
case SortType::RELEASE_TYPE: {
if (releaseType() > cast_other->releaseType())
return 1;
else if (releaseType() < cast_other->releaseType())
return -1;
break;
}
}
return 0;
}
@ -222,7 +248,35 @@ void Mod::finishResolvingWithDetails(ModDetails&& details)
auto Mod::provider() const -> std::optional<QString>
{
if (metadata())
return ProviderCaps.readableName(metadata()->provider);
return ModPlatform::ProviderCapabilities::readableName(metadata()->provider);
return {};
}
auto Mod::side() const -> Metadata::ModSide
{
if (metadata())
return metadata()->side;
return Metadata::ModSide::UniversalSide;
}
auto Mod::releaseType() const -> ModPlatform::IndexedVersionType
{
if (metadata())
return metadata()->releaseType;
return ModPlatform::IndexedVersionType::VersionType::Unknown;
}
auto Mod::loaders() const -> ModPlatform::ModLoaderTypes
{
if (metadata())
return metadata()->loaders;
return {};
}
auto Mod::mcVersions() const -> QStringList
{
if (metadata())
return metadata()->mcVersions;
return {};
}

View File

@ -2,6 +2,7 @@
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* 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
@ -47,6 +48,7 @@
#include "ModDetails.h"
#include "Resource.h"
#include "modplatform/ModIndex.h"
class Mod : public Resource {
Q_OBJECT
@ -70,6 +72,10 @@ class Mod : public Resource {
auto licenses() const -> const QList<ModLicense>&;
auto issueTracker() const -> QString;
auto metaurl() const -> QString;
auto side() const -> Metadata::ModSide;
auto loaders() const -> ModPlatform::ModLoaderTypes;
auto mcVersions() const -> QStringList;
auto releaseType() const -> ModPlatform::IndexedVersionType;
/** Get the intneral path to the mod's icon file*/
QString iconPath() const { return m_local_details.icon_file; }

View File

@ -3,6 +3,7 @@
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* 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
@ -47,12 +48,11 @@
#include <QThreadPool>
#include <QUrl>
#include <QUuid>
#include <algorithm>
#include "Application.h"
#include "Json.h"
#include "StringUtils.h"
#include "minecraft/mod/MetadataHandler.h"
#include "minecraft/mod/Resource.h"
#include "minecraft/mod/tasks/LocalModParseTask.h"
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
@ -64,14 +64,18 @@
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)
{
m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider", "Size" });
m_column_names_translated =
QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider"), tr("Size") });
m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider", "Size", "Side", "Loaders",
"Minecraft Versions", "Release Type" });
m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider"),
tr("Size"), tr("Side"), tr("Loaders"), tr("Minecraft Versions"), tr("Release Type") });
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION,
SortType::DATE, SortType::PROVIDER, SortType::SIZE };
SortType::DATE, SortType::PROVIDER, SortType::SIZE, SortType::SIDE,
SortType::LOADERS, SortType::MC_VERSIONS, SortType::RELEASE_TYPE };
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive,
QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive };
m_columnsHideable = { false, true, false, true, true, true, true };
m_columnsHideable = { false, true, false, true, true, true, true, true, true, true, true };
m_columnsHiddenByDefault = { false, false, false, false, false, false, false, true, true, true, true };
}
QVariant ModFolderModel::data(const QModelIndex& index, int role) const
@ -109,6 +113,26 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const
return provider.value();
}
case SideColumn: {
return Metadata::modSideToString(at(row)->side());
}
case LoadersColumn: {
QStringList loaders;
auto modLoaders = at(row)->loaders();
for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Cauldron, ModPlatform::LiteLoader,
ModPlatform::Fabric, ModPlatform::Quilt }) {
if (modLoaders & loader) {
loaders << getModLoaderAsString(loader);
}
}
return loaders.join(", ");
}
case McVersionsColumn: {
return at(row)->mcVersions().join(", ");
}
case ReleaseTypeColumn: {
return at(row)->releaseType().toString();
}
case SizeColumn:
return m_resources[row]->sizeStr();
default:
@ -165,6 +189,10 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio
case DateColumn:
case ProviderColumn:
case ImageColumn:
case SideColumn:
case LoadersColumn:
case McVersionsColumn:
case ReleaseTypeColumn:
case SizeColumn:
return columnNames().at(section);
default:
@ -183,6 +211,14 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio
return tr("The date and time this mod was last changed (or added).");
case ProviderColumn:
return tr("Where the mod was downloaded from.");
case SideColumn:
return tr("On what environment the mod is running.");
case LoadersColumn:
return tr("The mod loader.");
case McVersionsColumn:
return tr("The supported minecraft versions.");
case ReleaseTypeColumn:
return tr("The release type.");
case SizeColumn:
return tr("The size of the mod.");
default:

View File

@ -3,6 +3,7 @@
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* 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
@ -61,7 +62,20 @@ class QFileSystemWatcher;
class ModFolderModel : public ResourceFolderModel {
Q_OBJECT
public:
enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, VersionColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS };
enum Columns {
ActiveColumn = 0,
ImageColumn,
NameColumn,
VersionColumn,
DateColumn,
ProviderColumn,
SizeColumn,
SideColumn,
LoadersColumn,
McVersionsColumn,
ReleaseTypeColumn,
NUM_COLUMNS
};
enum ModStatusAction { Disable, Enable, Toggle };
ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed = false, bool create_dir = true);

View File

@ -1,3 +1,38 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QDateTime>
@ -15,7 +50,7 @@ enum class ResourceType {
LITEMOD, //!< The resource is a litemod
};
enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER, SIZE };
enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER, SIZE, SIDE, LOADERS, MC_VERSIONS, RELEASE_TYPE };
enum class EnableAction { ENABLE, DISABLE, TOGGLE };

View File

@ -282,8 +282,7 @@ void ResourceFolderModel::resolveResource(Resource* res)
connect(
task.get(), &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection);
connect(
task.get(), &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection);
connect(task.get(), &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection);
connect(
task.get(), &Task::finished, this,
[=] {
@ -539,6 +538,10 @@ void ResourceFolderModel::saveColumns(QTreeView* tree)
void ResourceFolderModel::loadColumns(QTreeView* tree)
{
for (auto i = 0; i < m_columnsHiddenByDefault.size(); ++i) {
tree->setColumnHidden(i, m_columnsHiddenByDefault[i]);
}
auto const setting_name = QString("UI/%1_Page/Columns").arg(id());
auto setting = (m_instance->settings()->contains(setting_name)) ? m_instance->settings()->getSetting(setting_name)
: m_instance->settings()->registerSetting(setting_name);

View File

@ -201,6 +201,7 @@ class ResourceFolderModel : public QAbstractListModel {
QList<QHeaderView::ResizeMode> m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive,
QHeaderView::Interactive };
QList<bool> m_columnsHideable = { false, false, true, true };
QList<bool> m_columnsHiddenByDefault = { false, false, false, false };
QDir m_dir;
BaseInstance* m_instance;

View File

@ -11,7 +11,7 @@
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
// Values taken from:
// https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
// https://minecraft.wiki/w/Pack_format#List_of_resource_pack_formats
static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
{ 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } },
{ 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } },
@ -19,7 +19,16 @@ static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = {
{ 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } },
{ 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("22w42a"), Version("22w44a") } },
{ 12, { Version("1.19.3"), Version("1.19.3") } }, { 13, { Version("1.19.4"), Version("1.19.4") } },
{ 14, { Version("1.20"), Version("1.20") } }
{ 14, { Version("23w14a"), Version("23w16a") } }, { 15, { Version("1.20"), Version("1.20.1") } },
{ 16, { Version("23w31a"), Version("23w31a") } }, { 17, { Version("23w32a"), Version("23w35a") } },
{ 18, { Version("1.20.2"), Version("23w16a") } }, { 19, { Version("23w42a"), Version("23w42a") } },
{ 20, { Version("23w43a"), Version("23w44a") } }, { 21, { Version("23w45a"), Version("23w46a") } },
{ 22, { Version("1.20.3-pre1"), Version("23w51b") } }, { 24, { Version("24w03a"), Version("24w04a") } },
{ 25, { Version("24w05a"), Version("24w05b") } }, { 26, { Version("24w06a"), Version("24w07a") } },
{ 28, { Version("24w09a"), Version("24w10a") } }, { 29, { Version("24w11a"), Version("24w11a") } },
{ 30, { Version("24w12a"), Version("23w12a") } }, { 31, { Version("24w13a"), Version("1.20.5-pre3") } },
{ 32, { Version("1.20.5-pre4"), Version("1.20.6") } }, { 33, { Version("24w18a"), Version("24w20a") } },
{ 34, { Version("24w21a"), Version("1.21") } }
};
void ResourcePack::setPackFormat(int new_format_id)

View File

@ -42,7 +42,6 @@
#include <QStyle>
#include "Application.h"
#include "StringUtils.h"
#include "Version.h"
#include "minecraft/mod/Resource.h"
@ -57,6 +56,7 @@ ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir, BaseInstanc
m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch,
QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive };
m_columnsHideable = { false, true, false, true, true, true };
m_columnsHiddenByDefault = { false, false, false, false, false, false };
}
QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const

View File

@ -37,7 +37,6 @@
#include "Application.h"
#include "StringUtils.h"
#include "TexturePackFolderModel.h"
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"

View File

@ -23,6 +23,7 @@
#include <memory>
#include "Json.h"
#include "QObjectPtr.h"
#include "minecraft/PackProfile.h"
#include "minecraft/mod/MetadataHandler.h"
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
@ -44,6 +45,14 @@ static ModPlatform::ModLoaderTypes mcLoaders(BaseInstance* inst)
return static_cast<MinecraftInstance*>(inst)->getPackProfile()->getSupportedModLoaders().value();
}
static bool checkDependencies(std::shared_ptr<GetModDependenciesTask::PackDependency> sel,
Version mcVersion,
ModPlatform::ModLoaderTypes loaders)
{
return (sel->pack->versions.isEmpty() || sel->version.mcVersion.contains(mcVersion.toString())) &&
(!loaders || !sel->version.loaders || sel->version.loaders & loaders);
}
GetModDependenciesTask::GetModDependenciesTask(QObject* parent,
BaseInstance* instance,
ModFolderModel* folder,
@ -68,6 +77,7 @@ GetModDependenciesTask::GetModDependenciesTask(QObject* parent,
void GetModDependenciesTask::prepare()
{
for (auto sel : m_selected) {
if (checkDependencies(sel, m_version, m_loaderType))
for (auto dep : getDependenciesForVersion(sel->version, sel->pack->provider)) {
addTask(prepareDependencyTask(dep, sel->pack->provider, 20));
}

View File

@ -15,8 +15,6 @@
#include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackIndex.h"
static ModPlatform::ProviderCapabilities ProviderCaps;
static ModrinthAPI modrinth_api;
static FlameAPI flame_api;
@ -162,10 +160,10 @@ void EnsureMetadataTask::executeTask()
});
if (m_mods.size() > 1)
setStatus(tr("Requesting metadata information from %1...").arg(ProviderCaps.readableName(m_provider)));
setStatus(tr("Requesting metadata information from %1...").arg(ModPlatform::ProviderCapabilities::readableName(m_provider)));
else if (!m_mods.empty())
setStatus(tr("Requesting metadata information from %1 for '%2'...")
.arg(ProviderCaps.readableName(m_provider), m_mods.begin().value()->name()));
.arg(ModPlatform::ProviderCapabilities::readableName(m_provider), m_mods.begin().value()->name()));
m_current_task = version_task;
version_task->start();
@ -215,7 +213,7 @@ void EnsureMetadataTask::emitFail(Mod* m, QString key, RemoveFromList remove)
Task::Ptr EnsureMetadataTask::modrinthVersionsTask()
{
auto hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first();
auto hash_type = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first();
auto response = std::make_shared<QByteArray>();
auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response);

View File

@ -2,6 +2,7 @@
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* 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
@ -58,7 +59,7 @@ IndexedVersionType::VersionType IndexedVersionType::enumFromString(const QString
return s_indexed_version_type_names.value(type, IndexedVersionType::VersionType::Unknown);
}
auto ProviderCapabilities::name(ResourceProvider p) -> const char*
const char* ProviderCapabilities::name(ResourceProvider p)
{
switch (p) {
case ResourceProvider::MODRINTH:
@ -68,7 +69,8 @@ auto ProviderCapabilities::name(ResourceProvider p) -> const char*
}
return {};
}
auto ProviderCapabilities::readableName(ResourceProvider p) -> QString
QString ProviderCapabilities::readableName(ResourceProvider p)
{
switch (p) {
case ResourceProvider::MODRINTH:
@ -78,7 +80,8 @@ auto ProviderCapabilities::readableName(ResourceProvider p) -> QString
}
return {};
}
auto ProviderCapabilities::hashType(ResourceProvider p) -> QStringList
QStringList ProviderCapabilities::hashType(ResourceProvider p)
{
switch (p) {
case ResourceProvider::MODRINTH:
@ -90,34 +93,13 @@ auto ProviderCapabilities::hashType(ResourceProvider p) -> QStringList
return {};
}
auto ProviderCapabilities::hash(ResourceProvider p, QIODevice* device, QString type) -> QString
{
QCryptographicHash::Algorithm algo = QCryptographicHash::Sha1;
switch (p) {
case ResourceProvider::MODRINTH: {
algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Sha512;
break;
}
case ResourceProvider::FLAME:
algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Md5;
break;
}
QCryptographicHash hash(algo);
if (!hash.addData(device))
qCritical() << "Failed to read JAR to create hash!";
Q_ASSERT(hash.result().length() == hash.hashLength(algo));
return { hash.result().toHex() };
}
QString getMetaURL(ResourceProvider provider, QVariant projectID)
{
return ((provider == ModPlatform::ResourceProvider::FLAME) ? "https://www.curseforge.com/projects/" : "https://modrinth.com/mod/") +
projectID.toString();
}
auto getModLoaderString(ModLoaderType type) -> const QString
auto getModLoaderAsString(ModLoaderType type) -> const QString
{
switch (type) {
case NeoForge:
@ -138,4 +120,21 @@ auto getModLoaderString(ModLoaderType type) -> const QString
return "";
}
auto getModLoaderFromString(QString type) -> ModLoaderType
{
if (type == "neoforge")
return NeoForge;
if (type == "forge")
return Forge;
if (type == "cauldron")
return Cauldron;
if (type == "liteloader")
return LiteLoader;
if (type == "fabric")
return Fabric;
if (type == "quilt")
return Quilt;
return {};
}
} // namespace ModPlatform

View File

@ -40,13 +40,11 @@ enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK };
enum class DependencyType { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN };
class ProviderCapabilities {
public:
auto name(ResourceProvider) -> const char*;
auto readableName(ResourceProvider) -> QString;
auto hashType(ResourceProvider) -> QStringList;
auto hash(ResourceProvider, QIODevice*, QString type = "") -> QString;
};
namespace ProviderCapabilities {
const char* name(ResourceProvider);
QString readableName(ResourceProvider);
QStringList hashType(ResourceProvider);
}; // namespace ProviderCapabilities
struct ModpackAuthor {
QString name;
@ -109,6 +107,7 @@ struct IndexedVersion {
bool is_preferred = true;
QString changelog;
QList<Dependency> dependencies;
QString side; // this is for flame API
// For internal use, not provided by APIs
bool is_currently_selected = false;
@ -183,7 +182,8 @@ inline auto getOverrideDeps() -> QList<OverrideDep>
QString getMetaURL(ResourceProvider provider, QVariant projectID);
auto getModLoaderString(ModLoaderType type) -> const QString;
auto getModLoaderAsString(ModLoaderType type) -> const QString;
auto getModLoaderFromString(QString type) -> ModLoaderType;
constexpr bool hasSingleModLoaderSelected(ModLoaderTypes l) noexcept
{
@ -191,6 +191,11 @@ constexpr bool hasSingleModLoaderSelected(ModLoaderTypes l) noexcept
return x && !(x & (x - 1));
}
struct Category {
QString name;
QString id;
};
} // namespace ModPlatform
Q_DECLARE_METATYPE(ModPlatform::IndexedPack)

View File

@ -4,6 +4,7 @@
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* 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
@ -73,6 +74,8 @@ class ResourceAPI {
std::optional<SortingMethod> sorting;
std::optional<ModPlatform::ModLoaderTypes> loaders;
std::optional<std::list<Version> > versions;
std::optional<QString> side;
std::optional<QStringList> categoryIds;
};
struct SearchCallbacks {
std::function<void(QJsonDocument&)> on_succeed;

View File

@ -3,10 +3,12 @@
// SPDX-License-Identifier: GPL-3.0-only
#include "FlameAPI.h"
#include <memory>
#include "FlameModIndex.h"
#include "Application.h"
#include "Json.h"
#include "modplatform/ModIndex.h"
#include "net/ApiDownload.h"
#include "net/ApiUpload.h"
#include "net/NetJob.h"
@ -220,3 +222,42 @@ QList<ResourceAPI::SortingMethod> FlameAPI::getSortingMethods() const
{ 7, "Category", QObject::tr("Sort by Category") },
{ 8, "GameVersion", QObject::tr("Sort by Game Version") } };
}
Task::Ptr FlameAPI::getModCategories(std::shared_ptr<QByteArray> response)
{
auto netJob = makeShared<NetJob>(QString("Flame::GetCategories"), APPLICATION->network());
netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl("https://api.curseforge.com/v1/categories?gameId=432&classId=6"), response));
QObject::connect(netJob.get(), &Task::failed, [](QString msg) { qDebug() << "Flame failed to get categories:" << msg; });
return netJob;
}
QList<ModPlatform::Category> FlameAPI::loadModCategories(std::shared_ptr<QByteArray> response)
{
QList<ModPlatform::Category> categories;
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from categories at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return categories;
}
try {
auto obj = Json::requireObject(doc);
auto arr = Json::requireArray(obj, "data");
for (auto val : arr) {
auto cat = Json::requireObject(val);
auto id = Json::requireInteger(cat, "id");
auto name = Json::requireString(cat, "name");
categories.push_back({ name, QString::number(id) });
}
} catch (Json::JsonException& e) {
qCritical() << "Failed to parse response from a version request.";
qCritical() << e.what();
qDebug() << doc;
}
return categories;
};

View File

@ -4,6 +4,7 @@
#pragma once
#include <QList>
#include <algorithm>
#include <memory>
#include "modplatform/ModIndex.h"
@ -22,6 +23,9 @@ class FlameAPI : public NetworkResourceAPI {
Task::Ptr getFiles(const QStringList& fileIds, std::shared_ptr<QByteArray> response) const;
Task::Ptr getFile(const QString& addonId, const QString& fileId, std::shared_ptr<QByteArray> response) const;
static Task::Ptr getModCategories(std::shared_ptr<QByteArray> response);
static QList<ModPlatform::Category> loadModCategories(std::shared_ptr<QByteArray> response);
[[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool
@ -96,6 +100,9 @@ class FlameAPI : public NetworkResourceAPI {
get_arguments.append("sortOrder=desc");
if (args.loaders.has_value())
get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(args.loaders.value())));
if (args.categoryIds.has_value() && !args.categoryIds->empty())
get_arguments.append(QString("categoryIds=[%1]").arg(args.categoryIds->join(",")));
get_arguments.append(gameVersionStr);
return "https://api.curseforge.com/v1/mods/search?gameId=432&" + get_arguments.join('&');

View File

@ -538,9 +538,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
}
for (const auto& result : results) {
auto fileName = result.fileName;
#ifdef Q_OS_WIN
fileName = FS::RemoveInvalidPathChars(fileName);
#endif
auto relpath = FS::PathCombine(result.targetFolder, fileName);
if (!result.required && !selectedOptionalMods.contains(relpath)) {

View File

@ -7,7 +7,6 @@
#include "modplatform/flame/FlameAPI.h"
static FlameAPI api;
static ModPlatform::ProviderCapabilities ProviderCaps;
void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
@ -21,6 +20,9 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
QJsonObject logo = Json::ensureObject(obj, "logo");
pack.logoName = Json::ensureString(logo, "title");
pack.logoUrl = Json::ensureString(logo, "thumbnailUrl");
if (pack.logoUrl.isEmpty()) {
pack.logoUrl = Json::ensureString(logo, "url");
}
auto authors = Json::ensureArray(obj, "authors");
for (auto authorIter : authors) {
@ -80,10 +82,6 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
const BaseInstance* inst)
{
QVector<ModPlatform::IndexedVersion> unsortedVersions;
auto profile = (dynamic_cast<const MinecraftInstance*>(inst))->getPackProfile();
QString mcVersion = profile->getComponentVersion("net.minecraft");
auto loaders = profile->getSupportedModLoaders();
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
@ -91,8 +89,7 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
if (!file.addonId.isValid())
file.addonId = pack.addonId;
if (file.fileId.isValid() &&
(!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid
if (file.fileId.isValid()) // Heuristic to check if the returned value is valid
unsortedVersions.append(file);
}
@ -118,19 +115,25 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
if (str.contains('.'))
file.mcVersion.append(str);
auto loader = str.toLower();
if (loader == "neoforge")
if (auto loader = str.toLower(); loader == "neoforge")
file.loaders |= ModPlatform::NeoForge;
if (loader == "forge")
else if (loader == "forge")
file.loaders |= ModPlatform::Forge;
if (loader == "cauldron")
else if (loader == "cauldron")
file.loaders |= ModPlatform::Cauldron;
if (loader == "liteloader")
else if (loader == "liteloader")
file.loaders |= ModPlatform::LiteLoader;
if (loader == "fabric")
else if (loader == "fabric")
file.loaders |= ModPlatform::Fabric;
if (loader == "quilt")
else if (loader == "quilt")
file.loaders |= ModPlatform::Quilt;
else if (loader == "server" || loader == "client") {
if (file.side.isEmpty())
file.side = loader;
else if (file.side != loader)
file.side = "both";
}
}
file.addonId = Json::requireInteger(obj, "modId");
@ -139,9 +142,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
file.version = Json::requireString(obj, "displayName");
file.downloadUrl = Json::ensureString(obj, "downloadUrl");
file.fileName = Json::requireString(obj, "fileName");
#ifdef Q_OS_WIN
file.fileName = FS::RemoveInvalidPathChars(file.fileName);
#endif
ModPlatform::IndexedVersionType::VersionType ver_type;
switch (Json::requireInteger(obj, "releaseType")) {
@ -162,7 +163,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) ->
auto hash_list = Json::ensureArray(obj, "hashes");
for (auto h : hash_list) {
auto hash_entry = Json::ensureObject(h);
auto hash_types = ProviderCaps.hashType(ModPlatform::ResourceProvider::FLAME);
auto hash_types = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::FLAME);
auto hash_algo = enumToString(Json::ensureInteger(hash_entry, "algo", 1, "algorithm"));
if (hash_types.contains(hash_algo)) {
file.hash = Json::requireString(hash_entry, "value");

View File

@ -116,7 +116,7 @@ void FlamePackExportTask::collectHashes()
if (relative.startsWith("resourcepacks/") &&
(relative.endsWith(".zip") || relative.endsWith(".zip.disabled"))) { // is resourcepack
auto hashTask = Hashing::createFlameHasher(file.absoluteFilePath());
auto hashTask = Hashing::createHasher(file.absoluteFilePath(), ModPlatform::ResourceProvider::FLAME);
connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, relative, file](QString hash) {
if (m_state == Task::State::Running) {
pendingHashes.insert(hash, { relative, file.absoluteFilePath(), relative.endsWith(".zip") });
@ -140,7 +140,7 @@ void FlamePackExportTask::collectHashes()
continue;
}
auto hashTask = Hashing::createFlameHasher(mod->fileinfo().absoluteFilePath());
auto hashTask = Hashing::createHasher(mod->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::FLAME);
connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) {
if (m_state == Task::State::Running) {
pendingHashes.insert(hash, { mod->name(), mod->fileinfo().absoluteFilePath(), mod->enabled(), true });

View File

@ -1,24 +1,22 @@
#include "HashUtils.h"
#include <QBuffer>
#include <QDebug>
#include <QFile>
#include "FileSystem.h"
#include "StringUtils.h"
#include <QtConcurrentRun>
#include <MurmurHash2.h>
namespace Hashing {
static ModPlatform::ProviderCapabilities ProviderCaps;
Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider)
{
switch (provider) {
case ModPlatform::ResourceProvider::MODRINTH:
return createModrinthHasher(file_path);
return makeShared<Hasher>(file_path,
ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first());
case ModPlatform::ResourceProvider::FLAME:
return createFlameHasher(file_path);
return makeShared<Hasher>(file_path, Algorithm::Murmur2);
default:
qCritical() << "[Hashing]"
<< "Unrecognized mod platform!";
@ -26,119 +24,142 @@ Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provid
}
}
Hasher::Ptr createModrinthHasher(QString file_path)
Hasher::Ptr createHasher(QString file_path, QString type)
{
return makeShared<ModrinthHasher>(file_path);
return makeShared<Hasher>(file_path, type);
}
Hasher::Ptr createFlameHasher(QString file_path)
class QIODeviceReader : public Murmur2::Reader {
public:
QIODeviceReader(QIODevice* device) : m_device(device) {}
virtual ~QIODeviceReader() = default;
virtual int read(char* s, int n) { return m_device->read(s, n); }
virtual bool eof() { return m_device->atEnd(); }
virtual void goToBeginning() { m_device->seek(0); }
virtual void close() { m_device->close(); }
private:
QIODevice* m_device;
};
QString algorithmToString(Algorithm type)
{
return makeShared<FlameHasher>(file_path);
switch (type) {
case Algorithm::Md4:
return "md4";
case Algorithm::Md5:
return "md5";
case Algorithm::Sha1:
return "sha1";
case Algorithm::Sha256:
return "sha256";
case Algorithm::Sha512:
return "sha512";
case Algorithm::Murmur2:
return "murmur2";
// case Algorithm::Unknown:
default:
break;
}
return "unknown";
}
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider)
Algorithm algorithmFromString(QString type)
{
return makeShared<BlockedModHasher>(file_path, provider);
if (type == "md4")
return Algorithm::Md4;
if (type == "md5")
return Algorithm::Md5;
if (type == "sha1")
return Algorithm::Sha1;
if (type == "sha256")
return Algorithm::Sha256;
if (type == "sha512")
return Algorithm::Sha512;
if (type == "murmur2")
return Algorithm::Murmur2;
return Algorithm::Unknown;
}
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider, QString type)
QString hash(QIODevice* device, Algorithm type)
{
auto hasher = makeShared<BlockedModHasher>(file_path, provider);
hasher->useHashType(type);
return hasher;
}
void ModrinthHasher::executeTask()
{
QFile file(m_path);
try {
file.open(QFile::ReadOnly);
} catch (FS::FileSystemException& e) {
qCritical() << QString("Failed to open JAR file in %1").arg(m_path);
qCritical() << QString("Reason: ") << e.cause();
emitFailed("Failed to open file for hashing.");
return;
}
auto hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first();
m_hash = ProviderCaps.hash(ModPlatform::ResourceProvider::MODRINTH, &file, hash_type);
file.close();
if (m_hash.isEmpty()) {
emitFailed("Empty hash!");
} else {
emitSucceeded();
emit resultsReady(m_hash);
}
}
void FlameHasher::executeTask()
{
// CF-specific
if (!device->isOpen() && !device->open(QFile::ReadOnly))
return "";
QCryptographicHash::Algorithm alg = QCryptographicHash::Sha1;
switch (type) {
case Algorithm::Md4:
alg = QCryptographicHash::Algorithm::Md4;
break;
case Algorithm::Md5:
alg = QCryptographicHash::Algorithm::Md5;
break;
case Algorithm::Sha1:
alg = QCryptographicHash::Algorithm::Sha1;
break;
case Algorithm::Sha256:
alg = QCryptographicHash::Algorithm::Sha256;
break;
case Algorithm::Sha512:
alg = QCryptographicHash::Algorithm::Sha512;
break;
case Algorithm::Murmur2: { // CF-specific
auto should_filter_out = [](char c) { return (c == 9 || c == 10 || c == 13 || c == 32); };
auto reader = std::make_unique<QIODeviceReader>(device);
auto result = QString::number(Murmur2::hash(reader.get(), 4 * MiB, should_filter_out));
device->close();
return result;
}
case Algorithm::Unknown:
device->close();
return "";
}
std::ifstream file_stream(StringUtils::toStdString(m_path).c_str(), std::ifstream::binary);
// TODO: This is very heavy work, but apparently QtConcurrent can't use move semantics, so we can't boop this to another thread.
// How do we make this non-blocking then?
m_hash = QString::number(MurmurHash2(std::move(file_stream), 4 * MiB, should_filter_out));
QCryptographicHash hash(alg);
if (!hash.addData(device))
qCritical() << "Failed to read JAR to create hash!";
if (m_hash.isEmpty()) {
Q_ASSERT(hash.result().length() == hash.hashLength(alg));
auto result = hash.result().toHex();
device->close();
return result;
}
QString hash(QString fileName, Algorithm type)
{
QFile file(fileName);
return hash(&file, type);
}
QString hash(QByteArray data, Algorithm type)
{
QBuffer buff(&data);
return hash(&buff, type);
}
void Hasher::executeTask()
{
m_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return hash(m_path, m_alg); });
connect(&m_watcher, &QFutureWatcher<QString>::finished, this, [this] {
if (m_future.isCanceled()) {
emitAborted();
} else if (m_result = m_future.result(); m_result.isEmpty()) {
emitFailed("Empty hash!");
} else {
emitSucceeded();
emit resultsReady(m_hash);
emit resultsReady(m_result);
}
});
m_watcher.setFuture(m_future);
}
BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider) : Hasher(file_path), provider(provider)
bool Hasher::abort()
{
setObjectName(QString("BlockedModHasher: %1").arg(file_path));
hash_type = ProviderCaps.hashType(provider).first();
}
void BlockedModHasher::executeTask()
{
QFile file(m_path);
try {
file.open(QFile::ReadOnly);
} catch (FS::FileSystemException& e) {
qCritical() << QString("Failed to open JAR file in %1").arg(m_path);
qCritical() << QString("Reason: ") << e.cause();
emitFailed("Failed to open file for hashing.");
return;
}
m_hash = ProviderCaps.hash(provider, &file, hash_type);
file.close();
if (m_hash.isEmpty()) {
emitFailed("Empty hash!");
} else {
emitSucceeded();
emit resultsReady(m_hash);
}
}
QStringList BlockedModHasher::getHashTypes()
{
return ProviderCaps.hashType(provider);
}
bool BlockedModHasher::useHashType(QString type)
{
auto types = ProviderCaps.hashType(provider);
if (types.contains(type)) {
hash_type = type;
if (m_future.isRunning()) {
m_future.cancel();
// NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not
// occur immediately.
return true;
}
qDebug() << "Bad hash type " << type << " for provider";
return false;
}
} // namespace Hashing

View File

@ -1,5 +1,8 @@
#pragma once
#include <QCryptographicHash>
#include <QFuture>
#include <QFutureWatcher>
#include <QString>
#include "modplatform/ModIndex.h"
@ -7,61 +10,42 @@
namespace Hashing {
enum class Algorithm { Md4, Md5, Sha1, Sha256, Sha512, Murmur2, Unknown };
QString algorithmToString(Algorithm type);
Algorithm algorithmFromString(QString type);
QString hash(QIODevice* device, Algorithm type);
QString hash(QString fileName, Algorithm type);
QString hash(QByteArray data, Algorithm type);
class Hasher : public Task {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<Hasher>;
Hasher(QString file_path) : m_path(std::move(file_path)) {}
Hasher(QString file_path, Algorithm alg) : m_path(file_path), m_alg(alg) {}
Hasher(QString file_path, QString alg) : Hasher(file_path, algorithmFromString(alg)) {}
/* We can't really abort this task, but we can say we aborted and finish our thing quickly :) */
bool abort() override { return true; }
bool abort() override;
void executeTask() override = 0;
void executeTask() override;
QString getResult() const { return m_hash; };
QString getResult() const { return m_result; };
QString getPath() const { return m_path; };
signals:
void resultsReady(QString hash);
protected:
QString m_hash;
QString m_path;
};
class FlameHasher : public Hasher {
public:
FlameHasher(QString file_path) : Hasher(file_path) { setObjectName(QString("FlameHasher: %1").arg(file_path)); }
void executeTask() override;
};
class ModrinthHasher : public Hasher {
public:
ModrinthHasher(QString file_path) : Hasher(file_path) { setObjectName(QString("ModrinthHasher: %1").arg(file_path)); }
void executeTask() override;
};
class BlockedModHasher : public Hasher {
public:
BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider);
void executeTask() override;
QStringList getHashTypes();
bool useHashType(QString type);
private:
ModPlatform::ResourceProvider provider;
QString hash_type;
QString m_result;
QString m_path;
Algorithm m_alg;
QFuture<QString> m_future;
QFutureWatcher<QString> m_watcher;
};
Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider);
Hasher::Ptr createFlameHasher(QString file_path);
Hasher::Ptr createModrinthHasher(QString file_path);
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider);
Hasher::Ptr createBlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider, QString type);
Hasher::Ptr createHasher(QString file_path, QString type);
} // namespace Hashing

View File

@ -120,3 +120,41 @@ QList<ResourceAPI::SortingMethod> ModrinthAPI::getSortingMethods() const
{ 4, "newest", QObject::tr("Sort by Newest") },
{ 5, "updated", QObject::tr("Sort by Last Updated") } };
}
Task::Ptr ModrinthAPI::getModCategories(std::shared_ptr<QByteArray> response)
{
auto netJob = makeShared<NetJob>(QString("Modrinth::GetCategories"), APPLICATION->network());
netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(BuildConfig.MODRINTH_PROD_URL + "/tag/category"), response));
QObject::connect(netJob.get(), &Task::failed, [](QString msg) { qDebug() << "Modrinth failed to get categories:" << msg; });
return netJob;
}
QList<ModPlatform::Category> ModrinthAPI::loadModCategories(std::shared_ptr<QByteArray> response)
{
QList<ModPlatform::Category> categories;
QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from categories at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return categories;
}
try {
auto arr = Json::requireArray(doc);
for (auto val : arr) {
auto cat = Json::requireObject(val);
auto name = Json::requireString(cat, "name");
if (Json::ensureString(cat, "project_type", "") == "mod")
categories.push_back({ name, name });
}
} catch (Json::JsonException& e) {
qCritical() << "Failed to parse response from a version request.";
qCritical() << e.what();
qDebug() << doc;
}
return categories;
};

View File

@ -30,6 +30,9 @@ class ModrinthAPI : public NetworkResourceAPI {
Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const override;
static Task::Ptr getModCategories(std::shared_ptr<QByteArray> response);
static QList<ModPlatform::Category> loadModCategories(std::shared_ptr<QByteArray> response);
public:
[[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;
@ -41,7 +44,7 @@ class ModrinthAPI : public NetworkResourceAPI {
for (auto loader :
{ ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader }) {
if (types & loader) {
l << getModLoaderString(loader);
l << getModLoaderAsString(loader);
}
}
return l;
@ -56,6 +59,27 @@ class ModrinthAPI : public NetworkResourceAPI {
return l.join(',');
}
static auto getCategoriesFilters(QStringList categories) -> const QString
{
QStringList l;
for (auto cat : categories) {
l << QString("\"categories:%1\"").arg(cat);
}
return l.join(',');
}
static auto getSideFilters(QString side) -> const QString
{
if (side.isEmpty() || side == "both") {
return {};
}
if (side == "client")
return QString("\"client_side:required\",\"client_side:optional\"");
if (side == "server")
return QString("\"server_side:required\",\"server_side:optional\"");
return {};
}
private:
[[nodiscard]] static QString resourceTypeParameter(ModPlatform::ResourceType type)
{
@ -73,6 +97,7 @@ class ModrinthAPI : public NetworkResourceAPI {
return "";
}
[[nodiscard]] QString createFacets(SearchArgs const& args) const
{
QStringList facets_list;
@ -81,6 +106,14 @@ class ModrinthAPI : public NetworkResourceAPI {
facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value())));
if (args.versions.has_value())
facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value())));
if (args.side.has_value()) {
auto side = getSideFilters(args.side.value());
if (!side.isEmpty())
facets_list.append(QString("[%1]").arg(side));
}
if (args.categoryIds.has_value() && !args.categoryIds->empty())
facets_list.append(QString("[%1]").arg(getCategoriesFilters(args.categoryIds.value())));
facets_list.append(QString("[\"project_type:%1\"]").arg(resourceTypeParameter(args.type)));
return QString("[%1]").arg(facets_list.join(','));

View File

@ -13,7 +13,6 @@
#include "minecraft/mod/ModFolderModel.h"
static ModrinthAPI api;
static ModPlatform::ProviderCapabilities ProviderCaps;
bool ModrinthCheckUpdate::abort()
{
@ -36,7 +35,7 @@ void ModrinthCheckUpdate::executeTask()
// Create all hashes
QStringList hashes;
auto best_hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first();
auto best_hash_type = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first();
ConcurrentTask hashing_task(this, "MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
for (auto* mod : m_mods) {
@ -46,7 +45,7 @@ void ModrinthCheckUpdate::executeTask()
// need to generate a new hash if the current one is innadequate
// (though it will rarely happen, if at all)
if (mod->metadata()->hash_format != best_hash_type) {
auto hash_task = Hashing::createModrinthHasher(mod->fileinfo().absoluteFilePath());
auto hash_task = Hashing::createHasher(mod->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::MODRINTH);
connect(hash_task.get(), &Hashing::Hasher::resultsReady, [&hashes, &mappings, mod](QString hash) {
hashes.append(hash);
mappings.insert(hash, mod);
@ -107,7 +106,7 @@ void ModrinthCheckUpdate::executeTask()
ModPlatform::ModLoaderType::Fabric, ModPlatform::ModLoaderType::Quilt };
for (auto flag : flags) {
if (m_loaders.value().testFlag(flag)) {
loader_filter = ModPlatform::getModLoaderString(flag);
loader_filter = ModPlatform::getModLoaderAsString(flag);
break;
}
}

View File

@ -241,9 +241,7 @@ bool ModrinthCreationTask::createInstance()
for (auto file : m_files) {
auto fileName = file.path;
#ifdef Q_OS_WIN
fileName = FS::RemoveInvalidPathChars(fileName);
#endif
auto file_path = FS::PathCombine(root_modpack_path, fileName);
if (!root_modpack_url.isParentOf(QUrl::fromLocalFile(file_path))) {
// This means we somehow got out of the root folder, so abort here to prevent exploits

View File

@ -27,6 +27,7 @@
#include "minecraft/PackProfile.h"
#include "minecraft/mod/MetadataHandler.h"
#include "minecraft/mod/ModFolderModel.h"
#include "modplatform/helpers/HashUtils.h"
const QStringList ModrinthPackExportTask::PREFIXES({ "mods/", "coremods/", "resourcepacks/", "texturepacks/", "shaderpacks/" });
const QStringList ModrinthPackExportTask::FILE_EXTENSIONS({ "jar", "litemod", "zip" });
@ -102,8 +103,6 @@ void ModrinthPackExportTask::collectHashes()
}))
continue;
QCryptographicHash sha512(QCryptographicHash::Algorithm::Sha512);
QFile openFile(file.absoluteFilePath());
if (!openFile.open(QFile::ReadOnly)) {
qWarning() << "Could not open" << file << "for hashing";
@ -115,7 +114,7 @@ void ModrinthPackExportTask::collectHashes()
qWarning() << "Could not read" << file;
continue;
}
sha512.addData(data);
auto sha512 = Hashing::hash(data, Hashing::Algorithm::Sha512);
auto allMods = mcInstance->loaderModList()->allMods();
if (auto modIter = std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; });
@ -127,11 +126,9 @@ void ModrinthPackExportTask::collectHashes()
if (!url.isEmpty() && BuildConfig.MODRINTH_MRPACK_HOSTS.contains(url.host())) {
qDebug() << "Resolving" << relative << "from index";
QCryptographicHash sha1(QCryptographicHash::Algorithm::Sha1);
sha1.addData(data);
auto sha1 = Hashing::hash(data, Hashing::Algorithm::Sha1);
ResolvedFile resolvedFile{ sha1.result().toHex(), sha512.result().toHex(), url.toEncoded(), openFile.size(),
mod->metadata()->side };
ResolvedFile resolvedFile{ sha1, sha512, url.toEncoded(), openFile.size(), mod->metadata()->side };
resolvedFiles[relative] = resolvedFile;
// nice! we've managed to resolve based on local metadata!
@ -142,7 +139,7 @@ void ModrinthPackExportTask::collectHashes()
}
qDebug() << "Enqueueing" << relative << "for Modrinth query";
pendingHashes[relative] = sha512.result().toHex();
pendingHashes[relative] = sha512;
}
setAbortable(true);

View File

@ -2,6 +2,7 @@
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* 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
@ -26,7 +27,6 @@
#include "modplatform/ModIndex.h"
static ModrinthAPI api;
static ModPlatform::ProviderCapabilities ProviderCaps;
bool shouldDownloadOnSide(QString side)
{
@ -115,16 +115,11 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob
void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const BaseInstance* inst)
{
QVector<ModPlatform::IndexedVersion> unsortedVersions;
auto profile = (dynamic_cast<const MinecraftInstance*>(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() &&
(!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid
if (file.fileId.isValid()) // Heuristic to check if the returned value is valid
unsortedVersions.append(file);
}
auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
@ -136,8 +131,9 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArra
pack.versionsLoaded = true;
}
auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_type, QString preferred_file_name)
-> ModPlatform::IndexedVersion
auto Modrinth::loadIndexedPackVersion(QJsonObject& obj,
QString preferred_hash_type,
QString preferred_file_name) -> ModPlatform::IndexedVersion
{
ModPlatform::IndexedVersion file;
@ -155,15 +151,15 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
for (auto loader : loaders) {
if (loader == "neoforge")
file.loaders |= ModPlatform::NeoForge;
if (loader == "forge")
else if (loader == "forge")
file.loaders |= ModPlatform::Forge;
if (loader == "cauldron")
else if (loader == "cauldron")
file.loaders |= ModPlatform::Cauldron;
if (loader == "liteloader")
else if (loader == "liteloader")
file.loaders |= ModPlatform::LiteLoader;
if (loader == "fabric")
else if (loader == "fabric")
file.loaders |= ModPlatform::Fabric;
if (loader == "quilt")
else if (loader == "quilt")
file.loaders |= ModPlatform::Quilt;
}
file.version = Json::requireString(obj, "name");
@ -227,9 +223,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
if (parent.contains("url")) {
file.downloadUrl = Json::requireString(parent, "url");
file.fileName = Json::requireString(parent, "filename");
#ifdef Q_OS_WIN
file.fileName = FS::RemoveInvalidPathChars(file.fileName);
#endif
file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1);
auto hash_list = Json::requireObject(parent, "hashes");
@ -237,7 +231,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
file.hash = Json::requireString(hash_list, preferred_hash_type);
file.hash_type = preferred_hash_type;
} else {
auto hash_types = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH);
auto hash_types = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH);
for (auto& hash_type : hash_types) {
if (hash_list.contains(hash_type)) {
file.hash = Json::requireString(hash_list, hash_type);
@ -253,8 +247,9 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t
return {};
}
auto Modrinth::loadDependencyVersions([[maybe_unused]] const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst)
-> ModPlatform::IndexedVersion
auto Modrinth::loadDependencyVersions([[maybe_unused]] const ModPlatform::Dependency& m,
QJsonArray& arr,
const BaseInstance* inst) -> ModPlatform::IndexedVersion
{
auto profile = (dynamic_cast<const MinecraftInstance*>(inst))->getPackProfile();
QString mcVersion = profile->getComponentVersion("net.minecraft");

View File

@ -2,6 +2,7 @@
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* 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
@ -66,8 +67,6 @@ static inline auto indexFileName(QString const& mod_slug) -> QString
return QString("%1.pw.toml").arg(mod_slug);
}
static ModPlatform::ProviderCapabilities ProviderCaps;
// Helper functions for extracting data from the TOML file
auto stringEntry(toml::table table, QString entry_name) -> QString
{
@ -91,8 +90,9 @@ auto intEntry(toml::table table, QString entry_name) -> int
return node.value_or(0);
}
auto V1::createModFormat([[maybe_unused]] QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version)
-> Mod
auto V1::createModFormat([[maybe_unused]] QDir& index_dir,
ModPlatform::IndexedPack& mod_pack,
ModPlatform::IndexedVersion& mod_version) -> Mod
{
Mod mod;
@ -113,7 +113,11 @@ auto V1::createModFormat([[maybe_unused]] QDir& index_dir, ModPlatform::IndexedP
mod.provider = mod_pack.provider;
mod.file_id = mod_version.fileId;
mod.project_id = mod_pack.addonId;
mod.side = stringToSide(mod_pack.side);
mod.side = stringToSide(mod_version.side.isEmpty() ? mod_pack.side : mod_version.side);
mod.loaders = mod_version.loaders;
mod.mcVersions = mod_version.mcVersion;
mod.mcVersions.sort();
mod.releaseType = mod_version.version_type;
return mod;
}
@ -181,6 +185,18 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod)
break;
}
toml::array loaders;
for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Cauldron, ModPlatform::LiteLoader, ModPlatform::Fabric,
ModPlatform::Quilt }) {
if (mod.loaders & loader) {
loaders.push_back(getModLoaderAsString(loader).toStdString());
}
}
toml::array mcVersions;
for (auto version : mod.mcVersions) {
mcVersions.push_back(version.toStdString());
}
if (!index_file.open(QIODevice::ReadWrite)) {
qCritical() << QString("Could not open file %1!").arg(normalized_fname);
return;
@ -192,6 +208,9 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod)
auto tbl = toml::table{ { "name", mod.name.toStdString() },
{ "filename", mod.filename.toStdString() },
{ "side", sideToString(mod.side).toStdString() },
{ "loaders", loaders },
{ "mcVersions", mcVersions },
{ "releaseType", mod.releaseType.toString().toStdString() },
{ "download",
toml::table{
{ "mode", mod.mode.toStdString() },
@ -199,7 +218,7 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod)
{ "hash-format", mod.hash_format.toStdString() },
{ "hash", mod.hash.toStdString() },
} },
{ "update", toml::table{ { ProviderCaps.name(mod.provider), update } } } };
{ "update", toml::table{ { ModPlatform::ProviderCapabilities::name(mod.provider), update } } } };
std::stringstream ss;
ss << tbl;
in_stream << QString::fromStdString(ss.str());
@ -276,6 +295,25 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod
mod.name = stringEntry(table, "name");
mod.filename = stringEntry(table, "filename");
mod.side = stringToSide(stringEntry(table, "side"));
mod.releaseType = ModPlatform::IndexedVersionType(stringEntry(table, "releaseType"));
if (auto loaders = table["loaders"]; loaders && loaders.is_array()) {
for (auto&& loader : *loaders.as_array()) {
if (loader.is_string()) {
mod.loaders |= ModPlatform::getModLoaderFromString(QString::fromStdString(loader.as_string()->value_or("")));
}
}
}
if (auto versions = table["mcVersions"]; versions && versions.is_array()) {
for (auto&& version : *versions.as_array()) {
if (version.is_string()) {
auto ver = QString::fromStdString(version.as_string()->value_or(""));
if (!ver.isEmpty()) {
mod.mcVersions << ver;
}
}
}
mod.mcVersions.sort();
}
}
{ // [download] info
@ -301,11 +339,11 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod
}
toml::table* mod_provider_table = nullptr;
if ((mod_provider_table = update_table[ProviderCaps.name(Provider::FLAME)].as_table())) {
if ((mod_provider_table = update_table[ModPlatform::ProviderCapabilities::name(Provider::FLAME)].as_table())) {
mod.provider = Provider::FLAME;
mod.file_id = intEntry(*mod_provider_table, "file-id");
mod.project_id = intEntry(*mod_provider_table, "project-id");
} else if ((mod_provider_table = update_table[ProviderCaps.name(Provider::MODRINTH)].as_table())) {
} else if ((mod_provider_table = update_table[ModPlatform::ProviderCapabilities::name(Provider::MODRINTH)].as_table())) {
mod.provider = Provider::MODRINTH;
mod.mod_id() = stringEntry(*mod_provider_table, "mod-id");
mod.version() = stringEntry(*mod_provider_table, "version");

View File

@ -2,6 +2,7 @@
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* 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
@ -41,6 +42,9 @@ class V1 {
QString name{};
QString filename{};
Side side{ Side::UniversalSide };
ModPlatform::ModLoaderTypes loaders;
QStringList mcVersions;
ModPlatform::IndexedVersionType releaseType;
// [download]
QString mode{};

View File

@ -83,8 +83,10 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
data = file.readAll();
file.close();
} else {
if (minecraftVersion.isEmpty())
if (minecraftVersion.isEmpty()) {
emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but Minecraft version is unknown"));
return;
}
components->setComponentVersion("net.minecraft", minecraftVersion, true);
components->installJarMods({ modpackJar });
@ -131,7 +133,9 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings,
file.close();
} else {
// This is the "Vanilla" modpack, excluded by the search code
emit failed(tr("Unable to find a \"version.json\"!"));
components->setComponentVersion("net.minecraft", minecraftVersion, true);
components->saveNow();
emit succeeded();
return;
}

View File

@ -84,9 +84,7 @@ auto HttpMetaCache::getEntry(QString base, QString resource_path) -> MetaEntryPt
auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) -> MetaEntryPtr
{
#ifdef Q_OS_WIN
resource_path = FS::RemoveInvalidPathChars(resource_path);
#endif
auto entry = getEntry(base, resource_path);
// it's not present? generate a default stale entry
if (!entry) {
@ -177,6 +175,8 @@ void HttpMetaCache::evictAll()
if (!evictEntry(entry))
qCWarning(taskHttpMetaCacheLogC) << "Unexpected missing cache entry" << entry->m_basePath;
}
map.entry_list.clear();
FS::deletePath(map.base_path);
}
}

View File

@ -51,7 +51,7 @@
Net::NetRequest::Ptr ImgurAlbumCreation::make(std::shared_ptr<ImgurAlbumCreation::Result> output, QList<ScreenShot::Ptr> screenshots)
{
auto up = makeShared<ImgurAlbumCreation>();
up->m_url = BuildConfig.IMGUR_BASE_URL + "album.json";
up->m_url = BuildConfig.IMGUR_BASE_URL + "album";
up->m_sink.reset(new Sink(output));
up->m_screenshots = screenshots;
return up;
@ -72,7 +72,7 @@ void ImgurAlbumCreation::init()
qDebug() << "Setting up imgur upload";
auto api_headers = new Net::StaticHeaderProxy(
QList<Net::HeaderPair>{ { "Content-Type", "application/x-www-form-urlencoded" },
{ "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str() },
{ "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toUtf8() },
{ "Accept", "application/json" } });
addHeaderProxy(api_headers);
}

View File

@ -50,9 +50,8 @@
void ImgurUpload::init()
{
qDebug() << "Setting up imgur upload";
auto api_headers = new Net::StaticHeaderProxy(
QList<Net::HeaderPair>{ { "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str() },
{ "Accept", "application/json" } });
auto api_headers = new Net::StaticHeaderProxy(QList<Net::HeaderPair>{
{ "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toUtf8() }, { "Accept", "application/json" } });
addHeaderProxy(api_headers);
}
@ -70,14 +69,14 @@ QNetworkReply* ImgurUpload::getReply(QNetworkRequest& request)
QHttpPart filePart;
filePart.setBodyDevice(file);
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png");
filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"image\"");
filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"image\"; filename=\"" + file->fileName() + "\"");
multipart->append(filePart);
QHttpPart typePart;
typePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"type\"");
typePart.setBody("file");
multipart->append(typePart);
QHttpPart namePart;
namePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"name\"");
namePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"title\"");
namePart.setBody(m_fileInfo.baseName().toUtf8());
multipart->append(namePart);
@ -124,7 +123,7 @@ auto ImgurUpload::Sink::finalize(QNetworkReply&) -> Task::State
Net::NetRequest::Ptr ImgurUpload::make(ScreenShot::Ptr m_shot)
{
auto up = makeShared<ImgurUpload>(m_shot->m_file);
up->m_url = std::move(BuildConfig.IMGUR_BASE_URL + "upload.json");
up->m_url = std::move(BuildConfig.IMGUR_BASE_URL + "image");
up->m_sink.reset(new Sink(m_shot));
return up;
}

View File

@ -868,30 +868,6 @@ void MainWindow::on_actionCopyInstance_triggered()
runModalTask(task.get());
}
void MainWindow::finalizeInstance(InstancePtr inst)
{
view->updateGeometries();
setSelectedInstanceById(inst->id());
if (APPLICATION->accounts()->anyAccountIsValid()) {
ProgressDialog loadDialog(this);
auto update = inst->createUpdateTask(Net::Mode::Online);
connect(update.get(), &Task::failed, [this](QString reason) {
QString error = QString("Instance load failed: %1").arg(reason);
CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show();
});
if (update) {
loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.execWithTask(update.get());
}
} 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 a Microsoft account."),
QMessageBox::Warning)
->show();
}
}
void MainWindow::addInstance(const QString& url, const QMap<QString, QString>& extra_info)
{
QString groupName;
@ -1584,7 +1560,7 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered()
QFileDialog fileDialog;
// workaround to make sure the portal file dialog opens in the desktop directory
fileDialog.setDirectoryUrl(desktopPath);
desktopFilePath = fileDialog.getSaveFileName(this, tr("Create Shortcut"), desktopFilePath, tr("Desktop Entries (*.desktop)"));
desktopFilePath = fileDialog.getSaveFileName(this, tr("Create Shortcut"), desktopFilePath, tr("Desktop Entries") + " (*.desktop)");
if (desktopFilePath.isEmpty())
return; // file dialog canceled by user
appPath = "flatpak";

View File

@ -229,7 +229,6 @@ class MainWindow : public QMainWindow {
void runModalTask(Task* task);
void instanceFromInstanceTask(InstanceTask* task);
void finalizeInstance(InstancePtr inst);
private:
Ui::MainWindow* ui;

View File

@ -266,7 +266,7 @@ void BlockedModsDialog::addHashTask(QString path)
/// @param path the path to the local file being hashed
void BlockedModsDialog::buildHashTask(QString path)
{
auto hash_task = Hashing::createBlockedModHasher(path, ModPlatform::ResourceProvider::FLAME, m_hash_type);
auto hash_task = Hashing::createHasher(path, m_hash_type);
qDebug() << "[Blocked Mods Dialog] Creating Hash task for path: " << path;

View File

@ -6,8 +6,6 @@
#include "modplatform/ModIndex.h"
static ModPlatform::ProviderCapabilities ProviderCaps;
ChooseProviderDialog::ChooseProviderDialog(QWidget* parent, bool single_choice, bool allow_skipping)
: QDialog(parent), ui(new Ui::ChooseProviderDialog)
{
@ -78,7 +76,7 @@ void ChooseProviderDialog::addProviders()
QRadioButton* btn;
for (auto& provider : { ModPlatform::ResourceProvider::MODRINTH, ModPlatform::ResourceProvider::FLAME }) {
btn = new QRadioButton(ProviderCaps.readableName(provider), this);
btn = new QRadioButton(ModPlatform::ProviderCapabilities::readableName(provider), this);
m_providers.addButton(btn, btn_index++);
ui->providersLayout->addWidget(btn);
}

View File

@ -129,14 +129,14 @@ void ExportPackDialog::done(int result)
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);
tr("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);
tr("CurseForge pack") + " (*.zip)", nullptr);
if (output.isEmpty())
return;
if (!output.endsWith(".zip"))

View File

@ -160,7 +160,7 @@ void ExportToModListDialog::done(int result)
const QString filename = FS::RemoveInvalidFilenameChars(m_name);
const QString output =
QFileDialog::getSaveFileName(this, tr("Export %1").arg(m_name), FS::PathCombine(QDir::homePath(), filename + extension()),
"File (*.txt *.html *.md *.json *.csv)", nullptr);
tr("File") + " (*.txt *.html *.md *.json *.csv)", nullptr);
if (output.isEmpty())
return;

View File

@ -25,8 +25,6 @@
#include <optional>
static ModPlatform::ProviderCapabilities ProviderCaps;
static std::list<Version> mcVersions(BaseInstance* inst)
{
return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() };
@ -94,17 +92,15 @@ void ModUpdateDialog::checkCandidates()
if (!m_modrinth_to_update.empty()) {
m_modrinth_check_task.reset(new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_mod_model));
connect(m_modrinth_check_task.get(), &CheckUpdateTask::checkFailed, this, [this](Mod* mod, QString reason, QUrl recover_url) {
m_failed_check_update.append({ mod, reason, recover_url });
});
connect(m_modrinth_check_task.get(), &CheckUpdateTask::checkFailed, this,
[this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({ mod, reason, recover_url }); });
check_task.addTask(m_modrinth_check_task);
}
if (!m_flame_to_update.empty()) {
m_flame_check_task.reset(new FlameCheckUpdate(m_flame_to_update, versions, loaders, m_mod_model));
connect(m_flame_check_task.get(), &CheckUpdateTask::checkFailed, this, [this](Mod* mod, QString reason, QUrl recover_url) {
m_failed_check_update.append({ mod, reason, recover_url });
});
connect(m_flame_check_task.get(), &CheckUpdateTask::checkFailed, this,
[this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({ mod, reason, recover_url }); });
check_task.addTask(m_flame_check_task);
}
@ -427,7 +423,7 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri
item_top->setExpanded(true);
auto provider_item = new QTreeWidgetItem(item_top);
provider_item->setText(0, tr("Provider: %1").arg(ProviderCaps.readableName(info.provider)));
provider_item->setText(0, tr("Provider: %1").arg(ModPlatform::ProviderCapabilities::readableName(info.provider)));
auto old_version_item = new QTreeWidgetItem(item_top);
old_version_item->setText(0, tr("Old version: %1").arg(info.old_version.isEmpty() ? tr("Not installed") : info.old_version));

View File

@ -200,7 +200,7 @@ void NewInstanceDialog::setSuggestedPack(const QString& name, InstanceTask* task
importVersion.clear();
if (!task) {
ui->iconButton->setIcon(APPLICATION->icons()->getIcon("default"));
ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
importIcon = false;
}
@ -216,7 +216,7 @@ void NewInstanceDialog::setSuggestedPack(const QString& name, QString version, I
importVersion = std::move(version);
if (!task) {
ui->iconButton->setIcon(APPLICATION->icons()->getIcon("default"));
ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey));
importIcon = false;
}
@ -236,6 +236,9 @@ void NewInstanceDialog::setSuggestedIconFromFile(const QString& path, const QStr
void NewInstanceDialog::setSuggestedIcon(const QString& key)
{
if (key == "default")
return;
auto icon = APPLICATION->icons()->getIcon(key);
importIcon = false;

View File

@ -27,6 +27,7 @@
#include "Application.h"
#include "ResourceDownloadTask.h"
#include "minecraft/PackProfile.h"
#include "minecraft/mod/ModFolderModel.h"
#include "minecraft/mod/ResourcePackFolderModel.h"
#include "minecraft/mod/ShaderPackFolderModel.h"
@ -91,6 +92,19 @@ void ResourceDownloadDialog::accept()
void ResourceDownloadDialog::reject()
{
auto selected = getTasks();
if (selected.count() > 0) {
auto reply = CustomMessageBox::selectable(this, tr("Confirmation Needed"),
tr("You have %1 selected resources.\n"
"Are you sure you want to close this dialog?")
.arg(selected.count()),
QMessageBox::Question, QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
->exec();
if (reply != QMessageBox::Yes) {
return;
}
}
if (!geometrySaveKey().isEmpty())
APPLICATION->settings()->set(geometrySaveKey(), saveGeometry().toBase64());
@ -125,8 +139,6 @@ void ResourceDownloadDialog::connectButtons()
connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help);
}
static ModPlatform::ProviderCapabilities ProviderCaps;
void ResourceDownloadDialog::confirm()
{
auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourcesString()));
@ -168,7 +180,7 @@ void ResourceDownloadDialog::confirm()
for (auto& task : selected) {
auto extraInfo = dependencyExtraInfo.value(task->getPack()->addonId.toString());
confirm_dialog->appendResource({ task->getName(), task->getFilename(), task->getCustomPath(),
ProviderCaps.name(task->getProvider()), extraInfo.required_by,
ModPlatform::ProviderCapabilities::name(task->getProvider()), extraInfo.required_by,
task->getVersion().version_type.toString(), !extraInfo.maybe_installed });
}
@ -357,4 +369,20 @@ QList<BasePage*> ShaderPackDownloadDialog::getPages()
return pages;
}
void ModDownloadDialog::setModMetadata(std::shared_ptr<Metadata::ModStruct> meta)
{
switch (meta->provider) {
case ModPlatform::ResourceProvider::MODRINTH:
selectPage(Modrinth::id());
break;
case ModPlatform::ResourceProvider::FLAME:
selectPage(Flame::id());
break;
}
setWindowTitle(tr("Change %1 version").arg(meta->name));
m_container->hidePageList();
m_buttons.hide();
auto page = selectedPage();
page->openProject(meta->project_id);
}
} // namespace ResourceDownload

View File

@ -107,6 +107,8 @@ class ModDownloadDialog final : public ResourceDownloadDialog {
QList<BasePage*> getPages() override;
GetModDependenciesTask::Ptr getModDependenciesTask() override;
void setModMetadata(std::shared_ptr<Metadata::ModStruct>);
private:
BaseInstance* m_instance;
};

View File

@ -118,7 +118,7 @@ void SkinManageDialog::selectionChanged(QItemSelection selected, QItemSelection
auto skin = m_list.skin(key);
if (!skin)
return;
ui->selectedModel->setPixmap(skin->getTexture().scaled(128, 128, Qt::KeepAspectRatio, Qt::FastTransformation));
ui->selectedModel->setPixmap(skin->getTexture().scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation));
ui->capeCombo->setCurrentIndex(m_capes_idx.value(skin->getCapeId()));
ui->steveBtn->setChecked(skin->getModel() == SkinModel::CLASSIC);
ui->alexBtn->setChecked(skin->getModel() == SkinModel::SLIM);
@ -212,7 +212,7 @@ void SkinManageDialog::setupCapes()
void SkinManageDialog::on_capeCombo_currentIndexChanged(int index)
{
auto id = ui->capeCombo->currentData();
ui->capeImage->setPixmap(m_capes.value(id.toString(), {}));
ui->capeImage->setPixmap(m_capes.value(id.toString(), {}).scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation));
if (auto skin = m_list.skin(m_selected_skin); skin) {
skin->setCapeId(id.toString());
}
@ -484,7 +484,7 @@ void SkinManageDialog::on_userBtn_clicked()
if (failReason.isEmpty()) {
failReason = tr("the skin is invalid");
}
CustomMessageBox::selectable(this, tr("Usename not found"),
CustomMessageBox::selectable(this, tr("Username not found"),
tr("Unable to find the skin for '%1'\n because: %2.").arg(user, failReason), QMessageBox::Critical)
->show();
QFile::remove(path);
@ -498,3 +498,15 @@ void SkinManageDialog::on_userBtn_clicked()
}
m_list.updateSkin(&s);
}
void SkinManageDialog::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
QSize s = size() * (1. / 3);
if (auto skin = m_list.skin(m_selected_skin); skin) {
ui->selectedModel->setPixmap(skin->getTexture().scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation));
}
auto id = ui->capeCombo->currentData();
ui->capeImage->setPixmap(m_capes.value(id.toString(), {}).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation));
}

View File

@ -34,6 +34,7 @@ class SkinManageDialog : public QDialog {
public:
explicit SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct);
virtual ~SkinManageDialog();
void resizeEvent(QResizeEvent* event) override;
public slots:
void selectionChanged(QItemSelection, QItemSelection);

View File

@ -24,7 +24,10 @@
<string/>
</property>
<property name="scaledContents">
<bool>true</bool>
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
@ -75,7 +78,10 @@
<string/>
</property>
<property name="scaledContents">
<bool>true</bool>
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>

View File

@ -196,6 +196,7 @@ void LauncherPage::applySettings()
// Updates
if (APPLICATION->updater()) {
APPLICATION->updater()->setAutomaticallyChecksForUpdates(ui->autoUpdateCheckBox->isChecked());
APPLICATION->updater()->setUpdateCheckInterval(ui->updateIntervalSpinBox->value() * 3600);
}
s->set("MenuBarInsteadOfToolBar", ui->preferMenuBarCheckBox->isChecked());
@ -246,6 +247,7 @@ void LauncherPage::loadSettings()
// Updates
if (APPLICATION->updater()) {
ui->autoUpdateCheckBox->setChecked(APPLICATION->updater()->getAutomaticallyChecksForUpdates());
ui->updateIntervalSpinBox->setValue(APPLICATION->updater()->getUpdateCheckInterval() / 3600);
}
// Toolbar/menu bar settings (not applicable if native menu bar is present)

View File

@ -58,6 +58,33 @@
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="updateSetingsLayout">
<item row="0" column="0">
<widget class="QLabel" name="updateIntervalLabel">
<property name="text">
<string>Update interval</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="updateIntervalSpinBox">
<property name="toolTip">
<string>Set it to 0 to only check on launch</string>
</property>
<property name="suffix">
<string>h</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>99999999</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>

View File

@ -371,7 +371,7 @@ void ModrinthManagedPackPage::update()
void ModrinthManagedPackPage::updateFromFile()
{
auto output = QFileDialog::getOpenFileUrl(this, tr("Choose update file"), QDir::homePath(), "Modrinth pack (*.mrpack *.zip)");
auto output = QFileDialog::getOpenFileUrl(this, tr("Choose update file"), QDir::homePath(), tr("Modrinth pack") + " (*.mrpack *.zip)");
if (output.isEmpty())
return;
QMap<QString, QString> extra_info;
@ -538,7 +538,7 @@ void FlameManagedPackPage::update()
void FlameManagedPackPage::updateFromFile()
{
auto output = QFileDialog::getOpenFileUrl(this, tr("Choose update file"), QDir::homePath(), "CurseForge pack (*.zip)");
auto output = QFileDialog::getOpenFileUrl(this, tr("Choose update file"), QDir::homePath(), tr("CurseForge pack") + " (*.zip)");
if (output.isEmpty())
return;

View File

@ -41,6 +41,7 @@
#include "ui_ExternalResourcesPage.h"
#include <QAbstractItemModel>
#include <QAction>
#include <QEvent>
#include <QKeyEvent>
#include <QMenu>
@ -122,13 +123,19 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel>
ui->actionsToolbar->addAction(ui->actionVisitItemPage);
connect(ui->actionVisitItemPage, &QAction::triggered, this, &ModFolderPage::visitModPages);
auto changeVersion = new QAction(tr("Change Version"));
changeVersion->setToolTip(tr("Change mod version"));
changeVersion->setEnabled(false);
ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, changeVersion);
connect(changeVersion, &QAction::triggered, this, &ModFolderPage::changeModVersion);
ui->actionsToolbar->insertActionAfter(ui->actionVisitItemPage, ui->actionExportMetadata);
connect(ui->actionExportMetadata, &QAction::triggered, this, &ModFolderPage::exportModMetadata);
auto check_allow_update = [this] { return ui->treeView->selectionModel()->hasSelection() || !m_model->empty(); };
connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this,
[this, check_allow_update, actionRemoveItemMetadata] {
[this, check_allow_update, actionRemoveItemMetadata, changeVersion] {
ui->actionUpdateItem->setEnabled(check_allow_update());
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes();
@ -138,11 +145,12 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel>
if (selected <= 1) {
ui->actionVisitItemPage->setText(tr("Visit mod's page"));
ui->actionVisitItemPage->setToolTip(tr("Go to mod's home page"));
} else {
ui->actionVisitItemPage->setText(tr("Visit mods' pages"));
ui->actionVisitItemPage->setToolTip(tr("Go to the pages of the selected mods"));
}
changeVersion->setEnabled(mods_list.length() == 1 && mods_list[0]->metadata() != nullptr);
ui->actionVisitItemPage->setEnabled(selected != 0);
actionRemoveItemMetadata->setEnabled(selected != 0);
});
@ -377,6 +385,57 @@ void ModFolderPage::deleteModMetadata()
m_model->deleteModsMetadata(selection);
}
void ModFolderPage::changeModVersion()
{
if (m_instance->typeName() != "Minecraft")
return; // this is a null instance or a legacy instance
auto profile = static_cast<MinecraftInstance*>(m_instance)->getPackProfile();
if (!profile->getModLoaders().has_value()) {
QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!"));
return;
}
if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) {
QMessageBox::critical(this, tr("Error"), tr("Mod updates are unavailable when metadata is disabled!"));
return;
}
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes();
auto mods_list = m_model->selectedMods(selection);
if (mods_list.length() != 1 || mods_list[0]->metadata() == nullptr)
return;
ResourceDownload::ModDownloadDialog mdownload(this, m_model, m_instance);
mdownload.setModMetadata((*mods_list.begin())->metadata());
if (mdownload.exec()) {
auto tasks = new ConcurrentTask(this, "Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt());
connect(tasks, &Task::failed, [this, tasks](QString reason) {
CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show();
tasks->deleteLater();
});
connect(tasks, &Task::aborted, [this, tasks]() {
CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show();
tasks->deleteLater();
});
connect(tasks, &Task::succeeded, [this, tasks]() {
QStringList warnings = tasks->warnings();
if (warnings.count())
CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show();
tasks->deleteLater();
});
for (auto& task : mdownload.getTasks()) {
tasks->addTask(task);
}
ProgressDialog loadDialog(this);
loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.execWithTask(tasks);
m_model->update();
}
}
void ModFolderPage::exportModMetadata()
{
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes();

View File

@ -67,6 +67,7 @@ class ModFolderPage : public ExternalResourcesPage {
void installMods();
void updateMods(bool includeDeps = false);
void visitModPages();
void changeModVersion();
protected:
std::shared_ptr<ModFolderModel> m_model;

View File

@ -297,7 +297,7 @@ void VersionPage::on_actionRemove_triggered()
void VersionPage::on_actionAdd_to_Minecraft_jar_triggered()
{
auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods (*.zip *.jar)"),
auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods") + " (*.zip *.jar)",
APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
if (!list.empty()) {
m_profile->installJarMods(list);
@ -307,7 +307,7 @@ void VersionPage::on_actionAdd_to_Minecraft_jar_triggered()
void VersionPage::on_actionReplace_Minecraft_jar_triggered()
{
auto jarPath = GuiUtil::BrowseForFile("jar", tr("Select jar"), tr("Minecraft.jar replacement (*.jar)"),
auto jarPath = GuiUtil::BrowseForFile("jar", tr("Select jar"), tr("Minecraft.jar replacement") + " (*.jar)",
APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
if (!jarPath.isEmpty()) {
m_profile->installCustomJar(jarPath);
@ -317,7 +317,7 @@ void VersionPage::on_actionReplace_Minecraft_jar_triggered()
void VersionPage::on_actionImport_Components_triggered()
{
QStringList list = GuiUtil::BrowseForFiles("component", tr("Select components"), tr("Components (*.json)"),
QStringList list = GuiUtil::BrowseForFiles("component", tr("Select components"), tr("Components") + " (*.json)",
APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
if (!list.isEmpty()) {
@ -332,7 +332,7 @@ void VersionPage::on_actionImport_Components_triggered()
void VersionPage::on_actionAdd_Agents_triggered()
{
QStringList list = GuiUtil::BrowseForFiles("agent", tr("Select agents"), tr("Java agents (*.jar)"),
QStringList list = GuiUtil::BrowseForFiles("agent", tr("Select agents"), tr("Java agents") + " (*.jar)",
APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget());
if (!list.isEmpty())

View File

@ -343,7 +343,7 @@ void WorldListPage::worldChanged([[maybe_unused]] const QModelIndex& current, [[
void WorldListPage::on_actionAdd_triggered()
{
auto list = GuiUtil::BrowseForFiles(displayName(), tr("Select a Minecraft world zip"), tr("Minecraft World Zip File (*.zip)"),
auto list = GuiUtil::BrowseForFiles(displayName(), tr("Select a Minecraft world zip"), tr("Minecraft World Zip File") + " (*.zip)",
QString(), this->parentWidget());
if (!list.empty()) {
m_worlds->stopWatching();

View File

@ -25,15 +25,21 @@ ResourceAPI::SearchArgs ModModel::createSearchArguments()
Q_ASSERT(m_filter);
std::optional<std::list<Version>> versions{};
std::optional<QStringList> categories{};
auto loaders = profile->getSupportedModLoaders();
{ // Version filter
// Version filter
if (!m_filter->versions.empty())
versions = m_filter->versions;
}
if (m_filter->loaders)
loaders = m_filter->loaders;
if (!m_filter->categoryIds.empty())
categories = m_filter->categoryIds;
auto side = m_filter->side;
auto sort = getCurrentSortingMethodByIndex();
return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, sort, profile->getSupportedModLoaders(), versions };
return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, sort, loaders, versions, side, categories };
}
ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& entry)
@ -45,10 +51,13 @@ ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& en
Q_ASSERT(m_filter);
std::optional<std::list<Version>> versions{};
auto loaders = profile->getSupportedModLoaders();
if (!m_filter->versions.empty())
versions = m_filter->versions;
if (m_filter->loaders)
loaders = m_filter->loaders;
return { pack, versions, profile->getSupportedModLoaders() };
return { pack, versions, loaders };
}
ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(QModelIndex& entry)
@ -79,4 +88,54 @@ bool ModModel::isPackInstalled(ModPlatform::IndexedPack::Ptr pack) const
});
}
QVariant ModModel::getInstalledPackVersion(ModPlatform::IndexedPack::Ptr pack) const
{
auto allMods = static_cast<MinecraftInstance&>(m_base_instance).loaderModList()->allMods();
for (auto mod : allMods) {
if (auto meta = mod->metadata(); meta && meta->provider == pack->provider && meta->project_id == pack->addonId) {
return meta->version();
}
}
return {};
}
bool checkSide(QString filter, QString value)
{
return filter.isEmpty() || value.isEmpty() || filter == "both" || value == "both" || filter == value;
}
bool checkMcVersions(std::list<Version> filter, QStringList value)
{
bool valid = false;
for (auto mcVersion : filter) {
if (value.contains(mcVersion.toString())) {
valid = true;
break;
}
}
return filter.empty() || valid;
}
bool ModModel::checkFilters(ModPlatform::IndexedPack::Ptr pack)
{
if (!m_filter)
return true;
return !(m_filter->hideInstalled && isPackInstalled(pack)) && checkSide(m_filter->side, pack->side);
}
bool ModModel::checkVersionFilters(const ModPlatform::IndexedVersion& v)
{
if (!m_filter)
return true;
auto loaders = static_cast<MinecraftInstance&>(m_base_instance).getPackProfile()->getSupportedModLoaders();
if (m_filter->loaders)
loaders = m_filter->loaders;
return (!optedOut(v) && // is opted out(aka curseforge download link)
(!loaders.has_value() || !v.loaders || loaders.value() & v.loaders) && // loaders
checkSide(m_filter->side, v.side) && // side
(m_filter->releases.empty() || // releases
std::find(m_filter->releases.cbegin(), m_filter->releases.cend(), v.version_type) != m_filter->releases.cend()) &&
checkMcVersions(m_filter->versions, v.mcVersion)); // mcVersions
}
} // namespace ResourceDownload

View File

@ -35,6 +35,7 @@ class ModModel : public ResourceModel {
virtual ModPlatform::IndexedVersion loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) = 0;
void setFilter(std::shared_ptr<ModFilterWidget::Filter> filter) { m_filter = filter; }
virtual QVariant getInstalledPackVersion(ModPlatform::IndexedPack::Ptr) const override;
public slots:
ResourceAPI::SearchArgs createSearchArguments() override;
@ -45,6 +46,9 @@ class ModModel : public ResourceModel {
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0;
virtual bool isPackInstalled(ModPlatform::IndexedPack::Ptr) const override;
virtual bool checkFilters(ModPlatform::IndexedPack::Ptr) override;
virtual bool checkVersionFilters(const ModPlatform::IndexedVersion&) override;
protected:
BaseInstance& m_base_instance;

View File

@ -5,6 +5,7 @@
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* 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
@ -57,7 +58,6 @@ namespace ResourceDownload {
ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance)
{
connect(m_ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch);
connect(m_ui->resourceFilterButton, &QPushButton::clicked, this, &ModPage::filterMods);
connect(m_ui->packView, &QListView::doubleClicked, this, &ModPage::onResourceSelected);
}
@ -67,17 +67,18 @@ void ModPage::setFilterWidget(unique_qobject_ptr<ModFilterWidget>& widget)
if (m_filter_widget)
disconnect(m_filter_widget.get(), nullptr, nullptr, nullptr);
auto old = m_ui->splitter->replaceWidget(0, widget.get());
// because we replaced the widget we also need to delete it
if (old) {
delete old;
}
m_filter_widget.swap(widget);
m_ui->gridLayout_3->addWidget(m_filter_widget.get(), 0, 0, 1, m_ui->gridLayout_3->columnCount());
m_filter_widget->setInstance(&static_cast<MinecraftInstance&>(m_base_instance));
m_filter = m_filter_widget->getFilter();
connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this,
[&] { m_ui->searchButton->setStyleSheet("text-decoration: underline"); });
connect(m_filter_widget.get(), &ModFilterWidget::filterUnchanged, this,
[&] { m_ui->searchButton->setStyleSheet("text-decoration: none"); });
connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, &ModPage::triggerSearch);
prepareProviderCategories();
}
/******** Callbacks to events in the UI (set up in the derived classes) ********/
@ -89,6 +90,7 @@ void ModPage::filterMods()
void ModPage::triggerSearch()
{
auto changed = m_filter_widget->changed();
m_filter = m_filter_widget->getFilter();
m_ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect);
m_ui->packView->clearSelection();
@ -96,7 +98,7 @@ void ModPage::triggerSearch()
m_ui->versionSelectionBox->clear();
updateSelectionButton();
static_cast<ModModel*>(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), m_filter_widget->changed());
static_cast<ModModel*>(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), changed);
m_fetch_progress.watch(m_model->activeSearchJob().get());
}
@ -111,40 +113,6 @@ QMap<QString, QString> ModPage::urlHandlers() const
/******** Make changes to the UI ********/
void ModPage::updateVersionList()
{
m_ui->versionSelectionBox->clear();
auto packProfile = (dynamic_cast<MinecraftInstance&>(m_base_instance)).getPackProfile();
QString mcVersion = packProfile->getComponentVersion("net.minecraft");
auto current_pack = getCurrentPack();
if (!current_pack)
return;
for (int i = 0; i < current_pack->versions.size(); i++) {
auto version = current_pack->versions[i];
bool valid = false;
for (auto& mcVer : m_filter->versions) {
if (validateVersion(version, mcVer.toString(), packProfile->getSupportedModLoaders())) {
valid = true;
break;
}
}
// Only add the version if it's valid or using the 'Any' filter, but never if the version is opted out
if ((valid || m_filter->versions.empty()) && !optedOut(version)) {
auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : "";
m_ui->versionSelectionBox->addItem(QString("%1%2").arg(version.version, release_type), QVariant(i));
}
}
if (m_ui->versionSelectionBox->count() == 0) {
m_ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1));
m_ui->resourceSelectionButton->setText(tr("Cannot select invalid version :("));
}
updateSelectionButton();
}
void ModPage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack,
ModPlatform::IndexedVersion& version,
const std::shared_ptr<ResourceFolderModel> base_model)

View File

@ -31,8 +31,7 @@ class ModPage : public ResourcePage {
auto page = new T(dialog, instance);
auto model = static_cast<ModModel*>(page->getModel());
auto filter_widget =
ModFilterWidget::create(static_cast<MinecraftInstance&>(instance).getPackProfile()->getComponentVersion("net.minecraft"), page);
auto filter_widget = page->createFilterWidget();
page->setFilterWidget(filter_widget);
model->setFilter(page->getFilter());
@ -51,20 +50,17 @@ class ModPage : public ResourcePage {
void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr<ResourceFolderModel>) override;
virtual auto validateVersion(ModPlatform::IndexedVersion& ver,
QString mineVer,
std::optional<ModPlatform::ModLoaderTypes> loaders = {}) const -> bool = 0;
virtual unique_qobject_ptr<ModFilterWidget> createFilterWidget() = 0;
[[nodiscard]] bool supportsFiltering() const override { return true; };
auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; }
void setFilterWidget(unique_qobject_ptr<ModFilterWidget>&);
public slots:
void updateVersionList() override;
protected:
ModPage(ModDownloadDialog* dialog, BaseInstance& instance);
virtual void prepareProviderCategories() {};
protected slots:
virtual void filterMods();
void triggerSearch() override;

View File

@ -410,12 +410,17 @@ void ResourceModel::searchRequestSucceeded(QJsonDocument& doc)
m_search_state = SearchState::CanFetchMore;
}
QList<ModPlatform::IndexedPack::Ptr> filteredNewList;
for (auto p : newList)
if (checkFilters(p))
filteredNewList << p;
// When you have a Qt build with assertions turned on, proceeding here will abort the application
if (newList.size() == 0)
if (filteredNewList.size() == 0)
return;
beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + newList.size() - 1);
m_packs.append(newList);
beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + filteredNewList.size() - 1);
m_packs.append(filteredNewList);
endInsertRows();
}
@ -558,4 +563,8 @@ void ResourceModel::removePack(const QString& rem)
ver.is_currently_selected = false;
}
bool ResourceModel::checkVersionFilters(const ModPlatform::IndexedVersion& v)
{
return (!optedOut(v));
}
} // namespace ResourceDownload

View File

@ -11,6 +11,7 @@
#include "QObjectPtr.h"
#include "ResourceDownloadTask.h"
#include "modplatform/ModIndex.h"
#include "modplatform/ResourceAPI.h"
#include "tasks/ConcurrentTask.h"
@ -55,6 +56,17 @@ class ResourceModel : public QAbstractListModel {
[[nodiscard]] auto getSortingMethods() const { return m_api->getSortingMethods(); }
virtual QVariant getInstalledPackVersion(ModPlatform::IndexedPack::Ptr) const { return {}; }
/** Whether the version is opted out or not. Currently only makes sense in CF. */
virtual bool optedOut(const ModPlatform::IndexedVersion& ver) const
{
Q_UNUSED(ver);
return false;
};
virtual bool checkFilters(ModPlatform::IndexedPack::Ptr) { return true; }
virtual bool checkVersionFilters(const ModPlatform::IndexedVersion&);
public slots:
void fetchMore(const QModelIndex& parent) override;
// NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12

View File

@ -15,7 +15,6 @@ namespace ResourceDownload {
ResourcePackResourcePage::ResourcePackResourcePage(ResourceDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance)
{
connect(m_ui->searchButton, &QPushButton::clicked, this, &ResourcePackResourcePage::triggerSearch);
connect(m_ui->packView, &QListView::doubleClicked, this, &ResourcePackResourcePage::onResourceSelected);
}

View File

@ -5,6 +5,7 @@
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* 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
@ -67,11 +68,13 @@ ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_in
connect(&m_search_timer, &QTimer::timeout, this, &ResourcePage::triggerSearch);
// hide progress bar to prevent weird artifact
m_fetch_progress.hide();
m_fetch_progress.hideIfInactive(true);
m_fetch_progress.setFixedHeight(24);
m_fetch_progress.progressFormat("");
m_ui->gridLayout_3->addWidget(&m_fetch_progress, 0, 0, 1, m_ui->gridLayout_3->columnCount());
m_ui->verticalLayout->insertWidget(1, &m_fetch_progress);
m_ui->packView->setItemDelegate(new ProjectItemDelegate(this));
m_ui->packView->installEventFilter(this);
@ -93,8 +96,10 @@ void ResourcePage::retranslate()
void ResourcePage::openedImpl()
{
if (!supportsFiltering())
if (!supportsFiltering()) {
m_ui->resourceFilterButton->setVisible(false);
m_ui->filterWidget->hide();
}
//: String in the search bar of the mod downloading dialog
m_ui->searchEdit->setPlaceholderText(tr("Search for %1...").arg(resourcesString()));
@ -266,18 +271,21 @@ void ResourcePage::updateVersionList()
m_ui->versionSelectionBox->clear();
m_ui->versionSelectionBox->blockSignals(false);
if (current_pack)
if (current_pack) {
auto installedVersion = m_model->getInstalledPackVersion(current_pack);
for (int i = 0; i < current_pack->versions.size(); i++) {
auto& version = current_pack->versions[i];
if (optedOut(version))
if (!m_model->checkVersionFilters(version))
continue;
auto release_type = current_pack->versions[i].version_type.isValid()
? QString(" [%1]").arg(current_pack->versions[i].version_type.toString())
: "";
m_ui->versionSelectionBox->addItem(current_pack->versions[i].version, QVariant(i));
}
m_ui->versionSelectionBox->addItem(QString("%1%2").arg(version.version, release_type), QVariant(i));
}
}
if (m_ui->versionSelectionBox->count() == 0) {
m_ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1));
m_ui->resourceSelectionButton->setText(tr("Cannot select invalid version :("));
@ -394,7 +402,7 @@ void ResourcePage::openUrl(const QUrl& url)
}
}
if (!page.isNull()) {
if (!page.isNull() && !m_do_not_jump_to_mod) {
const QString slug = match.captured(1);
// ensure the user isn't opening the same mod
@ -438,4 +446,52 @@ void ResourcePage::openUrl(const QUrl& url)
QDesktopServices::openUrl(url);
}
void ResourcePage::openProject(QVariant projectID)
{
m_ui->sortByBox->hide();
m_ui->searchEdit->hide();
m_ui->resourceFilterButton->hide();
m_ui->packView->hide();
m_ui->resourceSelectionButton->hide();
m_do_not_jump_to_mod = true;
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
auto okBtn = buttonBox->button(QDialogButtonBox::Ok);
okBtn->setDefault(true);
okBtn->setAutoDefault(true);
okBtn->setText(tr("Reinstall"));
okBtn->setShortcut(tr("Ctrl+Return"));
okBtn->setEnabled(false);
auto cancelBtn = buttonBox->button(QDialogButtonBox::Cancel);
cancelBtn->setDefault(false);
cancelBtn->setAutoDefault(false);
connect(okBtn, &QPushButton::clicked, this, [this] {
onResourceSelected();
m_parent_dialog->accept();
});
connect(cancelBtn, &QPushButton::clicked, m_parent_dialog, &ResourceDownloadDialog::reject);
m_ui->gridLayout_4->addWidget(buttonBox, 1, 2);
auto jump = [this, okBtn] {
for (int row = 0; row < m_model->rowCount({}); row++) {
const QModelIndex index = m_model->index(row);
m_ui->packView->setCurrentIndex(index);
okBtn->setEnabled(true);
return;
}
m_ui->packDescription->setText(tr("The resource was not found"));
};
m_ui->searchEdit->setText("#" + projectID.toString());
triggerSearch();
if (m_model->hasActiveSearchJob())
connect(m_model->activeSearchJob().get(), &Task::finished, jump);
else
jump();
}
} // namespace ResourceDownload

View File

@ -83,8 +83,10 @@ class ResourcePage : public QWidget, public BasePage {
QList<DownloadTaskPtr> selectedPacks() { return m_model->selectedPacks(); }
bool hasSelectedPacks() { return !(m_model->selectedPacks().isEmpty()); }
virtual void openProject(QVariant projectID);
protected slots:
virtual void triggerSearch() {}
virtual void triggerSearch() = 0;
void onSelectionChanged(QModelIndex first, QModelIndex second);
void onVersionSelectionChanged(QString data);
@ -96,13 +98,6 @@ class ResourcePage : public QWidget, public BasePage {
virtual QMap<QString, QString> urlHandlers() const = 0;
virtual void openUrl(const QUrl&);
/** Whether the version is opted out or not. Currently only makes sense in CF. */
virtual bool optedOut(ModPlatform::IndexedVersion& ver) const
{
Q_UNUSED(ver);
return false;
};
public:
BaseInstance& m_base_instance;
@ -118,6 +113,8 @@ class ResourcePage : public QWidget, public BasePage {
// Used to do instant searching with a delay to cache quick changes
QTimer m_search_timer;
bool m_do_not_jump_to_mod = false;
};
} // namespace ResourceDownload

View File

@ -10,20 +10,30 @@
<height>685</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0" colspan="4">
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="2">
<widget class="ProjectDescriptionPage" name="packDescription">
<property name="openExternalLinks">
<bool>false</bool>
</property>
<property name="openLinks">
<bool>false</bool>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="resourceFilterButton">
<property name="text">
<string>Filter options</string>
</property>
</widget>
</item>
<item row="1" column="0">
<item>
<widget class="QLineEdit" name="searchEdit"/>
</item>
</layout>
</item>
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="childrenCollapsible">
<bool>false</bool>
</property>
<widget class="QWidget" name="filterWidget" native="true"/>
<widget class="QListView" name="packView">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
@ -38,20 +48,17 @@
</size>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="3">
<widget class="QPushButton" name="searchButton">
<property name="text">
<string>Search</string>
<widget class="ProjectDescriptionPage" name="packDescription">
<property name="openExternalLinks">
<bool>false</bool>
</property>
<property name="openLinks">
<bool>false</bool>
</property>
</widget>
</widget>
</item>
<item row="0" column="0">
<widget class="QLineEdit" name="searchEdit"/>
</item>
<item row="2" column="0" colspan="4">
<item>
<layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0">
<item row="0" column="2">
<widget class="QComboBox" name="versionSelectionBox"/>
@ -74,20 +81,6 @@
</item>
</layout>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="resourceFilterButton">
<property name="text">
<string>Filter options</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
@ -98,8 +91,6 @@
</customwidget>
</customwidgets>
<tabstops>
<tabstop>searchEdit</tabstop>
<tabstop>searchButton</tabstop>
<tabstop>packView</tabstop>
<tabstop>packDescription</tabstop>
<tabstop>sortByBox</tabstop>

View File

@ -16,7 +16,6 @@ namespace ResourceDownload {
ShaderPackResourcePage::ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance)
{
connect(m_ui->searchButton, &QPushButton::clicked, this, &ShaderPackResourcePage::triggerSearch);
connect(m_ui->packView, &QListView::doubleClicked, this, &ShaderPackResourcePage::onResourceSelected);
}

View File

@ -41,7 +41,6 @@ class TexturePackResourcePage : public ResourcePackResourcePage {
protected:
TexturePackResourcePage(TexturePackDownloadDialog* dialog, BaseInstance& instance) : ResourcePackResourcePage(dialog, instance)
{
connect(m_ui->searchButton, &QPushButton::clicked, this, &TexturePackResourcePage::triggerSearch);
connect(m_ui->packView, &QListView::doubleClicked, this, &TexturePackResourcePage::onResourceSelected);
}
};

View File

@ -10,72 +10,8 @@
<height>685</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0">
<item row="0" column="2">
<widget class="QComboBox" name="versionSelectionBox"/>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label">
<property name="text">
<string>Version selected:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QComboBox" name="sortByBox"/>
</item>
</layout>
</item>
<item row="2" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="1">
<widget class="QTextBrowser" name="packDescription">
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="openLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QTreeView" name="packView">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="iconSize">
<size>
<width>96</width>
<height>48</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>Search and filter...</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="font">
<font>
@ -93,6 +29,63 @@
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>Search and filter...</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QTreeView" name="packView">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="iconSize">
<size>
<width>96</width>
<height>48</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QTextBrowser" name="packDescription">
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="openLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QComboBox" name="sortByBox"/>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Version selected:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="versionSelectionBox"/>
</item>
</layout>
</item>
</layout>
</widget>
<tabstops>

View File

@ -56,7 +56,6 @@ 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);
ui->searchEdit->installEventFilter(this);
listModel = new Flame::ListModel(this);
ui->packView->setModel(listModel);
@ -73,7 +72,7 @@ FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent)
m_fetch_progress.setFixedHeight(24);
m_fetch_progress.progressFormat("");
ui->gridLayout->addWidget(&m_fetch_progress, 2, 0, 1, ui->gridLayout->columnCount());
ui->verticalLayout->insertWidget(2, &m_fetch_progress);
// index is used to set the sorting with the curseforge api
ui->sortByBox->addItem(tr("Sort by Featured"));

View File

@ -10,8 +10,8 @@
<height>600</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="font">
<font>
@ -29,8 +29,6 @@
</property>
</widget>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout">
<item>
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
@ -39,15 +37,6 @@
</widget>
</item>
<item>
<widget class="QPushButton" name="searchButton">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<layout class="QHBoxLayout">
<item>
<widget class="QListView" name="packView">
@ -77,7 +66,7 @@
</item>
</layout>
</item>
<item row="4" column="0">
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QComboBox" name="sortByBox"/>

View File

@ -6,11 +6,17 @@
#include "Json.h"
#include "minecraft/PackProfile.h"
#include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlameModIndex.h"
namespace ResourceDownload {
static bool isOptedOut(const ModPlatform::IndexedVersion& ver)
{
return ver.downloadUrl.isEmpty();
}
FlameModModel::FlameModModel(BaseInstance& base) : ModModel(base, new FlameAPI) {}
void FlameModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
@ -34,6 +40,11 @@ auto FlameModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJs
return FlameMod::loadDependencyVersions(m, arr, &m_base_instance);
}
bool FlameModModel::optedOut(const ModPlatform::IndexedVersion& ver) const
{
return isOptedOut(ver);
}
auto FlameModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return Json::ensureArray(obj.object(), "data");
@ -57,6 +68,11 @@ void FlameResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
}
bool FlameResourcePackModel::optedOut(const ModPlatform::IndexedVersion& ver) const
{
return isOptedOut(ver);
}
auto FlameResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return Json::ensureArray(obj.object(), "data");
@ -116,6 +132,11 @@ ResourceAPI::VersionSearchArgs FlameTexturePackModel::createVersionsArguments(QM
return args;
}
bool FlameTexturePackModel::optedOut(const ModPlatform::IndexedVersion& ver) const
{
return isOptedOut(ver);
}
auto FlameTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return Json::ensureArray(obj.object(), "data");
@ -139,6 +160,11 @@ void FlameShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m,
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance);
}
bool FlameShaderPackModel::optedOut(const ModPlatform::IndexedVersion& ver) const
{
return isOptedOut(ver);
}
auto FlameShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return Json::ensureArray(obj.object(), "data");

View File

@ -17,6 +17,8 @@ class FlameModModel : public ModModel {
FlameModModel(BaseInstance&);
~FlameModModel() override = default;
bool optedOut(const ModPlatform::IndexedVersion& ver) const override;
private:
[[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; }
[[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); }
@ -36,6 +38,8 @@ class FlameResourcePackModel : public ResourcePackResourceModel {
FlameResourcePackModel(const BaseInstance&);
~FlameResourcePackModel() override = default;
bool optedOut(const ModPlatform::IndexedVersion& ver) const override;
private:
[[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; }
[[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); }
@ -54,6 +58,8 @@ class FlameTexturePackModel : public TexturePackResourceModel {
FlameTexturePackModel(const BaseInstance&);
~FlameTexturePackModel() override = default;
bool optedOut(const ModPlatform::IndexedVersion& ver) const override;
private:
[[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; }
[[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); }
@ -75,6 +81,8 @@ class FlameShaderPackModel : public ShaderPackResourceModel {
FlameShaderPackModel(const BaseInstance&);
~FlameShaderPackModel() override = default;
bool optedOut(const ModPlatform::IndexedVersion& ver) const override;
private:
[[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; }
[[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); }

View File

@ -5,6 +5,7 @@
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* 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
@ -37,6 +38,10 @@
*/
#include "FlameResourcePages.h"
#include <QList>
#include <memory>
#include "modplatform/ModIndex.h"
#include "modplatform/flame/FlameAPI.h"
#include "ui_ResourcePage.h"
#include "FlameResourceModels.h"
@ -44,11 +49,6 @@
namespace ResourceDownload {
static bool isOptedOut(ModPlatform::IndexedVersion const& ver)
{
return ver.downloadUrl.isEmpty();
}
FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ModPage(dialog, instance)
{
m_model = new FlameModModel(instance);
@ -66,19 +66,6 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance) :
m_ui->packDescription->setMetaEntry(metaEntryBase());
}
auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver,
QString mineVer,
std::optional<ModPlatform::ModLoaderTypes> loaders) const -> bool
{
return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty() &&
(!loaders.has_value() || !ver.loaders || loaders.value() & ver.loaders);
}
bool FlameModPage::optedOut(ModPlatform::IndexedVersion& ver) const
{
return isOptedOut(ver);
}
void FlameModPage::openUrl(const QUrl& url)
{
if (url.scheme().isEmpty()) {
@ -113,11 +100,6 @@ FlameResourcePackPage::FlameResourcePackPage(ResourcePackDownloadDialog* dialog,
m_ui->packDescription->setMetaEntry(metaEntryBase());
}
bool FlameResourcePackPage::optedOut(ModPlatform::IndexedVersion& ver) const
{
return isOptedOut(ver);
}
void FlameResourcePackPage::openUrl(const QUrl& url)
{
if (url.scheme().isEmpty()) {
@ -152,11 +134,6 @@ FlameTexturePackPage::FlameTexturePackPage(TexturePackDownloadDialog* dialog, Ba
m_ui->packDescription->setMetaEntry(metaEntryBase());
}
bool FlameTexturePackPage::optedOut(ModPlatform::IndexedVersion& ver) const
{
return isOptedOut(ver);
}
void FlameTexturePackPage::openUrl(const QUrl& url)
{
if (url.scheme().isEmpty()) {
@ -191,11 +168,6 @@ FlameShaderPackPage::FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseI
m_ui->packDescription->setMetaEntry(metaEntryBase());
}
bool FlameShaderPackPage::optedOut(ModPlatform::IndexedVersion& ver) const
{
return isOptedOut(ver);
}
void FlameShaderPackPage::openUrl(const QUrl& url)
{
if (url.scheme().isEmpty()) {
@ -232,4 +204,19 @@ auto FlameShaderPackPage::shouldDisplay() const -> bool
return true;
}
unique_qobject_ptr<ModFilterWidget> FlameModPage::createFilterWidget()
{
return ModFilterWidget::create(&static_cast<MinecraftInstance&>(m_base_instance), false, this);
}
void FlameModPage::prepareProviderCategories()
{
auto response = std::make_shared<QByteArray>();
auto task = FlameAPI::getModCategories(response);
QObject::connect(task.get(), &Task::succeeded, [this, response]() {
auto categories = FlameAPI::loadModCategories(response);
m_filter_widget->setCategories(categories);
});
task->start();
};
} // namespace ResourceDownload

View File

@ -5,6 +5,7 @@
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* 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
@ -94,12 +95,11 @@ class FlameModPage : public ModPage {
[[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; }
bool validateVersion(ModPlatform::IndexedVersion& ver,
QString mineVer,
std::optional<ModPlatform::ModLoaderTypes> loaders = {}) const override;
bool optedOut(ModPlatform::IndexedVersion& ver) const override;
void openUrl(const QUrl& url) override;
unique_qobject_ptr<ModFilterWidget> createFilterWidget() override;
protected:
virtual void prepareProviderCategories() override;
};
class FlameResourcePackPage : public ResourcePackResourcePage {
@ -124,8 +124,6 @@ class FlameResourcePackPage : public ResourcePackResourcePage {
[[nodiscard]] inline auto helpPage() const -> QString override { return ""; }
bool optedOut(ModPlatform::IndexedVersion& ver) const override;
void openUrl(const QUrl& url) override;
};
@ -151,8 +149,6 @@ class FlameTexturePackPage : public TexturePackResourcePage {
[[nodiscard]] inline auto helpPage() const -> QString override { return ""; }
bool optedOut(ModPlatform::IndexedVersion& ver) const override;
void openUrl(const QUrl& url) override;
};
@ -178,8 +174,6 @@ class FlameShaderPackPage : public ShaderPackResourcePage {
[[nodiscard]] inline auto helpPage() const -> QString override { return ""; }
bool optedOut(ModPlatform::IndexedVersion& ver) const override;
void openUrl(const QUrl& url) override;
};

View File

@ -10,8 +10,49 @@
<height>1011</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="1">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Note: If your FTB instances are not in the default location, select it using the button next to search.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>Search and filter...</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="browseButton">
<property name="toolTip">
<string>Select FTBApp instances directory</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="viewfolder">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTreeView" name="modpackList">
<property name="maximumSize">
<size>
@ -21,7 +62,7 @@
</property>
</widget>
</item>
<item row="3" column="1">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QComboBox" name="sortByBox">
@ -48,54 +89,6 @@
</item>
</layout>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>Search and filter...</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="browseButton">
<property name="toolTip">
<string>Select FTBApp instances directory</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="viewfolder">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label">
<property name="text">
<string>Note: If your FTB instances are not in the default location, select it using the button next to search.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>

View File

@ -10,8 +10,8 @@
<height>602</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="searchEdit">
@ -23,16 +23,9 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0">
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
@ -134,22 +127,9 @@
</widget>
</widget>
</item>
<item row="5" column="0">
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="1">
<widget class="QLabel" name="label">
<property name="text">
<string>Version selected:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QComboBox" name="versionSelectionBox"/>
</item>
<item row="0" column="0">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QComboBox" name="sortByBox">
<property name="minimumSize">
<size>
@ -159,6 +139,19 @@
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Version selected:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="versionSelectionBox"/>
</item>
</layout>
</item>
</layout>

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