diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 60bd86eec..d91d9507a 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -25,7 +25,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - name: Create backport PRs - uses: korthout/backport-action@v2.5.0 + uses: korthout/backport-action@v3.0.2 with: # Config README: https://github.com/korthout/backport-action#backport-action pull_description: |- diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 999029bd2..8212af233 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 @@ -611,7 +611,7 @@ jobs: flatpak: runs-on: ubuntu-latest container: - image: bilelmoussaoui/flatpak-github-actions:kde-5.15-23.08 + image: bilelmoussaoui/flatpak-github-actions:kde-6.7 options: --privileged steps: - name: Checkout diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 855b105ea..2226d0710 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -17,9 +17,9 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: cachix/install-nix-action@8887e596b4ee1134dae06b98d573bd674693f47c # v26 + - 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" diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 77b6eef54..bda80ac72 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -163,7 +163,6 @@ class Config { QString RESOURCE_BASE = "https://resources.download.minecraft.net/"; QString LIBRARY_BASE = "https://libraries.minecraft.net/"; - QString AUTH_BASE = "https://authserver.mojang.com/"; QString IMGUR_BASE_URL = "https://api.imgur.com/3/"; QString FMLLIBS_BASE_URL = "https://files.prismlauncher.org/fmllibs/"; // FIXME: move into CMakeLists QString TRANSLATIONS_BASE_URL = "https://i18n.prismlauncher.org/"; // FIXME: move into CMakeLists diff --git a/flake.lock b/flake.lock index 740d5c43e..caed1a708 100644 --- a/flake.lock +++ b/flake.lock @@ -23,11 +23,11 @@ ] }, "locked": { - "lastModified": 1714641030, - "narHash": "sha256-yzcRNDoyVP7+SCNX0wmuDju1NUCt8Dz9+lyUXEI0dbI=", + "lastModified": 1717285511, + "narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "e5d10a24b66c3ea8f150e47dfdb0416ab7c3390e", + "rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8", "type": "github" }, "original": { @@ -36,24 +36,6 @@ "type": "github" } }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, "gitignore": { "inputs": { "nixpkgs": [ @@ -93,16 +75,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1715413075, - "narHash": "sha256-FCi3R1MeS5bVp0M0xTheveP6hhcCYfW/aghSTPebYL4=", - "owner": "nixos", + "lastModified": 1718276985, + "narHash": "sha256-u1fA0DYQYdeG+5kDm1bOoGcHtX0rtC7qs2YA2N1X++I=", + "owner": "NixOS", "repo": "nixpkgs", - "rev": "e4e7a43a9db7e22613accfeb1005cca1b2b1ee0d", + "rev": "3f84a279f1a6290ce154c5531378acc827836fbb", "type": "github" }, "original": { - "owner": "nixos", - "ref": "nixpkgs-unstable", + "owner": "NixOS", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } @@ -112,7 +94,6 @@ "flake-compat": [ "flake-compat" ], - "flake-utils": "flake-utils", "gitignore": "gitignore", "nixpkgs": [ "nixpkgs" @@ -122,11 +103,11 @@ ] }, "locked": { - "lastModified": 1714478972, - "narHash": "sha256-q//cgb52vv81uOuwz1LaXElp3XAe1TqrABXODAEF6Sk=", + "lastModified": 1717664902, + "narHash": "sha256-7XfBuLULizXjXfBYy/VV+SpYMHreNRHk9nKMsm1bgb4=", "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "2849da033884f54822af194400f8dff435ada242", + "rev": "cc4d466cb1254af050ff7bdf47f6d404a7c646d1", "type": "github" }, "original": { @@ -143,21 +124,6 @@ "nixpkgs": "nixpkgs", "pre-commit-hooks": "pre-commit-hooks" } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index e16c76699..7cef5217a 100644 --- a/flake.nix +++ b/flake.nix @@ -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"; diff --git a/flatpak/org.prismlauncher.PrismLauncher.yml b/flatpak/org.prismlauncher.PrismLauncher.yml index b4c6e8143..bd09f7fd8 100644 --- a/flatpak/org.prismlauncher.PrismLauncher.yml +++ b/flatpak/org.prismlauncher.PrismLauncher.yml @@ -1,6 +1,6 @@ id: org.prismlauncher.PrismLauncher runtime: org.kde.Platform -runtime-version: 5.15-23.08 +runtime-version: 6.7 sdk: org.kde.Sdk sdk-extensions: - org.freedesktop.Sdk.Extension.openjdk21 @@ -38,7 +38,6 @@ modules: config-opts: - -DLauncher_BUILD_PLATFORM=flatpak - -DCMAKE_BUILD_TYPE=RelWithDebInfo - - -DLauncher_QT_VERSION_MAJOR=5 build-options: env: JAVA_HOME: /usr/lib/sdk/openjdk17/jvm/openjdk-17 @@ -65,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 diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 96a50f2ba..6d30d1101 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -395,20 +395,15 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) { static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "-%0.log"; static const QString logBase = FS::PathCombine("logs", baseLogFile); - auto moveFile = [](const QString& oldName, const QString& newName) { - QFile::remove(newName); - QFile::copy(oldName, newName); - QFile::remove(oldName); - }; if (FS::ensureFolderPathExists("logs")) { // if this did not fail for (auto i = 0; i <= 4; i++) if (auto oldName = baseLogFile.arg(i); QFile::exists(oldName)) // do not pointlessly delete new files if the old ones are not there - moveFile(oldName, logBase.arg(i)); + FS::move(oldName, logBase.arg(i)); } for (auto i = 4; i > 0; i--) - moveFile(logBase.arg(i - 1), logBase.arg(i)); + FS::move(logBase.arg(i - 1), logBase.arg(i)); logFile = std::unique_ptr(new QFile(logBase.arg(0))); if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { @@ -593,6 +588,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("IconsDir", "icons"); m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); m_settings->registerSetting("DownloadsDirWatchRecursive", false); + m_settings->registerSetting("SkinsDir", "skins"); // Editors m_settings->registerSetting("JsonEditor", QString()); @@ -851,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."; @@ -949,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; } } @@ -982,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; } } @@ -995,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); @@ -1211,6 +1198,12 @@ void Application::performMainStartupAction() qDebug() << "<> Updater started."; } + { // delete instances tmp dirctory + auto instDir = m_settings->get("InstanceDir").toString(); + const QString tempRoot = FS::PathCombine(instDir, ".tmp"); + FS::deletePath(tempRoot); + } + if (!m_urlsToImport.isEmpty()) { qDebug() << "<> Importing from url:" << m_urlsToImport; m_mainWindow->processURLs(m_urlsToImport); @@ -1671,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) { diff --git a/launcher/BaseInstaller.cpp b/launcher/BaseInstaller.cpp index 1ff86ed40..96a3b5ebe 100644 --- a/launcher/BaseInstaller.cpp +++ b/launcher/BaseInstaller.cpp @@ -16,6 +16,7 @@ #include #include "BaseInstaller.h" +#include "FileSystem.h" #include "minecraft/MinecraftInstance.h" BaseInstaller::BaseInstaller() {} @@ -42,7 +43,7 @@ bool BaseInstaller::add(MinecraftInstance* to) bool BaseInstaller::remove(MinecraftInstance* from) { - return QFile::remove(filename(from->instanceRoot())); + return FS::deletePath(filename(from->instanceRoot())); } QString BaseInstaller::filename(const QString& root) const diff --git a/launcher/BaseInstaller.h b/launcher/BaseInstaller.h index 6244ced7d..1cf7d65f5 100644 --- a/launcher/BaseInstaller.h +++ b/launcher/BaseInstaller.h @@ -29,7 +29,7 @@ class BaseVersion; class BaseInstaller { public: BaseInstaller(); - virtual ~BaseInstaller(){}; + virtual ~BaseInstaller() {}; bool isApplied(MinecraftInstance* on); virtual bool add(MinecraftInstance* to); diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index cda44b454..69cf95e3c 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -269,13 +269,18 @@ void BaseInstance::setRunning(bool running) m_isRunning = running; - if (!m_settings->get("RecordGameTime").toBool()) { - emit runningStatusChanged(running); + 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 diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index f4ed9113c..499ec7866 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -104,6 +104,7 @@ class BaseInstance : public QObject, public std::enable_shared_from_this:;*|!+\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) diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index 5496c3795..23bf5f16e 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -240,6 +240,7 @@ class create_link : public QObject { bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } int totalLinked() { return m_linked; } + int totalToLink() { return static_cast(m_links_to_make.size()); } void runPrivileged() { runPrivileged(QString()); } void runPrivileged(const QString& offset); @@ -378,6 +379,7 @@ enum class FilesystemType { HFSX, FUSEBLK, F2FS, + BCACHEFS, UNKNOWN }; @@ -406,6 +408,7 @@ static const QMap s_filesystem_type_names = { { Fil { FilesystemType::HFSX, { "HFSX" } }, { FilesystemType::FUSEBLK, { "FUSEBLK" } }, { FilesystemType::F2FS, { "F2FS" } }, + { FilesystemType::BCACHEFS, { "BCACHEFS" } }, { FilesystemType::UNKNOWN, { "UNKNOWN" } } }; /** @@ -458,7 +461,7 @@ QString nearestExistentAncestor(const QString& path); FilesystemInfo statFS(const QString& path); static const QList s_clone_filesystems = { FilesystemType::BTRFS, FilesystemType::APFS, FilesystemType::ZFS, - FilesystemType::XFS, FilesystemType::REFS }; + FilesystemType::XFS, FilesystemType::REFS, FilesystemType::BCACHEFS }; /** * @brief if the Filesystem is reflink/clone capable diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp index 52eb7d879..ff2d37723 100644 --- a/launcher/InstanceCopyTask.cpp +++ b/launcher/InstanceCopyTask.cpp @@ -1,10 +1,12 @@ #include "InstanceCopyTask.h" #include #include +#include #include "FileSystem.h" #include "NullInstance.h" #include "pathmatcher/RegexpMatcher.h" #include "settings/INISettingsObject.h" +#include "tasks/Task.h" InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs) { @@ -38,38 +40,50 @@ void InstanceCopyTask::executeTask() { setStatus(tr("Copying instance %1").arg(m_origInstance->name())); - auto copySaves = [&]() { - QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft")); - QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft")); - - QString staging_mc_dir; - if (dotMCDir.exists() && !mcDir.exists()) - staging_mc_dir = dotMCDir.filePath(); - else - staging_mc_dir = mcDir.filePath(); - - FS::copy savesCopy(FS::PathCombine(m_origInstance->gameRoot(), "saves"), FS::PathCombine(staging_mc_dir, "saves")); - savesCopy.followSymlinks(true); - - return savesCopy(); - }; - - m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this, copySaves] { + m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { if (m_useClone) { FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath); folderClone.matcher(m_matcher.get()); + folderClone(true); + setProgress(0, folderClone.totalCloned()); + connect(&folderClone, &FS::clone::fileCloned, + [this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); }); return folderClone(); - } else if (m_useLinks || m_useHardLinks) { + } + if (m_useLinks || m_useHardLinks) { + std::unique_ptr savesCopy; + if (m_copySaves) { + QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft")); + QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft")); + + QString staging_mc_dir; + if (dotMCDir.exists() && !mcDir.exists()) + staging_mc_dir = dotMCDir.filePath(); + else + staging_mc_dir = mcDir.filePath(); + + savesCopy = std::make_unique(FS::PathCombine(m_origInstance->gameRoot(), "saves"), + FS::PathCombine(staging_mc_dir, "saves")); + savesCopy->followSymlinks(true); + (*savesCopy)(true); + setProgress(0, savesCopy->totalCopied()); + connect(savesCopy.get(), &FS::copy::fileCopied, [this](QString src) { setProgress(m_progress + 1, m_progressTotal); }); + } FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath); int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the instance folder folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher.get()); + folderLink(true); + setProgress(0, m_progressTotal + folderLink.totalToLink()); + connect(&folderLink, &FS::create_link::fileLinked, + [this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); }); bool there_were_errors = false; if (!folderLink()) { #if defined Q_OS_WIN32 if (!m_useHardLinks) { + setProgress(0, m_progressTotal); qDebug() << "EXPECTED: Link failure, Windows requires permissions for symlinks"; qDebug() << "attempting to run with privelage"; @@ -94,13 +108,11 @@ void InstanceCopyTask::executeTask() } } - if (m_copySaves) { - there_were_errors |= !copySaves(); + if (savesCopy) { + there_were_errors |= !(*savesCopy)(); } return got_priv_results && !there_were_errors; - } else { - qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str(); } #else qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str(); @@ -108,17 +120,19 @@ void InstanceCopyTask::executeTask() return false; } - if (m_copySaves) { - there_were_errors |= !copySaves(); + if (savesCopy) { + there_were_errors |= !(*savesCopy)(); } return !there_were_errors; - } else { - FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); - folderCopy.followSymlinks(false).matcher(m_matcher.get()); - - return folderCopy(); } + FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); + folderCopy.followSymlinks(false).matcher(m_matcher.get()); + + folderCopy(true); + setProgress(0, folderCopy.totalCopied()); + connect(&folderCopy, &FS::copy::fileCopied, [this](QString src) { setProgress(m_progress + 1, m_progressTotal); }); + return folderCopy(); }); connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &InstanceCopyTask::copyFinished); connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &InstanceCopyTask::copyAborted); @@ -170,3 +184,14 @@ void InstanceCopyTask::copyAborted() emitFailed(tr("Instance folder copy has been aborted.")); return; } + +bool InstanceCopyTask::abort() +{ + if (m_copyFutureWatcher.isRunning()) { + m_copyFutureWatcher.cancel(); + // NOTE: Here we don't do `emitAborted()` because it will be done when `m_copyFutureWatcher` actually cancels, which may not occur + // immediately. + return true; + } + return false; +} \ No newline at end of file diff --git a/launcher/InstanceCopyTask.h b/launcher/InstanceCopyTask.h index 357c6df0b..0f7f1020d 100644 --- a/launcher/InstanceCopyTask.h +++ b/launcher/InstanceCopyTask.h @@ -19,6 +19,7 @@ class InstanceCopyTask : public InstanceTask { protected: //! Entry point for tasks. virtual void executeTask() override; + bool abort() override; void copyFinished(); void copyAborted(); diff --git a/launcher/InstanceCreationTask.cpp b/launcher/InstanceCreationTask.cpp index 1499199ae..9c17dfc9f 100644 --- a/launcher/InstanceCreationTask.cpp +++ b/launcher/InstanceCreationTask.cpp @@ -2,6 +2,7 @@ #include #include +#include "FileSystem.h" void InstanceCreationTask::executeTask() { @@ -45,7 +46,7 @@ void InstanceCreationTask::executeTask() if (!QFile::exists(path)) continue; qDebug() << "Removing" << path; - if (!QFile::remove(path)) { + if (!FS::deletePath(path)) { qCritical() << "Couldn't remove the old conflicting files."; emitFailed(tr("Failed to remove old conflicting files.")); return; diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index d4676f358..57cc77527 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -56,6 +56,7 @@ #include #include +#include #include @@ -68,15 +69,8 @@ bool InstanceImportTask::abort() if (!canAbort()) return false; - if (m_filesNetJob) - m_filesNetJob->abort(); - if (m_extractFuture.isRunning()) { - // NOTE: The tasks created by QtConcurrent::run() can't actually get cancelled, - // but we can use this call to check the state when the extraction finishes. - m_extractFuture.cancel(); - m_extractFuture.waitForFinished(); - } - + if (task) + task->abort(); return Task::abort(); } @@ -89,7 +83,6 @@ void InstanceImportTask::executeTask() processZipPack(); } else { setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); - m_downloadRequired = true; downloadFromUrl(); } @@ -97,115 +90,132 @@ void InstanceImportTask::executeTask() void InstanceImportTask::downloadFromUrl() { - const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path(); + const QString path(m_sourceUrl.host() + '/' + m_sourceUrl.path()); + auto entry = APPLICATION->metacache()->resolveEntry("general", path); entry->setStale(true); - m_filesNetJob.reset(new NetJob(tr("Modpack download"), APPLICATION->network())); - m_filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry)); m_archivePath = entry->getFullPath(); - connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded); - connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged); - connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress); - connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed); - connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted); - m_filesNetJob->start(); + auto filesNetJob = makeShared(tr("Modpack download"), APPLICATION->network()); + filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry)); + + connect(filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::processZipPack); + connect(filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::setProgress); + connect(filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress); + connect(filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::emitFailed); + connect(filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::emitAborted); + task.reset(filesNetJob); + filesNetJob->start(); } -void InstanceImportTask::downloadSucceeded() +QString InstanceImportTask::getRootFromZip(QuaZip* zip, const QString& root) { - processZipPack(); - m_filesNetJob.reset(); -} + if (!isRunning()) { + return {}; + } + QuaZipDir rootDir(zip, root); + for (auto&& fileName : rootDir.entryList(QDir::Files)) { + setDetails(fileName); + if (fileName == "instance.cfg") { + qDebug() << "MultiMC:" << true; + m_modpackType = ModpackType::MultiMC; + return root; + } + if (fileName == "manifest.json") { + qDebug() << "Flame:" << true; + m_modpackType = ModpackType::Flame; + return root; + } -void InstanceImportTask::downloadFailed(QString reason) -{ - emitFailed(reason); - m_filesNetJob.reset(); -} + QCoreApplication::processEvents(); + } -void InstanceImportTask::downloadProgressChanged(qint64 current, qint64 total) -{ - setProgress(current, total); -} + // Recurse the search to non-ignored subfolders + for (auto&& fileName : rootDir.entryList(QDir::Dirs)) { + if ("overrides/" == fileName) + continue; -void InstanceImportTask::downloadAborted() -{ - emitAborted(); - m_filesNetJob.reset(); + QString result = getRootFromZip(zip, root + fileName); + if (!result.isEmpty()) + return result; + } + + return {}; } void InstanceImportTask::processZipPack() { - setStatus(tr("Extracting modpack")); + setStatus(tr("Attempting to determine instance type")); QDir extractDir(m_stagingPath); qDebug() << "Attempting to create instance from" << m_archivePath; // open the zip and find relevant files in it - m_packZip.reset(new QuaZip(m_archivePath)); - if (!m_packZip->open(QuaZip::mdUnzip)) { + auto packZip = std::make_shared(m_archivePath); + if (!packZip->open(QuaZip::mdUnzip)) { emitFailed(tr("Unable to open supplied modpack zip file.")); return; } - QuaZipDir packZipDir(m_packZip.get()); + QuaZipDir packZipDir(packZip.get()); + qDebug() << "Attempting to determine instance type"; - // https://docs.modrinth.com/docs/modpacks/format_definition/#storage - bool modrinthFound = packZipDir.exists("/modrinth.index.json"); - bool technicFound = packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json"); QString root; // NOTE: Prioritize modpack platforms that aren't searched for recursively. // Especially Flame has a very common filename for its manifest, which may appear inside overrides for example - if (modrinthFound) { + // https://docs.modrinth.com/docs/modpacks/format_definition/#storage + if (packZipDir.exists("/modrinth.index.json")) { // process as Modrinth pack - qDebug() << "Modrinth:" << modrinthFound; + qDebug() << "Modrinth:" << true; m_modpackType = ModpackType::Modrinth; - } else if (technicFound) { + } else if (packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json")) { // process as Technic pack - qDebug() << "Technic:" << technicFound; + qDebug() << "Technic:" << true; extractDir.mkpath("minecraft"); extractDir.cd("minecraft"); m_modpackType = ModpackType::Technic; } else { - QStringList paths_to_ignore{ "overrides/" }; - - if (QString mmcRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg", paths_to_ignore); !mmcRoot.isNull()) { - // process as MultiMC instance/pack - qDebug() << "MultiMC:" << mmcRoot; - root = mmcRoot; - m_modpackType = ModpackType::MultiMC; - } else if (QString flameRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json", paths_to_ignore); - !flameRoot.isNull()) { - // process as Flame pack - qDebug() << "Flame:" << flameRoot; - root = flameRoot; - m_modpackType = ModpackType::Flame; - } + root = getRootFromZip(packZip.get()); + setDetails(""); } if (m_modpackType == ModpackType::Unknown) { emitFailed(tr("Archive does not contain a recognized modpack type.")); return; } + setStatus(tr("Extracting modpack")); // make sure we extract just the pack - m_extractFuture = - QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), root, extractDir.absolutePath()); - connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &InstanceImportTask::extractFinished); - m_extractFutureWatcher.setFuture(m_extractFuture); + auto zipTask = makeShared(packZip, extractDir, root); + + auto progressStep = std::make_shared(); + connect(zipTask.get(), &Task::finished, this, [this, progressStep] { + progressStep->state = TaskStepState::Succeeded; + stepProgress(*progressStep); + }); + + connect(zipTask.get(), &Task::succeeded, this, &InstanceImportTask::extractFinished); + connect(zipTask.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); + connect(zipTask.get(), &Task::failed, this, [this, progressStep](QString reason) { + progressStep->state = TaskStepState::Failed; + stepProgress(*progressStep); + emitFailed(reason); + }); + connect(zipTask.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress); + + connect(zipTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { + progressStep->update(current, total); + stepProgress(*progressStep); + }); + connect(zipTask.get(), &Task::status, this, [this, progressStep](QString status) { + progressStep->status = status; + stepProgress(*progressStep); + }); + task.reset(zipTask); + zipTask->start(); } void InstanceImportTask::extractFinished() { - m_packZip.reset(); - - if (m_extractFuture.isCanceled()) - return; - if (!m_extractFuture.result().has_value()) { - emitFailed(tr("Failed to extract modpack")); - return; - } - QDir extractDir(m_stagingPath); qDebug() << "Fixing permissions for extracted pack files..."; @@ -324,13 +334,15 @@ void InstanceImportTask::processMultiMC() m_instIcon = instance.iconKey(); auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon); + if (importIconPath.isNull() || !QFile::exists(importIconPath)) + importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), "icon.png"); if (!importIconPath.isNull() && QFile::exists(importIconPath)) { // import icon auto iconList = APPLICATION->icons(); if (iconList->iconFileExists(m_instIcon)) { iconList->deleteIcon(m_instIcon); } - iconList->installIcons({ importIconPath }); + iconList->installIcon(importIconPath, m_instIcon); } } emitSucceeded(); diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index 28efd7ec5..cf86af4ea 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -39,11 +39,8 @@ #include #include #include "InstanceTask.h" -#include "QObjectPtr.h" -#include "modplatform/flame/PackManifest.h" -#include "net/NetJob.h" -#include "settings/SettingsObject.h" +#include #include class QuaZip; @@ -54,35 +51,26 @@ class InstanceImportTask : public InstanceTask { explicit InstanceImportTask(const QUrl& sourceUrl, QWidget* parent = nullptr, QMap&& extra_info = {}); bool abort() override; - const QVector& getBlockedFiles() const { return m_blockedMods; } protected: //! Entry point for tasks. virtual void executeTask() override; private: - void processZipPack(); void processMultiMC(); void processTechnic(); void processFlame(); void processModrinth(); + QString getRootFromZip(QuaZip* zip, const QString& root = ""); private slots: - void downloadSucceeded(); - void downloadFailed(QString reason); - void downloadProgressChanged(qint64 current, qint64 total); - void downloadAborted(); + void processZipPack(); void extractFinished(); private: /* data */ - NetJob::Ptr m_filesNetJob; QUrl m_sourceUrl; QString m_archivePath; - bool m_downloadRequired = false; - std::unique_ptr m_packZip; - QFuture> m_extractFuture; - QFutureWatcher> m_extractFutureWatcher; - QVector m_blockedMods; + Task::Ptr task; enum class ModpackType { Unknown, MultiMC, diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 5e4abf020..0d53c7f25 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -972,7 +972,6 @@ bool InstanceList::commitStagedInstance(const QString& path, if (groupName.isEmpty() && !groupName.isNull()) groupName = QString(); - QDir dir; QString instID; InstancePtr inst; @@ -996,7 +995,7 @@ bool InstanceList::commitStagedInstance(const QString& path, return false; } } else { - if (!dir.rename(path, destination)) { + if (!FS::move(path, destination)) { qWarning() << "Failed to move" << path << "to" << destination; return false; } diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index 66d2b6750..52399e912 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -22,7 +22,7 @@ class InstancePageProvider : protected QObject, public BasePageProvider { public: explicit InstancePageProvider(InstancePtr parent) { inst = parent; } - virtual ~InstancePageProvider(){}; + virtual ~InstancePageProvider() {}; virtual QList getPages() override { QList values; diff --git a/launcher/JavaCommon.h b/launcher/JavaCommon.h index c96f7a985..cf3b75c9c 100644 --- a/launcher/JavaCommon.h +++ b/launcher/JavaCommon.h @@ -24,7 +24,7 @@ class TestCheck : public QObject { TestCheck(QWidget* parent, QString path, QString args, int minMem, int maxMem, int permGen) : m_parent(parent), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen) {} - virtual ~TestCheck(){}; + virtual ~TestCheck() {}; void run(); diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index cf8d0a794..3866a7672 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -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(); + 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; } @@ -142,7 +194,8 @@ void LaunchController::login() bool tryagain = true; unsigned int tries = 0; - if (m_accountToUse->accountType() != AccountType::Offline && m_accountToUse->accountState() == AccountState::Offline) { + if ((m_accountToUse->accountType() != AccountType::Offline && m_accountToUse->accountState() == AccountState::Offline) || + m_accountToUse->shouldRefresh()) { // Force account refresh on the account used to launch the instance updating the AccountState // only on first try and if it is not meant to be offline auto accounts = APPLICATION->accounts(); @@ -180,24 +233,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 +258,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.")); @@ -315,26 +346,9 @@ void LaunchController::launchInstance() online_mode = "online"; // Prepend Server Status - QStringList servers = { "authserver.mojang.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" }; - QString resolved_servers = ""; - QHostInfo host_info; + QStringList servers = { "login.microsoftonline.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" }; - 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(m_launcher.get(), resolved_servers, MessageLevel::Launcher)); + m_launcher->prependStep(makeShared(m_launcher.get(), servers)); } else { online_mode = m_demo ? "demo" : "offline"; } diff --git a/launcher/LaunchController.h b/launcher/LaunchController.h index f1c88afb7..bc688f2ba 100644 --- a/launcher/LaunchController.h +++ b/launcher/LaunchController.h @@ -48,7 +48,7 @@ class LaunchController : public Task { void executeTask() override; LaunchController(QObject* parent = nullptr); - virtual ~LaunchController(){}; + virtual ~LaunchController() {}; void setInstance(InstancePtr instance) { m_instance = instance; } @@ -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(); diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 9a5ae7a9d..00658b42b 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -42,6 +42,7 @@ #include #include +#include #include #if defined(LAUNCHER_APPLICATION) @@ -122,7 +123,7 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, zip.setUtf8Enabled(true); QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); if (!zip.open(QuaZip::mdCreate)) { - QFile::remove(fileCompressed); + FS::deletePath(fileCompressed); return false; } @@ -130,7 +131,7 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, zip.close(); if (zip.getZipError() != 0) { - QFile::remove(fileCompressed); + FS::deletePath(fileCompressed); return false; } @@ -144,7 +145,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QListtype() == ResourceType::ZIPFILE) { if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) { zipOut.close(); - QFile::remove(targetJarPath); + FS::deletePath(targetJarPath); qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; return false; } @@ -171,7 +172,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QListfileinfo(); if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) { zipOut.close(); - QFile::remove(targetJarPath); + FS::deletePath(targetJarPath); qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; return false; } @@ -194,7 +195,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QListfileinfo().fileName() << "to the jar."; return false; } @@ -202,7 +203,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QListfileinfo().fileName() << "to the jar."; return false; } @@ -210,7 +211,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList 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; @@ -332,9 +331,20 @@ std::optional extractSubDir(QuaZip* zip, const QString& subdir, con } extracted.append(target_file_path); - QFile::setPermissions(target_file_path, - QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser); + auto fileInfo = QFileInfo(target_file_path); + if (fileInfo.isFile()) { + auto permissions = fileInfo.permissions(); + auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser | + QFileDevice::Permission::ReadGroup | QFileDevice::Permission::ReadOther; + auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; + auto newPermisions = (permissions & maxPermisions) | minPermisions; + if (newPermisions != permissions) { + if (!QFile::setPermissions(target_file_path, newPermisions)) { + qWarning() << (QObject::tr("Could not fix permissions for %1").arg(target_file_path)); + } + } + } qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; } while (zip->goToNextFile()); @@ -492,10 +502,10 @@ auto ExportToZipTask::exportZip() -> ZipResult void ExportToZipTask::finish() { if (m_build_zip_future.isCanceled()) { - QFile::remove(m_output_path); + FS::deletePath(m_output_path); emitAborted(); } else if (auto result = m_build_zip_future.result(); result.has_value()) { - QFile::remove(m_output_path); + FS::deletePath(m_output_path); emitFailed(result.value()); } else { emitSucceeded(); @@ -512,6 +522,123 @@ bool ExportToZipTask::abort() } return false; } -#endif +void ExtractZipTask::executeTask() +{ + m_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return extractZip(); }); + connect(&m_zip_watcher, &QFutureWatcher::finished, this, &ExtractZipTask::finish); + m_zip_watcher.setFuture(m_zip_future); +} + +auto ExtractZipTask::extractZip() -> ZipResult +{ + auto target = m_output_dir.absolutePath(); + auto target_top_dir = QUrl::fromLocalFile(target); + + QStringList extracted; + + qDebug() << "Extracting subdir" << m_subdirectory << "from" << m_input->getZipName() << "to" << target; + auto numEntries = m_input->getEntriesCount(); + if (numEntries < 0) { + return ZipResult(tr("Failed to enumerate files in archive")); + } + if (numEntries == 0) { + logWarning(tr("Extracting empty archives seems odd...")); + return ZipResult(); + } + if (!m_input->goToFirstFile()) { + return ZipResult(tr("Failed to seek to first file in zip")); + } + + setStatus("Extracting files..."); + setProgress(0, numEntries); + do { + if (m_zip_future.isCanceled()) + return ZipResult(); + setProgress(m_progress + 1, m_progressTotal); + QString file_name = m_input->getCurrentFileName(); + if (!file_name.startsWith(m_subdirectory)) + continue; + + auto relative_file_name = QDir::fromNativeSeparators(file_name.remove(0, m_subdirectory.size())); + auto original_name = relative_file_name; + setStatus("Unziping: " + relative_file_name); + + // Fix subdirs/files ending with a / getting transformed into absolute paths + if (relative_file_name.startsWith('/')) + relative_file_name = relative_file_name.mid(1); + + // Fix weird "folders with a single file get squashed" thing + QString sub_path; + if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) { + sub_path = relative_file_name.section('/', 0, -2) + '/'; + FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); + + relative_file_name = relative_file_name.split('/').last(); + } + + QString target_file_path; + if (relative_file_name.isEmpty()) { + target_file_path = target + '/'; + } else { + target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); + if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) + target_file_path += '/'; + } + + if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { + return ZipResult(tr("Extracting %1 was cancelled, because it was effectively outside of the target path %2") + .arg(relative_file_name, target)); + } + + if (!JlCompress::extractFile(m_input.get(), "", target_file_path)) { + JlCompress::removeFile(extracted); + return ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path)); + } + + extracted.append(target_file_path); + auto fileInfo = QFileInfo(target_file_path); + if (fileInfo.isFile()) { + auto permissions = fileInfo.permissions(); + auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser | + QFileDevice::Permission::ReadGroup | QFileDevice::Permission::ReadOther; + auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; + + auto newPermisions = (permissions & maxPermisions) | minPermisions; + if (newPermisions != permissions) { + if (!QFile::setPermissions(target_file_path, newPermisions)) { + logWarning(tr("Could not fix permissions for %1").arg(target_file_path)); + } + } + } + + qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; + } while (m_input->goToNextFile()); + + return ZipResult(); +} + +void ExtractZipTask::finish() +{ + if (m_zip_future.isCanceled()) { + emitAborted(); + } else if (auto result = m_zip_future.result(); result.has_value()) { + emitFailed(result.value()); + } else { + emitSucceeded(); + } +} + +bool ExtractZipTask::abort() +{ + if (m_zip_future.isRunning()) { + m_zip_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; + } + return false; +} + +#endif } // namespace MMCZip diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index b28eb195c..35baa6ee3 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -176,7 +176,7 @@ class ExportToZipTask : public Task { QString destinationPrefix = "", bool followSymlinks = false, bool utf8Enabled = false) - : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks, utf8Enabled){}; + : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks, utf8Enabled) {}; virtual ~ExportToZipTask() = default; @@ -205,5 +205,30 @@ class ExportToZipTask : public Task { QFuture m_build_zip_future; QFutureWatcher m_build_zip_watcher; }; + +class ExtractZipTask : public Task { + public: + ExtractZipTask(std::shared_ptr input, QDir outputDir, QString subdirectory = "") + : m_input(input), m_output_dir(outputDir), m_subdirectory(subdirectory) + {} + virtual ~ExtractZipTask() = default; + + using ZipResult = std::optional; + + protected: + virtual void executeTask() override; + bool abort() override; + + ZipResult extractZip(); + void finish(); + + private: + std::shared_ptr m_input; + QDir m_output_dir; + QString m_subdirectory; + + QFuture m_zip_future; + QFutureWatcher m_zip_watcher; +}; #endif } // namespace MMCZip diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h index c79600e7d..b6a9478a8 100644 --- a/launcher/NullInstance.h +++ b/launcher/NullInstance.h @@ -46,7 +46,7 @@ class NullInstance : public BaseInstance { { setVersionBroken(true); } - virtual ~NullInstance(){}; + virtual ~NullInstance() {}; void saveNow() override {} void loadSpecificSettings() override { setSpecificSettingsLoaded(true); } QString getStatusbarDescription() override { return tr("Unknown instance type"); }; diff --git a/launcher/SkinUtils.cpp b/launcher/SkinUtils.cpp deleted file mode 100644 index 989114ad5..000000000 --- a/launcher/SkinUtils.cpp +++ /dev/null @@ -1,52 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "SkinUtils.h" -#include "Application.h" -#include "net/HttpMetaCache.h" - -#include -#include -#include -#include -#include - -namespace SkinUtils { -/* - * Given a username, return a pixmap of the cached skin (if it exists), QPixmap() otherwise - */ -QPixmap getFaceFromCache(QString username, int height, int width) -{ - QFile fskin(APPLICATION->metacache()->resolveEntry("skins", username + ".png")->getFullPath()); - - if (fskin.exists()) { - QPixmap skinTexture(fskin.fileName()); - if (!skinTexture.isNull()) { - QPixmap skin = QPixmap(8, 8); -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - skin.fill(QColorConstants::Transparent); -#else - skin.fill(QColor(0, 0, 0, 0)); -#endif - QPainter painter(&skin); - painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8)); - painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8)); - return skin.scaled(height, width, Qt::KeepAspectRatio); - } - } - - return QPixmap(); -} -} // namespace SkinUtils diff --git a/launcher/SkinUtils.h b/launcher/SkinUtils.h deleted file mode 100644 index 11bc8bc6f..000000000 --- a/launcher/SkinUtils.h +++ /dev/null @@ -1,22 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include - -namespace SkinUtils { -QPixmap getFaceFromCache(QString id, int height = 64, int width = 64); -} diff --git a/launcher/StringUtils.cpp b/launcher/StringUtils.cpp index 72ccdfbff..edda9f247 100644 --- a/launcher/StringUtils.cpp +++ b/launcher/StringUtils.cpp @@ -212,3 +212,25 @@ QPair StringUtils::splitFirst(const QString& s, const QRegular right = s.mid(end); return qMakePair(left, right); } + +static const QRegularExpression ulMatcher("<\\s*/\\s*ul\\s*>"); + +QString StringUtils::htmlListPatch(QString htmlStr) +{ + int pos = htmlStr.indexOf(ulMatcher); + int imgPos; + while (pos != -1) { + pos = htmlStr.indexOf(">", pos) + 1; // Get the size of the tag. Add one for zeroeth index + imgPos = htmlStr.indexOf(""); + + pos = htmlStr.indexOf(ulMatcher, pos); + } + return htmlStr; +} \ No newline at end of file diff --git a/launcher/StringUtils.h b/launcher/StringUtils.h index 9d2bdd85e..624ee41a3 100644 --- a/launcher/StringUtils.h +++ b/launcher/StringUtils.h @@ -85,4 +85,6 @@ QPair splitFirst(const QString& s, const QString& sep, Qt::Cas QPair splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs = Qt::CaseSensitive); QPair splitFirst(const QString& s, const QRegularExpression& re); +QString htmlListPatch(QString htmlStr); + } // namespace StringUtils diff --git a/launcher/VersionProxyModel.h b/launcher/VersionProxyModel.h index 0863a7c80..efea1a0bb 100644 --- a/launcher/VersionProxyModel.h +++ b/launcher/VersionProxyModel.h @@ -14,7 +14,7 @@ class VersionProxyModel : public QAbstractProxyModel { public: VersionProxyModel(QObject* parent = 0); - virtual ~VersionProxyModel(){}; + virtual ~VersionProxyModel() {}; virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override; virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index 5576b9745..e4157ea2d 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -322,7 +322,7 @@ const MMCIcon* IconList::icon(const QString& key) const bool IconList::deleteIcon(const QString& key) { - return iconFileExists(key) && QFile::remove(icon(key)->getFilePath()); + return iconFileExists(key) && FS::deletePath(icon(key)->getFilePath()); } bool IconList::trashIcon(const QString& key) diff --git a/launcher/icons/IconList.h b/launcher/icons/IconList.h index c51826057..553946c42 100644 --- a/launcher/icons/IconList.h +++ b/launcher/icons/IconList.h @@ -52,7 +52,7 @@ class IconList : public QAbstractListModel { Q_OBJECT public: explicit IconList(const QStringList& builtinPaths, QString path, QObject* parent = 0); - virtual ~IconList(){}; + virtual ~IconList() {}; QIcon getIcon(const QString& key) const; int getIconIndex(const QString& key) const; diff --git a/launcher/icons/IconUtils.cpp b/launcher/icons/IconUtils.cpp index 99c38f47a..6825dd6da 100644 --- a/launcher/icons/IconUtils.cpp +++ b/launcher/icons/IconUtils.cpp @@ -52,8 +52,7 @@ QString findBestIconIn(const QString& folder, const QString& iconKey) while (it.hasNext()) { it.next(); auto fileInfo = it.fileInfo(); - - if (fileInfo.completeBaseName() == iconKey && isIconSuffix(fileInfo.suffix())) + if ((fileInfo.completeBaseName() == iconKey || fileInfo.fileName() == iconKey) && isIconSuffix(fileInfo.suffix())) return fileInfo.absoluteFilePath(); } return {}; diff --git a/launcher/java/JavaCheckerJob.h b/launcher/java/JavaCheckerJob.h index ddf827968..16b572632 100644 --- a/launcher/java/JavaCheckerJob.h +++ b/launcher/java/JavaCheckerJob.h @@ -26,8 +26,8 @@ using JavaCheckerJobPtr = shared_qobject_ptr; class JavaCheckerJob : public Task { Q_OBJECT public: - explicit JavaCheckerJob(QString job_name) : Task(), m_job_name(job_name){}; - virtual ~JavaCheckerJob(){}; + explicit JavaCheckerJob(QString job_name) : Task(), m_job_name(job_name) {}; + virtual ~JavaCheckerJob() {}; bool addJavaCheckerAction(JavaCheckerPtr base) { diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index e767eff89..809d81087 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -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); @@ -207,7 +205,7 @@ QList JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString QString newKeyName = keyName + "\\" + newSubkeyName + subkeySuffix; HKEY newKey; - if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, newKeyName.toStdWString().c_str(), 0, KEY_READ | KEY_WOW64_64KEY, &newKey) == + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, newKeyName.toStdWString().c_str(), 0, KEY_READ | keyType, &newKey) == ERROR_SUCCESS) { // Read the JavaHome value to find where Java is installed. DWORD valueSz = 0; @@ -283,6 +281,12 @@ QList JavaUtils::FindJavaPaths() QList ADOPTIUMJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Adoptium\\JDK", "Path", "\\hotspot\\MSI"); + // IBM Semeru + QList SEMERUJRE32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Semeru\\JRE", "Path", "\\openj9\\MSI"); + QList SEMERUJRE64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Semeru\\JRE", "Path", "\\openj9\\MSI"); + QList SEMERUJDK32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Semeru\\JDK", "Path", "\\openj9\\MSI"); + QList SEMERUJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Semeru\\JDK", "Path", "\\openj9\\MSI"); + // Microsoft QList MICROSOFTJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Microsoft\\JDK", "Path", "\\hotspot\\MSI"); @@ -300,6 +304,7 @@ QList JavaUtils::FindJavaPaths() java_candidates.append(NEWJRE64s); java_candidates.append(ADOPTOPENJRE64s); java_candidates.append(ADOPTIUMJRE64s); + java_candidates.append(SEMERUJRE64s); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/javaw.exe")); @@ -308,6 +313,7 @@ QList JavaUtils::FindJavaPaths() java_candidates.append(ADOPTOPENJDK64s); java_candidates.append(FOUNDATIONJDK64s); java_candidates.append(ADOPTIUMJDK64s); + java_candidates.append(SEMERUJDK64s); java_candidates.append(MICROSOFTJDK64s); java_candidates.append(ZULU64s); java_candidates.append(LIBERICA64s); @@ -316,6 +322,7 @@ QList JavaUtils::FindJavaPaths() java_candidates.append(NEWJRE32s); java_candidates.append(ADOPTOPENJRE32s); java_candidates.append(ADOPTIUMJRE32s); + java_candidates.append(SEMERUJRE32s); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/javaw.exe")); @@ -324,6 +331,7 @@ QList JavaUtils::FindJavaPaths() java_candidates.append(ADOPTOPENJDK32s); java_candidates.append(FOUNDATIONJDK32s); java_candidates.append(ADOPTIUMJDK32s); + java_candidates.append(SEMERUJDK32s); java_candidates.append(ZULU32s); java_candidates.append(LIBERICA32s); @@ -366,7 +374,19 @@ QList 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); @@ -405,11 +425,16 @@ QList 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 scanJavaDirs("/opt/jdk"); scanJavaDirs("/opt/jdks"); + scanJavaDirs("/opt/ibm"); // IBM Semeru Certified Edition // flatpak scanJavaDirs("/app/jdk"); diff --git a/launcher/launch/LaunchStep.h b/launcher/launch/LaunchStep.h index b1bec2b4a..6a28afb1f 100644 --- a/launcher/launch/LaunchStep.h +++ b/launcher/launch/LaunchStep.h @@ -25,7 +25,7 @@ class LaunchStep : public Task { Q_OBJECT public: /* methods */ explicit LaunchStep(LaunchTask* parent) : Task(nullptr), m_parent(parent) { bind(parent); }; - virtual ~LaunchStep(){}; + virtual ~LaunchStep() {}; private: /* methods */ void bind(LaunchTask* parent); @@ -37,9 +37,9 @@ class LaunchStep : public Task { void progressReportingRequest(); public slots: - virtual void proceed(){}; + virtual void proceed() {}; // called in the opposite order than the Task launch(), used to clean up or otherwise undo things after the launch ends - virtual void finalize(){}; + virtual void finalize() {}; protected: /* data */ LaunchTask* m_parent; diff --git a/launcher/launch/LaunchTask.h b/launcher/launch/LaunchTask.h index e79c43557..ae24b9a26 100644 --- a/launcher/launch/LaunchTask.h +++ b/launcher/launch/LaunchTask.h @@ -55,7 +55,7 @@ class LaunchTask : public Task { public: /* methods */ static shared_qobject_ptr create(InstancePtr inst); - virtual ~LaunchTask(){}; + virtual ~LaunchTask() {}; void appendStep(shared_qobject_ptr step); void prependStep(shared_qobject_ptr step); diff --git a/launcher/launch/steps/CheckJava.h b/launcher/launch/steps/CheckJava.h index 4436e2a55..ac9c36244 100644 --- a/launcher/launch/steps/CheckJava.h +++ b/launcher/launch/steps/CheckJava.h @@ -22,8 +22,8 @@ class CheckJava : public LaunchStep { Q_OBJECT public: - explicit CheckJava(LaunchTask* parent) : LaunchStep(parent){}; - virtual ~CheckJava(){}; + explicit CheckJava(LaunchTask* parent) : LaunchStep(parent) {}; + virtual ~CheckJava() {}; virtual void executeTask(); virtual bool canAbort() const { return false; } diff --git a/launcher/launch/steps/LookupServerAddress.h b/launcher/launch/steps/LookupServerAddress.h index abd92a5e8..acbd74309 100644 --- a/launcher/launch/steps/LookupServerAddress.h +++ b/launcher/launch/steps/LookupServerAddress.h @@ -25,7 +25,7 @@ class LookupServerAddress : public LaunchStep { Q_OBJECT public: explicit LookupServerAddress(LaunchTask* parent); - virtual ~LookupServerAddress(){}; + virtual ~LookupServerAddress() {}; virtual void executeTask(); virtual bool abort(); diff --git a/launcher/launch/steps/PostLaunchCommand.h b/launcher/launch/steps/PostLaunchCommand.h index 578433b86..fd1443b29 100644 --- a/launcher/launch/steps/PostLaunchCommand.h +++ b/launcher/launch/steps/PostLaunchCommand.h @@ -22,7 +22,7 @@ class PostLaunchCommand : public LaunchStep { Q_OBJECT public: explicit PostLaunchCommand(LaunchTask* parent); - virtual ~PostLaunchCommand(){}; + virtual ~PostLaunchCommand() {}; virtual void executeTask(); virtual bool abort(); diff --git a/launcher/launch/steps/PreLaunchCommand.h b/launcher/launch/steps/PreLaunchCommand.h index 10568ea34..b6dc6cd8b 100644 --- a/launcher/launch/steps/PreLaunchCommand.h +++ b/launcher/launch/steps/PreLaunchCommand.h @@ -22,7 +22,7 @@ class PreLaunchCommand : public LaunchStep { Q_OBJECT public: explicit PreLaunchCommand(LaunchTask* parent); - virtual ~PreLaunchCommand(){}; + virtual ~PreLaunchCommand() {}; virtual void executeTask(); virtual bool abort(); diff --git a/launcher/launch/steps/PrintServers.cpp b/launcher/launch/steps/PrintServers.cpp new file mode 100644 index 000000000..ba96d37b9 --- /dev/null +++ b/launcher/launch/steps/PrintServers.cpp @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Leia uwu + * + * 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 . + */ + +#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; +} diff --git a/launcher/launch/steps/PrintServers.h b/launcher/launch/steps/PrintServers.h new file mode 100644 index 000000000..7d2f1b194 --- /dev/null +++ b/launcher/launch/steps/PrintServers.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Leia uwu + * + * 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 . + */ +#pragma once + +#include +#include +#include +#include + +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 m_server_to_address; + QStringList m_servers; +}; diff --git a/launcher/launch/steps/QuitAfterGameStop.h b/launcher/launch/steps/QuitAfterGameStop.h index 9326b2a8c..d4324cce6 100644 --- a/launcher/launch/steps/QuitAfterGameStop.h +++ b/launcher/launch/steps/QuitAfterGameStop.h @@ -23,8 +23,8 @@ class QuitAfterGameStop : public LaunchStep { Q_OBJECT public: - explicit QuitAfterGameStop(LaunchTask* parent) : LaunchStep(parent){}; - virtual ~QuitAfterGameStop(){}; + explicit QuitAfterGameStop(LaunchTask* parent) : LaunchStep(parent) {}; + virtual ~QuitAfterGameStop() {}; virtual void executeTask(); virtual bool canAbort() const { return false; } diff --git a/launcher/launch/steps/TextPrint.h b/launcher/launch/steps/TextPrint.h index bd6c28567..a96c2f887 100644 --- a/launcher/launch/steps/TextPrint.h +++ b/launcher/launch/steps/TextPrint.h @@ -28,7 +28,7 @@ class TextPrint : public LaunchStep { public: explicit TextPrint(LaunchTask* parent, const QStringList& lines, MessageLevel::Enum level); explicit TextPrint(LaunchTask* parent, const QString& line, MessageLevel::Enum level); - virtual ~TextPrint(){}; + virtual ~TextPrint() {}; virtual void executeTask(); virtual bool canAbort() const; diff --git a/launcher/launch/steps/Update.h b/launcher/launch/steps/Update.h index 9262cdbe4..878a43e7e 100644 --- a/launcher/launch/steps/Update.h +++ b/launcher/launch/steps/Update.h @@ -25,8 +25,8 @@ class Update : public LaunchStep { Q_OBJECT public: - explicit Update(LaunchTask* parent, Net::Mode mode) : LaunchStep(parent), m_mode(mode){}; - virtual ~Update(){}; + explicit Update(LaunchTask* parent, Net::Mode mode) : LaunchStep(parent), m_mode(mode) {}; + virtual ~Update() {}; void executeTask() override; bool canAbort() const override; diff --git a/launcher/meta/BaseEntity.cpp b/launcher/meta/BaseEntity.cpp index 5f9804e48..e74406752 100644 --- a/launcher/meta/BaseEntity.cpp +++ b/launcher/meta/BaseEntity.cpp @@ -15,6 +15,7 @@ #include "BaseEntity.h" +#include "FileSystem.h" #include "Json.h" #include "net/ApiDownload.h" #include "net/HttpMetaCache.h" @@ -25,8 +26,8 @@ class ParsingValidator : public Net::Validator { public: /* con/des */ - ParsingValidator(Meta::BaseEntity* entity) : m_entity(entity){}; - virtual ~ParsingValidator(){}; + ParsingValidator(Meta::BaseEntity* entity) : m_entity(entity) {}; + virtual ~ParsingValidator() {}; public: /* methods */ bool init(QNetworkRequest&) override { return true; } @@ -83,8 +84,7 @@ bool Meta::BaseEntity::loadLocalFile() } catch (const Exception& e) { qDebug() << QString("Unable to parse file %1: %2").arg(fname, e.cause()); // just make sure it's gone and we never consider it again. - QFile::remove(fname); - return false; + return !FS::deletePath(fname); } } diff --git a/launcher/minecraft/Component.cpp b/launcher/minecraft/Component.cpp index 79ea7a06d..ad2e4023c 100644 --- a/launcher/minecraft/Component.cpp +++ b/launcher/minecraft/Component.cpp @@ -336,7 +336,7 @@ bool Component::revert() bool result = true; // just kill the file and reload if (QFile::exists(filename)) { - result = QFile::remove(filename); + result = FS::deletePath(filename); } if (result) { // file gone... diff --git a/launcher/minecraft/Library.cpp b/launcher/minecraft/Library.cpp index 2c3f2035f..4e30f72d1 100644 --- a/launcher/minecraft/Library.cpp +++ b/launcher/minecraft/Library.cpp @@ -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(); diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 1de822b7f..d119104fe 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -1000,7 +1000,7 @@ QString MinecraftInstance::getStatusbarDescription() QString description; description.append(tr("Minecraft %1").arg(mcVersion)); if (m_settings->get("ShowGameTime").toBool()) { - if (lastTimePlayed() > 0) { + if (lastTimePlayed() > 0 && lastLaunch() > 0) { QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch()); description.append( tr(", last played on %1 for %2") diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index b1f305201..7af0df389 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -56,7 +56,7 @@ class MinecraftInstance : public BaseInstance { Q_OBJECT public: MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir); - virtual ~MinecraftInstance(){}; + virtual ~MinecraftInstance() {}; virtual void saveNow() override; void loadSpecificSettings() override; diff --git a/launcher/minecraft/MinecraftLoadAndCheck.h b/launcher/minecraft/MinecraftLoadAndCheck.h index 9556c1d6a..09edaf909 100644 --- a/launcher/minecraft/MinecraftLoadAndCheck.h +++ b/launcher/minecraft/MinecraftLoadAndCheck.h @@ -31,7 +31,7 @@ class MinecraftLoadAndCheck : public Task { Q_OBJECT public: explicit MinecraftLoadAndCheck(MinecraftInstance* inst, QObject* parent = 0); - virtual ~MinecraftLoadAndCheck(){}; + virtual ~MinecraftLoadAndCheck() {}; void executeTask() override; private slots: diff --git a/launcher/minecraft/MinecraftUpdate.h b/launcher/minecraft/MinecraftUpdate.h index 9c41d7f56..591c6afcb 100644 --- a/launcher/minecraft/MinecraftUpdate.h +++ b/launcher/minecraft/MinecraftUpdate.h @@ -33,7 +33,7 @@ class MinecraftUpdate : public Task { Q_OBJECT public: explicit MinecraftUpdate(MinecraftInstance* inst, QObject* parent = 0); - virtual ~MinecraftUpdate(){}; + virtual ~MinecraftUpdate() {}; void executeTask() override; bool canAbort() const override; diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index 180f8aa30..4b17cdf07 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -839,7 +839,7 @@ bool PackProfile::installCustomJar_internal(QString filepath) QFileInfo jarInfo(finalPath); if (jarInfo.exists()) { - if (!QFile::remove(finalPath)) { + if (!FS::deletePath(finalPath)) { return false; } } diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index 1a680ac56..1eba148a5 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -206,8 +206,8 @@ int64_t calculateWorldSize(const QFileInfo& file) QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories); int64_t total = 0; while (it.hasNext()) { - total += it.fileInfo().size(); it.next(); + total += it.fileInfo().size(); } return total; } diff --git a/launcher/minecraft/auth/AuthSession.cpp b/launcher/minecraft/auth/AuthSession.cpp index 37534f983..3657befec 100644 --- a/launcher/minecraft/auth/AuthSession.cpp +++ b/launcher/minecraft/auth/AuthSession.cpp @@ -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 +}; \ No newline at end of file diff --git a/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h index cec238033..54e7d69e0 100644 --- a/launcher/minecraft/auth/AuthSession.h +++ b/launcher/minecraft/auth/AuthSession.h @@ -10,7 +10,7 @@ class QNetworkAccessManager; struct AuthSession { bool MakeOffline(QString offline_playername); - void MakeDemo(); + void MakeDemo(QString name, QString uuid); QString serializeUserProperties(); diff --git a/launcher/minecraft/auth/AuthStep.h b/launcher/minecraft/auth/AuthStep.h index 4d2cf69c1..cbe157790 100644 --- a/launcher/minecraft/auth/AuthStep.h +++ b/launcher/minecraft/auth/AuthStep.h @@ -27,7 +27,7 @@ class AuthStep : public QObject { public: using Ptr = shared_qobject_ptr; - explicit AuthStep(AccountData* data) : QObject(nullptr), m_data(data){}; + explicit AuthStep(AccountData* data) : QObject(nullptr), m_data(data) {}; virtual ~AuthStep() noexcept = default; virtual QString describe() = 0; diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 3c7129d5f..5b063604c 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -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()) { diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index b5c623a26..f6fcfada2 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -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; } diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index a2b97e278..3aa458ace 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -347,7 +347,7 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) Skin skinOut; // fill in default skin info ourselves, as this endpoint doesn't provide it bool steve = isDefaultModelSteve(output.id); - skinOut.variant = steve ? "classic" : "slim"; + skinOut.variant = steve ? "CLASSIC" : "SLIM"; skinOut.url = steve ? SKIN_URL_STEVE : SKIN_URL_ALEX; // sadly we can't figure this out, but I don't think it really matters... skinOut.id = "00000000-0000-0000-0000-000000000000"; diff --git a/launcher/minecraft/launch/ClaimAccount.h b/launcher/minecraft/launch/ClaimAccount.h index 3d47539ac..357a4d4c5 100644 --- a/launcher/minecraft/launch/ClaimAccount.h +++ b/launcher/minecraft/launch/ClaimAccount.h @@ -22,7 +22,7 @@ class ClaimAccount : public LaunchStep { Q_OBJECT public: explicit ClaimAccount(LaunchTask* parent, AuthSessionPtr session); - virtual ~ClaimAccount(){}; + virtual ~ClaimAccount() {}; void executeTask() override; void finalize() override; diff --git a/launcher/minecraft/launch/CreateGameFolders.h b/launcher/minecraft/launch/CreateGameFolders.h index 44524ded5..b44762d62 100644 --- a/launcher/minecraft/launch/CreateGameFolders.h +++ b/launcher/minecraft/launch/CreateGameFolders.h @@ -24,7 +24,7 @@ class CreateGameFolders : public LaunchStep { Q_OBJECT public: explicit CreateGameFolders(LaunchTask* parent); - virtual ~CreateGameFolders(){}; + virtual ~CreateGameFolders() {}; virtual void executeTask(); virtual bool canAbort() const { return false; } diff --git a/launcher/minecraft/launch/ExtractNatives.h b/launcher/minecraft/launch/ExtractNatives.h index 4837a9dbb..1ad9a416e 100644 --- a/launcher/minecraft/launch/ExtractNatives.h +++ b/launcher/minecraft/launch/ExtractNatives.h @@ -21,8 +21,8 @@ class ExtractNatives : public LaunchStep { Q_OBJECT public: - explicit ExtractNatives(LaunchTask* parent) : LaunchStep(parent){}; - virtual ~ExtractNatives(){}; + explicit ExtractNatives(LaunchTask* parent) : LaunchStep(parent) {}; + virtual ~ExtractNatives() {}; void executeTask() override; bool canAbort() const override { return false; } diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index 4e021c4a8..b35d55924 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -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; diff --git a/launcher/minecraft/launch/LauncherPartLaunch.h b/launcher/minecraft/launch/LauncherPartLaunch.h index 9f6ca1e7b..fcd4daec6 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.h +++ b/launcher/minecraft/launch/LauncherPartLaunch.h @@ -25,7 +25,7 @@ class LauncherPartLaunch : public LaunchStep { Q_OBJECT public: explicit LauncherPartLaunch(LaunchTask* parent); - virtual ~LauncherPartLaunch(){}; + virtual ~LauncherPartLaunch() {}; virtual void executeTask(); virtual bool abort(); diff --git a/launcher/minecraft/launch/ModMinecraftJar.h b/launcher/minecraft/launch/ModMinecraftJar.h index 12e73b5f8..6fc2a8a26 100644 --- a/launcher/minecraft/launch/ModMinecraftJar.h +++ b/launcher/minecraft/launch/ModMinecraftJar.h @@ -21,8 +21,8 @@ class ModMinecraftJar : public LaunchStep { Q_OBJECT public: - explicit ModMinecraftJar(LaunchTask* parent) : LaunchStep(parent){}; - virtual ~ModMinecraftJar(){}; + explicit ModMinecraftJar(LaunchTask* parent) : LaunchStep(parent) {}; + virtual ~ModMinecraftJar() {}; virtual void executeTask() override; virtual bool canAbort() const override { return false; } diff --git a/launcher/minecraft/launch/PrintInstanceInfo.h b/launcher/minecraft/launch/PrintInstanceInfo.h index 8e1c41b62..93c5f0fd6 100644 --- a/launcher/minecraft/launch/PrintInstanceInfo.h +++ b/launcher/minecraft/launch/PrintInstanceInfo.h @@ -25,8 +25,8 @@ class PrintInstanceInfo : public LaunchStep { Q_OBJECT public: explicit PrintInstanceInfo(LaunchTask* parent, AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) - : LaunchStep(parent), m_session(session), m_serverToJoin(serverToJoin){}; - virtual ~PrintInstanceInfo(){}; + : LaunchStep(parent), m_session(session), m_serverToJoin(serverToJoin) {}; + virtual ~PrintInstanceInfo() {}; virtual void executeTask(); virtual bool canAbort() const { return false; } diff --git a/launcher/minecraft/launch/ReconstructAssets.h b/launcher/minecraft/launch/ReconstructAssets.h index bd867c8d4..2c910c595 100644 --- a/launcher/minecraft/launch/ReconstructAssets.h +++ b/launcher/minecraft/launch/ReconstructAssets.h @@ -21,8 +21,8 @@ class ReconstructAssets : public LaunchStep { Q_OBJECT public: - explicit ReconstructAssets(LaunchTask* parent) : LaunchStep(parent){}; - virtual ~ReconstructAssets(){}; + explicit ReconstructAssets(LaunchTask* parent) : LaunchStep(parent) {}; + virtual ~ReconstructAssets() {}; void executeTask() override; bool canAbort() const override { return false; } diff --git a/launcher/minecraft/launch/ScanModFolders.h b/launcher/minecraft/launch/ScanModFolders.h index a5b75825b..5d9350952 100644 --- a/launcher/minecraft/launch/ScanModFolders.h +++ b/launcher/minecraft/launch/ScanModFolders.h @@ -21,8 +21,8 @@ class ScanModFolders : public LaunchStep { Q_OBJECT public: - explicit ScanModFolders(LaunchTask* parent) : LaunchStep(parent){}; - virtual ~ScanModFolders(){}; + explicit ScanModFolders(LaunchTask* parent) : LaunchStep(parent) {}; + virtual ~ScanModFolders() {}; virtual void executeTask() override; virtual bool canAbort() const override { return false; } diff --git a/launcher/minecraft/launch/VerifyJavaInstall.h b/launcher/minecraft/launch/VerifyJavaInstall.h index dabbf3b25..3591ce665 100644 --- a/launcher/minecraft/launch/VerifyJavaInstall.h +++ b/launcher/minecraft/launch/VerifyJavaInstall.h @@ -42,7 +42,7 @@ class VerifyJavaInstall : public LaunchStep { Q_OBJECT public: - explicit VerifyJavaInstall(LaunchTask* parent) : LaunchStep(parent){}; + explicit VerifyJavaInstall(LaunchTask* parent) : LaunchStep(parent) {}; ~VerifyJavaInstall() override = default; void executeTask() override; diff --git a/launcher/minecraft/mod/DataPack.cpp b/launcher/minecraft/mod/DataPack.cpp index fc2d3f68b..4a9e77a70 100644 --- a/launcher/minecraft/mod/DataPack.cpp +++ b/launcher/minecraft/mod/DataPack.cpp @@ -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> 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> 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) { @@ -65,29 +102,24 @@ std::pair DataPack::compatibleVersions() const return s_pack_format_versions.constFind(m_pack_format).value(); } -std::pair DataPack::compare(const Resource& other, SortType type) const +int DataPack::compare(const Resource& other, SortType type) const { auto const& cast_other = static_cast(other); - switch (type) { - default: { - auto res = Resource::compare(other, type); - if (res.first != 0) - return res; - break; - } + default: + return Resource::compare(other, type); case SortType::PACK_FORMAT: { auto this_ver = packFormat(); auto other_ver = cast_other.packFormat(); if (this_ver > other_ver) - return { 1, type == SortType::PACK_FORMAT }; + return 1; if (this_ver < other_ver) - return { -1, type == SortType::PACK_FORMAT }; + return -1; break; } } - return { 0, false }; + return 0; } bool DataPack::applyFilter(QRegularExpression filter) const diff --git a/launcher/minecraft/mod/DataPack.h b/launcher/minecraft/mod/DataPack.h index b3787b238..4855b0203 100644 --- a/launcher/minecraft/mod/DataPack.h +++ b/launcher/minecraft/mod/DataPack.h @@ -56,7 +56,7 @@ class DataPack : public Resource { bool valid() const override; - [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair override; + [[nodiscard]] int compare(Resource const& other, SortType type) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; protected: diff --git a/launcher/minecraft/mod/MetadataHandler.h b/launcher/minecraft/mod/MetadataHandler.h index 3496da2a0..fb3a10133 100644 --- a/launcher/minecraft/mod/MetadataHandler.h +++ b/launcher/minecraft/mod/MetadataHandler.h @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 * * 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); } }; diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 32d0d1614..5442df7fe 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -3,6 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 * * 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 @@ -45,9 +46,9 @@ #include "MetadataHandler.h" #include "Version.h" #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() { @@ -77,7 +78,7 @@ void Mod::setDetails(const ModDetails& details) m_local_details = details; } -std::pair Mod::compare(const Resource& other, SortType type) const +int Mod::compare(const Resource& other, SortType type) const { auto cast_other = dynamic_cast(&other); if (!cast_other) @@ -87,30 +88,49 @@ std::pair Mod::compare(const Resource& other, SortType type) const default: case SortType::ENABLED: case SortType::NAME: - case SortType::DATE: { - auto res = Resource::compare(other, type); - if (res.first != 0) - return res; - break; - } + case SortType::DATE: + case SortType::SIZE: + return Resource::compare(other, type); case SortType::VERSION: { auto this_ver = Version(version()); auto other_ver = Version(cast_other->version()); if (this_ver > other_ver) - return { 1, type == SortType::VERSION }; + return 1; if (this_ver < other_ver) - return { -1, type == SortType::VERSION }; + return -1; break; } case SortType::PROVIDER: { - auto compare_result = - QString::compare(provider().value_or("Unknown"), cast_other->provider().value_or("Unknown"), Qt::CaseInsensitive); - if (compare_result != 0) - return { compare_result, type == 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, false }; + return 0; } bool Mod::applyFilter(QRegularExpression filter) const @@ -228,7 +248,35 @@ void Mod::finishResolvingWithDetails(ModDetails&& details) auto Mod::provider() const -> std::optional { 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 {}; } diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index e97ee9d3b..9bd76c2fd 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 * * 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&; 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; } @@ -88,7 +94,7 @@ class Mod : public Resource { bool valid() const override; - [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair override; + [[nodiscard]] int compare(Resource const& other, SortType type) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; // Delete all the files of this mod diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index fc543202f..bd68f14ef 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -3,6 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 * * 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,11 +48,12 @@ #include #include #include -#include #include "Application.h" #include "Json.h" +#include "minecraft/mod/MetadataHandler.h" +#include "minecraft/mod/Resource.h" #include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/LocalModUpdateTask.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h" @@ -62,12 +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" }); - m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider") }); - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, SortType::DATE, SortType::PROVIDER }; - m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, + 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::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 }; + 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 @@ -105,12 +113,34 @@ 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: return QVariant(); } case Qt::ToolTipRole: - if (column == NAME_COLUMN) { + if (column == NameColumn) { if (at(row)->isSymLinkUnder(instDirPath())) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." @@ -124,7 +154,7 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const } return m_resources[row]->internal_id(); case Qt::DecorationRole: { - if (column == NAME_COLUMN && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) + if (column == NameColumn && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) return APPLICATION->getThemedIcon("status-yellow"); if (column == ImageColumn) { return at(row)->icon({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); @@ -159,6 +189,11 @@ 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: return QVariant(); @@ -176,6 +211,16 @@ 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: return QVariant(); } diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 61d840f9b..db5edf66e 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -3,6 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 * * 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, 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); diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp index da806f0f4..863b0165f 100644 --- a/launcher/minecraft/mod/Resource.cpp +++ b/launcher/minecraft/mod/Resource.cpp @@ -1,9 +1,12 @@ #include "Resource.h" +#include #include #include +#include #include "FileSystem.h" +#include "StringUtils.h" Resource::Resource(QObject* parent) : QObject(parent) {} @@ -18,6 +21,20 @@ void Resource::setFile(QFileInfo file_info) parseFile(); } +std::tuple calculateFileSize(const QFileInfo& file) +{ + if (file.isDir()) { + auto dir = QDir(file.absoluteFilePath()); + dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); + auto count = dir.count(); + auto str = QObject::tr("item"); + if (count != 1) + str = QObject::tr("items"); + return { QString("%1 %2").arg(QString::number(count), str), count }; + } + return { StringUtils::humanReadableFileSize(file.size(), true), file.size() }; +} + void Resource::parseFile() { QString file_name{ m_file_info.fileName() }; @@ -26,6 +43,7 @@ void Resource::parseFile() m_internal_id = file_name; + std::tie(m_size_str, m_size_info) = calculateFileSize(m_file_info); if (m_file_info.isDir()) { m_type = ResourceType::FOLDER; m_name = file_name; @@ -61,15 +79,15 @@ static void removeThePrefix(QString& string) string = string.trimmed(); } -std::pair Resource::compare(const Resource& other, SortType type) const +int Resource::compare(const Resource& other, SortType type) const { switch (type) { default: case SortType::ENABLED: if (enabled() && !other.enabled()) - return { 1, type == SortType::ENABLED }; + return 1; if (!enabled() && other.enabled()) - return { -1, type == SortType::ENABLED }; + return -1; break; case SortType::NAME: { QString this_name{ name() }; @@ -78,20 +96,31 @@ std::pair Resource::compare(const Resource& other, SortType type) con removeThePrefix(this_name); removeThePrefix(other_name); - auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive); - if (compare_result != 0) - return { compare_result, type == SortType::NAME }; - break; + return QString::compare(this_name, other_name, Qt::CaseInsensitive); } case SortType::DATE: if (dateTimeChanged() > other.dateTimeChanged()) - return { 1, type == SortType::DATE }; + return 1; if (dateTimeChanged() < other.dateTimeChanged()) - return { -1, type == SortType::DATE }; + return -1; break; + case SortType::SIZE: { + if (this->type() != other.type()) { + if (this->type() == ResourceType::FOLDER) + return -1; + if (other.type() == ResourceType::FOLDER) + return 1; + } + + if (sizeInfo() > other.sizeInfo()) + return 1; + if (sizeInfo() < other.sizeInfo()) + return -1; + break; + } } - return { 0, false }; + return 0; } bool Resource::applyFilter(QRegularExpression filter) const diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index c1ed49461..f6f859cab 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + * + * 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 @@ -15,7 +50,7 @@ enum class ResourceType { LITEMOD, //!< The resource is a litemod }; -enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER }; +enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER, SIZE, SIDE, LOADERS, MC_VERSIONS, RELEASE_TYPE }; enum class EnableAction { ENABLE, DISABLE, TOGGLE }; @@ -45,6 +80,8 @@ class Resource : public QObject { [[nodiscard]] auto internal_id() const -> QString { return m_internal_id; } [[nodiscard]] auto type() const -> ResourceType { return m_type; } [[nodiscard]] bool enabled() const { return m_enabled; } + [[nodiscard]] QString sizeStr() const { return m_size_str; } + [[nodiscard]] qint64 sizeInfo() const { return m_size_info; } [[nodiscard]] virtual auto name() const -> QString { return m_name; } [[nodiscard]] virtual bool valid() const { return m_type != ResourceType::UNKNOWN; } @@ -53,10 +90,8 @@ class Resource : public QObject { * > 0: 'this' comes after 'other' * = 0: 'this' is equal to 'other' * < 0: 'this' comes before 'other' - * - * The second argument in the pair is true if the sorting type that decided which one is greater was 'type'. */ - [[nodiscard]] virtual auto compare(Resource const& other, SortType type = SortType::NAME) const -> std::pair; + [[nodiscard]] virtual int compare(Resource const& other, SortType type = SortType::NAME) const; /** Returns whether the given filter should filter out 'this' (false), * or if such filter includes the Resource (true). @@ -117,4 +152,6 @@ class Resource : public QObject { bool m_is_resolving = false; bool m_is_resolved = false; int m_resolution_ticket = 0; + QString m_size_str; + qint64 m_size_info; }; diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 74bbc3632..aba0886e8 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -16,6 +16,7 @@ #include "FileSystem.h" #include "QVariantUtils.h" +#include "StringUtils.h" #include "minecraft/mod/tasks/BasicFolderLoadTask.h" #include "settings/Setting.h" @@ -111,7 +112,7 @@ bool ResourceFolderModel::installResource(QString original_path) case ResourceType::ZIPFILE: case ResourceType::LITEMOD: { if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) { - if (!QFile::remove(new_path)) { + if (!FS::deletePath(new_path)) { qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!"; return false; } @@ -281,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, [=] { @@ -416,15 +416,17 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const switch (role) { case Qt::DisplayRole: switch (column) { - case NAME_COLUMN: + case NameColumn: return m_resources[row]->name(); - case DATE_COLUMN: + case DateColumn: return m_resources[row]->dateTimeChanged(); + case SizeColumn: + return m_resources[row]->sizeStr(); default: return {}; } case Qt::ToolTipRole: - if (column == NAME_COLUMN) { + if (column == NameColumn) { if (at(row).isSymLinkUnder(instDirPath())) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." @@ -440,14 +442,14 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const return m_resources[row]->internal_id(); case Qt::DecorationRole: { - if (column == NAME_COLUMN && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) + if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) return APPLICATION->getThemedIcon("status-yellow"); return {}; } case Qt::CheckStateRole: switch (column) { - case ACTIVE_COLUMN: + case ActiveColumn: return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; default: return {}; @@ -486,24 +488,27 @@ QVariant ResourceFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien switch (role) { case Qt::DisplayRole: switch (section) { - case ACTIVE_COLUMN: - case NAME_COLUMN: - case DATE_COLUMN: + case ActiveColumn: + case NameColumn: + case DateColumn: + case SizeColumn: return columnNames().at(section); default: return {}; } case Qt::ToolTipRole: { switch (section) { - case ACTIVE_COLUMN: + case ActiveColumn: //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. return tr("Is the resource enabled?"); - case NAME_COLUMN: + case NameColumn: //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. return tr("The name of the resource."); - case DATE_COLUMN: + case DateColumn: //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. return tr("The date and time this resource was last changed (or added)."); + case SizeColumn: + return tr("The size of the resource."); default: return {}; } @@ -533,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); @@ -610,12 +619,10 @@ SortType ResourceFolderModel::columnToSortKey(size_t column) const auto const& resource_right = model->at(source_right.row()); auto compare_result = resource_left.compare(resource_right, column_sort_key); - if (compare_result.first == 0) + if (compare_result == 0) return QSortFilterProxyModel::lessThan(source_left, source_right); - if (compare_result.second || sortOrder() != Qt::DescendingOrder) - return (compare_result.first < 0); - return (compare_result.first > 0); + return compare_result < 0; } QString ResourceFolderModel::instDirPath() const diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index 942caede5..ca919d3e3 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -96,7 +96,7 @@ class ResourceFolderModel : public QAbstractListModel { /* Qt behavior */ /* Basic columns */ - enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, NUM_COLUMNS }; + enum Columns { ActiveColumn = 0, NameColumn, DateColumn, SizeColumn, NUM_COLUMNS }; QStringList columnNames(bool translated = true) const { return translated ? m_column_names_translated : m_column_names; } [[nodiscard]] int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast(size()); } @@ -195,11 +195,13 @@ class ResourceFolderModel : public QAbstractListModel { protected: // Represents the relationship between a column's index (represented by the list index), and it's sorting key. // As such, the order in with they appear is very important! - QList m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE }; - QStringList m_column_names = { "Enable", "Name", "Last Modified" }; - QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified") }; - QList m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive }; - QList m_columnsHideable = { false, false, true }; + QList m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE, SortType::SIZE }; + QStringList m_column_names = { "Enable", "Name", "Last Modified", "Size" }; + QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified"), tr("Size") }; + QList m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, + QHeaderView::Interactive }; + QList m_columnsHideable = { false, false, true, true }; + QList m_columnsHiddenByDefault = { false, false, false, false }; QDir m_dir; BaseInstance* m_instance; diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp index 074534405..81fb91485 100644 --- a/launcher/minecraft/mod/ResourcePack.cpp +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -11,15 +11,24 @@ #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> 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") } }, - { 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.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") } } + { 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") } }, + { 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.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("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) @@ -94,29 +103,24 @@ std::pair ResourcePack::compatibleVersions() const return s_pack_format_versions.constFind(m_pack_format).value(); } -std::pair ResourcePack::compare(const Resource& other, SortType type) const +int ResourcePack::compare(const Resource& other, SortType type) const { auto const& cast_other = static_cast(other); - switch (type) { - default: { - auto res = Resource::compare(other, type); - if (res.first != 0) - return res; - break; - } + default: + return Resource::compare(other, type); case SortType::PACK_FORMAT: { auto this_ver = packFormat(); auto other_ver = cast_other.packFormat(); if (this_ver > other_ver) - return { 1, type == SortType::PACK_FORMAT }; + return 1; if (this_ver < other_ver) - return { -1, type == SortType::PACK_FORMAT }; + return -1; break; } } - return { 0, false }; + return 0; } bool ResourcePack::applyFilter(QRegularExpression filter) const diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h index c06f3793d..2254fc5c4 100644 --- a/launcher/minecraft/mod/ResourcePack.h +++ b/launcher/minecraft/mod/ResourcePack.h @@ -44,7 +44,7 @@ class ResourcePack : public Resource { bool valid() const override; - [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair override; + [[nodiscard]] int compare(Resource const& other, SortType type) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; protected: diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index 693b8af05..1aa3a2e8c 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -44,17 +44,19 @@ #include "Application.h" #include "Version.h" +#include "minecraft/mod/Resource.h" #include "minecraft/mod/tasks/BasicFolderLoadTask.h" #include "minecraft/mod/tasks/LocalResourcePackParseTask.h" ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) { - m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified" }); - m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") }); - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE }; - m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, - QHeaderView::Interactive }; - m_columnsHideable = { false, true, false, true, true }; + m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified", "Size" }); + m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified"), tr("Size") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE, SortType::SIZE }; + 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 @@ -85,6 +87,8 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const } case DateColumn: return m_resources[row]->dateTimeChanged(); + case SizeColumn: + return m_resources[row]->sizeStr(); default: return {}; @@ -144,6 +148,7 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O case PackFormatColumn: case DateColumn: case ImageColumn: + case SizeColumn: return columnNames().at(section); default: return {}; @@ -160,6 +165,8 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); case DateColumn: return tr("The date and time this resource pack was last changed (or added)."); + case SizeColumn: + return tr("The size of the resource pack."); default: return {}; } diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h index 29c2c5995..755b9c4c6 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.h +++ b/launcher/minecraft/mod/ResourcePackFolderModel.h @@ -7,7 +7,7 @@ class ResourcePackFolderModel : public ResourceFolderModel { Q_OBJECT public: - enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, NUM_COLUMNS }; + enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, SizeColumn, NUM_COLUMNS }; explicit ResourcePackFolderModel(const QString& dir, BaseInstance* instance); diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index f210501c7..48e940e9b 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -44,11 +44,12 @@ TexturePackFolderModel::TexturePackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) { - m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified" }); - m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified") }); - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE }; - m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive }; - m_columnsHideable = { false, true, false, true }; + m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified", "Size" }); + m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Size") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE, SortType::SIZE }; + m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, + QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true }; } Task* TexturePackFolderModel::createUpdateTask() @@ -76,6 +77,8 @@ QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const return m_resources[row]->name(); case DateColumn: return m_resources[row]->dateTimeChanged(); + case SizeColumn: + return m_resources[row]->sizeStr(); default: return {}; } @@ -127,6 +130,7 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or case NameColumn: case DateColumn: case ImageColumn: + case SizeColumn: return columnNames().at(section); default: return {}; @@ -135,13 +139,15 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or switch (section) { case ActiveColumn: //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. - return tr("Is the resource enabled?"); + return tr("Is the texture pack enabled?"); case NameColumn: //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. - return tr("The name of the resource."); + return tr("The name of the texture pack."); case DateColumn: //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. - return tr("The date and time this resource was last changed (or added)."); + return tr("The date and time this texture pack was last changed (or added)."); + case SizeColumn: + return tr("The size of the texture pack."); default: return {}; } diff --git a/launcher/minecraft/mod/TexturePackFolderModel.h b/launcher/minecraft/mod/TexturePackFolderModel.h index b975d8641..de90f879f 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.h +++ b/launcher/minecraft/mod/TexturePackFolderModel.h @@ -44,7 +44,7 @@ class TexturePackFolderModel : public ResourceFolderModel { Q_OBJECT public: - enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, DateColumn, NUM_COLUMNS }; + enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, DateColumn, SizeColumn, NUM_COLUMNS }; explicit TexturePackFolderModel(const QString& dir, std::shared_ptr instance); diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp index 238032532..cc92328ea 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp @@ -23,6 +23,7 @@ #include #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(inst)->getPackProfile()->getSupportedModLoaders().value(); } +static bool checkDependencies(std::shared_ptr 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,9 +77,10 @@ GetModDependenciesTask::GetModDependenciesTask(QObject* parent, void GetModDependenciesTask::prepare() { for (auto sel : m_selected) { - for (auto dep : getDependenciesForVersion(sel->version, sel->pack->provider)) { - addTask(prepareDependencyTask(dep, sel->pack->provider, 20)); - } + if (checkDependencies(sel, m_version, m_loaderType)) + for (auto dep : getDependenciesForVersion(sel->version, sel->pack->provider)) { + addTask(prepareDependencyTask(dep, sel->pack->provider, 20)); + } } } diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp index f495c9b8d..27fbf3c6d 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp @@ -178,6 +178,88 @@ bool processZIP(ResourcePack& pack, ProcessingLevel level) return true; } +QString buildStyle(const QJsonObject& obj) +{ + QStringList styles; + if (auto color = Json::ensureString(obj, "color"); !color.isEmpty()) { + styles << QString("color: %1;").arg(color); + } + if (obj.contains("bold")) { + QString weight = "normal"; + if (Json::ensureBoolean(obj, "bold", false)) { + weight = "bold"; + } + styles << QString("font-weight: %1;").arg(weight); + } + if (obj.contains("italic")) { + QString style = "normal"; + if (Json::ensureBoolean(obj, "italic", false)) { + style = "italic"; + } + styles << QString("font-style: %1;").arg(style); + } + + return styles.isEmpty() ? "" : QString("style=\"%1\"").arg(styles.join(" ")); +} + +QString processComponent(const QJsonArray& value, bool strikethrough, bool underline) +{ + QString result; + for (auto current : value) + result += processComponent(current, strikethrough, underline); + return result; +} + +QString processComponent(const QJsonObject& obj, bool strikethrough, bool underline) +{ + underline = Json::ensureBoolean(obj, "underlined", underline); + strikethrough = Json::ensureBoolean(obj, "strikethrough", strikethrough); + + QString result = Json::ensureString(obj, "text"); + if (underline) { + result = QString("%1").arg(result); + } + if (strikethrough) { + result = QString("%1").arg(result); + } + // the extra needs to be a array + result += processComponent(Json::ensureArray(obj, "extra"), strikethrough, underline); + if (auto style = buildStyle(obj); !style.isEmpty()) { + result = QString("%2").arg(style, result); + } + if (obj.contains("clickEvent")) { + auto click_event = Json::ensureObject(obj, "clickEvent"); + auto action = Json::ensureString(click_event, "action"); + auto value = Json::ensureString(click_event, "value"); + if (action == "open_url" && !value.isEmpty()) { + result = QString("%2").arg(value, result); + } + } + return result; +} + +QString processComponent(const QJsonValue& value, bool strikethrough, bool underline) +{ + if (value.isString()) { + return value.toString(); + } + if (value.isBool()) { + return value.toBool() ? "true" : "false"; + } + if (value.isDouble()) { + return QString::number(value.toDouble()); + } + if (value.isArray()) { + return processComponent(value.toArray(), strikethrough, underline); + } + if (value.isObject()) { + return processComponent(value.toObject(), strikethrough, underline); + } + qWarning() << "Invalid component type!"; + return {}; +} + +// https://minecraft.wiki/w/Raw_JSON_text_format // https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data) { @@ -186,7 +268,9 @@ bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data) auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); pack.setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0)); - pack.setDescription(Json::ensureString(pack_obj, "description", "")); + + pack.setDescription(processComponent(pack_obj.value("description"))); + } catch (Json::JsonException& e) { qWarning() << "JsonException: " << e.what() << e.cause(); return false; diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h index 5199bf3f0..97bf7b2ba 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h @@ -34,6 +34,7 @@ bool process(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); +QString processComponent(const QJsonValue& value, bool strikethrough = false, bool underline = false); bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data); bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data); diff --git a/launcher/minecraft/services/CapeChange.cpp b/launcher/minecraft/services/CapeChange.cpp deleted file mode 100644 index 2ba38a6af..000000000 --- a/launcher/minecraft/services/CapeChange.cpp +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu - * - * 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 . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "CapeChange.h" - -#include -#include - -#include "Application.h" - -CapeChange::CapeChange(QObject* parent, QString token, QString cape) : Task(parent), m_capeId(cape), m_token(token) {} - -void CapeChange::setCape([[maybe_unused]] QString& cape) -{ - QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active")); - auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); - QNetworkReply* rep = APPLICATION->network()->put(request, requestString.toUtf8()); - - setStatus(tr("Equipping cape")); - - m_reply = shared_qobject_ptr(rep); - connect(rep, &QNetworkReply::uploadProgress, this, &CapeChange::setProgress); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 - connect(rep, &QNetworkReply::errorOccurred, this, &CapeChange::downloadError); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &CapeChange::downloadError); -#endif - connect(rep, &QNetworkReply::sslErrors, this, &CapeChange::sslErrors); - connect(rep, &QNetworkReply::finished, this, &CapeChange::downloadFinished); -} - -void CapeChange::clearCape() -{ - QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active")); - auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); - QNetworkReply* rep = APPLICATION->network()->deleteResource(request); - - setStatus(tr("Removing cape")); - - m_reply = shared_qobject_ptr(rep); - connect(rep, &QNetworkReply::uploadProgress, this, &CapeChange::setProgress); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 - connect(rep, &QNetworkReply::errorOccurred, this, &CapeChange::downloadError); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &CapeChange::downloadError); -#endif - connect(rep, &QNetworkReply::sslErrors, this, &CapeChange::sslErrors); - connect(rep, &QNetworkReply::finished, this, &CapeChange::downloadFinished); -} - -void CapeChange::executeTask() -{ - if (m_capeId.isEmpty()) { - clearCape(); - } else { - setCape(m_capeId); - } -} - -void CapeChange::downloadError(QNetworkReply::NetworkError error) -{ - // error happened during download. - qCritical() << "Network error: " << error; - emitFailed(m_reply->errorString()); -} - -void CapeChange::sslErrors(const QList& errors) -{ - int i = 1; - for (auto error : errors) { - qCritical() << "Cape change SSL Error #" << i << " : " << error.errorString(); - auto cert = error.certificate(); - qCritical() << "Certificate in question:\n" << cert.toText(); - i++; - } -} - -void CapeChange::downloadFinished() -{ - // if the download failed - if (m_reply->error() != QNetworkReply::NetworkError::NoError) { - emitFailed(QString("Network error: %1").arg(m_reply->errorString())); - m_reply.reset(); - return; - } - emitSucceeded(); -} diff --git a/launcher/minecraft/services/CapeChange.h b/launcher/minecraft/services/CapeChange.h deleted file mode 100644 index d0c893c44..000000000 --- a/launcher/minecraft/services/CapeChange.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include -#include -#include -#include "QObjectPtr.h" -#include "tasks/Task.h" - -class CapeChange : public Task { - Q_OBJECT - public: - CapeChange(QObject* parent, QString token, QString capeId); - virtual ~CapeChange() {} - - private: - void setCape(QString& cape); - void clearCape(); - - private: - QString m_capeId; - QString m_token; - shared_qobject_ptr m_reply; - - protected: - virtual void executeTask(); - - public slots: - void downloadError(QNetworkReply::NetworkError); - void sslErrors(const QList& errors); - void downloadFinished(); -}; diff --git a/launcher/minecraft/services/SkinDelete.cpp b/launcher/minecraft/services/SkinDelete.cpp deleted file mode 100644 index 9e9020692..000000000 --- a/launcher/minecraft/services/SkinDelete.cpp +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu - * - * 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 . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "SkinDelete.h" - -#include -#include - -#include "Application.h" - -SkinDelete::SkinDelete(QObject* parent, QString token) : Task(parent), m_token(token) {} - -void SkinDelete::executeTask() -{ - QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active")); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); - QNetworkReply* rep = APPLICATION->network()->deleteResource(request); - m_reply = shared_qobject_ptr(rep); - - setStatus(tr("Deleting skin")); - connect(rep, &QNetworkReply::uploadProgress, this, &SkinDelete::setProgress); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 - connect(rep, &QNetworkReply::errorOccurred, this, &SkinDelete::downloadError); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &SkinDelete::downloadError); -#endif - connect(rep, &QNetworkReply::sslErrors, this, &SkinDelete::sslErrors); - connect(rep, &QNetworkReply::finished, this, &SkinDelete::downloadFinished); -} - -void SkinDelete::downloadError(QNetworkReply::NetworkError error) -{ - // error happened during download. - qCritical() << "Network error: " << error; - emitFailed(m_reply->errorString()); -} - -void SkinDelete::sslErrors(const QList& errors) -{ - int i = 1; - for (auto error : errors) { - qCritical() << "Skin Delete SSL Error #" << i << " : " << error.errorString(); - auto cert = error.certificate(); - qCritical() << "Certificate in question:\n" << cert.toText(); - i++; - } -} - -void SkinDelete::downloadFinished() -{ - // if the download failed - if (m_reply->error() != QNetworkReply::NetworkError::NoError) { - emitFailed(QString("Network error: %1").arg(m_reply->errorString())); - m_reply.reset(); - return; - } - emitSucceeded(); -} diff --git a/launcher/minecraft/services/SkinDelete.h b/launcher/minecraft/services/SkinDelete.h deleted file mode 100644 index 44e30453f..000000000 --- a/launcher/minecraft/services/SkinDelete.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include -#include -#include "tasks/Task.h" - -using SkinDeletePtr = shared_qobject_ptr; - -class SkinDelete : public Task { - Q_OBJECT - public: - SkinDelete(QObject* parent, QString token); - virtual ~SkinDelete() = default; - - private: - QString m_token; - shared_qobject_ptr m_reply; - - protected: - virtual void executeTask(); - - public slots: - void downloadError(QNetworkReply::NetworkError); - void sslErrors(const QList& errors); - void downloadFinished(); -}; diff --git a/launcher/minecraft/services/SkinUpload.cpp b/launcher/minecraft/services/SkinUpload.cpp deleted file mode 100644 index 163b481b1..000000000 --- a/launcher/minecraft/services/SkinUpload.cpp +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu - * - * 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 . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "SkinUpload.h" - -#include -#include - -#include "Application.h" - -QByteArray getVariant(SkinUpload::Model model) -{ - switch (model) { - default: - qDebug() << "Unknown skin type!"; - case SkinUpload::STEVE: - return "CLASSIC"; - case SkinUpload::ALEX: - return "SLIM"; - } -} - -SkinUpload::SkinUpload(QObject* parent, QString token, QByteArray skin, SkinUpload::Model model) - : Task(parent), m_model(model), m_skin(skin), m_token(token) -{} - -void SkinUpload::executeTask() -{ - QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins")); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); - QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); - - QHttpPart skin; - skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png")); - skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\"")); - skin.setBody(m_skin); - - QHttpPart model; - model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\"")); - model.setBody(getVariant(m_model)); - - multiPart->append(skin); - multiPart->append(model); - - QNetworkReply* rep = APPLICATION->network()->post(request, multiPart); - m_reply = shared_qobject_ptr(rep); - - setStatus(tr("Uploading skin")); - connect(rep, &QNetworkReply::uploadProgress, this, &SkinUpload::setProgress); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 - connect(rep, &QNetworkReply::errorOccurred, this, &SkinUpload::downloadError); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &SkinUpload::downloadError); -#endif - connect(rep, &QNetworkReply::sslErrors, this, &SkinUpload::sslErrors); - connect(rep, &QNetworkReply::finished, this, &SkinUpload::downloadFinished); -} - -void SkinUpload::downloadError(QNetworkReply::NetworkError error) -{ - // error happened during download. - qCritical() << "Network error: " << error; - emitFailed(m_reply->errorString()); -} - -void SkinUpload::sslErrors(const QList& errors) -{ - int i = 1; - for (auto error : errors) { - qCritical() << "Skin Upload SSL Error #" << i << " : " << error.errorString(); - auto cert = error.certificate(); - qCritical() << "Certificate in question:\n" << cert.toText(); - i++; - } -} - -void SkinUpload::downloadFinished() -{ - // if the download failed - if (m_reply->error() != QNetworkReply::NetworkError::NoError) { - emitFailed(QString("Network error: %1").arg(m_reply->errorString())); - m_reply.reset(); - return; - } - emitSucceeded(); -} diff --git a/launcher/minecraft/services/SkinUpload.h b/launcher/minecraft/services/SkinUpload.h deleted file mode 100644 index 016367ff8..000000000 --- a/launcher/minecraft/services/SkinUpload.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include -#include -#include -#include "tasks/Task.h" - -using SkinUploadPtr = shared_qobject_ptr; - -class SkinUpload : public Task { - Q_OBJECT - public: - enum Model { STEVE, ALEX }; - - // Note this class takes ownership of the file. - SkinUpload(QObject* parent, QString token, QByteArray skin, Model model = STEVE); - virtual ~SkinUpload() {} - - private: - Model m_model; - QByteArray m_skin; - QString m_token; - shared_qobject_ptr m_reply; - - protected: - virtual void executeTask(); - - public slots: - - void downloadError(QNetworkReply::NetworkError); - void sslErrors(const QList& errors); - - void downloadFinished(); -}; diff --git a/launcher/minecraft/skins/CapeChange.cpp b/launcher/minecraft/skins/CapeChange.cpp new file mode 100644 index 000000000..4db28e245 --- /dev/null +++ b/launcher/minecraft/skins/CapeChange.cpp @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 + * + * 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 . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "CapeChange.h" + +#include + +#include "net/ByteArraySink.h" +#include "net/StaticHeaderProxy.h" + +CapeChange::CapeChange(QString token, QString cape) : NetRequest(), m_capeId(cape), m_token(token) +{ + logCat = taskMCSkinsLogC; +} + +QNetworkReply* CapeChange::getReply(QNetworkRequest& request) +{ + if (m_capeId.isEmpty()) { + setStatus(tr("Removing cape")); + return m_network->deleteResource(request); + } else { + setStatus(tr("Equipping cape")); + return m_network->put(request, QString("{\"capeId\":\"%1\"}").arg(m_capeId).toUtf8()); + } +} + +void CapeChange::init() +{ + addHeaderProxy(new Net::StaticHeaderProxy(QList{ + { "Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit() }, + })); +} + +CapeChange::Ptr CapeChange::make(QString token, QString capeId) +{ + auto up = makeShared(token, capeId); + up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active"); + up->setObjectName(QString("BYTES:") + up->m_url.toString()); + up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); + return up; +} diff --git a/launcher/minecraft/skins/CapeChange.h b/launcher/minecraft/skins/CapeChange.h new file mode 100644 index 000000000..bcafcde87 --- /dev/null +++ b/launcher/minecraft/skins/CapeChange.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#pragma once + +#include "net/NetRequest.h" + +class CapeChange : public Net::NetRequest { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + CapeChange(QString token, QString capeId); + virtual ~CapeChange() = default; + + static CapeChange::Ptr make(QString token, QString capeId); + void init() override; + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; + + private: + QString m_capeId; + QString m_token; +}; diff --git a/launcher/minecraft/skins/SkinDelete.cpp b/launcher/minecraft/skins/SkinDelete.cpp new file mode 100644 index 000000000..3c50cf313 --- /dev/null +++ b/launcher/minecraft/skins/SkinDelete.cpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 + * + * 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 . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "SkinDelete.h" + +#include "net/ByteArraySink.h" +#include "net/StaticHeaderProxy.h" + +SkinDelete::SkinDelete(QString token) : NetRequest(), m_token(token) +{ + logCat = taskMCSkinsLogC; +} + +QNetworkReply* SkinDelete::getReply(QNetworkRequest& request) +{ + setStatus(tr("Deleting skin")); + return m_network->deleteResource(request); +} + +void SkinDelete::init() +{ + addHeaderProxy(new Net::StaticHeaderProxy(QList{ + { "Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit() }, + })); +} + +SkinDelete::Ptr SkinDelete::make(QString token) +{ + auto up = makeShared(token); + up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active"); + up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); + return up; +} diff --git a/launcher/minecraft/skins/SkinDelete.h b/launcher/minecraft/skins/SkinDelete.h new file mode 100644 index 000000000..5d02e0cc4 --- /dev/null +++ b/launcher/minecraft/skins/SkinDelete.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#pragma once + +#include "net/NetRequest.h" + +class SkinDelete : public Net::NetRequest { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + SkinDelete(QString token); + virtual ~SkinDelete() = default; + + static SkinDelete::Ptr make(QString token); + void init() override; + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; + + private: + QString m_token; +}; diff --git a/launcher/minecraft/skins/SkinList.cpp b/launcher/minecraft/skins/SkinList.cpp new file mode 100644 index 000000000..fd883ad52 --- /dev/null +++ b/launcher/minecraft/skins/SkinList.cpp @@ -0,0 +1,389 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#include "SkinList.h" + +#include +#include + +#include "FileSystem.h" +#include "Json.h" +#include "minecraft/skins/SkinModel.h" + +SkinList::SkinList(QObject* parent, QString path, MinecraftAccountPtr acct) : QAbstractListModel(parent), m_acct(acct) +{ + FS::ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + m_watcher.reset(new QFileSystemWatcher(this)); + is_watching = false; + connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &SkinList::directoryChanged); + connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &SkinList::fileChanged); + directoryChanged(path); +} + +void SkinList::startWatching() +{ + if (is_watching) { + return; + } + update(); + is_watching = m_watcher->addPath(m_dir.absolutePath()); + if (is_watching) { + qDebug() << "Started watching " << m_dir.absolutePath(); + } else { + qDebug() << "Failed to start watching " << m_dir.absolutePath(); + } +} + +void SkinList::stopWatching() +{ + save(); + if (!is_watching) { + return; + } + is_watching = !m_watcher->removePath(m_dir.absolutePath()); + if (!is_watching) { + qDebug() << "Stopped watching " << m_dir.absolutePath(); + } else { + qDebug() << "Failed to stop watching " << m_dir.absolutePath(); + } +} + +bool SkinList::update() +{ + QVector newSkins; + m_dir.refresh(); + + auto manifestInfo = QFileInfo(m_dir.absoluteFilePath("index.json")); + if (manifestInfo.exists()) { + try { + auto doc = Json::requireDocument(manifestInfo.absoluteFilePath(), "SkinList JSON file"); + const auto root = doc.object(); + auto skins = Json::ensureArray(root, "skins"); + for (auto jSkin : skins) { + SkinModel s(m_dir, Json::ensureObject(jSkin)); + if (s.isValid()) { + newSkins << s; + } + } + } catch (const Exception& e) { + qCritical() << "Couldn't load skins json:" << e.cause(); + } + } + + bool needsSave = false; + const auto& skin = m_acct->accountData()->minecraftProfile.skin; + if (!skin.url.isEmpty() && !skin.data.isEmpty()) { + QPixmap skinTexture; + SkinModel* nskin = nullptr; + for (auto i = 0; i < newSkins.size(); i++) { + if (newSkins[i].getURL() == skin.url) { + nskin = &newSkins[i]; + break; + } + } + if (!nskin) { + auto name = m_acct->profileName() + ".png"; + if (QFileInfo(m_dir.absoluteFilePath(name)).exists()) { + name = QUrl(skin.url).fileName() + ".png"; + } + auto path = m_dir.absoluteFilePath(name); + if (skinTexture.loadFromData(skin.data, "PNG") && skinTexture.save(path)) { + SkinModel s(path); + s.setModel(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); + s.setCapeId(m_acct->accountData()->minecraftProfile.currentCape); + s.setURL(skin.url); + newSkins << s; + needsSave = true; + } + } else { + nskin->setCapeId(m_acct->accountData()->minecraftProfile.currentCape); + nskin->setModel(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); + } + } + + auto folderContents = m_dir.entryInfoList(); + // if there are any untracked files... + for (QFileInfo entry : folderContents) { + if (!entry.isFile() && entry.suffix() != "png") + continue; + + SkinModel w(entry.absoluteFilePath()); + if (w.isValid()) { + auto add = true; + for (auto s : newSkins) { + if (s.name() == w.name()) { + add = false; + break; + } + } + if (add) { + newSkins.append(w); + needsSave = true; + } + } + } + std::sort(newSkins.begin(), newSkins.end(), + [](const SkinModel& a, const SkinModel& b) { return a.getPath().localeAwareCompare(b.getPath()) < 0; }); + beginResetModel(); + m_skin_list.swap(newSkins); + endResetModel(); + if (needsSave) + save(); + return true; +} + +void SkinList::directoryChanged(const QString& path) +{ + QDir new_dir(path); + if (!new_dir.exists()) + if (!FS::ensureFolderPathExists(new_dir.absolutePath())) + return; + if (m_dir.absolutePath() != new_dir.absolutePath()) { + m_dir.setPath(path); + m_dir.refresh(); + if (is_watching) + stopWatching(); + startWatching(); + } + update(); +} + +void SkinList::fileChanged(const QString& path) +{ + qDebug() << "Checking " << path; + QFileInfo checkfile(path); + if (!checkfile.exists()) + return; + + for (int i = 0; i < m_skin_list.count(); i++) { + if (m_skin_list[i].getPath() == checkfile.absoluteFilePath()) { + m_skin_list[i].refresh(); + dataChanged(index(i), index(i)); + break; + } + } +} + +QStringList SkinList::mimeTypes() const +{ + return { "text/uri-list" }; +} + +Qt::DropActions SkinList::supportedDropActions() const +{ + return Qt::CopyAction; +} + +bool SkinList::dropMimeData(const QMimeData* data, + Qt::DropAction action, + [[maybe_unused]] int row, + [[maybe_unused]] int column, + [[maybe_unused]] const QModelIndex& parent) +{ + if (action == Qt::IgnoreAction) + return true; + // check if the action is supported + if (!data || !(action & supportedDropActions())) + return false; + + // files dropped from outside? + if (data->hasUrls()) { + auto urls = data->urls(); + QStringList skinFiles; + for (auto url : urls) { + // only local files may be dropped... + if (!url.isLocalFile()) + continue; + skinFiles << url.toLocalFile(); + } + installSkins(skinFiles); + return true; + } + return false; +} + +Qt::ItemFlags SkinList::flags(const QModelIndex& index) const +{ + Qt::ItemFlags f = Qt::ItemIsDropEnabled | QAbstractListModel::flags(index); + if (index.isValid()) { + f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); + } + return f; +} + +QVariant SkinList::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + + if (row < 0 || row >= m_skin_list.size()) + return QVariant(); + auto skin = m_skin_list[row]; + switch (role) { + case Qt::DecorationRole: + return skin.getTexture(); + case Qt::DisplayRole: + return skin.name(); + case Qt::UserRole: + return skin.name(); + case Qt::EditRole: + return skin.name(); + default: + return QVariant(); + } +} + +int SkinList::rowCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : m_skin_list.size(); +} + +void SkinList::installSkins(const QStringList& iconFiles) +{ + for (QString file : iconFiles) + installSkin(file); +} + +QString SkinList::installSkin(const QString& file, const QString& name) +{ + if (file.isEmpty()) + return tr("Path is empty."); + QFileInfo fileinfo(file); + if (!fileinfo.exists()) + return tr("File doesn't exist."); + if (!fileinfo.isFile()) + return tr("Not a file."); + if (!fileinfo.isReadable()) + return tr("File is not readable."); + if (fileinfo.suffix() != "png" && !SkinModel(fileinfo.absoluteFilePath()).isValid()) + return tr("Skin images must be 64x64 or 64x32 pixel PNG files."); + + QString target = FS::PathCombine(m_dir.absolutePath(), name.isEmpty() ? fileinfo.fileName() : name); + + return QFile::copy(file, target) ? "" : tr("Unable to copy file"); +} + +int SkinList::getSkinIndex(const QString& key) const +{ + for (int i = 0; i < m_skin_list.count(); i++) { + if (m_skin_list[i].name() == key) { + return i; + } + } + return -1; +} + +const SkinModel* SkinList::skin(const QString& key) const +{ + int idx = getSkinIndex(key); + if (idx == -1) + return nullptr; + return &m_skin_list[idx]; +} + +SkinModel* SkinList::skin(const QString& key) +{ + int idx = getSkinIndex(key); + if (idx == -1) + return nullptr; + return &m_skin_list[idx]; +} + +bool SkinList::deleteSkin(const QString& key, const bool trash) +{ + int idx = getSkinIndex(key); + if (idx != -1) { + auto s = m_skin_list[idx]; + if (trash) { + if (FS::trash(s.getPath(), nullptr)) { + m_skin_list.remove(idx); + save(); + return true; + } + } else if (QFile::remove(s.getPath())) { + m_skin_list.remove(idx); + save(); + return true; + } + } + return false; +} + +void SkinList::save() +{ + QJsonObject doc; + QJsonArray arr; + for (auto s : m_skin_list) { + arr << s.toJSON(); + } + doc["skins"] = arr; + Json::write(doc, m_dir.absoluteFilePath("index.json")); +} + +int SkinList::getSelectedAccountSkin() +{ + const auto& skin = m_acct->accountData()->minecraftProfile.skin; + for (int i = 0; i < m_skin_list.count(); i++) { + if (m_skin_list[i].getURL() == skin.url) { + return i; + } + } + return -1; +} + +bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role) +{ + if (!idx.isValid() || role != Qt::EditRole) { + return false; + } + + int row = idx.row(); + if (row < 0 || row >= m_skin_list.size()) + return false; + auto& skin = m_skin_list[row]; + auto newName = value.toString(); + if (skin.name() != newName) { + skin.rename(newName); + save(); + } + return true; +} + +void SkinList::updateSkin(SkinModel* s) +{ + auto done = false; + for (auto i = 0; i < m_skin_list.size(); i++) { + if (m_skin_list[i].getPath() == s->getPath()) { + m_skin_list[i].setCapeId(s->getCapeId()); + m_skin_list[i].setModel(s->getModel()); + m_skin_list[i].setURL(s->getURL()); + done = true; + break; + } + } + if (!done) { + beginInsertRows(QModelIndex(), m_skin_list.count(), m_skin_list.count() + 1); + m_skin_list.append(*s); + endInsertRows(); + } + save(); +} diff --git a/launcher/minecraft/skins/SkinList.h b/launcher/minecraft/skins/SkinList.h new file mode 100644 index 000000000..66af6a17b --- /dev/null +++ b/launcher/minecraft/skins/SkinList.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#pragma once + +#include +#include +#include + +#include "QObjectPtr.h" +#include "SkinModel.h" +#include "minecraft/auth/MinecraftAccount.h" + +class SkinList : public QAbstractListModel { + Q_OBJECT + public: + explicit SkinList(QObject* parent, QString path, MinecraftAccountPtr acct); + virtual ~SkinList() { save(); }; + + int getSkinIndex(const QString& key) const; + + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex& idx, const QVariant& value, int role) override; + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; + + virtual QStringList mimeTypes() const override; + virtual Qt::DropActions supportedDropActions() const override; + virtual bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; + virtual Qt::ItemFlags flags(const QModelIndex& index) const override; + + bool deleteSkin(const QString& key, const bool trash); + + void installSkins(const QStringList& iconFiles); + QString installSkin(const QString& file, const QString& name = {}); + + const SkinModel* skin(const QString& key) const; + SkinModel* skin(const QString& key); + + void startWatching(); + void stopWatching(); + + QString getDir() const { return m_dir.absolutePath(); } + void save(); + int getSelectedAccountSkin(); + + void updateSkin(SkinModel* s); + + private: + // hide copy constructor + SkinList(const SkinList&) = delete; + // hide assign op + SkinList& operator=(const SkinList&) = delete; + + protected slots: + void directoryChanged(const QString& path); + void fileChanged(const QString& path); + bool update(); + + private: + shared_qobject_ptr m_watcher; + bool is_watching; + QVector m_skin_list; + QDir m_dir; + MinecraftAccountPtr m_acct; +}; \ No newline at end of file diff --git a/launcher/minecraft/skins/SkinModel.cpp b/launcher/minecraft/skins/SkinModel.cpp new file mode 100644 index 000000000..d53b9e762 --- /dev/null +++ b/launcher/minecraft/skins/SkinModel.cpp @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#include "SkinModel.h" +#include +#include +#include +#include + +#include "FileSystem.h" +#include "Json.h" + +SkinModel::SkinModel(QString path) : m_path(path), m_texture(path), m_model(Model::CLASSIC) {} + +SkinModel::SkinModel(QDir skinDir, QJsonObject obj) + : m_cape_id(Json::ensureString(obj, "capeId")), m_model(Model::CLASSIC), m_url(Json::ensureString(obj, "url")) +{ + auto name = Json::ensureString(obj, "name"); + + if (auto model = Json::ensureString(obj, "model"); model == "SLIM") { + m_model = Model::SLIM; + } + m_path = skinDir.absoluteFilePath(name) + ".png"; + m_texture = QPixmap(m_path); +} + +QString SkinModel::name() const +{ + return QFileInfo(m_path).baseName(); +} + +bool SkinModel::rename(QString newName) +{ + auto info = QFileInfo(m_path); + m_path = FS::PathCombine(info.absolutePath(), newName + ".png"); + return FS::move(info.absoluteFilePath(), m_path); +} + +QJsonObject SkinModel::toJSON() const +{ + QJsonObject obj; + obj["name"] = name(); + obj["capeId"] = m_cape_id; + obj["url"] = m_url; + obj["model"] = getModelString(); + return obj; +} + +QString SkinModel::getModelString() const +{ + switch (m_model) { + case CLASSIC: + return "CLASSIC"; + case SLIM: + return "SLIM"; + } + return {}; +} + +bool SkinModel::isValid() const +{ + return !m_texture.isNull() && (m_texture.size().height() == 32 || m_texture.size().height() == 64) && m_texture.size().width() == 64; +} diff --git a/launcher/minecraft/skins/SkinModel.h b/launcher/minecraft/skins/SkinModel.h new file mode 100644 index 000000000..46e9d6cf1 --- /dev/null +++ b/launcher/minecraft/skins/SkinModel.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#pragma once + +#include +#include +#include + +class SkinModel { + public: + enum Model { CLASSIC, SLIM }; + + SkinModel() = default; + SkinModel(QString path); + SkinModel(QDir skinDir, QJsonObject obj); + virtual ~SkinModel() = default; + + QString name() const; + QString getModelString() const; + bool isValid() const; + QString getPath() const { return m_path; } + QPixmap getTexture() const { return m_texture; } + QString getCapeId() const { return m_cape_id; } + Model getModel() const { return m_model; } + QString getURL() const { return m_url; } + + bool rename(QString newName); + void setCapeId(QString capeID) { m_cape_id = capeID; } + void setModel(Model model) { m_model = model; } + void setURL(QString url) { m_url = url; } + void refresh() { m_texture = QPixmap(m_path); } + + QJsonObject toJSON() const; + + private: + QString m_path; + QPixmap m_texture; + QString m_cape_id; + Model m_model; + QString m_url; +}; \ No newline at end of file diff --git a/launcher/minecraft/skins/SkinUpload.cpp b/launcher/minecraft/skins/SkinUpload.cpp new file mode 100644 index 000000000..dc1bc0bf9 --- /dev/null +++ b/launcher/minecraft/skins/SkinUpload.cpp @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 + * + * 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 . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "SkinUpload.h" + +#include + +#include "FileSystem.h" +#include "net/ByteArraySink.h" +#include "net/StaticHeaderProxy.h" + +SkinUpload::SkinUpload(QString token, QString path, QString variant) : NetRequest(), m_token(token), m_path(path), m_variant(variant) +{ + logCat = taskMCSkinsLogC; +} + +QNetworkReply* SkinUpload::getReply(QNetworkRequest& request) +{ + QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType, this); + + QHttpPart skin; + skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png")); + skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\"")); + + skin.setBody(FS::read(m_path)); + + QHttpPart model; + model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\"")); + model.setBody(m_variant.toUtf8()); + + multiPart->append(skin); + multiPart->append(model); + setStatus(tr("Uploading skin")); + return m_network->post(request, multiPart); +} + +void SkinUpload::init() +{ + addHeaderProxy(new Net::StaticHeaderProxy(QList{ + { "Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit() }, + })); +} + +SkinUpload::Ptr SkinUpload::make(QString token, QString path, QString variant) +{ + auto up = makeShared(token, path, variant); + up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins"); + up->setObjectName(QString("BYTES:") + up->m_url.toString()); + up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); + return up; +} diff --git a/launcher/minecraft/skins/SkinUpload.h b/launcher/minecraft/skins/SkinUpload.h new file mode 100644 index 000000000..f24cef5a2 --- /dev/null +++ b/launcher/minecraft/skins/SkinUpload.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#pragma once + +#include "net/NetRequest.h" + +class SkinUpload : public Net::NetRequest { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + + // Note this class takes ownership of the file. + SkinUpload(QString token, QString path, QString variant); + virtual ~SkinUpload() = default; + + static SkinUpload::Ptr make(QString token, QString path, QString variant); + void init() override; + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; + + private: + QString m_token; + QString m_path; + QString m_variant; +}; diff --git a/launcher/minecraft/update/FMLLibrariesTask.h b/launcher/minecraft/update/FMLLibrariesTask.h index 9d0102be7..32712d239 100644 --- a/launcher/minecraft/update/FMLLibrariesTask.h +++ b/launcher/minecraft/update/FMLLibrariesTask.h @@ -9,7 +9,7 @@ class FMLLibrariesTask : public Task { Q_OBJECT public: FMLLibrariesTask(MinecraftInstance* inst); - virtual ~FMLLibrariesTask(){}; + virtual ~FMLLibrariesTask() {}; void executeTask() override; diff --git a/launcher/minecraft/update/FoldersTask.h b/launcher/minecraft/update/FoldersTask.h index 2d2954b2a..be4e33eb7 100644 --- a/launcher/minecraft/update/FoldersTask.h +++ b/launcher/minecraft/update/FoldersTask.h @@ -7,7 +7,7 @@ class FoldersTask : public Task { Q_OBJECT public: FoldersTask(MinecraftInstance* inst); - virtual ~FoldersTask(){}; + virtual ~FoldersTask() {}; void executeTask() override; diff --git a/launcher/minecraft/update/LibrariesTask.h b/launcher/minecraft/update/LibrariesTask.h index c969e74df..441191154 100644 --- a/launcher/minecraft/update/LibrariesTask.h +++ b/launcher/minecraft/update/LibrariesTask.h @@ -7,7 +7,7 @@ class LibrariesTask : public Task { Q_OBJECT public: LibrariesTask(MinecraftInstance* inst); - virtual ~LibrariesTask(){}; + virtual ~LibrariesTask() {}; void executeTask() override; diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h index b19b25484..e17b1520a 100644 --- a/launcher/modplatform/CheckUpdateTask.h +++ b/launcher/modplatform/CheckUpdateTask.h @@ -17,7 +17,7 @@ class CheckUpdateTask : public Task { std::list& mcVersions, std::optional loaders, std::shared_ptr mods_folder) - : Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder){}; + : Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder) {}; struct UpdatableMod { QString name; diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index ce53ee62d..43acea1a2 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -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(); auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response); diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index fc79dff15..8c85ae122 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 * * 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 diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index eff7e7f9f..91c9898a9 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -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 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 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) diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index 2c7bec5d4..b7364d9ab 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -4,6 +4,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 * * 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 sorting; std::optional loaders; std::optional > versions; + std::optional side; + std::optional categoryIds; }; struct SearchCallbacks { std::function on_succeed; diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 01de88b04..a7721673e 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -282,7 +282,7 @@ void PackInstallTask::deleteExistingFiles() // Delete the files for (const auto& item : filesToDelete) { - QFile::remove(item); + FS::deletePath(item); } } @@ -987,7 +987,7 @@ bool PackInstallTask::extractMods(const QMap& toExtract, // the copy from the Configs.zip QFileInfo fileInfo(to); if (fileInfo.exists()) { - if (!QFile::remove(to)) { + if (!FS::deletePath(to)) { qWarning() << "Failed to delete" << to; return false; } diff --git a/launcher/modplatform/flame/FileResolvingTask.h b/launcher/modplatform/flame/FileResolvingTask.h index c280827af..cfa53cb22 100644 --- a/launcher/modplatform/flame/FileResolvingTask.h +++ b/launcher/modplatform/flame/FileResolvingTask.h @@ -9,7 +9,7 @@ class FileResolvingTask : public Task { Q_OBJECT public: explicit FileResolvingTask(const shared_qobject_ptr& network, Flame::Manifest& toProcess); - virtual ~FileResolvingTask(){}; + virtual ~FileResolvingTask() {}; bool canAbort() const override { return true; } bool abort() override; diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index bb4f18983..a1cfe1a60 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -3,10 +3,12 @@ // SPDX-License-Identifier: GPL-3.0-only #include "FlameAPI.h" +#include #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 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 response) +{ + auto netJob = makeShared(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 FlameAPI::loadModCategories(std::shared_ptr response) +{ + QList 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; +}; \ No newline at end of file diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index e22d8f0d8..dfe76f9d5 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include #include "modplatform/ModIndex.h" @@ -22,6 +23,9 @@ class FlameAPI : public NetworkResourceAPI { Task::Ptr getFiles(const QStringList& fileIds, std::shared_ptr response) const; Task::Ptr getFile(const QString& addonId, const QString& fileId, std::shared_ptr response) const; + static Task::Ptr getModCategories(std::shared_ptr response); + static QList loadModCategories(std::shared_ptr response); + [[nodiscard]] auto getSortingMethods() const -> QList 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('&'); diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index a1f10c156..a629cc15b 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -322,7 +322,7 @@ bool FlameCreationTask::createInstance() // Keep index file in case we need it some other time (like when changing versions) QString new_index_place(FS::PathCombine(parent_folder, "manifest.json")); FS::ensureFilePathExists(new_index_place); - QFile::rename(index_path, new_index_place); + FS::move(index_path, new_index_place); } catch (const JSONValidationError& e) { setError(tr("Could not understand pack manifest:\n") + e.cause()); @@ -336,7 +336,7 @@ bool FlameCreationTask::createInstance() Override::createOverrides("overrides", parent_folder, overridePath); QString mcPath = FS::PathCombine(m_stagingPath, "minecraft"); - if (!QFile::rename(overridePath, mcPath)) { + if (!FS::move(overridePath, mcPath)) { setError(tr("Could not rename the overrides folder:\n") + m_pack.overrides); return false; } @@ -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)) { diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 83a28fa2b..7de05f177 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -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 unsortedVersions; - auto profile = (dynamic_cast(inst))->getPackProfile(); - QString mcVersion = profile->getComponentVersion("net.minecraft"); - auto loaders = profile->getSupportedModLoaders(); - for (auto versionIter : arr) { auto obj = versionIter.toObject(); @@ -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"); diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp index 569181732..11bc3553b 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.cpp +++ b/launcher/modplatform/flame/FlamePackExportTask.cpp @@ -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 }); diff --git a/launcher/modplatform/helpers/ExportToModList.cpp b/launcher/modplatform/helpers/ExportToModList.cpp index 1f01c4a89..aea16ab50 100644 --- a/launcher/modplatform/helpers/ExportToModList.cpp +++ b/launcher/modplatform/helpers/ExportToModList.cpp @@ -42,17 +42,28 @@ QString toHTML(QList mods, OptionalData extraData) } if (extraData & Authors && !mod->authors().isEmpty()) line += " by " + mod->authors().join(", ").toHtmlEscaped(); + if (extraData & FileName) + line += QString(" (%1)").arg(mod->fileinfo().fileName().toHtmlEscaped()); + lines.append(QString("
  • %1
  • ").arg(line)); } return QString("
      \n\t%1\n
    ").arg(lines.join("\n\t")); } +QString toMarkdownEscaped(QString src) +{ + for (auto ch : "\\`*_{}[]<>()#+-.!|") + src.replace(ch, QString("\\%1").arg(ch)); + return src; +} + QString toMarkdown(QList mods, OptionalData extraData) { QStringList lines; + for (auto mod : mods) { auto meta = mod->metadata(); - auto modName = mod->name(); + auto modName = toMarkdownEscaped(mod->name()); if (extraData & Url) { auto url = mod->metaurl(); if (!url.isEmpty()) @@ -60,14 +71,16 @@ QString toMarkdown(QList mods, OptionalData extraData) } auto line = modName; if (extraData & Version) { - auto ver = mod->version(); + auto ver = toMarkdownEscaped(mod->version()); if (ver.isEmpty() && meta != nullptr) - ver = meta->version().toString(); + ver = toMarkdownEscaped(meta->version().toString()); if (!ver.isEmpty()) line += QString(" [%1]").arg(ver); } if (extraData & Authors && !mod->authors().isEmpty()) - line += " by " + mod->authors().join(", "); + line += " by " + toMarkdownEscaped(mod->authors().join(", ")); + if (extraData & FileName) + line += QString(" (%1)").arg(toMarkdownEscaped(mod->fileinfo().fileName())); lines << "- " + line; } return lines.join("\n"); @@ -95,6 +108,8 @@ QString toPlainTXT(QList mods, OptionalData extraData) } if (extraData & Authors && !mod->authors().isEmpty()) line += " by " + mod->authors().join(", "); + if (extraData & FileName) + line += QString(" (%1)").arg(mod->fileinfo().fileName()); lines << line; } return lines.join("\n"); @@ -122,6 +137,8 @@ QString toJSON(QList mods, OptionalData extraData) } if (extraData & Authors && !mod->authors().isEmpty()) line["authors"] = QJsonArray::fromStringList(mod->authors()); + if (extraData & FileName) + line["filename"] = mod->fileinfo().fileName(); lines << line; } QJsonDocument doc; @@ -154,6 +171,8 @@ QString toCSV(QList mods, OptionalData extraData) authors = QString("\"%1\"").arg(mod->authors().join(",")); data << authors; } + if (extraData & FileName) + data << mod->fileinfo().fileName(); lines << data.join(","); } return lines.join("\n"); @@ -189,11 +208,13 @@ QString exportToModList(QList mods, QString lineTemplate) if (ver.isEmpty() && meta != nullptr) ver = meta->version().toString(); auto authors = mod->authors().join(", "); + auto filename = mod->fileinfo().fileName(); lines << QString(lineTemplate) .replace("{name}", modName) .replace("{url}", url) .replace("{version}", ver) - .replace("{authors}", authors); + .replace("{authors}", authors) + .replace("{filename}", filename); } return lines.join("\n"); } diff --git a/launcher/modplatform/helpers/ExportToModList.h b/launcher/modplatform/helpers/ExportToModList.h index 7ea4ba9c2..ab7797fe6 100644 --- a/launcher/modplatform/helpers/ExportToModList.h +++ b/launcher/modplatform/helpers/ExportToModList.h @@ -23,11 +23,7 @@ namespace ExportToModList { enum Formats { HTML, MARKDOWN, PLAINTXT, JSON, CSV, CUSTOM }; -enum OptionalData { - Authors = 1 << 0, - Url = 1 << 1, - Version = 1 << 2, -}; +enum OptionalData { Authors = 1 << 0, Url = 1 << 1, Version = 1 << 2, FileName = 1 << 3 }; QString exportToModList(QList mods, Formats format, OptionalData extraData); QString exportToModList(QList mods, QString lineTemplate); } // namespace ExportToModList diff --git a/launcher/modplatform/helpers/HashUtils.cpp b/launcher/modplatform/helpers/HashUtils.cpp index 6ff1d1710..f20af1f09 100644 --- a/launcher/modplatform/helpers/HashUtils.cpp +++ b/launcher/modplatform/helpers/HashUtils.cpp @@ -1,24 +1,22 @@ #include "HashUtils.h" +#include #include #include - -#include "FileSystem.h" -#include "StringUtils.h" +#include #include 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(file_path, + ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first()); case ModPlatform::ResourceProvider::FLAME: - return createFlameHasher(file_path); + return makeShared(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(file_path); + return makeShared(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(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(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(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; + 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(device); + auto result = QString::number(Murmur2::hash(reader.get(), 4 * MiB, should_filter_out)); + device->close(); + return result; + } + case Algorithm::Unknown: + device->close(); + return ""; } - auto hash_type = ProviderCaps.hashType(ModPlatform::ResourceProvider::MODRINTH).first(); - m_hash = ProviderCaps.hash(ModPlatform::ResourceProvider::MODRINTH, &file, hash_type); + QCryptographicHash hash(alg); + if (!hash.addData(device)) + qCritical() << "Failed to read JAR to create hash!"; - file.close(); - - if (m_hash.isEmpty()) { - emitFailed("Empty hash!"); - } else { - emitSucceeded(); - emit resultsReady(m_hash); - } + Q_ASSERT(hash.result().length() == hash.hashLength(alg)); + auto result = hash.result().toHex(); + device->close(); + return result; } -void FlameHasher::executeTask() +QString hash(QString fileName, Algorithm type) { - // CF-specific - auto should_filter_out = [](char c) { return (c == 9 || c == 10 || c == 13 || c == 32); }; - - 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)); - - if (m_hash.isEmpty()) { - emitFailed("Empty hash!"); - } else { - emitSucceeded(); - emit resultsReady(m_hash); - } + QFile file(fileName); + return hash(&file, type); } -BlockedModHasher::BlockedModHasher(QString file_path, ModPlatform::ResourceProvider provider) : Hasher(file_path), provider(provider) +QString hash(QByteArray data, Algorithm type) { - setObjectName(QString("BlockedModHasher: %1").arg(file_path)); - hash_type = ProviderCaps.hashType(provider).first(); + QBuffer buff(&data); + return hash(&buff, type); } -void BlockedModHasher::executeTask() +void Hasher::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); - } + m_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return hash(m_path, m_alg); }); + connect(&m_watcher, &QFutureWatcher::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_result); + } + }); + m_watcher.setFuture(m_future); } -QStringList BlockedModHasher::getHashTypes() +bool Hasher::abort() { - 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 diff --git a/launcher/modplatform/helpers/HashUtils.h b/launcher/modplatform/helpers/HashUtils.h index 73a2435a2..5d8b7d132 100644 --- a/launcher/modplatform/helpers/HashUtils.h +++ b/launcher/modplatform/helpers/HashUtils.h @@ -1,5 +1,8 @@ #pragma once +#include +#include +#include #include #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(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 m_future; + QFutureWatcher 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 diff --git a/launcher/modplatform/helpers/OverrideUtils.cpp b/launcher/modplatform/helpers/OverrideUtils.cpp index 65b5f7603..60983a5cf 100644 --- a/launcher/modplatform/helpers/OverrideUtils.cpp +++ b/launcher/modplatform/helpers/OverrideUtils.cpp @@ -10,7 +10,7 @@ void createOverrides(const QString& name, const QString& parent_folder, const QS { QString file_path(FS::PathCombine(parent_folder, name + ".txt")); if (QFile::exists(file_path)) - QFile::remove(file_path); + FS::deletePath(file_path); FS::ensureFilePathExists(file_path); diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.h b/launcher/modplatform/legacy_ftb/PackFetchTask.h index f2116ce99..e37d949d5 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.h +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.h @@ -13,7 +13,7 @@ class PackFetchTask : public QObject { Q_OBJECT public: - PackFetchTask(shared_qobject_ptr network) : QObject(nullptr), m_network(network){}; + PackFetchTask(shared_qobject_ptr network) : QObject(nullptr), m_network(network) {}; virtual ~PackFetchTask() = default; void fetch(); diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index 0048c7fac..7157f7f2d 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -137,7 +137,7 @@ void PackInstallTask::install() QDir unzipMcDir(m_stagingPath + "/unzip/minecraft"); if (unzipMcDir.exists()) { // ok, found minecraft dir, move contents to instance dir - if (!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/minecraft")) { + if (!FS::move(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/minecraft")) { emitFailed(tr("Failed to move unzipped Minecraft!")); return; } diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp index 9777c2cfd..4798ace84 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.cpp +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -120,3 +120,41 @@ QList 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 response) +{ + auto netJob = makeShared(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 ModrinthAPI::loadModCategories(std::shared_ptr response) +{ + QList 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; +}; \ No newline at end of file diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index d0f0811b2..d1f8f712a 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -30,6 +30,9 @@ class ModrinthAPI : public NetworkResourceAPI { Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; + static Task::Ptr getModCategories(std::shared_ptr response); + static QList loadModCategories(std::shared_ptr response); + public: [[nodiscard]] auto getSortingMethods() const -> QList 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(',')); diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index e78061f27..700bcd2e6 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -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) { @@ -51,7 +50,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); @@ -112,7 +111,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; } } diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index e92489999..c0806a638 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -173,7 +173,7 @@ bool ModrinthCreationTask::createInstance() // Keep index file in case we need it some other time (like when changing versions) QString new_index_place(FS::PathCombine(parent_folder, "modrinth.index.json")); FS::ensureFilePathExists(new_index_place); - QFile::rename(index_path, new_index_place); + FS::move(index_path, new_index_place); auto mcPath = FS::PathCombine(m_stagingPath, m_root_path); @@ -183,7 +183,7 @@ bool ModrinthCreationTask::createInstance() Override::createOverrides("overrides", parent_folder, override_path); // Apply the overrides - if (!QFile::rename(override_path, mcPath)) { + if (!FS::move(override_path, mcPath)) { setError(tr("Could not rename the overrides folder:\n") + "overrides"); return false; } @@ -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 diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp index 7e52153b9..ef0a3df16 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp @@ -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); diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 4671a330d..48b27a597 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 * * 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 unsortedVersions; - auto profile = (dynamic_cast(inst))->getPackProfile(); - QString mcVersion = profile->getComponentVersion("net.minecraft"); - auto loaders = profile->getSupportedModLoaders(); - for (auto versionIter : arr) { auto obj = versionIter.toObject(); 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(inst))->getPackProfile(); QString mcVersion = profile->getComponentVersion("net.minecraft"); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 7846e966d..f360df43a 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -131,6 +131,10 @@ auto loadIndexedVersion(QJsonObject& obj) -> ModpackVersion file.name = Json::requireString(obj, "name"); file.version = Json::requireString(obj, "version_number"); + auto gameVersions = Json::ensureArray(obj, "game_versions"); + if (!gameVersions.isEmpty()) { + file.gameVersion = Json::ensureString(gameVersions[0]); + } file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type")); file.changelog = Json::ensureString(obj, "changelog"); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 1ffd31d83..2bd61c5d9 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -84,6 +84,7 @@ struct ModpackExtra { struct ModpackVersion { QString name; QString version; + QString gameVersion; ModPlatform::IndexedVersionType version_type; QString changelog; diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index e35567f24..77a0935f3 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 * * 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"); diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index dce198b0e..95362bbfe 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 * * 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{}; diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp index a47a4811f..9050e14d8 100644 --- a/launcher/modplatform/technic/TechnicPackProcessor.cpp +++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp @@ -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; } diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h index d636f6634..ac64052b9 100644 --- a/launcher/net/ByteArraySink.h +++ b/launcher/net/ByteArraySink.h @@ -45,7 +45,7 @@ namespace Net { */ class ByteArraySink : public Sink { public: - ByteArraySink(std::shared_ptr output) : m_output(output){}; + ByteArraySink(std::shared_ptr output) : m_output(output) {}; virtual ~ByteArraySink() = default; diff --git a/launcher/net/ChecksumValidator.h b/launcher/net/ChecksumValidator.h index dfee0aee5..34ee5f856 100644 --- a/launcher/net/ChecksumValidator.h +++ b/launcher/net/ChecksumValidator.h @@ -44,7 +44,7 @@ namespace Net { class ChecksumValidator : public Validator { public: ChecksumValidator(QCryptographicHash::Algorithm algorithm, QByteArray expected = QByteArray()) - : m_checksum(algorithm), m_expected(expected){}; + : m_checksum(algorithm), m_expected(expected) {}; virtual ~ChecksumValidator() = default; public: diff --git a/launcher/net/FileSink.h b/launcher/net/FileSink.h index 40134b5f4..816254ff9 100644 --- a/launcher/net/FileSink.h +++ b/launcher/net/FileSink.h @@ -42,7 +42,7 @@ namespace Net { class FileSink : public Sink { public: - FileSink(QString filename) : m_filename(filename){}; + FileSink(QString filename) : m_filename(filename) {}; virtual ~FileSink() = default; public: diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp index 648155412..4985ad080 100644 --- a/launcher/net/HttpMetaCache.cpp +++ b/launcher/net/HttpMetaCache.cpp @@ -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); } } diff --git a/launcher/net/Logging.cpp b/launcher/net/Logging.cpp index a9b9db7cf..cd0c88d3c 100644 --- a/launcher/net/Logging.cpp +++ b/launcher/net/Logging.cpp @@ -22,5 +22,6 @@ Q_LOGGING_CATEGORY(taskNetLogC, "launcher.task.net") Q_LOGGING_CATEGORY(taskDownloadLogC, "launcher.task.net.download") Q_LOGGING_CATEGORY(taskUploadLogC, "launcher.task.net.upload") +Q_LOGGING_CATEGORY(taskMCSkinsLogC, "launcher.task.minecraft.skins") Q_LOGGING_CATEGORY(taskMetaCacheLogC, "launcher.task.net.metacache") Q_LOGGING_CATEGORY(taskHttpMetaCacheLogC, "launcher.task.net.metacache.http") diff --git a/launcher/net/Logging.h b/launcher/net/Logging.h index 4deed2b49..2536f31aa 100644 --- a/launcher/net/Logging.h +++ b/launcher/net/Logging.h @@ -24,5 +24,6 @@ Q_DECLARE_LOGGING_CATEGORY(taskNetLogC) Q_DECLARE_LOGGING_CATEGORY(taskDownloadLogC) Q_DECLARE_LOGGING_CATEGORY(taskUploadLogC) +Q_DECLARE_LOGGING_CATEGORY(taskMCSkinsLogC) Q_DECLARE_LOGGING_CATEGORY(taskMetaCacheLogC) Q_DECLARE_LOGGING_CATEGORY(taskHttpMetaCacheLogC) diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index 1ceb0a860..a331a9769 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -40,13 +40,18 @@ #include "tasks/ConcurrentTask.h" #if defined(LAUNCHER_APPLICATION) #include "Application.h" +#include "ui/dialogs/CustomMessageBox.h" #endif -NetJob::NetJob(QString job_name, shared_qobject_ptr network) : ConcurrentTask(nullptr, job_name), m_network(network) +NetJob::NetJob(QString job_name, shared_qobject_ptr network, int max_concurrent) + : ConcurrentTask(nullptr, job_name), m_network(network) { #if defined(LAUNCHER_APPLICATION) - setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + if (max_concurrent < 0) + max_concurrent = APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt(); #endif + if (max_concurrent > 0) + setMaxConcurrent(max_concurrent); } auto NetJob::addNetAction(Net::NetRequest::Ptr action) -> bool @@ -63,8 +68,11 @@ void NetJob::executeNextSubTask() // We're finished, check for failures and retry if we can (up to 3 times) if (isRunning() && m_queue.isEmpty() && m_doing.isEmpty() && !m_failed.isEmpty() && m_try < 3) { m_try += 1; - while (!m_failed.isEmpty()) - m_queue.enqueue(m_failed.take(*m_failed.keyBegin())); + while (!m_failed.isEmpty()) { + auto task = m_failed.take(*m_failed.keyBegin()); + m_done.remove(task.get()); + m_queue.enqueue(task); + } } ConcurrentTask::executeNextSubTask(); } @@ -136,3 +144,32 @@ void NetJob::updateState() setStatus(tr("Executing %1 task(s) (%2 out of %3 are done)") .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(totalSize()))); } + +void NetJob::emitFailed(QString reason) +{ +#if defined(LAUNCHER_APPLICATION) + if (m_ask_retry) { + auto response = CustomMessageBox::selectable(nullptr, "Confirm retry", + "The tasks failed.\n" + "Failed urls\n" + + getFailedFiles().join("\n\t") + + ".\n" + "If this continues to happen please check the logs of the application.\n" + "Do you want to retry?", + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response == QMessageBox::Yes) { + m_try = 0; + executeNextSubTask(); + return; + } + } +#endif + ConcurrentTask::emitFailed(reason); +} + +void NetJob::setAskRetry(bool askRetry) +{ + m_ask_retry = askRetry; +} \ No newline at end of file diff --git a/launcher/net/NetJob.h b/launcher/net/NetJob.h index 1661842f0..af09f03ba 100644 --- a/launcher/net/NetJob.h +++ b/launcher/net/NetJob.h @@ -52,7 +52,7 @@ class NetJob : public ConcurrentTask { public: using Ptr = shared_qobject_ptr; - explicit NetJob(QString job_name, shared_qobject_ptr network); + explicit NetJob(QString job_name, shared_qobject_ptr network, int max_concurrent = -1); ~NetJob() override = default; auto size() const -> int; @@ -62,10 +62,12 @@ class NetJob : public ConcurrentTask { auto getFailedActions() -> QList; auto getFailedFiles() -> QList; + void setAskRetry(bool askRetry); public slots: // Qt can't handle auto at the start for some reason? bool abort() override; + void emitFailed(QString reason) override; protected slots: void executeNextSubTask() override; @@ -77,4 +79,5 @@ class NetJob : public ConcurrentTask { shared_qobject_ptr m_network; int m_try = 1; + bool m_ask_retry = true; }; diff --git a/launcher/net/NetRequest.cpp b/launcher/net/NetRequest.cpp index abecc0cf3..36d3bdb25 100644 --- a/launcher/net/NetRequest.cpp +++ b/launcher/net/NetRequest.cpp @@ -5,6 +5,7 @@ * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * Copyright (c) 2023 Trial97 * * 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 diff --git a/launcher/net/NetRequest.h b/launcher/net/NetRequest.h index 6c32215b0..0cd6ceabc 100644 --- a/launcher/net/NetRequest.h +++ b/launcher/net/NetRequest.h @@ -4,6 +4,7 @@ * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * Copyright (c) 2023 Trial97 * * 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 @@ -74,6 +75,7 @@ class NetRequest : public Task { virtual void init() {} QUrl url() const; + void setUrl(QUrl url) { m_url = url; } int replyStatusCode() const; QNetworkReply::NetworkError error() const; QString errorString() const; diff --git a/launcher/net/StaticHeaderProxy.h b/launcher/net/StaticHeaderProxy.h index 8af7d203d..73678c026 100644 --- a/launcher/net/StaticHeaderProxy.h +++ b/launcher/net/StaticHeaderProxy.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * Copyright (c) 2023 Trial97 * * 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 @@ -25,7 +25,7 @@ namespace Net { class StaticHeaderProxy : public HeaderProxy { public: - StaticHeaderProxy(QList hdrs = {}) : HeaderProxy(), m_hdrs(hdrs){}; + StaticHeaderProxy(QList hdrs = {}) : HeaderProxy(), m_hdrs(hdrs) {}; virtual ~StaticHeaderProxy() = default; public: diff --git a/launcher/pathmatcher/FSTreeMatcher.h b/launcher/pathmatcher/FSTreeMatcher.h index 52f1404df..689f11979 100644 --- a/launcher/pathmatcher/FSTreeMatcher.h +++ b/launcher/pathmatcher/FSTreeMatcher.h @@ -6,7 +6,7 @@ class FSTreeMatcher : public IPathMatcher { public: - virtual ~FSTreeMatcher(){}; + virtual ~FSTreeMatcher() {}; FSTreeMatcher(SeparatorPrefixTree<'/'>& tree) : m_fsTree(tree) {} bool matches(const QString& string) const override { return m_fsTree.covers(string); } diff --git a/launcher/pathmatcher/MultiMatcher.h b/launcher/pathmatcher/MultiMatcher.h index 101595809..3e2bdb95d 100644 --- a/launcher/pathmatcher/MultiMatcher.h +++ b/launcher/pathmatcher/MultiMatcher.h @@ -4,7 +4,7 @@ class MultiMatcher : public IPathMatcher { public: - virtual ~MultiMatcher(){}; + virtual ~MultiMatcher() {}; MultiMatcher() {} MultiMatcher& add(Ptr add) { diff --git a/launcher/pathmatcher/SimplePrefixMatcher.h b/launcher/pathmatcher/SimplePrefixMatcher.h index fc1f5cede..ff3805179 100644 --- a/launcher/pathmatcher/SimplePrefixMatcher.h +++ b/launcher/pathmatcher/SimplePrefixMatcher.h @@ -7,7 +7,7 @@ class SimplePrefixMatcher : public IPathMatcher { public: - virtual ~SimplePrefixMatcher(){}; + virtual ~SimplePrefixMatcher() {}; SimplePrefixMatcher(const QString& prefix) { m_prefix = prefix; diff --git a/launcher/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp index 7e42ff40c..c63c8b39b 100644 --- a/launcher/screenshots/ImgurAlbumCreation.cpp +++ b/launcher/screenshots/ImgurAlbumCreation.cpp @@ -51,7 +51,7 @@ Net::NetRequest::Ptr ImgurAlbumCreation::make(std::shared_ptr output, QList screenshots) { auto up = makeShared(); - 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{ { "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); } diff --git a/launcher/screenshots/ImgurAlbumCreation.h b/launcher/screenshots/ImgurAlbumCreation.h index 7c292db73..ecb225394 100644 --- a/launcher/screenshots/ImgurAlbumCreation.h +++ b/launcher/screenshots/ImgurAlbumCreation.h @@ -49,7 +49,7 @@ class ImgurAlbumCreation : public Net::NetRequest { class Sink : public Net::Sink { public: - Sink(std::shared_ptr res) : m_result(res){}; + Sink(std::shared_ptr res) : m_result(res) {}; virtual ~Sink() = default; public: diff --git a/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp index 15fb043e4..941b92ce6 100644 --- a/launcher/screenshots/ImgurUpload.cpp +++ b/launcher/screenshots/ImgurUpload.cpp @@ -50,9 +50,8 @@ void ImgurUpload::init() { qDebug() << "Setting up imgur upload"; - auto api_headers = new Net::StaticHeaderProxy( - QList{ { "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str() }, - { "Accept", "application/json" } }); + auto api_headers = new Net::StaticHeaderProxy(QList{ + { "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(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; } diff --git a/launcher/screenshots/ImgurUpload.h b/launcher/screenshots/ImgurUpload.h index 5867ad306..5a58ad2b5 100644 --- a/launcher/screenshots/ImgurUpload.h +++ b/launcher/screenshots/ImgurUpload.h @@ -43,7 +43,7 @@ class ImgurUpload : public Net::NetRequest { public: class Sink : public Net::Sink { public: - Sink(ScreenShot::Ptr shot) : m_shot(shot){}; + Sink(ScreenShot::Ptr shot) : m_shot(shot) {}; virtual ~Sink() = default; public: diff --git a/launcher/tools/GenericProfiler.h b/launcher/tools/GenericProfiler.h index e99fc059f..7868990ea 100644 --- a/launcher/tools/GenericProfiler.h +++ b/launcher/tools/GenericProfiler.h @@ -22,7 +22,7 @@ class GenericProfilerFactory : public BaseProfilerFactory { public: QString name() const override { return "Generic"; } - void registerSettings([[maybe_unused]] SettingsObjectPtr settings) override{}; + void registerSettings([[maybe_unused]] SettingsObjectPtr settings) override {}; BaseExternalTool* createTool(InstancePtr instance, QObject* parent = 0) override; bool check([[maybe_unused]] QString* error) override { return true; }; bool check([[maybe_unused]] const QString& path, [[maybe_unused]] QString* error) override { return true; }; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 6bbb10532..83a352410 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -77,7 +77,6 @@ #include #include #include -#include #include #include #include @@ -96,7 +95,6 @@ #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ExportInstanceDialog.h" #include "ui/dialogs/ExportPackDialog.h" -#include "ui/dialogs/ExportToModListDialog.h" #include "ui/dialogs/IconPickerDialog.h" #include "ui/dialogs/ImportResourceDialog.h" #include "ui/dialogs/NewInstanceDialog.h" @@ -209,7 +207,6 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi exportInstanceMenu->addAction(ui->actionExportInstanceZip); exportInstanceMenu->addAction(ui->actionExportInstanceMrPack); exportInstanceMenu->addAction(ui->actionExportInstanceFlamePack); - exportInstanceMenu->addAction(ui->actionExportInstanceToModList); ui->actionExportInstance->setMenu(exportInstanceMenu); } @@ -871,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& extra_info) { QString groupName; @@ -1212,6 +1185,11 @@ void MainWindow::on_actionViewCentralModsFolder_triggered() DesktopServices::openPath(APPLICATION->settings()->get("CentralModsDir").toString(), true); } +void MainWindow::on_actionViewSkinsFolder_triggered() +{ + DesktopServices::openPath(APPLICATION->settings()->get("SkinsDir").toString(), true); +} + void MainWindow::on_actionViewIconThemeFolder_triggered() { DesktopServices::openPath(APPLICATION->themeManager()->getIconThemesFolder().path(), true); @@ -1416,14 +1394,6 @@ void MainWindow::on_actionExportInstanceMrPack_triggered() } } -void MainWindow::on_actionExportInstanceToModList_triggered() -{ - if (m_selectedInstance) { - ExportToModListDialog dlg(m_selectedInstance, this); - dlg.exec(); - } -} - void MainWindow::on_actionExportInstanceFlamePack_triggered() { if (m_selectedInstance) { @@ -1590,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"; diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 07a6e1eba..41bef9980 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -120,6 +120,8 @@ class MainWindow : public QMainWindow { void on_actionViewIconsFolder_triggered(); void on_actionViewLogsFolder_triggered(); + void on_actionViewSkinsFolder_triggered(); + void on_actionViewSelectedInstFolder_triggered(); void refreshInstances(); @@ -158,7 +160,6 @@ class MainWindow : public QMainWindow { void on_actionExportInstanceZip_triggered(); void on_actionExportInstanceMrPack_triggered(); void on_actionExportInstanceFlamePack_triggered(); - void on_actionExportInstanceToModList_triggered(); void on_actionRenameInstance_triggered(); @@ -228,7 +229,6 @@ class MainWindow : public QMainWindow { void runModalTask(Task* task); void instanceFromInstanceTask(InstanceTask* task); - void finalizeInstance(InstancePtr inst); private: Ui::MainWindow* ui; diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index 889012105..bad8762ad 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -131,7 +131,7 @@ 0 0 800 - 20 + 22 @@ -191,6 +191,7 @@ + @@ -491,15 +492,6 @@ CurseForge (zip) - - - - .. - - - Mod List - - @@ -587,6 +579,18 @@ Open the central mods folder in a file browser. + + + + .. + + + &Skins + + + Open the skins folder in a file browser. + + diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp index 17b79ecaa..b652ba991 100644 --- a/launcher/ui/dialogs/AboutDialog.cpp +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -38,6 +38,7 @@ #include "Application.h" #include "BuildConfig.h" #include "Markdown.h" +#include "StringUtils.h" #include "ui_AboutDialog.h" #include @@ -139,10 +140,10 @@ AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AboutDia setWindowTitle(tr("About %1").arg(launcherName)); QString chtml = getCreditsHtml(); - ui->creditsText->setHtml(chtml); + ui->creditsText->setHtml(StringUtils::htmlListPatch(chtml)); QString lhtml = getLicenseHtml(); - ui->licenseText->setHtml(lhtml); + ui->licenseText->setHtml(StringUtils::htmlListPatch(lhtml)); ui->urlLabel->setOpenExternalLinks(true); diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index 2b415c2d9..5c93053d1 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -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; diff --git a/launcher/ui/dialogs/ChooseProviderDialog.cpp b/launcher/ui/dialogs/ChooseProviderDialog.cpp index 83748e1e2..68457802d 100644 --- a/launcher/ui/dialogs/ChooseProviderDialog.cpp +++ b/launcher/ui/dialogs/ChooseProviderDialog.cpp @@ -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); } diff --git a/launcher/ui/dialogs/ExportPackDialog.cpp b/launcher/ui/dialogs/ExportPackDialog.cpp index 73e44efb1..0278c6cb0 100644 --- a/launcher/ui/dialogs/ExportPackDialog.cpp +++ b/launcher/ui/dialogs/ExportPackDialog.cpp @@ -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")) diff --git a/launcher/ui/dialogs/ExportToModListDialog.cpp b/launcher/ui/dialogs/ExportToModListDialog.cpp index a343f555a..1e0ae87a3 100644 --- a/launcher/ui/dialogs/ExportToModListDialog.cpp +++ b/launcher/ui/dialogs/ExportToModListDialog.cpp @@ -22,8 +22,7 @@ #include #include "FileSystem.h" #include "Markdown.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/mod/ModFolderModel.h" +#include "StringUtils.h" #include "modplatform/helpers/ExportToModList.h" #include "ui_ExportToModListDialog.h" @@ -41,38 +40,31 @@ const QHash ExportToModListDialog::exampleLin { ExportToModList::CSV, "{name},{url},{version},\"{authors}\"" }, }; -ExportToModListDialog::ExportToModListDialog(InstancePtr instance, QWidget* parent) - : QDialog(parent), m_template_changed(false), name(instance->name()), ui(new Ui::ExportToModListDialog) +ExportToModListDialog::ExportToModListDialog(QString name, QList mods, QWidget* parent) + : QDialog(parent), m_mods(mods), m_template_changed(false), m_name(name), ui(new Ui::ExportToModListDialog) { ui->setupUi(this); enableCustom(false); - MinecraftInstance* mcInstance = dynamic_cast(instance.get()); - if (mcInstance) { - mcInstance->loaderModList()->update(); - connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, [this, mcInstance]() { - m_allMods = mcInstance->loaderModList()->allMods(); - triggerImp(); - }); - } - connect(ui->formatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ExportToModListDialog::formatChanged); connect(ui->authorsCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->versionCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->urlCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); + connect(ui->filenameCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->authorsButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Authors); }); connect(ui->versionButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Version); }); connect(ui->urlButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Url); }); + connect(ui->filenameButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::FileName); }); connect(ui->templateText, &QTextEdit::textChanged, this, [this] { - if (ui->templateText->toPlainText() != exampleLines[format]) + if (ui->templateText->toPlainText() != exampleLines[m_format]) ui->formatComboBox->setCurrentIndex(5); - else - triggerImp(); + triggerImp(); }); connect(ui->copyButton, &QPushButton::clicked, this, [this](bool) { this->ui->finalText->selectAll(); this->ui->finalText->copy(); }); + triggerImp(); } ExportToModListDialog::~ExportToModListDialog() @@ -86,38 +78,38 @@ void ExportToModListDialog::formatChanged(int index) case 0: { enableCustom(false); ui->resultText->show(); - format = ExportToModList::HTML; + m_format = ExportToModList::HTML; break; } case 1: { enableCustom(false); ui->resultText->show(); - format = ExportToModList::MARKDOWN; + m_format = ExportToModList::MARKDOWN; break; } case 2: { enableCustom(false); ui->resultText->hide(); - format = ExportToModList::PLAINTXT; + m_format = ExportToModList::PLAINTXT; break; } case 3: { enableCustom(false); ui->resultText->hide(); - format = ExportToModList::JSON; + m_format = ExportToModList::JSON; break; } case 4: { enableCustom(false); ui->resultText->hide(); - format = ExportToModList::CSV; + m_format = ExportToModList::CSV; break; } case 5: { m_template_changed = true; enableCustom(true); ui->resultText->hide(); - format = ExportToModList::CUSTOM; + m_format = ExportToModList::CUSTOM; break; } } @@ -126,8 +118,8 @@ void ExportToModListDialog::formatChanged(int index) void ExportToModListDialog::triggerImp() { - if (format == ExportToModList::CUSTOM) { - ui->finalText->setPlainText(ExportToModList::exportToModList(m_allMods, ui->templateText->toPlainText())); + if (m_format == ExportToModList::CUSTOM) { + ui->finalText->setPlainText(ExportToModList::exportToModList(m_mods, ui->templateText->toPlainText())); return; } auto opt = 0; @@ -137,16 +129,18 @@ void ExportToModListDialog::triggerImp() opt |= ExportToModList::Version; if (ui->urlCheckBox->isChecked()) opt |= ExportToModList::Url; - auto txt = ExportToModList::exportToModList(m_allMods, format, static_cast(opt)); + if (ui->filenameCheckBox->isChecked()) + opt |= ExportToModList::FileName; + auto txt = ExportToModList::exportToModList(m_mods, m_format, static_cast(opt)); ui->finalText->setPlainText(txt); - switch (format) { + switch (m_format) { case ExportToModList::CUSTOM: return; case ExportToModList::HTML: - ui->resultText->setHtml(txt); + ui->resultText->setHtml(StringUtils::htmlListPatch(txt)); break; case ExportToModList::MARKDOWN: - ui->resultText->setHtml(markdownToHTML(txt)); + ui->resultText->setHtml(StringUtils::htmlListPatch(markdownToHTML(txt))); break; case ExportToModList::PLAINTXT: break; @@ -155,7 +149,7 @@ void ExportToModListDialog::triggerImp() case ExportToModList::CSV: break; } - auto exampleLine = exampleLines[format]; + auto exampleLine = exampleLines[m_format]; if (!m_template_changed && ui->templateText->toPlainText() != exampleLine) ui->templateText->setPlainText(exampleLine); } @@ -163,10 +157,10 @@ void ExportToModListDialog::triggerImp() void ExportToModListDialog::done(int result) { if (result == Accepted) { - const QString filename = FS::RemoveInvalidFilenameChars(name); + const QString filename = FS::RemoveInvalidFilenameChars(m_name); const QString output = - QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + extension()), - "File (*.txt *.html *.md *.json *.csv)", nullptr); + QFileDialog::getSaveFileName(this, tr("Export %1").arg(m_name), FS::PathCombine(QDir::homePath(), filename + extension()), + tr("File") + " (*.txt *.html *.md *.json *.csv)", nullptr); if (output.isEmpty()) return; @@ -178,7 +172,7 @@ void ExportToModListDialog::done(int result) QString ExportToModListDialog::extension() { - switch (format) { + switch (m_format) { case ExportToModList::HTML: return ".html"; case ExportToModList::MARKDOWN: @@ -197,7 +191,7 @@ QString ExportToModListDialog::extension() void ExportToModListDialog::addExtra(ExportToModList::OptionalData option) { - if (format != ExportToModList::CUSTOM) + if (m_format != ExportToModList::CUSTOM) return; switch (option) { case ExportToModList::Authors: @@ -209,6 +203,9 @@ void ExportToModListDialog::addExtra(ExportToModList::OptionalData option) case ExportToModList::Version: ui->templateText->insertPlainText("{version}"); break; + case ExportToModList::FileName: + ui->templateText->insertPlainText("{filename}"); + break; } } void ExportToModListDialog::enableCustom(bool enabled) @@ -221,4 +218,7 @@ void ExportToModListDialog::enableCustom(bool enabled) ui->urlCheckBox->setHidden(enabled); ui->urlButton->setHidden(!enabled); + + ui->filenameCheckBox->setHidden(enabled); + ui->filenameButton->setHidden(!enabled); } diff --git a/launcher/ui/dialogs/ExportToModListDialog.h b/launcher/ui/dialogs/ExportToModListDialog.h index 9886ae5a0..4ebe203f7 100644 --- a/launcher/ui/dialogs/ExportToModListDialog.h +++ b/launcher/ui/dialogs/ExportToModListDialog.h @@ -20,7 +20,6 @@ #include #include -#include "BaseInstance.h" #include "minecraft/mod/Mod.h" #include "modplatform/helpers/ExportToModList.h" @@ -32,7 +31,7 @@ class ExportToModListDialog : public QDialog { Q_OBJECT public: - explicit ExportToModListDialog(InstancePtr instance, QWidget* parent = nullptr); + explicit ExportToModListDialog(QString name, QList mods, QWidget* parent = nullptr); ~ExportToModListDialog(); void done(int result) override; @@ -46,10 +45,11 @@ class ExportToModListDialog : public QDialog { private: QString extension(); void enableCustom(bool enabled); - QList m_allMods; + + QList m_mods; bool m_template_changed; - QString name; - ExportToModList::Formats format = ExportToModList::Formats::HTML; + QString m_name; + ExportToModList::Formats m_format = ExportToModList::Formats::HTML; Ui::ExportToModListDialog* ui; static const QHash exampleLines; }; diff --git a/launcher/ui/dialogs/ExportToModListDialog.ui b/launcher/ui/dialogs/ExportToModListDialog.ui index 4f8ab52b5..3afda2fa8 100644 --- a/launcher/ui/dialogs/ExportToModListDialog.ui +++ b/launcher/ui/dialogs/ExportToModListDialog.ui @@ -117,6 +117,13 @@ + + + + Filename + + + @@ -138,6 +145,13 @@ + + + + Filename + + + diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index 54893d775..3d04aca6b 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -3,6 +3,7 @@ #include "CustomMessageBox.h" #include "ProgressDialog.h" #include "ScrollMessageBox.h" +#include "StringUtils.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "modplatform/ModIndex.h" #include "modplatform/flame/FlameAPI.h" @@ -24,8 +25,6 @@ #include -static ModPlatform::ProviderCapabilities ProviderCaps; - static std::list mcVersions(BaseInstance* inst) { return { static_cast(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() }; @@ -93,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); } @@ -426,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)); @@ -473,7 +470,7 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri break; } - changelog_area->setHtml(text); + changelog_area->setHtml(StringUtils::htmlListPatch(text)); changelog_area->setOpenExternalLinks(true); changelog_area->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth); changelog_area->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index 3524d43f8..2e799d2a8 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -52,6 +52,7 @@ #include #include #include +#include #include #include @@ -63,6 +64,7 @@ #include "ui/pages/modplatform/modrinth/ModrinthPage.h" #include "ui/pages/modplatform/technic/TechnicPage.h" #include "ui/widgets/PageContainer.h" + NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, const QString& url, const QMap& extra_info, @@ -127,7 +129,17 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, updateDialogState(); - restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toByteArray())); + if (APPLICATION->settings()->get("NewInstanceGeometry").isValid()) { + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toByteArray())); + } else { +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + auto screen = parent->screen(); +#else + auto screen = QGuiApplication::primaryScreen(); +#endif + auto geometry = screen->availableSize(); + resize(width(), qMin(geometry.height() - 50, 710)); + } } void NewInstanceDialog::reject() @@ -188,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; } @@ -204,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; } @@ -224,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; diff --git a/launcher/ui/dialogs/ProfileSelectDialog.cpp b/launcher/ui/dialogs/ProfileSelectDialog.cpp index a62238bdb..fe03e1b6b 100644 --- a/launcher/ui/dialogs/ProfileSelectDialog.cpp +++ b/launcher/ui/dialogs/ProfileSelectDialog.cpp @@ -20,7 +20,6 @@ #include #include "Application.h" -#include "SkinUtils.h" #include "ui/dialogs/ProgressDialog.h" diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index 6d28cea1f..082b16101 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -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 ShaderPackDownloadDialog::getPages() return pages; } +void ModDownloadDialog::setModMetadata(std::shared_ptr 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 diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h index a6efca138..7a0d6e895 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.h +++ b/launcher/ui/dialogs/ResourceDownloadDialog.h @@ -107,6 +107,8 @@ class ModDownloadDialog final : public ResourceDownloadDialog { QList getPages() override; GetModDependenciesTask::Ptr getModDependenciesTask() override; + void setModMetadata(std::shared_ptr); + private: BaseInstance* m_instance; }; diff --git a/launcher/ui/dialogs/SkinUploadDialog.cpp b/launcher/ui/dialogs/SkinUploadDialog.cpp deleted file mode 100644 index 5b3ebfa23..000000000 --- a/launcher/ui/dialogs/SkinUploadDialog.cpp +++ /dev/null @@ -1,164 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu - * - * 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 . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include -#include - -#include - -#include -#include -#include - -#include "CustomMessageBox.h" -#include "ProgressDialog.h" -#include "SkinUploadDialog.h" -#include "ui_SkinUploadDialog.h" - -void SkinUploadDialog::on_buttonBox_rejected() -{ - close(); -} - -void SkinUploadDialog::on_buttonBox_accepted() -{ - QString fileName; - QString input = ui->skinPathTextBox->text(); - ProgressDialog prog(this); - SequentialTask skinUpload; - - if (!input.isEmpty()) { - QRegularExpression urlPrefixMatcher(QRegularExpression::anchoredPattern("^([a-z]+)://.+$")); - bool isLocalFile = false; - // it has an URL prefix -> it is an URL - if (urlPrefixMatcher.match(input).hasMatch()) { - QUrl fileURL = input; - if (fileURL.isValid()) { - // local? - if (fileURL.isLocalFile()) { - isLocalFile = true; - fileName = fileURL.toLocalFile(); - } else { - CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Using remote URLs for setting skins is not implemented yet."), - QMessageBox::Warning) - ->exec(); - close(); - return; - } - } else { - CustomMessageBox::selectable(this, tr("Skin Upload"), tr("You cannot use an invalid URL for uploading skins."), - QMessageBox::Warning) - ->exec(); - close(); - return; - } - } else { - // just assume it's a path then - isLocalFile = true; - fileName = ui->skinPathTextBox->text(); - } - if (isLocalFile && !QFile::exists(fileName)) { - CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec(); - close(); - return; - } - SkinUpload::Model model = SkinUpload::STEVE; - if (ui->steveBtn->isChecked()) { - model = SkinUpload::STEVE; - } else if (ui->alexBtn->isChecked()) { - model = SkinUpload::ALEX; - } - skinUpload.addTask(shared_qobject_ptr(new SkinUpload(this, m_acct->accessToken(), FS::read(fileName), model))); - } - - auto selectedCape = ui->capeCombo->currentData().toString(); - if (selectedCape != m_acct->accountData()->minecraftProfile.currentCape) { - skinUpload.addTask(shared_qobject_ptr(new CapeChange(this, m_acct->accessToken(), selectedCape))); - } - if (prog.execWithTask(&skinUpload) != QDialog::Accepted) { - CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)->exec(); - close(); - return; - } - CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Success"), QMessageBox::Information)->exec(); - close(); -} - -void SkinUploadDialog::on_skinBrowseBtn_clicked() -{ - auto filter = QMimeDatabase().mimeTypeForName("image/png").filterString(); - QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), filter); - if (raw_path.isEmpty() || !QFileInfo::exists(raw_path)) { - return; - } - QString cooked_path = FS::NormalizePath(raw_path); - ui->skinPathTextBox->setText(cooked_path); -} - -SkinUploadDialog::SkinUploadDialog(MinecraftAccountPtr acct, QWidget* parent) : QDialog(parent), m_acct(acct), ui(new Ui::SkinUploadDialog) -{ - ui->setupUi(this); - - // FIXME: add a model for this, download/refresh the capes on demand - auto& accountData = *acct->accountData(); - int index = 0; - ui->capeCombo->addItem(tr("No Cape"), QVariant()); - auto currentCape = accountData.minecraftProfile.currentCape; - if (currentCape.isEmpty()) { - ui->capeCombo->setCurrentIndex(index); - } - - for (auto& cape : accountData.minecraftProfile.capes) { - index++; - if (cape.data.size()) { - QPixmap capeImage; - if (capeImage.loadFromData(cape.data, "PNG")) { - QPixmap preview = QPixmap(10, 16); - QPainter painter(&preview); - painter.drawPixmap(0, 0, capeImage.copy(1, 1, 10, 16)); - ui->capeCombo->addItem(capeImage, cape.alias, cape.id); - if (currentCape == cape.id) { - ui->capeCombo->setCurrentIndex(index); - } - continue; - } - } - ui->capeCombo->addItem(cape.alias, cape.id); - if (currentCape == cape.id) { - ui->capeCombo->setCurrentIndex(index); - } - } -} diff --git a/launcher/ui/dialogs/SkinUploadDialog.h b/launcher/ui/dialogs/SkinUploadDialog.h deleted file mode 100644 index 81d6140cc..000000000 --- a/launcher/ui/dialogs/SkinUploadDialog.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include -#include - -namespace Ui { -class SkinUploadDialog; -} - -class SkinUploadDialog : public QDialog { - Q_OBJECT - public: - explicit SkinUploadDialog(MinecraftAccountPtr acct, QWidget* parent = 0); - virtual ~SkinUploadDialog(){}; - - public slots: - void on_buttonBox_accepted(); - - void on_buttonBox_rejected(); - - void on_skinBrowseBtn_clicked(); - - protected: - MinecraftAccountPtr m_acct; - - private: - Ui::SkinUploadDialog* ui; -}; diff --git a/launcher/ui/dialogs/SkinUploadDialog.ui b/launcher/ui/dialogs/SkinUploadDialog.ui deleted file mode 100644 index c6df92df3..000000000 --- a/launcher/ui/dialogs/SkinUploadDialog.ui +++ /dev/null @@ -1,95 +0,0 @@ - - - SkinUploadDialog - - - - 0 - 0 - 394 - 360 - - - - Skin Upload - - - - - - Skin File - - - - - - Leave empty to keep current skin - - - - - - - - 0 - 0 - - - - Browse - - - - - - - - - - Player Model - - - - - - Steve Model - - - true - - - - - - - Alex Model - - - - - - - - - - Cape - - - - - - - - - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - diff --git a/launcher/ui/dialogs/UpdateAvailableDialog.cpp b/launcher/ui/dialogs/UpdateAvailableDialog.cpp index 5eebe87a3..810a1f089 100644 --- a/launcher/ui/dialogs/UpdateAvailableDialog.cpp +++ b/launcher/ui/dialogs/UpdateAvailableDialog.cpp @@ -25,6 +25,7 @@ #include "Application.h" #include "BuildConfig.h" #include "Markdown.h" +#include "StringUtils.h" #include "ui_UpdateAvailableDialog.h" UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion, @@ -43,7 +44,7 @@ UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion, ui->icon->setPixmap(APPLICATION->getThemedIcon("checkupdate").pixmap(64)); auto releaseNotesHtml = markdownToHTML(releaseNotes); - ui->releaseNotes->setHtml(releaseNotesHtml); + ui->releaseNotes->setHtml(StringUtils::htmlListPatch(releaseNotesHtml)); ui->releaseNotes->setOpenExternalLinks(true); connect(ui->skipButton, &QPushButton::clicked, this, [this]() { diff --git a/launcher/ui/dialogs/VersionSelectDialog.h b/launcher/ui/dialogs/VersionSelectDialog.h index 0ccd45e74..34c5e66f8 100644 --- a/launcher/ui/dialogs/VersionSelectDialog.h +++ b/launcher/ui/dialogs/VersionSelectDialog.h @@ -37,7 +37,7 @@ class VersionSelectDialog : public QDialog { public: explicit VersionSelectDialog(BaseVersionList* vlist, QString title, QWidget* parent = 0, bool cancelable = true); - virtual ~VersionSelectDialog(){}; + virtual ~VersionSelectDialog() {}; int exec() override; diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp new file mode 100644 index 000000000..a947af632 --- /dev/null +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -0,0 +1,512 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#include "SkinManageDialog.h" +#include "ui_SkinManageDialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Application.h" +#include "DesktopServices.h" +#include "Json.h" +#include "QObjectPtr.h" + +#include "minecraft/auth/Parsers.h" +#include "minecraft/skins/CapeChange.h" +#include "minecraft/skins/SkinDelete.h" +#include "minecraft/skins/SkinList.h" +#include "minecraft/skins/SkinModel.h" +#include "minecraft/skins/SkinUpload.h" + +#include "net/Download.h" +#include "net/NetJob.h" +#include "tasks/Task.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" +#include "ui/instanceview/InstanceDelegate.h" + +SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct) + : QDialog(parent), m_acct(acct), ui(new Ui::SkinManageDialog), m_list(this, APPLICATION->settings()->get("SkinsDir").toString(), acct) +{ + ui->setupUi(this); + + setWindowModality(Qt::WindowModal); + + auto contentsWidget = ui->listView; + contentsWidget->setViewMode(QListView::IconMode); + contentsWidget->setFlow(QListView::LeftToRight); + contentsWidget->setIconSize(QSize(48, 48)); + contentsWidget->setMovement(QListView::Static); + contentsWidget->setResizeMode(QListView::Adjust); + contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection); + contentsWidget->setSpacing(5); + contentsWidget->setWordWrap(false); + contentsWidget->setWrapping(true); + contentsWidget->setUniformItemSizes(true); + contentsWidget->setTextElideMode(Qt::ElideRight); + contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + contentsWidget->installEventFilter(this); + contentsWidget->setItemDelegate(new ListViewDelegate(this)); + + contentsWidget->setAcceptDrops(true); + contentsWidget->setDropIndicatorShown(true); + contentsWidget->viewport()->setAcceptDrops(true); + contentsWidget->setDragDropMode(QAbstractItemView::DropOnly); + contentsWidget->setDefaultDropAction(Qt::CopyAction); + + contentsWidget->installEventFilter(this); + contentsWidget->setModel(&m_list); + + connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), SLOT(activated(QModelIndex))); + + connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), + SLOT(selectionChanged(QItemSelection, QItemSelection))); + connect(ui->listView, &QListView::customContextMenuRequested, this, &SkinManageDialog::show_context_menu); + + setupCapes(); + + ui->listView->setCurrentIndex(m_list.index(m_list.getSelectedAccountSkin())); +} + +SkinManageDialog::~SkinManageDialog() +{ + delete ui; +} + +void SkinManageDialog::activated(QModelIndex index) +{ + m_selected_skin = index.data(Qt::UserRole).toString(); + accept(); +} + +void SkinManageDialog::selectionChanged(QItemSelection selected, QItemSelection deselected) +{ + if (selected.empty()) + return; + + QString key = selected.first().indexes().first().data(Qt::UserRole).toString(); + if (key.isEmpty()) + return; + m_selected_skin = key; + auto skin = m_list.skin(key); + if (!skin) + return; + 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); +} + +void SkinManageDialog::delayed_scroll(QModelIndex model_index) +{ + auto contentsWidget = ui->listView; + contentsWidget->scrollTo(model_index); +} + +void SkinManageDialog::on_openDirBtn_clicked() +{ + DesktopServices::openPath(m_list.getDir(), true); +} + +void SkinManageDialog::on_fileBtn_clicked() +{ + auto filter = QMimeDatabase().mimeTypeForName("image/png").filterString(); + QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), filter); + auto message = m_list.installSkin(raw_path, {}); + if (!message.isEmpty()) { + CustomMessageBox::selectable(this, tr("Selected file is not a valid skin"), message, QMessageBox::Critical)->show(); + return; + } +} + +QPixmap previewCape(QPixmap capeImage) +{ + QPixmap preview = QPixmap(10, 16); + QPainter painter(&preview); + painter.drawPixmap(0, 0, capeImage.copy(1, 1, 10, 16)); + return preview.scaled(80, 128, Qt::IgnoreAspectRatio, Qt::FastTransformation); +} + +void SkinManageDialog::setupCapes() +{ + // FIXME: add a model for this, download/refresh the capes on demand + auto& accountData = *m_acct->accountData(); + int index = 0; + ui->capeCombo->addItem(tr("No Cape"), QVariant()); + auto currentCape = accountData.minecraftProfile.currentCape; + if (currentCape.isEmpty()) { + ui->capeCombo->setCurrentIndex(index); + } + + auto capesDir = FS::PathCombine(m_list.getDir(), "capes"); + NetJob::Ptr job{ new NetJob(tr("Download capes"), APPLICATION->network()) }; + bool needsToDownload = false; + for (auto& cape : accountData.minecraftProfile.capes) { + auto path = FS::PathCombine(capesDir, cape.id + ".png"); + if (cape.data.size()) { + QPixmap capeImage; + if (capeImage.loadFromData(cape.data, "PNG") && capeImage.save(path)) { + m_capes[cape.id] = previewCape(capeImage); + continue; + } + } + if (QFileInfo(path).exists()) { + continue; + } + if (!cape.url.isEmpty()) { + needsToDownload = true; + job->addNetAction(Net::Download::makeFile(cape.url, path)); + } + } + if (needsToDownload) { + ProgressDialog dlg(this); + dlg.execWithTask(job.get()); + } + for (auto& cape : accountData.minecraftProfile.capes) { + index++; + QPixmap capeImage; + if (!m_capes.contains(cape.id)) { + auto path = FS::PathCombine(capesDir, cape.id + ".png"); + if (QFileInfo(path).exists() && capeImage.load(path)) { + capeImage = previewCape(capeImage); + m_capes[cape.id] = capeImage; + } + } + if (!capeImage.isNull()) { + ui->capeCombo->addItem(capeImage, cape.alias, cape.id); + } else { + ui->capeCombo->addItem(cape.alias, cape.id); + } + + m_capes_idx[cape.id] = index; + } +} + +void SkinManageDialog::on_capeCombo_currentIndexChanged(int index) +{ + auto id = ui->capeCombo->currentData(); + 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()); + } +} + +void SkinManageDialog::on_steveBtn_toggled(bool checked) +{ + if (auto skin = m_list.skin(m_selected_skin); skin) { + skin->setModel(checked ? SkinModel::CLASSIC : SkinModel::SLIM); + } +} + +void SkinManageDialog::accept() +{ + auto skin = m_list.skin(m_selected_skin); + if (!skin) { + reject(); + return; + } + auto path = skin->getPath(); + + ProgressDialog prog(this); + NetJob::Ptr skinUpload{ new NetJob(tr("Change skin"), APPLICATION->network(), 1) }; + + if (!QFile::exists(path)) { + CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec(); + reject(); + return; + } + + skinUpload->addNetAction(SkinUpload::make(m_acct->accessToken(), skin->getPath(), skin->getModelString())); + + auto selectedCape = skin->getCapeId(); + if (selectedCape != m_acct->accountData()->minecraftProfile.currentCape) { + skinUpload->addNetAction(CapeChange::make(m_acct->accessToken(), selectedCape)); + } + + skinUpload->addTask(m_acct->refresh().staticCast()); + if (prog.execWithTask(skinUpload.get()) != QDialog::Accepted) { + CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)->exec(); + reject(); + return; + } + skin->setURL(m_acct->accountData()->minecraftProfile.skin.url); + QDialog::accept(); +} + +void SkinManageDialog::on_resetBtn_clicked() +{ + ProgressDialog prog(this); + NetJob::Ptr skinReset{ new NetJob(tr("Reset skin"), APPLICATION->network(), 1) }; + skinReset->addNetAction(SkinDelete::make(m_acct->accessToken())); + skinReset->addTask(m_acct->refresh().staticCast()); + if (prog.execWithTask(skinReset.get()) != QDialog::Accepted) { + CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to delete current skin!"), QMessageBox::Warning)->exec(); + reject(); + return; + } + QDialog::accept(); +} + +void SkinManageDialog::show_context_menu(const QPoint& pos) +{ + QMenu myMenu(tr("Context menu"), this); + myMenu.addAction(ui->action_Rename_Skin); + myMenu.addAction(ui->action_Delete_Skin); + + myMenu.exec(ui->listView->mapToGlobal(pos)); +} + +bool SkinManageDialog::eventFilter(QObject* obj, QEvent* ev) +{ + if (obj == ui->listView) { + if (ev->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(ev); + switch (keyEvent->key()) { + case Qt::Key_Delete: + on_action_Delete_Skin_triggered(false); + return true; + case Qt::Key_F2: + on_action_Rename_Skin_triggered(false); + return true; + default: + break; + } + } + } + return QDialog::eventFilter(obj, ev); +} + +void SkinManageDialog::on_action_Rename_Skin_triggered(bool checked) +{ + if (!m_selected_skin.isEmpty()) { + ui->listView->edit(ui->listView->currentIndex()); + } +} + +void SkinManageDialog::on_action_Delete_Skin_triggered(bool checked) +{ + if (m_selected_skin.isEmpty()) + return; + + if (m_list.getSkinIndex(m_selected_skin) == m_list.getSelectedAccountSkin()) { + CustomMessageBox::selectable(this, tr("Delete error"), tr("Can not delete skin that is in use."), QMessageBox::Warning)->exec(); + return; + } + + auto skin = m_list.skin(m_selected_skin); + if (!skin) + return; + + auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"), + tr("You are about to delete \"%1\".\n" + "Are you sure?") + .arg(skin->name()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response == QMessageBox::Yes) { + if (!m_list.deleteSkin(m_selected_skin, true)) { + m_list.deleteSkin(m_selected_skin, false); + } + } +} + +void SkinManageDialog::on_urlBtn_clicked() +{ + auto url = QUrl(ui->urlLine->text()); + if (!url.isValid()) { + CustomMessageBox::selectable(this, tr("Invalid url"), tr("Invalid url"), QMessageBox::Critical)->show(); + return; + } + + NetJob::Ptr job{ new NetJob(tr("Download skin"), APPLICATION->network()) }; + job->setAskRetry(false); + + auto path = FS::PathCombine(m_list.getDir(), url.fileName()); + job->addNetAction(Net::Download::makeFile(url, path)); + ProgressDialog dlg(this); + dlg.execWithTask(job.get()); + SkinModel s(path); + if (!s.isValid()) { + CustomMessageBox::selectable(this, tr("URL is not a valid skin"), + QFileInfo::exists(path) ? tr("Skin images must be 64x64 or 64x32 pixel PNG files.") + : tr("Unable to download the skin: '%1'.").arg(ui->urlLine->text()), + QMessageBox::Critical) + ->show(); + QFile::remove(path); + return; + } + ui->urlLine->setText(""); + if (QFileInfo(path).suffix().isEmpty()) { + QFile::rename(path, path + ".png"); + } +} + +class WaitTask : public Task { + public: + WaitTask() : m_loop(), m_done(false) {}; + virtual ~WaitTask() = default; + + public slots: + void quit() + { + m_done = true; + m_loop.quit(); + } + + protected: + virtual void executeTask() + { + if (!m_done) + m_loop.exec(); + emitSucceeded(); + }; + + private: + QEventLoop m_loop; + bool m_done; +}; + +void SkinManageDialog::on_userBtn_clicked() +{ + auto user = ui->urlLine->text(); + if (user.isEmpty()) { + return; + } + MinecraftProfile mcProfile; + auto path = FS::PathCombine(m_list.getDir(), user + ".png"); + + NetJob::Ptr job{ new NetJob(tr("Download user skin"), APPLICATION->network(), 1) }; + job->setAskRetry(false); + + auto uuidOut = std::make_shared(); + auto profileOut = std::make_shared(); + + auto uuidLoop = makeShared(); + auto profileLoop = makeShared(); + + auto getUUID = Net::Download::makeByteArray("https://api.mojang.com/users/profiles/minecraft/" + user, uuidOut); + auto getProfile = Net::Download::makeByteArray(QUrl(), profileOut); + auto downloadSkin = Net::Download::makeFile(QUrl(), path); + + QString failReason; + + connect(getUUID.get(), &Task::aborted, uuidLoop.get(), &WaitTask::quit); + connect(getUUID.get(), &Task::failed, this, [&failReason](QString reason) { + qCritical() << "Couldn't get user UUID:" << reason; + failReason = tr("failed to get user UUID"); + }); + connect(getUUID.get(), &Task::failed, uuidLoop.get(), &WaitTask::quit); + connect(getProfile.get(), &Task::aborted, profileLoop.get(), &WaitTask::quit); + connect(getProfile.get(), &Task::failed, profileLoop.get(), &WaitTask::quit); + connect(getProfile.get(), &Task::failed, this, [&failReason](QString reason) { + qCritical() << "Couldn't get user profile:" << reason; + failReason = tr("failed to get user profile"); + }); + connect(downloadSkin.get(), &Task::failed, this, [&failReason](QString reason) { + qCritical() << "Couldn't download skin:" << reason; + failReason = tr("failed to download skin"); + }); + + connect(getUUID.get(), &Task::succeeded, this, [uuidLoop, uuidOut, job, getProfile, &failReason] { + try { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*uuidOut, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Minecraft skin service at " << parse_error.offset + << " reason: " << parse_error.errorString(); + failReason = tr("failed to parse get user UUID response"); + uuidLoop->quit(); + return; + } + const auto root = doc.object(); + auto id = Json::ensureString(root, "id"); + if (!id.isEmpty()) { + getProfile->setUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + id); + } else { + failReason = tr("user id is empty"); + job->abort(); + } + } catch (const Exception& e) { + qCritical() << "Couldn't load skin json:" << e.cause(); + failReason = tr("failed to parse get user UUID response"); + } + uuidLoop->quit(); + }); + + connect(getProfile.get(), &Task::succeeded, this, [profileLoop, profileOut, job, getProfile, &mcProfile, downloadSkin, &failReason] { + if (Parsers::parseMinecraftProfileMojang(*profileOut, mcProfile)) { + downloadSkin->setUrl(mcProfile.skin.url); + } else { + failReason = tr("failed to parse get user profile response"); + job->abort(); + } + profileLoop->quit(); + }); + + job->addNetAction(getUUID); + job->addTask(uuidLoop); + job->addNetAction(getProfile); + job->addTask(profileLoop); + job->addNetAction(downloadSkin); + ProgressDialog dlg(this); + dlg.execWithTask(job.get()); + + SkinModel s(path); + if (!s.isValid()) { + if (failReason.isEmpty()) { + failReason = tr("the skin is invalid"); + } + 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); + return; + } + ui->urlLine->setText(""); + s.setModel(mcProfile.skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); + s.setURL(mcProfile.skin.url); + if (m_capes.contains(mcProfile.currentCape)) { + s.setCapeId(mcProfile.currentCape); + } + 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)); +} diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.h b/launcher/ui/dialogs/skins/SkinManageDialog.h new file mode 100644 index 000000000..cdb37a513 --- /dev/null +++ b/launcher/ui/dialogs/skins/SkinManageDialog.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#pragma once + +#include +#include +#include + +#include "minecraft/auth/MinecraftAccount.h" +#include "minecraft/skins/SkinList.h" + +namespace Ui { +class SkinManageDialog; +} + +class SkinManageDialog : public QDialog { + Q_OBJECT + public: + explicit SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct); + virtual ~SkinManageDialog(); + void resizeEvent(QResizeEvent* event) override; + + public slots: + void selectionChanged(QItemSelection, QItemSelection); + void activated(QModelIndex); + void delayed_scroll(QModelIndex); + void on_openDirBtn_clicked(); + void on_fileBtn_clicked(); + void on_urlBtn_clicked(); + void on_userBtn_clicked(); + void accept() override; + void on_capeCombo_currentIndexChanged(int index); + void on_steveBtn_toggled(bool checked); + void on_resetBtn_clicked(); + void show_context_menu(const QPoint& pos); + bool eventFilter(QObject* obj, QEvent* ev) override; + void on_action_Rename_Skin_triggered(bool checked); + void on_action_Delete_Skin_triggered(bool checked); + + private: + void setupCapes(); + + MinecraftAccountPtr m_acct; + Ui::SkinManageDialog* ui; + SkinList m_list; + QString m_selected_skin; + QHash m_capes; + QHash m_capes_idx; +}; diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.ui b/launcher/ui/dialogs/skins/SkinManageDialog.ui new file mode 100644 index 000000000..c77eeaaa3 --- /dev/null +++ b/launcher/ui/dialogs/skins/SkinManageDialog.ui @@ -0,0 +1,226 @@ + + + SkinManageDialog + + + + 0 + 0 + 968 + 757 + + + + Skin Upload + + + + + + + + + + + + + false + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + Model + + + + + + Classic + + + true + + + + + + + Slim + + + + + + + + + + Cape + + + + + + + + + + + + false + + + Qt::AlignCenter + + + + + + + + + + + + Qt::CustomContextMenu + + + false + + + 0 + + + + + + + + + + + Open Folder + + + + + + + Reset Skin + + + + + + + + + + + + + + Import URL + + + + + + + Import user + + + + + + + Import File + + + + + + + + 0 + 0 + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + &Delete Skin + + + Deletes selected skin + + + Del + + + + + &Rename Skin + + + Rename selected skin + + + F2 + + + + + + + buttonBox + rejected() + SkinManageDialog + reject() + + + 617 + 736 + + + 483 + 378 + + + + + buttonBox + accepted() + SkinManageDialog + accept() + + + 617 + 736 + + + 483 + 378 + + + + + diff --git a/launcher/ui/instanceview/VisualGroup.cpp b/launcher/ui/instanceview/VisualGroup.cpp index 7bff727fe..83103c502 100644 --- a/launcher/ui/instanceview/VisualGroup.cpp +++ b/launcher/ui/instanceview/VisualGroup.cpp @@ -66,6 +66,9 @@ void VisualGroup::update() rows[currentRow].height = maxRowHeight; rows[currentRow].top = offsetFromTop; currentRow++; + if (currentRow >= rows.size()) { + currentRow = rows.size() - 1; + } offsetFromTop += maxRowHeight + 5; positionInRow = 0; maxRowHeight = 0; diff --git a/launcher/ui/pages/BasePageContainer.h b/launcher/ui/pages/BasePageContainer.h index a497ef7b3..671c2735d 100644 --- a/launcher/ui/pages/BasePageContainer.h +++ b/launcher/ui/pages/BasePageContainer.h @@ -4,7 +4,7 @@ class BasePage; class BasePageContainer { public: - virtual ~BasePageContainer(){}; + virtual ~BasePageContainer() {}; virtual bool selectPage(QString pageId) = 0; virtual BasePage* selectedPage() const = 0; virtual BasePage* getPage(QString pageId) { return nullptr; }; diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 6f2b0bd47..8d0b66004 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -35,7 +35,7 @@ */ #include "AccountListPage.h" -#include "minecraft/auth/AccountData.h" +#include "ui/dialogs/skins/SkinManageDialog.h" #include "ui_AccountListPage.h" #include @@ -47,11 +47,6 @@ #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/MSALoginDialog.h" #include "ui/dialogs/OfflineLoginDialog.h" -#include "ui/dialogs/ProgressDialog.h" -#include "ui/dialogs/SkinUploadDialog.h" - -#include "minecraft/services/SkinDelete.h" -#include "tasks/Task.h" #include "Application.h" @@ -233,8 +228,7 @@ void AccountListPage::updateButtonStates() } ui->actionRemove->setEnabled(accountIsReady); ui->actionSetDefault->setEnabled(accountIsReady); - ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline); - ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline); + ui->actionManageSkins->setEnabled(accountIsReady && accountIsOnline); ui->actionRefresh->setEnabled(accountIsReady && accountIsOnline); if (m_accounts->defaultAccount().get() == nullptr) { @@ -247,29 +241,13 @@ void AccountListPage::updateButtonStates() ui->listView->resizeColumnToContents(3); } -void AccountListPage::on_actionUploadSkin_triggered() +void AccountListPage::on_actionManageSkins_triggered() { QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); if (selection.size() > 0) { QModelIndex selected = selection.first(); MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); - SkinUploadDialog dialog(account, this); + SkinManageDialog dialog(this, account); dialog.exec(); } } - -void AccountListPage::on_actionDeleteSkin_triggered() -{ - QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); - if (selection.size() <= 0) - return; - - QModelIndex selected = selection.first(); - MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); - ProgressDialog prog(this); - auto deleteSkinTask = std::make_shared(this, account->accessToken()); - if (prog.execWithTask((Task*)deleteSkinTask.get()) != QDialog::Accepted) { - CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to delete current skin!"), QMessageBox::Warning)->exec(); - return; - } -} diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h index f3b80191d..dd49a95d6 100644 --- a/launcher/ui/pages/global/AccountListPage.h +++ b/launcher/ui/pages/global/AccountListPage.h @@ -76,8 +76,7 @@ class AccountListPage : public QMainWindow, public BasePage { void on_actionRefresh_triggered(); void on_actionSetDefault_triggered(); void on_actionNoDefault_triggered(); - void on_actionUploadSkin_triggered(); - void on_actionDeleteSkin_triggered(); + void on_actionManageSkins_triggered(); void listChanged(); diff --git a/launcher/ui/pages/global/AccountListPage.ui b/launcher/ui/pages/global/AccountListPage.ui index d8cf3ac0a..c9b770ab2 100644 --- a/launcher/ui/pages/global/AccountListPage.ui +++ b/launcher/ui/pages/global/AccountListPage.ui @@ -59,14 +59,8 @@ - - + - - - Remo&ve - - &Set Default @@ -80,17 +74,12 @@ &No Default - + - &Upload Skin - - - - - &Delete Skin + &Manage Skins - Delete the currently active skin and go back to the default one + Manage Skins @@ -111,6 +100,11 @@ Refresh the account tokens + + + Remo&ve + + diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 78c44380a..04e27fd76 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -173,6 +173,17 @@ void LauncherPage::on_downloadsDirBrowseBtn_clicked() } } +void LauncherPage::on_skinsDirBrowseBtn_clicked() +{ + QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Skins Folder"), ui->skinsDirTextBox->text()); + + // do not allow current dir - it's dirty. Do not allow dirs that don't exist + if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) { + QString cooked_dir = FS::NormalizePath(raw_dir); + ui->skinsDirTextBox->setText(cooked_dir); + } +} + void LauncherPage::on_metadataDisableBtn_clicked() { ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); @@ -185,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()); @@ -208,6 +220,7 @@ void LauncherPage::applySettings() s->set("CentralModsDir", ui->modsDirTextBox->text()); s->set("IconsDir", ui->iconsDirTextBox->text()); s->set("DownloadsDir", ui->downloadsDirTextBox->text()); + s->set("SkinsDir", ui->skinsDirTextBox->text()); s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked()); auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId(); @@ -234,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) @@ -269,6 +283,7 @@ void LauncherPage::loadSettings() ui->modsDirTextBox->setText(s->get("CentralModsDir").toString()); ui->iconsDirTextBox->setText(s->get("IconsDir").toString()); ui->downloadsDirTextBox->setText(s->get("DownloadsDir").toString()); + ui->skinsDirTextBox->setText(s->get("SkinsDir").toString()); ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool()); QString sortMode = s->get("InstSortMode").toString(); diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index e733224d2..f9aefb171 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -74,6 +74,7 @@ class LauncherPage : public QWidget, public BasePage { void on_modsDirBrowseBtn_clicked(); void on_iconsDirBrowseBtn_clicked(); void on_downloadsDirBrowseBtn_clicked(); + void on_skinsDirBrowseBtn_clicked(); void on_metadataDisableBtn_clicked(); /*! diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 928ec8103..2d96c3a84 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -58,6 +58,33 @@ + + + + + + Update interval + + + + + + + Set it to 0 to only check on launch + + + h + + + 0 + + + 99999999 + + + + + @@ -67,7 +94,7 @@ Folders - + &Downloads: @@ -90,13 +117,16 @@ - + - + + + + Browse @@ -147,7 +177,24 @@ - + + + + Browse + + + + + + + &Skins: + + + skinsDirTextBox + + + + When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge). diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui index ff08e12d2..9d6f61db0 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -70,15 +70,15 @@ - - true - Actions Qt::ToolButtonTextOnly + + true + RightToolBarArea @@ -171,6 +171,17 @@ Try to check or update all selected resources (all resources if none are selected) + + + true + + + Export modlist + + + Export mod's metadata to text + + diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index 2210d0263..a909d10d1 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -20,6 +20,7 @@ #include "InstanceTask.h" #include "Json.h" #include "Markdown.h" +#include "StringUtils.h" #include "modplatform/modrinth/ModrinthPackManifest.h" @@ -332,7 +333,7 @@ void ModrinthManagedPackPage::suggestVersion() } auto version = m_pack.versions.at(index); - ui->changelogTextBrowser->setHtml(markdownToHTML(version.changelog.toUtf8())); + ui->changelogTextBrowser->setHtml(StringUtils::htmlListPatch(markdownToHTML(version.changelog.toUtf8()))); ManagedPackPage::suggestVersion(); } @@ -370,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 extra_info; @@ -420,7 +421,7 @@ void FlameManagedPackPage::parseManagedPack() "Don't worry though, it will ask you to update this instance instead, so you'll not lose this instance!" ""); - ui->changelogTextBrowser->setHtml(message); + ui->changelogTextBrowser->setHtml(StringUtils::htmlListPatch(message)); return; } @@ -502,7 +503,8 @@ void FlameManagedPackPage::suggestVersion() } auto version = m_pack.versions.at(index); - ui->changelogTextBrowser->setHtml(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId)); + ui->changelogTextBrowser->setHtml( + StringUtils::htmlListPatch(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId))); ManagedPackPage::suggestVersion(); } @@ -536,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; diff --git a/launcher/ui/pages/instance/ManagedPackPage.h b/launcher/ui/pages/instance/ManagedPackPage.h index d77cb97b8..fe5a200de 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.h +++ b/launcher/ui/pages/instance/ManagedPackPage.h @@ -50,7 +50,7 @@ class ManagedPackPage : public QWidget, public BasePage { /** Gets the necessary information about the managed pack, such as * available versions*/ - virtual void parseManagedPack(){}; + virtual void parseManagedPack() {}; /** URL of the managed pack. * Not the version-specific one. @@ -64,8 +64,8 @@ class ManagedPackPage : public QWidget, public BasePage { */ virtual void suggestVersion(); - virtual void update(){}; - virtual void updateFromFile(){}; + virtual void update() {}; + virtual void updateFromFile() {}; protected slots: /** Does the necessary UI changes for when something failed. diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 4b96338ae..4cf4b5bb0 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -37,9 +37,11 @@ */ #include "ModFolderPage.h" +#include "ui/dialogs/ExportToModListDialog.h" #include "ui_ExternalResourcesPage.h" #include +#include #include #include #include @@ -122,10 +124,19 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr 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(); @@ -135,11 +146,12 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr if (selected <= 1) { ui->actionVisitItemPage->setText(tr("Visit mod's page")); ui->actionVisitItemPage->setToolTip(tr("Go to mod's home page")); - } 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); }); @@ -381,3 +393,66 @@ 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(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(); + auto selectedMods = m_model->selectedMods(selection); + if (selectedMods.length() == 0) + selectedMods = m_model->allMods(); + + std::sort(selectedMods.begin(), selectedMods.end(), [](const Mod* a, const Mod* b) { return a->name() < b->name(); }); + ExportToModListDialog dlg(m_instance->name(), selectedMods, this); + dlg.exec(); +} diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 4672350c6..2ec2b402d 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -62,10 +62,12 @@ class ModFolderPage : public ExternalResourcesPage { private slots: void removeItems(const QItemSelection& selection) override; void deleteModMetadata(); + void exportModMetadata(); void installMods(); void updateMods(bool includeDeps = false); void visitModPages(); + void changeModVersion(); protected: std::shared_ptr m_model; diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 2142e6c9f..f842b4b93 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -168,7 +168,7 @@ class ServersModel : public QAbstractListModel { m_saveTimer.setInterval(5000); connect(&m_saveTimer, &QTimer::timeout, this, &ServersModel::save_internal); } - virtual ~ServersModel(){}; + virtual ~ServersModel() {}; void observe() { diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 487433230..807bc5d58 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -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()) diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 692db7ad7..4f30e4bb7 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -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(); diff --git a/launcher/ui/pages/modplatform/CustomPage.cpp b/launcher/ui/pages/modplatform/CustomPage.cpp index d2b73008d..ba22bd2e6 100644 --- a/launcher/ui/pages/modplatform/CustomPage.cpp +++ b/launcher/ui/pages/modplatform/CustomPage.cpp @@ -49,7 +49,6 @@ CustomPage::CustomPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), dialog(dialog), ui(new Ui::CustomPage) { ui->setupUi(this); - ui->tabWidget->tabBar()->hide(); connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &CustomPage::setSelectedVersion); filterChanged(); connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); diff --git a/launcher/ui/pages/modplatform/CustomPage.ui b/launcher/ui/pages/modplatform/CustomPage.ui index fda3e8a2e..39d9aa6dc 100644 --- a/launcher/ui/pages/modplatform/CustomPage.ui +++ b/launcher/ui/pages/modplatform/CustomPage.ui @@ -24,29 +24,21 @@ 0 - - - 0 + + + true - - - - - - - - - - 0 - 0 - - - - Qt::Horizontal - - - - + + + + 0 + 0 + 813 + 605 + + + + @@ -147,7 +139,20 @@ - + + + + + 0 + 0 + + + + Qt::Horizontal + + + + @@ -273,7 +278,6 @@ - tabWidget releaseFilter snapshotFilter betaFilter diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index ed7ebfad9..38208f5b0 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -40,6 +40,7 @@ #include "ui_ImportPage.h" #include +#include #include #include @@ -51,6 +52,7 @@ #include "Json.h" #include "InstanceImportTask.h" +#include "net/NetJob.h" class UrlValidator : public QValidator { public: diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index c628f74ac..e87a423fa 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -25,15 +25,21 @@ ResourceAPI::SearchArgs ModModel::createSearchArguments() Q_ASSERT(m_filter); std::optional> versions{}; + std::optional categories{}; + auto loaders = profile->getSupportedModLoaders(); - { // Version filter - if (!m_filter->versions.empty()) - versions = m_filter->versions; - } + // 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> 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(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 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(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 diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index dd187aa8d..5c994f373 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -35,6 +35,7 @@ class ModModel : public ResourceModel { virtual ModPlatform::IndexedVersion loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) = 0; void setFilter(std::shared_ptr 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; diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 851c1c9e5..c9817cdf7 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -5,6 +5,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad + * Copyright (c) 2023 Trial97 * * 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& 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(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(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), m_filter_widget->changed()); + static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), changed); m_fetch_progress.watch(m_model->activeSearchJob().get()); } @@ -111,40 +113,6 @@ QMap ModPage::urlHandlers() const /******** Make changes to the UI ********/ -void ModPage::updateVersionList() -{ - m_ui->versionSelectionBox->clear(); - auto packProfile = (dynamic_cast(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 base_model) diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index f3660dd48..5c9a82303 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -31,8 +31,7 @@ class ModPage : public ResourcePage { auto page = new T(dialog, instance); auto model = static_cast(page->getModel()); - auto filter_widget = - ModFilterWidget::create(static_cast(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) override; - virtual auto validateVersion(ModPlatform::IndexedVersion& ver, - QString mineVer, - std::optional loaders = {}) const -> bool = 0; + virtual unique_qobject_ptr createFilterWidget() = 0; [[nodiscard]] bool supportsFiltering() const override { return true; }; auto getFilter() const -> const std::shared_ptr { return m_filter; } void setFilterWidget(unique_qobject_ptr&); - public slots: - void updateVersionList() override; - protected: ModPage(ModDownloadDialog* dialog, BaseInstance& instance); + virtual void prepareProviderCategories() {}; + protected slots: virtual void filterMods(); void triggerSearch() override; diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index 8a69e910d..e85bba9c4 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -410,12 +410,17 @@ void ResourceModel::searchRequestSucceeded(QJsonDocument& doc) m_search_state = SearchState::CanFetchMore; } + QList 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 diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index 12db49080..4c7ea33a0 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -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 diff --git a/launcher/ui/pages/modplatform/ResourcePackPage.cpp b/launcher/ui/pages/modplatform/ResourcePackPage.cpp index fc2dc15f3..849ea1111 100644 --- a/launcher/ui/pages/modplatform/ResourcePackPage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePackPage.cpp @@ -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); } diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index ae48e5523..4d01fb1f0 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -5,6 +5,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad + * Copyright (c) 2023 Trial97 * * 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 @@ -45,6 +46,7 @@ #include "Markdown.h" +#include "StringUtils.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/pages/modplatform/ResourceModel.h" #include "ui/widgets/ProjectItem.h" @@ -66,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); @@ -92,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())); @@ -234,8 +240,8 @@ void ResourcePage::updateUi() text += "
    "; - m_ui->packDescription->setHtml( - text + (current_pack->extraData.body.isEmpty() ? current_pack->description : markdownToHTML(current_pack->extraData.body))); + m_ui->packDescription->setHtml(StringUtils::htmlListPatch( + text + (current_pack->extraData.body.isEmpty() ? current_pack->description : markdownToHTML(current_pack->extraData.body)))); m_ui->packDescription->flush(); } @@ -265,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 :(")); @@ -393,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 @@ -437,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 diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h index 235b44412..d5214dd34 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.h +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -83,8 +83,10 @@ class ResourcePage : public QWidget, public BasePage { QList 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 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 diff --git a/launcher/ui/pages/modplatform/ResourcePage.ui b/launcher/ui/pages/modplatform/ResourcePage.ui index 73a9d3b1a..491e7d9f0 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.ui +++ b/launcher/ui/pages/modplatform/ResourcePage.ui @@ -10,48 +10,55 @@ 685 - - - - - - - false - - - false + + + + + + + Filter options - - - - Qt::ScrollBarAlwaysOff - - - true - - - - 48 - 48 - - - + + - - - - Search + + + + Qt::Horizontal + + false + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + 48 + 48 + + + + + + false + + + false + + - - - - + @@ -74,20 +81,6 @@ - - - - Filter options - - - - - - - Qt::Vertical - - - @@ -98,8 +91,6 @@ - searchEdit - searchButton packView packDescription sortByBox diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.cpp b/launcher/ui/pages/modplatform/ShaderPackPage.cpp index 8be068312..ebd8d4ea2 100644 --- a/launcher/ui/pages/modplatform/ShaderPackPage.cpp +++ b/launcher/ui/pages/modplatform/ShaderPackPage.cpp @@ -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); } diff --git a/launcher/ui/pages/modplatform/TexturePackPage.h b/launcher/ui/pages/modplatform/TexturePackPage.h index 948e5286b..42aa921c5 100644 --- a/launcher/ui/pages/modplatform/TexturePackPage.h +++ b/launcher/ui/pages/modplatform/TexturePackPage.h @@ -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); } }; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index e492830c6..d79b7621a 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -39,6 +39,7 @@ #include "ui_AtlPage.h" #include "BuildConfig.h" +#include "StringUtils.h" #include "AtlUserInteractionSupportImpl.h" #include "modplatform/atlauncher/ATLPackInstallTask.h" @@ -144,7 +145,7 @@ void AtlPage::onSelectionChanged(QModelIndex first, [[maybe_unused]] QModelIndex selected = filterModel->data(first, Qt::UserRole).value(); - ui->packDescription->setHtml(selected.description.replace("\n", "
    ")); + ui->packDescription->setHtml(StringUtils::htmlListPatch(selected.description.replace("\n", "
    "))); for (const auto& version : selected.versions) { ui->versionSelectionBox->addItem(version.version); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui index 8b6747331..0b1411b96 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui @@ -10,72 +10,8 @@ 685 - - - - - - - - - - Version selected: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - - - true - - - true - - - - - - - true - - - - 96 - 48 - - - - - - - - - - Search and filter... - - - true - - - - - - - Search - - - - + + @@ -93,6 +29,63 @@ + + + + Search and filter... + + + true + + + + + + + + + true + + + + 96 + 48 + + + + + + + + true + + + true + + + + + + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index f1fd9b5d8..2c10dd085 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -43,6 +43,7 @@ #include "FlameModel.h" #include "InstanceImportTask.h" #include "Json.h" +#include "StringUtils.h" #include "modplatform/flame/FlameAPI.h" #include "ui/dialogs/NewInstanceDialog.h" #include "ui/widgets/ProjectItem.h" @@ -55,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); @@ -72,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")); @@ -178,7 +178,11 @@ void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelInde for (auto version : current.versions) { auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; - ui->versionSelectionBox->addItem(QString("%1%2").arg(version.version, release_type), QVariant(version.downloadUrl)); + auto mcVersion = !version.mcVersion.isEmpty() && !version.version.contains(version.mcVersion) + ? QString(" for %1").arg(version.mcVersion) + : ""; + ui->versionSelectionBox->addItem(QString("%1%2%3").arg(version.version, mcVersion, release_type), + QVariant(version.downloadUrl)); } QVariant current_updated; @@ -292,6 +296,6 @@ void FlamePage::updateUi() text += "
    "; text += api.getModDescription(current.addonId).toUtf8(); - ui->packDescription->setHtml(text + current.description); + ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description)); ui->packDescription->flush(); } diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui index f9e1fe67f..d4ddb37a4 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.ui +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -10,8 +10,8 @@ 600 - - + + @@ -29,25 +29,14 @@ - - - - - - Search and filter... - - - - - - - Search - - - - + + + + Search and filter... + + - + @@ -77,7 +66,7 @@ - + diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp index 7d18e72a6..ae4562be4 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -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"); diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h index 76dbd7b3d..458fd85d0 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h @@ -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(); } diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index 23373ec9d..d82c76c3a 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -5,6 +5,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad + * Copyright (c) 2023 Trial97 * * 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 +#include +#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 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 FlameModPage::createFilterWidget() +{ + return ModFilterWidget::create(&static_cast(m_base_instance), false, this); +} + +void FlameModPage::prepareProviderCategories() +{ + auto response = std::make_shared(); + 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 diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index f2f5cecad..6eef3e435 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -5,6 +5,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad + * Copyright (c) 2023 Trial97 * * 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 loaders = {}) const override; - bool optedOut(ModPlatform::IndexedVersion& ver) const override; - void openUrl(const QUrl& url) override; + unique_qobject_ptr 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; }; diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp index ac06f4cdd..db59fe10a 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp @@ -21,6 +21,7 @@ #include "ui_ImportFTBPage.h" #include +#include #include #include "FileSystem.h" #include "ListModel.h" @@ -58,8 +59,8 @@ ImportFTBPage::ImportFTBPage(NewInstanceDialog* dialog, QWidget* parent) : QWidg connect(ui->searchEdit, &QLineEdit::textChanged, this, &ImportFTBPage::triggerSearch); connect(ui->browseButton, &QPushButton::clicked, this, [this] { - auto path = listModel->getPath(); - QString dir = QFileDialog::getExistingDirectory(this, tr("Select FTBApp instances directory"), path, QFileDialog::ShowDirsOnly); + QString dir = QFileDialog::getExistingDirectory(this, tr("Select FTBApp instances directory"), listModel->getUserPath(), + QFileDialog::ShowDirsOnly); if (!dir.isEmpty()) listModel->setPath(dir); }); diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui index 6613a5939..18c604ca4 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui @@ -10,8 +10,49 @@ 1011 - - + + + + + Note: If your FTB instances are not in the default location, select it using the button next to search. + + + Qt::AlignCenter + + + + + + + + + Search and filter... + + + true + + + + + + + Select FTBApp instances directory + + + + + + + .. + + + true + + + + + + @@ -21,7 +62,7 @@ - + @@ -48,54 +89,6 @@ - - - - - - Search and filter... - - - true - - - - - - - Search - - - - - - - Select FTBApp instances directory - - - - - - - .. - - - true - - - - - - - - - Note: If your FTB instances are not in the default location, select it using the button next to search. - - - Qt::AlignCenter - - - diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp index e058937a6..f3c737977 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp @@ -24,45 +24,76 @@ #include #include #include "Application.h" +#include "Exception.h" #include "FileSystem.h" +#include "Json.h" #include "StringUtils.h" #include "modplatform/import_ftb/PackHelpers.h" #include "ui/widgets/ProjectItem.h" namespace FTBImportAPP { -QString getStaticPath() +QString getFTBRoot() { - QString partialPath; + QString partialPath = QDir::homePath(); #if defined(Q_OS_OSX) - partialPath = FS::PathCombine(QDir::homePath(), "Library/Application Support"); -#elif defined(Q_OS_WIN32) - partialPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", ""); -#else - partialPath = QDir::homePath(); + partialPath = FS::PathCombine(partialPath, "Library/Application Support"); #endif return FS::PathCombine(partialPath, ".ftba"); } -static const QString FTB_APP_PATH = FS::PathCombine(getStaticPath(), "instances"); +QString getDynamicPath() +{ + auto settingsPath = FS::PathCombine(getFTBRoot(), "storage", "settings.json"); + if (!QFileInfo::exists(settingsPath)) + settingsPath = FS::PathCombine(getFTBRoot(), "bin", "settings.json"); + if (!QFileInfo::exists(settingsPath)) { + qWarning() << "The ftb app setings doesn't exist."; + return {}; + } + try { + auto doc = Json::requireDocument(FS::read(settingsPath)); + return Json::requireString(Json::requireObject(doc), "instanceLocation"); + } catch (const Exception& e) { + qCritical() << "Could not read ftb settings file: " << e.cause(); + } + return {}; +} + +ListModel::ListModel(QObject* parent) : QAbstractListModel(parent), m_instances_path(getDynamicPath()) {} void ListModel::update() { beginResetModel(); - modpacks.clear(); + m_modpacks.clear(); - QString instancesPath = getPath(); - if (auto instancesInfo = QFileInfo(instancesPath); instancesInfo.exists() && instancesInfo.isDir()) { - QDirIterator directoryIterator(instancesPath, QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable | QDir::Hidden, + auto wasPathAdded = [this](QString path) { + for (auto pack : m_modpacks) { + if (pack.path == path) + return true; + } + return false; + }; + + auto scanPath = [this, wasPathAdded](QString path) { + if (path.isEmpty()) + return; + if (auto instancesInfo = QFileInfo(path); !instancesInfo.exists() || !instancesInfo.isDir()) + return; + QDirIterator directoryIterator(path, QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks); while (directoryIterator.hasNext()) { - auto modpack = parseDirectory(directoryIterator.next()); - if (!modpack.path.isEmpty()) - modpacks.append(modpack); + auto currentPath = directoryIterator.next(); + if (!wasPathAdded(currentPath)) { + auto modpack = parseDirectory(currentPath); + if (!modpack.path.isEmpty()) + m_modpacks.append(modpack); + } } - } else { - qDebug() << "Couldn't find ftb instances folder: " << instancesPath; - } + }; + + scanPath(APPLICATION->settings()->get("FTBAppInstancesPath").toString()); + scanPath(m_instances_path); endResetModel(); } @@ -70,11 +101,11 @@ void ListModel::update() QVariant ListModel::data(const QModelIndex& index, int role) const { int pos = index.row(); - if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { + if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) { return QVariant(); } - auto pack = modpacks.at(pos); + auto pack = m_modpacks.at(pos); if (role == Qt::ToolTipRole) { } @@ -110,9 +141,9 @@ QVariant ListModel::data(const QModelIndex& index, int role) const FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent) { - currentSorting = Sorting::ByGameVersion; - sortings.insert(tr("Sort by Name"), Sorting::ByName); - sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion); + m_currentSorting = Sorting::ByGameVersion; + m_sortings.insert(tr("Sort by Name"), Sorting::ByName); + m_sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion); } bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const @@ -120,12 +151,12 @@ bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) co Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value(); Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value(); - if (currentSorting == Sorting::ByGameVersion) { + if (m_currentSorting == Sorting::ByGameVersion) { Version lv(leftPack.mcVersion); Version rv(rightPack.mcVersion); return lv < rv; - } else if (currentSorting == Sorting::ByName) { + } else if (m_currentSorting == Sorting::ByName) { return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; } @@ -136,39 +167,39 @@ bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) co bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unused]] const QModelIndex& sourceParent) const { - if (searchTerm.isEmpty()) { + if (m_searchTerm.isEmpty()) { return true; } QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); Modpack pack = sourceModel()->data(index, Qt::UserRole).value(); - return pack.name.contains(searchTerm, Qt::CaseInsensitive); + return pack.name.contains(m_searchTerm, Qt::CaseInsensitive); } void FilterModel::setSearchTerm(const QString term) { - searchTerm = term.trimmed(); + m_searchTerm = term.trimmed(); invalidate(); } const QMap FilterModel::getAvailableSortings() { - return sortings; + return m_sortings; } QString FilterModel::translateCurrentSorting() { - return sortings.key(currentSorting); + return m_sortings.key(m_currentSorting); } void FilterModel::setSorting(Sorting s) { - currentSorting = s; + m_currentSorting = s; invalidate(); } FilterModel::Sorting FilterModel::getCurrentSorting() { - return currentSorting; + return m_currentSorting; } void ListModel::setPath(QString path) { @@ -176,11 +207,11 @@ void ListModel::setPath(QString path) update(); } -QString ListModel::getPath() +QString ListModel::getUserPath() { auto path = APPLICATION->settings()->get("FTBAppInstancesPath").toString(); - if (path.isEmpty() || !QFileInfo(path).exists()) - path = FTB_APP_PATH; + if (path.isEmpty()) + path = m_instances_path; return path; } } // namespace FTBImportAPP \ No newline at end of file diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.h b/launcher/ui/pages/modplatform/import_ftb/ListModel.h index ed33a88f3..a842ac8ff 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.h +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.h @@ -42,28 +42,29 @@ class FilterModel : public QSortFilterProxyModel { bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; private: - QMap sortings; - Sorting currentSorting; - QString searchTerm; + QMap m_sortings; + Sorting m_currentSorting; + QString m_searchTerm; }; class ListModel : public QAbstractListModel { Q_OBJECT public: - ListModel(QObject* parent) : QAbstractListModel(parent) {} + ListModel(QObject* parent); virtual ~ListModel() = default; - int rowCount(const QModelIndex& parent) const { return modpacks.size(); } + int rowCount(const QModelIndex& parent) const { return m_modpacks.size(); } int columnCount(const QModelIndex& parent) const { return 1; } QVariant data(const QModelIndex& index, int role) const; void update(); - QString getPath(); + QString getUserPath(); void setPath(QString path); private: - ModpackList modpacks; + ModpackList m_modpacks; + const QString m_instances_path; }; } // namespace FTBImportAPP \ No newline at end of file diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp index 0ecaf4625..a587b5baf 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -35,6 +35,7 @@ */ #include "Page.h" +#include "StringUtils.h" #include "ui/widgets/ProjectItem.h" #include "ui_Page.h" @@ -260,8 +261,9 @@ void Page::onPackSelectionChanged(Modpack* pack) { ui->versionSelectionBox->clear(); if (pack) { - currentModpackInfo->setHtml("Pack by " + pack->author + "" + "
    Minecraft " + pack->mcVersion + "
    " + "
    " + - pack->description + "
    • " + pack->mods.replace(";", "
    • ") + "
    "); + currentModpackInfo->setHtml(StringUtils::htmlListPatch("Pack by " + pack->author + "" + "
    Minecraft " + pack->mcVersion + + "
    " + "
    " + pack->description + "
    • " + + pack->mods.replace(";", "
    • ") + "
    ")); bool currentAdded = false; for (int i = 0; i < pack->oldVersions.size(); i++) { diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui index 56cba7485..544ad77d3 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui @@ -10,8 +10,8 @@ 602 - - + + @@ -23,16 +23,9 @@ - - - - Search - - - - + 0 @@ -134,22 +127,9 @@ - - - - - - Version selected: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - + + + @@ -159,6 +139,19 @@ + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index da5fe1e7b..cb811e428 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -44,6 +44,7 @@ #include "InstanceImportTask.h" #include "Json.h" #include "Markdown.h" +#include "StringUtils.h" #include "ui/widgets/ProjectItem.h" @@ -58,7 +59,6 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) { ui->setupUi(this); - connect(ui->searchButton, &QPushButton::clicked, this, &ModrinthPage::triggerSearch); ui->searchEdit->installEventFilter(this); m_model = new Modrinth::ModpackListModel(this); ui->packView->setModel(m_model); @@ -75,7 +75,7 @@ ModrinthPage::ModrinthPage(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(1, &m_fetch_progress); ui->sortByBox->addItem(tr("Sort by Relevance")); ui->sortByBox->addItem(tr("Sort by Total Downloads")); @@ -223,11 +223,12 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI } for (auto version : current.versions) { auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; - if (!version.name.contains(version.version)) - ui->versionSelectionBox->addItem(QString("%1 — %2%3").arg(version.name, version.version, release_type), - QVariant(version.id)); - else - ui->versionSelectionBox->addItem(QString("%1%2").arg(version.name, release_type), QVariant(version.id)); + auto mcVersion = !version.gameVersion.isEmpty() && !version.name.contains(version.gameVersion) + ? QString(" for %1").arg(version.gameVersion) + : ""; + auto versionStr = !version.name.contains(version.version) ? version.version : ""; + ui->versionSelectionBox->addItem(QString("%1%2 — %3%4").arg(version.name, mcVersion, versionStr, release_type), + QVariant(version.id)); } QVariant current_updated; @@ -304,7 +305,7 @@ void ModrinthPage::updateUI() text += markdownToHTML(current.extra.body.toUtf8()); - ui->packDescription->setHtml(text + current.description); + ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description)); ui->packDescription->flush(); } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index 68b1d4e24..7f4f903f6 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -10,8 +10,15 @@ 600 - - + + + + + Search and filter ... + + + + @@ -41,7 +48,7 @@ - + @@ -61,24 +68,6 @@ - - - - - - Search and filter ... - - - - - - - Search - - - - - @@ -89,8 +78,6 @@ - searchEdit - searchButton packView packDescription sortByBox diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index a4197b225..26fe46a54 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -4,6 +4,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 * * 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 @@ -63,13 +64,6 @@ ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instan m_ui->packDescription->setMetaEntry(metaEntryBase()); } -auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, - QString mineVer, - std::optional loaders) const -> bool -{ - return ver.mcVersion.contains(mineVer) && (!loaders.has_value() || !ver.loaders || loaders.value() & ver.loaders); -} - ModrinthResourcePackPage::ModrinthResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance) : ResourcePackResourcePage(dialog, instance) { @@ -144,4 +138,19 @@ auto ModrinthShaderPackPage::shouldDisplay() const -> bool return true; } +unique_qobject_ptr ModrinthModPage::createFilterWidget() +{ + return ModFilterWidget::create(&static_cast(m_base_instance), true, this); +} + +void ModrinthModPage::prepareProviderCategories() +{ + auto response = std::make_shared(); + auto task = ModrinthAPI::getModCategories(response); + QObject::connect(task.get(), &Task::succeeded, [this, response]() { + auto categories = ModrinthAPI::loadModCategories(response); + m_filter_widget->setCategories(categories); + }); + task->start(); +}; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h index 311bcfe32..eaf6129a5 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -4,6 +4,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 * * 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 @@ -93,8 +94,10 @@ class ModrinthModPage : public ModPage { [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } - auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional loaders = {}) const - -> bool override; + unique_qobject_ptr createFilterWidget() override; + + protected: + virtual void prepareProviderCategories() override; }; class ModrinthResourcePackPage : public ResourcePackResourcePage { diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 6b1ec8cb5..a8f06619f 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -44,6 +44,7 @@ #include "BuildConfig.h" #include "Json.h" +#include "StringUtils.h" #include "TechnicModel.h" #include "modplatform/technic/SingleZipPackInstallTask.h" #include "modplatform/technic/SolderPackInstallTask.h" @@ -57,7 +58,6 @@ TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog), m_fetch_progress(this, false) { ui->setupUi(this); - connect(ui->searchButton, &QPushButton::clicked, this, &TechnicPage::triggerSearch); ui->searchEdit->installEventFilter(this); model = new Technic::ListModel(this); ui->packView->setModel(model); @@ -71,7 +71,7 @@ TechnicPage::TechnicPage(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(1, &m_fetch_progress); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged); connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &TechnicPage::onVersionSelectionChanged); @@ -233,7 +233,7 @@ void TechnicPage::metadataLoaded() text += "

    "; - ui->packDescription->setHtml(text + current.description); + ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description)); // Strip trailing forward-slashes from Solder URL's if (current.isSolder) { diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.ui b/launcher/ui/pages/modplatform/technic/TechnicPage.ui index b988eda2b..f4e75ae12 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.ui +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.ui @@ -10,23 +10,41 @@ 405 - - - - - - - - - - Version selected: + + + + + Search and filter... + + + + + + + + + true - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 48 + 48 + - + + + + true + + + + + + + + Qt::Horizontal @@ -42,46 +60,21 @@ - - - - - - - - true + + + + Version selected: - - - 48 - 48 - + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - true - - + + - - - - Search and filter... - - - - - - - Search - - - diff --git a/launcher/ui/setupwizard/BaseWizardPage.h b/launcher/ui/setupwizard/BaseWizardPage.h index 80cc64969..b5ea06214 100644 --- a/launcher/ui/setupwizard/BaseWizardPage.h +++ b/launcher/ui/setupwizard/BaseWizardPage.h @@ -6,7 +6,7 @@ class BaseWizardPage : public QWizardPage { public: explicit BaseWizardPage(QWidget* parent = Q_NULLPTR) : QWizardPage(parent) {} - virtual ~BaseWizardPage(){}; + virtual ~BaseWizardPage() {}; virtual bool wantsRefreshButton() { return false; } virtual void refresh() {} diff --git a/launcher/ui/setupwizard/JavaWizardPage.h b/launcher/ui/setupwizard/JavaWizardPage.h index 6c083dc96..dde765f27 100644 --- a/launcher/ui/setupwizard/JavaWizardPage.h +++ b/launcher/ui/setupwizard/JavaWizardPage.h @@ -9,7 +9,7 @@ class JavaWizardPage : public BaseWizardPage { public: explicit JavaWizardPage(QWidget* parent = Q_NULLPTR); - virtual ~JavaWizardPage(){}; + virtual ~JavaWizardPage() {}; bool wantsRefreshButton() override; void refresh() override; diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp index a128fc3f5..1cb83ca26 100644 --- a/launcher/ui/themes/ThemeManager.cpp +++ b/launcher/ui/themes/ThemeManager.cpp @@ -313,3 +313,13 @@ void ThemeManager::initializeCatPacks() } } } + +void ThemeManager::refresh() +{ + m_themes.clear(); + m_icons.clear(); + m_cat_packs.clear(); + + initializeThemes(); + initializeCatPacks(); +}; \ No newline at end of file diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h index b77b5947a..47b9589e3 100644 --- a/launcher/ui/themes/ThemeManager.h +++ b/launcher/ui/themes/ThemeManager.h @@ -55,6 +55,8 @@ class ThemeManager { QString getCatPack(QString catName = ""); QList getValidCatPacks(); + void refresh(); + private: std::map> m_themes; std::map m_icons; diff --git a/launcher/ui/widgets/CheckComboBox.cpp b/launcher/ui/widgets/CheckComboBox.cpp new file mode 100644 index 000000000..41def3ba1 --- /dev/null +++ b/launcher/ui/widgets/CheckComboBox.cpp @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#include "CheckComboBox.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class CheckComboModel : public QIdentityProxyModel { + Q_OBJECT + + public: + explicit CheckComboModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} + + virtual Qt::ItemFlags flags(const QModelIndex& index) const { return QIdentityProxyModel::flags(index) | Qt::ItemIsUserCheckable; } + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const + { + if (role == Qt::CheckStateRole) { + auto txt = QIdentityProxyModel::data(index, Qt::DisplayRole).toString(); + return checked.contains(txt) ? Qt::Checked : Qt::Unchecked; + } + if (role == Qt::DisplayRole) + return QIdentityProxyModel::data(index, Qt::DisplayRole); + return {}; + } + virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) + { + if (role == Qt::CheckStateRole) { + auto txt = QIdentityProxyModel::data(index, Qt::DisplayRole).toString(); + if (checked.contains(txt)) { + checked.removeOne(txt); + } else { + checked.push_back(txt); + } + emit dataChanged(index, index); + emit checkStateChanged(); + return true; + } + return QIdentityProxyModel::setData(index, value, role); + } + QStringList getChecked() { return checked; } + + signals: + void checkStateChanged(); + + private: + QStringList checked; +}; + +CheckComboBox::CheckComboBox(QWidget* parent) : QComboBox(parent), m_separator(", ") +{ + view()->installEventFilter(this); + view()->window()->installEventFilter(this); + view()->viewport()->installEventFilter(this); + this->installEventFilter(this); +} + +void CheckComboBox::setSourceModel(QAbstractItemModel* new_model) +{ + auto proxy = new CheckComboModel(this); + proxy->setSourceModel(new_model); + model()->disconnect(this); + QComboBox::setModel(proxy); + connect(this, QOverload::of(&QComboBox::activated), this, &CheckComboBox::toggleCheckState); + connect(proxy, &CheckComboModel::checkStateChanged, this, &CheckComboBox::emitCheckedItemsChanged); + connect(model(), &CheckComboModel::rowsInserted, this, &CheckComboBox::emitCheckedItemsChanged); + connect(model(), &CheckComboModel::rowsRemoved, this, &CheckComboBox::emitCheckedItemsChanged); +} + +void CheckComboBox::hidePopup() +{ + if (!containerMousePress) + QComboBox::hidePopup(); +} + +void CheckComboBox::emitCheckedItemsChanged() +{ + emit checkedItemsChanged(checkedItems()); +} + +QString CheckComboBox::defaultText() const +{ + return m_default_text; +} + +void CheckComboBox::setDefaultText(const QString& text) +{ + m_default_text = text; +} + +QString CheckComboBox::separator() const +{ + return m_separator; +} + +void CheckComboBox::setSeparator(const QString& separator) +{ + m_separator = separator; +} + +bool CheckComboBox::eventFilter(QObject* receiver, QEvent* event) +{ + switch (event->type()) { + case QEvent::KeyPress: + case QEvent::KeyRelease: { + QKeyEvent* keyEvent = static_cast(event); + if (receiver == this && (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down)) { + showPopup(); + return true; + } else if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Escape) { + QComboBox::hidePopup(); + return (keyEvent->key() != Qt::Key_Escape); + } + break; + } + case QEvent::MouseButtonPress: { + auto ev = static_cast(event); + containerMousePress = ev && view()->indexAt(ev->pos()).isValid(); + break; + } + case QEvent::Wheel: + return receiver == this; + default: + break; + } + return false; +} + +void CheckComboBox::toggleCheckState(int index) +{ + QVariant value = itemData(index, Qt::CheckStateRole); + if (value.isValid()) { + Qt::CheckState state = static_cast(value.toInt()); + setItemData(index, (state == Qt::Unchecked ? Qt::Checked : Qt::Unchecked), Qt::CheckStateRole); + } + emitCheckedItemsChanged(); +} + +Qt::CheckState CheckComboBox::itemCheckState(int index) const +{ + return static_cast(itemData(index, Qt::CheckStateRole).toInt()); +} + +void CheckComboBox::setItemCheckState(int index, Qt::CheckState state) +{ + setItemData(index, state, Qt::CheckStateRole); +} + +QStringList CheckComboBox::checkedItems() const +{ + if (model()) + return dynamic_cast(model())->getChecked(); + return {}; +} + +void CheckComboBox::setCheckedItems(const QStringList& items) +{ + foreach (auto text, items) { + auto index = findText(text); + setItemCheckState(index, index != -1 ? Qt::Checked : Qt::Unchecked); + } +} + +void CheckComboBox::paintEvent(QPaintEvent*) +{ + QStylePainter painter(this); + painter.setPen(palette().color(QPalette::Text)); + + // draw the combobox frame, focusrect and selected etc. + QStyleOptionComboBox opt; + initStyleOption(&opt); + QStringList items = checkedItems(); + if (items.isEmpty()) + opt.currentText = defaultText(); + else + opt.currentText = items.join(separator()); + painter.drawComplexControl(QStyle::CC_ComboBox, opt); + + // draw the icon and text + painter.drawControl(QStyle::CE_ComboBoxLabel, opt); +} + +#include "CheckComboBox.moc" \ No newline at end of file diff --git a/launcher/ui/widgets/CheckComboBox.h b/launcher/ui/widgets/CheckComboBox.h new file mode 100644 index 000000000..876c6e3e1 --- /dev/null +++ b/launcher/ui/widgets/CheckComboBox.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + */ + +#pragma once + +#include +#include + +class CheckComboBox : public QComboBox { + Q_OBJECT + + public: + explicit CheckComboBox(QWidget* parent = nullptr); + virtual ~CheckComboBox() = default; + + void hidePopup() override; + + QString defaultText() const; + void setDefaultText(const QString& text); + + Qt::CheckState itemCheckState(int index) const; + void setItemCheckState(int index, Qt::CheckState state); + + QString separator() const; + void setSeparator(const QString& separator); + + QStringList checkedItems() const; + + void setSourceModel(QAbstractItemModel* model); + + public slots: + void setCheckedItems(const QStringList& items); + + signals: + void checkedItemsChanged(const QStringList& items); + + protected: + void paintEvent(QPaintEvent*) override; + + private: + void emitCheckedItemsChanged(); + bool eventFilter(QObject* receiver, QEvent* event) override; + void toggleCheckState(int index); + + private: + QString m_default_text; + QString m_separator; + bool containerMousePress; +}; \ No newline at end of file diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index 69f72fea2..44f702659 100644 --- a/launcher/ui/widgets/InfoFrame.cpp +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -36,6 +36,8 @@ #include #include +#include +#include #include #include "InfoFrame.h" @@ -274,12 +276,27 @@ void InfoFrame::setDescription(QString text) } QString labeltext; labeltext.reserve(300); - if (finaltext.length() > 290) { + + // elide rich text by getting characters without formatting + const int maxCharacterElide = 290; + QTextDocument doc; + doc.setHtml(text); + + if (doc.characterCount() > maxCharacterElide) { ui->descriptionLabel->setOpenExternalLinks(false); - ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText); + ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText); // This allows injecting HTML here. m_description = text; - // This allows injecting HTML here. - labeltext.append("" + finaltext.left(287) + "..."); + + // move the cursor to the character elide, doesn't see html + QTextCursor cursor(&doc); + cursor.movePosition(QTextCursor::End); + cursor.setPosition(maxCharacterElide, QTextCursor::KeepAnchor); + cursor.removeSelectedText(); + + // insert the post fix at the cursor + cursor.insertHtml("..."); + + labeltext.append(doc.toHtml()); QObject::connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler); } else { ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText); @@ -316,7 +333,7 @@ void InfoFrame::setLicense(QString text) if (finaltext.length() > 290) { ui->licenseLabel->setOpenExternalLinks(false); ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText); - m_description = text; + m_license = text; // This allows injecting HTML here. labeltext.append("" + finaltext.left(287) + "..."); QObject::connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler); diff --git a/launcher/ui/widgets/JavaSettingsWidget.h b/launcher/ui/widgets/JavaSettingsWidget.h index 6ea73da60..18a480532 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.h +++ b/launcher/ui/widgets/JavaSettingsWidget.h @@ -25,7 +25,7 @@ class JavaSettingsWidget : public QWidget { public: explicit JavaSettingsWidget(QWidget* parent); - virtual ~JavaSettingsWidget(){}; + virtual ~JavaSettingsWidget() {}; enum class JavaStatus { NotSet, Pending, Good, DoesNotExist, DoesNotStart, ReturnedInvalidData } javaStatus = JavaStatus::NotSet; diff --git a/launcher/ui/widgets/LanguageSelectionWidget.h b/launcher/ui/widgets/LanguageSelectionWidget.h index f034853dd..cf1f5bf3c 100644 --- a/launcher/ui/widgets/LanguageSelectionWidget.h +++ b/launcher/ui/widgets/LanguageSelectionWidget.h @@ -27,7 +27,7 @@ class LanguageSelectionWidget : public QWidget { Q_OBJECT public: explicit LanguageSelectionWidget(QWidget* parent = 0); - virtual ~LanguageSelectionWidget(){}; + virtual ~LanguageSelectionWidget() {}; QString getSelectedLanguageKey() const; void retranslate(); diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index c2c099eeb..bbb91eac2 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -1,13 +1,139 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ModFilterWidget.h" +#include +#include +#include +#include +#include +#include "BaseVersionList.h" +#include "Version.h" +#include "meta/Index.h" +#include "modplatform/ModIndex.h" +#include "ui/widgets/CheckComboBox.h" #include "ui_ModFilterWidget.h" #include "Application.h" +#include "minecraft/PackProfile.h" -unique_qobject_ptr ModFilterWidget::create(Version default_version, QWidget* parent) +unique_qobject_ptr ModFilterWidget::create(MinecraftInstance* instance, bool extended, QWidget* parent) { - auto filter_widget = new ModFilterWidget(default_version, parent); + return unique_qobject_ptr(new ModFilterWidget(instance, extended, parent)); +} - if (!filter_widget->versionList()->isLoaded()) { +class VersionBasicModel : public QIdentityProxyModel { + Q_OBJECT + + public: + explicit VersionBasicModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} + + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override + { + if (role == Qt::DisplayRole) + return QIdentityProxyModel::data(index, BaseVersionList::VersionIdRole); + return {}; + } +}; + +ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended, QWidget* parent) + : QTabWidget(parent), ui(new Ui::ModFilterWidget), m_instance(instance), m_filter(new Filter()) +{ + ui->setupUi(this); + + m_versions_proxy = new VersionProxyModel(this); + m_versions_proxy->setFilter(BaseVersionList::TypeRole, new ExactFilter("release")); + + auto proxy = new VersionBasicModel(this); + proxy->setSourceModel(m_versions_proxy); + + if (extended) { + ui->versions->setSourceModel(proxy); + ui->versions->setSeparator(", "); + ui->version->hide(); + } else { + ui->version->setModel(proxy); + ui->versions->hide(); + ui->showAllVersions->hide(); + ui->environmentGroup->hide(); + } + + ui->versions->setStyleSheet("combobox-popup: 0;"); + ui->version->setStyleSheet("combobox-popup: 0;"); + connect(ui->showAllVersions, &QCheckBox::stateChanged, this, &ModFilterWidget::onShowAllVersionsChanged); + connect(ui->versions, QOverload::of(&QComboBox::currentIndexChanged), this, &ModFilterWidget::onVersionFilterChanged); + connect(ui->versions, &CheckComboBox::checkedItemsChanged, this, [this] { onVersionFilterChanged(0); }); + connect(ui->version, &QComboBox::currentTextChanged, this, &ModFilterWidget::onVersionFilterTextChanged); + + connect(ui->neoForge, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->forge, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->fabric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->quilt, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + + connect(ui->neoForge, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->forge, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->fabric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->quilt, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + + if (extended) { + connect(ui->clientSide, &QCheckBox::stateChanged, this, &ModFilterWidget::onSideFilterChanged); + connect(ui->serverSide, &QCheckBox::stateChanged, this, &ModFilterWidget::onSideFilterChanged); + } + + connect(ui->hideInstalled, &QCheckBox::stateChanged, this, &ModFilterWidget::onHideInstalledFilterChanged); + + setHidden(true); + loadVersionList(); + prepareBasicFilter(); +} + +auto ModFilterWidget::getFilter() -> std::shared_ptr +{ + m_filter_changed = false; + return m_filter; +} + +ModFilterWidget::~ModFilterWidget() +{ + delete ui; +} + +void ModFilterWidget::loadVersionList() +{ + m_version_list = APPLICATION->metadataIndex()->get("net.minecraft"); + if (!m_version_list->isLoaded()) { QEventLoop load_version_list_loop; QTimer time_limit_for_list_load; @@ -16,10 +142,12 @@ unique_qobject_ptr ModFilterWidget::create(Version default_vers time_limit_for_list_load.callOnTimeout(&load_version_list_loop, &QEventLoop::quit); time_limit_for_list_load.start(4000); - auto task = filter_widget->versionList()->getLoadTask(); + auto task = m_version_list->getLoadTask(); - connect(task.get(), &Task::failed, - [filter_widget] { filter_widget->disableVersionButton(VersionButtonID::Major, tr("failed to get version index")); }); + connect(task.get(), &Task::failed, [this] { + ui->versions->setEnabled(false); + ui->showAllVersions->setEnabled(false); + }); connect(task.get(), &Task::finished, &load_version_list_loop, &QEventLoop::quit); if (!task->isRunning()) @@ -29,128 +157,132 @@ unique_qobject_ptr ModFilterWidget::create(Version default_vers if (time_limit_for_list_load.isActive()) time_limit_for_list_load.stop(); } - - return unique_qobject_ptr(filter_widget); + m_versions_proxy->setSourceModel(m_version_list.get()); } -ModFilterWidget::ModFilterWidget(Version def, QWidget* parent) : QTabWidget(parent), m_filter(new Filter()), ui(new Ui::ModFilterWidget) +void ModFilterWidget::prepareBasicFilter() { - ui->setupUi(this); - - m_mcVersion_buttons.addButton(ui->strictVersionButton, VersionButtonID::Strict); - ui->strictVersionButton->click(); - m_mcVersion_buttons.addButton(ui->majorVersionButton, VersionButtonID::Major); - m_mcVersion_buttons.addButton(ui->allVersionsButton, VersionButtonID::All); - // m_mcVersion_buttons.addButton(ui->betweenVersionsButton, VersionButtonID::Between); - - connect(&m_mcVersion_buttons, SIGNAL(idClicked(int)), this, SLOT(onVersionFilterChanged(int))); - - m_filter->versions.push_front(def); - - m_version_list = APPLICATION->metadataIndex()->get("net.minecraft"); - setHidden(true); + m_filter->hideInstalled = false; + m_filter->side = ""; // or "both" + auto loaders = m_instance->getPackProfile()->getSupportedModLoaders().value(); + ui->neoForge->setChecked(loaders & ModPlatform::NeoForge); + ui->forge->setChecked(loaders & ModPlatform::Forge); + ui->fabric->setChecked(loaders & ModPlatform::Fabric); + ui->quilt->setChecked(loaders & ModPlatform::Quilt); + m_filter->loaders = loaders; + auto def = m_instance->getPackProfile()->getComponentVersion("net.minecraft"); + m_filter->versions.emplace_front(def); + ui->versions->setCheckedItems({ def }); + ui->version->setCurrentIndex(ui->version->findText(def)); } -void ModFilterWidget::setInstance(MinecraftInstance* instance) +void ModFilterWidget::onShowAllVersionsChanged() { - m_instance = instance; - - ui->strictVersionButton->setText(tr("Strict match (= %1)").arg(mcVersionStr())); - - // we can't do this for snapshots sadly - if (mcVersionStr().contains('.')) { - auto mcVersionSplit = mcVersionStr().split("."); - ui->majorVersionButton->setText(tr("Major version match (= %1.%2.x)").arg(mcVersionSplit[0], mcVersionSplit[1])); - } else { - ui->majorVersionButton->setText(tr("Major version match (unsupported)")); - disableVersionButton(Major); - } - ui->allVersionsButton->setText(tr("Any version")); - // ui->betweenVersionsButton->setText( - // tr("Between two versions")); -} - -auto ModFilterWidget::getFilter() -> std::shared_ptr -{ - m_last_version_id = m_version_id; - emit filterUnchanged(); - return m_filter; -} - -void ModFilterWidget::disableVersionButton(VersionButtonID id, QString reason) -{ - QAbstractButton* btn = nullptr; - - switch (id) { - case (VersionButtonID::Strict): - btn = ui->strictVersionButton; - break; - case (VersionButtonID::Major): - btn = ui->majorVersionButton; - break; - case (VersionButtonID::All): - btn = ui->allVersionsButton; - break; - case (VersionButtonID::Between): - default: - break; - } - - if (btn) { - btn->setEnabled(false); - if (!reason.isEmpty()) - btn->setText(btn->text() + QString(" (%1)").arg(reason)); - } -} - -void ModFilterWidget::onVersionFilterChanged(int id) -{ - // ui->lowerVersionComboBox->setEnabled(id == VersionButtonID::Between); - // ui->upperVersionComboBox->setEnabled(id == VersionButtonID::Between); - - int index = 1; - - auto cast_id = (VersionButtonID)id; - if (cast_id != m_version_id) { - m_version_id = cast_id; - } else { - return; - } - - m_filter->versions.clear(); - - switch (cast_id) { - case (VersionButtonID::Strict): - m_filter->versions.push_front(mcVersion()); - break; - case (VersionButtonID::Major): { - auto versionSplit = mcVersionStr().split("."); - - auto major_version = QString("%1.%2").arg(versionSplit[0], versionSplit[1]); - QString version_str = major_version; - - while (m_version_list->hasVersion(version_str)) { - m_filter->versions.emplace_back(version_str); - version_str = QString("%1.%2").arg(major_version, QString::number(index++)); - } - - break; - } - case (VersionButtonID::All): - // Empty list to avoid enumerating all versions :P - break; - case (VersionButtonID::Between): - // TODO - break; - } - - if (changed()) - emit filterChanged(); + if (ui->showAllVersions->isChecked()) + m_versions_proxy->clearFilters(); else - emit filterUnchanged(); + m_versions_proxy->setFilter(BaseVersionList::TypeRole, new ExactFilter("release")); } -ModFilterWidget::~ModFilterWidget() +void ModFilterWidget::onVersionFilterChanged(int) { - delete ui; + auto versions = ui->versions->checkedItems(); + versions.sort(); + std::list current_list; + + for (const QString& version : versions) + current_list.emplace_back(version); + + m_filter_changed = m_filter->versions.size() != current_list.size() || + !std::equal(m_filter->versions.begin(), m_filter->versions.end(), current_list.begin(), current_list.end()); + m_filter->versions = current_list; + if (m_filter_changed) + emit filterChanged(); } + +void ModFilterWidget::onLoadersFilterChanged() +{ + ModPlatform::ModLoaderTypes loaders; + if (ui->neoForge->isChecked()) + loaders |= ModPlatform::NeoForge; + if (ui->forge->isChecked()) + loaders |= ModPlatform::Forge; + if (ui->fabric->isChecked()) + loaders |= ModPlatform::Fabric; + if (ui->quilt->isChecked()) + loaders |= ModPlatform::Quilt; + m_filter_changed = loaders != m_filter->loaders; + m_filter->loaders = loaders; + if (m_filter_changed) + emit filterChanged(); +} + +void ModFilterWidget::onSideFilterChanged() +{ + QString side; + + if (ui->clientSide->isChecked() != ui->serverSide->isChecked()) { + if (ui->clientSide->isChecked()) + side = "client"; + else + side = "server"; + } else { + // both are checked or none are checked; in either case no filtering will happen + side = ""; + } + + m_filter_changed = side != m_filter->side; + m_filter->side = side; + if (m_filter_changed) + emit filterChanged(); +} + +void ModFilterWidget::onHideInstalledFilterChanged() +{ + auto hide = ui->hideInstalled->isChecked(); + m_filter_changed = hide != m_filter->hideInstalled; + m_filter->hideInstalled = hide; + if (m_filter_changed) + emit filterChanged(); +} + +void ModFilterWidget::onVersionFilterTextChanged(const QString& version) +{ + m_filter->versions.clear(); + m_filter->versions.emplace_back(version); + m_filter_changed = true; + emit filterChanged(); +} + +void ModFilterWidget::setCategories(const QList& categories) +{ + m_categories = categories; + + delete ui->categoryGroup->layout(); + auto layout = new QVBoxLayout(ui->categoryGroup); + + for (const auto& category : categories) { + auto name = category.name; + name.replace("-", " "); + name.replace("&", "&&"); + auto checkbox = new QCheckBox(name); + auto font = checkbox->font(); + font.setCapitalization(QFont::Capitalize); + checkbox->setFont(font); + + layout->addWidget(checkbox); + + const QString id = category.id; + connect(checkbox, &QCheckBox::toggled, this, [this, id](bool checked) { + if (checked) + m_filter->categoryIds.append(id); + else + m_filter->categoryIds.removeOne(id); + + m_filter_changed = true; + emit filterChanged(); + }); + } +} + +#include "ModFilterWidget.moc" \ No newline at end of file diff --git a/launcher/ui/widgets/ModFilterWidget.h b/launcher/ui/widgets/ModFilterWidget.h index ed6cd0ea7..fdfd2c8bb 100644 --- a/launcher/ui/widgets/ModFilterWidget.h +++ b/launcher/ui/widgets/ModFilterWidget.h @@ -1,15 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * 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 . + * + * 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 +#include +#include #include #include "Version.h" -#include "meta/Index.h" +#include "VersionProxyModel.h" #include "meta/VersionList.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" +#include "modplatform/ModIndex.h" class MinecraftInstance; @@ -20,59 +57,57 @@ class ModFilterWidget; class ModFilterWidget : public QTabWidget { Q_OBJECT public: - enum VersionButtonID { Strict = 0, Major = 1, All = 2, Between = 3 }; - struct Filter { std::list versions; + std::list releases; + ModPlatform::ModLoaderTypes loaders; + QString side; + bool hideInstalled; + QStringList categoryIds; - bool operator==(const Filter& other) const { return versions == other.versions; } + bool operator==(const Filter& other) const + { + return hideInstalled == other.hideInstalled && side == other.side && loaders == other.loaders && versions == other.versions && + releases == other.releases && categoryIds == other.categoryIds; + } bool operator!=(const Filter& other) const { return !(*this == other); } }; - std::shared_ptr m_filter; - - public: - static unique_qobject_ptr create(Version default_version, QWidget* parent = nullptr); - ~ModFilterWidget(); - - void setInstance(MinecraftInstance* instance); - - /// By default all buttons are enabled - void disableVersionButton(VersionButtonID, QString reason = {}); + static unique_qobject_ptr create(MinecraftInstance* instance, bool extended, QWidget* parent = nullptr); + virtual ~ModFilterWidget(); auto getFilter() -> std::shared_ptr; - auto changed() const -> bool { return m_last_version_id != m_version_id; } + auto changed() const -> bool { return m_filter_changed; } - Meta::VersionList::Ptr versionList() { return m_version_list; } - - private: - ModFilterWidget(Version def, QWidget* parent = nullptr); - - inline auto mcVersionStr() const -> QString - { - return m_instance ? m_instance->getPackProfile()->getComponentVersion("net.minecraft") : ""; - } - inline auto mcVersion() const -> Version { return { mcVersionStr() }; } - - private slots: - void onVersionFilterChanged(int id); - - public: signals: void filterChanged(); - void filterUnchanged(); + + public slots: + void setCategories(const QList&); + + private: + ModFilterWidget(MinecraftInstance* instance, bool extendedSupport, QWidget* parent = nullptr); + + void loadVersionList(); + void prepareBasicFilter(); + + private slots: + void onVersionFilterChanged(int); + void onVersionFilterTextChanged(const QString& version); + void onLoadersFilterChanged(); + void onSideFilterChanged(); + void onHideInstalledFilterChanged(); + void onShowAllVersionsChanged(); private: Ui::ModFilterWidget* ui; MinecraftInstance* m_instance = nullptr; - - /* Version stuff */ - QButtonGroup m_mcVersion_buttons; + std::shared_ptr m_filter; + bool m_filter_changed = false; Meta::VersionList::Ptr m_version_list; + VersionProxyModel* m_versions_proxy = nullptr; - /* Used to tell if the filter was changed since the last getFilter() call */ - VersionButtonID m_last_version_id = VersionButtonID::Strict; - VersionButtonID m_version_id = VersionButtonID::Strict; + QList m_categories; }; diff --git a/launcher/ui/widgets/ModFilterWidget.ui b/launcher/ui/widgets/ModFilterWidget.ui index ebe5d2be1..236847094 100644 --- a/launcher/ui/widgets/ModFilterWidget.ui +++ b/launcher/ui/widgets/ModFilterWidget.ui @@ -1,54 +1,219 @@ ModFilterWidget - + 0 0 - 400 - 300 + 310 + 600 - + 0 0 - - - Minecraft versions - - - - - - - - allVersions - - - - - - - strictVersion - - - - - - - majorVersion - - - - - - - + + + 275 + 0 + + + + + 310 + 16777215 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 275 + 0 + + + + QAbstractScrollArea::AdjustToContentsOnFirstShow + + + true + + + + + 0 + 0 + 308 + 598 + + + + + + + Categories + + + false + + + false + + + + + + + Loaders + + + false + + + false + + + + + + NeoForge + + + + + + + Forge + + + + + + + Fabric + + + + + + + Quilt + + + + + + + + + + Versions + + + false + + + false + + + + + + Show all versions + + + + + + + + + + + + + + + + Environments + + + false + + + false + + + + + + Client + + + + + + + Server + + + + + + + + + + Hide installed items + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + CheckComboBox + QComboBox +
    ui/widgets/CheckComboBox.h
    +
    +
    diff --git a/launcher/ui/widgets/PageContainer.h b/launcher/ui/widgets/PageContainer.h index 05be1c3a5..ab4444c99 100644 --- a/launcher/ui/widgets/PageContainer.h +++ b/launcher/ui/widgets/PageContainer.h @@ -36,6 +36,7 @@ #pragma once +#include #include #include @@ -86,6 +87,8 @@ class PageContainer : public QWidget, public BasePageContainer { void changeEvent(QEvent*) override; + void hidePageList() { m_pageList->hide(); } + private: void createUI(); void retranslate(); diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.cpp b/launcher/ui/widgets/ThemeCustomizationWidget.cpp index 25b91857c..9c5df0067 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.cpp +++ b/launcher/ui/widgets/ThemeCustomizationWidget.cpp @@ -27,6 +27,7 @@ ThemeCustomizationWidget::ThemeCustomizationWidget(QWidget* parent) : QWidget(pa { ui->setupUi(this); loadSettings(); + ThemeCustomizationWidget::refresh(); connect(ui->iconsComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyIconTheme); connect(ui->widgetStyleComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, @@ -39,6 +40,8 @@ ThemeCustomizationWidget::ThemeCustomizationWidget(QWidget* parent) : QWidget(pa [] { DesktopServices::openPath(APPLICATION->themeManager()->getApplicationThemesFolder().path()); }); connect(ui->catPackFolder, &QPushButton::clicked, this, [] { DesktopServices::openPath(APPLICATION->themeManager()->getCatPacksFolder().path()); }); + + connect(ui->refreshButton, &QPushButton::clicked, this, &ThemeCustomizationWidget::refresh); } ThemeCustomizationWidget::~ThemeCustomizationWidget() @@ -169,3 +172,22 @@ void ThemeCustomizationWidget::retranslate() { ui->retranslateUi(this); } + +void ThemeCustomizationWidget::refresh() +{ + applySettings(); + disconnect(ui->iconsComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyIconTheme); + disconnect(ui->widgetStyleComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &ThemeCustomizationWidget::applyWidgetTheme); + disconnect(ui->backgroundCatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &ThemeCustomizationWidget::applyCatTheme); + APPLICATION->themeManager()->refresh(); + ui->iconsComboBox->clear(); + ui->widgetStyleComboBox->clear(); + ui->backgroundCatComboBox->clear(); + loadSettings(); + connect(ui->iconsComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyIconTheme); + connect(ui->widgetStyleComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &ThemeCustomizationWidget::applyWidgetTheme); + connect(ui->backgroundCatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyCatTheme); +}; \ No newline at end of file diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.h b/launcher/ui/widgets/ThemeCustomizationWidget.h index cef5fb6c6..6977b8495 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.h +++ b/launcher/ui/widgets/ThemeCustomizationWidget.h @@ -44,6 +44,7 @@ class ThemeCustomizationWidget : public QWidget { void applyIconTheme(int index); void applyWidgetTheme(int index); void applyCatTheme(int index); + void refresh(); signals: int currentIconThemeChanged(int index); diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.ui b/launcher/ui/widgets/ThemeCustomizationWidget.ui index 4503181c2..1faa45c4f 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.ui +++ b/launcher/ui/widgets/ThemeCustomizationWidget.ui @@ -13,7 +13,7 @@ Form - + QLayout::SetMinimumSize @@ -29,141 +29,179 @@ 0 - - - - &Icons - - - iconsComboBox - - - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - - - - - View icon themes folder. - + + + + - + &Icons - - - .. - - - true + + iconsComboBox - - - - - - &Widgets - - - widgetStyleComboBox - - - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + + + + View icon themes folder. + + + + + + + + + true + + + + - - - - View widget themes folder. - + + - + &Widgets - - - .. - - - true + + widgetStyleComboBox - - - - - - The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. - - - C&at - - - backgroundCatComboBox - - - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + + + + View widget themes folder. + + + + + + + + + true + + + + + + + The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. + + C&at + + + backgroundCatComboBox + + + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. + + + + + + + View cat packs folder. + + + + + + + + + true + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Refresh all + - - - View cat packs folder. + + + Qt::Horizontal - - + + + 40 + 20 + - - - .. - - - true - - + diff --git a/launcher/updater/PrismExternalUpdater.cpp b/launcher/updater/PrismExternalUpdater.cpp index bee72e3a0..dc1aae872 100644 --- a/launcher/updater/PrismExternalUpdater.cpp +++ b/launcher/updater/PrismExternalUpdater.cpp @@ -257,7 +257,7 @@ void PrismExternalUpdater::setBetaAllowed(bool allowed) void PrismExternalUpdater::resetAutoCheckTimer() { - if (priv->autoCheck) { + if (priv->autoCheck && priv->updateInterval > 0) { int timeoutDuration = 0; auto now = QDateTime::currentDateTime(); if (priv->lastCheck.isValid()) { diff --git a/launcher/updater/prismupdater/PrismUpdater.cpp b/launcher/updater/prismupdater/PrismUpdater.cpp index 5fe22bdd0..83d8efa22 100644 --- a/launcher/updater/prismupdater/PrismUpdater.cpp +++ b/launcher/updater/prismupdater/PrismUpdater.cpp @@ -352,15 +352,10 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar FS::ensureFolderPathExists(FS::PathCombine(m_dataPath, "logs")); static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "Updater" + (m_checkOnly ? "-CheckOnly" : "") + "-%0.log"; static const QString logBase = FS::PathCombine(m_dataPath, "logs", baseLogFile); - auto moveFile = [](const QString& oldName, const QString& newName) { - QFile::remove(newName); - QFile::copy(oldName, newName); - QFile::remove(oldName); - }; if (FS::ensureFolderPathExists("logs")) { // enough history to track both launches of the updater during a portable install - moveFile(logBase.arg(1), logBase.arg(2)); - moveFile(logBase.arg(0), logBase.arg(1)); + FS::move(logBase.arg(1), logBase.arg(2)); + FS::move(logBase.arg(0), logBase.arg(1)); } logFile = std::unique_ptr(new QFile(logBase.arg(0))); @@ -474,8 +469,7 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar target_dir = QDir(m_rootPath).absoluteFilePath(".."); } - QMetaObject::invokeMethod( - this, [this, target_dir]() { moveAndFinishUpdate(target_dir); }, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, [this, target_dir]() { moveAndFinishUpdate(target_dir); }, Qt::QueuedConnection); } else { QMetaObject::invokeMethod(this, &PrismUpdaterApp::loadReleaseList, Qt::QueuedConnection); @@ -924,7 +918,7 @@ bool PrismUpdaterApp::callAppImageUpdate() void PrismUpdaterApp::clearUpdateLog() { - QFile::remove(m_updateLogPath); + FS::deletePath(m_updateLogPath); } void PrismUpdaterApp::logUpdate(const QString& msg) @@ -1118,7 +1112,6 @@ void PrismUpdaterApp::backupAppDir() "Qt*.dll", }); } - file_list.append("portable.txt"); logUpdate("manifest.txt empty or missing. making best guess at files to back up."); } logUpdate(tr("Backing up:\n %1").arg(file_list.join(",\n "))); diff --git a/launcher/updater/prismupdater/UpdaterDialogs.cpp b/launcher/updater/prismupdater/UpdaterDialogs.cpp index 395b658db..06dc161b1 100644 --- a/launcher/updater/prismupdater/UpdaterDialogs.cpp +++ b/launcher/updater/prismupdater/UpdaterDialogs.cpp @@ -26,6 +26,7 @@ #include #include "Markdown.h" +#include "StringUtils.h" SelectReleaseDialog::SelectReleaseDialog(const Version& current_version, const QList& releases, QWidget* parent) : QDialog(parent), m_releases(releases), m_currentVersion(current_version), ui(new Ui::SelectReleaseDialog) @@ -96,7 +97,7 @@ void SelectReleaseDialog::selectionChanged(QTreeWidgetItem* current, QTreeWidget QString body = markdownToHTML(release.body.toUtf8()); m_selectedRelease = release; - ui->changelogTextBrowser->setHtml(body); + ui->changelogTextBrowser->setHtml(StringUtils::htmlListPatch(body)); } SelectReleaseAssetDialog::SelectReleaseAssetDialog(const QList& assets, QWidget* parent) diff --git a/libraries/murmur2/src/MurmurHash2.cpp b/libraries/murmur2/src/MurmurHash2.cpp index e73127953..99befd107 100644 --- a/libraries/murmur2/src/MurmurHash2.cpp +++ b/libraries/murmur2/src/MurmurHash2.cpp @@ -8,14 +8,14 @@ #include "MurmurHash2.h" -//----------------------------------------------------------------------------- +namespace Murmur2 { // 'm' and 'r' are mixing constants generated offline. // They're not really 'magic', they just happen to work well. const uint32_t m = 0x5bd1e995; const int r = 24; -uint32_t MurmurHash2(std::ifstream&& file_stream, std::size_t buffer_size, std::function filter_out) +uint32_t hash(Reader* file_stream, std::size_t buffer_size, std::function filter_out) { auto* buffer = new char[buffer_size]; char data[4]; @@ -26,24 +26,21 @@ uint32_t MurmurHash2(std::ifstream&& file_stream, std::size_t buffer_size, std:: // We need the size without the filtered out characters before actually calculating the hash, // to setup the initial value for the hash. do { - file_stream.read(buffer, buffer_size); - read = file_stream.gcount(); + read = file_stream->read(buffer, buffer_size); for (int i = 0; i < read; i++) { if (!filter_out(buffer[i])) size += 1; } - } while (!file_stream.eof()); + } while (!file_stream->eof()); - file_stream.clear(); - file_stream.seekg(0, file_stream.beg); + file_stream->goToBeginning(); int index = 0; // This forces a seed of 1. IncrementalHashInfo info{ (uint32_t)1 ^ size, (uint32_t)size }; do { - file_stream.read(buffer, buffer_size); - read = file_stream.gcount(); + read = file_stream->read(buffer, buffer_size); for (int i = 0; i < read; i++) { char c = buffer[i]; @@ -57,14 +54,13 @@ uint32_t MurmurHash2(std::ifstream&& file_stream, std::size_t buffer_size, std:: if (index == 0) FourBytes_MurmurHash2(reinterpret_cast(&data), info); } - } while (!file_stream.eof()); + } while (!file_stream->eof()); // Do one last bit shuffle in the hash FourBytes_MurmurHash2(reinterpret_cast(&data), info); delete[] buffer; - file_stream.close(); return info.h; } @@ -109,4 +105,4 @@ void FourBytes_MurmurHash2(const unsigned char* data, IncrementalHashInfo& prev) } } -//----------------------------------------------------------------------------- +} // namespace Murmur2 \ No newline at end of file diff --git a/libraries/murmur2/src/MurmurHash2.h b/libraries/murmur2/src/MurmurHash2.h index 5d4f48713..e6c196fd1 100644 --- a/libraries/murmur2/src/MurmurHash2.h +++ b/libraries/murmur2/src/MurmurHash2.h @@ -9,19 +9,22 @@ #pragma once #include -#include - #include -//----------------------------------------------------------------------------- +namespace Murmur2 { #define KiB 1024 #define MiB 1024 * KiB -uint32_t MurmurHash2( - std::ifstream&& file_stream, - std::size_t buffer_size = 4 * MiB, - std::function filter_out = [](char) { return false; }); +class Reader { + public: + virtual ~Reader() = default; + virtual int read(char* s, int n) = 0; + virtual bool eof() = 0; + virtual void goToBeginning() = 0; +}; + +uint32_t hash(Reader* file_stream, std::size_t buffer_size = 4 * MiB, std::function filter_out = [](char) { return false; }); struct IncrementalHashInfo { uint32_t h; @@ -29,5 +32,4 @@ struct IncrementalHashInfo { }; void FourBytes_MurmurHash2(const unsigned char* data, IncrementalHashInfo& prev); - -//----------------------------------------------------------------------------- +} // namespace Murmur2 diff --git a/nix/dev.nix b/nix/dev.nix index c476ed10f..cf61449a7 100644 --- a/nix/dev.nix +++ b/nix/dev.nix @@ -19,7 +19,7 @@ }; }; - tools.clang-tools = lib.mkForce pkgs.clang-tools_16; + tools.clang-tools = lib.mkForce pkgs.clang-tools_18; }; devShells.default = pkgs.mkShell { diff --git a/nix/distribution.nix b/nix/distribution.nix index 01c90f783..d9c6784a1 100644 --- a/nix/distribution.nix +++ b/nix/distribution.nix @@ -13,8 +13,6 @@ in { inherit (ourPackages) - prismlauncher-qt5-unwrapped - prismlauncher-qt5 prismlauncher-unwrapped prismlauncher ; @@ -25,21 +23,12 @@ flake = { overlays.default = final: prev: let version = builtins.substring 0 8 self.lastModifiedDate or "dirty"; - - # common args for prismlauncher evaluations - unwrappedArgs = { + in { + prismlauncher-unwrapped = prev.qt6Packages.callPackage ./pkg { inherit (inputs) libnbtplusplus; inherit ((final.darwin or prev.darwin).apple_sdk.frameworks) Cocoa; inherit version; }; - in { - prismlauncher-qt5-unwrapped = prev.libsForQt5.callPackage ./pkg unwrappedArgs; - - prismlauncher-qt5 = prev.libsForQt5.callPackage ./pkg/wrapper.nix { - prismlauncher-unwrapped = final.prismlauncher-qt5-unwrapped; - }; - - prismlauncher-unwrapped = prev.qt6Packages.callPackage ./pkg unwrappedArgs; prismlauncher = prev.qt6Packages.callPackage ./pkg/wrapper.nix { inherit (final) prismlauncher-unwrapped; diff --git a/nix/pkg/wrapper.nix b/nix/pkg/wrapper.nix index 1bcff1f9b..8c9c1cabc 100644 --- a/nix/pkg/wrapper.nix +++ b/nix/pkg/wrapper.nix @@ -18,7 +18,7 @@ jdk21, gamemode, flite, - mesa-demos, + glxinfo, udev, libusb1, msaClientID ? null, @@ -81,7 +81,7 @@ in runtimePrograms = [ xorg.xrandr - mesa-demos # need glxinfo + glxinfo ] ++ additionalPrograms; in diff --git a/program_info/org.prismlauncher.PrismLauncher.desktop.in b/program_info/org.prismlauncher.PrismLauncher.desktop.in index 76f4b19c0..a2846b06e 100644 --- a/program_info/org.prismlauncher.PrismLauncher.desktop.in +++ b/program_info/org.prismlauncher.PrismLauncher.desktop.in @@ -1,7 +1,7 @@ [Desktop Entry] Version=1.0 Name=Prism Launcher -Comment=A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once. +Comment=Discover, manage, and play Minecraft instances Type=Application Terminal=false Exec=@Launcher_APP_BINARY_NAME@ %U diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 59e0e3144..2dedb47cc 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -57,5 +57,8 @@ ecm_add_test(Index_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}: ecm_add_test(Version_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME Version) +ecm_add_test(MetaComponentParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME MetaComponentParse) + ecm_add_test(CatPack_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME CatPack) diff --git a/tests/MetaComponentParse_test.cpp b/tests/MetaComponentParse_test.cpp new file mode 100644 index 000000000..9979a9fa6 --- /dev/null +++ b/tests/MetaComponentParse_test.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * 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 . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include + +#include + +class MetaComponentParseTest : public QObject { + Q_OBJECT + + void doTest(QString name) + { + QString source = QFINDTESTDATA("testdata/MetaComponentParse"); + + QString comp_rp = FS::PathCombine(source, name); + + QFile file; + file.setFileName(comp_rp); + QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text)); + QString data = file.readAll(); + file.close(); + + QJsonDocument doc = QJsonDocument::fromJson(data.toUtf8()); + QJsonObject obj = doc.object(); + + QJsonValue description_json = obj.value("description"); + QJsonValue expected_json = obj.value("expected_output"); + + QVERIFY(!description_json.isUndefined()); + QVERIFY(expected_json.isString()); + + QString expected = expected_json.toString(); + + QString processed = ResourcePackUtils::processComponent(description_json); + + QCOMPARE(processed, expected); + } + + private slots: + void test_parseComponentBasic() { doTest("component_basic.json"); } + void test_parseComponentWithFormat() { doTest("component_with_format.json"); } + void test_parseComponentWithExtra() { doTest("component_with_extra.json"); } + void test_parseComponentWithLink() { doTest("component_with_link.json"); } + void test_parseComponentWithMixed() { doTest("component_with_mixed.json"); } +}; + +QTEST_GUILESS_MAIN(MetaComponentParseTest) + +#include "MetaComponentParse_test.moc" diff --git a/tests/ResourceFolderModel_test.cpp b/tests/ResourceFolderModel_test.cpp index 57c2cbdff..350ab615e 100644 --- a/tests/ResourceFolderModel_test.cpp +++ b/tests/ResourceFolderModel_test.cpp @@ -203,7 +203,10 @@ class ResourceFolderModelTest : public QObject { QCOMPARE(model.size(), 0); - { EXEC_UPDATE_TASK(model.installResource(folder_resource), QVERIFY) } { + { + EXEC_UPDATE_TASK(model.installResource(folder_resource), QVERIFY) + } + { EXEC_UPDATE_TASK(model.installResource(file_mod), QVERIFY) } diff --git a/tests/testdata/MetaComponentParse/component_basic.json b/tests/testdata/MetaComponentParse/component_basic.json new file mode 100644 index 000000000..908cb353c --- /dev/null +++ b/tests/testdata/MetaComponentParse/component_basic.json @@ -0,0 +1,8 @@ +{ + "description": [ + { + "text": "Hello, Component!" + } + ], + "expected_output": "Hello, Component!" +} diff --git a/tests/testdata/MetaComponentParse/component_with_extra.json b/tests/testdata/MetaComponentParse/component_with_extra.json new file mode 100644 index 000000000..887becdbe --- /dev/null +++ b/tests/testdata/MetaComponentParse/component_with_extra.json @@ -0,0 +1,21 @@ +{ + "description": [ + { + "text": "Hello, ", + "color": "red", + "bold": true, + "italic": true, + "extra": [ + { + "extra": [ + "Component!" + ], + "bold": false, + "italic": false + } + ] + } + ], + "expected_output": + "Hello, Component!" +} \ No newline at end of file diff --git a/tests/testdata/MetaComponentParse/component_with_format.json b/tests/testdata/MetaComponentParse/component_with_format.json new file mode 100644 index 000000000..1078886a6 --- /dev/null +++ b/tests/testdata/MetaComponentParse/component_with_format.json @@ -0,0 +1,13 @@ +{ + "description": [ + { + "text": "Hello, Component!", + "color": "blue", + "bold": true, + "italic": true, + "underlined": true, + "strikethrough": true + } + ], + "expected_output": "Hello, Component!" +} \ No newline at end of file diff --git a/tests/testdata/MetaComponentParse/component_with_link.json b/tests/testdata/MetaComponentParse/component_with_link.json new file mode 100644 index 000000000..188c004cd --- /dev/null +++ b/tests/testdata/MetaComponentParse/component_with_link.json @@ -0,0 +1,12 @@ +{ + "description": [ + { + "text": "Hello, Component!", + "clickEvent": { + "action": "open_url", + "value": "https://google.com" + } + } + ], + "expected_output": "Hello, Component!" +} diff --git a/tests/testdata/MetaComponentParse/component_with_mixed.json b/tests/testdata/MetaComponentParse/component_with_mixed.json new file mode 100644 index 000000000..661fc1a3e --- /dev/null +++ b/tests/testdata/MetaComponentParse/component_with_mixed.json @@ -0,0 +1,45 @@ +{ + "description": [ + { + "text": "The quick ", + "color": "blue", + "italic": true + }, + { + "text": "brown fox ", + "color": "#873600", + "bold": true, + "underlined": true, + "extra": [ + { + "text": "jumped over ", + "color": "blue", + "bold": false, + "underlined": false, + "italic": true, + "strikethrough": true + } + ] + }, + { + "text": "the lazy dog's back. ", + "color": "green", + "bold": true, + "italic": true, + "underlined": true, + "strikethrough": true, + "extra": [ + { + "text": "1234567890 ", + "color": "black", + "strikethrough": false, + "extra": [ + "How vexingly quick daft zebras jump!" + ] + } + ] + } + ], + "expected_output": + "The quick brown fox jumped over the lazy dog's back. 1234567890 How vexingly quick daft zebras jump!" +}